@agentclientprotocol/claude-agent-acp 0.85.1 → 0.85.2-preview.10
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 +101 -22
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +450 -171
- 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/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/serve.d.ts.map +1 -1
- package/dist/serve.js +1 -2
- 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/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 +0 -7
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +56 -23
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +4 -2
- 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 +15 -0
- package/dist/v2/session-update.d.ts.map +1 -0
- package/dist/v2/session-update.js +41 -0
- package/dist/v2/session.d.ts +23 -0
- package/dist/v2/session.d.ts.map +1 -0
- package/dist/v2/session.js +96 -0
- package/dist/v2/tool-call.d.ts +30 -0
- package/dist/v2/tool-call.d.ts.map +1 -0
- package/dist/v2/tool-call.js +30 -0
- package/package.json +3 -3
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) {
|
|
@@ -1402,6 +1392,56 @@ export class ClaudeAcpAgent {
|
|
|
1402
1392
|
}, 0);
|
|
1403
1393
|
return response;
|
|
1404
1394
|
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Start the CLI of a loaded session, and replay the history while the CLI
|
|
1397
|
+
* starts. The CLI start takes most of the load time, and the replay needs
|
|
1398
|
+
* only the transcript and the {@link ReplayState}. An invalid `cwd` fails
|
|
1399
|
+
* before the first update. A failed CLI start stops the replay.
|
|
1400
|
+
*/
|
|
1401
|
+
async createSessionWhileReplaying(params, timing) {
|
|
1402
|
+
await this.validateCwd(params.cwd);
|
|
1403
|
+
const pending = {
|
|
1404
|
+
state: {
|
|
1405
|
+
cwd: params.cwd,
|
|
1406
|
+
taskState: new Map(),
|
|
1407
|
+
forwardSubagentText: this.forwardsSubagentText(params._meta),
|
|
1408
|
+
messageIdToUuid: new Map(),
|
|
1409
|
+
sessionFailureState: createSessionFailureState(),
|
|
1410
|
+
},
|
|
1411
|
+
stopped: false,
|
|
1412
|
+
};
|
|
1413
|
+
const transcript = readResumedSession(params.sessionId, this.logger);
|
|
1414
|
+
const replay = transcript
|
|
1415
|
+
.then(({ messages }) => this.replaySessionHistory(params.sessionId, messages, pending))
|
|
1416
|
+
.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
1417
|
+
let response;
|
|
1418
|
+
try {
|
|
1419
|
+
response = await this.createSession({
|
|
1420
|
+
cwd: params.cwd,
|
|
1421
|
+
mcpServers: params.mcpServers ?? [],
|
|
1422
|
+
additionalDirectories: params.additionalDirectories,
|
|
1423
|
+
_meta: params._meta,
|
|
1424
|
+
}, {
|
|
1425
|
+
resume: params.sessionId,
|
|
1426
|
+
resumedModelHint: transcript.then(({ model }) => model),
|
|
1427
|
+
replayState: pending.state,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
pending.stopped = true;
|
|
1432
|
+
await replay;
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
timing.phase("session-ready");
|
|
1436
|
+
const replayed = await replay;
|
|
1437
|
+
if (!replayed.ok)
|
|
1438
|
+
throw replayed.error;
|
|
1439
|
+
return {
|
|
1440
|
+
sessionId: response.sessionId,
|
|
1441
|
+
modes: response.modes,
|
|
1442
|
+
configOptions: response.configOptions,
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1405
1445
|
async unstable_forkSession(params) {
|
|
1406
1446
|
if (this.providerUpdate)
|
|
1407
1447
|
await this.providerUpdate;
|
|
@@ -1426,10 +1466,16 @@ export class ClaudeAcpAgent {
|
|
|
1426
1466
|
const timing = new SessionTiming(this.logger, "load", params.sessionId);
|
|
1427
1467
|
if (this.providerUpdate)
|
|
1428
1468
|
await this.providerUpdate;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1469
|
+
let result;
|
|
1470
|
+
if (this.sessions[params.sessionId]) {
|
|
1471
|
+
const resumedSession = await readResumedSession(params.sessionId, this.logger);
|
|
1472
|
+
result = await this.getOrCreateSession(params, resumedSession);
|
|
1473
|
+
timing.phase("session-ready");
|
|
1474
|
+
await this.replaySessionHistory(params.sessionId, resumedSession.messages);
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
result = await this.createSessionWhileReplaying(params, timing);
|
|
1478
|
+
}
|
|
1433
1479
|
timing.phase("replay");
|
|
1434
1480
|
// Send available commands after replay so it doesn't interleave with history
|
|
1435
1481
|
setTimeout(() => {
|
|
@@ -1812,7 +1858,72 @@ export class ClaudeAcpAgent {
|
|
|
1812
1858
|
this.setAuthStatus(notLoggedInAuthStatus());
|
|
1813
1859
|
}
|
|
1814
1860
|
}
|
|
1861
|
+
/** The structured replacement for a prompt that is exactly `/usage` or a
|
|
1862
|
+
* `/mcp` command. Claude Code runs the command, and the replacement takes
|
|
1863
|
+
* the place of its text. Claude Code refuses a `/mcp` reconnect, enable,
|
|
1864
|
+
* or disable in SDK mode. The `/mcp` producer runs them through the SDK
|
|
1865
|
+
* control API when the text arrives, so the action stays in the turn. */
|
|
1866
|
+
localCommandMarkdown(params) {
|
|
1867
|
+
const text = params.prompt.length === 1 && params.prompt[0]?.type === "text"
|
|
1868
|
+
? params.prompt[0].text
|
|
1869
|
+
: undefined;
|
|
1870
|
+
if (text === undefined)
|
|
1871
|
+
return undefined;
|
|
1872
|
+
if (isUsageCommandText(text)) {
|
|
1873
|
+
return {
|
|
1874
|
+
startsAtActivation: true,
|
|
1875
|
+
produce: (query, signal) => structuredUsageMarkdown(query, signal, this.logger),
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const command = parseMcpCommand(text);
|
|
1879
|
+
if (!command)
|
|
1880
|
+
return undefined;
|
|
1881
|
+
let reconnected = [];
|
|
1882
|
+
return {
|
|
1883
|
+
startsAtActivation: false,
|
|
1884
|
+
produce: async (query, signal, originalOutput) => {
|
|
1885
|
+
const outcome = await runMcpCommand(query, command, originalOutput, signal, (message) => this.logger.error(message));
|
|
1886
|
+
reconnected = outcome.reconnected;
|
|
1887
|
+
return outcome.markdown;
|
|
1888
|
+
},
|
|
1889
|
+
// A reconnect starts OAuth for the ACP servers that it tried, as the
|
|
1890
|
+
// session start does. A plain `/mcp` and a cancelled turn start none.
|
|
1891
|
+
afterDelivery: () => {
|
|
1892
|
+
const session = this.sessions[params.sessionId];
|
|
1893
|
+
if (!session || session.cancelled)
|
|
1894
|
+
return;
|
|
1895
|
+
const acpServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
|
|
1896
|
+
const servers = new Set(reconnected.filter((server) => acpServers.has(server)));
|
|
1897
|
+
if (servers.size > 0)
|
|
1898
|
+
startMcpServerAuthentication(this, params.sessionId, servers);
|
|
1899
|
+
},
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
|
|
1904
|
+
* outcome of a turn is field for field a v1 prompt response.
|
|
1905
|
+
*/
|
|
1815
1906
|
async prompt(params) {
|
|
1907
|
+
let events;
|
|
1908
|
+
const outcome = new Promise((resolve, reject) => {
|
|
1909
|
+
events = {
|
|
1910
|
+
inserted() { },
|
|
1911
|
+
awaitingUser() { },
|
|
1912
|
+
resumed() { },
|
|
1913
|
+
ended: resolve,
|
|
1914
|
+
failed: reject,
|
|
1915
|
+
};
|
|
1916
|
+
});
|
|
1917
|
+
await this.startTurn(params, events);
|
|
1918
|
+
return outcome;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Starts a turn for a prompt. Resolves once the prompt is queued for Claude
|
|
1922
|
+
* Code, and rejects only when the prompt is refused before that. From then
|
|
1923
|
+
* on, `events` reports how the turn goes (see {@link TurnEvents}), possibly
|
|
1924
|
+
* before this resolves.
|
|
1925
|
+
*/
|
|
1926
|
+
async startTurn(params, events) {
|
|
1816
1927
|
if (this.providerUpdate)
|
|
1817
1928
|
await this.providerUpdate;
|
|
1818
1929
|
let session = this.sessions[params.sessionId];
|
|
@@ -1854,45 +1965,41 @@ export class ClaudeAcpAgent {
|
|
|
1854
1965
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
1855
1966
|
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
1856
1967
|
const fileChangeReport = session.fileChangeReporter?.request(params._meta);
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
isUsageCommandText(params.prompt[0].text);
|
|
1860
|
-
const usageMarkdownAbort = isUsageCommand ? new AbortController() : undefined;
|
|
1968
|
+
const localCommand = this.localCommandMarkdown(params);
|
|
1969
|
+
const localCommandAbort = localCommand ? new AbortController() : undefined;
|
|
1861
1970
|
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
|
|
1971
|
+
// Each prompt is a Turn that the persistent consumer settles once the
|
|
1972
|
+
// turn's outcome is known. `startTurn()` owns no loop: it enqueues the
|
|
1973
|
+
// turn, pushes the user message onto the streaming input, and makes sure
|
|
1974
|
+
// the consumer is running.
|
|
1975
|
+
let completeTurn;
|
|
1866
1976
|
const turn = {
|
|
1867
1977
|
promptUuid,
|
|
1978
|
+
events,
|
|
1868
1979
|
isLocalOnlyCommand,
|
|
1869
|
-
...(
|
|
1870
|
-
...(
|
|
1980
|
+
...(localCommand ? { localCommand } : {}),
|
|
1981
|
+
...(localCommandAbort ? { localCommandAbort } : {}),
|
|
1871
1982
|
...(fileChangeReport ? { fileChangeReport } : {}),
|
|
1872
1983
|
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);
|
|
1984
|
+
completion: new Promise((resolve) => {
|
|
1985
|
+
completeTurn = resolve;
|
|
1986
|
+
}),
|
|
1987
|
+
resolve: (outcome) => {
|
|
1988
|
+
events.ended(outcome);
|
|
1883
1989
|
completeTurn();
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1990
|
+
},
|
|
1991
|
+
reject: (error, title) => {
|
|
1992
|
+
events.failed(error, title);
|
|
1887
1993
|
completeTurn();
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1994
|
+
},
|
|
1995
|
+
};
|
|
1890
1996
|
session.turnQueue ??= [];
|
|
1891
1997
|
session.turnQueue.push(turn);
|
|
1892
1998
|
session.input.push(userMessage);
|
|
1893
1999
|
this.ensureConsumer(session, params.sessionId);
|
|
1894
|
-
|
|
1895
|
-
|
|
2000
|
+
// The prompt is queued, so its turn goes ahead even if the client misses
|
|
2001
|
+
// the goal it sets; the turn's events report the outcome.
|
|
2002
|
+
await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid).catch((error) => this.logger.error(`Session ${params.sessionId}: failed to publish the prompt's goal:`, error));
|
|
1896
2003
|
}
|
|
1897
2004
|
/** `--hide-claude-auth` applies only to the CLI's own login. A provider
|
|
1898
2005
|
* override (`providers/set` or gateway `authenticate`) routes traffic away
|
|
@@ -1993,7 +2100,10 @@ export class ClaudeAcpAgent {
|
|
|
1993
2100
|
try {
|
|
1994
2101
|
// `resume` names the Claude session, which shares the ACP session id, so
|
|
1995
2102
|
// the new query continues the same conversation under the same id.
|
|
1996
|
-
await this.createSession(creationParams, {
|
|
2103
|
+
await this.createSession(creationParams, {
|
|
2104
|
+
resume: sessionId,
|
|
2105
|
+
permissionMode: session.modes.currentModeId,
|
|
2106
|
+
});
|
|
1997
2107
|
}
|
|
1998
2108
|
catch (error) {
|
|
1999
2109
|
if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
|
|
@@ -2003,7 +2113,10 @@ export class ClaudeAcpAgent {
|
|
|
2003
2113
|
// better answer than a session the client can never use again.
|
|
2004
2114
|
this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
|
|
2005
2115
|
try {
|
|
2006
|
-
await this.createSession(creationParams, {
|
|
2116
|
+
await this.createSession(creationParams, {
|
|
2117
|
+
reuseSessionId: sessionId,
|
|
2118
|
+
permissionMode: session.modes.currentModeId,
|
|
2119
|
+
});
|
|
2007
2120
|
return;
|
|
2008
2121
|
}
|
|
2009
2122
|
catch (fallbackError) {
|
|
@@ -2465,11 +2578,11 @@ export class ClaudeAcpAgent {
|
|
|
2465
2578
|
};
|
|
2466
2579
|
};
|
|
2467
2580
|
const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
|
|
2468
|
-
const
|
|
2469
|
-
if (!turn.
|
|
2581
|
+
const ensureLocalCommandMarkdown = (turn, originalOutput = "") => {
|
|
2582
|
+
if (!turn.localCommand || !turn.localCommandAbort)
|
|
2470
2583
|
return undefined;
|
|
2471
|
-
turn.
|
|
2472
|
-
return turn.
|
|
2584
|
+
turn.localCommandMarkdown ??= turn.localCommand.produce(session.query, turn.localCommandAbort.signal, originalOutput);
|
|
2585
|
+
return turn.localCommandMarkdown;
|
|
2473
2586
|
};
|
|
2474
2587
|
const resetTurnScratch = () => {
|
|
2475
2588
|
lastAssistantTotalUsage = null;
|
|
@@ -2512,7 +2625,8 @@ export class ClaudeAcpAgent {
|
|
|
2512
2625
|
session.activeTurn = turn;
|
|
2513
2626
|
session.cancelled = false;
|
|
2514
2627
|
compaction.resume();
|
|
2515
|
-
|
|
2628
|
+
if (turn.localCommand?.startsAtActivation)
|
|
2629
|
+
ensureLocalCommandMarkdown(turn);
|
|
2516
2630
|
session.pendingOrphanResults = 0;
|
|
2517
2631
|
session.orphanCommands?.clear();
|
|
2518
2632
|
// Two-phase sweep of registry entries the level signal ended (see
|
|
@@ -2540,6 +2654,22 @@ export class ClaudeAcpAgent {
|
|
|
2540
2654
|
}
|
|
2541
2655
|
}
|
|
2542
2656
|
resetTurnScratch();
|
|
2657
|
+
// Without a command_lifecycle "started" frame before it (see that
|
|
2658
|
+
// handler), activation is the first sign that Claude Code took the
|
|
2659
|
+
// prompt in: its echo, or for an echo-less command, its result.
|
|
2660
|
+
reportInserted(turn);
|
|
2661
|
+
// A request can already be open: Claude Code asks for a queued prompt
|
|
2662
|
+
// before the consumer reaches its echo.
|
|
2663
|
+
this.syncAwaitingUser(session);
|
|
2664
|
+
};
|
|
2665
|
+
/** Report that Claude Code took the turn's prompt in, once. A turn can be
|
|
2666
|
+
* settled by now: an echo hand-off awaits settling the previous turn, and
|
|
2667
|
+
* a cancel in that window settles the queued one. */
|
|
2668
|
+
const reportInserted = (turn) => {
|
|
2669
|
+
if (turn.insertedReported || turn.settled)
|
|
2670
|
+
return;
|
|
2671
|
+
turn.insertedReported = true;
|
|
2672
|
+
turn.events.inserted(turn.promptUuid);
|
|
2543
2673
|
};
|
|
2544
2674
|
/** Ensure there is an active turn before a user-turn result that carries no
|
|
2545
2675
|
* echo to activate it, by promoting the queue head. Most turns are
|
|
@@ -2699,27 +2829,28 @@ export class ClaudeAcpAgent {
|
|
|
2699
2829
|
* the autonomous stretch-close guard. */
|
|
2700
2830
|
const firstUnsettledQueuedTurn = () => (session.turnQueue ?? []).find((t) => !t.settled);
|
|
2701
2831
|
/** 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
|
-
|
|
2832
|
+
* local-command output. Undefined means this is not a structured
|
|
2833
|
+
* local-command turn (or its request failed), null means another SDK
|
|
2834
|
+
* message shape already delivered it, and string is the one replacement
|
|
2835
|
+
* to publish. */
|
|
2836
|
+
const takeLocalCommandMarkdown = async (originalOutput) => {
|
|
2706
2837
|
const turn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
2707
2838
|
if (!turn)
|
|
2708
2839
|
return undefined;
|
|
2709
|
-
const
|
|
2710
|
-
if (!
|
|
2840
|
+
const pending = ensureLocalCommandMarkdown(turn, originalOutput);
|
|
2841
|
+
if (!pending)
|
|
2711
2842
|
return undefined;
|
|
2712
|
-
const markdown = await
|
|
2843
|
+
const markdown = await pending;
|
|
2713
2844
|
if (markdown === null)
|
|
2714
2845
|
return undefined;
|
|
2715
|
-
if (turn.
|
|
2846
|
+
if (turn.localCommandDelivered) {
|
|
2716
2847
|
// Different SDK message shapes can mirror the same local-command
|
|
2717
2848
|
// output. Suppress an exact mirror, but let a later, distinct frame
|
|
2718
2849
|
// (for example an interruption diagnostic) follow the normal path.
|
|
2719
|
-
return turn.
|
|
2850
|
+
return turn.localCommandOriginalOutput === originalOutput ? null : undefined;
|
|
2720
2851
|
}
|
|
2721
|
-
turn.
|
|
2722
|
-
turn.
|
|
2852
|
+
turn.localCommandDelivered = true;
|
|
2853
|
+
turn.localCommandOriginalOutput = originalOutput;
|
|
2723
2854
|
return markdown;
|
|
2724
2855
|
};
|
|
2725
2856
|
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
@@ -2872,7 +3003,9 @@ export class ClaudeAcpAgent {
|
|
|
2872
3003
|
// Captured before the settled flip below (isHeldOpen tests !settled).
|
|
2873
3004
|
const wasHeld = isHeldOpen(turn);
|
|
2874
3005
|
turn.settled = true;
|
|
2875
|
-
turn.
|
|
3006
|
+
turn.localCommandAbort?.abort();
|
|
3007
|
+
if (turn.localCommandDelivered)
|
|
3008
|
+
turn.localCommand?.afterDelivery?.();
|
|
2876
3009
|
disarmForceCancel(session);
|
|
2877
3010
|
session.turnQueue = (session.turnQueue ?? []).filter((t) => t !== turn);
|
|
2878
3011
|
session.activeTurn = null;
|
|
@@ -2896,7 +3029,7 @@ export class ClaudeAcpAgent {
|
|
|
2896
3029
|
};
|
|
2897
3030
|
/** Reject the active turn (auth required, error result, …) without tearing
|
|
2898
3031
|
* down the consumer: the stream continues to idle and later turns proceed. */
|
|
2899
|
-
const failActive = (error) => {
|
|
3032
|
+
const failActive = (error, title) => {
|
|
2900
3033
|
disarmForceCancel(session);
|
|
2901
3034
|
const turn = session.activeTurn;
|
|
2902
3035
|
if (!turn || turn.settled) {
|
|
@@ -2913,7 +3046,7 @@ export class ClaudeAcpAgent {
|
|
|
2913
3046
|
// start the next stretch clean, or its stale delivery record would
|
|
2914
3047
|
// suppress the next turn's issue-#453 result-text fallback.
|
|
2915
3048
|
session.emittedAssistantText = false;
|
|
2916
|
-
turn.reject(error);
|
|
3049
|
+
turn.reject(error, title);
|
|
2917
3050
|
};
|
|
2918
3051
|
/** Complete a negotiated terminal failure on the prompt response itself,
|
|
2919
3052
|
* which is the canonical AIR carrier. Legacy clients keep the historical
|
|
@@ -2937,7 +3070,7 @@ export class ClaudeAcpAgent {
|
|
|
2937
3070
|
return;
|
|
2938
3071
|
}
|
|
2939
3072
|
if (!supportsAirSessionFailures(this.clientCapabilities)) {
|
|
2940
|
-
failActive(error);
|
|
3073
|
+
failActive(error, title);
|
|
2941
3074
|
return;
|
|
2942
3075
|
}
|
|
2943
3076
|
if (!session.activeTurn || session.activeTurn.settled) {
|
|
@@ -2947,7 +3080,7 @@ export class ClaudeAcpAgent {
|
|
|
2947
3080
|
}
|
|
2948
3081
|
const failure = await createSessionFailure(kind, { title });
|
|
2949
3082
|
if (!failure) {
|
|
2950
|
-
failActive(error);
|
|
3083
|
+
failActive(error, title);
|
|
2951
3084
|
return;
|
|
2952
3085
|
}
|
|
2953
3086
|
sessionFailures.recordActive(failure);
|
|
@@ -3051,7 +3184,7 @@ export class ClaudeAcpAgent {
|
|
|
3051
3184
|
}
|
|
3052
3185
|
}
|
|
3053
3186
|
await compaction.interrupt();
|
|
3054
|
-
await settleActive(
|
|
3187
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3055
3188
|
// The cancelled turn's result may never come (that's why the
|
|
3056
3189
|
// backstop fired) — close its delivery stretch here so partial
|
|
3057
3190
|
// streamed text can't suppress the next turn's issue-#453 fallback.
|
|
@@ -3107,7 +3240,7 @@ export class ClaudeAcpAgent {
|
|
|
3107
3240
|
const inFlight = session.activeTurn;
|
|
3108
3241
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
3109
3242
|
await settleActive(session.cancelled
|
|
3110
|
-
?
|
|
3243
|
+
? cancelledOutcome(session, inFlight)
|
|
3111
3244
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
3112
3245
|
// Queued turns the SDK never started never ran, so reject them rather
|
|
3113
3246
|
// than reporting a success (end_turn) — or a misleading "cancelled" —
|
|
@@ -3142,10 +3275,10 @@ export class ClaudeAcpAgent {
|
|
|
3142
3275
|
// prompt() stamps a uuid on every message. The frame is @internal and
|
|
3143
3276
|
// absent from the SDKMessage union, so handle it BEFORE the exhaustive
|
|
3144
3277
|
// 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.)
|
|
3278
|
+
// for it wouldn't typecheck. It feeds the orphan accounting (see
|
|
3279
|
+
// Session.orphanCommands) and reports a turn inserted at "started";
|
|
3280
|
+
// turn activation and settlement stay driven by echoes/results/idle.
|
|
3281
|
+
// (Raw-mode emission above still forwards these frames.)
|
|
3149
3282
|
if (message.type === "command_lifecycle") {
|
|
3150
3283
|
const frame = message;
|
|
3151
3284
|
switch (frame.state) {
|
|
@@ -3156,6 +3289,17 @@ export class ClaudeAcpAgent {
|
|
|
3156
3289
|
if (queued) {
|
|
3157
3290
|
queued.commandStarted = true;
|
|
3158
3291
|
compaction.resume();
|
|
3292
|
+
// Claude Code took the prompt in: it drained into a turn. A
|
|
3293
|
+
// fresh turn reports that before anything it produces, its echo
|
|
3294
|
+
// and first stream events included, so this is the earliest
|
|
3295
|
+
// insertion point. (A prompt folded into a running turn reports
|
|
3296
|
+
// it after its echo, which already activated it.) While another
|
|
3297
|
+
// turn is still active (held for background work, steered, or
|
|
3298
|
+
// cancelled and awaiting its trailing idle), insertion waits
|
|
3299
|
+
// for this turn's echo, whose hand-off ends that turn first.
|
|
3300
|
+
const active = session.activeTurn;
|
|
3301
|
+
if (!active || active.settled || active === queued)
|
|
3302
|
+
reportInserted(queued);
|
|
3159
3303
|
}
|
|
3160
3304
|
// ...and promote an already-orphaned command: once dispatched,
|
|
3161
3305
|
// a bare `cancelled` no longer means "dropped without running".
|
|
@@ -3332,17 +3476,18 @@ export class ClaudeAcpAgent {
|
|
|
3332
3476
|
if (compaction.consumeDuplicateErrorOutput(message.content)) {
|
|
3333
3477
|
break;
|
|
3334
3478
|
}
|
|
3335
|
-
const
|
|
3336
|
-
const
|
|
3337
|
-
if (
|
|
3479
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
3480
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.content);
|
|
3481
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
3338
3482
|
break;
|
|
3339
|
-
if (
|
|
3483
|
+
if (commandMarkdown === null)
|
|
3340
3484
|
break;
|
|
3341
3485
|
await sendUpdate({
|
|
3342
3486
|
sessionId: params.sessionId,
|
|
3343
3487
|
update: {
|
|
3344
3488
|
sessionUpdate: "agent_message_chunk",
|
|
3345
|
-
content: { type: "text", text:
|
|
3489
|
+
content: { type: "text", text: commandMarkdown ?? message.content },
|
|
3490
|
+
messageId: messageIdForGrouping(message),
|
|
3346
3491
|
},
|
|
3347
3492
|
});
|
|
3348
3493
|
break;
|
|
@@ -3399,7 +3544,13 @@ export class ClaudeAcpAgent {
|
|
|
3399
3544
|
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
3400
3545
|
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
3401
3546
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
3402
|
-
|
|
3547
|
+
// A held turn's result passed long ago; this idle is the
|
|
3548
|
+
// trailer of the followup cycle the cancel interrupted,
|
|
3549
|
+
// whose result (an autonomous one) recorded it as owed.
|
|
3550
|
+
if (isHeldOpen(session.activeTurn) && session.owedTrailingIdles > 0) {
|
|
3551
|
+
session.owedTrailingIdles--;
|
|
3552
|
+
}
|
|
3553
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
3403
3554
|
// An interrupt can pre-empt the turn's result entirely
|
|
3404
3555
|
// (nothing ran the result-case `finally`), so close the
|
|
3405
3556
|
// delivery stretch here: idle is the SDK's authoritative
|
|
@@ -3501,11 +3652,12 @@ export class ClaudeAcpAgent {
|
|
|
3501
3652
|
// list with this payload. Forward message.commands directly —
|
|
3502
3653
|
// it's authoritative, and re-querying supportedCommands()
|
|
3503
3654
|
// would just return the same list with an extra round-trip.
|
|
3655
|
+
session.skillPaths = new Map();
|
|
3504
3656
|
await sendUpdate({
|
|
3505
3657
|
sessionId: params.sessionId,
|
|
3506
3658
|
update: {
|
|
3507
3659
|
sessionUpdate: "available_commands_update",
|
|
3508
|
-
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
|
|
3660
|
+
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, session.skillPaths),
|
|
3509
3661
|
},
|
|
3510
3662
|
});
|
|
3511
3663
|
break;
|
|
@@ -4277,6 +4429,7 @@ export class ClaudeAcpAgent {
|
|
|
4277
4429
|
update: {
|
|
4278
4430
|
sessionUpdate: "agent_message_chunk",
|
|
4279
4431
|
content: { type: "text", text: lastRefusalExplanation },
|
|
4432
|
+
messageId: messageIdForGrouping(message),
|
|
4280
4433
|
},
|
|
4281
4434
|
});
|
|
4282
4435
|
}
|
|
@@ -4351,10 +4504,13 @@ export class ClaudeAcpAgent {
|
|
|
4351
4504
|
(message.usage.output_tokens ?? 0) === 0 &&
|
|
4352
4505
|
!session.activeTurn?.noticeTexts?.includes(normalizeNoticeText(message.result)));
|
|
4353
4506
|
if (shouldForwardResult) {
|
|
4354
|
-
const
|
|
4355
|
-
|
|
4507
|
+
const commandTurn = session.activeTurn ?? firstUnsettledQueuedTurn();
|
|
4508
|
+
const commandMarkdown = await takeLocalCommandMarkdown(message.result);
|
|
4509
|
+
if (commandTurn?.localCommand && session.cancelled)
|
|
4510
|
+
break;
|
|
4511
|
+
if (commandMarkdown === null)
|
|
4356
4512
|
break;
|
|
4357
|
-
for (const notification of toAcpNotifications(
|
|
4513
|
+
for (const notification of toAcpNotifications(commandMarkdown ?? message.result, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4358
4514
|
await sendUpdate(notification);
|
|
4359
4515
|
}
|
|
4360
4516
|
}
|
|
@@ -4636,7 +4792,7 @@ export class ClaudeAcpAgent {
|
|
|
4636
4792
|
session.owedTrailingIdles++;
|
|
4637
4793
|
// Before activateTurn resets the accumulator, so the
|
|
4638
4794
|
// usage still belongs to the cancelled turn.
|
|
4639
|
-
await settleActive(
|
|
4795
|
+
await settleActive(cancelledOutcome(session, session.activeTurn));
|
|
4640
4796
|
}
|
|
4641
4797
|
else if (isHeldOpen(session.activeTurn)) {
|
|
4642
4798
|
// A turn held open for its background subagents (see
|
|
@@ -4747,16 +4903,16 @@ export class ClaudeAcpAgent {
|
|
|
4747
4903
|
// Depending on the Claude Code build, a local command can arrive
|
|
4748
4904
|
// as the dedicated system message above or as a synthetic
|
|
4749
4905
|
// assistant message. Replace only the output owned by the exact
|
|
4750
|
-
// /usage turn; no content signatures or text parsing are involved.
|
|
4906
|
+
// /usage or /mcp turn; no content signatures or text parsing are involved.
|
|
4751
4907
|
if (message.type === "assistant" &&
|
|
4752
4908
|
message.parent_tool_use_id === null &&
|
|
4753
4909
|
message.message.model === "<synthetic>") {
|
|
4754
|
-
const
|
|
4910
|
+
const commandMarkdown = await takeLocalCommandMarkdown(assistantMessageText(message.message) ?? "");
|
|
4755
4911
|
if (session.cancelled)
|
|
4756
4912
|
break;
|
|
4757
|
-
if (
|
|
4758
|
-
if (
|
|
4759
|
-
for (const notification of toAcpNotifications(
|
|
4913
|
+
if (commandMarkdown !== undefined) {
|
|
4914
|
+
if (commandMarkdown !== null) {
|
|
4915
|
+
for (const notification of toAcpNotifications(commandMarkdown, "assistant", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, { messageId: messageIdForGrouping(message) })) {
|
|
4760
4916
|
await sendUpdate(notification);
|
|
4761
4917
|
}
|
|
4762
4918
|
}
|
|
@@ -5040,6 +5196,36 @@ export class ClaudeAcpAgent {
|
|
|
5040
5196
|
session.pendingOrphanResults = (session.pendingOrphanResults ?? 0) + 1;
|
|
5041
5197
|
}
|
|
5042
5198
|
}
|
|
5199
|
+
/** Ask Claude Code to drop user messages it still has queued, so that a
|
|
5200
|
+
* prompt a cancel settled is not run after all. Claude Code keeps queued
|
|
5201
|
+
* messages through an interrupt and runs them next (the receipt's
|
|
5202
|
+
* `still_queued`), unless they are cancelled first with
|
|
5203
|
+
* `cancel_async_message`: a no-op for a message it already dequeued, which
|
|
5204
|
+
* then still runs, as an orphan (see `orphanCommands`).
|
|
5205
|
+
*
|
|
5206
|
+
* Not awaited: the SDK writes control requests in call order, so the drops
|
|
5207
|
+
* reach Claude Code before a request sent after them, and a wedged CLI must
|
|
5208
|
+
* not hold up the cancel. The orphan accounting needs no update here: a
|
|
5209
|
+
* dropped message gets a `cancelled` lifecycle frame and is missing from the
|
|
5210
|
+
* interrupt receipt's `still_queued`, which both already account for. The
|
|
5211
|
+
* SDK implements the request as `Query.cancelAsyncMessage` without typing
|
|
5212
|
+
* it, so it is looked up, and a CLI that lacks it rejects the request. */
|
|
5213
|
+
dropQueuedMessages(sessionId, query, uuids) {
|
|
5214
|
+
const cancelAsyncMessage = query
|
|
5215
|
+
.cancelAsyncMessage;
|
|
5216
|
+
if (typeof cancelAsyncMessage !== "function")
|
|
5217
|
+
return;
|
|
5218
|
+
const logFailure = (uuid, error) => this.logger.error(`Session ${sessionId}: could not drop the queued message ${uuid} from Claude Code:`, error);
|
|
5219
|
+
for (const uuid of uuids) {
|
|
5220
|
+
// Called synchronously: deferring it would let the interrupt go first.
|
|
5221
|
+
try {
|
|
5222
|
+
Promise.resolve(cancelAsyncMessage.call(query, uuid)).catch((error) => logFailure(uuid, error));
|
|
5223
|
+
}
|
|
5224
|
+
catch (error) {
|
|
5225
|
+
logFailure(uuid, error);
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5043
5229
|
async cancel(params) {
|
|
5044
5230
|
await this.cancelTurns(params, { awaitInterrupt: true });
|
|
5045
5231
|
}
|
|
@@ -5056,7 +5242,7 @@ export class ClaudeAcpAgent {
|
|
|
5056
5242
|
}
|
|
5057
5243
|
session.cancelled = true;
|
|
5058
5244
|
for (const turn of session.turnQueue ?? [])
|
|
5059
|
-
turn.
|
|
5245
|
+
turn.localCommandAbort?.abort();
|
|
5060
5246
|
session.pendingExitPlanModeInterruption = undefined;
|
|
5061
5247
|
session.pendingExitPlanContextReset = undefined;
|
|
5062
5248
|
// The stream already ended (see closeQueryStream): every in-flight turn was
|
|
@@ -5085,12 +5271,16 @@ export class ClaudeAcpAgent {
|
|
|
5085
5271
|
session.toolCallFields?.clear();
|
|
5086
5272
|
clearHookCallbacks(params.sessionId);
|
|
5087
5273
|
}
|
|
5274
|
+
// The user messages this cancel abandons while Claude Code may still have
|
|
5275
|
+
// them queued: they are dropped from its queue before the interrupt.
|
|
5276
|
+
const abandoned = [];
|
|
5088
5277
|
// A priority steer may still be queued in the SDK when cancellation
|
|
5089
5278
|
// settles its owning turn. Its later echo matches no live turn, and its
|
|
5090
5279
|
// result must be skipped rather than promoted onto the next prompt.
|
|
5091
5280
|
if (isSteering(session.activeTurn)) {
|
|
5092
5281
|
for (const uuid of session.activeTurn.steeredEchoes) {
|
|
5093
5282
|
this.trackOrphanCommand(session, uuid, "pending");
|
|
5283
|
+
abandoned.push(uuid);
|
|
5094
5284
|
}
|
|
5095
5285
|
}
|
|
5096
5286
|
// Capture the orphan-accounting lane before anything can await: the
|
|
@@ -5114,11 +5304,13 @@ export class ClaudeAcpAgent {
|
|
|
5114
5304
|
// accumulator (the active turn's tally) is not its spend.
|
|
5115
5305
|
turn.resolve({ stopReason: "cancelled" });
|
|
5116
5306
|
orphanedTurns.push(turn);
|
|
5307
|
+
abandoned.push(turn.promptUuid);
|
|
5117
5308
|
}
|
|
5118
5309
|
}
|
|
5119
|
-
// Each removed queued turn's user message was already pushed to the SDK
|
|
5120
|
-
//
|
|
5121
|
-
//
|
|
5310
|
+
// Each removed queued turn's user message was already pushed to the SDK.
|
|
5311
|
+
// It is dropped from Claude Code's queue below (see dropQueuedMessages),
|
|
5312
|
+
// but one Claude Code already dequeued still runs, FIFO, and emits a
|
|
5313
|
+
// result with no user echo to match (0.3.246+ CLIs do stamp results with the
|
|
5122
5314
|
// triggering send's user_message_uuid, which ensureActiveTurn uses as
|
|
5123
5315
|
// an exact join when present). Track those so the consumer skips them
|
|
5124
5316
|
// (see ensureActiveTurn) rather than misattributing them to the head.
|
|
@@ -5181,15 +5373,23 @@ export class ClaudeAcpAgent {
|
|
|
5181
5373
|
}
|
|
5182
5374
|
// A deferred active turn (see Turn.deferredSettle) already has its result
|
|
5183
5375
|
// 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
|
-
//
|
|
5376
|
+
// result but is awaiting the bounded checkpoint preview. Settle it
|
|
5377
|
+
// "cancelled" NOW where the consumer cannot: it cannot process the
|
|
5378
|
+
// interrupt's trailing idle while blocked in that preview, and a held turn
|
|
5379
|
+
// whose session sits idle gets no trailing idle (the interrupt emits
|
|
5380
|
+
// nothing; with no state events consumed yet, the same is assumed).
|
|
5381
|
+
// A held turn whose followup cycle is live (running, or blocked on a
|
|
5382
|
+
// permission request) is left to that idle, like any live turn: the
|
|
5383
|
+
// interrupt still flushes the cycle's output, which must reach the client
|
|
5384
|
+
// before the turn ends cancelled. The captured outcome preserves usage and
|
|
5385
|
+
// metadata (issue #844), while reporter state makes a late checkpoint
|
|
5386
|
+
// response harmless.
|
|
5189
5387
|
{
|
|
5190
5388
|
const active = session.activeTurn;
|
|
5191
|
-
const
|
|
5192
|
-
if (active &&
|
|
5389
|
+
const followupLive = session.lastSessionState === "running" || session.lastSessionState === "requires_action";
|
|
5390
|
+
if (active &&
|
|
5391
|
+
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5392
|
+
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5193
5393
|
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
5194
5394
|
active.settled = true;
|
|
5195
5395
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
@@ -5203,17 +5403,16 @@ export class ClaudeAcpAgent {
|
|
|
5203
5403
|
// text since the last boundary was its followups', and left latched
|
|
5204
5404
|
// it would suppress a following replayed turn's issue-#453 fallback.
|
|
5205
5405
|
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
|
|
5406
|
+
// When the interrupt below pre-empts a live cycle (a settling turn's,
|
|
5407
|
+
// or one no state event reported), it produces a trailer idle with no
|
|
5408
|
+
// counted result; that idle would be un-owed and could lag past the
|
|
5409
|
+
// next prompt's echo — read as the fresh turn ending without a result
|
|
5410
|
+
// (issue #825 false-fail).
|
|
5411
|
+
// Pre-count it unless the session sits idle: there the interrupt
|
|
5412
|
+
// emits nothing, and a debt that never drains would mask one future
|
|
5413
|
+
// #825 detection. (lastSessionState is last-CONSUMED, so both stale
|
|
5414
|
+
// reads exist and both are accepted one-cycle windows: a running
|
|
5415
|
+
// transition still in the backlog reads as stale idle and
|
|
5217
5416
|
// under-counts — that false-fail additionally needs the trailer to
|
|
5218
5417
|
// lag past the next echo — while a cycle already completed into the
|
|
5219
5418
|
// backlog reads as stale non-idle and over-counts, masking one
|
|
@@ -5223,14 +5422,7 @@ export class ClaudeAcpAgent {
|
|
|
5223
5422
|
if (session.lastSessionState !== "idle") {
|
|
5224
5423
|
session.owedTrailingIdles++;
|
|
5225
5424
|
}
|
|
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
|
-
});
|
|
5425
|
+
active.resolve(cancelledOutcome(session, active));
|
|
5234
5426
|
}
|
|
5235
5427
|
}
|
|
5236
5428
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -5263,6 +5455,9 @@ export class ClaudeAcpAgent {
|
|
|
5263
5455
|
cancelController.abort();
|
|
5264
5456
|
}, this.forceCancelGraceMs);
|
|
5265
5457
|
}
|
|
5458
|
+
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
5459
|
+
// the next queued message right away.
|
|
5460
|
+
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
5266
5461
|
const interrupt = session.query.interrupt();
|
|
5267
5462
|
if (!options.awaitInterrupt) {
|
|
5268
5463
|
// The caller closes the query next, which rejects the pending reply.
|
|
@@ -5498,13 +5693,15 @@ export class ClaudeAcpAgent {
|
|
|
5498
5693
|
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
|
|
5499
5694
|
return { configOptions: session.configOptions };
|
|
5500
5695
|
}
|
|
5501
|
-
async replaySessionHistory(sessionId, resumedMessages) {
|
|
5696
|
+
async replaySessionHistory(sessionId, resumedMessages, pending) {
|
|
5502
5697
|
const replayStartedAt = performance.now();
|
|
5503
5698
|
const toolUseCache = {};
|
|
5504
5699
|
const messages = resumedMessages ?? (await getSessionMessages(sessionId));
|
|
5505
5700
|
const historyLoadedAt = performance.now();
|
|
5506
5701
|
this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
|
|
5507
|
-
|
|
5702
|
+
// A pending replay has no session record yet. It uses the state that the record takes later.
|
|
5703
|
+
const replayState = () => pending?.state ?? this.sessions[sessionId];
|
|
5704
|
+
const session = replayState();
|
|
5508
5705
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
5509
5706
|
if (session?.taskState)
|
|
5510
5707
|
forgetPublishedTaskPlan(session.taskState);
|
|
@@ -5516,7 +5713,12 @@ export class ClaudeAcpAgent {
|
|
|
5516
5713
|
sessionId,
|
|
5517
5714
|
state: session.sessionFailureState,
|
|
5518
5715
|
capabilities: this.clientCapabilities,
|
|
5519
|
-
isCurrent: () =>
|
|
5716
|
+
isCurrent: () => {
|
|
5717
|
+
const live = this.sessions[sessionId];
|
|
5718
|
+
if (!live)
|
|
5719
|
+
return pending !== undefined && !pending.stopped;
|
|
5720
|
+
return live.sessionFailureState === session.sessionFailureState;
|
|
5721
|
+
},
|
|
5520
5722
|
sendUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
5521
5723
|
logger: this.logger,
|
|
5522
5724
|
})
|
|
@@ -5671,7 +5873,7 @@ export class ClaudeAcpAgent {
|
|
|
5671
5873
|
await replayAsyncTasks.taskNotification(notification);
|
|
5672
5874
|
};
|
|
5673
5875
|
const replayMessage = async (message) => {
|
|
5674
|
-
if (isReplayHiddenMetaMessage(message)) {
|
|
5876
|
+
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
5675
5877
|
return;
|
|
5676
5878
|
}
|
|
5677
5879
|
if (message.type === "user" &&
|
|
@@ -5685,7 +5887,7 @@ export class ClaudeAcpAgent {
|
|
|
5685
5887
|
// a client-supplied id without an extra getSessionMessages read. Not read
|
|
5686
5888
|
// yet (see Session.messageIdToUuid).
|
|
5687
5889
|
const replayMessageId = messageIdForGrouping(message);
|
|
5688
|
-
const replaySession =
|
|
5890
|
+
const replaySession = replayState();
|
|
5689
5891
|
if (replaySession && replayMessageId && message.uuid) {
|
|
5690
5892
|
replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
|
|
5691
5893
|
}
|
|
@@ -5779,8 +5981,8 @@ export class ClaudeAcpAgent {
|
|
|
5779
5981
|
registerHooks: false,
|
|
5780
5982
|
replay: true,
|
|
5781
5983
|
clientCapabilities: this.clientCapabilities,
|
|
5782
|
-
cwd:
|
|
5783
|
-
taskState:
|
|
5984
|
+
cwd: replayState()?.cwd,
|
|
5985
|
+
taskState: replayState()?.taskState,
|
|
5784
5986
|
messageId: replayMessageId,
|
|
5785
5987
|
parentToolUseId,
|
|
5786
5988
|
})) {
|
|
@@ -5825,7 +6027,7 @@ export class ClaudeAcpAgent {
|
|
|
5825
6027
|
};
|
|
5826
6028
|
for (const message of messages)
|
|
5827
6029
|
await replayMessage(message);
|
|
5828
|
-
if (nativeReplayEnabled) {
|
|
6030
|
+
if (nativeReplayEnabled && !pending?.stopped) {
|
|
5829
6031
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
5830
6032
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
5831
6033
|
// state from the launch tool_result. Missing results and malformed or
|
|
@@ -5862,18 +6064,40 @@ export class ClaudeAcpAgent {
|
|
|
5862
6064
|
/** Mark a client request as blocking on user input for exactly the lifetime
|
|
5863
6065
|
* of its promise. Steering consults this session-local count synchronously,
|
|
5864
6066
|
* so a message arriving while any permission/elicitation card is open uses
|
|
5865
|
-
* non-interrupting SDK delivery.
|
|
6067
|
+
* non-interrupting SDK delivery. The active turn reports that it awaits the
|
|
6068
|
+
* user while any such request is open (see `syncAwaitingUser`). */
|
|
5866
6069
|
async withPendingUserInput(sessionId, request) {
|
|
5867
6070
|
const session = this.sessions[sessionId];
|
|
5868
6071
|
if (!session)
|
|
5869
6072
|
return request();
|
|
5870
6073
|
session.pendingUserInputCount = (session.pendingUserInputCount ?? 0) + 1;
|
|
6074
|
+
this.syncAwaitingUser(session);
|
|
5871
6075
|
try {
|
|
5872
6076
|
return await request();
|
|
5873
6077
|
}
|
|
5874
6078
|
finally {
|
|
5875
6079
|
session.pendingUserInputCount = Math.max(0, (session.pendingUserInputCount ?? 1) - 1);
|
|
5876
|
-
|
|
6080
|
+
this.syncAwaitingUser(session);
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
/** Report whether the session's active turn awaits the user: exactly while
|
|
6084
|
+
* a permission request or question is open in the session. A request does
|
|
6085
|
+
* not say which prompt it is for, and Claude Code can ask for a queued
|
|
6086
|
+
* prompt before the consumer activates it, so a request is not tied to the
|
|
6087
|
+
* turn that was active when it opened. Called when the count changes and
|
|
6088
|
+
* when a turn activates. */
|
|
6089
|
+
syncAwaitingUser(session) {
|
|
6090
|
+
const turn = session.activeTurn;
|
|
6091
|
+
if (!turn || turn.settled || !turn.insertedReported)
|
|
6092
|
+
return;
|
|
6093
|
+
const waiting = (session.pendingUserInputCount ?? 0) > 0;
|
|
6094
|
+
if (waiting === (turn.awaitingUser ?? false))
|
|
6095
|
+
return;
|
|
6096
|
+
turn.awaitingUser = waiting;
|
|
6097
|
+
if (waiting)
|
|
6098
|
+
turn.events.awaitingUser();
|
|
6099
|
+
else
|
|
6100
|
+
turn.events.resumed();
|
|
5877
6101
|
}
|
|
5878
6102
|
/** Forward a permission request to the client, wiring the tool call's
|
|
5879
6103
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
@@ -6237,7 +6461,7 @@ export class ClaudeAcpAgent {
|
|
|
6237
6461
|
sessionId,
|
|
6238
6462
|
update: {
|
|
6239
6463
|
sessionUpdate: "available_commands_update",
|
|
6240
|
-
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands),
|
|
6464
|
+
availableCommands: getAvailableSlashCommands(commands, session.terminalSlashCommands, this.toolCallCapabilities.air.client ? session.cwd : undefined, (session.skillPaths ??= new Map())),
|
|
6241
6465
|
},
|
|
6242
6466
|
});
|
|
6243
6467
|
}
|
|
@@ -6558,6 +6782,8 @@ export class ClaudeAcpAgent {
|
|
|
6558
6782
|
}
|
|
6559
6783
|
async getOrCreateSession(params, resumedSession) {
|
|
6560
6784
|
const existingSession = this.sessions[params.sessionId];
|
|
6785
|
+
// A recreated live session keeps its mode, not the mode of the transcript.
|
|
6786
|
+
const livePermissionMode = existingSession?.modes.currentModeId;
|
|
6561
6787
|
if (existingSession) {
|
|
6562
6788
|
const fingerprint = computeSessionFingerprint(params);
|
|
6563
6789
|
if (fingerprint === existingSession.sessionFingerprint) {
|
|
@@ -6573,9 +6799,6 @@ export class ClaudeAcpAgent {
|
|
|
6573
6799
|
// up the new values.
|
|
6574
6800
|
await this.teardownSession(params.sessionId);
|
|
6575
6801
|
}
|
|
6576
|
-
const resumedModelHint = resumedSession
|
|
6577
|
-
? resumedSession.model
|
|
6578
|
-
: await readResumedModel(params.sessionId, this.logger);
|
|
6579
6802
|
const response = await this.createSession({
|
|
6580
6803
|
cwd: params.cwd,
|
|
6581
6804
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -6583,7 +6806,8 @@ export class ClaudeAcpAgent {
|
|
|
6583
6806
|
_meta: params._meta,
|
|
6584
6807
|
}, {
|
|
6585
6808
|
resume: params.sessionId,
|
|
6586
|
-
|
|
6809
|
+
permissionMode: livePermissionMode,
|
|
6810
|
+
...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
|
|
6587
6811
|
});
|
|
6588
6812
|
return {
|
|
6589
6813
|
sessionId: response.sessionId,
|
|
@@ -6612,6 +6836,12 @@ export class ClaudeAcpAgent {
|
|
|
6612
6836
|
throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
|
|
6613
6837
|
}
|
|
6614
6838
|
}
|
|
6839
|
+
/** Whether the session forwards the text and the thinking of a subagent. */
|
|
6840
|
+
forwardsSubagentText(meta) {
|
|
6841
|
+
return (clientSupportsSubagents(this.clientCapabilities) ||
|
|
6842
|
+
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6843
|
+
meta?.claudeCode?.options?.forwardSubagentText === true);
|
|
6844
|
+
}
|
|
6615
6845
|
async createSession(params, creationOpts = {}) {
|
|
6616
6846
|
const createStartedAt = performance.now();
|
|
6617
6847
|
// Validate `cwd` up front. The ACP spec requires an absolute path, and the
|
|
@@ -6649,10 +6879,15 @@ export class ClaudeAcpAgent {
|
|
|
6649
6879
|
// sign-out respawn, provider rerouting), so fill the same fast local hint
|
|
6650
6880
|
// here when the caller did not provide it. Never fall back to the slow
|
|
6651
6881
|
// getContextUsage control request.
|
|
6882
|
+
// The same tail read gives the permission mode of the resumed session.
|
|
6652
6883
|
let resumedModelHint = creationOpts.resumedModelHint;
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6884
|
+
let resumedPermissionMode;
|
|
6885
|
+
if (creationOpts.resume !== undefined) {
|
|
6886
|
+
const withModel = !Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint");
|
|
6887
|
+
const tail = await readResumedTail(creationOpts.resume, this.logger, withModel);
|
|
6888
|
+
if (withModel)
|
|
6889
|
+
resumedModelHint = tail.model;
|
|
6890
|
+
resumedPermissionMode = tail.permissionMode;
|
|
6656
6891
|
timing.phase("resume-transcript");
|
|
6657
6892
|
}
|
|
6658
6893
|
const input = new Pushable();
|
|
@@ -6714,7 +6949,11 @@ export class ClaudeAcpAgent {
|
|
|
6714
6949
|
const allowBypass = ALLOW_BYPASS &&
|
|
6715
6950
|
settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
|
|
6716
6951
|
sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
|
|
6717
|
-
const initialPermissionMode =
|
|
6952
|
+
const initialPermissionMode = resolveInitialPermissionMode({
|
|
6953
|
+
explicit: creationOpts.permissionMode,
|
|
6954
|
+
resumed: resumedPermissionMode,
|
|
6955
|
+
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
|
6956
|
+
}, this.logger, allowBypass);
|
|
6718
6957
|
const userProvidedOptions = sessionMeta?.claudeCode?.options
|
|
6719
6958
|
? { ...sessionMeta.claudeCode.options }
|
|
6720
6959
|
: undefined;
|
|
@@ -6723,9 +6962,7 @@ export class ClaudeAcpAgent {
|
|
|
6723
6962
|
// hidden state that the Client cannot inspect or change.
|
|
6724
6963
|
if (userProvidedOptions)
|
|
6725
6964
|
delete userProvidedOptions.agent;
|
|
6726
|
-
const forwardSubagentText =
|
|
6727
|
-
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
6728
|
-
userProvidedOptions?.forwardSubagentText === true;
|
|
6965
|
+
const forwardSubagentText = this.forwardsSubagentText(params._meta);
|
|
6729
6966
|
// Configure thinking behavior from environment variable
|
|
6730
6967
|
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
6731
6968
|
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
@@ -6751,7 +6988,7 @@ export class ClaudeAcpAgent {
|
|
|
6751
6988
|
// Per-session task state. Created here (rather than in the session record
|
|
6752
6989
|
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
6753
6990
|
// the same Map that the streaming message handler will read from.
|
|
6754
|
-
const taskState = new Map();
|
|
6991
|
+
const taskState = creationOpts.replayState?.taskState ?? new Map();
|
|
6755
6992
|
// Resolve every workspace root once. The native checkpoint report uses
|
|
6756
6993
|
// this same set for lexical path validation, and the SDK receives it below.
|
|
6757
6994
|
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
@@ -7044,7 +7281,7 @@ export class ClaudeAcpAgent {
|
|
|
7044
7281
|
const allowedModels = Array.isArray(settingsAvailableModels)
|
|
7045
7282
|
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
|
|
7046
7283
|
: initializationResult.models;
|
|
7047
|
-
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, resumedModelHint);
|
|
7284
|
+
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
|
|
7048
7285
|
timing.phase("models");
|
|
7049
7286
|
// Resolve the current model's capabilities separately from the stable
|
|
7050
7287
|
// permission-mode catalog advertised to ACP clients.
|
|
@@ -7193,8 +7430,8 @@ export class ClaudeAcpAgent {
|
|
|
7193
7430
|
nativeSubagentParentByToolUseId: new Map(),
|
|
7194
7431
|
emittedAssistantText: false,
|
|
7195
7432
|
owedTrailingIdles: 0,
|
|
7196
|
-
messageIdToUuid: new Map(),
|
|
7197
|
-
sessionFailureState: createSessionFailureState(),
|
|
7433
|
+
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7434
|
+
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7198
7435
|
claudeSubscriptionGuard,
|
|
7199
7436
|
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
7200
7437
|
fileChangeReporter,
|
|
@@ -7235,7 +7472,10 @@ export class ClaudeAcpAgent {
|
|
|
7235
7472
|
this.closeQueryStream(session);
|
|
7236
7473
|
delete this.sessions[sessionId];
|
|
7237
7474
|
try {
|
|
7238
|
-
await this.createSession(session.creationParams, {
|
|
7475
|
+
await this.createSession(session.creationParams, {
|
|
7476
|
+
resume: sessionId,
|
|
7477
|
+
permissionMode: session.modes.currentModeId,
|
|
7478
|
+
});
|
|
7239
7479
|
}
|
|
7240
7480
|
catch (error) {
|
|
7241
7481
|
// One session that cannot come back must not abort the switch. The
|
|
@@ -7329,6 +7569,20 @@ function turnOutcome(session, stopReason, extraMeta) {
|
|
|
7329
7569
|
_meta: { ...turnQuotaMeta(session), ...extraMeta },
|
|
7330
7570
|
};
|
|
7331
7571
|
}
|
|
7572
|
+
/** The outcome of `turn` when a cancel ends it. A turn that already has its
|
|
7573
|
+
* result, because it is held open for its background subagents (see
|
|
7574
|
+
* Turn.deferredSettle) or is settling (see Turn.settlingOutcome), keeps the
|
|
7575
|
+
* usage and metadata that result recorded (issue #844). */
|
|
7576
|
+
function cancelledOutcome(session, turn) {
|
|
7577
|
+
const recorded = turn?.deferredSettle ?? turn?.settlingOutcome;
|
|
7578
|
+
if (!recorded)
|
|
7579
|
+
return turnOutcome(session, "cancelled");
|
|
7580
|
+
return {
|
|
7581
|
+
stopReason: "cancelled",
|
|
7582
|
+
usage: recorded.usage,
|
|
7583
|
+
...(recorded._meta ? { _meta: recorded._meta } : {}),
|
|
7584
|
+
};
|
|
7585
|
+
}
|
|
7332
7586
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
7333
7587
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
7334
7588
|
* reads one shape from either agent.
|
|
@@ -7716,7 +7970,20 @@ function getAvailableSlashCommands(commands,
|
|
|
7716
7970
|
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
|
|
7717
7971
|
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
|
|
7718
7972
|
// the static list. Raw CLI names, matched before the MCP rename.
|
|
7719
|
-
terminalCommands
|
|
7973
|
+
terminalCommands,
|
|
7974
|
+
// The session cwd of an AIR client, or undefined for every other client.
|
|
7975
|
+
// AIR gets the kind and the SKILL.md path of each skill command.
|
|
7976
|
+
airSkillCwd,
|
|
7977
|
+
// The resolved SKILL.md paths of the session, keyed by the cwd and the name.
|
|
7978
|
+
skillPaths) {
|
|
7979
|
+
const skillPath = (name, cwd) => {
|
|
7980
|
+
const key = `${cwd}\0${name}`;
|
|
7981
|
+
if (skillPaths?.has(key))
|
|
7982
|
+
return skillPaths.get(key);
|
|
7983
|
+
const resolved = resolveSkillPath(name, cwd);
|
|
7984
|
+
skillPaths?.set(key, resolved);
|
|
7985
|
+
return resolved;
|
|
7986
|
+
};
|
|
7720
7987
|
const UNSUPPORTED_COMMANDS = [
|
|
7721
7988
|
"clear",
|
|
7722
7989
|
"cost",
|
|
@@ -7727,8 +7994,10 @@ terminalCommands) {
|
|
|
7727
7994
|
"release-notes",
|
|
7728
7995
|
"todos",
|
|
7729
7996
|
];
|
|
7730
|
-
|
|
7731
|
-
|
|
7997
|
+
// The adapter replaces the terminal text of `/mcp` and runs its actions,
|
|
7998
|
+
// even when Claude Code tags the command terminal-bound.
|
|
7999
|
+
const advertised = commands
|
|
8000
|
+
.filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
|
|
7732
8001
|
.map((command) => {
|
|
7733
8002
|
const input = command.argumentHint
|
|
7734
8003
|
? {
|
|
@@ -7738,16 +8007,26 @@ terminalCommands) {
|
|
|
7738
8007
|
}
|
|
7739
8008
|
: null;
|
|
7740
8009
|
let name = command.name;
|
|
7741
|
-
|
|
8010
|
+
const mcpPrompt = command.name.endsWith(" (MCP)");
|
|
8011
|
+
if (mcpPrompt) {
|
|
7742
8012
|
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
7743
8013
|
}
|
|
8014
|
+
const skillMdPath = airSkillCwd && !command.builtin && !mcpPrompt
|
|
8015
|
+
? skillPath(command.name, airSkillCwd)
|
|
8016
|
+
: undefined;
|
|
7744
8017
|
return {
|
|
7745
8018
|
name,
|
|
7746
8019
|
description: command.description || "",
|
|
7747
8020
|
input,
|
|
8021
|
+
...(skillMdPath
|
|
8022
|
+
? {
|
|
8023
|
+
_meta: withAirMeta(withAirMeta(undefined, AIR_KIND_KEY, "skill"), AIR_SKILL_PATH_KEY, skillMdPath),
|
|
8024
|
+
}
|
|
8025
|
+
: {}),
|
|
7748
8026
|
};
|
|
7749
8027
|
})
|
|
7750
8028
|
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
8029
|
+
return [...advertised, MCP_AVAILABLE_COMMAND];
|
|
7751
8030
|
}
|
|
7752
8031
|
function formatUriAsLink(uri) {
|
|
7753
8032
|
try {
|