From 5cbaf7a4affcedea2a9c479e1b3fa07bddc7ca3b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 25 May 2026 00:07:56 +0100 Subject: [PATCH] Rebuild swarm dashboard as state browser --- scripts/render-voidbot-swarm-dashboard.mjs | 533 +++++++++++++++++---- state/evidence.jsonl | 1 + 2 files changed, 440 insertions(+), 94 deletions(-) diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index bb8ed1a..b5d902f 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -89,7 +89,7 @@ async function buildSnapshot() { const heartbeat = await readJsonFile(heartbeatStatePath); const orchestrator = await readJsonFile(orchestratorPath); const pause = await readJsonFile(pausePath); - const avatarByIdentity = await readIdentityAvatars(env.REPO_DISCORD_IDENTITIES_PATH); + const identityMetadata = await readIdentityMetadata(env.REPO_DISCORD_IDENTITIES_PATH); const heartbeatMtime = await getMtime(heartbeatStatePath); const orchestratorMtime = await getMtime(orchestratorPath); const paused = pause.ok ? pause.value?.paused !== false : false; @@ -98,8 +98,17 @@ async function buildSnapshot() { const pendingMentions = Array.isArray(heartbeat.value?.pendingMentions) ? heartbeat.value.pendingMentions : []; const initiativeClock = numberOrNull(heartbeat.value?.initiativeClock); const participantSnapshots = participants.map((participant) => - projectParticipant(participant, pendingMentions, initiativeClock, avatarByIdentity), + projectParticipant(participant, pendingMentions, initiativeClock, identityMetadata), ); + const faceStates = await readFaceStates(identityMetadata); + for (const participant of participantSnapshots) { + participant.faceState = faceStates.get(participant.identityId.toLowerCase()) ?? { + readable: false, + path: participant.faceStatePath, + error: participant.faceStatePath ? "State file was not readable." : "No Face state path registered.", + tree: [], + }; + } const activeTurns = participantSnapshots.filter((participant) => participant.activeJobId); const readyNow = participantSnapshots.filter((participant) => participant.status === "active" && @@ -243,8 +252,9 @@ function findLastShuffleEvent(events) { return null; } -function projectParticipant(participant, pendingMentions, initiativeClock, avatarByIdentity) { +function projectParticipant(participant, pendingMentions, initiativeClock, identityMetadata) { const identityId = String(participant.identityId ?? "unknown"); + const metadata = identityMetadata.get(identityId.toLowerCase()) ?? {}; const nextTurnAt = numberOrNull(participant.nextTurnAt); const lastTurnAt = numberOrNull(participant.lastTurnAt); const activeTurnStartedAt = numberOrNull(participant.activeTurnStartedAt); @@ -259,9 +269,20 @@ function projectParticipant(participant, pendingMentions, initiativeClock, avata return { identityId, - displayName: String(participant.displayName ?? identityId), - repoName: String(participant.repoName ?? "unknown"), - avatarUrl: avatarByIdentity.get(identityId.toLowerCase()) ?? null, + displayName: String(participant.displayName ?? metadata.displayName ?? identityId), + repoName: String(participant.repoName ?? metadata.repoName ?? "unknown"), + repoPath: stringOrNull(metadata.repoPath), + faceStatePath: stringOrNull(metadata.faceStatePath), + description: stringOrNull(metadata.description), + avatarUrl: metadata.avatarUrl ?? null, + channelPermissions: Array.isArray(metadata.channelPermissions) + ? metadata.channelPermissions.slice(0, 12).map((entry) => ({ + label: stringOrNull(entry?.label), + topic: stringOrNull(entry?.topic), + speechThreshold: stringOrNull(entry?.speechThreshold), + speedMultiplier: numberOrNull(entry?.speedMultiplier), + })) + : [], participantKind: String(participant.participantKind ?? "repo_face"), turnKind: String(participant.turnKind ?? "repo_face_rumination"), status: String(participant.status ?? "unknown"), @@ -311,8 +332,8 @@ function projectHistoryEvent(event) { }; } -async function readIdentityAvatars(registryPathValue) { - const avatars = new Map(); +async function readIdentityMetadata(registryPathValue) { + const metadata = new Map(); const registryPath = resolveConfigPath(registryPathValue, resolve(repoRoot, ".voidbot", "private", "repo-discord-identities.json")); const registry = await readJsonFile(registryPath); const identities = Array.isArray(registry.value?.identities) @@ -322,12 +343,209 @@ async function readIdentityAvatars(registryPathValue) { : []; for (const identity of identities) { const id = typeof identity?.id === "string" ? identity.id.toLowerCase() : ""; - const avatarUrl = typeof identity?.avatarUrl === "string" ? identity.avatarUrl : ""; - if (id && avatarUrl) { - avatars.set(id, avatarUrl); + if (id) { + metadata.set(id, { + id, + displayName: stringOrNull(identity?.displayName), + repoName: stringOrNull(identity?.repoName), + repoPath: stringOrNull(identity?.repoPath), + faceStatePath: stringOrNull(identity?.faceStatePath), + description: stringOrNull(identity?.description), + avatarUrl: stringOrNull(identity?.avatarUrl), + channelPermissions: Array.isArray(identity?.channelPermissions) ? identity.channelPermissions : [], + }); } } - return avatars; + return metadata; +} + +async function readFaceStates(identityMetadata) { + const states = new Map(); + let core = null; + try { + const requireCore = createRequire(resolve(repoRoot, "packages", "core", "package.json")); + core = requireCore(resolve(repoRoot, "packages", "core", "dist", "index.js")); + } catch (error) { + for (const [id, identity] of identityMetadata.entries()) { + states.set(id, { + readable: false, + path: identity.faceStatePath ?? null, + error: `Core typed-state loader unavailable: ${error instanceof Error ? error.message : String(error)}`, + tree: [], + }); + } + return states; + } + + for (const [id, identity] of identityMetadata.entries()) { + if (!identity.faceStatePath) { + states.set(id, { + readable: false, + path: null, + error: "No Face state path registered.", + tree: [], + }); + continue; + } + try { + const typedState = await core.loadVoidSelfStateTypedDocuments({ + canonicalPath: identity.faceStatePath, + identity: { + agentId: id, + publicName: identity.displayName ?? id, + publicDescription: identity.description ?? undefined, + }, + }); + const rendered = core.buildVoidSelfStateContext + ? core.buildVoidSelfStateContext(typedState, { + sourcePath: identity.faceStatePath, + identity: { + agentId: id, + publicName: identity.displayName ?? id, + publicDescription: identity.description ?? undefined, + }, + }) + : null; + states.set(id, { + readable: true, + path: identity.faceStatePath, + summary: truncate(rendered?.summary ?? "", 1200), + counts: countFaceState(typedState), + tree: buildFaceStateTree(typedState), + }); + } catch (error) { + states.set(id, { + readable: false, + path: identity.faceStatePath, + error: error instanceof Error ? error.message : String(error), + tree: [], + }); + } + } + return states; +} + +function countFaceState(typedState) { + return { + shortTerm: typedState?.thoughtMemory?.shortTerm?.length ?? 0, + memories: typedState?.thoughtMemory?.memories?.length ?? 0, + incubation: typedState?.thoughtMemory?.incubation?.length ?? 0, + bonds: typedState?.faceAffect?.socialBonds?.length ?? 0, + pressures: typedState?.agencyPressure?.pressures?.length ?? 0, + candidates: typedState?.candidateInterventions?.interventions?.length ?? 0, + }; +} + +function buildFaceStateTree(typedState) { + return [ + node("Self Profile", "selfProfile", [ + leaf("Public Name", typedState?.selfProfile?.publicName), + leaf("Description", typedState?.selfProfile?.publicDescription), + collectionNode("Values", "selfProfile.values", typedState?.selfProfile?.values, valueNode), + objectNode("Activation", "selfProfile.activationProfile", typedState?.selfProfile?.activationProfile), + collectionNode("Private Notes", "selfProfile.privateNotes", typedState?.selfProfile?.privateNotes, valueNode), + ]), + node("Thought Memory", "thoughtMemory", [ + collectionNode("Short Term", "thoughtMemory.shortTerm", typedState?.thoughtMemory?.shortTerm, memoryNode), + collectionNode("Durable Memories", "thoughtMemory.memories", typedState?.thoughtMemory?.memories, memoryNode), + collectionNode("Incubation", "thoughtMemory.incubation", typedState?.thoughtMemory?.incubation, memoryNode), + ]), + node("Affect", "faceAffect", [ + collectionNode("Needs", "faceAffect.needs", typedState?.faceAffect?.needs, valueNode), + collectionNode("Social Bonds", "faceAffect.socialBonds", typedState?.faceAffect?.socialBonds, memoryNode), + collectionNode("Status Reads", "faceAffect.statusReads", typedState?.faceAffect?.statusReads, memoryNode), + objectNode("Mood", "faceAffect.moodDimensions", typedState?.faceAffect?.moodDimensions), + collectionNode("Social Biases", "faceAffect.socialBiases", typedState?.faceAffect?.socialBiases, valueNode), + ]), + node("Agency", "agencyPressure", [ + collectionNode("Pressures", "agencyPressure.pressures", typedState?.agencyPressure?.pressures, memoryNode), + collectionNode("Candidates", "candidateInterventions.interventions", typedState?.candidateInterventions?.interventions, memoryNode), + ]), + node("Runtime", "scheduledRuntime", [ + objectNode("Sleep Cycle", "scheduledRuntime.sleepCycle", typedState?.scheduledRuntime?.sleepCycle), + objectNode("Speaking Pressure", "scheduledRuntime.speakingPressure", typedState?.scheduledRuntime?.speakingPressure), + objectNode("Last Runs", "scheduledRuntime.lastRuns", typedState?.scheduledRuntime?.lastRuns), + ]), + ].filter(Boolean); +} + +function node(label, path, children = []) { + const compactChildren = children.filter(Boolean); + return { kind: "branch", label, path, children: compactChildren, count: compactChildren.length }; +} + +function leaf(label, value, path = label) { + if (value === null || value === undefined || value === "") return null; + return { + kind: "leaf", + label, + path, + title: label, + preview: truncate(renderValuePreview(value), 120), + detail: renderValueDetail(value), + }; +} + +function collectionNode(label, path, values, projector) { + if (!Array.isArray(values) || values.length === 0) return node(label, path, [leaf("empty", "No entries.", `${path}.empty`)]); + return node(label, path, values.slice(0, 80).map((value, index) => projector(value, `${path}.${index}`, index))); +} + +function objectNode(label, path, value) { + if (!value || typeof value !== "object") return node(label, path, [leaf("empty", "No entries.", `${path}.empty`)]); + const children = Object.entries(value).slice(0, 80).map(([key, entry]) => { + if (entry && typeof entry === "object") return objectNode(key, `${path}.${key}`, entry); + return leaf(key, entry, `${path}.${key}`); + }); + return node(label, path, children); +} + +function valueNode(value, path, index) { + if (value && typeof value === "object") { + const title = value.label ?? value.name ?? value.kind ?? value.id ?? `entry ${index + 1}`; + return leaf(String(title), value, path); + } + return leaf(`entry ${index + 1}`, value, path); +} + +function memoryNode(value, path, index) { + if (!value || typeof value !== "object") return leaf(`entry ${index + 1}`, value, path); + const target = value.target?.label ?? value.target?.id ?? value.targetKind ?? value.target ?? value.kind; + const title = value.summary ?? value.claim ?? value.question ?? value.title ?? value.id ?? value.memoryId ?? `entry ${index + 1}`; + const label = target ? `${target}: ${title}` : title; + return { + kind: "leaf", + label: truncate(String(label), 120), + path, + title: String(title), + preview: truncate(value.claim ?? value.question ?? value.tension ?? value.actionImplication ?? value.summary ?? renderValuePreview(value), 180), + detail: renderMemoryDetail(value), + }; +} + +function renderMemoryDetail(value) { + const lines = []; + for (const key of ["kind", "summary", "claim", "question", "tension", "actionImplication", "status", "intensity", "createdAt", "updatedAt"]) { + if (value?.[key] !== null && value?.[key] !== undefined && value?.[key] !== "") { + lines.push(`${key}: ${renderValuePreview(value[key])}`); + } + } + if (value?.target) lines.push(`target: ${renderValuePreview(value.target)}`); + if (Array.isArray(value?.anchorRefs) && value.anchorRefs.length > 0) { + lines.push(`anchors:\n${value.anchorRefs.map((anchor) => `- ${renderValuePreview(anchor)}`).join("\n")}`); + } + if (Array.isArray(value?.tags) && value.tags.length > 0) lines.push(`tags: ${value.tags.join(", ")}`); + return lines.join("\n\n") || renderValueDetail(value); +} + +function renderValuePreview(value) { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function renderValueDetail(value) { + return typeof value === "string" ? value : JSON.stringify(value, null, 2); } function projectPendingMention(mention) { @@ -460,7 +678,7 @@ function renderHtml(snapshot) { } h1, h2, h3, p { margin: 0; } - h1, h2, h3, .kicker, .badge, .metric strong, .turn-card strong, .agent-row strong { + h1, h2, h3, .kicker, .badge, .metric strong, .turn-card strong, .state-leaf strong { font-family: var(--display); font-weight: 100; letter-spacing: 0; @@ -516,6 +734,7 @@ function renderHtml(snapshot) { text-align: center; } .turn-card.active-turn { border-color: var(--cyan); box-shadow: 0 0 24px rgba(105, 226, 239, 0.22); } + .turn-card.selected { border-color: var(--amber); background: rgba(255, 174, 88, 0.11); } .turn-card.mention-turn { border-color: var(--amber); } .turn-card strong, .turn-card span { width: 100%; @@ -549,20 +768,17 @@ function renderHtml(snapshot) { gap: 12px; padding: 12px; overflow: hidden; - grid-template-columns: minmax(280px, 0.88fr) minmax(360px, 1.4fr) minmax(280px, 0.92fr); - grid-template-rows: auto var(--status-size) minmax(0, 1fr); + grid-template-columns: minmax(210px, 0.78fr) minmax(280px, 1.06fr) minmax(320px, 1.34fr) minmax(240px, 0.86fr); + grid-template-rows: minmax(0, 1fr); grid-template-areas: - "header header controls" - "queue detail status" - "queue detail events"; + "inspector tree memory command"; } .status-panel { - width: var(--status-size); - height: var(--status-size); - grid-area: status; - align-self: center; - justify-self: center; + width: 100%; + aspect-ratio: 1 / 1; + align-self: start; + justify-self: stretch; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 7px; @@ -658,14 +874,13 @@ function renderHtml(snapshot) { overflow: hidden; } .pane-scroll { overflow: auto; } - .header-pane { grid-area: header; align-content: center; } - .controls-pane { grid-area: controls; align-content: center; } - .queue-pane { grid-area: queue; } - .detail-pane { grid-area: detail; } - .events-pane { grid-area: events; } + .inspector-pane { grid-area: inspector; grid-template-rows: auto auto minmax(0, 1fr); } + .tree-pane { grid-area: tree; grid-template-rows: auto minmax(0, 1fr); } + .memory-pane { grid-area: memory; grid-template-rows: auto minmax(0, 1fr); } + .command-pane { grid-area: command; grid-template-rows: minmax(0, auto) minmax(0, 1fr); align-content: start; } .kicker { color: var(--green); font-size: 0.74rem; text-transform: uppercase; } - h1 { font-size: clamp(1.8rem, 5vw, 4rem); line-height: 0.95; } + h1 { font-size: clamp(1.55rem, 3.4vw, 3rem); line-height: 0.98; } h2 { font-size: 1rem; text-transform: uppercase; } .muted { color: var(--muted); } .mono { font-family: var(--mono); } @@ -682,33 +897,101 @@ function renderHtml(snapshot) { .metric strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 1.12rem; } .fact strong { display: block; overflow-wrap: anywhere; font: 0.9rem/1.15 var(--mono); } - .control-grid { display: grid; gap: 10px; } + .control-grid { display: grid; gap: 10px; align-content: start; } .control-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; } .force-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; } - .queue-list, .event-list { display: grid; gap: 8px; overflow: auto; } - .agent-row { + .inspector-hero { display: grid; - grid-template-columns: 42px minmax(0, 1fr) auto; - gap: 9px; + grid-template-columns: 64px minmax(0, 1fr); + gap: 12px; + align-items: center; + } + .inspector-hero .avatar { width: 64px; height: 64px; } + .inspector-hero h1, .inspector-hero p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .inspector-lore { + overflow: auto; + color: var(--muted); + font-size: 0.83rem; + } + .channel-list, .state-tree, .detail-body { + min-height: 0; + overflow: auto; + display: grid; + align-content: start; + gap: 8px; + } + .channel-chip { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; align-items: center; - width: 100%; - min-height: 58px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: rgba(142, 223, 176, 0.06); + font-size: 0.78rem; + } + .channel-chip strong, .channel-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + + details.state-node { + border: 1px solid rgba(142, 223, 176, 0.12); + border-radius: 8px; + background: rgba(142, 223, 176, 0.045); + overflow: hidden; + } + details.state-node[open] { border-color: rgba(105, 226, 239, 0.22); } + details.state-node > summary { + display: flex; + justify-content: space-between; + gap: 8px; + padding: 9px 10px; + cursor: pointer; + color: var(--text); + font-family: var(--display); + font-weight: 100; + } + .state-children { + display: grid; + gap: 6px; + padding: 0 8px 8px 14px; + border-top: 1px solid rgba(142, 223, 176, 0.08); + } + .state-leaf { + min-width: 0; + display: grid; + gap: 3px; + padding: 8px; + border: 1px solid rgba(142, 223, 176, 0.1); + border-radius: 8px; + background: rgba(3, 7, 13, 0.34); color: var(--text); text-align: left; } - .agent-row.selected { border-color: var(--amber); } - .agent-row strong, .agent-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .agent-row span { color: var(--muted); font: 0.72rem/1.1 var(--mono); } + .state-leaf:hover, .state-leaf.selected { border-color: var(--amber); background: rgba(255, 174, 88, 0.09); } + .state-leaf strong, .state-leaf span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .state-leaf span { color: var(--muted); font: 0.72rem/1.15 var(--mono); } - .details-head { display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 12px; align-items: center; } - .details-head .avatar { width: 58px; height: 58px; } - .facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } - .event-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 8px; border: 1px solid rgba(142, 223, 176, 0.1); border-radius: 8px; background: rgba(142, 223, 176, 0.05); color: var(--muted); font-size: 0.82rem; } + .facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } + .detail-card { + min-height: 0; + display: grid; + gap: 10px; + align-content: start; + padding: 12px; + border: 1px solid rgba(105, 226, 239, 0.16); + border-radius: 8px; + background: rgba(3, 7, 13, 0.42); + } + .detail-card pre { + min-height: 0; + margin: 0; + overflow: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: rgba(239, 252, 248, 0.88); + font: 0.84rem/1.35 var(--mono); + } .badge { display: inline-flex; @@ -753,18 +1036,13 @@ function renderHtml(snapshot) { .workspace { grid-row: 1; grid-column: 2; - grid-template-columns: minmax(0, 1fr); - grid-template-rows: auto auto var(--status-size) minmax(0, 1fr) minmax(0, 0.88fr); + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: - "header" - "controls" - "status" - "detail" - "queue"; + "inspector command" + "tree memory"; } - .status-panel { justify-self: end; } - .events-pane { display: none; } - .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .status-panel { max-height: 34vh; } .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } .turn-card { width: 72px; height: 96px; } } @@ -772,8 +1050,15 @@ function renderHtml(snapshot) { @media (max-width: 760px) { .workspace { padding: 10px; gap: 10px; } .pane { padding: 12px; } - .header-pane .muted { display: none; } - .controls-pane { align-content: start; } + .inspector-lore { display: none; } + .status-panel { max-height: none; } + } + + @media (max-width: 980px) and (orientation: landscape) { + .workspace { + grid-template-columns: minmax(180px, 0.8fr) minmax(220px, 1fr) minmax(260px, 1.16fr) minmax(210px, 0.86fr); + } + .inspector-lore { display: none; } } @@ -784,6 +1069,7 @@ function renderHtml(snapshot) { const initialSnapshot = JSON.parse(document.getElementById("initial-snapshot").textContent); const app = document.getElementById("app"); let selectedIdentity = null; + let selectedStatePath = null; const tone = (value) => String(value || "unknown").toLowerCase().replace(/_/g, "-"); const text = (value) => value === null || value === undefined || value === "" ? "missing" : String(value); @@ -809,23 +1095,24 @@ function renderHtml(snapshot) { const participants = sortedParticipants(snapshot); if (!selectedIdentity || !participants.some((agent) => agent.identityId === selectedIdentity)) { selectedIdentity = (snapshot.upcomingTurns || [])[0]?.identityId || participants[0]?.identityId || null; + selectedStatePath = null; } const selected = participants.find((agent) => agent.identityId === selectedIdentity) || participants[0] || null; + const selectedLeaf = findSelectedLeaf(selected); app.innerHTML = [ "
", renderCtb(snapshot.upcomingTurns || []), "
", - renderHeader(snapshot), - renderControls(snapshot, participants), - renderQueue(participants), - renderDetails(selected), - renderEvents(snapshot), - renderStatusPanel(snapshot, selected), + renderInspector(selected), + renderStateTree(selected), + renderStateDetail(selected, selectedLeaf), + renderCommandColumn(snapshot, participants, selected), "
", "
", ].join(""); attachControls(); attachSelection(); + attachStateSelection(); } function sortedParticipants(snapshot) { @@ -840,57 +1127,95 @@ function renderHtml(snapshot) { function renderCtb(turns) { return ""; } - function renderHeader(snapshot) { - const summary = snapshot.summary || {}; - return "

VoidBot Swarm Control

CTB Cockpit

CultMesh mirror and scheduler command surface. State comes from the heartbeat snapshot.

" + - metric("State", badge(summary.state)) + - metric("Participants", number(summary.participantCount)) + - metric("Ready", number(summary.readyNowCount)) + - metric("Mentions", number(summary.pendingMentionCount)) + - metric("Shuffle", summary.lastShuffle ? summary.lastShuffle.kind.replace(/_/g, " ") : "none") + - "
"; + function renderInspector(agent) { + if (!agent) return "

Face

No selected Face.

"; + const counts = agent.faceState?.counts || {}; + return "
" + avatarHtml(agent) + "

Selected Face

" + esc(agent.displayName) + "

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

" + + fact("Turn", minutes(agent.nextTurnInMinutes)) + + fact("Status", agent.activeJobId ? "running" : agent.status) + + fact("Memory", number((counts.shortTerm || 0) + (counts.memories || 0))) + + fact("Pressure", number(counts.pressures || 0)) + + fact("Heat", number(agent.heat)) + + fact("Load", number(agent.currentLoad)) + + "

" + esc(agent.description || "No Face description registered.") + "

" + (agent.channelPermissions || []).map((channel) => "
" + esc(channel.label || "channel") + "x" + esc(number(channel.speedMultiplier || 1)) + "" + esc(channel.topic || "no topic") + "" + esc(channel.speechThreshold || "threshold") + "
").join("") + "
"; } function renderControls(snapshot, participants) { const controls = snapshot.controls || {}; const latest = (controls.manualTurnRequests || [])[0]; - return "

Controls

" + + return "

Controls

" + "
Heartbeat cadence
x" + esc(number(controls.cadenceMultiplier || 1)) + "
" + "
Manual next turn
" + "
Latest request: " + esc(latest?.identityId || "none") + " " + esc(latest?.status || "") + "
" + - "
"; + ""; } - function renderQueue(participants) { - return "

Participants

" + participants.map((agent) => { - const klass = "agent-row" + (agent.identityId === selectedIdentity ? " selected" : ""); - return ""; - }).join("") + "
"; + function renderStateTree(agent) { + const state = agent?.faceState; + if (!agent) return "

State Graph

No Face selected.

"; + if (!state?.readable) return "

State Graph

" + esc(state?.error || "Face state unreadable.") + "

"; + return "

State Graph

" + esc(agent.displayName) + " memory tree

" + esc(state.path || "state path missing") + "

" + renderTreeNodes(state.tree || []) + "
"; } - function renderDetails(agent) { - if (!agent) return "

Selected Face

No participant state.

"; - return "
" + avatarHtml(agent) + "

Selected Face

" + esc(agent.displayName) + "

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

" + badge(agent.activeJobId ? "running" : agent.status) + " " + (agent.pendingMentionCount > 0 ? badge("warning", "mention") : "") + "
" + - fact("Next", minutes(agent.nextTurnInMinutes)) + - fact("Load", number(agent.currentLoad)) + - fact("Speed", number(agent.effectiveSpeed)) + - fact("Heat", number(agent.heat)) + - fact("Queued", number(agent.queuedCount)) + - fact("Mentions", number(agent.pendingMentionCount)) + - fact("Channels", number(agent.channelCount)) + - fact("Last queued", relative(agent.lastQueuedAt)) + - fact("Job", agent.activeJobId || "none") + - "
"; + function renderTreeNodes(nodes) { + return (nodes || []).map((node) => { + if (node.kind === "branch") { + return "
" + esc(node.label) + "" + esc(node.count || 0) + "
" + renderTreeNodes(node.children || []) + "
"; + } + const klass = "state-leaf" + (node.path === selectedStatePath ? " selected" : ""); + return ""; + }).join(""); } - function renderEvents(snapshot) { - const events = (snapshot.recentEvents || []).slice(0, 18); - return "

Mesh Events

" + (events.length ? events.map((event) => "
" + esc(event.type) + " " + esc(event.identityId || "") + "" + esc(relative(event.observedAt)) + "
").join("") : "

No recent events.

") + "
"; + function renderStateDetail(agent, leaf) { + if (!agent) return "

State Detail

No selected Face.

"; + if (!agent.faceState?.readable) return "

State Detail

" + esc(agent.faceState?.error || "Face state unreadable.") + "

"; + const selected = leaf || firstLeaf(agent.faceState.tree || []); + if (!selected) return "

State Detail

No entries in this state file.

"; + selectedStatePath = selected.path; + return "

State Detail

" + esc(selected.title || selected.label) + "

" + esc(selected.path) + "

" + esc(selected.detail || selected.preview || "") + "
"; + } + + function renderCommandColumn(snapshot, participants, selected) { + return "
" + renderStatusPanel(snapshot, selected) + renderControls(snapshot, participants) + "
"; + } + + function findSelectedLeaf(agent) { + const leaves = flattenLeaves(agent?.faceState?.tree || []); + if (!leaves.length) return null; + if (selectedStatePath) { + const existing = leaves.find((leaf) => leaf.path === selectedStatePath); + if (existing) return existing; + } + const memory = leaves.find((leaf) => + leaf.label !== "empty" && ( + leaf.path.startsWith("thoughtMemory.shortTerm") || + leaf.path.startsWith("thoughtMemory.memories") || + leaf.path.startsWith("thoughtMemory.incubation") + ) + ); + if (memory) return memory; + return leaves[0]; + } + + function firstLeaf(nodes) { + return flattenLeaves(nodes)[0] || null; + } + + function flattenLeaves(nodes) { + const output = []; + const visit = (node) => { + if (!node) return; + if (node.kind === "leaf") output.push(node); + for (const child of node.children || []) visit(child); + }; + for (const node of nodes || []) visit(node); + return output; } function renderStatusPanel(snapshot, agent) { @@ -987,6 +1312,16 @@ function renderHtml(snapshot) { document.querySelectorAll("[data-select]").forEach((element) => { element.addEventListener("click", () => { selectedIdentity = element.getAttribute("data-select"); + selectedStatePath = null; + refresh(); + }); + }); + } + + function attachStateSelection() { + document.querySelectorAll("[data-state-path]").forEach((element) => { + element.addEventListener("click", () => { + selectedStatePath = element.getAttribute("data-state-path"); refresh(); }); }); @@ -1074,6 +1409,16 @@ function redactSnapshot(snapshot) { activeJobId: participant.activeJobId ? "active" : null, constraints: [], constraintCount: participant.constraintCount, + description: participant.description ? truncate(participant.description, 220) : null, + faceStatePath: null, + faceState: participant.faceState + ? { + readable: participant.faceState.readable, + counts: participant.faceState.counts, + path: null, + tree: [], + } + : undefined, })); redacted.activeTurns = redacted.activeTurns.map((participant) => ({ ...participant, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index ae5cd39..715f300 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -332,3 +332,4 @@ {"ts":"2026-05-24T22:47:00Z","type":"implementation","status":"ok","note":"Fixed the missing HUD by moving the status panel into the cockpit workspace grid instead of leaving it as a shell sibling; browser verification places the square watchdog HUD in the visible right column between Controls and Mesh Events."} {"ts":"2026-05-24T22:48:35Z","type":"implementation","status":"ok","note":"Repo Face heartbeat parallelism increased: live REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK set to 3, and tracked default/example now allow three concurrent independent Face turns per CTB tick while active-turn freeze still prevents duplicate turns per Face."} {"ts":"2026-05-24T23:05:48Z","type":"implementation","status":"ok","note":"Operator absence now drives Face sleep projection: when idle cooling reaches nap mode, repo Face sleep cycles are forced/extended into napping with operator-absence-dreaming and memory-distillation themes before speaking pressure, CTB eligibility, and nap memory maintenance are computed."} +{"ts":"2026-05-24T23:06:38Z","type":"implementation","status":"ok","note":"Rebuilt the local swarm dashboard into a CTB-owned four-column SPA: Face inspector, typed Face-state tree, selected memory/detail viewer, and command/watchdog column. The page now reads registered Face .cc state through the typed loader for display-only memory browsing while cadence/manual-turn controls remain the only writers."}