fix: conflict-free .ai-memory merges for multi-developer teams (v1.2.0) - #14
Merged
Merged
Conversation
Reported from a multi-developer project: every branch integration conflicted in .ai-memory/ — failures.md, index.md, and the episodic day file both developers had journaled into. Three distinct causes. The merge driver was only half-installed. .gitattributes is committed and shared; the driver command is per-clone by git's own design, so a teammate who clones and merges gets textual conflicts with nothing to explain why. `doctor` now warns when a clone declares the driver but has not registered it, and any `ai-memory init` in such a clone registers the local half automatically. Generated views were merged as if they were authored content. Root maps and the two discovery indexes are rebuilt from memory-store/ on every Dream, but are regenerated wholesale rather than appended to, so they failed the pure-append test and conflicted deterministically. They now union-merge, and the merged file is re-stamped (body_hash recomputed, store_fingerprint blanked) — without that, regenerate_maps reads the merge as a hand edit and folds the union into memory-store/ as a permanent memory. The driver only understood pure appends. Two agents journaling the same day produce separate ## entries plus edits to older ones, which is not a shared prefix. Bodies now merge per ## block; only a block rewritten on both sides still defers to git's textual merge. Adds `ai-memory resolve-conflicts` for teams already sitting in a conflicted merge: it applies the same resolution to the index stages git already recorded, so the merge does not have to be redone. Files needing a human are left untouched, reported, and exit non-zero. Merge-driver tests 11 -> 26. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012DhrD1T9zVpSuDoZgYucZi
Bumps the version in all four synced locations (version.py, README status line, ROADMAP header, server.json) and closes out the CHANGELOG entry. Minor rather than patch: adds the `ai-memory resolve-conflicts` command and a new doctor check, and changes how generated views and store entries merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012DhrD1T9zVpSuDoZgYucZi
Caught by the Windows CI jobs (4 of 12 test jobs — exactly the Windows matrix). `Path.write_text` translates \n to \r\n there, so the temp files handed to `git merge-file` were CRLF, git faithfully returned CRLF, and the merged body was hashed in that form. `parse_frontmatter` normalizes line endings on the way back in, so every merged map on Windows carried a body_hash that could not match its own body — which is exactly the state that makes regenerate_maps read the merge as a hand edit and fold it into memory-store/ as a permanent memory. The re-stamp was silently useless on the platform. Writes the temp files as bytes and normalizes git's output to LF. The regression test reproduces it off Windows by making git's stdout CRLF directly: the CRLF comes from the write, not from the input, since bodies are already normalized by the time they reach the union. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012DhrD1T9zVpSuDoZgYucZi
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012DhrD1T9zVpSuDoZgYucZi
Found while diagnosing this branch's red CI: re-running CI on an unchanged, previously-green main commit failed identically, on every OS and Python version, plus mypy. The cause is upstream, not in the diff. The MCP SDK released 2.0.0, which removes `mcp.server.fastmcp` (FastMCP is replaced by `mcp.server.mcpserver`). The optional dependency was an open `mcp>=1.0.0`, so every fresh install picked up the new major: `from mcp.server.fastmcp import FastMCP` raised, server.py's ImportError fallback set FastMCP to None, and `memory-fabric[mcp]` shipped an MCP server that cannot start. Users installing today get that, not just CI. Pins `mcp>=1.0.0,<2`. Porting to the 2.0 API is its own piece of work; this is the stopgap that keeps installs working. With mcp installed and pinned, the suite is 383 passed and mypy is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012DhrD1T9zVpSuDoZgYucZi
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.
Summary
Reported from a multi-developer project: every branch integration conflicted in
.ai-memory/—failures.md,index.md, and the episodic day file both developers had journaled into.Reproduced that exact conflict set first, then found three separate causes, not one:
.gitattributesis committed and shared, but the driver command lives in per-clonegit config— git's own design, since a repo can't ship executable merge commands. Whoever raninit --merge-driverwas fine; everyone who just cloned got plain textual merges with nothing explaining why.generated: true) and the two discovery indexes are rebuilt frommemory-store/on every Dream, but are regenerated wholesale rather than appended to, so they could never satisfy the driver's pure-append test. Worse, the old code's claim that a conflicted map is "self-healing" was wrong:regenerate_mapscompares the body against the recordedbody_hash, and a merged or conflict-markered body fails that check — so the next Dream would fold the mess intomemory-store/as a permanent memory.##entries plus edits to older ones, which is not a shared prefix.Two further bugs were found by CI while this branch was in review — see Found in review below. One of them (
mcp2.0.0) is unrelated to this work and affects released versions.Related issue
No tracked issue — reported directly.
Changes
body_hashrecomputed,store_fingerprintblanked) so the nextregenerate_mapsrebuilds it from the store instead of mistaking the merge for a hand edit and folding the union intomemory-store/. Union merging delegates togit merge-file --unionfor correct anchoring, with a pure-Python fallback.##-delimited), replacing the pure-prefix-append requirement. Block text is compared with trailing blank lines stripped, so an untouched block doesn't look edited on the branch that appended after it. Only a single block rewritten on both sides — a real disagreement — still defers to git's textual merge.ai-memory resolve-conflicts— resolves the conflicted.ai-memoryfiles of an in-progress merge and stages them, reading the three sides from the index stages git already recorded (:1:/:2:/:3:), so the merge doesn't have to be redone. Files needing a human are left untouched, reported, and make the command exit non-zero.ai-memory doctorwarns when the driver is half-installed, in both directions (declared but not registered; registered but not declared).ai-memory initregisters the driver in a clone that already declares it, closing the teammate gap from the other side.%Pplaceholder so it knows the real pathname being merged. Clones registered before this keep working —index.mdis also recognized from its frontmatter..gitattributesso this repo dogfoods the driver.v1.2.0: minor rather than patch (new command, new doctor check, changed merge behavior).Verified end-to-end on real clones: without the driver the merge conflicts and
resolve-conflictsclears it; with the driver registered the same merge succeeds outright with zero conflicts; a fresh clone gets the doctor warning, and a plaininitin that clone repairs it.Found in review
Windows line endings (mine). The first CI run failed on exactly 4 of 12 test jobs — the Windows matrix.
Path.write_texttranslates\nto\r\nthere, so the temp files handed togit merge-filewere CRLF, git returned CRLF, and the merged body was hashed in that form — whileparse_frontmatternormalizes line endings on read. Every merged map on Windows therefore carried abody_hashthat could not match its own body: precisely the state that makesregenerate_mapsfold the merge into the store. The re-stamp was silently useless on that platform. Fixed by writing the temp files as bytes and normalizing git's output to LF; the regression test reproduces it off Windows by making git's stdout CRLF directly.mcp2.0.0 removedmcp.server.fastmcp(not mine, and not new here). The rest of CI was red on every job including mypy. Re-running CI on an unchanged, previously-greenmaincommit reproduced it identically, which ruled this branch out. The MCP SDK released 2.0.0, which dropsmcp.server.fastmcp— whatserver.pyis built on. The dependency was an openmcp>=1.0.0, so every fresh install resolves to the new major,FastMCPfalls back toNone, andmemory-fabric[mcp]ships an MCP server that cannot start. This affects users on released versions, not just CI. Pinned tomcp>=1.0.0,<2as a stopgap; porting to the 2.0 API is separate work worth its own issue.Testing
pytestpasses (coverage stays at or above the gate)ruff check .andruff format --check .are cleanmypy src/memory_fabricis cleanMerge-driver tests 11 → 27, covering block merges, generated-view union merges, the re-stamp that prevents the fold, the CRLF regression,
resolve-conflicts(resolved / deferred / no-op), driver registration status, and the doctor warning.Locally with
mcpinstalled and pinned: 383 passed, mypy clean.Checklist
CHANGELOG.md(entry closed out as[1.2.0])server.jsonall agree (tests/test_version_truth.py)