@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/mcp/server.js
CHANGED
|
@@ -31,6 +31,7 @@ function openDatabase(opts) {
|
|
|
31
31
|
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
32
32
|
}
|
|
33
33
|
ensureIndexes(db);
|
|
34
|
+
ensureFeedbackEventSchema(db);
|
|
34
35
|
return db;
|
|
35
36
|
}
|
|
36
37
|
function now() {
|
|
@@ -49,6 +50,14 @@ function getSchemaVersion(db) {
|
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
function tableExists(db, table) {
|
|
54
|
+
try {
|
|
55
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table);
|
|
56
|
+
return Boolean(row?.name);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
function tableHasColumn(db, table, column) {
|
|
53
62
|
try {
|
|
54
63
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -70,6 +79,16 @@ function migrateDatabase(db, from, to) {
|
|
|
70
79
|
v = 3;
|
|
71
80
|
continue;
|
|
72
81
|
}
|
|
82
|
+
if (v === 3) {
|
|
83
|
+
migrateV3ToV4(db);
|
|
84
|
+
v = 4;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (v === 4) {
|
|
88
|
+
migrateV4ToV5(db);
|
|
89
|
+
v = 5;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
73
92
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
74
93
|
}
|
|
75
94
|
}
|
|
@@ -151,14 +170,12 @@ function migrateV1ToV2(db) {
|
|
|
151
170
|
function inferSchemaVersion(db) {
|
|
152
171
|
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
153
172
|
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
154
|
-
const hasEmbeddings = (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
})();
|
|
173
|
+
const hasEmbeddings = tableExists(db, "embeddings");
|
|
174
|
+
const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
175
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
176
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
177
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
178
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
162
179
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
163
180
|
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
164
181
|
return 1;
|
|
@@ -171,14 +188,37 @@ function ensureIndexes(db) {
|
|
|
171
188
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
172
189
|
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
173
190
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
if (tableExists(db, "embeddings")) {
|
|
192
|
+
if (tableHasColumn(db, "embeddings", "provider_id")) {
|
|
193
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_provider_status ON embeddings(provider_id, status);");
|
|
194
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory_provider ON embeddings(memory_id, provider_id);");
|
|
195
|
+
} else {
|
|
177
196
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
178
197
|
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
179
198
|
}
|
|
180
|
-
} catch {
|
|
181
199
|
}
|
|
200
|
+
if (tableExists(db, "maintenance_jobs")) {
|
|
201
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
|
|
202
|
+
}
|
|
203
|
+
if (tableExists(db, "feedback_events")) {
|
|
204
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
205
|
+
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
206
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function ensureFeedbackEventSchema(db) {
|
|
211
|
+
if (!tableExists(db, "feedback_events")) return;
|
|
212
|
+
if (!tableHasColumn(db, "feedback_events", "source")) {
|
|
213
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN source TEXT NOT NULL DEFAULT 'surface';");
|
|
214
|
+
}
|
|
215
|
+
if (!tableHasColumn(db, "feedback_events", "useful")) {
|
|
216
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN useful INTEGER NOT NULL DEFAULT 1;");
|
|
217
|
+
}
|
|
218
|
+
if (!tableHasColumn(db, "feedback_events", "agent_id")) {
|
|
219
|
+
db.exec("ALTER TABLE feedback_events ADD COLUMN agent_id TEXT NOT NULL DEFAULT 'default';");
|
|
220
|
+
}
|
|
221
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
|
|
182
222
|
}
|
|
183
223
|
function migrateV2ToV3(db) {
|
|
184
224
|
try {
|
|
@@ -205,11 +245,99 @@ function migrateV2ToV3(db) {
|
|
|
205
245
|
throw e;
|
|
206
246
|
}
|
|
207
247
|
}
|
|
248
|
+
function migrateV3ToV4(db) {
|
|
249
|
+
const alreadyMigrated = tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
250
|
+
if (alreadyMigrated) {
|
|
251
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
db.exec("BEGIN");
|
|
256
|
+
const legacyRows = tableExists(db, "embeddings") ? db.prepare(
|
|
257
|
+
`SELECT e.agent_id, e.memory_id, e.model, e.vector, e.created_at, m.hash
|
|
258
|
+
FROM embeddings e
|
|
259
|
+
LEFT JOIN memories m ON m.id = e.memory_id`
|
|
260
|
+
).all() : [];
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS embeddings_v4 (
|
|
263
|
+
id TEXT PRIMARY KEY,
|
|
264
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
265
|
+
provider_id TEXT NOT NULL,
|
|
266
|
+
vector BLOB,
|
|
267
|
+
content_hash TEXT NOT NULL,
|
|
268
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
269
|
+
created_at TEXT NOT NULL,
|
|
270
|
+
UNIQUE(memory_id, provider_id)
|
|
271
|
+
);
|
|
272
|
+
`);
|
|
273
|
+
const insert = db.prepare(
|
|
274
|
+
`INSERT INTO embeddings_v4 (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)`
|
|
276
|
+
);
|
|
277
|
+
for (const row of legacyRows) {
|
|
278
|
+
insert.run(newId(), row.memory_id, `legacy:${row.agent_id}:${row.model}`, row.vector, row.hash ?? "", row.created_at);
|
|
279
|
+
}
|
|
280
|
+
if (tableExists(db, "embeddings")) {
|
|
281
|
+
db.exec("DROP TABLE embeddings;");
|
|
282
|
+
}
|
|
283
|
+
db.exec("ALTER TABLE embeddings_v4 RENAME TO embeddings;");
|
|
284
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
|
|
285
|
+
db.exec("COMMIT");
|
|
286
|
+
} catch (e) {
|
|
287
|
+
try {
|
|
288
|
+
db.exec("ROLLBACK");
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function migrateV4ToV5(db) {
|
|
295
|
+
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
296
|
+
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
297
|
+
if (hasMaintenanceJobs && hasFeedbackEvents) {
|
|
298
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
db.exec("BEGIN");
|
|
303
|
+
db.exec(`
|
|
304
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
305
|
+
job_id TEXT PRIMARY KEY,
|
|
306
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
307
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
308
|
+
checkpoint TEXT,
|
|
309
|
+
error TEXT,
|
|
310
|
+
started_at TEXT NOT NULL,
|
|
311
|
+
finished_at TEXT
|
|
312
|
+
);
|
|
313
|
+
`);
|
|
314
|
+
db.exec(`
|
|
315
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
316
|
+
id TEXT PRIMARY KEY,
|
|
317
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
318
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
319
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
320
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
321
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
322
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
323
|
+
created_at TEXT NOT NULL
|
|
324
|
+
);
|
|
325
|
+
`);
|
|
326
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
|
|
327
|
+
db.exec("COMMIT");
|
|
328
|
+
} catch (e) {
|
|
329
|
+
try {
|
|
330
|
+
db.exec("ROLLBACK");
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
throw e;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
208
336
|
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
209
337
|
var init_db = __esm({
|
|
210
338
|
"src/core/db.ts"() {
|
|
211
339
|
"use strict";
|
|
212
|
-
SCHEMA_VERSION =
|
|
340
|
+
SCHEMA_VERSION = 5;
|
|
213
341
|
SCHEMA_SQL = `
|
|
214
342
|
-- Memory entries
|
|
215
343
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -272,14 +400,37 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
272
400
|
|
|
273
401
|
-- Embeddings (optional semantic layer)
|
|
274
402
|
CREATE TABLE IF NOT EXISTS embeddings (
|
|
275
|
-
|
|
276
|
-
memory_id
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
403
|
+
id TEXT PRIMARY KEY,
|
|
404
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
405
|
+
provider_id TEXT NOT NULL,
|
|
406
|
+
vector BLOB,
|
|
407
|
+
content_hash TEXT NOT NULL,
|
|
408
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
409
|
+
created_at TEXT NOT NULL,
|
|
410
|
+
UNIQUE(memory_id, provider_id)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
-- Maintenance jobs (reflect / reindex checkpoints)
|
|
414
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
415
|
+
job_id TEXT PRIMARY KEY,
|
|
416
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
417
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
418
|
+
checkpoint TEXT,
|
|
419
|
+
error TEXT,
|
|
420
|
+
started_at TEXT NOT NULL,
|
|
421
|
+
finished_at TEXT
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
-- Feedback signals (recall/surface usefulness + governance priors)
|
|
425
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
426
|
+
id TEXT PRIMARY KEY,
|
|
427
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
428
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
429
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
430
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
431
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
432
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
433
|
+
created_at TEXT NOT NULL
|
|
283
434
|
);
|
|
284
435
|
|
|
285
436
|
-- Schema version tracking
|
|
@@ -296,6 +447,8 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
|
296
447
|
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
297
448
|
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
298
449
|
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
450
|
+
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
|
|
451
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
|
|
299
452
|
`;
|
|
300
453
|
}
|
|
301
454
|
});
|
|
@@ -384,6 +537,361 @@ var init_tokenizer = __esm({
|
|
|
384
537
|
}
|
|
385
538
|
});
|
|
386
539
|
|
|
540
|
+
// src/search/embedding.ts
|
|
541
|
+
import { createHash } from "crypto";
|
|
542
|
+
function trimTrailingSlashes(value) {
|
|
543
|
+
return value.replace(/\/+$/, "");
|
|
544
|
+
}
|
|
545
|
+
function resolveEndpoint(baseUrl, endpoint = "/embeddings") {
|
|
546
|
+
const trimmed = trimTrailingSlashes(baseUrl);
|
|
547
|
+
if (trimmed.endsWith("/embeddings")) {
|
|
548
|
+
return trimmed;
|
|
549
|
+
}
|
|
550
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
551
|
+
return `${trimmed}${normalizedEndpoint}`;
|
|
552
|
+
}
|
|
553
|
+
function stableProviderId(prefix, input) {
|
|
554
|
+
const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
555
|
+
return `${prefix}:${digest}`;
|
|
556
|
+
}
|
|
557
|
+
function getFetch(fetchImpl) {
|
|
558
|
+
const candidate = fetchImpl ?? globalThis.fetch;
|
|
559
|
+
if (!candidate) {
|
|
560
|
+
throw new Error("Global fetch is not available in this runtime");
|
|
561
|
+
}
|
|
562
|
+
return candidate;
|
|
563
|
+
}
|
|
564
|
+
function assertEmbeddingVector(vector, dimension, context) {
|
|
565
|
+
if (!Array.isArray(vector) || !vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
566
|
+
throw new Error(`${context} returned an invalid embedding vector`);
|
|
567
|
+
}
|
|
568
|
+
if (vector.length !== dimension) {
|
|
569
|
+
throw new Error(`${context} returned dimension ${vector.length}, expected ${dimension}`);
|
|
570
|
+
}
|
|
571
|
+
return vector;
|
|
572
|
+
}
|
|
573
|
+
function parseOpenAIResponse(json2, dimension, context) {
|
|
574
|
+
const rows = json2?.data;
|
|
575
|
+
if (!Array.isArray(rows)) {
|
|
576
|
+
throw new Error(`${context} returned an invalid embeddings payload`);
|
|
577
|
+
}
|
|
578
|
+
return rows.map((row, index) => assertEmbeddingVector(row?.embedding, dimension, `${context} item ${index}`));
|
|
579
|
+
}
|
|
580
|
+
function parseLocalHttpResponse(json2, dimension, context) {
|
|
581
|
+
if (Array.isArray(json2.embeddings)) {
|
|
582
|
+
const embeddings = json2.embeddings;
|
|
583
|
+
return embeddings.map((row, index) => assertEmbeddingVector(row, dimension, `${context} item ${index}`));
|
|
584
|
+
}
|
|
585
|
+
return parseOpenAIResponse(json2, dimension, context);
|
|
586
|
+
}
|
|
587
|
+
async function runEmbeddingRequest(input) {
|
|
588
|
+
const fetchFn = getFetch(input.fetchImpl);
|
|
589
|
+
const response = await fetchFn(input.url, {
|
|
590
|
+
method: "POST",
|
|
591
|
+
headers: {
|
|
592
|
+
"content-type": "application/json",
|
|
593
|
+
...input.headers
|
|
594
|
+
},
|
|
595
|
+
body: JSON.stringify(input.body)
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
const detail = await response.text().catch(() => "");
|
|
599
|
+
throw new Error(`${input.context} request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
600
|
+
}
|
|
601
|
+
const json2 = await response.json();
|
|
602
|
+
return input.parser(json2, input.dimension, input.context);
|
|
603
|
+
}
|
|
604
|
+
function createOpenAICompatibleEmbeddingProvider(opts) {
|
|
605
|
+
const url2 = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
606
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
607
|
+
const id = stableProviderId(`openai-compatible:${opts.model}`, providerDescriptor);
|
|
608
|
+
return {
|
|
609
|
+
id,
|
|
610
|
+
model: opts.model,
|
|
611
|
+
dimension: opts.dimension,
|
|
612
|
+
async embed(texts) {
|
|
613
|
+
if (texts.length === 0) return [];
|
|
614
|
+
return runEmbeddingRequest({
|
|
615
|
+
context: "openai-compatible embedding provider",
|
|
616
|
+
url: url2,
|
|
617
|
+
dimension: opts.dimension,
|
|
618
|
+
fetchImpl: opts.fetchImpl,
|
|
619
|
+
headers: {
|
|
620
|
+
...opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
|
|
621
|
+
...opts.headers
|
|
622
|
+
},
|
|
623
|
+
body: {
|
|
624
|
+
model: opts.model,
|
|
625
|
+
input: texts
|
|
626
|
+
},
|
|
627
|
+
parser: parseOpenAIResponse
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
async healthcheck() {
|
|
631
|
+
await this.embed(["healthcheck"]);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function createLocalHttpEmbeddingProvider(opts) {
|
|
636
|
+
const url2 = resolveEndpoint(opts.baseUrl, opts.endpoint);
|
|
637
|
+
const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
|
|
638
|
+
const id = stableProviderId(`local-http:${opts.model}`, providerDescriptor);
|
|
639
|
+
return {
|
|
640
|
+
id,
|
|
641
|
+
model: opts.model,
|
|
642
|
+
dimension: opts.dimension,
|
|
643
|
+
async embed(texts) {
|
|
644
|
+
if (texts.length === 0) return [];
|
|
645
|
+
return runEmbeddingRequest({
|
|
646
|
+
context: "local-http embedding provider",
|
|
647
|
+
url: url2,
|
|
648
|
+
dimension: opts.dimension,
|
|
649
|
+
fetchImpl: opts.fetchImpl,
|
|
650
|
+
headers: opts.headers,
|
|
651
|
+
body: {
|
|
652
|
+
model: opts.model,
|
|
653
|
+
input: texts
|
|
654
|
+
},
|
|
655
|
+
parser: parseLocalHttpResponse
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
async healthcheck() {
|
|
659
|
+
await this.embed(["healthcheck"]);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
664
|
+
return trimTrailingSlashes(baseUrl);
|
|
665
|
+
}
|
|
666
|
+
var init_embedding = __esm({
|
|
667
|
+
"src/search/embedding.ts"() {
|
|
668
|
+
"use strict";
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// src/search/providers.ts
|
|
673
|
+
function parseDimension(raw) {
|
|
674
|
+
if (!raw) return void 0;
|
|
675
|
+
const value = Number.parseInt(raw, 10);
|
|
676
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
677
|
+
}
|
|
678
|
+
function parseProvider(raw) {
|
|
679
|
+
if (!raw) return null;
|
|
680
|
+
if (raw === "openai-compatible" || raw === "local-http") {
|
|
681
|
+
return raw;
|
|
682
|
+
}
|
|
683
|
+
throw new Error(`Unsupported embedding provider: ${raw}`);
|
|
684
|
+
}
|
|
685
|
+
function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
686
|
+
const provider = parseProvider(env.AGENT_MEMORY_EMBEDDING_PROVIDER);
|
|
687
|
+
if (!provider) return null;
|
|
688
|
+
const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
|
|
689
|
+
const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
|
|
690
|
+
const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
|
|
691
|
+
if (!baseUrl) {
|
|
692
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
|
|
693
|
+
}
|
|
694
|
+
if (!model) {
|
|
695
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
|
|
696
|
+
}
|
|
697
|
+
if (!dimension) {
|
|
698
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_DIMENSION is required when embeddings are enabled");
|
|
699
|
+
}
|
|
700
|
+
if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
|
|
701
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
provider,
|
|
705
|
+
baseUrl,
|
|
706
|
+
model,
|
|
707
|
+
dimension,
|
|
708
|
+
apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function createEmbeddingProvider(input, opts) {
|
|
712
|
+
const normalized = {
|
|
713
|
+
...input,
|
|
714
|
+
baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
|
|
715
|
+
};
|
|
716
|
+
if (normalized.provider === "openai-compatible") {
|
|
717
|
+
return createOpenAICompatibleEmbeddingProvider({
|
|
718
|
+
baseUrl: normalized.baseUrl,
|
|
719
|
+
model: normalized.model,
|
|
720
|
+
dimension: normalized.dimension,
|
|
721
|
+
apiKey: normalized.apiKey,
|
|
722
|
+
fetchImpl: opts?.fetchImpl
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return createLocalHttpEmbeddingProvider({
|
|
726
|
+
baseUrl: normalized.baseUrl,
|
|
727
|
+
model: normalized.model,
|
|
728
|
+
dimension: normalized.dimension,
|
|
729
|
+
fetchImpl: opts?.fetchImpl
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function resolveEmbeddingProviderConfig(opts) {
|
|
733
|
+
const envConfig = getEmbeddingProviderConfigFromEnv(opts?.env);
|
|
734
|
+
if (!envConfig && !opts?.config?.provider) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
const provider = opts?.config?.provider ?? envConfig?.provider;
|
|
738
|
+
const baseUrl = opts?.config?.baseUrl ?? envConfig?.baseUrl;
|
|
739
|
+
const model = opts?.config?.model ?? envConfig?.model;
|
|
740
|
+
const dimension = opts?.config?.dimension ?? envConfig?.dimension;
|
|
741
|
+
const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
|
|
742
|
+
if (!provider || !baseUrl || !model || !dimension) {
|
|
743
|
+
throw new Error("Incomplete embedding provider configuration");
|
|
744
|
+
}
|
|
745
|
+
if (provider === "openai-compatible" && !apiKey) {
|
|
746
|
+
throw new Error("OpenAI-compatible embedding providers require an API key");
|
|
747
|
+
}
|
|
748
|
+
return { provider, baseUrl, model, dimension, apiKey };
|
|
749
|
+
}
|
|
750
|
+
function getEmbeddingProvider(opts) {
|
|
751
|
+
const config2 = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
|
|
752
|
+
if (!config2) return null;
|
|
753
|
+
return createEmbeddingProvider(config2, { fetchImpl: opts?.fetchImpl });
|
|
754
|
+
}
|
|
755
|
+
function getEmbeddingProviderFromEnv(env = process.env) {
|
|
756
|
+
try {
|
|
757
|
+
return getEmbeddingProvider({ env });
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function getConfiguredEmbeddingProviderId(opts) {
|
|
763
|
+
try {
|
|
764
|
+
const provider = getEmbeddingProvider({ config: opts?.config, env: opts?.env });
|
|
765
|
+
return provider?.id ?? null;
|
|
766
|
+
} catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
var init_providers = __esm({
|
|
771
|
+
"src/search/providers.ts"() {
|
|
772
|
+
"use strict";
|
|
773
|
+
init_embedding();
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// src/search/vector.ts
|
|
778
|
+
function encodeVector(vector) {
|
|
779
|
+
const float322 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
780
|
+
return Buffer.from(float322.buffer.slice(float322.byteOffset, float322.byteOffset + float322.byteLength));
|
|
781
|
+
}
|
|
782
|
+
function decodeVector(blob) {
|
|
783
|
+
const buffer = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
784
|
+
const aligned = buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength ? buffer.buffer : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
785
|
+
return Array.from(new Float32Array(aligned));
|
|
786
|
+
}
|
|
787
|
+
function cosineSimilarity(a, b) {
|
|
788
|
+
const length = Math.min(a.length, b.length);
|
|
789
|
+
if (length === 0 || a.length !== b.length) return 0;
|
|
790
|
+
let dot = 0;
|
|
791
|
+
let normA = 0;
|
|
792
|
+
let normB = 0;
|
|
793
|
+
for (let index = 0; index < length; index++) {
|
|
794
|
+
const left = Number(a[index] ?? 0);
|
|
795
|
+
const right = Number(b[index] ?? 0);
|
|
796
|
+
dot += left * right;
|
|
797
|
+
normA += left * left;
|
|
798
|
+
normB += right * right;
|
|
799
|
+
}
|
|
800
|
+
if (normA === 0 || normB === 0) return 0;
|
|
801
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
802
|
+
}
|
|
803
|
+
function markMemoryEmbeddingPending(db, memoryId, providerId, contentHash2) {
|
|
804
|
+
db.prepare(
|
|
805
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
806
|
+
VALUES (?, ?, ?, NULL, ?, 'pending', ?)
|
|
807
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
808
|
+
vector = NULL,
|
|
809
|
+
content_hash = excluded.content_hash,
|
|
810
|
+
status = 'pending'`
|
|
811
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
812
|
+
}
|
|
813
|
+
function markAllEmbeddingsPending(db, memoryId, contentHash2) {
|
|
814
|
+
const result = db.prepare(
|
|
815
|
+
`UPDATE embeddings
|
|
816
|
+
SET vector = NULL,
|
|
817
|
+
content_hash = ?,
|
|
818
|
+
status = 'pending'
|
|
819
|
+
WHERE memory_id = ?`
|
|
820
|
+
).run(contentHash2, memoryId);
|
|
821
|
+
return result.changes;
|
|
822
|
+
}
|
|
823
|
+
function upsertReadyEmbedding(input) {
|
|
824
|
+
input.db.prepare(
|
|
825
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
826
|
+
VALUES (?, ?, ?, ?, ?, 'ready', ?)
|
|
827
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
828
|
+
vector = excluded.vector,
|
|
829
|
+
content_hash = excluded.content_hash,
|
|
830
|
+
status = 'ready'`
|
|
831
|
+
).run(newId(), input.memoryId, input.providerId, encodeVector(input.vector), input.contentHash, now());
|
|
832
|
+
}
|
|
833
|
+
function markEmbeddingFailed(db, memoryId, providerId, contentHash2) {
|
|
834
|
+
db.prepare(
|
|
835
|
+
`INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
|
|
836
|
+
VALUES (?, ?, ?, NULL, ?, 'failed', ?)
|
|
837
|
+
ON CONFLICT(memory_id, provider_id) DO UPDATE SET
|
|
838
|
+
vector = NULL,
|
|
839
|
+
content_hash = excluded.content_hash,
|
|
840
|
+
status = 'failed'`
|
|
841
|
+
).run(newId(), memoryId, providerId, contentHash2, now());
|
|
842
|
+
}
|
|
843
|
+
function searchByVector(db, queryVector, opts) {
|
|
844
|
+
const limit = opts.limit ?? 20;
|
|
845
|
+
const agentId = opts.agent_id ?? "default";
|
|
846
|
+
const minVitality = opts.min_vitality ?? 0;
|
|
847
|
+
const rows = db.prepare(
|
|
848
|
+
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
849
|
+
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
850
|
+
m.stability, m.access_count, m.last_accessed, m.created_at,
|
|
851
|
+
m.updated_at, m.source, m.agent_id, m.hash
|
|
852
|
+
FROM embeddings e
|
|
853
|
+
JOIN memories m ON m.id = e.memory_id
|
|
854
|
+
WHERE e.provider_id = ?
|
|
855
|
+
AND e.status = 'ready'
|
|
856
|
+
AND e.vector IS NOT NULL
|
|
857
|
+
AND e.content_hash = m.hash
|
|
858
|
+
AND m.agent_id = ?
|
|
859
|
+
AND m.vitality >= ?`
|
|
860
|
+
).all(opts.providerId, agentId, minVitality);
|
|
861
|
+
const scored = rows.map((row) => ({
|
|
862
|
+
provider_id: row.provider_id,
|
|
863
|
+
memory: {
|
|
864
|
+
id: row.id,
|
|
865
|
+
content: row.content,
|
|
866
|
+
type: row.type,
|
|
867
|
+
priority: row.priority,
|
|
868
|
+
emotion_val: row.emotion_val,
|
|
869
|
+
vitality: row.vitality,
|
|
870
|
+
stability: row.stability,
|
|
871
|
+
access_count: row.access_count,
|
|
872
|
+
last_accessed: row.last_accessed,
|
|
873
|
+
created_at: row.created_at,
|
|
874
|
+
updated_at: row.updated_at,
|
|
875
|
+
source: row.source,
|
|
876
|
+
agent_id: row.agent_id,
|
|
877
|
+
hash: row.hash
|
|
878
|
+
},
|
|
879
|
+
similarity: cosineSimilarity(queryVector, decodeVector(row.vector))
|
|
880
|
+
})).filter((row) => Number.isFinite(row.similarity) && row.similarity > 0).sort((left, right) => right.similarity - left.similarity).slice(0, limit);
|
|
881
|
+
return scored.map((row, index) => ({
|
|
882
|
+
memory: row.memory,
|
|
883
|
+
similarity: row.similarity,
|
|
884
|
+
rank: index + 1,
|
|
885
|
+
provider_id: row.provider_id
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
var init_vector = __esm({
|
|
889
|
+
"src/search/vector.ts"() {
|
|
890
|
+
"use strict";
|
|
891
|
+
init_db();
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
387
895
|
// src/core/memory.ts
|
|
388
896
|
var memory_exports = {};
|
|
389
897
|
__export(memory_exports, {
|
|
@@ -396,9 +904,22 @@ __export(memory_exports, {
|
|
|
396
904
|
recordAccess: () => recordAccess,
|
|
397
905
|
updateMemory: () => updateMemory
|
|
398
906
|
});
|
|
399
|
-
import { createHash } from "crypto";
|
|
907
|
+
import { createHash as createHash2 } from "crypto";
|
|
400
908
|
function contentHash(content) {
|
|
401
|
-
return
|
|
909
|
+
return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
910
|
+
}
|
|
911
|
+
function resolveEmbeddingProviderId(explicitProviderId) {
|
|
912
|
+
if (explicitProviderId !== void 0) {
|
|
913
|
+
return explicitProviderId;
|
|
914
|
+
}
|
|
915
|
+
return getConfiguredEmbeddingProviderId();
|
|
916
|
+
}
|
|
917
|
+
function markEmbeddingDirtyIfNeeded(db, memoryId, hash2, providerId) {
|
|
918
|
+
if (!providerId) return;
|
|
919
|
+
try {
|
|
920
|
+
markMemoryEmbeddingPending(db, memoryId, providerId, hash2);
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
402
923
|
}
|
|
403
924
|
function createMemory(db, input) {
|
|
404
925
|
const hash2 = contentHash(input.content);
|
|
@@ -429,6 +950,7 @@ function createMemory(db, input) {
|
|
|
429
950
|
hash2
|
|
430
951
|
);
|
|
431
952
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
953
|
+
markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
432
954
|
return getMemory(db, id);
|
|
433
955
|
}
|
|
434
956
|
function getMemory(db, id) {
|
|
@@ -439,9 +961,11 @@ function updateMemory(db, id, input) {
|
|
|
439
961
|
if (!existing) return null;
|
|
440
962
|
const fields = [];
|
|
441
963
|
const values = [];
|
|
964
|
+
let nextHash = null;
|
|
442
965
|
if (input.content !== void 0) {
|
|
966
|
+
nextHash = contentHash(input.content);
|
|
443
967
|
fields.push("content = ?", "hash = ?");
|
|
444
|
-
values.push(input.content,
|
|
968
|
+
values.push(input.content, nextHash);
|
|
445
969
|
}
|
|
446
970
|
if (input.type !== void 0) {
|
|
447
971
|
fields.push("type = ?");
|
|
@@ -474,6 +998,13 @@ function updateMemory(db, id, input) {
|
|
|
474
998
|
if (input.content !== void 0) {
|
|
475
999
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
476
1000
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1001
|
+
if (nextHash) {
|
|
1002
|
+
try {
|
|
1003
|
+
markAllEmbeddingsPending(db, id, nextHash);
|
|
1004
|
+
} catch {
|
|
1005
|
+
}
|
|
1006
|
+
markEmbeddingDirtyIfNeeded(db, id, nextHash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
1007
|
+
}
|
|
477
1008
|
}
|
|
478
1009
|
return getMemory(db, id);
|
|
479
1010
|
}
|
|
@@ -531,6 +1062,8 @@ var init_memory = __esm({
|
|
|
531
1062
|
"use strict";
|
|
532
1063
|
init_db();
|
|
533
1064
|
init_tokenizer();
|
|
1065
|
+
init_providers();
|
|
1066
|
+
init_vector();
|
|
534
1067
|
TYPE_PRIORITY = {
|
|
535
1068
|
identity: 0,
|
|
536
1069
|
emotion: 1,
|
|
@@ -14366,269 +14899,73 @@ function getPathsByPrefix(db, prefix, agent_id = "default") {
|
|
|
14366
14899
|
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri LIKE ? ORDER BY uri").all(agent_id, `${prefix}%`);
|
|
14367
14900
|
}
|
|
14368
14901
|
|
|
14369
|
-
// src/
|
|
14370
|
-
|
|
14371
|
-
function
|
|
14372
|
-
const
|
|
14373
|
-
const
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
const agentId = agent_id ?? sourceAgent;
|
|
14379
|
-
db.prepare(
|
|
14380
|
-
`INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
14381
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
14382
|
-
).run(agentId, sourceId, targetId, relation, weight, now());
|
|
14383
|
-
return { agent_id: agentId, source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
14384
|
-
}
|
|
14385
|
-
function getLinks(db, memoryId, agent_id) {
|
|
14386
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id ?? "default";
|
|
14387
|
-
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND (source_id = ? OR target_id = ?)").all(agentId, memoryId, memoryId);
|
|
14388
|
-
}
|
|
14389
|
-
function traverse(db, startId, maxHops = 2, agent_id) {
|
|
14390
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(startId)?.agent_id ?? "default";
|
|
14391
|
-
const visited = /* @__PURE__ */ new Set();
|
|
14392
|
-
const results = [];
|
|
14393
|
-
const queue = [
|
|
14394
|
-
{ id: startId, hop: 0, relation: "self" }
|
|
14902
|
+
// src/sleep/boot.ts
|
|
14903
|
+
init_memory();
|
|
14904
|
+
function boot(db, opts) {
|
|
14905
|
+
const agentId = opts?.agent_id ?? "default";
|
|
14906
|
+
const corePaths = opts?.corePaths ?? [
|
|
14907
|
+
"core://agent",
|
|
14908
|
+
"core://user",
|
|
14909
|
+
"core://agent/identity",
|
|
14910
|
+
"core://user/identity"
|
|
14395
14911
|
];
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14912
|
+
const memories = /* @__PURE__ */ new Map();
|
|
14913
|
+
const identities = listMemories(db, { agent_id: agentId, priority: 0 });
|
|
14914
|
+
for (const mem of identities) {
|
|
14915
|
+
memories.set(mem.id, mem);
|
|
14916
|
+
recordAccess(db, mem.id, 1.1);
|
|
14917
|
+
}
|
|
14918
|
+
const bootPaths = [];
|
|
14919
|
+
for (const uri of corePaths) {
|
|
14920
|
+
const path = getPathByUri(db, uri, agentId);
|
|
14921
|
+
if (path) {
|
|
14922
|
+
bootPaths.push(uri);
|
|
14923
|
+
if (!memories.has(path.memory_id)) {
|
|
14924
|
+
const mem = getMemory(db, path.memory_id);
|
|
14925
|
+
if (mem) {
|
|
14926
|
+
memories.set(mem.id, mem);
|
|
14927
|
+
recordAccess(db, mem.id, 1.1);
|
|
14412
14928
|
}
|
|
14413
14929
|
}
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14930
|
+
}
|
|
14931
|
+
}
|
|
14932
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
14933
|
+
if (bootEntry) {
|
|
14934
|
+
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
14935
|
+
if (bootMem) {
|
|
14936
|
+
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
14937
|
+
for (const uri of additionalUris) {
|
|
14938
|
+
const path = getPathByUri(db, uri, agentId);
|
|
14939
|
+
if (path && !memories.has(path.memory_id)) {
|
|
14940
|
+
const mem = getMemory(db, path.memory_id);
|
|
14941
|
+
if (mem) {
|
|
14942
|
+
memories.set(mem.id, mem);
|
|
14943
|
+
bootPaths.push(uri);
|
|
14944
|
+
}
|
|
14422
14945
|
}
|
|
14423
14946
|
}
|
|
14424
14947
|
}
|
|
14425
14948
|
}
|
|
14426
|
-
return
|
|
14949
|
+
return {
|
|
14950
|
+
identityMemories: [...memories.values()],
|
|
14951
|
+
bootPaths
|
|
14952
|
+
};
|
|
14427
14953
|
}
|
|
14428
14954
|
|
|
14429
|
-
// src/
|
|
14430
|
-
|
|
14431
|
-
init_tokenizer();
|
|
14432
|
-
function createSnapshot(db, memoryId, action, changedBy) {
|
|
14433
|
-
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
14434
|
-
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
14435
|
-
const id = newId();
|
|
14436
|
-
db.prepare(
|
|
14437
|
-
`INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
|
|
14438
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
14439
|
-
).run(id, memoryId, memory.content, changedBy ?? null, action, now());
|
|
14440
|
-
return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
|
|
14441
|
-
}
|
|
14442
|
-
function getSnapshots(db, memoryId) {
|
|
14443
|
-
return db.prepare("SELECT * FROM snapshots WHERE memory_id = ? ORDER BY created_at DESC").all(memoryId);
|
|
14444
|
-
}
|
|
14445
|
-
function getSnapshot(db, id) {
|
|
14446
|
-
return db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id) ?? null;
|
|
14447
|
-
}
|
|
14448
|
-
function rollback(db, snapshotId) {
|
|
14449
|
-
const snapshot = getSnapshot(db, snapshotId);
|
|
14450
|
-
if (!snapshot) return false;
|
|
14451
|
-
createSnapshot(db, snapshot.memory_id, "update", "rollback");
|
|
14452
|
-
db.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?").run(
|
|
14453
|
-
snapshot.content,
|
|
14454
|
-
now(),
|
|
14455
|
-
snapshot.memory_id
|
|
14456
|
-
);
|
|
14457
|
-
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(snapshot.memory_id);
|
|
14458
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
|
|
14459
|
-
snapshot.memory_id,
|
|
14460
|
-
tokenizeForIndex(snapshot.content)
|
|
14461
|
-
);
|
|
14462
|
-
return true;
|
|
14463
|
-
}
|
|
14955
|
+
// src/sleep/sync.ts
|
|
14956
|
+
init_memory();
|
|
14464
14957
|
|
|
14465
|
-
// src/search/
|
|
14466
|
-
|
|
14467
|
-
var INTENT_PATTERNS = {
|
|
14468
|
-
factual: [
|
|
14469
|
-
// English
|
|
14470
|
-
/^(what|who|where|which|how much|how many)\b/i,
|
|
14471
|
-
/\b(name|address|number|password|config|setting)\b/i,
|
|
14472
|
-
// Chinese - questions about facts
|
|
14473
|
-
/是(什么|谁|哪|啥)/,
|
|
14474
|
-
/叫(什么|啥)/,
|
|
14475
|
-
/(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
|
|
14476
|
-
/(多少|几个|哪个|哪些|哪里)/,
|
|
14477
|
-
// Chinese - lookup patterns
|
|
14478
|
-
/(查一下|找一下|看看|搜一下)/,
|
|
14479
|
-
/(.+)是什么$/
|
|
14480
|
-
],
|
|
14481
|
-
temporal: [
|
|
14482
|
-
// English
|
|
14483
|
-
/^(when|what time|how long)\b/i,
|
|
14484
|
-
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
14485
|
-
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
14486
|
-
// Chinese - time expressions
|
|
14487
|
-
/什么时候/,
|
|
14488
|
-
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
14489
|
-
/(几月|几号|几点|多久|多长时间)/,
|
|
14490
|
-
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
14491
|
-
// Date patterns
|
|
14492
|
-
/\d{4}[-/.]\d{1,2}/,
|
|
14493
|
-
/\d{1,2}月\d{1,2}[日号]/,
|
|
14494
|
-
// Chinese - temporal context
|
|
14495
|
-
/(历史|记录|日志|以来|至今|期间)/
|
|
14496
|
-
],
|
|
14497
|
-
causal: [
|
|
14498
|
-
// English
|
|
14499
|
-
/^(why|how come|what caused)\b/i,
|
|
14500
|
-
/\b(because|due to|reason|cause|result)\b/i,
|
|
14501
|
-
// Chinese - causal questions
|
|
14502
|
-
/为(什么|啥|何)/,
|
|
14503
|
-
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
14504
|
-
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
14505
|
-
/(为啥|凭啥|凭什么)/,
|
|
14506
|
-
// Chinese - problem/diagnosis
|
|
14507
|
-
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
14508
|
-
],
|
|
14509
|
-
exploratory: [
|
|
14510
|
-
// English
|
|
14511
|
-
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
14512
|
-
/^(what do you think|what about|any)\b/i,
|
|
14513
|
-
/\b(overview|summary|list|compare)\b/i,
|
|
14514
|
-
// Chinese - exploratory
|
|
14515
|
-
/(怎么样|怎样|如何)/,
|
|
14516
|
-
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
14517
|
-
/(有哪些|有什么|有没有)/,
|
|
14518
|
-
/(关于|对于|至于|关联)/,
|
|
14519
|
-
/(总结|概括|梳理|回顾|盘点)/,
|
|
14520
|
-
// Chinese - opinion/analysis
|
|
14521
|
-
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
14522
|
-
/(对比|比较|区别|差异|优缺点)/
|
|
14523
|
-
]
|
|
14524
|
-
};
|
|
14525
|
-
var CN_STRUCTURE_BOOSTS = {
|
|
14526
|
-
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
14527
|
-
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
14528
|
-
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
14529
|
-
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
14530
|
-
};
|
|
14531
|
-
function classifyIntent(query) {
|
|
14532
|
-
const scores = {
|
|
14533
|
-
factual: 0,
|
|
14534
|
-
exploratory: 0,
|
|
14535
|
-
temporal: 0,
|
|
14536
|
-
causal: 0
|
|
14537
|
-
};
|
|
14538
|
-
for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
|
|
14539
|
-
for (const pattern of patterns) {
|
|
14540
|
-
if (pattern.test(query)) {
|
|
14541
|
-
scores[intent] += 1;
|
|
14542
|
-
}
|
|
14543
|
-
}
|
|
14544
|
-
}
|
|
14545
|
-
for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
|
|
14546
|
-
for (const pattern of patterns) {
|
|
14547
|
-
if (pattern.test(query)) {
|
|
14548
|
-
scores[intent] += 0.5;
|
|
14549
|
-
}
|
|
14550
|
-
}
|
|
14551
|
-
}
|
|
14552
|
-
const tokens = tokenize(query);
|
|
14553
|
-
const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
14554
|
-
if (totalPatternScore === 0 && tokens.length <= 3) {
|
|
14555
|
-
scores.factual += 1;
|
|
14556
|
-
}
|
|
14557
|
-
let maxIntent = "factual";
|
|
14558
|
-
let maxScore = 0;
|
|
14559
|
-
for (const [intent, score] of Object.entries(scores)) {
|
|
14560
|
-
if (score > maxScore) {
|
|
14561
|
-
maxScore = score;
|
|
14562
|
-
maxIntent = intent;
|
|
14563
|
-
}
|
|
14564
|
-
}
|
|
14565
|
-
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
14566
|
-
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
14567
|
-
return { intent: maxIntent, confidence };
|
|
14568
|
-
}
|
|
14569
|
-
function getStrategy(intent) {
|
|
14570
|
-
switch (intent) {
|
|
14571
|
-
case "factual":
|
|
14572
|
-
return { boostRecent: false, boostPriority: true, limit: 5 };
|
|
14573
|
-
case "temporal":
|
|
14574
|
-
return { boostRecent: true, boostPriority: false, limit: 10 };
|
|
14575
|
-
case "causal":
|
|
14576
|
-
return { boostRecent: false, boostPriority: false, limit: 10 };
|
|
14577
|
-
case "exploratory":
|
|
14578
|
-
return { boostRecent: false, boostPriority: false, limit: 15 };
|
|
14579
|
-
}
|
|
14580
|
-
}
|
|
14958
|
+
// src/search/hybrid.ts
|
|
14959
|
+
init_memory();
|
|
14581
14960
|
|
|
14582
|
-
// src/search/
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
const
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
const score = scoreMap.get(i);
|
|
14591
|
-
if (score === void 0) return r;
|
|
14592
|
-
return {
|
|
14593
|
-
...r,
|
|
14594
|
-
score,
|
|
14595
|
-
matchReason: `${r.matchReason}+rerank`
|
|
14596
|
-
};
|
|
14597
|
-
});
|
|
14598
|
-
} catch (err) {
|
|
14599
|
-
console.warn("[agent-memory] External rerank failed, falling back:", err);
|
|
14600
|
-
return results;
|
|
14601
|
-
}
|
|
14602
|
-
}
|
|
14603
|
-
function rerank(results, opts) {
|
|
14604
|
-
const now2 = Date.now();
|
|
14605
|
-
const scored = results.map((r) => {
|
|
14606
|
-
let finalScore = r.score;
|
|
14607
|
-
if (opts.boostPriority) {
|
|
14608
|
-
const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
|
|
14609
|
-
finalScore *= priorityMultiplier;
|
|
14610
|
-
}
|
|
14611
|
-
if (opts.boostRecent && r.memory.updated_at) {
|
|
14612
|
-
const age = now2 - new Date(r.memory.updated_at).getTime();
|
|
14613
|
-
const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
|
|
14614
|
-
const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
|
|
14615
|
-
finalScore *= recencyBoost;
|
|
14616
|
-
}
|
|
14617
|
-
finalScore *= Math.max(0.1, r.memory.vitality);
|
|
14618
|
-
return { ...r, score: finalScore };
|
|
14619
|
-
});
|
|
14620
|
-
scored.sort((a, b) => b.score - a.score);
|
|
14621
|
-
return scored.slice(0, opts.limit);
|
|
14622
|
-
}
|
|
14623
|
-
|
|
14624
|
-
// src/search/bm25.ts
|
|
14625
|
-
init_tokenizer();
|
|
14626
|
-
function searchBM25(db, query, opts) {
|
|
14627
|
-
const limit = opts?.limit ?? 20;
|
|
14628
|
-
const agentId = opts?.agent_id ?? "default";
|
|
14629
|
-
const minVitality = opts?.min_vitality ?? 0;
|
|
14630
|
-
const ftsQuery = buildFtsQuery(query);
|
|
14631
|
-
if (!ftsQuery) return [];
|
|
14961
|
+
// src/search/bm25.ts
|
|
14962
|
+
init_tokenizer();
|
|
14963
|
+
function searchBM25(db, query, opts) {
|
|
14964
|
+
const limit = opts?.limit ?? 20;
|
|
14965
|
+
const agentId = opts?.agent_id ?? "default";
|
|
14966
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
14967
|
+
const ftsQuery = buildFtsQuery(query);
|
|
14968
|
+
if (!ftsQuery) return [];
|
|
14632
14969
|
try {
|
|
14633
14970
|
const rows = db.prepare(
|
|
14634
14971
|
`SELECT m.*, rank AS score
|
|
@@ -14640,12 +14977,13 @@ function searchBM25(db, query, opts) {
|
|
|
14640
14977
|
ORDER BY rank
|
|
14641
14978
|
LIMIT ?`
|
|
14642
14979
|
).all(ftsQuery, agentId, minVitality, limit);
|
|
14643
|
-
return rows.map((row) => {
|
|
14980
|
+
return rows.map((row, index) => {
|
|
14644
14981
|
const { score: _score, ...memoryFields } = row;
|
|
14645
14982
|
return {
|
|
14646
14983
|
memory: memoryFields,
|
|
14647
14984
|
score: Math.abs(row.score),
|
|
14648
14985
|
// FTS5 rank is negative (lower = better)
|
|
14986
|
+
rank: index + 1,
|
|
14649
14987
|
matchReason: "bm25"
|
|
14650
14988
|
};
|
|
14651
14989
|
});
|
|
@@ -14660,10 +14998,10 @@ function searchSimple(db, query, agentId, minVitality, limit) {
|
|
|
14660
14998
|
ORDER BY priority ASC, updated_at DESC
|
|
14661
14999
|
LIMIT ?`
|
|
14662
15000
|
).all(agentId, minVitality, `%${query}%`, limit);
|
|
14663
|
-
return rows.map((
|
|
14664
|
-
memory
|
|
14665
|
-
score: 1 / (
|
|
14666
|
-
|
|
15001
|
+
return rows.map((memory, index) => ({
|
|
15002
|
+
memory,
|
|
15003
|
+
score: 1 / (index + 1),
|
|
15004
|
+
rank: index + 1,
|
|
14667
15005
|
matchReason: "like"
|
|
14668
15006
|
}));
|
|
14669
15007
|
}
|
|
@@ -14672,384 +15010,427 @@ function buildFtsQuery(text) {
|
|
|
14672
15010
|
if (tokens.length === 0) return null;
|
|
14673
15011
|
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
14674
15012
|
}
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
14678
|
-
|
|
14679
|
-
const
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
14693
|
-
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
14694
|
-
dim = excluded.dim,
|
|
14695
|
-
vector = excluded.vector,
|
|
14696
|
-
updated_at = excluded.updated_at`
|
|
14697
|
-
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
14698
|
-
}
|
|
14699
|
-
function listEmbeddings(db, agent_id, model) {
|
|
14700
|
-
const rows = db.prepare(
|
|
14701
|
-
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
14702
|
-
).all(agent_id, model);
|
|
14703
|
-
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
15013
|
+
function rebuildBm25Index(db, opts) {
|
|
15014
|
+
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();
|
|
15015
|
+
const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
|
|
15016
|
+
const deleteOne = db.prepare("DELETE FROM memories_fts WHERE id = ?");
|
|
15017
|
+
const transaction = db.transaction(() => {
|
|
15018
|
+
if (!opts?.agent_id) {
|
|
15019
|
+
db.exec("DELETE FROM memories_fts");
|
|
15020
|
+
}
|
|
15021
|
+
for (const memory of memories) {
|
|
15022
|
+
if (opts?.agent_id) {
|
|
15023
|
+
deleteOne.run(memory.id);
|
|
15024
|
+
}
|
|
15025
|
+
insert.run(memory.id, tokenizeForIndex(memory.content));
|
|
15026
|
+
}
|
|
15027
|
+
});
|
|
15028
|
+
transaction();
|
|
15029
|
+
return { reindexed: memories.length };
|
|
14704
15030
|
}
|
|
14705
15031
|
|
|
14706
15032
|
// src/search/hybrid.ts
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
async function searchHybrid(db, query, opts) {
|
|
14750
|
-
const agentId = opts?.agent_id ?? "default";
|
|
14751
|
-
const limit = opts?.limit ?? 10;
|
|
14752
|
-
const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
|
|
14753
|
-
const semanticCandidates = opts?.semanticCandidates ?? 50;
|
|
14754
|
-
const rrfK = opts?.rrfK ?? 60;
|
|
14755
|
-
const bm25 = searchBM25(db, query, {
|
|
14756
|
-
agent_id: agentId,
|
|
14757
|
-
limit: limit * bm25Mult
|
|
14758
|
-
});
|
|
14759
|
-
const provider = opts?.embeddingProvider ?? null;
|
|
14760
|
-
const model = opts?.embeddingModel ?? provider?.model;
|
|
14761
|
-
if (!provider || !model) {
|
|
14762
|
-
return bm25.slice(0, limit);
|
|
14763
|
-
}
|
|
14764
|
-
const embedFn = provider.embedQuery ?? provider.embed;
|
|
14765
|
-
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
14766
|
-
const embeddings = listEmbeddings(db, agentId, model);
|
|
14767
|
-
const scored = [];
|
|
14768
|
-
for (const e of embeddings) {
|
|
14769
|
-
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
14770
|
-
}
|
|
14771
|
-
scored.sort((a, b) => b.score - a.score);
|
|
14772
|
-
const semanticTop = scored.slice(0, semanticCandidates);
|
|
14773
|
-
const fused = fuseRrf(
|
|
14774
|
-
[
|
|
14775
|
-
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
14776
|
-
{ name: "semantic", items: semanticTop }
|
|
14777
|
-
],
|
|
14778
|
-
rrfK
|
|
14779
|
-
);
|
|
14780
|
-
const ids = [...fused.keys()];
|
|
14781
|
-
const memories = fetchMemories(db, ids, agentId);
|
|
14782
|
-
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
14783
|
-
const out = [];
|
|
14784
|
-
for (const [id, meta3] of fused) {
|
|
14785
|
-
const mem = byId.get(id);
|
|
14786
|
-
if (!mem) continue;
|
|
14787
|
-
out.push({
|
|
14788
|
-
memory: mem,
|
|
14789
|
-
score: meta3.score,
|
|
14790
|
-
matchReason: meta3.sources.sort().join("+")
|
|
15033
|
+
init_providers();
|
|
15034
|
+
init_vector();
|
|
15035
|
+
var PRIORITY_WEIGHT = {
|
|
15036
|
+
0: 4,
|
|
15037
|
+
1: 3,
|
|
15038
|
+
2: 2,
|
|
15039
|
+
3: 1
|
|
15040
|
+
};
|
|
15041
|
+
var PRIORITY_PRIOR = {
|
|
15042
|
+
0: 1,
|
|
15043
|
+
1: 0.75,
|
|
15044
|
+
2: 0.5,
|
|
15045
|
+
3: 0.25
|
|
15046
|
+
};
|
|
15047
|
+
function scoreBm25Only(results, limit) {
|
|
15048
|
+
return results.map((row) => {
|
|
15049
|
+
const weight = PRIORITY_WEIGHT[row.memory.priority] ?? 1;
|
|
15050
|
+
const vitality = Math.max(0.1, row.memory.vitality);
|
|
15051
|
+
return {
|
|
15052
|
+
memory: row.memory,
|
|
15053
|
+
score: row.score * weight * vitality,
|
|
15054
|
+
bm25_rank: row.rank,
|
|
15055
|
+
bm25_score: row.score
|
|
15056
|
+
};
|
|
15057
|
+
}).sort((left, right) => right.score - left.score).slice(0, limit);
|
|
15058
|
+
}
|
|
15059
|
+
function priorityPrior(priority) {
|
|
15060
|
+
return PRIORITY_PRIOR[priority] ?? 0.25;
|
|
15061
|
+
}
|
|
15062
|
+
function fusionScore(input) {
|
|
15063
|
+
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
15064
|
+
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
15065
|
+
return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
15066
|
+
}
|
|
15067
|
+
function fuseHybridResults(lexical, vector, limit) {
|
|
15068
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
15069
|
+
for (const row of lexical) {
|
|
15070
|
+
candidates.set(row.memory.id, {
|
|
15071
|
+
memory: row.memory,
|
|
15072
|
+
score: 0,
|
|
15073
|
+
bm25_rank: row.rank,
|
|
15074
|
+
bm25_score: row.score
|
|
14791
15075
|
});
|
|
14792
15076
|
}
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
}
|
|
14805
|
-
|
|
14806
|
-
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
14807
|
-
if (override !== void 0) {
|
|
14808
|
-
const normalized = override.trim();
|
|
14809
|
-
if (!normalized) return null;
|
|
14810
|
-
const lowered = normalized.toLowerCase();
|
|
14811
|
-
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
14812
|
-
return normalized;
|
|
14813
|
-
}
|
|
14814
|
-
return getDefaultInstruction(model);
|
|
14815
|
-
}
|
|
14816
|
-
function buildQueryInput(query, instructionPrefix) {
|
|
14817
|
-
if (!instructionPrefix) return query;
|
|
14818
|
-
return `Instruct: ${instructionPrefix}
|
|
14819
|
-
Query: ${query}`;
|
|
14820
|
-
}
|
|
14821
|
-
function getEmbeddingProviderFromEnv() {
|
|
14822
|
-
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
14823
|
-
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
14824
|
-
if (provider === "openai") {
|
|
14825
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
14826
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
14827
|
-
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
14828
|
-
if (!apiKey) return null;
|
|
14829
|
-
const instruction = resolveInstruction(model);
|
|
14830
|
-
return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
|
|
14831
|
-
}
|
|
14832
|
-
if (provider === "gemini" || provider === "google") {
|
|
14833
|
-
const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
14834
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
|
|
14835
|
-
const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
14836
|
-
if (!apiKey) return null;
|
|
14837
|
-
const instruction = resolveInstruction(model);
|
|
14838
|
-
return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
|
|
14839
|
-
}
|
|
14840
|
-
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
14841
|
-
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
14842
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
14843
|
-
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
14844
|
-
if (!apiKey) return null;
|
|
14845
|
-
const instruction = resolveInstruction(model);
|
|
14846
|
-
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
15077
|
+
for (const row of vector) {
|
|
15078
|
+
const existing = candidates.get(row.memory.id);
|
|
15079
|
+
if (existing) {
|
|
15080
|
+
existing.vector_rank = row.rank;
|
|
15081
|
+
existing.vector_score = row.similarity;
|
|
15082
|
+
} else {
|
|
15083
|
+
candidates.set(row.memory.id, {
|
|
15084
|
+
memory: row.memory,
|
|
15085
|
+
score: 0,
|
|
15086
|
+
vector_rank: row.rank,
|
|
15087
|
+
vector_score: row.similarity
|
|
15088
|
+
});
|
|
15089
|
+
}
|
|
14847
15090
|
}
|
|
14848
|
-
return
|
|
15091
|
+
return [...candidates.values()].map((row) => ({
|
|
15092
|
+
...row,
|
|
15093
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
15094
|
+
})).sort((left, right) => {
|
|
15095
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
15096
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
15097
|
+
}).slice(0, limit);
|
|
15098
|
+
}
|
|
15099
|
+
async function searchVectorBranch(db, query, opts) {
|
|
15100
|
+
const [queryVector] = await opts.provider.embed([query]);
|
|
15101
|
+
if (!queryVector) return [];
|
|
15102
|
+
return searchByVector(db, queryVector, {
|
|
15103
|
+
providerId: opts.provider.id,
|
|
15104
|
+
agent_id: opts.agent_id,
|
|
15105
|
+
limit: opts.limit,
|
|
15106
|
+
min_vitality: opts.min_vitality
|
|
15107
|
+
});
|
|
14849
15108
|
}
|
|
14850
|
-
function
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
}
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
if (!resp.ok) {
|
|
14874
|
-
const body = await resp.text().catch(() => "");
|
|
14875
|
-
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
15109
|
+
async function recallMemories(db, query, opts) {
|
|
15110
|
+
const limit = opts?.limit ?? 10;
|
|
15111
|
+
const agentId = opts?.agent_id ?? "default";
|
|
15112
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
15113
|
+
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
15114
|
+
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
15115
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
15116
|
+
const lexical = searchBM25(db, query, {
|
|
15117
|
+
agent_id: agentId,
|
|
15118
|
+
limit: lexicalLimit,
|
|
15119
|
+
min_vitality: minVitality
|
|
15120
|
+
});
|
|
15121
|
+
let vector = [];
|
|
15122
|
+
if (provider) {
|
|
15123
|
+
try {
|
|
15124
|
+
vector = await searchVectorBranch(db, query, {
|
|
15125
|
+
provider,
|
|
15126
|
+
agent_id: agentId,
|
|
15127
|
+
limit: vectorLimit,
|
|
15128
|
+
min_vitality: minVitality
|
|
15129
|
+
});
|
|
15130
|
+
} catch {
|
|
15131
|
+
vector = [];
|
|
14876
15132
|
}
|
|
14877
|
-
const data = await resp.json();
|
|
14878
|
-
return normalizeEmbedding(data.data?.[0]?.embedding);
|
|
14879
15133
|
}
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
return requestEmbedding(text);
|
|
14886
|
-
},
|
|
14887
|
-
async embedQuery(query) {
|
|
14888
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
15134
|
+
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
15135
|
+
const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
|
|
15136
|
+
if (opts?.recordAccess !== false) {
|
|
15137
|
+
for (const row of results) {
|
|
15138
|
+
recordAccess(db, row.memory.id);
|
|
14889
15139
|
}
|
|
15140
|
+
}
|
|
15141
|
+
return {
|
|
15142
|
+
mode,
|
|
15143
|
+
providerId: provider?.id ?? null,
|
|
15144
|
+
usedVectorSearch: vector.length > 0,
|
|
15145
|
+
results
|
|
14890
15146
|
};
|
|
14891
15147
|
}
|
|
14892
|
-
function
|
|
14893
|
-
const
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
if (
|
|
14908
|
-
|
|
14909
|
-
|
|
15148
|
+
function listReindexCandidates(db, providerId, agentId, force) {
|
|
15149
|
+
const rows = db.prepare(
|
|
15150
|
+
`SELECT m.id as memoryId,
|
|
15151
|
+
m.content as content,
|
|
15152
|
+
m.hash as contentHash,
|
|
15153
|
+
e.status as embeddingStatus,
|
|
15154
|
+
e.content_hash as embeddingHash
|
|
15155
|
+
FROM memories m
|
|
15156
|
+
LEFT JOIN embeddings e
|
|
15157
|
+
ON e.memory_id = m.id
|
|
15158
|
+
AND e.provider_id = ?
|
|
15159
|
+
WHERE m.agent_id = ?
|
|
15160
|
+
AND m.hash IS NOT NULL`
|
|
15161
|
+
).all(providerId, agentId);
|
|
15162
|
+
return rows.filter((row) => {
|
|
15163
|
+
if (force) return true;
|
|
15164
|
+
if (!row.embeddingStatus) return true;
|
|
15165
|
+
if (row.embeddingStatus !== "ready") return true;
|
|
15166
|
+
return row.embeddingHash !== row.contentHash;
|
|
15167
|
+
}).map((row) => ({
|
|
15168
|
+
memoryId: row.memoryId,
|
|
15169
|
+
content: row.content,
|
|
15170
|
+
contentHash: row.contentHash
|
|
15171
|
+
}));
|
|
15172
|
+
}
|
|
15173
|
+
async function reindexEmbeddings(db, opts) {
|
|
15174
|
+
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
15175
|
+
if (!provider) {
|
|
15176
|
+
return {
|
|
15177
|
+
enabled: false,
|
|
15178
|
+
providerId: null,
|
|
15179
|
+
scanned: 0,
|
|
15180
|
+
pending: 0,
|
|
15181
|
+
embedded: 0,
|
|
15182
|
+
failed: 0
|
|
15183
|
+
};
|
|
15184
|
+
}
|
|
15185
|
+
const agentId = opts?.agent_id ?? "default";
|
|
15186
|
+
const force = opts?.force ?? false;
|
|
15187
|
+
const batchSize = Math.max(1, opts?.batchSize ?? 16);
|
|
15188
|
+
const candidates = listReindexCandidates(db, provider.id, agentId, force);
|
|
15189
|
+
for (const candidate of candidates) {
|
|
15190
|
+
markMemoryEmbeddingPending(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
15191
|
+
}
|
|
15192
|
+
let embedded = 0;
|
|
15193
|
+
let failed = 0;
|
|
15194
|
+
for (let index = 0; index < candidates.length; index += batchSize) {
|
|
15195
|
+
const batch = candidates.slice(index, index + batchSize);
|
|
15196
|
+
try {
|
|
15197
|
+
const vectors = await provider.embed(batch.map((row) => row.content));
|
|
15198
|
+
if (vectors.length !== batch.length) {
|
|
15199
|
+
throw new Error(`Expected ${batch.length} embeddings, received ${vectors.length}`);
|
|
15200
|
+
}
|
|
15201
|
+
for (let offset = 0; offset < batch.length; offset++) {
|
|
15202
|
+
upsertReadyEmbedding({
|
|
15203
|
+
db,
|
|
15204
|
+
memoryId: batch[offset].memoryId,
|
|
15205
|
+
providerId: provider.id,
|
|
15206
|
+
vector: vectors[offset],
|
|
15207
|
+
contentHash: batch[offset].contentHash
|
|
15208
|
+
});
|
|
15209
|
+
embedded += 1;
|
|
15210
|
+
}
|
|
15211
|
+
} catch {
|
|
15212
|
+
for (const candidate of batch) {
|
|
15213
|
+
markEmbeddingFailed(db, candidate.memoryId, provider.id, candidate.contentHash);
|
|
15214
|
+
failed += 1;
|
|
15215
|
+
}
|
|
14910
15216
|
}
|
|
14911
|
-
const data = await resp.json();
|
|
14912
|
-
const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
|
|
14913
|
-
return normalizeEmbedding(emb);
|
|
14914
15217
|
}
|
|
14915
15218
|
return {
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
async embedQuery(query) {
|
|
14923
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
14924
|
-
}
|
|
15219
|
+
enabled: true,
|
|
15220
|
+
providerId: provider.id,
|
|
15221
|
+
scanned: candidates.length,
|
|
15222
|
+
pending: candidates.length,
|
|
15223
|
+
embedded,
|
|
15224
|
+
failed
|
|
14925
15225
|
};
|
|
14926
15226
|
}
|
|
14927
15227
|
|
|
14928
|
-
// src/
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
15228
|
+
// src/core/guard.ts
|
|
15229
|
+
init_providers();
|
|
15230
|
+
init_tokenizer();
|
|
15231
|
+
|
|
15232
|
+
// src/core/merge.ts
|
|
15233
|
+
function uniqueNonEmpty(values) {
|
|
15234
|
+
return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
|
|
15235
|
+
}
|
|
15236
|
+
function splitClauses(content) {
|
|
15237
|
+
return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
|
|
15238
|
+
}
|
|
15239
|
+
function mergeAliases(existing, incoming, content) {
|
|
15240
|
+
const aliases = uniqueNonEmpty([
|
|
15241
|
+
existing !== content ? existing : void 0,
|
|
15242
|
+
incoming !== content ? incoming : void 0
|
|
15243
|
+
]);
|
|
15244
|
+
return aliases.length > 0 ? aliases : void 0;
|
|
15245
|
+
}
|
|
15246
|
+
function replaceIdentity(context) {
|
|
15247
|
+
const content = context.incoming.content.trim();
|
|
14946
15248
|
return {
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
15249
|
+
strategy: "replace",
|
|
15250
|
+
content,
|
|
15251
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
15252
|
+
notes: ["identity canonicalized to the newest authoritative phrasing"]
|
|
15253
|
+
};
|
|
15254
|
+
}
|
|
15255
|
+
function appendEmotionEvidence(context) {
|
|
15256
|
+
const lines = uniqueNonEmpty([
|
|
15257
|
+
...context.existing.content.split(/\n+/),
|
|
15258
|
+
context.incoming.content
|
|
15259
|
+
]);
|
|
15260
|
+
const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
|
|
15261
|
+
return {
|
|
15262
|
+
strategy: "append_evidence",
|
|
15263
|
+
content,
|
|
15264
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
15265
|
+
notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
|
|
15266
|
+
};
|
|
15267
|
+
}
|
|
15268
|
+
function synthesizeKnowledge(context) {
|
|
15269
|
+
const clauses = uniqueNonEmpty([
|
|
15270
|
+
...splitClauses(context.existing.content),
|
|
15271
|
+
...splitClauses(context.incoming.content)
|
|
15272
|
+
]);
|
|
15273
|
+
const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
|
|
15274
|
+
return {
|
|
15275
|
+
strategy: "synthesize",
|
|
15276
|
+
content,
|
|
15277
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
15278
|
+
notes: ["knowledge statements synthesized into a canonical summary"]
|
|
15279
|
+
};
|
|
15280
|
+
}
|
|
15281
|
+
function compactEventTimeline(context) {
|
|
15282
|
+
const points = uniqueNonEmpty([
|
|
15283
|
+
...context.existing.content.split(/\n+/),
|
|
15284
|
+
context.incoming.content
|
|
15285
|
+
]).map((line) => line.replace(/^-\s*/, ""));
|
|
15286
|
+
const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
|
|
15287
|
+
return {
|
|
15288
|
+
strategy: "compact_timeline",
|
|
15289
|
+
content,
|
|
15290
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
15291
|
+
notes: ["event observations compacted into a single timeline window"]
|
|
14970
15292
|
};
|
|
14971
15293
|
}
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
memory_id: row.id,
|
|
14985
|
-
model,
|
|
14986
|
-
vector
|
|
14987
|
-
});
|
|
14988
|
-
return true;
|
|
15294
|
+
function buildMergePlan(context) {
|
|
15295
|
+
const type = context.incoming.type ?? context.existing.type;
|
|
15296
|
+
switch (type) {
|
|
15297
|
+
case "identity":
|
|
15298
|
+
return replaceIdentity(context);
|
|
15299
|
+
case "emotion":
|
|
15300
|
+
return appendEmotionEvidence(context);
|
|
15301
|
+
case "knowledge":
|
|
15302
|
+
return synthesizeKnowledge(context);
|
|
15303
|
+
case "event":
|
|
15304
|
+
return compactEventTimeline(context);
|
|
15305
|
+
}
|
|
14989
15306
|
}
|
|
14990
15307
|
|
|
14991
|
-
// src/sleep/sync.ts
|
|
14992
|
-
init_memory();
|
|
14993
|
-
|
|
14994
15308
|
// src/core/guard.ts
|
|
14995
15309
|
init_memory();
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15310
|
+
var NEAR_EXACT_THRESHOLD = 0.93;
|
|
15311
|
+
var MERGE_THRESHOLD = 0.82;
|
|
15312
|
+
function clamp01(value) {
|
|
15313
|
+
if (!Number.isFinite(value)) return 0;
|
|
15314
|
+
return Math.max(0, Math.min(1, value));
|
|
15315
|
+
}
|
|
15316
|
+
function uniqueTokenSet(text) {
|
|
15317
|
+
return new Set(tokenize(text));
|
|
15318
|
+
}
|
|
15319
|
+
function overlapScore(left, right) {
|
|
15320
|
+
const a = new Set(left);
|
|
15321
|
+
const b = new Set(right);
|
|
15322
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
15323
|
+
let shared = 0;
|
|
15324
|
+
for (const token of a) {
|
|
15325
|
+
if (b.has(token)) shared += 1;
|
|
15326
|
+
}
|
|
15327
|
+
return shared / Math.max(a.size, b.size);
|
|
15328
|
+
}
|
|
15329
|
+
function extractEntities(text) {
|
|
15330
|
+
const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
|
|
15331
|
+
return new Set(matches.map((value) => value.trim()).filter(Boolean));
|
|
15332
|
+
}
|
|
15333
|
+
function safeDomain(uri) {
|
|
15334
|
+
if (!uri) return null;
|
|
15335
|
+
try {
|
|
15336
|
+
return parseUri(uri).domain;
|
|
15337
|
+
} catch {
|
|
15338
|
+
return null;
|
|
15003
15339
|
}
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15340
|
+
}
|
|
15341
|
+
function getPrimaryUri(db, memoryId, agentId) {
|
|
15342
|
+
const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
|
|
15343
|
+
return row?.uri ?? null;
|
|
15344
|
+
}
|
|
15345
|
+
function uriScopeMatch(inputUri, candidateUri) {
|
|
15346
|
+
if (inputUri && candidateUri) {
|
|
15347
|
+
if (inputUri === candidateUri) return 1;
|
|
15348
|
+
const inputDomain2 = safeDomain(inputUri);
|
|
15349
|
+
const candidateDomain2 = safeDomain(candidateUri);
|
|
15350
|
+
if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
|
|
15351
|
+
return 0;
|
|
15352
|
+
}
|
|
15353
|
+
if (!inputUri && !candidateUri) {
|
|
15354
|
+
return 0.65;
|
|
15355
|
+
}
|
|
15356
|
+
const inputDomain = safeDomain(inputUri ?? null);
|
|
15357
|
+
const candidateDomain = safeDomain(candidateUri ?? null);
|
|
15358
|
+
if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
|
|
15359
|
+
return 0.75;
|
|
15360
|
+
}
|
|
15361
|
+
return 0.2;
|
|
15362
|
+
}
|
|
15363
|
+
function extractObservedAt(parts, fallback) {
|
|
15364
|
+
for (const part of parts) {
|
|
15365
|
+
if (!part) continue;
|
|
15366
|
+
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
15367
|
+
if (!match) continue;
|
|
15368
|
+
const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
|
|
15369
|
+
const parsed = new Date(iso);
|
|
15370
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
15371
|
+
return parsed;
|
|
15012
15372
|
}
|
|
15013
15373
|
}
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
const similar = db.prepare(
|
|
15019
|
-
`SELECT m.id, m.content, m.type, rank
|
|
15020
|
-
FROM memories_fts f
|
|
15021
|
-
JOIN memories m ON m.id = f.id
|
|
15022
|
-
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
15023
|
-
ORDER BY rank
|
|
15024
|
-
LIMIT 3`
|
|
15025
|
-
).all(ftsQuery, agentId);
|
|
15026
|
-
if (similar.length > 0) {
|
|
15027
|
-
const topRank = Math.abs(similar[0].rank);
|
|
15028
|
-
const tokenCount = ftsTokens.length;
|
|
15029
|
-
const dynamicThreshold = tokenCount * 1.5;
|
|
15030
|
-
if (topRank > dynamicThreshold) {
|
|
15031
|
-
const existing = similar[0];
|
|
15032
|
-
if (existing.type === input.type) {
|
|
15033
|
-
const merged = `${existing.content}
|
|
15034
|
-
|
|
15035
|
-
[Updated] ${input.content}`;
|
|
15036
|
-
return {
|
|
15037
|
-
action: "merge",
|
|
15038
|
-
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
15039
|
-
existingId: existing.id,
|
|
15040
|
-
mergedContent: merged
|
|
15041
|
-
};
|
|
15042
|
-
}
|
|
15043
|
-
}
|
|
15044
|
-
}
|
|
15045
|
-
} catch {
|
|
15374
|
+
if (fallback) {
|
|
15375
|
+
const parsed = new Date(fallback);
|
|
15376
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
15377
|
+
return parsed;
|
|
15046
15378
|
}
|
|
15047
15379
|
}
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15380
|
+
return null;
|
|
15381
|
+
}
|
|
15382
|
+
function timeProximity(input, memory, candidateUri) {
|
|
15383
|
+
if (input.type !== "event") {
|
|
15384
|
+
return 1;
|
|
15051
15385
|
}
|
|
15052
|
-
|
|
15386
|
+
const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
|
|
15387
|
+
const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
|
|
15388
|
+
if (!inputTime || !existingTime) {
|
|
15389
|
+
return 0.5;
|
|
15390
|
+
}
|
|
15391
|
+
const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
15392
|
+
return clamp01(1 - diffDays / 7);
|
|
15393
|
+
}
|
|
15394
|
+
function scoreCandidate(input, candidate, candidateUri) {
|
|
15395
|
+
const lexicalOverlap = overlapScore(uniqueTokenSet(input.content), uniqueTokenSet(candidate.memory.content));
|
|
15396
|
+
const entityOverlap = Math.max(
|
|
15397
|
+
overlapScore(extractEntities(input.content), extractEntities(candidate.memory.content)),
|
|
15398
|
+
lexicalOverlap * 0.75
|
|
15399
|
+
);
|
|
15400
|
+
const uriMatch = uriScopeMatch(input.uri, candidateUri);
|
|
15401
|
+
const temporal = timeProximity(input, candidate.memory, candidateUri);
|
|
15402
|
+
const semantic = clamp01(candidate.vector_score ?? lexicalOverlap);
|
|
15403
|
+
const dedupScore = clamp01(
|
|
15404
|
+
0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
|
|
15405
|
+
);
|
|
15406
|
+
return {
|
|
15407
|
+
semantic_similarity: semantic,
|
|
15408
|
+
lexical_overlap: lexicalOverlap,
|
|
15409
|
+
uri_scope_match: uriMatch,
|
|
15410
|
+
entity_overlap: entityOverlap,
|
|
15411
|
+
time_proximity: temporal,
|
|
15412
|
+
dedup_score: dedupScore
|
|
15413
|
+
};
|
|
15414
|
+
}
|
|
15415
|
+
async function recallCandidates(db, input, agentId) {
|
|
15416
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
15417
|
+
const response = await recallMemories(db, input.content, {
|
|
15418
|
+
agent_id: agentId,
|
|
15419
|
+
limit: Math.max(6, input.candidateLimit ?? 8),
|
|
15420
|
+
lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
15421
|
+
vectorLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
15422
|
+
provider,
|
|
15423
|
+
recordAccess: false
|
|
15424
|
+
});
|
|
15425
|
+
return response.results.filter((row) => row.memory.type === input.type).map((row) => {
|
|
15426
|
+
const uri = getPrimaryUri(db, row.memory.id, agentId);
|
|
15427
|
+
return {
|
|
15428
|
+
result: row,
|
|
15429
|
+
uri,
|
|
15430
|
+
domain: safeDomain(uri),
|
|
15431
|
+
score: scoreCandidate(input, row, uri)
|
|
15432
|
+
};
|
|
15433
|
+
}).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
|
|
15053
15434
|
}
|
|
15054
15435
|
function fourCriterionGate(input) {
|
|
15055
15436
|
const content = input.content.trim();
|
|
@@ -15074,15 +15455,9 @@ function fourCriterionGate(input) {
|
|
|
15074
15455
|
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
15075
15456
|
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
15076
15457
|
let coherence = 1;
|
|
15077
|
-
if (allCaps)
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
if (!hasWhitespaceOrPunctuation) {
|
|
15081
|
-
coherence -= 0.3;
|
|
15082
|
-
}
|
|
15083
|
-
if (excessiveRepetition) {
|
|
15084
|
-
coherence -= 0.5;
|
|
15085
|
-
}
|
|
15458
|
+
if (allCaps) coherence -= 0.5;
|
|
15459
|
+
if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
|
|
15460
|
+
if (excessiveRepetition) coherence -= 0.5;
|
|
15086
15461
|
coherence = Math.max(0, coherence);
|
|
15087
15462
|
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
15088
15463
|
return {
|
|
@@ -15091,10 +15466,81 @@ function fourCriterionGate(input) {
|
|
|
15091
15466
|
failedCriteria: failed
|
|
15092
15467
|
};
|
|
15093
15468
|
}
|
|
15469
|
+
async function guard(db, input) {
|
|
15470
|
+
const hash2 = contentHash(input.content);
|
|
15471
|
+
const agentId = input.agent_id ?? "default";
|
|
15472
|
+
const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash2, agentId);
|
|
15473
|
+
if (exactMatch) {
|
|
15474
|
+
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
15475
|
+
}
|
|
15476
|
+
if (input.uri) {
|
|
15477
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
15478
|
+
if (existingPath) {
|
|
15479
|
+
return {
|
|
15480
|
+
action: "update",
|
|
15481
|
+
reason: `URI ${input.uri} already exists, updating canonical content`,
|
|
15482
|
+
existingId: existingPath.memory_id,
|
|
15483
|
+
updatedContent: input.content
|
|
15484
|
+
};
|
|
15485
|
+
}
|
|
15486
|
+
}
|
|
15487
|
+
const gateResult = fourCriterionGate(input);
|
|
15488
|
+
if (!gateResult.pass) {
|
|
15489
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
15490
|
+
}
|
|
15491
|
+
if (input.conservative) {
|
|
15492
|
+
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
15493
|
+
}
|
|
15494
|
+
const candidates = await recallCandidates(db, input, agentId);
|
|
15495
|
+
const best = candidates[0];
|
|
15496
|
+
if (!best) {
|
|
15497
|
+
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
15498
|
+
}
|
|
15499
|
+
const score = best.score;
|
|
15500
|
+
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
15501
|
+
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
15502
|
+
return {
|
|
15503
|
+
action: shouldUpdateMetadata ? "update" : "skip",
|
|
15504
|
+
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
15505
|
+
existingId: best.result.memory.id,
|
|
15506
|
+
score
|
|
15507
|
+
};
|
|
15508
|
+
}
|
|
15509
|
+
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
15510
|
+
const mergePlan = buildMergePlan({
|
|
15511
|
+
existing: best.result.memory,
|
|
15512
|
+
incoming: {
|
|
15513
|
+
content: input.content,
|
|
15514
|
+
type: input.type,
|
|
15515
|
+
source: input.source
|
|
15516
|
+
}
|
|
15517
|
+
});
|
|
15518
|
+
return {
|
|
15519
|
+
action: "merge",
|
|
15520
|
+
reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
|
|
15521
|
+
existingId: best.result.memory.id,
|
|
15522
|
+
mergedContent: mergePlan.content,
|
|
15523
|
+
mergePlan,
|
|
15524
|
+
score
|
|
15525
|
+
};
|
|
15526
|
+
}
|
|
15527
|
+
return {
|
|
15528
|
+
action: "add",
|
|
15529
|
+
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
15530
|
+
score
|
|
15531
|
+
};
|
|
15532
|
+
}
|
|
15094
15533
|
|
|
15095
15534
|
// src/sleep/sync.ts
|
|
15096
|
-
|
|
15097
|
-
|
|
15535
|
+
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
15536
|
+
if (!uri) return;
|
|
15537
|
+
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
15538
|
+
try {
|
|
15539
|
+
createPath(db, memoryId, uri, void 0, void 0, agentId);
|
|
15540
|
+
} catch {
|
|
15541
|
+
}
|
|
15542
|
+
}
|
|
15543
|
+
async function syncOne(db, input) {
|
|
15098
15544
|
const memInput = {
|
|
15099
15545
|
content: input.content,
|
|
15100
15546
|
type: input.type ?? "event",
|
|
@@ -15102,38 +15548,606 @@ function syncOne(db, input) {
|
|
|
15102
15548
|
emotion_val: input.emotion_val,
|
|
15103
15549
|
source: input.source,
|
|
15104
15550
|
agent_id: input.agent_id,
|
|
15105
|
-
uri: input.uri
|
|
15551
|
+
uri: input.uri,
|
|
15552
|
+
provider: input.provider,
|
|
15553
|
+
conservative: input.conservative
|
|
15106
15554
|
};
|
|
15107
|
-
const guardResult = guard(db, memInput);
|
|
15555
|
+
const guardResult = await guard(db, memInput);
|
|
15108
15556
|
switch (guardResult.action) {
|
|
15109
15557
|
case "skip":
|
|
15110
15558
|
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
15111
15559
|
case "add": {
|
|
15112
15560
|
const mem = createMemory(db, memInput);
|
|
15113
15561
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
15114
|
-
|
|
15562
|
+
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
15563
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
15564
|
+
}
|
|
15565
|
+
case "update": {
|
|
15566
|
+
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
15567
|
+
if (guardResult.updatedContent !== void 0) {
|
|
15568
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
15569
|
+
}
|
|
15570
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15571
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
15572
|
+
}
|
|
15573
|
+
case "merge": {
|
|
15574
|
+
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
15575
|
+
return { action: "skipped", reason: "Missing merge data" };
|
|
15576
|
+
}
|
|
15577
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
15578
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15579
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
15580
|
+
}
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
|
|
15584
|
+
// src/ingest/ingest.ts
|
|
15585
|
+
function slugify2(input) {
|
|
15586
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
|
|
15587
|
+
}
|
|
15588
|
+
function classifyIngestType(title) {
|
|
15589
|
+
const normalized = title.toLowerCase();
|
|
15590
|
+
if (normalized.includes("\u60C5\u611F") || normalized.includes("emotion")) return "emotion";
|
|
15591
|
+
if (normalized.includes("\u4E8B\u4EF6") || normalized.includes("event") || normalized.includes("journal")) return "event";
|
|
15592
|
+
if (normalized.includes("\u8EAB\u4EFD") || normalized.includes("identity") || normalized.includes("about")) return "identity";
|
|
15593
|
+
return "knowledge";
|
|
15594
|
+
}
|
|
15595
|
+
function splitIngestBlocks(text) {
|
|
15596
|
+
const sections = text.split(/^##\s+/m).filter((section) => section.trim());
|
|
15597
|
+
if (sections.length === 0) {
|
|
15598
|
+
return [{ title: "knowledge", body: text.trim() }];
|
|
15599
|
+
}
|
|
15600
|
+
return sections.map((section) => {
|
|
15601
|
+
const [titleLine, ...rest] = section.split("\n");
|
|
15602
|
+
return {
|
|
15603
|
+
title: titleLine?.trim() || "knowledge",
|
|
15604
|
+
body: rest.join("\n").trim()
|
|
15605
|
+
};
|
|
15606
|
+
}).filter((section) => section.body.length > 0);
|
|
15607
|
+
}
|
|
15608
|
+
function extractIngestItems(text, source) {
|
|
15609
|
+
const blocks = splitIngestBlocks(text);
|
|
15610
|
+
const items = [];
|
|
15611
|
+
let index = 0;
|
|
15612
|
+
for (const block of blocks) {
|
|
15613
|
+
const type = classifyIngestType(block.title);
|
|
15614
|
+
const bulletItems = block.body.split(/\n+/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
15615
|
+
const lines = bulletItems.length > 0 ? bulletItems : [block.body.trim()];
|
|
15616
|
+
for (const line of lines) {
|
|
15617
|
+
index += 1;
|
|
15618
|
+
const uri = type === "identity" ? `core://ingest/${slugify2(block.title)}/${index}` : `${type}://${slugify2(source ?? "ingest")}/${index}`;
|
|
15619
|
+
items.push({
|
|
15620
|
+
index,
|
|
15621
|
+
type,
|
|
15622
|
+
uri,
|
|
15623
|
+
content: line
|
|
15624
|
+
});
|
|
15625
|
+
}
|
|
15626
|
+
}
|
|
15627
|
+
return items;
|
|
15628
|
+
}
|
|
15629
|
+
async function ingestText(db, options) {
|
|
15630
|
+
const extracted = extractIngestItems(options.text, options.source);
|
|
15631
|
+
const dryRun = options.dryRun ?? false;
|
|
15632
|
+
const agentId = options.agentId ?? "default";
|
|
15633
|
+
if (dryRun) {
|
|
15634
|
+
return {
|
|
15635
|
+
extracted: extracted.length,
|
|
15636
|
+
written: 0,
|
|
15637
|
+
skipped: extracted.length,
|
|
15638
|
+
dry_run: true,
|
|
15639
|
+
details: extracted.map((item) => ({
|
|
15640
|
+
index: item.index,
|
|
15641
|
+
type: item.type,
|
|
15642
|
+
uri: item.uri,
|
|
15643
|
+
preview: item.content.slice(0, 80)
|
|
15644
|
+
}))
|
|
15645
|
+
};
|
|
15646
|
+
}
|
|
15647
|
+
let written = 0;
|
|
15648
|
+
let skipped = 0;
|
|
15649
|
+
const details = [];
|
|
15650
|
+
for (const item of extracted) {
|
|
15651
|
+
const result = await syncOne(db, {
|
|
15652
|
+
content: item.content,
|
|
15653
|
+
type: item.type,
|
|
15654
|
+
uri: item.uri,
|
|
15655
|
+
source: `auto:${options.source ?? "ingest"}`,
|
|
15656
|
+
agent_id: agentId
|
|
15657
|
+
});
|
|
15658
|
+
if (result.action === "added" || result.action === "updated" || result.action === "merged") {
|
|
15659
|
+
written += 1;
|
|
15660
|
+
} else {
|
|
15661
|
+
skipped += 1;
|
|
15662
|
+
}
|
|
15663
|
+
details.push({
|
|
15664
|
+
index: item.index,
|
|
15665
|
+
type: item.type,
|
|
15666
|
+
uri: item.uri,
|
|
15667
|
+
action: result.action,
|
|
15668
|
+
reason: result.reason,
|
|
15669
|
+
memoryId: result.memoryId
|
|
15670
|
+
});
|
|
15671
|
+
}
|
|
15672
|
+
return {
|
|
15673
|
+
extracted: extracted.length,
|
|
15674
|
+
written,
|
|
15675
|
+
skipped,
|
|
15676
|
+
dry_run: false,
|
|
15677
|
+
details
|
|
15678
|
+
};
|
|
15679
|
+
}
|
|
15680
|
+
|
|
15681
|
+
// src/ingest/watcher.ts
|
|
15682
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync, statSync, watch } from "fs";
|
|
15683
|
+
import { join, relative, resolve } from "path";
|
|
15684
|
+
function runAutoIngestWatcher(options) {
|
|
15685
|
+
const workspaceDir = resolve(options.workspaceDir);
|
|
15686
|
+
const memoryDir = join(workspaceDir, "memory");
|
|
15687
|
+
const memoryMdPath = join(workspaceDir, "MEMORY.md");
|
|
15688
|
+
const debounceMs = options.debounceMs ?? 1200;
|
|
15689
|
+
const initialScan = options.initialScan ?? true;
|
|
15690
|
+
const logger = options.logger ?? console;
|
|
15691
|
+
const timers = /* @__PURE__ */ new Map();
|
|
15692
|
+
const watchers = [];
|
|
15693
|
+
const stats = {
|
|
15694
|
+
triggers: 0,
|
|
15695
|
+
filesProcessed: 0,
|
|
15696
|
+
extracted: 0,
|
|
15697
|
+
written: 0,
|
|
15698
|
+
skipped: 0,
|
|
15699
|
+
errors: 0
|
|
15700
|
+
};
|
|
15701
|
+
let stopped = false;
|
|
15702
|
+
let queue = Promise.resolve();
|
|
15703
|
+
const toSource = (absPath) => {
|
|
15704
|
+
const rel = relative(workspaceDir, absPath).replace(/\\/g, "/");
|
|
15705
|
+
return rel || absPath;
|
|
15706
|
+
};
|
|
15707
|
+
const isTrackedMarkdownFile = (absPath) => {
|
|
15708
|
+
if (!absPath.endsWith(".md")) return false;
|
|
15709
|
+
if (resolve(absPath) === memoryMdPath) return true;
|
|
15710
|
+
const rel = relative(memoryDir, absPath).replace(/\\/g, "/");
|
|
15711
|
+
if (rel.startsWith("..") || rel === "") return false;
|
|
15712
|
+
return !rel.includes("/");
|
|
15713
|
+
};
|
|
15714
|
+
const ingestFile = async (absPath, reason) => {
|
|
15715
|
+
if (stopped) return;
|
|
15716
|
+
if (!existsSync(absPath)) {
|
|
15717
|
+
logger.log(`[auto-ingest] skip missing file: ${toSource(absPath)} (reason=${reason})`);
|
|
15718
|
+
return;
|
|
15719
|
+
}
|
|
15720
|
+
let isFile = false;
|
|
15721
|
+
try {
|
|
15722
|
+
isFile = statSync(absPath).isFile();
|
|
15723
|
+
} catch (err) {
|
|
15724
|
+
stats.errors += 1;
|
|
15725
|
+
logger.warn(`[auto-ingest] stat failed for ${toSource(absPath)}: ${String(err)}`);
|
|
15726
|
+
return;
|
|
15727
|
+
}
|
|
15728
|
+
if (!isFile) return;
|
|
15729
|
+
try {
|
|
15730
|
+
const text = readFileSync2(absPath, "utf-8");
|
|
15731
|
+
const source = toSource(absPath);
|
|
15732
|
+
const result = await ingestText(options.db, {
|
|
15733
|
+
text,
|
|
15734
|
+
source,
|
|
15735
|
+
agentId: options.agentId
|
|
15736
|
+
});
|
|
15737
|
+
stats.filesProcessed += 1;
|
|
15738
|
+
stats.extracted += result.extracted;
|
|
15739
|
+
stats.written += result.written;
|
|
15740
|
+
stats.skipped += result.skipped;
|
|
15741
|
+
logger.log(
|
|
15742
|
+
`[auto-ingest] file=${source} reason=${reason} extracted=${result.extracted} written=${result.written} skipped=${result.skipped}`
|
|
15743
|
+
);
|
|
15744
|
+
} catch (err) {
|
|
15745
|
+
stats.errors += 1;
|
|
15746
|
+
logger.error(`[auto-ingest] ingest failed for ${toSource(absPath)}: ${String(err)}`);
|
|
15747
|
+
}
|
|
15748
|
+
};
|
|
15749
|
+
const scheduleIngest = (absPath, reason) => {
|
|
15750
|
+
if (stopped) return;
|
|
15751
|
+
if (!isTrackedMarkdownFile(absPath)) return;
|
|
15752
|
+
stats.triggers += 1;
|
|
15753
|
+
const previous = timers.get(absPath);
|
|
15754
|
+
if (previous) clearTimeout(previous);
|
|
15755
|
+
const timer = setTimeout(() => {
|
|
15756
|
+
timers.delete(absPath);
|
|
15757
|
+
queue = queue.then(() => ingestFile(absPath, reason)).catch((err) => {
|
|
15758
|
+
stats.errors += 1;
|
|
15759
|
+
logger.error(`[auto-ingest] queue error: ${String(err)}`);
|
|
15760
|
+
});
|
|
15761
|
+
}, debounceMs);
|
|
15762
|
+
timers.set(absPath, timer);
|
|
15763
|
+
};
|
|
15764
|
+
const safeWatch = (dir, onEvent) => {
|
|
15765
|
+
if (!existsSync(dir)) {
|
|
15766
|
+
logger.warn(`[auto-ingest] watch path does not exist, skipping: ${dir}`);
|
|
15767
|
+
return;
|
|
15768
|
+
}
|
|
15769
|
+
try {
|
|
15770
|
+
const watcher = watch(dir, { persistent: true }, (eventType, filename) => {
|
|
15771
|
+
if (!filename) return;
|
|
15772
|
+
onEvent(eventType, filename.toString());
|
|
15773
|
+
});
|
|
15774
|
+
watchers.push(watcher);
|
|
15775
|
+
logger.log(`[auto-ingest] watching ${dir}`);
|
|
15776
|
+
} catch (err) {
|
|
15777
|
+
stats.errors += 1;
|
|
15778
|
+
logger.error(`[auto-ingest] failed to watch ${dir}: ${String(err)}`);
|
|
15779
|
+
}
|
|
15780
|
+
};
|
|
15781
|
+
safeWatch(workspaceDir, (eventType, filename) => {
|
|
15782
|
+
if (filename === "MEMORY.md") {
|
|
15783
|
+
scheduleIngest(join(workspaceDir, filename), `workspace:${eventType}`);
|
|
15784
|
+
}
|
|
15785
|
+
});
|
|
15786
|
+
safeWatch(memoryDir, (eventType, filename) => {
|
|
15787
|
+
if (filename.endsWith(".md")) {
|
|
15788
|
+
scheduleIngest(join(memoryDir, filename), `memory:${eventType}`);
|
|
15789
|
+
}
|
|
15790
|
+
});
|
|
15791
|
+
if (initialScan) {
|
|
15792
|
+
scheduleIngest(memoryMdPath, "initial");
|
|
15793
|
+
if (existsSync(memoryDir)) {
|
|
15794
|
+
for (const file2 of readdirSync(memoryDir)) {
|
|
15795
|
+
if (file2.endsWith(".md")) {
|
|
15796
|
+
scheduleIngest(join(memoryDir, file2), "initial");
|
|
15797
|
+
}
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15800
|
+
}
|
|
15801
|
+
return {
|
|
15802
|
+
close: () => {
|
|
15803
|
+
if (stopped) return;
|
|
15804
|
+
stopped = true;
|
|
15805
|
+
for (const timer of timers.values()) {
|
|
15806
|
+
clearTimeout(timer);
|
|
15807
|
+
}
|
|
15808
|
+
timers.clear();
|
|
15809
|
+
for (const watcher of watchers) {
|
|
15115
15810
|
try {
|
|
15116
|
-
|
|
15811
|
+
watcher.close();
|
|
15117
15812
|
} catch {
|
|
15118
15813
|
}
|
|
15119
15814
|
}
|
|
15120
|
-
|
|
15815
|
+
logger.log(
|
|
15816
|
+
`[auto-ingest] stopped triggers=${stats.triggers} files=${stats.filesProcessed} extracted=${stats.extracted} written=${stats.written} skipped=${stats.skipped} errors=${stats.errors}`
|
|
15817
|
+
);
|
|
15818
|
+
}
|
|
15819
|
+
};
|
|
15820
|
+
}
|
|
15821
|
+
|
|
15822
|
+
// src/app/remember.ts
|
|
15823
|
+
async function rememberMemory(db, input) {
|
|
15824
|
+
return syncOne(db, {
|
|
15825
|
+
content: input.content,
|
|
15826
|
+
type: input.type,
|
|
15827
|
+
priority: input.priority,
|
|
15828
|
+
emotion_val: input.emotion_val,
|
|
15829
|
+
uri: input.uri,
|
|
15830
|
+
source: input.source,
|
|
15831
|
+
agent_id: input.agent_id,
|
|
15832
|
+
provider: input.provider,
|
|
15833
|
+
conservative: input.conservative
|
|
15834
|
+
});
|
|
15835
|
+
}
|
|
15836
|
+
|
|
15837
|
+
// src/app/recall.ts
|
|
15838
|
+
async function recallMemory(db, input) {
|
|
15839
|
+
return recallMemories(db, input.query, {
|
|
15840
|
+
agent_id: input.agent_id,
|
|
15841
|
+
limit: input.limit,
|
|
15842
|
+
min_vitality: input.min_vitality,
|
|
15843
|
+
lexicalLimit: input.lexicalLimit,
|
|
15844
|
+
vectorLimit: input.vectorLimit,
|
|
15845
|
+
provider: input.provider,
|
|
15846
|
+
recordAccess: input.recordAccess
|
|
15847
|
+
});
|
|
15848
|
+
}
|
|
15849
|
+
|
|
15850
|
+
// src/app/surface.ts
|
|
15851
|
+
init_memory();
|
|
15852
|
+
init_providers();
|
|
15853
|
+
init_tokenizer();
|
|
15854
|
+
init_vector();
|
|
15855
|
+
|
|
15856
|
+
// src/app/feedback.ts
|
|
15857
|
+
init_db();
|
|
15858
|
+
function clamp012(value) {
|
|
15859
|
+
if (!Number.isFinite(value)) return 0;
|
|
15860
|
+
return Math.max(0, Math.min(1, value));
|
|
15861
|
+
}
|
|
15862
|
+
function getFeedbackSummary(db, memoryId, agentId) {
|
|
15863
|
+
try {
|
|
15864
|
+
const row = db.prepare(
|
|
15865
|
+
`SELECT COUNT(*) as total,
|
|
15866
|
+
COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
|
|
15867
|
+
COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
|
|
15868
|
+
FROM feedback_events
|
|
15869
|
+
WHERE memory_id = ?
|
|
15870
|
+
AND (? IS NULL OR agent_id = ?)`
|
|
15871
|
+
).get(memoryId, agentId ?? null, agentId ?? null);
|
|
15872
|
+
if (!row || row.total === 0) {
|
|
15873
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
15874
|
+
}
|
|
15875
|
+
return {
|
|
15876
|
+
total: row.total,
|
|
15877
|
+
useful: row.useful,
|
|
15878
|
+
not_useful: row.not_useful,
|
|
15879
|
+
score: clamp012(row.useful / row.total)
|
|
15880
|
+
};
|
|
15881
|
+
} catch {
|
|
15882
|
+
const row = db.prepare(
|
|
15883
|
+
`SELECT COUNT(*) as total,
|
|
15884
|
+
COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
|
|
15885
|
+
COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
|
|
15886
|
+
COALESCE(AVG(value), 0.5) as avg_value
|
|
15887
|
+
FROM feedback_events
|
|
15888
|
+
WHERE memory_id = ?`
|
|
15889
|
+
).get(memoryId);
|
|
15890
|
+
if (!row || row.total === 0) {
|
|
15891
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
15892
|
+
}
|
|
15893
|
+
return {
|
|
15894
|
+
total: row.total,
|
|
15895
|
+
useful: row.useful,
|
|
15896
|
+
not_useful: row.not_useful,
|
|
15897
|
+
score: clamp012(row.avg_value)
|
|
15898
|
+
};
|
|
15899
|
+
}
|
|
15900
|
+
}
|
|
15901
|
+
|
|
15902
|
+
// src/app/surface.ts
|
|
15903
|
+
var INTENT_PRIORS = {
|
|
15904
|
+
factual: {
|
|
15905
|
+
identity: 0.25,
|
|
15906
|
+
emotion: 0.15,
|
|
15907
|
+
knowledge: 1,
|
|
15908
|
+
event: 0.8
|
|
15909
|
+
},
|
|
15910
|
+
preference: {
|
|
15911
|
+
identity: 1,
|
|
15912
|
+
emotion: 0.85,
|
|
15913
|
+
knowledge: 0.55,
|
|
15914
|
+
event: 0.25
|
|
15915
|
+
},
|
|
15916
|
+
temporal: {
|
|
15917
|
+
identity: 0.15,
|
|
15918
|
+
emotion: 0.35,
|
|
15919
|
+
knowledge: 0.5,
|
|
15920
|
+
event: 1
|
|
15921
|
+
},
|
|
15922
|
+
planning: {
|
|
15923
|
+
identity: 0.65,
|
|
15924
|
+
emotion: 0.2,
|
|
15925
|
+
knowledge: 1,
|
|
15926
|
+
event: 0.6
|
|
15927
|
+
},
|
|
15928
|
+
design: {
|
|
15929
|
+
identity: 0.8,
|
|
15930
|
+
emotion: 0.35,
|
|
15931
|
+
knowledge: 1,
|
|
15932
|
+
event: 0.25
|
|
15933
|
+
}
|
|
15934
|
+
};
|
|
15935
|
+
var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
|
|
15936
|
+
var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
|
|
15937
|
+
var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
|
|
15938
|
+
var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
|
|
15939
|
+
var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
|
|
15940
|
+
function clamp013(value) {
|
|
15941
|
+
if (!Number.isFinite(value)) return 0;
|
|
15942
|
+
return Math.max(0, Math.min(1, value));
|
|
15943
|
+
}
|
|
15944
|
+
function uniqueTokenSet2(values) {
|
|
15945
|
+
return new Set(
|
|
15946
|
+
values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
|
|
15947
|
+
);
|
|
15948
|
+
}
|
|
15949
|
+
function overlapScore2(left, right) {
|
|
15950
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
15951
|
+
let shared = 0;
|
|
15952
|
+
for (const token of left) {
|
|
15953
|
+
if (right.has(token)) shared += 1;
|
|
15954
|
+
}
|
|
15955
|
+
return clamp013(shared / Math.max(left.size, right.size));
|
|
15956
|
+
}
|
|
15957
|
+
function rankScore(rank, window) {
|
|
15958
|
+
if (!rank) return 0;
|
|
15959
|
+
return clamp013(1 - (rank - 1) / Math.max(window, 1));
|
|
15960
|
+
}
|
|
15961
|
+
function topicLabel(...parts) {
|
|
15962
|
+
const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
|
|
15963
|
+
const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
15964
|
+
return label || "context";
|
|
15965
|
+
}
|
|
15966
|
+
function intentKeywordBoost(memory, intent) {
|
|
15967
|
+
const content = memory.content;
|
|
15968
|
+
switch (intent) {
|
|
15969
|
+
case "design":
|
|
15970
|
+
return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
|
|
15971
|
+
case "planning":
|
|
15972
|
+
return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
|
|
15973
|
+
case "factual":
|
|
15974
|
+
return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
15975
|
+
case "temporal":
|
|
15976
|
+
return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
15977
|
+
case "preference":
|
|
15978
|
+
return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
|
|
15979
|
+
}
|
|
15980
|
+
}
|
|
15981
|
+
function intentMatch(memory, intent) {
|
|
15982
|
+
if (!intent) return 0;
|
|
15983
|
+
const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
|
|
15984
|
+
return clamp013(prior * intentKeywordBoost(memory, intent));
|
|
15985
|
+
}
|
|
15986
|
+
function buildReasonCodes(input) {
|
|
15987
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
15988
|
+
reasons.add(`type:${input.memory.type}`);
|
|
15989
|
+
if (input.semanticScore > 0.2) {
|
|
15990
|
+
reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
|
|
15991
|
+
}
|
|
15992
|
+
if (input.lexicalScore > 0.2 && input.query) {
|
|
15993
|
+
reasons.add(`lexical:${topicLabel(input.query)}`);
|
|
15994
|
+
}
|
|
15995
|
+
if (input.taskMatch > 0.2) {
|
|
15996
|
+
reasons.add(`task:${topicLabel(input.task, input.intent)}`);
|
|
15997
|
+
}
|
|
15998
|
+
if (input.intent) {
|
|
15999
|
+
reasons.add(`intent:${input.intent}`);
|
|
16000
|
+
}
|
|
16001
|
+
if (input.feedbackScore >= 0.67) {
|
|
16002
|
+
reasons.add("feedback:reinforced");
|
|
16003
|
+
} else if (input.feedbackScore <= 0.33) {
|
|
16004
|
+
reasons.add("feedback:negative");
|
|
16005
|
+
}
|
|
16006
|
+
return [...reasons];
|
|
16007
|
+
}
|
|
16008
|
+
function collectBranch(signals, rows, key, similarity) {
|
|
16009
|
+
for (const row of rows) {
|
|
16010
|
+
const existing = signals.get(row.memory.id) ?? { memory: row.memory };
|
|
16011
|
+
const currentRank = existing[key];
|
|
16012
|
+
if (currentRank === void 0 || row.rank < currentRank) {
|
|
16013
|
+
existing[key] = row.rank;
|
|
15121
16014
|
}
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
16015
|
+
if (similarity) {
|
|
16016
|
+
const currentSimilarity = similarity.get(row.memory.id);
|
|
16017
|
+
if (currentSimilarity !== void 0) {
|
|
16018
|
+
existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
|
|
16019
|
+
}
|
|
15127
16020
|
}
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
16021
|
+
signals.set(row.memory.id, existing);
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
16024
|
+
async function surfaceMemories(db, input) {
|
|
16025
|
+
const agentId = input.agent_id ?? "default";
|
|
16026
|
+
const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
|
|
16027
|
+
const lexicalWindow = Math.max(24, limit * 6);
|
|
16028
|
+
const minVitality = input.min_vitality ?? 0.05;
|
|
16029
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
16030
|
+
const signals = /* @__PURE__ */ new Map();
|
|
16031
|
+
const trimmedQuery = input.query?.trim();
|
|
16032
|
+
const trimmedTask = input.task?.trim();
|
|
16033
|
+
const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
|
|
16034
|
+
const queryTokens = uniqueTokenSet2([trimmedQuery, ...recentTurns]);
|
|
16035
|
+
const taskTokens = uniqueTokenSet2([trimmedTask]);
|
|
16036
|
+
if (trimmedQuery) {
|
|
16037
|
+
collectBranch(
|
|
16038
|
+
signals,
|
|
16039
|
+
searchBM25(db, trimmedQuery, {
|
|
16040
|
+
agent_id: agentId,
|
|
16041
|
+
limit: lexicalWindow,
|
|
16042
|
+
min_vitality: minVitality
|
|
16043
|
+
}),
|
|
16044
|
+
"queryRank"
|
|
16045
|
+
);
|
|
16046
|
+
}
|
|
16047
|
+
if (trimmedTask) {
|
|
16048
|
+
collectBranch(
|
|
16049
|
+
signals,
|
|
16050
|
+
searchBM25(db, trimmedTask, {
|
|
16051
|
+
agent_id: agentId,
|
|
16052
|
+
limit: lexicalWindow,
|
|
16053
|
+
min_vitality: minVitality
|
|
16054
|
+
}),
|
|
16055
|
+
"taskRank"
|
|
16056
|
+
);
|
|
16057
|
+
}
|
|
16058
|
+
if (recentTurns.length > 0) {
|
|
16059
|
+
collectBranch(
|
|
16060
|
+
signals,
|
|
16061
|
+
searchBM25(db, recentTurns.join(" "), {
|
|
16062
|
+
agent_id: agentId,
|
|
16063
|
+
limit: lexicalWindow,
|
|
16064
|
+
min_vitality: minVitality
|
|
16065
|
+
}),
|
|
16066
|
+
"recentRank"
|
|
16067
|
+
);
|
|
16068
|
+
}
|
|
16069
|
+
const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
|
|
16070
|
+
if (provider && semanticQuery) {
|
|
16071
|
+
try {
|
|
16072
|
+
const [queryVector] = await provider.embed([semanticQuery]);
|
|
16073
|
+
if (queryVector) {
|
|
16074
|
+
const vectorRows = searchByVector(db, queryVector, {
|
|
16075
|
+
providerId: provider.id,
|
|
16076
|
+
agent_id: agentId,
|
|
16077
|
+
limit: lexicalWindow,
|
|
16078
|
+
min_vitality: minVitality
|
|
16079
|
+
});
|
|
16080
|
+
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
16081
|
+
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
15131
16082
|
}
|
|
15132
|
-
|
|
15133
|
-
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
15134
|
-
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
16083
|
+
} catch {
|
|
15135
16084
|
}
|
|
15136
16085
|
}
|
|
16086
|
+
const fallbackMemories = listMemories(db, {
|
|
16087
|
+
agent_id: agentId,
|
|
16088
|
+
min_vitality: minVitality,
|
|
16089
|
+
limit: Math.max(48, lexicalWindow)
|
|
16090
|
+
});
|
|
16091
|
+
for (const memory of fallbackMemories) {
|
|
16092
|
+
if (!signals.has(memory.id)) {
|
|
16093
|
+
signals.set(memory.id, { memory });
|
|
16094
|
+
}
|
|
16095
|
+
}
|
|
16096
|
+
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) => {
|
|
16097
|
+
const signal = signals.get(memory.id) ?? { memory };
|
|
16098
|
+
const memoryTokens = new Set(tokenize(memory.content));
|
|
16099
|
+
const lexicalOverlap = overlapScore2(memoryTokens, queryTokens);
|
|
16100
|
+
const taskOverlap = overlapScore2(memoryTokens, taskTokens);
|
|
16101
|
+
const lexicalScore = clamp013(
|
|
16102
|
+
0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
|
|
16103
|
+
);
|
|
16104
|
+
const semanticScore = signal.semanticSimilarity !== void 0 ? clamp013(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp013(lexicalOverlap * 0.7) : 0;
|
|
16105
|
+
const intentScore = intentMatch(memory, input.intent);
|
|
16106
|
+
const taskMatch = trimmedTask ? clamp013(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
|
|
16107
|
+
const priorityScore = priorityPrior(memory.priority);
|
|
16108
|
+
const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
|
|
16109
|
+
const feedbackScore = feedbackSummary.score;
|
|
16110
|
+
const score = clamp013(
|
|
16111
|
+
0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
|
|
16112
|
+
);
|
|
16113
|
+
return {
|
|
16114
|
+
memory,
|
|
16115
|
+
score,
|
|
16116
|
+
semantic_score: semanticScore,
|
|
16117
|
+
lexical_score: lexicalScore,
|
|
16118
|
+
task_match: taskMatch,
|
|
16119
|
+
vitality: memory.vitality,
|
|
16120
|
+
priority_prior: priorityScore,
|
|
16121
|
+
feedback_score: feedbackScore,
|
|
16122
|
+
feedback_summary: feedbackSummary,
|
|
16123
|
+
reason_codes: buildReasonCodes({
|
|
16124
|
+
memory,
|
|
16125
|
+
query: semanticQuery || trimmedQuery,
|
|
16126
|
+
task: trimmedTask,
|
|
16127
|
+
intent: input.intent,
|
|
16128
|
+
semanticScore,
|
|
16129
|
+
lexicalScore,
|
|
16130
|
+
taskMatch,
|
|
16131
|
+
feedbackScore
|
|
16132
|
+
}),
|
|
16133
|
+
lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
|
|
16134
|
+
semantic_rank: signal.semanticRank,
|
|
16135
|
+
semantic_similarity: signal.semanticSimilarity
|
|
16136
|
+
};
|
|
16137
|
+
}).sort((left, right) => {
|
|
16138
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
16139
|
+
if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
|
|
16140
|
+
if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
|
|
16141
|
+
if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
|
|
16142
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
16143
|
+
}).slice(0, limit);
|
|
16144
|
+
return {
|
|
16145
|
+
count: results.length,
|
|
16146
|
+
query: trimmedQuery,
|
|
16147
|
+
task: trimmedTask,
|
|
16148
|
+
intent: input.intent,
|
|
16149
|
+
results
|
|
16150
|
+
};
|
|
15137
16151
|
}
|
|
15138
16152
|
|
|
15139
16153
|
// src/sleep/decay.ts
|
|
@@ -15201,59 +16215,99 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
15201
16215
|
init_memory();
|
|
15202
16216
|
function runTidy(db, opts) {
|
|
15203
16217
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
15204
|
-
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
15205
16218
|
const agentId = opts?.agent_id;
|
|
15206
16219
|
let archived = 0;
|
|
15207
|
-
let orphansCleaned = 0;
|
|
15208
|
-
let snapshotsPruned = 0;
|
|
15209
16220
|
const transaction = db.transaction(() => {
|
|
15210
16221
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
15211
16222
|
for (const mem of decayed) {
|
|
15212
|
-
try {
|
|
15213
|
-
createSnapshot(db, mem.id, "delete", "tidy");
|
|
15214
|
-
} catch {
|
|
15215
|
-
}
|
|
15216
16223
|
deleteMemory(db, mem.id);
|
|
15217
|
-
archived
|
|
15218
|
-
}
|
|
15219
|
-
const orphans = agentId ? db.prepare(
|
|
15220
|
-
`DELETE FROM paths
|
|
15221
|
-
WHERE agent_id = ?
|
|
15222
|
-
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
15223
|
-
).run(agentId, agentId) : db.prepare(
|
|
15224
|
-
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
15225
|
-
).run();
|
|
15226
|
-
orphansCleaned = orphans.changes;
|
|
15227
|
-
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
15228
|
-
`SELECT s.memory_id, COUNT(*) as cnt
|
|
15229
|
-
FROM snapshots s
|
|
15230
|
-
JOIN memories m ON m.id = s.memory_id
|
|
15231
|
-
WHERE m.agent_id = ?
|
|
15232
|
-
GROUP BY s.memory_id HAVING cnt > ?`
|
|
15233
|
-
).all(agentId, maxSnapshots) : db.prepare(
|
|
15234
|
-
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
15235
|
-
GROUP BY memory_id HAVING cnt > ?`
|
|
15236
|
-
).all(maxSnapshots);
|
|
15237
|
-
for (const { memory_id } of memoriesWithSnapshots) {
|
|
15238
|
-
const pruned = db.prepare(
|
|
15239
|
-
`DELETE FROM snapshots WHERE id NOT IN (
|
|
15240
|
-
SELECT id FROM snapshots WHERE memory_id = ?
|
|
15241
|
-
ORDER BY created_at DESC LIMIT ?
|
|
15242
|
-
) AND memory_id = ?`
|
|
15243
|
-
).run(memory_id, maxSnapshots, memory_id);
|
|
15244
|
-
snapshotsPruned += pruned.changes;
|
|
16224
|
+
archived += 1;
|
|
15245
16225
|
}
|
|
15246
16226
|
});
|
|
15247
16227
|
transaction();
|
|
15248
|
-
return { archived, orphansCleaned
|
|
16228
|
+
return { archived, orphansCleaned: 0 };
|
|
15249
16229
|
}
|
|
15250
16230
|
|
|
15251
16231
|
// src/sleep/govern.ts
|
|
16232
|
+
init_memory();
|
|
16233
|
+
init_tokenizer();
|
|
16234
|
+
function clamp014(value) {
|
|
16235
|
+
if (!Number.isFinite(value)) return 0;
|
|
16236
|
+
return Math.max(0, Math.min(1, value));
|
|
16237
|
+
}
|
|
16238
|
+
function overlapScore3(left, right) {
|
|
16239
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
16240
|
+
let shared = 0;
|
|
16241
|
+
for (const token of left) {
|
|
16242
|
+
if (right.has(token)) shared += 1;
|
|
16243
|
+
}
|
|
16244
|
+
return shared / Math.max(left.size, right.size);
|
|
16245
|
+
}
|
|
16246
|
+
function feedbackPenalty(db, memoryId) {
|
|
16247
|
+
try {
|
|
16248
|
+
const row = db.prepare(
|
|
16249
|
+
`SELECT COUNT(*) as count, COALESCE(AVG(value), 0) as avgValue
|
|
16250
|
+
FROM feedback_events
|
|
16251
|
+
WHERE memory_id = ?`
|
|
16252
|
+
).get(memoryId);
|
|
16253
|
+
if (!row || row.count === 0) return 1;
|
|
16254
|
+
return clamp014(1 - row.avgValue);
|
|
16255
|
+
} catch {
|
|
16256
|
+
return 1;
|
|
16257
|
+
}
|
|
16258
|
+
}
|
|
16259
|
+
function ageScore(memory, referenceMs = Date.now()) {
|
|
16260
|
+
const createdAt = new Date(memory.created_at).getTime();
|
|
16261
|
+
if (Number.isNaN(createdAt)) return 0;
|
|
16262
|
+
const ageDays = Math.max(0, (referenceMs - createdAt) / (1e3 * 60 * 60 * 24));
|
|
16263
|
+
return clamp014(ageDays / 180);
|
|
16264
|
+
}
|
|
16265
|
+
function computeEvictionScore(input) {
|
|
16266
|
+
return clamp014(
|
|
16267
|
+
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)
|
|
16268
|
+
);
|
|
16269
|
+
}
|
|
16270
|
+
function rankEvictionCandidates(db, opts) {
|
|
16271
|
+
const agentId = opts?.agent_id;
|
|
16272
|
+
const rows = db.prepare(
|
|
16273
|
+
agentId ? `SELECT * FROM memories WHERE agent_id = ? AND priority > 0 AND TRIM(content) != ''` : `SELECT * FROM memories WHERE priority > 0 AND TRIM(content) != ''`
|
|
16274
|
+
).all(...agentId ? [agentId] : []);
|
|
16275
|
+
const tokenSets = new Map(rows.map((memory) => [memory.id, new Set(tokenize(memory.content))]));
|
|
16276
|
+
return rows.map((memory) => {
|
|
16277
|
+
const ownTokens = tokenSets.get(memory.id) ?? /* @__PURE__ */ new Set();
|
|
16278
|
+
const redundancy = rows.filter((candidate2) => candidate2.id !== memory.id && candidate2.type === memory.type).reduce((maxOverlap, candidate2) => {
|
|
16279
|
+
const candidateTokens = tokenSets.get(candidate2.id) ?? /* @__PURE__ */ new Set();
|
|
16280
|
+
return Math.max(maxOverlap, overlapScore3(ownTokens, candidateTokens));
|
|
16281
|
+
}, 0);
|
|
16282
|
+
const candidate = {
|
|
16283
|
+
memory,
|
|
16284
|
+
redundancy_score: redundancy,
|
|
16285
|
+
age_score: ageScore(memory),
|
|
16286
|
+
low_feedback_penalty: feedbackPenalty(db, memory.id),
|
|
16287
|
+
low_priority_penalty: clamp014(memory.priority / 3),
|
|
16288
|
+
eviction_score: 0
|
|
16289
|
+
};
|
|
16290
|
+
candidate.eviction_score = computeEvictionScore({
|
|
16291
|
+
vitality: memory.vitality,
|
|
16292
|
+
redundancy_score: candidate.redundancy_score,
|
|
16293
|
+
age_score: candidate.age_score,
|
|
16294
|
+
low_feedback_penalty: candidate.low_feedback_penalty,
|
|
16295
|
+
low_priority_penalty: candidate.low_priority_penalty
|
|
16296
|
+
});
|
|
16297
|
+
return candidate;
|
|
16298
|
+
}).sort((left, right) => {
|
|
16299
|
+
if (right.eviction_score !== left.eviction_score) {
|
|
16300
|
+
return right.eviction_score - left.eviction_score;
|
|
16301
|
+
}
|
|
16302
|
+
return left.memory.priority - right.memory.priority;
|
|
16303
|
+
});
|
|
16304
|
+
}
|
|
15252
16305
|
function runGovern(db, opts) {
|
|
15253
16306
|
const agentId = opts?.agent_id;
|
|
16307
|
+
const maxMemories = opts?.maxMemories ?? 200;
|
|
15254
16308
|
let orphanPaths = 0;
|
|
15255
|
-
let orphanLinks = 0;
|
|
15256
16309
|
let emptyMemories = 0;
|
|
16310
|
+
let evicted = 0;
|
|
15257
16311
|
const transaction = db.transaction(() => {
|
|
15258
16312
|
const pathResult = agentId ? db.prepare(
|
|
15259
16313
|
`DELETE FROM paths
|
|
@@ -15261,190 +16315,509 @@ function runGovern(db, opts) {
|
|
|
15261
16315
|
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
15262
16316
|
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
15263
16317
|
orphanPaths = pathResult.changes;
|
|
15264
|
-
const linkResult = agentId ? db.prepare(
|
|
15265
|
-
`DELETE FROM links WHERE
|
|
15266
|
-
agent_id = ? AND (
|
|
15267
|
-
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
15268
|
-
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
15269
|
-
)`
|
|
15270
|
-
).run(agentId, agentId, agentId) : db.prepare(
|
|
15271
|
-
`DELETE FROM links WHERE
|
|
15272
|
-
source_id NOT IN (SELECT id FROM memories) OR
|
|
15273
|
-
target_id NOT IN (SELECT id FROM memories)`
|
|
15274
|
-
).run();
|
|
15275
|
-
orphanLinks = linkResult.changes;
|
|
15276
16318
|
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();
|
|
15277
16319
|
emptyMemories = emptyResult.changes;
|
|
16320
|
+
const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
|
|
16321
|
+
const excess = Math.max(0, total - maxMemories);
|
|
16322
|
+
if (excess <= 0) return;
|
|
16323
|
+
const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
|
|
16324
|
+
for (const candidate of candidates) {
|
|
16325
|
+
deleteMemory(db, candidate.memory.id);
|
|
16326
|
+
evicted += 1;
|
|
16327
|
+
}
|
|
15278
16328
|
});
|
|
15279
16329
|
transaction();
|
|
15280
|
-
return { orphanPaths,
|
|
16330
|
+
return { orphanPaths, emptyMemories, evicted };
|
|
15281
16331
|
}
|
|
15282
16332
|
|
|
15283
|
-
// src/sleep/
|
|
15284
|
-
|
|
15285
|
-
function
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
"core://user/identity"
|
|
15292
|
-
];
|
|
15293
|
-
const memories = /* @__PURE__ */ new Map();
|
|
15294
|
-
const identities = listMemories(db, { agent_id: agentId, priority: 0 });
|
|
15295
|
-
for (const mem of identities) {
|
|
15296
|
-
memories.set(mem.id, mem);
|
|
15297
|
-
recordAccess(db, mem.id, 1.1);
|
|
16333
|
+
// src/sleep/jobs.ts
|
|
16334
|
+
init_db();
|
|
16335
|
+
function parseCheckpoint(raw) {
|
|
16336
|
+
if (!raw) return null;
|
|
16337
|
+
try {
|
|
16338
|
+
return JSON.parse(raw);
|
|
16339
|
+
} catch {
|
|
16340
|
+
return null;
|
|
15298
16341
|
}
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
16342
|
+
}
|
|
16343
|
+
function serializeCheckpoint(checkpoint) {
|
|
16344
|
+
if (!checkpoint) return null;
|
|
16345
|
+
return JSON.stringify(checkpoint);
|
|
16346
|
+
}
|
|
16347
|
+
function toJob(row) {
|
|
16348
|
+
if (!row) return null;
|
|
16349
|
+
return {
|
|
16350
|
+
...row,
|
|
16351
|
+
checkpoint: parseCheckpoint(row.checkpoint)
|
|
16352
|
+
};
|
|
16353
|
+
}
|
|
16354
|
+
function createInitialCheckpoint(phase) {
|
|
16355
|
+
return {
|
|
16356
|
+
requestedPhase: phase,
|
|
16357
|
+
nextPhase: phase === "all" ? "decay" : phase,
|
|
16358
|
+
completedPhases: [],
|
|
16359
|
+
phaseResults: {}
|
|
16360
|
+
};
|
|
16361
|
+
}
|
|
16362
|
+
function createMaintenanceJob(db, phase, checkpoint = createInitialCheckpoint(phase)) {
|
|
16363
|
+
const jobId = newId();
|
|
16364
|
+
const startedAt = now();
|
|
16365
|
+
db.prepare(
|
|
16366
|
+
`INSERT INTO maintenance_jobs (job_id, phase, status, checkpoint, error, started_at, finished_at)
|
|
16367
|
+
VALUES (?, ?, 'running', ?, NULL, ?, NULL)`
|
|
16368
|
+
).run(jobId, phase, serializeCheckpoint(checkpoint), startedAt);
|
|
16369
|
+
return getMaintenanceJob(db, jobId);
|
|
16370
|
+
}
|
|
16371
|
+
function getMaintenanceJob(db, jobId) {
|
|
16372
|
+
const row = db.prepare("SELECT * FROM maintenance_jobs WHERE job_id = ?").get(jobId);
|
|
16373
|
+
return toJob(row);
|
|
16374
|
+
}
|
|
16375
|
+
function findResumableMaintenanceJob(db, phase) {
|
|
16376
|
+
const row = db.prepare(
|
|
16377
|
+
`SELECT *
|
|
16378
|
+
FROM maintenance_jobs
|
|
16379
|
+
WHERE phase = ?
|
|
16380
|
+
AND status IN ('running', 'failed')
|
|
16381
|
+
ORDER BY started_at DESC
|
|
16382
|
+
LIMIT 1`
|
|
16383
|
+
).get(phase);
|
|
16384
|
+
return toJob(row);
|
|
16385
|
+
}
|
|
16386
|
+
function updateMaintenanceCheckpoint(db, jobId, checkpoint) {
|
|
16387
|
+
db.prepare(
|
|
16388
|
+
`UPDATE maintenance_jobs
|
|
16389
|
+
SET checkpoint = ?,
|
|
16390
|
+
error = NULL,
|
|
16391
|
+
finished_at = NULL,
|
|
16392
|
+
status = 'running'
|
|
16393
|
+
WHERE job_id = ?`
|
|
16394
|
+
).run(serializeCheckpoint(checkpoint), jobId);
|
|
16395
|
+
return getMaintenanceJob(db, jobId);
|
|
16396
|
+
}
|
|
16397
|
+
function failMaintenanceJob(db, jobId, error48, checkpoint) {
|
|
16398
|
+
db.prepare(
|
|
16399
|
+
`UPDATE maintenance_jobs
|
|
16400
|
+
SET status = 'failed',
|
|
16401
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
16402
|
+
error = ?,
|
|
16403
|
+
finished_at = ?
|
|
16404
|
+
WHERE job_id = ?`
|
|
16405
|
+
).run(serializeCheckpoint(checkpoint), error48, now(), jobId);
|
|
16406
|
+
return getMaintenanceJob(db, jobId);
|
|
16407
|
+
}
|
|
16408
|
+
function completeMaintenanceJob(db, jobId, checkpoint) {
|
|
16409
|
+
db.prepare(
|
|
16410
|
+
`UPDATE maintenance_jobs
|
|
16411
|
+
SET status = 'completed',
|
|
16412
|
+
checkpoint = COALESCE(?, checkpoint),
|
|
16413
|
+
error = NULL,
|
|
16414
|
+
finished_at = ?
|
|
16415
|
+
WHERE job_id = ?`
|
|
16416
|
+
).run(serializeCheckpoint(checkpoint), now(), jobId);
|
|
16417
|
+
return getMaintenanceJob(db, jobId);
|
|
16418
|
+
}
|
|
16419
|
+
|
|
16420
|
+
// src/sleep/orchestrator.ts
|
|
16421
|
+
var DEFAULT_RUNNERS = {
|
|
16422
|
+
decay: (db, opts) => runDecay(db, opts),
|
|
16423
|
+
tidy: (db, opts) => runTidy(db, opts),
|
|
16424
|
+
govern: (db, opts) => runGovern(db, opts)
|
|
16425
|
+
};
|
|
16426
|
+
var PHASE_SEQUENCE = ["decay", "tidy", "govern"];
|
|
16427
|
+
function getSummaryStats(db, agentId) {
|
|
16428
|
+
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();
|
|
16429
|
+
return {
|
|
16430
|
+
total: row.total,
|
|
16431
|
+
avgVitality: row.avg
|
|
16432
|
+
};
|
|
16433
|
+
}
|
|
16434
|
+
function getPhaseSequence(phase) {
|
|
16435
|
+
return phase === "all" ? [...PHASE_SEQUENCE] : [phase];
|
|
16436
|
+
}
|
|
16437
|
+
function resolveJob(db, opts) {
|
|
16438
|
+
if (opts.jobId) {
|
|
16439
|
+
const job = getMaintenanceJob(db, opts.jobId);
|
|
16440
|
+
if (!job) {
|
|
16441
|
+
throw new Error(`Maintenance job not found: ${opts.jobId}`);
|
|
16442
|
+
}
|
|
16443
|
+
if (job.phase !== opts.phase) {
|
|
16444
|
+
throw new Error(`Maintenance job ${opts.jobId} phase mismatch: expected ${opts.phase}, got ${job.phase}`);
|
|
15311
16445
|
}
|
|
16446
|
+
return { job, resumed: true };
|
|
15312
16447
|
}
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
15318
|
-
for (const uri of additionalUris) {
|
|
15319
|
-
const path = getPathByUri(db, uri, agentId);
|
|
15320
|
-
if (path && !memories.has(path.memory_id)) {
|
|
15321
|
-
const mem = getMemory(db, path.memory_id);
|
|
15322
|
-
if (mem) {
|
|
15323
|
-
memories.set(mem.id, mem);
|
|
15324
|
-
bootPaths.push(uri);
|
|
15325
|
-
}
|
|
15326
|
-
}
|
|
15327
|
-
}
|
|
16448
|
+
if (opts.resume !== false) {
|
|
16449
|
+
const resumable = findResumableMaintenanceJob(db, opts.phase);
|
|
16450
|
+
if (resumable) {
|
|
16451
|
+
return { job: resumable, resumed: true };
|
|
15328
16452
|
}
|
|
15329
16453
|
}
|
|
15330
16454
|
return {
|
|
15331
|
-
|
|
15332
|
-
|
|
16455
|
+
job: createMaintenanceJob(db, opts.phase),
|
|
16456
|
+
resumed: false
|
|
16457
|
+
};
|
|
16458
|
+
}
|
|
16459
|
+
function nextPhase(current, requested) {
|
|
16460
|
+
if (requested !== "all") return null;
|
|
16461
|
+
const index = PHASE_SEQUENCE.indexOf(current);
|
|
16462
|
+
return PHASE_SEQUENCE[index + 1] ?? null;
|
|
16463
|
+
}
|
|
16464
|
+
async function runReflectOrchestrator(db, opts) {
|
|
16465
|
+
const runners = {
|
|
16466
|
+
...DEFAULT_RUNNERS,
|
|
16467
|
+
...opts.runners
|
|
16468
|
+
};
|
|
16469
|
+
const before = getSummaryStats(db, opts.agent_id);
|
|
16470
|
+
const { job: baseJob, resumed } = resolveJob(db, opts);
|
|
16471
|
+
let checkpoint = baseJob.checkpoint ?? createInitialCheckpoint(opts.phase);
|
|
16472
|
+
const jobId = baseJob.job_id;
|
|
16473
|
+
const orderedPhases = getPhaseSequence(opts.phase);
|
|
16474
|
+
const startPhase = checkpoint.nextPhase ?? orderedPhases[orderedPhases.length - 1] ?? "decay";
|
|
16475
|
+
const startIndex = Math.max(0, orderedPhases.indexOf(startPhase));
|
|
16476
|
+
const phasesToRun = checkpoint.nextPhase === null ? [] : orderedPhases.slice(startIndex);
|
|
16477
|
+
const totalPhases = Math.max(orderedPhases.length, 1);
|
|
16478
|
+
opts.onProgress?.({
|
|
16479
|
+
status: "started",
|
|
16480
|
+
phase: opts.phase,
|
|
16481
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
16482
|
+
jobId,
|
|
16483
|
+
detail: {
|
|
16484
|
+
resumed,
|
|
16485
|
+
nextPhase: checkpoint.nextPhase
|
|
16486
|
+
}
|
|
16487
|
+
});
|
|
16488
|
+
try {
|
|
16489
|
+
for (const phase of phasesToRun) {
|
|
16490
|
+
const result = await Promise.resolve(runners[phase](db, { agent_id: opts.agent_id }));
|
|
16491
|
+
checkpoint = {
|
|
16492
|
+
...checkpoint,
|
|
16493
|
+
completedPhases: [.../* @__PURE__ */ new Set([...checkpoint.completedPhases, phase])],
|
|
16494
|
+
phaseResults: {
|
|
16495
|
+
...checkpoint.phaseResults,
|
|
16496
|
+
[phase]: result
|
|
16497
|
+
},
|
|
16498
|
+
nextPhase: nextPhase(phase, opts.phase)
|
|
16499
|
+
};
|
|
16500
|
+
updateMaintenanceCheckpoint(db, jobId, checkpoint);
|
|
16501
|
+
opts.onProgress?.({
|
|
16502
|
+
status: "phase-completed",
|
|
16503
|
+
phase,
|
|
16504
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
16505
|
+
jobId,
|
|
16506
|
+
detail: result
|
|
16507
|
+
});
|
|
16508
|
+
}
|
|
16509
|
+
} catch (error48) {
|
|
16510
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
16511
|
+
const failed = failMaintenanceJob(db, jobId, message, checkpoint) ?? baseJob;
|
|
16512
|
+
opts.onProgress?.({
|
|
16513
|
+
status: "failed",
|
|
16514
|
+
phase: checkpoint.nextPhase ?? opts.phase,
|
|
16515
|
+
progress: checkpoint.completedPhases.length / totalPhases,
|
|
16516
|
+
jobId,
|
|
16517
|
+
detail: { error: message }
|
|
16518
|
+
});
|
|
16519
|
+
throw Object.assign(new Error(message), { job: failed, checkpoint });
|
|
16520
|
+
}
|
|
16521
|
+
const completedCheckpoint = {
|
|
16522
|
+
...checkpoint,
|
|
16523
|
+
nextPhase: null
|
|
16524
|
+
};
|
|
16525
|
+
const job = completeMaintenanceJob(db, jobId, completedCheckpoint) ?? baseJob;
|
|
16526
|
+
const after = getSummaryStats(db, opts.agent_id);
|
|
16527
|
+
opts.onProgress?.({
|
|
16528
|
+
status: "completed",
|
|
16529
|
+
phase: opts.phase,
|
|
16530
|
+
progress: 1,
|
|
16531
|
+
jobId,
|
|
16532
|
+
detail: completedCheckpoint.phaseResults
|
|
16533
|
+
});
|
|
16534
|
+
return {
|
|
16535
|
+
job,
|
|
16536
|
+
jobId,
|
|
16537
|
+
phase: opts.phase,
|
|
16538
|
+
resumed,
|
|
16539
|
+
checkpoint: completedCheckpoint,
|
|
16540
|
+
results: completedCheckpoint.phaseResults,
|
|
16541
|
+
before,
|
|
16542
|
+
after
|
|
16543
|
+
};
|
|
16544
|
+
}
|
|
16545
|
+
|
|
16546
|
+
// src/app/reflect.ts
|
|
16547
|
+
async function reflectMemories(db, input) {
|
|
16548
|
+
const options = {
|
|
16549
|
+
phase: input.phase,
|
|
16550
|
+
agent_id: input.agent_id,
|
|
16551
|
+
jobId: input.jobId,
|
|
16552
|
+
resume: input.resume,
|
|
16553
|
+
runners: input.runners,
|
|
16554
|
+
onProgress: input.onProgress
|
|
16555
|
+
};
|
|
16556
|
+
return runReflectOrchestrator(db, options);
|
|
16557
|
+
}
|
|
16558
|
+
|
|
16559
|
+
// src/app/status.ts
|
|
16560
|
+
init_memory();
|
|
16561
|
+
function getMemoryStatus(db, input) {
|
|
16562
|
+
const agentId = input?.agent_id ?? "default";
|
|
16563
|
+
const stats = countMemories(db, agentId);
|
|
16564
|
+
const lowVitality = db.prepare(
|
|
16565
|
+
"SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?"
|
|
16566
|
+
).get(agentId);
|
|
16567
|
+
const totalPaths = db.prepare(
|
|
16568
|
+
"SELECT COUNT(*) as c FROM paths WHERE agent_id = ?"
|
|
16569
|
+
).get(agentId);
|
|
16570
|
+
const feedbackEvents = db.prepare(
|
|
16571
|
+
"SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
|
|
16572
|
+
).get(agentId);
|
|
16573
|
+
return {
|
|
16574
|
+
...stats,
|
|
16575
|
+
paths: totalPaths.c,
|
|
16576
|
+
low_vitality: lowVitality.c,
|
|
16577
|
+
feedback_events: feedbackEvents.c,
|
|
16578
|
+
agent_id: agentId
|
|
15333
16579
|
};
|
|
15334
16580
|
}
|
|
15335
16581
|
|
|
16582
|
+
// src/app/reindex.ts
|
|
16583
|
+
async function reindexMemories(db, input) {
|
|
16584
|
+
input?.onProgress?.({ status: "started", stage: "fts", progress: 0 });
|
|
16585
|
+
try {
|
|
16586
|
+
const fts = rebuildBm25Index(db, { agent_id: input?.agent_id });
|
|
16587
|
+
input?.onProgress?.({
|
|
16588
|
+
status: "stage-completed",
|
|
16589
|
+
stage: "fts",
|
|
16590
|
+
progress: 0.5,
|
|
16591
|
+
detail: fts
|
|
16592
|
+
});
|
|
16593
|
+
const embeddings = await reindexEmbeddings(db, {
|
|
16594
|
+
agent_id: input?.agent_id,
|
|
16595
|
+
provider: input?.provider,
|
|
16596
|
+
force: input?.force,
|
|
16597
|
+
batchSize: input?.batchSize
|
|
16598
|
+
});
|
|
16599
|
+
input?.onProgress?.({
|
|
16600
|
+
status: "stage-completed",
|
|
16601
|
+
stage: "embeddings",
|
|
16602
|
+
progress: 0.9,
|
|
16603
|
+
detail: embeddings
|
|
16604
|
+
});
|
|
16605
|
+
const result = { fts, embeddings };
|
|
16606
|
+
input?.onProgress?.({
|
|
16607
|
+
status: "completed",
|
|
16608
|
+
stage: "done",
|
|
16609
|
+
progress: 1,
|
|
16610
|
+
detail: result
|
|
16611
|
+
});
|
|
16612
|
+
return result;
|
|
16613
|
+
} catch (error48) {
|
|
16614
|
+
input?.onProgress?.({
|
|
16615
|
+
status: "failed",
|
|
16616
|
+
stage: "done",
|
|
16617
|
+
progress: 1,
|
|
16618
|
+
detail: error48 instanceof Error ? error48.message : String(error48)
|
|
16619
|
+
});
|
|
16620
|
+
throw error48;
|
|
16621
|
+
}
|
|
16622
|
+
}
|
|
16623
|
+
|
|
15336
16624
|
// src/mcp/server.ts
|
|
15337
16625
|
var DB_PATH = process.env.AGENT_MEMORY_DB ?? "./agent-memory.db";
|
|
15338
16626
|
var AGENT_ID = process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
16627
|
+
function formatMemory(memory, score) {
|
|
16628
|
+
return {
|
|
16629
|
+
id: memory.id,
|
|
16630
|
+
uri: null,
|
|
16631
|
+
content: memory.content,
|
|
16632
|
+
type: memory.type,
|
|
16633
|
+
priority: memory.priority,
|
|
16634
|
+
vitality: memory.vitality,
|
|
16635
|
+
score,
|
|
16636
|
+
updated_at: memory.updated_at
|
|
16637
|
+
};
|
|
16638
|
+
}
|
|
16639
|
+
function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
|
|
16640
|
+
const currentTime = Date.now();
|
|
16641
|
+
const sevenDaysAgo = currentTime - 7 * 24 * 60 * 60 * 1e3;
|
|
16642
|
+
const recentEvents = events.filter((event) => new Date(event.updated_at).getTime() >= sevenDaysAgo);
|
|
16643
|
+
const olderEventCount = Math.max(0, events.length - recentEvents.length);
|
|
16644
|
+
const avgVitalitySource = [...identities, ...emotions, ...knowledges, ...events];
|
|
16645
|
+
const avgVitality = avgVitalitySource.length ? avgVitalitySource.reduce((sum, memory) => sum + memory.vitality, 0) / avgVitalitySource.length : 0;
|
|
16646
|
+
const lines = [];
|
|
16647
|
+
lines.push("## \u{1FAAA} \u6211\u662F\u8C01");
|
|
16648
|
+
if (identities.length === 0) {
|
|
16649
|
+
lines.push("\u6682\u65E0\u8EAB\u4EFD\u8BB0\u5FC6\u3002");
|
|
16650
|
+
} else {
|
|
16651
|
+
for (const memory of identities.slice(0, 6)) {
|
|
16652
|
+
lines.push(`- ${memory.content.slice(0, 140)}`);
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
lines.push("", "## \u{1F495} \u6700\u8FD1\u7684\u60C5\u611F");
|
|
16656
|
+
if (emotions.length === 0) {
|
|
16657
|
+
lines.push("\u6682\u65E0\u60C5\u611F\u8BB0\u5FC6\u3002");
|
|
16658
|
+
} else {
|
|
16659
|
+
for (const memory of emotions.slice(0, 6)) {
|
|
16660
|
+
lines.push(`- ${memory.content.slice(0, 140)}\uFF08vitality: ${memory.vitality.toFixed(2)}\uFF09`);
|
|
16661
|
+
}
|
|
16662
|
+
}
|
|
16663
|
+
lines.push("", "## \u{1F9E0} \u5173\u952E\u77E5\u8BC6");
|
|
16664
|
+
if (knowledges.length === 0) {
|
|
16665
|
+
lines.push("\u6682\u65E0\u77E5\u8BC6\u8BB0\u5FC6\u3002");
|
|
16666
|
+
} else {
|
|
16667
|
+
lines.push(`\u5171 ${knowledges.length} \u6761\u6D3B\u8DC3\u77E5\u8BC6\u8BB0\u5FC6`);
|
|
16668
|
+
for (const memory of knowledges.slice(0, 8)) {
|
|
16669
|
+
lines.push(`- ${memory.content.slice(0, 140)}\uFF08vitality: ${memory.vitality.toFixed(2)}\uFF09`);
|
|
16670
|
+
}
|
|
16671
|
+
}
|
|
16672
|
+
lines.push("", "## \u{1F4C5} \u8FD1\u671F\u4E8B\u4EF6");
|
|
16673
|
+
if (recentEvents.length === 0) {
|
|
16674
|
+
lines.push("\u6700\u8FD1 7 \u5929\u65E0\u4E8B\u4EF6\u8BB0\u5FC6\u3002");
|
|
16675
|
+
} else {
|
|
16676
|
+
lines.push("\u6700\u8FD1 7 \u5929\u5185\u7684\u4E8B\u4EF6\uFF1A");
|
|
16677
|
+
for (const memory of recentEvents.slice(0, 8)) {
|
|
16678
|
+
lines.push(`- [${memory.updated_at.slice(5, 10)}] ${memory.content.slice(0, 120)}`);
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16681
|
+
if (olderEventCount > 0) {
|
|
16682
|
+
lines.push(`- ... \u53CA ${olderEventCount} \u6761\u66F4\u65E9\u4E8B\u4EF6`);
|
|
16683
|
+
}
|
|
16684
|
+
lines.push("", "## \u{1F4CA} \u8BB0\u5FC6\u6982\u51B5");
|
|
16685
|
+
lines.push(
|
|
16686
|
+
`\u603B\u8BA1 ${totalStats.total} \u6761 | identity: ${totalStats.by_type.identity ?? 0} | emotion: ${totalStats.by_type.emotion ?? 0} | knowledge: ${totalStats.by_type.knowledge ?? 0} | event: ${totalStats.by_type.event ?? 0}`
|
|
16687
|
+
);
|
|
16688
|
+
lines.push(`\u5E73\u5747 vitality: ${avgVitality.toFixed(2)}`);
|
|
16689
|
+
return lines.join("\n");
|
|
16690
|
+
}
|
|
16691
|
+
function getSummaryStats2(db, agentId) {
|
|
16692
|
+
const row = db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories WHERE agent_id = ?").get(agentId);
|
|
16693
|
+
return { total: row.total, avgVitality: row.avg };
|
|
16694
|
+
}
|
|
16695
|
+
function formatReflectReport(input) {
|
|
16696
|
+
const lines = [];
|
|
16697
|
+
lines.push("## \u{1F319} Sleep Cycle \u62A5\u544A", "");
|
|
16698
|
+
lines.push(`job: ${input.jobId}${input.resumed ? "\uFF08resume\uFF09" : ""}`);
|
|
16699
|
+
lines.push(`phase: ${input.phase}`, "");
|
|
16700
|
+
for (const phase of ["decay", "tidy", "govern"]) {
|
|
16701
|
+
if (input.phase !== "all" && input.phase !== phase) continue;
|
|
16702
|
+
lines.push(`### ${phase}`);
|
|
16703
|
+
lines.push(JSON.stringify(input.results[phase] ?? {}, null, 2));
|
|
16704
|
+
lines.push("");
|
|
16705
|
+
}
|
|
16706
|
+
lines.push("### \u{1F4CA} \u603B\u7ED3");
|
|
16707
|
+
const delta = input.after.total - input.before.total;
|
|
16708
|
+
const deltaLabel = delta > 0 ? `+${delta}` : `${delta}`;
|
|
16709
|
+
lines.push(`\u8BB0\u5FC6\u603B\u6570\uFF1A${input.before.total} \u2192 ${input.after.total}\uFF08${deltaLabel}\uFF09`);
|
|
16710
|
+
lines.push(`\u5E73\u5747 vitality\uFF1A${input.before.avgVitality.toFixed(2)} \u2192 ${input.after.avgVitality.toFixed(2)}`);
|
|
16711
|
+
return lines.join("\n");
|
|
16712
|
+
}
|
|
16713
|
+
function formatRecallPayload(result) {
|
|
16714
|
+
return {
|
|
16715
|
+
mode: result.mode,
|
|
16716
|
+
provider_id: result.providerId,
|
|
16717
|
+
count: result.results.length,
|
|
16718
|
+
memories: result.results.map((row) => ({
|
|
16719
|
+
...formatMemory(row.memory, row.score),
|
|
16720
|
+
bm25_rank: row.bm25_rank,
|
|
16721
|
+
vector_rank: row.vector_rank,
|
|
16722
|
+
bm25_score: row.bm25_score,
|
|
16723
|
+
vector_score: row.vector_score
|
|
16724
|
+
}))
|
|
16725
|
+
};
|
|
16726
|
+
}
|
|
16727
|
+
function formatSurfacePayload(result) {
|
|
16728
|
+
return {
|
|
16729
|
+
count: result.count,
|
|
16730
|
+
query: result.query,
|
|
16731
|
+
task: result.task,
|
|
16732
|
+
intent: result.intent,
|
|
16733
|
+
results: result.results.map((row) => ({
|
|
16734
|
+
id: row.memory.id,
|
|
16735
|
+
content: row.memory.content,
|
|
16736
|
+
type: row.memory.type,
|
|
16737
|
+
priority: row.memory.priority,
|
|
16738
|
+
vitality: row.memory.vitality,
|
|
16739
|
+
score: row.score,
|
|
16740
|
+
semantic_score: row.semantic_score,
|
|
16741
|
+
lexical_score: row.lexical_score,
|
|
16742
|
+
task_match: row.task_match,
|
|
16743
|
+
priority_prior: row.priority_prior,
|
|
16744
|
+
feedback_score: row.feedback_score,
|
|
16745
|
+
reason_codes: row.reason_codes,
|
|
16746
|
+
updated_at: row.memory.updated_at
|
|
16747
|
+
}))
|
|
16748
|
+
};
|
|
16749
|
+
}
|
|
15339
16750
|
function createMcpServer(dbPath, agentId) {
|
|
15340
16751
|
const db = openDatabase({ path: dbPath ?? DB_PATH });
|
|
15341
16752
|
const aid = agentId ?? AGENT_ID;
|
|
15342
|
-
const embeddingProvider = getEmbeddingProviderFromEnv();
|
|
15343
|
-
const rerankerProvider = getRerankerProviderFromEnv();
|
|
15344
16753
|
const server = new McpServer({
|
|
15345
16754
|
name: "agent-memory",
|
|
15346
|
-
version: "
|
|
16755
|
+
version: "4.0.0-alpha.1"
|
|
15347
16756
|
});
|
|
15348
16757
|
server.tool(
|
|
15349
16758
|
"remember",
|
|
15350
|
-
"Store a memory. Runs Write Guard (dedup +
|
|
16759
|
+
"Store a memory. Runs Write Guard (semantic dedup + merge policy + four-criterion gate). Optionally assign a URI path.",
|
|
15351
16760
|
{
|
|
15352
16761
|
content: external_exports.string().describe("Memory content to store"),
|
|
15353
16762
|
type: external_exports.enum(["identity", "emotion", "knowledge", "event"]).default("knowledge").describe("Memory type (determines priority and decay rate)"),
|
|
15354
16763
|
uri: external_exports.string().optional().describe("URI path (e.g. core://user/name, emotion://2026-02-20/love)"),
|
|
15355
16764
|
emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
|
|
15356
|
-
source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)")
|
|
16765
|
+
source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
|
|
16766
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
15357
16767
|
},
|
|
15358
|
-
async ({ content, type, uri, emotion_val, source }) => {
|
|
15359
|
-
const result =
|
|
15360
|
-
|
|
15361
|
-
try {
|
|
15362
|
-
await embedMemory(db, result.memoryId, embeddingProvider, { agent_id: aid });
|
|
15363
|
-
} catch {
|
|
15364
|
-
}
|
|
15365
|
-
}
|
|
15366
|
-
return {
|
|
15367
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
15368
|
-
};
|
|
16768
|
+
async ({ content, type, uri, emotion_val, source, agent_id }) => {
|
|
16769
|
+
const result = await rememberMemory(db, { content, type, uri, emotion_val, source, agent_id: agent_id ?? aid });
|
|
16770
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
15369
16771
|
}
|
|
15370
16772
|
);
|
|
15371
16773
|
server.tool(
|
|
15372
16774
|
"recall",
|
|
15373
|
-
"Search memories using
|
|
16775
|
+
"Search memories using optional hybrid retrieval (BM25 + vector). Falls back to BM25-only when no embedding provider is configured.",
|
|
15374
16776
|
{
|
|
15375
16777
|
query: external_exports.string().describe("Search query (natural language)"),
|
|
15376
|
-
limit: external_exports.number().default(10).describe("Max results to return")
|
|
16778
|
+
limit: external_exports.number().default(10).describe("Max results to return"),
|
|
16779
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
15377
16780
|
},
|
|
15378
|
-
async ({ query, limit }) => {
|
|
15379
|
-
const {
|
|
15380
|
-
|
|
15381
|
-
let raw = await searchHybrid(db, query, { agent_id: aid, embeddingProvider, limit: limit * 2 });
|
|
15382
|
-
if (rerankerProvider) {
|
|
15383
|
-
raw = await rerankWithProvider(raw, query, rerankerProvider);
|
|
15384
|
-
}
|
|
15385
|
-
const results = rerank(raw, { ...strategy, limit });
|
|
15386
|
-
const output = {
|
|
15387
|
-
intent,
|
|
15388
|
-
confidence,
|
|
15389
|
-
count: results.length,
|
|
15390
|
-
memories: results.map((r) => ({
|
|
15391
|
-
id: r.memory.id,
|
|
15392
|
-
content: r.memory.content,
|
|
15393
|
-
type: r.memory.type,
|
|
15394
|
-
priority: r.memory.priority,
|
|
15395
|
-
vitality: r.memory.vitality,
|
|
15396
|
-
score: r.score,
|
|
15397
|
-
updated_at: r.memory.updated_at
|
|
15398
|
-
}))
|
|
15399
|
-
};
|
|
15400
|
-
for (const r of results) {
|
|
15401
|
-
recordAccess(db, r.memory.id);
|
|
15402
|
-
}
|
|
15403
|
-
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
16781
|
+
async ({ query, limit, agent_id }) => {
|
|
16782
|
+
const result = await recallMemory(db, { query, limit, agent_id: agent_id ?? aid });
|
|
16783
|
+
return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
|
|
15404
16784
|
}
|
|
15405
16785
|
);
|
|
15406
16786
|
server.tool(
|
|
15407
16787
|
"recall_path",
|
|
15408
|
-
"Read memory at a specific URI path, or list memories under a URI prefix.
|
|
16788
|
+
"Read memory at a specific URI path, or list memories under a URI prefix.",
|
|
15409
16789
|
{
|
|
15410
16790
|
uri: external_exports.string().describe("URI path (e.g. core://user/name) or prefix (e.g. core://user/)"),
|
|
15411
|
-
traverse_hops: external_exports.number().default(0).describe("
|
|
16791
|
+
traverse_hops: external_exports.number().default(0).describe("Traversal depth (deprecated, reserved for compatibility)")
|
|
15412
16792
|
},
|
|
15413
|
-
async ({ uri
|
|
16793
|
+
async ({ uri }) => {
|
|
15414
16794
|
const path = getPathByUri(db, uri, aid);
|
|
15415
16795
|
if (path) {
|
|
15416
|
-
const
|
|
15417
|
-
if (
|
|
15418
|
-
recordAccess(db,
|
|
15419
|
-
let related = [];
|
|
15420
|
-
if (traverse_hops > 0) {
|
|
15421
|
-
const hops = traverse(db, mem.id, traverse_hops, aid);
|
|
15422
|
-
related = hops.map((h) => {
|
|
15423
|
-
const m = getMemory(db, h.id);
|
|
15424
|
-
if (!m || m.agent_id !== aid) return { id: h.id, content: "", relation: h.relation, hop: h.hop };
|
|
15425
|
-
return { id: h.id, content: m.content, relation: h.relation, hop: h.hop };
|
|
15426
|
-
});
|
|
15427
|
-
}
|
|
16796
|
+
const memory = getMemory(db, path.memory_id);
|
|
16797
|
+
if (memory && memory.agent_id === aid) {
|
|
16798
|
+
recordAccess(db, memory.id);
|
|
15428
16799
|
return {
|
|
15429
|
-
content: [{
|
|
15430
|
-
type: "text",
|
|
15431
|
-
text: JSON.stringify({ found: true, memory: mem, related }, null, 2)
|
|
15432
|
-
}]
|
|
16800
|
+
content: [{ type: "text", text: JSON.stringify({ found: true, memory }, null, 2) }]
|
|
15433
16801
|
};
|
|
15434
16802
|
}
|
|
15435
16803
|
}
|
|
15436
16804
|
const paths = getPathsByPrefix(db, uri, aid);
|
|
15437
16805
|
if (paths.length > 0) {
|
|
15438
|
-
const memories = paths.map((
|
|
15439
|
-
const
|
|
15440
|
-
if (!
|
|
15441
|
-
|
|
16806
|
+
const memories = paths.map((entry) => {
|
|
16807
|
+
const memory = getMemory(db, entry.memory_id);
|
|
16808
|
+
if (!memory || memory.agent_id !== aid) {
|
|
16809
|
+
return { uri: entry.uri, content: void 0, type: void 0, priority: void 0 };
|
|
16810
|
+
}
|
|
16811
|
+
return {
|
|
16812
|
+
uri: entry.uri,
|
|
16813
|
+
content: memory.content,
|
|
16814
|
+
type: memory.type,
|
|
16815
|
+
priority: memory.priority,
|
|
16816
|
+
vitality: memory.vitality
|
|
16817
|
+
};
|
|
15442
16818
|
});
|
|
15443
16819
|
return {
|
|
15444
|
-
content: [{
|
|
15445
|
-
type: "text",
|
|
15446
|
-
text: JSON.stringify({ found: true, prefix: uri, count: paths.length, memories }, null, 2)
|
|
15447
|
-
}]
|
|
16820
|
+
content: [{ type: "text", text: JSON.stringify({ found: true, prefix: uri, count: paths.length, memories }, null, 2) }]
|
|
15448
16821
|
};
|
|
15449
16822
|
}
|
|
15450
16823
|
return { content: [{ type: "text", text: JSON.stringify({ found: false, uri }, null, 2) }] };
|
|
@@ -15452,21 +16825,36 @@ function createMcpServer(dbPath, agentId) {
|
|
|
15452
16825
|
);
|
|
15453
16826
|
server.tool(
|
|
15454
16827
|
"boot",
|
|
15455
|
-
"Load
|
|
15456
|
-
{
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
|
|
15462
|
-
|
|
15463
|
-
|
|
15464
|
-
content:
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
16828
|
+
"Load startup memories. Default output is narrative markdown; pass format=json for legacy output.",
|
|
16829
|
+
{
|
|
16830
|
+
format: external_exports.enum(["narrative", "json"]).default("narrative").optional()
|
|
16831
|
+
},
|
|
16832
|
+
async ({ format }) => {
|
|
16833
|
+
const outputFormat = format ?? "narrative";
|
|
16834
|
+
const base = boot(db, { agent_id: aid });
|
|
16835
|
+
if (outputFormat === "json") {
|
|
16836
|
+
return {
|
|
16837
|
+
content: [{
|
|
16838
|
+
type: "text",
|
|
16839
|
+
text: JSON.stringify({
|
|
16840
|
+
count: base.identityMemories.length,
|
|
16841
|
+
bootPaths: base.bootPaths,
|
|
16842
|
+
memories: base.identityMemories.map((memory) => ({
|
|
16843
|
+
id: memory.id,
|
|
16844
|
+
content: memory.content,
|
|
16845
|
+
type: memory.type,
|
|
16846
|
+
priority: memory.priority
|
|
16847
|
+
}))
|
|
16848
|
+
}, null, 2)
|
|
16849
|
+
}]
|
|
16850
|
+
};
|
|
16851
|
+
}
|
|
16852
|
+
const identity = listMemories(db, { agent_id: aid, type: "identity", limit: 12 });
|
|
16853
|
+
const emotion = listMemories(db, { agent_id: aid, type: "emotion", min_vitality: 0.1, limit: 12 }).sort((a, b) => b.vitality - a.vitality);
|
|
16854
|
+
const knowledge = listMemories(db, { agent_id: aid, type: "knowledge", min_vitality: 0.1, limit: 16 }).sort((a, b) => b.vitality - a.vitality);
|
|
16855
|
+
const event = listMemories(db, { agent_id: aid, type: "event", min_vitality: 0, limit: 24 }).sort((a, b) => b.vitality - a.vitality);
|
|
16856
|
+
const stats = countMemories(db, aid);
|
|
16857
|
+
return { content: [{ type: "text", text: formatWarmBootNarrative(identity.length > 0 ? identity : base.identityMemories, emotion, knowledge, event, stats) }] };
|
|
15470
16858
|
}
|
|
15471
16859
|
);
|
|
15472
16860
|
server.tool(
|
|
@@ -15477,126 +16865,123 @@ function createMcpServer(dbPath, agentId) {
|
|
|
15477
16865
|
hard: external_exports.boolean().default(false).describe("Hard delete (true) or soft decay (false)")
|
|
15478
16866
|
},
|
|
15479
16867
|
async ({ id, hard }) => {
|
|
15480
|
-
const
|
|
15481
|
-
if (!
|
|
16868
|
+
const memory = getMemory(db, id);
|
|
16869
|
+
if (!memory || memory.agent_id !== aid) {
|
|
16870
|
+
return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
|
|
16871
|
+
}
|
|
15482
16872
|
if (hard) {
|
|
15483
|
-
createSnapshot(db, id, "delete", "forget");
|
|
15484
16873
|
const { deleteMemory: deleteMemory2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
15485
16874
|
deleteMemory2(db, id);
|
|
15486
16875
|
return { content: [{ type: "text", text: JSON.stringify({ action: "deleted", id }) }] };
|
|
15487
16876
|
}
|
|
15488
|
-
updateMemory(db, id, { vitality: Math.max(0,
|
|
16877
|
+
updateMemory(db, id, { vitality: Math.max(0, memory.vitality * 0.1) });
|
|
15489
16878
|
return {
|
|
15490
|
-
content: [{
|
|
15491
|
-
type: "text",
|
|
15492
|
-
text: JSON.stringify({ action: "decayed", id, new_vitality: mem.vitality * 0.1 })
|
|
15493
|
-
}]
|
|
16879
|
+
content: [{ type: "text", text: JSON.stringify({ action: "decayed", id, new_vitality: memory.vitality * 0.1 }) }]
|
|
15494
16880
|
};
|
|
15495
16881
|
}
|
|
15496
16882
|
);
|
|
15497
16883
|
server.tool(
|
|
15498
|
-
"
|
|
15499
|
-
"
|
|
16884
|
+
"reflect",
|
|
16885
|
+
"Trigger sleep cycle phases via the maintenance orchestrator and return a human-readable markdown report.",
|
|
15500
16886
|
{
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
target_id: external_exports.string().optional().describe("Target memory ID (for create)"),
|
|
15504
|
-
relation: external_exports.enum(["related", "caused", "reminds", "evolved", "contradicts"]).optional().describe("Relation type"),
|
|
15505
|
-
max_hops: external_exports.number().default(2).describe("Max traversal depth (for traverse action)")
|
|
16887
|
+
phase: external_exports.enum(["decay", "tidy", "govern", "all"]).describe("Which sleep phase to run"),
|
|
16888
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
15506
16889
|
},
|
|
15507
|
-
async ({
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
return { ...n, content: m.content };
|
|
15522
|
-
});
|
|
15523
|
-
return { content: [{ type: "text", text: JSON.stringify({ nodes: detailed }) }] };
|
|
15524
|
-
}
|
|
15525
|
-
return { content: [{ type: "text", text: '{"error": "Invalid action or missing params"}' }] };
|
|
16890
|
+
async ({ phase, agent_id }) => {
|
|
16891
|
+
const effectiveAgentId = agent_id ?? aid;
|
|
16892
|
+
const before = getSummaryStats2(db, effectiveAgentId);
|
|
16893
|
+
const result = await reflectMemories(db, { phase, agent_id: effectiveAgentId });
|
|
16894
|
+
const after = getSummaryStats2(db, effectiveAgentId);
|
|
16895
|
+
const report = formatReflectReport({
|
|
16896
|
+
phase,
|
|
16897
|
+
jobId: result.jobId,
|
|
16898
|
+
resumed: result.resumed,
|
|
16899
|
+
before,
|
|
16900
|
+
after,
|
|
16901
|
+
results: result.results
|
|
16902
|
+
});
|
|
16903
|
+
return { content: [{ type: "text", text: report }] };
|
|
15526
16904
|
}
|
|
15527
16905
|
);
|
|
15528
16906
|
server.tool(
|
|
15529
|
-
"
|
|
15530
|
-
"
|
|
16907
|
+
"status",
|
|
16908
|
+
"Get memory system statistics: counts by type/priority and health metrics.",
|
|
15531
16909
|
{
|
|
15532
|
-
|
|
15533
|
-
memory_id: external_exports.string().optional().describe("Memory ID (for list)"),
|
|
15534
|
-
snapshot_id: external_exports.string().optional().describe("Snapshot ID (for rollback)")
|
|
16910
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
15535
16911
|
},
|
|
15536
|
-
async ({
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
const snap = getSnapshot(db, snapshot_id);
|
|
15545
|
-
if (!snap) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
|
|
15546
|
-
const mem = getMemory(db, snap.memory_id);
|
|
15547
|
-
if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
|
|
15548
|
-
const ok = rollback(db, snapshot_id);
|
|
15549
|
-
return { content: [{ type: "text", text: JSON.stringify({ rolled_back: ok }) }] };
|
|
15550
|
-
}
|
|
15551
|
-
return { content: [{ type: "text", text: '{"error": "Invalid action or missing params"}' }] };
|
|
16912
|
+
async ({ agent_id }) => {
|
|
16913
|
+
const stats = getMemoryStatus(db, { agent_id: agent_id ?? aid });
|
|
16914
|
+
return {
|
|
16915
|
+
content: [{
|
|
16916
|
+
type: "text",
|
|
16917
|
+
text: JSON.stringify(stats, null, 2)
|
|
16918
|
+
}]
|
|
16919
|
+
};
|
|
15552
16920
|
}
|
|
15553
16921
|
);
|
|
15554
16922
|
server.tool(
|
|
15555
|
-
"
|
|
15556
|
-
"
|
|
16923
|
+
"ingest",
|
|
16924
|
+
"Extract structured memories from markdown text and write via syncOne().",
|
|
15557
16925
|
{
|
|
15558
|
-
|
|
16926
|
+
text: external_exports.string().describe("Markdown/plain text to ingest"),
|
|
16927
|
+
source: external_exports.string().optional().describe("Source annotation, e.g. memory/2026-02-23.md"),
|
|
16928
|
+
dry_run: external_exports.boolean().default(false).optional().describe("Preview extraction without writing")
|
|
15559
16929
|
},
|
|
15560
|
-
async ({
|
|
15561
|
-
const
|
|
15562
|
-
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
}
|
|
15568
|
-
if (phase === "govern" || phase === "all") {
|
|
15569
|
-
results.govern = runGovern(db, { agent_id: aid });
|
|
15570
|
-
}
|
|
15571
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
16930
|
+
async ({ text, source, dry_run }) => {
|
|
16931
|
+
const result = await ingestText(db, {
|
|
16932
|
+
text,
|
|
16933
|
+
source,
|
|
16934
|
+
dryRun: dry_run,
|
|
16935
|
+
agentId: aid
|
|
16936
|
+
});
|
|
16937
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
15572
16938
|
}
|
|
15573
16939
|
);
|
|
15574
16940
|
server.tool(
|
|
15575
|
-
"
|
|
15576
|
-
"
|
|
15577
|
-
{
|
|
15578
|
-
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
16941
|
+
"reindex",
|
|
16942
|
+
"Rebuild BM25 index and (optionally) embedding vectors for the current agent.",
|
|
16943
|
+
{
|
|
16944
|
+
full: external_exports.boolean().default(false).optional().describe("Force full embedding rebuild instead of incremental backfill"),
|
|
16945
|
+
batch_size: external_exports.number().min(1).max(128).default(16).optional().describe("Embedding batch size for reindex"),
|
|
16946
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
16947
|
+
},
|
|
16948
|
+
async ({ full, batch_size, agent_id }) => {
|
|
16949
|
+
const result = await reindexMemories(db, {
|
|
16950
|
+
agent_id: agent_id ?? aid,
|
|
16951
|
+
force: full ?? false,
|
|
16952
|
+
batchSize: batch_size ?? 16
|
|
16953
|
+
});
|
|
16954
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
16955
|
+
}
|
|
16956
|
+
);
|
|
16957
|
+
server.tool(
|
|
16958
|
+
"surface",
|
|
16959
|
+
"Context-aware readonly memory surfacing with query/task/recent_turns/intent scoring (no access recording).",
|
|
16960
|
+
{
|
|
16961
|
+
query: external_exports.string().optional().describe("Optional semantic query for surfacing"),
|
|
16962
|
+
task: external_exports.string().optional().describe("Current task description"),
|
|
16963
|
+
recent_turns: external_exports.array(external_exports.string()).optional().describe("Recent conversation turns for context"),
|
|
16964
|
+
intent: external_exports.enum(["factual", "preference", "temporal", "planning", "design"]).optional().describe("Surface intent bias"),
|
|
16965
|
+
types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"]).describe("Optional type filter")).optional(),
|
|
16966
|
+
limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
|
|
16967
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
16968
|
+
keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted")
|
|
16969
|
+
},
|
|
16970
|
+
async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords }) => {
|
|
16971
|
+
const resolvedQuery = query ?? keywords?.join(" ");
|
|
16972
|
+
const result = await surfaceMemories(db, {
|
|
16973
|
+
query: resolvedQuery,
|
|
16974
|
+
task,
|
|
16975
|
+
recent_turns,
|
|
16976
|
+
intent,
|
|
16977
|
+
types,
|
|
16978
|
+
limit: limit ?? 5,
|
|
16979
|
+
agent_id: agent_id ?? aid
|
|
16980
|
+
});
|
|
15589
16981
|
return {
|
|
15590
16982
|
content: [{
|
|
15591
16983
|
type: "text",
|
|
15592
|
-
text: JSON.stringify(
|
|
15593
|
-
...stats,
|
|
15594
|
-
paths: totalPaths.c,
|
|
15595
|
-
links: totalLinks.c,
|
|
15596
|
-
snapshots: totalSnapshots.c,
|
|
15597
|
-
low_vitality: lowVitality.c,
|
|
15598
|
-
agent_id: aid
|
|
15599
|
-
}, null, 2)
|
|
16984
|
+
text: JSON.stringify(formatSurfacePayload(result), null, 2)
|
|
15600
16985
|
}]
|
|
15601
16986
|
};
|
|
15602
16987
|
}
|
|
@@ -15604,8 +16989,25 @@ function createMcpServer(dbPath, agentId) {
|
|
|
15604
16989
|
return { server, db };
|
|
15605
16990
|
}
|
|
15606
16991
|
async function main() {
|
|
15607
|
-
const { server } = createMcpServer();
|
|
16992
|
+
const { server, db } = createMcpServer();
|
|
15608
16993
|
const transport = new StdioServerTransport();
|
|
16994
|
+
const autoIngestEnabled = process.env.AGENT_MEMORY_AUTO_INGEST !== "0";
|
|
16995
|
+
const workspaceDir = process.env.AGENT_MEMORY_WORKSPACE ?? `${process.env.HOME ?? "."}/.openclaw/workspace`;
|
|
16996
|
+
const agentId = process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
16997
|
+
const watcher = autoIngestEnabled ? runAutoIngestWatcher({
|
|
16998
|
+
db,
|
|
16999
|
+
workspaceDir,
|
|
17000
|
+
agentId
|
|
17001
|
+
}) : null;
|
|
17002
|
+
const shutdown = () => {
|
|
17003
|
+
try {
|
|
17004
|
+
watcher?.close();
|
|
17005
|
+
} catch {
|
|
17006
|
+
}
|
|
17007
|
+
};
|
|
17008
|
+
process.once("SIGINT", shutdown);
|
|
17009
|
+
process.once("SIGTERM", shutdown);
|
|
17010
|
+
process.once("exit", shutdown);
|
|
15609
17011
|
await server.connect(transport);
|
|
15610
17012
|
}
|
|
15611
17013
|
var isMain = process.argv[1]?.includes("server");
|