@hasna/mementos 0.11.1 → 0.14.2

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.
Files changed (95) hide show
  1. package/dashboard/dist/assets/index-DqyMbv89.js +270 -0
  2. package/dashboard/dist/assets/index-UBCddFo_.css +1 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/dist/cli/brains.d.ts +3 -0
  5. package/dist/cli/brains.d.ts.map +1 -0
  6. package/dist/cli/index.js +2093 -512
  7. package/dist/db/database.d.ts.map +1 -1
  8. package/dist/db/memories.d.ts.map +1 -1
  9. package/dist/db/tool-events.d.ts +27 -0
  10. package/dist/db/tool-events.d.ts.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +177 -5
  14. package/dist/lib/activation-matcher.d.ts +16 -0
  15. package/dist/lib/activation-matcher.d.ts.map +1 -0
  16. package/dist/lib/asmr/categorizer.d.ts +31 -0
  17. package/dist/lib/asmr/categorizer.d.ts.map +1 -0
  18. package/dist/lib/asmr/context-agent.d.ts +4 -0
  19. package/dist/lib/asmr/context-agent.d.ts.map +1 -0
  20. package/dist/lib/asmr/ensemble.d.ts +23 -0
  21. package/dist/lib/asmr/ensemble.d.ts.map +1 -0
  22. package/dist/lib/asmr/fact-agent.d.ts +4 -0
  23. package/dist/lib/asmr/fact-agent.d.ts.map +1 -0
  24. package/dist/lib/asmr/index.d.ts +7 -0
  25. package/dist/lib/asmr/index.d.ts.map +1 -0
  26. package/dist/lib/asmr/orchestrator.d.ts +4 -0
  27. package/dist/lib/asmr/orchestrator.d.ts.map +1 -0
  28. package/dist/lib/asmr/temporal-agent.d.ts +4 -0
  29. package/dist/lib/asmr/temporal-agent.d.ts.map +1 -0
  30. package/dist/lib/asmr/types.d.ts +27 -0
  31. package/dist/lib/asmr/types.d.ts.map +1 -0
  32. package/dist/lib/auto-inject-orchestrator.d.ts +57 -0
  33. package/dist/lib/auto-inject-orchestrator.d.ts.map +1 -0
  34. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  35. package/dist/lib/channel-pusher.d.ts +39 -0
  36. package/dist/lib/channel-pusher.d.ts.map +1 -0
  37. package/dist/lib/connectors/files.d.ts +8 -0
  38. package/dist/lib/connectors/files.d.ts.map +1 -0
  39. package/dist/lib/connectors/github.d.ts +7 -0
  40. package/dist/lib/connectors/github.d.ts.map +1 -0
  41. package/dist/lib/connectors/index.d.ts +12 -0
  42. package/dist/lib/connectors/index.d.ts.map +1 -0
  43. package/dist/lib/connectors/notion.d.ts +7 -0
  44. package/dist/lib/connectors/notion.d.ts.map +1 -0
  45. package/dist/lib/connectors/types.d.ts +27 -0
  46. package/dist/lib/connectors/types.d.ts.map +1 -0
  47. package/dist/lib/context-extractor.d.ts +14 -0
  48. package/dist/lib/context-extractor.d.ts.map +1 -0
  49. package/dist/lib/extractors/audio.d.ts +8 -0
  50. package/dist/lib/extractors/audio.d.ts.map +1 -0
  51. package/dist/lib/extractors/index.d.ts +12 -0
  52. package/dist/lib/extractors/index.d.ts.map +1 -0
  53. package/dist/lib/extractors/ocr.d.ts +7 -0
  54. package/dist/lib/extractors/ocr.d.ts.map +1 -0
  55. package/dist/lib/extractors/pdf.d.ts +7 -0
  56. package/dist/lib/extractors/pdf.d.ts.map +1 -0
  57. package/dist/lib/extractors/types.d.ts +12 -0
  58. package/dist/lib/extractors/types.d.ts.map +1 -0
  59. package/dist/lib/gatherer.d.ts +16 -0
  60. package/dist/lib/gatherer.d.ts.map +1 -0
  61. package/dist/lib/injector.d.ts +48 -1
  62. package/dist/lib/injector.d.ts.map +1 -1
  63. package/dist/lib/matryoshka.d.ts +50 -0
  64. package/dist/lib/matryoshka.d.ts.map +1 -0
  65. package/dist/lib/model-config.d.ts +14 -0
  66. package/dist/lib/model-config.d.ts.map +1 -0
  67. package/dist/lib/procedural-extractor.d.ts +21 -0
  68. package/dist/lib/procedural-extractor.d.ts.map +1 -0
  69. package/dist/lib/profile-synthesizer.d.ts +20 -0
  70. package/dist/lib/profile-synthesizer.d.ts.map +1 -0
  71. package/dist/lib/session-processor.d.ts.map +1 -1
  72. package/dist/lib/session-registry.d.ts +47 -0
  73. package/dist/lib/session-registry.d.ts.map +1 -0
  74. package/dist/lib/session-start-briefing.d.ts +10 -0
  75. package/dist/lib/session-start-briefing.d.ts.map +1 -0
  76. package/dist/lib/session-watcher.d.ts +30 -0
  77. package/dist/lib/session-watcher.d.ts.map +1 -0
  78. package/dist/lib/tool-lesson-extractor.d.ts +24 -0
  79. package/dist/lib/tool-lesson-extractor.d.ts.map +1 -0
  80. package/dist/lib/tool-memory-synthesizer.d.ts +28 -0
  81. package/dist/lib/tool-memory-synthesizer.d.ts.map +1 -0
  82. package/dist/lib/topic-clusterer.d.ts +21 -0
  83. package/dist/lib/topic-clusterer.d.ts.map +1 -0
  84. package/dist/lib/when-to-use-generator.d.ts +22 -0
  85. package/dist/lib/when-to-use-generator.d.ts.map +1 -0
  86. package/dist/mcp/index.d.ts +3 -1
  87. package/dist/mcp/index.d.ts.map +1 -1
  88. package/dist/mcp/index.js +3825 -382
  89. package/dist/server/index.d.ts.map +1 -1
  90. package/dist/server/index.js +883 -18
  91. package/dist/types/index.d.ts +57 -0
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/package.json +4 -1
  94. package/dashboard/dist/assets/index-B1yiOEw3.js +0 -290
  95. package/dashboard/dist/assets/index-DnpbasSl.css +0 -1
