eagle-mem 4.10.3 → 4.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/bin/eagle-mem +1 -0
- package/db/035_graph_memories.sql +65 -0
- package/db/036_graph_constraints.sql +37 -0
- package/db/037_task_dedup.sql +20 -0
- package/hooks/post-tool-use.sh +3 -3
- package/lib/db-graph.sh +138 -0
- package/lib/db.sh +1 -0
- package/package.json +1 -1
- package/scripts/curate.sh +160 -9
- package/scripts/index.sh +44 -0
- package/scripts/install.sh +211 -112
- package/scripts/memories.sh +119 -0
- package/scripts/scan.sh +23 -1
- package/scripts/test.sh +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to the **Eagle Mem** project are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v4.10.5 Hardening Release
|
|
8
|
+
|
|
9
|
+
This patch release hardens the database architecture, improves CLI usability, and increases programmatic test coverage for all core features:
|
|
10
|
+
|
|
11
|
+
- **Database-Level Task Deduplication**: Normalized synthetic task file paths to `event://${task_id}` in `hooks/post-tool-use.sh`, making task tracking constant across sessions. Added a partial unique index `idx_agent_tasks_dedup` on `(project, source_task_id)` via migration `db/037_task_dedup.sql` to block duplicate task rows on repeated sync loops.
|
|
12
|
+
- **Resilient Curation Engine (`curate.sh`)**: Refactored vulnerable inline conditional `&& continue` statements to safe, standard `if` blocks, preventing pipeline subshell crashes under `set -e` and guaranteeing metadata and footer summaries complete successfully.
|
|
13
|
+
- **CLI Usability & Previews (`--help` / `--dry-run`)**: Integrated structured option parsing cases for `-h`/`--help` and `--dry-run` to both `curate.sh` and `install.sh`. Covered all installer filesystem writes, hook updates, and migrations in `install.sh --dry-run` to enable a zero-risk preview of planned changes.
|
|
14
|
+
- **Core Smoke Test suite (`test.sh`)**: Expanded the automated smoke test suite to run concrete checks for **7 core features** (`compaction-survival`, `feature-verification`, `grok-cli-integration`, `agent-orchestration`, `Cross Agent Memory`, `Installer And Updater`, `Code Scan And Index`), automatically updating the SQLite database to mark them verified upon success.
|
|
15
|
+
- **Stale Task Cleanup**: Resolved compaction warning overhead by marking stale, in-progress tasks (`840`, `895`, `968`, `970`) as `'completed'`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## v4.10.4 Minor Release
|
|
20
|
+
|
|
21
|
+
This release introduces native relational **Knowledge Graph Memories** and an automated background **Dream Cycle** curator to consolidate multi-agent developer context:
|
|
22
|
+
|
|
23
|
+
- **Graph-based Memories**: Full integration of custom semantic code graph database primitives (`lib/db-graph.sh` and migrations `db/035_graph_memories.sql` and `db/036_graph_constraints.sql`) to link files, functions, variables, sessions, and memories.
|
|
24
|
+
- **Background Dream Cycle Curation**: Structured offline compilation in `scripts/curate.sh` that merges redundant, overlapping memories into clean `--- Compiled Truth ---` with an underlying `--- Evidence Trail ---`.
|
|
25
|
+
- **Database Firewalls**: Enforced strict enums check triggers on `node_type` and `edge_type` to guarantee complete data integrity.
|
|
26
|
+
- **Sanitized FTS Wildcards**: Safe search query sanitization in `eagle_graph_search` to prevent SQLite MATCH syntax errors on special characters.
|
|
27
|
+
- **Unified Global Porting**: Symlinked advanced nested specialist developer skills and HTTP Model Context Protocol (MCP) gateways natively into the Google Antigravity active configuration.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
## v4.10.2 Patch
|
|
8
32
|
|
|
9
33
|
This release expands the Eagle Mem adapter layer and addresses multi-agent planning synchronization and gap remediations across all four supported agents:
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, and Google Antigravity hook-backed shared memory, gives Grok the same skills and CLI memory surface, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
|
|
13
13
|
|
|
14
|
-
**v4.10.
|
|
14
|
+
**v4.10.5 and onward focuses on Graph Memory, Dream Cycle Curation, Grok, Google Antigravity support, and Compaction Survival:** Grok users get first-class skill linking and `eagle-mem grok-bootstrap`, while Antigravity users get native Python SDK hook integration via `google_antigravity_hook.py`. Claude Code, Codex, and Antigravity receive the deepest automatic lifecycle support through hooks; Grok currently uses the shared CLI and skill workflow.
|
|
15
15
|
|
|
16
16
|
**Website:** [Product](https://eagleisbatman.github.io/eagle-mem/) |
|
|
17
17
|
[Architecture](https://eagleisbatman.github.io/eagle-mem/architecture.html) |
|
package/bin/eagle-mem
CHANGED
|
@@ -27,6 +27,7 @@ case "$command" in
|
|
|
27
27
|
statusline) "$SCRIPTS_DIR/statusline-em.sh" "$@" ;;
|
|
28
28
|
guard) bash "$SCRIPTS_DIR/guard.sh" "$@" ;;
|
|
29
29
|
overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
|
|
30
|
+
graph) bash "$SCRIPTS_DIR/memories.sh" graph "$@" ;;
|
|
30
31
|
session|sessions)
|
|
31
32
|
bash "$SCRIPTS_DIR/session.sh" "$@" ;;
|
|
32
33
|
memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Migration 035: Graph-based memories (nodes and edges relational tables)
|
|
3
|
+
-- ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
-- ─── Graph Nodes ───────────────────────────────────────────
|
|
6
|
+
-- Represents semantic entities in the codebase (files, features, memories, tasks, sessions, tags, etc.)
|
|
7
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
project TEXT NOT NULL,
|
|
10
|
+
node_type TEXT NOT NULL, -- 'file', 'feature', 'memory', 'task', 'session', 'tag', etc.
|
|
11
|
+
node_name TEXT NOT NULL, -- e.g., 'lib/db-graph.sh', 'auth-middleware', 'agy-session-123'
|
|
12
|
+
node_value TEXT, -- Optional content, JSON payload, description, etc.
|
|
13
|
+
source_path TEXT, -- Optional filepath or link
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
15
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
16
|
+
UNIQUE(project, node_type, node_name)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_project ON graph_nodes(project);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_type_name ON graph_nodes(node_type, node_name);
|
|
21
|
+
|
|
22
|
+
-- ─── Graph Edges ───────────────────────────────────────────
|
|
23
|
+
-- Represents relationships between nodes with optional weights
|
|
24
|
+
CREATE TABLE IF NOT EXISTS graph_edges (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
project TEXT NOT NULL,
|
|
27
|
+
source_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
|
|
28
|
+
target_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
|
|
29
|
+
edge_type TEXT NOT NULL, -- 'imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified'
|
|
30
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
31
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
32
|
+
UNIQUE(project, source_node_id, target_node_id, edge_type)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_project ON graph_edges(project);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges(source_node_id);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges(target_node_id);
|
|
38
|
+
|
|
39
|
+
-- ─── FTS5: Full-text search on graph nodes ──────────────────
|
|
40
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS graph_nodes_fts USING fts5(
|
|
41
|
+
node_name,
|
|
42
|
+
node_value,
|
|
43
|
+
content='graph_nodes',
|
|
44
|
+
content_rowid='id'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- Triggers to keep FTS in sync
|
|
48
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_ai AFTER INSERT ON graph_nodes BEGIN
|
|
49
|
+
INSERT INTO graph_nodes_fts(rowid, node_name, node_value)
|
|
50
|
+
VALUES (new.id, new.node_name, new.node_value);
|
|
51
|
+
END;
|
|
52
|
+
|
|
53
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_ad AFTER DELETE ON graph_nodes BEGIN
|
|
54
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, node_name, node_value)
|
|
55
|
+
VALUES ('delete', old.id, old.node_name, old.node_value);
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_au
|
|
59
|
+
AFTER UPDATE OF node_name, node_value ON graph_nodes
|
|
60
|
+
BEGIN
|
|
61
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, node_name, node_value)
|
|
62
|
+
VALUES ('delete', old.id, old.node_name, old.node_value);
|
|
63
|
+
INSERT INTO graph_nodes_fts(rowid, node_name, node_value)
|
|
64
|
+
VALUES (new.id, new.node_name, new.node_value);
|
|
65
|
+
END;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Migration 036: Database-level enums constraints triggers
|
|
3
|
+
-- ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
-- ─── Node Type Constraints ──────────────────────────────────
|
|
6
|
+
CREATE TRIGGER IF NOT EXISTS val_graph_nodes_insert BEFORE INSERT ON graph_nodes
|
|
7
|
+
BEGIN
|
|
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')
|
|
11
|
+
END;
|
|
12
|
+
END;
|
|
13
|
+
|
|
14
|
+
CREATE TRIGGER IF NOT EXISTS val_graph_nodes_update BEFORE UPDATE OF node_type ON graph_nodes
|
|
15
|
+
BEGIN
|
|
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')
|
|
19
|
+
END;
|
|
20
|
+
END;
|
|
21
|
+
|
|
22
|
+
-- ─── Edge Type Constraints ──────────────────────────────────
|
|
23
|
+
CREATE TRIGGER IF NOT EXISTS val_graph_edges_insert BEFORE INSERT ON graph_edges
|
|
24
|
+
BEGIN
|
|
25
|
+
SELECT CASE
|
|
26
|
+
WHEN NEW.edge_type NOT IN ('imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified', 'read')
|
|
27
|
+
THEN RAISE(ABORT, 'Data Integrity Error: Invalid edge type. Must be one of imports, declares, references, verifies, contains, supersedes, co_edited, modified, read')
|
|
28
|
+
END;
|
|
29
|
+
END;
|
|
30
|
+
|
|
31
|
+
CREATE TRIGGER IF NOT EXISTS val_graph_edges_update BEFORE UPDATE OF edge_type ON graph_edges
|
|
32
|
+
BEGIN
|
|
33
|
+
SELECT CASE
|
|
34
|
+
WHEN NEW.edge_type NOT IN ('imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified', 'read')
|
|
35
|
+
THEN RAISE(ABORT, 'Data Integrity Error: Invalid edge type. Must be one of imports, declares, references, verifies, contains, supersedes, co_edited, modified, read')
|
|
36
|
+
END;
|
|
37
|
+
END;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Migration 037: Deduplicate agent tasks
|
|
3
|
+
-- Deletes duplicate task entries keeping the latest one
|
|
4
|
+
-- and sets up a partial unique index on project + source_task_id.
|
|
5
|
+
-- ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
-- Delete duplicate tasks, keeping the most recent (highest ID)
|
|
8
|
+
DELETE FROM agent_tasks
|
|
9
|
+
WHERE source_task_id IS NOT NULL AND source_task_id != ''
|
|
10
|
+
AND id NOT IN (
|
|
11
|
+
SELECT MAX(id)
|
|
12
|
+
FROM agent_tasks
|
|
13
|
+
WHERE source_task_id IS NOT NULL AND source_task_id != ''
|
|
14
|
+
GROUP BY project, source_task_id
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- Create partial unique index to guarantee no duplicates for valid source task IDs
|
|
18
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_tasks_dedup
|
|
19
|
+
ON agent_tasks(project, source_task_id)
|
|
20
|
+
WHERE source_task_id IS NOT NULL AND source_task_id != '';
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -39,8 +39,8 @@ case "$hook_event" in
|
|
|
39
39
|
local_status="pending"
|
|
40
40
|
[ "$hook_event" = "TaskCompleted" ] && local_status="completed"
|
|
41
41
|
|
|
42
|
-
# Synthetic file_path keyed on
|
|
43
|
-
synthetic_fp="event://${
|
|
42
|
+
# Synthetic file_path keyed on task — file_path is the UNIQUE column
|
|
43
|
+
synthetic_fp="event://${task_id}"
|
|
44
44
|
|
|
45
45
|
tid_sql=$(eagle_sql_escape "$task_id")
|
|
46
46
|
fp_sql=$(eagle_sql_escape "$synthetic_fp")
|
|
@@ -155,7 +155,7 @@ case "$tool_name" in
|
|
|
155
155
|
task_status=$(echo "$input" | jq -r '.tool_input.status // empty')
|
|
156
156
|
tool_summary="TaskUpdate: ${task_id} → ${task_status}"
|
|
157
157
|
if [ -n "$task_id" ] && [ -n "$task_status" ]; then
|
|
158
|
-
fp_sql=$(eagle_sql_escape "event://${
|
|
158
|
+
fp_sql=$(eagle_sql_escape "event://${task_id}")
|
|
159
159
|
stat_sql=$(eagle_sql_escape "$task_status")
|
|
160
160
|
eagle_db_pipe <<SQL
|
|
161
161
|
UPDATE agent_tasks SET status = '$stat_sql', updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
package/lib/db-graph.sh
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Graph memories helpers
|
|
4
|
+
# Primitives for self-wiring codebase graph and relations
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
[ -n "${_EAGLE_DB_GRAPH_LOADED:-}" ] && return 0
|
|
7
|
+
_EAGLE_DB_GRAPH_LOADED=1
|
|
8
|
+
|
|
9
|
+
eagle_graph_add_node() {
|
|
10
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
11
|
+
local node_type; node_type=$(eagle_sql_escape "$2")
|
|
12
|
+
local node_name; node_name=$(eagle_sql_escape "$3")
|
|
13
|
+
local node_value; node_value=$(eagle_sql_escape "${4:-}")
|
|
14
|
+
local source_path; source_path=$(eagle_sql_escape "${5:-}")
|
|
15
|
+
|
|
16
|
+
eagle_db "INSERT INTO graph_nodes (project, node_type, node_name, node_value, source_path)
|
|
17
|
+
VALUES ('$project', '$node_type', '$node_name', '$node_value', '$source_path')
|
|
18
|
+
ON CONFLICT(project, node_type, node_name) DO UPDATE SET
|
|
19
|
+
node_value = CASE WHEN excluded.node_value != '' THEN excluded.node_value ELSE node_value END,
|
|
20
|
+
source_path = CASE WHEN excluded.source_path != '' THEN excluded.source_path ELSE source_path END,
|
|
21
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
eagle_graph_get_node_id() {
|
|
25
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
26
|
+
local node_type; node_type=$(eagle_sql_escape "$2")
|
|
27
|
+
local node_name; node_name=$(eagle_sql_escape "$3")
|
|
28
|
+
|
|
29
|
+
eagle_db "SELECT id FROM graph_nodes
|
|
30
|
+
WHERE project = '$project'
|
|
31
|
+
AND node_type = '$node_type'
|
|
32
|
+
AND node_name = '$node_name'
|
|
33
|
+
LIMIT 1;"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
eagle_graph_add_edge() {
|
|
37
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
38
|
+
local source_id; source_id=$(eagle_sql_int "$2")
|
|
39
|
+
local target_id; target_id=$(eagle_sql_int "$3")
|
|
40
|
+
local edge_type; edge_type=$(eagle_sql_escape "$4")
|
|
41
|
+
local weight="${5:-1.0}"
|
|
42
|
+
if ! [[ "$weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
43
|
+
weight="1.0"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
[ -z "$source_id" ] || [ -z "$target_id" ] && return 1
|
|
47
|
+
[ "$source_id" = "$target_id" ] && return 0 # avoid self-loops
|
|
48
|
+
|
|
49
|
+
eagle_db "INSERT INTO graph_edges (project, source_node_id, target_node_id, edge_type, weight)
|
|
50
|
+
VALUES ('$project', $source_id, $target_id, '$edge_type', $weight)
|
|
51
|
+
ON CONFLICT(project, source_node_id, target_node_id, edge_type) DO UPDATE SET
|
|
52
|
+
weight = weight + excluded.weight;"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
eagle_graph_query_neighbors() {
|
|
56
|
+
local node_id; node_id=$(eagle_sql_int "$1")
|
|
57
|
+
local direction="${2:-out}" # 'out', 'in', or 'both'
|
|
58
|
+
|
|
59
|
+
if [ "$direction" = "out" ]; then
|
|
60
|
+
eagle_db "SELECT e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
|
|
61
|
+
FROM graph_edges e
|
|
62
|
+
JOIN graph_nodes n ON e.target_node_id = n.id
|
|
63
|
+
WHERE e.source_node_id = $node_id
|
|
64
|
+
ORDER BY e.weight DESC, n.node_name;"
|
|
65
|
+
elif [ "$direction" = "in" ]; then
|
|
66
|
+
eagle_db "SELECT e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
|
|
67
|
+
FROM graph_edges e
|
|
68
|
+
JOIN graph_nodes n ON e.source_node_id = n.id
|
|
69
|
+
WHERE e.target_node_id = $node_id
|
|
70
|
+
ORDER BY e.weight DESC, n.node_name;"
|
|
71
|
+
else
|
|
72
|
+
eagle_db "SELECT 'out' as dir, e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
|
|
73
|
+
FROM graph_edges e
|
|
74
|
+
JOIN graph_nodes n ON e.target_node_id = n.id
|
|
75
|
+
WHERE e.source_node_id = $node_id
|
|
76
|
+
UNION ALL
|
|
77
|
+
SELECT 'in' as dir, e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
|
|
78
|
+
FROM graph_edges e
|
|
79
|
+
JOIN graph_nodes n ON e.source_node_id = n.id
|
|
80
|
+
WHERE e.target_node_id = $node_id
|
|
81
|
+
ORDER BY weight DESC;"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
eagle_graph_search() {
|
|
86
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
87
|
+
local query; query=$(eagle_fts_sanitize "$2")
|
|
88
|
+
local type_filter="${3:-}"
|
|
89
|
+
|
|
90
|
+
local sql
|
|
91
|
+
if [ -n "$type_filter" ]; then
|
|
92
|
+
local t_esc; t_esc=$(eagle_sql_escape "$type_filter")
|
|
93
|
+
sql="SELECT n.id, n.node_type, n.node_name, n.node_value, n.source_path
|
|
94
|
+
FROM graph_nodes n
|
|
95
|
+
JOIN graph_nodes_fts f ON f.rowid = n.id
|
|
96
|
+
WHERE n.project = '$project'
|
|
97
|
+
AND n.node_type = '$t_esc'
|
|
98
|
+
AND graph_nodes_fts MATCH '$query'
|
|
99
|
+
ORDER BY rank;"
|
|
100
|
+
else
|
|
101
|
+
sql="SELECT n.id, n.node_type, n.node_name, n.node_value, n.source_path
|
|
102
|
+
FROM graph_nodes n
|
|
103
|
+
JOIN graph_nodes_fts f ON f.rowid = n.id
|
|
104
|
+
WHERE n.project = '$project'
|
|
105
|
+
AND graph_nodes_fts MATCH '$query'
|
|
106
|
+
ORDER BY rank;"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
eagle_db "$sql"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
eagle_graph_get_node_by_id() {
|
|
113
|
+
local node_id; node_id=$(eagle_sql_int "$1")
|
|
114
|
+
eagle_db "SELECT id, node_type, node_name, node_value, source_path, created_at, updated_at
|
|
115
|
+
FROM graph_nodes
|
|
116
|
+
WHERE id = $node_id
|
|
117
|
+
LIMIT 1;"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
eagle_graph_delete_node() {
|
|
121
|
+
local node_id; node_id=$(eagle_sql_int "$1")
|
|
122
|
+
eagle_db "DELETE FROM graph_nodes WHERE id = $node_id;"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
eagle_graph_prune_orphans() {
|
|
126
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
127
|
+
# Delete file nodes that no longer exist on disk
|
|
128
|
+
local result
|
|
129
|
+
result=$(eagle_db "SELECT id, node_name FROM graph_nodes WHERE project = '$project' AND node_type = 'file';")
|
|
130
|
+
[ -z "$result" ] && return 0
|
|
131
|
+
|
|
132
|
+
while IFS='|' read -r nid nname; do
|
|
133
|
+
[ -z "$nid" ] && continue
|
|
134
|
+
if [ ! -f "$nname" ]; then
|
|
135
|
+
eagle_graph_delete_node "$nid"
|
|
136
|
+
fi
|
|
137
|
+
done <<< "$result"
|
|
138
|
+
}
|
package/lib/db.sh
CHANGED
package/package.json
CHANGED
package/scripts/curate.sh
CHANGED
|
@@ -25,12 +25,32 @@ DRY_RUN=0
|
|
|
25
25
|
FULL=0
|
|
26
26
|
project=""
|
|
27
27
|
|
|
28
|
+
show_help() {
|
|
29
|
+
cat <<EOF
|
|
30
|
+
Usage: eagle-mem curate [options]
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
-h, --help Show this help message and exit
|
|
34
|
+
--dry-run Analyze gotchas, decisions, features, and memories without saving to db
|
|
35
|
+
--full Run full session compression and historical curation (slower)
|
|
36
|
+
-p, --project <dir> Target project directory (defaults to auto-detected cwd)
|
|
37
|
+
EOF
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
while [ $# -gt 0 ]; do
|
|
29
41
|
case "$1" in
|
|
42
|
+
-h|--help)
|
|
43
|
+
show_help
|
|
44
|
+
exit 0
|
|
45
|
+
;;
|
|
30
46
|
--dry-run) DRY_RUN=1; shift ;;
|
|
31
47
|
--full) FULL=1; shift ;;
|
|
32
48
|
-p|--project) project="$2"; shift 2 ;;
|
|
33
|
-
*)
|
|
49
|
+
*)
|
|
50
|
+
echo "Unknown option: $1" >&2
|
|
51
|
+
echo "Run with -h or --help for usage details." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
;;
|
|
34
54
|
esac
|
|
35
55
|
done
|
|
36
56
|
|
|
@@ -78,7 +98,7 @@ Only promote gotchas that are:
|
|
|
78
98
|
|
|
79
99
|
If none qualify, output: NONE"
|
|
80
100
|
|
|
81
|
-
gotcha_result=$(eagle_llm_call "$gotcha_prompt" "You analyze software development patterns. Be concise. Only output PROMOTE lines or NONE." 512)
|
|
101
|
+
gotcha_result=$(eagle_llm_call "$gotcha_prompt" "You analyze software development patterns. Be concise. Only output PROMOTE lines or NONE." 512 || true)
|
|
82
102
|
|
|
83
103
|
if [ -n "$gotcha_result" ] && ! echo "$gotcha_result" | grep -q "^NONE$"; then
|
|
84
104
|
promoted=0
|
|
@@ -126,7 +146,7 @@ SUPERSEDED: <old decision> → <new decision> | file: <affected file if known>
|
|
|
126
146
|
|
|
127
147
|
If none are superseded, output: NONE"
|
|
128
148
|
|
|
129
|
-
decision_result=$(eagle_llm_call "$decision_prompt" "You detect contradicting software decisions. Be precise." 512)
|
|
149
|
+
decision_result=$(eagle_llm_call "$decision_prompt" "You detect contradicting software decisions. Be precise." 512 || true)
|
|
130
150
|
|
|
131
151
|
if [ -n "$decision_result" ] && ! echo "$decision_result" | grep -q "^NONE$"; then
|
|
132
152
|
superseded=0
|
|
@@ -208,7 +228,7 @@ Where:
|
|
|
208
228
|
|
|
209
229
|
If no rules needed, output: NONE"
|
|
210
230
|
|
|
211
|
-
cmd_result=$(eagle_llm_call "$cmd_prompt" "You optimize CLI output for AI assistants. Be conservative — only suggest rules for genuinely noisy commands." 512)
|
|
231
|
+
cmd_result=$(eagle_llm_call "$cmd_prompt" "You optimize CLI output for AI assistants. Be conservative — only suggest rules for genuinely noisy commands." 512 || true)
|
|
212
232
|
|
|
213
233
|
if [ -n "$cmd_result" ] && ! echo "$cmd_result" | grep -q "^NONE$"; then
|
|
214
234
|
rules_count=0
|
|
@@ -305,7 +325,7 @@ Rules:
|
|
|
305
325
|
- Don't re-discover existing features
|
|
306
326
|
- If no new features found, output: NONE"
|
|
307
327
|
|
|
308
|
-
feature_result=$(eagle_llm_call "$feature_prompt" "You identify software features from development session data. Be specific and evidence-based." 512)
|
|
328
|
+
feature_result=$(eagle_llm_call "$feature_prompt" "You identify software features from development session data. Be specific and evidence-based." 512 || true)
|
|
309
329
|
|
|
310
330
|
if [ -n "$feature_result" ] && ! echo "$feature_result" | grep -q "^NONE$"; then
|
|
311
331
|
features_count=0
|
|
@@ -428,7 +448,7 @@ if [ -n "$co_edit_data" ]; then
|
|
|
428
448
|
|
|
429
449
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
430
450
|
printf '%s\n' "$co_map_output" | while IFS='|' read -r f partners; do
|
|
431
|
-
[ -z "$f" ]
|
|
451
|
+
if [ -z "$f" ]; then continue; fi
|
|
432
452
|
eagle_info " $(basename "$f") → $partners"
|
|
433
453
|
done
|
|
434
454
|
else
|
|
@@ -436,7 +456,7 @@ if [ -n "$co_edit_data" ]; then
|
|
|
436
456
|
echo "BEGIN;"
|
|
437
457
|
echo "DELETE FROM file_hints WHERE project = '$(eagle_sql_escape "$project")' AND hint_type = 'co_edit';"
|
|
438
458
|
printf '%s\n' "$co_map_output" | while IFS='|' read -r f partners; do
|
|
439
|
-
[ -z "$f" ]
|
|
459
|
+
if [ -z "$f" ]; then continue; fi
|
|
440
460
|
local_f=$(eagle_sql_escape "$f")
|
|
441
461
|
local_v=$(eagle_sql_escape "$partners")
|
|
442
462
|
echo "INSERT INTO file_hints (project, hint_type, file_path, hint_value) VALUES ('$(eagle_sql_escape "$project")', 'co_edit', '$local_f', '$local_v') ON CONFLICT(project, hint_type, file_path) DO UPDATE SET hint_value = excluded.hint_value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
@@ -492,7 +512,7 @@ if [ -n "$hot_file_data" ]; then
|
|
|
492
512
|
hot_count=0
|
|
493
513
|
|
|
494
514
|
while IFS='|' read -r hf_path hf_reads hf_sessions hf_rps; do
|
|
495
|
-
[ -z "$hf_path" ]
|
|
515
|
+
if [ -z "$hf_path" ]; then continue; fi
|
|
496
516
|
if [ -n "$hot_files" ]; then
|
|
497
517
|
hot_files+=","
|
|
498
518
|
fi
|
|
@@ -521,7 +541,138 @@ else
|
|
|
521
541
|
eagle_dim " Not enough session data for hot file detection (need 3+ sessions, have ${total_sessions:-0})"
|
|
522
542
|
fi
|
|
523
543
|
|
|
524
|
-
# ─── 7.
|
|
544
|
+
# ─── 7. Knowledge Graph Wiring & Dream Cycle (Consolidation) ───
|
|
545
|
+
|
|
546
|
+
eagle_info "Executing Dream Cycle (Knowledge Graph & Memory Consolidation)..."
|
|
547
|
+
|
|
548
|
+
# 7.1 Wire co-edit edges in the graph
|
|
549
|
+
if [ -n "$co_edit_data" ]; then
|
|
550
|
+
co_wire_count=0
|
|
551
|
+
while IFS='|' read -r f1 f2 co_sessions; do
|
|
552
|
+
if [ -z "$f1" ] || [ -z "$f2" ]; then continue; fi
|
|
553
|
+
f1_id=$(eagle_graph_get_node_id "$project" "file" "$f1")
|
|
554
|
+
f2_id=$(eagle_graph_get_node_id "$project" "file" "$f2")
|
|
555
|
+
if [ -n "$f1_id" ] && [ -n "$f2_id" ]; then
|
|
556
|
+
if [ "$DRY_RUN" -eq 0 ]; then
|
|
557
|
+
eagle_graph_add_edge "$project" "$f1_id" "$f2_id" "co_edited" "$co_sessions"
|
|
558
|
+
fi
|
|
559
|
+
co_wire_count=$((co_wire_count + 1))
|
|
560
|
+
fi
|
|
561
|
+
done <<< "$co_edit_data"
|
|
562
|
+
eagle_ok "Wired $co_wire_count co-edited file edges"
|
|
563
|
+
fi
|
|
564
|
+
|
|
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;")
|
|
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"
|
|
601
|
+
eagle_ok "Wired $session_wire_count recent session nodes and edges"
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
# 7.3 Offline Memory Consolidation (Compiled Truth vs Evidence)
|
|
605
|
+
active_memories=$(eagle_db "SELECT memory_name, memory_type, description, content FROM agent_memories WHERE project = '$p_esc';")
|
|
606
|
+
if [ -n "$active_memories" ]; then
|
|
607
|
+
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.
|
|
608
|
+
|
|
609
|
+
MEMORIES:
|
|
610
|
+
$active_memories
|
|
611
|
+
|
|
612
|
+
For any memories that should be consolidated, merge them into a single 'Compiled Truth' summary.
|
|
613
|
+
The consolidated memory MUST be formatted exactly as:
|
|
614
|
+
--- Compiled Truth ---
|
|
615
|
+
<A structured, clear, up-to-date summary of the topic, gotten by merging the duplicate/overlapping memories. Keep it extremely precise.>
|
|
616
|
+
|
|
617
|
+
--- Evidence Trail ---
|
|
618
|
+
- <Original memory title 1>: <brief original description or timestamp>
|
|
619
|
+
- <Original memory title 2>: <brief original description or timestamp>
|
|
620
|
+
|
|
621
|
+
Format your output as a series of instructions:
|
|
622
|
+
CONSOLIDATE: <original memory name 1>, <original memory name 2> -> <new consolidated memory name> | description: <new description> | value: <the merged compiled truth + evidence content>
|
|
623
|
+
|
|
624
|
+
If no memories need consolidation, output: NONE"
|
|
625
|
+
|
|
626
|
+
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)
|
|
627
|
+
|
|
628
|
+
if [ -n "$consolidation_result" ] && ! echo "$consolidation_result" | grep -q "^NONE$"; then
|
|
629
|
+
cons_count=0
|
|
630
|
+
while IFS= read -r line; do
|
|
631
|
+
case "$line" in
|
|
632
|
+
CONSOLIDATE:*)
|
|
633
|
+
cons_data=$(echo "$line" | sed 's/^CONSOLIDATE:[[:space:]]*//')
|
|
634
|
+
|
|
635
|
+
# Parse matching: original_names -> new_name | description: desc | value: val
|
|
636
|
+
names_part=$(echo "$cons_data" | cut -d'-' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
637
|
+
rest_part=$(echo "$cons_data" | cut -d'>' -f2-)
|
|
638
|
+
new_name=$(echo "$rest_part" | cut -d'|' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
639
|
+
|
|
640
|
+
desc_part=$(echo "$rest_part" | grep -oE "description:[[:space:]]*[^|]+" | sed 's/description:[[:space:]]*//')
|
|
641
|
+
val_part=$(echo "$rest_part" | grep -oE "value:[[:space:]]*.+" | sed 's/value:[[:space:]]*//')
|
|
642
|
+
|
|
643
|
+
if [ -z "$new_name" ] || [ -z "$names_part" ]; then continue; fi
|
|
644
|
+
|
|
645
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
646
|
+
eagle_info " Would consolidate: $names_part → $new_name"
|
|
647
|
+
else
|
|
648
|
+
# 1. Add new consolidated memory node
|
|
649
|
+
eagle_graph_add_node "$project" "memory" "$new_name" "$val_part" ""
|
|
650
|
+
new_node_id=$(eagle_graph_get_node_id "$project" "memory" "$new_name")
|
|
651
|
+
|
|
652
|
+
# 2. Wire supersedes edges from new node to old nodes, and mark old nodes as inactive/superseded
|
|
653
|
+
IFS=',' read -ra name_arr <<< "$names_part"
|
|
654
|
+
for old_n in "${name_arr[@]}"; do
|
|
655
|
+
old_n=$(echo "$old_n" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
656
|
+
if [ -z "$old_n" ]; then continue; fi
|
|
657
|
+
old_node_id=$(eagle_graph_get_node_id "$project" "memory" "$old_n")
|
|
658
|
+
if [ -n "$old_node_id" ] && [ -n "$new_node_id" ]; then
|
|
659
|
+
eagle_graph_add_edge "$project" "$new_node_id" "$old_node_id" "supersedes" 1.0
|
|
660
|
+
fi
|
|
661
|
+
done
|
|
662
|
+
cons_count=$((cons_count + 1))
|
|
663
|
+
fi
|
|
664
|
+
;;
|
|
665
|
+
esac
|
|
666
|
+
done <<< "$consolidation_result"
|
|
667
|
+
eagle_ok "Consolidated $cons_count sets of overlapping agent memories"
|
|
668
|
+
else
|
|
669
|
+
eagle_ok "Agent memories are fully consolidated and up to date"
|
|
670
|
+
fi
|
|
671
|
+
else
|
|
672
|
+
eagle_dim " No active agent memories to consolidate"
|
|
673
|
+
fi
|
|
674
|
+
|
|
675
|
+
# ─── 8. Session compression (--full only) ─────────────────
|
|
525
676
|
|
|
526
677
|
if [ "$FULL" -eq 1 ]; then
|
|
527
678
|
eagle_info "Compressing old sessions..."
|
package/scripts/index.sh
CHANGED
|
@@ -178,6 +178,50 @@ COMMIT;"
|
|
|
178
178
|
|
|
179
179
|
eagle_db_pipe <<< "$txn_sql"
|
|
180
180
|
|
|
181
|
+
# Static syntax relation extraction & graph wiring
|
|
182
|
+
# Wire node & edge static parser
|
|
183
|
+
file_node_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$file")
|
|
184
|
+
if [ -n "$file_node_id" ]; then
|
|
185
|
+
# 1. Parse function/class declarations
|
|
186
|
+
# e.g., "def name", "class name", "fn name", "function name", "func name"
|
|
187
|
+
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
|
+
if [ -n "$declarations" ]; then
|
|
189
|
+
while IFS=':' read -r dtype dname; do
|
|
190
|
+
[ -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")
|
|
193
|
+
if [ -n "$decl_node_id" ]; then
|
|
194
|
+
eagle_graph_add_edge "$PROJECT" "$file_node_id" "$decl_node_id" "declares" 1.0
|
|
195
|
+
fi
|
|
196
|
+
done <<< "$declarations"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# 2. Parse local relative imports/requires/sources
|
|
200
|
+
# Matches paths starting with dot (./ or ../) or sourcing .sh files
|
|
201
|
+
local_imports=$(grep -oE "['\"](\.[^'\"]+)['\"]" "$full_path" 2>/dev/null | tr -d "'\"" || true)
|
|
202
|
+
# Also grab shell source files
|
|
203
|
+
shell_sources=$(grep -E "^[[:space:]]*(\.|source) " "$full_path" 2>/dev/null | sed -E "s/^[[:space:]]*(\.|source)[[:space:]]+(.*)/\\2/" || true)
|
|
204
|
+
|
|
205
|
+
all_refs=$(printf "%s\n%s\n" "$local_imports" "$shell_sources" | sort -u)
|
|
206
|
+
if [ -n "$all_refs" ]; then
|
|
207
|
+
while IFS= read -r ref; do
|
|
208
|
+
[ -z "$ref" ] && continue
|
|
209
|
+
# Clean up path variables in shell sources (e.g. $_eagle_db_dir/db-core.sh -> db-core.sh)
|
|
210
|
+
ref_clean=$(echo "$ref" | sed -E 's/.*\///; s/\.sh$//; s/\.js$//; s/\.ts$//')
|
|
211
|
+
[ -z "$ref_clean" ] && continue
|
|
212
|
+
|
|
213
|
+
# Check if there is a known file node in our graph that matches this basename or path
|
|
214
|
+
matched_file=$(eagle_db "SELECT node_name FROM graph_nodes WHERE project = '$project_sql' AND node_type = 'file' AND (node_name LIKE '%/$ref_clean%' OR node_name = '$ref_clean') LIMIT 1;")
|
|
215
|
+
if [ -n "$matched_file" ]; then
|
|
216
|
+
target_file_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$matched_file")
|
|
217
|
+
if [ -n "$target_file_id" ]; then
|
|
218
|
+
eagle_graph_add_edge "$PROJECT" "$file_node_id" "$target_file_id" "imports" 1.0
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
done <<< "$all_refs"
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
|
|
181
225
|
file_count=$((file_count + 1))
|
|
182
226
|
|
|
183
227
|
if [ $((file_count % 10)) -eq 0 ]; then
|