@kontextmind/kxm 0.7.14 → 0.7.16

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/configuration.md +1 -2
  3. package/docs/operator-pi-packages.md +5 -2
  4. package/package.json +1 -1
  5. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  6. package/plugins/kxm/dist/cli.js +20 -3
  7. package/plugins/kxm/dist/core.js +4 -0
  8. package/plugins/kxm/dist/extension.js +3066 -2
  9. package/plugins/kxm/dist/mcp-server.js +1 -1
  10. package/plugins/kxm/dist/runtime.js +263 -70
  11. package/plugins/kxm/dist/server.js +4 -0
  12. package/plugins/kxm/dist/vnext-runtime-supervisor.js +259 -70
  13. package/plugins/kxm/package.json +1 -1
  14. package/plugins/kxm/src/cli/vnext.ts +20 -3
  15. package/plugins/kxm/src/extension.ts +9 -0
  16. package/plugins/kxm/src/mcp-server.ts +1 -1
  17. package/plugins/kxm/src/providers/antigravity/LICENSE +21 -0
  18. package/plugins/kxm/src/providers/antigravity/README.md +19 -0
  19. package/plugins/kxm/src/providers/antigravity/auth/index.ts +17 -0
  20. package/plugins/kxm/src/providers/antigravity/auth/oauth.ts +449 -0
  21. package/plugins/kxm/src/providers/antigravity/client/client.ts +562 -0
  22. package/plugins/kxm/src/providers/antigravity/client/index.ts +1 -0
  23. package/plugins/kxm/src/providers/antigravity/diagnostics/diagnostics.ts +97 -0
  24. package/plugins/kxm/src/providers/antigravity/diagnostics/index.ts +1 -0
  25. package/plugins/kxm/src/providers/antigravity/index.ts +24 -0
  26. package/plugins/kxm/src/providers/antigravity/models/discovery.ts +155 -0
  27. package/plugins/kxm/src/providers/antigravity/models/grouping.ts +425 -0
  28. package/plugins/kxm/src/providers/antigravity/models/index.ts +3 -0
  29. package/plugins/kxm/src/providers/antigravity/models/models.ts +501 -0
  30. package/plugins/kxm/src/providers/antigravity/pi-compat.ts +279 -0
  31. package/plugins/kxm/src/providers/antigravity/register.ts +123 -0
  32. package/plugins/kxm/src/providers/antigravity/stream/index.ts +1 -0
  33. package/plugins/kxm/src/providers/antigravity/stream/stream.ts +1461 -0
  34. package/plugins/kxm/src/providers/antigravity/types/enums.ts +50 -0
  35. package/plugins/kxm/src/providers/antigravity/types/index.ts +2 -0
  36. package/plugins/kxm/src/providers/antigravity/types/types.ts +293 -0
  37. package/plugins/kxm/src/providers/antigravity/usage/index.ts +1 -0
  38. package/plugins/kxm/src/providers/antigravity/usage/usage.ts +361 -0
  39. package/plugins/kxm/src/providers/antigravity/utils/http.ts +32 -0
  40. package/plugins/kxm/src/providers/antigravity/utils/index.ts +3 -0
  41. package/plugins/kxm/src/providers/antigravity/utils/security.ts +75 -0
  42. package/plugins/kxm/src/providers/antigravity/utils/util.ts +133 -0
  43. package/plugins/kxm/src/redact.ts +4 -0
  44. package/plugins/kxm/src/vnext-engine.ts +183 -3
  45. package/plugins/kxm/src/vnext-runtime-owner.ts +50 -0
  46. package/plugins/kxm/src/vnext-runtime-supervisor.ts +83 -58
  47. package/plugins/kxm/src/vnext-runtime.ts +9 -8
