eagle-mem 4.10.5 → 4.10.6

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,20 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.10.6 Graph Memory Rebuild Release
8
+
9
+ This patch turns the local graph-memory workarounds into supported product behavior:
10
+
11
+ - **Official Graph Rebuild Path**: Added `eagle-mem graph rebuild` and `eagle-mem index --force` so stale code chunks, declaration nodes, file nodes, and import edges can be rebuilt without manual SQLite deletes.
12
+ - **Graph Node Type Migration**: Added migration `db/038_graph_node_types.sql` to recreate graph node validation triggers with all declaration node types emitted by the indexer: `class`, `struct`, `function`, `func`, `fn`, and `def`.
13
+ - **File-Scoped Declarations**: Declaration nodes now use file-scoped names like `path/to/file.sh::finishDictation`, avoiding collisions when multiple files define the same function/class name.
14
+ - **Dream Cycle Batching**: Replaced per-edge sqlite subprocess calls in session-to-file graph wiring with one batched transaction, and normalized absolute observation paths back to project-relative graph file nodes.
15
+ - **Stale File Filtering**: `eagle_collect_files` now filters deleted-but-tracked paths from `git ls-files`, so scans and rebuilds represent the current filesystem.
16
+ - **Overview Graph Sync**: `eagle-mem overview set` now syncs the graph project node value, keeping graph search aligned with the canonical overview.
17
+ - **Four-Agent Update Surface**: `eagle-mem update` now refreshes Antigravity integrations and Grok skill links in addition to Claude Code and Codex hooks/skills.
18
+
19
+ ---
20
+
7
21
  ## v4.10.5 Hardening Release
8
22
 
9
23
  This patch release hardens the database architecture, improves CLI usability, and increases programmatic test coverage for all core features:
package/README.md CHANGED
@@ -161,7 +161,28 @@ Eagle Mem prevents Claude from repeating past mistakes:
161
161
  | `eagle-mem test` | Run basic smoke tests for the memory layer |
162
162
  | `eagle-mem prune` | Clean old sessions and stale data |
163
163
  | `eagle-mem scan` | Scan codebase and generate overview |
