-
-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Description
With HOOK_SESSION_START_FETCH_REMOTE=true and HOOK_STOP_PUSH_REMOTE=true enabled, concurrent Claude sessions in different worktrees still experience notes ref conflicts that require manual resolution via /memory:sync --remote.
Expected Behavior
Auto-sync hooks should prevent conflicts by:
- SessionStart: Fetch and merge latest notes from remote
- SessionStop: Push local notes to remote
In multi-worktree environments, this should keep all worktrees in sync without manual intervention.
Actual Behavior
Notes refs diverged between worktrees, requiring manual sync. After merging code changes in issue-10-observability worktree, refs/notes/mem/* and refs/notes/origin/mem/* had different SHAs across the 4 active worktrees:
$ git worktree list
/Users/.../git-notes-memory c9f358e [main]
/Users/.../worktrees/git-notes-memory/issue-10-observability c8e87ce
/Users/.../worktrees/git-notes-memory/issue-11-subconsciousness eb611ec
/Users/.../worktrees/git-notes-memory/issue-13-multi-domain d22ecbd
$ git for-each-ref refs/notes/ --format='%(refname) %(objectname:short)'
refs/notes/mem/decisions 171da3a
refs/notes/origin/mem/decisions 9bdb4b8 # ← Diverged!Manual sync_with_remote(push=True) was required to resolve.
Investigation Findings
Root Cause
The push_notes_to_remote() method pushes without fetch-merge first, causing conflicts in multi-worktree scenarios where notes refs are shared across all worktrees.
Race condition timeline:
Worktree A Worktree B Worktree C
───────────────────────────────────────────────────────────────
SessionStart: fetch ──►
SessionStart: fetch ──►
Capture memory ──►
Capture memory ──►
SessionStart ──►
SessionStop: PUSH ──► (remote updated)
SessionStop: PUSH ──► CONFLICT! Capture ──►
(local refs stale)
───────────────────────────────────────────────────────────────
Between Worktree A's SessionStart (fetch) and SessionStop (push), Worktree B pushed, making A's local refs stale.
Affected Files
src/git_notes_memory/git_ops.py:1071-1085-push_notes_to_remote()missing fetch-before-pushsrc/git_notes_memory/hooks/stop_handler.py:530- Callspush_notes_to_remote()without pre-push syncsrc/git_notes_memory/hooks/session_start_handler.py:199-214- Fetch only happens at session start
Code Context
File: src/git_notes_memory/git_ops.py:1071-1085
def push_notes_to_remote(self) -> bool:
"""Push all notes to origin.
Pushes local notes to the remote repository. Uses the configured
push refspec (refs/notes/mem/*:refs/notes/mem/*).
Returns:
True if push succeeded, False otherwise.
"""
base = get_git_namespace()
result = self._run_git(
["push", "origin", f"{base}/*:{base}/*"], # ← Direct push, no fetch first
check=False,
)
return result.returncode == 0Issue: Pushes directly without fetching and merging remote changes first.
File: src/git_notes_memory/hooks/stop_handler.py:523-537
if config.stop_push_remote:
cwd = input_data.get("cwd")
if cwd:
try:
from git_notes_memory.git_ops import GitOps
git_ops = GitOps(repo_path=cwd)
if git_ops.push_notes_to_remote(): # ← No pre-push fetch
logger.debug("Pushed notes to remote on session stop")Related Code
A proper sync workflow already exists in the codebase:
File: src/git_notes_memory/git_ops.py:1087-1127
def sync_notes_with_remote(
self,
namespaces: list[str] | None = None,
*,
push: bool = True,
) -> dict[str, bool]:
"""Sync notes with remote using fetch → merge → push workflow.
This is the primary method for synchronizing notes between local
and remote repositories. It:
1. Fetches remote notes to tracking refs
2. Merges tracking refs into local notes using cat_sort_uniq
3. Pushes merged notes back to remote (optional)
...This method implements the correct fetch→merge→push workflow but is not used by the Stop hook.
Worktree Environment
All worktrees share the same refs/notes/mem/* storage location in the main repo's .git/:
$ ls -la /Users/.../git-notes-memory/.git/refs/notes/
drwxr-xr-x 7 staff 224 Dec 25 22:34 mem
drwxr-xr-x 3 staff 96 Dec 25 20:00 originThis is standard git behavior—notes refs are not per-worktree, so concurrent sessions can race.
Suggested Fix
Replace the push_notes_to_remote() call in stop_handler.py:530 with sync_notes_with_remote(push=True):
if config.stop_push_remote:
cwd = input_data.get("cwd")
if cwd:
try:
from git_notes_memory.git_ops import GitOps
git_ops = GitOps(repo_path=cwd)
# Use full sync workflow instead of direct push
results = git_ops.sync_notes_with_remote(push=True)
if any(results.values()):
logger.debug("Synced notes with remote on session stop")
else:
logger.debug("Sync to remote failed (will retry next session)")
except Exception as e:
logger.debug("Remote sync on stop skipped: %s", e)This ensures that before pushing, the Stop hook fetches and merges any remote changes, preventing conflicts.
Environment
- OS: Darwin 24.6.0 (macOS)
- Git Worktrees: 4 concurrent worktrees active
- Auto-sync enabled:
HOOK_SESSION_START_FETCH_REMOTE=true,HOOK_STOP_PUSH_REMOTE=true
Related
- Issue fix: Git notes fetch refspec causes non-fast-forward rejection on diverged notes #18: Fixed git notes fetch refspec and implemented
sync_notes_with_remote()method - The infrastructure to fix this already exists; the Stop hook just needs to use it
Investigated and reported via /claude-spec:report-issue
AI-actionable: This issue contains detailed context for automated resolution