claude-code-rust 0.14.2 → 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.
@@ -54,21 +54,29 @@ export async function requestExitPlanModeApproval(session, toolUseId, inputData,
54
54
  return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
55
55
  }
56
56
  export function parseAskUserQuestionPrompts(inputData) {
57
- const rawQuestions = Array.isArray(inputData.questions) ? inputData.questions : [];
57
+ const rawQuestions = Array.isArray(inputData.questions)
58
+ ? inputData.questions
59
+ : [];
58
60
  const prompts = [];
59
61
  for (const rawQuestion of rawQuestions) {
60
62
  const questionRecord = asRecordOrNull(rawQuestion);
61
63
  if (!questionRecord) {
62
64
  continue;
63
65
  }
64
- const question = typeof questionRecord.question === "string" ? questionRecord.question.trim() : "";
66
+ const question = typeof questionRecord.question === "string"
67
+ ? questionRecord.question.trim()
68
+ : "";
65
69
  if (!question) {
66
70
  continue;
67
71
  }
68
- const headerRaw = typeof questionRecord.header === "string" ? questionRecord.header.trim() : "";
72
+ const headerRaw = typeof questionRecord.header === "string"
73
+ ? questionRecord.header.trim()
74
+ : "";
69
75
  const header = headerRaw || `Q${prompts.length + 1}`;
70
76
  const multiSelect = Boolean(questionRecord.multiSelect);
71
- const rawOptions = Array.isArray(questionRecord.options) ? questionRecord.options : [];
77
+ const rawOptions = Array.isArray(questionRecord.options)
78
+ ? questionRecord.options
79
+ : [];
72
80
  const options = [];
73
81
  for (const rawOption of rawOptions) {
74
82
  const optionRecord = asRecordOrNull(rawOption);
@@ -76,8 +84,12 @@ export function parseAskUserQuestionPrompts(inputData) {
76
84
  continue;
77
85
  }
78
86
  const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
79
- const description = typeof optionRecord.description === "string" ? optionRecord.description.trim() : "";
80
- const preview = typeof optionRecord.preview === "string" ? optionRecord.preview.trim() : "";
87
+ const description = typeof optionRecord.description === "string"
88
+ ? optionRecord.description.trim()
89
+ : "";
90
+ const preview = typeof optionRecord.preview === "string"
91
+ ? optionRecord.preview.trim()
92
+ : "";
81
93
  if (!label) {
82
94
  continue;
83
95
  }
@@ -140,7 +152,9 @@ function buildQuestionRequest(promptToolCall, prompt, index, total) {
140
152
  };
141
153
  }
142
154
  function askUserQuestionTranscript(answers) {
143
- return answers.map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`).join("\n");
155
+ return answers
156
+ .map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`)
157
+ .join("\n");
144
158
  }
145
159
  function askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) {
146
160
  return {
@@ -155,7 +169,9 @@ function askUserQuestionCompletedRawInput(prompts, answers, annotations, questio
155
169
  })),
156
170
  })),
157
171
  answers,
158
- ...(Object.keys(annotations).length > 0 ? { annotations: questionAnnotationsJson(annotations) } : {}),
172
+ ...(Object.keys(annotations).length > 0
173
+ ? { annotations: questionAnnotationsJson(annotations) }
174
+ : {}),
159
175
  question_results: questionResults,
160
176
  };
161
177
  }
@@ -175,7 +191,9 @@ function deriveAnnotation(selectedOptions, annotation) {
175
191
  .map((option) => option.preview?.trim() ?? "")
176
192
  .filter((previewText) => previewText.length > 0)
177
193
  .join("\n\n");
178
- const notes = annotation?.notes?.trim().length ? annotation.notes.trim() : undefined;
194
+ const notes = annotation?.notes?.trim().length
195
+ ? annotation.notes.trim()
196
+ : undefined;
179
197
  if (!preview && !notes) {
180
198
  return undefined;
181
199
  }
@@ -226,13 +244,21 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
226
244
  });
227
245
  if (outcome.outcome !== "answered") {
228
246
  setToolCallStatus(session, toolUseId, "failed", "Question cancelled");
229
- return { behavior: "deny", message: "Question cancelled", toolUseID: toolUseId };
247
+ return {
248
+ behavior: "deny",
249
+ message: "Question cancelled",
250
+ toolUseID: toolUseId,
251
+ };
230
252
  }
