claude-code-rust 0.14.3 → 0.14.4

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 CHANGED
@@ -2,11 +2,10 @@
2
2
 
3
3
  A native Rust terminal interface for Claude Code. Drop-in replacement for Anthropic's stock Node.js/React Ink TUI, built for performance and a better user experience.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
6
- [![npm downloads](https://img.shields.io/npm/dm/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
5
+ [![Version](https://img.shields.io/github/v/release/srothgan/claude-code-rust?label=version)](https://github.com/srothgan/claude-code-rust/releases/latest)
7
6
  [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/pr.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/pr.yml)
8
7
  [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://srothgan.github.io/claude-code-rust/)
9
- [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
8
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://www.apache.org/licenses/LICENSE-2.0)
10
9
 
11
10
  <p align="center">
12
11
  <img src="assets/banner.png" alt="Claude Code Rust running a Read tool call with syntax-highlighted output" width="900">
@@ -9,7 +9,9 @@ const KNOWN_API_PROVIDERS = new Set([
9
9
  "gateway",
10
10
  ]);
11
11
  function trimmedString(value) {
12
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ return typeof value === "string" && value.trim().length > 0
13
+ ? value.trim()
14
+ : undefined;
13
15
  }
14
16
  export function isKnownApiProvider(provider) {
15
17
  return provider !== undefined && KNOWN_API_PROVIDERS.has(provider);
@@ -20,7 +22,9 @@ export function apiProviderIsExternal(provider) {
20
22
  export function mapSdkAccountInfo(account) {
21
23
  const apiProvider = trimmedString(account.apiProvider);
22
24
  return {
23
- ...(trimmedString(account.email) ? { email: trimmedString(account.email) } : {}),
25
+ ...(trimmedString(account.email)
26
+ ? { email: trimmedString(account.email) }
27
+ : {}),
24
28
  ...(trimmedString(account.organization)
25
29
  ? { organization: trimmedString(account.organization) }
26
30
  : {}),
@@ -23,13 +23,16 @@ export function mapAvailableAgents(value) {
23
23
  continue;
24
24
  }
25
25
  const description = typeof record.description === "string" ? record.description : "";
26
- const model = typeof record.model === "string" && record.model.trim().length > 0 ? record.model : undefined;
26
+ const model = typeof record.model === "string" && record.model.trim().length > 0
27
+ ? record.model
28
+ : undefined;
27
29
  const existing = byName.get(name);
28
30
  if (!existing) {
29
31
  byName.set(name, { name, description, model });
30
32
  continue;
31
33
  }
32
- if (existing.description.trim().length === 0 && description.trim().length > 0) {
34
+ if (existing.description.trim().length === 0 &&
35
+ description.trim().length > 0) {
33
36
  existing.description = description;
34
37
  }
35
38
  if (!existing.model && model) {
@@ -58,7 +61,10 @@ export function emitAvailableAgentsIfChanged(session, agents) {
58
61
  return;
59
62
  }
60
63
  session.lastAvailableAgentsSignature = signature;
61
- emitSessionUpdate(session.sessionId, { type: "available_agents_update", agents });
64
+ emitSessionUpdate(session.sessionId, {
65
+ type: "available_agents_update",
66
+ agents,
67
+ });
62
68
  }
63
69
  export function refreshAvailableAgents(session) {
64
70
  if (typeof session.query.supportedAgents !== "function") {
@@ -4,7 +4,11 @@ function isDynamicSource(source) {
4
4
  return source === "commands_changed" || source === "reload_plugins";
5
5
  }
6
6
  function commandSignature(commands) {
7
- return JSON.stringify(commands.map((command) => [command.name, command.description, command.input_hint ?? ""]));
7
+ return JSON.stringify(commands.map((command) => [
8
+ command.name,
9
+ command.description,
10
+ command.input_hint ?? "",
11
+ ]));
8
12
  }
9
13
  function logAvailableCommandsDecision(session, source, commands, outcome, reason, generation) {
10
14
  bridgeLogger.info({
@@ -35,7 +39,10 @@ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
35
39
  }
36
40
  if (!current) {
37
41
  if (commands.length === 0) {
38
- return { accept: false, reason: "empty bootstrap snapshot ignored before first command list" };
42
+ return {
43
+ accept: false,
44
+ reason: "empty bootstrap snapshot ignored before first command list",
45
+ };
39
46
  }
40
47
  return { accept: true, reason: "initial command snapshot" };
41
48
  }
@@ -54,7 +61,10 @@ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
54
61
  if (commands.length === 0) {
55
62
  return { accept: false, reason: "empty bootstrap snapshot ignored" };
56
63
  }
57
- return { accept: true, reason: "bootstrap refresh before dynamic command source" };
64
+ return {
65
+ accept: true,
66
+ reason: "bootstrap refresh before dynamic command source",
67
+ };
58
68
  }
59
69
  export function updateAvailableCommands(session, source, commands) {
60
70
  const decision = shouldAcceptAvailableCommandsSnapshot(session, source, commands);
@@ -1,9 +1,9 @@
1
1
  import { getSessionMessages, listSessions, } from "@anthropic-ai/claude-agent-sdk";
2
- import { currentSessionListOptions, emitSessionsList, failConnection, setSessionListingDir, slashError, writeEvent, } from "./events.js";
2
+ import { currentSessionListOptions, emitSessionResumeFailed, emitSessionsList, failConnection, setSessionListingDir, slashError, writeEvent, } from "./events.js";
3
3
  import { mapSessionMessagesToUpdates } from "./history.js";
4
4
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
5
- import { closeAllSessions, createSession, sessions, } from "./session_lifecycle.js";
6
- export async function handleLifecycleCommand(command, requestId, sdkVersionError) {
5
+ import { awaitSessionInitialization, closeAllSessions, closeSessionWithLogging, commitDeferredSession, createSession, detachSessionForClose, sessions, } from "./session_lifecycle.js";
6
+ export async function handleLifecycleCommand(command, requestId, sdkVersionError, deps) {
7
7
  switch (command.command) {
8
8
  case "initialize":
9
9
  await initialize(command, requestId, sdkVersionError);
@@ -14,6 +14,9 @@ export async function handleLifecycleCommand(command, requestId, sdkVersionError
14
14
  case "resume_session":
15
15
  await resume(command, requestId);
16
16
  return;
17
+ case "resume_session_at":
18
+ await resumeAt(command, requestId, deps.buildRewindConversationPlan);
19
+ return;
17
20
  case "new_session":
18
21
  await replace(command, requestId);
19
22
  return;
@@ -21,6 +24,76 @@ export async function handleLifecycleCommand(command, requestId, sdkVersionError
21
24
  await shutdown(requestId);
22
25
  }
23
26
  }
27
+ async function resumeAt(command, requestId, buildPlan) {
28
+ if (!requestId) {
29
+ slashError(command.session_id, "resume before a selected message requires an operation id");
30
+ return;
31
+ }
32
+ const targetUserMessageId = command.target_user_message_id.trim();
33
+ if (!targetUserMessageId) {
34
+ emitSessionResumeFailed(command.session_id, requestId, "resume target cannot be empty");
35
+ return;
36
+ }
37
+ let candidate;
38
+ try {
39
+ const sdkSessions = await listSessions(currentSessionListOptions());
40
+ const matched = sdkSessions.find((entry) => entry.sessionId === command.session_id);
41
+ if (!matched) {
42
+ emitSessionResumeFailed(command.session_id, requestId, `unknown session: ${command.session_id}`);
43
+ return;
44
+ }
45
+ const cwd = matched.cwd ?? process.cwd();
46
+ setSessionListingDir(cwd);
47
+ const historyMessages = await getSessionMessages(command.session_id, {
48
+ dir: cwd,
49
+ includeSystemMessages: true,
50
+ });
51
+ const plan = buildPlan(historyMessages, targetUserMessageId);
52
+ if (!plan) {
53
+ throw new Error(`stale or inconsistent resume target: ${targetUserMessageId}`);
54
+ }
55
+ const staleSessions = Array.from(sessions.values());
56
+ const connectEvent = staleSessions.length > 0 ? "session_replaced" : "connected";
57
+ candidate = plan.resumeSessionAtUuid
58
+ ? await createSession({
59
+ cwd,
60
+ resume: command.session_id,
61
+ resumeSessionAt: plan.resumeSessionAtUuid,
62
+ resumeDropsTurn: plan.resumeDropsTurnId,
63
+ forkSession: true,
64
+ launchSettings: command.launch_settings,
65
+ connectEvent,
66
+ requestId,
67
+ deferConnect: true,
68
+ resumeUpdates: plan.resumeUpdates,
69
+ sessionsToCloseAfterConnect: staleSessions,
70
+ })
71
+ : await createSession({
72
+ cwd,
73
+ launchSettings: command.launch_settings,
74
+ connectEvent,
75
+ requestId,
76
+ deferConnect: true,
77
+ sessionsToCloseAfterConnect: staleSessions,
78
+ });
79
+ await awaitSessionInitialization(candidate);
80
+ commitDeferredSession(candidate);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ if (candidate) {
85
+ detachSessionForClose(candidate);
86
+ await closeSessionWithLogging(candidate, {
87
+ reason: "resume_at_candidate_rejected",
88
+ requestId,
89
+ });
90
+ }
91
+ const userMessage = message.startsWith("Resume rejected by --resume-drops-turn:")
92
+ ? "The session changed while you were selecting a message, so Claude Code refused to discard the newer turn. Reopen Resume and try again."
93
+ : `Failed to fork before selected message: ${message}`;
94
+ emitSessionResumeFailed(command.session_id, requestId, userMessage);
95
+ }
96
+ }
24
97
  async function initialize(command, requestId, sdkVersionError) {
25
98
  if (sdkVersionError) {
26
99
  bridgeLogger.error({
@@ -129,7 +202,9 @@ async function resume(command, requestId) {
129
202
  ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
130
203
  connectEvent: hadActiveSession ? "session_replaced" : "connected",
131
204
  requestId,
132
- ...(hadActiveSession ? { sessionsToCloseAfterConnect: staleSessions } : {}),
205
+ ...(hadActiveSession
206
+ ? { sessionsToCloseAfterConnect: staleSessions }
207
+ : {}),
133
208
  });
134
209
  }
135
210
  catch (error) {
@@ -2,6 +2,7 @@ const LIFECYCLE_COMMANDS = new Set([
2
2
  "initialize",
3
3
  "create_session",
4
4
  "resume_session",
5
+ "resume_session_at",
5
6
  "new_session",
6
7
  "rewind",
7
8
  "shutdown",
@@ -89,7 +90,9 @@ export class BridgeCommandScheduler {
89
90
  if (this.lifecycleTail) {
90
91
  dependencies.add(this.lifecycleTail);
91
92
  }
92
- const operation = dependencies.size === 0 ? this.start(task) : Promise.all(dependencies).then(task);
93
+ const operation = dependencies.size === 0
94
+ ? this.start(task)
95
+ : Promise.all(dependencies).then(task);
93
96
  const tail = settle(operation);
94
97
  this.lifecycleTail = tail;
95
98
  void tail.then(() => {
@@ -108,7 +111,9 @@ export class BridgeCommandScheduler {
108
111
  if (previousSessionTask) {
109
112
  dependencies.push(previousSessionTask);
110
113
  }
111
- const operation = dependencies.length === 0 ? this.start(task) : Promise.all(dependencies).then(task);
114
+ const operation = dependencies.length === 0
115
+ ? this.start(task)
116
+ : Promise.all(dependencies).then(task);
112
117
  const tail = settle(operation);
113
118
  this.sessionTails.set(sessionId, tail);
114
119
  void tail.then(() => {
@@ -10,7 +10,11 @@ export async function handleSessionControlCommand(command, requestId, deps) {
10
10
  handlePrompt(command, requestId, deps);
11
11
  return;
12
12
  case "cancel_turn":
13
- await dispatchCancelTurnCommand(command, { requestId, sessionById, slashError });
13
+ await dispatchCancelTurnCommand(command, {
14
+ requestId,
15
+ sessionById,
16
+ slashError,
17
+ });
14
18
  return;
15
19
  case "set_model":
16
20
  await setModel(command, requestId);
@@ -1,6 +1,7 @@
1
- import { getSessionMessages, renameSession, } from "@anthropic-ai/claude-agent-sdk";
1
+ import { getSessionMessages, listSessions, renameSession, } from "@anthropic-ai/claude-agent-sdk";
2
+ import { asRecordOrNull } from "./shared.js";
2
3
  import { mapSdkAccountInfo } from "./account_metadata.js";
3
- import { emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
4
+ import { currentSessionListOptions, emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
4
5
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
5
6
  import { refreshCurrentModel, sessionById, } from "./session_lifecycle.js";
6
7
  export async function handleSessionDataCommand(command, requestId, deps) {
@@ -17,6 +18,9 @@ export async function handleSessionDataCommand(command, requestId, deps) {
17
18
  case "get_context_usage":
18
19
  await getContextUsage(command, requestId);
19
20
  return;
21
+ case "get_usage":
22
+ await getUsage(command, requestId);
23
+ return;
20
24
  case "get_rewind_targets":
21
25
  await getRewindTargets(command, requestId, deps);
22
26
  return;
@@ -124,14 +128,18 @@ async function getContextUsage(command, requestId) {
124
128
  normalized_percentage: normalizedPercentage,
125
129
  total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
126
130
  max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
127
- raw_max_tokens: typeof usage.rawMaxTokens === "number" ? usage.rawMaxTokens : undefined,
131
+ raw_max_tokens: typeof usage.rawMaxTokens === "number"
132
+ ? usage.rawMaxTokens
133
+ : undefined,
128
134
  model: typeof usage.model === "string" ? usage.model : undefined,
129
135
  },
130
136
  });
131
137
  writeEvent({
132
138
  event: "context_usage",
133
139
  session_id: session.sessionId,
134
- ...(normalizedPercentage !== undefined ? { percentage: normalizedPercentage } : {}),
140
+ ...(normalizedPercentage !== undefined
141
+ ? { percentage: normalizedPercentage }
142
+ : {}),
135
143
  }, requestId);
136
144
  }
137
145
  catch (error) {
@@ -151,14 +159,216 @@ async function getContextUsage(command, requestId) {
151
159
  }, requestId);
152
160
  }
153
161
  }
154
- async function getRewindTargets(command, requestId, deps) {
162
+ function finiteNumber(record, key) {
163
+ const value = record?.[key];
164
+ return typeof value === "number" && Number.isFinite(value)
165
+ ? value
166
+ : undefined;
167
+ }
168
+ function structuredUsageWindow(value) {
169
+ const record = asRecordOrNull(value);
170
+ const utilization = finiteNumber(record, "utilization");
171
+ if (utilization === undefined) {
172
+ return undefined;
173
+ }
174
+ const resetsAt = typeof record?.resets_at === "string" ? record.resets_at.trim() : "";
175
+ return {
176
+ utilization: Math.max(0, Math.min(100, utilization)),
177
+ ...(resetsAt ? { resets_at: resetsAt } : {}),
178
+ };
179
+ }
180
+ function structuredModelUsageWindows(value) {
181
+ if (!Array.isArray(value)) {
182
+ return [];
183
+ }
184
+ return value.flatMap((entry) => {
185
+ const record = asRecordOrNull(entry);
186
+ const displayName = typeof record?.display_name === "string"
187
+ ? record.display_name.trim()
188
+ : "";
189
+ const window = structuredUsageWindow(record);
190
+ return displayName && window
191
+ ? [{ display_name: displayName, ...window }]
192
+ : [];
193
+ });
194
+ }
195
+ function structuredExtraUsage(value) {
196
+ const record = asRecordOrNull(value);
197
+ if (!record || record.is_enabled === false) {
198
+ return undefined;
199
+ }
200
+ const monthlyLimit = finiteNumber(record, "monthly_limit");
201
+ const usedCredits = finiteNumber(record, "used_credits");
202
+ const utilization = finiteNumber(record, "utilization");
203
+ const normalized = {
204
+ ...(monthlyLimit !== undefined ? { monthly_limit: monthlyLimit } : {}),
205
+ ...(usedCredits !== undefined ? { used_credits: usedCredits } : {}),
206
+ ...(utilization !== undefined
207
+ ? { utilization: Math.max(0, Math.min(100, utilization)) }
208
+ : {}),
209
+ ...(typeof record.currency === "string" && record.currency.trim()
210
+ ? { currency: record.currency.trim() }
211
+ : {}),
212
+ };
213
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
214
+ }
215
+ function structuredSessionUsage(value) {
216
+ const record = asRecordOrNull(value);
217
+ if (!record) {
218
+ return undefined;
219
+ }
220
+ const modelUsage = asRecordOrNull(record.model_usage);
221
+ const normalized = {
222
+ ...(finiteNumber(record, "total_cost_usd") !== undefined
223
+ ? { total_cost_usd: finiteNumber(record, "total_cost_usd") }
224
+ : {}),
225
+ ...(finiteNumber(record, "total_api_duration_ms") !== undefined
226
+ ? { total_api_duration_ms: finiteNumber(record, "total_api_duration_ms") }
227
+ : {}),
228
+ ...(finiteNumber(record, "total_duration_ms") !== undefined
229
+ ? { total_duration_ms: finiteNumber(record, "total_duration_ms") }
230
+ : {}),
231
+ ...(finiteNumber(record, "total_lines_added") !== undefined
232
+ ? { total_lines_added: finiteNumber(record, "total_lines_added") }
233
+ : {}),
234
+ ...(finiteNumber(record, "total_lines_removed") !== undefined
235
+ ? { total_lines_removed: finiteNumber(record, "total_lines_removed") }
236
+ : {}),
237
+ ...(modelUsage ? { model_count: Object.keys(modelUsage).length } : {}),
238
+ };
239
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
240
+ }
241
+ function structuredActivityWindow(value) {
242
+ const record = asRecordOrNull(value);
243
+ const requestCount = finiteNumber(record, "request_count");
244
+ const sessionCount = finiteNumber(record, "session_count");
245
+ if (requestCount === undefined ||
246
+ sessionCount === undefined ||
247
+ requestCount < 0 ||
248
+ sessionCount < 0) {
249
+ return undefined;
250
+ }
251
+ return {
252
+ request_count: Math.trunc(requestCount),
253
+ session_count: Math.trunc(sessionCount),
254
+ behaviors: structuredBehaviorAttributions(record?.behaviors),
255
+ agents: structuredNamedAttributions(record?.agents),
256
+ skills: structuredNamedAttributions(record?.skills),
257
+ plugins: structuredNamedAttributions(record?.plugins),
258
+ mcp_servers: structuredNamedAttributions(record?.mcp_servers),
259
+ };
260
+ }
261
+ function structuredBehaviorAttributions(value) {
262
+ if (!Array.isArray(value)) {
263
+ return [];
264
+ }
265
+ return value.flatMap((entry) => {
266
+ const record = asRecordOrNull(entry);
267
+ const key = typeof record?.key === "string" ? record.key.trim() : "";
268
+ const pct = finiteNumber(record, "pct");
269
+ const count = finiteNumber(record, "count");
270
+ if (!key || pct === undefined || count === undefined || count < 0) {
271
+ return [];
272
+ }
273
+ return [
274
+ { key, pct: Math.max(0, Math.min(100, pct)), count: Math.trunc(count) },
275
+ ];
276
+ });
277
+ }
278
+ function structuredNamedAttributions(value) {
279
+ if (!Array.isArray(value)) {
280
+ return [];
281
+ }
282
+ return value.flatMap((entry) => {
283
+ const record = asRecordOrNull(entry);
284
+ const name = typeof record?.name === "string" ? record.name.trim() : "";
285
+ const pct = finiteNumber(record, "pct");
286
+ return name && pct !== undefined
287
+ ? [{ name, pct: Math.max(0, Math.min(100, pct)) }]
288
+ : [];
289
+ });
290
+ }
291
+ export function normalizeStructuredUsage(value) {
292
+ const root = asRecordOrNull(value);
293
+ const limits = asRecordOrNull(root?.rate_limits);
294
+ const behaviors = asRecordOrNull(root?.behaviors);
295
+ const fiveHour = structuredUsageWindow(limits?.five_hour);
296
+ const sevenDay = structuredUsageWindow(limits?.seven_day);
297
+ const sevenDayOauthApps = structuredUsageWindow(limits?.seven_day_oauth_apps);
298
+ const sevenDayOpus = structuredUsageWindow(limits?.seven_day_opus);
299
+ const sevenDaySonnet = structuredUsageWindow(limits?.seven_day_sonnet);
300
+ const modelScoped = structuredModelUsageWindows(limits?.model_scoped);
301
+ const extraUsage = structuredExtraUsage(limits?.extra_usage);
302
+ const session = structuredSessionUsage(root?.session);
303
+ const activityDay = structuredActivityWindow(behaviors?.day);
304
+ const activityWeek = structuredActivityWindow(behaviors?.week);
305
+ return {
306
+ ...(typeof root?.subscription_type === "string" &&
307
+ root.subscription_type.trim()
308
+ ? { subscription_type: root.subscription_type.trim() }
309
+ : {}),
310
+ ...(typeof root?.rate_limits_available === "boolean"
311
+ ? { rate_limits_available: root.rate_limits_available }
312
+ : {}),
313
+ ...(fiveHour ? { five_hour: fiveHour } : {}),
314
+ ...(sevenDay ? { seven_day: sevenDay } : {}),
315
+ ...(sevenDayOauthApps ? { seven_day_oauth_apps: sevenDayOauthApps } : {}),
316
+ ...(sevenDayOpus ? { seven_day_opus: sevenDayOpus } : {}),
317
+ ...(sevenDaySonnet ? { seven_day_sonnet: sevenDaySonnet } : {}),
318
+ ...(modelScoped.length > 0 ? { model_scoped: modelScoped } : {}),
319
+ ...(extraUsage ? { extra_usage: extraUsage } : {}),
320
+ ...(session ? { session } : {}),
321
+ ...(activityDay ? { activity_day: activityDay } : {}),
322
+ ...(activityWeek ? { activity_week: activityWeek } : {}),
323
+ };
324
+ }
325
+ async function getUsage(command, requestId) {
155
326
  const session = requireSession(command.session_id, requestId);
156
327
  if (!session) {
157
328
  return;
158
329
  }
159
330
  try {
331
+ const usageMethod = session.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
332
+ if (typeof usageMethod !== "function") {
333
+ throw new Error("structured SDK usage is unavailable in this runtime");
334
+ }
335
+ const snapshot = normalizeStructuredUsage(await usageMethod.call(session.query));
336
+ if (Object.keys(snapshot).length === 0) {
337
+ throw new Error("structured SDK usage returned an incompatible empty payload");
338
+ }
339
+ writeEvent({ event: "usage_snapshot", session_id: session.sessionId, snapshot }, requestId);
340
+ }
341
+ catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ bridgeLogger.warn({
344
+ target: LOG_TARGETS.APP_SESSION,
345
+ eventName: "structured_usage_failed",
346
+ message: "experimental structured SDK usage failed",
347
+ outcome: "fallback_required",
348
+ ...(requestId ? { requestId } : {}),
349
+ sessionId: session.sessionId,
350
+ fields: { error_message: message },
351
+ });
352
+ writeEvent({
353
+ event: "usage_snapshot",
354
+ session_id: session.sessionId,
355
+ error: message,
356
+ }, requestId);
357
+ }
358
+ }
359
+ async function getRewindTargets(command, requestId, deps) {
360
+ const activeSession = sessionById(command.session_id);
361
+ try {
362
+ let cwd = activeSession?.cwd;
363
+ if (!cwd) {
364
+ const listedSession = (await listSessions(currentSessionListOptions())).find((entry) => entry.sessionId === command.session_id);
365
+ if (!listedSession) {
366
+ throw new Error(`unknown session: ${command.session_id}`);
367
+ }
368
+ cwd = listedSession.cwd?.trim() || currentSessionListOptions().dir;
369
+ }
160
370
  const historyMessages = await getSessionMessages(command.session_id, {
161
- dir: session.cwd,
371
+ ...(cwd ? { dir: cwd } : {}),
162
372
  includeSystemMessages: true,
163
373
  });
164
374
  const targets = deps.rewindTargetsFromSessionMessages(historyMessages);
@@ -168,7 +378,7 @@ async function getRewindTargets(command, requestId, deps) {
168
378
  message: "rewind targets loaded from session history",
169
379
  outcome: "success",
170
380
  ...(requestId ? { requestId } : {}),
171
- sessionId: session.sessionId,
381
+ sessionId: command.session_id,
172
382
  fields: {
173
383
  history_message_count: historyMessages.length,
174
384
  target_count: targets.length,
@@ -176,7 +386,7 @@ async function getRewindTargets(command, requestId, deps) {
176
386
  });
177
387
  writeEvent({
178
388
  event: "rewind_targets",
179
- session_id: session.sessionId,
389
+ session_id: command.session_id,
180
390
  targets,
181
391
  }, requestId);
182
392
  }
@@ -188,10 +398,15 @@ async function getRewindTargets(command, requestId, deps) {
188
398
  message: "failed to load rewind targets",
189
399
  outcome: "failure",
190
400
  ...(requestId ? { requestId } : {}),
191
- sessionId: session.sessionId,
401
+ sessionId: command.session_id,
192
402
  fields: { error_message: message },
193
403
  });
194
- slashError(command.session_id, `failed to load rewind targets: ${message}`, requestId);
404
+ writeEvent({
405
+ event: "rewind_targets",
406
+ session_id: command.session_id,
407
+ targets: [],
408
+ error: `failed to load rewind targets: ${message}`,
409
+ }, requestId);
195
410
  }
196
411
  }
197
412
  function requireSession(sessionId, requestId) {
@@ -10,11 +10,27 @@ const MODE_NAMES = {
10
10
  };
11
11
  const MODE_OPTIONS = [
12
12
  { id: "default", name: "Default", description: "Standard permission flow" },
13
- { id: "auto", name: "Auto", description: "Model-classified permission approvals" },
14
- { id: "acceptEdits", name: "Accept Edits", description: "Auto-approve edit operations" },
13
+ {
14
+ id: "auto",
15
+ name: "Auto",
16
+ description: "Model-classified permission approvals",
17
+ },
18
+ {
19
+ id: "acceptEdits",
20
+ name: "Accept Edits",
21
+ description: "Auto-approve edit operations",
22
+ },
15
23
  { id: "plan", name: "Plan", description: "No tool execution" },
16
- { id: "dontAsk", name: "Don't Ask", description: "Reject non-approved tools" },
17
- { id: "bypassPermissions", name: "Bypass Permissions", description: "Auto-approve all tools" },
24
+ {
25
+ id: "dontAsk",
26
+ name: "Don't Ask",
27
+ description: "Reject non-approved tools",
28
+ },
29
+ {
30
+ id: "bypassPermissions",
31
+ name: "Bypass Permissions",
32
+ description: "Auto-approve all tools",
33
+ },
18
34
  ];
19
35
  const BASE_SUPPORTED_MODE_IDS = [
20
36
  "default",
@@ -27,10 +43,10 @@ function currentModelSupportsAutoMode(session) {
27
43
  return currentModel.supports_auto_mode === true;
28
44
  }
29
45
  function modeInfoForId(mode) {
30
- return MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
46
+ return (MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
31
47
  id: mode,
32
48
  name: MODE_NAMES[mode],
33
- };
49
+ });
34
50
  }
35
51
  function uniqueModeIds(modeIds) {
36
52
  const unique = new Set(modeIds);
@@ -53,13 +69,17 @@ function computedSupportedModeIds(session) {
53
69
  }
54
70
  export function refreshSupportedModesForSession(session) {
55
71
  const computed = computedSupportedModeIds(session);
56
- session.supportedModeIds = computed.filter((mode) => mode === session.mode || !session.runtimeUnavailableModeIds.includes(mode));
72
+ session.supportedModeIds = computed.filter((mode) => mode === session.mode ||
73
+ !session.runtimeUnavailableModeIds.includes(mode));
57
74
  }
58
75
  export function markModeUnavailableForSession(session, mode) {
59
76
  if (session.runtimeUnavailableModeIds.includes(mode)) {
60
77
  return false;
61
78
  }
62
- session.runtimeUnavailableModeIds = [...session.runtimeUnavailableModeIds, mode];
79
+ session.runtimeUnavailableModeIds = [
80
+ ...session.runtimeUnavailableModeIds,
81
+ mode,
82
+ ];
63
83
  refreshSupportedModesForSession(session);
64
84
  return true;
65
85
  }
@@ -225,6 +245,13 @@ export function parseCommandEnvelope(line) {
225
245
  launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session"),
226
246
  metadata: optionalMetadata(raw, "metadata"),
227
247
  };
248
+ case "resume_session_at":
249
+ return {
250
+ command: "resume_session_at",
251
+ session_id: expectString(raw, "session_id", "resume_session_at"),
252
+ target_user_message_id: expectString(raw, "target_user_message_id", "resume_session_at"),
253
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session_at"),
254
+ };
228
255
  case "new_session":
229
256
  return {
230
257
  command: "new_session",
@@ -294,6 +321,11 @@ export function parseCommandEnvelope(line) {
294
321
  command: "get_context_usage",
295
322
  session_id: expectString(raw, "session_id", "get_context_usage"),
296
323
  };
324
+ case "get_usage":
325
+ return {
326
+ command: "get_usage",
327
+ session_id: expectString(raw, "session_id", "get_usage"),
328
+ };
297
329
  case "get_rewind_targets":
298
330
  return {
299
331
  command: "get_rewind_targets",
@@ -367,9 +399,12 @@ export function parseCommandEnvelope(line) {
367
399
  ? {
368
400
  outcome: "answered",
369
401
  selected_option_ids: expectStringArray(outcome, "selected_option_ids", "question_response.outcome"),
370
- ...(outcome.annotation === undefined || outcome.annotation === null
402
+ ...(outcome.annotation === undefined ||
403
+ outcome.annotation === null
371
404
  ? {}
372
- : { annotation: parseQuestionAnnotation(outcome.annotation) }),
405
+ : {
406
+ annotation: parseQuestionAnnotation(outcome.annotation),
407
+ }),
373
408
  }
374
409
  : { outcome: "cancelled" };
375
410
  return {
@@ -405,7 +440,9 @@ export function parseCommandEnvelope(line) {
405
440
  elicitation_request_id: expectString(raw, "elicitation_request_id", "elicitation_response"),
406
441
  action: expectElicitationAction(raw, "action", "elicitation_response"),
407
442
  ...(optionalJsonObject(raw, "content", "elicitation_response")
408
- ? { content: optionalJsonObject(raw, "content", "elicitation_response") }
443
+ ? {
444
+ content: optionalJsonObject(raw, "content", "elicitation_response"),
445
+ }
409
446
  : {}),
410
447
  };
411
448
  case "mcp_authenticate":