@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/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) {
@@ -966,6 +1048,10 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
966
1048
  return;
967
1049
  }
968
1050
  if (msg.method === "fs/write_text_file") {
1051
+ if (typeof params.content !== "string") {
1052
+ await sender.sendErrorResponse(id, -32602, "content must be a string");
1053
+ return;
1054
+ }
969
1055
  const authorization = await authorizeAcpCallback(
970
1056
  permissionPolicy,
971
1057
  {
@@ -999,7 +1085,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
999
1085
  await fileServer.writeTextFile({
1000
1086
  sessionId: params.sessionId ?? "",
1001
1087
  path: params.path,
1002
- content: params.content ?? ""
1088
+ content: params.content
1003
1089
  });
1004
1090
  await sender.sendResult(id, {});
1005
1091
  }
@@ -1016,17 +1102,22 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
1016
1102
  try {
1017
1103
  switch (msg.method) {
1018
1104
  case "terminal/create": {
1105
+ const env = Array.isArray(params.env) ? params.env.filter(
1106
+ (entry) => !!entry && typeof entry.name === "string" && typeof entry.value === "string"
1107
+ ) : [];
1108
+ const envSuffix = env.length > 0 ? ` (env: ${env.map((e) => `${e.name}=${e.value}`).join(" ")})` : "";
1019
1109
  const authorization = await authorizeAcpCallback(
1020
1110
  permissionPolicy,
1021
1111
  {
1022
1112
  toolCallId: `acp-terminal-create-${id}`,
1023
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
1113
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim() + envSuffix,
1024
1114
  kind: "execute",
1025
1115
  rawInput: {
1026
1116
  command: params.command,
1027
1117
  args: params.args,
1028
1118
  cwd: params.cwd,
1029
- sessionId: params.sessionId
1119
+ sessionId: params.sessionId,
1120
+ ...env.length > 0 ? { env } : {}
1030
1121
  }
1031
1122
  },
1032
1123
  callbackOptions2
@@ -1045,9 +1136,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
1045
1136
  command: String(params.command ?? ""),
1046
1137
  args: Array.isArray(params.args) ? params.args : []
1047
1138
  };
1048
- if (Array.isArray(params.env)) {
1049
- createOpts.env = params.env;
1050
- }
1139
+ if (env.length > 0) createOpts.env = env;
1051
1140
  if (typeof params.cwd === "string") {
1052
1141
  createOpts.cwd = params.cwd;
1053
1142
  }
@@ -1201,6 +1290,7 @@ function emptyRunResult(stopReason) {
1201
1290
  function createSessionScratch() {
1202
1291
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1203
1292
  }
1293
+ var MESSAGE_SEPARATOR = "\n\n";
1204
1294
  function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1205
1295
  const update = msg.params?.update;
1206
1296
  if (typeof update !== "object" || update === null) return;
@@ -1210,6 +1300,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1210
1300
  case "agent_message_chunk": {
1211
1301
  const text = extractText(u.content);
1212
1302
  if (text) {
1303
+ if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
1304
+ scratch.text += MESSAGE_SEPARATOR;
1305
+ }
1306
+ scratch.messageBreak = false;
1213
1307
  scratch.text += text;
1214
1308
  emitProgress2({ type: "message", text });
1215
1309
  }
@@ -1234,7 +1328,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1234
1328
  emitProgress2({ type: "plan", entries: u.entries });
1235
1329
  }
1236
1330
  return;
1237
- case "usage_update":
1331
+ case "usage_update": {
1332
+ const rateLimit = u._meta?.["_claude/rateLimit"];
1333
+ if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
1238
1334
  if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
1239
1335
  const usage = {
1240
1336
  used: u.used,
@@ -1245,6 +1341,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1245
1341
  emitProgress2({ type: "usage", usage });
1246
1342
  }
1247
1343
  return;
1344
+ }
1248
1345
  case "available_commands_update":
1249
1346
  case "current_mode_update":
1250
1347
  case "config_option_update":
@@ -1257,6 +1354,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1257
1354
  return;
1258
1355
  }
1259
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
+ }
1260
1371
  function captureToolCall(u, isNew, scratch, emitProgress2) {
1261
1372
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1262
1373
  if (!toolCallId) return;
@@ -1267,12 +1378,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
1267
1378
  kind: typeof u.kind === "string" ? u.kind : prev?.kind,
1268
1379
  status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
1269
1380
  rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
1270
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
1381
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
1382
+ output: toolOutputText(u) ?? prev?.output
1271
1383
  };
1272
1384
  scratch.toolCalls.set(toolCallId, record);
1385
+ if (scratch.text) scratch.messageBreak = true;
1273
1386
  if (Array.isArray(u.content)) {
1274
1387
  for (const c of u.content) {
1275
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);
1276
1393
  const diff = {
1277
1394
  path: c.path,
1278
1395
  oldText: c.oldText,
@@ -1283,10 +1400,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
1283
1400
  }
1284
1401
  }
1285
1402
  }
1286
- emitProgress2({
1287
- type: isNew ? "tool_call" : "tool_call_update",
1288
- toolCall: record
1289
- });
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 });
1290
1412
  }
