claude-code-rust 0.9.0 → 0.11.0
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 +16 -9
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/commands.js +71 -2
- package/agent-sdk/dist/bridge/events.js +127 -12
- package/agent-sdk/dist/bridge/history.js +9 -4
- package/agent-sdk/dist/bridge/logger.js +253 -0
- package/agent-sdk/dist/bridge/mcp.js +81 -5
- package/agent-sdk/dist/bridge/message_handlers.js +318 -67
- package/agent-sdk/dist/bridge/session_lifecycle.js +774 -58
- package/agent-sdk/dist/bridge/shared.js +0 -7
- package/agent-sdk/dist/bridge/state_parsing.js +61 -0
- package/agent-sdk/dist/bridge/tool_calls.js +269 -66
- package/agent-sdk/dist/bridge/tooling.js +60 -27
- package/agent-sdk/dist/bridge/user_interaction.js +34 -23
- package/agent-sdk/dist/bridge.js +406 -42
- package/agent-sdk/dist/bridge.test.js +765 -23
- package/package.json +2 -2
|
@@ -2,20 +2,82 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { spawn as spawnChild } from "node:child_process";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { bridgeLogger, LOG_TARGETS, logSdkStderrLine } from "./logger.js";
|
|
6
|
+
import { AsyncQueue } from "./shared.js";
|
|
7
|
+
import { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
|
|
8
|
+
import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, } from "./events.js";
|
|
8
9
|
import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
|
|
9
10
|
import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
|
|
10
11
|
import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
|
|
11
12
|
import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
|
|
13
|
+
function permissionDisplayFromCanUseOptions(options) {
|
|
14
|
+
const title = typeof options.title === "string" ? options.title.trim() : "";
|
|
15
|
+
const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
|
|
16
|
+
const description = typeof options.description === "string" ? options.description.trim() : "";
|
|
17
|
+
if (!title && !displayName && !description) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...(title ? { title } : {}),
|
|
22
|
+
...(displayName ? { display_name: displayName } : {}),
|
|
23
|
+
...(description ? { description } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const requestUserDialogInterceptorInstalled = Symbol("requestUserDialogInterceptorInstalled");
|
|
12
27
|
export const sessions = new Map();
|
|
28
|
+
function nonEmptyString(value) {
|
|
29
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
export function shouldEmitStartupAuthRequiredForAccount(account) {
|
|
32
|
+
const provider = account.apiProvider;
|
|
33
|
+
if (nonEmptyString(provider) && provider !== "firstParty") {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return !nonEmptyString(account.email) && !nonEmptyString(account.apiKeySource);
|
|
37
|
+
}
|
|
13
38
|
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
14
39
|
const DEFAULT_MODEL_NAME = "default";
|
|
15
40
|
const DEFAULT_PERMISSION_MODE = "default";
|
|
41
|
+
function isSdkElicitationContentValue(value) {
|
|
42
|
+
return (typeof value === "string" ||
|
|
43
|
+
typeof value === "number" ||
|
|
44
|
+
typeof value === "boolean" ||
|
|
45
|
+
(Array.isArray(value) && value.every((entry) => typeof entry === "string")));
|
|
46
|
+
}
|
|
47
|
+
function normalizeSdkElicitationContent(content) {
|
|
48
|
+
if (!content) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const normalized = {};
|
|
52
|
+
for (const [key, value] of Object.entries(content)) {
|
|
53
|
+
if (isSdkElicitationContentValue(value)) {
|
|
54
|
+
normalized[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
58
|
+
}
|
|
16
59
|
function settingsObjectFromLaunchSettings(launchSettings) {
|
|
17
60
|
return launchSettings.settings;
|
|
18
61
|
}
|
|
62
|
+
function normalizedSettingsFromLaunchSettings(launchSettings) {
|
|
63
|
+
const settings = settingsObjectFromLaunchSettings(launchSettings);
|
|
64
|
+
if (!settings) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const sandbox = settings.sandbox && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)
|
|
68
|
+
? settings.sandbox
|
|
69
|
+
: undefined;
|
|
70
|
+
if (sandbox?.enabled === true && sandbox.failIfUnavailable === undefined) {
|
|
71
|
+
return {
|
|
72
|
+
...settings,
|
|
73
|
+
sandbox: {
|
|
74
|
+
...sandbox,
|
|
75
|
+
failIfUnavailable: false,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return settings;
|
|
80
|
+
}
|
|
19
81
|
export function sessionById(sessionId) {
|
|
20
82
|
return sessions.get(sessionId) ?? null;
|
|
21
83
|
}
|
|
@@ -27,6 +89,77 @@ export function updateSessionId(session, newSessionId) {
|
|
|
27
89
|
session.sessionId = newSessionId;
|
|
28
90
|
sessions.set(newSessionId, session);
|
|
29
91
|
}
|
|
92
|
+
function isRequestUserDialogControlRequest(value) {
|
|
93
|
+
if (!value || typeof value !== "object") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const record = value;
|
|
97
|
+
const request = record.request;
|
|
98
|
+
if (!request || typeof request !== "object") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const inner = request;
|
|
102
|
+
const payload = inner.payload;
|
|
103
|
+
return (typeof record.request_id === "string" &&
|
|
104
|
+
inner.subtype === "request_user_dialog" &&
|
|
105
|
+
typeof inner.dialog_kind === "string" &&
|
|
106
|
+
Boolean(payload && typeof payload === "object" && !Array.isArray(payload)));
|
|
107
|
+
}
|
|
108
|
+
export function attachRequestUserDialogInterceptor(query, sessionIdForLogs) {
|
|
109
|
+
const internalQuery = query;
|
|
110
|
+
if (internalQuery[requestUserDialogInterceptorInstalled]) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (typeof internalQuery.processControlRequest !== "function") {
|
|
114
|
+
bridgeLogger.warn({
|
|
115
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
116
|
+
eventName: "request_user_dialog_interceptor_unavailable",
|
|
117
|
+
message: "request_user_dialog interceptor could not be installed",
|
|
118
|
+
outcome: "failure",
|
|
119
|
+
sessionId: sessionIdForLogs(),
|
|
120
|
+
});
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const originalProcessControlRequest = internalQuery.processControlRequest.bind(query);
|
|
124
|
+
internalQuery.processControlRequest = async (request, signal) => {
|
|
125
|
+
// SDK 0.2.104 also added cancel_async_message and seed_read_state control
|
|
126
|
+
// requests. Keep those delegated to the SDK internals: claude-rs does not
|
|
127
|
+
// own the SDK async-message queue or read-state cache, so TUI-level commands
|
|
128
|
+
// for them would add unsupported host behavior without user-visible value.
|
|
129
|
+
if (isRequestUserDialogControlRequest(request)) {
|
|
130
|
+
bridgeLogger.warn({
|
|
131
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
132
|
+
eventName: "request_user_dialog_received",
|
|
133
|
+
message: "request_user_dialog control request received",
|
|
134
|
+
outcome: "failure",
|
|
135
|
+
sessionId: sessionIdForLogs(),
|
|
136
|
+
requestId: request.request_id,
|
|
137
|
+
...(typeof request.request.tool_use_id === "string"
|
|
138
|
+
? { toolCallId: request.request.tool_use_id }
|
|
139
|
+
: {}),
|
|
140
|
+
fields: {
|
|
141
|
+
dialog_kind: request.request.dialog_kind,
|
|
142
|
+
raw_payload: request.request.payload,
|
|
143
|
+
raw_request: request.request,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
// TODO(request_user_dialog): Revisit this when a real claude-rs host flow needs it.
|
|
147
|
+
// For now we only log the full control request and reject it explicitly because
|
|
148
|
+
// normal TUI sessions do not appear to exercise these dialog kinds.
|
|
149
|
+
throw new Error(`request_user_dialog is not supported by claude-rs yet (dialog_kind: ${request.request.dialog_kind})`);
|
|
150
|
+
}
|
|
151
|
+
return await originalProcessControlRequest(request, signal);
|
|
152
|
+
};
|
|
153
|
+
internalQuery[requestUserDialogInterceptorInstalled] = true;
|
|
154
|
+
bridgeLogger.info({
|
|
155
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
156
|
+
eventName: "request_user_dialog_interceptor_installed",
|
|
157
|
+
message: "request_user_dialog interceptor installed",
|
|
158
|
+
outcome: "success",
|
|
159
|
+
sessionId: sessionIdForLogs(),
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
30
163
|
export async function closeSession(session) {
|
|
31
164
|
session.input.close();
|
|
32
165
|
session.query.close();
|
|
@@ -44,34 +177,77 @@ export async function closeSession(session) {
|
|
|
44
177
|
}
|
|
45
178
|
session.pendingElicitations.clear();
|
|
46
179
|
}
|
|
47
|
-
export async function
|
|
180
|
+
export async function closeSessionWithLogging(session, options = {}) {
|
|
181
|
+
await closeSession(session);
|
|
182
|
+
bridgeLogger.info({
|
|
183
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
184
|
+
eventName: "session_closed",
|
|
185
|
+
message: "session closed",
|
|
186
|
+
outcome: "success",
|
|
187
|
+
sessionId: session.sessionId,
|
|
188
|
+
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
189
|
+
fields: { reason: options.reason ?? "unspecified" },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export async function closeAllSessions(options = {}) {
|
|
48
193
|
const active = Array.from(sessions.values());
|
|
49
194
|
sessions.clear();
|
|
50
|
-
await Promise.all(active.map((session) =>
|
|
195
|
+
await Promise.all(active.map((session) => closeSessionWithLogging(session, {
|
|
196
|
+
reason: options.reason ?? "bulk_close",
|
|
197
|
+
requestId: options.requestId,
|
|
198
|
+
})));
|
|
199
|
+
bridgeLogger.info({
|
|
200
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
201
|
+
eventName: "all_sessions_closed",
|
|
202
|
+
message: "all sessions closed",
|
|
203
|
+
outcome: "success",
|
|
204
|
+
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
205
|
+
count: active.length,
|
|
206
|
+
fields: { reason: options.reason ?? "bulk_close" },
|
|
207
|
+
});
|
|
51
208
|
}
|
|
52
209
|
export async function createSession(params) {
|
|
53
210
|
const input = new AsyncQueue();
|
|
54
211
|
const provisionalSessionId = params.resume ?? randomUUID();
|
|
55
212
|
const initialModel = initialSessionModel(params.launchSettings);
|
|
56
213
|
const initialMode = initialSessionMode(params.launchSettings);
|
|
214
|
+
const supportsBypassPermissionsMode = startupPermissionModeOptions(params.launchSettings).allowDangerouslySkipPermissions === true;
|
|
215
|
+
const historyUpdateCount = params.resumeUpdates?.length ?? 0;
|
|
216
|
+
const staleSessionCount = params.sessionsToCloseAfterConnect?.length ?? 0;
|
|
57
217
|
let session;
|
|
218
|
+
const sessionIdForLogs = () => session?.sessionId ?? provisionalSessionId;
|
|
58
219
|
const canUseTool = async (toolName, inputData, options) => {
|
|
59
220
|
const toolUseId = options.toolUseID;
|
|
60
221
|
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
61
222
|
const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
|
|
62
223
|
return await requestExitPlanModeApproval(session, toolUseId, inputData, existing);
|
|
63
224
|
}
|
|
64
|
-
logPermissionDebug(`request tool_use_id=${toolUseId} tool=${toolName} blocked_path=${options.blockedPath ?? "<none>"} ` +
|
|
65
|
-
`decision_reason=${options.decisionReason ?? "<none>"} suggestions=${formatPermissionUpdates(options.suggestions)}`);
|
|
66
225
|
const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
|
|
67
226
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
68
227
|
return await requestAskUserQuestionAnswers(session, toolUseId, inputData, existing);
|
|
69
228
|
}
|
|
229
|
+
const display = permissionDisplayFromCanUseOptions(options);
|
|
70
230
|
const request = {
|
|
71
231
|
tool_call: existing,
|
|
72
232
|
options: permissionOptionsFromSuggestions(options.suggestions),
|
|
233
|
+
...(display ? { display } : {}),
|
|
73
234
|
};
|
|
74
|
-
|
|
235
|
+
bridgeLogger.info({
|
|
236
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
237
|
+
eventName: "permission_request_created",
|
|
238
|
+
message: "permission request created",
|
|
239
|
+
outcome: "start",
|
|
240
|
+
sessionId: session.sessionId,
|
|
241
|
+
toolCallId: toolUseId,
|
|
242
|
+
count: request.options.length,
|
|
243
|
+
fields: {
|
|
244
|
+
tool_name: toolName,
|
|
245
|
+
agent_id: options.agentID,
|
|
246
|
+
blocked_path: options.blockedPath ?? "<none>",
|
|
247
|
+
decision_reason: options.decisionReason ?? "<none>",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
emitPermissionRequestEvent(session.sessionId, request);
|
|
75
251
|
return await new Promise((resolve) => {
|
|
76
252
|
session.pendingPermissions.set(toolUseId, {
|
|
77
253
|
resolve,
|
|
@@ -89,6 +265,21 @@ export async function createSession(params) {
|
|
|
89
265
|
throw new Error(`CLAUDE_CODE_EXECUTABLE does not exist: ${claudeCodeExecutable}`);
|
|
90
266
|
}
|
|
91
267
|
let queryHandle;
|
|
268
|
+
bridgeLogger.info({
|
|
269
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
270
|
+
eventName: "session_create_started",
|
|
271
|
+
message: "session creation started",
|
|
272
|
+
outcome: "start",
|
|
273
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
274
|
+
sessionId: provisionalSessionId,
|
|
275
|
+
fields: {
|
|
276
|
+
cwd: params.cwd,
|
|
277
|
+
connect_event: params.connectEvent,
|
|
278
|
+
resume_requested: params.resume !== undefined,
|
|
279
|
+
history_update_count: historyUpdateCount,
|
|
280
|
+
stale_session_count: staleSessionCount,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
92
283
|
try {
|
|
93
284
|
queryHandle = query({
|
|
94
285
|
prompt: input,
|
|
@@ -103,12 +294,26 @@ export async function createSession(params) {
|
|
|
103
294
|
sdkDebugFile,
|
|
104
295
|
enableSdkDebug,
|
|
105
296
|
enableSpawnDebug,
|
|
106
|
-
sessionIdForLogs
|
|
297
|
+
sessionIdForLogs,
|
|
107
298
|
}),
|
|
108
299
|
});
|
|
300
|
+
attachRequestUserDialogInterceptor(queryHandle, sessionIdForLogs);
|
|
109
301
|
}
|
|
110
302
|
catch (error) {
|
|
111
303
|
const message = error instanceof Error ? error.message : String(error);
|
|
304
|
+
bridgeLogger.error({
|
|
305
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
306
|
+
eventName: "session_query_failed",
|
|
307
|
+
message: "session query creation failed",
|
|
308
|
+
outcome: "failure",
|
|
309
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
310
|
+
sessionId: provisionalSessionId,
|
|
311
|
+
fields: {
|
|
312
|
+
cwd: params.cwd,
|
|
313
|
+
resume_requested: params.resume !== undefined,
|
|
314
|
+
error_message: message,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
112
317
|
throw new Error(`query() failed: node_executable=${process.execPath}; cwd=${params.cwd}; ` +
|
|
113
318
|
`resume=${params.resume ?? "<none>"}; ` +
|
|
114
319
|
`CLAUDE_CODE_EXECUTABLE=${claudeCodeExecutable ?? "<unset>"}; error=${message}`);
|
|
@@ -117,8 +322,12 @@ export async function createSession(params) {
|
|
|
117
322
|
sessionId: provisionalSessionId,
|
|
118
323
|
cwd: params.cwd,
|
|
119
324
|
model: initialModel,
|
|
325
|
+
...(initialModel ? { requestedModelId: initialModel } : {}),
|
|
120
326
|
availableModels: [],
|
|
121
327
|
mode: initialMode,
|
|
328
|
+
supportedModeIds: [],
|
|
329
|
+
runtimeUnavailableModeIds: [],
|
|
330
|
+
supportsBypassPermissionsMode,
|
|
122
331
|
fastModeState: "off",
|
|
123
332
|
query: queryHandle,
|
|
124
333
|
input,
|
|
@@ -139,23 +348,76 @@ export async function createSession(params) {
|
|
|
139
348
|
? { sessionsToCloseAfterConnect: params.sessionsToCloseAfterConnect }
|
|
140
349
|
: {}),
|
|
141
350
|
};
|
|
351
|
+
refreshCurrentModel(session);
|
|
352
|
+
const { refreshSupportedModesForSession } = await import("./commands.js");
|
|
353
|
+
refreshSupportedModesForSession(session);
|
|
142
354
|
sessions.set(provisionalSessionId, session);
|
|
355
|
+
bridgeLogger.info({
|
|
356
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
357
|
+
eventName: "session_query_started",
|
|
358
|
+
message: "session query started",
|
|
359
|
+
outcome: "success",
|
|
360
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
361
|
+
sessionId: session.sessionId,
|
|
362
|
+
fields: {
|
|
363
|
+
cwd: session.cwd,
|
|
364
|
+
connect_event: session.connectEvent,
|
|
365
|
+
resume_requested: params.resume !== undefined,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
bridgeLogger.info({
|
|
369
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
370
|
+
eventName: "session_create_registered",
|
|
371
|
+
message: "session registered in bridge state",
|
|
372
|
+
outcome: "success",
|
|
373
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
374
|
+
sessionId: session.sessionId,
|
|
375
|
+
count: sessions.size,
|
|
376
|
+
fields: {
|
|
377
|
+
active_session_count: sessions.size,
|
|
378
|
+
connect_event: session.connectEvent,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
143
381
|
// In stream-input mode the SDK may defer init until input arrives.
|
|
144
382
|
// Trigger initialization explicitly so the Rust UI can receive `connected`
|
|
145
383
|
// before the first user prompt.
|
|
146
384
|
void session.query
|
|
147
385
|
.initializationResult()
|
|
148
|
-
.then((result) => {
|
|
386
|
+
.then(async (result) => {
|
|
387
|
+
bridgeLogger.info({
|
|
388
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
389
|
+
eventName: "session_initialization_completed",
|
|
390
|
+
message: "session initialization completed",
|
|
391
|
+
outcome: "success",
|
|
392
|
+
...(session.connectRequestId ? { requestId: session.connectRequestId } : {}),
|
|
393
|
+
sessionId: session.sessionId,
|
|
394
|
+
fields: {
|
|
395
|
+
available_model_count: Array.isArray(result.models) ? result.models.length : 0,
|
|
396
|
+
connect_event: session.connectEvent,
|
|
397
|
+
history_update_count: session.resumeUpdates?.length ?? 0,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
149
400
|
session.availableModels = mapAvailableModels(result.models);
|
|
401
|
+
const currentModelChanged = refreshCurrentModel(session);
|
|
402
|
+
const { buildModeState, refreshSupportedModesForSession } = await import("./commands.js");
|
|
403
|
+
refreshSupportedModesForSession(session);
|
|
150
404
|
if (!session.connected) {
|
|
151
405
|
emitConnectEvent(session);
|
|
152
406
|
}
|
|
407
|
+
else {
|
|
408
|
+
if (currentModelChanged) {
|
|
409
|
+
emitCurrentModelUpdate(session);
|
|
410
|
+
}
|
|
411
|
+
if (session.mode) {
|
|
412
|
+
emitSessionUpdate(session.sessionId, {
|
|
413
|
+
type: "mode_state_update",
|
|
414
|
+
mode: buildModeState(session, session.mode),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
153
418
|
// Proactively detect missing auth from account info so the UI can
|
|
154
419
|
// show the login hint immediately, without waiting for the first prompt.
|
|
155
|
-
|
|
156
|
-
const hasCredentials = (typeof acct.email === "string" && acct.email.trim().length > 0) ||
|
|
157
|
-
(typeof acct.apiKeySource === "string" && acct.apiKeySource.trim().length > 0);
|
|
158
|
-
if (!hasCredentials) {
|
|
420
|
+
if (shouldEmitStartupAuthRequiredForAccount(result.account)) {
|
|
159
421
|
emitAuthRequired(session);
|
|
160
422
|
}
|
|
161
423
|
emitFastModeUpdateIfChanged(session, result.fast_mode_state);
|
|
@@ -177,6 +439,15 @@ export async function createSession(params) {
|
|
|
177
439
|
return;
|
|
178
440
|
}
|
|
179
441
|
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
bridgeLogger.error({
|
|
443
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
444
|
+
eventName: "session_initialization_failed",
|
|
445
|
+
message: "session initialization failed before connect",
|
|
446
|
+
outcome: "failure",
|
|
447
|
+
...(session.connectRequestId ? { requestId: session.connectRequestId } : {}),
|
|
448
|
+
sessionId: session.sessionId,
|
|
449
|
+
fields: { error_message: message },
|
|
450
|
+
});
|
|
180
451
|
failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
|
|
181
452
|
session.connectRequestId = undefined;
|
|
182
453
|
});
|
|
@@ -188,21 +459,81 @@ export async function createSession(params) {
|
|
|
188
459
|
handleSdkMessage(session, message);
|
|
189
460
|
}
|
|
190
461
|
if (!session.connected) {
|
|
462
|
+
bridgeLogger.error({
|
|
463
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
464
|
+
eventName: "session_stream_ended_before_connect",
|
|
465
|
+
message: "session stream ended before connect",
|
|
466
|
+
outcome: "failure",
|
|
467
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
468
|
+
sessionId: session.sessionId,
|
|
469
|
+
});
|
|
191
470
|
failConnection("agent stream ended before session initialization", params.requestId);
|
|
192
471
|
}
|
|
193
472
|
}
|
|
194
473
|
catch (error) {
|
|
195
474
|
const message = error instanceof Error ? error.message : String(error);
|
|
475
|
+
bridgeLogger.error({
|
|
476
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
477
|
+
eventName: "session_stream_failed_before_connect",
|
|
478
|
+
message: "session stream failed before connect",
|
|
479
|
+
outcome: "failure",
|
|
480
|
+
...(params.requestId ? { requestId: params.requestId } : {}),
|
|
481
|
+
sessionId: session.sessionId,
|
|
482
|
+
fields: { error_message: message },
|
|
483
|
+
});
|
|
196
484
|
failConnection(`agent stream failed: ${message}`, params.requestId);
|
|
197
485
|
}
|
|
198
486
|
})();
|
|
199
487
|
}
|
|
488
|
+
function logSdkProcessSpawnStarted(options, includeArgsPreview) {
|
|
489
|
+
bridgeLogger.info({
|
|
490
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
491
|
+
eventName: "sdk_spawn_started",
|
|
492
|
+
message: "spawning Claude Code process",
|
|
493
|
+
outcome: "start",
|
|
494
|
+
fields: {
|
|
495
|
+
command: options.command,
|
|
496
|
+
cwd: options.cwd ?? "<none>",
|
|
497
|
+
arg_count: options.args.length,
|
|
498
|
+
...(includeArgsPreview ? { args_preview: options.args.slice(0, 5) } : {}),
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function logSdkProcessSpawned(sessionId, child, cwd) {
|
|
503
|
+
bridgeLogger.info({
|
|
504
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
505
|
+
eventName: "sdk_spawned",
|
|
506
|
+
message: "Claude Code process spawned",
|
|
507
|
+
outcome: "success",
|
|
508
|
+
...(sessionId ? { sessionId } : {}),
|
|
509
|
+
fields: {
|
|
510
|
+
cwd: cwd ?? "<none>",
|
|
511
|
+
pid: child.pid ?? "<none>",
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function logSdkProcessExit(sessionId, code, signal) {
|
|
516
|
+
const exitedCleanly = code === 0 && signal === null;
|
|
517
|
+
const logger = exitedCleanly ? bridgeLogger.info : bridgeLogger.warn;
|
|
518
|
+
logger({
|
|
519
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
520
|
+
eventName: "sdk_process_exited",
|
|
521
|
+
message: "Claude Code process exited",
|
|
522
|
+
outcome: exitedCleanly ? "success" : "failure",
|
|
523
|
+
...(sessionId ? { sessionId } : {}),
|
|
524
|
+
fields: {
|
|
525
|
+
exit_code: code ?? "<none>",
|
|
526
|
+
exit_signal: signal ?? "<none>",
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
200
530
|
function permissionModeFromSettingsValue(rawMode) {
|
|
201
531
|
if (typeof rawMode !== "string") {
|
|
202
532
|
return undefined;
|
|
203
533
|
}
|
|
204
534
|
switch (rawMode) {
|
|
205
535
|
case "default":
|
|
536
|
+
case "auto":
|
|
206
537
|
case "acceptEdits":
|
|
207
538
|
case "bypassPermissions":
|
|
208
539
|
case "plan":
|
|
@@ -261,12 +592,14 @@ export function buildQueryOptions(params) {
|
|
|
261
592
|
const systemPrompt = systemPromptFromLaunchSettings(params.launchSettings);
|
|
262
593
|
const modelOption = startupModelOption(params.launchSettings);
|
|
263
594
|
const permissionModeOptions = startupPermissionModeOptions(params.launchSettings);
|
|
595
|
+
const settings = normalizedSettingsFromLaunchSettings(params.launchSettings);
|
|
264
596
|
return {
|
|
265
597
|
cwd: params.cwd,
|
|
266
598
|
includePartialMessages: true,
|
|
599
|
+
promptSuggestions: true,
|
|
267
600
|
executable: "node",
|
|
268
601
|
...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
|
|
269
|
-
...(
|
|
602
|
+
...(settings ? { settings } : {}),
|
|
270
603
|
...modelOption,
|
|
271
604
|
...permissionModeOptions,
|
|
272
605
|
toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
|
|
@@ -281,28 +614,37 @@ export function buildQueryOptions(params) {
|
|
|
281
614
|
...(params.sdkDebugFile ? { debugFile: params.sdkDebugFile } : {}),
|
|
282
615
|
stderr: (line) => {
|
|
283
616
|
if (line.trim().length > 0) {
|
|
284
|
-
|
|
617
|
+
logSdkStderrLine(line);
|
|
285
618
|
}
|
|
286
619
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
620
|
+
spawnClaudeCodeProcess: (options) => {
|
|
621
|
+
logSdkProcessSpawnStarted(options, params.enableSpawnDebug);
|
|
622
|
+
const child = spawnChild(options.command, options.args, {
|
|
623
|
+
cwd: options.cwd,
|
|
624
|
+
env: options.env,
|
|
625
|
+
signal: options.signal,
|
|
626
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
627
|
+
windowsHide: true,
|
|
628
|
+
});
|
|
629
|
+
logSdkProcessSpawned(params.sessionIdForLogs() || undefined, child, options.cwd);
|
|
630
|
+
child.on("error", (error) => {
|
|
631
|
+
const sessionId = params.sessionIdForLogs();
|
|
632
|
+
bridgeLogger.error({
|
|
633
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
634
|
+
eventName: "sdk_spawn_failed",
|
|
635
|
+
message: "Claude Code process spawn failed",
|
|
636
|
+
outcome: "failure",
|
|
637
|
+
...(sessionId ? { sessionId } : {}),
|
|
638
|
+
errorCode: error.code ?? "<none>",
|
|
639
|
+
fields: { error_message: error.message },
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
child.on("exit", (code, signal) => {
|
|
643
|
+
logSdkProcessExit(params.sessionIdForLogs() || undefined, code, signal);
|
|
644
|
+
});
|
|
645
|
+
return child;
|
|
646
|
+
},
|
|
647
|
+
// Match the Claude Code CLI defaults to avoid emitting an empty
|
|
306
648
|
// --setting-sources argument.
|
|
307
649
|
settingSources: DEFAULT_SETTING_SOURCES,
|
|
308
650
|
resume: params.resume,
|
|
@@ -333,14 +675,32 @@ export function buildQueryOptions(params) {
|
|
|
333
675
|
? { requested_schema: request.requestedSchema }
|
|
334
676
|
: {}),
|
|
335
677
|
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
678
|
+
bridgeLogger.info({
|
|
679
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
680
|
+
eventName: "elicitation_request_created",
|
|
681
|
+
message: "elicitation request created",
|
|
682
|
+
outcome: "start",
|
|
683
|
+
sessionId: params.sessionIdForLogs(),
|
|
684
|
+
requestId,
|
|
685
|
+
fields: {
|
|
686
|
+
server_name: normalized.server_name,
|
|
687
|
+
mode: normalized.mode,
|
|
688
|
+
has_url: normalized.url !== undefined,
|
|
689
|
+
},
|
|
340
690
|
});
|
|
691
|
+
emitElicitationRequestEvent(params.sessionIdForLogs(), normalized);
|
|
341
692
|
return await new Promise((resolve) => {
|
|
342
693
|
const currentSession = sessions.get(params.sessionIdForLogs());
|
|
343
694
|
if (!currentSession) {
|
|
695
|
+
bridgeLogger.warn({
|
|
696
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
697
|
+
eventName: "elicitation_request_dropped",
|
|
698
|
+
message: "elicitation request dropped without an active session",
|
|
699
|
+
outcome: "dropped",
|
|
700
|
+
sessionId: params.sessionIdForLogs(),
|
|
701
|
+
requestId,
|
|
702
|
+
fields: { reason: "unknown_session" },
|
|
703
|
+
});
|
|
344
704
|
resolve({ action: "cancel" });
|
|
345
705
|
return;
|
|
346
706
|
}
|
|
@@ -386,29 +746,76 @@ export function mapAvailableModels(models) {
|
|
|
386
746
|
}));
|
|
387
747
|
}
|
|
388
748
|
export function handlePermissionResponse(command) {
|
|
749
|
+
bridgeLogger.info({
|
|
750
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
751
|
+
eventName: "permission_response_received",
|
|
752
|
+
message: "permission response received",
|
|
753
|
+
outcome: "success",
|
|
754
|
+
sessionId: command.session_id,
|
|
755
|
+
toolCallId: command.tool_call_id,
|
|
756
|
+
fields: {
|
|
757
|
+
response_kind: command.outcome.outcome,
|
|
758
|
+
selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
|
|
759
|
+
},
|
|
760
|
+
});
|
|
389
761
|
const session = sessionById(command.session_id);
|
|
390
762
|
if (!session) {
|
|
391
|
-
|
|
763
|
+
bridgeLogger.warn({
|
|
764
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
765
|
+
eventName: "permission_response_dropped",
|
|
766
|
+
message: "permission response dropped for unknown session",
|
|
767
|
+
outcome: "dropped",
|
|
768
|
+
sessionId: command.session_id,
|
|
769
|
+
toolCallId: command.tool_call_id,
|
|
770
|
+
fields: { reason: "unknown_session" },
|
|
771
|
+
});
|
|
392
772
|
return;
|
|
393
773
|
}
|
|
394
774
|
const resolver = session.pendingPermissions.get(command.tool_call_id);
|
|
395
775
|
if (!resolver) {
|
|
396
|
-
|
|
776
|
+
bridgeLogger.warn({
|
|
777
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
778
|
+
eventName: "permission_response_dropped",
|
|
779
|
+
message: "permission response dropped without a pending resolver",
|
|
780
|
+
outcome: "dropped",
|
|
781
|
+
sessionId: command.session_id,
|
|
782
|
+
toolCallId: command.tool_call_id,
|
|
783
|
+
fields: { reason: "missing_pending_resolver" },
|
|
784
|
+
});
|
|
397
785
|
return;
|
|
398
786
|
}
|
|
399
787
|
session.pendingPermissions.delete(command.tool_call_id);
|
|
400
788
|
const outcome = command.outcome;
|
|
401
789
|
if (resolver.onOutcome) {
|
|
790
|
+
bridgeLogger.info({
|
|
791
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
792
|
+
eventName: "permission_response_applied",
|
|
793
|
+
message: "permission response applied to outcome callback",
|
|
794
|
+
outcome: "success",
|
|
795
|
+
sessionId: command.session_id,
|
|
796
|
+
toolCallId: command.tool_call_id,
|
|
797
|
+
fields: {
|
|
798
|
+
tool_name: resolver.toolName,
|
|
799
|
+
response_kind: outcome.outcome,
|
|
800
|
+
selected_option: outcome.outcome === "selected" ? outcome.option_id : "cancelled",
|
|
801
|
+
},
|
|
802
|
+
});
|
|
402
803
|
resolver.onOutcome(outcome);
|
|
403
804
|
return;
|
|
404
805
|
}
|
|
405
806
|
if (!resolver.resolve) {
|
|
406
|
-
|
|
807
|
+
bridgeLogger.warn({
|
|
808
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
809
|
+
eventName: "permission_response_dropped",
|
|
810
|
+
message: "permission response dropped because resolver callback was missing",
|
|
811
|
+
outcome: "dropped",
|
|
812
|
+
sessionId: command.session_id,
|
|
813
|
+
toolCallId: command.tool_call_id,
|
|
814
|
+
fields: { reason: "missing_resolver_callback" },
|
|
815
|
+
});
|
|
407
816
|
return;
|
|
408
817
|
}
|
|
409
818
|
const selectedOption = outcome.outcome === "selected" ? outcome.option_id : "cancelled";
|
|
410
|
-
logPermissionDebug(`response session_id=${command.session_id} tool_call_id=${command.tool_call_id} tool=${resolver.toolName} ` +
|
|
411
|
-
`selected=${selectedOption} suggestions=${formatPermissionUpdates(resolver.suggestions)}`);
|
|
412
819
|
if (outcome.outcome === "selected" &&
|
|
413
820
|
(outcome.option_id === "allow_once" ||
|
|
414
821
|
outcome.option_id === "allow_session" ||
|
|
@@ -422,45 +829,354 @@ export function handlePermissionResponse(command) {
|
|
|
422
829
|
setToolCallStatus(session, command.tool_call_id, "failed", "Permission cancelled");
|
|
423
830
|
}
|
|
424
831
|
const permissionResult = permissionResultFromOutcome(outcome, command.tool_call_id, resolver.inputData, resolver.suggestions, resolver.toolName);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
832
|
+
bridgeLogger.info({
|
|
833
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
834
|
+
eventName: "permission_response_applied",
|
|
835
|
+
message: "permission response applied",
|
|
836
|
+
outcome: "success",
|
|
837
|
+
sessionId: command.session_id,
|
|
838
|
+
toolCallId: command.tool_call_id,
|
|
839
|
+
fields: {
|
|
840
|
+
tool_name: resolver.toolName,
|
|
841
|
+
response_kind: outcome.outcome,
|
|
842
|
+
selected_option: selectedOption,
|
|
843
|
+
behavior: permissionResult.behavior,
|
|
844
|
+
},
|
|
845
|
+
});
|
|
432
846
|
resolver.resolve(permissionResult);
|
|
433
847
|
}
|
|
434
848
|
export function handleQuestionResponse(command) {
|
|
849
|
+
bridgeLogger.info({
|
|
850
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
851
|
+
eventName: "question_response_received",
|
|
852
|
+
message: "question response received",
|
|
853
|
+
outcome: "success",
|
|
854
|
+
sessionId: command.session_id,
|
|
855
|
+
toolCallId: command.tool_call_id,
|
|
856
|
+
fields: { response_kind: command.outcome.outcome },
|
|
857
|
+
});
|
|
435
858
|
const session = sessionById(command.session_id);
|
|
436
859
|
if (!session) {
|
|
437
|
-
|
|
860
|
+
bridgeLogger.warn({
|
|
861
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
862
|
+
eventName: "question_response_dropped",
|
|
863
|
+
message: "question response dropped for unknown session",
|
|
864
|
+
outcome: "dropped",
|
|
865
|
+
sessionId: command.session_id,
|
|
866
|
+
toolCallId: command.tool_call_id,
|
|
867
|
+
fields: { reason: "unknown_session" },
|
|
868
|
+
});
|
|
438
869
|
return;
|
|
439
870
|
}
|
|
440
871
|
const resolver = session.pendingQuestions.get(command.tool_call_id);
|
|
441
872
|
if (!resolver) {
|
|
442
|
-
|
|
873
|
+
bridgeLogger.warn({
|
|
874
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
875
|
+
eventName: "question_response_dropped",
|
|
876
|
+
message: "question response dropped without a pending resolver",
|
|
877
|
+
outcome: "dropped",
|
|
878
|
+
sessionId: command.session_id,
|
|
879
|
+
toolCallId: command.tool_call_id,
|
|
880
|
+
fields: { reason: "missing_pending_resolver" },
|
|
881
|
+
});
|
|
443
882
|
return;
|
|
444
883
|
}
|
|
445
884
|
session.pendingQuestions.delete(command.tool_call_id);
|
|
885
|
+
bridgeLogger.info({
|
|
886
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
887
|
+
eventName: "question_response_applied",
|
|
888
|
+
message: "question response applied",
|
|
889
|
+
outcome: "success",
|
|
890
|
+
sessionId: command.session_id,
|
|
891
|
+
toolCallId: command.tool_call_id,
|
|
892
|
+
fields: {
|
|
893
|
+
tool_name: resolver.toolName,
|
|
894
|
+
response_kind: command.outcome.outcome,
|
|
895
|
+
selected_option_count: command.outcome.outcome === "answered" ? command.outcome.selected_option_ids.length : 0,
|
|
896
|
+
has_annotation: command.outcome.outcome === "answered" && command.outcome.annotation !== undefined,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
446
899
|
resolver.onOutcome(command.outcome);
|
|
447
900
|
}
|
|
448
901
|
export function handleElicitationResponse(command) {
|
|
902
|
+
bridgeLogger.info({
|
|
903
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
904
|
+
eventName: "elicitation_response_received",
|
|
905
|
+
message: "elicitation response received",
|
|
906
|
+
outcome: "success",
|
|
907
|
+
sessionId: command.session_id,
|
|
908
|
+
requestId: command.elicitation_request_id,
|
|
909
|
+
fields: {
|
|
910
|
+
action: command.action,
|
|
911
|
+
has_content: command.content !== undefined,
|
|
912
|
+
},
|
|
913
|
+
});
|
|
449
914
|
const session = sessionById(command.session_id);
|
|
450
915
|
if (!session) {
|
|
451
|
-
|
|
452
|
-
|
|
916
|
+
bridgeLogger.warn({
|
|
917
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
918
|
+
eventName: "elicitation_response_dropped",
|
|
919
|
+
message: "elicitation response dropped for unknown session",
|
|
920
|
+
outcome: "dropped",
|
|
921
|
+
sessionId: command.session_id,
|
|
922
|
+
requestId: command.elicitation_request_id,
|
|
923
|
+
fields: { reason: "unknown_session" },
|
|
924
|
+
});
|
|
453
925
|
return;
|
|
454
926
|
}
|
|
455
927
|
const pending = session.pendingElicitations.get(command.elicitation_request_id);
|
|
456
928
|
if (!pending) {
|
|
457
|
-
|
|
458
|
-
|
|
929
|
+
bridgeLogger.warn({
|
|
930
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
931
|
+
eventName: "elicitation_response_dropped",
|
|
932
|
+
message: "elicitation response dropped without pending request",
|
|
933
|
+
outcome: "dropped",
|
|
934
|
+
sessionId: command.session_id,
|
|
935
|
+
requestId: command.elicitation_request_id,
|
|
936
|
+
fields: { reason: "missing_pending_request" },
|
|
937
|
+
});
|
|
459
938
|
return;
|
|
460
939
|
}
|
|
461
940
|
session.pendingElicitations.delete(command.elicitation_request_id);
|
|
941
|
+
bridgeLogger.info({
|
|
942
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
943
|
+
eventName: "elicitation_response_applied",
|
|
944
|
+
message: "elicitation response applied",
|
|
945
|
+
outcome: "success",
|
|
946
|
+
sessionId: command.session_id,
|
|
947
|
+
requestId: command.elicitation_request_id,
|
|
948
|
+
fields: {
|
|
949
|
+
action: command.action,
|
|
950
|
+
server_name: pending.serverName,
|
|
951
|
+
has_content: command.content !== undefined,
|
|
952
|
+
},
|
|
953
|
+
});
|
|
462
954
|
pending.resolve({
|
|
463
955
|
action: command.action,
|
|
464
|
-
...(command.content ? {
|
|
956
|
+
...(normalizeSdkElicitationContent(command.content) ? {
|
|
957
|
+
content: normalizeSdkElicitationContent(command.content),
|
|
958
|
+
} : {}),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
function normalizeModelKey(id) {
|
|
962
|
+
const original = id.trim();
|
|
963
|
+
if (!original || original === DEFAULT_MODEL_NAME) {
|
|
964
|
+
return { original, family: "default", versionParts: [], variantParts: [] };
|
|
965
|
+
}
|
|
966
|
+
const lower = original.toLowerCase();
|
|
967
|
+
const contextMatch = lower.match(/\[([^\]]+)\]$/);
|
|
968
|
+
const contextSuffix = contextMatch?.[1];
|
|
969
|
+
const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
|
|
970
|
+
const withoutPrefix = withoutContext.startsWith("claude-")
|
|
971
|
+
? withoutContext.slice("claude-".length)
|
|
972
|
+
: withoutContext;
|
|
973
|
+
const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
|
|
974
|
+
const familyPart = parts[0] ?? "";
|
|
975
|
+
const family = familyPart === "opus" || familyPart === "sonnet" || familyPart === "haiku"
|
|
976
|
+
? familyPart
|
|
977
|
+
: "unknown";
|
|
978
|
+
const versionParts = family === "unknown"
|
|
979
|
+
? []
|
|
980
|
+
: parts
|
|
981
|
+
.slice(1)
|
|
982
|
+
.filter((part) => /^\d+$/.test(part))
|
|
983
|
+
.map((part) => Number.parseInt(part, 10))
|
|
984
|
+
.filter((part) => Number.isFinite(part));
|
|
985
|
+
const variantParts = family === "unknown"
|
|
986
|
+
? []
|
|
987
|
+
: parts
|
|
988
|
+
.slice(1)
|
|
989
|
+
.filter((part) => !/^\d+$/.test(part));
|
|
990
|
+
return {
|
|
991
|
+
original,
|
|
992
|
+
family,
|
|
993
|
+
versionParts,
|
|
994
|
+
variantParts,
|
|
995
|
+
...(contextSuffix ? { contextSuffix } : {}),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function modelKeysAreCompatible(leftId, rightId) {
|
|
999
|
+
const left = normalizeModelKey(leftId);
|
|
1000
|
+
const right = normalizeModelKey(rightId);
|
|
1001
|
+
if (left.family === "default" || right.family === "default") {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
if (left.family === "unknown" || right.family === "unknown") {
|
|
1005
|
+
return left.original.toLowerCase() === right.original.toLowerCase();
|
|
1006
|
+
}
|
|
1007
|
+
if (left.family !== right.family) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
if (left.variantParts.join(".") !== right.variantParts.join(".")) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
if (left.versionParts.length === 0 || right.versionParts.length === 0) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
return left.versionParts.join(".") === right.versionParts.join(".");
|
|
1017
|
+
}
|
|
1018
|
+
function sameContextSuffix(leftId, rightId) {
|
|
1019
|
+
const left = normalizeModelKey(leftId);
|
|
1020
|
+
const right = normalizeModelKey(rightId);
|
|
1021
|
+
return (left.contextSuffix?.toLowerCase() ?? "") === (right.contextSuffix?.toLowerCase() ?? "");
|
|
1022
|
+
}
|
|
1023
|
+
function sameFamilyAndVersion(leftId, rightId) {
|
|
1024
|
+
const left = normalizeModelKey(leftId);
|
|
1025
|
+
const right = normalizeModelKey(rightId);
|
|
1026
|
+
if (left.family === "default" || right.family === "default") {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
if (left.family === "unknown" || right.family === "unknown") {
|
|
1030
|
+
return left.original.toLowerCase() === right.original.toLowerCase();
|
|
1031
|
+
}
|
|
1032
|
+
if (left.family !== right.family) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
if (left.versionParts.length === 0 || right.versionParts.length === 0) {
|
|
1036
|
+
return left.versionParts.length === right.versionParts.length;
|
|
1037
|
+
}
|
|
1038
|
+
return left.versionParts.join(".") === right.versionParts.join(".");
|
|
1039
|
+
}
|
|
1040
|
+
function hasVariantSiblingConflict(availableModels, candidateId, resolvedId) {
|
|
1041
|
+
if (sameContextSuffix(candidateId, resolvedId)) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
const resolvedContext = normalizeModelKey(resolvedId).contextSuffix?.toLowerCase() ?? "";
|
|
1045
|
+
if (!resolvedContext) {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
return availableModels.some((entry) => {
|
|
1049
|
+
if (entry.id === candidateId) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
if (!sameFamilyAndVersion(entry.id, resolvedId)) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
const entryContext = normalizeModelKey(entry.id).contextSuffix?.toLowerCase() ?? "";
|
|
1056
|
+
return entryContext === resolvedContext;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
function humanizeModelId(id) {
|
|
1060
|
+
const normalized = normalizeModelKey(id);
|
|
1061
|
+
if (normalized.family === "default") {
|
|
1062
|
+
return "Default";
|
|
1063
|
+
}
|
|
1064
|
+
if (normalized.family === "unknown") {
|
|
1065
|
+
return id;
|
|
1066
|
+
}
|
|
1067
|
+
const familyLabel = normalized.family === "opus"
|
|
1068
|
+
? "Opus"
|
|
1069
|
+
: normalized.family === "sonnet"
|
|
1070
|
+
? "Sonnet"
|
|
1071
|
+
: "Haiku";
|
|
1072
|
+
const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
|
|
1073
|
+
const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
|
|
1074
|
+
? " [1M]"
|
|
1075
|
+
: normalized.contextSuffix
|
|
1076
|
+
? ` [${normalized.contextSuffix}]`
|
|
1077
|
+
: "";
|
|
1078
|
+
return `${familyLabel}${versionLabel}${contextLabel}`;
|
|
1079
|
+
}
|
|
1080
|
+
function shortDisplayNameForModelId(id) {
|
|
1081
|
+
const normalized = normalizeModelKey(id);
|
|
1082
|
+
if (normalized.family === "default") {
|
|
1083
|
+
return "Default";
|
|
1084
|
+
}
|
|
1085
|
+
if (normalized.family === "unknown") {
|
|
1086
|
+
return id;
|
|
1087
|
+
}
|
|
1088
|
+
const familyLabel = normalized.family === "opus"
|
|
1089
|
+
? "Opus"
|
|
1090
|
+
: normalized.family === "sonnet"
|
|
1091
|
+
? "Sonnet"
|
|
1092
|
+
: "Haiku";
|
|
1093
|
+
const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
|
|
1094
|
+
? " [1M]"
|
|
1095
|
+
: normalized.contextSuffix
|
|
1096
|
+
? ` [${normalized.contextSuffix}]`
|
|
1097
|
+
: "";
|
|
1098
|
+
return `${familyLabel}${contextLabel}`;
|
|
1099
|
+
}
|
|
1100
|
+
function currentModelIsAuthoritative(resolvedId, requestedId) {
|
|
1101
|
+
const resolved = resolvedId.trim();
|
|
1102
|
+
if (!resolved || resolved === DEFAULT_MODEL_NAME || resolved === "Connecting...") {
|
|
1103
|
+
return Boolean(requestedId?.trim() && requestedId.trim() !== DEFAULT_MODEL_NAME);
|
|
1104
|
+
}
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
function resolveCatalogModel(availableModels, resolvedId, requestedId) {
|
|
1108
|
+
const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
|
|
1109
|
+
if (exactResolved) {
|
|
1110
|
+
return exactResolved;
|
|
1111
|
+
}
|
|
1112
|
+
if (requestedId) {
|
|
1113
|
+
const exactRequested = availableModels.find((entry) => entry.id === requestedId);
|
|
1114
|
+
if (exactRequested &&
|
|
1115
|
+
modelKeysAreCompatible(exactRequested.id, resolvedId) &&
|
|
1116
|
+
!hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
|
|
1117
|
+
return exactRequested;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId) &&
|
|
1121
|
+
!hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
|
|
1122
|
+
return compatible.length === 1 ? compatible[0] : undefined;
|
|
1123
|
+
}
|
|
1124
|
+
export function resolveCurrentModel(session) {
|
|
1125
|
+
const requestedId = session.requestedModelId?.trim() || undefined;
|
|
1126
|
+
const resolvedId = session.resolvedRuntimeModelId?.trim() ||
|
|
1127
|
+
session.model.trim() ||
|
|
1128
|
+
requestedId ||
|
|
1129
|
+
DEFAULT_MODEL_NAME;
|
|
1130
|
+
const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
|
|
1131
|
+
const runtimeDisplayId = resolvedId || requestedId || DEFAULT_MODEL_NAME;
|
|
1132
|
+
const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
|
|
1133
|
+
const displayNameLong = humanizeModelId(runtimeDisplayId);
|
|
1134
|
+
const currentModel = {
|
|
1135
|
+
resolved_id: resolvedId,
|
|
1136
|
+
display_name_short: displayNameShort,
|
|
1137
|
+
display_name_long: displayNameLong,
|
|
1138
|
+
supports_effort: catalogModel?.supports_effort === true,
|
|
1139
|
+
supported_effort_levels: catalogModel?.supported_effort_levels ?? [],
|
|
1140
|
+
is_authoritative: currentModelIsAuthoritative(resolvedId, requestedId),
|
|
1141
|
+
...(requestedId ? { requested_id: requestedId } : {}),
|
|
1142
|
+
...(catalogModel ? { catalog_id: catalogModel.id } : {}),
|
|
1143
|
+
...(catalogModel?.supports_fast_mode !== undefined
|
|
1144
|
+
? { supports_fast_mode: catalogModel.supports_fast_mode }
|
|
1145
|
+
: {}),
|
|
1146
|
+
...(catalogModel?.supports_auto_mode !== undefined
|
|
1147
|
+
? { supports_auto_mode: catalogModel.supports_auto_mode }
|
|
1148
|
+
: {}),
|
|
1149
|
+
...(catalogModel?.supports_adaptive_thinking !== undefined
|
|
1150
|
+
? { supports_adaptive_thinking: catalogModel.supports_adaptive_thinking }
|
|
1151
|
+
: {}),
|
|
1152
|
+
};
|
|
1153
|
+
return currentModel;
|
|
1154
|
+
}
|
|
1155
|
+
export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {
|
|
1156
|
+
const previousRequested = previousRequestedId?.trim() || previousSessionModel.trim();
|
|
1157
|
+
return previousRequested !== nextRequestedId.trim();
|
|
1158
|
+
}
|
|
1159
|
+
function currentModelsEqual(left, right) {
|
|
1160
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1161
|
+
}
|
|
1162
|
+
export function emitCurrentModelUpdate(session) {
|
|
1163
|
+
if (!session.connected || !session.currentModel) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
emitSessionUpdate(session.sessionId, {
|
|
1167
|
+
type: "current_model_update",
|
|
1168
|
+
current_model: session.currentModel,
|
|
465
1169
|
});
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
export function refreshCurrentModel(session, emitUpdate = false) {
|
|
1173
|
+
const nextModel = resolveCurrentModel(session);
|
|
1174
|
+
if (currentModelsEqual(session.currentModel, nextModel)) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
session.currentModel = nextModel;
|
|
1178
|
+
if (emitUpdate) {
|
|
1179
|
+
emitCurrentModelUpdate(session);
|
|
1180
|
+
}
|
|
1181
|
+
return true;
|
|
466
1182
|
}
|