@smyslenny/agent-memory 4.3.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-memory.js +417 -45
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +108 -44
- package/dist/index.js +433 -61
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +530 -80
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -94,6 +94,11 @@ function migrateDatabase(db, from, to) {
|
|
|
94
94
|
v = 6;
|
|
95
95
|
continue;
|
|
96
96
|
}
|
|
97
|
+
if (v === 6) {
|
|
98
|
+
migrateV6ToV7(db);
|
|
99
|
+
v = 7;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
97
102
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -180,6 +185,8 @@ function inferSchemaVersion(db) {
|
|
|
180
185
|
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
181
186
|
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
182
187
|
const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
|
|
188
|
+
const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
189
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
|
|
183
190
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
|
|
184
191
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
185
192
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
@@ -210,6 +217,10 @@ function ensureIndexes(db) {
|
|
|
210
217
|
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
211
218
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
212
219
|
}
|
|
220
|
+
if (tableHasColumn(db, "memories", "observed_at")) {
|
|
221
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
222
|
+
}
|
|
223
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
213
224
|
if (tableExists(db, "feedback_events")) {
|
|
214
225
|
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
215
226
|
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
@@ -362,11 +373,40 @@ function migrateV5ToV6(db) {
|
|
|
362
373
|
throw e;
|
|
363
374
|
}
|
|
364
375
|
}
|
|
376
|
+
function migrateV6ToV7(db) {
|
|
377
|
+
const alreadyMigrated = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
378
|
+
if (alreadyMigrated) {
|
|
379
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
db.exec("BEGIN");
|
|
384
|
+
if (!tableHasColumn(db, "memories", "source_session")) {
|
|
385
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_session TEXT;");
|
|
386
|
+
}
|
|
387
|
+
if (!tableHasColumn(db, "memories", "source_context")) {
|
|
388
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_context TEXT;");
|
|
389
|
+
}
|
|
390
|
+
if (!tableHasColumn(db, "memories", "observed_at")) {
|
|
391
|
+
db.exec("ALTER TABLE memories ADD COLUMN observed_at TEXT;");
|
|
392
|
+
}
|
|
393
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
394
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
395
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
396
|
+
db.exec("COMMIT");
|
|
397
|
+
} catch (e) {
|
|
398
|
+
try {
|
|
399
|
+
db.exec("ROLLBACK");
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
throw e;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
365
405
|
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
366
406
|
var init_db = __esm({
|
|
367
407
|
"src/core/db.ts"() {
|
|
368
408
|
"use strict";
|
|
369
|
-
SCHEMA_VERSION =
|
|
409
|
+
SCHEMA_VERSION = 7;
|
|
370
410
|
SCHEMA_SQL = `
|
|
371
411
|
-- Memory entries
|
|
372
412
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -385,6 +425,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
385
425
|
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
386
426
|
hash TEXT,
|
|
387
427
|
emotion_tag TEXT,
|
|
428
|
+
source_session TEXT,
|
|
429
|
+
source_context TEXT,
|
|
430
|
+
observed_at TEXT,
|
|
388
431
|
UNIQUE(hash, agent_id)
|
|
389
432
|
);
|
|
390
433
|
|
|
@@ -931,6 +974,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
931
974
|
const limit = opts.limit ?? 20;
|
|
932
975
|
const agentId = opts.agent_id ?? "default";
|
|
933
976
|
const minVitality = opts.min_vitality ?? 0;
|
|
977
|
+
const conditions = [
|
|
978
|
+
"e.provider_id = ?",
|
|
979
|
+
"e.status = 'ready'",
|
|
980
|
+
"e.vector IS NOT NULL",
|
|
981
|
+
"e.content_hash = m.hash",
|
|
982
|
+
"m.agent_id = ?",
|
|
983
|
+
"m.vitality >= ?"
|
|
984
|
+
];
|
|
985
|
+
const params = [opts.providerId, agentId, minVitality];
|
|
986
|
+
if (opts.after) {
|
|
987
|
+
conditions.push("m.updated_at >= ?");
|
|
988
|
+
params.push(opts.after);
|
|
989
|
+
}
|
|
990
|
+
if (opts.before) {
|
|
991
|
+
conditions.push("m.updated_at <= ?");
|
|
992
|
+
params.push(opts.before);
|
|
993
|
+
}
|
|
934
994
|
const rows = db.prepare(
|
|
935
995
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
936
996
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -938,13 +998,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
938
998
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
939
999
|
FROM embeddings e
|
|
940
1000
|
JOIN memories m ON m.id = e.memory_id
|
|
941
|
-
WHERE
|
|
942
|
-
|
|
943
|
-
AND e.vector IS NOT NULL
|
|
944
|
-
AND e.content_hash = m.hash
|
|
945
|
-
AND m.agent_id = ?
|
|
946
|
-
AND m.vitality >= ?`
|
|
947
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1001
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1002
|
+
).all(...params);
|
|
948
1003
|
const scored = rows.map((row) => ({
|
|
949
1004
|
provider_id: row.provider_id,
|
|
950
1005
|
memory: {
|
|
@@ -1019,10 +1074,12 @@ function createMemory(db, input) {
|
|
|
1019
1074
|
}
|
|
1020
1075
|
const id = newId();
|
|
1021
1076
|
const timestamp = now();
|
|
1077
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1022
1078
|
db.prepare(
|
|
1023
1079
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1024
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1025
|
-
|
|
1080
|
+
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag,
|
|
1081
|
+
source_session, source_context, observed_at)
|
|
1082
|
+
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1026
1083
|
).run(
|
|
1027
1084
|
id,
|
|
1028
1085
|
input.content,
|
|
@@ -1035,7 +1092,10 @@ function createMemory(db, input) {
|
|
|
1035
1092
|
input.source ?? null,
|
|
1036
1093
|
agentId,
|
|
1037
1094
|
hash2,
|
|
1038
|
-
input.emotion_tag ?? null
|
|
1095
|
+
input.emotion_tag ?? null,
|
|
1096
|
+
input.source_session ?? null,
|
|
1097
|
+
sourceContext,
|
|
1098
|
+
input.observed_at ?? null
|
|
1039
1099
|
);
|
|
1040
1100
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1041
1101
|
markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -15129,16 +15189,25 @@ function searchBM25(db, query, opts) {
|
|
|
15129
15189
|
const ftsQuery = buildFtsQuery(query);
|
|
15130
15190
|
if (!ftsQuery) return [];
|
|
15131
15191
|
try {
|
|
15192
|
+
const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
|
|
15193
|
+
const params = [ftsQuery, agentId, minVitality];
|
|
15194
|
+
if (opts?.after) {
|
|
15195
|
+
conditions.push("m.updated_at >= ?");
|
|
15196
|
+
params.push(opts.after);
|
|
15197
|
+
}
|
|
15198
|
+
if (opts?.before) {
|
|
15199
|
+
conditions.push("m.updated_at <= ?");
|
|
15200
|
+
params.push(opts.before);
|
|
15201
|
+
}
|
|
15202
|
+
params.push(limit);
|
|
15132
15203
|
const rows = db.prepare(
|
|
15133
15204
|
`SELECT m.*, rank AS score
|
|
15134
15205
|
FROM memories_fts f
|
|
15135
15206
|
JOIN memories m ON m.id = f.id
|
|
15136
|
-
WHERE
|
|
15137
|
-
AND m.agent_id = ?
|
|
15138
|
-
AND m.vitality >= ?
|
|
15207
|
+
WHERE ${conditions.join(" AND ")}
|
|
15139
15208
|
ORDER BY rank
|
|
15140
15209
|
LIMIT ?`
|
|
15141
|
-
).all(
|
|
15210
|
+
).all(...params);
|
|
15142
15211
|
return rows.map((row, index) => {
|
|
15143
15212
|
const { score: _score, ...memoryFields } = row;
|
|
15144
15213
|
return {
|
|
@@ -15224,16 +15293,23 @@ function priorityPrior(priority) {
|
|
|
15224
15293
|
function fusionScore(input) {
|
|
15225
15294
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
15226
15295
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15296
|
+
const baseScore = lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
15297
|
+
const boost = input.recency_boost ?? 0;
|
|
15298
|
+
if (boost <= 0) return baseScore;
|
|
15299
|
+
const updatedAt = new Date(input.memory.updated_at).getTime();
|
|
15300
|
+
const daysSince = Math.max(0, (Date.now() - updatedAt) / (1e3 * 60 * 60 * 24));
|
|
15301
|
+
const recencyScore = Math.exp(-daysSince / 30);
|
|
15302
|
+
return (1 - boost) * baseScore + boost * recencyScore;
|
|
15303
|
+
}
|
|
15304
|
+
function fuseHybridResults(lexical, vector, limit, recency_boost) {
|
|
15230
15305
|
const candidates = /* @__PURE__ */ new Map();
|
|
15231
15306
|
for (const row of lexical) {
|
|
15232
15307
|
candidates.set(row.memory.id, {
|
|
15233
15308
|
memory: row.memory,
|
|
15234
15309
|
score: 0,
|
|
15235
15310
|
bm25_rank: row.rank,
|
|
15236
|
-
bm25_score: row.score
|
|
15311
|
+
bm25_score: row.score,
|
|
15312
|
+
match_type: "direct"
|
|
15237
15313
|
});
|
|
15238
15314
|
}
|
|
15239
15315
|
for (const row of vector) {
|
|
@@ -15246,13 +15322,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
15246
15322
|
memory: row.memory,
|
|
15247
15323
|
score: 0,
|
|
15248
15324
|
vector_rank: row.rank,
|
|
15249
|
-
vector_score: row.similarity
|
|
15325
|
+
vector_score: row.similarity,
|
|
15326
|
+
match_type: "direct"
|
|
15250
15327
|
});
|
|
15251
15328
|
}
|
|
15252
15329
|
}
|
|
15253
15330
|
return [...candidates.values()].map((row) => ({
|
|
15254
15331
|
...row,
|
|
15255
|
-
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
15332
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
|
|
15256
15333
|
})).sort((left, right) => {
|
|
15257
15334
|
if (right.score !== left.score) return right.score - left.score;
|
|
15258
15335
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -15265,9 +15342,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
15265
15342
|
providerId: opts.provider.id,
|
|
15266
15343
|
agent_id: opts.agent_id,
|
|
15267
15344
|
limit: opts.limit,
|
|
15268
|
-
min_vitality: opts.min_vitality
|
|
15345
|
+
min_vitality: opts.min_vitality,
|
|
15346
|
+
after: opts.after,
|
|
15347
|
+
before: opts.before
|
|
15269
15348
|
});
|
|
15270
15349
|
}
|
|
15350
|
+
function expandRelated(db, results, agentId, maxTotal) {
|
|
15351
|
+
const existingIds = new Set(results.map((r) => r.memory.id));
|
|
15352
|
+
const related = [];
|
|
15353
|
+
for (const result of results) {
|
|
15354
|
+
const links = db.prepare(
|
|
15355
|
+
`SELECT l.target_id, l.weight, m.*
|
|
15356
|
+
FROM links l
|
|
15357
|
+
JOIN memories m ON m.id = l.target_id
|
|
15358
|
+
WHERE l.agent_id = ? AND l.source_id = ?
|
|
15359
|
+
ORDER BY l.weight DESC
|
|
15360
|
+
LIMIT 5`
|
|
15361
|
+
).all(agentId, result.memory.id);
|
|
15362
|
+
for (const link of links) {
|
|
15363
|
+
if (existingIds.has(link.target_id)) continue;
|
|
15364
|
+
existingIds.add(link.target_id);
|
|
15365
|
+
const relatedMemory = {
|
|
15366
|
+
id: link.id,
|
|
15367
|
+
content: link.content,
|
|
15368
|
+
type: link.type,
|
|
15369
|
+
priority: link.priority,
|
|
15370
|
+
emotion_val: link.emotion_val,
|
|
15371
|
+
vitality: link.vitality,
|
|
15372
|
+
stability: link.stability,
|
|
15373
|
+
access_count: link.access_count,
|
|
15374
|
+
last_accessed: link.last_accessed,
|
|
15375
|
+
created_at: link.created_at,
|
|
15376
|
+
updated_at: link.updated_at,
|
|
15377
|
+
source: link.source,
|
|
15378
|
+
agent_id: link.agent_id,
|
|
15379
|
+
hash: link.hash,
|
|
15380
|
+
emotion_tag: link.emotion_tag,
|
|
15381
|
+
source_session: link.source_session ?? null,
|
|
15382
|
+
source_context: link.source_context ?? null,
|
|
15383
|
+
observed_at: link.observed_at ?? null
|
|
15384
|
+
};
|
|
15385
|
+
related.push({
|
|
15386
|
+
memory: relatedMemory,
|
|
15387
|
+
score: result.score * link.weight * 0.6,
|
|
15388
|
+
related_source_id: result.memory.id,
|
|
15389
|
+
match_type: "related"
|
|
15390
|
+
});
|
|
15391
|
+
}
|
|
15392
|
+
}
|
|
15393
|
+
const directResults = results.map((r) => ({
|
|
15394
|
+
...r,
|
|
15395
|
+
match_type: "direct"
|
|
15396
|
+
}));
|
|
15397
|
+
const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
|
|
15398
|
+
return combined;
|
|
15399
|
+
}
|
|
15271
15400
|
async function recallMemories(db, query, opts) {
|
|
15272
15401
|
const limit = opts?.limit ?? 10;
|
|
15273
15402
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -15275,10 +15404,15 @@ async function recallMemories(db, query, opts) {
|
|
|
15275
15404
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
15276
15405
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
15277
15406
|
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
15407
|
+
const recencyBoost = opts?.recency_boost;
|
|
15408
|
+
const after = opts?.after;
|
|
15409
|
+
const before = opts?.before;
|
|
15278
15410
|
const lexical = searchBM25(db, query, {
|
|
15279
15411
|
agent_id: agentId,
|
|
15280
15412
|
limit: lexicalLimit,
|
|
15281
|
-
min_vitality: minVitality
|
|
15413
|
+
min_vitality: minVitality,
|
|
15414
|
+
after,
|
|
15415
|
+
before
|
|
15282
15416
|
});
|
|
15283
15417
|
let vector = [];
|
|
15284
15418
|
if (provider) {
|
|
@@ -15287,17 +15421,25 @@ async function recallMemories(db, query, opts) {
|
|
|
15287
15421
|
provider,
|
|
15288
15422
|
agent_id: agentId,
|
|
15289
15423
|
limit: vectorLimit,
|
|
15290
|
-
min_vitality: minVitality
|
|
15424
|
+
min_vitality: minVitality,
|
|
15425
|
+
after,
|
|
15426
|
+
before
|
|
15291
15427
|
});
|
|
15292
15428
|
} catch {
|
|
15293
15429
|
vector = [];
|
|
15294
15430
|
}
|
|
15295
15431
|
}
|
|
15296
15432
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
15297
|
-
|
|
15433
|
+
let results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit, recencyBoost);
|
|
15434
|
+
if (opts?.related) {
|
|
15435
|
+
const maxTotal = Math.floor(limit * 1.5);
|
|
15436
|
+
results = expandRelated(db, results, agentId, maxTotal);
|
|
15437
|
+
}
|
|
15298
15438
|
if (opts?.recordAccess !== false) {
|
|
15299
15439
|
for (const row of results) {
|
|
15300
|
-
|
|
15440
|
+
if (row.match_type !== "related") {
|
|
15441
|
+
recordAccess(db, row.memory.id);
|
|
15442
|
+
}
|
|
15301
15443
|
}
|
|
15302
15444
|
}
|
|
15303
15445
|
return {
|
|
@@ -15522,7 +15664,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
15522
15664
|
}
|
|
15523
15665
|
return 0.2;
|
|
15524
15666
|
}
|
|
15525
|
-
function extractObservedAt(parts, fallback) {
|
|
15667
|
+
function extractObservedAt(parts, fallback, observedAt) {
|
|
15668
|
+
if (observedAt) {
|
|
15669
|
+
const parsed = new Date(observedAt);
|
|
15670
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
15671
|
+
return parsed;
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15526
15674
|
for (const part of parts) {
|
|
15527
15675
|
if (!part) continue;
|
|
15528
15676
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -15545,8 +15693,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
15545
15693
|
if (input.type !== "event") {
|
|
15546
15694
|
return 1;
|
|
15547
15695
|
}
|
|
15548
|
-
const inputTime = extractObservedAt(
|
|
15549
|
-
|
|
15696
|
+
const inputTime = extractObservedAt(
|
|
15697
|
+
[input.uri, input.source, input.content],
|
|
15698
|
+
input.now ?? null,
|
|
15699
|
+
input.observed_at ?? null
|
|
15700
|
+
);
|
|
15701
|
+
const existingTime = extractObservedAt(
|
|
15702
|
+
[candidateUri, memory.source, memory.content],
|
|
15703
|
+
memory.created_at,
|
|
15704
|
+
memory.observed_at ?? null
|
|
15705
|
+
);
|
|
15550
15706
|
if (!inputTime || !existingTime) {
|
|
15551
15707
|
return 0.5;
|
|
15552
15708
|
}
|
|
@@ -15630,6 +15786,65 @@ function fourCriterionGate(input) {
|
|
|
15630
15786
|
failedCriteria: failed
|
|
15631
15787
|
};
|
|
15632
15788
|
}
|
|
15789
|
+
var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
|
|
15790
|
+
var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
|
|
15791
|
+
var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
|
|
15792
|
+
function extractNumbers(text) {
|
|
15793
|
+
return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
|
|
15794
|
+
}
|
|
15795
|
+
function detectConflict(inputContent, candidateContent, candidateId) {
|
|
15796
|
+
let conflictScore = 0;
|
|
15797
|
+
let conflictType = "negation";
|
|
15798
|
+
const details = [];
|
|
15799
|
+
const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
|
|
15800
|
+
const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
|
|
15801
|
+
if (inputHasNegation !== candidateHasNegation) {
|
|
15802
|
+
conflictScore += 0.4;
|
|
15803
|
+
conflictType = "negation";
|
|
15804
|
+
details.push("negation mismatch");
|
|
15805
|
+
}
|
|
15806
|
+
const inputNumbers = extractNumbers(inputContent);
|
|
15807
|
+
const candidateNumbers = extractNumbers(candidateContent);
|
|
15808
|
+
if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
|
|
15809
|
+
const inputSet = new Set(inputNumbers);
|
|
15810
|
+
const candidateSet = new Set(candidateNumbers);
|
|
15811
|
+
const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
|
|
15812
|
+
const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
|
|
15813
|
+
if (hasDiff && !hasCommon) {
|
|
15814
|
+
conflictScore += 0.3;
|
|
15815
|
+
conflictType = "value";
|
|
15816
|
+
details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
|
|
15817
|
+
}
|
|
15818
|
+
}
|
|
15819
|
+
const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
|
|
15820
|
+
const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
|
|
15821
|
+
const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
|
|
15822
|
+
const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
|
|
15823
|
+
if (inputDone && candidateActive || inputActive && candidateDone) {
|
|
15824
|
+
conflictScore += 0.3;
|
|
15825
|
+
conflictType = "status";
|
|
15826
|
+
details.push("status contradiction (done vs active)");
|
|
15827
|
+
}
|
|
15828
|
+
if (conflictScore <= 0.5) return null;
|
|
15829
|
+
return {
|
|
15830
|
+
memoryId: candidateId,
|
|
15831
|
+
content: candidateContent,
|
|
15832
|
+
conflict_score: Math.min(1, conflictScore),
|
|
15833
|
+
conflict_type: conflictType,
|
|
15834
|
+
detail: details.join("; ")
|
|
15835
|
+
};
|
|
15836
|
+
}
|
|
15837
|
+
function detectConflicts(inputContent, candidates) {
|
|
15838
|
+
const conflicts = [];
|
|
15839
|
+
for (const candidate of candidates) {
|
|
15840
|
+
if (candidate.score.dedup_score < 0.6) continue;
|
|
15841
|
+
const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
|
|
15842
|
+
if (conflict) {
|
|
15843
|
+
conflicts.push(conflict);
|
|
15844
|
+
}
|
|
15845
|
+
}
|
|
15846
|
+
return conflicts;
|
|
15847
|
+
}
|
|
15633
15848
|
async function guard(db, input) {
|
|
15634
15849
|
const hash2 = contentHash(input.content);
|
|
15635
15850
|
const agentId = input.agent_id ?? "default";
|
|
@@ -15656,18 +15871,37 @@ async function guard(db, input) {
|
|
|
15656
15871
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
15657
15872
|
}
|
|
15658
15873
|
const candidates = await recallCandidates(db, input, agentId);
|
|
15874
|
+
const candidatesList = candidates.map((c) => ({
|
|
15875
|
+
memoryId: c.result.memory.id,
|
|
15876
|
+
dedup_score: c.score.dedup_score
|
|
15877
|
+
}));
|
|
15878
|
+
const conflicts = detectConflicts(input.content, candidates);
|
|
15659
15879
|
const best = candidates[0];
|
|
15660
15880
|
if (!best) {
|
|
15661
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
15881
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
15662
15882
|
}
|
|
15663
15883
|
const score = best.score;
|
|
15664
15884
|
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
15885
|
+
const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
|
|
15886
|
+
if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
|
|
15887
|
+
return {
|
|
15888
|
+
action: "update",
|
|
15889
|
+
reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
|
|
15890
|
+
existingId: best.result.memory.id,
|
|
15891
|
+
updatedContent: input.content,
|
|
15892
|
+
score,
|
|
15893
|
+
candidates: candidatesList,
|
|
15894
|
+
conflicts
|
|
15895
|
+
};
|
|
15896
|
+
}
|
|
15665
15897
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
15666
15898
|
return {
|
|
15667
15899
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
15668
15900
|
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
15669
15901
|
existingId: best.result.memory.id,
|
|
15670
|
-
score
|
|
15902
|
+
score,
|
|
15903
|
+
candidates: candidatesList,
|
|
15904
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15671
15905
|
};
|
|
15672
15906
|
}
|
|
15673
15907
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -15685,17 +15919,22 @@ async function guard(db, input) {
|
|
|
15685
15919
|
existingId: best.result.memory.id,
|
|
15686
15920
|
mergedContent: mergePlan.content,
|
|
15687
15921
|
mergePlan,
|
|
15688
|
-
score
|
|
15922
|
+
score,
|
|
15923
|
+
candidates: candidatesList,
|
|
15924
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15689
15925
|
};
|
|
15690
15926
|
}
|
|
15691
15927
|
return {
|
|
15692
15928
|
action: "add",
|
|
15693
15929
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
15694
|
-
score
|
|
15930
|
+
score,
|
|
15931
|
+
candidates: candidatesList,
|
|
15932
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15695
15933
|
};
|
|
15696
15934
|
}
|
|
15697
15935
|
|
|
15698
15936
|
// src/sleep/sync.ts
|
|
15937
|
+
init_db();
|
|
15699
15938
|
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
15700
15939
|
if (!uri) return;
|
|
15701
15940
|
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
@@ -15704,6 +15943,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
15704
15943
|
} catch {
|
|
15705
15944
|
}
|
|
15706
15945
|
}
|
|
15946
|
+
function createAutoLinks(db, memoryId, candidates, agentId) {
|
|
15947
|
+
if (!candidates || candidates.length === 0) return;
|
|
15948
|
+
const linkCandidates = candidates.filter((c) => c.memoryId !== memoryId && c.dedup_score >= 0.45 && c.dedup_score < 0.82).sort((a, b) => b.dedup_score - a.dedup_score).slice(0, 5);
|
|
15949
|
+
if (linkCandidates.length === 0) return;
|
|
15950
|
+
const timestamp = now();
|
|
15951
|
+
const insert = db.prepare(
|
|
15952
|
+
`INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
15953
|
+
VALUES (?, ?, ?, 'related', ?, ?)`
|
|
15954
|
+
);
|
|
15955
|
+
for (const candidate of linkCandidates) {
|
|
15956
|
+
insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
|
|
15957
|
+
insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
|
|
15958
|
+
}
|
|
15959
|
+
}
|
|
15707
15960
|
async function syncOne(db, input) {
|
|
15708
15961
|
const memInput = {
|
|
15709
15962
|
content: input.content,
|
|
@@ -15715,17 +15968,22 @@ async function syncOne(db, input) {
|
|
|
15715
15968
|
uri: input.uri,
|
|
15716
15969
|
provider: input.provider,
|
|
15717
15970
|
conservative: input.conservative,
|
|
15718
|
-
emotion_tag: input.emotion_tag
|
|
15971
|
+
emotion_tag: input.emotion_tag,
|
|
15972
|
+
source_session: input.source_session,
|
|
15973
|
+
source_context: input.source_context,
|
|
15974
|
+
observed_at: input.observed_at
|
|
15719
15975
|
};
|
|
15720
15976
|
const guardResult = await guard(db, memInput);
|
|
15977
|
+
const agentId = input.agent_id ?? "default";
|
|
15721
15978
|
switch (guardResult.action) {
|
|
15722
15979
|
case "skip":
|
|
15723
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
15980
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
15724
15981
|
case "add": {
|
|
15725
15982
|
const mem = createMemory(db, memInput);
|
|
15726
15983
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
15727
15984
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
15728
|
-
|
|
15985
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
15986
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15729
15987
|
}
|
|
15730
15988
|
case "update": {
|
|
15731
15989
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -15733,7 +15991,7 @@ async function syncOne(db, input) {
|
|
|
15733
15991
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
15734
15992
|
}
|
|
15735
15993
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15736
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
15994
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15737
15995
|
}
|
|
15738
15996
|
case "merge": {
|
|
15739
15997
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -15741,7 +15999,8 @@ async function syncOne(db, input) {
|
|
|
15741
15999
|
}
|
|
15742
16000
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
15743
16001
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15744
|
-
|
|
16002
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
16003
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15745
16004
|
}
|
|
15746
16005
|
}
|
|
15747
16006
|
}
|
|
@@ -16022,33 +16281,13 @@ async function rememberMemory(db, input) {
|
|
|
16022
16281
|
agent_id: input.agent_id,
|
|
16023
16282
|
provider: input.provider,
|
|
16024
16283
|
conservative: input.conservative,
|
|
16025
|
-
emotion_tag: input.emotion_tag
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
// src/app/recall.ts
|
|
16030
|
-
async function recallMemory(db, input) {
|
|
16031
|
-
const result = await recallMemories(db, input.query, {
|
|
16032
|
-
agent_id: input.agent_id,
|
|
16033
|
-
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
16034
|
-
min_vitality: input.min_vitality,
|
|
16035
|
-
lexicalLimit: input.lexicalLimit,
|
|
16036
|
-
vectorLimit: input.vectorLimit,
|
|
16037
|
-
provider: input.provider,
|
|
16038
|
-
recordAccess: input.recordAccess
|
|
16284
|
+
emotion_tag: input.emotion_tag,
|
|
16285
|
+
source_session: input.source_session,
|
|
16286
|
+
source_context: input.source_context,
|
|
16287
|
+
observed_at: input.observed_at
|
|
16039
16288
|
});
|
|
16040
|
-
if (input.emotion_tag) {
|
|
16041
|
-
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
16042
|
-
}
|
|
16043
|
-
return result;
|
|
16044
16289
|
}
|
|
16045
16290
|
|
|
16046
|
-
// src/app/surface.ts
|
|
16047
|
-
init_memory();
|
|
16048
|
-
init_providers();
|
|
16049
|
-
init_tokenizer();
|
|
16050
|
-
init_vector();
|
|
16051
|
-
|
|
16052
16291
|
// src/app/feedback.ts
|
|
16053
16292
|
init_db();
|
|
16054
16293
|
function clamp012(value) {
|
|
@@ -16094,8 +16333,76 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
16094
16333
|
};
|
|
16095
16334
|
}
|
|
16096
16335
|
}
|
|
16336
|
+
function recordPassiveFeedback(db, memoryIds, agentId) {
|
|
16337
|
+
if (memoryIds.length === 0) return 0;
|
|
16338
|
+
const effectiveAgentId = agentId ?? "default";
|
|
16339
|
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
16340
|
+
const placeholders = memoryIds.map(() => "?").join(",");
|
|
16341
|
+
const recentCounts = /* @__PURE__ */ new Map();
|
|
16342
|
+
try {
|
|
16343
|
+
const rows = db.prepare(
|
|
16344
|
+
`SELECT memory_id, COUNT(*) as c
|
|
16345
|
+
FROM feedback_events
|
|
16346
|
+
WHERE memory_id IN (${placeholders})
|
|
16347
|
+
AND source = 'passive'
|
|
16348
|
+
AND created_at > ?
|
|
16349
|
+
GROUP BY memory_id`
|
|
16350
|
+
).all(...memoryIds, cutoff);
|
|
16351
|
+
for (const row of rows) {
|
|
16352
|
+
recentCounts.set(row.memory_id, row.c);
|
|
16353
|
+
}
|
|
16354
|
+
} catch {
|
|
16355
|
+
}
|
|
16356
|
+
let recorded = 0;
|
|
16357
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
16358
|
+
const insert = db.prepare(
|
|
16359
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
16360
|
+
VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
|
|
16361
|
+
);
|
|
16362
|
+
for (const memoryId of memoryIds) {
|
|
16363
|
+
const count = recentCounts.get(memoryId) ?? 0;
|
|
16364
|
+
if (count >= 3) continue;
|
|
16365
|
+
try {
|
|
16366
|
+
insert.run(newId(), memoryId, effectiveAgentId, timestamp);
|
|
16367
|
+
recorded++;
|
|
16368
|
+
} catch {
|
|
16369
|
+
}
|
|
16370
|
+
}
|
|
16371
|
+
return recorded;
|
|
16372
|
+
}
|
|
16373
|
+
|
|
16374
|
+
// src/app/recall.ts
|
|
16375
|
+
async function recallMemory(db, input) {
|
|
16376
|
+
const result = await recallMemories(db, input.query, {
|
|
16377
|
+
agent_id: input.agent_id,
|
|
16378
|
+
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
16379
|
+
min_vitality: input.min_vitality,
|
|
16380
|
+
lexicalLimit: input.lexicalLimit,
|
|
16381
|
+
vectorLimit: input.vectorLimit,
|
|
16382
|
+
provider: input.provider,
|
|
16383
|
+
recordAccess: input.recordAccess,
|
|
16384
|
+
related: input.related,
|
|
16385
|
+
after: input.after,
|
|
16386
|
+
before: input.before,
|
|
16387
|
+
recency_boost: input.recency_boost
|
|
16388
|
+
});
|
|
16389
|
+
if (input.emotion_tag) {
|
|
16390
|
+
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
16391
|
+
}
|
|
16392
|
+
if (input.recordAccess !== false) {
|
|
16393
|
+
const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
|
|
16394
|
+
if (top3DirectIds.length > 0) {
|
|
16395
|
+
recordPassiveFeedback(db, top3DirectIds, input.agent_id);
|
|
16396
|
+
}
|
|
16397
|
+
}
|
|
16398
|
+
return result;
|
|
16399
|
+
}
|
|
16097
16400
|
|
|
16098
16401
|
// src/app/surface.ts
|
|
16402
|
+
init_memory();
|
|
16403
|
+
init_providers();
|
|
16404
|
+
init_tokenizer();
|
|
16405
|
+
init_vector();
|
|
16099
16406
|
var INTENT_PRIORS = {
|
|
16100
16407
|
factual: {
|
|
16101
16408
|
identity: 0.25,
|
|
@@ -16235,7 +16542,9 @@ async function surfaceMemories(db, input) {
|
|
|
16235
16542
|
searchBM25(db, trimmedQuery, {
|
|
16236
16543
|
agent_id: agentId,
|
|
16237
16544
|
limit: lexicalWindow,
|
|
16238
|
-
min_vitality: minVitality
|
|
16545
|
+
min_vitality: minVitality,
|
|
16546
|
+
after: input.after,
|
|
16547
|
+
before: input.before
|
|
16239
16548
|
}),
|
|
16240
16549
|
"queryRank"
|
|
16241
16550
|
);
|
|
@@ -16246,7 +16555,9 @@ async function surfaceMemories(db, input) {
|
|
|
16246
16555
|
searchBM25(db, trimmedTask, {
|
|
16247
16556
|
agent_id: agentId,
|
|
16248
16557
|
limit: lexicalWindow,
|
|
16249
|
-
min_vitality: minVitality
|
|
16558
|
+
min_vitality: minVitality,
|
|
16559
|
+
after: input.after,
|
|
16560
|
+
before: input.before
|
|
16250
16561
|
}),
|
|
16251
16562
|
"taskRank"
|
|
16252
16563
|
);
|
|
@@ -16257,7 +16568,9 @@ async function surfaceMemories(db, input) {
|
|
|
16257
16568
|
searchBM25(db, recentTurns.join(" "), {
|
|
16258
16569
|
agent_id: agentId,
|
|
16259
16570
|
limit: lexicalWindow,
|
|
16260
|
-
min_vitality: minVitality
|
|
16571
|
+
min_vitality: minVitality,
|
|
16572
|
+
after: input.after,
|
|
16573
|
+
before: input.before
|
|
16261
16574
|
}),
|
|
16262
16575
|
"recentRank"
|
|
16263
16576
|
);
|
|
@@ -16271,7 +16584,9 @@ async function surfaceMemories(db, input) {
|
|
|
16271
16584
|
providerId: provider.id,
|
|
16272
16585
|
agent_id: agentId,
|
|
16273
16586
|
limit: lexicalWindow,
|
|
16274
|
-
min_vitality: minVitality
|
|
16587
|
+
min_vitality: minVitality,
|
|
16588
|
+
after: input.after,
|
|
16589
|
+
before: input.before
|
|
16275
16590
|
});
|
|
16276
16591
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
16277
16592
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -16409,19 +16724,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
16409
16724
|
|
|
16410
16725
|
// src/sleep/tidy.ts
|
|
16411
16726
|
init_memory();
|
|
16727
|
+
init_db();
|
|
16728
|
+
var EVENT_STALE_PATTERNS = [
|
|
16729
|
+
{ pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
|
|
16730
|
+
{ pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
16731
|
+
{ pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
16732
|
+
];
|
|
16733
|
+
var KNOWLEDGE_STALE_PATTERNS = [
|
|
16734
|
+
{ pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
16735
|
+
{ pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
16736
|
+
];
|
|
16737
|
+
function isStaleContent(content, type) {
|
|
16738
|
+
if (type === "identity" || type === "emotion") {
|
|
16739
|
+
return { stale: false, reason: "type excluded", decay_factor: 1 };
|
|
16740
|
+
}
|
|
16741
|
+
const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
|
|
16742
|
+
for (const { pattern, type: staleType, decay } of patterns) {
|
|
16743
|
+
if (pattern.test(content)) {
|
|
16744
|
+
return { stale: true, reason: staleType, decay_factor: decay };
|
|
16745
|
+
}
|
|
16746
|
+
}
|
|
16747
|
+
return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
|
|
16748
|
+
}
|
|
16749
|
+
function getAgeThresholdDays(staleType) {
|
|
16750
|
+
const thresholds = {
|
|
16751
|
+
in_progress: 7,
|
|
16752
|
+
pending: 14,
|
|
16753
|
+
ephemeral: 3
|
|
16754
|
+
};
|
|
16755
|
+
return thresholds[staleType] ?? 7;
|
|
16756
|
+
}
|
|
16412
16757
|
function runTidy(db, opts) {
|
|
16413
16758
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
16414
16759
|
const agentId = opts?.agent_id;
|
|
16415
16760
|
let archived = 0;
|
|
16761
|
+
let staleDecayed = 0;
|
|
16416
16762
|
const transaction = db.transaction(() => {
|
|
16417
16763
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
16418
16764
|
for (const mem of decayed) {
|
|
16419
16765
|
deleteMemory(db, mem.id);
|
|
16420
16766
|
archived += 1;
|
|
16421
16767
|
}
|
|
16768
|
+
const currentMs = Date.now();
|
|
16769
|
+
const currentTime = now();
|
|
16770
|
+
const agentCondition = agentId ? "AND agent_id = ?" : "";
|
|
16771
|
+
const agentParams = agentId ? [agentId] : [];
|
|
16772
|
+
const candidates = db.prepare(
|
|
16773
|
+
`SELECT id, content, type, created_at, updated_at, vitality
|
|
16774
|
+
FROM memories
|
|
16775
|
+
WHERE priority >= 2 AND vitality >= ?
|
|
16776
|
+
${agentCondition}`
|
|
16777
|
+
).all(threshold, ...agentParams);
|
|
16778
|
+
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
16779
|
+
for (const mem of candidates) {
|
|
16780
|
+
const detection = isStaleContent(mem.content, mem.type);
|
|
16781
|
+
if (!detection.stale) continue;
|
|
16782
|
+
const createdMs = new Date(mem.created_at).getTime();
|
|
16783
|
+
const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
|
|
16784
|
+
const thresholdDays = getAgeThresholdDays(detection.reason);
|
|
16785
|
+
if (ageDays < thresholdDays) continue;
|
|
16786
|
+
const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
|
|
16787
|
+
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
16788
|
+
updateStmt.run(newVitality, currentTime, mem.id);
|
|
16789
|
+
staleDecayed += 1;
|
|
16790
|
+
}
|
|
16791
|
+
}
|
|
16422
16792
|
});
|
|
16423
16793
|
transaction();
|
|
16424
|
-
return { archived, orphansCleaned: 0 };
|
|
16794
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
16425
16795
|
}
|
|
16426
16796
|
|
|
16427
16797
|
// src/sleep/govern.ts
|
|
@@ -16830,7 +17200,10 @@ function formatMemory(memory, score) {
|
|
|
16830
17200
|
priority: memory.priority,
|
|
16831
17201
|
vitality: memory.vitality,
|
|
16832
17202
|
score,
|
|
16833
|
-
updated_at: memory.updated_at
|
|
17203
|
+
updated_at: memory.updated_at,
|
|
17204
|
+
source_session: memory.source_session ?? void 0,
|
|
17205
|
+
source_context: memory.source_context ?? void 0,
|
|
17206
|
+
observed_at: memory.observed_at ?? void 0
|
|
16834
17207
|
};
|
|
16835
17208
|
}
|
|
16836
17209
|
function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
|
|
@@ -16917,7 +17290,9 @@ function formatRecallPayload(result) {
|
|
|
16917
17290
|
bm25_rank: row.bm25_rank,
|
|
16918
17291
|
vector_rank: row.vector_rank,
|
|
16919
17292
|
bm25_score: row.bm25_score,
|
|
16920
|
-
vector_score: row.vector_score
|
|
17293
|
+
vector_score: row.vector_score,
|
|
17294
|
+
related_source_id: row.related_source_id,
|
|
17295
|
+
match_type: row.match_type
|
|
16921
17296
|
}))
|
|
16922
17297
|
};
|
|
16923
17298
|
}
|
|
@@ -16940,7 +17315,10 @@ function formatSurfacePayload(result) {
|
|
|
16940
17315
|
priority_prior: row.priority_prior,
|
|
16941
17316
|
feedback_score: row.feedback_score,
|
|
16942
17317
|
reason_codes: row.reason_codes,
|
|
16943
|
-
updated_at: row.memory.updated_at
|
|
17318
|
+
updated_at: row.memory.updated_at,
|
|
17319
|
+
source_session: row.memory.source_session ?? void 0,
|
|
17320
|
+
source_context: row.memory.source_context ?? void 0,
|
|
17321
|
+
observed_at: row.memory.observed_at ?? void 0
|
|
16944
17322
|
}))
|
|
16945
17323
|
};
|
|
16946
17324
|
}
|
|
@@ -16949,7 +17327,7 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16949
17327
|
const aid = agentId ?? AGENT_ID;
|
|
16950
17328
|
const server = new McpServer({
|
|
16951
17329
|
name: "agent-memory",
|
|
16952
|
-
version: "
|
|
17330
|
+
version: "5.0.0"
|
|
16953
17331
|
});
|
|
16954
17332
|
server.tool(
|
|
16955
17333
|
"remember",
|
|
@@ -16961,10 +17339,24 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16961
17339
|
emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
|
|
16962
17340
|
source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
|
|
16963
17341
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
16964
|
-
emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)")
|
|
17342
|
+
emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)"),
|
|
17343
|
+
session_id: external_exports.string().optional().describe("Source session ID for provenance tracking"),
|
|
17344
|
+
context: external_exports.string().optional().describe("Trigger context for this memory (\u2264200 chars, auto-truncated)"),
|
|
17345
|
+
observed_at: external_exports.string().optional().describe("When the event actually happened (ISO 8601), distinct from write time")
|
|
16965
17346
|
},
|
|
16966
|
-
async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag }) => {
|
|
16967
|
-
const result = await rememberMemory(db, {
|
|
17347
|
+
async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag, session_id, context, observed_at }) => {
|
|
17348
|
+
const result = await rememberMemory(db, {
|
|
17349
|
+
content,
|
|
17350
|
+
type,
|
|
17351
|
+
uri,
|
|
17352
|
+
emotion_val,
|
|
17353
|
+
source,
|
|
17354
|
+
agent_id: agent_id ?? aid,
|
|
17355
|
+
emotion_tag,
|
|
17356
|
+
source_session: session_id,
|
|
17357
|
+
source_context: context,
|
|
17358
|
+
observed_at
|
|
17359
|
+
});
|
|
16968
17360
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
16969
17361
|
}
|
|
16970
17362
|
);
|
|
@@ -16975,10 +17367,23 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16975
17367
|
query: external_exports.string().describe("Search query (natural language)"),
|
|
16976
17368
|
limit: external_exports.number().default(10).describe("Max results to return"),
|
|
16977
17369
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
16978
|
-
emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)")
|
|
17370
|
+
emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)"),
|
|
17371
|
+
related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
|
|
17372
|
+
after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
|
|
17373
|
+
before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
|
|
17374
|
+
recency_boost: external_exports.number().min(0).max(1).default(0).optional().describe("Recency bias (0=none, 1=max). Higher values favor recently updated memories")
|
|
16979
17375
|
},
|
|
16980
|
-
async ({ query, limit, agent_id, emotion_tag }) => {
|
|
16981
|
-
const result = await recallMemory(db, {
|
|
17376
|
+
async ({ query, limit, agent_id, emotion_tag, related, after, before, recency_boost }) => {
|
|
17377
|
+
const result = await recallMemory(db, {
|
|
17378
|
+
query,
|
|
17379
|
+
limit,
|
|
17380
|
+
agent_id: agent_id ?? aid,
|
|
17381
|
+
emotion_tag,
|
|
17382
|
+
related: related ?? false,
|
|
17383
|
+
after,
|
|
17384
|
+
before,
|
|
17385
|
+
recency_boost: recency_boost ?? 0
|
|
17386
|
+
});
|
|
16982
17387
|
return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
|
|
16983
17388
|
}
|
|
16984
17389
|
);
|
|
@@ -17168,9 +17573,13 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17168
17573
|
types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"]).describe("Optional type filter")).optional(),
|
|
17169
17574
|
limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
|
|
17170
17575
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
17171
|
-
keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted")
|
|
17576
|
+
keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted"),
|
|
17577
|
+
related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
|
|
17578
|
+
after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
|
|
17579
|
+
before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
|
|
17580
|
+
recency_boost: external_exports.number().min(0).max(1).default(0).optional().describe("Recency bias (0=none, 1=max)")
|
|
17172
17581
|
},
|
|
17173
|
-
async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords }) => {
|
|
17582
|
+
async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords, related, after, before, recency_boost }) => {
|
|
17174
17583
|
const resolvedQuery = query ?? keywords?.join(" ");
|
|
17175
17584
|
const result = await surfaceMemories(db, {
|
|
17176
17585
|
query: resolvedQuery,
|
|
@@ -17179,7 +17588,11 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17179
17588
|
intent,
|
|
17180
17589
|
types,
|
|
17181
17590
|
limit: limit ?? 5,
|
|
17182
|
-
agent_id: agent_id ?? aid
|
|
17591
|
+
agent_id: agent_id ?? aid,
|
|
17592
|
+
related: related ?? false,
|
|
17593
|
+
after,
|
|
17594
|
+
before,
|
|
17595
|
+
recency_boost: recency_boost ?? 0
|
|
17183
17596
|
});
|
|
17184
17597
|
return {
|
|
17185
17598
|
content: [{
|
|
@@ -17189,6 +17602,43 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17189
17602
|
};
|
|
17190
17603
|
}
|
|
17191
17604
|
);
|
|
17605
|
+
server.tool(
|
|
17606
|
+
"link",
|
|
17607
|
+
"Manually create or remove an association between two memories.",
|
|
17608
|
+
{
|
|
17609
|
+
source_id: external_exports.string().describe("Source memory ID"),
|
|
17610
|
+
target_id: external_exports.string().describe("Target memory ID"),
|
|
17611
|
+
relation: external_exports.enum(["related", "supersedes", "contradicts"]).default("related").describe("Relation type"),
|
|
17612
|
+
weight: external_exports.number().min(0).max(1).default(1).optional().describe("Link weight (0-1)"),
|
|
17613
|
+
remove: external_exports.boolean().default(false).optional().describe("Remove the link instead of creating it"),
|
|
17614
|
+
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
|
|
17615
|
+
},
|
|
17616
|
+
async ({ source_id, target_id, relation, weight, remove, agent_id }) => {
|
|
17617
|
+
const effectiveAgentId = agent_id ?? aid;
|
|
17618
|
+
if (remove) {
|
|
17619
|
+
const result = db.prepare(
|
|
17620
|
+
"DELETE FROM links WHERE agent_id = ? AND source_id = ? AND target_id = ?"
|
|
17621
|
+
).run(effectiveAgentId, source_id, target_id);
|
|
17622
|
+
return {
|
|
17623
|
+
content: [{ type: "text", text: JSON.stringify({ action: "removed", changes: result.changes }) }]
|
|
17624
|
+
};
|
|
17625
|
+
}
|
|
17626
|
+
const timestamp = now();
|
|
17627
|
+
db.prepare(
|
|
17628
|
+
`INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
17629
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
17630
|
+
).run(effectiveAgentId, source_id, target_id, relation, weight ?? 1, timestamp);
|
|
17631
|
+
return {
|
|
17632
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
17633
|
+
action: "created",
|
|
17634
|
+
source_id,
|
|
17635
|
+
target_id,
|
|
17636
|
+
relation,
|
|
17637
|
+
weight: weight ?? 1
|
|
17638
|
+
}) }]
|
|
17639
|
+
};
|
|
17640
|
+
}
|
|
17641
|
+
);
|
|
17192
17642
|
return { server, db };
|
|
17193
17643
|
}
|
|
17194
17644
|
async function main() {
|