@@ -0,0 +1,32 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import { antigravityEnv } from "./util.ts";
3
+
4
+ const PREWARM_TIMEOUT_MS = 5_000;
5
+
6
+ /** Host fetch. Node 22+ already pools connections; no extra HTTP client dep. */
7
+ export async function antigravityFetch(
8
+ input: string | URL,
9
+ init: RequestInit = {},
10
+ ): Promise<Response> {
11
+ return fetch(input, init);
12
+ }
13
+
14
+ /**
15
+ * Open the TLS connection when the extension loads so the first message of a session
16
+ * does not pay the handshake either. Best-effort: failures are ignored.
17
+ */
18
+ export function prewarmConnection(url: string): void {
19
+ if (antigravityEnv("NO_PREWARM") === "1") return;
20
+ if (process.env.NODE_TEST_CONTEXT) return;
21
+ void (async () => {
22
+ try {
23
+ const res = await antigravityFetch(url, {
24
+ method: "HEAD",
25
+ signal: AbortSignal.timeout(PREWARM_TIMEOUT_MS),
26
+ });
27
+ await res.arrayBuffer();
28
+ } catch {
29
+ // Warm-up only; the real request will establish the connection instead.
30
+ }
31
+ })();
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./util.ts";
2
+ export * from "./security.ts";
3
+ export * from "./http.ts";
@@ -0,0 +1,75 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import { antigravityEnv } from "./util.ts";
3
+
4
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
5
+ const ALLOWED_API_HOST_SUFFIXES = [".googleapis.com", ".sandbox.googleapis.com"];
6
+
7
+ /** Only loopback binds are allowed so OAuth codes cannot be stolen off-machine. */
8
+ export function resolveCallbackHost(raw = antigravityEnv("CALLBACK_HOST")): string {
9
+ const host = (raw || "127.0.0.1").trim().toLowerCase();
10
+ if (!LOOPBACK_HOSTS.has(host)) {
11
+ throw new Error(
12
+ `Unsafe ANTIGRAVITY_CALLBACK_HOST="${host}". Only loopback hosts are allowed: 127.0.0.1, ::1, localhost.`,
13
+ );
14
+ }
15
+ return host === "localhost" ? "127.0.0.1" : host;
16
+ }
17
+
18
+ /** Prevent token exfiltration via poisoned BASE_URL (SSRF / credential leak). */
19
+ export function assertSafeApiBaseUrl(raw: string): string {
20
+ let url: URL;
21
+ try {
22
+ url = new URL(raw);
23
+ } catch {
24
+ throw new Error(`Invalid ANTIGRAVITY_BASE_URL: ${raw}`);
25
+ }
26
+ if (url.protocol !== "https:") {
27
+ throw new Error(`ANTIGRAVITY_BASE_URL must use https (got ${url.protocol})`);
28
+ }
29
+ if (url.username || url.password) {
30
+ throw new Error("ANTIGRAVITY_BASE_URL must not include credentials");
31
+ }
32
+ const host = url.hostname.toLowerCase();
33
+ const allowed =
34
+ host === "googleapis.com" || ALLOWED_API_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
35
+ if (!allowed) {
36
+ throw new Error(
37
+ `ANTIGRAVITY_BASE_URL host "${host}" is not allowed. Use a *.googleapis.com endpoint.`,
38
+ );
39
+ }
40
+ const path = url.pathname.replace(/\/+$/, "");
41
+ return `${url.origin}${path === "/" ? "" : path}`;
42
+ }
43
+
44
+ /** Redact bearer tokens, refresh tokens, and similar secrets from diagnostics/errors. */
45
+ export function redactSecrets(text: string): string {
46
+ return text
47
+ .replace(/\bya29\.[A-Za-z0-9._~+/-]+=*/g, "[redacted-access-token]")
48
+ .replace(/\b1\/\/[A-Za-z0-9_-]+/g, "[redacted-refresh-token]")
49
+ .replace(/\b1\/[A-Za-z0-9_-]{20,}/g, "[redacted-refresh-token]")
50
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [redacted]")
51
+ .replace(
52
+ /("?(?:access_token|refresh_token|id_token|token|client_secret|code_verifier|authorization)"?\s*[:=]\s*")[^"]*(")/gi,
53
+ "$1[redacted]$2",
54
+ )
55
+ .replace(
56
+ /("?(?:access_token|refresh_token|id_token|token|client_secret|code_verifier|authorization)"?\s*[:=]\s*)[^\s&,}]+/gi,
57
+ "$1[redacted]",
58
+ );
59
+ }
60
+
61
+ export function maskEmail(email: string | undefined): string | undefined {
62
+ if (!email || typeof email !== "string") return undefined;
63
+ const parts = email.split("@");
64
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return "[redacted-email]";
65
+ const name = parts[0];
66
+ const domain = parts[1];
67
+ const lastChar = name.at(-1) || "";
68
+ const maskedName = name.length > 2 ? `${name[0]}***${lastChar}` : `${name[0]}***`;
69
+ return `${maskedName}@${domain}`;
70
+ }
71
+
72
+ export function safeError(error: unknown): string {
73
+ const raw = error instanceof Error ? error.message : String(error);
74
+ return redactSecrets(raw);
75
+ }
@@ -0,0 +1,133 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import { createHash } from "node:crypto";
3
+ import { getModelEnum } from "../models/models.ts";
4
+
5
+ export function antigravityEnv(name: string): string | undefined {
6
+ return process.env[`ANTIGRAVITY_${name}`] || process.env[`NOAGY_${name}`];
7
+ }
8
+
9
+ export function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ export function asString(value: unknown): string | undefined {
14
+ return typeof value === "string" && value ? value : undefined;
15
+ }
16
+
17
+ export function sanitizeText(text: unknown): string {
18
+ return String(text ?? "").replace(/[\uD800-\uDFFF]/g, "\uFFFD");
19
+ }
20
+
21
+ export function escapeHtml(text: string): string {
22
+ return text
23
+ .replace(/&/g, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;")
27
+ .replace(/'/g, "&#39;");
28
+ }
29
+
30
+ export function escapeRegExp(text: string): string {
31
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ }
33
+
34
+ export function nowRequestId(): string {
35
+ return antigravityRequestEnvelope("unknown", false).requestId;
36
+ }
37
+
38
+ /** Deterministic RFC 4122 v5 UUID from seed (survives restarts for the same session seed). */
39
+ export function stableUuid(seed: string): string {
40
+ const bytes = createHash("sha1").update(seed).digest().subarray(0, 16);
41
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
42
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
43
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
44
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
45
+ }
46
+
47
+ export type AntigravityEnvelopeOptions = {
48
+ isClaude?: boolean;
49
+ isNonGemini?: boolean;
50
+ step?: number;
51
+ lastStepIndex?: string;
52
+ requestIndex?: number;
53
+ userTurnIndex?: number;
54
+ trajectoryId?: string;
55
+ conversationId?: string;
56
+ };
57
+
58
+ const sessionTrajectoryMap = new Map<string, { conversationId: string; trajectoryId: string }>();
59
+
60
+ /** Stable conversationId and trajectoryId within a multi-turn conversation session. */
61
+ export function resolveSessionTrajectory(context?: {
62
+ messages?: Array<{ role?: string; timestamp?: number; content?: unknown }>;
63
+ }): { conversationId: string; trajectoryId: string } {
64
+ const firstMsg = context?.messages?.[0];
65
+ if (!firstMsg) {
66
+ return { conversationId: crypto.randomUUID(), trajectoryId: crypto.randomUUID() };
67
+ }
68
+ const contentSeed =
69
+ typeof firstMsg.content === "string"
70
+ ? firstMsg.content.slice(0, 64)
71
+ : Array.isArray(firstMsg.content)
72
+ ? JSON.stringify(firstMsg.content[0] ?? "").slice(0, 64)
73
+ : "";
74
+ const seed = `${firstMsg.role || "user"}:${firstMsg.timestamp || ""}:${contentSeed}`;
75
+ let entry = sessionTrajectoryMap.get(seed);
76
+ if (!entry) {
77
+ entry = {
78
+ conversationId: stableUuid(`antigravity:conv:${seed}`),
79
+ trajectoryId: stableUuid(`antigravity:traj:${seed}`),
80
+ };
81
+ sessionTrajectoryMap.set(seed, entry);
82
+ if (sessionTrajectoryMap.size > 64) {
83
+ const oldestKey = sessionTrajectoryMap.keys().next().value;
84
+ if (oldestKey !== undefined) sessionTrajectoryMap.delete(oldestKey);
85
+ }
86
+ }
87
+ return entry;
88
+ }
89
+
90
+ export function clearSessionTrajectoryMap(): void {
91
+ sessionTrajectoryMap.clear();
92
+ }
93
+
94
+ export function antigravityRequestEnvelope(
95
+ wireModelId: string,
96
+ optionsOrIsClaude: boolean | AntigravityEnvelopeOptions = false,
97
+ ): { requestId: string; sessionId: string; labels: Record<string, string> } {
98
+ const options: AntigravityEnvelopeOptions =
99
+ typeof optionsOrIsClaude === "boolean" ? { isClaude: optionsOrIsClaude } : optionsOrIsClaude;
100
+
101
+ const isClaude = Boolean(options.isClaude);
102
+ const isNonGemini = Boolean(options.isNonGemini || isClaude);
103
+ const step = Math.max(1, options.step ?? 1);
104
+ const lastStepIndex = options.lastStepIndex ?? String(Math.max(0, step - 1));
105
+ const requestIndex = options.requestIndex ?? options.userTurnIndex ?? Math.max(0, step - 1);
106
+ const agentId = options.conversationId || crypto.randomUUID();
107
+ const trajectoryId = options.trajectoryId || crypto.randomUUID();
108
+ const bytes = crypto.getRandomValues(new Uint8Array(8));
109
+ const sessionId = String(new DataView(bytes.buffer, bytes.byteOffset, 8).getBigInt64(0, true));
110
+
111
+ const claudeLabel = isClaude ? "true" : "false";
112
+ const nonGeminiLabel = isNonGemini ? "true" : "false";
113
+
114
+ const labels: Record<string, string> = {
115
+ last_step_index: lastStepIndex,
116
+ request_id: `${trajectoryId}-${requestIndex}`,
117
+ trajectory_id: trajectoryId,
118
+ used_claude: claudeLabel,
119
+ used_claude_conservative: claudeLabel,
120
+ used_non_gemini_model: nonGeminiLabel,
121
+ };
122
+
123
+ const modelEnum = getModelEnum(wireModelId);
124
+ if (modelEnum) {
125
+ labels.model_enum = modelEnum;
126
+ }
127
+
128
+ return {
129
+ requestId: `agent/${agentId}/${Date.now()}/${trajectoryId}/${step}`,
130
+ sessionId,
131
+ labels,
132
+ };
133
+ }
@@ -4,6 +4,10 @@ const SECRET_PATTERNS: RegExp[] = [
4
4
  /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
5
5
  /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/gi,
6
6
  /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
7
+ /\bya29\.[A-Za-z0-9._~+/-]+=*/g,
8
+ /\b1\/\/[A-Za-z0-9_-]+/g,
9
+ /\b1\/[A-Za-z0-9_-]{20,}/g,
10
+ /("?(?:access_token|refresh_token|id_token)"?\s*[:=]\s*")[^"]*(")/gi,
7
11
  /\bKXM_[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*=\S+/gi,
8
12
  /\b(GITHUB_TOKEN|GH_TOKEN|KXM_AUTH_TOKEN|KXM_WORKFLOW_SIGNAL_SECRET)=\S+/gi,
9
13
  /\b[A-Fa-f0-9]{64}\b/g,
@@ -33,7 +33,9 @@ import { gateRegistryHash } from "./vnext-gate-hash.ts";
33
33
  import {
34
34
  admitVnextRun,
35
35
  armVnextGateHold,
36
+ attachVnextDriveSession,
36
37
  bindVnextSchedulerPolicy,
38
+ clearVnextDriveSession,
37
39
  dropVnextGateStopHook,
38
40
  enqueueVnextScheduledRun,
39
41
  finishVnextOwnedGate,
@@ -47,6 +49,7 @@ import {
47
49
  vnextAttemptControllers,
48
50
  vnextGateHold,
49
51
  vnextSchedulerPolicy,
52
+ type VnextDriveSession,
50
53
  } from "./vnext-runtime-owner.ts";
51
54
  import {
52
55
  cancelVnextRun,
@@ -161,6 +164,7 @@ export interface VnextProducerResult {
161
164
  export interface VnextProducer {
162
165
  readonly id: "driver-simulated" | "pi" | "oneshot";
163
166
  produce(request: VnextProducerRequest): Promise<VnextProducerResult>;
167
+ close?(): Promise<void>;
164
168
  }
165
169
 
166
170
  export interface VnextRunHandoff {
@@ -412,13 +416,97 @@ function admitPinnedRun(context: VnextRuntimeContext, runId: string): string {
412
416
  );
413
417
  }
414
418
 
419
+ export interface OpenVnextDriveSessionOptions {
420
+ mode: "simulated" | "live";
421
+ allowLimits?: boolean;
422
+ liveMode?: boolean;
423
+ createProducer: () => VnextProducer;
424
+ }
425
+
426
+ export interface VnextDriveSessionHandle {
427
+ driveId: string;
428
+ settled: Promise<VnextRunDriveResult>;
429
+ }
430
+
431
+ function newDriveId(runId: string, token: string, runtimeId: string, monotonicNs: string): string {
432
+ return `drv_${createHash("sha256").update(`${runId}\0${token}\0${runtimeId}\0${monotonicNs}`, "utf8").digest("hex").slice(0, 24)}`;
433
+ }
434
+
435
+ function inflightAttemptId(state: VnextRunState): string | undefined {
436
+ const leftover = unreconciledPanelAttemptId(state);
437
+ if (leftover) return leftover;
438
+ if (state.currentStep?.attemptId) {
439
+ const located = vnextFoldPanelAttempt(state.currentStep, state.currentStep.attemptId);
440
+ const status = located?.attempt.status;
441
+ if (status === "starting" || status === "executing" || status === "settling") return state.currentStep.attemptId;
442
+ }
443
+ return undefined;
444
+ }
445
+
446
+ function recordExecutingUnrecordedFailure(context: VnextRuntimeContext, runId: string, attemptId: string, stepId: string): void {
447
+ context.eventStore.transaction(() => {
448
+ const run = requireRun(context, runId);
449
+ const now = new Date().toISOString();
450
+ const sequence = context.eventStore.nextSequence(runId);
451
+ const event: VnextRunEvent = {
452
+ ...vnextEventBase(context, run, now, vnextMonotonicNs()),
453
+ eventId: newVnextEventId(),
454
+ eventType: "run.status_changed",
455
+ sequence,
456
+ payload: { status: "failed", reason: "executing_unrecorded", stepId },
457
+ };
458
+ context.eventStore.appendEvent(event);
459
+ context.eventStore.settleCapability(attemptId, "revoked");
460
+ const next = foldStoredVnextRun(context, run);
461
+ persistVnextRunState(context, runId, next, sequence);
462
+ context.eventStore.updateRunStatus(runId, "failed", now);
463
+ });
464
+ }
465
+
466
+ function recordBareDriveFailure(context: VnextRuntimeContext, runId: string): void {
467
+ if (isVnextRuntimeContextClosed(context)) return;
468
+ try {
469
+ const run = context.eventStore.run(runId);
470
+ if (!run) return;
471
+ const state = foldStoredVnextRun(context, run);
472
+ if (isTerminalRunStatus(state.status) || state.status === "cancelling") return;
473
+ const inflight = inflightAttemptId(state);
474
+ if (inflight) {
475
+ const currentStep = state.currentStep;
476
+ const located = vnextFoldPanelAttempt(currentStep, inflight);
477
+ if (currentStep && currentStep.panel.order.length === 1 && located?.attempt.status === "starting") {
478
+ const plan = rehydrateVnextCompiledPlanFromStore(context.eventStore, run);
479
+ const step = plan.steps[currentStep.stepId];
480
+ if (step?.kind === "gate") return;
481
+ if (step && (step.kind === "agent" || step.kind === "moa") && step.assignments.maximum > 1) return;
482
+ recordExecutingUnrecordedFailure(context, runId, inflight, currentStep.stepId);
483
+ }
484
+ return;
485
+ }
486
+ cancelVnextRun(context, runId, { reason: "drive_error" });
487
+ } catch {
488
+ // The original drive error governs; this is a best-effort bare-running brake.
489
+ }
490
+ }
491
+
492
+ async function closeDriveProducer(producer: VnextProducer): Promise<void> {
493
+ if (typeof producer.close !== "function") return;
494
+ try {
495
+ await producer.close();
496
+ } catch {
497
+ // Session ownership ends even if producer cleanup fails.
498
+ }
499
+ }
500
+
415
501
  export class VnextRunScheduler {
416
502
  private readonly context: VnextRuntimeContext;
503
+ private readonly bundle: VnextProjectBundle;
417
504
  private readonly configRevision: string;
418
505
 
419
- private constructor(context: VnextRuntimeContext, configRevision: string) {
506
+ private constructor(context: VnextRuntimeContext, bundle: VnextProjectBundle) {
420
507
  this.context = context;
421
- this.configRevision = configRevision;
508
+ this.bundle = bundle;
509
+ this.configRevision = bundle.configRevision;
422
510
  }
423
511
 
424
512
  static for(context: VnextRuntimeContext, bundle: VnextProjectBundle): VnextRunScheduler {
@@ -427,7 +515,7 @@ export class VnextRunScheduler {
427
515
  }
428
516
  const limits = vnextProjectAdmissionLimits(bundle);
429
517
  bindVnextSchedulerPolicy(context.eventStore.path, limits.maxConcurrentRuns, bundle.configRevision);
430
- return new VnextRunScheduler(context, bundle.configRevision);
518
+ return new VnextRunScheduler(context, bundle);
431
519
  }
432
520
 
433
521
  enqueue(
@@ -454,6 +542,94 @@ export class VnextRunScheduler {
454
542
  return Promise.reject(error);
455
543
  }
456
544
  }
545
+
546
+ openDriveSession(runId: string, options: OpenVnextDriveSessionOptions): Promise<VnextDriveSessionHandle> {
547
+ const policy = vnextSchedulerPolicy(this.context.eventStore.path);
548
+ if (!policy || policy.configRevision !== this.configRevision) {
549
+ return Promise.reject(runtimeError("scheduler_policy_conflict", runId, "scheduler handle does not match the active policy"));
550
+ }
551
+ try {
552
+ const run = requireRun(this.context, runId);
553
+ if (isTerminalRunStatus(run.status) || run.status === "cancelling") {
554
+ return Promise.reject(runtimeError("run_busy", runId, `run ${runId} is already ${run.status}`));
555
+ }
556
+ } catch (error) {
557
+ return Promise.reject(error);
558
+ }
559
+
560
+ const limits = vnextProjectAdmissionLimits(this.bundle);
561
+ const driveOptions: { allowLimits?: boolean; liveMode?: boolean } = {
562
+ liveMode: options.liveMode ?? options.mode === "live",
563
+ };
564
+ if (options.allowLimits !== undefined) driveOptions.allowLimits = options.allowLimits;
565
+ let opened = false;
566
+ const pending = {
567
+ resolve: (_value: VnextRunDriveResult) => undefined as void,
568
+ reject: (_error: unknown) => undefined as void,
569
+ };
570
+ const settled = new Promise<VnextRunDriveResult>((resolve, reject) => {
571
+ pending.resolve = resolve;
572
+ pending.reject = reject;
573
+ });
574
+ void settled.then(undefined, () => undefined);
575
+
576
+ return new Promise<VnextDriveSessionHandle>((resolveOpen, rejectOpen) => {
577
+ const queued = enqueueVnextScheduledRun(
578
+ this.context.eventStore.path,
579
+ runId,
580
+ this.configRevision,
581
+ limits.maxConcurrentRuns,
582
+ async (token) => {
583
+ let producer: VnextProducer | undefined;
584
+ try {
585
+ producer = options.createProducer();
586
+ requireTrustedProducer(producer);
587
+ const current = requireRun(this.context, runId);
588
+ if (current.status === "created") {
589
+ pinVnextCompiledPlan(this.context, this.bundle, runId);
590
+ }
591
+ const started = startVnextRun(this.context, runId, driveOptions);
592
+ if (started.handoff) {
593
+ const error = runtimeError("run_handoff_required", runId, started.handoff.detail);
594
+ Object.assign(error, { handoff: started.handoff });
595
+ throw error;
596
+ }
597
+ const driveId = newDriveId(runId, token, this.context.homeRuntimeId, vnextMonotonicNs());
598
+ const session: VnextDriveSession = {
599
+ driveId,
600
+ runId,
601
+ token,
602
+ homeRuntimeId: this.context.homeRuntimeId,
603
+ mode: options.mode,
604
+ openedAt: new Date().toISOString(),
605
+ controller: new AbortController(),
606
+ settled,
607
+ };
608
+ attachVnextDriveSession(this.context.eventStore.path, runId, token, session);
609
+ opened = true;
610
+ resolveOpen({ driveId, settled });
611
+ try {
612
+ const result = await driveAdmitted(this.context, runId, producer, token, driveOptions);
613
+ pending.resolve(result);
614
+ return result;
615
+ } catch (error) {
616
+ recordBareDriveFailure(this.context, runId);
617
+ pending.reject(error);
618
+ throw error;
619
+ }
620
+ } finally {
621
+ if (producer) await closeDriveProducer(producer);
622
+ clearVnextDriveSession(this.context.eventStore.path, runId, token);
623
+ }
624
+ },
625
+ ) as Promise<VnextRunDriveResult>;
626
+ void queued.then(undefined, (error) => {
627
+ if (!opened) {
628
+ rejectOpen(error);
629
+ }
630
+ });
631
+ });
632
+ }
457
633
  }
458
634
 
459
635
  export function verifyVnextAttemptCapability(
@@ -893,6 +1069,7 @@ export interface VnextPanelDispatchSeams {
893
1069
  failAppendExecuting?: ((member: VnextPanelMemberHook) => boolean) | undefined;
894
1070
  beforeInvoke?: ((member: VnextPanelMemberHook) => void) | undefined;
895
1071
  failSettleMember?: ((member: VnextPanelMemberHook) => boolean) | undefined;
1072
+ skipDispatch?: (() => boolean) | undefined;
896
1073
  }
897
1074
 
898
1075
  export const vnextPanelDispatchSeams: VnextPanelDispatchSeams = {};
@@ -993,6 +1170,9 @@ function prepareDispatch(
993
1170
  const run = requireRun(context, runId);
994
1171
  const plan = rehydrateVnextCompiledPlanFromStore(context.eventStore, run);
995
1172
  const state = foldStoredVnextRun(context, run);
1173
+ if (vnextPanelDispatchSeams.skipDispatch?.()) {
1174
+ return { kind: "return", state };
1175
+ }
996
1176
  if (state.status === "cancelling" && vnextAttemptControllers(context.eventStore.path, runId).length === 0) {
997
1177
  return {
998
1178
  kind: "return",
@@ -19,10 +19,22 @@ interface GateHold {
19
19
  stop?: () => void;
20
20
  }
21
21
 
22
+ export interface VnextDriveSession {
23
+ driveId: string;
24
+ runId: string;
25
+ token: string;
26
+ homeRuntimeId: string;
27
+ mode: "simulated" | "live";
28
+ openedAt: string;
29
+ controller: AbortController;
30
+ settled: Promise<unknown>;
31
+ }
32
+
22
33
  interface AdmissionRecord {
23
34
  token: string;
24
35
  configRevision: string;
25
36
  gateHold?: GateHold;
37
+ driveSession?: VnextDriveSession;
26
38
  }
27
39
 
28
40
  interface QueueItem {
@@ -270,6 +282,44 @@ export function vnextAdmittedToken(storePath: string, runId: string): string | u
270
282
  return owners.get(storePath)?.admitted.get(runId)?.token;
271
283
  }
272
284
 
285
+ export function attachVnextDriveSession(
286
+ storePath: string,
287
+ runId: string,
288
+ token: string,
289
+ session: VnextDriveSession,
290
+ ): void {
291
+ const owner = record(storePath);
292
+ const current = owner.admitted.get(runId);
293
+ if (!current || current.token !== token) {
294
+ throw runtimeError("run_events_illegal", runId, "drive session requires the exact admitted token");
295
+ }
296
+ if (current.driveSession) {
297
+ throw runtimeError("run_busy", runId, `run ${runId} already has a drive session`);
298
+ }
299
+ current.driveSession = session;
300
+ }
301
+
302
+ export function clearVnextDriveSession(storePath: string, runId: string, token: string): void {
303
+ const owner = owners.get(storePath);
304
+ const current = owner?.admitted.get(runId);
305
+ if (!current || current.token !== token) return;
306
+ delete current.driveSession;
307
+ }
308
+
309
+ export function vnextDriveSession(storePath: string, runId: string): VnextDriveSession | undefined {
310
+ return owners.get(storePath)?.admitted.get(runId)?.driveSession;
311
+ }
312
+
313
+ export function vnextOpenDriveSessions(storePath: string): VnextDriveSession[] {
314
+ const owner = owners.get(storePath);
315
+ if (!owner) return [];
316
+ const sessions: VnextDriveSession[] = [];
317
+ for (const current of owner.admitted.values()) {
318
+ if (current.driveSession) sessions.push(current.driveSession);
319
+ }
320
+ return sessions;
321
+ }
322
+
273
323
  export function enqueueVnextScheduledRun(
274
324
  storePath: string,
275
325
  runId: string,