package/dist/mcp/index.js CHANGED
@@ -1,13 +1,40 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toCommonJS = (from) => {
11
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
12
+ if (entry)
13
+ return entry;
14
+ entry = __defProp({}, "__esModule", { value: true });
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (var key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(entry, key))
18
+ __defProp(entry, key, {
19
+ get: __accessProp.bind(from, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
23
+ __moduleCache.set(from, entry);
24
+ return entry;
25
+ };
26
+ var __moduleCache;
27
+ var __returnValue = (v) => v;
28
+ function __exportSetter(name, newValue) {
29
+ this[name] = __returnValue.bind(null, newValue);
30
+ }
4
31
  var __export = (target, all) => {
5
32
  for (var name in all)
6
33
  __defProp(target, name, {
7
34
  get: all[name],
8
35
  enumerable: true,
9
36
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
37
+ set: __exportSetter.bind(all, name)
11
38
  });
12
39
  };
13
40
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -912,6 +939,44 @@ CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(mode
912
939
 
913
940
  PRAGMA foreign_keys = ON;
914
941
  INSERT OR IGNORE INTO _migrations (id) VALUES (29);
942
+ `,
943
+ `
944
+ ALTER TABLE memories ADD COLUMN when_to_use TEXT DEFAULT NULL;
945
+ CREATE INDEX IF NOT EXISTS idx_memories_when_to_use ON memories(when_to_use) WHERE when_to_use IS NOT NULL;
946
+ ALTER TABLE memory_versions ADD COLUMN when_to_use TEXT;
947
+ INSERT OR IGNORE INTO _migrations (id) VALUES (30);
948
+ `,
949
+ `
950
+ CREATE TABLE IF NOT EXISTS tool_events (
951
+ id TEXT PRIMARY KEY,
952
+ tool_name TEXT NOT NULL,
953
+ action TEXT,
954
+ success INTEGER NOT NULL DEFAULT 1,
955
+ error_type TEXT CHECK(error_type IS NULL OR error_type IN ('timeout', 'permission', 'not_found', 'syntax', 'rate_limit', 'other')),
956
+ error_message TEXT,
957
+ tokens_used INTEGER,
958
+ latency_ms INTEGER,
959
+ context TEXT,
960
+ lesson TEXT,
961
+ when_to_use TEXT,
962
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
963
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
964
+ session_id TEXT,
965
+ metadata TEXT DEFAULT '{}',
966
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
967
+ );
968
+ CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name ON tool_events(tool_name);
969
+ CREATE INDEX IF NOT EXISTS idx_tool_events_agent ON tool_events(agent_id);
970
+ CREATE INDEX IF NOT EXISTS idx_tool_events_project ON tool_events(project_id);
971
+ CREATE INDEX IF NOT EXISTS idx_tool_events_success ON tool_events(success);
972
+ CREATE INDEX IF NOT EXISTS idx_tool_events_created ON tool_events(created_at);
973
+ INSERT OR IGNORE INTO _migrations (id) VALUES (31);
974
+ `,
975
+ `
976
+ ALTER TABLE memories ADD COLUMN sequence_group TEXT DEFAULT NULL;
977
+ ALTER TABLE memories ADD COLUMN sequence_order INTEGER DEFAULT NULL;
978
+ CREATE INDEX IF NOT EXISTS idx_memories_sequence_group ON memories(sequence_group) WHERE sequence_group IS NOT NULL;
979
+ INSERT OR IGNORE INTO _migrations (id) VALUES (32);
915
980
  `
916
981
  ];
917
982
  });
@@ -1175,6 +1240,18 @@ var init_poisoning = __esm(() => {
1175
1240
  });
1176
1241
 
1177
1242
  // src/db/entity-memories.ts
1243
+ function parseEntityRow(row) {
1244
+ return {
1245
+ id: row["id"],
1246
+ name: row["name"],
1247
+ type: row["type"],
1248
+ description: row["description"] || null,
1249
+ metadata: JSON.parse(row["metadata"] || "{}"),
1250
+ project_id: row["project_id"] || null,
1251
+ created_at: row["created_at"],
1252
+ updated_at: row["updated_at"]
1253
+ };
1254
+ }
1178
1255
  function parseEntityMemoryRow(row) {
1179
1256
  return {
1180
1257
  entity_id: row["entity_id"],
@@ -1203,6 +1280,14 @@ function getMemoriesForEntity(entityId, db) {
1203
1280
  ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
1204
1281
  return rows.map(parseMemoryRow);
1205
1282
  }
1283
+ function getEntitiesForMemory(memoryId, db) {
1284
+ const d = db || getDatabase();
1285
+ const rows = d.query(`SELECT e.* FROM entities e
1286
+ INNER JOIN entity_memories em ON em.entity_id = e.id
1287
+ WHERE em.memory_id = ?
1288
+ ORDER BY e.name ASC`).all(memoryId);
1289
+ return rows.map(parseEntityRow);
1290
+ }
1206
1291
  function getEntityMemoryLinks(entityId, memoryId, db) {
1207
1292
  const d = db || getDatabase();
1208
1293
  const conditions = [];
@@ -1266,6 +1351,9 @@ function parseMemoryRow(row) {
1266
1351
  session_id: row["session_id"] || null,
1267
1352
  machine_id: row["machine_id"] || null,
1268
1353
  flag: row["flag"] || null,
1354
+ when_to_use: row["when_to_use"] || null,
1355
+ sequence_group: row["sequence_group"] || null,
1356
+ sequence_order: row["sequence_order"] ?? null,
1269
1357
  content_type: row["content_type"] || "text",
1270
1358
  namespace: row["namespace"] || null,
1271
1359
  created_by_agent: row["created_by_agent"] || null,
@@ -1324,6 +1412,7 @@ function createMemory(input, dedupeMode = "merge", db) {
1324
1412
  d.run(`UPDATE memories SET
1325
1413
  value = ?, category = ?, summary = ?, tags = ?,
1326
1414
  importance = ?, metadata = ?, expires_at = ?,
1415
+ when_to_use = ?,
1327
1416
  pinned = COALESCE(pinned, 0),
1328
1417
  version = version + 1, updated_at = ?
1329
1418
  WHERE id = ?`, [
@@ -1334,6 +1423,7 @@ function createMemory(input, dedupeMode = "merge", db) {
1334
1423
  input.importance ?? 5,
1335
1424
  metadataJson,
1336
1425
  expiresAt,
1426
+ input.when_to_use || null,
1337
1427
  timestamp,
1338
1428
  existing.id
1339
1429
  ]);
@@ -1358,8 +1448,8 @@ function createMemory(input, dedupeMode = "merge", db) {
1358
1448
  return merged;
1359
1449
  }
1360
1450
  }
1361
- d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
1362
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?, ?, ?, ?, ?)`, [
1451
+ d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
1452
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?, ?, ?, ?, ?)`, [
1363
1453
  id,
1364
1454
  input.key,
1365
1455
  input.value,
@@ -1375,6 +1465,9 @@ function createMemory(input, dedupeMode = "merge", db) {
1375
1465
  input.machine_id || null,
1376
1466
  input.namespace || null,
1377
1467
  input.agent_id || null,
1468
+ input.when_to_use || null,
1469
+ input.sequence_group || null,
1470
+ input.sequence_order ?? null,
1378
1471
  metadataJson,
1379
1472
  expiresAt,
1380
1473
  input.metadata?.valid_from ?? timestamp,
@@ -1585,8 +1678,8 @@ function updateMemory(id, input, db) {
1585
1678
  throw new VersionConflictError(id, input.version, existing.version);
1586
1679
  }
1587
1680
  try {
1588
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1589
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1681
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, when_to_use, created_at)
1682
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1590
1683
  uuid(),
1591
1684
  existing.id,
1592
1685
  existing.version,
@@ -1598,6 +1691,7 @@ function updateMemory(id, input, db) {
1598
1691
  existing.summary,
1599
1692
  existing.pinned ? 1 : 0,
1600
1693
  existing.status,
1694
+ existing.when_to_use || null,
1601
1695
  existing.updated_at
1602
1696
  ]);
1603
1697
  } catch {}
@@ -1643,6 +1737,10 @@ function updateMemory(id, input, db) {
1643
1737
  sets.push("flag = ?");
1644
1738
  params.push(input.flag ?? null);
1645
1739
  }
1740
+ if (input.when_to_use !== undefined) {
1741
+ sets.push("when_to_use = ?");
1742
+ params.push(input.when_to_use ?? null);
1743
+ }
1646
1744
  if (input.tags !== undefined) {
1647
1745
  sets.push("tags = ?");
1648
1746
  params.push(JSON.stringify(input.tags));
@@ -1806,7 +1904,7 @@ var init_memories = __esm(() => {
1806
1904
  var exports_entities = {};
1807
1905
  __export(exports_entities, {
1808
1906
  updateEntity: () => updateEntity,
1809
- parseEntityRow: () => parseEntityRow,
1907
+ parseEntityRow: () => parseEntityRow2,
1810
1908
  mergeEntities: () => mergeEntities,
1811
1909
  listEntities: () => listEntities,
1812
1910
  graphTraverse: () => graphTraverse,
@@ -1816,7 +1914,7 @@ __export(exports_entities, {
1816
1914
  deleteEntity: () => deleteEntity,
1817
1915
  createEntity: () => createEntity
1818
1916
  });
1819
- function parseEntityRow(row) {
1917
+ function parseEntityRow2(row) {
1820
1918
  return {
1821
1919
  id: row["id"],
1822
1920
  name: row["name"],
@@ -1876,7 +1974,7 @@ function getEntity(id, db) {
1876
1974
  const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1877
1975
  if (!row)
1878
1976
  throw new EntityNotFoundError(id);
1879
- return parseEntityRow(row);
1977
+ return parseEntityRow2(row);
1880
1978
  }
1881
1979
  function getEntityByName(name, type, projectId, db) {
1882
1980
  const d = db || getDatabase();
@@ -1894,7 +1992,7 @@ function getEntityByName(name, type, projectId, db) {
1894
1992
  const row = d.query(sql).get(...params);
1895
1993
  if (!row)
1896
1994
  return null;
1897
- return parseEntityRow(row);
1995
+ return parseEntityRow2(row);
1898
1996
  }
1899
1997
  function listEntities(filter = {}, db) {
1900
1998
  const d = db || getDatabase();
@@ -1927,7 +2025,7 @@ function listEntities(filter = {}, db) {
1927
2025
  params.push(filter.offset);
1928
2026
  }
1929
2027
  const rows = d.query(sql).all(...params);
1930
- return rows.map(parseEntityRow);
2028
+ return rows.map(parseEntityRow2);
1931
2029
  }
1932
2030
  function updateEntity(id, input, db) {
1933
2031
  const d = db || getDatabase();
@@ -2000,7 +2098,7 @@ function trigramSimilarity(a, b) {
2000
2098
  function findDuplicateEntities(threshold = 0.8, db) {
2001
2099
  const d = db || getDatabase();
2002
2100
  const entities = d.query("SELECT * FROM entities ORDER BY type, project_id, name").all();
2003
- const parsed = entities.map(parseEntityRow);
2101
+ const parsed = entities.map(parseEntityRow2);
2004
2102
  const groups = new Map;
2005
2103
  for (const e of parsed) {
2006
2104
  const groupKey = `${e.type}:${e.project_id || ""}`;
@@ -2152,7 +2250,7 @@ function parseRelationRow(row) {
2152
2250
  created_at: row["created_at"]
2153
2251
  };
2154
2252
  }
2155
- function parseEntityRow2(row) {
2253
+ function parseEntityRow3(row) {
2156
2254
  return {
2157
2255
  id: row["id"],
2158
2256
  name: row["name"],
@@ -2234,7 +2332,7 @@ function getEntityGraph(entityId, depth = 2, db) {
2234
2332
  WHERE g.depth < ?
2235
2333
  )
2236
2334
  SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
2237
- const entities = entityRows.map(parseEntityRow2);
2335
+ const entities = entityRows.map(parseEntityRow3);
2238
2336
  const entityIds = new Set(entities.map((e) => e.id));
2239
2337
  if (entityIds.size === 0) {
2240
2338
  return { entities: [], relations: [] };
@@ -2267,7 +2365,7 @@ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
2267
2365
  for (const id of ids) {
2268
2366
  const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
2269
2367
  if (row)
2270
- entities.push(parseEntityRow2(row));
2368
+ entities.push(parseEntityRow3(row));
2271
2369
  }
2272
2370
  return entities.length > 0 ? entities : null;
2273
2371
  }
@@ -2940,6 +3038,262 @@ var init_search = __esm(() => {
2940
3038
  ]);
2941
3039
  });
2942
3040
 
3041
+ // src/lib/profile-synthesizer.ts
3042
+ var exports_profile_synthesizer = {};
3043
+ __export(exports_profile_synthesizer, {
3044
+ synthesizeProfile: () => synthesizeProfile,
3045
+ markProfileStale: () => markProfileStale,
3046
+ getProfileKey: () => getProfileKey
3047
+ });
3048
+ function getProfileKey(scope, id) {
3049
+ return `_profile_${scope}_${id}`;
3050
+ }
3051
+ async function synthesizeProfile(options) {
3052
+ const scope = options.scope || (options.project_id ? "project" : options.agent_id ? "agent" : "global");
3053
+ const id = options.project_id || options.agent_id || "global";
3054
+ const profileKey = getProfileKey(scope, id);
3055
+ if (!options.force_refresh) {
3056
+ const cached = getMemoryByKey(profileKey, "shared", undefined, options.project_id);
3057
+ if (cached) {
3058
+ const age = Date.now() - new Date(cached.updated_at).getTime();
3059
+ const maxAge = 24 * 60 * 60 * 1000;
3060
+ const isStale = cached.metadata?.stale === true;
3061
+ if (age < maxAge && !isStale) {
3062
+ return { profile: cached.value, memory_count: 0, from_cache: true };
3063
+ }
3064
+ }
3065
+ }
3066
+ const prefMemories = listMemories({
3067
+ category: "preference",
3068
+ project_id: options.project_id,
3069
+ status: "active",
3070
+ limit: 30
3071
+ });
3072
+ const factMemories = listMemories({
3073
+ category: "fact",
3074
+ project_id: options.project_id,
3075
+ status: "active",
3076
+ limit: 30
3077
+ });
3078
+ const allMemories = [...prefMemories, ...factMemories];
3079
+ if (allMemories.length === 0)
3080
+ return null;
3081
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
3082
+ if (!apiKey) {
3083
+ const lines = allMemories.map((m) => `- ${m.key}: ${m.value}`).join(`
3084
+ `);
3085
+ const fallbackProfile = `## Profile
3086
+ ${lines}`;
3087
+ saveProfile(profileKey, fallbackProfile, allMemories.length, options);
3088
+ return { profile: fallbackProfile, memory_count: allMemories.length, from_cache: false };
3089
+ }
3090
+ try {
3091
+ const memoryList = allMemories.sort((a, b) => b.importance - a.importance).map((m) => `[${m.category}] ${m.key}: ${m.value}`).join(`
3092
+ `);
3093
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3094
+ method: "POST",
3095
+ headers: {
3096
+ "x-api-key": apiKey,
3097
+ "anthropic-version": "2023-06-01",
3098
+ "content-type": "application/json"
3099
+ },
3100
+ body: JSON.stringify({
3101
+ model: "claude-haiku-4-5-20251001",
3102
+ max_tokens: 500,
3103
+ system: PROFILE_PROMPT,
3104
+ messages: [{ role: "user", content: `Synthesize a profile from these ${allMemories.length} memories:
3105
+
3106
+ ${memoryList}` }]
3107
+ })
3108
+ });
3109
+ if (!response.ok)
3110
+ return null;
3111
+ const data = await response.json();
3112
+ const profile = data.content?.[0]?.text?.trim();
3113
+ if (!profile)
3114
+ return null;
3115
+ saveProfile(profileKey, profile, allMemories.length, options);
3116
+ return { profile, memory_count: allMemories.length, from_cache: false };
3117
+ } catch {
3118
+ return null;
3119
+ }
3120
+ }
3121
+ function saveProfile(key, value, memoryCount, options) {
3122
+ try {
3123
+ createMemory({
3124
+ key,
3125
+ value,
3126
+ category: "fact",
3127
+ scope: "shared",
3128
+ importance: 10,
3129
+ source: "auto",
3130
+ tags: ["profile", "synthesized"],
3131
+ when_to_use: "When needing to understand this agent's or project's preferences, style, and conventions",
3132
+ metadata: { memory_count: memoryCount, synthesized_at: new Date().toISOString(), stale: false },
3133
+ agent_id: options.agent_id,
3134
+ project_id: options.project_id
3135
+ });
3136
+ } catch {}
3137
+ }
3138
+ function markProfileStale(projectId, _agentId) {
3139
+ try {
3140
+ const { getDatabase: getDatabase2 } = (init_database(), __toCommonJS(exports_database));
3141
+ const db = getDatabase2();
3142
+ db.run(`UPDATE memories SET metadata = json_set(COALESCE(metadata, '{}'), '$.stale', json('true'))
3143
+ WHERE key LIKE '_profile_%' AND COALESCE(project_id, '') = ?`, [projectId || ""]);
3144
+ } catch {}
3145
+ }
3146
+ var PROFILE_PROMPT = `You synthesize a coherent agent/project profile from individual preference and fact memories.
3147
+
3148
+ Output a concise profile (200-300 words max) organized by:
3149
+ - **Stack & Tools**: Languages, frameworks, package managers, etc.
3150
+ - **Code Style**: Formatting, patterns, naming conventions
3151
+ - **Workflow**: Testing, deployment, git practices
3152
+ - **Communication**: Response style, verbosity, formatting preferences
3153
+ - **Key Facts**: Architecture decisions, constraints, team conventions
3154
+
3155
+ Only include sections that have relevant data. Be specific and actionable.
3156
+ Output in markdown format.`;
3157
+ var init_profile_synthesizer = __esm(() => {
3158
+ init_memories();
3159
+ });
3160
+
3161
+ // src/db/tool-events.ts
3162
+ function parseToolEventRow(row) {
3163
+ return {
3164
+ id: row["id"],
3165
+ tool_name: row["tool_name"],
3166
+ action: row["action"] || null,
3167
+ success: !!row["success"],
3168
+ error_type: row["error_type"] || null,
3169
+ error_message: row["error_message"] || null,
3170
+ tokens_used: row["tokens_used"] ?? null,
3171
+ latency_ms: row["latency_ms"] ?? null,
3172
+ context: row["context"] || null,
3173
+ lesson: row["lesson"] || null,
3174
+ when_to_use: row["when_to_use"] || null,
3175
+ agent_id: row["agent_id"] || null,
3176
+ project_id: row["project_id"] || null,
3177
+ session_id: row["session_id"] || null,
3178
+ metadata: JSON.parse(row["metadata"] || "{}"),
3179
+ created_at: row["created_at"]
3180
+ };
3181
+ }
3182
+ function saveToolEvent(input, db) {
3183
+ const d = db || getDatabase();
3184
+ const id = uuid();
3185
+ const timestamp = now();
3186
+ const metadataJson = JSON.stringify(input.metadata || {});
3187
+ d.run(`INSERT INTO tool_events (id, tool_name, action, success, error_type, error_message, tokens_used, latency_ms, context, lesson, when_to_use, agent_id, project_id, session_id, metadata, created_at)
3188
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3189
+ id,
3190
+ input.tool_name,
3191
+ input.action || null,
3192
+ input.success ? 1 : 0,
3193
+ input.error_type || null,
3194
+ input.error_message || null,
3195
+ input.tokens_used ?? null,
3196
+ input.latency_ms ?? null,
3197
+ input.context || null,
3198
+ input.lesson || null,
3199
+ input.when_to_use || null,
3200
+ input.agent_id || null,
3201
+ input.project_id || null,
3202
+ input.session_id || null,
3203
+ metadataJson,
3204
+ timestamp
3205
+ ]);
3206
+ return getToolEvent(id, d);
3207
+ }
3208
+ function getToolEvent(id, db) {
3209
+ const d = db || getDatabase();
3210
+ const row = d.query("SELECT * FROM tool_events WHERE id = ?").get(id);
3211
+ if (!row)
3212
+ return null;
3213
+ return parseToolEventRow(row);
3214
+ }
3215
+ function getToolEvents(filters, db) {
3216
+ const d = db || getDatabase();
3217
+ const conditions = [];
3218
+ const params = [];
3219
+ if (filters.tool_name) {
3220
+ conditions.push("tool_name = ?");
3221
+ params.push(filters.tool_name);
3222
+ }
3223
+ if (filters.agent_id) {
3224
+ conditions.push("agent_id = ?");
3225
+ params.push(filters.agent_id);
3226
+ }
3227
+ if (filters.project_id) {
3228
+ conditions.push("project_id = ?");
3229
+ params.push(filters.project_id);
3230
+ }
3231
+ if (filters.success !== undefined) {
3232
+ conditions.push("success = ?");
3233
+ params.push(filters.success ? 1 : 0);
3234
+ }
3235
+ if (filters.from_date) {
3236
+ conditions.push("created_at >= ?");
3237
+ params.push(filters.from_date);
3238
+ }
3239
+ if (filters.to_date) {
3240
+ conditions.push("created_at <= ?");
3241
+ params.push(filters.to_date);
3242
+ }
3243
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3244
+ const limit = filters.limit || 50;
3245
+ const offset = filters.offset || 0;
3246
+ const rows = d.query(`SELECT * FROM tool_events ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
3247
+ return rows.map(parseToolEventRow);
3248
+ }
3249
+ function getToolStats(tool_name, project_id, db) {
3250
+ const d = db || getDatabase();
3251
+ let where = "WHERE tool_name = ?";
3252
+ const params = [tool_name];
3253
+ if (project_id) {
3254
+ where += " AND project_id = ?";
3255
+ params.push(project_id);
3256
+ }
3257
+ const stats = d.query(`SELECT
3258
+ COUNT(*) as total_calls,
3259
+ SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
3260
+ SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failure_count,
3261
+ AVG(CASE WHEN tokens_used IS NOT NULL THEN tokens_used END) as avg_tokens,
3262
+ AVG(CASE WHEN latency_ms IS NOT NULL THEN latency_ms END) as avg_latency_ms,
3263
+ MAX(created_at) as last_used
3264
+ FROM tool_events ${where}`).get(...params);
3265
+ const total = stats["total_calls"] || 0;
3266
+ const successCount = stats["success_count"] || 0;
3267
+ const errorRows = d.query(`SELECT error_type, COUNT(*) as count
3268
+ FROM tool_events ${where} AND error_type IS NOT NULL
3269
+ GROUP BY error_type ORDER BY count DESC LIMIT 5`).all(...params);
3270
+ return {
3271
+ tool_name,
3272
+ total_calls: total,
3273
+ success_count: successCount,
3274
+ failure_count: stats["failure_count"] || 0,
3275
+ success_rate: total > 0 ? successCount / total : 0,
3276
+ avg_tokens: stats["avg_tokens"] ?? null,
3277
+ avg_latency_ms: stats["avg_latency_ms"] ?? null,
3278
+ common_errors: errorRows,
3279
+ last_used: stats["last_used"] || ""
3280
+ };
3281
+ }
3282
+ function getToolLessons(tool_name, project_id, limit, db) {
3283
+ const d = db || getDatabase();
3284
+ let where = "WHERE tool_name = ? AND lesson IS NOT NULL";
3285
+ const params = [tool_name];
3286
+ if (project_id) {
3287
+ where += " AND project_id = ?";
3288
+ params.push(project_id);
3289
+ }
3290
+ const rows = d.query(`SELECT lesson, when_to_use, created_at FROM tool_events ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit || 20);
3291
+ return rows;
3292
+ }
3293
+ var init_tool_events = __esm(() => {
3294
+ init_database();
3295
+ });
3296
+
2943
3297
  // src/db/webhook_hooks.ts
2944
3298
  function parseRow(row) {
2945
3299
  return {
@@ -4053,80 +4407,265 @@ var init_synthesis = __esm(() => {
4053
4407
  init_database();
4054
4408
  });
4055
4409
 
4056
- // src/lib/built-in-hooks.ts
4057
- var exports_built_in_hooks = {};
4058
- __export(exports_built_in_hooks, {
4059
- reloadWebhooks: () => reloadWebhooks,
4060
- loadWebhooksFromDb: () => loadWebhooksFromDb
4410
+ // src/lib/when-to-use-generator.ts
4411
+ var exports_when_to_use_generator = {};
4412
+ __export(exports_when_to_use_generator, {
4413
+ generateWhenToUse: () => generateWhenToUse,
4414
+ autoGenerateWhenToUse: () => autoGenerateWhenToUse
4061
4415
  });
4062
- async function getAutoMemory() {
4063
- if (!_processConversationTurn) {
4064
- const mod = await Promise.resolve().then(() => (init_auto_memory(), exports_auto_memory));
4065
- _processConversationTurn = mod.processConversationTurn;
4416
+ async function generateWhenToUse(key, value, category, tags) {
4417
+ if (process.env["MEMENTOS_AUTO_WHEN_TO_USE"] !== "true")
4418
+ return null;
4419
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
4420
+ if (!apiKey)
4421
+ return null;
4422
+ try {
4423
+ const userMessage = `Key: "${key}"
4424
+ Value: "${value}"
4425
+ Category: ${category}
4426
+ Tags: ${tags.join(", ") || "none"}
4427
+
4428
+ Generate the when_to_use activation context (1-2 sentences):`;
4429
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
4430
+ method: "POST",
4431
+ headers: {
4432
+ "x-api-key": apiKey,
4433
+ "anthropic-version": "2023-06-01",
4434
+ "content-type": "application/json"
4435
+ },
4436
+ body: JSON.stringify({
4437
+ model: "claude-haiku-4-5-20251001",
4438
+ max_tokens: 150,
4439
+ system: SYSTEM_PROMPT,
4440
+ messages: [{ role: "user", content: userMessage }]
4441
+ })
4442
+ });
4443
+ if (!response.ok)
4444
+ return null;
4445
+ const data = await response.json();
4446
+ const text = data.content?.[0]?.text?.trim();
4447
+ return text || null;
4448
+ } catch {
4449
+ return null;
4066
4450
  }
4067
- return _processConversationTurn;
4068
4451
  }
4069
- function loadWebhooksFromDb() {
4070
- if (_webhooksLoaded)
4452
+ async function autoGenerateWhenToUse(ctx) {
4453
+ if (ctx.memory.when_to_use)
4454
+ return;
4455
+ if (process.env["MEMENTOS_AUTO_WHEN_TO_USE"] !== "true")
4071
4456
  return;
4072
- _webhooksLoaded = true;
4073
4457
  try {
4074
- const webhooks = listWebhookHooks({ enabled: true });
4075
- for (const wh of webhooks) {
4076
- hookRegistry.register({
4077
- type: wh.type,
4078
- blocking: wh.blocking,
4079
- priority: wh.priority,
4080
- agentId: wh.agentId,
4081
- projectId: wh.projectId,
4082
- description: wh.description ?? `Webhook: ${wh.handlerUrl}`,
4083
- handler: makeWebhookHandler(wh.id, wh.handlerUrl)
4084
- });
4085
- }
4086
- if (webhooks.length > 0) {
4087
- console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
4458
+ const whenToUse = await generateWhenToUse(ctx.memory.key, ctx.memory.value, ctx.memory.category, ctx.memory.tags);
4459
+ if (whenToUse) {
4460
+ const db = getDatabase();
4461
+ db.run("UPDATE memories SET when_to_use = ? WHERE id = ? AND when_to_use IS NULL", [whenToUse, ctx.memory.id]);
4088
4462
  }
4089
- } catch (err) {
4090
- console.error("[hooks] Failed to load webhooks from DB:", err);
4091
- }
4463
+ } catch {}
4092
4464
  }
4093
- function makeWebhookHandler(webhookId, url) {
4094
- return async (context) => {
4095
- try {
4096
- const res = await fetch(url, {
4097
- method: "POST",
4098
- headers: { "Content-Type": "application/json" },
4099
- body: JSON.stringify(context),
4100
- signal: AbortSignal.timeout(1e4)
4101
- });
4102
- recordWebhookInvocation(webhookId, res.ok);
4103
- } catch {
4104
- recordWebhookInvocation(webhookId, false);
4465
+ var SYSTEM_PROMPT = `You generate activation contexts for memory records. Given a memory's key, value, category, and tags, output a 1-2 sentence "when to use" description that describes the SITUATION or CONDITION under which an AI agent should retrieve this memory.
4466
+
4467
+ Rules:
4468
+ - Start with "When" or "If"
4469
+ - Describe the situation, not the content
4470
+ - Be specific enough to avoid false matches but general enough to catch relevant scenarios
4471
+ - Focus on the task/action the agent would be doing, not what the memory contains
4472
+
4473
+ Examples:
4474
+ - Key: "preferred-language", Value: "Always use TypeScript, never JavaScript" \u2192 "When choosing a programming language for a new file or project"
4475
+ - Key: "db-migration-order", Value: "Always run migrations before deploying" \u2192 "When deploying or updating database schema"
4476
+ - Key: "bash-chain-bug", Value: "Bash tool mangles && chains" \u2192 "When chaining commands with && in the Bash tool"`;
4477
+ var init_when_to_use_generator = __esm(() => {
4478
+ init_database();
4479
+ });
4480
+
4481
+ // src/lib/contradiction.ts
4482
+ var exports_contradiction = {};
4483
+ __export(exports_contradiction, {
4484
+ invalidateFact: () => invalidateFact,
4485
+ detectContradiction: () => detectContradiction
4486
+ });
4487
+ function invalidateFact(oldMemoryId, newMemoryId, db) {
4488
+ const d = db || getDatabase();
4489
+ const timestamp = now();
4490
+ d.run("UPDATE memories SET valid_until = ?, updated_at = ? WHERE id = ?", [timestamp, timestamp, oldMemoryId]);
4491
+ if (newMemoryId) {
4492
+ const row = d.query("SELECT metadata FROM memories WHERE id = ?").get(newMemoryId);
4493
+ if (row) {
4494
+ const metadata = JSON.parse(row.metadata || "{}");
4495
+ metadata.supersedes_id = oldMemoryId;
4496
+ d.run("UPDATE memories SET metadata = ?, updated_at = ? WHERE id = ?", [JSON.stringify(metadata), timestamp, newMemoryId]);
4105
4497
  }
4498
+ }
4499
+ return {
4500
+ invalidated_memory_id: oldMemoryId,
4501
+ new_memory_id: newMemoryId || null,
4502
+ valid_until: timestamp,
4503
+ supersedes_id: oldMemoryId
4106
4504
  };
4107
4505
  }
4108
- function reloadWebhooks() {
4109
- _webhooksLoaded = false;
4110
- loadWebhooksFromDb();
4506
+ function heuristicContradictionScore(newValue, existingValue, newKey, existingKey) {
4507
+ if (newKey !== existingKey)
4508
+ return 0;
4509
+ const newLower = newValue.toLowerCase().trim();
4510
+ const existingLower = existingValue.toLowerCase().trim();
4511
+ if (newLower === existingLower)
4512
+ return 0;
4513
+ const newWords = new Set(newLower.split(/\s+/));
4514
+ const existingWords = new Set(existingLower.split(/\s+/));
4515
+ let overlap = 0;
4516
+ for (const w of newWords) {
4517
+ if (existingWords.has(w))
4518
+ overlap++;
4519
+ }
4520
+ const totalUnique = new Set([...newWords, ...existingWords]).size;
4521
+ const overlapRatio = totalUnique > 0 ? overlap / totalUnique : 0;
4522
+ if (overlapRatio < 0.3)
4523
+ return 0.7;
4524
+ if (overlapRatio < 0.5)
4525
+ return 0.4;
4526
+ return 0.1;
4111
4527
  }
4112
- var _processConversationTurn = null, _webhooksLoaded = false;
4113
- var init_built_in_hooks = __esm(() => {
4114
- init_hooks();
4115
- init_webhook_hooks();
4116
- hookRegistry.register({
4117
- type: "PostMemorySave",
4118
- blocking: false,
4119
- builtin: true,
4120
- priority: 100,
4121
- description: "Trigger async LLM entity extraction when a memory is saved",
4122
- handler: async (ctx) => {
4123
- if (ctx.wasUpdated)
4124
- return;
4125
- const processConversationTurn2 = await getAutoMemory();
4126
- processConversationTurn2(`${ctx.memory.key}: ${ctx.memory.value}`, {
4127
- agentId: ctx.agentId,
4128
- projectId: ctx.projectId,
4129
- sessionId: ctx.sessionId
4528
+ async function llmContradictionCheck(_newValue, _existingValue, _key) {
4529
+ const provider = providerRegistry.getAvailable();
4530
+ if (!provider) {
4531
+ return { contradicts: false, confidence: 0, reasoning: "No LLM provider available" };
4532
+ }
4533
+ try {
4534
+ return { contradicts: false, confidence: 0, reasoning: "LLM check skipped \u2014 using heuristic only" };
4535
+ } catch {
4536
+ return { contradicts: false, confidence: 0, reasoning: "LLM check failed" };
4537
+ }
4538
+ }
4539
+ async function detectContradiction(newKey, newValue, options = {}, db) {
4540
+ const d = db || getDatabase();
4541
+ const { scope, project_id, min_importance = 7, use_llm = false } = options;
4542
+ const conditions = ["key = ?", "status = 'active'", "importance >= ?"];
4543
+ const params = [newKey, min_importance];
4544
+ if (scope) {
4545
+ conditions.push("scope = ?");
4546
+ params.push(scope);
4547
+ }
4548
+ if (project_id) {
4549
+ conditions.push("project_id = ?");
4550
+ params.push(project_id);
4551
+ }
4552
+ conditions.push("(valid_until IS NULL OR valid_until > datetime('now'))");
4553
+ const sql = `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY importance DESC LIMIT 10`;
4554
+ const rows = d.query(sql).all(...params);
4555
+ if (rows.length === 0) {
4556
+ return { contradicts: false, conflicting_memory: null, confidence: 0, reasoning: "No existing memories with this key" };
4557
+ }
4558
+ let bestContradiction = {
4559
+ contradicts: false,
4560
+ conflicting_memory: null,
4561
+ confidence: 0,
4562
+ reasoning: "No contradiction detected"
4563
+ };
4564
+ for (const row of rows) {
4565
+ const existing = parseMemoryRow(row);
4566
+ const heuristicScore = heuristicContradictionScore(newValue, existing.value, newKey, existing.key);
4567
+ if (heuristicScore > bestContradiction.confidence) {
4568
+ bestContradiction = {
4569
+ contradicts: heuristicScore >= 0.5,
4570
+ conflicting_memory: existing,
4571
+ confidence: heuristicScore,
4572
+ reasoning: heuristicScore >= 0.7 ? `New value for "${newKey}" significantly differs from existing high-importance memory (importance ${existing.importance})` : heuristicScore >= 0.5 ? `New value for "${newKey}" partially conflicts with existing memory (importance ${existing.importance})` : `Minor difference detected for "${newKey}"`
4573
+ };
4574
+ }
4575
+ }
4576
+ if (use_llm && bestContradiction.confidence >= 0.3 && bestContradiction.confidence < 0.7 && bestContradiction.conflicting_memory) {
4577
+ const llmResult = await llmContradictionCheck(newValue, bestContradiction.conflicting_memory.value, newKey);
4578
+ if (llmResult.confidence > bestContradiction.confidence) {
4579
+ bestContradiction = {
4580
+ ...bestContradiction,
4581
+ contradicts: llmResult.contradicts,
4582
+ confidence: llmResult.confidence,
4583
+ reasoning: llmResult.reasoning
4584
+ };
4585
+ }
4586
+ }
4587
+ return bestContradiction;
4588
+ }
4589
+ var init_contradiction = __esm(() => {
4590
+ init_database();
4591
+ init_memories();
4592
+ init_registry();
4593
+ });
4594
+
4595
+ // src/lib/built-in-hooks.ts
4596
+ var exports_built_in_hooks = {};
4597
+ __export(exports_built_in_hooks, {
4598
+ reloadWebhooks: () => reloadWebhooks,
4599
+ loadWebhooksFromDb: () => loadWebhooksFromDb
4600
+ });
4601
+ async function getAutoMemory() {
4602
+ if (!_processConversationTurn) {
4603
+ const mod = await Promise.resolve().then(() => (init_auto_memory(), exports_auto_memory));
4604
+ _processConversationTurn = mod.processConversationTurn;
4605
+ }
4606
+ return _processConversationTurn;
4607
+ }
4608
+ function loadWebhooksFromDb() {
4609
+ if (_webhooksLoaded)
4610
+ return;
4611
+ _webhooksLoaded = true;
4612
+ try {
4613
+ const webhooks = listWebhookHooks({ enabled: true });
4614
+ for (const wh of webhooks) {
4615
+ hookRegistry.register({
4616
+ type: wh.type,
4617
+ blocking: wh.blocking,
4618
+ priority: wh.priority,
4619
+ agentId: wh.agentId,
4620
+ projectId: wh.projectId,
4621
+ description: wh.description ?? `Webhook: ${wh.handlerUrl}`,
4622
+ handler: makeWebhookHandler(wh.id, wh.handlerUrl)
4623
+ });
4624
+ }
4625
+ if (webhooks.length > 0) {
4626
+ console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
4627
+ }
4628
+ } catch (err) {
4629
+ console.error("[hooks] Failed to load webhooks from DB:", err);
4630
+ }
4631
+ }
4632
+ function makeWebhookHandler(webhookId, url) {
4633
+ return async (context) => {
4634
+ try {
4635
+ const res = await fetch(url, {
4636
+ method: "POST",
4637
+ headers: { "Content-Type": "application/json" },
4638
+ body: JSON.stringify(context),
4639
+ signal: AbortSignal.timeout(1e4)
4640
+ });
4641
+ recordWebhookInvocation(webhookId, res.ok);
4642
+ } catch {
4643
+ recordWebhookInvocation(webhookId, false);
4644
+ }
4645
+ };
4646
+ }
4647
+ function reloadWebhooks() {
4648
+ _webhooksLoaded = false;
4649
+ loadWebhooksFromDb();
4650
+ }
4651
+ var _processConversationTurn = null, _webhooksLoaded = false;
4652
+ var init_built_in_hooks = __esm(() => {
4653
+ init_hooks();
4654
+ init_webhook_hooks();
4655
+ hookRegistry.register({
4656
+ type: "PostMemorySave",
4657
+ blocking: false,
4658
+ builtin: true,
4659
+ priority: 100,
4660
+ description: "Trigger async LLM entity extraction when a memory is saved",
4661
+ handler: async (ctx) => {
4662
+ if (ctx.wasUpdated)
4663
+ return;
4664
+ const processConversationTurn2 = await getAutoMemory();
4665
+ processConversationTurn2(`${ctx.memory.key}: ${ctx.memory.value}`, {
4666
+ agentId: ctx.agentId,
4667
+ projectId: ctx.projectId,
4668
+ sessionId: ctx.sessionId
4130
4669
  });
4131
4670
  }
4132
4671
  });
@@ -4179,10 +4718,76 @@ var init_built_in_hooks = __esm(() => {
4179
4718
  description: "Generate and store vector embedding for semantic memory search",
4180
4719
  handler: async (ctx) => {
4181
4720
  const { indexMemoryEmbedding: indexMemoryEmbedding2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
4182
- const text = [ctx.memory.value, ctx.memory.summary].filter(Boolean).join(" ");
4721
+ const text = ctx.memory.when_to_use || [ctx.memory.value, ctx.memory.summary].filter(Boolean).join(" ");
4183
4722
  indexMemoryEmbedding2(ctx.memory.id, text);
4184
4723
  }
4185
4724
  });
4725
+ hookRegistry.register({
4726
+ type: "PostMemorySave",
4727
+ blocking: false,
4728
+ builtin: true,
4729
+ priority: 60,
4730
+ description: "Auto-generate when_to_use activation context via LLM if missing",
4731
+ handler: async (ctx) => {
4732
+ const { autoGenerateWhenToUse: autoGenerateWhenToUse2 } = await Promise.resolve().then(() => (init_when_to_use_generator(), exports_when_to_use_generator));
4733
+ await autoGenerateWhenToUse2(ctx);
4734
+ }
4735
+ });
4736
+ hookRegistry.register({
4737
+ type: "PostMemorySave",
4738
+ blocking: false,
4739
+ builtin: true,
4740
+ priority: 65,
4741
+ description: "Mark synthesized profile as stale when a preference or fact memory is saved",
4742
+ handler: async (ctx) => {
4743
+ const category = ctx.memory?.category;
4744
+ if (category !== "preference" && category !== "fact")
4745
+ return;
4746
+ try {
4747
+ const { markProfileStale: markProfileStale2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
4748
+ markProfileStale2(ctx.projectId, ctx.agentId);
4749
+ } catch {}
4750
+ }
4751
+ });
4752
+ hookRegistry.register({
4753
+ type: "PostMemorySave",
4754
+ blocking: false,
4755
+ builtin: true,
4756
+ priority: 70,
4757
+ description: "Auto-decay importance of existing memories contradicted by the newly saved memory",
4758
+ handler: async (ctx) => {
4759
+ if (ctx.wasUpdated)
4760
+ return;
4761
+ const memory = ctx.memory;
4762
+ if (memory.category !== "fact" && memory.category !== "knowledge")
4763
+ return;
4764
+ try {
4765
+ const { detectContradiction: detectContradiction2 } = await Promise.resolve().then(() => (init_contradiction(), exports_contradiction));
4766
+ const { updateMemory: updateMemory2, getMemory: getMemory2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
4767
+ const result = await detectContradiction2(memory.key, memory.value, {
4768
+ scope: memory.scope,
4769
+ project_id: ctx.projectId,
4770
+ min_importance: 1
4771
+ });
4772
+ if (!result.contradicts || !result.conflicting_memory)
4773
+ return;
4774
+ const conflicting = result.conflicting_memory;
4775
+ if (conflicting.id === memory.id)
4776
+ return;
4777
+ const fresh = getMemory2(conflicting.id);
4778
+ if (!fresh || fresh.status !== "active")
4779
+ return;
4780
+ const halvedImportance = Math.max(1, Math.floor(fresh.importance / 2));
4781
+ const metadata = { ...fresh.metadata || {}, contradicted_by: memory.id };
4782
+ updateMemory2(fresh.id, {
4783
+ importance: halvedImportance,
4784
+ flag: "contradicted",
4785
+ metadata,
4786
+ version: fresh.version
4787
+ });
4788
+ } catch {}
4789
+ }
4790
+ });
4186
4791
  hookRegistry.register({
4187
4792
  type: "PostMemoryInject",
4188
4793
  blocking: false,
@@ -4236,156 +4841,42 @@ var init_memory_broadcast = __esm(() => {
4236
4841
  CONVERSATIONS_API = process.env.CONVERSATIONS_API_URL || "http://localhost:7020";
4237
4842
  });
4238
4843
 
4239
- // src/lib/contradiction.ts
4240
- var exports_contradiction = {};
4241
- __export(exports_contradiction, {
4242
- invalidateFact: () => invalidateFact,
4243
- detectContradiction: () => detectContradiction
4844
+ // src/db/audit.ts
4845
+ var exports_audit = {};
4846
+ __export(exports_audit, {
4847
+ getMemoryAuditTrail: () => getMemoryAuditTrail,
4848
+ getAuditStats: () => getAuditStats,
4849
+ exportAuditLog: () => exportAuditLog
4244
4850
  });
4245
- function invalidateFact(oldMemoryId, newMemoryId, db) {
4246
- const d = db || getDatabase();
4247
- const timestamp = now();
4248
- d.run("UPDATE memories SET valid_until = ?, updated_at = ? WHERE id = ?", [timestamp, timestamp, oldMemoryId]);
4249
- if (newMemoryId) {
4250
- const row = d.query("SELECT metadata FROM memories WHERE id = ?").get(newMemoryId);
4251
- if (row) {
4252
- const metadata = JSON.parse(row.metadata || "{}");
4253
- metadata.supersedes_id = oldMemoryId;
4254
- d.run("UPDATE memories SET metadata = ?, updated_at = ? WHERE id = ?", [JSON.stringify(metadata), timestamp, newMemoryId]);
4255
- }
4256
- }
4851
+ function parseAuditRow(row) {
4257
4852
  return {
4258
- invalidated_memory_id: oldMemoryId,
4259
- new_memory_id: newMemoryId || null,
4260
- valid_until: timestamp,
4261
- supersedes_id: oldMemoryId
4853
+ id: row["id"],
4854
+ memory_id: row["memory_id"],
4855
+ memory_key: row["memory_key"] || null,
4856
+ operation: row["operation"],
4857
+ agent_id: row["agent_id"] || null,
4858
+ old_value_hash: row["old_value_hash"] || null,
4859
+ new_value_hash: row["new_value_hash"] || null,
4860
+ changes: JSON.parse(row["changes"] || "{}"),
4861
+ created_at: row["created_at"]
4262
4862
  };
4263
4863
  }
4264
- function heuristicContradictionScore(newValue, existingValue, newKey, existingKey) {
4265
- if (newKey !== existingKey)
4266
- return 0;
4267
- const newLower = newValue.toLowerCase().trim();
4268
- const existingLower = existingValue.toLowerCase().trim();
4269
- if (newLower === existingLower)
4270
- return 0;
4271
- const newWords = new Set(newLower.split(/\s+/));
4272
- const existingWords = new Set(existingLower.split(/\s+/));
4273
- let overlap = 0;
4274
- for (const w of newWords) {
4275
- if (existingWords.has(w))
4276
- overlap++;
4277
- }
4278
- const totalUnique = new Set([...newWords, ...existingWords]).size;
4279
- const overlapRatio = totalUnique > 0 ? overlap / totalUnique : 0;
4280
- if (overlapRatio < 0.3)
4281
- return 0.7;
4282
- if (overlapRatio < 0.5)
4283
- return 0.4;
4284
- return 0.1;
4864
+ function getMemoryAuditTrail(memoryId, limit = 50, db) {
4865
+ const d = db || getDatabase();
4866
+ const rows = d.query("SELECT * FROM memory_audit_log WHERE memory_id = ? ORDER BY created_at DESC LIMIT ?").all(memoryId, limit);
4867
+ return rows.map(parseAuditRow);
4285
4868
  }
4286
- async function llmContradictionCheck(_newValue, _existingValue, _key) {
4287
- const provider = providerRegistry.getAvailable();
4288
- if (!provider) {
4289
- return { contradicts: false, confidence: 0, reasoning: "No LLM provider available" };
4869
+ function exportAuditLog(options = {}, db) {
4870
+ const d = db || getDatabase();
4871
+ const conditions = [];
4872
+ const params = [];
4873
+ if (options.since) {
4874
+ conditions.push("created_at >= ?");
4875
+ params.push(options.since);
4290
4876
  }
4291
- try {
4292
- return { contradicts: false, confidence: 0, reasoning: "LLM check skipped \u2014 using heuristic only" };
4293
- } catch {
4294
- return { contradicts: false, confidence: 0, reasoning: "LLM check failed" };
4295
- }
4296
- }
4297
- async function detectContradiction(newKey, newValue, options = {}, db) {
4298
- const d = db || getDatabase();
4299
- const { scope, project_id, min_importance = 7, use_llm = false } = options;
4300
- const conditions = ["key = ?", "status = 'active'", "importance >= ?"];
4301
- const params = [newKey, min_importance];
4302
- if (scope) {
4303
- conditions.push("scope = ?");
4304
- params.push(scope);
4305
- }
4306
- if (project_id) {
4307
- conditions.push("project_id = ?");
4308
- params.push(project_id);
4309
- }
4310
- conditions.push("(valid_until IS NULL OR valid_until > datetime('now'))");
4311
- const sql = `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY importance DESC LIMIT 10`;
4312
- const rows = d.query(sql).all(...params);
4313
- if (rows.length === 0) {
4314
- return { contradicts: false, conflicting_memory: null, confidence: 0, reasoning: "No existing memories with this key" };
4315
- }
4316
- let bestContradiction = {
4317
- contradicts: false,
4318
- conflicting_memory: null,
4319
- confidence: 0,
4320
- reasoning: "No contradiction detected"
4321
- };
4322
- for (const row of rows) {
4323
- const existing = parseMemoryRow(row);
4324
- const heuristicScore = heuristicContradictionScore(newValue, existing.value, newKey, existing.key);
4325
- if (heuristicScore > bestContradiction.confidence) {
4326
- bestContradiction = {
4327
- contradicts: heuristicScore >= 0.5,
4328
- conflicting_memory: existing,
4329
- confidence: heuristicScore,
4330
- reasoning: heuristicScore >= 0.7 ? `New value for "${newKey}" significantly differs from existing high-importance memory (importance ${existing.importance})` : heuristicScore >= 0.5 ? `New value for "${newKey}" partially conflicts with existing memory (importance ${existing.importance})` : `Minor difference detected for "${newKey}"`
4331
- };
4332
- }
4333
- }
4334
- if (use_llm && bestContradiction.confidence >= 0.3 && bestContradiction.confidence < 0.7 && bestContradiction.conflicting_memory) {
4335
- const llmResult = await llmContradictionCheck(newValue, bestContradiction.conflicting_memory.value, newKey);
4336
- if (llmResult.confidence > bestContradiction.confidence) {
4337
- bestContradiction = {
4338
- ...bestContradiction,
4339
- contradicts: llmResult.contradicts,
4340
- confidence: llmResult.confidence,
4341
- reasoning: llmResult.reasoning
4342
- };
4343
- }
4344
- }
4345
- return bestContradiction;
4346
- }
4347
- var init_contradiction = __esm(() => {
4348
- init_database();
4349
- init_memories();
4350
- init_registry();
4351
- });
4352
-
4353
- // src/db/audit.ts
4354
- var exports_audit = {};
4355
- __export(exports_audit, {
4356
- getMemoryAuditTrail: () => getMemoryAuditTrail,
4357
- getAuditStats: () => getAuditStats,
4358
- exportAuditLog: () => exportAuditLog
4359
- });
4360
- function parseAuditRow(row) {
4361
- return {
4362
- id: row["id"],
4363
- memory_id: row["memory_id"],
4364
- memory_key: row["memory_key"] || null,
4365
- operation: row["operation"],
4366
- agent_id: row["agent_id"] || null,
4367
- old_value_hash: row["old_value_hash"] || null,
4368
- new_value_hash: row["new_value_hash"] || null,
4369
- changes: JSON.parse(row["changes"] || "{}"),
4370
- created_at: row["created_at"]
4371
- };
4372
- }
4373
- function getMemoryAuditTrail(memoryId, limit = 50, db) {
4374
- const d = db || getDatabase();
4375
- const rows = d.query("SELECT * FROM memory_audit_log WHERE memory_id = ? ORDER BY created_at DESC LIMIT ?").all(memoryId, limit);
4376
- return rows.map(parseAuditRow);
4377
- }
4378
- function exportAuditLog(options = {}, db) {
4379
- const d = db || getDatabase();
4380
- const conditions = [];
4381
- const params = [];
4382
- if (options.since) {
4383
- conditions.push("created_at >= ?");
4384
- params.push(options.since);
4385
- }
4386
- if (options.until) {
4387
- conditions.push("created_at <= ?");
4388
- params.push(options.until);
4877
+ if (options.until) {
4878
+ conditions.push("created_at <= ?");
4879
+ params.push(options.until);
4389
4880
  }
4390
4881
  if (options.operation) {
4391
4882
  conditions.push("operation = ?");
@@ -4560,55 +5051,712 @@ var init_export_v1 = __esm(() => {
4560
5051
  init_database();
4561
5052
  });
4562
5053
 
4563
- // src/lib/context.ts
4564
- var exports_context = {};
4565
- __export(exports_context, {
4566
- formatLayeredContext: () => formatLayeredContext,
4567
- assembleContext: () => assembleContext
4568
- });
4569
- function assembleContext(options = {}, db) {
4570
- const { project_id, agent_id, scope, max_per_section = 10 } = options;
4571
- const baseFilter = {
4572
- project_id,
4573
- agent_id,
4574
- scope
4575
- };
4576
- const sections = [];
4577
- const coreFacts = listMemories({ ...baseFilter, category: ["fact", "preference"], min_importance: 8, limit: max_per_section }, db);
4578
- if (coreFacts.length > 0) {
4579
- sections.push({ title: "Core Facts", memories: coreFacts });
5054
+ // src/lib/config.ts
5055
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync, unlinkSync } from "fs";
5056
+ import { homedir } from "os";
5057
+ import { basename as basename3, dirname as dirname5, join as join6, resolve as resolve4 } from "path";
5058
+ function deepMerge(target, source) {
5059
+ const result = { ...target };
5060
+ for (const key of Object.keys(source)) {
5061
+ const sourceVal = source[key];
5062
+ const targetVal = result[key];
5063
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
5064
+ result[key] = deepMerge(targetVal, sourceVal);
5065
+ } else {
5066
+ result[key] = sourceVal;
5067
+ }
4580
5068
  }
4581
- const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
4582
- const allRecent = listMemories({ ...baseFilter, limit: max_per_section * 3 }, db).filter((m) => m.created_at > oneDayAgo || m.accessed_at && m.accessed_at > oneDayAgo);
4583
- const recentSlice = allRecent.slice(0, max_per_section);
4584
- if (recentSlice.length > 0) {
4585
- sections.push({ title: "Recent History", memories: recentSlice });
5069
+ return result;
5070
+ }
5071
+ function isValidScope(value) {
5072
+ return VALID_SCOPES.includes(value);
5073
+ }
5074
+ function isValidCategory(value) {
5075
+ return VALID_CATEGORIES.includes(value);
5076
+ }
5077
+ function loadConfig() {
5078
+ const configPath = join6(homedir(), ".mementos", "config.json");
5079
+ let fileConfig = {};
5080
+ if (existsSync6(configPath)) {
5081
+ try {
5082
+ const raw = readFileSync3(configPath, "utf-8");
5083
+ fileConfig = JSON.parse(raw);
5084
+ } catch {}
4586
5085
  }
4587
- if (options.query) {
4588
- const knowledge = listMemories({ ...baseFilter, category: "knowledge", search: options.query, limit: max_per_section }, db);
4589
- if (knowledge.length > 0) {
4590
- sections.push({ title: "Relevant Knowledge", memories: knowledge });
5086
+ const merged = deepMerge(DEFAULT_CONFIG2, fileConfig);
5087
+ const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
5088
+ if (envScope && isValidScope(envScope)) {
5089
+ merged.default_scope = envScope;
5090
+ }
5091
+ const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
5092
+ if (envCategory && isValidCategory(envCategory)) {
5093
+ merged.default_category = envCategory;
5094
+ }
5095
+ const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
5096
+ if (envImportance) {
5097
+ const parsed = parseInt(envImportance, 10);
5098
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
5099
+ merged.default_importance = parsed;
4591
5100
  }
4592
5101
  }
4593
- const decisions = listMemories({ ...baseFilter, category: "fact", limit: max_per_section }, db);
4594
- const coreFactIds = new Set(coreFacts.map((m) => m.id));
4595
- const newDecisions = decisions.filter((m) => !coreFactIds.has(m.id));
4596
- if (newDecisions.length > 0) {
4597
- sections.push({ title: "Active Decisions", memories: newDecisions.slice(0, max_per_section) });
5102
+ return merged;
5103
+ }
5104
+ var DEFAULT_CONFIG2, VALID_SCOPES, VALID_CATEGORIES;
5105
+ var init_config = __esm(() => {
5106
+ DEFAULT_CONFIG2 = {
5107
+ default_scope: "private",
5108
+ default_category: "knowledge",
5109
+ default_importance: 5,
5110
+ max_entries: 1000,
5111
+ max_entries_per_scope: {
5112
+ global: 500,
5113
+ shared: 300,
5114
+ private: 200,
5115
+ working: 100
5116
+ },
5117
+ injection: {
5118
+ max_tokens: 500,
5119
+ min_importance: 5,
5120
+ categories: ["preference", "fact"],
5121
+ refresh_interval: 5
5122
+ },
5123
+ extraction: {
5124
+ enabled: true,
5125
+ min_confidence: 0.5
5126
+ },
5127
+ sync_agents: ["claude", "codex", "gemini"],
5128
+ auto_cleanup: {
5129
+ enabled: true,
5130
+ expired_check_interval: 3600,
5131
+ unused_archive_days: 7,
5132
+ stale_deprioritize_days: 14
5133
+ }
5134
+ };
5135
+ VALID_SCOPES = ["global", "shared", "private", "working"];
5136
+ VALID_CATEGORIES = [
5137
+ "preference",
5138
+ "fact",
5139
+ "knowledge",
5140
+ "history"
5141
+ ];
5142
+ });
5143
+
5144
+ // src/lib/injector.ts
5145
+ var exports_injector = {};
5146
+ __export(exports_injector, {
5147
+ smartInject: () => smartInject,
5148
+ MemoryInjector: () => MemoryInjector
5149
+ });
5150
+
5151
+ class MemoryInjector {
5152
+ config;
5153
+ injectedIds = new Set;
5154
+ constructor(config) {
5155
+ this.config = config || loadConfig();
4598
5156
  }
4599
- const seen = new Set;
4600
- for (const section of sections) {
4601
- section.memories = section.memories.filter((m) => {
5157
+ getInjectionContext(options = {}) {
5158
+ const maxTokens = options.max_tokens || this.config.injection.max_tokens;
5159
+ const minImportance = options.min_importance || this.config.injection.min_importance;
5160
+ const categories = options.categories || this.config.injection.categories;
5161
+ const db = options.db;
5162
+ const allMemories = [];
5163
+ const globalMems = listMemories({
5164
+ scope: "global",
5165
+ category: categories,
5166
+ min_importance: minImportance,
5167
+ status: "active",
5168
+ limit: 100
5169
+ }, db);
5170
+ allMemories.push(...globalMems);
5171
+ if (options.project_id) {
5172
+ const sharedMems = listMemories({
5173
+ scope: "shared",
5174
+ category: categories,
5175
+ min_importance: minImportance,
5176
+ status: "active",
5177
+ project_id: options.project_id,
5178
+ limit: 100
5179
+ }, db);
5180
+ allMemories.push(...sharedMems);
5181
+ }
5182
+ if (options.agent_id) {
5183
+ const privateMems = listMemories({
5184
+ scope: "private",
5185
+ category: categories,
5186
+ min_importance: minImportance,
5187
+ status: "active",
5188
+ agent_id: options.agent_id,
5189
+ limit: 100
5190
+ }, db);
5191
+ allMemories.push(...privateMems);
5192
+ }
5193
+ if (options.session_id || options.agent_id) {
5194
+ const workingMems = listMemories({
5195
+ scope: "working",
5196
+ status: "active",
5197
+ ...options.session_id ? { session_id: options.session_id } : {},
5198
+ ...options.agent_id ? { agent_id: options.agent_id } : {},
5199
+ ...options.project_id ? { project_id: options.project_id } : {},
5200
+ limit: 100
5201
+ }, db);
5202
+ allMemories.push(...workingMems);
5203
+ }
5204
+ const seen = new Set;
5205
+ const unique = allMemories.filter((m) => {
4602
5206
  if (seen.has(m.id))
4603
5207
  return false;
4604
5208
  seen.add(m.id);
4605
5209
  return true;
4606
5210
  });
5211
+ if (unique.length === 0) {
5212
+ return "";
5213
+ }
5214
+ const totalCharBudget = maxTokens * 4;
5215
+ const footer = "Tip: Use memory_search for deeper lookup on specific topics.";
5216
+ const footerChars = footer.length;
5217
+ const keyBudget = Math.floor((totalCharBudget - footerChars) * 0.67);
5218
+ const recentBudget = Math.floor((totalCharBudget - footerChars) * 0.33);
5219
+ const keyRanked = [...unique].sort((a, b) => {
5220
+ if (a.pinned !== b.pinned)
5221
+ return a.pinned ? -1 : 1;
5222
+ if (b.importance !== a.importance)
5223
+ return b.importance - a.importance;
5224
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
5225
+ });
5226
+ const keyLines = [];
5227
+ const keyIds = new Set;
5228
+ let keyChars = 0;
5229
+ for (const m of keyRanked) {
5230
+ if (this.injectedIds.has(m.id))
5231
+ continue;
5232
+ const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
5233
+ if (keyChars + line.length > keyBudget)
5234
+ break;
5235
+ keyLines.push(line);
5236
+ keyIds.add(m.id);
5237
+ keyChars += line.length;
5238
+ }
5239
+ const recentRanked = [...unique].sort((a, b) => {
5240
+ const aTime = a.accessed_at ? new Date(a.accessed_at).getTime() : 0;
5241
+ const bTime = b.accessed_at ? new Date(b.accessed_at).getTime() : 0;
5242
+ return bTime - aTime;
5243
+ });
5244
+ const recentLines = [];
5245
+ let recentChars = 0;
5246
+ const maxRecent = 5;
5247
+ for (const m of recentRanked) {
5248
+ if (recentLines.length >= maxRecent)
5249
+ break;
5250
+ if (keyIds.has(m.id))
5251
+ continue;
5252
+ if (this.injectedIds.has(m.id))
5253
+ continue;
5254
+ const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
5255
+ if (recentChars + line.length > recentBudget)
5256
+ break;
5257
+ recentLines.push(line);
5258
+ recentChars += line.length;
5259
+ }
5260
+ if (keyLines.length === 0 && recentLines.length === 0) {
5261
+ return "";
5262
+ }
5263
+ const allInjectedMemoryIds = [
5264
+ ...keyIds,
5265
+ ...recentLines.map((_, i) => {
5266
+ let idx = 0;
5267
+ for (const m of recentRanked) {
5268
+ if (keyIds.has(m.id) || this.injectedIds.has(m.id))
5269
+ continue;
5270
+ if (idx === i)
5271
+ return m.id;
5272
+ idx++;
5273
+ }
5274
+ return "";
5275
+ })
5276
+ ].filter(Boolean);
5277
+ for (const id of allInjectedMemoryIds) {
5278
+ this.injectedIds.add(id);
5279
+ touchMemory(id, db);
5280
+ }
5281
+ const sections = [];
5282
+ if (keyLines.length > 0) {
5283
+ sections.push(`## Key Memories
5284
+ ${keyLines.join(`
5285
+ `)}`);
5286
+ }
5287
+ if (recentLines.length > 0) {
5288
+ sections.push(`## Recent Context
5289
+ ${recentLines.join(`
5290
+ `)}`);
5291
+ }
5292
+ sections.push(footer);
5293
+ return `<agent-memories>
5294
+ ${sections.join(`
5295
+
5296
+ `)}
5297
+ </agent-memories>`;
4607
5298
  }
4608
- const allMemories = sections.flatMap((s) => s.memories);
4609
- const tokenEstimate = allMemories.reduce((acc, m) => acc + Math.ceil((m.key.length + m.value.length) / 4), 0);
4610
- return {
4611
- sections: sections.filter((s) => s.memories.length > 0),
5299
+ async getSmartInjectionContext(options = {}) {
5300
+ if (!options.query) {
5301
+ return this.getInjectionContext(options);
5302
+ }
5303
+ const maxTokens = options.max_tokens || this.config.injection.max_tokens;
5304
+ const minImportance = options.min_importance || this.config.injection.min_importance;
5305
+ const categories = options.categories || this.config.injection.categories;
5306
+ const db = options.db;
5307
+ const allMemories = [];
5308
+ const globalMems = listMemories({
5309
+ scope: "global",
5310
+ category: categories,
5311
+ min_importance: minImportance,
5312
+ status: "active",
5313
+ limit: 100
5314
+ }, db);
5315
+ allMemories.push(...globalMems);
5316
+ if (options.project_id) {
5317
+ const sharedMems = listMemories({
5318
+ scope: "shared",
5319
+ category: categories,
5320
+ min_importance: minImportance,
5321
+ status: "active",
5322
+ project_id: options.project_id,
5323
+ limit: 100
5324
+ }, db);
5325
+ allMemories.push(...sharedMems);
5326
+ }
5327
+ if (options.agent_id) {
5328
+ const privateMems = listMemories({
5329
+ scope: "private",
5330
+ category: categories,
5331
+ min_importance: minImportance,
5332
+ status: "active",
5333
+ agent_id: options.agent_id,
5334
+ limit: 100
5335
+ }, db);
5336
+ allMemories.push(...privateMems);
5337
+ }
5338
+ if (options.session_id || options.agent_id) {
5339
+ const workingMems = listMemories({
5340
+ scope: "working",
5341
+ status: "active",
5342
+ ...options.session_id ? { session_id: options.session_id } : {},
5343
+ ...options.agent_id ? { agent_id: options.agent_id } : {},
5344
+ ...options.project_id ? { project_id: options.project_id } : {},
5345
+ limit: 100
5346
+ }, db);
5347
+ allMemories.push(...workingMems);
5348
+ }
5349
+ const seen = new Set;
5350
+ const unique = allMemories.filter((m) => {
5351
+ if (seen.has(m.id))
5352
+ return false;
5353
+ seen.add(m.id);
5354
+ return true;
5355
+ });
5356
+ if (unique.length === 0) {
5357
+ return "";
5358
+ }
5359
+ const { embedding: queryEmbedding } = await generateEmbedding(options.query);
5360
+ const d = db || (await Promise.resolve().then(() => (init_database(), exports_database))).getDatabase();
5361
+ const embeddingRows = d.prepare(`SELECT memory_id, embedding FROM memory_embeddings WHERE memory_id IN (${unique.map(() => "?").join(",")})`).all(...unique.map((m) => m.id));
5362
+ const embeddingMap = new Map;
5363
+ for (const row of embeddingRows) {
5364
+ try {
5365
+ embeddingMap.set(row.memory_id, deserializeEmbedding(row.embedding));
5366
+ } catch {}
5367
+ }
5368
+ if (embeddingMap.size === 0) {
5369
+ return this.getInjectionContext(options);
5370
+ }
5371
+ const nowMs = Date.now();
5372
+ const oldestMs = Math.min(...unique.map((m) => new Date(m.updated_at).getTime()));
5373
+ const timeRange = nowMs - oldestMs || 1;
5374
+ const scored = unique.filter((m) => !this.injectedIds.has(m.id)).map((m) => {
5375
+ const memEmbedding = embeddingMap.get(m.id);
5376
+ const similarity = memEmbedding ? cosineSimilarity(queryEmbedding, memEmbedding) : 0;
5377
+ const importanceScore = m.importance / 10;
5378
+ const recencyScore = (new Date(m.updated_at).getTime() - oldestMs) / timeRange;
5379
+ const pinBonus = m.pinned ? 0.5 : 0;
5380
+ const score = similarity * 0.4 + importanceScore * 0.3 + recencyScore * 0.3 + pinBonus;
5381
+ return { memory: m, score };
5382
+ });
5383
+ scored.sort((a, b) => b.score - a.score);
5384
+ const totalCharBudget = maxTokens * 4;
5385
+ const footer = "Tip: Use memory_search for deeper lookup on specific topics.";
5386
+ const footerChars = footer.length;
5387
+ const contentBudget = totalCharBudget - footerChars;
5388
+ const lines = [];
5389
+ const injectedIds = [];
5390
+ let totalChars = 0;
5391
+ for (const { memory: m } of scored) {
5392
+ const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
5393
+ if (totalChars + line.length > contentBudget)
5394
+ break;
5395
+ lines.push(line);
5396
+ injectedIds.push(m.id);
5397
+ totalChars += line.length;
5398
+ }
5399
+ if (lines.length === 0) {
5400
+ return "";
5401
+ }
5402
+ for (const id of injectedIds) {
5403
+ this.injectedIds.add(id);
5404
+ touchMemory(id, db);
5405
+ }
5406
+ const sections = [];
5407
+ sections.push(`## Relevant Memories
5408
+ ${lines.join(`
5409
+ `)}`);
5410
+ sections.push(footer);
5411
+ return `<agent-memories>
5412
+ ${sections.join(`
5413
+
5414
+ `)}
5415
+ </agent-memories>`;
5416
+ }
5417
+ resetDedup() {
5418
+ this.injectedIds.clear();
5419
+ }
5420
+ getInjectedCount() {
5421
+ return this.injectedIds.size;
5422
+ }
5423
+ }
5424
+ function detectToolMentions(taskContext) {
5425
+ const tools = new Set;
5426
+ const mcpToolPattern = /\b(memory_\w+|entity_\w+|graph_\w+|relation_\w+|session_\w+|webhook_\w+|hook_\w+)\b/gi;
5427
+ let match;
5428
+ while ((match = mcpToolPattern.exec(taskContext)) !== null) {
5429
+ tools.add(match[1].toLowerCase());
5430
+ }
5431
+ const cliPattern = /\b(git|npm|bun|curl|docker|kubectl|mementos)\b/gi;
5432
+ while ((match = cliPattern.exec(taskContext)) !== null) {
5433
+ tools.add(match[1].toLowerCase());
5434
+ }
5435
+ return Array.from(tools);
5436
+ }
5437
+ function hasToolContext(taskContext) {
5438
+ return TOOL_KEYWORD_PATTERNS.some((p) => p.test(taskContext));
5439
+ }
5440
+ function categoryToSection(category, key) {
5441
+ if (key.startsWith("_profile_"))
5442
+ return "Profile";
5443
+ switch (category) {
5444
+ case "fact":
5445
+ return "Core Facts";
5446
+ case "procedural":
5447
+ return "Procedures";
5448
+ case "preference":
5449
+ return "Preferences";
5450
+ case "history":
5451
+ return "Recent History";
5452
+ case "knowledge":
5453
+ return "Core Facts";
5454
+ case "resource":
5455
+ return "Core Facts";
5456
+ default:
5457
+ return "Core Facts";
5458
+ }
5459
+ }
5460
+ function collectVisibleMemories(options, db) {
5461
+ const allMemories = [];
5462
+ const minImportance = options.min_importance ?? 1;
5463
+ allMemories.push(...listMemories({ scope: "global", min_importance: minImportance, status: "active", limit: 100 }, db));
5464
+ if (options.project_id) {
5465
+ allMemories.push(...listMemories({
5466
+ scope: "shared",
5467
+ min_importance: minImportance,
5468
+ status: "active",
5469
+ project_id: options.project_id,
5470
+ limit: 100
5471
+ }, db));
5472
+ }
5473
+ if (options.agent_id) {
5474
+ allMemories.push(...listMemories({
5475
+ scope: "private",
5476
+ min_importance: minImportance,
5477
+ status: "active",
5478
+ agent_id: options.agent_id,
5479
+ limit: 100
5480
+ }, db));
5481
+ }
5482
+ if (options.session_id || options.agent_id) {
5483
+ allMemories.push(...listMemories({
5484
+ scope: "working",
5485
+ status: "active",
5486
+ ...options.session_id ? { session_id: options.session_id } : {},
5487
+ ...options.agent_id ? { agent_id: options.agent_id } : {},
5488
+ ...options.project_id ? { project_id: options.project_id } : {},
5489
+ limit: 100
5490
+ }, db));
5491
+ }
5492
+ const seen = new Set;
5493
+ return allMemories.filter((m) => {
5494
+ if (seen.has(m.id))
5495
+ return false;
5496
+ seen.add(m.id);
5497
+ return true;
5498
+ });
5499
+ }
5500
+ function formatMemoryLine(m) {
5501
+ const value = m.value.length > 200 ? m.value.slice(0, 197) + "..." : m.value;
5502
+ return `- **${m.key}**: ${value}`;
5503
+ }
5504
+ async function smartInject(options) {
5505
+ const config = loadConfig();
5506
+ const maxTokens = options.max_tokens || config.injection.max_tokens;
5507
+ const minImportance = options.min_importance ?? config.injection.min_importance;
5508
+ const db = options.db;
5509
+ const taskContext = options.task_context;
5510
+ const totalCharBudget = maxTokens * 4;
5511
+ let profileText = null;
5512
+ let profileFromCache = false;
5513
+ try {
5514
+ const profileResult = await synthesizeProfile({
5515
+ project_id: options.project_id,
5516
+ agent_id: options.agent_id,
5517
+ scope: options.project_id ? "project" : options.agent_id ? "agent" : "global",
5518
+ force_refresh: options.force_profile_refresh
5519
+ });
5520
+ if (profileResult) {
5521
+ profileText = profileResult.profile;
5522
+ profileFromCache = profileResult.from_cache;
5523
+ }
5524
+ } catch {}
5525
+ const candidates = collectVisibleMemories({
5526
+ project_id: options.project_id,
5527
+ agent_id: options.agent_id,
5528
+ session_id: options.session_id,
5529
+ min_importance: minImportance
5530
+ }, db);
5531
+ const activationMap = new Map;
5532
+ try {
5533
+ const semanticResults = await semanticSearch(taskContext, {
5534
+ threshold: 0.25,
5535
+ limit: 50,
5536
+ project_id: options.project_id,
5537
+ agent_id: options.agent_id
5538
+ }, db);
5539
+ for (const result of semanticResults) {
5540
+ activationMap.set(result.memory.id, result.score);
5541
+ }
5542
+ } catch {}
5543
+ const detectedTools = detectToolMentions(taskContext);
5544
+ const includeToolGuides = hasToolContext(taskContext) || detectedTools.length > 0;
5545
+ const toolGuides = [];
5546
+ if (includeToolGuides) {
5547
+ for (const toolName of detectedTools.slice(0, 5)) {
5548
+ try {
5549
+ const stats = getToolStats(toolName, options.project_id, db);
5550
+ const lessons = getToolLessons(toolName, options.project_id, 5, db);
5551
+ if (stats.total_calls > 0 || lessons.length > 0) {
5552
+ const statsLine = stats.total_calls > 0 ? `${stats.total_calls} calls, ${Math.round(stats.success_rate * 100)}% success` + (stats.avg_latency_ms ? `, ~${Math.round(stats.avg_latency_ms)}ms avg` : "") : "no usage data";
5553
+ const lessonLines = lessons.map((l) => ` - ${l.lesson}${l.when_to_use ? ` (when: ${l.when_to_use})` : ""}`);
5554
+ toolGuides.push({ tool_name: toolName, stats_line: statsLine, lessons: lessonLines });
5555
+ }
5556
+ } catch {}
5557
+ }
5558
+ }
5559
+ const scored = candidates.map((m) => {
5560
+ const decay = computeDecayScore(m);
5561
+ const activation = activationMap.get(m.id) ?? 0;
5562
+ const normalizedImportance = m.importance / 10;
5563
+ const pinBonus = m.pinned ? 1 : 0;
5564
+ const normalizedDecay = Math.min(decay / 10, 1);
5565
+ const score = activation * 0.35 + normalizedDecay * 0.35 + normalizedImportance * 0.2 + pinBonus * 0.1;
5566
+ return { memory: m, score, activation, decay };
5567
+ });
5568
+ scored.sort((a, b) => b.score - a.score);
5569
+ const footer = "Tip: Use memory_search for deeper lookup on specific topics.";
5570
+ const headerOverhead = 200;
5571
+ const availableBudget = totalCharBudget - footer.length - headerOverhead;
5572
+ const profileBudget = profileText ? Math.floor(availableBudget * 0.15) : 0;
5573
+ const toolGuideBudget = toolGuides.length > 0 ? Math.floor(availableBudget * 0.15) : 0;
5574
+ const memoryBudget = availableBudget - profileBudget - toolGuideBudget;
5575
+ const sectionWeights = {
5576
+ Profile: 0,
5577
+ "Core Facts": 0.3,
5578
+ "Tool Guides": 0,
5579
+ Procedures: 0.25,
5580
+ Preferences: 0.25,
5581
+ "Recent History": 0.2
5582
+ };
5583
+ const sectionBuckets = new Map;
5584
+ for (const s of scored) {
5585
+ if (s.memory.key.startsWith("_profile_"))
5586
+ continue;
5587
+ if (profileText && s.memory.category === "preference")
5588
+ continue;
5589
+ const section = categoryToSection(s.memory.category, s.memory.key);
5590
+ if (section === "Profile")
5591
+ continue;
5592
+ if (!sectionBuckets.has(section)) {
5593
+ sectionBuckets.set(section, []);
5594
+ }
5595
+ sectionBuckets.get(section).push(s);
5596
+ }
5597
+ const sectionOutput = new Map;
5598
+ const injectedIds = [];
5599
+ let totalMemoryCount = 0;
5600
+ const memorySections = ["Core Facts", "Procedures", "Preferences", "Recent History"];
5601
+ for (const sectionName of memorySections) {
5602
+ const bucket = sectionBuckets.get(sectionName) || [];
5603
+ if (bucket.length === 0)
5604
+ continue;
5605
+ const weight = sectionWeights[sectionName];
5606
+ const budget = Math.floor(memoryBudget * weight);
5607
+ const lines = [];
5608
+ let chars = 0;
5609
+ for (const { memory: m } of bucket) {
5610
+ const line = formatMemoryLine(m);
5611
+ if (chars + line.length > budget)
5612
+ break;
5613
+ lines.push(line);
5614
+ injectedIds.push(m.id);
5615
+ chars += line.length;
5616
+ totalMemoryCount++;
5617
+ }
5618
+ if (lines.length > 0) {
5619
+ sectionOutput.set(sectionName, lines);
5620
+ }
5621
+ }
5622
+ const outputSections = [];
5623
+ if (profileText) {
5624
+ const trimmedProfile = profileText.length > profileBudget ? profileText.slice(0, profileBudget - 3) + "..." : profileText;
5625
+ outputSections.push(`## Profile
5626
+ ${trimmedProfile}`);
5627
+ }
5628
+ if (sectionOutput.has("Core Facts")) {
5629
+ outputSections.push(`## Core Facts
5630
+ ${sectionOutput.get("Core Facts").join(`
5631
+ `)}`);
5632
+ }
5633
+ if (toolGuides.length > 0) {
5634
+ const toolLines = [];
5635
+ let toolChars = 0;
5636
+ for (const guide of toolGuides) {
5637
+ const header = `- **${guide.tool_name}**: ${guide.stats_line}`;
5638
+ if (toolChars + header.length > toolGuideBudget)
5639
+ break;
5640
+ toolLines.push(header);
5641
+ toolChars += header.length;
5642
+ for (const lesson of guide.lessons) {
5643
+ if (toolChars + lesson.length > toolGuideBudget)
5644
+ break;
5645
+ toolLines.push(lesson);
5646
+ toolChars += lesson.length;
5647
+ }
5648
+ }
5649
+ if (toolLines.length > 0) {
5650
+ outputSections.push(`## Tool Guides
5651
+ ${toolLines.join(`
5652
+ `)}`);
5653
+ }
5654
+ }
5655
+ if (sectionOutput.has("Procedures")) {
5656
+ outputSections.push(`## Procedures
5657
+ ${sectionOutput.get("Procedures").join(`
5658
+ `)}`);
5659
+ }
5660
+ if (sectionOutput.has("Preferences")) {
5661
+ outputSections.push(`## Preferences
5662
+ ${sectionOutput.get("Preferences").join(`
5663
+ `)}`);
5664
+ }
5665
+ if (sectionOutput.has("Recent History")) {
5666
+ outputSections.push(`## Recent History
5667
+ ${sectionOutput.get("Recent History").join(`
5668
+ `)}`);
5669
+ }
5670
+ if (outputSections.length === 0) {
5671
+ return {
5672
+ output: "",
5673
+ token_estimate: 0,
5674
+ memory_count: 0,
5675
+ profile_from_cache: profileFromCache,
5676
+ detected_tools: detectedTools
5677
+ };
5678
+ }
5679
+ outputSections.push(footer);
5680
+ for (const id of injectedIds) {
5681
+ touchMemory(id, db);
5682
+ }
5683
+ const output = `<agent-memories>
5684
+ ${outputSections.join(`
5685
+
5686
+ `)}
5687
+ </agent-memories>`;
5688
+ const tokenEstimate = Math.ceil(output.length / 4);
5689
+ return {
5690
+ output,
5691
+ token_estimate: tokenEstimate,
5692
+ memory_count: totalMemoryCount,
5693
+ profile_from_cache: profileFromCache,
5694
+ detected_tools: detectedTools
5695
+ };
5696
+ }
5697
+ var TOOL_KEYWORD_PATTERNS;
5698
+ var init_injector = __esm(() => {
5699
+ init_memories();
5700
+ init_config();
5701
+ init_profile_synthesizer();
5702
+ init_tool_events();
5703
+ TOOL_KEYWORD_PATTERNS = [
5704
+ /\b(mcp|tool|command|cli|server|endpoint|api)\b/i,
5705
+ /\b(memory_\w+|entity_\w+|graph_\w+|relation_\w+)\b/i,
5706
+ /\b(git|npm|bun|curl|docker|kubectl)\b/i,
5707
+ /\bmementos[\s-]?\w*/i
5708
+ ];
5709
+ });
5710
+
5711
+ // src/lib/context.ts
5712
+ var exports_context = {};
5713
+ __export(exports_context, {
5714
+ formatLayeredContext: () => formatLayeredContext,
5715
+ assembleContext: () => assembleContext
5716
+ });
5717
+ function assembleContext(options = {}, db) {
5718
+ const { project_id, agent_id, scope, max_per_section = 10 } = options;
5719
+ const baseFilter = {
5720
+ project_id,
5721
+ agent_id,
5722
+ scope
5723
+ };
5724
+ const sections = [];
5725
+ const coreFacts = listMemories({ ...baseFilter, category: ["fact", "preference"], min_importance: 8, limit: max_per_section }, db);
5726
+ if (coreFacts.length > 0) {
5727
+ sections.push({ title: "Core Facts", memories: coreFacts });
5728
+ }
5729
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
5730
+ const allRecent = listMemories({ ...baseFilter, limit: max_per_section * 3 }, db).filter((m) => m.created_at > oneDayAgo || m.accessed_at && m.accessed_at > oneDayAgo);
5731
+ const recentSlice = allRecent.slice(0, max_per_section);
5732
+ if (recentSlice.length > 0) {
5733
+ sections.push({ title: "Recent History", memories: recentSlice });
5734
+ }
5735
+ if (options.query) {
5736
+ const knowledge = listMemories({ ...baseFilter, category: "knowledge", search: options.query, limit: max_per_section }, db);
5737
+ if (knowledge.length > 0) {
5738
+ sections.push({ title: "Relevant Knowledge", memories: knowledge });
5739
+ }
5740
+ }
5741
+ const decisions = listMemories({ ...baseFilter, category: "fact", limit: max_per_section }, db);
5742
+ const coreFactIds = new Set(coreFacts.map((m) => m.id));
5743
+ const newDecisions = decisions.filter((m) => !coreFactIds.has(m.id));
5744
+ if (newDecisions.length > 0) {
5745
+ sections.push({ title: "Active Decisions", memories: newDecisions.slice(0, max_per_section) });
5746
+ }
5747
+ const seen = new Set;
5748
+ for (const section of sections) {
5749
+ section.memories = section.memories.filter((m) => {
5750
+ if (seen.has(m.id))
5751
+ return false;
5752
+ seen.add(m.id);
5753
+ return true;
5754
+ });
5755
+ }
5756
+ const allMemories = sections.flatMap((s) => s.memories);
5757
+ const tokenEstimate = allMemories.reduce((acc, m) => acc + Math.ceil((m.key.length + m.value.length) / 4), 0);
5758
+ return {
5759
+ sections: sections.filter((s) => s.memories.length > 0),
4612
5760
  total_memories: allMemories.length,
4613
5761
  token_estimate: tokenEstimate
4614
5762
  };
@@ -4787,96 +5935,6 @@ var init_acl = __esm(() => {
4787
5935
  init_database();
4788
5936
  });
4789
5937
 
4790
- // src/lib/config.ts
4791
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync, unlinkSync } from "fs";
4792
- import { homedir } from "os";
4793
- import { basename as basename3, dirname as dirname4, join as join4, resolve as resolve4 } from "path";
4794
- function deepMerge(target, source) {
4795
- const result = { ...target };
4796
- for (const key of Object.keys(source)) {
4797
- const sourceVal = source[key];
4798
- const targetVal = result[key];
4799
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
4800
- result[key] = deepMerge(targetVal, sourceVal);
4801
- } else {
4802
- result[key] = sourceVal;
4803
- }
4804
- }
4805
- return result;
4806
- }
4807
- function isValidScope(value) {
4808
- return VALID_SCOPES.includes(value);
4809
- }
4810
- function isValidCategory(value) {
4811
- return VALID_CATEGORIES.includes(value);
4812
- }
4813
- function loadConfig() {
4814
- const configPath = join4(homedir(), ".mementos", "config.json");
4815
- let fileConfig = {};
4816
- if (existsSync4(configPath)) {
4817
- try {
4818
- const raw = readFileSync2(configPath, "utf-8");
4819
- fileConfig = JSON.parse(raw);
4820
- } catch {}
4821
- }
4822
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
4823
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
4824
- if (envScope && isValidScope(envScope)) {
4825
- merged.default_scope = envScope;
4826
- }
4827
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
4828
- if (envCategory && isValidCategory(envCategory)) {
4829
- merged.default_category = envCategory;
4830
- }
4831
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
4832
- if (envImportance) {
4833
- const parsed = parseInt(envImportance, 10);
4834
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
4835
- merged.default_importance = parsed;
4836
- }
4837
- }
4838
- return merged;
4839
- }
4840
- var DEFAULT_CONFIG, VALID_SCOPES, VALID_CATEGORIES;
4841
- var init_config = __esm(() => {
4842
- DEFAULT_CONFIG = {
4843
- default_scope: "private",
4844
- default_category: "knowledge",
4845
- default_importance: 5,
4846
- max_entries: 1000,
4847
- max_entries_per_scope: {
4848
- global: 500,
4849
- shared: 300,
4850
- private: 200,
4851
- working: 100
4852
- },
4853
- injection: {
4854
- max_tokens: 500,
4855
- min_importance: 5,
4856
- categories: ["preference", "fact"],
4857
- refresh_interval: 5
4858
- },
4859
- extraction: {
4860
- enabled: true,
4861
- min_confidence: 0.5
4862
- },
4863
- sync_agents: ["claude", "codex", "gemini"],
4864
- auto_cleanup: {
4865
- enabled: true,
4866
- expired_check_interval: 3600,
4867
- unused_archive_days: 7,
4868
- stale_deprioritize_days: 14
4869
- }
4870
- };
4871
- VALID_SCOPES = ["global", "shared", "private", "working"];
4872
- VALID_CATEGORIES = [
4873
- "preference",
4874
- "fact",
4875
- "knowledge",
4876
- "history"
4877
- ];
4878
- });
4879
-
4880
5938
  // src/lib/retention.ts
4881
5939
  var exports_retention = {};
4882
5940
  __export(exports_retention, {
@@ -9612,6 +10670,8 @@ async function buildFileDependencyGraph(opts, db) {
9612
10670
  }
9613
10671
 
9614
10672
  // src/mcp/index.ts
10673
+ init_profile_synthesizer();
10674
+ init_tool_events();
9615
10675
  init_built_in_hooks();
9616
10676
  init_webhook_hooks();
9617
10677
 
@@ -10641,6 +11701,207 @@ init_database();
10641
11701
  // src/lib/session-processor.ts
10642
11702
  init_memories();
10643
11703
  init_registry();
11704
+
11705
+ // src/lib/tool-lesson-extractor.ts
11706
+ init_tool_events();
11707
+ init_memories();
11708
+ var SYSTEM_PROMPT2 = `You are a tool usage analyst. Given a session transcript containing tool calls and their results, extract actionable lessons about tool usage.
11709
+
11710
+ For each tool lesson, output a JSON array of objects with these fields:
11711
+ - tool_name: name of the tool
11712
+ - lesson: the insight (1-2 sentences)
11713
+ - when_to_use: activation context \u2014 when should an agent recall this lesson (start with "When" or "If")
11714
+ - success: boolean \u2014 was the tool call that taught this lesson successful?
11715
+ - error_type: if failed, one of: timeout, permission, not_found, syntax, rate_limit, other (or null if success)
11716
+
11717
+ Focus on:
11718
+ 1. Successful patterns: what worked and why
11719
+ 2. Failure lessons: what went wrong and how to avoid it
11720
+ 3. Parameter insights: optimal settings discovered
11721
+ 4. Alternative tools: when one tool is better than another
11722
+ 5. Error recovery: what to do when a specific error occurs
11723
+
11724
+ Only extract genuinely useful, non-obvious lessons. Skip trivial observations.
11725
+ Output ONLY the JSON array, no markdown or explanation.`;
11726
+ async function extractToolLessons(transcript, options) {
11727
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
11728
+ if (!apiKey)
11729
+ return [];
11730
+ try {
11731
+ const truncated = transcript.length > 8000 ? transcript.slice(0, 8000) + `
11732
+ [...truncated]` : transcript;
11733
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
11734
+ method: "POST",
11735
+ headers: {
11736
+ "x-api-key": apiKey,
11737
+ "anthropic-version": "2023-06-01",
11738
+ "content-type": "application/json"
11739
+ },
11740
+ body: JSON.stringify({
11741
+ model: "claude-haiku-4-5-20251001",
11742
+ max_tokens: 1500,
11743
+ system: SYSTEM_PROMPT2,
11744
+ messages: [{ role: "user", content: `Extract tool lessons from this session transcript:
11745
+
11746
+ ${truncated}` }]
11747
+ })
11748
+ });
11749
+ if (!response.ok)
11750
+ return [];
11751
+ const data = await response.json();
11752
+ const text = data.content?.[0]?.text?.trim();
11753
+ if (!text)
11754
+ return [];
11755
+ const lessons = JSON.parse(text);
11756
+ if (!Array.isArray(lessons))
11757
+ return [];
11758
+ for (const lesson of lessons) {
11759
+ if (!lesson.tool_name || !lesson.lesson)
11760
+ continue;
11761
+ try {
11762
+ saveToolEvent({
11763
+ tool_name: lesson.tool_name,
11764
+ success: lesson.success,
11765
+ error_type: lesson.error_type || undefined,
11766
+ lesson: lesson.lesson,
11767
+ when_to_use: lesson.when_to_use,
11768
+ context: "extracted from session transcript",
11769
+ agent_id: options?.agent_id,
11770
+ project_id: options?.project_id,
11771
+ session_id: options?.session_id
11772
+ });
11773
+ } catch {}
11774
+ try {
11775
+ createMemory({
11776
+ key: `tool-lesson-${lesson.tool_name}-${Date.now()}`,
11777
+ value: lesson.lesson,
11778
+ category: "knowledge",
11779
+ scope: "shared",
11780
+ importance: 7,
11781
+ source: "auto",
11782
+ tags: ["tool-memory", lesson.tool_name, "auto-extracted"],
11783
+ when_to_use: lesson.when_to_use,
11784
+ agent_id: options?.agent_id,
11785
+ project_id: options?.project_id,
11786
+ session_id: options?.session_id
11787
+ });
11788
+ } catch {}
11789
+ }
11790
+ return lessons;
11791
+ } catch {
11792
+ return [];
11793
+ }
11794
+ }
11795
+
11796
+ // src/lib/procedural-extractor.ts
11797
+ init_memories();
11798
+ init_database();
11799
+ var SYSTEM_PROMPT3 = `You extract procedural knowledge from session transcripts \u2014 workflows, step sequences, and problem-solution patterns.
11800
+
11801
+ For each procedure found, output a JSON array of objects:
11802
+ {
11803
+ "title": "short name for the workflow",
11804
+ "steps": [
11805
+ {"action": "what to do", "when_to_use": "activation context for this step"},
11806
+ {"action": "next step", "when_to_use": "activation context"}
11807
+ ],
11808
+ "failure_patterns": ["what to avoid and why"],
11809
+ "when_to_use": "overall activation context for the whole procedure"
11810
+ }
11811
+
11812
+ Focus on:
11813
+ 1. Multi-step workflows that were completed successfully
11814
+ 2. Step sequences where order matters
11815
+ 3. Failure \u2192 recovery patterns (what went wrong, how it was fixed)
11816
+ 4. Problem-solution pairs (when X happens, do Y)
11817
+
11818
+ Only extract non-trivial procedures (3+ steps or genuinely useful patterns).
11819
+ Output ONLY the JSON array.`;
11820
+ async function extractProcedures(transcript, options) {
11821
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
11822
+ if (!apiKey)
11823
+ return [];
11824
+ try {
11825
+ const truncated = transcript.length > 8000 ? transcript.slice(0, 8000) + `
11826
+ [...truncated]` : transcript;
11827
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
11828
+ method: "POST",
11829
+ headers: {
11830
+ "x-api-key": apiKey,
11831
+ "anthropic-version": "2023-06-01",
11832
+ "content-type": "application/json"
11833
+ },
11834
+ body: JSON.stringify({
11835
+ model: "claude-haiku-4-5-20251001",
11836
+ max_tokens: 2000,
11837
+ system: SYSTEM_PROMPT3,
11838
+ messages: [{ role: "user", content: `Extract procedures from this session:
11839
+
11840
+ ${truncated}` }]
11841
+ })
11842
+ });
11843
+ if (!response.ok)
11844
+ return [];
11845
+ const data = await response.json();
11846
+ const text = data.content?.[0]?.text?.trim();
11847
+ if (!text)
11848
+ return [];
11849
+ const procedures = JSON.parse(text);
11850
+ if (!Array.isArray(procedures))
11851
+ return [];
11852
+ for (const proc of procedures) {
11853
+ if (!proc.title || !proc.steps?.length)
11854
+ continue;
11855
+ const sequenceGroup = `proc-${shortUuid()}`;
11856
+ for (let i = 0;i < proc.steps.length; i++) {
11857
+ const step = proc.steps[i];
11858
+ if (!step)
11859
+ continue;
11860
+ try {
11861
+ createMemory({
11862
+ key: `${sequenceGroup}-step-${i + 1}`,
11863
+ value: step.action,
11864
+ category: "procedural",
11865
+ scope: "shared",
11866
+ importance: 7,
11867
+ source: "auto",
11868
+ tags: ["procedure", "auto-extracted", proc.title.toLowerCase().replace(/\s+/g, "-")],
11869
+ when_to_use: step.when_to_use || proc.when_to_use,
11870
+ sequence_group: sequenceGroup,
11871
+ sequence_order: i + 1,
11872
+ agent_id: options?.agent_id,
11873
+ project_id: options?.project_id,
11874
+ session_id: options?.session_id
11875
+ });
11876
+ } catch {}
11877
+ }
11878
+ for (const pattern of proc.failure_patterns || []) {
11879
+ try {
11880
+ createMemory({
11881
+ key: `${sequenceGroup}-warning-${shortUuid()}`,
11882
+ value: `WARNING: ${pattern}`,
11883
+ category: "procedural",
11884
+ scope: "shared",
11885
+ importance: 8,
11886
+ source: "auto",
11887
+ tags: ["procedure", "failure-pattern", "auto-extracted"],
11888
+ when_to_use: proc.when_to_use,
11889
+ sequence_group: sequenceGroup,
11890
+ sequence_order: 999,
11891
+ agent_id: options?.agent_id,
11892
+ project_id: options?.project_id,
11893
+ session_id: options?.session_id
11894
+ });
11895
+ } catch {}
11896
+ }
11897
+ }
11898
+ return procedures;
11899
+ } catch {
11900
+ return [];
11901
+ }
11902
+ }
11903
+
11904
+ // src/lib/session-processor.ts
10644
11905
  var SESSION_EXTRACTION_USER_TEMPLATE = (chunk, sessionId) => `Extract memories from this session chunk (session: ${sessionId}):
10645
11906
 
10646
11907
  ${chunk}
@@ -10802,6 +12063,20 @@ async function processSessionJob(jobId, db) {
10802
12063
  }
10803
12064
  }
10804
12065
  result.memoriesExtracted = totalMemories;
12066
+ try {
12067
+ await extractToolLessons(job.transcript, {
12068
+ agent_id: job.agent_id ?? undefined,
12069
+ project_id: job.project_id ?? undefined,
12070
+ session_id: job.session_id
12071
+ });
12072
+ } catch {}
12073
+ try {
12074
+ await extractProcedures(job.transcript, {
12075
+ agent_id: job.agent_id ?? undefined,
12076
+ project_id: job.project_id ?? undefined,
12077
+ session_id: job.session_id
12078
+ });
12079
+ } catch {}
10805
12080
  try {
10806
12081
  if (result.errors.length > 0 && result.chunksProcessed === 0) {
10807
12082
  updateSessionJob(jobId, {
@@ -10862,17 +12137,1430 @@ async function _processNext() {
10862
12137
  }
10863
12138
  }
10864
12139
  }
10865
-
12140
+
12141
+ // src/lib/session-watcher.ts
12142
+ import { watch, existsSync as existsSync4, statSync as statSync2, readFileSync as readFileSync2 } from "fs";
12143
+ import { join as join4 } from "path";
12144
+ import { readdirSync as readdirSync2 } from "fs";
12145
+ function encodeCwd(cwd) {
12146
+ return cwd.replace(/\//g, "-");
12147
+ }
12148
+ function getProjectsDir() {
12149
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
12150
+ return join4(home, ".claude", "projects");
12151
+ }
12152
+ function findActiveSession(projectDir) {
12153
+ if (!existsSync4(projectDir))
12154
+ return null;
12155
+ const files = readdirSync2(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
12156
+ name: f,
12157
+ path: join4(projectDir, f),
12158
+ mtime: statSync2(join4(projectDir, f)).mtimeMs
12159
+ })).sort((a, b) => b.mtime - a.mtime);
12160
+ return files[0]?.path || null;
12161
+ }
12162
+ var _watcher = null;
12163
+ var _lastOffset = 0;
12164
+ var _watchedFile = null;
12165
+ var _callback = null;
12166
+ var _pollInterval = null;
12167
+ function processNewLines(filePath, callback) {
12168
+ try {
12169
+ const stat = statSync2(filePath);
12170
+ if (stat.size <= _lastOffset)
12171
+ return;
12172
+ const content = readFileSync2(filePath, "utf-8");
12173
+ const newContent = content.slice(_lastOffset);
12174
+ _lastOffset = stat.size;
12175
+ const lines = newContent.split(`
12176
+ `).filter((l) => l.trim());
12177
+ for (const line of lines) {
12178
+ try {
12179
+ const parsed = JSON.parse(line);
12180
+ const msg = parsed?.message;
12181
+ if (!msg)
12182
+ continue;
12183
+ const sessionMsg = {
12184
+ role: msg.role || "user",
12185
+ content: msg.content || "",
12186
+ timestamp: new Date().toISOString()
12187
+ };
12188
+ if (Array.isArray(msg.content)) {
12189
+ const toolUses = msg.content.filter((c) => c.type === "tool_use").map((c) => ({ name: c.name, input: c.input }));
12190
+ if (toolUses.length > 0) {
12191
+ sessionMsg.tool_use = toolUses;
12192
+ }
12193
+ }
12194
+ callback(sessionMsg);
12195
+ } catch {}
12196
+ }
12197
+ } catch {}
12198
+ }
12199
+ function startSessionWatcher(cwd, callback) {
12200
+ stopSessionWatcher();
12201
+ const projectDir = join4(getProjectsDir(), encodeCwd(cwd));
12202
+ const sessionFile = findActiveSession(projectDir);
12203
+ if (!sessionFile) {
12204
+ return { sessionFile: null };
12205
+ }
12206
+ _watchedFile = sessionFile;
12207
+ _callback = callback;
12208
+ _lastOffset = statSync2(sessionFile).size;
12209
+ try {
12210
+ _watcher = watch(sessionFile, () => {
12211
+ if (_callback && _watchedFile) {
12212
+ processNewLines(_watchedFile, _callback);
12213
+ }
12214
+ });
12215
+ } catch {}
12216
+ _pollInterval = setInterval(() => {
12217
+ if (_callback && _watchedFile) {
12218
+ processNewLines(_watchedFile, _callback);
12219
+ }
12220
+ }, 2000);
12221
+ return { sessionFile };
12222
+ }
12223
+ function stopSessionWatcher() {
12224
+ if (_watcher) {
12225
+ _watcher.close();
12226
+ _watcher = null;
12227
+ }
12228
+ if (_pollInterval) {
12229
+ clearInterval(_pollInterval);
12230
+ _pollInterval = null;
12231
+ }
12232
+ _watchedFile = null;
12233
+ _callback = null;
12234
+ _lastOffset = 0;
12235
+ }
12236
+ function getWatcherStatus() {
12237
+ return {
12238
+ active: _watcher !== null || _pollInterval !== null,
12239
+ watching_file: _watchedFile,
12240
+ last_offset: _lastOffset
12241
+ };
12242
+ }
12243
+
12244
+ // src/lib/context-extractor.ts
12245
+ var _recentContexts = [];
12246
+ var MAX_RECENT = 5;
12247
+ function wordSimilarity(a, b) {
12248
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
12249
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
12250
+ if (wordsA.size === 0 || wordsB.size === 0)
12251
+ return 0;
12252
+ let intersection = 0;
12253
+ for (const w of wordsA) {
12254
+ if (wordsB.has(w))
12255
+ intersection++;
12256
+ }
12257
+ const union = new Set([...wordsA, ...wordsB]).size;
12258
+ return union > 0 ? intersection / union : 0;
12259
+ }
12260
+ function isDuplicate2(text) {
12261
+ for (const recent of _recentContexts) {
12262
+ if (wordSimilarity(text, recent) > 0.9)
12263
+ return true;
12264
+ }
12265
+ return false;
12266
+ }
12267
+ function addToRecent(text) {
12268
+ _recentContexts.push(text);
12269
+ if (_recentContexts.length > MAX_RECENT) {
12270
+ _recentContexts.shift();
12271
+ }
12272
+ }
12273
+ var TOOL_NAMES = new Set([
12274
+ "bash",
12275
+ "read",
12276
+ "write",
12277
+ "edit",
12278
+ "glob",
12279
+ "grep",
12280
+ "agent",
12281
+ "memory_save",
12282
+ "memory_recall",
12283
+ "memory_search",
12284
+ "memory_inject",
12285
+ "memory_context",
12286
+ "memory_profile",
12287
+ "memory_save_tool_event",
12288
+ "git",
12289
+ "npm",
12290
+ "bun",
12291
+ "docker",
12292
+ "kubectl",
12293
+ "curl"
12294
+ ]);
12295
+ function detectTools(text) {
12296
+ const found = [];
12297
+ const lower = text.toLowerCase();
12298
+ for (const tool of TOOL_NAMES) {
12299
+ if (lower.includes(tool))
12300
+ found.push(tool);
12301
+ }
12302
+ return found;
12303
+ }
12304
+ function extractContext(message) {
12305
+ let contextText = "";
12306
+ let source = "user";
12307
+ let isSignificant = false;
12308
+ if (message.role === "user") {
12309
+ if (typeof message.content === "string") {
12310
+ contextText = message.content;
12311
+ } else if (Array.isArray(message.content)) {
12312
+ contextText = message.content.filter((c) => c.type === "text").map((c) => c.text || "").join(" ");
12313
+ }
12314
+ source = "user";
12315
+ isSignificant = contextText.length > 10;
12316
+ } else if (message.role === "assistant") {
12317
+ if (message.tool_use && message.tool_use.length > 0) {
12318
+ const parts = message.tool_use.map((tu) => {
12319
+ const inputSummary = tu.input ? Object.entries(tu.input).filter(([k]) => ["command", "pattern", "query", "file_path", "description", "prompt", "key", "value"].includes(k)).map(([k, v]) => `${k}=${String(v).slice(0, 100)}`).join(", ") : "";
12320
+ return `${tu.name}(${inputSummary})`;
12321
+ });
12322
+ contextText = parts.join("; ");
12323
+ source = "tool_use";
12324
+ isSignificant = false;
12325
+ } else if (typeof message.content === "string") {
12326
+ contextText = message.content.slice(0, 500);
12327
+ source = "assistant";
12328
+ isSignificant = false;
12329
+ } else if (Array.isArray(message.content)) {
12330
+ contextText = message.content.filter((c) => c.type === "text").map((c) => (c.text || "").slice(0, 200)).join(" ");
12331
+ source = "assistant";
12332
+ isSignificant = false;
12333
+ }
12334
+ }
12335
+ const errorPatterns = ["error", "failed", "exception", "enoent", "permission denied", "not found", "timeout"];
12336
+ if (errorPatterns.some((p) => contextText.toLowerCase().includes(p))) {
12337
+ isSignificant = true;
12338
+ }
12339
+ if (isSignificant && isDuplicate2(contextText)) {
12340
+ isSignificant = false;
12341
+ }
12342
+ if (isSignificant) {
12343
+ addToRecent(contextText);
12344
+ }
12345
+ return {
12346
+ context_text: contextText,
12347
+ tools_mentioned: detectTools(contextText),
12348
+ is_significant: isSignificant,
12349
+ source
12350
+ };
12351
+ }
12352
+ function resetContext() {
12353
+ _recentContexts.length = 0;
12354
+ }
12355
+
12356
+ // src/lib/activation-matcher.ts
12357
+ init_memories();
12358
+ var _recentlyPushed = new Map;
12359
+ var DEDUP_WINDOW_MS = 5 * 60 * 1000;
12360
+ function cleanRecentlyPushed() {
12361
+ const cutoff = Date.now() - DEDUP_WINDOW_MS;
12362
+ for (const [id, ts] of _recentlyPushed) {
12363
+ if (ts < cutoff)
12364
+ _recentlyPushed.delete(id);
12365
+ }
12366
+ }
12367
+ function markAsPushed(memoryIds) {
12368
+ const now2 = Date.now();
12369
+ for (const id of memoryIds) {
12370
+ _recentlyPushed.set(id, now2);
12371
+ }
12372
+ }
12373
+ async function findActivatedMemories(contextText, options) {
12374
+ if (!contextText || contextText.length < 10)
12375
+ return [];
12376
+ const minSimilarity = options?.min_similarity || 0.4;
12377
+ const maxResults = options?.max_results || 5;
12378
+ try {
12379
+ const results = await semanticSearch(contextText, {
12380
+ threshold: minSimilarity,
12381
+ limit: maxResults * 2,
12382
+ project_id: options?.project_id,
12383
+ agent_id: options?.agent_id
12384
+ });
12385
+ if (!results || results.length === 0) {
12386
+ return fallbackKeywordMatch(contextText, options);
12387
+ }
12388
+ cleanRecentlyPushed();
12389
+ let filtered = results.map((r) => r.memory).filter((m) => !_recentlyPushed.has(m.id)).filter((m) => m.status === "active");
12390
+ filtered = filtered.map((m) => ({
12391
+ memory: m,
12392
+ score: computeDecayScore({
12393
+ importance: m.importance,
12394
+ access_count: m.access_count,
12395
+ accessed_at: m.accessed_at,
12396
+ created_at: m.created_at,
12397
+ pinned: m.pinned
12398
+ })
12399
+ })).sort((a, b) => b.score - a.score).slice(0, maxResults).map((r) => r.memory);
12400
+ return filtered;
12401
+ } catch {
12402
+ return fallbackKeywordMatch(contextText, options);
12403
+ }
12404
+ }
12405
+ function fallbackKeywordMatch(contextText, options) {
12406
+ try {
12407
+ const allMemories = listMemories({
12408
+ project_id: options?.project_id,
12409
+ status: "active",
12410
+ limit: 100
12411
+ });
12412
+ const contextWords = new Set(contextText.toLowerCase().split(/\s+/).filter((w) => w.length > 3));
12413
+ const scored = allMemories.filter((m) => m.when_to_use).filter((m) => !_recentlyPushed.has(m.id)).map((m) => {
12414
+ const wtuWords = m.when_to_use.toLowerCase().split(/\s+/);
12415
+ const overlap = wtuWords.filter((w) => contextWords.has(w)).length;
12416
+ return { memory: m, score: overlap };
12417
+ }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, options?.max_results || 5);
12418
+ return scored.map((r) => r.memory);
12419
+ } catch {
12420
+ return [];
12421
+ }
12422
+ }
12423
+ function resetRecentlyPushed() {
12424
+ _recentlyPushed.clear();
12425
+ }
12426
+ function getRecentlyPushedCount() {
12427
+ cleanRecentlyPushed();
12428
+ return _recentlyPushed.size;
12429
+ }
12430
+
12431
+ // src/lib/session-registry.ts
12432
+ import { Database as Database2 } from "bun:sqlite";
12433
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
12434
+ import { dirname as dirname4, join as join5 } from "path";
12435
+ var DB_PATH = join5(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".open-sessions-registry.db");
12436
+ var _db2 = null;
12437
+ function getDb() {
12438
+ if (_db2)
12439
+ return _db2;
12440
+ const dir = dirname4(DB_PATH);
12441
+ if (!existsSync5(dir))
12442
+ mkdirSync2(dir, { recursive: true });
12443
+ _db2 = new Database2(DB_PATH, { create: true });
12444
+ _db2.run("PRAGMA journal_mode = WAL");
12445
+ _db2.run("PRAGMA busy_timeout = 3000");
12446
+ _db2.exec(`
12447
+ CREATE TABLE IF NOT EXISTS sessions (
12448
+ id TEXT PRIMARY KEY,
12449
+ pid INTEGER NOT NULL,
12450
+ cwd TEXT NOT NULL,
12451
+ git_root TEXT,
12452
+ agent_name TEXT,
12453
+ project_name TEXT,
12454
+ tty TEXT,
12455
+ mcp_server TEXT NOT NULL,
12456
+ metadata TEXT DEFAULT '{}',
12457
+ registered_at TEXT NOT NULL DEFAULT (datetime('now')),
12458
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
12459
+ );
12460
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_pid_mcp
12461
+ ON sessions(pid, mcp_server);
12462
+ CREATE INDEX IF NOT EXISTS idx_sessions_project
12463
+ ON sessions(project_name);
12464
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent
12465
+ ON sessions(agent_name);
12466
+ CREATE INDEX IF NOT EXISTS idx_sessions_git_root
12467
+ ON sessions(git_root);
12468
+ `);
12469
+ return _db2;
12470
+ }
12471
+ function generateId() {
12472
+ return crypto.randomUUID().slice(0, 8);
12473
+ }
12474
+ function now2() {
12475
+ return new Date().toISOString();
12476
+ }
12477
+ function parseRow2(row) {
12478
+ return {
12479
+ id: row["id"],
12480
+ pid: row["pid"],
12481
+ cwd: row["cwd"],
12482
+ git_root: row["git_root"] || null,
12483
+ agent_name: row["agent_name"] || null,
12484
+ project_name: row["project_name"] || null,
12485
+ tty: row["tty"] || null,
12486
+ mcp_server: row["mcp_server"],
12487
+ metadata: JSON.parse(row["metadata"] || "{}"),
12488
+ registered_at: row["registered_at"],
12489
+ last_seen_at: row["last_seen_at"]
12490
+ };
12491
+ }
12492
+ function isProcessAlive(pid) {
12493
+ try {
12494
+ process.kill(pid, 0);
12495
+ return true;
12496
+ } catch {
12497
+ return false;
12498
+ }
12499
+ }
12500
+ function registerSession(opts) {
12501
+ const db = getDb();
12502
+ const pid = process.pid;
12503
+ const cwd = opts.cwd || process.cwd();
12504
+ const id = generateId();
12505
+ const timestamp = now2();
12506
+ const existing = db.query("SELECT id FROM sessions WHERE pid = ? AND mcp_server = ?").get(pid, opts.mcp_server);
12507
+ if (existing) {
12508
+ db.run(`UPDATE sessions SET agent_name = ?, project_name = ?, cwd = ?,
12509
+ git_root = ?, tty = ?, metadata = ?, last_seen_at = ? WHERE id = ?`, [
12510
+ opts.agent_name || null,
12511
+ opts.project_name || null,
12512
+ cwd,
12513
+ opts.git_root || null,
12514
+ opts.tty || null,
12515
+ JSON.stringify(opts.metadata || {}),
12516
+ timestamp,
12517
+ existing.id
12518
+ ]);
12519
+ return getSession(existing.id);
12520
+ }
12521
+ db.run(`INSERT INTO sessions (id, pid, cwd, git_root, agent_name, project_name, tty, mcp_server, metadata, registered_at, last_seen_at)
12522
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12523
+ id,
12524
+ pid,
12525
+ cwd,
12526
+ opts.git_root || null,
12527
+ opts.agent_name || null,
12528
+ opts.project_name || null,
12529
+ opts.tty || null,
12530
+ opts.mcp_server,
12531
+ JSON.stringify(opts.metadata || {}),
12532
+ timestamp,
12533
+ timestamp
12534
+ ]);
12535
+ return getSession(id);
12536
+ }
12537
+ function heartbeatSession(id) {
12538
+ const db = getDb();
12539
+ db.run("UPDATE sessions SET last_seen_at = ? WHERE id = ?", [now2(), id]);
12540
+ }
12541
+ function unregisterSession(id) {
12542
+ const db = getDb();
12543
+ db.run("DELETE FROM sessions WHERE id = ?", [id]);
12544
+ }
12545
+ function getSession(id) {
12546
+ const db = getDb();
12547
+ const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id);
12548
+ return row ? parseRow2(row) : null;
12549
+ }
12550
+ function cleanStaleSessions() {
12551
+ const db = getDb();
12552
+ const rows = db.query("SELECT id, pid FROM sessions").all();
12553
+ let cleaned = 0;
12554
+ for (const row of rows) {
12555
+ if (!isProcessAlive(row.pid)) {
12556
+ db.run("DELETE FROM sessions WHERE id = ?", [row.id]);
12557
+ cleaned++;
12558
+ }
12559
+ }
12560
+ return cleaned;
12561
+ }
12562
+
12563
+ // src/lib/channel-pusher.ts
12564
+ var _serverRef = null;
12565
+ function setServerRef(server) {
12566
+ _serverRef = server;
12567
+ }
12568
+ async function pushMemoryNotification(memories, context, type = "auto-inject") {
12569
+ if (!_serverRef)
12570
+ return false;
12571
+ if (memories.length === 0)
12572
+ return false;
12573
+ const formatted = formatMemories(memories, context, type);
12574
+ try {
12575
+ const server = _serverRef.server || _serverRef;
12576
+ await server.notification({
12577
+ method: "notifications/claude/channel",
12578
+ params: {
12579
+ content: formatted,
12580
+ meta: {
12581
+ source: "mementos",
12582
+ type,
12583
+ memory_count: memories.length,
12584
+ context: context.slice(0, 200)
12585
+ }
12586
+ }
12587
+ });
12588
+ return true;
12589
+ } catch (e) {
12590
+ return false;
12591
+ }
12592
+ }
12593
+ async function pushRawNotification(text, type = "info") {
12594
+ if (!_serverRef)
12595
+ return false;
12596
+ try {
12597
+ const server = _serverRef.server || _serverRef;
12598
+ await server.notification({
12599
+ method: "notifications/claude/channel",
12600
+ params: {
12601
+ content: text,
12602
+ meta: { source: "mementos", type }
12603
+ }
12604
+ });
12605
+ return true;
12606
+ } catch {
12607
+ return false;
12608
+ }
12609
+ }
12610
+ function formatMemories(memories, context, type) {
12611
+ const header = type === "session-briefing" ? "\uD83D\uDCCB Mementos Session Briefing" : `\u26A1 Mementos activated (${context.slice(0, 100)})`;
12612
+ const lines = memories.map((m) => {
12613
+ const importance = m.importance >= 8 ? "\u2757" : "";
12614
+ const flag = m.flag ? ` \u26A0\uFE0F ${m.flag}` : "";
12615
+ return `\u2022 [${m.category}] ${m.key}: ${m.value.slice(0, 200)}${m.value.length > 200 ? "..." : ""} (importance: ${m.importance})${importance}${flag}`;
12616
+ });
12617
+ const footer = memories.length > 3 ? `
12618
+ Use memory_recall(key="...") for full details on any of these.` : "";
12619
+ return `${header}
12620
+
12621
+ ${lines.join(`
12622
+ `)}${footer}`;
12623
+ }
12624
+ function formatBriefing(sections) {
12625
+ const parts = [];
12626
+ parts.push(`\uD83D\uDCCB Mementos Session Briefing${sections.projectName ? ` \u2014 project: ${sections.projectName}` : ""}`);
12627
+ if (sections.profile) {
12628
+ parts.push(`
12629
+ ## Profile
12630
+ ${sections.profile}`);
12631
+ }
12632
+ if (sections.memories && sections.memories.length > 0) {
12633
+ parts.push(`
12634
+ ## Key Memories (${sections.memories.length})`);
12635
+ for (const m of sections.memories) {
12636
+ parts.push(`\u2022 [${m.category}] ${m.key}: ${m.value.slice(0, 150)}${m.value.length > 150 ? "..." : ""}`);
12637
+ }
12638
+ }
12639
+ if (sections.lastSession) {
12640
+ parts.push(`
12641
+ ## Last Session
12642
+ ${sections.lastSession}`);
12643
+ }
12644
+ if (sections.flagged && sections.flagged.length > 0) {
12645
+ parts.push(`
12646
+ ## Needs Attention`);
12647
+ for (const m of sections.flagged) {
12648
+ parts.push(`\u26A0\uFE0F [${m.flag}] ${m.key}: ${m.value.slice(0, 150)}`);
12649
+ }
12650
+ }
12651
+ parts.push(`
12652
+ Use memory_recall, memory_search, or memory_inject for more details.`);
12653
+ return parts.join(`
12654
+ `);
12655
+ }
12656
+
12657
+ // src/lib/session-start-briefing.ts
12658
+ init_memories();
12659
+ async function pushSessionBriefing(options) {
12660
+ if (!options.project_id)
12661
+ return false;
12662
+ try {
12663
+ let profile = null;
12664
+ try {
12665
+ const { synthesizeProfile: synthesizeProfile2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
12666
+ const result = await synthesizeProfile2({
12667
+ project_id: options.project_id,
12668
+ scope: "project"
12669
+ });
12670
+ if (result)
12671
+ profile = result.profile;
12672
+ } catch {}
12673
+ const pinnedMemories = listMemories({
12674
+ project_id: options.project_id,
12675
+ pinned: true,
12676
+ status: "active",
12677
+ limit: 5
12678
+ });
12679
+ const topMemories = listMemories({
12680
+ project_id: options.project_id,
12681
+ status: "active",
12682
+ min_importance: 7,
12683
+ limit: 10
12684
+ });
12685
+ const seenIds = new Set;
12686
+ const memories = [];
12687
+ for (const m of [...pinnedMemories, ...topMemories]) {
12688
+ if (!seenIds.has(m.id) && !m.key.startsWith("_profile_")) {
12689
+ seenIds.add(m.id);
12690
+ memories.push(m);
12691
+ if (memories.length >= 10)
12692
+ break;
12693
+ }
12694
+ }
12695
+ let lastSession = null;
12696
+ try {
12697
+ const sessionMemories = listMemories({
12698
+ project_id: options.project_id,
12699
+ category: "history",
12700
+ status: "active",
12701
+ limit: 5
12702
+ });
12703
+ const summary = sessionMemories.find((m) => m.key.includes("summary"));
12704
+ if (summary)
12705
+ lastSession = summary.value;
12706
+ } catch {}
12707
+ const allMemories = listMemories({
12708
+ project_id: options.project_id,
12709
+ status: "active",
12710
+ limit: 100
12711
+ });
12712
+ const flagged = allMemories.filter((m) => m.flag && m.flag !== "");
12713
+ if (!profile && memories.length === 0 && !lastSession && flagged.length === 0) {
12714
+ return false;
12715
+ }
12716
+ const briefingText = formatBriefing({
12717
+ profile: profile || undefined,
12718
+ memories: memories.length > 0 ? memories : undefined,
12719
+ lastSession: lastSession || undefined,
12720
+ flagged: flagged.length > 0 ? flagged : undefined,
12721
+ projectName: options.project_name
12722
+ });
12723
+ return await pushRawNotification(briefingText, "session-briefing");
12724
+ } catch {
12725
+ return false;
12726
+ }
12727
+ }
12728
+
12729
+ // src/lib/auto-inject-orchestrator.ts
12730
+ var DEFAULT_CONFIG = {
12731
+ enabled: true,
12732
+ throttle_ms: 30000,
12733
+ debounce_ms: 2000,
12734
+ max_pushes_per_5min: 5,
12735
+ min_similarity: 0.4,
12736
+ session_briefing: true
12737
+ };
12738
+ var _config = { ...DEFAULT_CONFIG };
12739
+ var _running = false;
12740
+ var _sessionId = null;
12741
+ var _heartbeatInterval = null;
12742
+ var _debounceTimer = null;
12743
+ var _lastPushTime = 0;
12744
+ var _pushCount5min = [];
12745
+ var _pendingMessages = [];
12746
+ var _pushHistory = [];
12747
+ var MAX_HISTORY = 20;
12748
+ async function processMessages() {
12749
+ if (!_running || !_config.enabled)
12750
+ return;
12751
+ const now3 = Date.now();
12752
+ if (now3 - _lastPushTime < _config.throttle_ms)
12753
+ return;
12754
+ _pushCount5min = _pushCount5min.filter((p) => now3 - p.ts < 5 * 60 * 1000);
12755
+ if (_pushCount5min.length >= _config.max_pushes_per_5min)
12756
+ return;
12757
+ let bestContext = null;
12758
+ for (const msg of _pendingMessages) {
12759
+ const extracted = extractContext(msg);
12760
+ if (extracted.is_significant && extracted.context_text.length > 10) {
12761
+ bestContext = { context_text: extracted.context_text, tools_mentioned: extracted.tools_mentioned };
12762
+ }
12763
+ }
12764
+ _pendingMessages = [];
12765
+ if (!bestContext)
12766
+ return;
12767
+ try {
12768
+ const memories = await findActivatedMemories(bestContext.context_text, {
12769
+ min_similarity: _config.min_similarity,
12770
+ max_results: 5
12771
+ });
12772
+ if (memories.length === 0)
12773
+ return;
12774
+ const pushed = await pushMemoryNotification(memories, bestContext.context_text);
12775
+ if (pushed) {
12776
+ markAsPushed(memories.map((m) => m.id));
12777
+ _lastPushTime = Date.now();
12778
+ _pushCount5min.push({ ts: Date.now() });
12779
+ _pushHistory.unshift({
12780
+ timestamp: new Date().toISOString(),
12781
+ context: bestContext.context_text.slice(0, 200),
12782
+ memory_count: memories.length
12783
+ });
12784
+ if (_pushHistory.length > MAX_HISTORY)
12785
+ _pushHistory.pop();
12786
+ }
12787
+ } catch {}
12788
+ }
12789
+ function onNewMessage(message) {
12790
+ if (!_running || !_config.enabled)
12791
+ return;
12792
+ _pendingMessages.push(message);
12793
+ if (_debounceTimer)
12794
+ clearTimeout(_debounceTimer);
12795
+ _debounceTimer = setTimeout(() => {
12796
+ processMessages();
12797
+ }, _config.debounce_ms);
12798
+ }
12799
+ async function startAutoInject(options) {
12800
+ const envEnabled = process.env["MEMENTOS_AUTO_INJECT"] === "true";
12801
+ if (!envEnabled && !options.config?.enabled) {
12802
+ return { started: false, session_id: null, watcher_file: null, briefing_pushed: false };
12803
+ }
12804
+ _config = { ...DEFAULT_CONFIG, ...options.config };
12805
+ if (envEnabled)
12806
+ _config.enabled = true;
12807
+ setServerRef(options.server);
12808
+ const cwd = options.cwd || process.cwd();
12809
+ const session = registerSession({
12810
+ mcp_server: "mementos",
12811
+ agent_name: options.agent_name,
12812
+ project_name: options.project_name,
12813
+ cwd,
12814
+ git_root: options.git_root
12815
+ });
12816
+ _sessionId = session.id;
12817
+ _heartbeatInterval = setInterval(() => {
12818
+ if (_sessionId)
12819
+ heartbeatSession(_sessionId);
12820
+ cleanStaleSessions();
12821
+ }, 15000);
12822
+ let briefingPushed = false;
12823
+ if (_config.session_briefing) {
12824
+ try {
12825
+ briefingPushed = await pushSessionBriefing({
12826
+ project_id: options.project_id,
12827
+ project_name: options.project_name,
12828
+ agent_id: options.agent_id
12829
+ });
12830
+ } catch {}
12831
+ }
12832
+ const { sessionFile } = startSessionWatcher(cwd, onNewMessage);
12833
+ _running = true;
12834
+ return {
12835
+ started: true,
12836
+ session_id: _sessionId,
12837
+ watcher_file: sessionFile,
12838
+ briefing_pushed: briefingPushed
12839
+ };
12840
+ }
12841
+ function stopAutoInject() {
12842
+ _running = false;
12843
+ if (_debounceTimer) {
12844
+ clearTimeout(_debounceTimer);
12845
+ _debounceTimer = null;
12846
+ }
12847
+ if (_heartbeatInterval) {
12848
+ clearInterval(_heartbeatInterval);
12849
+ _heartbeatInterval = null;
12850
+ }
12851
+ stopSessionWatcher();
12852
+ resetContext();
12853
+ resetRecentlyPushed();
12854
+ if (_sessionId) {
12855
+ try {
12856
+ unregisterSession(_sessionId);
12857
+ } catch {}
12858
+ _sessionId = null;
12859
+ }
12860
+ _pendingMessages = [];
12861
+ }
12862
+ function getAutoInjectConfig() {
12863
+ return { ..._config };
12864
+ }
12865
+ function updateAutoInjectConfig(updates) {
12866
+ _config = { ..._config, ...updates };
12867
+ return { ..._config };
12868
+ }
12869
+ function getAutoInjectStatus() {
12870
+ const now3 = Date.now();
12871
+ const recent5min = _pushCount5min.filter((p) => now3 - p.ts < 5 * 60 * 1000);
12872
+ const nextAvailable = Math.max(0, _config.throttle_ms - (now3 - _lastPushTime));
12873
+ return {
12874
+ running: _running,
12875
+ config: { ..._config },
12876
+ session_id: _sessionId,
12877
+ watcher: getWatcherStatus(),
12878
+ pushes: {
12879
+ total: _pushHistory.length,
12880
+ last_5min: recent5min.length,
12881
+ recently_pushed_memories: getRecentlyPushedCount(),
12882
+ next_available_in_ms: nextAvailable
12883
+ },
12884
+ history: [..._pushHistory]
12885
+ };
12886
+ }
12887
+
12888
+ // src/lib/asmr/fact-agent.ts
12889
+ init_memories();
12890
+ function escapeFts5Token(token) {
12891
+ return `"${token.replace(/"/g, '""')}"`;
12892
+ }
12893
+ function hasFts5Table2(db) {
12894
+ try {
12895
+ const row = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
12896
+ return !!row;
12897
+ } catch {
12898
+ return false;
12899
+ }
12900
+ }
12901
+ function buildScopeFilter(opts) {
12902
+ const conditions = [
12903
+ "m.status = 'active'",
12904
+ "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))",
12905
+ "m.category IN ('preference', 'fact', 'knowledge')"
12906
+ ];
12907
+ const params = [];
12908
+ if (opts.project_id) {
12909
+ conditions.push("m.project_id = ?");
12910
+ params.push(opts.project_id);
12911
+ }
12912
+ if (opts.agent_id) {
12913
+ conditions.push("m.agent_id = ?");
12914
+ params.push(opts.agent_id);
12915
+ }
12916
+ return { conditions, params };
12917
+ }
12918
+ function scoreFactMemory(memory, queryLower) {
12919
+ const keyLower = memory.key.toLowerCase();
12920
+ const valueLower = memory.value.toLowerCase();
12921
+ let score = 0;
12922
+ let matchField = "value";
12923
+ if (keyLower === queryLower) {
12924
+ score = 10;
12925
+ matchField = "key (exact)";
12926
+ } else if (keyLower.includes(queryLower)) {
12927
+ score = 7;
12928
+ matchField = "key (partial)";
12929
+ } else if (valueLower.includes(queryLower)) {
12930
+ score = 3;
12931
+ matchField = "value";
12932
+ }
12933
+ if (score === 0) {
12934
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
12935
+ if (tokens.length > 1) {
12936
+ let tokenHits = 0;
12937
+ for (const token of tokens) {
12938
+ if (keyLower.includes(token))
12939
+ tokenHits += 2;
12940
+ else if (valueLower.includes(token))
12941
+ tokenHits += 1;
12942
+ }
12943
+ if (tokenHits > 0) {
12944
+ score = tokenHits / tokens.length * 5;
12945
+ matchField = "tokens";
12946
+ }
12947
+ }
12948
+ }
12949
+ if (memory.pinned)
12950
+ score *= 1.3;
12951
+ const effectiveImportance = computeDecayScore(memory);
12952
+ score = score * effectiveImportance / 10;
12953
+ if (memory.importance >= 7)
12954
+ score *= 1.2;
12955
+ return { score, matchField };
12956
+ }
12957
+ async function runFactAgent(db, query, opts) {
12958
+ const queryLower = query.toLowerCase().trim();
12959
+ if (!queryLower)
12960
+ return { memories: [], reasoning: "Empty query" };
12961
+ const { conditions, params } = buildScopeFilter(opts);
12962
+ const limit = (opts.max_results ?? 20) * 2;
12963
+ let rows;
12964
+ if (hasFts5Table2(db)) {
12965
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
12966
+ const ftsQuery = tokens.map(escapeFts5Token).join(" ");
12967
+ try {
12968
+ const ftsCondition = `m.rowid IN (SELECT f.rowid FROM memories_fts f WHERE memories_fts MATCH ?)`;
12969
+ const sql = `SELECT m.* FROM memories m WHERE ${ftsCondition} AND ${conditions.join(" AND ")} LIMIT ?`;
12970
+ rows = db.query(sql).all(ftsQuery, ...params, limit);
12971
+ } catch {
12972
+ rows = [];
12973
+ }
12974
+ } else {
12975
+ rows = [];
12976
+ }
12977
+ if (rows.length === 0) {
12978
+ const likePattern = `%${queryLower}%`;
12979
+ const likeSql = `SELECT m.* FROM memories m WHERE (m.key LIKE ? OR m.value LIKE ?) AND ${conditions.join(" AND ")} LIMIT ?`;
12980
+ rows = db.query(likeSql).all(likePattern, likePattern, ...params, limit);
12981
+ }
12982
+ const results = [];
12983
+ for (const row of rows) {
12984
+ const memory = parseMemoryRow(row);
12985
+ const { score, matchField } = scoreFactMemory(memory, queryLower);
12986
+ if (score <= 0)
12987
+ continue;
12988
+ results.push({
12989
+ memory,
12990
+ score,
12991
+ source_agent: "facts",
12992
+ reasoning: `Direct fact match on ${matchField}`,
12993
+ verbatim_excerpt: memory.value
12994
+ });
12995
+ }
12996
+ results.sort((a, b) => b.score - a.score);
12997
+ const trimmed = results.slice(0, opts.max_results ?? 20);
12998
+ return {
12999
+ memories: trimmed,
13000
+ reasoning: `Fact agent searched ${rows.length} candidate memories, returned ${trimmed.length} factual matches`
13001
+ };
13002
+ }
13003
+
13004
+ // src/lib/asmr/context-agent.ts
13005
+ init_memories();
13006
+ init_entities();
13007
+ init_entity_memories();
13008
+ function deduplicateCandidates(candidates) {
13009
+ const seen = new Map;
13010
+ for (const c of candidates) {
13011
+ const existing = seen.get(c.memory.id);
13012
+ if (!existing) {
13013
+ seen.set(c.memory.id, c);
13014
+ } else {
13015
+ const existingTotal = existing.semanticScore + existing.entityLinkStrength;
13016
+ const currentTotal = c.semanticScore + c.entityLinkStrength;
13017
+ if (currentTotal > existingTotal) {
13018
+ seen.set(c.memory.id, c);
13019
+ }
13020
+ }
13021
+ }
13022
+ return Array.from(seen.values());
13023
+ }
13024
+ async function runContextAgent(db, query, opts) {
13025
+ const queryLower = query.toLowerCase().trim();
13026
+ if (!queryLower)
13027
+ return { memories: [], reasoning: "Empty query" };
13028
+ const maxResults = opts.max_results ?? 20;
13029
+ const candidates = [];
13030
+ let semanticCount = 0;
13031
+ let entityQueryCount = 0;
13032
+ try {
13033
+ const semanticResults = await semanticSearch(query, {
13034
+ threshold: 0.3,
13035
+ limit: maxResults * 2,
13036
+ project_id: opts.project_id,
13037
+ agent_id: opts.agent_id
13038
+ }, db);
13039
+ for (const sr of semanticResults) {
13040
+ candidates.push({
13041
+ memory: sr.memory,
13042
+ semanticScore: sr.score,
13043
+ entityLinkStrength: 0,
13044
+ entityName: null,
13045
+ entityRelation: null
13046
+ });
13047
+ }
13048
+ semanticCount = semanticResults.length;
13049
+ } catch {}
13050
+ const topSemantic = candidates.slice(0, 10);
13051
+ const entityMemoryIds = new Set;
13052
+ for (const candidate of topSemantic) {
13053
+ try {
13054
+ const entities = getEntitiesForMemory(candidate.memory.id, db);
13055
+ for (const entity of entities) {
13056
+ const linkedMemories = getMemoriesForEntity(entity.id, db);
13057
+ for (const mem of linkedMemories) {
13058
+ if (entityMemoryIds.has(mem.id))
13059
+ continue;
13060
+ entityMemoryIds.add(mem.id);
13061
+ candidates.push({
13062
+ memory: mem,
13063
+ semanticScore: 0,
13064
+ entityLinkStrength: 1,
13065
+ entityName: entity.name,
13066
+ entityRelation: entity.type
13067
+ });
13068
+ }
13069
+ }
13070
+ } catch {}
13071
+ }
13072
+ try {
13073
+ const matchingEntities = listEntities({ search: query, limit: 10, project_id: opts.project_id }, db);
13074
+ const exactEntity = getEntityByName(query, undefined, opts.project_id, db);
13075
+ if (exactEntity && !matchingEntities.find((e) => e.id === exactEntity.id)) {
13076
+ matchingEntities.unshift(exactEntity);
13077
+ }
13078
+ for (const entity of matchingEntities) {
13079
+ entityQueryCount++;
13080
+ const linkedMemories = getMemoriesForEntity(entity.id, db);
13081
+ for (const mem of linkedMemories) {
13082
+ if (entityMemoryIds.has(mem.id))
13083
+ continue;
13084
+ entityMemoryIds.add(mem.id);
13085
+ const nameMatch = entity.name.toLowerCase() === queryLower ? 1 : 0.7;
13086
+ candidates.push({
13087
+ memory: mem,
13088
+ semanticScore: 0,
13089
+ entityLinkStrength: nameMatch,
13090
+ entityName: entity.name,
13091
+ entityRelation: entity.type
13092
+ });
13093
+ }
13094
+ }
13095
+ } catch {}
13096
+ const unique = deduplicateCandidates(candidates);
13097
+ const results = [];
13098
+ for (const c of unique) {
13099
+ if (opts.project_id && c.memory.project_id && c.memory.project_id !== opts.project_id)
13100
+ continue;
13101
+ if (c.memory.status !== "active")
13102
+ continue;
13103
+ const effectiveImportance = computeDecayScore(c.memory) / 10;
13104
+ const score = c.semanticScore * 0.4 + c.entityLinkStrength * 0.3 + effectiveImportance * 0.3;
13105
+ let reasoning;
13106
+ if (c.entityName) {
13107
+ reasoning = `Found via entity ${c.entityName} (relation: ${c.entityRelation})`;
13108
+ } else {
13109
+ reasoning = `Semantic similarity match (score: ${c.semanticScore.toFixed(3)})`;
13110
+ }
13111
+ results.push({
13112
+ memory: c.memory,
13113
+ score,
13114
+ source_agent: "context",
13115
+ reasoning,
13116
+ verbatim_excerpt: c.memory.value
13117
+ });
13118
+ }
13119
+ results.sort((a, b) => b.score - a.score);
13120
+ const trimmed = results.slice(0, maxResults);
13121
+ return {
13122
+ memories: trimmed,
13123
+ reasoning: `Context agent found ${semanticCount} semantic matches, traversed ${entityQueryCount} entities, returned ${trimmed.length} contextual results`
13124
+ };
13125
+ }
13126
+
13127
+ // src/lib/asmr/temporal-agent.ts
13128
+ init_memories();
13129
+ function hasFts5Table3(db) {
13130
+ try {
13131
+ const row = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
13132
+ return !!row;
13133
+ } catch {
13134
+ return false;
13135
+ }
13136
+ }
13137
+ function escapeFts5Token2(token) {
13138
+ return `"${token.replace(/"/g, '""')}"`;
13139
+ }
13140
+ function queryRelevance(memory, queryLower) {
13141
+ const keyLower = memory.key.toLowerCase();
13142
+ const valueLower = memory.value.toLowerCase();
13143
+ if (keyLower === queryLower)
13144
+ return 1;
13145
+ if (keyLower.includes(queryLower))
13146
+ return 0.8;
13147
+ if (valueLower.includes(queryLower))
13148
+ return 0.5;
13149
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
13150
+ if (tokens.length > 1) {
13151
+ let hits = 0;
13152
+ for (const t of tokens) {
13153
+ if (keyLower.includes(t) || valueLower.includes(t))
13154
+ hits++;
13155
+ }
13156
+ return hits / tokens.length * 0.6;
13157
+ }
13158
+ return 0.1;
13159
+ }
13160
+ function formatDate(iso) {
13161
+ if (!iso)
13162
+ return "unknown";
13163
+ return iso.slice(0, 10);
13164
+ }
13165
+ async function runTemporalAgent(db, query, opts) {
13166
+ const queryLower = query.toLowerCase().trim();
13167
+ if (!queryLower)
13168
+ return { memories: [], reasoning: "Empty query" };
13169
+ const maxResults = opts.max_results ?? 20;
13170
+ const limit = maxResults * 3;
13171
+ const conditions = [
13172
+ "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))"
13173
+ ];
13174
+ const params = [];
13175
+ if (opts.project_id) {
13176
+ conditions.push("m.project_id = ?");
13177
+ params.push(opts.project_id);
13178
+ }
13179
+ if (opts.agent_id) {
13180
+ conditions.push("m.agent_id = ?");
13181
+ params.push(opts.agent_id);
13182
+ }
13183
+ let temporalRows = [];
13184
+ const temporalCondition = "(m.valid_from IS NOT NULL OR m.valid_until IS NOT NULL)";
13185
+ if (hasFts5Table3(db)) {
13186
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
13187
+ const ftsQuery = tokens.map(escapeFts5Token2).join(" ");
13188
+ try {
13189
+ const ftsCondition = `m.rowid IN (SELECT f.rowid FROM memories_fts f WHERE memories_fts MATCH ?)`;
13190
+ const sql = `SELECT m.* FROM memories m WHERE ${ftsCondition} AND ${temporalCondition} AND ${conditions.join(" AND ")} ORDER BY m.valid_from DESC NULLS LAST LIMIT ?`;
13191
+ temporalRows = db.query(sql).all(ftsQuery, ...params, limit);
13192
+ } catch {
13193
+ temporalRows = [];
13194
+ }
13195
+ }
13196
+ if (temporalRows.length === 0) {
13197
+ const likePattern = `%${queryLower}%`;
13198
+ const sql = `SELECT m.* FROM memories m WHERE (m.key LIKE ? OR m.value LIKE ?) AND ${temporalCondition} AND ${conditions.join(" AND ")} ORDER BY m.valid_from DESC NULLS LAST LIMIT ?`;
13199
+ temporalRows = db.query(sql).all(likePattern, likePattern, ...params, limit);
13200
+ }
13201
+ let supersededRows = [];
13202
+ try {
13203
+ const likePattern = `%${queryLower}%`;
13204
+ const sql = `SELECT m.* FROM memories m WHERE (m.key LIKE ? OR m.value LIKE ?) AND (m.status = 'archived' OR m.valid_until IS NOT NULL) ${conditions.length > 0 ? "AND " + conditions.join(" AND ") : ""} ORDER BY m.updated_at DESC LIMIT ?`;
13205
+ supersededRows = db.query(sql).all(likePattern, likePattern, ...params, limit);
13206
+ } catch {}
13207
+ const seen = new Map;
13208
+ const allRows = [...temporalRows, ...supersededRows];
13209
+ for (const row of allRows) {
13210
+ const memory = parseMemoryRow(row);
13211
+ if (!seen.has(memory.id)) {
13212
+ seen.set(memory.id, memory);
13213
+ }
13214
+ }
13215
+ const memories = Array.from(seen.values());
13216
+ const timeline = [];
13217
+ const results = [];
13218
+ for (const memory of memories) {
13219
+ const hasTemporal = memory.valid_from !== null || memory.valid_until !== null;
13220
+ const isSuperseded = memory.status === "archived";
13221
+ const relevance = queryRelevance(memory, queryLower);
13222
+ let versions = [];
13223
+ let newerVersionNote = "";
13224
+ try {
13225
+ versions = getMemoryVersions(memory.id, db);
13226
+ if (versions.length > 1) {
13227
+ const latest = versions[versions.length - 1];
13228
+ if (latest.version > memory.version) {
13229
+ newerVersionNote = `, superseded at version ${latest.version}`;
13230
+ }
13231
+ }
13232
+ } catch {}
13233
+ if (hasTemporal || isSuperseded) {
13234
+ const from = formatDate(memory.valid_from);
13235
+ const until = formatDate(memory.valid_until);
13236
+ const status = isSuperseded ? " [superseded]" : "";
13237
+ timeline.push(`${from} - ${until}: ${memory.key}${status}`);
13238
+ }
13239
+ const recencyMs = memory.valid_from ? Date.now() - new Date(memory.valid_from).getTime() : Date.now() - new Date(memory.created_at).getTime();
13240
+ const daysSince = recencyMs / (1000 * 60 * 60 * 24);
13241
+ const recencyScore = Math.max(0, 1 - daysSince / 365);
13242
+ const temporalBonus = hasTemporal ? 1 : 0.3;
13243
+ const effectiveImportance = computeDecayScore(memory) / 10;
13244
+ const score = recencyScore * 0.4 + relevance * 0.3 + temporalBonus * 0.3;
13245
+ const finalScore = score * effectiveImportance;
13246
+ let reasoning;
13247
+ if (isSuperseded) {
13248
+ reasoning = `Superseded memory from ${formatDate(memory.valid_from)}${newerVersionNote}`;
13249
+ } else if (memory.valid_until && new Date(memory.valid_until) < new Date) {
13250
+ reasoning = `Expired temporal fact (valid ${formatDate(memory.valid_from)} to ${formatDate(memory.valid_until)})${newerVersionNote}`;
13251
+ } else if (hasTemporal) {
13252
+ reasoning = `Current as of ${formatDate(memory.valid_from)}${newerVersionNote}`;
13253
+ } else {
13254
+ reasoning = `Historical record from ${formatDate(memory.created_at)}${newerVersionNote}`;
13255
+ }
13256
+ results.push({
13257
+ memory,
13258
+ score: finalScore,
13259
+ source_agent: "temporal",
13260
+ reasoning,
13261
+ verbatim_excerpt: memory.value
13262
+ });
13263
+ }
13264
+ timeline.sort();
13265
+ results.sort((a, b) => b.score - a.score);
13266
+ const trimmed = results.slice(0, maxResults);
13267
+ return {
13268
+ memories: trimmed,
13269
+ reasoning: `Temporal agent found ${temporalRows.length} temporal memories and ${supersededRows.length} superseded records, built ${timeline.length}-entry timeline`
13270
+ };
13271
+ }
13272
+
13273
+ // src/lib/asmr/orchestrator.ts
13274
+ var DEFAULT_MAX_RESULTS = 20;
13275
+ function mergeResults(agentResults) {
13276
+ const byId = new Map;
13277
+ for (const { agent, result } of agentResults) {
13278
+ for (const mem of result.memories) {
13279
+ const existing = byId.get(mem.memory.id);
13280
+ if (!existing) {
13281
+ byId.set(mem.memory.id, {
13282
+ best: mem,
13283
+ sources: new Set([agent]),
13284
+ maxScore: mem.score
13285
+ });
13286
+ } else {
13287
+ existing.sources.add(agent);
13288
+ if (mem.score > existing.maxScore) {
13289
+ existing.best = mem;
13290
+ existing.maxScore = mem.score;
13291
+ }
13292
+ }
13293
+ }
13294
+ }
13295
+ const merged = [];
13296
+ for (const [, entry] of byId) {
13297
+ const multiAgentBoost = entry.sources.size > 1 ? 1.5 : 1;
13298
+ merged.push({
13299
+ ...entry.best,
13300
+ score: entry.maxScore * multiAgentBoost
13301
+ });
13302
+ }
13303
+ merged.sort((a, b) => b.score - a.score);
13304
+ return merged;
13305
+ }
13306
+ function extractFacts(factResult) {
13307
+ const facts = [];
13308
+ const seen = new Set;
13309
+ for (const mem of factResult.memories) {
13310
+ const statement = `${mem.memory.key}: ${mem.memory.value}`;
13311
+ if (!seen.has(mem.memory.id)) {
13312
+ seen.add(mem.memory.id);
13313
+ facts.push(statement);
13314
+ }
13315
+ }
13316
+ return facts;
13317
+ }
13318
+ function extractTimeline(temporalResult) {
13319
+ const entries = [];
13320
+ const seen = new Set;
13321
+ for (const mem of temporalResult.memories) {
13322
+ if (seen.has(mem.memory.id))
13323
+ continue;
13324
+ seen.add(mem.memory.id);
13325
+ const date = mem.memory.valid_from ?? mem.memory.created_at;
13326
+ const status = mem.memory.status === "archived" ? " [superseded]" : "";
13327
+ entries.push({
13328
+ date,
13329
+ label: `${date.slice(0, 10)}: ${mem.memory.key}${status}`
13330
+ });
13331
+ }
13332
+ entries.sort((a, b) => a.date.localeCompare(b.date));
13333
+ return entries.map((e) => e.label);
13334
+ }
13335
+ async function asmrRecall(db, query, opts) {
13336
+ const options = {
13337
+ max_results: DEFAULT_MAX_RESULTS,
13338
+ include_reasoning: true,
13339
+ ...opts
13340
+ };
13341
+ const start = performance.now();
13342
+ const [factResult, contextResult, temporalResult] = await Promise.all([
13343
+ runFactAgent(db, query, options),
13344
+ runContextAgent(db, query, options),
13345
+ runTemporalAgent(db, query, options)
13346
+ ]);
13347
+ const agentResults = [
13348
+ { agent: "facts", result: factResult },
13349
+ { agent: "context", result: contextResult },
13350
+ { agent: "temporal", result: temporalResult }
13351
+ ];
13352
+ const merged = mergeResults(agentResults);
13353
+ const trimmed = merged.slice(0, options.max_results ?? DEFAULT_MAX_RESULTS);
13354
+ const facts = extractFacts(factResult);
13355
+ const timeline = extractTimeline(temporalResult);
13356
+ const agentsUsed = [];
13357
+ if (factResult.memories.length > 0)
13358
+ agentsUsed.push("facts");
13359
+ if (contextResult.memories.length > 0)
13360
+ agentsUsed.push("context");
13361
+ if (temporalResult.memories.length > 0)
13362
+ agentsUsed.push("temporal");
13363
+ const reasoningParts = [
13364
+ factResult.reasoning,
13365
+ contextResult.reasoning,
13366
+ temporalResult.reasoning
13367
+ ].filter(Boolean);
13368
+ const duration = Math.round(performance.now() - start);
13369
+ return {
13370
+ memories: trimmed,
13371
+ facts,
13372
+ timeline,
13373
+ reasoning: reasoningParts.join(". "),
13374
+ agents_used: agentsUsed,
13375
+ duration_ms: duration
13376
+ };
13377
+ }
13378
+ // src/lib/asmr/ensemble.ts
13379
+ var DEFAULT_MODEL = "gpt-4.1-mini";
13380
+ var ESCALATION_MODEL = "gpt-4.1";
13381
+ var CONSENSUS_THRESHOLD = 0.7;
13382
+ var VARIANT_PROMPTS = [
13383
+ "Focus on the most recent and up-to-date facts. If there are contradictions, prefer the newer information.",
13384
+ "Focus on factual accuracy. Only use information explicitly stated in the memories. Do not infer.",
13385
+ "Focus on relationships and connections between entities. Consider how different facts relate to each other.",
13386
+ "Focus on the chronological timeline. Consider when events happened and how the situation evolved over time.",
13387
+ "Synthesize all available information into a comprehensive answer. Consider all perspectives and resolve contradictions.",
13388
+ "Focus on preferences and personal information. Prioritize what the user has explicitly stated they prefer or want.",
13389
+ "Be skeptical. Look for contradictions, outdated information, or facts that might no longer be true.",
13390
+ "Focus on the most frequently mentioned and highly-rated facts. These are likely the most important."
13391
+ ];
13392
+ function buildContext(result) {
13393
+ const sections = [];
13394
+ if (result.facts.length > 0) {
13395
+ sections.push(`Known Facts:
13396
+ ` + result.facts.map((f) => `- ${f}`).join(`
13397
+ `));
13398
+ }
13399
+ if (result.timeline.length > 0) {
13400
+ sections.push(`Timeline:
13401
+ ` + result.timeline.map((t) => `- ${t}`).join(`
13402
+ `));
13403
+ }
13404
+ const memories = result.memories.slice(0, 30);
13405
+ if (memories.length > 0) {
13406
+ sections.push(`Memory Excerpts:
13407
+ ` + memories.map((m) => `[${m.source_agent}] ${m.memory.key}: ${m.verbatim_excerpt.slice(0, 500)}`).join(`
13408
+
13409
+ `));
13410
+ }
13411
+ return sections.join(`
13412
+
13413
+ ---
13414
+
13415
+ `);
13416
+ }
13417
+ async function callLLM(systemPrompt, userPrompt, model) {
13418
+ const apiKey = process.env["OPENAI_API_KEY"] ?? process.env["LLM_API_KEY"];
13419
+ if (!apiKey)
13420
+ throw new Error("No API key for ensemble LLM calls");
13421
+ const baseUrl = process.env["LLM_BASE_URL"] ?? "https://api.openai.com/v1";
13422
+ const res = await fetch(`${baseUrl}/chat/completions`, {
13423
+ method: "POST",
13424
+ headers: {
13425
+ "Content-Type": "application/json",
13426
+ Authorization: `Bearer ${apiKey}`
13427
+ },
13428
+ body: JSON.stringify({
13429
+ model,
13430
+ messages: [
13431
+ { role: "system", content: systemPrompt },
13432
+ { role: "user", content: userPrompt }
13433
+ ],
13434
+ temperature: 0.3,
13435
+ max_tokens: 1024
13436
+ }),
13437
+ signal: AbortSignal.timeout(30000)
13438
+ });
13439
+ if (!res.ok)
13440
+ throw new Error(`LLM API ${res.status}`);
13441
+ const data = await res.json();
13442
+ return data.choices[0]?.message?.content ?? "";
13443
+ }
13444
+ function findMajority(answers) {
13445
+ if (answers.length === 0)
13446
+ return null;
13447
+ const normalized = answers.map((a) => a.toLowerCase().trim().replace(/[.!?]+$/, ""));
13448
+ const counts = new Map;
13449
+ for (let i = 0;i < normalized.length; i++) {
13450
+ const key = normalized[i];
13451
+ const existing = counts.get(key);
13452
+ if (existing) {
13453
+ existing.count++;
13454
+ } else {
13455
+ counts.set(key, { original: answers[i], count: 1 });
13456
+ }
13457
+ }
13458
+ let best = null;
13459
+ for (const entry of counts.values()) {
13460
+ if (!best || entry.count > best.count) {
13461
+ best = entry;
13462
+ }
13463
+ }
13464
+ return best ? { answer: best.original, count: best.count, total: answers.length } : null;
13465
+ }
13466
+ async function ensembleAnswer(context, query, opts) {
13467
+ const numVariants = Math.min(opts?.variants ?? 5, VARIANT_PROMPTS.length);
13468
+ const model = opts?.model ?? DEFAULT_MODEL;
13469
+ const escalationModel = opts?.escalation_model ?? ESCALATION_MODEL;
13470
+ const threshold = opts?.consensus_threshold ?? CONSENSUS_THRESHOLD;
13471
+ const contextStr = buildContext(context);
13472
+ if (!contextStr.trim()) {
13473
+ return {
13474
+ answer: "No relevant information found in memory.",
13475
+ confidence: 0,
13476
+ reasoning: "No context available from ASMR search agents.",
13477
+ variants_used: 0,
13478
+ consensus_reached: false,
13479
+ escalated: false
13480
+ };
13481
+ }
13482
+ const variantPromises = VARIANT_PROMPTS.slice(0, numVariants).map((variantInstruction) => callLLM(`You are a memory retrieval assistant. Answer the user's question based ONLY on the provided context.
13483
+
13484
+ ${variantInstruction}
13485
+
13486
+ Context:
13487
+ ${contextStr}`, query, model).catch(() => null));
13488
+ const results = await Promise.all(variantPromises);
13489
+ const answers = results.filter((r) => r !== null && r.trim().length > 0);
13490
+ if (answers.length === 0) {
13491
+ return {
13492
+ answer: "Unable to generate an answer from the available context.",
13493
+ confidence: 0,
13494
+ reasoning: "All variant prompts failed.",
13495
+ variants_used: numVariants,
13496
+ consensus_reached: false,
13497
+ escalated: false
13498
+ };
13499
+ }
13500
+ const majority = findMajority(answers);
13501
+ if (majority && majority.count / majority.total >= threshold) {
13502
+ return {
13503
+ answer: majority.answer,
13504
+ confidence: majority.count / majority.total,
13505
+ reasoning: `${majority.count}/${majority.total} variants agreed on this answer.`,
13506
+ variants_used: numVariants,
13507
+ consensus_reached: true,
13508
+ escalated: false
13509
+ };
13510
+ }
13511
+ try {
13512
+ const escalationPrompt = `Multiple analysis variants produced different answers to the question. Review all of them and produce the single best answer.
13513
+
13514
+ Question: ${query}
13515
+
13516
+ Context:
13517
+ ${contextStr}
13518
+
13519
+ Variant answers:
13520
+ ${answers.map((a, i) => `Variant ${i + 1}: ${a}`).join(`
13521
+
13522
+ `)}
13523
+
13524
+ Produce the single most accurate answer based on the context. If there are genuine contradictions in the data, acknowledge them.`;
13525
+ const finalAnswer = await callLLM("You are an expert arbitrator. Given multiple candidate answers and the original context, determine the most accurate answer.", escalationPrompt, escalationModel);
13526
+ return {
13527
+ answer: finalAnswer,
13528
+ confidence: 0.6,
13529
+ reasoning: `No consensus (best: ${majority?.count ?? 0}/${answers.length}). Escalated to ${escalationModel} for conflict resolution.`,
13530
+ variants_used: numVariants,
13531
+ consensus_reached: false,
13532
+ escalated: true
13533
+ };
13534
+ } catch {
13535
+ return {
13536
+ answer: majority?.answer ?? answers[0],
13537
+ confidence: (majority?.count ?? 1) / answers.length,
13538
+ reasoning: `No consensus, escalation failed. Returning best available answer (${majority?.count ?? 1}/${answers.length} agreement).`,
13539
+ variants_used: numVariants,
13540
+ consensus_reached: false,
13541
+ escalated: false
13542
+ };
13543
+ }
13544
+ }
10866
13545
  // src/mcp/index.ts
10867
13546
  init_auto_memory();
10868
13547
  init_registry();
10869
13548
  import { createRequire } from "module";
10870
13549
  var _require = createRequire(import.meta.url);
10871
13550
  var _pkg = _require("../../package.json");
13551
+ var mcpServer = null;
10872
13552
  var server = new McpServer({
10873
13553
  name: "mementos",
10874
13554
  version: _pkg.version
13555
+ }, {
13556
+ capabilities: {
13557
+ experimental: { "claude/channel": {} }
13558
+ },
13559
+ instructions: `Mementos is the persistent memory layer for AI agents. It stores, searches, and manages memories across sessions and projects.
13560
+
13561
+ When running with --dangerously-load-development-channels, mementos will proactively push relevant memories into your conversation via channel notifications. These appear as <channel source="mementos"> tags. They contain memories activated by your current task context \u2014 use them to inform your work. You don't need to call memory_inject when auto-inject is active.`
10875
13562
  });
13563
+ mcpServer = server;
10876
13564
  var _autoProjectInitialized = false;
10877
13565
  function ensureAutoProject() {
10878
13566
  if (_autoProjectInitialized)
@@ -10891,8 +13579,20 @@ function formatError(error) {
10891
13579
  return `Duplicate: ${error.message}`;
10892
13580
  if (error instanceof InvalidScopeError)
10893
13581
  return `Invalid scope: ${error.message}`;
10894
- if (error instanceof Error)
10895
- return error.message;
13582
+ if (error instanceof Error) {
13583
+ const msg = error.message;
13584
+ if (msg.includes("UNIQUE constraint failed: projects.")) {
13585
+ return `Project already registered at this path. Use list_projects to find it.`;
13586
+ }
13587
+ if (msg.includes("UNIQUE constraint failed")) {
13588
+ const table = msg.match(/UNIQUE constraint failed: (\w+)\./)?.[1] ?? "unknown";
13589
+ return `Duplicate entry in ${table}. The record already exists \u2014 use the list or get tool to find it.`;
13590
+ }
13591
+ if (msg.includes("FOREIGN KEY constraint failed")) {
13592
+ return `Referenced record not found. Check that the project_id or agent_id exists.`;
13593
+ }
13594
+ return msg;
13595
+ }
10896
13596
  return String(error);
10897
13597
  }
10898
13598
  function resolveId(partialId, table = "memories") {
@@ -10936,6 +13636,32 @@ function formatMemory(m) {
10936
13636
  return parts.join(`
10937
13637
  `);
10938
13638
  }
13639
+ function formatAsmrResult(result, query) {
13640
+ const sections = [];
13641
+ sections.push(`[deep] ASMR recall for "${query}" (${result.duration_ms}ms, agents: ${result.agents_used.join(", ")})`);
13642
+ if (result.memories.length > 0) {
13643
+ const memLines = result.memories.map((m, i) => `${i + 1}. [${m.source_agent}] [score:${m.score.toFixed(3)}] [${m.memory.scope}/${m.memory.category}] ${m.memory.key} = ${m.memory.value.slice(0, 120)}${m.memory.value.length > 120 ? "..." : ""}`);
13644
+ sections.push(`Memories (${result.memories.length}):
13645
+ ${memLines.join(`
13646
+ `)}`);
13647
+ }
13648
+ if (result.facts.length > 0) {
13649
+ sections.push(`Facts:
13650
+ ${result.facts.map((f) => `- ${f}`).join(`
13651
+ `)}`);
13652
+ }
13653
+ if (result.timeline.length > 0) {
13654
+ sections.push(`Timeline:
13655
+ ${result.timeline.map((t) => `- ${t}`).join(`
13656
+ `)}`);
13657
+ }
13658
+ if (result.reasoning) {
13659
+ sections.push(`Reasoning: ${result.reasoning}`);
13660
+ }
13661
+ return sections.join(`
13662
+
13663
+ `);
13664
+ }
10939
13665
  server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shared=project, private=single agent, working=transient session scratchpad (auto-expires in 1h, excluded from ALMA synthesis). conflict controls what happens when key already exists.", {
10940
13666
  key: exports_external.string(),
10941
13667
  value: exports_external.string(),
@@ -10952,11 +13678,15 @@ server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shar
10952
13678
  metadata: exports_external.record(exports_external.unknown()).optional(),
10953
13679
  conflict: exports_external.enum(["merge", "overwrite", "error", "version-fork"]).optional().describe("Conflict strategy: merge=upsert(default), overwrite=same as merge, error=fail if key exists, version-fork=always create new"),
10954
13680
  conflict_strategy: exports_external.enum(["last_writer_wins", "reject"]).optional().describe("Vector clock conflict strategy: last_writer_wins (default) proceeds even if diverged, reject returns error on divergence"),
10955
- machine_id: exports_external.string().optional().describe("Machine ID (from register_machine). If omitted, auto-detected from current hostname.")
13681
+ machine_id: exports_external.string().optional().describe("Machine ID (from register_machine). If omitted, auto-detected from current hostname."),
13682
+ when_to_use: exports_external.string().optional().describe("Activation context \u2014 describes WHEN this memory should be retrieved. Used for intent-based retrieval. Example: 'when deploying to production' or 'when debugging database issues'. If set, semantic search matches against this instead of the value."),
13683
+ sequence_group: exports_external.string().optional().describe("Chain/sequence group ID \u2014 links memories into an ordered procedural sequence. Use memory_chain_get to retrieve the full chain."),
13684
+ sequence_order: exports_external.coerce.number().optional().describe("Position within the sequence group (1-based). Memories in a chain are returned ordered by this field."),
13685
+ dedup_mode: exports_external.enum(["key", "semantic", "llm"]).optional().default("key").describe("Dedup strategy: 'key' = current key-based (default), 'semantic' = skip if >0.92 embedding similarity to existing memory, 'llm' = ask LLM if content is already covered")
10956
13686
  }, async (args) => {
10957
13687
  try {
10958
13688
  ensureAutoProject();
10959
- const { conflict, ...restArgs } = args;
13689
+ const { conflict, dedup_mode, ...restArgs } = args;
10960
13690
  const input = { ...restArgs };
10961
13691
  if (restArgs.ttl_ms !== undefined) {
10962
13692
  input.ttl_ms = parseDuration(restArgs.ttl_ms);
@@ -10987,6 +13717,89 @@ server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shar
10987
13717
  }
10988
13718
  } catch {}
10989
13719
  }
13720
+ if (dedup_mode === "semantic") {
13721
+ try {
13722
+ const textToEmbed = input.when_to_use || input.value;
13723
+ const results = await semanticSearch(textToEmbed, {
13724
+ threshold: 0.3,
13725
+ limit: 5,
13726
+ scope: input.scope,
13727
+ agent_id: input.agent_id,
13728
+ project_id: input.project_id
13729
+ });
13730
+ const SEMANTIC_THRESHOLD = 0.92;
13731
+ for (const result of results) {
13732
+ if (result.score >= SEMANTIC_THRESHOLD) {
13733
+ return { content: [{ type: "text", text: `Skipped: content similar to existing memory ${result.memory.id.slice(0, 8)} (similarity: ${result.score.toFixed(2)})` }] };
13734
+ }
13735
+ }
13736
+ } catch {}
13737
+ }
13738
+ if (dedup_mode === "llm") {
13739
+ try {
13740
+ const textToEmbed = input.when_to_use || input.value;
13741
+ const candidates = await semanticSearch(textToEmbed, {
13742
+ threshold: 0.3,
13743
+ limit: 5,
13744
+ scope: input.scope,
13745
+ agent_id: input.agent_id,
13746
+ project_id: input.project_id
13747
+ });
13748
+ if (candidates.length > 0) {
13749
+ const anthropicKey = process.env["ANTHROPIC_API_KEY"];
13750
+ if (anthropicKey) {
13751
+ const existingList = candidates.map((c, i) => `[${i + 1}] key="${c.memory.key}" value="${c.memory.value}" (similarity: ${c.score.toFixed(2)})`).join(`
13752
+ `);
13753
+ const llmPrompt = `New memory to save:
13754
+ key="${input.key}"
13755
+ value="${input.value}"
13756
+
13757
+ Existing similar memories:
13758
+ ${existingList}
13759
+
13760
+ Is the new memory already covered by these existing memories? Reply with JSON only:
13761
+ {"action": "skip" | "merge" | "add", "reason": "string", "merge_text": "string if action=merge \u2014 the merged content combining new and existing"}`;
13762
+ const llmRes = await fetch("https://api.anthropic.com/v1/messages", {
13763
+ method: "POST",
13764
+ headers: {
13765
+ "x-api-key": anthropicKey,
13766
+ "anthropic-version": "2023-06-01",
13767
+ "content-type": "application/json"
13768
+ },
13769
+ body: JSON.stringify({
13770
+ model: "claude-haiku-4-5-20251001",
13771
+ max_tokens: 300,
13772
+ system: "You are a deduplication assistant. Given a new memory and existing similar memories, decide: skip (already covered), merge (combine with existing), or add (genuinely new). Reply with JSON only.",
13773
+ messages: [{ role: "user", content: llmPrompt }]
13774
+ }),
13775
+ signal: AbortSignal.timeout(15000)
13776
+ });
13777
+ if (llmRes.ok) {
13778
+ const llmData = await llmRes.json();
13779
+ const llmText = llmData.content?.[0]?.text?.trim() ?? "";
13780
+ const jsonMatch = llmText.match(/\{[\s\S]*\}/);
13781
+ if (jsonMatch) {
13782
+ const decision = JSON.parse(jsonMatch[0]);
13783
+ if (decision.action === "skip") {
13784
+ return { content: [{ type: "text", text: `Skipped (LLM dedup): ${decision.reason}` }] };
13785
+ }
13786
+ if (decision.action === "merge" && decision.merge_text && candidates[0]) {
13787
+ const target = candidates[0].memory;
13788
+ try {
13789
+ updateMemory(target.id, { value: decision.merge_text, version: target.version });
13790
+ return { content: [{ type: "text", text: JSON.stringify({
13791
+ merged_into: target.id.slice(0, 8),
13792
+ key: target.key,
13793
+ reason: decision.reason
13794
+ }) }] };
13795
+ } catch {}
13796
+ }
13797
+ }
13798
+ }
13799
+ }
13800
+ }
13801
+ } catch {}
13802
+ }
10990
13803
  const memory = createMemory(input, dedupeMode);
10991
13804
  if (input.agent_id) {
10992
13805
  try {
@@ -11034,7 +13847,21 @@ server.tool("memory_recall", "Recall a memory by key. Returns the best matching
11034
13847
  touchMemory(memory.id);
11035
13848
  if (args.agent_id)
11036
13849
  touchAgent(args.agent_id);
11037
- return { content: [{ type: "text", text: formatMemory(memory) }] };
13850
+ let text = formatMemory(memory);
13851
+ if (memory.sequence_group) {
13852
+ try {
13853
+ const db = getDatabase();
13854
+ const chainRows = db.prepare("SELECT * FROM memories WHERE sequence_group = ? AND status = 'active' ORDER BY sequence_order ASC").all(memory.sequence_group);
13855
+ if (chainRows.length > 1) {
13856
+ const steps = chainRows.map(parseMemoryRow);
13857
+ const chainLine = steps.map((s) => `[${s.sequence_order ?? "?"}] ${s.key}`).join(" \u2192 ");
13858
+ text += `
13859
+
13860
+ Chain context: ${chainLine}`;
13861
+ }
13862
+ } catch {}
13863
+ }
13864
+ return { content: [{ type: "text", text }] };
11038
13865
  }
11039
13866
  const results = searchMemories(args.key, {
11040
13867
  scope: args.scope,
@@ -11172,7 +13999,8 @@ server.tool("memory_update", "Update a memory's metadata (value, importance, tag
11172
13999
  status: exports_external.enum(["active", "archived", "expired"]).optional(),
11173
14000
  metadata: exports_external.record(exports_external.unknown()).optional(),
11174
14001
  expires_at: exports_external.string().nullable().optional(),
11175
- version: exports_external.coerce.number().optional()
14002
+ version: exports_external.coerce.number().optional(),
14003
+ when_to_use: exports_external.string().optional().describe("Update the activation context for this memory")
11176
14004
  }, async (args) => {
11177
14005
  try {
11178
14006
  const id = resolveId(args.id);
@@ -11437,6 +14265,34 @@ server.tool("memory_diff", "Show what changed between two versions of a memory.
11437
14265
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
11438
14266
  }
11439
14267
  });
14268
+ server.tool("memory_chain_get", "Retrieve an ordered memory chain/sequence by group ID. Returns all steps in order.", {
14269
+ sequence_group: exports_external.string().describe("The chain/sequence group ID to retrieve"),
14270
+ project_id: exports_external.string().optional()
14271
+ }, async (args) => {
14272
+ try {
14273
+ ensureAutoProject();
14274
+ const db = getDatabase();
14275
+ let effectiveProjectId = args.project_id;
14276
+ const conditions = ["sequence_group = ?", "status = 'active'"];
14277
+ const params = [args.sequence_group];
14278
+ if (effectiveProjectId) {
14279
+ conditions.push("project_id = ?");
14280
+ params.push(effectiveProjectId);
14281
+ }
14282
+ const rows = db.prepare(`SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY sequence_order ASC`).all(...params);
14283
+ if (rows.length === 0) {
14284
+ return { content: [{ type: "text", text: `No chain found for sequence_group: "${args.sequence_group}"` }] };
14285
+ }
14286
+ const memories = rows.map(parseMemoryRow);
14287
+ const chainSteps = memories.map((m, i) => `[Step ${m.sequence_order ?? i + 1}] ${m.key}: ${m.value}`).join(`
14288
+ `);
14289
+ const header = `Chain "${args.sequence_group}" (${memories.length} steps):
14290
+ `;
14291
+ return { content: [{ type: "text", text: header + chainSteps }] };
14292
+ } catch (e) {
14293
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
14294
+ }
14295
+ });
11440
14296
  server.tool("memory_search", "Search memories by keyword across key, value, summary, and tags", {
11441
14297
  query: exports_external.string(),
11442
14298
  scope: exports_external.enum(["global", "shared", "private", "working"]).optional(),
@@ -11490,10 +14346,10 @@ server.tool("memory_search_semantic", "Semantic (meaning-based) memory search us
11490
14346
  ensureAutoProject();
11491
14347
  if (args.index_missing) {
11492
14348
  const db = getDatabase();
11493
- const unindexed = db.prepare(`SELECT id, value, summary FROM memories
14349
+ const unindexed = db.prepare(`SELECT id, value, summary, when_to_use FROM memories
11494
14350
  WHERE status = 'active' AND id NOT IN (SELECT memory_id FROM memory_embeddings)
11495
14351
  LIMIT 100`).all();
11496
- await Promise.all(unindexed.map((m) => indexMemoryEmbedding(m.id, [m.value, m.summary].filter(Boolean).join(" "))));
14352
+ await Promise.all(unindexed.map((m) => indexMemoryEmbedding(m.id, m.when_to_use || [m.value, m.summary].filter(Boolean).join(" "))));
11497
14353
  }
11498
14354
  let effectiveProjectId = args.project_id;
11499
14355
  if (!args.project_id && args.agent_id) {
@@ -11595,6 +14451,91 @@ ${lines.join(`
11595
14451
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
11596
14452
  }
11597
14453
  });
14454
+ server.tool("memory_recall_deep", "Deep memory recall using ASMR (Agentic Search and Memory Retrieval) \u2014 runs 3 parallel search agents (facts, context, temporal) for high-accuracy retrieval with optional ensemble answering", {
14455
+ query: exports_external.string().describe("Natural language query"),
14456
+ mode: exports_external.enum(["fast", "deep", "auto"]).default("deep").describe("fast=FTS+semantic, deep=ASMR 3-agent, auto=fast then escalate"),
14457
+ max_results: exports_external.coerce.number().default(20),
14458
+ ensemble: exports_external.coerce.boolean().default(false).describe("Use ensemble answering with majority voting"),
14459
+ project_id: exports_external.string().optional()
14460
+ }, async (args) => {
14461
+ try {
14462
+ ensureAutoProject();
14463
+ const db = getDatabase();
14464
+ const FAST_SCORE_THRESHOLD = 0.6;
14465
+ if (args.mode === "fast") {
14466
+ const results = await hybridSearch(args.query, {
14467
+ filter: { project_id: args.project_id, limit: args.max_results },
14468
+ limit: args.max_results
14469
+ });
14470
+ if (results.length === 0) {
14471
+ return { content: [{ type: "text", text: `No memories found for "${args.query}" via fast search.` }] };
14472
+ }
14473
+ const lines = results.map((r, i) => `${i + 1}. [score:${r.score.toFixed(3)}] [${r.memory.scope}/${r.memory.category}] ${r.memory.key} = ${r.memory.value.slice(0, 120)}${r.memory.value.length > 120 ? "..." : ""}`);
14474
+ return { content: [{ type: "text", text: `[fast] ${results.length} result(s) for "${args.query}":
14475
+ ${lines.join(`
14476
+ `)}` }] };
14477
+ }
14478
+ if (args.mode === "deep") {
14479
+ const asmrResult2 = await asmrRecall(db, args.query, {
14480
+ max_results: args.max_results,
14481
+ project_id: args.project_id
14482
+ });
14483
+ let text2 = formatAsmrResult(asmrResult2, args.query);
14484
+ if (args.ensemble) {
14485
+ try {
14486
+ const answer = await ensembleAnswer(asmrResult2, args.query);
14487
+ text2 += `
14488
+
14489
+ --- Ensemble Answer (confidence: ${(answer.confidence * 100).toFixed(0)}%, consensus: ${answer.consensus_reached ? "yes" : "no"}, escalated: ${answer.escalated ? "yes" : "no"}) ---
14490
+ ${answer.answer}
14491
+
14492
+ Reasoning: ${answer.reasoning}`;
14493
+ } catch (ensErr) {
14494
+ text2 += `
14495
+
14496
+ [Ensemble failed: ${ensErr instanceof Error ? ensErr.message : "unknown error"}]`;
14497
+ }
14498
+ }
14499
+ return { content: [{ type: "text", text: text2 }] };
14500
+ }
14501
+ const fastResults = await hybridSearch(args.query, {
14502
+ filter: { project_id: args.project_id, limit: args.max_results },
14503
+ limit: args.max_results
14504
+ });
14505
+ const topScore = fastResults.length > 0 ? fastResults[0].score : 0;
14506
+ if (topScore >= FAST_SCORE_THRESHOLD && fastResults.length >= 3) {
14507
+ const lines = fastResults.map((r, i) => `${i + 1}. [score:${r.score.toFixed(3)}] [${r.memory.scope}/${r.memory.category}] ${r.memory.key} = ${r.memory.value.slice(0, 120)}${r.memory.value.length > 120 ? "..." : ""}`);
14508
+ return { content: [{ type: "text", text: `[auto/fast] ${fastResults.length} result(s) for "${args.query}" (top score ${topScore.toFixed(3)} >= threshold):
14509
+ ${lines.join(`
14510
+ `)}` }] };
14511
+ }
14512
+ const asmrResult = await asmrRecall(db, args.query, {
14513
+ max_results: args.max_results,
14514
+ project_id: args.project_id
14515
+ });
14516
+ let text = `[auto/escalated] Fast search top score ${topScore.toFixed(3)} < ${FAST_SCORE_THRESHOLD} threshold \u2014 escalated to ASMR deep recall.
14517
+
14518
+ ${formatAsmrResult(asmrResult, args.query)}`;
14519
+ if (args.ensemble) {
14520
+ try {
14521
+ const answer = await ensembleAnswer(asmrResult, args.query);
14522
+ text += `
14523
+
14524
+ --- Ensemble Answer (confidence: ${(answer.confidence * 100).toFixed(0)}%, consensus: ${answer.consensus_reached ? "yes" : "no"}, escalated: ${answer.escalated ? "yes" : "no"}) ---
14525
+ ${answer.answer}
14526
+
14527
+ Reasoning: ${answer.reasoning}`;
14528
+ } catch (ensErr) {
14529
+ text += `
14530
+
14531
+ [Ensemble failed: ${ensErr instanceof Error ? ensErr.message : "unknown error"}]`;
14532
+ }
14533
+ }
14534
+ return { content: [{ type: "text", text }] };
14535
+ } catch (e) {
14536
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
14537
+ }
14538
+ });
11598
14539
  server.tool("memory_check_contradiction", "Check if a new memory would contradict existing high-importance facts. Call before saving to detect conflicts. Returns contradiction details if found.", {
11599
14540
  key: exports_external.string().describe("Memory key to check"),
11600
14541
  value: exports_external.string().describe("New value to check for contradictions"),
@@ -11900,9 +14841,23 @@ server.tool("memory_inject", "Get memory context for system prompt injection. Se
11900
14841
  format: exports_external.enum(["xml", "markdown", "compact", "json"]).optional(),
11901
14842
  raw: exports_external.boolean().optional(),
11902
14843
  strategy: exports_external.enum(["default", "smart"]).optional().describe("Injection strategy: 'default' uses importance+recency, 'smart' uses embedding similarity+importance+recency"),
11903
- query: exports_external.string().optional().describe("Query for smart injection relevance scoring. Required when strategy='smart'.")
14844
+ query: exports_external.string().optional().describe("Query for smart injection relevance scoring. Required when strategy='smart'."),
14845
+ task_context: exports_external.string().optional().describe("What the agent is about to do. When provided, activates intent-based retrieval \u2014 matches against when_to_use fields for situationally relevant memories."),
14846
+ mode: exports_external.enum(["full", "hints"]).optional().default("full").describe("'full' = inject complete memory content (default), 'hints' = inject lightweight topic summary with counts, saving 60-70% tokens. Agent uses memory_recall to pull details as needed.")
11904
14847
  }, async (args) => {
11905
14848
  try {
14849
+ if (args.strategy === "smart" && args.task_context && args.mode !== "hints") {
14850
+ const { smartInject: smartInject2 } = await Promise.resolve().then(() => (init_injector(), exports_injector));
14851
+ const result = await smartInject2({
14852
+ task_context: args.task_context,
14853
+ project_id: args.project_id,
14854
+ agent_id: args.agent_id,
14855
+ session_id: args.session_id,
14856
+ max_tokens: args.max_tokens,
14857
+ min_importance: args.min_importance
14858
+ });
14859
+ return { content: [{ type: "text", text: result.output }] };
14860
+ }
11906
14861
  const maxTokens = args.max_tokens || 500;
11907
14862
  const minImportance = args.min_importance || 3;
11908
14863
  const categories = args.categories || ["preference", "fact", "knowledge"];
@@ -11956,10 +14911,29 @@ server.tool("memory_inject", "Get memory context for system prompt injection. Se
11956
14911
  seen.add(m.id);
11957
14912
  return true;
11958
14913
  });
14914
+ const activationBoostedIds = new Set;
14915
+ if (args.task_context) {
14916
+ try {
14917
+ const activationResults = await semanticSearch(args.task_context, {
14918
+ threshold: 0.3,
14919
+ limit: 20,
14920
+ scope: undefined,
14921
+ agent_id: args.agent_id,
14922
+ project_id: args.project_id
14923
+ });
14924
+ for (const r of activationResults) {
14925
+ activationBoostedIds.add(r.memory.id);
14926
+ if (!seen.has(r.memory.id)) {
14927
+ seen.add(r.memory.id);
14928
+ unique.push(r.memory);
14929
+ }
14930
+ }
14931
+ } catch {}
14932
+ }
11959
14933
  if (args.strategy === "smart" && args.query) {
11960
14934
  const { generateEmbedding: genEmb, cosineSimilarity: cosSim, deserializeEmbedding: deserEmb } = await Promise.resolve().then(() => exports_embeddings);
11961
- const { getDatabase: getDb } = await Promise.resolve().then(() => (init_database(), exports_database));
11962
- const d = getDb();
14935
+ const { getDatabase: getDb2 } = await Promise.resolve().then(() => (init_database(), exports_database));
14936
+ const d = getDb2();
11963
14937
  const { embedding: queryEmbedding } = await genEmb(args.query);
11964
14938
  const embeddingMap = new Map;
11965
14939
  if (unique.length > 0) {
@@ -11978,7 +14952,7 @@ server.tool("memory_inject", "Get memory context for system prompt injection. Se
11978
14952
  for (const m of unique) {
11979
14953
  const memEmb = embeddingMap.get(m.id);
11980
14954
  const similarity = memEmb ? cosSim(queryEmbedding, memEmb) : 0;
11981
- const importanceScore = m.importance / 10;
14955
+ const importanceScore = (m.importance + (activationBoostedIds.has(m.id) ? 3 : 0)) / 10;
11982
14956
  const recencyScore = (new Date(m.updated_at).getTime() - oldestMs) / timeRange;
11983
14957
  const pinBonus = m.pinned ? 0.5 : 0;
11984
14958
  scores.set(m.id, similarity * 0.4 + importanceScore * 0.3 + recencyScore * 0.3 + pinBonus);
@@ -11986,18 +14960,83 @@ server.tool("memory_inject", "Get memory context for system prompt injection. Se
11986
14960
  unique.sort((a, b) => (scores.get(b.id) || 0) - (scores.get(a.id) || 0));
11987
14961
  } else {
11988
14962
  unique.sort((a, b) => {
11989
- if (b.importance !== a.importance)
11990
- return b.importance - a.importance;
14963
+ const aImp = a.importance + (activationBoostedIds.has(a.id) ? 3 : 0);
14964
+ const bImp = b.importance + (activationBoostedIds.has(b.id) ? 3 : 0);
14965
+ if (bImp !== aImp)
14966
+ return bImp - aImp;
11991
14967
  return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
11992
14968
  });
11993
14969
  }
11994
14970
  } else {
11995
14971
  unique.sort((a, b) => {
11996
- if (b.importance !== a.importance)
11997
- return b.importance - a.importance;
14972
+ const aImp = a.importance + (activationBoostedIds.has(a.id) ? 3 : 0);
14973
+ const bImp = b.importance + (activationBoostedIds.has(b.id) ? 3 : 0);
14974
+ if (bImp !== aImp)
14975
+ return bImp - aImp;
11998
14976
  return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
11999
14977
  });
12000
14978
  }
14979
+ if (args.mode === "hints") {
14980
+ let extractTopics = function(memories, maxTopics = 8) {
14981
+ const wordCounts = new Map;
14982
+ for (const m of memories) {
14983
+ const keyWords = m.key.split(/[-_\s]+/).filter((w) => w.length > 2 && !stopWords.has(w.toLowerCase()));
14984
+ for (const w of keyWords) {
14985
+ const lower = w.toLowerCase();
14986
+ wordCounts.set(lower, (wordCounts.get(lower) || 0) + 1);
14987
+ }
14988
+ if (m.tags) {
14989
+ const tags = Array.isArray(m.tags) ? m.tags : typeof m.tags === "string" ? m.tags.split(",") : [];
14990
+ for (const t of tags) {
14991
+ const tag = t.trim().toLowerCase();
14992
+ if (tag.length > 2 && !stopWords.has(tag)) {
14993
+ wordCounts.set(tag, (wordCounts.get(tag) || 0) + 1);
14994
+ }
14995
+ }
14996
+ }
14997
+ }
14998
+ return Array.from(wordCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxTopics).map(([word]) => word);
14999
+ };
15000
+ if (unique.length === 0) {
15001
+ return { content: [{ type: "text", text: "No relevant memories found." }] };
15002
+ }
15003
+ const groups = new Map;
15004
+ for (const m of unique) {
15005
+ const cat = m.category || "knowledge";
15006
+ if (!groups.has(cat))
15007
+ groups.set(cat, []);
15008
+ groups.get(cat).push(m);
15009
+ }
15010
+ const stopWords = new Set(["the", "a", "an", "is", "in", "on", "at", "to", "for", "of", "and", "or", "with", "my", "this", "that"]);
15011
+ const categoryLabels = {
15012
+ fact: "Facts",
15013
+ preference: "Preferences",
15014
+ knowledge: "Knowledge",
15015
+ history: "History",
15016
+ procedural: "Procedures",
15017
+ resource: "Resources"
15018
+ };
15019
+ const hintLines = ["You have relevant memories available:"];
15020
+ const categoryOrder = ["fact", "knowledge", "preference", "procedural", "history", "resource"];
15021
+ for (const cat of categoryOrder) {
15022
+ const mems = groups.get(cat);
15023
+ if (!mems || mems.length === 0)
15024
+ continue;
15025
+ const label = categoryLabels[cat] || cat;
15026
+ const topics = extractTopics(mems);
15027
+ hintLines.push(`- ${label} (${mems.length}): ${topics.join(", ")}`);
15028
+ }
15029
+ for (const [cat, mems] of groups) {
15030
+ if (categoryOrder.includes(cat))
15031
+ continue;
15032
+ const topics = extractTopics(mems);
15033
+ hintLines.push(`- ${cat} (${mems.length}): ${topics.join(", ")}`);
15034
+ }
15035
+ hintLines.push("");
15036
+ hintLines.push('Use memory_recall(key="...") to access any of these. Use memory_search(query="...") for broader searches.');
15037
+ return { content: [{ type: "text", text: hintLines.join(`
15038
+ `) }] };
15039
+ }
12001
15040
  const fmt = args.format ?? (args.raw ? "compact" : "xml");
12002
15041
  const preCtx = {
12003
15042
  memories: unique,
@@ -12624,9 +15663,21 @@ server.tool("memory_context", "Get memories relevant to current context. Uses ti
12624
15663
  scope: exports_external.enum(["global", "shared", "private", "working"]).optional(),
12625
15664
  limit: exports_external.coerce.number().optional(),
12626
15665
  decay_halflife_days: exports_external.coerce.number().optional().describe("Importance half-life in days (default: 90). Lower = more weight on recent memories."),
12627
- no_decay: exports_external.coerce.boolean().optional().describe("Set true to disable decay and sort purely by importance.")
15666
+ no_decay: exports_external.coerce.boolean().optional().describe("Set true to disable decay and sort purely by importance."),
15667
+ task_context: exports_external.string().optional().describe("What the agent is about to do. When provided, activates intent-based retrieval \u2014 matches against when_to_use fields for situationally relevant memories."),
15668
+ strategy: exports_external.enum(["default", "smart"]).optional().default("default").describe("Injection strategy: 'default' = decay-scored, 'smart' = activation-matched + layered + tool-aware (requires task_context)")
12628
15669
  }, async (args) => {
12629
15670
  try {
15671
+ if (args.strategy === "smart" && args.task_context) {
15672
+ const { smartInject: smartInject2 } = await Promise.resolve().then(() => (init_injector(), exports_injector));
15673
+ const result = await smartInject2({
15674
+ task_context: args.task_context,
15675
+ project_id: args.project_id,
15676
+ agent_id: args.agent_id,
15677
+ max_tokens: args.limit ? args.limit * 20 : undefined
15678
+ });
15679
+ return { content: [{ type: "text", text: result.output }] };
15680
+ }
12630
15681
  const filter = {
12631
15682
  scope: args.scope,
12632
15683
  agent_id: args.agent_id,
@@ -12635,18 +15686,39 @@ server.tool("memory_context", "Get memories relevant to current context. Uses ti
12635
15686
  limit: (args.limit || 30) * 2
12636
15687
  };
12637
15688
  const memories = listMemories(filter);
15689
+ const activationBoostedIds = new Set;
15690
+ if (args.task_context) {
15691
+ try {
15692
+ const activationResults = await semanticSearch(args.task_context, {
15693
+ threshold: 0.3,
15694
+ limit: 20,
15695
+ scope: args.scope,
15696
+ agent_id: args.agent_id,
15697
+ project_id: args.project_id
15698
+ });
15699
+ const seenIds = new Set(memories.map((m) => m.id));
15700
+ for (const r of activationResults) {
15701
+ activationBoostedIds.add(r.memory.id);
15702
+ if (!seenIds.has(r.memory.id)) {
15703
+ seenIds.add(r.memory.id);
15704
+ memories.push(r.memory);
15705
+ }
15706
+ }
15707
+ } catch {}
15708
+ }
12638
15709
  if (memories.length === 0) {
12639
15710
  return { content: [{ type: "text", text: "No memories in current context." }] };
12640
15711
  }
12641
15712
  const halflifeDays = args.decay_halflife_days ?? 90;
12642
- const now2 = Date.now();
15713
+ const now3 = Date.now();
12643
15714
  const scored = memories.map((m) => {
12644
- let effectiveScore = m.importance;
15715
+ const activationBoost = activationBoostedIds.has(m.id) ? 3 : 0;
15716
+ let effectiveScore = m.importance + activationBoost;
12645
15717
  if (!args.no_decay && !m.pinned) {
12646
- const ageMs = now2 - new Date(m.updated_at).getTime();
12647
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
15718
+ const ageMs = now3 - new Date(m.updated_at).getTime();
15719
+ const ageDays = ageMs / 86400000;
12648
15720
  const decayFactor = Math.pow(0.5, ageDays / halflifeDays);
12649
- effectiveScore = m.importance * decayFactor;
15721
+ effectiveScore = (m.importance + activationBoost) * decayFactor;
12650
15722
  }
12651
15723
  if (m.flag)
12652
15724
  effectiveScore = Math.max(effectiveScore, 11);
@@ -12692,6 +15764,25 @@ ${ctx.total_memories} memories, ~${ctx.token_estimate} tokens` }] };
12692
15764
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12693
15765
  }
12694
15766
  });
15767
+ server.tool("memory_profile", "Synthesize a coherent profile from preference and fact memories using LLM. Cached for 24h, auto-refreshed when preferences change. Returns markdown profile.", {
15768
+ project_id: exports_external.string().optional(),
15769
+ agent_id: exports_external.string().optional(),
15770
+ scope: exports_external.enum(["agent", "project", "global"]).optional().default("project"),
15771
+ force_refresh: exports_external.boolean().optional().default(false).describe("Force re-synthesis even if cached profile exists")
15772
+ }, async (args) => {
15773
+ try {
15774
+ ensureAutoProject();
15775
+ const result = await synthesizeProfile(args);
15776
+ if (!result) {
15777
+ return { content: [{ type: "text", text: "No preference or fact memories found to synthesize a profile from." }] };
15778
+ }
15779
+ return { content: [{ type: "text", text: `${result.from_cache ? "[cached] " : "[synthesized] "}(${result.memory_count} memories)
15780
+
15781
+ ${result.profile}` }] };
15782
+ } catch (e) {
15783
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
15784
+ }
15785
+ });
12695
15786
  function resolveEntityParam(nameOrId, type) {
12696
15787
  const byName = getEntityByName(nameOrId, type);
12697
15788
  if (byName)
@@ -13043,6 +16134,95 @@ server.tool("build_file_dep_graph", "Scan a codebase directory and build a file
13043
16134
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
13044
16135
  }
13045
16136
  });
16137
+ server.tool("memory_tool_insights", "Get usage stats, lessons learned, and recommendations for MCP tools. Helps agents avoid past mistakes and reuse successful patterns.", {
16138
+ tool_name: exports_external.string().optional().describe("Specific tool to get insights for. If omitted, returns insights for all tools."),
16139
+ task_context: exports_external.string().optional().describe("What the agent is about to do \u2014 used to find relevant tool lessons via semantic match"),
16140
+ project_id: exports_external.string().optional(),
16141
+ agent_id: exports_external.string().optional(),
16142
+ limit: exports_external.coerce.number().optional().default(10).describe("Max lessons to return per tool")
16143
+ }, async (args) => {
16144
+ try {
16145
+ const db = getDatabase();
16146
+ const projId = args.project_id ? resolvePartialId(db, "projects", args.project_id) ?? args.project_id : undefined;
16147
+ const limit = args.limit ?? 10;
16148
+ let toolNames;
16149
+ if (args.tool_name) {
16150
+ toolNames = [args.tool_name];
16151
+ } else {
16152
+ const filters = { limit: 200 };
16153
+ if (projId)
16154
+ filters.project_id = projId;
16155
+ if (args.agent_id)
16156
+ filters.agent_id = args.agent_id;
16157
+ const events = getToolEvents(filters);
16158
+ toolNames = [...new Set(events.map((e) => e.tool_name))];
16159
+ }
16160
+ if (toolNames.length === 0) {
16161
+ return { content: [{ type: "text", text: "No tool events recorded yet." }] };
16162
+ }
16163
+ const sections = [];
16164
+ for (const tn of toolNames) {
16165
+ const stats = getToolStats(tn, projId);
16166
+ const lessons = getToolLessons(tn, projId, limit);
16167
+ const successPct = stats.total_calls > 0 ? Math.round(stats.success_rate * 100) : 0;
16168
+ const avgTok = stats.avg_tokens != null ? Math.round(stats.avg_tokens) : "?";
16169
+ const avgLat = stats.avg_latency_ms != null ? (stats.avg_latency_ms / 1000).toFixed(1) : "?";
16170
+ let section = `## Tool: ${tn}
16171
+ Stats: ${stats.total_calls} calls | ${successPct}% success | avg ${avgTok} tokens | avg ${avgLat}s`;
16172
+ if (stats.common_errors.length > 0) {
16173
+ const errParts = stats.common_errors.map((e) => `${e.error_type} (${e.count})`);
16174
+ section += `
16175
+ Common errors: ${errParts.join(", ")}`;
16176
+ }
16177
+ if (lessons.length > 0) {
16178
+ const dos = [];
16179
+ const donts = [];
16180
+ for (const l of lessons) {
16181
+ const ctx = (l.when_to_use || "").toLowerCase();
16182
+ if (ctx.includes("fail") || ctx.includes("error") || ctx.includes("avoid") || ctx.includes("don't") || ctx.includes("never")) {
16183
+ donts.push(l.lesson);
16184
+ } else {
16185
+ dos.push(l.lesson);
16186
+ }
16187
+ }
16188
+ if (dos.length > 0 || donts.length > 0) {
16189
+ section += `
16190
+
16191
+ ### Recommendations`;
16192
+ for (const d of dos.slice(0, 5))
16193
+ section += `
16194
+ \u2705 DO: ${d}`;
16195
+ for (const d of donts.slice(0, 5))
16196
+ section += `
16197
+ \u274C DON'T: ${d}`;
16198
+ }
16199
+ section += `
16200
+
16201
+ ### Lessons (newest first)`;
16202
+ for (const l of lessons) {
16203
+ const when = l.when_to_use ? ` (when: ${l.when_to_use})` : "";
16204
+ section += `
16205
+ - ${l.lesson}${when}`;
16206
+ }
16207
+ }
16208
+ sections.push(section);
16209
+ }
16210
+ let header = "";
16211
+ if (args.task_context) {
16212
+ header = `> Context: "${args.task_context}"
16213
+ > Showing insights filtered for relevance.
16214
+
16215
+ `;
16216
+ }
16217
+ return { content: [{ type: "text", text: header + sections.join(`
16218
+
16219
+ ---
16220
+
16221
+ `) }] };
16222
+ } catch (e) {
16223
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
16224
+ }
16225
+ });
13046
16226
  var FULL_SCHEMAS = {
13047
16227
  memory_save: {
13048
16228
  description: "Save/upsert a memory. Creates new or merges with existing key.",
@@ -13060,7 +16240,10 @@ var FULL_SCHEMAS = {
13060
16240
  session_id: { type: "string", description: "Session UUID" },
13061
16241
  ttl_ms: { type: "string|number", description: "Time-to-live e.g. '7d', '2h', or ms integer" },
13062
16242
  source: { type: "string", description: "Origin of the memory", enum: ["user", "agent", "system", "auto", "imported"] },
13063
- metadata: { type: "object", description: "Arbitrary JSON metadata" }
16243
+ metadata: { type: "object", description: "Arbitrary JSON metadata" },
16244
+ when_to_use: { type: "string", description: "Activation context \u2014 describes WHEN this memory should be retrieved. Used for intent-based retrieval. Example: 'when deploying to production'" },
16245
+ sequence_group: { type: "string", description: "Chain/sequence group ID \u2014 links memories into an ordered procedural sequence" },
16246
+ sequence_order: { type: "number", description: "Position within the sequence group (1-based)" }
13064
16247
  },
13065
16248
  example: '{"key":"preferred-language","value":"TypeScript","scope":"global","importance":8,"tags":["language","preference"]}'
13066
16249
  },
@@ -13092,6 +16275,15 @@ var FULL_SCHEMAS = {
13092
16275
  },
13093
16276
  example: '{"key":"preferred-language","scope":"global"}'
13094
16277
  },
16278
+ memory_chain_get: {
16279
+ description: "Retrieve an ordered memory chain/sequence by group ID. Returns all steps in procedural order.",
16280
+ category: "memory",
16281
+ params: {
16282
+ sequence_group: { type: "string", description: "The chain/sequence group ID to retrieve", required: true },
16283
+ project_id: { type: "string", description: "Project UUID filter" }
16284
+ },
16285
+ example: '{"sequence_group":"deploy-to-production"}'
16286
+ },
13095
16287
  memory_list: {
13096
16288
  description: "List memories with optional filters. Returns compact lines by default.",
13097
16289
  category: "memory",
@@ -13127,7 +16319,8 @@ var FULL_SCHEMAS = {
13127
16319
  pinned: { type: "boolean", description: "Pin/unpin the memory" },
13128
16320
  status: { type: "string", description: "New status", enum: ["active", "archived", "expired"] },
13129
16321
  metadata: { type: "object", description: "New metadata (replaces existing)" },
13130
- expires_at: { type: "string", description: "New expiry ISO timestamp (null to clear)" }
16322
+ expires_at: { type: "string", description: "New expiry ISO timestamp (null to clear)" },
16323
+ when_to_use: { type: "string", description: "Update the activation context for this memory" }
13131
16324
  },
13132
16325
  example: '{"id":"abc123","version":1,"importance":9,"tags":["correction","important"]}'
13133
16326
  },
@@ -13217,6 +16410,18 @@ var FULL_SCHEMAS = {
13217
16410
  },
13218
16411
  example: '{"query":"database migration","limit":5}'
13219
16412
  },
16413
+ memory_recall_deep: {
16414
+ description: "Deep memory recall using ASMR 3-agent search (facts, context, temporal). Modes: fast (hybrid), deep (ASMR), auto (fast then escalate). Optional ensemble answering.",
16415
+ category: "memory",
16416
+ params: {
16417
+ query: { type: "string", description: "Natural language query", required: true },
16418
+ mode: { type: "string", description: "fast=FTS+semantic, deep=ASMR 3-agent, auto=fast then escalate", enum: ["fast", "deep", "auto"] },
16419
+ max_results: { type: "number", description: "Max results (default 20)" },
16420
+ ensemble: { type: "boolean", description: "Use ensemble answering with majority voting (default false)" },
16421
+ project_id: { type: "string", description: "Project UUID filter" }
16422
+ },
16423
+ example: '{"query":"what is the deployment process","mode":"deep","ensemble":true}'
16424
+ },
13220
16425
  memory_activity: {
13221
16426
  description: "Get daily memory creation counts over N days (max 365). Like 'git log --stat' for memories.",
13222
16427
  category: "memory",
@@ -13255,7 +16460,7 @@ var FULL_SCHEMAS = {
13255
16460
  example: '{"memories":[{"key":"foo","value":"bar","scope":"global","importance":7}]}'
13256
16461
  },
13257
16462
  memory_inject: {
13258
- description: "Get formatted memory context for system prompt injection. Respects token budget. Use strategy='smart' with a query for embedding-based relevance scoring.",
16463
+ description: "Get formatted memory context for system prompt injection. Respects token budget. Use strategy='smart' with task_context for full activation-matched + layered + tool-aware pipeline. Use mode='hints' for a lightweight topic summary (60-70% fewer tokens) \u2014 agent can then use memory_recall for details on demand.",
13259
16464
  category: "memory",
13260
16465
  params: {
13261
16466
  agent_id: { type: "string", description: "Agent UUID to include private memories" },
@@ -13266,10 +16471,12 @@ var FULL_SCHEMAS = {
13266
16471
  min_importance: { type: "number", description: "Minimum importance (default 3)" },
13267
16472
  format: { type: "string", description: "Output format: xml (default, <agent-memories>), compact (key: value, ~60% smaller), markdown, json", enum: ["xml", "compact", "markdown", "json"] },
13268
16473
  raw: { type: "boolean", description: "Deprecated: use format=compact instead. true=plain lines only" },
13269
- strategy: { type: "string", description: "Injection strategy: 'default' (importance+recency) or 'smart' (embedding similarity+importance+recency)", enum: ["default", "smart"] },
13270
- query: { type: "string", description: "Query for smart injection relevance scoring. Required when strategy='smart'." }
16474
+ strategy: { type: "string", description: "Injection strategy: 'default' = decay-scored (importance+recency), 'smart' = full pipeline (activation-matched + layered + tool-aware). Smart requires task_context.", enum: ["default", "smart"] },
16475
+ query: { type: "string", description: "Query for smart injection relevance scoring. Required when strategy='smart'." },
16476
+ task_context: { type: "string", description: "What the agent is about to do. Required for strategy='smart'. Activates intent-based retrieval \u2014 matches against when_to_use fields for situationally relevant memories." },
16477
+ mode: { type: "string", description: "Injection mode: 'full' (default) = inject complete memory content, 'hints' = lightweight topic summary with counts per category, saving 60-70% tokens. In hints mode, use memory_recall(key=...) or memory_search(query=...) to pull details on demand.", enum: ["full", "hints"] }
13271
16478
  },
13272
- example: '{"project_id":"proj-uuid","max_tokens":300,"min_importance":5,"format":"compact","strategy":"smart","query":"database migrations"}'
16479
+ example: '{"project_id":"proj-uuid","max_tokens":300,"strategy":"smart","task_context":"writing database migration for user table"}'
13273
16480
  },
13274
16481
  session_extract: {
13275
16482
  description: "Auto-create memories from a session summary (title, topics, notes, project). Designed for sessions\u2192mementos integration.",
@@ -13288,15 +16495,28 @@ var FULL_SCHEMAS = {
13288
16495
  example: '{"session_id":"abc123","title":"Fix auth middleware","project":"alumia","key_topics":["jwt","compliance"],"agent_id":"galba-id"}'
13289
16496
  },
13290
16497
  memory_context: {
13291
- description: "Get active memories for the current context (agent/project/scope).",
16498
+ description: "Get active memories for the current context (agent/project/scope). Supports intent-based retrieval via task_context. Use strategy='smart' for full activation-matched + layered + tool-aware pipeline.",
13292
16499
  category: "memory",
13293
16500
  params: {
13294
16501
  agent_id: { type: "string", description: "Agent UUID filter" },
13295
16502
  project_id: { type: "string", description: "Project UUID filter" },
13296
16503
  scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private", "working"] },
13297
- limit: { type: "number", description: "Max results (default 30)" }
16504
+ limit: { type: "number", description: "Max results (default 30)" },
16505
+ task_context: { type: "string", description: "What the agent is about to do. Required for strategy='smart'. Activates intent-based retrieval \u2014 matches against when_to_use fields for situationally relevant memories." },
16506
+ strategy: { type: "string", description: "Injection strategy: 'default' = decay-scored, 'smart' = activation-matched + layered + tool-aware (requires task_context)", enum: ["default", "smart"] }
16507
+ },
16508
+ example: '{"project_id":"proj-uuid","scope":"shared","limit":20,"strategy":"smart","task_context":"deploying to production"}'
16509
+ },
16510
+ memory_profile: {
16511
+ description: "Synthesize a coherent profile from preference and fact memories using LLM. Cached for 24h, auto-refreshed when preferences change.",
16512
+ category: "memory",
16513
+ params: {
16514
+ project_id: { type: "string", description: "Project UUID to scope profile to" },
16515
+ agent_id: { type: "string", description: "Agent UUID to scope profile to" },
16516
+ scope: { type: "string", description: "Profile scope", enum: ["agent", "project", "global"] },
16517
+ force_refresh: { type: "boolean", description: "Force re-synthesis even if cached profile exists (default false)" }
13298
16518
  },
13299
- example: '{"project_id":"proj-uuid","scope":"shared","limit":20}'
16519
+ example: '{"project_id":"proj-uuid","scope":"project"}'
13300
16520
  },
13301
16521
  register_agent: {
13302
16522
  description: "Register an agent. Idempotent \u2014 same name returns existing agent.",
@@ -13637,6 +16857,68 @@ var FULL_SCHEMAS = {
13637
16857
  },
13638
16858
  example: '{"id":"sub-abc12"}'
13639
16859
  },
16860
+ memory_tool_insights: {
16861
+ description: "Get usage stats, lessons learned, and recommendations for MCP tools. Helps agents avoid past mistakes and reuse successful patterns.",
16862
+ category: "utility",
16863
+ params: {
16864
+ tool_name: { type: "string", description: "Specific tool to get insights for. If omitted, returns insights for all tools." },
16865
+ task_context: { type: "string", description: "What the agent is about to do \u2014 used to find relevant tool lessons via semantic match" },
16866
+ project_id: { type: "string", description: "Project ID filter" },
16867
+ agent_id: { type: "string", description: "Agent ID filter" },
16868
+ limit: { type: "number", description: "Max lessons to return per tool (default: 10)" }
16869
+ },
16870
+ example: '{"tool_name":"bash","limit":5}'
16871
+ },
16872
+ memory_save_tool_event: {
16873
+ description: "Record a tool call event (success/failure, latency, tokens). Optionally saves a lesson as a shared memory.",
16874
+ category: "memory",
16875
+ params: {
16876
+ tool_name: { type: "string", description: "Name of the tool that was called", required: true },
16877
+ action: { type: "string", description: "What was attempted" },
16878
+ success: { type: "boolean", description: "Whether the tool call succeeded", required: true },
16879
+ error_type: { type: "string", description: "Error category if failed", enum: ["timeout", "permission", "not_found", "syntax", "rate_limit", "other"] },
16880
+ error_message: { type: "string", description: "Raw error text if failed" },
16881
+ tokens_used: { type: "number", description: "Tokens consumed by the tool call" },
16882
+ latency_ms: { type: "number", description: "Time taken in milliseconds" },
16883
+ context: { type: "string", description: "What task triggered this tool call" },
16884
+ lesson: { type: "string", description: "Qualitative insight learned from this call" },
16885
+ when_to_use: { type: "string", description: "Activation context for the lesson" },
16886
+ agent_id: { type: "string", description: "Agent ID" },
16887
+ project_id: { type: "string", description: "Project ID" },
16888
+ session_id: { type: "string", description: "Session ID" }
16889
+ },
16890
+ example: '{"tool_name":"bash","action":"npm install","success":false,"error_type":"timeout","lesson":"npm install hangs on large monorepos \u2014 use --prefer-offline","when_to_use":"when installing deps in a monorepo"}'
16891
+ },
16892
+ memory_autoinject_config: {
16893
+ description: "Get or set auto-inject orchestrator config (channel-based proactive memory push). Controls throttle, debounce, rate limits, similarity thresholds.",
16894
+ category: "utility",
16895
+ params: {
16896
+ action: { type: "string", description: "Get or set auto-inject config", enum: ["get", "set"], required: true },
16897
+ throttle_ms: { type: "number", description: "Min ms between pushes (default 30000)" },
16898
+ debounce_ms: { type: "number", description: "Wait ms after last message before processing (default 2000)" },
16899
+ max_pushes_per_5min: { type: "number", description: "Rate limit per 5-minute window (default 5)" },
16900
+ min_similarity: { type: "number", description: "Minimum activation match threshold 0-1 (default 0.4)" },
16901
+ enabled: { type: "boolean", description: "Enable/disable auto-inject" },
16902
+ session_briefing: { type: "boolean", description: "Push session-start briefing (default true)" }
16903
+ },
16904
+ example: '{"action":"set","throttle_ms":15000,"min_similarity":0.5}'
16905
+ },
16906
+ memory_autoinject_status: {
16907
+ description: "Get auto-inject orchestrator status: running state, session watcher, push history, rate limit counters, and full config.",
16908
+ category: "utility",
16909
+ params: {},
16910
+ example: "{}"
16911
+ },
16912
+ memory_autoinject_test: {
16913
+ description: "Test what memories would be activated by a given context WITHOUT pushing. Shows what the auto-inject pipeline would match \u2014 useful for tuning min_similarity.",
16914
+ category: "utility",
16915
+ params: {
16916
+ context_text: { type: "string", description: "Simulated context to test activation matching", required: true },
16917
+ project_id: { type: "string", description: "Scope to a specific project" },
16918
+ min_similarity: { type: "number", description: "Minimum similarity threshold (default 0.4)" }
16919
+ },
16920
+ example: '{"context_text":"debugging a SQLite FTS5 index issue","min_similarity":0.3}'
16921
+ },
13640
16922
  search_tools: {
13641
16923
  description: "Search available tools by name or keyword. Returns matching tool names and categories.",
13642
16924
  category: "meta",
@@ -14333,6 +17615,149 @@ server.tool("memory_unsubscribe", "Remove a memory subscription by ID.", {
14333
17615
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
14334
17616
  }
14335
17617
  });
17618
+ server.tool("memory_save_tool_event", "Record a tool call event (success/failure, latency, tokens). Optionally saves a lesson as a shared memory.", {
17619
+ tool_name: exports_external.string().describe("Name of the tool that was called (e.g. 'bash', 'read', 'grep')"),
17620
+ action: exports_external.string().optional().describe("What was attempted (e.g. 'npm install', 'git push')"),
17621
+ success: exports_external.boolean().describe("Whether the tool call succeeded"),
17622
+ error_type: exports_external.enum(["timeout", "permission", "not_found", "syntax", "rate_limit", "other"]).optional().describe("Error category if failed"),
17623
+ error_message: exports_external.string().optional().describe("Raw error text if failed"),
17624
+ tokens_used: exports_external.number().optional().describe("Tokens consumed by the tool call"),
17625
+ latency_ms: exports_external.number().optional().describe("Time taken in milliseconds"),
17626
+ context: exports_external.string().optional().describe("What task triggered this tool call"),
17627
+ lesson: exports_external.string().optional().describe("Qualitative insight learned from this call"),
17628
+ when_to_use: exports_external.string().optional().describe("Activation context for the lesson"),
17629
+ agent_id: exports_external.string().optional(),
17630
+ project_id: exports_external.string().optional(),
17631
+ session_id: exports_external.string().optional()
17632
+ }, async (args) => {
17633
+ try {
17634
+ const event = saveToolEvent(args);
17635
+ if (args.lesson) {
17636
+ try {
17637
+ createMemory({
17638
+ key: `tool-lesson-${args.tool_name}-${Date.now()}`,
17639
+ value: args.lesson,
17640
+ category: "knowledge",
17641
+ scope: "shared",
17642
+ importance: 7,
17643
+ tags: ["tool-memory", args.tool_name],
17644
+ when_to_use: args.when_to_use,
17645
+ agent_id: args.agent_id,
17646
+ project_id: args.project_id,
17647
+ session_id: args.session_id,
17648
+ source: "auto"
17649
+ });
17650
+ } catch {}
17651
+ }
17652
+ return { content: [{ type: "text", text: JSON.stringify({
17653
+ id: event.id,
17654
+ tool_name: event.tool_name,
17655
+ success: event.success,
17656
+ error_type: event.error_type,
17657
+ lesson_saved: !!args.lesson,
17658
+ created_at: event.created_at
17659
+ }) }] };
17660
+ } catch (e) {
17661
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
17662
+ }
17663
+ });
17664
+ server.tool("memory_autoinject_config", "Get or set auto-inject orchestrator config (channel-based memory push). Controls throttle, debounce, rate limits, and similarity thresholds for proactive memory injection.", {
17665
+ action: exports_external.enum(["get", "set"]).describe("Get or set auto-inject config"),
17666
+ throttle_ms: exports_external.number().optional().describe("Min ms between pushes (default 30000)"),
17667
+ debounce_ms: exports_external.number().optional().describe("Wait ms after last message before processing (default 2000)"),
17668
+ max_pushes_per_5min: exports_external.number().optional().describe("Rate limit per 5-minute window (default 5)"),
17669
+ min_similarity: exports_external.number().optional().describe("Minimum activation match threshold 0-1 (default 0.4)"),
17670
+ enabled: exports_external.boolean().optional().describe("Enable/disable auto-inject"),
17671
+ session_briefing: exports_external.boolean().optional().describe("Push session-start briefing (default true)")
17672
+ }, async (args) => {
17673
+ try {
17674
+ if (args.action === "get") {
17675
+ return { content: [{ type: "text", text: JSON.stringify(getAutoInjectConfig(), null, 2) }] };
17676
+ }
17677
+ const updates = {};
17678
+ if (args.throttle_ms !== undefined)
17679
+ updates.throttle_ms = args.throttle_ms;
17680
+ if (args.debounce_ms !== undefined)
17681
+ updates.debounce_ms = args.debounce_ms;
17682
+ if (args.max_pushes_per_5min !== undefined)
17683
+ updates.max_pushes_per_5min = args.max_pushes_per_5min;
17684
+ if (args.min_similarity !== undefined)
17685
+ updates.min_similarity = args.min_similarity;
17686
+ if (args.enabled !== undefined)
17687
+ updates.enabled = args.enabled;
17688
+ if (args.session_briefing !== undefined)
17689
+ updates.session_briefing = args.session_briefing;
17690
+ const updated = updateAutoInjectConfig(updates);
17691
+ return { content: [{ type: "text", text: JSON.stringify({ updated: true, config: updated }, null, 2) }] };
17692
+ } catch (e) {
17693
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
17694
+ }
17695
+ });
17696
+ server.tool("memory_autoinject_status", "Get auto-inject orchestrator status: running state, session watcher, push history, rate limit counters, and full config.", {}, async () => {
17697
+ try {
17698
+ const status = getAutoInjectStatus();
17699
+ const lines = [
17700
+ `Running: ${status.running}`,
17701
+ `Session ID: ${status.session_id || "none"}`,
17702
+ `Watcher active: ${status.watcher.active}`,
17703
+ `Watching file: ${status.watcher.watching_file || "none"}`,
17704
+ `Last offset: ${status.watcher.last_offset}`,
17705
+ ``,
17706
+ `Pushes:`,
17707
+ ` Total: ${status.pushes.total}`,
17708
+ ` Last 5 min: ${status.pushes.last_5min}`,
17709
+ ` Recently pushed memories: ${status.pushes.recently_pushed_memories}`,
17710
+ ` Next available in: ${status.pushes.next_available_in_ms}ms`,
17711
+ ``,
17712
+ `Config:`,
17713
+ ` Enabled: ${status.config.enabled}`,
17714
+ ` Throttle: ${status.config.throttle_ms}ms`,
17715
+ ` Debounce: ${status.config.debounce_ms}ms`,
17716
+ ` Max pushes/5min: ${status.config.max_pushes_per_5min}`,
17717
+ ` Min similarity: ${status.config.min_similarity}`,
17718
+ ` Session briefing: ${status.config.session_briefing}`
17719
+ ];
17720
+ if (status.history.length > 0) {
17721
+ lines.push(``, `Recent push history:`);
17722
+ for (const h of status.history) {
17723
+ lines.push(` [${h.timestamp}] ${h.memory_count} memories \u2014 "${h.context}"`);
17724
+ }
17725
+ }
17726
+ return { content: [{ type: "text", text: lines.join(`
17727
+ `) }] };
17728
+ } catch (e) {
17729
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
17730
+ }
17731
+ });
17732
+ server.tool("memory_autoinject_test", "Test what memories would be activated by a given context WITHOUT pushing. Shows what the auto-inject pipeline would match \u2014 useful for tuning min_similarity.", {
17733
+ context_text: exports_external.string().describe("Simulated context to test activation matching"),
17734
+ project_id: exports_external.string().optional().describe("Scope to a specific project"),
17735
+ min_similarity: exports_external.number().optional().default(0.4).describe("Minimum similarity threshold (default 0.4)")
17736
+ }, async (args) => {
17737
+ try {
17738
+ const memories = await findActivatedMemories(args.context_text, {
17739
+ project_id: args.project_id,
17740
+ min_similarity: args.min_similarity
17741
+ });
17742
+ if (memories.length === 0) {
17743
+ return { content: [{ type: "text", text: "No memories matched the given context. Try lowering min_similarity or broadening the context text." }] };
17744
+ }
17745
+ const lines = [`${memories.length} memories would be activated:
17746
+ `];
17747
+ for (const m of memories) {
17748
+ lines.push(`- [${m.id.slice(0, 8)}] ${m.key}: ${m.value.slice(0, 200)}${m.value.length > 200 ? "\u2026" : ""}`);
17749
+ if (m.tags.length > 0)
17750
+ lines.push(` Tags: ${m.tags.join(", ")}`);
17751
+ lines.push(` Scope: ${m.scope} | Importance: ${m.importance}/10`);
17752
+ }
17753
+ lines.push(`
17754
+ Note: DRY RUN \u2014 nothing was pushed.`);
17755
+ return { content: [{ type: "text", text: lines.join(`
17756
+ `) }] };
17757
+ } catch (e) {
17758
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
17759
+ }
17760
+ });
14336
17761
  async function ensureRestServerRunning() {
14337
17762
  try {
14338
17763
  const res = await fetch("http://127.0.0.1:19428/api/memories?limit=0", {
@@ -14363,8 +17788,26 @@ async function main() {
14363
17788
  loadWebhooksFromDb();
14364
17789
  const transport = new StdioServerTransport;
14365
17790
  await server.connect(transport);
17791
+ const autoProject = detectProject();
17792
+ startAutoInject({
17793
+ server,
17794
+ project_id: autoProject?.id,
17795
+ project_name: autoProject?.name,
17796
+ cwd: process.cwd()
17797
+ }).catch(() => {});
17798
+ process.on("SIGINT", () => {
17799
+ stopAutoInject();
17800
+ process.exit(0);
17801
+ });
17802
+ process.on("SIGTERM", () => {
17803
+ stopAutoInject();
17804
+ process.exit(0);
17805
+ });
14366
17806
  }
14367
17807
  main().catch((error) => {
14368
17808
  console.error("MCP server error:", error);
14369
17809
  process.exit(1);
14370
17810
  });
17811
+ export {
17812
+ mcpServer
17813
+ };