@byok-sdk/client 0.4.0 → 0.4.2

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execFile, spawn, spawnSync } from 'child_process';
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.19+ pi sidecar`,
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.19+ pi sidecar`
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
 
@@ -931,6 +931,24 @@ function mapPermissionPolicyToPiArgs(policy) {
931
931
  }
932
932
 
933
933
  // src/adapters/pi/events.ts
934
+ function requireToolCallId(msg) {
935
+ if (typeof msg.toolCallId === "string" && msg.toolCallId.trim().length > 0) return msg.toolCallId;
936
+ throw new RuntimeExecutionFailure({
937
+ phase: "run",
938
+ category: "authority",
939
+ retry: "non-retryable",
940
+ reason: `pi ${msg.type} frame had no authoritative tool call id`
941
+ });
942
+ }
943
+ function requireToolResultOutcome(msg) {
944
+ if (typeof msg.isError === "boolean") return msg.isError;
945
+ throw new RuntimeExecutionFailure({
946
+ phase: "run",
947
+ category: "authority",
948
+ retry: "non-retryable",
949
+ reason: "pi tool_execution_end frame had no authoritative isError outcome"
950
+ });
951
+ }
934
952
  function mapPiMessageToAgentEvent(msg) {
935
953
  switch (msg.type) {
936
954
  case "message_update": {
@@ -941,16 +959,22 @@ function mapPiMessageToAgentEvent(msg) {
941
959
  return void 0;
942
960
  }
943
961
  case "tool_execution_start": {
962
+ const toolCallId = requireToolCallId(msg);
944
963
  if (typeof msg.toolName !== "string") return void 0;
945
- return { type: "tool_use", tool: msg.toolName, input: msg.args };
964
+ return { type: "tool_use", tool: msg.toolName, input: msg.args, toolCallId };
946
965
  }
947
966
  case "tool_execution_end": {
967
+ const toolCallId = requireToolCallId(msg);
968
+ const isError = requireToolResultOutcome(msg);
948
969
  if (typeof msg.toolName !== "string") return void 0;
949
- return {
970
+ const event = {
950
971
  type: "tool_result",
951
972
  tool: msg.toolName,
952
- output: { result: msg.result, isError: msg.isError === true }
973
+ output: { result: msg.result },
974
+ toolCallId,
975
+ isError
953
976
  };
977
+ return event;
954
978
  }
955
979
  case "agent_settled":
956
980
  return { type: "turn_end" };
@@ -1097,11 +1121,54 @@ var AsyncQueue = class {
1097
1121
  };
1098
1122
  }
1099
1123
  };
1124
+
1125
+ // src/adapters/taskkill-pid-set.ts
1126
+ var INTEGER_PATTERN = /\d+/g;
1127
+ function isCandidatePid(value) {
1128
+ return Number.isSafeInteger(value) && value > 0;
1129
+ }
1130
+ function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
1131
+ const excluded = new Set(excludedPids);
1132
+ const accepted = /* @__PURE__ */ new Set();
1133
+ if (isCandidatePid(rootPid)) accepted.add(rootPid);
1134
+ const lines = text.split(/\r?\n/).map((line) => {
1135
+ const pids = [];
1136
+ for (const match of line.matchAll(INTEGER_PATTERN)) {
1137
+ const pid = Number(match[0]);
1138
+ if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
1139
+ }
1140
+ return pids;
1141
+ });
1142
+ let changed = true;
1143
+ while (changed) {
1144
+ changed = false;
1145
+ for (const pids of lines) {
1146
+ if (!pids.some((pid) => accepted.has(pid))) continue;
1147
+ for (const pid of pids) {
1148
+ if (accepted.has(pid)) continue;
1149
+ accepted.add(pid);
1150
+ changed = true;
1151
+ }
1152
+ }
1153
+ }
1154
+ return accepted;
1155
+ }
1156
+
1157
+ // src/adapters/process-tree.ts
1100
1158
  var DEFAULT_TERM_GRACE_MS = 750;
1101
1159
  var DEFAULT_KILL_GRACE_MS = 2e3;
1102
1160
  var POLL_MS = 20;
1103
- var terminationRequested = /* @__PURE__ */ new WeakSet();
1104
- var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
1161
+ var terminationState = /* @__PURE__ */ new WeakMap();
1162
+ function stateFor(child) {
1163
+ const existing = terminationState.get(child);
1164
+ if (existing) return existing;
1165
+ const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
1166
+ terminationState.set(child, created);
1167
+ return created;
1168
+ }
1169
+ function defaultKill(pid, signal) {
1170
+ process.kill(pid, signal);
1171
+ }
1105
1172
  function withOwnedProcessTree(options) {
1106
1173
  return {
1107
1174
  ...options,
@@ -1119,9 +1186,9 @@ function positivePid(child, label) {
1119
1186
  }
1120
1187
  return pid;
1121
1188
  }
1122
- function groupExists(pid, label) {
1189
+ function groupExists(pid, label, kill) {
1123
1190
  try {
1124
- process.kill(-pid, 0);
1191
+ kill(-pid, 0);
1125
1192
  return true;
1126
1193
  } catch (cause) {
1127
1194
  const code = cause.code;
@@ -1133,9 +1200,23 @@ function groupExists(pid, label) {
1133
1200
  }, { cause });
1134
1201
  }
1135
1202
  }
1136
- function signalGroup(pid, signal, label) {
1203
+ function processExists(pid, label, kill) {
1137
1204
  try {
1138
- process.kill(-pid, signal);
1205
+ kill(pid, 0);
1206
+ return true;
1207
+ } catch (cause) {
1208
+ const code = cause.code;
1209
+ if (code === "ESRCH") return false;
1210
+ if (code === "EPERM") return true;
1211
+ throw new RuntimeDisposalFailure({
1212
+ stage: "quiescence",
1213
+ reason: `${label} runtime process ${pid} state could not be verified`
1214
+ }, { cause });
1215
+ }
1216
+ }
1217
+ function signalGroup(pid, signal, label, kill) {
1218
+ try {
1219
+ kill(-pid, signal);
1139
1220
  } catch (cause) {
1140
1221
  const code = cause.code;
1141
1222
  if (code === "ESRCH" || code === "EPERM") return;
@@ -1145,6 +1226,39 @@ function signalGroup(pid, signal, label) {
1145
1226
  }, { cause });
1146
1227
  }
1147
1228
  }
1229
+ async function runTaskkill(pid, options) {
1230
+ const spawnFn = options.spawnFn ?? spawn;
1231
+ return new Promise((resolve, reject) => {
1232
+ const signalFailure = (cause) => {
1233
+ reject(new RuntimeDisposalFailure({
1234
+ stage: "signal",
1235
+ reason: `${options.label} runtime process tree termination could not be requested`
1236
+ }, { cause }));
1237
+ };
1238
+ let taskkill;
1239
+ try {
1240
+ taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
1241
+ windowsHide: true,
1242
+ stdio: ["ignore", "pipe", "pipe"]
1243
+ });
1244
+ } catch (cause) {
1245
+ signalFailure(cause);
1246
+ return;
1247
+ }
1248
+ const chunks = [];
1249
+ taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
1250
+ taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
1251
+ taskkill.once("error", signalFailure);
1252
+ taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
1253
+ });
1254
+ }
1255
+ function liveAcceptedPids(accepted, label, kill) {
1256
+ const live = [];
1257
+ for (const pid of accepted) {
1258
+ if (processExists(pid, label, kill)) live.push(pid);
1259
+ }
1260
+ return live;
1261
+ }
1148
1262
  async function waitUntil(predicate, timeoutMs) {
1149
1263
  const deadline = Date.now() + timeoutMs;
1150
1264
  while (predicate()) {
@@ -1182,29 +1296,27 @@ async function waitWithDeadline(promise, timeoutMs) {
1182
1296
  );
1183
1297
  });
1184
1298
  }
1185
- function requestOwnedProcessTreeTermination(options) {
1186
- if (options.isClosed()) return;
1299
+ async function requestOwnedProcessTreeTermination(options) {
1300
+ const platform = options.platform ?? process.platform;
1301
+ if (platform !== "win32" && options.isClosed()) return;
1187
1302
  const pid = positivePid(options.child, options.label);
1188
1303
  if (pid === void 0) return;
1189
- if (process.platform === "win32") {
1190
- const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
1191
- if (result.error) {
1192
- throw new RuntimeDisposalFailure({
1193
- stage: "signal",
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);
1304
+ if (platform === "win32") {
1305
+ const output = await runTaskkill(pid, options);
1306
+ const state = stateFor(options.child);
1307
+ for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
1308
+ state.requested = true;
1199
1309
  return;
1200
1310
  }
1201
- signalGroup(pid, "SIGTERM", options.label);
1202
- terminationRequested.add(options.child);
1311
+ signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
1312
+ stateFor(options.child).requested = true;
1203
1313
  }
1204
1314
  async function disposeOwnedProcessTree(options) {
1205
1315
  const pid = positivePid(options.child, options.label);
1206
1316
  const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
1207
1317
  const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
1318
+ const platform = options.platform ?? process.platform;
1319
+ const kill = options.killFn ?? defaultKill;
1208
1320
  if (pid === void 0) {
1209
1321
  if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
1210
1322
  throw new RuntimeDisposalFailure({
@@ -1212,27 +1324,50 @@ async function disposeOwnedProcessTree(options) {
1212
1324
  reason: `${options.label} runtime process did not settle after spawn failure`
1213
1325
  });
1214
1326
  }
1215
- if (process.platform === "win32") {
1216
- if (!options.isClosed() && !terminationRequested.has(options.child)) requestOwnedProcessTreeTermination(options);
1217
- if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
1218
- if (terminationRequestFailed.has(options.child)) {
1327
+ if (platform === "win32") {
1328
+ if (!terminationState.get(options.child)?.requested) {
1329
+ await requestOwnedProcessTreeTermination(options);
1330
+ }
1331
+ const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
1332
+ const deadline = Date.now() + killGraceMs;
1333
+ const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
1334
+ let reswept = false;
1335
+ let live = liveAcceptedPids(accepted, options.label, kill);
1336
+ while (live.length > 0) {
1337
+ if (Date.now() >= deadline) {
1338
+ throw new RuntimeDisposalFailure({
1339
+ stage: "quiescence",
1340
+ reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
1341
+ });
1342
+ }
1343
+ if (!reswept && Date.now() >= resweepAt) {
1344
+ reswept = true;
1345
+ for (const livePid of live) {
1346
+ for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
1347
+ accepted.add(walked);
1348
+ }
1349
+ }
1350
+ }
1351
+ await new Promise((resolve) => {
1352
+ setTimeout(resolve, POLL_MS);
1353
+ });
1354
+ live = liveAcceptedPids(accepted, options.label, kill);
1355
+ }
1356
+ if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
1219
1357
  throw new RuntimeDisposalFailure({
1220
- stage: "signal",
1221
- reason: `${options.label} runtime process tree could not be terminated`
1358
+ stage: "quiescence",
1359
+ reason: `${options.label} runtime root did not emit close after its process tree quiesced`
1222
1360
  });
1223
1361
  }
1224
- throw new RuntimeDisposalFailure({
1225
- stage: "quiescence",
1226
- reason: `${options.label} runtime process tree did not close before the disposal deadline`
1227
- });
1362
+ return;
1228
1363
  }
1229
- if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
1230
- signalGroup(pid, "SIGTERM", options.label);
1231
- terminationRequested.add(options.child);
1364
+ if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
1365
+ signalGroup(pid, "SIGTERM", options.label, kill);
1366
+ stateFor(options.child).requested = true;
1232
1367
  }
1233
- if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
1234
- signalGroup(pid, "SIGKILL", options.label);
1235
- if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
1368
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
1369
+ signalGroup(pid, "SIGKILL", options.label, kill);
1370
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
1236
1371
  throw new RuntimeDisposalFailure({
1237
1372
  stage: "quiescence",
1238
1373
  reason: `${options.label} runtime process group remained live after SIGKILL`
@@ -1330,9 +1465,16 @@ var PiRpcClient = class {
1330
1465
  );
1331
1466
  }
1332
1467
  }
1333
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
1468
+ /**
1469
+ * Immediate process-tree termination request. `dispose()` is the settlement
1470
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
1471
+ * terminator. A request that could not be spawned is left unrecorded, so
1472
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
1473
+ * swallowing it here loses nothing.
1474
+ */
1334
1475
  kill() {
1335
- requestOwnedProcessTreeTermination(this.processTreeOptions());
1476
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
1477
+ });
1336
1478
  }
1337
1479
  waitClosed() {
1338
1480
  return this.closedPromise;
@@ -1844,6 +1986,17 @@ function subtractDenied(tools, denyTools) {
1844
1986
  function createToolUseCorrelation() {
1845
1987
  return { toolNameByUseId: /* @__PURE__ */ new Map() };
1846
1988
  }
1989
+ function missingToolCallIdFailure(frame) {
1990
+ return new RuntimeExecutionFailure({
1991
+ phase: "run",
1992
+ category: "authority",
1993
+ retry: "non-retryable",
1994
+ reason: `claude ${frame} frame had no authoritative tool call id`
1995
+ });
1996
+ }
1997
+ function isAuthoritativeToolCallId(value) {
1998
+ return typeof value === "string" && value.trim().length > 0;
1999
+ }
1847
2000
  var ROUTINE_CLAUDE_SYSTEM_SUBTYPES = /* @__PURE__ */ new Set([
1848
2001
  "init",
1849
2002
  "hook_started",
@@ -1887,9 +2040,12 @@ function mapAssistant(msg, correlation) {
1887
2040
  }
1888
2041
  break;
1889
2042
  case "tool_use":
1890
- if (typeof block.id === "string" && typeof block.name === "string") {
2043
+ if (!isAuthoritativeToolCallId(block.id)) {
2044
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_use") };
2045
+ }
2046
+ if (typeof block.name === "string") {
1891
2047
  correlation.toolNameByUseId.set(block.id, block.name);
1892
- events.push({ type: "tool_use", tool: block.name, input: block.input });
2048
+ events.push({ type: "tool_use", tool: block.name, input: block.input, toolCallId: block.id });
1893
2049
  }
1894
2050
  break;
1895
2051
  // Deliberately NOT mapped to `progress` — mirrors pi's own choice to
@@ -1925,11 +2081,20 @@ function mapUser(msg, correlation, options) {
1925
2081
  unmappedLabel = unmappedLabel ?? `user-block:${String(block.type)}`;
1926
2082
  continue;
1927
2083
  }
1928
- const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : void 0;
1929
- const tool = toolUseId && correlation.toolNameByUseId.get(toolUseId) || "unknown";
1930
- const isError = block.is_error === true;
1931
- events.push({ type: "tool_result", tool, output: { content: block.content, isError } });
1932
- if (!isError && FILE_WRITING_TOOLS.has(tool)) {
2084
+ if (!isAuthoritativeToolCallId(block.tool_use_id)) {
2085
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_result") };
2086
+ }
2087
+ const tool = correlation.toolNameByUseId.get(block.tool_use_id) ?? "unknown";
2088
+ const isError = typeof block.is_error === "boolean" ? block.is_error : void 0;
2089
+ const event = {
2090
+ type: "tool_result",
2091
+ tool,
2092
+ output: { content: block.content },
2093
+ toolCallId: block.tool_use_id
2094
+ };
2095
+ if (isError !== void 0) event.isError = isError;
2096
+ events.push(event);
2097
+ if (isError === false && FILE_WRITING_TOOLS.has(tool)) {
1933
2098
  const artifact = tryBuildArtifactEvent(msg, options.workspaceDir);
1934
2099
  if (artifact) events.push(artifact);
1935
2100
  }
@@ -2135,9 +2300,16 @@ var ClaudeProcessClient = class {
2135
2300
  );
2136
2301
  }
2137
2302
  }
2138
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
2303
+ /**
2304
+ * Immediate process-tree termination request. `dispose()` is the settlement
2305
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
2306
+ * terminator. A request that could not be spawned is left unrecorded, so
2307
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
2308
+ * swallowing it here loses nothing.
2309
+ */
2139
2310
  kill() {
2140
- requestOwnedProcessTreeTermination(this.processTreeOptions());
2311
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
2312
+ });
2141
2313
  }
2142
2314
  waitClosed() {
2143
2315
  return this.closedPromise;
@@ -2545,8 +2717,8 @@ var ClaudeSession = class {
2545
2717
  for (; ; ) {
2546
2718
  const buffered = pending.shift();
2547
2719
  if (buffered) return { value: buffered, done: false };
2720
+ if (terminalFailure) throw terminalFailure;
2548
2721
  if (turnSettled) {
2549
- if (terminalFailure) throw terminalFailure;
2550
2722
  return { value: void 0, done: true };
2551
2723
  }
2552
2724
  let raw;
@@ -2748,6 +2920,15 @@ function extractCodexUsageEvent(rawUsage) {
2748
2920
  function toNonNegativeInt2(value) {
2749
2921
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2750
2922
  }
2923
+ function requireToolCallId2(item) {
2924
+ if (typeof item.id === "string" && item.id.trim().length > 0) return item.id;
2925
+ throw new RuntimeExecutionFailure({
2926
+ phase: "run",
2927
+ category: "authority",
2928
+ retry: "non-retryable",
2929
+ reason: "codex tool item had no authoritative tool call id"
2930
+ });
2931
+ }
2751
2932
  function mapItem(rawItem, phase, workspaceDir) {
2752
2933
  if (!rawItem || typeof rawItem !== "object") return [];
2753
2934
  const item = rawItem;
@@ -2758,9 +2939,10 @@ function mapItem(rawItem, phase, workspaceDir) {
2758
2939
  return typeof item.text === "string" ? [{ type: "progress", text: item.text }] : [];
2759
2940
  }
2760
2941
  case "command_execution": {
2942
+ const toolCallId = requireToolCallId2(item);
2761
2943
  const command = typeof item.command === "string" ? item.command : void 0;
2762
2944
  if (phase === "started") {
2763
- return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command } }] : [];
2945
+ return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command }, toolCallId }] : [];
2764
2946
  }
2765
2947
  return [
2766
2948
  {
@@ -2771,17 +2953,19 @@ function mapItem(rawItem, phase, workspaceDir) {
2771
2953
  aggregatedOutput: item.aggregated_output,
2772
2954
  exitCode: item.exit_code,
2773
2955
  status: item.status
2774
- }
2956
+ },
2957
+ toolCallId
2775
2958
  }
2776
2959
  ];
2777
2960
  }
2778
2961
  case "file_change": {
2962
+ const toolCallId = requireToolCallId2(item);
2779
2963
  const changes = Array.isArray(item.changes) ? item.changes : [];
2780
2964
  if (phase === "started") {
2781
- return [{ type: "tool_use", tool: "file_change", input: { changes } }];
2965
+ return [{ type: "tool_use", tool: "file_change", input: { changes }, toolCallId }];
2782
2966
  }
2783
2967
  return [
2784
- { type: "tool_result", tool: "file_change", output: { changes, status: item.status } },
2968
+ { type: "tool_result", tool: "file_change", output: { changes, status: item.status }, toolCallId },
2785
2969
  ...extractArtifactEvents(changes, workspaceDir)
2786
2970
  ];
2787
2971
  }
@@ -2902,9 +3086,15 @@ var CodexProcessRunner = class {
2902
3086
  * cleanly resumable afterward via `codex exec resume` (no corruption from
2903
3087
  * killing mid-turn). `taskkill /T /F` on Windows, mirroring
2904
3088
  * `../pi/rpc-client.ts`'s own cross-platform convention.
3089
+ *
3090
+ * Fire-and-forget by design: an interrupt must not block on a terminator,
3091
+ * and `dispose()` is the settlement receipt. A request that could not be
3092
+ * spawned is left unrecorded, so `dispose()` re-issues it and raises the
3093
+ * typed `stage:'signal'` failure — swallowing it here loses nothing.
2905
3094
  */
2906
3095
  kill() {
2907
- requestOwnedProcessTreeTermination(this.processTreeOptions());
3096
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
3097
+ });
2908
3098
  }
2909
3099
  dispose() {
2910
3100
  if (!this.disposalAttempt) {
@@ -3198,7 +3388,15 @@ async function runCodexTurn(params) {
3198
3388
  }
3199
3389
  return;
3200
3390
  }
3201
- const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3391
+ let mapped;
3392
+ try {
3393
+ mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3394
+ } catch (cause) {
3395
+ if (!isRuntimeExecutionFailure(cause)) throw cause;
3396
+ params.terminal.failure = cause;
3397
+ params.queue.end();
3398
+ return;
3399
+ }
3202
3400
  for (const agentEvent of mapped) {
3203
3401
  if (agentEvent.type === "turn_end") turnEnded = true;
3204
3402
  params.queue.push(agentEvent);
@@ -4684,14 +4882,17 @@ function parseLooseEventsPollResponse(raw) {
4684
4882
  if (typeof raw !== "object" || raw === null) {
4685
4883
  throw new Error("events poll response is not an object");
4686
4884
  }
4687
- const { events, cursor } = raw;
4885
+ const { events, cursor, capabilities } = raw;
4688
4886
  if (!Array.isArray(events)) {
4689
4887
  throw new Error("events poll response.events is not an array");
4690
4888
  }
4691
4889
  if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
4692
4890
  throw new Error("events poll response.cursor is not an integer");
4693
4891
  }
4694
- return { events, cursor };
4892
+ if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
4893
+ throw new Error("events poll response.capabilities is not an array of strings");
4894
+ }
4895
+ return { events, cursor, capabilities: capabilities ?? [] };
4695
4896
  }
4696
4897
  function extractSkippableSeq(raw) {
4697
4898
  if (typeof raw !== "object" || raw === null) return void 0;
@@ -4771,12 +4972,14 @@ var LongPollClient = class {
4771
4972
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
4772
4973
  const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
4773
4974
  if (!res.ok) {
4975
+ this.opts.onServerCapabilities?.([]);
4774
4976
  this.opts.onOperationalOutcome?.("failure");
4775
4977
  const baseMs = this.opts.retryDelayMs ?? 2e3;
4776
4978
  await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
4777
4979
  continue;
4778
4980
  }
4779
4981
  const parsed = parseLooseEventsPollResponse(await res.json());
4982
+ this.opts.onServerCapabilities?.(parsed.capabilities);
4780
4983
  let hadValidationFailureThisBatch = false;
4781
4984
  for (const raw of parsed.events) {
4782
4985
  let envelope;
@@ -4820,6 +5023,7 @@ var LongPollClient = class {
4820
5023
  this.opts.onOperationalOutcome?.("success");
4821
5024
  }
4822
5025
  } catch (err) {
5026
+ this.opts.onServerCapabilities?.([]);
4823
5027
  if (err instanceof DeviceRevokedError) {
4824
5028
  this.running = false;
4825
5029
  this.opts.onRevoked?.();
@@ -5055,6 +5259,9 @@ var ConnectionManager = class {
5055
5259
  // re-attempted.
5056
5260
  getCursor: () => this.dedupWatermark(),
5057
5261
  onEnvelope: (envelope) => this.deliver(envelope),
5262
+ onServerCapabilities: (capabilities) => {
5263
+ if (this.mode === "long-poll") this.serverCapabilities = capabilities;
5264
+ },
5058
5265
  onRevoked: () => this.enterRevoked(),
5059
5266
  // M4 Phase 4 (version-negotiation drill fix): a batch entry
5060
5267
  // LongPollClient couldn't parse into a known Envelope at all (an
@@ -5186,25 +5393,24 @@ var ConnectionManager = class {
5186
5393
  */
5187
5394
  cancelPendingDrainRetry;
5188
5395
  /**
5189
- * The capabilities the CURRENTLY connected server advertised in its
5190
- * `conn.ack` — untyped `string[]` (forward-compat: a server may advertise
5191
- * a flag this build doesn't recognize yet), populated by {@link onAcked}
5192
- * and read by {@link getServerCapabilities}. Empty until the very first
5193
- * successful handshake.
5396
+ * The capabilities the CURRENT transport's server advertised untyped
5397
+ * `string[]` for forward compatibility. WS populates it from `conn.ack`;
5398
+ * long-poll populates it from each successful events response. Empty until
5399
+ * the active transport supplies an advertisement.
5194
5400
  *
5195
5401
  * Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
5196
5402
  * not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
5197
5403
  * connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
5198
5404
  * `acked` branch), `stop()`, or a transport switch to long-poll
5199
- * (`enterLongPoll`) — and only ever repopulated by a FRESH `conn.ack`.
5405
+ * (`enterLongPoll`) — and only repopulated by a fresh advertisement from
5406
+ * the transport that is still current.
5200
5407
  * The previous version of this doc comment claimed long-poll mode simply
5201
5408
  * "stays at whatever the last real WS `conn.ack` said" — that was the bug:
5202
5409
  * a daemon that once learned e.g. `approval_resolved` from an earlier WS
5203
5410
  * session kept believing it applied to whatever it's connected to NOW,
5204
5411
  * even after a disconnect/degrade where nothing has actually confirmed
5205
5412
  * that's still true (a reconnect could land on a DIFFERENT server behind a
5206
- * load balancer; long-poll fallback itself never performs an equivalent
5207
- * handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
5413
+ * load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
5208
5414
  * `task.approval_resolved` on this list — sending it to a server that
5209
5415
  * doesn't actually understand it over the long-poll path would get a
5210
5416
  * batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
@@ -5324,13 +5530,10 @@ var ConnectionManager = class {
5324
5530
  return this.mode === "long-poll";
5325
5531
  }
5326
5532
  /**
5327
- * The capabilities the CURRENTLY connected server advertised in its
5328
- * `conn.ack` e.g. lets a caller gate a daemon->server message on whether
5329
- * THIS server understands it before sending (see `task-runner.ts`'s
5330
- * `sendApprovalResolved`, gated on `approval_resolved`). Empty before the
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.
5533
+ * The capabilities the CURRENT transport's server advertised: from
5534
+ * `conn.ack` on WS, or the latest successful `GET /byok/events` response
5535
+ * on long-poll. Empty before either transport has supplied its current
5536
+ * advertisement, and cleared across disconnect/switch boundaries.
5334
5537
  */
5335
5538
  getServerCapabilities() {
5336
5539
  return this.serverCapabilities;
@@ -9734,9 +9937,9 @@ var TaskRunner = class {
9734
9937
  /**
9735
9938
  * Whether the CURRENTLY connected server advertised `result-document` —
9736
9939
  * read fresh on every call, never captured, because the answer changes
9737
- * across a reconnect (`ConnectionManager.getServerCapabilities` returns
9738
- * `[]` from the moment an acked connection closes until a fresh
9739
- * `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
9940
+ * across a reconnect or transport switch (`ConnectionManager` clears the
9941
+ * old advertisement at the boundary, then repopulates it from a fresh WS
9942
+ * ack or successful poll response). An absent `getServerCapabilities` seam is
9740
9943
  * "no capabilities", the fail-closed reading.
9741
9944
  */
9742
9945
  hasResultDocumentCapability() {