fix(reply): flush memory for pending in-turn auto-compaction even when tokens drop below threshold#87088
fix(reply): flush memory for pending in-turn auto-compaction even when tokens drop below threshold#87088YOMXXX wants to merge 1 commit into
Conversation
…n tokens drop below threshold
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Keep open: current main and the latest release still skip this below-threshold catch-up, but the PR is not merge-ready because the helper branch broadens session-state behavior beyond the stated in-turn path and the supplied proof is still test-only. Canonical path: Close this PR as superseded by #32358. So I’m closing this here and keeping the remaining discussion on #32358. Review detailsBest possible solution: Close this PR as superseded by #32358. Do we have a high-confidence way to reproduce the issue? Yes at the source-helper level: current main returns false below the token threshold before considering an unflushed compaction count. I did not reproduce the full long-running OpenClaw session path in this read-only review, and the PR body says that path was not tested. Is this the best way to solve the issue? No, not as submitted. The helper-level catch-up is plausible, but current caller order makes it affect preflight compaction too, so it needs scoped gating or explicit acceptance of the broader semantics before it is the best fix. Security review: Security review cleared: The diff only changes in-repo TypeScript gate logic and tests, with no dependency, CI, secret, permission, or supply-chain surface added. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 6de357ad4772. |
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
Summary
shouldRunMemoryFlushinsrc/auto-reply/reply/memory-flush.tshonor the explicitcompaction.memoryFlush.enabledcontract for in-turn auto-compaction: when the session entry'scompactionCounthas moved pastmemoryFlushCompactionCount(i.e., there is a compaction that has not yet been memory-flushed), trigger the flush regardless of the current token level. Compaction itself drops tokens back below the soft threshold, so the pure token check would otherwise never fire for in-turn compaction cycles (Bug: in-turn auto-compaction can bypass memoryFlush because memoryFlush only runs pre-turn #62420).hasAlreadyFlushedForCurrentCompaction, so a session that has already flushed for its current compaction count is unaffected — no double-flush.resolveMemoryFlushGateState, which still requires a freshtotalTokenssnapshot (or an explicittokenCountoverride) — stale entries do not trigger a catch-up flush by accident.src/auto-reply/reply/reply-state.test.tscovering: catch-up after in-turn compaction with low post-compaction tokens, first-compaction catch-up without priormemoryFlushCompactionCount, no catch-up when already flushed for the current count, no catch-up whencompactionCount === 0, no catch-up on stale persisted totals, and catch-up honored when a freshtokenCountoverride is supplied.Behavior change to flag
Sessions where
compaction.memoryFlush.enabled = trueand a runtime-triggered auto-compaction fired during a turn now get their memory file written on the nextrunMemoryFlushIfNeededpass (which runs at the next pre-turnrunReplyAgententry), instead of being silently skipped. This restores the documented contract — the samememory/*.mdwrite the user would see for a pre-turn compaction. Sessions that have not had a compaction (or have already flushed for their current compaction count) are completely unaffected: the duplicate guard and thecompactionCount > 0check together preserve the existing semantics for those paths.The catch-up flush still respects every other gate that already lives in
runMemoryFlushIfNeeded—memoryFlush.enabledconfig plan, sandbox write-ability, heartbeat skip, CLI-provider skip — so disabled / heartbeat / CLI sessions remain no-ops.The timing of the catch-up is "next turn's pre-flush": when in-turn auto-compaction happens during turn N, the memory file gets written at the start of turn N+1. The vast majority of OpenClaw sessions are interactive (there is always a turn N+1), so the audit/persistence semantics the issue describes are restored in practice; a session that goes completely idle right after in-turn compaction would still pick up the flush on the next user message. Synchronous post-turn flushing was considered but rejected for this change: it would require wiring a new
runMemoryFlushIfNeededcall into the post-turn block inrunReplyAgent(cross-cutting), while the helper-level catch-up is a single conditional with no broader runtime surface change.Real behavior proof
shouldRunMemoryFlushnow returnstruefor{ totalTokens: 10_000, compactionCount: 1, memoryFlushCompactionCount: 0 }against a 100K context / 5K reserve / 2K soft threshold profile — exactly the in-turn compaction shape from Bug: in-turn auto-compaction can bypass memoryFlush because memoryFlush only runs pre-turn #62420 (compaction reduced tokens below threshold, butmemoryFlush.enabledshould still cover the compaction event).upstream/main; behavior verified through (a) 6 new colocated unit cases inreply-state.test.tsdrivingshouldRunMemoryFlushdirectly, (b) the full existingreply-state.test.tssuite (now 46 cases) including the original 7shouldRunMemoryFlushcases that pin pre-existing behavior, and (c)agent-runner-memory.test.ts(34 cases) — the integration that wrapsshouldRunMemoryFlushviarunMemoryFlushIfNeeded— still passes unchanged. The catch-up triggers from the next turn's existingrunMemoryFlushIfNeededcall atsrc/auto-reply/reply/agent-runner.ts:1352; no new call site is added.pnpm test src/auto-reply/reply/reply-state.test.tspnpm test src/auto-reply/reply/agent-runner-memory.test.tspnpm test:changedpnpm check:changed(totalTokens: 10_000, compactionCount: 1, memoryFlushCompactionCount: 0)→ flush triggers (wasfalsebefore).memoryFlushCompactionCountrecorded → flush triggers.(compactionCount: 3, memoryFlushCompactionCount: 3)→false(no double-flush).(compactionCount: 0)→ falls through to the existing threshold check, unchanged.totalTokensFresh: false) withouttokenCountoverride → stillfalse(we don't blindly flush on stale snapshots).tokenCountoverride (tokenCount: 8_000) on an otherwise stale entry withcompactionCount: 1→ flush triggers.src/auto-reply/reply/reply-state.test.ts: 46 / 46 passed (40 existing + 6 new) in ~1.2 s.src/auto-reply/reply/agent-runner-memory.test.ts: 34 / 34 passed — confirmsrunMemoryFlushIfNeededstill works end-to-end with the modified helper.pnpm test:changed: 7,260+ tests pass across the affected lanes. The only red is the pre-existingsrc/tui/tui-pty-local.e2e.test.ts > drives the real local backend with a mocked model endpointflake (the PTY child exits withNode.js v22.19+ is required (current: v22.14.0)), which reproduces on pristineupstream/mainand has no overlap withsrc/auto-reply/**or any memory-flush path.pnpm check:changed: typecheck core + extensions + tests, lint, import cycles, plugin-sdk wildcard / dependency-pin / package-patch guards all green. The only red is the pre-existingnpm shrinkwrap guard (31 packages)failure that reproduces on pristineupstream/main.memoryFlushCompactionCount = compactionCount - 1persisted state, and the next pre-turn flush writingmemory/*.md. The fix is verified through the helper unit surface that owns the gate; the production setup (Discord thread session,compaction.mode: safeguard,compaction.memoryFlush.enabled: true, real in-turn auto-compaction) requires running OpenClaw against a live agent that hits the per-turn token threshold mid-stream, which is not reproducible from a Mac source checkout. The reporter's transcript-vs-status-card observation in Bug: in-turn auto-compaction can bypass memoryFlush because memoryFlush only runs pre-turn #62420 is the upstream evidence that the runtime path exists.Notes
shouldRunMemoryFlush, a small pure helper. TherunReplyAgentflow is unchanged — no new call site, no signature additions torunMemoryFlushIfNeeded, no new runtime registry surface — so the blast radius is exactly the gate decision.memoryFlush.enabledcontract for that path.runMemoryFlushIfNeededcall intoagent-runner.tsright afterif (autoCompactionCount > 0) { incrementRunCompactionCount(...) }, plus plumbing the visible-error-payloads accumulator a second time. The catch-up-on-next-pre-turn approach achieves the same documented end state for interactive sessions with a one-line change to a pure helper, which is why we picked it.AGENTS.mdandCONTRIBUTING.md. NoCHANGELOG.mdedits (per CONTRIBUTING).Refs #62420