164
- | `eagle-mem index` | Index source files for FTS5 code search |
164
+ | `eagle-mem index` | Index source files for FTS5 code search and static graph declarations |
165
+ | `eagle-mem index --force` | Rebuild current source chunks, declarations, and import edges |
166
+ | `eagle-mem graph rebuild` | Clear and rebuild the current project's code graph and chunks |
167
+
168
+ ### Graph Memory
169
+
170
+ Graph Memory has two layers:
171
+
172
+ - `eagle-mem scan --force` refreshes the project overview and file graph from the current working tree.
173
+ - `eagle-mem index --force` rebuilds FTS5 source chunks plus file-scoped declarations and import edges.
174
+ - `eagle-mem graph rebuild` does both code-graph cleanup and forced indexing without requiring manual SQLite deletes.
175
+
176
+ Useful checks:
177
+
178
+ ```bash
179
+ eagle-mem graph
180
+ eagle-mem graph query EscapeKeyMonitor
181
+ eagle-mem graph neighbors lib/db-graph.sh
182
+ eagle-mem overview set "Current project briefing..."
183
+ ```
184
+
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`.
165
186
 
166
187
  ### Trust and Recovery
167
188
 
package/architecture.html CHANGED
@@ -1294,6 +1294,11 @@
1294
1294
  <td>Which source files look relevant to this prompt?</td>
1295
1295
  <td>UserPromptSubmit, index CLI</td>
1296
1296
  </tr>
1297
+ <tr>
1298
+ <td><code>graph_nodes</code>, <code>graph_edges</code>, <code>graph_nodes_fts</code></td>
1299
+ <td>Which files, declarations, sessions, memories, and relationships make up the codebase graph?</td>
1300
+ <td>graph CLI, scan, index, overview set, curator Dream Cycle</td>
1301
+ </tr>
1297
1302
  <tr>
1298
1303
  <td><code>agent_memories</code>, <code>agent_plans</code>, <code>agent_tasks</code></td>
1299
1304
  <td>What durable agent artifacts should be shared across Claude Code, Codex, Grok, and Antigravity?</td>
@@ -1316,6 +1321,11 @@
1316
1321
  </tr>
1317
1322
  </tbody>
1318
1323
  </table>
1324
+
1325
+ <div class="callout">
1326
+ <h3>Graph rebuild path</h3>
1327
+ <p><code>eagle-mem graph rebuild</code> is the supported recovery command when graph search shows stale deleted files or stale declarations. It filters missing tracked paths, clears code graph nodes and chunks for the current project, preserves the canonical overview, rewires file nodes, and runs <code>eagle-mem index --force</code> so declarations are file-scoped instead of colliding by bare function name.</p>
1328
+ </div>
1319
1329
  </div>
1320
1330
  </section>
1321
1331
 
@@ -1792,6 +1802,7 @@ eagle-mem install</code></pre>
1792
1802
  <ul>
1793
1803
  <li><code>lib/common.sh</code> - project detection, agent detection, command parsing, redaction, release command detection.</li>
1794
1804
  <li><code>lib/db-core.sh</code> and <code>lib/db.sh</code> - SQLite connection setup and module loading.</li>
1805
+ <li><code>lib/db-graph.sh</code> - graph node/edge helpers, code graph rebuild, file path normalization, and Dream Cycle batching.</li>
1795
1806
  <li><code>lib/db-features.sh</code> - feature lookup, pending verification, verify/waive behavior.</li>
1796
1807
  <li><code>db/*.sql</code> - schema and migrations for all persistent state.</li>
1797
1808
  </ul>
@@ -6,16 +6,16 @@
6
6
  CREATE TRIGGER IF NOT EXISTS val_graph_nodes_insert BEFORE INSERT ON graph_nodes
7
7
  BEGIN
8
8
  SELECT CASE
9
- WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'def')
10
- THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, def')
9
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'struct', 'function', 'func', 'fn', 'def')
10
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, struct, function, func, fn, def')
11
11
  END;
12
12
  END;
13
13
 
14
14
  CREATE TRIGGER IF NOT EXISTS val_graph_nodes_update BEFORE UPDATE OF node_type ON graph_nodes
15
15
  BEGIN
16
16
  SELECT CASE
17
- WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'def')
18
- THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, def')
17
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'struct', 'function', 'func', 'fn', 'def')
18
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, struct, function, func, fn, def')
19
19
  END;
20
20
  END;
21
21
 
@@ -0,0 +1,22 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 038: Expand graph node type validation
3
+ -- ═══════════════════════════════════════════════════════════
4
+
5
+ DROP TRIGGER IF EXISTS val_graph_nodes_insert;
6
+ DROP TRIGGER IF EXISTS val_graph_nodes_update;
7
+
8
+ CREATE TRIGGER val_graph_nodes_insert BEFORE INSERT ON graph_nodes
9
+ BEGIN
10
+ SELECT CASE
11
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'struct', 'function', 'func', 'fn', 'def')
12
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, struct, function, func, fn, def')
13
+ END;
14
+ END;
15
+
16
+ CREATE TRIGGER val_graph_nodes_update BEFORE UPDATE OF node_type ON graph_nodes
17
+ BEGIN
18
+ SELECT CASE
19
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'struct', 'function', 'func', 'fn', 'def')
20
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, struct, function, func, fn, def')
21
+ END;
22
+ END;
package/lib/common.sh CHANGED
@@ -1240,7 +1240,12 @@ eagle_collect_files() {
1240
1240
  local output_file="$2"
1241
1241
 
1242
1242
  if git -C "$target_dir" rev-parse --is-inside-work-tree &>/dev/null; then
1243
- git -C "$target_dir" ls-files --cached --others --exclude-standard > "$output_file"
1243
+ git -C "$target_dir" ls-files --cached --others --exclude-standard \
1244
+ | while IFS= read -r file; do
1245
+ [ -n "$file" ] || continue
1246
+ [ -f "$target_dir/$file" ] || continue
1247
+ printf '%s\n' "$file"
1248
+ done > "$output_file"
1244
1249
  else
1245
1250
  (cd "$target_dir" && find . -type f \
1246
1251
  -not -path '*/node_modules/*' \
package/lib/db-graph.sh CHANGED
@@ -6,6 +6,66 @@
6
6
  [ -n "${_EAGLE_DB_GRAPH_LOADED:-}" ] && return 0
7
7
  _EAGLE_DB_GRAPH_LOADED=1
8
8
 
9
+ eagle_graph_decl_types_sql() {
10
+ printf "'class','struct','function','func','fn','def'"
11
+ }
12
+
13
+ eagle_graph_project_root() {
14
+ local project="${1:-}"
15
+ local candidate=""
16
+
17
+ case "$project" in
18
+ /*) candidate="$project" ;;
19
+ "")
20
+ candidate="$(pwd)"
21
+ ;;
22
+ *)
23
+ if [ -d "$HOME/$project" ]; then
24
+ candidate="$HOME/$project"
25
+ elif [ -d "/$project" ]; then
26
+ candidate="/$project"
27
+ else
28
+ candidate="$(pwd)"
29
+ fi
30
+ ;;
31
+ esac
32
+
33
+ (cd "$candidate" 2>/dev/null && pwd) || printf '%s\n' "$candidate"
34
+ }
35
+
36
+ eagle_graph_normalize_file_path() {
37
+ local project="${1:-}"
38
+ local raw="${2:-}"
39
+ local root="${3:-}"
40
+
41
+ raw=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
42
+ raw="${raw#file://}"
43
+ raw="${raw#./}"
44
+ [ -z "$raw" ] && return 0
45
+
46
+ case "$raw" in
47
+ "~/"*) raw="$HOME/${raw#~/}" ;;
48
+ esac
49
+
50
+ [ -z "$root" ] && root=$(eagle_graph_project_root "$project")
51
+ root="${root%/}"
52
+
53
+ case "$raw" in
54
+ "$root"/*) raw="${raw#"$root"/}" ;;
55
+ "$project"/*) raw="${raw#"$project"/}" ;;
56
+ esac
57
+
58
+ raw="${raw#./}"
59
+ printf '%s\n' "$raw"
60
+ }
61
+
62
+ eagle_graph_declaration_node_name() {
63
+ local file="${1:-}"
64
+ local declaration="${2:-}"
65
+ [ -z "$file" ] && { printf '%s\n' "$declaration"; return; }
66
+ printf '%s::%s\n' "$file" "$declaration"
67
+ }
68
+
9
69
  eagle_graph_add_node() {
10
70
  local project; project=$(eagle_sql_escape "$1")
11
71
  local node_type; node_type=$(eagle_sql_escape "$2")
@@ -52,6 +112,243 @@ eagle_graph_add_edge() {
52
112
  weight = weight + excluded.weight;"
53
113
  }
54
114
 
115
+ eagle_graph_sync_project_overview() {
116
+ local project_raw="${1:-}"
117
+ local overview_raw="${2:-}"
118
+ [ -n "$project_raw" ] || return 0
119
+
120
+ local exists
121
+ exists=$(eagle_db "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'graph_nodes' LIMIT 1;" 2>/dev/null || true)
122
+ [ "$exists" = "graph_nodes" ] || return 0
123
+
124
+ eagle_graph_add_node "$project_raw" "project" "$project_raw" "$overview_raw" ""
125
+ }
126
+
127
+ eagle_graph_reset_file_static_edges() {
128
+ local project_raw="${1:-}"
129
+ local file_raw="${2:-}"
130
+ local file_node_id="${3:-}"
131
+ [ -n "$project_raw" ] && [ -n "$file_raw" ] || return 0
132
+
133
+ local project file type_sql
134
+ project=$(eagle_sql_escape "$project_raw")
135
+ file=$(eagle_sql_escape "$file_raw")
136
+ type_sql=$(eagle_graph_decl_types_sql)
137
+
138
+ if [ -z "$file_node_id" ]; then
139
+ file_node_id=$(eagle_graph_get_node_id "$project_raw" "file" "$file_raw")
140
+ fi
141
+ if [ -n "$file_node_id" ]; then
142
+ file_node_id=$(eagle_sql_int "$file_node_id")
143
+ eagle_db "DELETE FROM graph_edges
144
+ WHERE project = '$project'
145
+ AND source_node_id = $file_node_id
146
+ AND edge_type IN ('declares', 'imports');" >/dev/null
147
+ fi
148
+
149
+ eagle_db "DELETE FROM graph_nodes
150
+ WHERE project = '$project'
151
+ AND node_type IN ($type_sql)
152
+ AND source_path = '$file';" >/dev/null
153
+ }
154
+
155
+ eagle_graph_clear_index_state() {
156
+ local project_raw="${1:-}"
157
+ [ -n "$project_raw" ] || return 0
158
+
159
+ local project type_sql
160
+ project=$(eagle_sql_escape "$project_raw")
161
+ type_sql=$(eagle_graph_decl_types_sql)
162
+
163
+ eagle_db_pipe <<SQL >/dev/null
164
+ BEGIN;
165
+ DELETE FROM code_chunks WHERE project = '$project';
166
+ DELETE FROM graph_edges
167
+ WHERE project = '$project'
168
+ AND edge_type IN ('declares', 'imports');
169
+ DELETE FROM graph_nodes
170
+ WHERE project = '$project'
171
+ AND node_type IN ($type_sql);
172
+ COMMIT;
173
+ SQL
174
+ }
175
+
176
+ eagle_graph_clear_code_state() {
177
+ local project_raw="${1:-}"
178
+ [ -n "$project_raw" ] || return 0
179
+
180
+ local project type_sql
181
+ project=$(eagle_sql_escape "$project_raw")
182
+ type_sql=$(eagle_graph_decl_types_sql)
183
+
184
+ eagle_db_pipe <<SQL >/dev/null
185
+ BEGIN;
186
+ DELETE FROM code_chunks WHERE project = '$project';
187
+ DELETE FROM graph_nodes
188
+ WHERE project = '$project'
189
+ AND node_type IN ('project', 'file', $type_sql);
190
+ COMMIT;
191
+ SQL
192
+ }
193
+
194
+ eagle_graph_rebuild_codebase() {
195
+ local project_raw="${1:-}"
196
+ local target_dir="${2:-.}"
197
+ [ -n "$project_raw" ] || return 1
198
+
199
+ target_dir="$(cd "$target_dir" && pwd)"
200
+ local tmp_files overview project_node_id file file_node_id file_count
201
+ tmp_files=$(mktemp)
202
+ eagle_collect_files "$target_dir" "$tmp_files"
203
+
204
+ eagle_graph_clear_code_state "$project_raw"
205
+
206
+ overview=""
207
+ if declare -F eagle_get_overview >/dev/null 2>&1; then
208
+ overview=$(eagle_get_overview "$project_raw" 2>/dev/null || true)
209
+ fi
210
+ eagle_graph_add_node "$project_raw" "project" "$project_raw" "$overview" ""
211
+ project_node_id=$(eagle_graph_get_node_id "$project_raw" "project" "$project_raw")
212
+
213
+ file_count=0
214
+ if [ -n "$project_node_id" ]; then
215
+ while IFS= read -r file; do
216
+ [ -n "$file" ] || continue
217
+ [ -f "$target_dir/$file" ] || continue
218
+ eagle_graph_add_node "$project_raw" "file" "$file" "" "$target_dir/$file"
219
+ file_node_id=$(eagle_graph_get_node_id "$project_raw" "file" "$file")
220
+ if [ -n "$file_node_id" ]; then
221
+ eagle_graph_add_edge "$project_raw" "$project_node_id" "$file_node_id" "contains" 1.0 >/dev/null || true
222
+ fi
223
+ file_count=$((file_count + 1))
224
+ done < "$tmp_files"
225
+ fi
226
+
227
+ rm -f "$tmp_files"
228
+ printf '%s\n' "$file_count"
229
+ }
230
+
231
+ eagle_graph_wire_recent_session_edges() {
232
+ local project_raw="${1:-}"
233
+ local limit="${2:-15}"
234
+ [ -n "$project_raw" ] || { printf '0\n'; return 0; }
235
+ if ! [[ "$limit" =~ ^[0-9]+$ ]] || [ "$limit" -lt 1 ]; then
236
+ limit=15
237
+ fi
238
+
239
+ local project rows session_count root sql_file
240
+ project=$(eagle_sql_escape "$project_raw")
241
+ rows=$(eagle_db_json "WITH recent_sessions AS (
242
+ SELECT id, started_at, model
243
+ FROM sessions
244
+ WHERE project = '$project'
245
+ ORDER BY started_at DESC
246
+ LIMIT $limit
247
+ )
248
+ SELECT r.id AS session_id,
249
+ r.started_at AS started_at,
250
+ r.model AS model,
251
+ COALESCE(o.files_read, '[]') AS files_read,
252
+ COALESCE(o.files_modified, '[]') AS files_modified
253
+ FROM recent_sessions r
254
+ LEFT JOIN observations o ON o.session_id = r.id
255
+ ORDER BY r.started_at DESC;" 2>/dev/null || true)
256
+
257
+ if [ -z "$rows" ] || [ "$rows" = "[]" ]; then
258
+ printf '0\n'
259
+ return 0
260
+ fi
261
+
262
+ session_count=$(printf '%s' "$rows" | jq -r '.[].session_id // empty' 2>/dev/null | sort -u | wc -l | tr -d ' ')
263
+ root=$(eagle_graph_project_root "$project_raw")
264
+ sql_file=$(mktemp)
265
+
266
+ {
267
+ echo "BEGIN;"
268
+ printf '%s' "$rows" | jq -c '.[]' 2>/dev/null | while IFS= read -r row; do
269
+ local sid started model sid_sql value_sql
270
+ sid=$(printf '%s' "$row" | jq -r '.session_id // empty')
271
+ [ -n "$sid" ] || continue
272
+ started=$(printf '%s' "$row" | jq -r '.started_at // "unknown"')
273
+ model=$(printf '%s' "$row" | jq -r '.model // "unknown"')
274
+ sid_sql=$(eagle_sql_escape "$sid")
275
+ value_sql=$(eagle_sql_escape "Session run on $started using $model")
276
+
277
+ cat <<SQL
278
+ INSERT INTO graph_nodes (project, node_type, node_name, node_value, source_path)
279
+ VALUES ('$project', 'session', '$sid_sql', '$value_sql', '')
280
+ ON CONFLICT(project, node_type, node_name) DO UPDATE SET
281
+ node_value = excluded.node_value,
282
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');
283
+ SQL
284
+
285
+ printf '%s' "$row" | jq -r '.files_read | if type == "string" then (try fromjson catch []) else (. // []) end | .[]?' 2>/dev/null \
286
+ | while IFS= read -r file_path; do
287
+ [ -n "$file_path" ] || continue
288
+ eagle_graph_emit_session_file_edge_sql "$project_raw" "$sid" "$file_path" "$root" "read" "1.0"
289
+ done
290
+ printf '%s' "$row" | jq -r '.files_modified | if type == "string" then (try fromjson catch []) else (. // []) end | .[]?' 2>/dev/null \
291
+ | while IFS= read -r file_path; do
292
+ [ -n "$file_path" ] || continue
293
+ eagle_graph_emit_session_file_edge_sql "$project_raw" "$sid" "$file_path" "$root" "modified" "2.0"
294
+ done
295
+ done
296
+ echo "COMMIT;"
297
+ } > "$sql_file"
298
+
299
+ eagle_db_pipe < "$sql_file" >/dev/null
300
+ rm -f "$sql_file"
301
+ printf '%s\n' "${session_count:-0}"
302
+ }
303
+
304
+ eagle_graph_emit_session_file_edge_sql() {
305
+ local project_raw="${1:-}"
306
+ local session_id="${2:-}"
307
+ local file_path="${3:-}"
308
+ local root="${4:-}"
309
+ local edge_type="${5:-read}"
310
+ local weight="${6:-1.0}"
311
+ local normalized project sid file edge
312
+
313
+ normalized=$(eagle_graph_normalize_file_path "$project_raw" "$file_path" "$root")
314
+ [ -n "$normalized" ] || return 0
315
+
316
+ project=$(eagle_sql_escape "$project_raw")
317
+ sid=$(eagle_sql_escape "$session_id")
318
+ file=$(eagle_sql_escape "$normalized")
319
+ edge=$(eagle_sql_escape "$edge_type")
320
+ if ! [[ "$weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
321
+ weight="1.0"
322
+ fi
323
+
324
+ cat <<SQL
325
+ INSERT OR IGNORE INTO graph_edges (project, source_node_id, target_node_id, edge_type, weight)
326
+ SELECT '$project', s.id, f.id, '$edge', 0.0
327
+ FROM graph_nodes s, graph_nodes f
328
+ WHERE s.project = '$project'
329
+ AND s.node_type = 'session'
330
+ AND s.node_name = '$sid'
331
+ AND f.project = '$project'
332
+ AND f.node_type = 'file'
333
+ AND f.node_name = '$file'
334
+ AND s.id != f.id;
335
+ UPDATE graph_edges
336
+ SET weight = weight + $weight
337
+ WHERE project = '$project'
338
+ AND edge_type = '$edge'
339
+ AND source_node_id = (
340
+ SELECT id FROM graph_nodes
341
+ WHERE project = '$project' AND node_type = 'session' AND node_name = '$sid'
342
+ LIMIT 1
343
+ )
344
+ AND target_node_id = (
345
+ SELECT id FROM graph_nodes
346
+ WHERE project = '$project' AND node_type = 'file' AND node_name = '$file'
347
+ LIMIT 1
348
+ );
349
+ SQL
350
+ }
351
+
55
352
  eagle_graph_query_neighbors() {
56
353
  local node_id; node_id=$(eagle_sql_int "$1")
57
354
  local direction="${2:-out}" # 'out', 'in', or 'both'
@@ -124,14 +421,24 @@ eagle_graph_delete_node() {
124
421
 
125
422
  eagle_graph_prune_orphans() {
126
423
  local project; project=$(eagle_sql_escape "$1")
424
+ local target_dir="${2:-}"
425
+ [ -n "$target_dir" ] && target_dir="${target_dir%/}"
127
426
  # Delete file nodes that no longer exist on disk
128
427
  local result
129
- result=$(eagle_db "SELECT id, node_name FROM graph_nodes WHERE project = '$project' AND node_type = 'file';")
428
+ result=$(eagle_db "SELECT id, node_name, COALESCE(source_path, '') FROM graph_nodes WHERE project = '$project' AND node_type = 'file';")
130
429
  [ -z "$result" ] && return 0
131
430
 
132
- while IFS='|' read -r nid nname; do
431
+ while IFS='|' read -r nid nname nsource; do
133
432
  [ -z "$nid" ] && continue
134
- if [ ! -f "$nname" ]; then
433
+ local candidate="$nsource"
434
+ if [ -z "$candidate" ]; then
435
+ if [ -n "$target_dir" ]; then
436
+ candidate="$target_dir/$nname"
437
+ else
438
+ candidate="$nname"
439
+ fi
440
+ fi
441
+ if [ ! -f "$candidate" ]; then
135
442
  eagle_graph_delete_node "$nid"
136
443
  fi
137
444
  done <<< "$result"
@@ -124,6 +124,10 @@ eagle_upsert_overview() {
124
124
  content = excluded.content,
125
125
  source = excluded.source,
126
126
  updated_at = excluded.updated_at;"
127
+
128
+ if declare -F eagle_graph_sync_project_overview >/dev/null 2>&1; then
129
+ eagle_graph_sync_project_overview "$1" "$raw_content" || true
130
+ fi
127
131
  }
128
132
 
129
133
  eagle_get_overview_source() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.5",
3
+ "version": "4.10.6",
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"
@@ -12,9 +12,11 @@
12
12
  "lib/",
13
13
  "db/",
14
14
  "skills/",
15
+ "tests/*.py",
16
+ "tests/*.sh",
15
17
  "docs/",
16
18
  "architecture.html",
17
- "integrations/",
19
+ "integrations/*.py",
18
20
  "CHANGELOG.md"
19
21
  ],
20
22
  "keywords": [
package/scripts/curate.sh CHANGED
@@ -563,41 +563,12 @@ if [ -n "$co_edit_data" ]; then
563
563
  fi
564
564
 
565
565
  # 7.2 Wire session nodes and access edges
566
- recent_sessions=$(eagle_db "SELECT id, started_at, model FROM sessions WHERE project = '$p_esc' ORDER BY started_at DESC LIMIT 15;")
566
+ recent_sessions=$(eagle_db "SELECT id FROM sessions WHERE project = '$p_esc' ORDER BY started_at DESC LIMIT 15;")
567
567
  if [ -n "$recent_sessions" ]; then
568
- session_wire_count=0
569
- while IFS='|' read -r sid sstart smodel; do
570
- if [ -z "$sid" ]; then continue; fi
571
- if [ "$DRY_RUN" -eq 0 ]; then
572
- eagle_graph_add_node "$project" "session" "$sid" "Session run on $sstart using $smodel" ""
573
- sid_node=$(eagle_graph_get_node_id "$project" "session" "$sid")
574
- if [ -n "$sid_node" ]; then
575
- # Find read/modified files in this session from observations
576
- session_files=$(eagle_db "SELECT files_read, files_modified FROM observations WHERE session_id = '$(eagle_sql_escape "$sid")';")
577
- if [ -n "$session_files" ]; then
578
- while IFS='|' read -r f_read f_mod; do
579
- # Parse files_read JSON list
580
- if [ -n "$f_read" ] && [ "$f_read" != "[]" ]; then
581
- echo "$f_read" | grep -oE '"[^"]+"' | tr -d '"' | while read -r rf; do
582
- if [ -z "$rf" ]; then continue; fi
583
- rfid=$(eagle_graph_get_node_id "$project" "file" "$rf")
584
- [ -n "$rfid" ] && eagle_graph_add_edge "$project" "$sid_node" "$rfid" "read" 1.0
585
- done
586
- fi
587
- # Parse files_modified JSON list
588
- if [ -n "$f_mod" ] && [ "$f_mod" != "[]" ]; then
589
- echo "$f_mod" | grep -oE '"[^"]+"' | tr -d '"' | while read -r mf; do
590
- if [ -z "$mf" ]; then continue; fi
591
- mfid=$(eagle_graph_get_node_id "$project" "file" "$mf")
592
- [ -n "$mfid" ] && eagle_graph_add_edge "$project" "$sid_node" "$mfid" "modified" 2.0
593
- done
594
- fi
595
- done <<< "$session_files"
596
- fi
597
- fi
598
- fi
599
- session_wire_count=$((session_wire_count + 1))
600
- done <<< "$recent_sessions"
568
+ session_wire_count=$(printf '%s\n' "$recent_sessions" | awk 'NF {count++} END {print count + 0}')
569
+ if [ "$DRY_RUN" -eq 0 ]; then
570
+ session_wire_count=$(eagle_graph_wire_recent_session_edges "$project" 15)
571
+ fi
601
572
  eagle_ok "Wired $session_wire_count recent session nodes and edges"
602
573
  fi
603
574
 
package/scripts/help.sh CHANGED
@@ -14,7 +14,7 @@ eagle_banner
14
14
 
15
15
  echo -e " ${BOLD}Eagle Mem${RESET} ${DIM}v${version}${RESET}"
16
16
  echo -e " ${DIM}Shared memory, release guardrails, RTK token protection, and worker lanes${RESET}"
17
- echo -e " ${DIM}for Claude Code, Codex, and Grok.${RESET}"
17
+ echo -e " ${DIM}for Claude Code, Codex, Grok, and Google Antigravity.${RESET}"
18
18
  echo ""
19
19
  echo -e " ${BOLD}Core commands:${RESET}"
20
20
  echo -e " ${CYAN}install${RESET} First-time setup: hooks, database, skills"
@@ -28,6 +28,7 @@ echo -e " ${CYAN}overview${RESET} Build or view project overview"
28
28
  echo -e " ${CYAN}session${RESET} Save a manual session summary"
29
29
  echo -e " ${CYAN}memories${RESET} View/sync agent memories"
30
30
  echo -e " ${CYAN}tasks${RESET} View mirrored tasks"
31
+ echo -e " ${CYAN}graph${RESET} View, query, or rebuild the codebase knowledge graph"
31
32
  echo ""
32
33
  echo -e " ${BOLD}Safety and token controls:${RESET}"
33
34
  echo -e " ${CYAN}feature${RESET} Track, verify, and unblock feature changes"
@@ -59,6 +60,8 @@ echo -e " ${DIM}\$${RESET} eagle-mem search --tasks ${DIM}# in-flight
59
60
  echo -e " ${DIM}\$${RESET} eagle-mem search --files ${DIM}# hot files${RESET}"
60
61
  echo -e " ${DIM}\$${RESET} eagle-mem search --stats ${DIM}# project stats${RESET}"
61
62
  echo -e " ${DIM}\$${RESET} eagle-mem session save --summary \"fixed auth\""
63
+ echo -e " ${DIM}\$${RESET} eagle-mem graph rebuild ${DIM}# rebuild code graph + chunks${RESET}"
64
+ echo -e " ${DIM}\$${RESET} eagle-mem index --force ${DIM}# force source chunk/declaration indexing${RESET}"
62
65
  echo ""
63
66
  echo -e " ${BOLD}Anti-regression:${RESET}"
64
67
  echo -e " ${DIM}\$${RESET} eagle-mem feature pending ${DIM}# pending release blockers${RESET}"
package/scripts/index.sh CHANGED
@@ -15,7 +15,28 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
15
15
 
16
16
  eagle_ensure_db
17
17
 
18
- TARGET_DIR="${1:-.}"
18
+ force=false
19
+ args=()
20
+
21
+ show_help() {
22
+ echo -e " ${BOLD}eagle-mem index${RESET} — Index source files and wire code graph declarations"
23
+ echo ""
24
+ echo -e " ${BOLD}Usage:${RESET}"
25
+ echo -e " eagle-mem index [path] ${DIM}# incrementally index changed files${RESET}"
26
+ echo -e " eagle-mem index ${CYAN}--force${RESET} [path] ${DIM}# rebuild chunks and static code graph edges${RESET}"
27
+ echo ""
28
+ exit 0
29
+ }
30
+
31
+ while [ $# -gt 0 ]; do
32
+ case "$1" in
33
+ --force|-f) force=true; shift ;;
34
+ --help|-h) show_help ;;
35
+ *) args+=("$1"); shift ;;
36
+ esac
37
+ done
38
+
39
+ TARGET_DIR="${args[0]:-.}"
19
40
  TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
20
41
  PROJECT=$(eagle_project_from_cwd "$TARGET_DIR")
21
42
 
@@ -106,10 +127,20 @@ NEEDS_INDEX="$TMPDIR_IDX/needs_index"
106
127
 
107
128
  skipped_count=0
108
129
 
130
+ if [ "$force" = true ]; then
131
+ eagle_info "Force rebuild requested: clearing chunks, declarations, and import edges"
132
+ eagle_graph_clear_index_state "$PROJECT"
133
+ fi
134
+
109
135
  while IFS= read -r file; do
110
136
  full_path="$TARGET_DIR/$file"
111
137
  current_mtime=$(stat -f '%m' "$full_path" 2>/dev/null || stat -c '%Y' "$full_path" 2>/dev/null || echo "0")
112
138
 
139
+ if [ "$force" = true ]; then
140
+ echo "$file"
141
+ continue
142
+ fi
143
+
113
144
  stored_mtime=$(eagle_db "SELECT MAX(mtime) FROM code_chunks WHERE project = '$project_sql' AND file_path = '$(eagle_sql_escape "$file")';")
114
145
 
115
146
  if [ -n "$stored_mtime" ] && [ "$stored_mtime" = "$current_mtime" ]; then
@@ -180,16 +211,26 @@ COMMIT;"
180
211
 
181
212
  # Static syntax relation extraction & graph wiring
182
213
  # Wire node & edge static parser
214
+ overview=$(eagle_get_overview "$PROJECT" 2>/dev/null || true)
215
+ eagle_graph_add_node "$PROJECT" "project" "$PROJECT" "$overview" ""
216
+ project_node_id=$(eagle_graph_get_node_id "$PROJECT" "project" "$PROJECT")
217
+ eagle_graph_add_node "$PROJECT" "file" "$file" "" "$full_path"
183
218
  file_node_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$file")
184
219
  if [ -n "$file_node_id" ]; then
220
+ if [ -n "$project_node_id" ]; then
221
+ eagle_graph_add_edge "$PROJECT" "$project_node_id" "$file_node_id" "contains" 1.0 >/dev/null || true
222
+ fi
223
+ eagle_graph_reset_file_static_edges "$PROJECT" "$file" "$file_node_id"
224
+
185
225
  # 1. Parse function/class declarations
186
226
  # e.g., "def name", "class name", "fn name", "function name", "func name"
187
227
  declarations=$(grep -oE '\<(class|struct|function|def|fn|func)[[:space:]]+[A-Za-z0-9_]+' "$full_path" 2>/dev/null | awk '{print $1 ":" $2}' | sort -u || true)
188
228
  if [ -n "$declarations" ]; then
189
229
  while IFS=':' read -r dtype dname; do
190
230
  [ -z "$dname" ] && continue
191
- eagle_graph_add_node "$PROJECT" "$dtype" "$dname" "Declared in $file" "$file"
192
- decl_node_id=$(eagle_graph_get_node_id "$PROJECT" "$dtype" "$dname")
231
+ decl_node_name=$(eagle_graph_declaration_node_name "$file" "$dname")
232
+ eagle_graph_add_node "$PROJECT" "$dtype" "$decl_node_name" "Declared $dname in $file" "$file"
233
+ decl_node_id=$(eagle_graph_get_node_id "$PROJECT" "$dtype" "$decl_node_name")
193
234
  if [ -n "$decl_node_id" ]; then
194
235
  eagle_graph_add_edge "$PROJECT" "$file_node_id" "$decl_node_id" "declares" 1.0
195
236
  fi
@@ -48,6 +48,7 @@ show_help() {
48
48
  echo -e " eagle-mem memories graph ${DIM}# view codebase knowledge graph summary${RESET}"
49
49
  echo -e " eagle-mem memories graph query ${CYAN}<term>${RESET} ${DIM}# search knowledge graph nodes${RESET}"
50
50
  echo -e " eagle-mem memories graph neighbors ${CYAN}<name>${RESET} ${DIM}# view a node's local network connections${RESET}"
51
+ echo -e " eagle-mem memories graph rebuild ${DIM}# rebuild current project's code graph and chunks${RESET}"
51
52
  echo ""
52
53
  echo -e " ${BOLD}Options:${RESET}"
53
54
  echo -e " ${CYAN}-p, --project${RESET} <name> Filter by project (default: current project)"
@@ -943,6 +944,28 @@ memories_graph() {
943
944
  fi
944
945
  echo ""
945
946
  ;;
947
+ rebuild)
948
+ if [ -z "$project" ]; then
949
+ eagle_err "Cannot determine project for graph rebuild."
950
+ exit 1
951
+ fi
952
+
953
+ local target_dir file_count
954
+ target_dir=$(eagle_graph_project_root "$project")
955
+ if [ ! -d "$target_dir" ]; then
956
+ target_dir="$(pwd)"
957
+ fi
958
+
959
+ eagle_header "Knowledge Graph Rebuild"
960
+ eagle_info "Project: $project"
961
+ eagle_info "Path: $target_dir"
962
+ echo ""
963
+
964
+ file_count=$(eagle_graph_rebuild_codebase "$project" "$target_dir")
965
+ eagle_ok "Rebuilt file graph ($file_count files)"
966
+ bash "$SCRIPTS_DIR/index.sh" --force "$target_dir"
967
+ eagle_footer "Graph rebuild complete."
968
+ ;;
946
969
  summary|*)
947
970
  eagle_header "Knowledge Graph Summary"
948
971
  echo ""
package/scripts/scan.sh CHANGED
@@ -210,9 +210,9 @@ detect_framework "mix.exs" "Elixir/Mix" || true
210
210
 
211
211
  top_dirs=""
212
212
  if [ "$is_git" = true ]; then
213
- git -C "$TARGET_DIR" ls-files --cached --others --exclude-standard | cut -d/ -f1 | sort -u | while read -r item; do
213
+ cut -d/ -f1 "$TMPFILE" | sort -u | while read -r item; do
214
214
  if [ -d "$TARGET_DIR/$item" ]; then
215
- count=$(git -C "$TARGET_DIR" ls-files --cached --others --exclude-standard "$item/" 2>/dev/null | wc -l | tr -d ' ')
215
+ count=$(grep -c "^$item/" "$TMPFILE" 2>/dev/null || true)
216
216
  echo "$item/ ($count)"
217
217
  fi
218
218
  done > "${TMPFILE}.dirs"
@@ -372,7 +372,7 @@ eagle_graph_add_node "$PROJECT" "project" "$PROJECT" "$overview" ""
372
372
  project_node_id=$(eagle_graph_get_node_id "$PROJECT" "project" "$PROJECT")
373
373
 
374
374
  # Prune deleted/removed files from graph
375
- eagle_graph_prune_orphans "$PROJECT"
375
+ eagle_graph_prune_orphans "$PROJECT" "$TARGET_DIR"
376
376
 
377
377
  file_node_count=0
378
378
  if [ -n "$project_node_id" ]; then
package/scripts/test.sh CHANGED
@@ -51,6 +51,7 @@ run_check "Agent Orchestration (orchestrate help)" "\"$EAGLE_BIN\" orchestrate -
51
51
  run_check "Cross Agent Memory (memories query)" "\"$EAGLE_BIN\" memories --json > /dev/null"
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
+ run_check "Graph Memory Rebuild (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_graph_memory.sh\""
54
55
 
55
56
  echo ""
56
57
  if [ "$errors" -eq 0 ]; then
@@ -67,4 +68,4 @@ else
67
68
  fi
68
69
 
69
70
  echo ""
70
- eagle_info "Run 'eagle-mem test' regularly to guard the memory layer itself."
71
+ eagle_info "Run 'eagle-mem test' regularly to guard the memory layer itself."
package/scripts/update.sh CHANGED
@@ -20,6 +20,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
20
20
  SETTINGS="$EAGLE_SETTINGS"
21
21
  claude_found=false
22
22
  codex_found=false
23
+ grok_found=false
23
24
 
24
25
  eagle_header "Update"
25
26
 
@@ -39,18 +40,22 @@ fi
39
40
  if [ -d "$EAGLE_CODEX_DIR" ] || command -v codex &>/dev/null; then
40
41
  codex_found=true
41
42
  fi
43
+ if [ -d "$EAGLE_GROK_DIR" ]; then
44
+ grok_found=true
45
+ fi
42
46
 
43
47
  eagle_runtime_change_plan "update" "$PACKAGE_DIR" "$claude_found" "$codex_found"
44
48
 
45
49
  # ─── Update files ──────────────────────────────────────────
46
50
 
47
- mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts}
51
+ mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts,integrations}
48
52
 
49
53
  cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
50
54
  cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
51
55
  cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
52
56
  cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
53
57
  cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
58
+ cp -r "$PACKAGE_DIR"/integrations/* "$EAGLE_MEM_DIR/integrations/" 2>/dev/null || true
54
59
 
55
60
  chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
56
61
  chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
@@ -140,6 +145,25 @@ if [ "$codex_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
140
145
  eagle_ok "Codex skills updated"
141
146
  fi
142
147
 
148
+ if [ "$grok_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
149
+ mkdir -p "$EAGLE_GROK_SKILLS_DIR"
150
+ find "$EAGLE_GROK_SKILLS_DIR" -maxdepth 1 -name "eagle-mem-*" -type l 2>/dev/null | while read -r existing; do
151
+ skill_name=$(basename "$existing")
152
+ if [ ! -d "$PACKAGE_DIR/skills/$skill_name" ]; then
153
+ rm "$existing"
154
+ eagle_ok "Removed stale Grok skill: $skill_name"
155
+ fi
156
+ done
157
+ for skill_dir in "$PACKAGE_DIR"/skills/*/; do
158
+ [ ! -d "$skill_dir" ] && continue
159
+ skill_name=$(basename "$skill_dir")
160
+ dst="$EAGLE_GROK_SKILLS_DIR/$skill_name"
161
+ [ -L "$dst" ] && rm "$dst"
162
+ ln -sf "$skill_dir" "$dst"
163
+ done
164
+ eagle_ok "Grok skills updated"
165
+ fi
166
+
143
167
  # ─── Refresh generated Claude statusline wrapper ───────────
144
168
 
145
169
  statusline_wrapper="$EAGLE_MEM_DIR/scripts/statusline-wrapper.sh"
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: eagle-mem-memories
3
3
  description: >
4
- View and sync Claude Code and Codex memories, plans, and tasks mirrored in Eagle Mem. Use when:
4
+ View and sync Claude Code, Codex, Grok, and Antigravity memories, plans, tasks,
5
+ and graph memory mirrored in Eagle Mem. Use when:
5
6
  'eagle memories', 'show memories', 'sync memories', 'what does the agent remember',
6
7
  'show plans', 'show tasks', 'mirror memories', 'onboard project',
7
8
  'what did past sessions learn'. Uses the eagle-mem CLI.
@@ -11,9 +12,9 @@ description: >
11
12
 
12
13
  ## Purpose
13
14
 
14
- **For the user:** Claude Code and Codex remember across sessions. Decisions, preferences, project context, and architectural plans survive session boundaries. The user never has to re-explain "we chose Postgres because..." or "don't use semicolons in this project."
15
+ **For the user:** Claude Code, Codex, Grok, and Antigravity remember across sessions. Decisions, preferences, project context, graph relationships, and architectural plans survive session boundaries. The user never has to re-explain "we chose Postgres because..." or "don't use semicolons in this project."
15
16
 
16
- **For you:** Access to what past Claude Code and Codex sessions learned about this project. Memories tell you *why* decisions were made. Plans tell you *what's coming*. Tasks tell you *what's in flight*. Together they're the knowledge bridge that makes you effective from message one.
17
+ **For you:** Access to what past agent sessions learned about this project. Memories tell you *why* decisions were made. Plans tell you *what's coming*. Tasks tell you *what's in flight*. Graph memory tells you *which files, declarations, and sessions are connected*. Together they're the knowledge bridge that makes you effective from message one.
17
18
 
18
19
  ## Judgment
19
20
 
@@ -64,6 +65,14 @@ eagle-mem memories tasks search "refactor"
64
65
  eagle-mem memories tasks show <file_path>
65
66
  ```
