acpx 0.11.2 → 0.12.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.
@@ -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-ClPCSdrW.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) {
@@ -160,13 +161,13 @@ async function terminateProcess(pid) {
160
161
  } catch {
161
162
  return false;
162
163
  }
163
- if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) return true;
164
+ if (await waitForProcessExit(pid, PROCESS_SIGTERM_GRACE_MS)) return true;
164
165
  try {
165
166
  process.kill(pid, "SIGKILL");
166
167
  } catch {
167
168
  return false;
168
169
  }
169
- await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
170
+ await waitForProcessExit(pid, PROCESS_SIGKILL_GRACE_MS);
170
171
  return true;
171
172
  }
172
173
  async function ensureOwnerIsUsable(sessionId, owner) {
@@ -753,6 +754,7 @@ function isValidOwnerErrorCore(message) {
753
754
  }
754
755
  //#endregion
755
756
  //#region src/cli/queue/ipc-server.ts
757
+ const QUEUE_SOCKET_DRAIN_TIMEOUT_MS = 1e3;
756
758
  function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
757
759
  return {
758
760
  type: "error",
@@ -795,6 +797,9 @@ var SessionQueueOwner = class SessionQueueOwner {
795
797
  onQueueDepthChanged;
796
798
  pending = [];
797
799
  waiters = [];
800
+ sockets = /* @__PURE__ */ new Set();
801
+ taskSockets = /* @__PURE__ */ new Set();
802
+ drainingSockets = /* @__PURE__ */ new Set();
798
803
  closed = false;
799
804
  constructor(server, controlHandlers, lease, options) {
800
805
  this.server = server;
@@ -824,7 +829,7 @@ var SessionQueueOwner = class SessionQueueOwner {
824
829
  });
825
830
  return ownerRef.current;
826
831
  }
827
- async close() {
832
+ beginShutdown() {
828
833
  if (this.closed) return;
829
834
  this.closed = true;
830
835
  for (const waiter of this.waiters.splice(0)) waiter(void 0);
@@ -832,9 +837,20 @@ var SessionQueueOwner = class SessionQueueOwner {
832
837
  if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner shutting down before prompt execution", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
833
838
  task.close();
834
839
  }
840
+ for (const socket of this.sockets) if (!this.taskSockets.has(socket) && !this.drainingSockets.has(socket)) socket.destroy();
835
841
  this.emitQueueDepth();
842
+ }
843
+ async close() {
844
+ this.beginShutdown();
836
845
  await new Promise((resolve) => {
837
- this.server.close(() => resolve());
846
+ const forceCloseTimer = setTimeout(() => {
847
+ for (const socket of this.sockets) socket.destroy();
848
+ }, QUEUE_SOCKET_DRAIN_TIMEOUT_MS);
849
+ forceCloseTimer.unref();
850
+ this.server.close(() => {
851
+ clearTimeout(forceCloseTimer);
852
+ resolve();
853
+ });
838
854
  });
839
855
  }
840
856
  async nextTask(timeoutMs) {
@@ -904,7 +920,16 @@ var SessionQueueOwner = class SessionQueueOwner {
904
920
  ownerGeneration: this.ownerGeneration
905
921
  });
906
922
  }).finally(() => {
907
- if (!options.socket.destroyed) options.socket.end();
923
+ this.endSocket(options.socket);
924
+ });
925
+ }
926
+ endSocket(socket) {
927
+ if (socket.destroyed || this.drainingSockets.has(socket)) return;
928
+ this.taskSockets.delete(socket);
929
+ this.drainingSockets.add(socket);
930
+ socket.end(() => {
931
+ this.drainingSockets.delete(socket);
932
+ socket.destroy();
908
933
  });
909
934
  }
910
935
  failRequest(socket, requestId, message, detailCode) {
@@ -912,7 +937,7 @@ var SessionQueueOwner = class SessionQueueOwner {
912
937
  ...makeQueueOwnerError(requestId, message, detailCode, { retryable: false }),
913
938
  ownerGeneration: this.ownerGeneration
914
939
  });
