@agenticmail/core 0.9.14 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -711,6 +711,7 @@ __export(index_exports, {
711
711
  AGENT_ROLES: () => AGENT_ROLES,
712
712
  AccountManager: () => AccountManager,
713
713
  AgentDeletionService: () => AgentDeletionService,
714
+ AgentMemoryManager: () => AgentMemoryManager,
714
715
  AgentMemoryStore: () => AgentMemoryStore,
715
716
  AgenticMailClient: () => AgenticMailClient,
716
717
  BRIDGE_OPERATOR_LIVE_WINDOW_MS: () => BRIDGE_OPERATOR_LIVE_WINDOW_MS,
@@ -727,8 +728,10 @@ __export(index_exports, {
727
728
  EmailSearchIndex: () => EmailSearchIndex,
728
729
  GatewayManager: () => GatewayManager,
729
730
  InboxWatcher: () => InboxWatcher,
731
+ MEMORY_CATEGORIES: () => MEMORY_CATEGORIES,
730
732
  MailReceiver: () => MailReceiver,
731
733
  MailSender: () => MailSender,
734
+ MemorySearchIndex: () => MemorySearchIndex,
732
735
  PHONE_MAX_CONCURRENT_MISSIONS: () => PHONE_MAX_CONCURRENT_MISSIONS,
733
736
  PHONE_MIN_WEBHOOK_SECRET_LENGTH: () => PHONE_MIN_WEBHOOK_SECRET_LENGTH,
734
737
  PHONE_MISSION_STATES: () => PHONE_MISSION_STATES,
@@ -819,7 +822,9 @@ __export(index_exports, {
819
822
  setTelemetryVersion: () => setTelemetryVersion,
820
823
  shouldSkipBridgeWakeForLiveOperator: () => shouldSkipBridgeWakeForLiveOperator,
821
824
  startRelayBridge: () => startRelayBridge,
825
+ stem: () => stem,
822
826
  threadIdFor: () => threadIdFor,
827
+ tokenize: () => tokenize,
823
828
  tryJoin: () => tryJoin,
824
829
  validateApiUrl: () => validateApiUrl,
825
830
  validatePhoneMissionPolicy: () => validatePhoneMissionPolicy,
@@ -2221,6 +2226,10 @@ var AccountManager = class {
2221
2226
  }
2222
2227
  const stmt = this.db.prepare("DELETE FROM agents WHERE id = ?");
2223
2228
  const result = stmt.run(id);
2229
+ try {
2230
+ this.db.prepare("DELETE FROM agent_memory WHERE agent_id = ?").run(id);
2231
+ } catch {
2232
+ }
2224
2233
  return result.changes > 0;
2225
2234
  }
2226
2235
  /**
@@ -10099,11 +10108,891 @@ function parse(raw) {
10099
10108
  }
10100
10109
  return out;
10101
10110
  }
10111
+
10112
+ // src/memory/manager.ts
10113
+ var import_node_crypto6 = require("crypto");
10114
+
10115
+ // src/memory/text-search.ts
10116
+ var BM25_K1 = 1.2;
10117
+ var BM25_B = 0.75;
10118
+ var FIELD_WEIGHT_TITLE = 3;
10119
+ var FIELD_WEIGHT_TAGS = 2;
10120
+ var FIELD_WEIGHT_CONTENT = 1;
10121
+ var PREFIX_MATCH_PENALTY = 0.7;
10122
+ var STOP_WORDS = /* @__PURE__ */ new Set([
10123
+ "a",
10124
+ "about",
10125
+ "above",
10126
+ "after",
10127
+ "again",
10128
+ "against",
10129
+ "all",
10130
+ "am",
10131
+ "an",
10132
+ "and",
10133
+ "any",
10134
+ "are",
10135
+ "as",
10136
+ "at",
10137
+ "be",
10138
+ "because",
10139
+ "been",
10140
+ "before",
10141
+ "being",
10142
+ "below",
10143
+ "between",
10144
+ "both",
10145
+ "but",
10146
+ "by",
10147
+ "can",
10148
+ "could",
10149
+ "did",
10150
+ "do",
10151
+ "does",
10152
+ "doing",
10153
+ "down",
10154
+ "during",
10155
+ "each",
10156
+ "either",
10157
+ "every",
10158
+ "few",
10159
+ "for",
10160
+ "from",
10161
+ "further",
10162
+ "get",
10163
+ "got",
10164
+ "had",
10165
+ "has",
10166
+ "have",
10167
+ "having",
10168
+ "he",
10169
+ "her",
10170
+ "here",
10171
+ "hers",
10172
+ "herself",
10173
+ "him",
10174
+ "himself",
10175
+ "his",
10176
+ "how",
10177
+ "i",
10178
+ "if",
10179
+ "in",
10180
+ "into",
10181
+ "is",
10182
+ "it",
10183
+ "its",
10184
+ "itself",
10185
+ "just",
10186
+ "may",
10187
+ "me",
10188
+ "might",
10189
+ "more",
10190
+ "most",
10191
+ "must",
10192
+ "my",
10193
+ "myself",
10194
+ "neither",
10195
+ "no",
10196
+ "nor",
10197
+ "not",
10198
+ "now",
10199
+ "of",
10200
+ "off",
10201
+ "on",
10202
+ "once",
10203
+ "only",
10204
+ "or",
10205
+ "other",
10206
+ "ought",
10207
+ "our",
10208
+ "ours",
10209
+ "ourselves",
10210
+ "out",
10211
+ "over",
10212
+ "own",
10213
+ "same",
10214
+ "shall",
10215
+ "she",
10216
+ "should",
10217
+ "so",
10218
+ "some",
10219
+ "such",
10220
+ "than",
10221
+ "that",
10222
+ "the",
10223
+ "their",
10224
+ "theirs",
10225
+ "them",
10226
+ "themselves",
10227
+ "then",
10228
+ "there",
10229
+ "these",
10230
+ "they",
10231
+ "this",
10232
+ "those",
10233
+ "through",
10234
+ "to",
10235
+ "too",
10236
+ "under",
10237
+ "until",
10238
+ "up",
10239
+ "us",
10240
+ "very",
10241
+ "was",
10242
+ "we",
10243
+ "were",
10244
+ "what",
10245
+ "when",
10246
+ "where",
10247
+ "which",
10248
+ "while",
10249
+ "who",
10250
+ "whom",
10251
+ "why",
10252
+ "will",
10253
+ "with",
10254
+ "would",
10255
+ "yet",
10256
+ "you",
10257
+ "your",
10258
+ "yours",
10259
+ "yourself",
10260
+ "yourselves"
10261
+ ]);
10262
+ var STEM_RULES = [
10263
+ // Step 1: plurals and past participles
10264
+ [/ies$/, "i", 3],
10265
+ // policies → polici,eries → eri
10266
+ [/sses$/, "ss", 4],
10267
+ // addresses → address
10268
+ [/([^s])s$/, "$1", 3],
10269
+ // items → item, but not "ss"
10270
+ [/eed$/, "ee", 4],
10271
+ // agreed → agree
10272
+ [/ed$/, "", 3],
10273
+ // configured → configur, but min length 3
10274
+ [/ing$/, "", 4],
10275
+ // running → runn → run (handled below)
10276
+ // Step 2: derivational suffixes
10277
+ [/ational$/, "ate", 6],
10278
+ // relational → relate
10279
+ [/tion$/, "t", 5],
10280
+ // adoption → adopt
10281
+ [/ness$/, "", 5],
10282
+ // awareness → aware
10283
+ [/ment$/, "", 5],
10284
+ // deployment → deploy
10285
+ [/able$/, "", 5],
10286
+ // configurable → configur
10287
+ [/ible$/, "", 5],
10288
+ // accessible → access
10289
+ [/ful$/, "", 5],
10290
+ // powerful → power
10291
+ [/ous$/, "", 5],
10292
+ // dangerous → danger
10293
+ [/ive$/, "", 5],
10294
+ // interactive → interact
10295
+ [/ize$/, "", 4],
10296
+ // normalize → normal
10297
+ [/ise$/, "", 4],
10298
+ // organise → organ
10299
+ [/ally$/, "", 5],
10300
+ // automatically → automat
10301
+ [/ly$/, "", 4],
10302
+ // quickly → quick
10303
+ [/er$/, "", 4]
10304
+ // handler → handl
10305
+ ];
10306
+ var DOUBLE_CONSONANT = /([^aeiou])\1$/;
10307
+ function stem(word) {
10308
+ if (word.length < 3) return word;
10309
+ let stemmed = word;
10310
+ for (const [pattern, replacement, minLen] of STEM_RULES) {
10311
+ if (stemmed.length >= minLen && pattern.test(stemmed)) {
10312
+ stemmed = stemmed.replace(pattern, replacement);
10313
+ break;
10314
+ }
10315
+ }
10316
+ if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
10317
+ stemmed = stemmed.slice(0, -1);
10318
+ }
10319
+ return stemmed;
10320
+ }
10321
+ function tokenize(text) {
10322
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
10323
+ }
10324
+ var MemorySearchIndex = class {
10325
+ /** Posting lists: stemmed term → Set of memory IDs containing it */
10326
+ postings = /* @__PURE__ */ new Map();
10327
+ /** Per-document metadata for BM25 scoring */
10328
+ docs = /* @__PURE__ */ new Map();
10329
+ /** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
10330
+ idf = /* @__PURE__ */ new Map();
10331
+ idfStale = true;
10332
+ /** 3-character prefix map for prefix matching: prefix → Set of full stems */
10333
+ prefixMap = /* @__PURE__ */ new Map();
10334
+ /** Total weighted document length (for computing average) */
10335
+ totalWeightedLen = 0;
10336
+ get docCount() {
10337
+ return this.docs.size;
10338
+ }
10339
+ get avgDocLen() {
10340
+ return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
10341
+ }
10342
+ /**
10343
+ * Index a memory entry. Extracts stems from title, content, and tags
10344
+ * with field-specific weighting and builds posting lists.
10345
+ */
10346
+ addDocument(id, entry) {
10347
+ if (this.docs.has(id)) this.removeDocument(id);
10348
+ const titleTokens = tokenize(entry.title);
10349
+ const contentTokens = tokenize(entry.content);
10350
+ const tagTokens = entry.tags.flatMap((t) => tokenize(t));
10351
+ const weightedTf = /* @__PURE__ */ new Map();
10352
+ for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
10353
+ for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
10354
+ for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
10355
+ const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
10356
+ const allStems = /* @__PURE__ */ new Set();
10357
+ for (const t of weightedTf.keys()) allStems.add(t);
10358
+ const stemSequence = [...titleTokens, ...contentTokens];
10359
+ const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
10360
+ this.docs.set(id, docRecord);
10361
+ this.totalWeightedLen += weightedLen;
10362
+ for (const term of allStems) {
10363
+ let posting = this.postings.get(term);
10364
+ if (!posting) {
10365
+ posting = /* @__PURE__ */ new Set();
10366
+ this.postings.set(term, posting);
10367
+ }
10368
+ posting.add(id);
10369
+ if (term.length >= 3) {
10370
+ const prefix = term.slice(0, 3);
10371
+ let prefixSet = this.prefixMap.get(prefix);
10372
+ if (!prefixSet) {
10373
+ prefixSet = /* @__PURE__ */ new Set();
10374
+ this.prefixMap.set(prefix, prefixSet);
10375
+ }
10376
+ prefixSet.add(term);
10377
+ }
10378
+ }
10379
+ this.idfStale = true;
10380
+ }
10381
+ /** Remove a document from the index. */
10382
+ removeDocument(id) {
10383
+ const doc = this.docs.get(id);
10384
+ if (!doc) return;
10385
+ this.totalWeightedLen -= doc.weightedLen;
10386
+ this.docs.delete(id);
10387
+ for (const term of doc.allStems) {
10388
+ const posting = this.postings.get(term);
10389
+ if (posting) {
10390
+ posting.delete(id);
10391
+ if (posting.size === 0) {
10392
+ this.postings.delete(term);
10393
+ if (term.length >= 3) {
10394
+ const prefixSet = this.prefixMap.get(term.slice(0, 3));
10395
+ if (prefixSet) {
10396
+ prefixSet.delete(term);
10397
+ if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
10398
+ }
10399
+ }
10400
+ }
10401
+ }
10402
+ }
10403
+ this.idfStale = true;
10404
+ }
10405
+ /** Recompute IDF values for all terms. Called lazily before search. */
10406
+ refreshIdf() {
10407
+ if (!this.idfStale) return;
10408
+ const N = this.docs.size;
10409
+ this.idf.clear();
10410
+ for (const [term, posting] of this.postings) {
10411
+ const df = posting.size;
10412
+ this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
10413
+ }
10414
+ this.idfStale = false;
10415
+ }
10416
+ /**
10417
+ * Expand query terms with prefix matches.
10418
+ * "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
10419
+ */
10420
+ expandQueryTerms(queryStems) {
10421
+ const expanded = /* @__PURE__ */ new Map();
10422
+ for (const qs of queryStems) {
10423
+ if (this.postings.has(qs)) {
10424
+ expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
10425
+ }
10426
+ if (qs.length >= 3) {
10427
+ const prefix = qs.slice(0, 3);
10428
+ const candidates = this.prefixMap.get(prefix);
10429
+ if (candidates) {
10430
+ for (const candidate of candidates) {
10431
+ if (candidate !== qs && candidate.startsWith(qs)) {
10432
+ expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
10433
+ }
10434
+ }
10435
+ }
10436
+ }
10437
+ }
10438
+ return expanded;
10439
+ }
10440
+ /**
10441
+ * Compute bigram proximity boost: if two query terms appear adjacent
10442
+ * in the document's stem sequence, boost the score.
10443
+ */
10444
+ bigramProximityBoost(docId, queryStems) {
10445
+ if (queryStems.length < 2) return 0;
10446
+ const doc = this.docs.get(docId);
10447
+ if (!doc || doc.stemSequence.length < 2) return 0;
10448
+ let boost = 0;
10449
+ const seq = doc.stemSequence;
10450
+ const querySet = new Set(queryStems);
10451
+ for (let i = 0; i < seq.length - 1; i++) {
10452
+ if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
10453
+ boost += 0.5;
10454
+ }
10455
+ }
10456
+ return Math.min(boost, 2);
10457
+ }
10458
+ /**
10459
+ * Search the index for documents matching a query.
10460
+ * Returns scored results sorted by BM25F relevance.
10461
+ *
10462
+ * @param query - Raw query string
10463
+ * @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
10464
+ * @returns Array of { id, score } sorted by descending score
10465
+ */
10466
+ search(query, candidateIds) {
10467
+ const queryStems = tokenize(query);
10468
+ if (queryStems.length === 0) return [];
10469
+ this.refreshIdf();
10470
+ const expandedTerms = this.expandQueryTerms(queryStems);
10471
+ if (expandedTerms.size === 0) return [];
10472
+ const avgDl = this.avgDocLen;
10473
+ const candidates = /* @__PURE__ */ new Set();
10474
+ for (const term of expandedTerms.keys()) {
10475
+ const posting = this.postings.get(term);
10476
+ if (posting) {
10477
+ for (const docId of posting) {
10478
+ if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
10479
+ }
10480
+ }
10481
+ }
10482
+ const results = [];
10483
+ for (const docId of candidates) {
10484
+ const doc = this.docs.get(docId);
10485
+ if (!doc) continue;
10486
+ let score = 0;
10487
+ for (const [term, weight] of expandedTerms) {
10488
+ const tf = doc.weightedTf.get(term) || 0;
10489
+ if (tf === 0) continue;
10490
+ const termIdf = this.idf.get(term) || 0;
10491
+ const numerator = tf * (BM25_K1 + 1);
10492
+ const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
10493
+ score += termIdf * (numerator / denominator) * weight;
10494
+ }
10495
+ score += this.bigramProximityBoost(docId, queryStems);
10496
+ if (score > 0) results.push({ id: docId, score });
10497
+ }
10498
+ results.sort((a, b) => b.score - a.score);
10499
+ return results;
10500
+ }
10501
+ /** Check if a document exists in the index. */
10502
+ has(id) {
10503
+ return this.docs.has(id);
10504
+ }
10505
+ };
10506
+
10507
+ // src/memory/manager.ts
10508
+ function sj(v, fb = {}) {
10509
+ if (!v) return fb;
10510
+ try {
10511
+ return JSON.parse(v);
10512
+ } catch {
10513
+ return fb;
10514
+ }
10515
+ }
10516
+ var MEMORY_CATEGORIES = {
10517
+ knowledge: {
10518
+ label: "Knowledge",
10519
+ description: "Facts, procedures, and reference information the agent has learned"
10520
+ },
10521
+ interaction_pattern: {
10522
+ label: "Interaction Patterns",
10523
+ description: "Learned patterns from past interactions"
10524
+ },
10525
+ preference: {
10526
+ label: "Preferences",
10527
+ description: "User and counterparty preferences"
10528
+ },
10529
+ correction: {
10530
+ label: "Corrections",
10531
+ description: "Corrections and feedback received"
10532
+ },
10533
+ skill: {
10534
+ label: "Skills",
10535
+ description: "Learned abilities and competencies"
10536
+ },
10537
+ context: {
10538
+ label: "Context",
10539
+ description: "Contextual information and background knowledge"
10540
+ },
10541
+ reflection: {
10542
+ label: "Reflections",
10543
+ description: "Self-reflective insights and learnings"
10544
+ },
10545
+ session_learning: {
10546
+ label: "Session Learnings",
10547
+ description: "Insights captured during conversation sessions"
10548
+ },
10549
+ system_notice: {
10550
+ label: "System Notices",
10551
+ description: "System-generated notifications about configuration changes"
10552
+ }
10553
+ };
10554
+ var VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
10555
+ var VALID_IMPORTANCE = /* @__PURE__ */ new Set(["critical", "high", "normal", "low"]);
10556
+ var IMPORTANCE_WEIGHT = {
10557
+ critical: 4,
10558
+ high: 3,
10559
+ normal: 2,
10560
+ low: 1
10561
+ };
10562
+ var AgentMemoryManager = class {
10563
+ constructor(db2) {
10564
+ this.db = db2;
10565
+ this.ensureTable();
10566
+ this.loadFromDb();
10567
+ }
10568
+ memories = /* @__PURE__ */ new Map();
10569
+ /** Per-agent index: agentId → Set of memory IDs for O(1) agent lookups */
10570
+ agentIndex = /* @__PURE__ */ new Map();
10571
+ /** Full-text search index (BM25F + stemming + inverted index) */
10572
+ searchIndex = new MemorySearchIndex();
10573
+ initialized = false;
10574
+ // ─── Database layer ─────────────────────────────────
10575
+ ensureTable() {
10576
+ if (this.initialized) return;
10577
+ this.db.exec(`
10578
+ CREATE TABLE IF NOT EXISTS agent_memory (
10579
+ id TEXT PRIMARY KEY,
10580
+ agent_id TEXT NOT NULL,
10581
+ category TEXT NOT NULL,
10582
+ title TEXT NOT NULL,
10583
+ content TEXT NOT NULL,
10584
+ source TEXT NOT NULL DEFAULT 'interaction',
10585
+ importance TEXT NOT NULL DEFAULT 'normal',
10586
+ confidence REAL NOT NULL DEFAULT 1.0,
10587
+ access_count INTEGER NOT NULL DEFAULT 0,
10588
+ last_accessed_at TEXT,
10589
+ expires_at TEXT,
10590
+ tags TEXT NOT NULL DEFAULT '[]',
10591
+ metadata TEXT NOT NULL DEFAULT '{}',
10592
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10593
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10594
+ )
10595
+ `);
10596
+ try {
10597
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_agent ON agent_memory(agent_id)");
10598
+ } catch {
10599
+ }
10600
+ try {
10601
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_category ON agent_memory(category)");
10602
+ } catch {
10603
+ }
10604
+ this.initialized = true;
10605
+ }
10606
+ /** Run a write statement, swallowing errors with a log (memory must never crash a caller). */
10607
+ dbRun(sql, params) {
10608
+ try {
10609
+ this.db.prepare(sql).run(...params);
10610
+ } catch (err) {
10611
+ console.error("[agent-memory] DB write failed:", err.message);
10612
+ }
10613
+ }
10614
+ dbAll(sql, params = []) {
10615
+ try {
10616
+ return this.db.prepare(sql).all(...params);
10617
+ } catch (err) {
10618
+ console.error("[agent-memory] DB read failed:", err.message);
10619
+ return [];
10620
+ }
10621
+ }
10622
+ loadFromDb() {
10623
+ const rows = this.dbAll("SELECT * FROM agent_memory");
10624
+ for (const r of rows) {
10625
+ try {
10626
+ const entry = this.rowToEntry(r);
10627
+ this.memories.set(entry.id, entry);
10628
+ this.indexAdd(entry.agentId, entry.id);
10629
+ this.searchIndex.addDocument(entry.id, entry);
10630
+ } catch {
10631
+ }
10632
+ }
10633
+ }
10634
+ /** Add a memory ID to the per-agent index. */
10635
+ indexAdd(agentId, memoryId) {
10636
+ let set = this.agentIndex.get(agentId);
10637
+ if (!set) {
10638
+ set = /* @__PURE__ */ new Set();
10639
+ this.agentIndex.set(agentId, set);
10640
+ }
10641
+ set.add(memoryId);
10642
+ }
10643
+ /** Remove a memory ID from the per-agent index. */
10644
+ indexRemove(agentId, memoryId) {
10645
+ const set = this.agentIndex.get(agentId);
10646
+ if (set) {
10647
+ set.delete(memoryId);
10648
+ if (set.size === 0) this.agentIndex.delete(agentId);
10649
+ }
10650
+ }
10651
+ /** Get all memory entries for an agent via the index. */
10652
+ getAgentMemories(agentId) {
10653
+ const ids = this.agentIndex.get(agentId);
10654
+ if (!ids || ids.size === 0) return [];
10655
+ const result = [];
10656
+ for (const id of ids) {
10657
+ const entry = this.memories.get(id);
10658
+ if (entry) result.push(entry);
10659
+ }
10660
+ return result;
10661
+ }
10662
+ // ─── Convenience Methods ─────────────────────────────
10663
+ /** Store a memory with minimal input — the common "just remember this" case. */
10664
+ async storeMemory(agentId, opts) {
10665
+ const category = opts.category && VALID_CATEGORIES.has(opts.category) ? opts.category : "context";
10666
+ const importance = opts.importance && VALID_IMPORTANCE.has(opts.importance) ? opts.importance : "normal";
10667
+ return this.createMemory({
10668
+ agentId,
10669
+ content: opts.content,
10670
+ category,
10671
+ importance,
10672
+ confidence: opts.confidence ?? 1,
10673
+ title: opts.title || opts.content.slice(0, 80),
10674
+ source: "system",
10675
+ tags: opts.tags ?? [],
10676
+ metadata: {}
10677
+ });
10678
+ }
10679
+ /** Search memories by text query, sorted by relevance. */
10680
+ async recall(agentId, query, limit = 5) {
10681
+ return this.queryMemories({ agentId, query, limit });
10682
+ }
10683
+ // ─── CRUD Operations ────────────────────────────────
10684
+ /** Create a new memory entry with auto-generated id + timestamps. */
10685
+ async createMemory(input) {
10686
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10687
+ const entry = {
10688
+ ...input,
10689
+ confidence: input.confidence ?? 0.8,
10690
+ tags: input.tags ?? [],
10691
+ metadata: input.metadata ?? {},
10692
+ id: (0, import_node_crypto6.randomUUID)(),
10693
+ accessCount: 0,
10694
+ createdAt: now,
10695
+ updatedAt: now
10696
+ };
10697
+ this.memories.set(entry.id, entry);
10698
+ this.indexAdd(entry.agentId, entry.id);
10699
+ this.searchIndex.addDocument(entry.id, entry);
10700
+ this.dbRun(
10701
+ `INSERT INTO agent_memory (id, agent_id, category, title, content, source, importance, confidence, access_count, last_accessed_at, expires_at, tags, metadata, created_at, updated_at)
10702
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
10703
+ [
10704
+ entry.id,
10705
+ entry.agentId,
10706
+ entry.category,
10707
+ entry.title,
10708
+ entry.content,
10709
+ entry.source,
10710
+ entry.importance,
10711
+ entry.confidence,
10712
+ entry.accessCount,
10713
+ entry.lastAccessedAt || null,
10714
+ entry.expiresAt || null,
10715
+ JSON.stringify(entry.tags),
10716
+ JSON.stringify(entry.metadata),
10717
+ entry.createdAt,
10718
+ entry.updatedAt
10719
+ ]
10720
+ );
10721
+ return entry;
10722
+ }
10723
+ /** Update an existing memory entry by merging provided fields. */
10724
+ async updateMemory(id, updates) {
10725
+ const existing = this.memories.get(id);
10726
+ if (!existing) return null;
10727
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10728
+ const updated = {
10729
+ ...existing,
10730
+ ...updates,
10731
+ id: existing.id,
10732
+ agentId: existing.agentId,
10733
+ createdAt: existing.createdAt,
10734
+ updatedAt: now
10735
+ };
10736
+ this.memories.set(id, updated);
10737
+ if (updates.title !== void 0 || updates.content !== void 0 || updates.tags !== void 0) {
10738
+ this.searchIndex.addDocument(id, updated);
10739
+ }
10740
+ this.dbRun(
10741
+ `UPDATE agent_memory SET
10742
+ category = ?, title = ?, content = ?, source = ?,
10743
+ importance = ?, confidence = ?, access_count = ?,
10744
+ last_accessed_at = ?, expires_at = ?, tags = ?,
10745
+ metadata = ?, updated_at = ?
10746
+ WHERE id = ?`,
10747
+ [
10748
+ updated.category,
10749
+ updated.title,
10750
+ updated.content,
10751
+ updated.source,
10752
+ updated.importance,
10753
+ updated.confidence,
10754
+ updated.accessCount,
10755
+ updated.lastAccessedAt || null,
10756
+ updated.expiresAt || null,
10757
+ JSON.stringify(updated.tags),
10758
+ JSON.stringify(updated.metadata),
10759
+ updated.updatedAt,
10760
+ id
10761
+ ]
10762
+ );
10763
+ return updated;
10764
+ }
10765
+ /** Delete a single memory entry. Returns true if it existed. */
10766
+ async deleteMemory(id) {
10767
+ const entry = this.memories.get(id);
10768
+ const existed = this.memories.delete(id);
10769
+ if (entry) this.indexRemove(entry.agentId, id);
10770
+ this.searchIndex.removeDocument(id);
10771
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [id]);
10772
+ return existed;
10773
+ }
10774
+ /**
10775
+ * Purge every memory entry belonging to an agent — Map, per-agent
10776
+ * index, search index, and the database row. Called when an agent is
10777
+ * deleted so no orphaned memory is left behind.
10778
+ * Returns the number of entries removed.
10779
+ */
10780
+ async deleteAgentMemories(agentId) {
10781
+ const ids = Array.from(this.agentIndex.get(agentId) ?? []);
10782
+ for (const id of ids) {
10783
+ this.memories.delete(id);
10784
+ this.searchIndex.removeDocument(id);
10785
+ }
10786
+ this.agentIndex.delete(agentId);
10787
+ this.dbRun("DELETE FROM agent_memory WHERE agent_id = ?", [agentId]);
10788
+ return ids.length;
10789
+ }
10790
+ /** Retrieve a single memory entry by id. */
10791
+ async getMemory(id) {
10792
+ return this.memories.get(id);
10793
+ }
10794
+ // ─── Query Operations ───────────────────────────────
10795
+ /** Query an agent's memory with optional category/importance/source filters + text search. */
10796
+ async queryMemories(opts) {
10797
+ let results = this.getAgentMemories(opts.agentId);
10798
+ if (opts.category) results = results.filter((m) => m.category === opts.category);
10799
+ if (opts.importance) results = results.filter((m) => m.importance === opts.importance);
10800
+ if (opts.source) results = results.filter((m) => m.source === opts.source);
10801
+ if (opts.query) {
10802
+ const candidateIds = new Set(results.map((m) => m.id));
10803
+ const searchResults = this.searchIndex.search(opts.query, candidateIds);
10804
+ if (searchResults.length > 0) {
10805
+ const scored = searchResults.map((r) => {
10806
+ const entry = this.memories.get(r.id);
10807
+ return entry ? { entry, score: r.score * IMPORTANCE_WEIGHT[entry.importance] } : null;
10808
+ }).filter((r) => r !== null);
10809
+ scored.sort((a, b) => b.score - a.score);
10810
+ return scored.slice(0, opts.limit || 100).map((d) => d.entry);
10811
+ }
10812
+ return [];
10813
+ }
10814
+ results.sort((a, b) => {
10815
+ const weightDiff = IMPORTANCE_WEIGHT[b.importance] - IMPORTANCE_WEIGHT[a.importance];
10816
+ if (weightDiff !== 0) return weightDiff;
10817
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
10818
+ });
10819
+ return results.slice(0, opts.limit || 100);
10820
+ }
10821
+ /** Memories created within the last N hours for an agent. */
10822
+ async getRecentMemories(agentId, hours = 24) {
10823
+ const cutoff = new Date(Date.now() - hours * 36e5).toISOString();
10824
+ return this.getAgentMemories(agentId).filter((m) => m.createdAt >= cutoff).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10825
+ }
10826
+ // ─── Access Tracking ────────────────────────────────
10827
+ /** Bump access count + lastAccessedAt for a memory entry. */
10828
+ async recordAccess(memoryId) {
10829
+ const entry = this.memories.get(memoryId);
10830
+ if (!entry) return;
10831
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10832
+ entry.accessCount += 1;
10833
+ entry.lastAccessedAt = now;
10834
+ entry.updatedAt = now;
10835
+ this.dbRun(
10836
+ "UPDATE agent_memory SET access_count = ?, last_accessed_at = ?, updated_at = ? WHERE id = ?",
10837
+ [entry.accessCount, entry.lastAccessedAt, entry.updatedAt, memoryId]
10838
+ );
10839
+ }
10840
+ // ─── Context Generation ─────────────────────────────
10841
+ /**
10842
+ * Render an agent's memory as a markdown block for prompt injection.
10843
+ * Ranks entries by confidence × access × recency × importance, with a
10844
+ * BM25F relevance boost when a query is supplied, groups by category,
10845
+ * and truncates to ~maxTokens (estimated at 4 chars/token).
10846
+ */
10847
+ async generateMemoryContext(agentId, query, maxTokens = 1500) {
10848
+ const entries = this.getAgentMemories(agentId).filter((m) => m.confidence >= 0.1);
10849
+ if (entries.length === 0) return "";
10850
+ const now = Date.now();
10851
+ let relevanceMap;
10852
+ if (query) {
10853
+ const candidateIds = new Set(entries.map((e) => e.id));
10854
+ const searchResults = this.searchIndex.search(query, candidateIds);
10855
+ if (searchResults.length > 0) {
10856
+ relevanceMap = /* @__PURE__ */ new Map();
10857
+ const maxScore = searchResults[0].score;
10858
+ for (const r of searchResults) {
10859
+ relevanceMap.set(r.id, maxScore > 0 ? r.score / maxScore : 0);
10860
+ }
10861
+ }
10862
+ }
10863
+ const scored = entries.map((entry) => {
10864
+ const accessWeight = 1 + Math.log1p(entry.accessCount) * 0.3;
10865
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10866
+ const ageHours = Math.max(1, (now - new Date(lastTouch).getTime()) / 36e5);
10867
+ const recencyWeight = 1 / (1 + Math.log1p(ageHours / 24) * 0.2);
10868
+ let score = entry.confidence * accessWeight * recencyWeight;
10869
+ score *= IMPORTANCE_WEIGHT[entry.importance];
10870
+ if (relevanceMap) {
10871
+ const relevance = relevanceMap.get(entry.id) || 0;
10872
+ if (relevance > 0) score *= 1 + relevance * 3;
10873
+ }
10874
+ return { entry, score };
10875
+ });
10876
+ scored.sort((a, b) => b.score - a.score);
10877
+ const grouped = /* @__PURE__ */ new Map();
10878
+ for (const { entry } of scored) {
10879
+ const group = grouped.get(entry.category) || [];
10880
+ group.push(entry);
10881
+ grouped.set(entry.category, group);
10882
+ }
10883
+ const maxChars = maxTokens * 4;
10884
+ const lines = ["## Agent Memory", ""];
10885
+ let charCount = lines.join("\n").length;
10886
+ for (const [category, categoryEntries] of Array.from(grouped.entries())) {
10887
+ const meta = MEMORY_CATEGORIES[category];
10888
+ if (!meta) continue;
10889
+ const header2 = `### ${meta.label}`;
10890
+ if (charCount + header2.length + 2 > maxChars) break;
10891
+ lines.push(header2, "");
10892
+ charCount += header2.length + 2;
10893
+ for (const entry of categoryEntries) {
10894
+ const badge = entry.importance === "critical" ? "[CRITICAL] " : entry.importance === "high" ? "[HIGH] " : "";
10895
+ const entryLine = `- **${badge}${entry.title}**: ${entry.content}`;
10896
+ if (charCount + entryLine.length + 1 > maxChars) break;
10897
+ lines.push(entryLine);
10898
+ charCount += entryLine.length + 1;
10899
+ }
10900
+ lines.push("");
10901
+ charCount += 1;
10902
+ }
10903
+ return lines.join("\n").trim();
10904
+ }
10905
+ // ─── Memory Lifecycle ───────────────────────────────
10906
+ /** Decay confidence for entries unaccessed for 7+ days. Critical entries are exempt. */
10907
+ async decayConfidence(agentId, decayRate = 0.05) {
10908
+ const cutoff = new Date(Date.now() - 7 * 864e5).toISOString();
10909
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10910
+ let decayed = 0;
10911
+ for (const entry of this.getAgentMemories(agentId)) {
10912
+ if (entry.importance === "critical") continue;
10913
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10914
+ if (lastTouch >= cutoff) continue;
10915
+ const newConfidence = Math.max(0, entry.confidence - decayRate);
10916
+ if (newConfidence === entry.confidence) continue;
10917
+ entry.confidence = parseFloat(newConfidence.toFixed(4));
10918
+ entry.updatedAt = now;
10919
+ this.dbRun(
10920
+ "UPDATE agent_memory SET confidence = ?, updated_at = ? WHERE id = ?",
10921
+ [entry.confidence, now, entry.id]
10922
+ );
10923
+ decayed += 1;
10924
+ }
10925
+ return decayed;
10926
+ }
10927
+ /** Prune entries with confidence < 0.1 or past their expiresAt. */
10928
+ async pruneExpired(agentId) {
10929
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10930
+ const toDelete = [];
10931
+ const entries = agentId ? this.getAgentMemories(agentId) : Array.from(this.memories.values());
10932
+ for (const entry of entries) {
10933
+ const isLowConfidence = entry.confidence < 0.1;
10934
+ const isExpired = !!entry.expiresAt && entry.expiresAt <= now;
10935
+ if (isLowConfidence || isExpired) toDelete.push({ id: entry.id, agentId: entry.agentId });
10936
+ }
10937
+ for (const item of toDelete) {
10938
+ this.memories.delete(item.id);
10939
+ this.indexRemove(item.agentId, item.id);
10940
+ this.searchIndex.removeDocument(item.id);
10941
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [item.id]);
10942
+ }
10943
+ return toDelete.length;
10944
+ }
10945
+ // ─── Statistics ─────────────────────────────────────
10946
+ /** Aggregate statistics for a specific agent's memory. */
10947
+ async getStats(agentId) {
10948
+ return this.computeStats(this.getAgentMemories(agentId));
10949
+ }
10950
+ computeStats(entries) {
10951
+ const byCategory = {};
10952
+ const byImportance = {};
10953
+ const bySource = {};
10954
+ let totalConfidence = 0;
10955
+ for (const entry of entries) {
10956
+ byCategory[entry.category] = (byCategory[entry.category] || 0) + 1;
10957
+ byImportance[entry.importance] = (byImportance[entry.importance] || 0) + 1;
10958
+ bySource[entry.source] = (bySource[entry.source] || 0) + 1;
10959
+ totalConfidence += entry.confidence;
10960
+ }
10961
+ return {
10962
+ totalEntries: entries.length,
10963
+ byCategory,
10964
+ byImportance,
10965
+ bySource,
10966
+ avgConfidence: entries.length > 0 ? parseFloat((totalConfidence / entries.length).toFixed(4)) : 0
10967
+ };
10968
+ }
10969
+ // ─── Row Mapper ─────────────────────────────────────
10970
+ rowToEntry(row) {
10971
+ return {
10972
+ id: row.id,
10973
+ agentId: row.agent_id,
10974
+ category: row.category,
10975
+ title: row.title,
10976
+ content: row.content,
10977
+ source: row.source,
10978
+ importance: row.importance,
10979
+ confidence: row.confidence,
10980
+ accessCount: row.access_count || 0,
10981
+ lastAccessedAt: row.last_accessed_at || void 0,
10982
+ expiresAt: row.expires_at || void 0,
10983
+ tags: Array.isArray(sj(row.tags)) ? sj(row.tags) : [],
10984
+ metadata: sj(row.metadata || "{}"),
10985
+ createdAt: row.created_at,
10986
+ updatedAt: row.updated_at
10987
+ };
10988
+ }
10989
+ };
10102
10990
  // Annotate the CommonJS export names for ESM import in node:
10103
10991
  0 && (module.exports = {
10104
10992
  AGENT_ROLES,
10105
10993
  AccountManager,
10106
10994
  AgentDeletionService,
10995
+ AgentMemoryManager,
10107
10996
  AgentMemoryStore,
10108
10997
  AgenticMailClient,
10109
10998
  BRIDGE_OPERATOR_LIVE_WINDOW_MS,
@@ -10120,8 +11009,10 @@ function parse(raw) {
10120
11009
  EmailSearchIndex,
10121
11010
  GatewayManager,
10122
11011
  InboxWatcher,
11012
+ MEMORY_CATEGORIES,
10123
11013
  MailReceiver,
10124
11014
  MailSender,
11015
+ MemorySearchIndex,
10125
11016
  PHONE_MAX_CONCURRENT_MISSIONS,
10126
11017
  PHONE_MIN_WEBHOOK_SECRET_LENGTH,
10127
11018
  PHONE_MISSION_STATES,
@@ -10212,7 +11103,9 @@ function parse(raw) {
10212
11103
  setTelemetryVersion,
10213
11104
  shouldSkipBridgeWakeForLiveOperator,
10214
11105
  startRelayBridge,
11106
+ stem,
10215
11107
  threadIdFor,
11108
+ tokenize,
10216
11109
  tryJoin,
10217
11110
  validateApiUrl,
10218
11111
  validatePhoneMissionPolicy,