@kontextmind/kxm 0.7.24 → 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.
- 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 +13 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +203 -23
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +203 -23
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/vnext.ts +20 -1
- 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 +8 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +1 -1
- 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
|
@@ -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) {
|
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
} from "./context-packet.ts";
|
|
14
14
|
import { compileVnextWorkflow, type VnextCompiledPlan, type VnextCompiledStep } from "./vnext-engine-compile.ts";
|
|
15
15
|
import {
|
|
16
|
+
effectiveRunDurationBudget,
|
|
16
17
|
isTerminalRunStatus,
|
|
17
18
|
vnextFoldPanelAttempt,
|
|
18
19
|
vnextFoldPanelAttemptIds,
|
|
19
20
|
vnextJoinAll,
|
|
21
|
+
type VnextRunDurationBudget,
|
|
20
22
|
type VnextRunState,
|
|
21
23
|
} from "./vnext-engine-fold.ts";
|
|
22
24
|
import {
|
|
@@ -76,6 +78,7 @@ import {
|
|
|
76
78
|
VNEXT_DRIVE_RECEIPT_SCHEMA,
|
|
77
79
|
type VnextAttemptCapabilityRow,
|
|
78
80
|
type VnextDriveReceipt,
|
|
81
|
+
type VnextDriveReceiptBudget,
|
|
79
82
|
type VnextDriveReceiptHandoff,
|
|
80
83
|
type VnextGateAttemptRow,
|
|
81
84
|
type VnextGateEvidenceRow,
|
|
@@ -388,6 +391,8 @@ async function driveAdmitted(
|
|
|
388
391
|
}
|
|
389
392
|
let latest: VnextRunDriveResult = { state: foldStoredVnextRun(context, requireRun(context, runId)) };
|
|
390
393
|
while (latest.state.status === "running") {
|
|
394
|
+
const overBudget = cancelRunDurationIfExceeded(context, runId);
|
|
395
|
+
if (overBudget) return overBudget;
|
|
391
396
|
const before = context.eventStore.runState(runId)?.lastSequence;
|
|
392
397
|
latest = await stepLocked(context, runId, producer, token);
|
|
393
398
|
if (latest.handoff) return latest;
|
|
@@ -513,6 +518,7 @@ export function recordDriveReceipt(
|
|
|
513
518
|
const last = events[events.length - 1]!;
|
|
514
519
|
const openedSequence = opened?.openedSequence ?? openedEvent?.sequence;
|
|
515
520
|
if (openedSequence === undefined) return;
|
|
521
|
+
const closedAt = new Date().toISOString();
|
|
516
522
|
const receipt: VnextDriveReceipt = {
|
|
517
523
|
schema: VNEXT_DRIVE_RECEIPT_SCHEMA,
|
|
518
524
|
driveId: session.driveId,
|
|
@@ -522,7 +528,7 @@ export function recordDriveReceipt(
|
|
|
522
528
|
mode: session.mode,
|
|
523
529
|
openedAt: openedEvent?.occurredAt ?? session.openedAt,
|
|
524
530
|
openedSequence,
|
|
525
|
-
closedAt
|
|
531
|
+
closedAt,
|
|
526
532
|
lastSequence: last.sequence,
|
|
527
533
|
logHash: hashVnextDriveLog(events.filter((event) => event.sequence >= 1 && event.sequence <= last.sequence)),
|
|
528
534
|
settlement: {
|
|
@@ -532,7 +538,7 @@ export function recordDriveReceipt(
|
|
|
532
538
|
...(closeInfo.kind === "handoff" && closeInfo.handoff ? { handoff: receiptHandoff(closeInfo.handoff) } : {}),
|
|
533
539
|
...(closeInfo.error !== undefined ? { error: closeInfo.error } : {}),
|
|
534
540
|
},
|
|
535
|
-
budget:
|
|
541
|
+
budget: driveReceiptBudget(context, run.runId, state, events, closedAt),
|
|
536
542
|
producer: { id: closeInfo.producerId, closed: closeInfo.producerClosed },
|
|
537
543
|
};
|
|
538
544
|
context.eventStore.insertDriveReceipt(receipt);
|
|
@@ -744,6 +750,8 @@ export class VnextRunScheduler {
|
|
|
744
750
|
}
|
|
745
751
|
const driveId = newDriveId(runId, token, this.context.homeRuntimeId, vnextMonotonicNs());
|
|
746
752
|
const openedMeta = appendDriveOpened(this.context, runId, driveId, options.mode);
|
|
753
|
+
const startedState = foldStoredVnextRun(this.context, requireRun(this.context, runId));
|
|
754
|
+
const deadlineAt = driveDeadlineAt(this.context, runId, startedState);
|
|
747
755
|
const session: VnextDriveSession = {
|
|
748
756
|
driveId,
|
|
749
757
|
runId,
|
|
@@ -754,6 +762,7 @@ export class VnextRunScheduler {
|
|
|
754
762
|
producerId: producer.id,
|
|
755
763
|
controller: new AbortController(),
|
|
756
764
|
settled,
|
|
765
|
+
...(deadlineAt !== undefined ? { deadlineAt } : {}),
|
|
757
766
|
};
|
|
758
767
|
attachVnextDriveSession(this.context.eventStore.path, runId, token, session);
|
|
759
768
|
opened = true;
|
|
@@ -900,11 +909,13 @@ async function stepLocked(context: VnextRuntimeContext, runId: string, producer:
|
|
|
900
909
|
throw runtimeError("run_events_illegal", prepared.run.runId, "prepared gate is not artifacts-exist");
|
|
901
910
|
}
|
|
902
911
|
registerVnextAttemptController(context.eventStore.path, runId, { attemptId: prepared.attemptId, controller: prepared.controller });
|
|
912
|
+
const clearBudget = armRunDurationBudgetTimer(context, runId);
|
|
903
913
|
try {
|
|
904
914
|
const observation = evaluateArtifactsGate(context, definition);
|
|
905
915
|
vnextGateDispatchSeams.afterEvaluate?.(prepared);
|
|
906
916
|
return context.eventStore.transaction(() => settlePreparedGate(context, prepared, observation));
|
|
907
917
|
} finally {
|
|
918
|
+
clearBudget();
|
|
908
919
|
unregisterVnextAttemptController(context.eventStore.path, runId, prepared.attemptId);
|
|
909
920
|
}
|
|
910
921
|
}
|
|
@@ -945,6 +956,7 @@ async function runPreparedCommandGate(
|
|
|
945
956
|
});
|
|
946
957
|
armVnextGateHold(storePath, prepared.run.runId, token, prepared.attemptId, () => observer.requestStop("cancel"));
|
|
947
958
|
registerVnextAttemptController(storePath, prepared.run.runId, { attemptId: prepared.attemptId, controller: prepared.controller });
|
|
959
|
+
const clearBudget = armRunDurationBudgetTimer(context, prepared.run.runId);
|
|
948
960
|
const unhookClose = registerVnextRuntimeCloseHook(context, () => {
|
|
949
961
|
try {
|
|
950
962
|
markVnextGateHoldUnsettled(storePath, prepared.run.runId, token, prepared.attemptId);
|
|
@@ -992,6 +1004,7 @@ async function runPreparedCommandGate(
|
|
|
992
1004
|
return commitCommandUncertainty(context, prepared, token, outcome.reason, outcome.observation);
|
|
993
1005
|
} finally {
|
|
994
1006
|
unhookClose();
|
|
1007
|
+
clearBudget();
|
|
995
1008
|
unregisterVnextAttemptController(storePath, prepared.run.runId, prepared.attemptId);
|
|
996
1009
|
}
|
|
997
1010
|
}
|
|
@@ -1798,8 +1811,10 @@ async function drivePanel(
|
|
|
1798
1811
|
let stopBirths = false;
|
|
1799
1812
|
let settlementFailed = false;
|
|
1800
1813
|
|
|
1814
|
+
const budgetClears = new Map<string, () => void>();
|
|
1801
1815
|
const register = (member: PreparedDispatch): void => {
|
|
1802
1816
|
registerVnextAttemptController(context.eventStore.path, runId, { attemptId: member.attemptId, controller: member.controller });
|
|
1817
|
+
budgetClears.set(member.attemptId, armRunDurationBudgetTimer(context, runId));
|
|
1803
1818
|
owned.set(member.attemptId, member);
|
|
1804
1819
|
};
|
|
1805
1820
|
|
|
@@ -2006,6 +2021,7 @@ async function drivePanel(
|
|
|
2006
2021
|
throw error;
|
|
2007
2022
|
} finally {
|
|
2008
2023
|
for (const attemptId of owned.keys()) {
|
|
2024
|
+
budgetClears.get(attemptId)?.();
|
|
2009
2025
|
unregisterVnextAttemptController(context.eventStore.path, runId, attemptId);
|
|
2010
2026
|
}
|
|
2011
2027
|
}
|
|
@@ -2194,7 +2210,7 @@ function joinPanel(context: VnextRuntimeContext, panel: PreparedPanel): VnextRun
|
|
|
2194
2210
|
|
|
2195
2211
|
const cancelRun = (): VnextRunDriveResult => {
|
|
2196
2212
|
push("step.status_changed", { stepId: panel.stepId, status: "cancelled", previousStatus: "running" });
|
|
2197
|
-
push("run.status_changed", { status: "cancelled", reason:
|
|
2213
|
+
push("run.status_changed", { status: "cancelled", reason: cancelReasonFromLog(context, run.runId) });
|
|
2198
2214
|
for (const event of events) context.eventStore.appendEvent(event);
|
|
2199
2215
|
const next = foldStoredVnextRun(context, run);
|
|
2200
2216
|
persistVnextRunState(context, run.runId, next, events[events.length - 1]!.sequence);
|
|
@@ -2316,16 +2332,146 @@ function transitionBudgetFailure(plan: VnextCompiledPlan, state: VnextRunState,
|
|
|
2316
2332
|
return undefined;
|
|
2317
2333
|
}
|
|
2318
2334
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2335
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
2336
|
+
|
|
2337
|
+
function elapsedMsBetween(fromIso: string, toIso: string): number {
|
|
2338
|
+
const from = Date.parse(fromIso);
|
|
2339
|
+
const to = Date.parse(toIso);
|
|
2340
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
|
|
2341
|
+
return Math.max(0, Math.trunc(to - from));
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function declaredRunDurationBudget(context: VnextRuntimeContext, runId: string): VnextRunDurationBudget | undefined {
|
|
2345
|
+
const run = context.eventStore.run(runId);
|
|
2346
|
+
if (!run) return undefined;
|
|
2347
|
+
try {
|
|
2348
|
+
const envelope = loadVnextRunPlanEnvelope(context.eventStore, run);
|
|
2349
|
+
return effectiveRunDurationBudget(envelope.plan.limits, envelope.projectLimits);
|
|
2350
|
+
} catch {
|
|
2351
|
+
return undefined;
|
|
2322
2352
|
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function runDurationOverrunPayload(
|
|
2356
|
+
context: VnextRuntimeContext,
|
|
2357
|
+
runId: string,
|
|
2358
|
+
nowIso = new Date().toISOString(),
|
|
2359
|
+
): { budgetMs: number; elapsedMs: number; source: VnextRunDurationBudget["source"] } | undefined {
|
|
2360
|
+
const declared = declaredRunDurationBudget(context, runId);
|
|
2361
|
+
if (!declared) return undefined;
|
|
2362
|
+
const run = context.eventStore.run(runId);
|
|
2363
|
+
if (!run) return undefined;
|
|
2364
|
+
const state = foldStoredVnextRun(context, run);
|
|
2365
|
+
if (!state.runningSince) return undefined;
|
|
2366
|
+
if (isTerminalRunStatus(state.status) || state.status === "cancelling") return undefined;
|
|
2367
|
+
const elapsedMs = elapsedMsBetween(state.runningSince, nowIso);
|
|
2368
|
+
if (elapsedMs < declared.budgetMs) return undefined;
|
|
2369
|
+
return { budgetMs: declared.budgetMs, elapsedMs, source: declared.source };
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function cancelRunDurationIfExceeded(context: VnextRuntimeContext, runId: string): VnextRunDriveResult | undefined {
|
|
2373
|
+
if (isVnextRuntimeContextClosed(context)) return undefined;
|
|
2374
|
+
const payload = runDurationOverrunPayload(context, runId);
|
|
2375
|
+
if (!payload) return undefined;
|
|
2376
|
+
cancelVnextRun(context, runId, { reason: "budget_run_duration", budget: payload });
|
|
2377
|
+
return { state: foldStoredVnextRun(context, requireRun(context, runId)) };
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function driveDeadlineAt(context: VnextRuntimeContext, runId: string, state: VnextRunState): string | undefined {
|
|
2381
|
+
const declared = declaredRunDurationBudget(context, runId);
|
|
2382
|
+
if (!declared || !state.runningSince) return undefined;
|
|
2383
|
+
const start = Date.parse(state.runningSince);
|
|
2384
|
+
if (!Number.isFinite(start)) return undefined;
|
|
2385
|
+
return new Date(start + declared.budgetMs).toISOString();
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function driveReceiptBudget(
|
|
2389
|
+
context: VnextRuntimeContext,
|
|
2390
|
+
runId: string,
|
|
2391
|
+
state: VnextRunState,
|
|
2392
|
+
events: readonly VnextRunEvent[],
|
|
2393
|
+
closedAt: string,
|
|
2394
|
+
): VnextDriveReceiptBudget | null {
|
|
2395
|
+
const declared = declaredRunDurationBudget(context, runId);
|
|
2396
|
+
if (!declared) return null;
|
|
2397
|
+
const elapsedMs = state.runningSince ? elapsedMsBetween(state.runningSince, closedAt) : 0;
|
|
2398
|
+
const overrun = events.some((event) => (
|
|
2399
|
+
event.eventType === "run.cancel_requested" && event.payload.reason === "budget_run_duration"
|
|
2400
|
+
));
|
|
2401
|
+
return {
|
|
2402
|
+
budgetMs: declared.budgetMs,
|
|
2403
|
+
source: declared.source,
|
|
2404
|
+
elapsedMs,
|
|
2405
|
+
overrun,
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function cancelReasonFromLog(context: VnextRuntimeContext, runId: string): string {
|
|
2410
|
+
const events = context.eventStore.events(runId, 0, 1_000_000);
|
|
2411
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
2412
|
+
const event = events[index]!;
|
|
2413
|
+
if (event.eventType === "run.cancel_requested" && typeof event.payload.reason === "string") {
|
|
2414
|
+
return event.payload.reason;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return "operator_cancel";
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function armRunDurationBudgetTimer(context: VnextRuntimeContext, runId: string): () => void {
|
|
2421
|
+
const declared = declaredRunDurationBudget(context, runId);
|
|
2422
|
+
if (!declared) return () => undefined;
|
|
2423
|
+
const run = context.eventStore.run(runId);
|
|
2424
|
+
if (!run) return () => undefined;
|
|
2425
|
+
const state = foldStoredVnextRun(context, run);
|
|
2426
|
+
if (!state.runningSince) return () => undefined;
|
|
2427
|
+
const startMs = Date.parse(state.runningSince);
|
|
2428
|
+
if (!Number.isFinite(startMs)) return () => undefined;
|
|
2429
|
+
const deadlineMs = startMs + declared.budgetMs;
|
|
2430
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2431
|
+
let cleared = false;
|
|
2432
|
+
const fire = (): void => {
|
|
2433
|
+
timer = undefined;
|
|
2434
|
+
if (cleared || isVnextRuntimeContextClosed(context)) return;
|
|
2435
|
+
const nowIso = new Date().toISOString();
|
|
2436
|
+
const elapsedMs = elapsedMsBetween(state.runningSince!, nowIso);
|
|
2437
|
+
if (elapsedMs < declared.budgetMs) {
|
|
2438
|
+
arm(declared.budgetMs - elapsedMs);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
try {
|
|
2442
|
+
cancelVnextRun(context, runId, {
|
|
2443
|
+
reason: "budget_run_duration",
|
|
2444
|
+
budget: { budgetMs: declared.budgetMs, elapsedMs, source: declared.source },
|
|
2445
|
+
});
|
|
2446
|
+
} catch {
|
|
2447
|
+
// Already cancelling, terminal, or the store closed under us.
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
const arm = (remaining: number): void => {
|
|
2451
|
+
if (cleared) return;
|
|
2452
|
+
const delay = Math.min(Math.max(0, remaining), MAX_TIMER_DELAY_MS);
|
|
2453
|
+
timer = setTimeout(fire, delay);
|
|
2454
|
+
timer.unref();
|
|
2455
|
+
};
|
|
2456
|
+
const unhook = registerVnextRuntimeCloseHook(context, () => {
|
|
2457
|
+
cleared = true;
|
|
2458
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
2459
|
+
timer = undefined;
|
|
2460
|
+
});
|
|
2461
|
+
arm(deadlineMs - Date.now());
|
|
2462
|
+
return () => {
|
|
2463
|
+
if (cleared) return;
|
|
2464
|
+
cleared = true;
|
|
2465
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
2466
|
+
timer = undefined;
|
|
2467
|
+
unhook();
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function unsupportedLimit(envelope: VnextRunPlanEnvelope): VnextRunHandoff | undefined {
|
|
2323
2472
|
if (envelope.plan.limits.maxAgentTimeMs !== undefined) {
|
|
2324
2473
|
return { reason: "limit_unsupported", field: "limits.maxAgentTimeMs", detail: "agent-time budget enforcement is not available in this slice" };
|
|
2325
2474
|
}
|
|
2326
|
-
if (envelope.projectLimits.maxRunDurationMs !== undefined) {
|
|
2327
|
-
return { reason: "limit_unsupported", field: "project.limits.maxRunDurationMs", detail: "duration budget enforcement is not available in this slice" };
|
|
2328
|
-
}
|
|
2329
2475
|
if (envelope.projectLimits.maxAgentTimeMs !== undefined) {
|
|
2330
2476
|
return { reason: "limit_unsupported", field: "project.limits.maxAgentTimeMs", detail: "agent-time budget enforcement is not available in this slice" };
|
|
2331
2477
|
}
|
|
@@ -392,10 +392,17 @@ export interface VnextDriveReceipt {
|
|
|
392
392
|
lastSequence: number;
|
|
393
393
|
logHash: string;
|
|
394
394
|
settlement: VnextDriveReceiptSettlement;
|
|
395
|
-
budget: null;
|
|
395
|
+
budget: VnextDriveReceiptBudget | null;
|
|
396
396
|
producer: { id: string; closed: boolean };
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
export interface VnextDriveReceiptBudget {
|
|
400
|
+
budgetMs: number;
|
|
401
|
+
source: "workflow" | "project" | "both";
|
|
402
|
+
elapsedMs: number;
|
|
403
|
+
overrun: boolean;
|
|
404
|
+
}
|
|
405
|
+
|
|
399
406
|
/** sha256 over `eventId:sequence` for events 1..lastSequence, in sequence order. */
|
|
400
407
|
export function hashVnextDriveLog(events: readonly { eventId: string; sequence: number }[]): string {
|
|
401
408
|
const body = events.map((event) => `${event.eventId}:${event.sequence}`).join("\n");
|
|
@@ -505,7 +505,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
505
505
|
const scheduler = VnextRunScheduler.for(context, bundle);
|
|
506
506
|
const opening = scheduler.openDriveSession(runId, {
|
|
507
507
|
mode: body.mode,
|
|
508
|
-
allowLimits:
|
|
508
|
+
allowLimits: false,
|
|
509
509
|
liveMode: body.mode === "live",
|
|
510
510
|
createProducer,
|
|
511
511
|
});
|
|
@@ -464,6 +464,7 @@ export function foldStoredVnextRun(context: VnextRuntimeContext, run: VnextRunRe
|
|
|
464
464
|
if (!step || step.kind !== "gate") return undefined;
|
|
465
465
|
return envelope.gates.definitions[step.gate]?.kind;
|
|
466
466
|
},
|
|
467
|
+
projectLimits: envelope.projectLimits,
|
|
467
468
|
}
|
|
468
469
|
: {});
|
|
469
470
|
if (envelope) verifyVnextGateEvidence(context.eventStore, run, envelope, events, state);
|
|
@@ -551,7 +552,13 @@ export interface VnextRunCancelResult {
|
|
|
551
552
|
export function cancelVnextRun(
|
|
552
553
|
context: VnextRuntimeContext,
|
|
553
554
|
runId: string,
|
|
554
|
-
options: {
|
|
555
|
+
options: {
|
|
556
|
+
commandId?: string;
|
|
557
|
+
now?: string;
|
|
558
|
+
monotonicNs?: string;
|
|
559
|
+
reason?: string;
|
|
560
|
+
budget?: { budgetMs: number; elapsedMs: number; source: "workflow" | "project" | "both" };
|
|
561
|
+
} = {},
|
|
555
562
|
): VnextRunCancelResult {
|
|
556
563
|
const commandId = options.commandId ?? newVnextCommandId();
|
|
557
564
|
const now = options.now ?? new Date().toISOString();
|
|
@@ -600,10 +607,17 @@ export function cancelVnextRun(
|
|
|
600
607
|
|
|
601
608
|
const cancelReason = options.reason ?? "operator_cancel";
|
|
602
609
|
const activeAttempt = Boolean(folded.currentStep?.attemptId) || vnextFoldPanelAttemptIds(folded.currentStep).length > 0;
|
|
603
|
-
|
|
610
|
+
const cancelPayload: Record<string, unknown> = {
|
|
604
611
|
actor: { kind: "runtime", id: context.homeRuntimeId },
|
|
605
612
|
reason: cancelReason,
|
|
606
|
-
}
|
|
613
|
+
};
|
|
614
|
+
if (cancelReason === "budget_run_duration") {
|
|
615
|
+
if (!options.budget) {
|
|
616
|
+
throw runtimeError("run_events_illegal", runId, "budget_run_duration requires a budget payload");
|
|
617
|
+
}
|
|
618
|
+
cancelPayload.budget = options.budget;
|
|
619
|
+
}
|
|
620
|
+
push("run.cancel_requested", cancelPayload);
|
|
607
621
|
|
|
608
622
|
let status: VnextRunStatus = "cancelled";
|
|
609
623
|
if (folded.status === "running" && activeAttempt) {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"lastSequence": { "type": "integer", "minimum": 1 },
|
|
33
33
|
"logHash": { "$ref": "common.schema.json#/$defs/sha256" },
|
|
34
34
|
"settlement": { "$ref": "#/$defs/settlement" },
|
|
35
|
-
"budget": { "
|
|
35
|
+
"budget": { "$ref": "#/$defs/budget" },
|
|
36
36
|
"producer": {
|
|
37
37
|
"type": "object",
|
|
38
38
|
"required": ["id", "closed"],
|
|
@@ -64,6 +64,22 @@
|
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
},
|
|
67
|
+
"budget": {
|
|
68
|
+
"oneOf": [
|
|
69
|
+
{ "type": "null" },
|
|
70
|
+
{
|
|
71
|
+
"type": "object",
|
|
72
|
+
"required": ["budgetMs", "source", "elapsedMs", "overrun"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"budgetMs": { "$ref": "common.schema.json#/$defs/durationMs" },
|
|
75
|
+
"source": { "enum": ["workflow", "project", "both"] },
|
|
76
|
+
"elapsedMs": { "type": "integer", "minimum": 0 },
|
|
77
|
+
"overrun": { "type": "boolean" }
|
|
78
|
+
},
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
67
83
|
"handoff": {
|
|
68
84
|
"type": "object",
|
|
69
85
|
"required": ["reason", "detail"],
|
|
@@ -90,6 +90,24 @@
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
"if": {
|
|
95
|
+
"properties": {
|
|
96
|
+
"eventType": { "const": "run.cancel_requested" },
|
|
97
|
+
"payload": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": { "reason": { "const": "budget_run_duration" } },
|
|
100
|
+
"required": ["reason"]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": ["eventType", "payload"]
|
|
104
|
+
},
|
|
105
|
+
"then": {
|
|
106
|
+
"properties": {
|
|
107
|
+
"payload": { "type": "object", "required": ["actor", "reason", "budget"] }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
93
111
|
{
|
|
94
112
|
"if": { "properties": { "eventType": { "const": "step.entered" } }, "required": ["eventType"] },
|
|
95
113
|
"then": {
|
|
@@ -290,6 +308,16 @@
|
|
|
290
308
|
],
|
|
291
309
|
"additionalProperties": false,
|
|
292
310
|
"$defs": {
|
|
311
|
+
"runDurationBudget": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"required": ["budgetMs", "elapsedMs", "source"],
|
|
314
|
+
"properties": {
|
|
315
|
+
"budgetMs": { "$ref": "common.schema.json#/$defs/durationMs" },
|
|
316
|
+
"elapsedMs": { "type": "integer", "minimum": 0 },
|
|
317
|
+
"source": { "enum": ["workflow", "project", "both"] }
|
|
318
|
+
},
|
|
319
|
+
"additionalProperties": false
|
|
320
|
+
},
|
|
293
321
|
"repositorySnapshot": {
|
|
294
322
|
"type": "object",
|
|
295
323
|
"required": ["repositoryId", "baseCommit", "contentSnapshotHash"],
|
|
@@ -391,6 +419,7 @@
|
|
|
391
419
|
"timing": { "$ref": "common.schema.json#/$defs/timing" },
|
|
392
420
|
"usage": { "$ref": "common.schema.json#/$defs/usage" },
|
|
393
421
|
"error": { "$ref": "common.schema.json#/$defs/errorSummary" },
|
|
422
|
+
"budget": { "$ref": "#/$defs/runDurationBudget" },
|
|
394
423
|
"actor": { "$ref": "common.schema.json#/$defs/actor" },
|
|
395
424
|
"resolvedBy": { "$ref": "common.schema.json#/$defs/actor" },
|
|
396
425
|
"resolutionAction": {
|