231
253
  const selectedOptions = request.prompt.options.filter((option) => outcome.selected_option_ids.includes(option.option_id));
232
254
  if (selectedOptions.length === 0 ||
233
255
  (!prompt.multiSelect && selectedOptions.length !== 1)) {
234
256
  setToolCallStatus(session, toolUseId, "failed", "Question answer was invalid");
235
- return { behavior: "deny", message: "Question answer was invalid", toolUseID: toolUseId };
257
+ return {
258
+ behavior: "deny",
259
+ message: "Question answer was invalid",
260
+ toolUseID: toolUseId,
261
+ };
236
262
  }
237
263
  const answer = selectedOptions.map((option) => option.label).join(", ");
238
264
  answers[prompt.question] = answer;
@@ -240,7 +266,11 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
240
266
  if (annotation) {
241
267
  annotations[prompt.question] = annotation;
242
268
  }
243
- transcript.push({ header: prompt.header, question: prompt.question, answer });
269
+ transcript.push({
270
+ header: prompt.header,
271
+ question: prompt.question,
272
+ answer,
273
+ });
244
274
  questionResults.push({
245
275
  question: prompt.question,
246
276
  header: prompt.header,
@@ -268,7 +298,9 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
268
298
  raw_output: summary,
269
299
  content: [{ type: "content", content: { type: "text", text: summary } }],
270
300
  ...(completed
271
- ? { raw_input: askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) }
301
+ ? {
302
+ raw_input: askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults),
303
+ }
272
304
  : {}),
273
305
  };
274
306
  emitToolCallUpdate(session, toolUseId, progressFields, "summary");
@@ -8,12 +8,12 @@ import { parseCommandEnvelope } from "./bridge/commands.js";
8
8
  import { parseFastModeDisabledReason, parseFastModeState, } from "./bridge/state_parsing.js";
9
9
  import { writeEvent, failConnection, slashError, emitRuntimeReloadCompleted, emitRuntimeReloadFailed, emitSessionUpdate, setSessionListingDir, } from "./bridge/events.js";
10
10
  import { contentFromPrompt } from "./bridge/message_handlers.js";
11
- import { sessions, sessionById, createSession, closeAllSessions, } from "./bridge/session_lifecycle.js";
11
+ import { sessions, sessionById, createSession, closeAllSessions, closeSessionWithLogging, detachSessionForClose, awaitSessionInitialization, commitDeferredSession, } from "./bridge/session_lifecycle.js";
12
12
  import { mapSessionMessagesToUpdates } from "./bridge/history.js";
13
- import { emitAvailableAgentsIfChanged, mapAvailableAgents } from "./bridge/agents.js";
14
- import { mapSdkSlashCommands, updateAvailableCommands } from "./bridge/available_commands.js";
13
+ import { emitAvailableAgentsIfChanged, mapAvailableAgents, } from "./bridge/agents.js";
14
+ import { mapSdkSlashCommands, updateAvailableCommands, } from "./bridge/available_commands.js";
15
15
  import { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, emitReconciledMcpSnapshotFromStatuses, staleMcpAuthCandidates, } from "./bridge/mcp.js";
16
- import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived } from "./bridge/logger.js";
16
+ import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived, } from "./bridge/logger.js";
17
17
  import { BridgeCommandScheduler } from "./bridge/command_scheduler.js";
18
18
  import { handleLifecycleCommand } from "./bridge/command_lifecycle.js";
19
19
  import { handleInteractionCommand } from "./bridge/command_interactions.js";
@@ -23,7 +23,7 @@ import { handleSessionDataCommand } from "./bridge/command_session_data.js";
23
23
  // Re-exports: all symbols that tests and external consumers import from bridge.js.
24
24
  export { AsyncQueue } from "./bridge/shared.js";
25
25
  export { asRecordOrNull } from "./bridge/shared.js";
