eagle-mem 4.9.3 → 4.9.5

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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code and Codex the same local memory, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
13
13
 
14
- **v4.8.5 hardens first-run setup:** `eagle-mem config init` now falls through cleanly when Ollama is not running, and DB-backed commands fail loudly when the active `sqlite3` lacks FTS5 support.
14
+ **v4.9.5 hardens hooks and SQLite:** Stop hooks save immediately and queue LLM enrichment in the background instead of launching nested agents during turn shutdown. SQLite calls also resolve through one FTS5-capable binary selector, so Android SDK or other PATH shims do not accidentally break Eagle Mem.
15
15
 
16
16
  **Website:** [Product](https://eagleisbatman.github.io/eagle-mem/) |
17
17
  [Architecture](https://eagleisbatman.github.io/eagle-mem/architecture.html) |
@@ -73,7 +73,7 @@ For Codex, the installer enables `codex_hooks` in `~/.codex/config.toml`, regist
73
73
 
74
74
  ### Prerequisites
75
75
 
76
- - `sqlite3` with FTS5 support (ships with macOS; the installer offers to install if missing)
76
+ - `sqlite3` with FTS5 support (ships with macOS; Eagle Mem prefers known system/Homebrew SQLite binaries before PATH shims)
77
77
  - `jq` (the installer offers to install if missing)
78
78
  - [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, or both installed
79
79
 
@@ -87,7 +87,7 @@ Hooks fire automatically at different points in the agent lifecycle:
87
87
  | **PreToolUse** | before Bash/shell, Read, Edit, Write, apply_patch | Surfaces guardrails and decisions before edits. Blocks release-boundary commands while feature verification is pending. Rewrites noisy commands through RTK when available. Detects redundant reads, nudges co-edit partners, detects stuck loops. |
88
88
  | **UserPromptSubmit** | user sends a message | FTS5 search across past sessions and indexed code for relevant context |
89
89
  | **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, surfaces decision history and feature impacts on reads, stale memory warnings on edits |
90
- | **Stop** | agent turn ends | Extracts `<eagle-summary>` blocks for rich session summaries from Claude Code and Codex transcripts |
90
+ | **Stop** | agent turn ends | Saves fast heuristic summaries and extracts `<eagle-summary>` blocks when present. LLM enrichment runs later in the background so the agent lifecycle is not blocked. |
91
91
  | **SessionEnd** | session closes | Re-syncs tasks, marks session completed |
92
92
 
93
93
  Codex shell hooks are registered for `Bash`, `exec_command`, `shell_command`, and `unified_exec` tool names so release-boundary protection works across current Codex shell paths.
@@ -152,6 +152,14 @@ Eagle Mem prevents Claude from repeating past mistakes:
152
152
  | `eagle-mem scan` | Scan codebase and generate overview |
153
153
  | `eagle-mem index` | Index source files for FTS5 code search |
154
154
 
155
+ ### v4.9.5 Patch
156
+
157
+ Stop hooks now use a fast path: they save heuristic summaries immediately, extract explicit summary blocks when present, and queue LLM enrichment in the background so Codex/Claude lifecycle hooks do not time out. SQLite access now goes through a shared FTS5-capable binary resolver used by migrations, DB helpers, updater backups, install checks, and the statusline, avoiding Android SDK or other PATH shims that shadow working SQLite builds.
158
+
159
+ ### v4.9.4 Patch
160
+
161
+ Project-key hardening for agents that move between folders: hooks now keep a per-session project identity instead of recalculating from every new cwd, and statuslines prefer the stored session project before falling back to folder paths. Install/update also repairs older embedded Eagle Mem statusline blocks so nested-repo projects stop showing `Memories: 0` when the session belongs to the parent workspace.
162
+
155
163
  ### v4.9.3 Patch
156
164
 
157
165
  Follow-up hardening for the v4.9.2 project-key repair: Claude transcript workspace detection now reads complete early JSONL records instead of a fixed byte slice, so large SessionStart hook context cannot hide the first `cwd`. Metadata-only memory/plan/task repairs also avoid touching FTS-indexed columns, preventing SQLite FTS update triggers from firing during safe project/source rekeys.
@@ -0,0 +1,17 @@
1
+ -- Migration 034: Make feature FTS updates safe for project-key repairs.
2
+ --
3
+ -- Project-key repairs update features.project, but the older trigger fired on
4
+ -- every UPDATE and tried to rewrite the FTS5 row even when searchable text did
5
+ -- not change. Restrict it to the indexed columns so metadata-only rekeys are
6
+ -- safe.
7
+
8
+ DROP TRIGGER IF EXISTS features_au;
9
+
10
+ CREATE TRIGGER features_au
11
+ AFTER UPDATE OF name, description ON features
12
+ BEGIN
13
+ INSERT INTO features_fts(features_fts, rowid, name, description)
14
+ VALUES ('delete', old.id, old.name, old.description);
15
+ INSERT INTO features_fts(rowid, name, description)
16
+ VALUES (new.id, new.name, new.description);
17
+ END;
package/db/migrate.sh CHANGED
@@ -13,11 +13,12 @@ COMMON_SH="$SCRIPT_DIR/../lib/common.sh"
13
13
  if [ -f "$COMMON_SH" ]; then
14
14
  . "$COMMON_SH"
15
15
  eagle_require_sqlite_fts5 || exit 1
16
+ SQLITE_BIN=$(eagle_sqlite_path)
16
17
  else
17
- sqlite_path=$(command -v sqlite3 2>/dev/null || true)
18
- if [ -z "$sqlite_path" ] || ! sqlite3 :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" >/dev/null 2>&1; then
18
+ SQLITE_BIN=$(command -v sqlite3 2>/dev/null || true)
19
+ if [ -z "$SQLITE_BIN" ] || ! "$SQLITE_BIN" :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" >/dev/null 2>&1; then
19
20
  echo "Eagle Mem requires SQLite FTS5, but the active sqlite3 does not support it." >&2
20
- [ -n "$sqlite_path" ] && echo "Detected sqlite3: $sqlite_path" >&2
21
+ [ -n "$SQLITE_BIN" ] && echo "Detected sqlite3: $SQLITE_BIN" >&2
21
22
  echo "Fix PATH so an FTS5-capable sqlite3 is first, then re-run the command." >&2
22
23
  exit 1
23
24
  fi
@@ -33,7 +34,7 @@ run_migration() {
33
34
  local file="$2"
34
35
 
35
36
  local already_applied
36
- already_applied=$(sqlite3 "$DB" "SELECT COUNT(*) FROM _migrations WHERE name = '$name';" 2>/dev/null || echo "0")
37
+ already_applied=$("$SQLITE_BIN" "$DB" "SELECT COUNT(*) FROM _migrations WHERE name = '$name';" 2>/dev/null || echo "0")
37
38
 
38
39
  if [ "$already_applied" = "0" ]; then
39
40
  # Strip PRAGMAs from migration body (they can't run inside transactions
@@ -43,20 +44,20 @@ run_migration() {
43
44
 
44
45
  # Set connection PRAGMAs, then run migration body + tracking insert atomically
45
46
  # .bail on ensures sqlite3 stops on the first error instead of continuing
46
- { echo ".bail on"; echo "PRAGMA trusted_schema=ON;"; echo "PRAGMA foreign_keys=ON;"; echo "PRAGMA busy_timeout=5000;"; echo "BEGIN;"; echo "$body"; echo "INSERT INTO _migrations (name) VALUES ('$name');"; echo "COMMIT;"; } | sqlite3 "$DB"
47
+ { echo ".bail on"; echo "PRAGMA trusted_schema=ON;"; echo "PRAGMA foreign_keys=ON;"; echo "PRAGMA busy_timeout=5000;"; echo "BEGIN;"; echo "$body"; echo "INSERT INTO _migrations (name) VALUES ('$name');"; echo "COMMIT;"; } | "$SQLITE_BIN" "$DB"
47
48
  echo " applied: $name"
48
49
  fi
49
50
  }
50
51
 
51
52
  # Ensure _migrations table exists (bootstrap)
52
- sqlite3 "$DB" "CREATE TABLE IF NOT EXISTS _migrations (
53
+ "$SQLITE_BIN" "$DB" "CREATE TABLE IF NOT EXISTS _migrations (
53
54
  id INTEGER PRIMARY KEY,
54
55
  name TEXT NOT NULL UNIQUE,
55
56
  applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
56
57
  );"
57
58
 
58
59
  # Set PRAGMAs (these must be set on every connection)
59
- sqlite3 "$DB" "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;"
60
+ "$SQLITE_BIN" "$DB" "PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;"
60
61
 
61
62
  # ─── Migration 001: Initial schema (special name) ──────────
62
63
  run_migration "001_initial_schema" "$SCRIPT_DIR/schema.sql"
package/hooks/stop.sh CHANGED
@@ -223,6 +223,7 @@ if echo "$request" | grep -qE '<(local-command-caveat|system-reminder|command-na
223
223
  fi
224
224
 
225
225
  needs_enrichment=0
226
+ defer_enrichment=0
226
227
  if [ "$has_rich_data" -eq 0 ]; then
227
228
  needs_enrichment=1
228
229
  elif [ "$context_pressure" -eq 1 ]; then
@@ -235,7 +236,13 @@ fi
235
236
 
236
237
  if [ "$needs_enrichment" -eq 1 ]; then
237
238
  provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
238
- if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
239
+ # Stop hooks must stay fast. Expensive LLM enrichment belongs in curate or
240
+ # another background path; nested agent_cli calls can exceed Codex/Claude
241
+ # lifecycle timeouts and make the hook look broken to users.
242
+ if [ "${EAGLE_MEM_STOP_ENRICH:-0}" != "1" ]; then
243
+ defer_enrichment=1
244
+ eagle_log "INFO" "Stop: LLM enrichment skipped — fast hook path (provider=$provider)"
245
+ elif [ "$provider" != "none" ] && [ -n "$text_content" ]; then
239
246
  excerpt=$(echo "$text_content" | tail -c 3000)
240
247
 
241
248
  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.
@@ -401,4 +408,26 @@ if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
401
408
  fi
402
409
  fi
403
410
 
411
+ if [ "$defer_enrichment" -eq 1 ] && [ "${EAGLE_MEM_STOP_BACKGROUND_ENRICH:-1}" = "1" ] && [ -n "$text_content" ]; then
412
+ mkdir -p "$EAGLE_MEM_DIR/tmp" 2>/dev/null || true
413
+ enrich_job=$(mktemp "$EAGLE_MEM_DIR/tmp/summary-enrich.XXXXXX.json" 2>/dev/null)
414
+ if [ -n "$enrich_job" ]; then
415
+ jq -cn \
416
+ --arg session_id "$session_id" \
417
+ --arg project "$project" \
418
+ --arg agent "$agent" \
419
+ --arg text "$text_content" \
420
+ '{session_id:$session_id, project:$project, agent:$agent, text:$text}' > "$enrich_job"
421
+
422
+ enrich_script="$SCRIPT_DIR/../scripts/enrich-summary.sh"
423
+ if [ -x "$enrich_script" ]; then
424
+ nohup env EAGLE_MEM_DISABLE_HOOKS=1 EAGLE_AGENT_SOURCE="$agent" EAGLE_AGENT_CWD="$cwd" bash "$enrich_script" "$enrich_job" >/dev/null 2>&1 &
425
+ eagle_log "INFO" "Stop: queued background summary enrichment for session=$session_id"
426
+ else
427
+ rm -f "$enrich_job" 2>/dev/null || true
428
+ eagle_log "WARN" "Stop: background enrichment script missing: $enrich_script"
429
+ fi
430
+ fi
431
+ fi
432
+
404
433
  exit 0
@@ -163,7 +163,7 @@ eagle_register_codex_hooks() {
163
163
  "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
164
164
  "Codex Stop hook" \
165
165
  "Saving Eagle Mem summary" \
166
- "30"
166
+ "60"
167
167
  }
168
168
 
169
169
  eagle_remove_codex_hooks() {
package/lib/common.sh CHANGED
@@ -20,24 +20,63 @@ EAGLE_CODEX_SKILLS_DIR="${EAGLE_CODEX_SKILLS_DIR:-$EAGLE_CODEX_DIR/skills}"
20
20
  EAGLE_CODEX_MEMORIES_DIR="${EAGLE_CODEX_MEMORIES_DIR:-$EAGLE_CODEX_DIR/memories}"
21
21
  EAGLE_RAW_BASH_UNLOCK="${EAGLE_RAW_BASH_UNLOCK:-/tmp/eagle-mem-raw-bash-unlock}"
22
22
 
23
+ _eagle_sqlite_candidate_paths() {
24
+ [ -n "${EAGLE_SQLITE_BIN:-}" ] && printf '%s\n' "$EAGLE_SQLITE_BIN"
25
+ [ -f "$EAGLE_MEM_DIR/.sqlite-bin" ] && sed -n '1p' "$EAGLE_MEM_DIR/.sqlite-bin" 2>/dev/null
26
+ printf '%s\n' \
27
+ "/opt/homebrew/opt/sqlite/bin/sqlite3" \
28
+ "/usr/local/opt/sqlite/bin/sqlite3" \
29
+ "/usr/bin/sqlite3" \
30
+ "/opt/homebrew/bin/sqlite3" \
31
+ "/usr/local/bin/sqlite3"
32
+ command -v sqlite3 2>/dev/null || true
33
+ }
34
+
35
+ _eagle_sqlite_bin_supports_fts5() {
36
+ local bin="$1"
37
+ [ -n "$bin" ] && [ -x "$bin" ] || return 1
38
+ "$bin" :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" >/dev/null 2>&1
39
+ }
40
+
23
41
  eagle_sqlite_path() {
42
+ local seen="" candidate
43
+ while IFS= read -r candidate; do
44
+ [ -n "$candidate" ] || continue
45
+ case "|$seen|" in *"|$candidate|"*) continue ;; esac
46
+ seen="${seen}|${candidate}"
47
+ if _eagle_sqlite_bin_supports_fts5 "$candidate"; then
48
+ printf '%s\n' "$candidate"
49
+ return 0
50
+ fi
51
+ done <<EOF
52
+ $(_eagle_sqlite_candidate_paths)
53
+ EOF
54
+
24
55
  command -v sqlite3 2>/dev/null || true
25
56
  }
26
57
 
27
58
  eagle_sqlite_version() {
28
- sqlite3 --version 2>/dev/null | awk '{print $1}'
59
+ local sqlite_bin
60
+ sqlite_bin=$(eagle_sqlite_path)
61
+ [ -n "$sqlite_bin" ] || return 0
62
+ "$sqlite_bin" --version 2>/dev/null | awk '{print $1}'
29
63
  }
30
64
 
31
65
  eagle_sqlite_supports_fts5() {
32
- command -v sqlite3 >/dev/null 2>&1 || return 1
33
- sqlite3 :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" >/dev/null 2>&1
66
+ local sqlite_bin
67
+ sqlite_bin=$(eagle_sqlite_path)
68
+ _eagle_sqlite_bin_supports_fts5 "$sqlite_bin"
34
69
  }
35
70
 
36
71
  eagle_print_sqlite_fts5_error() {
37
72
  local sqlite_path sqlite_version probe_error
38
73
  sqlite_path=$(eagle_sqlite_path)
39
74
  sqlite_version=$(eagle_sqlite_version)
40
- probe_error=$(sqlite3 :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" 2>&1 >/dev/null || true)
75
+ if [ -n "$sqlite_path" ]; then
76
+ probe_error=$("$sqlite_path" :memory: "CREATE VIRTUAL TABLE eagle_mem_fts5_probe USING fts5(value);" 2>&1 >/dev/null || true)
77
+ else
78
+ probe_error=""
79
+ fi
41
80
 
42
81
  printf '%s\n' "Eagle Mem requires SQLite FTS5, but the active sqlite3 does not support it." >&2
43
82
  if [ -n "$sqlite_path" ]; then
@@ -47,8 +86,8 @@ eagle_print_sqlite_fts5_error() {
47
86
  fi
48
87
  [ -n "$sqlite_version" ] && printf '%s\n' "SQLite version: $sqlite_version" >&2
49
88
  [ -n "$probe_error" ] && printf '%s\n' "SQLite error: $probe_error" >&2
50
- printf '%s\n' "Fix: put an FTS5-capable sqlite3 earlier in PATH, then re-run the command." >&2
51
- printf '%s\n' "macOS: check 'command -v sqlite3'; /usr/bin/sqlite3 usually has FTS5. If Android SDK platform-tools is first, move it later in PATH." >&2
89
+ printf '%s\n' "Fix: install an FTS5-capable sqlite3, or set EAGLE_SQLITE_BIN to its absolute path." >&2
90
+ printf '%s\n' "macOS: /usr/bin/sqlite3 usually has FTS5. Eagle Mem now prefers known system/Homebrew sqlite3 paths over Android SDK shims." >&2
52
91
  printf '%s\n' "Homebrew: install sqlite and prepend its bin directory, for example: export PATH=\"/opt/homebrew/opt/sqlite/bin:\$PATH\"" >&2
53
92
  printf '%s\n' "Linux: install a sqlite3 package compiled with ENABLE_FTS5." >&2
54
93
  }
@@ -210,14 +249,231 @@ eagle_transcript_first_cwd() {
210
249
  local transcript_path="${1:-}"
211
250
  [ -f "$transcript_path" ] || return 1
212
251
 
252
+ local head_lines
253
+ head_lines=$(sed -n '1,200p' "$transcript_path" 2>/dev/null || true)
254
+ [ -n "$head_lines" ] || return 1
255
+
213
256
  local cwd
214
- cwd=$(sed -n '1,200p' "$transcript_path" 2>/dev/null \
215
- | jq -r 'select((.cwd? // "") != "") | .cwd' 2>/dev/null \
257
+ cwd=$(printf '%s\n' "$head_lines" \
258
+ | jq -r '
259
+ [
260
+ (.payload? | objects | .workspace? | objects | .project_dir? | strings),
261
+ (.workspace? | objects | .project_dir? | strings),
262
+ (.payload? | objects | .workspace? | objects | .current_dir? | strings),
263
+ (.workspace? | objects | .current_dir? | strings),
264
+ (.cwd? | strings),
265
+ (.payload? | objects | .cwd? | strings),
266
+ (.payload? | objects | .current_dir? | strings)
267
+ ]
268
+ | .[0] // empty
269
+ ' 2>/dev/null \
216
270
  | awk 'NF { print; exit }' || true)
271
+
217
272
  [ -n "$cwd" ] || return 1
218
273
  printf '%s\n' "$cwd"
219
274
  }
220
275
 
276
+ eagle_session_project_marker_file() {
277
+ local session_id="${1:-}"
278
+ eagle_validate_session_id "$session_id" || return 1
279
+ mkdir -p "$EAGLE_MEM_DIR/session-projects" 2>/dev/null || return 1
280
+ printf '%s/session-projects/%s\n' "$EAGLE_MEM_DIR" "$session_id"
281
+ }
282
+
283
+ eagle_get_session_project_marker() {
284
+ local marker
285
+ marker=$(eagle_session_project_marker_file "$1") || return 1
286
+ [ -s "$marker" ] || return 1
287
+ awk 'NF { print; exit }' "$marker"
288
+ }
289
+
290
+ eagle_remember_session_project() {
291
+ local session_id="${1:-}"
292
+ local project="${2:-}"
293
+ local force="${3:-0}"
294
+ [ -n "$project" ] || return 1
295
+
296
+ local marker
297
+ marker=$(eagle_session_project_marker_file "$session_id") || return 1
298
+ if [ "$force" = "1" ] || [ ! -s "$marker" ]; then
299
+ printf '%s\n' "$project" > "$marker" 2>/dev/null || return 1
300
+ fi
301
+ }
302
+
303
+ eagle_get_session_project_light() {
304
+ local session_id="${1:-}"
305
+ eagle_validate_session_id "$session_id" || return 1
306
+ command -v sqlite3 >/dev/null 2>&1 || return 1
307
+ [ -f "$EAGLE_MEM_DB" ] || return 1
308
+
309
+ local sid_sql project
310
+ sid_sql=$(eagle_sql_escape "$session_id")
311
+ project=$(sqlite3 "$EAGLE_MEM_DB" "SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
312
+ [ -n "$project" ] || return 1
313
+ printf '%s\n' "$project"
314
+ }
315
+
316
+ eagle_project_has_table_row() {
317
+ local table="${1:-}"
318
+ local project="${2:-}"
319
+ [ -n "$table" ] && [ -n "$project" ] || return 1
320
+ command -v sqlite3 >/dev/null 2>&1 || return 1
321
+ [ -f "$EAGLE_MEM_DB" ] || return 1
322
+
323
+ local project_sql found
324
+ project_sql=$(eagle_sql_escape "$project")
325
+ found=$(sqlite3 "$EAGLE_MEM_DB" "SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
326
+ [ "$found" = "1" ]
327
+ }
328
+
329
+ eagle_project_from_existing_ancestor() {
330
+ local path="${1:-}"
331
+ [ -n "$path" ] || return 1
332
+
333
+ local current key
334
+ current=$(eagle_normalize_project_path "$path")
335
+ eagle_is_ephemeral_project_path "$current" && return 1
336
+
337
+ while [ -n "$current" ] && [ "$current" != "/" ]; do
338
+ key=$(eagle_project_key_from_target_dir "$current")
339
+ if [ -n "$key" ]; then
340
+ # Prefer ancestors with durable memory/summary content. Session-only
341
+ # rows can be created by the very folder drift this helper repairs.
342
+ if eagle_project_has_table_row "agent_memories" "$key" \
343
+ || eagle_project_has_table_row "summaries" "$key"; then
344
+ printf '%s\n' "$key"
345
+ return 0
346
+ fi
347
+ fi
348
+
349
+ [ "$current" = "$HOME" ] && break
350
+ current=$(dirname "$current")
351
+ done
352
+
353
+ return 1
354
+ }
355
+
356
+ eagle_project_from_workspace_path() {
357
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
358
+ printf '%s\n' "$EAGLE_MEM_PROJECT"
359
+ return 0
360
+ fi
361
+
362
+ local path="${1:-}"
363
+ [ -n "$path" ] || return 1
364
+
365
+ local resolved worktree_project git_root project
366
+ resolved=$(eagle_normalize_project_path "$path")
367
+ eagle_is_ephemeral_project_path "$resolved" && return 1
368
+
369
+ if worktree_project=$(eagle_project_key_for_worktree_path "$resolved"); then
370
+ printf '%s\n' "$worktree_project"
371
+ return 0
372
+ fi
373
+
374
+ git_root=$(git -C "$resolved" rev-parse --show-toplevel 2>/dev/null)
375
+ if [ -n "$git_root" ]; then
376
+ project=$(eagle_project_key_from_target_dir "$git_root")
377
+ [ -n "$project" ] && { printf '%s\n' "$project"; return 0; }
378
+ fi
379
+
380
+ if project=$(eagle_project_from_existing_ancestor "$path"); then
381
+ printf '%s\n' "$project"
382
+ return 0
383
+ fi
384
+
385
+ project=$(eagle_project_from_path_no_git "$path")
386
+ [ -n "$project" ] || return 1
387
+ printf '%s\n' "$project"
388
+ }
389
+
390
+ eagle_project_from_transcript_start() {
391
+ local transcript_path="${1:-}"
392
+ local cwd="${2:-}"
393
+ [ -f "$transcript_path" ] || return 1
394
+
395
+ local transcript_cwd project
396
+ if project=$(eagle_project_from_claude_transcript "$transcript_path" "$cwd"); then
397
+ printf '%s\n' "$project"
398
+ return 0
399
+ fi
400
+
401
+ transcript_cwd=$(eagle_transcript_first_cwd "$transcript_path") || return 1
402
+ if [ -n "$cwd" ] && ! eagle_path_is_same_or_child "$transcript_cwd" "$cwd"; then
403
+ return 1
404
+ fi
405
+
406
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
407
+ [ -n "$project" ] || return 1
408
+ printf '%s\n' "$project"
409
+ }
410
+
411
+ eagle_project_from_statusline_input() {
412
+ local input="${1:-}"
413
+ local project_dir="${2:-}"
414
+ local cwd="${3:-}"
415
+ local session_id="${4:-}"
416
+ local project workspace_project_dir transcript_path
417
+
418
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
419
+ printf '%s\n' "$EAGLE_MEM_PROJECT"
420
+ return
421
+ fi
422
+
423
+ if [ -z "$session_id" ] && [ -n "$input" ]; then
424
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // .session.id // empty' 2>/dev/null)
425
+ fi
426
+
427
+ if [ -n "$input" ]; then
428
+ workspace_project_dir=$(printf '%s' "$input" | jq -r '.workspace.project_dir // empty' 2>/dev/null)
429
+ transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
430
+ [ -z "$cwd" ] && cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
431
+ fi
432
+
433
+ # Explicit workspace project and transcript start are stronger than older
434
+ # cached session rows because they repair pre-fix sessions that were stored
435
+ # under a nested folder key.
436
+ if [ -n "$workspace_project_dir" ]; then
437
+ project=$(eagle_project_from_workspace_path "$workspace_project_dir")
438
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
439
+ fi
440
+
441
+ if [ -n "$transcript_path" ]; then
442
+ if project=$(eagle_project_from_transcript_start "$transcript_path" "${cwd:-$project_dir}"); then
443
+ printf '%s\n' "$project"
444
+ return
445
+ fi
446
+ fi
447
+
448
+ if [ -n "$session_id" ]; then
449
+ if project=$(eagle_get_session_project_light "$session_id"); then
450
+ printf '%s\n' "$project"
451
+ return
452
+ fi
453
+ if project=$(eagle_get_session_project_marker "$session_id"); then
454
+ printf '%s\n' "$project"
455
+ return
456
+ fi
457
+ fi
458
+
459
+ if [ -z "$project_dir" ] && [ -n "$input" ]; then
460
+ project_dir=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
461
+ fi
462
+ if [ -n "$project_dir" ]; then
463
+ project=$(eagle_project_from_workspace_path "$project_dir")
464
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
465
+ fi
466
+
467
+ if [ -z "$cwd" ] && [ -n "$input" ]; then
468
+ cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
469
+ fi
470
+ if [ -n "$cwd" ]; then
471
+ project=$(eagle_project_from_workspace_path "$cwd")
472
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
473
+ fi
474
+ eagle_project_from_cwd "$cwd"
475
+ }
476
+
221
477
  eagle_project_from_claude_project_dir() {
222
478
  local project_dir="${1:-}"
223
479
  project_dir="${project_dir%/}"
@@ -228,7 +484,7 @@ eagle_project_from_claude_project_dir() {
228
484
  [ -f "$jsonl" ] || continue
229
485
  cwd=$(eagle_transcript_first_cwd "$jsonl")
230
486
  [ -z "$cwd" ] && continue
231
- project=$(eagle_project_from_path_no_git "$cwd")
487
+ project=$(eagle_project_from_workspace_path "$cwd")
232
488
  [ -n "$project" ] && { printf '%s\n' "$project"; return 0; }
233
489
  done
234
490
 
@@ -252,7 +508,7 @@ eagle_project_from_claude_transcript() {
252
508
  return 1
253
509
  fi
254
510
 
255
- project=$(eagle_project_from_path_no_git "$transcript_cwd")
511
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
256
512
  [ -n "$project" ] || return 1
257
513
  printf '%s\n' "$project"
258
514
  }
@@ -265,16 +521,54 @@ eagle_project_from_hook_input() {
265
521
  return
266
522
  fi
267
523
 
268
- local cwd transcript_path project
524
+ local session_id cwd transcript_path workspace_project project
525
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
269
526
  cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
270
527
  transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
271
528
 
529
+ workspace_project=$(printf '%s' "$input" | jq -r '.workspace.project_dir // empty' 2>/dev/null)
530
+ if [ -n "$workspace_project" ]; then
531
+ project=$(eagle_project_from_workspace_path "$workspace_project")
532
+ if [ -n "$project" ]; then
533
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
534
+ printf '%s\n' "$project"
535
+ return
536
+ fi
537
+ fi
538
+
272
539
  if project=$(eagle_project_from_claude_transcript "$transcript_path" "$cwd"); then
540
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
273
541
  printf '%s\n' "$project"
274
542
  return
275
543
  fi
276
544
 
277
- eagle_project_from_cwd "$cwd"
545
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
546
+ local transcript_cwd
547
+ transcript_cwd=$(eagle_transcript_first_cwd "$transcript_path")
548
+ if [ -n "$transcript_cwd" ]; then
549
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
550
+ if [ -n "$project" ]; then
551
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
552
+ printf '%s\n' "$project"
553
+ return
554
+ fi
555
+ fi
556
+ fi
557
+
558
+ if [ -n "$session_id" ]; then
559
+ if project=$(eagle_get_session_project_light "$session_id"); then
560
+ printf '%s\n' "$project"
561
+ return
562
+ fi
563
+ if project=$(eagle_get_session_project_marker "$session_id"); then
564
+ printf '%s\n' "$project"
565
+ return
566
+ fi
567
+ fi
568
+
569
+ project=$(eagle_project_from_cwd "$cwd")
570
+ [ -n "$session_id" ] && [ -n "$project" ] && eagle_remember_session_project "$session_id" "$project" 0 >/dev/null 2>&1
571
+ printf '%s\n' "$project"
278
572
  }
279
573
 
280
574
  eagle_project_file_path() {
@@ -810,6 +1104,114 @@ eagle_collect_files() {
810
1104
  fi
811
1105
  }
812
1106
 
1107
+ eagle_statusline_script_from_command() {
1108
+ local cmd="${1:-}"
1109
+ [ -z "$cmd" ] && return 1
1110
+
1111
+ cmd="${cmd#sh }"
1112
+ cmd="${cmd#bash }"
1113
+ cmd="${cmd#/bin/sh }"
1114
+ cmd="${cmd#/bin/bash }"
1115
+ cmd="${cmd#zsh }"
1116
+ cmd="${cmd#/bin/zsh }"
1117
+ cmd="${cmd%\"}"
1118
+ cmd="${cmd#\"}"
1119
+ cmd="${cmd%\'}"
1120
+ cmd="${cmd#\'}"
1121
+
1122
+ case "$cmd" in
1123
+ "~/"*) cmd="$HOME/${cmd#\~/}" ;;
1124
+ "\$HOME/"*) cmd="$HOME/${cmd#\$HOME/}" ;;
1125
+ "\${HOME}/"*) cmd="$HOME/${cmd#\$\{HOME\}/}" ;;
1126
+ esac
1127
+
1128
+ if [ -L "$cmd" ]; then
1129
+ local link_target link_dir
1130
+ link_target=$(readlink "$cmd" 2>/dev/null || true)
1131
+ if [ -n "$link_target" ]; then
1132
+ case "$link_target" in
1133
+ /*) cmd="$link_target" ;;
1134
+ *)
1135
+ link_dir=$(cd "$(dirname "$cmd")" && pwd -P) || return 1
1136
+ cmd="$link_dir/$link_target"
1137
+ ;;
1138
+ esac
1139
+ fi
1140
+ fi
1141
+
1142
+ [ -f "$cmd" ] || return 1
1143
+ printf '%s\n' "$cmd"
1144
+ }
1145
+
1146
+ eagle_statusline_script_uses_input() {
1147
+ local sl_file="${1:-}"
1148
+ [ -f "$sl_file" ] || return 1
1149
+ grep -Eq 'eagle_project_from_statusline_input|eagle_mem_statusline.*(\$\{input:-\}|\$input)' "$sl_file"
1150
+ }
1151
+
1152
+ eagle_patch_statusline_script() {
1153
+ local sl_file="${1:-}"
1154
+ [ -f "$sl_file" ] || return 1
1155
+ command -v perl >/dev/null 2>&1 || return 1
1156
+
1157
+ if [ -L "$sl_file" ]; then
1158
+ local link_target link_dir
1159
+ link_target=$(readlink "$sl_file" 2>/dev/null || true)
1160
+ if [ -n "$link_target" ]; then
1161
+ case "$link_target" in
1162
+ /*) sl_file="$link_target" ;;
1163
+ *)
1164
+ link_dir=$(cd "$(dirname "$sl_file")" && pwd -P) || return 1
1165
+ sl_file="$link_dir/$link_target"
1166
+ ;;
1167
+ esac
1168
+ fi
1169
+ fi
1170
+
1171
+ local tmp backup mode
1172
+ tmp=$(mktemp) || return 1
1173
+ cp "$sl_file" "$tmp" || { rm -f "$tmp"; return 1; }
1174
+
1175
+ if ! grep -Eq 'eagle_mem_statusline|eagle_project_from_cwd|claude_memories|agent_memories|\.eagle-mem/scripts/statusline-em' "$tmp" 2>/dev/null; then
1176
+ rm -f "$tmp"
1177
+ return 1
1178
+ fi
1179
+
1180
+ perl -0pi -e '
1181
+ s/(project_dir=\$\(echo "\$input" \| jq -r \x27)\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1182
+ s/(project_dir=\$\(echo "\$input" \| jq -r ")\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1183
+ s/eagle_mem_statusline "\$project_dir" "\$session_id" "\$\{input\}"/eagle_mem_statusline "\$project_dir" "\$session_id" "\${input:-}"/g;
1184
+ s/eagle_mem_statusline "\$project_dir" "\$session_id"(?=[\)\n;])/eagle_mem_statusline "\$project_dir" "\$session_id" "\${input:-}"/g;
1185
+ s/eagle_mem_statusline "\$project_dir"(?=[\)\n;])/eagle_mem_statusline "\$project_dir" "\${session_id:-}" "\${input:-}"/g;
1186
+ s/eagle_project_from_cwd "\$cwd"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1187
+ s/eagle_project_from_cwd "\$project_dir"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1188
+ s/eagle_project_from_cwd "\${project_dir:-\$cwd}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1189
+ s/eagle_project_from_cwd "\${project_dir:-\$(pwd)}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1190
+ s/eagle_project_from_cwd "\${eagle_mem_project_dir:-\${project_dir:-\$cwd}}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1191
+ s/eagle_project_from_cwd\("\$cwd"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1192
+ s/eagle_project_from_cwd\("\$project_dir"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1193
+ s/eagle_project_from_cwd\("\${project_dir:-\$cwd}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1194
+ s/eagle_project_from_cwd\("\${project_dir:-\$(pwd)}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1195
+ s/eagle_project_from_cwd\("\${eagle_mem_project_dir:-\${project_dir:-\$cwd}}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1196
+ s/FROM claude_memories WHERE project/FROM agent_memories WHERE project/g;
1197
+ ' "$tmp" || { rm -f "$tmp"; return 1; }
1198
+
1199
+ if cmp -s "$sl_file" "$tmp"; then
1200
+ rm -f "$tmp"
1201
+ return 1
1202
+ fi
1203
+
1204
+ backup="${sl_file}.eagle-mem.bak-$(date -u +%Y%m%dT%H%M%SZ)"
1205
+ cp "$sl_file" "$backup" 2>/dev/null || true
1206
+ mode=$(stat -f %Lp "$sl_file" 2>/dev/null || stat -c %a "$sl_file" 2>/dev/null || echo "")
1207
+ if ! mv "$tmp" "$sl_file"; then
1208
+ rm -f "$tmp"
1209
+ return 1
1210
+ fi
1211
+ [ -n "$mode" ] && chmod "$mode" "$sl_file" 2>/dev/null || chmod +x "$sl_file" 2>/dev/null || true
1212
+ return 0
1213
+ }
1214
+
813
1215
  _eagle_claude_md_section() {
814
1216
  cat << 'EAGLE_MD'
815
1217
 
package/lib/db-core.sh CHANGED
@@ -15,10 +15,13 @@ PRAGMA trusted_schema=ON;
15
15
  .output stdout"
16
16
 
17
17
  eagle_db() {
18
+ local _eagle_sqlite_bin
19
+ _eagle_sqlite_bin=$(eagle_sqlite_path)
20
+ [ -n "$_eagle_sqlite_bin" ] || return 1
18
21
  local _eagle_db_err
19
22
  _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_err.$$")
20
23
  local _eagle_db_out
21
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
24
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | "$_eagle_sqlite_bin" "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
22
25
  local _eagle_db_rc=$?
23
26
  if [ -s "$_eagle_db_err" ]; then
24
27
  cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
@@ -29,10 +32,13 @@ eagle_db() {
29
32
  }
30
33
 
31
34
  eagle_db_pipe() {
35
+ local _eagle_sqlite_bin
36
+ _eagle_sqlite_bin=$(eagle_sqlite_path)
37
+ [ -n "$_eagle_sqlite_bin" ] || return 1
32
38
  local _eagle_db_err
33
39
  _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_pipe_err.$$")
34
40
  local _eagle_db_out
35
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
41
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | "$_eagle_sqlite_bin" "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
36
42
  local _eagle_db_rc=$?
37
43
  if [ -s "$_eagle_db_err" ]; then
38
44
  cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
@@ -43,10 +49,13 @@ eagle_db_pipe() {
43
49
  }
44
50
 
45
51
  eagle_db_json() {
52
+ local _eagle_sqlite_bin
53
+ _eagle_sqlite_bin=$(eagle_sqlite_path)
54
+ [ -n "$_eagle_sqlite_bin" ] || return 1
46
55
  local _eagle_db_err
47
56
  _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_json_err.$$")
48
57
  local _eagle_db_out
49
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
58
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | "$_eagle_sqlite_bin" "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
50
59
  local _eagle_db_rc=$?
51
60
  if [ -s "$_eagle_db_err" ]; then
52
61
  cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
@@ -6,12 +6,16 @@
6
6
  _EAGLE_DB_SESSIONS_LOADED=1
7
7
 
8
8
  eagle_upsert_session() {
9
- local session_id; session_id=$(eagle_sql_escape "$1")
10
- local project; project=$(eagle_sql_escape "$2")
9
+ local session_id_raw="${1:-}"
10
+ local project_raw="${2:-}"
11
+ local session_id; session_id=$(eagle_sql_escape "$session_id_raw")
12
+ local project; project=$(eagle_sql_escape "$project_raw")
11
13
  local cwd; cwd=$(eagle_sql_escape "${3:-}")
12
14
  local model; model=$(eagle_sql_escape "${4:-}")
13
15
  local source; source=$(eagle_sql_escape "${5:-}")
14
16
  local agent; agent=$(eagle_sql_escape "${6:-$(eagle_agent_source)}")
17
+ local prior_project
18
+ prior_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$session_id' LIMIT 1;" 2>/dev/null || true)
15
19
 
16
20
  eagle_db "INSERT INTO sessions (id, project, cwd, model, source, agent, last_activity_at)
17
21
  VALUES ('$session_id', '$project', '$cwd', '$model', '$source', '$agent', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
@@ -22,6 +26,127 @@ eagle_upsert_session() {
22
26
  agent = COALESCE(NULLIF(excluded.agent, ''), sessions.agent),
23
27
  status = 'active',
24
28
  last_activity_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
29
+
30
+ local needs_project_repair=0
31
+ if [ -n "$project_raw" ]; then
32
+ if [ "$prior_project" != "$project_raw" ]; then
33
+ needs_project_repair=1
34
+ else
35
+ local stale_child
36
+ stale_child=$(eagle_db "SELECT 1 WHERE
37
+ EXISTS (SELECT 1 FROM summaries WHERE session_id = '$session_id' AND project != '$project')
38
+ OR EXISTS (SELECT 1 FROM observations WHERE session_id = '$session_id' AND project != '$project')
39
+ OR EXISTS (SELECT 1 FROM agent_tasks WHERE source_session_id = '$session_id' AND project != '$project')
40
+ OR EXISTS (SELECT 1 FROM agent_memories WHERE origin_session_id = '$session_id' AND project != '$project')
41
+ OR EXISTS (SELECT 1 FROM agent_plans WHERE origin_session_id = '$session_id' AND project != '$project')
42
+ OR EXISTS (SELECT 1 FROM pending_feature_verifications WHERE source_session_id = '$session_id' AND project != '$project')
43
+ OR EXISTS (
44
+ SELECT 1
45
+ FROM pending_feature_verifications p
46
+ JOIN features f ON f.id = p.feature_id
47
+ WHERE p.source_session_id = '$session_id'
48
+ AND f.project != '$project'
49
+ )
50
+ LIMIT 1;" 2>/dev/null || true)
51
+ [ "$stale_child" = "1" ] && needs_project_repair=1
52
+ fi
53
+ fi
54
+
55
+ if [ "$needs_project_repair" = "1" ]; then
56
+ eagle_db_pipe <<SQL >/dev/null 2>&1
57
+ BEGIN;
58
+ UPDATE summaries SET project = '$project' WHERE session_id = '$session_id' AND project != '$project';
59
+ UPDATE observations SET project = '$project' WHERE session_id = '$session_id' AND project != '$project';
60
+ UPDATE agent_tasks SET project = '$project' WHERE source_session_id = '$session_id' AND project != '$project';
61
+ UPDATE agent_memories SET project = '$project' WHERE origin_session_id = '$session_id' AND project != '$project';
62
+ UPDATE agent_plans SET project = '$project' WHERE origin_session_id = '$session_id' AND project != '$project';
63
+ CREATE TEMP TABLE IF NOT EXISTS eagle_feature_repair_map (
64
+ old_feature_id INTEGER PRIMARY KEY,
65
+ new_feature_id INTEGER NOT NULL
66
+ );
67
+ DELETE FROM eagle_feature_repair_map;
68
+ INSERT OR IGNORE INTO features (project, name, description, status, last_verified_at, last_verified_notes)
69
+ SELECT '$project', f_old.name, f_old.description, f_old.status, f_old.last_verified_at, f_old.last_verified_notes
70
+ FROM pending_feature_verifications p
71
+ JOIN features f_old ON f_old.id = p.feature_id
72
+ WHERE p.source_session_id = '$session_id'
73
+ AND (p.project != '$project' OR f_old.project != '$project')
74
+ GROUP BY f_old.name;
75
+ INSERT OR REPLACE INTO eagle_feature_repair_map (old_feature_id, new_feature_id)
76
+ SELECT DISTINCT f_old.id, f_new.id
77
+ FROM pending_feature_verifications p
78
+ JOIN features f_old ON f_old.id = p.feature_id
79
+ JOIN features f_new ON f_new.project = '$project' AND f_new.name = f_old.name
80
+ WHERE p.source_session_id = '$session_id'
81
+ AND (p.project != '$project' OR f_old.project != '$project');
82
+ INSERT OR IGNORE INTO feature_files (feature_id, file_path, role)
83
+ SELECT f_new.id, ff.file_path, ff.role
84
+ FROM pending_feature_verifications p
85
+ JOIN features f_old ON f_old.id = p.feature_id
86
+ JOIN eagle_feature_repair_map m ON m.old_feature_id = f_old.id
87
+ JOIN features f_new ON f_new.id = m.new_feature_id
88
+ JOIN feature_files ff ON ff.feature_id = f_old.id
89
+ WHERE p.source_session_id = '$session_id'
90
+ AND (p.project != '$project' OR f_old.project != '$project');
91
+ INSERT OR IGNORE INTO feature_dependencies (feature_id, kind, target, name, notes)
92
+ SELECT f_new.id, fd.kind, fd.target, fd.name, fd.notes
93
+ FROM pending_feature_verifications p
94
+ JOIN features f_old ON f_old.id = p.feature_id
95
+ JOIN eagle_feature_repair_map m ON m.old_feature_id = f_old.id
96
+ JOIN features f_new ON f_new.id = m.new_feature_id
97
+ JOIN feature_dependencies fd ON fd.feature_id = f_old.id
98
+ WHERE p.source_session_id = '$session_id'
99
+ AND (p.project != '$project' OR f_old.project != '$project');
100
+ INSERT OR IGNORE INTO feature_smoke_tests (feature_id, command, description)
101
+ SELECT f_new.id, fst.command, fst.description
102
+ FROM pending_feature_verifications p
103
+ JOIN features f_old ON f_old.id = p.feature_id
104
+ JOIN eagle_feature_repair_map m ON m.old_feature_id = f_old.id
105
+ JOIN features f_new ON f_new.id = m.new_feature_id
106
+ JOIN feature_smoke_tests fst ON fst.feature_id = f_old.id
107
+ WHERE p.source_session_id = '$session_id'
108
+ AND (p.project != '$project' OR f_old.project != '$project');
109
+ DELETE FROM pending_feature_verifications
110
+ WHERE source_session_id = '$session_id'
111
+ AND status = 'pending'
112
+ AND id NOT IN (
113
+ SELECT MIN(p.id)
114
+ FROM pending_feature_verifications p
115
+ LEFT JOIN eagle_feature_repair_map m ON m.old_feature_id = p.feature_id
116
+ WHERE p.source_session_id = '$session_id'
117
+ AND p.status = 'pending'
118
+ GROUP BY
119
+ CASE WHEN m.new_feature_id IS NOT NULL THEN '$project' ELSE p.project END,
120
+ COALESCE(m.new_feature_id, p.feature_id),
121
+ p.file_path
122
+ )
123
+ AND EXISTS (
124
+ SELECT 1
125
+ FROM eagle_feature_repair_map m
126
+ WHERE m.old_feature_id = pending_feature_verifications.feature_id
127
+ );
128
+ UPDATE pending_feature_verifications
129
+ SET feature_id = COALESCE((SELECT new_feature_id FROM eagle_feature_repair_map WHERE old_feature_id = feature_id), feature_id)
130
+ WHERE source_session_id = '$session_id'
131
+ AND EXISTS (SELECT 1 FROM eagle_feature_repair_map WHERE old_feature_id = pending_feature_verifications.feature_id);
132
+ DELETE FROM pending_feature_verifications
133
+ WHERE source_session_id = '$session_id'
134
+ AND project != '$project'
135
+ AND status = 'pending'
136
+ AND EXISTS (
137
+ SELECT 1
138
+ FROM pending_feature_verifications p2
139
+ WHERE p2.project = '$project'
140
+ AND p2.feature_id = pending_feature_verifications.feature_id
141
+ AND p2.file_path = pending_feature_verifications.file_path
142
+ AND p2.status = 'pending'
143
+ AND p2.id != pending_feature_verifications.id
144
+ );
145
+ UPDATE pending_feature_verifications SET project = '$project' WHERE source_session_id = '$session_id' AND project != '$project';
146
+ UPDATE sessions SET project = '$project' WHERE id = '$session_id' AND project != '$project';
147
+ COMMIT;
148
+ SQL
149
+ fi
25
150
  }
26
151
 
27
152
  eagle_end_session() {
package/lib/updater.sh CHANGED
@@ -186,8 +186,10 @@ eagle_update_backup_runtime() {
186
186
  done
187
187
 
188
188
  if [ -f "$EAGLE_MEM_DB" ]; then
189
- if command -v sqlite3 >/dev/null 2>&1; then
190
- sqlite3 "$EAGLE_MEM_DB" ".backup '$backup_dir/memory.db'" >/dev/null 2>&1 || cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null || true
189
+ local sqlite_bin
190
+ sqlite_bin=$(eagle_sqlite_path)
191
+ if [ -n "$sqlite_bin" ]; then
192
+ "$sqlite_bin" "$EAGLE_MEM_DB" ".backup '$backup_dir/memory.db'" >/dev/null 2>&1 || cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null || true
191
193
  else
192
194
  cp "$EAGLE_MEM_DB" "$backup_dir/memory.db" 2>/dev/null || true
193
195
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.9.3",
3
+ "version": "4.9.5",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code and Codex",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # Eagle Mem — background summary enrichment
3
+ # Runs outside lifecycle hook timeouts. Input is a JSON job file created by Stop.
4
+ set +e
5
+ [ "${EAGLE_MEM_DISABLE_BACKGROUND_ENRICH:-}" = "1" ] && exit 0
6
+
7
+ job_file="${1:-}"
8
+ [ -n "$job_file" ] && [ -f "$job_file" ] || exit 0
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ LIB_DIR="$SCRIPT_DIR/../lib"
12
+
13
+ . "$LIB_DIR/common.sh"
14
+ . "$LIB_DIR/db.sh"
15
+ . "$LIB_DIR/provider.sh"
16
+
17
+ eagle_ensure_db || exit 0
18
+
19
+ session_id=$(jq -r '.session_id // empty' "$job_file" 2>/dev/null)
20
+ project=$(jq -r '.project // empty' "$job_file" 2>/dev/null)
21
+ agent=$(jq -r '.agent // empty' "$job_file" 2>/dev/null)
22
+ text_content=$(jq -r '.text // empty' "$job_file" 2>/dev/null)
23
+
24
+ if [ -z "$session_id" ] || [ -z "$project" ] || [ -z "$text_content" ]; then
25
+ rm -f "$job_file" 2>/dev/null || true
26
+ exit 0
27
+ fi
28
+
29
+ provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
30
+ if [ "$provider" = "none" ]; then
31
+ eagle_log "INFO" "Summary enrichment skipped: no provider for session=$session_id"
32
+ rm -f "$job_file" 2>/dev/null || true
33
+ exit 0
34
+ fi
35
+
36
+ excerpt=$(printf '%s' "$text_content" | tail -c 3000)
37
+
38
+ 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
+
40
+ Respond with EXACTLY these sections (omit sections with no evidence):
41
+
42
+ REQUEST:
43
+ One-line summary of what the user asked for. No system tags or XML.
44
+
45
+ COMPLETED:
46
+ What was actually accomplished. Be specific about changes made.
47
+
48
+ LEARNED:
49
+ Non-obvious discoveries or insights from the session.
50
+
51
+ DECISIONS:
52
+ Each as: <what was decided> — why: <reason>
53
+
54
+ GOTCHAS:
55
+ Each as: <surprising finding or pitfall>
56
+
57
+ KEY_FILES:
58
+ Each as: <filepath>
59
+
60
+ SESSION TEXT:
61
+ $excerpt"
62
+
63
+ enrich_system="You extract structured facts from Claude Code and Codex development sessions. Output format for decisions: '- Did X — why: Y'. Output format for gotchas: '- Gotcha description'. Be concise. Only include items with clear evidence in the session text. Never fabricate content."
64
+ enrich_result=$(eagle_llm_call "$enrich_prompt" "$enrich_system" 768 2>/dev/null)
65
+ llm_rc=$?
66
+
67
+ if [ $llm_rc -ne 0 ] || [ -z "$enrich_result" ]; then
68
+ eagle_log "WARN" "Summary enrichment failed (rc=$llm_rc) for session=$session_id provider=$provider"
69
+ rm -f "$job_file" 2>/dev/null || true
70
+ exit 0
71
+ fi
72
+
73
+ extract_section() {
74
+ local result="$1" header="$2"
75
+ printf '%s\n' "$result" | awk -v h="$header:" '
76
+ $0 == h || $0 ~ "^"h { found=1; next }
77
+ found && /^[A-Z_]+:/ { exit }
78
+ found && /^[[:space:]]*$/ { next }
79
+ found && /^- / { sub(/^- /, ""); lines[++n] = $0; next }
80
+ found { lines[++n] = $0 }
81
+ END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
82
+ '
83
+ }
84
+
85
+ request=$(extract_section "$enrich_result" "REQUEST" | eagle_redact)
86
+ completed=$(extract_section "$enrich_result" "COMPLETED" | eagle_redact)
87
+ learned=$(extract_section "$enrich_result" "LEARNED" | eagle_redact)
88
+ decisions=$(extract_section "$enrich_result" "DECISIONS" | eagle_redact)
89
+ gotchas=$(extract_section "$enrich_result" "GOTCHAS" | eagle_redact)
90
+ key_files=$(extract_section "$enrich_result" "KEY_FILES" | eagle_redact)
91
+
92
+ if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ] || [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ]; then
93
+ eagle_insert_summary "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent"
94
+ eagle_log "INFO" "Summary enrichment saved for session=$session_id provider=$provider"
95
+ fi
96
+
97
+ rm -f "$job_file" 2>/dev/null || true
98
+ exit 0
@@ -80,16 +80,18 @@ echo ""
80
80
  prereqs_ok=true
81
81
 
82
82
  # sqlite3
83
- if command -v sqlite3 &>/dev/null; then
84
- sqlite_version=$(sqlite3 --version 2>/dev/null | awk '{print $1}')
85
- eagle_ok "sqlite3 ${DIM}($sqlite_version)${RESET}"
83
+ sqlite_path=$(eagle_sqlite_path)
84
+ if [ -n "$sqlite_path" ]; then
85
+ sqlite_version=$(eagle_sqlite_version)
86
+ eagle_ok "sqlite3 ${DIM}($sqlite_version at $sqlite_path)${RESET}"
86
87
  else
87
88
  eagle_fail "sqlite3 not found"
88
89
  if eagle_confirm "Install sqlite3?"; then
89
90
  install_package sqlite3
90
- if command -v sqlite3 &>/dev/null; then
91
- sqlite_version=$(sqlite3 --version 2>/dev/null | awk '{print $1}')
92
- eagle_ok "sqlite3 installed ${DIM}($sqlite_version)${RESET}"
91
+ sqlite_path=$(eagle_sqlite_path)
92
+ if [ -n "$sqlite_path" ]; then
93
+ sqlite_version=$("$sqlite_path" --version 2>/dev/null | awk '{print $1}')
94
+ eagle_ok "sqlite3 installed ${DIM}($sqlite_version at $sqlite_path)${RESET}"
93
95
  else
94
96
  eagle_fail "sqlite3 installation failed"
95
97
  prereqs_ok=false
@@ -100,8 +102,7 @@ else
100
102
  fi
101
103
 
102
104
  # FTS5 support
103
- if command -v sqlite3 &>/dev/null; then
104
- sqlite_path=$(eagle_sqlite_path)
105
+ if [ -n "$sqlite_path" ]; then
105
106
  if eagle_sqlite_supports_fts5; then
106
107
  eagle_ok "FTS5 support ${DIM}($sqlite_path)${RESET}"
107
108
  else
@@ -109,9 +110,9 @@ if command -v sqlite3 &>/dev/null; then
109
110
  eagle_dim "Detected sqlite3: $sqlite_path"
110
111
  sqlite_version=$(eagle_sqlite_version)
111
112
  [ -n "$sqlite_version" ] && eagle_dim "SQLite version: $sqlite_version"
112
- eagle_dim "Run: command -v sqlite3"
113
- eagle_dim "Fix PATH so an FTS5-capable sqlite3 is first."
114
- eagle_dim "macOS: /usr/bin/sqlite3 usually has FTS5; move Android SDK platform-tools later if it shadows sqlite3."
113
+ eagle_dim "Run: eagle-mem health, or set EAGLE_SQLITE_BIN=/absolute/path/to/sqlite3."
114
+ eagle_dim "Eagle Mem prefers known system/Homebrew SQLite paths before PATH shims."
115
+ eagle_dim "macOS: /usr/bin/sqlite3 usually has FTS5; Android SDK sqlite3 shims are ignored when a better binary exists."
115
116
  eagle_dim "Homebrew: brew install sqlite, then prepend /opt/homebrew/opt/sqlite/bin or /usr/local/opt/sqlite/bin."
116
117
  eagle_dim "Linux: install a sqlite3 package compiled with ENABLE_FTS5."
117
118
  prereqs_ok=false
@@ -299,6 +300,7 @@ fi
299
300
  if [ "$claude_found" = true ]; then
300
301
  EM_STATUSLINE="$EAGLE_MEM_DIR/scripts/statusline-em.sh"
301
302
  existing_sl=$(jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null)
303
+ existing_sl_file=$(eagle_statusline_script_from_command "$existing_sl" 2>/dev/null || true)
302
304
 
303
305
  if [ -z "$existing_sl" ]; then
304
306
  # No statusline configured — set up a minimal one that shows Eagle Mem
@@ -309,29 +311,37 @@ input=$(cat)
309
311
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
310
312
  session_id=$(echo "$input" | jq -r '.session_id // .session.id // ""' 2>/dev/null)
311
313
  source "$HOME/.eagle-mem/scripts/statusline-em.sh"
312
- eagle_mem_statusline "$project_dir" "$session_id"
314
+ eagle_mem_statusline "$project_dir" "$session_id" "$input"
313
315
  WRAPPER
314
316
  chmod +x "$wrapper"
315
317
  tmp=$(mktemp)
316
318
  jq --arg cmd "sh $wrapper" '.statusLine = {"type": "command", "command": $cmd, "refreshInterval": 30}' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
317
319
  eagle_ok "Statusline ${DIM}(new — Eagle Mem indicator)${RESET}"
318
- elif echo "$existing_sl" | grep -q "eagle-mem"; then
319
- eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
320
320
  else
321
- # Existing statusline — check if it's a .sh file we can patch
322
- sl_file=$(echo "$existing_sl" | sed 's/^sh //')
323
- if [ -f "$sl_file" ] && ! grep -q "eagle-mem" "$sl_file"; then
324
- eagle_dim " Statusline detected: $sl_file"
325
- eagle_dim " To add Eagle Mem, add this snippet before your ASSEMBLE section:"
326
- echo ""
327
- eagle_dim " # ── Eagle Mem ──"
328
- eagle_dim " em_section=\"\""
329
- eagle_dim " if [ -f \"\$HOME/.eagle-mem/scripts/statusline-em.sh\" ]; then"
330
- eagle_dim " source \"\$HOME/.eagle-mem/scripts/statusline-em.sh\""
331
- eagle_dim " em_section=\$(eagle_mem_statusline \"\$project_dir\" \"\$session_id\")"
332
- eagle_dim " fi"
333
- echo ""
334
- eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
321
+ # Existing statusline — if it points at a shell script, inspect the
322
+ # target file. Custom HUD commands often do not include "eagle-mem" in
323
+ # the command string even when the script contains an embedded block.
324
+ sl_file="$existing_sl_file"
325
+ if [ -n "$sl_file" ] && [ -f "$sl_file" ]; then
326
+ if eagle_patch_statusline_script "$sl_file"; then
327
+ eagle_ok "Statusline ${DIM}(patched existing Eagle Mem block)${RESET}"
328
+ elif eagle_statusline_script_uses_input "$sl_file"; then
329
+ eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
330
+ else
331
+ eagle_dim " Statusline detected: $sl_file"
332
+ eagle_dim " To add Eagle Mem, add this snippet before your ASSEMBLE section:"
333
+ echo ""
334
+ eagle_dim " # ── Eagle Mem ──"
335
+ eagle_dim " em_section=\"\""
336
+ eagle_dim " if [ -f \"\$HOME/.eagle-mem/scripts/statusline-em.sh\" ]; then"
337
+ eagle_dim " source \"\$HOME/.eagle-mem/scripts/statusline-em.sh\""
338
+ eagle_dim " em_section=\$(eagle_mem_statusline \"\$project_dir\" \"\$session_id\" \"\$input\")"
339
+ eagle_dim " fi"
340
+ echo ""
341
+ eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
342
+ fi
343
+ elif echo "$existing_sl" | grep -q "eagle-mem"; then
344
+ eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
335
345
  else
336
346
  eagle_ok "Statusline ${DIM}(existing — cannot auto-patch; add Eagle Mem manually)${RESET}"
337
347
  fi
@@ -6,15 +6,19 @@
6
6
  eagle_mem_statusline() {
7
7
  local project_dir="${1:-}"
8
8
  local session_id="${2:-}"
9
+ local statusline_input="${3:-}"
9
10
  local em_db="$HOME/.eagle-mem/memory.db"
10
11
  [ -f "$em_db" ] || return
11
12
 
12
13
  local SCRIPT_DIR; SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
14
  . "$SCRIPT_DIR/../lib/common.sh"
15
+ local sqlite_bin
16
+ sqlite_bin=$(eagle_sqlite_path)
17
+ [ -n "$sqlite_bin" ] || return
14
18
 
15
19
  local proj
16
20
  [ -z "$project_dir" ] && project_dir="$(pwd)"
17
- proj=$(eagle_project_from_cwd "$project_dir")
21
+ proj=$(eagle_project_from_statusline_input "$statusline_input" "$project_dir" "$project_dir" "$session_id")
18
22
  [ -z "$proj" ] && return
19
23
 
20
24
  proj=$(eagle_sql_escape "$proj")
@@ -23,9 +27,9 @@ eagle_mem_statusline() {
23
27
  version=$(tr -d '[:space:]' < "$HOME/.eagle-mem/.version" 2>/dev/null)
24
28
  [ -z "$version" ] && version="?"
25
29
  cnt=$(echo ".headers off
26
- SELECT COUNT(*) FROM sessions WHERE project = '${proj}';" | sqlite3 "$em_db" 2>/dev/null | tr -d '[:space:]')
30
+ SELECT COUNT(*) FROM sessions WHERE project = '${proj}';" | "$sqlite_bin" "$em_db" 2>/dev/null | tr -d '[:space:]')
27
31
  mem=$(echo ".headers off
28
- SELECT COUNT(*) FROM agent_memories WHERE project = '${proj}';" | sqlite3 "$em_db" 2>/dev/null | tr -d '[:space:]')
32
+ SELECT COUNT(*) FROM agent_memories WHERE project = '${proj}';" | "$sqlite_bin" "$em_db" 2>/dev/null | tr -d '[:space:]')
29
33
  if [ -n "$session_id" ] && [ -f "$HOME/.eagle-mem/.turn-counter.${session_id}" ]; then
30
34
  turns=$(tr -d '[:space:]' < "$HOME/.eagle-mem/.turn-counter.${session_id}" 2>/dev/null)
31
35
  else
@@ -57,5 +61,5 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
57
61
  fi
58
62
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // empty' 2>/dev/null)
59
63
  session_id=$(echo "$input" | jq -r '.session_id // .session.id // empty' 2>/dev/null)
60
- eagle_mem_statusline "${project_dir:-$(pwd)}" "$session_id"
64
+ eagle_mem_statusline "${project_dir:-$(pwd)}" "$session_id" "$input"
61
65
  fi
package/scripts/update.sh CHANGED
@@ -153,12 +153,22 @@ input=$(cat)
153
153
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
154
154
  session_id=$(echo "$input" | jq -r '.session_id // .session.id // ""' 2>/dev/null)
155
155
  source "$HOME/.eagle-mem/scripts/statusline-em.sh"
156
- eagle_mem_statusline "$project_dir" "$session_id"
156
+ eagle_mem_statusline "$project_dir" "$session_id" "$input"
157
157
  WRAPPER
158
158
  chmod +x "$statusline_wrapper"
159
159
  eagle_ok "Statusline wrapper updated"
160
160
  fi
161
161
 
162
+ if [ "$claude_found" = true ] && [ -f "$SETTINGS" ] && command -v jq >/dev/null 2>&1; then
163
+ existing_sl=$(jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null)
164
+ existing_sl_file=$(eagle_statusline_script_from_command "$existing_sl" 2>/dev/null || true)
165
+ if eagle_patch_statusline_script "$existing_sl_file"; then
166
+ eagle_ok "Statusline custom Eagle Mem block patched"
167
+ elif [ -n "$existing_sl_file" ] && [ -f "$existing_sl_file" ] && grep -q "eagle-mem" "$existing_sl_file" && ! eagle_statusline_script_uses_input "$existing_sl_file"; then
168
+ eagle_warn "Statusline custom Eagle Mem block needs manual input-aware update"
169
+ fi
170
+ fi
171
+
162
172
  # ─── Backfill project names ───────────────────────────────
163
173
 
164
174
  backfilled=$(eagle_backfill_projects 2>/dev/null)