-
Notifications
You must be signed in to change notification settings - Fork 655
Expand file tree
/
Copy pathuser-prompt-submit.sh
More file actions
executable file
·219 lines (188 loc) · 9.25 KB
/
Copy pathuser-prompt-submit.sh
File metadata and controls
executable file
·219 lines (188 loc) · 9.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/bin/bash
# Engram — UserPromptSubmit hook for Codex
#
# On the FIRST message of a session: injects a ToolSearch instruction to force
# Codex to load all engram memory tools (which are deferred by default).
#
# On subsequent messages: checks when the last mem_save was for the current
# project. If it's been > 15 minutes AND the session has been active > 5
# minutes, injects a nudge reminding the agent to save.
#
# The nudge is debounced per session: once shown, it stays quiet for
# ENGRAM_NUDGE_COOLDOWN_SECS (default 900s) before it can fire again.
#
# MUST exit 0 always and output valid JSON — otherwise Codex blocks the message.
ENGRAM_PORT="${ENGRAM_PORT:-7437}"
ENGRAM_URL="http://127.0.0.1:${ENGRAM_PORT}"
print_toolsearch_message() {
printf '%s\n' '{"systemMessage":"CRITICAL FIRST ACTION — Execute this ToolSearch NOW before responding to the user:\nselect:mcp__engram__mem_save,mcp__engram__mem_search,mcp__engram__mem_context,mcp__engram__mem_session_summary,mcp__engram__mem_session_start,mcp__engram__mem_session_end,mcp__engram__mem_get_observation,mcp__engram__mem_suggest_topic_key,mcp__engram__mem_capture_passive,mcp__engram__mem_save_prompt,mcp__engram__mem_update,mcp__engram__mem_current_project,mcp__engram__mem_judge\n\nAfter loading tools, call mem_context to check for prior session history before responding."}'
}
# Load shared helpers
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/_helpers.sh"
# Read hook input from stdin
INPUT=$(cat)
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
# ──────────────────────────────────────────────────────────────────────────────
# PROMPT PERSIST
#
# Every user message is captured to POST /prompts so mem_save can attach the
# originating prompt via SessionActivity. Detached subshell: never blocks and
# never fails the hook.
# ──────────────────────────────────────────────────────────────────────────────
PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
if [ -n "$PROMPT" ] && [ -n "$SESSION_ID" ]; then
(
_PROMPT_PROJECT=$(detect_project "$CWD")
curl -sf -X POST "${ENGRAM_URL}/prompts" --max-time 2 \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg s "$SESSION_ID" --arg p "${_PROMPT_PROJECT:-}" --arg c "$PROMPT" \
'{session_id:$s, project:$p, content:$c}')" >/dev/null 2>&1 || true
) &
fi
parse_epoch() {
TS="$1"
if [ -z "$TS" ]; then
return 1
fi
# Drop fractional seconds without dropping timezone information.
if [[ "$TS" == *.* ]]; then
TS_PREFIX="${TS%%.*}"
TS_SUFFIX="${TS#*.}"
case "$TS_SUFFIX" in
*Z) TS="${TS_PREFIX}Z" ;;
*+*) TS="${TS_PREFIX}+${TS_SUFFIX#*+}" ;;
*-*) TS="${TS_PREFIX}-${TS_SUFFIX#*-}" ;;
*) TS="$TS_PREFIX" ;;
esac
fi
# BSD date accepts numeric RFC3339 offsets with %z, but requires +HHMM.
if [[ "$TS" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})([+-][0-9]{2}):([0-9]{2})$ ]]; then
TZ_TS="${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[3]}"
date -j -f "%Y-%m-%dT%H:%M:%S%z" "$TZ_TS" "+%s" 2>/dev/null && return 0
fi
if [[ "$TS" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{4}$ ]]; then
date -j -f "%Y-%m-%dT%H:%M:%S%z" "$TS" "+%s" 2>/dev/null && return 0
fi
if [[ "$TS" == *Z ]]; then
Z_TS="${TS%Z}"
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$Z_TS" "+%s" 2>/dev/null && return 0
fi
# No timezone marker at all — these are naive timestamps from sqlite's
# datetime('now'), which is always UTC. Must parse with -u; parsing as
# local time skews the result by the host's UTC offset (e.g. on a UTC-6
# host, session age comes out ~6h in the future, so it's always seen as
# "just started" and the save-nudge can never fire).
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -j -u -f "%Y-%m-%d %H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -u -d "$TS" "+%s" 2>/dev/null
}
# Default: no injection
OUTPUT="{}"
# ──────────────────────────────────────────────────────────────────────────────
# FIRST-MESSAGE DETECTION
#
# Use a state file per session to determine if this is the first user message.
# State file lives in /tmp and is keyed by session_id (falls back to project+pid).
# ──────────────────────────────────────────────────────────────────────────────
# Build a stable session key — prefer SESSION_ID, fall back to project name
if [ -n "$SESSION_ID" ]; then
SESSION_KEY="engram-codex-${SESSION_ID}-tools-loaded"
else
# No session ID available — only then detect project for the fallback state key.
PROJECT=$(detect_project "$CWD")
SAFE_PROJECT=$(printf '%s' "${PROJECT:-unknown}" | tr -cs 'a-zA-Z0-9_-' '_')
SESSION_KEY="engram-codex-${SAFE_PROJECT}-$$-tools-loaded"
fi
STATE_FILE="/tmp/${SESSION_KEY}"
if [ ! -f "$STATE_FILE" ]; then
# ── FIRST MESSAGE ────────────────────────────────────────────────────────────
# Create the state file immediately to prevent repeat injections
touch "$STATE_FILE" 2>/dev/null || true
# Inject ToolSearch + mem_context instruction.
print_toolsearch_message
exit 0
fi
# ──────────────────────────────────────────────────────────────────────────────
# SUBSEQUENT MESSAGES — save-nudge logic
# ──────────────────────────────────────────────────────────────────────────────
# Detect project only after the first-message path has had a chance to return.
if [ -z "${PROJECT:-}" ]; then
PROJECT=$(detect_project "$CWD")
fi
# Bail early if we can't determine the project
if [ -z "$PROJECT" ]; then
echo "$OUTPUT"
exit 0
fi
# Get session start time to check if session is > 5 minutes old
SESSION_START=""
if [ -n "$SESSION_ID" ]; then
SESSION_START=$(curl -sf "${ENGRAM_URL}/sessions/${SESSION_ID}" --max-time 0.2 2>/dev/null \
| jq -r '.started_at // empty' 2>/dev/null)
fi
# Check session age — skip nudge if session is new (< 5 minutes)
if [ -n "$SESSION_START" ]; then
SESSION_START_EPOCH=$(parse_epoch "$SESSION_START")
if [ -z "$SESSION_START_EPOCH" ]; then
echo "$OUTPUT"
exit 0
fi
NOW_EPOCH=$(date "+%s")
SESSION_AGE_SECS=$(( NOW_EPOCH - SESSION_START_EPOCH ))
if [ "$SESSION_AGE_SECS" -lt 300 ]; then
# Session < 5 minutes old — no nudge yet
echo "$OUTPUT"
exit 0
fi
fi
# Fetch the most recent observation for this project (any type)
ENCODED_PROJECT=$(printf '%s' "$PROJECT" | jq -sRr @uri)
LAST_SAVE_JSON=$(curl -sf \
"${ENGRAM_URL}/observations?project=${ENCODED_PROJECT}&limit=1&sort=created_at:desc" \
--max-time 0.2 2>/dev/null)
if [ -z "$LAST_SAVE_JSON" ]; then
# Server not responding or slow — fail silently, no nudge
echo "$OUTPUT"
exit 0
fi
LAST_SAVE_AT=$(echo "$LAST_SAVE_JSON" | jq -r '.[0].created_at // empty' 2>/dev/null)
NOW_EPOCH=$(date "+%s")
if [ -z "$LAST_SAVE_AT" ]; then
# No observations exist yet for this project. This is the "never saved"
# case, not "session just started" (session age was already gated to
# >= 5 minutes above) — treat it as maximally stale so the nudge can fire
# and break the cycle where a project with zero observations can never
# reach its first one.
ELAPSED=901
else
# Parse last save timestamp and compare to now
LAST_EPOCH=$(parse_epoch "$LAST_SAVE_AT")
if [ -z "$LAST_EPOCH" ]; then
echo "$OUTPUT"
exit 0
fi
ELAPSED=$(( NOW_EPOCH - LAST_EPOCH ))
fi
# Nudge if last save was > 15 minutes ago (900 seconds), but debounce so we do
# not repeat the reminder on every message while the agent has nothing to save.
if [ "$ELAPSED" -gt 900 ]; then
NUDGE_COOLDOWN="${ENGRAM_NUDGE_COOLDOWN_SECS:-900}"
NUDGE_STATE_FILE="${STATE_FILE%-tools-loaded}-last-nudge"
LAST_NUDGE_EPOCH=""
if [ -f "$NUDGE_STATE_FILE" ]; then
read -r LAST_NUDGE_EPOCH < "$NUDGE_STATE_FILE" 2>/dev/null || LAST_NUDGE_EPOCH=""
fi
# Ignore a corrupt/non-numeric state file — treat as "never nudged".
case "$LAST_NUDGE_EPOCH" in
''|*[!0-9]*) LAST_NUDGE_EPOCH="" ;;
esac
if [ -z "$LAST_NUDGE_EPOCH" ] || [ "$(( NOW_EPOCH - LAST_NUDGE_EPOCH ))" -ge "$NUDGE_COOLDOWN" ]; then
printf '%s' "$NOW_EPOCH" > "$NUDGE_STATE_FILE" 2>/dev/null || true
OUTPUT=$(jq -n \
'{"systemMessage": "MEMORY REMINDER: It'\''s been over 15 minutes since your last save. If you'\''ve made decisions, discoveries, or completed significant work, call mem_save now."}')
fi
fi
echo "$OUTPUT"
exit 0