@byok-sdk/client 0.3.0 → 0.4.0

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.
Files changed (38) hide show
  1. package/README.md +13 -0
  2. package/dist/adapters/claude/claude-adapter.d.ts +4 -20
  3. package/dist/adapters/claude/events.d.ts +3 -0
  4. package/dist/adapters/claude/process-client.d.ts +9 -1
  5. package/dist/adapters/codex/codex-adapter.d.ts +4 -16
  6. package/dist/adapters/codex/process-runner.d.ts +4 -1
  7. package/dist/adapters/index.d.ts +3 -1
  8. package/dist/adapters/index.js +923 -258
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/pi/pi-adapter.d.ts +3 -16
  11. package/dist/adapters/pi/rpc-client.d.ts +9 -1
  12. package/dist/adapters/process-tree.d.ts +19 -0
  13. package/dist/bin/audit-log.d.ts +12 -0
  14. package/dist/bin/byok-agent.js +1293 -484
  15. package/dist/bin/byok-agent.js.map +1 -1
  16. package/dist/bin/byok-approval-mcp.js.map +1 -1
  17. package/dist/bin/commands/workspaces.d.ts +11 -0
  18. package/dist/bin/format.d.ts +13 -0
  19. package/dist/bin/runtime-probe.d.ts +1 -1
  20. package/dist/bin/tasks-view.d.ts +13 -0
  21. package/dist/daemon/approvals.d.ts +2 -2
  22. package/dist/daemon/connection-manager.d.ts +4 -2
  23. package/dist/daemon/control-server.d.ts +18 -1
  24. package/dist/daemon/create-daemon.d.ts +2 -2
  25. package/dist/daemon/daemon-owner.d.ts +4 -2
  26. package/dist/daemon/environment.d.ts +9 -9
  27. package/dist/daemon/git-workspace.d.ts +21 -0
  28. package/dist/daemon/observer.d.ts +13 -0
  29. package/dist/daemon/presence-publisher.d.ts +29 -0
  30. package/dist/daemon/runtime-capabilities.d.ts +1 -1
  31. package/dist/daemon/task-runner.d.ts +27 -34
  32. package/dist/daemon/ws-transport.d.ts +3 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +1254 -432
  35. package/dist/index.js.map +1 -1
  36. package/dist/runtime-failure.d.ts +64 -0
  37. package/dist/types.d.ts +100 -73
  38. package/package.json +4 -4
@@ -5,7 +5,7 @@ import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, con
5
5
  import path20, { isAbsolute, join } from 'path';
6
6
  import os from 'os';
7
7
  import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
8
- import { TASK_STATES, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
8
+ import { TASK_STATES, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
9
9
  import { promisify } from 'util';
10
10
  import { fileURLToPath } from 'url';
11
11
  import 'readline';
@@ -15,6 +15,52 @@ import { createRequire } from 'module';
15
15
  import { createInterface } from 'readline/promises';
16
16
 
17
17
  // src/types.ts
18
+ function frozenStrings(values) {
19
+ return values === void 0 ? void 0 : Object.freeze([...values]);
20
+ }
21
+ function frozenPolicy(policy) {
22
+ const allowTools = policy.allowTools === void 0 ? void 0 : Object.freeze([...policy.allowTools]);
23
+ const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
24
+ return Object.freeze({
25
+ mode: policy.mode,
26
+ ...allowTools === void 0 ? {} : { allowTools },
27
+ ...denyTools === void 0 ? {} : { denyTools },
28
+ ...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
29
+ ...policy.network === void 0 ? {} : { network: policy.network }
30
+ });
31
+ }
32
+ function freezeRuntimeAdapterDescriptor(descriptor) {
33
+ const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
34
+ const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
35
+ return Object.freeze({
36
+ id: descriptor.id,
37
+ supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
38
+ capabilities: Object.freeze({
39
+ steer: descriptor.capabilities.steer === true,
40
+ resume: descriptor.capabilities.resume === true,
41
+ approvalInteractive: descriptor.capabilities.approvalInteractive === true,
42
+ ...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
43
+ permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
44
+ }),
45
+ environmentRequirements: Object.freeze({
46
+ ...baseNames === void 0 ? {} : { baseNames },
47
+ ...credentialNames === void 0 ? {} : { credentialNames }
48
+ })
49
+ });
50
+ }
51
+ function sealRuntimeOperationManifest(manifest) {
52
+ return Object.freeze({
53
+ taskId: manifest.taskId,
54
+ runtimeId: manifest.runtimeId,
55
+ descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
56
+ policy: frozenPolicy(manifest.policy),
57
+ requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
58
+ ...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
59
+ ...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
60
+ workspace: Object.freeze({ ...manifest.workspace }),
61
+ forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
62
+ });
63
+ }
18
64
  var PolicyUnsupportedError = class extends Error {
19
65
  constructor(message) {
20
66
  super(message);
@@ -22,7 +68,7 @@ var PolicyUnsupportedError = class extends Error {
22
68
  }
23
69
  };
24
70
  var SteerUnsupportedError = class extends Error {
25
- /** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
71
+ /** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
26
72
  runtimeId;
27
73
  constructor(runtimeId, message) {
28
74
  super(message);
@@ -30,6 +76,112 @@ var SteerUnsupportedError = class extends Error {
30
76
  this.runtimeId = runtimeId;
31
77
  }
32
78
  };
79
+
80
+ // src/runtime-failure.ts
81
+ var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
82
+ var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
83
+ var RuntimeDisposalFailure = class extends Error {
84
+ stage;
85
+ constructor(input, options) {
86
+ if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
87
+ throw new TypeError("invalid RuntimeDisposalFailure input");
88
+ }
89
+ super(input.reason, options);
90
+ this.name = "RuntimeDisposalFailure";
91
+ this.stage = input.stage;
92
+ Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
93
+ Object.freeze(this);
94
+ }
95
+ };
96
+ function isRuntimeDisposalStage(value) {
97
+ return value === "signal" || value === "quiescence" || value === "cleanup";
98
+ }
99
+ function isRuntimeDisposalFailure(value) {
100
+ if (typeof value !== "object" || value === null) return false;
101
+ const candidate = value;
102
+ return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
103
+ }
104
+ var RuntimeExecutionFailure = class extends Error {
105
+ phase;
106
+ category;
107
+ retry;
108
+ constructor(input, options) {
109
+ if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
110
+ throw new TypeError("invalid RuntimeExecutionFailure input");
111
+ }
112
+ super(input.reason, options);
113
+ this.name = "RuntimeExecutionFailure";
114
+ this.phase = input.phase;
115
+ this.category = input.category;
116
+ this.retry = input.retry;
117
+ Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
118
+ Object.freeze(this);
119
+ }
120
+ };
121
+ function isRuntimeFailurePhase(value) {
122
+ return value === "start" || value === "run";
123
+ }
124
+ function isRuntimeFailureCategory(value) {
125
+ return value === "semantic" || value === "infrastructure" || value === "authority";
126
+ }
127
+ function isRuntimeRetryDisposition(value) {
128
+ return value === "retryable" || value === "non-retryable";
129
+ }
130
+ function isRuntimeExecutionFailure(value) {
131
+ if (typeof value !== "object" || value === null) return false;
132
+ const candidate = value;
133
+ return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
134
+ }
135
+ function retryableFromDisposition(disposition) {
136
+ switch (disposition) {
137
+ case "retryable":
138
+ return true;
139
+ case "non-retryable":
140
+ return false;
141
+ }
142
+ }
143
+ function projectRuntimeExecutionFailure(failure) {
144
+ return {
145
+ reason: failure.message,
146
+ retryable: retryableFromDisposition(failure.retry)
147
+ };
148
+ }
149
+ var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
150
+ start: "runtime adapter contract violation during start",
151
+ run: "runtime adapter contract violation during run"
152
+ });
153
+ function projectRuntimeBoundaryFailure(value, expectedPhase) {
154
+ if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
155
+ return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
156
+ }
157
+ return {
158
+ reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
159
+ retryable: false,
160
+ contractViolation: true
161
+ };
162
+ }
163
+ var GIT_ERROR_CATEGORIES = [
164
+ "git-unavailable",
165
+ "git-timeout",
166
+ "git-output-limit",
167
+ "git-command-failed",
168
+ "workspace-root-invalid",
169
+ "workspace-root-conflict",
170
+ "workspace-not-owned",
171
+ "repository-root-mismatch",
172
+ "repository-invalid",
173
+ "lease-busy",
174
+ "ledger-invalid"
175
+ ];
176
+ var GIT_WORKSPACE_PHASES = [
177
+ "preparing",
178
+ "active",
179
+ "completed",
180
+ "failed",
181
+ "cancelled",
182
+ "interrupted",
183
+ "salvage"
184
+ ];
33
185
  var GitWorkspaceError = class extends Error {
34
186
  constructor(category, message = category) {
35
187
  super(message);
@@ -945,6 +1097,155 @@ var AsyncQueue = class {
945
1097
  };
946
1098
  }
947
1099
  };
1100
+ var DEFAULT_TERM_GRACE_MS = 750;
1101
+ var DEFAULT_KILL_GRACE_MS = 2e3;
1102
+ var POLL_MS = 20;
1103
+ var terminationRequested = /* @__PURE__ */ new WeakSet();
1104
+ var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
1105
+ function withOwnedProcessTree(options) {
1106
+ return {
1107
+ ...options,
1108
+ ...process.platform === "win32" ? { windowsHide: true } : { detached: true }
1109
+ };
1110
+ }
1111
+ function positivePid(child, label) {
1112
+ const pid = child.pid;
1113
+ if (pid === void 0) return void 0;
1114
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
1115
+ throw new RuntimeDisposalFailure({
1116
+ stage: "signal",
1117
+ reason: `${label} runtime process has an unsafe owned pid`
1118
+ });
1119
+ }
1120
+ return pid;
1121
+ }
1122
+ function groupExists(pid, label) {
1123
+ try {
1124
+ process.kill(-pid, 0);
1125
+ return true;
1126
+ } catch (cause) {
1127
+ const code = cause.code;
1128
+ if (code === "ESRCH") return false;
1129
+ if (code === "EPERM") return true;
1130
+ throw new RuntimeDisposalFailure({
1131
+ stage: "quiescence",
1132
+ reason: `${label} runtime process-group state could not be verified`
1133
+ }, { cause });
1134
+ }
1135
+ }
1136
+ function signalGroup(pid, signal, label) {
1137
+ try {
1138
+ process.kill(-pid, signal);
1139
+ } catch (cause) {
1140
+ const code = cause.code;
1141
+ if (code === "ESRCH" || code === "EPERM") return;
1142
+ throw new RuntimeDisposalFailure({
1143
+ stage: "signal",
1144
+ reason: `${label} runtime process group ${pid} could not receive ${signal} (${code ?? "unknown"})`
1145
+ }, { cause });
1146
+ }
1147
+ }
1148
+ async function waitUntil(predicate, timeoutMs) {
1149
+ const deadline = Date.now() + timeoutMs;
1150
+ while (predicate()) {
1151
+ if (Date.now() >= deadline) return false;
1152
+ await new Promise((resolve) => {
1153
+ setTimeout(resolve, POLL_MS);
1154
+ });
1155
+ }
1156
+ return true;
1157
+ }
1158
+ async function waitWithDeadline(promise, timeoutMs) {
1159
+ return new Promise((resolve) => {
1160
+ let settled = false;
1161
+ const timer = setTimeout(() => {
1162
+ if (!settled) {
1163
+ settled = true;
1164
+ resolve(false);
1165
+ }
1166
+ }, timeoutMs);
1167
+ void promise.then(
1168
+ () => {
1169
+ if (!settled) {
1170
+ settled = true;
1171
+ clearTimeout(timer);
1172
+ resolve(true);
1173
+ }
1174
+ },
1175
+ () => {
1176
+ if (!settled) {
1177
+ settled = true;
1178
+ clearTimeout(timer);
1179
+ resolve(false);
1180
+ }
1181
+ }
1182
+ );
1183
+ });
1184
+ }
1185
+ function requestOwnedProcessTreeTermination(options) {
1186
+ if (options.isClosed()) return;
1187
+ const pid = positivePid(options.child, options.label);
1188
+ 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);
1199
+ return;
1200
+ }
1201
+ signalGroup(pid, "SIGTERM", options.label);
1202
+ terminationRequested.add(options.child);
1203
+ }
1204
+ async function disposeOwnedProcessTree(options) {
1205
+ const pid = positivePid(options.child, options.label);
1206
+ const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
1207
+ const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
1208
+ if (pid === void 0) {
1209
+ if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
1210
+ throw new RuntimeDisposalFailure({
1211
+ stage: "quiescence",
1212
+ reason: `${options.label} runtime process did not settle after spawn failure`
1213
+ });
1214
+ }
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)) {
1219
+ throw new RuntimeDisposalFailure({
1220
+ stage: "signal",
1221
+ reason: `${options.label} runtime process tree could not be terminated`
1222
+ });
1223
+ }
1224
+ throw new RuntimeDisposalFailure({
1225
+ stage: "quiescence",
1226
+ reason: `${options.label} runtime process tree did not close before the disposal deadline`
1227
+ });
1228
+ }
1229
+ if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
1230
+ signalGroup(pid, "SIGTERM", options.label);
1231
+ terminationRequested.add(options.child);
1232
+ }
1233
+ if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
1234
+ signalGroup(pid, "SIGKILL", options.label);
1235
+ if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
1236
+ throw new RuntimeDisposalFailure({
1237
+ stage: "quiescence",
1238
+ reason: `${options.label} runtime process group remained live after SIGKILL`
1239
+ });
1240
+ }
1241
+ }
1242
+ if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
1243
+ throw new RuntimeDisposalFailure({
1244
+ stage: "quiescence",
1245
+ reason: `${options.label} runtime root did not emit close after its process group exited`
1246
+ });
1247
+ }
1248
+ }
948
1249
 
949
1250
  // src/adapters/pi/rpc-client.ts
950
1251
  var STDERR_RING_CAPACITY = 20;
