@kontextmind/kxm 0.7.7 → 0.7.9
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 +250 -38
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +318 -33
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +85 -28
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/system.ts +3 -3
- package/plugins/kxm/src/cli/tasks.ts +2 -2
- package/plugins/kxm/src/cli/vnext.ts +2 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +8 -8
- package/plugins/kxm/src/runtime.ts +1 -0
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/vnext-harness.ts +88 -20
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +39 -7
- package/plugins/kxm/src/vnext-oneshot-process.ts +25 -4
- package/plugins/kxm/src/vnext-oneshot-producer.ts +4 -1
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +5 -3
|
@@ -58,14 +58,22 @@ export interface HarnessCatalogEntry {
|
|
|
58
58
|
oneShot?: HarnessOneShotConfig | undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export type HarnessRunCommand = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
|
|
62
|
+
export type HarnessRunCommandAsync = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>;
|
|
63
|
+
|
|
61
64
|
export interface HarnessProbeOptions {
|
|
62
65
|
env?: NodeJS.ProcessEnv | undefined;
|
|
63
|
-
runCommand?:
|
|
66
|
+
runCommand?: HarnessRunCommand | undefined;
|
|
64
67
|
timeoutMs?: number | undefined;
|
|
65
68
|
platform?: NodeJS.Platform | undefined;
|
|
66
69
|
existsSync?: ((path: string) => boolean) | undefined;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
export interface AsyncHarnessProbeOptions extends Omit<HarnessProbeOptions, "runCommand"> {
|
|
73
|
+
signal?: AbortSignal | undefined;
|
|
74
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export interface HarnessDispatchStatus {
|
|
70
78
|
status: "yes" | "no";
|
|
71
79
|
supported: boolean;
|
|
@@ -598,7 +606,7 @@ export function findWinNpmInnerExe(
|
|
|
598
606
|
return undefined;
|
|
599
607
|
}
|
|
600
608
|
|
|
601
|
-
function defaultRunner(env: NodeJS.ProcessEnv):
|
|
609
|
+
function defaultRunner(env: NodeJS.ProcessEnv): HarnessRunCommand {
|
|
602
610
|
return (command, args, timeoutMs) => {
|
|
603
611
|
try {
|
|
604
612
|
const result = spawnSync(command, [...args], {
|
|
@@ -624,6 +632,37 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
|
|
|
624
632
|
};
|
|
625
633
|
}
|
|
626
634
|
|
|
635
|
+
function commandResultFromSpawn(result: Awaited<ReturnType<typeof defaultSpawn>>): HarnessCommandResult {
|
|
636
|
+
const errno = result.error && "code" in result.error
|
|
637
|
+
? (result.error as NodeJS.ErrnoException).code
|
|
638
|
+
: undefined;
|
|
639
|
+
const error = (typeof errno === "string" && errno ? errno : undefined)
|
|
640
|
+
?? result.error?.message
|
|
641
|
+
?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
642
|
+
return {
|
|
643
|
+
ok: result.code === 0 && !error,
|
|
644
|
+
code: result.code,
|
|
645
|
+
stdout: result.stdout,
|
|
646
|
+
stderr: result.stderr,
|
|
647
|
+
...(error ? { error } : {}),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function defaultAsyncRunner(env: NodeJS.ProcessEnv, signal?: AbortSignal): HarnessRunCommandAsync {
|
|
652
|
+
return async (command, args, timeoutMs) => {
|
|
653
|
+
try {
|
|
654
|
+
return commandResultFromSpawn(await defaultSpawn(command, args, {
|
|
655
|
+
env,
|
|
656
|
+
timeoutMs,
|
|
657
|
+
signal,
|
|
658
|
+
shell: harnessSpawnUsesShell(command),
|
|
659
|
+
}));
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
627
666
|
function firstLine(text: string): string | undefined {
|
|
628
667
|
const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
|
|
629
668
|
return line && line.length <= 200 ? line : undefined;
|
|
@@ -858,6 +897,18 @@ export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInvent
|
|
|
858
897
|
};
|
|
859
898
|
}
|
|
860
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Inventory probe that never uses spawnSync. Product CLI/hub/extension paths
|
|
902
|
+
* must await this so capability/auth checks cannot block the event loop.
|
|
903
|
+
*/
|
|
904
|
+
export async function probeHarnessesAsync(options: AsyncHarnessProbeOptions = {}): Promise<HarnessInventory> {
|
|
905
|
+
return replayHarnessProbe(
|
|
906
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
907
|
+
(runCommand) => probeHarnesses({ ...options, runCommand }),
|
|
908
|
+
256,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
861
912
|
export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
|
|
862
913
|
const eligible = inventory.harnesses
|
|
863
914
|
.filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
|
|
@@ -1118,7 +1169,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
1118
1169
|
|
|
1119
1170
|
export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
|
|
1120
1171
|
signal?: AbortSignal | undefined;
|
|
1121
|
-
runCommand?:
|
|
1172
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
1122
1173
|
}
|
|
1123
1174
|
|
|
1124
1175
|
class PendingHarnessCommand {
|
|
@@ -1134,37 +1185,43 @@ class PendingHarnessCommand {
|
|
|
1134
1185
|
}
|
|
1135
1186
|
}
|
|
1136
1187
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
*/
|
|
1143
|
-
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1188
|
+
async function replayHarnessProbe<T>(
|
|
1189
|
+
run: HarnessRunCommandAsync,
|
|
1190
|
+
execute: (runCommand: HarnessRunCommand) => T,
|
|
1191
|
+
limit: number,
|
|
1192
|
+
): Promise<T> {
|
|
1144
1193
|
const observed = new Map<string, HarnessCommandResult>();
|
|
1145
|
-
|
|
1146
|
-
const result = await defaultSpawn(command, args, { env: options.env, timeoutMs, signal: options.signal });
|
|
1147
|
-
const error = result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
1148
|
-
return { ok: result.code === 0 && !error, code: result.code, stdout: result.stdout, stderr: result.stderr,
|
|
1149
|
-
...(error ? { error } : {}) };
|
|
1150
|
-
});
|
|
1151
|
-
for (let commands = 0; commands <= 32; commands++) {
|
|
1194
|
+
for (let commands = 0; commands <= limit; commands++) {
|
|
1152
1195
|
try {
|
|
1153
|
-
return
|
|
1196
|
+
return execute((command, args, timeoutMs) => {
|
|
1154
1197
|
const key = JSON.stringify([command, args, timeoutMs]);
|
|
1155
1198
|
const result = observed.get(key);
|
|
1156
1199
|
if (result) return result;
|
|
1157
1200
|
throw new PendingHarnessCommand(command, args, timeoutMs, key);
|
|
1158
|
-
}
|
|
1201
|
+
});
|
|
1159
1202
|
} catch (pending) {
|
|
1160
1203
|
if (!(pending instanceof PendingHarnessCommand)) throw pending;
|
|
1161
|
-
if (commands ===
|
|
1204
|
+
if (commands === limit) throw new Error("auth_probe_command_limit");
|
|
1162
1205
|
observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
|
|
1163
1206
|
}
|
|
1164
1207
|
}
|
|
1165
1208
|
throw new Error("auth_probe_command_limit");
|
|
1166
1209
|
}
|
|
1167
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Execute the existing probe policy without blocking the Runtime event loop.
|
|
1213
|
+
* Replay pure policy decisions from per-call command observations, yielding for
|
|
1214
|
+
* every missing observation. This reuses the exact auth/model/brake parsers,
|
|
1215
|
+
* deduplicates repeated detection, and never caches auth across assignments.
|
|
1216
|
+
*/
|
|
1217
|
+
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1218
|
+
return replayHarnessProbe(
|
|
1219
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1220
|
+
(runCommand) => probeHarnessAssignment({ ...options, runCommand }),
|
|
1221
|
+
32,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1168
1225
|
export function probeHarnessesForModel(
|
|
1169
1226
|
modelSpec: HarnessModelSpec,
|
|
1170
1227
|
options: HarnessProbeOptions = {},
|
|
@@ -1189,6 +1246,17 @@ export function probeHarnessesForModel(
|
|
|
1189
1246
|
};
|
|
1190
1247
|
}
|
|
1191
1248
|
|
|
1249
|
+
export async function probeHarnessesForModelAsync(
|
|
1250
|
+
modelSpec: HarnessModelSpec,
|
|
1251
|
+
options: AsyncHarnessProbeOptions = {},
|
|
1252
|
+
): Promise<HarnessInventory> {
|
|
1253
|
+
return replayHarnessProbe(
|
|
1254
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1255
|
+
(runCommand) => probeHarnessesForModel(modelSpec, { ...options, runCommand }),
|
|
1256
|
+
256,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1192
1260
|
function scopesFor(scope: HarnessUpdateScope, entry: HarnessCatalogEntry): Exclude<HarnessUpdateScope, "all">[] {
|
|
1193
1261
|
if (scope === "self") return ["self"];
|
|
1194
1262
|
if (scope === "extensions") return entry.update.extensions ? ["extensions"] : [];
|
|
@@ -3,10 +3,37 @@ import { lstat, mkdir, open, rename } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { VnextOneShotProcessResult } from "./vnext-oneshot-process.ts";
|
|
5
5
|
|
|
6
|
+
export const ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2" as const;
|
|
7
|
+
const RETIRED_ONESHOT_EVIDENCE_SCHEMAS = new Set(["kxm.oneshot-evidence.v1"]);
|
|
8
|
+
|
|
6
9
|
const TEXT_LIMIT = 4 * 1024 * 1024;
|
|
10
|
+
const ARGV_LIMIT = 64 * 1024;
|
|
7
11
|
const RECORD_LIMIT = 16 * 1024 * 1024;
|
|
8
12
|
const digest = (value: string): string => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
9
13
|
|
|
14
|
+
export interface OneShotEvidenceRecord {
|
|
15
|
+
schema: typeof ONESHOT_EVIDENCE_SCHEMA;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Brake: retired and unknown persisted schema ids are never aliased or upgraded. */
|
|
20
|
+
export function parseOneShotEvidenceRecord(bytes: string): OneShotEvidenceRecord {
|
|
21
|
+
let parsed: unknown;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(bytes) as unknown;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("oneshot_evidence_record_invalid");
|
|
26
|
+
}
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28
|
+
throw new Error("oneshot_evidence_record_invalid");
|
|
29
|
+
}
|
|
30
|
+
const schema = (parsed as { schema?: unknown }).schema;
|
|
31
|
+
if (typeof schema !== "string") throw new Error("oneshot_evidence_record_invalid");
|
|
32
|
+
if (RETIRED_ONESHOT_EVIDENCE_SCHEMAS.has(schema)) throw new Error("oneshot_evidence_schema_retired");
|
|
33
|
+
if (schema !== ONESHOT_EVIDENCE_SCHEMA) throw new Error("oneshot_evidence_schema_unknown");
|
|
34
|
+
return parsed as OneShotEvidenceRecord;
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
/** Private diagnostic evidence, never a peer reply or acceptance authority.
|
|
11
38
|
* Retained until operator archival; no automatic deletion of failed attempts.
|
|
12
39
|
* Each record is bounded; inability to reserve/write evidence fails closed.
|
|
@@ -32,11 +59,13 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
32
59
|
const redact = (value: string): string => {
|
|
33
60
|
let text = value;
|
|
34
61
|
for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
|
|
35
|
-
return text
|
|
62
|
+
return text
|
|
63
|
+
.replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]")
|
|
64
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
|
|
36
65
|
};
|
|
37
|
-
const bounded = (value: string): { text: string; truncated: boolean; sha256: string } => {
|
|
66
|
+
const bounded = (value: string, max = TEXT_LIMIT): { text: string; truncated: boolean; sha256: string } => {
|
|
38
67
|
const bytes = Buffer.from(redact(value));
|
|
39
|
-
return { text: bytes.subarray(0,
|
|
68
|
+
return { text: bytes.subarray(0, max).toString("utf8"), truncated: bytes.length > max, sha256: digest(redact(value)) };
|
|
40
69
|
};
|
|
41
70
|
const write = async (name: string, value: unknown): Promise<string> => {
|
|
42
71
|
const current = await lstat(dir);
|
|
@@ -46,6 +75,7 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
46
75
|
}
|
|
47
76
|
const bytes = JSON.stringify(value);
|
|
48
77
|
if (Buffer.byteLength(bytes) > RECORD_LIMIT) throw new Error("oneshot_evidence_record_limit");
|
|
78
|
+
parseOneShotEvidenceRecord(bytes);
|
|
49
79
|
const target = join(dir, name);
|
|
50
80
|
const partial = `${target}.partial`;
|
|
51
81
|
const file = await open(partial, "wx", 0o600);
|
|
@@ -59,11 +89,12 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
59
89
|
return digest(bytes);
|
|
60
90
|
};
|
|
61
91
|
await write("intent.json", {
|
|
62
|
-
schema:
|
|
92
|
+
schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
|
|
63
93
|
runId: intent.runId, stepId: intent.stepId, attemptId: intent.attemptId, assignmentId: intent.assignmentId,
|
|
64
94
|
harness: intent.harness, provider: intent.provider, requestedModel: intent.model,
|
|
65
|
-
cwd: intent.cwd, command: intent.command,
|
|
66
|
-
argv: bounded(JSON.stringify(intent.args)), stdin: intent.input === undefined ? null : bounded(intent.input),
|
|
95
|
+
cwd: intent.cwd, command: redact(intent.command),
|
|
96
|
+
argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT), stdin: intent.input === undefined ? null : bounded(intent.input),
|
|
97
|
+
env: null,
|
|
67
98
|
acceptance: false,
|
|
68
99
|
});
|
|
69
100
|
let finished = false;
|
|
@@ -73,10 +104,11 @@ export async function beginOneShotEvidence(root: string, intent: {
|
|
|
73
104
|
if (finished) throw new Error("oneshot_evidence_already_finished");
|
|
74
105
|
finished = true;
|
|
75
106
|
return write("result.json", {
|
|
76
|
-
schema:
|
|
107
|
+
schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
|
|
77
108
|
stdout: bounded(result.stdout), stderr: bounded(result.stderr),
|
|
78
109
|
code: result.code, signal: result.signal ?? null, started: result.started ?? null,
|
|
79
110
|
observedChildExit: result.observedChildExit ?? null, terminationRequested: result.terminationRequested ?? null,
|
|
111
|
+
unverifiedDescendants: result.unverifiedDescendants ?? null,
|
|
80
112
|
error: result.error ? bounded(result.error.message) : null,
|
|
81
113
|
observation: bounded(JSON.stringify(observation)), acceptance: false,
|
|
82
114
|
});
|
|
@@ -9,6 +9,11 @@ export interface VnextOneShotProcessResult {
|
|
|
9
9
|
started?: boolean | undefined;
|
|
10
10
|
/** Stop was requested; direct-child reaping cannot attest escaped descendants. */
|
|
11
11
|
terminationRequested?: boolean | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Direct-child close/reap is not descendant death. Unobserved process-group
|
|
14
|
+
* members stay unverified: never success, never a forged descendant failure.
|
|
15
|
+
*/
|
|
16
|
+
unverifiedDescendants?: boolean | undefined;
|
|
12
17
|
error?: Error | undefined;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -18,6 +23,8 @@ export interface VnextOneShotSpawnOptions {
|
|
|
18
23
|
input?: string | undefined;
|
|
19
24
|
timeoutMs?: number | undefined;
|
|
20
25
|
signal?: AbortSignal | undefined;
|
|
26
|
+
/** Only allowlisted Windows `.cmd` probes may set this; never a user string. */
|
|
27
|
+
shell?: boolean | undefined;
|
|
21
28
|
}
|
|
22
29
|
export type VnextOneShotSpawn = (command: string, args: readonly string[], options: VnextOneShotSpawnOptions) => Promise<VnextOneShotProcessResult>;
|
|
23
30
|
|
|
@@ -71,6 +78,9 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
71
78
|
let child: ReturnType<typeof spawn>;
|
|
72
79
|
const finish = (): void => {
|
|
73
80
|
if (finished) return;
|
|
81
|
+
// Direct-child close must not cancel SIGTERM → SIGKILL. Refuse to settle
|
|
82
|
+
// until the group kill has been sent, then reap on the existing timers.
|
|
83
|
+
if (stopping && !killSent) return;
|
|
74
84
|
finished = true;
|
|
75
85
|
for (const timer of [wallTimer, killTimer, drainTimer, reapTimer]) clearTimeout(timer);
|
|
76
86
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -80,7 +90,18 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
80
90
|
child?.unref();
|
|
81
91
|
const started = Boolean(child?.pid);
|
|
82
92
|
if (started && !observedChildExit) error ??= new Error("process_exit_unobserved");
|
|
83
|
-
|
|
93
|
+
const unverifiedDescendants = stopping || (started && !observedChildExit);
|
|
94
|
+
resolve({
|
|
95
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
96
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
97
|
+
code,
|
|
98
|
+
signal,
|
|
99
|
+
started,
|
|
100
|
+
observedChildExit,
|
|
101
|
+
terminationRequested: stopping,
|
|
102
|
+
...(unverifiedDescendants ? { unverifiedDescendants: true } : {}),
|
|
103
|
+
...(error ? { error } : {}),
|
|
104
|
+
});
|
|
84
105
|
};
|
|
85
106
|
const kill = (requested: NodeJS.Signals): void => {
|
|
86
107
|
killProcessTree(child, requested);
|
|
@@ -108,7 +129,7 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
108
129
|
cwd: options.cwd,
|
|
109
130
|
env: options.env ?? process.env,
|
|
110
131
|
stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
-
shell:
|
|
132
|
+
shell: options.shell === true,
|
|
112
133
|
windowsHide: true,
|
|
113
134
|
detached: process.platform !== "win32",
|
|
114
135
|
});
|
|
@@ -153,8 +174,8 @@ export function defaultSpawn(command: string, args: readonly string[], options:
|
|
|
153
174
|
code = exitCode;
|
|
154
175
|
signal = exitSignal;
|
|
155
176
|
// The leader can exit and close its pipes while a descendant ignores TERM.
|
|
156
|
-
//
|
|
157
|
-
|
|
177
|
+
// finish() no-ops until SIGKILL when escalation is in flight.
|
|
178
|
+
finish();
|
|
158
179
|
});
|
|
159
180
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
160
181
|
if (options.signal?.aborted) onAbort();
|
|
@@ -252,9 +252,11 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
252
252
|
const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
|
|
253
253
|
// Process groups are cleanup, not containment. A detached descendant or
|
|
254
254
|
// provider leader may outlive the client; interrupted effects need recovery.
|
|
255
|
-
const
|
|
255
|
+
const unverifiedDescendants = procResult.unverifiedDescendants === true
|
|
256
|
+
|| procResult.terminationRequested === true
|
|
256
257
|
|| procResult.error?.message === "process_exit_unobserved"
|
|
257
258
|
|| (procResult.observedChildExit === false && procResult.started !== false);
|
|
259
|
+
const effectUncertain = unverifiedDescendants || Boolean(procResult.signal);
|
|
258
260
|
const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
|
|
259
261
|
const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
|
|
260
262
|
: determineOutcome(parsed.text, request.allowedOutcomes);
|
|
@@ -267,6 +269,7 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
|
|
|
267
269
|
if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
|
|
268
270
|
if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
|
|
269
271
|
if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
|
|
272
|
+
if (unverifiedDescendants) providerMetadata.descendantEffects = "unverified";
|
|
270
273
|
if (procResult.error) {
|
|
271
274
|
const reason = procResult.error.message;
|
|
272
275
|
providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
type VnextProducerResult,
|
|
9
9
|
} from "./vnext-engine.ts";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
probeHarnessAssignmentAsync,
|
|
12
|
+
type HarnessAssignmentProbeOptions,
|
|
12
13
|
type HarnessInventory,
|
|
14
|
+
type HarnessStatus,
|
|
13
15
|
} from "./vnext-harness.ts";
|
|
14
16
|
import {
|
|
15
17
|
calculateModelCost,
|
|
@@ -386,7 +388,7 @@ export interface VnextPiProducerOptions {
|
|
|
386
388
|
priceCatalog?: PriceCatalog | undefined;
|
|
387
389
|
env?: NodeJS.ProcessEnv | undefined;
|
|
388
390
|
spawnProcess?: ((command: string, args: readonly string[], options: Record<string, unknown>) => PiRpcProcess) | undefined;
|
|
389
|
-
probeHarness?:
|
|
391
|
+
probeHarness?: ((options: Omit<HarnessAssignmentProbeOptions, "runCommand"> & { signal?: AbortSignal | undefined }) => HarnessStatus | Promise<HarnessStatus>) | undefined;
|
|
390
392
|
resolveModel?: ((agentId: string, runId: string) => { provider?: string | undefined; model?: string | undefined; thinking?: string | undefined } | undefined) | undefined;
|
|
391
393
|
defaultModel?: string | undefined;
|
|
392
394
|
defaultProvider?: string | undefined;
|
|
@@ -438,7 +440,7 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
438
440
|
return { provider: parsed.provider, model: parsed.model };
|
|
439
441
|
}
|
|
440
442
|
|
|
441
|
-
function checkPiAuth(provider: string, model: string): void {
|
|
443
|
+
async function checkPiAuth(provider: string, model: string, signal: AbortSignal): Promise<void> {
|
|
442
444
|
if (options.inventory) {
|
|
443
445
|
const piEntry = options.inventory.harnesses.find((h) => h.id === "pi");
|
|
444
446
|
if (!piEntry || !piEntry.detected || piEntry.authenticated === false) {
|
|
@@ -446,12 +448,14 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
450
|
|
|
449
|
-
const probeFn = options.probeHarness ??
|
|
450
|
-
const probe = probeFn({
|
|
451
|
+
const probeFn = options.probeHarness ?? probeHarnessAssignmentAsync;
|
|
452
|
+
const probe = await probeFn({
|
|
451
453
|
harness: "pi",
|
|
452
454
|
provider,
|
|
453
455
|
model,
|
|
454
456
|
env: options.env,
|
|
457
|
+
signal,
|
|
458
|
+
timeoutMs: 10_000,
|
|
455
459
|
});
|
|
456
460
|
|
|
457
461
|
if (!probe.detected) {
|
|
@@ -538,8 +542,8 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
|
|
|
538
542
|
const startTime = Date.now();
|
|
539
543
|
const resolved = resolveModelForRequest(request);
|
|
540
544
|
|
|
541
|
-
// Preflight auth check - fails closed
|
|
542
|
-
checkPiAuth(resolved.provider, resolved.model);
|
|
545
|
+
// Preflight auth check - fails closed via the async bounded probe
|
|
546
|
+
await checkPiAuth(resolved.provider, resolved.model, request.signal);
|
|
543
547
|
|
|
544
548
|
// Get or create session
|
|
545
549
|
const session = await getOrCreateSession(request, resolved.model);
|
|
@@ -476,7 +476,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
476
476
|
});
|
|
477
477
|
})();
|
|
478
478
|
|
|
479
|
-
let drivePromise: Promise<unknown
|
|
479
|
+
let drivePromise: Promise<unknown> | undefined;
|
|
480
480
|
let earlyError: unknown;
|
|
481
481
|
try {
|
|
482
482
|
const scheduler = VnextRunScheduler.for(context, bundle);
|
|
@@ -484,7 +484,6 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
484
484
|
drivePromise.catch((err) => { earlyError = err; });
|
|
485
485
|
} catch (err) {
|
|
486
486
|
earlyError = err;
|
|
487
|
-
drivePromise = Promise.reject(err);
|
|
488
487
|
}
|
|
489
488
|
|
|
490
489
|
// Yield microtask to catch synchronous duplicate queue check (e.g. run_busy)
|
|
@@ -503,11 +502,14 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
503
502
|
throw earlyError;
|
|
504
503
|
}
|
|
505
504
|
|
|
506
|
-
void drivePromise
|
|
505
|
+
void drivePromise!.finally(async () => {
|
|
507
506
|
cleanupActive();
|
|
508
507
|
if ("close" in producer && typeof producer.close === "function") {
|
|
509
508
|
try { await producer.close(); } catch { /* ignore */ }
|
|
510
509
|
}
|
|
510
|
+
}).catch(() => {
|
|
511
|
+
// Derived finally() re-rejects when the admitted drive or cleanup
|
|
512
|
+
// throws; void does not consume that. Keep the request 202.
|
|
511
513
|
});
|
|
512
514
|
|
|
513
515
|
sendJson(response, 202, {
|