acpx 0.11.2 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +6 -4
  2. package/dist/{cli-CXlEcGWV.js → cli-pKoIEH5O.js} +4 -4
  3. package/dist/{cli-CXlEcGWV.js.map → cli-pKoIEH5O.js.map} +1 -1
  4. package/dist/cli.d.ts +1 -2
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +37 -16
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-DIlpCkHw.d.ts → client-CFo_9MIo.d.ts} +5 -3
  9. package/dist/client-CFo_9MIo.d.ts.map +1 -0
  10. package/dist/{flags-Dl_mhic3.js → flags-CjeFX5ki.js} +4 -4
  11. package/dist/{flags-Dl_mhic3.js.map → flags-CjeFX5ki.js.map} +1 -1
  12. package/dist/{flows-7OHjgEgq.js → flows-Cym6H3vg.js} +3 -3
  13. package/dist/flows-Cym6H3vg.js.map +1 -0
  14. package/dist/flows.d.ts +2 -2
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{live-checkpoint-mdAaF3qJ.js → live-checkpoint-DQp4JSHD.js} +34 -6
  18. package/dist/live-checkpoint-DQp4JSHD.js.map +1 -0
  19. package/dist/{output-Smw6kFL_.js → output-fNqTAsFQ.js} +427 -90
  20. package/dist/output-fNqTAsFQ.js.map +1 -0
  21. package/dist/runtime.d.ts +16 -12
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +3 -2
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/{session-options-jkYbBxGE.d.ts → session-options-C4uYkKDy.d.ts} +1 -2
  26. package/dist/session-options-C4uYkKDy.d.ts.map +1 -0
  27. package/package.json +19 -19
  28. package/skills/acpx/SKILL.md +7 -6
  29. package/dist/client-DIlpCkHw.d.ts.map +0 -1
  30. package/dist/flows-7OHjgEgq.js.map +0 -1
  31. package/dist/live-checkpoint-mdAaF3qJ.js.map +0 -1
  32. package/dist/output-Smw6kFL_.js.map +0 -1
  33. package/dist/session-options-jkYbBxGE.d.ts.map +0 -1
