mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(cli): bound fresh session reseed
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user