fix(reply): project preflight compaction gate by next-input size on fresh tokens#91488
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 15, 2026, 10:13 AM ET / 14:13 UTC. Summary PR surface: Source +14, Tests +73. Total +87 across 2 files. Reproducibility: yes. Current main's fresh-token branch skips transcript usage whenever a fresh snapshot exists and then falls back to raw prompt-only Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the bounded fresh-token projection fix after required checks and maintainer acceptance, while keeping broader transcript-read and stale-transcript policy in separate PRs. Do we have a high-confidence way to reproduce the issue? Yes. Current main's fresh-token branch skips transcript usage whenever a fresh snapshot exists and then falls back to raw prompt-only Is this the best way to solve the issue? Yes, with maintainer acceptance. Reusing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 66079161d72a. Label changesLabel justifications:
Evidence reviewedPR surface: Source +14, Tests +73. Total +87 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
6f3efa1 to
303d642
Compare
|
Addressed both rank-up items (force-pushed
Local: @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
303d642 to
87c4549
Compare
|
Addressed both items (force-pushed
Local: @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
…tokens The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only entry.totalTokens snapshot straight into the budget threshold check, dropping the current user prompt estimate and the previous turn's output. The sibling memory-flush gate and this function's own stale branch already project base + output + estimate via resolveEffectivePromptTokens, so the preflight gate under-triggered and let over-budget requests through to overflow-retry. Project the fresh persisted base the same way: read transcript output when near the threshold (mirroring the memory-flush gate's buffer) and run the fresh base through resolveEffectivePromptTokens before the threshold check.
87c4549 to
9116203
Compare
…tokens (openclaw#91488) The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only entry.totalTokens snapshot straight into the budget threshold check, dropping the current user prompt estimate and the previous turn's output. The sibling memory-flush gate and this function's own stale branch already project base + output + estimate via resolveEffectivePromptTokens, so the preflight gate under-triggered and let over-budget requests through to overflow-retry. Project the fresh persisted base the same way: read transcript output when near the threshold (mirroring the memory-flush gate's buffer) and run the fresh base through resolveEffectivePromptTokens before the threshold check.
…tokens (openclaw#91488) The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only entry.totalTokens snapshot straight into the budget threshold check, dropping the current user prompt estimate and the previous turn's output. The sibling memory-flush gate and this function's own stale branch already project base + output + estimate via resolveEffectivePromptTokens, so the preflight gate under-triggered and let over-budget requests through to overflow-retry. Project the fresh persisted base the same way: read transcript output when near the threshold (mirroring the memory-flush gate's buffer) and run the fresh base through resolveEffectivePromptTokens before the threshold check.
…tokens (openclaw#91488) The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only entry.totalTokens snapshot straight into the budget threshold check, dropping the current user prompt estimate and the previous turn's output. The sibling memory-flush gate and this function's own stale branch already project base + output + estimate via resolveEffectivePromptTokens, so the preflight gate under-triggered and let over-budget requests through to overflow-retry. Project the fresh persisted base the same way: read transcript output when near the threshold (mirroring the memory-flush gate's buffer) and run the fresh base through resolveEffectivePromptTokens before the threshold check.
Summary
On the embedded runtime, proactive preflight compaction under-triggers on long-lived sessions. The pre-send gate (
runPreflightCompactionIfNeeded) decides whether to compact before the next request, but in the common fresh-tokens path it sizes the next request using only the prompt-onlyentry.totalTokenssnapshot, dropping both the current user prompt and the previous turn's output. As a result the gate skips compaction on a turn whose request actually crosses the context budget, and the session falls into the overflow-and-retry recovery path instead of a clean proactive checkpoint.This is distinct from #63892 / #64384 (the gate not running at all after the first compaction, which current
mainalready reaches): this is the token count the gate uses once it runs.Root cause
entry.totalTokensis, by design, a prompt-only context snapshot that excludes completion/output tokens (src/agents/usage.ts,deriveSessionTotalTokens). The intended projection for "what will the next request weigh" isbase + previous output + current prompt estimate, encoded inresolveEffectivePromptTokens(src/auto-reply/reply/agent-runner-memory.ts).Two paths apply that projection correctly and one did not:
runMemoryFlushIfNeeded, same file) projectsbase + output + estimate, and reads the transcript for output tokens even when persisted tokens are fresh, gated byTRANSCRIPT_OUTPUT_READ_BUFFER_TOKENSwhen near the threshold.runPreflightCompactionIfNeededitself projectsbase + output + estimate.runPreflightCompactionIfNeededsettranscriptUsageTokenstoundefinedwheneverfreshPersistedTokenswas a number (so output was never read), and fed the raw prompt-onlyentry.totalTokensinto the threshold check. ThepromptTokenEstimatewas computed and then discarded in this branch.So with the same threshold formula as the sibling, the preflight gate under-counted the next request by
previousOutput + currentPrompton every fresh turn.Fix
Make the fresh preflight path project the next-input the same way the memory-flush sibling and the stale branch already do:
TRANSCRIPT_OUTPUT_READ_BUFFER_TOKENSof the threshold (freshNeedsOutputRead), mirroring the sibling.resolveEffectivePromptTokens(freshPersistedTokens, transcriptOutputTokens, promptTokenEstimate)and feed that into the gate.The threshold computation is hoisted above the transcript read so the read gate can use it. No config, schema, or public surface changes. Net +14 lines, single function.
Real behavior proof
Behavior addressed: On the embedded runtime,
runPreflightCompactionIfNeededskips proactive compaction for a session whosetotalTokenssnapshot is fresh and just below the budget threshold, even though the request about to be sent (history + current user prompt, plus the previous turn's output) crosses the threshold. The turn is then sent over budget and only recovers via overflow-retry.Real environment tested: Local checkout on a branch off
origin/main105d77d, Node 22.19. Drove the productionrunPreflightCompactionIfNeededdecision path directly withnode(tsx loader) for an Anthropic embedded followup run; a freshSessionEntry(totalTokens: 985,totalTokensFresh: true), context window 1000, reserve 10, soft 0 (threshold 990), and a real user prompt of ~106 tokens. The compaction call was routed to an in-process recorder so the real gate decision is observed without performing an actual compaction. Same script, same inputs, run once before the patch and once after.Exact steps or command run after this patch: ran the production preflight decision path for the fresh-snapshot session described above and printed whether the gate requested compaction, before vs after the change.
Evidence after fix: copied live terminal output from the production preflight decision path.
Before (origin/main, no patch):
After:
Observed result after fix: with the next request projected as base + previous output + current prompt (matching the memory-flush sibling), the fresh-snapshot session now triggers proactive compaction before the over-budget request is sent. A session safely below the threshold (no output/prompt projection pushing it over) is unaffected and still proceeds without compaction.
What was not tested: a live model round-trip over a real provider API (decision path only, no network call); the stale-tokens and transcript-byte-size branches were already projecting correctly and are unchanged.
Verification
Local (Node 22.19.0):
oxfmt --checkon the changed file clean; type-aware oxlint on the changed file clean;tsgo:coreexit 0; existingsrc/auto-reply/reply/agent-runner-memory.test.tssuite green.Related (not duplicates)
mainreaches the threshold check; this PR fixes the count that check consumes.entry.totalTokensprojection this PR fixes (the estimator is not even called when tokens are fresh), so the two are complementary, not duplicates.