@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
747 lines (687 loc) • 29.8 kB
text/typescript
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;
}