@agentclientprotocol/claude-agent-acp 0.85.2-preview.19 → 0.85.2-preview.3

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.
Files changed (62) hide show
  1. package/README.md +0 -1
  2. package/dist/acp-agent.d.ts +18 -81
  3. package/dist/acp-agent.d.ts.map +1 -1
  4. package/dist/acp-agent.js +170 -435
  5. package/dist/air-extension.d.ts +0 -2
  6. package/dist/air-extension.d.ts.map +1 -1
  7. package/dist/air-extension.js +0 -2
  8. package/dist/diff.d.ts +8 -45
  9. package/dist/diff.d.ts.map +1 -1
  10. package/dist/diff.js +30 -111
  11. package/dist/permissions/presentation.d.ts +2 -17
  12. package/dist/permissions/presentation.d.ts.map +1 -1
  13. package/dist/permissions/presentation.js +0 -4
  14. package/dist/resumed-session.d.ts +6 -16
  15. package/dist/resumed-session.d.ts.map +1 -1
  16. package/dist/resumed-session.js +25 -91
  17. package/dist/session-mode.d.ts +0 -14
  18. package/dist/session-mode.d.ts.map +1 -1
  19. package/dist/session-mode.js +0 -22
  20. package/dist/tool-calls/client-capabilities.d.ts +2 -20
  21. package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
  22. package/dist/tool-calls/client-capabilities.js +4 -14
  23. package/dist/tool-calls/renderer.d.ts.map +1 -1
  24. package/dist/tool-calls/renderer.js +2 -3
  25. package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
  26. package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
  27. package/dist/tool-calls/reporters/file-edit.js +14 -20
  28. package/dist/turn-events.d.ts +5 -11
  29. package/dist/turn-events.d.ts.map +1 -1
  30. package/dist/usage-markdown.d.ts +0 -2
  31. package/dist/usage-markdown.d.ts.map +1 -1
  32. package/dist/usage-markdown.js +1 -2
  33. package/dist/utils.d.ts +0 -12
  34. package/dist/utils.d.ts.map +1 -1
  35. package/dist/utils.js +0 -28
  36. package/dist/v2/agent.d.ts.map +1 -1
  37. package/dist/v2/agent.js +17 -121
  38. package/dist/v2/initialize.d.ts +5 -2
  39. package/dist/v2/initialize.d.ts.map +1 -1
  40. package/dist/v2/initialize.js +6 -23
  41. package/dist/v2/session-update.d.ts +0 -6
  42. package/dist/v2/session-update.d.ts.map +1 -1
  43. package/dist/v2/session-update.js +0 -27
  44. package/dist/v2/session.d.ts +4 -15
  45. package/dist/v2/session.d.ts.map +1 -1
  46. package/dist/v2/session.js +6 -20
  47. package/package.json +2 -2
  48. package/dist/mcp-command.d.ts +0 -51
  49. package/dist/mcp-command.d.ts.map +0 -1
  50. package/dist/mcp-command.js +0 -321
  51. package/dist/v2/permission.d.ts +0 -25
  52. package/dist/v2/permission.d.ts.map +0 -1
  53. package/dist/v2/permission.js +0 -38
  54. package/dist/v2/prompt.d.ts +0 -47
  55. package/dist/v2/prompt.d.ts.map +0 -1
  56. package/dist/v2/prompt.js +0 -153
  57. package/dist/v2/terminal.d.ts +0 -42
  58. package/dist/v2/terminal.d.ts.map +0 -1
  59. package/dist/v2/terminal.js +0 -95
  60. package/dist/v2/tool-call.d.ts +0 -38
  61. package/dist/v2/tool-call.d.ts.map +0 -1
  62. package/dist/v2/tool-call.js +0 -49
package/dist/acp-agent.js CHANGED
@@ -4,7 +4,7 @@ import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFr
4
4
  import { sanitizeTitle, SessionTitles } from "./session-titles.js";
5
5
  import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
6
6
  import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, nativeSubagentState, resumedNativeSubagentId, sendMessageResumePrompt, } from "./native-subagents.js";
