claude-code-rust 0.14.0 → 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/README.md +5 -9
- package/agent-sdk/dist/bridge/account_metadata.js +1 -0
- 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 +35 -9
- package/agent-sdk/dist/bridge/session_lifecycle.js +70 -11
- package/agent-sdk/dist/bridge/state_parsing.js +35 -0
- package/agent-sdk/dist/bridge/tool_calls.js +45 -10
- package/agent-sdk/dist/bridge/tooling.js +106 -8
- package/agent-sdk/dist/bridge.js +135 -629
- package/package.json +8 -8
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { buildModeState, markModeUnavailableForSession, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, toPermissionMode, } from "./commands.js";
|
|
2
|
+
import { dispatchCancelTurnCommand } from "./command_dispatch.js";
|
|
3
|
+
import { emitFastModeUpdate } from "./error_classification.js";
|
|
4
|
+
import { emitSessionUpdate, slashError } from "./events.js";
|
|
5
|
+
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
6
|
+
import { emitCurrentModelUpdate, refreshCurrentModel, sessionById, shouldInvalidateResolvedRuntimeModel, } from "./session_lifecycle.js";
|
|
7
|
+
export async function handleSessionControlCommand(command, requestId, deps) {
|
|
8
|
+
switch (command.command) {
|
|
9
|
+
case "prompt":
|
|
10
|
+
handlePrompt(command, requestId, deps);
|
|
11
|
+
return;
|
|
12
|
+
case "cancel_turn":
|
|
13
|
+
await dispatchCancelTurnCommand(command, { requestId, sessionById, slashError });
|
|
14
|
+
return;
|
|
15
|
+
case "set_model":
|
|
16
|
+
await setModel(command, requestId);
|
|
17
|
+
return;
|
|
18
|
+
case "set_mode":
|
|
19
|
+
await setMode(command, requestId);
|
|
20
|
+
return;
|
|
21
|
+
case "set_effort":
|
|
22
|
+
await setEffort(command, requestId, deps);
|
|
23
|
+
return;
|
|
24
|
+
case "set_agent":
|
|
25
|
+
await setAgent(command, requestId, deps);
|
|
26
|
+
return;
|
|
27
|
+
case "set_fast_mode":
|
|
28
|
+
await setFastMode(command, requestId, deps);
|
|
29
|
+
return;
|
|
30
|
+
case "reload_plugins":
|
|
31
|
+
await reloadPlugins(command, requestId, deps);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function handlePrompt(command, requestId, deps) {
|
|
35
|
+
const session = requireSession(command.session_id, requestId);
|
|
36
|
+
if (!session) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const message = deps.buildPromptUserMessage(command, session.sessionId);
|
|
40
|
+
if (message) {
|
|
41
|
+
session.input.enqueue(message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function setModel(command, requestId) {
|
|
45
|
+
const session = requireSession(command.session_id, requestId);
|
|
46
|
+
if (!session) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
bridgeLogger.info({
|
|
50
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
51
|
+
eventName: "set_model_started",
|
|
52
|
+
message: "set model started",
|
|
53
|
+
outcome: "start",
|
|
54
|
+
sessionId: session.sessionId,
|
|
55
|
+
requestId,
|
|
56
|
+
fields: {
|
|
57
|
+
requested_model: command.model,
|
|
58
|
+
previous_requested_model: session.requestedModelId,
|
|
59
|
+
previous_session_model: session.model,
|
|
60
|
+
previous_resolved_runtime_model: session.resolvedRuntimeModelId,
|
|
61
|
+
previous_current_model: session.currentModel?.resolved_id,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
const previousRequestedModel = session.requestedModelId;
|
|
66
|
+
const previousSessionModel = session.model;
|
|
67
|
+
await session.query.setModel(command.model);
|
|
68
|
+
session.requestedModelId = command.model;
|
|
69
|
+
session.model = command.model;
|
|
70
|
+
const invalidatedResolvedRuntimeModel = shouldInvalidateResolvedRuntimeModel(previousRequestedModel, previousSessionModel, command.model);
|
|
71
|
+
if (invalidatedResolvedRuntimeModel) {
|
|
72
|
+
session.resolvedRuntimeModelId = undefined;
|
|
73
|
+
}
|
|
74
|
+
const changed = refreshCurrentModel(session, true);
|
|
75
|
+
const forcedCurrentModelUpdate = !changed && emitCurrentModelUpdate(session);
|
|
76
|
+
bridgeLogger.info({
|
|
77
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
78
|
+
eventName: "set_model_succeeded",
|
|
79
|
+
message: "set model completed",
|
|
80
|
+
outcome: "success",
|
|
81
|
+
sessionId: session.sessionId,
|
|
82
|
+
requestId,
|
|
83
|
+
fields: {
|
|
84
|
+
requested_model: command.model,
|
|
85
|
+
session_model_after: session.model,
|
|
86
|
+
resolved_runtime_model_after: session.resolvedRuntimeModelId,
|
|
87
|
+
current_model_after: session.currentModel?.resolved_id,
|
|
88
|
+
current_model_display_short: session.currentModel?.display_name_short,
|
|
89
|
+
current_model_display_long: session.currentModel?.display_name_long,
|
|
90
|
+
current_model_update_emitted: changed || forcedCurrentModelUpdate,
|
|
91
|
+
current_model_update_forced: forcedCurrentModelUpdate,
|
|
92
|
+
resolved_runtime_model_invalidated: invalidatedResolvedRuntimeModel,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
refreshSupportedModesForSession(session);
|
|
96
|
+
if (session.mode) {
|
|
97
|
+
emitSessionUpdate(session.sessionId, {
|
|
98
|
+
type: "mode_state_update",
|
|
99
|
+
mode: buildModeState(session, session.mode),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
bridgeLogger.warn({
|
|
106
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
107
|
+
eventName: "set_model_failed",
|
|
108
|
+
message: "set model failed",
|
|
109
|
+
outcome: "failure",
|
|
110
|
+
sessionId: session.sessionId,
|
|
111
|
+
requestId,
|
|
112
|
+
fields: {
|
|
113
|
+
requested_model: command.model,
|
|
114
|
+
error_message: message,
|
|
115
|
+
previous_requested_model: session.requestedModelId,
|
|
116
|
+
previous_session_model: session.model,
|
|
117
|
+
previous_resolved_runtime_model: session.resolvedRuntimeModelId,
|
|
118
|
+
previous_current_model: session.currentModel?.resolved_id,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
slashError(command.session_id, `failed to set model: ${message}`, requestId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function setMode(command, requestId) {
|
|
125
|
+
const session = requireSession(command.session_id, requestId);
|
|
126
|
+
if (!session) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const mode = toPermissionMode(command.mode);
|
|
130
|
+
if (!mode) {
|
|
131
|
+
slashError(command.session_id, `unsupported mode: ${command.mode}`, requestId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await session.query.setPermissionMode(mode);
|
|
136
|
+
session.mode = mode;
|
|
137
|
+
refreshSupportedModesForSession(session);
|
|
138
|
+
emitSessionUpdate(session.sessionId, {
|
|
139
|
+
type: "current_mode_update",
|
|
140
|
+
current_mode_id: mode,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
if (permissionModeFailureLooksUnsupported(mode, message)) {
|
|
146
|
+
const changed = markModeUnavailableForSession(session, mode);
|
|
147
|
+
if (changed && session.mode) {
|
|
148
|
+
emitSessionUpdate(session.sessionId, {
|
|
149
|
+
type: "mode_state_update",
|
|
150
|
+
mode: buildModeState(session, session.mode),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
slashError(command.session_id, `failed to set mode to ${mode}: ${message}`, requestId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function setEffort(command, requestId, deps) {
|
|
158
|
+
const session = requireSession(command.session_id, requestId);
|
|
159
|
+
if (!session) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
await deps.applySessionEffort(session.query, command.effort);
|
|
164
|
+
deps.emitEffortConfigOptionUpdate(session.sessionId, command.effort);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
slashError(command.session_id, `failed to set effort: ${message}`, requestId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function setAgent(command, requestId, deps) {
|
|
172
|
+
const session = requireSession(command.session_id, requestId);
|
|
173
|
+
if (!session) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await deps.applySessionAgent(session.query, command.agent);
|
|
178
|
+
deps.emitAgentConfigOptionUpdate(session.sessionId, command.agent);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
slashError(command.session_id, `failed to set agent: ${message}`, requestId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function setFastMode(command, requestId, deps) {
|
|
186
|
+
const session = requireSession(command.session_id, requestId);
|
|
187
|
+
if (!session) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
bridgeLogger.info({
|
|
191
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
192
|
+
eventName: "set_fast_mode_started",
|
|
193
|
+
message: "set fast mode started",
|
|
194
|
+
outcome: "start",
|
|
195
|
+
sessionId: session.sessionId,
|
|
196
|
+
requestId,
|
|
197
|
+
fields: {
|
|
198
|
+
requested_enabled: command.enabled,
|
|
199
|
+
previous_state: session.fastModeState,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
const snapshot = await deps.applySessionFastMode(session.query, command.enabled);
|
|
204
|
+
const state = snapshot.state;
|
|
205
|
+
session.fastModeState = state;
|
|
206
|
+
session.fastModeDisabledReason = snapshot.disabled_reason;
|
|
207
|
+
emitFastModeUpdate(session);
|
|
208
|
+
const reportedEnabled = state !== "off";
|
|
209
|
+
if (reportedEnabled !== command.enabled) {
|
|
210
|
+
bridgeLogger.warn({
|
|
211
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
212
|
+
eventName: "set_fast_mode_mismatch",
|
|
213
|
+
message: "SDK reported a fast-mode state that did not match the request",
|
|
214
|
+
outcome: "failure",
|
|
215
|
+
sessionId: session.sessionId,
|
|
216
|
+
requestId,
|
|
217
|
+
fields: {
|
|
218
|
+
requested_enabled: command.enabled,
|
|
219
|
+
reported_state: state,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const action = command.enabled ? "enable" : "disable";
|
|
223
|
+
slashError(command.session_id, `failed to ${action} fast mode: SDK reported state ${state}`, requestId);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
bridgeLogger.info({
|
|
227
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
228
|
+
eventName: "set_fast_mode_succeeded",
|
|
229
|
+
message: "set fast mode completed",
|
|
230
|
+
outcome: "success",
|
|
231
|
+
sessionId: session.sessionId,
|
|
232
|
+
requestId,
|
|
233
|
+
fields: {
|
|
234
|
+
requested_enabled: command.enabled,
|
|
235
|
+
reported_state: state,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
+
bridgeLogger.warn({
|
|
242
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
243
|
+
eventName: "set_fast_mode_failed",
|
|
244
|
+
message: "set fast mode failed",
|
|
245
|
+
outcome: "failure",
|
|
246
|
+
sessionId: session.sessionId,
|
|
247
|
+
requestId,
|
|
248
|
+
fields: {
|
|
249
|
+
requested_enabled: command.enabled,
|
|
250
|
+
previous_state: session.fastModeState,
|
|
251
|
+
error_message: message,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
slashError(command.session_id, `failed to set fast mode: ${message}`, requestId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function reloadPlugins(command, requestId, deps) {
|
|
258
|
+
const session = requireSession(command.session_id, requestId);
|
|
259
|
+
if (session) {
|
|
260
|
+
await deps.handleReloadPluginsCommand(session, requestId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function requireSession(sessionId, requestId) {
|
|
264
|
+
const session = sessionById(sessionId);
|
|
265
|
+
if (!session) {
|
|
266
|
+
slashError(sessionId, `unknown session: ${sessionId}`, requestId);
|
|
267
|
+
}
|
|
268
|
+
return session;
|
|
269
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { getSessionMessages, renameSession, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { mapSdkAccountInfo } from "./account_metadata.js";
|
|
3
|
+
import { emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
|
|
4
|
+
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
5
|
+
import { refreshCurrentModel, sessionById, } from "./session_lifecycle.js";
|
|
6
|
+
export async function handleSessionDataCommand(command, requestId, deps) {
|
|
7
|
+
switch (command.command) {
|
|
8
|
+
case "generate_session_title":
|
|
9
|
+
await generateTitle(command, requestId, deps);
|
|
10
|
+
return;
|
|
11
|
+
case "rename_session":
|
|
12
|
+
await rename(command, requestId, deps);
|
|
13
|
+
return;
|
|
14
|
+
case "get_status_snapshot":
|
|
15
|
+
await getStatusSnapshot(command, requestId);
|
|
16
|
+
return;
|
|
17
|
+
case "get_context_usage":
|
|
18
|
+
await getContextUsage(command, requestId);
|
|
19
|
+
return;
|
|
20
|
+
case "get_rewind_targets":
|
|
21
|
+
await getRewindTargets(command, requestId, deps);
|
|
22
|
+
return;
|
|
23
|
+
case "rewind":
|
|
24
|
+
await deps.handleRewind(command, requestId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function generateTitle(command, requestId, deps) {
|
|
28
|
+
const session = requireSession(command.session_id, requestId);
|
|
29
|
+
if (!session) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await deps.generatePersistedSessionTitle(session.query, command.description);
|
|
34
|
+
setSessionListingDir(session.cwd);
|
|
35
|
+
await emitSessionsList(requestId);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
slashError(command.session_id, `failed to generate session title: ${message}`, requestId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function rename(command, requestId, deps) {
|
|
43
|
+
const session = requireSession(command.session_id, requestId);
|
|
44
|
+
if (!session) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await renameSession(command.session_id, command.title, deps.buildSessionMutationOptions(session.cwd));
|
|
49
|
+
setSessionListingDir(session.cwd);
|
|
50
|
+
await emitSessionsList(requestId);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
slashError(command.session_id, `failed to rename session: ${message}`, requestId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function getStatusSnapshot(command, requestId) {
|
|
58
|
+
const session = requireSession(command.session_id, requestId);
|
|
59
|
+
if (!session) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const account = await session.query.accountInfo();
|
|
64
|
+
bridgeLogger.info({
|
|
65
|
+
target: LOG_TARGETS.APP_AUTH,
|
|
66
|
+
eventName: "status_snapshot_emitted",
|
|
67
|
+
message: "status snapshot emitted",
|
|
68
|
+
outcome: "success",
|
|
69
|
+
...(requestId ? { requestId } : {}),
|
|
70
|
+
sessionId: session.sessionId,
|
|
71
|
+
fields: {
|
|
72
|
+
has_email: typeof account.email === "string" && account.email.trim().length > 0,
|
|
73
|
+
has_organization: account.organization !== undefined,
|
|
74
|
+
subscription_type: account.subscriptionType,
|
|
75
|
+
token_source: account.tokenSource,
|
|
76
|
+
api_key_source: account.apiKeySource,
|
|
77
|
+
api_provider: account.apiProvider,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
writeEvent({
|
|
81
|
+
event: "status_snapshot",
|
|
82
|
+
session_id: session.sessionId,
|
|
83
|
+
account: mapSdkAccountInfo(account),
|
|
84
|
+
}, requestId);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
bridgeLogger.warn({
|
|
89
|
+
target: LOG_TARGETS.APP_AUTH,
|
|
90
|
+
eventName: "status_snapshot_failed",
|
|
91
|
+
message: "failed to build status snapshot",
|
|
92
|
+
outcome: "failure",
|
|
93
|
+
...(requestId ? { requestId } : {}),
|
|
94
|
+
sessionId: session.sessionId,
|
|
95
|
+
fields: { error_message: message },
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function getContextUsage(command, requestId) {
|
|
101
|
+
const session = requireSession(command.session_id, requestId);
|
|
102
|
+
if (!session) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const usage = await session.query.getContextUsage();
|
|
107
|
+
if (typeof usage.model === "string" && usage.model.trim().length > 0) {
|
|
108
|
+
session.resolvedRuntimeModelId = usage.model.trim();
|
|
109
|
+
refreshCurrentModel(session, true);
|
|
110
|
+
}
|
|
111
|
+
const rawPercentage = typeof usage.percentage === "number" ? usage.percentage : undefined;
|
|
112
|
+
const normalizedPercentage = rawPercentage === undefined || !Number.isFinite(rawPercentage)
|
|
113
|
+
? undefined
|
|
114
|
+
: Math.max(0, Math.min(100, Math.round(rawPercentage)));
|
|
115
|
+
bridgeLogger.debug({
|
|
116
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
117
|
+
eventName: "context_usage_succeeded",
|
|
118
|
+
message: "session context usage received from SDK",
|
|
119
|
+
outcome: "success",
|
|
120
|
+
...(requestId ? { requestId } : {}),
|
|
121
|
+
sessionId: session.sessionId,
|
|
122
|
+
fields: {
|
|
123
|
+
raw_percentage: rawPercentage,
|
|
124
|
+
normalized_percentage: normalizedPercentage,
|
|
125
|
+
total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
|
|
126
|
+
max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
|
|
127
|
+
raw_max_tokens: typeof usage.rawMaxTokens === "number" ? usage.rawMaxTokens : undefined,
|
|
128
|
+
model: typeof usage.model === "string" ? usage.model : undefined,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
writeEvent({
|
|
132
|
+
event: "context_usage",
|
|
133
|
+
session_id: session.sessionId,
|
|
134
|
+
...(normalizedPercentage !== undefined ? { percentage: normalizedPercentage } : {}),
|
|
135
|
+
}, requestId);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
bridgeLogger.warn({
|
|
140
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
141
|
+
eventName: "context_usage_failed",
|
|
142
|
+
message: "failed to get session context usage",
|
|
143
|
+
outcome: "failure",
|
|
144
|
+
...(requestId ? { requestId } : {}),
|
|
145
|
+
sessionId: session.sessionId,
|
|
146
|
+
fields: { error_message: message },
|
|
147
|
+
});
|
|
148
|
+
writeEvent({
|
|
149
|
+
event: "context_usage",
|
|
150
|
+
session_id: session.sessionId,
|
|
151
|
+
}, requestId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function getRewindTargets(command, requestId, deps) {
|
|
155
|
+
const session = requireSession(command.session_id, requestId);
|
|
156
|
+
if (!session) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const historyMessages = await getSessionMessages(command.session_id, {
|
|
161
|
+
dir: session.cwd,
|
|
162
|
+
includeSystemMessages: true,
|
|
163
|
+
});
|
|
164
|
+
const targets = deps.rewindTargetsFromSessionMessages(historyMessages);
|
|
165
|
+
bridgeLogger.info({
|
|
166
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
167
|
+
eventName: "rewind_targets_loaded",
|
|
168
|
+
message: "rewind targets loaded from session history",
|
|
169
|
+
outcome: "success",
|
|
170
|
+
...(requestId ? { requestId } : {}),
|
|
171
|
+
sessionId: session.sessionId,
|
|
172
|
+
fields: {
|
|
173
|
+
history_message_count: historyMessages.length,
|
|
174
|
+
target_count: targets.length,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
writeEvent({
|
|
178
|
+
event: "rewind_targets",
|
|
179
|
+
session_id: session.sessionId,
|
|
180
|
+
targets,
|
|
181
|
+
}, requestId);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
185
|
+
bridgeLogger.warn({
|
|
186
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
187
|
+
eventName: "rewind_targets_failed",
|
|
188
|
+
message: "failed to load rewind targets",
|
|
189
|
+
outcome: "failure",
|
|
190
|
+
...(requestId ? { requestId } : {}),
|
|
191
|
+
sessionId: session.sessionId,
|
|
192
|
+
fields: { error_message: message },
|
|
193
|
+
});
|
|
194
|
+
slashError(command.session_id, `failed to load rewind targets: ${message}`, requestId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function requireSession(sessionId, requestId) {
|
|
198
|
+
const session = sessionById(sessionId);
|
|
199
|
+
if (!session) {
|
|
200
|
+
slashError(sessionId, `unknown session: ${sessionId}`, requestId);
|
|
201
|
+
}
|
|
202
|
+
return session;
|
|
203
|
+
}
|
|
@@ -266,6 +266,12 @@ export function parseCommandEnvelope(line) {
|
|
|
266
266
|
session_id: expectString(raw, "session_id", "set_agent"),
|
|
267
267
|
agent: expectNonEmptyStringOrNull(raw, "agent", "set_agent"),
|
|
268
268
|
};
|
|
269
|
+
case "set_fast_mode":
|
|
270
|
+
return {
|
|
271
|
+
command: "set_fast_mode",
|
|
272
|
+
session_id: expectString(raw, "session_id", "set_fast_mode"),
|
|
273
|
+
enabled: expectBoolean(raw, "enabled", "set_fast_mode"),
|
|
274
|
+
};
|
|
269
275
|
case "generate_session_title":
|
|
270
276
|
return {
|
|
271
277
|
command: "generate_session_title",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { looksLikeAuthRequired } from "./auth.js";
|
|
2
2
|
import { writeEvent } from "./events.js";
|
|
3
3
|
import { emitSessionUpdate } from "./events.js";
|
|
4
|
-
import { parseFastModeState } from "./state_parsing.js";
|
|
4
|
+
import { parseFastModeDisabledReason, parseFastModeState } from "./state_parsing.js";
|
|
5
5
|
export function emitAuthRequired(session, detail) {
|
|
6
6
|
if (session.authHintSent) {
|
|
7
7
|
return;
|
|
@@ -58,11 +58,28 @@ export function classifyTurnErrorKind(subtype, errors, assistantError) {
|
|
|
58
58
|
}
|
|
59
59
|
return "other";
|
|
60
60
|
}
|
|
61
|
-
export function
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
export function setFastModeSnapshotIfChanged(session, stateValue, disabledReasonValue) {
|
|
62
|
+
const nextState = parseFastModeState(stateValue) ?? session.fastModeState;
|
|
63
|
+
const nextDisabledReason = parseFastModeDisabledReason(disabledReasonValue);
|
|
64
|
+
if (nextState === session.fastModeState &&
|
|
65
|
+
nextDisabledReason === session.fastModeDisabledReason) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
session.fastModeState = nextState;
|
|
69
|
+
session.fastModeDisabledReason = nextDisabledReason;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
export function emitFastModeUpdate(session) {
|
|
73
|
+
emitSessionUpdate(session.sessionId, {
|
|
74
|
+
type: "fast_mode_update",
|
|
75
|
+
fast_mode_state: session.fastModeState,
|
|
76
|
+
...(session.fastModeDisabledReason
|
|
77
|
+
? { fast_mode_disabled_reason: session.fastModeDisabledReason }
|
|
78
|
+
: {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function emitFastModeUpdateIfChanged(session, stateValue, disabledReasonValue) {
|
|
82
|
+
if (setFastModeSnapshotIfChanged(session, stateValue, disabledReasonValue)) {
|
|
83
|
+
emitFastModeUpdate(session);
|
|
65
84
|
}
|
|
66
|
-
session.fastModeState = next;
|
|
67
|
-
emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
|
|
68
85
|
}
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { listSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
2
3
|
import { buildModeState } from "./commands.js";
|
|
3
4
|
import { mapSdkSessions } from "./history.js";
|
|
4
5
|
import { bridgeLogger, LOG_TARGETS, logBridgeEventSent } from "./logger.js";
|
|
5
|
-
import { resolveCurrentModel } from "./session_lifecycle.js";
|
|
6
|
+
import { detachSessionForClose, resolveCurrentModel, trackSessionCloseTask, } from "./session_lifecycle.js";
|
|
6
7
|
const SESSION_LIST_LIMIT = 50;
|
|
7
8
|
let sessionListingDir;
|
|
9
|
+
function writeProtocolEventToStdout(line) {
|
|
10
|
+
const payload = Buffer.from(line);
|
|
11
|
+
let offset = 0;
|
|
12
|
+
while (offset < payload.length) {
|
|
13
|
+
const written = writeSync(process.stdout.fd, payload, offset, payload.length - offset);
|
|
14
|
+
if (written <= 0) {
|
|
15
|
+
throw new Error("bridge stdout write made no progress");
|
|
16
|
+
}
|
|
17
|
+
offset += written;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let protocolEventWriter = writeProtocolEventToStdout;
|
|
21
|
+
export function replaceProtocolEventWriter(writer) {
|
|
22
|
+
const previous = protocolEventWriter;
|
|
23
|
+
protocolEventWriter = writer;
|
|
24
|
+
return () => {
|
|
25
|
+
protocolEventWriter = previous;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
8
28
|
export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
|
|
9
29
|
return dir
|
|
10
30
|
? { dir, includeProgrammatic: true, includeWorktrees: true, limit }
|
|
@@ -23,7 +43,7 @@ export function writeEvent(event, requestId) {
|
|
|
23
43
|
};
|
|
24
44
|
const serialized = JSON.stringify(envelope);
|
|
25
45
|
logBridgeEventSent(event, requestId, Buffer.byteLength(serialized) + 1);
|
|
26
|
-
|
|
46
|
+
protocolEventWriter(`${serialized}\n`);
|
|
27
47
|
}
|
|
28
48
|
export function failConnection(message, requestId) {
|
|
29
49
|
writeEvent({ event: "connection_failed", message }, requestId);
|
|
@@ -112,7 +132,7 @@ export function emitElicitationRequestEvent(sessionId, request) {
|
|
|
112
132
|
});
|
|
113
133
|
writeEvent({ event: "elicitation_request", session_id: sessionId, request });
|
|
114
134
|
}
|
|
115
|
-
function buildConnectBridgeEvent(session, eventName) {
|
|
135
|
+
export function buildConnectBridgeEvent(session, eventName) {
|
|
116
136
|
const historyUpdates = session.resumeUpdates;
|
|
117
137
|
return eventName === "session_replaced"
|
|
118
138
|
? {
|
|
@@ -122,6 +142,10 @@ function buildConnectBridgeEvent(session, eventName) {
|
|
|
122
142
|
current_model: session.currentModel ?? resolveCurrentModel(session),
|
|
123
143
|
available_models: session.availableModels,
|
|
124
144
|
mode: session.mode ? buildModeState(session, session.mode) : null,
|
|
145
|
+
fast_mode_state: session.fastModeState,
|
|
146
|
+
...(session.fastModeDisabledReason
|
|
147
|
+
? { fast_mode_disabled_reason: session.fastModeDisabledReason }
|
|
148
|
+
: {}),
|
|
125
149
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
126
150
|
...(session.restoredInput !== undefined ? { restored_input: session.restoredInput } : {}),
|
|
127
151
|
}
|
|
@@ -132,6 +156,10 @@ function buildConnectBridgeEvent(session, eventName) {
|
|
|
132
156
|
current_model: session.currentModel ?? resolveCurrentModel(session),
|
|
133
157
|
available_models: session.availableModels,
|
|
134
158
|
mode: session.mode ? buildModeState(session, session.mode) : null,
|
|
159
|
+
fast_mode_state: session.fastModeState,
|
|
160
|
+
...(session.fastModeDisabledReason
|
|
161
|
+
? { fast_mode_disabled_reason: session.fastModeDisabledReason }
|
|
162
|
+
: {}),
|
|
135
163
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
136
164
|
};
|
|
137
165
|
}
|
|
@@ -152,6 +180,14 @@ function logConnectEventEmission(session, eventName, requestId) {
|
|
|
152
180
|
});
|
|
153
181
|
}
|
|
154
182
|
export function emitConnectEvent(session) {
|
|
183
|
+
const staleSessions = session.sessionsToCloseAfterConnect;
|
|
184
|
+
if (staleSessions) {
|
|
185
|
+
for (const stale of staleSessions) {
|
|
186
|
+
if (stale !== session) {
|
|
187
|
+
detachSessionForClose(stale);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
155
191
|
const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
|
|
156
192
|
logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
|
|
157
193
|
writeEvent(bridgeEvent, session.connectRequestId);
|
|
@@ -164,26 +200,23 @@ export function emitConnectEvent(session) {
|
|
|
164
200
|
session.authHintSent = false;
|
|
165
201
|
session.resumeUpdates = undefined;
|
|
166
202
|
session.restoredInput = undefined;
|
|
167
|
-
const staleSessions = session.sessionsToCloseAfterConnect;
|
|
168
203
|
session.sessionsToCloseAfterConnect = undefined;
|
|
169
204
|
if (!staleSessions || staleSessions.length === 0) {
|
|
170
205
|
refreshSessionsList();
|
|
171
206
|
return;
|
|
172
207
|
}
|
|
173
|
-
|
|
208
|
+
const closeTask = (async () => {
|
|
174
209
|
// Lazy import to break circular dependency at module-evaluation time.
|
|
175
|
-
const {
|
|
210
|
+
const { closeSessionWithLogging } = await import("./session_lifecycle.js");
|
|
176
211
|
for (const stale of staleSessions) {
|
|
177
212
|
if (stale === session) {
|
|
178
213
|
continue;
|
|
179
214
|
}
|
|
180
|
-
if (sessions.get(stale.sessionId) === stale) {
|
|
181
|
-
sessions.delete(stale.sessionId);
|
|
182
|
-
}
|
|
183
215
|
await closeSessionWithLogging(stale, { reason: "stale_after_connect" });
|
|
184
216
|
}
|
|
185
217
|
refreshSessionsList();
|
|
186
218
|
})();
|
|
219
|
+
trackSessionCloseTask(closeTask);
|
|
187
220
|
}
|
|
188
221
|
export function emitSessionReplacedEvent(session, requestId) {
|
|
189
222
|
const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asRecordOrNull } from "./shared.js";
|
|
2
|
-
import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolSearchToolName, isToolSearchToolResultType, isToolUseBlockType, } from "./tooling.js";
|
|
2
|
+
import { applyToolNonExecutionMetadata, TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolSearchToolName, isToolSearchToolResultType, isToolUseBlockType, parseToolNonExecutionMetadata, } from "./tooling.js";
|
|
3
3
|
function nonEmptyTrimmed(value) {
|
|
4
4
|
if (typeof value !== "string") {
|
|
5
5
|
return undefined;
|
|
@@ -197,7 +197,7 @@ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentTo
|
|
|
197
197
|
toolCalls.set(toolUseId, toolCall);
|
|
198
198
|
updates.push({ type: "tool_call", tool_call: toolCall });
|
|
199
199
|
}
|
|
200
|
-
function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid) {
|
|
200
|
+
function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, nonExecutionByToolUseId, sourceMessageUuid) {
|
|
201
201
|
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
|
|
202
202
|
if (!toolUseId) {
|
|
203
203
|
return;
|
|
@@ -210,6 +210,7 @@ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourc
|
|
|
210
210
|
const isError = Boolean(block.is_error);
|
|
211
211
|
const base = toolCalls.get(toolUseId);
|
|
212
212
|
const fields = buildToolResultFields(isError, block.content, base, block);
|
|
213
|
+
applyToolNonExecutionMetadata(fields, nonExecutionByToolUseId.get(toolUseId));
|
|
213
214
|
updates.push({
|
|
214
215
|
type: "tool_call_update",
|
|
215
216
|
tool_call_update: {
|
|
@@ -294,6 +295,9 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
294
295
|
? message.parent_tool_use_id
|
|
295
296
|
: null;
|
|
296
297
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
298
|
+
const nonExecutionByToolUseId = parseToolNonExecutionMetadata(Object.hasOwn(entry, "tool_result_meta")
|
|
299
|
+
? entry.tool_result_meta
|
|
300
|
+
: message.tool_result_meta);
|
|
297
301
|
for (const item of content) {
|
|
298
302
|
const block = asRecordOrNull(item);
|
|
299
303
|
if (!block) {
|
|
@@ -312,7 +316,7 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
312
316
|
continue;
|
|
313
317
|
}
|
|
314
318
|
if (TOOL_RESULT_TYPES.has(blockType)) {
|
|
315
|
-
pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid);
|
|
319
|
+
pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, nonExecutionByToolUseId, sourceMessageUuid);
|
|
316
320
|
continue;
|
|
317
321
|
}
|
|
318
322
|
if (blockType === "image") {
|