@smyslenny/agent-memory 3.1.0 → 4.1.0-alpha.1
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 +122 -4
- package/README.md +229 -84
- package/dist/bin/agent-memory.js +2884 -457
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +560 -81
- package/dist/index.js +2642 -366
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2393 -710
- package/dist/mcp/server.js.map +1 -1
- package/docs/README-zh.md +23 -0
- package/docs/architecture.md +239 -0
- package/docs/assets/architecture-diagram.jpg +0 -0
- package/docs/assets/banner.jpg +0 -0
- package/docs/assets/icon.jpg +0 -0
- package/docs/assets/npm-badge.jpg +0 -0
- package/docs/assets/social-preview.jpg +0 -0
- package/docs/design/.next-id +1 -0
- package/docs/design/0015-v4-overhaul.md +631 -0
- package/docs/design/0016-v41-warm-boot-surface-emotion.md +228 -0
- package/docs/integrations/generic.md +293 -0
- package/docs/integrations/openclaw.md +148 -0
- package/docs/migration-v3-v4.md +236 -0
- package/package.json +11 -4
- package/README.en.md +0 -153
package/dist/bin/agent-memory.js
CHANGED
|
@@ -1,97 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// AgentMemory v2 — Sleep-cycle memory for AI agents
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
3
12
|
|
|
4
13
|
// src/core/db.ts
|
|
5
14
|
import Database from "better-sqlite3";
|
|
6
15
|
import { randomUUID } from "crypto";
|
|
7
|
-
var SCHEMA_VERSION = 3;
|
|
8
|
-
var SCHEMA_SQL = `
|
|
9
|
-
-- Memory entries
|
|
10
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
11
|
-
id TEXT PRIMARY KEY,
|
|
12
|
-
content TEXT NOT NULL,
|
|
13
|
-
type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
|
|
14
|
-
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
|
|
15
|
-
emotion_val REAL NOT NULL DEFAULT 0.0,
|
|
16
|
-
vitality REAL NOT NULL DEFAULT 1.0,
|
|
17
|
-
stability REAL NOT NULL DEFAULT 1.0,
|
|
18
|
-
access_count INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
last_accessed TEXT,
|
|
20
|
-
created_at TEXT NOT NULL,
|
|
21
|
-
updated_at TEXT NOT NULL,
|
|
22
|
-
source TEXT,
|
|
23
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
24
|
-
hash TEXT,
|
|
25
|
-
UNIQUE(hash, agent_id)
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
-- URI paths (Content-Path separation, from nocturne)
|
|
29
|
-
CREATE TABLE IF NOT EXISTS paths (
|
|
30
|
-
id TEXT PRIMARY KEY,
|
|
31
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
32
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
33
|
-
uri TEXT NOT NULL,
|
|
34
|
-
alias TEXT,
|
|
35
|
-
domain TEXT NOT NULL,
|
|
36
|
-
created_at TEXT NOT NULL,
|
|
37
|
-
UNIQUE(agent_id, uri)
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
-- Association network (knowledge graph)
|
|
41
|
-
CREATE TABLE IF NOT EXISTS links (
|
|
42
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
43
|
-
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
44
|
-
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
45
|
-
relation TEXT NOT NULL,
|
|
46
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
47
|
-
created_at TEXT NOT NULL,
|
|
48
|
-
PRIMARY KEY (agent_id, source_id, target_id)
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
52
|
-
CREATE TABLE IF NOT EXISTS snapshots (
|
|
53
|
-
id TEXT PRIMARY KEY,
|
|
54
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
55
|
-
content TEXT NOT NULL,
|
|
56
|
-
changed_by TEXT,
|
|
57
|
-
action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
|
|
58
|
-
created_at TEXT NOT NULL
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
-- Full-text search index (BM25)
|
|
62
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
63
|
-
id UNINDEXED,
|
|
64
|
-
content,
|
|
65
|
-
tokenize='unicode61'
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
-- Embeddings (optional semantic layer)
|
|
69
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
70
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
71
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
72
|
-
model TEXT NOT NULL,
|
|
73
|
-
dim INTEGER NOT NULL,
|
|
74
|
-
vector BLOB NOT NULL,
|
|
75
|
-
created_at TEXT NOT NULL,
|
|
76
|
-
updated_at TEXT NOT NULL,
|
|
77
|
-
PRIMARY KEY (agent_id, memory_id, model)
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
-- Schema version tracking
|
|
81
|
-
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
82
|
-
key TEXT PRIMARY KEY,
|
|
83
|
-
value TEXT NOT NULL
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
-- Indexes for common queries
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
90
|
-
CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
91
|
-
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
92
|
-
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
93
|
-
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
94
|
-
`;
|
|
95
16
|
function openDatabase(opts) {
|
|
96
17
|
const db = new Database(opts.path);
|
|
97
18
|
if (opts.walMode !== false) {
|
|
@@ -111,6 +32,7 @@ function openDatabase(opts) {
|
|
|
111
32
|
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
112
33
|
}
|
|
113
34
|
ensureIndexes(db);
|
|
35
|
+
ensureFeedbackEventSchema(db);
|
|
114
36
|
return db;
|
|
115
37
|
}
|
|
116
38
|
function now() {
|
|
@@ -129,6 +51,14 @@ function getSchemaVersion(db) {
|
|
|
129
51
|
return null;
|
|
130
52
|
}
|
|
131
53
|
}
|
|
54
|
+
function tableExists(db, table) {
|
|
55
|
+
try {
|
|
56
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table);
|
|
57
|
+
return Boolean(row?.name);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
132
62
|
function tableHasColumn(db, table, column) {
|
|
133
63
|
try {
|
|
134
64
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -150,6 +80,21 @@ function migrateDatabase(db, from, to) {
|
|
|
150
80
|
v = 3;
|
|
151
81
|
continue;
|
|
152
82
|
}
|
|
83
|
+
if (v === 3) {
|
|
84
|
+
migrateV3ToV4(db);
|
|
85
|
+
v = 4;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (v === 4) {
|
|
89
|
+
migrateV4ToV5(db);
|
|
90
|
+
v = 5;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (v === 5) {
|
|
94
|
+
migrateV5ToV6(db);
|
|
95
|
+
v = 6;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
153
98
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
154
99
|
}
|
|
155
100
|
}
|
|
@@ -231,14 +176,14 @@ function migrateV1ToV2(db) {
|
|
|
231
176
|
function inferSchemaVersion(db) {
|
|
232
177
|
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
233
178
|
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
234
|
-
const hasEmbeddings = (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
179
|
+
const hasEmbeddings = tableExists(db, "embeddings");
|
|
180
|
+
const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
181
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
182
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
183
|
+
const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
|
|
184
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
|
|
185
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
186
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
242
187
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
243
188
|
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
244
189
|
return 1;
|
|
@@ -251,14 +196,40 @@ function ensureIndexes(db) {
|
|
|
251
196
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
252
197
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
253
198
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
199
|
+
if (tableExists(db, "embeddings")) {
|
|
200
|
+
if (tableHasColumn(db, "embeddings", "provider_id")) {
|
|
201
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_provider_status ON embeddings(provider_id, status);");
|
|
202
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory_provider ON embeddings(memory_id, provider_id);");
|
|
203
|
+
} else {
|
|
257
204
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
258
205
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
259
206
|
}
|
|
260
|
-
} catch {
|
|
261
207
|
}
|
|
208
|
+
if (tableExists(db, "maintenance_jobs")) {
|
|
209
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
|
|
210
|
+
}
|
|
211
|
+
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
212
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
213
|
+
}
|
|
214
|
+
if (tableExists(db, "feedback_events")) {
|
|
215
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
216
|
+
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
217
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function ensureFeedbackEventSchema(db) {
|
|
222
|
+
if (!tableExists(db, "feedback_events")) return;
|
|
223
|
+
if (!tableHasColumn(db, "feedback_events", "source")) {
|
|
224
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN source TEXT NOT NULL DEFAULT 'surface';");
|
|
225
|
+
}
|
|
226
|
+
if (!tableHasColumn(db, "feedback_events", "useful")) {
|
|
227
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN useful INTEGER NOT NULL DEFAULT 1;");
|
|
228
|
+
}
|
|
229
|
+
if (!tableHasColumn(db, "feedback_events", "agent_id")) {
|
|
230
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN agent_id TEXT NOT NULL DEFAULT 'default';");
|
|
231
|
+
}
|
|
232
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
262
233
|
}
|
|
263
234
|
function migrateV2ToV3(db) {
|
|
264
235
|
try {
|
|
@@ -285,14 +256,237 @@ function migrateV2ToV3(db) {
|
|
|
285
256
|
throw e;
|
|
286
257
|
}
|
|
287
258
|
}
|
|
259
|
+
function migrateV3ToV4(db) {
|
|
260
|
+
const alreadyMigrated = tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
261
|
+
if (alreadyMigrated) {
|
|
262
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
db.exec("BEGIN");
|
|
267
|
+
const legacyRows = tableExists(db, "embeddings") ? db.prepare(
|
|
268
|
+
`SELECT e.agent_id, e.memory_id, e.model, e.vector, e.created_at, m.hash
|
|
269
|
+
FROM embeddings e
|
|
270
|
+
LEFT JOIN memories m ON m.id = e.memory_id`
|
|
271
|
+
).all() : [];
|
|
272
|
+
db.exec(`
|
|
273
|
+
CREATE TABLE IF NOT EXISTS embeddings_v4 (
|
|
274
|
+
id TEXT PRIMARY KEY,
|
|
275
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
276
|
+
provider_id TEXT NOT NULL,
|
|
277
|
+
vector BLOB,
|
|
278
|
+
content_hash TEXT NOT NULL,
|
|
279
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
280
|
+
created_at TEXT NOT NULL,
|
|
281
|
+
UNIQUE(memory_id, provider_id)
|
|
282
|
+
);
|
|
283
|
+
`);
|
|
284
|
+
const insert = db.prepare(
|
|
285
|
+
`INSERT INTO embeddings_v4 (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
286
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)`
|
|
287
|
+
);
|
|
288
|
+
for (const row of legacyRows) {
|
|
289
|
+
insert.run(newId(), row.memory_id, `legacy:${row.agent_id}:${row.model}`, row.vector, row.hash ?? "", row.created_at);
|
|
290
|
+
}
|
|
291
|
+
if (tableExists(db, "embeddings")) {
|
|
292
|
+
db.exec("DROP TABLE embeddings;");
|
|
293
|
+
}
|
|
294
|
+
db.exec("ALTER TABLE embeddings_v4 RENAME TO embeddings;");
|
|
295
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
296
|
+
db.exec("COMMIT");
|
|
297
|
+
} catch (e) {
|
|
298
|
+
try {
|
|
299
|
+
db.exec("ROLLBACK");
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function migrateV4ToV5(db) {
|
|
306
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
307
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
308
|
+
if (hasMaintenanceJobs && hasFeedbackEvents) {
|
|
309
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
db.exec("BEGIN");
|
|
314
|
+
db.exec(`
|
|
315
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
316
|
+
job_id TEXT PRIMARY KEY,
|
|
317
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
318
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
319
|
+
checkpoint TEXT,
|
|
320
|
+
error TEXT,
|
|
321
|
+
started_at TEXT NOT NULL,
|
|
322
|
+
finished_at TEXT
|
|
323
|
+
);
|
|
324
|
+
`);
|
|
325
|
+
db.exec(`
|
|
326
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
327
|
+
id TEXT PRIMARY KEY,
|
|
328
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
329
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
330
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
331
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
332
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
333
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
334
|
+
created_at TEXT NOT NULL
|
|
335
|
+
);
|
|
336
|
+
`);
|
|
337
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
338
|
+
db.exec("COMMIT");
|
|
339
|
+
} catch (e) {
|
|
340
|
+
try {
|
|
341
|
+
db.exec("ROLLBACK");
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
throw e;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function migrateV5ToV6(db) {
|
|
348
|
+
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
349
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
db.exec("BEGIN");
|
|
354
|
+
db.exec("ALTER TABLE memories ADD COLUMN emotion_tag TEXT;");
|
|
355
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
356
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
|
|
357
|
+
db.exec("COMMIT");
|
|
358
|
+
} catch (e) {
|
|
359
|
+
try {
|
|
360
|
+
db.exec("ROLLBACK");
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
throw e;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
367
|
+
var init_db = __esm({
|
|
368
|
+
"src/core/db.ts"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
SCHEMA_VERSION = 6;
|
|
371
|
+
SCHEMA_SQL = `
|
|
372
|
+
-- Memory entries
|
|
373
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
374
|
+
id TEXT PRIMARY KEY,
|
|
375
|
+
content TEXT NOT NULL,
|
|
376
|
+
type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
|
|
377
|
+
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
|
|
378
|
+
emotion_val REAL NOT NULL DEFAULT 0.0,
|
|
379
|
+
vitality REAL NOT NULL DEFAULT 1.0,
|
|
380
|
+
stability REAL NOT NULL DEFAULT 1.0,
|
|
381
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
last_accessed TEXT,
|
|
383
|
+
created_at TEXT NOT NULL,
|
|
384
|
+
updated_at TEXT NOT NULL,
|
|
385
|
+
source TEXT,
|
|
386
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
387
|
+
hash TEXT,
|
|
388
|
+
emotion_tag TEXT,
|
|
389
|
+
UNIQUE(hash, agent_id)
|
|
390
|
+
);
|
|
288
391
|
|
|
289
|
-
|
|
290
|
-
|
|
392
|
+
-- URI paths (Content-Path separation, from nocturne)
|
|
393
|
+
CREATE TABLE IF NOT EXISTS paths (
|
|
394
|
+
id TEXT PRIMARY KEY,
|
|
395
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
396
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
397
|
+
uri TEXT NOT NULL,
|
|
398
|
+
alias TEXT,
|
|
399
|
+
domain TEXT NOT NULL,
|
|
400
|
+
created_at TEXT NOT NULL,
|
|
401
|
+
UNIQUE(agent_id, uri)
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
-- Association network (knowledge graph)
|
|
405
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
406
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
407
|
+
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
408
|
+
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
409
|
+
relation TEXT NOT NULL,
|
|
410
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
411
|
+
created_at TEXT NOT NULL,
|
|
412
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
416
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
417
|
+
id TEXT PRIMARY KEY,
|
|
418
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
419
|
+
content TEXT NOT NULL,
|
|
420
|
+
changed_by TEXT,
|
|
421
|
+
action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
|
|
422
|
+
created_at TEXT NOT NULL
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
-- Full-text search index (BM25)
|
|
426
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
427
|
+
id UNINDEXED,
|
|
428
|
+
content,
|
|
429
|
+
tokenize='unicode61'
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
-- Embeddings (optional semantic layer)
|
|
433
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
434
|
+
id TEXT PRIMARY KEY,
|
|
435
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
436
|
+
provider_id TEXT NOT NULL,
|
|
437
|
+
vector BLOB,
|
|
438
|
+
content_hash TEXT NOT NULL,
|
|
439
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
440
|
+
created_at TEXT NOT NULL,
|
|
441
|
+
UNIQUE(memory_id, provider_id)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
-- Maintenance jobs (reflect / reindex checkpoints)
|
|
445
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
446
|
+
job_id TEXT PRIMARY KEY,
|
|
447
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
448
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
449
|
+
checkpoint TEXT,
|
|
450
|
+
error TEXT,
|
|
451
|
+
started_at TEXT NOT NULL,
|
|
452
|
+
finished_at TEXT
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
-- Feedback signals (recall/surface usefulness + governance priors)
|
|
456
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
457
|
+
id TEXT PRIMARY KEY,
|
|
458
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
459
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
460
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
461
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
462
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
463
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
464
|
+
created_at TEXT NOT NULL
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
-- Schema version tracking
|
|
468
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
469
|
+
key TEXT PRIMARY KEY,
|
|
470
|
+
value TEXT NOT NULL
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
-- Indexes for common queries
|
|
474
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
475
|
+
CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
|
|
476
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
478
|
+
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
291
486
|
|
|
292
487
|
// src/search/tokenizer.ts
|
|
293
488
|
import { readFileSync } from "fs";
|
|
294
489
|
import { createRequire } from "module";
|
|
295
|
-
var _jieba;
|
|
296
490
|
function getJieba() {
|
|
297
491
|
if (_jieba !== void 0) return _jieba;
|
|
298
492
|
try {
|
|
@@ -306,38 +500,6 @@ function getJieba() {
|
|
|
306
500
|
}
|
|
307
501
|
return _jieba;
|
|
308
502
|
}
|
|
309
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
310
|
-
"\u7684",
|
|
311
|
-
"\u4E86",
|
|
312
|
-
"\u5728",
|
|
313
|
-
"\u662F",
|
|
314
|
-
"\u6211",
|
|
315
|
-
"\u6709",
|
|
316
|
-
"\u548C",
|
|
317
|
-
"\u5C31",
|
|
318
|
-
"\u4E0D",
|
|
319
|
-
"\u4EBA",
|
|
320
|
-
"\u90FD",
|
|
321
|
-
"\u4E00",
|
|
322
|
-
"\u4E2A",
|
|
323
|
-
"\u4E0A",
|
|
324
|
-
"\u4E5F",
|
|
325
|
-
"\u5230",
|
|
326
|
-
"\u4ED6",
|
|
327
|
-
"\u6CA1",
|
|
328
|
-
"\u8FD9",
|
|
329
|
-
"\u8981",
|
|
330
|
-
"\u4F1A",
|
|
331
|
-
"\u5BF9",
|
|
332
|
-
"\u8BF4",
|
|
333
|
-
"\u800C",
|
|
334
|
-
"\u53BB",
|
|
335
|
-
"\u4E4B",
|
|
336
|
-
"\u88AB",
|
|
337
|
-
"\u5979",
|
|
338
|
-
"\u628A",
|
|
339
|
-
"\u90A3"
|
|
340
|
-
]);
|
|
341
503
|
function tokenize(text) {
|
|
342
504
|
const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
|
|
343
505
|
const tokens = [];
|
|
@@ -367,42 +529,433 @@ function tokenizeForIndex(text) {
|
|
|
367
529
|
const tokens = tokenize(text);
|
|
368
530
|
return tokens.join(" ");
|
|
369
531
|
}
|
|
532
|
+
var _jieba, STOPWORDS;
|
|
533
|
+
var init_tokenizer = __esm({
|
|
534
|
+
"src/search/tokenizer.ts"() {
|
|
535
|
+
"use strict";
|
|
536
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
537
|
+
"\u7684",
|
|
538
|
+
"\u4E86",
|
|
539
|
+
"\u5728",
|
|
540
|
+
"\u662F",
|
|
541
|
+
"\u6211",
|
|
542
|
+
"\u6709",
|
|
543
|
+
"\u548C",
|
|
544
|
+
"\u5C31",
|
|
545
|
+
"\u4E0D",
|
|
546
|
+
"\u4EBA",
|
|
547
|
+
"\u90FD",
|
|
548
|
+
"\u4E00",
|
|
549
|
+
"\u4E2A",
|
|
550
|
+
"\u4E0A",
|
|
551
|
+
"\u4E5F",
|
|
552
|
+
"\u5230",
|
|
553
|
+
"\u4ED6",
|
|
554
|
+
"\u6CA1",
|
|
555
|
+
"\u8FD9",
|
|
556
|
+
"\u8981",
|
|
557
|
+
"\u4F1A",
|
|
558
|
+
"\u5BF9",
|
|
559
|
+
"\u8BF4",
|
|
560
|
+
"\u800C",
|
|
561
|
+
"\u53BB",
|
|
562
|
+
"\u4E4B",
|
|
563
|
+
"\u88AB",
|
|
564
|
+
"\u5979",
|
|
565
|
+
"\u628A",
|
|
566
|
+
"\u90A3"
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
370
570
|
|
|
371
|
-
// src/
|
|
372
|
-
|
|
373
|
-
|
|
571
|
+
// src/search/embedding.ts
|
|
572
|
+
import { createHash } from "crypto";
|
|
573
|
+
function trimTrailingSlashes(value) {
|
|
574
|
+
return value.replace(/\/+$/, "");
|
|
374
575
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
576
|
+
function resolveEndpoint(baseUrl, endpoint = "/embeddings") {
|
|
577
|
+
const trimmed = trimTrailingSlashes(baseUrl);
|
|
578
|
+
if (trimmed.endsWith("/embeddings")) {
|
|
579
|
+
return trimmed;
|
|
580
|
+
}
|
|
581
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
582
|
+
return `${trimmed}${normalizedEndpoint}`;
|
|
583
|
+
}
|
|
584
|
+
function stableProviderId(prefix, input) {
|
|
585
|
+
const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
586
|
+
return `${prefix}:${digest}`;
|
|
587
|
+
}
|
|
588
|
+
function getFetch(fetchImpl) {
|
|
589
|
+
const candidate = fetchImpl ?? globalThis.fetch;
|
|
590
|
+
if (!candidate) {
|
|
591
|
+
throw new Error("Global fetch is not available in this runtime");
|
|
592
|
+
}
|
|
593
|
+
return candidate;
|
|
594
|
+
}
|
|
595
|
+
function assertEmbeddingVector(vector, dimension, context) {
|
|
596
|
+
if (!Array.isArray(vector) || !vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
597
|
+
throw new Error(`${context} returned an invalid embedding vector`);
|
|
598
|
+
}
|
|
599
|
+
if (vector.length !== dimension) {
|
|
600
|
+
throw new Error(`${context} returned dimension ${vector.length}, expected ${dimension}`);
|
|
601
|
+
}
|
|
602
|
+
return vector;
|
|
603
|
+
}
|
|
604
|
+
function parseOpenAIResponse(json2, dimension, context) {
|
|
605
|
+
const rows = json2?.data;
|
|
606
|
+
if (!Array.isArray(rows)) {
|
|
607
|
+
throw new Error(`${context} returned an invalid embeddings payload`);
|
|
608
|
+
}
|
|
609
|
+
return rows.map((row, index) => assertEmbeddingVector(row?.embedding, dimension, `${context} item ${index}`));
|
|
610
|
+
}
|
|
611
|
+
function parseLocalHttpResponse(json2, dimension, context) {
|
|
612
|
+
if (Array.isArray(json2.embeddings)) {
|
|
613
|
+
const embeddings = json2.embeddings;
|
|
614
|
+
return embeddings.map((row, index) => assertEmbeddingVector(row, dimension, `${context} item ${index}`));
|
|
615
|
+
}
|
|
616
|
+
return parseOpenAIResponse(json2, dimension, context);
|
|
617
|
+
}
|
|
618
|
+
async function runEmbeddingRequest(input) {
|
|
619
|
+
const fetchFn = getFetch(input.fetchImpl);
|
|
620
|
+
const response = await fetchFn(input.url, {
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: {
|
|
623
|
+
"content-type": "application/json",
|
|
624
|
+
...input.headers
|
|
625
|
+
},
|
|
626
|
+
body: JSON.stringify(input.body)
|
|
627
|
+
});
|
|
628
|
+
if (!response.ok) {
|
|
629
|
+
const detail = await response.text().catch(() => "");
|
|
630
|
+
throw new Error(`${input.context} request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
631
|
+
}
|
|
632
|
+
const json2 = await response.json();
|
|
633
|
+
return input.parser(json2, input.dimension, input.context);
|
|
634
|
+
}
|
|
635
|
+
function createOpenAICompatibleEmbeddingProvider(opts) {
|
|
636
|
+
const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
637
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
638
|
+
const id = stableProviderId(`openai-compatible:${opts.model}`, providerDescriptor);
|
|
639
|
+
return {
|
|
640
|
+
id,
|
|
641
|
+
model: opts.model,
|
|
642
|
+
dimension: opts.dimension,
|
|
643
|
+
async embed(texts) {
|
|
644
|
+
if (texts.length === 0) return [];
|
|
645
|
+
return runEmbeddingRequest({
|
|
646
|
+
context: "openai-compatible embedding provider",
|
|
647
|
+
url,
|
|
648
|
+
dimension: opts.dimension,
|
|
649
|
+
fetchImpl: opts.fetchImpl,
|
|
650
|
+
headers: {
|
|
651
|
+
...opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
|
|
652
|
+
...opts.headers
|
|
653
|
+
},
|
|
654
|
+
body: {
|
|
655
|
+
model: opts.model,
|
|
656
|
+
input: texts
|
|
657
|
+
},
|
|
658
|
+
parser: parseOpenAIResponse
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
async healthcheck() {
|
|
662
|
+
await this.embed(["healthcheck"]);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function createLocalHttpEmbeddingProvider(opts) {
|
|
667
|
+
const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
668
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
669
|
+
const id = stableProviderId(`local-http:${opts.model}`, providerDescriptor);
|
|
670
|
+
return {
|
|
671
|
+
id,
|
|
672
|
+
model: opts.model,
|
|
673
|
+
dimension: opts.dimension,
|
|
674
|
+
async embed(texts) {
|
|
675
|
+
if (texts.length === 0) return [];
|
|
676
|
+
return runEmbeddingRequest({
|
|
677
|
+
context: "local-http embedding provider",
|
|
678
|
+
url,
|
|
679
|
+
dimension: opts.dimension,
|
|
680
|
+
fetchImpl: opts.fetchImpl,
|
|
681
|
+
headers: opts.headers,
|
|
682
|
+
body: {
|
|
683
|
+
model: opts.model,
|
|
684
|
+
input: texts
|
|
685
|
+
},
|
|
686
|
+
parser: parseLocalHttpResponse
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
async healthcheck() {
|
|
690
|
+
await this.embed(["healthcheck"]);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
695
|
+
return trimTrailingSlashes(baseUrl);
|
|
696
|
+
}
|
|
697
|
+
var init_embedding = __esm({
|
|
698
|
+
"src/search/embedding.ts"() {
|
|
699
|
+
"use strict";
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// src/search/providers.ts
|
|
704
|
+
function parseDimension(raw) {
|
|
705
|
+
if (!raw) return void 0;
|
|
706
|
+
const value = Number.parseInt(raw, 10);
|
|
707
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
708
|
+
}
|
|
709
|
+
function parseProvider(raw) {
|
|
710
|
+
if (!raw) return null;
|
|
711
|
+
if (raw === "openai-compatible" || raw === "local-http") {
|
|
712
|
+
return raw;
|
|
713
|
+
}
|
|
714
|
+
throw new Error(`Unsupported embedding provider: ${raw}`);
|
|
715
|
+
}
|
|
716
|
+
function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
717
|
+
const provider = parseProvider(env.AGENT_MEMORY_EMBEDDING_PROVIDER);
|
|
718
|
+
if (!provider) return null;
|
|
719
|
+
const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
|
|
720
|
+
const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
|
|
721
|
+
const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
|
|
722
|
+
if (!baseUrl) {
|
|
723
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
|
|
724
|
+
}
|
|
725
|
+
if (!model) {
|
|
726
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
|
|
727
|
+
}
|
|
728
|
+
if (!dimension) {
|
|
729
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_DIMENSION is required when embeddings are enabled");
|
|
730
|
+
}
|
|
731
|
+
if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
|
|
732
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
provider,
|
|
736
|
+
baseUrl,
|
|
737
|
+
model,
|
|
738
|
+
dimension,
|
|
739
|
+
apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function createEmbeddingProvider(input, opts) {
|
|
743
|
+
const normalized = {
|
|
744
|
+
...input,
|
|
745
|
+
baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
|
|
746
|
+
};
|
|
747
|
+
if (normalized.provider === "openai-compatible") {
|
|
748
|
+
return createOpenAICompatibleEmbeddingProvider({
|
|
749
|
+
baseUrl: normalized.baseUrl,
|
|
750
|
+
model: normalized.model,
|
|
751
|
+
dimension: normalized.dimension,
|
|
752
|
+
apiKey: normalized.apiKey,
|
|
753
|
+
fetchImpl: opts?.fetchImpl
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
return createLocalHttpEmbeddingProvider({
|
|
757
|
+
baseUrl: normalized.baseUrl,
|
|
758
|
+
model: normalized.model,
|
|
759
|
+
dimension: normalized.dimension,
|
|
760
|
+
fetchImpl: opts?.fetchImpl
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
function resolveEmbeddingProviderConfig(opts) {
|
|
764
|
+
const envConfig = getEmbeddingProviderConfigFromEnv(opts?.env);
|
|
765
|
+
if (!envConfig && !opts?.config?.provider) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
const provider = opts?.config?.provider ?? envConfig?.provider;
|
|
769
|
+
const baseUrl = opts?.config?.baseUrl ?? envConfig?.baseUrl;
|
|
770
|
+
const model = opts?.config?.model ?? envConfig?.model;
|
|
771
|
+
const dimension = opts?.config?.dimension ?? envConfig?.dimension;
|
|
772
|
+
const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
|
|
773
|
+
if (!provider || !baseUrl || !model || !dimension) {
|
|
774
|
+
throw new Error("Incomplete embedding provider configuration");
|
|
775
|
+
}
|
|
776
|
+
if (provider === "openai-compatible" && !apiKey) {
|
|
777
|
+
throw new Error("OpenAI-compatible embedding providers require an API key");
|
|
778
|
+
}
|
|
779
|
+
return { provider, baseUrl, model, dimension, apiKey };
|
|
780
|
+
}
|
|
781
|
+
function getEmbeddingProvider(opts) {
|
|
782
|
+
const config = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
|
|
783
|
+
if (!config) return null;
|
|
784
|
+
return createEmbeddingProvider(config, { fetchImpl: opts?.fetchImpl });
|
|
785
|
+
}
|
|
786
|
+
function getEmbeddingProviderFromEnv(env = process.env) {
|
|
787
|
+
try {
|
|
788
|
+
return getEmbeddingProvider({ env });
|
|
789
|
+
} catch {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function getConfiguredEmbeddingProviderId(opts) {
|
|
794
|
+
try {
|
|
795
|
+
const provider = getEmbeddingProvider({ config: opts?.config, env: opts?.env });
|
|
796
|
+
return provider?.id ?? null;
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
var init_providers = __esm({
|
|
802
|
+
"src/search/providers.ts"() {
|
|
803
|
+
"use strict";
|
|
804
|
+
init_embedding();
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// src/search/vector.ts
|
|
809
|
+
function encodeVector(vector) {
|
|
810
|
+
const float32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
811
|
+
return Buffer.from(float32.buffer.slice(float32.byteOffset, float32.byteOffset + float32.byteLength));
|
|
812
|
+
}
|
|
813
|
+
function decodeVector(blob) {
|
|
814
|
+
const buffer = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
815
|
+
const aligned = buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength ? buffer.buffer : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
816
|
+
return Array.from(new Float32Array(aligned));
|
|
817
|
+
}
|
|
818
|
+
function cosineSimilarity(a, b) {
|
|
819
|
+
const length = Math.min(a.length, b.length);
|
|
820
|
+
if (length === 0 || a.length !== b.length) return 0;
|
|
821
|
+
let dot = 0;
|
|
822
|
+
let normA = 0;
|
|
823
|
+
let normB = 0;
|
|
824
|
+
for (let index = 0; index < length; index++) {
|
|
825
|
+
const left = Number(a[index] ?? 0);
|
|
826
|
+
const right = Number(b[index] ?? 0);
|
|
827
|
+
dot += left * right;
|
|
828
|
+
normA += left * left;
|
|
829
|
+
normB += right * right;
|
|
830
|
+
}
|
|
831
|
+
if (normA === 0 || normB === 0) return 0;
|
|
832
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
833
|
+
}
|
|
834
|
+
function markMemoryEmbeddingPending(db, memoryId, providerId, contentHash2) {
|
|
835
|
+
db.prepare(
|
|
836
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
837
|
+
VALUES (?, ?, ?, NULL, ?, 'pending', ?)
|
|
838
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
839
|
+
vector = NULL,
|
|
840
|
+
content_hash = excluded.content_hash,
|
|
841
|
+
status = 'pending'`
|
|
842
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
843
|
+
}
|
|
844
|
+
function markAllEmbeddingsPending(db, memoryId, contentHash2) {
|
|
845
|
+
const result = db.prepare(
|
|
846
|
+
`UPDATE embeddings
|
|
847
|
+
SET vector = NULL,
|
|
848
|
+
content_hash = ?,
|
|
849
|
+
status = 'pending'
|
|
850
|
+
WHERE memory_id = ?`
|
|
851
|
+
).run(contentHash2, memoryId);
|
|
852
|
+
return result.changes;
|
|
853
|
+
}
|
|
854
|
+
function upsertReadyEmbedding(input) {
|
|
855
|
+
input.db.prepare(
|
|
856
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
857
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)
|
|
858
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
859
|
+
vector = excluded.vector,
|
|
860
|
+
content_hash = excluded.content_hash,
|
|
861
|
+
status = 'ready'`
|
|
862
|
+
).run(newId(), input.memoryId, input.providerId, encodeVector(input.vector), input.contentHash, now());
|
|
863
|
+
}
|
|
864
|
+
function markEmbeddingFailed(db, memoryId, providerId, contentHash2) {
|
|
865
|
+
db.prepare(
|
|
866
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
867
|
+
VALUES (?, ?, ?, NULL, ?, 'failed', ?)
|
|
868
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
869
|
+
vector = NULL,
|
|
870
|
+
content_hash = excluded.content_hash,
|
|
871
|
+
status = 'failed'`
|
|
872
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
873
|
+
}
|
|
874
|
+
function searchByVector(db, queryVector, opts) {
|
|
875
|
+
const limit = opts.limit ?? 20;
|
|
876
|
+
const agentId = opts.agent_id ?? "default";
|
|
877
|
+
const minVitality = opts.min_vitality ?? 0;
|
|
878
|
+
const rows = db.prepare(
|
|
879
|
+
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
880
|
+
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
881
|
+
m.stability, m.access_count, m.last_accessed, m.created_at,
|
|
882
|
+
m.updated_at, m.source, m.agent_id, m.hash
|
|
883
|
+
FROM embeddings e
|
|
884
|
+
JOIN memories m ON m.id = e.memory_id
|
|
885
|
+
WHERE e.provider_id = ?
|
|
886
|
+
AND e.status = 'ready'
|
|
887
|
+
AND e.vector IS NOT NULL
|
|
888
|
+
AND e.content_hash = m.hash
|
|
889
|
+
AND m.agent_id = ?
|
|
890
|
+
AND m.vitality >= ?`
|
|
891
|
+
).all(opts.providerId, agentId, minVitality);
|
|
892
|
+
const scored = rows.map((row) => ({
|
|
893
|
+
provider_id: row.provider_id,
|
|
894
|
+
memory: {
|
|
895
|
+
id: row.id,
|
|
896
|
+
content: row.content,
|
|
897
|
+
type: row.type,
|
|
898
|
+
priority: row.priority,
|
|
899
|
+
emotion_val: row.emotion_val,
|
|
900
|
+
vitality: row.vitality,
|
|
901
|
+
stability: row.stability,
|
|
902
|
+
access_count: row.access_count,
|
|
903
|
+
last_accessed: row.last_accessed,
|
|
904
|
+
created_at: row.created_at,
|
|
905
|
+
updated_at: row.updated_at,
|
|
906
|
+
source: row.source,
|
|
907
|
+
agent_id: row.agent_id,
|
|
908
|
+
hash: row.hash
|
|
909
|
+
},
|
|
910
|
+
similarity: cosineSimilarity(queryVector, decodeVector(row.vector))
|
|
911
|
+
})).filter((row) => Number.isFinite(row.similarity) && row.similarity > 0).sort((left, right) => right.similarity - left.similarity).slice(0, limit);
|
|
912
|
+
return scored.map((row, index) => ({
|
|
913
|
+
memory: row.memory,
|
|
914
|
+
similarity: row.similarity,
|
|
915
|
+
rank: index + 1,
|
|
916
|
+
provider_id: row.provider_id
|
|
917
|
+
}));
|
|
918
|
+
}
|
|
919
|
+
var init_vector = __esm({
|
|
920
|
+
"src/search/vector.ts"() {
|
|
921
|
+
"use strict";
|
|
922
|
+
init_db();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// src/core/memory.ts
|
|
927
|
+
import { createHash as createHash2 } from "crypto";
|
|
928
|
+
function contentHash(content) {
|
|
929
|
+
return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
930
|
+
}
|
|
931
|
+
function resolveEmbeddingProviderId(explicitProviderId) {
|
|
932
|
+
if (explicitProviderId !== void 0) {
|
|
933
|
+
return explicitProviderId;
|
|
934
|
+
}
|
|
935
|
+
return getConfiguredEmbeddingProviderId();
|
|
936
|
+
}
|
|
937
|
+
function markEmbeddingDirtyIfNeeded(db, memoryId, hash, providerId) {
|
|
938
|
+
if (!providerId) return;
|
|
939
|
+
try {
|
|
940
|
+
markMemoryEmbeddingPending(db, memoryId, providerId, hash);
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function createMemory(db, input) {
|
|
945
|
+
const hash = contentHash(input.content);
|
|
946
|
+
const agentId = input.agent_id ?? "default";
|
|
947
|
+
const priority = input.priority ?? TYPE_PRIORITY[input.type];
|
|
948
|
+
const stability = PRIORITY_STABILITY[priority];
|
|
949
|
+
const existing = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
|
|
950
|
+
if (existing) {
|
|
951
|
+
return null;
|
|
399
952
|
}
|
|
400
953
|
const id = newId();
|
|
401
954
|
const timestamp = now();
|
|
402
955
|
db.prepare(
|
|
403
956
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
404
|
-
access_count, created_at, updated_at, source, agent_id, hash)
|
|
405
|
-
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
|
|
957
|
+
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
|
|
958
|
+
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
|
|
406
959
|
).run(
|
|
407
960
|
id,
|
|
408
961
|
input.content,
|
|
@@ -414,9 +967,11 @@ function createMemory(db, input) {
|
|
|
414
967
|
timestamp,
|
|
415
968
|
input.source ?? null,
|
|
416
969
|
agentId,
|
|
417
|
-
hash
|
|
970
|
+
hash,
|
|
971
|
+
input.emotion_tag ?? null
|
|
418
972
|
);
|
|
419
973
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
974
|
+
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
420
975
|
return getMemory(db, id);
|
|
421
976
|
}
|
|
422
977
|
function getMemory(db, id) {
|
|
@@ -427,9 +982,11 @@ function updateMemory(db, id, input) {
|
|
|
427
982
|
if (!existing) return null;
|
|
428
983
|
const fields = [];
|
|
429
984
|
const values = [];
|
|
985
|
+
let nextHash = null;
|
|
430
986
|
if (input.content !== void 0) {
|
|
987
|
+
nextHash = contentHash(input.content);
|
|
431
988
|
fields.push("content = ?", "hash = ?");
|
|
432
|
-
values.push(input.content,
|
|
989
|
+
values.push(input.content, nextHash);
|
|
433
990
|
}
|
|
434
991
|
if (input.type !== void 0) {
|
|
435
992
|
fields.push("type = ?");
|
|
@@ -455,6 +1012,10 @@ function updateMemory(db, id, input) {
|
|
|
455
1012
|
fields.push("source = ?");
|
|
456
1013
|
values.push(input.source);
|
|
457
1014
|
}
|
|
1015
|
+
if (input.emotion_tag !== void 0) {
|
|
1016
|
+
fields.push("emotion_tag = ?");
|
|
1017
|
+
values.push(input.emotion_tag);
|
|
1018
|
+
}
|
|
458
1019
|
fields.push("updated_at = ?");
|
|
459
1020
|
values.push(now());
|
|
460
1021
|
values.push(id);
|
|
@@ -462,6 +1023,13 @@ function updateMemory(db, id, input) {
|
|
|
462
1023
|
if (input.content !== void 0) {
|
|
463
1024
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
464
1025
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1026
|
+
if (nextHash) {
|
|
1027
|
+
try {
|
|
1028
|
+
markAllEmbeddingsPending(db, id, nextHash);
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
markEmbeddingDirtyIfNeeded(db, id, nextHash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
1032
|
+
}
|
|
465
1033
|
}
|
|
466
1034
|
return getMemory(db, id);
|
|
467
1035
|
}
|
|
@@ -489,6 +1057,10 @@ function listMemories(db, opts) {
|
|
|
489
1057
|
conditions.push("vitality >= ?");
|
|
490
1058
|
params.push(opts.min_vitality);
|
|
491
1059
|
}
|
|
1060
|
+
if (opts?.emotion_tag) {
|
|
1061
|
+
conditions.push("emotion_tag = ?");
|
|
1062
|
+
params.push(opts.emotion_tag);
|
|
1063
|
+
}
|
|
492
1064
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
493
1065
|
const limit = opts?.limit ?? 100;
|
|
494
1066
|
const offset = opts?.offset ?? 0;
|
|
@@ -513,8 +1085,215 @@ function countMemories(db, agent_id = "default") {
|
|
|
513
1085
|
by_priority: Object.fromEntries(byPriority.map((r) => [`P${r.priority}`, r.c]))
|
|
514
1086
|
};
|
|
515
1087
|
}
|
|
1088
|
+
var TYPE_PRIORITY, PRIORITY_STABILITY;
|
|
1089
|
+
var init_memory = __esm({
|
|
1090
|
+
"src/core/memory.ts"() {
|
|
1091
|
+
"use strict";
|
|
1092
|
+
init_db();
|
|
1093
|
+
init_tokenizer();
|
|
1094
|
+
init_providers();
|
|
1095
|
+
init_vector();
|
|
1096
|
+
TYPE_PRIORITY = {
|
|
1097
|
+
identity: 0,
|
|
1098
|
+
emotion: 1,
|
|
1099
|
+
knowledge: 2,
|
|
1100
|
+
event: 3
|
|
1101
|
+
};
|
|
1102
|
+
PRIORITY_STABILITY = {
|
|
1103
|
+
0: Infinity,
|
|
1104
|
+
// P0: never decays
|
|
1105
|
+
1: 365,
|
|
1106
|
+
// P1: 365-day half-life
|
|
1107
|
+
2: 90,
|
|
1108
|
+
// P2: 90-day half-life
|
|
1109
|
+
3: 14
|
|
1110
|
+
// P3: 14-day half-life
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// src/core/path.ts
|
|
1116
|
+
function parseUri(uri) {
|
|
1117
|
+
const match = uri.match(/^([a-z]+):\/\/(.+)$/);
|
|
1118
|
+
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
1119
|
+
return { domain: match[1], path: match[2] };
|
|
1120
|
+
}
|
|
1121
|
+
function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
|
|
1122
|
+
const { domain } = parseUri(uri);
|
|
1123
|
+
const domains = validDomains ?? DEFAULT_DOMAINS;
|
|
1124
|
+
if (!domains.has(domain)) {
|
|
1125
|
+
throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
|
|
1126
|
+
}
|
|
1127
|
+
const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
|
|
1128
|
+
if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
|
|
1129
|
+
if (agent_id && agent_id !== memoryAgent) {
|
|
1130
|
+
throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
|
|
1131
|
+
}
|
|
1132
|
+
const agentId = agent_id ?? memoryAgent;
|
|
1133
|
+
const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
|
|
1134
|
+
if (existing) {
|
|
1135
|
+
throw new Error(`URI already exists: ${uri}`);
|
|
1136
|
+
}
|
|
1137
|
+
const id = newId();
|
|
1138
|
+
db.prepare(
|
|
1139
|
+
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1140
|
+
).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
|
|
1141
|
+
return getPath(db, id);
|
|
1142
|
+
}
|
|
1143
|
+
function getPath(db, id) {
|
|
1144
|
+
return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
|
|
1145
|
+
}
|
|
1146
|
+
function getPathByUri(db, uri, agent_id = "default") {
|
|
1147
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
1148
|
+
}
|
|
1149
|
+
var DEFAULT_DOMAINS;
|
|
1150
|
+
var init_path = __esm({
|
|
1151
|
+
"src/core/path.ts"() {
|
|
1152
|
+
"use strict";
|
|
1153
|
+
init_db();
|
|
1154
|
+
DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// src/sleep/boot.ts
|
|
1159
|
+
var boot_exports = {};
|
|
1160
|
+
__export(boot_exports, {
|
|
1161
|
+
boot: () => boot,
|
|
1162
|
+
formatNarrativeBoot: () => formatNarrativeBoot,
|
|
1163
|
+
formatRelativeDate: () => formatRelativeDate,
|
|
1164
|
+
loadWarmBootLayers: () => loadWarmBootLayers
|
|
1165
|
+
});
|
|
1166
|
+
function formatRelativeDate(isoDate) {
|
|
1167
|
+
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
1168
|
+
const diffDays = Math.floor(diffMs / 864e5);
|
|
1169
|
+
if (diffDays <= 0) return "\u4ECA\u5929";
|
|
1170
|
+
if (diffDays === 1) return "\u6628\u5929";
|
|
1171
|
+
if (diffDays <= 7) return `${diffDays}\u5929\u524D`;
|
|
1172
|
+
return isoDate.slice(0, 10);
|
|
1173
|
+
}
|
|
1174
|
+
function loadWarmBootLayers(db, agentId) {
|
|
1175
|
+
const identity = listMemories(db, { agent_id: agentId, type: "identity", limit: 50 });
|
|
1176
|
+
const emotion = listMemories(db, { agent_id: agentId, type: "emotion", limit: 5 });
|
|
1177
|
+
const event = listMemories(db, { agent_id: agentId, type: "event", limit: 7 });
|
|
1178
|
+
const knowledge = listMemories(db, {
|
|
1179
|
+
agent_id: agentId,
|
|
1180
|
+
type: "knowledge",
|
|
1181
|
+
min_vitality: 0.5,
|
|
1182
|
+
limit: 10
|
|
1183
|
+
});
|
|
1184
|
+
return { identity, emotion, event, knowledge };
|
|
1185
|
+
}
|
|
1186
|
+
function formatNarrativeBoot(layers, agentName) {
|
|
1187
|
+
const lines = [];
|
|
1188
|
+
lines.push(`# ${agentName}\u7684\u56DE\u5FC6`);
|
|
1189
|
+
lines.push("");
|
|
1190
|
+
if (layers.identity.length > 0) {
|
|
1191
|
+
lines.push("## \u6211\u662F\u8C01");
|
|
1192
|
+
for (const mem of layers.identity) {
|
|
1193
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
|
|
1194
|
+
}
|
|
1195
|
+
lines.push("");
|
|
1196
|
+
}
|
|
1197
|
+
if (layers.emotion.length > 0) {
|
|
1198
|
+
lines.push("## \u6700\u8FD1\u7684\u5FC3\u60C5");
|
|
1199
|
+
for (const mem of layers.emotion) {
|
|
1200
|
+
const tag = mem.emotion_tag;
|
|
1201
|
+
const time = formatRelativeDate(mem.updated_at);
|
|
1202
|
+
const tagStr = tag ? `${tag}, ${time}` : time;
|
|
1203
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${tagStr})`);
|
|
1204
|
+
}
|
|
1205
|
+
lines.push("");
|
|
1206
|
+
}
|
|
1207
|
+
if (layers.event.length > 0) {
|
|
1208
|
+
lines.push("## \u6700\u8FD1\u53D1\u751F\u7684\u4E8B");
|
|
1209
|
+
for (const mem of layers.event) {
|
|
1210
|
+
const time = formatRelativeDate(mem.updated_at);
|
|
1211
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${time})`);
|
|
1212
|
+
}
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
}
|
|
1215
|
+
if (layers.knowledge.length > 0) {
|
|
1216
|
+
lines.push("## \u8FD8\u8BB0\u5F97\u7684\u77E5\u8BC6");
|
|
1217
|
+
for (const mem of layers.knowledge) {
|
|
1218
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
|
|
1219
|
+
}
|
|
1220
|
+
lines.push("");
|
|
1221
|
+
}
|
|
1222
|
+
return lines.join("\n");
|
|
1223
|
+
}
|
|
1224
|
+
function boot(db, opts) {
|
|
1225
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1226
|
+
const format = opts?.format ?? "json";
|
|
1227
|
+
const agentName = opts?.agent_name ?? "Agent";
|
|
1228
|
+
const corePaths = opts?.corePaths ?? [
|
|
1229
|
+
"core://agent",
|
|
1230
|
+
"core://user",
|
|
1231
|
+
"core://agent/identity",
|
|
1232
|
+
"core://user/identity"
|
|
1233
|
+
];
|
|
1234
|
+
const memories = /* @__PURE__ */ new Map();
|
|
1235
|
+
const identities = listMemories(db, { agent_id: agentId, priority: 0 });
|
|
1236
|
+
for (const mem of identities) {
|
|
1237
|
+
memories.set(mem.id, mem);
|
|
1238
|
+
recordAccess(db, mem.id, 1.1);
|
|
1239
|
+
}
|
|
1240
|
+
const bootPaths = [];
|
|
1241
|
+
for (const uri of corePaths) {
|
|
1242
|
+
const path = getPathByUri(db, uri, agentId);
|
|
1243
|
+
if (path) {
|
|
1244
|
+
bootPaths.push(uri);
|
|
1245
|
+
if (!memories.has(path.memory_id)) {
|
|
1246
|
+
const mem = getMemory(db, path.memory_id);
|
|
1247
|
+
if (mem) {
|
|
1248
|
+
memories.set(mem.id, mem);
|
|
1249
|
+
recordAccess(db, mem.id, 1.1);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
1255
|
+
if (bootEntry) {
|
|
1256
|
+
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
1257
|
+
if (bootMem) {
|
|
1258
|
+
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
1259
|
+
for (const uri of additionalUris) {
|
|
1260
|
+
const path = getPathByUri(db, uri, agentId);
|
|
1261
|
+
if (path && !memories.has(path.memory_id)) {
|
|
1262
|
+
const mem = getMemory(db, path.memory_id);
|
|
1263
|
+
if (mem) {
|
|
1264
|
+
memories.set(mem.id, mem);
|
|
1265
|
+
bootPaths.push(uri);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const result = {
|
|
1272
|
+
identityMemories: [...memories.values()],
|
|
1273
|
+
bootPaths
|
|
1274
|
+
};
|
|
1275
|
+
if (format === "narrative") {
|
|
1276
|
+
const layers = loadWarmBootLayers(db, agentId);
|
|
1277
|
+
result.layers = layers;
|
|
1278
|
+
result.narrative = formatNarrativeBoot(layers, agentName);
|
|
1279
|
+
}
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
var init_boot = __esm({
|
|
1283
|
+
"src/sleep/boot.ts"() {
|
|
1284
|
+
"use strict";
|
|
1285
|
+
init_path();
|
|
1286
|
+
init_memory();
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// src/bin/agent-memory.ts
|
|
1291
|
+
init_db();
|
|
1292
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1293
|
+
import { basename, resolve } from "path";
|
|
516
1294
|
|
|
517
1295
|
// src/core/export.ts
|
|
1296
|
+
init_memory();
|
|
518
1297
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
519
1298
|
import { join } from "path";
|
|
520
1299
|
function exportMemories(db, dirPath, opts) {
|
|
@@ -577,7 +1356,17 @@ function exportMemories(db, dirPath, opts) {
|
|
|
577
1356
|
return { exported, files };
|
|
578
1357
|
}
|
|
579
1358
|
|
|
1359
|
+
// src/bin/agent-memory.ts
|
|
1360
|
+
init_boot();
|
|
1361
|
+
|
|
1362
|
+
// src/app/surface.ts
|
|
1363
|
+
init_memory();
|
|
1364
|
+
|
|
1365
|
+
// src/search/hybrid.ts
|
|
1366
|
+
init_memory();
|
|
1367
|
+
|
|
580
1368
|
// src/search/bm25.ts
|
|
1369
|
+
init_tokenizer();
|
|
581
1370
|
function searchBM25(db, query, opts) {
|
|
582
1371
|
const limit = opts?.limit ?? 20;
|
|
583
1372
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -595,12 +1384,13 @@ function searchBM25(db, query, opts) {
|
|
|
595
1384
|
ORDER BY rank
|
|
596
1385
|
LIMIT ?`
|
|
597
1386
|
).all(ftsQuery, agentId, minVitality, limit);
|
|
598
|
-
return rows.map((row) => {
|
|
1387
|
+
return rows.map((row, index) => {
|
|
599
1388
|
const { score: _score, ...memoryFields } = row;
|
|
600
1389
|
return {
|
|
601
1390
|
memory: memoryFields,
|
|
602
1391
|
score: Math.abs(row.score),
|
|
603
1392
|
// FTS5 rank is negative (lower = better)
|
|
1393
|
+
rank: index + 1,
|
|
604
1394
|
matchReason: "bm25"
|
|
605
1395
|
};
|
|
606
1396
|
});
|
|
@@ -615,10 +1405,10 @@ function searchSimple(db, query, agentId, minVitality, limit) {
|
|
|
615
1405
|
ORDER BY priority ASC, updated_at DESC
|
|
616
1406
|
LIMIT ?`
|
|
617
1407
|
).all(agentId, minVitality, `%${query}%`, limit);
|
|
618
|
-
return rows.map((
|
|
619
|
-
memory
|
|
620
|
-
score: 1 / (
|
|
621
|
-
|
|
1408
|
+
return rows.map((memory, index) => ({
|
|
1409
|
+
memory,
|
|
1410
|
+
score: 1 / (index + 1),
|
|
1411
|
+
rank: index + 1,
|
|
622
1412
|
matchReason: "like"
|
|
623
1413
|
}));
|
|
624
1414
|
}
|
|
@@ -627,96 +1417,957 @@ function buildFtsQuery(text) {
|
|
|
627
1417
|
if (tokens.length === 0) return null;
|
|
628
1418
|
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
629
1419
|
}
|
|
1420
|
+
function rebuildBm25Index(db, opts) {
|
|
1421
|
+
const memories = opts?.agent_id ? db.prepare("SELECT id, content FROM memories WHERE agent_id = ?").all(opts.agent_id) : db.prepare("SELECT id, content FROM memories").all();
|
|
1422
|
+
const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
|
|
1423
|
+
const deleteOne = db.prepare("DELETE FROM memories_fts WHERE id = ?");
|
|
1424
|
+
const transaction = db.transaction(() => {
|
|
1425
|
+
if (!opts?.agent_id) {
|
|
1426
|
+
db.exec("DELETE FROM memories_fts");
|
|
1427
|
+
}
|
|
1428
|
+
for (const memory of memories) {
|
|
1429
|
+
if (opts?.agent_id) {
|
|
1430
|
+
deleteOne.run(memory.id);
|
|
1431
|
+
}
|
|
1432
|
+
insert.run(memory.id, tokenizeForIndex(memory.content));
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
transaction();
|
|
1436
|
+
return { reindexed: memories.length };
|
|
1437
|
+
}
|
|
630
1438
|
|
|
631
|
-
// src/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1439
|
+
// src/search/hybrid.ts
|
|
1440
|
+
init_providers();
|
|
1441
|
+
init_vector();
|
|
1442
|
+
var PRIORITY_WEIGHT = {
|
|
1443
|
+
0: 4,
|
|
1444
|
+
1: 3,
|
|
1445
|
+
2: 2,
|
|
1446
|
+
3: 1
|
|
1447
|
+
};
|
|
1448
|
+
var PRIORITY_PRIOR = {
|
|
1449
|
+
0: 1,
|
|
1450
|
+
1: 0.75,
|
|
1451
|
+
2: 0.5,
|
|
1452
|
+
3: 0.25
|
|
1453
|
+
};
|
|
1454
|
+
function scoreBm25Only(results, limit) {
|
|
1455
|
+
return results.map((row) => {
|
|
1456
|
+
const weight = PRIORITY_WEIGHT[row.memory.priority] ?? 1;
|
|
1457
|
+
const vitality = Math.max(0.1, row.memory.vitality);
|
|
1458
|
+
return {
|
|
1459
|
+
memory: row.memory,
|
|
1460
|
+
score: row.score * weight * vitality,
|
|
1461
|
+
bm25_rank: row.rank,
|
|
1462
|
+
bm25_score: row.score
|
|
1463
|
+
};
|
|
1464
|
+
}).sort((left, right) => right.score - left.score).slice(0, limit);
|
|
637
1465
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1466
|
+
function priorityPrior(priority) {
|
|
1467
|
+
return PRIORITY_PRIOR[priority] ?? 0.25;
|
|
1468
|
+
}
|
|
1469
|
+
function fusionScore(input) {
|
|
1470
|
+
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1471
|
+
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1472
|
+
return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
1473
|
+
}
|
|
1474
|
+
function fuseHybridResults(lexical, vector, limit) {
|
|
1475
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (const row of lexical) {
|
|
1477
|
+
candidates.set(row.memory.id, {
|
|
1478
|
+
memory: row.memory,
|
|
1479
|
+
score: 0,
|
|
1480
|
+
bm25_rank: row.rank,
|
|
1481
|
+
bm25_score: row.score
|
|
1482
|
+
});
|
|
648
1483
|
}
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1484
|
+
for (const row of vector) {
|
|
1485
|
+
const existing = candidates.get(row.memory.id);
|
|
1486
|
+
if (existing) {
|
|
1487
|
+
existing.vector_rank = row.rank;
|
|
1488
|
+
existing.vector_score = row.similarity;
|
|
1489
|
+
} else {
|
|
1490
|
+
candidates.set(row.memory.id, {
|
|
1491
|
+
memory: row.memory,
|
|
1492
|
+
score: 0,
|
|
1493
|
+
vector_rank: row.rank,
|
|
1494
|
+
vector_score: row.similarity
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
653
1497
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
).
|
|
658
|
-
|
|
1498
|
+
return [...candidates.values()].map((row) => ({
|
|
1499
|
+
...row,
|
|
1500
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
1501
|
+
})).sort((left, right) => {
|
|
1502
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1503
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
1504
|
+
}).slice(0, limit);
|
|
659
1505
|
}
|
|
660
|
-
function
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1506
|
+
async function searchVectorBranch(db, query, opts) {
|
|
1507
|
+
const [queryVector] = await opts.provider.embed([query]);
|
|
1508
|
+
if (!queryVector) return [];
|
|
1509
|
+
return searchByVector(db, queryVector, {
|
|
1510
|
+
providerId: opts.provider.id,
|
|
1511
|
+
agent_id: opts.agent_id,
|
|
1512
|
+
limit: opts.limit,
|
|
1513
|
+
min_vitality: opts.min_vitality
|
|
1514
|
+
});
|
|
665
1515
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
function boot(db, opts) {
|
|
1516
|
+
async function recallMemories(db, query, opts) {
|
|
1517
|
+
const limit = opts?.limit ?? 10;
|
|
669
1518
|
const agentId = opts?.agent_id ?? "default";
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1519
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
1520
|
+
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1521
|
+
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1522
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1523
|
+
const lexical = searchBM25(db, query, {
|
|
1524
|
+
agent_id: agentId,
|
|
1525
|
+
limit: lexicalLimit,
|
|
1526
|
+
min_vitality: minVitality
|
|
1527
|
+
});
|
|
1528
|
+
let vector = [];
|
|
1529
|
+
if (provider) {
|
|
1530
|
+
try {
|
|
1531
|
+
vector = await searchVectorBranch(db, query, {
|
|
1532
|
+
provider,
|
|
1533
|
+
agent_id: agentId,
|
|
1534
|
+
limit: vectorLimit,
|
|
1535
|
+
min_vitality: minVitality
|
|
1536
|
+
});
|
|
1537
|
+
} catch {
|
|
1538
|
+
vector = [];
|
|
1539
|
+
}
|
|
681
1540
|
}
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1541
|
+
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1542
|
+
const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
|
|
1543
|
+
if (opts?.recordAccess !== false) {
|
|
1544
|
+
for (const row of results) {
|
|
1545
|
+
recordAccess(db, row.memory.id);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
mode,
|
|
1550
|
+
providerId: provider?.id ?? null,
|
|
1551
|
+
usedVectorSearch: vector.length > 0,
|
|
1552
|
+
results
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function listReindexCandidates(db, providerId, agentId, force) {
|
|
1556
|
+
const rows = db.prepare(
|
|
1557
|
+
`SELECT m.id as memoryId,
|
|
1558
|
+
m.content as content,
|
|
1559
|
+
m.hash as contentHash,
|
|
1560
|
+
e.status as embeddingStatus,
|
|
1561
|
+
e.content_hash as embeddingHash
|
|
1562
|
+
FROM memories m
|
|
1563
|
+
LEFT JOIN embeddings e
|
|
1564
|
+
ON e.memory_id = m.id
|
|
1565
|
+
AND e.provider_id = ?
|
|
1566
|
+
WHERE m.agent_id = ?
|
|
1567
|
+
AND m.hash IS NOT NULL`
|
|
1568
|
+
).all(providerId, agentId);
|
|
1569
|
+
return rows.filter((row) => {
|
|
1570
|
+
if (force) return true;
|
|
1571
|
+
if (!row.embeddingStatus) return true;
|
|
1572
|
+
if (row.embeddingStatus !== "ready") return true;
|
|
1573
|
+
return row.embeddingHash !== row.contentHash;
|
|
1574
|
+
}).map((row) => ({
|
|
1575
|
+
memoryId: row.memoryId,
|
|
1576
|
+
content: row.content,
|
|
1577
|
+
contentHash: row.contentHash
|
|
1578
|
+
}));
|
|
1579
|
+
}
|
|
1580
|
+
async function reindexEmbeddings(db, opts) {
|
|
1581
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1582
|
+
if (!provider) {
|
|
1583
|
+
return {
|
|
1584
|
+
enabled: false,
|
|
1585
|
+
providerId: null,
|
|
1586
|
+
scanned: 0,
|
|
1587
|
+
pending: 0,
|
|
1588
|
+
embedded: 0,
|
|
1589
|
+
failed: 0
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1593
|
+
const force = opts?.force ?? false;
|
|
1594
|
+
const batchSize = Math.max(1, opts?.batchSize ?? 16);
|
|
1595
|
+
const candidates = listReindexCandidates(db, provider.id, agentId, force);
|
|
1596
|
+
for (const candidate of candidates) {
|
|
1597
|
+
markMemoryEmbeddingPending(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
1598
|
+
}
|
|
1599
|
+
let embedded = 0;
|
|
1600
|
+
let failed = 0;
|
|
1601
|
+
for (let index = 0; index < candidates.length; index += batchSize) {
|
|
1602
|
+
const batch = candidates.slice(index, index + batchSize);
|
|
1603
|
+
try {
|
|
1604
|
+
const vectors = await provider.embed(batch.map((row) => row.content));
|
|
1605
|
+
if (vectors.length !== batch.length) {
|
|
1606
|
+
throw new Error(`Expected ${batch.length} embeddings, received ${vectors.length}`);
|
|
1607
|
+
}
|
|
1608
|
+
for (let offset = 0; offset < batch.length; offset++) {
|
|
1609
|
+
upsertReadyEmbedding({
|
|
1610
|
+
db,
|
|
1611
|
+
memoryId: batch[offset].memoryId,
|
|
1612
|
+
providerId: provider.id,
|
|
1613
|
+
vector: vectors[offset],
|
|
1614
|
+
contentHash: batch[offset].contentHash
|
|
1615
|
+
});
|
|
1616
|
+
embedded += 1;
|
|
1617
|
+
}
|
|
1618
|
+
} catch {
|
|
1619
|
+
for (const candidate of batch) {
|
|
1620
|
+
markEmbeddingFailed(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
1621
|
+
failed += 1;
|
|
693
1622
|
}
|
|
694
1623
|
}
|
|
695
1624
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1625
|
+
return {
|
|
1626
|
+
enabled: true,
|
|
1627
|
+
providerId: provider.id,
|
|
1628
|
+
scanned: candidates.length,
|
|
1629
|
+
pending: candidates.length,
|
|
1630
|
+
embedded,
|
|
1631
|
+
failed
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/app/surface.ts
|
|
1636
|
+
init_providers();
|
|
1637
|
+
init_tokenizer();
|
|
1638
|
+
init_vector();
|
|
1639
|
+
|
|
1640
|
+
// src/app/feedback.ts
|
|
1641
|
+
init_db();
|
|
1642
|
+
function clamp01(value) {
|
|
1643
|
+
if (!Number.isFinite(value)) return 0;
|
|
1644
|
+
return Math.max(0, Math.min(1, value));
|
|
1645
|
+
}
|
|
1646
|
+
function recordFeedbackEvent(db, input) {
|
|
1647
|
+
const id = newId();
|
|
1648
|
+
const created_at = now();
|
|
1649
|
+
const agentId = input.agent_id ?? "default";
|
|
1650
|
+
const useful = input.useful ? 1 : 0;
|
|
1651
|
+
const value = input.useful ? 1 : 0;
|
|
1652
|
+
const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
|
|
1653
|
+
const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
|
|
1654
|
+
if (!exists) {
|
|
1655
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1658
|
+
db.prepare(
|
|
1659
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
1660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1661
|
+
).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
|
|
1662
|
+
} catch {
|
|
1663
|
+
db.prepare(
|
|
1664
|
+
`INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
|
|
1665
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1666
|
+
).run(id, input.memory_id, eventType, value, created_at);
|
|
1667
|
+
}
|
|
1668
|
+
return {
|
|
1669
|
+
id,
|
|
1670
|
+
memory_id: input.memory_id,
|
|
1671
|
+
source: input.source,
|
|
1672
|
+
useful: input.useful,
|
|
1673
|
+
agent_id: agentId,
|
|
1674
|
+
created_at,
|
|
1675
|
+
value
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function getFeedbackSummary(db, memoryId, agentId) {
|
|
1679
|
+
try {
|
|
1680
|
+
const row = db.prepare(
|
|
1681
|
+
`SELECT COUNT(*) as total,
|
|
1682
|
+
COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
|
|
1683
|
+
COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
|
|
1684
|
+
FROM feedback_events
|
|
1685
|
+
WHERE memory_id = ?
|
|
1686
|
+
AND (? IS NULL OR agent_id = ?)`
|
|
1687
|
+
).get(memoryId, agentId ?? null, agentId ?? null);
|
|
1688
|
+
if (!row || row.total === 0) {
|
|
1689
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
total: row.total,
|
|
1693
|
+
useful: row.useful,
|
|
1694
|
+
not_useful: row.not_useful,
|
|
1695
|
+
score: clamp01(row.useful / row.total)
|
|
1696
|
+
};
|
|
1697
|
+
} catch {
|
|
1698
|
+
const row = db.prepare(
|
|
1699
|
+
`SELECT COUNT(*) as total,
|
|
1700
|
+
COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
|
|
1701
|
+
COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
|
|
1702
|
+
COALESCE(AVG(value), 0.5) as avg_value
|
|
1703
|
+
FROM feedback_events
|
|
1704
|
+
WHERE memory_id = ?`
|
|
1705
|
+
).get(memoryId);
|
|
1706
|
+
if (!row || row.total === 0) {
|
|
1707
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
total: row.total,
|
|
1711
|
+
useful: row.useful,
|
|
1712
|
+
not_useful: row.not_useful,
|
|
1713
|
+
score: clamp01(row.avg_value)
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/app/surface.ts
|
|
1719
|
+
var INTENT_PRIORS = {
|
|
1720
|
+
factual: {
|
|
1721
|
+
identity: 0.25,
|
|
1722
|
+
emotion: 0.15,
|
|
1723
|
+
knowledge: 1,
|
|
1724
|
+
event: 0.8
|
|
1725
|
+
},
|
|
1726
|
+
preference: {
|
|
1727
|
+
identity: 1,
|
|
1728
|
+
emotion: 0.85,
|
|
1729
|
+
knowledge: 0.55,
|
|
1730
|
+
event: 0.25
|
|
1731
|
+
},
|
|
1732
|
+
temporal: {
|
|
1733
|
+
identity: 0.15,
|
|
1734
|
+
emotion: 0.35,
|
|
1735
|
+
knowledge: 0.5,
|
|
1736
|
+
event: 1
|
|
1737
|
+
},
|
|
1738
|
+
planning: {
|
|
1739
|
+
identity: 0.65,
|
|
1740
|
+
emotion: 0.2,
|
|
1741
|
+
knowledge: 1,
|
|
1742
|
+
event: 0.6
|
|
1743
|
+
},
|
|
1744
|
+
design: {
|
|
1745
|
+
identity: 0.8,
|
|
1746
|
+
emotion: 0.35,
|
|
1747
|
+
knowledge: 1,
|
|
1748
|
+
event: 0.25
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
|
|
1752
|
+
var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
|
|
1753
|
+
var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
|
|
1754
|
+
var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
|
|
1755
|
+
var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
|
|
1756
|
+
function clamp012(value) {
|
|
1757
|
+
if (!Number.isFinite(value)) return 0;
|
|
1758
|
+
return Math.max(0, Math.min(1, value));
|
|
1759
|
+
}
|
|
1760
|
+
function uniqueTokenSet(values) {
|
|
1761
|
+
return new Set(
|
|
1762
|
+
values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
function overlapScore(left, right) {
|
|
1766
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
1767
|
+
let shared = 0;
|
|
1768
|
+
for (const token of left) {
|
|
1769
|
+
if (right.has(token)) shared += 1;
|
|
1770
|
+
}
|
|
1771
|
+
return clamp012(shared / Math.max(left.size, right.size));
|
|
1772
|
+
}
|
|
1773
|
+
function rankScore(rank, window) {
|
|
1774
|
+
if (!rank) return 0;
|
|
1775
|
+
return clamp012(1 - (rank - 1) / Math.max(window, 1));
|
|
1776
|
+
}
|
|
1777
|
+
function topicLabel(...parts) {
|
|
1778
|
+
const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
|
|
1779
|
+
const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
1780
|
+
return label || "context";
|
|
1781
|
+
}
|
|
1782
|
+
function intentKeywordBoost(memory, intent) {
|
|
1783
|
+
const content = memory.content;
|
|
1784
|
+
switch (intent) {
|
|
1785
|
+
case "design":
|
|
1786
|
+
return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
|
|
1787
|
+
case "planning":
|
|
1788
|
+
return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
|
|
1789
|
+
case "factual":
|
|
1790
|
+
return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1791
|
+
case "temporal":
|
|
1792
|
+
return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1793
|
+
case "preference":
|
|
1794
|
+
return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function intentMatch(memory, intent) {
|
|
1798
|
+
if (!intent) return 0;
|
|
1799
|
+
const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
|
|
1800
|
+
return clamp012(prior * intentKeywordBoost(memory, intent));
|
|
1801
|
+
}
|
|
1802
|
+
function buildReasonCodes(input) {
|
|
1803
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
1804
|
+
reasons.add(`type:${input.memory.type}`);
|
|
1805
|
+
if (input.semanticScore > 0.2) {
|
|
1806
|
+
reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
|
|
1807
|
+
}
|
|
1808
|
+
if (input.lexicalScore > 0.2 && input.query) {
|
|
1809
|
+
reasons.add(`lexical:${topicLabel(input.query)}`);
|
|
1810
|
+
}
|
|
1811
|
+
if (input.taskMatch > 0.2) {
|
|
1812
|
+
reasons.add(`task:${topicLabel(input.task, input.intent)}`);
|
|
1813
|
+
}
|
|
1814
|
+
if (input.intent) {
|
|
1815
|
+
reasons.add(`intent:${input.intent}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (input.feedbackScore >= 0.67) {
|
|
1818
|
+
reasons.add("feedback:reinforced");
|
|
1819
|
+
} else if (input.feedbackScore <= 0.33) {
|
|
1820
|
+
reasons.add("feedback:negative");
|
|
1821
|
+
}
|
|
1822
|
+
return [...reasons];
|
|
1823
|
+
}
|
|
1824
|
+
function collectBranch(signals, rows, key, similarity) {
|
|
1825
|
+
for (const row of rows) {
|
|
1826
|
+
const existing = signals.get(row.memory.id) ?? { memory: row.memory };
|
|
1827
|
+
const currentRank = existing[key];
|
|
1828
|
+
if (currentRank === void 0 || row.rank < currentRank) {
|
|
1829
|
+
existing[key] = row.rank;
|
|
1830
|
+
}
|
|
1831
|
+
if (similarity) {
|
|
1832
|
+
const currentSimilarity = similarity.get(row.memory.id);
|
|
1833
|
+
if (currentSimilarity !== void 0) {
|
|
1834
|
+
existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
signals.set(row.memory.id, existing);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function surfaceMemories(db, input) {
|
|
1841
|
+
const agentId = input.agent_id ?? "default";
|
|
1842
|
+
const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
|
|
1843
|
+
const lexicalWindow = Math.max(24, limit * 6);
|
|
1844
|
+
const minVitality = input.min_vitality ?? 0.05;
|
|
1845
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
1846
|
+
const signals = /* @__PURE__ */ new Map();
|
|
1847
|
+
const trimmedQuery = input.query?.trim();
|
|
1848
|
+
const trimmedTask = input.task?.trim();
|
|
1849
|
+
const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
|
|
1850
|
+
const queryTokens = uniqueTokenSet([trimmedQuery, ...recentTurns]);
|
|
1851
|
+
const taskTokens = uniqueTokenSet([trimmedTask]);
|
|
1852
|
+
if (trimmedQuery) {
|
|
1853
|
+
collectBranch(
|
|
1854
|
+
signals,
|
|
1855
|
+
searchBM25(db, trimmedQuery, {
|
|
1856
|
+
agent_id: agentId,
|
|
1857
|
+
limit: lexicalWindow,
|
|
1858
|
+
min_vitality: minVitality
|
|
1859
|
+
}),
|
|
1860
|
+
"queryRank"
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
if (trimmedTask) {
|
|
1864
|
+
collectBranch(
|
|
1865
|
+
signals,
|
|
1866
|
+
searchBM25(db, trimmedTask, {
|
|
1867
|
+
agent_id: agentId,
|
|
1868
|
+
limit: lexicalWindow,
|
|
1869
|
+
min_vitality: minVitality
|
|
1870
|
+
}),
|
|
1871
|
+
"taskRank"
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (recentTurns.length > 0) {
|
|
1875
|
+
collectBranch(
|
|
1876
|
+
signals,
|
|
1877
|
+
searchBM25(db, recentTurns.join(" "), {
|
|
1878
|
+
agent_id: agentId,
|
|
1879
|
+
limit: lexicalWindow,
|
|
1880
|
+
min_vitality: minVitality
|
|
1881
|
+
}),
|
|
1882
|
+
"recentRank"
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
|
|
1886
|
+
if (provider && semanticQuery) {
|
|
1887
|
+
try {
|
|
1888
|
+
const [queryVector] = await provider.embed([semanticQuery]);
|
|
1889
|
+
if (queryVector) {
|
|
1890
|
+
const vectorRows = searchByVector(db, queryVector, {
|
|
1891
|
+
providerId: provider.id,
|
|
1892
|
+
agent_id: agentId,
|
|
1893
|
+
limit: lexicalWindow,
|
|
1894
|
+
min_vitality: minVitality
|
|
1895
|
+
});
|
|
1896
|
+
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
1897
|
+
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
710
1898
|
}
|
|
1899
|
+
} catch {
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const fallbackMemories = listMemories(db, {
|
|
1903
|
+
agent_id: agentId,
|
|
1904
|
+
min_vitality: minVitality,
|
|
1905
|
+
limit: Math.max(48, lexicalWindow)
|
|
1906
|
+
});
|
|
1907
|
+
for (const memory of fallbackMemories) {
|
|
1908
|
+
if (!signals.has(memory.id)) {
|
|
1909
|
+
signals.set(memory.id, { memory });
|
|
711
1910
|
}
|
|
712
1911
|
}
|
|
1912
|
+
const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).filter((memory) => input.emotion_tag ? memory.emotion_tag === input.emotion_tag : true).map((memory) => {
|
|
1913
|
+
const signal = signals.get(memory.id) ?? { memory };
|
|
1914
|
+
const memoryTokens = new Set(tokenize(memory.content));
|
|
1915
|
+
const lexicalOverlap = overlapScore(memoryTokens, queryTokens);
|
|
1916
|
+
const taskOverlap = overlapScore(memoryTokens, taskTokens);
|
|
1917
|
+
const lexicalScore = clamp012(
|
|
1918
|
+
0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
|
|
1919
|
+
);
|
|
1920
|
+
const semanticScore = signal.semanticSimilarity !== void 0 ? clamp012(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp012(lexicalOverlap * 0.7) : 0;
|
|
1921
|
+
const intentScore = intentMatch(memory, input.intent);
|
|
1922
|
+
const taskMatch = trimmedTask ? clamp012(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
|
|
1923
|
+
const priorityScore = priorityPrior(memory.priority);
|
|
1924
|
+
const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
|
|
1925
|
+
const feedbackScore = feedbackSummary.score;
|
|
1926
|
+
const score = clamp012(
|
|
1927
|
+
0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
|
|
1928
|
+
);
|
|
1929
|
+
return {
|
|
1930
|
+
memory,
|
|
1931
|
+
score,
|
|
1932
|
+
semantic_score: semanticScore,
|
|
1933
|
+
lexical_score: lexicalScore,
|
|
1934
|
+
task_match: taskMatch,
|
|
1935
|
+
vitality: memory.vitality,
|
|
1936
|
+
priority_prior: priorityScore,
|
|
1937
|
+
feedback_score: feedbackScore,
|
|
1938
|
+
feedback_summary: feedbackSummary,
|
|
1939
|
+
reason_codes: buildReasonCodes({
|
|
1940
|
+
memory,
|
|
1941
|
+
query: semanticQuery || trimmedQuery,
|
|
1942
|
+
task: trimmedTask,
|
|
1943
|
+
intent: input.intent,
|
|
1944
|
+
semanticScore,
|
|
1945
|
+
lexicalScore,
|
|
1946
|
+
taskMatch,
|
|
1947
|
+
feedbackScore
|
|
1948
|
+
}),
|
|
1949
|
+
lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
|
|
1950
|
+
semantic_rank: signal.semanticRank,
|
|
1951
|
+
semantic_similarity: signal.semanticSimilarity
|
|
1952
|
+
};
|
|
1953
|
+
}).sort((left, right) => {
|
|
1954
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1955
|
+
if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
|
|
1956
|
+
if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
|
|
1957
|
+
if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
|
|
1958
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
1959
|
+
}).slice(0, limit);
|
|
713
1960
|
return {
|
|
714
|
-
|
|
715
|
-
|
|
1961
|
+
count: results.length,
|
|
1962
|
+
query: trimmedQuery,
|
|
1963
|
+
task: trimmedTask,
|
|
1964
|
+
intent: input.intent,
|
|
1965
|
+
results
|
|
716
1966
|
};
|
|
717
1967
|
}
|
|
718
1968
|
|
|
1969
|
+
// src/transports/http.ts
|
|
1970
|
+
init_db();
|
|
1971
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1972
|
+
import http from "http";
|
|
1973
|
+
|
|
1974
|
+
// src/sleep/sync.ts
|
|
1975
|
+
init_memory();
|
|
1976
|
+
init_path();
|
|
1977
|
+
|
|
1978
|
+
// src/core/guard.ts
|
|
1979
|
+
init_providers();
|
|
1980
|
+
init_tokenizer();
|
|
1981
|
+
init_path();
|
|
1982
|
+
|
|
1983
|
+
// src/core/merge.ts
|
|
1984
|
+
function uniqueNonEmpty(values) {
|
|
1985
|
+
return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
|
|
1986
|
+
}
|
|
1987
|
+
function splitClauses(content) {
|
|
1988
|
+
return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
|
|
1989
|
+
}
|
|
1990
|
+
function mergeAliases(existing, incoming, content) {
|
|
1991
|
+
const aliases = uniqueNonEmpty([
|
|
1992
|
+
existing !== content ? existing : void 0,
|
|
1993
|
+
incoming !== content ? incoming : void 0
|
|
1994
|
+
]);
|
|
1995
|
+
return aliases.length > 0 ? aliases : void 0;
|
|
1996
|
+
}
|
|
1997
|
+
function replaceIdentity(context) {
|
|
1998
|
+
const content = context.incoming.content.trim();
|
|
1999
|
+
return {
|
|
2000
|
+
strategy: "replace",
|
|
2001
|
+
content,
|
|
2002
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2003
|
+
notes: ["identity canonicalized to the newest authoritative phrasing"]
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
function appendEmotionEvidence(context) {
|
|
2007
|
+
const lines = uniqueNonEmpty([
|
|
2008
|
+
...context.existing.content.split(/\n+/),
|
|
2009
|
+
context.incoming.content
|
|
2010
|
+
]);
|
|
2011
|
+
const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
|
|
2012
|
+
return {
|
|
2013
|
+
strategy: "append_evidence",
|
|
2014
|
+
content,
|
|
2015
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2016
|
+
notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function synthesizeKnowledge(context) {
|
|
2020
|
+
const clauses = uniqueNonEmpty([
|
|
2021
|
+
...splitClauses(context.existing.content),
|
|
2022
|
+
...splitClauses(context.incoming.content)
|
|
2023
|
+
]);
|
|
2024
|
+
const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
|
|
2025
|
+
return {
|
|
2026
|
+
strategy: "synthesize",
|
|
2027
|
+
content,
|
|
2028
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2029
|
+
notes: ["knowledge statements synthesized into a canonical summary"]
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function compactEventTimeline(context) {
|
|
2033
|
+
const points = uniqueNonEmpty([
|
|
2034
|
+
...context.existing.content.split(/\n+/),
|
|
2035
|
+
context.incoming.content
|
|
2036
|
+
]).map((line) => line.replace(/^-\s*/, ""));
|
|
2037
|
+
const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
|
|
2038
|
+
return {
|
|
2039
|
+
strategy: "compact_timeline",
|
|
2040
|
+
content,
|
|
2041
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2042
|
+
notes: ["event observations compacted into a single timeline window"]
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function buildMergePlan(context) {
|
|
2046
|
+
const type = context.incoming.type ?? context.existing.type;
|
|
2047
|
+
switch (type) {
|
|
2048
|
+
case "identity":
|
|
2049
|
+
return replaceIdentity(context);
|
|
2050
|
+
case "emotion":
|
|
2051
|
+
return appendEmotionEvidence(context);
|
|
2052
|
+
case "knowledge":
|
|
2053
|
+
return synthesizeKnowledge(context);
|
|
2054
|
+
case "event":
|
|
2055
|
+
return compactEventTimeline(context);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/core/guard.ts
|
|
2060
|
+
init_memory();
|
|
2061
|
+
var NEAR_EXACT_THRESHOLD = 0.93;
|
|
2062
|
+
var MERGE_THRESHOLD = 0.82;
|
|
2063
|
+
function clamp013(value) {
|
|
2064
|
+
if (!Number.isFinite(value)) return 0;
|
|
2065
|
+
return Math.max(0, Math.min(1, value));
|
|
2066
|
+
}
|
|
2067
|
+
function uniqueTokenSet2(text) {
|
|
2068
|
+
return new Set(tokenize(text));
|
|
2069
|
+
}
|
|
2070
|
+
function overlapScore2(left, right) {
|
|
2071
|
+
const a = new Set(left);
|
|
2072
|
+
const b = new Set(right);
|
|
2073
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
2074
|
+
let shared = 0;
|
|
2075
|
+
for (const token of a) {
|
|
2076
|
+
if (b.has(token)) shared += 1;
|
|
2077
|
+
}
|
|
2078
|
+
return shared / Math.max(a.size, b.size);
|
|
2079
|
+
}
|
|
2080
|
+
function extractEntities(text) {
|
|
2081
|
+
const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
|
|
2082
|
+
return new Set(matches.map((value) => value.trim()).filter(Boolean));
|
|
2083
|
+
}
|
|
2084
|
+
function safeDomain(uri) {
|
|
2085
|
+
if (!uri) return null;
|
|
2086
|
+
try {
|
|
2087
|
+
return parseUri(uri).domain;
|
|
2088
|
+
} catch {
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function getPrimaryUri(db, memoryId, agentId) {
|
|
2093
|
+
const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
|
|
2094
|
+
return row?.uri ?? null;
|
|
2095
|
+
}
|
|
2096
|
+
function uriScopeMatch(inputUri, candidateUri) {
|
|
2097
|
+
if (inputUri && candidateUri) {
|
|
2098
|
+
if (inputUri === candidateUri) return 1;
|
|
2099
|
+
const inputDomain2 = safeDomain(inputUri);
|
|
2100
|
+
const candidateDomain2 = safeDomain(candidateUri);
|
|
2101
|
+
if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
|
|
2102
|
+
return 0;
|
|
2103
|
+
}
|
|
2104
|
+
if (!inputUri && !candidateUri) {
|
|
2105
|
+
return 0.65;
|
|
2106
|
+
}
|
|
2107
|
+
const inputDomain = safeDomain(inputUri ?? null);
|
|
2108
|
+
const candidateDomain = safeDomain(candidateUri ?? null);
|
|
2109
|
+
if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
|
|
2110
|
+
return 0.75;
|
|
2111
|
+
}
|
|
2112
|
+
return 0.2;
|
|
2113
|
+
}
|
|
2114
|
+
function extractObservedAt(parts, fallback) {
|
|
2115
|
+
for (const part of parts) {
|
|
2116
|
+
if (!part) continue;
|
|
2117
|
+
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
2118
|
+
if (!match) continue;
|
|
2119
|
+
const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
|
|
2120
|
+
const parsed = new Date(iso);
|
|
2121
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
2122
|
+
return parsed;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (fallback) {
|
|
2126
|
+
const parsed = new Date(fallback);
|
|
2127
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
2128
|
+
return parsed;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
function timeProximity(input, memory, candidateUri) {
|
|
2134
|
+
if (input.type !== "event") {
|
|
2135
|
+
return 1;
|
|
2136
|
+
}
|
|
2137
|
+
const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
|
|
2138
|
+
const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
|
|
2139
|
+
if (!inputTime || !existingTime) {
|
|
2140
|
+
return 0.5;
|
|
2141
|
+
}
|
|
2142
|
+
const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2143
|
+
return clamp013(1 - diffDays / 7);
|
|
2144
|
+
}
|
|
2145
|
+
function scoreCandidate(input, candidate, candidateUri) {
|
|
2146
|
+
const lexicalOverlap = overlapScore2(uniqueTokenSet2(input.content), uniqueTokenSet2(candidate.memory.content));
|
|
2147
|
+
const entityOverlap = Math.max(
|
|
2148
|
+
overlapScore2(extractEntities(input.content), extractEntities(candidate.memory.content)),
|
|
2149
|
+
lexicalOverlap * 0.75
|
|
2150
|
+
);
|
|
2151
|
+
const uriMatch = uriScopeMatch(input.uri, candidateUri);
|
|
2152
|
+
const temporal = timeProximity(input, candidate.memory, candidateUri);
|
|
2153
|
+
const semantic = clamp013(candidate.vector_score ?? lexicalOverlap);
|
|
2154
|
+
const dedupScore = clamp013(
|
|
2155
|
+
0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
|
|
2156
|
+
);
|
|
2157
|
+
return {
|
|
2158
|
+
semantic_similarity: semantic,
|
|
2159
|
+
lexical_overlap: lexicalOverlap,
|
|
2160
|
+
uri_scope_match: uriMatch,
|
|
2161
|
+
entity_overlap: entityOverlap,
|
|
2162
|
+
time_proximity: temporal,
|
|
2163
|
+
dedup_score: dedupScore
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
async function recallCandidates(db, input, agentId) {
|
|
2167
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
2168
|
+
const response = await recallMemories(db, input.content, {
|
|
2169
|
+
agent_id: agentId,
|
|
2170
|
+
limit: Math.max(6, input.candidateLimit ?? 8),
|
|
2171
|
+
lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
2172
|
+
vectorLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
2173
|
+
provider,
|
|
2174
|
+
recordAccess: false
|
|
2175
|
+
});
|
|
2176
|
+
return response.results.filter((row) => row.memory.type === input.type).map((row) => {
|
|
2177
|
+
const uri = getPrimaryUri(db, row.memory.id, agentId);
|
|
2178
|
+
return {
|
|
2179
|
+
result: row,
|
|
2180
|
+
uri,
|
|
2181
|
+
domain: safeDomain(uri),
|
|
2182
|
+
score: scoreCandidate(input, row, uri)
|
|
2183
|
+
};
|
|
2184
|
+
}).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
|
|
2185
|
+
}
|
|
2186
|
+
function fourCriterionGate(input) {
|
|
2187
|
+
const content = input.content.trim();
|
|
2188
|
+
const failed = [];
|
|
2189
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
2190
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
2191
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
2192
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
2193
|
+
const tokens = tokenize(content);
|
|
2194
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
2195
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
2196
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
2197
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
2198
|
+
const hasNumbers = /\d+/.test(content);
|
|
2199
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
2200
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
2201
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
2202
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
2203
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
2204
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
2205
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
2206
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
2207
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
2208
|
+
let coherence = 1;
|
|
2209
|
+
if (allCaps) coherence -= 0.5;
|
|
2210
|
+
if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
|
|
2211
|
+
if (excessiveRepetition) coherence -= 0.5;
|
|
2212
|
+
coherence = Math.max(0, coherence);
|
|
2213
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
2214
|
+
return {
|
|
2215
|
+
pass: failed.length === 0,
|
|
2216
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
2217
|
+
failedCriteria: failed
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
async function guard(db, input) {
|
|
2221
|
+
const hash = contentHash(input.content);
|
|
2222
|
+
const agentId = input.agent_id ?? "default";
|
|
2223
|
+
const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
|
|
2224
|
+
if (exactMatch) {
|
|
2225
|
+
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
2226
|
+
}
|
|
2227
|
+
if (input.uri) {
|
|
2228
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
2229
|
+
if (existingPath) {
|
|
2230
|
+
return {
|
|
2231
|
+
action: "update",
|
|
2232
|
+
reason: `URI ${input.uri} already exists, updating canonical content`,
|
|
2233
|
+
existingId: existingPath.memory_id,
|
|
2234
|
+
updatedContent: input.content
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
const gateResult = fourCriterionGate(input);
|
|
2239
|
+
if (!gateResult.pass) {
|
|
2240
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
2241
|
+
}
|
|
2242
|
+
if (input.conservative) {
|
|
2243
|
+
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
2244
|
+
}
|
|
2245
|
+
const candidates = await recallCandidates(db, input, agentId);
|
|
2246
|
+
const best = candidates[0];
|
|
2247
|
+
if (!best) {
|
|
2248
|
+
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
2249
|
+
}
|
|
2250
|
+
const score = best.score;
|
|
2251
|
+
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
2252
|
+
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
2253
|
+
return {
|
|
2254
|
+
action: shouldUpdateMetadata ? "update" : "skip",
|
|
2255
|
+
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
2256
|
+
existingId: best.result.memory.id,
|
|
2257
|
+
score
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
2261
|
+
const mergePlan = buildMergePlan({
|
|
2262
|
+
existing: best.result.memory,
|
|
2263
|
+
incoming: {
|
|
2264
|
+
content: input.content,
|
|
2265
|
+
type: input.type,
|
|
2266
|
+
source: input.source
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
return {
|
|
2270
|
+
action: "merge",
|
|
2271
|
+
reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
|
|
2272
|
+
existingId: best.result.memory.id,
|
|
2273
|
+
mergedContent: mergePlan.content,
|
|
2274
|
+
mergePlan,
|
|
2275
|
+
score
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
return {
|
|
2279
|
+
action: "add",
|
|
2280
|
+
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
2281
|
+
score
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/sleep/sync.ts
|
|
2286
|
+
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
2287
|
+
if (!uri) return;
|
|
2288
|
+
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
2289
|
+
try {
|
|
2290
|
+
createPath(db, memoryId, uri, void 0, void 0, agentId);
|
|
2291
|
+
} catch {
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
async function syncOne(db, input) {
|
|
2295
|
+
const memInput = {
|
|
2296
|
+
content: input.content,
|
|
2297
|
+
type: input.type ?? "event",
|
|
2298
|
+
priority: input.priority,
|
|
2299
|
+
emotion_val: input.emotion_val,
|
|
2300
|
+
source: input.source,
|
|
2301
|
+
agent_id: input.agent_id,
|
|
2302
|
+
uri: input.uri,
|
|
2303
|
+
provider: input.provider,
|
|
2304
|
+
conservative: input.conservative,
|
|
2305
|
+
emotion_tag: input.emotion_tag
|
|
2306
|
+
};
|
|
2307
|
+
const guardResult = await guard(db, memInput);
|
|
2308
|
+
switch (guardResult.action) {
|
|
2309
|
+
case "skip":
|
|
2310
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2311
|
+
case "add": {
|
|
2312
|
+
const mem = createMemory(db, memInput);
|
|
2313
|
+
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
2314
|
+
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
2315
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
2316
|
+
}
|
|
2317
|
+
case "update": {
|
|
2318
|
+
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
2319
|
+
if (guardResult.updatedContent !== void 0) {
|
|
2320
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
2321
|
+
}
|
|
2322
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2323
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2324
|
+
}
|
|
2325
|
+
case "merge": {
|
|
2326
|
+
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
2327
|
+
return { action: "skipped", reason: "Missing merge data" };
|
|
2328
|
+
}
|
|
2329
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
2330
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2331
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/app/remember.ts
|
|
2337
|
+
async function rememberMemory(db, input) {
|
|
2338
|
+
return syncOne(db, {
|
|
2339
|
+
content: input.content,
|
|
2340
|
+
type: input.type,
|
|
2341
|
+
priority: input.priority,
|
|
2342
|
+
emotion_val: input.emotion_val,
|
|
2343
|
+
uri: input.uri,
|
|
2344
|
+
source: input.source,
|
|
2345
|
+
agent_id: input.agent_id,
|
|
2346
|
+
provider: input.provider,
|
|
2347
|
+
conservative: input.conservative,
|
|
2348
|
+
emotion_tag: input.emotion_tag
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/app/recall.ts
|
|
2353
|
+
async function recallMemory(db, input) {
|
|
2354
|
+
const result = await recallMemories(db, input.query, {
|
|
2355
|
+
agent_id: input.agent_id,
|
|
2356
|
+
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
2357
|
+
min_vitality: input.min_vitality,
|
|
2358
|
+
lexicalLimit: input.lexicalLimit,
|
|
2359
|
+
vectorLimit: input.vectorLimit,
|
|
2360
|
+
provider: input.provider,
|
|
2361
|
+
recordAccess: input.recordAccess
|
|
2362
|
+
});
|
|
2363
|
+
if (input.emotion_tag) {
|
|
2364
|
+
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
2365
|
+
}
|
|
2366
|
+
return result;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
719
2369
|
// src/sleep/decay.ts
|
|
2370
|
+
init_db();
|
|
720
2371
|
var MIN_VITALITY = {
|
|
721
2372
|
0: 1,
|
|
722
2373
|
// P0: identity — never decays
|
|
@@ -777,35 +2428,102 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
777
2428
|
}
|
|
778
2429
|
|
|
779
2430
|
// src/sleep/tidy.ts
|
|
2431
|
+
init_memory();
|
|
780
2432
|
function runTidy(db, opts) {
|
|
781
2433
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
782
2434
|
const agentId = opts?.agent_id;
|
|
783
2435
|
let archived = 0;
|
|
784
|
-
let orphansCleaned = 0;
|
|
785
2436
|
const transaction = db.transaction(() => {
|
|
786
2437
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
787
2438
|
for (const mem of decayed) {
|
|
788
2439
|
deleteMemory(db, mem.id);
|
|
789
|
-
archived
|
|
2440
|
+
archived += 1;
|
|
790
2441
|
}
|
|
791
|
-
const orphans = agentId ? db.prepare(
|
|
792
|
-
`DELETE FROM paths
|
|
793
|
-
WHERE agent_id = ?
|
|
794
|
-
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
795
|
-
).run(agentId, agentId) : db.prepare(
|
|
796
|
-
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
797
|
-
).run();
|
|
798
|
-
orphansCleaned = orphans.changes;
|
|
799
2442
|
});
|
|
800
2443
|
transaction();
|
|
801
|
-
return { archived, orphansCleaned };
|
|
2444
|
+
return { archived, orphansCleaned: 0 };
|
|
802
2445
|
}
|
|
803
2446
|
|
|
804
2447
|
// src/sleep/govern.ts
|
|
2448
|
+
init_memory();
|
|
2449
|
+
init_tokenizer();
|
|
2450
|
+
function clamp014(value) {
|
|
2451
|
+
if (!Number.isFinite(value)) return 0;
|
|
2452
|
+
return Math.max(0, Math.min(1, value));
|
|
2453
|
+
}
|
|
2454
|
+
function overlapScore3(left, right) {
|
|
2455
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
2456
|
+
let shared = 0;
|
|
2457
|
+
for (const token of left) {
|
|
2458
|
+
if (right.has(token)) shared += 1;
|
|
2459
|
+
}
|
|
2460
|
+
return shared / Math.max(left.size, right.size);
|
|
2461
|
+
}
|
|
2462
|
+
function feedbackPenalty(db, memoryId) {
|
|
2463
|
+
try {
|
|
2464
|
+
const row = db.prepare(
|
|
2465
|
+
`SELECT COUNT(*) as count, COALESCE(AVG(value), 0) as avgValue
|
|
2466
|
+
FROM feedback_events
|
|
2467
|
+
WHERE memory_id = ?`
|
|
2468
|
+
).get(memoryId);
|
|
2469
|
+
if (!row || row.count === 0) return 1;
|
|
2470
|
+
return clamp014(1 - row.avgValue);
|
|
2471
|
+
} catch {
|
|
2472
|
+
return 1;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function ageScore(memory, referenceMs = Date.now()) {
|
|
2476
|
+
const createdAt = new Date(memory.created_at).getTime();
|
|
2477
|
+
if (Number.isNaN(createdAt)) return 0;
|
|
2478
|
+
const ageDays = Math.max(0, (referenceMs - createdAt) / (1e3 * 60 * 60 * 24));
|
|
2479
|
+
return clamp014(ageDays / 180);
|
|
2480
|
+
}
|
|
2481
|
+
function computeEvictionScore(input) {
|
|
2482
|
+
return clamp014(
|
|
2483
|
+
0.4 * (1 - clamp014(input.vitality)) + 0.2 * clamp014(input.redundancy_score) + 0.2 * clamp014(input.age_score) + 0.1 * clamp014(input.low_feedback_penalty) + 0.1 * clamp014(input.low_priority_penalty)
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
function rankEvictionCandidates(db, opts) {
|
|
2487
|
+
const agentId = opts?.agent_id;
|
|
2488
|
+
const rows = db.prepare(
|
|
2489
|
+
agentId ? `SELECT * FROM memories WHERE agent_id = ? AND priority > 0 AND TRIM(content) != ''` : `SELECT * FROM memories WHERE priority > 0 AND TRIM(content) != ''`
|
|
2490
|
+
).all(...agentId ? [agentId] : []);
|
|
2491
|
+
const tokenSets = new Map(rows.map((memory) => [memory.id, new Set(tokenize(memory.content))]));
|
|
2492
|
+
return rows.map((memory) => {
|
|
2493
|
+
const ownTokens = tokenSets.get(memory.id) ?? /* @__PURE__ */ new Set();
|
|
2494
|
+
const redundancy = rows.filter((candidate2) => candidate2.id !== memory.id && candidate2.type === memory.type).reduce((maxOverlap, candidate2) => {
|
|
2495
|
+
const candidateTokens = tokenSets.get(candidate2.id) ?? /* @__PURE__ */ new Set();
|
|
2496
|
+
return Math.max(maxOverlap, overlapScore3(ownTokens, candidateTokens));
|
|
2497
|
+
}, 0);
|
|
2498
|
+
const candidate = {
|
|
2499
|
+
memory,
|
|
2500
|
+
redundancy_score: redundancy,
|
|
2501
|
+
age_score: ageScore(memory),
|
|
2502
|
+
low_feedback_penalty: feedbackPenalty(db, memory.id),
|
|
2503
|
+
low_priority_penalty: clamp014(memory.priority / 3),
|
|
2504
|
+
eviction_score: 0
|
|
2505
|
+
};
|
|
2506
|
+
candidate.eviction_score = computeEvictionScore({
|
|
2507
|
+
vitality: memory.vitality,
|
|
2508
|
+
redundancy_score: candidate.redundancy_score,
|
|
2509
|
+
age_score: candidate.age_score,
|
|
2510
|
+
low_feedback_penalty: candidate.low_feedback_penalty,
|
|
2511
|
+
low_priority_penalty: candidate.low_priority_penalty
|
|
2512
|
+
});
|
|
2513
|
+
return candidate;
|
|
2514
|
+
}).sort((left, right) => {
|
|
2515
|
+
if (right.eviction_score !== left.eviction_score) {
|
|
2516
|
+
return right.eviction_score - left.eviction_score;
|
|
2517
|
+
}
|
|
2518
|
+
return left.memory.priority - right.memory.priority;
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
805
2521
|
function runGovern(db, opts) {
|
|
806
2522
|
const agentId = opts?.agent_id;
|
|
2523
|
+
const maxMemories = opts?.maxMemories ?? 200;
|
|
807
2524
|
let orphanPaths = 0;
|
|
808
2525
|
let emptyMemories = 0;
|
|
2526
|
+
let evicted = 0;
|
|
809
2527
|
const transaction = db.transaction(() => {
|
|
810
2528
|
const pathResult = agentId ? db.prepare(
|
|
811
2529
|
`DELETE FROM paths
|
|
@@ -815,154 +2533,750 @@ function runGovern(db, opts) {
|
|
|
815
2533
|
orphanPaths = pathResult.changes;
|
|
816
2534
|
const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
817
2535
|
emptyMemories = emptyResult.changes;
|
|
2536
|
+
const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
|
|
2537
|
+
const excess = Math.max(0, total - maxMemories);
|
|
2538
|
+
if (excess <= 0) return;
|
|
2539
|
+
const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
|
|
2540
|
+
for (const candidate of candidates) {
|
|
2541
|
+
deleteMemory(db, candidate.memory.id);
|
|
2542
|
+
evicted += 1;
|
|
2543
|
+
}
|
|
818
2544
|
});
|
|
819
2545
|
transaction();
|
|
820
|
-
return { orphanPaths, emptyMemories };
|
|
2546
|
+
return { orphanPaths, emptyMemories, evicted };
|
|
821
2547
|
}
|
|
822
2548
|
|
|
823
|
-
// src/
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
2549
|
+
// src/sleep/jobs.ts
|
|
2550
|
+
init_db();
|
|
2551
|
+
function parseCheckpoint(raw) {
|
|
2552
|
+
if (!raw) return null;
|
|
2553
|
+
try {
|
|
2554
|
+
return JSON.parse(raw);
|
|
2555
|
+
} catch {
|
|
2556
|
+
return null;
|
|
830
2557
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
2558
|
+
}
|
|
2559
|
+
function serializeCheckpoint(checkpoint) {
|
|
2560
|
+
if (!checkpoint) return null;
|
|
2561
|
+
return JSON.stringify(checkpoint);
|
|
2562
|
+
}
|
|
2563
|
+
function toJob(row) {
|
|
2564
|
+
if (!row) return null;
|
|
2565
|
+
return {
|
|
2566
|
+
...row,
|
|
2567
|
+
checkpoint: parseCheckpoint(row.checkpoint)
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
function createInitialCheckpoint(phase) {
|
|
2571
|
+
return {
|
|
2572
|
+
requestedPhase: phase,
|
|
2573
|
+
nextPhase: phase === "all" ? "decay" : phase,
|
|
2574
|
+
completedPhases: [],
|
|
2575
|
+
phaseResults: {}
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function createMaintenanceJob(db, phase, checkpoint = createInitialCheckpoint(phase)) {
|
|
2579
|
+
const jobId = newId();
|
|
2580
|
+
const startedAt = now();
|
|
2581
|
+
db.prepare(
|
|
2582
|
+
`INSERT INTO maintenance_jobs (job_id, phase, status, checkpoint, error, started_at, finished_at)
|
|
2583
|
+
VALUES (?, ?, 'running', ?, NULL, ?, NULL)`
|
|
2584
|
+
).run(jobId, phase, serializeCheckpoint(checkpoint), startedAt);
|
|
2585
|
+
return getMaintenanceJob(db, jobId);
|
|
2586
|
+
}
|
|
2587
|
+
function getMaintenanceJob(db, jobId) {
|
|
2588
|
+
const row = db.prepare("SELECT * FROM maintenance_jobs WHERE job_id = ?").get(jobId);
|
|
2589
|
+
return toJob(row);
|
|
2590
|
+
}
|
|
2591
|
+
function findResumableMaintenanceJob(db, phase) {
|
|
2592
|
+
const row = db.prepare(
|
|
2593
|
+
`SELECT *
|
|
2594
|
+
FROM maintenance_jobs
|
|
2595
|
+
WHERE phase = ?
|
|
2596
|
+
AND status IN ('running', 'failed')
|
|
2597
|
+
ORDER BY started_at DESC
|
|
2598
|
+
LIMIT 1`
|
|
2599
|
+
).get(phase);
|
|
2600
|
+
return toJob(row);
|
|
2601
|
+
}
|
|
2602
|
+
function updateMaintenanceCheckpoint(db, jobId, checkpoint) {
|
|
2603
|
+
db.prepare(
|
|
2604
|
+
`UPDATE maintenance_jobs
|
|
2605
|
+
SET checkpoint = ?,
|
|
2606
|
+
error = NULL,
|
|
2607
|
+
finished_at = NULL,
|
|
2608
|
+
status = 'running'
|
|
2609
|
+
WHERE job_id = ?`
|
|
2610
|
+
).run(serializeCheckpoint(checkpoint), jobId);
|
|
2611
|
+
return getMaintenanceJob(db, jobId);
|
|
2612
|
+
}
|
|
2613
|
+
function failMaintenanceJob(db, jobId, error, checkpoint) {
|
|
2614
|
+
db.prepare(
|
|
2615
|
+
`UPDATE maintenance_jobs
|
|
2616
|
+
SET status = 'failed',
|
|
2617
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
2618
|
+
error = ?,
|
|
2619
|
+
finished_at = ?
|
|
2620
|
+
WHERE job_id = ?`
|
|
2621
|
+
).run(serializeCheckpoint(checkpoint), error, now(), jobId);
|
|
2622
|
+
return getMaintenanceJob(db, jobId);
|
|
2623
|
+
}
|
|
2624
|
+
function completeMaintenanceJob(db, jobId, checkpoint) {
|
|
2625
|
+
db.prepare(
|
|
2626
|
+
`UPDATE maintenance_jobs
|
|
2627
|
+
SET status = 'completed',
|
|
2628
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
2629
|
+
error = NULL,
|
|
2630
|
+
finished_at = ?
|
|
2631
|
+
WHERE job_id = ?`
|
|
2632
|
+
).run(serializeCheckpoint(checkpoint), now(), jobId);
|
|
2633
|
+
return getMaintenanceJob(db, jobId);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/sleep/orchestrator.ts
|
|
2637
|
+
var DEFAULT_RUNNERS = {
|
|
2638
|
+
decay: (db, opts) => runDecay(db, opts),
|
|
2639
|
+
tidy: (db, opts) => runTidy(db, opts),
|
|
2640
|
+
govern: (db, opts) => runGovern(db, opts)
|
|
2641
|
+
};
|
|
2642
|
+
var PHASE_SEQUENCE = ["decay", "tidy", "govern"];
|
|
2643
|
+
function getSummaryStats(db, agentId) {
|
|
2644
|
+
const row = agentId ? db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories WHERE agent_id = ?").get(agentId) : db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories").get();
|
|
2645
|
+
return {
|
|
2646
|
+
total: row.total,
|
|
2647
|
+
avgVitality: row.avg
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
function getPhaseSequence(phase) {
|
|
2651
|
+
return phase === "all" ? [...PHASE_SEQUENCE] : [phase];
|
|
2652
|
+
}
|
|
2653
|
+
function resolveJob(db, opts) {
|
|
2654
|
+
if (opts.jobId) {
|
|
2655
|
+
const job = getMaintenanceJob(db, opts.jobId);
|
|
2656
|
+
if (!job) {
|
|
2657
|
+
throw new Error(`Maintenance job not found: ${opts.jobId}`);
|
|
2658
|
+
}
|
|
2659
|
+
if (job.phase !== opts.phase) {
|
|
2660
|
+
throw new Error(`Maintenance job ${opts.jobId} phase mismatch: expected ${opts.phase}, got ${job.phase}`);
|
|
839
2661
|
}
|
|
2662
|
+
return { job, resumed: true };
|
|
840
2663
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const similar = db.prepare(
|
|
846
|
-
`SELECT m.id, m.content, m.type, rank
|
|
847
|
-
FROM memories_fts f
|
|
848
|
-
JOIN memories m ON m.id = f.id
|
|
849
|
-
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
850
|
-
ORDER BY rank
|
|
851
|
-
LIMIT 3`
|
|
852
|
-
).all(ftsQuery, agentId);
|
|
853
|
-
if (similar.length > 0) {
|
|
854
|
-
const topRank = Math.abs(similar[0].rank);
|
|
855
|
-
const tokenCount = ftsTokens.length;
|
|
856
|
-
const dynamicThreshold = tokenCount * 1.5;
|
|
857
|
-
if (topRank > dynamicThreshold) {
|
|
858
|
-
const existing = similar[0];
|
|
859
|
-
if (existing.type === input.type) {
|
|
860
|
-
const merged = `${existing.content}
|
|
861
|
-
|
|
862
|
-
[Updated] ${input.content}`;
|
|
863
|
-
return {
|
|
864
|
-
action: "merge",
|
|
865
|
-
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
866
|
-
existingId: existing.id,
|
|
867
|
-
mergedContent: merged
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
} catch {
|
|
2664
|
+
if (opts.resume !== false) {
|
|
2665
|
+
const resumable = findResumableMaintenanceJob(db, opts.phase);
|
|
2666
|
+
if (resumable) {
|
|
2667
|
+
return { job: resumable, resumed: true };
|
|
873
2668
|
}
|
|
874
2669
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
2670
|
+
return {
|
|
2671
|
+
job: createMaintenanceJob(db, opts.phase),
|
|
2672
|
+
resumed: false
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
function nextPhase(current, requested) {
|
|
2676
|
+
if (requested !== "all") return null;
|
|
2677
|
+
const index = PHASE_SEQUENCE.indexOf(current);
|
|
2678
|
+
return PHASE_SEQUENCE[index + 1] ?? null;
|
|
2679
|
+
}
|
|
2680
|
+
async function runReflectOrchestrator(db, opts) {
|
|
2681
|
+
const runners = {
|
|
2682
|
+
...DEFAULT_RUNNERS,
|
|
2683
|
+
...opts.runners
|
|
2684
|
+
};
|
|
2685
|
+
const before = getSummaryStats(db, opts.agent_id);
|
|
2686
|
+
const { job: baseJob, resumed } = resolveJob(db, opts);
|
|
2687
|
+
let checkpoint = baseJob.checkpoint ?? createInitialCheckpoint(opts.phase);
|
|
2688
|
+
const jobId = baseJob.job_id;
|
|
2689
|
+
const orderedPhases = getPhaseSequence(opts.phase);
|
|
2690
|
+
const startPhase = checkpoint.nextPhase ?? orderedPhases[orderedPhases.length - 1] ?? "decay";
|
|
2691
|
+
const startIndex = Math.max(0, orderedPhases.indexOf(startPhase));
|
|
2692
|
+
const phasesToRun = checkpoint.nextPhase === null ? [] : orderedPhases.slice(startIndex);
|
|
2693
|
+
const totalPhases = Math.max(orderedPhases.length, 1);
|
|
2694
|
+
opts.onProgress?.({
|
|
2695
|
+
status: "started",
|
|
2696
|
+
phase: opts.phase,
|
|
2697
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2698
|
+
jobId,
|
|
2699
|
+
detail: {
|
|
2700
|
+
resumed,
|
|
2701
|
+
nextPhase: checkpoint.nextPhase
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
try {
|
|
2705
|
+
for (const phase of phasesToRun) {
|
|
2706
|
+
const result = await Promise.resolve(runners[phase](db, { agent_id: opts.agent_id }));
|
|
2707
|
+
checkpoint = {
|
|
2708
|
+
...checkpoint,
|
|
2709
|
+
completedPhases: [.../* @__PURE__ */ new Set([...checkpoint.completedPhases, phase])],
|
|
2710
|
+
phaseResults: {
|
|
2711
|
+
...checkpoint.phaseResults,
|
|
2712
|
+
[phase]: result
|
|
2713
|
+
},
|
|
2714
|
+
nextPhase: nextPhase(phase, opts.phase)
|
|
2715
|
+
};
|
|
2716
|
+
updateMaintenanceCheckpoint(db, jobId, checkpoint);
|
|
2717
|
+
opts.onProgress?.({
|
|
2718
|
+
status: "phase-completed",
|
|
2719
|
+
phase,
|
|
2720
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2721
|
+
jobId,
|
|
2722
|
+
detail: result
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2727
|
+
const failed = failMaintenanceJob(db, jobId, message, checkpoint) ?? baseJob;
|
|
2728
|
+
opts.onProgress?.({
|
|
2729
|
+
status: "failed",
|
|
2730
|
+
phase: checkpoint.nextPhase ?? opts.phase,
|
|
2731
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2732
|
+
jobId,
|
|
2733
|
+
detail: { error: message }
|
|
2734
|
+
});
|
|
2735
|
+
throw Object.assign(new Error(message), { job: failed, checkpoint });
|
|
878
2736
|
}
|
|
879
|
-
|
|
2737
|
+
const completedCheckpoint = {
|
|
2738
|
+
...checkpoint,
|
|
2739
|
+
nextPhase: null
|
|
2740
|
+
};
|
|
2741
|
+
const job = completeMaintenanceJob(db, jobId, completedCheckpoint) ?? baseJob;
|
|
2742
|
+
const after = getSummaryStats(db, opts.agent_id);
|
|
2743
|
+
opts.onProgress?.({
|
|
2744
|
+
status: "completed",
|
|
2745
|
+
phase: opts.phase,
|
|
2746
|
+
progress: 1,
|
|
2747
|
+
jobId,
|
|
2748
|
+
detail: completedCheckpoint.phaseResults
|
|
2749
|
+
});
|
|
2750
|
+
return {
|
|
2751
|
+
job,
|
|
2752
|
+
jobId,
|
|
2753
|
+
phase: opts.phase,
|
|
2754
|
+
resumed,
|
|
2755
|
+
checkpoint: completedCheckpoint,
|
|
2756
|
+
results: completedCheckpoint.phaseResults,
|
|
2757
|
+
before,
|
|
2758
|
+
after
|
|
2759
|
+
};
|
|
880
2760
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
2761
|
+
|
|
2762
|
+
// src/app/reflect.ts
|
|
2763
|
+
async function reflectMemories(db, input) {
|
|
2764
|
+
const options = {
|
|
2765
|
+
phase: input.phase,
|
|
2766
|
+
agent_id: input.agent_id,
|
|
2767
|
+
jobId: input.jobId,
|
|
2768
|
+
resume: input.resume,
|
|
2769
|
+
runners: input.runners,
|
|
2770
|
+
onProgress: input.onProgress
|
|
2771
|
+
};
|
|
2772
|
+
return runReflectOrchestrator(db, options);
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/app/status.ts
|
|
2776
|
+
init_memory();
|
|
2777
|
+
function getMemoryStatus(db, input) {
|
|
2778
|
+
const agentId = input?.agent_id ?? "default";
|
|
2779
|
+
const stats = countMemories(db, agentId);
|
|
2780
|
+
const lowVitality = db.prepare(
|
|
2781
|
+
"SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?"
|
|
2782
|
+
).get(agentId);
|
|
2783
|
+
const totalPaths = db.prepare(
|
|
2784
|
+
"SELECT COUNT(*) as c FROM paths WHERE agent_id = ?"
|
|
2785
|
+
).get(agentId);
|
|
2786
|
+
const feedbackEvents = db.prepare(
|
|
2787
|
+
"SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
|
|
2788
|
+
).get(agentId);
|
|
2789
|
+
return {
|
|
2790
|
+
...stats,
|
|
2791
|
+
paths: totalPaths.c,
|
|
2792
|
+
low_vitality: lowVitality.c,
|
|
2793
|
+
feedback_events: feedbackEvents.c,
|
|
2794
|
+
agent_id: agentId
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// src/app/reindex.ts
|
|
2799
|
+
async function reindexMemories(db, input) {
|
|
2800
|
+
input?.onProgress?.({ status: "started", stage: "fts", progress: 0 });
|
|
2801
|
+
try {
|
|
2802
|
+
const fts = rebuildBm25Index(db, { agent_id: input?.agent_id });
|
|
2803
|
+
input?.onProgress?.({
|
|
2804
|
+
status: "stage-completed",
|
|
2805
|
+
stage: "fts",
|
|
2806
|
+
progress: 0.5,
|
|
2807
|
+
detail: fts
|
|
2808
|
+
});
|
|
2809
|
+
const embeddings = await reindexEmbeddings(db, {
|
|
2810
|
+
agent_id: input?.agent_id,
|
|
2811
|
+
provider: input?.provider,
|
|
2812
|
+
force: input?.force,
|
|
2813
|
+
batchSize: input?.batchSize
|
|
2814
|
+
});
|
|
2815
|
+
input?.onProgress?.({
|
|
2816
|
+
status: "stage-completed",
|
|
2817
|
+
stage: "embeddings",
|
|
2818
|
+
progress: 0.9,
|
|
2819
|
+
detail: embeddings
|
|
2820
|
+
});
|
|
2821
|
+
const result = { fts, embeddings };
|
|
2822
|
+
input?.onProgress?.({
|
|
2823
|
+
status: "completed",
|
|
2824
|
+
stage: "done",
|
|
2825
|
+
progress: 1,
|
|
2826
|
+
detail: result
|
|
2827
|
+
});
|
|
2828
|
+
return result;
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
input?.onProgress?.({
|
|
2831
|
+
status: "failed",
|
|
2832
|
+
stage: "done",
|
|
2833
|
+
progress: 1,
|
|
2834
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2835
|
+
});
|
|
2836
|
+
throw error;
|
|
906
2837
|
}
|
|
907
|
-
|
|
908
|
-
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// src/transports/http.ts
|
|
2841
|
+
var VALID_MEMORY_TYPES = /* @__PURE__ */ new Set(["identity", "emotion", "knowledge", "event"]);
|
|
2842
|
+
var VALID_PHASES = /* @__PURE__ */ new Set(["decay", "tidy", "govern", "all"]);
|
|
2843
|
+
var VALID_INTENTS = /* @__PURE__ */ new Set(["factual", "preference", "temporal", "planning", "design"]);
|
|
2844
|
+
function json(value) {
|
|
2845
|
+
return JSON.stringify(value);
|
|
2846
|
+
}
|
|
2847
|
+
function now2() {
|
|
2848
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2849
|
+
}
|
|
2850
|
+
function sendJson(res, statusCode, payload) {
|
|
2851
|
+
res.writeHead(statusCode, {
|
|
2852
|
+
"content-type": "application/json; charset=utf-8",
|
|
2853
|
+
"cache-control": "no-store"
|
|
2854
|
+
});
|
|
2855
|
+
res.end(json(payload));
|
|
2856
|
+
}
|
|
2857
|
+
function sendError(res, statusCode, error, details) {
|
|
2858
|
+
sendJson(res, statusCode, { error, details });
|
|
2859
|
+
}
|
|
2860
|
+
function openSse(res) {
|
|
2861
|
+
res.writeHead(200, {
|
|
2862
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
2863
|
+
"cache-control": "no-cache, no-transform",
|
|
2864
|
+
connection: "keep-alive"
|
|
2865
|
+
});
|
|
2866
|
+
res.write(": connected\n\n");
|
|
2867
|
+
}
|
|
2868
|
+
function sendSse(res, event, payload) {
|
|
2869
|
+
if (res.writableEnded) return;
|
|
2870
|
+
res.write(`event: ${event}
|
|
2871
|
+
`);
|
|
2872
|
+
res.write(`data: ${json(payload)}
|
|
2873
|
+
|
|
2874
|
+
`);
|
|
2875
|
+
}
|
|
2876
|
+
async function readJsonBody(req) {
|
|
2877
|
+
const chunks = [];
|
|
2878
|
+
for await (const chunk of req) {
|
|
2879
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
909
2880
|
}
|
|
910
|
-
|
|
911
|
-
|
|
2881
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
2882
|
+
if (!raw) return {};
|
|
2883
|
+
try {
|
|
2884
|
+
const parsed = JSON.parse(raw);
|
|
2885
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2886
|
+
throw new Error("Request body must be a JSON object");
|
|
2887
|
+
}
|
|
2888
|
+
return parsed;
|
|
2889
|
+
} catch (error) {
|
|
2890
|
+
throw new Error(error instanceof Error ? error.message : "Invalid JSON body");
|
|
912
2891
|
}
|
|
913
|
-
|
|
914
|
-
|
|
2892
|
+
}
|
|
2893
|
+
function asString(value) {
|
|
2894
|
+
return typeof value === "string" ? value : void 0;
|
|
2895
|
+
}
|
|
2896
|
+
function asBoolean(value) {
|
|
2897
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2898
|
+
}
|
|
2899
|
+
function asNumber(value) {
|
|
2900
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2901
|
+
}
|
|
2902
|
+
function asStringArray(value) {
|
|
2903
|
+
if (!Array.isArray(value)) return void 0;
|
|
2904
|
+
const result = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
2905
|
+
return result.length > 0 ? result : [];
|
|
2906
|
+
}
|
|
2907
|
+
function wantsSse(req, body) {
|
|
2908
|
+
const accept = req.headers.accept ?? "";
|
|
2909
|
+
return accept.includes("text/event-stream") || body.stream === true;
|
|
2910
|
+
}
|
|
2911
|
+
function formatRecallResponse(result) {
|
|
915
2912
|
return {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
2913
|
+
mode: result.mode,
|
|
2914
|
+
provider_id: result.providerId,
|
|
2915
|
+
used_vector_search: result.usedVectorSearch,
|
|
2916
|
+
count: result.results.length,
|
|
2917
|
+
memories: result.results.map((row) => ({
|
|
2918
|
+
id: row.memory.id,
|
|
2919
|
+
content: row.memory.content,
|
|
2920
|
+
type: row.memory.type,
|
|
2921
|
+
priority: row.memory.priority,
|
|
2922
|
+
vitality: row.memory.vitality,
|
|
2923
|
+
score: row.score,
|
|
2924
|
+
bm25_rank: row.bm25_rank,
|
|
2925
|
+
vector_rank: row.vector_rank,
|
|
2926
|
+
bm25_score: row.bm25_score,
|
|
2927
|
+
vector_score: row.vector_score,
|
|
2928
|
+
updated_at: row.memory.updated_at
|
|
2929
|
+
}))
|
|
919
2930
|
};
|
|
920
2931
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
2932
|
+
function formatSurfaceResponse(result) {
|
|
2933
|
+
return {
|
|
2934
|
+
count: result.count,
|
|
2935
|
+
query: result.query,
|
|
2936
|
+
task: result.task,
|
|
2937
|
+
intent: result.intent,
|
|
2938
|
+
results: result.results.map((row) => ({
|
|
2939
|
+
id: row.memory.id,
|
|
2940
|
+
content: row.memory.content,
|
|
2941
|
+
type: row.memory.type,
|
|
2942
|
+
priority: row.memory.priority,
|
|
2943
|
+
vitality: row.memory.vitality,
|
|
2944
|
+
score: row.score,
|
|
2945
|
+
semantic_score: row.semantic_score,
|
|
2946
|
+
lexical_score: row.lexical_score,
|
|
2947
|
+
task_match: row.task_match,
|
|
2948
|
+
priority_prior: row.priority_prior,
|
|
2949
|
+
feedback_score: row.feedback_score,
|
|
2950
|
+
feedback_summary: row.feedback_summary,
|
|
2951
|
+
reason_codes: row.reason_codes,
|
|
2952
|
+
updated_at: row.memory.updated_at
|
|
2953
|
+
}))
|
|
932
2954
|
};
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
2955
|
+
}
|
|
2956
|
+
function createJob(jobs, kind, agentId) {
|
|
2957
|
+
const job = {
|
|
2958
|
+
id: randomUUID2(),
|
|
2959
|
+
kind,
|
|
2960
|
+
status: "running",
|
|
2961
|
+
stage: "queued",
|
|
2962
|
+
progress: 0,
|
|
2963
|
+
agent_id: agentId,
|
|
2964
|
+
started_at: now2(),
|
|
2965
|
+
finished_at: null
|
|
2966
|
+
};
|
|
2967
|
+
jobs.set(job.id, job);
|
|
2968
|
+
return job;
|
|
2969
|
+
}
|
|
2970
|
+
function updateJob(job, patch) {
|
|
2971
|
+
Object.assign(job, patch);
|
|
2972
|
+
return job;
|
|
2973
|
+
}
|
|
2974
|
+
function createHttpServer(options) {
|
|
2975
|
+
const ownsDb = !options?.db;
|
|
2976
|
+
const db = options?.db ?? openDatabase({ path: options?.dbPath ?? process.env.AGENT_MEMORY_DB ?? "./agent-memory.db" });
|
|
2977
|
+
const defaultAgentId = options?.agentId ?? process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
2978
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
2979
|
+
const executeReflectJob = async (job, body, stream) => {
|
|
2980
|
+
const phase = asString(body.phase) ?? "all";
|
|
2981
|
+
if (!VALID_PHASES.has(phase)) {
|
|
2982
|
+
throw new Error(`Invalid phase: ${String(body.phase)}`);
|
|
2983
|
+
}
|
|
2984
|
+
updateJob(job, { stage: phase, progress: 0.01 });
|
|
2985
|
+
try {
|
|
2986
|
+
const result = await reflectMemories(db, {
|
|
2987
|
+
phase,
|
|
2988
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2989
|
+
runners: options?.reflectRunners,
|
|
2990
|
+
onProgress: (event) => {
|
|
2991
|
+
updateJob(job, {
|
|
2992
|
+
stage: String(event.phase),
|
|
2993
|
+
progress: event.progress,
|
|
2994
|
+
backend_job_id: event.jobId ?? job.backend_job_id
|
|
2995
|
+
});
|
|
2996
|
+
if (stream) {
|
|
2997
|
+
sendSse(stream, "progress", {
|
|
2998
|
+
job,
|
|
2999
|
+
event
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
944
3002
|
}
|
|
945
|
-
}
|
|
946
|
-
|
|
3003
|
+
});
|
|
3004
|
+
updateJob(job, {
|
|
3005
|
+
status: "completed",
|
|
3006
|
+
stage: "done",
|
|
3007
|
+
progress: 1,
|
|
3008
|
+
backend_job_id: result.jobId,
|
|
3009
|
+
finished_at: now2(),
|
|
3010
|
+
result
|
|
3011
|
+
});
|
|
3012
|
+
return { job, result };
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
updateJob(job, {
|
|
3015
|
+
status: "failed",
|
|
3016
|
+
stage: "failed",
|
|
3017
|
+
progress: 1,
|
|
3018
|
+
finished_at: now2(),
|
|
3019
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3020
|
+
});
|
|
3021
|
+
throw error;
|
|
947
3022
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
3023
|
+
};
|
|
3024
|
+
const executeReindexJob = async (job, body, stream) => {
|
|
3025
|
+
updateJob(job, { stage: "fts", progress: 0.01 });
|
|
3026
|
+
try {
|
|
3027
|
+
const result = await reindexMemories(db, {
|
|
3028
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
3029
|
+
provider: options?.provider,
|
|
3030
|
+
force: asBoolean(body.full) ?? false,
|
|
3031
|
+
batchSize: asNumber(body.batch_size) ?? 16,
|
|
3032
|
+
onProgress: (event) => {
|
|
3033
|
+
updateJob(job, {
|
|
3034
|
+
stage: event.stage,
|
|
3035
|
+
progress: event.progress
|
|
3036
|
+
});
|
|
3037
|
+
if (stream) {
|
|
3038
|
+
sendSse(stream, "progress", {
|
|
3039
|
+
job,
|
|
3040
|
+
event
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
3045
|
+
updateJob(job, {
|
|
3046
|
+
status: "completed",
|
|
3047
|
+
stage: "done",
|
|
3048
|
+
progress: 1,
|
|
3049
|
+
finished_at: now2(),
|
|
3050
|
+
result
|
|
3051
|
+
});
|
|
3052
|
+
return { job, result };
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
updateJob(job, {
|
|
3055
|
+
status: "failed",
|
|
3056
|
+
stage: "failed",
|
|
3057
|
+
progress: 1,
|
|
3058
|
+
finished_at: now2(),
|
|
3059
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3060
|
+
});
|
|
3061
|
+
throw error;
|
|
952
3062
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
3063
|
+
};
|
|
3064
|
+
const server = http.createServer(async (req, res) => {
|
|
3065
|
+
try {
|
|
3066
|
+
const method = req.method ?? "GET";
|
|
3067
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
3068
|
+
const pathname = url.pathname;
|
|
3069
|
+
if (method === "GET" && pathname === "/health") {
|
|
3070
|
+
sendJson(res, 200, {
|
|
3071
|
+
ok: true,
|
|
3072
|
+
service: "agent-memory",
|
|
3073
|
+
time: now2()
|
|
3074
|
+
});
|
|
3075
|
+
return;
|
|
956
3076
|
}
|
|
957
|
-
|
|
958
|
-
|
|
3077
|
+
if (method === "GET" && pathname === "/v1/status") {
|
|
3078
|
+
const agentId = url.searchParams.get("agent_id") ?? defaultAgentId;
|
|
3079
|
+
sendJson(res, 200, getMemoryStatus(db, { agent_id: agentId }));
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
if (method === "GET" && pathname.startsWith("/v1/jobs/")) {
|
|
3083
|
+
const id = decodeURIComponent(pathname.slice("/v1/jobs/".length));
|
|
3084
|
+
const job = jobs.get(id);
|
|
3085
|
+
if (job) {
|
|
3086
|
+
sendJson(res, 200, job);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
const maintenanceJob = getMaintenanceJob(db, id);
|
|
3090
|
+
if (maintenanceJob) {
|
|
3091
|
+
sendJson(res, 200, maintenanceJob);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
sendError(res, 404, `Job not found: ${id}`);
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (method !== "POST") {
|
|
3098
|
+
sendError(res, 404, `Route not found: ${method} ${pathname}`);
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const body = await readJsonBody(req);
|
|
3102
|
+
if (pathname === "/v1/memories") {
|
|
3103
|
+
const content = asString(body.content)?.trim();
|
|
3104
|
+
if (!content) {
|
|
3105
|
+
sendError(res, 400, "content is required");
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const type = asString(body.type) ?? "knowledge";
|
|
3109
|
+
if (!VALID_MEMORY_TYPES.has(type)) {
|
|
3110
|
+
sendError(res, 400, `Invalid memory type: ${String(body.type)}`);
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
const result = await rememberMemory(db, {
|
|
3114
|
+
content,
|
|
3115
|
+
type,
|
|
3116
|
+
uri: asString(body.uri),
|
|
3117
|
+
source: asString(body.source),
|
|
3118
|
+
emotion_val: asNumber(body.emotion_val),
|
|
3119
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
3120
|
+
conservative: asBoolean(body.conservative),
|
|
3121
|
+
provider: options?.provider
|
|
3122
|
+
});
|
|
3123
|
+
sendJson(res, 200, result);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (pathname === "/v1/recall") {
|
|
3127
|
+
const query = asString(body.query)?.trim();
|
|
3128
|
+
if (!query) {
|
|
3129
|
+
sendError(res, 400, "query is required");
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
const result = await recallMemory(db, {
|
|
3133
|
+
query,
|
|
3134
|
+
limit: asNumber(body.limit),
|
|
3135
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
3136
|
+
provider: options?.provider
|
|
3137
|
+
});
|
|
3138
|
+
sendJson(res, 200, formatRecallResponse(result));
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (pathname === "/v1/surface") {
|
|
3142
|
+
const types = asStringArray(body.types)?.filter((type) => VALID_MEMORY_TYPES.has(type));
|
|
3143
|
+
const intent = asString(body.intent);
|
|
3144
|
+
if (intent !== void 0 && !VALID_INTENTS.has(intent)) {
|
|
3145
|
+
sendError(res, 400, `Invalid intent: ${intent}`);
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
const result = await surfaceMemories(db, {
|
|
3149
|
+
query: asString(body.query),
|
|
3150
|
+
task: asString(body.task),
|
|
3151
|
+
recent_turns: asStringArray(body.recent_turns),
|
|
3152
|
+
intent,
|
|
3153
|
+
types,
|
|
3154
|
+
limit: asNumber(body.limit),
|
|
3155
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
3156
|
+
provider: options?.provider
|
|
3157
|
+
});
|
|
3158
|
+
sendJson(res, 200, formatSurfaceResponse(result));
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
if (pathname === "/v1/feedback") {
|
|
3162
|
+
const memoryId = asString(body.memory_id)?.trim();
|
|
3163
|
+
const source = asString(body.source);
|
|
3164
|
+
const useful = asBoolean(body.useful);
|
|
3165
|
+
if (!memoryId) {
|
|
3166
|
+
sendError(res, 400, "memory_id is required");
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (source !== "recall" && source !== "surface") {
|
|
3170
|
+
sendError(res, 400, "source must be 'recall' or 'surface'");
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
if (useful === void 0) {
|
|
3174
|
+
sendError(res, 400, "useful must be boolean");
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const result = recordFeedbackEvent(db, {
|
|
3178
|
+
memory_id: memoryId,
|
|
3179
|
+
source,
|
|
3180
|
+
useful,
|
|
3181
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId
|
|
3182
|
+
});
|
|
3183
|
+
sendJson(res, 200, result);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
if (pathname === "/v1/reflect") {
|
|
3187
|
+
const agentId = asString(body.agent_id) ?? defaultAgentId;
|
|
3188
|
+
const job = createJob(jobs, "reflect", agentId);
|
|
3189
|
+
if (wantsSse(req, body)) {
|
|
3190
|
+
openSse(res);
|
|
3191
|
+
sendSse(res, "job", job);
|
|
3192
|
+
void executeReflectJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
|
|
3193
|
+
sendSse(res, "done", { job: currentJob, result: result2 });
|
|
3194
|
+
}).catch((error) => {
|
|
3195
|
+
sendSse(res, "error", {
|
|
3196
|
+
job,
|
|
3197
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3198
|
+
});
|
|
3199
|
+
}).finally(() => {
|
|
3200
|
+
if (!res.writableEnded) res.end();
|
|
3201
|
+
});
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const result = await executeReflectJob(job, body);
|
|
3205
|
+
sendJson(res, 200, result);
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
if (pathname === "/v1/reindex") {
|
|
3209
|
+
const agentId = asString(body.agent_id) ?? defaultAgentId;
|
|
3210
|
+
const job = createJob(jobs, "reindex", agentId);
|
|
3211
|
+
if (wantsSse(req, body)) {
|
|
3212
|
+
openSse(res);
|
|
3213
|
+
sendSse(res, "job", job);
|
|
3214
|
+
void executeReindexJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
|
|
3215
|
+
sendSse(res, "done", { job: currentJob, result: result2 });
|
|
3216
|
+
}).catch((error) => {
|
|
3217
|
+
sendSse(res, "error", {
|
|
3218
|
+
job,
|
|
3219
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3220
|
+
});
|
|
3221
|
+
}).finally(() => {
|
|
3222
|
+
if (!res.writableEnded) res.end();
|
|
3223
|
+
});
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const result = await executeReindexJob(job, body);
|
|
3227
|
+
sendJson(res, 200, result);
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
sendError(res, 404, `Route not found: ${method} ${pathname}`);
|
|
3231
|
+
} catch (error) {
|
|
3232
|
+
sendError(res, 500, error instanceof Error ? error.message : String(error));
|
|
959
3233
|
}
|
|
960
|
-
}
|
|
3234
|
+
});
|
|
3235
|
+
return {
|
|
3236
|
+
server,
|
|
3237
|
+
db,
|
|
3238
|
+
jobs,
|
|
3239
|
+
listen(port = 3e3, host = "127.0.0.1") {
|
|
3240
|
+
return new Promise((resolve2, reject) => {
|
|
3241
|
+
server.once("error", reject);
|
|
3242
|
+
server.listen(port, host, () => {
|
|
3243
|
+
server.off("error", reject);
|
|
3244
|
+
const address = server.address();
|
|
3245
|
+
if (!address || typeof address === "string") {
|
|
3246
|
+
resolve2({ port, host });
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
resolve2({ port: address.port, host: address.address });
|
|
3250
|
+
});
|
|
3251
|
+
});
|
|
3252
|
+
},
|
|
3253
|
+
close() {
|
|
3254
|
+
return new Promise((resolve2, reject) => {
|
|
3255
|
+
server.close((error) => {
|
|
3256
|
+
if (ownsDb) {
|
|
3257
|
+
try {
|
|
3258
|
+
db.close();
|
|
3259
|
+
} catch {
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (error) {
|
|
3263
|
+
reject(error);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
resolve2();
|
|
3267
|
+
});
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
async function startHttpServer(options) {
|
|
3273
|
+
const service = createHttpServer(options);
|
|
3274
|
+
await service.listen(options?.port, options?.host);
|
|
3275
|
+
return service;
|
|
961
3276
|
}
|
|
962
3277
|
|
|
963
3278
|
// src/bin/agent-memory.ts
|
|
964
|
-
import {
|
|
965
|
-
import { resolve, basename } from "path";
|
|
3279
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
966
3280
|
var args = process.argv.slice(2);
|
|
967
3281
|
var command = args[0];
|
|
968
3282
|
function getDbPath() {
|
|
@@ -973,19 +3287,21 @@ function getAgentId() {
|
|
|
973
3287
|
}
|
|
974
3288
|
function printHelp() {
|
|
975
3289
|
console.log(`
|
|
976
|
-
\u{1F9E0} AgentMemory
|
|
3290
|
+
\u{1F9E0} AgentMemory v4 \u2014 Sleep-cycle memory for AI agents
|
|
977
3291
|
|
|
978
3292
|
Usage: agent-memory <command> [options]
|
|
979
3293
|
|
|
980
3294
|
Commands:
|
|
981
3295
|
init Create database
|
|
982
3296
|
db:migrate Run schema migrations (no-op if up-to-date)
|
|
983
|
-
remember <content> [--uri X] [--type T] Store a memory
|
|
984
|
-
recall <query> [--limit N]
|
|
985
|
-
boot
|
|
3297
|
+
remember <content> [--uri X] [--type T] [--emotion-tag TAG] Store a memory
|
|
3298
|
+
recall <query> [--limit N] [--emotion-tag TAG] Search memories (hybrid retrieval)
|
|
3299
|
+
boot [--format json|narrative] [--agent-name NAME] Load identity memories
|
|
3300
|
+
surface [--out FILE] [--days N] [--limit N] Export recent memories as Markdown
|
|
986
3301
|
status Show statistics
|
|
987
3302
|
reflect [decay|tidy|govern|all] Run sleep cycle
|
|
988
|
-
reindex
|
|
3303
|
+
reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
|
|
3304
|
+
serve [--host H] [--port N] Start the HTTP/SSE API server
|
|
989
3305
|
migrate <dir> Import from Markdown files
|
|
990
3306
|
export <dir> Export memories to Markdown files
|
|
991
3307
|
help Show this help
|
|
@@ -996,10 +3312,28 @@ Environment:
|
|
|
996
3312
|
`);
|
|
997
3313
|
}
|
|
998
3314
|
function getFlag(flag) {
|
|
999
|
-
const
|
|
1000
|
-
if (
|
|
3315
|
+
const index = args.indexOf(flag);
|
|
3316
|
+
if (index >= 0 && index + 1 < args.length) return args[index + 1];
|
|
1001
3317
|
return void 0;
|
|
1002
3318
|
}
|
|
3319
|
+
function hasFlag(flag) {
|
|
3320
|
+
return args.includes(flag);
|
|
3321
|
+
}
|
|
3322
|
+
function getPositionalArgs(startIndex = 1) {
|
|
3323
|
+
const values = [];
|
|
3324
|
+
for (let index = startIndex; index < args.length; index++) {
|
|
3325
|
+
const token = args[index];
|
|
3326
|
+
if (token.startsWith("--")) {
|
|
3327
|
+
const next = args[index + 1];
|
|
3328
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
3329
|
+
index += 1;
|
|
3330
|
+
}
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
values.push(token);
|
|
3334
|
+
}
|
|
3335
|
+
return values;
|
|
3336
|
+
}
|
|
1003
3337
|
async function main() {
|
|
1004
3338
|
try {
|
|
1005
3339
|
switch (command) {
|
|
@@ -1018,7 +3352,7 @@ async function main() {
|
|
|
1018
3352
|
break;
|
|
1019
3353
|
}
|
|
1020
3354
|
case "remember": {
|
|
1021
|
-
const content =
|
|
3355
|
+
const content = getPositionalArgs(1).join(" ");
|
|
1022
3356
|
if (!content) {
|
|
1023
3357
|
console.error("Usage: agent-memory remember <content>");
|
|
1024
3358
|
process.exit(1);
|
|
@@ -1026,116 +3360,203 @@ async function main() {
|
|
|
1026
3360
|
const db = openDatabase({ path: getDbPath() });
|
|
1027
3361
|
const uri = getFlag("--uri");
|
|
1028
3362
|
const type = getFlag("--type") ?? "knowledge";
|
|
1029
|
-
const
|
|
1030
|
-
const result =
|
|
3363
|
+
const emotionTag = getFlag("--emotion-tag");
|
|
3364
|
+
const result = await rememberMemory(db, {
|
|
3365
|
+
content,
|
|
3366
|
+
type,
|
|
3367
|
+
uri,
|
|
3368
|
+
source: "manual",
|
|
3369
|
+
agent_id: getAgentId(),
|
|
3370
|
+
emotion_tag: emotionTag
|
|
3371
|
+
});
|
|
1031
3372
|
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
1032
3373
|
db.close();
|
|
1033
3374
|
break;
|
|
1034
3375
|
}
|
|
1035
3376
|
case "recall": {
|
|
1036
|
-
const query =
|
|
3377
|
+
const query = getPositionalArgs(1).join(" ");
|
|
1037
3378
|
if (!query) {
|
|
1038
3379
|
console.error("Usage: agent-memory recall <query>");
|
|
1039
3380
|
process.exit(1);
|
|
1040
3381
|
}
|
|
1041
3382
|
const db = openDatabase({ path: getDbPath() });
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
console.log(`\u{1F50D} Results: ${weighted.length}
|
|
3383
|
+
const emotionTag = getFlag("--emotion-tag");
|
|
3384
|
+
const result = await recallMemory(db, {
|
|
3385
|
+
query,
|
|
3386
|
+
agent_id: getAgentId(),
|
|
3387
|
+
limit: Number.parseInt(getFlag("--limit") ?? "10", 10),
|
|
3388
|
+
emotion_tag: emotionTag
|
|
3389
|
+
});
|
|
3390
|
+
console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
|
|
1051
3391
|
`);
|
|
1052
|
-
for (const
|
|
1053
|
-
const
|
|
1054
|
-
const
|
|
1055
|
-
|
|
3392
|
+
for (const row of result.results) {
|
|
3393
|
+
const priorityLabel = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][row.memory.priority];
|
|
3394
|
+
const vitality = (row.memory.vitality * 100).toFixed(0);
|
|
3395
|
+
const branches = [
|
|
3396
|
+
row.bm25_rank ? `bm25#${row.bm25_rank}` : null,
|
|
3397
|
+
row.vector_rank ? `vec#${row.vector_rank}` : null
|
|
3398
|
+
].filter(Boolean).join(" + ");
|
|
3399
|
+
console.log(`${priorityLabel} P${row.memory.priority} [${vitality}%] ${row.memory.content.slice(0, 80)}${branches ? ` (${branches})` : ""}`);
|
|
1056
3400
|
}
|
|
1057
3401
|
db.close();
|
|
1058
3402
|
break;
|
|
1059
3403
|
}
|
|
1060
3404
|
case "boot": {
|
|
1061
3405
|
const db = openDatabase({ path: getDbPath() });
|
|
1062
|
-
const
|
|
1063
|
-
|
|
3406
|
+
const format = getFlag("--format") ?? "narrative";
|
|
3407
|
+
const agentName = getFlag("--agent-name") ?? "Agent";
|
|
3408
|
+
const result = boot(db, { agent_id: getAgentId(), format, agent_name: agentName });
|
|
3409
|
+
if (format === "narrative" && result.narrative) {
|
|
3410
|
+
console.log(result.narrative);
|
|
3411
|
+
} else {
|
|
3412
|
+
console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
|
|
1064
3413
|
`);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
3414
|
+
for (const memory of result.identityMemories) {
|
|
3415
|
+
console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
|
|
3416
|
+
}
|
|
3417
|
+
if (result.bootPaths.length) {
|
|
3418
|
+
console.log(`
|
|
1070
3419
|
\u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
db.close();
|
|
3423
|
+
break;
|
|
3424
|
+
}
|
|
3425
|
+
case "surface": {
|
|
3426
|
+
const db = openDatabase({ path: getDbPath() });
|
|
3427
|
+
const days = Number.parseInt(getFlag("--days") ?? "7", 10);
|
|
3428
|
+
const limit = Number.parseInt(getFlag("--limit") ?? "50", 10);
|
|
3429
|
+
const minVitality = Number.parseFloat(getFlag("--min-vitality") ?? "0.1");
|
|
3430
|
+
const outFile = getFlag("--out");
|
|
3431
|
+
const typesRaw = getFlag("--types");
|
|
3432
|
+
const types = typesRaw ? typesRaw.split(",").map((t) => t.trim()) : void 0;
|
|
3433
|
+
const cutoff = new Date(Date.now() - days * 864e5).toISOString();
|
|
3434
|
+
const surfaceResult = await surfaceMemories(db, {
|
|
3435
|
+
task: "context loading",
|
|
3436
|
+
intent: "temporal",
|
|
3437
|
+
agent_id: getAgentId(),
|
|
3438
|
+
types,
|
|
3439
|
+
limit: Math.max(limit, 100),
|
|
3440
|
+
// fetch more, filter by date after
|
|
3441
|
+
min_vitality: minVitality
|
|
3442
|
+
});
|
|
3443
|
+
const filtered = surfaceResult.results.filter((r) => r.memory.updated_at >= cutoff).slice(0, limit);
|
|
3444
|
+
const grouped = {};
|
|
3445
|
+
for (const r of filtered) {
|
|
3446
|
+
const t = r.memory.type;
|
|
3447
|
+
if (!grouped[t]) grouped[t] = [];
|
|
3448
|
+
grouped[t].push(r);
|
|
3449
|
+
}
|
|
3450
|
+
const { formatRelativeDate: formatRelativeDate2 } = await Promise.resolve().then(() => (init_boot(), boot_exports));
|
|
3451
|
+
const lines = [];
|
|
3452
|
+
lines.push("# Recent Memories");
|
|
3453
|
+
lines.push("");
|
|
3454
|
+
lines.push(`> Auto-generated by AgentMemory surface. Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
3455
|
+
lines.push("");
|
|
3456
|
+
const typeOrder = ["identity", "emotion", "knowledge", "event"];
|
|
3457
|
+
const typeLabels = { identity: "Identity", emotion: "Emotion", knowledge: "Knowledge", event: "Events" };
|
|
3458
|
+
for (const t of typeOrder) {
|
|
3459
|
+
const items = grouped[t];
|
|
3460
|
+
if (!items?.length) continue;
|
|
3461
|
+
lines.push(`## ${typeLabels[t]}`);
|
|
3462
|
+
for (const item of items) {
|
|
3463
|
+
const content = item.memory.content.split("\n")[0].slice(0, 200);
|
|
3464
|
+
const time = formatRelativeDate2(item.memory.updated_at);
|
|
3465
|
+
const tag = item.memory.emotion_tag;
|
|
3466
|
+
const meta = t === "emotion" && tag ? `(${tag}, ${time})` : `(${time})`;
|
|
3467
|
+
lines.push(`- ${content} ${meta}`);
|
|
3468
|
+
}
|
|
3469
|
+
lines.push("");
|
|
3470
|
+
}
|
|
3471
|
+
const markdown = lines.join("\n");
|
|
3472
|
+
if (outFile) {
|
|
3473
|
+
writeFileSync2(outFile, markdown, "utf-8");
|
|
3474
|
+
console.log(`\u2705 Surface: ${filtered.length} memories written to ${outFile}`);
|
|
3475
|
+
} else {
|
|
3476
|
+
console.log(markdown);
|
|
1071
3477
|
}
|
|
1072
3478
|
db.close();
|
|
1073
3479
|
break;
|
|
1074
3480
|
}
|
|
1075
3481
|
case "status": {
|
|
1076
3482
|
const db = openDatabase({ path: getDbPath() });
|
|
1077
|
-
const
|
|
1078
|
-
const stats = countMemories(db, agentId);
|
|
1079
|
-
const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
|
|
1080
|
-
const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
|
|
3483
|
+
const status = getMemoryStatus(db, { agent_id: getAgentId() });
|
|
1081
3484
|
console.log("\u{1F9E0} AgentMemory Status\n");
|
|
1082
|
-
console.log(` Total memories: ${
|
|
1083
|
-
console.log(` By type: ${Object.entries(
|
|
1084
|
-
console.log(` By priority: ${Object.entries(
|
|
1085
|
-
console.log(` Paths: ${paths}`);
|
|
1086
|
-
console.log(` Low vitality (<10%): ${
|
|
3485
|
+
console.log(` Total memories: ${status.total}`);
|
|
3486
|
+
console.log(` By type: ${Object.entries(status.by_type).map(([key, value]) => `${key}=${value}`).join(", ")}`);
|
|
3487
|
+
console.log(` By priority: ${Object.entries(status.by_priority).map(([key, value]) => `${key}=${value}`).join(", ")}`);
|
|
3488
|
+
console.log(` Paths: ${status.paths}`);
|
|
3489
|
+
console.log(` Low vitality (<10%): ${status.low_vitality}`);
|
|
3490
|
+
console.log(` Feedback events: ${status.feedback_events}`);
|
|
1087
3491
|
db.close();
|
|
1088
3492
|
break;
|
|
1089
3493
|
}
|
|
1090
3494
|
case "reflect": {
|
|
1091
3495
|
const phase = args[1] ?? "all";
|
|
1092
3496
|
const db = openDatabase({ path: getDbPath() });
|
|
1093
|
-
const
|
|
1094
|
-
console.log(`\u{1F319}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const r = runDecay(db, { agent_id: agentId });
|
|
1098
|
-
console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
|
|
1099
|
-
}
|
|
1100
|
-
if (phase === "tidy" || phase === "all") {
|
|
1101
|
-
const r = runTidy(db, { agent_id: agentId });
|
|
1102
|
-
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans`);
|
|
1103
|
-
}
|
|
1104
|
-
if (phase === "govern" || phase === "all") {
|
|
1105
|
-
const r = runGovern(db, { agent_id: agentId });
|
|
1106
|
-
console.log(` Govern: ${r.orphanPaths} paths, ${r.emptyMemories} empty cleaned`);
|
|
3497
|
+
const result = await reflectMemories(db, { phase, agent_id: getAgentId() });
|
|
3498
|
+
console.log(`\u{1F319} Reflect job ${result.jobId}${result.resumed ? " (resume)" : ""}`);
|
|
3499
|
+
for (const [name, summary] of Object.entries(result.results)) {
|
|
3500
|
+
console.log(` ${name}: ${JSON.stringify(summary)}`);
|
|
1107
3501
|
}
|
|
1108
3502
|
db.close();
|
|
1109
3503
|
break;
|
|
1110
3504
|
}
|
|
1111
3505
|
case "reindex": {
|
|
1112
3506
|
const db = openDatabase({ path: getDbPath() });
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const txn = db.transaction(() => {
|
|
1118
|
-
for (const mem of memories) {
|
|
1119
|
-
insert.run(mem.id, tokenizeForIndex(mem.content));
|
|
1120
|
-
count++;
|
|
1121
|
-
}
|
|
3507
|
+
const result = await reindexMemories(db, {
|
|
3508
|
+
agent_id: getAgentId(),
|
|
3509
|
+
force: hasFlag("--full"),
|
|
3510
|
+
batchSize: Number.parseInt(getFlag("--batch-size") ?? "16", 10)
|
|
1122
3511
|
});
|
|
1123
|
-
|
|
1124
|
-
|
|
3512
|
+
console.log(`\u{1F504} Reindexed ${result.fts.reindexed} memories in BM25 index`);
|
|
3513
|
+
if (result.embeddings.enabled) {
|
|
3514
|
+
console.log(`\u{1F9EC} Embeddings: provider=${result.embeddings.providerId} scanned=${result.embeddings.scanned} embedded=${result.embeddings.embedded} failed=${result.embeddings.failed}`);
|
|
3515
|
+
} else {
|
|
3516
|
+
console.log("\u{1F9EC} Embeddings: disabled (no provider configured)");
|
|
3517
|
+
}
|
|
1125
3518
|
db.close();
|
|
1126
3519
|
break;
|
|
1127
3520
|
}
|
|
3521
|
+
case "serve": {
|
|
3522
|
+
const port = Number.parseInt(getFlag("--port") ?? process.env.AGENT_MEMORY_HTTP_PORT ?? "3000", 10);
|
|
3523
|
+
const host = getFlag("--host") ?? process.env.AGENT_MEMORY_HTTP_HOST ?? "127.0.0.1";
|
|
3524
|
+
const service = await startHttpServer({
|
|
3525
|
+
dbPath: getDbPath(),
|
|
3526
|
+
agentId: getAgentId(),
|
|
3527
|
+
port,
|
|
3528
|
+
host
|
|
3529
|
+
});
|
|
3530
|
+
const address = service.server.address();
|
|
3531
|
+
if (address && typeof address !== "string") {
|
|
3532
|
+
console.log(`\u{1F310} AgentMemory HTTP server listening on http://${address.address}:${address.port}`);
|
|
3533
|
+
} else {
|
|
3534
|
+
console.log(`\u{1F310} AgentMemory HTTP server listening on http://${host}:${port}`);
|
|
3535
|
+
}
|
|
3536
|
+
const shutdown = async () => {
|
|
3537
|
+
try {
|
|
3538
|
+
await service.close();
|
|
3539
|
+
} finally {
|
|
3540
|
+
process.exit(0);
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
process.once("SIGINT", () => {
|
|
3544
|
+
void shutdown();
|
|
3545
|
+
});
|
|
3546
|
+
process.once("SIGTERM", () => {
|
|
3547
|
+
void shutdown();
|
|
3548
|
+
});
|
|
3549
|
+
break;
|
|
3550
|
+
}
|
|
1128
3551
|
case "export": {
|
|
1129
3552
|
const dir = args[1];
|
|
1130
3553
|
if (!dir) {
|
|
1131
3554
|
console.error("Usage: agent-memory export <directory>");
|
|
1132
3555
|
process.exit(1);
|
|
1133
3556
|
}
|
|
1134
|
-
const dirPath = resolve(dir);
|
|
1135
3557
|
const db = openDatabase({ path: getDbPath() });
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
|
|
3558
|
+
const result = exportMemories(db, resolve(dir), { agent_id: getAgentId() });
|
|
3559
|
+
console.log(`\u2705 Export complete: ${result.exported} items to ${resolve(dir)} (${result.files.length} files)`);
|
|
1139
3560
|
db.close();
|
|
1140
3561
|
break;
|
|
1141
3562
|
}
|
|
@@ -1153,10 +3574,10 @@ async function main() {
|
|
|
1153
3574
|
const db = openDatabase({ path: getDbPath() });
|
|
1154
3575
|
const agentId = getAgentId();
|
|
1155
3576
|
let imported = 0;
|
|
1156
|
-
const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((
|
|
3577
|
+
const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((file) => resolve(dirPath, file)).find((file) => existsSync2(file));
|
|
1157
3578
|
if (memoryFile) {
|
|
1158
3579
|
const content = readFileSync2(memoryFile, "utf-8");
|
|
1159
|
-
const sections = content.split(/^## /m).filter((
|
|
3580
|
+
const sections = content.split(/^## /m).filter((section) => section.trim());
|
|
1160
3581
|
for (const section of sections) {
|
|
1161
3582
|
const lines = section.split("\n");
|
|
1162
3583
|
const title = lines[0]?.trim();
|
|
@@ -1164,40 +3585,46 @@ async function main() {
|
|
|
1164
3585
|
if (!body) continue;
|
|
1165
3586
|
const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
|
|
1166
3587
|
const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
3588
|
+
await rememberMemory(db, {
|
|
3589
|
+
content: `## ${title}
|
|
3590
|
+
${body}`,
|
|
3591
|
+
type,
|
|
3592
|
+
uri,
|
|
3593
|
+
source: `migrate:${basename(memoryFile)}`,
|
|
3594
|
+
agent_id: agentId
|
|
3595
|
+
});
|
|
3596
|
+
imported += 1;
|
|
1170
3597
|
}
|
|
1171
3598
|
console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
|
|
1172
3599
|
}
|
|
1173
|
-
const mdFiles = readdirSync(dirPath).filter((
|
|
3600
|
+
const mdFiles = readdirSync(dirPath).filter((file) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(file)).sort();
|
|
1174
3601
|
for (const file of mdFiles) {
|
|
1175
3602
|
const content = readFileSync2(resolve(dirPath, file), "utf-8");
|
|
1176
3603
|
const date = file.replace(/\.(md|qmd)$/i, "");
|
|
1177
|
-
|
|
3604
|
+
await rememberMemory(db, {
|
|
1178
3605
|
content,
|
|
1179
3606
|
type: "event",
|
|
1180
3607
|
uri: `event://journal/${date}`,
|
|
1181
3608
|
source: `migrate:${file}`,
|
|
1182
3609
|
agent_id: agentId
|
|
1183
3610
|
});
|
|
1184
|
-
imported
|
|
3611
|
+
imported += 1;
|
|
1185
3612
|
}
|
|
1186
3613
|
if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
|
|
1187
3614
|
const weeklyDir = resolve(dirPath, "weekly");
|
|
1188
3615
|
if (existsSync2(weeklyDir)) {
|
|
1189
|
-
const weeklyFiles = readdirSync(weeklyDir).filter((
|
|
3616
|
+
const weeklyFiles = readdirSync(weeklyDir).filter((file) => file.endsWith(".md") || file.endsWith(".qmd"));
|
|
1190
3617
|
for (const file of weeklyFiles) {
|
|
1191
3618
|
const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
|
|
1192
3619
|
const week = file.replace(/\.(md|qmd)$/i, "");
|
|
1193
|
-
|
|
3620
|
+
await rememberMemory(db, {
|
|
1194
3621
|
content,
|
|
1195
3622
|
type: "knowledge",
|
|
1196
3623
|
uri: `knowledge://weekly/${week}`,
|
|
1197
3624
|
source: `migrate:weekly/${file}`,
|
|
1198
3625
|
agent_id: agentId
|
|
1199
3626
|
});
|
|
1200
|
-
imported
|
|
3627
|
+
imported += 1;
|
|
1201
3628
|
}
|
|
1202
3629
|
if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
|
|
1203
3630
|
}
|
|
@@ -1217,8 +3644,8 @@ ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentI
|
|
|
1217
3644
|
printHelp();
|
|
1218
3645
|
process.exit(1);
|
|
1219
3646
|
}
|
|
1220
|
-
} catch (
|
|
1221
|
-
const message =
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1222
3649
|
console.error("Error:", message);
|
|
1223
3650
|
process.exit(1);
|
|
1224
3651
|
}
|