agent-working-memory 0.3.0
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/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +388 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/decay.d.ts +36 -0
- package/dist/core/decay.d.ts.map +1 -0
- package/dist/core/decay.js +38 -0
- package/dist/core/decay.js.map +1 -0
- package/dist/core/embeddings.d.ts +33 -0
- package/dist/core/embeddings.d.ts.map +1 -0
- package/dist/core/embeddings.js +76 -0
- package/dist/core/embeddings.js.map +1 -0
- package/dist/core/hebbian.d.ts +38 -0
- package/dist/core/hebbian.d.ts.map +1 -0
- package/dist/core/hebbian.js +74 -0
- package/dist/core/hebbian.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/query-expander.d.ts +24 -0
- package/dist/core/query-expander.d.ts.map +1 -0
- package/dist/core/query-expander.js +58 -0
- package/dist/core/query-expander.js.map +1 -0
- package/dist/core/reranker.d.ts +25 -0
- package/dist/core/reranker.d.ts.map +1 -0
- package/dist/core/reranker.js +75 -0
- package/dist/core/reranker.js.map +1 -0
- package/dist/core/salience.d.ts +30 -0
- package/dist/core/salience.d.ts.map +1 -0
- package/dist/core/salience.js +81 -0
- package/dist/core/salience.js.map +1 -0
- package/dist/engine/activation.d.ts +38 -0
- package/dist/engine/activation.d.ts.map +1 -0
- package/dist/engine/activation.js +516 -0
- package/dist/engine/activation.js.map +1 -0
- package/dist/engine/connections.d.ts +31 -0
- package/dist/engine/connections.d.ts.map +1 -0
- package/dist/engine/connections.js +74 -0
- package/dist/engine/connections.js.map +1 -0
- package/dist/engine/consolidation-scheduler.d.ts +31 -0
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -0
- package/dist/engine/consolidation-scheduler.js +115 -0
- package/dist/engine/consolidation-scheduler.js.map +1 -0
- package/dist/engine/consolidation.d.ts +62 -0
- package/dist/engine/consolidation.d.ts.map +1 -0
- package/dist/engine/consolidation.js +368 -0
- package/dist/engine/consolidation.js.map +1 -0
- package/dist/engine/eval.d.ts +22 -0
- package/dist/engine/eval.d.ts.map +1 -0
- package/dist/engine/eval.js +79 -0
- package/dist/engine/eval.js.map +1 -0
- package/dist/engine/eviction.d.ts +29 -0
- package/dist/engine/eviction.d.ts.map +1 -0
- package/dist/engine/eviction.js +86 -0
- package/dist/engine/eviction.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/retraction.d.ts +32 -0
- package/dist/engine/retraction.d.ts.map +1 -0
- package/dist/engine/retraction.js +77 -0
- package/dist/engine/retraction.js.map +1 -0
- package/dist/engine/staging.d.ts +33 -0
- package/dist/engine/staging.d.ts.map +1 -0
- package/dist/engine/staging.js +63 -0
- package/dist/engine/staging.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +532 -0
- package/dist/mcp.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/sqlite.d.ts +116 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +750 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/types/agent.d.ts +30 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +23 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/checkpoint.d.ts +50 -0
- package/dist/types/checkpoint.d.ts.map +1 -0
- package/dist/types/checkpoint.js +8 -0
- package/dist/types/checkpoint.js.map +1 -0
- package/dist/types/engram.d.ts +165 -0
- package/dist/types/engram.d.ts.map +1 -0
- package/dist/types/engram.js +8 -0
- package/dist/types/engram.js.map +1 -0
- package/dist/types/eval.d.ts +84 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +11 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +55 -0
- package/src/api/index.ts +1 -0
- package/src/api/routes.ts +528 -0
- package/src/cli.ts +260 -0
- package/src/core/decay.ts +61 -0
- package/src/core/embeddings.ts +82 -0
- package/src/core/hebbian.ts +91 -0
- package/src/core/index.ts +3 -0
- package/src/core/query-expander.ts +64 -0
- package/src/core/reranker.ts +99 -0
- package/src/core/salience.ts +95 -0
- package/src/engine/activation.ts +577 -0
- package/src/engine/connections.ts +101 -0
- package/src/engine/consolidation-scheduler.ts +123 -0
- package/src/engine/consolidation.ts +443 -0
- package/src/engine/eval.ts +100 -0
- package/src/engine/eviction.ts +99 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/retraction.ts +98 -0
- package/src/engine/staging.ts +72 -0
- package/src/index.ts +100 -0
- package/src/mcp.ts +635 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/sqlite.ts +893 -0
- package/src/types/agent.ts +65 -0
- package/src/types/checkpoint.ts +44 -0
- package/src/types/engram.ts +194 -0
- package/src/types/eval.ts +98 -0
- package/src/types/index.ts +4 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite storage layer — persistence for engrams, associations, and eval events.
|
|
3
|
+
*
|
|
4
|
+
* Uses better-sqlite3 for synchronous, fast, embedded storage.
|
|
5
|
+
* FTS5 provides BM25 full-text search for the activation pipeline.
|
|
6
|
+
*/
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
const DEFAULT_SALIENCE_FEATURES = {
|
|
10
|
+
surprise: 0, decisionMade: false, causalDepth: 0, resolutionEffort: 0, eventType: 'observation',
|
|
11
|
+
};
|
|
12
|
+
export class EngramStore {
|
|
13
|
+
db;
|
|
14
|
+
constructor(dbPath = 'memory.db') {
|
|
15
|
+
this.db = new Database(dbPath);
|
|
16
|
+
this.db.pragma('journal_mode = WAL');
|
|
17
|
+
this.db.pragma('foreign_keys = ON');
|
|
18
|
+
this.init();
|
|
19
|
+
}
|
|
20
|
+
init() {
|
|
21
|
+
this.db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS engrams (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
agent_id TEXT NOT NULL,
|
|
25
|
+
concept TEXT NOT NULL,
|
|
26
|
+
content TEXT NOT NULL,
|
|
27
|
+
embedding BLOB,
|
|
28
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
29
|
+
salience REAL NOT NULL DEFAULT 0.5,
|
|
30
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
last_accessed TEXT NOT NULL,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
salience_features TEXT NOT NULL DEFAULT '{}',
|
|
34
|
+
reason_codes TEXT NOT NULL DEFAULT '[]',
|
|
35
|
+
stage TEXT NOT NULL DEFAULT 'active',
|
|
36
|
+
ttl INTEGER,
|
|
37
|
+
retracted INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
retracted_by TEXT,
|
|
39
|
+
retracted_at TEXT,
|
|
40
|
+
tags TEXT NOT NULL DEFAULT '[]'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_engrams_agent ON engrams(agent_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_engrams_stage ON engrams(agent_id, stage);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_engrams_concept ON engrams(concept);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_engrams_retracted ON engrams(agent_id, retracted);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS associations (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
|
|
51
|
+
to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
|
|
52
|
+
weight REAL NOT NULL DEFAULT 0.1,
|
|
53
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
54
|
+
type TEXT NOT NULL DEFAULT 'hebbian',
|
|
55
|
+
activation_count INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
created_at TEXT NOT NULL,
|
|
57
|
+
last_activated TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
|
|
62
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
name TEXT NOT NULL,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
config TEXT NOT NULL DEFAULT '{}'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- FTS5 for full-text search (BM25 ranking built in)
|
|
72
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
|
|
73
|
+
concept, content, tags,
|
|
74
|
+
content=engrams,
|
|
75
|
+
content_rowid=rowid
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Triggers to keep FTS in sync
|
|
79
|
+
CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
|
|
80
|
+
INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
|
|
81
|
+
END;
|
|
82
|
+
CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
|
|
83
|
+
INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
|
|
84
|
+
END;
|
|
85
|
+
CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
|
|
86
|
+
INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
|
|
87
|
+
INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
-- Eval event logs
|
|
91
|
+
CREATE TABLE IF NOT EXISTS activation_events (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
agent_id TEXT NOT NULL,
|
|
94
|
+
timestamp TEXT NOT NULL,
|
|
95
|
+
context TEXT NOT NULL,
|
|
96
|
+
results_returned INTEGER NOT NULL,
|
|
97
|
+
top_score REAL,
|
|
98
|
+
latency_ms REAL NOT NULL,
|
|
99
|
+
engram_ids TEXT NOT NULL DEFAULT '[]'
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS staging_events (
|
|
103
|
+
engram_id TEXT NOT NULL,
|
|
104
|
+
agent_id TEXT NOT NULL,
|
|
105
|
+
action TEXT NOT NULL,
|
|
106
|
+
resonance_score REAL,
|
|
107
|
+
timestamp TEXT NOT NULL,
|
|
108
|
+
age_ms INTEGER NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS retrieval_feedback (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
activation_event_id TEXT,
|
|
114
|
+
engram_id TEXT NOT NULL,
|
|
115
|
+
useful INTEGER NOT NULL,
|
|
116
|
+
context TEXT,
|
|
117
|
+
timestamp TEXT NOT NULL
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
agent_id TEXT NOT NULL,
|
|
123
|
+
label TEXT NOT NULL,
|
|
124
|
+
embedding BLOB,
|
|
125
|
+
engram_count INTEGER NOT NULL DEFAULT 0,
|
|
126
|
+
start_time TEXT NOT NULL,
|
|
127
|
+
end_time TEXT NOT NULL,
|
|
128
|
+
created_at TEXT NOT NULL
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_time ON episodes(agent_id, end_time);
|
|
133
|
+
`);
|
|
134
|
+
// Migration: add episode_id column if missing
|
|
135
|
+
try {
|
|
136
|
+
this.db.prepare('SELECT episode_id FROM engrams LIMIT 0').get();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
this.db.exec('ALTER TABLE engrams ADD COLUMN episode_id TEXT');
|
|
140
|
+
}
|
|
141
|
+
// Migration: add task management columns if missing
|
|
142
|
+
try {
|
|
143
|
+
this.db.prepare('SELECT task_status FROM engrams LIMIT 0').get();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
this.db.exec(`
|
|
147
|
+
ALTER TABLE engrams ADD COLUMN task_status TEXT;
|
|
148
|
+
ALTER TABLE engrams ADD COLUMN task_priority TEXT;
|
|
149
|
+
ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
|
|
150
|
+
`);
|
|
151
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_engrams_task ON engrams(agent_id, task_status)');
|
|
152
|
+
}
|
|
153
|
+
// Migration: add conscious_state table for checkpointing
|
|
154
|
+
this.db.exec(`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS conscious_state (
|
|
156
|
+
agent_id TEXT PRIMARY KEY,
|
|
157
|
+
last_write_id TEXT,
|
|
158
|
+
last_recall_context TEXT,
|
|
159
|
+
last_recall_ids TEXT NOT NULL DEFAULT '[]',
|
|
160
|
+
last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
161
|
+
write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
execution_state TEXT,
|
|
164
|
+
checkpoint_at TEXT,
|
|
165
|
+
last_consolidation_at TEXT,
|
|
166
|
+
last_mini_consolidation_at TEXT,
|
|
167
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
168
|
+
)
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
// --- Engram CRUD ---
|
|
172
|
+
createEngram(input) {
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const id = randomUUID();
|
|
175
|
+
const embeddingBlob = input.embedding
|
|
176
|
+
? Buffer.from(new Float32Array(input.embedding).buffer)
|
|
177
|
+
: null;
|
|
178
|
+
this.db.prepare(`
|
|
179
|
+
INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
|
|
180
|
+
access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
|
|
181
|
+
ttl, task_status, task_priority, blocked_by)
|
|
182
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
|
|
183
|
+
`).run(id, input.agentId, input.concept, input.content, embeddingBlob, input.confidence ?? 0.5, input.salience ?? 0.5, now, now, JSON.stringify(input.salienceFeatures ?? DEFAULT_SALIENCE_FEATURES), JSON.stringify(input.reasonCodes ?? []), JSON.stringify(input.tags ?? []), input.episodeId ?? null, input.ttl ?? null, input.taskStatus ?? null, input.taskPriority ?? null, input.blockedBy ?? null);
|
|
184
|
+
return this.getEngram(id);
|
|
185
|
+
}
|
|
186
|
+
getEngram(id) {
|
|
187
|
+
const row = this.db.prepare('SELECT * FROM engrams WHERE id = ?').get(id);
|
|
188
|
+
return row ? this.rowToEngram(row) : null;
|
|
189
|
+
}
|
|
190
|
+
getEngramsByAgent(agentId, stage, includeRetracted = false) {
|
|
191
|
+
let query = 'SELECT * FROM engrams WHERE agent_id = ?';
|
|
192
|
+
const params = [agentId];
|
|
193
|
+
if (stage) {
|
|
194
|
+
query += ' AND stage = ?';
|
|
195
|
+
params.push(stage);
|
|
196
|
+
}
|
|
197
|
+
if (!includeRetracted) {
|
|
198
|
+
query += ' AND retracted = 0';
|
|
199
|
+
}
|
|
200
|
+
return this.db.prepare(query).all(...params).map(r => this.rowToEngram(r));
|
|
201
|
+
}
|
|
202
|
+
touchEngram(id) {
|
|
203
|
+
this.db.prepare(`
|
|
204
|
+
UPDATE engrams SET access_count = access_count + 1, last_accessed = ? WHERE id = ?
|
|
205
|
+
`).run(new Date().toISOString(), id);
|
|
206
|
+
}
|
|
207
|
+
updateStage(id, stage) {
|
|
208
|
+
this.db.prepare('UPDATE engrams SET stage = ? WHERE id = ?').run(stage, id);
|
|
209
|
+
}
|
|
210
|
+
updateConfidence(id, confidence) {
|
|
211
|
+
this.db.prepare('UPDATE engrams SET confidence = ? WHERE id = ?').run(Math.max(0, Math.min(1, confidence)), id);
|
|
212
|
+
}
|
|
213
|
+
updateEmbedding(id, embedding) {
|
|
214
|
+
const blob = Buffer.from(new Float32Array(embedding).buffer);
|
|
215
|
+
this.db.prepare('UPDATE engrams SET embedding = ? WHERE id = ?').run(blob, id);
|
|
216
|
+
}
|
|
217
|
+
retractEngram(id, retractedBy) {
|
|
218
|
+
this.db.prepare(`
|
|
219
|
+
UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
|
|
220
|
+
`).run(retractedBy, new Date().toISOString(), id);
|
|
221
|
+
}
|
|
222
|
+
deleteEngram(id) {
|
|
223
|
+
this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Time warp — shift all timestamps backward by ms milliseconds.
|
|
227
|
+
* Used for testing time-dependent behavior (decay, forgetting).
|
|
228
|
+
* Returns count of records shifted.
|
|
229
|
+
*/
|
|
230
|
+
timeWarp(agentId, ms) {
|
|
231
|
+
let count = 0;
|
|
232
|
+
const shiftSec = Math.round(ms / 1000);
|
|
233
|
+
// Shift engram timestamps
|
|
234
|
+
const r1 = this.db.prepare(`
|
|
235
|
+
UPDATE engrams SET
|
|
236
|
+
created_at = datetime(created_at, '-${shiftSec} seconds'),
|
|
237
|
+
last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
|
|
238
|
+
WHERE agent_id = ?
|
|
239
|
+
`).run(agentId);
|
|
240
|
+
count += r1.changes;
|
|
241
|
+
// Shift association timestamps
|
|
242
|
+
const r2 = this.db.prepare(`
|
|
243
|
+
UPDATE associations SET
|
|
244
|
+
created_at = datetime(created_at, '-${shiftSec} seconds'),
|
|
245
|
+
last_activated = datetime(last_activated, '-${shiftSec} seconds')
|
|
246
|
+
WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
|
|
247
|
+
OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
|
|
248
|
+
`).run(agentId, agentId);
|
|
249
|
+
count += r2.changes;
|
|
250
|
+
return count;
|
|
251
|
+
}
|
|
252
|
+
// --- Full-text search (BM25) ---
|
|
253
|
+
searchBM25(agentId, query, limit = 10) {
|
|
254
|
+
return this.searchBM25WithRank(agentId, query, limit).map(r => r.engram);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* BM25 search returning rank scores alongside engrams.
|
|
258
|
+
* FTS5 rank is negative (lower = better match).
|
|
259
|
+
* We normalize to 0-1 where higher = better.
|
|
260
|
+
*/
|
|
261
|
+
searchBM25WithRank(agentId, query, limit = 10) {
|
|
262
|
+
// Sanitize query for FTS5: quote each word to prevent column name interpretation
|
|
263
|
+
const sanitized = query
|
|
264
|
+
.replace(/[^\w\s]/g, '')
|
|
265
|
+
.split(/\s+/)
|
|
266
|
+
.filter(w => w.length > 1)
|
|
267
|
+
.map(w => `"${w}"`)
|
|
268
|
+
.join(' OR ');
|
|
269
|
+
if (!sanitized)
|
|
270
|
+
return [];
|
|
271
|
+
try {
|
|
272
|
+
const rows = this.db.prepare(`
|
|
273
|
+
SELECT e.*, rank FROM engrams e
|
|
274
|
+
JOIN engrams_fts ON e.rowid = engrams_fts.rowid
|
|
275
|
+
WHERE engrams_fts MATCH ? AND e.agent_id = ? AND e.retracted = 0
|
|
276
|
+
ORDER BY rank
|
|
277
|
+
LIMIT ?
|
|
278
|
+
`).all(sanitized, agentId, limit);
|
|
279
|
+
return rows.map(r => ({
|
|
280
|
+
engram: this.rowToEngram(r),
|
|
281
|
+
// Normalize: rank is negative, more negative = better match.
|
|
282
|
+
// |rank| / (1 + |rank|) gives 0-1 where higher = better.
|
|
283
|
+
bm25Score: Math.abs(r.rank ?? 0) / (1 + Math.abs(r.rank ?? 0)),
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// --- Diagnostic search (deterministic, not cognitive) ---
|
|
291
|
+
search(query) {
|
|
292
|
+
let sql = 'SELECT * FROM engrams WHERE agent_id = ?';
|
|
293
|
+
const params = [query.agentId];
|
|
294
|
+
if (query.text) {
|
|
295
|
+
sql += ' AND (content LIKE ? OR concept LIKE ?)';
|
|
296
|
+
params.push(`%${query.text}%`, `%${query.text}%`);
|
|
297
|
+
}
|
|
298
|
+
if (query.concept) {
|
|
299
|
+
sql += ' AND concept = ?';
|
|
300
|
+
params.push(query.concept);
|
|
301
|
+
}
|
|
302
|
+
if (query.stage) {
|
|
303
|
+
sql += ' AND stage = ?';
|
|
304
|
+
params.push(query.stage);
|
|
305
|
+
}
|
|
306
|
+
if (query.retracted !== undefined) {
|
|
307
|
+
sql += ' AND retracted = ?';
|
|
308
|
+
params.push(query.retracted ? 1 : 0);
|
|
309
|
+
}
|
|
310
|
+
if (query.tags && query.tags.length > 0) {
|
|
311
|
+
for (const tag of query.tags) {
|
|
312
|
+
sql += ' AND tags LIKE ?';
|
|
313
|
+
params.push(`%"${tag}"%`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
sql += ' ORDER BY last_accessed DESC';
|
|
317
|
+
sql += ` LIMIT ? OFFSET ?`;
|
|
318
|
+
params.push(query.limit ?? 50, query.offset ?? 0);
|
|
319
|
+
return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get the most recently created engram for an agent (for temporal adjacency edges).
|
|
323
|
+
*/
|
|
324
|
+
getLatestEngram(agentId, excludeId) {
|
|
325
|
+
let sql = 'SELECT * FROM engrams WHERE agent_id = ? AND retracted = 0';
|
|
326
|
+
const params = [agentId];
|
|
327
|
+
if (excludeId) {
|
|
328
|
+
sql += ' AND id != ?';
|
|
329
|
+
params.push(excludeId);
|
|
330
|
+
}
|
|
331
|
+
sql += ' ORDER BY created_at DESC LIMIT 1';
|
|
332
|
+
const row = this.db.prepare(sql).get(...params);
|
|
333
|
+
return row ? this.rowToEngram(row) : null;
|
|
334
|
+
}
|
|
335
|
+
// --- Task management ---
|
|
336
|
+
updateTaskStatus(id, status) {
|
|
337
|
+
this.db.prepare('UPDATE engrams SET task_status = ? WHERE id = ?').run(status, id);
|
|
338
|
+
}
|
|
339
|
+
updateTaskPriority(id, priority) {
|
|
340
|
+
this.db.prepare('UPDATE engrams SET task_priority = ? WHERE id = ?').run(priority, id);
|
|
341
|
+
}
|
|
342
|
+
updateBlockedBy(id, blockedBy) {
|
|
343
|
+
this.db.prepare('UPDATE engrams SET blocked_by = ?, task_status = ? WHERE id = ?')
|
|
344
|
+
.run(blockedBy, blockedBy ? 'blocked' : 'open', id);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get tasks for an agent, optionally filtered by status.
|
|
348
|
+
* Results ordered by priority (urgent > high > medium > low), then creation date.
|
|
349
|
+
*/
|
|
350
|
+
getTasks(agentId, status) {
|
|
351
|
+
let sql = 'SELECT * FROM engrams WHERE agent_id = ? AND task_status IS NOT NULL AND retracted = 0';
|
|
352
|
+
const params = [agentId];
|
|
353
|
+
if (status) {
|
|
354
|
+
sql += ' AND task_status = ?';
|
|
355
|
+
params.push(status);
|
|
356
|
+
}
|
|
357
|
+
sql += ` ORDER BY
|
|
358
|
+
CASE task_priority
|
|
359
|
+
WHEN 'urgent' THEN 0
|
|
360
|
+
WHEN 'high' THEN 1
|
|
361
|
+
WHEN 'medium' THEN 2
|
|
362
|
+
WHEN 'low' THEN 3
|
|
363
|
+
ELSE 4
|
|
364
|
+
END,
|
|
365
|
+
created_at DESC`;
|
|
366
|
+
return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get the next actionable task — highest priority that's not blocked or done.
|
|
370
|
+
*/
|
|
371
|
+
getNextTask(agentId) {
|
|
372
|
+
const row = this.db.prepare(`
|
|
373
|
+
SELECT * FROM engrams
|
|
374
|
+
WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
|
|
375
|
+
ORDER BY
|
|
376
|
+
CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
|
|
377
|
+
CASE task_priority
|
|
378
|
+
WHEN 'urgent' THEN 0
|
|
379
|
+
WHEN 'high' THEN 1
|
|
380
|
+
WHEN 'medium' THEN 2
|
|
381
|
+
WHEN 'low' THEN 3
|
|
382
|
+
ELSE 4
|
|
383
|
+
END,
|
|
384
|
+
created_at ASC
|
|
385
|
+
LIMIT 1
|
|
386
|
+
`).get(agentId);
|
|
387
|
+
return row ? this.rowToEngram(row) : null;
|
|
388
|
+
}
|
|
389
|
+
// --- Associations ---
|
|
390
|
+
upsertAssociation(fromId, toId, weight, type = 'hebbian', confidence = 0.5) {
|
|
391
|
+
const now = new Date().toISOString();
|
|
392
|
+
const id = randomUUID();
|
|
393
|
+
this.db.prepare(`
|
|
394
|
+
INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
396
|
+
ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
|
|
397
|
+
weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
|
|
398
|
+
`).run(id, fromId, toId, weight, confidence, type, now, now, weight, confidence, now);
|
|
399
|
+
return this.getAssociation(fromId, toId);
|
|
400
|
+
}
|
|
401
|
+
getAssociation(fromId, toId) {
|
|
402
|
+
const row = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? AND to_engram_id = ?').get(fromId, toId);
|
|
403
|
+
return row ? this.rowToAssociation(row) : null;
|
|
404
|
+
}
|
|
405
|
+
getAssociationsFor(engramId) {
|
|
406
|
+
const rows = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? OR to_engram_id = ?').all(engramId, engramId);
|
|
407
|
+
return rows.map(r => this.rowToAssociation(r));
|
|
408
|
+
}
|
|
409
|
+
countAssociationsFor(engramId) {
|
|
410
|
+
const row = this.db.prepare('SELECT COUNT(*) as count FROM associations WHERE from_engram_id = ?').get(engramId);
|
|
411
|
+
return row.count;
|
|
412
|
+
}
|
|
413
|
+
getWeakestAssociation(engramId) {
|
|
414
|
+
const row = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? ORDER BY weight ASC LIMIT 1').get(engramId);
|
|
415
|
+
return row ? this.rowToAssociation(row) : null;
|
|
416
|
+
}
|
|
417
|
+
deleteAssociation(id) {
|
|
418
|
+
this.db.prepare('DELETE FROM associations WHERE id = ?').run(id);
|
|
419
|
+
}
|
|
420
|
+
getAllAssociations(agentId) {
|
|
421
|
+
const rows = this.db.prepare(`
|
|
422
|
+
SELECT a.* FROM associations a
|
|
423
|
+
JOIN engrams e ON a.from_engram_id = e.id
|
|
424
|
+
WHERE e.agent_id = ?
|
|
425
|
+
`).all(agentId);
|
|
426
|
+
return rows.map(r => this.rowToAssociation(r));
|
|
427
|
+
}
|
|
428
|
+
// --- Eviction ---
|
|
429
|
+
getEvictionCandidates(agentId, limit) {
|
|
430
|
+
// Lowest combined score: low salience + low access + low confidence + oldest
|
|
431
|
+
const rows = this.db.prepare(`
|
|
432
|
+
SELECT * FROM engrams
|
|
433
|
+
WHERE agent_id = ? AND stage = 'active' AND retracted = 0
|
|
434
|
+
ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
|
|
435
|
+
(1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
|
|
436
|
+
LIMIT ?
|
|
437
|
+
`).all(agentId, limit);
|
|
438
|
+
return rows.map(r => this.rowToEngram(r));
|
|
439
|
+
}
|
|
440
|
+
getActiveCount(agentId) {
|
|
441
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM engrams WHERE agent_id = ? AND stage = 'active'").get(agentId);
|
|
442
|
+
return row.count;
|
|
443
|
+
}
|
|
444
|
+
getStagingCount(agentId) {
|
|
445
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM engrams WHERE agent_id = ? AND stage = 'staging'").get(agentId);
|
|
446
|
+
return row.count;
|
|
447
|
+
}
|
|
448
|
+
// --- Staging buffer ---
|
|
449
|
+
getExpiredStaging() {
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
const rows = this.db.prepare(`
|
|
452
|
+
SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
|
|
453
|
+
`).all();
|
|
454
|
+
return rows
|
|
455
|
+
.map(r => this.rowToEngram(r))
|
|
456
|
+
.filter(e => e.ttl && (e.createdAt.getTime() + e.ttl) < now);
|
|
457
|
+
}
|
|
458
|
+
// --- Eval event logging ---
|
|
459
|
+
logActivationEvent(event) {
|
|
460
|
+
this.db.prepare(`
|
|
461
|
+
INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
|
|
462
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
463
|
+
`).run(event.id, event.agentId, event.timestamp.toISOString(), event.context, event.resultsReturned, event.topScore, event.latencyMs, JSON.stringify(event.engramIds));
|
|
464
|
+
}
|
|
465
|
+
logStagingEvent(event) {
|
|
466
|
+
this.db.prepare(`
|
|
467
|
+
INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
|
|
468
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
469
|
+
`).run(event.engramId, event.agentId, event.action, event.resonanceScore, event.timestamp.toISOString(), event.ageMs);
|
|
470
|
+
}
|
|
471
|
+
logRetrievalFeedback(activationEventId, engramId, useful, context) {
|
|
472
|
+
this.db.prepare(`
|
|
473
|
+
INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
|
|
474
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
475
|
+
`).run(randomUUID(), activationEventId, engramId, useful ? 1 : 0, context, new Date().toISOString());
|
|
476
|
+
}
|
|
477
|
+
// --- Eval metrics queries ---
|
|
478
|
+
getRetrievalPrecision(agentId, windowHours = 24) {
|
|
479
|
+
const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
|
|
480
|
+
const row = this.db.prepare(`
|
|
481
|
+
SELECT
|
|
482
|
+
COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
|
|
483
|
+
COUNT(*) as total_count
|
|
484
|
+
FROM retrieval_feedback rf
|
|
485
|
+
LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
|
|
486
|
+
JOIN engrams e ON rf.engram_id = e.id
|
|
487
|
+
WHERE e.agent_id = ? AND rf.timestamp > ?
|
|
488
|
+
`).get(agentId, since);
|
|
489
|
+
return row.total_count > 0 ? row.useful_count / row.total_count : 0;
|
|
490
|
+
}
|
|
491
|
+
getStagingMetrics(agentId) {
|
|
492
|
+
const row = this.db.prepare(`
|
|
493
|
+
SELECT
|
|
494
|
+
COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
|
|
495
|
+
COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
|
|
496
|
+
COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
|
|
497
|
+
FROM staging_events WHERE agent_id = ?
|
|
498
|
+
`).get(agentId);
|
|
499
|
+
return { promoted: row.promoted, discarded: row.discarded, expired: row.expired };
|
|
500
|
+
}
|
|
501
|
+
getActivationStats(agentId, windowHours = 24) {
|
|
502
|
+
const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
|
|
503
|
+
const rows = this.db.prepare(`
|
|
504
|
+
SELECT latency_ms FROM activation_events
|
|
505
|
+
WHERE agent_id = ? AND timestamp > ?
|
|
506
|
+
ORDER BY latency_ms ASC
|
|
507
|
+
`).all(agentId, since);
|
|
508
|
+
if (rows.length === 0)
|
|
509
|
+
return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
|
|
510
|
+
const total = rows.reduce((s, r) => s + r.latency_ms, 0);
|
|
511
|
+
const p95Index = Math.min(Math.floor(rows.length * 0.95), rows.length - 1);
|
|
512
|
+
return {
|
|
513
|
+
count: rows.length,
|
|
514
|
+
avgLatencyMs: total / rows.length,
|
|
515
|
+
p95LatencyMs: rows[p95Index].latency_ms,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
getConsolidatedCount(agentId) {
|
|
519
|
+
const row = this.db.prepare(`SELECT COUNT(*) as cnt FROM engrams WHERE agent_id = ? AND stage = 'consolidated'`).get(agentId);
|
|
520
|
+
return row.cnt;
|
|
521
|
+
}
|
|
522
|
+
// --- Helpers ---
|
|
523
|
+
rowToEngram(row) {
|
|
524
|
+
return {
|
|
525
|
+
id: row.id,
|
|
526
|
+
agentId: row.agent_id,
|
|
527
|
+
concept: row.concept,
|
|
528
|
+
content: row.content,
|
|
529
|
+
embedding: row.embedding
|
|
530
|
+
? Array.from(new Float32Array(row.embedding.buffer ?? row.embedding))
|
|
531
|
+
: null,
|
|
532
|
+
confidence: row.confidence,
|
|
533
|
+
salience: row.salience,
|
|
534
|
+
accessCount: row.access_count,
|
|
535
|
+
lastAccessed: new Date(row.last_accessed),
|
|
536
|
+
createdAt: new Date(row.created_at),
|
|
537
|
+
salienceFeatures: JSON.parse(row.salience_features || '{}'),
|
|
538
|
+
reasonCodes: JSON.parse(row.reason_codes || '[]'),
|
|
539
|
+
stage: row.stage,
|
|
540
|
+
ttl: row.ttl,
|
|
541
|
+
retracted: !!row.retracted,
|
|
542
|
+
retractedBy: row.retracted_by,
|
|
543
|
+
retractedAt: row.retracted_at ? new Date(row.retracted_at) : null,
|
|
544
|
+
tags: JSON.parse(row.tags),
|
|
545
|
+
episodeId: row.episode_id ?? null,
|
|
546
|
+
taskStatus: row.task_status ?? null,
|
|
547
|
+
taskPriority: row.task_priority ?? null,
|
|
548
|
+
blockedBy: row.blocked_by ?? null,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
rowToAssociation(row) {
|
|
552
|
+
return {
|
|
553
|
+
id: row.id,
|
|
554
|
+
fromEngramId: row.from_engram_id,
|
|
555
|
+
toEngramId: row.to_engram_id,
|
|
556
|
+
weight: row.weight,
|
|
557
|
+
confidence: row.confidence ?? 0.5,
|
|
558
|
+
type: row.type,
|
|
559
|
+
activationCount: row.activation_count ?? 0,
|
|
560
|
+
createdAt: new Date(row.created_at),
|
|
561
|
+
lastActivated: new Date(row.last_activated),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
// --- Episodes ---
|
|
565
|
+
createEpisode(input) {
|
|
566
|
+
const now = new Date().toISOString();
|
|
567
|
+
const id = randomUUID();
|
|
568
|
+
const embeddingBlob = input.embedding
|
|
569
|
+
? Buffer.from(new Float32Array(input.embedding).buffer)
|
|
570
|
+
: null;
|
|
571
|
+
this.db.prepare(`
|
|
572
|
+
INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
|
|
573
|
+
VALUES (?, ?, ?, ?, 0, ?, ?, ?)
|
|
574
|
+
`).run(id, input.agentId, input.label, embeddingBlob, now, now, now);
|
|
575
|
+
return this.getEpisode(id);
|
|
576
|
+
}
|
|
577
|
+
getEpisode(id) {
|
|
578
|
+
const row = this.db.prepare('SELECT * FROM episodes WHERE id = ?').get(id);
|
|
579
|
+
return row ? this.rowToEpisode(row) : null;
|
|
580
|
+
}
|
|
581
|
+
getEpisodesByAgent(agentId) {
|
|
582
|
+
const rows = this.db.prepare('SELECT * FROM episodes WHERE agent_id = ? ORDER BY end_time DESC').all(agentId);
|
|
583
|
+
return rows.map(r => this.rowToEpisode(r));
|
|
584
|
+
}
|
|
585
|
+
getActiveEpisode(agentId, windowMs = 3600_000) {
|
|
586
|
+
// Find most recent episode that ended within the time window
|
|
587
|
+
const cutoff = new Date(Date.now() - windowMs).toISOString();
|
|
588
|
+
const row = this.db.prepare(`
|
|
589
|
+
SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
|
|
590
|
+
ORDER BY end_time DESC LIMIT 1
|
|
591
|
+
`).get(agentId, cutoff);
|
|
592
|
+
return row ? this.rowToEpisode(row) : null;
|
|
593
|
+
}
|
|
594
|
+
addEngramToEpisode(engramId, episodeId) {
|
|
595
|
+
this.db.prepare('UPDATE engrams SET episode_id = ? WHERE id = ?').run(episodeId, engramId);
|
|
596
|
+
this.db.prepare(`
|
|
597
|
+
UPDATE episodes SET
|
|
598
|
+
engram_count = engram_count + 1,
|
|
599
|
+
end_time = MAX(end_time, ?)
|
|
600
|
+
WHERE id = ?
|
|
601
|
+
`).run(new Date().toISOString(), episodeId);
|
|
602
|
+
}
|
|
603
|
+
getEngramsByEpisode(episodeId) {
|
|
604
|
+
const rows = this.db.prepare('SELECT * FROM engrams WHERE episode_id = ? AND retracted = 0 ORDER BY created_at ASC').all(episodeId);
|
|
605
|
+
return rows.map(r => this.rowToEngram(r));
|
|
606
|
+
}
|
|
607
|
+
updateEpisodeEmbedding(id, embedding) {
|
|
608
|
+
const blob = Buffer.from(new Float32Array(embedding).buffer);
|
|
609
|
+
this.db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?').run(blob, id);
|
|
610
|
+
}
|
|
611
|
+
getEpisodeCount(agentId) {
|
|
612
|
+
const row = this.db.prepare('SELECT COUNT(*) as cnt FROM episodes WHERE agent_id = ?').get(agentId);
|
|
613
|
+
return row.cnt;
|
|
614
|
+
}
|
|
615
|
+
rowToEpisode(row) {
|
|
616
|
+
return {
|
|
617
|
+
id: row.id,
|
|
618
|
+
agentId: row.agent_id,
|
|
619
|
+
label: row.label,
|
|
620
|
+
embedding: row.embedding
|
|
621
|
+
? Array.from(new Float32Array(row.embedding.buffer ?? row.embedding))
|
|
622
|
+
: null,
|
|
623
|
+
engramCount: row.engram_count,
|
|
624
|
+
startTime: new Date(row.start_time),
|
|
625
|
+
endTime: new Date(row.end_time),
|
|
626
|
+
createdAt: new Date(row.created_at),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Find engrams whose tags contain any of the given tag values.
|
|
631
|
+
* Used for entity-bridge retrieval: given entity tags from top results,
|
|
632
|
+
* find other engrams mentioning the same entities.
|
|
633
|
+
*/
|
|
634
|
+
findEngramsByTags(agentId, tags, excludeIds) {
|
|
635
|
+
if (tags.length === 0)
|
|
636
|
+
return [];
|
|
637
|
+
// Build OR conditions for tag matching
|
|
638
|
+
const conditions = tags.map(() => 'tags LIKE ?').join(' OR ');
|
|
639
|
+
const params = [agentId, ...tags.map(t => `%"${t}"%`)];
|
|
640
|
+
let sql = `SELECT * FROM engrams WHERE agent_id = ? AND retracted = 0 AND (${conditions})`;
|
|
641
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
642
|
+
const results = rows.map(r => this.rowToEngram(r));
|
|
643
|
+
if (excludeIds) {
|
|
644
|
+
return results.filter(e => !excludeIds.has(e.id));
|
|
645
|
+
}
|
|
646
|
+
return results;
|
|
647
|
+
}
|
|
648
|
+
// --- Checkpointing ---
|
|
649
|
+
updateAutoCheckpointWrite(agentId, engramId) {
|
|
650
|
+
const now = new Date().toISOString();
|
|
651
|
+
this.db.prepare(`
|
|
652
|
+
INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
|
|
653
|
+
VALUES (?, ?, ?, 1, ?)
|
|
654
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
655
|
+
last_write_id = excluded.last_write_id,
|
|
656
|
+
last_activity_at = excluded.last_activity_at,
|
|
657
|
+
write_count_since_consolidation = write_count_since_consolidation + 1,
|
|
658
|
+
updated_at = excluded.updated_at
|
|
659
|
+
`).run(agentId, engramId, now, now);
|
|
660
|
+
}
|
|
661
|
+
updateAutoCheckpointRecall(agentId, context, engramIds) {
|
|
662
|
+
const now = new Date().toISOString();
|
|
663
|
+
this.db.prepare(`
|
|
664
|
+
INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
|
|
665
|
+
VALUES (?, ?, ?, ?, 1, ?)
|
|
666
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
667
|
+
last_recall_context = excluded.last_recall_context,
|
|
668
|
+
last_recall_ids = excluded.last_recall_ids,
|
|
669
|
+
last_activity_at = excluded.last_activity_at,
|
|
670
|
+
recall_count_since_consolidation = recall_count_since_consolidation + 1,
|
|
671
|
+
updated_at = excluded.updated_at
|
|
672
|
+
`).run(agentId, context, JSON.stringify(engramIds), now, now);
|
|
673
|
+
}
|
|
674
|
+
touchActivity(agentId) {
|
|
675
|
+
const now = new Date().toISOString();
|
|
676
|
+
this.db.prepare(`
|
|
677
|
+
INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
|
|
678
|
+
VALUES (?, ?, ?)
|
|
679
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
680
|
+
last_activity_at = excluded.last_activity_at,
|
|
681
|
+
updated_at = excluded.updated_at
|
|
682
|
+
`).run(agentId, now, now);
|
|
683
|
+
}
|
|
684
|
+
saveCheckpoint(agentId, state) {
|
|
685
|
+
const now = new Date().toISOString();
|
|
686
|
+
this.db.prepare(`
|
|
687
|
+
INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
|
|
688
|
+
VALUES (?, ?, ?, ?, ?)
|
|
689
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
690
|
+
execution_state = excluded.execution_state,
|
|
691
|
+
checkpoint_at = excluded.checkpoint_at,
|
|
692
|
+
last_activity_at = excluded.last_activity_at,
|
|
693
|
+
updated_at = excluded.updated_at
|
|
694
|
+
`).run(agentId, JSON.stringify(state), now, now, now);
|
|
695
|
+
}
|
|
696
|
+
getCheckpoint(agentId) {
|
|
697
|
+
const row = this.db.prepare('SELECT * FROM conscious_state WHERE agent_id = ?').get(agentId);
|
|
698
|
+
if (!row)
|
|
699
|
+
return null;
|
|
700
|
+
return {
|
|
701
|
+
agentId: row.agent_id,
|
|
702
|
+
auto: {
|
|
703
|
+
lastWriteId: row.last_write_id ?? null,
|
|
704
|
+
lastRecallContext: row.last_recall_context ?? null,
|
|
705
|
+
lastRecallIds: JSON.parse(row.last_recall_ids || '[]'),
|
|
706
|
+
lastActivityAt: new Date(row.last_activity_at),
|
|
707
|
+
writeCountSinceConsolidation: row.write_count_since_consolidation,
|
|
708
|
+
recallCountSinceConsolidation: row.recall_count_since_consolidation,
|
|
709
|
+
},
|
|
710
|
+
executionState: row.execution_state ? JSON.parse(row.execution_state) : null,
|
|
711
|
+
checkpointAt: row.checkpoint_at ? new Date(row.checkpoint_at) : null,
|
|
712
|
+
lastConsolidationAt: row.last_consolidation_at ? new Date(row.last_consolidation_at) : null,
|
|
713
|
+
lastMiniConsolidationAt: row.last_mini_consolidation_at ? new Date(row.last_mini_consolidation_at) : null,
|
|
714
|
+
updatedAt: new Date(row.updated_at),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
markConsolidation(agentId, mini) {
|
|
718
|
+
const now = new Date().toISOString();
|
|
719
|
+
if (mini) {
|
|
720
|
+
this.db.prepare(`
|
|
721
|
+
UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
|
|
722
|
+
`).run(now, now, agentId);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
this.db.prepare(`
|
|
726
|
+
UPDATE conscious_state SET
|
|
727
|
+
last_consolidation_at = ?,
|
|
728
|
+
last_mini_consolidation_at = ?,
|
|
729
|
+
write_count_since_consolidation = 0,
|
|
730
|
+
recall_count_since_consolidation = 0,
|
|
731
|
+
updated_at = ?
|
|
732
|
+
WHERE agent_id = ?
|
|
733
|
+
`).run(now, now, now, agentId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
getActiveAgents() {
|
|
737
|
+
const rows = this.db.prepare('SELECT * FROM conscious_state').all();
|
|
738
|
+
return rows.map(row => ({
|
|
739
|
+
agentId: row.agent_id,
|
|
740
|
+
lastActivityAt: new Date(row.last_activity_at),
|
|
741
|
+
writeCount: row.write_count_since_consolidation,
|
|
742
|
+
recallCount: row.recall_count_since_consolidation,
|
|
743
|
+
lastConsolidationAt: row.last_consolidation_at ? new Date(row.last_consolidation_at) : null,
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
close() {
|
|
747
|
+
this.db.close();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
//# sourceMappingURL=sqlite.js.map
|