eagle-mem 4.10.9 → 4.10.10
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 +11 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/curate.sh +130 -62
- package/scripts/index.sh +21 -2
- package/tests/test_curate_graph_memories.sh +99 -52
- package/tests/test_graph_memory.sh +60 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to the **Eagle Mem** project are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v4.10.10 Dream Cycle Consolidation Hardening
|
|
8
|
+
|
|
9
|
+
This patch closes the review findings from the multi-model Spectral pass:
|
|
10
|
+
|
|
11
|
+
- **Structured Consolidation Parsing**: Dream Cycle memory consolidation now asks providers for strict JSON and validates the response with `jq`, removing the brittle `CONSOLIDATE:` text parser that could break on punctuation, arrows, pipes, or whitespace in memory names.
|
|
12
|
+
- **Dry-Run Safety**: Memory graph consolidation dry-runs now preview graph wiring and skip the provider call, avoiding token spend and provider side effects during preview.
|
|
13
|
+
- **Regression Coverage**: The Dream Cycle regression now covers JSON consolidation with punctuation-heavy names, `NONE` responses, malformed legacy text responses, idempotent reruns, and dry-run provider skipping.
|
|
14
|
+
- **Indexer Edge Coverage**: Graph-memory indexing now verifies dot-command-like source lines, leading blank lines, all-whitespace chunks, and empty-file behavior.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
7
18
|
## v4.10.9 Dream Cycle Graph Memory Hotfix
|
|
8
19
|
|
|
9
20
|
This hotfix closes the remaining graph-memory curation gap:
|
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ eagle-mem overview set "Current project briefing..."
|
|
|
184
184
|
|
|
185
185
|
If graph search shows stale deleted files, run `eagle-mem graph rebuild` from the project root. The rebuild command filters missing tracked paths, clears stale code chunks and declaration nodes, preserves manual overviews, and rewires declarations with file-scoped names such as `apps/mac/DictationController.swift::finishDictation`.
|
|
186
186
|
|
|
187
|
-
Dream Cycle curation also wires mirrored agent memories into graph `memory` nodes before consolidation, so `supersedes` relationships stay attached to the source memories rather than to incidental text inside memory content.
|
|
187
|
+
Dream Cycle curation also wires mirrored agent memories into graph `memory` nodes before consolidation, so `supersedes` relationships stay attached to the source memories rather than to incidental text inside memory content. Consolidation responses use a strict JSON contract, and dry-run previews skip the provider call for the memory-consolidation step.
|
|
188
188
|
|
|
189
189
|
### Trust and Recovery
|
|
190
190
|
|
package/package.json
CHANGED
package/scripts/curate.sh
CHANGED
|
@@ -37,6 +37,46 @@ Options:
|
|
|
37
37
|
EOF
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
parse_consolidations_json() {
|
|
41
|
+
local result="$1"
|
|
42
|
+
local trimmed json_payload
|
|
43
|
+
|
|
44
|
+
trimmed=$(printf '%s' "$result" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
45
|
+
case "$trimmed" in
|
|
46
|
+
""|NONE|none|null)
|
|
47
|
+
printf '[]\n'
|
|
48
|
+
return 0
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
json_payload=$(printf '%s' "$result" \
|
|
53
|
+
| sed -e '1s/^[[:space:]]*```json[[:space:]]*$//' \
|
|
54
|
+
-e '1s/^[[:space:]]*```[[:space:]]*$//' \
|
|
55
|
+
-e '$s/^[[:space:]]*```[[:space:]]*$//')
|
|
56
|
+
|
|
57
|
+
printf '%s' "$json_payload" | jq -c '
|
|
58
|
+
def trim: gsub("^\\s+|\\s+$"; "");
|
|
59
|
+
def names:
|
|
60
|
+
if type == "array" then map(tostring | trim) | map(select(length > 0))
|
|
61
|
+
elif type == "string" then split(",") | map(trim) | map(select(length > 0))
|
|
62
|
+
else []
|
|
63
|
+
end;
|
|
64
|
+
def root:
|
|
65
|
+
if type == "array" then .
|
|
66
|
+
elif type == "object" then (.consolidations // .items // .instructions // [])
|
|
67
|
+
else []
|
|
68
|
+
end;
|
|
69
|
+
root
|
|
70
|
+
| map({
|
|
71
|
+
source_names: ((.source_names // .sourceNames // .source_memories // .sourceMemories // .original_names // .originalNames // .originals // .names) | names),
|
|
72
|
+
new_name: ((.new_name // .newName // .name // .title // "") | tostring | trim),
|
|
73
|
+
description: ((.description // "") | tostring),
|
|
74
|
+
value: ((.value // .content // .compiled_truth // .compiledTruth // "") | tostring)
|
|
75
|
+
})
|
|
76
|
+
| map(select((.source_names | length) > 0 and (.new_name | length) > 0))
|
|
77
|
+
' 2>/dev/null
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
while [ $# -gt 0 ]; do
|
|
41
81
|
case "$1" in
|
|
42
82
|
-h|--help)
|
|
@@ -559,7 +599,11 @@ if [ -n "$co_edit_data" ]; then
|
|
|
559
599
|
co_wire_count=$((co_wire_count + 1))
|
|
560
600
|
fi
|
|
561
601
|
done <<< "$co_edit_data"
|
|
562
|
-
|
|
602
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
603
|
+
eagle_info "Would wire $co_wire_count co-edited file edges"
|
|
604
|
+
else
|
|
605
|
+
eagle_ok "Wired $co_wire_count co-edited file edges"
|
|
606
|
+
fi
|
|
563
607
|
fi
|
|
564
608
|
|
|
565
609
|
# 7.2 Wire session nodes and access edges
|
|
@@ -569,35 +613,55 @@ if [ -n "$recent_sessions" ]; then
|
|
|
569
613
|
if [ "$DRY_RUN" -eq 0 ]; then
|
|
570
614
|
session_wire_count=$(eagle_graph_wire_recent_session_edges "$project" 15)
|
|
571
615
|
fi
|
|
572
|
-
|
|
616
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
617
|
+
eagle_info "Would wire $session_wire_count recent session nodes and edges"
|
|
618
|
+
else
|
|
619
|
+
eagle_ok "Wired $session_wire_count recent session nodes and edges"
|
|
620
|
+
fi
|
|
573
621
|
fi
|
|
574
622
|
|
|
575
623
|
# 7.3 Offline Memory Consolidation (Compiled Truth vs Evidence)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
624
|
+
active_memory_rows=$(eagle_db_json "SELECT memory_name, memory_type, description, content FROM agent_memories WHERE project = '$p_esc';" || {
|
|
625
|
+
eagle_log "WARN" "Unable to read active agent memories for consolidation"
|
|
626
|
+
echo "[]"
|
|
627
|
+
})
|
|
628
|
+
active_memory_count=$(printf '%s' "$active_memory_rows" | jq 'length' 2>/dev/null || echo 0)
|
|
629
|
+
if [ "$active_memory_count" -gt 0 ]; then
|
|
630
|
+
wired_memory_count=0
|
|
581
631
|
memory_node_index=0
|
|
582
|
-
while [ "$memory_node_index" -lt "$
|
|
583
|
-
mname=$(printf '%s' "$
|
|
584
|
-
mcontent=$(printf '%s' "$
|
|
632
|
+
while [ "$memory_node_index" -lt "$active_memory_count" ]; do
|
|
633
|
+
mname=$(printf '%s' "$active_memory_rows" | jq -r ".[$memory_node_index].memory_name // empty")
|
|
634
|
+
mcontent=$(printf '%s' "$active_memory_rows" | jq -r ".[$memory_node_index].content // empty")
|
|
585
635
|
memory_node_index=$((memory_node_index + 1))
|
|
586
636
|
[ -n "$mname" ] || continue
|
|
587
637
|
if [ "$DRY_RUN" -eq 0 ]; then
|
|
588
638
|
eagle_graph_add_node "$project" "memory" "$mname" "$mcontent" ""
|
|
589
639
|
fi
|
|
590
|
-
|
|
640
|
+
wired_memory_count=$((wired_memory_count + 1))
|
|
591
641
|
done
|
|
592
|
-
|
|
642
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
643
|
+
eagle_info "Would wire $wired_memory_count agent memory graph nodes"
|
|
644
|
+
else
|
|
645
|
+
eagle_ok "Wired $wired_memory_count agent memory graph nodes"
|
|
646
|
+
fi
|
|
593
647
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
648
|
+
memory_prompt_json=$(printf '%s' "$active_memory_rows" | jq -c '[.[] | {
|
|
649
|
+
memory_name: .memory_name,
|
|
650
|
+
memory_type: .memory_type,
|
|
651
|
+
description: .description,
|
|
652
|
+
content: .content
|
|
653
|
+
}]')
|
|
598
654
|
|
|
599
|
-
|
|
600
|
-
|
|
655
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
656
|
+
eagle_info "Would analyze $wired_memory_count agent memories for consolidation"
|
|
657
|
+
else
|
|
658
|
+
consolidation_prompt="Analyze these mirrored agent memories for project '$project'. Identify memories that are redundant, overlap in scope, or describe the same subsystem/gotcha/concept.
|
|
659
|
+
|
|
660
|
+
INPUT_MEMORIES_JSON:
|
|
661
|
+
$memory_prompt_json
|
|
662
|
+
|
|
663
|
+
For any memories that should be consolidated, merge them into a single Compiled Truth summary.
|
|
664
|
+
The consolidated memory value MUST be formatted exactly as:
|
|
601
665
|
--- Compiled Truth ---
|
|
602
666
|
<A structured, clear, up-to-date summary of the topic, gotten by merging the duplicate/overlapping memories. Keep it extremely precise.>
|
|
603
667
|
|
|
@@ -605,55 +669,59 @@ The consolidated memory MUST be formatted exactly as:
|
|
|
605
669
|
- <Original memory title 1>: <brief original description or timestamp>
|
|
606
670
|
- <Original memory title 2>: <brief original description or timestamp>
|
|
607
671
|
|
|
608
|
-
|
|
609
|
-
|
|
672
|
+
Return strict JSON only, with this schema:
|
|
673
|
+
{
|
|
674
|
+
\"consolidations\": [
|
|
675
|
+
{
|
|
676
|
+
\"source_names\": [\"<exact source memory_name>\", \"<exact source memory_name>\"],
|
|
677
|
+
\"new_name\": \"<new consolidated memory name>\",
|
|
678
|
+
\"description\": \"<new description>\",
|
|
679
|
+
\"value\": \"<the merged compiled truth + evidence content>\"
|
|
680
|
+
}
|
|
681
|
+
]
|
|
682
|
+
}
|
|
610
683
|
|
|
611
|
-
If no memories need consolidation,
|
|
684
|
+
If no memories need consolidation, return {\"consolidations\":[]}."
|
|
612
685
|
|
|
613
|
-
|
|
686
|
+
consolidation_result=$(eagle_llm_call "$consolidation_prompt" "You consolidate software development memories into a single compiled truth. Return strict JSON only." 1024 || true)
|
|
687
|
+
if ! parsed_consolidations=$(parse_consolidations_json "$consolidation_result"); then
|
|
688
|
+
eagle_log "WARN" "Unable to parse memory consolidation provider response"
|
|
689
|
+
parsed_consolidations="[]"
|
|
690
|
+
fi
|
|
614
691
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
692
|
+
consolidation_count=$(printf '%s' "$parsed_consolidations" | jq 'length' 2>/dev/null || echo 0)
|
|
693
|
+
if [ "$consolidation_count" -gt 0 ]; then
|
|
694
|
+
cons_count=0
|
|
695
|
+
consolidation_index=0
|
|
696
|
+
while [ "$consolidation_index" -lt "$consolidation_count" ]; do
|
|
697
|
+
consolidation_item=$(printf '%s' "$parsed_consolidations" | jq -c ".[$consolidation_index]")
|
|
698
|
+
consolidation_index=$((consolidation_index + 1))
|
|
699
|
+
new_name=$(printf '%s' "$consolidation_item" | jq -r '.new_name // empty')
|
|
700
|
+
val_part=$(printf '%s' "$consolidation_item" | jq -r '.value // empty')
|
|
701
|
+
[ -n "$new_name" ] || continue
|
|
702
|
+
|
|
703
|
+
eagle_graph_add_node "$project" "memory" "$new_name" "$val_part" ""
|
|
704
|
+
new_node_id=$(eagle_graph_get_node_id "$project" "memory" "$new_name")
|
|
705
|
+
|
|
706
|
+
old_count=$(printf '%s' "$consolidation_item" | jq '.source_names | length' 2>/dev/null || echo 0)
|
|
707
|
+
old_index=0
|
|
708
|
+
while [ "$old_index" -lt "$old_count" ]; do
|
|
709
|
+
old_n=$(printf '%s' "$consolidation_item" | jq -r ".source_names[$old_index] // empty")
|
|
710
|
+
old_index=$((old_index + 1))
|
|
711
|
+
[ -n "$old_n" ] || continue
|
|
712
|
+
old_node_id=$(eagle_graph_get_node_id "$project" "memory" "$old_n")
|
|
713
|
+
if [ -n "$old_node_id" ] && [ -n "$new_node_id" ]; then
|
|
714
|
+
eagle_graph_add_edge "$project" "$new_node_id" "$old_node_id" "supersedes" 1.0
|
|
634
715
|
else
|
|
635
|
-
|
|
636
|
-
eagle_graph_add_node "$project" "memory" "$new_name" "$val_part" ""
|
|
637
|
-
new_node_id=$(eagle_graph_get_node_id "$project" "memory" "$new_name")
|
|
638
|
-
|
|
639
|
-
# 2. Wire supersedes edges from new node to old nodes, and mark old nodes as inactive/superseded
|
|
640
|
-
IFS=',' read -ra name_arr <<< "$names_part"
|
|
641
|
-
for old_n in "${name_arr[@]}"; do
|
|
642
|
-
old_n=$(echo "$old_n" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
643
|
-
if [ -z "$old_n" ]; then continue; fi
|
|
644
|
-
old_node_id=$(eagle_graph_get_node_id "$project" "memory" "$old_n")
|
|
645
|
-
if [ -n "$old_node_id" ] && [ -n "$new_node_id" ]; then
|
|
646
|
-
eagle_graph_add_edge "$project" "$new_node_id" "$old_node_id" "supersedes" 1.0
|
|
647
|
-
fi
|
|
648
|
-
done
|
|
649
|
-
cons_count=$((cons_count + 1))
|
|
716
|
+
eagle_log "WARN" "Unable to wire supersedes edge: source='$old_n' consolidated='$new_name'"
|
|
650
717
|
fi
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
718
|
+
done
|
|
719
|
+
cons_count=$((cons_count + 1))
|
|
720
|
+
done
|
|
721
|
+
eagle_ok "Consolidated $cons_count sets of overlapping agent memories"
|
|
722
|
+
else
|
|
723
|
+
eagle_ok "Agent memories are fully consolidated and up to date"
|
|
724
|
+
fi
|
|
657
725
|
fi
|
|
658
726
|
else
|
|
659
727
|
eagle_dim " No active agent memories to consolidate"
|
package/scripts/index.sh
CHANGED
|
@@ -89,6 +89,25 @@ ext_to_lang() {
|
|
|
89
89
|
esac
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
sql_literal_expr() {
|
|
93
|
+
awk '
|
|
94
|
+
BEGIN { first = 1 }
|
|
95
|
+
{
|
|
96
|
+
gsub(/\047/, "\047\047")
|
|
97
|
+
if (!first) {
|
|
98
|
+
printf "||char(10)||"
|
|
99
|
+
}
|
|
100
|
+
printf "\047%s\047", $0
|
|
101
|
+
first = 0
|
|
102
|
+
}
|
|
103
|
+
END {
|
|
104
|
+
if (first) {
|
|
105
|
+
printf "\047\047"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
'
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
# ─── Collect files ─────────────────────────────────────────
|
|
93
112
|
|
|
94
113
|
TMPDIR_IDX=$(mktemp -d)
|
|
@@ -194,11 +213,11 @@ DELETE FROM code_chunks WHERE project = '$project_sql' AND file_path = '$file_sq
|
|
|
194
213
|
[ "$end" -gt "$total_lines" ] && end="$total_lines"
|
|
195
214
|
|
|
196
215
|
content=$(sed -n "${start},${end}p" "$full_path" | eagle_redact)
|
|
197
|
-
|
|
216
|
+
content_expr=$(printf '%s\n' "$content" | sql_literal_expr)
|
|
198
217
|
|
|
199
218
|
txn_sql+="
|
|
200
219
|
INSERT INTO code_chunks (project, file_path, language, start_line, end_line, content, mtime)
|
|
201
|
-
VALUES ('$project_sql', '$file_sql', '$lang_sql', $start, $end,
|
|
220
|
+
VALUES ('$project_sql', '$file_sql', '$lang_sql', $start, $end, $content_expr, $current_mtime);"
|
|
202
221
|
|
|
203
222
|
chunk_count=$((chunk_count + 1))
|
|
204
223
|
start=$((end + 1))
|
|
@@ -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'
|