@smyslenny/agent-memory 4.3.0 → 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 +424 -47
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +108 -44
- package/dist/index.js +439 -63
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +537 -82
- 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
|
|
|
@@ -691,7 +734,9 @@ function createLocalHttpEmbeddingProvider(opts) {
|
|
|
691
734
|
};
|
|
692
735
|
}
|
|
693
736
|
function createGeminiEmbeddingProvider(opts) {
|
|
694
|
-
const
|
|
737
|
+
const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
|
|
738
|
+
const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
|
|
739
|
+
const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
|
|
695
740
|
return {
|
|
696
741
|
id,
|
|
697
742
|
model: opts.model,
|
|
@@ -699,7 +744,7 @@ function createGeminiEmbeddingProvider(opts) {
|
|
|
699
744
|
async embed(texts) {
|
|
700
745
|
if (texts.length === 0) return [];
|
|
701
746
|
const fetchFn = getFetch(opts.fetchImpl);
|
|
702
|
-
const url2 =
|
|
747
|
+
const url2 = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
|
|
703
748
|
const requests = texts.map((text) => ({
|
|
704
749
|
model: `models/${opts.model}`,
|
|
705
750
|
content: { parts: [{ text }] },
|
|
@@ -731,9 +776,11 @@ function createGeminiEmbeddingProvider(opts) {
|
|
|
731
776
|
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
732
777
|
return trimTrailingSlashes(baseUrl);
|
|
733
778
|
}
|
|
779
|
+
var GEMINI_DEFAULT_BASE_URL;
|
|
734
780
|
var init_embedding = __esm({
|
|
735
781
|
"src/search/embedding.ts"() {
|
|
736
782
|
"use strict";
|
|
783
|
+
GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
737
784
|
}
|
|
738
785
|
});
|
|
739
786
|
|
|
@@ -789,6 +836,7 @@ function createEmbeddingProvider(input, opts) {
|
|
|
789
836
|
model: normalized.model,
|
|
790
837
|
dimension: normalized.dimension,
|
|
791
838
|
apiKey: normalized.apiKey,
|
|
839
|
+
baseUrl: normalized.baseUrl || void 0,
|
|
792
840
|
fetchImpl: opts?.fetchImpl
|
|
793
841
|
});
|
|
794
842
|
}
|
|
@@ -926,6 +974,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
926
974
|
const limit = opts.limit ?? 20;
|
|
927
975
|
const agentId = opts.agent_id ?? "default";
|
|
928
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
|
+
}
|
|
929
994
|
const rows = db.prepare(
|
|
930
995
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
931
996
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -933,13 +998,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
933
998
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
934
999
|
FROM embeddings e
|
|
935
1000
|
JOIN memories m ON m.id = e.memory_id
|
|
936
|
-
WHERE
|
|
937
|
-
|
|
938
|
-
AND e.vector IS NOT NULL
|
|
939
|
-
AND e.content_hash = m.hash
|
|
940
|
-
AND m.agent_id = ?
|
|
941
|
-
AND m.vitality >= ?`
|
|
942
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1001
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1002
|
+
).all(...params);
|
|
943
1003
|
const scored = rows.map((row) => ({
|
|
944
1004
|
provider_id: row.provider_id,
|
|
945
1005
|
memory: {
|
|
@@ -1014,10 +1074,12 @@ function createMemory(db, input) {
|
|
|
1014
1074
|
}
|
|
1015
1075
|
const id = newId();
|
|
1016
1076
|
const timestamp = now();
|
|
1077
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1017
1078
|
db.prepare(
|
|
1018
1079
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1019
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1020
|
-
|
|
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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1021
1083
|
).run(
|
|
1022
1084
|
id,
|
|
1023
1085
|
input.content,
|
|
@@ -1030,7 +1092,10 @@ function createMemory(db, input) {
|
|
|
1030
1092
|
input.source ?? null,
|
|
1031
1093
|
agentId,
|
|
1032
1094
|
hash2,
|
|
1033
|
-
input.emotion_tag ?? null
|
|
1095
|
+
input.emotion_tag ?? null,
|
|
1096
|
+
input.source_session ?? null,
|
|
1097
|
+
sourceContext,
|
|
1098
|
+
input.observed_at ?? null
|
|
1034
1099
|
);
|
|
1035
1100
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1036
1101
|
markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -15124,16 +15189,25 @@ function searchBM25(db, query, opts) {
|
|
|
15124
15189
|
const ftsQuery = buildFtsQuery(query);
|
|
15125
15190
|
if (!ftsQuery) return [];
|
|
15126
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);
|
|
15127
15203
|
const rows = db.prepare(
|
|
15128
15204
|
`SELECT m.*, rank AS score
|
|
15129
15205
|
FROM memories_fts f
|
|
15130
15206
|
JOIN memories m ON m.id = f.id
|
|
15131
|
-
WHERE
|
|
15132
|
-
AND m.agent_id = ?
|
|
15133
|
-
AND m.vitality >= ?
|
|
15207
|
+
WHERE ${conditions.join(" AND ")}
|
|
15134
15208
|
ORDER BY rank
|
|
15135
15209
|
LIMIT ?`
|
|
15136
|
-
).all(
|
|
15210
|
+
).all(...params);
|
|
15137
15211
|
return rows.map((row, index) => {
|
|
15138
15212
|
const { score: _score, ...memoryFields } = row;
|
|
15139
15213
|
return {
|
|
@@ -15219,16 +15293,23 @@ function priorityPrior(priority) {
|
|
|
15219
15293
|
function fusionScore(input) {
|
|
15220
15294
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
15221
15295
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
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) {
|
|
15225
15305
|
const candidates = /* @__PURE__ */ new Map();
|
|
15226
15306
|
for (const row of lexical) {
|
|
15227
15307
|
candidates.set(row.memory.id, {
|
|
15228
15308
|
memory: row.memory,
|
|
15229
15309
|
score: 0,
|
|
15230
15310
|
bm25_rank: row.rank,
|
|
15231
|
-
bm25_score: row.score
|
|
15311
|
+
bm25_score: row.score,
|
|
15312
|
+
match_type: "direct"
|
|
15232
15313
|
});
|
|
15233
15314
|
}
|
|
15234
15315
|
for (const row of vector) {
|
|
@@ -15241,13 +15322,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
15241
15322
|
memory: row.memory,
|
|
15242
15323
|
score: 0,
|
|
15243
15324
|
vector_rank: row.rank,
|
|
15244
|
-
vector_score: row.similarity
|
|
15325
|
+
vector_score: row.similarity,
|
|
15326
|
+
match_type: "direct"
|
|
15245
15327
|
});
|
|
15246
15328
|
}
|
|
15247
15329
|
}
|
|
15248
15330
|
return [...candidates.values()].map((row) => ({
|
|
15249
15331
|
...row,
|
|
15250
|
-
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 })
|
|
15251
15333
|
})).sort((left, right) => {
|
|
15252
15334
|
if (right.score !== left.score) return right.score - left.score;
|
|
15253
15335
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -15260,9 +15342,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
15260
15342
|
providerId: opts.provider.id,
|
|
15261
15343
|
agent_id: opts.agent_id,
|
|
15262
15344
|
limit: opts.limit,
|
|
15263
|
-
min_vitality: opts.min_vitality
|
|
15345
|
+
min_vitality: opts.min_vitality,
|
|
15346
|
+
after: opts.after,
|
|
15347
|
+
before: opts.before
|
|
15264
15348
|
});
|
|
15265
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
|
+
}
|
|
15266
15400
|
async function recallMemories(db, query, opts) {
|
|
15267
15401
|
const limit = opts?.limit ?? 10;
|
|
15268
15402
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -15270,10 +15404,15 @@ async function recallMemories(db, query, opts) {
|
|
|
15270
15404
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
15271
15405
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
15272
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;
|
|
15273
15410
|
const lexical = searchBM25(db, query, {
|
|
15274
15411
|
agent_id: agentId,
|
|
15275
15412
|
limit: lexicalLimit,
|
|
15276
|
-
min_vitality: minVitality
|
|
15413
|
+
min_vitality: minVitality,
|
|
15414
|
+
after,
|
|
15415
|
+
before
|
|
15277
15416
|
});
|
|
15278
15417
|
let vector = [];
|
|
15279
15418
|
if (provider) {
|
|
@@ -15282,17 +15421,25 @@ async function recallMemories(db, query, opts) {
|
|
|
15282
15421
|
provider,
|
|
15283
15422
|
agent_id: agentId,
|
|
15284
15423
|
limit: vectorLimit,
|
|
15285
|
-
min_vitality: minVitality
|
|
15424
|
+
min_vitality: minVitality,
|
|
15425
|
+
after,
|
|
15426
|
+
before
|
|
15286
15427
|
});
|
|
15287
15428
|
} catch {
|
|
15288
15429
|
vector = [];
|
|
15289
15430
|
}
|
|
15290
15431
|
}
|
|
15291
15432
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
15292
|
-
|
|
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
|
+
}
|
|
15293
15438
|
if (opts?.recordAccess !== false) {
|
|
15294
15439
|
for (const row of results) {
|
|
15295
|
-
|
|
15440
|
+
if (row.match_type !== "related") {
|
|
15441
|
+
recordAccess(db, row.memory.id);
|
|
15442
|
+
}
|
|
15296
15443
|
}
|
|
15297
15444
|
}
|
|
15298
15445
|
return {
|
|
@@ -15517,7 +15664,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
15517
15664
|
}
|
|
15518
15665
|
return 0.2;
|
|
15519
15666
|
}
|
|
15520
|
-
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
|
+
}
|
|
15521
15674
|
for (const part of parts) {
|
|
15522
15675
|
if (!part) continue;
|
|
15523
15676
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -15540,8 +15693,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
15540
15693
|
if (input.type !== "event") {
|
|
15541
15694
|
return 1;
|
|
15542
15695
|
}
|
|
15543
|
-
const inputTime = extractObservedAt(
|
|
15544
|
-
|
|
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
|
+
);
|
|
15545
15706
|
if (!inputTime || !existingTime) {
|
|
15546
15707
|
return 0.5;
|
|
15547
15708
|
}
|
|
@@ -15625,6 +15786,65 @@ function fourCriterionGate(input) {
|
|
|
15625
15786
|
failedCriteria: failed
|
|
15626
15787
|
};
|
|
15627
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
|
+
}
|
|
15628
15848
|
async function guard(db, input) {
|
|
15629
15849
|
const hash2 = contentHash(input.content);
|
|
15630
15850
|
const agentId = input.agent_id ?? "default";
|
|
@@ -15651,18 +15871,37 @@ async function guard(db, input) {
|
|
|
15651
15871
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
15652
15872
|
}
|
|
15653
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);
|
|
15654
15879
|
const best = candidates[0];
|
|
15655
15880
|
if (!best) {
|
|
15656
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
15881
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
15657
15882
|
}
|
|
15658
15883
|
const score = best.score;
|
|
15659
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
|
+
}
|
|
15660
15897
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
15661
15898
|
return {
|
|
15662
15899
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
15663
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)})`,
|
|
15664
15901
|
existingId: best.result.memory.id,
|
|
15665
|
-
score
|
|
15902
|
+
score,
|
|
15903
|
+
candidates: candidatesList,
|
|
15904
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15666
15905
|
};
|
|
15667
15906
|
}
|
|
15668
15907
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -15680,17 +15919,22 @@ async function guard(db, input) {
|
|
|
15680
15919
|
existingId: best.result.memory.id,
|
|
15681
15920
|
mergedContent: mergePlan.content,
|
|
15682
15921
|
mergePlan,
|
|
15683
|
-
score
|
|
15922
|
+
score,
|
|
15923
|
+
candidates: candidatesList,
|
|
15924
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15684
15925
|
};
|
|
15685
15926
|
}
|
|
15686
15927
|
return {
|
|
15687
15928
|
action: "add",
|
|
15688
15929
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
15689
|
-
score
|
|
15930
|
+
score,
|
|
15931
|
+
candidates: candidatesList,
|
|
15932
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
15690
15933
|
};
|
|
15691
15934
|
}
|
|
15692
15935
|
|
|
15693
15936
|
// src/sleep/sync.ts
|
|
15937
|
+
init_db();
|
|
15694
15938
|
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
15695
15939
|
if (!uri) return;
|
|
15696
15940
|
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
@@ -15699,6 +15943,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
15699
15943
|
} catch {
|
|
15700
15944
|
}
|
|
15701
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
|
+
}
|
|
15702
15960
|
async function syncOne(db, input) {
|
|
15703
15961
|
const memInput = {
|
|
15704
15962
|
content: input.content,
|
|
@@ -15710,17 +15968,22 @@ async function syncOne(db, input) {
|
|
|
15710
15968
|
uri: input.uri,
|
|
15711
15969
|
provider: input.provider,
|
|
15712
15970
|
conservative: input.conservative,
|
|
15713
|
-
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
|
|
15714
15975
|
};
|
|
15715
15976
|
const guardResult = await guard(db, memInput);
|
|
15977
|
+
const agentId = input.agent_id ?? "default";
|
|
15716
15978
|
switch (guardResult.action) {
|
|
15717
15979
|
case "skip":
|
|
15718
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
15980
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
15719
15981
|
case "add": {
|
|
15720
15982
|
const mem = createMemory(db, memInput);
|
|
15721
15983
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
15722
15984
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
15723
|
-
|
|
15985
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
15986
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15724
15987
|
}
|
|
15725
15988
|
case "update": {
|
|
15726
15989
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -15728,7 +15991,7 @@ async function syncOne(db, input) {
|
|
|
15728
15991
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
15729
15992
|
}
|
|
15730
15993
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15731
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
15994
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15732
15995
|
}
|
|
15733
15996
|
case "merge": {
|
|
15734
15997
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -15736,7 +15999,8 @@ async function syncOne(db, input) {
|
|
|
15736
15999
|
}
|
|
15737
16000
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
15738
16001
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
15739
|
-
|
|
16002
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
16003
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
15740
16004
|
}
|
|
15741
16005
|
}
|
|
15742
16006
|
}
|
|
@@ -16017,33 +16281,13 @@ async function rememberMemory(db, input) {
|
|
|
16017
16281
|
agent_id: input.agent_id,
|
|
16018
16282
|
provider: input.provider,
|
|
16019
16283
|
conservative: input.conservative,
|
|
16020
|
-
emotion_tag: input.emotion_tag
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
// src/app/recall.ts
|
|
16025
|
-
async function recallMemory(db, input) {
|
|
16026
|
-
const result = await recallMemories(db, input.query, {
|
|
16027
|
-
agent_id: input.agent_id,
|
|
16028
|
-
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
16029
|
-
min_vitality: input.min_vitality,
|
|
16030
|
-
lexicalLimit: input.lexicalLimit,
|
|
16031
|
-
vectorLimit: input.vectorLimit,
|
|
16032
|
-
provider: input.provider,
|
|
16033
|
-
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
|
|
16034
16288
|
});
|
|
16035
|
-
if (input.emotion_tag) {
|
|
16036
|
-
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
16037
|
-
}
|
|
16038
|
-
return result;
|
|
16039
16289
|
}
|
|
16040
16290
|
|
|
16041
|
-
// src/app/surface.ts
|
|
16042
|
-
init_memory();
|
|
16043
|
-
init_providers();
|
|
16044
|
-
init_tokenizer();
|
|
16045
|
-
init_vector();
|
|
16046
|
-
|
|
16047
16291
|
// src/app/feedback.ts
|
|
16048
16292
|
init_db();
|
|
16049
16293
|
function clamp012(value) {
|
|
@@ -16089,8 +16333,76 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
16089
16333
|
};
|
|
16090
16334
|
}
|
|
16091
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
|
+
}
|
|
16092
16400
|
|
|
16093
16401
|
// src/app/surface.ts
|
|
16402
|
+
init_memory();
|
|
16403
|
+
init_providers();
|
|
16404
|
+
init_tokenizer();
|
|
16405
|
+
init_vector();
|
|
16094
16406
|
var INTENT_PRIORS = {
|
|
16095
16407
|
factual: {
|
|
16096
16408
|
identity: 0.25,
|
|
@@ -16230,7 +16542,9 @@ async function surfaceMemories(db, input) {
|
|
|
16230
16542
|
searchBM25(db, trimmedQuery, {
|
|
16231
16543
|
agent_id: agentId,
|
|
16232
16544
|
limit: lexicalWindow,
|
|
16233
|
-
min_vitality: minVitality
|
|
16545
|
+
min_vitality: minVitality,
|
|
16546
|
+
after: input.after,
|
|
16547
|
+
before: input.before
|
|
16234
16548
|
}),
|
|
16235
16549
|
"queryRank"
|
|
16236
16550
|
);
|
|
@@ -16241,7 +16555,9 @@ async function surfaceMemories(db, input) {
|
|
|
16241
16555
|
searchBM25(db, trimmedTask, {
|
|
16242
16556
|
agent_id: agentId,
|
|
16243
16557
|
limit: lexicalWindow,
|
|
16244
|
-
min_vitality: minVitality
|
|
16558
|
+
min_vitality: minVitality,
|
|
16559
|
+
after: input.after,
|
|
16560
|
+
before: input.before
|
|
16245
16561
|
}),
|
|
16246
16562
|
"taskRank"
|
|
16247
16563
|
);
|
|
@@ -16252,7 +16568,9 @@ async function surfaceMemories(db, input) {
|
|
|
16252
16568
|
searchBM25(db, recentTurns.join(" "), {
|
|
16253
16569
|
agent_id: agentId,
|
|
16254
16570
|
limit: lexicalWindow,
|
|
16255
|
-
min_vitality: minVitality
|
|
16571
|
+
min_vitality: minVitality,
|
|
16572
|
+
after: input.after,
|
|
16573
|
+
before: input.before
|
|
16256
16574
|
}),
|
|
16257
16575
|
"recentRank"
|
|
16258
16576
|
);
|
|
@@ -16266,7 +16584,9 @@ async function surfaceMemories(db, input) {
|
|
|
16266
16584
|
providerId: provider.id,
|
|
16267
16585
|
agent_id: agentId,
|
|
16268
16586
|
limit: lexicalWindow,
|
|
16269
|
-
min_vitality: minVitality
|
|
16587
|
+
min_vitality: minVitality,
|
|
16588
|
+
after: input.after,
|
|
16589
|
+
before: input.before
|
|
16270
16590
|
});
|
|
16271
16591
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
16272
16592
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -16404,19 +16724,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
16404
16724
|
|
|
16405
16725
|
// src/sleep/tidy.ts
|
|
16406
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
|
+
}
|
|
16407
16757
|
function runTidy(db, opts) {
|
|
16408
16758
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
16409
16759
|
const agentId = opts?.agent_id;
|
|
16410
16760
|
let archived = 0;
|
|
16761
|
+
let staleDecayed = 0;
|
|
16411
16762
|
const transaction = db.transaction(() => {
|
|
16412
16763
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
16413
16764
|
for (const mem of decayed) {
|
|
16414
16765
|
deleteMemory(db, mem.id);
|
|
16415
16766
|
archived += 1;
|
|
16416
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
|
+
}
|
|
16417
16792
|
});
|
|
16418
16793
|
transaction();
|
|
16419
|
-
return { archived, orphansCleaned: 0 };
|
|
16794
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
16420
16795
|
}
|
|
16421
16796
|
|
|
16422
16797
|
// src/sleep/govern.ts
|
|
@@ -16825,7 +17200,10 @@ function formatMemory(memory, score) {
|
|
|
16825
17200
|
priority: memory.priority,
|
|
16826
17201
|
vitality: memory.vitality,
|
|
16827
17202
|
score,
|
|
16828
|
-
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
|
|
16829
17207
|
};
|
|
16830
17208
|
}
|
|
16831
17209
|
function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
|
|
@@ -16912,7 +17290,9 @@ function formatRecallPayload(result) {
|
|
|
16912
17290
|
bm25_rank: row.bm25_rank,
|
|
16913
17291
|
vector_rank: row.vector_rank,
|
|
16914
17292
|
bm25_score: row.bm25_score,
|
|
16915
|
-
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
|
|
16916
17296
|
}))
|
|
16917
17297
|
};
|
|
16918
17298
|
}
|
|
@@ -16935,7 +17315,10 @@ function formatSurfacePayload(result) {
|
|
|
16935
17315
|
priority_prior: row.priority_prior,
|
|
16936
17316
|
feedback_score: row.feedback_score,
|
|
16937
17317
|
reason_codes: row.reason_codes,
|
|
16938
|
-
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
|
|
16939
17322
|
}))
|
|
16940
17323
|
};
|
|
16941
17324
|
}
|
|
@@ -16944,7 +17327,7 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16944
17327
|
const aid = agentId ?? AGENT_ID;
|
|
16945
17328
|
const server = new McpServer({
|
|
16946
17329
|
name: "agent-memory",
|
|
16947
|
-
version: "
|
|
17330
|
+
version: "5.0.0"
|
|
16948
17331
|
});
|
|
16949
17332
|
server.tool(
|
|
16950
17333
|
"remember",
|
|
@@ -16956,10 +17339,24 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16956
17339
|
emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
|
|
16957
17340
|
source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
|
|
16958
17341
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
16959
|
-
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")
|
|
16960
17346
|
},
|
|
16961
|
-
async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag }) => {
|
|
16962
|
-
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
|
+
});
|
|
16963
17360
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
16964
17361
|
}
|
|
16965
17362
|
);
|
|
@@ -16970,10 +17367,23 @@ function createMcpServer(dbPath, agentId) {
|
|
|
16970
17367
|
query: external_exports.string().describe("Search query (natural language)"),
|
|
16971
17368
|
limit: external_exports.number().default(10).describe("Max results to return"),
|
|
16972
17369
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
16973
|
-
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")
|
|
16974
17375
|
},
|
|
16975
|
-
async ({ query, limit, agent_id, emotion_tag }) => {
|
|
16976
|
-
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
|
+
});
|
|
16977
17387
|
return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
|
|
16978
17388
|
}
|
|
16979
17389
|
);
|
|
@@ -17163,9 +17573,13 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17163
17573
|
types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"]).describe("Optional type filter")).optional(),
|
|
17164
17574
|
limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
|
|
17165
17575
|
agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
|
|
17166
|
-
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)")
|
|
17167
17581
|
},
|
|
17168
|
-
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 }) => {
|
|
17169
17583
|
const resolvedQuery = query ?? keywords?.join(" ");
|
|
17170
17584
|
const result = await surfaceMemories(db, {
|
|
17171
17585
|
query: resolvedQuery,
|
|
@@ -17174,7 +17588,11 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17174
17588
|
intent,
|
|
17175
17589
|
types,
|
|
17176
17590
|
limit: limit ?? 5,
|
|
17177
|
-
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
|
|
17178
17596
|
});
|
|
17179
17597
|
return {
|
|
17180
17598
|
content: [{
|
|
@@ -17184,6 +17602,43 @@ function createMcpServer(dbPath, agentId) {
|
|
|
17184
17602
|
};
|
|
17185
17603
|
}
|
|
17186
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
|
+
);
|
|
17187
17642
|
return { server, db };
|
|
17188
17643
|
}
|
|
17189
17644
|
async function main() {
|