66
67
 
68
+ **Graph memory** -- codebase file nodes, file-scoped declarations, session/file access edges, memory nodes, and relationship edges.
69
+ ```bash
70
+ eagle-mem graph # graph summary
71
+ eagle-mem graph query "finishDictation" # search nodes
72
+ eagle-mem graph neighbors "lib/db-graph.sh"
73
+ eagle-mem graph rebuild # rebuild stale code graph + chunks
74
+ ```
75
+
67
76
  ### 2. Understand how data flows in
68
77
 
69
78
  Two paths feed the mirror:
@@ -0,0 +1,114 @@
1
+ """
2
+ Mock Unit Test Suite for Native Eagle Mem Google Antigravity Hooks.
3
+ Verifies that all 5 lifecycle hooks run without any errors, trigger appropriate
4
+ asynchronous subprocess mock calls, and correctly output and format findings.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import asyncio
10
+ import unittest
11
+
12
+ # Ensure the integrations folder is on the Python path
13
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
14
+
15
+ from integrations.google_antigravity_hook import (
16
+ EagleMemAntigravityHook,
17
+ get_session_id,
18
+ map_tool_name,
19
+ run_cmd_async,
20
+ run_hook_async,
21
+ HAS_ANTIGRAVITY
22
+ )
23
+
24
+ class MockToolCall:
25
+ def __init__(self, name, arguments):
26
+ self.name = name
27
+ self.arguments = arguments
28
+
29
+ class MockToolCallResult:
30
+ def __init__(self, tool_call, output):
31
+ self.tool_call = tool_call
32
+ self.output = output
33
+
34
+ class TestAntigravityHooks(unittest.IsolatedAsyncioTestCase):
35
+
36
+ async def asyncSetUp(self):
37
+ self.hook = EagleMemAntigravityHook(agent_name="antigravity-test")
38
+ self.session_id = get_session_id()
39
+ self.assertTrue(self.session_id.startswith("agy-") or "EAGLE_SESSION_ID" in os.environ)
40
+
41
+ def test_tool_mapping(self):
42
+ self.assertEqual(map_tool_name("run_command"), "Bash")
43
+ self.assertEqual(map_tool_name("exec_command"), "Bash")
44
+ self.assertEqual(map_tool_name("view_file"), "Read")
45
+ self.assertEqual(map_tool_name("edit_file"), "Edit")
46
+ self.assertEqual(map_tool_name("create_file"), "Write")
47
+ self.assertEqual(map_tool_name("custom_tool"), "custom_tool")
48
+
49
+ async def test_session_start_hook(self):
50
+ print("\n--- Testing SessionStart Hook ---")
51
+ # Run on_session_start. Should trigger native start or fallback search gracefully.
52
+ try:
53
+ await self.hook.on_session_start()
54
+ print("✓ SessionStart executed successfully.")
55
+ except Exception as e:
56
+ self.fail(f"on_session_start failed: {e}")
57
+
58
+ async def test_pre_tool_call_decide_hook_allow(self):
59
+ print("\n--- Testing PreToolCallDecide Hook (Allow) ---")
60
+ tool_call = MockToolCall("run_command", {"CommandLine": "echo 'Hello World'"})
61
+ result = await self.hook.pre_tool_call_decide(tool_call)
62
+ self.assertTrue(result.allow)
63
+ print("✓ PreToolCallDecide (Allow) executed successfully.")
64
+
65
+ async def test_pre_tool_call_decide_hook_deny(self):
66
+ print("\n--- Testing PreToolCallDecide Hook (Deny - Release Boundary) ---")
67
+ # Under normal conditions, git push might be blocked if features are pending.
68
+ # Let's verify that the hook runs cleanly when processing git push
69
+ tool_call = MockToolCall("run_command", {"CommandLine": "git push"})
70
+ result = await self.hook.pre_tool_call_decide(tool_call)
71
+ self.assertIn(result.allow, [True, False])
72
+ print(f"✓ PreToolCallDecide (Deny Check) executed successfully with allow={result.allow}.")
73
+
74
+ async def test_post_tool_call_hook(self):
75
+ print("\n--- Testing PostToolCall Hook ---")
76
+ tool_call = MockToolCall("run_command", {"CommandLine": "echo 'Test'"})
77
+ tool_call_result = MockToolCallResult(tool_call, "Test stdout output")
78
+ try:
79
+ await self.hook.post_tool_call(tool_call_result)
80
+ # Give a small slice of time for background tasks to start
81
+ await asyncio.sleep(0.1)
82
+ print("✓ PostToolCall executed successfully.")
83
+ except Exception as e:
84
+ self.fail(f"post_tool_call failed: {e}")
85
+
86
+ async def test_post_turn_hook(self):
87
+ print("\n--- Testing PostTurn Hook ---")
88
+ final_response = "I have successfully resolved the issue by editing the configuration files."
89
+ try:
90
+ await self.hook.post_turn(final_response)
91
+ await asyncio.sleep(0.1)
92
+ print("✓ PostTurn executed successfully.")
93
+ except Exception as e:
94
+ self.fail(f"post_turn failed: {e}")
95
+
96
+ async def test_compaction_hook(self):
97
+ print("\n--- Testing Compaction Hook ---")
98
+ try:
99
+ await self.hook.on_compaction(data=None)
100
+ print("✓ Compaction executed successfully.")
101
+ except Exception as e:
102
+ self.fail(f"on_compaction failed: {e}")
103
+
104
+ async def test_session_end_hook(self):
105
+ print("\n--- Testing SessionEnd Hook ---")
106
+ try:
107
+ await self.hook.on_session_end()
108
+ await asyncio.sleep(0.1)
109
+ print("✓ SessionEnd executed successfully.")
110
+ except Exception as e:
111
+ self.fail(f"on_session_end failed: {e}")
112
+
113
+ if __name__ == "__main__":
114
+ unittest.main()
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bash
2
+ # Focused graph-memory regressions. Runs in an isolated HOME/EAGLE_MEM_DIR.
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-graph-memory.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"
14
+
15
+ . "$ROOT_DIR/lib/common.sh"
16
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
17
+ . "$ROOT_DIR/lib/db.sh"
18
+
19
+ repo="$HOME/project"
20
+ mkdir -p "$repo"
21
+ git -C "$repo" init -q
22
+ git -C "$repo" config user.email "test@example.com"
23
+ git -C "$repo" config user.name "Eagle Mem Test"
24
+
25
+ cat > "$repo/live.sh" <<'EOF'
26
+ function liveThing() {
27
+ echo live
28
+ }
29
+ EOF
30
+
31
+ cat > "$repo/gone.sh" <<'EOF'
32
+ function goneThing() {
33
+ echo gone
34
+ }
35
+ EOF
36
+
37
+ git -C "$repo" add live.sh gone.sh
38
+ rm "$repo/gone.sh"
39
+
40
+ collected="$tmp_dir/files.txt"
41
+ eagle_collect_files "$repo" "$collected"
42
+ grep -qx "live.sh" "$collected"
43
+ if grep -qx "gone.sh" "$collected"; then
44
+ echo "deleted tracked file was collected" >&2
45
+ exit 1
46
+ fi
47
+
48
+ for node_type in func struct function fn def class; do
49
+ eagle_db "INSERT INTO graph_nodes (project, node_type, node_name)
50
+ VALUES ('migration-test', '$node_type', '$node_type-node');" >/dev/null
51
+ done
52
+
53
+ cat > "$repo/a.sh" <<'EOF'
54
+ function finishDictation() {
55
+ echo a
56
+ }
57
+ EOF
58
+
59
+ cat > "$repo/b.sh" <<'EOF'
60
+ function finishDictation() {
61
+ echo b
62
+ }
63
+ EOF
64
+
65
+ cat > "$repo/old.sh" <<'EOF'
66
+ function CloudDictationPipeline() {
67
+ echo old
68
+ }
69
+ EOF
70
+
71
+ git -C "$repo" add a.sh b.sh old.sh
72
+
73
+ "$EAGLE_BIN" scan --force "$repo" >/dev/null
74
+ "$EAGLE_BIN" index --force "$repo" >/dev/null
75
+
76
+ decl_count=$(eagle_db "SELECT COUNT(*)
77
+ FROM graph_nodes
78
+ WHERE project = 'project'
79
+ AND node_type = 'function'
80
+ AND node_name LIKE '%::finishDictation';")
81
+ if [ "$decl_count" != "2" ]; then
82
+ echo "expected duplicate declarations to remain file-scoped, got $decl_count" >&2
83
+ exit 1
84
+ fi
85
+
86
+ (cd "$repo" && "$EAGLE_BIN" overview set "Fresh offline-only overview" >/dev/null)
87
+ overview_value=$(eagle_db "SELECT node_value
88
+ FROM graph_nodes
89
+ WHERE project = 'project'
90
+ AND node_type = 'project'
91
+ AND node_name = 'project'
92
+ LIMIT 1;")
93
+ case "$overview_value" in
94
+ *"Fresh offline-only overview"*) ;;
95
+ *)
96
+ echo "overview set did not sync graph project node" >&2
97
+ exit 1
98
+ ;;
99
+ esac
100
+
101
+ eagle_db "INSERT INTO sessions (id, project, cwd, model, status)
102
+ VALUES ('session-graph-test', 'project', '$repo', 'test-model', 'completed');" >/dev/null
103
+ eagle_db "INSERT INTO observations (session_id, project, tool_name, files_read, files_modified)
104
+ VALUES ('session-graph-test', 'project', 'Read',
105
+ '[\"$repo/a.sh\"]',
106
+ '[\"$repo/b.sh\"]');" >/dev/null
107
+
108
+ eagle_graph_wire_recent_session_edges "project" 15 >/dev/null
109
+ read_edges=$(eagle_db "SELECT COUNT(*)
110
+ FROM graph_edges e
111
+ JOIN graph_nodes s ON s.id = e.source_node_id
112
+ JOIN graph_nodes f ON f.id = e.target_node_id
113
+ WHERE e.project = 'project'
114
+ AND e.edge_type = 'read'
115
+ AND s.node_type = 'session'
116
+ AND s.node_name = 'session-graph-test'
117
+ AND f.node_type = 'file'
118
+ AND f.node_name = 'a.sh';")
119
+ modified_edges=$(eagle_db "SELECT COUNT(*)
120
+ FROM graph_edges e
121
+ JOIN graph_nodes s ON s.id = e.source_node_id
122
+ JOIN graph_nodes f ON f.id = e.target_node_id
123
+ WHERE e.project = 'project'
124
+ AND e.edge_type = 'modified'
125
+ AND s.node_type = 'session'
126
+ AND s.node_name = 'session-graph-test'
127
+ AND f.node_type = 'file'
128
+ AND f.node_name = 'b.sh';")
129
+ if [ "$read_edges" != "1" ] || [ "$modified_edges" != "1" ]; then
130
+ echo "batched session graph wiring did not normalize absolute paths" >&2
131
+ exit 1
132
+ fi
133
+
134
+ rm "$repo/old.sh"
135
+ git -C "$repo" add -u old.sh
136
+ (cd "$repo" && "$EAGLE_BIN" graph rebuild >/dev/null)
137
+
138
+ stale_count=$(eagle_db "SELECT COUNT(*)
139
+ FROM graph_nodes
140
+ WHERE project = 'project'
141
+ AND (node_name LIKE '%CloudDictationPipeline%'
142
+ OR node_name = 'old.sh');")
143
+ if [ "$stale_count" != "0" ]; then
144
+ echo "graph rebuild left stale removed-file nodes" >&2
145
+ exit 1
146
+ fi
147
+
148
+ echo "graph memory regressions passed"