@agentclientprotocol/claude-agent-acp 0.85.2-preview.1 → 0.85.2-preview.11
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 +1 -0
- package/dist/acp-agent.d.ts +113 -27
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +477 -175
- package/dist/air-extension.d.ts +2 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +2 -0
- package/dist/diff.d.ts +42 -5
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +103 -26
- package/dist/mcp-command.d.ts +51 -0
- package/dist/mcp-command.d.ts.map +1 -0
- package/dist/mcp-command.js +321 -0
- package/dist/resumed-session.d.ts +16 -6
- package/dist/resumed-session.d.ts.map +1 -1
- package/dist/resumed-session.js +91 -25
- package/dist/session-mode.d.ts +14 -0
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +22 -0
- package/dist/tool-calls/client-capabilities.d.ts +16 -2
- package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
- package/dist/tool-calls/client-capabilities.js +11 -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 +18 -14
- package/dist/turn-events.d.ts +71 -0
- package/dist/turn-events.d.ts.map +1 -0
- package/dist/turn-events.js +1 -0
- package/dist/usage-markdown.d.ts +2 -0
- package/dist/usage-markdown.d.ts.map +1 -1
- package/dist/usage-markdown.js +2 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +28 -0
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +27 -5
- package/dist/v2/prompt.d.ts +47 -0
- package/dist/v2/prompt.d.ts.map +1 -0
- package/dist/v2/prompt.js +153 -0
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +14 -0
- package/dist/v2/tool-call.d.ts +32 -0
- package/dist/v2/tool-call.d.ts.map +1 -0
- package/dist/v2/tool-call.js +42 -0
- package/package.json +2 -2
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_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.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";
|
|
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,9 +16,9 @@ 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 { readResumedSession, readResumedTail, } from "./resumed-session.js";
|
|
20
20
|
import { SessionTiming } from "./session-timing.js";
|
|
21
|
-
import { ALLOW_BYPASS
|
|
21
|
+
import { ALLOW_BYPASS } from "./permissions/modes.js";
|
|
22
22
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
23
23
|
import { buildClaudePermissionOptions } from "./permissions/options.js";
|
|
24
24
|
import { buildClaudePermissionPresentation } from "./permissions/presentation.js";
|
|
@@ -36,11 +36,13 @@ 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 {
|
|
39
|
+
import { resolveSkillPath } from "./tool-calls/reporters/interaction.js";
|
|
40
|
+
import { nodeToWebReadable, nodeToWebWritable, Pushable, raceTimeoutAndAbort, unreachable, } from "./utils.js";
|
|
40
41
|
import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
41
42
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
42
43
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
43
|
-
import {
|
|
44
|
+
import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
|
|
45
|
+
import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
|
|
44
46
|
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
45
47
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
46
48
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
@@ -105,32 +107,18 @@ function settleLiveBackgroundTask(session, taskId) {
|
|
|
105
107
|
async function structuredUsageMarkdown(query, signal, logger) {
|
|
106
108
|
if (signal.aborted)
|
|
107
109
|
return null;
|
|
108
|
-
let timeout;
|
|
109
|
-
let onAbort;
|
|
110
110
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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) {
|
|
111
|
+
const outcome = await raceTimeoutAndAbort(
|
|
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(), STRUCTURED_USAGE_TIMEOUT_MS, signal);
|
|
115
|
+
if (outcome.type !== "done") {
|
|
116
|
+
if (outcome.type === "timeout") {
|
|
129
117
|
logger.error("Structured /usage timed out; preserving Claude Code output");
|
|
130
118
|
}
|
|
131
119
|
return null;
|
|
132
120
|
}
|
|
133
|
-
const usage = parseUsageResponse(
|
|
121
|
+
const usage = parseUsageResponse(outcome.value);
|
|
134
122
|
if (!usage) {
|
|
135
123
|
logger.error("Structured /usage returned an incompatible response; preserving Claude Code output");
|
|
136
124
|
return null;
|
|
@@ -141,12 +129,6 @@ async function structuredUsageMarkdown(query, signal, logger) {
|
|
|
141
129
|
logger.error(`Structured /usage failed; preserving Claude Code output: ${error}`);
|
|
142
130
|
return null;
|
|
143
131
|
}
|
|
144
|
-
finally {
|
|
145
|
-
if (timeout)
|
|
146
|
-
clearTimeout(timeout);
|
|
147
|
-
if (onAbort)
|
|
148
|
-
signal.removeEventListener("abort", onAbort);
|
|
149
|
-
}
|
|
150
132
|
}
|
|
151
133
|
/** Claude Code keeps the OAuth callback listener open in the background after
|
|
152
134
|
* `mcpAuthenticate` returns the authorization URL. The SDK does not expose
|
|
@@ -680,6 +662,7 @@ const REPLAY_HIDDEN_COMMANDS = new Set([
|
|
|
680
662
|
...LOCAL_ONLY_COMMANDS,
|
|
681
663
|
"/compact",
|
|
682
664
|
"/model",
|
|
665
|
+
"/mcp",
|
|
683
666
|
"/status",
|
|
684
667
|
"/usage",
|
|
685
668
|
]);
|
|
@@ -1017,12 +1000,19 @@ const mcpAuthentications = new WeakMap();
|
|
|
1017
1000
|
* authentication. This runs after session creation has returned so an
|
|
1018
1001
|
* interactive browser flow never delays `session/new`. */
|
|
1019
1002
|
function startMcpAuthentication(host, sessionId, mcpServers) {
|
|
1020
|
-
if (
|
|
1003
|
+
if (mcpServers.length === 0)
|
|
1004
|
+
return;
|
|
1005
|
+
startMcpServerAuthentication(host, sessionId, new Set(mcpServers.map((server) => server.name)));
|
|
1006
|
+
}
|
|
1007
|
+
/** Start OAuth in the background for the servers in `requestedServers` that
|
|
1008
|
+
* Claude reports as needing authentication. Startup and `/mcp` use it. It
|
|
1009
|
+
* does nothing while another OAuth run of the session is in progress. */
|
|
1010
|
+
function startMcpServerAuthentication(host, sessionId, requestedServers) {
|
|
1011
|
+
if (!host.clientCapabilities?.elicitation?.url)
|
|
1021
1012
|
return;
|
|
1022
1013
|
const session = host.sessions[sessionId];
|
|
1023
1014
|
if (!session || mcpAuthentications.has(session))
|
|
1024
1015
|
return;
|
|
1025
|
-
const requestedServers = new Set(mcpServers.map((server) => server.name));
|
|
1026
1016
|
const authentication = authenticateMcpServers(host, sessionId, session.query, requestedServers)
|
|
1027
1017
|
.catch((error) => {
|
|
1028
1018
|
if (!session.abortController.signal.aborted) {
|
|
@@ -1161,7 +1151,14 @@ export class ClaudeAcpAgent {
|
|
|
1161
1151
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
1162
1152
|
* tests can shrink it. */
|
|
1163
1153
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
1164
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
|
|
1156
|
+
* types, except for the tool call content that v2 needs and v1 cannot
|
|
1157
|
+
* express: see {@link ToolCallClientCapabilities.v2}.
|
|
1158
|
+
*/
|
|
1159
|
+
v2;
|
|
1160
|
+
constructor(client, logger, options = {}) {
|
|
1161
|
+
this.v2 = options.v2 ?? false;
|
|
1165
1162
|
this.sessions = {};
|
|
1166
1163
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1167
1164
|
this.logger = logger ?? console;
|
|
@@ -1218,7 +1215,9 @@ export class ClaudeAcpAgent {
|
|
|
1218
1215
|
}
|
|
1219
1216
|
async initialize(request) {
|
|
1220
1217
|
this.clientCapabilities = request.clientCapabilities;
|
|
1221
|
-
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities
|
|
1218
|
+
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities, {
|
|
1219
|
+
v2: this.v2,
|
|
1220
|
+
});
|
|
1222
1221
|
// Learn the auth identity in the background: `initialize` never waits on
|
|
1223
1222
|
// the CLI probe, and no snapshot rides in its response. When the probe
|
|
1224
1223
|
// lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
|
|
@@ -1402,6 +1401,56 @@ export class ClaudeAcpAgent {
|
|
|
1402
1401
|
}, 0);
|
|
1403
1402
|
return response;
|
|
1404
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Start the CLI of a loaded session, and replay the history while the CLI
|
|
1406
|
+
* starts. The CLI start takes most of the load time, and the replay needs
|
|
1407
|
+
* only the transcript and the {@link ReplayState}. An invalid `cwd` fails
|
|
1408
|
+
* before the first update. A failed CLI start stops the replay.
|
|
1409
|
+
*/
|
|
1410
|
+
async createSessionWhileReplaying(params, timing) {
|
|
1411
|
+
await this.validateCwd(params.cwd);
|
|
1412
|
+
const pending = {
|
|
1413
|
+
state: {
|
|
1414
|
+
cwd: params.cwd,
|
|
1415
|
+
taskState: new Map(),
|
|
1416
|
+
forwardSubagentText: this.forwardsSubagentText(params._meta),
|
|
1417
|
+
messageIdToUuid: new Map(),
|
|
1418
|
+
sessionFailureState: createSessionFailureState(),
|
|
1419
|
+
},
|
|
1420
|
+
stopped: false,
|
|
1421
|
+
};
|
|
1422
|
+
const transcript = readResumedSession(params.sessionId, this.logger);
|
|
1423
|
+
const replay = transcript
|
|
1424
|
+
.then(({ messages }) => this.replaySessionHistory(params.sessionId, messages, pending))
|
|
1425
|
+
.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
1426
|
+
let response;
|
|
1427
|
+
try {
|
|
1428
|
+
response = await this.createSession({
|
|
1429
|
+
cwd: params.cwd,
|
|
1430
|
+
mcpServers: params.mcpServers ?? [],
|
|
1431
|
+
additionalDirectories: params.additionalDirectories,
|
|
1432
|
+
_meta: params._meta,
|
|
1433
|
+
}, {
|
|
1434
|
+
resume: params.sessionId,
|
|
1435
|
+
resumedModelHint: transcript.then(({ model }) => model),
|
|
1436
|
+
replayState: pending.state,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
pending.stopped = true;
|
|
1441
|
+
await replay;
|
|
1442
|
+
throw error;
|
|
1443
|
+
}
|
|
1444
|
+
timing.phase("session-ready");
|
|
1445
|
+
const replayed = await replay;
|
|
1446
|
+
if (!replayed.ok)
|
|
1447
|
+
throw replayed.error;
|
|
1448
|
+
return {
|
|
1449
|
+
sessionId: response.sessionId,
|
|
1450
|
+
modes: response.modes,
|
|
1451
|
+
configOptions: response.configOptions,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1405
1454
|
async unstable_forkSession(params) {
|
|
1406
1455
|
if (this.providerUpdate)
|
|
1407
1456
|
await this.providerUpdate;
|
|
@@ -1426,10 +1475,16 @@ export class ClaudeAcpAgent {
|
|
|
1426
1475
|
const timing = new SessionTiming(this.logger, "load", params.sessionId);
|
|
1427
1476
|
if (this.providerUpdate)
|
|
1428
1477
|
await this.providerUpdate;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1478
|
+
let result;
|
|
1479
|
+
if (this.sessions[params.sessionId]) {
|
|
1480
|
+
const resumedSession = await readResumedSession(params.sessionId, this.logger);
|
|
1481
|
+
result = await this.getOrCreateSession(params, resumedSession);
|
|
1482
|
+
timing.phase("session-ready");
|
|
1483
|
+
await this.replaySessionHistory(params.sessionId, resumedSession.messages);
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
result = await this.createSessionWhileReplaying(params, timing);
|
|
1487
|
+
}
|
|
1433
1488
|
timing.phase("replay");
|
|
1434
1489
|
// Send available commands after replay so it doesn't interleave with history
|
|
1435
1490
|
setTimeout(() => {
|
|
@@ -1812,7 +1867,72 @@ export class ClaudeAcpAgent {
|
|
|
1812
1867
|
this.setAuthStatus(notLoggedInAuthStatus());
|
|
1813
1868
|
}
|
|
1814
1869
|
}
|
|
1870
|
+
/** The structured replacement for a prompt that is exactly `/usage` or a
|
|
1871
|
+
* `/mcp` command. Claude Code runs the command, and the replacement takes
|
|
1872
|
+
* the place of its text. Claude Code refuses a `/mcp` reconnect, enable,
|
|
1873
|
+
* or disable in SDK mode. The `/mcp` producer runs them through the SDK
|
|
1874
|
+
* control API when the text arrives, so the action stays in the turn. */
|
|
1875
|
+
localCommandMarkdown(params) {
|
|
1876
|
+
const text = params.prompt.length === 1 && params.prompt[0]?.type === "text"
|
|
1877
|
+
? params.prompt[0].text
|
|
1878
|
+
: undefined;
|
|
1879
|
+
if (text === undefined)
|
|
1880
|
+
return undefined;
|
|
1881
|
+
if (isUsageCommandText(text)) {
|
|
1882
|
+
return {
|
|
1883
|
+
startsAtActivation: true,
|
|
1884
|
+
produce: (query, signal) => structuredUsageMarkdown(query, signal, this.logger),
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
const command = parseMcpCommand(text);
|
|
1888
|
+
if (!command)
|
|
1889
|
+
return undefined;
|
|
1890
|
+
let reconnected = [];
|
|
1891
|
+
return {
|
|
1892
|
+
startsAtActivation: false,
|
|
1893
|
+
produce: async (query, signal, originalOutput) => {
|
|
1894
|
+
const outcome = await runMcpCommand(query, command, originalOutput, signal, (message) => this.logger.error(message));
|
|
1895
|
+
reconnected = outcome.reconnected;
|
|
1896
|
+
return outcome.markdown;
|
|
1897
|
+
},
|
|
1898
|
+
// A reconnect starts OAuth for the ACP servers that it tried, as the
|
|
1899
|
+
// session start does. A plain `/mcp` and a cancelled turn start none.
|
|
1900
|
+
afterDelivery: () => {
|
|
1901
|
+
const session = this.sessions[params.sessionId];
|
|
1902
|
+
if (!session || session.cancelled)
|
|
1903
|
+
return;
|
|
1904
|
+
const acpServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
|
|
1905
|
+
const servers = new Set(reconnected.filter((server) => acpServers.has(server)));
|
|
1906
|
+
if (servers.size > 0)
|
|
1907
|
+
startMcpServerAuthentication(this, params.sessionId, servers);
|
|
1908
|
+
},
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
|
|
1913
|
+
* outcome of a turn is field for field a v1 prompt response.
|
|
1914
|
+
*/
|
|
1815
1915
|
async prompt(params) {
|
|
1916
|
+
let events;
|
|
1917
|
+
const outcome = new Promise((resolve, reject) => {
|
|
1918
|
+
events = {
|
|
1919
|
+
inserted() { },
|
|
1920
|
+
awaitingUser() { },
|
|
1921
|
+
resumed() { },
|
|
1922
|
+
ended: resolve,
|
|
1923
|
+
failed: reject,
|
|
1924
|
+
};
|
|
1925
|
+
});
|
|
1926
|
+
await this.startTurn(params, events);
|
|
1927
|
+
return outcome;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Starts a turn for a prompt. Resolves once the prompt is queued for Claude
|
|
1931
|
+
* Code, and rejects only when the prompt is refused before that. From then
|
|
1932
|
+
* on, `events` reports how the turn goes (see {@link TurnEvents}), possibly
|
|
1933
|
+
* before this resolves.
|
|
1934
|
+
*/
|
|
1935
|
+
async startTurn(params, events) {
|
|
1816
1936
|
if (this.providerUpdate)
|
|
1817
1937
|
await this.providerUpdate;
|
|
1818
1938
|
let session = this.sessions[params.sessionId];
|
|
@@ -1854,45 +1974,41 @@ export class ClaudeAcpAgent {
|
|
|
1854
1974
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
1855
1975
|
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
1856
1976
|
const fileChangeReport = session.fileChangeReporter?.request(params._meta);
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
isUsageCommandText(params.prompt[0].text);
|
|
1860
|
-
const usageMarkdownAbort = isUsageCommand ? new AbortController() : undefined;
|
|
1977
|
+
const localCommand = this.localCommandMarkdown(params);
|
|
1978
|
+
const localCommandAbort = localCommand ? new AbortController() : undefined;
|
|
1861
1979
|
session.titles.onPrompt(params.prompt);
|
|
1862
|
-
// Each prompt is a Turn
|
|
1863
|
-
//
|
|
1864
|
-
// turn, pushes the user message onto the streaming input, makes sure
|
|
1865
|
-
// consumer is running
|
|
1980
|
+
// Each prompt is a Turn that the persistent consumer settles once the
|
|
1981
|
+
// turn's outcome is known. `startTurn()` owns no loop: it enqueues the
|
|
1982
|
+
// turn, pushes the user message onto the streaming input, and makes sure
|
|
1983
|
+
// the consumer is running.
|
|
1984
|
+
let completeTurn;
|
|
1866
1985
|
const turn = {
|
|
1867
1986
|
promptUuid,
|
|
1987
|
+
events,
|
|
1868
1988
|
isLocalOnlyCommand,
|
|
1869
|
-
...(
|
|
1870
|
-
...(
|
|
1989
|
+
...(localCommand ? { localCommand } : {}),
|
|
1990
|
+
...(localCommandAbort ? { localCommandAbort } : {}),
|
|
1871
1991
|
...(fileChangeReport ? { fileChangeReport } : {}),
|
|
1872
1992
|
settled: false,
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
completeTurn = resolve;
|
|
1879
|
-
});
|
|
1880
|
-
const response = new Promise((resolve, reject) => {
|
|
1881
|
-
turn.resolve = (result) => {
|
|
1882
|
-
resolve(result);
|
|
1993
|
+
completion: new Promise((resolve) => {
|
|
1994
|
+
completeTurn = resolve;
|
|
1995
|
+
}),
|
|
1996
|
+
resolve: (outcome) => {
|
|
1997
|
+
events.ended(outcome);
|
|
1883
1998
|
completeTurn();
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1999
|
+
},
|
|
2000
|
+
reject: (error, title) => {
|
|
2001
|
+
events.failed(error, title);
|
|
1887
2002
|
completeTurn();
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
2003
|
+
},
|
|
2004
|
+
};
|
|
1890
2005
|
session.turnQueue ??= [];
|
|
1891
2006
|
session.turnQueue.push(turn);
|
|
1892
2007
|
session.input.push(userMessage);
|
|
1893
2008
|
this.ensureConsumer(session, params.sessionId);
|
|
1894
|
-
|
|
1895
|
-
|
|
2009
|
+
// The prompt is queued, so its turn goes ahead even if the client misses
|
|
2010
|
+
// the goal it sets; the turn's events report the outcome.
|
|
2011
|
+
await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid).catch((error) => this.logger.error(`Session ${params.sessionId}: failed to publish the prompt's goal:`, error));
|
|
1896
2012
|
}
|
|
1897
2013
|
/** `--hide-claude-auth` applies only to the CLI's own login. A provider
|
|
1898
2014
|
* override (`providers/set` or gateway `authenticate`) routes traffic away
|
|
@@ -1993,7 +2109,10 @@ export class ClaudeAcpAgent {
|
|
|
1993
2109
|
try {
|
|
1994
2110
|
// `resume` names the Claude session, which shares the ACP session id, so
|
|
1995
2111
|
// the new query continues the same conversation under the same id.
|
|
1996
|
-
await this.createSession(creationParams, {
|
|
2112
|
+
await this.createSession(creationParams, {
|
|
2113
|
+
resume: sessionId,
|
|
2114
|
+
permissionMode: session.modes.currentModeId,
|
|
2115
|
+
});
|
|
1997
2116
|
}
|
|
1998
2117
|
catch (error) {
|
|
1999
2118
|
if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
|
|
@@ -2003,7 +2122,10 @@ export class ClaudeAcpAgent {
|
|
|
2003
2122
|
// better answer than a session the client can never use again.
|
|
2004
2123
|
this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
|
|
2005
2124
|
try {
|
|
2006
|
-
await this.createSession(creationParams, {
|
|
2125
|
+
await this.createSession(creationParams, {
|
|
2126
|
+
reuseSessionId: sessionId,
|
|
2127
|
+
permissionMode: session.modes.currentModeId,
|
|
2128
|
+
});
|
|
2007
2129
|
return;
|
|
2008
2130
|
}
|
|
2009
2131
|
catch (fallbackError) {
|
|
@@ -2465,11 +2587,11 @@ export class ClaudeAcpAgent {
|
|
|
2465
2587
|
};
|
|
2466
2588
|
};
|
|
2467
2589
|
const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
|
|
2468
|
-
const
|
|
2469
|
-
if (!turn.
|
|
2590
|
+
const ensureLocalCommandMarkdown = (turn, originalOutput = "") => {
|
|
2591
|
+
if (!turn.localCommand || !turn.localCommandAbort)
|
|
2470
2592
|
return undefined;
|
|
2471
|
-
turn.
|
|
2472
|
-
return turn.
|
|
2593
|
+
turn.localCommandMarkdown ??= turn.localCommand.produce(session.query, turn.localCommandAbort.signal, originalOutput);
|
|
2594
|
+
return turn.localCommandMarkdown;
|
|
2473
2595
|
};
|
|
2474
2596
|
const resetTurnScratch = () => {
|
|
2475
2597
|
lastAssistantTotalUsage = null;
|
|
@@ -2512,7 +2634,8 @@ export class ClaudeAcpAgent {
|
|
|
2512
2634
|
session.activeTurn = turn;
|
|
2513
2635
|
session.cancelled = false;
|
|
2514
2636
|
compaction.resume();
|
|
2515
|
-
|
|
2637
|
+
if (turn.localCommand?.startsAtActivation)
|
|
2638
|
+
ensureLocalCommandMarkdown(turn);
|
|
2516
2639
|
session.pendingOrphanResults = 0;
|
|
2517
2640
|
session.orphanCommands?.clear();
|
|
2518
2641
|
// Two-phase sweep of registry entries the level signal ended (see
|
|
@@ -2540,6 +2663,22 @@ export class ClaudeAcpAgent {
|
|
|
2540
2663
|
}
|
|
2541
2664
|
}
|
|
2542
2665
|
resetTurnScratch();
|
|
2666
|
+
// Without a command_lifecycle "started" frame before it (see that
|
|
2667
|
+
// handler), activation is the first sign that Claude Code took the
|
|
2668
|
+
// prompt in: its echo, or for an echo-less command, its result.
|
|
2669
|
+
reportInserted(turn);
|
|
2670
|
+
// A request can already be open: Claude Code asks for a queued prompt
|
|
2671
|
+
// before the consumer reaches its echo.
|
|
2672
|
+
this.syncAwaitingUser(session);
|
|
2673
|
+
};
|
|
2674
|
+
/** Report that Claude Code took the turn's prompt in, once. A turn can be
|
|
2675
|
+
* settled by now: an echo hand-off awaits settling the previous turn, and
|
|
2676
|
+
* a cancel in that window settles the queued one. */
|
|
2677
|
+
const reportInserted = (turn) => {
|
|
2678
|
+
if (turn.insertedReported || turn.settled)
|
|
2679
|
+
return;
|
|
2680
|
+
turn.insertedReported = true;
|
|
2681
|
+
turn.events.inserted(turn.promptUuid);
|
|
2543
2682
|
};
|
|
2544
2683
|
/** Ensure there is an active turn before a user-turn result that carries no
|
|
2545
2684
|
* echo to activate it, by promoting the queue head. Most turns are
|
|
@@ -2699,27 +2838,28 @@ export class ClaudeAcpAgent {
|
|
|
2699
2838
|
* the autonomous stretch-close guard. */
|
|
2700
2839
|
const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
|
|
2701
2840
|
/** Claim the structured replacement for the turn currently producing a
|
|
2702
|
-
* local-command output. Undefined means this is not a structured
|
|
2703
|
-
* turn (or its request failed), null means another SDK
|
|
2704
|
-
* already delivered it, and string is the one replacement
|
|
2705
|
-
|
|
2841
|
+
* local-command output. Undefined means this is not a structured
|
|
2842
|
+
* local-command turn (or its request failed), null means another SDK
|
|
2843
|
+
* message shape already delivered it, and string is the one replacement
|
|
2844
|
+
* to publish. */
|
|
2845
|
+
const takeLocalCommandMarkdown = async (originalOutput) => {
|
|
2706
2846
|
const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
2707
2847
|
if (!turn)
|
|
2708
2848
|
return undefined;
|
|
2709
|
-
const
|
|
2710
|
-
if (!
|
|
2849
|
+
const pending = ensureLocalCommandMarkdown(turn, originalOutput);
|
|
2850
|
+
if (!pending)
|
|
2711
2851
|
return undefined;
|
|
2712
|
-
const markdown = await
|
|
2852
|
+
const markdown = await pending;
|
|
2713
2853
|
if (markdown === null)
|
|
2714
2854
|
return undefined;
|
|
2715
|
-
if (turn.
|
|
2855
|
+
if (turn.localCommandDelivered) {
|
|
2716
2856
|
// Different SDK message shapes can mirror the same local-command
|
|
2717
2857
|
// output. Suppress an exact mirror, but let a later, distinct frame
|
|
2718
2858
|
// (for example an interruption diagnostic) follow the normal path.
|
|
2719
|
-
return turn.
|
|
2859
|
+
return turn.localCommandOriginalOutput === originalOutput ? null : undefined;
|
|
2720
2860
|
}
|
|
2721
|
-
turn.
|
|
2722
|
-
turn.
|
|
2861
|
+
turn.localCommandDelivered = true;
|
|
2862
|
+
turn.localCommandOriginalOutput = originalOutput;
|
|
2723
2863
|
return markdown;
|
|
2724
2864
|
};
|
|
2725
2865
|
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
@@ -2872,7 +3012,9 @@ export class ClaudeAcpAgent {
|
|
|
2872
3012
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
2873
3013
|
const wasHeld = isHeldOpen(turn);
|
|
2874
3014
|
turn.settled = true;
|
|
2875
|
-
turn.
|
|
3015
|
+
turn.localCommandAbort?.abort();
|
|
3016
|
+
if (turn.localCommandDelivered)
|
|
3017
|
+
turn.localCommand?.afterDelivery?.();
|
|
2876
3018
|
disarmForceCancel(session);
|
|
2877
3019
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
2878
3020
|
session.activeTurn = null;
|
|
@@ -2896,7 +3038,7 @@ export class ClaudeAcpAgent {
|
|
|
2896
3038
|
};
|
|
2897
3039
|
/** Reject the active turn (auth required, error result, …) without tearing
|
|
2898
3040
|
* down the consumer: the stream continues to idle and later turns proceed. */
|
|
2899
|
-
const failActive = (error) => {
|
|
3041
|
+
const failActive = (error, title) => {
|
|
2900
3042
|
disarmForceCancel(session);
|
|
2901
3043
|
const turn = session.activeTurn;
|
|
2902
3044
|
if (!turn || turn.settled) {
|
|
@@ -2913,7 +3055,7 @@ export class ClaudeAcpAgent {
|
|
|
2913
3055
|
// start the next stretch clean, or its stale delivery record would
|
|
2914
3056
|
// suppress the next turn's issue-#453 result-text fallback.
|
|
2915
3057
|
session.emittedAssistantText = false;
|
|
2916
|
-
turn.reject(error);
|
|
3058
|
+
turn.reject(error, title);
|
|
2917
3059
|
};
|
|
2918
3060
|
/** Complete a negotiated terminal failure on the prompt response itself,
|
|
2919
3061
|
* which is the canonical AIR carrier. Legacy clients keep the historical
|
|
@@ -2937,7 +3079,7 @@ export class ClaudeAcpAgent {
|
|
|
2937
3079
|
return;
|
|
2938
3080
|
}
|
|
2939
3081
|
if (!supportsAirSessionFailures(this.clientCapabilities)) {
|
|
2940
|
-
failActive(error);
|
|
3082
|
+
failActive(error, title);
|
|
2941
3083
|
return;
|
|
2942
3084
|
}
|
|
2943
3085
|
if (!session.activeTurn || session.activeTurn.settled) {
|
|
@@ -2947,7 +3089,7 @@ export class ClaudeAcpAgent {
|
|
|
2947
3089
|
}
|
|
2948
3090
|
const failure = await createSessionFailure(kind, { title });
|
|
2949
3091
|
if (!failure) {
|
|
2950
|
-
failActive(error);
|
|
3092
|
+
failActive(error, title);
|
|
2951
3093
|
return;
|
|
2952
3094
|
}
|
|
2953
3095
|
sessionFailures.recordActive(failure);
|
|
@@ -3051,7 +3193,7 @@ export class ClaudeAcpAgent {
|
|
|
3051
3193
|
}
|
|
3052
3194
|
}
|
|
3053
3195
|
await compaction.interrupt();
|
|
3054
|
-
await settleActive(
|
|
3196
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3055
3197
|
// The cancelled turn's result may never come (that's why the
|
|
3056
3198
|
// backstop fired) — close its delivery stretch here so partial
|
|
3057
3199
|
// streamed text can't suppress the next turn's issue-#453 fallback.
|
|
@@ -3107,7 +3249,7 @@ export class ClaudeAcpAgent {
|
|
|
3107
3249
|
const inFlight = session.activeTurn;
|
|
3108
3250
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
3109
3251
|
await settleActive(session.cancelled
|
|
3110
|
-
?
|
|
3252
|
+
? cancelledOutcome(session, inFlight)
|
|
3111
3253
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
3112
3254
|
// Queued turns the SDK never started never ran, so reject them rather
|
|
3113
3255
|
// than reporting a success (end_turn) — or a misleading "cancelled" —
|
|
@@ -3142,10 +3284,10 @@ export class ClaudeAcpAgent {
|
|
|
3142
3284
|
// prompt() stamps a uuid on every message. The frame is @internal and
|
|
3143
3285
|
// absent from the SDKMessage union, so handle it BEFORE the exhaustive
|
|
3144
3286
|
// switch: it must not reach `unreachable`'s error log, and a `case`
|
|
3145
|
-
// for it wouldn't typecheck. It feeds
|
|
3146
|
-
// Session.orphanCommands)
|
|
3147
|
-
// echoes/results/idle.
|
|
3148
|
-
// frames.)
|
|
3287
|
+
// for it wouldn't typecheck. It feeds the orphan accounting (see
|
|
3288
|
+
// Session.orphanCommands) and reports a turn inserted at "started";
|
|
3289
|
+
// turn activation and settlement stay driven by echoes/results/idle.
|
|
3290
|
+
// (Raw-mode emission above still forwards these frames.)
|
|
3149
3291
|
if (message.type === "command_lifecycle") {
|
|
3150
3292
|
const frame = message;
|
|
3151
3293
|
switch (frame.state) {
|
|
@@ -3156,6 +3298,17 @@ export class ClaudeAcpAgent {
|
|
|
3156
3298
|
if (queued) {
|
|
3157
3299
|
queued.commandStarted = true;
|
|
3158
3300
|
compaction.resume();
|
|
3301
|
+
// Claude Code took the prompt in: it drained into a turn. A
|
|
3302
|
+
// fresh turn reports that before anything it produces, its echo
|
|
3303
|
+
// and first stream events included, so this is the earliest
|
|
3304
|
+
// insertion point. (A prompt folded into a running turn reports
|
|
3305
|
+
// it after its echo, which already activated it.) While another
|
|
3306
|
+
// turn is still active (held for background work, steered, or
|
|
3307
|
+
// cancelled and awaiting its trailing idle), insertion waits
|
|
3308
|
+
// for this turn's echo, whose hand-off ends that turn first.
|
|
3309
|
+
const active = session.activeTurn;
|
|
3310
|
+
if (!active || active.settled || active === queued)
|
|
3311
|
+
reportInserted(queued);
|
|
3159
3312
|
}
|
|
3160
3313
|
// ...and promote an already-orphaned command: once dispatched,
|
|
3161
3314
|
// a bare `cancelled` no longer means "dropped without running".
|
|
@@ -3332,17 +3485,18 @@ export class ClaudeAcpAgent {
|
|
|
3332
3485
|
if (compaction.consumeDuplicateErrorOutput(message.content)) {
|
|
3333
3486
|
break;
|
|
3334
3487
|
}
|
|
3335
|
-
const
|
|
3336
|
-
const
|
|
3337
|
-
if (
|
|
3488
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
3489
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.content);
|
|
3490
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
3338
3491
|
break;
|
|
3339
|
-
if (
|
|
3492
|
+
if (commandMarkdown === null)
|
|
3340
3493
|
break;
|
|
3341
3494
|
await sendUpdate({
|
|
3342
3495
|
sessionId: params.sessionId,
|
|
3343
3496
|
update: {
|
|
3344
3497
|
sessionUpdate: "agent_message_chunk",
|
|
3345
|
-
content: { type: "text", text:
|
|
3498
|
+
content: { type: "text", text: commandMarkdown ?? message.content },
|
|
3499
|
+
messageId: messageIdForGrouping(message),
|
|
3346
3500
|
},
|
|
3347
3501
|
});
|
|
3348
3502
|
break;
|
|
@@ -3399,7 +3553,13 @@ export class ClaudeAcpAgent {
|
|
|
3399
3553
|
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
3400
3554
|
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
3401
3555
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
3402
|
-
|
|
3556
|
+
// A held turn's result passed long ago; this idle is the
|
|
3557
|
+
// trailer of the followup cycle the cancel interrupted,
|
|
3558
|
+
// whose result (an autonomous one) recorded it as owed.
|
|
3559
|
+
if (isHeldOpen(session.activeTurn) && session.owedTrailingIdles > 0) {
|
|
3560
|
+
session.owedTrailingIdles--;
|
|
3561
|
+
}
|
|
3562
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3403
3563
|
// An interrupt can pre-empt the turn's result entirely
|
|
3404
3564
|
// (nothing ran the result-case `finally`), so close the
|
|
3405
3565
|
// delivery stretch here: idle is the SDK's authoritative
|
|
@@ -3501,11 +3661,12 @@ export class ClaudeAcpAgent {
|
|
|
3501
3661
|
// list with this payload. Forward message.commands directly —
|
|
3502
3662
|
// it's authoritative, and re-querying supportedCommands()
|
|
3503
3663
|
// would just return the same list with an extra round-trip.
|
|
3664
|
+
session.skillPaths = new Map();
|
|
3504
3665
|
await sendUpdate({
|
|
3505
3666
|
sessionId: params.sessionId,
|
|
3506
3667
|
update: {
|
|
3507
3668
|
sessionUpdate: "available_commands_update",
|
|
3508
|
-
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
|
|
3669
|
+
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, session.skillPaths),
|
|
3509
3670
|
},
|
|
3510
3671
|
});
|
|
3511
3672
|
break;
|
|
@@ -4277,6 +4438,7 @@ export class ClaudeAcpAgent {
|
|
|
4277
4438
|
update: {
|
|
4278
4439
|
sessionUpdate: "agent_message_chunk",
|
|
4279
4440
|
content: { type: "text", text: lastRefusalExplanation },
|
|
4441
|
+
messageId: messageIdForGrouping(message),
|
|
4280
4442
|
},
|
|
4281
4443
|
});
|
|
4282
4444
|
}
|
|
@@ -4351,10 +4513,13 @@ export class ClaudeAcpAgent {
|
|
|
4351
4513
|
(message.usage.output_tokens ?? 0) === 0 &&
|
|
4352
4514
|
!session.activeTurn?.noticeTexts?.includes(normalizeNoticeText(message.result)));
|
|
4353
4515
|
if (shouldForwardResult) {
|
|
4354
|
-
const
|
|
4355
|
-
|
|
4516
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
4517
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.result);
|
|
4518
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
4519
|
+
break;
|
|
4520
|
+
if (commandMarkdown === null)
|
|
4356
4521
|
break;
|
|
4357
|
-
for (const notification of toAcpNotifications(
|
|
4522
|
+
for (const notification of toAcpNotifications(commandMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4358
4523
|
await sendUpdate(notification);
|
|
4359
4524
|
}
|
|
4360
4525
|
}
|
|
@@ -4567,6 +4732,7 @@ export class ClaudeAcpAgent {
|
|
|
4567
4732
|
}
|
|
4568
4733
|
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
4569
4734
|
clientCapabilities: this.clientCapabilities,
|
|
4735
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4570
4736
|
cwd: session.cwd,
|
|
4571
4737
|
taskState: session.taskState,
|
|
4572
4738
|
emittedToolCalls: session.emittedToolCalls,
|
|
@@ -4636,7 +4802,7 @@ export class ClaudeAcpAgent {
|
|
|
4636
4802
|
session.owedTrailingIdles++;
|
|
4637
4803
|
// Before activateTurn resets the accumulator, so the
|
|
4638
4804
|
// usage still belongs to the cancelled turn.
|
|
4639
|
-
await settleActive(
|
|
4805
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
4640
4806
|
}
|
|
4641
4807
|
else if (isHeldOpen(session.activeTurn)) {
|
|
4642
4808
|
// A turn held open for its background subagents (see
|
|
@@ -4747,16 +4913,16 @@ export class ClaudeAcpAgent {
|
|
|
4747
4913
|
// Depending on the Claude Code build, a local command can arrive
|
|
4748
4914
|
// as the dedicated system message above or as a synthetic
|
|
4749
4915
|
// assistant message. Replace only the output owned by the exact
|
|
4750
|
-
// /usage turn; no content signatures or text parsing are involved.
|
|
4916
|
+
// /usage or /mcp turn; no content signatures or text parsing are involved.
|
|
4751
4917
|
if (message.type === "assistant" &&
|
|
4752
4918
|
message.parent_tool_use_id === null &&
|
|
4753
4919
|
message.message.model === "<synthetic>") {
|
|
4754
|
-
const
|
|
4920
|
+
const commandMarkdown = await takeLocalCommandMarkdown(assistantMessageText(message.message) ?? "");
|
|
4755
4921
|
if (session.cancelled)
|
|
4756
4922
|
break;
|
|
4757
|
-
if (
|
|
4758
|
-
if (
|
|
4759
|
-
for (const notification of toAcpNotifications(
|
|
4923
|
+
if (commandMarkdown !== undefined) {
|
|
4924
|
+
if (commandMarkdown !== null) {
|
|
4925
|
+
for (const notification of toAcpNotifications(commandMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4760
4926
|
await sendUpdate(notification);
|
|
4761
4927
|
}
|
|
4762
4928
|
}
|
|
@@ -4770,6 +4936,7 @@ export class ClaudeAcpAgent {
|
|
|
4770
4936
|
if (typeof stripped === "string") {
|
|
4771
4937
|
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4772
4938
|
clientCapabilities: this.clientCapabilities,
|
|
4939
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4773
4940
|
parentToolUseId: message.parent_tool_use_id,
|
|
4774
4941
|
cwd: session.cwd,
|
|
4775
4942
|
taskState: session.taskState,
|
|
@@ -4851,6 +5018,7 @@ export class ClaudeAcpAgent {
|
|
|
4851
5018
|
}
|
|
4852
5019
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4853
5020
|
clientCapabilities: this.clientCapabilities,
|
|
5021
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4854
5022
|
parentToolUseId: message.parent_tool_use_id,
|
|
4855
5023
|
cwd: session.cwd,
|
|
4856
5024
|
taskState: session.taskState,
|
|
@@ -5040,6 +5208,36 @@ export class ClaudeAcpAgent {
|
|
|
5040
5208
|
session.pendingOrphanResults = (session.pendingOrphanResults ?? 0) + 1;
|
|
5041
5209
|
}
|
|
5042
5210
|
}
|
|
5211
|
+
/** Ask Claude Code to drop user messages it still has queued, so that a
|
|
5212
|
+
* prompt a cancel settled is not run after all. Claude Code keeps queued
|
|
5213
|
+
* messages through an interrupt and runs them next (the receipt's
|
|
5214
|
+
* `still_queued`), unless they are cancelled first with
|
|
5215
|
+
* `cancel_async_message`: a no-op for a message it already dequeued, which
|
|
5216
|
+
* then still runs, as an orphan (see `orphanCommands`).
|
|
5217
|
+
*
|
|
5218
|
+
* Not awaited: the SDK writes control requests in call order, so the drops
|
|
5219
|
+
* reach Claude Code before a request sent after them, and a wedged CLI must
|
|
5220
|
+
* not hold up the cancel. The orphan accounting needs no update here: a
|
|
5221
|
+
* dropped message gets a `cancelled` lifecycle frame and is missing from the
|
|
5222
|
+
* interrupt receipt's `still_queued`, which both already account for. The
|
|
5223
|
+
* SDK implements the request as `Query.cancelAsyncMessage` without typing
|
|
5224
|
+
* it, so it is looked up, and a CLI that lacks it rejects the request. */
|
|
5225
|
+
dropQueuedMessages(sessionId, query, uuids) {
|
|
5226
|
+
const cancelAsyncMessage = query
|
|
5227
|
+
.cancelAsyncMessage;
|
|
5228
|
+
if (typeof cancelAsyncMessage !== "function")
|
|
5229
|
+
return;
|
|
5230
|
+
const logFailure = (uuid, error) => this.logger.error(`Session ${sessionId}: could not drop the queued message ${uuid} from Claude Code:`, error);
|
|
5231
|
+
for (const uuid of uuids) {
|
|
5232
|
+
// Called synchronously: deferring it would let the interrupt go first.
|
|
5233
|
+
try {
|
|
5234
|
+
Promise.resolve(cancelAsyncMessage.call(query, uuid)).catch((error) => logFailure(uuid, error));
|
|
5235
|
+
}
|
|
5236
|
+
catch (error) {
|
|
5237
|
+
logFailure(uuid, error);
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5043
5241
|
async cancel(params) {
|
|
5044
5242
|
await this.cancelTurns(params, { awaitInterrupt: true });
|
|
5045
5243
|
}
|
|
@@ -5056,7 +5254,7 @@ export class ClaudeAcpAgent {
|
|
|
5056
5254
|
}
|
|
5057
5255
|
session.cancelled = true;
|
|
5058
5256
|
for (const turn of session.turnQueue ?? [])
|
|
5059
|
-
turn.
|
|
5257
|
+
turn.localCommandAbort?.abort();
|
|
5060
5258
|
session.pendingExitPlanModeInterruption = undefined;
|
|
5061
5259
|
session.pendingExitPlanContextReset = undefined;
|
|
5062
5260
|
// The stream already ended (see closeQueryStream): every in-flight turn was
|
|
@@ -5085,12 +5283,16 @@ export class ClaudeAcpAgent {
|
|
|
5085
5283
|
session.toolCallFields?.clear();
|
|
5086
5284
|
clearHookCallbacks(params.sessionId);
|
|
5087
5285
|
}
|
|
5286
|
+
// The user messages this cancel abandons while Claude Code may still have
|
|
5287
|
+
// them queued: they are dropped from its queue before the interrupt.
|
|
5288
|
+
const abandoned = [];
|
|
5088
5289
|
// A priority steer may still be queued in the SDK when cancellation
|
|
5089
5290
|
// settles its owning turn. Its later echo matches no live turn, and its
|
|
5090
5291
|
// result must be skipped rather than promoted onto the next prompt.
|
|
5091
5292
|
if (isSteering(session.activeTurn)) {
|
|
5092
5293
|
for (const uuid of session.activeTurn.steeredEchoes) {
|
|
5093
5294
|
this.trackOrphanCommand(session, uuid, "pending");
|
|
5295
|
+
abandoned.push(uuid);
|
|
5094
5296
|
}
|
|
5095
5297
|
}
|
|
5096
5298
|
// Capture the orphan-accounting lane before anything can await: the
|
|
@@ -5114,11 +5316,13 @@ export class ClaudeAcpAgent {
|
|
|
5114
5316
|
// accumulator (the active turn's tally) is not its spend.
|
|
5115
5317
|
turn.resolve({ stopReason: "cancelled" });
|
|
5116
5318
|
orphanedTurns.push(turn);
|
|
5319
|
+
abandoned.push(turn.promptUuid);
|
|
5117
5320
|
}
|
|
5118
5321
|
}
|
|
5119
|
-
// Each removed queued turn's user message was already pushed to the SDK
|
|
5120
|
-
//
|
|
5121
|
-
//
|
|
5322
|
+
// Each removed queued turn's user message was already pushed to the SDK.
|
|
5323
|
+
// It is dropped from Claude Code's queue below (see dropQueuedMessages),
|
|
5324
|
+
// but one Claude Code already dequeued still runs, FIFO, and emits a
|
|
5325
|
+
// result with no user echo to match (0.3.246+ CLIs do stamp results with the
|
|
5122
5326
|
// triggering send's user_message_uuid, which ensureActiveTurn uses as
|
|
5123
5327
|
// an exact join when present). Track those so the consumer skips them
|
|
5124
5328
|
// (see ensureActiveTurn) rather than misattributing them to the head.
|
|
@@ -5181,15 +5385,23 @@ export class ClaudeAcpAgent {
|
|
|
5181
5385
|
}
|
|
5182
5386
|
// A deferred active turn (see Turn.deferredSettle) already has its result
|
|
5183
5387
|
// and is only held open for subagents. A settling turn likewise has its
|
|
5184
|
-
// result but is awaiting the bounded checkpoint preview. Settle
|
|
5185
|
-
// "cancelled" NOW
|
|
5186
|
-
// idle while blocked in that preview, and a held turn
|
|
5187
|
-
//
|
|
5188
|
-
//
|
|
5388
|
+
// result but is awaiting the bounded checkpoint preview. Settle it
|
|
5389
|
+
// "cancelled" NOW where the consumer cannot: it cannot process the
|
|
5390
|
+
// interrupt's trailing idle while blocked in that preview, and a held turn
|
|
5391
|
+
// whose session sits idle gets no trailing idle (the interrupt emits
|
|
5392
|
+
// nothing; with no state events consumed yet, the same is assumed).
|
|
5393
|
+
// A held turn whose followup cycle is live (running, or blocked on a
|
|
5394
|
+
// permission request) is left to that idle, like any live turn: the
|
|
5395
|
+
// interrupt still flushes the cycle's output, which must reach the client
|
|
5396
|
+
// before the turn ends cancelled. The captured outcome preserves usage and
|
|
5397
|
+
// metadata (issue #844), while reporter state makes a late checkpoint
|
|
5398
|
+
// response harmless.
|
|
5189
5399
|
{
|
|
5190
5400
|
const active = session.activeTurn;
|
|
5191
|
-
const
|
|
5192
|
-
if (active &&
|
|
5401
|
+
const followupLive = session.lastSessionState === "running" || session.lastSessionState === "requires_action";
|
|
5402
|
+
if (active &&
|
|
5403
|
+
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5404
|
+
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5193
5405
|
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
5194
5406
|
active.settled = true;
|
|
5195
5407
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
@@ -5203,17 +5415,16 @@ export class ClaudeAcpAgent {
|
|
|
5203
5415
|
// text since the last boundary was its followups', and left latched
|
|
5204
5416
|
// it would suppress a following replayed turn's issue-#453 fallback.
|
|
5205
5417
|
session.emittedAssistantText = false;
|
|
5206
|
-
// When the interrupt below pre-empts a live cycle
|
|
5207
|
-
//
|
|
5208
|
-
//
|
|
5209
|
-
//
|
|
5210
|
-
//
|
|
5211
|
-
//
|
|
5212
|
-
//
|
|
5213
|
-
//
|
|
5214
|
-
//
|
|
5215
|
-
//
|
|
5216
|
-
// running transition still in the backlog reads as stale idle and
|
|
5418
|
+
// When the interrupt below pre-empts a live cycle (a settling turn's,
|
|
5419
|
+
// or one no state event reported), it produces a trailer idle with no
|
|
5420
|
+
// counted result; that idle would be un-owed and could lag past the
|
|
5421
|
+
// next prompt's echo — read as the fresh turn ending without a result
|
|
5422
|
+
// (issue #825 false-fail).
|
|
5423
|
+
// Pre-count it unless the session sits idle: there the interrupt
|
|
5424
|
+
// emits nothing, and a debt that never drains would mask one future
|
|
5425
|
+
// #825 detection. (lastSessionState is last-CONSUMED, so both stale
|
|
5426
|
+
// reads exist and both are accepted one-cycle windows: a running
|
|
5427
|
+
// transition still in the backlog reads as stale idle and
|
|
5217
5428
|
// under-counts — that false-fail additionally needs the trailer to
|
|
5218
5429
|
// lag past the next echo — while a cycle already completed into the
|
|
5219
5430
|
// backlog reads as stale non-idle and over-counts, masking one
|
|
@@ -5223,14 +5434,7 @@ export class ClaudeAcpAgent {
|
|
|
5223
5434
|
if (session.lastSessionState !== "idle") {
|
|
5224
5435
|
session.owedTrailingIdles++;
|
|
5225
5436
|
}
|
|
5226
|
-
|
|
5227
|
-
// metadata is its quota breakdown, the counterpart of the usage taken
|
|
5228
|
-
// from it here.
|
|
5229
|
-
active.resolve({
|
|
5230
|
-
stopReason: "cancelled",
|
|
5231
|
-
usage: pendingOutcome.usage,
|
|
5232
|
-
...(pendingOutcome._meta ? { _meta: pendingOutcome._meta } : {}),
|
|
5233
|
-
});
|
|
5437
|
+
active.resolve(cancelledOutcome(session, active));
|
|
5234
5438
|
}
|
|
5235
5439
|
}
|
|
5236
5440
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -5263,6 +5467,9 @@ export class ClaudeAcpAgent {
|
|
|
5263
5467
|
cancelController.abort();
|
|
5264
5468
|
}, this.forceCancelGraceMs);
|
|
5265
5469
|
}
|
|
5470
|
+
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
5471
|
+
// the next queued message right away.
|
|
5472
|
+
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
5266
5473
|
const interrupt = session.query.interrupt();
|
|
5267
5474
|
if (!options.awaitInterrupt) {
|
|
5268
5475
|
// The caller closes the query next, which rejects the pending reply.
|
|
@@ -5498,13 +5705,15 @@ export class ClaudeAcpAgent {
|
|
|
5498
5705
|
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
|
|
5499
5706
|
return { configOptions: session.configOptions };
|
|
5500
5707
|
}
|
|
5501
|
-
async replaySessionHistory(sessionId, resumedMessages) {
|
|
5708
|
+
async replaySessionHistory(sessionId, resumedMessages, pending) {
|
|
5502
5709
|
const replayStartedAt = performance.now();
|
|
5503
5710
|
const toolUseCache = {};
|
|
5504
5711
|
const messages = resumedMessages ?? (await getSessionMessages(sessionId));
|
|
5505
5712
|
const historyLoadedAt = performance.now();
|
|
5506
5713
|
this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
|
|
5507
|
-
|
|
5714
|
+
// A pending replay has no session record yet. It uses the state that the record takes later.
|
|
5715
|
+
const replayState = () => pending?.state ?? this.sessions[sessionId];
|
|
5716
|
+
const session = replayState();
|
|
5508
5717
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
5509
5718
|
if (session?.taskState)
|
|
5510
5719
|
forgetPublishedTaskPlan(session.taskState);
|
|
@@ -5516,7 +5725,12 @@ export class ClaudeAcpAgent {
|
|
|
5516
5725
|
sessionId,
|
|
5517
5726
|
state: session.sessionFailureState,
|
|
5518
5727
|
capabilities: this.clientCapabilities,
|
|
5519
|
-
isCurrent: () =>
|
|
5728
|
+
isCurrent: () => {
|
|
5729
|
+
const live = this.sessions[sessionId];
|
|
5730
|
+
if (!live)
|
|
5731
|
+
return pending !== undefined && !pending.stopped;
|
|
5732
|
+
return live.sessionFailureState === session.sessionFailureState;
|
|
5733
|
+
},
|
|
5520
5734
|
sendUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
5521
5735
|
logger: this.logger,
|
|
5522
5736
|
})
|
|
@@ -5671,7 +5885,7 @@ export class ClaudeAcpAgent {
|
|
|
5671
5885
|
await replayAsyncTasks.taskNotification(notification);
|
|
5672
5886
|
};
|
|
5673
5887
|
const replayMessage = async (message) => {
|
|
5674
|
-
if (isReplayHiddenMetaMessage(message)) {
|
|
5888
|
+
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
5675
5889
|
return;
|
|
5676
5890
|
}
|
|
5677
5891
|
if (message.type === "user" &&
|
|
@@ -5685,7 +5899,7 @@ export class ClaudeAcpAgent {
|
|
|
5685
5899
|
// a client-supplied id without an extra getSessionMessages read. Not read
|
|
5686
5900
|
// yet (see Session.messageIdToUuid).
|
|
5687
5901
|
const replayMessageId = messageIdForGrouping(message);
|
|
5688
|
-
const replaySession =
|
|
5902
|
+
const replaySession = replayState();
|
|
5689
5903
|
if (replaySession && replayMessageId && message.uuid) {
|
|
5690
5904
|
replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
|
|
5691
5905
|
}
|
|
@@ -5779,8 +5993,9 @@ export class ClaudeAcpAgent {
|
|
|
5779
5993
|
registerHooks: false,
|
|
5780
5994
|
replay: true,
|
|
5781
5995
|
clientCapabilities: this.clientCapabilities,
|
|
5782
|
-
|
|
5783
|
-
|
|
5996
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
5997
|
+
cwd: replayState()?.cwd,
|
|
5998
|
+
taskState: replayState()?.taskState,
|
|
5784
5999
|
messageId: replayMessageId,
|
|
5785
6000
|
parentToolUseId,
|
|
5786
6001
|
})) {
|
|
@@ -5825,7 +6040,7 @@ export class ClaudeAcpAgent {
|
|
|
5825
6040
|
};
|
|
5826
6041
|
for (const message of messages)
|
|
5827
6042
|
await replayMessage(message);
|
|
5828
|
-
if (nativeReplayEnabled) {
|
|
6043
|
+
if (nativeReplayEnabled && !pending?.stopped) {
|
|
5829
6044
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
5830
6045
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
5831
6046
|
// state from the launch tool_result. Missing results and malformed or
|
|
@@ -5862,18 +6077,40 @@ export class ClaudeAcpAgent {
|
|
|
5862
6077
|
/** Mark a client request as blocking on user input for exactly the lifetime
|
|
5863
6078
|
* of its promise. Steering consults this session-local count synchronously,
|
|
5864
6079
|
* so a message arriving while any permission/elicitation card is open uses
|
|
5865
|
-
* non-interrupting SDK delivery.
|
|
6080
|
+
* non-interrupting SDK delivery. The active turn reports that it awaits the
|
|
6081
|
+
* user while any such request is open (see `syncAwaitingUser`). */
|
|
5866
6082
|
async withPendingUserInput(sessionId, request) {
|
|
5867
6083
|
const session = this.sessions[sessionId];
|
|
5868
6084
|
if (!session)
|
|
5869
6085
|
return request();
|
|
5870
6086
|
session.pendingUserInputCount = (session.pendingUserInputCount ?? 0) + 1;
|
|
6087
|
+
this.syncAwaitingUser(session);
|
|
5871
6088
|
try {
|
|
5872
6089
|
return await request();
|
|
5873
6090
|
}
|
|
5874
6091
|
finally {
|
|
5875
6092
|
session.pendingUserInputCount = Math.max(0, (session.pendingUserInputCount ?? 1) - 1);
|
|
5876
|
-
|
|
6093
|
+
this.syncAwaitingUser(session);
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
/** Report whether the session's active turn awaits the user: exactly while
|
|
6097
|
+
* a permission request or question is open in the session. A request does
|
|
6098
|
+
* not say which prompt it is for, and Claude Code can ask for a queued
|
|
6099
|
+
* prompt before the consumer activates it, so a request is not tied to the
|
|
6100
|
+
* turn that was active when it opened. Called when the count changes and
|
|
6101
|
+
* when a turn activates. */
|
|
6102
|
+
syncAwaitingUser(session) {
|
|
6103
|
+
const turn = session.activeTurn;
|
|
6104
|
+
if (!turn || turn.settled || !turn.insertedReported)
|
|
6105
|
+
return;
|
|
6106
|
+
const waiting = (session.pendingUserInputCount ?? 0) > 0;
|
|
6107
|
+
if (waiting === (turn.awaitingUser ?? false))
|
|
6108
|
+
return;
|
|
6109
|
+
turn.awaitingUser = waiting;
|
|
6110
|
+
if (waiting)
|
|
6111
|
+
turn.events.awaitingUser();
|
|
6112
|
+
else
|
|
6113
|
+
turn.events.resumed();
|
|
5877
6114
|
}
|
|
5878
6115
|
/** Forward a permission request to the client, wiring the tool call's
|
|
5879
6116
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
@@ -6237,7 +6474,7 @@ export class ClaudeAcpAgent {
|
|
|
6237
6474
|
sessionId,
|
|
6238
6475
|
update: {
|
|
6239
6476
|
sessionUpdate: "available_commands_update",
|
|
6240
|
-
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands),
|
|
6477
|
+
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, (session.skillPaths ??= new Map())),
|
|
6241
6478
|
},
|
|
6242
6479
|
});
|
|
6243
6480
|
}
|
|
@@ -6558,6 +6795,8 @@ export class ClaudeAcpAgent {
|
|
|
6558
6795
|
}
|
|
6559
6796
|
async getOrCreateSession(params, resumedSession) {
|
|
6560
6797
|
const existingSession = this.sessions[params.sessionId];
|
|
6798
|
+
// A recreated live session keeps its mode, not the mode of the transcript.
|
|
6799
|
+
const livePermissionMode = existingSession?.modes.currentModeId;
|
|
6561
6800
|
if (existingSession) {
|
|
6562
6801
|
const fingerprint = computeSessionFingerprint(params);
|
|
6563
6802
|
if (fingerprint === existingSession.sessionFingerprint) {
|
|
@@ -6573,9 +6812,6 @@ export class ClaudeAcpAgent {
|
|
|
6573
6812
|
// up the new values.
|
|
6574
6813
|
await this.teardownSession(params.sessionId);
|
|
6575
6814
|
}
|
|
6576
|
-
const resumedModelHint = resumedSession
|
|
6577
|
-
? resumedSession.model
|
|
6578
|
-
: await readResumedModel(params.sessionId, this.logger);
|
|
6579
6815
|
const response = await this.createSession({
|
|
6580
6816
|
cwd: params.cwd,
|
|
6581
6817
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -6583,7 +6819,8 @@ export class ClaudeAcpAgent {
|
|
|
6583
6819
|
_meta: params._meta,
|
|
6584
6820
|
}, {
|
|
6585
6821
|
resume: params.sessionId,
|
|
6586
|
-
|
|
6822
|
+
permissionMode: livePermissionMode,
|
|
6823
|
+
...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
|
|
6587
6824
|
});
|
|
6588
6825
|
return {
|
|
6589
6826
|
sessionId: response.sessionId,
|
|
@@ -6612,6 +6849,12 @@ export class ClaudeAcpAgent {
|
|
|
6612
6849
|
throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
|
|
6613
6850
|
}
|
|
6614
6851
|
}
|
|
6852
|
+
/** Whether the session forwards the text and the thinking of a subagent. */
|
|
6853
|
+
forwardsSubagentText(meta) {
|
|
6854
|
+
return (clientSupportsSubagents(this.clientCapabilities) ||
|
|
6855
|
+
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6856
|
+
meta?.claudeCode?.options?.forwardSubagentText === true);
|
|
6857
|
+
}
|
|
6615
6858
|
async createSession(params, creationOpts = {}) {
|
|
6616
6859
|
const createStartedAt = performance.now();
|
|
6617
6860
|
// Validate `cwd` up front. The ACP spec requires an absolute path, and the
|
|
@@ -6649,10 +6892,15 @@ export class ClaudeAcpAgent {
|
|
|
6649
6892
|
// sign-out respawn, provider rerouting), so fill the same fast local hint
|
|
6650
6893
|
// here when the caller did not provide it. Never fall back to the slow
|
|
6651
6894
|
// getContextUsage control request.
|
|
6895
|
+
// The same tail read gives the permission mode of the resumed session.
|
|
6652
6896
|
let resumedModelHint = creationOpts.resumedModelHint;
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6897
|
+
let resumedPermissionMode;
|
|
6898
|
+
if (creationOpts.resume !== undefined) {
|
|
6899
|
+
const withModel = !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint");
|
|
6900
|
+
const tail = await readResumedTail(creationOpts.resume, this.logger, withModel);
|
|
6901
|
+
if (withModel)
|
|
6902
|
+
resumedModelHint = tail.model;
|
|
6903
|
+
resumedPermissionMode = tail.permissionMode;
|
|
6656
6904
|
timing.phase("resume-transcript");
|
|
6657
6905
|
}
|
|
6658
6906
|
const input = new Pushable();
|
|
@@ -6714,7 +6962,11 @@ export class ClaudeAcpAgent {
|
|
|
6714
6962
|
const allowBypass = ALLOW_BYPASS &&
|
|
6715
6963
|
settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
|
|
6716
6964
|
sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
|
|
6717
|
-
const initialPermissionMode =
|
|
6965
|
+
const initialPermissionMode = resolveInitialPermissionMode({
|
|
6966
|
+
explicit: creationOpts.permissionMode,
|
|
6967
|
+
resumed: resumedPermissionMode,
|
|
6968
|
+
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
|
6969
|
+
}, this.logger, allowBypass);
|
|
6718
6970
|
const userProvidedOptions = sessionMeta?.claudeCode?.options
|
|
6719
6971
|
? { ...sessionMeta.claudeCode.options }
|
|
6720
6972
|
: undefined;
|
|
@@ -6723,9 +6975,7 @@ export class ClaudeAcpAgent {
|
|
|
6723
6975
|
// hidden state that the Client cannot inspect or change.
|
|
6724
6976
|
if (userProvidedOptions)
|
|
6725
6977
|
delete userProvidedOptions.agent;
|
|
6726
|
-
const forwardSubagentText =
|
|
6727
|
-
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6728
|
-
userProvidedOptions?.forwardSubagentText === true;
|
|
6978
|
+
const forwardSubagentText = this.forwardsSubagentText(params._meta);
|
|
6729
6979
|
// Configure thinking behavior from environment variable
|
|
6730
6980
|
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
6731
6981
|
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
@@ -6751,7 +7001,7 @@ export class ClaudeAcpAgent {
|
|
|
6751
7001
|
// Per-session task state. Created here (rather than in the session record
|
|
6752
7002
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
6753
7003
|
// the same Map that the streaming message handler will read from.
|
|
6754
|
-
const taskState = new Map();
|
|
7004
|
+
const taskState = creationOpts.replayState?.taskState ?? new Map();
|
|
6755
7005
|
// Resolve every workspace root once. The native checkpoint report uses
|
|
6756
7006
|
// this same set for lexical path validation, and the SDK receives it below.
|
|
6757
7007
|
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
@@ -7044,7 +7294,7 @@ export class ClaudeAcpAgent {
|
|
|
7044
7294
|
const allowedModels = Array.isArray(settingsAvailableModels)
|
|
7045
7295
|
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
|
|
7046
7296
|
: initializationResult.models;
|
|
7047
|
-
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, resumedModelHint);
|
|
7297
|
+
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
|
|
7048
7298
|
timing.phase("models");
|
|
7049
7299
|
// Resolve the current model's capabilities separately from the stable
|
|
7050
7300
|
// permission-mode catalog advertised to ACP clients.
|
|
@@ -7193,8 +7443,8 @@ export class ClaudeAcpAgent {
|
|
|
7193
7443
|
nativeSubagentParentByToolUseId: new Map(),
|
|
7194
7444
|
emittedAssistantText: false,
|
|
7195
7445
|
owedTrailingIdles: 0,
|
|
7196
|
-
messageIdToUuid: new Map(),
|
|
7197
|
-
sessionFailureState: createSessionFailureState(),
|
|
7446
|
+
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7447
|
+
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7198
7448
|
claudeSubscriptionGuard,
|
|
7199
7449
|
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
7200
7450
|
fileChangeReporter,
|
|
@@ -7235,7 +7485,10 @@ export class ClaudeAcpAgent {
|
|
|
7235
7485
|
this.closeQueryStream(session);
|
|
7236
7486
|
delete this.sessions[sessionId];
|
|
7237
7487
|
try {
|
|
7238
|
-
await this.createSession(session.creationParams, {
|
|
7488
|
+
await this.createSession(session.creationParams, {
|
|
7489
|
+
resume: sessionId,
|
|
7490
|
+
permissionMode: session.modes.currentModeId,
|
|
7491
|
+
});
|
|
7239
7492
|
}
|
|
7240
7493
|
catch (error) {
|
|
7241
7494
|
// One session that cannot come back must not abort the switch. The
|
|
@@ -7329,6 +7582,20 @@ function turnOutcome(session, stopReason, extraMeta) {
|
|
|
7329
7582
|
_meta: { ...turnQuotaMeta(session), ...extraMeta },
|
|
7330
7583
|
};
|
|
7331
7584
|
}
|
|
7585
|
+
/** The outcome of `turn` when a cancel ends it. A turn that already has its
|
|
7586
|
+
* result, because it is held open for its background subagents (see
|
|
7587
|
+
* Turn.deferredSettle) or is settling (see Turn.settlingOutcome), keeps the
|
|
7588
|
+
* usage and metadata that result recorded (issue #844). */
|
|
7589
|
+
function cancelledOutcome(session, turn) {
|
|
7590
|
+
const recorded = turn?.deferredSettle ?? turn?.settlingOutcome;
|
|
7591
|
+
if (!recorded)
|
|
7592
|
+
return turnOutcome(session, "cancelled");
|
|
7593
|
+
return {
|
|
7594
|
+
stopReason: "cancelled",
|
|
7595
|
+
usage: recorded.usage,
|
|
7596
|
+
...(recorded._meta ? { _meta: recorded._meta } : {}),
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7332
7599
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
7333
7600
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
7334
7601
|
* reads one shape from either agent.
|
|
@@ -7716,7 +7983,20 @@ function getAvailableSlashCommands(commands,
|
|
|
7716
7983
|
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
|
|
7717
7984
|
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
|
|
7718
7985
|
// the static list. Raw CLI names, matched before the MCP rename.
|
|
7719
|
-
terminalCommands
|
|
7986
|
+
terminalCommands,
|
|
7987
|
+
// The session cwd of an AIR client, or undefined for every other client.
|
|
7988
|
+
// AIR gets the kind and the SKILL.md path of each skill command.
|
|
7989
|
+
airSkillCwd,
|
|
7990
|
+
// The resolved SKILL.md paths of the session, keyed by the cwd and the name.
|
|
7991
|
+
skillPaths) {
|
|
7992
|
+
const skillPath = (name, cwd) => {
|
|
7993
|
+
const key = `${cwd}\0${name}`;
|
|
7994
|
+
if (skillPaths?.has(key))
|
|
7995
|
+
return skillPaths.get(key);
|
|
7996
|
+
const resolved = resolveSkillPath(name, cwd);
|
|
7997
|
+
skillPaths?.set(key, resolved);
|
|
7998
|
+
return resolved;
|
|
7999
|
+
};
|
|
7720
8000
|
const UNSUPPORTED_COMMANDS = [
|
|
7721
8001
|
"clear",
|
|
7722
8002
|
"cost",
|
|
@@ -7727,8 +8007,10 @@ terminalCommands) {
|
|
|
7727
8007
|
"release-notes",
|
|
7728
8008
|
"todos",
|
|
7729
8009
|
];
|
|
7730
|
-
|
|
7731
|
-
|
|
8010
|
+
// The adapter replaces the terminal text of `/mcp` and runs its actions,
|
|
8011
|
+
// even when Claude Code tags the command terminal-bound.
|
|
8012
|
+
const advertised = commands
|
|
8013
|
+
.filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
|
|
7732
8014
|
.map((command) => {
|
|
7733
8015
|
const input = command.argumentHint
|
|
7734
8016
|
? {
|
|
@@ -7738,16 +8020,26 @@ terminalCommands) {
|
|
|
7738
8020
|
}
|
|
7739
8021
|
: null;
|
|
7740
8022
|
let name = command.name;
|
|
7741
|
-
|
|
8023
|
+
const mcpPrompt = command.name.endsWith(" (MCP)");
|
|
8024
|
+
if (mcpPrompt) {
|
|
7742
8025
|
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
7743
8026
|
}
|
|
8027
|
+
const skillMdPath = airSkillCwd && !command.builtin && !mcpPrompt
|
|
8028
|
+
? skillPath(command.name, airSkillCwd)
|
|
8029
|
+
: undefined;
|
|
7744
8030
|
return {
|
|
7745
8031
|
name,
|
|
7746
8032
|
description: command.description || "",
|
|
7747
8033
|
input,
|
|
8034
|
+
...(skillMdPath
|
|
8035
|
+
? {
|
|
8036
|
+
_meta: withAirMeta(withAirMeta(undefined, AIR_KIND_KEY, "skill"), AIR_SKILL_PATH_KEY, skillMdPath),
|
|
8037
|
+
}
|
|
8038
|
+
: {}),
|
|
7748
8039
|
};
|
|
7749
8040
|
})
|
|
7750
8041
|
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
8042
|
+
return [...advertised, MCP_AVAILABLE_COMMAND];
|
|
7751
8043
|
}
|
|
7752
8044
|
function formatUriAsLink(uri) {
|
|
7753
8045
|
try {
|
|
@@ -7922,10 +8214,19 @@ function toolCallFieldsOf(session) {
|
|
|
7922
8214
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
7923
8215
|
* Only handles text, image, and thinking chunks for now.
|
|
7924
8216
|
*/
|
|
8217
|
+
/**
|
|
8218
|
+
* The renderer of the notification functions: the agent's tool call
|
|
8219
|
+
* capabilities when they are passed, else those of the ACP capabilities.
|
|
8220
|
+
*/
|
|
8221
|
+
function toolCallRenderer(options) {
|
|
8222
|
+
return options?.toolCallCapabilities
|
|
8223
|
+
? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
|
|
8224
|
+
: AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
|
|
8225
|
+
}
|
|
7925
8226
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
7926
8227
|
const taskState = options?.taskState ?? new Map();
|
|
7927
8228
|
const registerHooks = options?.registerHooks !== false;
|
|
7928
|
-
const renderer =
|
|
8229
|
+
const renderer = toolCallRenderer(options);
|
|
7929
8230
|
if (typeof content === "string") {
|
|
7930
8231
|
if (content.length === 0) {
|
|
7931
8232
|
return [];
|
|
@@ -8263,6 +8564,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8263
8564
|
const streamedToolInputs = options?.streamedToolInputs;
|
|
8264
8565
|
const forwardedOptions = {
|
|
8265
8566
|
clientCapabilities: options?.clientCapabilities,
|
|
8567
|
+
toolCallCapabilities: options?.toolCallCapabilities,
|
|
8266
8568
|
parentToolUseId: message.parent_tool_use_id,
|
|
8267
8569
|
cwd: options?.cwd,
|
|
8268
8570
|
taskState: options?.taskState,
|
|
@@ -8326,7 +8628,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8326
8628
|
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
|
|
8327
8629
|
if (!shouldEmitToolCall(streamedInput.name))
|
|
8328
8630
|
return [];
|
|
8329
|
-
const update =
|
|
8631
|
+
const update = toolCallRenderer(options).partialRefinement(streamedInput, input, options?.cwd);
|
|
8330
8632
|
if (message.parent_tool_use_id)
|
|
8331
8633
|
stampParentToolUseId(update, message.parent_tool_use_id);
|
|
8332
8634
|
applyMessageId(update, options?.messageId);
|