@kontextmind/kxm 0.7.23 → 0.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxm-claude-plugin",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -473,6 +473,61 @@ export async function cmdVnextRun(runtime: Runtime, workflow: string | undefined
473
473
  }
474
474
  }
475
475
 
476
+ interface VnextStatusDrive {
477
+ driveId?: string;
478
+ mode?: string;
479
+ openedAt?: string;
480
+ receipt?: {
481
+ settlement?: { kind?: string; status?: string; reason?: string };
482
+ } | null;
483
+ verified?: boolean;
484
+ divergence?: string;
485
+ }
486
+
487
+ function formatCancelledStatus(reason: string): string {
488
+ return reason.length > 0 ? `cancelled (${reason})` : "cancelled";
489
+ }
490
+
491
+ function formatDriveStatusLine(runStatus: string, drive: VnextStatusDrive | undefined): string | undefined {
492
+ if (!drive || typeof drive.driveId !== "string" || drive.driveId.length === 0) return undefined;
493
+ const receipt = drive.receipt ?? null;
494
+ if (receipt === null) {
495
+ if (runStatus === "running" || runStatus === "blocked_uncertain") {
496
+ return `drive ${drive.driveId}: open`;
497
+ }
498
+ return `drive ${drive.driveId}: no receipt (orphaned)`;
499
+ }
500
+ const kind = receipt.settlement?.kind;
501
+ const reason = typeof receipt.settlement?.reason === "string" ? receipt.settlement.reason : "";
502
+ if (kind === "unsettled") {
503
+ return `drive ${drive.driveId}: unsettled ${reason}`.trimEnd();
504
+ }
505
+ if (kind === "handoff") {
506
+ return drive.verified === true
507
+ ? `drive ${drive.driveId}: handoff (receipt verified)`
508
+ : `drive ${drive.driveId}: handoff`;
509
+ }
510
+ if (receipt.settlement?.status === "cancelled") {
511
+ const label = formatCancelledStatus(reason);
512
+ return drive.verified === true
513
+ ? `drive ${drive.driveId}: ${label} (receipt verified)`
514
+ : `drive ${drive.driveId}: ${label}`;
515
+ }
516
+ if (drive.verified === true) {
517
+ return `drive ${drive.driveId}: completed (receipt verified)`;
518
+ }
519
+ return `drive ${drive.driveId}: completed`;
520
+ }
521
+
522
+ function formatRunStatusLine(
523
+ run: { runId: string; status: string; workflowId: string; updatedAt: string },
524
+ drive: VnextStatusDrive | undefined,
525
+ ): string {
526
+ const reason = typeof drive?.receipt?.settlement?.reason === "string" ? drive.receipt.settlement.reason : "";
527
+ const statusLabel = run.status === "cancelled" ? formatCancelledStatus(reason) : run.status;
528
+ return `run ${run.runId}: ${statusLabel} (workflow ${run.workflowId}, updated ${run.updatedAt})`;
529
+ }
530
+
476
531
  export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
477
532
  try {
478
533
  const projectRoot = discoverVnextProjectRoot(runtime.cwd);
@@ -480,10 +535,17 @@ export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promis
480
535
  print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a vNext project (run kxm init first)");
481
536
  return 1;
482
537
  }
483
- const supervisor = await ensureVnextSupervisor({ env: runtime.env });
484
- const result = await vnextRuntimeRequest(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
538
+ const supervisor = await (vnextDriveCliSeams.ensureSupervisor ?? ensureVnextSupervisor)({ env: runtime.env });
539
+ const result = await (vnextDriveCliSeams.runtimeRequest ?? vnextRuntimeRequest)(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
485
540
  const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
486
- print(runtime.io, runtime.json, { ok: true, command: "runs status", run }, `run ${run.runId}: ${run.status} (workflow ${run.workflowId}, updated ${run.updatedAt})`);
541
+ const drive = result.drive as VnextStatusDrive | undefined;
542
+ const driveLine = formatDriveStatusLine(run.status, drive);
543
+ print(
544
+ runtime.io,
545
+ runtime.json,
546
+ { ok: true, command: "runs status", run, ...(drive !== undefined ? { drive } : {}) },
547
+ `${formatRunStatusLine(run, drive)}${driveLine ? `\n${driveLine}` : ""}`,
548
+ );
487
549
  return 0;
488
550
  } catch (error) {
489
551
  if (error instanceof VnextConfigError) {
@@ -382,7 +382,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
382
382
  }));
