Repository navigation
fix(session-flow): re-collect audit-sessions only when collector inputs change #6257
Description
Problem
/session-flow:audit-sessions re-reads every transcript on the machine after almost every
session-flow release, even when nothing that shapes a stored record changed.
Paths are relative to plugins/session-flow/skills/audit-sessions/ unless they start with
plugins/. Line numbers are on origin/main 5779559 (session-flow 0.48.8).
scripts/collect.py:125-130collector_version()returns theversionfield of
plugins/session-flow/.claude-plugin/plugin.json.scripts/collect.py:785builds the skip policy from that version, the excerpt limits and the
redaction fail-closed state, and:816skips a session only when both its fingerprint and that
policy match the stored record. A plugin version change therefore re-ingests every session.- The plugin version moves on every session-flow PR, most of which never touch the collector.
Since audit-sessions shipped at 0.46.0 (feat(session-flow): add audit-sessions with a durable session store and drift guard #5925, cb30935) there have been 16 more version bumps
(0.46.1 to 0.48.8, 2026-10-02 to 2026-10-04). Only 2 of them changed a file the collector runs or
reads: 5f9e04f (lib/state-key.sh) and f334dec (fix(session-flow): classify automated, Desktop and orphaned sessions in audit-sessions #6179:collect.py,census.py,
plugins/session-flow/scripts/transcript_reader.py). Each of the other 14 forces a full re-read
on the next collect for nothing (several bumps between two collects cost one re-read). SKILL.md:71-74tells the agent the first run on a machine is the slow one ("about 70 s for
2.6 GB", give it a 600000 ms Bash timeout or run it in the background) and that "Later runs take
under a second when nothing changed." It mentions the plugin-version re-ingest at:70-71but
gives no timing for it, so an agent following the text runs a post-release collect under the
default Bash timeout.
Evidence
Verified this pass (code read at 5779559; git log -p over plugin.json; git show --stat of
every commit since 0.46.0 that touched a collector input):
- The lines above.
- The collector's inputs:
collect.pyimportscensus,redact(both beside it) and
plugins/session-flow/scripts/transcript_reader.py(collect.py:52-54);redact.py:22loads
vendor/gitleaks/gitleaks-rules.json;collect.py:60runsplugins/session-flow/lib/state-key.sh
for repository identity. Those six files decide what a record holds, together with the excerpt
limits and redaction state already in the policy. - The record already stores
redaction.rules_version(collect.py:730), but the skip policy does
not use it; it relies on the plugin version changing.
Measured during the 2026-10-04 Windows performance audit (Claude Code 2.1.289; not re-run here):
a collect after a session-flow version change re-ingested all 793 transcripts and took 272 s, over
about 7.2 GB of transcripts in 135 project directories.
What the item claimed that this pass corrects: the item framed 272 s as "near the skill's 300 s
recheck trigger". That trigger is for a first run on a machine of similar size to the 2.6 GB
reference, and 272 s over about 7 GB is roughly the documented rate scaled by size, so it is not a
per-GB regression and the trigger did not fire. The defect is how often the full re-read happens,
not how long one takes.
Inferred, not observed: a post-release collect run under the default 120 s Bash timeout on a store
of that size would be killed partway. The audit ran it with a long timeout.
Proposed approach
Recommended: key the skip on a content hash of the collector's inputs instead of the plugin
version.
- In
collect.py, computecollector_digest: SHA-256 over the bytes ofcollect.py,
census.py,redact.py,plugins/session-flow/scripts/transcript_reader.py,
vendor/gitleaks/gitleaks-rules.jsonandplugins/session-flow/lib/state-key.sh, in a fixed
order, with CRLF normalized to LF so a checkout's line-ending setting does not change it. - Use the digest in place of the version in the policy tuple at
:785, and store it as a new
record field (for examplecollector_digest). Keepcollector_versionin the record as
provenance.reference/store-layout.md:74allows new fields within v1, and readers treat a
missing field as null, so a record without the digest is re-ingested once and then skipped. - Update
SKILL.md:70-74andreference/store-layout.md:22-25to say a full re-read happens on
the first run and whenever the collector's code or rules change, and to give that run the same
long timeout or background advice as the first run.
Alternative: a hand-bumped COLLECTOR_VERSION constant in collect.py. Fewer bytes hashed, but it
fails silently when someone changes the reader and forgets the bump, and #6075 is written on the
assumption that any collector change re-ingests without a bump. Not recommended.
Not recommended: keeping the plugin version and only fixing the SKILL.md timing text. It leaves the
full re-read on 14 of 16 releases.
Files: scripts/collect.py, scripts/tests/test_collect.py, SKILL.md,
reference/store-layout.md, plugins/session-flow/CHANGELOG.md,
plugins/session-flow/.claude-plugin/plugin.json.
Acceptance criteria
- A collect after a change to
plugin.jsonalone skips every session whose fingerprint is
unchanged. - A collect after a change to any one of the six hashed inputs re-ingests every session (one
test case per input, or one parametrized test). -
--forcestill re-ingests every session. - A test fails when
collect.pyimports a local module, or opens a file under the plugin, that
is not in the hashed set. - New records carry both
collector_versionand the digest field; a stored record without the
digest is re-ingested once and skipped on the next run. - The
other-collector-versioncase atscripts/tests/test_collect.py:274is replaced by a
digest-mismatch case. -
SKILL.mdnames every condition that triggers a full re-read and gives that run the long
timeout or background advice.
Constraints and gotchas
- Contract shift: fix(session-flow): make audit-sessions numbers, baseline, routes and report trustworthy #6074 and fix(session-flow): measure Stop-hook wait and active time correctly in audit-sessions #6075 both say "No collector-version bump is needed: the plugin.json
version drives re-ingest." With this change that sentence stays true in effect, because fix(session-flow): measure Stop-hook wait and active time correctly in audit-sessions #6075's
edits tocollect.pychange the digest. Say so on fix(session-flow): measure Stop-hook wait and active time correctly in audit-sessions #6075 when this lands so neither PR assumes the
other's mechanism. - Do not redefine existing record keys in place (
reference/store-layout.md:72-77); the store
cannot be rebuilt once Claude Code has swept the transcripts. - Hash file bytes, not
gitstate: the scripts run from the plugin cache, which is not a checkout. - Bump the session-flow version and add a CHANGELOG line, per the repo's release convention.
Context
Part of the 2026-10-04 performance audit, parent #6244.
Source: local handoff item 20261004-152632-claude-perf-audit-tooling-bugs.md, bug 4 (retired into
this issue and a comment on #6077). The threshold half of that bug (hooks.stop_ms flags every
session) is already covered by #6075 (max-based statistic, threshold nulled) and #6077 (re-derive
the baseline, compute the recheck trigger). Related: #6074, #5925, #6179.
Activity
github-actions commentedon Oct 4, 2026
Thanks for the detailed writeup. As I understand it:
audit-sessions' skip policy keys on the session-flow plugin version, so any version bump (most of which don't touch the collector) forces a full re-read of every transcript on the next run, andSKILL.mddoesn't warn about the resulting long runtime for that case. The proposed fix is to key the skip on a content hash of the collector's actual inputs (collect.py,census.py,redact.py,transcript_reader.py, the gitleaks rules file, andstate-key.sh) instead, keeping the version field for provenance only.The report is thorough: it cites exact line numbers, measured timing, acceptance criteria, and a migration note for stored records without the new digest field. Nothing appears missing for someone to act on it. I didn't find a duplicate; it's a distinct follow-up alongside the related #6074, #6075, #6077, and parent #6244.
Automated triage. A maintainer reviews it.