#!/usr/bin/env node import { createServer } from "node:http"; import { createRequire } from "node:module"; import { networkInterfaces } from "node:os"; import { dirname, extname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { existsSync } from "node:fs"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const defaultStorageRoot = resolve(repoRoot, ".voidbot"); const defaultStatusDir = resolve(defaultStorageRoot, "status"); const defaultSnapshotPath = resolve(defaultStatusDir, "swarm-state.json"); const defaultDashboardPath = resolve(defaultStatusDir, "swarm-dashboard.html"); const defaultCultMeshStorePath = resolve(defaultStatusDir, "cultmesh", "voidbot-swarm-state.cc"); const snapshotDocumentType = "voidbot.swarm_state_snapshot"; const snapshotSchemaId = "voidbot.swarm_state_snapshot.v1"; const args = parseArgs(process.argv.slice(2)); const env = await readDotEnv(resolve(repoRoot, ".env")); const storageRoot = resolveConfigPath(env.STORAGE_ROOT, defaultStorageRoot); const statusDir = resolve(storageRoot, "status"); const heartbeatStatePath = resolveConfigPath( env.REPO_FACE_HEARTBEAT_STATE_PATH, resolve(statusDir, "repo-face-heartbeats.json"), ); const orchestratorPath = resolve(statusDir, "gamecult-orchestrator.json"); const pausePath = resolve(repoRoot, "state", "agent-swarm-paused.json"); const snapshotPath = resolveConfigPath(args.snapshot, defaultSnapshotPath.replace(defaultStatusDir, statusDir)); const dashboardPath = resolveConfigPath(args.out, defaultDashboardPath.replace(defaultStatusDir, statusDir)); const cultMeshStorePath = resolveConfigPath( args.cultmeshStore, defaultCultMeshStorePath.replace(defaultStatusDir, statusDir), ); const renderResult = await render(); if (args.open) { console.log(pathToFileURL(renderResult.dashboardPath).href); } if (args.serve) { await serveDashboard({ host: args.host ?? "0.0.0.0", port: Number.parseInt(args.port ?? "8787", 10), rootDir: dirname(renderResult.dashboardPath), refreshSeconds: Number.parseInt(args.refreshSeconds ?? "10", 10), }); } else { console.log(`Swarm dashboard HTML: ${renderResult.dashboardPath}`); console.log(`Swarm snapshot JSON: ${renderResult.snapshotPath}`); console.log(`CultMesh snapshot store: ${renderResult.cultMeshStorePath}`); } async function render() { let snapshot = await buildSnapshot(); if (args.public) { snapshot = redactSnapshot(snapshot); } await mkdir(dirname(snapshotPath), { recursive: true }); await writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); await mkdir(dirname(dashboardPath), { recursive: true }); await writeFile(dashboardPath, renderHtml(snapshot), "utf8"); const cultMeshWrite = await writeCultMeshSnapshot(snapshot, cultMeshStorePath); const finalSnapshot = { ...snapshot, cultMesh: { ...snapshot.cultMesh, ...cultMeshWrite, }, }; await writeFile(snapshotPath, `${JSON.stringify(finalSnapshot, null, 2)}\n`, "utf8"); await writeFile(dashboardPath, renderHtml(finalSnapshot), "utf8"); return { dashboardPath, snapshotPath, cultMeshStorePath, }; } async function buildSnapshot() { const now = new Date(); const heartbeat = await readJsonFile(heartbeatStatePath); const orchestrator = await readJsonFile(orchestratorPath); const pause = await readJsonFile(pausePath); const heartbeatMtime = await getMtime(heartbeatStatePath); const orchestratorMtime = await getMtime(orchestratorPath); const paused = pause.ok ? pause.value?.paused !== false : false; const pauseReason = pause.ok && typeof pause.value?.reason === "string" ? pause.value.reason : undefined; const participants = Array.isArray(heartbeat.value?.participants) ? heartbeat.value.participants : []; 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), ); const activeTurns = participantSnapshots.filter((participant) => participant.activeJobId); const readyNow = participantSnapshots.filter((participant) => participant.status === "active" && !participant.activeJobId && typeof participant.nextTurnInMinutes === "number" && participant.nextTurnInMinutes <= 0, ); const nextParticipant = [...participantSnapshots] .filter((participant) => typeof participant.nextTurnInMinutes === "number") .sort((left, right) => left.nextTurnInMinutes - right.nextTurnInMinutes)[0]; const organs = projectOrgans(orchestrator.value); const recentEvents = Array.isArray(heartbeat.value?.history) ? heartbeat.value.history.slice(-24).reverse().map(projectHistoryEvent) : []; return { schemaVersion: snapshotSchemaId, generatedAt: now.toISOString(), title: "VoidBot Swarm State", sources: { heartbeatStatePath, heartbeatStateUpdatedAt: heartbeatMtime, heartbeatStateReadable: heartbeat.ok, heartbeatStateError: heartbeat.ok ? undefined : heartbeat.error, orchestratorPath, orchestratorUpdatedAt: orchestratorMtime, orchestratorReadable: orchestrator.ok, orchestratorError: orchestrator.ok ? undefined : orchestrator.error, pausePath, pauseReadable: pause.ok, pauseError: pause.ok ? undefined : pause.error, }, cultMesh: { documentType: snapshotDocumentType, schemaId: snapshotSchemaId, documentKey: "voidbot-swarm", storePath: cultMeshStorePath, writeStatus: "pending", note: "CultMesh should distribute this typed CultCache snapshot; the web page remains read-only.", }, summary: { state: deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }), paused, pauseReason, participantCount: participantSnapshots.length, activeTurnCount: activeTurns.length, pendingMentionCount: pendingMentions.length, readyNowCount: readyNow.length, nextIdentityId: nextParticipant?.identityId ?? null, nextDisplayName: nextParticipant?.displayName ?? null, nextTurnInMinutes: nextParticipant?.nextTurnInMinutes ?? null, initiativeClock, baseRecoveryMinutes: numberOrNull(heartbeat.value?.baseRecoveryMinutes), globalHeat: numberOrNull(heartbeat.value?.globalHeat), lastTickAt: stringOrNull(heartbeat.value?.lastTickAt), }, participants: participantSnapshots, activeTurns, pendingMentions: pendingMentions.map(projectPendingMention), orchestrator: { state: deriveOrchestratorState(organs), organs, }, recentEvents, }; } function projectParticipant(participant, pendingMentions, initiativeClock) { const identityId = String(participant.identityId ?? "unknown"); const nextTurnAt = numberOrNull(participant.nextTurnAt); const lastTurnAt = numberOrNull(participant.lastTurnAt); const activeTurnStartedAt = numberOrNull(participant.activeTurnStartedAt); const channelCount = Array.isArray(participant.groups) ? participant.groups.filter((entry) => typeof entry === "string" && entry.startsWith("channel:")).length : 0; const mentionCount = pendingMentions.filter((mention) => mention?.identityId === identityId).length; const nextTurnInMinutes = typeof nextTurnAt === "number" && typeof initiativeClock === "number" ? round(nextTurnAt - initiativeClock, 3) : null; return { identityId, displayName: String(participant.displayName ?? identityId), repoName: String(participant.repoName ?? "unknown"), participantKind: String(participant.participantKind ?? "repo_face"), turnKind: String(participant.turnKind ?? "repo_face_rumination"), status: String(participant.status ?? "unknown"), currentLoad: numberOrNull(participant.currentLoad), heat: numberOrNull(participant.heat), effectiveSpeed: numberOrNull(participant.effectiveSpeed), initiativeSpeed: numberOrNull(participant.initiativeSpeed), nextTurnAt, nextTurnInMinutes, lastTurnAt, activeTurnStartedAt, activeJobId: stringOrNull(participant.activeJobId), lastQueuedAt: stringOrNull(participant.lastQueuedAt), queuedCount: numberOrNull(participant.queuedCount), pendingMentionCount: mentionCount, channelCount, constraintCount: Array.isArray(participant.constraints) ? participant.constraints.length : 0, constraints: Array.isArray(participant.constraints) ? participant.constraints.slice(0, 4).map(String) : [], }; } function projectOrgans(orchestrator) { const organs = orchestrator?.organs && typeof orchestrator.organs === "object" ? orchestrator.organs : {}; return Object.entries(organs).map(([id, organ]) => ({ id, label: String(organ?.label ?? id), intervalMinutes: numberOrNull(organ?.intervalMinutes), lastStartedAt: stringOrNull(organ?.lastStartedAt), lastFinishedAt: stringOrNull(organ?.lastFinishedAt), lastExitCode: numberOrNull(organ?.lastExitCode), lastStatus: String(organ?.lastStatus ?? "unknown"), lastLogPath: stringOrNull(organ?.lastLogPath), })); } function projectHistoryEvent(event) { return { type: String(event?.type ?? "event"), identityId: stringOrNull(event?.identityId), observedAt: stringOrNull(event?.observedAt ?? event?.queuedAt), activeJobId: stringOrNull(event?.activeJobId), statusPath: stringOrNull(event?.statusPath), reason: stringOrNull(event?.reason), nextTurnAt: numberOrNull(event?.nextTurnAt), recoveryMinutes: numberOrNull(event?.recoveryMinutes), }; } function projectPendingMention(mention) { return { identityId: stringOrNull(mention?.identityId), sourceChannelId: stringOrNull(mention?.sourceChannelId), sourceMessageId: stringOrNull(mention?.sourceMessageId), createdAt: stringOrNull(mention?.createdAt), prompt: truncate(String(mention?.prompt ?? mention?.content ?? ""), 220), }; } function deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }) { if (!heartbeat.ok) return "missing"; if (paused) return "paused"; const organ = projectOrgans(orchestrator.value).find((entry) => entry.id === "repo-face-heartbeats"); if (organ && organ.lastStatus && !["ok", "skipped_disabled"].includes(organ.lastStatus)) { return "warning"; } if (activeTurns.length > 0) return "running"; return "ready"; } function deriveOrchestratorState(organs) { if (organs.length === 0) return "missing"; if (organs.some((organ) => ["failed", "error", "stalled"].includes(organ.lastStatus))) return "warning"; if (organs.some((organ) => organ.lastStatus === "running")) return "running"; return "ok"; } async function writeCultMeshSnapshot(snapshot, storePath) { try { const cultCacheRoot = resolve(repoRoot, "..", "CultCacheTS"); const cultCachePackage = resolve(cultCacheRoot, "package.json"); if (!existsSync(cultCachePackage)) { return { writeStatus: "skipped", writeError: `CultCacheTS package was not found at ${cultCachePackage}.`, }; } const requireCultCache = createRequire(cultCachePackage); const { CultCache, SingleFileMessagePackBackingStore, defineDocumentType } = requireCultCache( resolve(cultCacheRoot, "dist", "index.js"), ); const snapshotDefinition = defineDocumentType({ type: snapshotDocumentType, schemaName: snapshotDocumentType, schemaId: snapshotSchemaId, schemaVersion: snapshotSchemaId, contentHash: snapshotSchemaId, canonicalSchemaJson: JSON.stringify({ schemaName: snapshotDocumentType, schemaVersion: snapshotSchemaId, description: "Redacted read-only VoidBot swarm state snapshot for CultMesh distribution.", }), global: true, schema: { parse(input) { if (!input || typeof input !== "object") { throw new Error("VoidBot swarm snapshot must be an object."); } return input; }, }, }); const store = new SingleFileMessagePackBackingStore(storePath); const cache = CultCache.builder() .withDocumentType(snapshotDefinition) .withGenericStore(store) .build(); await cache.pullAllBackingStores(); await cache.put(snapshotDefinition, "voidbot-swarm", snapshot); return { writeStatus: "ok", storePath, writtenAt: new Date().toISOString(), }; } catch (error) { return { writeStatus: "failed", storePath, writeError: error instanceof Error ? error.message : String(error), }; } } function renderHtml(snapshot) { return `