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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.9",
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"
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,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
- 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
- memory_node_count=0
579
- active_memory_nodes=$(eagle_db_json "SELECT memory_name, content FROM agent_memories WHERE project = '$p_esc';" || true)
580
- active_memory_node_count=$(printf '%s' "$active_memory_nodes" | jq 'length' 2>/dev/null || echo 0)
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 "$active_memory_node_count" ]; do
583
- mname=$(printf '%s' "$active_memory_nodes" | jq -r ".[$memory_node_index].memory_name // empty")
584
- mcontent=$(printf '%s' "$active_memory_nodes" | jq -r ".[$memory_node_index].content // empty")
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
- memory_node_count=$((memory_node_count + 1))
640
+ wired_memory_count=$((wired_memory_count + 1))
591
641
  done
592
- eagle_ok "Wired $memory_node_count agent memory graph nodes"
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
- 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.
595
-
596
- MEMORIES:
597
- $active_memories
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
- For any memories that should be consolidated, merge them into a single 'Compiled Truth' summary.
600
- The consolidated memory MUST be formatted exactly as:
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
- Format your output as a series of instructions:
609
- 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
+ }
610
683
 
611
- If no memories need consolidation, output: NONE"
684
+ If no memories need consolidation, return {\"consolidations\":[]}."
612
685
 
613
- 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
614
691
 
615
- if [ -n "$consolidation_result" ] && ! echo "$consolidation_result" | grep -q "^NONE$"; then
616
- cons_count=0
617
- while IFS= read -r line; do
618
- case "$line" in
619
- CONSOLIDATE:*)
620
- cons_data=$(echo "$line" | sed 's/^CONSOLIDATE:[[:space:]]*//')
621
-
622
- # Parse matching: original_names -> new_name | description: desc | value: val
623
- names_part=$(echo "$cons_data" | cut -d'-' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
624
- rest_part=$(echo "$cons_data" | cut -d'>' -f2-)
625
- new_name=$(echo "$rest_part" | cut -d'|' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
626
-
627
- desc_part=$(echo "$rest_part" | grep -oE "description:[[:space:]]*[^|]+" | sed 's/description:[[:space:]]*//')
628
- val_part=$(echo "$rest_part" | grep -oE "value:[[:space:]]*.+" | sed 's/value:[[:space:]]*//')
629
-
630
- if [ -z "$new_name" ] || [ -z "$names_part" ]; then continue; fi
631
-
632
- if [ "$DRY_RUN" -eq 1 ]; then
633
- 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
634
715
  else
635
- # 1. Add new consolidated memory node
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
- esac
653
- done <<< "$consolidation_result"
654
- eagle_ok "Consolidated $cons_count sets of overlapping agent memories"
655
- else
656
- 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
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
- 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))
@@ -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'