eagle-mem 4.10.9 → 4.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +6 -1
- package/bin/eagle-mem +1 -0
- package/hooks/post-tool-use.sh +9 -6
- package/hooks/pre-tool-use.sh +68 -1
- package/hooks/stop.sh +1 -1
- package/hooks/user-prompt-submit.sh +1 -1
- package/lib/common.sh +85 -0
- package/lib/hooks-sessionstart.sh +60 -7
- package/lib/provider.sh +187 -40
- package/package.json +1 -1
- package/scripts/config.sh +2 -0
- package/scripts/curate.sh +139 -63
- package/scripts/health.sh +5 -7
- package/scripts/help.sh +1 -0
- package/scripts/index.sh +33 -3
- package/scripts/logs.sh +84 -0
- package/scripts/scan.sh +11 -1
- package/scripts/test.sh +1 -0
- package/tests/test_curate_graph_memories.sh +99 -52
- package/tests/test_graph_memory.sh +60 -1
- package/tests/test_reliability_guards.sh +107 -0
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
|
-
|
|
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,
|
|
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))
|
package/scripts/logs.sh
ADDED
|
@@ -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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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"
|