eagle-mem 4.10.10 → 4.10.11

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.10.11 Reliability Guards and Provider Fallback
8
+
9
+ This patch closes the active reliability items that remained after the Dream Cycle hotfix:
10
+
11
+ - **Command-Scoped Logs**: `scan`, `index`, and `curate` now write per-run logs under `~/.eagle-mem/runs`, preserve normal CLI output, and print the log path on command failure. Added `eagle-mem logs list|tail|show` for inspection.
12
+ - **Provider Fallback Transparency**: Provider calls now use an explicit fallback chain. `agent_cli` can fall through from a failed preferred Codex call to Claude Code when available, and provider display now shows the actual chain instead of `unknown`.
13
+ - **Read Prediction / Token Guard Scoring**: `PreToolUse` now scores repeated, large, or recently modified reads and emits a targeted read-score nudge. A configurable `read_guard.mode=block` path is available for stricter high-confidence duplicate-read gating.
14
+ - **Auto-Scan Retry Reliability**: SessionStart auto-scan/index freshness markers are now cleared when the background job fails, so failed scans do not block retries for the next 24 hours.
15
+ - **Hook Field Parsing**: Hook JSON field extraction now uses the intended unit separator in `PreToolUse`, `UserPromptSubmit`, and `Stop`, preserving clean `tool_name`, `session_id`, and `cwd` parsing.
16
+ - **Regression Coverage**: Added an isolated reliability test for provider fallback, read scoring, auto-scan failure state cleanup, and run-log creation.
17
+
18
+ ---
19
+
7
20
  ## v4.10.10 Dream Cycle Consolidation Hardening
8
21
 
9
22
  This patch closes the review findings from the multi-model Spectral pass:
package/README.md CHANGED
@@ -146,6 +146,7 @@ Eagle Mem prevents Claude from repeating past mistakes:
146
146
  | `eagle-mem search` | Search past sessions, memories, and code |
147
147
  | `eagle-mem health` | Diagnose pipeline health and background automation |
148
148
  | `eagle-mem doctor` | Verify installed runtime files, hooks, SQLite/FTS5, statusline, manifest, and drift |
149
+ | `eagle-mem logs` | Inspect command-scoped `scan`, `index`, and `curate` run logs |
149
150
  | `eagle-mem config` | View or change LLM provider and token-guard settings |
150
151
  | `eagle-mem updates` | View or change auto-update policy |
151
152
  | `eagle-mem guard` | Manage regression guardrails for files |
@@ -346,6 +347,8 @@ eagle-mem config set agent_cli.preferred current
346
347
 
347
348
  Provider preference is local-first: Ollama is auto-detected when running, then Eagle Mem can use the installed Codex/Claude CLI via `agent_cli` before falling back to explicit Anthropic/OpenAI API providers. Eagle Mem works fully without a provider — LLM features gracefully degrade to heuristic fallbacks.
348
349
 
350
+ Provider calls use an explicit fallback chain by default. For example, `agent_cli` can try the preferred/current agent first and then fall through to another supported local CLI when available. `eagle-mem config`, `eagle-mem health`, and `eagle-mem curate` display the resolved provider path so failed or unavailable agent CLIs are visible instead of appearing as `unknown`.
351
+
349
352
  RTK is configured separately from the LLM provider:
350
353
 
351
354
  ```bash
@@ -353,6 +356,8 @@ eagle-mem config set token_guard.rtk auto # default: use RTK when available
353
356
  eagle-mem config set token_guard.rtk enforce # block known raw-output commands if RTK is missing
354
357
  eagle-mem config set token_guard.rtk off # disable RTK behavior
355
358
  eagle-mem config set token_guard.raw_bash block
359
+ eagle-mem config set read_guard.mode advisory # score repeated reads and nudge
360
+ eagle-mem config set read_guard.mode block # optionally block high-confidence duplicate reads
356
361
  ```
357
362
 
358
363
  ## Long-term Direction
package/bin/eagle-mem CHANGED
@@ -22,6 +22,7 @@ case "$command" in
22
22
  search) bash "$SCRIPTS_DIR/search.sh" "$@" ;;
23
23
  health) bash "$SCRIPTS_DIR/health.sh" "$@" ;;
24
24
  doctor) bash "$SCRIPTS_DIR/doctor.sh" "$PACKAGE_DIR" "$@" ;;
25
+ logs) bash "$SCRIPTS_DIR/logs.sh" "$@" ;;
25
26
  config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
26
27
  updates) bash "$SCRIPTS_DIR/updates.sh" "$@" ;;
27
28
  statusline) "$SCRIPTS_DIR/statusline-em.sh" "$@" ;;
@@ -18,7 +18,7 @@ input=$(eagle_read_stdin)
18
18
  [ -z "$input" ] && exit 0
19
19
 
20
20
  IFS=$'\x1f' read -r session_id cwd tool_name hook_event <<< \
21
- "$(echo "$input" | jq -r '[.session_id, .cwd, .tool_name, .hook_event_name] | map(. // "") | join("")')"
21
+ "$(echo "$input" | jq -r '[.session_id, .cwd, .tool_name, .hook_event_name] | map(. // "") | join("\u001f")')"
22
22
  agent=$(eagle_agent_source_from_json "$input")
23
23
 
24
24
  if [ -z "$session_id" ]; then exit 0; fi
@@ -173,12 +173,15 @@ if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_i
173
173
  mod_dir="$EAGLE_MEM_DIR/mod-tracker"
174
174
  mkdir -p "$mod_dir" 2>/dev/null
175
175
  mod_file="$mod_dir/${session_id}"
176
- echo "$fp" >> "$mod_file"
177
- # Keep only last 3 entries — use per-process tmp to avoid
178
- # race when parallel PostToolUse hooks fire on same session
179
- if [ -f "$mod_file" ]; then
176
+ mod_lock="${mod_file}.lock"
177
+ if mkdir "$mod_lock" 2>/dev/null; then
180
178
  _mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
181
- tail -3 "$mod_file" > "$_mod_tmp" && mv "$_mod_tmp" "$mod_file" || rm -f "$_mod_tmp"
179
+ (cat "$mod_file" 2>/dev/null; printf '%s\n' "$fp") | tail -3 > "$_mod_tmp"
180
+ mv "$_mod_tmp" "$mod_file" 2>/dev/null || rm -f "$_mod_tmp"
181
+ rmdir "$mod_lock" 2>/dev/null || true
182
+ else
183
+ # If another hook is trimming, append is still safer than losing the edit.
184
+ printf '%s\n' "$fp" >> "$mod_file"
182
185
  fi
183
186
 
184
187
  # Full edit history for stuck loop detection (not truncated)
@@ -22,7 +22,7 @@ input=$(eagle_read_stdin)
22
22
  [ -z "$input" ] && exit 0
23
23
 
24
24
  IFS=$'\x1f' read -r tool_name session_id cwd <<< \
25
- "$(echo "$input" | jq -r '[.tool_name, .session_id, .cwd] | map(. // "") | join("")')"
25
+ "$(echo "$input" | jq -r '[.tool_name, .session_id, .cwd] | map(. // "") | join("\u001f")')"
26
26
  agent=$(eagle_agent_source_from_json "$input")
27
27
 
