@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
|
@@ -159,25 +159,24 @@ export declare class ConnectionManager {
|
|
|
159
159
|
*/
|
|
160
160
|
private cancelPendingDrainRetry;
|
|
161
161
|
/**
|
|
162
|
-
* The capabilities the
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* successful handshake.
|
|
162
|
+
* The capabilities the CURRENT transport's server advertised — untyped
|
|
163
|
+
* `string[]` for forward compatibility. WS populates it from `conn.ack`;
|
|
164
|
+
* long-poll populates it from each successful events response. Empty until
|
|
165
|
+
* the active transport supplies an advertisement.
|
|
167
166
|
*
|
|
168
167
|
* Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
|
|
169
168
|
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
170
169
|
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
171
170
|
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
172
|
-
* (`enterLongPoll`) — and only
|
|
171
|
+
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
172
|
+
* the transport that is still current.
|
|
173
173
|
* The previous version of this doc comment claimed long-poll mode simply
|
|
174
174
|
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
175
175
|
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
176
176
|
* session kept believing it applied to whatever it's connected to NOW,
|
|
177
177
|
* even after a disconnect/degrade where nothing has actually confirmed
|
|
178
178
|
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
179
|
-
* load balancer
|
|
180
|
-
* handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
179
|
+
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
181
180
|
* `task.approval_resolved` on this list — sending it to a server that
|
|
182
181
|
* doesn't actually understand it over the long-poll path would get a
|
|
183
182
|
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
@@ -242,13 +241,10 @@ export declare class ConnectionManager {
|
|
|
242
241
|
private drainRetryDelay;
|
|
243
242
|
isTransportDegraded(): boolean;
|
|
244
243
|
/**
|
|
245
|
-
* The capabilities the
|
|
246
|
-
* `conn.ack`
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* first handshake completes, AND (finding R2) once again empty after any
|
|
250
|
-
* disconnect/degrade — see `serverCapabilities`'s own doc comment for why
|
|
251
|
-
* this is strictly per-connection rather than "sticky" across one.
|
|
244
|
+
* The capabilities the CURRENT transport's server advertised: from
|
|
245
|
+
* `conn.ack` on WS, or the latest successful `GET /byok/events` response
|
|
246
|
+
* on long-poll. Empty before either transport has supplied its current
|
|
247
|
+
* advertisement, and cleared across disconnect/switch boundaries.
|
|
252
248
|
*/
|
|
253
249
|
getServerCapabilities(): readonly string[];
|
|
254
250
|
isConnected(): boolean;
|
|
@@ -5,6 +5,12 @@ export interface LongPollClientOptions {
|
|
|
5
5
|
auth: AuthManager;
|
|
6
6
|
getCursor: () => number | undefined;
|
|
7
7
|
onEnvelope: (envelope: Envelope) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Capabilities advertised by the server that produced the current poll
|
|
10
|
+
* response. Called before any envelopes from that response are delivered.
|
|
11
|
+
* An older responder omitting the additive field is reported as `[]`.
|
|
12
|
+
*/
|
|
13
|
+
onServerCapabilities?: (capabilities: string[]) => void;
|
|
8
14
|
/** Called once the device is found to be revoked (401 surfaced through {@link AuthManager}) — the loop stops itself rather than retrying. */
|
|
9
15
|
onRevoked?: () => void;
|
|
10
16
|
/**
|
|
@@ -264,9 +264,10 @@ export interface TaskRunnerDeps {
|
|
|
264
264
|
*/
|
|
265
265
|
maxTaskOutputBytes?: number;
|
|
266
266
|
/**
|
|
267
|
-
* M4 (additive-minor, `task.approval_resolved`): the
|
|
268
|
-
* `conn.ack
|
|
269
|
-
*
|
|
267
|
+
* M4 (additive-minor, `task.approval_resolved`): the capabilities advertised
|
|
268
|
+
* by the CURRENT transport's server (`conn.ack` on WS, the latest successful
|
|
269
|
+
* events response on long-poll) — read fresh at call time (mirrors
|
|
270
|
+
* `getCursor`/`getToken`'s own
|
|
270
271
|
* "read fresh, not captured once" convention elsewhere in this codebase),
|
|
271
272
|
* since the capability is learned asynchronously, after this `TaskRunner`
|
|
272
273
|
* is already constructed (`create-daemon.ts`'s `start()` builds `deps`
|
|
@@ -935,9 +936,9 @@ export declare class TaskRunner {
|
|
|
935
936
|
/**
|
|
936
937
|
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
937
938
|
* read fresh on every call, never captured, because the answer changes
|
|
938
|
-
* across a reconnect (`ConnectionManager
|
|
939
|
-
*
|
|
940
|
-
*
|
|
939
|
+
* across a reconnect or transport switch (`ConnectionManager` clears the
|
|
940
|
+
* old advertisement at the boundary, then repopulates it from a fresh WS
|
|
941
|
+
* ack or successful poll response). An absent `getServerCapabilities` seam is
|
|
941
942
|
* "no capabilities", the fail-closed reading.
|
|
942
943
|
*/
|
|
943
944
|
private hasResultDocumentCapability;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile, spawn
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
2
|
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
3
3
|
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
|
|
4
4
|
import path17, { join, isAbsolute } from 'path';
|
|
@@ -834,12 +834,12 @@ function resolvePiBin() {
|
|
|
834
834
|
}
|
|
835
835
|
} catch (cause) {
|
|
836
836
|
throw new Error(
|
|
837
|
-
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.
|
|
837
|
+
`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`,
|
|
838
838
|
{ cause }
|
|
839
839
|
);
|
|
840
840
|
}
|
|
841
841
|
throw new Error(
|
|
842
|
-
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.
|
|
842
|
+
`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`
|
|
843
843
|
);
|
|
844
844
|
}
|
|
845
845
|
|
|
@@ -1044,11 +1044,54 @@ var AsyncQueue = class {
|
|
|
1044
1044
|
};
|
|
1045
1045
|
}
|
|
1046
1046
|
};
|
|
1047
|
+
|
|
1048
|
+
// src/adapters/taskkill-pid-set.ts
|
|
1049
|
+
var INTEGER_PATTERN = /\d+/g;
|
|
1050
|
+
function isCandidatePid(value) {
|
|
1051
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1052
|
+
}
|
|
1053
|
+
function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
|
|
1054
|
+
const excluded = new Set(excludedPids);
|
|
1055
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1056
|
+
if (isCandidatePid(rootPid)) accepted.add(rootPid);
|
|
1057
|
+
const lines = text.split(/\r?\n/).map((line) => {
|
|
1058
|
+
const pids = [];
|
|
1059
|
+
for (const match of line.matchAll(INTEGER_PATTERN)) {
|
|
1060
|
+
const pid = Number(match[0]);
|
|
1061
|
+
if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
|
|
1062
|
+
}
|
|
1063
|
+
return pids;
|
|
1064
|
+
});
|
|
1065
|
+
let changed = true;
|
|
1066
|
+
while (changed) {
|
|
1067
|
+
changed = false;
|
|
1068
|
+
for (const pids of lines) {
|
|
1069
|
+
if (!pids.some((pid) => accepted.has(pid))) continue;
|
|
1070
|
+
for (const pid of pids) {
|
|
1071
|
+
if (accepted.has(pid)) continue;
|
|
1072
|
+
accepted.add(pid);
|
|
1073
|
+
changed = true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return accepted;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/adapters/process-tree.ts
|
|
1047
1081
|
var DEFAULT_TERM_GRACE_MS = 750;
|
|
1048
1082
|
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
1049
1083
|
var POLL_MS = 20;
|
|
1050
|
-
var
|
|
1051
|
-
|
|
1084
|
+
var terminationState = /* @__PURE__ */ new WeakMap();
|
|
1085
|
+
function stateFor(child) {
|
|
1086
|
+
const existing = terminationState.get(child);
|
|
1087
|
+
if (existing) return existing;
|
|
1088
|
+
const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
|
|
1089
|
+
terminationState.set(child, created);
|
|
1090
|
+
return created;
|
|
1091
|
+
}
|
|
1092
|
+
function defaultKill(pid, signal) {
|
|
1093
|
+
process.kill(pid, signal);
|
|
1094
|
+
}
|
|
1052
1095
|
function withOwnedProcessTree(options) {
|
|
1053
1096
|
return {
|
|
1054
1097
|
...options,
|
|
@@ -1066,9 +1109,9 @@ function positivePid(child, label) {
|
|
|
1066
1109
|
}
|
|
1067
1110
|
return pid;
|
|
1068
1111
|
}
|
|
1069
|
-
function groupExists(pid, label) {
|
|
1112
|
+
function groupExists(pid, label, kill) {
|
|
1070
1113
|
try {
|
|
1071
|
-
|
|
1114
|
+
kill(-pid, 0);
|
|
1072
1115
|
return true;
|
|
1073
1116
|
} catch (cause) {
|
|
1074
1117
|
const code = cause.code;
|
|
@@ -1080,9 +1123,23 @@ function groupExists(pid, label) {
|
|
|
1080
1123
|
}, { cause });
|
|
1081
1124
|
}
|
|
1082
1125
|
}
|
|
1083
|
-
function
|
|
1126
|
+
function processExists(pid, label, kill) {
|
|
1084
1127
|
try {
|
|
1085
|
-
|
|
1128
|
+
kill(pid, 0);
|
|
1129
|
+
return true;
|
|
1130
|
+
} catch (cause) {
|
|
1131
|
+
const code = cause.code;
|
|
1132
|
+
if (code === "ESRCH") return false;
|
|
1133
|
+
if (code === "EPERM") return true;
|
|
1134
|
+
throw new RuntimeDisposalFailure({
|
|
1135
|
+
stage: "quiescence",
|
|
1136
|
+
reason: `${label} runtime process ${pid} state could not be verified`
|
|
1137
|
+
}, { cause });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function signalGroup(pid, signal, label, kill) {
|
|
1141
|
+
try {
|
|
1142
|
+
kill(-pid, signal);
|
|
1086
1143
|
} catch (cause) {
|
|
1087
1144
|
const code = cause.code;
|
|
1088
1145
|
if (code === "ESRCH" || code === "EPERM") return;
|
|
@@ -1092,6 +1149,39 @@ function signalGroup(pid, signal, label) {
|
|
|
1092
1149
|
}, { cause });
|
|
1093
1150
|
}
|
|
1094
1151
|
}
|
|
1152
|
+
async function runTaskkill(pid, options) {
|
|
1153
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
1154
|
+
return new Promise((resolve, reject) => {
|
|
1155
|
+
const signalFailure = (cause) => {
|
|
1156
|
+
reject(new RuntimeDisposalFailure({
|
|
1157
|
+
stage: "signal",
|
|
1158
|
+
reason: `${options.label} runtime process tree termination could not be requested`
|
|
1159
|
+
}, { cause }));
|
|
1160
|
+
};
|
|
1161
|
+
let taskkill;
|
|
1162
|
+
try {
|
|
1163
|
+
taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1164
|
+
windowsHide: true,
|
|
1165
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1166
|
+
});
|
|
1167
|
+
} catch (cause) {
|
|
1168
|
+
signalFailure(cause);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const chunks = [];
|
|
1172
|
+
taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
1173
|
+
taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
1174
|
+
taskkill.once("error", signalFailure);
|
|
1175
|
+
taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
function liveAcceptedPids(accepted, label, kill) {
|
|
1179
|
+
const live = [];
|
|
1180
|
+
for (const pid of accepted) {
|
|
1181
|
+
if (processExists(pid, label, kill)) live.push(pid);
|
|
1182
|
+
}
|
|
1183
|
+
return live;
|
|
1184
|
+
}
|
|
1095
1185
|
async function waitUntil(predicate, timeoutMs) {
|
|
1096
1186
|
const deadline = Date.now() + timeoutMs;
|
|
1097
1187
|
while (predicate()) {
|
|
@@ -1129,29 +1219,27 @@ async function waitWithDeadline(promise, timeoutMs) {
|
|
|
1129
1219
|
);
|
|
1130
1220
|
});
|
|
1131
1221
|
}
|
|
1132
|
-
function requestOwnedProcessTreeTermination(options) {
|
|
1133
|
-
|
|
1222
|
+
async function requestOwnedProcessTreeTermination(options) {
|
|
1223
|
+
const platform = options.platform ?? process.platform;
|
|
1224
|
+
if (platform !== "win32" && options.isClosed()) return;
|
|
1134
1225
|
const pid = positivePid(options.child, options.label);
|
|
1135
1226
|
if (pid === void 0) return;
|
|
1136
|
-
if (
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
reason: `${options.label} runtime process tree could not be terminated`
|
|
1142
|
-
}, { cause: result.error });
|
|
1143
|
-
}
|
|
1144
|
-
terminationRequested.add(options.child);
|
|
1145
|
-
if (result.status !== 0) terminationRequestFailed.add(options.child);
|
|
1227
|
+
if (platform === "win32") {
|
|
1228
|
+
const output = await runTaskkill(pid, options);
|
|
1229
|
+
const state = stateFor(options.child);
|
|
1230
|
+
for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
|
|
1231
|
+
state.requested = true;
|
|
1146
1232
|
return;
|
|
1147
1233
|
}
|
|
1148
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
1149
|
-
|
|
1234
|
+
signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
|
|
1235
|
+
stateFor(options.child).requested = true;
|
|
1150
1236
|
}
|
|
1151
1237
|
async function disposeOwnedProcessTree(options) {
|
|
1152
1238
|
const pid = positivePid(options.child, options.label);
|
|
1153
1239
|
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
1154
1240
|
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
1241
|
+
const platform = options.platform ?? process.platform;
|
|
1242
|
+
const kill = options.killFn ?? defaultKill;
|
|
1155
1243
|
if (pid === void 0) {
|
|
1156
1244
|
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1157
1245
|
throw new RuntimeDisposalFailure({
|
|
@@ -1159,27 +1247,50 @@ async function disposeOwnedProcessTree(options) {
|
|
|
1159
1247
|
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
1160
1248
|
});
|
|
1161
1249
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
if (!
|
|
1164
|
-
|
|
1165
|
-
|
|
1250
|
+
if (platform === "win32") {
|
|
1251
|
+
if (!terminationState.get(options.child)?.requested) {
|
|
1252
|
+
await requestOwnedProcessTreeTermination(options);
|
|
1253
|
+
}
|
|
1254
|
+
const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
|
|
1255
|
+
const deadline = Date.now() + killGraceMs;
|
|
1256
|
+
const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
|
|
1257
|
+
let reswept = false;
|
|
1258
|
+
let live = liveAcceptedPids(accepted, options.label, kill);
|
|
1259
|
+
while (live.length > 0) {
|
|
1260
|
+
if (Date.now() >= deadline) {
|
|
1261
|
+
throw new RuntimeDisposalFailure({
|
|
1262
|
+
stage: "quiescence",
|
|
1263
|
+
reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (!reswept && Date.now() >= resweepAt) {
|
|
1267
|
+
reswept = true;
|
|
1268
|
+
for (const livePid of live) {
|
|
1269
|
+
for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
|
|
1270
|
+
accepted.add(walked);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
await new Promise((resolve) => {
|
|
1275
|
+
setTimeout(resolve, POLL_MS);
|
|
1276
|
+
});
|
|
1277
|
+
live = liveAcceptedPids(accepted, options.label, kill);
|
|
1278
|
+
}
|
|
1279
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1166
1280
|
throw new RuntimeDisposalFailure({
|
|
1167
|
-
stage: "
|
|
1168
|
-
reason: `${options.label} runtime
|
|
1281
|
+
stage: "quiescence",
|
|
1282
|
+
reason: `${options.label} runtime root did not emit close after its process tree quiesced`
|
|
1169
1283
|
});
|
|
1170
1284
|
}
|
|
1171
|
-
|
|
1172
|
-
stage: "quiescence",
|
|
1173
|
-
reason: `${options.label} runtime process tree did not close before the disposal deadline`
|
|
1174
|
-
});
|
|
1285
|
+
return;
|
|
1175
1286
|
}
|
|
1176
|
-
if (groupExists(pid, options.label) && !
|
|
1177
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
1178
|
-
|
|
1287
|
+
if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
|
|
1288
|
+
signalGroup(pid, "SIGTERM", options.label, kill);
|
|
1289
|
+
stateFor(options.child).requested = true;
|
|
1179
1290
|
}
|
|
1180
|
-
if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
|
|
1181
|
-
signalGroup(pid, "SIGKILL", options.label);
|
|
1182
|
-
if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
|
|
1291
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
|
|
1292
|
+
signalGroup(pid, "SIGKILL", options.label, kill);
|
|
1293
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
|
|
1183
1294
|
throw new RuntimeDisposalFailure({
|
|
1184
1295
|
stage: "quiescence",
|
|
1185
1296
|
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
@@ -1277,9 +1388,16 @@ var PiRpcClient = class {
|
|
|
1277
1388
|
);
|
|
1278
1389
|
}
|
|
1279
1390
|
}
|
|
1280
|
-
/**
|
|
1391
|
+
/**
|
|
1392
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
1393
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
1394
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
1395
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
1396
|
+
* swallowing it here loses nothing.
|
|
1397
|
+
*/
|
|
1281
1398
|
kill() {
|
|
1282
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
1399
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
1400
|
+
});
|
|
1283
1401
|
}
|
|
1284
1402
|
waitClosed() {
|
|
1285
1403
|
return this.closedPromise;
|
|
@@ -2082,9 +2200,16 @@ var ClaudeProcessClient = class {
|
|
|
2082
2200
|
);
|
|
2083
2201
|
}
|
|
2084
2202
|
}
|
|
2085
|
-
/**
|
|
2203
|
+
/**
|
|
2204
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
2205
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
2206
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
2207
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
2208
|
+
* swallowing it here loses nothing.
|
|
2209
|
+
*/
|
|
2086
2210
|
kill() {
|
|
2087
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
2211
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2212
|
+
});
|
|
2088
2213
|
}
|
|
2089
2214
|
waitClosed() {
|
|
2090
2215
|
return this.closedPromise;
|
|
@@ -2849,9 +2974,15 @@ var CodexProcessRunner = class {
|
|
|
2849
2974
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
2850
2975
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
2851
2976
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
2977
|
+
*
|
|
2978
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
2979
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
2980
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
2981
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
2852
2982
|
*/
|
|
2853
2983
|
kill() {
|
|
2854
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
2984
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2985
|
+
});
|
|
2855
2986
|
}
|
|
2856
2987
|
dispose() {
|
|
2857
2988
|
if (!this.disposalAttempt) {
|
|
@@ -4631,14 +4762,17 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
4631
4762
|
if (typeof raw !== "object" || raw === null) {
|
|
4632
4763
|
throw new Error("events poll response is not an object");
|
|
4633
4764
|
}
|
|
4634
|
-
const { events, cursor } = raw;
|
|
4765
|
+
const { events, cursor, capabilities } = raw;
|
|
4635
4766
|
if (!Array.isArray(events)) {
|
|
4636
4767
|
throw new Error("events poll response.events is not an array");
|
|
4637
4768
|
}
|
|
4638
4769
|
if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
|
|
4639
4770
|
throw new Error("events poll response.cursor is not an integer");
|
|
4640
4771
|
}
|
|
4641
|
-
|
|
4772
|
+
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
4773
|
+
throw new Error("events poll response.capabilities is not an array of strings");
|
|
4774
|
+
}
|
|
4775
|
+
return { events, cursor, capabilities: capabilities ?? [] };
|
|
4642
4776
|
}
|
|
4643
4777
|
function extractSkippableSeq(raw) {
|
|
4644
4778
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
@@ -4718,12 +4852,14 @@ var LongPollClient = class {
|
|
|
4718
4852
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
4719
4853
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
4720
4854
|
if (!res.ok) {
|
|
4855
|
+
this.opts.onServerCapabilities?.([]);
|
|
4721
4856
|
this.opts.onOperationalOutcome?.("failure");
|
|
4722
4857
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
4723
4858
|
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
4724
4859
|
continue;
|
|
4725
4860
|
}
|
|
4726
4861
|
const parsed = parseLooseEventsPollResponse(await res.json());
|
|
4862
|
+
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
4727
4863
|
let hadValidationFailureThisBatch = false;
|
|
4728
4864
|
for (const raw of parsed.events) {
|
|
4729
4865
|
let envelope;
|
|
@@ -4767,6 +4903,7 @@ var LongPollClient = class {
|
|
|
4767
4903
|
this.opts.onOperationalOutcome?.("success");
|
|
4768
4904
|
}
|
|
4769
4905
|
} catch (err) {
|
|
4906
|
+
this.opts.onServerCapabilities?.([]);
|
|
4770
4907
|
if (err instanceof DeviceRevokedError) {
|
|
4771
4908
|
this.running = false;
|
|
4772
4909
|
this.opts.onRevoked?.();
|
|
@@ -5002,6 +5139,9 @@ var ConnectionManager = class {
|
|
|
5002
5139
|
// re-attempted.
|
|
5003
5140
|
getCursor: () => this.dedupWatermark(),
|
|
5004
5141
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
5142
|
+
onServerCapabilities: (capabilities) => {
|
|
5143
|
+
if (this.mode === "long-poll") this.serverCapabilities = capabilities;
|
|
5144
|
+
},
|
|
5005
5145
|
onRevoked: () => this.enterRevoked(),
|
|
5006
5146
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
5007
5147
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
@@ -5133,25 +5273,24 @@ var ConnectionManager = class {
|
|
|
5133
5273
|
*/
|
|
5134
5274
|
cancelPendingDrainRetry;
|
|
5135
5275
|
/**
|
|
5136
|
-
* The capabilities the
|
|
5137
|
-
* `
|
|
5138
|
-
*
|
|
5139
|
-
*
|
|
5140
|
-
* successful handshake.
|
|
5276
|
+
* The capabilities the CURRENT transport's server advertised — untyped
|
|
5277
|
+
* `string[]` for forward compatibility. WS populates it from `conn.ack`;
|
|
5278
|
+
* long-poll populates it from each successful events response. Empty until
|
|
5279
|
+
* the active transport supplies an advertisement.
|
|
5141
5280
|
*
|
|
5142
5281
|
* Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
|
|
5143
5282
|
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
5144
5283
|
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
5145
5284
|
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
5146
|
-
* (`enterLongPoll`) — and only
|
|
5285
|
+
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
5286
|
+
* the transport that is still current.
|
|
5147
5287
|
* The previous version of this doc comment claimed long-poll mode simply
|
|
5148
5288
|
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
5149
5289
|
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
5150
5290
|
* session kept believing it applied to whatever it's connected to NOW,
|
|
5151
5291
|
* even after a disconnect/degrade where nothing has actually confirmed
|
|
5152
5292
|
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
5153
|
-
* load balancer
|
|
5154
|
-
* handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5293
|
+
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5155
5294
|
* `task.approval_resolved` on this list — sending it to a server that
|
|
5156
5295
|
* doesn't actually understand it over the long-poll path would get a
|
|
5157
5296
|
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
@@ -5271,13 +5410,10 @@ var ConnectionManager = class {
|
|
|
5271
5410
|
return this.mode === "long-poll";
|
|
5272
5411
|
}
|
|
5273
5412
|
/**
|
|
5274
|
-
* The capabilities the
|
|
5275
|
-
* `conn.ack`
|
|
5276
|
-
*
|
|
5277
|
-
*
|
|
5278
|
-
* first handshake completes, AND (finding R2) once again empty after any
|
|
5279
|
-
* disconnect/degrade — see `serverCapabilities`'s own doc comment for why
|
|
5280
|
-
* this is strictly per-connection rather than "sticky" across one.
|
|
5413
|
+
* The capabilities the CURRENT transport's server advertised: from
|
|
5414
|
+
* `conn.ack` on WS, or the latest successful `GET /byok/events` response
|
|
5415
|
+
* on long-poll. Empty before either transport has supplied its current
|
|
5416
|
+
* advertisement, and cleared across disconnect/switch boundaries.
|
|
5281
5417
|
*/
|
|
5282
5418
|
getServerCapabilities() {
|
|
5283
5419
|
return this.serverCapabilities;
|
|
@@ -9647,9 +9783,9 @@ var TaskRunner = class {
|
|
|
9647
9783
|
/**
|
|
9648
9784
|
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
9649
9785
|
* read fresh on every call, never captured, because the answer changes
|
|
9650
|
-
* across a reconnect (`ConnectionManager
|
|
9651
|
-
*
|
|
9652
|
-
*
|
|
9786
|
+
* across a reconnect or transport switch (`ConnectionManager` clears the
|
|
9787
|
+
* old advertisement at the boundary, then repopulates it from a fresh WS
|
|
9788
|
+
* ack or successful poll response). An absent `getServerCapabilities` seam is
|
|
9653
9789
|
* "no capabilities", the fail-closed reading.
|
|
9654
9790
|
*/
|
|
9655
9791
|
hasResultDocumentCapability() {
|