/** * LION — pure run ledger. * * No filesystem or pi subprocess logic here. This module handles ids, run * lifecycle, summaries, and JSON coercion so it is easy to test and reuse by * CEREBEL later. */ import { randomUUID } from "node:crypto"; import { LION_CANCEL_DELIVERY_STATUSES, LION_PROGRESS_EVENTS, LION_RUN_STATUSES, coerceLionReport, redactLionDiagnosticText, LION_TERMINAL_DIAGNOSTIC_REASONS, MAX_ACTIVE_TOOL_NAME_CHARS, MAX_LION_REPORT_ITEMS, MAX_ACTIVE_TOOL_NAMES, isActiveLionStatus, isTerminalLionStatus, LionError, LION_STEERING_STATUSES, type LionFile, type LionModelRole, type LionProgressEvent, type LionRunnerMode, type LionCancelDeliveryStatus, type LionControlState, type LionProgressSnapshot, type LionReport, type LionRun, type LionSteeringMessage, type LionRunStatus, type LionSummary, type LionPartialEvidence, type LionTerminalDiagnostic, type LionTerminalDiagnosticReason, } from "./schema.ts"; import { MAX_PROGRESS_TEXT } from "./lifecycle.ts"; const VERSION = 1; const STATUS_SET = new Set(LION_RUN_STATUSES); const PROGRESS_EVENT_SET = new Set(LION_PROGRESS_EVENTS); const CANCEL_DELIVERY_STATUS_SET = new Set(LION_CANCEL_DELIVERY_STATUSES); const STEERING_STATUS_SET = new Set(LION_STEERING_STATUSES); const TERMINAL_DIAGNOSTIC_REASON_SET = new Set(LION_TERMINAL_DIAGNOSTIC_REASONS); const DEFAULT_RECONCILE_GRACE_MS = 30_000; const MAX_STEERING_MESSAGE_CHARS = 4_000; const MAX_OPEN_STEERING_MESSAGES = 100; const MAX_TERMINAL_STEERING_HISTORY = 100; const MAX_TERMINAL_DIAGNOSTIC_TAIL_CHARS = 4_000; const OPEN_STEERING_STATUSES = new Set(["queued", "pending_delivery", "delivering"]); function now(): string { return new Date().toISOString(); } function clone(x: T): T { return JSON.parse(JSON.stringify(x)) as T; } function normalizeStringList(xs: unknown): string[] { return Array.isArray(xs) ? xs.filter((x): x is string => typeof x === "string") : []; } function normalizeActiveTools(xs: unknown): string[] { if (!Array.isArray(xs)) return []; const names = new Set(); for (const value of xs) { if (typeof value !== "string") continue; const name = value.trim().slice(0, MAX_ACTIVE_TOOL_NAME_CHARS); if (name) names.add(name); if (names.size >= MAX_ACTIVE_TOOL_NAMES) break; } return [...names]; } export function canTransition(from: LionRunStatus, to: LionRunStatus): boolean { if (from === to) return true; if (from === "queued") return to === "running" || to === "aborted"; if (from === "running") return to === "completed" || to === "blocked" || to === "failed" || to === "aborted"; return false; } export interface CreateRunInput { agent_id?: string; task_id?: string | null; objective: string; context?: string; model?: string | null; model_role?: LionModelRole | null; runner_mode?: LionRunnerMode | null; tools?: string[] | null; start?: boolean; } export interface FinishRunInput { output: string; report?: LionReport | null; status?: LionRunStatus; error?: string | null; /** Structured terminal diagnostic for report-less terminalizations; persisted additively. */ terminal?: LionTerminalDiagnostic | null; } export type UpdateProgressInput = Partial> & { last_event_at?: string; }; export type UpdateControlInput = Partial; export interface CancelRunResult { run: LionRun; signal?: "SIGTERM"; pid?: number; pgid?: number | null; already_terminal?: boolean; } export interface ReconcileControlsOptions { active_run_refs?: Iterable>; /** Restrict reconciliation to these exact run incarnations. */ target_run_refs?: Iterable>; /** Run ids protected by a different exact process-local owner during a fenced operation. */ protected_run_ids?: Iterable; now_ms?: number; stale_after_ms?: number; /** Observational lookup only; never signaling authority. Null means unverifiable. */ get_process_identity?: (pid: number) => string | null; } export interface SteerRunResult { run: LionRun; message: LionSteeringMessage; accepted: boolean; } export interface SteerRunOptions { liveDeliveryAvailable?: boolean; reason?: string | null; } export interface ListFilter { status?: LionRunStatus; agent_id?: string; task_id?: string; limit?: number; } export class LionLedger { readonly project?: string; private runsById: Map; constructor(project?: string, runs: LionRun[] = []) { this.project = project; this.runsById = new Map(runs.map((r) => [r.id, clone(r)])); } create(input: CreateRunInput): LionRun { const objective = (input.objective ?? "").trim(); if (!objective && !input.task_id) throw new LionError("invalid_arg", "run requires objective or task_id"); const id = this.nextId(); const ts = now(); const run: LionRun = { id, incarnation_id: randomUUID(), agent_id: (input.agent_id?.trim() || `lion-${id.replace(/^run-/, "")}`).toLowerCase(), status: input.start === false ? "queued" : "running", task_id: input.task_id ?? null, objective: objective || `Work AXON task ${input.task_id}`, context: input.context ?? "", model: input.model ?? null, model_role: input.model_role ?? null, runner_mode: input.runner_mode ?? "json", tools: input.tools ? [...input.tools] : null, started_at: ts, updated_at: ts, finished_at: null, duration_ms: null, output: null, report: null, progress: null, control: null, steering_messages: [], error: null, }; this.runsById.set(id, run); return clone(run); } start(id: string): LionRun { const r = this.require(id); if (r.status !== "queued") throw new LionError("invalid_transition", `cannot start ${r.id} from ${r.status}`); this.transition(r, "running"); const ts = now(); for (const msg of r.steering_messages ?? []) { if (msg.status === "queued") { msg.status = "applied"; msg.applied_at = ts; } } r.started_at = ts; r.updated_at = ts; return clone(r); } finish(id: string, input: FinishRunInput): LionRun { const r = this.require(id); const report = coerceLionReport(input.report); const requestedStatus = input.status ?? statusFromReport(report, input.error); // Defense in depth: direct ledger callers cannot bypass the worker contract // and mark a report-less or malformed-report run completed. const status = requestedStatus === "completed" && !report ? "failed" : requestedStatus; this.transition(r, status); const ts = now(); this.failOpenSteeringForRun(r, `run finalized as ${status}`, ts); r.output = input.output; r.report = report; r.error = input.error ?? null; r.terminal_diagnostic = coerceTerminalDiagnostic(input.terminal); r.finished_at = ts; r.updated_at = ts; r.duration_ms = Math.max(0, Date.parse(ts) - Date.parse(r.started_at)); return clone(r); } /** * The sole exact-incarnation terminal commit primitive. Cancellation is read * and applied inside the same ledger mutation as finalization, so a success * result can never overwrite a cancellation that committed first. */ finalizeIfCurrent(id: string, incarnationId: string | null | undefined, input: FinishRunInput): { run: LionRun | undefined; committed: boolean } { const current = this.runsById.get(id); if (!current || (current.incarnation_id ?? null) !== (incarnationId ?? null) || isTerminalLionStatus(current.status)) { return { run: current ? clone(current) : undefined, committed: false }; } const cancellation = current.control?.cancel_requested_at; const finalInput = cancellation ? { // Cancellation remains authoritative and discards raw output/report, while // retaining any bounded terminal evidence already collected. output: "", report: null, status: "aborted" as const, error: current.control?.cancel_reason ? `Cancelled: ${current.control.cancel_reason}` : "Cancelled", terminal: input.terminal ?? null, } : input; return { run: this.finish(id, finalInput), committed: true }; } updateProgress(id: string, input: UpdateProgressInput): LionRun { const r = this.require(id); if (!isActiveLionStatus(r.status)) { throw new LionError("invalid_transition", `cannot update progress for ${r.id} while ${r.status}`); } const previous = r.progress ?? defaultProgress(); const ts = input.last_event_at ?? now(); r.progress = { event: isProgressEvent(input.event) ? input.event : previous.event, activity: trimText(input.activity ?? previous.activity), active_tools: input.active_tools ? normalizeActiveTools(input.active_tools) : previous.active_tools, tool_uses: typeof input.tool_uses === "number" ? Math.max(0, Math.floor(input.tool_uses)) : previous.tool_uses, turn_count: typeof input.turn_count === "number" ? Math.max(0, Math.floor(input.turn_count)) : previous.turn_count, token_total: typeof input.token_total === "number" ? Math.max(0, Math.floor(input.token_total)) : (input.token_total === null ? null : previous.token_total), last_text: typeof input.last_text === "string" ? trimText(input.last_text) : (input.last_text === null ? null : previous.last_text), last_event_at: ts, }; r.updated_at = ts; return clone(r); } updateProgressIfCurrent(id: string, incarnationId: string | null | undefined, input: UpdateProgressInput): { run: LionRun | undefined; committed: boolean } { const current = this.runsById.get(id); if (!current || (current.incarnation_id ?? null) !== (incarnationId ?? null) || !isActiveLionStatus(current.status)) { return { run: current ? clone(current) : undefined, committed: false }; } return { run: this.updateProgress(id, input), committed: true }; } /** Storage-only fold used after exact-incarnation sidecar validation. */ foldProgressIfCurrent(id: string, incarnationId: string, progress: LionProgressSnapshot): { run: LionRun | undefined; committed: boolean } { const current = this.runsById.get(id); if (!current || current.incarnation_id !== incarnationId) return { run: current ? clone(current) : undefined, committed: false }; current.progress = clone(progress); if (isActiveLionStatus(current.status) && progress.last_event_at > current.updated_at) current.updated_at = progress.last_event_at; return { run: clone(current), committed: true }; } updateControl(id: string, input: UpdateControlInput): LionRun { const r = this.require(id); if (!isActiveLionStatus(r.status)) throw new LionError("invalid_transition", `cannot update control for ${r.id} while ${r.status}`); const ts = now(); r.control = { ...(r.control ?? {}), ...input, last_seen_at: input.last_seen_at ?? ts }; r.updated_at = ts; return clone(r); } updateControlIfCurrent(id: string, incarnationId: string | null | undefined, input: UpdateControlInput): { run: LionRun | undefined; committed: boolean } { const current = this.runsById.get(id); if (!current || (current.incarnation_id ?? null) !== (incarnationId ?? null) || !isActiveLionStatus(current.status)) { return { run: current ? clone(current) : undefined, committed: false }; } return { run: this.updateControl(id, input), committed: true }; } requestCancel(id: string, reason?: string | null): CancelRunResult { const r = this.require(id); const ts = now(); if (isTerminalLionStatus(r.status)) { return { run: clone(r), already_terminal: true }; } const deliveryStatus = r.control?.cancel_delivery_status === "delivered" ? "delivered" : r.status === "running" ? "requested" : "not_needed"; r.control = { ...(r.control ?? {}), cancel_requested_at: r.control?.cancel_requested_at ?? ts, cancel_reason: reason ?? r.control?.cancel_reason ?? null, cancel_signal: "SIGTERM", cancel_delivery_status: deliveryStatus, last_seen_at: r.control?.last_seen_at ?? null }; if (r.status === "queued") { this.transition(r, "aborted"); this.failOpenSteeringForRun(r, "run cancelled before queued steering could be applied", ts); r.error = reason ? `Cancelled before start: ${reason}` : "Cancelled before start"; r.finished_at = ts; r.duration_ms = Math.max(0, Date.parse(ts) - Date.parse(r.started_at)); } r.updated_at = ts; return { run: clone(r), signal: r.status === "running" ? "SIGTERM" : undefined, pid: r.control.pid ?? undefined, pgid: r.control.pgid ?? null }; } markCancelDelivery(id: string, status: LionCancelDeliveryStatus, error?: string | null): LionRun { const r = this.require(id); if (r.control?.cancel_delivery_status === "delivered" && status !== "delivered") return clone(r); const ts = now(); r.control = { ...(r.control ?? {}), cancel_delivery_status: status, cancel_delivered_at: status === "delivered" ? ts : r.control?.cancel_delivered_at ?? null, cancel_delivery_error: status === "delivered" ? null : error ?? status, last_seen_at: r.control?.last_seen_at ?? null, }; r.updated_at = ts; return clone(r); } markCancelDeliveryIfCurrent(id: string, incarnationId: string | null | undefined, status: LionCancelDeliveryStatus, error?: string | null): { run: LionRun; committed: boolean } { const r = this.require(id); if ((r.incarnation_id ?? null) !== (incarnationId ?? null)) return { run: clone(r), committed: false }; return { run: this.markCancelDelivery(id, status, error), committed: true }; } steer(id: string, message: string, options: SteerRunOptions = {}): SteerRunResult { const r = this.require(id); const text = message.trim().slice(0, MAX_STEERING_MESSAGE_CHARS); if (!text) throw new LionError("invalid_arg", "steer requires non-empty message"); const ts = now(); const msg: LionSteeringMessage = { id: this.nextSteeringId(r), message: text, status: "queued", created_at: ts, applied_at: null, delivery_attempted_at: null, delivered_at: null, rejected_at: null, reason: null, }; if (r.status === "running") { if (options.liveDeliveryAvailable && r.runner_mode === "rpc") { msg.status = "pending_delivery"; msg.reason = options.reason ?? "queued for live RPC delivery"; } else { msg.status = "rejected_running"; msg.rejected_at = ts; msg.reason = options.reason ?? (r.runner_mode === "rpc" ? "rpc steering channel is not attached to a live worker" : "running json subprocess backend does not support live steering"); } } else if (r.status !== "queued") { msg.status = "rejected_terminal"; msg.rejected_at = ts; msg.reason = options.reason ?? `cannot steer terminal run ${r.status}`; } r.steering_messages ??= []; if (OPEN_STEERING_STATUSES.has(msg.status) && r.steering_messages.filter((entry) => OPEN_STEERING_STATUSES.has(entry.status)).length >= MAX_OPEN_STEERING_MESSAGES) { throw new LionError("invalid_arg", `run ${r.id} already has the maximum ${MAX_OPEN_STEERING_MESSAGES} open steering messages`); } r.steering_messages.push(msg); this.compactSteeringHistory(r); r.updated_at = ts; return { run: clone(r), message: clone(msg), accepted: msg.status === "queued" || msg.status === "pending_delivery" }; } hasPendingSteering(id: string): boolean { const r = this.require(id); return r.status === "running" && r.runner_mode === "rpc" && (r.steering_messages ?? []).some((msg) => msg.status === "pending_delivery"); } hasPendingSteeringIfCurrent(id: string, incarnationId: string | null | undefined): boolean { const r = this.runsById.get(id); return Boolean(r && (r.incarnation_id ?? null) === (incarnationId ?? null) && r.status === "running" && r.runner_mode === "rpc" && (r.steering_messages ?? []).some((msg) => msg.status === "pending_delivery")); } reservePendingSteering(id: string, limit = 10): LionSteeringMessage[] { const r = this.require(id); if (r.status !== "running" || r.runner_mode !== "rpc") return []; const ts = now(); const out: LionSteeringMessage[] = []; for (const msg of r.steering_messages ?? []) { if (msg.status !== "pending_delivery") continue; msg.status = "delivering"; msg.delivery_attempted_at = ts; msg.reason = "delivering via RPC steer"; out.push(clone(msg)); if (out.length >= limit) break; } if (out.length) r.updated_at = ts; return out; } reservePendingSteeringIfCurrent(id: string, incarnationId: string | null | undefined, limit = 10): LionSteeringMessage[] { const r = this.runsById.get(id); if (!r || (r.incarnation_id ?? null) !== (incarnationId ?? null)) return []; return this.reservePendingSteering(id, limit); } markSteeringDelivered(id: string, steeringId: string): LionRun { const r = this.require(id); const msg = this.requireSteering(r, steeringId); const ts = now(); msg.status = "delivered"; msg.delivered_at = ts; msg.reason = "delivered via RPC steer"; this.compactSteeringHistory(r); r.updated_at = ts; return clone(r); } markSteeringDeliveredIfCurrent(id: string, incarnationId: string | null | undefined, steeringId: string): { run: LionRun | undefined; committed: boolean } { const r = this.runsById.get(id); if (!r || (r.incarnation_id ?? null) !== (incarnationId ?? null)) return { run: r ? clone(r) : undefined, committed: false }; return { run: this.markSteeringDelivered(id, steeringId), committed: true }; } markSteeringFailed(id: string, steeringId: string, reason: string): LionRun { const r = this.require(id); const msg = this.requireSteering(r, steeringId); const ts = now(); msg.status = "delivery_failed"; msg.rejected_at = ts; msg.reason = reason; this.compactSteeringHistory(r); r.updated_at = ts; return clone(r); } markSteeringFailedIfCurrent(id: string, incarnationId: string | null | undefined, steeringId: string, reason: string): { run: LionRun | undefined; committed: boolean } { const r = this.runsById.get(id); if (!r || (r.incarnation_id ?? null) !== (incarnationId ?? null)) return { run: r ? clone(r) : undefined, committed: false }; return { run: this.markSteeringFailed(id, steeringId, reason), committed: true }; } settleSteeringBatchIfCurrent(id: string, incarnationId: string | null | undefined, outcomes: Array<{ steering_id: string; delivered: boolean; reason?: string }>): { run: LionRun | undefined; committed: boolean } { const r = this.runsById.get(id); if (!r || (r.incarnation_id ?? null) !== (incarnationId ?? null)) return { run: r ? clone(r) : undefined, committed: false }; const ts = now(); for (const outcome of outcomes) { const msg = this.requireSteering(r, outcome.steering_id); if (outcome.delivered) { msg.status = "delivered"; msg.delivered_at = ts; msg.reason = "delivered via RPC steer"; } else { msg.status = "delivery_failed"; msg.rejected_at = ts; msg.reason = outcome.reason ?? "RPC steer failed"; } } if (outcomes.length) { this.compactSteeringHistory(r); r.updated_at = ts; } return { run: clone(r), committed: true }; } failOpenSteering(id: string, reason: string): LionRun { const r = this.require(id); const ts = now(); if (this.failOpenSteeringForRun(r, reason, ts)) r.updated_at = ts; return clone(r); } failOpenSteeringIfCurrent(id: string, incarnationId: string | null | undefined, reason: string): { run: LionRun | undefined; committed: boolean } { const r = this.runsById.get(id); if (!r || (r.incarnation_id ?? null) !== (incarnationId ?? null)) return { run: r ? clone(r) : undefined, committed: false }; return { run: this.failOpenSteering(id, reason), committed: true }; } reconcileControls(isAlive: (pid: number) => boolean, options: ReconcileControlsOptions = {}): LionRun[] { const changed: LionRun[] = []; const active = new Set(Array.from(options.active_run_refs ?? [], (ref) => JSON.stringify([ref.id, ref.incarnation_id ?? null]))); const targets = options.target_run_refs ? new Set(Array.from(options.target_run_refs, (ref) => JSON.stringify([ref.id, ref.incarnation_id ?? null]))) : undefined; const protectedRunIds = new Set(options.protected_run_ids ?? []); const nowMs = options.now_ms ?? Date.now(); const staleAfterMs = options.stale_after_ms ?? DEFAULT_RECONCILE_GRACE_MS; for (const r of this.runsById.values()) { if (r.status !== "running" || protectedRunIds.has(r.id)) continue; if (targets && !targets.has(JSON.stringify([r.id, r.incarnation_id ?? null]))) continue; if (active.has(JSON.stringify([r.id, r.incarnation_id ?? null]))) continue; if (!isReconcileStale(r, nowMs, staleAfterMs)) continue; const pid = r.control?.pid; const pending = r.control?.cleanup_pending; if (pending) { // A cleanup handoff was durably observed. Any missing or inconsistent // observation fails closed; persisted metadata grants no authority to // signal or reattach, and absence of proof is never proof of exit. if ((pending.incarnation_id ?? null) !== (r.incarnation_id ?? null) || typeof pid !== "number" || pending.pid !== pid || (pending.process_identity ?? null) !== (r.control?.process_identity ?? null)) continue; } if (typeof pid === "number" && isAlive(pid)) { const expectedIdentity = r.control?.process_identity; const observedIdentity = expectedIdentity && options.get_process_identity ? options.get_process_identity(pid) : null; if (!expectedIdentity || !observedIdentity || observedIdentity === expectedIdentity) continue; // Same numeric PID now belongs to another process. This proves the // original child is gone, but never grants authority over the replacement. } const ts = new Date(nowMs).toISOString(); const terminalStatus = r.control?.cancel_requested_at ? "aborted" : "failed"; this.transition(r, terminalStatus); this.failOpenSteeringForRun(r, `run reconciled as ${terminalStatus}`, ts); r.error = r.control?.cancel_requested_at ? (r.control.cancel_reason ? `Cancelled: ${r.control.cancel_reason}` : "Cancelled") : typeof pid === "number" ? "Subprocess is no longer running" : "Active owner was lost before process metadata attached"; r.finished_at = ts; r.updated_at = ts; r.duration_ms = Math.max(0, Date.parse(ts) - Date.parse(r.started_at)); r.control = { ...(r.control ?? {}), reconciled_at: ts, last_seen_at: ts }; changed.push(clone(r)); } return changed; } delete(id: string): LionRun { const r = this.require(id); if (isActiveLionStatus(r.status)) { throw new LionError("invalid_transition", `cannot delete nonterminal run ${r.id} while ${r.status}`); } this.runsById.delete(id); return clone(r); } get(id: string): LionRun | undefined { const r = this.runsById.get(id); return r ? clone(r) : undefined; } /** Exact active references for storage overlays; deliberately avoids cloning/sorting history. */ activeExactRefs(): Array<{ id: string; incarnation_id: string }> { const refs: Array<{ id: string; incarnation_id: string }> = []; for (const run of this.runsById.values()) { if (isActiveLionStatus(run.status) && run.incarnation_id) refs.push({ id: run.id, incarnation_id: run.incarnation_id }); } return refs; } all(): LionRun[] { return Array.from(this.runsById.values()) .map(clone) .sort((a, b) => b.started_at.localeCompare(a.started_at)); } list(filter: ListFilter = {}): LionRun[] { let runs = this.all(); if (filter.status) runs = runs.filter((r) => r.status === filter.status); if (filter.agent_id) runs = runs.filter((r) => r.agent_id === filter.agent_id); if (filter.task_id) runs = runs.filter((r) => r.task_id === filter.task_id); const limit = filter.limit ?? 20; return limit > 0 ? runs.slice(0, limit) : runs; } summary(limit = 10): LionSummary { const all = this.all(); const by_status: Partial> = {}; for (const r of all) by_status[r.status] = (by_status[r.status] ?? 0) + 1; return { total: all.length, by_status, running: all.filter((r) => r.status === "running" || r.status === "queued"), recent: limit > 0 ? all.slice(0, limit) : all, }; } toJSON(): LionFile { const ts = now(); const runs: Record = {}; for (const r of this.runsById.values()) runs[r.id] = clone(r); return { version: VERSION, project: this.project, updated_at: ts, runs }; } static fromJSON(raw: unknown): LionLedger { const obj = isObject(raw) ? raw : {}; const runsObj = isObject(obj.runs) ? obj.runs : {}; const runs: LionRun[] = []; for (const [id, value] of Object.entries(runsObj)) { const r = coerceRun(id, value); if (r) runs.push(r); } return new LionLedger(typeof obj.project === "string" ? obj.project : undefined, runs); } private require(id: string): LionRun { const r = this.runsById.get(id); if (!r) throw new LionError("not_found", `run ${id} not found`); return r; } private compactSteeringHistory(run: LionRun): void { const messages = run.steering_messages ?? []; const open = messages.filter((message) => OPEN_STEERING_STATUSES.has(message.status)); const terminal = messages.filter((message) => !OPEN_STEERING_STATUSES.has(message.status)); run.steering_messages = [...terminal.slice(-MAX_TERMINAL_STEERING_HISTORY), ...open]; } private requireSteering(run: LionRun, steeringId: string): LionSteeringMessage { const msg = (run.steering_messages ?? []).find((m) => m.id === steeringId); if (!msg) throw new LionError("not_found", `steering message ${steeringId} not found on ${run.id}`); return msg; } private failOpenSteeringForRun(run: LionRun, reason: string, ts: string): boolean { let changed = false; for (const msg of run.steering_messages ?? []) { if (!OPEN_STEERING_STATUSES.has(msg.status)) continue; msg.status = "delivery_failed"; msg.rejected_at = ts; msg.reason = reason; changed = true; } if (changed) this.compactSteeringHistory(run); return changed; } private transition(r: LionRun, to: LionRunStatus): void { if (!canTransition(r.status, to)) { throw new LionError("invalid_transition", `cannot transition ${r.id} from ${r.status} to ${to}`); } r.status = to; } private nextSteeringId(run: LionRun): string { let max = 0; for (const msg of run.steering_messages ?? []) { const m = /^steer-(\d+)$/.exec(msg.id); if (m) max = Math.max(max, Number(m[1])); } return `steer-${String(max + 1).padStart(3, "0")}`; } private nextId(): string { let max = 0; for (const id of this.runsById.keys()) { const m = /^run-(\d+)$/.exec(id); if (m) max = Math.max(max, Number(m[1])); } return `run-${String(max + 1).padStart(3, "0")}`; } } function statusFromReport(report: LionReport | null | undefined, error?: string | null): LionRunStatus { if (error) return "failed"; // Invariant: a missing/unusable report can never resolve to completed. Only a // valid report (completed/partial outcome) completes; absence is a failure. if (!report) return "failed"; if (report.outcome === "completed" || report.outcome === "partial") return "completed"; if (report.outcome === "blocked") return "blocked"; return "failed"; } function isModelRole(value: unknown): value is LionModelRole { return value === "implementation" || value === "review" || value === "default"; } function isRunnerMode(value: unknown): value is LionRunnerMode { return value === "json" || value === "rpc"; } function isProgressEvent(value: unknown): value is LionProgressEvent { return typeof value === "string" && PROGRESS_EVENT_SET.has(value); } function trimText(value: string): string { return value.length > MAX_PROGRESS_TEXT ? value.slice(-MAX_PROGRESS_TEXT) : value; } function isReconcileStale(run: LionRun, nowMs: number, staleAfterMs: number): boolean { // Cancellation and ledger bookkeeping are not owner heartbeats. Using updated_at // here lets repeated control requests postpone owner-loss reconciliation forever. const candidates = [run.control?.last_seen_at, run.started_at] .filter((value): value is string => typeof value === "string") .map((value) => Date.parse(value)) .filter((value) => Number.isFinite(value)); const newest = candidates.length ? Math.max(...candidates) : 0; return nowMs - newest >= staleAfterMs; } function defaultProgress(): LionProgressSnapshot { return { event: "heartbeat", activity: "running…", active_tools: [], tool_uses: 0, turn_count: 0, token_total: null, last_text: null, last_event_at: now(), }; } function coerceRun(id: string, value: unknown): LionRun | null { if (!isObject(value)) return null; const status = typeof value.status === "string" && STATUS_SET.has(value.status) ? (value.status as LionRunStatus) : "failed"; const started = typeof value.started_at === "string" ? value.started_at : now(); const updated = typeof value.updated_at === "string" ? value.updated_at : started; return { id: typeof value.id === "string" ? value.id : id, incarnation_id: typeof value.incarnation_id === "string" ? value.incarnation_id : null, agent_id: typeof value.agent_id === "string" ? value.agent_id : "lion-unknown", status, task_id: typeof value.task_id === "string" ? value.task_id : null, objective: typeof value.objective === "string" ? value.objective : "", context: typeof value.context === "string" ? value.context : "", model: typeof value.model === "string" ? value.model : null, model_role: isModelRole(value.model_role) ? value.model_role : null, runner_mode: isRunnerMode(value.runner_mode) ? value.runner_mode : "json", tools: Array.isArray(value.tools) ? normalizeStringList(value.tools) : null, started_at: started, updated_at: updated, finished_at: typeof value.finished_at === "string" ? value.finished_at : null, duration_ms: typeof value.duration_ms === "number" ? value.duration_ms : null, output: typeof value.output === "string" ? value.output : null, report: coerceReport(value.report), terminal_diagnostic: coerceTerminalDiagnostic(value.terminal_diagnostic), progress: coerceProgress(value.progress), control: coerceControl(value.control, typeof value.id === "string" ? value.id : id), steering_messages: coerceSteeringMessages(value.steering_messages), error: typeof value.error === "string" ? value.error : null, }; } function coerceTerminalDiagnostic(value: unknown): LionTerminalDiagnostic | null { if (!isObject(value)) return null; const reason = typeof value.reason === "string" && TERMINAL_DIAGNOSTIC_REASON_SET.has(value.reason) ? (value.reason as LionTerminalDiagnosticReason) : null; if (!reason) return null; return { reason, stdout_tail: optionalBoundedString(value.stdout_tail), stderr_tail: optionalBoundedString(value.stderr_tail), output_tail: optionalBoundedString(value.output_tail), event_count: optionalNonNegativeInt(value.event_count), message_count: optionalNonNegativeInt(value.message_count), turn_count: optionalNonNegativeInt(value.turn_count), tool_uses: optionalNonNegativeInt(value.tool_uses), malformed_line_count: optionalNonNegativeInt(value.malformed_line_count), exit_code: optionalInt(value.exit_code), signal: optionalBoundedString(value.signal), timed_out: typeof value.timed_out === "boolean" ? value.timed_out : null, git_head: optionalBoundedString(value.git_head), git_status: optionalBoundedString(value.git_status), last_tool_action: optionalBoundedString(value.last_tool_action), partial_evidence: coercePartialEvidence(value.partial_evidence), report_attempted: typeof value.report_attempted === "boolean" ? value.report_attempted : null, captured_at: typeof value.captured_at === "string" ? value.captured_at : now(), }; } function optionalBoundedString(value: unknown): string | null { return typeof value === "string" ? sanitizeTerminalDiagnosticText(value) : null; } /** Defense in depth for direct ledger callers that bypass subprocess sanitization. */ function sanitizeTerminalDiagnosticText(value: string): string { const bounded = value .slice(0, MAX_TERMINAL_DIAGNOSTIC_TAIL_CHARS) .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " "); return redactLionDiagnosticText(bounded); } function optionalNonNegativeInt(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : null; } function optionalInt(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : null; } function coercePartialEvidence(value: unknown): LionPartialEvidence | null { if (!isObject(value) || value.observational !== true) return null; const changed_files = normalizeStringList(value.changed_files) .map(normalizeEvidencePath) .filter((item): item is string => item !== null) .slice(0, MAX_LION_REPORT_ITEMS) .map(sanitizeTerminalDiagnosticText); const tests_run = normalizeStringList(value.tests_run).slice(0, MAX_LION_REPORT_ITEMS); return { changed_files, tests_run: tests_run.map(sanitizeTerminalDiagnosticText), last_tool_action: optionalBoundedString(value.last_tool_action), git_head: optionalBoundedString(value.git_head), git_status: optionalBoundedString(value.git_status), observational: true, }; } function normalizeEvidencePath(value: string): string | null { const path = value.replace(/\\/g, "/"); if (!path || path.startsWith("/") || path === ".." || path.startsWith("../") || path.length > 512 || /[\x00-\x1f\x7f]/.test(path)) return null; return path; } function coerceReport(value: unknown): LionReport | null { return coerceLionReport(value); } function coerceProgress(value: unknown): LionProgressSnapshot | null { if (!isObject(value)) return null; return { event: isProgressEvent(value.event) ? value.event : "heartbeat", activity: typeof value.activity === "string" ? trimText(value.activity) : "running…", active_tools: normalizeActiveTools(value.active_tools), tool_uses: typeof value.tool_uses === "number" ? Math.max(0, Math.floor(value.tool_uses)) : 0, turn_count: typeof value.turn_count === "number" ? Math.max(0, Math.floor(value.turn_count)) : 0, token_total: typeof value.token_total === "number" ? Math.max(0, Math.floor(value.token_total)) : null, last_text: typeof value.last_text === "string" ? trimText(value.last_text) : null, last_event_at: typeof value.last_event_at === "string" ? value.last_event_at : now(), }; } function coerceControl(value: unknown, runId: string): LionControlState | null { if (!isObject(value)) return null; return { pid: typeof value.pid === "number" ? Math.floor(value.pid) : null, pgid: typeof value.pgid === "number" ? Math.floor(value.pgid) : null, process_identity: typeof value.process_identity === "string" ? value.process_identity : null, started_at: typeof value.started_at === "string" ? value.started_at : null, last_seen_at: typeof value.last_seen_at === "string" ? value.last_seen_at : null, cancel_requested_at: typeof value.cancel_requested_at === "string" ? value.cancel_requested_at : null, cancel_reason: typeof value.cancel_reason === "string" ? value.cancel_reason : null, cancel_signal: typeof value.cancel_signal === "string" ? value.cancel_signal : null, cancel_delivery_status: typeof value.cancel_delivery_status === "string" && CANCEL_DELIVERY_STATUS_SET.has(value.cancel_delivery_status) ? value.cancel_delivery_status as LionCancelDeliveryStatus : null, cancel_delivered_at: typeof value.cancel_delivered_at === "string" ? value.cancel_delivered_at : null, cancel_delivery_error: typeof value.cancel_delivery_error === "string" ? value.cancel_delivery_error : null, reconciled_at: typeof value.reconciled_at === "string" ? value.reconciled_at : null, cleanup_pending: coerceCleanupPendingObservation(value.cleanup_pending, runId), }; } function coerceCleanupPendingObservation(value: unknown, runId: string): import("./schema.ts").LionCleanupPendingObservation | null { if (value === null || value === undefined) return null; const malformed = () => new LionError("invalid_arg", `run ${runId} has malformed cleanup_pending observation; delete/reset this clean-slate LION ledger because cleanup liveness cannot be proven and migration is unsupported`); if (!isObject(value)) throw malformed(); const observedAt = value.observed_at; const incarnationId = value.incarnation_id; const pid = value.pid; const pgid = value.pgid; const processIdentity = value.process_identity; if (typeof observedAt !== "string" || !observedAt || !(typeof incarnationId === "string" || incarnationId === null) || typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 || !(typeof pgid === "number" || pgid === null) || !(typeof processIdentity === "string" || processIdentity === null)) { throw malformed(); } return { observed_at: observedAt, incarnation_id: typeof incarnationId === "string" ? incarnationId : null, pid, pgid: typeof pgid === "number" ? Math.floor(pgid) : null, process_identity: typeof processIdentity === "string" ? processIdentity : null, }; } function coerceSteeringMessages(value: unknown): LionSteeringMessage[] { if (!Array.isArray(value)) return []; // Select bounded raw entries before reading or coercing message payloads. Open // messages preserve FIFO delivery order; terminal history keeps the newest. const open: unknown[] = []; const terminal: unknown[] = []; for (const entry of value) { const status = isObject(entry) && typeof entry.status === "string" && STEERING_STATUS_SET.has(entry.status) ? entry.status as LionSteeringMessage["status"] : "rejected_terminal"; if (OPEN_STEERING_STATUSES.has(status)) { if (open.length < MAX_OPEN_STEERING_MESSAGES) open.push(entry); continue; } terminal.push(entry); if (terminal.length > MAX_TERMINAL_STEERING_HISTORY) terminal.shift(); } return [...terminal, ...open] .map(coerceSteering) .filter((entry): entry is LionSteeringMessage => entry !== null); } function coerceSteering(value: unknown): LionSteeringMessage | null { if (!isObject(value) || typeof value.message !== "string") return null; const status = typeof value.status === "string" && STEERING_STATUS_SET.has(value.status) ? value.status as LionSteeringMessage["status"] : "rejected_terminal"; return { id: typeof value.id === "string" ? value.id : "steer-unknown", message: value.message.slice(0, MAX_STEERING_MESSAGE_CHARS), status, created_at: typeof value.created_at === "string" ? value.created_at : now(), applied_at: typeof value.applied_at === "string" ? value.applied_at : null, delivery_attempted_at: typeof value.delivery_attempted_at === "string" ? value.delivery_attempted_at : null, delivered_at: typeof value.delivered_at === "string" ? value.delivered_at : null, rejected_at: typeof value.rejected_at === "string" ? value.rejected_at : null, reason: typeof value.reason === "string" ? value.reason : null, }; } function isObject(x: unknown): x is Record { return typeof x === "object" && x !== null; }