Skip to content

fix(reply): project preflight compaction gate by next-input size on fresh tokens#91488

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
yetval:fix/preflight-fresh-token-undercount
Jun 15, 2026
Merged

fix(reply): project preflight compaction gate by next-input size on fresh tokens#91488
vincentkoc merged 1 commit into
openclaw:mainfrom
yetval:fix/preflight-fresh-token-undercount

Conversation

@yetval

@yetval yetval commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

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-only entry.totalTokens snapshot, 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 main already reaches): this is the token count the gate uses once it runs.

Root cause

entry.totalTokens is, 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" is base + previous output + current prompt estimate, encoded in resolveEffectivePromptTokens (src/auto-reply/reply/agent-runner-memory.ts).

Two paths apply that projection correctly and one did not:

  • The sibling memory-flush gate (runMemoryFlushIfNeeded, same file) projects base + output + estimate, and reads the transcript for output tokens even when persisted tokens are fresh, gated by TRANSCRIPT_OUTPUT_READ_BUFFER_TOKENS when near the threshold.
  • The stale branch of runPreflightCompactionIfNeeded itself projects base + output + estimate.
  • The fresh branch of runPreflightCompactionIfNeeded set transcriptUsageTokens to undefined whenever freshPersistedTokens was a number (so output was never read), and fed the raw prompt-only entry.totalTokens into the threshold check. The promptTokenEstimate was 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 + currentPrompt on 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:

  • Read transcript output tokens in the fresh case when within TRANSCRIPT_OUTPUT_READ_BUFFER_TOKENS of the threshold (freshNeedsOutputRead), mirroring the sibling.
  • Project the fresh persisted base through 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, runPreflightCompactionIfNeeded skips proactive compaction for a session whose totalTokens snapshot 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/main 105d77d, Node 22.19. Drove the production runPreflightCompactionIfNeeded decision path directly with node (tsx loader) for an Anthropic embedded followup run; a fresh SessionEntry (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):

context=1000 reserve=10 soft=0 threshold=990
session totalTokens(prompt-only,fresh)=985  next user prompt=~106 tokens
actual next request input >= 1091 (exceeds threshold 990 by >= 101)
preflight decision => NOT REQUESTED (request sent over budget)

After:

context=1000 reserve=10 soft=0 threshold=990
session totalTokens(prompt-only,fresh)=985  next user prompt=~106 tokens
actual next request input >= 1091 (exceeds threshold 990 by >= 101)
preflight decision => COMPACTION REQUESTED

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 --check on the changed file clean; type-aware oxlint on the changed file clean; tsgo:core exit 0; existing src/auto-reply/reply/agent-runner-memory.test.ts suite green.

Related (not duplicates)

@openclaw-barnacle openclaw-barnacle Bot added size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 8, 2026
@clawsweeper

clawsweeper Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 15, 2026, 10:13 AM ET / 14:13 UTC.

Summary
The PR changes runPreflightCompactionIfNeeded to project fresh session token snapshots with the current prompt estimate and near-threshold transcript-derived output, plus regression tests.

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 totalTokens; the PR body also supplies before/after live output for the same decision path.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] Merging will make fresh-token embedded sessions near the threshold create a proactive compaction checkpoint before the turn instead of reaching overflow-and-retry recovery, so maintainers should accept that session-continuity timing change.
  • [P1] The PR intentionally keeps the existing 8192-token near-threshold transcript-output read policy; broader always-read or wider-buffer behavior remains separate from this branch.

Maintainer options:

  1. Accept Bounded Fresh Projection (recommended)
    Merge after required checks if maintainers agree fresh snapshots should compact based on projected next input instead of waiting for overflow recovery.
  2. Broaden Transcript Output Policy First
    Ask for a follow-up patch before merge if maintainers want the output-read buffer widened or removed as part of this change.

Next step before merge

  • No automated repair is needed; the remaining action is maintainer review of the session-state timing tradeoff and normal merge gates.

Security
Cleared: The diff is limited to runtime compaction logic and adjacent tests, with no dependency, workflow, credential, package, or external code execution changes.

Review details

Best 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 totalTokens; the PR body also supplies before/after live output for the same decision path.

Is this the best way to solve the issue?

Yes, with maintainer acceptance. Reusing resolveEffectivePromptTokens for the fresh preflight branch matches the stale branch and sibling memory-flush projection without changing config or public surfaces.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 66079161d72a.

Label changes

Label justifications:

  • P2: This is a focused bug fix for embedded preflight compaction undercounting with limited blast radius in session memory behavior.
  • merge-risk: 🚨 session-state: Merging changes when long-running sessions proactively compact and can affect checkpoint timing and session continuity.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes copied before/after live terminal output from the production preflight decision path showing the gate changes from not requested to compaction requested after the patch.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied before/after live terminal output from the production preflight decision path showing the gate changes from not requested to compaction requested after the patch.
Evidence reviewed

PR surface:

Source +14, Tests +73. Total +87 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 24 10 +14
Tests 1 74 1 +73
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 98 11 +87

