You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
The two halves of the plugin could disagree about where memory lives.⚠️If your memory looks empty after this update, it is not gone — read the rescue note below. Hooks get $CLAUDE_PLUGIN_DATA injected; model-run scripts (summon.py, memory.py) do not, and derive the data dir from their own install path instead. When those two answers differed, the summon-gate blocked every edit no matter how many times the agent re-summoned (summon.py wrote its marker where the gate never looked), and the memory store quietly forked in two — on the machine that surfaced this, 17 records against 10, zero overlap, diverging for a week before anyone noticed. Three separate defects fed it, all of the same shape: a guess about where the data dir is, returned without ever checking the disk. _glob_data demanded exactly one<plugins>/data/<plugin>-* match and returned None otherwise — so a stale dir left by an earlier install flavor sent every model-side write to the in-place fallback. It now picks the live dir on ambiguity, judged by the newest write across the data root and the state dirs it owns. The data root's own mtime is the wrong signal and would have inverted the choice: POSIX moves it only when a direct entry is added or removed, so a dir in daily use looks untouched since install, while one stray file dropped into a dead dir makes the dead one look fresh. .summon-state/ and memory/ do move on every write, because both writers land their os.replace inside them. _derive_from_cache treated any path segment named cache as a plugin cache root, so a checkout living under one (~/cache/projects/…) derived ~/data/<name> out of thin air and wrote its store to a phantom directory. The layout now has to corroborate itself: the root is named plugins, or it already holds the data/ dir Claude Code creates beside cache/. Going by evidence rather than the name means a renamed root still resolves — and an install already using such a directory keeps it, so nobody's store moves.
The predicted data-dir name was likewise never checked. _sanitize only mirrors how Claude Code names <plugin>-<marketplace>; if that mirror drifts, the prediction points at nothing while the real dir sits beside it. A missing prediction now accepts a neighbour matching once case and punctuation are normalised away — and only such a neighbour, so an unrelated <plugin>-inline leftover is never adopted. With no twin it still returns the prediction rather than falling through to the glob: that is the first-run case, and a lone stale dir would be indistinguishable from a match.
Stray stores are rescued, not stranded. Fixing resolution stops new forks but does not heal an existing one — worse, when the orphaned store sits in the skill root, a plugin update replaces that directory wholesale and takes the store with it. SessionStart now looks for a store outside the live one (the skill root, or a sibling data dir) and copies it to <data_root>/rescued-store-<source>/, reporting what it holds. Copy, never merge: two diverged stores can hold contradictory records, so folding them together is a judgement call for the memory-agent through the memory.py CLI, not something a hook does silently. The live store is never touched, the orphan is left where it is, the shipped seed reads as "no store" so a clean install stays quiet, and the notice repeats until you delete the rescued directory. See Rescued stores in references/memory-protocol.md.
Regression coverage. The plugin flavor had no test suite; it now has two. scripts/test_pluginpaths.py (21 cases) pins resolution across healthy installs, ambiguous globs, cache look-alikes, drifted names and unreadable state dirs — five of them fail against the naive dir-mtime fix. hooks/test_rescue.py (22 cases) covers seed detection, both orphan shapes, copy-not-move, idempotence, and failing soft on corrupt stores. No schema change; the store format is untouched.