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.
@@ -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 emitFastModeUpdateIfChanged(session, value) {
62
- const next = parseFastModeState(value);
63
- if (!next || next === session.fastModeState) {
64
- return;
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
- process.stdout.write(`${serialized}\n`);
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
- void (async () => {
208
+ const closeTask = (async () => {
174
209
  // Lazy import to break circular dependency at module-evaluation time.
175
- const { sessions, closeSessionWithLogging } = await import("./session_lifecycle.js");
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") {