@wrongstack/acp 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1015,6 +1015,19 @@ import {
1015
1015
  var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
1016
1016
  var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
1017
1017
  var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
1018
+ var STDERR_TAIL_CHARS = 4096;
1019
+ function echoChildStderr() {
1020
+ const v = process.env.WRONGSTACK_DEBUG;
1021
+ return !!v && v !== "0" && v !== "false";
1022
+ }
1023
+ function lastLine(text) {
1024
+ const lines = text.split(/\r?\n/);
1025
+ for (let i = lines.length - 1; i >= 0; i--) {
1026
+ const line = lines[i]?.trim();
1027
+ if (line) return line.length > 300 ? `${line.slice(0, 300)}\u2026` : line;
1028
+ }
1029
+ return "";
1030
+ }
1018
1031
  function takeLines(pending, chunk) {
1019
1032
  const lines = [];
1020
1033
  let start = 0;
@@ -1196,6 +1209,8 @@ var StdioTransport = class {
1196
1209
  var ClientTransport = class {
1197
1210
  child = null;
1198
1211
  buffer = "";
1212
+ stderrTail = "";
1213
+ stopRequested = false;
1199
1214
  handlers = /* @__PURE__ */ new Set();
1200
1215
  claimHandlers = /* @__PURE__ */ new Set();
1201
1216
  closeHandlers = /* @__PURE__ */ new Set();
@@ -1296,9 +1311,11 @@ var ClientTransport = class {
1296
1311
  }
1297
1312
  const idx = this.buffer.indexOf("[wstack-acp]\n");
1298
1313
  if (idx !== -1) {
1299
- this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
1314
+ const rest = this.buffer.slice(idx + "[wstack-acp]\n".length);
1315
+ this.buffer = "";
1300
1316
  child.stdout.removeListener("data", waitForMarker);
1301
1317
  onReady();
1318
+ if (rest) this.onChildData(rest);
1302
1319
  }
1303
1320
  };
1304
1321
  child.stdout.on("data", waitForMarker);
@@ -1361,6 +1378,7 @@ var ClientTransport = class {
1361
1378
  }
1362
1379
  stop(reason = "transport stopped") {
1363
1380
  this.closed = true;
1381
+ this.stopRequested = true;
1364
1382
  this.resolveRead?.(null);
1365
1383
  this.resolveRead = null;
1366
1384
  this.buffer = "";
@@ -1398,7 +1416,8 @@ var ClientTransport = class {
1398
1416
  }
1399
1417
  }
1400
1418
  onChildError(chunk) {
1401
- writeErr(`[acp-child stderr] ${chunk}`);
1419
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
1420
+ if (echoChildStderr()) writeErr(`[acp-child stderr] ${chunk}`);
1402
1421
  }
1403
1422
  onChildClose(code) {
1404
1423
  this.closed = true;
@@ -1410,11 +1429,16 @@ var ClientTransport = class {
1410
1429
  this.queuedChars = 0;
1411
1430
  this.handlers.clear();
1412
1431
  this.claimHandlers.clear();
1413
- if (code !== 0 && code !== null) {
1432
+ const failed = code !== 0 && code !== null && !this.stopRequested;
1433
+ if (failed) {
1434
+ const tail = echoChildStderr() ? "" : this.stderrTail.trimEnd();
1414
1435
  writeErr(`[acp-child exited with code ${code}]
1415
- `);
1436
+ ${tail ? `${tail}
1437
+ ` : ""}`);
1416
1438
  }
1417
- this.notifyClosed(`agent process exited with code ${code}`);
1439
+ const cause = failed ? lastLine(this.stderrTail) : "";
1440
+ this.stderrTail = "";
1441
+ this.notifyClosed(`agent process exited with code ${code}${cause ? `: ${cause}` : ""}`);
1418
1442
  }
1419
1443
  dispatch(msg, chars = JSON.stringify(msg).length) {
1420
1444
  if (this.resolveRead) {
@@ -1939,6 +1963,9 @@ function isAuthRequiredError(err) {
1939
1963
  }
1940
1964
 
1941
1965
  // src/client/acp-request-state.ts
1966
+ function touchIdleRequests(pending) {
1967
+ for (const p of pending.values()) p.rearm?.();
1968
+ }
1942
1969
  function rejectPendingRequests(pending, reason) {
1943
1970
  for (const p of pending.values()) {
1944
1971
  clearTimeout(p.timeoutHandle);
@@ -2135,11 +2162,21 @@ async function executeMcpMessage(ctx, connectionId, message) {
2135
2162
  }
2136
2163
  async function executeCreateSession(ctx) {
2137
2164
  const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
2138
- const id = ctx.allocId();
2139
- const result = await ctx.sendRequest(id, "session/new", {
2165
+ const params = {
2140
2166
  cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
2141
- mcpServers: servers
2142
- });
2167
+ mcpServers: servers,
2168
+ ...ctx.opts.sessionMeta ? { _meta: ctx.opts.sessionMeta } : {}
2169
+ };
2170
+ const resumeId = ctx.opts.resumeSessionId;
2171
+ if (resumeId && ctx.agentCapabilities.sessionCapabilities?.resume) {
2172
+ const resumed = await ctx.sendRequest(ctx.allocId(), "session/resume", { ...params, sessionId: resumeId }).catch((err) => ({ refused: err }));
2173
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2174
+ if (!isJsonRpcError(resumed) && !resumed.refused) {
2175
+ return adoptSessionMode(ctx, resumeId, resumed);
2176
+ }
2177
+ }
2178
+ const id = ctx.allocId();
2179
+ const result = await ctx.sendRequest(id, "session/new", params);
2143
2180
  if (isJsonRpcError(result)) {
2144
2181
  throw new ACPSessionError(
2145
2182
  "session_create_failed",
@@ -2151,8 +2188,53 @@ async function executeCreateSession(ctx) {
2151
2188
  if (typeof sessionId !== "string" || sessionId.length === 0) {
2152
2189
  throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
2153
2190
  }
2191
+ return adoptSessionMode(ctx, sessionId, result);
2192
+ }
2193
+ async function executeOpenSession(ctx, create) {
2194
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2195
+ if (ctx.sessionId) return { sessionId: ctx.sessionId, resumed: false };
2196
+ const sessionId = await create();
2197
+ ctx.setSessionId(sessionId);
2198
+ return { sessionId, resumed: sessionId === ctx.opts.resumeSessionId };
2199
+ }
2200
+ async function adoptSessionMode(ctx, sessionId, result) {
2201
+ const want = ctx.opts.initialModeId;
2202
+ const modes = result.modes;
2203
+ const offered = Array.isArray(modes?.availableModes) && modes.availableModes.some((m) => m?.id === want);
2204
+ if (want && !offered && ctx.opts.requireInitialMode) {
2205
+ throw new ACPSessionError(
2206
+ "session_create_failed",
2207
+ `the agent does not offer the "${want}" session mode this host requires`,
2208
+ result
2209
+ );
2210
+ }
2211
+ if (want && offered && modes?.currentModeId !== want) {
2212
+ await executeSetMode(ctx, sessionId, want);
2213
+ }
2154
2214
  return sessionId;
2155
2215
  }
2216
+ async function executeSteer(ctx, supported, text) {
2217
+ if (ctx.closed || !ctx.sessionId || !supported) return "unsupported";
2218
+ const result = await ctx.sendRequest(
2219
+ ctx.allocId(),
2220
+ "_session/steering",
2221
+ {
2222
+ sessionId: ctx.sessionId,
2223
+ prompt: [{ type: "text", text }],
2224
+ _meta: { steering: { idleBehavior: "promptRequired" } }
2225
+ },
2226
+ 1e4
2227
+ );
2228
+ if (isJsonRpcError(result)) {
2229
+ throw new ACPSessionError(
2230
+ "prompt_failed",
2231
+ `_session/steering failed: ${result.message}`,
2232
+ result
2233
+ );
2234
+ }
2235
+ const outcome = result?.outcome;
2236
+ return outcome === "injected" || outcome === "promptRequired" || outcome === "startedNewTurn" ? outcome : "unsupported";
2237
+ }
2156
2238
 
2157
2239
  // src/client/acp-session-auth.ts
2158
2240
  function ensureOpen(host) {
@@ -2759,6 +2841,7 @@ function emptyRunResult(stopReason) {
2759
2841
  function createSessionScratch() {
2760
2842
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2761
2843
  }
2844
+ var MESSAGE_SEPARATOR = "\n\n";
2762
2845
  function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2763
2846
  const update = msg.params?.update;
2764
2847
  if (typeof update !== "object" || update === null) return;
@@ -2768,6 +2851,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2768
2851
  case "agent_message_chunk": {
2769
2852
  const text = extractText(u.content);
2770
2853
  if (text) {
2854
+ if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
2855
+ scratch.text += MESSAGE_SEPARATOR;
2856
+ }
2857
+ scratch.messageBreak = false;
2771
2858
  scratch.text += text;
2772
2859
  emitProgress2({ type: "message", text });
2773
2860
  }
@@ -2792,7 +2879,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2792
2879
  emitProgress2({ type: "plan", entries: u.entries });
2793
2880
  }
2794
2881
  return;
2795
- case "usage_update":
2882
+ case "usage_update": {
2883
+ const rateLimit = u._meta?.["_claude/rateLimit"];
2884
+ if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
2796
2885
  if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
2797
2886
  const usage = {
2798
2887
  used: u.used,
@@ -2803,6 +2892,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2803
2892
  emitProgress2({ type: "usage", usage });
2804
2893
  }
2805
2894
  return;
2895
+ }
2806
2896
  case "available_commands_update":
2807
2897
  case "current_mode_update":
2808
2898
  case "config_option_update":
@@ -2815,6 +2905,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2815
2905
  return;
2816
2906
  }
2817
2907
  }
2908
+ function toolOutputText(u) {
2909
+ const joined = (blocks) => blocks.map(extractText).filter((text) => text.length > 0).join("\n");
2910
+ if (Array.isArray(u.content)) {
2911
+ const text = joined(
2912
+ u.content.map(
2913
+ (c) => c && typeof c === "object" && c.type === "content" ? c.content : void 0
2914
+ )
2915
+ );
2916
+ if (text) return text;
2917
+ }
2918
+ if (typeof u.rawOutput === "string") return u.rawOutput || void 0;
2919
+ if (Array.isArray(u.rawOutput)) return joined(u.rawOutput) || void 0;
2920
+ return void 0;
2921
+ }
2818
2922
  function captureToolCall(u, isNew, scratch, emitProgress2) {
2819
2923
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2820
2924
  if (!toolCallId) return;
@@ -2825,12 +2929,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
2825
2929
  kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2826
2930
  status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2827
2931
  rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2828
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2932
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
2933
+ output: toolOutputText(u) ?? prev?.output
2829
2934
  };
2830
2935
  scratch.toolCalls.set(toolCallId, record);
2936
+ if (scratch.text) scratch.messageBreak = true;
2831
2937
  if (Array.isArray(u.content)) {
2832
2938
  for (const c of u.content) {
2833
2939
  if (c && typeof c === "object" && c.type === "diff") {
2940
+ const key = `${toolCallId}\0${c.path}\0${c.oldText ?? ""}\0${c.newText}`;
2941
+ scratch.seenDiffs ??= /* @__PURE__ */ new Set();
2942
+ if (scratch.seenDiffs.has(key)) continue;
2943
+ scratch.seenDiffs.add(key);
2834
2944
  const diff = {
2835
2945
  path: c.path,
2836
2946
  oldText: c.oldText,
@@ -2841,10 +2951,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
2841
2951
  }
2842
2952
  }
2843
2953
  }
2844
- emitProgress2({
2845
- type: isNew ? "tool_call" : "tool_call_update",
2846
- toolCall: record
2847
- });
2954
+ scratch.announced ??= /* @__PURE__ */ new Set();
2955
+ if (!scratch.announced.has(toolCallId)) {
2956
+ const hasInput = record.rawInput !== void 0 && Object.keys(record.rawInput).length > 0;
2957
+ if (isNew && !hasInput) return;
2958
+ scratch.announced.add(toolCallId);
2959
+ emitProgress2({ type: "tool_call", toolCall: record });
2960
+ if (isNew || record.status !== "completed" && record.status !== "failed") return;
2961
+ }
2962
+ emitProgress2({ type: "tool_call_update", toolCall: record });
2848
2963
  }
2849
2964
 
2850
2965
  // src/client/acp-session-messages.ts
@@ -2876,6 +2991,7 @@ function handleMessage(msg) {
2876
2991
  }
2877
2992
  return;
2878
2993
  }
2994
+ touchIdleRequests(this.pending);
2879
2995
  if (msg.method === "session/update") {
2880
2996
  const updateSessionId = msg.params?.sessionId;
2881
2997
  if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
@@ -2932,6 +3048,7 @@ function handleMessage(msg) {
2932
3048
  );
2933
3049
  return;
2934
3050
  }
3051
+ if (msg.method.startsWith("_")) return;
2935
3052
  console.warn(
2936
3053
  JSON.stringify({
2937
3054
  level: "warn",
@@ -3041,7 +3158,9 @@ async function prompt(host, blocks, signal, onProgress) {
3041
3158
  sessionId: host.sessionId,
3042
3159
  prompt: blocks
3043
3160
  },
3044
- host.timeoutMs
3161
+ // Timed by silence, not length: a working agent keeps it alive.
3162
+ host.promptIdleTimeoutMs,
3163
+ true
3045
3164
  ),
3046
3165
  new Promise((_, reject) => {
3047
3166
  rejectTurn = reject;
@@ -3057,6 +3176,17 @@ async function prompt(host, blocks, signal, onProgress) {
3057
3176
  if (cancelled || abortedKind) {
3058
3177
  return emptyRunResult("cancelled");
3059
3178
  }
3179
+ if (err instanceof ACPSessionError && err.kind === "stalled") {
3180
+ if (host.sessionId) {
3181
+ host.transport.send({
3182
+ jsonrpc: "2.0",
3183
+ method: "session/cancel",
3184
+ params: { sessionId: host.sessionId }
3185
+ }).catch(() => {
3186
+ });
3187
+ }
3188
+ throw err;
3189
+ }
3060
3190
  const msg = err instanceof Error ? err.message : String(err);
3061
3191
  if (signal.aborted) {
3062
3192
  throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
@@ -3078,17 +3208,31 @@ async function prompt(host, blocks, signal, onProgress) {
3078
3208
  }
3079
3209
  const stopReason = response.stopReason ?? "end_turn";
3080
3210
  const finalText = host.scratch.text;
3211
+ const tokens = readTokenUsage(response.usage);
3081
3212
  return {
3082
3213
  text: finalText,
3083
3214
  stopReason,
3084
3215
  hasText: finalText.length > 0,
3085
3216
  usage: host.scratch.usage,
3217
+ ...tokens ? { tokens } : {},
3086
3218
  plan: host.scratch.plan,
3087
3219
  toolCalls: [...host.scratch.toolCalls.values()],
3088
3220
  diffs: host.scratch.diffs,
3089
3221
  thoughts: host.scratch.thoughts
3090
3222
  };
3091
3223
  }
3224
+ function readTokenUsage(raw) {
3225
+ if (typeof raw !== "object" || raw === null) return void 0;
3226
+ const r = raw;
3227
+ const n = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
3228
+ if (typeof r.inputTokens !== "number" && typeof r.outputTokens !== "number") return void 0;
3229
+ return {
3230
+ input: n(r.inputTokens),
3231
+ output: n(r.outputTokens),
3232
+ cacheRead: n(r.cachedReadTokens),
3233
+ cacheWrite: n(r.cachedWriteTokens)
3234
+ };
3235
+ }
3092
3236
 
3093
3237
  // src/client/permission.ts
3094
3238
  function pickAllow(options) {
@@ -3738,6 +3882,7 @@ var ACPSession = class _ACPSession {
3738
3882
  terminalServer;
3739
3883
  permissionPolicy;
3740
3884
  timeoutMs;
3885
+ promptIdleTimeoutMs;
3741
3886
  opts;
3742
3887
  transportOff = null;
3743
3888
  callbackAbort = new AbortController();
@@ -3753,12 +3898,15 @@ var ACPSession = class _ACPSession {
3753
3898
  agentCapabilities = {};
3754
3899
  agentInfo = null;
3755
3900
  authMethods = [];
3901
+ /** The agent accepts `_session/steering` (see executeSteer). */
3902
+ steeringSupported = false;
3756
3903
  /** Protocol version negotiated with the agent during initialize. */
3757
3904
  negotiatedVersion = ACP_PROTOCOL_VERSION;
3758
3905
  constructor(opts, transport) {
3759
3906
  this.opts = opts;
3760
3907
  this.transport = transport;
3761
3908
  this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
3909
+ this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
3762
3910
  const fsOpts = {
3763
3911
  projectRoot: opts.projectRoot
3764
3912
  };
@@ -3913,6 +4061,8 @@ var ACPSession = class _ACPSession {
3913
4061
  }
3914
4062
  this.negotiatedVersion = r.protocolVersion;
3915
4063
  this.agentCapabilities = r.agentCapabilities ?? {};
4064
+ const meta = result._meta;
4065
+ this.steeringSupported = meta?.steering?.supported === true;
3916
4066
  this.agentInfo = r.agentInfo ?? null;
3917
4067
  this.authMethods = r.authMethods ?? [];
3918
4068
  this.state = "ready";
@@ -3957,6 +4107,10 @@ var ACPSession = class _ACPSession {
3957
4107
  closeSession: () => this.closeSession()
3958
4108
  };
3959
4109
  }
4110
+ /** Start (or, with `resumeSessionId`, resume) the agent session now, not at the first prompt. */
4111
+ async openSession() {
4112
+ return executeOpenSession(this.opContext(), () => this.createSessionWithAuth());
4113
+ }
3960
4114
  async loadSession(sessionId, mcpServers, cwd) {
3961
4115
  return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
3962
4116
  }
@@ -3990,6 +4144,14 @@ var ACPSession = class _ACPSession {
3990
4144
  async disableProvider() {
3991
4145
  return executeDisableProvider(this.opContext());
3992
4146
  }
4147
+ /** Whether {@link steer} can reach the agent. */
4148
+ canSteer() {
4149
+ return this.steeringSupported;
4150
+ }
4151
+ /** Inject a message into the agent's running turn. */
4152
+ async steer(text) {
4153
+ return executeSteer(this.opContext(), this.steeringSupported, text);
4154
+ }
3993
4155
  // ──────────────────────────────────────────────────────────────────────
3994
4156
  // Prompt
3995
4157
  // ──────────────────────────────────────────────────────────────────────
@@ -4008,14 +4170,14 @@ var ACPSession = class _ACPSession {
4008
4170
  cancelLateSession(createPromise) {
4009
4171
  cancelLateAcpSession(this.transport, createPromise);
4010
4172
  }
4011
- async closeSession() {
4173
+ async closeSession(graceMs = 1e4) {
4012
4174
  if (!this.sessionId) return;
4013
4175
  const sid = this.sessionId;
4014
4176
  this.sessionId = null;
4015
4177
  if (this.agentCapabilities.sessionCapabilities?.close) {
4016
4178
  const id = this.allocId();
4017
4179
  try {
4018
- await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
4180
+ await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
4019
4181
  } catch {
4020
4182
  }
4021
4183
  }
@@ -4023,7 +4185,12 @@ var ACPSession = class _ACPSession {
4023
4185
  // ──────────────────────────────────────────────────────────────────────
4024
4186
  // Lifecycle — close
4025
4187
  // ──────────────────────────────────────────────────────────────────────
4026
- async close() {
4188
+ /**
4189
+ * Close the session and stop the agent. `graceMs` bounds how long the
4190
+ * agent may take to answer `session/close` before it is stopped anyway —
4191
+ * shutdown passes a short one so a slow answer cannot outlive the process.
4192
+ */
4193
+ async close(opts = {}) {
4027
4194
  if (this.closed) return;
4028
4195
  this.closed = true;
4029
4196
  this.state = "closed";
@@ -4033,7 +4200,7 @@ var ACPSession = class _ACPSession {
4033
4200
  this.terminalServer.dispose();
4034
4201
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
4035
4202
  try {
4036
- await this.closeSession();
4203
+ await this.closeSession(opts.graceMs);
4037
4204
  } catch {
4038
4205
  }
4039
4206
  }
@@ -4057,24 +4224,37 @@ var ACPSession = class _ACPSession {
4057
4224
  }
4058
4225
  throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
4059
4226
  }
4060
- async sendRequest(id, method, params, timeoutMs) {
4227
+ async sendRequest(id, method, params, timeoutMs, idle = false) {
4061
4228
  return new Promise((resolve5, reject) => {
4062
4229
  const effectiveTimeout = timeoutMs ?? this.timeoutMs;
4063
- const handle = setTimeout(() => {
4230
+ const expire = () => {
4064
4231
  this.pending.delete(id);
4065
4232
  reject(
4066
- new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
4233
+ idle ? new ACPSessionError(
4234
+ "stalled",
4235
+ `${method}: no activity from the agent for ${Math.round(effectiveTimeout / 1e3)}s`
4236
+ ) : new ACPSessionError(
4237
+ "protocol_error",
4238
+ `${method} timed out after ${effectiveTimeout}ms`
4239
+ )
4067
4240
  );
4068
- }, effectiveTimeout);
4069
- this.pending.set(id, {
4241
+ };
4242
+ const entry = {
4070
4243
  method,
4071
4244
  resolve: resolve5,
4072
4245
  reject,
4073
4246
  timeoutMs: effectiveTimeout,
4074
- timeoutHandle: handle
4075
- });
4247
+ timeoutHandle: setTimeout(expire, effectiveTimeout)
4248
+ };
4249
+ if (idle) {
4250
+ entry.rearm = () => {
4251
+ clearTimeout(entry.timeoutHandle);
4252
+ entry.timeoutHandle = setTimeout(expire, effectiveTimeout);
4253
+ };
4254
+ }
4255
+ this.pending.set(id, entry);
4076
4256
  this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
4077
- clearTimeout(handle);
4257
+ clearTimeout(entry.timeoutHandle);
4078
4258
  this.pending.delete(id);
4079
4259
  const msg = err instanceof Error ? err.message : String(err);
4080
4260
  reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
@@ -4178,7 +4358,7 @@ var ACPSession = class _ACPSession {
4178
4358
  progressHandler: this.progressHandler,
4179
4359
  allocId: this.allocId,
4180
4360
  sendRequest: this.sendRequest,
4181
- timeoutMs: this.timeoutMs,
4361
+ promptIdleTimeoutMs: this.promptIdleTimeoutMs,
4182
4362
  scratch: this.scratch
4183
4363
  };
4184
4364
  return this;
@@ -4263,6 +4443,67 @@ var ToolTranslator = class {
4263
4443
  }
4264
4444
  };
4265
4445
 
4446
+ // src/integration/acp-agent-failure.ts
4447
+ var USAGE_LIMIT_PREFIXES = [
4448
+ "You've hit your",
4449
+ "You've reached your",
4450
+ "You're out of usage credits",
4451
+ "Your org is out of usage",
4452
+ "Your seat type doesn't include usage",
4453
+ "Your seat type doesn't include extra usage",
4454
+ "Your usage allocation has been disabled",
4455
+ "Your group's usage limit is set to $0",
4456
+ "requires usage credits",
4457
+ "You're out of extra usage",
4458
+ "Claude AI usage limit reached"
4459
+ ];
4460
+ var DEFAULT_QUOTA_HOLD_MS = 15 * 6e4;
4461
+ function parseQuotaResetAt(text, now = Date.now()) {
4462
+ const epoch = /usage limit reached\|(\d{9,11})/i.exec(text);
4463
+ if (epoch?.[1]) return Number(epoch[1]) * 1e3;
4464
+ const m = /\bresets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i.exec(text);
4465
+ if (!m?.[1]) return void 0;
4466
+ let hour = Number(m[1]);
4467
+ const minute = m[2] ? Number(m[2]) : 0;
4468
+ const meridiem = m[3]?.toLowerCase();
4469
+ if (meridiem === "pm" && hour < 12) hour += 12;
4470
+ if (meridiem === "am" && hour === 12) hour = 0;
4471
+ if (hour > 23 || minute > 59 || !meridiem && !m[2]) return void 0;
4472
+ const at = new Date(now);
4473
+ at.setHours(hour, minute, 0, 0);
4474
+ if (at.getTime() <= now) at.setDate(at.getDate() + 1);
4475
+ return at.getTime();
4476
+ }
4477
+ function classifyAgentFailure(rpc, agent, now = Date.now()) {
4478
+ const text = rpc.message.replace(/^Internal error:\s*/i, "").trim();
4479
+ const errorKind = rpc.data?.errorKind;
4480
+ const isQuota = USAGE_LIMIT_PREFIXES.some((p) => text.startsWith(p) || text.includes(p)) || errorKind === "billing_error" || errorKind === "account_on_hold";
4481
+ if (isQuota) {
4482
+ const resetAt = parseQuotaResetAt(text, now);
4483
+ return {
4484
+ kind: "provider_quota",
4485
+ message: `${agent}: usage quota spent \u2014 ${text || "the account has no usage left"}`,
4486
+ retryable: false,
4487
+ backoffMs: resetAt !== void 0 ? Math.max(0, resetAt - now) : DEFAULT_QUOTA_HOLD_MS
4488
+ };
4489
+ }
4490
+ const message = `${agent}: ${text}`;
4491
+ switch (errorKind) {
4492
+ case "rate_limit":
4493
+ return { kind: "provider_rate_limit", message, retryable: true, backoffMs: 3e4 };
4494
+ case "overloaded":
4495
+ case "server_error":
4496
+ return { kind: "provider_5xx", message, retryable: true, backoffMs: 1e4 };
4497
+ case "authentication_failed":
4498
+ case "oauth_org_not_allowed":
4499
+ case "verification_required":
4500
+ case "cloud_credential_error":
4501
+ return { kind: "provider_auth", message, retryable: false };
4502
+ default:
4503
+ return rpc.code === -32e3 ? { kind: "provider_auth", message, retryable: false } : void 0;
4504
+ }
4505
+ }
4506
+
4266
4507
  // src/integration/acp-bench.ts
4267
4508
  import * as fsp3 from "node:fs/promises";
4268
4509
  import * as path4 from "node:path";
@@ -4498,6 +4739,7 @@ var AGENTS_CATALOG = [
4498
4739
  fs: true
4499
4740
  },
4500
4741
  integration: "adapter",
4742
+ contextWindow: 1e6,
4501
4743
  docs: "https://docs.anthropic.com/en/docs/claude-code"
4502
4744
  },
4503
4745
  // ── Google ───────────────────────────────────────────────────────────
@@ -4752,13 +4994,37 @@ async function makeACPSubagentRunner(options) {
4752
4994
  };
4753
4995
  return wrappedRunner;
4754
4996
  }
4997
+ var sessionCostSeen = /* @__PURE__ */ new WeakMap();
4998
+ function recordTurnUsage(ctx, session, result) {
4999
+ const cost = result.usage?.cost;
5000
+ const total = cost?.currency === "USD" && Number.isFinite(cost.amount) && cost.amount >= 0 ? cost.amount : void 0;
5001
+ const turnCost = total === void 0 ? 0 : Math.max(0, total - (sessionCostSeen.get(session) ?? 0));
5002
+ if (total !== void 0) sessionCostSeen.set(session, total);
5003
+ const t = result.tokens;
5004
+ if (!t && turnCost === 0) return void 0;
5005
+ try {
5006
+ ctx.budget.recordUsage(
5007
+ {
5008
+ input: t?.input ?? 0,
5009
+ output: t?.output ?? 0,
5010
+ cacheRead: t?.cacheRead ?? 0,
5011
+ cacheWrite: t?.cacheWrite ?? 0
5012
+ },
5013
+ turnCost
5014
+ );
5015
+ } catch {
5016
+ }
5017
+ return { type: "turn_usage", ...t ? { tokens: t } : {}, costUsd: turnCost };
5018
+ }
4755
5019
  async function makeACPSubagentRunnerWithStop(options) {
4756
5020
  const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
4757
5021
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
4758
5022
  const persistent = options.persistent === true;
4759
5023
  let shared = null;
4760
5024
  let turns = Promise.resolve();
4761
- const startSession = async () => {
5025
+ const startSession = async (setup) => {
5026
+ const mcpServers = setup?.mcpServers && setup.mcpServers.length > 0 ? [...options.mcpServers ?? [], ...setup.mcpServers] : options.mcpServers;
5027
+ const permissionPolicy = setup?.permissionPolicy ?? options.permissionPolicy;
4762
5028
  return ACPSession.start({
4763
5029
  command: options.command,
4764
5030
  ...options.args !== void 0 ? { args: options.args } : {},
@@ -4766,23 +5032,29 @@ async function makeACPSubagentRunnerWithStop(options) {
4766
5032
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
4767
5033
  projectRoot,
4768
5034
  timeoutMs,
5035
+ ...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
4769
5036
  role: options.role,
4770
- ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
4771
- ...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
5037
+ ...permissionPolicy !== void 0 ? { permissionPolicy } : {},
5038
+ ...mcpServers !== void 0 ? { mcpServers } : {},
5039
+ ...setup?.meta ? { sessionMeta: setup.meta } : {},
5040
+ ...options.initialModeId ? { initialModeId: options.initialModeId } : {},
5041
+ ...options.requireInitialMode ? { requireInitialMode: true } : {}
4772
5042
  });
4773
5043
  };
4774
5044
  const runner = async (task, ctx) => {
4775
5045
  let session;
5046
+ let setup;
4776
5047
  try {
5048
+ setup = options.sessionSetup?.(task, ctx);
4777
5049
  if (persistent) {
4778
- shared ??= startSession();
5050
+ shared ??= startSession(setup);
4779
5051
  const starting = shared;
4780
5052
  session = await starting.catch((err) => {
4781
5053
  if (shared === starting) shared = null;
4782
5054
  throw err;
4783
5055
  });
4784
5056
  } else {
4785
- session = await startSession();
5057
+ session = await startSession(setup);
4786
5058
  }
4787
5059
  } catch (err) {
4788
5060
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
@@ -4799,14 +5071,20 @@ async function makeACPSubagentRunnerWithStop(options) {
4799
5071
  }
4800
5072
  };
4801
5073
  let result;
5074
+ let endLive;
4802
5075
  try {
4803
- const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
5076
+ if (session.canSteer()) endLive = options.onTurnLive?.(ctx, task, (t) => session.steer(t));
5077
+ const prompt2 = setup?.promptPrefix ? `${setup.promptPrefix}
5078
+
5079
+ ${task.description}` : task.description;
5080
+ const turn = () => session.prompt([textContent(prompt2)], ctx.signal, onProgress);
4804
5081
  const run = persistent ? turns.then(turn, turn) : turn();
4805
5082
  if (persistent) turns = run;
4806
5083
  result = await run;
4807
5084
  } catch (err) {
4808
5085
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
4809
5086
  } finally {
5087
+ endLive?.();
4810
5088
  if (!persistent) {
4811
5089
  try {
4812
5090
  await session.close();
@@ -4814,6 +5092,8 @@ async function makeACPSubagentRunnerWithStop(options) {
4814
5092
  }
4815
5093
  }
4816
5094
  }
5095
+ const turnUsage = recordTurnUsage(ctx, session, result);
5096
+ if (turnUsage) onProgress(turnUsage);
4817
5097
  const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
4818
5098
  if (failure) throw failure;
4819
5099
  return {
@@ -4834,7 +5114,20 @@ async function makeACPSubagentRunnerWithStop(options) {
4834
5114
  };
4835
5115
  return { runner, stop };
4836
5116
  }
5117
+ function agentRpcError(err) {
5118
+ let cur = err;
5119
+ for (let depth = 0; depth < 4 && cur; depth++) {
5120
+ if (isJsonRpcError(cur)) return cur;
5121
+ cur = cur.cause;
5122
+ }
5123
+ return void 0;
5124
+ }
4837
5125
  function acpErrorToSubagentError(err, subagentId) {
5126
+ const rpc = agentRpcError(err);
5127
+ if (rpc) {
5128
+ const classified = classifyAgentFailure(rpc, subagentId);
5129
+ if (classified) return classified;
5130
+ }
4838
5131
  if (err instanceof ACPSessionError) {
4839
5132
  const kind = mapACPKind(err.kind);
4840
5133
  return {
@@ -4902,6 +5195,9 @@ function mapACPKind(acpKind) {
4902
5195
  return "bridge_failed";
4903
5196
  case "aborted":
4904
5197
  return "aborted_by_parent";
5198
+ // The agent went silent mid-turn and the turn was cancelled.
5199
+ case "stalled":
5200
+ return "budget_timeout";
4905
5201
  case "closed":
4906
5202
  case "unsupported_capability":
4907
5203
  return "unknown";
@@ -5448,7 +5744,8 @@ async function runOneAcpTask(opts) {
5448
5744
  role,
5449
5745
  timeoutMs,
5450
5746
  ...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
5451
- ...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
5747
+ ...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {},
5748
+ ...opts.sessionSetup !== void 0 ? { sessionSetup: opts.sessionSetup } : {}
5452
5749
  });
5453
5750
  try {
5454
5751
  const budget = new SubagentBudget2({
@@ -5592,6 +5889,7 @@ export {
5592
5889
  WrongStackACPServer,
5593
5890
  assertNeverSessionUpdate,
5594
5891
  audioContent,
5892
+ classifyAgentFailure,
5595
5893
  defaultEnsembleCmdResolver,
5596
5894
  defaultPermissionPolicy,
5597
5895
  fetchAcpRegistry,