Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions hooks/agent-memory-hooks/agent-memory-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions hooks/agent-memory-hooks/agent-memory-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions hooks/claude-code/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand Down
4 changes: 2 additions & 2 deletions hooks/codex/config.toml.snippet
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions hooks/codex/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand Down
4 changes: 2 additions & 2 deletions hooks/copilot/agent-memory.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
4 changes: 2 additions & 2 deletions hooks/cursor/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
4 changes: 2 additions & 2 deletions hooks/gemini/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand Down
66 changes: 66 additions & 0 deletions tests/hooks-checkpoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down
4 changes: 2 additions & 2 deletions tests/hooks-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
]
}
}
Expand Down Expand Up @@ -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" }
]
}
]
Expand Down
Loading