26
- export { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./bridge/cache_policy.js";
26
+ export { CACHE_SPLIT_POLICY, previewKilobyteLabel, } from "./bridge/cache_policy.js";
27
27
  export { buildToolResultFields, createToolCall, isShellToolName, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
28
28
  export { looksLikeAuthRequired } from "./bridge/auth.js";
29
29
  export { parseCommandEnvelope } from "./bridge/commands.js";
@@ -31,7 +31,7 @@ export { buildSessionListOptions } from "./bridge/events.js";
31
31
  export { mapInitSlashCommands, mapSdkSlashCommands, updateAvailableCommands, } from "./bridge/available_commands.js";
32
32
  export { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./bridge/permissions.js";
33
33
  export { mapSessionMessagesToUpdates, mapSdkSessions, } from "./bridge/history.js";
34
- export { handleSdkMessage, handleTaskSystemMessage } from "./bridge/message_handlers.js";
34
+ export { handleSdkMessage, handleTaskSystemMessage, } from "./bridge/message_handlers.js";
35
35
  export { mapAvailableAgents } from "./bridge/agents.js";
36
36
  export { buildQueryOptions, resolveClaudeCodeSpawnCommand, } from "./bridge/session_lifecycle.js";
37
37
  export { mapAvailableModels } from "./bridge/model_metadata.js";
@@ -72,8 +72,12 @@ function userTextPartsFromSessionMessage(message) {
72
72
  textBlocks.push(blockRecord.text);
73
73
  }
74
74
  }
75
- const firstText = textBlocks.map(normalizeRewindTargetText).find((text) => text.length > 0);
76
- return firstText ? { firstText, inputText: textBlocks.join("\n").trim() } : undefined;
75
+ const firstText = textBlocks
76
+ .map(normalizeRewindTargetText)
77
+ .find((text) => text.length > 0);
78
+ return firstText
79
+ ? { firstText, inputText: textBlocks.join("\n").trim() }
80
+ : undefined;
77
81
  }
78
82
  export function rewindTargetsFromSessionMessages(messages) {
79
83
  const targets = [];
@@ -89,24 +93,36 @@ export function rewindTargetsFromSessionMessages(messages) {
89
93
  if (!uuid || !textParts) {
90
94
  return;
91
95
  }
96
+ const plan = buildRewindConversationPlan(messages, uuid);
97
+ if (!plan) {
98
+ return;
99
+ }
92
100
  targets.push({
93
101
  uuid,
94
102
  first_text: textParts.firstText,
95
103
  input_text: textParts.inputText,
96
104
  index,
97
- ...(previousAssistantUuid ? { previous_assistant_uuid: previousAssistantUuid } : {}),
105
+ ...(previousAssistantUuid
106
+ ? { previous_assistant_uuid: previousAssistantUuid }
107
+ : {}),
108
+ ...(plan.resumeSessionAtUuid
109
+ ? { resume_anchor_uuid: plan.resumeSessionAtUuid }
110
+ : {}),
98
111
  });
99
112
  });
100
113
  return targets.reverse();
101
114
  }
102
115
  export function canGenerateSessionTitle(query) {
103
- return typeof query.generateSessionTitle === "function";
116
+ return (typeof query
117
+ .generateSessionTitle === "function");
104
118
  }
105
119
  export async function generatePersistedSessionTitle(query, description) {
106
120
  if (!canGenerateSessionTitle(query)) {
107
121
  throw new Error("SDK query does not support generateSessionTitle");
108
122
  }
109
- const title = await query.generateSessionTitle(description, { persist: true });
123
+ const title = await query.generateSessionTitle(description, {
124
+ persist: true,
125
+ });
110
126
  if (typeof title !== "string" || title.trim().length === 0) {
111
127
  throw new Error("SDK did not return a generated session title");
112
128
  }
@@ -178,7 +194,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
178
194
  value: agent,
179
195
  });
180
196
  }
181
- const EXPECTED_AGENT_SDK_VERSION = "0.3.220";
197
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.227";
182
198
  const require = createRequire(import.meta.url);
