UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

747 lines (687 loc) • 29.8 kB
import { createHash } from "node:crypto"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { ContextItem } from "./context.ts"; import { KxmConfigError, loadKxmProject, lookupKxmResource, kxmCanonicalJson, type JsonObject, type JsonValue, type KxmConfigOptions, type KxmProjectBundle, } from "./project-config.ts"; import { foldKxmRunState, isTerminalRunStatus, KXM_RUN_STATE_SCHEMA, kxmFoldPanelAttemptIds, type KxmRunState } from "./engine-fold.ts"; import { verifyKxmGateEvidence } from "./engine-evidence.ts"; import { loadKxmRunPlanEnvelope } from "./engine-plan.ts"; import { registerKxmRuntimeHandle, resolveKxmGateHold, unregisterKxmRuntimeHandle, kxmAttemptControllers, } from "./runtime-owner.ts"; import { KXM_ABSENT_MEMORY_REVISION, KXM_RUN_EVENT_SCHEMA, KxmRunEventStore, KxmRuntimeRegistry, newKxmCommandId, newKxmEventId, newKxmRunId, runtimeError, kxmRuntimePaths, type KxmRunEvent, type KxmRunRecord, type KxmRunStatus, } from "./runtime-store.ts"; /* ------------------------------------------------------------------ * * Policy revisions derived deterministically from the bundle * ------------------------------------------------------------------ */ export interface KxmPolicyRevisions { configRevision: string; memoryRevision: string; executorPolicyRevision: string; toolPolicyRevision: string; } export interface KxmMemoryRevisionOptions { promotedState?: readonly ContextItem[]; memoryDir?: string; skillsDir?: string; } function compareCodeUnits(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function sha256Of(input: string): string { return `sha256:${createHash("sha256").update(input, "utf8").digest("hex")}`; } function objectValue(value: JsonValue | undefined): JsonObject | undefined { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : undefined; } /** Executor policy: the union of executor identities referenced anywhere in the bundle. */ export function kxmExecutorPolicyRevision(bundle: KxmProjectBundle): string { const executors = new Set<string>(); const defaultExecutor = bundle.project.value.defaultExecutor; if (typeof defaultExecutor === "string") executors.add(defaultExecutor); for (const agent of bundle.agents.values()) { const executor = agent.value.executor; if (typeof executor === "string") executors.add(executor); } return sha256Of(kxmCanonicalJson([...executors].sort() as JsonValue)); } /** Tool policy: canonical agent/step tool ceilings across the bundle. */ export function kxmToolPolicyRevision(bundle: KxmProjectBundle): string { const policies: JsonObject[] = []; for (const agent of [...bundle.agents.values()].sort((left, right) => String(left.id).localeCompare(String(right.id)))) { policies.push({ agent: agent.id ?? "unknown", tools: (objectValue(agent.value.tools) ?? null) as JsonValue }); } for (const workflow of [...bundle.workflows.values()].sort((left, right) => String(left.id).localeCompare(String(right.id)))) { const steps = Array.isArray(workflow.value.steps) ? workflow.value.steps : []; for (const step of steps) { const record = objectValue(step); if (record?.tools !== undefined) { policies.push({ workflow: workflow.id ?? "unknown", step: record.id ?? null, tools: (objectValue(record.tools) ?? null) as JsonValue }); } } } return sha256Of(kxmCanonicalJson({ policies, gateRegistry: bundle.gateRegistry?.value ?? null })); } function collectMemoryFiles(dir: string, baseDir: string, ignoreSubdirs: Set<string> = new Set()): { relPath: string; fullPath: string }[] { if (!existsSync(dir)) return []; const entries = readdirSync(dir, { withFileTypes: true }); const results: { relPath: string; fullPath: string }[] = []; for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { if (ignoreSubdirs.has(entry.name)) continue; results.push(...collectMemoryFiles(fullPath, baseDir, ignoreSubdirs)); } else if (entry.isFile()) { const relPath = fullPath.slice(baseDir.length + 1).replace(/\\/g, "/"); results.push({ relPath, fullPath }); } } return results; } /** * Compute the memory revision pinned at run creation as the SHA-256 hash of the * Git-authored set (.kxm/memory, excluding candidates; .kxm/skills/promoted) plus * the promoted-state snapshot. */ export function computeKxmMemoryRevision( bundle: Pick<KxmProjectBundle, "projectRoot">, options: KxmMemoryRevisionOptions = {}, ): string { const hash = createHash("sha256"); // 1. Git-authored memory files (.kxm/memory), excluding candidates const memoryDir = options.memoryDir ?? join(bundle.projectRoot, ".kxm", "memory"); const memoryFiles = collectMemoryFiles(memoryDir, memoryDir, new Set(["candidates"])) .sort((left, right) => compareCodeUnits(left.relPath, right.relPath)); for (const file of memoryFiles) { hash.update(`memory:${file.relPath}\0`, "utf8"); hash.update(readFileSync(file.fullPath)); hash.update("\0", "utf8"); } // 2. Promoted skills (.kxm/skills/promoted) const skillsDir = options.skillsDir ?? join(bundle.projectRoot, ".kxm", "skills"); const promotedSkillsDir = join(skillsDir, "promoted"); const skillFiles = collectMemoryFiles(promotedSkillsDir, promotedSkillsDir) .sort((left, right) => compareCodeUnits(left.relPath, right.relPath)); for (const file of skillFiles) { hash.update(`skill:${file.relPath}\0`, "utf8"); hash.update(readFileSync(file.fullPath)); hash.update("\0", "utf8"); } // 3. Promoted-state snapshot if (options.promotedState && options.promotedState.length > 0) { const currentItems = options.promotedState .filter((item) => !item.status || item.status === "current") .slice() .sort((left, right) => compareCodeUnits(left.id, right.id)); for (const item of currentItems) { hash.update(`state:${item.id}\0`, "utf8"); hash.update(kxmCanonicalJson(item as unknown as JsonValue), "utf8"); hash.update("\0", "utf8"); } } return `ctxrev_${hash.digest("hex")}`; } export function checkMemoryRevisionDrift( pinnedRevision: string, bundle: KxmProjectBundle, options?: KxmMemoryRevisionOptions, ): { drifted: boolean; currentRevision: string; pinnedRevision: string } { const currentRevision = computeKxmMemoryRevision(bundle, options); return { drifted: currentRevision !== pinnedRevision, currentRevision, pinnedRevision, }; } export function kxmPolicyRevisions( bundle: KxmProjectBundle, options?: KxmMemoryRevisionOptions, ): KxmPolicyRevisions { return { configRevision: bundle.configRevision, memoryRevision: computeKxmMemoryRevision(bundle, options), executorPolicyRevision: kxmExecutorPolicyRevision(bundle), toolPolicyRevision: kxmToolPolicyRevision(bundle), }; } export function kxmDeclaredRepositoryIds(bundle: KxmProjectBundle): string[] { return [...bundle.repositories.keys()].sort(); } export function kxmDeclaredExecutorIds(bundle: KxmProjectBundle): string[] { return [...new Set([ typeof bundle.project.value.defaultExecutor === "string" ? bundle.project.value.defaultExecutor : undefined, ...[...bundle.agents.values()].map((agent) => agent.value.executor).filter((value): value is string => typeof value === "string"), ].filter((value): value is string => value !== undefined))].sort(); } /** Wall-clock bound for one live agent or moa step when the step omits `timeoutMs`. */ export const KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS = 3_600_000; export const KXM_MIN_AGENT_STEP_TIMEOUT_MS = 60_000; export function kxmProjectAdmissionLimits(bundle: KxmProjectBundle): { maxConcurrentRuns: number; maxRunDurationMs?: number; maxAgentTimeMs?: number; agentStepTimeoutMs: number; } { const limits = objectValue(bundle.project.value.limits); const maxConcurrentRuns = typeof limits?.maxConcurrentRuns === "number" && Number.isInteger(limits.maxConcurrentRuns) && limits.maxConcurrentRuns >= 1 ? limits.maxConcurrentRuns : 1; const declared = limits?.agentStepTimeoutMs; const agentStepTimeoutMs = typeof declared === "number" && Number.isInteger(declared) && declared >= KXM_MIN_AGENT_STEP_TIMEOUT_MS ? declared : KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS; return { maxConcurrentRuns, ...(typeof limits?.maxRunDurationMs === "number" ? { maxRunDurationMs: limits.maxRunDurationMs } : {}), ...(typeof limits?.maxAgentTimeMs === "number" ? { maxAgentTimeMs: limits.maxAgentTimeMs } : {}), agentStepTimeoutMs, }; } /* ------------------------------------------------------------------ * * Run acceptance and lifecycle * ------------------------------------------------------------------ */ export interface KxmRunAcceptanceRequest { commandId?: string; workflowId: string; prompt: string; } export interface KxmRunAcceptance { accepted: boolean; idempotent: boolean; run: KxmRunRecord; event: KxmRunEvent; } export interface KxmRuntimeContext { registry: KxmRuntimeRegistry; projectRoot: string; projectId: string; homeRuntimeId: string; eventStore: KxmRunEventStore; /** Deterministic revision of the loaded project configuration (`sha256:...`). */ readonly configRevision: string; /** * Optional clock for **run-duration budget accounting only**. Budgets are * measured from the log's `runningSince`; this answers "what time is it now" * at the far end of that subtraction so engine tests can exercise a budget * boundary without racing a loaded event loop. * * It is honoured only when `KXM_DETERMINISTIC_TEST_CLOCK=1` is set (see * `budgetNowIso` in `engine.ts`), it is never consulted for event timestamps, * and the Runtime supervisor does not pass one — so no production path can * freeze a budget by supplying a stale clock. */ readonly budgetClock?: () => string; /** * Optional structured logger (the Runtime supervisor's). The engine writes * metadata-only lines here, ids and counts, never task or memory text. */ readonly logger?: (entry: Record<string, unknown>) => void; } const closedRuntimeContexts = new WeakSet<KxmRuntimeContext>(); const runtimeCloseHooks = new WeakMap<KxmRuntimeContext, Set<() => void>>(); export function isKxmRuntimeContextClosed(context: KxmRuntimeContext): boolean { return closedRuntimeContexts.has(context); } export function registerKxmRuntimeCloseHook(context: KxmRuntimeContext, hook: () => void): () => void { if (closedRuntimeContexts.has(context)) { hook(); return () => undefined; } let hooks = runtimeCloseHooks.get(context); if (!hooks) { hooks = new Set(); runtimeCloseHooks.set(context, hooks); } hooks.add(hook); return () => { hooks.delete(hook); }; } /** * Open (and register if needed) the runtime context for a project root. * homeRuntimeId is the accepting Runtime's identity: the supervisor passes its * active runtime id; direct callers must supply one explicitly. An existing * registration always wins and is revalidated immutably. */ export function openKxmRuntimeContext( projectRoot: string, options: KxmConfigOptions & { stateRoot?: string; now?: string; homeRuntimeId: string; budgetClock?: () => string; logger?: (entry: Record<string, unknown>) => void; }, ): KxmRuntimeContext { const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {}); const registry = new KxmRuntimeRegistry(paths.registryDb); try { const bundle = loadKxmProject(projectRoot, options); const projectId = String(bundle.project.value.id); const registration = registry.registerProject({ projectId, projectRoot: bundle.projectRoot, homeRuntimeId: options.homeRuntimeId, configRevision: bundle.configRevision, now: options.now ?? new Date().toISOString(), ...(options.logger ? { logger: options.logger } : {}), }); const eventStore = new KxmRunEventStore(join(paths.projectsDir, registration.projectKey, "run-events.db")); registerKxmRuntimeHandle(eventStore.path); return { registry, projectRoot: bundle.projectRoot, projectId: registration.projectId, homeRuntimeId: registration.homeRuntimeId, eventStore, configRevision: bundle.configRevision, ...(options.budgetClock ? { budgetClock: options.budgetClock } : {}), ...(options.logger ? { logger: options.logger } : {}), }; } catch (error) { registry.close(); throw error; } } export function closeKxmRuntimeContext(context: KxmRuntimeContext): void { if (closedRuntimeContexts.has(context)) return; closedRuntimeContexts.add(context); const hooks = runtimeCloseHooks.get(context); runtimeCloseHooks.delete(context); if (hooks) { for (const hook of hooks) { try { hook(); } catch { // Close must still close stores; observer cleanup is best-effort. } } } unregisterKxmRuntimeHandle(context.eventStore.path); context.eventStore.close(); context.registry.close(); } const RUNTIME_EPOCH_NS = process.hrtime.bigint(); /** Intra-process monotonic nanoseconds as a decimal string, ordered only by sequence. */ export function kxmMonotonicNs(): string { return (process.hrtime.bigint() - RUNTIME_EPOCH_NS).toString(); } export function kxmIncrementMonotonicNs(value: string): string { return (BigInt(value) + 1n).toString(); } export function kxmEventBase(context: KxmRuntimeContext, run: KxmRunRecord, now: string, monotonicNs: string, commandId?: string): Omit<KxmRunEvent, "eventId" | "eventType" | "sequence" | "payload"> { return { schema: KXM_RUN_EVENT_SCHEMA, projectId: context.projectId, runId: run.runId, homeRuntimeId: run.homeRuntimeId, occurredAt: now, recordedAt: now, monotonicNs, configRevision: run.configRevision, memoryRevision: run.memoryRevision, executorPolicyRevision: run.executorPolicyRevision, toolPolicyRevision: run.toolPolicyRevision, ...(commandId !== undefined ? { commandId } : {}), }; } export interface KxmRunAcceptanceOptions { now?: string; monotonicNs?: string; memoryRevision?: string; promotedState?: readonly ContextItem[]; memoryDir?: string; skillsDir?: string; } /** Accept a run idempotently: a repeated commandId returns the prior acceptance. */ export function acceptKxmRun( context: KxmRuntimeContext, bundle: KxmProjectBundle, request: KxmRunAcceptanceRequest, options: KxmRunAcceptanceOptions = {}, ): KxmRunAcceptance { const commandId = request.commandId ?? newKxmCommandId(); const now = options.now ?? new Date().toISOString(); const monotonicNs = options.monotonicNs ?? kxmMonotonicNs(); const workflow = lookupKxmResource(bundle.workflows.values(), request.workflowId, "workflow"); if (!workflow) { throw runtimeError("run_workflow_unknown", ".kxm/workflows", `workflow ${request.workflowId} does not exist in this project`); } const revisions = kxmPolicyRevisions(bundle, options); const memoryRevision = options.memoryRevision ?? revisions.memoryRevision; const promptSha256 = `sha256:${createHash("sha256").update(request.prompt, "utf8").digest("hex")}`; return context.eventStore.transaction(() => { const prior = context.eventStore.command(commandId); if (prior) { if (prior.kind !== "run.accept") { throw runtimeError("run_command_conflict", commandId, `command ${commandId} was already used for ${prior.kind}`); } const run = context.eventStore.run(prior.runId); if (!run) throw runtimeError("run_command_corrupt", commandId, "idempotent command references a missing run"); if (run.workflowId !== request.workflowId || run.promptSha256 !== promptSha256) { throw runtimeError("run_command_conflict", commandId, "idempotent retry must repeat the same workflow and prompt"); } const events = context.eventStore.events(run.runId, 0, 1); if (events.length === 0) throw runtimeError("run_command_corrupt", commandId, "idempotent command references a run with no creation event"); return { accepted: true, idempotent: true, run, event: events[0]! }; } const runId = newKxmRunId(); const payload: Record<string, unknown> = { workflowId: request.workflowId, status: "created", promptHash: promptSha256, repositoryIds: kxmDeclaredRepositoryIds(bundle), executorIds: kxmDeclaredExecutorIds(bundle), }; const event: KxmRunEvent = { schema: KXM_RUN_EVENT_SCHEMA, eventId: newKxmEventId(), eventType: "run.created", projectId: context.projectId, runId, homeRuntimeId: context.homeRuntimeId, sequence: 1, commandId, occurredAt: now, recordedAt: now, monotonicNs, configRevision: revisions.configRevision, memoryRevision, executorPolicyRevision: revisions.executorPolicyRevision, toolPolicyRevision: revisions.toolPolicyRevision, payload, }; const run: KxmRunRecord = { runId, projectId: context.projectId, homeRuntimeId: context.homeRuntimeId, workflowId: request.workflowId, promptSha256, status: "created", configRevision: revisions.configRevision, memoryRevision, executorPolicyRevision: revisions.executorPolicyRevision, toolPolicyRevision: revisions.toolPolicyRevision, createdAt: now, updatedAt: now, }; context.eventStore.insertRun(run); context.eventStore.appendEvent(event); context.eventStore.insertCommand({ commandId, runId, kind: "run.accept", result: { runId, homeRuntimeId: context.homeRuntimeId, status: "created" }, recordedAt: now, }); context.eventStore.putRunPrompt(runId, request.prompt); return { accepted: true, idempotent: false, run, event }; }); } /* ------------------------------------------------------------------ * * Projection * ------------------------------------------------------------------ */ export function foldStoredKxmRun(context: KxmRuntimeContext, run: KxmRunRecord): KxmRunState { const events = context.eventStore.events(run.runId, 0, 1_000_000); const planRow = context.eventStore.runPlan(run.runId); if (!planRow && events.some((event) => event.eventType.startsWith("step.") || event.eventType.startsWith("effect.") || event.payload.status === "preparing" || event.payload.status === "running" || event.payload.status === "cancelling")) { throw runtimeError("run_plan_missing", run.runId, "run reached preparing or later without a pinned plan"); } const envelope = planRow ? loadKxmRunPlanEnvelope(context.eventStore, run) : undefined; const state = foldKxmRunState(run, envelope?.plan, events, envelope ? { observationLookup: (attemptId, observationId) => { const row = context.eventStore.gateObservationForAttempt(attemptId); if (!row || row.observationId !== observationId) return undefined; return { completeness: row.completeness }; }, gateKind: (stepId) => { const step = envelope.plan.steps[stepId]; if (!step || step.kind !== "gate") return undefined; return envelope.gates.definitions[step.gate]?.kind; }, projectLimits: envelope.projectLimits, } : {}); if (envelope) verifyKxmGateEvidence(context.eventStore, run, envelope, events, state); assertStoredProjection(context, run); return state; } function storedProjectionSchema(stateJson: string): string | undefined { try { const parsed = JSON.parse(stateJson) as { schema?: unknown }; return typeof parsed.schema === "string" ? parsed.schema : undefined; } catch { return undefined; } } function assertStoredProjection(context: KxmRuntimeContext, run: KxmRunRecord): void { const stored = context.eventStore.runState(run.runId); if (!stored) return; const schema = storedProjectionSchema(stored.state); if (schema !== KXM_RUN_STATE_SCHEMA) { throw runtimeError("run_projection_divergent", run.runId, "stored run_state does not match the folded projection"); } } /** Fold a stored run into its current status with evidence verification. */ export function readKxmRunStatus(context: KxmRuntimeContext, run: KxmRunRecord): KxmRunStatus { return foldStoredKxmRun(context, run).status; } function persistProjection(context: KxmRuntimeContext, run: KxmRunRecord, state: KxmRunState, now: string): KxmRunRecord { const canonical = kxmCanonicalJson(state as unknown as JsonValue); const stored = context.eventStore.runState(run.runId); if (stored && stored.state !== canonical) { throw runtimeError("run_projection_divergent", run.runId, "stored run_state does not match the folded projection"); } if (!stored) { context.eventStore.upsertRunState({ runId: run.runId, lastSequence: context.eventStore.nextSequence(run.runId) - 1, state: canonical, }); } const updated = { ...run, status: state.status, updatedAt: now }; if (run.status !== state.status) context.eventStore.updateRunStatus(run.runId, state.status, now); return updated; } /** Rebuild the stored run row from the event log; returns the projected record. */ export function rebuildKxmRunProjection(context: KxmRuntimeContext, runId: string): KxmRunRecord { const stored = context.eventStore.run(runId); if (!stored) throw runtimeError("run_unknown", runId, "run does not exist in this event store"); const state = foldStoredKxmRun(context, stored); const events = context.eventStore.events(runId, 0, 1_000_000); if (events.length === 0) throw runtimeError("run_events_corrupt", runId, "run has no events"); const last = events[events.length - 1]!; return persistProjection(context, stored, state, last.occurredAt); } /** Read-only projection: folds the event log over the stored record without * persisting. GET paths must not write (no run_state upsert, no status * update, no run_projection_divergent throw) — the event log stays the sole * source of truth and the folded status is returned as-is. */ export function projectKxmRunReadOnly(context: KxmRuntimeContext, runId: string): KxmRunRecord { const stored = context.eventStore.run(runId); if (!stored) throw runtimeError("run_unknown", runId, "run does not exist in this event store"); const state = foldStoredKxmRun(context, stored); const events = context.eventStore.events(runId, 0, 1_000_000); if (events.length === 0) throw runtimeError("run_events_corrupt", runId, "run has no events"); const last = events[events.length - 1]!; return { ...stored, status: state.status, updatedAt: last.occurredAt }; } export function persistKxmRunState(context: KxmRuntimeContext, runId: string, state: KxmRunState, lastSequence: number): void { const stored = context.eventStore.runState(runId); if (stored) { const schema = storedProjectionSchema(stored.state); if (schema !== KXM_RUN_STATE_SCHEMA) { throw runtimeError("run_projection_divergent", runId, "stored run_state does not match the folded projection"); } } context.eventStore.upsertRunState({ runId, lastSequence, state: kxmCanonicalJson(state as unknown as JsonValue), }); } /* ------------------------------------------------------------------ * * Cancel * ------------------------------------------------------------------ */ export interface KxmRunCancelResult { run: KxmRunRecord; idempotent: boolean; events: KxmRunEvent[]; } export function cancelKxmRun( context: KxmRuntimeContext, runId: string, options: { commandId?: string; now?: string; monotonicNs?: string; reason?: string; budget?: { budgetMs: number; elapsedMs: number; source: "workflow" | "project" | "both" }; } = {}, ): KxmRunCancelResult { const commandId = options.commandId ?? newKxmCommandId(); const now = options.now ?? new Date().toISOString(); const monotonicNs = options.monotonicNs ?? kxmMonotonicNs(); const abortControllers: AbortController[] = []; const result = context.eventStore.transaction(() => { const prior = context.eventStore.command(commandId); if (prior) { if (prior.kind !== "run.cancel" || prior.runId !== runId) { throw runtimeError("run_command_conflict", commandId, `command ${commandId} was already used for ${prior.kind}${prior.runId === runId ? "" : " on a different run"}`); } const run = context.eventStore.run(prior.runId); if (!run) throw runtimeError("run_command_corrupt", commandId, "idempotent command references a missing run"); return { run, idempotent: true, events: [] as KxmRunEvent[] }; } const run = context.eventStore.run(runId); if (!run) throw runtimeError("run_unknown", runId, "run does not exist in this event store"); const folded = foldStoredKxmRun(context, run); if (isTerminalRunStatus(folded.status) || folded.status === "cancelling") { context.eventStore.insertCommand({ commandId, runId, kind: "run.cancel", result: { runId, status: folded.status }, recordedAt: now, }); return { run: { ...run, status: folded.status }, idempotent: true, events: [] as KxmRunEvent[] }; } const events: KxmRunEvent[] = []; let sequence = context.eventStore.nextSequence(runId); let nextMono = monotonicNs; const push = (eventType: string, payload: Record<string, unknown>): KxmRunEvent => { const event: KxmRunEvent = { ...kxmEventBase(context, run, now, nextMono, commandId), eventId: newKxmEventId(), eventType, sequence: sequence++, payload, }; nextMono = kxmIncrementMonotonicNs(nextMono); events.push(event); return event; }; const cancelReason = options.reason ?? "operator_cancel"; const activeAttempt = Boolean(folded.currentStep?.attemptId) || kxmFoldPanelAttemptIds(folded.currentStep).length > 0; const cancelPayload: Record<string, unknown> = { actor: { kind: "runtime", id: context.homeRuntimeId }, reason: cancelReason, }; if (cancelReason === "budget_run_duration") { if (!options.budget) { throw runtimeError("run_events_illegal", runId, "budget_run_duration requires a budget payload"); } cancelPayload.budget = options.budget; } push("run.cancel_requested", cancelPayload); let status: KxmRunStatus = "cancelled"; if (folded.status === "running" && activeAttempt) { status = "cancelling"; push("run.status_changed", { status: "cancelling", reason: cancelReason }); const attemptIds = new Set(kxmFoldPanelAttemptIds(folded.currentStep)); if (folded.currentStep?.attemptId) attemptIds.add(folded.currentStep.attemptId); for (const attemptId of attemptIds) { const capability = context.eventStore.capabilityByAttempt(attemptId); if (capability && capability.state === "issued") { context.eventStore.settleCapability(attemptId, "revoked"); } } for (const owned of kxmAttemptControllers(context.eventStore.path, runId)) { abortControllers.push(owned.controller); } } else if (folded.status === "blocked_uncertain") { push("run.status_changed", { status: "cancelling", reason: cancelReason }); const attemptId = folded.currentStep?.attemptId; if (attemptId) { const capability = context.eventStore.capabilityByAttempt(attemptId); if (capability && (capability.state === "issued" || capability.state === "revoked")) { context.eventStore.settleCapability(attemptId, "revoked"); } resolveKxmGateHold(context.eventStore.path, runId, attemptId); } for (const owned of kxmAttemptControllers(context.eventStore.path, runId)) { abortControllers.push(owned.controller); } push("run.status_changed", { status: "cancelled", reason: cancelReason }); status = "cancelled"; } else if (folded.status === "running" && !activeAttempt) { push("run.status_changed", { status: "cancelling", reason: cancelReason }); push("run.status_changed", { status: "cancelled", reason: cancelReason }); } else { push("run.status_changed", { status: "cancelled", reason: cancelReason }); } for (const event of events) context.eventStore.appendEvent(event); const nextState = foldStoredKxmRun(context, run); persistKxmRunState(context, runId, nextState, events[events.length - 1]!.sequence); context.eventStore.updateRunStatus(runId, status, now); context.eventStore.insertCommand({ commandId, runId, kind: "run.cancel", result: { runId, status }, recordedAt: now, }); return { run: { ...run, status, updatedAt: now }, idempotent: false, events }; }); for (const controller of abortControllers) controller.abort(); return result; } /* ------------------------------------------------------------------ * * Revision drift check * ------------------------------------------------------------------ */ /** Runs pin revisions at acceptance; drift is reported, never silently adopted. */ export function kxmRunRevisionDrift(run: KxmRunRecord, bundle: KxmProjectBundle): { configDrift: boolean; currentRevision: string; pinnedRevision: string } { return { configDrift: run.configRevision !== bundle.configRevision, currentRevision: bundle.configRevision, pinnedRevision: run.configRevision, }; } export function assertKxmConfigError(error: unknown): asserts error is KxmConfigError { if (!(error instanceof KxmConfigError)) throw error; }