eagle-mem 4.12.1 → 4.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +4 -0
- package/db/migrate.sh +11 -1
- package/docs/agent-compatibility/claude-code.md +27 -0
- package/docs/agent-compatibility/codex.md +1 -0
- package/docs/reviews/2026-06-10-full-spectrum-hardening.md +90 -0
- package/hooks/post-tool-use.sh +73 -22
- package/hooks/session-end.sh +10 -0
- package/hooks/session-start.sh +24 -1
- package/hooks/stop.sh +7 -2
- package/integrations/google_antigravity_hook.py +61 -26
- package/lib/codex-hooks.sh +5 -1
- package/lib/common.sh +104 -4
- package/lib/db-core.sh +28 -0
- package/lib/db-events.sh +13 -0
- package/lib/db-observations.sh +10 -3
- package/lib/db-sessions.sh +10 -1
- package/lib/db-summaries.sh +4 -1
- package/lib/hooks-sessionstart.sh +32 -13
- package/lib/hooks.sh +37 -0
- package/lib/provider.sh +10 -2
- package/lib/updater.sh +16 -2
- package/package.json +1 -1
- package/scripts/enrich-summary.sh +4 -1
- package/scripts/install.sh +3 -41
- package/scripts/logs.sh +44 -12
- package/scripts/orchestrate.sh +34 -4
- package/scripts/session.sh +5 -0
- package/scripts/statusline-em.sh +5 -1
- package/scripts/tasks.sh +6 -3
- package/scripts/test.sh +31 -3
- package/scripts/update.sh +3 -17
- package/tests/test_compaction_survival_matrix.sh +13 -1
- package/tests/test_context_budget.sh +117 -0
- package/tests/test_data_integrity_hardening.sh +115 -0
- package/tests/test_mod_tracker_concurrency.sh +142 -0
- package/tests/test_redaction_coverage.sh +183 -0
- package/tests/test_reliability_retention.sh +75 -0
- package/tests/test_rust_migration_plan.sh +8 -1
- package/tests/test_test_runner_no_abort.sh +86 -0
package/lib/common.sh
CHANGED
|
@@ -456,7 +456,9 @@ eagle_get_session_project_light() {
|
|
|
456
456
|
|
|
457
457
|
local sid_sql project
|
|
458
458
|
sid_sql=$(eagle_sql_escape "$session_id")
|
|
459
|
-
|
|
459
|
+
# busy_timeout so a momentary SQLITE_BUSY waits for the lock instead of
|
|
460
|
+
# exiting non-zero and being misread as "session has no project" (fail-open).
|
|
461
|
+
project=$("$sqlite_bin" "$EAGLE_MEM_DB" "PRAGMA busy_timeout=10000; SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
|
|
460
462
|
[ -n "$project" ] || return 1
|
|
461
463
|
printf '%s\n' "$project"
|
|
462
464
|
}
|
|
@@ -465,6 +467,13 @@ eagle_project_has_table_row() {
|
|
|
465
467
|
local table="${1:-}"
|
|
466
468
|
local project="${2:-}"
|
|
467
469
|
[ -n "$table" ] && [ -n "$project" ] || return 1
|
|
470
|
+
# The table name is interpolated raw (SQLite cannot bind identifiers), so
|
|
471
|
+
# allowlist the only callers' tables. Reject anything else rather than risk
|
|
472
|
+
# identifier injection if a future caller passes a non-constant.
|
|
473
|
+
case "$table" in
|
|
474
|
+
agent_memories|summaries|observations|agent_tasks|agent_plans|sessions) ;;
|
|
475
|
+
*) return 1 ;;
|
|
476
|
+
esac
|
|
468
477
|
local sqlite_bin
|
|
469
478
|
sqlite_bin=$(eagle_sqlite_path)
|
|
470
479
|
[ -n "$sqlite_bin" ] || return 1
|
|
@@ -472,7 +481,9 @@ eagle_project_has_table_row() {
|
|
|
472
481
|
|
|
473
482
|
local project_sql found
|
|
474
483
|
project_sql=$(eagle_sql_escape "$project")
|
|
475
|
-
|
|
484
|
+
# busy_timeout so a momentary SQLITE_BUSY waits for the lock instead of
|
|
485
|
+
# exiting non-zero and being misread as "row doesn't exist" (fail-open).
|
|
486
|
+
found=$("$sqlite_bin" "$EAGLE_MEM_DB" "PRAGMA busy_timeout=10000; SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
|
|
476
487
|
[ "$found" = "1" ]
|
|
477
488
|
}
|
|
478
489
|
|
|
@@ -1103,6 +1114,62 @@ eagle_read_guard_block_threshold() {
|
|
|
1103
1114
|
printf '%s\n' "$threshold"
|
|
1104
1115
|
}
|
|
1105
1116
|
|
|
1117
|
+
# Generous ceiling (chars) on the recall body SessionStart injects. Typical
|
|
1118
|
+
# recall is a few KB; this only clips pathological projects (huge/many
|
|
1119
|
+
# summaries, lanes, etc.). It is NOT meant to shrink normal output. Default
|
|
1120
|
+
# 24000 chars (~6K tokens).
|
|
1121
|
+
eagle_sessionstart_inject_budget() {
|
|
1122
|
+
local budget
|
|
1123
|
+
if declare -F eagle_config_get >/dev/null 2>&1; then
|
|
1124
|
+
budget=$(eagle_config_get "context_budget" "sessionstart_chars" "24000")
|
|
1125
|
+
else
|
|
1126
|
+
budget=$(eagle_config_get_light "context_budget" "sessionstart_chars" "24000")
|
|
1127
|
+
fi
|
|
1128
|
+
case "$budget" in *[!0-9]*|"") budget=24000 ;; esac
|
|
1129
|
+
# Refuse a self-defeating tiny budget; floor keeps the top sections intact.
|
|
1130
|
+
[ "$budget" -lt 4000 ] 2>/dev/null && budget=4000
|
|
1131
|
+
printf '%s\n' "$budget"
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
# Trim a SessionStart recall body to a char budget by dropping whole
|
|
1135
|
+
# "=== Eagle Mem: ..." sections from the END (lowest priority appended last)
|
|
1136
|
+
# until the body fits. The body is passed on stdin; trimmed body is printed on
|
|
1137
|
+
# stdout; the count of dropped sections is printed on fd 3 if open, else to
|
|
1138
|
+
# the EAGLE_INJECT_TRIM_COUNT file when set. Sections are never split — a
|
|
1139
|
+
# section is kept whole or dropped whole, so no surface is half-emitted.
|
|
1140
|
+
eagle_trim_inject_body() {
|
|
1141
|
+
local budget="$1"
|
|
1142
|
+
local body; body=$(cat)
|
|
1143
|
+
local dropped=0
|
|
1144
|
+
|
|
1145
|
+
if [ "${#body}" -le "$budget" ] 2>/dev/null; then
|
|
1146
|
+
printf '%s' "$body"
|
|
1147
|
+
[ -n "${EAGLE_INJECT_TRIM_COUNT:-}" ] && printf '0' > "$EAGLE_INJECT_TRIM_COUNT" 2>/dev/null
|
|
1148
|
+
return 0
|
|
1149
|
+
fi
|
|
1150
|
+
|
|
1151
|
+
# Iteratively remove the last "=== Eagle Mem:" section (and everything
|
|
1152
|
+
# after it) until we fit or only the first section remains.
|
|
1153
|
+
while [ "${#body}" -gt "$budget" ] 2>/dev/null; do
|
|
1154
|
+
# Byte offset of the last section header.
|
|
1155
|
+
local last_idx="${body##*=== Eagle Mem:}"
|
|
1156
|
+
# No (more) section headers, or only one section left → stop trimming.
|
|
1157
|
+
if [ "$last_idx" = "$body" ]; then
|
|
1158
|
+
break
|
|
1159
|
+
fi
|
|
1160
|
+
local head="${body%=== Eagle Mem:*}"
|
|
1161
|
+
# If trimming would leave nothing, keep at least the first section.
|
|
1162
|
+
case "$head" in
|
|
1163
|
+
*"=== Eagle Mem:"*) body="$head"; dropped=$((dropped + 1)) ;;
|
|
1164
|
+
*) break ;;
|
|
1165
|
+
esac
|
|
1166
|
+
done
|
|
1167
|
+
|
|
1168
|
+
printf '%s' "$body"
|
|
1169
|
+
[ -n "${EAGLE_INJECT_TRIM_COUNT:-}" ] && printf '%s' "$dropped" > "$EAGLE_INJECT_TRIM_COUNT" 2>/dev/null
|
|
1170
|
+
return 0
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1106
1173
|
eagle_raw_output_command_needs_guard() {
|
|
1107
1174
|
local cmd="$1"
|
|
1108
1175
|
local first
|
|
@@ -1378,8 +1445,32 @@ eagle_read_stdin() {
|
|
|
1378
1445
|
# Redact secrets from text before storage.
|
|
1379
1446
|
# Covers: Bearer tokens, API keys, passwords, secrets, tokens,
|
|
1380
1447
|
# Stripe/AWS/GitHub/Anthropic/OpenAI key patterns, named env vars.
|
|
1448
|
+
# Read configured [redaction] extra_patterns (TOML array) into a list of regexes,
|
|
1449
|
+
# one per line. Parses `extra_patterns = ["A.*", "B-[0-9]+"]` from config.toml.
|
|
1450
|
+
# Lines beginning with '#' (the commented default) are ignored by the config reader.
|
|
1451
|
+
eagle_redaction_extra_patterns() {
|
|
1452
|
+
local raw
|
|
1453
|
+
if declare -F eagle_config_get >/dev/null 2>&1; then
|
|
1454
|
+
raw=$(eagle_config_get "redaction" "extra_patterns" "" 2>/dev/null)
|
|
1455
|
+
else
|
|
1456
|
+
raw=$(eagle_config_get_light "redaction" "extra_patterns" "" 2>/dev/null)
|
|
1457
|
+
fi
|
|
1458
|
+
[ -n "$raw" ] || return 0
|
|
1459
|
+
# Strip surrounding [ ] brackets, then extract each double/single-quoted
|
|
1460
|
+
# pattern. Using awk avoids splitting on commas that appear inside a regex
|
|
1461
|
+
# character class (e.g. [A-Z,0-9]).
|
|
1462
|
+
printf '%s' "$raw" \
|
|
1463
|
+
| sed -E 's/^[[:space:]]*\[//; s/\][[:space:]]*$//' \
|
|
1464
|
+
| grep -oE '"[^"]*"|'"'"'[^'"'"']*'"'"'' \
|
|
1465
|
+
| sed -E 's/^["'"'"']//; s/["'"'"']$//' \
|
|
1466
|
+
| while IFS= read -r pat; do
|
|
1467
|
+
[ -n "$pat" ] && printf '%s\n' "$pat"
|
|
1468
|
+
done
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1381
1471
|
eagle_redact() {
|
|
1382
|
-
|
|
1472
|
+
local _redacted
|
|
1473
|
+
_redacted=$(sed -E \
|
|
1383
1474
|
-e 's/(Bearer )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
|
|
1384
1475
|
-e 's/(api[_-]?key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
|
|
1385
1476
|
-e 's/(password[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
|
|
@@ -1407,7 +1498,16 @@ eagle_redact() {
|
|
|
1407
1498
|
-e 's/(OPENAI_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
|
|
1408
1499
|
-e 's/(GOOGLE_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
|
|
1409
1500
|
-e 's/(SLACK_TOKEN[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
|
|
1410
|
-
-e 's/(DATABASE_URL[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g'
|
|
1501
|
+
-e 's/(DATABASE_URL[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g')
|
|
1502
|
+
|
|
1503
|
+
# Apply user-configured extra patterns (if any) on top of the built-in set.
|
|
1504
|
+
local pat
|
|
1505
|
+
while IFS= read -r pat; do
|
|
1506
|
+
[ -n "$pat" ] || continue
|
|
1507
|
+
_redacted=$(printf '%s' "$_redacted" | sed -E "s/${pat}/[REDACTED]/g" 2>/dev/null) || true
|
|
1508
|
+
done < <(eagle_redaction_extra_patterns)
|
|
1509
|
+
|
|
1510
|
+
printf '%s\n' "$_redacted"
|
|
1411
1511
|
}
|
|
1412
1512
|
|
|
1413
1513
|
# Collect project files into a destination file.
|
package/lib/db-core.sh
CHANGED
|
@@ -14,6 +14,13 @@ PRAGMA foreign_keys=ON;
|
|
|
14
14
|
PRAGMA trusted_schema=ON;
|
|
15
15
|
.output stdout"
|
|
16
16
|
|
|
17
|
+
# eagle_db — CONTRACT: continue-on-error (NO `.bail on`). When passed
|
|
18
|
+
# multi-statement SQL, sqlite3 runs every statement even if an earlier one
|
|
19
|
+
# errors. Many callers depend on this best-effort behavior (e.g. probing for a
|
|
20
|
+
# table then querying it, or fire-and-forget multi-table writes). The return
|
|
21
|
+
# code reflects sqlite3's exit status (non-zero if the LAST statement failed),
|
|
22
|
+
# and any stderr is mirrored to the log. If you need fail-fast atomicity across
|
|
23
|
+
# statements, use eagle_db_strict (or eagle_db_pipe, which also sets `.bail on`).
|
|
17
24
|
eagle_db() {
|
|
18
25
|
local _eagle_sqlite_bin
|
|
19
26
|
_eagle_sqlite_bin=$(eagle_sqlite_path)
|
|
@@ -31,6 +38,27 @@ eagle_db() {
|
|
|
31
38
|
return $_eagle_db_rc
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
# eagle_db_strict — same as eagle_db but with `.bail on`, so a multi-statement
|
|
42
|
+
# script aborts on the FIRST error instead of continuing. Use for fail-fast
|
|
43
|
+
# transactions (BEGIN/.../COMMIT) where a mid-script error must not commit a
|
|
44
|
+
# partially-applied result. Single-statement callers can use either function.
|
|
45
|
+
eagle_db_strict() {
|
|
46
|
+
local _eagle_sqlite_bin
|
|
47
|
+
_eagle_sqlite_bin=$(eagle_sqlite_path)
|
|
48
|
+
[ -n "$_eagle_sqlite_bin" ] || return 1
|
|
49
|
+
local _eagle_db_err
|
|
50
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_strict_err.$$")
|
|
51
|
+
local _eagle_db_out
|
|
52
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; echo "$*"; } | "$_eagle_sqlite_bin" "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
53
|
+
local _eagle_db_rc=$?
|
|
54
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
55
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
56
|
+
fi
|
|
57
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
58
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
59
|
+
return $_eagle_db_rc
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
eagle_db_pipe() {
|
|
35
63
|
local _eagle_sqlite_bin
|
|
36
64
|
_eagle_sqlite_bin=$(eagle_sqlite_path)
|
package/lib/db-events.sh
CHANGED
|
@@ -41,6 +41,19 @@ eagle_insert_event() {
|
|
|
41
41
|
);" >/dev/null 2>&1 || true
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
# eagle_events is hook-observability telemetry written on every hook fire, so it
|
|
45
|
+
# grows much faster than any user-data table and was previously never pruned.
|
|
46
|
+
# Bound it by age at SessionEnd (mirrors eagle_prune_observations).
|
|
47
|
+
eagle_prune_events() {
|
|
48
|
+
local days; days=$(eagle_sql_int "${1:-30}")
|
|
49
|
+
local project_filter=""
|
|
50
|
+
if [ -n "${2:-}" ]; then
|
|
51
|
+
local proj; proj=$(eagle_sql_escape "$2")
|
|
52
|
+
project_filter="AND project = '$proj'"
|
|
53
|
+
fi
|
|
54
|
+
eagle_db "DELETE FROM eagle_events WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-$days days') $project_filter;" >/dev/null 2>&1 || true
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
eagle_hook_observability_begin() {
|
|
45
58
|
local input="$1"
|
|
46
59
|
local default_hook="$2"
|
package/lib/db-observations.sh
CHANGED
|
@@ -24,7 +24,13 @@ eagle_insert_observation() {
|
|
|
24
24
|
extra_vals=", $(eagle_sql_int "$output_bytes"), $(eagle_sql_int "$output_lines"), '$command_category'"
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# BEGIN IMMEDIATE serializes the check-then-insert under the write lock so
|
|
28
|
+
# two concurrent hooks can't both pass NOT EXISTS and double-insert (the
|
|
29
|
+
# second blocks via busy_timeout, then sees the first row). Without it the
|
|
30
|
+
# 5-second dedup window is racy. eagle_db_strict (.bail on) ensures a failed
|
|
31
|
+
# INSERT aborts before COMMIT so the transaction rolls back cleanly.
|
|
32
|
+
eagle_db_strict "BEGIN IMMEDIATE;
|
|
33
|
+
INSERT INTO observations (session_id, project, agent, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
|
|
28
34
|
SELECT '$session_id', '$project', '$agent', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
|
|
29
35
|
WHERE NOT EXISTS (
|
|
30
36
|
SELECT 1 FROM observations
|
|
@@ -32,7 +38,8 @@ eagle_insert_observation() {
|
|
|
32
38
|
AND tool_name = '$tool_name'
|
|
33
39
|
AND tool_input_summary = '$tool_input_summary'
|
|
34
40
|
AND created_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-5 seconds')
|
|
35
|
-
);
|
|
41
|
+
);
|
|
42
|
+
COMMIT;"
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
eagle_insert_recall_event() {
|
|
@@ -40,7 +47,7 @@ eagle_insert_recall_event() {
|
|
|
40
47
|
local project; project=$(eagle_sql_escape "${2:-}")
|
|
41
48
|
local cwd; cwd=$(eagle_sql_escape "${3:-}")
|
|
42
49
|
local agent; agent=$(eagle_sql_escape "${4:-$(eagle_agent_source)}")
|
|
43
|
-
local prompt_snippet; prompt_snippet=$(eagle_sql_escape "$(eagle_trim_text "${5:-}" 240)")
|
|
50
|
+
local prompt_snippet; prompt_snippet=$(eagle_sql_escape "$(eagle_trim_text "${5:-}" 240 | eagle_redact)")
|
|
44
51
|
local fts_query; fts_query=$(eagle_sql_escape "${6:-}")
|
|
45
52
|
local summary_matches; summary_matches=$(eagle_sql_int "${7:-0}")
|
|
46
53
|
local memory_matches; memory_matches=$(eagle_sql_int "${8:-0}")
|
package/lib/db-sessions.sh
CHANGED
|
@@ -53,7 +53,12 @@ eagle_upsert_session() {
|
|
|
53
53
|
fi
|
|
54
54
|
|
|
55
55
|
if [ "$needs_project_repair" = "1" ]; then
|
|
56
|
-
|
|
56
|
+
# Capture rc instead of >/dev/null 2>&1 swallowing it: a failed repair
|
|
57
|
+
# must be distinguishable from success. eagle_db_pipe (.bail on) already
|
|
58
|
+
# mirrors sqlite stderr to EAGLE_MEM_LOG; we additionally log a clear
|
|
59
|
+
# marker so a half-applied repair is greppable. Happy path is unchanged.
|
|
60
|
+
local _repair_rc
|
|
61
|
+
eagle_db_pipe >/dev/null <<SQL
|
|
57
62
|
BEGIN;
|
|
58
63
|
UPDATE summaries SET project = '$project' WHERE session_id = '$session_id' AND project != '$project';
|
|
59
64
|
UPDATE observations SET project = '$project' WHERE session_id = '$session_id' AND project != '$project';
|
|
@@ -146,6 +151,10 @@ UPDATE pending_feature_verifications SET project = '$project' WHERE source_sessi
|
|
|
146
151
|
UPDATE sessions SET project = '$project' WHERE id = '$session_id' AND project != '$project';
|
|
147
152
|
COMMIT;
|
|
148
153
|
SQL
|
|
154
|
+
_repair_rc=$?
|
|
155
|
+
if [ "$_repair_rc" -ne 0 ]; then
|
|
156
|
+
eagle_log "ERROR" "session project repair failed (rc=$_repair_rc) session=$session_id_raw project=$project_raw — child rows may remain under a stale project key; see prior sqlite errors in this log"
|
|
157
|
+
fi
|
|
149
158
|
fi
|
|
150
159
|
}
|
|
151
160
|
|
package/lib/db-summaries.sh
CHANGED
|
@@ -42,7 +42,10 @@ VALUES (
|
|
|
42
42
|
'$capture_source'
|
|
43
43
|
)
|
|
44
44
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
45
|
-
project =
|
|
45
|
+
project = CASE
|
|
46
|
+
WHEN summaries.capture_source = 'agent' THEN summaries.project
|
|
47
|
+
ELSE excluded.project
|
|
48
|
+
END,
|
|
46
49
|
agent = COALESCE(NULLIF(excluded.agent, ''), summaries.agent),
|
|
47
50
|
request = COALESCE(NULLIF(excluded.request, ''), summaries.request),
|
|
48
51
|
investigated = COALESCE(NULLIF(excluded.investigated, ''), summaries.investigated),
|
|
@@ -31,6 +31,17 @@ _eagle_state_touch() {
|
|
|
31
31
|
touch "$state_file"
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
# In-flight markers debounce concurrent SessionStart spawns WITHOUT doubling as
|
|
35
|
+
# the freshness marker. The freshness marker ("$key") is touched ONLY by the
|
|
36
|
+
# background job on genuine success, so a job that crashes, is killed, or exits 0
|
|
37
|
+
# without producing output never blocks retry for a full day — it leaves at most
|
|
38
|
+
# a short-lived in-flight marker that ages out in minutes. Checked in minutes.
|
|
39
|
+
_eagle_state_inflight_fresh() {
|
|
40
|
+
local key="$1" project="$2" max_age_min="${3:-15}"
|
|
41
|
+
local state_file; state_file=$(_eagle_state_file "${key}-inflight" "$project")
|
|
42
|
+
[ -f "$state_file" ] && [ -z "$(find "$state_file" -mmin +"${max_age_min}" 2>/dev/null)" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
eagle_sessionstart_auto_provision() {
|
|
35
46
|
local project="$1" cwd="$2" scripts_dir="$3"
|
|
36
47
|
local needs_scan=false needs_index=false
|
|
@@ -38,7 +49,7 @@ eagle_sessionstart_auto_provision() {
|
|
|
38
49
|
# Auto-scan: no overview exists
|
|
39
50
|
local overview
|
|
40
51
|
overview=$(eagle_get_overview "$project")
|
|
41
|
-
if [ -z "$overview" ] && ! _eagle_state_fresh "scan" "$project" 1; then
|
|
52
|
+
if [ -z "$overview" ] && ! _eagle_state_fresh "scan" "$project" 1 && ! _eagle_state_inflight_fresh "scan" "$project" 15; then
|
|
42
53
|
needs_scan=true
|
|
43
54
|
fi
|
|
44
55
|
|
|
@@ -46,22 +57,25 @@ eagle_sessionstart_auto_provision() {
|
|
|
46
57
|
local chunk_count
|
|
47
58
|
chunk_count=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$(eagle_sql_escape "$project")';" 2>/dev/null)
|
|
48
59
|
chunk_count=${chunk_count:-0}
|
|
49
|
-
if [ "$chunk_count" -eq 0 ] && ! _eagle_state_fresh "index" "$project" 1; then
|
|
60
|
+
if [ "$chunk_count" -eq 0 ] && ! _eagle_state_fresh "index" "$project" 1 && ! _eagle_state_inflight_fresh "index" "$project" 15; then
|
|
50
61
|
needs_index=true
|
|
51
|
-
elif [ "$chunk_count" -gt 0 ] && ! _eagle_state_fresh "index" "$project" 7; then
|
|
62
|
+
elif [ "$chunk_count" -gt 0 ] && ! _eagle_state_fresh "index" "$project" 7 && ! _eagle_state_inflight_fresh "index" "$project" 15; then
|
|
52
63
|
needs_index=true
|
|
53
64
|
fi
|
|
54
65
|
|
|
55
66
|
if [ "$needs_scan" = true ] && [ "$needs_index" = true ]; then
|
|
56
67
|
eagle_log "INFO" "SessionStart: first-session provision — scan then index"
|
|
57
|
-
_eagle_state_touch "scan" "$project"
|
|
58
|
-
_eagle_state_touch "index" "$project"
|
|
68
|
+
_eagle_state_touch "scan-inflight" "$project"
|
|
69
|
+
_eagle_state_touch "index-inflight" "$project"
|
|
59
70
|
scan_state=$(_eagle_state_file "scan" "$project")
|
|
71
|
+
scan_inflight=$(_eagle_state_file "scan-inflight" "$project")
|
|
60
72
|
index_state=$(_eagle_state_file "index" "$project")
|
|
73
|
+
index_inflight=$(_eagle_state_file "index-inflight" "$project")
|
|
61
74
|
nohup bash -c '
|
|
62
|
-
scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4";
|
|
75
|
+
scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4"; scan_inflight="$5"; index_state="$6"; index_inflight="$7"
|
|
63
76
|
bash "$scripts_dir/scan.sh" "$cwd" >> "$log" 2>&1
|
|
64
77
|
scan_rc=$?
|
|
78
|
+
rm -f "$scan_inflight" 2>/dev/null || true
|
|
65
79
|
if [ "$scan_rc" -eq 0 ]; then
|
|
66
80
|
touch "$scan_state" 2>/dev/null || true
|
|
67
81
|
else
|
|
@@ -71,6 +85,7 @@ eagle_sessionstart_auto_provision() {
|
|
|
71
85
|
|
|
72
86
|
bash "$scripts_dir/index.sh" "$cwd" >> "$log" 2>&1
|
|
73
87
|
index_rc=$?
|
|
88
|
+
rm -f "$index_inflight" 2>/dev/null || true
|
|
74
89
|
if [ "$index_rc" -eq 0 ]; then
|
|
75
90
|
touch "$index_state" 2>/dev/null || true
|
|
76
91
|
else
|
|
@@ -80,15 +95,17 @@ eagle_sessionstart_auto_provision() {
|
|
|
80
95
|
|
|
81
96
|
[ "$scan_rc" -eq 0 ] && exit "$index_rc"
|
|
82
97
|
exit "$scan_rc"
|
|
83
|
-
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" "$index_state" &
|
|
98
|
+
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" "$scan_inflight" "$index_state" "$index_inflight" &
|
|
84
99
|
elif [ "$needs_scan" = true ]; then
|
|
85
100
|
eagle_log "INFO" "SessionStart: auto-scan triggered"
|
|
86
|
-
_eagle_state_touch "scan" "$project"
|
|
101
|
+
_eagle_state_touch "scan-inflight" "$project"
|
|
87
102
|
scan_state=$(_eagle_state_file "scan" "$project")
|
|
103
|
+
scan_inflight=$(_eagle_state_file "scan-inflight" "$project")
|
|
88
104
|
nohup bash -c '
|
|
89
|
-
scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4"
|
|
105
|
+
scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4"; scan_inflight="$5"
|
|
90
106
|
bash "$scripts_dir/scan.sh" "$cwd" >> "$log" 2>&1
|
|
91
107
|
rc=$?
|
|
108
|
+
rm -f "$scan_inflight" 2>/dev/null || true
|
|
92
109
|
if [ "$rc" -eq 0 ]; then
|
|
93
110
|
touch "$scan_state" 2>/dev/null || true
|
|
94
111
|
else
|
|
@@ -96,15 +113,17 @@ eagle_sessionstart_auto_provision() {
|
|
|
96
113
|
printf "[%s] [ERROR] SessionStart: auto-scan failed rc=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$rc" >> "$log" 2>/dev/null || true
|
|
97
114
|
fi
|
|
98
115
|
exit "$rc"
|
|
99
|
-
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" &
|
|
116
|
+
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" "$scan_inflight" &
|
|
100
117
|
elif [ "$needs_index" = true ]; then
|
|
101
118
|
eagle_log "INFO" "SessionStart: auto-index triggered"
|
|
102
|
-
_eagle_state_touch "index" "$project"
|
|
119
|
+
_eagle_state_touch "index-inflight" "$project"
|
|
103
120
|
index_state=$(_eagle_state_file "index" "$project")
|
|
121
|
+
index_inflight=$(_eagle_state_file "index-inflight" "$project")
|
|
104
122
|
nohup bash -c '
|
|
105
|
-
scripts_dir="$1"; cwd="$2"; log="$3"; index_state="$4"
|
|
123
|
+
scripts_dir="$1"; cwd="$2"; log="$3"; index_state="$4"; index_inflight="$5"
|
|
106
124
|
bash "$scripts_dir/index.sh" "$cwd" >> "$log" 2>&1
|
|
107
125
|
rc=$?
|
|
126
|
+
rm -f "$index_inflight" 2>/dev/null || true
|
|
108
127
|
if [ "$rc" -eq 0 ]; then
|
|
109
128
|
touch "$index_state" 2>/dev/null || true
|
|
110
129
|
else
|
|
@@ -112,7 +131,7 @@ eagle_sessionstart_auto_provision() {
|
|
|
112
131
|
printf "[%s] [ERROR] SessionStart: auto-index failed rc=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$rc" >> "$log" 2>/dev/null || true
|
|
113
132
|
fi
|
|
114
133
|
exit "$rc"
|
|
115
|
-
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$index_state" &
|
|
134
|
+
' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$index_state" "$index_inflight" &
|
|
116
135
|
fi
|
|
117
136
|
}
|
|
118
137
|
|
package/lib/hooks.sh
CHANGED
|
@@ -100,3 +100,40 @@ eagle_patch_hook() {
|
|
|
100
100
|
[ -n "$description" ] && eagle_ok "$description"
|
|
101
101
|
return 0
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
# Register (idempotently) the full Claude Code hook set into a settings.json.
|
|
105
|
+
# Single source of truth for the event→matcher→script mapping so install.sh and
|
|
106
|
+
# update.sh can never drift (the historical bug class). Requires $EAGLE_MEM_DIR.
|
|
107
|
+
# $1 = settings.json path
|
|
108
|
+
# $2 = "verbose" to print a "✓ <Event> hook" line per registration (installer);
|
|
109
|
+
# omitted/anything else = quiet (updater prints its own summary line).
|
|
110
|
+
eagle_register_claude_hooks() {
|
|
111
|
+
local settings="$1"
|
|
112
|
+
local V=""
|
|
113
|
+
[ "${2:-}" = "verbose" ] && V=1
|
|
114
|
+
|
|
115
|
+
# Clean old registrations before re-registering (handles matcher changes across versions).
|
|
116
|
+
eagle_clean_hook_entries "$settings" "Stop" "$EAGLE_MEM_DIR/hooks/stop.sh"
|
|
117
|
+
eagle_clean_hook_entries "$settings" "PostToolUse" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
|
|
118
|
+
eagle_clean_hook_entries "$settings" "PreToolUse" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
|
|
119
|
+
|
|
120
|
+
# ${V:+label} expands to the label only in verbose mode; otherwise to "",
|
|
121
|
+
# which makes eagle_patch_hook silent (it only prints when given a description).
|
|
122
|
+
eagle_patch_hook "$settings" "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh" "${V:+SessionStart hook}"
|
|
123
|
+
eagle_patch_hook "$settings" "Stop" "" "bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" "${V:+Stop hook}"
|
|
124
|
+
eagle_patch_hook "$settings" "PostToolUse" "Read|Write|Edit|Bash|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+PostToolUse hook}"
|
|
125
|
+
eagle_patch_hook "$settings" "TaskCreated" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+TaskCreated hook}"
|
|
126
|
+
eagle_patch_hook "$settings" "TaskCompleted" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+TaskCompleted hook}"
|
|
127
|
+
eagle_patch_hook "$settings" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh" "${V:+SessionEnd hook}"
|
|
128
|
+
eagle_patch_hook "$settings" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh" "${V:+UserPromptSubmit hook}"
|
|
129
|
+
eagle_patch_hook "$settings" "PreToolUse" "Bash|Read|Edit|Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" "${V:+PreToolUse hook}"
|
|
130
|
+
|
|
131
|
+
# Allow agent-issued session capture to run without a permission prompt.
|
|
132
|
+
if eagle_patch_permission_allow "$settings" "Bash(eagle-mem session save:*)"; then
|
|
133
|
+
[ -n "$V" ] && eagle_ok "Capture permission ${DIM}(eagle-mem session save)${RESET}"
|
|
134
|
+
fi
|
|
135
|
+
# Explicit success: the trailing conditional above evaluates false in quiet
|
|
136
|
+
# mode (V empty), which would otherwise make this function return 1 and abort
|
|
137
|
+
# a `set -e` caller (update.sh) right after a fully successful registration.
|
|
138
|
+
return 0
|
|
139
|
+
}
|
package/lib/provider.sh
CHANGED
|
@@ -173,6 +173,12 @@ claude_model = ""
|
|
|
173
173
|
route = "opposite"
|
|
174
174
|
auto_worktree = "true"
|
|
175
175
|
worktree_root = ""
|
|
176
|
+
# worker_autonomy: "safe" (default) runs spawned workers with a sandbox and an
|
|
177
|
+
# approval/permission gate. "danger" runs them with full filesystem access and
|
|
178
|
+
# no approvals (codex --sandbox danger-full-access, claude --permission-mode
|
|
179
|
+
# dontAsk). Only enable "danger" when you trust the lane descriptions, since
|
|
180
|
+
# worker prompts are assembled from DB-stored lane text.
|
|
181
|
+
worker_autonomy = "safe"
|
|
176
182
|
codex_worker_model = "gpt-5.5"
|
|
177
183
|
codex_worker_effort = "xhigh"
|
|
178
184
|
claude_worker_model = "claude-opus-4-7"
|
|
@@ -561,6 +567,8 @@ _eagle_call_claude_cli() {
|
|
|
561
567
|
|
|
562
568
|
local _had_errexit=0
|
|
563
569
|
case "$-" in *e*) _had_errexit=1; set +e ;; esac
|
|
570
|
+
# Pass the prompt via stdin (not argv) so secrets in the prompt are not
|
|
571
|
+
# visible in `ps`, matching the Codex provider path above.
|
|
564
572
|
if [ -n "$model" ]; then
|
|
565
573
|
EAGLE_MEM_DISABLE_HOOKS=1 CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 claude -p \
|
|
566
574
|
--no-session-persistence \
|
|
@@ -569,7 +577,7 @@ _eagle_call_claude_cli() {
|
|
|
569
577
|
--tools "" \
|
|
570
578
|
--output-format text \
|
|
571
579
|
--model "$model" \
|
|
572
|
-
|
|
580
|
+
< "$prompt_file" > "$out_file" 2>> "$EAGLE_MEM_LOG"
|
|
573
581
|
rc=$?
|
|
574
582
|
else
|
|
575
583
|
EAGLE_MEM_DISABLE_HOOKS=1 CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 claude -p \
|
|
@@ -578,7 +586,7 @@ _eagle_call_claude_cli() {
|
|
|
578
586
|
--permission-mode dontAsk \
|
|
579
587
|
--tools "" \
|
|
580
588
|
--output-format text \
|
|
581
|
-
|
|
589
|
+
< "$prompt_file" > "$out_file" 2>> "$EAGLE_MEM_LOG"
|
|
582
590
|
rc=$?
|
|
583
591
|
fi
|
|
584
592
|
[ "$_had_errexit" -eq 1 ] && set -e
|
package/lib/updater.sh
CHANGED
|
@@ -195,9 +195,16 @@ eagle_update_backup_runtime() {
|
|
|
195
195
|
local sqlite_bin
|
|
196
196
|
sqlite_bin=$(eagle_sqlite_path)
|
|
197
197
|
if [ -n "$sqlite_bin" ]; then
|
|
198
|
-
|
|
198
|
+
# busy_timeout so a concurrent hook holding the lock doesn't make
|
|
199
|
+
# .backup fail immediately and fall through to a raw cp. The raw cp
|
|
200
|
+
# last-resort copies the -wal/-shm too so the snapshot stays
|
|
201
|
+
# internally consistent (a bare cp of a WAL DB drops uncommitted WAL).
|
|
202
|
+
"$sqlite_bin" "$EAGLE_MEM_DB" "PRAGMA busy_timeout=10000; .backup '$backup_dir/memory.db'" >/dev/null 2>&1 \
|
|
203
|
+
|| { cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null \
|
|
204
|
+
&& cp "$EAGLE_MEM_DB-wal" "$backup_dir/memory.db-wal" 2>/dev/null; cp "$EAGLE_MEM_DB-shm" "$backup_dir/memory.db-shm" 2>/dev/null; true; }
|
|
199
205
|
else
|
|
200
|
-
cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null
|
|
206
|
+
cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null \
|
|
207
|
+
&& { cp "$EAGLE_MEM_DB-wal" "$backup_dir/memory.db-wal" 2>/dev/null; cp "$EAGLE_MEM_DB-shm" "$backup_dir/memory.db-shm" 2>/dev/null; true; }
|
|
201
208
|
fi
|
|
202
209
|
fi
|
|
203
210
|
}
|
|
@@ -218,7 +225,14 @@ eagle_update_restore_runtime() {
|
|
|
218
225
|
done
|
|
219
226
|
|
|
220
227
|
if [ -f "$backup_dir/memory.db" ]; then
|
|
228
|
+
# Drop any live sidecars first so a restored main DB is never paired with
|
|
229
|
+
# a stale -wal/-shm. Then restore the main DB and (if the raw-cp fallback
|
|
230
|
+
# captured them) its sidecars, keeping the snapshot internally consistent.
|
|
231
|
+
rm -f "$EAGLE_MEM_DB-wal" "$EAGLE_MEM_DB-shm" 2>/dev/null || true
|
|
221
232
|
cp "$backup_dir/memory.db" "$EAGLE_MEM_DB" 2>/dev/null || true
|
|
233
|
+
[ -f "$backup_dir/memory.db-wal" ] && cp "$backup_dir/memory.db-wal" "$EAGLE_MEM_DB-wal" 2>/dev/null
|
|
234
|
+
[ -f "$backup_dir/memory.db-shm" ] && cp "$backup_dir/memory.db-shm" "$EAGLE_MEM_DB-shm" 2>/dev/null
|
|
235
|
+
true
|
|
222
236
|
fi
|
|
223
237
|
}
|
|
224
238
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eagle-mem",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.1",
|
|
4
4
|
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, and Google Antigravity",
|
|
5
5
|
"bin": {
|
|
6
6
|
"eagle-mem": "bin/eagle-mem"
|
|
@@ -33,7 +33,10 @@ if [ "$provider" = "none" ]; then
|
|
|
33
33
|
exit 0
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
# Redact secrets BEFORE the transcript tail is sent to an LLM provider. The job
|
|
37
|
+
# file is already redacted by Stop, but redact here too in case this script is
|
|
38
|
+
# invoked directly or against a legacy job file.
|
|
39
|
+
excerpt=$(printf '%s' "$text_content" | tail -c 3000 | eagle_redact)
|
|
37
40
|
|
|
38
41
|
enrich_prompt="Extract facts from this AI coding session. Only include items with clear evidence in the session text. Do NOT invent or repeat example content.
|
|
39
42
|
|
package/scripts/install.sh
CHANGED
|
@@ -283,47 +283,9 @@ if [ "$claude_found" = true ]; then
|
|
|
283
283
|
echo '{}' > "$SETTINGS"
|
|
284
284
|
fi
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
# Clean old registrations before re-registering (handles matcher changes across versions)
|
|
291
|
-
eagle_clean_hook_entries "$SETTINGS" "Stop" "$EAGLE_MEM_DIR/hooks/stop.sh"
|
|
292
|
-
eagle_clean_hook_entries "$SETTINGS" "PostToolUse" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
|
|
293
|
-
eagle_clean_hook_entries "$SETTINGS" "PreToolUse" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
|
|
294
|
-
|
|
295
|
-
eagle_patch_hook "$SETTINGS" "Stop" "" \
|
|
296
|
-
"bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
|
|
297
|
-
"Stop hook"
|
|
298
|
-
|
|
299
|
-
eagle_patch_hook "$SETTINGS" "PostToolUse" "Read|Write|Edit|Bash|TaskUpdate" \
|
|
300
|
-
"$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
|
|
301
|
-
"PostToolUse hook"
|
|
302
|
-
|
|
303
|
-
eagle_patch_hook "$SETTINGS" "TaskCreated" "" \
|
|
304
|
-
"$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
|
|
305
|
-
"TaskCreated hook"
|
|
306
|
-
|
|
307
|
-
eagle_patch_hook "$SETTINGS" "TaskCompleted" "" \
|
|
308
|
-
"$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
|
|
309
|
-
"TaskCompleted hook"
|
|
310
|
-
|
|
311
|
-
eagle_patch_hook "$SETTINGS" "SessionEnd" "" \
|
|
312
|
-
"$EAGLE_MEM_DIR/hooks/session-end.sh" \
|
|
313
|
-
"SessionEnd hook"
|
|
314
|
-
|
|
315
|
-
eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" \
|
|
316
|
-
"$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh" \
|
|
317
|
-
"UserPromptSubmit hook"
|
|
318
|
-
|
|
319
|
-
eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" \
|
|
320
|
-
"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
|
|
321
|
-
"PreToolUse hook"
|
|
322
|
-
|
|
323
|
-
# Allow agent-issued session capture to run without a permission prompt
|
|
324
|
-
if eagle_patch_permission_allow "$SETTINGS" "Bash(eagle-mem session save:*)"; then
|
|
325
|
-
eagle_ok "Capture permission ${DIM}(eagle-mem session save)${RESET}"
|
|
326
|
-
fi
|
|
286
|
+
# Single source of truth for the Claude hook set (see lib/hooks.sh);
|
|
287
|
+
# verbose mode prints a line per registration.
|
|
288
|
+
eagle_register_claude_hooks "$SETTINGS" verbose
|
|
327
289
|
fi
|
|
328
290
|
else
|
|
329
291
|
eagle_info "Claude hooks skipped ${DIM}(Claude Code not detected)${RESET}"
|