UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

474 lines (441 loc) • 17.4 kB
import { existsSync, readdirSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; import { DatabaseSync, openReadOnlyDatabase } from "./sqlite.ts"; import type { AgentIdentity, AgentRecord, MessageRecord } from "./protocol.ts"; import type { WorkflowRun } from "./workflow.ts"; import { readRoutingRecords } from "./telemetry.ts"; import type { RoutingRecord, RoutingRecordV2 } from "./routing.ts"; export interface MeshTuiPidClaim { file: string; role?: string; pid?: number; live: boolean; } export interface MeshTuiRunStage { id: string; label?: string; status: string; attempts?: number; } export interface MeshTuiRun { id: string; status: string; definitionId: string; project: string; currentStage?: string; targetAgentName?: string; updatedAt?: string; progress?: { done: number; total: number }; stages?: MeshTuiRunStage[]; } export interface MeshTuiPlan { id: string; runId: string; summary: string; createdAt: string; stageId?: string; severity?: string; } export type MeshTuiOpenMessage = Pick<MessageRecord, "id" | "status" | "fromName" | "toName" | "delivery" | "createdAt" | "correlationId">; export interface LocalMeshSnapshot { source?: "legacy" | "runtime" | "both" | undefined; agents: AgentRecord[]; openMessages: MeshTuiOpenMessage[]; openMessageTotal: number; runs: MeshTuiRun[]; runTotal: number; plans: MeshTuiPlan[]; pids: MeshTuiPidClaim[]; spend?: Array<{ recordedAt: string; routing: RoutingRecord | RoutingRecordV2 }> | undefined; } /** Limits a snapshot to one project and one recipient. * * `hubProject` is the hub project key stored on legacy messages and runs. * `runtimeProjectId` is the `.kxm/project.yaml` id that keys Runtime runs; * without it the snapshot reads no Runtime runs. `recipientName` selects the * open messages addressed to this agent; without it no messages are read. A * scoped snapshot never reads journal plans. */ export interface LocalMeshSnapshotScope { hubProject: string; runtimeProjectId?: string | undefined; recipientName?: string | undefined; } export interface LocalMeshSnapshotOptions { env?: NodeJS.ProcessEnv; projectRoot?: string; kxmStateRoot?: string; /** SQLite busy timeout for every database this snapshot opens. Default 5000. */ busyTimeoutMs?: number; scope?: LocalMeshSnapshotScope; } const DEFAULT_BUSY_TIMEOUT_MS = 5000; const RUNTIME_PROJECT_KEY = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/; function processExists(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; } } function readJsonRows<T>(database: DatabaseSync, sql: string, params: string[] = []): T[] { const rows = database.prepare(sql).all(...params) as Array<{ record: string }>; const out: T[] = []; for (const row of rows) { try { out.push(JSON.parse(row.record) as T); } catch { // skip corrupt rows } } return out; } const DONE_STAGE_STATUSES = new Set(["passed", "failed", "warning"]); export function summarizeMeshRun(run: Pick<WorkflowRun, "id" | "status" | "definitionId" | "project" | "currentStage" | "targetAgentName" | "updatedAt" | "stages">): MeshTuiRun { const stages = (run.stages ?? []).map((stage) => ({ id: stage.id, ...(stage.label ? { label: stage.label } : {}), status: stage.status, ...(stage.attempts ? { attempts: stage.attempts } : {}), })); const total = stages.length; const done = stages.filter((stage) => DONE_STAGE_STATUSES.has(stage.status)).length; return { id: run.id, status: run.status, definitionId: run.definitionId, project: run.project, ...(run.currentStage ? { currentStage: run.currentStage } : {}), ...(run.targetAgentName ? { targetAgentName: run.targetAgentName } : {}), ...(run.updatedAt ? { updatedAt: run.updatedAt } : {}), ...(total > 0 ? { progress: { done, total }, stages } : {}), }; } function summarizeMeshPlan(entry: { id: string; runId: string; category?: string; summary: string; createdAt: string; stageId?: string; severity?: string }): MeshTuiPlan | undefined { if (entry.category && entry.category !== "plan") return undefined; const summary = entry.summary.replace(/\s+/g, " ").trim().slice(0, 120); if (!summary) return undefined; return { id: entry.id, runId: entry.runId, summary, createdAt: entry.createdAt, ...(entry.stageId ? { stageId: entry.stageId } : {}), ...(entry.severity ? { severity: entry.severity } : {}), }; } function readPlanMetadata(database: DatabaseSync): MeshTuiPlan[] { try { const rows = database.prepare(` SELECT record FROM workflow_journal WHERE category = 'plan' ORDER BY rowid DESC LIMIT 16 `).all() as Array<{ record: string }>; const plans: MeshTuiPlan[] = []; for (const row of rows) { try { const parsed = JSON.parse(row.record) as { id?: string; runId?: string; category?: string; summary?: string; createdAt?: string; stageId?: string; severity?: string; details?: string; }; if (typeof parsed.id !== "string" || typeof parsed.runId !== "string" || typeof parsed.summary !== "string" || typeof parsed.createdAt !== "string") continue; const plan = summarizeMeshPlan({ id: parsed.id, runId: parsed.runId, summary: parsed.summary, createdAt: parsed.createdAt, ...(parsed.category ? { category: parsed.category } : {}), ...(parsed.stageId ? { stageId: parsed.stageId } : {}), ...(parsed.severity ? { severity: parsed.severity } : {}), }); if (plan) plans.push(plan); } catch { /* skip malformed journal rows */ } } return plans; } catch { return []; } } function countRows(database: DatabaseSync, table: "messages" | "workflow_runs", where = "", params: string[] = []): number { const row = database.prepare(`SELECT COUNT(*) AS count FROM ${table}${where}`).get(...params) as { count?: number | bigint } | undefined; return Number(row?.count ?? 0); } const OPEN_MESSAGE_WHERE = " WHERE json_extract(record, '$.status') IN ('queued', 'delivered')"; const SCOPED_MESSAGE_WHERE = `${OPEN_MESSAGE_WHERE} AND json_extract(record, '$.project') = ? AND COALESCE(json_extract(record, '$.toName'), json_extract(record, '$.to')) = ?`; function readOpenMessageMetadata(database: DatabaseSync, where = OPEN_MESSAGE_WHERE, params: string[] = []): MeshTuiOpenMessage[] { const rows = database.prepare(` SELECT json_extract(record, '$.id') AS id, json_extract(record, '$.status') AS status, COALESCE(json_extract(record, '$.fromName'), json_extract(record, '$.from')) AS fromName, COALESCE(json_extract(record, '$.toName'), json_extract(record, '$.to')) AS toName, json_extract(record, '$.delivery') AS delivery, json_extract(record, '$.createdAt') AS createdAt, json_extract(record, '$.correlationId') AS correlationId FROM messages${where} ORDER BY json_extract(record, '$.createdAt') DESC LIMIT 16 `).all(...params) as Array<Record<string, unknown>>; const messages: MeshTuiOpenMessage[] = []; for (const row of rows) { if ( typeof row.id !== "string" || (row.status !== "queued" && row.status !== "delivered") || typeof row.fromName !== "string" || typeof row.toName !== "string" || (row.delivery !== "steer" && row.delivery !== "followUp" && row.delivery !== "nextTurn") || typeof row.createdAt !== "string" ) continue; messages.push({ id: row.id, status: row.status, fromName: row.fromName, toName: row.toName, delivery: row.delivery, createdAt: row.createdAt, ...(typeof row.correlationId === "string" ? { correlationId: row.correlationId } : {}), }); } return messages; } export function resolveKxmSnapshotPaths(cwd: string, env: NodeJS.ProcessEnv = process.env): { dataPath: string; stateDir: string } { const stateDir = env.KXM_STATE_DIR?.trim() || join(cwd, ".kxm", "state"); const configured = env.KXM_DATA_PATH?.trim(); const dataPath = configured ? resolve(cwd, configured) : join(stateDir, "kxm.db"); return { dataPath, stateDir }; } function resolveKxmStateRoot( stateDir: string, options?: { env?: NodeJS.ProcessEnv; kxmStateRoot?: string }, ): string | undefined { if (options?.kxmStateRoot && existsSync(options.kxmStateRoot)) { return resolve(options.kxmStateRoot); } if (existsSync(join(stateDir, "runtime", "registry.db")) || existsSync(join(stateDir, "runtime", "projects"))) { return stateDir; } const env = options?.env ?? process.env; const explicit = env.KXM_STATE_HOME?.trim() || env.KXM_USER_STATE_DIR?.trim() || env.KXM_STATE_ROOT?.trim(); if (explicit && isAbsolute(explicit) && existsSync(resolve(explicit))) { return resolve(explicit); } let base: string; if (process.platform === "win32") { const localAppData = env.LOCALAPPDATA?.trim(); base = localAppData && isAbsolute(localAppData) ? localAppData : join(homedir(), "AppData", "Local"); base = resolve(base, "KXM"); } else if (process.platform === "darwin") { base = resolve(homedir(), "Library", "Application Support", "KXM"); } else { const xdgState = env.XDG_STATE_HOME?.trim(); base = xdgState && isAbsolute(xdgState) ? xdgState : join(homedir(), ".local", "state"); base = resolve(base, "kxm"); } if (existsSync(base)) return base; return undefined; } function readRuntimeRuns(eventDb: DatabaseSync, projectId: string | undefined): { runs: MeshTuiRun[]; total: number } { const where = projectId === undefined ? "" : " WHERE project_id = ?"; const params = projectId === undefined ? [] : [projectId]; const runRows = eventDb.prepare(` SELECT run_id, project_id, workflow_id, status, created_at, updated_at FROM runs${where} ORDER BY created_at DESC, run_id DESC LIMIT 8 `).all(...params) as Array<{ run_id: string; project_id: string; workflow_id: string; status: string; created_at: string; updated_at: string; }>; const countRow = eventDb.prepare(`SELECT COUNT(*) AS total FROM runs${where}`).get(...params) as { total: number } | undefined; return { total: Number(countRow?.total ?? runRows.length), runs: runRows.map((r) => ({ id: r.run_id, status: r.status, definitionId: r.workflow_id, project: r.project_id, updatedAt: r.updated_at || r.created_at, })), }; } export function loadLocalMeshSnapshot( dataPath: string, stateDir: string, options?: LocalMeshSnapshotOptions, ): LocalMeshSnapshot { const busyTimeoutMs = Math.max(0, Math.trunc(Number(options?.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS)) || 0); const busyTimeout = `PRAGMA busy_timeout = ${busyTimeoutMs}`; const scope = options?.scope; let hasLegacy = false; let agents: AgentRecord[] = []; let openMessages: MeshTuiOpenMessage[] = []; let openMessageTotal = 0; let legacyRuns: WorkflowRun[] = []; let legacyRunTotal = 0; let plans: MeshTuiPlan[] = []; if (existsSync(dataPath)) { hasLegacy = true; const database = openReadOnlyDatabase(dataPath); try { database.exec(busyTimeout); // Stored rows carry identity only. Presence is derived here against the // default lease window, because the hub's configured `staleAfterMs` is // not in the file; a reachable hub's own records replace these. // Stored rows carry identity only. Presence is NOT computed here: the hub's // configured staleAfterMs and its clock are not in the file, so a reader-side // lease would mislabel agents against the hub's own projection. The stored // `online` boolean is the only durable truth; the hub's /v1/agents or the // ops snapshot is the authoritative presence source. agents = readJsonRows<AgentIdentity>(database, "SELECT record FROM agents"); if (!scope) { openMessages = readOpenMessageMetadata(database); openMessageTotal = countRows(database, "messages", OPEN_MESSAGE_WHERE); legacyRuns = readJsonRows<WorkflowRun>(database, "SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 8"); legacyRunTotal = countRows(database, "workflow_runs"); plans = readPlanMetadata(database); } else { // Scoped: this project's runs and this recipient's open messages only. // Plans stay empty: journal text belongs to other agents' runs. if (scope.recipientName) { const messageParams = [scope.hubProject, scope.recipientName]; openMessages = readOpenMessageMetadata(database, SCOPED_MESSAGE_WHERE, messageParams); openMessageTotal = countRows(database, "messages", SCOPED_MESSAGE_WHERE, messageParams); } const runWhere = " WHERE json_extract(record, '$.project') = ?"; legacyRuns = readJsonRows<WorkflowRun>(database, `SELECT record FROM workflow_runs${runWhere} ORDER BY rowid DESC LIMIT 8`, [scope.hubProject]); legacyRunTotal = countRows(database, "workflow_runs", runWhere, [scope.hubProject]); } } finally { database.close(); } } // KXM discovery let hasKxm = false; const kxmRuns: MeshTuiRun[] = []; let kxmRunTotal = 0; // A scoped snapshot without a Runtime project id reads no Runtime runs. const kxmStateRoot = scope && !scope.runtimeProjectId ? undefined : resolveKxmStateRoot(stateDir, options); if (kxmStateRoot) { const runtimeDir = join(kxmStateRoot, "runtime"); const registryDbPath = join(runtimeDir, "registry.db"); const projectsDir = join(runtimeDir, "projects"); const projectKeys = new Set<string>(); if (existsSync(registryDbPath)) { hasKxm = true; try { const regDb = openReadOnlyDatabase(registryDbPath); try { regDb.exec(busyTimeout); const pRows = (scope ? regDb.prepare("SELECT project_key FROM projects WHERE project_id = ?").all(scope.runtimeProjectId) : regDb.prepare("SELECT project_key FROM projects").all()) as Array<{ project_key: string }>; for (const row of pRows) { if (row.project_key) projectKeys.add(row.project_key); } } finally { regDb.close(); } } catch { /* registry error */ } } // Scoped: only the registry's key for this project, never every project // directory on the machine. if (!scope && existsSync(projectsDir)) { try { for (const entry of readdirSync(projectsDir, { withFileTypes: true })) { if (entry.isDirectory()) { projectKeys.add(entry.name); } } } catch { /* projectsDir unreadable */ } } for (const key of projectKeys) { if (scope && !RUNTIME_PROJECT_KEY.test(key)) continue; const eventDbPath = join(projectsDir, key, "run-events.db"); if (existsSync(eventDbPath)) { hasKxm = true; try { const eventDb = openReadOnlyDatabase(eventDbPath); try { eventDb.exec(busyTimeout); const { runs, total } = readRuntimeRuns(eventDb, scope?.runtimeProjectId); kxmRunTotal += total; kxmRuns.push(...runs); } finally { eventDb.close(); } } catch { /* eventDb unreadable */ } } } } const pids: MeshTuiPidClaim[] = []; if (existsSync(stateDir)) { for (const file of readdirSync(stateDir).filter((name) => name.endsWith(".pid"))) { try { const record = JSON.parse(readFileSync(join(stateDir, file), "utf8")) as { pid?: number; role?: string }; pids.push({ file, ...(record.role ? { role: record.role } : {}), ...(record.pid !== undefined ? { pid: record.pid } : {}), live: Number.isInteger(record.pid) && record.pid! > 0 && processExists(record.pid!), }); } catch { pids.push({ file, live: false }); } } } // Combine runs const combinedRuns = [ ...legacyRuns.map((run) => summarizeMeshRun(run)), ...kxmRuns, ]; const seenIds = new Set<string>(); const uniqueRuns: MeshTuiRun[] = []; for (const run of combinedRuns) { if (!seenIds.has(run.id)) { seenIds.add(run.id); uniqueRuns.push(run); } } uniqueRuns.sort((a, b) => { const at = a.updatedAt ? Date.parse(a.updatedAt) : 0; const bt = b.updatedAt ? Date.parse(b.updatedAt) : 0; return bt - at; }); const runs = uniqueRuns.slice(0, 16); const runTotal = legacyRunTotal + kxmRunTotal; // Source determination let source: "legacy" | "runtime" | "both"; if (hasLegacy && hasKxm) { source = "both"; } else if (hasKxm) { source = "runtime"; } else { source = "legacy"; } // Spend from telemetry const telemetryFile = join(stateDir, "telemetry.jsonl"); const spend = existsSync(telemetryFile) ? readRoutingRecords(telemetryFile) : []; return { source, agents: agents.sort((a, b) => Number(b.online) - Number(a.online) || a.name.localeCompare(b.name)), openMessages, openMessageTotal, runs, runTotal, plans, pids, spend, }; }