Version: v7.1.1
The defect
hooks/MemoryReviewFire.hook.ts keeps the entire memory-review cadence in one file with no session scoping:
LIFEOS/MEMORY/OBSERVABILITY/review-state.json
{
"turn_count_since_last_review": 6,
"last_review_at": "...",
"last_message_at": "...",
"pending_review": false,
"schema_version": 1
}
The hook fires on every primary-session Stop, does a read-modify-write on that file, and decides whether to spawn the reviewer from turn_threshold (default 8) and min_minutes_between (default 30).
With one session this is correct. With N concurrent sessions it fails two ways:
-
The counter is shared. Every session's Stop increments the same turn_count_since_last_review. Eight parallel sessions cross the threshold roughly eight times faster than intended, and the 30-minute gate then throttles all of them together. Curation frequency stops tracking any individual conversation, and a long conversation can go uncurated while a burst of short ones consumes the budget.
-
Read-modify-write is unsynchronised. loadState() reads, the handler mutates, saveState() writes a temp file and renames. The rename is atomic; the read-modify-write around it is not. Concurrent Stop events lose updates.
Why it is separate from #1495
#1495 was about which transcript the reviewer reads. This is about when it runs at all. Fixing the transcript pinning still leaves the cadence global, so a session can be pinned correctly and simply never reach its own threshold because other sessions keep resetting the shared counter.
Repro
- Open two Claude Code sessions in the same install.
watch -n1 cat ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/review-state.json
- Take turns in either session.
turn_count_since_last_review advances on turns from both, and nothing in the file distinguishes them.
Measured on an install that normally runs several sessions in parallel: 88 reviewer fires in 11 days, against a much larger number of sessions per day.
Suggested direction
Scope the cadence per session, keyed on the session_id the harness already passes on stdin at Stop. The codebase already uses exactly this pattern elsewhere: hooks/MemoryTurnStart.hook.ts keeps its injection state at LIFEOS/MEMORY/STATE/memory-inject/<session_id>.json.
Keeping the existing global file as a mirror preserves the statusline and MemoryHealthCheck, both of which read that exact path. I patched my install that way (per-session file drives the decision, global file still written for the readers) and it works, so happy to send a PR if the direction is right.
Note that raising the effective curation rate is the point, but it does raise inference volume: with N active sessions the ceiling goes from one review per min_minutes_between to N. That may warrant a separate global rate cap.
Version: v7.1.1
The defect
hooks/MemoryReviewFire.hook.tskeeps the entire memory-review cadence in one file with no session scoping:LIFEOS/MEMORY/OBSERVABILITY/review-state.json{ "turn_count_since_last_review": 6, "last_review_at": "...", "last_message_at": "...", "pending_review": false, "schema_version": 1 }The hook fires on every primary-session
Stop, does a read-modify-write on that file, and decides whether to spawn the reviewer fromturn_threshold(default 8) andmin_minutes_between(default 30).With one session this is correct. With N concurrent sessions it fails two ways:
The counter is shared. Every session's
Stopincrements the sameturn_count_since_last_review. Eight parallel sessions cross the threshold roughly eight times faster than intended, and the 30-minute gate then throttles all of them together. Curation frequency stops tracking any individual conversation, and a long conversation can go uncurated while a burst of short ones consumes the budget.Read-modify-write is unsynchronised.
loadState()reads, the handler mutates,saveState()writes a temp file and renames. The rename is atomic; the read-modify-write around it is not. ConcurrentStopevents lose updates.Why it is separate from #1495
#1495 was about which transcript the reviewer reads. This is about when it runs at all. Fixing the transcript pinning still leaves the cadence global, so a session can be pinned correctly and simply never reach its own threshold because other sessions keep resetting the shared counter.
Repro
watch -n1 cat ~/.claude/LIFEOS/MEMORY/OBSERVABILITY/review-state.jsonturn_count_since_last_reviewadvances on turns from both, and nothing in the file distinguishes them.Measured on an install that normally runs several sessions in parallel: 88 reviewer fires in 11 days, against a much larger number of sessions per day.
Suggested direction
Scope the cadence per session, keyed on the
session_idthe harness already passes on stdin atStop. The codebase already uses exactly this pattern elsewhere:hooks/MemoryTurnStart.hook.tskeeps its injection state atLIFEOS/MEMORY/STATE/memory-inject/<session_id>.json.Keeping the existing global file as a mirror preserves the statusline and
MemoryHealthCheck, both of which read that exact path. I patched my install that way (per-session file drives the decision, global file still written for the readers) and it works, so happy to send a PR if the direction is right.Note that raising the effective curation rate is the point, but it does raise inference volume: with N active sessions the ceiling goes from one review per
min_minutes_betweento N. That may warrant a separate global rate cap.