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.
- package/README.md +3 -7
- package/agent-sdk/dist/bridge/commands.js +20 -0
- package/agent-sdk/dist/bridge/events.js +15 -1
- package/agent-sdk/dist/bridge/logger.js +10 -0
- package/agent-sdk/dist/bridge/message_handlers.js +172 -1
- package/agent-sdk/dist/bridge/session_lifecycle.js +37 -0
- package/agent-sdk/dist/bridge/state_parsing.js +9 -0
- package/agent-sdk/dist/bridge/tooling.js +71 -18
- package/agent-sdk/dist/bridge.js +410 -2
- package/agent-sdk/dist/bridge.test.js +650 -5
- package/package.json +9 -4
- package/scripts/jscpd-warning-summary.mjs +132 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
281
|
+
return `${name} ${server} ${uri}`;
|
|
272
282
|
}
|
|
273
283
|
if (uri) {
|
|
274
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1604
|
-
if (toolName
|
|
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 =
|
|
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
|
|
1831
|
+
const shellResultRecord = isShellToolName(toolName)
|
|
1832
|
+
? findShellResultRecord(rawResult, rawContent)
|
|
1833
|
+
: undefined;
|
|
1781
1834
|
const normalizedRawOutput = normalizeToolResultText(rawContent, isError);
|
|
1782
|
-
const rawOutput =
|
|
1783
|
-
?
|
|
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 ===
|
|
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;
|
package/agent-sdk/dist/bridge.js
CHANGED
|
@@ -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.
|
|
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) {
|