@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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execFile, spawn, spawnSync } from 'child_process';
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.19+ pi sidecar`,
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.19+ pi sidecar`
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
 
@@ -878,6 +878,24 @@ function mapPermissionPolicyToPiArgs(policy) {
878
878
  }
879
879
 
880
880
  // src/adapters/pi/events.ts
881
+ function requireToolCallId(msg) {
882
+ if (typeof msg.toolCallId === "string" && msg.toolCallId.trim().length > 0) return msg.toolCallId;
883
+ throw new RuntimeExecutionFailure({
884
+ phase: "run",
885
+ category: "authority",
886
+ retry: "non-retryable",
887
+ reason: `pi ${msg.type} frame had no authoritative tool call id`
888
+ });
889
+ }
890
+ function requireToolResultOutcome(msg) {
891
+ if (typeof msg.isError === "boolean") return msg.isError;
892
+ throw new RuntimeExecutionFailure({
893
+ phase: "run",
894
+ category: "authority",
895
+ retry: "non-retryable",
896
+ reason: "pi tool_execution_end frame had no authoritative isError outcome"
897
+ });
898
+ }
881
899
  function mapPiMessageToAgentEvent(msg) {
882
900
  switch (msg.type) {
883
901
  case "message_update": {
@@ -888,16 +906,22 @@ function mapPiMessageToAgentEvent(msg) {
888
906
  return void 0;
889
907
  }
890
908
  case "tool_execution_start": {
909
+ const toolCallId = requireToolCallId(msg);
891
910
  if (typeof msg.toolName !== "string") return void 0;
892
- return { type: "tool_use", tool: msg.toolName, input: msg.args };
911
+ return { type: "tool_use", tool: msg.toolName, input: msg.args, toolCallId };
893
912
  }
894
913
  case "tool_execution_end": {
914
+ const toolCallId = requireToolCallId(msg);
915
+ const isError = requireToolResultOutcome(msg);
895
916
  if (typeof msg.toolName !== "string") return void 0;
896
- return {
917
+ const event = {
897
918
  type: "tool_result",
898
919
  tool: msg.toolName,
899
- output: { result: msg.result, isError: msg.isError === true }
920
+ output: { result: msg.result },
921
+ toolCallId,
922
+ isError
900
923
  };
924
+ return event;
901
925
  }
902
926
  case "agent_settled":
903
927
  return { type: "turn_end" };
@@ -1044,11 +1068,54 @@ var AsyncQueue = class {
1044
1068
  };
1045
1069
  }
1046
1070
  };
1071
+
1072
+ // src/adapters/taskkill-pid-set.ts
1073
+ var INTEGER_PATTERN = /\d+/g;
1074
+ function isCandidatePid(value) {
1075
+ return Number.isSafeInteger(value) && value > 0;
1076
+ }
1077
+ function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
1078
+ const excluded = new Set(excludedPids);
1079
+ const accepted = /* @__PURE__ */ new Set();
1080
+ if (isCandidatePid(rootPid)) accepted.add(rootPid);
1081
+ const lines = text.split(/\r?\n/).map((line) => {
1082
+ const pids = [];
1083
+ for (const match of line.matchAll(INTEGER_PATTERN)) {
1084
+ const pid = Number(match[0]);
1085
+ if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
1086
+ }
1087
+ return pids;
1088
+ });
1089
+ let changed = true;
1090
+ while (changed) {
1091
+ changed = false;
1092
+ for (const pids of lines) {
1093
+ if (!pids.some((pid) => accepted.has(pid))) continue;
1094
+ for (const pid of pids) {
1095
+ if (accepted.has(pid)) continue;
1096
+ accepted.add(pid);
1097
+ changed = true;
1098
+ }
1099
+ }
1100
+ }
1101
+ return accepted;
1102
+ }
1103
+
1104
+ // src/adapters/process-tree.ts
1047
1105
  var DEFAULT_TERM_GRACE_MS = 750;
1048
1106
  var DEFAULT_KILL_GRACE_MS = 2e3;
1049
1107
  var POLL_MS = 20;
1050
- var terminationRequested = /* @__PURE__ */ new WeakSet();
1051
- var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
1108
+ var terminationState = /* @__PURE__ */ new WeakMap();
1109
+ function stateFor(child) {
1110
+ const existing = terminationState.get(child);
1111
+ if (existing) return existing;
1112
+ const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
1113
+ terminationState.set(child, created);
1114
+ return created;
1115
+ }
1116
+ function defaultKill(pid, signal) {
1117
+ process.kill(pid, signal);
1118
+ }
1052
1119
  function withOwnedProcessTree(options) {
1053
1120
  return {
1054
1121
  ...options,
@@ -1066,9 +1133,9 @@ function positivePid(child, label) {
1066
1133
  }
1067
1134
  return pid;
1068
1135
  }
1069
- function groupExists(pid, label) {
1136
+ function groupExists(pid, label, kill) {
1070
1137
  try {
1071
- process.kill(-pid, 0);
1138
+ kill(-pid, 0);
1072
1139
  return true;
1073
1140
  } catch (cause) {
1074
1141
  const code = cause.code;
@@ -1080,9 +1147,23 @@ function groupExists(pid, label) {
1080
1147
  }, { cause });
1081
1148
  }
1082
1149
  }
1083
- function signalGroup(pid, signal, label) {
1150
+ function processExists(pid, label, kill) {
1084
1151
  try {
1085
- process.kill(-pid, signal);
1152
+ kill(pid, 0);
1153
+ return true;
1154
+ } catch (cause) {
1155
+ const code = cause.code;
1156
+ if (code === "ESRCH") return false;
1157
+ if (code === "EPERM") return true;
1158
+ throw new RuntimeDisposalFailure({
1159
+ stage: "quiescence",
1160
+ reason: `${label} runtime process ${pid} state could not be verified`
1161
+ }, { cause });
1162
+ }
1163
+ }
1164
+ function signalGroup(pid, signal, label, kill) {
1165
+ try {
1166
+ kill(-pid, signal);
1086
1167
  } catch (cause) {
1087
1168
  const code = cause.code;
1088
1169
  if (code === "ESRCH" || code === "EPERM") return;
@@ -1092,6 +1173,39 @@ function signalGroup(pid, signal, label) {
1092
1173
  }, { cause });
1093
1174
  }
1094
1175
  }
1176
+ async function runTaskkill(pid, options) {
1177
+ const spawnFn = options.spawnFn ?? spawn;
1178
+ return new Promise((resolve, reject) => {
1179
+ const signalFailure = (cause) => {
1180
+ reject(new RuntimeDisposalFailure({
1181
+ stage: "signal",
1182
+ reason: `${options.label} runtime process tree termination could not be requested`
1183
+ }, { cause }));
1184
+ };
1185
+ let taskkill;
1186
+ try {
1187
+ taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
1188
+ windowsHide: true,
1189
+ stdio: ["ignore", "pipe", "pipe"]
1190
+ });
1191
+ } catch (cause) {
1192
+ signalFailure(cause);
1193
+ return;
1194
+ }
1195
+ const chunks = [];
1196
+ taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
1197
+ taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
1198
+ taskkill.once("error", signalFailure);
1199
+ taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
1200
+ });
1201
+ }
1202
+ function liveAcceptedPids(accepted, label, kill) {
1203
+ const live = [];
1204
+ for (const pid of accepted) {
1205
+ if (processExists(pid, label, kill)) live.push(pid);
1206
+ }
1207
+ return live;
1208
+ }
1095
1209
  async function waitUntil(predicate, timeoutMs) {
1096
1210
  const deadline = Date.now() + timeoutMs;
1097
1211
  while (predicate()) {
@@ -1129,29 +1243,27 @@ async function waitWithDeadline(promise, timeoutMs) {
1129
1243
  );
1130
1244
  });
