Fix repo face ready state after operator naps

This commit is contained in:
Meta Vens 2026-05-25 22:28:51 +01:00
commit a46ee828a6
3 changed files with 52 additions and 5 deletions

View file

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 "<nav class=\\"ctb-rail\\" aria-label=\\"Upcoming CTB turns\\">" + turns.map((turn) => {
const klass = ["turn-card", turn.identityId === selectedIdentity ? "selected" : "", turn.activeJobId ? "active-turn" : "", turn.pendingMentionCount > 0 ? "mention-turn" : ""].filter(Boolean).join(" ");
return "<button class=\\"" + klass + "\\" type=\\"button\\" data-select=\\"" + esc(turn.identityId) + "\\" title=\\"" + esc(turn.displayName + " / " + minutes(turn.nextTurnInMinutes)) + "\\">" + avatarHtml(turn) + "<strong>" + esc(turn.displayName) + "</strong><span>" + esc(minutes(turn.nextTurnInMinutes)) + "</span>" + (turn.shuffleReason ? "<span class=\\"turn-reason\\">" + esc(turn.shuffleReason) + "</span>" : "") + "</button>";
return "<button class=\\"" + klass + "\\" type=\\"button\\" data-select=\\"" + esc(turn.identityId) + "\\" title=\\"" + esc(turn.displayName + " / " + minutes(turn.nextTurnInMinutes, turn.restState)) + "\\">" + avatarHtml(turn) + "<strong>" + esc(turn.displayName) + "</strong><span>" + esc(minutes(turn.nextTurnInMinutes, turn.restState)) + "</span>" + (turn.shuffleReason ? "<span class=\\"turn-reason\\">" + esc(turn.shuffleReason) + "</span>" : "") + "</button>";
}).join("") + "</nav>";
}
@ -1312,7 +1341,7 @@ function renderHtml(snapshot) {
const counts = agent.faceState?.counts || {};
const memoryCount = (counts.shortTerm || 0) + (counts.memories || 0);
return "<section class=\\"pane inspector-pane\\">" + renderControls(snapshot, participants) + "<div class=\\"inspector-hero\\">" + avatarHtml(agent) + "<div><p class=\\"kicker\\">Selected Face</p><h1>" + esc(agent.displayName) + "</h1><p class=\\"muted mono\\">" + esc(agent.identityId) + " / " + esc(agent.repoName) + "</p></div></div><div class=\\"agent-stats\\">" +
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" : "") +

View file

@ -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();

View file

@ -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."}