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/scripts/index.sh CHANGED
@@ -40,6 +40,15 @@ TARGET_DIR="${args[0]:-.}"
40
40
  TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
41
41
  PROJECT=$(eagle_project_from_cwd "$TARGET_DIR")
42
42
 
43
+ TMPDIR_IDX=""
44
+ cleanup_index() {
45
+ local rc=$?
46
+ [ -n "${TMPDIR_IDX:-}" ] && rm -rf "$TMPDIR_IDX" 2>/dev/null || true
47
+ eagle_run_finish "$rc" "$LINENO"
48
+ }
49
+ eagle_run_start "index" "$PROJECT" "$TARGET_DIR"
50
+ trap cleanup_index EXIT
51
+
43
52
  CHUNK_SIZE="${EAGLE_MEM_CHUNK_SIZE:-80}"
44
53
  if ! [[ "$CHUNK_SIZE" =~ ^[0-9]+$ ]] || [ "$CHUNK_SIZE" -lt 1 ]; then
45
54
  CHUNK_SIZE=80
@@ -89,13 +98,32 @@ ext_to_lang() {
89
98
  esac
90
99
  }
91
100
 
101
+ sql_literal_expr() {
102
+ awk '
103
+ BEGIN { first = 1 }
104
+ {
105
+ gsub(/\047/, "\047\047")
106
+ if (!first) {
107
+ printf "||char(10)||"
108
+ }
109
+ printf "\047%s\047", $0
110
+ first = 0
111
+ }
112
+ END {
113
+ if (first) {
114
+ printf "\047\047"
115
+ }
116
+ }
117
+ '
118
+ }
119
+
92
120
  # ─── Collect files ─────────────────────────────────────────
93
121
 
94
122
  TMPDIR_IDX=$(mktemp -d)
95
- trap 'rm -rf "$TMPDIR_IDX"' EXIT
96
123
 
97
124
  ALL_FILES="$TMPDIR_IDX/all_files"
98
125
 
126
+ eagle_run_step "collect_files"
99
127
  eagle_collect_files "$TARGET_DIR" "$ALL_FILES"
100
128
 
101
129
  # Filter to source files only, skip large files
@@ -128,6 +156,7 @@ NEEDS_INDEX="$TMPDIR_IDX/needs_index"
128
156
  skipped_count=0
129
157
 
130
158
  if [ "$force" = true ]; then
159
+ eagle_run_step "force_clear_index_state"
131
160
  eagle_info "Force rebuild requested: clearing chunks, declarations, and import edges"
132
161
  eagle_graph_clear_index_state "$PROJECT"
133
162
  fi
@@ -164,6 +193,7 @@ if [ "$needs_count" -eq 0 ]; then
164
193
  fi
165
194
 
166
195
  eagle_info "$needs_count files to index"
196
+ eagle_run_step "index_files count=$needs_count"
167
197
 
168
198
  # ─── Chunk and index files ─────────────────────────────────
169
199
 
@@ -194,11 +224,11 @@ DELETE FROM code_chunks WHERE project = '$project_sql' AND file_path = '$file_sq
194
224
  [ "$end" -gt "$total_lines" ] && end="$total_lines"
195
225
 
196
226
  content=$(sed -n "${start},${end}p" "$full_path" | eagle_redact)
197
- content_sql=$(eagle_sql_escape "$content")
227
+ content_expr=$(printf '%s\n' "$content" | sql_literal_expr)
198
228
 
199
229
  txn_sql+="
200
230
  INSERT INTO code_chunks (project, file_path, language, start_line, end_line, content, mtime)
201
- VALUES ('$project_sql', '$file_sql', '$lang_sql', $start, $end, '$content_sql', $current_mtime);"
231
+ VALUES ('$project_sql', '$file_sql', '$lang_sql', $start, $end, $content_expr, $current_mtime);"
202
232
 
203
233
  chunk_count=$((chunk_count + 1))
