Skip to content

Commit cafbd74

Browse files
authored
fix(gateway): preserve legacy reseed attachments (#99839)
1 parent d71a24f commit cafbd74

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

src/gateway/cli-session-history.claude.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ function cloneJsonValue<T>(value: T): T {
102102
return structuredClone(value);
103103
}
104104

105+
function removeContentBlock<T>(content: T[], blockIndex: number): T[] | null {
106+
const nextContent = cloneJsonValue(content);
107+
nextContent.splice(blockIndex, 1);
108+
return nextContent.length > 0 ? nextContent : null;
109+
}
110+
105111
function normalizeClaudeCliContent(
106112
content: string | unknown[],
107113
toolNameRegistry: ToolNameRegistry,
@@ -305,9 +311,8 @@ function parseClaudeCliHistoryEntry(
305311
}
306312
// The receipt proves only the matching text block is synthetic.
307313
// Preserve sibling images or other native content that has no local duplicate proof.
308-
const nextContent = cloneJsonValue(content);
309-
nextContent.splice(candidate.blockIndex, 1);
310-
if (nextContent.length === 0) {
314+
const nextContent = removeContentBlock(content, candidate.blockIndex);
315+
if (!nextContent) {
311316
return null;
312317
}
313318
content = nextContent;
@@ -316,12 +321,20 @@ function parseClaudeCliHistoryEntry(
316321
for (const candidate of promptTextCandidates) {
317322
const reseedPrompt = parseCliReseedPrompt(candidate.text);
318323
if (reseedPrompt.kind === "legacy") {
319-
if (!reseedPrompt.userMessage) {
320-
return null;
321-
}
322324
if (candidate.blockIndex === undefined) {
325+
if (!reseedPrompt.userMessage) {
326+
return null;
327+
}
323328
content = reseedPrompt.userMessage;
324329
} else if (Array.isArray(content)) {
330+
if (!reseedPrompt.userMessage) {
331+
const contentWithoutReseed = removeContentBlock(content, candidate.blockIndex);
332+
if (!contentWithoutReseed) {
333+
return null;
334+
}
335+
content = contentWithoutReseed;
336+
break;
337+
}
325338
const nextContent = cloneJsonValue(content);
326339
const block = nextContent[candidate.blockIndex];
327340
if (block && typeof block === "object") {

src/gateway/cli-session-history.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,60 @@ describe("cli session history", () => {
609609
});
610610
});
611611

612+
it("drops empty legacy reseed text while preserving sibling native content", async () => {
613+
await withClaudeProjectsDir(async ({ homeDir, sessionId, filePath }) => {
614+
const caption = { type: "text", text: "real caption" };
615+
const image = {
616+
type: "image",
617+
source: { type: "base64", media_type: "image/png", data: "x" },
618+
};
619+
const document = { type: "document", source: { type: "text", data: "notes" } };
620+
await fs.writeFile(
621+
filePath,
622+
`${JSON.stringify({
623+
type: "user",
624+
uuid: "legacy-empty-reseed",
625+
message: {
626+
role: "user",
627+
content: [
628+
caption,
629+
{ type: "text", text: buildLegacyReseedPrompt("") },
630+
image,
631+
document,
632+
],
633+
},
634+
})}\n`,
635+
"utf-8",
636+
);
637+
638+
const messages = readClaudeCliSessionMessages({ cliSessionId: sessionId, homeDir });
639+
640+
expect(messages).toHaveLength(1);
641+
expect(readRecord(messages[0]).content).toEqual([caption, image, document]);
642+
});
643+
});
644+
645+
it.each([
646+
["string", buildLegacyReseedPrompt("")],
647+
["single text block", [{ type: "text", text: buildLegacyReseedPrompt("") }]],
648+
])("drops empty legacy reseed rows in %s form", async (_label, content) => {
649+
await withClaudeProjectsDir(async ({ homeDir, sessionId, filePath }) => {
650+
await fs.writeFile(
651+
filePath,
652+
`${JSON.stringify({
653+
type: "user",
654+
uuid: "legacy-empty-reseed",
655+
message: { role: "user", content },
656+
})}\n`,
657+
"utf-8",
658+
);
659+
660+
const messages = readClaudeCliSessionMessages({ cliSessionId: sessionId, homeDir });
661+
662+
expect(messages).toEqual([]);
663+
});
664+
});
665+
612666
it("fails open when the first user row does not match the reseed receipt", async () => {
613667
await withClaudeProjectsDir(async ({ homeDir, sessionId, filePath }) => {
614668
const expectedPrompt = "expected synthetic prompt";

0 commit comments

Comments
 (0)