Skip to content

fix: scope session-event indexing to the current project (#1214) - #1229

Open
ayojeges wants to merge 2 commits into
mksglu:nextfrom
ayojeges:fix/1214-scope-session-events
Open

ayojeges wants to merge 2 commits into
mksglu:nextfrom
ayojeges:fix/1214-scope-session-events

Conversation

@ayojeges

Copy link
Copy Markdown

Fixes #1214.

The bug

maybeIndexSessionEvents globs every *-events.md in the shared sessions directory, indexes all of them into whichever project's ContentStore happens 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:

  1. the open project's store is polluted with another repository's paths and commands, so ctx_search returns a stranger's work;
  2. the rightful project never receives its own events, because the open project already consumed the file. Its next session starts with no continuity and no error.

While writing the test I found a third symptom I had not expected. 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:

labelA: "session-events", labelB: "session-events"
sources: [ { label: "session-events", chunkCount: 1 } ]
alpha: 0     <- first file's content, gone
bravo: 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_DIR is 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, 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 through resolveSessionPath — the generalised resolver in src/session/db.ts that also powers .db and .cleanup, and that hooks/session-helpers.mjs imports from the bundle. Reusing it is the point: the JS hooks and the TS server cannot drift on hash, suffix or migration policy. One existsSync also costs less than the old readdirSync.

No regex, no truncation, no new dependency.

maybeIndexSessionEvents is exported for the test, matching the existing __resetSuppressionDiagnosticForTests idiom in this file. Happy to drop that and test through getStore() instead if you would rather keep it private.

Tests

tests/integration/session-events-project-scope.test.ts, four cases:

  • 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.

All four are red against the old body and green against the new one. 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 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.mjs spawned as a stdio server with one project's env, real handshake, then a ctx_search call so the production getStore() path runs. Fixture is one shared sessions directory with a pending events file for two projects, server started for project A only. Traced through fs:

existsSync  ...\sessions\b6fb7fa122e98b32-events.md -> true      (A, this project)
unlinkSync  ...\sessions\b6fb7fa122e98b32-events.md              (consumed)
final dir   ["c5d8bfbcb66be341-events.md", "stats-pid-22916.json"]   (B untouched)

ctx_search "alphaliveaaa"
  --- [current-session | session-events] ---
  # Session events
  | Bash | alphaliveaaa |

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

  • Windows only. Everything above ran on Windows 11 with Node 22 and bun. Nothing here is platform-specific, but the canonical/legacy hash split that resolveSessionPath handles is precisely the case-folding path that differs on Linux, and I did not run it there.
  • Worktrees. resolveSessionPath derives the suffix through getWorktreeSuffix, 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 real git worktree layout end to end.
  • Real compaction. The live run drives 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: three healPartialInstallFromMarketplace symlink cases and one cache-heal-self-heal case, 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.tmp into the repo root, and neither is gitignored. It also writes into the real ~/.claude rather than a sandbox: on this machine the run added a SessionStart hook and an enabledPlugins entry to my global settings.json. Happy to open that as a separate issue if it is news.

🤖 Generated with Claude Code

ayojeges and others added 2 commits September 30, 2026 06:12
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant