fix(agents): preserve native CLI compaction bindings (#123466)

This commit is contained in:
Peter Steinberger
2026-08-13 21:55:36 -07:00
committed by GitHub
parent d60a98ec87
commit 71ea4e3838
4 changed files with 47 additions and 34 deletions
+9 -6
View File
@@ -391,6 +391,7 @@ describe("runCliTurnCompactionLifecycle", () => {
);
expect(recordCliCompactionInStore).toHaveBeenCalledWith(
expect.objectContaining({
compactionKind: "context-engine",
newSessionId: successorSessionId,
tokensAfter: 100,
}),
@@ -494,12 +495,12 @@ describe("runCliTurnCompactionLifecycle", () => {
});
it.each([
["agent", { agentId: "other" }],
["session key", { sessionKey: "agent:main:other" }],
["store", { storePath: "/tmp/other-openclaw-sessions.sqlite" }],
])("rejects a CLI successor outside the active %s binding", async (_label, override) => {
["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(" ", "-")}`,
suffix: `outside-${label.replace(" ", "-")}`,
tmpDir,
result: ({ sessionKey, storePath }) => ({
ok: true,
@@ -511,7 +512,7 @@ describe("runCliTurnCompactionLifecycle", () => {
sessionId: "outside-successor",
sessionKey,
storePath,
...override,
...buildOverride(),
},
},
}),
@@ -672,6 +673,7 @@ describe("runCliTurnCompactionLifecycle", () => {
expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1);
expect(recordCliCompactionInStore).toHaveBeenCalledWith(
expect.objectContaining({
compactionKind: "native-harness",
provider: "openai",
sessionKey,
tokensAfter: 100,
@@ -924,6 +926,7 @@ describe("runCliTurnCompactionLifecycle", () => {
expect(maintenance).toHaveBeenCalledTimes(1);
expect(recordCliCompactionInStore).toHaveBeenCalledWith(
expect.objectContaining({
compactionKind: "context-engine",
provider: "external-harness",
sessionKey,
tokensAfter: 100,
+9 -12
View File
@@ -646,7 +646,7 @@ export async function runCliTurnCompactionLifecycle(params: {
return params.sessionEntry;
}
let compacted = false;
let compactionKind: "context-engine" | "native-harness" | undefined;
let contextCompactionOutcome: CliTranscriptCompactionOutcome | undefined;
let nativeCompactionResult: EmbeddedAgentCompactResult | undefined;
let useContextEngineCompaction = true;
@@ -695,7 +695,7 @@ export async function runCliTurnCompactionLifecycle(params: {
extraSystemPrompt: params.extraSystemPrompt,
});
if (nativeOutcome.compacted) {
compacted = true;
compactionKind = "native-harness";
nativeCompactionResult = nativeOutcome.result;
useContextEngineCompaction = false;
} else if (nativeOutcome.fallbackToContextEngine) {
@@ -704,9 +704,7 @@ export async function runCliTurnCompactionLifecycle(params: {
nativeFallbackNeedsBindingClear = nativeOutcome.clearCliSessionBinding === true;
} else if (nativeOutcome.failureReason) {
throw new Error(
`CLI native harness compaction failed for ${params.provider}/${params.model}: ${
nativeOutcome.failureReason ?? "compaction did not reduce context"
}`,
`CLI native harness compaction failed for ${params.provider}/${params.model}: ${nativeOutcome.failureReason}`,
);
} else {
useContextEngineCompaction = false;
@@ -749,17 +747,15 @@ export async function runCliTurnCompactionLifecycle(params: {
bestEffortMaintenance: nativeFallbackToContextEngine,
});
contextCompactionOutcome = contextOutcome;
compacted = contextOutcome.compacted;
if (!compacted && contextOutcome.failureReason) {
compactionKind = contextOutcome.compacted ? "context-engine" : undefined;
if (!compactionKind && contextOutcome.failureReason) {
throw new Error(
`CLI transcript compaction failed for ${params.provider}/${params.model}: ${
contextOutcome.failureReason ?? "compaction did not reduce context"
}`,
`CLI transcript compaction failed for ${params.provider}/${params.model}: ${contextOutcome.failureReason}`,
);
}
}
if (nativeFallbackNeedsBindingClear && !compacted && params.sessionStore && params.storePath) {
if (nativeFallbackNeedsBindingClear && !compactionKind && params.sessionStore) {
return (
(await cliCompactionDeps.clearCliSessionInStore({
provider: params.provider,
@@ -771,12 +767,13 @@ export async function runCliTurnCompactionLifecycle(params: {
);
}
if (!compacted || !params.sessionStore || !params.storePath) {
if (!compactionKind || !params.sessionStore) {
return params.sessionEntry;
}
return (
(await cliCompactionDeps.recordCliCompactionInStore({
compactionKind,
provider: params.provider,
sessionKey: params.sessionKey,
sessionStore: params.sessionStore,
+16 -4
View File
@@ -2729,7 +2729,7 @@ describe("updateSessionStoreAfterAgentRun", () => {
});
describe("recordCliCompactionInStore", () => {
it("persists native compaction token counts and clears stale CLI usage breakdown", async () => {
it("persists native compaction token counts without clearing its CLI binding", async () => {
await withTempSessionStore(async ({ storePath }) => {
const sessionKey = "agent:main:explicit:test-record-cli-compaction";
const sessionId = "test-record-cli-compaction-session";
@@ -2775,6 +2775,7 @@ describe("recordCliCompactionInStore", () => {
await seedSessionStore(storePath, sessionStore);
await recordCliCompactionInStore({
compactionKind: "native-harness",
provider: "codex",
sessionKey,
sessionStore,
@@ -2791,11 +2792,17 @@ describe("recordCliCompactionInStore", () => {
expect(sessionStore[sessionKey]?.cacheRead).toBeUndefined();
expect(sessionStore[sessionKey]?.cacheWrite).toBeUndefined();
expect(sessionStore[sessionKey]?.contextBudgetStatus).toBeUndefined();
expect(sessionStore[sessionKey]?.cliSessionBindings?.codex).toBeUndefined();
expect(sessionStore[sessionKey]?.cliSessionIds?.codex).toBeUndefined();
expect(sessionStore[sessionKey]?.cliSessionBindings?.codex).toEqual({
sessionId: "stale-cli-session",
});
expect(sessionStore[sessionKey]?.cliSessionIds?.codex).toBe("stale-cli-session");
expect(persisted[sessionKey]?.totalTokens).toBe(0);
expect(persisted[sessionKey]?.totalTokensFresh).toBe(true);
expect(persisted[sessionKey]?.contextBudgetStatus).toBeUndefined();
expect(persisted[sessionKey]?.cliSessionBindings?.codex).toEqual({
sessionId: "stale-cli-session",
});
expect(persisted[sessionKey]?.cliSessionIds?.codex).toBe("stale-cli-session");
});
});
@@ -2837,6 +2844,7 @@ describe("recordCliCompactionInStore", () => {
await seedSessionStore(storePath, sessionStore);
await recordCliCompactionInStore({
compactionKind: "native-harness",
provider: "codex",
sessionKey,
sessionStore,
@@ -2872,6 +2880,7 @@ describe("recordCliCompactionInStore", () => {
await seedSessionStore(storePath, sessionStore);
await recordCliCompactionInStore({
compactionKind: "native-harness",
provider: "codex",
sessionKey,
sessionStore,
@@ -2890,7 +2899,7 @@ describe("recordCliCompactionInStore", () => {
});
});
it("recreates a complete persisted row when the caller snapshot survived a missing store row", async () => {
it("recreates a complete persisted row and clears its context-engine CLI binding", async () => {
await withTempSessionStore(async ({ storePath }) => {
const sessionKey = "agent:main:explicit:test-record-cli-compaction-missing-row";
const sessionId = "test-record-cli-compaction-missing-row-session";
@@ -2918,6 +2927,7 @@ describe("recordCliCompactionInStore", () => {
};
await recordCliCompactionInStore({
compactionKind: "context-engine",
provider: "codex",
sessionKey,
sessionStore,
@@ -2938,6 +2948,7 @@ describe("recordCliCompactionInStore", () => {
expect(persisted?.compactionCount).toBe(1);
expect(persisted?.totalTokens).toBe(42);
expect(persisted?.cliSessionBindings?.codex).toBeUndefined();
expect(persisted?.cliSessionIds?.codex).toBeUndefined();
});
});
@@ -2956,6 +2967,7 @@ describe("recordCliCompactionInStore", () => {
};
const result = await recordCliCompactionInStore({
compactionKind: "context-engine",
provider: "codex",
sessionKey,
sessionStore,
+13 -12
View File
@@ -494,6 +494,7 @@ export async function persistCliSessionForkSuccessorInStore(params: {
/** Records CLI compaction metadata on the persisted session entry. */
export async function recordCliCompactionInStore(params: {
compactionKind: "context-engine" | "native-harness";
provider: string;
sessionKey: string;
sessionStore: Record<string, SessionEntry>;
@@ -502,19 +503,23 @@ export async function recordCliCompactionInStore(params: {
newSessionId?: string;
expectedSessionId?: string;
}): Promise<SessionEntry | undefined> {
const { provider, sessionKey, sessionStore, storePath, expectedSessionId } = params;
const { compactionKind, provider, sessionKey, sessionStore, storePath, expectedSessionId } =
params;
const entry = sessionStore[sessionKey];
if (!entry) {
return undefined;
}
const next = { ...entry };
clearCliSession(next, provider);
// Context-engine compaction rewrites history outside the CLI process, invalidating its native
// session id. Native harness compaction updates that same session in place, so preserve it.
if (compactionKind === "context-engine") {
clearCliSession(next, provider);
}
next.compactionCount = (entry.compactionCount ?? 0) + 1;
next.updatedAt = Date.now();
const newSessionId = normalizeOptionalString(params.newSessionId);
const sessionIdChanged = Boolean(newSessionId && newSessionId !== entry.sessionId);
if (sessionIdChanged && newSessionId) {
if (newSessionId && newSessionId !== entry.sessionId) {
delete (next as { sessionFile?: unknown }).sessionFile;
next.sessionId = newSessionId;
next.usageFamilyKey = entry.usageFamilyKey ?? sessionKey;
@@ -524,21 +529,17 @@ export async function recordCliCompactionInStore(params: {
}
const tokensAfterCompaction = asNonNegativeFiniteNumber(params.tokensAfter);
next.contextBudgetStatus = undefined;
next.inputTokens = undefined;
next.outputTokens = undefined;
next.cacheRead = undefined;
next.cacheWrite = undefined;
if (tokensAfterCompaction !== undefined) {
next.totalTokens = Math.floor(tokensAfterCompaction);
next.totalTokensFresh = true;
next.totalTokensVersion = SESSION_TOTAL_TOKENS_VERSION;
next.inputTokens = undefined;
next.outputTokens = undefined;
next.cacheRead = undefined;
next.cacheWrite = undefined;
} else {
next.totalTokensFresh = false;
next.totalTokensVersion = undefined;
next.inputTokens = undefined;
next.outputTokens = undefined;
next.cacheRead = undefined;
next.cacheWrite = undefined;
}
const persisted = await patchSessionEntryCore(