@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/client.js CHANGED
@@ -11,6 +11,19 @@ import {
11
11
  var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
12
12
  var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
13
13
  var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
14
+ var STDERR_TAIL_CHARS = 4096;
15
+ function echoChildStderr() {
16
+ const v = process.env.WRONGSTACK_DEBUG;
17
+ return !!v && v !== "0" && v !== "false";
18
+ }
19
+ function lastLine(text) {
20
+ const lines = text.split(/\r?\n/);
21
+ for (let i = lines.length - 1; i >= 0; i--) {
22
+ const line = lines[i]?.trim();
23
+ if (line) return line.length > 300 ? `${line.slice(0, 300)}\u2026` : line;
24
+ }
25
+ return "";
26
+ }
14
27
  function takeLines(pending, chunk) {
15
28
  const lines = [];
16
29
  let start = 0;
@@ -27,6 +40,8 @@ function positiveLimit(value, fallback) {
27
40
  var ClientTransport = class {
28
41
  child = null;
29
42
  buffer = "";
43
+ stderrTail = "";
44
+ stopRequested = false;
30
45
  handlers = /* @__PURE__ */ new Set();
31
46
  claimHandlers = /* @__PURE__ */ new Set();
32
47
  closeHandlers = /* @__PURE__ */ new Set();
@@ -127,9 +142,11 @@ var ClientTransport = class {
127
142
  }
128
143
  const idx = this.buffer.indexOf("[wstack-acp]\n");
129
144
  if (idx !== -1) {
130
- this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
145
+ const rest = this.buffer.slice(idx + "[wstack-acp]\n".length);
146
+ this.buffer = "";
131
147
  child.stdout.removeListener("data", waitForMarker);
132
148
  onReady();
149
+ if (rest) this.onChildData(rest);
133
150
  }
134
151
  };
135
152
  child.stdout.on("data", waitForMarker);
@@ -192,6 +209,7 @@ var ClientTransport = class {
192
209
  }
193
210
  stop(reason = "transport stopped") {
194
211
  this.closed = true;
212
+ this.stopRequested = true;
195
213
  this.resolveRead?.(null);
196
214
  this.resolveRead = null;
197
215
  this.buffer = "";
@@ -229,7 +247,8 @@ var ClientTransport = class {
229
247
  }
230
248
  }
231
249
  onChildError(chunk) {
232
- writeErr(`[acp-child stderr] ${chunk}`);
250
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
251
+ if (echoChildStderr()) writeErr(`[acp-child stderr] ${chunk}`);
233
252
  }
234
253
  onChildClose(code) {
235
254
  this.closed = true;
@@ -241,11 +260,16 @@ var ClientTransport = class {
241
260
  this.queuedChars = 0;
242
261
  this.handlers.clear();
243
262
  this.claimHandlers.clear();
244
- if (code !== 0 && code !== null) {
263
+ const failed = code !== 0 && code !== null && !this.stopRequested;
264
+ if (failed) {
265
+ const tail = echoChildStderr() ? "" : this.stderrTail.trimEnd();
245
266
  writeErr(`[acp-child exited with code ${code}]
246
- `);
267
+ ${tail ? `${tail}
268
+ ` : ""}`);
247
269
  }
248
- this.notifyClosed(`agent process exited with code ${code}`);
270
+ const cause = failed ? lastLine(this.stderrTail) : "";
271
+ this.stderrTail = "";
272
+ this.notifyClosed(`agent process exited with code ${code}${cause ? `: ${cause}` : ""}`);
249
273
  }
250
274
  dispatch(msg, chars = JSON.stringify(msg).length) {
251
275
  if (this.resolveRead) {
@@ -388,6 +412,9 @@ function isAuthRequiredError(err) {
388
412
  }
389
413
 
390
414
  // src/client/acp-request-state.ts
415
+ function touchIdleRequests(pending) {
416
+ for (const p of pending.values()) p.rearm?.();
417
+ }
391
418
  function rejectPendingRequests(pending, reason) {
392
419
  for (const p of pending.values()) {
393
420
  clearTimeout(p.timeoutHandle);
@@ -584,11 +611,21 @@ async function executeMcpMessage(ctx, connectionId, message) {
584
611
  }
585
612
  async function executeCreateSession(ctx) {
586
613
  const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
587
- const id = ctx.allocId();
588
- const result = await ctx.sendRequest(id, "session/new", {
614
+ const params = {
589
615
  cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
590
- mcpServers: servers
591
- });
616
+ mcpServers: servers,
617
+ ...ctx.opts.sessionMeta ? { _meta: ctx.opts.sessionMeta } : {}
618
+ };
619
+ const resumeId = ctx.opts.resumeSessionId;
620
+ if (resumeId && ctx.agentCapabilities.sessionCapabilities?.resume) {
621
+ const resumed = await ctx.sendRequest(ctx.allocId(), "session/resume", { ...params, sessionId: resumeId }).catch((err) => ({ refused: err }));
622
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
623
+ if (!isJsonRpcError(resumed) && !resumed.refused) {
624
+ return adoptSessionMode(ctx, resumeId, resumed);
625
+ }
626
+ }
627
+ const id = ctx.allocId();
628
+ const result = await ctx.sendRequest(id, "session/new", params);
592
629
  if (isJsonRpcError(result)) {
593
630
  throw new ACPSessionError(
594
631
  "session_create_failed",
@@ -600,8 +637,53 @@ async function executeCreateSession(ctx) {
600
637
  if (typeof sessionId !== "string" || sessionId.length === 0) {
601
638
  throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
602
639
  }
640
+ return adoptSessionMode(ctx, sessionId, result);
641
+ }
642
+ async function executeOpenSession(ctx, create) {
643
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
644
+ if (ctx.sessionId) return { sessionId: ctx.sessionId, resumed: false };
645
+ const sessionId = await create();
646
+ ctx.setSessionId(sessionId);
647
+ return { sessionId, resumed: sessionId === ctx.opts.resumeSessionId };
648
+ }
649
+ async function adoptSessionMode(ctx, sessionId, result) {
650
+ const want = ctx.opts.initialModeId;
651
+ const modes = result.modes;
652
+ const offered = Array.isArray(modes?.availableModes) && modes.availableModes.some((m) => m?.id === want);
653
+ if (want && !offered && ctx.opts.requireInitialMode) {
654
+ throw new ACPSessionError(
655
+ "session_create_failed",
656
+ `the agent does not offer the "${want}" session mode this host requires`,
657
+ result
658
+ );
659
+ }
660
+ if (want && offered && modes?.currentModeId !== want) {
661
+ await executeSetMode(ctx, sessionId, want);
662
+ }
603
663
  return sessionId;
604
664
  }
665
+ async function executeSteer(ctx, supported, text) {
666
+ if (ctx.closed || !ctx.sessionId || !supported) return "unsupported";
667
+ const result = await ctx.sendRequest(
668
+ ctx.allocId(),
669
+ "_session/steering",
670
+ {
671
+ sessionId: ctx.sessionId,
672
+ prompt: [{ type: "text", text }],
673
+ _meta: { steering: { idleBehavior: "promptRequired" } }
674
+ },
675
+ 1e4
676
+ );
677
+ if (isJsonRpcError(result)) {
678
+ throw new ACPSessionError(
679
+ "prompt_failed",
680
+ `_session/steering failed: ${result.message}`,
681
+ result
682
+ );
683
+ }
684
+ const outcome = result?.outcome;
685
+ return outcome === "injected" || outcome === "promptRequired" || outcome === "startedNewTurn" ? outcome : "unsupported";
686
+ }
605
687
 
606
688
  // src/client/acp-session-auth.ts
607
689
  function ensureOpen(host) {
@@ -1208,6 +1290,7 @@ function emptyRunResult(stopReason) {
1208
1290
  function createSessionScratch() {
1209
1291
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1210
1292
  }
1293
+ var MESSAGE_SEPARATOR = "\n\n";
1211
1294
  function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1212
1295
  const update = msg.params?.update;
1213
1296
  if (typeof update !== "object" || update === null) return;
@@ -1217,6 +1300,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1217
1300
  case "agent_message_chunk": {
1218
1301
  const text = extractText(u.content);
1219
1302
  if (text) {
1303
+ if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
1304
+ scratch.text += MESSAGE_SEPARATOR;
1305
+ }
1306
+ scratch.messageBreak = false;
1220
1307
  scratch.text += text;
1221
1308
  emitProgress2({ type: "message", text });
1222
1309
  }
@@ -1241,7 +1328,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1241
1328
  emitProgress2({ type: "plan", entries: u.entries });
1242
1329
  }
1243
1330
  return;
1244
- case "usage_update":
1331
+ case "usage_update": {
1332
+ const rateLimit = u._meta?.["_claude/rateLimit"];
1333
+ if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
1245
1334
  if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
1246
1335
  const usage = {
1247
1336
  used: u.used,
@@ -1252,6 +1341,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1252
1341
  emitProgress2({ type: "usage", usage });
1253
1342
  }
1254
1343
  return;
1344
+ }
1255
1345
  case "available_commands_update":
1256
1346
  case "current_mode_update":
1257
1347
  case "config_option_update":
@@ -1264,6 +1354,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1264
1354
  return;
1265
1355
  }
1266
1356
  }
1357
+ function toolOutputText(u) {
1358
+ const joined = (blocks) => blocks.map(extractText).filter((text) => text.length > 0).join("\n");
1359
+ if (Array.isArray(u.content)) {
1360
+ const text = joined(
1361
+ u.content.map(
1362
+ (c) => c && typeof c === "object" && c.type === "content" ? c.content : void 0
1363
+ )
1364
+ );
1365
+ if (text) return text;
1366
+ }
1367
+ if (typeof u.rawOutput === "string") return u.rawOutput || void 0;
1368
+ if (Array.isArray(u.rawOutput)) return joined(u.rawOutput) || void 0;
1369
+ return void 0;
1370
+ }
1267
1371
  function captureToolCall(u, isNew, scratch, emitProgress2) {
1268
1372
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1269
1373
  if (!toolCallId) return;
@@ -1274,12 +1378,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
1274
1378
  kind: typeof u.kind === "string" ? u.kind : prev?.kind,
1275
1379
  status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
1276
1380
  rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
1277
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
1381
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
1382
+ output: toolOutputText(u) ?? prev?.output
1278
1383
  };
1279
1384
  scratch.toolCalls.set(toolCallId, record);
1385
+ if (scratch.text) scratch.messageBreak = true;
1280
1386
  if (Array.isArray(u.content)) {
1281
1387
  for (const c of u.content) {
1282
1388
  if (c && typeof c === "object" && c.type === "diff") {
1389
+ const key = `${toolCallId}\0${c.path}\0${c.oldText ?? ""}\0${c.newText}`;
1390
+ scratch.seenDiffs ??= /* @__PURE__ */ new Set();
1391
+ if (scratch.seenDiffs.has(key)) continue;
1392
+ scratch.seenDiffs.add(key);
1283
1393
  const diff = {
1284
1394
  path: c.path,
1285
1395
  oldText: c.oldText,
@@ -1290,10 +1400,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
1290
1400
  }
1291
1401
  }
1292
1402
  }
1293
- emitProgress2({
1294
- type: isNew ? "tool_call" : "tool_call_update",
1295
- toolCall: record
1296
- });
1403
+ scratch.announced ??= /* @__PURE__ */ new Set();
1404
+ if (!scratch.announced.has(toolCallId)) {
1405
+ const hasInput = record.rawInput !== void 0 && Object.keys(record.rawInput).length > 0;
1406
+ if (isNew && !hasInput) return;
1407
+ scratch.announced.add(toolCallId);
1408
+ emitProgress2({ type: "tool_call", toolCall: record });
1409
+ if (isNew || record.status !== "completed" && record.status !== "failed") return;
1410
+ }
1411
+ emitProgress2({ type: "tool_call_update", toolCall: record });
1297
1412
  }
1298
1413
 
1299
1414
  // src/client/acp-session-messages.ts
@@ -1325,6 +1440,7 @@ function handleMessage(msg) {
1325
1440
  }
1326
1441
  return;
1327
1442
  }
1443
+ touchIdleRequests(this.pending);
1328
1444
  if (msg.method === "session/update") {
1329
1445
  const updateSessionId = msg.params?.sessionId;
1330
1446
  if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
@@ -1381,6 +1497,7 @@ function handleMessage(msg) {
1381
1497
  );
1382
1498
  return;
1383
1499
  }
1500
+ if (msg.method.startsWith("_")) return;
1384
1501
  console.warn(
1385
1502
  JSON.stringify({
1386
1503
  level: "warn",
@@ -1490,7 +1607,9 @@ async function prompt(host, blocks, signal, onProgress) {
1490
1607
  sessionId: host.sessionId,
1491
1608
  prompt: blocks
1492
1609
  },
1493
- host.timeoutMs
1610
+ // Timed by silence, not length: a working agent keeps it alive.
1611
+ host.promptIdleTimeoutMs,
1612
+ true
1494
1613
  ),
1495
1614
  new Promise((_, reject) => {
1496
1615
  rejectTurn = reject;
@@ -1506,6 +1625,17 @@ async function prompt(host, blocks, signal, onProgress) {
1506
1625
  if (cancelled || abortedKind) {
1507
1626
  return emptyRunResult("cancelled");
1508
1627
  }
1628
+ if (err instanceof ACPSessionError && err.kind === "stalled") {
1629
+ if (host.sessionId) {
1630
+ host.transport.send({
1631
+ jsonrpc: "2.0",
1632
+ method: "session/cancel",
1633
+ params: { sessionId: host.sessionId }
1634
+ }).catch(() => {
1635
+ });
1636
+ }
1637
+ throw err;
1638
+ }
1509
1639
  const msg = err instanceof Error ? err.message : String(err);
1510
1640
  if (signal.aborted) {
1511
1641
  throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
@@ -1527,17 +1657,31 @@ async function prompt(host, blocks, signal, onProgress) {
1527
1657
  }
1528
1658
  const stopReason = response.stopReason ?? "end_turn";
1529
1659
  const finalText = host.scratch.text;
1660
+ const tokens = readTokenUsage(response.usage);
1530
1661
  return {
1531
1662
  text: finalText,
1532
1663
  stopReason,
1533
1664
  hasText: finalText.length > 0,
1534
1665
  usage: host.scratch.usage,
1666
+ ...tokens ? { tokens } : {},
1535
1667
  plan: host.scratch.plan,
1536
1668
  toolCalls: [...host.scratch.toolCalls.values()],
1537
1669
  diffs: host.scratch.diffs,
1538
1670
  thoughts: host.scratch.thoughts
1539
1671
  };
1540
1672
  }
1673
+ function readTokenUsage(raw) {
1674
+ if (typeof raw !== "object" || raw === null) return void 0;
1675
+ const r = raw;
1676
+ const n = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
1677
+ if (typeof r.inputTokens !== "number" && typeof r.outputTokens !== "number") return void 0;
1678
+ return {
1679
+ input: n(r.inputTokens),
1680
+ output: n(r.outputTokens),
1681
+ cacheRead: n(r.cachedReadTokens),
1682
+ cacheWrite: n(r.cachedWriteTokens)
1683
+ };
1684
+ }
1541
1685
 
1542
1686
  // src/client/permission.ts
1543
1687
  function pickAllow(options) {
@@ -2187,6 +2331,7 @@ var ACPSession = class _ACPSession {
2187
2331
  terminalServer;
2188
2332
  permissionPolicy;
2189
2333
  timeoutMs;
2334
+ promptIdleTimeoutMs;
2190
2335
  opts;
2191
2336
  transportOff = null;
2192
2337
  callbackAbort = new AbortController();
@@ -2202,12 +2347,15 @@ var ACPSession = class _ACPSession {
2202
2347
  agentCapabilities = {};
2203
2348
  agentInfo = null;
2204
2349
  authMethods = [];
2350
+ /** The agent accepts `_session/steering` (see executeSteer). */
2351
+ steeringSupported = false;
2205
2352
  /** Protocol version negotiated with the agent during initialize. */
2206
2353
  negotiatedVersion = ACP_PROTOCOL_VERSION;
2207
2354
  constructor(opts, transport) {
2208
2355
  this.opts = opts;
2209
2356
  this.transport = transport;
2210
2357
  this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
2358
+ this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
2211
2359
  const fsOpts = {
2212
2360
  projectRoot: opts.projectRoot
2213
2361
  };
@@ -2362,6 +2510,8 @@ var ACPSession = class _ACPSession {
2362
2510
  }
2363
2511
  this.negotiatedVersion = r.protocolVersion;
2364
2512
  this.agentCapabilities = r.agentCapabilities ?? {};
2513
+ const meta = result._meta;
2514
+ this.steeringSupported = meta?.steering?.supported === true;
2365
2515
  this.agentInfo = r.agentInfo ?? null;
2366
2516
  this.authMethods = r.authMethods ?? [];
2367
2517
  this.state = "ready";
@@ -2406,6 +2556,10 @@ var ACPSession = class _ACPSession {
2406
2556
  closeSession: () => this.closeSession()
2407
2557
  };
2408
2558
  }
2559
+ /** Start (or, with `resumeSessionId`, resume) the agent session now, not at the first prompt. */
2560
+ async openSession() {
2561
+ return executeOpenSession(this.opContext(), () => this.createSessionWithAuth());
2562
+ }
2409
2563
  async loadSession(sessionId, mcpServers, cwd) {
2410
2564
  return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
2411
2565
  }
@@ -2439,6 +2593,14 @@ var ACPSession = class _ACPSession {
2439
2593
  async disableProvider() {
2440
2594
  return executeDisableProvider(this.opContext());
2441
2595
  }
2596
+ /** Whether {@link steer} can reach the agent. */
2597
+ canSteer() {
2598
+ return this.steeringSupported;
2599
+ }
2600
+ /** Inject a message into the agent's running turn. */
2601
+ async steer(text) {
2602
+ return executeSteer(this.opContext(), this.steeringSupported, text);
2603
+ }
2442
2604
  // ──────────────────────────────────────────────────────────────────────
2443
2605
  // Prompt
2444
2606
  // ──────────────────────────────────────────────────────────────────────
@@ -2457,14 +2619,14 @@ var ACPSession = class _ACPSession {
2457
2619
  cancelLateSession(createPromise) {
2458
2620
  cancelLateAcpSession(this.transport, createPromise);
2459
2621
  }
2460
- async closeSession() {
2622
+ async closeSession(graceMs = 1e4) {
2461
2623
  if (!this.sessionId) return;
2462
2624
  const sid = this.sessionId;
2463
2625
  this.sessionId = null;
2464
2626
  if (this.agentCapabilities.sessionCapabilities?.close) {
2465
2627
  const id = this.allocId();
2466
2628
  try {
2467
- await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
2629
+ await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
2468
2630
  } catch {
2469
2631
  }
2470
2632
  }
@@ -2472,7 +2634,12 @@ var ACPSession = class _ACPSession {
2472
2634
  // ──────────────────────────────────────────────────────────────────────
2473
2635
  // Lifecycle — close
2474
2636
  // ──────────────────────────────────────────────────────────────────────
2475
- async close() {
2637
+ /**
2638
+ * Close the session and stop the agent. `graceMs` bounds how long the
2639
+ * agent may take to answer `session/close` before it is stopped anyway —
2640
+ * shutdown passes a short one so a slow answer cannot outlive the process.
2641
+ */
2642
+ async close(opts = {}) {
2476
2643
  if (this.closed) return;
2477
2644
  this.closed = true;
2478
2645
  this.state = "closed";
@@ -2482,7 +2649,7 @@ var ACPSession = class _ACPSession {
2482
2649
  this.terminalServer.dispose();
2483
2650
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
2484
2651
  try {
2485
- await this.closeSession();
2652
+ await this.closeSession(opts.graceMs);
2486
2653
  } catch {
2487
2654
  }
2488
2655
  }
@@ -2506,24 +2673,37 @@ var ACPSession = class _ACPSession {
2506
2673
  }
2507
2674
  throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
2508
2675
  }
2509
- async sendRequest(id, method, params, timeoutMs) {
2676
+ async sendRequest(id, method, params, timeoutMs, idle = false) {
2510
2677
  return new Promise((resolve3, reject) => {
2511
2678
  const effectiveTimeout = timeoutMs ?? this.timeoutMs;
2512
- const handle = setTimeout(() => {
2679
+ const expire = () => {
2513
2680
  this.pending.delete(id);
2514
2681
  reject(
2515
- new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
2682
+ idle ? new ACPSessionError(
2683
+ "stalled",
2684
+ `${method}: no activity from the agent for ${Math.round(effectiveTimeout / 1e3)}s`
2685
+ ) : new ACPSessionError(
2686
+ "protocol_error",
2687
+ `${method} timed out after ${effectiveTimeout}ms`
2688
+ )
2516
2689
  );
2517
- }, effectiveTimeout);
2518
- this.pending.set(id, {
2690
+ };
2691
+ const entry = {
2519
2692
  method,
2520
2693
  resolve: resolve3,
2521
2694
  reject,
2522
2695
  timeoutMs: effectiveTimeout,
2523
- timeoutHandle: handle
2524
- });
2696
+ timeoutHandle: setTimeout(expire, effectiveTimeout)
2697
+ };
2698
+ if (idle) {
2699
+ entry.rearm = () => {
2700
+ clearTimeout(entry.timeoutHandle);
2701
+ entry.timeoutHandle = setTimeout(expire, effectiveTimeout);
2702
+ };
2703
+ }
2704
+ this.pending.set(id, entry);
2525
2705
  this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
2526
- clearTimeout(handle);
2706
+ clearTimeout(entry.timeoutHandle);
2527
2707
  this.pending.delete(id);
2528
2708
  const msg = err instanceof Error ? err.message : String(err);
2529
2709
  reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
@@ -2627,13 +2807,74 @@ var ACPSession = class _ACPSession {
2627
2807
  progressHandler: this.progressHandler,
2628
2808
  allocId: this.allocId,
2629
2809
  sendRequest: this.sendRequest,
2630
- timeoutMs: this.timeoutMs,
2810
+ promptIdleTimeoutMs: this.promptIdleTimeoutMs,
2631
2811
  scratch: this.scratch
2632
2812
  };
2633
2813
  return this;
2634
2814
  }
2635
2815
  };
2636
2816
 
2817
+ // src/integration/acp-agent-failure.ts
2818
+ var USAGE_LIMIT_PREFIXES = [
2819
+ "You've hit your",
2820
+ "You've reached your",
2821
+ "You're out of usage credits",
2822
+ "Your org is out of usage",
2823
+ "Your seat type doesn't include usage",
2824
+ "Your seat type doesn't include extra usage",
2825
+ "Your usage allocation has been disabled",
2826
+ "Your group's usage limit is set to $0",
2827
+ "requires usage credits",
2828
+ "You're out of extra usage",
2829
+ "Claude AI usage limit reached"
2830
+ ];
2831
+ var DEFAULT_QUOTA_HOLD_MS = 15 * 6e4;
2832
+ function parseQuotaResetAt(text, now = Date.now()) {
2833
+ const epoch = /usage limit reached\|(\d{9,11})/i.exec(text);
2834
+ if (epoch?.[1]) return Number(epoch[1]) * 1e3;
2835
+ const m = /\bresets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i.exec(text);
2836
+ if (!m?.[1]) return void 0;
2837
+ let hour = Number(m[1]);
2838
+ const minute = m[2] ? Number(m[2]) : 0;
2839
+ const meridiem = m[3]?.toLowerCase();
2840
+ if (meridiem === "pm" && hour < 12) hour += 12;
2841
+ if (meridiem === "am" && hour === 12) hour = 0;
2842
+ if (hour > 23 || minute > 59 || !meridiem && !m[2]) return void 0;
2843
+ const at = new Date(now);
2844
+ at.setHours(hour, minute, 0, 0);
2845
+ if (at.getTime() <= now) at.setDate(at.getDate() + 1);
2846
+ return at.getTime();
2847
+ }
2848
+ function classifyAgentFailure(rpc, agent, now = Date.now()) {
2849
+ const text = rpc.message.replace(/^Internal error:\s*/i, "").trim();
2850
+ const errorKind = rpc.data?.errorKind;
2851
+ const isQuota = USAGE_LIMIT_PREFIXES.some((p) => text.startsWith(p) || text.includes(p)) || errorKind === "billing_error" || errorKind === "account_on_hold";
2852
+ if (isQuota) {
2853
+ const resetAt = parseQuotaResetAt(text, now);
2854
+ return {
2855
+ kind: "provider_quota",
2856
+ message: `${agent}: usage quota spent \u2014 ${text || "the account has no usage left"}`,
2857
+ retryable: false,
2858
+ backoffMs: resetAt !== void 0 ? Math.max(0, resetAt - now) : DEFAULT_QUOTA_HOLD_MS
2859
+ };
2860
+ }
2861
+ const message = `${agent}: ${text}`;
2862
+ switch (errorKind) {
2863
+ case "rate_limit":
2864
+ return { kind: "provider_rate_limit", message, retryable: true, backoffMs: 3e4 };
2865
+ case "overloaded":
2866
+ case "server_error":
2867
+ return { kind: "provider_5xx", message, retryable: true, backoffMs: 1e4 };
2868
+ case "authentication_failed":
2869
+ case "oauth_org_not_allowed":
2870
+ case "verification_required":
2871
+ case "cloud_credential_error":
2872
+ return { kind: "provider_auth", message, retryable: false };
2873
+ default:
2874
+ return rpc.code === -32e3 ? { kind: "provider_auth", message, retryable: false } : void 0;
2875
+ }
2876
+ }
2877
+
2637
2878
  // src/integration/acp-subagent-runner.ts
2638
2879
  async function makeACPSubagentRunner(options) {
2639
2880
  const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
@@ -2646,13 +2887,37 @@ async function makeACPSubagentRunner(options) {
2646
2887
  };
2647
2888
  return wrappedRunner;
2648
2889
  }
2890
+ var sessionCostSeen = /* @__PURE__ */ new WeakMap();
2891
+ function recordTurnUsage(ctx, session, result) {
2892
+ const cost = result.usage?.cost;
2893
+ const total = cost?.currency === "USD" && Number.isFinite(cost.amount) && cost.amount >= 0 ? cost.amount : void 0;
2894
+ const turnCost = total === void 0 ? 0 : Math.max(0, total - (sessionCostSeen.get(session) ?? 0));
2895
+ if (total !== void 0) sessionCostSeen.set(session, total);
2896
+ const t = result.tokens;
2897
+ if (!t && turnCost === 0) return void 0;
2898
+ try {
2899
+ ctx.budget.recordUsage(
2900
+ {
2901
+ input: t?.input ?? 0,
2902
+ output: t?.output ?? 0,
2903
+ cacheRead: t?.cacheRead ?? 0,
2904
+ cacheWrite: t?.cacheWrite ?? 0
2905
+ },
2906
+ turnCost
2907
+ );
2908
+ } catch {
2909
+ }
2910
+ return { type: "turn_usage", ...t ? { tokens: t } : {}, costUsd: turnCost };
2911
+ }
2649
2912
  async function makeACPSubagentRunnerWithStop(options) {
2650
2913
  const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
2651
2914
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
2652
2915
  const persistent = options.persistent === true;
2653
2916
  let shared = null;
2654
2917
  let turns = Promise.resolve();
2655
- const startSession = async () => {
2918
+ const startSession = async (setup) => {
2919
+ const mcpServers = setup?.mcpServers && setup.mcpServers.length > 0 ? [...options.mcpServers ?? [], ...setup.mcpServers] : options.mcpServers;
2920
+ const permissionPolicy = setup?.permissionPolicy ?? options.permissionPolicy;
2656
2921
  return ACPSession.start({
2657
2922
  command: options.command,
2658
2923
  ...options.args !== void 0 ? { args: options.args } : {},
@@ -2660,23 +2925,29 @@ async function makeACPSubagentRunnerWithStop(options) {
2660
2925
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2661
2926
  projectRoot,
2662
2927
  timeoutMs,
2928
+ ...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
2663
2929
  role: options.role,
2664
- ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
2665
- ...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
2930
+ ...permissionPolicy !== void 0 ? { permissionPolicy } : {},
2931
+ ...mcpServers !== void 0 ? { mcpServers } : {},
2932
+ ...setup?.meta ? { sessionMeta: setup.meta } : {},
2933
+ ...options.initialModeId ? { initialModeId: options.initialModeId } : {},
2934
+ ...options.requireInitialMode ? { requireInitialMode: true } : {}
2666
2935
  });
2667
2936
  };
2668
2937
  const runner = async (task, ctx) => {
2669
2938
  let session;
2939
+ let setup;
2670
2940
  try {
2941
+ setup = options.sessionSetup?.(task, ctx);
2671
2942
  if (persistent) {
2672
- shared ??= startSession();
2943
+ shared ??= startSession(setup);
2673
2944
  const starting = shared;
2674
2945
  session = await starting.catch((err) => {
2675
2946
  if (shared === starting) shared = null;
2676
2947
  throw err;
2677
2948
  });
2678
2949
  } else {
2679
- session = await startSession();
2950
+ session = await startSession(setup);
2680
2951
  }
2681
2952
  } catch (err) {
2682
2953
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
@@ -2693,14 +2964,20 @@ async function makeACPSubagentRunnerWithStop(options) {
2693
2964
  }
2694
2965
  };
2695
2966
  let result;
2967
+ let endLive;
2696
2968
  try {
2697
- const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
2969
+ if (session.canSteer()) endLive = options.onTurnLive?.(ctx, task, (t) => session.steer(t));
2970
+ const prompt2 = setup?.promptPrefix ? `${setup.promptPrefix}
2971
+
2972
+ ${task.description}` : task.description;
2973
+ const turn = () => session.prompt([textContent(prompt2)], ctx.signal, onProgress);
2698
2974
  const run = persistent ? turns.then(turn, turn) : turn();
2699
2975
  if (persistent) turns = run;
2700
2976
  result = await run;
2701
2977
  } catch (err) {
2702
2978
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
2703
2979
  } finally {
2980
+ endLive?.();
2704
2981
  if (!persistent) {
2705
2982
  try {
2706
2983
  await session.close();
@@ -2708,6 +2985,8 @@ async function makeACPSubagentRunnerWithStop(options) {
2708
2985
  }
2709
2986
  }
2710
2987
  }
2988
+ const turnUsage = recordTurnUsage(ctx, session, result);
2989
+ if (turnUsage) onProgress(turnUsage);
2711
2990
  const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
2712
2991
  if (failure) throw failure;
2713
2992
  return {
@@ -2728,7 +3007,20 @@ async function makeACPSubagentRunnerWithStop(options) {
2728
3007
  };
2729
3008
  return { runner, stop };
2730
3009
  }
3010
+ function agentRpcError(err) {
3011
+ let cur = err;
3012
+ for (let depth = 0; depth < 4 && cur; depth++) {
3013
+ if (isJsonRpcError(cur)) return cur;
3014
+ cur = cur.cause;
3015
+ }
3016
+ return void 0;
3017
+ }
2731
3018
  function acpErrorToSubagentError(err, subagentId) {
3019
+ const rpc = agentRpcError(err);
3020
+ if (rpc) {
3021
+ const classified = classifyAgentFailure(rpc, subagentId);
3022
+ if (classified) return classified;
3023
+ }
2732
3024
  if (err instanceof ACPSessionError) {
2733
3025
  const kind = mapACPKind(err.kind);
2734
3026
  return {
@@ -2796,6 +3088,9 @@ function mapACPKind(acpKind) {
2796
3088
  return "bridge_failed";
2797
3089
  case "aborted":
2798
3090
  return "aborted_by_parent";
3091
+ // The agent went silent mid-turn and the turn was cancelled.
3092
+ case "stalled":
3093
+ return "budget_timeout";
2799
3094
  case "closed":
2800
3095
  case "unsupported_capability":
2801
3096
  return "unknown";