mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 01:32:40 +00:00
fix(claude): pass only transcript entries to the rewind session store
The session store parsed each transcript line and cast it to a session entry without checking it. Keep only JSON objects with a string type, and cover transcripts with non-entry lines in the real-SDK rewind test.
This commit is contained in:
@@ -103,9 +103,15 @@ interface TranscriptLine {
|
||||
text: string;
|
||||
}
|
||||
|
||||
function writeTranscript(transcriptPath: string, sessionId: string, lines: TranscriptLine[]): void {
|
||||
const entries = lines.map(({ type, uuid, parentUuid, text }) =>
|
||||
JSON.stringify({
|
||||
function writeTranscript(
|
||||
transcriptPath: string,
|
||||
sessionId: string,
|
||||
lines: Array<TranscriptLine | string>,
|
||||
): void {
|
||||
const entries = lines.map((line) => {
|
||||
if (typeof line === "string") return line;
|
||||
const { type, uuid, parentUuid, text } = line;
|
||||
return JSON.stringify({
|
||||
type,
|
||||
uuid,
|
||||
parentUuid,
|
||||
@@ -113,8 +119,8 @@ function writeTranscript(transcriptPath: string, sessionId: string, lines: Trans
|
||||
cwd: "/repo",
|
||||
timestamp: new Date().toISOString(),
|
||||
message: { role: type, content: text },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
writeFileSync(transcriptPath, `${entries.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
@@ -175,4 +181,40 @@ describe("Claude rewind against the Claude SDK", () => {
|
||||
"first reply",
|
||||
]);
|
||||
});
|
||||
|
||||
test("skips transcript lines that are not entries when forking", async () => {
|
||||
tempRoot = mkdtempSync(path.join(tmpdir(), "claude-rewind-config-dir-"));
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
const projectDir = path.join(tempRoot, "custom-claude-dir", "projects", "-repo");
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
const sessionId = randomUUID();
|
||||
const firstUser = randomUUID();
|
||||
const firstReply = randomUUID();
|
||||
const transcriptPath = path.join(projectDir, `${sessionId}.jsonl`);
|
||||
writeTranscript(transcriptPath, sessionId, [
|
||||
"null",
|
||||
{ type: "user", uuid: firstUser, parentUuid: null, text: "first" },
|
||||
"[1, 2]",
|
||||
'"text"',
|
||||
"{}",
|
||||
{ type: "assistant", uuid: firstReply, parentUuid: firstUser, text: "first reply" },
|
||||
{ type: "user", uuid: randomUUID(), parentUuid: firstReply, text: "second" },
|
||||
]);
|
||||
let currentSessionId = sessionId;
|
||||
|
||||
await revertClaudeConversation({
|
||||
sdk: realClaudeRewindSdk,
|
||||
sessionId,
|
||||
transcriptPath,
|
||||
messageId: firstReply,
|
||||
setSessionId: (nextSessionId) => {
|
||||
currentSessionId = nextSessionId;
|
||||
},
|
||||
});
|
||||
|
||||
expect(readMessageTexts(path.join(projectDir, `${currentSessionId}.jsonl`))).toEqual([
|
||||
"first",
|
||||
"first reply",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,15 +49,27 @@ function parseTranscriptEntries(text: string): SessionStoreEntry[] {
|
||||
const entries: SessionStoreEntry[] = [];
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
entries.push(JSON.parse(line) as SessionStoreEntry);
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
// Skip a line that is not JSON, as the SDK's own transcript reader does.
|
||||
continue;
|
||||
}
|
||||
if (isSessionStoreEntry(parsed)) entries.push(parsed);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function isSessionStoreEntry(value: unknown): value is SessionStoreEntry {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
typeof (value as { type?: unknown }).type === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export async function revertClaudeConversation(input: {
|
||||
sdk: ClaudeRewindSdk;
|
||||
sessionId: string | null;
|
||||
|
||||
Reference in New Issue
Block a user