eagle-mem 4.10.8 → 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 CHANGED
@@ -4,6 +4,26 @@ 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
+
18
+ ## v4.10.9 Dream Cycle Graph Memory Hotfix
19
+
20
+ This hotfix closes the remaining graph-memory curation gap:
21
+
22
+ - **Memory Node Wiring**: Dream Cycle curation now creates graph `memory` nodes for active mirrored agent memories before consolidation runs, so consolidated memories can supersede real source memory nodes instead of depending on fuzzy or accidental matches.
23
+ - **Regression Coverage**: The smoke suite now runs an isolated Dream Cycle graph-memory regression that proves multiline memory content does not become bogus graph nodes and that consolidated memories supersede both originals.
24
+
25
+ ---
26
+
7
27
  ## v4.10.8 Graph Neighbors Hotfix
8
28
 
9
29
  This hotfix tightens the final graph-memory verification path:
package/README.md CHANGED
@@ -184,6 +184,8 @@ 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. Consolidation responses use a strict JSON contract, and dry-run previews skip the provider call for the memory-consolidation step.
188
+
187
189
  ### Trust and Recovery
188
190
 
189
191
  Eagle Mem now treats installation as a visible product surface, not a black box:
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.8",
3
+ "version": "4.10.10",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
7
7
  },
8
+ "scripts": {
9
+ "test": "bash scripts/test.sh"
10
+ },
8
11
  "files": [
9
12
  "bin/",
10
13
  "scripts/",
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
- eagle_ok "Wired $co_wire_count co-edited file edges"
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,19 +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
- eagle_ok "Wired $session_wire_count recent session nodes and edges"
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
- active_memories=$(eagle_db "SELECT memory_name, memory_type, description, content FROM agent_memories WHERE project = '$p_esc';")
577
- if [ -n "$active_memories" ]; then
578
- consolidation_prompt="Analyze these mirrored agent memories for project '$project'. Identify any memories that are redundant, overlap in scope, or describe the same subsystem/gotcha/concept.
579
-
580
- MEMORIES:
581
- $active_memories
582
-
583
- For any memories that should be consolidated, merge them into a single 'Compiled Truth' summary.
584
- The consolidated memory MUST be formatted exactly as:
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
631
+ memory_node_index=0
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")
635
+ memory_node_index=$((memory_node_index + 1))
636
+ [ -n "$mname" ] || continue
637
+ if [ "$DRY_RUN" -eq 0 ]; then
638
+ eagle_graph_add_node "$project" "memory" "$mname" "$mcontent" ""
639
+ fi
640
+ wired_memory_count=$((wired_memory_count + 1))
641
+ done
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
647
+
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
+ }]')
654
+
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:
585
665
  --- Compiled Truth ---
586
666
  <A structured, clear, up-to-date summary of the topic, gotten by merging the duplicate/overlapping memories. Keep it extremely precise.>
587
667
 
@@ -589,55 +669,59 @@ The consolidated memory MUST be formatted exactly as:
589
669
  - <Original memory title 1>: <brief original description or timestamp>
590
670
  - <Original memory title 2>: <brief original description or timestamp>
591
671
 
592
- Format your output as a series of instructions:
593
- CONSOLIDATE: <original memory name 1>, <original memory name 2> -> <new consolidated memory name> | description: <new description> | value: <the merged compiled truth + evidence content>
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
+ }
594
683
 
