claude-code-rust 0.12.3 → 0.13.0

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.
@@ -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.198";
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) {