@byok-sdk/client 0.4.0 → 0.4.1
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/README.md +1 -1
- package/dist/adapters/claude/process-client.d.ts +7 -1
- package/dist/adapters/codex/process-runner.d.ts +5 -0
- package/dist/adapters/index.js +175 -44
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/resolve-bin.d.ts +1 -1
- package/dist/adapters/pi/rpc-client.d.ts +7 -1
- package/dist/adapters/process-tree.d.ts +45 -4
- package/dist/adapters/taskkill-pid-set.d.ts +34 -0
- package/dist/bin/byok-agent.js +200 -64
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/daemon/connection-manager.d.ts +11 -15
- package/dist/daemon/long-poll-transport.d.ts +6 -0
- package/dist/daemon/task-runner.d.ts +7 -6
- package/dist/index.js +200 -64
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
|
@@ -25,7 +25,7 @@ export interface ResolvedBin {
|
|
|
25
25
|
* only ever constructs `new PiAdapter()` with no options (see `createDaemon`),
|
|
26
26
|
* so an out-of-process substitution (e.g. examples/basic's e2e run swapping
|
|
27
27
|
* in the fake-pi fixture, or a single-file product injecting its required
|
|
28
|
-
* Node 22.
|
|
28
|
+
* Node 22.22+ pi sidecar) has no other seam to use.
|
|
29
29
|
*
|
|
30
30
|
* Deliberately does NOT use `createRequire(...).resolve()`: this package is
|
|
31
31
|
* pure ESM with no `require` export condition (`exports["."]` only offers
|
|
@@ -66,7 +66,13 @@ export declare class PiRpcClient {
|
|
|
66
66
|
* a post-mortem on a failed/hung task has it without separate log scraping.
|
|
67
67
|
*/
|
|
68
68
|
recordUnmappedFrame(type: string): void;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
71
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
72
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
73
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
74
|
+
* swallowing it here loses nothing.
|
|
75
|
+
*/
|
|
70
76
|
kill(): void;
|
|
71
77
|
waitClosed(): Promise<void>;
|
|
72
78
|
dispose(): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ChildProcess, type SpawnOptions } from 'node:child_process';
|
|
1
|
+
import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
type KillFn = (pid: number, signal: NodeJS.Signals | number) => void;
|
|
2
3
|
export interface OwnedProcessTreeOptions {
|
|
3
4
|
child: ChildProcess;
|
|
4
5
|
waitClosed: () => Promise<void>;
|
|
@@ -6,6 +7,12 @@ export interface OwnedProcessTreeOptions {
|
|
|
6
7
|
label: string;
|
|
7
8
|
termGraceMs?: number;
|
|
8
9
|
killGraceMs?: number;
|
|
10
|
+
/** DI seam — defaults to `process.platform`. Lets the win32 branch be exercised from POSIX CI, mirroring `util/secure-dir.ts`'s identical convention. */
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
/** DI seam for the win32 `taskkill` sweep — defaults to `node:child_process`'s `spawn`. */
|
|
13
|
+
spawnFn?: typeof spawn;
|
|
14
|
+
/** DI seam for liveness probing — defaults to `process.kill`. */
|
|
15
|
+
killFn?: KillFn;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* Every bundled runtime root is an owned process-group leader on POSIX. Pipes
|
|
@@ -13,7 +20,41 @@ export interface OwnedProcessTreeOptions {
|
|
|
13
20
|
* the runtime outlive the daemon. Windows uses taskkill's `/T` tree authority.
|
|
14
21
|
*/
|
|
15
22
|
export declare function withOwnedProcessTree<T extends SpawnOptions>(options: T): T;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Immediate termination request used by interrupt paths; close remains the
|
|
25
|
+
* receipt. On win32 the request also RECORDS the pid set taskkill walked,
|
|
26
|
+
* which is what {@link disposeOwnedProcessTree} later measures — so an
|
|
27
|
+
* interrupt that is fired and forgotten still leaves disposal a measurable
|
|
28
|
+
* tree. A request that could not be spawned records nothing, which makes
|
|
29
|
+
* disposal re-issue it and surface `stage:'signal'` itself.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requestOwnedProcessTreeTermination(options: OwnedProcessTreeOptions): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve only after the adapter-owned root and descendants are quiescent.
|
|
34
|
+
*
|
|
35
|
+
* POSIX measures the owned process GROUP; win32 measures the pid set taskkill
|
|
36
|
+
* reported walking (`stage:'quiescence'` names how many of those were still
|
|
37
|
+
* alive at the deadline). Neither platform reads a terminator's exit status:
|
|
38
|
+
* on win32 `stage:'signal'` now means only that taskkill could not be spawned.
|
|
39
|
+
* `close` stays the final receipt on both — it is the stdio-flush guarantee,
|
|
40
|
+
* not the liveness proof.
|
|
41
|
+
*
|
|
42
|
+
* Grace budget: each phase carries its own full grace on both platforms. On
|
|
43
|
+
* win32 the quiescence poll gets `killGraceMs` and the close wait that follows
|
|
44
|
+
* gets a fresh `killGraceMs`; POSIX likewise gives the SIGTERM wait
|
|
45
|
+
* `termGraceMs`, the SIGKILL wait `killGraceMs`, and the close wait another
|
|
46
|
+
* `killGraceMs`. Worst-case disposal is therefore bounded by the sum, never by
|
|
47
|
+
* one shared deadline that could starve the close wait after a slow drain.
|
|
48
|
+
*
|
|
49
|
+
* Residual boundary, stated honestly. Three cases this mechanism cannot cover:
|
|
50
|
+
* a descendant whose intermediate parent died before any sweep observed it is
|
|
51
|
+
* unreachable, because Windows does not re-parent orphans — nothing in the
|
|
52
|
+
* surviving tree links back to it and `taskkill /T` cannot find it. The same
|
|
53
|
+
* window means a recycled pid could read as alive. And if taskkill's output is
|
|
54
|
+
* empty or unparseable, the walked set collapses to `{root}`: disposal then
|
|
55
|
+
* measures the root alone and reports quiescence on that basis, which is a
|
|
56
|
+
* narrower claim than the tree, not a false one. Preventing orphans left behind
|
|
57
|
+
* by a DAEMON crash is a separate job-object concern, explicitly out of scope.
|
|
58
|
+
*/
|
|
19
59
|
export declare function disposeOwnedProcessTree(options: OwnedProcessTreeOptions): Promise<void>;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows-only: recovers the process-tree PID set that `taskkill /T /F`
|
|
3
|
+
* reports it walked, so `process-tree.ts` can MEASURE quiescence instead of
|
|
4
|
+
* trusting taskkill's exit status.
|
|
5
|
+
*
|
|
6
|
+
* Two constraints shape this parser:
|
|
7
|
+
*
|
|
8
|
+
* - taskkill's messages are LOCALIZED (the same run prints
|
|
9
|
+
* `SUCCESS: The process with PID ...` on en-US and a translated sentence on
|
|
10
|
+
* de-DE/zh-CN/ja-JP), so no word, prefix, or field label may be matched.
|
|
11
|
+
* Only the integers and their CO-OCCURRENCE on one line are stable: every
|
|
12
|
+
* line taskkill emits for a walked process names that process and its
|
|
13
|
+
* parent together. Seeding with the root pid and closing over co-occurrence
|
|
14
|
+
* therefore reaches exactly the walked tree, in any locale.
|
|
15
|
+
* - the text arrives in the console OEM codepage, not UTF-8. It is decoded as
|
|
16
|
+
* latin1 (byte-preserving) rather than guessed: every OEM codepage taskkill
|
|
17
|
+
* can use encodes ASCII digits as single bytes 0x30-0x39, and no DBCS trail
|
|
18
|
+
* byte (CP932/CP936/CP949/CP950 all start their trail range at 0x40) can
|
|
19
|
+
* land in that range. Undecodable non-ASCII bytes become mojibake, which is
|
|
20
|
+
* irrelevant: they can never manufacture a digit.
|
|
21
|
+
*
|
|
22
|
+
* Line ORDER is deliberately not relied on (taskkill emits children before
|
|
23
|
+
* parents today); the walk iterates to a fixpoint instead.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Returns the PID set reachable from `rootPid` by co-occurrence over `text`.
|
|
27
|
+
*
|
|
28
|
+
* `excludedPids` removes integers that must never enter the set even when
|
|
29
|
+
* they share a line with an accepted pid — taskkill names the ROOT's own
|
|
30
|
+
* parent (this daemon process) on the root's line, and accepting it would
|
|
31
|
+
* make quiescence unreachable by construction. `rootPid` is always seeded and
|
|
32
|
+
* is never subject to exclusion.
|
|
33
|
+
*/
|
|
34
|
+
export declare function walkTaskkillPidSet(text: string, rootPid: number, excludedPids?: Iterable<number>): Set<number>;
|
package/dist/bin/byok-agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFile, spawn
|
|
2
|
+
import { execFile, spawn } from 'child_process';
|
|
3
3
|
import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
|
|
4
4
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
5
5
|
import path20, { isAbsolute, join } from 'path';
|
|
@@ -887,12 +887,12 @@ function resolvePiBin() {
|
|
|
887
887
|
}
|
|
888
888
|
} catch (cause) {
|
|
889
889
|
throw new Error(
|
|
890
|
-
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.
|
|
890
|
+
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`,
|
|
891
891
|
{ cause }
|
|
892
892
|
);
|
|
893
893
|
}
|
|
894
894
|
throw new Error(
|
|
895
|
-
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.
|
|
895
|
+
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`
|
|
896
896
|
);
|
|
897
897
|
}
|
|
898
898
|
|
|
@@ -1097,11 +1097,54 @@ var AsyncQueue = class {
|
|
|
1097
1097
|
};
|
|
1098
1098
|
}
|
|
1099
1099
|
};
|
|
1100
|
+
|
|
1101
|
+
// src/adapters/taskkill-pid-set.ts
|
|
1102
|
+
var INTEGER_PATTERN = /\d+/g;
|
|
1103
|
+
function isCandidatePid(value) {
|
|
1104
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1105
|
+
}
|
|
1106
|
+
function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
|
|
1107
|
+
const excluded = new Set(excludedPids);
|
|
1108
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1109
|
+
if (isCandidatePid(rootPid)) accepted.add(rootPid);
|
|
1110
|
+
const lines = text.split(/\r?\n/).map((line) => {
|
|
1111
|
+
const pids = [];
|
|
1112
|
+
for (const match of line.matchAll(INTEGER_PATTERN)) {
|
|
1113
|
+
const pid = Number(match[0]);
|
|
1114
|
+
if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
|
|
1115
|
+
}
|
|
1116
|
+
return pids;
|
|
1117
|
+
});
|
|
1118
|
+
let changed = true;
|
|
1119
|
+
while (changed) {
|
|
1120
|
+
changed = false;
|
|
1121
|
+
for (const pids of lines) {
|
|
1122
|
+
if (!pids.some((pid) => accepted.has(pid))) continue;
|
|
1123
|
+
for (const pid of pids) {
|
|
1124
|
+
if (accepted.has(pid)) continue;
|
|
1125
|
+
accepted.add(pid);
|
|
1126
|
+
changed = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return accepted;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/adapters/process-tree.ts
|
|
1100
1134
|
var DEFAULT_TERM_GRACE_MS = 750;
|
|
1101
1135
|
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
1102
1136
|
var POLL_MS = 20;
|
|
1103
|
-
var
|
|
1104
|
-
|
|
1137
|
+
var terminationState = /* @__PURE__ */ new WeakMap();
|
|
1138
|
+
function stateFor(child) {
|
|
1139
|
+
const existing = terminationState.get(child);
|
|
1140
|
+
if (existing) return existing;
|
|
1141
|
+
const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
|
|
1142
|
+
terminationState.set(child, created);
|
|
1143
|
+
return created;
|
|
1144
|
+
}
|
|
1145
|
+
function defaultKill(pid, signal) {
|
|
1146
|
+
process.kill(pid, signal);
|
|
1147
|
+
}
|
|
1105
1148
|
function withOwnedProcessTree(options) {
|
|
1106
1149
|
return {
|
|
1107
1150
|
...options,
|
|
@@ -1119,9 +1162,9 @@ function positivePid(child, label) {
|
|
|
1119
1162
|
}
|
|
1120
1163
|
return pid;
|
|
1121
1164
|
}
|
|
1122
|
-
function groupExists(pid, label) {
|
|
1165
|
+
function groupExists(pid, label, kill) {
|
|
1123
1166
|
try {
|
|
1124
|
-
|
|
1167
|
+
kill(-pid, 0);
|
|
1125
1168
|
return true;
|
|
1126
1169
|
} catch (cause) {
|
|
1127
1170
|
const code = cause.code;
|
|
@@ -1133,9 +1176,23 @@ function groupExists(pid, label) {
|
|
|
1133
1176
|
}, { cause });
|
|
1134
1177
|
}
|
|
1135
1178
|
}
|
|
1136
|
-
function
|
|
1179
|
+
function processExists(pid, label, kill) {
|
|
1180
|
+
try {
|
|
1181
|
+
kill(pid, 0);
|
|
1182
|
+
return true;
|
|
1183
|
+
} catch (cause) {
|
|
1184
|
+
const code = cause.code;
|
|
1185
|
+
if (code === "ESRCH") return false;
|
|
1186
|
+
if (code === "EPERM") return true;
|
|
1187
|
+
throw new RuntimeDisposalFailure({
|
|
1188
|
+
stage: "quiescence",
|
|
1189
|
+
reason: `${label} runtime process ${pid} state could not be verified`
|
|
1190
|
+
}, { cause });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function signalGroup(pid, signal, label, kill) {
|
|
1137
1194
|
try {
|
|
1138
|
-
|
|
1195
|
+
kill(-pid, signal);
|
|
1139
1196
|
} catch (cause) {
|
|
1140
1197
|
const code = cause.code;
|
|
1141
1198
|
if (code === "ESRCH" || code === "EPERM") return;
|
|
@@ -1145,6 +1202,39 @@ function signalGroup(pid, signal, label) {
|
|
|
1145
1202
|
}, { cause });
|
|
1146
1203
|
}
|
|
1147
1204
|
}
|
|
1205
|
+
async function runTaskkill(pid, options) {
|
|
1206
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
1207
|
+
return new Promise((resolve, reject) => {
|
|
1208
|
+
const signalFailure = (cause) => {
|
|
1209
|
+
reject(new RuntimeDisposalFailure({
|
|
1210
|
+
stage: "signal",
|
|
1211
|
+
reason: `${options.label} runtime process tree termination could not be requested`
|
|
1212
|
+
}, { cause }));
|
|
1213
|
+
};
|
|
1214
|
+
let taskkill;
|
|
1215
|
+
try {
|
|
1216
|
+
taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1217
|
+
windowsHide: true,
|
|
1218
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1219
|
+
});
|
|
1220
|
+
} catch (cause) {
|
|
1221
|
+
signalFailure(cause);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const chunks = [];
|
|
1225
|
+
taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
1226
|
+
taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
1227
|
+
taskkill.once("error", signalFailure);
|
|
1228
|
+
taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
function liveAcceptedPids(accepted, label, kill) {
|
|
1232
|
+
const live = [];
|
|
1233
|
+
for (const pid of accepted) {
|
|
1234
|
+
if (processExists(pid, label, kill)) live.push(pid);
|
|
1235
|
+
}
|
|
1236
|
+
return live;
|
|
1237
|
+
}
|
|
1148
1238
|
async function waitUntil(predicate, timeoutMs) {
|
|
1149
1239
|
const deadline = Date.now() + timeoutMs;
|
|
1150
1240
|
while (predicate()) {
|
|
@@ -1182,29 +1272,27 @@ async function waitWithDeadline(promise, timeoutMs) {
|
|
|
1182
1272
|
);
|
|
1183
1273
|
});
|
|
1184
1274
|
}
|
|
1185
|
-
function requestOwnedProcessTreeTermination(options) {
|
|
1186
|
-
|
|
1275
|
+
async function requestOwnedProcessTreeTermination(options) {
|
|
1276
|
+
const platform = options.platform ?? process.platform;
|
|
1277
|
+
if (platform !== "win32" && options.isClosed()) return;
|
|
1187
1278
|
const pid = positivePid(options.child, options.label);
|
|
1188
1279
|
if (pid === void 0) return;
|
|
1189
|
-
if (
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
reason: `${options.label} runtime process tree could not be terminated`
|
|
1195
|
-
}, { cause: result.error });
|
|
1196
|
-
}
|
|
1197
|
-
terminationRequested.add(options.child);
|
|
1198
|
-
if (result.status !== 0) terminationRequestFailed.add(options.child);
|
|
1280
|
+
if (platform === "win32") {
|
|
1281
|
+
const output = await runTaskkill(pid, options);
|
|
1282
|
+
const state = stateFor(options.child);
|
|
1283
|
+
for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
|
|
1284
|
+
state.requested = true;
|
|
1199
1285
|
return;
|
|
1200
1286
|
}
|
|
1201
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
1202
|
-
|
|
1287
|
+
signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
|
|
1288
|
+
stateFor(options.child).requested = true;
|
|
1203
1289
|
}
|
|
1204
1290
|
async function disposeOwnedProcessTree(options) {
|
|
1205
1291
|
const pid = positivePid(options.child, options.label);
|
|
1206
1292
|
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
1207
1293
|
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
1294
|
+
const platform = options.platform ?? process.platform;
|
|
1295
|
+
const kill = options.killFn ?? defaultKill;
|
|
1208
1296
|
if (pid === void 0) {
|
|
1209
1297
|
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1210
1298
|
throw new RuntimeDisposalFailure({
|
|
@@ -1212,27 +1300,50 @@ async function disposeOwnedProcessTree(options) {
|
|
|
1212
1300
|
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
1213
1301
|
});
|
|
1214
1302
|
}
|
|
1215
|
-
if (
|
|
1216
|
-
if (!
|
|
1217
|
-
|
|
1218
|
-
|
|
1303
|
+
if (platform === "win32") {
|
|
1304
|
+
if (!terminationState.get(options.child)?.requested) {
|
|
1305
|
+
await requestOwnedProcessTreeTermination(options);
|
|
1306
|
+
}
|
|
1307
|
+
const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
|
|
1308
|
+
const deadline = Date.now() + killGraceMs;
|
|
1309
|
+
const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
|
|
1310
|
+
let reswept = false;
|
|
1311
|
+
let live = liveAcceptedPids(accepted, options.label, kill);
|
|
1312
|
+
while (live.length > 0) {
|
|
1313
|
+
if (Date.now() >= deadline) {
|
|
1314
|
+
throw new RuntimeDisposalFailure({
|
|
1315
|
+
stage: "quiescence",
|
|
1316
|
+
reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (!reswept && Date.now() >= resweepAt) {
|
|
1320
|
+
reswept = true;
|
|
1321
|
+
for (const livePid of live) {
|
|
1322
|
+
for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
|
|
1323
|
+
accepted.add(walked);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
await new Promise((resolve) => {
|
|
1328
|
+
setTimeout(resolve, POLL_MS);
|
|
1329
|
+
});
|
|
1330
|
+
live = liveAcceptedPids(accepted, options.label, kill);
|
|
1331
|
+
}
|
|
1332
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1219
1333
|
throw new RuntimeDisposalFailure({
|
|
1220
|
-
stage: "
|
|
1221
|
-
reason: `${options.label} runtime
|
|
1334
|
+
stage: "quiescence",
|
|
1335
|
+
reason: `${options.label} runtime root did not emit close after its process tree quiesced`
|
|
1222
1336
|
});
|
|
1223
1337
|
}
|
|
1224
|
-
|
|
1225
|
-
stage: "quiescence",
|
|
1226
|
-
reason: `${options.label} runtime process tree did not close before the disposal deadline`
|
|
1227
|
-
});
|
|
1338
|
+
return;
|
|
1228
1339
|
}
|
|
1229
|
-
if (groupExists(pid, options.label) && !
|
|
1230
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
1231
|
-
|
|
1340
|
+
if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
|
|
1341
|
+
signalGroup(pid, "SIGTERM", options.label, kill);
|
|
1342
|
+
stateFor(options.child).requested = true;
|
|
1232
1343
|
}
|
|
1233
|
-
if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
|
|
1234
|
-
signalGroup(pid, "SIGKILL", options.label);
|
|
1235
|
-
if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
|
|
1344
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
|
|
1345
|
+
signalGroup(pid, "SIGKILL", options.label, kill);
|
|
1346
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
|
|
1236
1347
|
throw new RuntimeDisposalFailure({
|
|
1237
1348
|
stage: "quiescence",
|
|
1238
1349
|
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
@@ -1330,9 +1441,16 @@ var PiRpcClient = class {
|
|
|
1330
1441
|
);
|
|
1331
1442
|
}
|
|
1332
1443
|
}
|
|
1333
|
-
/**
|
|
1444
|
+
/**
|
|
1445
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
1446
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
1447
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
1448
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
1449
|
+
* swallowing it here loses nothing.
|
|
1450
|
+
*/
|
|
1334
1451
|
kill() {
|
|
1335
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
1452
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
1453
|
+
});
|
|
1336
1454
|
}
|
|
1337
1455
|
waitClosed() {
|
|
1338
1456
|
return this.closedPromise;
|
|
@@ -2135,9 +2253,16 @@ var ClaudeProcessClient = class {
|
|
|
2135
2253
|
);
|
|
2136
2254
|
}
|
|
2137
2255
|
}
|
|
2138
|
-
/**
|
|
2256
|
+
/**
|
|
2257
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
2258
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
2259
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
2260
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
2261
|
+
* swallowing it here loses nothing.
|
|
2262
|
+
*/
|
|
2139
2263
|
kill() {
|
|
2140
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
2264
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2265
|
+
});
|
|
2141
2266
|
}
|
|
2142
2267
|
waitClosed() {
|
|
2143
2268
|
return this.closedPromise;
|
|
@@ -2902,9 +3027,15 @@ var CodexProcessRunner = class {
|
|
|
2902
3027
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
2903
3028
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
2904
3029
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
3030
|
+
*
|
|
3031
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
3032
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
3033
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
3034
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
2905
3035
|
*/
|
|
2906
3036
|
kill() {
|
|
2907
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
3037
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
3038
|
+
});
|
|
2908
3039
|
}
|
|
2909
3040
|
dispose() {
|
|
2910
3041
|
if (!this.disposalAttempt) {
|
|
@@ -4684,14 +4815,17 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
4684
4815
|
if (typeof raw !== "object" || raw === null) {
|
|
4685
4816
|
throw new Error("events poll response is not an object");
|
|
4686
4817
|
}
|
|
4687
|
-
const { events, cursor } = raw;
|
|
4818
|
+
const { events, cursor, capabilities } = raw;
|
|
4688
4819
|
if (!Array.isArray(events)) {
|
|
4689
4820
|
throw new Error("events poll response.events is not an array");
|
|
4690
4821
|
}
|
|
4691
4822
|
if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
|
|
4692
4823
|
throw new Error("events poll response.cursor is not an integer");
|
|
4693
4824
|
}
|
|
4694
|
-
|
|
4825
|
+
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
4826
|
+
throw new Error("events poll response.capabilities is not an array of strings");
|
|
4827
|
+
}
|
|
4828
|
+
return { events, cursor, capabilities: capabilities ?? [] };
|
|
4695
4829
|
}
|
|
4696
4830
|
function extractSkippableSeq(raw) {
|
|
4697
4831
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
@@ -4771,12 +4905,14 @@ var LongPollClient = class {
|
|
|
4771
4905
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
4772
4906
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
4773
4907
|
if (!res.ok) {
|
|
4908
|
+
this.opts.onServerCapabilities?.([]);
|
|
4774
4909
|
this.opts.onOperationalOutcome?.("failure");
|
|
4775
4910
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
4776
4911
|
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
4777
4912
|
continue;
|
|
4778
4913
|
}
|
|
4779
4914
|
const parsed = parseLooseEventsPollResponse(await res.json());
|
|
4915
|
+
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
4780
4916
|
let hadValidationFailureThisBatch = false;
|
|
4781
4917
|
for (const raw of parsed.events) {
|
|
4782
4918
|
let envelope;
|
|
@@ -4820,6 +4956,7 @@ var LongPollClient = class {
|
|
|
4820
4956
|
this.opts.onOperationalOutcome?.("success");
|
|
4821
4957
|
}
|
|
4822
4958
|
} catch (err) {
|
|
4959
|
+
this.opts.onServerCapabilities?.([]);
|
|
4823
4960
|
if (err instanceof DeviceRevokedError) {
|
|
4824
4961
|
this.running = false;
|
|
4825
4962
|
this.opts.onRevoked?.();
|
|
@@ -5055,6 +5192,9 @@ var ConnectionManager = class {
|
|
|
5055
5192
|
// re-attempted.
|
|
5056
5193
|
getCursor: () => this.dedupWatermark(),
|
|
5057
5194
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
5195
|
+
onServerCapabilities: (capabilities) => {
|
|
5196
|
+
if (this.mode === "long-poll") this.serverCapabilities = capabilities;
|
|
5197
|
+
},
|
|
5058
5198
|
onRevoked: () => this.enterRevoked(),
|
|
5059
5199
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
5060
5200
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
@@ -5186,25 +5326,24 @@ var ConnectionManager = class {
|
|
|
5186
5326
|
*/
|
|
5187
5327
|
cancelPendingDrainRetry;
|
|
5188
5328
|
/**
|
|
5189
|
-
* The capabilities the
|
|
5190
|
-
* `
|
|
5191
|
-
*
|
|
5192
|
-
*
|
|
5193
|
-
* successful handshake.
|
|
5329
|
+
* The capabilities the CURRENT transport's server advertised — untyped
|
|
5330
|
+
* `string[]` for forward compatibility. WS populates it from `conn.ack`;
|
|
5331
|
+
* long-poll populates it from each successful events response. Empty until
|
|
5332
|
+
* the active transport supplies an advertisement.
|
|
5194
5333
|
*
|
|
5195
5334
|
* Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
|
|
5196
5335
|
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
5197
5336
|
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
5198
5337
|
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
5199
|
-
* (`enterLongPoll`) — and only
|
|
5338
|
+
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
5339
|
+
* the transport that is still current.
|
|
5200
5340
|
* The previous version of this doc comment claimed long-poll mode simply
|
|
5201
5341
|
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
5202
5342
|
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
5203
5343
|
* session kept believing it applied to whatever it's connected to NOW,
|
|
5204
5344
|
* even after a disconnect/degrade where nothing has actually confirmed
|
|
5205
5345
|
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
5206
|
-
* load balancer
|
|
5207
|
-
* handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5346
|
+
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5208
5347
|
* `task.approval_resolved` on this list — sending it to a server that
|
|
5209
5348
|
* doesn't actually understand it over the long-poll path would get a
|
|
5210
5349
|
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
@@ -5324,13 +5463,10 @@ var ConnectionManager = class {
|
|
|
5324
5463
|
return this.mode === "long-poll";
|
|
5325
5464
|
}
|
|
5326
5465
|
/**
|
|
5327
|
-
* The capabilities the
|
|
5328
|
-
* `conn.ack`
|
|
5329
|
-
*
|
|
5330
|
-
*
|
|
5331
|
-
* first handshake completes, AND (finding R2) once again empty after any
|
|
5332
|
-
* disconnect/degrade — see `serverCapabilities`'s own doc comment for why
|
|
5333
|
-
* this is strictly per-connection rather than "sticky" across one.
|
|
5466
|
+
* The capabilities the CURRENT transport's server advertised: from
|
|
5467
|
+
* `conn.ack` on WS, or the latest successful `GET /byok/events` response
|
|
5468
|
+
* on long-poll. Empty before either transport has supplied its current
|
|
5469
|
+
* advertisement, and cleared across disconnect/switch boundaries.
|
|
5334
5470
|
*/
|
|
5335
5471
|
getServerCapabilities() {
|
|
5336
5472
|
return this.serverCapabilities;
|
|
@@ -9734,9 +9870,9 @@ var TaskRunner = class {
|
|
|
9734
9870
|
/**
|
|
9735
9871
|
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
9736
9872
|
* read fresh on every call, never captured, because the answer changes
|
|
9737
|
-
* across a reconnect (`ConnectionManager
|
|
9738
|
-
*
|
|
9739
|
-
*
|
|
9873
|
+
* across a reconnect or transport switch (`ConnectionManager` clears the
|
|
9874
|
+
* old advertisement at the boundary, then repopulates it from a fresh WS
|
|
9875
|
+
* ack or successful poll response). An absent `getServerCapabilities` seam is
|
|
9740
9876
|
* "no capabilities", the fail-closed reading.
|
|
9741
9877
|
*/
|
|
9742
9878
|
hasResultDocumentCapability() {
|