1291
1413
 
1292
1414
  // src/client/acp-session-messages.ts
@@ -1318,6 +1440,7 @@ function handleMessage(msg) {
1318
1440
  }
1319
1441
  return;
1320
1442
  }
1443
+ touchIdleRequests(this.pending);
1321
1444
  if (msg.method === "session/update") {
1322
1445
  const updateSessionId = msg.params?.sessionId;
1323
1446
  if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
@@ -1374,6 +1497,7 @@ function handleMessage(msg) {
1374
1497
  );
1375
1498
  return;
1376
1499
  }
1500
+ if (msg.method.startsWith("_")) return;
1377
1501
  console.warn(
1378
1502
  JSON.stringify({
1379
1503
  level: "warn",
@@ -1483,7 +1607,9 @@ async function prompt(host, blocks, signal, onProgress) {
1483
1607
  sessionId: host.sessionId,
1484
1608
  prompt: blocks
1485
1609
  },
1486
- host.timeoutMs
1610
+ // Timed by silence, not length: a working agent keeps it alive.
1611
+ host.promptIdleTimeoutMs,
1612
+ true
1487
1613
  ),
1488
1614
  new Promise((_, reject) => {
1489
1615
  rejectTurn = reject;
@@ -1499,6 +1625,17 @@ async function prompt(host, blocks, signal, onProgress) {
1499
1625
  if (cancelled || abortedKind) {
1500
1626
  return emptyRunResult("cancelled");
1501
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
+ }
1502
1639
  const msg = err instanceof Error ? err.message : String(err);
1503
1640
  if (signal.aborted) {
1504
1641
  throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
@@ -1520,17 +1657,31 @@ async function prompt(host, blocks, signal, onProgress) {
1520
1657
  }
1521
1658
  const stopReason = response.stopReason ?? "end_turn";
1522
1659
  const finalText = host.scratch.text;
1660
+ const tokens = readTokenUsage(response.usage);
1523
1661
  return {
1524
1662
  text: finalText,
1525
1663
  stopReason,
1526
1664
  hasText: finalText.length > 0,
1527
1665
  usage: host.scratch.usage,
1666
+ ...tokens ? { tokens } : {},
1528
1667
  plan: host.scratch.plan,
1529
1668
  toolCalls: [...host.scratch.toolCalls.values()],
1530
1669
  diffs: host.scratch.diffs,
1531
1670
  thoughts: host.scratch.thoughts
1532
1671
  };
1533
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
+ }
1534
1685
 
1535
1686
  // src/client/permission.ts
1536
1687
  function pickAllow(options) {
@@ -2180,6 +2331,7 @@ var ACPSession = class _ACPSession {
2180
2331
  terminalServer;
2181
2332
  permissionPolicy;
2182
2333
  timeoutMs;
2334
+ promptIdleTimeoutMs;
2183
2335
  opts;
2184
2336
  transportOff = null;
2185
2337
  callbackAbort = new AbortController();
@@ -2195,12 +2347,15 @@ var ACPSession = class _ACPSession {
2195
2347
  agentCapabilities = {};
2196
2348
  agentInfo = null;
2197
2349
  authMethods = [];
2350
+ /** The agent accepts `_session/steering` (see executeSteer). */
2351
+ steeringSupported = false;
2198
2352
  /** Protocol version negotiated with the agent during initialize. */
2199
2353
  negotiatedVersion = ACP_PROTOCOL_VERSION;
2200
2354
  constructor(opts, transport) {
2201
2355
  this.opts = opts;
2202
2356
  this.transport = transport;
2203
2357
  this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
2358
+ this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
2204
2359
  const fsOpts = {
2205
2360
  projectRoot: opts.projectRoot
2206
2361
  };
@@ -2355,6 +2510,8 @@ var ACPSession = class _ACPSession {
2355
2510
  }
2356
2511
  this.negotiatedVersion = r.protocolVersion;
2357
2512
  this.agentCapabilities = r.agentCapabilities ?? {};
2513
+ const meta = result._meta;
2514
+ this.steeringSupported = meta?.steering?.supported === true;
2358
2515
  this.agentInfo = r.agentInfo ?? null;
2359
2516
  this.authMethods = r.authMethods ?? [];
2360
2517
  this.state = "ready";
@@ -2399,6 +2556,10 @@ var ACPSession = class _ACPSession {
2399
2556
  closeSession: () => this.closeSession()
2400
2557
  };
2401
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
+ }
2402
2563
  async loadSession(sessionId, mcpServers, cwd) {
2403
2564
  return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
2404
2565
  }
@@ -2432,6 +2593,14 @@ var ACPSession = class _ACPSession {
2432
2593
  async disableProvider() {
2433
2594
  return executeDisableProvider(this.opContext());
2434
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
+ }
2435
2604
  // ──────────────────────────────────────────────────────────────────────
2436
2605
  // Prompt
2437
2606
  // ──────────────────────────────────────────────────────────────────────
@@ -2450,14 +2619,14 @@ var ACPSession = class _ACPSession {
2450
2619
  cancelLateSession(createPromise) {
2451
2620
  cancelLateAcpSession(this.transport, createPromise);
2452
2621
  }
2453
- async closeSession() {
2622
+ async closeSession(graceMs = 1e4) {
2454
2623
  if (!this.sessionId) return;
2455
2624
  const sid = this.sessionId;
2456
2625
  this.sessionId = null;
2457
2626
  if (this.agentCapabilities.sessionCapabilities?.close) {
2458
2627
  const id = this.allocId();
2459
2628
  try {
2460
- await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
2629
+ await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
2461
2630
  } catch {
2462
2631
  }
2463
2632
  }
@@ -2465,7 +2634,12 @@ var ACPSession = class _ACPSession {
2465
2634
  // ──────────────────────────────────────────────────────────────────────
2466
2635
  // Lifecycle — close
2467
2636
  // ──────────────────────────────────────────────────────────────────────
2468
- 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 = {}) {
2469
2643
  if (this.closed) return;
2470
2644
  this.closed = true;
2471
2645
  this.state = "closed";
@@ -2475,7 +2649,7 @@ var ACPSession = class _ACPSession {
2475
2649
  this.terminalServer.dispose();
2476
2650
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
2477
2651
  try {
2478
- await this.closeSession();
2652
+ await this.closeSession(opts.graceMs);
2479
2653
  } catch {
2480
2654
  }
2481
2655
  }
@@ -2499,24 +2673,37 @@ var ACPSession = class _ACPSession {
2499
2673
  }
2500
2674
  throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
2501
2675
  }
2502
- async sendRequest(id, method, params, timeoutMs) {
2676
+ async sendRequest(id, method, params, timeoutMs, idle = false) {
2503
2677
  return new Promise((resolve3, reject) => {
2504
2678
  const effectiveTimeout = timeoutMs ?? this.timeoutMs;
2505
- const handle = setTimeout(() => {
2679
+ const expire = () => {
2506
2680
  this.pending.delete(id);
2507
2681
  reject(
2508
- 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
+ )
2509
2689
  );
2510
- }, effectiveTimeout);
2511
- this.pending.set(id, {
2690
+ };
2691
+ const entry = {
2512
2692
  method,
2513
2693
  resolve: resolve3,
2514
2694
  reject,
2515
2695
  timeoutMs: effectiveTimeout,
2516
- timeoutHandle: handle
2517
- });
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);
2518
2705
  this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
2519
- clearTimeout(handle);
2706
+ clearTimeout(entry.timeoutHandle);
2520
2707
  this.pending.delete(id);
2521
2708
  const msg = err instanceof Error ? err.message : String(err);
2522
2709
  reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
@@ -2620,13 +2807,74 @@ var ACPSession = class _ACPSession {
2620
2807
  progressHandler: this.progressHandler,
2621
2808
  allocId: this.allocId,
2622
2809
  sendRequest: this.sendRequest,
2623
- timeoutMs: this.timeoutMs,
2810
+ promptIdleTimeoutMs: this.promptIdleTimeoutMs,
2624
2811
  scratch: this.scratch
2625
2812
  };
2626
2813
  return this;
2627
2814
  }
2628
2815
  };
2629
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
+
2630
2878
  // src/integration/acp-subagent-runner.ts
2631
2879
  async function makeACPSubagentRunner(options) {
2632
2880
  const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
@@ -2639,13 +2887,37 @@ async function makeACPSubagentRunner(options) {
2639
2887
  };
2640
2888
  return wrappedRunner;
2641
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
+ }
2642
2912
  async function makeACPSubagentRunnerWithStop(options) {
2643
2913
  const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
2644
2914
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
2645
2915
  const persistent = options.persistent === true;
2646
2916
  let shared = null;
2647
2917
  let turns = Promise.resolve();
2648
- 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;
2649
2921
  return ACPSession.start({
2650
2922
  command: options.command,
2651
2923
  ...options.args !== void 0 ? { args: options.args } : {},
@@ -2653,23 +2925,29 @@ async function makeACPSubagentRunnerWithStop(options) {
2653
2925
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2654
2926
  projectRoot,
2655
2927
  timeoutMs,
2928
+ ...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
2656
2929
  role: options.role,
2657
- ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
2658
- ...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 } : {}
2659
2935
  });
2660
2936
  };
2661
2937
  const runner = async (task, ctx) => {
2662
2938
  let session;
2939
+ let setup;
2663
2940
  try {
2941
+ setup = options.sessionSetup?.(task, ctx);
2664
2942
  if (persistent) {
2665
- shared ??= startSession();
2943
+ shared ??= startSession(setup);
2666
2944
  const starting = shared;
2667
2945
  session = await starting.catch((err) => {
2668
2946
  if (shared === starting) shared = null;
2669
2947
  throw err;
2670
2948
  });
2671
2949
  } else {
2672
- session = await startSession();
2950
+ session = await startSession(setup);
2673
2951
  }
2674
2952
  } catch (err) {
2675
2953
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
@@ -2686,14 +2964,20 @@ async function makeACPSubagentRunnerWithStop(options) {
2686
2964
  }
2687
2965
  };
2688
2966
  let result;
2967
+ let endLive;
2689
2968
  try {
2690
- 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);
2691
2974
  const run = persistent ? turns.then(turn, turn) : turn();
2692
2975
  if (persistent) turns = run;
2693
2976
  result = await run;
2694
2977
  } catch (err) {
2695
2978
  throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
2696
2979
  } finally {
2980
+ endLive?.();
2697
2981
  if (!persistent) {
2698
2982
  try {
2699
2983
  await session.close();
@@ -2701,6 +2985,8 @@ async function makeACPSubagentRunnerWithStop(options) {
2701
2985
  }
2702
2986
  }
2703
2987
  }
2988
+ const turnUsage = recordTurnUsage(ctx, session, result);
2989
+ if (turnUsage) onProgress(turnUsage);
2704
2990
  const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
2705
2991
  if (failure) throw failure;
2706
2992
  return {
@@ -2721,7 +3007,20 @@ async function makeACPSubagentRunnerWithStop(options) {
2721
3007
  };
2722
3008
  return { runner, stop };
2723
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
+ }
2724
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
+ }
2725
3024
  if (err instanceof ACPSessionError) {
2726
3025
  const kind = mapACPKind(err.kind);
2727
3026
  return {
@@ -2789,6 +3088,9 @@ function mapACPKind(acpKind) {
2789
3088
  return "bridge_failed";
2790
3089
  case "aborted":
2791
3090
  return "aborted_by_parent";
3091
+ // The agent went silent mid-turn and the turn was cancelled.
3092
+ case "stalled":
3093
+ return "budget_timeout";
2792
3094
  case "closed":
2793
3095
  case "unsupported_capability":
2794
3096
  return "unknown";