@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
|
@@ -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,21 @@ 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 type VnextDriveReceiptListItem =
|
|
400
|
+
| VnextDriveReceipt
|
|
401
|
+
| { readonly driveId: string; readonly divergence: string };
|
|
402
|
+
|
|
403
|
+
export interface VnextDriveReceiptBudget {
|
|
404
|
+
budgetMs: number;
|
|
405
|
+
source: "workflow" | "project" | "both";
|
|
406
|
+
elapsedMs: number;
|
|
407
|
+
overrun: boolean;
|
|
408
|
+
}
|
|
409
|
+
|
|
399
410
|
/** sha256 over `eventId:sequence` for events 1..lastSequence, in sequence order. */
|
|
400
411
|
export function hashVnextDriveLog(events: readonly { eventId: string; sequence: number }[]): string {
|
|
401
412
|
const body = events.map((event) => `${event.eventId}:${event.sequence}`).join("\n");
|
|
@@ -1007,11 +1018,17 @@ export class VnextRunEventStore {
|
|
|
1007
1018
|
return row ? parseDriveReceipt(row.receipt) : undefined;
|
|
1008
1019
|
}
|
|
1009
1020
|
|
|
1010
|
-
driveReceiptsForRun(runId: string, limit = 20):
|
|
1021
|
+
driveReceiptsForRun(runId: string, limit = 20): VnextDriveReceiptListItem[] {
|
|
1011
1022
|
const rows = this.database.prepare(`
|
|
1012
|
-
SELECT receipt FROM drive_receipts WHERE run_id = ? ORDER BY closed_at DESC, drive_id DESC LIMIT ?
|
|
1013
|
-
`).all(runId, limit) as Array<{ receipt: string }>;
|
|
1014
|
-
return rows.map((row) =>
|
|
1023
|
+
SELECT drive_id, receipt FROM drive_receipts WHERE run_id = ? ORDER BY closed_at DESC, drive_id DESC LIMIT ?
|
|
1024
|
+
`).all(runId, limit) as Array<{ drive_id: string; receipt: string }>;
|
|
1025
|
+
return rows.map((row) => {
|
|
1026
|
+
try {
|
|
1027
|
+
return parseDriveReceipt(row.receipt);
|
|
1028
|
+
} catch {
|
|
1029
|
+
return { driveId: row.drive_id, divergence: "receipt unreadable" };
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1015
1032
|
}
|
|
1016
1033
|
|
|
1017
1034
|
insertCapability(row: VnextAttemptCapabilityRow): void {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
VnextRuntimeRegistry,
|
|
10
10
|
projectRuntimeKey,
|
|
11
11
|
runtimeError,
|
|
12
|
+
verifyVnextDriveReceipt,
|
|
12
13
|
vnextRuntimePaths,
|
|
13
14
|
type VnextRuntimePaths,
|
|
14
15
|
} from "./vnext-runtime-store.ts";
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
import { createVnextOneShotProducer } from "./vnext-oneshot-producer.ts";
|
|
25
26
|
import { isProducerAdmitted } from "./producers.ts";
|
|
26
27
|
import { VnextRunScheduler, createVnextSimulatedProducer, recordDriveReceipt, recoverVnextRun, vnextDrivePollProjection } from "./vnext-engine.ts";
|
|
27
|
-
import { vnextOpenDriveSessions } from "./vnext-runtime-owner.ts";
|
|
28
|
+
import { vnextDriveSession, vnextOpenDriveSessions } from "./vnext-runtime-owner.ts";
|
|
28
29
|
|
|
29
30
|
/* ------------------------------------------------------------------ *
|
|
30
31
|
* Token management
|
|
@@ -505,7 +506,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
505
506
|
const scheduler = VnextRunScheduler.for(context, bundle);
|
|
506
507
|
const opening = scheduler.openDriveSession(runId, {
|
|
507
508
|
mode: body.mode,
|
|
508
|
-
allowLimits:
|
|
509
|
+
allowLimits: false,
|
|
509
510
|
liveMode: body.mode === "live",
|
|
510
511
|
createProducer,
|
|
511
512
|
});
|
|
@@ -551,6 +552,24 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
551
552
|
sendJson(response, 200, { ok: true, events });
|
|
552
553
|
return;
|
|
553
554
|
}
|
|
555
|
+
|
|
556
|
+
if (request.method === "GET" && sub === "drive") {
|
|
557
|
+
const run = context.eventStore.run(runId);
|
|
558
|
+
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
559
|
+
|
|
560
|
+
const activeSession = vnextDriveSession(context.eventStore.path, runId);
|
|
561
|
+
const session = activeSession
|
|
562
|
+
? {
|
|
563
|
+
driveId: activeSession.driveId,
|
|
564
|
+
mode: activeSession.mode,
|
|
565
|
+
openedAt: activeSession.openedAt,
|
|
566
|
+
deadlineAt: activeSession.deadlineAt ?? null,
|
|
567
|
+
}
|
|
568
|
+
: null;
|
|
569
|
+
const receipts = context.eventStore.driveReceiptsForRun(runId, 20);
|
|
570
|
+
sendJson(response, 200, { ok: true, session, receipts });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
554
573
|
if (request.method === "POST" && sub === "cancel") {
|
|
555
574
|
const body = await readJsonBody(request);
|
|
556
575
|
const result = cancelVnextRun(context, runId, {
|
|
@@ -585,6 +604,47 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
585
604
|
}
|
|
586
605
|
}
|
|
587
606
|
|
|
607
|
+
const driveMatch = /^\/v1\/drives\/([A-Za-z0-9_-]+)$/.exec(url.pathname);
|
|
608
|
+
if (request.method === "GET" && driveMatch) {
|
|
609
|
+
const driveId = driveMatch[1] as string;
|
|
610
|
+
const projectRoot = url.searchParams.get("projectRoot") ?? "";
|
|
611
|
+
if (!projectRoot) {
|
|
612
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const context = contextFor(projectRoot);
|
|
616
|
+
|
|
617
|
+
let receipt;
|
|
618
|
+
try {
|
|
619
|
+
receipt = context.eventStore.driveReceipt(driveId);
|
|
620
|
+
} catch {
|
|
621
|
+
sendJson(response, 200, { ok: true, receipt: null, verified: false, divergence: "receipt unreadable" });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!receipt) {
|
|
626
|
+
sendJson(response, 404, { ok: false, error: "drive_receipt_missing", message: "receipt not found" });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const runId = receipt.runId;
|
|
631
|
+
const stored = context.eventStore.run(runId);
|
|
632
|
+
if (!stored) {
|
|
633
|
+
sendJson(response, 200, { ok: true, receipt, verified: false, divergence: "run missing" });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const state = foldStoredVnextRun(context, stored);
|
|
637
|
+
const events = context.eventStore.events(runId, 0, 1_000_000);
|
|
638
|
+
const checked = verifyVnextDriveReceipt(receipt, events, state.status, { runId, driveId });
|
|
639
|
+
sendJson(response, 200, {
|
|
640
|
+
ok: true,
|
|
641
|
+
receipt,
|
|
642
|
+
verified: checked.verified,
|
|
643
|
+
...(checked.divergence !== undefined ? { divergence: checked.divergence } : {}),
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
588
648
|
const projectRunsMatch = /^\/v1\/projects\/(prj_[A-Za-z0-9_-]+)\/runs$/.exec(url.pathname);
|
|
589
649
|
if (request.method === "GET" && projectRunsMatch) {
|
|
590
650
|
const projectRoot = url.searchParams.get("projectRoot") ?? "";
|
|
@@ -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": {
|