diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 3ccaae0324..0bfb9e4b2f 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -255,6 +255,51 @@ describe("session-memory hook", () => { expect(memoryContent).toContain("assistant: Fourth message"); }); + it("falls back to latest .jsonl.reset.* transcript when active file is empty", async () => { + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); + const sessionsDir = path.join(tempDir, "sessions"); + await fs.mkdir(sessionsDir, { recursive: true }); + + const activeSessionFile = await writeWorkspaceFile({ + dir: sessionsDir, + name: "test-session.jsonl", + content: "", + }); + + // Simulate /new rotation where useful content is now in .reset.* file + const resetContent = createMockSessionContent([ + { role: "user", content: "Message from rotated transcript" }, + { role: "assistant", content: "Recovered from reset fallback" }, + ]); + await writeWorkspaceFile({ + dir: sessionsDir, + name: "test-session.jsonl.reset.2026-02-16T22-26-33.000Z", + content: resetContent, + }); + + const cfg = { + agents: { defaults: { workspace: tempDir } }, + } satisfies OpenClawConfig; + + const event = createHookEvent("command", "new", "agent:main:main", { + cfg, + previousSessionEntry: { + sessionId: "test-123", + sessionFile: activeSessionFile, + }, + }); + + await handler(event); + + const memoryDir = path.join(tempDir, "memory"); + const files = await fs.readdir(memoryDir); + expect(files.length).toBe(1); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); + + expect(memoryContent).toContain("user: Message from rotated transcript"); + expect(memoryContent).toContain("assistant: Recovered from reset fallback"); + }); + it("handles empty session files gracefully", async () => { // Should not throw const { files } = await runNewWithPreviousSession({ sessionContent: "" }); diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 4f1a0662c8..c8f59fb514 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -67,6 +67,46 @@ async function getRecentSessionContent( } } +/** + * Try the active transcript first; if /new already rotated it, + * fallback to the latest .jsonl.reset.* sibling. + */ +async function getRecentSessionContentWithResetFallback( + sessionFilePath: string, + messageCount: number = 15, +): Promise { + const primary = await getRecentSessionContent(sessionFilePath, messageCount); + if (primary) { + return primary; + } + + try { + const dir = path.dirname(sessionFilePath); + const base = path.basename(sessionFilePath); + const resetPrefix = `${base}.reset.`; + const files = await fs.readdir(dir); + const resetCandidates = files.filter((name) => name.startsWith(resetPrefix)).toSorted(); + + if (resetCandidates.length === 0) { + return primary; + } + + const latestResetPath = path.join(dir, resetCandidates[resetCandidates.length - 1]); + const fallback = await getRecentSessionContent(latestResetPath, messageCount); + + if (fallback) { + log.debug("Loaded session content from reset fallback", { + sessionFilePath, + latestResetPath, + }); + } + + return fallback || primary; + } catch { + return primary; + } +} + /** * Save session context to memory when /new command is triggered */ @@ -119,8 +159,8 @@ const saveSessionToMemory: HookHandler = async (event) => { let sessionContent: string | null = null; if (sessionFile) { - // Get recent conversation content - sessionContent = await getRecentSessionContent(sessionFile, messageCount); + // Get recent conversation content, with fallback to rotated reset transcript. + sessionContent = await getRecentSessionContentWithResetFallback(sessionFile, messageCount); log.debug("Session content loaded", { length: sessionContent?.length ?? 0, messageCount,