383
383
  const runCmd = addGlobalOptions(program.command("runs").description("Inspect vNext runs"));
384
384
  runCmd.helpCommand("help", "Show runs help");
385
- addGlobalOptions(runCmd.command("status").description("Show the projected status of a run"))
385
+ addGlobalOptions(runCmd.command("status").description("Show the projected status of a run, including durable drive receipt state (open / receipt verified / unsettled / orphaned)"))
386
386
  .argument("<runId>", "Run id")
387
387
  .action(async function runStatusAction(this: Command, runId: string) {
388
388
  result.code = await cmdVnextRunStatus(runtimeFrom(ctx, this), runId);
@@ -420,7 +420,7 @@ export function discoverProjectStores(projectRoot: string, options: { hubDataPat
420
420
  stores.push({
421
421
  storeId: `events:${key}`,
422
422
  sourcePath: join(eventsDir, entry.name),
423
- maxSupportedVersion: 3,
423
+ maxSupportedVersion: 4,
424
424
  });
425
425
  }
426
426
  }
@@ -537,6 +537,8 @@ export function restoreBackup(
537
537
  let maxSupported = 3;
538
538
  if (store.storeId === "registry" || store.storeId === "binding-store") {
539
539
  maxSupported = 1;
540
+ } else if (store.storeId.startsWith("events:")) {
541
+ maxSupported = 4;
540
542
  }
541
543
 
542
544
  let targetPath = store.sourcePath;
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.23";
11
+ const VERSION = "0.7.25";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -166,6 +166,7 @@ export class VnextSchemaRegistry {
166
166
  readonly migrationReceiptValidator: ValidateFunction;
167
167
  readonly permissionDiffValidator: ValidateFunction;
168
168
  readonly runEventValidator: ValidateFunction;
169
+ readonly driveReceiptValidator: ValidateFunction;
169
170
 
170
171
  constructor(schemasDir = DEFAULT_SCHEMA_DIR) {
171
172
  this.schemasDir = resolve(schemasDir);
@@ -183,6 +184,7 @@ export class VnextSchemaRegistry {
183
184
  const migrationReceiptFile = "migration-receipt.schema.json";
184
185
  const permissionDiffFile = "permission-diff.schema.json";
185
186
  const runEventFile = "run-event.schema.json";
187
+ const driveReceiptFile = "drive-receipt.schema.json";
186
188
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, localBindingsFile)));
187
189
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, templateProvenanceFile)));
188
190
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, initOperationFile)));
@@ -191,6 +193,7 @@ export class VnextSchemaRegistry {
191
193
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, migrationReceiptFile)));
192
194
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, permissionDiffFile)));
193
195
  this.ajv.addSchema(readJsonObject(join(this.schemasDir, runEventFile)));
