Conversation
maybeIndexSessionEvents glob-scanned every `*-events.md` in the shared sessions directory, indexed them all into whichever project's store happened to be open, then unlinked them. On a machine running several projects that means one project's session events land in another project's context, and the rightful project never gets them because the file is already gone. The old comment blamed CLAUDE_PROJECT_DIR being unavailable to MCP servers. That is true of the env var but misleading about the server: `getProjectDir()` is exported from this same file with a full resolution chain, and `getStorePath()` already calls it to hash the content DB path. The events filename already carries the project hash (`<canonicalHash><suffix>-events.md`), so this resolves that one path via `resolveSessionPath` — the project's own generalised resolver that also powers `.db` and `.cleanup` — instead of globbing. Reusing it means the JS hooks and the TS server cannot drift on hash, suffix or migration policy. One existsSync also costs less than the old readdir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four cases, all red against the glob implementation:
- this project's events file is indexed and consumed;
- a sibling project's file is left on disk, byte for byte, and its
content never enters this store;
- a missing file for us is not a reason to take someone else's;
- a second call does not re-consume, and still spares the sibling.
The fixture plants both files through `resolveSessionPath` itself rather
than guessing the hash scheme, and points CONTEXT_MODE_DIR at a temp root
so the run never reads or unlinks the developer's real sessions.
Running the old body against these surfaced a second symptom worth
recording: both files index under the same `session-events` label, and
the store keys sources by label, so the second index REPLACES the first.
With two files pending the store ends on chunkCount 1 — the open
project loses its own continuity and keeps a stranger's events instead.
`maybeIndexSessionEvents` is exported for the test, matching the existing
`__resetSuppressionDiagnosticForTests` idiom in this file.
Co-Authored-By: Claude Opus 5 <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.
Fixes #1214.
The bug
maybeIndexSessionEventsglobs every*-events.mdin the shared sessions directory, indexes all of them into whichever project'sContentStorehappens to be open, and unlinks each one.The sessions directory is shared by every project on the machine, so on a box running more than one repo it holds one pending events file per project that has just started a session. Two losses follow, and neither announces itself:
ctx_searchreturns a stranger's work;While writing the test I found a third symptom I had not expected. Both files index under the same
session-eventslabel, and the store keys sources by label, so the second index replaces the first. With two files pending, the store ends onchunkCount: 1:So it is not only that the open project may get someone else's events. In the usual case it gets only the last file read and loses its own, while every other project's file is deleted from under it.
The fix
The old comment said the glob was necessary because
CLAUDE_PROJECT_DIRis not available to MCP servers. That is true of the env var and not of the server:getProjectDir()is exported from this same file with a full resolution chain, andgetStorePath()already calls it to hash the content DB path.The events filename already carries the project hash (
<canonicalHash><suffix>-events.md), so this resolves that one path throughresolveSessionPath— the generalised resolver insrc/session/db.tsthat also powers.dband.cleanup, and thathooks/session-helpers.mjsimports from the bundle. Reusing it is the point: the JS hooks and the TS server cannot drift on hash, suffix or migration policy. OneexistsSyncalso costs less than the oldreaddirSync.No regex, no truncation, no new dependency.
maybeIndexSessionEventsis exported for the test, matching the existing__resetSuppressionDiagnosticForTestsidiom in this file. Happy to drop that and test throughgetStore()instead if you would rather keep it private.Tests
tests/integration/session-events-project-scope.test.ts, four cases:All four are red against the old body and green against the new one. The fixture plants both files through
resolveSessionPathitself rather than guessing the hash scheme, and pointsCONTEXT_MODE_DIRat a temp root so the run never reads or unlinks a real sessions directory.Live run, per rule 3
A unit case passing is not proof, so I also drove the built bundle over the MCP wire:
node start.mjsspawned as a stdio server with one project's env, real handshake, then actx_searchcall so the productiongetStore()path runs. Fixture is one shared sessions directory with a pending events file for two projects, server started for project A only. Traced throughfs:A indexed and consumed, B never opened, search returns A's content. The same run against the pre-fix bundle deleted both files and returned nothing.
What I did not test
resolveSessionPathhandles is precisely the case-folding path that differs on Linux, and I did not run it there.resolveSessionPathderives the suffix throughgetWorktreeSuffix, and my fixtures were plain directories. The scoped call inherits whatever the hooks compute, which is the reason for reusing their resolver, but I have not exercised a realgit worktreelayout end to end.getStore()through a tool call, not a genuine PreCompact/SessionStart cycle, so whether the restored snapshot is useful is still untested here. This PR only claims the right file reaches the right project.Three pre-existing test failures on my machine are unrelated and reproduce identically on unpatched
next: threehealPartialInstallFromMarketplacesymlink cases and onecache-heal-self-healcase, all needing symlink privileges Windows withholds.One small thing while I was in there, not fixed in this PR: running the suite writes
f0.tmp,f1.tmp,f2.tmpinto the repo root, and neither is gitignored. It also writes into the real~/.clauderather than a sandbox: on this machine the run added aSessionStarthook and anenabledPluginsentry to my globalsettings.json. Happy to open that as a separate issue if it is news.🤖 Generated with Claude Code