28
28
  case "$tool_name" in
@@ -295,11 +295,15 @@ Edit|Write|apply_patch)
295
295
  Read)
296
296
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
297
297
  if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
298
+ read_score=0
299
+ read_reasons=""
298
300
 
299
301
  # ─── Read-after-modify detection ──────────────────────
300
302
  mod_file="$EAGLE_MEM_DIR/mod-tracker/${session_id}"
301
303
  if [ -f "$mod_file" ] && grep -qFx -- "$fp" "$mod_file" 2>/dev/null; then
302
304
  context+="Eagle Mem recall: '$(basename "$fp")' was just edited/written — the diff is already in context from the tool output. "
305
+ read_score=$((read_score + 45))
306
+ read_reasons="${read_reasons}recently modified; "
303
307
  fi
304
308
 
305
309
  # ─── Read dedup tracker (soft nudge) ──────────────────
@@ -312,6 +316,69 @@ Read)
312
316
  if [ "$read_count" -ge 3 ]; then
313
317
  context+="Eagle Mem recall: '$(basename "$fp")' has been read ${read_count} times this session. Its contents are likely already in context."
314
318
  fi
319
+
320
+ if [ "$read_count" -ge 2 ]; then
321
+ repeat_score=$((20 + (read_count - 2) * 10))
322
+ [ "$repeat_score" -gt 40 ] && repeat_score=40
323
+ read_score=$((read_score + repeat_score))
324
+ read_reasons="${read_reasons}${read_count} reads this session; "
325
+ fi
326
+
327
+ hot_files=$(eagle_get_hot_files "$project" 2>/dev/null || true)
328
+ if [ -n "$hot_files" ]; then
329
+ fp_base=$(basename "$fp")
330
+ case ",$hot_files," in
331
+ *"/$fp_base,"*|*",$fp_base,"*)
332
+ read_score=$((read_score + 10))
333
+ read_reasons="${read_reasons}hot file; "
334
+ ;;
335
+ esac
336
+ fi
337
+
338
+ full_fp="$fp"
339
+ if [ ! -f "$full_fp" ] && [ -n "$cwd" ] && [ -f "$cwd/$fp" ]; then
340
+ full_fp="$cwd/$fp"
341
+ fi
342
+ if [ -f "$full_fp" ]; then
343
+ file_size=$(wc -c < "$full_fp" 2>/dev/null | tr -d ' ')
344
+ file_size=${file_size:-0}
345
+ if [ "$file_size" -ge 500000 ] 2>/dev/null; then
346
+ read_score=$((read_score + 20))
347
+ read_reasons="${read_reasons}large file; "
348
+ elif [ "$file_size" -ge 150000 ] 2>/dev/null; then
349
+ read_score=$((read_score + 10))
350
+ read_reasons="${read_reasons}medium-large file; "
351
+ fi
352
+ fi
353
+
354
+ [ "$read_score" -gt 100 ] && read_score=100
355
+ score_threshold=$(eagle_read_guard_score_threshold)
356
+ block_threshold=$(eagle_read_guard_block_threshold)
357
+ read_guard_mode=$(eagle_read_guard_mode)
358
+ read_reasons=${read_reasons%; }
359
+ if [ "$read_score" -ge "$score_threshold" ] 2>/dev/null; then
360
+ context+=" Eagle Mem read score: ${read_score}/100 for '$(basename "$fp")'"
361
+ [ -n "$read_reasons" ] && context+=" (${read_reasons})"
362
+ context+=". Prefer the existing context, recent diff, or targeted search unless you need exact fresh lines."
363
+ fi
364
+ if [ "$read_guard_mode" = "block" ] && [ "$read_score" -ge "$block_threshold" ] 2>/dev/null && ! eagle_raw_bash_unlock_active; then
365
+ reason="Eagle Mem blocked this high-confidence duplicate read to save context tokens.
366
+
367
+ File: $(basename "$fp")
368
+ Score: ${read_score}/100
369
+ Reason: ${read_reasons:-repeated read}
370
+
371
+ Use the existing context, run a narrower search, or bypass once with:
372
+ touch $EAGLE_RAW_BASH_UNLOCK"
373
+ jq -nc --arg reason "$reason" '{
374
+ "hookSpecificOutput":{
375
+ "hookEventName":"PreToolUse",
376
+ "permissionDecision":"deny",
377
+ "permissionDecisionReason":$reason
378
+ }
379
+ }'
380
+ exit 0
381
+ fi
315
382
  fi
316
383
  ;;
317
384
  esac
package/hooks/stop.sh CHANGED
@@ -22,7 +22,7 @@ input=$(eagle_read_stdin)
22
22
  [ -z "$input" ] && exit 0
23
23
 
24
24
  IFS=$'\x1f' read -r session_id cwd transcript_path agent_type <<< \
25
- "$(echo "$input" | jq -r '[.session_id, .cwd, .transcript_path, .agent_type] | map(. // "") | join("")')"
25
+ "$(echo "$input" | jq -r '[.session_id, .cwd, .transcript_path, .agent_type] | map(. // "") | join("\u001f")')"
26
26
  last_assistant_message=$(echo "$input" | jq -r '.last_assistant_message // empty')
27
27
  agent=$(eagle_agent_source_from_json "$input")
28
28
 
@@ -19,7 +19,7 @@ input=$(eagle_read_stdin)
19
19
  [ -z "$input" ] && exit 0
20
20
 
21
21
  IFS=$'\x1f' read -r session_id cwd <<< \
22
- "$(echo "$input" | jq -r '[.session_id, .cwd] | map(. // "") | join("")')"
22
+ "$(echo "$input" | jq -r '[.session_id, .cwd] | map(. // "") | join("\u001f")')"
23
23
  user_prompt=$(echo "$input" | jq -r '.prompt // empty')
24
24
  agent=$(eagle_agent_source_from_json "$input")
25
25
 
package/lib/common.sh CHANGED
@@ -7,6 +7,7 @@
7
7
  EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
8
8
  EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
9
9
  EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
10
+ EAGLE_RUNS_DIR="${EAGLE_RUNS_DIR:-$EAGLE_MEM_DIR/runs}"
10
11
  EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
11
12
  EAGLE_SKILLS_DIR="${EAGLE_SKILLS_DIR:-$HOME/.claude/skills}"
12
13
  EAGLE_CLAUDE_PROJECTS_DIR="${EAGLE_CLAUDE_PROJECTS_DIR:-$HOME/.claude/projects}"
@@ -112,6 +113,60 @@ eagle_log() {
112
113
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
113
114
  }
114
115
 
