@wrongstack/acp 1.1.1 → 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) {
@@ -2517,6 +2599,10 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2517
2599
  return;
2518
2600
  }
2519
2601
  if (msg.method === "fs/write_text_file") {
2602
+ if (typeof params.content !== "string") {
2603
+ await sender.sendErrorResponse(id, -32602, "content must be a string");
2604
+ return;
2605
+ }
2520
2606
  const authorization = await authorizeAcpCallback(
2521
2607
  permissionPolicy,
2522
2608
  {
@@ -2550,7 +2636,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2550
2636
  await fileServer.writeTextFile({
2551
2637
  sessionId: params.sessionId ?? "",
2552
2638
  path: params.path,
2553
- content: params.content ?? ""
2639
+ content: params.content
2554
2640
  });
2555
2641
  await sender.sendResult(id, {});
2556
2642
  }
@@ -2567,17 +2653,22 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2567
2653
  try {
2568
2654
  switch (msg.method) {
2569
2655
  case "terminal/create": {
2656
+ const env = Array.isArray(params.env) ? params.env.filter(
2657
+ (entry) => !!entry && typeof entry.name === "string" && typeof entry.value === "string"
2658
+ ) : [];
2659
+ const envSuffix = env.length > 0 ? ` (env: ${env.map((e) => `${e.name}=${e.value}`).join(" ")})` : "";
2570
2660
  const authorization = await authorizeAcpCallback(
2571
2661
  permissionPolicy,
2572
2662
  {
2573
2663
  toolCallId: `acp-terminal-create-${id}`,
2574
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2664
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim() + envSuffix,
2575
2665
  kind: "execute",
2576
2666
  rawInput: {
2577
2667
  command: params.command,
2578
2668
  args: params.args,
2579
2669
  cwd: params.cwd,
2580
- sessionId: params.sessionId
2670
+ sessionId: params.sessionId,
2671
+ ...env.length > 0 ? { env } : {}
2581
2672
  }
2582
2673
  },
2583
2674
  callbackOptions2
@@ -2596,9 +2687,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2596
2687
  command: String(params.command ?? ""),
2597
2688
  args: Array.isArray(params.args) ? params.args : []
2598
2689
  };
2599
- if (Array.isArray(params.env)) {
2600
- createOpts.env = params.env;
2601
- }
2690
+ if (env.length > 0) createOpts.env = env;
2602
2691
  if (typeof params.cwd === "string") {
2603
2692
  createOpts.cwd = params.cwd;
2604
2693
  }
@@ -2752,6 +2841,7 @@ function emptyRunResult(stopReason) {
2752
2841
  function createSessionScratch() {
2753
2842
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2754
2843
  }
2844
+ var MESSAGE_SEPARATOR = "\n\n";
2755
2845
  function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2756
2846
  const update = msg.params?.update;
2757
2847
  if (typeof update !== "object" || update === null) return;
@@ -2761,6 +2851,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2761
2851
  case "agent_message_chunk": {
2762
2852
  const text = extractText(u.content);
2763
2853
  if (text) {
2854
+ if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
2855
+ scratch.text += MESSAGE_SEPARATOR;
2856
+ }
2857
+ scratch.messageBreak = false;
2764
2858
  scratch.text += text;
2765
2859
  emitProgress2({ type: "message", text });
2766
2860
  }
@@ -2785,7 +2879,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2785
2879
  emitProgress2({ type: "plan", entries: u.entries });
2786
2880
  }
2787
2881
  return;
2788
- case "usage_update":
2882
+ case "usage_update": {
2883
+ const rateLimit = u._meta?.["_claude/rateLimit"];
2884
+ if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
2789
2885
  if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
2790
2886
  const usage = {
2791
2887
  used: u.used,
@@ -2796,6 +2892,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2796
2892
  emitProgress2({ type: "usage", usage });
2797
2893
  }
2798
2894
  return;
2895
+ }
2799
2896
  case "available_commands_update":
2800
2897
  case "current_mode_update":
2801
2898
  case "config_option_update":
@@ -2808,6 +2905,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2808
2905
  return;
2809
2906
  }
2810
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
+ }
2811
2922
  function captureToolCall(u, isNew, scratch, emitProgress2) {
2812
2923
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2813
2924
  if (!toolCallId) return;
@@ -2818,12 +2929,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
2818
2929
  kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2819
2930
  status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2820
2931
  rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2821
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2932
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
2933
+ output: toolOutputText(u) ?? prev?.output
2822
2934
  };
2823
2935
  scratch.toolCalls.set(toolCallId, record);
2936
+ if (scratch.text) scratch.messageBreak = true;
2824
2937
  if (Array.isArray(u.content)) {
2825
2938
  for (const c of u.content) {
2826
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);
2827
2944
  const diff = {
2828
2945
  path: c.path,
2829
2946
  oldText: c.oldText,
@@ -2834,10 +2951,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
2834
2951
  }
2835
2952
  }
2836
2953
  }
2837
- emitProgress2({
2838
- type: isNew ? "tool_call" : "tool_call_update",
2839
- toolCall: record
2840
- });
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 });
2841
2963
  }
