diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts
index 56d3fd725d2..ec71f60e14a 100644
--- a/src/agents/cli-runner.reliability.test.ts
+++ b/src/agents/cli-runner.reliability.test.ts
@@ -74,6 +74,7 @@ function buildPreparedContext(params?: {
sessionKey?: string;
cliSessionId?: string;
runId?: string;
+ openClawHistoryPrompt?: string;
}): PreparedCliRunContext {
const backend = {
command: "codex",
@@ -115,6 +116,9 @@ function buildPreparedContext(params?: {
systemPrompt: "You are a helpful assistant.",
systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"],
bootstrapPromptWarningLines: [],
+ ...(params?.openClawHistoryPrompt
+ ? { openClawHistoryPrompt: params.openClawHistoryPrompt }
+ : {}),
authEpochVersion: 2,
};
}
@@ -324,6 +328,56 @@ describe("runCliAgent reliability", () => {
});
});
+ it("seeds fresh CLI sessions from the OpenClaw transcript", async () => {
+ supervisorSpawnMock.mockResolvedValueOnce(
+ createManagedRun({
+ reason: "exit",
+ exitCode: 0,
+ exitSignal: null,
+ durationMs: 50,
+ stdout: "hello from cli",
+ stderr: "",
+ timedOut: false,
+ noOutputTimedOut: false,
+ }),
+ );
+
+ const result = await runPreparedCliAgent(
+ buildPreparedContext({
+ openClawHistoryPrompt:
+ "Continue this conversation using the OpenClaw transcript below.\n\nUser: earlier ask\n\nAssistant: earlier answer\n\n\nhi\n",
+ }),
+ );
+
+ expect(result.meta.finalPromptText).toContain("User: earlier ask");
+ expect(result.meta.finalPromptText).toContain("Assistant: earlier answer");
+ });
+
+ it("keeps resumed CLI sessions on native resume history", async () => {
+ supervisorSpawnMock.mockResolvedValueOnce(
+ createManagedRun({
+ reason: "exit",
+ exitCode: 0,
+ exitSignal: null,
+ durationMs: 50,
+ stdout: "hello from cli",
+ stderr: "",
+ timedOut: false,
+ noOutputTimedOut: false,
+ }),
+ );
+
+ const result = await runPreparedCliAgent(
+ buildPreparedContext({
+ cliSessionId: "cli-session",
+ openClawHistoryPrompt: "User: earlier ask",
+ }),
+ );
+
+ expect(result.meta.finalPromptText).not.toContain("User: earlier ask");
+ expect(result.meta.finalPromptText).toContain("hi");
+ });
+
it("reports CLI reply backends as streaming until the managed run finishes", async () => {
const operation = createReplyOperation({
sessionKey: "agent:main:main",
diff --git a/src/agents/cli-runner/session-history.test.ts b/src/agents/cli-runner/session-history.test.ts
index b26949730c4..32bbff89d19 100644
--- a/src/agents/cli-runner/session-history.test.ts
+++ b/src/agents/cli-runner/session-history.test.ts
@@ -4,6 +4,7 @@ import path from "node:path";
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
+ buildCliSessionHistoryPrompt,
loadCliSessionHistoryMessages,
MAX_CLI_SESSION_HISTORY_FILE_BYTES,
MAX_CLI_SESSION_HISTORY_MESSAGES,
@@ -218,3 +219,28 @@ describe("loadCliSessionHistoryMessages", () => {
}
});
});
+
+describe("buildCliSessionHistoryPrompt", () => {
+ it("renders OpenClaw transcript history around the next user message", () => {
+ const prompt = buildCliSessionHistoryPrompt({
+ messages: [
+ { role: "user", content: "old ask" },
+ { role: "assistant", content: [{ type: "text", text: "old answer" }] },
+ ],
+ prompt: "new ask",
+ });
+
+ expect(prompt).toContain("User: old ask");
+ expect(prompt).toContain("Assistant: old answer");
+ expect(prompt).toContain("\nnew ask\n");
+ });
+
+ it("skips reseed text when the transcript has no renderable conversation", () => {
+ expect(
+ buildCliSessionHistoryPrompt({
+ messages: [{ role: "tool", content: "ignored" }],
+ prompt: "new ask",
+ }),
+ ).toBeUndefined();
+ });
+});
diff --git a/src/agents/command/cli-compaction.test.ts b/src/agents/command/cli-compaction.test.ts
new file mode 100644
index 00000000000..c83c5f64f1b
--- /dev/null
+++ b/src/agents/command/cli-compaction.test.ts
@@ -0,0 +1,170 @@
+import fs from "node:fs/promises";
+import os from "node:os";
+import path from "node:path";
+import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import type { SessionEntry } from "../../config/sessions/types.js";
+import type { OpenClawConfig } from "../../config/types.openclaw.js";
+import type { ContextEngine } from "../../context-engine/types.js";
+import {
+ resetCliCompactionTestDeps,
+ runCliTurnCompactionLifecycle,
+ setCliCompactionTestDeps,
+} from "./cli-compaction.js";
+
+function buildContextEngine(params: {
+ compactCalls: Array[0]>;
+}): ContextEngine {
+ return {
+ info: {
+ id: "legacy",
+ name: "Legacy Context Engine",
+ },
+ async ingest() {
+ return { ingested: false };
+ },
+ async assemble(assembleParams) {
+ return { messages: assembleParams.messages, estimatedTokens: 0 };
+ },
+ async compact(compactParams) {
+ params.compactCalls.push(compactParams);
+ return {
+ ok: true,
+ compacted: true,
+ result: {
+ summary: "compacted",
+ tokensBefore: compactParams.currentTokenCount ?? 0,
+ tokensAfter: 100,
+ },
+ };
+ },
+ };
+}
+
+async function writeSessionFile(params: { sessionFile: string; sessionId: string }) {
+ await fs.mkdir(path.dirname(params.sessionFile), { recursive: true });
+ await fs.writeFile(
+ params.sessionFile,
+ [
+ JSON.stringify({
+ type: "session",
+ version: CURRENT_SESSION_VERSION,
+ id: params.sessionId,
+ timestamp: new Date(0).toISOString(),
+ cwd: path.dirname(params.sessionFile),
+ }),
+ JSON.stringify({
+ type: "message",
+ message: { role: "user", content: "old ask", timestamp: 1 },
+ }),
+ JSON.stringify({
+ type: "message",
+ message: {
+ role: "assistant",
+ content: [{ type: "text", text: "old answer" }],
+ timestamp: 2,
+ },
+ }),
+ "",
+ ].join("\n"),
+ "utf-8",
+ );
+}
+
+describe("runCliTurnCompactionLifecycle", () => {
+ let tmpDir: string;
+
+ beforeEach(async () => {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-compaction-"));
+ });
+
+ afterEach(async () => {
+ resetCliCompactionTestDeps();
+ await fs.rm(tmpDir, { recursive: true, force: true });
+ });
+
+ it("compacts over-budget CLI transcripts and clears external CLI resume state", async () => {
+ const sessionKey = "agent:main:cli";
+ const sessionId = "session-cli";
+ const sessionFile = path.join(tmpDir, "session.jsonl");
+ const storePath = path.join(tmpDir, "sessions.json");
+ await writeSessionFile({ sessionFile, sessionId });
+
+ const sessionEntry: SessionEntry = {
+ sessionId,
+ updatedAt: Date.now(),
+ sessionFile,
+ contextTokens: 1_000,
+ totalTokens: 950,
+ totalTokensFresh: true,
+ cliSessionBindings: {
+ "claude-cli": { sessionId: "claude-session" },
+ },
+ cliSessionIds: {
+ "claude-cli": "claude-session",
+ },
+ claudeCliSessionId: "claude-session",
+ };
+ const sessionStore: Record = { [sessionKey]: sessionEntry };
+ await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
+
+ const compactCalls: Array[0]> = [];
+ const maintenance = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 }));
+ setCliCompactionTestDeps({
+ resolveContextEngine: async () => buildContextEngine({ compactCalls }),
+ createPreparedEmbeddedPiSettingsManager: async () => ({
+ getCompactionReserveTokens: () => 200,
+ getCompactionKeepRecentTokens: () => 0,
+ applyOverrides: () => {},
+ }),
+ shouldPreemptivelyCompactBeforePrompt: () => ({
+ route: "fits",
+ shouldCompact: false,
+ estimatedPromptTokens: 600,
+ promptBudgetBeforeReserve: 800,
+ overflowTokens: 0,
+ toolResultReducibleChars: 0,
+ effectiveReserveTokens: 200,
+ }),
+ resolveLiveToolResultMaxChars: () => 20_000,
+ runContextEngineMaintenance: maintenance,
+ });
+
+ const updatedEntry = await runCliTurnCompactionLifecycle({
+ cfg: {} as OpenClawConfig,
+ sessionId,
+ sessionKey,
+ sessionEntry,
+ sessionStore,
+ storePath,
+ sessionAgentId: "main",
+ workspaceDir: tmpDir,
+ agentDir: tmpDir,
+ provider: "claude-cli",
+ model: "opus",
+ });
+
+ expect(compactCalls).toHaveLength(1);
+ expect(compactCalls[0]).toMatchObject({
+ sessionId,
+ sessionKey,
+ sessionFile,
+ tokenBudget: 1_000,
+ currentTokenCount: 950,
+ force: true,
+ compactionTarget: "budget",
+ });
+ expect(maintenance).toHaveBeenCalledWith(
+ expect.objectContaining({
+ reason: "compaction",
+ sessionId,
+ sessionKey,
+ sessionFile,
+ }),
+ );
+ expect(updatedEntry?.compactionCount).toBe(1);
+ expect(updatedEntry?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
+ expect(updatedEntry?.cliSessionIds?.["claude-cli"]).toBeUndefined();
+ expect(updatedEntry?.claudeCliSessionId).toBeUndefined();
+ });
+});