From 8559a84e4ea51e48ef41685d2a60e1c829ff6d57 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 26 Apr 2026 09:07:26 +0530 Subject: [PATCH] fix(cli): bound fresh session reseed --- src/agents/cli-runner.reliability.test.ts | 15 +++- src/agents/cli-runner/prepare.ts | 14 ++- src/agents/cli-runner/session-history.test.ts | 86 +++++++++++++++++++ src/agents/cli-runner/session-history.ts | 76 ++++++++++++++-- 4 files changed, 180 insertions(+), 11 deletions(-) diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index 81adc6e2376..9cc4d6e6375 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -759,6 +759,19 @@ describe("runCliAgent reliability", () => { const { dir, sessionFile } = createSessionFile({ history: [{ role: "user", content: "earlier ask" }], }); + fs.appendFileSync( + sessionFile, + `${JSON.stringify({ + type: "compaction", + id: "compaction-1", + parentId: "msg-0", + timestamp: new Date(2).toISOString(), + summary: "compacted earlier ask", + firstKeptEntryId: "msg-0", + tokensBefore: 10_000, + })}\n`, + "utf-8", + ); const config: OpenClawConfig = { agents: { defaults: { @@ -796,7 +809,7 @@ describe("runCliAgent reliability", () => { }); expect(context.params.prompt).toBe("hook context\n\ncurrent ask"); - expect(context.openClawHistoryPrompt).toContain("User: earlier ask"); + expect(context.openClawHistoryPrompt).toContain("Compaction summary: compacted earlier ask"); expect(context.openClawHistoryPrompt).toContain("hook context"); expect(context.openClawHistoryPrompt).toContain("current ask"); } finally { diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 2b13ff0e0a1..1aa6b2cd7db 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -42,7 +42,11 @@ import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js import { prepareCliBundleMcpConfig } from "./bundle-mcp.js"; import { buildSystemPrompt, normalizeCliModel } from "./helpers.js"; import { cliBackendLog } from "./log.js"; -import { buildCliSessionHistoryPrompt, loadCliSessionHistoryMessages } from "./session-history.js"; +import { + buildCliSessionHistoryPrompt, + loadCliSessionHistoryMessages, + loadCliSessionReseedMessages, +} from "./session-history.js"; import type { PreparedCliRunContext, RunCliAgentParams } from "./types.js"; const prepareDeps = { @@ -363,7 +367,13 @@ export async function prepareCliRunContext( const openClawHistoryPrompt = reusableCliSession.sessionId ? undefined : buildCliSessionHistoryPrompt({ - messages: loadOpenClawHistoryMessages(), + messages: loadCliSessionReseedMessages({ + sessionId: params.sessionId, + sessionFile: params.sessionFile, + sessionKey: params.sessionKey, + agentId: params.agentId, + config: params.config, + }), prompt: preparedPrompt, }); systemPrompt = applyPluginTextReplacements(systemPrompt, backendResolved.textTransforms?.input); diff --git a/src/agents/cli-runner/session-history.test.ts b/src/agents/cli-runner/session-history.test.ts index 32bbff89d19..effe9756b3a 100644 --- a/src/agents/cli-runner/session-history.test.ts +++ b/src/agents/cli-runner/session-history.test.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { buildCliSessionHistoryPrompt, loadCliSessionHistoryMessages, + loadCliSessionReseedMessages, MAX_CLI_SESSION_HISTORY_FILE_BYTES, MAX_CLI_SESSION_HISTORY_MESSAGES, } from "./session-history.js"; @@ -220,6 +221,91 @@ describe("loadCliSessionHistoryMessages", () => { }); }); +describe("loadCliSessionReseedMessages", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("does not reseed fresh CLI sessions from raw transcript history before compaction", () => { + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-state-")); + vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); + const sessionFile = createSessionTranscript({ + rootDir: stateDir, + sessionId: "session-no-compaction", + messages: ["raw secret", "large context"], + }); + + try { + expect( + loadCliSessionReseedMessages({ + sessionId: "session-no-compaction", + sessionFile, + sessionKey: "agent:main:main", + agentId: "main", + }), + ).toEqual([]); + } finally { + fs.rmSync(stateDir, { recursive: true, force: true }); + } + }); + + it("reseeds fresh CLI sessions from the latest compaction summary and post-compaction tail", () => { + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-state-")); + vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); + const sessionFile = createSessionTranscript({ + rootDir: stateDir, + sessionId: "session-compacted", + messages: ["pre-compaction raw history"], + }); + fs.appendFileSync( + sessionFile, + `${JSON.stringify({ + type: "compaction", + id: "compaction-1", + parentId: "msg-0", + timestamp: new Date(2).toISOString(), + summary: "safe compacted summary", + firstKeptEntryId: "msg-0", + tokensBefore: 10_000, + })}\n`, + "utf-8", + ); + fs.appendFileSync( + sessionFile, + `${JSON.stringify({ + type: "message", + id: "msg-1", + parentId: "compaction-1", + timestamp: new Date(3).toISOString(), + message: { + role: "user", + content: "post-compaction ask", + timestamp: 3, + }, + })}\n`, + "utf-8", + ); + + try { + const reseed = loadCliSessionReseedMessages({ + sessionId: "session-compacted", + sessionFile, + sessionKey: "agent:main:main", + agentId: "main", + }); + expect(reseed).toMatchObject([ + { role: "compactionSummary", summary: "safe compacted summary" }, + { role: "user", content: "post-compaction ask" }, + ]); + expect(buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" })).toContain( + "Compaction summary: safe compacted summary", + ); + } finally { + fs.rmSync(stateDir, { recursive: true, force: true }); + } + }); +}); + describe("buildCliSessionHistoryPrompt", () => { it("renders OpenClaw transcript history around the next user message", () => { const prompt = buildCliSessionHistoryPrompt({ diff --git a/src/agents/cli-runner/session-history.ts b/src/agents/cli-runner/session-history.ts index 4ed266ceb63..0fbd5b102e1 100644 --- a/src/agents/cli-runner/session-history.ts +++ b/src/agents/cli-runner/session-history.ts @@ -18,6 +18,12 @@ export const MAX_CLI_SESSION_HISTORY_MESSAGES = MAX_AGENT_HOOK_HISTORY_MESSAGES; type HistoryMessage = { role?: unknown; content?: unknown; + summary?: unknown; +}; +type HistoryEntry = { + type?: unknown; + message?: unknown; + summary?: unknown; }; function coerceHistoryText(content: unknown): string { @@ -50,11 +56,20 @@ export function buildCliSessionHistoryPrompt(params: { } const entry = message as HistoryMessage; const role = - entry.role === "assistant" ? "Assistant" : entry.role === "user" ? "User" : undefined; + entry.role === "assistant" + ? "Assistant" + : entry.role === "user" + ? "User" + : entry.role === "compactionSummary" + ? "Compaction summary" + : undefined; if (!role) { return []; } - const text = coerceHistoryText(entry.content); + const text = + entry.role === "compactionSummary" && typeof entry.summary === "string" + ? entry.summary.trim() + : coerceHistoryText(entry.content); return text ? [`${role}: ${text}`] : []; }) .join("\n\n") @@ -118,7 +133,7 @@ function resolveSafeCliSessionFile(params: { }; } -export function loadCliSessionHistoryMessages(params: { +function loadCliSessionEntries(params: { sessionId: string; sessionFile: string; sessionKey?: string; @@ -140,12 +155,57 @@ export function loadCliSessionHistoryMessages(params: { if (!stat.isFile() || stat.size > MAX_CLI_SESSION_HISTORY_FILE_BYTES) { return []; } - const entries = SessionManager.open(realSessionFile).getEntries(); - const history = entries.flatMap((entry) => - entry?.type === "message" ? [entry.message as unknown] : [], - ); - return limitAgentHookHistoryMessages(history, MAX_CLI_SESSION_HISTORY_MESSAGES); + return SessionManager.open(realSessionFile).getEntries(); } catch { return []; } } + +export function loadCliSessionHistoryMessages(params: { + sessionId: string; + sessionFile: string; + sessionKey?: string; + agentId?: string; + config?: OpenClawConfig; +}): unknown[] { + const history = loadCliSessionEntries(params).flatMap((entry) => { + const candidate = entry as HistoryEntry; + return candidate.type === "message" ? [candidate.message] : []; + }); + return limitAgentHookHistoryMessages(history, MAX_CLI_SESSION_HISTORY_MESSAGES); +} + +export function loadCliSessionReseedMessages(params: { + sessionId: string; + sessionFile: string; + sessionKey?: string; + agentId?: string; + config?: OpenClawConfig; +}): unknown[] { + const entries = loadCliSessionEntries(params); + const latestCompactionIndex = entries.findLastIndex((entry) => { + const candidate = entry as HistoryEntry; + return candidate.type === "compaction" && typeof candidate.summary === "string"; + }); + if (latestCompactionIndex < 0) { + return []; + } + + const compaction = entries[latestCompactionIndex] as HistoryEntry; + const summary = typeof compaction.summary === "string" ? compaction.summary.trim() : ""; + if (!summary) { + return []; + } + + const tailMessages = entries.slice(latestCompactionIndex + 1).flatMap((entry) => { + const candidate = entry as HistoryEntry; + return candidate.type === "message" ? [candidate.message] : []; + }); + return [ + { + role: "compactionSummary", + summary, + }, + ...limitAgentHookHistoryMessages(tailMessages, MAX_CLI_SESSION_HISTORY_MESSAGES - 1), + ]; +}