196
+ this.ajv.addSchema(readJsonObject(join(this.schemasDir, driveReceiptFile)));
194
197
  for (const [kind, definition] of Object.entries(RESOURCE_SCHEMA) as [VnextResourceKind, { identity: string; file: string }][]) {
195
198
  const validator = this.ajv.getSchema(`https://schemas.kxm.dev/vnext/${definition.file}`);
196
199
  if (!validator) throw new Error(`schema did not compile: ${definition.file}`);
@@ -204,6 +207,7 @@ export class VnextSchemaRegistry {
204
207
  const migrationReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/vnext/${migrationReceiptFile}`);
205
208
  const permissionDiffValidator = this.ajv.getSchema(`https://schemas.kxm.dev/vnext/${permissionDiffFile}`);
206
209
  const runEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev/vnext/${runEventFile}`);
210
+ const driveReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/vnext/${driveReceiptFile}`);
207
211
  if (!localBindingsValidator) throw new Error(`schema did not compile: ${localBindingsFile}`);
208
212
  if (!templateProvenanceValidator) throw new Error(`schema did not compile: ${templateProvenanceFile}`);
209
213
  if (!initOperationValidator) throw new Error(`schema did not compile: ${initOperationFile}`);
@@ -212,6 +216,7 @@ export class VnextSchemaRegistry {
212
216
  if (!migrationReceiptValidator) throw new Error(`schema did not compile: ${migrationReceiptFile}`);
213
217
  if (!permissionDiffValidator) throw new Error(`schema did not compile: ${permissionDiffFile}`);
214
218
  if (!runEventValidator) throw new Error(`schema did not compile: ${runEventFile}`);
219
+ if (!driveReceiptValidator) throw new Error(`schema did not compile: ${driveReceiptFile}`);
215
220
  this.localBindingsValidator = localBindingsValidator;
216
221
  this.templateProvenanceValidator = templateProvenanceValidator;
217
222
  this.initOperationValidator = initOperationValidator;
@@ -220,6 +225,7 @@ export class VnextSchemaRegistry {
220
225
  this.migrationReceiptValidator = migrationReceiptValidator;
221
226
  this.permissionDiffValidator = permissionDiffValidator;
222
227
  this.runEventValidator = runEventValidator;
228
+ this.driveReceiptValidator = driveReceiptValidator;
223
229
  }
224
230
 
225
231
  validate(kind: VnextResourceKind, value: JsonObject, file: string): VnextConfigIssue[] {
@@ -261,6 +267,10 @@ export class VnextSchemaRegistry {
261
267
  return this.validateAuxiliary(value, file, "kxm.permission-diff.v1", this.permissionDiffValidator);
262
268
  }
263
269
 
270
+ validateDriveReceipt(value: JsonObject, file: string): VnextConfigIssue[] {
271
+ return this.validateAuxiliary(value, file, "kxm.drive-receipt.v1", this.driveReceiptValidator);
272
+ }
273
+
264
274
  private validateAuxiliary(value: JsonObject, file: string, identity: string, validator: ValidateFunction): VnextConfigIssue[] {
265
275
  if (value.schema !== identity) {
266
276
  return [issue("schema", "schema_identity_mismatch", file, `expected ${identity}, received ${String(value.schema)}`)];
@@ -285,6 +295,27 @@ export function validateRunEvent(value: unknown, file: string): void {
285
295
  }
286
296
  }
287
297
 
298
+ /** Validate a drive receipt against `kxm.drive-receipt.v1`. Throws `drive_receipt_invalid`. */
299
+ export function validateDriveReceipt(value: unknown, file: string): void {
300
+ const registry = (cachedRunEventRegistry ??= new VnextSchemaRegistry());
301
+ if (!value || typeof value !== "object" || Array.isArray(value) || (value as JsonObject).schema !== "kxm.drive-receipt.v1") {
302
+ throw new VnextConfigError([issue(
303
+ "schema",
304
+ "drive_receipt_invalid",
305
+ file,
306
+ "expected kxm.drive-receipt.v1",
307
+ )]);
308
+ }
309
+ if (!registry.driveReceiptValidator(value)) {
310
+ throw new VnextConfigError([issue(
311
+ "schema",
312
+ "drive_receipt_invalid",
313
+ file,
314
+ registry.ajv.errorsText(registry.driveReceiptValidator.errors, { separator: "; " }),
315
+ )]);
316
+ }
317
+ }
318
+
288
319
  function schemaIssue(file: string, error: ErrorObject): VnextConfigIssue {
289
320
  const location = error.instancePath || "/";
290
321
  const suffix = error.params && "additionalProperty" in error.params
@@ -62,6 +62,30 @@ export interface VnextFoldOptions {
62
62
  observationId: string,
63
63
  ) => { completeness: "complete" | "incomplete" | "no-start" } | undefined;
64
64
  readonly gateKind?: (stepId: string) => string | undefined;
65
+ readonly projectLimits?: {
66
+ readonly maxRunDurationMs?: number;
67
+ };
68
+ }
69
+
70
+ export type VnextRunDurationBudgetSource = "workflow" | "project" | "both";
71
+
72
+ export interface VnextRunDurationBudget {
73
+ readonly budgetMs: number;
74
+ readonly source: VnextRunDurationBudgetSource;
75
+ }
76
+
77
+ export function effectiveRunDurationBudget(
78
+ planLimits: { readonly maxRunDurationMs?: number } | undefined,
79
+ projectLimits: { readonly maxRunDurationMs?: number } | undefined,
80
+ ): VnextRunDurationBudget | undefined {
81
+ const workflow = planLimits?.maxRunDurationMs;
82
+ const project = projectLimits?.maxRunDurationMs;
83
+ if (workflow === undefined && project === undefined) return undefined;
84
+ if (workflow !== undefined && project !== undefined) {
85
+ return { budgetMs: Math.min(workflow, project), source: "both" };
86
+ }
87
+ if (workflow !== undefined) return { budgetMs: workflow, source: "workflow" };
88
+ return { budgetMs: project!, source: "project" };
65
89
  }
66
90
 
67
91
  export interface VnextFoldAttemptState {
@@ -107,6 +131,12 @@ export interface VnextRunCurrentStep {
107
131
  readonly evidenceRefs?: readonly { id: string; kind: string; hash: string; status: string }[] | undefined;
108
132
  }
109
133
 
134
+ export interface VnextRunDriveBinding {
135
+ readonly driveId: string;
136
+ readonly mode: "simulated" | "live";
137
+ readonly openedSequence: number;
138
+ }
139
+
110
140
  export interface VnextRunState {
111
141
  readonly schema: typeof VNEXT_RUN_STATE_SCHEMA;
112
142
  readonly runId: string;
@@ -120,6 +150,8 @@ export interface VnextRunState {
120
150
  readonly cancelRequested: boolean;
121
151
  readonly terminalReason?: string | undefined;
122
152
  readonly failureReason?: string | undefined;
153
+ readonly drive?: VnextRunDriveBinding | undefined;
154
+ readonly runningSince?: string | undefined;
123
155
  }
124
156
 
125
157
  interface MutableAttempt {
@@ -171,6 +203,8 @@ interface MutableState {
171
203
  awaitingTransition: boolean;
172
204
  cancelCommandId?: string | undefined;
173
205
  lastTerminalTransition?: VnextTerminalStatus | undefined;
206
+ drive?: VnextRunDriveBinding | undefined;
207
+ runningSince?: string | undefined;
174
208
  }
175
209
 
176
210
  function emptyPanel(): MutablePanel {
@@ -460,7 +494,10 @@ export function foldVnextRunState(
460
494
  foldRunStatus(state, plan, event);
461
495
  break;
462
496
  case "run.cancel_requested":
463
- foldCancelRequested(state, event);
497
+ foldCancelRequested(state, event, plan, options);
498
+ break;
499
+ case "run.drive_opened":
500
+ foldDriveOpened(state, event);
464
501
  break;
465
502
  case "step.entered":
466
503
  foldStepEntered(state, plan!, event);
@@ -595,6 +632,9 @@ function foldRunStatus(state: MutableState, plan: VnextCompiledPlan | undefined,
595
632
  if ((status === "running" || status === "cancelling") && !plan) {
596
633
  throw runtimeError("run_plan_missing", state.runId, `status ${status} requires a pinned run plan`);
597
634
  }
635
+ if (status === "running" && state.runningSince === undefined) {
636
+ state.runningSince = event.occurredAt;
637
+ }
598
638
  if (TERMINAL_RUN.has(status)) {
599
639
  assertTerminalRunStatus(state, plan, status, event);
600
640
  if (typeof event.payload.reason === "string") state.terminalReason = event.payload.reason;
@@ -694,10 +734,53 @@ function isProvenFailure(state: MutableState, plan: VnextCompiledPlan | undefine
694
734
  return Boolean(step && used >= step.maxAttempts);
695
735
  }
696
736
 
697
- function foldCancelRequested(state: MutableState, event: VnextRunEvent): void {
737
+ function foldDriveOpened(state: MutableState, event: VnextRunEvent): void {
738
+ if (state.status !== "running" && state.status !== "blocked_uncertain") {
739
+ throw runtimeError("run_events_illegal", state.runId, "run.drive_opened is only legal while running or blocked_uncertain");
740
+ }
741
+ const driveId = stringPayload(event, "driveId");
742
+ const mode = stringPayload(event, "mode");
743
+ if (mode !== "simulated" && mode !== "live") {
744
+ throw runtimeError("run_events_illegal", state.runId, "run.drive_opened mode must be simulated or live");
745
+ }
746
+ state.drive = { driveId, mode, openedSequence: event.sequence };
747
+ }
748
+
749
+ function foldCancelRequested(
750
+ state: MutableState,
751
+ event: VnextRunEvent,
752
+ plan: VnextCompiledPlan | undefined,
753
+ options: VnextFoldOptions,
754
+ ): void {
698
755
  if (state.cancelRequested || TERMINAL_RUN.has(state.status)) {
699
756
  throw runtimeError("run_events_illegal", state.runId, "duplicate run.cancel_requested is not legal");
700
757
  }
758
+ if (event.payload.reason === "budget_run_duration") {
759
+ const budget = event.payload.budget;
760
+ if (!budget || typeof budget !== "object" || Array.isArray(budget)) {
761
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration requires a budget payload");
762
+ }
763
+ const body = budget as Record<string, unknown>;
764
+ const budgetMs = body.budgetMs;
765
+ const elapsedMs = body.elapsedMs;
766
+ const source = body.source;
767
+ if (typeof budgetMs !== "number" || !Number.isInteger(budgetMs) || budgetMs < 0) {
768
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration budgetMs is invalid");
769
+ }
770
+ if (typeof elapsedMs !== "number" || !Number.isInteger(elapsedMs) || elapsedMs < 0) {
771
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration elapsedMs is invalid");
772
+ }
773
+ if (source !== "workflow" && source !== "project" && source !== "both") {
774
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration source is invalid");
775
+ }
776
+ const expected = effectiveRunDurationBudget(plan?.limits, options.projectLimits);
777
+ if (!expected || expected.budgetMs !== budgetMs || expected.source !== source) {
778
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration budgetMs does not match the plan");
779
+ }
780
+ if (elapsedMs < budgetMs) {
781
+ throw runtimeError("run_events_illegal", state.runId, "budget_run_duration elapsedMs is below budgetMs");
782
+ }
783
+ }
701
784
  state.cancelRequested = true;
702
785
  if (event.commandId) state.cancelCommandId = event.commandId;
703
786
  }
@@ -1379,6 +1462,8 @@ function freezeState(state: MutableState): VnextRunState {
1379
1462
  cancelRequested: state.cancelRequested,
1380
1463
  ...(state.terminalReason !== undefined ? { terminalReason: state.terminalReason } : {}),
1381
1464
  ...(state.terminalReason !== undefined && state.status === "failed" ? { failureReason: state.terminalReason } : {}),
1465
+ ...(state.drive !== undefined ? { drive: Object.freeze({ ...state.drive }) } : {}),
1466
+ ...(state.runningSince !== undefined ? { runningSince: state.runningSince } : {}),
1382
1467
  };
1383
1468
  return Object.freeze(frozen);
1384
1469
  }
@@ -257,7 +257,7 @@ export function recordGateNoStartInTransaction(
257
257
  const cancelled = observation.stopCause === "cancel";
258
258
  const now = new Date().toISOString();
259
259
  const seq = sequencer(context, run, now);
260
- if (cancelled) ensureOperatorCancel(seq, ids.state, run);
260
+ const cancelReason = cancelled ? ensureOperatorCancel(seq, ids.state, run, context) : undefined;
261
261
  const observationId = newVnextObservationId();
262
262
  const observed = seq.push("effect.observed", {
263
263
  effect: { id: ids.effectId, policy: { class: ids.gateKind === "artifacts-exist" ? "read-only" : "unknown", sharedMutable: false } },
@@ -289,7 +289,7 @@ export function recordGateNoStartInTransaction(
289
289
  seq.push("attempt.status_changed", { attemptId: ids.attemptId, status: "terminal" });
290
290
  seq.push("assignment.terminal", { assignmentId: ids.assignmentId, outcome: "cancelled", status: "terminal" });
291
291
  seq.push("step.status_changed", { stepId, status: "cancelled", previousStatus: "running" });
292
- seq.push("run.status_changed", { status: "cancelled", reason: "operator_cancel", stepId });
292
+ seq.push("run.status_changed", { status: "cancelled", reason: cancelReason ?? "operator_cancel", stepId });
293
293
  } else {
294
294
  seq.push("assignment.result_recorded", { assignmentId: ids.assignmentId, resultClass: "producer_rejected", status: "result_recorded" });
295
295
  seq.push("attempt.status_changed", { attemptId: ids.attemptId, status: "terminal" });
@@ -385,7 +385,7 @@ export function recordGateCancelObservedInTransaction(
385
385
  }
386
386
  const now = new Date().toISOString();
387
387
  const seq = sequencer(context, run, now);
388
- ensureOperatorCancel(seq, ids.state, run);
388
+ const cancelReason = ensureOperatorCancel(seq, ids.state, run, context);
389
389
  if (ids.gateKind === "artifacts-exist") {
390
390
  seq.push("assignment.executing", { assignmentId: ids.assignmentId, stepId, stepAttempt: ids.stepAttempt, attemptId: ids.attemptId, status: "executing" });
391
391
  seq.push("attempt.status_changed", { attemptId: ids.attemptId, assignmentId: ids.assignmentId, stepId, stepAttempt: ids.stepAttempt, status: "executing" });
@@ -428,7 +428,7 @@ export function recordGateCancelObservedInTransaction(
428
428
  seq.push("attempt.status_changed", { attemptId: ids.attemptId, status: "terminal" });
429
429
  seq.push("assignment.terminal", { assignmentId: ids.assignmentId, outcome: "cancelled", status: "terminal" });
430
430
  seq.push("step.status_changed", { stepId, status: "cancelled", previousStatus: "running" });
431
- seq.push("run.status_changed", { status: "cancelled", reason: "operator_cancel", stepId });
431
+ seq.push("run.status_changed", { status: "cancelled", reason: cancelReason, stepId });
432
432
  for (const event of seq.events) context.eventStore.appendEvent(event);
433
433
  context.eventStore.insertGateObservation(observationRow);
434
434
  context.eventStore.settleCapability(ids.attemptId, "settled");
@@ -536,8 +536,9 @@ function ensureOperatorCancel(
536
536
  seq: ReturnType<typeof sequencer>,
537
537
  state: ReturnType<typeof foldStoredVnextRun>,
538
538
  run: VnextRunRecord,
539
- ): void {
540
- if (state.status === "cancelling") return;
539
+ context: VnextRuntimeContext,
540
+ ): string {
541
+ if (state.status === "cancelling") return cancelReasonFromLog(context, run.runId);
541
542
  if (state.status !== "running") {
542
543
  throw runtimeError("run_events_illegal", run.runId, "cancelled gate proof requires a running or cancelling run");
543
544
  }
@@ -546,6 +547,18 @@ function ensureOperatorCancel(
546
547
  reason: "operator_cancel",
547
548
  });
548
549
  seq.push("run.status_changed", { status: "cancelling", reason: "operator_cancel" });
550
+ return "operator_cancel";
551
+ }
552
+
553
+ function cancelReasonFromLog(context: VnextRuntimeContext, runId: string): string {
554
+ const events = context.eventStore.events(runId, 0, 1_000_000);
555
+ for (let index = events.length - 1; index >= 0; index -= 1) {
556
+ const event = events[index]!;
557
+ if (event.eventType === "run.cancel_requested" && typeof event.payload.reason === "string") {
558
+ return event.payload.reason;
559
+ }
560
+ }
561
+ return "operator_cancel";
549
562
  }
550
563
 
551
564
  function sequencer(context: VnextRuntimeContext, run: VnextRunRecord, now: string) {