From 93bb3b69c494480217cb51109c1e36525b45e5ce Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Fri, 22 May 2026 01:09:22 +0530 Subject: [PATCH] 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. --- mem0-plugin/hooks/hooks.json | 46 +++++++++++++++ mem0-plugin/scripts/on_post_compact.sh | 48 +++++++++++++++ mem0-plugin/scripts/on_session_end.sh | 44 ++++++++++++++ mem0-plugin/scripts/on_stop.sh | 7 +++ mem0-plugin/scripts/on_subagent_stop.sh | 51 ++++++++++++++++ mem0-plugin/scripts/on_tool_failure.sh | 77 +++++++++++++++++++++++++ 6 files changed, 273 insertions(+) create mode 100755 mem0-plugin/scripts/on_post_compact.sh create mode 100755 mem0-plugin/scripts/on_session_end.sh create mode 100755 mem0-plugin/scripts/on_subagent_stop.sh create mode 100755 mem0-plugin/scripts/on_tool_failure.sh diff --git a/mem0-plugin/hooks/hooks.json b/mem0-plugin/hooks/hooks.json index 992dd80c0..a1c6e29af 100644 --- a/mem0-plugin/hooks/hooks.json +++ b/mem0-plugin/hooks/hooks.json @@ -132,6 +132,52 @@ } ] } + ], + "PostToolUseFailure": [ + { + "matcher": "mcp__mem0__", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_tool_failure.sh", + "timeout": 5 + } + ] + } + ], + "PostCompact": [ + { + "matcher": "manual|auto", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_post_compact.sh", + "timeout": 10 + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_session_end.sh", + "timeout": 15 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_subagent_stop.sh", + "timeout": 5 + } + ] + } ] } } diff --git a/mem0-plugin/scripts/on_post_compact.sh b/mem0-plugin/scripts/on_post_compact.sh new file mode 100755 index 000000000..dbd4e260a --- /dev/null +++ b/mem0-plugin/scripts/on_post_compact.sh @@ -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 <>"$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 diff --git a/mem0-plugin/scripts/on_stop.sh b/mem0-plugin/scripts/on_stop.sh index 87403ae63..33beaa3dd 100755 --- a/mem0-plugin/scripts/on_stop.sh +++ b/mem0-plugin/scripts/on_stop.sh @@ -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 diff --git a/mem0-plugin/scripts/on_subagent_stop.sh b/mem0-plugin/scripts/on_subagent_stop.sh new file mode 100755 index 000000000..4601be5b5 --- /dev/null +++ b/mem0-plugin/scripts/on_subagent_stop.sh @@ -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 <>"$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 <