What I checked:

  • Current main fresh path skips transcript usage: On current main, runPreflightCompactionIfNeeded sets transcriptUsageTokens to undefined whenever freshPersistedTokens is a number, so fresh snapshots do not read transcript output before the gate. (src/auto-reply/reply/agent-runner-memory.ts:784, 66079161d72a)
  • Current main projection omits the fresh snapshot path: Current main's projectedTokenCount only considers transcript-derived usage and stale persisted prompt tokens; it does not add prompt/output projection to fresh persisted prompt tokens. (src/auto-reply/reply/agent-runner-memory.ts:827, 66079161d72a)
  • PR projects fresh prompt snapshots: At PR head, the patch hoists the threshold, reads transcript output for fresh snapshots near the threshold, and includes freshProjectedTokenCount in the gate projection. (src/auto-reply/reply/agent-runner-memory.ts:768, 911620328e86)
  • Session token contract supports the root cause: deriveSessionTotalTokens documents that SessionEntry.totalTokens is a prompt/context snapshot and intentionally excludes completion/output tokens. (src/agents/usage.ts:304, 66079161d72a)
  • Sibling memory flush uses the same projection invariant: The sibling memory-flush path reads transcript output near threshold and calls resolveEffectivePromptTokens with prompt snapshot, transcript output, and current prompt estimate. (src/auto-reply/reply/agent-runner-memory.ts:1091, 66079161d72a)
  • Runtime entry point is pre-send: The direct reply runner invokes runPreflightCompactionIfNeeded before the main memory flush and reply execution, so the changed projection affects pre-send compaction timing. (src/auto-reply/reply/agent-runner.ts:1540, 66079161d72a)

Likely related people:

  • steipete: GitHub commit history shows repeated recent work on transcript scan performance, preflight transcript-size reuse, and auto-reply runner internals in the same file family. (role: recent area contributor; confidence: high; commits: 39bc43cb6068, 8d5f6c8ae465, 82e5dd4da74f; files: src/auto-reply/reply/agent-runner-memory.ts, src/auto-reply/reply/memory-flush.ts)
  • ArthurNie: Commit 9d54285b0d4a recently changed required preflight compaction behavior and adjacent tests in the same function this PR adjusts. (role: recent feature contributor; confidence: medium; commits: 9d54285b0d4a; files: src/auto-reply/reply/agent-runner-memory.ts, src/auto-reply/reply/agent-runner-memory.test.ts)
  • jared596: Commit c6d8318d07f5 introduced transcript-estimate based preflight compaction and shouldRunPreflightCompaction, the earlier behavior this PR extends for fresh snapshots. (role: adjacent behavior introducer; confidence: medium; commits: c6d8318d07f5; files: src/auto-reply/reply/agent-runner-memory.ts, src/auto-reply/reply/memory-flush.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels Jun 8, 2026
@yetval
yetval force-pushed the fix/preflight-fresh-token-undercount branch from 6f3efa1 to 303d642 Compare June 8, 2026 19:22
@yetval

yetval commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both rank-up items (force-pushed 303d6426c2b, rebased on latest main):

  1. Previous-output source when transcript output is not read. The fresh projection now uses transcriptOutputTokens ?? freshPersistedOutputTokens, where freshPersistedOutputTokens reads the persisted entry.outputTokens (gated on totalTokensFresh === true, so it is undefined after a compaction snapshot clears it). Outside the 8192-token transcript-read buffer the gate still accounts for the previous turn's completion instead of dropping it.

  2. [P2] Regression added. New test "preflight compacts a fresh session when stored output tokens alone push the next request over budget" (totalTokens: 985, outputTokens: 20, empty prompt, threshold 990): red without the output source (985 < 990), green with it (1005 >= 990). Also added a prompt-estimate-fires case and an under-budget no-fire guard.

Local: node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-memory.test.ts 43/43 pass; git diff --check clean; oxfmt --check clean; tsgo:core exit 0.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@yetval
yetval force-pushed the fix/preflight-fresh-token-undercount branch from 303d642 to 87c4549 Compare June 8, 2026 19:38
@yetval

yetval commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both items (force-pushed 87c4549fef9, rebased on latest main):

  1. Replaced entry.outputTokens with the transcript-derived latest output. entry.outputTokens is the accumulated per-run output (src/auto-reply/reply/session-usage.ts:202-206 stores params.usage?.output, the run total; the adjacent cache comment contrasts it with non-accumulated lastCallUsage), so on a tool-loop turn it overshoots the real last completion and can falsely trip the gate. The fresh projection now counts only transcriptOutputTokens, which comes from readLastNonzeroUsageFromSessionLog (the single last non-zero usage record = the true last-call output), read near the threshold via the same TRANSCRIPT_OUTPUT_READ_BUFFER_TOKENS buffer the memory-flush sibling uses. This keeps the preflight gate at parity with the sibling and drops the accumulated-field shortcut entirely.

  2. [P2] Regression added. New test "does not preflight compact a fresh session when only accumulated output tokens are large and the latest output keeps the request under budget" (totalTokens: 985, outputTokens: 50000 accumulated, empty prompt, threshold 990): asserts no compaction. Green now (accumulated output is not counted, 985 < 990); would be red under the previous entry.outputTokens approach (985 + 50000 -> false trigger). The prompt-estimate-fires regression is retained.

Local: node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-memory.test.ts 42/42 pass; git diff --check clean; oxfmt --check clean; tsgo:core exit 0.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 8, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 8, 2026
…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.
@vincentkoc vincentkoc self-assigned this Jun 15, 2026
@vincentkoc
vincentkoc merged commit caab343 into openclaw:main Jun 15, 2026
170 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 16, 2026
…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.
crh-code pushed a commit to crh-code/openclaw that referenced this pull request Jun 18, 2026
…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.
badgerbees pushed a commit to badgerbees/openclaw that referenced this pull request Jul 8, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants