/** CEREBEL — pi extension entry point. */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { CerebelStore } from "./backend.ts"; import { resolveConfiguredCerebelMaxParallel } from "./config.ts"; import { CerebelError, CerebelToolParams, type Assignment, type AssignmentStatus, type CerebelToolInput, type CerebelSummary, type Wave, type WaveStatus } from "./schema.ts"; import { isTerminalAssignmentStatus } from "./store.ts"; import { renderCerebelCall, renderCerebelResult, RUN_WAVE_DASHBOARD_HINT, summarizeList, summarizeSummary, summarizeWave } from "./render.ts"; import { RunWaveBatchError, runWave, type RunWaveLionAdapter, type RunWaveResult } from "./run-wave.ts"; import type { LionModelRole, LionProgressSnapshot, LionRun } from "@nervous-system/lion/extension/schema.ts"; import type { LionRunnerOutcome } from "@nervous-system/lion/extension/subprocess.ts"; import type { LionTerminalIntent } from "@nervous-system/lion/extension/cleanup-supervisor.ts"; interface CerebelDetails { action: string; wave?: Wave; waves?: Wave[]; summary?: CerebelSummary; run_wave?: import("./run-wave.ts").RunWaveResult; error?: string } type ToolResult = { content: Array<{ type: "text"; text: string }>; details: CerebelDetails; isError?: boolean }; type LionRunner = (req: import("@nervous-system/lion/extension/subprocess.ts").LionRunRequest) => Promise; interface LionAdapterDeps { lionStore?: { namespaceId: string; mutate(fn: (ledger: import("@nervous-system/lion/extension/store.ts").LionLedger) => T): Promise<{ result: T }>; query(fn: (ledger: import("@nervous-system/lion/extension/store.ts").LionLedger) => T): Promise<{ result: T }>; flushProgress?(run: Pick, progress: LionProgressSnapshot): Promise; finishRun?(id: string, incarnationId: string | null | undefined, input: import("@nervous-system/lion/extension/store.ts").FinishRunInput): Promise<{ result: { run: LionRun | undefined; committed: boolean } }>; }; createLionRunner?: (opts: { cwd: string }) => LionRunner; createLionRpcRunner?: (opts: { cwd: string; store: unknown }) => LionRunner; activeRuns?: typeof import("@nervous-system/lion/extension/active-runs.ts"); lifecycle?: typeof import("@nervous-system/lion/extension/lifecycle.ts"); options?: typeof import("@nervous-system/lion/extension/options.ts"); progressBatcher?: typeof import("@nervous-system/lion/extension/progress-batcher.ts"); cleanupSupervisor?: typeof import("@nervous-system/lion/extension/cleanup-supervisor.ts"); } function ok(action: string, text: string, details: Omit = {}): ToolResult { return { content: [{ type: "text", text }], details: { action, ...details } }; } function fail(action: string, message: string, details: Omit = {}): ToolResult { return { content: [{ type: "text", text: message }], details: { action, ...details, error: message }, isError: true }; } export function runWaveBatchFailureResult(error: RunWaveBatchError, suffix = ""): ToolResult { return fail("run_wave", `cerebel ${error.message}${suffix ? ` ${suffix.trim()}` : ""}`, { wave: error.result.wave, run_wave: error.result }); } async function runOp(store: CerebelStore, action: string, op: (l: import("./store.ts").CerebelLedger) => ToolResult): Promise { try { const { result } = await store.mutate(op); return result; } catch (e) { return e instanceof CerebelError ? fail(action, `cerebel ${action} failed (${e.code}): ${e.message}`) : fail(action, `cerebel ${action} failed: ${e instanceof Error ? e.message : String(e)}`); } } async function runQuery(store: CerebelStore, action: string, op: (l: import("./store.ts").CerebelLedger) => ToolResult): Promise { try { const { result } = await store.query(op); return result; } catch (e) { return fail(action, `cerebel ${action} failed: ${e instanceof Error ? e.message : String(e)}`); } } function waveId(l: import("./store.ts").CerebelLedger, id?: string): string | undefined { if (!id || id === "current" || id === "latest") return l.current_wave_id ?? l.current()?.id; return id; } const DEFAULT_RUN_WAVE_TIMEOUT_MS = 10 * 60_000; export const MAX_RUN_WAVE_TIMEOUT_MS = 2_147_483_647; export function validateRunWaveTimeoutMs(value: number | undefined): number { if (value === undefined) return DEFAULT_RUN_WAVE_TIMEOUT_MS; if (!Number.isInteger(value) || value < 1 || value > MAX_RUN_WAVE_TIMEOUT_MS) { throw new CerebelError("invalid_arg", `run_wave timeout_ms must be an integer from 1 through ${MAX_RUN_WAVE_TIMEOUT_MS}`); } return value; } function ganglionStatusFromAssignment(status: AssignmentStatus): "completed" | "blocked" | "failed" | "cancelled" { return status === "blocked" ? "blocked" : status === "failed" ? "failed" : status === "cancelled" ? "cancelled" : "completed"; } function findRecordedAssignment(wave: Wave, p: CerebelToolInput): Assignment | undefined { if (p.assignment_id) return wave.assignments.find((a) => a.id === p.assignment_id); if (p.task_id) return wave.assignments.find((a) => a.task_id === p.task_id); if (p.lion_run_id && p.lion_run_incarnation_id) { return wave.assignments.find((a) => a.lion_run_id === p.lion_run_id && a.lion_run_incarnation_id === p.lion_run_incarnation_id); } return undefined; } function formatGanglionRecordMessage( ganglionId: string, allocationId: string, disposition: import("../../ganglion/extension/store.ts").AllocationReleaseDisposition, formatDisposition: typeof import("../../ganglion/extension/disposition.ts").formatAllocationReleaseDisposition, ): string { return `GANGLION ${ganglionId}/${allocationId} recorded; ${formatDisposition(disposition)}.`; } async function recordLinkedGanglion(cwd: string, assignment: Assignment | undefined, p: CerebelToolInput, outcome: AssignmentStatus): Promise { if (!assignment || !isTerminalAssignmentStatus(outcome)) return null; if (assignment.cleanup_pending_settlement) return `GANGLION capacity retained for cleanup-pending assignment ${assignment.id}.`; const ganglionId = p.ganglion_id ?? assignment.ganglion_id; const allocationId = p.ganglion_allocation_id ?? assignment.ganglion_allocation_id; if (!allocationId) return null; if (!ganglionId) return `GANGLION release skipped: assignment ${assignment.id} has allocation ${allocationId} but no ganglion_id.`; try { const [{ GanglionStore }, { formatAllocationReleaseDisposition }] = await Promise.all([ import("../../ganglion/extension/backend.ts"), import("../../ganglion/extension/disposition.ts"), ]); const { result } = await GanglionStore.fromCwd(cwd).mutate((l) => l.recordWithResult(ganglionId, { allocation_id: allocationId, lion_run_id: p.lion_run_id ?? assignment.lion_run_id ?? undefined, lion_run_incarnation_id: p.lion_run_incarnation_id ?? assignment.lion_run_incarnation_id ?? undefined, status: ganglionStatusFromAssignment(outcome), summary: p.summary })); return formatGanglionRecordMessage(ganglionId, allocationId, result.release_disposition, formatAllocationReleaseDisposition); } catch (e) { return `GANGLION release failed for ${ganglionId}/${allocationId}: ${e instanceof Error ? e.message : String(e)}`; } } interface GroupedGanglionRecord { assignmentId: string; ganglionId?: string | null; allocationId: string; lionRunId?: string; lionRunIncarnationId?: string; outcome: AssignmentStatus; summary: string; } function* runWaveGanglionRecords(result: RunWaveResult): Generator { const assignments = new Map(result.wave.assignments.map((assignment) => [assignment.id, assignment])); for (const assignmentResult of result.assignment_results) { if (assignmentResult.outcome === "skipped" || assignmentResult.outcome === "cleanup_pending") continue; const assignment = assignments.get(assignmentResult.assignment_id); if (!assignment || assignment.cleanup_pending_settlement || !assignment.ganglion_allocation_id) continue; yield { assignmentId: assignment.id, ganglionId: assignment.ganglion_id, allocationId: assignment.ganglion_allocation_id, lionRunId: assignmentResult.lion_run_id, lionRunIncarnationId: assignmentResult.lion_run_incarnation_id, outcome: assignmentResult.outcome, summary: assignmentResult.summary, }; } } function* cancelledWaveGanglionRecords(wave: Wave): Generator { for (const assignment of wave.assignments) { if (!isTerminalAssignmentStatus(assignment.status) || !assignment.ganglion_allocation_id) continue; yield { assignmentId: assignment.id, ganglionId: assignment.ganglion_id, allocationId: assignment.ganglion_allocation_id, lionRunId: assignment.lion_run_id ?? undefined, lionRunIncarnationId: assignment.lion_run_incarnation_id ?? undefined, outcome: assignment.status, summary: `CEREBEL cancellation reconciled terminal assignment ${assignment.status}`, }; } } async function recordGroupedGanglion(cwd: string, entries: Iterable): Promise { const messages: string[] = []; const grouped = new Map(); for (const entry of entries) { if (!entry.ganglionId) { messages.push(`GANGLION release skipped: assignment ${entry.assignmentId} has allocation ${entry.allocationId} but no ganglion_id.`); continue; } const group = grouped.get(entry.ganglionId) ?? []; group.push(entry); grouped.set(entry.ganglionId, group); } if (!grouped.size) return messages; try { const [{ GanglionStore }, { GanglionError }, { formatAllocationReleaseDisposition }] = await Promise.all([ import("../../ganglion/extension/backend.ts"), import("../../ganglion/extension/schema.ts"), import("../../ganglion/extension/disposition.ts"), ]); for (const [ganglionId, group] of grouped) { try { const { result: records } = await GanglionStore.fromCwd(cwd).mutate((ledger) => { const ganglion = ledger.get(ganglionId); if (!ganglion) throw new GanglionError("not_found", `ganglion ${ganglionId} not found`); const allocationIds = new Set(ganglion.allocations.map((allocation) => allocation.id)); return group.map((entry) => { if (!allocationIds.has(entry.allocationId)) return { entry, errorMessage: `allocation ${entry.allocationId} not found` }; try { return { entry, releaseDisposition: ledger.recordWithResult(ganglionId, { allocation_id: entry.allocationId, lion_run_id: entry.lionRunId, lion_run_incarnation_id: entry.lionRunIncarnationId, status: ganglionStatusFromAssignment(entry.outcome), summary: entry.summary }).release_disposition }; } catch (error) { return { entry, errorMessage: error instanceof Error ? error.message : String(error) }; } }); }); for (const record of records) { if ("errorMessage" in record) messages.push(`GANGLION release failed for ${ganglionId}/${record.entry.allocationId}: ${record.errorMessage}`); else messages.push(formatGanglionRecordMessage(ganglionId, record.entry.allocationId, record.releaseDisposition, formatAllocationReleaseDisposition)); } } catch (error) { for (const entry of group) messages.push(`GANGLION release failed for ${ganglionId}/${entry.allocationId}: ${error instanceof Error ? error.message : String(error)}`); } } } catch (error) { for (const [ganglionId, group] of grouped) for (const entry of group) messages.push(`GANGLION release failed for ${ganglionId}/${entry.allocationId}: ${error instanceof Error ? error.message : String(error)}`); } return messages; } export async function recordRunWaveGanglion(cwd: string, result: RunWaveResult): Promise { return recordGroupedGanglion(cwd, runWaveGanglionRecords(result)); } async function reconcileCancelledWaveGanglion(cwd: string, wave: Wave): Promise { return recordGroupedGanglion(cwd, cancelledWaveGanglionRecords(wave)); } async function settleCleanupGanglion(cwd: string, assignment: Assignment, runId: string, incarnationId: string, summary: string): Promise { if (!assignment.ganglion_allocation_id) return; if (!assignment.ganglion_id) throw new Error(`cleanup-pending assignment ${assignment.id} has allocation ${assignment.ganglion_allocation_id} but no ganglion_id`); const { GanglionStore } = await import("../../ganglion/extension/backend.ts"); const { result } = await GanglionStore.fromCwd(cwd).mutate((ledger) => ledger.recordIfOwned( assignment.ganglion_id!, assignment.ganglion_allocation_id!, runId, incarnationId, { status: ganglionStatusFromAssignment(assignment.status), summary }, )); const exactTerminal = result.allocation.lion_run_id === runId && result.allocation.lion_run_incarnation_id === incarnationId && ["completed", "blocked", "failed", "cancelled"].includes(result.allocation.status); if (!result.committed && !exactTerminal) throw new Error(`GANGLION allocation settlement was superseded for ${assignment.ganglion_id}/${assignment.ganglion_allocation_id}/${runId}/${incarnationId}`); } async function linkRunWaveGanglion(cwd: string, assignment: Assignment, run: Pick): Promise { if (!assignment.ganglion_allocation_id) return; if (!assignment.ganglion_id) throw new Error(`run_wave assignment ${assignment.id} has allocation ${assignment.ganglion_allocation_id} but no ganglion_id`); if (!run.incarnation_id) throw new Error(`run_wave LION ${run.id} has no incarnation provenance`); const { GanglionStore } = await import("../../ganglion/extension/backend.ts"); const { result } = await GanglionStore.fromCwd(cwd).mutate((ledger) => ledger.linkRunIfUnlinked(assignment.ganglion_id!, assignment.ganglion_allocation_id!, run.id, run.incarnation_id!)); if (!result.committed) throw new Error(`GANGLION allocation provenance was superseded for ${assignment.ganglion_id}/${assignment.ganglion_allocation_id}/${run.id}/${run.incarnation_id}`); } export interface LinkedLionSettlement { assignment: Assignment; settled: boolean; run_status?: string; error?: string; } const DEFAULT_CANCEL_SETTLE_TIMEOUT_MS = 15_000; const MAX_CANCEL_SETTLE_TIMEOUT_MS = 120_000; /** Covers short reservation→exact-link races; exhaustion fails closed and retains capacity. */ const CANCEL_STABILITY_MAX_ATTEMPTS = 10; const CANCEL_STABILITY_RETRY_MS = 50; const CANCEL_RESERVATION_STALE_MS = 30_000; function lionRunRefKey(runId: string, incarnationId: string | null | undefined): string { return JSON.stringify([runId, incarnationId ?? null]); } export function hasPendingCancellationAssignments(wave: Wave, settledRunRefs: ReadonlySet): boolean { return wave.assignments.some((assignment) => (assignment.status === "dispatched" && !assignment.lion_run_id) || Boolean(assignment.lion_run_id && !settledRunRefs.has(lionRunRefKey(assignment.lion_run_id, assignment.lion_run_incarnation_id))), ); } export function resolveCancelSettlementTimeout(value = process.env.CEREBEL_CANCEL_SETTLE_TIMEOUT_MS): number { if (value === undefined || value.trim() === "") return DEFAULT_CANCEL_SETTLE_TIMEOUT_MS; const parsed = Number(value); return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_CANCEL_SETTLE_TIMEOUT_MS ? parsed : DEFAULT_CANCEL_SETTLE_TIMEOUT_MS; } export async function settleLinkedLionsBeforeCancel( cwd: string, wave: Wave, reason: string, timeoutMs = resolveCancelSettlementTimeout(), loadRuntime = () => Promise.all([import("@nervous-system/lion/extension/backend.ts"), import("@nervous-system/lion/extension/active-runs.ts")]), ): Promise { const assignments = wave.assignments.filter((assignment) => assignment.lion_run_id); if (!assignments.length) return []; const results = new Map(); const [{ LionStore }, controls] = await loadRuntime(); const lionStore = LionStore.fromCwd(cwd); const pending: Array<{ assignment: Assignment; run: Pick }> = []; let cancellations: Awaited>; try { cancellations = await controls.requestRunCancellations(lionStore, assignments.map((assignment) => ({ runId: assignment.lion_run_id!, reason, expectedIncarnationId: assignment.lion_run_incarnation_id!, expectIncarnation: true, }))); } catch (error) { const message = error instanceof Error ? error.message : String(error); return assignments.map((assignment) => ({ assignment, settled: false, error: message })); } cancellations.forEach((cancellation, index) => { const assignment = assignments[index]!; if (cancellation.settled) { results.set(assignment.id, { assignment, settled: true, run_status: cancellation.run?.status }); } else if (cancellation.run_ref ?? cancellation.run) { pending.push({ assignment, run: (cancellation.run_ref ?? cancellation.run)! }); } else { results.set(assignment.id, { assignment, settled: cancellation.superseded, error: cancellation.superseded ? undefined : "LION run disappeared during cancellation" }); } }); if (pending.length) { const settlements = await controls.waitForRunSettlements(lionStore, pending.map((entry) => entry.run), timeoutMs); settlements.forEach((settlement, index) => { const assignment = pending[index]!.assignment; results.set(assignment.id, { assignment, settled: settlement.settled, run_status: settlement.run?.status, error: settlement.settled ? undefined : `LION ${assignment.lion_run_id} remained ${settlement.run?.status ?? "unknown"}`, }); }); } return assignments.map((assignment) => results.get(assignment.id)!); } function lateTerminalFinishInput( intent: LionTerminalIntent, cleanupError?: Error, hostAborted = false, ): import("@nervous-system/lion/extension/store.ts").FinishRunInput { if (intent.kind === "result" && !cleanupError && !hostAborted) return { output: intent.output.text, report: intent.output.report }; const error = intent.kind === "error" ? intent.error : cleanupError ?? new Error("Host aborted run_wave during cleanup"); return { output: "", report: null, status: hostAborted ? "aborted" : "failed", error: hostAborted ? "Cancelled" : error.message, }; } export async function createLionAdapter(ctx: ExtensionContext, p: CerebelToolInput, signal: AbortSignal | undefined, onUpdate: ((update: { content: Array<{ type: "text"; text: string }>; details: unknown }) => void) | undefined, deps: LionAdapterDeps = {}, pi?: ExtensionAPI): Promise { const timeoutMs = validateRunWaveTimeoutMs(p.timeout_ms); try { const [{ LionStore }, jsonRunnerMod, rpcRunnerMod, activeRunsMod, lifecycleMod, optionsMod, progressBatcherMod, cleanupSupervisorMod] = await Promise.all([ deps.lionStore ? Promise.resolve({ LionStore: { fromCwd: () => deps.lionStore! as never } }) : import("@nervous-system/lion/extension/backend.ts"), deps.createLionRunner ? Promise.resolve({ createLionRunner: deps.createLionRunner }) : import("@nervous-system/lion/extension/subprocess.ts"), deps.createLionRpcRunner ? Promise.resolve({ createLionRpcRunner: deps.createLionRpcRunner }) : import("@nervous-system/lion/extension/rpc-runner.ts"), deps.activeRuns ? Promise.resolve(deps.activeRuns) : import("@nervous-system/lion/extension/active-runs.ts"), deps.lifecycle ? Promise.resolve(deps.lifecycle) : import("@nervous-system/lion/extension/lifecycle.ts"), deps.options ? Promise.resolve(deps.options) : import("@nervous-system/lion/extension/options.ts"), deps.progressBatcher ? Promise.resolve(deps.progressBatcher) : import("@nervous-system/lion/extension/progress-batcher.ts"), deps.cleanupSupervisor ? Promise.resolve(deps.cleanupSupervisor) : import("@nervous-system/lion/extension/cleanup-supervisor.ts"), ]); const { createLionRunner } = jsonRunnerMod; const { createLionRpcRunner } = rpcRunnerMod; const activeRuns = activeRunsMod; const lifecycle = lifecycleMod; const lionStore = LionStore.fromCwd(ctx.cwd); const modelRole = (p.model_role as LionModelRole | undefined) ?? "implementation"; const model = p.model?.trim() || optionsMod.resolveConfiguredLionModel(ctx.cwd, () => ctx.isProjectTrusted?.() ?? false, modelRole); const runnerMode = optionsMod.resolveLionRunnerMode(p.runner_mode); const runner = runnerMode === "rpc" ? createLionRpcRunner({ cwd: ctx.cwd, store: lionStore }) : createLionRunner({ cwd: ctx.cwd }); const activeOwners = new Map>(); const activeRunViews = new Map(); return { createProgressUpdater: lifecycle.createProgressUpdater, async createRun(assignment) { let activeOwner: ReturnType | undefined; let started: LionRun | undefined; try { const initialProgress = lifecycle.startedProgress(); ({ result: started } = await lionStore.mutate((l) => { const queued = l.create({ agent_id: assignment.agent_id, task_id: assignment.task_id, objective: assignment.objective, context: assignment.context, model, model_role: modelRole, runner_mode: runnerMode, tools: p.tools, start: false, }); activeOwner = activeRuns.beginActiveRun({ namespaceId: lionStore.namespaceId, runId: queued.id, incarnationId: queued.incarnation_id ?? null }, runnerMode); return l.start(queued.id); })); if (!started) throw new Error("LION start did not return a run"); if ("flushProgress" in lionStore && typeof lionStore.flushProgress === "function") { const accepted = await lionStore.flushProgress(started, initialProgress); if (!accepted) throw new Error(`LION start progress was rejected for run_id=${started.id}`); } else { const persisted = await lionStore.mutate((ledger) => ledger.updateProgressIfCurrent(started!.id, started!.incarnation_id, initialProgress)); if (!persisted.result.committed || !persisted.result.run) throw new Error(`LION start progress was superseded for run_id=${started.id}`); started = persisted.result.run; } const result = { ...started, progress: initialProgress, updated_at: initialProgress.last_event_at }; activeOwners.set(result.id, activeOwner!); activeRunViews.set(result.id, result); lifecycle.emitLionEvent(pi, "started", result, initialProgress); try { onUpdate?.({ content: [{ type: "text", text: `${result.id}/${result.agent_id}: ${initialProgress.activity}` }], details: { action: "run_wave", run: result } }); } catch { /* progress display is best-effort */ } return result; } catch (err) { if (started && activeOwner) { const message = err instanceof Error ? err.message : String(err); try { if ("finishRun" in lionStore && typeof lionStore.finishRun === "function") await lionStore.finishRun(started.id, activeOwner.incarnationId, { output: "", report: null, status: "failed", error: `LION startup progress failed: ${message}` }); else await lionStore.mutate((ledger) => ledger.finalizeIfCurrent(started!.id, activeOwner!.incarnationId, { output: "", report: null, status: "failed", error: `LION startup progress failed: ${message}` })); } catch (cleanupError) { console.warn(`[nervous-system/cerebel] failed to terminalize startup error for ${started.id}:`, cleanupError); } } if (activeOwner) activeRuns.finishActiveRun(activeOwner); throw err; } }, async run(run: LionRun, _assignment, onProgress, runSignal, onCleanupSettled, prepareCleanupHandoff, beforeCleanupFinalize) { const activeOwner = activeOwners.get(run.id); if (!activeOwner) throw new Error(`active LION ownership missing for ${run.id}`); return runner({ run, cleanupOwner: activeOwner, registerCleanupSupervisor: async (handoff) => { // The exact LION observation must commit before the cross-store // CEREBEL obligation becomes actionable. A crash between these // writes may retain capacity, but can never release it without proof. await cleanupSupervisorMod.persistCleanupPendingObservation(lionStore, activeOwner, handoff); await prepareCleanupHandoff?.(); return cleanupSupervisorMod.registerLionCleanupSupervisor({ owner: activeOwner, handoff, finalize: async (intent, cleanupError) => { await beforeCleanupFinalize?.(); return cleanupSupervisorMod.finalizeExactLionRun( lionStore, activeOwner, lateTerminalFinishInput(intent, cleanupError, Boolean((runSignal ?? signal)?.aborted)), ); }, emitTerminal: (settlement) => { if (settlement.disposition === "terminal") lifecycle.emitLionEvent(pi, lifecycle.terminalEventKind(settlement.run.status as import("@nervous-system/lion/extension/schema.ts").TerminalLionRunStatus), settlement.run); }, onSettled: async (settlement) => { await onCleanupSettled?.(settlement); }, releaseOwner: () => { activeRuns.finishActiveRun(activeOwner); activeOwners.delete(run.id); }, }); }, signal: runSignal ?? signal, timeout_ms: timeoutMs, onProcessStart: (info) => { activeRuns.attachActiveRunProcess(activeOwner, info); void lionStore.mutate((l) => activeRuns.isActiveRunOwner(activeOwner) ? l.updateControlIfCurrent(run.id, activeOwner.incarnationId, { pid: info.pid, pgid: info.pgid, process_identity: info.process_identity ?? null, started_at: new Date().toISOString() }) : { run: l.get(run.id), committed: false }) .catch((error) => console.warn(`[nervous-system/cerebel] process metadata persistence failed for ${run.id}:`, error)) .finally(() => activeRuns.replayPendingCancellation(activeOwner, lionStore).catch((error) => { console.warn(`[nervous-system/cerebel] pending cancellation replay failed for ${run.id}:`, error); })); }, onControlClosed: () => activeRuns.markActiveRunControlClosed(activeOwner), onProcessExit: () => activeRuns.markActiveRunExited(activeOwner), onProgress: (progress: LionProgressSnapshot) => { try { onUpdate?.({ content: [{ type: "text", text: `${run.id}/${run.agent_id}: ${progress.activity}` }], details: { action: "run_wave", run } }); } catch { /* progress display is best-effort */ } onProgress(progress); }, }); }, async finishRun(runId, result) { const owner = activeOwners.get(runId); if (!owner) throw new Error(`active LION ownership missing while finalizing ${runId}`); try { const outcome = await cleanupSupervisorMod.finalizeExactLionRun(lionStore, owner, result); if (outcome.disposition !== "terminal") { throw new Error(`LION finalization superseded for run_id=${runId} incarnation_id=${owner.incarnationId ?? "null"}`); } const finished = outcome.run; if (finished.status === "queued" || finished.status === "running") throw new Error(`LION ${runId} remained nonterminal after finish`); lifecycle.emitLionEvent(pi, lifecycle.terminalEventKind(finished.status), finished); return finished; } finally { activeRuns.finishActiveRun(owner); activeOwners.delete(runId); activeRunViews.delete(runId); } }, async getRun(runId) { return (await lionStore.query((l) => l.get(runId))).result; }, async updateProgress(runId, progress) { const owner = activeOwners.get(runId); if (!owner) return; const accepted = ("flushProgress" in lionStore && typeof lionStore.flushProgress === "function") ? await progressBatcherMod.persistBatchedProgress(lionStore as never, { id: runId, incarnation_id: owner.incarnationId ?? null }, progress) : (await lionStore.mutate((ledger) => ledger.updateProgressIfCurrent(runId, owner.incarnationId, progress))).result.committed ? progress : undefined; if (accepted) { const current = activeRunViews.get(runId); if (current) { const updated = { ...current, progress: accepted, updated_at: accepted.last_event_at }; activeRunViews.set(runId, updated); lifecycle.emitLionEvent(pi, "progress", updated, accepted); } } }, }; } catch (e) { throw new Error(`cerebel run_wave LION adapter initialization failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); } } export default function (pi: ExtensionAPI) { pi.registerTool({ name: "cerebel", label: "CEREBEL", description: [ "Orchestration controller for LION worker waves. Forms assignments from ready AXON tasks,", "records LION run outcomes, can run planned waves through LION with run_wave, and decides whether to dispatch, wait, complete, replan, or escalate.", "State persists in the active NERVous project/context namespace. Actions: plan_wave, dispatch, record, decide, complete_wave, cancel, run_wave, get, list, summary.", ].join(" "), promptSnippet: "Orchestrate ready AXON tasks into LION worker waves and record outcomes", promptGuidelines: [ "Opt-in: use/mention this component only for explicit NERVous, durable-state, orchestration, delegation, coordination, or risk-triage requests.", "Use cerebel after CORTEX has planned work into AXON and ready AXON tasks exist.", "First read axon list/summary, then pass ready task briefs into cerebel plan_wave.", "For manual control, call lion run with task_id/objective/context/agent_id, then cerebel dispatch the returned LION run id and incarnation id before recording its outcome. For bounded active execution, use cerebel run_wave on an already planned wave.", "When assignments come from GANGLION, include ganglion_id and ganglion_allocation_id on the CEREBEL assignment/dispatch/record so CEREBEL releases member capacity on terminal outcomes.", "After blocked/failed results: cerebel record/decide, update AXON, post a SYNAPSE risk/blocker note, then use AMYGDALA or replan; never silently continue.", ], parameters: CerebelToolParams, async execute(_toolCallId, params, signal, onUpdate, ctx) { const store = CerebelStore.fromCwd(ctx.cwd); const p = params as CerebelToolInput; const action = p.action; switch (action) { case "plan_wave": { return runOp(store, action, (l) => { const maxParallel = p.max_parallel ?? resolveConfiguredCerebelMaxParallel(); const wave = l.planWave({ goal_id: p.goal_id, tasks: p.tasks, assignments: p.assignments, context: p.context, max_parallel: maxParallel }); return ok(action, `Planned ${wave.id}: ${wave.assignments.length} assignment(s). Next: run LION for ready assignments, then cerebel dispatch/record.`, { wave }); }); } case "dispatch": { return runOp(store, action, (l) => { const id = waveId(l, p.wave_id); if (!id) return fail(action, "dispatch requires wave_id or current wave."); const wave = l.dispatch(id, { links: p.links }); return ok(action, `Dispatched ${wave.id}. Decision: ${wave.decision?.decision ?? "—"}.`, { wave }); }); } case "record": { const outcome = p.outcome; if (!outcome) return fail(action, "record requires `outcome`."); const result = await runOp(store, action, (l) => { const id = waveId(l, p.wave_id); if (!id) return fail(action, "record requires wave_id or current wave."); const wave = l.record(id, { assignment_id: p.assignment_id, task_id: p.task_id, lion_run_id: p.lion_run_id, lion_run_incarnation_id: p.lion_run_incarnation_id, ganglion_id: p.ganglion_id, ganglion_allocation_id: p.ganglion_allocation_id, outcome, summary: p.summary, changed_files: p.changed_files, tests_run: p.tests_run, blockers: p.blockers, next_steps: p.next_steps }); return ok(action, `Recorded result in ${wave.id}. Decision: ${wave.decision?.decision ?? "—"}.`, { wave }); }); const assignment = result.details.wave ? findRecordedAssignment(result.details.wave, p) : undefined; const ganglionMessage = await recordLinkedGanglion(ctx.cwd, assignment, p, outcome as AssignmentStatus); if (ganglionMessage) result.content[0]!.text += ` ${ganglionMessage}`; return result; } case "decide": { return runOp(store, action, (l) => { const id = waveId(l, p.wave_id); if (!id) return fail(action, "decide requires wave_id or current wave."); const decision = l.decide(id); const wave = l.get(id)!; return ok(action, `Decision for ${id}: ${decision.decision} — ${decision.reason}`, { wave }); }); } case "complete_wave": { return runOp(store, action, (l) => { const id = waveId(l, p.wave_id); if (!id) return fail(action, "complete_wave requires wave_id or current wave."); const wave = l.complete(id); return ok(action, `Completed ${wave.id}.`, { wave }); }); } case "cancel": { try { const initial = (await store.query((l) => { const id = waveId(l, p.wave_id); return id ? l.get(id) : undefined; })).result; if (!initial) return fail(action, "cancel requires wave_id or current wave."); const settledRunRefs = new Set(); let cancelledWave: Wave | undefined; for (let pass = 0; pass < CANCEL_STABILITY_MAX_ATTEMPTS && !cancelledWave; pass++) { const latest = (await store.query((l) => l.get(initial.id))).result ?? initial; const outstandingWave: Wave = { ...latest, assignments: latest.assignments.filter((assignment) => assignment.lion_run_id && !settledRunRefs.has(lionRunRefKey(assignment.lion_run_id, assignment.lion_run_incarnation_id))), }; const settlements = await settleLinkedLionsBeforeCancel(ctx.cwd, outstandingWave, p.reason ?? "CEREBEL wave cancelled"); const failures = settlements.filter((settlement) => !settlement.settled); if (failures.length) { const message = failures.map((failure) => `${failure.assignment.id}: ${failure.error ?? "LION did not settle"}`).join("; "); return fail(action, `cerebel cancel retained wave/capacity because linked LIONs did not settle: ${message}`, { wave: latest }); } for (const settlement of settlements) if (settlement.assignment.lion_run_id) { settledRunRefs.add(lionRunRefKey(settlement.assignment.lion_run_id, settlement.assignment.lion_run_incarnation_id)); } const attempt = await store.mutate((l) => { l.recoverOrphanedReservations(initial.id, { stale_after_ms: CANCEL_RESERVATION_STALE_MS }); const current = l.get(initial.id); if (!current) throw new CerebelError("not_found", `wave ${initial.id} not found`); const pending = hasPendingCancellationAssignments(current, settledRunRefs); return pending ? undefined : l.cancel(initial.id); }); cancelledWave = attempt.result; if (!cancelledWave && pass + 1 < CANCEL_STABILITY_MAX_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, CANCEL_STABILITY_RETRY_MS)); } if (!cancelledWave) return fail(action, "cerebel cancel could not obtain a stable settled assignment set; no capacity was released", { wave: (await store.query((l) => l.get(initial.id))).result }); const result = ok(action, `Cancelled ${cancelledWave.id}.`, { wave: cancelledWave }); const releaseMessages = await reconcileCancelledWaveGanglion(ctx.cwd, cancelledWave); if (releaseMessages.length) result.content[0]!.text += ` ${releaseMessages.join(" ")}`; return result; } catch (e) { return e instanceof CerebelError ? fail(action, `cerebel cancel failed (${e.code}): ${e.message}`) : fail(action, `cerebel cancel failed: ${e instanceof Error ? e.message : String(e)}`); } } case "run_wave": { try { const adapter = await createLionAdapter(ctx, p, signal, onUpdate, {}, pi); try { onUpdate?.({ content: [{ type: "text", text: RUN_WAVE_DASHBOARD_HINT.replace(/`/g, "") }], details: { action: "run_wave", hint: "dashboard" } }); } catch { /* dashboard hint is best-effort */ } const result = await runWave(store, adapter, { wave_id: p.wave_id, max_parallel: p.max_parallel, signal, onRunLinked: async (assignment, run) => linkRunWaveGanglion(ctx.cwd, assignment, run), onLateSettlement: async (lateResult, lateWaveId) => { if (lateResult.outcome === "skipped" || lateResult.outcome === "cleanup_pending") return; const { result: latestWave } = await store.query((ledger) => ledger.get(lateWaveId)); const assignment = latestWave?.assignments.find((candidate) => candidate.id === lateResult.assignment_id); if (!assignment || !isTerminalAssignmentStatus(assignment.status)) return; const message = await recordLinkedGanglion(ctx.cwd, assignment, { action: "record", lion_run_id: lateResult.lion_run_id, lion_run_incarnation_id: lateResult.lion_run_incarnation_id, summary: lateResult.summary, } as CerebelToolInput, assignment.status); if (message?.startsWith("GANGLION release failed")) throw new Error(message); }, }); const ganglionMessages = await recordRunWaveGanglion(ctx.cwd, result); const suffix = ganglionMessages.length ? ` ${ganglionMessages.join(" ")}` : ""; return ok(action, `Ran ${result.summary}.${suffix}`, { wave: result.wave, run_wave: result }); } catch (e) { if (e instanceof RunWaveBatchError) { const ganglionMessages = await recordRunWaveGanglion(ctx.cwd, e.result); const suffix = ganglionMessages.length ? ` ${ganglionMessages.join(" ")}` : ""; return runWaveBatchFailureResult(e, suffix); } return e instanceof CerebelError ? fail(action, `cerebel run_wave failed (${e.code}): ${e.message}`) : fail(action, `cerebel run_wave failed: ${e instanceof Error ? e.message : String(e)}`); } } case "get": { return runQuery(store, action, (l) => { const id = waveId(l, p.wave_id); const wave = id ? l.get(id) : l.current(); if (!wave) return fail(action, "No CEREBEL wave found."); return ok(action, summarizeWave(wave), { wave }); }); } case "list": { return runQuery(store, action, (l) => { const waves = l.list({ status: p.status_filter as WaveStatus | undefined, limit: p.limit }); return ok(action, summarizeList(waves), { waves }); }); } case "summary": { return runQuery(store, action, (l) => { const summary = l.summary(p.limit ?? 10); return ok(action, summarizeSummary(summary), { summary }); }); } default: return fail(action, `Unknown action: ${action as string}`); } }, renderCall(args, theme) { return renderCerebelCall(args as { action: string; wave_id?: string }, theme as never); }, renderResult(result, options, theme) { return renderCerebelResult(result as Parameters[0], options as Parameters[1], theme as never); }, }); pi.registerCommand("cerebel", { description: "Show CEREBEL orchestration summary", handler: async (_args, ctx) => { const store = CerebelStore.fromCwd(ctx.cwd); const { result } = await store.query((l) => l.summary(10)); post(ctx, pi, summarizeSummary(result), { summary: result }); } }); pi.registerCommand("cerebel:waves", { description: "List recent CEREBEL waves", handler: async (_args, ctx) => { const store = CerebelStore.fromCwd(ctx.cwd); const { result } = await store.query((l) => l.list({ limit: 20 })); post(ctx, pi, summarizeList(result), { waves: result }); } }); } function post(ctx: ExtensionContext, pi: ExtensionAPI, markdown: string, details: Record): void { if (ctx.hasUI) pi.sendMessage({ customType: "cerebel", content: markdown, display: true, details }, { triggerTurn: false }); else ctx.ui.notify(markdown, "info"); }