fix(compaction): use canonical session context projection for post-turn estimator (#117400)

* [AI] fix(compaction): use canonical session context projection for post-turn estimator

* test: focus CLI compaction regression

---------

Co-authored-by: Mason Huang <masonxhuang@icloud.com>
Co-authored-by: Mason Huang <masonxhuang@proton.me>
This commit is contained in:
wangyan2026
2026-08-17 00:00:50 +08:00
committed by GitHub
parent 421e319fb7
commit 394ce62ef7
2 changed files with 121 additions and 13 deletions
+120 -1
View File
@@ -9,6 +9,7 @@ import { SESSION_TOTAL_TOKENS_VERSION, type SessionEntry } from "../../config/se
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ContextEngine } from "../../context-engine/types.js";
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
import { SessionManager } from "../sessions/session-manager.js";
import {
resetCliCompactionTestDeps,
runCliTurnCompactionLifecycle,
@@ -267,7 +268,10 @@ describe("runCliTurnCompactionLifecycle", () => {
return result;
},
}),
deps: { openSessionManager: () => ({ getBranch: () => [] }) as never },
deps: {
openSessionManager: () =>
({ getBranch: () => [], buildSessionContext: () => ({ messages: [] }) }) as never,
},
});
const runLifecycle = (
ok: boolean,
@@ -1222,5 +1226,120 @@ describe("runCliTurnCompactionLifecycle", () => {
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<string, SessionEntry> = { [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. */
+1 -12
View File
@@ -43,7 +43,6 @@ import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-
import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/compaction.js";
import { ensureSelectedAgentHarnessPlugin as ensureSelectedAgentHarnessPluginImpl } from "../harness/runtime-plugin.js";
import { loadAgentRuntimePluginRegistryHandle } from "../runtime-plugins.js";
import type { AgentMessage } from "../runtime/index.js";
import { SessionManager } from "../sessions/session-manager.js";
import {
clearCliSessionInStore as clearCliSessionInStoreImpl,
@@ -177,16 +176,6 @@ function resolvePositiveInteger(value: number | undefined): number | undefined {
return Math.floor(value);
}
function getSessionBranchMessages(sessionManager: SessionManagerLike): AgentMessage[] {
return sessionManager
.getBranch()
.flatMap((entry) =>
entry.type === "message" && typeof entry.message === "object" && entry.message !== null
? [entry.message]
: [],
);
}
function resolveSessionTokenSnapshot(sessionEntry: SessionEntry | undefined): number | undefined {
return resolvePositiveInteger(resolveFreshSessionTotalTokens(sessionEntry));
}
@@ -609,7 +598,7 @@ export async function runCliTurnCompactionLifecycle(params: {
});
const preemptiveCompaction = cliCompactionDeps.shouldPreemptivelyCompactBeforePrompt({
messages: getSessionBranchMessages(sessionManager),
messages: sessionManager.buildSessionContext().messages,
prompt: "",
contextTokenBudget,
reserveTokens: settingsManager.getCompactionReserveTokens(),