VoidBot/scripts/voidbot-swarm-operator-view.cjs
2026-07-15 02:39:01 +01:00

221 lines
9.6 KiB
JavaScript

"use strict";
const fs = require("fs");
const pathModule = require("path");
const SCHEMA_ID = "voidbot.swarm_operator_view.v1";
const RECORD_KEY = "voidbot-swarm";
function projectSwarmOperatorView({ heartbeat, orchestrator, pause, control, observedAt = new Date().toISOString(), sources = {}, route = {} }) {
const initiativeClock = number(heartbeat?.initiativeClock);
const participants = array(heartbeat?.participants).map((participant) => {
const identityId = text(participant?.identityId) || "unknown";
const pendingMentionCount = array(heartbeat?.pendingMentions)
.filter((mention) => text(mention?.identityId) === identityId).length;
return {
identityId,
displayName: text(participant?.displayName) || identityId,
repoName: text(participant?.repoName) || "unknown",
status: text(participant?.status) || "unknown",
activeJobId: text(participant?.activeJobId),
nextTurnInMinutes: number(participant?.nextTurnInMinutes) ?? subtract(number(participant?.nextTurnAt), initiativeClock),
heat: number(participant?.heat),
avatarUri: text(participant?.avatarUri) || text(participant?.avatarUrl),
memoryCount: number(participant?.memoryCount) ?? array(participant?.memories).length,
pressureCount: array(participant?.responsePressureEvidence).length,
description: text(participant?.description),
statePath: text(participant?.statePath) || text(participant?.faceStatePath),
statePreview: participant?.statePreview ?? null,
pendingMentionCount,
};
});
const organs = Object.entries(object(orchestrator?.organs)).map(([id, organ]) => ({
id,
label: text(organ?.label) || id,
status: text(organ?.lastStatus) || "unknown",
lastFinishedAt: text(organ?.lastFinishedAt),
durationSeconds: number(organ?.lastDurationSeconds),
}));
const activeTurnCount = participants.filter((participant) => participant.activeJobId).length;
const readyNowCount = participants.filter((participant) =>
participant.status === "active" && !participant.activeJobId && participant.nextTurnInMinutes !== null && participant.nextTurnInMinutes <= 0,
).length;
const paused = pause?.paused === true;
const heartbeatReadable = sources.heartbeat?.readable !== false;
const orchestratorReadable = sources.orchestrator?.readable !== false;
const nextTurn = selectNextTurn(participants);
const selected = participants.find((participant) => participant.identityId === nextTurn?.identityId) ?? participants[0] ?? null;
const surfaceOrgan = object(orchestrator?.organs)["voidbot-swarm-surface"];
const cultMeshServer = object(surfaceOrgan?.cultMeshServer);
const alerts = buildAlerts({ sources, organs });
return {
version: SCHEMA_ID,
observedAt,
summary: {
state: deriveSwarmState({ heartbeatReadable, paused, activeTurnCount, organs }),
paused,
participantCount: participants.length,
activeTurnCount,
readyNowCount,
pendingMentionCount: array(heartbeat?.pendingMentions).length,
nextIdentityId: nextTurn?.identityId ?? null,
nextDisplayName: nextTurn?.displayName ?? null,
lastTickAt: text(heartbeat?.lastTickAt),
},
route: {
storeWriteStatus: text(route.storeWriteStatus) || "ok",
providerStatus: text(route.providerStatus) || "unknown",
odinStatus: text(route.odinStatus) || text(cultMeshServer.status) || deriveOrchestratorState(organs, orchestratorReadable),
endpoint: text(route.endpoint) || text(cultMeshServer.bind),
lastAnnouncementAt: text(route.lastAnnouncementAt) || text(surfaceOrgan?.lastFinishedAt),
alerts,
},
queue: [...participants]
.sort((left, right) => nullableOrder(left.nextTurnInMinutes, right.nextTurnInMinutes))
.map((participant, slot) => ({
slot: slot + 1,
identityId: participant.identityId,
displayName: participant.displayName,
repoName: participant.repoName,
state: participant.activeJobId ? "active" : participant.status,
nextTurnInMinutes: participant.nextTurnInMinutes,
heat: participant.heat ?? number(control?.globalHeat) ?? number(heartbeat?.globalHeat),
label: participant.displayName,
status: participant.activeJobId ? "active" : participant.status,
detail: `${participant.repoName} · ${formatTurnDistance(participant.nextTurnInMinutes)}`,
badges: compact([
participant.heat === null ? null : `heat ${participant.heat}`,
participant.pendingMentionCount > 0 ? `${participant.pendingMentionCount} mention${participant.pendingMentionCount === 1 ? "" : "s"}` : null,
]),
})),
selectedFace: selected ? {
identityId: selected.identityId,
displayName: selected.displayName,
repoName: selected.repoName,
avatarUri: selected.avatarUri,
status: selected.status,
nextTurnInMinutes: selected.nextTurnInMinutes,
heat: selected.heat ?? number(control?.globalHeat) ?? number(heartbeat?.globalHeat),
memoryCount: selected.memoryCount,
pressureCount: selected.pressureCount,
description: selected.description,
statePath: selected.statePath,
statePreview: selected.statePreview,
} : null,
};
}
async function readSwarmOperatorInputs(paths) {
const entries = await Promise.all(Object.entries(paths).map(async ([key, path]) => {
try {
const [raw, stats] = await Promise.all([
fs.promises.readFile(path, "utf8"),
fs.promises.stat(path),
]);
return [key, { value: JSON.parse(raw), status: { readable: true, updatedAt: stats.mtime.toISOString() } }];
} catch (error) {
return [key, { value: null, status: { readable: false, error: error instanceof Error ? error.message : String(error) } }];
}
}));
const read = Object.fromEntries(entries);
return {
heartbeat: read.heartbeat.value,
orchestrator: read.orchestrator.value,
pause: read.pause.value,
control: read.control?.value ?? null,
sources: Object.fromEntries(Object.entries(read).map(([key, result]) => [key, result.status])),
};
}
function watchSwarmOperatorInputs(paths, onChange, { debounceMs = 75 } = {}) {
const watched = [...new Set(Object.values(paths).map(pathModule.resolve))];
const byDirectory = new Map();
for (const path of watched) {
const directory = pathModule.dirname(path);
const names = byDirectory.get(directory) ?? new Set();
names.add(pathModule.basename(path).toLowerCase());
byDirectory.set(directory, names);
}
let timer = null;
const schedule = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
Promise.resolve(onChange()).catch(() => {});
}, debounceMs);
timer.unref?.();
};
const watchers = [...byDirectory].map(([directory, names]) => fs.watch(directory, { persistent: false }, (_event, filename) => {
if (filename === null || names.has(String(filename).toLowerCase())) schedule();
}));
return () => {
if (timer) clearTimeout(timer);
for (const watcher of watchers) watcher.close();
};
}
function operatorViewPublication(value) {
return {
publicationId: "voidbot.swarm.operator-view",
documentType: "voidbot.swarm_operator_view",
schemaId: SCHEMA_ID,
recordKey: RECORD_KEY,
value,
};
}
function deriveSwarmState({ heartbeatReadable, paused, activeTurnCount, organs }) {
if (!heartbeatReadable) return "missing";
if (paused) return "paused";
if (organs.some((organ) => ["failed", "error", "stalled"].includes(organ.status))) return "warning";
if (activeTurnCount > 0) return "running";
return "ready";
}
function deriveOrchestratorState(organs, readable) {
if (!readable || organs.length === 0) return "missing";
if (organs.some((organ) => ["failed", "error", "stalled"].includes(organ.status))) return "warning";
if (organs.some((organ) => organ.status === "running")) return "running";
return "ready";
}
function selectNextTurn(participants) {
const next = [...participants]
.filter((participant) => participant.nextTurnInMinutes !== null)
.sort((left, right) => left.nextTurnInMinutes - right.nextTurnInMinutes)[0];
return next ? {
identityId: next.identityId,
displayName: next.displayName,
inMinutes: next.nextTurnInMinutes,
} : null;
}
function buildAlerts({ sources, organs }) {
const alerts = [];
for (const [source, status] of Object.entries(sources)) {
if (status?.readable === false) alerts.push({ code: `source.${source}.unreadable`, severity: "error", detail: text(status.error) || `${source} is unreadable` });
}
for (const organ of organs.filter((entry) => ["failed", "error", "stalled"].includes(entry.status))) {
alerts.push({ code: `organ.${organ.id}.${organ.status}`, severity: "warning", detail: `${organ.label} is ${organ.status}` });
}
return alerts;
}
function array(value) { return Array.isArray(value) ? value : []; }
function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
function text(value) { return typeof value === "string" && value.trim() ? value : null; }
function number(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; }
function subtract(left, right) { return left === null || right === null ? null : Math.round((left - right) * 1000) / 1000; }
function nullableOrder(left, right) { return left === null ? 1 : right === null ? -1 : left - right; }
function compact(values) { return values.filter((value) => value !== null); }
function formatTurnDistance(value) { return value === null ? "unscheduled" : value <= 0 ? "ready now" : `turn in ${value}m`; }
module.exports = {
RECORD_KEY,
SCHEMA_ID,
operatorViewPublication,
projectSwarmOperatorView,
readSwarmOperatorInputs,
watchSwarmOperatorInputs,
};