595
- If no memories need consolidation, output: NONE"
684
+ If no memories need consolidation, return {\"consolidations\":[]}."
596
685
 
597
- consolidation_result=$(eagle_llm_call "$consolidation_prompt" "You consolidate software development memories into a single compiled truth. Be precise. Output CONSOLIDATE lines or NONE." 1024 || true)
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
598
691
 
599
- if [ -n "$consolidation_result" ] && ! echo "$consolidation_result" | grep -q "^NONE$"; then
600
- cons_count=0
601
- while IFS= read -r line; do
602
- case "$line" in
603
- CONSOLIDATE:*)
604
- cons_data=$(echo "$line" | sed 's/^CONSOLIDATE:[[:space:]]*//')
605
-
606
- # Parse matching: original_names -> new_name | description: desc | value: val
607
- names_part=$(echo "$cons_data" | cut -d'-' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
608
- rest_part=$(echo "$cons_data" | cut -d'>' -f2-)
609
- new_name=$(echo "$rest_part" | cut -d'|' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
610
-
611
- desc_part=$(echo "$rest_part" | grep -oE "description:[[:space:]]*[^|]+" | sed 's/description:[[:space:]]*//')
612
- val_part=$(echo "$rest_part" | grep -oE "value:[[:space:]]*.+" | sed 's/value:[[:space:]]*//')
613
-
614
- if [ -z "$new_name" ] || [ -z "$names_part" ]; then continue; fi
615
-
616
- if [ "$DRY_RUN" -eq 1 ]; then
617
- eagle_info " Would consolidate: $names_part → $new_name"
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
618
715
  else
619
- # 1. Add new consolidated memory node
620
- eagle_graph_add_node "$project" "memory" "$new_name" "$val_part" ""
621
- new_node_id=$(eagle_graph_get_node_id "$project" "memory" "$new_name")
622
-
623
- # 2. Wire supersedes edges from new node to old nodes, and mark old nodes as inactive/superseded
624
- IFS=',' read -ra name_arr <<< "$names_part"
625
- for old_n in "${name_arr[@]}"; do
626
- old_n=$(echo "$old_n" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
627
- if [ -z "$old_n" ]; then continue; fi
628
- old_node_id=$(eagle_graph_get_node_id "$project" "memory" "$old_n")
629
- if [ -n "$old_node_id" ] && [ -n "$new_node_id" ]; then
630
- eagle_graph_add_edge "$project" "$new_node_id" "$old_node_id" "supersedes" 1.0
631
- fi
632
- done
633
- cons_count=$((cons_count + 1))
716
+ eagle_log "WARN" "Unable to wire supersedes edge: source='$old_n' consolidated='$new_name'"
634
717
  fi
635
- ;;
636
- esac
637
- done <<< "$consolidation_result"
638
- eagle_ok "Consolidated $cons_count sets of overlapping agent memories"
639
- else
640
- eagle_ok "Agent memories are fully consolidated and up to date"
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
641
725
  fi
642
726
  else
643
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
- content_sql=$(eagle_sql_escape "$content")
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, '$content_sql', $current_mtime);"
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))
package/scripts/test.sh CHANGED
@@ -52,6 +52,7 @@ run_check "Cross Agent Memory (memories query)" "\"$EAGLE_BIN\" memories --json
52
52
  run_check "Installer And Updater (install / update syntax)" "bash -n \"$SCRIPTS_DIR/install.sh\" && bash -n \"$SCRIPTS_DIR/update.sh\""
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
+ run_check "Dream Cycle Memory Graph Wiring (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_curate_graph_memories.sh\""
55
56
 
56
57
  echo ""
57
58
  if [ "$errors" -eq 0 ]; then
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # Ensures curate wires source agent memories before adding consolidated graph edges.
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+ EAGLE_BIN="$ROOT_DIR/bin/eagle-mem"
7
+
8
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-curate-graph.XXXXXX")
9
+ trap 'rm -rf "$tmp_dir"' EXIT
10
+
11
+ export HOME="$tmp_dir/home"
12
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
13
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR" "$tmp_dir/bin"
14
+
15
+ cat > "$tmp_dir/bin/curl" <<'EOF'
16
+ #!/usr/bin/env bash
17
+ if [ -n "${EAGLE_CURATE_FAKE_CURL_MARKER:-}" ]; then
18
+ echo called >> "$EAGLE_CURATE_FAKE_CURL_MARKER"
19
+ fi
20
+
21
+ content="${EAGLE_CURATE_FAKE_RESPONSE:-NONE}"
22
+ jq -nc --arg content "$content" '{message:{role:"assistant",content:$content}}'
23
+ EOF
24
+ chmod +x "$tmp_dir/bin/curl"
25
+ export PATH="$tmp_dir/bin:$PATH"
26
+
27
+ . "$ROOT_DIR/lib/common.sh"
28
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
29
+ . "$ROOT_DIR/lib/db.sh"
30
+
31
+ cat > "$EAGLE_MEM_DIR/config.toml" <<'EOF'
32
+ [provider]
33
+ type = "ollama"
34
+
35
+ [ollama]
36
+ url = "http://127.0.0.1:11434"
37
+ model = "fake"
38
+ EOF
39
+
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
+ }
91
+
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
133
+ exit 1
134
+ fi
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
138
+ exit 1
139
+ fi
140
+
141
+ echo "curate graph memory regressions passed"
@@ -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'