@smyslenny/agent-memory 2.2.0 → 4.0.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 +163 -41
- package/README.md +234 -156
- package/dist/bin/agent-memory.js +2423 -784
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +535 -215
- package/dist/index.js +2635 -876
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2330 -928
- 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/0014-memory-core-dedup.md +722 -0
- package/docs/design/0015-v4-overhaul.md +631 -0
- package/docs/design/TEMPLATE.md +67 -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 -5
- package/README.zh-CN.md +0 -170
package/dist/bin/agent-memory.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// AgentMemory v2 — Sleep-cycle memory for AI agents
|
|
3
3
|
|
|
4
|
+
// src/bin/agent-memory.ts
|
|
5
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
6
|
+
import { basename, resolve } from "path";
|
|
7
|
+
|
|
4
8
|
// src/core/db.ts
|
|
5
9
|
import Database from "better-sqlite3";
|
|
6
10
|
import { randomUUID } from "crypto";
|
|
7
|
-
var SCHEMA_VERSION =
|
|
11
|
+
var SCHEMA_VERSION = 5;
|
|
8
12
|
var SCHEMA_SQL = `
|
|
9
13
|
-- Memory entries
|
|
10
14
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -67,14 +71,37 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
67
71
|
|
|
68
72
|
-- Embeddings (optional semantic layer)
|
|
69
73
|
CREATE TABLE IF NOT EXISTS embeddings (
|
|
70
|
-
|
|
71
|
-
memory_id
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
76
|
+
provider_id TEXT NOT NULL,
|
|
77
|
+
vector BLOB,
|
|
78
|
+
content_hash TEXT NOT NULL,
|
|
79
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
80
|
+
created_at TEXT NOT NULL,
|
|
81
|
+
UNIQUE(memory_id, provider_id)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
-- Maintenance jobs (reflect / reindex checkpoints)
|
|
85
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
86
|
+
job_id TEXT PRIMARY KEY,
|
|
87
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
88
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
89
|
+
checkpoint TEXT,
|
|
90
|
+
error TEXT,
|
|
91
|
+
started_at TEXT NOT NULL,
|
|
92
|
+
finished_at TEXT
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
-- Feedback signals (recall/surface usefulness + governance priors)
|
|
96
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
99
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
100
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
101
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
102
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
103
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
104
|
+
created_at TEXT NOT NULL
|
|
78
105
|
);
|
|
79
106
|
|
|
80
107
|
-- Schema version tracking
|
|
@@ -91,6 +118,8 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
|
91
118
|
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
92
119
|
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
93
120
|
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
|
|
94
123
|
`;
|
|
95
124
|
function openDatabase(opts) {
|
|
96
125
|
const db = new Database(opts.path);
|
|
@@ -111,6 +140,7 @@ function openDatabase(opts) {
|
|
|
111
140
|
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
112
141
|
}
|
|
113
142
|
ensureIndexes(db);
|
|
143
|
+
ensureFeedbackEventSchema(db);
|
|
114
144
|
return db;
|
|
115
145
|
}
|
|
116
146
|
function now() {
|
|
@@ -129,6 +159,14 @@ function getSchemaVersion(db) {
|
|
|
129
159
|
return null;
|
|
130
160
|
}
|
|
131
161
|
}
|
|
162
|
+
function tableExists(db, table) {
|
|
163
|
+
try {
|
|
164
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table);
|
|
165
|
+
return Boolean(row?.name);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
132
170
|
function tableHasColumn(db, table, column) {
|
|
133
171
|
try {
|
|
134
172
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -150,6 +188,16 @@ function migrateDatabase(db, from, to) {
|
|
|
150
188
|
v = 3;
|
|
151
189
|
continue;
|
|
152
190
|
}
|
|
191
|
+
if (v === 3) {
|
|
192
|
+
migrateV3ToV4(db);
|
|
193
|
+
v = 4;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (v === 4) {
|
|
197
|
+
migrateV4ToV5(db);
|
|
198
|
+
v = 5;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
153
201
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
154
202
|
}
|
|
155
203
|
}
|
|
@@ -231,14 +279,12 @@ function migrateV1ToV2(db) {
|
|
|
231
279
|
function inferSchemaVersion(db) {
|
|
232
280
|
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
233
281
|
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
234
|
-
const hasEmbeddings = (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
})();
|
|
282
|
+
const hasEmbeddings = tableExists(db, "embeddings");
|
|
283
|
+
const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
284
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
285
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
286
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
287
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
242
288
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
243
289
|
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
244
290
|
return 1;
|
|
@@ -251,14 +297,37 @@ function ensureIndexes(db) {
|
|
|
251
297
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
252
298
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
253
299
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
300
|
+
if (tableExists(db, "embeddings")) {
|
|
301
|
+
if (tableHasColumn(db, "embeddings", "provider_id")) {
|
|
302
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_provider_status ON embeddings(provider_id, status);");
|
|
303
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory_provider ON embeddings(memory_id, provider_id);");
|
|
304
|
+
} else {
|
|
257
305
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
258
306
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
259
307
|
}
|
|
260
|
-
} catch {
|
|
261
308
|
}
|
|
309
|
+
if (tableExists(db, "maintenance_jobs")) {
|
|
310
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
|
|
311
|
+
}
|
|
312
|
+
if (tableExists(db, "feedback_events")) {
|
|
313
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
314
|
+
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
315
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function ensureFeedbackEventSchema(db) {
|
|
320
|
+
if (!tableExists(db, "feedback_events")) return;
|
|
321
|
+
if (!tableHasColumn(db, "feedback_events", "source")) {
|
|
322
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN source TEXT NOT NULL DEFAULT 'surface';");
|
|
323
|
+
}
|
|
324
|
+
if (!tableHasColumn(db, "feedback_events", "useful")) {
|
|
325
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN useful INTEGER NOT NULL DEFAULT 1;");
|
|
326
|
+
}
|
|
327
|
+
if (!tableHasColumn(db, "feedback_events", "agent_id")) {
|
|
328
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN agent_id TEXT NOT NULL DEFAULT 'default';");
|
|
329
|
+
}
|
|
330
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
262
331
|
}
|
|
263
332
|
function migrateV2ToV3(db) {
|
|
264
333
|
try {
|
|
@@ -285,9 +354,97 @@ function migrateV2ToV3(db) {
|
|
|
285
354
|
throw e;
|
|
286
355
|
}
|
|
287
356
|
}
|
|
357
|
+
function migrateV3ToV4(db) {
|
|
358
|
+
const alreadyMigrated = tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
359
|
+
if (alreadyMigrated) {
|
|
360
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
db.exec("BEGIN");
|
|
365
|
+
const legacyRows = tableExists(db, "embeddings") ? db.prepare(
|
|
366
|
+
`SELECT e.agent_id, e.memory_id, e.model, e.vector, e.created_at, m.hash
|
|
367
|
+
FROM embeddings e
|
|
368
|
+
LEFT JOIN memories m ON m.id = e.memory_id`
|
|
369
|
+
).all() : [];
|
|
370
|
+
db.exec(`
|
|
371
|
+
CREATE TABLE IF NOT EXISTS embeddings_v4 (
|
|
372
|
+
id TEXT PRIMARY KEY,
|
|
373
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
374
|
+
provider_id TEXT NOT NULL,
|
|
375
|
+
vector BLOB,
|
|
376
|
+
content_hash TEXT NOT NULL,
|
|
377
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
378
|
+
created_at TEXT NOT NULL,
|
|
379
|
+
UNIQUE(memory_id, provider_id)
|
|
380
|
+
);
|
|
381
|
+
`);
|
|
382
|
+
const insert = db.prepare(
|
|
383
|
+
`INSERT INTO embeddings_v4 (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
384
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)`
|
|
385
|
+
);
|
|
386
|
+
for (const row of legacyRows) {
|
|
387
|
+
insert.run(newId(), row.memory_id, `legacy:${row.agent_id}:${row.model}`, row.vector, row.hash ?? "", row.created_at);
|
|
388
|
+
}
|
|
389
|
+
if (tableExists(db, "embeddings")) {
|
|
390
|
+
db.exec("DROP TABLE embeddings;");
|
|
391
|
+
}
|
|
392
|
+
db.exec("ALTER TABLE embeddings_v4 RENAME TO embeddings;");
|
|
393
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
394
|
+
db.exec("COMMIT");
|
|
395
|
+
} catch (e) {
|
|
396
|
+
try {
|
|
397
|
+
db.exec("ROLLBACK");
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function migrateV4ToV5(db) {
|
|
404
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
405
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
406
|
+
if (hasMaintenanceJobs && hasFeedbackEvents) {
|
|
407
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
db.exec("BEGIN");
|
|
412
|
+
db.exec(`
|
|
413
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
414
|
+
job_id TEXT PRIMARY KEY,
|
|
415
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
416
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
417
|
+
checkpoint TEXT,
|
|
418
|
+
error TEXT,
|
|
419
|
+
started_at TEXT NOT NULL,
|
|
420
|
+
finished_at TEXT
|
|
421
|
+
);
|
|
422
|
+
`);
|
|
423
|
+
db.exec(`
|
|
424
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
425
|
+
id TEXT PRIMARY KEY,
|
|
426
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
427
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
428
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
429
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
430
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
431
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
432
|
+
created_at TEXT NOT NULL
|
|
433
|
+
);
|
|
434
|
+
`);
|
|
435
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
436
|
+
db.exec("COMMIT");
|
|
437
|
+
} catch (e) {
|
|
438
|
+
try {
|
|
439
|
+
db.exec("ROLLBACK");
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
throw e;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
288
445
|
|
|
289
446
|
// src/core/memory.ts
|
|
290
|
-
import { createHash } from "crypto";
|
|
447
|
+
import { createHash as createHash2 } from "crypto";
|
|
291
448
|
|
|
292
449
|
// src/search/tokenizer.ts
|
|
293
450
|
import { readFileSync } from "fs";
|
|
@@ -368,9 +525,347 @@ function tokenizeForIndex(text) {
|
|
|
368
525
|
return tokens.join(" ");
|
|
369
526
|
}
|
|
370
527
|
|
|
528
|
+
// src/search/embedding.ts
|
|
529
|
+
import { createHash } from "crypto";
|
|
530
|
+
function trimTrailingSlashes(value) {
|
|
531
|
+
return value.replace(/\/+$/, "");
|
|
532
|
+
}
|
|
533
|
+
function resolveEndpoint(baseUrl, endpoint = "/embeddings") {
|
|
534
|
+
const trimmed = trimTrailingSlashes(baseUrl);
|
|
535
|
+
if (trimmed.endsWith("/embeddings")) {
|
|
536
|
+
return trimmed;
|
|
537
|
+
}
|
|
538
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
539
|
+
return `${trimmed}${normalizedEndpoint}`;
|
|
540
|
+
}
|
|
541
|
+
function stableProviderId(prefix, input) {
|
|
542
|
+
const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
543
|
+
return `${prefix}:${digest}`;
|
|
544
|
+
}
|
|
545
|
+
function getFetch(fetchImpl) {
|
|
546
|
+
const candidate = fetchImpl ?? globalThis.fetch;
|
|
547
|
+
if (!candidate) {
|
|
548
|
+
throw new Error("Global fetch is not available in this runtime");
|
|
549
|
+
}
|
|
550
|
+
return candidate;
|
|
551
|
+
}
|
|
552
|
+
function assertEmbeddingVector(vector, dimension, context) {
|
|
553
|
+
if (!Array.isArray(vector) || !vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
554
|
+
throw new Error(`${context} returned an invalid embedding vector`);
|
|
555
|
+
}
|
|
556
|
+
if (vector.length !== dimension) {
|
|
557
|
+
throw new Error(`${context} returned dimension ${vector.length}, expected ${dimension}`);
|
|
558
|
+
}
|
|
559
|
+
return vector;
|
|
560
|
+
}
|
|
561
|
+
function parseOpenAIResponse(json2, dimension, context) {
|
|
562
|
+
const rows = json2?.data;
|
|
563
|
+
if (!Array.isArray(rows)) {
|
|
564
|
+
throw new Error(`${context} returned an invalid embeddings payload`);
|
|
565
|
+
}
|
|
566
|
+
return rows.map((row, index) => assertEmbeddingVector(row?.embedding, dimension, `${context} item ${index}`));
|
|
567
|
+
}
|
|
568
|
+
function parseLocalHttpResponse(json2, dimension, context) {
|
|
569
|
+
if (Array.isArray(json2.embeddings)) {
|
|
570
|
+
const embeddings = json2.embeddings;
|
|
571
|
+
return embeddings.map((row, index) => assertEmbeddingVector(row, dimension, `${context} item ${index}`));
|
|
572
|
+
}
|
|
573
|
+
return parseOpenAIResponse(json2, dimension, context);
|
|
574
|
+
}
|
|
575
|
+
async function runEmbeddingRequest(input) {
|
|
576
|
+
const fetchFn = getFetch(input.fetchImpl);
|
|
577
|
+
const response = await fetchFn(input.url, {
|
|
578
|
+
method: "POST",
|
|
579
|
+
headers: {
|
|
580
|
+
"content-type": "application/json",
|
|
581
|
+
...input.headers
|
|
582
|
+
},
|
|
583
|
+
body: JSON.stringify(input.body)
|
|
584
|
+
});
|
|
585
|
+
if (!response.ok) {
|
|
586
|
+
const detail = await response.text().catch(() => "");
|
|
587
|
+
throw new Error(`${input.context} request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
588
|
+
}
|
|
589
|
+
const json2 = await response.json();
|
|
590
|
+
return input.parser(json2, input.dimension, input.context);
|
|
591
|
+
}
|
|
592
|
+
function createOpenAICompatibleEmbeddingProvider(opts) {
|
|
593
|
+
const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
594
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
595
|
+
const id = stableProviderId(`openai-compatible:${opts.model}`, providerDescriptor);
|
|
596
|
+
return {
|
|
597
|
+
id,
|
|
598
|
+
model: opts.model,
|
|
599
|
+
dimension: opts.dimension,
|
|
600
|
+
async embed(texts) {
|
|
601
|
+
if (texts.length === 0) return [];
|
|
602
|
+
return runEmbeddingRequest({
|
|
603
|
+
context: "openai-compatible embedding provider",
|
|
604
|
+
url,
|
|
605
|
+
dimension: opts.dimension,
|
|
606
|
+
fetchImpl: opts.fetchImpl,
|
|
607
|
+
headers: {
|
|
608
|
+
...opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
|
|
609
|
+
...opts.headers
|
|
610
|
+
},
|
|
611
|
+
body: {
|
|
612
|
+
model: opts.model,
|
|
613
|
+
input: texts
|
|
614
|
+
},
|
|
615
|
+
parser: parseOpenAIResponse
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
async healthcheck() {
|
|
619
|
+
await this.embed(["healthcheck"]);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function createLocalHttpEmbeddingProvider(opts) {
|
|
624
|
+
const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
625
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
626
|
+
const id = stableProviderId(`local-http:${opts.model}`, providerDescriptor);
|
|
627
|
+
return {
|
|
628
|
+
id,
|
|
629
|
+
model: opts.model,
|
|
630
|
+
dimension: opts.dimension,
|
|
631
|
+
async embed(texts) {
|
|
632
|
+
if (texts.length === 0) return [];
|
|
633
|
+
return runEmbeddingRequest({
|
|
634
|
+
context: "local-http embedding provider",
|
|
635
|
+
url,
|
|
636
|
+
dimension: opts.dimension,
|
|
637
|
+
fetchImpl: opts.fetchImpl,
|
|
638
|
+
headers: opts.headers,
|
|
639
|
+
body: {
|
|
640
|
+
model: opts.model,
|
|
641
|
+
input: texts
|
|
642
|
+
},
|
|
643
|
+
parser: parseLocalHttpResponse
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
async healthcheck() {
|
|
647
|
+
await this.embed(["healthcheck"]);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
652
|
+
return trimTrailingSlashes(baseUrl);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/search/providers.ts
|
|
656
|
+
function parseDimension(raw) {
|
|
657
|
+
if (!raw) return void 0;
|
|
658
|
+
const value = Number.parseInt(raw, 10);
|
|
659
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
660
|
+
}
|
|
661
|
+
function parseProvider(raw) {
|
|
662
|
+
if (!raw) return null;
|
|
663
|
+
if (raw === "openai-compatible" || raw === "local-http") {
|
|
664
|
+
return raw;
|
|
665
|
+
}
|
|
666
|
+
throw new Error(`Unsupported embedding provider: ${raw}`);
|
|
667
|
+
}
|
|
668
|
+
function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
669
|
+
const provider = parseProvider(env.AGENT_MEMORY_EMBEDDING_PROVIDER);
|
|
670
|
+
if (!provider) return null;
|
|
671
|
+
const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
|
|
672
|
+
const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
|
|
673
|
+
const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
|
|
674
|
+
if (!baseUrl) {
|
|
675
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
|
|
676
|
+
}
|
|
677
|
+
if (!model) {
|
|
678
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
|
|
679
|
+
}
|
|
680
|
+
if (!dimension) {
|
|
681
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_DIMENSION is required when embeddings are enabled");
|
|
682
|
+
}
|
|
683
|
+
if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
|
|
684
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
provider,
|
|
688
|
+
baseUrl,
|
|
689
|
+
model,
|
|
690
|
+
dimension,
|
|
691
|
+
apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function createEmbeddingProvider(input, opts) {
|
|
695
|
+
const normalized = {
|
|
696
|
+
...input,
|
|
697
|
+
baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
|
|
698
|
+
};
|
|
699
|
+
if (normalized.provider === "openai-compatible") {
|
|
700
|
+
return createOpenAICompatibleEmbeddingProvider({
|
|
701
|
+
baseUrl: normalized.baseUrl,
|
|
702
|
+
model: normalized.model,
|
|
703
|
+
dimension: normalized.dimension,
|
|
704
|
+
apiKey: normalized.apiKey,
|
|
705
|
+
fetchImpl: opts?.fetchImpl
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return createLocalHttpEmbeddingProvider({
|
|
709
|
+
baseUrl: normalized.baseUrl,
|
|
710
|
+
model: normalized.model,
|
|
711
|
+
dimension: normalized.dimension,
|
|
712
|
+
fetchImpl: opts?.fetchImpl
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function resolveEmbeddingProviderConfig(opts) {
|
|
716
|
+
const envConfig = getEmbeddingProviderConfigFromEnv(opts?.env);
|
|
717
|
+
if (!envConfig && !opts?.config?.provider) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const provider = opts?.config?.provider ?? envConfig?.provider;
|
|
721
|
+
const baseUrl = opts?.config?.baseUrl ?? envConfig?.baseUrl;
|
|
722
|
+
const model = opts?.config?.model ?? envConfig?.model;
|
|
723
|
+
const dimension = opts?.config?.dimension ?? envConfig?.dimension;
|
|
724
|
+
const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
|
|
725
|
+
if (!provider || !baseUrl || !model || !dimension) {
|
|
726
|
+
throw new Error("Incomplete embedding provider configuration");
|
|
727
|
+
}
|
|
728
|
+
if (provider === "openai-compatible" && !apiKey) {
|
|
729
|
+
throw new Error("OpenAI-compatible embedding providers require an API key");
|
|
730
|
+
}
|
|
731
|
+
return { provider, baseUrl, model, dimension, apiKey };
|
|
732
|
+
}
|
|
733
|
+
function getEmbeddingProvider(opts) {
|
|
734
|
+
const config = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
|
|
735
|
+
if (!config) return null;
|
|
736
|
+
return createEmbeddingProvider(config, { fetchImpl: opts?.fetchImpl });
|
|
737
|
+
}
|
|
738
|
+
function getEmbeddingProviderFromEnv(env = process.env) {
|
|
739
|
+
try {
|
|
740
|
+
return getEmbeddingProvider({ env });
|
|
741
|
+
} catch {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function getConfiguredEmbeddingProviderId(opts) {
|
|
746
|
+
try {
|
|
747
|
+
const provider = getEmbeddingProvider({ config: opts?.config, env: opts?.env });
|
|
748
|
+
return provider?.id ?? null;
|
|
749
|
+
} catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/search/vector.ts
|
|
755
|
+
function encodeVector(vector) {
|
|
756
|
+
const float32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
757
|
+
return Buffer.from(float32.buffer.slice(float32.byteOffset, float32.byteOffset + float32.byteLength));
|
|
758
|
+
}
|
|
759
|
+
function decodeVector(blob) {
|
|
760
|
+
const buffer = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
761
|
+
const aligned = buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength ? buffer.buffer : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
762
|
+
return Array.from(new Float32Array(aligned));
|
|
763
|
+
}
|
|
764
|
+
function cosineSimilarity(a, b) {
|
|
765
|
+
const length = Math.min(a.length, b.length);
|
|
766
|
+
if (length === 0 || a.length !== b.length) return 0;
|
|
767
|
+
let dot = 0;
|
|
768
|
+
let normA = 0;
|
|
769
|
+
let normB = 0;
|
|
770
|
+
for (let index = 0; index < length; index++) {
|
|
771
|
+
const left = Number(a[index] ?? 0);
|
|
772
|
+
const right = Number(b[index] ?? 0);
|
|
773
|
+
dot += left * right;
|
|
774
|
+
normA += left * left;
|
|
775
|
+
normB += right * right;
|
|
776
|
+
}
|
|
777
|
+
if (normA === 0 || normB === 0) return 0;
|
|
778
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
779
|
+
}
|
|
780
|
+
function markMemoryEmbeddingPending(db, memoryId, providerId, contentHash2) {
|
|
781
|
+
db.prepare(
|
|
782
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
783
|
+
VALUES (?, ?, ?, NULL, ?, 'pending', ?)
|
|
784
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
785
|
+
vector = NULL,
|
|
786
|
+
content_hash = excluded.content_hash,
|
|
787
|
+
status = 'pending'`
|
|
788
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
789
|
+
}
|
|
790
|
+
function markAllEmbeddingsPending(db, memoryId, contentHash2) {
|
|
791
|
+
const result = db.prepare(
|
|
792
|
+
`UPDATE embeddings
|
|
793
|
+
SET vector = NULL,
|
|
794
|
+
content_hash = ?,
|
|
795
|
+
status = 'pending'
|
|
796
|
+
WHERE memory_id = ?`
|
|
797
|
+
).run(contentHash2, memoryId);
|
|
798
|
+
return result.changes;
|
|
799
|
+
}
|
|
800
|
+
function upsertReadyEmbedding(input) {
|
|
801
|
+
input.db.prepare(
|
|
802
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
803
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)
|
|
804
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
805
|
+
vector = excluded.vector,
|
|
806
|
+
content_hash = excluded.content_hash,
|
|
807
|
+
status = 'ready'`
|
|
808
|
+
).run(newId(), input.memoryId, input.providerId, encodeVector(input.vector), input.contentHash, now());
|
|
809
|
+
}
|
|
810
|
+
function markEmbeddingFailed(db, memoryId, providerId, contentHash2) {
|
|
811
|
+
db.prepare(
|
|
812
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
813
|
+
VALUES (?, ?, ?, NULL, ?, 'failed', ?)
|
|
814
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
815
|
+
vector = NULL,
|
|
816
|
+
content_hash = excluded.content_hash,
|
|
817
|
+
status = 'failed'`
|
|
818
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
819
|
+
}
|
|
820
|
+
function searchByVector(db, queryVector, opts) {
|
|
821
|
+
const limit = opts.limit ?? 20;
|
|
822
|
+
const agentId = opts.agent_id ?? "default";
|
|
823
|
+
const minVitality = opts.min_vitality ?? 0;
|
|
824
|
+
const rows = db.prepare(
|
|
825
|
+
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
826
|
+
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
827
|
+
m.stability, m.access_count, m.last_accessed, m.created_at,
|
|
828
|
+
m.updated_at, m.source, m.agent_id, m.hash
|
|
829
|
+
FROM embeddings e
|
|
830
|
+
JOIN memories m ON m.id = e.memory_id
|
|
831
|
+
WHERE e.provider_id = ?
|
|
832
|
+
AND e.status = 'ready'
|
|
833
|
+
AND e.vector IS NOT NULL
|
|
834
|
+
AND e.content_hash = m.hash
|
|
835
|
+
AND m.agent_id = ?
|
|
836
|
+
AND m.vitality >= ?`
|
|
837
|
+
).all(opts.providerId, agentId, minVitality);
|
|
838
|
+
const scored = rows.map((row) => ({
|
|
839
|
+
provider_id: row.provider_id,
|
|
840
|
+
memory: {
|
|
841
|
+
id: row.id,
|
|
842
|
+
content: row.content,
|
|
843
|
+
type: row.type,
|
|
844
|
+
priority: row.priority,
|
|
845
|
+
emotion_val: row.emotion_val,
|
|
846
|
+
vitality: row.vitality,
|
|
847
|
+
stability: row.stability,
|
|
848
|
+
access_count: row.access_count,
|
|
849
|
+
last_accessed: row.last_accessed,
|
|
850
|
+
created_at: row.created_at,
|
|
851
|
+
updated_at: row.updated_at,
|
|
852
|
+
source: row.source,
|
|
853
|
+
agent_id: row.agent_id,
|
|
854
|
+
hash: row.hash
|
|
855
|
+
},
|
|
856
|
+
similarity: cosineSimilarity(queryVector, decodeVector(row.vector))
|
|
857
|
+
})).filter((row) => Number.isFinite(row.similarity) && row.similarity > 0).sort((left, right) => right.similarity - left.similarity).slice(0, limit);
|
|
858
|
+
return scored.map((row, index) => ({
|
|
859
|
+
memory: row.memory,
|
|
860
|
+
similarity: row.similarity,
|
|
861
|
+
rank: index + 1,
|
|
862
|
+
provider_id: row.provider_id
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
|
|
371
866
|
// src/core/memory.ts
|
|
372
867
|
function contentHash(content) {
|
|
373
|
-
return
|
|
868
|
+
return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
374
869
|
}
|
|
375
870
|
var TYPE_PRIORITY = {
|
|
376
871
|
identity: 0,
|
|
@@ -388,6 +883,19 @@ var PRIORITY_STABILITY = {
|
|
|
388
883
|
3: 14
|
|
389
884
|
// P3: 14-day half-life
|
|
390
885
|
};
|
|
886
|
+
function resolveEmbeddingProviderId(explicitProviderId) {
|
|
887
|
+
if (explicitProviderId !== void 0) {
|
|
888
|
+
return explicitProviderId;
|
|
889
|
+
}
|
|
890
|
+
return getConfiguredEmbeddingProviderId();
|
|
891
|
+
}
|
|
892
|
+
function markEmbeddingDirtyIfNeeded(db, memoryId, hash, providerId) {
|
|
893
|
+
if (!providerId) return;
|
|
894
|
+
try {
|
|
895
|
+
markMemoryEmbeddingPending(db, memoryId, providerId, hash);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
391
899
|
function createMemory(db, input) {
|
|
392
900
|
const hash = contentHash(input.content);
|
|
393
901
|
const agentId = input.agent_id ?? "default";
|
|
@@ -417,6 +925,7 @@ function createMemory(db, input) {
|
|
|
417
925
|
hash
|
|
418
926
|
);
|
|
419
927
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
928
|
+
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
420
929
|
return getMemory(db, id);
|
|
421
930
|
}
|
|
422
931
|
function getMemory(db, id) {
|
|
@@ -427,9 +936,11 @@ function updateMemory(db, id, input) {
|
|
|
427
936
|
if (!existing) return null;
|
|
428
937
|
const fields = [];
|
|
429
938
|
const values = [];
|
|
939
|
+
let nextHash = null;
|
|
430
940
|
if (input.content !== void 0) {
|
|
941
|
+
nextHash = contentHash(input.content);
|
|
431
942
|
fields.push("content = ?", "hash = ?");
|
|
432
|
-
values.push(input.content,
|
|
943
|
+
values.push(input.content, nextHash);
|
|
433
944
|
}
|
|
434
945
|
if (input.type !== void 0) {
|
|
435
946
|
fields.push("type = ?");
|
|
@@ -462,6 +973,13 @@ function updateMemory(db, id, input) {
|
|
|
462
973
|
if (input.content !== void 0) {
|
|
463
974
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
464
975
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
976
|
+
if (nextHash) {
|
|
977
|
+
try {
|
|
978
|
+
markAllEmbeddingsPending(db, id, nextHash);
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
markEmbeddingDirtyIfNeeded(db, id, nextHash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
982
|
+
}
|
|
465
983
|
}
|
|
466
984
|
return getMemory(db, id);
|
|
467
985
|
}
|
|
@@ -577,144 +1095,98 @@ function exportMemories(db, dirPath, opts) {
|
|
|
577
1095
|
return { exported, files };
|
|
578
1096
|
}
|
|
579
1097
|
|
|
580
|
-
// src/
|
|
581
|
-
var
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
/(查一下|找一下|看看|搜一下)/,
|
|
593
|
-
/(.+)是什么$/
|
|
594
|
-
],
|
|
595
|
-
temporal: [
|
|
596
|
-
// English
|
|
597
|
-
/^(when|what time|how long)\b/i,
|
|
598
|
-
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
599
|
-
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
600
|
-
// Chinese - time expressions
|
|
601
|
-
/什么时候/,
|
|
602
|
-
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
603
|
-
/(几月|几号|几点|多久|多长时间)/,
|
|
604
|
-
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
605
|
-
// Date patterns
|
|
606
|
-
/\d{4}[-/.]\d{1,2}/,
|
|
607
|
-
/\d{1,2}月\d{1,2}[日号]/,
|
|
608
|
-
// Chinese - temporal context
|
|
609
|
-
/(历史|记录|日志|以来|至今|期间)/
|
|
610
|
-
],
|
|
611
|
-
causal: [
|
|
612
|
-
// English
|
|
613
|
-
/^(why|how come|what caused)\b/i,
|
|
614
|
-
/\b(because|due to|reason|cause|result)\b/i,
|
|
615
|
-
// Chinese - causal questions
|
|
616
|
-
/为(什么|啥|何)/,
|
|
617
|
-
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
618
|
-
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
619
|
-
/(为啥|凭啥|凭什么)/,
|
|
620
|
-
// Chinese - problem/diagnosis
|
|
621
|
-
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
622
|
-
],
|
|
623
|
-
exploratory: [
|
|
624
|
-
// English
|
|
625
|
-
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
626
|
-
/^(what do you think|what about|any)\b/i,
|
|
627
|
-
/\b(overview|summary|list|compare)\b/i,
|
|
628
|
-
// Chinese - exploratory
|
|
629
|
-
/(怎么样|怎样|如何)/,
|
|
630
|
-
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
631
|
-
/(有哪些|有什么|有没有)/,
|
|
632
|
-
/(关于|对于|至于|关联)/,
|
|
633
|
-
/(总结|概括|梳理|回顾|盘点)/,
|
|
634
|
-
// Chinese - opinion/analysis
|
|
635
|
-
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
636
|
-
/(对比|比较|区别|差异|优缺点)/
|
|
637
|
-
]
|
|
638
|
-
};
|
|
639
|
-
var CN_STRUCTURE_BOOSTS = {
|
|
640
|
-
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
641
|
-
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
642
|
-
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
643
|
-
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
644
|
-
};
|
|
645
|
-
function classifyIntent(query) {
|
|
646
|
-
const scores = {
|
|
647
|
-
factual: 0,
|
|
648
|
-
exploratory: 0,
|
|
649
|
-
temporal: 0,
|
|
650
|
-
causal: 0
|
|
651
|
-
};
|
|
652
|
-
for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
|
|
653
|
-
for (const pattern of patterns) {
|
|
654
|
-
if (pattern.test(query)) {
|
|
655
|
-
scores[intent] += 1;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
1098
|
+
// src/core/path.ts
|
|
1099
|
+
var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1100
|
+
function parseUri(uri) {
|
|
1101
|
+
const match = uri.match(/^([a-z]+):\/\/(.+)$/);
|
|
1102
|
+
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
1103
|
+
return { domain: match[1], path: match[2] };
|
|
1104
|
+
}
|
|
1105
|
+
function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
|
|
1106
|
+
const { domain } = parseUri(uri);
|
|
1107
|
+
const domains = validDomains ?? DEFAULT_DOMAINS;
|
|
1108
|
+
if (!domains.has(domain)) {
|
|
1109
|
+
throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
|
|
658
1110
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
}
|
|
1111
|
+
const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
|
|
1112
|
+
if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
|
|
1113
|
+
if (agent_id && agent_id !== memoryAgent) {
|
|
1114
|
+
throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
|
|
665
1115
|
}
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
let maxIntent = "factual";
|
|
672
|
-
let maxScore = 0;
|
|
673
|
-
for (const [intent, score] of Object.entries(scores)) {
|
|
674
|
-
if (score > maxScore) {
|
|
675
|
-
maxScore = score;
|
|
676
|
-
maxIntent = intent;
|
|
677
|
-
}
|
|
1116
|
+
const agentId = agent_id ?? memoryAgent;
|
|
1117
|
+
const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
|
|
1118
|
+
if (existing) {
|
|
1119
|
+
throw new Error(`URI already exists: ${uri}`);
|
|
678
1120
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
1121
|
+
const id = newId();
|
|
1122
|
+
db.prepare(
|
|
1123
|
+
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1124
|
+
).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
|
|
1125
|
+
return getPath(db, id);
|
|
682
1126
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return { boostRecent: true, boostPriority: false, limit: 10 };
|
|
689
|
-
case "causal":
|
|
690
|
-
return { boostRecent: false, boostPriority: false, limit: 10 };
|
|
691
|
-
case "exploratory":
|
|
692
|
-
return { boostRecent: false, boostPriority: false, limit: 15 };
|
|
693
|
-
}
|
|
1127
|
+
function getPath(db, id) {
|
|
1128
|
+
return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
|
|
1129
|
+
}
|
|
1130
|
+
function getPathByUri(db, uri, agent_id = "default") {
|
|
1131
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
694
1132
|
}
|
|
695
1133
|
|
|
696
|
-
// src/
|
|
697
|
-
function
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1134
|
+
// src/sleep/boot.ts
|
|
1135
|
+
function boot(db, opts) {
|
|
1136
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1137
|
+
const corePaths = opts?.corePaths ?? [
|
|
1138
|
+
"core://agent",
|
|
1139
|
+
"core://user",
|
|
1140
|
+
"core://agent/identity",
|
|
1141
|
+
"core://user/identity"
|
|
1142
|
+
];
|
|
1143
|
+
const memories = /* @__PURE__ */ new Map();
|
|
1144
|
+
const identities = listMemories(db, { agent_id: agentId, priority: 0 });
|
|
1145
|
+
for (const mem of identities) {
|
|
1146
|
+
memories.set(mem.id, mem);
|
|
1147
|
+
recordAccess(db, mem.id, 1.1);
|
|
1148
|
+
}
|
|
1149
|
+
const bootPaths = [];
|
|
1150
|
+
for (const uri of corePaths) {
|
|
1151
|
+
const path = getPathByUri(db, uri, agentId);
|
|
1152
|
+
if (path) {
|
|
1153
|
+
bootPaths.push(uri);
|
|
1154
|
+
if (!memories.has(path.memory_id)) {
|
|
1155
|
+
const mem = getMemory(db, path.memory_id);
|
|
1156
|
+
if (mem) {
|
|
1157
|
+
memories.set(mem.id, mem);
|
|
1158
|
+
recordAccess(db, mem.id, 1.1);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
704
1161
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1162
|
+
}
|
|
1163
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
1164
|
+
if (bootEntry) {
|
|
1165
|
+
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
1166
|
+
if (bootMem) {
|
|
1167
|
+
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
1168
|
+
for (const uri of additionalUris) {
|
|
1169
|
+
const path = getPathByUri(db, uri, agentId);
|
|
1170
|
+
if (path && !memories.has(path.memory_id)) {
|
|
1171
|
+
const mem = getMemory(db, path.memory_id);
|
|
1172
|
+
if (mem) {
|
|
1173
|
+
memories.set(mem.id, mem);
|
|
1174
|
+
bootPaths.push(uri);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
710
1178
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
identityMemories: [...memories.values()],
|
|
1182
|
+
bootPaths
|
|
1183
|
+
};
|
|
716
1184
|
}
|
|
717
1185
|
|
|
1186
|
+
// src/transports/http.ts
|
|
1187
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1188
|
+
import http from "http";
|
|
1189
|
+
|
|
718
1190
|
// src/search/bm25.ts
|
|
719
1191
|
function searchBM25(db, query, opts) {
|
|
720
1192
|
const limit = opts?.limit ?? 20;
|
|
@@ -733,12 +1205,13 @@ function searchBM25(db, query, opts) {
|
|
|
733
1205
|
ORDER BY rank
|
|
734
1206
|
LIMIT ?`
|
|
735
1207
|
).all(ftsQuery, agentId, minVitality, limit);
|
|
736
|
-
return rows.map((row) => {
|
|
1208
|
+
return rows.map((row, index) => {
|
|
737
1209
|
const { score: _score, ...memoryFields } = row;
|
|
738
1210
|
return {
|
|
739
1211
|
memory: memoryFields,
|
|
740
1212
|
score: Math.abs(row.score),
|
|
741
1213
|
// FTS5 rank is negative (lower = better)
|
|
1214
|
+
rank: index + 1,
|
|
742
1215
|
matchReason: "bm25"
|
|
743
1216
|
};
|
|
744
1217
|
});
|
|
@@ -753,10 +1226,10 @@ function searchSimple(db, query, agentId, minVitality, limit) {
|
|
|
753
1226
|
ORDER BY priority ASC, updated_at DESC
|
|
754
1227
|
LIMIT ?`
|
|
755
1228
|
).all(agentId, minVitality, `%${query}%`, limit);
|
|
756
|
-
return rows.map((
|
|
757
|
-
memory
|
|
758
|
-
score: 1 / (
|
|
759
|
-
|
|
1229
|
+
return rows.map((memory, index) => ({
|
|
1230
|
+
memory,
|
|
1231
|
+
score: 1 / (index + 1),
|
|
1232
|
+
rank: index + 1,
|
|
760
1233
|
matchReason: "like"
|
|
761
1234
|
}));
|
|
762
1235
|
}
|
|
@@ -765,381 +1238,923 @@ function buildFtsQuery(text) {
|
|
|
765
1238
|
if (tokens.length === 0) return null;
|
|
766
1239
|
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
767
1240
|
}
|
|
1241
|
+
function rebuildBm25Index(db, opts) {
|
|
1242
|
+
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();
|
|
1243
|
+
const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
|
|
1244
|
+
const deleteOne = db.prepare("DELETE FROM memories_fts WHERE id = ?");
|
|
1245
|
+
const transaction = db.transaction(() => {
|
|
1246
|
+
if (!opts?.agent_id) {
|
|
1247
|
+
db.exec("DELETE FROM memories_fts");
|
|
1248
|
+
}
|
|
1249
|
+
for (const memory of memories) {
|
|
1250
|
+
if (opts?.agent_id) {
|
|
1251
|
+
deleteOne.run(memory.id);
|
|
1252
|
+
}
|
|
1253
|
+
insert.run(memory.id, tokenizeForIndex(memory.content));
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
transaction();
|
|
1257
|
+
return { reindexed: memories.length };
|
|
1258
|
+
}
|
|
768
1259
|
|
|
769
|
-
// src/search/
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1260
|
+
// src/search/hybrid.ts
|
|
1261
|
+
var PRIORITY_WEIGHT = {
|
|
1262
|
+
0: 4,
|
|
1263
|
+
1: 3,
|
|
1264
|
+
2: 2,
|
|
1265
|
+
3: 1
|
|
1266
|
+
};
|
|
1267
|
+
var PRIORITY_PRIOR = {
|
|
1268
|
+
0: 1,
|
|
1269
|
+
1: 0.75,
|
|
1270
|
+
2: 0.5,
|
|
1271
|
+
3: 0.25
|
|
1272
|
+
};
|
|
1273
|
+
function scoreBm25Only(results, limit) {
|
|
1274
|
+
return results.map((row) => {
|
|
1275
|
+
const weight = PRIORITY_WEIGHT[row.memory.priority] ?? 1;
|
|
1276
|
+
const vitality = Math.max(0.1, row.memory.vitality);
|
|
1277
|
+
return {
|
|
1278
|
+
memory: row.memory,
|
|
1279
|
+
score: row.score * weight * vitality,
|
|
1280
|
+
bm25_rank: row.rank,
|
|
1281
|
+
bm25_score: row.score
|
|
1282
|
+
};
|
|
1283
|
+
}).sort((left, right) => right.score - left.score).slice(0, limit);
|
|
1284
|
+
}
|
|
1285
|
+
function priorityPrior(priority) {
|
|
1286
|
+
return PRIORITY_PRIOR[priority] ?? 0.25;
|
|
1287
|
+
}
|
|
1288
|
+
function fusionScore(input) {
|
|
1289
|
+
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1290
|
+
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1291
|
+
return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
1292
|
+
}
|
|
1293
|
+
function fuseHybridResults(lexical, vector, limit) {
|
|
1294
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1295
|
+
for (const row of lexical) {
|
|
1296
|
+
candidates.set(row.memory.id, {
|
|
1297
|
+
memory: row.memory,
|
|
1298
|
+
score: 0,
|
|
1299
|
+
bm25_rank: row.rank,
|
|
1300
|
+
bm25_score: row.score
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
for (const row of vector) {
|
|
1304
|
+
const existing = candidates.get(row.memory.id);
|
|
1305
|
+
if (existing) {
|
|
1306
|
+
existing.vector_rank = row.rank;
|
|
1307
|
+
existing.vector_score = row.similarity;
|
|
1308
|
+
} else {
|
|
1309
|
+
candidates.set(row.memory.id, {
|
|
1310
|
+
memory: row.memory,
|
|
1311
|
+
score: 0,
|
|
1312
|
+
vector_rank: row.rank,
|
|
1313
|
+
vector_score: row.similarity
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return [...candidates.values()].map((row) => ({
|
|
1318
|
+
...row,
|
|
1319
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
1320
|
+
})).sort((left, right) => {
|
|
1321
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1322
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
1323
|
+
}).slice(0, limit);
|
|
1324
|
+
}
|
|
1325
|
+
async function searchVectorBranch(db, query, opts) {
|
|
1326
|
+
const [queryVector] = await opts.provider.embed([query]);
|
|
1327
|
+
if (!queryVector) return [];
|
|
1328
|
+
return searchByVector(db, queryVector, {
|
|
1329
|
+
providerId: opts.provider.id,
|
|
1330
|
+
agent_id: opts.agent_id,
|
|
1331
|
+
limit: opts.limit,
|
|
1332
|
+
min_vitality: opts.min_vitality
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
async function recallMemories(db, query, opts) {
|
|
1336
|
+
const limit = opts?.limit ?? 10;
|
|
1337
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1338
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
1339
|
+
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1340
|
+
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1341
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1342
|
+
const lexical = searchBM25(db, query, {
|
|
1343
|
+
agent_id: agentId,
|
|
1344
|
+
limit: lexicalLimit,
|
|
1345
|
+
min_vitality: minVitality
|
|
1346
|
+
});
|
|
1347
|
+
let vector = [];
|
|
1348
|
+
if (provider) {
|
|
1349
|
+
try {
|
|
1350
|
+
vector = await searchVectorBranch(db, query, {
|
|
1351
|
+
provider,
|
|
1352
|
+
agent_id: agentId,
|
|
1353
|
+
limit: vectorLimit,
|
|
1354
|
+
min_vitality: minVitality
|
|
1355
|
+
});
|
|
1356
|
+
} catch {
|
|
1357
|
+
vector = [];
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1361
|
+
const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
|
|
1362
|
+
if (opts?.recordAccess !== false) {
|
|
1363
|
+
for (const row of results) {
|
|
1364
|
+
recordAccess(db, row.memory.id);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
mode,
|
|
1369
|
+
providerId: provider?.id ?? null,
|
|
1370
|
+
usedVectorSearch: vector.length > 0,
|
|
1371
|
+
results
|
|
1372
|
+
};
|
|
790
1373
|
}
|
|
791
|
-
function
|
|
1374
|
+
function listReindexCandidates(db, providerId, agentId, force) {
|
|
792
1375
|
const rows = db.prepare(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1376
|
+
`SELECT m.id as memoryId,
|
|
1377
|
+
m.content as content,
|
|
1378
|
+
m.hash as contentHash,
|
|
1379
|
+
e.status as embeddingStatus,
|
|
1380
|
+
e.content_hash as embeddingHash
|
|
1381
|
+
FROM memories m
|
|
1382
|
+
LEFT JOIN embeddings e
|
|
1383
|
+
ON e.memory_id = m.id
|
|
1384
|
+
AND e.provider_id = ?
|
|
1385
|
+
WHERE m.agent_id = ?
|
|
1386
|
+
AND m.hash IS NOT NULL`
|
|
1387
|
+
).all(providerId, agentId);
|
|
1388
|
+
return rows.filter((row) => {
|
|
1389
|
+
if (force) return true;
|
|
1390
|
+
if (!row.embeddingStatus) return true;
|
|
1391
|
+
if (row.embeddingStatus !== "ready") return true;
|
|
1392
|
+
return row.embeddingHash !== row.contentHash;
|
|
1393
|
+
}).map((row) => ({
|
|
1394
|
+
memoryId: row.memoryId,
|
|
1395
|
+
content: row.content,
|
|
1396
|
+
contentHash: row.contentHash
|
|
1397
|
+
}));
|
|
796
1398
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1399
|
+
async function reindexEmbeddings(db, opts) {
|
|
1400
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1401
|
+
if (!provider) {
|
|
1402
|
+
return {
|
|
1403
|
+
enabled: false,
|
|
1404
|
+
providerId: null,
|
|
1405
|
+
scanned: 0,
|
|
1406
|
+
pending: 0,
|
|
1407
|
+
embedded: 0,
|
|
1408
|
+
failed: 0
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1412
|
+
const force = opts?.force ?? false;
|
|
1413
|
+
const batchSize = Math.max(1, opts?.batchSize ?? 16);
|
|
1414
|
+
const candidates = listReindexCandidates(db, provider.id, agentId, force);
|
|
1415
|
+
for (const candidate of candidates) {
|
|
1416
|
+
markMemoryEmbeddingPending(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
1417
|
+
}
|
|
1418
|
+
let embedded = 0;
|
|
1419
|
+
let failed = 0;
|
|
1420
|
+
for (let index = 0; index < candidates.length; index += batchSize) {
|
|
1421
|
+
const batch = candidates.slice(index, index + batchSize);
|
|
1422
|
+
try {
|
|
1423
|
+
const vectors = await provider.embed(batch.map((row) => row.content));
|
|
1424
|
+
if (vectors.length !== batch.length) {
|
|
1425
|
+
throw new Error(`Expected ${batch.length} embeddings, received ${vectors.length}`);
|
|
1426
|
+
}
|
|
1427
|
+
for (let offset = 0; offset < batch.length; offset++) {
|
|
1428
|
+
upsertReadyEmbedding({
|
|
1429
|
+
db,
|
|
1430
|
+
memoryId: batch[offset].memoryId,
|
|
1431
|
+
providerId: provider.id,
|
|
1432
|
+
vector: vectors[offset],
|
|
1433
|
+
contentHash: batch[offset].contentHash
|
|
1434
|
+
});
|
|
1435
|
+
embedded += 1;
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
for (const candidate of batch) {
|
|
1439
|
+
markEmbeddingFailed(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
1440
|
+
failed += 1;
|
|
829
1441
|
}
|
|
830
1442
|
}
|
|
831
1443
|
}
|
|
832
|
-
return
|
|
1444
|
+
return {
|
|
1445
|
+
enabled: true,
|
|
1446
|
+
providerId: provider.id,
|
|
1447
|
+
scanned: candidates.length,
|
|
1448
|
+
pending: candidates.length,
|
|
1449
|
+
embedded,
|
|
1450
|
+
failed
|
|
1451
|
+
};
|
|
833
1452
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1453
|
+
|
|
1454
|
+
// src/core/merge.ts
|
|
1455
|
+
function uniqueNonEmpty(values) {
|
|
1456
|
+
return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
|
|
1457
|
+
}
|
|
1458
|
+
function splitClauses(content) {
|
|
1459
|
+
return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
|
|
1460
|
+
}
|
|
1461
|
+
function mergeAliases(existing, incoming, content) {
|
|
1462
|
+
const aliases = uniqueNonEmpty([
|
|
1463
|
+
existing !== content ? existing : void 0,
|
|
1464
|
+
incoming !== content ? incoming : void 0
|
|
1465
|
+
]);
|
|
1466
|
+
return aliases.length > 0 ? aliases : void 0;
|
|
1467
|
+
}
|
|
1468
|
+
function replaceIdentity(context) {
|
|
1469
|
+
const content = context.incoming.content.trim();
|
|
1470
|
+
return {
|
|
1471
|
+
strategy: "replace",
|
|
1472
|
+
content,
|
|
1473
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
1474
|
+
notes: ["identity canonicalized to the newest authoritative phrasing"]
|
|
1475
|
+
};
|
|
840
1476
|
}
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
);
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1477
|
+
function appendEmotionEvidence(context) {
|
|
1478
|
+
const lines = uniqueNonEmpty([
|
|
1479
|
+
...context.existing.content.split(/\n+/),
|
|
1480
|
+
context.incoming.content
|
|
1481
|
+
]);
|
|
1482
|
+
const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
|
|
1483
|
+
return {
|
|
1484
|
+
strategy: "append_evidence",
|
|
1485
|
+
content,
|
|
1486
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
1487
|
+
notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function synthesizeKnowledge(context) {
|
|
1491
|
+
const clauses = uniqueNonEmpty([
|
|
1492
|
+
...splitClauses(context.existing.content),
|
|
1493
|
+
...splitClauses(context.incoming.content)
|
|
1494
|
+
]);
|
|
1495
|
+
const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
|
|
1496
|
+
return {
|
|
1497
|
+
strategy: "synthesize",
|
|
1498
|
+
content,
|
|
1499
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
1500
|
+
notes: ["knowledge statements synthesized into a canonical summary"]
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function compactEventTimeline(context) {
|
|
1504
|
+
const points = uniqueNonEmpty([
|
|
1505
|
+
...context.existing.content.split(/\n+/),
|
|
1506
|
+
context.incoming.content
|
|
1507
|
+
]).map((line) => line.replace(/^-\s*/, ""));
|
|
1508
|
+
const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
|
|
1509
|
+
return {
|
|
1510
|
+
strategy: "compact_timeline",
|
|
1511
|
+
content,
|
|
1512
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
1513
|
+
notes: ["event observations compacted into a single timeline window"]
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function buildMergePlan(context) {
|
|
1517
|
+
const type = context.incoming.type ?? context.existing.type;
|
|
1518
|
+
switch (type) {
|
|
1519
|
+
case "identity":
|
|
1520
|
+
return replaceIdentity(context);
|
|
1521
|
+
case "emotion":
|
|
1522
|
+
return appendEmotionEvidence(context);
|
|
1523
|
+
case "knowledge":
|
|
1524
|
+
return synthesizeKnowledge(context);
|
|
1525
|
+
case "event":
|
|
1526
|
+
return compactEventTimeline(context);
|
|
884
1527
|
}
|
|
885
|
-
out.sort((a, b) => b.score - a.score);
|
|
886
|
-
return out.slice(0, limit);
|
|
887
1528
|
}
|
|
888
1529
|
|
|
889
|
-
// src/
|
|
890
|
-
var
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
1530
|
+
// src/core/guard.ts
|
|
1531
|
+
var NEAR_EXACT_THRESHOLD = 0.93;
|
|
1532
|
+
var MERGE_THRESHOLD = 0.82;
|
|
1533
|
+
function clamp01(value) {
|
|
1534
|
+
if (!Number.isFinite(value)) return 0;
|
|
1535
|
+
return Math.max(0, Math.min(1, value));
|
|
1536
|
+
}
|
|
1537
|
+
function uniqueTokenSet(text) {
|
|
1538
|
+
return new Set(tokenize(text));
|
|
1539
|
+
}
|
|
1540
|
+
function overlapScore(left, right) {
|
|
1541
|
+
const a = new Set(left);
|
|
1542
|
+
const b = new Set(right);
|
|
1543
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
1544
|
+
let shared = 0;
|
|
1545
|
+
for (const token of a) {
|
|
1546
|
+
if (b.has(token)) shared += 1;
|
|
1547
|
+
}
|
|
1548
|
+
return shared / Math.max(a.size, b.size);
|
|
1549
|
+
}
|
|
1550
|
+
function extractEntities(text) {
|
|
1551
|
+
const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
|
|
1552
|
+
return new Set(matches.map((value) => value.trim()).filter(Boolean));
|
|
1553
|
+
}
|
|
1554
|
+
function safeDomain(uri) {
|
|
1555
|
+
if (!uri) return null;
|
|
1556
|
+
try {
|
|
1557
|
+
return parseUri(uri).domain;
|
|
1558
|
+
} catch {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
896
1561
|
}
|
|
897
|
-
function
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (!
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
936
|
-
if (!apiKey) return null;
|
|
937
|
-
const instruction = resolveInstruction(model);
|
|
938
|
-
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
1562
|
+
function getPrimaryUri(db, memoryId, agentId) {
|
|
1563
|
+
const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
|
|
1564
|
+
return row?.uri ?? null;
|
|
1565
|
+
}
|
|
1566
|
+
function uriScopeMatch(inputUri, candidateUri) {
|
|
1567
|
+
if (inputUri && candidateUri) {
|
|
1568
|
+
if (inputUri === candidateUri) return 1;
|
|
1569
|
+
const inputDomain2 = safeDomain(inputUri);
|
|
1570
|
+
const candidateDomain2 = safeDomain(candidateUri);
|
|
1571
|
+
if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
if (!inputUri && !candidateUri) {
|
|
1575
|
+
return 0.65;
|
|
1576
|
+
}
|
|
1577
|
+
const inputDomain = safeDomain(inputUri ?? null);
|
|
1578
|
+
const candidateDomain = safeDomain(candidateUri ?? null);
|
|
1579
|
+
if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
|
|
1580
|
+
return 0.75;
|
|
1581
|
+
}
|
|
1582
|
+
return 0.2;
|
|
1583
|
+
}
|
|
1584
|
+
function extractObservedAt(parts, fallback) {
|
|
1585
|
+
for (const part of parts) {
|
|
1586
|
+
if (!part) continue;
|
|
1587
|
+
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
1588
|
+
if (!match) continue;
|
|
1589
|
+
const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
|
|
1590
|
+
const parsed = new Date(iso);
|
|
1591
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1592
|
+
return parsed;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (fallback) {
|
|
1596
|
+
const parsed = new Date(fallback);
|
|
1597
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1598
|
+
return parsed;
|
|
1599
|
+
}
|
|
939
1600
|
}
|
|
940
1601
|
return null;
|
|
941
1602
|
}
|
|
942
|
-
function
|
|
943
|
-
|
|
1603
|
+
function timeProximity(input, memory, candidateUri) {
|
|
1604
|
+
if (input.type !== "event") {
|
|
1605
|
+
return 1;
|
|
1606
|
+
}
|
|
1607
|
+
const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
|
|
1608
|
+
const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
|
|
1609
|
+
if (!inputTime || !existingTime) {
|
|
1610
|
+
return 0.5;
|
|
1611
|
+
}
|
|
1612
|
+
const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1613
|
+
return clamp01(1 - diffDays / 7);
|
|
1614
|
+
}
|
|
1615
|
+
function scoreCandidate(input, candidate, candidateUri) {
|
|
1616
|
+
const lexicalOverlap = overlapScore(uniqueTokenSet(input.content), uniqueTokenSet(candidate.memory.content));
|
|
1617
|
+
const entityOverlap = Math.max(
|
|
1618
|
+
overlapScore(extractEntities(input.content), extractEntities(candidate.memory.content)),
|
|
1619
|
+
lexicalOverlap * 0.75
|
|
1620
|
+
);
|
|
1621
|
+
const uriMatch = uriScopeMatch(input.uri, candidateUri);
|
|
1622
|
+
const temporal = timeProximity(input, candidate.memory, candidateUri);
|
|
1623
|
+
const semantic = clamp01(candidate.vector_score ?? lexicalOverlap);
|
|
1624
|
+
const dedupScore = clamp01(
|
|
1625
|
+
0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
|
|
1626
|
+
);
|
|
1627
|
+
return {
|
|
1628
|
+
semantic_similarity: semantic,
|
|
1629
|
+
lexical_overlap: lexicalOverlap,
|
|
1630
|
+
uri_scope_match: uriMatch,
|
|
1631
|
+
entity_overlap: entityOverlap,
|
|
1632
|
+
time_proximity: temporal,
|
|
1633
|
+
dedup_score: dedupScore
|
|
1634
|
+
};
|
|
944
1635
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1636
|
+
async function recallCandidates(db, input, agentId) {
|
|
1637
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
1638
|
+
const response = await recallMemories(db, input.content, {
|
|
1639
|
+
agent_id: agentId,
|
|
1640
|
+
limit: Math.max(6, input.candidateLimit ?? 8),
|
|
1641
|
+
lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
1642
|
+
vectorLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
1643
|
+
provider,
|
|
1644
|
+
recordAccess: false
|
|
951
1645
|
});
|
|
1646
|
+
return response.results.filter((row) => row.memory.type === input.type).map((row) => {
|
|
1647
|
+
const uri = getPrimaryUri(db, row.memory.id, agentId);
|
|
1648
|
+
return {
|
|
1649
|
+
result: row,
|
|
1650
|
+
uri,
|
|
1651
|
+
domain: safeDomain(uri),
|
|
1652
|
+
score: scoreCandidate(input, row, uri)
|
|
1653
|
+
};
|
|
1654
|
+
}).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
|
|
952
1655
|
}
|
|
953
|
-
function
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1656
|
+
function fourCriterionGate(input) {
|
|
1657
|
+
const content = input.content.trim();
|
|
1658
|
+
const failed = [];
|
|
1659
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
1660
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
1661
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
1662
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
1663
|
+
const tokens = tokenize(content);
|
|
1664
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
1665
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
1666
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
1667
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
1668
|
+
const hasNumbers = /\d+/.test(content);
|
|
1669
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
1670
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
1671
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
1672
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
1673
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
1674
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
1675
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
1676
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
1677
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
1678
|
+
let coherence = 1;
|
|
1679
|
+
if (allCaps) coherence -= 0.5;
|
|
1680
|
+
if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
|
|
1681
|
+
if (excessiveRepetition) coherence -= 0.5;
|
|
1682
|
+
coherence = Math.max(0, coherence);
|
|
1683
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
972
1684
|
return {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
async embed(text) {
|
|
977
|
-
return requestEmbedding(text);
|
|
978
|
-
},
|
|
979
|
-
async embedQuery(query) {
|
|
980
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
981
|
-
}
|
|
1685
|
+
pass: failed.length === 0,
|
|
1686
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
1687
|
+
failedCriteria: failed
|
|
982
1688
|
};
|
|
983
1689
|
}
|
|
984
|
-
function
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const body = await resp.text().catch(() => "");
|
|
1001
|
-
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1690
|
+
async function guard(db, input) {
|
|
1691
|
+
const hash = contentHash(input.content);
|
|
1692
|
+
const agentId = input.agent_id ?? "default";
|
|
1693
|
+
const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
|
|
1694
|
+
if (exactMatch) {
|
|
1695
|
+
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
1696
|
+
}
|
|
1697
|
+
if (input.uri) {
|
|
1698
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
1699
|
+
if (existingPath) {
|
|
1700
|
+
return {
|
|
1701
|
+
action: "update",
|
|
1702
|
+
reason: `URI ${input.uri} already exists, updating canonical content`,
|
|
1703
|
+
existingId: existingPath.memory_id,
|
|
1704
|
+
updatedContent: input.content
|
|
1705
|
+
};
|
|
1002
1706
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1707
|
+
}
|
|
1708
|
+
const gateResult = fourCriterionGate(input);
|
|
1709
|
+
if (!gateResult.pass) {
|
|
1710
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
1711
|
+
}
|
|
1712
|
+
if (input.conservative) {
|
|
1713
|
+
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
1714
|
+
}
|
|
1715
|
+
const candidates = await recallCandidates(db, input, agentId);
|
|
1716
|
+
const best = candidates[0];
|
|
1717
|
+
if (!best) {
|
|
1718
|
+
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
1719
|
+
}
|
|
1720
|
+
const score = best.score;
|
|
1721
|
+
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
1722
|
+
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
1723
|
+
return {
|
|
1724
|
+
action: shouldUpdateMetadata ? "update" : "skip",
|
|
1725
|
+
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
1726
|
+
existingId: best.result.memory.id,
|
|
1727
|
+
score
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
1731
|
+
const mergePlan = buildMergePlan({
|
|
1732
|
+
existing: best.result.memory,
|
|
1733
|
+
incoming: {
|
|
1734
|
+
content: input.content,
|
|
1735
|
+
type: input.type,
|
|
1736
|
+
source: input.source
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
return {
|
|
1740
|
+
action: "merge",
|
|
1741
|
+
reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
|
|
1742
|
+
existingId: best.result.memory.id,
|
|
1743
|
+
mergedContent: mergePlan.content,
|
|
1744
|
+
mergePlan,
|
|
1745
|
+
score
|
|
1746
|
+
};
|
|
1006
1747
|
}
|
|
1007
1748
|
return {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
async embed(text) {
|
|
1012
|
-
return requestEmbedding(text);
|
|
1013
|
-
},
|
|
1014
|
-
async embedQuery(query) {
|
|
1015
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1016
|
-
}
|
|
1749
|
+
action: "add",
|
|
1750
|
+
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
1751
|
+
score
|
|
1017
1752
|
};
|
|
1018
1753
|
}
|
|
1019
1754
|
|
|
1020
|
-
// src/
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const vector = await provider.embed(text);
|
|
1029
|
-
upsertEmbedding(db, {
|
|
1030
|
-
agent_id: row.agent_id,
|
|
1031
|
-
memory_id: row.id,
|
|
1032
|
-
model,
|
|
1033
|
-
vector
|
|
1034
|
-
});
|
|
1035
|
-
return true;
|
|
1755
|
+
// src/sleep/sync.ts
|
|
1756
|
+
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
1757
|
+
if (!uri) return;
|
|
1758
|
+
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
1759
|
+
try {
|
|
1760
|
+
createPath(db, memoryId, uri, void 0, void 0, agentId);
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1036
1763
|
}
|
|
1037
|
-
async function
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1764
|
+
async function syncOne(db, input) {
|
|
1765
|
+
const memInput = {
|
|
1766
|
+
content: input.content,
|
|
1767
|
+
type: input.type ?? "event",
|
|
1768
|
+
priority: input.priority,
|
|
1769
|
+
emotion_val: input.emotion_val,
|
|
1770
|
+
source: input.source,
|
|
1771
|
+
agent_id: input.agent_id,
|
|
1772
|
+
uri: input.uri,
|
|
1773
|
+
provider: input.provider,
|
|
1774
|
+
conservative: input.conservative
|
|
1775
|
+
};
|
|
1776
|
+
const guardResult = await guard(db, memInput);
|
|
1777
|
+
switch (guardResult.action) {
|
|
1778
|
+
case "skip":
|
|
1779
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
1780
|
+
case "add": {
|
|
1781
|
+
const mem = createMemory(db, memInput);
|
|
1782
|
+
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
1783
|
+
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
1784
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
1785
|
+
}
|
|
1786
|
+
case "update": {
|
|
1787
|
+
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
1788
|
+
if (guardResult.updatedContent !== void 0) {
|
|
1789
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
1790
|
+
}
|
|
1791
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1792
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1793
|
+
}
|
|
1794
|
+
case "merge": {
|
|
1795
|
+
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
1796
|
+
return { action: "skipped", reason: "Missing merge data" };
|
|
1797
|
+
}
|
|
1798
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1799
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1800
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1801
|
+
}
|
|
1054
1802
|
}
|
|
1055
|
-
return { embedded, scanned: rows.length };
|
|
1056
1803
|
}
|
|
1057
1804
|
|
|
1058
|
-
// src/
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1805
|
+
// src/app/remember.ts
|
|
1806
|
+
async function rememberMemory(db, input) {
|
|
1807
|
+
return syncOne(db, {
|
|
1808
|
+
content: input.content,
|
|
1809
|
+
type: input.type,
|
|
1810
|
+
priority: input.priority,
|
|
1811
|
+
emotion_val: input.emotion_val,
|
|
1812
|
+
uri: input.uri,
|
|
1813
|
+
source: input.source,
|
|
1814
|
+
agent_id: input.agent_id,
|
|
1815
|
+
provider: input.provider,
|
|
1816
|
+
conservative: input.conservative
|
|
1817
|
+
});
|
|
1064
1818
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1819
|
+
|
|
1820
|
+
// src/app/recall.ts
|
|
1821
|
+
async function recallMemory(db, input) {
|
|
1822
|
+
return recallMemories(db, input.query, {
|
|
1823
|
+
agent_id: input.agent_id,
|
|
1824
|
+
limit: input.limit,
|
|
1825
|
+
min_vitality: input.min_vitality,
|
|
1826
|
+
lexicalLimit: input.lexicalLimit,
|
|
1827
|
+
vectorLimit: input.vectorLimit,
|
|
1828
|
+
provider: input.provider,
|
|
1829
|
+
recordAccess: input.recordAccess
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/app/feedback.ts
|
|
1834
|
+
function clamp012(value) {
|
|
1835
|
+
if (!Number.isFinite(value)) return 0;
|
|
1836
|
+
return Math.max(0, Math.min(1, value));
|
|
1837
|
+
}
|
|
1838
|
+
function recordFeedbackEvent(db, input) {
|
|
1839
|
+
const id = newId();
|
|
1840
|
+
const created_at = now();
|
|
1841
|
+
const agentId = input.agent_id ?? "default";
|
|
1842
|
+
const useful = input.useful ? 1 : 0;
|
|
1843
|
+
const value = input.useful ? 1 : 0;
|
|
1844
|
+
const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
|
|
1845
|
+
const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
|
|
1846
|
+
if (!exists) {
|
|
1847
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
1070
1848
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1849
|
+
try {
|
|
1850
|
+
db.prepare(
|
|
1851
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
1852
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1853
|
+
).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
|
|
1854
|
+
} catch {
|
|
1855
|
+
db.prepare(
|
|
1856
|
+
`INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
|
|
1857
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1858
|
+
).run(id, input.memory_id, eventType, value, created_at);
|
|
1075
1859
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1860
|
+
return {
|
|
1861
|
+
id,
|
|
1862
|
+
memory_id: input.memory_id,
|
|
1863
|
+
source: input.source,
|
|
1864
|
+
useful: input.useful,
|
|
1865
|
+
agent_id: agentId,
|
|
1866
|
+
created_at,
|
|
1867
|
+
value
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function getFeedbackSummary(db, memoryId, agentId) {
|
|
1871
|
+
try {
|
|
1872
|
+
const row = db.prepare(
|
|
1873
|
+
`SELECT COUNT(*) as total,
|
|
1874
|
+
COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
|
|
1875
|
+
COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
|
|
1876
|
+
FROM feedback_events
|
|
1877
|
+
WHERE memory_id = ?
|
|
1878
|
+
AND (? IS NULL OR agent_id = ?)`
|
|
1879
|
+
).get(memoryId, agentId ?? null, agentId ?? null);
|
|
1880
|
+
if (!row || row.total === 0) {
|
|
1881
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
total: row.total,
|
|
1885
|
+
useful: row.useful,
|
|
1886
|
+
not_useful: row.not_useful,
|
|
1887
|
+
score: clamp012(row.useful / row.total)
|
|
1888
|
+
};
|
|
1889
|
+
} catch {
|
|
1890
|
+
const row = db.prepare(
|
|
1891
|
+
`SELECT COUNT(*) as total,
|
|
1892
|
+
COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
|
|
1893
|
+
COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
|
|
1894
|
+
COALESCE(AVG(value), 0.5) as avg_value
|
|
1895
|
+
FROM feedback_events
|
|
1896
|
+
WHERE memory_id = ?`
|
|
1897
|
+
).get(memoryId);
|
|
1898
|
+
if (!row || row.total === 0) {
|
|
1899
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
total: row.total,
|
|
1903
|
+
useful: row.useful,
|
|
1904
|
+
not_useful: row.not_useful,
|
|
1905
|
+
score: clamp012(row.avg_value)
|
|
1906
|
+
};
|
|
1080
1907
|
}
|
|
1081
|
-
const id = newId();
|
|
1082
|
-
db.prepare(
|
|
1083
|
-
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1084
|
-
).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
|
|
1085
|
-
return getPath(db, id);
|
|
1086
1908
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1909
|
+
|
|
1910
|
+
// src/app/surface.ts
|
|
1911
|
+
var INTENT_PRIORS = {
|
|
1912
|
+
factual: {
|
|
1913
|
+
identity: 0.25,
|
|
1914
|
+
emotion: 0.15,
|
|
1915
|
+
knowledge: 1,
|
|
1916
|
+
event: 0.8
|
|
1917
|
+
},
|
|
1918
|
+
preference: {
|
|
1919
|
+
identity: 1,
|
|
1920
|
+
emotion: 0.85,
|
|
1921
|
+
knowledge: 0.55,
|
|
1922
|
+
event: 0.25
|
|
1923
|
+
},
|
|
1924
|
+
temporal: {
|
|
1925
|
+
identity: 0.15,
|
|
1926
|
+
emotion: 0.35,
|
|
1927
|
+
knowledge: 0.5,
|
|
1928
|
+
event: 1
|
|
1929
|
+
},
|
|
1930
|
+
planning: {
|
|
1931
|
+
identity: 0.65,
|
|
1932
|
+
emotion: 0.2,
|
|
1933
|
+
knowledge: 1,
|
|
1934
|
+
event: 0.6
|
|
1935
|
+
},
|
|
1936
|
+
design: {
|
|
1937
|
+
identity: 0.8,
|
|
1938
|
+
emotion: 0.35,
|
|
1939
|
+
knowledge: 1,
|
|
1940
|
+
event: 0.25
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
|
|
1944
|
+
var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
|
|
1945
|
+
var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
|
|
1946
|
+
var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
|
|
1947
|
+
var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
|
|
1948
|
+
function clamp013(value) {
|
|
1949
|
+
if (!Number.isFinite(value)) return 0;
|
|
1950
|
+
return Math.max(0, Math.min(1, value));
|
|
1951
|
+
}
|
|
1952
|
+
function uniqueTokenSet2(values) {
|
|
1953
|
+
return new Set(
|
|
1954
|
+
values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
|
|
1955
|
+
);
|
|
1089
1956
|
}
|
|
1090
|
-
function
|
|
1091
|
-
|
|
1957
|
+
function overlapScore2(left, right) {
|
|
1958
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
1959
|
+
let shared = 0;
|
|
1960
|
+
for (const token of left) {
|
|
1961
|
+
if (right.has(token)) shared += 1;
|
|
1962
|
+
}
|
|
1963
|
+
return clamp013(shared / Math.max(left.size, right.size));
|
|
1092
1964
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1965
|
+
function rankScore(rank, window) {
|
|
1966
|
+
if (!rank) return 0;
|
|
1967
|
+
return clamp013(1 - (rank - 1) / Math.max(window, 1));
|
|
1968
|
+
}
|
|
1969
|
+
function topicLabel(...parts) {
|
|
1970
|
+
const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
|
|
1971
|
+
const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
1972
|
+
return label || "context";
|
|
1973
|
+
}
|
|
1974
|
+
function intentKeywordBoost(memory, intent) {
|
|
1975
|
+
const content = memory.content;
|
|
1976
|
+
switch (intent) {
|
|
1977
|
+
case "design":
|
|
1978
|
+
return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
|
|
1979
|
+
case "planning":
|
|
1980
|
+
return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
|
|
1981
|
+
case "factual":
|
|
1982
|
+
return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1983
|
+
case "temporal":
|
|
1984
|
+
return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1985
|
+
case "preference":
|
|
1986
|
+
return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
|
|
1108
1987
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1988
|
+
}
|
|
1989
|
+
function intentMatch(memory, intent) {
|
|
1990
|
+
if (!intent) return 0;
|
|
1991
|
+
const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
|
|
1992
|
+
return clamp013(prior * intentKeywordBoost(memory, intent));
|
|
1993
|
+
}
|
|
1994
|
+
function buildReasonCodes(input) {
|
|
1995
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
1996
|
+
reasons.add(`type:${input.memory.type}`);
|
|
1997
|
+
if (input.semanticScore > 0.2) {
|
|
1998
|
+
reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
|
|
1999
|
+
}
|
|
2000
|
+
if (input.lexicalScore > 0.2 && input.query) {
|
|
2001
|
+
reasons.add(`lexical:${topicLabel(input.query)}`);
|
|
2002
|
+
}
|
|
2003
|
+
if (input.taskMatch > 0.2) {
|
|
2004
|
+
reasons.add(`task:${topicLabel(input.task, input.intent)}`);
|
|
2005
|
+
}
|
|
2006
|
+
if (input.intent) {
|
|
2007
|
+
reasons.add(`intent:${input.intent}`);
|
|
2008
|
+
}
|
|
2009
|
+
if (input.feedbackScore >= 0.67) {
|
|
2010
|
+
reasons.add("feedback:reinforced");
|
|
2011
|
+
} else if (input.feedbackScore <= 0.33) {
|
|
2012
|
+
reasons.add("feedback:negative");
|
|
2013
|
+
}
|
|
2014
|
+
return [...reasons];
|
|
2015
|
+
}
|
|
2016
|
+
function collectBranch(signals, rows, key, similarity) {
|
|
2017
|
+
for (const row of rows) {
|
|
2018
|
+
const existing = signals.get(row.memory.id) ?? { memory: row.memory };
|
|
2019
|
+
const currentRank = existing[key];
|
|
2020
|
+
if (currentRank === void 0 || row.rank < currentRank) {
|
|
2021
|
+
existing[key] = row.rank;
|
|
2022
|
+
}
|
|
2023
|
+
if (similarity) {
|
|
2024
|
+
const currentSimilarity = similarity.get(row.memory.id);
|
|
2025
|
+
if (currentSimilarity !== void 0) {
|
|
2026
|
+
existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
|
|
1120
2027
|
}
|
|
1121
2028
|
}
|
|
2029
|
+
signals.set(row.memory.id, existing);
|
|
1122
2030
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
2031
|
+
}
|
|
2032
|
+
async function surfaceMemories(db, input) {
|
|
2033
|
+
const agentId = input.agent_id ?? "default";
|
|
2034
|
+
const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
|
|
2035
|
+
const lexicalWindow = Math.max(24, limit * 6);
|
|
2036
|
+
const minVitality = input.min_vitality ?? 0.05;
|
|
2037
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
2038
|
+
const signals = /* @__PURE__ */ new Map();
|
|
2039
|
+
const trimmedQuery = input.query?.trim();
|
|
2040
|
+
const trimmedTask = input.task?.trim();
|
|
2041
|
+
const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
|
|
2042
|
+
const queryTokens = uniqueTokenSet2([trimmedQuery, ...recentTurns]);
|
|
2043
|
+
const taskTokens = uniqueTokenSet2([trimmedTask]);
|
|
2044
|
+
if (trimmedQuery) {
|
|
2045
|
+
collectBranch(
|
|
2046
|
+
signals,
|
|
2047
|
+
searchBM25(db, trimmedQuery, {
|
|
2048
|
+
agent_id: agentId,
|
|
2049
|
+
limit: lexicalWindow,
|
|
2050
|
+
min_vitality: minVitality
|
|
2051
|
+
}),
|
|
2052
|
+
"queryRank"
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
if (trimmedTask) {
|
|
2056
|
+
collectBranch(
|
|
2057
|
+
signals,
|
|
2058
|
+
searchBM25(db, trimmedTask, {
|
|
2059
|
+
agent_id: agentId,
|
|
2060
|
+
limit: lexicalWindow,
|
|
2061
|
+
min_vitality: minVitality
|
|
2062
|
+
}),
|
|
2063
|
+
"taskRank"
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (recentTurns.length > 0) {
|
|
2067
|
+
collectBranch(
|
|
2068
|
+
signals,
|
|
2069
|
+
searchBM25(db, recentTurns.join(" "), {
|
|
2070
|
+
agent_id: agentId,
|
|
2071
|
+
limit: lexicalWindow,
|
|
2072
|
+
min_vitality: minVitality
|
|
2073
|
+
}),
|
|
2074
|
+
"recentRank"
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
|
|
2078
|
+
if (provider && semanticQuery) {
|
|
2079
|
+
try {
|
|
2080
|
+
const [queryVector] = await provider.embed([semanticQuery]);
|
|
2081
|
+
if (queryVector) {
|
|
2082
|
+
const vectorRows = searchByVector(db, queryVector, {
|
|
2083
|
+
providerId: provider.id,
|
|
2084
|
+
agent_id: agentId,
|
|
2085
|
+
limit: lexicalWindow,
|
|
2086
|
+
min_vitality: minVitality
|
|
2087
|
+
});
|
|
2088
|
+
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
2089
|
+
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
1137
2090
|
}
|
|
2091
|
+
} catch {
|
|
1138
2092
|
}
|
|
1139
2093
|
}
|
|
2094
|
+
const fallbackMemories = listMemories(db, {
|
|
2095
|
+
agent_id: agentId,
|
|
2096
|
+
min_vitality: minVitality,
|
|
2097
|
+
limit: Math.max(48, lexicalWindow)
|
|
2098
|
+
});
|
|
2099
|
+
for (const memory of fallbackMemories) {
|
|
2100
|
+
if (!signals.has(memory.id)) {
|
|
2101
|
+
signals.set(memory.id, { memory });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).map((memory) => {
|
|
2105
|
+
const signal = signals.get(memory.id) ?? { memory };
|
|
2106
|
+
const memoryTokens = new Set(tokenize(memory.content));
|
|
2107
|
+
const lexicalOverlap = overlapScore2(memoryTokens, queryTokens);
|
|
2108
|
+
const taskOverlap = overlapScore2(memoryTokens, taskTokens);
|
|
2109
|
+
const lexicalScore = clamp013(
|
|
2110
|
+
0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
|
|
2111
|
+
);
|
|
2112
|
+
const semanticScore = signal.semanticSimilarity !== void 0 ? clamp013(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp013(lexicalOverlap * 0.7) : 0;
|
|
2113
|
+
const intentScore = intentMatch(memory, input.intent);
|
|
2114
|
+
const taskMatch = trimmedTask ? clamp013(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
|
|
2115
|
+
const priorityScore = priorityPrior(memory.priority);
|
|
2116
|
+
const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
|
|
2117
|
+
const feedbackScore = feedbackSummary.score;
|
|
2118
|
+
const score = clamp013(
|
|
2119
|
+
0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
|
|
2120
|
+
);
|
|
2121
|
+
return {
|
|
2122
|
+
memory,
|
|
2123
|
+
score,
|
|
2124
|
+
semantic_score: semanticScore,
|
|
2125
|
+
lexical_score: lexicalScore,
|
|
2126
|
+
task_match: taskMatch,
|
|
2127
|
+
vitality: memory.vitality,
|
|
2128
|
+
priority_prior: priorityScore,
|
|
2129
|
+
feedback_score: feedbackScore,
|
|
2130
|
+
feedback_summary: feedbackSummary,
|
|
2131
|
+
reason_codes: buildReasonCodes({
|
|
2132
|
+
memory,
|
|
2133
|
+
query: semanticQuery || trimmedQuery,
|
|
2134
|
+
task: trimmedTask,
|
|
2135
|
+
intent: input.intent,
|
|
2136
|
+
semanticScore,
|
|
2137
|
+
lexicalScore,
|
|
2138
|
+
taskMatch,
|
|
2139
|
+
feedbackScore
|
|
2140
|
+
}),
|
|
2141
|
+
lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
|
|
2142
|
+
semantic_rank: signal.semanticRank,
|
|
2143
|
+
semantic_similarity: signal.semanticSimilarity
|
|
2144
|
+
};
|
|
2145
|
+
}).sort((left, right) => {
|
|
2146
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
2147
|
+
if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
|
|
2148
|
+
if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
|
|
2149
|
+
if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
|
|
2150
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
2151
|
+
}).slice(0, limit);
|
|
1140
2152
|
return {
|
|
1141
|
-
|
|
1142
|
-
|
|
2153
|
+
count: results.length,
|
|
2154
|
+
query: trimmedQuery,
|
|
2155
|
+
task: trimmedTask,
|
|
2156
|
+
intent: input.intent,
|
|
2157
|
+
results
|
|
1143
2158
|
};
|
|
1144
2159
|
}
|
|
1145
2160
|
|
|
@@ -1203,74 +2218,100 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
1203
2218
|
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
1204
2219
|
}
|
|
1205
2220
|
|
|
1206
|
-
// src/core/snapshot.ts
|
|
1207
|
-
function createSnapshot(db, memoryId, action, changedBy) {
|
|
1208
|
-
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
1209
|
-
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
1210
|
-
const id = newId();
|
|
1211
|
-
db.prepare(
|
|
1212
|
-
`INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
|
|
1213
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1214
|
-
).run(id, memoryId, memory.content, changedBy ?? null, action, now());
|
|
1215
|
-
return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
2221
|
// src/sleep/tidy.ts
|
|
1219
2222
|
function runTidy(db, opts) {
|
|
1220
2223
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
1221
|
-
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1222
2224
|
const agentId = opts?.agent_id;
|
|
1223
2225
|
let archived = 0;
|
|
1224
|
-
let orphansCleaned = 0;
|
|
1225
|
-
let snapshotsPruned = 0;
|
|
1226
2226
|
const transaction = db.transaction(() => {
|
|
1227
2227
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
1228
2228
|
for (const mem of decayed) {
|
|
1229
|
-
try {
|
|
1230
|
-
createSnapshot(db, mem.id, "delete", "tidy");
|
|
1231
|
-
} catch {
|
|
1232
|
-
}
|
|
1233
2229
|
deleteMemory(db, mem.id);
|
|
1234
|
-
archived
|
|
2230
|
+
archived += 1;
|
|
1235
2231
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2232
|
+
});
|
|
2233
|
+
transaction();
|
|
2234
|
+
return { archived, orphansCleaned: 0 };
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/sleep/govern.ts
|
|
2238
|
+
function clamp014(value) {
|
|
2239
|
+
if (!Number.isFinite(value)) return 0;
|
|
2240
|
+
return Math.max(0, Math.min(1, value));
|
|
2241
|
+
}
|
|
2242
|
+
function overlapScore3(left, right) {
|
|
2243
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
2244
|
+
let shared = 0;
|
|
2245
|
+
for (const token of left) {
|
|
2246
|
+
if (right.has(token)) shared += 1;
|
|
2247
|
+
}
|
|
2248
|
+
return shared / Math.max(left.size, right.size);
|
|
2249
|
+
}
|
|
2250
|
+
function feedbackPenalty(db, memoryId) {
|
|
2251
|
+
try {
|
|
2252
|
+
const row = db.prepare(
|
|
2253
|
+
`SELECT COUNT(*) as count, COALESCE(AVG(value), 0) as avgValue
|
|
2254
|
+
FROM feedback_events
|
|
2255
|
+
WHERE memory_id = ?`
|
|
2256
|
+
).get(memoryId);
|
|
2257
|
+
if (!row || row.count === 0) return 1;
|
|
2258
|
+
return clamp014(1 - row.avgValue);
|
|
2259
|
+
} catch {
|
|
2260
|
+
return 1;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function ageScore(memory, referenceMs = Date.now()) {
|
|
2264
|
+
const createdAt = new Date(memory.created_at).getTime();
|
|
2265
|
+
if (Number.isNaN(createdAt)) return 0;
|
|
2266
|
+
const ageDays = Math.max(0, (referenceMs - createdAt) / (1e3 * 60 * 60 * 24));
|
|
2267
|
+
return clamp014(ageDays / 180);
|
|
2268
|
+
}
|
|
2269
|
+
function computeEvictionScore(input) {
|
|
2270
|
+
return clamp014(
|
|
2271
|
+
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)
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
function rankEvictionCandidates(db, opts) {
|
|
2275
|
+
const agentId = opts?.agent_id;
|
|
2276
|
+
const rows = db.prepare(
|
|
2277
|
+
agentId ? `SELECT * FROM memories WHERE agent_id = ? AND priority > 0 AND TRIM(content) != ''` : `SELECT * FROM memories WHERE priority > 0 AND TRIM(content) != ''`
|
|
2278
|
+
).all(...agentId ? [agentId] : []);
|
|
2279
|
+
const tokenSets = new Map(rows.map((memory) => [memory.id, new Set(tokenize(memory.content))]));
|
|
2280
|
+
return rows.map((memory) => {
|
|
2281
|
+
const ownTokens = tokenSets.get(memory.id) ?? /* @__PURE__ */ new Set();
|
|
2282
|
+
const redundancy = rows.filter((candidate2) => candidate2.id !== memory.id && candidate2.type === memory.type).reduce((maxOverlap, candidate2) => {
|
|
2283
|
+
const candidateTokens = tokenSets.get(candidate2.id) ?? /* @__PURE__ */ new Set();
|
|
2284
|
+
return Math.max(maxOverlap, overlapScore3(ownTokens, candidateTokens));
|
|
2285
|
+
}, 0);
|
|
2286
|
+
const candidate = {
|
|
2287
|
+
memory,
|
|
2288
|
+
redundancy_score: redundancy,
|
|
2289
|
+
age_score: ageScore(memory),
|
|
2290
|
+
low_feedback_penalty: feedbackPenalty(db, memory.id),
|
|
2291
|
+
low_priority_penalty: clamp014(memory.priority / 3),
|
|
2292
|
+
eviction_score: 0
|
|
2293
|
+
};
|
|
2294
|
+
candidate.eviction_score = computeEvictionScore({
|
|
2295
|
+
vitality: memory.vitality,
|
|
2296
|
+
redundancy_score: candidate.redundancy_score,
|
|
2297
|
+
age_score: candidate.age_score,
|
|
2298
|
+
low_feedback_penalty: candidate.low_feedback_penalty,
|
|
2299
|
+
low_priority_penalty: candidate.low_priority_penalty
|
|
2300
|
+
});
|
|
2301
|
+
return candidate;
|
|
2302
|
+
}).sort((left, right) => {
|
|
2303
|
+
if (right.eviction_score !== left.eviction_score) {
|
|
2304
|
+
return right.eviction_score - left.eviction_score;
|
|
1262
2305
|
}
|
|
2306
|
+
return left.memory.priority - right.memory.priority;
|
|
1263
2307
|
});
|
|
1264
|
-
transaction();
|
|
1265
|
-
return { archived, orphansCleaned, snapshotsPruned };
|
|
1266
2308
|
}
|
|
1267
|
-
|
|
1268
|
-
// src/sleep/govern.ts
|
|
1269
2309
|
function runGovern(db, opts) {
|
|
1270
2310
|
const agentId = opts?.agent_id;
|
|
2311
|
+
const maxMemories = opts?.maxMemories ?? 200;
|
|
1271
2312
|
let orphanPaths = 0;
|
|
1272
|
-
let orphanLinks = 0;
|
|
1273
2313
|
let emptyMemories = 0;
|
|
2314
|
+
let evicted = 0;
|
|
1274
2315
|
const transaction = db.transaction(() => {
|
|
1275
2316
|
const pathResult = agentId ? db.prepare(
|
|
1276
2317
|
`DELETE FROM paths
|
|
@@ -1278,170 +2319,749 @@ function runGovern(db, opts) {
|
|
|
1278
2319
|
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1279
2320
|
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
1280
2321
|
orphanPaths = pathResult.changes;
|
|
1281
|
-
const linkResult = agentId ? db.prepare(
|
|
1282
|
-
`DELETE FROM links WHERE
|
|
1283
|
-
agent_id = ? AND (
|
|
1284
|
-
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1285
|
-
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1286
|
-
)`
|
|
1287
|
-
).run(agentId, agentId, agentId) : db.prepare(
|
|
1288
|
-
`DELETE FROM links WHERE
|
|
1289
|
-
source_id NOT IN (SELECT id FROM memories) OR
|
|
1290
|
-
target_id NOT IN (SELECT id FROM memories)`
|
|
1291
|
-
).run();
|
|
1292
|
-
orphanLinks = linkResult.changes;
|
|
1293
2322
|
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();
|
|
1294
2323
|
emptyMemories = emptyResult.changes;
|
|
2324
|
+
const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
|
|
2325
|
+
const excess = Math.max(0, total - maxMemories);
|
|
2326
|
+
if (excess <= 0) return;
|
|
2327
|
+
const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
|
|
2328
|
+
for (const candidate of candidates) {
|
|
2329
|
+
deleteMemory(db, candidate.memory.id);
|
|
2330
|
+
evicted += 1;
|
|
2331
|
+
}
|
|
1295
2332
|
});
|
|
1296
2333
|
transaction();
|
|
1297
|
-
return { orphanPaths,
|
|
2334
|
+
return { orphanPaths, emptyMemories, evicted };
|
|
1298
2335
|
}
|
|
1299
2336
|
|
|
1300
|
-
// src/
|
|
1301
|
-
function
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
return
|
|
2337
|
+
// src/sleep/jobs.ts
|
|
2338
|
+
function parseCheckpoint(raw) {
|
|
2339
|
+
if (!raw) return null;
|
|
2340
|
+
try {
|
|
2341
|
+
return JSON.parse(raw);
|
|
2342
|
+
} catch {
|
|
2343
|
+
return null;
|
|
1307
2344
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
2345
|
+
}
|
|
2346
|
+
function serializeCheckpoint(checkpoint) {
|
|
2347
|
+
if (!checkpoint) return null;
|
|
2348
|
+
return JSON.stringify(checkpoint);
|
|
2349
|
+
}
|
|
2350
|
+
function toJob(row) {
|
|
2351
|
+
if (!row) return null;
|
|
2352
|
+
return {
|
|
2353
|
+
...row,
|
|
2354
|
+
checkpoint: parseCheckpoint(row.checkpoint)
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function createInitialCheckpoint(phase) {
|
|
2358
|
+
return {
|
|
2359
|
+
requestedPhase: phase,
|
|
2360
|
+
nextPhase: phase === "all" ? "decay" : phase,
|
|
2361
|
+
completedPhases: [],
|
|
2362
|
+
phaseResults: {}
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
function createMaintenanceJob(db, phase, checkpoint = createInitialCheckpoint(phase)) {
|
|
2366
|
+
const jobId = newId();
|
|
2367
|
+
const startedAt = now();
|
|
2368
|
+
db.prepare(
|
|
2369
|
+
`INSERT INTO maintenance_jobs (job_id, phase, status, checkpoint, error, started_at, finished_at)
|
|
2370
|
+
VALUES (?, ?, 'running', ?, NULL, ?, NULL)`
|
|
2371
|
+
).run(jobId, phase, serializeCheckpoint(checkpoint), startedAt);
|
|
2372
|
+
return getMaintenanceJob(db, jobId);
|
|
2373
|
+
}
|
|
2374
|
+
function getMaintenanceJob(db, jobId) {
|
|
2375
|
+
const row = db.prepare("SELECT * FROM maintenance_jobs WHERE job_id = ?").get(jobId);
|
|
2376
|
+
return toJob(row);
|
|
2377
|
+
}
|
|
2378
|
+
function findResumableMaintenanceJob(db, phase) {
|
|
2379
|
+
const row = db.prepare(
|
|
2380
|
+
`SELECT *
|
|
2381
|
+
FROM maintenance_jobs
|
|
2382
|
+
WHERE phase = ?
|
|
2383
|
+
AND status IN ('running', 'failed')
|
|
2384
|
+
ORDER BY started_at DESC
|
|
2385
|
+
LIMIT 1`
|
|
2386
|
+
).get(phase);
|
|
2387
|
+
return toJob(row);
|
|
2388
|
+
}
|
|
2389
|
+
function updateMaintenanceCheckpoint(db, jobId, checkpoint) {
|
|
2390
|
+
db.prepare(
|
|
2391
|
+
`UPDATE maintenance_jobs
|
|
2392
|
+
SET checkpoint = ?,
|
|
2393
|
+
error = NULL,
|
|
2394
|
+
finished_at = NULL,
|
|
2395
|
+
status = 'running'
|
|
2396
|
+
WHERE job_id = ?`
|
|
2397
|
+
).run(serializeCheckpoint(checkpoint), jobId);
|
|
2398
|
+
return getMaintenanceJob(db, jobId);
|
|
2399
|
+
}
|
|
2400
|
+
function failMaintenanceJob(db, jobId, error, checkpoint) {
|
|
2401
|
+
db.prepare(
|
|
2402
|
+
`UPDATE maintenance_jobs
|
|
2403
|
+
SET status = 'failed',
|
|
2404
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
2405
|
+
error = ?,
|
|
2406
|
+
finished_at = ?
|
|
2407
|
+
WHERE job_id = ?`
|
|
2408
|
+
).run(serializeCheckpoint(checkpoint), error, now(), jobId);
|
|
2409
|
+
return getMaintenanceJob(db, jobId);
|
|
2410
|
+
}
|
|
2411
|
+
function completeMaintenanceJob(db, jobId, checkpoint) {
|
|
2412
|
+
db.prepare(
|
|
2413
|
+
`UPDATE maintenance_jobs
|
|
2414
|
+
SET status = 'completed',
|
|
2415
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
2416
|
+
error = NULL,
|
|
2417
|
+
finished_at = ?
|
|
2418
|
+
WHERE job_id = ?`
|
|
2419
|
+
).run(serializeCheckpoint(checkpoint), now(), jobId);
|
|
2420
|
+
return getMaintenanceJob(db, jobId);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// src/sleep/orchestrator.ts
|
|
2424
|
+
var DEFAULT_RUNNERS = {
|
|
2425
|
+
decay: (db, opts) => runDecay(db, opts),
|
|
2426
|
+
tidy: (db, opts) => runTidy(db, opts),
|
|
2427
|
+
govern: (db, opts) => runGovern(db, opts)
|
|
2428
|
+
};
|
|
2429
|
+
var PHASE_SEQUENCE = ["decay", "tidy", "govern"];
|
|
2430
|
+
function getSummaryStats(db, agentId) {
|
|
2431
|
+
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();
|
|
2432
|
+
return {
|
|
2433
|
+
total: row.total,
|
|
2434
|
+
avgVitality: row.avg
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function getPhaseSequence(phase) {
|
|
2438
|
+
return phase === "all" ? [...PHASE_SEQUENCE] : [phase];
|
|
2439
|
+
}
|
|
2440
|
+
function resolveJob(db, opts) {
|
|
2441
|
+
if (opts.jobId) {
|
|
2442
|
+
const job = getMaintenanceJob(db, opts.jobId);
|
|
2443
|
+
if (!job) {
|
|
2444
|
+
throw new Error(`Maintenance job not found: ${opts.jobId}`);
|
|
2445
|
+
}
|
|
2446
|
+
if (job.phase !== opts.phase) {
|
|
2447
|
+
throw new Error(`Maintenance job ${opts.jobId} phase mismatch: expected ${opts.phase}, got ${job.phase}`);
|
|
1316
2448
|
}
|
|
2449
|
+
return { job, resumed: true };
|
|
1317
2450
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const similar = db.prepare(
|
|
1323
|
-
`SELECT m.id, m.content, m.type, rank
|
|
1324
|
-
FROM memories_fts f
|
|
1325
|
-
JOIN memories m ON m.id = f.id
|
|
1326
|
-
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
1327
|
-
ORDER BY rank
|
|
1328
|
-
LIMIT 3`
|
|
1329
|
-
).all(ftsQuery, agentId);
|
|
1330
|
-
if (similar.length > 0) {
|
|
1331
|
-
const topRank = Math.abs(similar[0].rank);
|
|
1332
|
-
const tokenCount = ftsTokens.length;
|
|
1333
|
-
const dynamicThreshold = tokenCount * 1.5;
|
|
1334
|
-
if (topRank > dynamicThreshold) {
|
|
1335
|
-
const existing = similar[0];
|
|
1336
|
-
if (existing.type === input.type) {
|
|
1337
|
-
const merged = `${existing.content}
|
|
1338
|
-
|
|
1339
|
-
[Updated] ${input.content}`;
|
|
1340
|
-
return {
|
|
1341
|
-
action: "merge",
|
|
1342
|
-
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
1343
|
-
existingId: existing.id,
|
|
1344
|
-
mergedContent: merged
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
} catch {
|
|
2451
|
+
if (opts.resume !== false) {
|
|
2452
|
+
const resumable = findResumableMaintenanceJob(db, opts.phase);
|
|
2453
|
+
if (resumable) {
|
|
2454
|
+
return { job: resumable, resumed: true };
|
|
1350
2455
|
}
|
|
1351
2456
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
2457
|
+
return {
|
|
2458
|
+
job: createMaintenanceJob(db, opts.phase),
|
|
2459
|
+
resumed: false
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
function nextPhase(current, requested) {
|
|
2463
|
+
if (requested !== "all") return null;
|
|
2464
|
+
const index = PHASE_SEQUENCE.indexOf(current);
|
|
2465
|
+
return PHASE_SEQUENCE[index + 1] ?? null;
|
|
2466
|
+
}
|
|
2467
|
+
async function runReflectOrchestrator(db, opts) {
|
|
2468
|
+
const runners = {
|
|
2469
|
+
...DEFAULT_RUNNERS,
|
|
2470
|
+
...opts.runners
|
|
2471
|
+
};
|
|
2472
|
+
const before = getSummaryStats(db, opts.agent_id);
|
|
2473
|
+
const { job: baseJob, resumed } = resolveJob(db, opts);
|
|
2474
|
+
let checkpoint = baseJob.checkpoint ?? createInitialCheckpoint(opts.phase);
|
|
2475
|
+
const jobId = baseJob.job_id;
|
|
2476
|
+
const orderedPhases = getPhaseSequence(opts.phase);
|
|
2477
|
+
const startPhase = checkpoint.nextPhase ?? orderedPhases[orderedPhases.length - 1] ?? "decay";
|
|
2478
|
+
const startIndex = Math.max(0, orderedPhases.indexOf(startPhase));
|
|
2479
|
+
const phasesToRun = checkpoint.nextPhase === null ? [] : orderedPhases.slice(startIndex);
|
|
2480
|
+
const totalPhases = Math.max(orderedPhases.length, 1);
|
|
2481
|
+
opts.onProgress?.({
|
|
2482
|
+
status: "started",
|
|
2483
|
+
phase: opts.phase,
|
|
2484
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2485
|
+
jobId,
|
|
2486
|
+
detail: {
|
|
2487
|
+
resumed,
|
|
2488
|
+
nextPhase: checkpoint.nextPhase
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
try {
|
|
2492
|
+
for (const phase of phasesToRun) {
|
|
2493
|
+
const result = await Promise.resolve(runners[phase](db, { agent_id: opts.agent_id }));
|
|
2494
|
+
checkpoint = {
|
|
2495
|
+
...checkpoint,
|
|
2496
|
+
completedPhases: [.../* @__PURE__ */ new Set([...checkpoint.completedPhases, phase])],
|
|
2497
|
+
phaseResults: {
|
|
2498
|
+
...checkpoint.phaseResults,
|
|
2499
|
+
[phase]: result
|
|
2500
|
+
},
|
|
2501
|
+
nextPhase: nextPhase(phase, opts.phase)
|
|
2502
|
+
};
|
|
2503
|
+
updateMaintenanceCheckpoint(db, jobId, checkpoint);
|
|
2504
|
+
opts.onProgress?.({
|
|
2505
|
+
status: "phase-completed",
|
|
2506
|
+
phase,
|
|
2507
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2508
|
+
jobId,
|
|
2509
|
+
detail: result
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2514
|
+
const failed = failMaintenanceJob(db, jobId, message, checkpoint) ?? baseJob;
|
|
2515
|
+
opts.onProgress?.({
|
|
2516
|
+
status: "failed",
|
|
2517
|
+
phase: checkpoint.nextPhase ?? opts.phase,
|
|
2518
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
2519
|
+
jobId,
|
|
2520
|
+
detail: { error: message }
|
|
2521
|
+
});
|
|
2522
|
+
throw Object.assign(new Error(message), { job: failed, checkpoint });
|
|
1355
2523
|
}
|
|
1356
|
-
|
|
2524
|
+
const completedCheckpoint = {
|
|
2525
|
+
...checkpoint,
|
|
2526
|
+
nextPhase: null
|
|
2527
|
+
};
|
|
2528
|
+
const job = completeMaintenanceJob(db, jobId, completedCheckpoint) ?? baseJob;
|
|
2529
|
+
const after = getSummaryStats(db, opts.agent_id);
|
|
2530
|
+
opts.onProgress?.({
|
|
2531
|
+
status: "completed",
|
|
2532
|
+
phase: opts.phase,
|
|
2533
|
+
progress: 1,
|
|
2534
|
+
jobId,
|
|
2535
|
+
detail: completedCheckpoint.phaseResults
|
|
2536
|
+
});
|
|
2537
|
+
return {
|
|
2538
|
+
job,
|
|
2539
|
+
jobId,
|
|
2540
|
+
phase: opts.phase,
|
|
2541
|
+
resumed,
|
|
2542
|
+
checkpoint: completedCheckpoint,
|
|
2543
|
+
results: completedCheckpoint.phaseResults,
|
|
2544
|
+
before,
|
|
2545
|
+
after
|
|
2546
|
+
};
|
|
1357
2547
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
2548
|
+
|
|
2549
|
+
// src/app/reflect.ts
|
|
2550
|
+
async function reflectMemories(db, input) {
|
|
2551
|
+
const options = {
|
|
2552
|
+
phase: input.phase,
|
|
2553
|
+
agent_id: input.agent_id,
|
|
2554
|
+
jobId: input.jobId,
|
|
2555
|
+
resume: input.resume,
|
|
2556
|
+
runners: input.runners,
|
|
2557
|
+
onProgress: input.onProgress
|
|
2558
|
+
};
|
|
2559
|
+
return runReflectOrchestrator(db, options);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/app/status.ts
|
|
2563
|
+
function getMemoryStatus(db, input) {
|
|
2564
|
+
const agentId = input?.agent_id ?? "default";
|
|
2565
|
+
const stats = countMemories(db, agentId);
|
|
2566
|
+
const lowVitality = db.prepare(
|
|
2567
|
+
"SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?"
|
|
2568
|
+
).get(agentId);
|
|
2569
|
+
const totalPaths = db.prepare(
|
|
2570
|
+
"SELECT COUNT(*) as c FROM paths WHERE agent_id = ?"
|
|
2571
|
+
).get(agentId);
|
|
2572
|
+
const feedbackEvents = db.prepare(
|
|
2573
|
+
"SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
|
|
2574
|
+
).get(agentId);
|
|
2575
|
+
return {
|
|
2576
|
+
...stats,
|
|
2577
|
+
paths: totalPaths.c,
|
|
2578
|
+
low_vitality: lowVitality.c,
|
|
2579
|
+
feedback_events: feedbackEvents.c,
|
|
2580
|
+
agent_id: agentId
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/app/reindex.ts
|
|
2585
|
+
async function reindexMemories(db, input) {
|
|
2586
|
+
input?.onProgress?.({ status: "started", stage: "fts", progress: 0 });
|
|
2587
|
+
try {
|
|
2588
|
+
const fts = rebuildBm25Index(db, { agent_id: input?.agent_id });
|
|
2589
|
+
input?.onProgress?.({
|
|
2590
|
+
status: "stage-completed",
|
|
2591
|
+
stage: "fts",
|
|
2592
|
+
progress: 0.5,
|
|
2593
|
+
detail: fts
|
|
2594
|
+
});
|
|
2595
|
+
const embeddings = await reindexEmbeddings(db, {
|
|
2596
|
+
agent_id: input?.agent_id,
|
|
2597
|
+
provider: input?.provider,
|
|
2598
|
+
force: input?.force,
|
|
2599
|
+
batchSize: input?.batchSize
|
|
2600
|
+
});
|
|
2601
|
+
input?.onProgress?.({
|
|
2602
|
+
status: "stage-completed",
|
|
2603
|
+
stage: "embeddings",
|
|
2604
|
+
progress: 0.9,
|
|
2605
|
+
detail: embeddings
|
|
2606
|
+
});
|
|
2607
|
+
const result = { fts, embeddings };
|
|
2608
|
+
input?.onProgress?.({
|
|
2609
|
+
status: "completed",
|
|
2610
|
+
stage: "done",
|
|
2611
|
+
progress: 1,
|
|
2612
|
+
detail: result
|
|
2613
|
+
});
|
|
2614
|
+
return result;
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
input?.onProgress?.({
|
|
2617
|
+
status: "failed",
|
|
2618
|
+
stage: "done",
|
|
2619
|
+
progress: 1,
|
|
2620
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2621
|
+
});
|
|
2622
|
+
throw error;
|
|
1383
2623
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/transports/http.ts
|
|
2627
|
+
var VALID_MEMORY_TYPES = /* @__PURE__ */ new Set(["identity", "emotion", "knowledge", "event"]);
|
|
2628
|
+
var VALID_PHASES = /* @__PURE__ */ new Set(["decay", "tidy", "govern", "all"]);
|
|
2629
|
+
var VALID_INTENTS = /* @__PURE__ */ new Set(["factual", "preference", "temporal", "planning", "design"]);
|
|
2630
|
+
function json(value) {
|
|
2631
|
+
return JSON.stringify(value);
|
|
2632
|
+
}
|
|
2633
|
+
function now2() {
|
|
2634
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2635
|
+
}
|
|
2636
|
+
function sendJson(res, statusCode, payload) {
|
|
2637
|
+
res.writeHead(statusCode, {
|
|
2638
|
+
"content-type": "application/json; charset=utf-8",
|
|
2639
|
+
"cache-control": "no-store"
|
|
2640
|
+
});
|
|
2641
|
+
res.end(json(payload));
|
|
2642
|
+
}
|
|
2643
|
+
function sendError(res, statusCode, error, details) {
|
|
2644
|
+
sendJson(res, statusCode, { error, details });
|
|
2645
|
+
}
|
|
2646
|
+
function openSse(res) {
|
|
2647
|
+
res.writeHead(200, {
|
|
2648
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
2649
|
+
"cache-control": "no-cache, no-transform",
|
|
2650
|
+
connection: "keep-alive"
|
|
2651
|
+
});
|
|
2652
|
+
res.write(": connected\n\n");
|
|
2653
|
+
}
|
|
2654
|
+
function sendSse(res, event, payload) {
|
|
2655
|
+
if (res.writableEnded) return;
|
|
2656
|
+
res.write(`event: ${event}
|
|
2657
|
+
`);
|
|
2658
|
+
res.write(`data: ${json(payload)}
|
|
2659
|
+
|
|
2660
|
+
`);
|
|
2661
|
+
}
|
|
2662
|
+
async function readJsonBody(req) {
|
|
2663
|
+
const chunks = [];
|
|
2664
|
+
for await (const chunk of req) {
|
|
2665
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1386
2666
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
2667
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
2668
|
+
if (!raw) return {};
|
|
2669
|
+
try {
|
|
2670
|
+
const parsed = JSON.parse(raw);
|
|
2671
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2672
|
+
throw new Error("Request body must be a JSON object");
|
|
2673
|
+
}
|
|
2674
|
+
return parsed;
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
throw new Error(error instanceof Error ? error.message : "Invalid JSON body");
|
|
1389
2677
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
2678
|
+
}
|
|
2679
|
+
function asString(value) {
|
|
2680
|
+
return typeof value === "string" ? value : void 0;
|
|
2681
|
+
}
|
|
2682
|
+
function asBoolean(value) {
|
|
2683
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2684
|
+
}
|
|
2685
|
+
function asNumber(value) {
|
|
2686
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2687
|
+
}
|
|
2688
|
+
function asStringArray(value) {
|
|
2689
|
+
if (!Array.isArray(value)) return void 0;
|
|
2690
|
+
const result = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
2691
|
+
return result.length > 0 ? result : [];
|
|
2692
|
+
}
|
|
2693
|
+
function wantsSse(req, body) {
|
|
2694
|
+
const accept = req.headers.accept ?? "";
|
|
2695
|
+
return accept.includes("text/event-stream") || body.stream === true;
|
|
2696
|
+
}
|
|
2697
|
+
function formatRecallResponse(result) {
|
|
1392
2698
|
return {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
2699
|
+
mode: result.mode,
|
|
2700
|
+
provider_id: result.providerId,
|
|
2701
|
+
used_vector_search: result.usedVectorSearch,
|
|
2702
|
+
count: result.results.length,
|
|
2703
|
+
memories: result.results.map((row) => ({
|
|
2704
|
+
id: row.memory.id,
|
|
2705
|
+
content: row.memory.content,
|
|
2706
|
+
type: row.memory.type,
|
|
2707
|
+
priority: row.memory.priority,
|
|
2708
|
+
vitality: row.memory.vitality,
|
|
2709
|
+
score: row.score,
|
|
2710
|
+
bm25_rank: row.bm25_rank,
|
|
2711
|
+
vector_rank: row.vector_rank,
|
|
2712
|
+
bm25_score: row.bm25_score,
|
|
2713
|
+
vector_score: row.vector_score,
|
|
2714
|
+
updated_at: row.memory.updated_at
|
|
2715
|
+
}))
|
|
1396
2716
|
};
|
|
1397
2717
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
2718
|
+
function formatSurfaceResponse(result) {
|
|
2719
|
+
return {
|
|
2720
|
+
count: result.count,
|
|
2721
|
+
query: result.query,
|
|
2722
|
+
task: result.task,
|
|
2723
|
+
intent: result.intent,
|
|
2724
|
+
results: result.results.map((row) => ({
|
|
2725
|
+
id: row.memory.id,
|
|
2726
|
+
content: row.memory.content,
|
|
2727
|
+
type: row.memory.type,
|
|
2728
|
+
priority: row.memory.priority,
|
|
2729
|
+
vitality: row.memory.vitality,
|
|
2730
|
+
score: row.score,
|
|
2731
|
+
semantic_score: row.semantic_score,
|
|
2732
|
+
lexical_score: row.lexical_score,
|
|
2733
|
+
task_match: row.task_match,
|
|
2734
|
+
priority_prior: row.priority_prior,
|
|
2735
|
+
feedback_score: row.feedback_score,
|
|
2736
|
+
feedback_summary: row.feedback_summary,
|
|
2737
|
+
reason_codes: row.reason_codes,
|
|
2738
|
+
updated_at: row.memory.updated_at
|
|
2739
|
+
}))
|
|
1409
2740
|
};
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
2741
|
+
}
|
|
2742
|
+
function createJob(jobs, kind, agentId) {
|
|
2743
|
+
const job = {
|
|
2744
|
+
id: randomUUID2(),
|
|
2745
|
+
kind,
|
|
2746
|
+
status: "running",
|
|
2747
|
+
stage: "queued",
|
|
2748
|
+
progress: 0,
|
|
2749
|
+
agent_id: agentId,
|
|
2750
|
+
started_at: now2(),
|
|
2751
|
+
finished_at: null
|
|
2752
|
+
};
|
|
2753
|
+
jobs.set(job.id, job);
|
|
2754
|
+
return job;
|
|
2755
|
+
}
|
|
2756
|
+
function updateJob(job, patch) {
|
|
2757
|
+
Object.assign(job, patch);
|
|
2758
|
+
return job;
|
|
2759
|
+
}
|
|
2760
|
+
function createHttpServer(options) {
|
|
2761
|
+
const ownsDb = !options?.db;
|
|
2762
|
+
const db = options?.db ?? openDatabase({ path: options?.dbPath ?? process.env.AGENT_MEMORY_DB ?? "./agent-memory.db" });
|
|
2763
|
+
const defaultAgentId = options?.agentId ?? process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
2764
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
2765
|
+
const executeReflectJob = async (job, body, stream) => {
|
|
2766
|
+
const phase = asString(body.phase) ?? "all";
|
|
2767
|
+
if (!VALID_PHASES.has(phase)) {
|
|
2768
|
+
throw new Error(`Invalid phase: ${String(body.phase)}`);
|
|
2769
|
+
}
|
|
2770
|
+
updateJob(job, { stage: phase, progress: 0.01 });
|
|
2771
|
+
try {
|
|
2772
|
+
const result = await reflectMemories(db, {
|
|
2773
|
+
phase,
|
|
2774
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2775
|
+
runners: options?.reflectRunners,
|
|
2776
|
+
onProgress: (event) => {
|
|
2777
|
+
updateJob(job, {
|
|
2778
|
+
stage: String(event.phase),
|
|
2779
|
+
progress: event.progress,
|
|
2780
|
+
backend_job_id: event.jobId ?? job.backend_job_id
|
|
2781
|
+
});
|
|
2782
|
+
if (stream) {
|
|
2783
|
+
sendSse(stream, "progress", {
|
|
2784
|
+
job,
|
|
2785
|
+
event
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
1421
2788
|
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
2789
|
+
});
|
|
2790
|
+
updateJob(job, {
|
|
2791
|
+
status: "completed",
|
|
2792
|
+
stage: "done",
|
|
2793
|
+
progress: 1,
|
|
2794
|
+
backend_job_id: result.jobId,
|
|
2795
|
+
finished_at: now2(),
|
|
2796
|
+
result
|
|
2797
|
+
});
|
|
2798
|
+
return { job, result };
|
|
2799
|
+
} catch (error) {
|
|
2800
|
+
updateJob(job, {
|
|
2801
|
+
status: "failed",
|
|
2802
|
+
stage: "failed",
|
|
2803
|
+
progress: 1,
|
|
2804
|
+
finished_at: now2(),
|
|
2805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2806
|
+
});
|
|
2807
|
+
throw error;
|
|
1424
2808
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
2809
|
+
};
|
|
2810
|
+
const executeReindexJob = async (job, body, stream) => {
|
|
2811
|
+
updateJob(job, { stage: "fts", progress: 0.01 });
|
|
2812
|
+
try {
|
|
2813
|
+
const result = await reindexMemories(db, {
|
|
2814
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2815
|
+
provider: options?.provider,
|
|
2816
|
+
force: asBoolean(body.full) ?? false,
|
|
2817
|
+
batchSize: asNumber(body.batch_size) ?? 16,
|
|
2818
|
+
onProgress: (event) => {
|
|
2819
|
+
updateJob(job, {
|
|
2820
|
+
stage: event.stage,
|
|
2821
|
+
progress: event.progress
|
|
2822
|
+
});
|
|
2823
|
+
if (stream) {
|
|
2824
|
+
sendSse(stream, "progress", {
|
|
2825
|
+
job,
|
|
2826
|
+
event
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
updateJob(job, {
|
|
2832
|
+
status: "completed",
|
|
2833
|
+
stage: "done",
|
|
2834
|
+
progress: 1,
|
|
2835
|
+
finished_at: now2(),
|
|
2836
|
+
result
|
|
2837
|
+
});
|
|
2838
|
+
return { job, result };
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
updateJob(job, {
|
|
2841
|
+
status: "failed",
|
|
2842
|
+
stage: "failed",
|
|
2843
|
+
progress: 1,
|
|
2844
|
+
finished_at: now2(),
|
|
2845
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2846
|
+
});
|
|
2847
|
+
throw error;
|
|
1430
2848
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
2849
|
+
};
|
|
2850
|
+
const server = http.createServer(async (req, res) => {
|
|
2851
|
+
try {
|
|
2852
|
+
const method = req.method ?? "GET";
|
|
2853
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2854
|
+
const pathname = url.pathname;
|
|
2855
|
+
if (method === "GET" && pathname === "/health") {
|
|
2856
|
+
sendJson(res, 200, {
|
|
2857
|
+
ok: true,
|
|
2858
|
+
service: "agent-memory",
|
|
2859
|
+
time: now2()
|
|
2860
|
+
});
|
|
2861
|
+
return;
|
|
1434
2862
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
2863
|
+
if (method === "GET" && pathname === "/v1/status") {
|
|
2864
|
+
const agentId = url.searchParams.get("agent_id") ?? defaultAgentId;
|
|
2865
|
+
sendJson(res, 200, getMemoryStatus(db, { agent_id: agentId }));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
if (method === "GET" && pathname.startsWith("/v1/jobs/")) {
|
|
2869
|
+
const id = decodeURIComponent(pathname.slice("/v1/jobs/".length));
|
|
2870
|
+
const job = jobs.get(id);
|
|
2871
|
+
if (job) {
|
|
2872
|
+
sendJson(res, 200, job);
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const maintenanceJob = getMaintenanceJob(db, id);
|
|
2876
|
+
if (maintenanceJob) {
|
|
2877
|
+
sendJson(res, 200, maintenanceJob);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
sendError(res, 404, `Job not found: ${id}`);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (method !== "POST") {
|
|
2884
|
+
sendError(res, 404, `Route not found: ${method} ${pathname}`);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
const body = await readJsonBody(req);
|
|
2888
|
+
if (pathname === "/v1/memories") {
|
|
2889
|
+
const content = asString(body.content)?.trim();
|
|
2890
|
+
if (!content) {
|
|
2891
|
+
sendError(res, 400, "content is required");
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
const type = asString(body.type) ?? "knowledge";
|
|
2895
|
+
if (!VALID_MEMORY_TYPES.has(type)) {
|
|
2896
|
+
sendError(res, 400, `Invalid memory type: ${String(body.type)}`);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
const result = await rememberMemory(db, {
|
|
2900
|
+
content,
|
|
2901
|
+
type,
|
|
2902
|
+
uri: asString(body.uri),
|
|
2903
|
+
source: asString(body.source),
|
|
2904
|
+
emotion_val: asNumber(body.emotion_val),
|
|
2905
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2906
|
+
conservative: asBoolean(body.conservative),
|
|
2907
|
+
provider: options?.provider
|
|
2908
|
+
});
|
|
2909
|
+
sendJson(res, 200, result);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (pathname === "/v1/recall") {
|
|
2913
|
+
const query = asString(body.query)?.trim();
|
|
2914
|
+
if (!query) {
|
|
2915
|
+
sendError(res, 400, "query is required");
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const result = await recallMemory(db, {
|
|
2919
|
+
query,
|
|
2920
|
+
limit: asNumber(body.limit),
|
|
2921
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2922
|
+
provider: options?.provider
|
|
2923
|
+
});
|
|
2924
|
+
sendJson(res, 200, formatRecallResponse(result));
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
if (pathname === "/v1/surface") {
|
|
2928
|
+
const types = asStringArray(body.types)?.filter((type) => VALID_MEMORY_TYPES.has(type));
|
|
2929
|
+
const intent = asString(body.intent);
|
|
2930
|
+
if (intent !== void 0 && !VALID_INTENTS.has(intent)) {
|
|
2931
|
+
sendError(res, 400, `Invalid intent: ${intent}`);
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
const result = await surfaceMemories(db, {
|
|
2935
|
+
query: asString(body.query),
|
|
2936
|
+
task: asString(body.task),
|
|
2937
|
+
recent_turns: asStringArray(body.recent_turns),
|
|
2938
|
+
intent,
|
|
2939
|
+
types,
|
|
2940
|
+
limit: asNumber(body.limit),
|
|
2941
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId,
|
|
2942
|
+
provider: options?.provider
|
|
2943
|
+
});
|
|
2944
|
+
sendJson(res, 200, formatSurfaceResponse(result));
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
if (pathname === "/v1/feedback") {
|
|
2948
|
+
const memoryId = asString(body.memory_id)?.trim();
|
|
2949
|
+
const source = asString(body.source);
|
|
2950
|
+
const useful = asBoolean(body.useful);
|
|
2951
|
+
if (!memoryId) {
|
|
2952
|
+
sendError(res, 400, "memory_id is required");
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
if (source !== "recall" && source !== "surface") {
|
|
2956
|
+
sendError(res, 400, "source must be 'recall' or 'surface'");
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (useful === void 0) {
|
|
2960
|
+
sendError(res, 400, "useful must be boolean");
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const result = recordFeedbackEvent(db, {
|
|
2964
|
+
memory_id: memoryId,
|
|
2965
|
+
source,
|
|
2966
|
+
useful,
|
|
2967
|
+
agent_id: asString(body.agent_id) ?? defaultAgentId
|
|
2968
|
+
});
|
|
2969
|
+
sendJson(res, 200, result);
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
if (pathname === "/v1/reflect") {
|
|
2973
|
+
const agentId = asString(body.agent_id) ?? defaultAgentId;
|
|
2974
|
+
const job = createJob(jobs, "reflect", agentId);
|
|
2975
|
+
if (wantsSse(req, body)) {
|
|
2976
|
+
openSse(res);
|
|
2977
|
+
sendSse(res, "job", job);
|
|
2978
|
+
void executeReflectJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
|
|
2979
|
+
sendSse(res, "done", { job: currentJob, result: result2 });
|
|
2980
|
+
}).catch((error) => {
|
|
2981
|
+
sendSse(res, "error", {
|
|
2982
|
+
job,
|
|
2983
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2984
|
+
});
|
|
2985
|
+
}).finally(() => {
|
|
2986
|
+
if (!res.writableEnded) res.end();
|
|
2987
|
+
});
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
const result = await executeReflectJob(job, body);
|
|
2991
|
+
sendJson(res, 200, result);
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (pathname === "/v1/reindex") {
|
|
2995
|
+
const agentId = asString(body.agent_id) ?? defaultAgentId;
|
|
2996
|
+
const job = createJob(jobs, "reindex", agentId);
|
|
2997
|
+
if (wantsSse(req, body)) {
|
|
2998
|
+
openSse(res);
|
|
2999
|
+
sendSse(res, "job", job);
|
|
3000
|
+
void executeReindexJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
|
|
3001
|
+
sendSse(res, "done", { job: currentJob, result: result2 });
|
|
3002
|
+
}).catch((error) => {
|
|
3003
|
+
sendSse(res, "error", {
|
|
3004
|
+
job,
|
|
3005
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3006
|
+
});
|
|
3007
|
+
}).finally(() => {
|
|
3008
|
+
if (!res.writableEnded) res.end();
|
|
3009
|
+
});
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
const result = await executeReindexJob(job, body);
|
|
3013
|
+
sendJson(res, 200, result);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
sendError(res, 404, `Route not found: ${method} ${pathname}`);
|
|
3017
|
+
} catch (error) {
|
|
3018
|
+
sendError(res, 500, error instanceof Error ? error.message : String(error));
|
|
1438
3019
|
}
|
|
1439
|
-
}
|
|
3020
|
+
});
|
|
3021
|
+
return {
|
|
3022
|
+
server,
|
|
3023
|
+
db,
|
|
3024
|
+
jobs,
|
|
3025
|
+
listen(port = 3e3, host = "127.0.0.1") {
|
|
3026
|
+
return new Promise((resolve2, reject) => {
|
|
3027
|
+
server.once("error", reject);
|
|
3028
|
+
server.listen(port, host, () => {
|
|
3029
|
+
server.off("error", reject);
|
|
3030
|
+
const address = server.address();
|
|
3031
|
+
if (!address || typeof address === "string") {
|
|
3032
|
+
resolve2({ port, host });
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
resolve2({ port: address.port, host: address.address });
|
|
3036
|
+
});
|
|
3037
|
+
});
|
|
3038
|
+
},
|
|
3039
|
+
close() {
|
|
3040
|
+
return new Promise((resolve2, reject) => {
|
|
3041
|
+
server.close((error) => {
|
|
3042
|
+
if (ownsDb) {
|
|
3043
|
+
try {
|
|
3044
|
+
db.close();
|
|
3045
|
+
} catch {
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (error) {
|
|
3049
|
+
reject(error);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
resolve2();
|
|
3053
|
+
});
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
async function startHttpServer(options) {
|
|
3059
|
+
const service = createHttpServer(options);
|
|
3060
|
+
await service.listen(options?.port, options?.host);
|
|
3061
|
+
return service;
|
|
1440
3062
|
}
|
|
1441
3063
|
|
|
1442
3064
|
// src/bin/agent-memory.ts
|
|
1443
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1444
|
-
import { resolve, basename } from "path";
|
|
1445
3065
|
var args = process.argv.slice(2);
|
|
1446
3066
|
var command = args[0];
|
|
1447
3067
|
function getDbPath() {
|
|
@@ -1452,34 +3072,52 @@ function getAgentId() {
|
|
|
1452
3072
|
}
|
|
1453
3073
|
function printHelp() {
|
|
1454
3074
|
console.log(`
|
|
1455
|
-
\u{1F9E0} AgentMemory
|
|
3075
|
+
\u{1F9E0} AgentMemory v4 \u2014 Sleep-cycle memory for AI agents
|
|
1456
3076
|
|
|
1457
3077
|
Usage: agent-memory <command> [options]
|
|
1458
3078
|
|
|
1459
3079
|
Commands:
|
|
1460
|
-
init
|
|
1461
|
-
db:migrate
|
|
1462
|
-
embed [--limit N] Embed missing memories (requires provider)
|
|
3080
|
+
init Create database
|
|
3081
|
+
db:migrate Run schema migrations (no-op if up-to-date)
|
|
1463
3082
|
remember <content> [--uri X] [--type T] Store a memory
|
|
1464
|
-
recall <query> [--limit N]
|
|
1465
|
-
boot
|
|
1466
|
-
status
|
|
1467
|
-
reflect [decay|tidy|govern|all]
|
|
1468
|
-
reindex
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
3083
|
+
recall <query> [--limit N] Search memories (hybrid retrieval, auto-fallback to BM25)
|
|
3084
|
+
boot Load identity memories
|
|
3085
|
+
status Show statistics
|
|
3086
|
+
reflect [decay|tidy|govern|all] Run sleep cycle
|
|
3087
|
+
reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
|
|
3088
|
+
serve [--host H] [--port N] Start the HTTP/SSE API server
|
|
3089
|
+
migrate <dir> Import from Markdown files
|
|
3090
|
+
export <dir> Export memories to Markdown files
|
|
3091
|
+
help Show this help
|
|
1472
3092
|
|
|
1473
3093
|
Environment:
|
|
1474
|
-
AGENT_MEMORY_DB
|
|
1475
|
-
AGENT_MEMORY_AGENT_ID
|
|
3094
|
+
AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
|
|
3095
|
+
AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
|
|
1476
3096
|
`);
|
|
1477
3097
|
}
|
|
1478
3098
|
function getFlag(flag) {
|
|
1479
|
-
const
|
|
1480
|
-
if (
|
|
3099
|
+
const index = args.indexOf(flag);
|
|
3100
|
+
if (index >= 0 && index + 1 < args.length) return args[index + 1];
|
|
1481
3101
|
return void 0;
|
|
1482
3102
|
}
|
|
3103
|
+
function hasFlag(flag) {
|
|
3104
|
+
return args.includes(flag);
|
|
3105
|
+
}
|
|
3106
|
+
function getPositionalArgs(startIndex = 1) {
|
|
3107
|
+
const values = [];
|
|
3108
|
+
for (let index = startIndex; index < args.length; index++) {
|
|
3109
|
+
const token = args[index];
|
|
3110
|
+
if (token.startsWith("--")) {
|
|
3111
|
+
const next = args[index + 1];
|
|
3112
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
3113
|
+
index += 1;
|
|
3114
|
+
}
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
values.push(token);
|
|
3118
|
+
}
|
|
3119
|
+
return values;
|
|
3120
|
+
}
|
|
1483
3121
|
async function main() {
|
|
1484
3122
|
try {
|
|
1485
3123
|
switch (command) {
|
|
@@ -1497,22 +3135,8 @@ async function main() {
|
|
|
1497
3135
|
db.close();
|
|
1498
3136
|
break;
|
|
1499
3137
|
}
|
|
1500
|
-
case "embed": {
|
|
1501
|
-
const provider = getEmbeddingProviderFromEnv();
|
|
1502
|
-
if (!provider) {
|
|
1503
|
-
console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
|
|
1504
|
-
process.exit(1);
|
|
1505
|
-
}
|
|
1506
|
-
const db = openDatabase({ path: getDbPath() });
|
|
1507
|
-
const agentId = getAgentId();
|
|
1508
|
-
const limit = parseInt(getFlag("--limit") ?? "200");
|
|
1509
|
-
const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
|
|
1510
|
-
console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
|
|
1511
|
-
db.close();
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
3138
|
case "remember": {
|
|
1515
|
-
const content =
|
|
3139
|
+
const content = getPositionalArgs(1).join(" ");
|
|
1516
3140
|
if (!content) {
|
|
1517
3141
|
console.error("Usage: agent-memory remember <content>");
|
|
1518
3142
|
process.exit(1);
|
|
@@ -1520,39 +3144,39 @@ async function main() {
|
|
|
1520
3144
|
const db = openDatabase({ path: getDbPath() });
|
|
1521
3145
|
const uri = getFlag("--uri");
|
|
1522
3146
|
const type = getFlag("--type") ?? "knowledge";
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
3147
|
+
const result = await rememberMemory(db, {
|
|
3148
|
+
content,
|
|
3149
|
+
type,
|
|
3150
|
+
uri,
|
|
3151
|
+
source: "manual",
|
|
3152
|
+
agent_id: getAgentId()
|
|
3153
|
+
});
|
|
1532
3154
|
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
1533
3155
|
db.close();
|
|
1534
3156
|
break;
|
|
1535
3157
|
}
|
|
1536
3158
|
case "recall": {
|
|
1537
|
-
const query =
|
|
3159
|
+
const query = getPositionalArgs(1).join(" ");
|
|
1538
3160
|
if (!query) {
|
|
1539
3161
|
console.error("Usage: agent-memory recall <query>");
|
|
1540
3162
|
process.exit(1);
|
|
1541
3163
|
}
|
|
1542
3164
|
const db = openDatabase({ path: getDbPath() });
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
const results = rerank(raw, { ...strategy, limit });
|
|
1550
|
-
console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
|
|
3165
|
+
const result = await recallMemory(db, {
|
|
3166
|
+
query,
|
|
3167
|
+
agent_id: getAgentId(),
|
|
3168
|
+
limit: Number.parseInt(getFlag("--limit") ?? "10", 10)
|
|
3169
|
+
});
|
|
3170
|
+
console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
|
|
1551
3171
|
`);
|
|
1552
|
-
for (const
|
|
1553
|
-
const
|
|
1554
|
-
const
|
|
1555
|
-
|
|
3172
|
+
for (const row of result.results) {
|
|
3173
|
+
const priorityLabel = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][row.memory.priority];
|
|
3174
|
+
const vitality = (row.memory.vitality * 100).toFixed(0);
|
|
3175
|
+
const branches = [
|
|
3176
|
+
row.bm25_rank ? `bm25#${row.bm25_rank}` : null,
|
|
3177
|
+
row.vector_rank ? `vec#${row.vector_rank}` : null
|
|
3178
|
+
].filter(Boolean).join(" + ");
|
|
3179
|
+
console.log(`${priorityLabel} P${row.memory.priority} [${vitality}%] ${row.memory.content.slice(0, 80)}${branches ? ` (${branches})` : ""}`);
|
|
1556
3180
|
}
|
|
1557
3181
|
db.close();
|
|
1558
3182
|
break;
|
|
@@ -1562,8 +3186,8 @@ async function main() {
|
|
|
1562
3186
|
const result = boot(db, { agent_id: getAgentId() });
|
|
1563
3187
|
console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
|
|
1564
3188
|
`);
|
|
1565
|
-
for (const
|
|
1566
|
-
console.log(` \u{1F534} ${
|
|
3189
|
+
for (const memory of result.identityMemories) {
|
|
3190
|
+
console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
|
|
1567
3191
|
}
|
|
1568
3192
|
if (result.bootPaths.length) {
|
|
1569
3193
|
console.log(`
|
|
@@ -1574,74 +3198,83 @@ async function main() {
|
|
|
1574
3198
|
}
|
|
1575
3199
|
case "status": {
|
|
1576
3200
|
const db = openDatabase({ path: getDbPath() });
|
|
1577
|
-
const
|
|
1578
|
-
const stats = countMemories(db, agentId);
|
|
1579
|
-
const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
|
|
1580
|
-
const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
|
|
1581
|
-
const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
|
|
1582
|
-
const snaps = db.prepare(
|
|
1583
|
-
`SELECT COUNT(*) as c FROM snapshots s
|
|
1584
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1585
|
-
WHERE m.agent_id = ?`
|
|
1586
|
-
).get(agentId).c;
|
|
3201
|
+
const status = getMemoryStatus(db, { agent_id: getAgentId() });
|
|
1587
3202
|
console.log("\u{1F9E0} AgentMemory Status\n");
|
|
1588
|
-
console.log(` Total memories: ${
|
|
1589
|
-
console.log(` By type: ${Object.entries(
|
|
1590
|
-
console.log(` By priority: ${Object.entries(
|
|
1591
|
-
console.log(` Paths: ${paths}
|
|
1592
|
-
console.log(` Low vitality (<10%): ${
|
|
3203
|
+
console.log(` Total memories: ${status.total}`);
|
|
3204
|
+
console.log(` By type: ${Object.entries(status.by_type).map(([key, value]) => `${key}=${value}`).join(", ")}`);
|
|
3205
|
+
console.log(` By priority: ${Object.entries(status.by_priority).map(([key, value]) => `${key}=${value}`).join(", ")}`);
|
|
3206
|
+
console.log(` Paths: ${status.paths}`);
|
|
3207
|
+
console.log(` Low vitality (<10%): ${status.low_vitality}`);
|
|
3208
|
+
console.log(` Feedback events: ${status.feedback_events}`);
|
|
1593
3209
|
db.close();
|
|
1594
3210
|
break;
|
|
1595
3211
|
}
|
|
1596
3212
|
case "reflect": {
|
|
1597
3213
|
const phase = args[1] ?? "all";
|
|
1598
3214
|
const db = openDatabase({ path: getDbPath() });
|
|
1599
|
-
const
|
|
1600
|
-
console.log(`\u{1F319}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
const r = runDecay(db, { agent_id: agentId });
|
|
1604
|
-
console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
|
|
1605
|
-
}
|
|
1606
|
-
if (phase === "tidy" || phase === "all") {
|
|
1607
|
-
const r = runTidy(db, { agent_id: agentId });
|
|
1608
|
-
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
|
|
1609
|
-
}
|
|
1610
|
-
if (phase === "govern" || phase === "all") {
|
|
1611
|
-
const r = runGovern(db, { agent_id: agentId });
|
|
1612
|
-
console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
|
|
3215
|
+
const result = await reflectMemories(db, { phase, agent_id: getAgentId() });
|
|
3216
|
+
console.log(`\u{1F319} Reflect job ${result.jobId}${result.resumed ? " (resume)" : ""}`);
|
|
3217
|
+
for (const [name, summary] of Object.entries(result.results)) {
|
|
3218
|
+
console.log(` ${name}: ${JSON.stringify(summary)}`);
|
|
1613
3219
|
}
|
|
1614
3220
|
db.close();
|
|
1615
3221
|
break;
|
|
1616
3222
|
}
|
|
1617
3223
|
case "reindex": {
|
|
1618
3224
|
const db = openDatabase({ path: getDbPath() });
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const txn = db.transaction(() => {
|
|
1624
|
-
for (const mem of memories) {
|
|
1625
|
-
insert.run(mem.id, tokenizeForIndex(mem.content));
|
|
1626
|
-
count++;
|
|
1627
|
-
}
|
|
3225
|
+
const result = await reindexMemories(db, {
|
|
3226
|
+
agent_id: getAgentId(),
|
|
3227
|
+
force: hasFlag("--full"),
|
|
3228
|
+
batchSize: Number.parseInt(getFlag("--batch-size") ?? "16", 10)
|
|
1628
3229
|
});
|
|
1629
|
-
|
|
1630
|
-
|
|
3230
|
+
console.log(`\u{1F504} Reindexed ${result.fts.reindexed} memories in BM25 index`);
|
|
3231
|
+
if (result.embeddings.enabled) {
|
|
3232
|
+
console.log(`\u{1F9EC} Embeddings: provider=${result.embeddings.providerId} scanned=${result.embeddings.scanned} embedded=${result.embeddings.embedded} failed=${result.embeddings.failed}`);
|
|
3233
|
+
} else {
|
|
3234
|
+
console.log("\u{1F9EC} Embeddings: disabled (no provider configured)");
|
|
3235
|
+
}
|
|
1631
3236
|
db.close();
|
|
1632
3237
|
break;
|
|
1633
3238
|
}
|
|
3239
|
+
case "serve": {
|
|
3240
|
+
const port = Number.parseInt(getFlag("--port") ?? process.env.AGENT_MEMORY_HTTP_PORT ?? "3000", 10);
|
|
3241
|
+
const host = getFlag("--host") ?? process.env.AGENT_MEMORY_HTTP_HOST ?? "127.0.0.1";
|
|
3242
|
+
const service = await startHttpServer({
|
|
3243
|
+
dbPath: getDbPath(),
|
|
3244
|
+
agentId: getAgentId(),
|
|
3245
|
+
port,
|
|
3246
|
+
host
|
|
3247
|
+
});
|
|
3248
|
+
const address = service.server.address();
|
|
3249
|
+
if (address && typeof address !== "string") {
|
|
3250
|
+
console.log(`\u{1F310} AgentMemory HTTP server listening on http://${address.address}:${address.port}`);
|
|
3251
|
+
} else {
|
|
3252
|
+
console.log(`\u{1F310} AgentMemory HTTP server listening on http://${host}:${port}`);
|
|
3253
|
+
}
|
|
3254
|
+
const shutdown = async () => {
|
|
3255
|
+
try {
|
|
3256
|
+
await service.close();
|
|
3257
|
+
} finally {
|
|
3258
|
+
process.exit(0);
|
|
3259
|
+
}
|
|
3260
|
+
};
|
|
3261
|
+
process.once("SIGINT", () => {
|
|
3262
|
+
void shutdown();
|
|
3263
|
+
});
|
|
3264
|
+
process.once("SIGTERM", () => {
|
|
3265
|
+
void shutdown();
|
|
3266
|
+
});
|
|
3267
|
+
break;
|
|
3268
|
+
}
|
|
1634
3269
|
case "export": {
|
|
1635
3270
|
const dir = args[1];
|
|
1636
3271
|
if (!dir) {
|
|
1637
3272
|
console.error("Usage: agent-memory export <directory>");
|
|
1638
3273
|
process.exit(1);
|
|
1639
3274
|
}
|
|
1640
|
-
const dirPath = resolve(dir);
|
|
1641
3275
|
const db = openDatabase({ path: getDbPath() });
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
|
|
3276
|
+
const result = exportMemories(db, resolve(dir), { agent_id: getAgentId() });
|
|
3277
|
+
console.log(`\u2705 Export complete: ${result.exported} items to ${resolve(dir)} (${result.files.length} files)`);
|
|
1645
3278
|
db.close();
|
|
1646
3279
|
break;
|
|
1647
3280
|
}
|
|
@@ -1659,10 +3292,10 @@ async function main() {
|
|
|
1659
3292
|
const db = openDatabase({ path: getDbPath() });
|
|
1660
3293
|
const agentId = getAgentId();
|
|
1661
3294
|
let imported = 0;
|
|
1662
|
-
const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((
|
|
3295
|
+
const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((file) => resolve(dirPath, file)).find((file) => existsSync2(file));
|
|
1663
3296
|
if (memoryFile) {
|
|
1664
3297
|
const content = readFileSync2(memoryFile, "utf-8");
|
|
1665
|
-
const sections = content.split(/^## /m).filter((
|
|
3298
|
+
const sections = content.split(/^## /m).filter((section) => section.trim());
|
|
1666
3299
|
for (const section of sections) {
|
|
1667
3300
|
const lines = section.split("\n");
|
|
1668
3301
|
const title = lines[0]?.trim();
|
|
@@ -1670,40 +3303,46 @@ async function main() {
|
|
|
1670
3303
|
if (!body) continue;
|
|
1671
3304
|
const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
|
|
1672
3305
|
const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
3306
|
+
await rememberMemory(db, {
|
|
3307
|
+
content: `## ${title}
|
|
3308
|
+
${body}`,
|
|
3309
|
+
type,
|
|
3310
|
+
uri,
|
|
3311
|
+
source: `migrate:${basename(memoryFile)}`,
|
|
3312
|
+
agent_id: agentId
|
|
3313
|
+
});
|
|
3314
|
+
imported += 1;
|
|
1676
3315
|
}
|
|
1677
3316
|
console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
|
|
1678
3317
|
}
|
|
1679
|
-
const mdFiles = readdirSync(dirPath).filter((
|
|
3318
|
+
const mdFiles = readdirSync(dirPath).filter((file) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(file)).sort();
|
|
1680
3319
|
for (const file of mdFiles) {
|
|
1681
3320
|
const content = readFileSync2(resolve(dirPath, file), "utf-8");
|
|
1682
3321
|
const date = file.replace(/\.(md|qmd)$/i, "");
|
|
1683
|
-
|
|
3322
|
+
await rememberMemory(db, {
|
|
1684
3323
|
content,
|
|
1685
3324
|
type: "event",
|
|
1686
3325
|
uri: `event://journal/${date}`,
|
|
1687
3326
|
source: `migrate:${file}`,
|
|
1688
3327
|
agent_id: agentId
|
|
1689
3328
|
});
|
|
1690
|
-
imported
|
|
3329
|
+
imported += 1;
|
|
1691
3330
|
}
|
|
1692
3331
|
if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
|
|
1693
3332
|
const weeklyDir = resolve(dirPath, "weekly");
|
|
1694
3333
|
if (existsSync2(weeklyDir)) {
|
|
1695
|
-
const weeklyFiles = readdirSync(weeklyDir).filter((
|
|
3334
|
+
const weeklyFiles = readdirSync(weeklyDir).filter((file) => file.endsWith(".md") || file.endsWith(".qmd"));
|
|
1696
3335
|
for (const file of weeklyFiles) {
|
|
1697
3336
|
const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
|
|
1698
3337
|
const week = file.replace(/\.(md|qmd)$/i, "");
|
|
1699
|
-
|
|
3338
|
+
await rememberMemory(db, {
|
|
1700
3339
|
content,
|
|
1701
3340
|
type: "knowledge",
|
|
1702
3341
|
uri: `knowledge://weekly/${week}`,
|
|
1703
3342
|
source: `migrate:weekly/${file}`,
|
|
1704
3343
|
agent_id: agentId
|
|
1705
3344
|
});
|
|
1706
|
-
imported
|
|
3345
|
+
imported += 1;
|
|
1707
3346
|
}
|
|
1708
3347
|
if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
|
|
1709
3348
|
}
|
|
@@ -1723,8 +3362,8 @@ ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentI
|
|
|
1723
3362
|
printHelp();
|
|
1724
3363
|
process.exit(1);
|
|
1725
3364
|
}
|
|
1726
|
-
} catch (
|
|
1727
|
-
const message =
|
|
3365
|
+
} catch (error) {
|
|
3366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1728
3367
|
console.error("Error:", message);
|
|
1729
3368
|
process.exit(1);
|
|
1730
3369
|
}
|