@kontextmind/kxm 0.7.24 → 0.7.26
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +117 -12
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +265 -25
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +265 -25
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-runs/SKILL.md +4 -0
- package/plugins/kxm/src/cli/vnext.ts +133 -10
- package/plugins/kxm/src/cli.ts +14 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/vnext-engine-fold.ts +63 -2
- package/plugins/kxm/src/vnext-engine-gate-records.ts +19 -6
- package/plugins/kxm/src/vnext-engine.ts +155 -9
- package/plugins/kxm/src/vnext-runtime-owner.ts +1 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +22 -5
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +62 -2
- package/plugins/kxm/src/vnext-runtime.ts +17 -3
- package/schemas/vnext/drive-receipt.schema.json +17 -1
- package/schemas/vnext/run-event.schema.json +29 -0
|
@@ -484,6 +484,10 @@ interface VnextStatusDrive {
|
|
|
484
484
|
divergence?: string;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
function formatCancelledStatus(reason: string): string {
|
|
488
|
+
return reason.length > 0 ? `cancelled (${reason})` : "cancelled";
|
|
489
|
+
}
|
|
490
|
+
|
|
487
491
|
function formatDriveStatusLine(runStatus: string, drive: VnextStatusDrive | undefined): string | undefined {
|
|
488
492
|
if (!drive || typeof drive.driveId !== "string" || drive.driveId.length === 0) return undefined;
|
|
489
493
|
const receipt = drive.receipt ?? null;
|
|
@@ -503,12 +507,27 @@ function formatDriveStatusLine(runStatus: string, drive: VnextStatusDrive | unde
|
|
|
503
507
|
? `drive ${drive.driveId}: handoff (receipt verified)`
|
|
504
508
|
: `drive ${drive.driveId}: handoff`;
|
|
505
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
|
+
}
|
|
506
516
|
if (drive.verified === true) {
|
|
507
517
|
return `drive ${drive.driveId}: completed (receipt verified)`;
|
|
508
518
|
}
|
|
509
519
|
return `drive ${drive.driveId}: completed`;
|
|
510
520
|
}
|
|
511
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
|
+
|
|
512
531
|
export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promise<number> {
|
|
513
532
|
try {
|
|
514
533
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
@@ -525,7 +544,7 @@ export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promis
|
|
|
525
544
|
runtime.io,
|
|
526
545
|
runtime.json,
|
|
527
546
|
{ ok: true, command: "runs status", run, ...(drive !== undefined ? { drive } : {}) },
|
|
528
|
-
|
|
547
|
+
`${formatRunStatusLine(run, drive)}${driveLine ? `\n${driveLine}` : ""}`,
|
|
529
548
|
);
|
|
530
549
|
return 0;
|
|
531
550
|
} catch (error) {
|
|
@@ -538,7 +557,32 @@ export async function cmdVnextRunStatus(runtime: Runtime, runId: string): Promis
|
|
|
538
557
|
}
|
|
539
558
|
}
|
|
540
559
|
|
|
541
|
-
|
|
560
|
+
const DEFAULT_DRIVE_WAIT_MS = 60_000;
|
|
561
|
+
const MAX_DRIVE_WAIT_MS = 600_000;
|
|
562
|
+
|
|
563
|
+
function parseDriveWaitTimeoutMs(raw: number | string | undefined): number | undefined {
|
|
564
|
+
if (raw === undefined) return DEFAULT_DRIVE_WAIT_MS;
|
|
565
|
+
const value = typeof raw === "number" ? raw : Number(typeof raw === "string" ? raw.trim() : raw);
|
|
566
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_DRIVE_WAIT_MS) return undefined;
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function sleepMs(ms: number): Promise<void> {
|
|
571
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function driveWaitCompleted(drive: VnextStatusDrive | undefined): boolean {
|
|
575
|
+
return drive?.verified === true
|
|
576
|
+
&& drive.receipt?.settlement?.kind === "terminal"
|
|
577
|
+
&& drive.receipt.settlement.status === "completed";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export async function cmdVnextRunDrive(
|
|
581
|
+
runtime: Runtime,
|
|
582
|
+
runId: string,
|
|
583
|
+
simulated: boolean,
|
|
584
|
+
options: { wait?: boolean; timeoutMs?: number | string } = {},
|
|
585
|
+
): Promise<number> {
|
|
542
586
|
try {
|
|
543
587
|
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
544
588
|
if (!projectRoot) {
|
|
@@ -550,8 +594,19 @@ export async function cmdVnextRunDrive(runtime: Runtime, runId: string, simulate
|
|
|
550
594
|
print(runtime.io, runtime.json, { ok: true, command: "runs drive", dryRun: true, runId, mode }, `drive plan: run ${runId} in ${mode} mode (no events written)`);
|
|
551
595
|
return 0;
|
|
552
596
|
}
|
|
597
|
+
const timeoutMs = options.wait === true ? parseDriveWaitTimeoutMs(options.timeoutMs) : DEFAULT_DRIVE_WAIT_MS;
|
|
598
|
+
if (options.wait === true && timeoutMs === undefined) {
|
|
599
|
+
print(
|
|
600
|
+
runtime.io,
|
|
601
|
+
runtime.json,
|
|
602
|
+
{ ok: false, command: "runs drive", error: "run_drive_timeout_invalid" },
|
|
603
|
+
`run drive --wait --timeout-ms must be an integer between 1 and ${MAX_DRIVE_WAIT_MS}`,
|
|
604
|
+
);
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
553
607
|
const supervisor = await (vnextDriveCliSeams.ensureSupervisor ?? ensureVnextSupervisor)({ env: runtime.env });
|
|
554
|
-
const
|
|
608
|
+
const request = vnextDriveCliSeams.runtimeRequest ?? vnextRuntimeRequest;
|
|
609
|
+
const result = await request(supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/drive?projectRoot=${encodeURIComponent(projectRoot)}`, { mode });
|
|
555
610
|
const driveId = result.driveId;
|
|
556
611
|
const poll = result.poll;
|
|
557
612
|
const status = result.status;
|
|
@@ -559,13 +614,39 @@ export async function cmdVnextRunDrive(runtime: Runtime, runId: string, simulate
|
|
|
559
614
|
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_io_failed" }, "run drive failed because a local operation did not complete");
|
|
560
615
|
return 1;
|
|
561
616
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
617
|
+
|
|
618
|
+
if (options.wait !== true) {
|
|
619
|
+
print(
|
|
620
|
+
runtime.io,
|
|
621
|
+
runtime.json,
|
|
622
|
+
{ ok: true, command: "runs drive", runId, driveId, poll, mode, status },
|
|
623
|
+
`drive ${driveId}: accepted (poll ${poll})`,
|
|
624
|
+
);
|
|
625
|
+
return 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const deadline = Date.now() + timeoutMs!;
|
|
629
|
+
let interval = 25;
|
|
630
|
+
while (true) {
|
|
631
|
+
const stateResult = await request(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
632
|
+
const drive = stateResult.drive as VnextStatusDrive | undefined;
|
|
633
|
+
if (drive?.receipt) {
|
|
634
|
+
print(
|
|
635
|
+
runtime.io,
|
|
636
|
+
runtime.json,
|
|
637
|
+
{ ok: driveWaitCompleted(drive), command: "runs drive", receipt: drive.receipt, verified: drive.verified === true },
|
|
638
|
+
JSON.stringify(drive.receipt, null, 2),
|
|
639
|
+
);
|
|
640
|
+
return driveWaitCompleted(drive) ? 0 : 1;
|
|
641
|
+
}
|
|
642
|
+
const remaining = deadline - Date.now();
|
|
643
|
+
if (remaining <= 0) break;
|
|
644
|
+
await sleepMs(Math.min(interval, remaining));
|
|
645
|
+
interval = Math.min(Math.floor(interval * 1.5), 250);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "timeout" }, "drive wait timed out");
|
|
649
|
+
return 1;
|
|
569
650
|
} catch (error) {
|
|
570
651
|
if (error instanceof VnextConfigError) {
|
|
571
652
|
print(runtime.io, runtime.json, { ok: false, command: "runs drive", error: "run_drive_failed", issues: error.issues }, `run drive failed: ${error.message}`);
|
|
@@ -753,3 +834,45 @@ export async function cmdVnextRuntime(runtime: Runtime, action: string): Promise
|
|
|
753
834
|
return 1;
|
|
754
835
|
}
|
|
755
836
|
}
|
|
837
|
+
|
|
838
|
+
export async function cmdVnextRunReceipt(runtime: Runtime, runId: string, options: { all?: boolean } = {}): Promise<number> {
|
|
839
|
+
try {
|
|
840
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd);
|
|
841
|
+
if (!projectRoot) {
|
|
842
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "project_required" }, "kxm runs receipt requires a vNext project (run kxm init first)");
|
|
843
|
+
return 1;
|
|
844
|
+
}
|
|
845
|
+
const supervisor = await (vnextDriveCliSeams.ensureSupervisor ?? ensureVnextSupervisor)({ env: runtime.env });
|
|
846
|
+
const result = await (vnextDriveCliSeams.runtimeRequest ?? vnextRuntimeRequest)(
|
|
847
|
+
supervisor,
|
|
848
|
+
"GET",
|
|
849
|
+
`/v1/runs/${encodeURIComponent(runId)}/drive?projectRoot=${encodeURIComponent(projectRoot)}`,
|
|
850
|
+
);
|
|
851
|
+
if (!Array.isArray(result.receipts)) {
|
|
852
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "run_receipt_io_failed" }, "run receipt failed because a local operation did not complete");
|
|
853
|
+
return 1;
|
|
854
|
+
}
|
|
855
|
+
const receipts = result.receipts as unknown[];
|
|
856
|
+
if (receipts.length === 0) {
|
|
857
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "no_receipts" }, `run ${runId} has no drive receipts`);
|
|
858
|
+
return 1;
|
|
859
|
+
}
|
|
860
|
+
if (options.all === true) {
|
|
861
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs receipt", receipts }, JSON.stringify(receipts, null, 2));
|
|
862
|
+
return 0;
|
|
863
|
+
}
|
|
864
|
+
const latest = receipts[0];
|
|
865
|
+
const settlement = latest && typeof latest === "object" && "settlement" in latest
|
|
866
|
+
? (latest as { settlement: unknown }).settlement
|
|
867
|
+
: latest;
|
|
868
|
+
print(runtime.io, runtime.json, { ok: true, command: "runs receipt", receipt: latest }, JSON.stringify(settlement, null, 2));
|
|
869
|
+
return 0;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (error instanceof VnextConfigError) {
|
|
872
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "run_receipt_failed", issues: error.issues }, `run receipt failed: ${error.message}`);
|
|
873
|
+
return 1;
|
|
874
|
+
}
|
|
875
|
+
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "run_receipt_io_failed" }, "run receipt failed because a local operation did not complete");
|
|
876
|
+
return 1;
|
|
877
|
+
}
|
|
878
|
+
}
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
cmdVnextRun,
|
|
108
108
|
cmdVnextRunStatus,
|
|
109
109
|
cmdVnextRunDrive,
|
|
110
|
+
cmdVnextRunReceipt,
|
|
110
111
|
cmdVnextRunCancel,
|
|
111
112
|
cmdVnextRunList,
|
|
112
113
|
cmdHarnessList,
|
|
@@ -390,8 +391,19 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
390
391
|
addGlobalOptions(runCmd.command("drive").description("Drive a run with an explicit model-free simulation"))
|
|
391
392
|
.argument("<runId>", "Run id")
|
|
392
393
|
.option("--simulated", "Use the model-free simulation producer")
|
|
393
|
-
.
|
|
394
|
-
|
|
394
|
+
.option("--wait", "Wait until a drive receipt is recorded; exits 0 only for a VERIFIED COMPLETED settlement")
|
|
395
|
+
.option("--timeout-ms <n>", "Wait timeout in milliseconds (default 60000, max 600000)")
|
|
396
|
+
.action(async function runDriveAction(this: Command, runId: string, options: { simulated?: boolean; wait?: boolean; timeoutMs?: string }) {
|
|
397
|
+
result.code = await cmdVnextRunDrive(runtimeFrom(ctx, this), runId, options.simulated === true, {
|
|
398
|
+
wait: options.wait === true,
|
|
399
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
addGlobalOptions(runCmd.command("receipt").description("Print the newest drive receipt for a run"))
|
|
403
|
+
.argument("<runId>", "Run id")
|
|
404
|
+
.option("--all", "Print the capped receipt list for the run")
|
|
405
|
+
.action(async function runReceiptAction(this: Command, runId: string, options: { all?: boolean }) {
|
|
406
|
+
result.code = await cmdVnextRunReceipt(runtimeFrom(ctx, this), runId, { all: options.all === true });
|
|
395
407
|
});
|
|
396
408
|
addGlobalOptions(runCmd.command("cancel").description("Durably request cancellation of a run"))
|
|
397
409
|
.argument("<runId>", "Run id")
|
|
@@ -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.
|
|
11
|
+
const VERSION = "0.7.26";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -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 {
|
|
@@ -127,6 +151,7 @@ export interface VnextRunState {
|
|
|
127
151
|
readonly terminalReason?: string | undefined;
|
|
128
152
|
readonly failureReason?: string | undefined;
|
|
129
153
|
readonly drive?: VnextRunDriveBinding | undefined;
|
|
154
|
+
readonly runningSince?: string | undefined;
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
interface MutableAttempt {
|
|
@@ -179,6 +204,7 @@ interface MutableState {
|
|
|
179
204
|
cancelCommandId?: string | undefined;
|
|
180
205
|
lastTerminalTransition?: VnextTerminalStatus | undefined;
|
|
181
206
|
drive?: VnextRunDriveBinding | undefined;
|
|
207
|
+
runningSince?: string | undefined;
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
function emptyPanel(): MutablePanel {
|
|
@@ -468,7 +494,7 @@ export function foldVnextRunState(
|
|
|
468
494
|
foldRunStatus(state, plan, event);
|
|
469
495
|
break;
|
|
470
496
|
case "run.cancel_requested":
|
|
471
|
-
foldCancelRequested(state, event);
|
|
497
|
+
foldCancelRequested(state, event, plan, options);
|
|
472
498
|
break;
|
|
473
499
|
case "run.drive_opened":
|
|
474
500
|
foldDriveOpened(state, event);
|
|
@@ -606,6 +632,9 @@ function foldRunStatus(state: MutableState, plan: VnextCompiledPlan | undefined,
|
|
|
606
632
|
if ((status === "running" || status === "cancelling") && !plan) {
|
|
607
633
|
throw runtimeError("run_plan_missing", state.runId, `status ${status} requires a pinned run plan`);
|
|
608
634
|
}
|
|
635
|
+
if (status === "running" && state.runningSince === undefined) {
|
|
636
|
+
state.runningSince = event.occurredAt;
|
|
637
|
+
}
|
|
609
638
|
if (TERMINAL_RUN.has(status)) {
|
|
610
639
|
assertTerminalRunStatus(state, plan, status, event);
|
|
611
640
|
if (typeof event.payload.reason === "string") state.terminalReason = event.payload.reason;
|
|
@@ -717,10 +746,41 @@ function foldDriveOpened(state: MutableState, event: VnextRunEvent): void {
|
|
|
717
746
|
state.drive = { driveId, mode, openedSequence: event.sequence };
|
|
718
747
|
}
|
|
719
748
|
|
|
720
|
-
function foldCancelRequested(
|
|
749
|
+
function foldCancelRequested(
|
|
750
|
+
state: MutableState,
|
|
751
|
+
event: VnextRunEvent,
|
|
752
|
+
plan: VnextCompiledPlan | undefined,
|
|
753
|
+
options: VnextFoldOptions,
|
|
754
|
+
): void {
|
|
721
755
|
if (state.cancelRequested || TERMINAL_RUN.has(state.status)) {
|
|
722
756
|
throw runtimeError("run_events_illegal", state.runId, "duplicate run.cancel_requested is not legal");
|
|
723
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
|
+
}
|
|
724
784
|
state.cancelRequested = true;
|
|
725
785
|
if (event.commandId) state.cancelCommandId = event.commandId;
|
|
726
786
|
}
|
|
@@ -1403,6 +1463,7 @@ function freezeState(state: MutableState): VnextRunState {
|
|
|
1403
1463
|
...(state.terminalReason !== undefined ? { terminalReason: state.terminalReason } : {}),
|
|
1404
1464
|
...(state.terminalReason !== undefined && state.status === "failed" ? { failureReason: state.terminalReason } : {}),
|
|
1405
1465
|
...(state.drive !== undefined ? { drive: Object.freeze({ ...state.drive }) } : {}),
|
|
1466
|
+
...(state.runningSince !== undefined ? { runningSince: state.runningSince } : {}),
|
|
1406
1467
|
};
|
|
1407
1468
|
return Object.freeze(frozen);
|
|
1408
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
|
-
|
|
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:
|
|
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
|
-
|
|
540
|
-
|
|
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) {
|