@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.
- 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 +59 -5
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +507 -27
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +500 -26
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/vnext.ts +65 -3
- package/plugins/kxm/src/cli.ts +1 -1
- package/plugins/kxm/src/database.ts +3 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +31 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +87 -2
- package/plugins/kxm/src/vnext-engine-gate-records.ts +19 -6
- package/plugins/kxm/src/vnext-engine.ts +325 -9
- package/plugins/kxm/src/vnext-runtime-owner.ts +2 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +158 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +24 -3
- package/plugins/kxm/src/vnext-runtime.ts +17 -3
- package/schemas/vnext/common.schema.json +1 -0
- package/schemas/vnext/drive-receipt.schema.json +95 -0
- package/schemas/vnext/run-event.schema.json +39 -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 {
|
|
@@ -66,12 +68,18 @@ import {
|
|
|
66
68
|
type VnextRuntimeContext,
|
|
67
69
|
} from "./vnext-runtime.ts";
|
|
68
70
|
import {
|
|
71
|
+
hashVnextDriveLog,
|
|
69
72
|
newVnextAssignmentId,
|
|
70
73
|
newVnextAttemptId,
|
|
71
74
|
newVnextEventId,
|
|
72
75
|
projectRuntimeKey,
|
|
73
76
|
runtimeError,
|
|
77
|
+
verifyVnextDriveReceipt,
|
|
78
|
+
VNEXT_DRIVE_RECEIPT_SCHEMA,
|
|
74
79
|
type VnextAttemptCapabilityRow,
|
|
80
|
+
type VnextDriveReceipt,
|
|
81
|
+
type VnextDriveReceiptBudget,
|
|
82
|
+
type VnextDriveReceiptHandoff,
|
|
75
83
|
type VnextGateAttemptRow,
|
|
76
84
|
type VnextGateEvidenceRow,
|
|
77
85
|
type VnextGateObservationRow,
|
|
@@ -79,7 +87,7 @@ import {
|
|
|
79
87
|
type VnextRunRecord,
|
|
80
88
|
type VnextRunStatus,
|
|
81
89
|
} from "./vnext-runtime-store.ts";
|
|
82
|
-
import { vnextCanonicalJson, type JsonValue, type VnextProjectBundle } from "./vnext-config.ts";
|
|
90
|
+
import { VnextConfigError, vnextCanonicalJson, type JsonValue, type VnextProjectBundle } from "./vnext-config.ts";
|
|
83
91
|
import { evaluateArtifactsGate } from "./vnext-engine-artifacts.ts";
|
|
84
92
|
import { createCommandObserver } from "./vnext-engine-command.ts";
|
|
85
93
|
import {
|
|
@@ -383,6 +391,8 @@ async function driveAdmitted(
|
|
|
383
391
|
}
|
|
384
392
|
let latest: VnextRunDriveResult = { state: foldStoredVnextRun(context, requireRun(context, runId)) };
|
|
385
393
|
while (latest.state.status === "running") {
|
|
394
|
+
const overBudget = cancelRunDurationIfExceeded(context, runId);
|
|
395
|
+
if (overBudget) return overBudget;
|
|
386
396
|
const before = context.eventStore.runState(runId)?.lastSequence;
|
|
387
397
|
latest = await stepLocked(context, runId, producer, token);
|
|
388
398
|
if (latest.handoff) return latest;
|
|
@@ -432,6 +442,150 @@ function newDriveId(runId: string, token: string, runtimeId: string, monotonicNs
|
|
|
432
442
|
return `drv_${createHash("sha256").update(`${runId}\0${token}\0${runtimeId}\0${monotonicNs}`, "utf8").digest("hex").slice(0, 24)}`;
|
|
433
443
|
}
|
|
434
444
|
|
|
445
|
+
export interface VnextDrivePollProjection {
|
|
446
|
+
driveId: string;
|
|
447
|
+
mode: "simulated" | "live";
|
|
448
|
+
openedAt: string;
|
|
449
|
+
receipt: VnextDriveReceipt | null;
|
|
450
|
+
verified: boolean;
|
|
451
|
+
divergence?: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface VnextDriveReceiptCloseInfo {
|
|
455
|
+
kind: "terminal" | "handoff" | "unsettled";
|
|
456
|
+
reason?: string;
|
|
457
|
+
handoff?: VnextRunHandoff;
|
|
458
|
+
error?: { class: string; component: string; retryable: boolean };
|
|
459
|
+
producerId: string;
|
|
460
|
+
producerClosed: boolean;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function driveErrorSummary(error: unknown): { class: string; component: string; retryable: boolean } {
|
|
464
|
+
const raw = error instanceof VnextConfigError ? error.issues[0]?.code : undefined;
|
|
465
|
+
const className = raw && /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(raw) ? raw : "drive-failure";
|
|
466
|
+
return { class: className, component: "vnext-engine", retryable: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function receiptHandoff(handoff: VnextRunHandoff): VnextDriveReceiptHandoff {
|
|
470
|
+
return {
|
|
471
|
+
reason: handoff.reason,
|
|
472
|
+
detail: handoff.detail,
|
|
473
|
+
...(handoff.field !== undefined ? { field: handoff.field } : {}),
|
|
474
|
+
...(handoff.stepId !== undefined ? { stepId: handoff.stepId } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function appendDriveOpened(
|
|
479
|
+
context: VnextRuntimeContext,
|
|
480
|
+
runId: string,
|
|
481
|
+
driveId: string,
|
|
482
|
+
mode: "simulated" | "live",
|
|
483
|
+
): { sequence: number; occurredAt: string } {
|
|
484
|
+
return context.eventStore.transaction(() => {
|
|
485
|
+
const run = requireRun(context, runId);
|
|
486
|
+
const now = new Date().toISOString();
|
|
487
|
+
const sequence = context.eventStore.nextSequence(runId);
|
|
488
|
+
const event: VnextRunEvent = {
|
|
489
|
+
...vnextEventBase(context, run, now, vnextMonotonicNs()),
|
|
490
|
+
eventId: newVnextEventId(),
|
|
491
|
+
eventType: "run.drive_opened",
|
|
492
|
+
sequence,
|
|
493
|
+
payload: { driveId, mode },
|
|
494
|
+
};
|
|
495
|
+
context.eventStore.appendEvent(event);
|
|
496
|
+
const next = foldStoredVnextRun(context, run);
|
|
497
|
+
persistVnextRunState(context, runId, next, sequence);
|
|
498
|
+
return { sequence, occurredAt: now };
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function recordDriveReceipt(
|
|
503
|
+
context: VnextRuntimeContext,
|
|
504
|
+
session: VnextDriveSession,
|
|
505
|
+
closeInfo: VnextDriveReceiptCloseInfo,
|
|
506
|
+
): void {
|
|
507
|
+
if (isVnextRuntimeContextClosed(context)) return;
|
|
508
|
+
try {
|
|
509
|
+
const run = context.eventStore.run(session.runId);
|
|
510
|
+
if (!run) return;
|
|
511
|
+
const state = foldStoredVnextRun(context, run);
|
|
512
|
+
const events = context.eventStore.events(session.runId, 0, 1_000_000);
|
|
513
|
+
if (events.length === 0) return;
|
|
514
|
+
const opened = state.drive;
|
|
515
|
+
const openedEvent = opened
|
|
516
|
+
? events.find((event) => event.sequence === opened.openedSequence && event.eventType === "run.drive_opened")
|
|
517
|
+
: events.find((event) => event.eventType === "run.drive_opened" && event.payload.driveId === session.driveId);
|
|
518
|
+
const last = events[events.length - 1]!;
|
|
519
|
+
const openedSequence = opened?.openedSequence ?? openedEvent?.sequence;
|
|
520
|
+
if (openedSequence === undefined) return;
|
|
521
|
+
const closedAt = new Date().toISOString();
|
|
522
|
+
const receipt: VnextDriveReceipt = {
|
|
523
|
+
schema: VNEXT_DRIVE_RECEIPT_SCHEMA,
|
|
524
|
+
driveId: session.driveId,
|
|
525
|
+
runId: session.runId,
|
|
526
|
+
projectId: run.projectId,
|
|
527
|
+
homeRuntimeId: session.homeRuntimeId,
|
|
528
|
+
mode: session.mode,
|
|
529
|
+
openedAt: openedEvent?.occurredAt ?? session.openedAt,
|
|
530
|
+
openedSequence,
|
|
531
|
+
closedAt,
|
|
532
|
+
lastSequence: last.sequence,
|
|
533
|
+
logHash: hashVnextDriveLog(events.filter((event) => event.sequence >= 1 && event.sequence <= last.sequence)),
|
|
534
|
+
settlement: {
|
|
535
|
+
kind: closeInfo.kind,
|
|
536
|
+
status: state.status,
|
|
537
|
+
reason: closeInfo.reason ?? state.terminalReason ?? "",
|
|
538
|
+
...(closeInfo.kind === "handoff" && closeInfo.handoff ? { handoff: receiptHandoff(closeInfo.handoff) } : {}),
|
|
539
|
+
...(closeInfo.error !== undefined ? { error: closeInfo.error } : {}),
|
|
540
|
+
},
|
|
541
|
+
budget: driveReceiptBudget(context, run.runId, state, events, closedAt),
|
|
542
|
+
producer: { id: closeInfo.producerId, closed: closeInfo.producerClosed },
|
|
543
|
+
};
|
|
544
|
+
context.eventStore.insertDriveReceipt(receipt);
|
|
545
|
+
} catch {
|
|
546
|
+
// The drive result governs; a missing receipt is visible on poll.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function vnextDrivePollProjection(
|
|
551
|
+
context: VnextRuntimeContext,
|
|
552
|
+
runId: string,
|
|
553
|
+
state: VnextRunState,
|
|
554
|
+
): VnextDrivePollProjection | undefined {
|
|
555
|
+
if (!state.drive) return undefined;
|
|
556
|
+
const events = context.eventStore.events(runId, 0, 1_000_000);
|
|
557
|
+
const openedEvent = events.find((event) => event.sequence === state.drive!.openedSequence);
|
|
558
|
+
let receipt: VnextDriveReceipt | null = null;
|
|
559
|
+
let unreadable = false;
|
|
560
|
+
try {
|
|
561
|
+
receipt = context.eventStore.driveReceipt(state.drive.driveId) ?? null;
|
|
562
|
+
} catch {
|
|
563
|
+
unreadable = true;
|
|
564
|
+
}
|
|
565
|
+
const projection: VnextDrivePollProjection = {
|
|
566
|
+
driveId: state.drive.driveId,
|
|
567
|
+
mode: state.drive.mode,
|
|
568
|
+
openedAt: openedEvent?.occurredAt ?? "",
|
|
569
|
+
receipt: unreadable ? null : receipt,
|
|
570
|
+
verified: false,
|
|
571
|
+
};
|
|
572
|
+
if (unreadable) {
|
|
573
|
+
projection.divergence = "receipt unreadable";
|
|
574
|
+
return projection;
|
|
575
|
+
}
|
|
576
|
+
if (!receipt) {
|
|
577
|
+
projection.divergence = "no receipt";
|
|
578
|
+
return projection;
|
|
579
|
+
}
|
|
580
|
+
const checked = verifyVnextDriveReceipt(receipt, events, state.status, {
|
|
581
|
+
runId,
|
|
582
|
+
driveId: state.drive.driveId,
|
|
583
|
+
});
|
|
584
|
+
projection.verified = checked.verified;
|
|
585
|
+
if (checked.divergence !== undefined) projection.divergence = checked.divergence;
|
|
586
|
+
return projection;
|
|
587
|
+
}
|
|
588
|
+
|
|
435
589
|
function inflightAttemptId(state: VnextRunState): string | undefined {
|
|
436
590
|
const leftover = unreconciledPanelAttemptId(state);
|
|
437
591
|
if (leftover) return leftover;
|
|
@@ -595,25 +749,50 @@ export class VnextRunScheduler {
|
|
|
595
749
|
throw error;
|
|
596
750
|
}
|
|
597
751
|
const driveId = newDriveId(runId, token, this.context.homeRuntimeId, vnextMonotonicNs());
|
|
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);
|
|
598
755
|
const session: VnextDriveSession = {
|
|
599
756
|
driveId,
|
|
600
757
|
runId,
|
|
601
758
|
token,
|
|
602
759
|
homeRuntimeId: this.context.homeRuntimeId,
|
|
603
760
|
mode: options.mode,
|
|
604
|
-
openedAt:
|
|
761
|
+
openedAt: openedMeta.occurredAt,
|
|
762
|
+
producerId: producer.id,
|
|
605
763
|
controller: new AbortController(),
|
|
606
764
|
settled,
|
|
765
|
+
...(deadlineAt !== undefined ? { deadlineAt } : {}),
|
|
607
766
|
};
|
|
608
767
|
attachVnextDriveSession(this.context.eventStore.path, runId, token, session);
|
|
609
768
|
opened = true;
|
|
610
769
|
resolveOpen({ driveId, settled });
|
|
611
770
|
try {
|
|
612
771
|
const result = await driveAdmitted(this.context, runId, producer, token, driveOptions);
|
|
772
|
+
const kind = result.handoff ? "handoff" : isTerminalRunStatus(result.state.status) ? "terminal" : "unsettled";
|
|
773
|
+
recordDriveReceipt(this.context, session, {
|
|
774
|
+
kind,
|
|
775
|
+
...(result.state.terminalReason !== undefined ? { reason: result.state.terminalReason } : result.handoff ? { reason: result.handoff.reason } : {}),
|
|
776
|
+
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
777
|
+
producerId: producer.id,
|
|
778
|
+
producerClosed: false,
|
|
779
|
+
});
|
|
613
780
|
pending.resolve(result);
|
|
614
781
|
return result;
|
|
615
782
|
} catch (error) {
|
|
616
783
|
recordBareDriveFailure(this.context, runId);
|
|
784
|
+
let foldedStatus: string | undefined;
|
|
785
|
+
try {
|
|
786
|
+
foldedStatus = foldStoredVnextRun(this.context, requireRun(this.context, runId)).status;
|
|
787
|
+
} catch {
|
|
788
|
+
foldedStatus = undefined;
|
|
789
|
+
}
|
|
790
|
+
recordDriveReceipt(this.context, session, {
|
|
791
|
+
kind: foldedStatus !== undefined && isTerminalRunStatus(foldedStatus as VnextRunStatus) ? "terminal" : "unsettled",
|
|
792
|
+
error: driveErrorSummary(error),
|
|
793
|
+
producerId: producer.id,
|
|
794
|
+
producerClosed: false,
|
|
795
|
+
});
|
|
617
796
|
pending.reject(error);
|
|
618
797
|
throw error;
|
|
619
798
|
}
|
|
@@ -730,11 +909,13 @@ async function stepLocked(context: VnextRuntimeContext, runId: string, producer:
|
|
|
730
909
|
throw runtimeError("run_events_illegal", prepared.run.runId, "prepared gate is not artifacts-exist");
|
|
731
910
|
}
|
|
732
911
|
registerVnextAttemptController(context.eventStore.path, runId, { attemptId: prepared.attemptId, controller: prepared.controller });
|
|
912
|
+
const clearBudget = armRunDurationBudgetTimer(context, runId);
|
|
733
913
|
try {
|
|
734
914
|
const observation = evaluateArtifactsGate(context, definition);
|
|
735
915
|
vnextGateDispatchSeams.afterEvaluate?.(prepared);
|
|
736
916
|
return context.eventStore.transaction(() => settlePreparedGate(context, prepared, observation));
|
|
737
917
|
} finally {
|
|
918
|
+
clearBudget();
|
|
738
919
|
unregisterVnextAttemptController(context.eventStore.path, runId, prepared.attemptId);
|
|
739
920
|
}
|
|
740
921
|
}
|
|
@@ -775,6 +956,7 @@ async function runPreparedCommandGate(
|
|
|
775
956
|
});
|
|
776
957
|
armVnextGateHold(storePath, prepared.run.runId, token, prepared.attemptId, () => observer.requestStop("cancel"));
|
|
777
958
|
registerVnextAttemptController(storePath, prepared.run.runId, { attemptId: prepared.attemptId, controller: prepared.controller });
|
|
959
|
+
const clearBudget = armRunDurationBudgetTimer(context, prepared.run.runId);
|
|
778
960
|
const unhookClose = registerVnextRuntimeCloseHook(context, () => {
|
|
779
961
|
try {
|
|
780
962
|
markVnextGateHoldUnsettled(storePath, prepared.run.runId, token, prepared.attemptId);
|
|
@@ -822,6 +1004,7 @@ async function runPreparedCommandGate(
|
|
|
822
1004
|
return commitCommandUncertainty(context, prepared, token, outcome.reason, outcome.observation);
|
|
823
1005
|
} finally {
|
|
824
1006
|
unhookClose();
|
|
1007
|
+
clearBudget();
|
|
825
1008
|
unregisterVnextAttemptController(storePath, prepared.run.runId, prepared.attemptId);
|
|
826
1009
|
}
|
|
827
1010
|
}
|
|
@@ -1628,8 +1811,10 @@ async function drivePanel(
|
|
|
1628
1811
|
let stopBirths = false;
|
|
1629
1812
|
let settlementFailed = false;
|
|
1630
1813
|
|
|
1814
|
+
const budgetClears = new Map<string, () => void>();
|
|
1631
1815
|
const register = (member: PreparedDispatch): void => {
|
|
1632
1816
|
registerVnextAttemptController(context.eventStore.path, runId, { attemptId: member.attemptId, controller: member.controller });
|
|
1817
|
+
budgetClears.set(member.attemptId, armRunDurationBudgetTimer(context, runId));
|
|
1633
1818
|
owned.set(member.attemptId, member);
|
|
1634
1819
|
};
|
|
1635
1820
|
|
|
@@ -1836,6 +2021,7 @@ async function drivePanel(
|
|
|
1836
2021
|
throw error;
|
|
1837
2022
|
} finally {
|
|
1838
2023
|
for (const attemptId of owned.keys()) {
|
|
2024
|
+
budgetClears.get(attemptId)?.();
|
|
1839
2025
|
unregisterVnextAttemptController(context.eventStore.path, runId, attemptId);
|
|
1840
2026
|
}
|
|
1841
2027
|
}
|
|
@@ -2024,7 +2210,7 @@ function joinPanel(context: VnextRuntimeContext, panel: PreparedPanel): VnextRun
|
|
|
2024
2210
|
|
|
2025
2211
|
const cancelRun = (): VnextRunDriveResult => {
|
|
2026
2212
|
push("step.status_changed", { stepId: panel.stepId, status: "cancelled", previousStatus: "running" });
|
|
2027
|
-
push("run.status_changed", { status: "cancelled", reason:
|
|
2213
|
+
push("run.status_changed", { status: "cancelled", reason: cancelReasonFromLog(context, run.runId) });
|
|
2028
2214
|
for (const event of events) context.eventStore.appendEvent(event);
|
|
2029
2215
|
const next = foldStoredVnextRun(context, run);
|
|
2030
2216
|
persistVnextRunState(context, run.runId, next, events[events.length - 1]!.sequence);
|
|
@@ -2146,16 +2332,146 @@ function transitionBudgetFailure(plan: VnextCompiledPlan, state: VnextRunState,
|
|
|
2146
2332
|
return undefined;
|
|
2147
2333
|
}
|
|
2148
2334
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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;
|
|
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
|
+
}
|
|
2152
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 {
|
|
2153
2472
|
if (envelope.plan.limits.maxAgentTimeMs !== undefined) {
|
|
2154
2473
|
return { reason: "limit_unsupported", field: "limits.maxAgentTimeMs", detail: "agent-time budget enforcement is not available in this slice" };
|
|
2155
2474
|
}
|
|
2156
|
-
if (envelope.projectLimits.maxRunDurationMs !== undefined) {
|
|
2157
|
-
return { reason: "limit_unsupported", field: "project.limits.maxRunDurationMs", detail: "duration budget enforcement is not available in this slice" };
|
|
2158
|
-
}
|
|
2159
2475
|
if (envelope.projectLimits.maxAgentTimeMs !== undefined) {
|
|
2160
2476
|
return { reason: "limit_unsupported", field: "project.limits.maxAgentTimeMs", detail: "agent-time budget enforcement is not available in this slice" };
|
|
2161
2477
|
}
|
|
@@ -26,8 +26,10 @@ export interface VnextDriveSession {
|
|
|
26
26
|
homeRuntimeId: string;
|
|
27
27
|
mode: "simulated" | "live";
|
|
28
28
|
openedAt: string;
|
|
29
|
+
producerId: string;
|
|
29
30
|
controller: AbortController;
|
|
30
31
|
settled: Promise<unknown>;
|
|
32
|
+
deadlineAt?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
interface AdmissionRecord {
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "./sqlite.ts";
|
|
5
|
-
import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
|
|
5
|
+
import { VnextConfigError, validateDriveReceipt, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
|
|
6
6
|
import { vnextUserStateRoot } from "./vnext-bindings.ts";
|
|
7
7
|
|
|
8
8
|
/* ------------------------------------------------------------------ *
|
|
@@ -364,6 +364,80 @@ export interface VnextRunStateRow {
|
|
|
364
364
|
state: string;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
export interface VnextDriveReceiptHandoff {
|
|
368
|
+
reason: string;
|
|
369
|
+
field?: string;
|
|
370
|
+
stepId?: string;
|
|
371
|
+
detail: string;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface VnextDriveReceiptSettlement {
|
|
375
|
+
kind: "terminal" | "handoff" | "unsettled";
|
|
376
|
+
status: string;
|
|
377
|
+
reason: string;
|
|
378
|
+
handoff?: VnextDriveReceiptHandoff;
|
|
379
|
+
error?: { class: string; component: string; retryable: boolean };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface VnextDriveReceipt {
|
|
383
|
+
schema: typeof VNEXT_DRIVE_RECEIPT_SCHEMA;
|
|
384
|
+
driveId: string;
|
|
385
|
+
runId: string;
|
|
386
|
+
projectId: string;
|
|
387
|
+
homeRuntimeId: string;
|
|
388
|
+
mode: "simulated" | "live";
|
|
389
|
+
openedAt: string;
|
|
390
|
+
openedSequence: number;
|
|
391
|
+
closedAt: string;
|
|
392
|
+
lastSequence: number;
|
|
393
|
+
logHash: string;
|
|
394
|
+
settlement: VnextDriveReceiptSettlement;
|
|
395
|
+
budget: VnextDriveReceiptBudget | null;
|
|
396
|
+
producer: { id: string; closed: boolean };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface VnextDriveReceiptBudget {
|
|
400
|
+
budgetMs: number;
|
|
401
|
+
source: "workflow" | "project" | "both";
|
|
402
|
+
elapsedMs: number;
|
|
403
|
+
overrun: boolean;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** sha256 over `eventId:sequence` for events 1..lastSequence, in sequence order. */
|
|
407
|
+
export function hashVnextDriveLog(events: readonly { eventId: string; sequence: number }[]): string {
|
|
408
|
+
const body = events.map((event) => `${event.eventId}:${event.sequence}`).join("\n");
|
|
409
|
+
return `sha256:${createHash("sha256").update(body, "utf8").digest("hex")}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function verifyVnextDriveReceipt(
|
|
413
|
+
receipt: VnextDriveReceipt,
|
|
414
|
+
events: readonly VnextRunEvent[],
|
|
415
|
+
foldedStatus: string,
|
|
416
|
+
binding: { runId: string; driveId: string },
|
|
417
|
+
): { verified: boolean; divergence?: string } {
|
|
418
|
+
const reasons: string[] = [];
|
|
419
|
+
if (receipt.runId !== binding.runId) {
|
|
420
|
+
reasons.push(`runId ${receipt.runId} != binding ${binding.runId}`);
|
|
421
|
+
}
|
|
422
|
+
if (receipt.driveId !== binding.driveId) {
|
|
423
|
+
reasons.push(`driveId ${receipt.driveId} != binding ${binding.driveId}`);
|
|
424
|
+
}
|
|
425
|
+
const last = events[events.length - 1];
|
|
426
|
+
const currentLast = last?.sequence ?? 0;
|
|
427
|
+
if (receipt.lastSequence !== currentLast) {
|
|
428
|
+
reasons.push(`lastSequence ${receipt.lastSequence} != log ${currentLast}`);
|
|
429
|
+
}
|
|
430
|
+
const prefix = events.filter((event) => event.sequence >= 1 && event.sequence <= receipt.lastSequence);
|
|
431
|
+
if (hashVnextDriveLog(prefix) !== receipt.logHash) {
|
|
432
|
+
reasons.push("logHash mismatch");
|
|
433
|
+
}
|
|
434
|
+
if (receipt.settlement.status !== foldedStatus) {
|
|
435
|
+
reasons.push(`settlement.status ${receipt.settlement.status} != folded ${foldedStatus}`);
|
|
436
|
+
}
|
|
437
|
+
if (reasons.length === 0) return { verified: true };
|
|
438
|
+
return { verified: false, divergence: reasons.join("; ") };
|
|
439
|
+
}
|
|
440
|
+
|
|
367
441
|
export interface VnextAttemptCapabilityRow {
|
|
368
442
|
attemptId: string;
|
|
369
443
|
runId: string;
|
|
@@ -472,7 +546,9 @@ export interface VnextCommandRecord {
|
|
|
472
546
|
recordedAt: string;
|
|
473
547
|
}
|
|
474
548
|
|
|
475
|
-
export const VNEXT_EVENT_STORE_SCHEMA_VERSION =
|
|
549
|
+
export const VNEXT_EVENT_STORE_SCHEMA_VERSION = 4;
|
|
550
|
+
export const VNEXT_DRIVE_RECEIPT_SCHEMA = "kxm.drive-receipt.v1";
|
|
551
|
+
export const DRIVE_RECEIPT_MAX_BYTES = 8 * 1024;
|
|
476
552
|
|
|
477
553
|
const EVENT_STORE_TABLES = {
|
|
478
554
|
runs: ["run_id", "project_id", "home_runtime_id", "workflow_id", "prompt_sha256", "status", "config_revision", "memory_revision", "executor_policy_revision", "tool_policy_revision", "created_at", "updated_at"],
|
|
@@ -497,6 +573,7 @@ const EVENT_STORE_TABLES = {
|
|
|
497
573
|
"evidence_id", "attempt_id", "observation_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt",
|
|
498
574
|
"assignment_id", "effect_id", "evidence_key", "kind", "expect", "outcome", "settled_event_id", "content_hash",
|
|
499
575
|
],
|
|
576
|
+
drive_receipts: ["drive_id", "run_id", "project_id", "opened_sequence", "last_sequence", "closed_at", "schema", "receipt"],
|
|
500
577
|
} as const;
|
|
501
578
|
|
|
502
579
|
const EVENT_STORE_SCHEMA = `
|
|
@@ -639,8 +716,43 @@ CREATE TABLE gate_evidence (
|
|
|
639
716
|
) STRICT;
|
|
640
717
|
CREATE UNIQUE INDEX gate_evidence_key ON gate_evidence(run_id, step_id, step_attempt, evidence_key) WHERE evidence_key IS NOT NULL;
|
|
641
718
|
CREATE INDEX gate_evidence_run ON gate_evidence(run_id);
|
|
719
|
+
CREATE TABLE drive_receipts (
|
|
720
|
+
drive_id TEXT PRIMARY KEY,
|
|
721
|
+
run_id TEXT NOT NULL,
|
|
722
|
+
project_id TEXT NOT NULL,
|
|
723
|
+
opened_sequence INTEGER NOT NULL,
|
|
724
|
+
last_sequence INTEGER NOT NULL,
|
|
725
|
+
closed_at TEXT NOT NULL,
|
|
726
|
+
schema TEXT NOT NULL,
|
|
727
|
+
receipt TEXT NOT NULL
|
|
728
|
+
) STRICT;
|
|
729
|
+
CREATE INDEX drive_receipts_run ON drive_receipts(run_id);
|
|
642
730
|
`;
|
|
643
731
|
|
|
732
|
+
const DRIVE_RECEIPTS_DDL = `
|
|
733
|
+
CREATE TABLE IF NOT EXISTS drive_receipts (
|
|
734
|
+
drive_id TEXT PRIMARY KEY,
|
|
735
|
+
run_id TEXT NOT NULL,
|
|
736
|
+
project_id TEXT NOT NULL,
|
|
737
|
+
opened_sequence INTEGER NOT NULL,
|
|
738
|
+
last_sequence INTEGER NOT NULL,
|
|
739
|
+
closed_at TEXT NOT NULL,
|
|
740
|
+
schema TEXT NOT NULL,
|
|
741
|
+
receipt TEXT NOT NULL
|
|
742
|
+
) STRICT;
|
|
743
|
+
CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
|
|
744
|
+
`;
|
|
745
|
+
|
|
746
|
+
const EVENT_STORE_MIGRATIONS = [
|
|
747
|
+
{
|
|
748
|
+
fromVersion: 3,
|
|
749
|
+
toVersion: 4,
|
|
750
|
+
migrate(database: DatabaseSync): void {
|
|
751
|
+
database.exec(DRIVE_RECEIPTS_DDL);
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
];
|
|
755
|
+
|
|
644
756
|
export class VnextRunEventStore {
|
|
645
757
|
readonly path: string;
|
|
646
758
|
private readonly database: DatabaseSync;
|
|
@@ -651,6 +763,7 @@ export class VnextRunEventStore {
|
|
|
651
763
|
schema: EVENT_STORE_SCHEMA,
|
|
652
764
|
version: VNEXT_EVENT_STORE_SCHEMA_VERSION,
|
|
653
765
|
tables: EVENT_STORE_TABLES,
|
|
766
|
+
migrations: EVENT_STORE_MIGRATIONS,
|
|
654
767
|
});
|
|
655
768
|
}
|
|
656
769
|
|
|
@@ -871,6 +984,43 @@ export class VnextRunEventStore {
|
|
|
871
984
|
`).run(row.runId, row.lastSequence, row.state);
|
|
872
985
|
}
|
|
873
986
|
|
|
987
|
+
insertDriveReceipt(receipt: VnextDriveReceipt): { inserted: boolean } {
|
|
988
|
+
validateDriveReceipt(receipt, "drive-receipt");
|
|
989
|
+
const canonical = vnextCanonicalJson(receipt as unknown as JsonValue);
|
|
990
|
+
if (Buffer.byteLength(canonical, "utf8") > DRIVE_RECEIPT_MAX_BYTES) {
|
|
991
|
+
throw runtimeError("drive_receipt_invalid", receipt.driveId, `drive receipt exceeds ${DRIVE_RECEIPT_MAX_BYTES} bytes`);
|
|
992
|
+
}
|
|
993
|
+
const result = this.database.prepare(`
|
|
994
|
+
INSERT INTO drive_receipts (drive_id, run_id, project_id, opened_sequence, last_sequence, closed_at, schema, receipt)
|
|
995
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
996
|
+
ON CONFLICT(drive_id) DO NOTHING
|
|
997
|
+
`).run(
|
|
998
|
+
receipt.driveId,
|
|
999
|
+
receipt.runId,
|
|
1000
|
+
receipt.projectId,
|
|
1001
|
+
receipt.openedSequence,
|
|
1002
|
+
receipt.lastSequence,
|
|
1003
|
+
receipt.closedAt,
|
|
1004
|
+
receipt.schema,
|
|
1005
|
+
canonical,
|
|
1006
|
+
);
|
|
1007
|
+
return { inserted: Number(result.changes) === 1 };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
driveReceipt(driveId: string): VnextDriveReceipt | undefined {
|
|
1011
|
+
const row = this.database.prepare("SELECT receipt FROM drive_receipts WHERE drive_id = ?").get(driveId) as
|
|
1012
|
+
| { receipt: string }
|
|
1013
|
+
| undefined;
|
|
1014
|
+
return row ? parseDriveReceipt(row.receipt) : undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
driveReceiptsForRun(runId: string, limit = 20): VnextDriveReceipt[] {
|
|
1018
|
+
const rows = this.database.prepare(`
|
|
1019
|
+
SELECT receipt FROM drive_receipts WHERE run_id = ? ORDER BY closed_at DESC, drive_id DESC LIMIT ?
|
|
1020
|
+
`).all(runId, limit) as Array<{ receipt: string }>;
|
|
1021
|
+
return rows.map((row) => parseDriveReceipt(row.receipt));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
874
1024
|
insertCapability(row: VnextAttemptCapabilityRow): void {
|
|
875
1025
|
this.database.prepare(`
|
|
876
1026
|
INSERT INTO attempt_capabilities (attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state)
|
|
@@ -1075,6 +1225,12 @@ export class VnextRunEventStore {
|
|
|
1075
1225
|
}
|
|
1076
1226
|
}
|
|
1077
1227
|
|
|
1228
|
+
function parseDriveReceipt(raw: string): VnextDriveReceipt {
|
|
1229
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
1230
|
+
validateDriveReceipt(parsed, "drive-receipt");
|
|
1231
|
+
return parsed as VnextDriveReceipt;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1078
1234
|
function runFromRow(row: {
|
|
1079
1235
|
run_id: string;
|
|
1080
1236
|
project_id: string;
|