diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index c79359f..96ab072 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -108,11 +108,13 @@ async function buildSnapshot() { error: participant.faceStatePath ? "State file was not readable." : "No Face state path registered.", tree: [], }; + participant.restState = participant.faceState.restState ?? null; } const activeTurns = participantSnapshots.filter((participant) => participant.activeJobId); const readyNow = participantSnapshots.filter((participant) => participant.status === "active" && !participant.activeJobId && + participant.restState?.isNapping !== true && typeof participant.nextTurnInMinutes === "number" && participant.nextTurnInMinutes <= 0, ); @@ -218,8 +220,15 @@ function buildUpcomingTurns(participants) { avatarUrl: participant.avatarUrl, participantKind: participant.participantKind, activeJobId: participant.activeJobId, + restState: participant.restState, pendingMentionCount: participant.pendingMentionCount, - shuffleReason: participant.pendingMentionCount > 0 ? "mention" : participant.activeJobId ? "active" : null, + shuffleReason: participant.pendingMentionCount > 0 + ? "mention" + : participant.activeJobId + ? "active" + : participant.restState?.isNapping === true + ? "nap" + : null, nextTurnInMinutes: participant.nextTurnInMinutes, effectiveSpeed: participant.effectiveSpeed, heat: participant.heat, @@ -411,6 +420,7 @@ async function readFaceStates(identityMetadata) { path: identity.faceStatePath, summary: truncate(rendered?.summary ?? "", 1200), counts: countFaceState(typedState), + restState: projectRestState(typedState), tree: buildFaceStateTree(typedState), }); } catch (error) { @@ -425,6 +435,21 @@ async function readFaceStates(identityMetadata) { return states; } +function projectRestState(typedState) { + const sleepCycle = typedState?.scheduledRuntime?.sleepCycle; + if (!sleepCycle || typeof sleepCycle !== "object") { + return null; + } + return { + isNapping: sleepCycle.isNapping === true, + napEndsAt: stringOrNull(sleepCycle.currentNapEndsAt), + nextNapStartsAt: stringOrNull(sleepCycle.nextNapStartsAt), + activeDreamThemes: Array.isArray(sleepCycle.activeDreamThemes) + ? sleepCycle.activeDreamThemes.slice(0, 8).map(String) + : [], + }; +} + function countFaceState(typedState) { return { shortTerm: typedState?.thoughtMemory?.shortTerm?.length ?? 0, @@ -1242,7 +1267,11 @@ function renderHtml(snapshot) { const text = (value) => value === null || value === undefined || value === "" ? "missing" : String(value); const esc = (value) => text(value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); const number = (value) => typeof value === "number" ? value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : "missing"; - const minutes = (value) => typeof value === "number" ? (value <= 0 ? "ready" : value.toLocaleString(undefined, { maximumFractionDigits: 1 }) + "m") : "missing"; + const minutes = (value, restState = null) => restState?.isNapping === true + ? "napping" + : typeof value === "number" + ? (value <= 0 ? "ready" : value.toLocaleString(undefined, { maximumFractionDigits: 1 }) + "m") + : "missing"; const relative = (timestamp) => { if (!timestamp) return "missing"; const time = Date.parse(timestamp); @@ -1303,7 +1332,7 @@ function renderHtml(snapshot) { function renderCtb(turns) { return ""; } @@ -1312,7 +1341,7 @@ function renderHtml(snapshot) { const counts = agent.faceState?.counts || {}; const memoryCount = (counts.shortTerm || 0) + (counts.memories || 0); return "
" + renderControls(snapshot, participants) + "
" + avatarHtml(agent) + "

Selected Face

" + esc(agent.displayName) + "

" + esc(agent.identityId) + " / " + esc(agent.repoName) + "

" + - statBar("Turn", minutes(agent.nextTurnInMinutes), turnPercent(agent.nextTurnInMinutes), "cool") + + statBar("Turn", minutes(agent.nextTurnInMinutes, agent.restState), turnPercent(agent.nextTurnInMinutes), agent.restState?.isNapping === true ? "warm" : "cool") + statBar("Memory", number(memoryCount), Math.min(100, memoryCount * 7), "cool") + statBar("Pressure", number(counts.pressures || 0), Math.min(100, (counts.pressures || 0) * 14), (counts.pressures || 0) > 4 ? "hot" : "") + statBar("Heat", number(agent.heat), Math.min(100, (agent.heat || 0) * 34), (agent.heat || 0) > 1.5 ? "hot" : "") + diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index cca28f9..41789db 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -758,7 +758,24 @@ function projectSleepCycleForOperatorAbsence( now: Date, ): VoidScheduledRuntime["sleepCycle"] { if (idleCooling?.napModeActive !== true) { - return sleepCycle; + const activeDreamThemes = sleepCycle.activeDreamThemes.filter((theme) => theme !== "operator-absence-dreaming"); + if (sleepCycle.isNapping !== true || !sleepCycle.activeDreamThemes.includes("operator-absence-dreaming")) { + return activeDreamThemes.length === sleepCycle.activeDreamThemes.length + ? sleepCycle + : { + ...sleepCycle, + activeDreamThemes, + }; + } + + return { + ...sleepCycle, + isNapping: false, + currentNapStartedAt: undefined, + currentNapEndsAt: undefined, + nextNapStartsAt: new Date(now.getTime() + 180 * 60_000).toISOString(), + activeDreamThemes, + }; } const nowMs = now.getTime(); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index a9501d8..479cf53 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -344,3 +344,4 @@ {"ts":"2026-05-25T12:49:01Z","type":"implementation","status":"ok","note":"Renamed the local EpiphanyGraph repo to Norn and updated the Norn Face registry/state to use E:/Projects/Norn and repo:Norn. The Norn repo builds with dotnet and the Norn viewer typecheck/build passes."} {"ts":"2026-05-25T17:36:36Z","type":"implementation","status":"ok","note":"Revived the local VoidBot stack and repo Face swarm: npm run stack:start restored runtime-stack.json to ready with bot PID 24512 and worker PID 30640, npm run state:check passed live-service gates with only a backup warning, state/agent-swarm-paused.json is now paused=false, repo Face heartbeats and mood drift ran ok, first unpaused Void moderation turn completed ok, and Norn reconciled into the active CTB participant roster."} {"ts":"2026-05-25T18:53:32Z","type":"implementation","status":"ok","note":"Slowed repo Face CTB cadence by 10x in the live control surface: cadenceMultiplier is now 0.04, scheduler/dashboard/API minimum is 0.01, and effective-speed/recovery floors now preserve sub-0.1 cadence instead of silently flattening it. Typecheck and a real heartbeat pass succeeded without queuing an unprompted Face turn."} +{"ts":"2026-05-25T21:28:29Z","type":"implementation","status":"ok","note":"Fixed repo Face CTB wake/ready split: operator-absence naps now end when operator absence ends, and the swarm dashboard includes Face rest state so napping Faces are not shown as ready. Verified build and a forced repo-face heartbeat queued Fensalir, Norn, and Nibu."}