Repository navigation
session: an opened chat lets go of the journal lines it replayed - #2045
Merged
Merged
Conversation
AbirAbbas
added a commit
that referenced
this pull request
Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
perf/engine-long-chat-memory
branch
from
October 29, 2026 23:18
09edb35 to
d6a3826
Compare
AbirAbbas
added a commit
that referenced
this pull request
Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Closing unmerged: the Mac push stopped early to save usage. What this fixes and what is left is tracked in #2060. The branch |
AbirAbbas
added a commit
that referenced
this pull request
Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
perf/engine-long-chat-memory
branch
from
November 2, 2026 13:37
d6a3826 to
e7586ae
Compare
The stamp index is keyed by a pointer into each message's content, and a replay remembers a stamp for every message line it reads, including the hundreds of thousands a later compaction marker throws away. So opening a 1,371-entry chat (a 157 MB journal of re-recorded windows) kept 154 MB of their words alive for as long as the chat was open, and a live chat that compacts kept every copy it re-journalled. The stamps are now pruned with the presentation marks, at the open and at every boundary that lets messages go. And a journal too large for the record cache to keep is read into ~300 MB that is garbage once the reading is done, but an idle engine runs no collection to shrink its heap goal, so the footprint stayed at the read's peak for minutes. After such a reading is let go of, one coalesced background debug.FreeOSMemory returns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…read is bounded A rewind's cut now prunes stamps, so the undo carries the tail's instants beside its presentation marks and puts them back. A steer, which the undo re-journals without a fresh stamp, keeps its original time again; ordinary messages are re-stamped by their write, as before. The release after a large read is runtime.GC rather than FreeOSMemory: a collection lowers the heap goal and the runtime's own paced scavenger returns the memory (82 MB 6.5 s after opening the 1,371-entry chat, against 73 MB at 3.8 s with the forced scavenge). It runs at most once per five seconds, with one trailing run, so paging back through a long journal no longer asks for a collection per page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
perf/engine-long-chat-memory
branch
from
November 2, 2026 15:46
e7586ae to
35da513
Compare
AbirAbbas
marked this pull request as ready for review
November 2, 2026 16:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
On a Mac, opening a 1,376-entry chat made the engine hold ~730 MB: codeaf at 792-796 MB, against 66 MB idle, and still 778 MB after two idle minutes. An A/B on the same home showed it is the open itself: 73 MB with the chat listed but not opened, 766-801 MB once it is opened.
Attribution first. A symbolized engine, driven over stdio with no UI, on a 1,371-entry chat made through the real engine (480 turns x 400 tokens, a tool every 10th turn). Its journal is 157 MB: 211k lines, but only 799 unique message contents. Every compaction (315, each "folded 1") re-journals the whole kept window. A forced GC plus
debug.FreeOSMemoryat 15 s splits the footprint:agents.entriesaloneagents.openaloneSo the record cache's per-callback clone is not the holder:
agents.entrieskeeps 7 MB. Two things are:newAgent->openSessionFile->readJournal: 121 MB of decoded strings, plus the stamp index's 13.6 MB, plus 13.5 MB of replayed messages.entrytime.go) is keyed by&message.Content[0]. The replay remembers a stamp for every message line, including the hundreds of thousands a later compaction marker throws away, so each key keeps its message's words alive.retainPresentationLocked). The stamps never were. So a live chat that compacts every turn also kept every copy it re-journalled.What changed (internal/session only; no wire, record or protocol change)
stampIndex.retain, mirroringpresentationIndex.retain, throughsessionFile.retainStamps. The agent prunes its journal stamps with its presentation marks, to the same messages, at the open (after the earlier region is shaped) and inretainPresentationLocked: after a compaction pass, a rewind, a turn fold.releaseAfterLargeRead: after a journal too large for the record cache to keep (recordCacheFileBytes, 16 MiB) has been read and let go of, run aruntime.GCon a background goroutine.releaseCooldown), with one trailing run, and the caller never waits.debug.FreeOSMemory's forced scavenge, which round 1 of the review asked to drop.Contract tests
TestResumeRetainsOnlyLiveStamps: a resumed agent's stamp index holds stamps only for messages it holds.TestResumeHeapDoesNotScaleWithDiscardedWindows: post-GC heap growth from opening a 60-pass journal is no larger than a 6-pass one. Branch: 442,600 B for 6 passes, 430,912 B for 60. Base: 1.09 MB vs 7.44 MB, i.e. the 54 extra windows' 6.3 MB stays retained.TestLiveCompactionAndRewindRetainOnlyLiveStamps,TestStampRetainKeepsQueueTimes,TestResumeStampPrunePreservesShapedEntries: the shaped entries a person sees (ids, kinds, text, times) are unchanged.readrelease_test.go(fake clock): the threshold; one collection for one read; a burst gives one plus at most one trailing; sustained requests (one per 0.9 s for 30 s) give at most ceil(30/5)+1; callers never block during a collection; the release follows the record callback and the resume prune.rewind_test.go: after RewindKeep and UndoRewind on a resumed journal, a steer keeps its journal instant (it lost it before the undo fix).Fail-before: with the two prune calls disabled (today's behaviour), 4 of the new tests fail. For example, "1312 stamps for 33 live identities", and "discarded windows retained 6345392 extra bytes; limit 1492992".
Byte-identical pages. Builds of the merge base and of this branch, each on its own copy of the same home:
agents.open. All 17 answers are byte-identical.agents.open's per-open randommodel_identity.epoch.Gates
-race.GOOS=linuxandGOOS=windows; gofmt clean.Codex review
Manual verification
Pending: the whole app on the s7 home. The byte-identity check above was run against builds of the merge base and this branch.
Numbers
Engine only, the same 1,371-entry chat,
agents.entries+agents.openat once as the desktop sends them:Whole-app s7 and longchat-ab: pending.
Not fixed here, reported
agents.entriesandagents.openparse that journal at the same time is the same root cause. A shared parse between the two would halve it.🤖 Generated with Claude Code