@agentclientprotocol/claude-agent-acp 0.99.2-preview.1 → 0.99.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 +8 -0
- package/dist/acp-agent.d.ts +60 -18
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +325 -100
- package/dist/diff.d.ts +1 -1
- package/dist/diff.js +5 -5
- package/dist/elicitation.d.ts +1 -2
- package/dist/elicitation.d.ts.map +1 -1
- package/dist/elicitation.js +34 -16
- package/dist/fork-session.d.ts.map +1 -1
- package/dist/fork-session.js +23 -0
- package/dist/native-subagents.d.ts +49 -3
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +118 -4
- package/dist/session-index/service.d.ts +3 -1
- package/dist/session-index/service.d.ts.map +1 -1
- package/dist/session-index/service.js +5 -10
- package/dist/startup-failure.d.ts +16 -0
- package/dist/startup-failure.d.ts.map +1 -0
- package/dist/startup-failure.js +38 -0
- package/dist/subagent-history.d.ts +8 -1
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +40 -1
- package/dist/subagent-replay.d.ts +19 -3
- package/dist/subagent-replay.d.ts.map +1 -1
- package/dist/subagent-replay.js +46 -4
- package/dist/tool-calls/reporters/interaction.d.ts.map +1 -1
- package/dist/tool-calls/reporters/interaction.js +8 -3
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +42 -17
- package/dist/v2/initialize.d.ts +16 -1
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +26 -3
- package/dist/v2/permission.d.ts +5 -2
- package/dist/v2/permission.d.ts.map +1 -1
- package/dist/v2/permission.js +5 -2
- package/dist/v2/prompt.d.ts +2 -1
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +5 -1
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +21 -9
- package/package.json +6 -6
package/dist/acp-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agent as acpAgent,
|
|
1
|
+
import { agent as acpAgent, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk/unstable";
|
|
2
2
|
import { filterEscalatingDefaultMode, getSessionMessages, listSessions, query, resolveSettings, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFromPrompt, parseGoalRequest, toGoalSnapshot, } from "./goal-extension.js";
|
|
4
4
|
import { blobResourceToClaude } from "./blob-resources.js";
|
|
@@ -55,6 +55,7 @@ import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableMode
|
|
|
55
55
|
import { GatewayListingError, listBedrockModels, listGatewayModels, PROVIDER_AUTH_TOKEN_PLACEHOLDER, selectBedrockRegion, } from "./gateway-models.js";
|
|
56
56
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, effortLevelName, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
57
57
|
import { createUltracodeConfigOption, readUltracodeSettings, resolveUltracodeEnabled, ULTRACODE_CONFIG_ID, ultracodeSupported, ultracodeWorkflowsEnabled, } from "./session-ultracode.js";
|
|
58
|
+
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "./startup-failure.js";
|
|
58
59
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
59
60
|
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
60
61
|
const execFileAsync = promisify(execFile);
|
|
@@ -512,6 +513,16 @@ export function computeSessionFingerprint(params) {
|
|
|
512
513
|
options,
|
|
513
514
|
}));
|
|
514
515
|
}
|
|
516
|
+
/** `params` without `_meta.claudeCode.options.effort`. */
|
|
517
|
+
function withoutEffort(params) {
|
|
518
|
+
const meta = params._meta;
|
|
519
|
+
if (meta?.claudeCode?.options?.effort === undefined) {
|
|
520
|
+
return params;
|
|
521
|
+
}
|
|
522
|
+
const options = { ...meta.claudeCode.options };
|
|
523
|
+
delete options.effort;
|
|
524
|
+
return { ...params, _meta: { ...meta, claudeCode: { ...meta.claudeCode, options } } };
|
|
525
|
+
}
|
|
515
526
|
/** The JSON-RPC code ACP assigns to `authRequired`. Read from the SDK so it
|
|
516
527
|
* cannot drift from the errors this agent throws. */
|
|
517
528
|
const AUTH_REQUIRED_CODE = RequestError.authRequired().code;
|
|
@@ -944,6 +955,7 @@ class ClientConnection {
|
|
|
944
955
|
requestPermission(params, signal) {
|
|
945
956
|
return this.ctx.request(methods.client.session.requestPermission, params, {
|
|
946
957
|
cancellationSignal: signal,
|
|
958
|
+
stopWaitingOnCancel: true,
|
|
947
959
|
});
|
|
948
960
|
}
|
|
949
961
|
readTextFile(params) {
|
|
@@ -955,6 +967,7 @@ class ClientConnection {
|
|
|
955
967
|
createElicitation(params, signal) {
|
|
956
968
|
return this.ctx.request(methods.client.elicitation.create, params, {
|
|
957
969
|
cancellationSignal: signal,
|
|
970
|
+
stopWaitingOnCancel: true,
|
|
958
971
|
});
|
|
959
972
|
}
|
|
960
973
|
completeElicitation(params) {
|
|
@@ -1110,7 +1123,10 @@ async function authenticateMcpServer(host, sessionId, query, serverName) {
|
|
|
1110
1123
|
completed.then((authenticated) => ({ type: "completed", authenticated })),
|
|
1111
1124
|
elicitation.then((response) => ({ type: "elicitation", response })),
|
|
1112
1125
|
]);
|
|
1113
|
-
|
|
1126
|
+
// A url-mode accept carries no content to check. The guard
|
|
1127
|
+
// `CreateElicitationResponse.isAccept` would also reject an accept whose
|
|
1128
|
+
// `content` holds a v2-only value, such as an object.
|
|
1129
|
+
if (first.type === "elicitation" && first.response.action !== "accept") {
|
|
1114
1130
|
return;
|
|
1115
1131
|
}
|
|
1116
1132
|
if (first.type === "elicitation") {
|
|
@@ -1159,6 +1175,8 @@ export class ClaudeAcpAgent {
|
|
|
1159
1175
|
providerConfig;
|
|
1160
1176
|
/** Serializes provider changes while every open query is recreated between turns. */
|
|
1161
1177
|
providerUpdate = null;
|
|
1178
|
+
/** Per session: queue that runs mode/config changes and query restarts one at a time. */
|
|
1179
|
+
configurationChanges = new Map();
|
|
1162
1180
|
exitPlan;
|
|
1163
1181
|
/** Last auth identity reported to the client, connection-scoped like
|
|
1164
1182
|
* `authenticate`/`logout`. Undefined means "not determined yet". */
|
|
@@ -1207,6 +1225,12 @@ export class ClaudeAcpAgent {
|
|
|
1207
1225
|
replayedChildModels = new Map();
|
|
1208
1226
|
/** See the `foreground` constructor option. */
|
|
1209
1227
|
foregroundEvents;
|
|
1228
|
+
/** Counts the prompts `startTurn()` has received, across sessions. */
|
|
1229
|
+
promptsReceived = 0;
|
|
1230
|
+
/** Per session, the `promptsReceived` count when its last cancel started.
|
|
1231
|
+
* A prompt received before that cancel that `startTurn()` had not queued
|
|
1232
|
+
* yet ends as cancelled instead of being queued. */
|
|
1233
|
+
cancelledThrough = new Map();
|
|
1210
1234
|
/**
|
|
1211
1235
|
* The foreground work of each session while it runs, by the session id that
|
|
1212
1236
|
* the client knows, on every connection: only the `foreground` option
|
|
@@ -1490,7 +1514,7 @@ export class ClaudeAcpAgent {
|
|
|
1490
1514
|
* before the first update. A failed CLI start stops the replay.
|
|
1491
1515
|
*/
|
|
1492
1516
|
async createSessionWhileReplaying(params, timing) {
|
|
1493
|
-
await this.validateCwd(params.cwd);
|
|
1517
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
1494
1518
|
clientSystemPrompt(params.systemPrompt);
|
|
1495
1519
|
const pending = {
|
|
1496
1520
|
state: {
|
|
@@ -1548,7 +1572,8 @@ export class ClaudeAcpAgent {
|
|
|
1548
1572
|
async unstable_forkSession(params) {
|
|
1549
1573
|
while (this.providerUpdate)
|
|
1550
1574
|
await this.providerUpdate;
|
|
1551
|
-
// Reject a
|
|
1575
|
+
// Reject a bad `cwd` or prompt before copying the transcript.
|
|
1576
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
1552
1577
|
clientSystemPrompt(params.systemPrompt);
|
|
1553
1578
|
const { sessionId } = await forkSession(params, {
|
|
1554
1579
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
@@ -1607,13 +1632,19 @@ export class ClaudeAcpAgent {
|
|
|
1607
1632
|
* reads the start and the end only of the transcripts of the page.
|
|
1608
1633
|
*/
|
|
1609
1634
|
async listSessions(params) {
|
|
1635
|
+
if (params.cwd != null && !path.isAbsolute(params.cwd)) {
|
|
1636
|
+
throw RequestError.invalidParams({ cwd: params.cwd }, `\`cwd\` must be an absolute path, but received: ${params.cwd}`);
|
|
1637
|
+
}
|
|
1610
1638
|
const indexed = this.sessionIndex.list(params);
|
|
1611
1639
|
if (indexed)
|
|
1612
1640
|
return indexed;
|
|
1613
1641
|
const offset = sessionListOffset(params.cursor);
|
|
1614
1642
|
// One more session than the page tells whether a next page exists.
|
|
1643
|
+
// `includeWorktrees: false`: a `cwd` filter matches sessions of exactly
|
|
1644
|
+
// that directory, not of the other git worktrees of its repository.
|
|
1615
1645
|
const sdkSessions = await listSessions({
|
|
1616
1646
|
dir: params.cwd ?? undefined,
|
|
1647
|
+
includeWorktrees: false,
|
|
1617
1648
|
limit: SESSION_LIST_PAGE_SIZE + 1,
|
|
1618
1649
|
offset,
|
|
1619
1650
|
});
|
|
@@ -1689,7 +1720,7 @@ export class ClaudeAcpAgent {
|
|
|
1689
1720
|
this.setAuthStatus(gatewayAuthStatus(gatewayRequestToProviderConfig(this.gatewayAuthRequest)?.baseUrl));
|
|
1690
1721
|
return;
|
|
1691
1722
|
}
|
|
1692
|
-
throw
|
|
1723
|
+
throw RequestError.invalidParams({ methodId: _params.methodId }, `Unsupported authentication method: ${_params.methodId}`);
|
|
1693
1724
|
}
|
|
1694
1725
|
/**
|
|
1695
1726
|
* Stores `next` and pushes it to the client.
|
|
@@ -2131,8 +2162,18 @@ export class ClaudeAcpAgent {
|
|
|
2131
2162
|
/**
|
|
2132
2163
|
* Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
|
|
2133
2164
|
* outcome of a turn is field for field a v1 prompt response.
|
|
2165
|
+
*
|
|
2166
|
+
* A content block that the agent does not advertise, such as `audio`, fails
|
|
2167
|
+
* the request with invalid params (-32602) before anything is queued:
|
|
2168
|
+
* `promptToClaude` would drop it. v2 prompts reach `startTurn` through
|
|
2169
|
+
* `v1PromptRequest`, which rejects these blocks itself.
|
|
2134
2170
|
*/
|
|
2135
2171
|
async prompt(params) {
|
|
2172
|
+
for (const block of params.prompt) {
|
|
2173
|
+
if (!SUPPORTED_PROMPT_CONTENT_TYPES.includes(block.type)) {
|
|
2174
|
+
throw RequestError.invalidParams({ type: block.type }, `Content blocks of type ${block.type} are not supported`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2136
2177
|
let events;
|
|
2137
2178
|
const outcome = new Promise((resolve, reject) => {
|
|
2138
2179
|
events = { inserted() { }, ended: resolve, failed: reject };
|
|
@@ -2147,6 +2188,7 @@ export class ClaudeAcpAgent {
|
|
|
2147
2188
|
* before this resolves.
|
|
2148
2189
|
*/
|
|
2149
2190
|
async startTurn(params, events) {
|
|
2191
|
+
const received = ++this.promptsReceived;
|
|
2150
2192
|
while (this.providerUpdate)
|
|
2151
2193
|
await this.providerUpdate;
|
|
2152
2194
|
let session = this.sessions[params.sessionId];
|
|
@@ -2164,8 +2206,16 @@ export class ClaudeAcpAgent {
|
|
|
2164
2206
|
this.useAuthStatusSettings(session.settingsManager, session.effortSettingsOverride, session.creationParams?._meta?.claudeCode?.options?.env);
|
|
2165
2207
|
void this.probeCliAuthStatus();
|
|
2166
2208
|
const signOutRespawn = this.respawnSignedOutSession(params.sessionId, session);
|
|
2167
|
-
if (signOutRespawn)
|
|
2168
|
-
|
|
2209
|
+
if (signOutRespawn) {
|
|
2210
|
+
await signOutRespawn;
|
|
2211
|
+
// A provider update can queue its replacement behind the sign-out respawn.
|
|
2212
|
+
// Wait for that replacement too before a prompt starts on the old route.
|
|
2213
|
+
while (this.providerUpdate)
|
|
2214
|
+
await this.providerUpdate;
|
|
2215
|
+
session = this.sessions[params.sessionId];
|
|
2216
|
+
if (!session)
|
|
2217
|
+
throw RequestError.sessionNotFound(params.sessionId);
|
|
2218
|
+
}
|
|
2169
2219
|
// The SDK query stream already terminated (see `queryClosed`); its iterator
|
|
2170
2220
|
// can't be revived, so enqueueing here would hang on a deferred that never
|
|
2171
2221
|
// settles. Fail clearly and let the client start a fresh session.
|
|
@@ -2252,7 +2302,9 @@ export class ClaudeAcpAgent {
|
|
|
2252
2302
|
// its consumer is gone, so the prompt would never be inserted. End it as
|
|
2253
2303
|
// the close ends a queued turn: `cancelled` on v1, -32800 on v2. A stream
|
|
2254
2304
|
// that ended meanwhile fails the prompt as the check above does.
|
|
2255
|
-
|
|
2305
|
+
// A `session/cancel` that arrived after this prompt, during the awaits
|
|
2306
|
+
// above, ends it the same way.
|
|
2307
|
+
if (session.tornDown || received <= (this.cancelledThrough.get(params.sessionId) ?? 0)) {
|
|
2256
2308
|
session.fileChangeReporter?.finish(fileChangeReport, "cancelled");
|
|
2257
2309
|
events.ended({ stopReason: "cancelled" });
|
|
2258
2310
|
return;
|
|
@@ -2266,6 +2318,7 @@ export class ClaudeAcpAgent {
|
|
|
2266
2318
|
// the consumer is running.
|
|
2267
2319
|
let completeTurn;
|
|
2268
2320
|
const turn = {
|
|
2321
|
+
received,
|
|
2269
2322
|
promptUuid,
|
|
2270
2323
|
events,
|
|
2271
2324
|
isLocalOnlyCommand,
|
|
@@ -2457,36 +2510,29 @@ export class ClaudeAcpAgent {
|
|
|
2457
2510
|
});
|
|
2458
2511
|
}
|
|
2459
2512
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
// Already closed by `markSessionForSignOutRespawn`; idempotent here so a
|
|
2467
|
-
// husk that reached this by another route still releases its resources.
|
|
2468
|
-
this.closeQueryStream(session);
|
|
2469
|
-
try {
|
|
2470
|
-
// The very first turn may be the one that signed out, so the CLI may
|
|
2471
|
-
// never have written the conversation.
|
|
2472
|
-
await this.recreateLiveSession(creationParams, sessionId, {
|
|
2473
|
-
permissionMode: session.modes.currentModeId,
|
|
2474
|
-
});
|
|
2475
|
-
// The new Session has the same MCP servers; keep the ones already
|
|
2476
|
-
// reported so its first `system`/init doesn't repeat their notices.
|
|
2477
|
-
const recreated = this.sessions[sessionId];
|
|
2478
|
-
if (recreated && session.reportedMcpServers) {
|
|
2479
|
-
recreated.reportedMcpServers = new Set(session.reportedMcpServers);
|
|
2513
|
+
/** Queued per session, so a choice sent during the respawn lands on the new query. */
|
|
2514
|
+
recreateSignedOutQuery(sessionId, session) {
|
|
2515
|
+
return this.serializeConfigurationChange(sessionId, async () => {
|
|
2516
|
+
const creationParams = session.creationParams;
|
|
2517
|
+
if (!creationParams) {
|
|
2518
|
+
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
|
2480
2519
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2520
|
+
// Already replaced by a provider restart, or closed.
|
|
2521
|
+
if (this.sessions[sessionId] !== session)
|
|
2522
|
+
return;
|
|
2523
|
+
this.logger.log(`Recreating Claude session ${sessionId} after a sign-out`);
|
|
2524
|
+
try {
|
|
2525
|
+
// The signed-out turn may be the first, so the conversation may not be saved yet.
|
|
2526
|
+
await this.replaceSessionQuery(sessionId, session, creationParams);
|
|
2487
2527
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2528
|
+
catch (error) {
|
|
2529
|
+
// Keep the husk so the client can sign in and retry; a closed session stays closed.
|
|
2530
|
+
if (!session.tornDown && !this.sessions[sessionId]) {
|
|
2531
|
+
this.sessions[sessionId] = session;
|
|
2532
|
+
}
|
|
2533
|
+
throw error;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2490
2536
|
}
|
|
2491
2537
|
/**
|
|
2492
2538
|
* Starts a new query for the session `sessionId`, which ran on this
|
|
@@ -2510,6 +2556,7 @@ export class ClaudeAcpAgent {
|
|
|
2510
2556
|
return await this.createSession(params, {
|
|
2511
2557
|
reuseSessionId: sessionId,
|
|
2512
2558
|
permissionMode: options.permissionMode,
|
|
2559
|
+
replacingSession: options.replacingSession,
|
|
2513
2560
|
});
|
|
2514
2561
|
}
|
|
2515
2562
|
}
|
|
@@ -6442,6 +6489,12 @@ export class ClaudeAcpAgent {
|
|
|
6442
6489
|
return;
|
|
6443
6490
|
}
|
|
6444
6491
|
session.cancelled = true;
|
|
6492
|
+
// Only prompts received before this cancel are cancelled. A prompt that
|
|
6493
|
+
// arrives while the awaits below run is queued and runs after the cancel
|
|
6494
|
+
// (ACP v2: a `session/prompt` received after `session/cancel` is not
|
|
6495
|
+
// cancelled by it).
|
|
6496
|
+
const cancelledThrough = this.promptsReceived;
|
|
6497
|
+
this.cancelledThrough.set(params.sessionId, cancelledThrough);
|
|
6445
6498
|
// The interrupt below makes Claude Code withdraw the requests that are open
|
|
6446
6499
|
// now (see `withPendingUserInput`).
|
|
6447
6500
|
for (const input of session.openUserInputs ?? [])
|
|
@@ -6450,7 +6503,7 @@ export class ClaudeAcpAgent {
|
|
|
6450
6503
|
// the meantime is already judged against these prompts.
|
|
6451
6504
|
session.cancelledPrompts ??= new Set();
|
|
6452
6505
|
for (const turn of session.turnQueue ?? []) {
|
|
6453
|
-
if (turn.settled)
|
|
6506
|
+
if (turn.settled || turn.received > cancelledThrough)
|
|
6454
6507
|
continue;
|
|
6455
6508
|
session.cancelledPrompts.add(turn.promptUuid);
|
|
6456
6509
|
for (const uuid of turn.steeredUuids ?? [])
|
|
@@ -6525,7 +6578,7 @@ export class ClaudeAcpAgent {
|
|
|
6525
6578
|
const orphanedTurns = [];
|
|
6526
6579
|
if (session.turnQueue) {
|
|
6527
6580
|
for (const turn of session.turnQueue) {
|
|
6528
|
-
if (turn !== session.activeTurn && !turn.settled) {
|
|
6581
|
+
if (turn !== session.activeTurn && !turn.settled && turn.received <= cancelledThrough) {
|
|
6529
6582
|
session.fileChangeReporter?.finish(turn.fileChangeReport, "cancelled");
|
|
6530
6583
|
turn.settled = true;
|
|
6531
6584
|
// Deliberately no `usage`: a queued turn never ran, so the session
|
|
@@ -6579,7 +6632,7 @@ export class ClaudeAcpAgent {
|
|
|
6579
6632
|
}
|
|
6580
6633
|
this.trackOrphanCommand(session, turn.promptUuid, turn.commandStarted ? "started" : "pending");
|
|
6581
6634
|
}
|
|
6582
|
-
session.turnQueue = session.turnQueue.filter((turn) =>
|
|
6635
|
+
session.turnQueue = session.turnQueue.filter((turn) => !turn.settled);
|
|
6583
6636
|
}
|
|
6584
6637
|
const diagnosticTurns = orphanedTurns.filter((turn) => {
|
|
6585
6638
|
if (turn.commandFinished === "completed" ||
|
|
@@ -6702,8 +6755,8 @@ export class ClaudeAcpAgent {
|
|
|
6702
6755
|
}
|
|
6703
6756
|
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
6704
6757
|
// the next queued message right away.
|
|
6705
|
-
// A
|
|
6706
|
-
//
|
|
6758
|
+
// A steer that arrived while this cancel awaited is abandoned too, after
|
|
6759
|
+
// the record above was taken.
|
|
6707
6760
|
for (const uuid of abandoned)
|
|
6708
6761
|
(session.cancelledPrompts ??= new Set()).add(uuid);
|
|
6709
6762
|
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
@@ -6853,6 +6906,7 @@ export class ClaudeAcpAgent {
|
|
|
6853
6906
|
session.nativeSubagentRuntime?.clear();
|
|
6854
6907
|
session.asyncTaskRuntime?.clear();
|
|
6855
6908
|
this.replayedChildModels.delete(sessionId);
|
|
6909
|
+
this.cancelledThrough.delete(sessionId);
|
|
6856
6910
|
delete this.sessions[sessionId];
|
|
6857
6911
|
this.sessionIndex.onTeardown(sessionId);
|
|
6858
6912
|
const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
|
|
@@ -6895,10 +6949,13 @@ export class ClaudeAcpAgent {
|
|
|
6895
6949
|
async deleteSession(params) {
|
|
6896
6950
|
return this.sessionIndex.deleteSession(params);
|
|
6897
6951
|
}
|
|
6898
|
-
|
|
6899
|
-
return this.sessionModes.setSessionMode(params);
|
|
6952
|
+
setSessionMode(params) {
|
|
6953
|
+
return this.serializeConfigurationChange(params.sessionId, () => this.sessionModes.setSessionMode(params));
|
|
6954
|
+
}
|
|
6955
|
+
setSessionConfigOption(params) {
|
|
6956
|
+
return this.serializeConfigurationChange(params.sessionId, () => this.selectSessionConfigOption(params));
|
|
6900
6957
|
}
|
|
6901
|
-
async
|
|
6958
|
+
async selectSessionConfigOption(params) {
|
|
6902
6959
|
const session = this.sessions[params.sessionId];
|
|
6903
6960
|
if (!session) {
|
|
6904
6961
|
throw RequestError.sessionNotFound(params.sessionId);
|
|
@@ -6914,6 +6971,13 @@ export class ClaudeAcpAgent {
|
|
|
6914
6971
|
if (!option) {
|
|
6915
6972
|
throw RequestError.invalidParams(undefined, `Unknown config option: ${params.configId}`);
|
|
6916
6973
|
}
|
|
6974
|
+
// The value must match the type the option was advertised with: a
|
|
6975
|
+
// `type: "boolean"` option takes `true`/`false`, not "on"/"off", and a
|
|
6976
|
+
// `type: "select"` option takes a string.
|
|
6977
|
+
const expectedType = option.type === "boolean" ? "boolean" : "string";
|
|
6978
|
+
if (typeof params.value !== expectedType) {
|
|
6979
|
+
throw RequestError.invalidParams({ configId: params.configId, value: params.value }, `Config option ${params.configId} takes a ${expectedType} value, but received: ${JSON.stringify(params.value)}`);
|
|
6980
|
+
}
|
|
6917
6981
|
// Fast mode and ultracode carry a boolean value (for Clients that opted
|
|
6918
6982
|
// into boolean config options) or the "on"/"off" select fallback, so they
|
|
6919
6983
|
// bypass the
|
|
@@ -7131,7 +7195,7 @@ export class ClaudeAcpAgent {
|
|
|
7131
7195
|
? await subagentHistory(sessionId)
|
|
7132
7196
|
: { agents: new Map() };
|
|
7133
7197
|
const rfdReplay = form === "rfd"
|
|
7134
|
-
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.subagentModels(sessionId), !pending)
|
|
7198
|
+
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.subagentModels(sessionId), !pending, session?.cwd)
|
|
7135
7199
|
: undefined;
|
|
7136
7200
|
rfdReplay?.scan(messages, sessionId);
|
|
7137
7201
|
// History persists no task_started frame, so a background task gets its
|
|
@@ -7678,10 +7742,11 @@ export class ClaudeAcpAgent {
|
|
|
7678
7742
|
/** Forward a permission request to the client, wiring the tool call's
|
|
7679
7743
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
7680
7744
|
* while the client's prompt is still open the signal aborts, the SDK sends
|
|
7681
|
-
* `$/cancel_request
|
|
7682
|
-
*
|
|
7683
|
-
*
|
|
7684
|
-
async requestPermissionFromClient(params, toolName,
|
|
7745
|
+
* `$/cancel_request` and stops waiting for the client's answer. The
|
|
7746
|
+
* rejection surfaces as the "Tool use aborted" that the callers expect.
|
|
7747
|
+
* `canUseTool` decides what a `cancelled` outcome means. */
|
|
7748
|
+
async requestPermissionFromClient(params, toolName, toolSignal, parentToolUseId, ownerSessionId, agentID, toolInput, previewContent) {
|
|
7749
|
+
const signal = this.untilSessionClosed(ownerSessionId, toolSignal);
|
|
7685
7750
|
if (signal.aborted)
|
|
7686
7751
|
throw new Error("Tool use aborted");
|
|
7687
7752
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -7693,13 +7758,10 @@ export class ClaudeAcpAgent {
|
|
|
7693
7758
|
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId, toolInput, parentToolUseId, signal, params.sessionId, previewContent);
|
|
7694
7759
|
if (signal.aborted)
|
|
7695
7760
|
throw new Error("Tool use aborted");
|
|
7696
|
-
// Do not rely on every ACP client settling requestPermission after the
|
|
7697
|
-
// cancellation signal. The local race guarantees that Claude's tool call
|
|
7698
|
-
// is released even when an older or broken client ignores $/cancel_request.
|
|
7699
7761
|
// The request goes to a subagent's child session for a tool call of that
|
|
7700
7762
|
// subagent, but it blocks the turn of the owner session.
|
|
7701
7763
|
try {
|
|
7702
|
-
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () =>
|
|
7764
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () => this.client.requestPermission(params, signal), agentID));
|
|
7703
7765
|
}
|
|
7704
7766
|
catch (error) {
|
|
7705
7767
|
if (signal.aborted) {
|
|
@@ -7709,15 +7771,23 @@ export class ClaudeAcpAgent {
|
|
|
7709
7771
|
}
|
|
7710
7772
|
}
|
|
7711
7773
|
/** Send the client an elicitation for a callback of Claude Code. When the
|
|
7712
|
-
* callback's `signal` aborts, the SDK sends `$/cancel_request` and
|
|
7713
|
-
* waiting for
|
|
7714
|
-
* never sends.
|
|
7715
|
-
* {@link
|
|
7716
|
-
*
|
|
7717
|
-
* {@link withPendingUserInput}), which keeps steering non-interrupting, and
|
|
7718
|
-
* a child's `openRequests` would not drop. */
|
|
7774
|
+
* callback's `signal` aborts, the SDK sends `$/cancel_request` and the call
|
|
7775
|
+
* rejects at once, without waiting for a reply that a client ignoring the
|
|
7776
|
+
* cancel never sends. Left open, the request would stay in
|
|
7777
|
+
* `openUserInputs` (see {@link withPendingUserInput}), which keeps steering
|
|
7778
|
+
* non-interrupting, and a child's `openRequests` would not drop. */
|
|
7719
7779
|
elicitFromClient(request, signal) {
|
|
7720
|
-
return
|
|
7780
|
+
return this.client.createElicitation(request, signal);
|
|
7781
|
+
}
|
|
7782
|
+
/** `signal`, also aborted when `teardownSession` aborts the
|
|
7783
|
+
* `abortController` of session `sessionId`. Claude Code aborts a callback's
|
|
7784
|
+
* own signal when its query closes, but not necessarily before
|
|
7785
|
+
* `session/close` responds, and v2 requires every pending permission
|
|
7786
|
+
* request and elicitation of the session to be cancelled with
|
|
7787
|
+
* `$/cancel_request` before that response. */
|
|
7788
|
+
untilSessionClosed(sessionId, signal) {
|
|
7789
|
+
const sessionSignal = this.sessions[sessionId]?.abortController?.signal;
|
|
7790
|
+
return sessionSignal ? AbortSignal.any([signal, sessionSignal]) : signal;
|
|
7721
7791
|
}
|
|
7722
7792
|
/** Settle the tool call `ensureToolCallEmitted` emitted for a
|
|
7723
7793
|
* SandboxNetworkAccess request once the request is answered. The callback
|
|
@@ -7852,6 +7922,11 @@ export class ClaudeAcpAgent {
|
|
|
7852
7922
|
const session = this.sessions[sessionId];
|
|
7853
7923
|
if (!session || !input.agent_id)
|
|
7854
7924
|
return { continue: true };
|
|
7925
|
+
// A hook's `cwd` is the child's working directory, e.g. its worktree.
|
|
7926
|
+
// Reported before the hook returns, so before the work that follows it.
|
|
7927
|
+
await session.nativeSubagentRuntime
|
|
7928
|
+
?.reportCwd(input.agent_id, input.cwd)
|
|
7929
|
+
.catch((error) => this.logger.error(`Session ${sessionId}: failed to report subagent cwd: ${error}`));
|
|
7855
7930
|
const options = [];
|
|
7856
7931
|
if (input.permission_mode) {
|
|
7857
7932
|
const mode = session.modes.availableModes.find((mode) => mode.id === input.permission_mode);
|
|
@@ -8077,9 +8152,13 @@ export class ClaudeAcpAgent {
|
|
|
8077
8152
|
// cancel does not stop. Answer such a request with a denial so the
|
|
8078
8153
|
// subagent reads "the user refused" and goes on. Throwing instead makes
|
|
8079
8154
|
// Claude Code deny it as "Tool permission request failed".
|
|
8155
|
+
// On ACP v2, `cancelled` also answers a prompt that the user dismissed
|
|
8156
|
+
// or that the client declined by policy. Without a `session/cancel`
|
|
8157
|
+
// (`session.cancelled` unset), that is the user refusing the tool, so it
|
|
8158
|
+
// gets the same denial and the turn goes on.
|
|
8080
8159
|
if (response.outcome?.outcome === "cancelled" &&
|
|
8081
8160
|
this.sessions[sessionId] === session &&
|
|
8082
|
-
!blocksForeground(session, agentID)) {
|
|
8161
|
+
(!blocksForeground(session, agentID) || (this.v2 && !session.cancelled))) {
|
|
8083
8162
|
return {
|
|
8084
8163
|
behavior: "deny",
|
|
8085
8164
|
message: "User refused permission to run tool",
|
|
@@ -8143,7 +8222,8 @@ export class ClaudeAcpAgent {
|
|
|
8143
8222
|
* an error, is a `cancel`.
|
|
8144
8223
|
*/
|
|
8145
8224
|
handleMcpElicitation(sessionId, support) {
|
|
8146
|
-
return async (request, { signal }) => {
|
|
8225
|
+
return async (request, { signal: callbackSignal }) => {
|
|
8226
|
+
const signal = this.untilSessionClosed(sessionId, callbackSignal);
|
|
8147
8227
|
const isUrl = request.mode === "url";
|
|
8148
8228
|
if ((isUrl && !support.url) || (!isUrl && !support.form)) {
|
|
8149
8229
|
return { action: "cancel" };
|
|
@@ -8180,7 +8260,8 @@ export class ClaudeAcpAgent {
|
|
|
8180
8260
|
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
8181
8261
|
* a question it asks, and blocks the turn of `sessionId`.
|
|
8182
8262
|
*/
|
|
8183
|
-
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID,
|
|
8263
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, toolSignal, agentID) {
|
|
8264
|
+
const signal = this.untilSessionClosed(sessionId, toolSignal);
|
|
8184
8265
|
const questions = extractAskUserQuestions(toolInput);
|
|
8185
8266
|
if (!questions) {
|
|
8186
8267
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
@@ -8219,7 +8300,8 @@ export class ClaudeAcpAgent {
|
|
|
8219
8300
|
* refusal-fallback consent prompt, rendered as an ACP form elicitation.
|
|
8220
8301
|
*/
|
|
8221
8302
|
handleUserDialog(sessionId) {
|
|
8222
|
-
return async (request, { signal }) => {
|
|
8303
|
+
return async (request, { signal: callbackSignal }) => {
|
|
8304
|
+
const signal = this.untilSessionClosed(sessionId, callbackSignal);
|
|
8223
8305
|
if (request.dialogKind !== REFUSAL_FALLBACK_DIALOG_KIND) {
|
|
8224
8306
|
return { behavior: "cancelled" };
|
|
8225
8307
|
}
|
|
@@ -8323,19 +8405,27 @@ export class ClaudeAcpAgent {
|
|
|
8323
8405
|
/**
|
|
8324
8406
|
* The work that follows a session setup response: the session's commands,
|
|
8325
8407
|
* the current states of the subagents a load replayed, and MCP server
|
|
8326
|
-
* authentication.
|
|
8327
|
-
*
|
|
8328
|
-
* response (`src/v2/agent.ts`), so it gets neither here.
|
|
8408
|
+
* authentication. On v2 the setup handlers of `src/v2/agent.ts` do this
|
|
8409
|
+
* work once they have sent their response, so it does nothing here.
|
|
8329
8410
|
*/
|
|
8330
8411
|
afterSetupResponse(sessionId, mcpServers) {
|
|
8412
|
+
if (this.v2)
|
|
8413
|
+
return;
|
|
8331
8414
|
setTimeout(() => {
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
void this.publishCurrentSubagentStates(sessionId);
|
|
8335
|
-
}
|
|
8415
|
+
this.sendAvailableCommandsUpdate(sessionId);
|
|
8416
|
+
void this.publishCurrentSubagentStates(sessionId);
|
|
8336
8417
|
startMcpAuthentication(this, sessionId, mcpServers);
|
|
8337
8418
|
}, 0);
|
|
8338
8419
|
}
|
|
8420
|
+
/**
|
|
8421
|
+
* Starts OAuth in the background for the servers of `mcpServers` that
|
|
8422
|
+
* Claude Code reports as needing it, which sends the client a URL
|
|
8423
|
+
* elicitation. The v2 surface calls it after the setup response, since v2
|
|
8424
|
+
* asks for no elicitation before the `session/new` response.
|
|
8425
|
+
*/
|
|
8426
|
+
authenticateMcpServers(sessionId, mcpServers) {
|
|
8427
|
+
startMcpAuthentication(this, sessionId, mcpServers);
|
|
8428
|
+
}
|
|
8339
8429
|
/**
|
|
8340
8430
|
* Sends the current state of each subagent that the last load of
|
|
8341
8431
|
* `sessionId` replayed (see {@link RfdSubagentReplay.currentStates}). Run it
|
|
@@ -8595,6 +8685,7 @@ export class ClaudeAcpAgent {
|
|
|
8595
8685
|
agent: value === DEFAULT_AGENT_ID ? null : value,
|
|
8596
8686
|
});
|
|
8597
8687
|
session.currentAgent = value;
|
|
8688
|
+
session.agentSelected = true;
|
|
8598
8689
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
8599
8690
|
}
|
|
8600
8691
|
else if (configId === EFFORT_CONFIG_ID) {
|
|
@@ -8740,7 +8831,8 @@ export class ClaudeAcpAgent {
|
|
|
8740
8831
|
});
|
|
8741
8832
|
}
|
|
8742
8833
|
async getOrCreateSession(params, resumedSession) {
|
|
8743
|
-
// Reject a
|
|
8834
|
+
// Reject a bad `cwd` or prompt before reusing or tearing down a live session.
|
|
8835
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
8744
8836
|
clientSystemPrompt(params.systemPrompt);
|
|
8745
8837
|
const existingSession = this.sessions[params.sessionId];
|
|
8746
8838
|
// A recreated live session keeps its mode, not the mode of the transcript.
|
|
@@ -8788,8 +8880,16 @@ export class ClaudeAcpAgent {
|
|
|
8788
8880
|
* directory before we create a session. Throws an `invalidParams` error with
|
|
8789
8881
|
* an actionable message so clients (e.g. Zed) can surface it to the user
|
|
8790
8882
|
* instead of failing later with an opaque SDK error.
|
|
8883
|
+
*
|
|
8884
|
+
* Each `additionalDirectories` entry must be absolute too. Only the lexical
|
|
8885
|
+
* rule is checked there: a missing extra root does not fail a session.
|
|
8791
8886
|
*/
|
|
8792
|
-
async validateCwd(cwd) {
|
|
8887
|
+
async validateCwd(cwd, additionalDirectories) {
|
|
8888
|
+
for (const dir of additionalDirectories ?? []) {
|
|
8889
|
+
if (!path.isAbsolute(dir)) {
|
|
8890
|
+
throw RequestError.invalidParams({ additionalDirectories }, `\`additionalDirectories\` entries must be absolute paths, but received: ${dir}`);
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8793
8893
|
if (!path.isAbsolute(cwd)) {
|
|
8794
8894
|
throw RequestError.invalidParams({ cwd }, `\`cwd\` must be an absolute path, but received: ${cwd}`);
|
|
8795
8895
|
}
|
|
@@ -8817,7 +8917,7 @@ export class ClaudeAcpAgent {
|
|
|
8817
8917
|
// this check a session is created against a missing directory and the
|
|
8818
8918
|
// failure only surfaces later as a confusing "native binary failed to
|
|
8819
8919
|
// launch" error from the SDK (see issue #749).
|
|
8820
|
-
await this.validateCwd(params.cwd);
|
|
8920
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
8821
8921
|
const clientPrompt = clientSystemPrompt(params.systemPrompt);
|
|
8822
8922
|
// We want to create a new session id unless it is resume,
|
|
8823
8923
|
// but not resume + forkSession.
|
|
@@ -9016,6 +9116,7 @@ export class ClaudeAcpAgent {
|
|
|
9016
9116
|
...providerEnv,
|
|
9017
9117
|
// Opt-in to session state events like when the agent is idle
|
|
9018
9118
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
9119
|
+
...STARTUP_FAILURE_RESULTS_ENV,
|
|
9019
9120
|
};
|
|
9020
9121
|
// Scopes the context-window cache to this query's backend (see
|
|
9021
9122
|
// `contextWindowCache`). Derived from the same `env` object handed to the
|
|
@@ -9260,7 +9361,7 @@ export class ClaudeAcpAgent {
|
|
|
9260
9361
|
error.message.includes("No conversation found with session ID"))) {
|
|
9261
9362
|
throw RequestError.sessionNotFound(sessionId);
|
|
9262
9363
|
}
|
|
9263
|
-
throw error;
|
|
9364
|
+
throw startupFailureError(error) ?? error;
|
|
9264
9365
|
}
|
|
9265
9366
|
timing.phase("sdk-initialize");
|
|
9266
9367
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
@@ -9427,6 +9528,9 @@ export class ClaudeAcpAgent {
|
|
|
9427
9528
|
// "opus[1m]" row matched for a bare 200k id), so on the fallback path only
|
|
9428
9529
|
// the id itself is a trustworthy window signal.
|
|
9429
9530
|
const seededWindow = immediateContextWindow(providerCacheKey, models.currentModelId, allowlistedModelInfo);
|
|
9531
|
+
if (creationOpts.replacingSession?.tornDown) {
|
|
9532
|
+
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
|
9533
|
+
}
|
|
9430
9534
|
this.sessions[sessionId] = {
|
|
9431
9535
|
query: q,
|
|
9432
9536
|
input: input,
|
|
@@ -9537,27 +9641,8 @@ export class ClaudeAcpAgent {
|
|
|
9537
9641
|
await Promise.all(activeTurns);
|
|
9538
9642
|
}
|
|
9539
9643
|
this.providerConfig = config;
|
|
9540
|
-
for (const [sessionId
|
|
9541
|
-
|
|
9542
|
-
continue;
|
|
9543
|
-
}
|
|
9544
|
-
this.logger.log(`Recreating Claude session ${sessionId} for provider update`);
|
|
9545
|
-
this.closeQueryStream(session);
|
|
9546
|
-
delete this.sessions[sessionId];
|
|
9547
|
-
try {
|
|
9548
|
-
await this.createSession(session.creationParams, {
|
|
9549
|
-
resume: sessionId,
|
|
9550
|
-
permissionMode: session.modes.currentModeId,
|
|
9551
|
-
});
|
|
9552
|
-
}
|
|
9553
|
-
catch (error) {
|
|
9554
|
-
// One session that cannot come back must not abort the switch. The
|
|
9555
|
-
// `--hide-claude-auth` guard makes this a normal outcome of
|
|
9556
|
-
// `providers/disable`: the override kept a subscription account
|
|
9557
|
-
// usable, and creation refuses without it. The session is already
|
|
9558
|
-
// gone, so tell the client why and go on to the next one.
|
|
9559
|
-
this.reportSessionLostOnProviderUpdate(sessionId, session, error);
|
|
9560
|
-
}
|
|
9644
|
+
for (const [sessionId] of sessions) {
|
|
9645
|
+
await this.serializeConfigurationChange(sessionId, () => this.restartSessionForProvider(sessionId));
|
|
9561
9646
|
}
|
|
9562
9647
|
});
|
|
9563
9648
|
// Sessions and prompts await `providerUpdate` before they run. A rejected
|
|
@@ -9577,6 +9662,126 @@ export class ClaudeAcpAgent {
|
|
|
9577
9662
|
}
|
|
9578
9663
|
}
|
|
9579
9664
|
}
|
|
9665
|
+
/** Starts a new query for the session under the new provider and restores its choices. */
|
|
9666
|
+
async restartSessionForProvider(sessionId) {
|
|
9667
|
+
// A sign-out respawn ahead of this change can have replaced the snapshot.
|
|
9668
|
+
const session = this.sessions[sessionId];
|
|
9669
|
+
if (!session?.creationParams) {
|
|
9670
|
+
return;
|
|
9671
|
+
}
|
|
9672
|
+
this.logger.log(`Recreating Claude session ${sessionId} for provider update`);
|
|
9673
|
+
try {
|
|
9674
|
+
await this.replaceSessionQuery(sessionId, session, session.creationParams);
|
|
9675
|
+
}
|
|
9676
|
+
catch (error) {
|
|
9677
|
+
if (this.sessions[sessionId] === session)
|
|
9678
|
+
delete this.sessions[sessionId];
|
|
9679
|
+
if (session.tornDown)
|
|
9680
|
+
return;
|
|
9681
|
+
// One session that cannot come back must not abort the switch. The
|
|
9682
|
+
// `--hide-claude-auth` guard makes this a normal outcome of
|
|
9683
|
+
// `providers/disable`: the override kept a subscription account
|
|
9684
|
+
// usable, and creation refuses without it. The session is already
|
|
9685
|
+
// gone, so tell the client why and go on to the next one.
|
|
9686
|
+
this.reportSessionLostOnProviderUpdate(sessionId, session, error);
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9689
|
+
/** Recreates the session's query and restores its choices and reported MCP servers, unless it was closed meanwhile. */
|
|
9690
|
+
async replaceSessionQuery(sessionId, session, creationParams) {
|
|
9691
|
+
this.closeQueryStream(session);
|
|
9692
|
+
// Keep the old record visible so closeSession and dispose can mark it torn down.
|
|
9693
|
+
// Without the creation-time effort: restoreSessionChoices restores the current pin instead.
|
|
9694
|
+
await this.recreateLiveSession(withoutEffort(creationParams), sessionId, {
|
|
9695
|
+
permissionMode: session.modes.currentModeId,
|
|
9696
|
+
replacingSession: session,
|
|
9697
|
+
});
|
|
9698
|
+
const recreated = this.sessions[sessionId];
|
|
9699
|
+
if (!recreated || recreated.tornDown)
|
|
9700
|
+
return;
|
|
9701
|
+
// Same MCP servers, so don't repeat their notices.
|
|
9702
|
+
if (session.reportedMcpServers) {
|
|
9703
|
+
recreated.reportedMcpServers = new Set(session.reportedMcpServers);
|
|
9704
|
+
}
|
|
9705
|
+
await this.restoreSessionChoices(sessionId, recreated, session);
|
|
9706
|
+
if (recreated.tornDown)
|
|
9707
|
+
return;
|
|
9708
|
+
// The recreated session works without this update, so a failed send is only logged.
|
|
9709
|
+
await this.client
|
|
9710
|
+
.sessionUpdate({
|
|
9711
|
+
sessionId,
|
|
9712
|
+
update: { sessionUpdate: "config_option_update", configOptions: recreated.configOptions },
|
|
9713
|
+
})
|
|
9714
|
+
.catch((error) => {
|
|
9715
|
+
this.logger.error(`Session ${sessionId}: could not publish the configuration restored after the query restart:`, error);
|
|
9716
|
+
});
|
|
9717
|
+
}
|
|
9718
|
+
/** Re-applies the choices of `previous` to the session that replaced it; a choice the new query rejects is logged. */
|
|
9719
|
+
async restoreSessionChoices(sessionId, session, previous) {
|
|
9720
|
+
const restore = async (choice, apply) => {
|
|
9721
|
+
if (session.tornDown)
|
|
9722
|
+
return;
|
|
9723
|
+
try {
|
|
9724
|
+
await apply();
|
|
9725
|
+
}
|
|
9726
|
+
catch (error) {
|
|
9727
|
+
if (!session.tornDown) {
|
|
9728
|
+
this.logger.error(`Could not restore ${choice} after the query restart:`, error);
|
|
9729
|
+
}
|
|
9730
|
+
}
|
|
9731
|
+
};
|
|
9732
|
+
const select = (configId, value) => restore(`${configId} "${value}"`, () => this.selectSessionConfigOption({ sessionId, configId, value }));
|
|
9733
|
+
// First: the model decides which modes, effort levels, Fast mode and ultracode exist.
|
|
9734
|
+
if (session.models.currentModelId !== previous.models.currentModelId) {
|
|
9735
|
+
await select(MODEL_CONFIG_ID, previous.models.currentModelId);
|
|
9736
|
+
}
|
|
9737
|
+
// After the model: creation falls back from "auto" to acceptEdits on a model without Auto.
|
|
9738
|
+
if (session.modes.currentModeId !== previous.modes.currentModeId) {
|
|
9739
|
+
await select(MODE_CONFIG_ID, previous.modes.currentModeId);
|
|
9740
|
+
if (session.modes.currentModeId === previous.modes.currentModeId) {
|
|
9741
|
+
session.autoModeFallbackWarningPending = false;
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9744
|
+
if (session.modes.currentModeId === "plan") {
|
|
9745
|
+
session.prePlanMode = previous.prePlanMode;
|
|
9746
|
+
}
|
|
9747
|
+
// Compare intent, not the option: the option is hidden on a model without support.
|
|
9748
|
+
if (session.ultracodeEnabled !== previous.ultracodeEnabled) {
|
|
9749
|
+
await restore("ultracode", () => this.applyUltracode(session, previous.ultracodeEnabled));
|
|
9750
|
+
}
|
|
9751
|
+
if (session.fastModeEnabled !== previous.fastModeEnabled) {
|
|
9752
|
+
await restore("Fast mode", () => this.applyFastMode(session, previous.fastModeEnabled));
|
|
9753
|
+
}
|
|
9754
|
+
// After ultracode, since the effort flag carries it.
|
|
9755
|
+
if (previous.effortPinnedLevel !== undefined) {
|
|
9756
|
+
await select(EFFORT_CONFIG_ID, previous.effortPinnedLevel);
|
|
9757
|
+
}
|
|
9758
|
+
// Even when unchanged: Default over a hidden creation-time agent like "Explore" must clear it.
|
|
9759
|
+
if (previous.agentSelected) {
|
|
9760
|
+
if (previous.currentAgent === DEFAULT_AGENT_ID) {
|
|
9761
|
+
// Default is always valid, even when discovery returned no custom agents and hid the picker.
|
|
9762
|
+
await restore("Default agent", () => this.applyConfigOptionValue(sessionId, session, AGENT_CONFIG_ID, DEFAULT_AGENT_ID));
|
|
9763
|
+
}
|
|
9764
|
+
else {
|
|
9765
|
+
await select(AGENT_CONFIG_ID, previous.currentAgent);
|
|
9766
|
+
}
|
|
9767
|
+
}
|
|
9768
|
+
// Keep the original params and fingerprint paired for later load/resume calls.
|
|
9769
|
+
session.creationParams = previous.creationParams;
|
|
9770
|
+
session.sessionFingerprint = previous.sessionFingerprint;
|
|
9771
|
+
}
|
|
9772
|
+
serializeConfigurationChange(sessionId, change) {
|
|
9773
|
+
const result = (this.configurationChanges.get(sessionId) ?? Promise.resolve()).then(change);
|
|
9774
|
+
// Drops the entry once no newer change has queued behind this one.
|
|
9775
|
+
const tail = result
|
|
9776
|
+
.catch(() => { })
|
|
9777
|
+
.then(() => {
|
|
9778
|
+
if (this.configurationChanges.get(sessionId) === tail) {
|
|
9779
|
+
this.configurationChanges.delete(sessionId);
|
|
9780
|
+
}
|
|
9781
|
+
});
|
|
9782
|
+
this.configurationChanges.set(sessionId, tail);
|
|
9783
|
+
return result;
|
|
9784
|
+
}
|
|
9580
9785
|
/** Tell a capable client when a non-auth error ends a session during a provider switch. */
|
|
9581
9786
|
reportSessionLostOnProviderUpdate(sessionId, session, error) {
|
|
9582
9787
|
this.logger.error(`Session ${sessionId}: could not be recreated for the provider update: ${error}`);
|
|
@@ -10289,11 +10494,20 @@ skillFiles) {
|
|
|
10289
10494
|
};
|
|
10290
10495
|
})
|
|
10291
10496
|
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
10497
|
+
// Command names must not contain whitespace and must be unique. A name
|
|
10498
|
+
// like "my command" is dropped, and of two commands named "review" only the
|
|
10499
|
+
// first is kept.
|
|
10500
|
+
const names = new Set();
|
|
10292
10501
|
return [
|
|
10293
10502
|
...advertised,
|
|
10294
10503
|
MCP_AVAILABLE_COMMAND,
|
|
10295
10504
|
...remoteControlCommands.filter((command) => !claudeCodeOffersCommand(commands, terminalCommands, command.name)),
|
|
10296
|
-
]
|
|
10505
|
+
].filter((command) => {
|
|
10506
|
+
if (/\s/.test(command.name) || names.has(command.name))
|
|
10507
|
+
return false;
|
|
10508
|
+
names.add(command.name);
|
|
10509
|
+
return true;
|
|
10510
|
+
});
|
|
10297
10511
|
}
|
|
10298
10512
|
function formatUriAsLink(uri) {
|
|
10299
10513
|
try {
|
|
@@ -10313,15 +10527,26 @@ function formatUriAsLink(uri) {
|
|
|
10313
10527
|
return uri;
|
|
10314
10528
|
}
|
|
10315
10529
|
}
|
|
10530
|
+
/** The prompt content block types that `promptToClaude` converts. The agent
|
|
10531
|
+
* advertises no `audio` prompt capability. */
|
|
10532
|
+
const SUPPORTED_PROMPT_CONTENT_TYPES = [
|
|
10533
|
+
"text",
|
|
10534
|
+
"image",
|
|
10535
|
+
"resource_link",
|
|
10536
|
+
"resource",
|
|
10537
|
+
];
|
|
10316
10538
|
export function promptToClaude(prompt) {
|
|
10317
10539
|
const content = [];
|
|
10318
10540
|
const context = [];
|
|
10541
|
+
const firstText = prompt.prompt.find((chunk) => chunk.type === "text");
|
|
10319
10542
|
for (const chunk of prompt.prompt) {
|
|
10320
10543
|
switch (chunk.type) {
|
|
10321
10544
|
case "text": {
|
|
10322
10545
|
let text = chunk.text;
|
|
10323
|
-
//
|
|
10324
|
-
|
|
10546
|
+
// A slash command is the first text block. Change
|
|
10547
|
+
// `/mcp:server:command args` to `/server:command (MCP) args`; the
|
|
10548
|
+
// arguments can span lines.
|
|
10549
|
+
const mcpMatch = chunk === firstText ? text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s+([\s\S]*))?$/) : null;
|
|
10325
10550
|
if (mcpMatch) {
|
|
10326
10551
|
const [, server, command, args] = mcpMatch;
|
|
10327
10552
|
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
|