@@ -1,4 +1,4 @@
1
- import { $ as getPerfMetricsSnapshot, B as absolutePath, C as mergeSessionOptions, Ct as promptToDisplayText, D as applyLifecycleSnapshotToRecord, Dt as withInterrupt, E as applyConversation, Et as TimeoutError, F as modelStateFromConfigOptions, Ft as isRetryablePromptError, G as listSessions, H as findSession, I as splitCommandLine, It as normalizeOutputError, J as pruneSessions, K as listSessionsForAgent, N as assertRequestedModelSupported, Ot as withTimeout, Pt as formatErrorMessage, Q as formatPerfMetric, Qt as QueueProtocolError, S as advertisedModelState, T as sessionOptionsFromRecord, Tt as InterruptedError, U as findSessionByDirectoryWalk, Ut as OUTPUT_ERROR_CODES, V as findGitRepositoryRoot, W as isoNow, Wt as OUTPUT_ERROR_ORIGINS, X as writeSessionRecord, Y as resolveSessionRecord, Zt as QueueConnectionError, _ as createSessionConversation, _t as parseJsonRpcErrorMessage, a as applyRequestedModelIfAdvertised, at as startPerfTimer, b as recordSessionUpdate, bt as isPromptInput, c as setCurrentModelId, ct as normalizeRuntimeSessionId, d as setDesiredModelId, dt as sessionBaseDir, et as incrementPerfCounter, f as syncAdvertisedModelState, ft as sessionEventActivePath, g as cloneSessionConversation, gt as isAcpJsonRpcMessage, h as cloneSessionAcpxState, ht as extractSessionUpdateNotification, i as connectAndLoadSession, it as setPerfGauge, k as AcpClient, l as setDesiredConfigOption, m as applyConfigOptionsToState, mt as sessionEventSegmentPath, n as runPromptTurn, nt as recordPerfDuration, o as currentModelIdFromSetModelResponse, p as applyConfigOptionsToRecord, pt as sessionEventLockPath, q as normalizeName, r as withConnectedSession, rt as resetPerfMetrics, s as clearDesiredConfigOption, t as LiveSessionCheckpoint, tt as measurePerf, u as setDesiredModeId, ut as defaultSessionEventLog, v as recordClientOperation, vt as parsePromptStopReason, w as persistSessionOptions, wt as textPrompt, x as trimConversationForRuntime, y as recordPromptSubmission, zt as toAcpErrorPayload } from "./live-checkpoint-mdAaF3qJ.js";
1
+ import { $ as getPerfMetricsSnapshot, B as absolutePath, C as mergeSessionOptions, Ct as promptToDisplayText, D as applyLifecycleSnapshotToRecord, Dt as withInterrupt, E as applyConversation, Et as TimeoutError, F as modelStateFromConfigOptions, Ft as isRetryablePromptError, G as listSessions, H as findSession, I as splitCommandLine, It as normalizeOutputError, J as pruneSessions, K as listSessionsForAgent, Lt as extractAcpError, N as assertRequestedModelSupported, Ot as withTimeout, Pt as formatErrorMessage, Q as formatPerfMetric, Qt as QueueProtocolError, S as advertisedModelState, T as sessionOptionsFromRecord, Tt as InterruptedError, U as findSessionByDirectoryWalk, Ut as OUTPUT_ERROR_CODES, V as findGitRepositoryRoot, W as isoNow, Wt as OUTPUT_ERROR_ORIGINS, X as writeSessionRecord, Y as resolveSessionRecord, Zt as QueueConnectionError, _ as createSessionConversation, _t as parseJsonRpcErrorMessage, a as applyRequestedModelIfAdvertised, at as startPerfTimer, b as recordSessionUpdate, bt as isPromptInput, c as setCurrentModelId, ct as normalizeRuntimeSessionId, d as setDesiredModelId, dt as sessionBaseDir, et as incrementPerfCounter, f as syncAdvertisedModelState, ft as sessionEventActivePath, g as cloneSessionConversation, gt as isAcpJsonRpcMessage, h as cloneSessionAcpxState, ht as extractSessionUpdateNotification, i as connectAndLoadSession, it as setPerfGauge, k as AcpClient, l as setDesiredConfigOption, m as applyConfigOptionsToState, mt as sessionEventSegmentPath, n as runPromptTurn, nt as recordPerfDuration, o as currentModelIdFromSetModelResponse, p as applyConfigOptionsToRecord, pt as sessionEventLockPath, q as normalizeName, r as withConnectedSession, rt as resetPerfMetrics, s as clearDesiredConfigOption, t as LiveSessionCheckpoint, tt as measurePerf, u as setDesiredModeId, ut as defaultSessionEventLog, v as recordClientOperation, vt as parsePromptStopReason, w as persistSessionOptions, wt as textPrompt, x as trimConversationForRuntime, y as recordPromptSubmission, zt as toAcpErrorPayload } from "./live-checkpoint-DQp4JSHD.js";
2
2
  import fs, { mkdtempSync, realpathSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import fs$1 from "node:fs/promises";
@@ -62,7 +62,8 @@ function queueSocketPath(sessionId, homeDir = os.homedir()) {
62
62
  }
63
63
  //#endregion
64
64
  //#region src/cli/queue/lease-store.ts
65
- const PROCESS_EXIT_GRACE_MS = 1500;
65
+ const PROCESS_SIGTERM_GRACE_MS = 4e3;
66
+ const PROCESS_SIGKILL_GRACE_MS = 1500;
66
67
  const PROCESS_POLL_MS = 50;
67
68
  const QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
68
69
  function parseQueueOwnerRecord(raw) {
@@ -103,18 +104,26 @@ function isQueueOwnerHeartbeatStale(owner) {
103
104
  }
104
105
  async function ensureQueueDir() {
105
106
  const baseDir = queueBaseDir();
106
- await fs$1.mkdir(baseDir, {
107
- recursive: true,
108
- mode: 448
109
- });
110
- await fs$1.chmod(baseDir, 448);
107
+ try {
108
+ await fs$1.mkdir(baseDir, {
109
+ recursive: true,
110
+ mode: 448
111
+ });
112
+ await fs$1.chmod(baseDir, 448);
113
+ } catch (error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ throw new Error(`Failed to prepare queue directory ${baseDir}: ${message}`, { cause: error });
116
+ }
111
117
  const socketDir = queueSocketBaseDir();
112
- if (socketDir) {
118
+ if (socketDir) try {
113
119
  await fs$1.mkdir(socketDir, {
114
120
  recursive: true,
115
121
  mode: 448
116
122
  });
117
123
  await fs$1.chmod(socketDir, 448);
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ throw new Error(`Failed to prepare queue socket directory ${socketDir}: ${message}`, { cause: error });
118
127
  }
119
128
  }
120
129
  async function removeSocketFile(socketPath) {
@@ -160,13 +169,13 @@ async function terminateProcess(pid) {
160
169
  } catch {
161
170
  return false;
162
171
  }
163
- if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) return true;
172
+ if (await waitForProcessExit(pid, PROCESS_SIGTERM_GRACE_MS)) return true;
164
173
  try {
165
174
  process.kill(pid, "SIGKILL");
166
175
  } catch {
167
176
  return false;
168
177
  }
169
- await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
178
+ await waitForProcessExit(pid, PROCESS_SIGKILL_GRACE_MS);
170
179
  return true;
171
180
  }
172
181
  async function ensureOwnerIsUsable(sessionId, owner) {
@@ -753,6 +762,7 @@ function isValidOwnerErrorCore(message) {
753
762
  }
754
763
  //#endregion
755
764
  //#region src/cli/queue/ipc-server.ts
765
+ const QUEUE_SOCKET_DRAIN_TIMEOUT_MS = 1e3;
756
766
  function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
757
767
  return {
758
768
  type: "error",
@@ -795,6 +805,9 @@ var SessionQueueOwner = class SessionQueueOwner {
795
805
  onQueueDepthChanged;
796
806
  pending = [];
797
807
  waiters = [];
808
+ sockets = /* @__PURE__ */ new Set();
809
+ taskSockets = /* @__PURE__ */ new Set();
810
+ drainingSockets = /* @__PURE__ */ new Set();
798
811
  closed = false;
799
812
  constructor(server, controlHandlers, lease, options) {
800
813
  this.server = server;
@@ -824,7 +837,7 @@ var SessionQueueOwner = class SessionQueueOwner {
824
837
  });
825
838
  return ownerRef.current;
826
839
  }
827
- async close() {
840
+ beginShutdown() {
828
841
  if (this.closed) return;
829
842
  this.closed = true;
830
843
  for (const waiter of this.waiters.splice(0)) waiter(void 0);
@@ -832,9 +845,20 @@ var SessionQueueOwner = class SessionQueueOwner {
832
845
  if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner shutting down before prompt execution", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
833
846
  task.close();
834
847
  }
848
+ for (const socket of this.sockets) if (!this.taskSockets.has(socket) && !this.drainingSockets.has(socket)) socket.destroy();
835
849
  this.emitQueueDepth();
850
+ }
851
+ async close() {
852
+ this.beginShutdown();
836
853
  await new Promise((resolve) => {
837
- this.server.close(() => resolve());
854
+ const forceCloseTimer = setTimeout(() => {
855
+ for (const socket of this.sockets) socket.destroy();
856
+ }, QUEUE_SOCKET_DRAIN_TIMEOUT_MS);
857
+ forceCloseTimer.unref();
858
+ this.server.close(() => {
859
+ clearTimeout(forceCloseTimer);
860
+ resolve();
861
+ });
838
862
  });
839
863
  }
840
864
  async nextTask(timeoutMs) {
@@ -904,7 +928,16 @@ var SessionQueueOwner = class SessionQueueOwner {
904
928
  ownerGeneration: this.ownerGeneration
905
929
  });
906
930
  }).finally(() => {
907
- if (!options.socket.destroyed) options.socket.end();
931
+ this.endSocket(options.socket);
932
+ });
933
+ }
934
+ endSocket(socket) {
935
+ if (socket.destroyed || this.drainingSockets.has(socket)) return;
936
+ this.taskSockets.delete(socket);
937
+ this.drainingSockets.add(socket);
938
+ socket.end(() => {
939
+ this.drainingSockets.delete(socket);
940
+ socket.destroy();
908
941
  });
909
942
  }
910
943
  failRequest(socket, requestId, message, detailCode) {
@@ -912,7 +945,7 @@ var SessionQueueOwner = class SessionQueueOwner {
912
945
  ...makeQueueOwnerError(requestId, message, detailCode, { retryable: false }),
913
946
  ownerGeneration: this.ownerGeneration
914
947
  });
915
- socket.end();
948
+ this.endSocket(socket);
916
949
  }
917
950
  parseRequestLine(socket, line) {
918
951
  let parsed;
@@ -931,6 +964,15 @@ var SessionQueueOwner = class SessionQueueOwner {
931
964
  this.failRequest(socket, request.requestId, "Queue request targeted a stale queue owner generation", "QUEUE_OWNER_GENERATION_MISMATCH");
932
965
  return true;
933
966
  }
967
+ rejectClosedOwner(socket, request) {
968
+ if (!this.closed) return false;
969
+ writeQueueMessage(socket, {
970
+ ...makeQueueOwnerError(request.requestId, "Queue owner is shutting down", "QUEUE_OWNER_CLOSED", { retryable: true }),
971
+ ownerGeneration: this.ownerGeneration
972
+ });
973
+ this.endSocket(socket);
974
+ return true;
975
+ }
934
976
  handleControlQueueRequest(socket, request) {
935
977
  if (request.type === "cancel_prompt") {
936
978
  this.handleControlRequest({
@@ -1005,6 +1047,7 @@ var SessionQueueOwner = class SessionQueueOwner {
1005
1047
  return false;
1006
1048
  }
1007
1049
  enqueuePromptRequest(socket, request) {
1050
+ this.taskSockets.add(socket);
1008
1051
  const task = {
1009
1052
  requestId: request.requestId,
1010
1053
  message: request.message,
@@ -1026,7 +1069,7 @@ var SessionQueueOwner = class SessionQueueOwner {
1026
1069
  });
1027
1070
  },
1028
1071
  close: () => {
1029
- if (!socket.destroyed) socket.end();
1072
+ this.endSocket(socket);
1030
1073
  }
1031
1074
  };
1032
1075
  if (!this.enqueue(task)) return;
@@ -1038,19 +1081,20 @@ var SessionQueueOwner = class SessionQueueOwner {
1038
1081
  if (!request.waitForCompletion) task.close();
1039
1082
  }
1040
1083
  handleConnection(socket) {
1084
+ this.sockets.add(socket);
1041
1085
  socket.setEncoding("utf8");
1042
- if (this.closed) {
1043
- writeQueueMessage(socket, makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", { retryable: true }));
1044
- socket.end();
1045
- return;
1046
- }
1086
+ socket.once("close", () => {
1087
+ this.sockets.delete(socket);
1088
+ this.taskSockets.delete(socket);
1089
+ this.drainingSockets.delete(socket);
1090
+ });
1047
1091
  let buffer = "";
1048
1092
  let handled = false;
1049
1093
  const processLine = (line) => {
1050
1094
  if (handled) return;
1051
1095
  handled = true;
1052
1096
  const request = this.parseRequestLine(socket, line);
1053
- if (!request || this.rejectStaleOwnerGeneration(socket, request)) return;
1097
+ if (!request || this.rejectClosedOwner(socket, request) || this.rejectStaleOwnerGeneration(socket, request)) return;
1054
1098
  if (this.handleControlQueueRequest(socket, request)) return;
1055
1099
  if (request.type !== "submit_prompt") {
1056
1100
  this.failRequest(socket, request.requestId, "Invalid queue request", "QUEUE_REQUEST_INVALID");
@@ -1112,7 +1156,8 @@ function makeMalformedQueueMessageError() {
1112
1156
  function emitQueueOwnerError(formatter, policy, sessionId, message) {
1113
1157
  formatter.setContext({ sessionId });
1114
1158
  const queueErrorAlreadyEmitted = policy?.queueErrorAlreadyEmitted ?? true;
1115
- if (message.outputAlreadyEmitted !== true || !queueErrorAlreadyEmitted) {
1159
+ const shouldEmitInFormatter = message.outputAlreadyEmitted !== true || !queueErrorAlreadyEmitted;
1160
+ if (shouldEmitInFormatter) {
1116
1161
  formatter.onError({
1117
1162
  code: message.code ?? "RUNTIME",
1118
1163
  detailCode: message.detailCode,
@@ -1123,7 +1168,7 @@ function emitQueueOwnerError(formatter, policy, sessionId, message) {
1123
1168
  });
1124
1169
  formatter.flush();
1125
1170
  }
1126
- return queueConnectionErrorFromOwner(message, queueErrorAlreadyEmitted);
1171
+ return queueConnectionErrorFromOwner(message, queueErrorAlreadyEmitted || shouldEmitInFormatter);
1127
1172
  }
1128
1173
  function parseQueueOwnerResponseLine(owner, requestId, line) {
1129
1174
  let parsed;
@@ -1273,6 +1318,7 @@ async function submitToQueueOwner(owner, options) {
1273
1318
  owner,
1274
1319
  request,
1275
1320
  onAccepted: ({ resolve }) => {
1321
+ options.onQueueAccepted?.();
1276
1322
  options.outputFormatter.setContext({ sessionId: options.sessionId });
1277
1323
  if (!options.waitForCompletion) resolve({
1278
1324
  queued: true,
@@ -2274,7 +2320,9 @@ function writeQueueOwnerPayloadFile(payload) {
2274
2320
  });
2275
2321
  return payloadPath;
2276
2322
  }
2277
- function buildQueueOwnerSpawnOptions(payloadFilePath) {
2323
+ /** Max stderr bytes retained for cold-start diagnostics. */
2324
+ const QUEUE_OWNER_STARTUP_STDERR_MAX_BYTES = 4e3;
2325
+ function buildQueueOwnerSpawnOptions(payloadFilePath, opts) {
2278
2326
  const env = {
2279
2327
  ...process.env,
2280
2328
  [QUEUE_OWNER_PAYLOAD_FILE_ENV]: payloadFilePath
@@ -2282,14 +2330,77 @@ function buildQueueOwnerSpawnOptions(payloadFilePath) {
2282
2330
  delete env[QUEUE_OWNER_PAYLOAD_ENV];
2283
2331
  return {
2284
2332
  detached: true,
2285
- stdio: "ignore",
2333
+ stdio: opts?.captureStderr === true ? [
2334
+ "ignore",
2335
+ "ignore",
2336
+ "pipe"
2337
+ ] : "ignore",
2286
2338
  env,
2287
2339
  windowsHide: true
2288
2340
  };
2289
2341
  }
2342
+ function formatQueueOwnerStartupFailure(params) {
2343
+ const parts = [`Session queue owner failed to start for session ${params.sessionId}`];
2344
+ if (params.exit.spawnError) parts.push(`spawn error: ${params.exit.spawnError.message}`);
2345
+ else if (params.exit.exited) {
2346
+ const codePart = params.exit.code === null ? "null" : String(params.exit.code);
2347
+ const signalPart = params.exit.signal ? `, signal ${params.exit.signal}` : "";
2348
+ parts.push(`exited with code ${codePart}${signalPart} before binding its socket`);
2349
+ }
2350
+ const tail = params.logTail.trim();
2351
+ if (tail.length > 0) parts.push(`stderr:\n${tail}`);
2352
+ return parts.join(": ");
2353
+ }
2290
2354
  function spawnQueueOwnerProcess(options) {
2291
2355
  const payloadPath = writeQueueOwnerPayloadFile(JSON.stringify(options));
2292
- spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payloadPath)).unref();
2356
+ let exited = false;
2357
+ let code = null;
2358
+ let signal = null;
2359
+ let spawnError;
2360
+ let capturing = true;
2361
+ let stderrTail = Buffer.alloc(0);
2362
+ const child = spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payloadPath, { captureStderr: true }));
2363
+ const stopStartupCapture = () => {
2364
+ if (!capturing) return;
2365
+ capturing = false;
2366
+ const stderr = child.stderr;
2367
+ if (!stderr) return;
2368
+ stderr.removeAllListeners("data");
2369
+ stderr.on("data", () => {});
2370
+ stderr.resume();
2371
+ stderr.unref();
2372
+ };
2373
+ child.stderr?.on("data", (chunk) => {
2374
+ if (!capturing) return;
2375
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
2376
+ const combined = Buffer.concat([stderrTail, buf]);
2377
+ stderrTail = combined.length > 4e3 ? combined.subarray(combined.length - QUEUE_OWNER_STARTUP_STDERR_MAX_BYTES) : combined;
2378
+ });
2379
+ child.stderr?.on("error", () => {});
2380
+ child.on("error", (error) => {
2381
+ spawnError = error;
2382
+ });
2383
+ child.on("close", (exitCode, exitSignal) => {
2384
+ exited = true;
2385
+ code = exitCode;
2386
+ signal = exitSignal;
2387
+ stopStartupCapture();
2388
+ });
2389
+ child.unref();
2390
+ return {
2391
+ pid: child.pid,
2392
+ getExitState: () => ({
2393
+ exited,
2394
+ code,
2395
+ signal,
2396
+ ...spawnError ? { spawnError } : {}
2397
+ }),
2398
+ readLogTail: (maxBytes = QUEUE_OWNER_STARTUP_STDERR_MAX_BYTES) => {
2399
+ const start = Math.max(0, stderrTail.length - maxBytes);
2400
+ return stderrTail.subarray(start).toString("utf8");
2401
+ },
2402
+ stopStartupCapture
2403
+ };
2293
2404
  }
2294
2405
  //#endregion
2295
2406
  //#region src/session/events.ts
@@ -2527,6 +2638,51 @@ const DISCARD_OUTPUT_FORMATTER = {
2527
2638
  onPermissionEscalation() {},
2528
2639
  flush() {}
2529
2640
  };
2641
+ function markOutputAlreadyEmitted(error, outputAlreadyEmitted) {
2642
+ if (!outputAlreadyEmitted || !error || typeof error !== "object") return;
2643
+ error.outputAlreadyEmitted = true;
2644
+ }
2645
+ async function runWithOptionalInterrupt(options) {
2646
+ if (options.handleProcessInterrupts === false) return await options.run();
2647
+ return await withInterrupt(options.run, options.handleInterrupt);
2648
+ }
2649
+ function attachAcpErrorPayload(error, acp) {
2650
+ if (!acp || !error || typeof error !== "object") return;
2651
+ error.acp = acp;
2652
+ }
2653
+ function rendersAcpErrors(policy) {
2654
+ return policy?.queueErrorAlreadyEmitted ?? true;
2655
+ }
2656
+ function normalizedErrorText(value) {
2657
+ return value.trim().toLowerCase();
2658
+ }
2659
+ function outboundAcpErrorMatches(error, acp) {
2660
+ const errorText = normalizedErrorText(formatErrorMessage(error));
2661
+ const details = acp.data?.details;
2662
+ const candidateText = normalizedErrorText(typeof details === "string" && details.trim().length > 0 ? details : acp.message);
2663
+ return errorText === candidateText || errorText.includes(candidateText);
2664
+ }
2665
+ var AcpErrorTracker = class {
2666
+ latestInbound;
2667
+ outbound = [];
2668
+ reset() {
2669
+ this.latestInbound = void 0;
2670
+ this.outbound.length = 0;
2671
+ }
2672
+ observe(output, direction, message) {
2673
+ output.onAcpMessage(message);
2674
+ const acp = extractAcpError(message);
2675
+ if (!acp) return;
2676
+ if (direction === "inbound") {
2677
+ this.latestInbound = acp;
2678
+ return;
2679
+ }
2680
+ this.outbound.push(acp);
2681
+ }
2682
+ match(error) {
2683
+ return this.latestInbound ?? this.outbound.findLast((acp) => outboundAcpErrorMatches(error, acp));
2684
+ }
2685
+ };
2530
2686
  function toPromptResult(stopReason, sessionId, client) {
2531
2687
  return {
2532
2688
  stopReason,
@@ -2636,6 +2792,11 @@ function filterRecoverableLoadFallbackOutput(messages) {
2636
2792
  return true;
2637
2793
  });
2638
2794
  }
2795
+ function filterBufferedConnectOutput(messages, loadError) {
2796
+ if (loadError == null) return messages;
2797
+ const filteredMessages = new Set(filterRecoverableLoadFallbackOutput(messages.map(({ message }) => message)));
2798
+ return messages.filter(({ message }) => filteredMessages.has(message));
2799
+ }
2639
2800
  function emitPromptRetryNotice(params) {
2640
2801
  if (params.suppressSdkConsoleErrors) return;
2641
2802
  process.stderr.write(`[acpx] prompt failed (${formatErrorMessage(params.error)}), retrying in ${params.delayMs}ms (attempt ${params.attempt}/${params.maxRetries})\n`);
@@ -2687,6 +2848,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
2687
2848
  authCredentials: options.authCredentials,
2688
2849
  authPolicy: options.authPolicy,
2689
2850
  outputFormatter,
2851
+ errorEmissionPolicy: { queueErrorAlreadyEmitted: true },
2690
2852
  timeoutMs: task.timeoutMs,
2691
2853
  suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
2692
2854
  verbose: options.verbose,
@@ -2695,6 +2857,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
2695
2857
  onClientAvailable: options.onClientAvailable,
2696
2858
  onClientClosed: options.onClientClosed,
2697
2859
  onPromptActive: options.onPromptActive,
2860
+ handleProcessInterrupts: options.handleProcessInterrupts,
2698
2861
  client: options.sharedClient
2699
2862
  };
2700
2863
  }
@@ -2739,6 +2902,7 @@ async function runQueuedTask(sessionRecordId, task, options) {
2739
2902
  async function runSessionPrompt(options) {
2740
2903
  const stopTotalTimer = startPerfTimer("runtime.prompt.total");
2741
2904
  const output = options.outputFormatter;
2905
+ const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
2742
2906
  const record = await measurePerf("session.resolve_prompt_record", async () => {
2743
2907
  return await resolveSessionRecord(options.sessionRecordId);
2744
2908
  });
@@ -2761,7 +2925,7 @@ async function runSessionPrompt(options) {
2761
2925
  let bufferingConnectOutput = true;
2762
2926
  let promptTurnActive = false;
2763
2927
  let promptTurnHadSideEffects = false;
2764
- let sawAcpMessage = false;
2928
+ const acpErrors = new AcpErrorTracker();
2765
2929
  let eventWriterClosed = false;
2766
2930
  const closeEventWriter = async (checkpoint) => {
2767
2931
  if (eventWriterClosed) return;
@@ -2824,16 +2988,18 @@ async function runSessionPrompt(options) {
2824
2988
  });
2825
2989
  client.setEventHandlers({
2826
2990
  onAcpMessage: (direction, message) => {
2827
- sawAcpMessage = true;
2828
2991
  pendingMessages.push(message);
2829
2992
  options.onAcpMessage?.(direction, message);
2830
2993
  },
2831
- onAcpOutputMessage: (_direction, message) => {
2994
+ onAcpOutputMessage: (direction, message) => {
2832
2995
  if (bufferingConnectOutput) {
2833
- pendingConnectOutputMessages.push(message);
2996
+ pendingConnectOutputMessages.push({
2997
+ direction,
2998
+ message
2999
+ });
2834
3000
  return;
2835
3001
  }
2836
- output.onAcpMessage(message);
3002
+ acpErrors.observe(output, direction, message);
2837
3003
  },
2838
3004
  onSessionUpdate: (notification) => {
2839
3005
  if (promptTurnActive) promptTurnHadSideEffects = true;
@@ -2898,8 +3064,8 @@ async function runSessionPrompt(options) {
2898
3064
  };
2899
3065
  const flushConnectOutput = (loadError) => {
2900
3066
  bufferingConnectOutput = false;
2901
- const messages = loadError == null ? pendingConnectOutputMessages : filterRecoverableLoadFallbackOutput(pendingConnectOutputMessages);
2902
- for (const message of messages) output.onAcpMessage(message);
3067
+ const outputMessages = filterBufferedConnectOutput(pendingConnectOutputMessages, loadError);
3068
+ for (const { direction, message } of outputMessages) acpErrors.observe(output, direction, message);
2903
3069
  pendingConnectOutputMessages.length = 0;
2904
3070
  };
2905
3071
  const connectForPrompt = async () => {
@@ -2946,6 +3112,7 @@ async function runSessionPrompt(options) {
2946
3112
  };
2947
3113
  };
2948
3114
  const runPromptAttempt = async (sessionId, attempt) => {
3115
+ acpErrors.reset();
2949
3116
  const promptStartedAt = Date.now();
2950
3117
  const response = await measurePerf("runtime.prompt.agent_turn", async () => {
2951
3118
  return await runPromptTurn({
@@ -2973,14 +3140,19 @@ async function runSessionPrompt(options) {
2973
3140
  promptTurnActive = false;
2974
3141
  applyLifecycleSnapshotToRecord(record, snapshot);
2975
3142
  emitPromptDisconnectNotice(snapshot, options.verbose);
2976
- const normalizedError = normalizeOutputError(error, { origin: "runtime" });
3143
+ const matchedAcpError = acpErrors.match(error);
3144
+ const normalizedError = normalizeOutputError(error, {
3145
+ origin: "runtime",
3146
+ acp: matchedAcpError
3147
+ });
2977
3148
  await flushPendingMessages(false).catch(() => {});
2978
3149
  output.flush();
2979
3150
  record.lastUsedAt = isoNow();
2980
3151
  applyConversation(record, conversation);
2981
3152
  record.acpx = acpxState;
2982
3153
  const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
2983
- propagated.outputAlreadyEmitted = sawAcpMessage;
3154
+ attachAcpErrorPayload(propagated, normalizedError.acp);
3155
+ propagated.outputAlreadyEmitted = matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted;
2984
3156
  propagated.normalizedOutputError = normalizedError;
2985
3157
  throw propagated;
2986
3158
  };
@@ -2995,7 +3167,8 @@ async function runSessionPrompt(options) {
2995
3167
  const savePromptSuccess = async (response) => {
2996
3168
  await flushPendingMessages(false);
2997
3169
  output.flush();
2998
- record.lastUsedAt = isoNow();
3170
+ const now = isoNow();
3171
+ record.lastUsedAt = now;
2999
3172
  record.closed = false;
3000
3173
  record.closedAt = void 0;
3001
3174
  record.protocolVersion = client.initializeResult?.protocolVersion;
@@ -3006,37 +3179,48 @@ async function runSessionPrompt(options) {
3006
3179
  stopTotalTimer();
3007
3180
  return response;
3008
3181
  };
3182
+ const runPrompt = async () => {
3183
+ const { sessionId: activeSessionId, resumed, loadError } = await connectForPrompt();
3184
+ await applyPromptModelIfAdvertised({
3185
+ client,
3186
+ sessionId: activeSessionId,
3187
+ requestedModel: sessionOptions?.model,
3188
+ record,
3189
+ timeoutMs: options.timeoutMs,
3190
+ suppressWarnings: options.suppressSdkConsoleErrors
3191
+ });
3192
+ acpxState = cloneSessionAcpxState(record.acpx);
3193
+ output.setContext({ sessionId: record.acpxRecordId });
3194
+ await liveCheckpoint.checkpoint();
3195
+ const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
3196
+ promptTurnActive = false;
3197
+ return {
3198
+ ...toPromptResult(response.stopReason, record.acpxRecordId, client),
3199
+ record,
3200
+ resumed,
3201
+ loadError
3202
+ };
3203
+ };
3204
+ const handleInterrupt = async () => {
3205
+ await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3206
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3207
+ record.lastUsedAt = isoNow();
3208
+ applyConversation(record, conversation);
3209
+ record.acpx = acpxState;
3210
+ await flushPendingMessages(false).catch(() => {});
3211
+ if (ownClient) await client.close();
3212
+ };
3009
3213
  try {
3010
- return await withInterrupt(async () => {
3011
- const { sessionId: activeSessionId, resumed, loadError } = await connectForPrompt();
3012
- await applyPromptModelIfAdvertised({
3013
- client,
3014
- sessionId: activeSessionId,
3015
- requestedModel: sessionOptions?.model,
3016
- record,
3017
- timeoutMs: options.timeoutMs,
3018
- suppressWarnings: options.suppressSdkConsoleErrors
3019
- });
3020
- acpxState = cloneSessionAcpxState(record.acpx);
3021
- output.setContext({ sessionId: record.acpxRecordId });
3022
- await liveCheckpoint.checkpoint();
3023
- const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
3024
- promptTurnActive = false;
3025
- return {
3026
- ...toPromptResult(response.stopReason, record.acpxRecordId, client),
3027
- record,
3028
- resumed,
3029
- loadError
3030
- };
3031
- }, async () => {
3032
- await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3033
- applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3034
- record.lastUsedAt = isoNow();
3035
- applyConversation(record, conversation);
3036
- record.acpx = acpxState;
3037
- await flushPendingMessages(false).catch(() => {});
3038
- if (ownClient) await client.close();
3214
+ return await runWithOptionalInterrupt({
3215
+ handleProcessInterrupts: options.handleProcessInterrupts,
3216
+ run: runPrompt,
3217
+ handleInterrupt
3039
3218
  });
3219
+ } catch (error) {
3220
+ const matchedAcpError = acpErrors.match(error);
3221
+ attachAcpErrorPayload(error, matchedAcpError);
3222
+ markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
3223
+ throw error;
3040
3224
  } finally {
3041
3225
  if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", stopTotalTimer())}\n`);
3042
3226
  else stopTotalTimer();
@@ -3054,8 +3238,10 @@ async function runSessionPrompt(options) {
3054
3238
  }
3055
3239
  async function runOnce(options) {
3056
3240
  const output = options.outputFormatter;
3241
+ const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
3057
3242
  let promptTurnActive = false;
3058
3243
  let promptTurnHadSideEffects = false;
3244
+ const acpErrors = new AcpErrorTracker();
3059
3245
  const client = new AcpClient({
3060
3246
  agentCommand: options.agentCommand,
3061
3247
  cwd: absolutePath(options.cwd),
@@ -3069,7 +3255,9 @@ async function runOnce(options) {
3069
3255
  suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3070
3256
  verbose: options.verbose,
3071
3257
  onAcpMessage: options.onAcpMessage,
3072
- onAcpOutputMessage: (_direction, message) => output.onAcpMessage(message),
3258
+ onAcpOutputMessage: (direction, message) => {
3259
+ acpErrors.observe(output, direction, message);
3260
+ },
3073
3261
  onSessionUpdate: (notification) => {
3074
3262
  if (promptTurnActive) promptTurnHadSideEffects = true;
3075
3263
  options.onSessionUpdate?.(notification);
@@ -3085,6 +3273,7 @@ async function runOnce(options) {
3085
3273
  sessionOptions: options.sessionOptions
3086
3274
  });
3087
3275
  const runExecPromptAttempt = async (sessionId) => {
3276
+ acpErrors.reset();
3088
3277
  return await measurePerf("runtime.exec.prompt", async () => {
3089
3278
  return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
3090
3279
  });
@@ -3130,6 +3319,11 @@ async function runOnce(options) {
3130
3319
  await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3131
3320
  await client.close();
3132
3321
  });
3322
+ } catch (error) {
3323
+ const matchedAcpError = acpErrors.match(error);
3324
+ attachAcpErrorPayload(error, matchedAcpError);
3325
+ markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
3326
+ throw error;
3133
3327
  } finally {
3134
3328
  await client.close();
3135
3329
  }
@@ -3147,6 +3341,7 @@ async function sendSessionDirect(options) {
3147
3341
  authPolicy: options.authPolicy,
3148
3342
  terminal: options.terminal,
3149
3343
  outputFormatter: options.outputFormatter,
3344
+ errorEmissionPolicy: options.errorEmissionPolicy,
3150
3345
  onAcpMessage: options.onAcpMessage,
3151
3346
  onSessionUpdate: options.onSessionUpdate,
3152
3347
  onClientOperation: options.onClientOperation,
@@ -3161,7 +3356,8 @@ async function sendSessionDirect(options) {
3161
3356
  //#region src/cli/session/queue-owner-runtime.ts
3162
3357
  const QUEUE_OWNER_STARTUP_MAX_ATTEMPTS = 120;
3163
3358
  const QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
3164
- async function submitToRunningOwner(options, waitForCompletion) {
3359
+ const QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS = 750;
3360
+ async function submitToRunningOwner(options, waitForCompletion, extras) {
3165
3361
  return await trySubmitToRunningOwner({
3166
3362
  sessionId: options.sessionId,
3167
3363
  message: promptToDisplayText(options.prompt),
@@ -3178,7 +3374,8 @@ async function submitToRunningOwner(options, waitForCompletion) {
3178
3374
  promptRetries: options.promptRetries,
3179
3375
  waitForCompletion,
3180
3376
  verbose: options.verbose,
3181
- sessionOptions: options.sessionOptions
3377
+ sessionOptions: options.sessionOptions,
3378
+ onQueueAccepted: extras?.onQueueAccepted
3182
3379
  });
3183
3380
  }
3184
3381
  function createQueueOwnerSharedClient(options, sessionRecord) {
@@ -3245,6 +3442,9 @@ function logDeferredCancelFailure(error, verbose) {
3245
3442
  if (!verbose) return;
3246
3443
  process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
3247
3444
  }
3445
+ function queueOwnerExitIsFatal(exit) {
3446
+ return exit.exited && (exit.spawnError !== void 0 || exit.code !== 0 || exit.signal !== null);
3447
+ }
3248
3448
  function logQueueOwnerReady(params) {
3249
3449
  if (!params.verbose) return;
3250
3450
  process.stderr.write(`[acpx] queue owner ready for session ${params.sessionId} (ttlMs=${params.ttlMs}, maxQueueDepth=${params.maxQueueDepth})\n`);
@@ -3252,8 +3452,8 @@ function logQueueOwnerReady(params) {
3252
3452
  async function closeQueueOwnerRuntime(params) {
3253
3453
  if (params.heartbeatTimer) clearInterval(params.heartbeatTimer);
3254
3454
  params.turnController.beginClosing();
3255
- await params.owner?.close();
3256
3455
  await params.sharedClient.close().catch(() => {});
3456
+ await params.owner?.close();
3257
3457
  await writeQueueOwnerLifecycleSnapshot(params.sessionId, params.sharedClient);
3258
3458
  await releaseQueueOwnerLease(params.lease);
3259
3459
  if (params.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${params.sessionId}\n`);
@@ -3265,6 +3465,75 @@ async function writeQueueOwnerLifecycleSnapshot(sessionId, sharedClient) {
3265
3465
  await writeSessionRecord(record);
3266
3466
  } catch {}
3267
3467
  }
3468
+ async function settlesWithin(promise, timeoutMs) {
3469
+ let timer;
3470
+ const timeout = new Promise((resolve) => {
3471
+ timer = setTimeout(() => resolve(false), timeoutMs);
3472
+ });
3473
+ try {
3474
+ return await Promise.race([promise.then(() => true, () => true), timeout]);
3475
+ } finally {
3476
+ if (timer) clearTimeout(timer);
3477
+ }
3478
+ }
3479
+ function createQueueOwnerShutdownController(params) {
3480
+ let requested = false;
3481
+ let activeTurn;
3482
+ let activeTurnShutdown;
3483
+ let shutdownPromise;
3484
+ const drainActiveTurn = async () => {
3485
+ const turn = activeTurn;
3486
+ if (!turn) return;
3487
+ params.turnController.requestCancel().catch((error) => {
3488
+ logDeferredCancelFailure(error, params.verbose);
3489
+ });
3490
+ if (!await settlesWithin(turn, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
3491
+ await turn.catch(() => {});
3492
+ };
3493
+ const request = () => {
3494
+ requested = true;
3495
+ params.stopHeartbeat();
3496
+ params.getOwner()?.beginShutdown();
3497
+ activeTurnShutdown ??= drainActiveTurn();
3498
+ };
3499
+ return {
3500
+ get requested() {
3501
+ return requested;
3502
+ },
3503
+ request,
3504
+ setActiveTurn: (turn) => {
3505
+ activeTurn = turn;
3506
+ },
3507
+ clearActiveTurn: (turn) => {
3508
+ if (activeTurn === turn) activeTurn = void 0;
3509
+ },
3510
+ shutdown: () => {
3511
+ request();
3512
+ shutdownPromise ??= (async () => {
3513
+ await activeTurnShutdown;
3514
+ await closeQueueOwnerRuntime({
3515
+ lease: params.lease,
3516
+ owner: params.getOwner(),
3517
+ heartbeatTimer: void 0,
3518
+ turnController: params.turnController,
3519
+ sharedClient: params.sharedClient,
3520
+ sessionId: params.sessionId,
3521
+ verbose: params.verbose
3522
+ });
3523
+ })();
3524
+ return shutdownPromise;
3525
+ }
3526
+ };
3527
+ }
3528
+ function applyRequestedShutdown(owner, shutdown) {
3529
+ if (shutdown.requested) owner.beginShutdown();
3530
+ }
3531
+ function startQueueOwnerHeartbeat(params) {
3532
+ if (!params.enabled) return;
3533
+ return setInterval(() => {
3534
+ refreshQueueOwnerLease(params.lease, { queueDepth: params.owner.queueDepth() }).catch(() => {});
3535
+ }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
3536
+ }
3268
3537
  async function runSessionQueueOwner(options) {
3269
3538
  const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
3270
3539
  path: options.mcpConfigPath,
@@ -3309,6 +3578,26 @@ async function runSessionQueueOwner(options) {
3309
3578
  turnController.endTurn();
3310
3579
  }
3311
3580
  };
3581
+ const shutdown = createQueueOwnerShutdownController({
3582
+ lease,
3583
+ getOwner: () => owner,
3584
+ stopHeartbeat: () => {
3585
+ if (heartbeatTimer) {
3586
+ clearInterval(heartbeatTimer);
3587
+ heartbeatTimer = void 0;
3588
+ }
3589
+ },
3590
+ turnController,
3591
+ sharedClient,
3592
+ sessionId: options.sessionId,
3593
+ verbose: options.verbose
3594
+ });
3595
+ const onSignal = () => {
3596
+ shutdown.request();
3597
+ };
3598
+ process.once("SIGINT", onSignal);
3599
+ process.once("SIGTERM", onSignal);
3600
+ process.once("SIGHUP", onSignal);
3312
3601
  try {
3313
3602
  owner = await SessionQueueOwner.start(lease, {
3314
3603
  cancelPrompt: async () => {
@@ -3331,6 +3620,7 @@ async function runSessionQueueOwner(options) {
3331
3620
  refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
3332
3621
  }
3333
3622
  });
3623
+ applyRequestedShutdown(owner, shutdown);
3334
3624
  logQueueOwnerReady({
3335
3625
  sessionId: options.sessionId,
3336
3626
  ttlMs,
@@ -3338,16 +3628,18 @@ async function runSessionQueueOwner(options) {
3338
3628
  verbose: options.verbose
3339
3629
  });
3340
3630
  await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
3341
- heartbeatTimer = setInterval(() => {
3342
- refreshQueueOwnerLease(lease, { queueDepth: owner?.queueDepth() ?? 0 }).catch(() => {});
3343
- }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
3631
+ heartbeatTimer = startQueueOwnerHeartbeat({
3632
+ enabled: !shutdown.requested,
3633
+ lease,
3634
+ owner
3635
+ });
3344
3636
  let isFirstTask = true;
3345
3637
  while (true) {
3346
3638
  const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
3347
3639
  const task = await owner.nextTask(pollTimeoutMs);
3348
3640
  if (!task) break;
3349
3641
  isFirstTask = false;
3350
- await runPromptTurn(async () => {
3642
+ const turnPromise = runPromptTurn(async () => {
3351
3643
  try {
3352
3644
  await runQueuedTask(options.sessionId, task, {
3353
3645
  sharedClient,
@@ -3364,37 +3656,67 @@ async function runSessionQueueOwner(options) {
3364
3656
  onPromptActive: async () => {
3365
3657
  turnController.markPromptActive();
3366
3658
  await applyPendingCancel();
3367
- }
3659
+ },
3660
+ handleProcessInterrupts: false
3368
3661
  });
3369
3662
  } finally {
3370
3663
  checkpointPerfMetricsCapture();
3371
3664
  }
3372
3665
  });
3666
+ shutdown.setActiveTurn(turnPromise);
3667
+ try {
3668
+ await turnPromise;
3669
+ } finally {
3670
+ shutdown.clearActiveTurn(turnPromise);
3671
+ }
3373
3672
  }
3374
3673
  } finally {
3375
- await closeQueueOwnerRuntime({
3376
- lease,
3377
- owner,
3378
- heartbeatTimer,
3379
- turnController,
3380
- sharedClient,
3381
- sessionId: options.sessionId,
3382
- verbose: options.verbose
3383
- });
3674
+ process.off("SIGINT", onSignal);
3675
+ process.off("SIGTERM", onSignal);
3676
+ process.off("SIGHUP", onSignal);
3677
+ await shutdown.shutdown();
3384
3678
  }
3385
3679
  }
3386
3680
  async function sendSession(options) {
3387
3681
  const waitForCompletion = options.waitForCompletion !== false;
3388
3682
  const queuedToOwner = await submitToRunningOwner(options, waitForCompletion);
3389
3683
  if (queuedToOwner) return queuedToOwner;
3390
- spawnQueueOwnerProcess(queueOwnerRuntimeOptionsFromSend(options));
3684
+ const owner = spawnQueueOwnerProcess(queueOwnerRuntimeOptionsFromSend(options));
3685
+ const onQueueAccepted = () => {
3686
+ owner.stopStartupCapture();
3687
+ };
3391
3688
  for (let attempt = 0; attempt < QUEUE_OWNER_STARTUP_MAX_ATTEMPTS; attempt += 1) {
3392
- const queued = await submitToRunningOwner(options, waitForCompletion);
3393
- if (queued) return queued;
3689
+ const queued = await submitToRunningOwner(options, waitForCompletion, { onQueueAccepted });
3690
+ if (queued) {
3691
+ owner.stopStartupCapture();
3692
+ return queued;
3693
+ }
3694
+ const exit = owner.getExitState();
3695
+ if (queueOwnerExitIsFatal(exit)) {
3696
+ const message = formatQueueOwnerStartupFailure({
3697
+ sessionId: options.sessionId,
3698
+ exit,
3699
+ logTail: owner.readLogTail()
3700
+ });
3701
+ owner.stopStartupCapture();
3702
+ throw new Error(message);
3703
+ }
3394
3704
  await waitMs(50);
3395
3705
  }
3396
- throw new Error(`Session queue owner failed to start for session ${options.sessionId}`);
3706
+ const finalExit = owner.getExitState();
3707
+ const message = formatQueueOwnerStartupFailure({
3708
+ sessionId: options.sessionId,
3709
+ exit: finalExit.exited ? finalExit : {
3710
+ exited: false,
3711
+ code: null,
3712
+ signal: null
3713
+ },
3714
+ logTail: owner.readLogTail()
3715
+ });
3716
+ owner.stopStartupCapture();
3717
+ throw new Error(message);
3397
3718
  }
3719
+ const queueOwnerRuntimeTestInternals = { queueOwnerExitIsFatal };
3398
3720
  //#endregion
3399
3721
  //#region src/session/session.ts
3400
3722
  var session_exports = /* @__PURE__ */ __exportAll({
@@ -3417,6 +3739,7 @@ var session_exports = /* @__PURE__ */ __exportAll({
3417
3739
  mergeConnectedModelState: () => mergeConnectedModelState,
3418
3740
  normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
3419
3741
  pruneSessions: () => pruneSessions,
3742
+ queueOwnerRuntimeTestInternals: () => queueOwnerRuntimeTestInternals,
3420
3743
  runOnce: () => runOnce,
3421
3744
  runQueuedTask: () => runQueuedTask,
3422
3745
  runSessionQueueOwner: () => runSessionQueueOwner,
@@ -3695,6 +4018,12 @@ function asRecord(value) {
3695
4018
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
3696
4019
  return value;
3697
4020
  }
4021
+ function parseJsonRpcErrorSummary(message) {
4022
+ const fallback = parseJsonRpcErrorMessage(message);
4023
+ if (!fallback) return;
4024
+ const details = asRecord(asRecord(message.error)?.data)?.details;
4025
+ return typeof details === "string" && details.trim().length > 0 ? details.trim() : fallback;
4026
+ }
3698
4027
  function extractJsonRpcMethod(message) {
3699
4028
  return Object.hasOwn(message, "method") ? message.method?.toString() : void 0;
3700
4029
  }
@@ -4060,7 +4389,7 @@ var TextOutputFormatter = class {
4060
4389
  this.renderDone(stopReason);
4061
4390
  return;
4062
4391
  }
4063
- const errorMessage = parseJsonRpcErrorMessage(message);
4392
+ const errorMessage = parseJsonRpcErrorSummary(message);
4064
4393
  if (errorMessage) this.onError({
4065
4394
  code: "RUNTIME",
4066
4395
  origin: "acp",
@@ -4286,7 +4615,11 @@ var QuietOutputFormatter = class {
4286
4615
  this.flushMetadata(message);
4287
4616
  }
4288
4617
  }
4289
- onError(_params) {}
4618
+ onError(params) {
4619
+ const qualifier = params.detailCode ? `${params.code} ${params.detailCode}` : params.code;
4620
+ const message = preferredAcpErrorDetails(params.acp) ?? params.message;
4621
+ this.stderr.write(`[acpx] error: ${qualifier} ${message.replace(/\r\n?|\n/g, " ")}\n`);
4622
+ }
4290
4623
  onPermissionEscalation(_event) {}
4291
4624
  flush() {}
4292
4625
  flushBufferedOutput() {
@@ -4347,6 +4680,10 @@ var QuietOutputFormatter = class {
4347
4680
  if (typeof cost === "string" && cost.trim()) return `[acpx] cost: ${cost.trim()}`;
4348
4681
  }
4349
4682
  };
4683
+ function preferredAcpErrorDetails(acp) {
4684
+ const details = asRecord(acp?.data)?.details;
4685
+ return typeof details === "string" && details.trim().length > 0 ? details.trim() : void 0;
4686
+ }
4350
4687
  function createOutputFormatter(format, options = {}) {
4351
4688
  const stdout = options.stdout ?? process.stdout;
4352
4689
  const stderr = options.stderr ?? process.stderr;
@@ -4361,4 +4698,4 @@ function createOutputFormatter(format, options = {}) {
4361
4698
  //#endregion
4362
4699
  export { runSessionQueueOwner as a, buildQueueOwnerArgOverride as c, createSessionWithClient as d, cancelSessionPrompt as f, __exportAll as g, isProcessAlive as h, session_exports as i, flushPerfMetricsCapture as l, DEFAULT_QUEUE_OWNER_TTL_MS as m, getTextErrorRemediationHints as n, runOnce as o, probeQueueOwnerHealth as p, output_exports as r, sendSessionDirect as s, createOutputFormatter as t, installPerfMetricsCapture as u };
4363
4700
 
4364
- //# sourceMappingURL=output-Smw6kFL_.js.map
4701
+ //# sourceMappingURL=output-fNqTAsFQ.js.map