7
- import { AIR_ASYNC_TASKS_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_KIND_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, AIR_SKILL_PATH_KEY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
7
+ import { AIR_ASYNC_TASKS_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
8
8
  import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, clientSupportsAsyncTasks, } from "./async-tasks.js";
9
9
  import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
10
10
  import { execFile } from "node:child_process";
@@ -16,12 +16,12 @@ import packageJson from "../package.json" with { type: "json" };
16
16
  import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
17
17
  import { forkSession } from "./fork-session.js";
18
18
  import { subagentHistory } from "./subagent-history.js";
19
- import { readResumedSession, readResumedTail, } from "./resumed-session.js";
19
+ import { readResumedModel, readResumedSession, } from "./resumed-session.js";
20
20
  import { SessionTiming } from "./session-timing.js";
21
- import { ALLOW_BYPASS } from "./permissions/modes.js";
21
+ import { ALLOW_BYPASS, resolvePermissionMode } from "./permissions/modes.js";
22
22
  import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
23
23
  import { buildClaudePermissionOptions } from "./permissions/options.js";
24
- import { buildClaudePermissionPresentation, } from "./permissions/presentation.js";
24
+ import { buildClaudePermissionPresentation } from "./permissions/presentation.js";
25
25
  import { decodeClaudePermissionResponse } from "./permissions/response.js";
26
26
  import { SettingsManager } from "./settings.js";
27
27
  import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
@@ -36,13 +36,11 @@ import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
36
36
  import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
37
37
  import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
38
38
  import { AcpToolCallRenderer } from "./tool-calls/renderer.js";
39
- import { resolveSkillPath } from "./tool-calls/reporters/interaction.js";
40
- import { nodeToWebReadable, nodeToWebWritable, Pushable, raceTimeoutAndAbort, unreachable, } from "./utils.js";
39
+ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
41
40
  import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
42
41
  import { parseToolResultMeta } from "./tool-result-meta.js";
43
42
  import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
44
- import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
45
- import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
43
+ import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
46
44
  import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
47
45
  import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
48
46
  export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
@@ -79,10 +77,6 @@ const DEFAULT_CONTEXT_WINDOW = 200000;
79
77
  * pre-empt a slow-but-healthy interrupt. */
80
78
  const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
81
79
  const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
82
- /** How long a session teardown waits for the turns that it cancels to end.
83
- * The teardown wakes the consumer itself, so they end within a few ticks
84
- * unless the consumer is wedged. */
85
- const TEARDOWN_TURN_END_TIMEOUT_MS = 5_000;
86
80
  /** The number of settled subagents whose parent tool call a session keeps for
87
81
  * a later resume (see `resumableSubagents`). */
88
82
  const MAX_RESUMABLE_SUBAGENTS = 256;
@@ -111,18 +105,32 @@ function settleLiveBackgroundTask(session, taskId) {
111
105
  async function structuredUsageMarkdown(query, signal, logger) {
112
106
  if (signal.aborted)
113
107
  return null;
108
+ let timeout;
109
+ let onAbort;
114
110
  try {
115
- const outcome = await raceTimeoutAndAbort(
116
- // Keeping the deliberately unstable method name visible makes an SDK
117
- // upgrade fail at compile time if Anthropic removes or renames it.
118
- query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(), STRUCTURED_USAGE_TIMEOUT_MS, signal);
119
- if (outcome.type !== "done") {
120
- if (outcome.type === "timeout") {
111
+ const response = await Promise.race([
112
+ // Keeping the deliberately unstable method name visible makes an SDK
113
+ // upgrade fail at compile time if Anthropic removes or renames it.
114
+ query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(),
115
+ new Promise((resolve) => {
116
+ timeout = setTimeout(() => resolve(null), STRUCTURED_USAGE_TIMEOUT_MS);
117
+ timeout.unref?.();
118
+ }),
119
+ new Promise((resolve) => {
120
+ onAbort = () => resolve(null);
121
+ if (signal.aborted)
122
+ onAbort();
123
+ else
124
+ signal.addEventListener("abort", onAbort, { once: true });
125
+ }),
126
+ ]);
127
+ if (response === null) {
128
+ if (!signal.aborted) {
121
129
  logger.error("Structured /usage timed out; preserving Claude Code output");
122
130
  }
123
131
  return null;
124
132
  }
125
- const usage = parseUsageResponse(outcome.value);
133
+ const usage = parseUsageResponse(response);
126
134
  if (!usage) {
127
135
  logger.error("Structured /usage returned an incompatible response; preserving Claude Code output");
128
136
  return null;
@@ -133,6 +141,12 @@ async function structuredUsageMarkdown(query, signal, logger) {
133
141
  logger.error(`Structured /usage failed; preserving Claude Code output: ${error}`);
134
142
  return null;
135
143
  }
144
+ finally {
145
+ if (timeout)
146
+ clearTimeout(timeout);
147
+ if (onAbort)
148
+ signal.removeEventListener("abort", onAbort);
149
+ }
136
150
  }
137
151
  /** Claude Code keeps the OAuth callback listener open in the background after
138
152
  * `mcpAuthenticate` returns the authorization URL. The SDK does not expose
@@ -666,7 +680,6 @@ const REPLAY_HIDDEN_COMMANDS = new Set([
666
680
  ...LOCAL_ONLY_COMMANDS,
667
681
  "/compact",
668
682
  "/model",
669
- "/mcp",
670
683
  "/status",
671
684
  "/usage",
672
685
  ]);
@@ -1004,19 +1017,12 @@ const mcpAuthentications = new WeakMap();
1004
1017
  * authentication. This runs after session creation has returned so an
1005
1018
  * interactive browser flow never delays `session/new`. */
1006
1019
  function startMcpAuthentication(host, sessionId, mcpServers) {
1007
- if (mcpServers.length === 0)
1008
- return;
1009
- startMcpServerAuthentication(host, sessionId, new Set(mcpServers.map((server) => server.name)));
1010
- }
1011
- /** Start OAuth in the background for the servers in `requestedServers` that
1012
- * Claude reports as needing authentication. Startup and `/mcp` use it. It
1013
- * does nothing while another OAuth run of the session is in progress. */
1014
- function startMcpServerAuthentication(host, sessionId, requestedServers) {
1015
- if (!host.clientCapabilities?.elicitation?.url)
1020
+ if (!host.clientCapabilities?.elicitation?.url || mcpServers.length === 0)
1016
1021
  return;
1017
1022
  const session = host.sessions[sessionId];
1018
1023
  if (!session || mcpAuthentications.has(session))
1019
1024
  return;
1025
+ const requestedServers = new Set(mcpServers.map((server) => server.name));
1020
1026
  const authentication = authenticateMcpServers(host, sessionId, session.query, requestedServers)
1021
1027
  .catch((error) => {
1022
1028
  if (!session.abortController.signal.aborted) {
@@ -1155,14 +1161,7 @@ export class ClaudeAcpAgent {
1155
1161
  * return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
1156
1162
  * tests can shrink it. */
1157
1163
  forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
1158
- /**
1159
- * The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
1160
- * types, except for the tool call content that v2 needs and v1 cannot
1161
- * express: see {@link ToolCallClientCapabilities.v2}.
1162
- */
1163
- v2;
1164
- constructor(client, logger, options = {}) {
1165
- this.v2 = options.v2 ?? false;
1164
+ constructor(client, logger) {
1166
1165
  this.sessions = {};
1167
1166
  this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
1168
1167
  this.logger = logger ?? console;
@@ -1219,9 +1218,7 @@ export class ClaudeAcpAgent {
1219
1218
  }
1220
1219
  async initialize(request) {
1221
1220
  this.clientCapabilities = request.clientCapabilities;
1222
- this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities, {
1223
- v2: this.v2,
1224
- });
1221
+ this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities);
1225
1222
  // Learn the auth identity in the background: `initialize` never waits on
1226
1223
  // the CLI probe, and no snapshot rides in its response. When the probe
1227
1224
  // lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
@@ -1405,56 +1402,6 @@ export class ClaudeAcpAgent {
1405
1402
  }, 0);
1406
1403
  return response;
1407
1404
  }
1408
- /**
1409
- * Start the CLI of a loaded session, and replay the history while the CLI
1410
- * starts. The CLI start takes most of the load time, and the replay needs
1411
- * only the transcript and the {@link ReplayState}. An invalid `cwd` fails
1412
- * before the first update. A failed CLI start stops the replay.
1413
- */
1414
- async createSessionWhileReplaying(params, timing) {
1415
- await this.validateCwd(params.cwd);
1416
- const pending = {
1417
- state: {
1418
- cwd: params.cwd,
1419
- taskState: new Map(),
1420
- forwardSubagentText: this.forwardsSubagentText(params._meta),
1421
- messageIdToUuid: new Map(),
1422
- sessionFailureState: createSessionFailureState(),
1423
- },
1424
- stopped: false,
1425
- };
1426
- const transcript = readResumedSession(params.sessionId, this.logger);
1427
- const replay = transcript
1428
- .then(({ messages }) => this.replaySessionHistory(params.sessionId, messages, pending))
1429
- .then(() => ({ ok: true }), (error) => ({ ok: false, error }));
1430
- let response;
1431
- try {
1432
- response = await this.createSession({
1433
- cwd: params.cwd,
1434
- mcpServers: params.mcpServers ?? [],
1435
- additionalDirectories: params.additionalDirectories,
1436
- _meta: params._meta,
1437
- }, {
1438
- resume: params.sessionId,
1439
- resumedModelHint: transcript.then(({ model }) => model),
1440
- replayState: pending.state,
1441
- });
1442
- }
1443
- catch (error) {
1444
- pending.stopped = true;
1445
- await replay;
1446
- throw error;
1447
- }
1448
- timing.phase("session-ready");
1449
- const replayed = await replay;
1450
- if (!replayed.ok)
1451
- throw replayed.error;
1452
- return {
1453
- sessionId: response.sessionId,
1454
- modes: response.modes,
1455
- configOptions: response.configOptions,
1456
- };
1457
- }
1458
1405
  async unstable_forkSession(params) {
1459
1406
  if (this.providerUpdate)
1460
1407
  await this.providerUpdate;
@@ -1479,16 +1426,10 @@ export class ClaudeAcpAgent {
1479
1426
  const timing = new SessionTiming(this.logger, "load", params.sessionId);
1480
1427
  if (this.providerUpdate)
1481
1428
  await this.providerUpdate;
1482
- let result;
1483
- if (this.sessions[params.sessionId]) {
1484
- const resumedSession = await readResumedSession(params.sessionId, this.logger);
1485
- result = await this.getOrCreateSession(params, resumedSession);
1486
- timing.phase("session-ready");
1487
- await this.replaySessionHistory(params.sessionId, resumedSession.messages);
1488
- }
1489
- else {
1490
- result = await this.createSessionWhileReplaying(params, timing);
1491
- }
1429
+ const resumedSession = await readResumedSession(params.sessionId, this.logger);
1430
+ const result = await this.getOrCreateSession(params, resumedSession);
1431
+ timing.phase("session-ready");
1432
+ await this.replaySessionHistory(params.sessionId, resumedSession.messages);
1492
1433
  timing.phase("replay");
1493
1434
  // Send available commands after replay so it doesn't interleave with history
1494
1435
  setTimeout(() => {
@@ -1871,47 +1812,6 @@ export class ClaudeAcpAgent {
1871
1812
  this.setAuthStatus(notLoggedInAuthStatus());
1872
1813
  }
1873
1814
  }
1874
- /** The structured replacement for a prompt that is exactly `/usage` or a
1875
- * `/mcp` command. Claude Code runs the command, and the replacement takes
1876
- * the place of its text. Claude Code refuses a `/mcp` reconnect, enable,
1877
- * or disable in SDK mode. The `/mcp` producer runs them through the SDK
1878
- * control API when the text arrives, so the action stays in the turn. */
1879
- localCommandMarkdown(params) {
1880
- const text = params.prompt.length === 1 && params.prompt[0]?.type === "text"
1881
- ? params.prompt[0].text
1882
- : undefined;
1883
- if (text === undefined)
1884
- return undefined;
1885
- if (isUsageCommandText(text)) {
1886
- return {
1887
- startsAtActivation: true,
1888
- produce: (query, signal) => structuredUsageMarkdown(query, signal, this.logger),
1889
- };
1890
- }
1891
- const command = parseMcpCommand(text);
1892
- if (!command)
1893
- return undefined;
1894
- let reconnected = [];
1895
- return {
1896
- startsAtActivation: false,
1897
- produce: async (query, signal, originalOutput) => {
1898
- const outcome = await runMcpCommand(query, command, originalOutput, signal, (message) => this.logger.error(message));
1899
- reconnected = outcome.reconnected;
1900
- return outcome.markdown;
1901
- },
1902
- // A reconnect starts OAuth for the ACP servers that it tried, as the
1903
- // session start does. A plain `/mcp` and a cancelled turn start none.
1904
- afterDelivery: () => {
1905
- const session = this.sessions[params.sessionId];
1906
- if (!session || session.cancelled)
1907
- return;
1908
- const acpServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
1909
- const servers = new Set(reconnected.filter((server) => acpServers.has(server)));
1910
- if (servers.size > 0)
1911
- startMcpServerAuthentication(this, params.sessionId, servers);
1912
- },
1913
- };
1914
- }
1915
1815
  /**
1916
1816
  * Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
1917
1817
  * outcome of a turn is field for field a v1 prompt response.
@@ -1978,8 +1878,10 @@ export class ClaudeAcpAgent {
1978
1878
  const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
1979
1879
  const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
1980
1880
  const fileChangeReport = session.fileChangeReporter?.request(params._meta);
1981
- const localCommand = this.localCommandMarkdown(params);
1982
- const localCommandAbort = localCommand ? new AbortController() : undefined;
1881
+ const isUsageCommand = params.prompt.length === 1 &&
1882
+ params.prompt[0]?.type === "text" &&
1883
+ isUsageCommandText(params.prompt[0].text);
1884
+ const usageMarkdownAbort = isUsageCommand ? new AbortController() : undefined;
1983
1885
  session.titles.onPrompt(params.prompt);
1984
1886
  // Each prompt is a Turn that the persistent consumer settles once the
1985
1887
  // turn's outcome is known. `startTurn()` owns no loop: it enqueues the
@@ -1990,8 +1892,8 @@ export class ClaudeAcpAgent {
1990
1892
  promptUuid,
1991
1893
  events,
1992
1894
  isLocalOnlyCommand,
1993
- ...(localCommand ? { localCommand } : {}),
1994
- ...(localCommandAbort ? { localCommandAbort } : {}),
1895
+ ...(isUsageCommand ? { isUsageCommand: true } : {}),
1896
+ ...(usageMarkdownAbort ? { usageMarkdownAbort } : {}),
1995
1897
  ...(fileChangeReport ? { fileChangeReport } : {}),
1996
1898
  settled: false,
1997
1899
  completion: new Promise((resolve) => {
@@ -2001,8 +1903,8 @@ export class ClaudeAcpAgent {
2001
1903
  events.ended(outcome);
2002
1904
  completeTurn();
2003
1905
  },
2004
- reject: (error, title) => {
2005
- events.failed(error, title);
1906
+ reject: (error) => {
1907
+ events.failed(error);
2006
1908
  completeTurn();
2007
1909
  },
2008
1910
  };
@@ -2113,10 +2015,7 @@ export class ClaudeAcpAgent {
2113
2015
  try {
2114
2016
  // `resume` names the Claude session, which shares the ACP session id, so
2115
2017
  // the new query continues the same conversation under the same id.
2116
- await this.createSession(creationParams, {
2117
- resume: sessionId,
2118
- permissionMode: session.modes.currentModeId,
2119
- });
2018
+ await this.createSession(creationParams, { resume: sessionId });
2120
2019
  }
2121
2020
  catch (error) {
2122
2021
  if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
@@ -2126,10 +2025,7 @@ export class ClaudeAcpAgent {
2126
2025
  // better answer than a session the client can never use again.
2127
2026
  this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
2128
2027
  try {
2129
- await this.createSession(creationParams, {
2130
- reuseSessionId: sessionId,
2131
- permissionMode: session.modes.currentModeId,
2132
- });
2028
+ await this.createSession(creationParams, { reuseSessionId: sessionId });
2133
2029
  return;
2134
2030
  }
2135
2031
  catch (fallbackError) {
@@ -2591,11 +2487,11 @@ export class ClaudeAcpAgent {
2591
2487
  };
2592
2488
  };
2593
2489
  const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
2594
- const ensureLocalCommandMarkdown = (turn, originalOutput = "") => {
2595
- if (!turn.localCommand || !turn.localCommandAbort)
2490
+ const ensureUsageMarkdown = (turn) => {
2491
+ if (!turn.isUsageCommand || !turn.usageMarkdownAbort)
2596
2492
  return undefined;
2597
- turn.localCommandMarkdown ??= turn.localCommand.produce(session.query, turn.localCommandAbort.signal, originalOutput);
2598
- return turn.localCommandMarkdown;
2493
+ turn.usageMarkdown ??= structuredUsageMarkdown(session.query, turn.usageMarkdownAbort.signal, this.logger);
2494
+ return turn.usageMarkdown;
2599
2495
  };
2600
2496
  const resetTurnScratch = () => {
2601
2497
  lastAssistantTotalUsage = null;
@@ -2638,8 +2534,7 @@ export class ClaudeAcpAgent {
2638
2534
  session.activeTurn = turn;
2639
2535
  session.cancelled = false;
2640
2536
  compaction.resume();
2641
- if (turn.localCommand?.startsAtActivation)
2642
- ensureLocalCommandMarkdown(turn);
2537
+ ensureUsageMarkdown(turn);
2643
2538
  session.pendingOrphanResults = 0;
2644
2539
  session.orphanCommands?.clear();
2645
2540
  // Two-phase sweep of registry entries the level signal ended (see
@@ -2667,23 +2562,18 @@ export class ClaudeAcpAgent {
2667
2562
  }
2668
2563
  }
2669
2564
  resetTurnScratch();
2670
- // Without a command_lifecycle "started" frame before it (see that
2671
- // handler), activation is the first sign that Claude Code took the
2672
- // prompt in: its echo, or for an echo-less command, its result.
2673
- reportInserted(turn);
2565
+ // Activation is when Claude Code takes the prompt in: its echo, or for an
2566
+ // echo-less command, its result. That inserts it into the conversation.
2567
+ // A turn can be settled by now: an echo hand-off awaits settling the
2568
+ // previous turn, and a cancel in that window settles the queued one.
2569
+ if (!turn.insertedReported && !turn.settled) {
2570
+ turn.insertedReported = true;
2571
+ turn.events.inserted(turn.promptUuid);
2572
+ }
2674
2573
  // A request can already be open: Claude Code asks for a queued prompt
2675
2574
  // before the consumer reaches its echo.
2676
2575
  this.syncAwaitingUser(session);
2677
2576
  };
2678
- /** Report that Claude Code took the turn's prompt in, once. A turn can be
2679
- * settled by now: an echo hand-off awaits settling the previous turn, and
2680
- * a cancel in that window settles the queued one. */
2681
- const reportInserted = (turn) => {
2682
- if (turn.insertedReported || turn.settled)
2683
- return;
2684
- turn.insertedReported = true;
2685
- turn.events.inserted(turn.promptUuid);
2686
- };
2687
2577
  /** Ensure there is an active turn before a user-turn result that carries no
2688
2578
  * echo to activate it, by promoting the queue head. Most turns are
2689
2579
  * activated by their replayed user message before their result, but some
@@ -2842,28 +2732,27 @@ export class ClaudeAcpAgent {
2842
2732
  * the autonomous stretch-close guard. */
2843
2733
  const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
2844
2734
  /** Claim the structured replacement for the turn currently producing a
2845
- * local-command output. Undefined means this is not a structured
2846
- * local-command turn (or its request failed), null means another SDK
2847
- * message shape already delivered it, and string is the one replacement
2848
- * to publish. */
2849
- const takeLocalCommandMarkdown = async (originalOutput) => {
2735
+ * local-command output. Undefined means this is not a structured usage
2736
+ * turn (or its request failed), null means another SDK message shape
2737
+ * already delivered it, and string is the one replacement to publish. */
2738
+ const takeUsageMarkdown = async (originalOutput) => {
2850
2739
  const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
2851
2740
  if (!turn)
2852
2741
  return undefined;
2853
- const pending = ensureLocalCommandMarkdown(turn, originalOutput);
2854
- if (!pending)
2742
+ const usageMarkdown = ensureUsageMarkdown(turn);
2743
+ if (!usageMarkdown)
2855
2744
  return undefined;
2856
- const markdown = await pending;
2745
+ const markdown = await usageMarkdown;
2857
2746
  if (markdown === null)
2858
2747
  return undefined;
2859
- if (turn.localCommandDelivered) {
2748
+ if (turn.usageMarkdownDelivered) {
2860
2749
  // Different SDK message shapes can mirror the same local-command
2861
2750
  // output. Suppress an exact mirror, but let a later, distinct frame
2862
2751
  // (for example an interruption diagnostic) follow the normal path.
2863
- return turn.localCommandOriginalOutput === originalOutput ? null : undefined;
2752
+ return turn.usageOriginalOutput === originalOutput ? null : undefined;
2864
2753
  }
2865
- turn.localCommandDelivered = true;
2866
- turn.localCommandOriginalOutput = originalOutput;
2754
+ turn.usageMarkdownDelivered = true;
2755
+ turn.usageOriginalOutput = originalOutput;
2867
2756
  return markdown;
2868
2757
  };
2869
2758
  /** Registers a live background task (see `liveBackgroundTasks`). A
@@ -3016,9 +2905,7 @@ export class ClaudeAcpAgent {
3016
2905
  // Captured before the settled flip below (isHeldOpen tests !settled).
3017
2906
  const wasHeld = isHeldOpen(turn);
3018
2907
  turn.settled = true;
3019
- turn.localCommandAbort?.abort();
3020
- if (turn.localCommandDelivered)
3021
- turn.localCommand?.afterDelivery?.();
2908
+ turn.usageMarkdownAbort?.abort();
3022
2909
  disarmForceCancel(session);
3023
2910
  session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
3024
2911
  session.activeTurn = null;
@@ -3042,7 +2929,7 @@ export class ClaudeAcpAgent {
3042
2929
  };
3043
2930
  /** Reject the active turn (auth required, error result, …) without tearing
3044
2931
  * down the consumer: the stream continues to idle and later turns proceed. */
3045
- const failActive = (error, title) => {
2932
+ const failActive = (error) => {
3046
2933
  disarmForceCancel(session);
3047
2934
  const turn = session.activeTurn;
3048
2935
  if (!turn || turn.settled) {
@@ -3059,7 +2946,7 @@ export class ClaudeAcpAgent {
3059
2946
  // start the next stretch clean, or its stale delivery record would
3060
2947
  // suppress the next turn's issue-#453 result-text fallback.
3061
2948
  session.emittedAssistantText = false;
3062
- turn.reject(error, title);
2949
+ turn.reject(error);
3063
2950
  };
3064
2951
  /** Complete a negotiated terminal failure on the prompt response itself,
3065
2952
  * which is the canonical AIR carrier. Legacy clients keep the historical
@@ -3083,7 +2970,7 @@ export class ClaudeAcpAgent {
3083
2970
  return;
3084
2971
  }
3085
2972
  if (!supportsAirSessionFailures(this.clientCapabilities)) {
3086
- failActive(error, title);
2973
+ failActive(error);
3087
2974
  return;
3088
2975
  }
3089
2976
  if (!session.activeTurn || session.activeTurn.settled) {
@@ -3093,7 +2980,7 @@ export class ClaudeAcpAgent {
3093
2980
  }
3094
2981
  const failure = await createSessionFailure(kind, { title });
3095
2982
  if (!failure) {
3096
- failActive(error, title);
2983
+ failActive(error);
3097
2984
  return;
3098
2985
  }
3099
2986
  sessionFailures.recordActive(failure);
@@ -3197,7 +3084,7 @@ export class ClaudeAcpAgent {
3197
3084
  }
3198
3085
  }
3199
3086
  await compaction.interrupt();
3200
- await settleActive(cancelledOutcome(session, session.activeTurn));
3087
+ await settleActive(turnOutcome(session, "cancelled"));
3201
3088
  // The cancelled turn's result may never come (that's why the
3202
3089
  // backstop fired) — close its delivery stretch here so partial
3203
3090
  // streamed text can't suppress the next turn's issue-#453 fallback.
@@ -3253,7 +3140,7 @@ export class ClaudeAcpAgent {
3253
3140
  const inFlight = session.activeTurn;
3254
3141
  await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
3255
3142
  await settleActive(session.cancelled
3256
- ? cancelledOutcome(session, inFlight)
3143
+ ? turnOutcome(session, "cancelled")
3257
3144
  : (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
3258
3145
  // Queued turns the SDK never started never ran, so reject them rather
3259
3146
  // than reporting a success (end_turn) — or a misleading "cancelled" —
@@ -3288,10 +3175,10 @@ export class ClaudeAcpAgent {
3288
3175
  // prompt() stamps a uuid on every message. The frame is @internal and
3289
3176
  // absent from the SDKMessage union, so handle it BEFORE the exhaustive
3290
3177
  // switch: it must not reach `unreachable`'s error log, and a `case`
3291
- // for it wouldn't typecheck. It feeds the orphan accounting (see
3292
- // Session.orphanCommands) and reports a turn inserted at "started";
3293
- // turn activation and settlement stay driven by echoes/results/idle.
3294
- // (Raw-mode emission above still forwards these frames.)
3178
+ // for it wouldn't typecheck. It feeds only the orphan accounting (see
3179
+ // Session.orphanCommands); turn settlement stays driven by
3180
+ // echoes/results/idle. (Raw-mode emission above still forwards these
3181
+ // frames.)
3295
3182
  if (message.type === "command_lifecycle") {
3296
3183
  const frame = message;
3297
3184
  switch (frame.state) {
@@ -3302,17 +3189,6 @@ export class ClaudeAcpAgent {
3302
3189
  if (queued) {
3303
3190
  queued.commandStarted = true;
3304
3191
  compaction.resume();
3305
- // Claude Code took the prompt in: it drained into a turn. A
3306
- // fresh turn reports that before anything it produces, its echo
3307
- // and first stream events included, so this is the earliest
3308
- // insertion point. (A prompt folded into a running turn reports
3309
- // it after its echo, which already activated it.) While another
3310
- // turn is still active (held for background work, steered, or
3311
- // cancelled and awaiting its trailing idle), insertion waits
3312
- // for this turn's echo, whose hand-off ends that turn first.
3313
- const active = session.activeTurn;
3314
- if (!active || active.settled || active === queued)
3315
- reportInserted(queued);
3316
3192
  }
3317
3193
  // ...and promote an already-orphaned command: once dispatched,
3318
3194
  // a bare `cancelled` no longer means "dropped without running".
@@ -3489,18 +3365,17 @@ export class ClaudeAcpAgent {
3489
3365
  if (compaction.consumeDuplicateErrorOutput(message.content)) {
3490
3366
  break;
3491
3367
  }
3492
- const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
3493
- const commandMarkdown = await takeLocalCommandMarkdown(message.content);
3494
- if (commandTurn?.localCommand && session.cancelled)
3368
+ const usageTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
3369
+ const usageMarkdown = await takeUsageMarkdown(message.content);
3370
+ if (usageTurn?.isUsageCommand && session.cancelled)
3495
3371
  break;
3496
- if (commandMarkdown === null)
3372
+ if (usageMarkdown === null)
3497
3373
  break;
3498
3374
  await sendUpdate({
3499
3375
  sessionId: params.sessionId,
3500
3376
  update: {
3501
3377
  sessionUpdate: "agent_message_chunk",
3502
- content: { type: "text", text: commandMarkdown ?? message.content },
3503
- messageId: messageIdForGrouping(message),
3378
+ content: { type: "text", text: usageMarkdown ?? message.content },
3504
3379
  },
3505
3380
  });
3506
3381
  break;
@@ -3557,13 +3432,7 @@ export class ClaudeAcpAgent {
3557
3432
  // the interrupted turn's tokens entirely (issue #844). Zero
3558
3433
  // when the cancel pre-empted the result (wedge/force-cancel).
3559
3434
  if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
3560
- // A held turn's result passed long ago; this idle is the
3561
- // trailer of the followup cycle the cancel interrupted,
3562
- // whose result (an autonomous one) recorded it as owed.
3563
- if (isHeldOpen(session.activeTurn) && session.owedTrailingIdles > 0) {
3564
- session.owedTrailingIdles--;
3565
- }
3566
- await settleActive(cancelledOutcome(session, session.activeTurn));
3435
+ await settleActive(turnOutcome(session, "cancelled"));
3567
3436
  // An interrupt can pre-empt the turn's result entirely
3568
3437
  // (nothing ran the result-case `finally`), so close the
3569
3438
  // delivery stretch here: idle is the SDK's authoritative
@@ -3665,12 +3534,11 @@ export class ClaudeAcpAgent {
3665
3534
  // list with this payload. Forward message.commands directly —
3666
3535
  // it's authoritative, and re-querying supportedCommands()
3667
3536
  // would just return the same list with an extra round-trip.
3668
- session.skillPaths = new Map();
3669
3537
  await sendUpdate({
3670
3538
  sessionId: params.sessionId,
3671
3539
  update: {
3672
3540
  sessionUpdate: "available_commands_update",
3673
- availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, session.skillPaths),
3541
+ availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
3674
3542
  },
3675
3543
  });
3676
3544
  break;
@@ -4442,7 +4310,6 @@ export class ClaudeAcpAgent {
4442
4310
  update: {
4443
4311
  sessionUpdate: "agent_message_chunk",
4444
4312
  content: { type: "text", text: lastRefusalExplanation },
4445
- messageId: messageIdForGrouping(message),
4446
4313
  },
4447
4314
  });
4448
4315
  }
@@ -4517,13 +4384,10 @@ export class ClaudeAcpAgent {
4517
4384
  (message.usage.output_tokens ?? 0) === 0 &&
4518
4385
  !session.activeTurn?.noticeTexts?.includes(normalizeNoticeText(message.result)));
4519
4386
  if (shouldForwardResult) {
4520
- const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
4521
- const commandMarkdown = await takeLocalCommandMarkdown(message.result);
4522
- if (commandTurn?.localCommand && session.cancelled)
4387
+ const usageMarkdown = await takeUsageMarkdown(message.result);
4388
+ if (usageMarkdown === null)
4523
4389
  break;
4524
- if (commandMarkdown === null)
4525
- break;
4526
- for (const notification of toAcpNotifications(commandMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4390
+ for (const notification of toAcpNotifications(usageMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger)) {
4527
4391
  await sendUpdate(notification);
4528
4392
  }
4529
4393
  }
@@ -4736,7 +4600,6 @@ export class ClaudeAcpAgent {
4736
4600
  }
4737
4601
  for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
4738
4602
  clientCapabilities: this.clientCapabilities,
4739
- toolCallCapabilities: this.toolCallCapabilities,
4740
4603
  cwd: session.cwd,
4741
4604
  taskState: session.taskState,
4742
4605
  emittedToolCalls: session.emittedToolCalls,
@@ -4806,7 +4669,7 @@ export class ClaudeAcpAgent {
4806
4669
  session.owedTrailingIdles++;
4807
4670
  // Before activateTurn resets the accumulator, so the
4808
4671
  // usage still belongs to the cancelled turn.
4809
- await settleActive(cancelledOutcome(session, session.activeTurn));
4672
+ await settleActive(turnOutcome(session, "cancelled"));
4810
4673
  }
4811
4674
  else if (isHeldOpen(session.activeTurn)) {
4812
4675
  // A turn held open for its background subagents (see
@@ -4917,16 +4780,16 @@ export class ClaudeAcpAgent {
4917
4780
  // Depending on the Claude Code build, a local command can arrive
4918
4781
  // as the dedicated system message above or as a synthetic
4919
4782
  // assistant message. Replace only the output owned by the exact
4920
- // /usage or /mcp turn; no content signatures or text parsing are involved.
4783
+ // /usage turn; no content signatures or text parsing are involved.
4921
4784
  if (message.type === "assistant" &&
4922
4785
  message.parent_tool_use_id === null &&
4923
4786
  message.message.model === "<synthetic>") {
4924
- const commandMarkdown = await takeLocalCommandMarkdown(assistantMessageText(message.message) ?? "");
4787
+ const usageMarkdown = await takeUsageMarkdown(assistantMessageText(message.message) ?? "");
4925
4788
  if (session.cancelled)
4926
4789
  break;
4927
- if (commandMarkdown !== undefined) {
4928
- if (commandMarkdown !== null) {
4929
- for (const notification of toAcpNotifications(commandMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4790
+ if (usageMarkdown !== undefined) {
4791
+ if (usageMarkdown !== null) {
4792
+ for (const notification of toAcpNotifications(usageMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
4930
4793
  await sendUpdate(notification);
4931
4794
  }
4932
4795
  }
@@ -4940,7 +4803,6 @@ export class ClaudeAcpAgent {
4940
4803
  if (typeof stripped === "string") {
4941
4804
  for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
4942
4805
  clientCapabilities: this.clientCapabilities,
4943
- toolCallCapabilities: this.toolCallCapabilities,
4944
4806
  parentToolUseId: message.parent_tool_use_id,
4945
4807
  cwd: session.cwd,
4946
4808
  taskState: session.taskState,
@@ -5022,7 +4884,6 @@ export class ClaudeAcpAgent {
5022
4884
  }
5023
4885
  for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
5024
4886
  clientCapabilities: this.clientCapabilities,
5025
- toolCallCapabilities: this.toolCallCapabilities,
5026
4887
  parentToolUseId: message.parent_tool_use_id,
5027
4888
  cwd: session.cwd,
5028
4889
  taskState: session.taskState,
@@ -5212,36 +5073,6 @@ export class ClaudeAcpAgent {
5212
5073
  session.pendingOrphanResults = (session.pendingOrphanResults ?? 0) + 1;
5213
5074
  }
5214
5075
  }
5215
- /** Ask Claude Code to drop user messages it still has queued, so that a
5216
- * prompt a cancel settled is not run after all. Claude Code keeps queued
5217
- * messages through an interrupt and runs them next (the receipt's
5218
- * `still_queued`), unless they are cancelled first with
5219
- * `cancel_async_message`: a no-op for a message it already dequeued, which
5220
- * then still runs, as an orphan (see `orphanCommands`).
5221
- *
5222
- * Not awaited: the SDK writes control requests in call order, so the drops
5223
- * reach Claude Code before a request sent after them, and a wedged CLI must
5224
- * not hold up the cancel. The orphan accounting needs no update here: a
5225
- * dropped message gets a `cancelled` lifecycle frame and is missing from the
5226
- * interrupt receipt's `still_queued`, which both already account for. The
5227
- * SDK implements the request as `Query.cancelAsyncMessage` without typing
5228
- * it, so it is looked up, and a CLI that lacks it rejects the request. */
5229
- dropQueuedMessages(sessionId, query, uuids) {
5230
- const cancelAsyncMessage = query
5231
- .cancelAsyncMessage;
5232
- if (typeof cancelAsyncMessage !== "function")
5233
- return;
5234
- const logFailure = (uuid, error) => this.logger.error(`Session ${sessionId}: could not drop the queued message ${uuid} from Claude Code:`, error);
5235
- for (const uuid of uuids) {
5236
- // Called synchronously: deferring it would let the interrupt go first.
5237
- try {
5238
- Promise.resolve(cancelAsyncMessage.call(query, uuid)).catch((error) => logFailure(uuid, error));
5239
- }
5240
- catch (error) {
5241
- logFailure(uuid, error);
5242
- }
5243
- }
5244
- }
5245
5076
  async cancel(params) {
5246
5077
  await this.cancelTurns(params, { awaitInterrupt: true });
5247
5078
  }
@@ -5258,7 +5089,7 @@ export class ClaudeAcpAgent {
5258
5089
  }
5259
5090
  session.cancelled = true;
5260
5091
  for (const turn of session.turnQueue ?? [])
5261
- turn.localCommandAbort?.abort();
5092
+ turn.usageMarkdownAbort?.abort();
5262
5093
  session.pendingExitPlanModeInterruption = undefined;
5263
5094
  session.pendingExitPlanContextReset = undefined;
5264
5095
  // The stream already ended (see closeQueryStream): every in-flight turn was
@@ -5287,16 +5118,12 @@ export class ClaudeAcpAgent {
5287
5118
  session.toolCallFields?.clear();
5288
5119
  clearHookCallbacks(params.sessionId);
5289
5120
  }
5290
- // The user messages this cancel abandons while Claude Code may still have
5291
- // them queued: they are dropped from its queue before the interrupt.
5292
- const abandoned = [];
5293
5121
  // A priority steer may still be queued in the SDK when cancellation
5294
5122
  // settles its owning turn. Its later echo matches no live turn, and its
5295
5123
  // result must be skipped rather than promoted onto the next prompt.
5296
5124
  if (isSteering(session.activeTurn)) {
5297
5125
  for (const uuid of session.activeTurn.steeredEchoes) {
5298
5126
  this.trackOrphanCommand(session, uuid, "pending");
5299
- abandoned.push(uuid);
5300
5127
  }
5301
5128
  }
5302
5129
  // Capture the orphan-accounting lane before anything can await: the
@@ -5320,13 +5147,11 @@ export class ClaudeAcpAgent {
5320
5147
  // accumulator (the active turn's tally) is not its spend.
5321
5148
  turn.resolve({ stopReason: "cancelled" });
5322
5149
  orphanedTurns.push(turn);
5323
- abandoned.push(turn.promptUuid);
5324
5150
  }
5325
5151
  }
5326
- // Each removed queued turn's user message was already pushed to the SDK.
5327
- // It is dropped from Claude Code's queue below (see dropQueuedMessages),
5328
- // but one Claude Code already dequeued still runs, FIFO, and emits a
5329
- // result with no user echo to match (0.3.246+ CLIs do stamp results with the
5152
+ // Each removed queued turn's user message was already pushed to the SDK,
5153
+ // which processes input FIFO and will still emit a result for it with no
5154
+ // user echo to match (0.3.246+ CLIs do stamp results with the
5330
5155
  // triggering send's user_message_uuid, which ensureActiveTurn uses as
5331
5156
  // an exact join when present). Track those so the consumer skips them
5332
5157
  // (see ensureActiveTurn) rather than misattributing them to the head.
@@ -5389,23 +5214,15 @@ export class ClaudeAcpAgent {
5389
5214
  }
5390
5215
  // A deferred active turn (see Turn.deferredSettle) already has its result
5391
5216
  // and is only held open for subagents. A settling turn likewise has its
5392
- // result but is awaiting the bounded checkpoint preview. Settle it
5393
- // "cancelled" NOW where the consumer cannot: it cannot process the
5394
- // interrupt's trailing idle while blocked in that preview, and a held turn
5395
- // whose session sits idle gets no trailing idle (the interrupt emits
5396
- // nothing; with no state events consumed yet, the same is assumed).
5397
- // A held turn whose followup cycle is live (running, or blocked on a
5398
- // permission request) is left to that idle, like any live turn: the
5399
- // interrupt still flushes the cycle's output, which must reach the client
5400
- // before the turn ends cancelled. The captured outcome preserves usage and
5401
- // metadata (issue #844), while reporter state makes a late checkpoint
5402
- // response harmless.
5217
+ // result but is awaiting the bounded checkpoint preview. Settle either one
5218
+ // "cancelled" NOW: the consumer cannot process the interrupt's trailing
5219
+ // idle while blocked in that preview, and a held turn may already be idle.
5220
+ // The captured outcome preserves usage and metadata (issue #844), while
5221
+ // reporter state makes a late checkpoint response harmless.
5403
5222
  {
5404
5223
  const active = session.activeTurn;
5405
- const followupLive = session.lastSessionState === "running" || session.lastSessionState === "requires_action";
5406
- if (active &&
5407
- (active.settling || (isHeldOpen(active) && !followupLive)) &&
5408
- (active.deferredSettle ?? active.settlingOutcome)) {
5224
+ const pendingOutcome = active?.deferredSettle ?? active?.settlingOutcome;
5225
+ if (active && pendingOutcome && (isHeldOpen(active) || active.settling)) {
5409
5226
  session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
5410
5227
  active.settled = true;
5411
5228
  // Mirror settleActive's invariants (it is consumer-scoped and
@@ -5419,16 +5236,17 @@ export class ClaudeAcpAgent {
5419
5236
  // text since the last boundary was its followups', and left latched
5420
5237
  // it would suppress a following replayed turn's issue-#453 fallback.
5421
5238
  session.emittedAssistantText = false;
5422
- // When the interrupt below pre-empts a live cycle (a settling turn's,
5423
- // or one no state event reported), it produces a trailer idle with no
5424
- // counted result; that idle would be un-owed and could lag past the
5425
- // next prompt's echo — read as the fresh turn ending without a result
5426
- // (issue #825 false-fail).
5427
- // Pre-count it unless the session sits idle: there the interrupt
5428
- // emits nothing, and a debt that never drains would mask one future
5429
- // #825 detection. (lastSessionState is last-CONSUMED, so both stale
5430
- // reads exist and both are accepted one-cycle windows: a running
5431
- // transition still in the backlog reads as stale idle and
5239
+ // When the interrupt below pre-empts a live cycle — running, or
5240
+ // blocked on a permission request (requires_action, the #866 shape
5241
+ // users cancel out of) — it produces a trailer idle with no counted
5242
+ // result; with the hold's own trailer typically already absorbed,
5243
+ // that idle would be un-owed and could lag past the next prompt's
5244
+ // echo — read as the fresh turn ending without a result (issue #825
5245
+ // false-fail). Pre-count it unless the session sits idle: there the
5246
+ // interrupt emits nothing, and a debt that never drains would mask
5247
+ // one future #825 detection. (lastSessionState is last-CONSUMED, so
5248
+ // both stale reads exist and both are accepted one-cycle windows: a
5249
+ // running transition still in the backlog reads as stale idle and
5432
5250
  // under-counts — that false-fail additionally needs the trailer to
5433
5251
  // lag past the next echo — while a cycle already completed into the
5434
5252
  // backlog reads as stale non-idle and over-counts, masking one
@@ -5438,7 +5256,14 @@ export class ClaudeAcpAgent {
5438
5256
  if (session.lastSessionState !== "idle") {
5439
5257
  session.owedTrailingIdles++;
5440
5258
  }
5441
- active.resolve(cancelledOutcome(session, active));
5259
+ // Carries the held outcome's `_meta` too: a deferred outcome's only
5260
+ // metadata is its quota breakdown, the counterpart of the usage taken
5261
+ // from it here.
5262
+ active.resolve({
5263
+ stopReason: "cancelled",
5264
+ usage: pendingOutcome.usage,
5265
+ ...(pendingOutcome._meta ? { _meta: pendingOutcome._meta } : {}),
5266
+ });
5442
5267
  }
5443
5268
  }
5444
5269
  // A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
@@ -5471,9 +5296,6 @@ export class ClaudeAcpAgent {
5471
5296
  cancelController.abort();
5472
5297
  }, this.forceCancelGraceMs);
5473
5298
  }
5474
- // Before the interrupt: once it stops the running turn, Claude Code starts
5475
- // the next queued message right away.
5476
- this.dropQueuedMessages(params.sessionId, session.query, abandoned);
5477
5299
  const interrupt = session.query.interrupt();
5478
5300
  if (!options.awaitInterrupt) {
5479
5301
  // The caller closes the query next, which rejects the pending reply.
@@ -5570,18 +5392,12 @@ export class ClaudeAcpAgent {
5570
5392
  session.query.close();
5571
5393
  }
5572
5394
  /** Cleanly tear down a session: cancel in-flight work, release stream
5573
- * resources, and remove it from the session map. Returns once the turns it
5574
- * cancels have ended, so their end (a v1 `session/prompt` response, a v2
5575
- * `idle` state) goes out before the caller answers: ACP v2's
5576
- * `session/close` cancels "as if `session/cancel` had been called", then
5577
- * frees the session. A wedged consumer holds it at most
5578
- * {@link TEARDOWN_TURN_END_TIMEOUT_MS}. */
5395
+ * resources, and remove it from the session map. */
5579
5396
  async teardownSession(sessionId) {
5580
5397
  const session = this.sessions[sessionId];
5581
5398
  if (!session) {
5582
5399
  return;
5583
5400
  }
5584
- const turnsEnded = Promise.all((session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
5585
5401
  try {
5586
5402
  await this.cancelTurns({ sessionId }, { awaitInterrupt: false });
5587
5403
  }
@@ -5609,10 +5425,6 @@ export class ClaudeAcpAgent {
5609
5425
  session.nativeSubagentRuntime?.clear();
5610
5426
  session.asyncTaskRuntime?.clear();
5611
5427
  delete this.sessions[sessionId];
5612
- const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
5613
- if (ended.type === "timeout") {
5614
- this.logger.error(`Session ${sessionId}: its cancelled turns did not end within ${TEARDOWN_TURN_END_TIMEOUT_MS}ms of the teardown`);
5615
- }
5616
5428
  }
5617
5429
  /** Tear down all active sessions. Called when the ACP connection closes. */
5618
5430
  async dispose() {
@@ -5719,15 +5531,13 @@ export class ClaudeAcpAgent {
5719
5531
  await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
5720
5532
  return { configOptions: session.configOptions };
5721
5533
  }
5722
- async replaySessionHistory(sessionId, resumedMessages, pending) {
5534
+ async replaySessionHistory(sessionId, resumedMessages) {
5723
5535
  const replayStartedAt = performance.now();
5724
5536
  const toolUseCache = {};
5725
5537
  const messages = resumedMessages ?? (await getSessionMessages(sessionId));
5726
5538
  const historyLoadedAt = performance.now();
5727
5539
  this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
5728
- // A pending replay has no session record yet. It uses the state that the record takes later.
5729
- const replayState = () => pending?.state ?? this.sessions[sessionId];
5730
- const session = replayState();
5540
+ const session = this.sessions[sessionId];
5731
5541
  // A replay rebuilds the client view, so its plan goes out again.
5732
5542
  if (session?.taskState)
5733
5543
  forgetPublishedTaskPlan(session.taskState);
@@ -5739,12 +5549,7 @@ export class ClaudeAcpAgent {
5739
5549
  sessionId,
5740
5550
  state: session.sessionFailureState,
5741
5551
  capabilities: this.clientCapabilities,
5742
- isCurrent: () => {
5743
- const live = this.sessions[sessionId];
5744
- if (!live)
5745
- return pending !== undefined && !pending.stopped;
5746
- return live.sessionFailureState === session.sessionFailureState;
5747
- },
5552
+ isCurrent: () => this.sessions[sessionId] === session,
5748
5553
  sendUpdate: (notification) => this.client.sessionUpdate(notification),
5749
5554
  logger: this.logger,
5750
5555
  })
@@ -5899,7 +5704,7 @@ export class ClaudeAcpAgent {
5899
5704
  await replayAsyncTasks.taskNotification(notification);
5900
5705
  };
5901
5706
  const replayMessage = async (message) => {
5902
- if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
5707
+ if (isReplayHiddenMetaMessage(message)) {
5903
5708
  return;
5904
5709
  }
5905
5710
  if (message.type === "user" &&
@@ -5913,7 +5718,7 @@ export class ClaudeAcpAgent {
5913
5718
  // a client-supplied id without an extra getSessionMessages read. Not read
5914
5719
  // yet (see Session.messageIdToUuid).
5915
5720
  const replayMessageId = messageIdForGrouping(message);
5916
- const replaySession = replayState();
5721
+ const replaySession = this.sessions[sessionId];
5917
5722
  if (replaySession && replayMessageId && message.uuid) {
5918
5723
  replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
5919
5724
  }
@@ -6007,9 +5812,8 @@ export class ClaudeAcpAgent {
6007
5812
  registerHooks: false,
6008
5813
  replay: true,
6009
5814
  clientCapabilities: this.clientCapabilities,
6010
- toolCallCapabilities: this.toolCallCapabilities,
6011
- cwd: replayState()?.cwd,
6012
- taskState: replayState()?.taskState,
5815
+ cwd: this.sessions[sessionId]?.cwd,
5816
+ taskState: this.sessions[sessionId]?.taskState,
6013
5817
  messageId: replayMessageId,
6014
5818
  parentToolUseId,
6015
5819
  })) {
@@ -6054,7 +5858,7 @@ export class ClaudeAcpAgent {
6054
5858
  };
6055
5859
  for (const message of messages)
6056
5860
  await replayMessage(message);
6057
- if (nativeReplayEnabled && !pending?.stopped) {
5861
+ if (nativeReplayEnabled) {
6058
5862
  // Claude history persists sidechain messages and Agent/Task tool uses,
6059
5863
  // but not task_started/task_updated lifecycle frames. Recover terminal
6060
5864
  // state from the launch tool_result. Missing results and malformed or
@@ -6147,10 +5951,8 @@ export class ClaudeAcpAgent {
6147
5951
  // Do not rely on every ACP client settling requestPermission after the
6148
5952
  // cancellation signal. The local race guarantees that Claude's tool call
6149
5953
  // is released even when an older or broken client ignores $/cancel_request.
6150
- // The request goes to a subagent's child session for a tool call of that
6151
- // subagent, but it blocks the turn of the owner session.
6152
5954
  try {
6153
- return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
5955
+ return await this.withPendingUserInput(params.sessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
6154
5956
  }
6155
5957
  catch (error) {
6156
5958
  if (signal.aborted) {
@@ -6260,7 +6062,7 @@ export class ClaudeAcpAgent {
6260
6062
  // Like permission requests, the elicitation references this toolUseID, so
6261
6063
  // make sure the tool_call has surfaced to the client before we send it.
6262
6064
  await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal, permissionSessionId);
6263
- return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal);
6065
+ return this.handleAskUserQuestion(permissionSessionId, toolInput, toolUseID, signal);
6264
6066
  }
6265
6067
  // Do not auto-allow here based on the session's advertised mode. Claude
6266
6068
  // Code applies bypassPermissions before invoking canUseTool; a request
@@ -6276,8 +6078,8 @@ export class ClaudeAcpAgent {
6276
6078
  const noPersistentRule = matchedAskRule !== undefined || suppressAlwaysAllowRule === true;
6277
6079
  const durableChangeSet = normalizeDurablePermissionChangeSet(suggestions, noPersistentRule);
6278
6080
  const capabilities = this.toolCallCapabilities;
6279
- const previewContent = capabilities.diffPatch || capabilities.v2
6280
- ? await previewPatchContent(toolName, toolInput, session.cwd, capabilities.v2 ? "v2" : "air")
6081
+ const previewContent = capabilities.diffPatch
6082
+ ? await previewPatchContent(toolName, toolInput, session.cwd)
6281
6083
  : undefined;
6282
6084
  const presentation = buildClaudePermissionPresentation({
6283
6085
  toolName,
@@ -6414,15 +6216,13 @@ export class ClaudeAcpAgent {
6414
6216
  * Present the built-in AskUserQuestion tool's questions as an ACP form
6415
6217
  * elicitation and return the answers as the tool's `updatedInput`. Called from
6416
6218
  * `canUseTool` since that is where the SDK routes the tool's permission check.
6417
- * The elicitation goes to `requestSessionId`, a subagent's child session for
6418
- * a question it asks, and blocks the turn of `sessionId`.
6419
6219
  */
6420
- async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal) {
6220
+ async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal) {
6421
6221
  const questions = extractAskUserQuestions(toolInput);
6422
6222
  if (!questions) {
6423
6223
  return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
6424
6224
  }
6425
- const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
6225
+ const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID, this.toolCallCapabilities.air.client);
6426
6226
  let response;
6427
6227
  try {
6428
6228
  response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
@@ -6492,7 +6292,7 @@ export class ClaudeAcpAgent {
6492
6292
  sessionId,
6493
6293
  update: {
6494
6294
  sessionUpdate: "available_commands_update",
6495
- availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, (session.skillPaths ??= new Map())),
6295
+ availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands),
6496
6296
  },
6497
6297
  });
6498
6298
  }
@@ -6813,8 +6613,6 @@ export class ClaudeAcpAgent {
6813
6613
  }
6814
6614
  async getOrCreateSession(params, resumedSession) {
6815
6615
  const existingSession = this.sessions[params.sessionId];
6816
- // A recreated live session keeps its mode, not the mode of the transcript.
6817
- const livePermissionMode = existingSession?.modes.currentModeId;
6818
6616
  if (existingSession) {
6819
6617
  const fingerprint = computeSessionFingerprint(params);
6820
6618
  if (fingerprint === existingSession.sessionFingerprint) {
@@ -6830,6 +6628,9 @@ export class ClaudeAcpAgent {
6830
6628
  // up the new values.
6831
6629
  await this.teardownSession(params.sessionId);
6832
6630
  }
6631
+ const resumedModelHint = resumedSession
6632
+ ? resumedSession.model
6633
+ : await readResumedModel(params.sessionId, this.logger);
6833
6634
  const response = await this.createSession({
6834
6635
  cwd: params.cwd,
6835
6636
  mcpServers: params.mcpServers ?? [],
@@ -6837,8 +6638,7 @@ export class ClaudeAcpAgent {
6837
6638
  _meta: params._meta,
6838
6639
  }, {
6839
6640
  resume: params.sessionId,
6840
- permissionMode: livePermissionMode,
6841
- ...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
6641
+ resumedModelHint,
6842
6642
  });
6843
6643
  return {
6844
6644
  sessionId: response.sessionId,
@@ -6867,12 +6667,6 @@ export class ClaudeAcpAgent {
6867
6667
  throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
6868
6668
  }
6869
6669
  }
6870
- /** Whether the session forwards the text and the thinking of a subagent. */
6871
- forwardsSubagentText(meta) {
6872
- return (clientSupportsSubagents(this.clientCapabilities) ||
6873
- supportsSubagentTranscript(this.clientCapabilities) ||
6874
- meta?.claudeCode?.options?.forwardSubagentText === true);
6875
- }
6876
6670
  async createSession(params, creationOpts = {}) {
6877
6671
  const createStartedAt = performance.now();
6878
6672
  // Validate `cwd` up front. The ACP spec requires an absolute path, and the
@@ -6910,15 +6704,10 @@ export class ClaudeAcpAgent {
6910
6704
  // sign-out respawn, provider rerouting), so fill the same fast local hint
6911
6705
  // here when the caller did not provide it. Never fall back to the slow
6912
6706
  // getContextUsage control request.
6913
- // The same tail read gives the permission mode of the resumed session.
6914
6707
  let resumedModelHint = creationOpts.resumedModelHint;
6915
- let resumedPermissionMode;
6916
- if (creationOpts.resume !== undefined) {
6917
- const withModel = !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint");
6918
- const tail = await readResumedTail(creationOpts.resume, this.logger, withModel);
6919
- if (withModel)
6920
- resumedModelHint = tail.model;
6921
- resumedPermissionMode = tail.permissionMode;
6708
+ if (creationOpts.resume !== undefined &&
6709
+ !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint")) {
6710
+ resumedModelHint = await readResumedModel(creationOpts.resume, this.logger);
6922
6711
  timing.phase("resume-transcript");
6923
6712
  }
6924
6713
  const input = new Pushable();
@@ -6980,11 +6769,7 @@ export class ClaudeAcpAgent {
6980
6769
  const allowBypass = ALLOW_BYPASS &&
6981
6770
  settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
6982
6771
  sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
6983
- const initialPermissionMode = resolveInitialPermissionMode({
6984
- explicit: creationOpts.permissionMode,
6985
- resumed: resumedPermissionMode,
6986
- defaultMode: settingsManager.getSettings().permissions?.defaultMode,
6987
- }, this.logger, allowBypass);
6772
+ const initialPermissionMode = resolvePermissionMode(creationOpts.permissionMode ?? settingsManager.getSettings().permissions?.defaultMode, this.logger, allowBypass);
6988
6773
  const userProvidedOptions = sessionMeta?.claudeCode?.options
6989
6774
  ? { ...sessionMeta.claudeCode.options }
6990
6775
  : undefined;
@@ -6993,7 +6778,9 @@ export class ClaudeAcpAgent {
6993
6778
  // hidden state that the Client cannot inspect or change.
6994
6779
  if (userProvidedOptions)
6995
6780
  delete userProvidedOptions.agent;
6996
- const forwardSubagentText = this.forwardsSubagentText(params._meta);
6781
+ const forwardSubagentText = clientSupportsSubagents(this.clientCapabilities) ||
6782
+ supportsSubagentTranscript(this.clientCapabilities) ||
6783
+ userProvidedOptions?.forwardSubagentText === true;
6997
6784
  // Configure thinking behavior from environment variable
6998
6785
  const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
6999
6786
  // Parse model configuration from environment (e.g. Bedrock model overrides)
@@ -7019,7 +6806,7 @@ export class ClaudeAcpAgent {
7019
6806
  // Per-session task state. Created here (rather than in the session record
7020
6807
  // below) so the TaskCreated/TaskCompleted hook callbacks can close over
7021
6808
  // the same Map that the streaming message handler will read from.
7022
- const taskState = creationOpts.replayState?.taskState ?? new Map();
6809
+ const taskState = new Map();
7023
6810
  // Resolve every workspace root once. The native checkpoint report uses
7024
6811
  // this same set for lexical path validation, and the SDK receives it below.
7025
6812
  const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
@@ -7312,7 +7099,7 @@ export class ClaudeAcpAgent {
7312
7099
  const allowedModels = Array.isArray(settingsAvailableModels)
7313
7100
  ? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
7314
7101
  : initializationResult.models;
7315
- const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
7102
+ const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, resumedModelHint);
7316
7103
  timing.phase("models");
7317
7104
  // Resolve the current model's capabilities separately from the stable
7318
7105
  // permission-mode catalog advertised to ACP clients.
@@ -7461,8 +7248,8 @@ export class ClaudeAcpAgent {
7461
7248
  nativeSubagentParentByToolUseId: new Map(),
7462
7249
  emittedAssistantText: false,
7463
7250
  owedTrailingIdles: 0,
7464
- messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
7465
- sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
7251
+ messageIdToUuid: new Map(),
7252
+ sessionFailureState: createSessionFailureState(),
7466
7253
  claudeSubscriptionGuard,
7467
7254
  accountKind: fromAccountInfo(initializationResult.account)?.kind,
7468
7255
  fileChangeReporter,
@@ -7503,10 +7290,7 @@ export class ClaudeAcpAgent {
7503
7290
  this.closeQueryStream(session);
7504
7291
  delete this.sessions[sessionId];
7505
7292
  try {
7506
- await this.createSession(session.creationParams, {
7507
- resume: sessionId,
7508
- permissionMode: session.modes.currentModeId,
7509
- });
7293
+ await this.createSession(session.creationParams, { resume: sessionId });
7510
7294
  }
7511
7295
  catch (error) {
7512
7296
  // One session that cannot come back must not abort the switch. The
@@ -7600,20 +7384,6 @@ function turnOutcome(session, stopReason, extraMeta) {
7600
7384
  _meta: { ...turnQuotaMeta(session), ...extraMeta },
7601
7385
  };
7602
7386
  }
7603
- /** The outcome of `turn` when a cancel ends it. A turn that already has its
7604
- * result, because it is held open for its background subagents (see
7605
- * Turn.deferredSettle) or is settling (see Turn.settlingOutcome), keeps the
7606
- * usage and metadata that result recorded (issue #844). */
7607
- function cancelledOutcome(session, turn) {
7608
- const recorded = turn?.deferredSettle ?? turn?.settlingOutcome;
7609
- if (!recorded)
7610
- return turnOutcome(session, "cancelled");
7611
- return {
7612
- stopReason: "cancelled",
7613
- usage: recorded.usage,
7614
- ...(recorded._meta ? { _meta: recorded._meta } : {}),
7615
- };
7616
- }
7617
7387
  /** `_meta.quota` for a prompt response: what the turn spent, shaped like
7618
7388
  * codex-acp's (snake_case container keys, camelCase counters) so a client
7619
7389
  * reads one shape from either agent.
@@ -8001,20 +7771,7 @@ function getAvailableSlashCommands(commands,
8001
7771
  // Names the CLI tagged terminal-bound on `system`/init (their UX lives in
8002
7772
  // the CLI's own terminal, which ACP clients aren't) — filtered alongside
8003
7773
  // the static list. Raw CLI names, matched before the MCP rename.
8004
- terminalCommands,
8005
- // The session cwd of an AIR client, or undefined for every other client.
8006
- // AIR gets the kind and the SKILL.md path of each skill command.
8007
- airSkillCwd,
8008
- // The resolved SKILL.md paths of the session, keyed by the cwd and the name.
8009
- skillPaths) {
8010
- const skillPath = (name, cwd) => {
8011
- const key = `${cwd}\0${name}`;
8012
- if (skillPaths?.has(key))
8013
- return skillPaths.get(key);
8014
- const resolved = resolveSkillPath(name, cwd);
8015
- skillPaths?.set(key, resolved);
8016
- return resolved;
8017
- };
7774
+ terminalCommands) {
8018
7775
  const UNSUPPORTED_COMMANDS = [
8019
7776
  "clear",
8020
7777
  "cost",
@@ -8025,10 +7782,8 @@ skillPaths) {
8025
7782
  "release-notes",
8026
7783
  "todos",
8027
7784
  ];
8028
- // The adapter replaces the terminal text of `/mcp` and runs its actions,
8029
- // even when Claude Code tags the command terminal-bound.
8030
- const advertised = commands
8031
- .filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
7785
+ return commands
7786
+ .filter((command) => !terminalCommands?.includes(command.name))
8032
7787
  .map((command) => {
8033
7788
  const input = command.argumentHint
8034
7789
  ? {
@@ -8038,26 +7793,16 @@ skillPaths) {
8038
7793
  }
8039
7794
  : null;
8040
7795
  let name = command.name;
8041
- const mcpPrompt = command.name.endsWith(" (MCP)");
8042
- if (mcpPrompt) {
7796
+ if (command.name.endsWith(" (MCP)")) {
8043
7797
  name = `mcp:${name.replace(" (MCP)", "")}`;
8044
7798
  }
8045
- const skillMdPath = airSkillCwd && !command.builtin && !mcpPrompt
8046
- ? skillPath(command.name, airSkillCwd)
8047
- : undefined;
8048
7799
  return {
8049
7800
  name,
8050
7801
  description: command.description || "",
8051
7802
  input,
8052
- ...(skillMdPath
8053
- ? {
8054
- _meta: withAirMeta(withAirMeta(undefined, AIR_KIND_KEY, "skill"), AIR_SKILL_PATH_KEY, skillMdPath),
8055
- }
8056
- : {}),
8057
7803
  };
8058
7804
  })
8059
7805
  .filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
8060
- return [...advertised, MCP_AVAILABLE_COMMAND];
8061
7806
  }
8062
7807
  function formatUriAsLink(uri) {
8063
7808
  try {
@@ -8232,19 +7977,10 @@ function toolCallFieldsOf(session) {
8232
7977
  * Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
8233
7978
  * Only handles text, image, and thinking chunks for now.
8234
7979
  */
8235
- /**
8236
- * The renderer of the notification functions: the agent's tool call
8237
- * capabilities when they are passed, else those of the ACP capabilities.
8238
- */
8239
- function toolCallRenderer(options) {
8240
- return options?.toolCallCapabilities
8241
- ? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
8242
- : AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
8243
- }
8244
7980
  export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
8245
7981
  const taskState = options?.taskState ?? new Map();
8246
7982
  const registerHooks = options?.registerHooks !== false;
8247
- const renderer = toolCallRenderer(options);
7983
+ const renderer = AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
8248
7984
  if (typeof content === "string") {
8249
7985
  if (content.length === 0) {
8250
7986
  return [];
@@ -8582,7 +8318,6 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
8582
8318
  const streamedToolInputs = options?.streamedToolInputs;
8583
8319
  const forwardedOptions = {
8584
8320
  clientCapabilities: options?.clientCapabilities,
8585
- toolCallCapabilities: options?.toolCallCapabilities,
8586
8321
  parentToolUseId: message.parent_tool_use_id,
8587
8322
  cwd: options?.cwd,
8588
8323
  taskState: options?.taskState,
@@ -8646,7 +8381,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
8646
8381
  // TodoWrite and the Task* tools never surfaced a tool_call to refine.
8647
8382
  if (!shouldEmitToolCall(streamedInput.name))
8648
8383
  return [];
8649
- const update = toolCallRenderer(options).partialRefinement(streamedInput, input, options?.cwd);
8384
+ const update = AcpToolCallRenderer.for(options?.clientCapabilities).partialRefinement(streamedInput, input, options?.cwd);
8650
8385
  if (message.parent_tool_use_id)
8651
8386
  stampParentToolUseId(update, message.parent_tool_use_id);
8652
8387
  applyMessageId(update, options?.messageId);