116
+ eagle_run_slug() {
117
+ printf '%s' "${1:-command}" \
118
+ | tr -c 'A-Za-z0-9._-' '-' \
119
+ | sed 's/^-*//;s/-*$//' \
120
+ | cut -c1-48
121
+ }
122
+
123
+ eagle_run_start() {
124
+ [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] && return 0
125
+
126
+ local command_name="$1" project="${2:-}" target="${3:-}"
127
+ local slug
128
+ slug=$(eagle_run_slug "$command_name")
129
+ [ -n "$slug" ] || slug="command"
130
+
131
+ mkdir -p "$EAGLE_RUNS_DIR" "$EAGLE_MEM_DIR" 2>/dev/null || true
132
+ EAGLE_RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-${slug}-$$"
133
+ EAGLE_RUN_LOG="$EAGLE_RUNS_DIR/${EAGLE_RUN_ID}.log"
134
+ EAGLE_RUN_COMMAND="$command_name"
135
+ EAGLE_RUN_PROJECT="$project"
136
+ EAGLE_RUN_TARGET="$target"
137
+ EAGLE_RUN_ACTIVE=1
138
+ export EAGLE_RUN_ID EAGLE_RUN_LOG EAGLE_RUN_COMMAND EAGLE_RUN_PROJECT EAGLE_RUN_TARGET EAGLE_RUN_ACTIVE
139
+
140
+ touch "$EAGLE_RUN_LOG" 2>/dev/null && chmod 600 "$EAGLE_RUN_LOG" 2>/dev/null || true
141
+ {
142
+ printf '[%s] [INFO] run_start id=%s command=%s project=%s target=%s cwd=%s\n' \
143
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$EAGLE_RUN_ID" "$command_name" "$project" "$target" "$(pwd)"
144
+ } >> "$EAGLE_RUN_LOG" 2>/dev/null || true
145
+
146
+ # Keep normal CLI output intact while also preserving a command-scoped log.
147
+ exec > >(tee -a "$EAGLE_RUN_LOG") 2> >(tee -a "$EAGLE_RUN_LOG" >&2)
148
+ eagle_log "INFO" "Run started: id=$EAGLE_RUN_ID command=$command_name project=$project log=$EAGLE_RUN_LOG"
149
+ }
150
+
151
+ eagle_run_step() {
152
+ [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] || return 0
153
+ printf '[%s] [STEP] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$EAGLE_RUN_LOG" 2>/dev/null || true
154
+ }
155
+
156
+ eagle_run_finish() {
157
+ [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] || return 0
158
+ local rc="${1:-0}" line="${2:-unknown}"
159
+ printf '[%s] [INFO] run_finish id=%s rc=%s line=%s\n' \
160
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$EAGLE_RUN_ID" "$rc" "$line" >> "$EAGLE_RUN_LOG" 2>/dev/null || true
161
+ if [ "$rc" -ne 0 ] 2>/dev/null; then
162
+ eagle_log "ERROR" "Run failed: id=$EAGLE_RUN_ID command=${EAGLE_RUN_COMMAND:-unknown} rc=$rc line=$line log=${EAGLE_RUN_LOG:-}"
163
+ printf '\nEagle Mem command failed: %s (exit %s, line %s)\nLog: %s\n' \
164
+ "${EAGLE_RUN_COMMAND:-unknown}" "$rc" "$line" "${EAGLE_RUN_LOG:-unknown}" >&2
165
+ else
166
+ eagle_log "INFO" "Run finished: id=$EAGLE_RUN_ID command=${EAGLE_RUN_COMMAND:-unknown} rc=0"
167
+ fi
168
+ }
169
+
115
170
  eagle_normalize_project_path() {
116
171
  local path="${1:-$(pwd)}"
117
172
 
@@ -925,6 +980,36 @@ eagle_token_guard_raw_bash_mode() {
925
980
  fi
926
981
  }
927
982
 
983
+ eagle_read_guard_mode() {
984
+ if declare -F eagle_config_get >/dev/null 2>&1; then
985
+ eagle_config_get "read_guard" "mode" "advisory"
986
+ else
987
+ eagle_config_get_light "read_guard" "mode" "advisory"
988
+ fi
989
+ }
990
+
991
+ eagle_read_guard_score_threshold() {
992
+ local threshold
993
+ if declare -F eagle_config_get >/dev/null 2>&1; then
994
+ threshold=$(eagle_config_get "read_guard" "score_threshold" "70")
995
+ else
996
+ threshold=$(eagle_config_get_light "read_guard" "score_threshold" "70")
997
+ fi
998
+ case "$threshold" in *[!0-9]*|"") threshold=70 ;; esac
999
+ printf '%s\n' "$threshold"
1000
+ }
1001
+
1002
+ eagle_read_guard_block_threshold() {
1003
+ local threshold
1004
+ if declare -F eagle_config_get >/dev/null 2>&1; then
1005
+ threshold=$(eagle_config_get "read_guard" "block_threshold" "90")
1006
+ else
1007
+ threshold=$(eagle_config_get_light "read_guard" "block_threshold" "90")
1008
+ fi
1009
+ case "$threshold" in *[!0-9]*|"") threshold=90 ;; esac
1010
+ printf '%s\n' "$threshold"
1011
+ }
1012
+
928
1013
  eagle_raw_output_command_needs_guard() {
929
1014
  local cmd="$1"
930
1015
  local first
@@ -12,18 +12,23 @@ _eagle_state_slug() {
12
12
  printf '%s' "$1" | shasum | cut -c1-12
13
13
  }
14
14
 
15
+ _eagle_state_file() {
16
+ local key="$1" project="$2"
17
+ local safe_project; safe_project=$(_eagle_state_slug "$project")
18
+ printf '%s/%s-%s\n' "$_state_dir" "$key" "$safe_project"
19
+ }
20
+
15
21
  _eagle_state_fresh() {
16
22
  local key="$1" project="$2" max_age_days="${3:-1}"
17
- local safe_project; safe_project=$(_eagle_state_slug "$project")
18
- local state_file="$_state_dir/${key}-${safe_project}"
23
+ local state_file; state_file=$(_eagle_state_file "$key" "$project")
19
24
  [ -f "$state_file" ] && [ -z "$(find "$state_file" -mtime +${max_age_days} 2>/dev/null)" ]
20
25
  }
21
26
 
22
27
  _eagle_state_touch() {
23
28
  local key="$1" project="$2"
24
- local safe_project; safe_project=$(_eagle_state_slug "$project")
29
+ local state_file; state_file=$(_eagle_state_file "$key" "$project")
25
30
  mkdir -p "$_state_dir" 2>/dev/null
26
- touch "$_state_dir/${key}-${safe_project}"
31
+ touch "$state_file"
27
32
  }
28
33
 
29
34
  eagle_sessionstart_auto_provision() {
@@ -51,15 +56,63 @@ eagle_sessionstart_auto_provision() {
51
56
  eagle_log "INFO" "SessionStart: first-session provision — scan then index"
52
57
  _eagle_state_touch "scan" "$project"
53
58
  _eagle_state_touch "index" "$project"
54
- nohup bash -c "bash '$scripts_dir/scan.sh' '$cwd' >> '$EAGLE_MEM_LOG' 2>&1; bash '$scripts_dir/index.sh' '$cwd' >> '$EAGLE_MEM_LOG' 2>&1" &
59
+ scan_state=$(_eagle_state_file "scan" "$project")
60
+ index_state=$(_eagle_state_file "index" "$project")
61
+ nohup bash -c '
62
+ scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4"; index_state="$5"
63
+ bash "$scripts_dir/scan.sh" "$cwd" >> "$log" 2>&1
64
+ scan_rc=$?
65
+ if [ "$scan_rc" -eq 0 ]; then
66
+ touch "$scan_state" 2>/dev/null || true
67
+ else
68
+ rm -f "$scan_state" 2>/dev/null || true
69
+ printf "[%s] [ERROR] SessionStart: auto-scan failed rc=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$scan_rc" >> "$log" 2>/dev/null || true
70
+ fi
71
+
72
+ bash "$scripts_dir/index.sh" "$cwd" >> "$log" 2>&1
73
+ index_rc=$?
74
+ if [ "$index_rc" -eq 0 ]; then
75
+ touch "$index_state" 2>/dev/null || true
76
+ else
77
+ rm -f "$index_state" 2>/dev/null || true
78
+ printf "[%s] [ERROR] SessionStart: auto-index failed rc=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$index_rc" >> "$log" 2>/dev/null || true
79
+ fi
80
+
81
+ [ "$scan_rc" -eq 0 ] && exit "$index_rc"
82
+ exit "$scan_rc"
83
+ ' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" "$index_state" &
55
84
  elif [ "$needs_scan" = true ]; then
56
85
  eagle_log "INFO" "SessionStart: auto-scan triggered"
57
86
  _eagle_state_touch "scan" "$project"
58
- nohup bash "$scripts_dir/scan.sh" "$cwd" >> "$EAGLE_MEM_LOG" 2>&1 &
87
+ scan_state=$(_eagle_state_file "scan" "$project")
88
+ nohup bash -c '
89
+ scripts_dir="$1"; cwd="$2"; log="$3"; scan_state="$4"
90
+ bash "$scripts_dir/scan.sh" "$cwd" >> "$log" 2>&1
91
+ rc=$?
92
+ if [ "$rc" -eq 0 ]; then
93
+ touch "$scan_state" 2>/dev/null || true
94
+ else
95
+ rm -f "$scan_state" 2>/dev/null || true
96
+ 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
+ fi
98
+ exit "$rc"
99
+ ' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$scan_state" &
59
100
  elif [ "$needs_index" = true ]; then
60
101
  eagle_log "INFO" "SessionStart: auto-index triggered"
61
102
  _eagle_state_touch "index" "$project"
62
- nohup bash "$scripts_dir/index.sh" "$cwd" >> "$EAGLE_MEM_LOG" 2>&1 &
103
+ index_state=$(_eagle_state_file "index" "$project")
104
+ nohup bash -c '
105
+ scripts_dir="$1"; cwd="$2"; log="$3"; index_state="$4"
106
+ bash "$scripts_dir/index.sh" "$cwd" >> "$log" 2>&1
107
+ rc=$?
108
+ if [ "$rc" -eq 0 ]; then
109
+ touch "$index_state" 2>/dev/null || true
110
+ else
111
+ rm -f "$index_state" 2>/dev/null || true
112
+ 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
+ fi
114
+ exit "$rc"
115
+ ' eagle-auto "$scripts_dir" "$cwd" "$EAGLE_MEM_LOG" "$index_state" &
63
116
  fi
64
117
  }
65
118
 
package/lib/provider.sh CHANGED
@@ -152,6 +152,8 @@ eagle_config_init() {
152
152
  # Which LLM provider to use for the curator and analysis features
153
153
  # Options: "ollama" (free, local), "agent_cli" (Codex/Claude CLI auth), "anthropic", "openai"
154
154
  type = "$provider"
155
+ # "auto" retries local/agent/API fallbacks when the primary provider fails.
156
+ fallback = "auto"
155
157
 
156
158
  [ollama]
157
159
  url = "$ollama_url"
@@ -207,6 +209,12 @@ rtk = "auto"
207
209
  # "allow" keeps RTK advisory only.
208
210
  raw_bash = "block"
209
211
 
212
+ [read_guard]
213
+ # advisory = score repeated/expensive reads and nudge; block = deny high-score rereads.
214
+ mode = "advisory"
215
+ score_threshold = "70"
216
+ block_threshold = "90"
217
+
210
218
  [redaction]
211
219
  # Additional secret patterns (regex) beyond built-in defaults
212
220
  # extra_patterns = ["MY_CUSTOM_SECRET_.*"]
@@ -223,14 +231,39 @@ eagle_llm_call() {
223
231
  local system_prompt="${2:-You are a helpful assistant that analyzes software development sessions.}"
224
232
  local max_tokens="${3:-1024}"
225
233
 
226
- local provider
234
+ local provider chain candidate result rc tried=0
227
235
  provider=$(eagle_config_get "provider" "type" "none")
236
+ chain=$(_eagle_provider_chain "$provider")
237
+
238
+ if [ -z "$chain" ]; then
239
+ eagle_log "ERROR" "No LLM provider configured or available. Run: eagle-mem config"
240
+ return 1
241
+ fi
242
+
243
+ while IFS= read -r candidate; do
244
+ [ -z "$candidate" ] && continue
245
+ tried=$((tried + 1))
246
+ result=$(_eagle_call_provider_once "$candidate" "$prompt" "$system_prompt" "$max_tokens")
247
+ rc=$?
248
+ if [ "$rc" -eq 0 ] && [ -n "$result" ]; then
249
+ [ "$candidate" != "$provider" ] && eagle_log "INFO" "LLM provider fallback succeeded: primary=$provider used=$candidate"
250
+ printf '%s\n' "$result"
251
+ return 0
252
+ fi
253
+ eagle_log "WARN" "LLM provider candidate failed: primary=$provider candidate=$candidate rc=$rc"
254
+ done <<< "$chain"
255
+
256
+ eagle_log "ERROR" "All LLM provider candidates failed: primary=$provider tried=$tried"
257
+ return 1
258
+ }
228
259
 
260
+ _eagle_call_provider_once() {
261
+ local provider="$1" prompt="$2" system_prompt="$3" max_tokens="$4"
229
262
  case "$provider" in
230
- ollama) _eagle_call_ollama "$prompt" "$system_prompt" "$max_tokens" ;;
263
+ ollama) _eagle_call_ollama "$prompt" "$system_prompt" "$max_tokens" ;;
231
264
  agent_cli) _eagle_call_agent_cli "$prompt" "$system_prompt" "$max_tokens" ;;
232
265
  anthropic) _eagle_call_anthropic "$prompt" "$system_prompt" "$max_tokens" ;;
233
- openai) _eagle_call_openai "$prompt" "$system_prompt" "$max_tokens" ;;
266
+ openai) _eagle_call_openai "$prompt" "$system_prompt" "$max_tokens" ;;
234
267
  none)
235
268
  eagle_log "ERROR" "No LLM provider configured. Run: eagle-mem config"
236
269
  return 1
@@ -242,6 +275,79 @@ eagle_llm_call() {
242
275
  esac
243
276
  }
244
277
 
278
+ _eagle_provider_chain() {
279
+ local primary="${1:-none}" fallback
280
+ fallback=$(eagle_config_get "provider" "fallback" "auto")
281
+
282
+ local chain=""
283
+ _eagle_provider_chain_add() {
284
+ local candidate="$1" required="${2:-available}"
285
+ [ -n "$candidate" ] || return 0
286
+ case " $chain " in *" $candidate "*) return 0 ;; esac
287
+ if [ "$required" = "available" ] && ! _eagle_provider_available "$candidate"; then
288
+ return 0
289
+ fi
290
+ chain="${chain}${chain:+ }$candidate"
291
+ }
292
+
293
+ if [ "$primary" != "none" ]; then
294
+ _eagle_provider_chain_add "$primary" "always"
295
+ fi
296
+
297
+ if [ "$fallback" != "off" ]; then
298
+ case "$primary" in
299
+ ollama) _eagle_provider_chain_add "agent_cli"; _eagle_provider_chain_add "anthropic"; _eagle_provider_chain_add "openai" ;;
300
+ agent_cli) _eagle_provider_chain_add "ollama"; _eagle_provider_chain_add "anthropic"; _eagle_provider_chain_add "openai" ;;
301
+ anthropic) _eagle_provider_chain_add "ollama"; _eagle_provider_chain_add "agent_cli"; _eagle_provider_chain_add "openai" ;;
302
+ openai) _eagle_provider_chain_add "ollama"; _eagle_provider_chain_add "agent_cli"; _eagle_provider_chain_add "anthropic" ;;
303
+ none|"") _eagle_provider_chain_add "ollama"; _eagle_provider_chain_add "agent_cli"; _eagle_provider_chain_add "anthropic"; _eagle_provider_chain_add "openai" ;;
304
+ esac
305
+ fi
306
+
307
+ printf '%s\n' "$chain" | tr ' ' '\n' | sed '/^$/d'
308
+ }
309
+
310
+ _eagle_provider_available() {
311
+ case "$1" in
312
+ ollama)
313
+ [ -n "$(eagle_detect_ollama "$(eagle_config_get "ollama" "url" "$EAGLE_DEFAULT_OLLAMA_URL")" 2>/dev/null || true)" ]
314
+ ;;
315
+ agent_cli)
316
+ [ -n "$(_eagle_agent_cli_target_chain)" ]
317
+ ;;
318
+ anthropic)
319
+ [ -n "${ANTHROPIC_API_KEY:-}" ]
320
+ ;;
321
+ openai)
322
+ [ -n "${OPENAI_API_KEY:-}" ]
323
+ ;;
324
+ *) return 1 ;;
325
+ esac
326
+ }
327
+
328
+ _eagle_provider_model_label() {
329
+ case "$1" in
330
+ ollama) printf 'ollama:%s' "$(eagle_config_get "ollama" "model" "mistral")" ;;
331
+ agent_cli) printf 'agent_cli:%s' "$(_eagle_agent_cli_target_summary)" ;;
332
+ anthropic) printf 'anthropic:%s' "$(eagle_config_get "anthropic" "model" "claude-haiku-4-5-20251001")" ;;
333
+ openai) printf 'openai:%s' "$(eagle_config_get "openai" "model" "gpt-4o-mini")" ;;
334
+ *) printf '%s' "$1" ;;
335
+ esac
336
+ }
337
+
338
+ eagle_llm_provider_label() {
339
+ local provider chain labels="" candidate sep=""
340
+ provider=$(eagle_config_get "provider" "type" "none")
341
+ chain=$(_eagle_provider_chain "$provider")
342
+ while IFS= read -r candidate; do
343
+ [ -z "$candidate" ] && continue
344
+ labels="${labels}${sep}$(_eagle_provider_model_label "$candidate")"
345
+ sep=" -> "
346
+ done <<< "$chain"
347
+ [ -n "$labels" ] || labels="none"
348
+ printf '%s\n' "$labels"
349
+ }
350
+
245
351
  _eagle_call_ollama() {
246
352
  local prompt="$1" system="$2" max_tokens="$3"
247
353
  local url model
@@ -281,35 +387,57 @@ _eagle_call_ollama() {
281
387
  }
282
388
 
283
389
  _eagle_agent_cli_target() {
284
- local preferred
390
+ local first
391
+ first=$(_eagle_agent_cli_target_chain | sed -n '1p')
392
+ if [ -n "$first" ]; then
393
+ printf '%s\n' "$first"
394
+ else
395
+ printf 'none\n'
396
+ fi
397
+ }
398
+
399
+ _eagle_agent_cli_target_chain() {
400
+ local preferred current preferred_target targets="" candidate
285
401
  preferred=$(eagle_config_get "agent_cli" "preferred" "current")
402
+ current=""
403
+ if [ -n "${EAGLE_AGENT_SOURCE:-${EAGLE_AGENT:-}}" ]; then
404
+ current=$(eagle_agent_source)
405
+ fi
286
406
 
287
407
  case "$preferred" in
288
- codex|openai-codex) echo "codex"; return 0 ;;
289
- claude|claude-code|cloud-code) echo "claude-code"; return 0 ;;
290
- auto)
291
- if [ -n "${EAGLE_AGENT_SOURCE:-${EAGLE_AGENT:-}}" ]; then
292
- eagle_agent_source
293
- elif command -v codex &>/dev/null; then
294
- echo "codex"
295
- elif command -v claude &>/dev/null; then
296
- echo "claude-code"
297
- else
298
- echo "none"
299
- fi
300
- ;;
301
- current|*)
302
- if [ -n "${EAGLE_AGENT_SOURCE:-${EAGLE_AGENT:-}}" ]; then
303
- eagle_agent_source
304
- elif command -v codex &>/dev/null; then
305
- echo "codex"
306
- elif command -v claude &>/dev/null; then
307
- echo "claude-code"
308
- else
309
- echo "none"
310
- fi
311
- ;;
408
+ codex|openai-codex) preferred_target="codex" ;;
409
+ claude|claude-code|cloud-code) preferred_target="claude-code" ;;
410
+ current) preferred_target="$current" ;;
411
+ auto|"") preferred_target="" ;;
412
+ *) preferred_target="$preferred" ;;
312
413
  esac
414
+
415
+ for candidate in "$preferred_target" "$current" codex claude-code; do
416
+ case "$candidate" in
417
+ codex|claude-code) ;;
418
+ *) continue ;;
419
+ esac
420
+ case "$candidate" in
421
+ codex) command -v codex >/dev/null 2>&1 || continue ;;
422
+ claude-code) command -v claude >/dev/null 2>&1 || continue ;;
423
+ esac
424
+ case " $targets " in *" $candidate "*) continue ;; esac
425
+ targets="${targets}${targets:+ }$candidate"
426
+ done
427
+
428
+ printf '%s\n' "$targets" | tr ' ' '\n' | sed '/^$/d'
429
+ }
430
+
431
+ _eagle_agent_cli_target_summary() {
432
+ local chain summary="" sep="" target
433
+ chain=$(_eagle_agent_cli_target_chain)
434
+ while IFS= read -r target; do
435
+ [ -z "$target" ] && continue
436
+ summary="${summary}${sep}${target}"
437
+ sep=" -> "
438
+ done <<< "$chain"
439
+ [ -n "$summary" ] || summary="none"
440
+ printf '%s\n' "$summary"
313
441
  }
314
442
 
315
443
  _eagle_agent_cli_prompt_file() {
@@ -327,17 +455,32 @@ _eagle_agent_cli_prompt_file() {
327
455
 
328
456
  _eagle_call_agent_cli() {
329
457
  local prompt="$1" system="$2" max_tokens="$3"
330
- local target
331
- target=$(_eagle_agent_cli_target)
458
+ local target targets result rc tried=0
459
+ targets=$(_eagle_agent_cli_target_chain)
332
460
 
333
- case "$target" in
334
- codex) _eagle_call_codex_cli "$prompt" "$system" "$max_tokens" ;;
335
- claude-code) _eagle_call_claude_cli "$prompt" "$system" "$max_tokens" ;;
336
- *)
337
- eagle_log "ERROR" "agent_cli provider unavailable: no Codex or Claude CLI found"
338
- return 1
339
- ;;
340
- esac
461
+ if [ -z "$targets" ]; then
462
+ eagle_log "ERROR" "agent_cli provider unavailable: no supported Codex or Claude CLI found"
463
+ return 1
464
+ fi
465
+
466
+ while IFS= read -r target; do
467
+ [ -z "$target" ] && continue
468
+ tried=$((tried + 1))
469
+ case "$target" in
470
+ codex) result=$(_eagle_call_codex_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
471
+ claude-code) result=$(_eagle_call_claude_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
472
+ *) rc=1; result="" ;;
473
+ esac
474
+ if [ "$rc" -eq 0 ] && [ -n "$result" ]; then
475
+ [ "$tried" -gt 1 ] && eagle_log "INFO" "agent_cli fallback succeeded with $target"
476
+ printf '%s\n' "$result"
477
+ return 0
478
+ fi
479
+ eagle_log "WARN" "agent_cli target failed: target=$target rc=$rc"
480
+ done <<< "$targets"
481
+
482
+ eagle_log "ERROR" "All agent_cli targets failed: targets=$(printf '%s' "$targets" | tr '\n' ',')"
483
+ return 1
341
484
  }
