eagle-mem 4.9.3 → 4.9.4

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
@@ -152,6 +152,10 @@ 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.4 Patch
156
+
157
+ 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.
158
+
155
159
  ### v4.9.3 Patch
156
160
 
157
161
  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/lib/common.sh CHANGED
@@ -210,14 +210,231 @@ eagle_transcript_first_cwd() {
210
210
  local transcript_path="${1:-}"
211
211
  [ -f "$transcript_path" ] || return 1
212
212
 
213
+ local head_lines
214
+ head_lines=$(sed -n '1,200p' "$transcript_path" 2>/dev/null || true)
215
+ [ -n "$head_lines" ] || return 1
216
+
213
217
  local cwd
214
- cwd=$(sed -n '1,200p' "$transcript_path" 2>/dev/null \
215
- | jq -r 'select((.cwd? // "") != "") | .cwd' 2>/dev/null \
218
+ cwd=$(printf '%s\n' "$head_lines" \
219
+ | jq -r '
220
+ [
221
+ (.payload? | objects | .workspace? | objects | .project_dir? | strings),
222
+ (.workspace? | objects | .project_dir? | strings),
223
+ (.payload? | objects | .workspace? | objects | .current_dir? | strings),
224
+ (.workspace? | objects | .current_dir? | strings),
225
+ (.cwd? | strings),
226
+ (.payload? | objects | .cwd? | strings),
227
+ (.payload? | objects | .current_dir? | strings)
228
+ ]
229
+ | .[0] // empty
230
+ ' 2>/dev/null \
216
231
  | awk 'NF { print; exit }' || true)
232
+
217
233
  [ -n "$cwd" ] || return 1
218
234
  printf '%s\n' "$cwd"
219
235
  }
220
236
 
237
+ eagle_session_project_marker_file() {
238
+ local session_id="${1:-}"
239
+ eagle_validate_session_id "$session_id" || return 1
240
+ mkdir -p "$EAGLE_MEM_DIR/session-projects" 2>/dev/null || return 1
241
+ printf '%s/session-projects/%s\n' "$EAGLE_MEM_DIR" "$session_id"
242
+ }
243
+
244
+ eagle_get_session_project_marker() {
245
+ local marker
246
+ marker=$(eagle_session_project_marker_file "$1") || return 1
247
+ [ -s "$marker" ] || return 1
248
+ awk 'NF { print; exit }' "$marker"
249
+ }
250
+
251
+ eagle_remember_session_project() {
252
+ local session_id="${1:-}"
253
+ local project="${2:-}"
254
+ local force="${3:-0}"
255
+ [ -n "$project" ] || return 1
256
+
257
+ local marker
258
+ marker=$(eagle_session_project_marker_file "$session_id") || return 1
259
+ if [ "$force" = "1" ] || [ ! -s "$marker" ]; then
260
+ printf '%s\n' "$project" > "$marker" 2>/dev/null || return 1
261
+ fi
262
+ }
263
+
264
+ eagle_get_session_project_light() {
265
+ local session_id="${1:-}"
266
+ eagle_validate_session_id "$session_id" || return 1
267
+ command -v sqlite3 >/dev/null 2>&1 || return 1
268
+ [ -f "$EAGLE_MEM_DB" ] || return 1
269
+
270
+ local sid_sql project
271
+ sid_sql=$(eagle_sql_escape "$session_id")
272
+ project=$(sqlite3 "$EAGLE_MEM_DB" "SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
273
+ [ -n "$project" ] || return 1
274
+ printf '%s\n' "$project"
275
+ }
276
+
277
+ eagle_project_has_table_row() {
278
+ local table="${1:-}"
279
+ local project="${2:-}"
280
+ [ -n "$table" ] && [ -n "$project" ] || return 1
281
+ command -v sqlite3 >/dev/null 2>&1 || return 1
282
+ [ -f "$EAGLE_MEM_DB" ] || return 1
283
+
284
+ local project_sql found
285
+ project_sql=$(eagle_sql_escape "$project")
286
+ found=$(sqlite3 "$EAGLE_MEM_DB" "SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
287
+ [ "$found" = "1" ]
288
+ }
289
+
290
+ eagle_project_from_existing_ancestor() {
291
+ local path="${1:-}"
292
+ [ -n "$path" ] || return 1
293
+
294
+ local current key
295
+ current=$(eagle_normalize_project_path "$path")
296
+ eagle_is_ephemeral_project_path "$current" && return 1
297
+
298
+ while [ -n "$current" ] && [ "$current" != "/" ]; do
299
+ key=$(eagle_project_key_from_target_dir "$current")
300
+ if [ -n "$key" ]; then
301
+ # Prefer ancestors with durable memory/summary content. Session-only
302
+ # rows can be created by the very folder drift this helper repairs.
303
+ if eagle_project_has_table_row "agent_memories" "$key" \
304
+ || eagle_project_has_table_row "summaries" "$key"; then
305
+ printf '%s\n' "$key"
306
+ return 0
307
+ fi
308
+ fi
309
+
310
+ [ "$current" = "$HOME" ] && break
311
+ current=$(dirname "$current")
312
+ done
313
+
314
+ return 1
315
+ }
316
+
317
+ eagle_project_from_workspace_path() {
318
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
319
+ printf '%s\n' "$EAGLE_MEM_PROJECT"
320
+ return 0
321
+ fi
322
+
323
+ local path="${1:-}"
324
+ [ -n "$path" ] || return 1
325
+
326
+ local resolved worktree_project git_root project
327
+ resolved=$(eagle_normalize_project_path "$path")
328
+ eagle_is_ephemeral_project_path "$resolved" && return 1
329
+
330
+ if worktree_project=$(eagle_project_key_for_worktree_path "$resolved"); then
331
+ printf '%s\n' "$worktree_project"
332
+ return 0
333
+ fi
334
+
335
+ git_root=$(git -C "$resolved" rev-parse --show-toplevel 2>/dev/null)
336
+ if [ -n "$git_root" ]; then
337
+ project=$(eagle_project_key_from_target_dir "$git_root")
338
+ [ -n "$project" ] && { printf '%s\n' "$project"; return 0; }
339
+ fi
340
+
341
+ if project=$(eagle_project_from_existing_ancestor "$path"); then
342
+ printf '%s\n' "$project"
343
+ return 0
344
+ fi
345
+
346
+ project=$(eagle_project_from_path_no_git "$path")
347
+ [ -n "$project" ] || return 1
348
+ printf '%s\n' "$project"
349
+ }
350
+
351
+ eagle_project_from_transcript_start() {
352
+ local transcript_path="${1:-}"
353
+ local cwd="${2:-}"
354
+ [ -f "$transcript_path" ] || return 1
355
+
356
+ local transcript_cwd project
357
+ if project=$(eagle_project_from_claude_transcript "$transcript_path" "$cwd"); then
358
+ printf '%s\n' "$project"
359
+ return 0
360
+ fi
361
+
362
+ transcript_cwd=$(eagle_transcript_first_cwd "$transcript_path") || return 1
363
+ if [ -n "$cwd" ] && ! eagle_path_is_same_or_child "$transcript_cwd" "$cwd"; then
364
+ return 1
365
+ fi
366
+
367
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
368
+ [ -n "$project" ] || return 1
369
+ printf '%s\n' "$project"
370
+ }
371
+
372
+ eagle_project_from_statusline_input() {
373
+ local input="${1:-}"
374
+ local project_dir="${2:-}"
375
+ local cwd="${3:-}"
376
+ local session_id="${4:-}"
377
+ local project workspace_project_dir transcript_path
378
+
379
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
380
+ printf '%s\n' "$EAGLE_MEM_PROJECT"
381
+ return
382
+ fi
383
+
384
+ if [ -z "$session_id" ] && [ -n "$input" ]; then
385
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // .session.id // empty' 2>/dev/null)
386
+ fi
387
+
388
+ if [ -n "$input" ]; then
389
+ workspace_project_dir=$(printf '%s' "$input" | jq -r '.workspace.project_dir // empty' 2>/dev/null)
390
+ transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
391
+ [ -z "$cwd" ] && cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
392
+ fi
393
+
394
+ # Explicit workspace project and transcript start are stronger than older
395
+ # cached session rows because they repair pre-fix sessions that were stored
396
+ # under a nested folder key.
397
+ if [ -n "$workspace_project_dir" ]; then
398
+ project=$(eagle_project_from_workspace_path "$workspace_project_dir")
399
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
400
+ fi
401
+
402
+ if [ -n "$transcript_path" ]; then
403
+ if project=$(eagle_project_from_transcript_start "$transcript_path" "${cwd:-$project_dir}"); then
404
+ printf '%s\n' "$project"
405
+ return
406
+ fi
407
+ fi
408
+
409
+ if [ -n "$session_id" ]; then
410
+ if project=$(eagle_get_session_project_light "$session_id"); then
411
+ printf '%s\n' "$project"
412
+ return
413
+ fi
414
+ if project=$(eagle_get_session_project_marker "$session_id"); then
415
+ printf '%s\n' "$project"
416
+ return
417
+ fi
418
+ fi
419
+
420
+ if [ -z "$project_dir" ] && [ -n "$input" ]; then
421
+ project_dir=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
422
+ fi
423
+ if [ -n "$project_dir" ]; then
424
+ project=$(eagle_project_from_workspace_path "$project_dir")
425
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
426
+ fi
427
+
428
+ if [ -z "$cwd" ] && [ -n "$input" ]; then
429
+ cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
430
+ fi
431
+ if [ -n "$cwd" ]; then
432
+ project=$(eagle_project_from_workspace_path "$cwd")
433
+ [ -n "$project" ] && { printf '%s\n' "$project"; return; }
434
+ fi
435
+ eagle_project_from_cwd "$cwd"
436
+ }
437
+
221
438
  eagle_project_from_claude_project_dir() {
222
439
  local project_dir="${1:-}"
223
440
  project_dir="${project_dir%/}"
@@ -228,7 +445,7 @@ eagle_project_from_claude_project_dir() {
228
445
  [ -f "$jsonl" ] || continue
229
446
  cwd=$(eagle_transcript_first_cwd "$jsonl")
230
447
  [ -z "$cwd" ] && continue
231
- project=$(eagle_project_from_path_no_git "$cwd")
448
+ project=$(eagle_project_from_workspace_path "$cwd")
232
449
  [ -n "$project" ] && { printf '%s\n' "$project"; return 0; }
233
450
  done
234
451
 
@@ -252,7 +469,7 @@ eagle_project_from_claude_transcript() {
252
469
  return 1
253
470
  fi
254
471
 
255
- project=$(eagle_project_from_path_no_git "$transcript_cwd")
472
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
256
473
  [ -n "$project" ] || return 1
257
474
  printf '%s\n' "$project"
258
475
  }
@@ -265,16 +482,54 @@ eagle_project_from_hook_input() {
265
482
  return
266
483
  fi
267
484
 
268
- local cwd transcript_path project
485
+ local session_id cwd transcript_path workspace_project project
486
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
269
487
  cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
270
488
  transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
271
489
 
490
+ workspace_project=$(printf '%s' "$input" | jq -r '.workspace.project_dir // empty' 2>/dev/null)
491
+ if [ -n "$workspace_project" ]; then
492
+ project=$(eagle_project_from_workspace_path "$workspace_project")
493
+ if [ -n "$project" ]; then
494
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
495
+ printf '%s\n' "$project"
496
+ return
497
+ fi
498
+ fi
499
+
272
500
  if project=$(eagle_project_from_claude_transcript "$transcript_path" "$cwd"); then
501
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
273
502
  printf '%s\n' "$project"
274
503
  return
275
504
  fi
276
505
 
277
- eagle_project_from_cwd "$cwd"
506
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
507
+ local transcript_cwd
508
+ transcript_cwd=$(eagle_transcript_first_cwd "$transcript_path")
509
+ if [ -n "$transcript_cwd" ]; then
510
+ project=$(eagle_project_from_workspace_path "$transcript_cwd")
511
+ if [ -n "$project" ]; then
512
+ [ -n "$session_id" ] && eagle_remember_session_project "$session_id" "$project" 1 >/dev/null 2>&1
513
+ printf '%s\n' "$project"
514
+ return
515
+ fi
516
+ fi
517
+ fi
518
+
519
+ if [ -n "$session_id" ]; then
520
+ if project=$(eagle_get_session_project_light "$session_id"); then
521
+ printf '%s\n' "$project"
522
+ return
523
+ fi
524
+ if project=$(eagle_get_session_project_marker "$session_id"); then
525
+ printf '%s\n' "$project"
526
+ return
527
+ fi
528
+ fi
529
+
530
+ project=$(eagle_project_from_cwd "$cwd")
531
+ [ -n "$session_id" ] && [ -n "$project" ] && eagle_remember_session_project "$session_id" "$project" 0 >/dev/null 2>&1
532
+ printf '%s\n' "$project"
278
533
  }
279
534
 
280
535
  eagle_project_file_path() {
@@ -810,6 +1065,114 @@ eagle_collect_files() {
810
1065
  fi
811
1066
  }
812
1067
 
1068
+ eagle_statusline_script_from_command() {
1069
+ local cmd="${1:-}"
1070
+ [ -z "$cmd" ] && return 1
1071
+
1072
+ cmd="${cmd#sh }"
1073
+ cmd="${cmd#bash }"
1074
+ cmd="${cmd#/bin/sh }"
1075
+ cmd="${cmd#/bin/bash }"
1076
+ cmd="${cmd#zsh }"
1077
+ cmd="${cmd#/bin/zsh }"
1078
+ cmd="${cmd%\"}"
1079
+ cmd="${cmd#\"}"
1080
+ cmd="${cmd%\'}"
1081
+ cmd="${cmd#\'}"
1082
+
1083
+ case "$cmd" in
1084
+ "~/"*) cmd="$HOME/${cmd#\~/}" ;;
1085
+ "\$HOME/"*) cmd="$HOME/${cmd#\$HOME/}" ;;
1086
+ "\${HOME}/"*) cmd="$HOME/${cmd#\$\{HOME\}/}" ;;
1087
+ esac
1088
+
1089
+ if [ -L "$cmd" ]; then
1090
+ local link_target link_dir
1091
+ link_target=$(readlink "$cmd" 2>/dev/null || true)
1092
+ if [ -n "$link_target" ]; then
1093
+ case "$link_target" in
1094
+ /*) cmd="$link_target" ;;
1095
+ *)
1096
+ link_dir=$(cd "$(dirname "$cmd")" && pwd -P) || return 1
1097
+ cmd="$link_dir/$link_target"
1098
+ ;;
1099
+ esac
1100
+ fi
1101
+ fi
1102
+
1103
+ [ -f "$cmd" ] || return 1
1104
+ printf '%s\n' "$cmd"
1105
+ }
1106
+
1107
+ eagle_statusline_script_uses_input() {
1108
+ local sl_file="${1:-}"
1109
+ [ -f "$sl_file" ] || return 1
1110
+ grep -Eq 'eagle_project_from_statusline_input|eagle_mem_statusline.*(\$\{input:-\}|\$input)' "$sl_file"
1111
+ }
1112
+
1113
+ eagle_patch_statusline_script() {
1114
+ local sl_file="${1:-}"
1115
+ [ -f "$sl_file" ] || return 1
1116
+ command -v perl >/dev/null 2>&1 || return 1
1117
+
1118
+ if [ -L "$sl_file" ]; then
1119
+ local link_target link_dir
1120
+ link_target=$(readlink "$sl_file" 2>/dev/null || true)
1121
+ if [ -n "$link_target" ]; then
1122
+ case "$link_target" in
1123
+ /*) sl_file="$link_target" ;;
1124
+ *)
1125
+ link_dir=$(cd "$(dirname "$sl_file")" && pwd -P) || return 1
1126
+ sl_file="$link_dir/$link_target"
1127
+ ;;
1128
+ esac
1129
+ fi
1130
+ fi
1131
+
1132
+ local tmp backup mode
1133
+ tmp=$(mktemp) || return 1
1134
+ cp "$sl_file" "$tmp" || { rm -f "$tmp"; return 1; }
1135
+
1136
+ if ! grep -Eq 'eagle_mem_statusline|eagle_project_from_cwd|claude_memories|agent_memories|\.eagle-mem/scripts/statusline-em' "$tmp" 2>/dev/null; then
1137
+ rm -f "$tmp"
1138
+ return 1
1139
+ fi
1140
+
1141
+ perl -0pi -e '
1142
+ s/(project_dir=\$\(echo "\$input" \| jq -r \x27)\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1143
+ s/(project_dir=\$\(echo "\$input" \| jq -r ")\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1144
+ s/eagle_mem_statusline "\$project_dir" "\$session_id" "\$\{input\}"/eagle_mem_statusline "\$project_dir" "\$session_id" "\${input:-}"/g;
1145
+ s/eagle_mem_statusline "\$project_dir" "\$session_id"(?=[\)\n;])/eagle_mem_statusline "\$project_dir" "\$session_id" "\${input:-}"/g;
1146
+ s/eagle_mem_statusline "\$project_dir"(?=[\)\n;])/eagle_mem_statusline "\$project_dir" "\${session_id:-}" "\${input:-}"/g;
1147
+ s/eagle_project_from_cwd "\$cwd"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1148
+ s/eagle_project_from_cwd "\$project_dir"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1149
+ s/eagle_project_from_cwd "\${project_dir:-\$cwd}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1150
+ s/eagle_project_from_cwd "\${project_dir:-\$(pwd)}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1151
+ s/eagle_project_from_cwd "\${eagle_mem_project_dir:-\${project_dir:-\$cwd}}"/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1152
+ s/eagle_project_from_cwd\("\$cwd"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1153
+ s/eagle_project_from_cwd\("\$project_dir"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1154
+ s/eagle_project_from_cwd\("\${project_dir:-\$cwd}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1155
+ s/eagle_project_from_cwd\("\${project_dir:-\$(pwd)}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1156
+ s/eagle_project_from_cwd\("\${eagle_mem_project_dir:-\${project_dir:-\$cwd}}"\)/eagle_project_from_statusline_input "\${input:-}" "\${project_dir:-}" "\${cwd:-}" "\${session_id:-}"/g;
1157
+ s/FROM claude_memories WHERE project/FROM agent_memories WHERE project/g;
1158
+ ' "$tmp" || { rm -f "$tmp"; return 1; }
1159
+
1160
+ if cmp -s "$sl_file" "$tmp"; then
1161
+ rm -f "$tmp"
1162
+ return 1
1163
+ fi
1164
+
1165
+ backup="${sl_file}.eagle-mem.bak-$(date -u +%Y%m%dT%H%M%SZ)"
1166
+ cp "$sl_file" "$backup" 2>/dev/null || true
1167
+ mode=$(stat -f %Lp "$sl_file" 2>/dev/null || stat -c %a "$sl_file" 2>/dev/null || echo "")
1168
+ if ! mv "$tmp" "$sl_file"; then
1169
+ rm -f "$tmp"
1170
+ return 1
1171
+ fi
1172
+ [ -n "$mode" ] && chmod "$mode" "$sl_file" 2>/dev/null || chmod +x "$sl_file" 2>/dev/null || true
1173
+ return 0
1174
+ }
1175
+
813
1176
  _eagle_claude_md_section() {
814
1177
  cat << 'EAGLE_MD'
815
1178
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.9.3",
3
+ "version": "4.9.4",
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"
@@ -299,6 +299,7 @@ fi
299
299
  if [ "$claude_found" = true ]; then
300
300
  EM_STATUSLINE="$EAGLE_MEM_DIR/scripts/statusline-em.sh"
301
301
  existing_sl=$(jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null)
302
+ existing_sl_file=$(eagle_statusline_script_from_command "$existing_sl" 2>/dev/null || true)
302
303
 
303
304
  if [ -z "$existing_sl" ]; then
304
305
  # No statusline configured — set up a minimal one that shows Eagle Mem
@@ -309,29 +310,37 @@ input=$(cat)
309
310
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
310
311
  session_id=$(echo "$input" | jq -r '.session_id // .session.id // ""' 2>/dev/null)
311
312
  source "$HOME/.eagle-mem/scripts/statusline-em.sh"
312
- eagle_mem_statusline "$project_dir" "$session_id"
313
+ eagle_mem_statusline "$project_dir" "$session_id" "$input"
313
314
  WRAPPER
314
315
  chmod +x "$wrapper"
315
316
  tmp=$(mktemp)
316
317
  jq --arg cmd "sh $wrapper" '.statusLine = {"type": "command", "command": $cmd, "refreshInterval": 30}' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
317
318
  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
319
  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}"
320
+ # Existing statusline — if it points at a shell script, inspect the
321
+ # target file. Custom HUD commands often do not include "eagle-mem" in
322
+ # the command string even when the script contains an embedded block.
323
+ sl_file="$existing_sl_file"
324
+ if [ -n "$sl_file" ] && [ -f "$sl_file" ]; then
325
+ if eagle_patch_statusline_script "$sl_file"; then
326
+ eagle_ok "Statusline ${DIM}(patched existing Eagle Mem block)${RESET}"
327
+ elif eagle_statusline_script_uses_input "$sl_file"; then
328
+ eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
329
+ else
330
+ eagle_dim " Statusline detected: $sl_file"
331
+ eagle_dim " To add Eagle Mem, add this snippet before your ASSEMBLE section:"
332
+ echo ""
333
+ eagle_dim " # ── Eagle Mem ──"
334
+ eagle_dim " em_section=\"\""
335
+ eagle_dim " if [ -f \"\$HOME/.eagle-mem/scripts/statusline-em.sh\" ]; then"
336
+ eagle_dim " source \"\$HOME/.eagle-mem/scripts/statusline-em.sh\""
337
+ eagle_dim " em_section=\$(eagle_mem_statusline \"\$project_dir\" \"\$session_id\" \"\$input\")"
338
+ eagle_dim " fi"
339
+ echo ""
340
+ eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
341
+ fi
342
+ elif echo "$existing_sl" | grep -q "eagle-mem"; then
343
+ eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
335
344
  else
336
345
  eagle_ok "Statusline ${DIM}(existing — cannot auto-patch; add Eagle Mem manually)${RESET}"
337
346
  fi
@@ -6,6 +6,7 @@
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
 
@@ -14,7 +15,7 @@ eagle_mem_statusline() {
14
15
 
15
16
  local proj
16
17
  [ -z "$project_dir" ] && project_dir="$(pwd)"
17
- proj=$(eagle_project_from_cwd "$project_dir")
18
+ proj=$(eagle_project_from_statusline_input "$statusline_input" "$project_dir" "$project_dir" "$session_id")
18
19
  [ -z "$proj" ] && return
19
20
 
20
21
  proj=$(eagle_sql_escape "$proj")
@@ -57,5 +58,5 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
57
58
  fi
58
59
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // empty' 2>/dev/null)
59
60
  session_id=$(echo "$input" | jq -r '.session_id // .session.id // empty' 2>/dev/null)
60
- eagle_mem_statusline "${project_dir:-$(pwd)}" "$session_id"
61
+ eagle_mem_statusline "${project_dir:-$(pwd)}" "$session_id" "$input"
61
62
  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)