eagle-mem 4.10.9 → 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,30 @@ 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
+
20
+ ## v4.10.10 Dream Cycle Consolidation Hardening
21
+
22
+ This patch closes the review findings from the multi-model Spectral pass:
23
+
24
+ - **Structured Consolidation Parsing**: Dream Cycle memory consolidation now asks providers for strict JSON and validates the response with `jq`, removing the brittle `CONSOLIDATE:` text parser that could break on punctuation, arrows, pipes, or whitespace in memory names.
25
+ - **Dry-Run Safety**: Memory graph consolidation dry-runs now preview graph wiring and skip the provider call, avoiding token spend and provider side effects during preview.
26
+ - **Regression Coverage**: The Dream Cycle regression now covers JSON consolidation with punctuation-heavy names, `NONE` responses, malformed legacy text responses, idempotent reruns, and dry-run provider skipping.
27
+ - **Indexer Edge Coverage**: Graph-memory indexing now verifies dot-command-like source lines, leading blank lines, all-whitespace chunks, and empty-file behavior.
28
+
29
+ ---
30
+
7
31
  ## v4.10.9 Dream Cycle Graph Memory Hotfix
8
32
 
9
33
  This hotfix closes the remaining graph-memory curation gap:
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 |
@@ -184,7 +185,7 @@ eagle-mem overview set "Current project briefing..."
184
185
 
185
186
  If graph search shows stale deleted files, run `eagle-mem graph rebuild` from the project root. The rebuild command filters missing tracked paths, clears stale code chunks and declaration nodes, preserves manual overviews, and rewires declarations with file-scoped names such as `apps/mac/DictationController.swift::finishDictation`.
186
187
 
187
- Dream Cycle curation also wires mirrored agent memories into graph `memory` nodes before consolidation, so `supersedes` relationships stay attached to the source memories rather than to incidental text inside memory content.
188
+ Dream Cycle curation also wires mirrored agent memories into graph `memory` nodes before consolidation, so `supersedes` relationships stay attached to the source memories rather than to incidental text inside memory content. Consolidation responses use a strict JSON contract, and dry-run previews skip the provider call for the memory-consolidation step.
188
189
 
189
190
  ### Trust and Recovery
190
191
 
@@ -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