// Covers CLI turn compaction lifecycle and external CLI resume-state cleanup. import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createDeferred } from "../../../test/helpers/promise.js"; import { replaceSessionEntry } from "../../config/sessions/session-accessor.js"; import { SESSION_TOTAL_TOKENS_VERSION, type SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { registerContextEngineInRegistry, resolveContextEngine as resolveContextEngineFromRegistry, } from "../../context-engine/registry.js"; import type { ContextEngine } from "../../context-engine/types.js"; import { resolveRuntimeWorkerUrl } from "../../infra/runtime-worker-url.js"; import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; import { PluginRegistryInspectionResources } from "../../plugins/registry-inspection-resources.js"; import { retireInspectionInstances } from "../../plugins/registry-inspection.test-support.js"; import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js"; import { getAsyncWorkSignal } from "../../shared/async-work-scope.js"; import { withEnv } from "../../test-utils/env.js"; import { resolveCliBackendConfig } from "../cli-backends.js"; import { createModelGenerationFixture } from "../embedded-agent-runner/model.generation-scope.test-support.js"; import { SessionManager } from "../sessions/session-manager.js"; import { cliCompactionBackendEntrypoints } from "./cli-compaction-runtime.test-support.js"; import { resetCliCompactionTestDeps, runCliTurnCompactionLifecycle, setCliCompactionTestDeps, } from "./cli-compaction.js"; import { recordCliCompactionInStore as recordCliCompactionInStoreImpl } from "./session-store.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 }) { // The lifecycle compacts canonical OpenClaw session JSONL, so tests write the // same session/message envelope the real store appends. 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", ); } async function persistSessionEntry(params: { sessionKey: string; storePath: string; entry: SessionEntry; }) { await replaceSessionEntry( { sessionKey: params.sessionKey, storePath: params.storePath, }, params.entry, ); } type CliCompactionTestDeps = Parameters[0]; type CliCompactionParams = Parameters[0]; type CompactParams = Parameters[0]; const defaultSettingsManager = async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, }); const defaultPreemptiveCompaction = () => ({ route: "fits" as const, shouldCompact: false, estimatedPromptTokens: 600, promptBudgetBeforeReserve: 800, overflowTokens: 0, toolResultReducibleChars: 0, effectiveReserveTokens: 200, }); function createPreparedRuntimeLease(input: { config: OpenClawConfig; agentDir: string; agentId?: string; workspaceDir?: string; }) { const prepared = createModelGenerationFixture({ config: input.config, label: "cli", agentDir: input.agentDir, workspaceDir: expectDefined(input.workspaceDir, "compaction fixture workspace"), }); return { snapshot: { ...prepared.preparedModelRuntime, ...(input.agentId ? { agentId: input.agentId } : {}), }, pluginGeneration: { configuredCatalogEntries: [], inlineProviderModels: [], pluginMetadataSnapshot: prepared.metadataSnapshot, pluginRegistry: prepared.pluginRegistry, }, [Symbol.asyncDispose]: vi.fn(async () => {}), }; } async function prepareCompactionScenario(params: { tmpDir: string; suffix: string; provider?: string; model?: string; sessionKey?: string; sessionId?: string; sessionEntry?: Partial; cfg?: OpenClawConfig; cwd?: string; contextEngine?: (compactCalls: CompactParams[]) => ContextEngine; maintenance?: CliCompactionTestDeps["runContextEngineMaintenance"]; recordCliCompactionInStore?: CliCompactionTestDeps["recordCliCompactionInStore"]; deps?: CliCompactionTestDeps; }) { const sessionKey = params.sessionKey ?? `agent:main:${params.suffix}`; const sessionId = params.sessionId ?? `session-${params.suffix}`; const transcriptFile = path.join(params.tmpDir, `${params.suffix}.jsonl`); const storePath = path.join(params.tmpDir, `${params.suffix}.sqlite`); await writeSessionFile({ sessionFile: transcriptFile, sessionId }); const sessionEntry: SessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptFile, contextTokens: 1_000, totalTokens: 950, totalTokensFresh: true, totalTokensVersion: SESSION_TOTAL_TOKENS_VERSION, ...params.sessionEntry, }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await persistSessionEntry({ sessionKey, storePath, entry: sessionEntry }); const compactCalls: CompactParams[] = []; const contextEngine = params.contextEngine?.(compactCalls) ?? buildContextEngine({ compactCalls }); const maintenance = vi.fn( params.maintenance ?? (async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 })), ); const recordCliCompactionInStore = params.recordCliCompactionInStore ?? vi.fn(recordCliCompactionInStoreImpl); setCliCompactionTestDeps({ resolveContextEngine: async () => contextEngine, ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined), createPreparedEmbeddedAgentSettingsManager: defaultSettingsManager, shouldPreemptivelyCompactBeforePrompt: defaultPreemptiveCompaction, resolveLiveToolResultMaxChars: () => 20_000, runContextEngineMaintenance: maintenance, recordCliCompactionInStore, ...params.deps, }); const runParams: CliCompactionParams = { cfg: params.cfg ?? ({} as OpenClawConfig), sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", workspaceDir: params.tmpDir, cwd: params.cwd, agentDir: params.tmpDir, provider: params.provider ?? "claude-cli", model: params.model ?? "opus", }; return { compactCalls, contextEngine, maintenance, recordCliCompactionInStore, run: (overrides: Partial = {}) => runCliTurnCompactionLifecycle({ ...runParams, ...overrides }), sessionEntry, sessionId, sessionKey, sessionStore, storePath, transcriptFile, }; } async function prepareContextSuccessorScenario(params: { result: (target: { sessionKey: string; sessionId: string; storePath: string; }) => | Awaited> | Promise>>; suffix: string; tmpDir: string; }) { return prepareCompactionScenario({ suffix: `cli-successor-${params.suffix}`, tmpDir: params.tmpDir, contextEngine: () => ({ ...buildContextEngine({ compactCalls: [] }), async compact() { const sessionKey = `agent:main:cli-successor-${params.suffix}`; const sessionId = `session-cli-successor-${params.suffix}`; const storePath = path.join(params.tmpDir, `cli-successor-${params.suffix}.sqlite`); return params.result({ sessionId, sessionKey, storePath }); }, }), recordCliCompactionInStore: vi.fn( async ({ sessionKey, sessionStore }) => sessionStore[sessionKey], ), }); } describe("runCliTurnCompactionLifecycle", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-compaction-")); setCliCompactionTestDeps({ resolveCliBackendConfig: () => null, acquirePreparedModelRuntime: async (input) => createPreparedRuntimeLease(input), }); }); afterEach(async () => { resetCliCompactionTestDeps(); vi.clearAllTimers(); vi.useRealTimers(); await fs.rm(tmpDir, { recursive: true, force: true }); }); it.each(["context", "native", "fallback", "failure", "stale"] as const)( "retains one resolved engine through compaction and awaits its cleanup (%s)", async (mode) => { const registry = createEmptyPluginRegistry(); const resources = new PluginRegistryInspectionResources(retireInspectionInstances); resources.attach(registry); const retire = vi.fn(); resources.register("fixture", { id: "resource", dispose: retire }); const cleanupStarted = createDeferred(); const cleanupGate = createDeferred(); const cleanupFile = path.join(tmpDir, `owned-${mode}-cleanup.txt`); const controller = new AbortController(); const staleError = new Error("compaction owner retired"); const compactCalls: CompactParams[] = []; const raw = { ...buildContextEngine({ compactCalls }), async dispose() { cleanupStarted.resolve(); await cleanupGate.promise; await fs.writeFile(cleanupFile, "disposed", { signal: getAsyncWorkSignal() }); if (mode === "failure") { throw new Error("cleanup failed too"); } }, }; registerContextEngineInRegistry(registry, "legacy", () => raw, "core"); let engine: ContextEngine | undefined; const scenario = await prepareCompactionScenario({ tmpDir, suffix: `owned-${mode}`, provider: "github-copilot", sessionEntry: mode === "native" || mode === "fallback" ? { agentHarnessId: "copilot" } : {}, maintenance: async () => { expect(retire).not.toHaveBeenCalled(); if (mode === "failure") { throw new Error("maintenance failed"); } return { changed: false, bytesFreed: 0, rewrittenEntries: 0 }; }, deps: { ensureContextEnginesInitialized: vi.fn(), resolveContextEngine: async (cfg) => { engine = await withPluginRuntimeRegistryScope(registry, () => resolveContextEngineFromRegistry(cfg), ); if (mode === "stale") { controller.abort(staleError); } return engine; }, maybeCompactAgentHarnessSession: async () => { expect(retire).not.toHaveBeenCalled(); return mode === "fallback" ? { ok: false, compacted: false, reason: "unsupported", failure: { reason: "unsupported_harness_compaction" }, } : { ok: true, compacted: true }; }, }, }); const result = scenario.run({ abortSignal: controller.signal }).then( () => undefined, (error: unknown) => error, ); try { expect( await Promise.race([ cleanupStarted.promise.then(() => "cleanup"), result.then(() => "returned"), ]), ).toBe("cleanup"); await resources.release(); expect(retire).not.toHaveBeenCalled(); cleanupGate.resolve(); const error = await result; if (mode === "failure") { expect(error).toEqual(new Error("maintenance failed")); } else { expect(error).toBe(mode === "stale" ? staleError : undefined); } expect(compactCalls).toHaveLength(mode === "native" || mode === "stale" ? 0 : 1); expect(await fs.readFile(cleanupFile, "utf8")).toBe("disposed"); expect(retire).toHaveBeenCalledTimes(1); } finally { cleanupGate.resolve(); await result; await engine?.dispose?.().catch(() => {}); await resources.release(); } }, ); it("retains an aborted compaction engine until its raw work and cleanup settle", async () => { const registry = createEmptyPluginRegistry(); const resources = new PluginRegistryInspectionResources(retireInspectionInstances); resources.attach(registry); const retired = createDeferred(); const retire = vi.fn(() => retired.resolve()); resources.register("fixture", { id: "resource", dispose: retire }); const compactStarted = createDeferred(); const compactGate = createDeferred(); const cleanupStarted = createDeferred(); const cleanupGate = createDeferred(); const dispose = vi.fn(async () => { cleanupStarted.resolve(); await cleanupGate.promise; }); const engine = { ...buildContextEngine({ compactCalls: [] }), async compact() { compactStarted.resolve(); await compactGate.promise; expect(retire).not.toHaveBeenCalled(); return { ok: false, compacted: false, reason: "aborted" }; }, dispose, }; registerContextEngineInRegistry(registry, "legacy", () => engine, "core"); let owned: ContextEngine | undefined; const scenario = await prepareCompactionScenario({ tmpDir, suffix: "owned-abort-tail", deps: { ensureContextEnginesInitialized: vi.fn(), resolveContextEngine: async (cfg) => { owned = await withPluginRuntimeRegistryScope(registry, () => resolveContextEngineFromRegistry(cfg), ); return owned; }, }, }); const controller = new AbortController(); const result = scenario .run({ abortSignal: controller.signal }) .catch((error: unknown) => error); try { await compactStarted.promise; controller.abort(new Error("compaction cancelled")); expect(await result).toEqual( expect.objectContaining({ message: expect.stringContaining("compaction cancelled") }), ); await resources.release(); expect(dispose).not.toHaveBeenCalled(); expect(retire).not.toHaveBeenCalled(); compactGate.resolve(); await cleanupStarted.promise; expect(retire).not.toHaveBeenCalled(); cleanupGate.resolve(); await retired.promise; expect(retire).toHaveBeenCalledTimes(1); } finally { compactGate.resolve(); cleanupGate.resolve(); await result; await owned?.dispose?.(); await resources.release(); } }); it("ignores an unversioned fresh total on the first upgraded turn", async () => { const scenario = await prepareCompactionScenario({ suffix: "legacy-unversioned-total", tmpDir, sessionEntry: { totalTokens: 950, totalTokensFresh: true, totalTokensVersion: undefined }, }); const updatedEntry = await scenario.run(); expect(scenario.compactCalls).toEqual([]); expect(updatedEntry).toBe(scenario.sessionEntry); }); it("accepts no compactable entries only from a successful compaction result", async () => { let result = { ok: true, compacted: false, reason: "no real conversation messages" }; const scenario = await prepareCompactionScenario({ suffix: "no-compactable-entries", tmpDir, provider: "test-provider", model: "test-model", contextEngine: () => ({ ...buildContextEngine({ compactCalls: [] }), async compact() { return result; }, }), deps: { openSessionManager: () => ({ getBranch: () => [], buildSessionContext: () => ({ messages: [] }) }) as never, }, }); const runLifecycle = ( ok: boolean, reason = "no real conversation messages", compacted = false, ) => { result = { ok, compacted, reason }; return scenario.run(); }; await expect(runLifecycle(true)).resolves.toBe(scenario.sessionEntry); await expect(runLifecycle(false)).rejects.toThrow( "CLI transcript compaction failed for test-provider/test-model: no real conversation messages", ); await expect(runLifecycle(false, "already under target")).resolves.toBe(scenario.sessionEntry); await expect(runLifecycle(false, "contradictory result", true)).rejects.toThrow( "CLI transcript compaction failed for test-provider/test-model: contradictory result", ); }); it("compacts over-budget CLI transcripts and clears external CLI resume state", async () => { const taskCwd = path.join(tmpDir, "task-repo"); await fs.mkdir(taskCwd, { recursive: true }); const settingsCwds: string[] = []; // Compaction settings should be resolved against the task cwd, not the // bootstrap workspace, because CLI prompts may run from nested repos. const scenario = await prepareCompactionScenario({ suffix: "cli", tmpDir, sessionKey: "agent:main:cli", cwd: taskCwd, sessionEntry: { sessionFile: "agent:main:cli", cliSessionBindings: { "claude-cli": { sessionId: "claude-session" } }, cliSessionIds: { "claude-cli": "claude-session" }, claudeCliSessionId: "claude-session", }, deps: { createPreparedEmbeddedAgentSettingsManager: async (params) => { settingsCwds.push(params.cwd); return defaultSettingsManager(); }, }, }); const { compactCalls, maintenance, sessionId, sessionKey, storePath } = scenario; const updatedEntry = await scenario.run(); expect(compactCalls).toHaveLength(1); const compactCall = compactCalls[0]; expect(compactCall?.sessionId).toBe(sessionId); expect(compactCall?.sessionKey).toBe(sessionKey); expect(compactCall?.sessionTarget).toEqual({ sessionId, sessionKey, storePath }); expect(compactCall?.tokenBudget).toBe(1_000); expect(compactCall?.currentTokenCount).toBe(950); expect(compactCall?.force).toBe(true); expect(compactCall?.compactionTarget).toBe("budget"); expect(compactCall?.runtimeContext?.workspaceDir).toBe(tmpDir); expect(compactCall?.runtimeContext?.cwd).toBe(taskCwd); expect(settingsCwds).toEqual([taskCwd]); expect(maintenance).toHaveBeenCalledTimes(1); const maintenanceCalls = maintenance.mock.calls as unknown as Array< [ { reason?: string; sessionId?: string; sessionKey?: string; sessionFile?: string; }, ] >; const maintenanceCall = maintenanceCalls[0]?.[0]; expect(maintenanceCall?.reason).toBe("compaction"); expect(maintenanceCall?.sessionId).toBe(sessionId); expect(maintenanceCall?.sessionKey).toBe(sessionKey); expect(maintenanceCall?.sessionFile).toBe(sessionKey); expect(updatedEntry?.compactionCount).toBe(1); // Once OpenClaw rewrites the transcript, external CLI resume ids are stale // and must be cleared so the next turn starts from the compacted prompt. expect(updatedEntry?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(updatedEntry?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(updatedEntry?.claudeCliSessionId).toBeUndefined(); }); it("records context-engine compaction successor session targets", async () => { const successorSessionId = "session-cli-rotated"; const recordCliCompactionInStore = vi.fn(recordCliCompactionInStoreImpl); const scenario = await prepareCompactionScenario({ suffix: "cli-rotates", tmpDir, contextEngine: (compactCalls) => ({ ...buildContextEngine({ compactCalls }), async compact(compactParams) { compactCalls.push(compactParams); return { ok: true, compacted: true, result: { summary: "compacted", tokensBefore: compactParams.currentTokenCount ?? 0, tokensAfter: 100, sessionId: successorSessionId, sessionTarget: { sessionKey: "agent:main:cli-rotates", storePath: path.join(tmpDir, "cli-rotates.sqlite"), }, }, }; }, }), recordCliCompactionInStore, }); const { compactCalls, maintenance, sessionId, sessionKey, storePath } = scenario; await scenario.run(); expect(compactCalls[0]?.sessionTarget).toEqual({ sessionId, sessionKey, storePath }); expect(maintenance).toHaveBeenCalledWith( expect.objectContaining({ sessionId: successorSessionId, sessionFile: sessionKey, }), ); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ compactionKind: "context-engine", expectedSession: expect.objectContaining({ sessionId: successorSessionId }), tokensAfter: 100, }), ); }); it("preserves deprecated SQLite-marker successors during CLI maintenance", async () => { const successorId = "session-cli-marker-successor"; const scenario = await prepareContextSuccessorScenario({ suffix: "marker", tmpDir, result: ({ storePath }) => ({ ok: true, compacted: true, result: { tokensBefore: 950, tokensAfter: 100, sessionId: successorId, sessionFile: `sqlite:main:${successorId}:${storePath}`, }, }), }); await scenario.run(); expect(scenario.maintenance).toHaveBeenCalledWith( expect.objectContaining({ sessionFile: `sqlite:main:${successorId}:${scenario.storePath}`, sessionId: successorId, sessionTarget: { agentId: "main", sessionId: successorId, sessionKey: scenario.sessionKey, storePath: scenario.storePath, }, }), ); expect(scenario.recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ expectedSession: expect.objectContaining({ sessionId: successorId }), }), ); }); it("rejects an engine that changes the host row before successor acceptance", async () => { const successorId = "session-cli-key-successor"; const scenario = await prepareContextSuccessorScenario({ suffix: "session-key", tmpDir, result: async ({ sessionKey, storePath }) => { await persistSessionEntry({ sessionKey, storePath, entry: { sessionId: successorId, updatedAt: Date.now() }, }); return { ok: true, compacted: true, result: { tokensBefore: 950, sessionId: successorId, sessionFile: sessionKey, }, }; }, }); await expect(scenario.run()).rejects.toThrow(); expect(scenario.maintenance).not.toHaveBeenCalled(); expect(scenario.recordCliCompactionInStore).not.toHaveBeenCalled(); }); it("rejects conflicting CLI successor ids", async () => { const scenario = await prepareContextSuccessorScenario({ suffix: "conflicting-ids", tmpDir, result: ({ sessionKey, storePath }) => ({ ok: true, compacted: true, result: { tokensBefore: 950, sessionId: "reported-successor", sessionTarget: { agentId: "main", sessionId: "target-successor", sessionKey, storePath, }, }, }), }); await expect(scenario.run()).rejects.toThrow("successor identity is inconsistent"); }); it.each([ ["agent", () => ({ agentId: "other" })], ["session key", () => ({ sessionKey: "agent:main:other" })], ["store", () => ({ storePath: path.join(tmpDir, "other-openclaw-sessions.sqlite") })], ])("rejects a CLI successor outside the active %s binding", async (label, buildOverride) => { const scenario = await prepareContextSuccessorScenario({ suffix: `outside-${label.replace(" ", "-")}`, tmpDir, result: ({ sessionKey, storePath }) => ({ ok: true, compacted: true, result: { tokensBefore: 950, sessionTarget: { agentId: "main", sessionId: "outside-successor", sessionKey, storePath, ...buildOverride(), }, }, }), }); await expect(scenario.run()).rejects.toThrow( "successor target changed the active session binding", ); }); it("treats below-target CLI transcript compaction as a no-op", async () => { const scenario = await prepareCompactionScenario({ suffix: "cli-under-target", tmpDir, sessionEntry: { sessionFile: "agent:main:cli-under-target", cliSessionBindings: { "claude-cli": { sessionId: "claude-session" } }, }, contextEngine: (compactCalls) => ({ ...buildContextEngine({ compactCalls }), async compact(compactParams) { compactCalls.push(compactParams); return { ok: true, compacted: false, reason: "already under target", }; }, }), }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionEntry } = scenario; const updatedEntry = await scenario.run(); expect(compactCalls).toHaveLength(1); expect(maintenance).not.toHaveBeenCalled(); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(updatedEntry).toBe(sessionEntry); expect( scenario.sessionStore[scenario.sessionKey]?.cliSessionBindings?.["claude-cli"]?.sessionId, ).toBe("claude-session"); }); it("treats already-compacted CLI transcript compaction as a no-op", async () => { const scenario = await prepareCompactionScenario({ suffix: "qwen-already-compacted", tmpDir, provider: "ollama", model: "qwen3:14b", contextEngine: (compactCalls) => ({ ...buildContextEngine({ compactCalls }), async compact(compactParams) { compactCalls.push(compactParams); throw new Error("Already compacted"); }, }), }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionEntry } = scenario; const updatedEntry = await scenario.run(); expect(compactCalls).toHaveLength(1); expect(maintenance).not.toHaveBeenCalled(); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(updatedEntry).toBe(sessionEntry); }); it("routes OpenAI Codex harness CLI compaction through native harness compaction", async () => { const compactCalls: CompactParams[] = []; const contextEngine = buildContextEngine({ compactCalls }); const resolveContextEngine = vi.fn(async () => contextEngine); const preparedRuntimeLease = createPreparedRuntimeLease({ config: {}, agentId: "main", agentDir: tmpDir, workspaceDir: tmpDir, }); const pluginGeneration = { configuredCatalogEntries: [], inlineProviderModels: [], pluginMetadataSnapshot: preparedRuntimeLease.snapshot.metadataSnapshot, pluginRegistry: preparedRuntimeLease.snapshot.pluginRegistry, } as never; const acquirePreparedModelRuntime = vi.fn< NonNullable >(async (_input, options) => { expect(options?.pluginGeneration).toBe(pluginGeneration); return preparedRuntimeLease; }); const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: true, compacted: true, result: { tokensBefore: 950, tokensAfter: 100 }, })); const applyAgentAutoCompactionGuard = vi.fn(async () => ({ supported: true, disabled: false, })); const recordCliCompactionInStore = vi.fn(async () => ({ sessionId: "session-codex", updatedAt: Date.now(), compactionCount: 1, })); const scenario = await prepareCompactionScenario({ suffix: "codex", tmpDir, provider: "openai", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex", modelSelectionLocked: true, authProfileOverride: "github-copilot:work", authProfileOverrideSource: "auto", }, recordCliCompactionInStore, deps: { resolveContextEngine, acquirePreparedModelRuntime, ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, applyAgentAutoCompactionGuard, }, }); const { sessionId, sessionKey } = scenario; const updatedEntry = await scenario.run({ pluginGeneration }); expect(resolveContextEngine).toHaveBeenCalledTimes(1); expect(applyAgentAutoCompactionGuard).toHaveBeenCalledWith( expect.objectContaining({ contextEngineInfo: contextEngine.info, }), ); expect(ensureSelectedAgentHarnessPlugin).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", modelId: "gpt-5.5", sessionKey, agentHarnessRuntimeOverride: "codex", pluginRegistry: preparedRuntimeLease.snapshot.pluginRegistry, }), ); expect(acquirePreparedModelRuntime).toHaveBeenCalledOnce(); expect(applyAgentAutoCompactionGuard.mock.invocationCallOrder[0] ?? 0).toBeLessThan( compactAgentHarnessSession.mock.invocationCallOrder[0] ?? 0, ); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); const compactAgentHarnessSessionCalls = compactAgentHarnessSession.mock .calls as unknown as Array<[Record, { preparedModelRuntime?: unknown }]>; expect(compactAgentHarnessSessionCalls[0]?.[0]).toMatchObject({ sessionId, sessionKey, sessionFile: sessionKey, provider: "openai", model: "gpt-5.5", contextTokenBudget: 1_000, currentTokenCount: 950, contextEngine, agentHarnessId: "codex", modelSelectionLocked: true, authProfileId: "github-copilot:work", trigger: "budget", force: true, }); expect(compactAgentHarnessSessionCalls[0]?.[0].contextEngineRuntimeContext).toMatchObject({ authProfileId: "github-copilot:work", agentHarnessId: "codex", modelSelectionLocked: true, }); expect(compactAgentHarnessSessionCalls[0]?.[1]?.preparedModelRuntime).toBe( preparedRuntimeLease.snapshot, ); expect(preparedRuntimeLease[Symbol.asyncDispose]).toHaveBeenCalledOnce(); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ compactionKind: "native-harness", sessionKey, tokensAfter: 100, }), ); expect(updatedEntry?.compactionCount).toBe(1); }); it("treats below-target Copilot native CLI compaction as a no-op", async () => { const compactAgentHarnessSession = vi.fn(async () => ({ ok: true, compacted: false, reason: "already under target", })); const scenario = await prepareCompactionScenario({ suffix: "copilot-under-target", tmpDir, provider: "github-copilot", model: "gpt-5.5", sessionEntry: { agentHarnessId: "copilot" }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never }, }); const { compactCalls, recordCliCompactionInStore, sessionEntry } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(updatedEntry).toBe(sessionEntry); }); it("ignores stale native harness ids when the active provider no longer matches", async () => { const compactAgentHarnessSession = vi.fn(); const scenario = await prepareCompactionScenario({ suffix: "openclaw-after-codex", tmpDir, provider: "openclaw", model: "sonnet-4.6", sessionEntry: { agentHarnessId: "codex" }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never }, }); const { compactCalls, sessionEntry, sessionKey } = scenario; await scenario.run(); expect(compactAgentHarnessSession).not.toHaveBeenCalled(); expect(compactCalls).toHaveLength(1); const lockedEntry: SessionEntry = { ...sessionEntry, modelSelectionLocked: true }; await expect( scenario.run({ sessionEntry: lockedEntry, sessionStore: { [sessionKey]: lockedEntry } }), ).rejects.toThrow("CLI compaction cannot replace a model-locked native harness runtime"); expect(compactAgentHarnessSession).not.toHaveBeenCalled(); expect(compactCalls).toHaveLength(1); }); it.each([ { name: "a normal failed result", compacted: false, reason: "timed out waiting for codex app-server compaction", }, { name: "a contradictory compacted failure", compacted: true, reason: "contradictory native result", }, ])("surfaces nonrecoverable native harness CLI compaction failures for $name", async (result) => { const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: false, compacted: result.compacted, reason: result.reason, })); const scenario = await prepareCompactionScenario({ suffix: "codex-native-failure", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, deps: { ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, }, }); const { compactCalls, recordCliCompactionInStore } = scenario; await expect(scenario.run()).rejects.toThrow( `CLI native harness compaction failed for codex/gpt-5.5: ${result.reason}`, ); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); }); it("skips context-engine fallback when Codex owns automatic compaction", async () => { const compactAgentHarnessSession = vi.fn(async (_params: Record) => ({ ok: true, compacted: false, reason: "codex app-server owns automatic compaction", result: { summary: "", firstKeptEntryId: "", tokensBefore: 950, }, })); const scenario = await prepareCompactionScenario({ suffix: "codex-native-auto-compaction", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never }, }); const { compactCalls, recordCliCompactionInStore, sessionEntry, sessionKey, sessionStore } = scenario; const result = await scenario.run(); // Codex owns automatic compaction; the ownership skip must not fall back to // context-engine compaction (OAuth-only sessions have no direct API key). expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(result).toBe(sessionEntry); const lockedEntry: SessionEntry = { ...sessionEntry, modelSelectionLocked: true }; sessionStore[sessionKey] = lockedEntry; const lockedResult = await scenario.run({ sessionEntry: lockedEntry }); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(2); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); const lockedNativeCall = compactAgentHarnessSession.mock.calls[1]?.[0]; expect(lockedNativeCall).toMatchObject({ agentHarnessId: "codex", modelSelectionLocked: true, contextEngineRuntimeContext: expect.objectContaining({ agentHarnessId: "codex", modelSelectionLocked: true, provider: "codex", model: "gpt-5.5", }), }); expect(lockedResult).toBe(lockedEntry); }); it("does not fall back when native harness compaction returns no result", async () => { const scenario = await prepareCompactionScenario({ suffix: "codex-native-empty", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, deps: { maybeCompactAgentHarnessSession: vi.fn(async () => undefined) as never }, }); await expect(scenario.run()).rejects.toThrow( "CLI native harness compaction failed for codex/gpt-5.5: native harness compaction did not reduce context", ); expect(scenario.compactCalls).toHaveLength(0); }); it("does not interpret a native harness result id as a host successor", async () => { const compactCalls: CompactParams[] = []; const contextEngine = { ...buildContextEngine({ compactCalls }), info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true, }, } satisfies ContextEngine; const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async (compactParams) => { expect(compactParams.contextEngine).toBe(contextEngine); expect(compactParams.contextEngineRuntimeContext).toMatchObject({ currentTokenCount: 950, tokenBudget: 1_000, trigger: "cli_native_budget", }); return { ok: true, compacted: true, result: { summary: "engine-owned", firstKeptEntryId: "entry-1", tokensBefore: 950, tokensAfter: 42, sessionId: "session-codex-owned-engine-rotated", sessionFile: path.join(tmpDir, "session-codex-owned-engine-rotated.jsonl"), }, }; }); const recordCliCompactionInStore = vi.fn(async () => ({ sessionId: "session-codex-owned-engine", updatedAt: Date.now(), compactionCount: 1, })); const scenario = await prepareCompactionScenario({ suffix: "codex-owned-engine", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, recordCliCompactionInStore, deps: { resolveContextEngine: async () => contextEngine, ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, }, }); const { sessionKey } = scenario; await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ sessionKey, tokensAfter: 42, expectedSession: expect.objectContaining({ sessionId: "session-codex-owned-engine" }), }), ); }); it("falls back to context-engine compaction when a pinned harness has no native compactor", async () => { const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: false, compacted: false, reason: 'Agent harness "external-harness" does not support compaction.', failure: { reason: "unsupported_harness_compaction" }, })); const scenario = await prepareCompactionScenario({ suffix: "external-harness", tmpDir, provider: "external-harness", model: "model", sessionEntry: { agentHarnessId: "external-harness" }, deps: { ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, }, }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionKey } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(1); expect(maintenance).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ compactionKind: "context-engine", sessionKey, tokensAfter: 100, }), ); expect(updatedEntry?.compactionCount).toBe(1); }); it("falls back to context-engine compaction when Codex native binding is stale", async () => { const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: false, compacted: false, reason: "thread not found: thread-1", failure: { reason: "stale_thread_binding", }, })); const scenario = await prepareCompactionScenario({ suffix: "codex-stale-binding", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex", authProfileOverride: "github-copilot:work", authProfileOverrideSource: "auto", }, deps: { ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, }, }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionKey } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(1); expect(compactCalls[0]?.runtimeContext).toMatchObject({ authProfileId: "github-copilot:work", }); expect(maintenance).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ sessionKey, tokensAfter: 100, }), ); expect(updatedEntry?.compactionCount).toBe(1); }); it("clears stale Codex native binding when context-engine fallback is below target", async () => { const compactAgentHarnessSession = vi.fn(async () => ({ ok: false, compacted: false, reason: "thread not found: thread-1", failure: { reason: "stale_thread_binding", }, })); const scenario = await prepareCompactionScenario({ suffix: "codex-stale-binding-under-target", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex", cliSessionBindings: { codex: { sessionId: "thread-1" } }, cliSessionIds: { codex: "thread-1" }, }, contextEngine: (compactCalls) => ({ ...buildContextEngine({ compactCalls }), async compact(compactParams) { compactCalls.push(compactParams); return { ok: true, compacted: false, reason: "already under target", }; }, }), deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never }, }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionKey, sessionStore } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(1); expect(maintenance).not.toHaveBeenCalled(); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(updatedEntry?.compactionCount).toBeUndefined(); expect(updatedEntry?.cliSessionBindings?.codex).toBeUndefined(); expect(updatedEntry?.cliSessionIds?.codex).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionBindings?.codex).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.codex).toBeUndefined(); }); it("falls back to context-engine compaction when Codex native compaction returns a raw missing thread reason", async () => { const compactAgentHarnessSession = vi.fn(async () => ({ ok: false, compacted: false, reason: "thread not found: thread-raw", })); const scenario = await prepareCompactionScenario({ suffix: "codex-raw-stale-binding", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never }, }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionKey } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(1); expect(maintenance).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ sessionKey, tokensAfter: 100, }), ); expect(updatedEntry?.compactionCount).toBe(1); }); it("keeps successful context-engine fallback when post-compaction maintenance fails", async () => { const maintenance = vi.fn(async () => { throw new Error("maintenance rotated stale binding"); }); const scenario = await prepareCompactionScenario({ suffix: "codex-stale-maintenance", tmpDir, provider: "codex", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, maintenance, deps: { maybeCompactAgentHarnessSession: vi.fn(async () => ({ ok: false, compacted: false, reason: "thread not found: thread-1", failure: { reason: "stale_thread_binding" }, })) as never, }, }); const { compactCalls, recordCliCompactionInStore, sessionKey } = scenario; const updatedEntry = await scenario.run(); expect(compactCalls).toHaveLength(1); expect(maintenance).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( expect.objectContaining({ sessionKey }), ); expect(updatedEntry?.compactionCount).toBe(1); }); it("initializes built-in context engines before resolving CLI compaction engine", async () => { const calls: string[] = []; const scenario = await prepareCompactionScenario({ suffix: "cli-init", tmpDir, sessionKey: "agent:main:cli", sessionEntry: { sessionFile: undefined }, deps: { ensureContextEnginesInitialized: () => { calls.push("ensure"); }, resolveContextEngine: async () => { calls.push("resolve"); return buildContextEngine({ compactCalls: [] }); }, }, }); await scenario.run(); expect(calls).toEqual(["ensure", "resolve"]); }); it("bounds a hung CLI context-engine compaction and leaves resume state intact", async () => { const scenario = await prepareCompactionScenario({ suffix: "cli-timeout", tmpDir, sessionKey: "agent:main:cli", cfg: { agents: { defaults: { compaction: { timeoutSeconds: 1 } } } } as OpenClawConfig, sessionEntry: { cliSessionBindings: { "claude-cli": { sessionId: "claude-session" } }, cliSessionIds: { "claude-cli": "claude-session" }, claudeCliSessionId: "claude-session", }, contextEngine: (compactCalls) => ({ ...buildContextEngine({ compactCalls }), async compact(compactParams) { compactCalls.push(compactParams); return await new Promise(() => {}); }, }), }); const { compactCalls, maintenance, recordCliCompactionInStore, sessionKey, sessionStore } = scenario; vi.useFakeTimers(); const pending = scenario.run(); const rejection = expect(pending).rejects.toThrow( "CLI transcript compaction failed for claude-cli/opus: Compaction timed out", ); await vi.advanceTimersByTimeAsync(1_000); await rejection; vi.useRealTimers(); expect(compactCalls).toHaveLength(1); expect(compactCalls[0]?.abortSignal).toBeInstanceOf(AbortSignal); expect(compactCalls[0]?.abortSignal?.aborted).toBe(true); expect(maintenance).not.toHaveBeenCalled(); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]?.sessionId).toBe( "claude-session", ); }); it.each(cliCompactionBackendEntrypoints.map((entry) => [entry.provider, entry] as const))( "preserves %s native history and resume binding under compaction pressure", async (provider, entry) => { const { pluginId } = entry; const bundled = path.join(tmpDir, "bundled"); const pluginRoot = path.join(bundled, pluginId); await fs.mkdir(pluginRoot, { recursive: true }); await fs.copyFile( new URL(`../../../extensions/${pluginId}/openclaw.plugin.json`, import.meta.url), path.join(pluginRoot, "openclaw.plugin.json"), ); await fs.writeFile( path.join(pluginRoot, "setup-api.mjs"), `export { default } from ${JSON.stringify(resolveRuntimeWorkerUrl(entry).href)};`, ); await fs.copyFile(path.join(pluginRoot, "setup-api.mjs"), path.join(pluginRoot, "index.mjs")); const backend = withEnv({ OPENCLAW_BUNDLED_PLUGINS_DIR: bundled }, () => resolveCliBackendConfig(provider), ); expect(backend).not.toBeNull(); const compactAgentHarnessSession = vi.fn(); const scenario = await prepareCompactionScenario({ suffix: `${provider}-owns-compaction`, tmpDir, provider, sessionEntry: { cliSessionBindings: { [provider]: { sessionId: "native-session" } }, }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, resolveCliBackendConfig: () => backend, }, }); const { compactCalls, recordCliCompactionInStore, sessionEntry } = scenario; const updatedEntry = await scenario.run(); expect(compactAgentHarnessSession).not.toHaveBeenCalled(); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); expect(updatedEntry).toBe(sessionEntry); expect(updatedEntry?.cliSessionBindings?.[provider]?.sessionId).toBe("native-session"); }, ); it("does not skip compaction when backend does not declare ownsNativeCompaction", async () => { const scenario = await prepareCompactionScenario({ suffix: "generic-no-ownership", tmpDir, provider: "generic-backend", model: "model", deps: { resolveCliBackendConfig: () => ({ id: "generic-backend", config: { command: "generic" }, bundleMcp: false, }), }, }); await scenario.run(); expect(scenario.compactCalls).toHaveLength(1); }); it("still uses native harness path when backend declares ownsNativeCompaction and has agentHarnessId", async () => { const compactAgentHarnessSession = vi.fn(async () => ({ ok: true, compacted: true, result: { tokensBefore: 950, tokensAfter: 100 }, })); const scenario = await prepareCompactionScenario({ suffix: "codex-with-ownership", tmpDir, provider: "openai", model: "gpt-5.5", sessionEntry: { agentHarnessId: "codex" }, deps: { maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, resolveCliBackendConfig: () => ({ id: "codex", config: { command: "codex" }, bundleMcp: false, ownsNativeCompaction: true, }), applyAgentAutoCompactionGuard: vi.fn(async () => ({ supported: true, disabled: false, })), }, }); const { compactCalls, recordCliCompactionInStore } = scenario; await scenario.run(); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1); }); it("does not compact reset-discarded tool output in the post-turn lifecycle", async () => { const sessionKey = "agent:main:reset-lifecycle-proof"; const sessionId = "session-reset-lifecycle-proof"; const storePath = path.join(tmpDir, "sessions-reset-lifecycle.sqlite"); const sessionEntry: SessionEntry = { sessionId, updatedAt: Date.now(), contextTokens: 4_096, totalTokens: 400, totalTokensFresh: true, totalTokensVersion: SESSION_TOTAL_TOKENS_VERSION, }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await persistSessionEntry({ sessionKey, storePath, entry: sessionEntry }); const bigOutput = "x".repeat(20_000); const compactCalls: CompactParams[] = []; const recordCliCompactionInStore = vi.fn(async () => sessionEntry); setCliCompactionTestDeps({ openSessionManager: () => SessionManager.fromEntries([ { type: "session", version: CURRENT_SESSION_VERSION, id: sessionId, timestamp: new Date(0).toISOString(), cwd: tmpDir, }, { type: "message", id: "kept-user", parentId: null, timestamp: new Date(0).toISOString(), message: { role: "user", content: "kept question", timestamp: 1 }, }, { type: "message", id: "kept-tool", parentId: "kept-user", timestamp: new Date(0).toISOString(), message: { role: "toolResult", toolCallId: "call-1", toolName: "bash", content: [{ type: "text", text: bigOutput }], isError: false, timestamp: 2, }, }, { type: "message", id: "kept-assistant", parentId: "kept-tool", timestamp: new Date(0).toISOString(), message: { role: "assistant", content: [{ type: "text", text: "kept answer" }], provider: "test-provider", model: "test-model", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "stop", timestamp: 3, }, }, { type: "reset", id: "reset", parentId: "kept-assistant", timestamp: new Date(0).toISOString(), reason: "new", firstKeptEntryId: "kept-user", }, { type: "message", id: "new", parentId: "reset", timestamp: new Date(0).toISOString(), message: { role: "user", content: "new turn after reset", timestamp: 4 }, }, ]), resolveContextEngine: async () => buildContextEngine({ compactCalls }), createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 512, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, }), resolveLiveToolResultMaxChars: () => 20_000, recordCliCompactionInStore, }); const result = await runCliTurnCompactionLifecycle({ cfg: {} as OpenClawConfig, sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", workspaceDir: tmpDir, agentDir: tmpDir, provider: "claude-cli", model: "opus", }); expect(result).toBe(sessionEntry); expect(compactCalls).toEqual([]); expect(recordCliCompactionInStore).not.toHaveBeenCalled(); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */