fix(cli): bound fresh session reseed

This commit is contained in:
Ayaan Zaidi
2026-04-26 09:07:26 +05:30
parent 12e4841d96
commit 8559a84e4e
4 changed files with 180 additions and 11 deletions

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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({

View File

@@ -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),
];
}