183
199
  export function resolveInstalledAgentSdkVersion() {
184
200
  try {
@@ -226,8 +242,18 @@ export async function handleReloadPluginsCommand(session, requestId) {
226
242
  }
227
243
  }
228
244
  function resolveRewindTarget(messages, targetUserMessageId) {
245
+ const seenUuids = new Set();
246
+ for (const message of messages) {
247
+ const uuid = message.uuid;
248
+ if (typeof uuid !== "string" ||
249
+ uuid.trim().length === 0 ||
250
+ seenUuids.has(uuid.trim())) {
251
+ return null;
252
+ }
253
+ seenUuids.add(uuid.trim());
254
+ }
229
255
  let previousAssistantUuid;
230
- let textUserCountBeforeTarget = 0;
256
+ let previousTextUserIndex;
231
257
  for (let index = 0; index < messages.length; index += 1) {
232
258
  const message = messages[index];
233
259
  const record = message;
@@ -241,22 +267,22 @@ function resolveRewindTarget(messages, targetUserMessageId) {
241
267
  continue;
242
268
  }
243
269
  if (uuid === targetUserMessageId) {
244
- if (!previousAssistantUuid && textUserCountBeforeTarget > 0) {
245
- return null;
246
- }
247
- if (previousAssistantUuid) {
248
- const anchorIndex = messages.findIndex((candidate) => {
249
- const candidateRecord = candidate;
250
- return candidateRecord.uuid === previousAssistantUuid;
251
- });
252
- if (anchorIndex < 0) {
270
+ const isLatestTextUser = !messages
271
+ .slice(index + 1)
272
+ .some((candidate) => userTextPartsFromSessionMessage(candidate) !== undefined);
273
+ if (previousTextUserIndex !== undefined) {
274
+ const anchorRecord = messages[index - 1];
275
+ const resumeSessionAtUuid = typeof anchorRecord.uuid === "string" ? anchorRecord.uuid.trim() : "";
276
+ if (!resumeSessionAtUuid || index - 1 < previousTextUserIndex) {
253
277
  return null;
254
278
  }
255
279
  return {
256
280
  inputText: textParts.inputText,
257
281
  previousAssistantUuid,
282
+ resumeSessionAtUuid,
283
+ ...(isLatestTextUser ? { resumeDropsTurnId: uuid } : {}),
258
284
  targetIndex: index,
259
- retainedMessages: messages.slice(0, anchorIndex + 1),
285
+ retainedMessages: messages.slice(0, index),
260
286
  };
261
287
  }
262
288
  return {
@@ -266,7 +292,7 @@ function resolveRewindTarget(messages, targetUserMessageId) {
266
292
  };
267
293
  }
268
294
  if (uuid) {
269
- textUserCountBeforeTarget += 1;
295
+ previousTextUserIndex = index;
270
296
  }
271
297
  }
272
298
  return null;
@@ -291,7 +317,9 @@ export function mapRewindFilesResult(result) {
291
317
  can_rewind: result.canRewind,
292
318
  ...(result.error ? { error: result.error } : {}),
293
319
  files_changed: result.filesChanged ?? [],
294
- ...(result.insertions !== undefined ? { insertions: result.insertions } : {}),
320
+ ...(result.insertions !== undefined
321
+ ? { insertions: result.insertions }
322
+ : {}),
295
323
  ...(result.deletions !== undefined ? { deletions: result.deletions } : {}),
296
324
  ...(skippedLinks !== undefined ? { skipped_links: skippedLinks } : {}),
297
325
  };
@@ -329,7 +357,7 @@ function requestIdFromCommandLine(line) {
329
357
  return undefined;
330
358
  }
331
359
  }
332
- async function replaceConversationForRewind(command, session, targetUserMessageId, requestId, pendingRewindResult) {
360
+ async function prepareConversationRewindCandidate(command, session, targetUserMessageId, requestId) {
333
361
  const historyMessages = await getSessionMessages(command.session_id, {
334
362
  dir: session.cwd,
335
363
  includeSystemMessages: true,
@@ -371,74 +399,83 @@ async function replaceConversationForRewind(command, session, targetUserMessageI
371
399
  stale_session_count: staleSessions.length,
372
400
  },
373
401
  });
374
- if (!resolved.previousAssistantUuid) {
375
- bridgeLogger.info({
376
- target: LOG_TARGETS.APP_SESSION,
377
- eventName: "rewind_first_message_branch",
378
- message: "conversation rewind target is first user message; creating fresh replacement",
379
- outcome: "start",
380
- ...(requestId ? { requestId } : {}),
381
- sessionId: session.sessionId,
382
- fields: {
383
- target_user_message_id: targetUserMessageId,
384
- history_message_count: historyMessages.length,
385
- stale_session_count: staleSessions.length,
386
- },
387
- });
388
- await createSession({
389
- cwd: session.cwd,
390
- launchSettings: command.launch_settings,
391
- connectEvent: "session_replaced",
392
- requestId,
393
- restoredInput: resolved.inputText,
394
- ...(pendingRewindResult ? { pendingRewindResult } : {}),
395
- ...(staleSessions.length > 0 ? { sessionsToCloseAfterConnect: staleSessions } : {}),
396
- });
402
+ let candidate;
403
+ try {
404
+ if (!resolved.resumeSessionAtUuid) {
405
+ bridgeLogger.info({
406
+ target: LOG_TARGETS.APP_SESSION,
407
+ eventName: "rewind_first_message_branch",
408
+ message: "conversation rewind target is first user message; creating fresh replacement",
409
+ outcome: "start",
410
+ ...(requestId ? { requestId } : {}),
411
+ sessionId: session.sessionId,
412
+ fields: {
413
+ target_user_message_id: targetUserMessageId,
414
+ history_message_count: historyMessages.length,
415
+ stale_session_count: staleSessions.length,
416
+ },
417
+ });
418
+ candidate = await createSession({
419
+ cwd: session.cwd,
420
+ launchSettings: command.launch_settings,
421
+ connectEvent: "session_replaced",
422
+ requestId,
423
+ deferConnect: true,
424
+ restoredInput: resolved.inputText,
425
+ ...(staleSessions.length > 0
426
+ ? { sessionsToCloseAfterConnect: staleSessions }
427
+ : {}),
428
+ });
429
+ }
430
+ else {
431
+ candidate = await createSession({
432
+ cwd: session.cwd,
433
+ resume: command.session_id,
434
+ resumeSessionAt: resolved.resumeSessionAtUuid,
435
+ resumeDropsTurn: resolved.resumeDropsTurnId,
436
+ forkSession: true,
437
+ launchSettings: command.launch_settings,
438
+ connectEvent: "session_replaced",
439
+ requestId,
440
+ deferConnect: true,
441
+ ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
442
+ restoredInput: resolved.inputText,
443
+ ...(staleSessions.length > 0
444
+ ? { sessionsToCloseAfterConnect: staleSessions }
445
+ : {}),
446
+ });
447
+ }
448
+ await awaitSessionInitialization(candidate);
397
449
  bridgeLogger.info({
398
450
  target: LOG_TARGETS.APP_SESSION,
399
- eventName: "rewind_replacement_created",
400
- message: "conversation rewind replacement session created",
451
+ eventName: "rewind_candidate_validated",
452
+ message: "conversation rewind candidate passed initialization and truncation validation",
401
453
  outcome: "success",
402
454
  ...(requestId ? { requestId } : {}),
455
+ sessionId: candidate.sessionId,
403
456
  fields: {
404
457
  target_user_message_id: targetUserMessageId,
405
- resume_session_at: "<none>",
406
- retained_message_count: 0,
407
- retained_update_count: 0,
458
+ resume_session_at: resolved.resumeSessionAtUuid ?? "<none>",
459
+ resume_drops_turn: resolved.resumeSessionAtUuid
460
+ ? (resolved.resumeDropsTurnId ?? "<none>")
461
+ : "<none>",
462
+ retained_message_count: resolved.retainedMessages.length,
463
+ retained_update_count: resumeUpdates.length,
408
464
  stale_session_count: staleSessions.length,
409
465
  },
410
466
  });
411
- return;
467
+ return candidate;
468
+ }
469
+ catch (error) {
470
+ if (candidate) {
471
+ detachSessionForClose(candidate);
472
+ await closeSessionWithLogging(candidate, {
473
+ reason: "rewind_candidate_rejected",
474
+ requestId,
475
+ });
476
+ }
477
+ throw error;
412
478
  }
413
- const sessionsToCloseAfterConnect = staleSessions.filter((stale) => stale !== session);
414
- await createSession({
415
- cwd: session.cwd,
416
- resume: command.session_id,
417
- resumeSessionAt: resolved.previousAssistantUuid,
418
- launchSettings: command.launch_settings,
419
- connectEvent: "session_replaced",
420
- requestId,
421
- sessionsToCloseBeforeRegister: [session],
422
- ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
423
- restoredInput: resolved.inputText,
424
- ...(pendingRewindResult ? { pendingRewindResult } : {}),
425
- ...(sessionsToCloseAfterConnect.length > 0 ? { sessionsToCloseAfterConnect } : {}),
426
- });
427
- bridgeLogger.info({
428
- target: LOG_TARGETS.APP_SESSION,
429
- eventName: "rewind_replacement_created",
430
- message: "conversation rewind replacement session created",
431
- outcome: "success",
432
- ...(requestId ? { requestId } : {}),
433
- sessionId: command.session_id,
434
- fields: {
435
- target_user_message_id: targetUserMessageId,
436
- resume_session_at: resolved.previousAssistantUuid,
437
- retained_message_count: resolved.retainedMessages.length,
438
- retained_update_count: resumeUpdates.length,
439
- stale_session_count: staleSessions.length,
440
- },
441
- });
442
479
  }
443
480
  async function handleRewind(command, requestId) {
444
481
  const session = sessionById(command.session_id);
@@ -465,7 +502,8 @@ async function handleRewind(command, requestId) {
465
502
  });
466
503
  if (command.restore_mode === "conversation") {
467
504
  try {
468
- await replaceConversationForRewind(command, session, targetUserMessageId, requestId);
505
+ const candidate = await prepareConversationRewindCandidate(command, session, targetUserMessageId, requestId);
506
+ commitDeferredSession(candidate);
469
507
  }
470
508
  catch (error) {
471
509
  const message = error instanceof Error ? error.message : String(error);
@@ -486,18 +524,13 @@ async function handleRewind(command, requestId) {
486
524
  }
487
525
  return;
488
526
  }
489
- let appliedFileResult;
527
+ let dryRunResult;
490
528
  try {
491
- const dryRunResult = await rewindFiles(session, targetUserMessageId, true);
529
+ dryRunResult = await rewindFiles(session, targetUserMessageId, true);
492
530
  if (!dryRunResult.can_rewind) {
493
531
  slashError(command.session_id, dryRunResult.error ?? "failed to dry-run file rewind", requestId);
494
532
  return;
495
533
  }
496
- appliedFileResult = await rewindFiles(session, targetUserMessageId, false);
497
- if (!appliedFileResult.can_rewind) {
498
- slashError(command.session_id, appliedFileResult.error ?? "failed to restore code", requestId);
499
- return;
500
- }
501
534
  }
502
535
  catch (error) {
503
536
  const message = error instanceof Error ? error.message : String(error);
@@ -518,24 +551,61 @@ async function handleRewind(command, requestId) {
518
551
  return;
519
552
  }
520
553
  if (command.restore_mode === "code") {
521
- emitRewindResult(session.sessionId, command.restore_mode, "success", requestId, appliedFileResult);
554
+ try {
555
+ const appliedFileResult = await rewindFiles(session, targetUserMessageId, false);
556
+ if (!appliedFileResult.can_rewind) {
557
+ slashError(command.session_id, appliedFileResult.error ?? "failed to restore code", requestId);
558
+ return;
559
+ }
560
+ emitRewindResult(session.sessionId, command.restore_mode, "success", requestId, appliedFileResult);
561
+ }
562
+ catch (error) {
563
+ const message = error instanceof Error ? error.message : String(error);
564
+ slashError(command.session_id, `failed to restore code: ${message}`, requestId);
565
+ }
522
566
  return;
523
567
  }
568
+ let candidate;
524
569
  try {
525
- await replaceConversationForRewind(command, session, targetUserMessageId, requestId, {
526
- event: "rewind_result",
527
- restore_mode: command.restore_mode,
528
- status: "success",
529
- file_result: appliedFileResult,
570
+ candidate = await prepareConversationRewindCandidate(command, session, targetUserMessageId, requestId);
571
+ }
572
+ catch (error) {
573
+ const message = error instanceof Error ? error.message : String(error);
574
+ bridgeLogger.error({
575
+ target: LOG_TARGETS.APP_SESSION,
576
+ eventName: "rewind_failed",
577
+ message: "conversation candidate failed before file restore",
578
+ outcome: "failure",
579
+ ...(requestId ? { requestId } : {}),
580
+ sessionId: session.sessionId,
581
+ fields: {
582
+ target_user_message_id: targetUserMessageId,
583
+ restore_mode: command.restore_mode,
584
+ error_message: message,
585
+ },
530
586
  });
587
+ slashError(command.session_id, `failed to validate conversation rewind: ${message}`, requestId);
588
+ return;
589
+ }
590
+ let appliedFileResult;
591
+ try {
592
+ appliedFileResult = await rewindFiles(session, targetUserMessageId, false);
593
+ if (!appliedFileResult.can_rewind) {
594
+ throw new Error(appliedFileResult.error ?? "failed to restore code");
595
+ }
531
596
  }
532
597
  catch (error) {
598
+ detachSessionForClose(candidate);
599
+ await closeSessionWithLogging(candidate, {
600
+ reason: "rewind_file_apply_failed",
601
+ requestId,
602
+ });
533
603
  const message = error instanceof Error ? error.message : String(error);
534
604
  bridgeLogger.error({
535
605
  target: LOG_TARGETS.APP_SESSION,
536
606
  eventName: "rewind_failed",
537
- message: "conversation rewind failed after file restore",
538
- outcome: "partial_failure",
607
+ message: "file rewind failed after conversation candidate validation",
608
+ outcome: "failure",
539
609
  ...(requestId ? { requestId } : {}),
540
610
  sessionId: session.sessionId,
541
611
  fields: {
@@ -544,13 +614,34 @@ async function handleRewind(command, requestId) {
544
614
  error_message: message,
545
615
  },
546
616
  });
547
- emitRewindResult(session.sessionId, command.restore_mode, "partial_failure", requestId, appliedFileResult, `Code was restored, but the conversation could not be rewound: ${message}`);
617
+ slashError(command.session_id, `failed to restore code: ${message}`, requestId);
618
+ return;
619
+ }
620
+ candidate.pendingRewindResult = {
621
+ event: "rewind_result",
622
+ restore_mode: command.restore_mode,
623
+ status: "success",
624
+ file_result: appliedFileResult,
625
+ };
626
+ try {
627
+ commitDeferredSession(candidate);
628
+ }
629
+ catch (error) {
630
+ detachSessionForClose(candidate);
631
+ await closeSessionWithLogging(candidate, {
632
+ reason: "rewind_candidate_failed_after_file_apply",
633
+ requestId,
634
+ });
635
+ const message = error instanceof Error ? error.message : String(error);
636
+ emitRewindResult(session.sessionId, command.restore_mode, "partial_failure", requestId, appliedFileResult, `Code was restored, but the conversation candidate could not be committed: ${message}`);
548
637
  }
549
638
  }
550
639
  async function handleCommand(command, requestId) {
551
640
  logBridgeCommandReceived(command, requestId);
552
641
  const sdkVersionError = agentSdkVersionCompatibilityError();
553
- if (sdkVersionError && command.command !== "initialize" && command.command !== "shutdown") {
642
+ if (sdkVersionError &&
643
+ command.command !== "initialize" &&
644
+ command.command !== "shutdown") {
554
645
  bridgeLogger.error({
555
646
  target: LOG_TARGETS.BRIDGE_LIFECYCLE,
556
647
  eventName: "bridge_command_rejected",
@@ -569,9 +660,12 @@ async function handleCommand(command, requestId) {
569
660
  case "initialize":
570
661
  case "create_session":
571
662
  case "resume_session":
663
+ case "resume_session_at":
572
664
  case "new_session":
573
665
  case "shutdown":
574
- await handleLifecycleCommand(command, requestId, sdkVersionError);
666
+ await handleLifecycleCommand(command, requestId, sdkVersionError, {
667
+ buildRewindConversationPlan,
668
+ });
575
669
  return;
576
670
  case "prompt":
577
671
  case "cancel_turn":
@@ -595,6 +689,7 @@ async function handleCommand(command, requestId) {
595
689
  case "rename_session":
596
690
  case "get_status_snapshot":
597
691
  case "get_context_usage":
692
+ case "get_usage":
598
693
  case "get_rewind_targets":
599
694
  case "rewind":
600
695
  await handleSessionDataCommand(command, requestId, {
@@ -720,6 +815,7 @@ function main() {
720
815
  });
721
816
  });
722
817
  }
723
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
818
+ if (process.argv[1] &&
819
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
724
820
  main();
725
821
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,15 +33,15 @@
33
33
  "LICENSE"
34
34
  ],
35
35
  "dependencies": {
36
- "@anthropic-ai/claude-agent-sdk": "0.3.220"
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.227"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@srothgan/claude-code-rust-darwin-arm64": "0.14.2",
40
- "@srothgan/claude-code-rust-darwin-x64": "0.14.2",
41
- "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.2",
42
- "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.2",
43
- "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.2",
44
- "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.2"
39
+ "@srothgan/claude-code-rust-darwin-arm64": "0.14.4",
40
+ "@srothgan/claude-code-rust-darwin-x64": "0.14.4",
41
+ "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.4",
42
+ "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.4",
43
+ "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.4",
44
+ "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.4"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24"