fix(compaction): ignore stale persisted totalTokens in preflight gate#93749
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Keep open: current main still trusts a raw persisted Canonical path: Close this PR as superseded by #81916. So I’m closing this here and keeping the remaining discussion on #81916. Review detailsBest possible solution: Close this PR as superseded by #81916. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main retains stale Is this the best way to solve the issue? Yes, this is the best narrow fix: it applies the existing freshness contract at the one raw persisted-total consumer without changing Security review: Security review cleared: The diff only changes a compaction gating expression and adds tests; it does not introduce dependency, workflow, secret, permission, or supply-chain changes. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 319e41d0c55e. |
|
Maintainer verification complete after rebasing onto current
|
1ea2eb1 to
8236e99
Compare
Summary
runPreflightCompactionIfNeededdecides whether to compact by taking theMath.maxof three token estimates: the transcript-usage projection, the fresh persisted total, and a raw persisted total (stalePersistedPromptTokens). The first two respect the session freshness flag, but the third readsentry.totalTokensdirectly with nototalTokensFreshcheck:When a compaction completes without a
tokensAfterfigure,incrementCompactionCount(src/auto-reply/reply/session-updates.ts:310) setstotalTokensFresh = falsebut deliberately keeps the old largetotalTokens.resolveFreshSessionTotalTokens(src/config/sessions/types.ts:629) returnsundefinedin that state by design, sofreshProjectedTokenCountcorrectly drops out, but the rawstalePersistedPromptTokensstill feeds theMath.maxgate. The stale large value then overrides the small fresh transcript estimate and force-fires another preflight compaction on a session that is actually small, producing an unnecessary (lossy, user-visible) summarization and a "compacting" stall on the next turn.This is the persisted-total leg of the same symptom tracked in #81178. PR #81916 bounds the transcript-usage leg (
usageProjectedTokenCount/ snapshot tail bytes); it does not touchstalePersistedPromptTokens, so even with #81916 merged the stale persisted total still trips the gate. It is also the opposite direction of the rejected #82919, which wantedresolveFreshSessionTotalTokensto return stale values; this change instead keeps the existing freshness contract and extends it to the one consumer that bypassed it.Fix
The fresh-large path is unaffected: when
totalTokensFreshistrueor absent, the persisted total still counts, so a genuinely large session still compacts.Real behavior proof
Behavior addressed: after a compaction that reports no
tokensAfter(totalTokensFresh = false, largetotalTokensretained), the next preflight check must not force another compaction when the real active transcript is small. A genuinely large fresh session must still compact.Real environment tested: local OpenClaw source runtime at
origin/mainhead617c9d4b7f, Linux, Node 22. A standalonenode --import tsxscript drove the realrunPreflightCompactionIfNeededpath against a real temporary JSONL transcript and session store; the compaction dependency was instrumented only to count whether the runtime attempted compaction.Exact steps or command run after this patch: ran the proof script twice, once with the source fix reverted (
git stash) and once with it applied, each invokingrunPreflightCompactionIfNeededwithtotalTokens=200000, a ~2KB transcript, and a 100000-token context window, for bothtotalTokensFresh: false(stale) andtotalTokensFresh: true(fresh).Evidence after fix (copied terminal output):
Observed result after fix: the stale small session no longer compacts; the fresh large session still compacts.
Regression test:
src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts. Case A (large but stale total, small transcript) fails before the fix and passes after; Case B (large fresh total) confirms the gate still fires. Run withnode scripts/run-vitest.mjs.What was not tested: no full
pnpm check/ build run (memory-constrained box). Scope is one expression in a single decision path; existing colocated suites were run as regression coverage.