204
234
  start=$((end + 1))
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Command Run Logs
4
+ # ═══════════════════════════════════════════════════════════
5
+ set -euo pipefail
6
+
7
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ LIB_DIR="$SCRIPTS_DIR/../lib"
9
+
10
+ . "$SCRIPTS_DIR/style.sh"
11
+ . "$LIB_DIR/common.sh"
12
+
13
+ cmd="${1:-list}"
14
+ [ $# -gt 0 ] && shift || true
15
+
16
+ show_help() {
17
+ cat <<EOF
18
+ Usage: eagle-mem logs [list|tail|show] [run-id-or-path]
19
+
20
+ Commands:
21
+ list Show recent command-scoped run logs
22
+ tail [id|path] Tail a run log, or the latest run log when omitted
23
+ show <id|path> Print a run log
24
+ EOF
25
+ }
26
+
27
+ resolve_log_path() {
28
+ local ref="${1:-}"
29
+ if [ -z "$ref" ]; then
30
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | sed -n '1p'
31
+ return 0
32
+ fi
33
+ if [ -f "$ref" ]; then
34
+ printf '%s\n' "$ref"
35
+ return 0
36
+ fi
37
+ if [ -f "$EAGLE_RUNS_DIR/$ref" ]; then
38
+ printf '%s\n' "$EAGLE_RUNS_DIR/$ref"
39
+ return 0
40
+ fi
41
+ if [ -f "$EAGLE_RUNS_DIR/$ref.log" ]; then
42
+ printf '%s\n' "$EAGLE_RUNS_DIR/$ref.log"
43
+ return 0
44
+ fi
45
+ return 1
46
+ }
47
+
48
+ case "$cmd" in
49
+ -h|--help|help)
50
+ show_help
51
+ ;;
52
+ list)
53
+ eagle_header "Run Logs"
54
+ if ! ls "$EAGLE_RUNS_DIR"/*.log >/dev/null 2>&1; then
55
+ eagle_info "No run logs found yet"
56
+ eagle_dim "Run eagle-mem scan, index, or curate to create command-scoped logs."
57
+ exit 0
58
+ fi
59
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | head -20 | while IFS= read -r log_path; do
60
+ first_line=$(sed -n '1p' "$log_path" 2>/dev/null)
61
+ run_id=$(basename "$log_path" .log)
62
+ printf ' %s %s\n' "$run_id" "$first_line"
63
+ done
64
+ ;;
65
+ tail)
66
+ log_path=$(resolve_log_path "${1:-}") || {
67
+ eagle_err "Run log not found"
68
+ exit 1
69
+ }
70
+ tail -n "${EAGLE_LOG_TAIL_LINES:-80}" "$log_path"
71
+ ;;
72
+ show)
73
+ log_path=$(resolve_log_path "${1:-}") || {
74
+ eagle_err "Run log not found"
75
+ exit 1
76
+ }
77
+ cat "$log_path"
78
+ ;;
79
+ *)
80
+ eagle_err "Unknown logs command: $cmd"
81
+ show_help
82
+ exit 1
83
+ ;;
84
+ esac
package/scripts/scan.sh CHANGED
@@ -29,6 +29,15 @@ TARGET_DIR="${args[0]:-.}"
29
29
  TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
30
30
  PROJECT=$(eagle_project_from_cwd "$TARGET_DIR")
31
31
 
32
+ TMPFILE=""
33
+ cleanup_scan() {
34
+ local rc=$?
35
+ [ -n "${TMPFILE:-}" ] && rm -f "$TMPFILE" "${TMPFILE}.analysis" 2>/dev/null || true
36
+ eagle_run_finish "$rc" "$LINENO"
37
+ }
38
+ eagle_run_start "scan" "$PROJECT" "$TARGET_DIR"
39
+ trap cleanup_scan EXIT
40
+
32
41
  eagle_header "Scan"
33
42
  eagle_info "Scanning ${BOLD}$PROJECT${RESET} at $TARGET_DIR"
34
43
  echo ""
@@ -52,8 +61,8 @@ if git -C "$TARGET_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
52
61
  fi
53
62
 
54
63
  TMPFILE=$(mktemp)
55
- trap 'rm -f "$TMPFILE"' EXIT
56
64
 
65
+ eagle_run_step "collect_files"
57
66
  eagle_collect_files "$TARGET_DIR" "$TMPFILE"
58
67
 
59
68
  total_files=$(wc -l < "$TMPFILE" | tr -d ' ')
@@ -67,6 +76,7 @@ eagle_ok "$total_files files found"
67
76
 
68
77
  # ─── Language breakdown (bash 3 compatible — no assoc arrays) ──
69
78
 
79
+ eagle_run_step "language_breakdown"
70
80
  while IFS= read -r file; do
71
81
  ext="${file##*.}"
72
82
  [ "$ext" = "$file" ] && continue
package/scripts/test.sh CHANGED
@@ -53,6 +53,7 @@ run_check "Installer And Updater (install / update syntax)" "bash -n \"$SCRIPTS_
53
53
  run_check "Code Scan And Index (scan / index syntax)" "bash -n \"$SCRIPTS_DIR/scan.sh\" && bash -n \"$SCRIPTS_DIR/index.sh\""
54
54
  run_check "Graph Memory Rebuild (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_graph_memory.sh\""
55
55
  run_check "Dream Cycle Memory Graph Wiring (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_curate_graph_memories.sh\""
56
+ run_check "Reliability Guards (provider fallback, logs, autoscan, read scoring)" "bash \"$SCRIPTS_DIR/../tests/test_reliability_guards.sh\""
56
57
 
57
58
  echo ""
58
59
  if [ "$errors" -eq 0 ]; then
@@ -14,24 +14,12 @@ mkdir -p "$HOME" "$EAGLE_MEM_DIR" "$tmp_dir/bin"
14
14
 
15
15
  cat > "$tmp_dir/bin/curl" <<'EOF'
16
16
  #!/usr/bin/env bash
17
- body=""
18
- while [ "$#" -gt 0 ]; do
19
- case "$1" in
20
- -d)
21
- shift
22
- body="${1:-}"
23
- ;;
24
- esac
25
- shift || true
26
- done
27
-
28
- if printf '%s' "$body" | grep -q "mirrored agent memories"; then
29
- content="CONSOLIDATE: Memory A, Memory B -> Compiled AB | description: merged memories | value: --- Compiled Truth --- merged truth"
30
- else
31
- content="NONE"
17
+ if [ -n "${EAGLE_CURATE_FAKE_CURL_MARKER:-}" ]; then
18
+ echo called >> "$EAGLE_CURATE_FAKE_CURL_MARKER"
32
19
  fi
33
20
 
34
- jq -nc --arg content "$content" '{message:{content:$content}}'
21
+ content="${EAGLE_CURATE_FAKE_RESPONSE:-NONE}"
22
+ jq -nc --arg content "$content" '{message:{role:"assistant",content:$content}}'
35
23
  EOF
36
24
  chmod +x "$tmp_dir/bin/curl"
37
25
  export PATH="$tmp_dir/bin:$PATH"
@@ -49,45 +37,104 @@ url = "http://127.0.0.1:11434"
49
37
  model = "fake"
50
38
  EOF
51
39
 
52
- eagle_db "INSERT INTO agent_memories (project, file_path, memory_name, memory_type, description, content)
53
- VALUES
54
- ('project', 'memory://a', 'Memory A', 'project', 'First memory', 'Content A
55
- Evidence line that must not become a graph node'),
56
- ('project', 'memory://b', 'Memory B', 'project', 'Second memory', 'Content B');" >/dev/null
57
-
58
- "$EAGLE_BIN" curate -p project >/dev/null
59
-
60
- original_count=$(eagle_db "SELECT COUNT(*)
61
- FROM graph_nodes
62
- WHERE project = 'project'
63
- AND node_type = 'memory'
64
- AND node_name IN ('Memory A', 'Memory B');")
65
- if [ "$original_count" != "2" ]; then
66
- echo "expected original agent memories to be graph nodes, got $original_count" >&2
67
- exit 1
68
- fi
40
+ assert_eq() {
41
+ local expected="$1" actual="$2" message="$3"
42
+ if [ "$actual" != "$expected" ]; then
43
+ echo "$message: expected $expected, got $actual" >&2
44
+ exit 1
45
+ fi
46
+ }
47
+
48
+ insert_memory() {
49
+ local project="$1" file_path="$2" memory_name="$3" description="$4" content="$5"
50
+ eagle_db "INSERT INTO agent_memories (project, file_path, memory_name, memory_type, description, content)
51
+ VALUES
52
+ ('$(eagle_sql_escape "$project")',
53
+ '$(eagle_sql_escape "$file_path")',
54
+ '$(eagle_sql_escape "$memory_name")',
55
+ 'project',
56
+ '$(eagle_sql_escape "$description")',
57
+ '$(eagle_sql_escape "$content")');" >/dev/null
58
+ }
59
+
60
+ agent_memory_rows() {
61
+ local project="$1"
62
+ eagle_db "SELECT COUNT(*)
63
+ FROM agent_memories
64
+ WHERE project = '$(eagle_sql_escape "$project")';"
65
+ }
66
+
67
+ memory_graph_nodes() {
68
+ local project="$1"
69
+ eagle_db "SELECT COUNT(*)
70
+ FROM graph_nodes
71
+ WHERE project = '$(eagle_sql_escape "$project")'
72
+ AND node_type = 'memory';"
73
+ }
74
+
75
+ supersedes_edges() {
76
+ local project="$1" new_name="${2:-}"
77
+ local new_filter=""
78
+ if [ -n "$new_name" ]; then
79
+ new_filter="AND s.node_name = '$(eagle_sql_escape "$new_name")'"
80
+ fi
81
+ eagle_db "SELECT COUNT(*)
82
+ FROM graph_edges e
83
+ JOIN graph_nodes s ON s.id = e.source_node_id
84
+ JOIN graph_nodes t ON t.id = e.target_node_id
85
+ WHERE e.project = '$(eagle_sql_escape "$project")'
86
+ AND e.edge_type = 'supersedes'
87
+ AND s.node_type = 'memory'
88
+ AND t.node_type = 'memory'
89
+ $new_filter;"
90
+ }
69
91
 
70
- memory_node_count=$(eagle_db "SELECT COUNT(*)
71
- FROM graph_nodes
72
- WHERE project = 'project'
73
- AND node_type = 'memory';")
74
- if [ "$memory_node_count" != "3" ]; then
75
- echo "expected two originals plus one consolidated memory node, got $memory_node_count" >&2
92
+ # Happy path: structured JSON survives punctuation that broke the old text parser.
93
+ export EAGLE_CURATE_FAKE_RESPONSE='{"consolidations":[{"source_names":["Memory A - Dash","Memory B > Pipe | Name"],"new_name":"Compiled AB JSON","description":"merged memories","value":"--- Compiled Truth ---\nmerged truth\n\n--- Evidence Trail ---\n- Memory A - Dash\n- Memory B > Pipe | Name"}]}'
94
+ insert_memory "project-json" "memory://a" "Memory A - Dash" "First memory" $'Content A\nEvidence line that must not become a graph node'
95
+ insert_memory "project-json" "memory://b" "Memory B > Pipe | Name" "Second memory" "Content B"
96
+
97
+ "$EAGLE_BIN" curate -p project-json >/dev/null
98
+ assert_eq "2" "$(agent_memory_rows project-json)" "source agent memories should survive curate"
99
+ assert_eq "3" "$(memory_graph_nodes project-json)" "curate should create two originals plus one consolidated memory node"
100
+ assert_eq "2" "$(supersedes_edges project-json "Compiled AB JSON")" "consolidated memory should supersede both originals"
101
+
102
+ # Idempotency: re-running the same consolidation should not create duplicate nodes or edges.
103
+ "$EAGLE_BIN" curate -p project-json >/dev/null
104
+ assert_eq "3" "$(memory_graph_nodes project-json)" "curate should keep memory graph nodes idempotent"
105
+ assert_eq "2" "$(supersedes_edges project-json "Compiled AB JSON")" "curate should keep supersedes edge count idempotent"
106
+
107
+ # No consolidation: source nodes are still wired, but no supersedes edges are created.
108
+ export EAGLE_CURATE_FAKE_RESPONSE='{"consolidations":[]}'
109
+ insert_memory "project-none" "memory://none-a" "Memory None A" "First none memory" "Content A"
110
+ insert_memory "project-none" "memory://none-b" "Memory None B" "Second none memory" "Content B"
111
+ "$EAGLE_BIN" curate -p project-none >/dev/null
112
+ assert_eq "2" "$(memory_graph_nodes project-none)" "NONE response should still wire source memory nodes"
113
+ assert_eq "0" "$(supersedes_edges project-none)" "NONE response should not create supersedes edges"
114
+
115
+ # Malformed legacy/text output is ignored safely instead of producing partial graph edges.
116
+ export EAGLE_CURATE_FAKE_RESPONSE='CONSOLIDATE: Memory Broken A, Memory Broken B -> Broken AB | description: legacy | value: legacy text'
117
+ insert_memory "project-malformed" "memory://bad-a" "Memory Broken A" "First malformed memory" "Content A"
118
+ insert_memory "project-malformed" "memory://bad-b" "Memory Broken B" "Second malformed memory" "Content B"
119
+ "$EAGLE_BIN" curate -p project-malformed >/dev/null
120
+ assert_eq "2" "$(memory_graph_nodes project-malformed)" "malformed response should still wire source memory nodes"
121
+ assert_eq "0" "$(supersedes_edges project-malformed)" "malformed response should not create supersedes edges"
122
+
123
+ # Dry-run: memory graph writes and provider calls are skipped for consolidation.
124
+ dry_run_marker="$tmp_dir/curl-called"
125
+ dry_run_output="$tmp_dir/dry-run.out"
126
+ export EAGLE_CURATE_FAKE_CURL_MARKER="$dry_run_marker"
127
+ export EAGLE_CURATE_FAKE_RESPONSE='{"consolidations":[{"source_names":["Dry A","Dry B"],"new_name":"Dry AB","description":"dry","value":"dry"}]}'
128
+ insert_memory "project-dry" "memory://dry-a" "Dry A" "First dry memory" "Content A"
129
+ insert_memory "project-dry" "memory://dry-b" "Dry B" "Second dry memory" "Content B"
130
+ "$EAGLE_BIN" curate --dry-run -p project-dry > "$dry_run_output"
131
+ if [ -f "$dry_run_marker" ]; then
132
+ echo "dry-run should not call the consolidation provider" >&2
76
133
  exit 1
77
134
  fi
78
-
79
- supersedes_edges=$(eagle_db "SELECT COUNT(*)
80
- FROM graph_edges e
81
- JOIN graph_nodes s ON s.id = e.source_node_id
82
- JOIN graph_nodes t ON t.id = e.target_node_id
83
- WHERE e.project = 'project'
84
- AND e.edge_type = 'supersedes'
85
- AND s.node_type = 'memory'
86
- AND s.node_name = 'Compiled AB'
87
- AND t.node_type = 'memory'
88
- AND t.node_name IN ('Memory A', 'Memory B');")
89
- if [ "$supersedes_edges" != "2" ]; then
90
- echo "expected consolidated memory to supersede both originals, got $supersedes_edges" >&2
135
+ assert_eq "0" "$(memory_graph_nodes project-dry)" "dry-run should not write memory graph nodes"
136
+ if ! grep -q "Would wire 2 agent memory graph nodes" "$dry_run_output"; then
137
+ echo "dry-run did not report the memory graph wiring preview" >&2
91
138
  exit 1
92
139
  fi
93
140
 
@@ -77,11 +77,70 @@ CREATE TABLE graph_fixture (
77
77
  );
78
78
  EOF
79
79
 
80
- git -C "$repo" add a.sh b.sh old.sh db/source_column.sql
80
+ cat > "$repo/sqlite_cli_literal.sh" <<'EOF'
81
+ EAGLE_DB_SETUP=".headers off
82
+ .output /dev/null
83
+ PRAGMA busy_timeout=10000;
84
+ .output stdout"
85
+ EOF
86
+
87
+ {
88
+ printf '\n'
89
+ printf '%s\n' 'echo "starts after an intentional blank line"'
90
+ printf '%s\n' ' '
91
+ } > "$repo/sql_literal_edges.sh"
92
+
93
+ {
94
+ printf '%s\n' ' '
95
+ printf '\t\n'
96
+ } > "$repo/space_only.sh"
97
+
98
+ : > "$repo/empty.sh"
99
+
100
+ git -C "$repo" add a.sh b.sh old.sh db/source_column.sql sqlite_cli_literal.sh sql_literal_edges.sh space_only.sh empty.sh
81
101
 
82
102
  "$EAGLE_BIN" scan --force "$repo" >/dev/null
83
103
  "$EAGLE_BIN" index --force "$repo" >/dev/null
84
104
 
105
+ dot_command_chunk_count=$(eagle_db "SELECT COUNT(*)
106
+ FROM code_chunks
107
+ WHERE project = 'project'
108
+ AND file_path = 'sqlite_cli_literal.sh'
109
+ AND content LIKE '%.output stdout%';")
110
+ if [ "$dot_command_chunk_count" != "1" ]; then
111
+ echo "index did not preserve sqlite dot-command-like source lines" >&2
112
+ exit 1
113
+ fi
114
+
115
+ leading_blank_hex=$(eagle_db "SELECT hex(substr(content, 1, 1))
116
+ FROM code_chunks
117
+ WHERE project = 'project'
118
+ AND file_path = 'sql_literal_edges.sh'
119
+ LIMIT 1;")
120
+ if [ "$leading_blank_hex" != "0A" ]; then
121
+ echo "index did not preserve leading blank line in SQL literal expression" >&2
122
+ exit 1
123
+ fi
124
+
125
+ whitespace_chunk_count=$(eagle_db "SELECT COUNT(*)
126
+ FROM code_chunks
127
+ WHERE project = 'project'
128
+ AND file_path = 'space_only.sh'
129
+ AND length(content) > 0;")
130
+ if [ "$whitespace_chunk_count" != "1" ]; then
131
+ echo "index did not preserve all-whitespace source chunk" >&2
132
+ exit 1
133
+ fi
134
+
135
+ empty_chunk_count=$(eagle_db "SELECT COUNT(*)
136
+ FROM code_chunks
137
+ WHERE project = 'project'
138
+ AND file_path = 'empty.sh';")
139
+ if [ "$empty_chunk_count" != "0" ]; then
140
+ echo "index should not create chunks for empty files" >&2
141
+ exit 1
142
+ fi
143
+
85
144
  decl_count=$(eagle_db "SELECT COUNT(*)
86
145
  FROM graph_nodes
87
146
  WHERE project = 'project'
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ tmp_dir=$(mktemp -d)
6
+ trap 'rm -rf "$tmp_dir"' EXIT
7
+
8
+ assert_contains() {
9
+ local haystack="$1" needle="$2" message="$3"
10
+ case "$haystack" in
11
+ *"$needle"*) ;;
12
+ *)
13
+ echo "$message" >&2
14
+ echo "Expected to find: $needle" >&2
15
+ echo "Actual: $haystack" >&2
16
+ exit 1
17
+ ;;
18
+ esac
19
+ }
20
+
21
+ # Provider fallback: a failed preferred Codex CLI should fall through to Claude.
22
+ provider_home="$tmp_dir/provider-home"
23
+ fake_bin="$tmp_dir/bin"
24
+ mkdir -p "$provider_home" "$fake_bin"
25
+ cat > "$fake_bin/codex" <<'SH'
26
+ #!/usr/bin/env bash
27
+ exit 42
28
+ SH
29
+ cat > "$fake_bin/claude" <<'SH'
30
+ #!/usr/bin/env bash
31
+ printf 'claude fallback ok\n'
32
+ SH
33
+ chmod +x "$fake_bin/codex" "$fake_bin/claude"
34
+ cat > "$provider_home/config.toml" <<'TOML'
35
+ [provider]
36
+ type = "agent_cli"
37
+ fallback = "auto"
38
+
39
+ [agent_cli]
40
+ preferred = "codex"
41
+ codex_model = ""
42
+ claude_model = ""
43
+ TOML
44
+ provider_result=$(EAGLE_MEM_DIR="$provider_home" PATH="$fake_bin:$PATH" bash -c "
45
+ . '$ROOT_DIR/lib/common.sh'
46
+ . '$ROOT_DIR/lib/provider.sh'
47
+ eagle_llm_call 'say ok' 'system' 20
48
+ ")
49
+ assert_contains "$provider_result" "claude fallback ok" "agent_cli fallback did not use Claude after Codex failed"
50
+
51
+ # PreToolUse parsing + read scoring: repeated large read after modification should emit scored context.
52
+ hook_home="$tmp_dir/hook-home"
53
+ repo="$tmp_dir/repo"
54
+ mkdir -p "$hook_home/mod-tracker" "$repo"
55
+ touch "$hook_home/memory.db"
56
+ large_file="$repo/large.txt"
57
+ dd if=/dev/zero bs=1024 count=600 2>/dev/null | tr '\0' 'x' > "$large_file"
58
+ session_id="session_reliability_123"
59
+ printf '%s\n' "$large_file" > "$hook_home/mod-tracker/$session_id"
60
+ read_input=$(jq -nc --arg fp "$large_file" --arg sid "$session_id" --arg cwd "$repo" \
61
+ '{tool_name:"Read",session_id:$sid,cwd:$cwd,tool_input:{file_path:$fp}}')
62
+ EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input" >/dev/null
63
+ EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input" >/dev/null
64
+ read_output=$(EAGLE_MEM_DIR="$hook_home" EAGLE_MEM_PROJECT="project-read" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$read_input")
65
+ assert_contains "$read_output" "Eagle Mem read score" "Read guard did not emit scored duplicate-read context"
66
+ grep -q 'mod_file}.lock' "$ROOT_DIR/hooks/post-tool-use.sh" || {
67
+ echo "post-tool-use modification tracker should use a lock directory" >&2
68
+ exit 1
69
+ }
70
+
71
+ # Auto-scan state race: failed background scan must clear the freshness marker.
72
+ state_home="$tmp_dir/state-home"
73
+ auto_scripts="$tmp_dir/auto-scripts"
74
+ auto_repo="$tmp_dir/auto-repo"
75
+ mkdir -p "$state_home" "$auto_scripts" "$auto_repo"
76
+ cat > "$auto_scripts/scan.sh" <<'SH'
77
+ #!/usr/bin/env bash
78
+ exit 9
79
+ SH
80
+ cat > "$auto_scripts/index.sh" <<'SH'
81
+ #!/usr/bin/env bash
82
+ exit 0
83
+ SH
84
+ EAGLE_MEM_DIR="$state_home" EAGLE_MEM_LOG="$state_home/eagle-mem.log" bash -c "
85
+ . '$ROOT_DIR/lib/common.sh'
86
+ . '$ROOT_DIR/lib/hooks-sessionstart.sh'
87
+ eagle_get_overview() { return 0; }
88
+ eagle_db() { printf '0\n'; }
89
+ eagle_sessionstart_auto_provision 'project-auto-fail' '$auto_repo' '$auto_scripts'
90
+ scan_state=\$(_eagle_state_file scan 'project-auto-fail')
91
+ for _i in 1 2 3 4 5 6 7 8 9 10; do
92
+ grep -q 'auto-scan failed' '$state_home/eagle-mem.log' 2>/dev/null && break
93
+ sleep 0.2
94
+ done
95
+ [ ! -f \"\$scan_state\" ]
96
+ "
97
+
98
+ # Command-scoped logs: scan should create an inspectable run log and logs list should show it.
99
+ log_home="$tmp_dir/log-home"
100
+ log_repo="$tmp_dir/log-repo"
101
+ mkdir -p "$log_home" "$log_repo"
102
+ printf '# demo\n' > "$log_repo/README.md"
103
+ EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/scripts/scan.sh" "$log_repo" >/dev/null
104
+ log_list=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
105
+ assert_contains "$log_list" "command=scan" "logs list did not show the scan command run"
106
+
107
+ echo "reliability guard regressions passed"