feat(mem0-plugin): add PostToolUseFailure, PostCompact, SessionEnd, SubagentStop hooks

Close 4 lifecycle coverage gaps:
- PostToolUseFailure: catch failed mem0 MCP calls, classify errors, inject retry guidance
- PostCompact: inject parallel search commands for context recovery after compaction
- SessionEnd: last-chance transcript capture with dedup against Stop hook
- SubagentStop: remind parent agent to persist subagent learnings

Also patch on_stop.sh to write capture marker for SessionEnd dedup.
This commit is contained in:
kartik-mem0
2026-05-22 01:09:22 +05:30
parent 97a3a2cf8d
commit 93bb3b69c4
6 changed files with 273 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Hook: PostCompact (matcher: manual|auto)
#
# Fires after context compaction completes. Injects a recovery prompt
# telling the agent to reload context from mem0.
#
# Input: JSON on stdin with trigger, messages_retained, messages_removed
# Output: Context injected into Claude's post-compaction context (exit 0)
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "auto"' 2>/dev/null || echo "auto")
RETAINED=$(echo "$INPUT" | jq -r '.messages_retained // "?"' 2>/dev/null || echo "?")
REMOVED=$(echo "$INPUT" | jq -r '.messages_removed // "?"' 2>/dev/null || echo "?")
# Telemetry (background)
python3 "$SCRIPT_DIR/telemetry.py" post_compact --trigger="$TRIGGER" --retained="$RETAINED" --removed="$REMOVED" 2>/dev/null &
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
USER_ID="${MEM0_RESOLVED_USER_ID:-$USER}"
PROJECT_ID="${MEM0_PROJECT_ID:-unknown}"
cat <<EOF
## Mem0 Post-Compaction Recovery
Compaction complete ($TRIGGER). $REMOVED messages removed, $RETAINED retained.
You lost most conversation history. Recover context NOW:
1. \`search_memories(query="session state current task", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$PROJECT_ID"}, {"metadata": {"type": "session_state"}}]})\`
2. \`search_memories(query="recent decisions and learnings", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$PROJECT_ID"}, {"metadata": {"type": "decision"}}]})\`
3. \`search_memories(query="compact summary", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$PROJECT_ID"}, {"metadata": {"type": "compact_summary"}}]})\`
Run all 3 in parallel. Use results to resume work without asking user to repeat context.
EOF
exit 0
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Hook: SessionEnd
#
# Fires when session actually terminates (after Stop).
# Last-chance capture: if on_stop.sh background REST call didn't complete,
# this fires a synchronous capture attempt.
#
# Input: JSON on stdin with session_id, transcript_path, cwd, reason
# Output: ignored (session is ending)
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
REASON=$(echo "$INPUT" | jq -r '.reason // "other"' 2>/dev/null || echo "other")
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
# Telemetry (fire-and-forget — session dying, best-effort)
python3 "$SCRIPT_DIR/telemetry.py" session_end --reason="$REASON" 2>/dev/null &
# Check if on_stop.sh already captured this session (avoid duplicates)
CAPTURE_MARKER="$HOME/.mem0/.captured_${SESSION_ID}"
if [ -n "$SESSION_ID" ] && [ -f "$CAPTURE_MARKER" ]; then
exit 0
fi
# Last-chance transcript capture (synchronous — session is ending anyway)
if [ -n "${MEM0_API_KEY:-}" ]; then
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null || true
# Mark as captured to prevent duplicate if Stop also ran
if [ -n "$SESSION_ID" ]; then
mkdir -p "$HOME/.mem0" 2>/dev/null || true
touch "$CAPTURE_MARKER" 2>/dev/null || true
# Clean up old markers (> 7 days)
find "$HOME/.mem0" -name ".captured_*" -mtime +7 -delete 2>/dev/null || true
fi
fi
exit 0
+7
View File
@@ -63,6 +63,13 @@ Always include `app_id` (the active project_id from SessionStart) as a top-level
EOF
# Capture transcript state in the background via Mem0 REST API
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null &
# Mark session as captured so SessionEnd hook can skip duplicate capture
if [ -n "$SESSION_ID" ]; then
mkdir -p "$HOME/.mem0" 2>/dev/null || true
touch "$HOME/.mem0/.captured_${SESSION_ID}" 2>/dev/null || true
fi
exit 0
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Hook: SubagentStop
#
# Fires when a subagent finishes. Injects a reminder to capture any
# learnings the subagent produced that the parent agent should store.
#
# Input: JSON on stdin with agent_type, result_summary
# Output: Context injected into parent agent's context (exit 0)
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // ""' 2>/dev/null || echo "")
RESULT_SUMMARY=$(echo "$INPUT" | jq -r '.result_summary // ""' 2>/dev/null || echo "")
# Skip short/empty results — nothing worth capturing
if [ ${#RESULT_SUMMARY} -lt 50 ]; then
exit 0
fi
# Skip explorer/plan agents — read-only, rarely produce storable learnings
case "$AGENT_TYPE" in
Explore|Plan)
exit 0
;;
esac
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
cat <<EOF
## Subagent completed: $AGENT_TYPE
Review the subagent result for learnings worth persisting to mem0.
If the subagent discovered something reusable (a fix, pattern, decision, or anti-pattern),
store it via \`add_memory\` with appropriate metadata type.
Only store if genuinely valuable — skip trivial subagent results.
EOF
exit 0
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Hook: PostToolUseFailure (matcher: mcp__mem0__)
#
# Fires when a mem0 MCP tool call fails. Logs the failure, bumps telemetry,
# and injects a retry hint so Claude can recover.
#
# Input: JSON on stdin with tool_name, tool_input, tool_result, is_error
# Output: Context injected into Claude's next response (exit 0)
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_result // ""' 2>/dev/null || echo "")
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null || echo "{}")
# Extract the short tool name (strip mcp__mem0__ prefix)
SHORT_NAME="${TOOL_NAME#mcp__mem0__}"
# Log failure to persistent file for debugging
mkdir -p "$HOME/.mem0" 2>/dev/null || true
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) FAIL $TOOL_NAME: $TOOL_RESULT" >> "$HOME/.mem0/tool-failures.log" 2>/dev/null || true
# Telemetry (background, fire-and-forget)
python3 "$SCRIPT_DIR/telemetry.py" tool_failure --tool="$SHORT_NAME" 2>/dev/null &
# Classify the error
IS_AUTH_ERROR=""
IS_RATE_LIMIT=""
IS_NETWORK_ERROR=""
if echo "$TOOL_RESULT" | grep -qiE '(401|403|unauthorized|forbidden|invalid.*token|invalid.*key)'; then
IS_AUTH_ERROR="true"
elif echo "$TOOL_RESULT" | grep -qiE '(429|rate.?limit|too many requests|quota)'; then
IS_RATE_LIMIT="true"
elif echo "$TOOL_RESULT" | grep -qiE '(timeout|connect|ECONNREFUSED|network|DNS|resolve)'; then
IS_NETWORK_ERROR="true"
fi
cat <<EOF
## mem0 tool failure: \`$SHORT_NAME\`
**Error:** $TOOL_RESULT
**Input:** \`$TOOL_INPUT\`
EOF
if [ -n "$IS_AUTH_ERROR" ]; then
cat <<'EOF'
**Cause:** Authentication failure. MEM0_API_KEY may be invalid or expired.
**Action:** Tell the user their API key needs to be checked. Get a new key at https://app.mem0.ai/dashboard/api-keys
Do NOT retry — it will fail again with the same key.
EOF
elif [ -n "$IS_RATE_LIMIT" ]; then
cat <<'EOF'
**Cause:** Rate limit hit.
**Action:** Wait a few seconds, then retry the same call. If it fails again, reduce the number of parallel mem0 calls.
EOF
elif [ -n "$IS_NETWORK_ERROR" ]; then
cat <<'EOF'
**Cause:** Network connectivity issue reaching mem0 API.
**Action:** Retry once. If it fails again, inform the user and continue without memory context.
EOF
else
cat <<EOF
**Action:** Retry the \`$SHORT_NAME\` call once. If it fails again, continue without memory context and inform the user.
EOF
fi
exit 0