claude-code-rust 0.14.1 → 0.14.2
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/agent-sdk/dist/bridge/available_commands.js +1 -1
- package/agent-sdk/dist/bridge/command_interactions.js +16 -0
- package/agent-sdk/dist/bridge/command_lifecycle.js +184 -0
- package/agent-sdk/dist/bridge/command_mcp.js +32 -0
- package/agent-sdk/dist/bridge/command_scheduler.js +136 -0
- package/agent-sdk/dist/bridge/command_session_control.js +269 -0
- package/agent-sdk/dist/bridge/command_session_data.js +203 -0
- package/agent-sdk/dist/bridge/commands.js +6 -0
- package/agent-sdk/dist/bridge/error_classification.js +24 -7
- package/agent-sdk/dist/bridge/events.js +42 -9
- package/agent-sdk/dist/bridge/history.js +7 -3
- package/agent-sdk/dist/bridge/logger.js +2 -0
- package/agent-sdk/dist/bridge/mcp.js +90 -80
- package/agent-sdk/dist/bridge/mcp_auth_adapter.js +67 -0
- package/agent-sdk/dist/bridge/mcp_monitor.js +59 -0
- package/agent-sdk/dist/bridge/message_handlers.js +11 -7
- package/agent-sdk/dist/bridge/session_lifecycle.js +54 -4
- package/agent-sdk/dist/bridge/state_parsing.js +7 -0
- package/agent-sdk/dist/bridge/tool_calls.js +5 -4
- package/agent-sdk/dist/bridge/tooling.js +53 -1
- package/agent-sdk/dist/bridge.js +118 -617
- package/package.json +8 -8
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { emitMcpOperationError, slashError, writeEvent } from "./events.js";
|
|
2
2
|
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
3
3
|
import { bridgeMcpServersToSdk, mapMcpServerStatus, summarizeMcpServersForDiagnostics, } from "./mcp_metadata.js";
|
|
4
|
+
import { authenticateMcpServer, clearMcpServerAuth, detectMcpAuthCapabilities, submitMcpOAuthCallbackUrl, } from "./mcp_auth_adapter.js";
|
|
5
|
+
import { runMcpAuthMonitor, } from "./mcp_monitor.js";
|
|
4
6
|
export const MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS = 30_000;
|
|
5
|
-
const knownConnectedMcpServers = new Set();
|
|
6
7
|
function logMcpSuccess(eventName, message, sessionId, requestId, fields) {
|
|
7
8
|
bridgeLogger.info({
|
|
8
9
|
target: LOG_TARGETS.BRIDGE_MCP,
|
|
@@ -28,9 +29,6 @@ function logMcpFailure(eventName, message, sessionId, errorMessage, requestId, f
|
|
|
28
29
|
},
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
|
-
function queryWithMcpAuth(session) {
|
|
32
|
-
return session.query;
|
|
33
|
-
}
|
|
34
32
|
function mapMcpSetServersResult(result) {
|
|
35
33
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
36
34
|
return { added: [], removed: [], errors: {} };
|
|
@@ -47,41 +45,6 @@ function mapMcpSetServersResult(result) {
|
|
|
47
45
|
: {};
|
|
48
46
|
return { added, removed, errors };
|
|
49
47
|
}
|
|
50
|
-
async function callMcpAuthMethod(session, methodName, args) {
|
|
51
|
-
const query = queryWithMcpAuth(session);
|
|
52
|
-
switch (methodName) {
|
|
53
|
-
case "mcpAuthenticate":
|
|
54
|
-
if (typeof query.mcpAuthenticate !== "function") {
|
|
55
|
-
throw new Error("installed SDK does not support mcpAuthenticate");
|
|
56
|
-
}
|
|
57
|
-
return await query.mcpAuthenticate(args[0] ?? "");
|
|
58
|
-
case "mcpClearAuth":
|
|
59
|
-
if (typeof query.mcpClearAuth !== "function") {
|
|
60
|
-
throw new Error("installed SDK does not support mcpClearAuth");
|
|
61
|
-
}
|
|
62
|
-
return await query.mcpClearAuth(args[0] ?? "");
|
|
63
|
-
case "mcpSubmitOAuthCallbackUrl":
|
|
64
|
-
if (typeof query.mcpSubmitOAuthCallbackUrl !== "function") {
|
|
65
|
-
throw new Error("installed SDK does not support mcpSubmitOAuthCallbackUrl");
|
|
66
|
-
}
|
|
67
|
-
return await query.mcpSubmitOAuthCallbackUrl(args[0] ?? "", args[1] ?? "");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function extractMcpAuthRedirect(serverName, value) {
|
|
71
|
-
if (!value || typeof value !== "object") {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
const authUrl = Reflect.get(value, "authUrl");
|
|
75
|
-
if (typeof authUrl !== "string" || authUrl.trim().length === 0) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const requiresUserAction = Reflect.get(value, "requiresUserAction");
|
|
79
|
-
return {
|
|
80
|
-
server_name: serverName,
|
|
81
|
-
auth_url: authUrl,
|
|
82
|
-
requires_user_action: requiresUserAction === true,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
48
|
function emitMcpCommandError(sessionId, operation, message, requestId, serverName) {
|
|
86
49
|
emitMcpOperationError(sessionId, {
|
|
87
50
|
...(serverName ? { server_name: serverName } : {}),
|
|
@@ -90,9 +53,10 @@ function emitMcpCommandError(sessionId, operation, message, requestId, serverNam
|
|
|
90
53
|
}, requestId);
|
|
91
54
|
}
|
|
92
55
|
export async function emitMcpSnapshotEvent(session, requestId, source = "mcp_status") {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
const mapped = await loadReconciledMcpStatuses(session);
|
|
57
|
+
if (!mapped) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
96
60
|
return emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId);
|
|
97
61
|
}
|
|
98
62
|
export function emitMcpSnapshotFromStatuses(session, servers, source, requestId) {
|
|
@@ -104,7 +68,7 @@ export async function emitReconciledMcpSnapshotFromStatuses(session, servers, so
|
|
|
104
68
|
return emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId);
|
|
105
69
|
}
|
|
106
70
|
function emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId) {
|
|
107
|
-
rememberKnownConnectedMcpServers(mapped);
|
|
71
|
+
rememberKnownConnectedMcpServers(session, mapped);
|
|
108
72
|
logMcpSuccess("mcp_snapshot_emitted", "MCP snapshot emitted", session.sessionId, requestId, {
|
|
109
73
|
source,
|
|
110
74
|
server_count: mapped.length,
|
|
@@ -115,6 +79,7 @@ function emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId) {
|
|
|
115
79
|
session_id: session.sessionId,
|
|
116
80
|
source,
|
|
117
81
|
servers: mapped,
|
|
82
|
+
auth_capabilities: detectMcpAuthCapabilities(session.query),
|
|
118
83
|
}, requestId);
|
|
119
84
|
return mapped;
|
|
120
85
|
}
|
|
@@ -132,23 +97,33 @@ export function staleMcpAuthCandidates(servers, knownConnectedServerNames, lastR
|
|
|
132
97
|
})
|
|
133
98
|
.map((server) => server.name);
|
|
134
99
|
}
|
|
135
|
-
function rememberKnownConnectedMcpServers(servers) {
|
|
100
|
+
function rememberKnownConnectedMcpServers(session, servers) {
|
|
136
101
|
for (const server of servers) {
|
|
137
102
|
if (server.status === "connected") {
|
|
138
|
-
knownConnectedMcpServers.add(server.name);
|
|
103
|
+
session.knownConnectedMcpServers.add(server.name);
|
|
139
104
|
}
|
|
140
105
|
}
|
|
141
106
|
}
|
|
142
|
-
function forgetKnownConnectedMcpServer(serverName) {
|
|
143
|
-
knownConnectedMcpServers.delete(serverName);
|
|
107
|
+
function forgetKnownConnectedMcpServer(session, serverName) {
|
|
108
|
+
session.knownConnectedMcpServers.delete(serverName);
|
|
144
109
|
}
|
|
145
|
-
async function
|
|
146
|
-
const
|
|
110
|
+
async function loadReconciledMcpStatuses(session, isActive = () => true) {
|
|
111
|
+
const servers = await session.query.mcpServerStatus();
|
|
112
|
+
if (!isActive()) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return await reconcileSuspiciousMcpStatuses(session, servers.map(mapMcpServerStatus), isActive);
|
|
116
|
+
}
|
|
117
|
+
async function reconcileSuspiciousMcpStatuses(session, servers, isActive = () => true) {
|
|
118
|
+
const candidates = staleMcpAuthCandidates(servers, session.knownConnectedMcpServers, session.mcpStatusRevalidatedAt);
|
|
147
119
|
if (candidates.length === 0) {
|
|
148
120
|
return servers;
|
|
149
121
|
}
|
|
150
122
|
const now = Date.now();
|
|
151
123
|
for (const serverName of candidates) {
|
|
124
|
+
if (!isActive()) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
152
127
|
session.mcpStatusRevalidatedAt.set(serverName, now);
|
|
153
128
|
bridgeLogger.info({
|
|
154
129
|
target: LOG_TARGETS.BRIDGE_MCP,
|
|
@@ -165,8 +140,14 @@ async function reconcileSuspiciousMcpStatuses(session, servers) {
|
|
|
165
140
|
});
|
|
166
141
|
try {
|
|
167
142
|
await session.query.reconnectMcpServer(serverName);
|
|
143
|
+
if (!isActive()) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
168
146
|
}
|
|
169
147
|
catch (error) {
|
|
148
|
+
if (!isActive()) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
170
151
|
const message = error instanceof Error ? error.message : String(error);
|
|
171
152
|
bridgeLogger.warn({
|
|
172
153
|
target: LOG_TARGETS.BRIDGE_MCP,
|
|
@@ -182,35 +163,67 @@ async function reconcileSuspiciousMcpStatuses(session, servers) {
|
|
|
182
163
|
});
|
|
183
164
|
}
|
|
184
165
|
}
|
|
185
|
-
|
|
166
|
+
const refreshed = await session.query.mcpServerStatus();
|
|
167
|
+
return isActive() ? refreshed.map(mapMcpServerStatus) : undefined;
|
|
186
168
|
}
|
|
187
169
|
function shouldKeepMonitoringMcpAuth(server) {
|
|
188
170
|
return server?.status === "needs-auth" || server?.status === "pending";
|
|
189
171
|
}
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
172
|
+
function logMcpAuthMonitorExhausted(session, serverName, result) {
|
|
173
|
+
bridgeLogger.warn({
|
|
174
|
+
target: LOG_TARGETS.BRIDGE_MCP,
|
|
175
|
+
eventName: "mcp_auth_monitor_exhausted",
|
|
176
|
+
message: "MCP authentication status monitor exhausted",
|
|
177
|
+
outcome: "failure",
|
|
178
|
+
sessionId: session.sessionId,
|
|
179
|
+
count: result.attempts,
|
|
180
|
+
fields: {
|
|
181
|
+
server_name: serverName,
|
|
182
|
+
attempts: result.attempts,
|
|
183
|
+
reason: result.reason,
|
|
184
|
+
...(result.lastError ? { error_message: result.lastError } : {}),
|
|
185
|
+
},
|
|
186
|
+
});
|
|
196
187
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const server = servers.find((candidate) => candidate.name === serverName);
|
|
201
|
-
if (attempt < maxAttempts && shouldKeepMonitoringMcpAuth(server)) {
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
void monitorMcpAuthSnapshot(session, serverName, attempt + 1, maxAttempts, delayMs);
|
|
204
|
-
}, delayMs);
|
|
205
|
-
}
|
|
188
|
+
export function startMcpAuthSnapshotMonitor(session, serverName, timing = {}) {
|
|
189
|
+
if (session.closing) {
|
|
190
|
+
return Promise.resolve({ outcome: "cancelled", attempts: 0 });
|
|
206
191
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
void monitorMcpAuthSnapshot(session, serverName, attempt + 1, maxAttempts, delayMs);
|
|
211
|
-
}, delayMs);
|
|
212
|
-
}
|
|
192
|
+
const existing = session.mcpAuthMonitors.get(serverName);
|
|
193
|
+
if (existing) {
|
|
194
|
+
return existing.task;
|
|
213
195
|
}
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
let monitor;
|
|
198
|
+
const isActive = () => !session.closing &&
|
|
199
|
+
!controller.signal.aborted &&
|
|
200
|
+
session.mcpAuthMonitors.get(serverName) === monitor;
|
|
201
|
+
const task = runMcpAuthMonitor({
|
|
202
|
+
...timing,
|
|
203
|
+
signal: controller.signal,
|
|
204
|
+
poll: async () => {
|
|
205
|
+
const servers = await loadReconciledMcpStatuses(session, isActive);
|
|
206
|
+
if (!isActive() || !servers) {
|
|
207
|
+
return "complete";
|
|
208
|
+
}
|
|
209
|
+
emitMcpSnapshotFromMappedStatuses(session, servers, "mcp_status");
|
|
210
|
+
const server = servers.find((candidate) => candidate.name === serverName);
|
|
211
|
+
return shouldKeepMonitoringMcpAuth(server) ? "continue" : "complete";
|
|
212
|
+
},
|
|
213
|
+
}).then((result) => {
|
|
214
|
+
if (result.outcome === "exhausted") {
|
|
215
|
+
logMcpAuthMonitorExhausted(session, serverName, result);
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
});
|
|
219
|
+
monitor = { controller, task };
|
|
220
|
+
session.mcpAuthMonitors.set(serverName, monitor);
|
|
221
|
+
void task.then(() => {
|
|
222
|
+
if (session.mcpAuthMonitors.get(serverName) === monitor) {
|
|
223
|
+
session.mcpAuthMonitors.delete(serverName);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return task;
|
|
214
227
|
}
|
|
215
228
|
export async function handleMcpStatusCommand(session, requestId, source = "mcp_status") {
|
|
216
229
|
try {
|
|
@@ -224,6 +237,7 @@ export async function handleMcpStatusCommand(session, requestId, source = "mcp_s
|
|
|
224
237
|
session_id: session.sessionId,
|
|
225
238
|
source,
|
|
226
239
|
servers: [],
|
|
240
|
+
auth_capabilities: detectMcpAuthCapabilities(session.query),
|
|
227
241
|
error: message,
|
|
228
242
|
}, requestId);
|
|
229
243
|
}
|
|
@@ -277,8 +291,7 @@ export async function handleMcpSetServersCommand(session, command, requestId) {
|
|
|
277
291
|
}
|
|
278
292
|
export async function handleMcpAuthenticateCommand(session, command, requestId) {
|
|
279
293
|
try {
|
|
280
|
-
const
|
|
281
|
-
const redirect = extractMcpAuthRedirect(command.server_name, result);
|
|
294
|
+
const redirect = await authenticateMcpServer(session.query, command.server_name);
|
|
282
295
|
if (redirect) {
|
|
283
296
|
logMcpSuccess("mcp_auth_redirect_emitted", "MCP auth redirect emitted", command.session_id, requestId, { server_name: command.server_name, requires_user_action: redirect.requires_user_action });
|
|
284
297
|
writeEvent({
|
|
@@ -290,7 +303,7 @@ export async function handleMcpAuthenticateCommand(session, command, requestId)
|
|
|
290
303
|
else {
|
|
291
304
|
logMcpSuccess("mcp_authenticate_completed", "MCP authentication command completed", command.session_id, requestId, { server_name: command.server_name });
|
|
292
305
|
}
|
|
293
|
-
|
|
306
|
+
startMcpAuthSnapshotMonitor(session, command.server_name);
|
|
294
307
|
}
|
|
295
308
|
catch (error) {
|
|
296
309
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -300,8 +313,8 @@ export async function handleMcpAuthenticateCommand(session, command, requestId)
|
|
|
300
313
|
}
|
|
301
314
|
export async function handleMcpClearAuthCommand(session, command, requestId) {
|
|
302
315
|
try {
|
|
303
|
-
await
|
|
304
|
-
forgetKnownConnectedMcpServer(command.server_name);
|
|
316
|
+
await clearMcpServerAuth(session.query, command.server_name);
|
|
317
|
+
forgetKnownConnectedMcpServer(session, command.server_name);
|
|
305
318
|
session.mcpStatusRevalidatedAt.delete(command.server_name);
|
|
306
319
|
logMcpSuccess("mcp_clear_auth_completed", "MCP auth cleared", command.session_id, requestId, {
|
|
307
320
|
server_name: command.server_name,
|
|
@@ -315,10 +328,7 @@ export async function handleMcpClearAuthCommand(session, command, requestId) {
|
|
|
315
328
|
}
|
|
316
329
|
export async function handleMcpOauthCallbackUrlCommand(session, command, requestId) {
|
|
317
330
|
try {
|
|
318
|
-
await
|
|
319
|
-
command.server_name,
|
|
320
|
-
command.callback_url,
|
|
321
|
-
]);
|
|
331
|
+
await submitMcpOAuthCallbackUrl(session.query, command.server_name, command.callback_url);
|
|
322
332
|
logMcpSuccess("mcp_oauth_callback_completed", "MCP OAuth callback URL submitted", command.session_id, requestId, {
|
|
323
333
|
server_name: command.server_name,
|
|
324
334
|
callback_url_chars: command.callback_url.length,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const MCP_AUTH_METHODS = {
|
|
2
|
+
authenticate: "mcpAuthenticate",
|
|
3
|
+
clear_auth: "mcpClearAuth",
|
|
4
|
+
submit_oauth_callback_url: "mcpSubmitOAuthCallbackUrl",
|
|
5
|
+
};
|
|
6
|
+
function runtimeMethod(query, methodName) {
|
|
7
|
+
if ((!query || typeof query !== "object") && typeof query !== "function") {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const method = Reflect.get(query, methodName);
|
|
11
|
+
return typeof method === "function" ? method : undefined;
|
|
12
|
+
}
|
|
13
|
+
function requiredRuntimeMethod(query, methodName) {
|
|
14
|
+
const method = runtimeMethod(query, methodName);
|
|
15
|
+
if (!method) {
|
|
16
|
+
throw new Error(`installed SDK does not support ${methodName}`);
|
|
17
|
+
}
|
|
18
|
+
return method;
|
|
19
|
+
}
|
|
20
|
+
async function invokeRuntimeMethod(query, methodName, args) {
|
|
21
|
+
const method = requiredRuntimeMethod(query, methodName);
|
|
22
|
+
return await Reflect.apply(method, query, args);
|
|
23
|
+
}
|
|
24
|
+
export function detectMcpAuthCapabilities(query) {
|
|
25
|
+
return {
|
|
26
|
+
authenticate: runtimeMethod(query, MCP_AUTH_METHODS.authenticate) !== undefined,
|
|
27
|
+
clear_auth: runtimeMethod(query, MCP_AUTH_METHODS.clear_auth) !== undefined,
|
|
28
|
+
submit_oauth_callback_url: runtimeMethod(query, MCP_AUTH_METHODS.submit_oauth_callback_url) !== undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function parseMcpAuthRedirect(serverName, value) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
36
|
+
throw new Error("installed SDK returned an invalid mcpAuthenticate response");
|
|
37
|
+
}
|
|
38
|
+
const authUrl = Reflect.get(value, "authUrl");
|
|
39
|
+
if (authUrl === undefined || authUrl === null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (typeof authUrl !== "string" || authUrl.trim().length === 0) {
|
|
43
|
+
throw new Error("installed SDK returned an invalid mcpAuthenticate authUrl");
|
|
44
|
+
}
|
|
45
|
+
const requiresUserAction = Reflect.get(value, "requiresUserAction");
|
|
46
|
+
if (requiresUserAction !== undefined && typeof requiresUserAction !== "boolean") {
|
|
47
|
+
throw new Error("installed SDK returned an invalid mcpAuthenticate requiresUserAction");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
server_name: serverName,
|
|
51
|
+
auth_url: authUrl,
|
|
52
|
+
requires_user_action: requiresUserAction === true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function authenticateMcpServer(query, serverName) {
|
|
56
|
+
const result = await invokeRuntimeMethod(query, MCP_AUTH_METHODS.authenticate, [serverName]);
|
|
57
|
+
return parseMcpAuthRedirect(serverName, result);
|
|
58
|
+
}
|
|
59
|
+
export async function clearMcpServerAuth(query, serverName) {
|
|
60
|
+
await invokeRuntimeMethod(query, MCP_AUTH_METHODS.clear_auth, [serverName]);
|
|
61
|
+
}
|
|
62
|
+
export async function submitMcpOAuthCallbackUrl(query, serverName, callbackUrl) {
|
|
63
|
+
await invokeRuntimeMethod(query, MCP_AUTH_METHODS.submit_oauth_callback_url, [
|
|
64
|
+
serverName,
|
|
65
|
+
callbackUrl,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
const DEFAULT_MAX_ATTEMPTS = 24;
|
|
3
|
+
const DEFAULT_INITIAL_DELAY_MS = 1_000;
|
|
4
|
+
const DEFAULT_MAX_DELAY_MS = 10_000;
|
|
5
|
+
async function abortableDelay(delayMs, signal) {
|
|
6
|
+
await delay(delayMs, undefined, { signal, ref: false });
|
|
7
|
+
}
|
|
8
|
+
function errorMessage(error) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
export async function runMcpAuthMonitor({ signal, poll, maxAttempts = DEFAULT_MAX_ATTEMPTS, initialDelayMs = DEFAULT_INITIAL_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, sleep = abortableDelay, }) {
|
|
12
|
+
let nextDelayMs = initialDelayMs;
|
|
13
|
+
let lastError;
|
|
14
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
15
|
+
try {
|
|
16
|
+
await sleep(nextDelayMs, signal);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (signal.aborted) {
|
|
20
|
+
return { outcome: "cancelled", attempts: attempt - 1 };
|
|
21
|
+
}
|
|
22
|
+
lastError = errorMessage(error);
|
|
23
|
+
if (attempt === maxAttempts) {
|
|
24
|
+
return { outcome: "exhausted", attempts: attempt, reason: "error", lastError };
|
|
25
|
+
}
|
|
26
|
+
nextDelayMs = Math.min(nextDelayMs * 2, maxDelayMs);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (signal.aborted) {
|
|
30
|
+
return { outcome: "cancelled", attempts: attempt - 1 };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const result = await poll();
|
|
34
|
+
if (signal.aborted) {
|
|
35
|
+
return { outcome: "cancelled", attempts: attempt };
|
|
36
|
+
}
|
|
37
|
+
if (result === "complete") {
|
|
38
|
+
return { outcome: "completed", attempts: attempt };
|
|
39
|
+
}
|
|
40
|
+
lastError = undefined;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (signal.aborted) {
|
|
44
|
+
return { outcome: "cancelled", attempts: attempt };
|
|
45
|
+
}
|
|
46
|
+
lastError = errorMessage(error);
|
|
47
|
+
}
|
|
48
|
+
if (attempt === maxAttempts) {
|
|
49
|
+
return {
|
|
50
|
+
outcome: "exhausted",
|
|
51
|
+
attempts: attempt,
|
|
52
|
+
reason: lastError ? "error" : "status",
|
|
53
|
+
...(lastError ? { lastError } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
nextDelayMs = Math.min(nextDelayMs * 2, maxDelayMs);
|
|
57
|
+
}
|
|
58
|
+
return { outcome: "exhausted", attempts: 0, reason: "status" };
|
|
59
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { asRecordOrNull } from "./shared.js";
|
|
2
2
|
import { toPermissionMode, buildModeState, refreshSupportedModesForSession } from "./commands.js";
|
|
3
3
|
import { writeEvent, emitSessionUpdate, emitConnectEvent, emitSessionReplacedEvent, } from "./events.js";
|
|
4
|
-
import { TOOL_RESULT_TYPES, isToolSearchToolName, isToolSearchToolResultType, unwrapToolUseResult, } from "./tooling.js";
|
|
4
|
+
import { TOOL_RESULT_TYPES, isToolSearchToolName, isToolSearchToolResultType, unwrapToolUseResult, parseToolNonExecutionMetadata, } from "./tooling.js";
|
|
5
5
|
import { emitToolCall, emitToolCallUpdate, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, defersTaskNotificationCompletion, toolAcceptsTaskLifecycle, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
|
|
6
6
|
import { applyBackgroundTasksChanged, applyTaskLifecycleState } from "./tasks.js";
|
|
7
7
|
import { linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
|
|
8
|
-
import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdateIfChanged } from "./error_classification.js";
|
|
8
|
+
import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdate, emitFastModeUpdateIfChanged, setFastModeSnapshotIfChanged, } from "./error_classification.js";
|
|
9
9
|
import { mapAvailableAgentsFromNames, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
|
|
10
10
|
import { mapInitSlashCommands, mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
|
|
11
11
|
import { buildApiRetryUpdate, buildRateLimitUpdate, buildSubagentRetryUpdate, normalizeSettingsParseErrors, numberField, parseApiRetryError, parseRuntimeSessionState, } from "./state_parsing.js";
|
|
@@ -744,6 +744,7 @@ export function handleUserToolResultBlocks(session, message) {
|
|
|
744
744
|
return false;
|
|
745
745
|
}
|
|
746
746
|
const content = Array.isArray(messageObject.content) ? messageObject.content : [];
|
|
747
|
+
const nonExecutionByToolUseId = parseToolNonExecutionMetadata(message.tool_result_meta);
|
|
747
748
|
let handled = false;
|
|
748
749
|
for (const block of content) {
|
|
749
750
|
if (!block || typeof block !== "object") {
|
|
@@ -766,13 +767,13 @@ export function handleUserToolResultBlocks(session, message) {
|
|
|
766
767
|
parentToolUseId,
|
|
767
768
|
sourceMessageUuid: sourceMessageUuid(message),
|
|
768
769
|
});
|
|
769
|
-
emitToolResultUpdate(session, toolUseId, Boolean(blockRecord.is_error), blockRecord.content, messageToolUseResult(message) ?? blockRecord, sourceMessageUuid(message));
|
|
770
|
+
emitToolResultUpdate(session, toolUseId, Boolean(blockRecord.is_error), blockRecord.content, messageToolUseResult(message) ?? blockRecord, sourceMessageUuid(message), nonExecutionByToolUseId.get(toolUseId));
|
|
770
771
|
}
|
|
771
772
|
}
|
|
772
773
|
return handled;
|
|
773
774
|
}
|
|
774
775
|
export function handleResultMessage(session, message) {
|
|
775
|
-
emitFastModeUpdateIfChanged(session, message.fast_mode_state);
|
|
776
|
+
emitFastModeUpdateIfChanged(session, message.fast_mode_state, message.fast_mode_disabled_reason);
|
|
776
777
|
const terminalReason = terminalReasonFromValue(message.terminal_reason);
|
|
777
778
|
const subtype = typeof message.subtype === "string" ? message.subtype : "";
|
|
778
779
|
if (subtype === "success") {
|
|
@@ -979,7 +980,7 @@ export function handleSdkMessage(session, message) {
|
|
|
979
980
|
session.mode = incomingMode;
|
|
980
981
|
}
|
|
981
982
|
refreshSupportedModesForSession(session);
|
|
982
|
-
|
|
983
|
+
const fastModeChanged = setFastModeSnapshotIfChanged(session, msg.fast_mode_state, msg.fast_mode_disabled_reason);
|
|
983
984
|
if (!session.connected) {
|
|
984
985
|
emitConnectEvent(session);
|
|
985
986
|
}
|
|
@@ -996,6 +997,9 @@ export function handleSdkMessage(session, message) {
|
|
|
996
997
|
mode: buildModeState(session, incomingMode),
|
|
997
998
|
});
|
|
998
999
|
}
|
|
1000
|
+
if (fastModeChanged) {
|
|
1001
|
+
emitFastModeUpdate(session);
|
|
1002
|
+
}
|
|
999
1003
|
}
|
|
1000
1004
|
if (Array.isArray(msg.slash_commands)) {
|
|
1001
1005
|
updateAvailableCommands(session, "init_slash_commands", mapInitSlashCommands(msg.slash_commands));
|
|
@@ -1040,7 +1044,7 @@ export function handleSdkMessage(session, message) {
|
|
|
1040
1044
|
else if (msg.status === null) {
|
|
1041
1045
|
emitSessionUpdate(session.sessionId, { type: "session_status_update", status: "idle" });
|
|
1042
1046
|
}
|
|
1043
|
-
emitFastModeUpdateIfChanged(session, msg.fast_mode_state);
|
|
1047
|
+
emitFastModeUpdateIfChanged(session, msg.fast_mode_state, msg.fast_mode_disabled_reason);
|
|
1044
1048
|
return;
|
|
1045
1049
|
}
|
|
1046
1050
|
if (subtype === "compact_boundary") {
|
|
@@ -1270,7 +1274,7 @@ export function handleSdkMessage(session, message) {
|
|
|
1270
1274
|
return;
|
|
1271
1275
|
}
|
|
1272
1276
|
const parsed = unwrapToolUseResult(rawToolUseResult);
|
|
1273
|
-
emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content, rawToolUseResult, sourceMessageUuid(msg));
|
|
1277
|
+
emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content, rawToolUseResult, sourceMessageUuid(msg), parseToolNonExecutionMetadata(msg.tool_result_meta).get(toolUseId));
|
|
1274
1278
|
}
|
|
1275
1279
|
return;
|
|
1276
1280
|
}
|
|
@@ -11,7 +11,7 @@ import { isToolSearchToolName } from "./tooling.js";
|
|
|
11
11
|
import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
|
|
12
12
|
import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
|
|
13
13
|
import { mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
|
|
14
|
-
import { emitAuthRequired,
|
|
14
|
+
import { emitAuthRequired, emitFastModeUpdate, setFastModeSnapshotIfChanged, } from "./error_classification.js";
|
|
15
15
|
import { mapAvailableModels, resolveCurrentModel, currentModelsEqual, } from "./model_metadata.js";
|
|
16
16
|
import { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
|
|
17
17
|
export { mapAvailableModels, resolveCurrentModel } from "./model_metadata.js";
|
|
@@ -34,6 +34,7 @@ function permissionDisplayFromCanUseOptions(options) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
export const sessions = new Map();
|
|
37
|
+
const pendingSessionCloseTasks = new Set();
|
|
37
38
|
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
38
39
|
const DEFAULT_PERMISSION_MODE = "default";
|
|
39
40
|
function isSdkElicitationContentValue(value) {
|
|
@@ -134,7 +135,44 @@ export function updateSessionId(session, newSessionId) {
|
|
|
134
135
|
session.sessionId = newSessionId;
|
|
135
136
|
sessions.set(newSessionId, session);
|
|
136
137
|
}
|
|
138
|
+
export function beginSessionClose(session) {
|
|
139
|
+
session.closing = true;
|
|
140
|
+
for (const monitor of session.mcpAuthMonitors.values()) {
|
|
141
|
+
monitor.controller.abort();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function detachSessionForClose(session) {
|
|
145
|
+
beginSessionClose(session);
|
|
146
|
+
if (sessions.get(session.sessionId) === session) {
|
|
147
|
+
sessions.delete(session.sessionId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function trackSessionCloseTask(task) {
|
|
151
|
+
const ownedTask = task.catch((error) => {
|
|
152
|
+
bridgeLogger.error({
|
|
153
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
154
|
+
eventName: "session_close_task_failed",
|
|
155
|
+
message: "background session cleanup failed",
|
|
156
|
+
outcome: "failure",
|
|
157
|
+
fields: {
|
|
158
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
pendingSessionCloseTasks.add(ownedTask);
|
|
163
|
+
void ownedTask.then(() => {
|
|
164
|
+
pendingSessionCloseTasks.delete(ownedTask);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function waitForPendingSessionCloseTasks() {
|
|
168
|
+
while (pendingSessionCloseTasks.size > 0) {
|
|
169
|
+
await Promise.all(Array.from(pendingSessionCloseTasks));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
137
172
|
export async function closeSession(session) {
|
|
173
|
+
beginSessionClose(session);
|
|
174
|
+
const mcpAuthMonitors = Array.from(session.mcpAuthMonitors.values());
|
|
175
|
+
session.mcpAuthMonitors.clear();
|
|
138
176
|
session.input.close();
|
|
139
177
|
session.query.close();
|
|
140
178
|
for (const pending of session.pendingPermissions.values()) {
|
|
@@ -154,6 +192,11 @@ export async function closeSession(session) {
|
|
|
154
192
|
pending.resolve({ action: "cancel" });
|
|
155
193
|
}
|
|
156
194
|
session.pendingElicitations.clear();
|
|
195
|
+
await Promise.all([
|
|
196
|
+
session.initializationTask,
|
|
197
|
+
session.queryConsumerTask,
|
|
198
|
+
...mcpAuthMonitors.map((monitor) => monitor.task),
|
|
199
|
+
].filter((task) => task !== undefined));
|
|
157
200
|
}
|
|
158
201
|
export async function closeSessionWithLogging(session, options = {}) {
|
|
159
202
|
await closeSession(session);
|
|
@@ -191,6 +234,7 @@ export async function closeAllSessions(options = {}) {
|
|
|
191
234
|
reason: options.reason ?? "bulk_close",
|
|
192
235
|
requestId: options.requestId,
|
|
193
236
|
})));
|
|
237
|
+
await waitForPendingSessionCloseTasks();
|
|
194
238
|
bridgeLogger.info({
|
|
195
239
|
target: LOG_TARGETS.APP_SESSION,
|
|
196
240
|
eventName: "all_sessions_closed",
|
|
@@ -335,6 +379,7 @@ export async function createSession(params) {
|
|
|
335
379
|
query: queryHandle,
|
|
336
380
|
input,
|
|
337
381
|
connected: false,
|
|
382
|
+
closing: false,
|
|
338
383
|
connectEvent: params.connectEvent,
|
|
339
384
|
connectRequestId: params.requestId,
|
|
340
385
|
toolCalls: new Map(),
|
|
@@ -347,7 +392,9 @@ export async function createSession(params) {
|
|
|
347
392
|
pendingUserDialogs: new Map(),
|
|
348
393
|
pendingElicitations: new Map(),
|
|
349
394
|
informationalDedupKeys: new Set(),
|
|
395
|
+
knownConnectedMcpServers: new Set(),
|
|
350
396
|
mcpStatusRevalidatedAt: new Map(),
|
|
397
|
+
mcpAuthMonitors: new Map(),
|
|
351
398
|
hiddenToolUseIds: new Set(),
|
|
352
399
|
authHintSent: false,
|
|
353
400
|
...(params.resumeUpdates && params.resumeUpdates.length > 0
|
|
@@ -396,7 +443,7 @@ export async function createSession(params) {
|
|
|
396
443
|
// In stream-input mode the SDK may defer init until input arrives.
|
|
397
444
|
// Trigger initialization explicitly so the Rust UI can receive `connected`
|
|
398
445
|
// before the first user prompt.
|
|
399
|
-
|
|
446
|
+
session.initializationTask = session.query
|
|
400
447
|
.initializationResult()
|
|
401
448
|
.then(async (result) => {
|
|
402
449
|
bridgeLogger.info({
|
|
@@ -416,6 +463,7 @@ export async function createSession(params) {
|
|
|
416
463
|
const currentModelChanged = refreshCurrentModel(session);
|
|
417
464
|
const { buildModeState, refreshSupportedModesForSession } = await import("./commands.js");
|
|
418
465
|
refreshSupportedModesForSession(session);
|
|
466
|
+
const fastModeChanged = setFastModeSnapshotIfChanged(session, result.fast_mode_state, result.fast_mode_disabled_reason);
|
|
419
467
|
if (!session.connected) {
|
|
420
468
|
emitConnectEvent(session);
|
|
421
469
|
}
|
|
@@ -429,13 +477,15 @@ export async function createSession(params) {
|
|
|
429
477
|
mode: buildModeState(session, session.mode),
|
|
430
478
|
});
|
|
431
479
|
}
|
|
480
|
+
if (fastModeChanged) {
|
|
481
|
+
emitFastModeUpdate(session);
|
|
482
|
+
}
|
|
432
483
|
}
|
|
433
484
|
// Proactively detect missing auth from account info so the UI can
|
|
434
485
|
// show the login hint immediately, without waiting for the first prompt.
|
|
435
486
|
if (shouldEmitStartupAuthRequiredForAccount(result.account)) {
|
|
436
487
|
emitAuthRequired(session);
|
|
437
488
|
}
|
|
438
|
-
emitFastModeUpdateIfChanged(session, result.fast_mode_state);
|
|
439
489
|
updateAvailableCommands(session, "session_result_commands", mapSdkSlashCommands(result.commands));
|
|
440
490
|
emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
|
|
441
491
|
refreshAvailableAgents(session);
|
|
@@ -457,7 +507,7 @@ export async function createSession(params) {
|
|
|
457
507
|
failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
|
|
458
508
|
session.connectRequestId = undefined;
|
|
459
509
|
});
|
|
460
|
-
|
|
510
|
+
session.queryConsumerTask = (async () => {
|
|
461
511
|
try {
|
|
462
512
|
for await (const message of session.query) {
|
|
463
513
|
// Lazy import to break circular dependency at module-evaluation time.
|
|
@@ -49,6 +49,13 @@ export function parseFastModeState(value) {
|
|
|
49
49
|
}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
+
export function parseFastModeDisabledReason(value) {
|
|
53
|
+
if (typeof value !== "string") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const reason = value.trim();
|
|
57
|
+
return reason.length > 0 ? reason : undefined;
|
|
58
|
+
}
|
|
52
59
|
export function parseRateLimitStatus(value) {
|
|
53
60
|
if (value === "allowed" || value === "allowed_warning" || value === "rejected") {
|
|
54
61
|
return value;
|