342
485
 
343
486
  _eagle_call_codex_cli() {
@@ -539,16 +682,19 @@ eagle_show_config() {
539
682
  return 1
540
683
  fi
541
684
 
542
- local provider model
685
+ local provider model fallback
543
686
  provider=$(eagle_config_get "provider" "type" "none")
687
+ fallback=$(eagle_config_get "provider" "fallback" "auto")
544
688
  if [ "$provider" = "agent_cli" ]; then
545
- model=$(_eagle_agent_cli_target)
689
+ model=$(_eagle_agent_cli_target_summary)
546
690
  else
547
691
  model=$(eagle_config_get "$provider" "model" "unknown")
548
692
  fi
549
693
 
550
694
  echo "Provider: $provider"
551
695
  echo "Model: $model"
696
+ echo "Fallback: $fallback"
697
+ echo "Chain: $(eagle_llm_provider_label)"
552
698
 
553
699
  if [ "$provider" = "ollama" ]; then
554
700
  local url
@@ -564,6 +710,7 @@ eagle_show_config() {
564
710
  fi
565
711
  elif [ "$provider" = "agent_cli" ]; then
566
712
  echo "Preferred: $(eagle_config_get "agent_cli" "preferred" "current")"
713
+ echo "Targets: $(_eagle_agent_cli_target_summary)"
567
714
  echo "Codex: $(command -v codex 2>/dev/null || echo "not found")"
568
715
  echo "Claude: $(command -v claude 2>/dev/null || echo "not found")"
569
716
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.10",
3
+ "version": "4.10.11",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/config.sh CHANGED
@@ -36,6 +36,7 @@ show_help() {
36
36
  echo -e " eagle-mem config set updates.allow patch"
37
37
  echo -e " eagle-mem config set token_guard.rtk enforce"
38
38
  echo -e " eagle-mem config set token_guard.raw_bash block"
39
+ echo -e " eagle-mem config set read_guard.mode advisory"
39
40
  echo ""
40
41
  exit 0
41
42
  }
@@ -72,6 +73,7 @@ case "$subcommand" in
72
73
  eagle_info " eagle-mem config set anthropic.model claude-haiku-4-5-20251001"
73
74
  eagle_info " eagle-mem config set token_guard.rtk enforce"
74
75
  eagle_info " eagle-mem config set token_guard.raw_bash block"
76
+ eagle_info " eagle-mem config set read_guard.mode advisory"
75
77
  exit 1
76
78
  fi
77
79
  section="${key%%.*}"
package/scripts/curate.sh CHANGED
@@ -100,19 +100,27 @@ fi
100
100
 
101
101
  p_esc=$(eagle_sql_escape "$project")
102
102
 
103
+ cleanup_curate() {
104
+ local rc=$?
105
+ eagle_run_finish "$rc" "$LINENO"
106
+ }
107
+ eagle_run_start "curate" "$project" "$(pwd)"
108
+ trap cleanup_curate EXIT
109
+
103
110
  # Verify provider is configured
104
111
  provider=$(eagle_config_get "provider" "type" "none")
105
112
  if [ "$provider" = "none" ]; then
106
113
  eagle_err "No LLM provider configured. Run: eagle-mem config init"
107
114
  exit 1
108
115
  fi
109
- eagle_info "Provider: $provider ($(eagle_config_get "$provider" "model" "unknown"))"
116
+ eagle_info "Provider: $(eagle_llm_provider_label)"
110
117
  eagle_info "Project: $project"
111
118
  [ "$DRY_RUN" -eq 1 ] && eagle_info "Dry run — no changes will be made"
112
119
  echo ""
113
120
 
114
121
  # ─── 1. Analyze gotchas for promotion ─────────────────────
115
122
 
123
+ eagle_run_step "promote_gotchas"
116
124
  eagle_info "Analyzing gotchas for promotion..."
117
125
 
118
126
  recent_gotchas=$(eagle_db "SELECT gotchas, created_at
package/scripts/health.sh CHANGED
@@ -136,14 +136,11 @@ max_score=$((max_score + 15))
136
136
 
137
137
  provider=$(eagle_config_get "provider" "type" "none")
138
138
  if [ "$provider" != "none" ]; then
139
- if [ "$provider" = "agent_cli" ]; then
140
- model=$(_eagle_agent_cli_target)
141
- else
142
- model=$(eagle_config_get "$provider" "model" "default")
143
- fi
144
- eagle_ok "Provider: ${provider} (${model})"
139
+ provider_chain=$(eagle_llm_provider_label)
140
+ eagle_ok "Provider: ${provider_chain}"
145
141
  score=$((score + 15))
146
142
  else
143
+ provider_chain="none"
147
144
  eagle_fail "No LLM provider — curator and enrichment disabled"
148
145
  issues+=("Configure a provider: eagle-mem config init")
149
146
  fi
@@ -321,6 +318,7 @@ if [ "$JSON_OUT" -eq 1 ]; then
321
318
  --argjson enriched_summaries "${enriched_summaries:-0}" \
322
319
  --argjson features "${feature_count:-0}" \
323
320
  --arg provider "$provider" \
321
+ --arg provider_chain "$provider_chain" \
324
322
  --arg token_guard_rtk "$rtk_mode" \
325
323
  --arg token_guard_raw_bash "$raw_bash_mode" \
326
324
  --arg rtk_bin "${rtk_bin:-}" \
@@ -345,7 +343,7 @@ if [ "$JSON_OUT" -eq 1 ]; then
345
343
  '{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
346
344
  capture:{sessions:$total_sessions, summaries:$total_summaries, heuristic:$heuristic_summaries},
347
345
  enrichment:$enriched_summaries,
348
- features:$features, provider:$provider,
346
+ features:$features, provider:$provider, provider_chain:$provider_chain,
349
347
  token_guard:{rtk:$token_guard_rtk, raw_bash:$token_guard_raw_bash, rtk_bin:$rtk_bin},
350
348
  orchestration:{
351
349
  route:$orchestration_route,
package/scripts/help.sh CHANGED
@@ -23,6 +23,7 @@ echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"
23
23
  echo -e " ${CYAN}search${RESET} Search past sessions, memories, and code"
24
24
  echo -e " ${CYAN}health${RESET} Diagnose pipeline health and background automation"
25
25
  echo -e " ${CYAN}doctor${RESET} Show install footprint, hooks, SQLite, manifest, and runtime drift"
26
+ echo -e " ${CYAN}logs${RESET} Inspect command-scoped scan/index/curate logs"
26
27
  echo -e " ${CYAN}updates${RESET} Auto-update status and policy"
27
28
  echo -e " ${CYAN}overview${RESET} Build or view project overview"
28
29
  echo -e " ${CYAN}session${RESET} Save a manual session summary"
package/scripts/index.sh CHANGED
@@ -40,6 +40,15 @@ TARGET_DIR="${args[0]:-.}"
40
40
  TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
41
41
  PROJECT=$(eagle_project_from_cwd "$TARGET_DIR")
42
42
 
43
+ TMPDIR_IDX=""
44
+ cleanup_index() {
45
+ local rc=$?
46
+ [ -n "${TMPDIR_IDX:-}" ] && rm -rf "$TMPDIR_IDX" 2>/dev/null || true
47
+ eagle_run_finish "$rc" "$LINENO"
48
+ }
49
+ eagle_run_start "index" "$PROJECT" "$TARGET_DIR"
50
+ trap cleanup_index EXIT
51
+
43
52
  CHUNK_SIZE="${EAGLE_MEM_CHUNK_SIZE:-80}"
44
53
  if ! [[ "$CHUNK_SIZE" =~ ^[0-9]+$ ]] || [ "$CHUNK_SIZE" -lt 1 ]; then
45
54
  CHUNK_SIZE=80
@@ -111,10 +120,10 @@ sql_literal_expr() {
111
120
  # ─── Collect files ─────────────────────────────────────────
112
121
 
113
122
  TMPDIR_IDX=$(mktemp -d)
114
- trap 'rm -rf "$TMPDIR_IDX"' EXIT
115
123
 
116
124
  ALL_FILES="$TMPDIR_IDX/all_files"
117
125
 
126
+ eagle_run_step "collect_files"
118
127
  eagle_collect_files "$TARGET_DIR" "$ALL_FILES"
119
128
 
120
129
  # Filter to source files only, skip large files
@@ -147,6 +156,7 @@ NEEDS_INDEX="$TMPDIR_IDX/needs_index"
147
156
  skipped_count=0
148
157
 
149
158
  if [ "$force" = true ]; then
159
+ eagle_run_step "force_clear_index_state"
150
160
  eagle_info "Force rebuild requested: clearing chunks, declarations, and import edges"
151
161
  eagle_graph_clear_index_state "$PROJECT"
152
162
  fi
@@ -183,6 +193,7 @@ if [ "$needs_count" -eq 0 ]; then
183
193
  fi
184
194
 
185
195
  eagle_info "$needs_count files to index"
196
+ eagle_run_step "index_files count=$needs_count"
186
197
 
187
198
  # ─── Chunk and index files ─────────────────────────────────
188
199
 
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Command Run Logs
4
+ # ═══════════════════════════════════════════════════════════
5
+ set -euo pipefail
6
+
7
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ LIB_DIR="$SCRIPTS_DIR/../lib"
9
+
10
+ . "$SCRIPTS_DIR/style.sh"
11
+ . "$LIB_DIR/common.sh"
12
+
13
+ cmd="${1:-list}"
14
+ [ $# -gt 0 ] && shift || true
15
+
16
+ show_help() {
17
+ cat <<EOF
18
+ Usage: eagle-mem logs [list|tail|show] [run-id-or-path]
19
+
20
+ Commands:
21
+ list Show recent command-scoped run logs
22
+ tail [id|path] Tail a run log, or the latest run log when omitted
23
+ show <id|path> Print a run log
24
+ EOF
25
+ }
26
+
27
+ resolve_log_path() {
28
+ local ref="${1:-}"
29
+ if [ -z "$ref" ]; then
30
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | sed -n '1p'
31
+ return 0
32
+ fi
33
+ if [ -f "$ref" ]; then
34
+ printf '%s\n' "$ref"
35
+ return 0
36
+ fi
37
+ if [ -f "$EAGLE_RUNS_DIR/$ref" ]; then
38
+ printf '%s\n' "$EAGLE_RUNS_DIR/$ref"
39
+ return 0
40
+ fi
41
+ if [ -f "$EAGLE_RUNS_DIR/$ref.log" ]; then
42
+ printf '%s\n' "$EAGLE_RUNS_DIR/$ref.log"
43
+ return 0
44
+ fi
45
+ return 1
46
+ }
47
+
48
+ case "$cmd" in
49
+ -h|--help|help)
50
+ show_help
51
+ ;;
52
+ list)
53
+ eagle_header "Run Logs"
54
+ if ! ls "$EAGLE_RUNS_DIR"/*.log >/dev/null 2>&1; then
55
+ eagle_info "No run logs found yet"
56
+ eagle_dim "Run eagle-mem scan, index, or curate to create command-scoped logs."
57
+ exit 0
58
+ fi
59
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | head -20 | while IFS= read -r log_path; do
60
+ first_line=$(sed -n '1p' "$log_path" 2>/dev/null)
61
+ run_id=$(basename "$log_path" .log)
62
+ printf ' %s %s\n' "$run_id" "$first_line"
63
+ done
64
+ ;;
65
+ tail)
66
+ log_path=$(resolve_log_path "${1:-}") || {
67
+ eagle_err "Run log not found"
68
+ exit 1
69
+ }
70
+ tail -n "${EAGLE_LOG_TAIL_LINES:-80}" "$log_path"
71
+ ;;
72
+ show)
73
+ log_path=$(resolve_log_path "${1:-}") || {
74
+ eagle_err "Run log not found"
75
+ exit 1
76
+ }
77
+ cat "$log_path"
78
+ ;;
79
+ *)
80
+ eagle_err "Unknown logs command: $cmd"
81
+ show_help
82
+ exit 1
83
+ ;;
84
+ esac
package/scripts/scan.sh CHANGED
@@ -29,6 +29,15 @@ TARGET_DIR="${args[0]:-.}"
29
29
  TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
30
30
  PROJECT=$(eagle_project_from_cwd "$TARGET_DIR")
31
31
 
32
+ TMPFILE=""
33
+ cleanup_scan() {
34
+ local rc=$?
35
+ [ -n "${TMPFILE:-}" ] && rm -f "$TMPFILE" "${TMPFILE}.analysis" 2>/dev/null || true
36
+ eagle_run_finish "$rc" "$LINENO"
37
+ }
38
+ eagle_run_start "scan" "$PROJECT" "$TARGET_DIR"
39
+ trap cleanup_scan EXIT
40
+
32
41
  eagle_header "Scan"
33
42
  eagle_info "Scanning ${BOLD}$PROJECT${RESET} at $TARGET_DIR"
34
43
  echo ""
@@ -52,8 +61,8 @@ if git -C "$TARGET_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
52
61
  fi
53
62
 
54
63
  TMPFILE=$(mktemp)
55
- trap 'rm -f "$TMPFILE"' EXIT
56
64
 
65
+ eagle_run_step "collect_files"
57
66
  eagle_collect_files "$TARGET_DIR" "$TMPFILE"
58
67
 
59
68
  total_files=$(wc -l < "$TMPFILE" | tr -d ' ')
@@ -67,6 +76,7 @@ eagle_ok "$total_files files found"
67
76
 
68
77
  # ─── Language breakdown (bash 3 compatible — no assoc arrays) ──
69
78
 
79
+ eagle_run_step "language_breakdown"
70
80
  while IFS= read -r file; do
71
81
  ext="${file##*.}"
72
82
  [ "$ext" = "$file" ] && continue
package/scripts/test.sh CHANGED
@@ -53,6 +53,7 @@ run_check "Installer And Updater (install / update syntax)" "bash -n \"$SCRIPTS_
53
53
  run_check "Code Scan And Index (scan / index syntax)" "bash -n \"$SCRIPTS_DIR/scan.sh\" && bash -n \"$SCRIPTS_DIR/index.sh\""
54
54
  run_check "Graph Memory Rebuild (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_graph_memory.sh\""
55
55
  run_check "Dream Cycle Memory Graph Wiring (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_curate_graph_memories.sh\""
56
+ run_check "Reliability Guards (provider fallback, logs, autoscan, read scoring)" "bash \"$SCRIPTS_DIR/../tests/test_reliability_guards.sh\""
56
57
 
57
58
  echo ""
58
59
  if [ "$errors" -eq 0 ]; then
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ tmp_dir=$(mktemp -d)
6
+ trap 'rm -rf "$tmp_dir"' EXIT
7
+
8
+ assert_contains() {
9
+ local haystack="$1" needle="$2" message="$3"
10
+ case "$haystack" in
11
+ *"$needle"*) ;;
12
+ *)
13
+ echo "$message" >&2
14
+ echo "Expected to find: $needle" >&2
15
+ echo "Actual: $haystack" >&2
16
+ exit 1
17
+ ;;
18
+ esac
19
+ }
20
+
21
+ # Provider fallback: a failed preferred Codex CLI should fall through to Claude.
22
+ provider_home="$tmp_dir/provider-home"
23
+ fake_bin="$tmp_dir/bin"
24
+ mkdir -p "$provider_home" "$fake_bin"
25
+ cat > "$fake_bin/codex" <<'SH'
26
+ #!/usr/bin/env bash
27
+ exit 42
28
+ SH
29
+ cat > "$fake_bin/claude" <<'SH'
30
+ #!/usr/bin/env bash
31
+ printf 'claude fallback ok\n'
32
+ SH
33
+ chmod +x "$fake_bin/codex" "$fake_bin/claude"
34
+ cat > "$provider_home/config.toml" <<'TOML'
35
+ [provider]
36
+ type = "agent_cli"
37
+ fallback = "auto"
38
+
39
+ [agent_cli]
40
+ preferred = "codex"
41
+ codex_model = ""
42
+ claude_model = ""
43
+ TOML
44
+ provider_result=$(EAGLE_MEM_DIR="$provider_home" PATH="$fake_bin:$PATH" bash -c "
45
+ . '$ROOT_DIR/lib/common.sh'
46
+ . '$ROOT_DIR/lib/provider.sh'
47
+ eagle_llm_call 'say ok' 'system' 20
48
+ ")
49
+ assert_contains "$provider_result" "claude fallback ok" "agent_cli fallback did not use Claude after Codex failed"
50
+
51
+ # PreToolUse parsing + read scoring: repeated large read after modification should emit scored context.
52
+ hook_home="$tmp_dir/hook-home"
53
+ repo="$tmp_dir/repo"
54
+ mkdir -p "$hook_home/mod-tracker" "$repo"
55
+ touch "$hook_home/memory.db"
56
+ large_file="$repo/large.txt"
57
+ dd if=/dev/zero bs=1024 count=600 2>/dev/null | tr '\0' 'x' > "$large_file"
58
+ session_id="session_reliability_123"
59
+ printf '%s\n' "$large_file" > "$hook_home/mod-tracker/$session_id"
60
+ read_input=$(jq -nc --arg fp "$large_file" --arg sid "$session_id" --arg cwd "$repo" \
61
+ '{tool_name:"Read",session_id:$sid,cwd:$cwd,tool_input:{file_path:$fp}}')
62
+ EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input" >/dev/null
63
+ EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input" >/dev/null
64
+ read_output=$(EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input")
65
+ assert_contains "$read_output" "Eagle Mem read score" "Read guard did not emit scored duplicate-read context"
66
+ grep -q 'mod_file}.lock' "$ROOT_DIR/hooks/post-tool-use.sh" || {
67
+ echo "post-tool-use modification tracker should use a lock directory" >&2
68
+ exit 1
69
+ }
70
+
71
+ # Auto-scan state race: failed background scan must clear the freshness marker.
72
+ state_home="$tmp_dir/state-home"
73
+ auto_scripts="$tmp_dir/auto-scripts"
74
+ auto_repo="$tmp_dir/auto-repo"
75
+ mkdir -p "$state_home" "$auto_scripts" "$auto_repo"
76
+ cat > "$auto_scripts/scan.sh" <<'SH'
77
+ #!/usr/bin/env bash
78
+ exit 9
79
+ SH
80
+ cat > "$auto_scripts/index.sh" <<'SH'
81
+ #!/usr/bin/env bash
82
+ exit 0
83
+ SH
84
+ EAGLE_MEM_DIR="$state_home" EAGLE_MEM_LOG="$state_home/eagle-mem.log" bash -c "
85
+ . '$ROOT_DIR/lib/common.sh'
86
+ . '$ROOT_DIR/lib/hooks-sessionstart.sh'
87
+ eagle_get_overview() { return 0; }
88
+ eagle_db() { printf '0\n'; }
89
+ eagle_sessionstart_auto_provision 'project-auto-fail' '$auto_repo' '$auto_scripts'
90
+ scan_state=\$(_eagle_state_file scan 'project-auto-fail')
91
+ for _i in 1 2 3 4 5 6 7 8 9 10; do
92
+ grep -q 'auto-scan failed' '$state_home/eagle-mem.log' 2>/dev/null && break
93
+ sleep 0.2
94
+ done
95
+ [ ! -f \"\$scan_state\" ]
96
+ "
97
+
98
+ # Command-scoped logs: scan should create an inspectable run log and logs list should show it.
99
+ log_home="$tmp_dir/log-home"
100
+ log_repo="$tmp_dir/log-repo"
101
+ mkdir -p "$log_home" "$log_repo"
102
+ printf '# demo\n' > "$log_repo/README.md"
103
+ EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/scripts/scan.sh" "$log_repo" >/dev/null
104
+ log_list=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
105
+ assert_contains "$log_list" "command=scan" "logs list did not show the scan command run"
106
+
107
+ echo "reliability guard regressions passed"