@@ -957,16 +1258,22 @@ var PiRpcClient = class {
957
1258
  eventQueue = new AsyncQueue();
958
1259
  closed = false;
959
1260
  exitError;
1261
+ closedPromise;
1262
+ resolveClosed;
1263
+ disposalAttempt;
960
1264
  /** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
961
1265
  stderrRing = [];
962
1266
  /** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
963
1267
  unmappedFrameCounts = /* @__PURE__ */ new Map();
964
1268
  constructor(options) {
965
1269
  const spawnFn = options.spawnFn ?? spawn;
966
- this.child = spawnFn(options.command, options.args, {
1270
+ this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
967
1271
  cwd: options.cwd,
968
1272
  env: options.env,
969
1273
  stdio: ["pipe", "pipe", "pipe"]
1274
+ }));
1275
+ this.closedPromise = new Promise((resolve) => {
1276
+ this.resolveClosed = resolve;
970
1277
  });
971
1278
  this.child.stdout.setEncoding("utf8");
972
1279
  this.child.stdout.on("data", (chunk) => this.onData(chunk));
@@ -1001,6 +1308,10 @@ var PiRpcClient = class {
1001
1308
  get events() {
1002
1309
  return this.eventQueue;
1003
1310
  }
1311
+ /** Local transport diagnostic retained when the process closes; consumers must classify it explicitly. */
1312
+ get terminalError() {
1313
+ return this.exitError;
1314
+ }
1004
1315
  /**
1005
1316
  * Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
1006
1317
  * has no `AgentEvent` mapping and isn't routine bookkeeping (see
@@ -1019,15 +1330,30 @@ var PiRpcClient = class {
1019
1330
  );
1020
1331
  }
1021
1332
  }
1022
- /** Best-effort teardown. SIGTERM on POSIX; `taskkill /T /F` on Windows to also reap child processes pi itself spawned (e.g. bash). */
1333
+ /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
1023
1334
  kill() {
1024
- if (this.closed) return;
1025
- const pid = this.child.pid;
1026
- if (process.platform === "win32" && pid !== void 0) {
1027
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"]);
1028
- } else {
1029
- this.child.kill("SIGTERM");
1335
+ requestOwnedProcessTreeTermination(this.processTreeOptions());
1336
+ }
1337
+ waitClosed() {
1338
+ return this.closedPromise;
1339
+ }
1340
+ dispose() {
1341
+ if (!this.disposalAttempt) {
1342
+ const attempt = disposeOwnedProcessTree(this.processTreeOptions());
1343
+ this.disposalAttempt = attempt.catch((error) => {
1344
+ this.disposalAttempt = void 0;
1345
+ throw error;
1346
+ });
1030
1347
  }
1348
+ return this.disposalAttempt;
1349
+ }
1350
+ processTreeOptions() {
1351
+ return {
1352
+ child: this.child,
1353
+ waitClosed: () => this.closedPromise,
1354
+ isClosed: () => this.closed,
1355
+ label: "pi"
1356
+ };
1031
1357
  }
1032
1358
  onData(chunk) {
1033
1359
  this.buffer += chunk;
@@ -1113,6 +1439,7 @@ var PiRpcClient = class {
1113
1439
  if (this.closed) return;
1114
1440
  this.closed = true;
1115
1441
  this.exitError = err;
1442
+ this.resolveClosed();
1116
1443
  for (const [, waiter] of this.pending) waiter.reject(err);
1117
1444
  this.pending.clear();
1118
1445
  this.eventQueue.end();
@@ -1183,8 +1510,17 @@ var PiAdapter = class {
1183
1510
  this.options = options;
1184
1511
  }
1185
1512
  options;
1186
- id = "pi";
1187
- supportsDispatchSelection = true;
1513
+ descriptor = freezeRuntimeAdapterDescriptor({
1514
+ id: "pi",
1515
+ supportsDispatchSelection: true,
1516
+ capabilities: {
1517
+ steer: true,
1518
+ resume: true,
1519
+ approvalInteractive: false,
1520
+ permissionModes: ["auto", "readonly"]
1521
+ },
1522
+ environmentRequirements: { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES }
1523
+ });
1188
1524
  async detect() {
1189
1525
  try {
1190
1526
  const bin = this.resolveBin();
@@ -1196,49 +1532,26 @@ var PiAdapter = class {
1196
1532
  return { present: false };
1197
1533
  }
1198
1534
  }
1199
- capabilities() {
1200
- return { steer: true, resume: true, approvalInteractive: false, permissionModes: ["auto", "readonly"] };
1201
- }
1202
- /**
1203
- * M5: pi authenticates to its ~30 supported providers via env-var API
1204
- * keys — `detect()`'s own `authPresent` probe above checks this identical
1205
- * list — so these MUST keep flowing into pi's spawned process or pi auth
1206
- * breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
1207
- * of truth, reused here rather than duplicated. No `baseNames`: nothing
1208
- * in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
1209
- * variable beyond the platform baseline (`daemon/environment.ts`).
1210
- */
1211
- environmentRequirements() {
1212
- return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
1213
- }
1214
- async start(task, ctx) {
1215
- if (typeof task.instruction !== "string") {
1216
- throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
1217
- }
1218
- const mapping = mapPermissionPolicyToPiArgs(ctx.policy);
1535
+ async prepare(input) {
1536
+ const mapping = mapPermissionPolicyToPiArgs(input.policy);
1219
1537
  if (!mapping.ok) {
1220
- throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by pi adapter");
1538
+ return { kind: "reject", reason: mapping.reason ?? "policy rejected by pi adapter", retryable: false };
1221
1539
  }
1222
1540
  const bin = this.resolveBin();
1223
- const resumeSessionId = task.sessionRef;
1224
- const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
1225
- const selection = task.dispatchSelection;
1541
+ const selection = input.offer.dispatchSelection;
1542
+ const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
1226
1543
  let command = bin.command;
1227
- let args = piArgs;
1228
- if (selection !== void 0) {
1229
- if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
1230
- throw new PolicyUnsupportedError(
1231
- `pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
1232
- );
1544
+ let launcherArgs;
1545
+ if (pinnedSelection !== void 0) {
1546
+ if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
1547
+ return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
1233
1548
  }
1234
1549
  const launcher = this.options.byokLauncher;
1235
1550
  if (launcher === void 0) {
1236
- throw new PolicyUnsupportedError(
1237
- "pi BYOK selection requires a configured credential-custody launcher"
1238
- );
1551
+ return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
1239
1552
  }
1240
1553
  command = launcher.command;
1241
- args = [
1554
+ launcherArgs = [
1242
1555
  ...launcher.args ?? [],
1243
1556
  "--pi-bin",
1244
1557
  bin.command,
@@ -1248,62 +1561,136 @@ var PiAdapter = class {
1248
1561
  launcher.sessionDir,
1249
1562
  ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
1250
1563
  "--provider",
1251
- selection.providerId,
1564
+ pinnedSelection.providerId,
1252
1565
  "--model",
1253
- selection.modelId,
1254
- "--",
1255
- ...piArgs
1566
+ pinnedSelection.modelId
1256
1567
  ];
1257
1568
  }
1258
- const rpc = new PiRpcClient({
1259
- command,
1260
- args,
1261
- cwd: ctx.workspaceDir,
1262
- env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
1263
- spawnFn: this.options.spawnFn
1264
- });
1265
- const response = await rpc.send({ type: "prompt", message: task.instruction });
1266
- if (response.success === false) {
1267
- rpc.kill();
1268
- throw new Error(typeof response.error === "string" ? response.error : "pi rejected the initial prompt");
1269
- }
1270
- let sessionRef;
1271
- if (resumeSessionId) {
1272
- sessionRef = resumeSessionId;
1273
- } else {
1274
- try {
1275
- sessionRef = await resolveFreshSessionId(rpc);
1276
- } catch (err) {
1277
- rpc.kill();
1278
- throw err;
1569
+ return {
1570
+ kind: "prepared",
1571
+ operation: {
1572
+ start: async (startInput) => {
1573
+ const manifestSelection = startInput.manifest.dispatchSelection;
1574
+ if (!sameDispatchSelection(manifestSelection, pinnedSelection)) {
1575
+ throw new RuntimeExecutionFailure({
1576
+ phase: "start",
1577
+ category: "authority",
1578
+ retry: "non-retryable",
1579
+ reason: "prepared pi operation received a manifest with different runtime selection"
1580
+ });
1581
+ }
1582
+ if (typeof startInput.instruction !== "string") {
1583
+ throw new RuntimeExecutionFailure({
1584
+ phase: "start",
1585
+ category: "authority",
1586
+ retry: "non-retryable",
1587
+ reason: "prepared pi operation requires a resolved string instruction"
1588
+ });
1589
+ }
1590
+ const resumeSessionId = startInput.manifest.sessionRef;
1591
+ const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
1592
+ const args = launcherArgs === void 0 ? piArgs : [...launcherArgs, "--", ...piArgs];
1593
+ let rpc;
1594
+ try {
1595
+ rpc = new PiRpcClient({
1596
+ command,
1597
+ args,
1598
+ cwd: startInput.manifest.workspace.workspaceDir,
1599
+ env: manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env),
1600
+ spawnFn: this.options.spawnFn
1601
+ });
1602
+ } catch (cause) {
1603
+ throw new RuntimeExecutionFailure({
1604
+ phase: "start",
1605
+ category: "infrastructure",
1606
+ retry: "retryable",
1607
+ reason: "pi runtime process could not be spawned"
1608
+ }, { cause });
1609
+ }
1610
+ let response;
1611
+ try {
1612
+ response = await rpc.send({ type: "prompt", message: startInput.instruction });
1613
+ } catch (cause) {
1614
+ rpc.kill();
1615
+ throw new RuntimeExecutionFailure({
1616
+ phase: "start",
1617
+ category: "infrastructure",
1618
+ retry: "retryable",
1619
+ reason: `pi initial prompt transport failed: ${errorMessage(cause)}`
1620
+ }, { cause });
1621
+ }
1622
+ if (response.success === false) {
1623
+ rpc.kill();
1624
+ throw new RuntimeExecutionFailure({
1625
+ phase: "start",
1626
+ category: "semantic",
1627
+ retry: "non-retryable",
1628
+ reason: typeof response.error === "string" ? response.error : "pi rejected the initial prompt"
1629
+ });
1630
+ }
1631
+ let sessionRef;
1632
+ try {
1633
+ sessionRef = await resolveAuthoritativeSessionId(rpc);
1634
+ } catch (err) {
1635
+ rpc.kill();
1636
+ throw err;
1637
+ }
1638
+ if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1639
+ rpc.kill();
1640
+ throw new RuntimeExecutionFailure({
1641
+ phase: "start",
1642
+ category: "authority",
1643
+ retry: "non-retryable",
1644
+ reason: "pi resumed a different authoritative session than requested"
1645
+ });
1646
+ }
1647
+ return new PiSession(sessionRef, rpc, manifestSelection);
1648
+ }
1279
1649
  }
1280
- }
1281
- return new PiSession(sessionRef, rpc, selection);
1650
+ };
1282
1651
  }
1283
1652
  resolveBin() {
1284
1653
  return (this.options.resolveBin ?? resolvePiBin)();
1285
1654
  }
1286
1655
  };
1287
- async function resolveFreshSessionId(rpc) {
1656
+ function sameDispatchSelection(left, right) {
1657
+ if (left === void 0 || right === void 0) return left === right;
1658
+ return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
1659
+ }
1660
+ async function resolveAuthoritativeSessionId(rpc) {
1288
1661
  let state;
1289
1662
  try {
1290
1663
  state = await rpc.send({ type: "get_state" });
1291
1664
  } catch (err) {
1292
- throw new Error(`pi did not yield an authoritative session id (get_state failed): ${errorMessage(err)}`, {
1665
+ if (isRuntimeExecutionFailure(err)) throw err;
1666
+ throw new RuntimeExecutionFailure({
1667
+ phase: "start",
1668
+ category: "infrastructure",
1669
+ retry: "retryable",
1670
+ reason: `pi transport ended before yielding an authoritative session id: ${errorMessage(err)}`
1671
+ }, {
1293
1672
  cause: err
1294
1673
  });
1295
1674
  }
1296
1675
  if (state.success === false) {
1297
1676
  const reason = typeof state.error === "string" ? state.error : "get_state reported failure";
1298
- throw new Error(`pi did not yield an authoritative session id (get_state failed): ${reason}`);
1677
+ throw new RuntimeExecutionFailure({
1678
+ phase: "start",
1679
+ category: "authority",
1680
+ retry: "non-retryable",
1681
+ reason: `pi did not yield an authoritative session id: ${reason}`
1682
+ });
1299
1683
  }
1300
1684
  const data = state.data;
1301
1685
  if (typeof data?.sessionId === "string" && data.sessionId.length > 0) {
1302
1686
  return data.sessionId;
1303
1687
  }
1304
- throw new Error(
1305
- "pi did not yield an authoritative session id (get_state succeeded but reported no sessionId) \u2014 cannot mint a resumable session"
1306
- );
1688
+ throw new RuntimeExecutionFailure({
1689
+ phase: "start",
1690
+ category: "authority",
1691
+ retry: "non-retryable",
1692
+ reason: "pi get_state reported no authoritative session id"
1693
+ });
1307
1694
  }
1308
1695
  var PiSession = class {
1309
1696
  constructor(sessionRef, rpc, selection) {
@@ -1319,12 +1706,40 @@ var PiSession = class {
1319
1706
  return {
1320
1707
  [Symbol.asyncIterator]() {
1321
1708
  const inner = rpc.events[Symbol.asyncIterator]();
1709
+ let terminalFailure;
1322
1710
  return {
1323
1711
  async next() {
1324
1712
  for (; ; ) {
1325
- const { value, done } = await inner.next();
1326
- if (done) return { value: void 0, done: true };
1713
+ if (terminalFailure) throw terminalFailure;
1714
+ let result;
1715
+ try {
1716
+ result = await inner.next();
1717
+ } catch (cause) {
1718
+ throw new RuntimeExecutionFailure({
1719
+ phase: "run",
1720
+ category: "infrastructure",
1721
+ retry: "retryable",
1722
+ reason: "pi runtime event transport failed"
1723
+ }, { cause });
1724
+ }
1725
+ const { value, done } = result;
1726
+ if (done) {
1727
+ throw new RuntimeExecutionFailure({
1728
+ phase: "run",
1729
+ category: "infrastructure",
1730
+ retry: "retryable",
1731
+ reason: "pi runtime process ended before agent_settled"
1732
+ }, { cause: rpc.terminalError });
1733
+ }
1327
1734
  const mapped = mapPiMessageToAgentEvent(value);
1735
+ if (value.type === "auto_retry_end" && value.success === false) {
1736
+ terminalFailure = new RuntimeExecutionFailure({
1737
+ phase: "run",
1738
+ category: "semantic",
1739
+ retry: "non-retryable",
1740
+ reason: "pi exhausted its native retry policy"
1741
+ });
1742
+ }
1328
1743
  if (mapped) return { value: mapped, done: false };
1329
1744
  if (!ROUTINE_PI_EVENT_TYPES.has(value.type)) {
1330
1745
  rpc.recordUnmappedFrame(value.type);
@@ -1354,7 +1769,7 @@ var PiSession = class {
1354
1769
  await this.rpc.send({ type: "abort" });
1355
1770
  }
1356
1771
  async close() {
1357
- this.rpc.kill();
1772
+ await this.rpc.dispose();
1358
1773
  }
1359
1774
  async resolveApproval() {
1360
1775
  throw new Error("pi adapter does not support approval resume: pi never emits needs_approval in M0/M1");
@@ -1566,7 +1981,15 @@ function mapResult(msg) {
1566
1981
  diagnostic ? `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed; diagnostic content on the frame: ${truncateResultDiagnostic(diagnostic)}` : `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed`
1567
1982
  );
1568
1983
  const events = usageEvent ? [usageEvent, { type: "error", message }] : [{ type: "error", message }];
1569
- return { events };
1984
+ return {
1985
+ events,
1986
+ terminalFailure: new RuntimeExecutionFailure({
1987
+ phase: "run",
1988
+ category: msg.is_error === true ? "semantic" : "authority",
1989
+ retry: "non-retryable",
1990
+ reason: msg.is_error === true ? "claude reported terminal task failure" : "claude emitted a malformed terminal result frame"
1991
+ })
1992
+ };
1570
1993
  }
1571
1994
  var RESULT_DIAGNOSTIC_MAX_CHARS = 2e3;
1572
1995
  function truncateResultDiagnostic(text) {
@@ -1618,16 +2041,22 @@ var ClaudeProcessClient = class {
1618
2041
  eventQueue = new AsyncQueue();
1619
2042
  closed = false;
1620
2043
  exitError;
2044
+ closedPromise;
2045
+ resolveClosed;
2046
+ disposalAttempt;
1621
2047
  stderrRing = [];
1622
2048
  unmappedFrameCounts = /* @__PURE__ */ new Map();
1623
2049
  sessionId;
1624
2050
  initWaiter;
1625
2051
  constructor(options) {
1626
2052
  const spawnFn = options.spawnFn ?? spawn;
1627
- this.child = spawnFn(options.command, options.args, {
2053
+ this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
1628
2054
  cwd: options.cwd,
1629
2055
  env: options.env,
1630
2056
  stdio: ["pipe", "pipe", "pipe"]
2057
+ }));
2058
+ this.closedPromise = new Promise((resolve) => {
2059
+ this.resolveClosed = resolve;
1631
2060
  });
1632
2061
  this.child.stdout.setEncoding("utf8");
1633
2062
  this.child.stdout.on("data", (chunk) => this.onData(chunk));
@@ -1683,6 +2112,10 @@ var ClaudeProcessClient = class {
1683
2112
  get events() {
1684
2113
  return this.eventQueue;
1685
2114
  }
2115
+ /** Local transport diagnostic retained when the process closes; consumers classify it at the session boundary. */
2116
+ get terminalError() {
2117
+ return this.exitError;
2118
+ }
1686
2119
  /**
1687
2120
  * Record a claude stream-json frame/subtype/content-block label that
1688
2121
  * `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
@@ -1702,15 +2135,30 @@ var ClaudeProcessClient = class {
1702
2135
  );
1703
2136
  }
1704
2137
  }
1705
- /** Best-effort teardown. SIGTERM on POSIX; `taskkill /T /F` on Windows to also reap child processes claude itself spawned (e.g. Bash) — mirrors pi's cross-platform `kill()` exactly. Empirically confirmed on this (POSIX) machine: a running claude process exits cleanly within ~1s of SIGTERM (observed exit code 143 = 128+SIGTERM, i.e. claude catches and handles the signal itself rather than needing a harder kill). */
2138
+ /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
1706
2139
  kill() {
1707
- if (this.closed) return;
1708
- const pid = this.child.pid;
1709
- if (process.platform === "win32" && pid !== void 0) {
1710
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"]);
1711
- } else {
1712
- this.child.kill("SIGTERM");
2140
+ requestOwnedProcessTreeTermination(this.processTreeOptions());
2141
+ }
2142
+ waitClosed() {
2143
+ return this.closedPromise;
2144
+ }
2145
+ dispose() {
2146
+ if (!this.disposalAttempt) {
2147
+ const attempt = disposeOwnedProcessTree(this.processTreeOptions());
2148
+ this.disposalAttempt = attempt.catch((error) => {
2149
+ this.disposalAttempt = void 0;
2150
+ throw error;
2151
+ });
1713
2152
  }
2153
+ return this.disposalAttempt;
2154
+ }
2155
+ processTreeOptions() {
2156
+ return {
2157
+ child: this.child,
2158
+ waitClosed: () => this.closedPromise,
2159
+ isClosed: () => this.closed,
2160
+ label: "claude"
2161
+ };
1714
2162
  }
1715
2163
  onData(chunk) {
1716
2164
  this.buffer += chunk;
@@ -1761,6 +2209,7 @@ var ClaudeProcessClient = class {
1761
2209
  if (this.closed) return;
1762
2210
  this.closed = true;
1763
2211
  this.exitError = err;
2212
+ this.resolveClosed();
1764
2213
  this.initWaiter?.reject(err);
1765
2214
  this.initWaiter = void 0;
1766
2215
  this.eventQueue.end();
@@ -1772,18 +2221,37 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
1772
2221
  var APPROVAL_MCP_SERVER_NAME = "byokapproval";
1773
2222
  var execFileAsync2 = promisify(execFile);
1774
2223
  var DETECT_TIMEOUT_MS2 = 5e3;
2224
+ function errorMessage2(err) {
2225
+ return err instanceof Error ? err.message : String(err);
2226
+ }
1775
2227
  async function cleanupMcpConfigDir(dir) {
1776
2228
  if (!dir) return;
1777
- await promises.rm(dir, { recursive: true, force: true }).catch(() => {
1778
- });
2229
+ try {
2230
+ await promises.rm(dir, { recursive: true, force: true });
2231
+ } catch (cause) {
2232
+ throw new RuntimeDisposalFailure({
2233
+ stage: "cleanup",
2234
+ reason: "claude task-scoped MCP configuration could not be removed"
2235
+ }, { cause });
2236
+ }
1779
2237
  }
1780
2238
  var ClaudeAdapter = class {
1781
2239
  constructor(options = {}) {
1782
2240
  this.options = options;
1783
2241
  }
1784
2242
  options;
1785
- supportsDispatchSelection = true;
1786
- id = "claude";
2243
+ descriptor = freezeRuntimeAdapterDescriptor({
2244
+ id: "claude",
2245
+ supportsDispatchSelection: true,
2246
+ capabilities: {
2247
+ steer: false,
2248
+ resume: true,
2249
+ approvalInteractive: true,
2250
+ mcpToolsets: true,
2251
+ permissionModes: ["auto", "readonly", "plan", "confirm"]
2252
+ },
2253
+ environmentRequirements: { credentialNames: [] }
2254
+ });
1787
2255
  async detect() {
1788
2256
  const bin = this.resolveBin();
1789
2257
  try {
@@ -1795,54 +2263,73 @@ var ClaudeAdapter = class {
1795
2263
  return { present: false };
1796
2264
  }
1797
2265
  }
1798
- capabilities() {
2266
+ async prepare(input) {
2267
+ const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
2268
+ if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
2269
+ let modelId;
2270
+ try {
2271
+ modelId = subscriptionModel(input.offer.dispatchSelection, "claude");
2272
+ } catch (error) {
2273
+ return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
2274
+ }
2275
+ if (mapping.needsApprovalMcp && Object.prototype.hasOwnProperty.call(input.mcpServers ?? {}, APPROVAL_MCP_SERVER_NAME)) {
2276
+ return { kind: "reject", reason: `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`, retryable: false };
2277
+ }
2278
+ let bin;
2279
+ try {
2280
+ bin = this.resolveBin();
2281
+ } catch (error) {
2282
+ return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
2283
+ }
2284
+ let approvalMcpBin;
2285
+ if (mapping.needsApprovalMcp) {
2286
+ try {
2287
+ approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
2288
+ } catch (error) {
2289
+ return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
2290
+ }
2291
+ }
1799
2292
  return {
1800
- steer: false,
1801
- resume: true,
1802
- approvalInteractive: true,
1803
- mcpToolsets: true,
1804
- permissionModes: ["auto", "readonly", "plan", "confirm"]
2293
+ kind: "prepared",
2294
+ operation: {
2295
+ start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
2296
+ }
1805
2297
  };
1806
2298
  }
1807
- /**
1808
- * M5: deliberate product-boundary decision, not an oversight — byok's
1809
- * current ToS posture for claude is login-state-only (`claude auth
1810
- * login`'s own OAuth session — see `probeAuthPresent` below), so this
1811
- * adapter declares NO credential env vars at all; env-based API-key
1812
- * passthrough for claude is a separate, still-pending product decision.
1813
- * A product that genuinely needs it can opt in locally per-device via
1814
- * `DaemonConfig.runtimeEnvironment.claude.allow` (`create-daemon.ts`).
1815
- * `baseNames` is empty too: nothing in this adapter reads a
1816
- * claude-specific config-discovery variable (e.g. `CLAUDE_CONFIG_DIR`)
1817
- * today — if a future version of this adapter starts reading one, it
1818
- * belongs here, not left to rely on the platform baseline alone.
1819
- */
1820
- environmentRequirements() {
1821
- return { credentialNames: [] };
1822
- }
1823
- async start(task, ctx) {
1824
- if (typeof task.instruction !== "string") {
1825
- throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
1826
- }
1827
- const mapping = mapPermissionPolicyToClaudeArgs(ctx.policy);
1828
- if (!mapping.ok) {
1829
- throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
2299
+ async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
2300
+ if (!initialMapping.ok) throw new RuntimeExecutionFailure({
2301
+ phase: "start",
2302
+ category: "authority",
2303
+ retry: "non-retryable",
2304
+ reason: "prepared claude permission mapping was invalid"
2305
+ });
2306
+ if (typeof startInput.instruction !== "string") {
2307
+ throw new RuntimeExecutionFailure({
2308
+ phase: "start",
2309
+ category: "authority",
2310
+ retry: "non-retryable",
2311
+ reason: "prepared claude operation requires a resolved string instruction"
2312
+ });
1830
2313
  }
1831
- const modelId = subscriptionModel(task, "claude");
2314
+ const mapping = { ...initialMapping, args: [...initialMapping.args] };
1832
2315
  let mcpConfigDir;
1833
- const taskMcpServers = ctx.mcpServers ?? {};
2316
+ const taskMcpServers = startInput.mcpServers ?? {};
1834
2317
  const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
1835
2318
  if (mapping.needsApprovalMcp) {
1836
- if (!ctx.approvalChannel) {
1837
- throw new PolicyUnsupportedError(
1838
- 'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
1839
- );
1840
- }
1841
- if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
1842
- throw new PolicyUnsupportedError(
1843
- `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
1844
- );
2319
+ if (!startInput.approvalChannel) {
2320
+ throw new RuntimeExecutionFailure({
2321
+ phase: "start",
2322
+ category: "authority",
2323
+ retry: "non-retryable",
2324
+ reason: 'claude adapter requires policy.mode "confirm" to be started with an approval channel'
2325
+ });
1845
2326
  }
2327
+ if (!approvalMcpBin) throw new RuntimeExecutionFailure({
2328
+ phase: "start",
2329
+ category: "authority",
2330
+ retry: "non-retryable",
2331
+ reason: "prepared claude approval MCP binary was not resolved"
2332
+ });
1846
2333
  }
1847
2334
  if (needsMcpConfig) {
1848
2335
  mcpConfigDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-mcp-"));
@@ -1851,12 +2338,23 @@ var ClaudeAdapter = class {
1851
2338
  const mcpConfigPath = path20.join(mcpConfigDir, "mcp-config.json");
1852
2339
  const mcpServers = { ...taskMcpServers };
1853
2340
  if (mapping.needsApprovalMcp) {
1854
- const approvalChannel = ctx.approvalChannel;
1855
- if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
1856
- const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
2341
+ const approvalChannel = startInput.approvalChannel;
2342
+ if (!approvalChannel) throw new RuntimeExecutionFailure({
2343
+ phase: "start",
2344
+ category: "authority",
2345
+ retry: "non-retryable",
2346
+ reason: "prepared claude approval channel was not available"
2347
+ });
2348
+ const preparedApprovalMcpBin = approvalMcpBin;
2349
+ if (!preparedApprovalMcpBin) throw new RuntimeExecutionFailure({
2350
+ phase: "start",
2351
+ category: "authority",
2352
+ retry: "non-retryable",
2353
+ reason: "prepared claude approval MCP binary was not resolved"
2354
+ });
1857
2355
  mcpServers[APPROVAL_MCP_SERVER_NAME] = {
1858
- command: approvalMcpBin.command,
1859
- args: approvalMcpBin.args,
2356
+ command: preparedApprovalMcpBin.command,
2357
+ args: preparedApprovalMcpBin.args,
1860
2358
  env: {
1861
2359
  BYOK_STORE_DIR: approvalChannel.storeDir,
1862
2360
  BYOK_PRODUCT_ID: approvalChannel.productId,
@@ -1876,8 +2374,26 @@ var ClaudeAdapter = class {
1876
2374
  "--strict-mcp-config"
1877
2375
  ];
1878
2376
  }
1879
- const bin = this.resolveBin();
1880
- const resumeSessionId = task.sessionRef;
2377
+ const resumeSessionId = startInput.manifest.sessionRef;
2378
+ let manifestModelId;
2379
+ try {
2380
+ manifestModelId = subscriptionModel(startInput.manifest.dispatchSelection, "claude");
2381
+ } catch (cause) {
2382
+ throw new RuntimeExecutionFailure({
2383
+ phase: "start",
2384
+ category: "authority",
2385
+ retry: "non-retryable",
2386
+ reason: "prepared claude operation received an invalid runtime selection manifest"
2387
+ }, { cause });
2388
+ }
2389
+ if (manifestModelId !== modelId) {
2390
+ throw new RuntimeExecutionFailure({
2391
+ phase: "start",
2392
+ category: "authority",
2393
+ retry: "non-retryable",
2394
+ reason: "prepared claude operation received a manifest with different runtime selection"
2395
+ });
2396
+ }
1881
2397
  const args = [
1882
2398
  "-p",
1883
2399
  "--input-format",
@@ -1889,40 +2405,71 @@ var ClaudeAdapter = class {
1889
2405
  // "Error: When using --print, --output-format=stream-json requires
1890
2406
  // --verbose", before spawning any model call.
1891
2407
  "--verbose",
1892
- ...modelId ? ["--model", modelId] : [],
2408
+ ...manifestModelId ? ["--model", manifestModelId] : [],
1893
2409
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
1894
2410
  ...mapping.args
1895
2411
  ];
1896
- const client = new ClaudeProcessClient({
1897
- command: bin.command,
1898
- args,
1899
- cwd: ctx.workspaceDir,
1900
- env: withoutProviderCredentials(ctx.env),
1901
- spawnFn: this.options.spawnFn
1902
- });
1903
- client.writeUserMessage(task.instruction);
2412
+ let client;
2413
+ try {
2414
+ client = new ClaudeProcessClient({
2415
+ command: bin.command,
2416
+ args,
2417
+ cwd: startInput.manifest.workspace.workspaceDir,
2418
+ env: withoutProviderCredentials(startInput.env),
2419
+ spawnFn: this.options.spawnFn
2420
+ });
2421
+ } catch (cause) {
2422
+ await cleanupMcpConfigDir(mcpConfigDir);
2423
+ throw new RuntimeExecutionFailure({
2424
+ phase: "start",
2425
+ category: "infrastructure",
2426
+ retry: "retryable",
2427
+ reason: "claude runtime process could not be spawned"
2428
+ }, { cause });
2429
+ }
2430
+ try {
2431
+ client.writeUserMessage(startInput.instruction);
2432
+ } catch (cause) {
2433
+ client.kill();
2434
+ await cleanupMcpConfigDir(mcpConfigDir);
2435
+ throw new RuntimeExecutionFailure({
2436
+ phase: "start",
2437
+ category: "infrastructure",
2438
+ retry: "retryable",
2439
+ reason: "claude initial instruction transport failed"
2440
+ }, { cause });
2441
+ }
1904
2442
  let sessionRef;
1905
2443
  try {
1906
2444
  sessionRef = await client.waitForInit();
1907
2445
  } catch (err) {
1908
2446
  client.kill();
1909
2447
  await cleanupMcpConfigDir(mcpConfigDir);
1910
- throw err;
2448
+ if (isRuntimeExecutionFailure(err)) throw err;
2449
+ throw new RuntimeExecutionFailure({
2450
+ phase: "start",
2451
+ category: "infrastructure",
2452
+ retry: "retryable",
2453
+ reason: `claude exited before yielding an authoritative session id: ${errorMessage2(err)}`
2454
+ }, { cause: err });
1911
2455
  }
1912
2456
  if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
1913
2457
  client.kill();
1914
2458
  await cleanupMcpConfigDir(mcpConfigDir);
1915
- throw new Error(
1916
- `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
1917
- );
2459
+ throw new RuntimeExecutionFailure({
2460
+ phase: "start",
2461
+ category: "authority",
2462
+ retry: "non-retryable",
2463
+ reason: `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef})`
2464
+ });
1918
2465
  }
1919
2466
  return new ClaudeSession(
1920
2467
  sessionRef,
1921
2468
  client,
1922
- ctx.workspaceDir,
1923
- ctx.approvalChannel,
2469
+ startInput.manifest.workspace.workspaceDir,
2470
+ startInput.approvalChannel,
1924
2471
  mcpConfigDir,
1925
- modelId
2472
+ manifestModelId
1926
2473
  );
1927
2474
  }
1928
2475
  /**
@@ -1957,8 +2504,7 @@ var ClaudeAdapter = class {
1957
2504
  return (this.options.resolveBin ?? resolveClaudeBin)();
1958
2505
  }
1959
2506
  };
1960
- function subscriptionModel(task, runtimeId) {
1961
- const selection = task.dispatchSelection;
2507
+ function subscriptionModel(selection, runtimeId) {
1962
2508
  if (selection === void 0) return void 0;
1963
2509
  if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
1964
2510
  throw new PolicyUnsupportedError(
@@ -1983,6 +2529,7 @@ var ClaudeSession = class {
1983
2529
  mcpConfigDir;
1984
2530
  modelId;
1985
2531
  correlation = createToolUseCorrelation();
2532
+ closeAttempt;
1986
2533
  get events() {
1987
2534
  const client = this.client;
1988
2535
  const correlation = this.correlation;
@@ -1991,17 +2538,40 @@ var ClaudeSession = class {
1991
2538
  [Symbol.asyncIterator]() {
1992
2539
  const inner = client.events[Symbol.asyncIterator]();
1993
2540
  let pending = [];
2541
+ let terminalFailure;
1994
2542
  let turnSettled = false;
1995
2543
  return {
1996
2544
  async next() {
1997
2545
  for (; ; ) {
1998
2546
  const buffered = pending.shift();
1999
2547
  if (buffered) return { value: buffered, done: false };
2000
- if (turnSettled) return { value: void 0, done: true };
2001
- const { value, done } = await inner.next();
2002
- if (done) return { value: void 0, done: true };
2548
+ if (turnSettled) {
2549
+ if (terminalFailure) throw terminalFailure;
2550
+ return { value: void 0, done: true };
2551
+ }
2552
+ let raw;
2553
+ try {
2554
+ raw = await inner.next();
2555
+ } catch (cause) {
2556
+ throw new RuntimeExecutionFailure({
2557
+ phase: "run",
2558
+ category: "infrastructure",
2559
+ retry: "retryable",
2560
+ reason: "claude runtime event transport failed"
2561
+ }, { cause });
2562
+ }
2563
+ const { value, done } = raw;
2564
+ if (done) {
2565
+ throw new RuntimeExecutionFailure({
2566
+ phase: "run",
2567
+ category: "infrastructure",
2568
+ retry: "retryable",
2569
+ reason: "claude runtime process ended before a terminal result frame"
2570
+ }, { cause: client.terminalError });
2571
+ }
2003
2572
  if (value.type === "result") turnSettled = true;
2004
2573
  const mapped = mapClaudeMessageToAgentEvents(value, correlation, { workspaceDir });
2574
+ terminalFailure = mapped.terminalFailure ?? terminalFailure;
2005
2575
  if (mapped.unmappedLabel) {
2006
2576
  client.recordUnmappedFrame(mapped.unmappedLabel);
2007
2577
  }
@@ -2032,7 +2602,7 @@ var ClaudeSession = class {
2032
2602
  if (typeof task.instruction !== "string") {
2033
2603
  throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
2034
2604
  }
2035
- const requestedModel = subscriptionModel(task, "claude");
2605
+ const requestedModel = subscriptionModel(task.dispatchSelection, "claude");
2036
2606
  if (requestedModel !== void 0 && requestedModel !== this.modelId) {
2037
2607
  throw new PolicyUnsupportedError(
2038
2608
  `claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
@@ -2056,8 +2626,17 @@ var ClaudeSession = class {
2056
2626
  this.client.kill();
2057
2627
  }
2058
2628
  async close() {
2059
- this.client.kill();
2060
- await cleanupMcpConfigDir(this.mcpConfigDir);
2629
+ if (!this.closeAttempt) {
2630
+ const attempt = (async () => {
2631
+ await this.client.dispose();
2632
+ await cleanupMcpConfigDir(this.mcpConfigDir);
2633
+ })();
2634
+ this.closeAttempt = attempt.catch((error) => {
2635
+ this.closeAttempt = void 0;
2636
+ throw error;
2637
+ });
2638
+ }
2639
+ await this.closeAttempt;
2061
2640
  }
2062
2641
  /**
2063
2642
  * M4 Phase 3: routes into the out-of-band approval channel `start()`
@@ -2274,14 +2853,15 @@ var CodexProcessRunner = class {
2274
2853
  exitSignal = null;
2275
2854
  closedPromise;
2276
2855
  resolveClosed;
2856
+ disposalAttempt;
2277
2857
  constructor(options) {
2278
2858
  this.onEvent = options.onEvent;
2279
2859
  const spawnFn = options.spawnFn ?? spawn;
2280
- this.child = spawnFn(options.command, options.args, {
2860
+ this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
2281
2861
  cwd: options.cwd,
2282
2862
  env: options.env,
2283
2863
  stdio: ["ignore", "pipe", "pipe"]
2284
- });
2864
+ }));
2285
2865
  this.closedPromise = new Promise((resolve) => {
2286
2866
  this.resolveClosed = resolve;
2287
2867
  });
@@ -2311,7 +2891,7 @@ var CodexProcessRunner = class {
2311
2891
  return this.closed;
2312
2892
  }
2313
2893
  /**
2314
- * Best-effort teardown. SIGTERM on POSIX: SIGINT was empirically confirmed
2894
+ * Immediate tree termination request. SIGTERM on POSIX: SIGINT was empirically confirmed
2315
2895
  * to be silently ignored by `codex exec` (a real, direct test — a 60s
2316
2896
  * shell `sleep` ran to full, unaffected completion despite SIGINT sent at
2317
2897
  * t=4s) — a genuine, evidence-based correction to this task's own initial
@@ -2324,13 +2904,25 @@ var CodexProcessRunner = class {
2324
2904
  * `../pi/rpc-client.ts`'s own cross-platform convention.
2325
2905
  */
2326
2906
  kill() {
2327
- if (this.closed) return;
2328
- const pid = this.child.pid;
2329
- if (process.platform === "win32" && pid !== void 0) {
2330
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"]);
2331
- } else {
2332
- this.child.kill("SIGTERM");
2907
+ requestOwnedProcessTreeTermination(this.processTreeOptions());
2908
+ }
2909
+ dispose() {
2910
+ if (!this.disposalAttempt) {
2911
+ const attempt = disposeOwnedProcessTree(this.processTreeOptions());
2912
+ this.disposalAttempt = attempt.catch((error) => {
2913
+ this.disposalAttempt = void 0;
2914
+ throw error;
2915
+ });
2333
2916
  }
2917
+ return this.disposalAttempt;
2918
+ }
2919
+ processTreeOptions() {
2920
+ return {
2921
+ child: this.child,
2922
+ waitClosed: () => this.closedPromise,
2923
+ isClosed: () => this.closed,
2924
+ label: "codex"
2925
+ };
2334
2926
  }
2335
2927
  /** Builds a descriptive error folding in the exit code/signal and the stderr tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a failed start/resume should never need separately re-running codex by hand with a raw JSONL logger to learn why. */
2336
2928
  buildExitError(context) {
@@ -2381,8 +2973,17 @@ var CodexAdapter = class {
2381
2973
  this.options = options;
2382
2974
  }
2383
2975
  options;
2384
- supportsDispatchSelection = true;
2385
- id = "codex";
2976
+ descriptor = freezeRuntimeAdapterDescriptor({
2977
+ id: "codex",
2978
+ supportsDispatchSelection: true,
2979
+ capabilities: {
2980
+ steer: false,
2981
+ resume: true,
2982
+ approvalInteractive: false,
2983
+ permissionModes: ["auto", "readonly"]
2984
+ },
2985
+ environmentRequirements: { credentialNames: [] }
2986
+ });
2386
2987
  async detect() {
2387
2988
  const bin = this.resolveBin();
2388
2989
  try {
@@ -2431,61 +3032,100 @@ ${result.stderr}`);
2431
3032
  ${withStreams.stderr ?? ""}`);
2432
3033
  }
2433
3034
  }
2434
- capabilities() {
2435
- return { steer: false, resume: true, approvalInteractive: false, permissionModes: ["auto", "readonly"] };
2436
- }
2437
- /**
2438
- * M5: same deliberate posture as the claude adapter (see its own doc
2439
- * comment) — codex authenticates via its own `codex login`-managed
2440
- * ChatGPT OAuth session (`probeAuthPresent` above), not an env var, so
2441
- * there is no credential env var this adapter needs forwarded; env-based
2442
- * API-key passthrough remains a separate, pending product decision. No
2443
- * `baseNames` either: nothing in this adapter reads a codex-specific
2444
- * config-discovery variable (e.g. `CODEX_HOME`) today.
2445
- */
2446
- environmentRequirements() {
2447
- return { credentialNames: [] };
2448
- }
2449
- async start(task, ctx) {
2450
- if (typeof task.instruction !== "string") {
2451
- throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
3035
+ async prepare(input) {
3036
+ const mapping = mapPermissionPolicyToCodexArgs(input.policy);
3037
+ if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by codex adapter", retryable: false };
3038
+ let modelId;
3039
+ try {
3040
+ modelId = subscriptionModel2(input.offer.dispatchSelection);
3041
+ } catch (error) {
3042
+ return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
2452
3043
  }
2453
- const mapping = mapPermissionPolicyToCodexArgs(ctx.policy);
2454
- if (!mapping.ok) {
2455
- throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
3044
+ let command;
3045
+ try {
3046
+ command = this.resolveBin().command;
3047
+ } catch (error) {
3048
+ return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
3049
+ }
3050
+ return {
3051
+ kind: "prepared",
3052
+ operation: {
3053
+ start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
3054
+ }
3055
+ };
3056
+ }
3057
+ async startPrepared(startInput, policyArgs, modelId, command) {
3058
+ if (typeof startInput.instruction !== "string") {
3059
+ throw new RuntimeExecutionFailure({
3060
+ phase: "start",
3061
+ category: "authority",
3062
+ retry: "non-retryable",
3063
+ reason: "prepared codex operation requires a resolved string instruction"
3064
+ });
2456
3065
  }
2457
- const modelId = subscriptionModel2(task);
2458
- const bin = this.resolveBin();
2459
3066
  const queue = new AsyncQueue();
3067
+ const terminal = {};
2460
3068
  const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
2461
- const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
2462
- const runtimeEnv = withoutProviderCredentials(ctx.env);
3069
+ let workspaceDir;
3070
+ try {
3071
+ workspaceDir = await resolveRealWorkspaceDir(startInput.manifest.workspace.workspaceDir);
3072
+ } catch (cause) {
3073
+ throw new RuntimeExecutionFailure({
3074
+ phase: "start",
3075
+ category: "infrastructure",
3076
+ retry: "retryable",
3077
+ reason: "codex runtime workspace could not be resolved"
3078
+ }, { cause });
3079
+ }
3080
+ const runtimeEnv = withoutProviderCredentials(startInput.env);
3081
+ let manifestModelId;
3082
+ try {
3083
+ manifestModelId = subscriptionModel2(startInput.manifest.dispatchSelection);
3084
+ } catch (cause) {
3085
+ throw new RuntimeExecutionFailure({
3086
+ phase: "start",
3087
+ category: "authority",
3088
+ retry: "non-retryable",
3089
+ reason: "prepared codex operation received an invalid runtime selection manifest"
3090
+ }, { cause });
3091
+ }
3092
+ if (manifestModelId !== modelId) {
3093
+ throw new RuntimeExecutionFailure({
3094
+ phase: "start",
3095
+ category: "authority",
3096
+ retry: "non-retryable",
3097
+ reason: "prepared codex operation received a manifest with different runtime selection"
3098
+ });
3099
+ }
2463
3100
  const { sessionRef, runner } = await runCodexTurn({
2464
- command: bin.command,
2465
- resumeRef: task.sessionRef,
2466
- instruction: task.instruction,
2467
- modelId,
2468
- policyArgs: mapping.args,
2469
- cwd: ctx.workspaceDir,
3101
+ command,
3102
+ resumeRef: startInput.manifest.sessionRef,
3103
+ instruction: startInput.instruction,
3104
+ modelId: manifestModelId,
3105
+ policyArgs: [...policyArgs],
3106
+ cwd: startInput.manifest.workspace.workspaceDir,
2470
3107
  env: runtimeEnv,
2471
3108
  spawnFn: this.options.spawnFn,
2472
3109
  workspaceDir,
2473
3110
  queue,
2474
3111
  recordUnmapped,
2475
- expectedSessionRef: task.sessionRef,
2476
- preparedGit: ctx.gitWorkspace !== void 0
3112
+ expectedSessionRef: startInput.manifest.sessionRef,
3113
+ preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
3114
+ failurePhase: "start",
3115
+ terminal
2477
3116
  });
2478
3117
  return new CodexSession({
2479
3118
  sessionRef,
2480
- command: bin.command,
3119
+ command,
2481
3120
  workspaceDir,
2482
- env: ctx.env,
3121
+ env: startInput.env,
2483
3122
  spawnFn: this.options.spawnFn,
2484
3123
  queue,
2485
3124
  recordUnmapped,
2486
3125
  initialRunner: runner,
2487
- preparedGit: ctx.gitWorkspace !== void 0,
2488
- modelId
3126
+ preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
3127
+ modelId: manifestModelId,
3128
+ terminal
2489
3129
  });
2490
3130
  }
2491
3131
  resolveBin() {
@@ -2533,37 +3173,69 @@ async function runCodexTurn(params) {
2533
3173
  rejectFirstLine = reject;
2534
3174
  });
2535
3175
  let turnEnded = false;
2536
- const runner = new CodexProcessRunner({
2537
- command: params.command,
2538
- args: argv,
2539
- cwd: params.cwd,
2540
- env: params.env,
2541
- spawnFn: params.spawnFn,
2542
- onEvent: (evt) => {
2543
- if (!firstLineSettled) {
2544
- firstLineSettled = true;
2545
- if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
2546
- resolveFirstLine(evt.thread_id);
2547
- } else {
2548
- rejectFirstLine(
2549
- new Error(`codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`)
2550
- );
3176
+ let runner;
3177
+ try {
3178
+ runner = new CodexProcessRunner({
3179
+ command: params.command,
3180
+ args: argv,
3181
+ cwd: params.cwd,
3182
+ env: params.env,
3183
+ spawnFn: params.spawnFn,
3184
+ onEvent: (evt) => {
3185
+ if (!firstLineSettled) {
3186
+ firstLineSettled = true;
3187
+ if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
3188
+ resolveFirstLine(evt.thread_id);
3189
+ } else {
3190
+ rejectFirstLine(
3191
+ new RuntimeExecutionFailure({
3192
+ phase: params.failurePhase,
3193
+ category: "authority",
3194
+ retry: "non-retryable",
3195
+ reason: `codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`
3196
+ })
3197
+ );
3198
+ }
3199
+ return;
3200
+ }
3201
+ const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3202
+ for (const agentEvent of mapped) {
3203
+ if (agentEvent.type === "turn_end") turnEnded = true;
3204
+ params.queue.push(agentEvent);
3205
+ }
3206
+ if (evt.type === "turn.failed") {
3207
+ params.terminal.failure = new RuntimeExecutionFailure({
3208
+ phase: "run",
3209
+ category: "semantic",
3210
+ retry: "non-retryable",
3211
+ reason: "codex reported terminal task failure"
3212
+ });
3213
+ params.queue.end();
3214
+ }
3215
+ if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
3216
+ params.recordUnmapped(unmappedFrameKey(evt));
2551
3217
  }
2552
- return;
2553
- }
2554
- const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
2555
- for (const agentEvent of mapped) {
2556
- if (agentEvent.type === "turn_end") turnEnded = true;
2557
- params.queue.push(agentEvent);
2558
- }
2559
- if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
2560
- params.recordUnmapped(unmappedFrameKey(evt));
2561
3218
  }
2562
- }
2563
- });
3219
+ });
3220
+ } catch (cause) {
3221
+ throw new RuntimeExecutionFailure({
3222
+ phase: params.failurePhase,
3223
+ category: "infrastructure",
3224
+ retry: "retryable",
3225
+ reason: "codex runtime process could not be spawned"
3226
+ }, { cause });
3227
+ }
3228
+ params.onRunnerCreated?.(runner);
2564
3229
  void runner.waitClosed().then(() => {
2565
- if (turnEnded) return;
2566
- params.queue.push({ type: "error", message: runner.buildExitError("codex exited without completing the turn").message });
3230
+ if (turnEnded || params.terminal.failure) return;
3231
+ const cause = runner.buildExitError("codex exited without completing the turn");
3232
+ params.terminal.failure = new RuntimeExecutionFailure({
3233
+ phase: "run",
3234
+ category: "infrastructure",
3235
+ retry: "retryable",
3236
+ reason: cause.message
3237
+ }, { cause });
3238
+ params.queue.push({ type: "error", message: cause.message });
2567
3239
  params.queue.end();
2568
3240
  });
2569
3241
  let sessionRef;
@@ -2587,7 +3259,13 @@ async function runCodexTurn(params) {
2587
3259
  void runner.waitClosed().then(() => {
2588
3260
  if (!settled) {
2589
3261
  settled = true;
2590
- reject(runner.buildExitError("codex exited before yielding an authoritative thread id"));
3262
+ const cause = runner.buildExitError("codex exited before yielding an authoritative thread id");
3263
+ reject(new RuntimeExecutionFailure({
3264
+ phase: params.failurePhase,
3265
+ category: "infrastructure",
3266
+ retry: "retryable",
3267
+ reason: cause.message
3268
+ }, { cause }));
2591
3269
  }
2592
3270
  });
2593
3271
  });
@@ -2597,9 +3275,12 @@ async function runCodexTurn(params) {
2597
3275
  }
2598
3276
  if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
2599
3277
  runner.kill();
2600
- throw new Error(
2601
- `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
2602
- );
3278
+ throw new RuntimeExecutionFailure({
3279
+ phase: params.failurePhase,
3280
+ category: "authority",
3281
+ retry: "non-retryable",
3282
+ reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
3283
+ });
2603
3284
  }
2604
3285
  return { sessionRef, runner };
2605
3286
  }
@@ -2626,8 +3307,12 @@ var CodexSession = class {
2626
3307
  recordUnmapped;
2627
3308
  preparedGit;
2628
3309
  modelId;
3310
+ terminal;
2629
3311
  currentRunner;
3312
+ ownedRunners = /* @__PURE__ */ new Set();
3313
+ followUpAttempts = /* @__PURE__ */ new Set();
2630
3314
  closed = false;
3315
+ closeAttempt;
2631
3316
  constructor(options) {
2632
3317
  this.sessionRef = options.sessionRef;
2633
3318
  this.command = options.command;
@@ -2638,11 +3323,36 @@ var CodexSession = class {
2638
3323
  this.recordUnmapped = options.recordUnmapped;
2639
3324
  this.preparedGit = options.preparedGit;
2640
3325
  this.modelId = options.modelId;
3326
+ this.terminal = options.terminal;
2641
3327
  this.currentRunner = options.initialRunner;
3328
+ this.ownedRunners.add(options.initialRunner);
2642
3329
  void this.forgetRunnerOnceClosed(options.initialRunner);
2643
3330
  }
2644
3331
  get events() {
2645
- return this.queue;
3332
+ const queue = this.queue;
3333
+ const session = this;
3334
+ return {
3335
+ [Symbol.asyncIterator]() {
3336
+ const inner = queue[Symbol.asyncIterator]();
3337
+ return {
3338
+ async next() {
3339
+ let result;
3340
+ try {
3341
+ result = await inner.next();
3342
+ } catch (cause) {
3343
+ throw new RuntimeExecutionFailure({
3344
+ phase: "run",
3345
+ category: "infrastructure",
3346
+ retry: "retryable",
3347
+ reason: "codex runtime event transport failed"
3348
+ }, { cause });
3349
+ }
3350
+ if (result.done && session.terminal.failure) throw session.terminal.failure;
3351
+ return result;
3352
+ }
3353
+ };
3354
+ }
3355
+ };
2646
3356
  }
2647
3357
  async forgetRunnerOnceClosed(runner) {
2648
3358
  await runner.waitClosed();
@@ -2687,7 +3397,14 @@ var CodexSession = class {
2687
3397
  * stale id even after codex had moved on) — it just can now only ever be
2688
3398
  * the SAME id this call asked to resume, never a silently-different one.
2689
3399
  */
2690
- async followUp(task) {
3400
+ followUp(task) {
3401
+ const attempt = this.runFollowUp(task);
3402
+ this.followUpAttempts.add(attempt);
3403
+ void attempt.finally(() => this.followUpAttempts.delete(attempt)).catch(() => {
3404
+ });
3405
+ return attempt;
3406
+ }
3407
+ async runFollowUp(task) {
2691
3408
  if (typeof task.instruction !== "string") {
2692
3409
  throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
2693
3410
  }
@@ -2698,7 +3415,7 @@ var CodexSession = class {
2698
3415
  if (!mapping.ok) {
2699
3416
  throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
2700
3417
  }
2701
- const requestedModel = subscriptionModel2(task);
3418
+ const requestedModel = subscriptionModel2(task.dispatchSelection);
2702
3419
  if (requestedModel !== void 0 && requestedModel !== this.modelId) {
2703
3420
  throw new PolicyUnsupportedError(
2704
3421
  `codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
@@ -2708,6 +3425,7 @@ var CodexSession = class {
2708
3425
  const resumeRef = this.sessionRef;
2709
3426
  let sessionRef;
2710
3427
  let runner;
3428
+ const terminal = {};
2711
3429
  try {
2712
3430
  ({ sessionRef, runner } = await runCodexTurn({
2713
3431
  command: this.command,
@@ -2722,25 +3440,54 @@ var CodexSession = class {
2722
3440
  queue: this.queue,
2723
3441
  recordUnmapped: this.recordUnmapped,
2724
3442
  expectedSessionRef: resumeRef,
2725
- preparedGit: this.preparedGit
3443
+ preparedGit: this.preparedGit,
3444
+ failurePhase: "run",
3445
+ terminal,
3446
+ onRunnerCreated: (created) => {
3447
+ this.ownedRunners.add(created);
3448
+ void this.forgetRunnerOnceClosed(created);
3449
+ }
2726
3450
  }));
2727
3451
  } catch (err) {
2728
3452
  this.queue.end();
3453
+ this.terminal = {
3454
+ failure: isRuntimeExecutionFailure(err) ? err : new RuntimeExecutionFailure({
3455
+ phase: "run",
3456
+ category: "authority",
3457
+ retry: "non-retryable",
3458
+ reason: "codex follow-up violated the runtime adapter contract"
3459
+ }, { cause: err })
3460
+ };
2729
3461
  throw err;
2730
3462
  }
3463
+ if (this.closed) {
3464
+ await runner.dispose();
3465
+ throw new Error("codex session closed while follow-up was starting");
3466
+ }
3467
+ this.terminal = terminal;
2731
3468
  this.sessionRef = sessionRef;
2732
3469
  this.currentRunner = runner;
2733
- void this.forgetRunnerOnceClosed(runner);
2734
3470
  }
2735
3471
  /** Best-effort abort of the current turn. SIGTERM's the currently-running child, if any — see `process-runner.ts`'s `kill()` doc comment for why SIGTERM (not SIGINT) and why this is safe: the underlying codex thread survives and stays resumable, confirmed empirically. A no-op when no turn is currently in flight. */
2736
3472
  async interrupt() {
2737
3473
  this.currentRunner?.kill();
2738
3474
  }
2739
3475
  async close() {
2740
- if (this.closed) return;
2741
- this.closed = true;
2742
- this.currentRunner?.kill();
2743
- this.queue.end();
3476
+ if (!this.closeAttempt) {
3477
+ this.closed = true;
3478
+ this.queue.end();
3479
+ const attempt = (async () => {
3480
+ const runners = [...this.ownedRunners];
3481
+ await Promise.all(runners.map((runner) => runner.dispose()));
3482
+ for (const runner of runners) this.ownedRunners.delete(runner);
3483
+ await Promise.allSettled([...this.followUpAttempts]);
3484
+ })();
3485
+ this.closeAttempt = attempt.catch((error) => {
3486
+ this.closeAttempt = void 0;
3487
+ throw error;
3488
+ });
3489
+ }
3490
+ await this.closeAttempt;
2744
3491
  }
2745
3492
  /**
2746
3493
  * `codex exec` has no in-band channel to inject text into an already-
@@ -2776,8 +3523,7 @@ var CodexSession = class {
2776
3523
  );
2777
3524
  }
2778
3525
  };
2779
- function subscriptionModel2(task) {
2780
- const selection = task.dispatchSelection;
3526
+ function subscriptionModel2(selection) {
2781
3527
  if (selection === void 0) return void 0;
2782
3528
  if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
2783
3529
  throw new PolicyUnsupportedError(
@@ -2842,7 +3588,7 @@ var ApprovalRegistry = class {
2842
3588
  * `requestApproval` timeout and `finish()` fail-closed cleanup
2843
3589
  * (`task-runner.ts`) — resolves a decision this device made on its own.
2844
3590
  * The one exception, a server-sent wire `task.approve`/`task.reject`
2845
- * relayed through `TaskContext.approvalChannel.resolve`
3591
+ * relayed through `RuntimeOperationStartInput.approvalChannel.resolve`
2846
3592
  * (`task-runner.ts`'s `handleOffer`), passes `'wire'` explicitly.
2847
3593
  */
2848
3594
  resolve(approvalId, decision, reason, origin = "local") {
@@ -3473,7 +4219,10 @@ var PresencePublisher = class {
3473
4219
  {
3474
4220
  method: "PUT",
3475
4221
  headers: { "content-type": "application/json" },
3476
- body: JSON.stringify({ level: "online" })
4222
+ body: JSON.stringify({
4223
+ level: "online",
4224
+ ...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets }
4225
+ })
3477
4226
  },
3478
4227
  this.opts.auth
3479
4228
  );
@@ -3667,12 +4416,12 @@ var AnotherControlServerRunningError = class extends Error {
3667
4416
  }
3668
4417
  };
3669
4418
  var MAX_HALF_OPEN_CONNECTIONS = 8;
3670
- function errorMessage2(err) {
4419
+ function errorMessage3(err) {
3671
4420
  return err instanceof Error ? err.message : String(err);
3672
4421
  }
3673
4422
  function toControlErrorShape(err) {
3674
4423
  if (err instanceof ControlError) return { code: err.code, message: err.message };
3675
- return { code: "internal_error", message: errorMessage2(err) };
4424
+ return { code: "internal_error", message: errorMessage3(err) };
3676
4425
  }
3677
4426
  function probeUnixSocketAlive(socketPath) {
3678
4427
  return new Promise((resolve) => {
@@ -3879,12 +4628,19 @@ async function startControlServer(opts) {
3879
4628
  }
3880
4629
  throw err;
3881
4630
  }
4631
+ let stopServingPromise;
4632
+ async function stopServing() {
4633
+ stopServingPromise ??= (async () => {
4634
+ for (const socket of sockets) socket.destroy();
4635
+ await new Promise((resolve) => server.close(() => resolve()));
4636
+ })();
4637
+ await stopServingPromise;
4638
+ }
3882
4639
  let closed = false;
3883
4640
  async function close() {
3884
4641
  if (closed) return;
3885
4642
  closed = true;
3886
- for (const socket of sockets) socket.destroy();
3887
- await new Promise((resolve) => server.close(() => resolve()));
4643
+ await stopServing();
3888
4644
  if (process.platform !== "win32") {
3889
4645
  await promises.rm(endpoint, { force: true }).catch(() => {
3890
4646
  });
@@ -3892,7 +4648,7 @@ async function startControlServer(opts) {
3892
4648
  await promises.rm(tokenPath, { force: true }).catch(() => {
3893
4649
  });
3894
4650
  }
3895
- return { endpoint, close };
4651
+ return { endpoint, stopServing, close };
3896
4652
  }
3897
4653
  function deterministicJitterMs(input) {
3898
4654
  const ratio = input.ratio ?? 0.2;
@@ -4186,6 +4942,7 @@ var WsTransport = class {
4186
4942
  deviceId: this.opts.deviceId,
4187
4943
  productId: this.opts.productId,
4188
4944
  runtimes: this.opts.runtimes,
4945
+ configuredToolsets: this.opts.configuredToolsets === void 0 ? void 0 : [...this.opts.configuredToolsets],
4189
4946
  cursor: this.opts.getCursor?.()
4190
4947
  });
4191
4948
  socket.send(encodeEnvelope(hello));
@@ -4275,6 +5032,7 @@ var ConnectionManager = class {
4275
5032
  productId: opts.productId,
4276
5033
  capabilities: opts.capabilities,
4277
5034
  runtimes: opts.runtimes,
5035
+ configuredToolsets: opts.configuredToolsets,
4278
5036
  getCursor: () => this.cursor,
4279
5037
  onEnvelope: (envelope) => this.deliver(envelope),
4280
5038
  onStateChange: (state) => {
@@ -4397,7 +5155,7 @@ var ConnectionManager = class {
4397
5155
  * already-delivered seqs (see its own doc comment) so the failed seq's own
4398
5156
  * redelivery can get through — but that same frozen watermark also means
4399
5157
  * every OTHER seq above it rides along on every re-poll too. Without this,
4400
- * a seq already mid-flight (e.g. a `task.offer` whose `adapter.start()`
5158
+ * a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
4401
5159
  * hasn't resolved yet) would be re-enqueued into `processingChain` on
4402
5160
  * every such re-poll, piling up duplicate copies that — once the first
4403
5161
  * finally resolves and the chain unwinds through them — run its handler
@@ -5273,6 +6031,12 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
5273
6031
  var MAX_OWNER_BYTES = 4096;
5274
6032
  var RECLAIM_MALFORMED_GRACE_MS = 3e4;
5275
6033
  var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
6034
+ function endOwnershipProbe(socket, response) {
6035
+ socket.on("error", () => {
6036
+ });
6037
+ if (response === void 0) socket.end();
6038
+ else socket.end(response);
6039
+ }
5276
6040
  var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
5277
6041
  var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
5278
6042
  var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
@@ -5379,7 +6143,7 @@ async function portIsBound(port) {
5379
6143
  });
5380
6144
  }
5381
6145
  async function createLivenessListener() {
5382
- const server = createServer((socket) => socket.end());
6146
+ const server = createServer((socket) => endOwnershipProbe(socket));
5383
6147
  const port = await new Promise((resolve, reject) => {
5384
6148
  server.once("error", reject);
5385
6149
  server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
@@ -5464,7 +6228,7 @@ async function acquireStoreMutex(canonicalStoreDir) {
5464
6228
  }
5465
6229
  await clearStaleStoreMutexSocket(endpoint, identity);
5466
6230
  }
5467
- const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
6231
+ const server = createServer((socket) => endOwnershipProbe(socket, `${STORE_MUTEX_ID_PREFIX}${identity}
5468
6232
  `));
5469
6233
  try {
5470
6234
  await new Promise((resolve, reject) => {
@@ -5606,7 +6370,7 @@ function toRuntimeInfoCapabilities(caps) {
5606
6370
  resume: caps.resume,
5607
6371
  approvalInteractive: caps.approvalInteractive,
5608
6372
  ...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
5609
- permissionModes: caps.permissionModes
6373
+ permissionModes: [...caps.permissionModes]
5610
6374
  };
5611
6375
  }
5612
6376
  var CursorStore = class {
@@ -5846,6 +6610,9 @@ var DaemonObserver = class {
5846
6610
  noteGitWorkspace(event) {
5847
6611
  this.emit({ kind: "git-workspace", ts: nowIso(), ...event });
5848
6612
  }
6613
+ noteRuntimeDisposalFailure(event) {
6614
+ this.emit({ kind: "runtime-disposal-failed", ts: nowIso(), ...event });
6615
+ }
5849
6616
  /**
5850
6617
  * Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
5851
6618
  * synchronously by `TaskRunner.dispatchApproval` BEFORE its own
@@ -7499,20 +8266,20 @@ function isKnownRuntimeId(id) {
7499
8266
  var DEFAULT_RUNTIME_PREFERENCE = ["claude", "codex", "pi"];
7500
8267
  function orderByPreference(candidates, preference) {
7501
8268
  const rank = new Map(preference.map((id, index) => [id, index]));
7502
- return [...candidates].sort((a, b) => (rank.get(a.id) ?? preference.length) - (rank.get(b.id) ?? preference.length));
8269
+ return [...candidates].sort((a, b) => (rank.get(a.descriptor.id) ?? preference.length) - (rank.get(b.descriptor.id) ?? preference.length));
7503
8270
  }
7504
- function adapterSupportsMode(adapter, mode) {
7505
- return adapter.capabilities().permissionModes.includes(mode);
8271
+ function adapterSupportsMode(descriptor, mode) {
8272
+ return descriptor.capabilities.permissionModes.includes(mode);
7506
8273
  }
7507
- function adapterSupportsMcpToolsets(adapter) {
7508
- return adapter.capabilities().mcpToolsets === true;
8274
+ function adapterSupportsMcpToolsets(descriptor) {
8275
+ return descriptor.capabilities.mcpToolsets === true;
7509
8276
  }
7510
8277
  function withoutRequiredToolsets(payload) {
7511
8278
  if (!("requiredToolsets" in payload)) return payload;
7512
8279
  const { requiredToolsets, ...offer } = payload;
7513
8280
  return offer;
7514
8281
  }
7515
- function errorMessage3(err) {
8282
+ function errorMessage4(err) {
7516
8283
  return err instanceof Error ? err.message : String(err);
7517
8284
  }
7518
8285
  function raceSettleFirst(fn, timeoutMs) {
@@ -7565,7 +8332,7 @@ async function openArtifact(workspaceDir, name) {
7565
8332
  try {
7566
8333
  handle = await promises.open(candidate, constants.O_RDONLY | O_NOFOLLOW);
7567
8334
  } catch (err) {
7568
- return { ok: false, reason: `artifact "${name}" could not be opened: ${errorMessage3(err)}` };
8335
+ return { ok: false, reason: `artifact "${name}" could not be opened: ${errorMessage4(err)}` };
7569
8336
  }
7570
8337
  try {
7571
8338
  const st = await handle.stat();
@@ -7577,7 +8344,7 @@ async function openArtifact(workspaceDir, name) {
7577
8344
  } catch (err) {
7578
8345
  await handle.close().catch(() => {
7579
8346
  });
7580
- return { ok: false, reason: `artifact "${name}" could not be verified: ${errorMessage3(err)}` };
8347
+ return { ok: false, reason: `artifact "${name}" could not be verified: ${errorMessage4(err)}` };
7581
8348
  }
7582
8349
  return { ok: true, handle };
7583
8350
  }
@@ -7591,13 +8358,13 @@ var TaskRunner = class {
7591
8358
  * Finding F4 (cancel lost during the offer-processing window): a
7592
8359
  * `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
7593
8360
  * awaiting adapter detection / instruction resolution / workspace setup /
7594
- * `adapter.start()`) has no `this.tasks` entry to land on — it used to be
8361
+ * prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
7595
8362
  * silently dropped, and the runtime session `handleOffer` was about to
7596
8363
  * register would then run an unsupervised ("zombie") turn nobody asked
7597
8364
  * for anymore. Recording the taskId here lets `handleOffer` consult it at
7598
8365
  * the two points where it can still safely react (see its body): before
7599
8366
  * claiming at all (decline instead of ever starting a session), and right
7600
- * after `adapter.start()` resolves but before this task is registered as
8367
+ * after the prepared operation resolves but before this task is registered as
7601
8368
  * active (tear the just-started session down immediately, before its
7602
8369
  * event loop ever pumps a single event). Consumed (deleted) at whichever
7603
8370
  * checkpoint handles it; a cancel for a taskId that's already active,
@@ -7623,12 +8390,12 @@ var TaskRunner = class {
7623
8390
  * checkpoint-2 cancel-teardown, or successful registration into
7624
8391
  * `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
7625
8392
  * remove an entry for a taskId in this set: doing so is exactly the bug —
7626
- * block task A in `adapter.start()`, deliver A's own `task.cancel` (so
8393
+ * block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
7627
8394
  * `pendingCancelled` gets an entry for A while A is still in-flight),
7628
8395
  * then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
7629
8396
  * nobody ever offered — under naive oldest-wins eviction, A's entry (the
7630
8397
  * single oldest) gets evicted purely because of unrelated churn, so when
7631
- * `adapter.start()` finally resolves, checkpoint 2 finds no cancel marker
8398
+ * the prepared operation finally resolves, checkpoint 2 finds no cancel marker
7632
8399
  * and the already-cancelled task starts a real session. See
7633
8400
  * `evictPendingCancelled` below for the fix, and
7634
8401
  * `task-runner-bounded-collections.test.ts` for a test mirroring this
@@ -7648,7 +8415,7 @@ var TaskRunner = class {
7648
8415
  * explicitly relies on redelivered handlers being idempotent for exactly
7649
8416
  * this reason). `handleOffer` must treat a redelivered offer for a taskId
7650
8417
  * that's already active (`this.tasks`) or already finished (this set) as
7651
- * a no-op — never a second `adapter.start()` call, which would orphan the
8418
+ * a no-op — never a second prepared-operation `start()` call, which would orphan the
7652
8419
  * first session.
7653
8420
  *
7654
8421
  * M3-B: unbounded otherwise — a long-lived daemon that's finished many
@@ -7717,10 +8484,9 @@ var TaskRunner = class {
7717
8484
  this.stoppingOffers = true;
7718
8485
  }
7719
8486
  /**
7720
- * M4 Phase 2: best-effort shutdown of every currently ACTIVE task, for the
7721
- * control socket's `shutdown` RPC. Mirrors `handleCancel`'s best-effort
7722
- * `session.interrupt()` style (an interrupt failure is swallowed; the
7723
- * terminal message is sent either way) but reports `task.fail` rather than
8487
+ * Shutdown of every currently ACTIVE task for the control socket's
8488
+ * `shutdown` RPC. Soft interrupt remains bounded, but each task's
8489
+ * authoritative close receipt must settle successfully. Reports `task.fail` rather than
7724
8490
  * `task.cancelled` — these tasks aren't ending because the SERVER
7725
8491
  * cancelled them, they're ending because this device is shutting down.
7726
8492
  * `retryable: true` throughout: nothing about the task/policy itself was
@@ -7774,28 +8540,13 @@ var TaskRunner = class {
7774
8540
  * unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
7775
8541
  * never block `task.fail` from being sent at all.
7776
8542
  *
7777
- * New in this batch hard-kill escalation: when `interrupt()` does NOT
7778
- * settle within that same grace window, `session.close()` is tried next
7779
- * (ALSO raced against `timeoutMs`, for the identical reason: a hung
7780
- * `close()` must not be able to block this forever either — which matters
7781
- * far more here than it used to for the pre-existing graceful-shutdown-only
7782
- * caller, since THAT path is additionally bounded by an outer deadline
7783
- * (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
7784
- * `create-daemon.ts`), while resource-limit enforcement fires during
7785
- * ordinary operation with no such outer bound watching it). `close()` is
7786
- * every adapter's harder teardown primitive — an actual process-level kill
7787
- * (SIGTERM, or `taskkill /F` on Windows — see e.g.
7788
- * `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
7789
- * soft in-band `interrupt()` (an RPC `abort` message that leaves the
7790
- * process alive and resumable) — so escalating to it is the closest thing
7791
- * to a "hard kill" the `Session` interface exposes. `finish()` below calls
7792
- * `session.close()` again regardless (documented idempotent) — this isn't
7793
- * a substitute for that, only an earlier, bounded attempt at actually
7794
- * stopping a stuck runtime before this method gives up and reports failure
7795
- * anyway.
8543
+ * After the bounded soft interrupt, `finish()` always awaits the authoritative
8544
+ * `Session.close()` receipt. A failed receipt retains active/Git ownership;
8545
+ * shutdown surfaces the rejection while resource enforcement leaves local
8546
+ * evidence for a later retry.
7796
8547
  *
7797
8548
  * Re-checks task identity (`this.tasks.get(...) === active`) immediately
7798
- * before sending `task.fail`: the interrupt/hard-kill race above has await
8549
+ * before sending `task.fail`: the interrupt race above has await
7799
8550
  * points during which a DIFFERENT path (a racing `task.cancel`/
7800
8551
  * `task.reject`, or the session completing normally on its own) may have
7801
8552
  * already finished this exact task and sent its own terminal message.
@@ -7804,20 +8555,25 @@ var TaskRunner = class {
7804
8555
  * identity-check guard for the same class of race.
7805
8556
  */
7806
8557
  async teardownActiveTask(active, reason, retryable) {
8558
+ if (active.finalizationStarted) return this.finish(active.taskId);
8559
+ if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
7807
8560
  active.beingTornDown = true;
7808
8561
  await this.observeGit(active, "salvage");
7809
8562
  const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
7810
- const interrupted = await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
7811
- if (!interrupted) {
7812
- await raceSettleFirst(() => active.session.close(), timeoutMs);
7813
- }
7814
- if (this.tasks.get(active.taskId) !== active) return;
8563
+ await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
8564
+ if (this.tasks.get(active.taskId) !== active) return true;
7815
8565
  this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId: active.taskId }));
7816
- await this.finish(active.taskId);
8566
+ return this.finish(active.taskId);
7817
8567
  }
7818
8568
  /** Graceful-shutdown caller of {@link teardownActiveTask} — see `shutdownActiveTasks`'s own doc comment. `retryable: true`: nothing about the task/policy itself was ever at fault, only this device's own availability right now. */
7819
8569
  async shutdownTask(active, reason) {
7820
- await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
8570
+ const disposed = await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
8571
+ if (!disposed) {
8572
+ throw new RuntimeDisposalFailure({
8573
+ stage: "quiescence",
8574
+ reason: `${active.adapter.descriptor.id} runtime ownership remains active after shutdown disposal failed`
8575
+ });
8576
+ }
7821
8577
  }
7822
8578
  /**
7823
8579
  * M5 batch-3 (workstream 2): shared entry point for both resource-limit
@@ -7937,15 +8693,33 @@ var TaskRunner = class {
7937
8693
  this.decline(taskId, resolvedMcp.reason, true);
7938
8694
  return;
7939
8695
  }
8696
+ const decision = computeEffectivePolicy(payload.policy, this.deps.permissionDefaults);
8697
+ if (!decision.ok) {
8698
+ this.decline(taskId, decision.reason ?? "policy rejected", false);
8699
+ return;
8700
+ }
8701
+ const offered = withoutRequiredToolsets(payload);
7940
8702
  const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
7941
8703
  const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
7942
8704
  if (!pick.ok) {
7943
8705
  this.decline(taskId, pick.reason, pick.retryable);
7944
8706
  return;
7945
8707
  }
7946
- const decision = computeEffectivePolicy(payload.policy, this.deps.permissionDefaults);
7947
- if (!decision.ok) {
7948
- this.decline(taskId, decision.reason ?? "policy rejected", false);
8708
+ let prepared;
8709
+ try {
8710
+ prepared = await pick.adapter.prepare({
8711
+ offer: offered,
8712
+ policy: decision.policy,
8713
+ descriptor: pick.descriptor,
8714
+ requiredToolsetIds: requiredToolsets ?? [],
8715
+ ...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {}
8716
+ });
8717
+ } catch (error) {
8718
+ this.decline(taskId, `runtime preparation failed: ${errorMessage4(error)}`, true);
8719
+ return;
8720
+ }
8721
+ if (prepared.kind === "reject") {
8722
+ this.decline(taskId, prepared.reason, prepared.retryable);
7949
8723
  return;
7950
8724
  }
7951
8725
  let known = void 0;
@@ -7980,6 +8754,7 @@ var TaskRunner = class {
7980
8754
  }
7981
8755
  } else {
7982
8756
  workspaceDir = path20.join(this.deps.workspaceRoot, taskId);
8757
+ gitWorkspaceId = randomUUID();
7983
8758
  }
7984
8759
  try {
7985
8760
  gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
@@ -7995,6 +8770,26 @@ var TaskRunner = class {
7995
8770
  this.decline(taskId, "workspace mode is unavailable", true);
7996
8771
  return;
7997
8772
  }
8773
+ const env = buildRuntimeEnv({
8774
+ ambient: process.env,
8775
+ requirements: pick.descriptor.environmentRequirements,
8776
+ locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.descriptor.id]?.allow
8777
+ });
8778
+ const manifest = sealRuntimeOperationManifest({
8779
+ taskId,
8780
+ runtimeId: pick.descriptor.id,
8781
+ descriptor: pick.descriptor,
8782
+ policy: decision.policy,
8783
+ requiredToolsetIds: requiredToolsets ?? [],
8784
+ ...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
8785
+ ...known === void 0 || payload.sessionRef === void 0 ? {} : { sessionRef: payload.sessionRef },
8786
+ workspace: {
8787
+ workspaceDir,
8788
+ ...gitWorkspaceId === void 0 ? {} : { workspaceId: gitWorkspaceId },
8789
+ ...gitBaseline === void 0 ? {} : { baseline: gitBaseline }
8790
+ },
8791
+ forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
8792
+ });
7998
8793
  this.deps.send(
7999
8794
  createEnvelope(
8000
8795
  "task.claim",
@@ -8010,7 +8805,7 @@ var TaskRunner = class {
8010
8805
  // (the merely REQUESTED runtime): this is what closes the gap
8011
8806
  // where an auto-selected task left the server never learning
8012
8807
  // which runtime actually ran.
8013
- runtime: isKnownRuntimeId(pick.adapter.id) ? pick.adapter.id : void 0,
8808
+ runtime: isKnownRuntimeId(manifest.descriptor.id) ? manifest.descriptor.id : void 0,
8014
8809
  // S0/D-4 (`task.claim.capabilities`, docs/protocol.md §2.4): the
8015
8810
  // selected adapter's own capability self-report, carried on the
8016
8811
  // same message that establishes the task↔runtime binding. The
@@ -8027,7 +8822,7 @@ var TaskRunner = class {
8027
8822
  // Gating them would silently strip a custom steer-capable
8028
8823
  // adapter's own truth and leave the server fail-closing on it
8029
8824
  // forever.
8030
- capabilities: toRuntimeInfoCapabilities(pick.adapter.capabilities())
8825
+ capabilities: toRuntimeInfoCapabilities(manifest.descriptor.capabilities)
8031
8826
  },
8032
8827
  { taskId }
8033
8828
  )
@@ -8038,7 +8833,7 @@ var TaskRunner = class {
8038
8833
  if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
8039
8834
  } catch (err) {
8040
8835
  gitLease?.release();
8041
- await this.fail(taskId, `failed to resolve instruction blob: ${errorMessage3(err)}`, true);
8836
+ await this.fail(taskId, `failed to resolve instruction blob: ${errorMessage4(err)}`, true);
8042
8837
  return;
8043
8838
  }
8044
8839
  if (this.deps.gitWorkspaceManager && gitLease) {
@@ -8047,7 +8842,7 @@ var TaskRunner = class {
8047
8842
  if (gitExisting) {
8048
8843
  observation = await this.deps.gitWorkspaceManager.validateExisting(workspaceDir);
8049
8844
  } else {
8050
- const workspaceId2 = randomUUID();
8845
+ const workspaceId2 = gitWorkspaceId ?? randomUUID();
8051
8846
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
8052
8847
  gitWorkspaceId = workspaceId2;
8053
8848
  await this.deps.gitWorkspaceStore?.upsert({
@@ -8098,30 +8893,11 @@ var TaskRunner = class {
8098
8893
  return;
8099
8894
  }
8100
8895
  }
8101
- const ctx = {
8102
- workspaceDir,
8103
- policy: decision.policy,
8896
+ const startInput = {
8897
+ manifest,
8898
+ instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
8899
+ env,
8104
8900
  ...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
8105
- ...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
8106
- // M5: no longer `process.env` verbatim (see `environment.ts`'s own
8107
- // module doc comment for the credential-leak gap that closed) —
8108
- // built fresh per task from the SPECIFIC adapter `pickAdapter`
8109
- // above already selected, so this always runs after adapter
8110
- // selection: `pick.adapter.environmentRequirements?.()` (undefined
8111
- // ⇒ platform baseline only, fail-closed) plus this device's own
8112
- // `runtimeEnvironment` override, keyed by that same adapter's `id`.
8113
- env: buildRuntimeEnv({
8114
- ambient: process.env,
8115
- requirements: pick.adapter.environmentRequirements?.(),
8116
- locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.adapter.id]?.allow
8117
- }),
8118
- // M4 Phase 3: adapter-agnostic and cheap to always populate — only an
8119
- // adapter whose runtime genuinely supports an out-of-band approval
8120
- // pause (claude, today) ever reads this. `resolve` is a closure over
8121
- // `taskId` (not a pre-bound approvalId): it looks up whichever
8122
- // approval is CURRENTLY pending for this task at call time, since one
8123
- // task/session can face several approval requests, one at a time,
8124
- // over its life. See `types.ts`'s `ApprovalChannel` doc comment.
8125
8901
  approvalChannel: {
8126
8902
  taskId,
8127
8903
  storeDir: this.deps.storeDir,
@@ -8137,45 +8913,19 @@ var TaskRunner = class {
8137
8913
  }
8138
8914
  }
8139
8915
  };
8140
- const effectiveOffer = {
8141
- ...withoutRequiredToolsets(payload),
8142
- instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
8143
- // Never forward a sessionRef this device has no recorded workspace
8144
- // for (stale, from another device, or simply made up) — an adapter
8145
- // that tries to resume an id it never minted fails outright (pi:
8146
- // "No session found matching '<id>'", exit 1, empirically confirmed)
8147
- // instead of silently starting fresh, so an unresolvable sessionRef
8148
- // must look identical to "none supplied" by the time it reaches the
8149
- // adapter, not get forwarded as a resume attempt doomed to fail.
8150
- sessionRef: known ? payload.sessionRef : void 0
8151
- };
8152
8916
  let session;
8153
8917
  try {
8154
- session = await pick.adapter.start(effectiveOffer, ctx);
8918
+ session = await prepared.operation.start(startInput);
8155
8919
  } catch (err) {
8156
- const retryable = !(err instanceof PolicyUnsupportedError);
8157
- await this.updateGitPhaseBestEffort(gitWorkspaceId, "failed", "repository-invalid");
8158
- gitLease?.release();
8159
- await this.fail(taskId, `adapter failed to start: ${errorMessage3(err)}`, retryable);
8160
- return;
8161
- }
8162
- if (this.pendingCancelled.has(taskId)) {
8163
- const reason = this.pendingCancelled.get(taskId);
8164
- this.pendingCancelled.delete(taskId);
8165
- try {
8166
- await session.interrupt();
8167
- } catch {
8168
- }
8169
- try {
8170
- await session.close();
8171
- } catch {
8920
+ const failure = projectRuntimeBoundaryFailure(err, "start");
8921
+ if (failure.contractViolation) {
8922
+ console.error("[byok/client] runtime adapter start() returned an untyped failure", err);
8172
8923
  }
8924
+ await this.updateGitPhaseBestEffort(gitWorkspaceId, "failed", "repository-invalid");
8173
8925
  gitLease?.release();
8174
- await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
8175
- this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
8926
+ await this.fail(taskId, failure.reason, failure.retryable);
8176
8927
  return;
8177
8928
  }
8178
- this.deps.send(createEnvelope("task.started", {}, { taskId }));
8179
8929
  const active = {
8180
8930
  taskId,
8181
8931
  adapter: pick.adapter,
@@ -8192,6 +8942,21 @@ var TaskRunner = class {
8192
8942
  approvalQueue: [],
8193
8943
  outputBytesSoFar: 0
8194
8944
  };
8945
+ if (this.pendingCancelled.has(taskId)) {
8946
+ const reason = this.pendingCancelled.get(taskId);
8947
+ this.pendingCancelled.delete(taskId);
8948
+ this.tasks.set(taskId, active);
8949
+ this.reserveSemanticTerminal(active);
8950
+ try {
8951
+ await session.interrupt();
8952
+ } catch {
8953
+ }
8954
+ await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
8955
+ this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
8956
+ await this.finish(taskId);
8957
+ return;
8958
+ }
8959
+ this.deps.send(createEnvelope("task.started", {}, { taskId }));
8195
8960
  this.tasks.set(taskId, active);
8196
8961
  if (payload.limits?.maxDurationMs !== void 0) {
8197
8962
  this.armMaxDurationTimer(active, payload.limits.maxDurationMs);
@@ -8249,6 +9014,7 @@ var TaskRunner = class {
8249
9014
  async pump(active) {
8250
9015
  try {
8251
9016
  for await (const event of active.session.events) {
9017
+ if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
8252
9018
  if (this.tasks.get(active.taskId) !== active) return;
8253
9019
  active.outputBytesSoFar += estimateEventBytes(event);
8254
9020
  if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
@@ -8271,7 +9037,7 @@ var TaskRunner = class {
8271
9037
  try {
8272
9038
  await active.session.resolveApproval(approved, reason);
8273
9039
  } catch (err) {
8274
- await this.fail(taskId, `failed to resume session after approval decision: ${errorMessage3(err)}`, false);
9040
+ await this.fail(taskId, `failed to resume session after approval decision: ${errorMessage4(err)}`, false);
8275
9041
  }
8276
9042
  });
8277
9043
  continue;
@@ -8283,6 +9049,7 @@ var TaskRunner = class {
8283
9049
  const outcome = await this.resolveResultDocument(active, finalOutput);
8284
9050
  if (!outcome.deliver) return;
8285
9051
  await this.observeGit(active, "completed");
9052
+ if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
8286
9053
  if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
8287
9054
  await this.fail(
8288
9055
  active.taskId,
@@ -8291,6 +9058,7 @@ var TaskRunner = class {
8291
9058
  );
8292
9059
  return;
8293
9060
  }
9061
+ if (!this.reserveSemanticTerminal(active)) return;
8294
9062
  this.deps.send(
8295
9063
  createEnvelope(
8296
9064
  "task.complete",
@@ -8326,11 +9094,17 @@ var TaskRunner = class {
8326
9094
  active.batcher.push(event);
8327
9095
  }
8328
9096
  if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
8329
- await this.fail(active.taskId, "runtime session ended without completing the task", true);
9097
+ const failure = projectRuntimeBoundaryFailure(void 0, "run");
9098
+ console.error("[byok/client] runtime adapter events iterable ended without terminal authority");
9099
+ await this.fail(active.taskId, failure.reason, failure.retryable);
8330
9100
  } catch (err) {
8331
9101
  if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
8332
9102
  active.batcher.flush();
8333
- await this.fail(active.taskId, `runtime error: ${errorMessage3(err)}`, true);
9103
+ const failure = projectRuntimeBoundaryFailure(err, "run");
9104
+ if (failure.contractViolation) {
9105
+ console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
9106
+ }
9107
+ await this.fail(active.taskId, failure.reason, failure.retryable);
8334
9108
  }
8335
9109
  }
8336
9110
  /**
@@ -8369,7 +9143,7 @@ var TaskRunner = class {
8369
9143
  try {
8370
9144
  bytes = await opened.handle.readFile();
8371
9145
  } catch (err) {
8372
- this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage3(err)}`);
9146
+ this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage4(err)}`);
8373
9147
  return;
8374
9148
  } finally {
8375
9149
  await opened.handle.close().catch(() => {
@@ -8386,7 +9160,7 @@ var TaskRunner = class {
8386
9160
  const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
8387
9161
  this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
8388
9162
  } catch (err) {
8389
- this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage3(err)}`);
9163
+ this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage4(err)}`);
8390
9164
  }
8391
9165
  }
8392
9166
  /** Loud, non-silent artifact failure (finding F7): logged, and folded into this task's own progress stream as an `error` AgentEvent rather than swallowed — the task itself can still complete normally, but the omission is now visible. */
@@ -8400,6 +9174,14 @@ var TaskRunner = class {
8400
9174
  this.setPendingCancelled(taskId, reason);
8401
9175
  return;
8402
9176
  }
9177
+ if (active.finalizationStarted) {
9178
+ await this.finish(taskId);
9179
+ return;
9180
+ }
9181
+ if (!this.reserveSemanticTerminal(active)) {
9182
+ await active.semanticTerminalSettled;
9183
+ return;
9184
+ }
8403
9185
  try {
8404
9186
  await active.session.interrupt();
8405
9187
  } catch {
@@ -8428,7 +9210,7 @@ var TaskRunner = class {
8428
9210
  *
8429
9211
  * `inFlightOffers` is naturally tiny (bounded by this device's real
8430
9212
  * concurrent-offer-processing count — normally single digits, driven by
8431
- * how many `task.offer`s are simultaneously mid-`adapter.start()` — nowhere
9213
+ * how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
8432
9214
  * near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
8433
9215
  * finds a safe entry at or near the front almost always. The only case
8434
9216
  * where NO entry is safe to evict is every single tracked cancel
@@ -8781,7 +9563,7 @@ var TaskRunner = class {
8781
9563
  this.deps.onStaleApprovalDecision?.(taskId, "approve");
8782
9564
  return;
8783
9565
  }
8784
- await this.fail(taskId, `failed to resume session after approval: ${errorMessage3(err)}`, false);
9566
+ await this.fail(taskId, `failed to resume session after approval: ${errorMessage4(err)}`, false);
8785
9567
  return;
8786
9568
  }
8787
9569
  this.clearPendingApproval(resolvedId, "approve", void 0);
@@ -8812,6 +9594,10 @@ var TaskRunner = class {
8812
9594
  async handleReject(taskId, reason, approvalId) {
8813
9595
  const active = this.tasks.get(taskId);
8814
9596
  if (!active) return;
9597
+ if (active.finalizationStarted) {
9598
+ await this.finish(taskId);
9599
+ return;
9600
+ }
8815
9601
  if (approvalId !== void 0 && approvalId !== active.pendingApprovalId) {
8816
9602
  this.deps.onStaleApprovalDecision?.(
8817
9603
  taskId,
@@ -8830,6 +9616,10 @@ var TaskRunner = class {
8830
9616
  }
8831
9617
  }
8832
9618
  this.clearPendingApproval(resolvedId, "reject", reason);
9619
+ if (!this.reserveSemanticTerminal(active)) {
9620
+ await active.semanticTerminalSettled;
9621
+ return;
9622
+ }
8833
9623
  try {
8834
9624
  await active.session.interrupt();
8835
9625
  } catch {
@@ -8844,6 +9634,14 @@ var TaskRunner = class {
8844
9634
  }
8845
9635
  async fail(taskId, reason, retryable) {
8846
9636
  const active = this.tasks.get(taskId);
9637
+ if (active?.finalizationStarted) {
9638
+ await this.finish(taskId);
9639
+ return;
9640
+ }
9641
+ if (active && !this.reserveSemanticTerminal(active)) {
9642
+ await active.semanticTerminalSettled;
9643
+ return;
9644
+ }
8847
9645
  if (active) await this.observeGit(active, "salvage");
8848
9646
  this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
8849
9647
  await this.finish(taskId);
@@ -8903,7 +9701,7 @@ var TaskRunner = class {
8903
9701
  } catch (err) {
8904
9702
  await this.fail(
8905
9703
  active.taskId,
8906
- `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
9704
+ `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage4(err)}`,
8907
9705
  false
8908
9706
  );
8909
9707
  return { deliver: false };
@@ -8978,32 +9776,64 @@ var TaskRunner = class {
8978
9776
  }
8979
9777
  async finish(taskId) {
8980
9778
  const active = this.tasks.get(taskId);
8981
- if (!active) return;
8982
- if (active.maxDurationTimer) {
8983
- clearTimeout(active.maxDurationTimer);
8984
- active.maxDurationTimer = void 0;
8985
- }
8986
- active.batcher.stop();
8987
- this.tasks.delete(taskId);
8988
- this.addFinishedTaskId(taskId);
8989
- const queued = active.approvalQueue.splice(0);
8990
- for (const request of queued) {
8991
- request.resolve({
8992
- approved: false,
8993
- reason: `task ${taskId} finished before this queued approval request could be dispatched`
8994
- });
8995
- }
8996
- if (active.pendingApprovalId !== void 0) {
8997
- try {
8998
- this.deps.approvalRegistry.resolve(active.pendingApprovalId, "reject", `task ${taskId} finished`);
8999
- } catch {
9779
+ if (!active) return true;
9780
+ if (!active.finalizationStarted) {
9781
+ active.finalizationStarted = true;
9782
+ active.beingTornDown = true;
9783
+ if (active.maxDurationTimer) {
9784
+ clearTimeout(active.maxDurationTimer);
9785
+ active.maxDurationTimer = void 0;
9786
+ }
9787
+ active.batcher.stop();
9788
+ this.addFinishedTaskId(taskId);
9789
+ const queued = active.approvalQueue.splice(0);
9790
+ for (const request of queued) {
9791
+ request.resolve({
9792
+ approved: false,
9793
+ reason: `task ${taskId} finished before this queued approval request could be dispatched`
9794
+ });
9795
+ }
9796
+ if (active.pendingApprovalId !== void 0) {
9797
+ try {
9798
+ this.deps.approvalRegistry.resolve(active.pendingApprovalId, "reject", `task ${taskId} finished`);
9799
+ } catch {
9800
+ }
9000
9801
  }
9001
9802
  }
9002
- if (active.gitLease) active.gitLease.release();
9803
+ const attempt = active.disposalAttempt ?? active.session.close();
9804
+ active.disposalAttempt = attempt;
9003
9805
  try {
9004
- await active.session.close();
9005
- } catch {
9806
+ await attempt;
9807
+ } catch (caught) {
9808
+ if (active.disposalAttempt === attempt) active.disposalAttempt = void 0;
9809
+ const failure = isRuntimeDisposalFailure(caught) ? caught : new RuntimeDisposalFailure({
9810
+ stage: "quiescence",
9811
+ reason: `${active.adapter.descriptor.id} session.close() returned an untyped disposal failure`
9812
+ }, { cause: caught });
9813
+ console.error(`[byok/client] runtime disposal failed for task ${taskId}: ${failure.message}`);
9814
+ this.deps.onRuntimeDisposalFailure?.({
9815
+ taskId,
9816
+ runtimeId: active.adapter.descriptor.id,
9817
+ stage: failure.stage,
9818
+ reason: failure.message
9819
+ });
9820
+ active.resolveSemanticTerminalSettled?.(false);
9821
+ return false;
9006
9822
  }
9823
+ if (this.tasks.get(taskId) !== active) return true;
9824
+ active.gitLease?.release();
9825
+ this.tasks.delete(taskId);
9826
+ active.resolveSemanticTerminalSettled?.(true);
9827
+ return true;
9828
+ }
9829
+ reserveSemanticTerminal(active) {
9830
+ if (active.semanticTerminalReserved || active.finalizationStarted) return false;
9831
+ active.semanticTerminalReserved = true;
9832
+ active.beingTornDown = true;
9833
+ active.semanticTerminalSettled = new Promise((resolve) => {
9834
+ active.resolveSemanticTerminalSettled = resolve;
9835
+ });
9836
+ return true;
9007
9837
  }
9008
9838
  /** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
9009
9839
  addFinishedTaskId(taskId) {
@@ -9056,18 +9886,19 @@ var TaskRunner = class {
9056
9886
  retryable: false
9057
9887
  };
9058
9888
  }
9059
- const adapter = this.deps.adapters.find((a) => a.id === requestedRuntime);
9889
+ const adapter = this.deps.adapters.find((a) => a.descriptor.id === requestedRuntime);
9060
9890
  if (!adapter) {
9061
9891
  return { ok: false, reason: `unknown runtime "${requestedRuntime}"`, retryable: false };
9062
9892
  }
9063
- if (!adapterSupportsMode(adapter, policyMode)) {
9893
+ const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
9894
+ if (!adapterSupportsMode(descriptor, policyMode)) {
9064
9895
  return {
9065
9896
  ok: false,
9066
9897
  reason: `runtime "${requestedRuntime}" cannot express permission mode "${policyMode}"`,
9067
9898
  retryable: false
9068
9899
  };
9069
9900
  }
9070
- if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
9901
+ if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) {
9071
9902
  return {
9072
9903
  ok: false,
9073
9904
  reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
@@ -9082,15 +9913,16 @@ var TaskRunner = class {
9082
9913
  retryable: true
9083
9914
  };
9084
9915
  }
9085
- return { ok: true, adapter };
9916
+ return { ok: true, adapter, descriptor };
9086
9917
  }
9087
- const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.id)) : this.deps.adapters;
9918
+ const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.descriptor.id)) : this.deps.adapters;
9088
9919
  const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
9089
9920
  for (const adapter of candidates) {
9090
- if (!adapterSupportsMode(adapter, policyMode)) continue;
9091
- if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
9921
+ const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
9922
+ if (!adapterSupportsMode(descriptor, policyMode)) continue;
9923
+ if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) continue;
9092
9924
  const detected = await adapter.detect();
9093
- if (detected.present) return { ok: true, adapter };
9925
+ if (detected.present) return { ok: true, adapter, descriptor };
9094
9926
  }
9095
9927
  return {
9096
9928
  ok: false,
@@ -9135,27 +9967,27 @@ async function detectRuntimes(adapters) {
9135
9967
  const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: await adapter.detect() })));
9136
9968
  const runtimes = [];
9137
9969
  for (const { adapter, detected } of detections) {
9138
- if (!detected.present || !isRuntimeId(adapter.id)) continue;
9139
- const info = { id: adapter.id };
9970
+ if (!detected.present || !isRuntimeId(adapter.descriptor.id)) continue;
9971
+ const info = { id: adapter.descriptor.id };
9140
9972
  if (detected.version !== void 0) info.version = detected.version;
9141
9973
  if (detected.authPresent !== void 0) info.authPresent = detected.authPresent;
9142
- info.capabilities = toRuntimeInfoCapabilities(adapter.capabilities());
9974
+ info.capabilities = toRuntimeInfoCapabilities(adapter.descriptor.capabilities);
9143
9975
  runtimes.push(info);
9144
9976
  }
9145
9977
  return runtimes;
9146
9978
  }
9147
9979
  function computeCapabilities(adapters) {
9148
9980
  const flags = [];
9149
- if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
9981
+ if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
9150
9982
  flags.push("blob-upload");
9151
9983
  flags.push("approval-targeting");
9152
9984
  const selectionAdapters = adapters.filter(
9153
- (adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
9985
+ (adapter) => ALL_RUNTIME_IDS.includes(adapter.descriptor.id)
9154
9986
  );
9155
- if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
9987
+ if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
9156
9988
  flags.push("dispatch-selection");
9157
9989
  }
9158
- if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
9990
+ if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
9159
9991
  flags.push("toolset-selection");
9160
9992
  }
9161
9993
  return flags;
@@ -9215,7 +10047,6 @@ function validatePiByokLauncherConfig(launcher) {
9215
10047
  throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
9216
10048
  }
9217
10049
  }
9218
- var MAX_LOCAL_MCP_TOOLSETS = 64;
9219
10050
  var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
9220
10051
  var MAX_LOCAL_MCP_ARGS = 64;
9221
10052
  var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
@@ -9228,8 +10059,10 @@ function resolveMcpToolsets(configured) {
9228
10059
  throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
9229
10060
  }
9230
10061
  const toolsetEntries = Object.entries(configured);
9231
- if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
9232
- throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
10062
+ if (toolsetEntries.length > CONFIGURED_TOOLSETS_MAX_ITEMS) {
10063
+ throw new Error(
10064
+ `DaemonConfig.mcpToolsets may contain at most ${CONFIGURED_TOOLSETS_MAX_ITEMS} toolsets`
10065
+ );
9233
10066
  }
9234
10067
  const resolved = /* @__PURE__ */ new Map();
9235
10068
  for (const [toolsetId, rawToolset] of toolsetEntries) {
@@ -9342,6 +10175,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
9342
10175
  }
9343
10176
  function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
9344
10177
  const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
10178
+ const configuredToolsets = Object.freeze(
10179
+ [...mcpToolsets?.keys() ?? []].sort()
10180
+ );
9345
10181
  if (config.piByokLauncher !== void 0) {
9346
10182
  validatePiByokLauncherConfig(config.piByokLauncher);
9347
10183
  }
@@ -9648,12 +10484,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9648
10484
  dirty: event.observation ? { staged: event.observation.staged, unstaged: event.observation.unstaged, untracked: event.observation.untracked, conflicted: event.observation.conflicted } : void 0,
9649
10485
  errorCategory: event.errorCategory
9650
10486
  }),
10487
+ onRuntimeDisposalFailure: (event) => observer.noteRuntimeDisposalFailure(event),
9651
10488
  // M4 Phase 3: the SAME `ApprovalRegistry` instance the control
9652
10489
  // socket's own `approvals.list`/`approvals.resolve` methods already
9653
10490
  // share (see that field's own construction above) — `TaskRunner
9654
10491
  // .requestApproval` registers into it directly, so a decision arriving
9655
10492
  // via either the server wire or the local CLI resolves the identical
9656
- // entry. `storeDir`/`productId` let `TaskContext.approvalChannel`
10493
+ // entry. `storeDir`/`productId` let the prepared operation approval channel
9657
10494
  // (populated per-task by `TaskRunner`) tell an out-of-process helper
9658
10495
  // (`bin/byok-approval-mcp.ts`) exactly which control socket to dial.
9659
10496
  approvalRegistry,
@@ -9705,6 +10542,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9705
10542
  productId: config.productId,
9706
10543
  capabilities,
9707
10544
  runtimes,
10545
+ configuredToolsets,
9708
10546
  auth,
9709
10547
  cursorStore,
9710
10548
  // Finding F3: return (not void-and-forget) so ConnectionManager can
@@ -9791,6 +10629,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9791
10629
  presencePublisher ??= new PresencePublisher({
9792
10630
  serverUrl: config.serverUrl,
9793
10631
  auth,
10632
+ configuredToolsets,
9794
10633
  ...presenceCadence,
9795
10634
  onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
9796
10635
  });
@@ -9884,8 +10723,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9884
10723
  await attempt(() => auth.stop(), true);
9885
10724
  connectionState = "closed";
9886
10725
  await attempt(async () => {
9887
- await controlServerHandle?.close();
9888
- controlServerHandle = void 0;
10726
+ await controlServerHandle?.stopServing();
9889
10727
  }, true);
9890
10728
  if (mutationBarrierComplete && daemonOwnerLease) {
9891
10729
  await attempt(() => operationalHealth.markCleanStop(), false);
@@ -9894,6 +10732,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9894
10732
  daemonOwnerLease = void 0;
9895
10733
  }, false);
9896
10734
  }
10735
+ if (daemonOwnerLease === void 0) {
10736
+ await attempt(async () => {
10737
+ await controlServerHandle?.close();
10738
+ controlServerHandle = void 0;
10739
+ }, false);
10740
+ }
9897
10741
  if (errors.length === 1) throw errors[0];
9898
10742
  if (errors.length > 1) {
9899
10743
  throw new AggregateError(errors, "daemon shutdown completed with errors");
@@ -9973,7 +10817,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
9973
10817
  deviceId: auth.deviceId,
9974
10818
  transport: connectionState,
9975
10819
  activeTasks,
9976
- runtimeIds: adapters.map((adapter) => adapter.id),
10820
+ runtimeIds: adapters.map((adapter) => adapter.descriptor.id),
9977
10821
  // M4 Phase 4 (part B.3): queue watermarks come from TaskRunner's own
9978
10822
  // active-task map (distinct from `observer.tasks()` above, which is
9979
10823
  // derived from the envelope feed) — see `TaskRunner.getQueueWatermarks`'s
@@ -10181,7 +11025,7 @@ function createDaemon(config) {
10181
11025
  return createDaemonWithAdapters(config, buildDefaultAdapters(config));
10182
11026
  }
10183
11027
  var MAX_CONTROL_TOKEN_BYTES = 256;
10184
- function errorMessage4(err) {
11028
+ function errorMessage5(err) {
10185
11029
  return err instanceof Error ? err.message : String(err);
10186
11030
  }
10187
11031
  function sameFileState3(left, right) {
@@ -10234,7 +11078,7 @@ async function connectControlClient(opts) {
10234
11078
  }
10235
11079
  token = read;
10236
11080
  } catch (err) {
10237
- return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
11081
+ return { ok: false, reason: `could not read the control token: ${errorMessage5(err)}` };
10238
11082
  }
10239
11083
  if (!token) {
10240
11084
  return { ok: false, reason: "control token file is empty" };
@@ -10244,7 +11088,7 @@ async function connectControlClient(opts) {
10244
11088
  const client = await connectAndHandshake(endpoint, token, opts);
10245
11089
  return { ok: true, client };
10246
11090
  } catch (err) {
10247
- return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
11091
+ return { ok: false, reason: `daemon control socket not reachable: ${errorMessage5(err)}` };
10248
11092
  }
10249
11093
  }
10250
11094
  function connectAndHandshake(endpoint, token, opts) {
@@ -10863,19 +11707,7 @@ function quote(text) {
10863
11707
  function redactedByteCountPlaceholder(text) {
10864
11708
  return `[redacted: ${Buffer.byteLength(text, "utf8")} bytes]`;
10865
11709
  }
10866
- var STABLE_GIT_ERROR_CATEGORIES = /* @__PURE__ */ new Set([
10867
- "git-unavailable",
10868
- "git-timeout",
10869
- "git-output-limit",
10870
- "git-command-failed",
10871
- "workspace-root-invalid",
10872
- "workspace-root-conflict",
10873
- "workspace-not-owned",
10874
- "repository-root-mismatch",
10875
- "repository-invalid",
10876
- "lease-busy",
10877
- "ledger-invalid"
10878
- ]);
11710
+ var STABLE_GIT_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
10879
11711
  function stableGitErrorCategory(value) {
10880
11712
  return value !== void 0 && STABLE_GIT_ERROR_CATEGORIES.has(value) ? value : void 0;
10881
11713
  }
@@ -10941,6 +11773,8 @@ function formatDaemonEventLine(event, options = {}) {
10941
11773
  return `${prefix} shutdown-complete reason=${quote(event.reason)}${event.undeliveredOutboxCount !== void 0 ? ` undeliveredOutboxCount=${event.undeliveredOutboxCount}` : ""}`;
10942
11774
  case "stale-approval-decision":
10943
11775
  return `${prefix} stale-approval-decision taskId=${event.taskId} decision=${event.decision}${event.reason ? ` reason=${quote(event.reason)}` : ""}`;
11776
+ case "runtime-disposal-failed":
11777
+ return `${prefix} runtime-disposal-failed taskId=${event.taskId} runtime=${event.runtimeId} stage=${event.stage} reason=${quote(event.reason)}`;
10944
11778
  case "git-workspace": {
10945
11779
  const parts = [
10946
11780
  `${prefix} git-workspace taskId=${event.taskId}`,
@@ -11219,8 +12053,8 @@ async function probeRuntimes(adapters, options = {}) {
11219
12053
  let resume = false;
11220
12054
  let permissionModes = [];
11221
12055
  try {
11222
- id = boundedSingleLine(adapter.id, MAX_RUNTIME_ID_CHARS);
11223
- const caps = adapter.capabilities();
12056
+ id = boundedSingleLine(adapter.descriptor.id, MAX_RUNTIME_ID_CHARS);
12057
+ const caps = adapter.descriptor.capabilities;
11224
12058
  steer = caps.steer === true;
11225
12059
  resume = caps.resume === true;
11226
12060
  permissionModes = caps.permissionModes.slice(0, MAX_PERMISSION_MODES).map((mode) => boundedSingleLine(mode, MAX_PERMISSION_MODE_CHARS));
@@ -12085,19 +12919,7 @@ function valueByteSize(value) {
12085
12919
  function placeholderFor(size) {
12086
12920
  return size === void 0 ? "[redacted]" : `[redacted: ${size} bytes]`;
12087
12921
  }
12088
- var STABLE_GIT_ERROR_CATEGORIES2 = /* @__PURE__ */ new Set([
12089
- "git-unavailable",
12090
- "git-timeout",
12091
- "git-output-limit",
12092
- "git-command-failed",
12093
- "workspace-root-invalid",
12094
- "workspace-root-conflict",
12095
- "workspace-not-owned",
12096
- "repository-root-mismatch",
12097
- "repository-invalid",
12098
- "lease-busy",
12099
- "ledger-invalid"
12100
- ]);
12922
+ var STABLE_GIT_ERROR_CATEGORIES2 = new Set(GIT_ERROR_CATEGORIES);
12101
12923
  function stableGitErrorCategory2(value) {
12102
12924
  return typeof value === "string" && STABLE_GIT_ERROR_CATEGORIES2.has(value) ? value : void 0;
12103
12925
  }
@@ -12196,6 +13018,8 @@ function redactForAudit(event) {
12196
13018
  return { ...base, reason: event.reason, undeliveredOutboxCount: event.undeliveredOutboxCount };
12197
13019
  case "stale-approval-decision":
12198
13020
  return { ...base, taskId: event.taskId, decision: event.decision, reasonSize: byteSize(event.reason) };
13021
+ case "runtime-disposal-failed":
13022
+ return { ...base, taskId: event.taskId, runtimeId: event.runtimeId, stage: event.stage, reason: event.reason };
12199
13023
  case "device-assertion":
12200
13024
  return event.result === "issued" ? {
12201
13025
  ...base,
@@ -12344,6 +13168,15 @@ function reconstructDaemonEvent(raw) {
12344
13168
  reason: reasonSize === void 0 ? void 0 : placeholderFor(reasonSize)
12345
13169
  };
12346
13170
  }
13171
+ case "runtime-disposal-failed":
13172
+ return {
13173
+ kind: "runtime-disposal-failed",
13174
+ ts,
13175
+ taskId: str(raw.taskId),
13176
+ runtimeId: str(raw.runtimeId),
13177
+ stage: str(raw.stage),
13178
+ reason: str(raw.reason)
13179
+ };
12347
13180
  case "device-assertion": {
12348
13181
  if (raw.result === "issued") {
12349
13182
  return {
@@ -12616,20 +13449,8 @@ async function runStartCommand(config, deps) {
12616
13449
  throw err;
12617
13450
  }
12618
13451
  }
12619
- var STABLE_GIT_PHASES = /* @__PURE__ */ new Set(["preparing", "active", "completed", "failed", "cancelled", "interrupted", "salvage"]);
12620
- var STABLE_GIT_ERROR_CATEGORIES3 = /* @__PURE__ */ new Set([
12621
- "git-unavailable",
12622
- "git-timeout",
12623
- "git-output-limit",
12624
- "git-command-failed",
12625
- "workspace-root-invalid",
12626
- "workspace-root-conflict",
12627
- "workspace-not-owned",
12628
- "repository-root-mismatch",
12629
- "repository-invalid",
12630
- "lease-busy",
12631
- "ledger-invalid"
12632
- ]);
13452
+ var STABLE_GIT_PHASES = new Set(GIT_WORKSPACE_PHASES);
13453
+ var STABLE_GIT_ERROR_CATEGORIES3 = new Set(GIT_ERROR_CATEGORIES);
12633
13454
  function nonNegativeSafeInteger(value) {
12634
13455
  return value !== void 0 && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
12635
13456
  }
@@ -13128,19 +13949,7 @@ async function runUnpairCommand(daemon, deps = {}) {
13128
13949
  }
13129
13950
 
13130
13951
  // src/bin/commands/workspaces.ts
13131
- var STABLE_ERROR_CATEGORIES = /* @__PURE__ */ new Set([
13132
- "git-unavailable",
13133
- "git-timeout",
13134
- "git-output-limit",
13135
- "git-command-failed",
13136
- "workspace-root-invalid",
13137
- "workspace-root-conflict",
13138
- "workspace-not-owned",
13139
- "repository-root-mismatch",
13140
- "repository-invalid",
13141
- "lease-busy",
13142
- "ledger-invalid"
13143
- ]);
13952
+ var STABLE_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
13144
13953
  function abbreviateCommit(value) {
13145
13954
  if (!value) return "-";
13146
13955
  return value.length > 8 ? value.slice(0, 8) : value;