Skip to content

fix: resolve the .gitmem root independently of cwd (GIT-91) - #28

Merged
nTEG-dev merged 3 commits into
mainfrom
bugfix/GIT-91-gitmem-root-resolution
Aug 9, 2026
Merged

fix: resolve the .gitmem root independently of cwd (GIT-91)#28
nTEG-dev merged 3 commits into
mainfrom
bugfix/GIT-91-gitmem-root-resolution

Conversation

@nTEG-dev

@nTEG-dev nTEG-dev commented Aug 9, 2026

Copy link
Copy Markdown
Member

Fixes GIT-91.

The defect

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 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:

  • Test-written sessions carry a structurally valid session.json (GIT-92), so the repo still qualified
  • More fundamentally, no cwd-derived rule can make two processes with different cwds agree

The 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

~/.gitmem is authoritative; the walk-up is gone. Project-scoped roots stay supported but must be named 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 fix:

[gitmem-dir] Project-scoped .gitmem found with live state, NOT being used:
<repo>/.gitmem, <parent>/.gitmem. gitmem now resolves ~/.gitmem regardless of
cwd, so every process in a session agrees on one store (GIT-91). To use a
project-scoped root, set GITMEM_DIR=<path> explicitly. Nothing has been moved
or deleted.

Silence there would be GIT-93's "Proceed freely" again — reporting a clean state while a store it used to read sits unread.

Verification

cwd before after
<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.ts asserted 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

Claude and others added 3 commits August 8, 2026 23:18
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>
@nTEG-dev
nTEG-dev merged commit 536cd4f into main Aug 9, 2026
4 checks passed
@nTEG-dev
nTEG-dev deleted the bugfix/GIT-91-gitmem-root-resolution branch August 9, 2026 12:33
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