2842
2964
 
2843
2965
  // src/client/acp-session-messages.ts
@@ -2869,6 +2991,7 @@ function handleMessage(msg) {
2869
2991
  }
2870
2992
  return;
2871
2993
  }
2994
+ touchIdleRequests(this.pending);
2872
2995
  if (msg.method === "session/update") {
2873
2996
  const updateSessionId = msg.params?.sessionId;
2874
2997
  if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
@@ -2925,6 +3048,7 @@ function handleMessage(msg) {
2925
3048
  );
2926
3049
  return;
2927
3050
  }
3051
+ if (msg.method.startsWith("_")) return;
2928
3052
  console.warn(
2929
3053
  JSON.stringify({
2930
3054
  level: "warn",
@@ -3034,7 +3158,9 @@ async function prompt(host, blocks, signal, onProgress) {
3034
3158
  sessionId: host.sessionId,
3035
3159
  prompt: blocks
3036
3160
  },
3037
- host.timeoutMs
3161
+ // Timed by silence, not length: a working agent keeps it alive.
3162
+ host.promptIdleTimeoutMs,
3163
+ true
3038
3164
  ),
3039
3165
  new Promise((_, reject) => {
3040
3166
  rejectTurn = reject;
@@ -3050,6 +3176,17 @@ async function prompt(host, blocks, signal, onProgress) {
3050
3176
  if (cancelled || abortedKind) {
3051
3177
  return emptyRunResult("cancelled");
3052
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
+ }
3053
3190
  const msg = err instanceof Error ? err.message : String(err);
3054
3191
  if (signal.aborted) {
3055
3192
  throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
@@ -3071,17 +3208,31 @@ async function prompt(host, blocks, signal, onProgress) {
3071
3208
  }
3072
3209
  const stopReason = response.stopReason ?? "end_turn";
3073
3210
  const finalText = host.scratch.text;
3211
+ const tokens = readTokenUsage(response.usage);
3074
3212
  return {
3075
3213
  text: finalText,
3076
3214
  stopReason,
3077
3215
  hasText: finalText.length > 0,
3078
3216
  usage: host.scratch.usage,
3217
+ ...tokens ? { tokens } : {},
3079
3218
  plan: host.scratch.plan,
3080
3219
  toolCalls: [...host.scratch.toolCalls.values()],
3081
3220
  diffs: host.scratch.diffs,
3082
3221
  thoughts: host.scratch.thoughts
3083
3222
  };
3084
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
+ }
3085
3236
 
3086
3237
  // src/client/permission.ts
3087
3238
  function pickAllow(options) {
@@ -3731,6 +3882,7 @@ var ACPSession = class _ACPSession {
3731
3882
  terminalServer;
3732
3883
  permissionPolicy;
3733
3884
  timeoutMs;
3885
+ promptIdleTimeoutMs;
3734
3886
  opts;
3735
3887
  transportOff = null;
3736
3888
  callbackAbort = new AbortController();
@@ -3746,12 +3898,15 @@ var ACPSession = class _ACPSession {
3746
3898
  agentCapabilities = {};
3747
3899
  agentInfo = null;
3748
3900
  authMethods = [];
3901
+ /** The agent accepts `_session/steering` (see executeSteer). */
3902
+ steeringSupported = false;
3749
3903
  /** Protocol version negotiated with the agent during initialize. */
3750
3904
  negotiatedVersion = ACP_PROTOCOL_VERSION;
3751
3905
  constructor(opts, transport) {
3752
3906
  this.opts = opts;
3753
3907
  this.transport = transport;
3754
3908
  this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
3909
+ this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
3755
3910
  const fsOpts = {
3756
3911
  projectRoot: opts.projectRoot
3757
3912
  };
@@ -3906,6 +4061,8 @@ var ACPSession = class _ACPSession {
3906
4061
  }
3907
4062
  this.negotiatedVersion = r.protocolVersion;
3908
4063
  this.agentCapabilities = r.agentCapabilities ?? {};
4064
+ const meta = result._meta;
4065
+ this.steeringSupported = meta?.steering?.supported === true;
3909
4066
  this.agentInfo = r.agentInfo ?? null;
3910
4067
  this.authMethods = r.authMethods ?? [];
3911
4068
  this.state = "ready";
@@ -3950,6 +4107,10 @@ var ACPSession = class _ACPSession {
3950
4107
  closeSession: () => this.closeSession()
3951
4108
  };
3952
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
+ }
3953
4114
  async loadSession(sessionId, mcpServers, cwd) {
3954
4115
  return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
3955
4116
  }
@@ -3983,6 +4144,14 @@ var ACPSession = class _ACPSession {
3983
4144
  async disableProvider() {
3984
4145
  return executeDisableProvider(this.opContext());
3985
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
+ }
3986
4155
  // ──────────────────────────────────────────────────────────────────────
3987
4156
  // Prompt
3988
4157
  // ──────────────────────────────────────────────────────────────────────
@@ -4001,14 +4170,14 @@ var ACPSession = class _ACPSession {
4001
4170
  cancelLateSession(createPromise) {
4002
4171
  cancelLateAcpSession(this.transport, createPromise);
4003
4172
  }
4004
- async closeSession() {
4173
+ async closeSession(graceMs = 1e4) {
4005
4174
  if (!this.sessionId) return;
4006
4175
  const sid = this.sessionId;
4007
4176
  this.sessionId = null;
4008
4177
  if (this.agentCapabilities.sessionCapabilities?.close) {
4009
4178
  const id = this.allocId();
4010
4179
  try {
4011
- await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
4180
+ await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
4012
4181
  } catch {
4013
4182
  }
4014
4183
  }
@@ -4016,7 +4185,12 @@ var ACPSession = class _ACPSession {
4016
4185
  // ──────────────────────────────────────────────────────────────────────
4017
4186
  // Lifecycle — close
4018
4187
  // ──────────────────────────────────────────────────────────────────────
4019
- 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 = {}) {
4020
4194
  if (this.closed) return;
4021
4195
  this.closed = true;
4022
4196
  this.state = "closed";
@@ -4026,7 +4200,7 @@ var ACPSession = class _ACPSession {
4026
4200
  this.terminalServer.dispose();
4027
4201
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
4028
4202
  try {
4029
- await this.closeSession();
4203
+ await this.closeSession(opts.graceMs);
4030
4204
  } catch {
4031
4205
  }
4032
4206
  }
@@ -4050,24 +4224,37 @@ var ACPSession = class _ACPSession {
4050
4224
  }
4051
4225
  throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
4052
4226
  }
4053
- async sendRequest(id, method, params, timeoutMs) {
4227
+ async sendRequest(id, method, params, timeoutMs, idle = false) {
4054
4228
  return new Promise((resolve5, reject) => {
4055
4229
  const effectiveTimeout = timeoutMs ?? this.timeoutMs;
4056
- const handle = setTimeout(() => {
4230
+ const expire = () => {
4057
4231
  this.pending.delete(id);
4058
4232
  reject(
4059
- 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
+ )
4060
4240
  );
4061
- }, effectiveTimeout);
4062
- this.pending.set(id, {
4241
+ };
4242
+ const entry = {
4063
4243
  method,
4064
4244
  resolve: resolve5,
4065
4245
  reject,
4066
4246
  timeoutMs: effectiveTimeout,
4067
- timeoutHandle: handle
4068
- });
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);
4069
4256
  this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
4070
- clearTimeout(handle);
4257
+ clearTimeout(entry.timeoutHandle);
4071
4258
  this.pending.delete(id);
4072
4259
  const msg = err instanceof Error ? err.message : String(err);
4073
4260
  reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
@@ -4171,7 +4358,7 @@ var ACPSession = class _ACPSession {
4171
4358
  progressHandler: this.progressHandler,
4172
4359
  allocId: this.allocId,
4173
4360
  sendRequest: this.sendRequest,
4174
- timeoutMs: this.timeoutMs,
4361
+ promptIdleTimeoutMs: this.promptIdleTimeoutMs,
4175
4362
  scratch: this.scratch
4176
4363
  };
4177
4364
  return this;
@@ -4256,6 +4443,67 @@ var ToolTranslator = class {
4256
4443
  }
4257
4444
  };
4258
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
+
4259
4507
  // src/integration/acp-bench.ts
4260
4508
  import * as fsp3 from "node:fs/promises";
4261
4509
  import * as path4 from "node:path";
@@ -4491,6 +4739,7 @@ var AGENTS_CATALOG = [
4491
4739
  fs: true
4492
4740
  },
4493
4741
  integration: "adapter",
4742
+ contextWindow: 1e6,
4494
4743
  docs: "https://docs.anthropic.com/en/docs/claude-code"
4495
4744
  },
4496
4745
  // ── Google ───────────────────────────────────────────────────────────
@@ -4745,13 +4994,37 @@ async function makeACPSubagentRunner(options) {
4745
4994
  };
4746
4995
  return wrappedRunner;
4747
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
+ }
4748
5019
  async function makeACPSubagentRunnerWithStop(options) {
4749
5020
  const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
4750
5021
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
4751
5022
  const persistent = options.persistent === true;
4752
5023
  let shared = null;
4753
5024
  let turns = Promise.resolve();
4754
- 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;
4755
5028
  return ACPSession.start({
4756
5029
  command: options.command,
4757
5030
  ...options.args !== void 0 ? { args: options.args } : {},
@@ -4759,23 +5032,29 @@ async function makeACPSubagentRunnerWithStop(options) {
4759
5032
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
4760
5033
  projectRoot,
4761
5034
  timeoutMs,
5035
+ ...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
4762
5036
  role: options.role,
4763
- ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
4764
- ...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 } : {}
4765
5042
  });
4766
5043
  };
4767
5044
  const runner = async (task, ctx) => {
4768
5045
  let session;
5046
+ let setup;
4769
5047
  try {
5048
+ setup = options.sessionSetup?.(task, ctx);
4770
5049
  if (persistent) {
4771
- shared ??= startSession();
5050
+ shared ??= startSession(setup);
4772
5051
  const starting = shared;
4773
5052
  session = await starting.catch((err) => {
4774
5053
  if (shared === starting) shared = null;
4775
5054
  throw err;
4776
5055
  });
4777
5056
  } else {
4778
- session = await startSession();
5057
+ session = await startSession(setup);
4779
5058
  }
4780
5059
  } catch (err) {
4781
5060
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
@@ -4792,14 +5071,20 @@ async function makeACPSubagentRunnerWithStop(options) {
4792
5071
  }
4793
5072
  };
4794
5073
  let result;
5074
+ let endLive;
4795
5075
  try {
4796
- 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);
4797
5081
  const run = persistent ? turns.then(turn, turn) : turn();
4798
5082
  if (persistent) turns = run;
4799
5083
  result = await run;
4800
5084
  } catch (err) {
4801
5085
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
4802
5086
  } finally {
5087
+ endLive?.();
4803
5088
  if (!persistent) {
4804
5089
  try {
4805
5090
  await session.close();
@@ -4807,6 +5092,8 @@ async function makeACPSubagentRunnerWithStop(options) {
4807
5092
  }
4808
5093
  }
4809
5094
  }
5095
+ const turnUsage = recordTurnUsage(ctx, session, result);
5096
+ if (turnUsage) onProgress(turnUsage);
4810
5097
  const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
4811
5098
  if (failure) throw failure;
4812
5099
  return {
@@ -4827,7 +5114,20 @@ async function makeACPSubagentRunnerWithStop(options) {
4827
5114
  };
4828
5115
  return { runner, stop };
4829
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
+ }
4830
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
+ }
4831
5131
  if (err instanceof ACPSessionError) {
4832
5132
  const kind = mapACPKind(err.kind);
4833
5133
  return {
@@ -4895,6 +5195,9 @@ function mapACPKind(acpKind) {
4895
5195
  return "bridge_failed";
4896
5196
  case "aborted":
4897
5197
  return "aborted_by_parent";
5198
+ // The agent went silent mid-turn and the turn was cancelled.
5199
+ case "stalled":
5200
+ return "budget_timeout";
4898
5201
  case "closed":
4899
5202
  case "unsupported_capability":
4900
5203
  return "unknown";
@@ -5441,7 +5744,8 @@ async function runOneAcpTask(opts) {
5441
5744
  role,
5442
5745
  timeoutMs,
5443
5746
  ...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
5444
- ...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
5747
+ ...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {},
5748
+ ...opts.sessionSetup !== void 0 ? { sessionSetup: opts.sessionSetup } : {}
5445
5749
  });
5446
5750
  try {
5447
5751
  const budget = new SubagentBudget2({
@@ -5585,6 +5889,7 @@ export {
5585
5889
  WrongStackACPServer,
5586
5890
  assertNeverSessionUpdate,
5587
5891
  audioContent,
5892
+ classifyAgentFailure,
5588
5893
  defaultEnsembleCmdResolver,
5589
5894
  defaultPermissionPolicy,
5590
5895
  fetchAcpRegistry,