diff --git a/CHANGELOG.md b/CHANGELOG.md index e44f5a8..f3bc9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Migration details for `/agent-memory update` live in [`skills/agent-memory/vendo ## [Unreleased] +### Fixed + +- Hooks: `_rebind_session_state_unlocked` preserves `session_binding_host` from `.hook-sync-state` when `AGENT_MEMORY_HOST` is unset; sync harness configs now set `AGENT_MEMORY_HOST` on checkpoint commands (re-run hooks installer to pick up). +- Hooks: `refresh_branch_cache` updates `branch` and clears `session_touched_files` under one lock; fail-open skips both (no path wipe without branch update). + ## [0.1.1] - 2026-07-31 ### Added diff --git a/hooks/agent-memory-hooks/agent-memory-common.sh b/hooks/agent-memory-hooks/agent-memory-common.sh index 9da6460..09ab181 100755 --- a/hooks/agent-memory-hooks/agent-memory-common.sh +++ b/hooks/agent-memory-hooks/agent-memory-common.sh @@ -250,6 +250,9 @@ _rebind_session_state_unlocked() { local host="${AGENT_MEMORY_HOST:-}" local day tmp day=$(_today_ymd) + if [ -z "$host" ]; then + host=$(read_state session_binding_host "") + fi case "$sid" in *$'\n'* | *$'\r'*) printf 'agent-memory: write_session_binding refused newline in sid\n' >&2 @@ -377,13 +380,25 @@ reset_session_state_if_changed() { refresh_branch_cache() { [ -n "${cwd:-}" ] || return 0 - local b last + local b b=$(git -C "$cwd" branch --show-current 2>/dev/null || true) [ -n "$b" ] || return 0 + agent_memory_with_state_lock _refresh_branch_cache_unlocked "$b" +} + +_refresh_branch_cache_unlocked() { + local b=$1 last + if [ "${AGENT_MEMORY_LOCK_ACQUIRED:-0}" != "1" ]; then + printf 'agent-memory: skip branch cache refresh (lock not held)\n' >&2 + return 0 + fi last=$(read_state branch "") - write_state branch "$b" - if [ -n "$last" ] && [ "$last" != "$b" ]; then - _clear_session_path_state + if [ "$last" = "$b" ]; then + return 0 + fi + _write_state_unlocked branch "$b" + if [ -n "$last" ]; then + _write_state_unlocked session_touched_files "" fi } diff --git a/hooks/agent-memory-hooks/agent-memory-sync.sh b/hooks/agent-memory-hooks/agent-memory-sync.sh index 9f2f25e..e7d25af 100755 --- a/hooks/agent-memory-hooks/agent-memory-sync.sh +++ b/hooks/agent-memory-hooks/agent-memory-sync.sh @@ -6,6 +6,9 @@ # # Reads harness stdin JSON when present (session_id, cwd). # Session ID also from AGENT_MEMORY_SESSION_ID env or state. +# Set AGENT_MEMORY_HOST to the harness name when possible (cursor | claude | +# codex | copilot | opencode | gemini); when omitted, rebind preserves +# session_binding_host from .hook-sync-state. # # Set AGENT_MEMORY_EVENT (any host naming): # afterAgentResponse | Stop | agentStop | AfterAgent — end of turn diff --git a/hooks/claude-code/settings.json b/hooks/claude-code/settings.json index cd43f83..8a73c41 100644 --- a/hooks/claude-code/settings.json +++ b/hooks/claude-code/settings.json @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=Stop .claude/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=claude AGENT_MEMORY_EVENT=Stop .claude/hooks/agent-memory-sync.sh", "timeout": 15 } ] @@ -29,7 +29,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=PreCompact .claude/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=claude AGENT_MEMORY_EVENT=PreCompact .claude/hooks/agent-memory-sync.sh", "timeout": 15 } ] diff --git a/hooks/codex/config.toml.snippet b/hooks/codex/config.toml.snippet index 77c1464..4b2adda 100644 --- a/hooks/codex/config.toml.snippet +++ b/hooks/codex/config.toml.snippet @@ -17,14 +17,14 @@ # [[hooks.Stop]] # [[hooks.Stop.hooks]] # type = "command" -# command = ["sh", "-c", "AGENT_MEMORY_EVENT=Stop .codex/hooks/agent-memory-sync.sh"] +# command = ["sh", "-c", "AGENT_MEMORY_HOST=codex AGENT_MEMORY_EVENT=Stop .codex/hooks/agent-memory-sync.sh"] # timeout = 15 # # [[hooks.PreCompact]] # matcher = "auto|manual" # [[hooks.PreCompact.hooks]] # type = "command" -# command = ["sh", "-c", "AGENT_MEMORY_EVENT=PreCompact .codex/hooks/agent-memory-sync.sh"] +# command = ["sh", "-c", "AGENT_MEMORY_HOST=codex AGENT_MEMORY_EVENT=PreCompact .codex/hooks/agent-memory-sync.sh"] # timeout = 15 # # After adding project hooks, run `/hooks` in the Codex TUI to trust them. diff --git a/hooks/codex/hooks.json b/hooks/codex/hooks.json index ebba08b..ecf9430 100644 --- a/hooks/codex/hooks.json +++ b/hooks/codex/hooks.json @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=Stop .codex/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=codex AGENT_MEMORY_EVENT=Stop .codex/hooks/agent-memory-sync.sh", "timeout": 15 } ] @@ -29,7 +29,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=PreCompact .codex/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=codex AGENT_MEMORY_EVENT=PreCompact .codex/hooks/agent-memory-sync.sh", "timeout": 15 } ] diff --git a/hooks/copilot/agent-memory.json b/hooks/copilot/agent-memory.json index e69bf8c..9585a5d 100644 --- a/hooks/copilot/agent-memory.json +++ b/hooks/copilot/agent-memory.json @@ -11,14 +11,14 @@ "agentStop": [ { "type": "command", - "bash": "AGENT_MEMORY_EVENT=agentStop .github/hooks/agent-memory-sync.sh", + "bash": "AGENT_MEMORY_HOST=copilot AGENT_MEMORY_EVENT=agentStop .github/hooks/agent-memory-sync.sh", "timeoutSec": 15 } ], "preCompact": [ { "type": "command", - "bash": "AGENT_MEMORY_EVENT=preCompact .github/hooks/agent-memory-sync.sh", + "bash": "AGENT_MEMORY_HOST=copilot AGENT_MEMORY_EVENT=preCompact .github/hooks/agent-memory-sync.sh", "timeoutSec": 15 } ] diff --git a/hooks/cursor/hooks.json b/hooks/cursor/hooks.json index 83c5c9e..1309469 100644 --- a/hooks/cursor/hooks.json +++ b/hooks/cursor/hooks.json @@ -9,13 +9,13 @@ ], "afterAgentResponse": [ { - "command": "AGENT_MEMORY_EVENT=afterAgentResponse .cursor/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=cursor AGENT_MEMORY_EVENT=afterAgentResponse .cursor/hooks/agent-memory-sync.sh", "timeout": 15 } ], "preCompact": [ { - "command": "AGENT_MEMORY_EVENT=preCompact .cursor/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=cursor AGENT_MEMORY_EVENT=preCompact .cursor/hooks/agent-memory-sync.sh", "timeout": 15 } ] diff --git a/hooks/gemini/settings.json b/hooks/gemini/settings.json index 1fe08d3..e3834f2 100644 --- a/hooks/gemini/settings.json +++ b/hooks/gemini/settings.json @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=AfterAgent .gemini/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=gemini AGENT_MEMORY_EVENT=AfterAgent .gemini/hooks/agent-memory-sync.sh", "timeout": 15000 } ] @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "AGENT_MEMORY_EVENT=PreCompress .gemini/hooks/agent-memory-sync.sh", + "command": "AGENT_MEMORY_HOST=gemini AGENT_MEMORY_EVENT=PreCompress .gemini/hooks/agent-memory-sync.sh", "timeout": 15000 } ] diff --git a/tests/hooks-checkpoint.sh b/tests/hooks-checkpoint.sh index 3506d43..ec530bb 100755 --- a/tests/hooks-checkpoint.sh +++ b/tests/hooks-checkpoint.sh @@ -318,6 +318,72 @@ grep -q 'session_binding=s-new' .agents/memory/.hook-sync-state || ! grep -q 'stale-session.txt' .agents/memory/.hook-sync-state || fail "distinct cursor sessions must clear session_touched_files" +# --- sync without AGENT_MEMORY_HOST preserves session_binding_host --- +printf '%s\n' \ + 'session_binding=s-old-host' \ + 'session_binding_host=cursor' \ + "session_binding_day=$today" \ + 'session_touched_files=host-preserve-test.txt' \ + >.agents/memory/.hook-sync-state +printf '{"session_id":"s-new-host","cwd":"%s"}\n' "$TMP" | + AGENT_MEMORY_PROJECT_DIR="$TMP" \ + AGENT_MEMORY_EVENT=afterAgentResponse AGENT_MEMORY_SESSION_ID=s-new-host \ + ./agent-memory-sync.sh >/dev/null +grep -q 'session_binding=s-new-host' .agents/memory/.hook-sync-state || + fail "sync without host should rebind session" +grep -q 'session_binding_host=cursor' .agents/memory/.hook-sync-state || + fail "sync without AGENT_MEMORY_HOST must preserve session_binding_host from state" +! grep -q 'host-preserve-test.txt' .agents/memory/.hook-sync-state || + fail "distinct session without host must still clear paths" + +# --- explicit AGENT_MEMORY_HOST overrides preserved host --- +printf '%s\n' \ + 'session_binding=s-old-override' \ + 'session_binding_host=cursor' \ + "session_binding_day=$today" \ + 'session_touched_files=override-test.txt' \ + >.agents/memory/.hook-sync-state +printf '{"session_id":"s-new-override","cwd":"%s"}\n' "$TMP" | + AGENT_MEMORY_HOST=opencode AGENT_MEMORY_PROJECT_DIR="$TMP" \ + AGENT_MEMORY_EVENT=Stop AGENT_MEMORY_SESSION_ID=s-new-override \ + ./agent-memory-sync.sh >/dev/null +grep -q 'session_binding_host=opencode' .agents/memory/.hook-sync-state || + fail "explicit AGENT_MEMORY_HOST must override session_binding_host" + +# --- branch cache refresh fail-open skips branch + path clear --- +current_branch=$(git branch --show-current) +fake_branch="stale-branch-name" +printf '%s\n' \ + 'session_binding=s-branch' \ + "branch=$fake_branch" \ + 'session_touched_files=branch-atomic-keep.txt' \ + >.agents/memory/.hook-sync-state +mkdir -p .agents/memory/.hook-sync-state.lock +printf '%s\n' "$$" >.agents/memory/.hook-sync-state.lock/pid +printf '{"session_id":"s-branch","cwd":"%s"}\n' "$TMP" | + AGENT_MEMORY_HOST=cursor AGENT_MEMORY_PROJECT_DIR="$TMP" \ + AGENT_MEMORY_EVENT=afterAgentResponse AGENT_MEMORY_SESSION_ID=s-branch \ + ./agent-memory-sync.sh >/dev/null 2>"$TMP/branch-lock.err" || true +grep -qi 'skip branch cache\|fail-open\|lock busy' "$TMP/branch-lock.err" || + fail "branch refresh under lock contention should log skip/fail-open" +grep -q "branch=$fake_branch" .agents/memory/.hook-sync-state || + fail "fail-open branch refresh must not update branch" +grep -q 'branch-atomic-keep.txt' .agents/memory/.hook-sync-state || + fail "fail-open branch refresh must not clear paths without updating branch" +[[ -d .agents/memory/.hook-sync-state.lock ]] || + fail "branch fail-open must not remove foreign lock" +rm -rf .agents/memory/.hook-sync-state.lock + +# Successful branch refresh after lock release updates branch + clears paths +printf '{"session_id":"s-branch","cwd":"%s"}\n' "$TMP" | + AGENT_MEMORY_HOST=cursor AGENT_MEMORY_PROJECT_DIR="$TMP" \ + AGENT_MEMORY_EVENT=afterAgentResponse AGENT_MEMORY_SESSION_ID=s-branch \ + ./agent-memory-sync.sh >/dev/null +grep -q "branch=$current_branch" .agents/memory/.hook-sync-state || + fail "unlocked branch refresh should update branch to git head" +! grep -q 'branch-atomic-keep.txt' .agents/memory/.hook-sync-state || + fail "branch change must clear session_touched_files" + # --- sync without session id keeps existing binding/paths (e.g. pre-commit) --- printf '%s\n' \ 'session_binding=s-active' \ diff --git a/tests/hooks-merge.sh b/tests/hooks-merge.sh index a5774af..a0cca44 100755 --- a/tests/hooks-merge.sh +++ b/tests/hooks-merge.sh @@ -23,7 +23,7 @@ cat >"$TMP/src.json" <<'EOF' { "command": "AGENT_MEMORY_HOST=cursor .cursor/hooks/agent-memory-session.sh" } ], "afterAgentResponse": [ - { "command": "AGENT_MEMORY_EVENT=afterAgentResponse .cursor/hooks/agent-memory-sync.sh" } + { "command": "AGENT_MEMORY_HOST=cursor AGENT_MEMORY_EVENT=afterAgentResponse .cursor/hooks/agent-memory-sync.sh" } ] } } @@ -77,7 +77,7 @@ cat >"$TMP/src-n.json" <<'EOF' "Stop": [ { "hooks": [ - { "type": "command", "command": "AGENT_MEMORY_EVENT=Stop .claude/hooks/agent-memory-sync.sh" } + { "type": "command", "command": "AGENT_MEMORY_HOST=claude AGENT_MEMORY_EVENT=Stop .claude/hooks/agent-memory-sync.sh" } ] } ]