fix(compaction): apply host transcript byte guard to codex sessions (#115192) (#115514)

Co-authored-by: pcpilot-dev <pcpilot-dev@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-07-29 00:23:44 -04:00
committed by GitHub
parent 83a3975eca
commit 4e651a43f4
8 changed files with 140 additions and 22 deletions
@@ -4352,7 +4352,7 @@ describe("createTelegramBot", () => {
sendMessageSpy.mockClear();
dispatchReplyWithBufferedBlockDispatcher.mockClear();
replySpy.mockResolvedValue({
text: "⚙️ Compaction skipped: already_compacted_recently • ctx 0%",
text: "⚙️ Compaction skipped: already_compacted • ctx 0%",
});
loadConfig.mockReturnValue({
@@ -43,6 +43,10 @@ describe("classifyCompactionReason", () => {
expect(classifyCompactionReason("already under target")).toBe("below_threshold");
});
it('classifies "already compacted" without implying recency', () => {
expect(classifyCompactionReason("already compacted")).toBe("already_compacted");
});
it("classifies deferred background maintenance as a skip-like reason", () => {
expect(classifyCompactionReason("deferred to background context-engine maintenance")).toBe(
"deferred_background",
@@ -63,7 +67,7 @@ describe("classifyCompactionReason", () => {
});
describe("isBenignCompactionSkipReason", () => {
it.each(["already under target", "already compacted recently"])(
it.each(["already under target", "already compacted"])(
"keeps the established %s skip contract",
(reason) => {
expect(isBenignCompactionSkipReason(reason)).toBe(true);
@@ -40,7 +40,7 @@ export function classifyCompactionReason(reason?: string): string {
return "below_threshold";
}
if (text.includes("already compacted") || text.includes("already_compacted")) {
return "already_compacted_recently";
return "already_compacted";
}
if (text.includes("deferred to background")) {
return "deferred_background";
@@ -79,7 +79,7 @@ export function classifyCompactionReason(reason?: string): string {
/** Return whether a classified reason represents an intentional compaction no-op. */
export function isBenignCompactionSkipReason(reason?: string): boolean {
const classification = classifyCompactionReason(reason);
return classification === "below_threshold" || classification === "already_compacted_recently";
return classification === "below_threshold" || classification === "already_compacted";
}
/** Return whether a compaction result is an intentional no-op rather than a failure. */
@@ -3890,20 +3890,21 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
});
});
it("does not fire after_compaction when compaction fails", async () => {
it("does not fire after_compaction when the session is already compacted", async () => {
hookRunner.hasHooks.mockReturnValue(true);
const sync = vi.fn(async () => {});
getMemorySearchManagerMock.mockResolvedValue({ manager: { sync } });
contextEngineCompactMock.mockResolvedValue({
ok: false,
compacted: false,
reason: "nothing to compact",
reason: "already_compacted",
result: undefined,
});
const result = await compactEmbeddedAgentSession(wrappedCompactionArgs());
expect(result.ok).toBe(false);
expect(result.reason).toBe("already_compacted");
expect(hookRunner.runBeforeCompaction).toHaveBeenCalledTimes(1);
expect(hookRunner.runAfterCompaction).not.toHaveBeenCalled();
expect(sync).not.toHaveBeenCalled();
@@ -2664,7 +2664,117 @@ describe("runMemoryFlushIfNeeded", () => {
expect(compactCall.sessionFile).toContain("large-session.jsonl");
});
it("skips OpenClaw maintenance when model policy routes a SQLite session to native-compacting claude-cli", async () => {
it("byte-guards a Codex runtime session through SQLite semantic compaction", async () => {
const storePath = path.join(rootDir, "sqlite-codex-byte-guard.json");
const sessionKey = "agent:main:main";
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
await replaceSqliteTranscriptEvents(scope, [
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
]);
expect(readTranscriptStatsSync(scope).sizeBytes).toBeGreaterThan(10);
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 10,
totalTokensFresh: true,
compactionCount: 0,
agentRuntimeOverride: "codex",
agentHarnessId: "openclaw",
};
const sessionStore = { [sessionKey]: sessionEntry };
const replyOperation = createReplyOperation();
const entry = await runPreflightCompactionIfNeeded({
cfg: {
agents: {
defaults: {
compaction: { maxActiveTranscriptBytes: "10b" },
},
},
},
followupRun: createTestFollowupRun({
provider: "openai",
model: "gpt-5.5",
sessionId: "session",
sessionKey,
}),
defaultModel: "gpt-5.5",
agentCfgContextTokens: 1_000_000,
sessionEntry,
sessionStore,
sessionKey,
storePath,
isHeartbeat: false,
replyOperation,
});
expect(entry?.compactionCount).toBe(1);
expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting");
expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({
agentHarnessId: "openclaw",
deferOwningContextEngineCompaction: false,
preflightCompactionTrigger: "transcript_bytes",
preflightRequired: true,
sessionId: "session",
sessionKey,
trigger: "budget",
});
});
it("leaves an under-limit SQLite Codex session to native token compaction", async () => {
const storePath = path.join(rootDir, "sqlite-codex-under-byte-guard.json");
const sessionKey = "agent:main:main";
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
await replaceSqliteTranscriptEvents(scope, [
{ message: { role: "user", content: "small" }, type: "message" },
]);
expect(readTranscriptStatsSync(scope).sizeBytes).toBeLessThan(10 * 1024);
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 347_000,
totalTokensFresh: true,
compactionCount: 0,
agentRuntimeOverride: "codex",
agentHarnessId: "openclaw",
};
const replyOperation = createReplyOperation();
const entry = await runPreflightCompactionIfNeeded({
cfg: {
agents: {
defaults: {
compaction: { maxActiveTranscriptBytes: "10kb" },
},
},
},
followupRun: createTestFollowupRun({
provider: "openai",
model: "gpt-5.5",
sessionId: "session",
sessionKey,
}),
defaultModel: "gpt-5.5",
agentCfgContextTokens: 350_000,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
isHeartbeat: false,
replyOperation,
});
expect(entry).toBe(sessionEntry);
expect(replyOperation.setPhase).not.toHaveBeenCalled();
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
});
it("keeps ownsNativeCompaction absolute over the SQLite transcript byte guard", async () => {
registerClaudeCliBackend(true);
registerMemoryFlushPlanResolverForTest(() => ({
softThresholdTokens: 4_000,
+13 -10
View File
@@ -796,15 +796,7 @@ export async function runPreflightCompactionIfNeeded(params: {
if (params.isHeartbeat || isCli || ownsNativeCompaction) {
return entry ?? params.sessionEntry;
}
if (normalizeLowercaseStringOrEmpty(runtimeId) === "codex") {
// Codex runtime sessions should reach Codex with their real thread state.
// Its harness owns automatic compaction; OpenClaw preflight compaction is
// only for non-Codex embedded runtimes.
logVerbose(
`preflightCompaction skipped: sessionKey=${params.sessionKey} runtime=codex reason=codex_native_auto_compaction`,
);
return entry ?? params.sessionEntry;
}
const isCodexRuntime = normalizeLowercaseStringOrEmpty(runtimeId) === "codex";
const compactionSessionKey = params.sessionKey ?? params.followupRun.run.sessionKey;
if (!compactionSessionKey) {
@@ -866,7 +858,7 @@ export async function runPreflightCompactionIfNeeded(params: {
const maxActiveTranscriptBytes = resolveMaxActiveTranscriptBytes(params.cfg);
const shouldCheckActiveTranscriptBytes = typeof maxActiveTranscriptBytes === "number";
const transcriptUsageTokens =
typeof freshPersistedTokens === "number" && !freshNeedsOutputRead
isCodexRuntime || (typeof freshPersistedTokens === "number" && !freshNeedsOutputRead)
? undefined
: await estimatePromptTokensFromSessionTranscript({
agentId: compactionAgentId,
@@ -893,6 +885,17 @@ export async function runPreflightCompactionIfNeeded(params: {
typeof activeTranscriptBytes === "number" &&
typeof maxActiveTranscriptBytes === "number" &&
activeTranscriptBytes >= maxActiveTranscriptBytes;
if (isCodexRuntime && !shouldCompactByTranscriptBytes) {
// Codex owns native-thread token pressure; OpenClaw owns the host transcript byte fuse
// that bounds fresh-thread bootstrap seeds.
logVerbose(
`preflightCompaction skipped: sessionKey=${params.sessionKey} runtime=codex ` +
`reason=codex_native_auto_compaction ` +
`activeTranscriptBytes=${activeTranscriptBytes ?? "undefined"} ` +
`maxActiveTranscriptBytes=${maxActiveTranscriptBytes ?? "undefined"}`,
);
return entry ?? params.sessionEntry;
}
const stalePersistedPromptTokens =
hasPersistedTotalTokens && entry.totalTokensFresh !== false
? Math.floor(persistedTotalTokens)
@@ -343,11 +343,11 @@ describe("handleCompactCommand", () => {
expect(vi.mocked(incrementCompactionCount)).not.toHaveBeenCalled();
});
it("treats already_compacted_recently manual compaction as skipped", async () => {
it("treats already_compacted manual compaction as skipped", async () => {
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
ok: false,
compacted: false,
reason: "already_compacted_recently",
reason: "already_compacted",
});
const result = await handleCompactCommand(
@@ -365,7 +365,7 @@ describe("handleCompactCommand", () => {
);
expect(result?.reply?.text).toBe(
"⚙️ Compaction skipped: session was already compacted recently • Context 12.1k",
"⚙️ Compaction skipped: session is already compacted • Context 12.1k",
);
expect(result?.reply?.isStatusNotice).toBe(true);
});
+2 -2
View File
@@ -73,8 +73,8 @@ function formatCompactionReason(reason?: string): string | undefined {
return lower.includes("already under target")
? "context is already under the compaction target"
: "context is below the compaction threshold";
case "already_compacted_recently":
return "session was already compacted recently";
case "already_compacted":
return "session is already compacted";
default:
return text;
}