@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.
- package/README.md +0 -1
- package/dist/acp-agent.d.ts +18 -81
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +170 -435
- package/dist/air-extension.d.ts +0 -2
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +0 -2
- package/dist/diff.d.ts +8 -45
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +30 -111
- package/dist/permissions/presentation.d.ts +2 -17
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/presentation.js +0 -4
- package/dist/resumed-session.d.ts +6 -16
- package/dist/resumed-session.d.ts.map +1 -1
- package/dist/resumed-session.js +25 -91
- package/dist/session-mode.d.ts +0 -14
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +0 -22
- package/dist/tool-calls/client-capabilities.d.ts +2 -20
- package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
- package/dist/tool-calls/client-capabilities.js +4 -14
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +2 -3
- package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
- package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
- package/dist/tool-calls/reporters/file-edit.js +14 -20
- package/dist/turn-events.d.ts +5 -11
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/usage-markdown.d.ts +0 -2
- package/dist/usage-markdown.d.ts.map +1 -1
- package/dist/usage-markdown.js +1 -2
- package/dist/utils.d.ts +0 -12
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -28
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +17 -121
- package/dist/v2/initialize.d.ts +5 -2
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +6 -23
- package/dist/v2/session-update.d.ts +0 -6
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +0 -27
- package/dist/v2/session.d.ts +4 -15
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +6 -20
- package/package.json +2 -2
- package/dist/mcp-command.d.ts +0 -51
- package/dist/mcp-command.d.ts.map +0 -1
- package/dist/mcp-command.js +0 -321
- package/dist/v2/permission.d.ts +0 -25
- package/dist/v2/permission.d.ts.map +0 -1
- package/dist/v2/permission.js +0 -38
- package/dist/v2/prompt.d.ts +0 -47
- package/dist/v2/prompt.d.ts.map +0 -1
- package/dist/v2/prompt.js +0 -153
- package/dist/v2/terminal.d.ts +0 -42
- package/dist/v2/terminal.d.ts.map +0 -1
- package/dist/v2/terminal.js +0 -95
- package/dist/v2/tool-call.d.ts +0 -38
- package/dist/v2/tool-call.d.ts.map +0 -1
- 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,
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
|
1982
|
-
|
|
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
|
-
...(
|
|
1994
|
-
...(
|
|
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
|
|
2005
|
-
events.failed(error
|
|
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
|
|
2595
|
-
if (!turn.
|
|
2490
|
+
const ensureUsageMarkdown = (turn) => {
|
|
2491
|
+
if (!turn.isUsageCommand || !turn.usageMarkdownAbort)
|
|
2596
2492
|
return undefined;
|
|
2597
|
-
turn.
|
|
2598
|
-
return turn.
|
|
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
|
-
|
|
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
|
-
//
|
|
2671
|
-
//
|
|
2672
|
-
//
|
|
2673
|
-
|
|
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
|
-
*
|
|
2847
|
-
*
|
|
2848
|
-
|
|
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
|
|
2854
|
-
if (!
|
|
2742
|
+
const usageMarkdown = ensureUsageMarkdown(turn);
|
|
2743
|
+
if (!usageMarkdown)
|
|
2855
2744
|
return undefined;
|
|
2856
|
-
const markdown = await
|
|
2745
|
+
const markdown = await usageMarkdown;
|
|
2857
2746
|
if (markdown === null)
|
|
2858
2747
|
return undefined;
|
|
2859
|
-
if (turn.
|
|
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.
|
|
2752
|
+
return turn.usageOriginalOutput === originalOutput ? null : undefined;
|
|
2864
2753
|
}
|
|
2865
|
-
turn.
|
|
2866
|
-
turn.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
?
|
|
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)
|
|
3293
|
-
//
|
|
3294
|
-
//
|
|
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
|
|
3493
|
-
const
|
|
3494
|
-
if (
|
|
3368
|
+
const usageTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
3369
|
+
const usageMarkdown = await takeUsageMarkdown(message.content);
|
|
3370
|
+
if (usageTurn?.isUsageCommand && session.cancelled)
|
|
3495
3371
|
break;
|
|
3496
|
-
if (
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
4521
|
-
|
|
4522
|
-
if (commandTurn?.localCommand && session.cancelled)
|
|
4387
|
+
const usageMarkdown = await takeUsageMarkdown(message.result);
|
|
4388
|
+
if (usageMarkdown === null)
|
|
4523
4389
|
break;
|
|
4524
|
-
|
|
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(
|
|
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
|
|
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
|
|
4787
|
+
const usageMarkdown = await takeUsageMarkdown(assistantMessageText(message.message) ?? "");
|
|
4925
4788
|
if (session.cancelled)
|
|
4926
4789
|
break;
|
|
4927
|
-
if (
|
|
4928
|
-
if (
|
|
4929
|
-
for (const notification of toAcpNotifications(
|
|
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.
|
|
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
|
-
//
|
|
5328
|
-
//
|
|
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
|
|
5393
|
-
// "cancelled" NOW
|
|
5394
|
-
//
|
|
5395
|
-
//
|
|
5396
|
-
//
|
|
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
|
|
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
|
|
5423
|
-
//
|
|
5424
|
-
//
|
|
5425
|
-
//
|
|
5426
|
-
//
|
|
5427
|
-
//
|
|
5428
|
-
//
|
|
5429
|
-
//
|
|
5430
|
-
//
|
|
5431
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
6011
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
6280
|
-
? await previewPatchContent(toolName, toolInput, session.cwd
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
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 =
|
|
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.
|
|
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 =
|
|
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,
|
|
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:
|
|
7465
|
-
sessionFailureState:
|
|
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
|
-
|
|
8029
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|