Repository navigation
Bound the session-history reads in preference tracking - #305
Merged
Merged
Conversation
A stack sampler on a real pipeline run found where the pipeline's time was going. 106 seconds of a 184-second run were inside _collect_session_texts, which read every .md/.txt/.json/.log file under every session directory, fully into memory, with no bound of any kind. The directory is ~/.claude/memory/logs/sessions. On the machine that found this it held 4.4 GB across 1,658 session directories -- 11,050 matching text files, read in full, on every run. The engine writes a session directory per run, so each run made the next one slower, permanently and without limit. One run had already been pushed past the entry point's 300-second orchestration cap. What the reading is for sets the bound, and it is a modest purpose for 4.4 GB of I/O: track_preferences lowercases the text and asks whether known skill and agent names appear in it, keeping anything seen in at least MIN_OCCURRENCES sessions. That is a frequency heuristic over recent behaviour. The hundred-and-fifty-first megabyte of one log cannot establish a preference that three separate sessions did not, and a session from six months ago is not evidence about what the user prefers now. So: the newest MAX_SESSIONS_SCANNED directories by mtime, and at most MAX_FILE_BYTES per file. Sorted by mtime rather than by name because session directory names are UUID-bearing and do not sort chronologically -- sorting by name would have dropped an arbitrary set rather than the oldest. Measured end to end on the same machine and task: 174s -> 51s. The collection step itself went 106s -> 1.0s. In that run the recency cap did all the work and the per-file cap never engaged, which is the expected shape: the bulk is many sessions, not one enormous file. The per-file cap is there for the other shape. The cap reports itself on stderr -- "skipped 1633 older session(s), truncated 0 file(s)". This repository's rule is that a cap must not be silent, and a sampled history that does not say it was sampled reads as a complete one. stderr rather than a logger because this module is loaded by path as a standalone script. Ten tests, each bound paired with a case proving the bound is what makes the difference: without the cap all 20 fixture sessions are read, with it 5 are, and the three dropped are the oldest by mtime. They assert on HOW MUCH is read, not on what comes back, because the function's output was always correct -- correctness was never the problem, and no correctness test could have seen this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
A stack sampler on a real pipeline run found the "ghost gap" — a 90–140 second stretch with zero logging that had been showing up between session tagging and the first checkpoint.
_collect_session_textsread every.md/.txt/.json/.logfile under every session directory, fully into memory, with no bound of any kind.The directory is
~/.claude/memory/logs/sessions. On the machine that found this:The engine writes a session directory per run, so each run made the next one slower, permanently and without limit. One run had already been pushed past the entry point's 300-second orchestration cap.
The bound comes from what the reading is for
track_preferenceslowercases the text and asks whether known skill and agent names appear in it, keeping anything seen in at leastMIN_OCCURRENCES(3) sessions. That is a frequency heuristic over recent behaviour — a modest purpose for 4.4 GB of I/O.The hundred-and-fifty-first megabyte of one log cannot establish a preference that three separate sessions did not, and a session from six months ago is not evidence about what the user prefers now.
So: the newest
MAX_SESSIONS_SCANNED(25) directories by mtime, and at mostMAX_FILE_BYTES(1 MB) per file. Sorted by mtime rather than by name because session directory names are UUID-bearing and do not sort chronologically — sorting by name would have dropped an arbitrary set rather than the oldest.Measured, same machine and task
In that run the recency cap did all the work and the per-file cap never engaged — the expected shape, since the bulk is many sessions rather than one enormous file. The per-file cap is there for the other shape.
The cap reports itself
This repository's rule is that a cap must not be silent: a sampled history that does not say it was sampled reads as a complete one. On stderr rather than through a logger because this module is loaded by path as a standalone script and has none.
Tests
Ten, each bound paired with a case proving the bound is what makes the difference — without the cap all 20 fixture sessions are read, with it 5 are, and the ones dropped are the oldest by mtime. A second pair proves the notice is not unconditional.
They assert on how much is read, not on what comes back. The function's output was always correct; correctness was never the problem, and no correctness test could have seen this.
A correction to my own earlier reports
I twice attributed this to the wrong place before measuring. First to
session_loader— right neighbourhood, wrong mechanism, since that module scans nothing. Then to~/.claude/logs/sessionsat 684 MB / 47 directories — the wrong directory entirely; the scanned one is~/.claude/memory/logs/sessionsat 4.4 GB / 1,658. Both guesses came before the sampler. The sampler took one run.🤖 Generated with Claude Code