fix: resolve the .gitmem root independently of cwd (GIT-91) - #28
Merged
Conversation
Resolution walked up from process.cwd() and adopted any directory containing active-sessions.json or config.json. That makes the root a function of cwd, and the processes sharing a session do not share one: the MCP server runs from wherever the client launched it, the SessionStart hook runs in the repo. So a single logical session bound to two stores, and writes landed in a root that identity resolution never read. Three such roots existed on the machine where this was found. GIT-89 made identity resolve from the durable per-session store rather than the registry index. It did not pin WHICH store, so the same symptom could return through this door. Two designs were recorded on the issue and both were rejected on inspection. Pinning the root into the session needs a machine-level pointer to bootstrap from, which is a new index that can diverge from disk — the GIT-51/GIT-89 failure class rebuilt one level up, with a worse failure mode, since a stale pointer sends every process to the wrong store rather than to no store. Migrating project roots into ~/.gitmem moves a user's memory store, the highest -risk operation available, to fix a bug whose observed instance involves no real data. A third approach was implemented first and abandoned under test: keep the walk-up but require evidence a root is live. It does not hold. Test-written sessions carry a structurally valid session.json (GIT-92), so the repo still qualified — and no cwd-derived rule, however strict, can make two processes with different cwds agree. The dependency on cwd is the defect, not the looseness of the check. So ~/.gitmem is authoritative and the walk-up is gone. Project-scoped roots stay supported but must be named explicitly via GITMEM_DIR. Nothing is moved or deleted: a project root still holding live state is reported once per process, with its path and the one-line fix. Staying silent there would be GIT-93's "Proceed freely" again — a system reporting a clean state while a store it used to read sits unread. isLiveGitmemRoot() is retained for that notice. Presence of a file is not evidence: an empty registry means the opposite of "sessions live here", and fixture directories without a session.json are not sessions. Verified: the same root resolves from the repo, from $HOME and from /tmp, where these previously differed. +10 tests (1191 -> 1201), 4 of which fail against the old walk-up. GIT-89's restart e2e still passes. Three tests in gitmem-dir-multisession.test.ts asserted the walk-up directly and now assert its replacement. Flagging explicitly: they encoded the behaviour this commit removes, so they could not be preserved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The root-resolution fix is correct about the invariant and was wrong about the transition. Before v1.0.10 gitmem stored data in <project>/.gitmem, and the cwd walk-up removed in the previous commit was the backward-compatibility bridge for those installs. Removing it leaves such a store unread — and on the free tier that store IS the memory: learnings.json, threads.json, sessions. Even on Pro, threads never migrate to Supabase. The experience would be "my institutional memory vanished after an upgrade", from the tool whose entire promise is that it does not do that. The previous commit claimed a "loud" warning. It was not: console.error goes to MCP stderr, which is invisible in most clients. That is GIT-93's "Proceed freely" one layer up — a clean-looking state while a store sits unread — so: 1. session_start renders the notice in its display, where the user actually looks, naming the stranded path, the root now being read, and both ways out. 2. `npx gitmem-mcp migrate-root [--dry-run] [--from <path>]` copies a stranded store into the developer-scoped root. It COPIES, never moves, so a wrong call leaves the original intact; it never overwrites, so stale memory cannot clobber current memory; and it reports every skip, because a partial merge silently reported as complete is the failure this whole issue is about. Detection is shared: findStrandedProjectRoots() backs the stderr warning, the session_start notice and the command, so they cannot disagree about what counts as a store worth migrating. Two problems surfaced while building this and are fixed here. The root change redirected the test suite's writes. Tests write real session state through getGitmemDir(); that used to land in <repo>/.gitmem (GIT-92, already wrong) and now resolves ~/.gitmem — the developer's real store. Running the suite created directories in it. vitest.config.ts now hands every worker a throwaway GITMEM_HOME and tests/setup asserts the resolved root is under tmpdir, failing the run otherwise. HOME cannot be used: vitest runs pool "threads", where process.env is a JS-level copy that never reaches native getenv(), so os.homedir() is unaffected from inside a worker. That is also why getHomeGitmemDir() exists — GITMEM_HOME relocates only the fallback, leaving the GITMEM_DIR > cache > home precedence intact, which GITMEM_DIR could not do without outranking setGitmemDir() and funnelling every suite into one directory. migrate-root computed its destination with os.homedir() instead of that resolver, so under a GITMEM_HOME override it wrote into the real ~/.gitmem rather than the configured root. Caught in manual verification, which is the only reason it is not shipping: a migration tool that writes where the product does not read is worse than no tool. Now covered by test. +7 tests (1201 -> 1208). GIT-89's restart e2e still passes. Verified manually: the notice renders in session_start against a seeded pre-1.0.10 layout, and migrate-root copies into the configured root while leaving both the source and an existing destination learnings.json untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R18 as amended: 1.8.0, not 2.0.0. The only cohort whose memory resolves
differently is pre-Feb-15 installs, and that population is effectively zero —
gitmem had no meaningful adoption before v1.0.10 moved the default to
~/.gitmem. A major bump would tap the shoulder of users who do not exist while
alarming the ones who do.
Release notes follow the ruled framing. "Breaking" is retired from them; the
lead sentence is "No one loses any information", and a signpost paragraph
carries the rest — where a project-local store is, what it holds, and the one
command that copies it in. That is the honest description: nothing is deleted,
moved, or overwritten, and for almost everyone nothing changes at all.
Completes R18's outstanding acceptance criterion. Detection-without-use only
works if the detection says something a user can weigh, so the notice now
states what the stranded store HOLDS, not merely that one exists:
Memory store found but NOT being read
/path/to/repo/.gitmem
holds: 142 learnings, 6 threads, 2 sessions
gitmem reads ~/.gitmem regardless of directory (1.8.0).
Nothing was moved or deleted. Copy it in:
npx gitmem-mcp migrate-root --dry-run
Or keep using that store: set GITMEM_DIR=<path>
describeGitmemRoot() counts learnings, threads and sessions, handling both the
bare-array and {key: array} file shapes, and counting only session directories
that actually contain a session.json. Counts are best-effort by design: a
malformed file yields 0 rather than throwing, because a notice that fails to
render because one file is corrupt would reintroduce the silence detection
exists to prevent.
+5 tests (1208 -> 1213), including the fixture R18 named. Verified end to end
against a seeded pre-1.0.10 layout: correct counts, path, and command.
No tag. The release word is Chris's.
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 GIT-91.
The defect
Resolution walked up from
process.cwd()and adopted any directory containingactive-sessions.jsonorconfig.json. That makes the root a function of cwd — and the processes sharing a session don't share one. The MCP server runs from wherever the client launched it; the SessionStart hook runs in the repo.So one logical session bound to two stores, and writes landed in a root that identity resolution never read. Three such roots existed on the machine where this was found.
GIT-89 made identity resolve from the durable per-session store instead of the registry index. It never pinned which store, so the same symptom could return through this door.
Two recorded designs, both rejected on inspection
Pin the root + machine-level pointer — the pointer is a new index that can diverge from disk. That's the GIT-51/GIT-89 failure class rebuilt one level up, with a worse failure mode: a stale pointer sends every process to the wrong store rather than to no store.
Migrate project roots into
~/.gitmem— moves a user's memory store, the highest-risk operation available, to fix a bug whose observed instance involves no real data.A third approach was implemented, then abandoned under test
Keep the walk-up but require evidence a root is live. It doesn't hold:
session.json(GIT-92), so the repo still qualifiedThe dependency on cwd is the defect, not the looseness of the check. That's what the earlier commit in this branch's history got wrong, and testing it against the real machine is what surfaced it.
What landed
~/.gitmemis authoritative; the walk-up is gone. Project-scoped roots stay supported but must be named viaGITMEM_DIR.Nothing is moved or deleted. A project root still holding live state is reported once per process, with its path and the fix:
Silence there would be GIT-93's "Proceed freely" again — reporting a clean state while a store it used to read sits unread.
Verification
<repo><repo>/.gitmem~/.gitmem$HOME~/.gitmem~/.gitmem/tmp~/.gitmem~/.gitmem+10 tests (1191 → 1201), 4 of which fail against the old walk-up. GIT-89's restart e2e still passes (9/9).
Needs review
Three tests in
gitmem-dir-multisession.test.tsasserted the walk-up directly and now assert its replacement. They encoded the behaviour this PR removes, so they could not be preserved — worth a second pair of eyes.🤖 Generated with Claude Code