1131
1245
  }
1132
- function requestOwnedProcessTreeTermination(options) {
1133
- if (options.isClosed()) return;
1246
+ async function requestOwnedProcessTreeTermination(options) {
1247
+ const platform = options.platform ?? process.platform;
1248
+ if (platform !== "win32" && options.isClosed()) return;
1134
1249
  const pid = positivePid(options.child, options.label);
1135
1250
  if (pid === void 0) return;
1136
- if (process.platform === "win32") {
1137
- const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
1138
- if (result.error) {
1139
- throw new RuntimeDisposalFailure({
1140
- stage: "signal",
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);
1251
+ if (platform === "win32") {
1252
+ const output = await runTaskkill(pid, options);
1253
+ const state = stateFor(options.child);
1254
+ for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
1255
+ state.requested = true;
1146
1256
  return;
1147
1257
  }
1148
- signalGroup(pid, "SIGTERM", options.label);
1149
- terminationRequested.add(options.child);
1258
+ signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
1259
+ stateFor(options.child).requested = true;
1150
1260
  }
1151
1261
  async function disposeOwnedProcessTree(options) {
1152
1262
  const pid = positivePid(options.child, options.label);
1153
1263
  const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
1154
1264
  const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
1265
+ const platform = options.platform ?? process.platform;
1266
+ const kill = options.killFn ?? defaultKill;
1155
1267
  if (pid === void 0) {
1156
1268
  if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
1157
1269
  throw new RuntimeDisposalFailure({
@@ -1159,27 +1271,50 @@ async function disposeOwnedProcessTree(options) {
1159
1271
  reason: `${options.label} runtime process did not settle after spawn failure`
1160
1272
  });
1161
1273
  }
1162
- if (process.platform === "win32") {
1163
- if (!options.isClosed() && !terminationRequested.has(options.child)) requestOwnedProcessTreeTermination(options);
1164
- if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
1165
- if (terminationRequestFailed.has(options.child)) {
1274
+ if (platform === "win32") {
1275
+ if (!terminationState.get(options.child)?.requested) {
1276
+ await requestOwnedProcessTreeTermination(options);
1277
+ }
1278
+ const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
1279
+ const deadline = Date.now() + killGraceMs;
1280
+ const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
1281
+ let reswept = false;
1282
+ let live = liveAcceptedPids(accepted, options.label, kill);
1283
+ while (live.length > 0) {
1284
+ if (Date.now() >= deadline) {
1285
+ throw new RuntimeDisposalFailure({
1286
+ stage: "quiescence",
1287
+ reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
1288
+ });
1289
+ }
1290
+ if (!reswept && Date.now() >= resweepAt) {
1291
+ reswept = true;
1292
+ for (const livePid of live) {
1293
+ for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
1294
+ accepted.add(walked);
1295
+ }
1296
+ }
1297
+ }
1298
+ await new Promise((resolve) => {
1299
+ setTimeout(resolve, POLL_MS);
1300
+ });
1301
+ live = liveAcceptedPids(accepted, options.label, kill);
1302
+ }
1303
+ if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
1166
1304
  throw new RuntimeDisposalFailure({
1167
- stage: "signal",
1168
- reason: `${options.label} runtime process tree could not be terminated`
1305
+ stage: "quiescence",
1306
+ reason: `${options.label} runtime root did not emit close after its process tree quiesced`
1169
1307
  });
1170
1308
  }
1171
- throw new RuntimeDisposalFailure({
1172
- stage: "quiescence",
1173
- reason: `${options.label} runtime process tree did not close before the disposal deadline`
1174
- });
1309
+ return;
1175
1310
  }
1176
- if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
1177
- signalGroup(pid, "SIGTERM", options.label);
1178
- terminationRequested.add(options.child);
1311
+ if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
1312
+ signalGroup(pid, "SIGTERM", options.label, kill);
1313
+ stateFor(options.child).requested = true;
1179
1314
  }
1180
- if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
1181
- signalGroup(pid, "SIGKILL", options.label);
1182
- if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
1315
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
1316
+ signalGroup(pid, "SIGKILL", options.label, kill);
1317
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
1183
1318
  throw new RuntimeDisposalFailure({
1184
1319
  stage: "quiescence",
1185
1320
  reason: `${options.label} runtime process group remained live after SIGKILL`
@@ -1277,9 +1412,16 @@ var PiRpcClient = class {
1277
1412
  );
1278
1413
  }
1279
1414
  }
1280
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
1415
+ /**
1416
+ * Immediate process-tree termination request. `dispose()` is the settlement
1417
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
1418
+ * terminator. A request that could not be spawned is left unrecorded, so
1419
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
1420
+ * swallowing it here loses nothing.
1421
+ */
1281
1422
  kill() {
1282
- requestOwnedProcessTreeTermination(this.processTreeOptions());
1423
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
1424
+ });
1283
1425
  }
1284
1426
  waitClosed() {
1285
1427
  return this.closedPromise;
@@ -1791,6 +1933,17 @@ function subtractDenied(tools, denyTools) {
1791
1933
  function createToolUseCorrelation() {
1792
1934
  return { toolNameByUseId: /* @__PURE__ */ new Map() };
1793
1935
  }
1936
+ function missingToolCallIdFailure(frame) {
1937
+ return new RuntimeExecutionFailure({
1938
+ phase: "run",
1939
+ category: "authority",
1940
+ retry: "non-retryable",
1941
+ reason: `claude ${frame} frame had no authoritative tool call id`
1942
+ });
1943
+ }
1944
+ function isAuthoritativeToolCallId(value) {
1945
+ return typeof value === "string" && value.trim().length > 0;
1946
+ }
1794
1947
  var ROUTINE_CLAUDE_SYSTEM_SUBTYPES = /* @__PURE__ */ new Set([
1795
1948
  "init",
1796
1949
  "hook_started",
@@ -1834,9 +1987,12 @@ function mapAssistant(msg, correlation) {
1834
1987
  }
1835
1988
  break;
1836
1989
  case "tool_use":
1837
- if (typeof block.id === "string" && typeof block.name === "string") {
1990
+ if (!isAuthoritativeToolCallId(block.id)) {
1991
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_use") };
1992
+ }
1993
+ if (typeof block.name === "string") {
1838
1994
  correlation.toolNameByUseId.set(block.id, block.name);
1839
- events.push({ type: "tool_use", tool: block.name, input: block.input });
1995
+ events.push({ type: "tool_use", tool: block.name, input: block.input, toolCallId: block.id });
1840
1996
  }
1841
1997
  break;
1842
1998
  // Deliberately NOT mapped to `progress` — mirrors pi's own choice to
@@ -1872,11 +2028,20 @@ function mapUser(msg, correlation, options) {
1872
2028
  unmappedLabel = unmappedLabel ?? `user-block:${String(block.type)}`;
1873
2029
  continue;
1874
2030
  }
1875
- const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : void 0;
1876
- const tool = toolUseId && correlation.toolNameByUseId.get(toolUseId) || "unknown";
1877
- const isError = block.is_error === true;
1878
- events.push({ type: "tool_result", tool, output: { content: block.content, isError } });
1879
- if (!isError && FILE_WRITING_TOOLS.has(tool)) {
2031
+ if (!isAuthoritativeToolCallId(block.tool_use_id)) {
2032
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_result") };
2033
+ }
2034
+ const tool = correlation.toolNameByUseId.get(block.tool_use_id) ?? "unknown";
2035
+ const isError = typeof block.is_error === "boolean" ? block.is_error : void 0;
2036
+ const event = {
2037
+ type: "tool_result",
2038
+ tool,
2039
+ output: { content: block.content },
2040
+ toolCallId: block.tool_use_id
2041
+ };
2042
+ if (isError !== void 0) event.isError = isError;
2043
+ events.push(event);
2044
+ if (isError === false && FILE_WRITING_TOOLS.has(tool)) {
1880
2045
  const artifact = tryBuildArtifactEvent(msg, options.workspaceDir);
1881
2046
  if (artifact) events.push(artifact);
1882
2047
  }
@@ -2082,9 +2247,16 @@ var ClaudeProcessClient = class {
2082
2247
  );
2083
2248
  }
2084
2249
  }
2085
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
2250
+ /**
2251
+ * Immediate process-tree termination request. `dispose()` is the settlement
2252
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
2253
+ * terminator. A request that could not be spawned is left unrecorded, so
2254
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
2255
+ * swallowing it here loses nothing.
2256
+ */
2086
2257
  kill() {
2087
- requestOwnedProcessTreeTermination(this.processTreeOptions());
2258
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
2259
+ });
2088
2260
  }
2089
2261
  waitClosed() {
2090
2262
  return this.closedPromise;
@@ -2492,8 +2664,8 @@ var ClaudeSession = class {
2492
2664
  for (; ; ) {
2493
2665
  const buffered = pending.shift();
2494
2666
  if (buffered) return { value: buffered, done: false };
2667
+ if (terminalFailure) throw terminalFailure;
2495
2668
  if (turnSettled) {
2496
- if (terminalFailure) throw terminalFailure;
2497
2669
  return { value: void 0, done: true };
2498
2670
  }
2499
2671
  let raw;
@@ -2695,6 +2867,15 @@ function extractCodexUsageEvent(rawUsage) {
2695
2867
  function toNonNegativeInt2(value) {
2696
2868
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2697
2869
  }
2870
+ function requireToolCallId2(item) {
2871
+ if (typeof item.id === "string" && item.id.trim().length > 0) return item.id;
2872
+ throw new RuntimeExecutionFailure({
2873
+ phase: "run",
2874
+ category: "authority",
2875
+ retry: "non-retryable",
2876
+ reason: "codex tool item had no authoritative tool call id"
2877
+ });
2878
+ }
2698
2879
  function mapItem(rawItem, phase, workspaceDir) {
2699
2880
  if (!rawItem || typeof rawItem !== "object") return [];
2700
2881
  const item = rawItem;
@@ -2705,9 +2886,10 @@ function mapItem(rawItem, phase, workspaceDir) {
2705
2886
  return typeof item.text === "string" ? [{ type: "progress", text: item.text }] : [];
2706
2887
  }
2707
2888
  case "command_execution": {
2889
+ const toolCallId = requireToolCallId2(item);
2708
2890
  const command = typeof item.command === "string" ? item.command : void 0;
2709
2891
  if (phase === "started") {
2710
- return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command } }] : [];
2892
+ return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command }, toolCallId }] : [];
2711
2893
  }
2712
2894
  return [
2713
2895
  {
@@ -2718,17 +2900,19 @@ function mapItem(rawItem, phase, workspaceDir) {
2718
2900
  aggregatedOutput: item.aggregated_output,
2719
2901
  exitCode: item.exit_code,
2720
2902
  status: item.status
2721
- }
2903
+ },
2904
+ toolCallId
2722
2905
  }
2723
2906
  ];
2724
2907
  }
2725
2908
  case "file_change": {
2909
+ const toolCallId = requireToolCallId2(item);
2726
2910
  const changes = Array.isArray(item.changes) ? item.changes : [];
2727
2911
  if (phase === "started") {
2728
- return [{ type: "tool_use", tool: "file_change", input: { changes } }];
2912
+ return [{ type: "tool_use", tool: "file_change", input: { changes }, toolCallId }];
2729
2913
  }
2730
2914
  return [
2731
- { type: "tool_result", tool: "file_change", output: { changes, status: item.status } },
2915
+ { type: "tool_result", tool: "file_change", output: { changes, status: item.status }, toolCallId },
2732
2916
  ...extractArtifactEvents(changes, workspaceDir)
2733
2917
  ];
2734
2918
  }
@@ -2849,9 +3033,15 @@ var CodexProcessRunner = class {
2849
3033
  * cleanly resumable afterward via `codex exec resume` (no corruption from
2850
3034
  * killing mid-turn). `taskkill /T /F` on Windows, mirroring
2851
3035
  * `../pi/rpc-client.ts`'s own cross-platform convention.
3036
+ *
3037
+ * Fire-and-forget by design: an interrupt must not block on a terminator,
3038
+ * and `dispose()` is the settlement receipt. A request that could not be
3039
+ * spawned is left unrecorded, so `dispose()` re-issues it and raises the
3040
+ * typed `stage:'signal'` failure — swallowing it here loses nothing.
2852
3041
  */
2853
3042
  kill() {
2854
- requestOwnedProcessTreeTermination(this.processTreeOptions());
3043
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
3044
+ });
2855
3045
  }
2856
3046
  dispose() {
2857
3047
  if (!this.disposalAttempt) {
@@ -3145,7 +3335,15 @@ async function runCodexTurn(params) {
3145
3335
  }
3146
3336
  return;
3147
3337
  }
3148
- const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3338
+ let mapped;
3339
+ try {
3340
+ mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3341
+ } catch (cause) {
3342
+ if (!isRuntimeExecutionFailure(cause)) throw cause;
3343
+ params.terminal.failure = cause;
3344
+ params.queue.end();
3345
+ return;
3346
+ }
3149
3347
  for (const agentEvent of mapped) {
3150
3348
  if (agentEvent.type === "turn_end") turnEnded = true;
3151
3349
  params.queue.push(agentEvent);
@@ -4631,14 +4829,17 @@ function parseLooseEventsPollResponse(raw) {
4631
4829
  if (typeof raw !== "object" || raw === null) {
4632
4830
  throw new Error("events poll response is not an object");
4633
4831
  }
4634
- const { events, cursor } = raw;
4832
+ const { events, cursor, capabilities } = raw;
4635
4833
  if (!Array.isArray(events)) {
4636
4834
  throw new Error("events poll response.events is not an array");
4637
4835
  }
4638
4836
  if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
4639
4837
  throw new Error("events poll response.cursor is not an integer");
4640
4838
  }
4641
- return { events, cursor };
4839
+ if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
4840
+ throw new Error("events poll response.capabilities is not an array of strings");
4841
+ }
4842
+ return { events, cursor, capabilities: capabilities ?? [] };
4642
4843
  }
4643
4844
  function extractSkippableSeq(raw) {
4644
4845
  if (typeof raw !== "object" || raw === null) return void 0;
@@ -4718,12 +4919,14 @@ var LongPollClient = class {
4718
4919
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
4719
4920
  const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
4720
4921
  if (!res.ok) {
4922
+ this.opts.onServerCapabilities?.([]);
4721
4923
  this.opts.onOperationalOutcome?.("failure");
4722
4924
  const baseMs = this.opts.retryDelayMs ?? 2e3;
4723
4925
  await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
4724
4926
  continue;
4725
4927
  }
4726
4928
  const parsed = parseLooseEventsPollResponse(await res.json());
4929
+ this.opts.onServerCapabilities?.(parsed.capabilities);
4727
4930
  let hadValidationFailureThisBatch = false;
4728
4931
  for (const raw of parsed.events) {
4729
4932
  let envelope;
@@ -4767,6 +4970,7 @@ var LongPollClient = class {
4767
4970
  this.opts.onOperationalOutcome?.("success");
4768
4971
  }
4769
4972
  } catch (err) {
4973
+ this.opts.onServerCapabilities?.([]);
4770
4974
  if (err instanceof DeviceRevokedError) {
4771
4975
  this.running = false;
4772
4976
  this.opts.onRevoked?.();
@@ -5002,6 +5206,9 @@ var ConnectionManager = class {
5002
5206
  // re-attempted.
5003
5207
  getCursor: () => this.dedupWatermark(),
5004
5208
  onEnvelope: (envelope) => this.deliver(envelope),
5209
+ onServerCapabilities: (capabilities) => {
5210
+ if (this.mode === "long-poll") this.serverCapabilities = capabilities;
5211
+ },
5005
5212
  onRevoked: () => this.enterRevoked(),
5006
5213
  // M4 Phase 4 (version-negotiation drill fix): a batch entry
5007
5214
  // LongPollClient couldn't parse into a known Envelope at all (an
@@ -5133,25 +5340,24 @@ var ConnectionManager = class {
5133
5340
  */
5134
5341
  cancelPendingDrainRetry;
5135
5342
  /**
5136
- * The capabilities the CURRENTLY connected server advertised in its
5137
- * `conn.ack` — untyped `string[]` (forward-compat: a server may advertise
5138
- * a flag this build doesn't recognize yet), populated by {@link onAcked}
5139
- * and read by {@link getServerCapabilities}. Empty until the very first
5140
- * successful handshake.
5343
+ * The capabilities the CURRENT transport's server advertised untyped
5344
+ * `string[]` for forward compatibility. WS populates it from `conn.ack`;
5345
+ * long-poll populates it from each successful events response. Empty until
5346
+ * the active transport supplies an advertisement.
5141
5347
  *
5142
5348
  * Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
5143
5349
  * not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
5144
5350
  * connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
5145
5351
  * `acked` branch), `stop()`, or a transport switch to long-poll
5146
- * (`enterLongPoll`) — and only ever repopulated by a FRESH `conn.ack`.
5352
+ * (`enterLongPoll`) — and only repopulated by a fresh advertisement from
5353
+ * the transport that is still current.
5147
5354
  * The previous version of this doc comment claimed long-poll mode simply
5148
5355
  * "stays at whatever the last real WS `conn.ack` said" — that was the bug:
5149
5356
  * a daemon that once learned e.g. `approval_resolved` from an earlier WS
5150
5357
  * session kept believing it applied to whatever it's connected to NOW,
5151
5358
  * even after a disconnect/degrade where nothing has actually confirmed
5152
5359
  * that's still true (a reconnect could land on a DIFFERENT server behind a
5153
- * load balancer; long-poll fallback itself never performs an equivalent
5154
- * handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
5360
+ * load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
5155
5361
  * `task.approval_resolved` on this list — sending it to a server that
5156
5362
  * doesn't actually understand it over the long-poll path would get a
5157
5363
  * batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
@@ -5271,13 +5477,10 @@ var ConnectionManager = class {
5271
5477
  return this.mode === "long-poll";
5272
5478
  }
5273
5479
  /**
5274
- * The capabilities the CURRENTLY connected server advertised in its
5275
- * `conn.ack` e.g. lets a caller gate a daemon->server message on whether
5276
- * THIS server understands it before sending (see `task-runner.ts`'s
5277
- * `sendApprovalResolved`, gated on `approval_resolved`). Empty before the
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.
5480
+ * The capabilities the CURRENT transport's server advertised: from
5481
+ * `conn.ack` on WS, or the latest successful `GET /byok/events` response
5482
+ * on long-poll. Empty before either transport has supplied its current
5483
+ * advertisement, and cleared across disconnect/switch boundaries.
5281
5484
  */
5282
5485
  getServerCapabilities() {
5283
5486
  return this.serverCapabilities;
@@ -9647,9 +9850,9 @@ var TaskRunner = class {
9647
9850
  /**
9648
9851
  * Whether the CURRENTLY connected server advertised `result-document` —
9649
9852
  * read fresh on every call, never captured, because the answer changes
9650
- * across a reconnect (`ConnectionManager.getServerCapabilities` returns
9651
- * `[]` from the moment an acked connection closes until a fresh
9652
- * `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
9853
+ * across a reconnect or transport switch (`ConnectionManager` clears the
9854
+ * old advertisement at the boundary, then repopulates it from a fresh WS
9855
+ * ack or successful poll response). An absent `getServerCapabilities` seam is
9653
9856
  * "no capabilities", the fail-closed reading.
9654
9857
  */
9655
9858
  hasResultDocumentCapability() {