915
- socket.end();
940
+ this.endSocket(socket);
916
941
  }
917
942
  parseRequestLine(socket, line) {
918
943
  let parsed;
@@ -931,6 +956,15 @@ var SessionQueueOwner = class SessionQueueOwner {
931
956
  this.failRequest(socket, request.requestId, "Queue request targeted a stale queue owner generation", "QUEUE_OWNER_GENERATION_MISMATCH");
932
957
  return true;
933
958
  }
959
+ rejectClosedOwner(socket, request) {
960
+ if (!this.closed) return false;
961
+ writeQueueMessage(socket, {
962
+ ...makeQueueOwnerError(request.requestId, "Queue owner is shutting down", "QUEUE_OWNER_CLOSED", { retryable: true }),
963
+ ownerGeneration: this.ownerGeneration
964
+ });
965
+ this.endSocket(socket);
966
+ return true;
967
+ }
934
968
  handleControlQueueRequest(socket, request) {
935
969
  if (request.type === "cancel_prompt") {
936
970
  this.handleControlRequest({
@@ -1005,6 +1039,7 @@ var SessionQueueOwner = class SessionQueueOwner {
1005
1039
  return false;
1006
1040
  }
1007
1041
  enqueuePromptRequest(socket, request) {
1042
+ this.taskSockets.add(socket);
1008
1043
  const task = {
1009
1044
  requestId: request.requestId,
1010
1045
  message: request.message,
@@ -1026,7 +1061,7 @@ var SessionQueueOwner = class SessionQueueOwner {
1026
1061
  });
1027
1062
  },
1028
1063
  close: () => {
1029
- if (!socket.destroyed) socket.end();
1064
+ this.endSocket(socket);
1030
1065
  }
1031
1066
  };
1032
1067
  if (!this.enqueue(task)) return;
@@ -1038,19 +1073,20 @@ var SessionQueueOwner = class SessionQueueOwner {
1038
1073
  if (!request.waitForCompletion) task.close();
1039
1074
  }
1040
1075
  handleConnection(socket) {
1076
+ this.sockets.add(socket);
1041
1077
  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
- }
1078
+ socket.once("close", () => {
1079
+ this.sockets.delete(socket);
1080
+ this.taskSockets.delete(socket);
1081
+ this.drainingSockets.delete(socket);
1082
+ });
1047
1083
  let buffer = "";
1048
1084
  let handled = false;
1049
1085
  const processLine = (line) => {
1050
1086
  if (handled) return;
1051
1087
  handled = true;
1052
1088
  const request = this.parseRequestLine(socket, line);
1053
- if (!request || this.rejectStaleOwnerGeneration(socket, request)) return;
1089
+ if (!request || this.rejectClosedOwner(socket, request) || this.rejectStaleOwnerGeneration(socket, request)) return;
1054
1090
  if (this.handleControlQueueRequest(socket, request)) return;
1055
1091
  if (request.type !== "submit_prompt") {
1056
1092
  this.failRequest(socket, request.requestId, "Invalid queue request", "QUEUE_REQUEST_INVALID");
@@ -1112,7 +1148,8 @@ function makeMalformedQueueMessageError() {
1112
1148
  function emitQueueOwnerError(formatter, policy, sessionId, message) {
1113
1149
  formatter.setContext({ sessionId });
1114
1150
  const queueErrorAlreadyEmitted = policy?.queueErrorAlreadyEmitted ?? true;
1115
- if (message.outputAlreadyEmitted !== true || !queueErrorAlreadyEmitted) {
1151
+ const shouldEmitInFormatter = message.outputAlreadyEmitted !== true || !queueErrorAlreadyEmitted;
1152
+ if (shouldEmitInFormatter) {
1116
1153
  formatter.onError({
1117
1154
  code: message.code ?? "RUNTIME",
1118
1155
  detailCode: message.detailCode,
@@ -1123,7 +1160,7 @@ function emitQueueOwnerError(formatter, policy, sessionId, message) {
1123
1160
  });
1124
1161
  formatter.flush();
1125
1162
  }
1126
- return queueConnectionErrorFromOwner(message, queueErrorAlreadyEmitted);
1163
+ return queueConnectionErrorFromOwner(message, queueErrorAlreadyEmitted || shouldEmitInFormatter);
1127
1164
  }
1128
1165
  function parseQueueOwnerResponseLine(owner, requestId, line) {
1129
1166
  let parsed;
@@ -2527,6 +2564,51 @@ const DISCARD_OUTPUT_FORMATTER = {
2527
2564
  onPermissionEscalation() {},
2528
2565
  flush() {}
2529
2566
  };
2567
+ function markOutputAlreadyEmitted(error, outputAlreadyEmitted) {
2568
+ if (!outputAlreadyEmitted || !error || typeof error !== "object") return;
2569
+ error.outputAlreadyEmitted = true;
2570
+ }
2571
+ async function runWithOptionalInterrupt(options) {
2572
+ if (options.handleProcessInterrupts === false) return await options.run();
2573
+ return await withInterrupt(options.run, options.handleInterrupt);
2574
+ }
2575
+ function attachAcpErrorPayload(error, acp) {
2576
+ if (!acp || !error || typeof error !== "object") return;
2577
+ error.acp = acp;
2578
+ }
2579
+ function rendersAcpErrors(policy) {
2580
+ return policy?.queueErrorAlreadyEmitted ?? true;
2581
+ }
2582
+ function normalizedErrorText(value) {
2583
+ return value.trim().toLowerCase();
2584
+ }
2585
+ function outboundAcpErrorMatches(error, acp) {
2586
+ const errorText = normalizedErrorText(formatErrorMessage(error));
2587
+ const details = acp.data?.details;
2588
+ const candidateText = normalizedErrorText(typeof details === "string" && details.trim().length > 0 ? details : acp.message);
2589
+ return errorText === candidateText || errorText.includes(candidateText);
2590
+ }
2591
+ var AcpErrorTracker = class {
2592
+ latestInbound;
2593
+ outbound = [];
2594
+ reset() {
2595
+ this.latestInbound = void 0;
2596
+ this.outbound.length = 0;
2597
+ }
2598
+ observe(output, direction, message) {
2599
+ output.onAcpMessage(message);
2600
+ const acp = extractAcpError(message);
2601
+ if (!acp) return;
2602
+ if (direction === "inbound") {
2603
+ this.latestInbound = acp;
2604
+ return;
2605
+ }
2606
+ this.outbound.push(acp);
2607
+ }
2608
+ match(error) {
2609
+ return this.latestInbound ?? this.outbound.findLast((acp) => outboundAcpErrorMatches(error, acp));
2610
+ }
2611
+ };
2530
2612
  function toPromptResult(stopReason, sessionId, client) {
2531
2613
  return {
2532
2614
  stopReason,
@@ -2636,6 +2718,11 @@ function filterRecoverableLoadFallbackOutput(messages) {
2636
2718
  return true;
2637
2719
  });
2638
2720
  }
2721
+ function filterBufferedConnectOutput(messages, loadError) {
2722
+ if (loadError == null) return messages;
2723
+ const filteredMessages = new Set(filterRecoverableLoadFallbackOutput(messages.map(({ message }) => message)));
2724
+ return messages.filter(({ message }) => filteredMessages.has(message));
2725
+ }
2639
2726
  function emitPromptRetryNotice(params) {
2640
2727
  if (params.suppressSdkConsoleErrors) return;
2641
2728
  process.stderr.write(`[acpx] prompt failed (${formatErrorMessage(params.error)}), retrying in ${params.delayMs}ms (attempt ${params.attempt}/${params.maxRetries})\n`);
@@ -2687,6 +2774,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
2687
2774
  authCredentials: options.authCredentials,
2688
2775
  authPolicy: options.authPolicy,
2689
2776
  outputFormatter,
2777
+ errorEmissionPolicy: { queueErrorAlreadyEmitted: true },
2690
2778
  timeoutMs: task.timeoutMs,
2691
2779
  suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
2692
2780
  verbose: options.verbose,
@@ -2695,6 +2783,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
2695
2783
  onClientAvailable: options.onClientAvailable,
2696
2784
  onClientClosed: options.onClientClosed,
2697
2785
  onPromptActive: options.onPromptActive,
2786
+ handleProcessInterrupts: options.handleProcessInterrupts,
2698
2787
  client: options.sharedClient
2699
2788
  };
2700
2789
  }
@@ -2739,6 +2828,7 @@ async function runQueuedTask(sessionRecordId, task, options) {
2739
2828
  async function runSessionPrompt(options) {
2740
2829
  const stopTotalTimer = startPerfTimer("runtime.prompt.total");
2741
2830
  const output = options.outputFormatter;
2831
+ const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
2742
2832
  const record = await measurePerf("session.resolve_prompt_record", async () => {
2743
2833
  return await resolveSessionRecord(options.sessionRecordId);
2744
2834
  });
@@ -2761,7 +2851,7 @@ async function runSessionPrompt(options) {
2761
2851
  let bufferingConnectOutput = true;
2762
2852
  let promptTurnActive = false;
2763
2853
  let promptTurnHadSideEffects = false;
2764
- let sawAcpMessage = false;
2854
+ const acpErrors = new AcpErrorTracker();
2765
2855
  let eventWriterClosed = false;
2766
2856
  const closeEventWriter = async (checkpoint) => {
2767
2857
  if (eventWriterClosed) return;
@@ -2824,16 +2914,18 @@ async function runSessionPrompt(options) {
2824
2914
  });
2825
2915
  client.setEventHandlers({
2826
2916
  onAcpMessage: (direction, message) => {
2827
- sawAcpMessage = true;
2828
2917
  pendingMessages.push(message);
2829
2918
  options.onAcpMessage?.(direction, message);
2830
2919
  },
2831
- onAcpOutputMessage: (_direction, message) => {
2920
+ onAcpOutputMessage: (direction, message) => {
2832
2921
  if (bufferingConnectOutput) {
2833
- pendingConnectOutputMessages.push(message);
2922
+ pendingConnectOutputMessages.push({
2923
+ direction,
2924
+ message
2925
+ });
2834
2926
  return;
2835
2927
  }
2836
- output.onAcpMessage(message);
2928
+ acpErrors.observe(output, direction, message);
2837
2929
  },
2838
2930
  onSessionUpdate: (notification) => {
2839
2931
  if (promptTurnActive) promptTurnHadSideEffects = true;
@@ -2898,8 +2990,8 @@ async function runSessionPrompt(options) {
2898
2990
  };
2899
2991
  const flushConnectOutput = (loadError) => {
2900
2992
  bufferingConnectOutput = false;
2901
- const messages = loadError == null ? pendingConnectOutputMessages : filterRecoverableLoadFallbackOutput(pendingConnectOutputMessages);
2902
- for (const message of messages) output.onAcpMessage(message);
2993
+ const outputMessages = filterBufferedConnectOutput(pendingConnectOutputMessages, loadError);
2994
+ for (const { direction, message } of outputMessages) acpErrors.observe(output, direction, message);
2903
2995
  pendingConnectOutputMessages.length = 0;
2904
2996
  };
2905
2997
  const connectForPrompt = async () => {
@@ -2946,6 +3038,7 @@ async function runSessionPrompt(options) {
2946
3038
  };
2947
3039
  };
2948
3040
  const runPromptAttempt = async (sessionId, attempt) => {
3041
+ acpErrors.reset();
2949
3042
  const promptStartedAt = Date.now();
2950
3043
  const response = await measurePerf("runtime.prompt.agent_turn", async () => {
2951
3044
  return await runPromptTurn({
@@ -2973,14 +3066,19 @@ async function runSessionPrompt(options) {
2973
3066
  promptTurnActive = false;
2974
3067
  applyLifecycleSnapshotToRecord(record, snapshot);
2975
3068
  emitPromptDisconnectNotice(snapshot, options.verbose);
2976
- const normalizedError = normalizeOutputError(error, { origin: "runtime" });
3069
+ const matchedAcpError = acpErrors.match(error);
3070
+ const normalizedError = normalizeOutputError(error, {
3071
+ origin: "runtime",
3072
+ acp: matchedAcpError
3073
+ });
2977
3074
  await flushPendingMessages(false).catch(() => {});
2978
3075
  output.flush();
2979
3076
  record.lastUsedAt = isoNow();
2980
3077
  applyConversation(record, conversation);
2981
3078
  record.acpx = acpxState;
2982
3079
  const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
2983
- propagated.outputAlreadyEmitted = sawAcpMessage;
3080
+ attachAcpErrorPayload(propagated, normalizedError.acp);
3081
+ propagated.outputAlreadyEmitted = matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted;
2984
3082
  propagated.normalizedOutputError = normalizedError;
2985
3083
  throw propagated;
2986
3084
  };
@@ -3006,37 +3104,48 @@ async function runSessionPrompt(options) {
3006
3104
  stopTotalTimer();
3007
3105
  return response;
3008
3106
  };
3107
+ const runPrompt = async () => {
3108
+ const { sessionId: activeSessionId, resumed, loadError } = await connectForPrompt();
3109
+ await applyPromptModelIfAdvertised({
3110
+ client,
3111
+ sessionId: activeSessionId,
3112
+ requestedModel: sessionOptions?.model,
3113
+ record,
3114
+ timeoutMs: options.timeoutMs,
3115
+ suppressWarnings: options.suppressSdkConsoleErrors
3116
+ });
3117
+ acpxState = cloneSessionAcpxState(record.acpx);
3118
+ output.setContext({ sessionId: record.acpxRecordId });
3119
+ await liveCheckpoint.checkpoint();
3120
+ const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
3121
+ promptTurnActive = false;
3122
+ return {
3123
+ ...toPromptResult(response.stopReason, record.acpxRecordId, client),
3124
+ record,
3125
+ resumed,
3126
+ loadError
3127
+ };
3128
+ };
3129
+ const handleInterrupt = async () => {
3130
+ await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3131
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3132
+ record.lastUsedAt = isoNow();
3133
+ applyConversation(record, conversation);
3134
+ record.acpx = acpxState;
3135
+ await flushPendingMessages(false).catch(() => {});
3136
+ if (ownClient) await client.close();
3137
+ };
3009
3138
  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();
3139
+ return await runWithOptionalInterrupt({
3140
+ handleProcessInterrupts: options.handleProcessInterrupts,
3141
+ run: runPrompt,
3142
+ handleInterrupt
3039
3143
  });
3144
+ } catch (error) {
3145
+ const matchedAcpError = acpErrors.match(error);
3146
+ attachAcpErrorPayload(error, matchedAcpError);
3147
+ markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
3148
+ throw error;
3040
3149
  } finally {
3041
3150
  if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", stopTotalTimer())}\n`);
3042
3151
  else stopTotalTimer();
@@ -3054,8 +3163,10 @@ async function runSessionPrompt(options) {
3054
3163
  }
3055
3164
  async function runOnce(options) {
3056
3165
  const output = options.outputFormatter;
3166
+ const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
3057
3167
  let promptTurnActive = false;
3058
3168
  let promptTurnHadSideEffects = false;
3169
+ const acpErrors = new AcpErrorTracker();
3059
3170
  const client = new AcpClient({
3060
3171
  agentCommand: options.agentCommand,
3061
3172
  cwd: absolutePath(options.cwd),
@@ -3069,7 +3180,9 @@ async function runOnce(options) {
3069
3180
  suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3070
3181
  verbose: options.verbose,
3071
3182
  onAcpMessage: options.onAcpMessage,
3072
- onAcpOutputMessage: (_direction, message) => output.onAcpMessage(message),
3183
+ onAcpOutputMessage: (direction, message) => {
3184
+ acpErrors.observe(output, direction, message);
3185
+ },
3073
3186
  onSessionUpdate: (notification) => {
3074
3187
  if (promptTurnActive) promptTurnHadSideEffects = true;
3075
3188
  options.onSessionUpdate?.(notification);
@@ -3085,6 +3198,7 @@ async function runOnce(options) {
3085
3198
  sessionOptions: options.sessionOptions
3086
3199
  });
3087
3200
  const runExecPromptAttempt = async (sessionId) => {
3201
+ acpErrors.reset();
3088
3202
  return await measurePerf("runtime.exec.prompt", async () => {
3089
3203
  return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
3090
3204
  });
@@ -3130,6 +3244,11 @@ async function runOnce(options) {
3130
3244
  await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3131
3245
  await client.close();
3132
3246
  });
3247
+ } catch (error) {
3248
+ const matchedAcpError = acpErrors.match(error);
3249
+ attachAcpErrorPayload(error, matchedAcpError);
3250
+ markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
3251
+ throw error;
3133
3252
  } finally {
3134
3253
  await client.close();
3135
3254
  }
@@ -3147,6 +3266,7 @@ async function sendSessionDirect(options) {
3147
3266
  authPolicy: options.authPolicy,
3148
3267
  terminal: options.terminal,
3149
3268
  outputFormatter: options.outputFormatter,
3269
+ errorEmissionPolicy: options.errorEmissionPolicy,
3150
3270
  onAcpMessage: options.onAcpMessage,
3151
3271
  onSessionUpdate: options.onSessionUpdate,
3152
3272
  onClientOperation: options.onClientOperation,
@@ -3161,6 +3281,7 @@ async function sendSessionDirect(options) {
3161
3281
  //#region src/cli/session/queue-owner-runtime.ts
3162
3282
  const QUEUE_OWNER_STARTUP_MAX_ATTEMPTS = 120;
3163
3283
  const QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
3284
+ const QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS = 750;
3164
3285
  async function submitToRunningOwner(options, waitForCompletion) {
3165
3286
  return await trySubmitToRunningOwner({
3166
3287
  sessionId: options.sessionId,
@@ -3252,8 +3373,8 @@ function logQueueOwnerReady(params) {
3252
3373
  async function closeQueueOwnerRuntime(params) {
3253
3374
  if (params.heartbeatTimer) clearInterval(params.heartbeatTimer);
3254
3375
  params.turnController.beginClosing();
3255
- await params.owner?.close();
3256
3376
  await params.sharedClient.close().catch(() => {});
3377
+ await params.owner?.close();
3257
3378
  await writeQueueOwnerLifecycleSnapshot(params.sessionId, params.sharedClient);
3258
3379
  await releaseQueueOwnerLease(params.lease);
3259
3380
  if (params.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${params.sessionId}\n`);
@@ -3265,6 +3386,75 @@ async function writeQueueOwnerLifecycleSnapshot(sessionId, sharedClient) {
3265
3386
  await writeSessionRecord(record);
3266
3387
  } catch {}
3267
3388
  }
3389
+ async function settlesWithin(promise, timeoutMs) {
3390
+ let timer;
3391
+ const timeout = new Promise((resolve) => {
3392
+ timer = setTimeout(() => resolve(false), timeoutMs);
3393
+ });
3394
+ try {
3395
+ return await Promise.race([promise.then(() => true, () => true), timeout]);
3396
+ } finally {
3397
+ if (timer) clearTimeout(timer);
3398
+ }
3399
+ }
3400
+ function createQueueOwnerShutdownController(params) {
3401
+ let requested = false;
3402
+ let activeTurn;
3403
+ let activeTurnShutdown;
3404
+ let shutdownPromise;
3405
+ const drainActiveTurn = async () => {
3406
+ const turn = activeTurn;
3407
+ if (!turn) return;
3408
+ params.turnController.requestCancel().catch((error) => {
3409
+ logDeferredCancelFailure(error, params.verbose);
3410
+ });
3411
+ if (!await settlesWithin(turn, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
3412
+ await turn.catch(() => {});
3413
+ };
3414
+ const request = () => {
3415
+ requested = true;
3416
+ params.stopHeartbeat();
3417
+ params.getOwner()?.beginShutdown();
3418
+ activeTurnShutdown ??= drainActiveTurn();
3419
+ };
3420
+ return {
3421
+ get requested() {
3422
+ return requested;
3423
+ },
3424
+ request,
3425
+ setActiveTurn: (turn) => {
3426
+ activeTurn = turn;
3427
+ },
3428
+ clearActiveTurn: (turn) => {
3429
+ if (activeTurn === turn) activeTurn = void 0;
3430
+ },
3431
+ shutdown: () => {
3432
+ request();
3433
+ shutdownPromise ??= (async () => {
3434
+ await activeTurnShutdown;
3435
+ await closeQueueOwnerRuntime({
3436
+ lease: params.lease,
3437
+ owner: params.getOwner(),
3438
+ heartbeatTimer: void 0,
3439
+ turnController: params.turnController,
3440
+ sharedClient: params.sharedClient,
3441
+ sessionId: params.sessionId,
3442
+ verbose: params.verbose
3443
+ });
3444
+ })();
3445
+ return shutdownPromise;
3446
+ }
3447
+ };
3448
+ }
3449
+ function applyRequestedShutdown(owner, shutdown) {
3450
+ if (shutdown.requested) owner.beginShutdown();
3451
+ }
3452
+ function startQueueOwnerHeartbeat(params) {
3453
+ if (!params.enabled) return;
3454
+ return setInterval(() => {
3455
+ refreshQueueOwnerLease(params.lease, { queueDepth: params.owner.queueDepth() }).catch(() => {});
3456
+ }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
3457
+ }
3268
3458
  async function runSessionQueueOwner(options) {
3269
3459
  const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
3270
3460
  path: options.mcpConfigPath,
@@ -3309,6 +3499,26 @@ async function runSessionQueueOwner(options) {
3309
3499
  turnController.endTurn();
3310
3500
  }
3311
3501
  };
3502
+ const shutdown = createQueueOwnerShutdownController({
3503
+ lease,
3504
+ getOwner: () => owner,
3505
+ stopHeartbeat: () => {
3506
+ if (heartbeatTimer) {
3507
+ clearInterval(heartbeatTimer);
3508
+ heartbeatTimer = void 0;
3509
+ }
3510
+ },
3511
+ turnController,
3512
+ sharedClient,
3513
+ sessionId: options.sessionId,
3514
+ verbose: options.verbose
3515
+ });
3516
+ const onSignal = () => {
3517
+ shutdown.request();
3518
+ };
3519
+ process.once("SIGINT", onSignal);
3520
+ process.once("SIGTERM", onSignal);
3521
+ process.once("SIGHUP", onSignal);
3312
3522
  try {
3313
3523
  owner = await SessionQueueOwner.start(lease, {
3314
3524
  cancelPrompt: async () => {
@@ -3331,6 +3541,7 @@ async function runSessionQueueOwner(options) {
3331
3541
  refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
3332
3542
  }
3333
3543
  });
3544
+ applyRequestedShutdown(owner, shutdown);
3334
3545
  logQueueOwnerReady({
3335
3546
  sessionId: options.sessionId,
3336
3547
  ttlMs,
@@ -3338,16 +3549,18 @@ async function runSessionQueueOwner(options) {
3338
3549
  verbose: options.verbose
3339
3550
  });
3340
3551
  await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
3341
- heartbeatTimer = setInterval(() => {
3342
- refreshQueueOwnerLease(lease, { queueDepth: owner?.queueDepth() ?? 0 }).catch(() => {});
3343
- }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
3552
+ heartbeatTimer = startQueueOwnerHeartbeat({
3553
+ enabled: !shutdown.requested,
3554
+ lease,
3555
+ owner
3556
+ });
3344
3557
  let isFirstTask = true;
3345
3558
  while (true) {
3346
3559
  const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
3347
3560
  const task = await owner.nextTask(pollTimeoutMs);
3348
3561
  if (!task) break;
3349
3562
  isFirstTask = false;
3350
- await runPromptTurn(async () => {
3563
+ const turnPromise = runPromptTurn(async () => {
3351
3564
  try {
3352
3565
  await runQueuedTask(options.sessionId, task, {
3353
3566
  sharedClient,
@@ -3364,23 +3577,25 @@ async function runSessionQueueOwner(options) {
3364
3577
  onPromptActive: async () => {
3365
3578
  turnController.markPromptActive();
3366
3579
  await applyPendingCancel();
3367
- }
3580
+ },
3581
+ handleProcessInterrupts: false
3368
3582
  });
3369
3583
  } finally {
3370
3584
  checkpointPerfMetricsCapture();
3371
3585
  }
3372
3586
  });
3587
+ shutdown.setActiveTurn(turnPromise);
3588
+ try {
3589
+ await turnPromise;
3590
+ } finally {
3591
+ shutdown.clearActiveTurn(turnPromise);
3592
+ }
3373
3593
  }
3374
3594
  } finally {
3375
- await closeQueueOwnerRuntime({
3376
- lease,
3377
- owner,
3378
- heartbeatTimer,
3379
- turnController,
3380
- sharedClient,
3381
- sessionId: options.sessionId,
3382
- verbose: options.verbose
3383
- });
3595
+ process.off("SIGINT", onSignal);
3596
+ process.off("SIGTERM", onSignal);
3597
+ process.off("SIGHUP", onSignal);
3598
+ await shutdown.shutdown();
3384
3599
  }
3385
3600
  }
3386
3601
  async function sendSession(options) {
@@ -3695,6 +3910,12 @@ function asRecord(value) {
3695
3910
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
3696
3911
  return value;
3697
3912
  }
3913
+ function parseJsonRpcErrorSummary(message) {
3914
+ const fallback = parseJsonRpcErrorMessage(message);
3915
+ if (!fallback) return;
3916
+ const details = asRecord(asRecord(message.error)?.data)?.details;
3917
+ return typeof details === "string" && details.trim().length > 0 ? details.trim() : fallback;
3918
+ }
3698
3919
  function extractJsonRpcMethod(message) {
3699
3920
  return Object.hasOwn(message, "method") ? message.method?.toString() : void 0;
3700
3921
  }
@@ -4060,7 +4281,7 @@ var TextOutputFormatter = class {
4060
4281
  this.renderDone(stopReason);
4061
4282
  return;
4062
4283
  }
4063
- const errorMessage = parseJsonRpcErrorMessage(message);
4284
+ const errorMessage = parseJsonRpcErrorSummary(message);
4064
4285
  if (errorMessage) this.onError({
4065
4286
  code: "RUNTIME",
4066
4287
  origin: "acp",
@@ -4286,7 +4507,11 @@ var QuietOutputFormatter = class {
4286
4507
  this.flushMetadata(message);
4287
4508
  }
4288
4509
  }
4289
- onError(_params) {}
4510
+ onError(params) {
4511
+ const qualifier = params.detailCode ? `${params.code} ${params.detailCode}` : params.code;
4512
+ const message = preferredAcpErrorDetails(params.acp) ?? params.message;
4513
+ this.stderr.write(`[acpx] error: ${qualifier} ${message.replace(/\r\n?|\n/g, " ")}\n`);
4514
+ }
4290
4515
  onPermissionEscalation(_event) {}
4291
4516
  flush() {}
4292
4517
  flushBufferedOutput() {
@@ -4347,6 +4572,10 @@ var QuietOutputFormatter = class {
4347
4572
  if (typeof cost === "string" && cost.trim()) return `[acpx] cost: ${cost.trim()}`;
4348
4573
  }
4349
4574
  };
4575
+ function preferredAcpErrorDetails(acp) {
4576
+ const details = asRecord(acp?.data)?.details;
4577
+ return typeof details === "string" && details.trim().length > 0 ? details.trim() : void 0;
4578
+ }
4350
4579
  function createOutputFormatter(format, options = {}) {
4351
4580
  const stdout = options.stdout ?? process.stdout;
4352
4581
  const stderr = options.stderr ?? process.stderr;
@@ -4361,4 +4590,4 @@ function createOutputFormatter(format, options = {}) {
4361
4590
  //#endregion
4362
4591
  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
4592
 
4364
- //# sourceMappingURL=output-Smw6kFL_.js.map
4593
+ //# sourceMappingURL=output-CjdF5rHk.js.map