555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
229 lines
8.9 KiB
Bash
Executable File
229 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Hook: UserPromptSubmit
|
|
#
|
|
# Fires on every user message. Prefetches memories relevant to the current
|
|
# prompt (so relevant context is guaranteed) and also injects a decision
|
|
# rubric telling the agent when to search further itself -- including
|
|
# follow-up searches for multi-part questions. Prefetch can be disabled
|
|
# with MEM0_PREFETCH=false.
|
|
#
|
|
# Input: JSON on stdin (prompt, session_id, cwd, transcript_path)
|
|
# Output: Decision rubric injected into Claude's context (exit 0)
|
|
|
|
# Intentionally omit -e so the script always exits 0 even if jq fails --
|
|
# must never block the user's prompt.
|
|
set -uo pipefail
|
|
|
|
if [ -n "${MEM0_DEBUG:-}" ]; then
|
|
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
|
fi
|
|
|
|
INPUT=$(cat)
|
|
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
|
|
|
|
# Acknowledgements and short replies don't warrant memory context
|
|
if [ ${#PROMPT} -lt 20 ]; then
|
|
exit 0
|
|
fi
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck source=_identity.sh
|
|
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
|
|
|
# Rubric dedup: only inject full rubric once per session.
|
|
# Key on session ID to avoid cross-session interference.
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
|
if [ -z "$SESSION_ID" ]; then
|
|
_SID_FILE="/tmp/mem0_session_id_${USER:-default}"
|
|
[ -f "$_SID_FILE" ] && SESSION_ID=$(cat "$_SID_FILE" 2>/dev/null) || true
|
|
fi
|
|
if [ -z "$SESSION_ID" ]; then
|
|
SESSION_ID="default_${USER:-unknown}"
|
|
fi
|
|
RUBRIC_DIR="${MEM0_RUBRIC_DIR:-/tmp}"
|
|
RUBRIC_FLAG="$RUBRIC_DIR/mem0_rubric_${SESSION_ID}"
|
|
RUBRIC_ALREADY_SHOWN=""
|
|
if [ -f "$RUBRIC_FLAG" ]; then
|
|
RUBRIC_ALREADY_SHOWN="true"
|
|
fi
|
|
|
|
# Track message count for periodic memory-save nudges.
|
|
# Every 5th substantial message, remind the agent to store learnings.
|
|
MSG_COUNT_FILE="/tmp/mem0_msg_count_${USER:-default}"
|
|
MSG_COUNT=0
|
|
if [ -f "$MSG_COUNT_FILE" ]; then
|
|
MSG_COUNT=$(cat "$MSG_COUNT_FILE" 2>/dev/null || echo "0")
|
|
fi
|
|
MSG_COUNT=$((MSG_COUNT + 1))
|
|
printf '%s' "$MSG_COUNT" > "$MSG_COUNT_FILE" 2>/dev/null || true
|
|
NEEDS_SAVE_NUDGE=""
|
|
if [ $((MSG_COUNT % 5)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ]; then
|
|
NEEDS_SAVE_NUDGE="true"
|
|
fi
|
|
|
|
# Detect stack traces and error patterns in the prompt (no API needed)
|
|
HAS_ERROR=""
|
|
if echo "$PROMPT" | grep -qE '(Traceback|panic:)'; then
|
|
HAS_ERROR="true"
|
|
elif echo "$PROMPT" | grep -qE '^\s*fatal: '; then
|
|
HAS_ERROR="true"
|
|
elif [ "$(echo "$PROMPT" | grep -cE '(Error:|Exception:|FAIL:)')" -ge 2 ]; then
|
|
HAS_ERROR="true"
|
|
fi
|
|
|
|
# Detect file paths in the prompt (no API needed)
|
|
FILE_PATHS=$(echo "$PROMPT" | grep -oE '([a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html))\b' 2>/dev/null | head -5 || echo "")
|
|
|
|
# Detect session-resume patterns
|
|
HAS_RESUME=""
|
|
if echo "$PROMPT" | grep -qiE '(where (did )?(we|I) (leave|left) off|continue (from )?(where|last)|what were we (working|doing)|pick up where|resume (from |where)|what.s the (current|latest) (state|status)|catch me up|where are we)'; then
|
|
HAS_RESUME="true"
|
|
fi
|
|
|
|
# Detect explicit memory-save intent
|
|
HAS_REMEMBER=""
|
|
if echo "$PROMPT" | grep -qiE '(remember (this|that)|save (this|that) (fact|info|memory|note)|store (this|that)|don.t forget (this|that)|keep (this|that) in (mind|memory))'; then
|
|
HAS_REMEMBER="true"
|
|
fi
|
|
|
|
# Telemetry (background, fire-and-forget)
|
|
_TELEM_ARGS=""
|
|
[ -n "$HAS_ERROR" ] && _TELEM_ARGS="$_TELEM_ARGS --error_detected"
|
|
[ -n "$FILE_PATHS" ] && _TELEM_ARGS="$_TELEM_ARGS --file_paths_detected"
|
|
[ -n "$HAS_RESUME" ] && _TELEM_ARGS="$_TELEM_ARGS --resume_detected"
|
|
[ -n "$HAS_REMEMBER" ] && _TELEM_ARGS="$_TELEM_ARGS --remember_detected"
|
|
python3 "$SCRIPT_DIR/telemetry.py" user_prompt $_TELEM_ARGS 2>/dev/null &
|
|
|
|
# No API key — emit detections only, skip search rubric
|
|
if [ -z "${MEM0_API_KEY:-}" ]; then
|
|
_PROMPT_CTX=""
|
|
if [ -n "$HAS_ERROR" ]; then
|
|
_PROMPT_CTX="Error detected in prompt. Set MEM0_API_KEY to search past debugging context."
|
|
fi
|
|
if [ -n "$FILE_PATHS" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
|
|
fi
|
|
if [ -n "$_PROMPT_CTX" ]; then
|
|
jq -cn --arg ctx "$_PROMPT_CTX" '{
|
|
hookSpecificOutput: {
|
|
hookEventName: "UserPromptSubmit",
|
|
additionalContext: $ctx
|
|
}
|
|
}'
|
|
fi
|
|
exit 0
|
|
fi
|
|
USER_ID="$MEM0_RESOLVED_USER_ID"
|
|
|
|
_PROMPT_CTX=""
|
|
|
|
if [ -n "$HAS_RESUME" ]; then
|
|
RESUME_RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" python3 -c "
|
|
import os, sys
|
|
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
|
|
from _search import search_memories, format_results_for_context, should_rerank
|
|
|
|
api_key = os.environ.get('MEM0_API_KEY', '')
|
|
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
|
|
project_id = os.environ.get('MEM0_PROJECT_ID', 'unknown')
|
|
rerank = should_rerank()
|
|
|
|
state = search_memories(api_key, user_id, project_id, 'session state current task', metadata_type='session_state', top_k=3, rerank=rerank)
|
|
decisions = search_memories(api_key, user_id, project_id, 'recent decisions and learnings', metadata_type='decision', top_k=3, rerank=rerank)
|
|
|
|
all_r = state + decisions
|
|
seen = set()
|
|
unique = []
|
|
for m in all_r:
|
|
mid = m.get('id', '')
|
|
if mid not in seen:
|
|
seen.add(mid)
|
|
unique.append(m)
|
|
|
|
if unique:
|
|
print(format_results_for_context(unique, heading='Session context recovered from mem0'))
|
|
print()
|
|
print('These memories provide context for resuming work.')
|
|
else:
|
|
print('No session state found in mem0.')
|
|
" 2>/dev/null || echo "")
|
|
|
|
if [ -n "$RESUME_RESULTS" ]; then
|
|
_PROMPT_CTX="${RESUME_RESULTS}"
|
|
fi
|
|
fi
|
|
|
|
# Query-driven prefetch: search mem0 with the current prompt and inject the top
|
|
# matches so relevant memories are guaranteed in context, not left for the agent
|
|
# to fetch. Skipped on resume (handled above with targeted queries) and when
|
|
# MEM0_PREFETCH=false.
|
|
if [ -z "$HAS_RESUME" ] && [ "${MEM0_PREFETCH:-true}" != "false" ]; then
|
|
PREFETCH_RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" MEM0_SEARCH_QUERY="$PROMPT" python3 -c "
|
|
import os, sys
|
|
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
|
|
from _search import search_memories, format_results_for_context, should_rerank
|
|
|
|
api_key = os.environ.get('MEM0_API_KEY', '')
|
|
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
|
|
project_id = os.environ.get('MEM0_PROJECT_ID', 'unknown')
|
|
query = os.environ.get('MEM0_SEARCH_QUERY', '')
|
|
|
|
results = search_memories(api_key, user_id, project_id, query, top_k=5, rerank=should_rerank())
|
|
if results:
|
|
print(format_results_for_context(results, heading='Relevant memories (auto-retrieved for this request)'))
|
|
" 2>/dev/null || echo "")
|
|
|
|
if [ -n "$PREFETCH_RESULTS" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}${PREFETCH_RESULTS}"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$HAS_REMEMBER" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Remember intent detected. The /mem0:remember skill auto-classifies, sets confidence=1.0, and stores verbatim."
|
|
fi
|
|
|
|
if [ -z "$RUBRIC_ALREADY_SHOWN" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id. For multi-part or comparative questions, run follow-up searches and combine results before answering -- one search is rarely enough."
|
|
touch "$RUBRIC_FLAG" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -n "$HAS_ERROR" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Error detected in prompt. Prior occurrences are available in mem0 via anti_pattern and task_learning type filters."
|
|
fi
|
|
|
|
if [ -n "$FILE_PATHS" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
|
|
fi
|
|
|
|
# Auto-capture: directly call mem0 API in background every 3rd message.
|
|
# At MSG_COUNT=3 the 3rd response isn't in the transcript yet (hook fires
|
|
# before Claude responds), so we capture 4 exchanges instead of 3. The
|
|
# overlapping window ensures the next batch (MSG_COUNT=6) picks up the
|
|
# exchange that was incomplete in the previous batch.
|
|
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
if [ "${MEM0_AUTO_SAVE:-true}" != "false" ] && [ $((MSG_COUNT % 3)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ] && [ -n "$TRANSCRIPT_PATH" ]; then
|
|
python3 "$SCRIPT_DIR/auto_capture.py" "$TRANSCRIPT_PATH" 2>/dev/null &
|
|
fi
|
|
|
|
# Prompt-based nudge as fallback when auto-capture hasn't run yet.
|
|
_ADDS=0
|
|
_STATS_FILE="/tmp/mem0_session_stats_${USER:-default}.json"
|
|
if [ -f "$_STATS_FILE" ]; then
|
|
_ADDS=$(python3 -c "import json; print(json.load(open('$_STATS_FILE')).get('adds',0))" 2>/dev/null || echo "0")
|
|
fi
|
|
|
|
if [ "$MSG_COUNT" -ge 3 ] && [ "$_ADDS" -lt "$((MSG_COUNT / 3))" ]; then
|
|
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}After responding, store any new decisions, learnings, or preferences from this exchange via add_memory. Keep it to 1 sentence per memory."
|
|
fi
|
|
|
|
if [ -n "$_PROMPT_CTX" ]; then
|
|
jq -cn --arg ctx "$_PROMPT_CTX" '{
|
|
hookSpecificOutput: {
|
|
hookEventName: "UserPromptSubmit",
|
|
additionalContext: $ctx
|
|
}
|
|
}'
|
|
fi
|
|
|
|
exit 0
|