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:
Executable
+48
@@ -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
|
||||
Executable
+44
@@ -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
|
||||
@@ -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
|
||||
|
||||
Executable
+51
@@ -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
|
||||
Executable
+77
@@ -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
|
||||
Reference in New Issue
Block a user