claude-code-rust 0.12.2 → 0.12.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.
@@ -23,6 +23,8 @@ const WORKFLOW_TOOL_NAME = "Workflow";
23
23
  const PROJECTS_TOOL_NAME = "Projects";
24
24
  const ARTIFACT_TOOL_NAME = "Artifact";
25
25
  const SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME = "ShowOnboardingRolePicker";
26
+ const READ_MCP_RESOURCE_TOOL_NAME = "ReadMcpResource";
27
+ const READ_MCP_RESOURCE_DIR_TOOL_NAME = "ReadMcpResourceDir";
26
28
  const SEARCH_OUTPUT_MODES = new Set(["content", "files_with_matches", "count"]);
27
29
  function isCronToolName(name) {
28
30
  return CRON_TOOL_NAMES.has(name);
@@ -50,6 +52,12 @@ function inputBoolean(input, key) {
50
52
  function isAgentLikeToolName(name) {
51
53
  return name === "Agent" || name === "Task";
52
54
  }
55
+ export function isShellToolName(name) {
56
+ return name === "Bash" || name === "PowerShell";
57
+ }
58
+ function isMcpResourceReadToolName(name) {
59
+ return name === READ_MCP_RESOURCE_TOOL_NAME || name === READ_MCP_RESOURCE_DIR_TOOL_NAME;
60
+ }
53
61
  function agentInputTitle(name, input) {
54
62
  if (!isAgentLikeToolName(name)) {
55
63
  return undefined;
@@ -135,11 +143,13 @@ function formatGrepTitle(input) {
135
143
  return flags.length > 0 ? `${scoped} (${flags.join(", ")})` : scoped;
136
144
  }
137
145
  export function normalizeToolKind(name) {
146
+ if (isShellToolName(name)) {
147
+ return "execute";
148
+ }
138
149
  switch (name) {
139
- case "Bash":
140
- return "execute";
141
150
  case "Read":
142
- case "ReadMcpResource":
151
+ case READ_MCP_RESOURCE_TOOL_NAME:
152
+ case READ_MCP_RESOURCE_DIR_TOOL_NAME:
143
153
  return "read";
144
154
  case "Write":
145
155
  case "Edit":
@@ -189,7 +199,7 @@ export function toolTitle(name, input, context = {}) {
189
199
  if (agentTitle) {
190
200
  return agentTitle;
191
201
  }
192
- if (name === "Bash") {
202
+ if (isShellToolName(name)) {
193
203
  const command = typeof input.command === "string" ? input.command : "";
194
204
  return command || "Terminal";
195
205
  }
@@ -264,14 +274,14 @@ export function toolTitle(name, input, context = {}) {
264
274
  if ((name === "Read" || name === "Write" || name === "Edit") && typeof input.file_path === "string") {
265
275
  return `${name} ${input.file_path}`;
266
276
  }
267
- if (name === "ReadMcpResource") {
277
+ if (isMcpResourceReadToolName(name)) {
268
278
  const uri = typeof input.uri === "string" ? input.uri : "";
269
279
  const server = typeof input.server === "string" ? input.server : "";
270
280
  if (server && uri) {
271
- return `ReadMcpResource ${server} ${uri}`;
281
+ return `${name} ${server} ${uri}`;
272
282
  }
273
283
  if (uri) {
274
- return `ReadMcpResource ${uri}`;
284
+ return `${name} ${uri}`;
275
285
  }
276
286
  }
277
287
  return name;
@@ -436,6 +446,37 @@ function mcpResourceContentFromResult(rawResult, rawContent) {
436
446
  }
437
447
  return [];
438
448
  }
449
+ function mcpResourceDirTextFromResult(toolName, rawResult, rawContent) {
450
+ if (toolName !== READ_MCP_RESOURCE_DIR_TOOL_NAME) {
451
+ return undefined;
452
+ }
453
+ for (const candidate of collectResultCandidates(rawResult, rawContent)) {
454
+ if (!Array.isArray(candidate.resources)) {
455
+ continue;
456
+ }
457
+ const lines = [];
458
+ for (const entry of candidate.resources) {
459
+ const record = asRecordOrNull(entry);
460
+ if (!record) {
461
+ continue;
462
+ }
463
+ const name = nonEmptyString(record.name);
464
+ const uri = nonEmptyString(record.uri);
465
+ if (!name || !uri) {
466
+ continue;
467
+ }
468
+ const mimeType = nonEmptyString(record.mimeType);
469
+ const suffix = mimeType
470
+ ? mimeType === "inode/directory"
471
+ ? " (directory)"
472
+ : ` (${mimeType})`
473
+ : "";
474
+ lines.push(`${name} - ${uri}${suffix}`);
475
+ }
476
+ return lines.length > 0 ? lines.join("\n") : "No resources found.";
477
+ }
478
+ return undefined;
479
+ }
439
480
  function extractToolOutputMetadata(toolName, rawResult, rawContent) {
440
481
  const candidates = collectResultCandidates(rawResult, rawContent);
441
482
  const metadata = {};
@@ -684,14 +725,14 @@ function editDiffFromResult(rawResult, rawInput) {
684
725
  }
685
726
  return editDiffFromInput(rawInput);
686
727
  }
687
- function findBashResultRecord(rawResult, rawContent) {
728
+ function findShellResultRecord(rawResult, rawContent) {
688
729
  return resultRecordCandidates(rawResult, rawContent).find((candidate) => "stdout" in candidate ||
689
730
  "stderr" in candidate ||
690
731
  "backgroundTaskId" in candidate ||
691
732
  "backgroundedByUser" in candidate ||
692
733
  "assistantAutoBackgrounded" in candidate);
693
734
  }
694
- function bashBackgroundMessage(record) {
735
+ function shellBackgroundMessage(record) {
695
736
  const backgroundTaskId = typeof record.backgroundTaskId === "string" ? record.backgroundTaskId : "";
696
737
  if (!backgroundTaskId) {
697
738
  return "";
@@ -704,7 +745,7 @@ function bashBackgroundMessage(record) {
704
745
  }
705
746
  return `Command is running in background with ID: ${backgroundTaskId}.`;
706
747
  }
707
- function buildBashDisplayOutput(record) {
748
+ function buildShellDisplayOutput(record) {
708
749
  const segments = [];
709
750
  const stdout = typeof record.stdout === "string" ? record.stdout : "";
710
751
  const stderr = typeof record.stderr === "string" ? record.stderr : "";
@@ -717,7 +758,7 @@ function buildBashDisplayOutput(record) {
717
758
  if (record.interrupted === true) {
718
759
  segments.push("Command was aborted before completion.");
719
760
  }
720
- const backgroundMessage = bashBackgroundMessage(record);
761
+ const backgroundMessage = shellBackgroundMessage(record);
721
762
  if (backgroundMessage) {
722
763
  segments.push(backgroundMessage);
723
764
  }
@@ -1600,8 +1641,8 @@ function enterPlanModeStructuredOutputHandled(toolName, rawResult, rawContent) {
1600
1641
  }
1601
1642
  return false;
1602
1643
  }
1603
- function readMcpResourceErrorText(toolName, rawResult, rawContent) {
1604
- if (toolName !== "ReadMcpResource") {
1644
+ function mcpResourceReadErrorText(toolName, rawResult, rawContent) {
1645
+ if (!isMcpResourceReadToolName(toolName)) {
1605
1646
  return undefined;
1606
1647
  }
1607
1648
  for (const candidate of collectResultCandidates(rawResult, rawContent)) {
@@ -1671,13 +1712,23 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
1671
1712
  if (agentTitle) {
1672
1713
  fields.title = agentTitle;
1673
1714
  }
1674
- const readMcpResourceError = readMcpResourceErrorText(toolName, rawResult, rawContent);
1715
+ const readMcpResourceError = mcpResourceReadErrorText(toolName, rawResult, rawContent);
1675
1716
  if (readMcpResourceError) {
1676
1717
  fields.status = "failed";
1677
1718
  fields.raw_output = readMcpResourceError;
1678
1719
  fields.content = [{ type: "content", content: { type: "text", text: readMcpResourceError } }];
1679
1720
  return fields;
1680
1721
  }
1722
+ const readMcpResourceDirOutput = !isError
1723
+ ? mcpResourceDirTextFromResult(toolName, rawResult, rawContent)
1724
+ : undefined;
1725
+ if (readMcpResourceDirOutput !== undefined) {
1726
+ fields.raw_output = readMcpResourceDirOutput;
1727
+ fields.content = [
1728
+ { type: "content", content: { type: "text", text: readMcpResourceDirOutput } },
1729
+ ];
1730
+ return fields;
1731
+ }
1681
1732
  const searchOutput = !isError ? searchResultText(toolName, rawResult, rawContent) : undefined;
1682
1733
  if (searchOutput !== undefined) {
1683
1734
  fields.raw_output = searchOutput;
@@ -1777,10 +1828,12 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
1777
1828
  }
1778
1829
  return fields;
1779
1830
  }
1780
- const bashResultRecord = toolName === "Bash" ? findBashResultRecord(rawResult, rawContent) : undefined;
1831
+ const shellResultRecord = isShellToolName(toolName)
1832
+ ? findShellResultRecord(rawResult, rawContent)
1833
+ : undefined;
1781
1834
  const normalizedRawOutput = normalizeToolResultText(rawContent, isError);
1782
- const rawOutput = bashResultRecord
1783
- ? buildBashDisplayOutput(bashResultRecord)
1835
+ const rawOutput = shellResultRecord
1836
+ ? buildShellDisplayOutput(shellResultRecord)
1784
1837
  : normalizedRawOutput || JSON.stringify(rawContent);
1785
1838
  if (rawOutput && !(isTaskToolName(toolName) && !isError)) {
1786
1839
  fields.raw_output = rawOutput;
@@ -1820,7 +1873,7 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
1820
1873
  return fields;
1821
1874
  }
1822
1875
  }
1823
- if (!isError && toolName === "ReadMcpResource") {
1876
+ if (!isError && toolName === READ_MCP_RESOURCE_TOOL_NAME) {
1824
1877
  const structuredResourceContent = mcpResourceContentFromResult(rawResult, rawContent);
1825
1878
  if (structuredResourceContent.length > 0) {
1826
1879
  fields.content = structuredResourceContent;
@@ -18,7 +18,7 @@ import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived } from "./bridge/lo
18
18
  export { AsyncQueue } from "./bridge/shared.js";
19
19
  export { asRecordOrNull } from "./bridge/shared.js";
20
20
  export { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./bridge/cache_policy.js";
21
- export { buildToolResultFields, createToolCall, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
21
+ export { buildToolResultFields, createToolCall, isShellToolName, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
22
22
  export { looksLikeAuthRequired } from "./bridge/auth.js";
23
23
  export { parseCommandEnvelope } from "./bridge/commands.js";
24
24
  export { buildSessionListOptions } from "./bridge/events.js";
@@ -36,6 +36,63 @@ export { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, staleMcpAuthCandidates };
36
36
  export function buildSessionMutationOptions(cwd) {
37
37
  return cwd ? { dir: cwd } : undefined;
38
38
  }
39
+ function normalizeRewindTargetText(text) {
40
+ return text.replace(/\s+/g, " ").trim();
41
+ }
42
+ function userTextPartsFromSessionMessage(message) {
43
+ const record = message;
44
+ if (record.type !== "user") {
45
+ return undefined;
46
+ }
47
+ const payload = record.message;
48
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
49
+ return undefined;
50
+ }
51
+ const content = payload.content;
52
+ if (typeof content === "string") {
53
+ const firstText = normalizeRewindTargetText(content);
54
+ return firstText ? { firstText, inputText: content } : undefined;
55
+ }
56
+ if (!Array.isArray(content)) {
57
+ return undefined;
58
+ }
59
+ const textBlocks = [];
60
+ for (const block of content) {
61
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
62
+ continue;
63
+ }
64
+ const blockRecord = block;
65
+ if (blockRecord.type === "text" && typeof blockRecord.text === "string") {
66
+ textBlocks.push(blockRecord.text);
67
+ }
68
+ }
69
+ const firstText = textBlocks.map(normalizeRewindTargetText).find((text) => text.length > 0);
70
+ return firstText ? { firstText, inputText: textBlocks.join("\n").trim() } : undefined;
71
+ }
72
+ export function rewindTargetsFromSessionMessages(messages) {
73
+ const targets = [];
74
+ let previousAssistantUuid;
75
+ messages.forEach((message, index) => {
76
+ const record = message;
77
+ const uuid = typeof record.uuid === "string" ? record.uuid.trim() : "";
78
+ if (record.type === "assistant" && uuid) {
79
+ previousAssistantUuid = uuid;
80
+ return;
81
+ }
82
+ const textParts = userTextPartsFromSessionMessage(message);
83
+ if (!uuid || !textParts) {
84
+ return;
85
+ }
86
+ targets.push({
87
+ uuid,
88
+ first_text: textParts.firstText,
89
+ input_text: textParts.inputText,
90
+ index,
91
+ ...(previousAssistantUuid ? { previous_assistant_uuid: previousAssistantUuid } : {}),
92
+ });
93
+ });
94
+ return targets.reverse();
95
+ }
39
96
  export function canGenerateSessionTitle(query) {
40
97
  return typeof query.generateSessionTitle === "function";
41
98
  }
@@ -72,7 +129,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
72
129
  value: agent,
73
130
  });
74
131
  }
75
- const EXPECTED_AGENT_SDK_VERSION = "0.3.177";
132
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.193";
76
133
  const require = createRequire(import.meta.url);
77
134
  export function resolveInstalledAgentSdkVersion() {
78
135
  try {
@@ -119,6 +176,309 @@ export async function handleReloadPluginsCommand(session, requestId) {
119
176
  emitRuntimeReloadFailed(session.sessionId, message, requestId);
120
177
  }
121
178
  }
179
+ function resolveRewindTarget(messages, targetUserMessageId) {
180
+ let previousAssistantUuid;
181
+ let textUserCountBeforeTarget = 0;
182
+ for (let index = 0; index < messages.length; index += 1) {
183
+ const message = messages[index];
184
+ const record = message;
185
+ const uuid = typeof record.uuid === "string" ? record.uuid.trim() : "";
186
+ if (record.type === "assistant" && uuid) {
187
+ previousAssistantUuid = uuid;
188
+ continue;
189
+ }
190
+ const textParts = userTextPartsFromSessionMessage(message);
191
+ if (!textParts) {
192
+ continue;
193
+ }
194
+ if (uuid === targetUserMessageId) {
195
+ if (!previousAssistantUuid && textUserCountBeforeTarget > 0) {
196
+ return null;
197
+ }
198
+ if (previousAssistantUuid) {
199
+ const anchorIndex = messages.findIndex((candidate) => {
200
+ const candidateRecord = candidate;
201
+ return candidateRecord.uuid === previousAssistantUuid;
202
+ });
203
+ if (anchorIndex < 0) {
204
+ return null;
205
+ }
206
+ return {
207
+ inputText: textParts.inputText,
208
+ previousAssistantUuid,
209
+ targetIndex: index,
210
+ retainedMessages: messages.slice(0, anchorIndex + 1),
211
+ };
212
+ }
213
+ return {
214
+ inputText: textParts.inputText,
215
+ targetIndex: index,
216
+ retainedMessages: [],
217
+ };
218
+ }
219
+ if (uuid) {
220
+ textUserCountBeforeTarget += 1;
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ export function buildRewindConversationPlan(messages, targetUserMessageId) {
226
+ const resolved = resolveRewindTarget(messages, targetUserMessageId);
227
+ if (!resolved) {
228
+ return null;
229
+ }
230
+ return {
231
+ ...resolved,
232
+ resumeUpdates: mapSessionMessagesToUpdates(resolved.retainedMessages),
233
+ };
234
+ }
235
+ function mapRewindFilesResult(result) {
236
+ return {
237
+ can_rewind: result.canRewind,
238
+ ...(result.error ? { error: result.error } : {}),
239
+ files_changed: result.filesChanged ?? [],
240
+ ...(result.insertions !== undefined ? { insertions: result.insertions } : {}),
241
+ ...(result.deletions !== undefined ? { deletions: result.deletions } : {}),
242
+ };
243
+ }
244
+ async function rewindFiles(session, targetUserMessageId, dryRun) {
245
+ if (typeof session.query.rewindFiles !== "function") {
246
+ return {
247
+ can_rewind: false,
248
+ error: "file rewind is not supported by this SDK runtime",
249
+ files_changed: [],
250
+ };
251
+ }
252
+ return mapRewindFilesResult(await session.query.rewindFiles(targetUserMessageId, { dryRun }));
253
+ }
254
+ function emitRewindResult(sessionId, restoreMode, status, requestId, fileResult, message) {
255
+ writeEvent({
256
+ event: "rewind_result",
257
+ session_id: sessionId,
258
+ restore_mode: restoreMode,
259
+ status,
260
+ ...(fileResult ? { file_result: fileResult } : {}),
261
+ ...(message ? { message } : {}),
262
+ }, requestId);
263
+ }
264
+ async function replaceConversationForRewind(command, session, targetUserMessageId, requestId, pendingRewindResult) {
265
+ const historyMessages = await getSessionMessages(command.session_id, {
266
+ dir: session.cwd,
267
+ includeSystemMessages: true,
268
+ });
269
+ const resolved = buildRewindConversationPlan(historyMessages, targetUserMessageId);
270
+ if (!resolved) {
271
+ bridgeLogger.warn({
272
+ target: LOG_TARGETS.APP_SESSION,
273
+ eventName: "rewind_failed",
274
+ message: "conversation rewind target was stale or inconsistent",
275
+ outcome: "failure",
276
+ ...(requestId ? { requestId } : {}),
277
+ sessionId: session.sessionId,
278
+ fields: {
279
+ target_user_message_id: targetUserMessageId,
280
+ history_message_count: historyMessages.length,
281
+ reason: "target_not_found_or_inconsistent",
282
+ },
283
+ });
284
+ throw new Error(`unknown rewind target: ${targetUserMessageId}`);
285
+ }
286
+ const resumeUpdates = resolved.resumeUpdates;
287
+ const staleSessions = Array.from(sessions.values());
288
+ setSessionListingDir(session.cwd);
289
+ bridgeLogger.info({
290
+ target: LOG_TARGETS.APP_SESSION,
291
+ eventName: "rewind_target_resolved",
292
+ message: "conversation rewind target resolved",
293
+ outcome: "success",
294
+ ...(requestId ? { requestId } : {}),
295
+ sessionId: session.sessionId,
296
+ fields: {
297
+ target_user_message_id: targetUserMessageId,
298
+ target_index: resolved.targetIndex,
299
+ previous_assistant_uuid: resolved.previousAssistantUuid ?? "<none>",
300
+ history_message_count: historyMessages.length,
301
+ retained_message_count: resolved.retainedMessages.length,
302
+ retained_update_count: resumeUpdates.length,
303
+ stale_session_count: staleSessions.length,
304
+ },
305
+ });
306
+ if (!resolved.previousAssistantUuid) {
307
+ bridgeLogger.info({
308
+ target: LOG_TARGETS.APP_SESSION,
309
+ eventName: "rewind_first_message_branch",
310
+ message: "conversation rewind target is first user message; creating fresh replacement",
311
+ outcome: "start",
312
+ ...(requestId ? { requestId } : {}),
313
+ sessionId: session.sessionId,
314
+ fields: {
315
+ target_user_message_id: targetUserMessageId,
316
+ history_message_count: historyMessages.length,
317
+ stale_session_count: staleSessions.length,
318
+ },
319
+ });
320
+ await createSession({
321
+ cwd: session.cwd,
322
+ launchSettings: command.launch_settings,
323
+ connectEvent: "session_replaced",
324
+ requestId,
325
+ restoredInput: resolved.inputText,
326
+ ...(pendingRewindResult ? { pendingRewindResult } : {}),
327
+ ...(staleSessions.length > 0 ? { sessionsToCloseAfterConnect: staleSessions } : {}),
328
+ });
329
+ bridgeLogger.info({
330
+ target: LOG_TARGETS.APP_SESSION,
331
+ eventName: "rewind_replacement_created",
332
+ message: "conversation rewind replacement session created",
333
+ outcome: "success",
334
+ ...(requestId ? { requestId } : {}),
335
+ fields: {
336
+ target_user_message_id: targetUserMessageId,
337
+ resume_session_at: "<none>",
338
+ retained_message_count: 0,
339
+ retained_update_count: 0,
340
+ stale_session_count: staleSessions.length,
341
+ },
342
+ });
343
+ return;
344
+ }
345
+ const sessionsToCloseAfterConnect = staleSessions.filter((stale) => stale !== session);
346
+ await createSession({
347
+ cwd: session.cwd,
348
+ resume: command.session_id,
349
+ resumeSessionAt: resolved.previousAssistantUuid,
350
+ launchSettings: command.launch_settings,
351
+ connectEvent: "session_replaced",
352
+ requestId,
353
+ sessionsToCloseBeforeRegister: [session],
354
+ ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
355
+ restoredInput: resolved.inputText,
356
+ ...(pendingRewindResult ? { pendingRewindResult } : {}),
357
+ ...(sessionsToCloseAfterConnect.length > 0 ? { sessionsToCloseAfterConnect } : {}),
358
+ });
359
+ bridgeLogger.info({
360
+ target: LOG_TARGETS.APP_SESSION,
361
+ eventName: "rewind_replacement_created",
362
+ message: "conversation rewind replacement session created",
363
+ outcome: "success",
364
+ ...(requestId ? { requestId } : {}),
365
+ sessionId: command.session_id,
366
+ fields: {
367
+ target_user_message_id: targetUserMessageId,
368
+ resume_session_at: resolved.previousAssistantUuid,
369
+ retained_message_count: resolved.retainedMessages.length,
370
+ retained_update_count: resumeUpdates.length,
371
+ stale_session_count: staleSessions.length,
372
+ },
373
+ });
374
+ }
375
+ async function handleRewind(command, requestId) {
376
+ const session = sessionById(command.session_id);
377
+ if (!session) {
378
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
379
+ return;
380
+ }
381
+ const targetUserMessageId = command.target_user_message_id.trim();
382
+ if (!targetUserMessageId) {
383
+ slashError(command.session_id, "rewind target cannot be empty", requestId);
384
+ return;
385
+ }
386
+ bridgeLogger.info({
387
+ target: LOG_TARGETS.APP_SESSION,
388
+ eventName: "rewind_requested",
389
+ message: "rewind requested",
390
+ outcome: "start",
391
+ ...(requestId ? { requestId } : {}),
392
+ sessionId: session.sessionId,
393
+ fields: {
394
+ target_user_message_id: targetUserMessageId,
395
+ restore_mode: command.restore_mode,
396
+ },
397
+ });
398
+ if (command.restore_mode === "conversation") {
399
+ try {
400
+ await replaceConversationForRewind(command, session, targetUserMessageId, requestId);
401
+ }
402
+ catch (error) {
403
+ const message = error instanceof Error ? error.message : String(error);
404
+ bridgeLogger.error({
405
+ target: LOG_TARGETS.APP_SESSION,
406
+ eventName: "rewind_failed",
407
+ message: "conversation rewind failed",
408
+ outcome: "failure",
409
+ ...(requestId ? { requestId } : {}),
410
+ sessionId: session.sessionId,
411
+ fields: {
412
+ target_user_message_id: targetUserMessageId,
413
+ restore_mode: command.restore_mode,
414
+ error_message: message,
415
+ },
416
+ });
417
+ slashError(command.session_id, `failed to rewind conversation: ${message}`, requestId);
418
+ }
419
+ return;
420
+ }
421
+ let appliedFileResult;
422
+ try {
423
+ const dryRunResult = await rewindFiles(session, targetUserMessageId, true);
424
+ if (!dryRunResult.can_rewind) {
425
+ slashError(command.session_id, dryRunResult.error ?? "failed to dry-run file rewind", requestId);
426
+ return;
427
+ }
428
+ appliedFileResult = await rewindFiles(session, targetUserMessageId, false);
429
+ if (!appliedFileResult.can_rewind) {
430
+ slashError(command.session_id, appliedFileResult.error ?? "failed to restore code", requestId);
431
+ return;
432
+ }
433
+ }
434
+ catch (error) {
435
+ const message = error instanceof Error ? error.message : String(error);
436
+ bridgeLogger.error({
437
+ target: LOG_TARGETS.APP_SESSION,
438
+ eventName: "rewind_failed",
439
+ message: "file rewind failed",
440
+ outcome: "failure",
441
+ ...(requestId ? { requestId } : {}),
442
+ sessionId: session.sessionId,
443
+ fields: {
444
+ target_user_message_id: targetUserMessageId,
445
+ restore_mode: command.restore_mode,
446
+ error_message: message,
447
+ },
448
+ });
449
+ slashError(command.session_id, `failed to restore code: ${message}`, requestId);
450
+ return;
451
+ }
452
+ if (command.restore_mode === "code") {
453
+ emitRewindResult(session.sessionId, command.restore_mode, "success", requestId, appliedFileResult);
454
+ return;
455
+ }
456
+ try {
457
+ await replaceConversationForRewind(command, session, targetUserMessageId, requestId, {
458
+ event: "rewind_result",
459
+ restore_mode: command.restore_mode,
460
+ status: "success",
461
+ file_result: appliedFileResult,
462
+ });
463
+ }
464
+ catch (error) {
465
+ const message = error instanceof Error ? error.message : String(error);
466
+ bridgeLogger.error({
467
+ target: LOG_TARGETS.APP_SESSION,
468
+ eventName: "rewind_failed",
469
+ message: "conversation rewind failed after file restore",
470
+ outcome: "partial_failure",
471
+ ...(requestId ? { requestId } : {}),
472
+ sessionId: session.sessionId,
473
+ fields: {
474
+ target_user_message_id: targetUserMessageId,
475
+ restore_mode: command.restore_mode,
476
+ error_message: message,
477
+ },
478
+ });
479
+ emitRewindResult(session.sessionId, command.restore_mode, "partial_failure", requestId, appliedFileResult, `Code was restored, but the conversation could not be rewound: ${message}`);
480
+ }
481
+ }
122
482
  async function handleCommand(command, requestId) {
123
483
  logBridgeCommandReceived(command, requestId);
124
484
  const sdkVersionError = agentSdkVersionCompatibilityError();
@@ -597,6 +957,54 @@ async function handleCommand(command, requestId) {
597
957
  }
598
958
  return;
599
959
  }
960
+ case "get_rewind_targets": {
961
+ const session = sessionById(command.session_id);
962
+ if (!session) {
963
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
964
+ return;
965
+ }
966
+ try {
967
+ const historyMessages = await getSessionMessages(command.session_id, {
968
+ dir: session.cwd,
969
+ includeSystemMessages: true,
970
+ });
971
+ const targets = rewindTargetsFromSessionMessages(historyMessages);
972
+ bridgeLogger.info({
973
+ target: LOG_TARGETS.APP_SESSION,
974
+ eventName: "rewind_targets_loaded",
975
+ message: "rewind targets loaded from session history",
976
+ outcome: "success",
977
+ ...(requestId ? { requestId } : {}),
978
+ sessionId: session.sessionId,
979
+ fields: {
980
+ history_message_count: historyMessages.length,
981
+ target_count: targets.length,
982
+ },
983
+ });
984
+ writeEvent({
985
+ event: "rewind_targets",
986
+ session_id: session.sessionId,
987
+ targets,
988
+ }, requestId);
989
+ }
990
+ catch (error) {
991
+ const message = error instanceof Error ? error.message : String(error);
992
+ bridgeLogger.warn({
993
+ target: LOG_TARGETS.APP_SESSION,
994
+ eventName: "rewind_targets_failed",
995
+ message: "failed to load rewind targets",
996
+ outcome: "failure",
997
+ ...(requestId ? { requestId } : {}),
998
+ sessionId: session.sessionId,
999
+ fields: { error_message: message },
1000
+ });
1001
+ slashError(command.session_id, `failed to load rewind targets: ${message}`, requestId);
1002
+ }
1003
+ return;
1004
+ }
1005
+ case "rewind":
1006
+ await handleRewind(command, requestId);
1007
+ return;
600
1008
  case "reload_plugins": {
601
1009
  const session = sessionById(command.session_id);
602
1010
  if (!session) {