@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.js CHANGED
@@ -1401,6 +1401,10 @@ var AccountManager = class {
1401
1401
  }
1402
1402
  const stmt = this.db.prepare("DELETE FROM agents WHERE id = ?");
1403
1403
  const result = stmt.run(id);
1404
+ try {
1405
+ this.db.prepare("DELETE FROM agent_memory WHERE agent_id = ?").run(id);
1406
+ } catch {
1407
+ }
1404
1408
  return result.changes > 0;
1405
1409
  }
1406
1410
  /**
@@ -9289,10 +9293,890 @@ function parse(raw) {
9289
9293
  }
9290
9294
  return out;
9291
9295
  }
9296
+
9297
+ // src/memory/manager.ts
9298
+ import { randomUUID as randomUUID3 } from "crypto";
9299
+
9300
+ // src/memory/text-search.ts
9301
+ var BM25_K1 = 1.2;
9302
+ var BM25_B = 0.75;
9303
+ var FIELD_WEIGHT_TITLE = 3;
9304
+ var FIELD_WEIGHT_TAGS = 2;
9305
+ var FIELD_WEIGHT_CONTENT = 1;
9306
+ var PREFIX_MATCH_PENALTY = 0.7;
9307
+ var STOP_WORDS = /* @__PURE__ */ new Set([
9308
+ "a",
9309
+ "about",
9310
+ "above",
9311
+ "after",
9312
+ "again",
9313
+ "against",
9314
+ "all",
9315
+ "am",
9316
+ "an",
9317
+ "and",
9318
+ "any",
9319
+ "are",
9320
+ "as",
9321
+ "at",
9322
+ "be",
9323
+ "because",
9324
+ "been",
9325
+ "before",
9326
+ "being",
9327
+ "below",
9328
+ "between",
9329
+ "both",
9330
+ "but",
9331
+ "by",
9332
+ "can",
9333
+ "could",
9334
+ "did",
9335
+ "do",
9336
+ "does",
9337
+ "doing",
9338
+ "down",
9339
+ "during",
9340
+ "each",
9341
+ "either",
9342
+ "every",
9343
+ "few",
9344
+ "for",
9345
+ "from",
9346
+ "further",
9347
+ "get",
9348
+ "got",
9349
+ "had",
9350
+ "has",
9351
+ "have",
9352
+ "having",
9353
+ "he",
9354
+ "her",
9355
+ "here",
9356
+ "hers",
9357
+ "herself",
9358
+ "him",
9359
+ "himself",
9360
+ "his",
9361
+ "how",
9362
+ "i",
9363
+ "if",
9364
+ "in",
9365
+ "into",
9366
+ "is",
9367
+ "it",
9368
+ "its",
9369
+ "itself",
9370
+ "just",
9371
+ "may",
9372
+ "me",
9373
+ "might",
9374
+ "more",
9375
+ "most",
9376
+ "must",
9377
+ "my",
9378
+ "myself",
9379
+ "neither",
9380
+ "no",
9381
+ "nor",
9382
+ "not",
9383
+ "now",
9384
+ "of",
9385
+ "off",
9386
+ "on",
9387
+ "once",
9388
+ "only",
9389
+ "or",
9390
+ "other",
9391
+ "ought",
9392
+ "our",
9393
+ "ours",
9394
+ "ourselves",
9395
+ "out",
9396
+ "over",
9397
+ "own",
9398
+ "same",
9399
+ "shall",
9400
+ "she",
9401
+ "should",
9402
+ "so",
9403
+ "some",
9404
+ "such",
9405
+ "than",
9406
+ "that",
9407
+ "the",
9408
+ "their",
9409
+ "theirs",
9410
+ "them",
9411
+ "themselves",
9412
+ "then",
9413
+ "there",
9414
+ "these",
9415
+ "they",
9416
+ "this",
9417
+ "those",
9418
+ "through",
9419
+ "to",
9420
+ "too",
9421
+ "under",
9422
+ "until",
9423
+ "up",
9424
+ "us",
9425
+ "very",
9426
+ "was",
9427
+ "we",
9428
+ "were",
9429
+ "what",
9430
+ "when",
9431
+ "where",
9432
+ "which",
9433
+ "while",
9434
+ "who",
9435
+ "whom",
9436
+ "why",
9437
+ "will",
9438
+ "with",
9439
+ "would",
9440
+ "yet",
9441
+ "you",
9442
+ "your",
9443
+ "yours",
9444
+ "yourself",
9445
+ "yourselves"
9446
+ ]);
9447
+ var STEM_RULES = [
9448
+ // Step 1: plurals and past participles
9449
+ [/ies$/, "i", 3],
9450
+ // policies → polici,eries → eri
9451
+ [/sses$/, "ss", 4],
9452
+ // addresses → address
9453
+ [/([^s])s$/, "$1", 3],
9454
+ // items → item, but not "ss"
9455
+ [/eed$/, "ee", 4],
9456
+ // agreed → agree
9457
+ [/ed$/, "", 3],
9458
+ // configured → configur, but min length 3
9459
+ [/ing$/, "", 4],
9460
+ // running → runn → run (handled below)
9461
+ // Step 2: derivational suffixes
9462
+ [/ational$/, "ate", 6],
9463
+ // relational → relate
9464
+ [/tion$/, "t", 5],
9465
+ // adoption → adopt
9466
+ [/ness$/, "", 5],
9467
+ // awareness → aware
9468
+ [/ment$/, "", 5],
9469
+ // deployment → deploy
9470
+ [/able$/, "", 5],
9471
+ // configurable → configur
9472
+ [/ible$/, "", 5],
9473
+ // accessible → access
9474
+ [/ful$/, "", 5],
9475
+ // powerful → power
9476
+ [/ous$/, "", 5],
9477
+ // dangerous → danger
9478
+ [/ive$/, "", 5],
9479
+ // interactive → interact
9480
+ [/ize$/, "", 4],
9481
+ // normalize → normal
9482
+ [/ise$/, "", 4],
9483
+ // organise → organ
9484
+ [/ally$/, "", 5],
9485
+ // automatically → automat
9486
+ [/ly$/, "", 4],
9487
+ // quickly → quick
9488
+ [/er$/, "", 4]
9489
+ // handler → handl
9490
+ ];
9491
+ var DOUBLE_CONSONANT = /([^aeiou])\1$/;
9492
+ function stem(word) {
9493
+ if (word.length < 3) return word;
9494
+ let stemmed = word;
9495
+ for (const [pattern, replacement, minLen] of STEM_RULES) {
9496
+ if (stemmed.length >= minLen && pattern.test(stemmed)) {
9497
+ stemmed = stemmed.replace(pattern, replacement);
9498
+ break;
9499
+ }
9500
+ }
9501
+ if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
9502
+ stemmed = stemmed.slice(0, -1);
9503
+ }
9504
+ return stemmed;
9505
+ }
9506
+ function tokenize(text) {
9507
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
9508
+ }
9509
+ var MemorySearchIndex = class {
9510
+ /** Posting lists: stemmed term → Set of memory IDs containing it */
9511
+ postings = /* @__PURE__ */ new Map();
9512
+ /** Per-document metadata for BM25 scoring */
9513
+ docs = /* @__PURE__ */ new Map();
9514
+ /** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
9515
+ idf = /* @__PURE__ */ new Map();
9516
+ idfStale = true;
9517
+ /** 3-character prefix map for prefix matching: prefix → Set of full stems */
9518
+ prefixMap = /* @__PURE__ */ new Map();
9519
+ /** Total weighted document length (for computing average) */
9520
+ totalWeightedLen = 0;
9521
+ get docCount() {
9522
+ return this.docs.size;
9523
+ }
9524
+ get avgDocLen() {
9525
+ return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
9526
+ }
9527
+ /**
9528
+ * Index a memory entry. Extracts stems from title, content, and tags
9529
+ * with field-specific weighting and builds posting lists.
9530
+ */
9531
+ addDocument(id, entry) {
9532
+ if (this.docs.has(id)) this.removeDocument(id);
9533
+ const titleTokens = tokenize(entry.title);
9534
+ const contentTokens = tokenize(entry.content);
9535
+ const tagTokens = entry.tags.flatMap((t) => tokenize(t));
9536
+ const weightedTf = /* @__PURE__ */ new Map();
9537
+ for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
9538
+ for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
9539
+ for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
9540
+ const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
9541
+ const allStems = /* @__PURE__ */ new Set();
9542
+ for (const t of weightedTf.keys()) allStems.add(t);
9543
+ const stemSequence = [...titleTokens, ...contentTokens];
9544
+ const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
9545
+ this.docs.set(id, docRecord);
9546
+ this.totalWeightedLen += weightedLen;
9547
+ for (const term of allStems) {
9548
+ let posting = this.postings.get(term);
9549
+ if (!posting) {
9550
+ posting = /* @__PURE__ */ new Set();
9551
+ this.postings.set(term, posting);
9552
+ }
9553
+ posting.add(id);
9554
+ if (term.length >= 3) {
9555
+ const prefix = term.slice(0, 3);
9556
+ let prefixSet = this.prefixMap.get(prefix);
9557
+ if (!prefixSet) {
9558
+ prefixSet = /* @__PURE__ */ new Set();
9559
+ this.prefixMap.set(prefix, prefixSet);
9560
+ }
9561
+ prefixSet.add(term);
9562
+ }
9563
+ }
9564
+ this.idfStale = true;
9565
+ }
9566
+ /** Remove a document from the index. */
9567
+ removeDocument(id) {
9568
+ const doc = this.docs.get(id);
9569
+ if (!doc) return;
9570
+ this.totalWeightedLen -= doc.weightedLen;
9571
+ this.docs.delete(id);
9572
+ for (const term of doc.allStems) {
9573
+ const posting = this.postings.get(term);
9574
+ if (posting) {
9575
+ posting.delete(id);
9576
+ if (posting.size === 0) {
9577
+ this.postings.delete(term);
9578
+ if (term.length >= 3) {
9579
+ const prefixSet = this.prefixMap.get(term.slice(0, 3));
9580
+ if (prefixSet) {
9581
+ prefixSet.delete(term);
9582
+ if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
9583
+ }
9584
+ }
9585
+ }
9586
+ }
9587
+ }
9588
+ this.idfStale = true;
9589
+ }
9590
+ /** Recompute IDF values for all terms. Called lazily before search. */
9591
+ refreshIdf() {
9592
+ if (!this.idfStale) return;
9593
+ const N = this.docs.size;
9594
+ this.idf.clear();
9595
+ for (const [term, posting] of this.postings) {
9596
+ const df = posting.size;
9597
+ this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
9598
+ }
9599
+ this.idfStale = false;
9600
+ }
9601
+ /**
9602
+ * Expand query terms with prefix matches.
9603
+ * "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
9604
+ */
9605
+ expandQueryTerms(queryStems) {
9606
+ const expanded = /* @__PURE__ */ new Map();
9607
+ for (const qs of queryStems) {
9608
+ if (this.postings.has(qs)) {
9609
+ expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
9610
+ }
9611
+ if (qs.length >= 3) {
9612
+ const prefix = qs.slice(0, 3);
9613
+ const candidates = this.prefixMap.get(prefix);
9614
+ if (candidates) {
9615
+ for (const candidate of candidates) {
9616
+ if (candidate !== qs && candidate.startsWith(qs)) {
9617
+ expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
9618
+ }
9619
+ }
9620
+ }
9621
+ }
9622
+ }
9623
+ return expanded;
9624
+ }
9625
+ /**
9626
+ * Compute bigram proximity boost: if two query terms appear adjacent
9627
+ * in the document's stem sequence, boost the score.
9628
+ */
9629
+ bigramProximityBoost(docId, queryStems) {
9630
+ if (queryStems.length < 2) return 0;
9631
+ const doc = this.docs.get(docId);
9632
+ if (!doc || doc.stemSequence.length < 2) return 0;
9633
+ let boost = 0;
9634
+ const seq = doc.stemSequence;
9635
+ const querySet = new Set(queryStems);
9636
+ for (let i = 0; i < seq.length - 1; i++) {
9637
+ if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
9638
+ boost += 0.5;
9639
+ }
9640
+ }
9641
+ return Math.min(boost, 2);
9642
+ }
9643
+ /**
9644
+ * Search the index for documents matching a query.
9645
+ * Returns scored results sorted by BM25F relevance.
9646
+ *
9647
+ * @param query - Raw query string
9648
+ * @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
9649
+ * @returns Array of { id, score } sorted by descending score
9650
+ */
9651
+ search(query, candidateIds) {
9652
+ const queryStems = tokenize(query);
9653
+ if (queryStems.length === 0) return [];
9654
+ this.refreshIdf();
9655
+ const expandedTerms = this.expandQueryTerms(queryStems);
9656
+ if (expandedTerms.size === 0) return [];
9657
+ const avgDl = this.avgDocLen;
9658
+ const candidates = /* @__PURE__ */ new Set();
9659
+ for (const term of expandedTerms.keys()) {
9660
+ const posting = this.postings.get(term);
9661
+ if (posting) {
9662
+ for (const docId of posting) {
9663
+ if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
9664
+ }
9665
+ }
9666
+ }
9667
+ const results = [];
9668
+ for (const docId of candidates) {
9669
+ const doc = this.docs.get(docId);
9670
+ if (!doc) continue;
9671
+ let score = 0;
9672
+ for (const [term, weight] of expandedTerms) {
9673
+ const tf = doc.weightedTf.get(term) || 0;
9674
+ if (tf === 0) continue;
9675
+ const termIdf = this.idf.get(term) || 0;
9676
+ const numerator = tf * (BM25_K1 + 1);
9677
+ const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
9678
+ score += termIdf * (numerator / denominator) * weight;
9679
+ }
9680
+ score += this.bigramProximityBoost(docId, queryStems);
9681
+ if (score > 0) results.push({ id: docId, score });
9682
+ }
9683
+ results.sort((a, b) => b.score - a.score);
9684
+ return results;
9685
+ }
9686
+ /** Check if a document exists in the index. */
9687
+ has(id) {
9688
+ return this.docs.has(id);
9689
+ }
9690
+ };
9691
+
9692
+ // src/memory/manager.ts
9693
+ function sj(v, fb = {}) {
9694
+ if (!v) return fb;
9695
+ try {
9696
+ return JSON.parse(v);
9697
+ } catch {
9698
+ return fb;
9699
+ }
9700
+ }
9701
+ var MEMORY_CATEGORIES = {
9702
+ knowledge: {
9703
+ label: "Knowledge",
9704
+ description: "Facts, procedures, and reference information the agent has learned"
9705
+ },
9706
+ interaction_pattern: {
9707
+ label: "Interaction Patterns",
9708
+ description: "Learned patterns from past interactions"
9709
+ },
9710
+ preference: {
9711
+ label: "Preferences",
9712
+ description: "User and counterparty preferences"
9713
+ },
9714
+ correction: {
9715
+ label: "Corrections",
9716
+ description: "Corrections and feedback received"
9717
+ },
9718
+ skill: {
9719
+ label: "Skills",
9720
+ description: "Learned abilities and competencies"
9721
+ },
9722
+ context: {
9723
+ label: "Context",
9724
+ description: "Contextual information and background knowledge"
9725
+ },
9726
+ reflection: {
9727
+ label: "Reflections",
9728
+ description: "Self-reflective insights and learnings"
9729
+ },
9730
+ session_learning: {
9731
+ label: "Session Learnings",
9732
+ description: "Insights captured during conversation sessions"
9733
+ },
9734
+ system_notice: {
9735
+ label: "System Notices",
9736
+ description: "System-generated notifications about configuration changes"
9737
+ }
9738
+ };
9739
+ var VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
9740
+ var VALID_IMPORTANCE = /* @__PURE__ */ new Set(["critical", "high", "normal", "low"]);
9741
+ var IMPORTANCE_WEIGHT = {
9742
+ critical: 4,
9743
+ high: 3,
9744
+ normal: 2,
9745
+ low: 1
9746
+ };
9747
+ var AgentMemoryManager = class {
9748
+ constructor(db2) {
9749
+ this.db = db2;
9750
+ this.ensureTable();
9751
+ this.loadFromDb();
9752
+ }
9753
+ memories = /* @__PURE__ */ new Map();
9754
+ /** Per-agent index: agentId → Set of memory IDs for O(1) agent lookups */
9755
+ agentIndex = /* @__PURE__ */ new Map();
9756
+ /** Full-text search index (BM25F + stemming + inverted index) */
9757
+ searchIndex = new MemorySearchIndex();
9758
+ initialized = false;
9759
+ // ─── Database layer ─────────────────────────────────
9760
+ ensureTable() {
9761
+ if (this.initialized) return;
9762
+ this.db.exec(`
9763
+ CREATE TABLE IF NOT EXISTS agent_memory (
9764
+ id TEXT PRIMARY KEY,
9765
+ agent_id TEXT NOT NULL,
9766
+ category TEXT NOT NULL,
9767
+ title TEXT NOT NULL,
9768
+ content TEXT NOT NULL,
9769
+ source TEXT NOT NULL DEFAULT 'interaction',
9770
+ importance TEXT NOT NULL DEFAULT 'normal',
9771
+ confidence REAL NOT NULL DEFAULT 1.0,
9772
+ access_count INTEGER NOT NULL DEFAULT 0,
9773
+ last_accessed_at TEXT,
9774
+ expires_at TEXT,
9775
+ tags TEXT NOT NULL DEFAULT '[]',
9776
+ metadata TEXT NOT NULL DEFAULT '{}',
9777
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9778
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9779
+ )
9780
+ `);
9781
+ try {
9782
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_agent ON agent_memory(agent_id)");
9783
+ } catch {
9784
+ }
9785
+ try {
9786
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_category ON agent_memory(category)");
9787
+ } catch {
9788
+ }
9789
+ this.initialized = true;
9790
+ }
9791
+ /** Run a write statement, swallowing errors with a log (memory must never crash a caller). */
9792
+ dbRun(sql, params) {
9793
+ try {
9794
+ this.db.prepare(sql).run(...params);
9795
+ } catch (err) {
9796
+ console.error("[agent-memory] DB write failed:", err.message);
9797
+ }
9798
+ }
9799
+ dbAll(sql, params = []) {
9800
+ try {
9801
+ return this.db.prepare(sql).all(...params);
9802
+ } catch (err) {
9803
+ console.error("[agent-memory] DB read failed:", err.message);
9804
+ return [];
9805
+ }
9806
+ }
9807
+ loadFromDb() {
9808
+ const rows = this.dbAll("SELECT * FROM agent_memory");
9809
+ for (const r of rows) {
9810
+ try {
9811
+ const entry = this.rowToEntry(r);
9812
+ this.memories.set(entry.id, entry);
9813
+ this.indexAdd(entry.agentId, entry.id);
9814
+ this.searchIndex.addDocument(entry.id, entry);
9815
+ } catch {
9816
+ }
9817
+ }
9818
+ }
9819
+ /** Add a memory ID to the per-agent index. */
9820
+ indexAdd(agentId, memoryId) {
9821
+ let set = this.agentIndex.get(agentId);
9822
+ if (!set) {
9823
+ set = /* @__PURE__ */ new Set();
9824
+ this.agentIndex.set(agentId, set);
9825
+ }
9826
+ set.add(memoryId);
9827
+ }
9828
+ /** Remove a memory ID from the per-agent index. */
9829
+ indexRemove(agentId, memoryId) {
9830
+ const set = this.agentIndex.get(agentId);
9831
+ if (set) {
9832
+ set.delete(memoryId);
9833
+ if (set.size === 0) this.agentIndex.delete(agentId);
9834
+ }
9835
+ }
9836
+ /** Get all memory entries for an agent via the index. */
9837
+ getAgentMemories(agentId) {
9838
+ const ids = this.agentIndex.get(agentId);
9839
+ if (!ids || ids.size === 0) return [];
9840
+ const result = [];
9841
+ for (const id of ids) {
9842
+ const entry = this.memories.get(id);
9843
+ if (entry) result.push(entry);
9844
+ }
9845
+ return result;
9846
+ }
9847
+ // ─── Convenience Methods ─────────────────────────────
9848
+ /** Store a memory with minimal input — the common "just remember this" case. */
9849
+ async storeMemory(agentId, opts) {
9850
+ const category = opts.category && VALID_CATEGORIES.has(opts.category) ? opts.category : "context";
9851
+ const importance = opts.importance && VALID_IMPORTANCE.has(opts.importance) ? opts.importance : "normal";
9852
+ return this.createMemory({
9853
+ agentId,
9854
+ content: opts.content,
9855
+ category,
9856
+ importance,
9857
+ confidence: opts.confidence ?? 1,
9858
+ title: opts.title || opts.content.slice(0, 80),
9859
+ source: "system",
9860
+ tags: opts.tags ?? [],
9861
+ metadata: {}
9862
+ });
9863
+ }
9864
+ /** Search memories by text query, sorted by relevance. */
9865
+ async recall(agentId, query, limit = 5) {
9866
+ return this.queryMemories({ agentId, query, limit });
9867
+ }
9868
+ // ─── CRUD Operations ────────────────────────────────
9869
+ /** Create a new memory entry with auto-generated id + timestamps. */
9870
+ async createMemory(input) {
9871
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9872
+ const entry = {
9873
+ ...input,
9874
+ confidence: input.confidence ?? 0.8,
9875
+ tags: input.tags ?? [],
9876
+ metadata: input.metadata ?? {},
9877
+ id: randomUUID3(),
9878
+ accessCount: 0,
9879
+ createdAt: now,
9880
+ updatedAt: now
9881
+ };
9882
+ this.memories.set(entry.id, entry);
9883
+ this.indexAdd(entry.agentId, entry.id);
9884
+ this.searchIndex.addDocument(entry.id, entry);
9885
+ this.dbRun(
9886
+ `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)
9887
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
9888
+ [
9889
+ entry.id,
9890
+ entry.agentId,
9891
+ entry.category,
9892
+ entry.title,
9893
+ entry.content,
9894
+ entry.source,
9895
+ entry.importance,
9896
+ entry.confidence,
9897
+ entry.accessCount,
9898
+ entry.lastAccessedAt || null,
9899
+ entry.expiresAt || null,
9900
+ JSON.stringify(entry.tags),
9901
+ JSON.stringify(entry.metadata),
9902
+ entry.createdAt,
9903
+ entry.updatedAt
9904
+ ]
9905
+ );
9906
+ return entry;
9907
+ }
9908
+ /** Update an existing memory entry by merging provided fields. */
9909
+ async updateMemory(id, updates) {
9910
+ const existing = this.memories.get(id);
9911
+ if (!existing) return null;
9912
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9913
+ const updated = {
9914
+ ...existing,
9915
+ ...updates,
9916
+ id: existing.id,
9917
+ agentId: existing.agentId,
9918
+ createdAt: existing.createdAt,
9919
+ updatedAt: now
9920
+ };
9921
+ this.memories.set(id, updated);
9922
+ if (updates.title !== void 0 || updates.content !== void 0 || updates.tags !== void 0) {
9923
+ this.searchIndex.addDocument(id, updated);
9924
+ }
9925
+ this.dbRun(
9926
+ `UPDATE agent_memory SET
9927
+ category = ?, title = ?, content = ?, source = ?,
9928
+ importance = ?, confidence = ?, access_count = ?,
9929
+ last_accessed_at = ?, expires_at = ?, tags = ?,
9930
+ metadata = ?, updated_at = ?
9931
+ WHERE id = ?`,
9932
+ [
9933
+ updated.category,
9934
+ updated.title,
9935
+ updated.content,
9936
+ updated.source,
9937
+ updated.importance,
9938
+ updated.confidence,
9939
+ updated.accessCount,
9940
+ updated.lastAccessedAt || null,
9941
+ updated.expiresAt || null,
9942
+ JSON.stringify(updated.tags),
9943
+ JSON.stringify(updated.metadata),
9944
+ updated.updatedAt,
9945
+ id
9946
+ ]
9947
+ );
9948
+ return updated;
9949
+ }
9950
+ /** Delete a single memory entry. Returns true if it existed. */
9951
+ async deleteMemory(id) {
9952
+ const entry = this.memories.get(id);
9953
+ const existed = this.memories.delete(id);
9954
+ if (entry) this.indexRemove(entry.agentId, id);
9955
+ this.searchIndex.removeDocument(id);
9956
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [id]);
9957
+ return existed;
9958
+ }
9959
+ /**
9960
+ * Purge every memory entry belonging to an agent — Map, per-agent
9961
+ * index, search index, and the database row. Called when an agent is
9962
+ * deleted so no orphaned memory is left behind.
9963
+ * Returns the number of entries removed.
9964
+ */
9965
+ async deleteAgentMemories(agentId) {
9966
+ const ids = Array.from(this.agentIndex.get(agentId) ?? []);
9967
+ for (const id of ids) {
9968
+ this.memories.delete(id);
9969
+ this.searchIndex.removeDocument(id);
9970
+ }
9971
+ this.agentIndex.delete(agentId);
9972
+ this.dbRun("DELETE FROM agent_memory WHERE agent_id = ?", [agentId]);
9973
+ return ids.length;
9974
+ }
9975
+ /** Retrieve a single memory entry by id. */
9976
+ async getMemory(id) {
9977
+ return this.memories.get(id);
9978
+ }
9979
+ // ─── Query Operations ───────────────────────────────
9980
+ /** Query an agent's memory with optional category/importance/source filters + text search. */
9981
+ async queryMemories(opts) {
9982
+ let results = this.getAgentMemories(opts.agentId);
9983
+ if (opts.category) results = results.filter((m) => m.category === opts.category);
9984
+ if (opts.importance) results = results.filter((m) => m.importance === opts.importance);
9985
+ if (opts.source) results = results.filter((m) => m.source === opts.source);
9986
+ if (opts.query) {
9987
+ const candidateIds = new Set(results.map((m) => m.id));
9988
+ const searchResults = this.searchIndex.search(opts.query, candidateIds);
9989
+ if (searchResults.length > 0) {
9990
+ const scored = searchResults.map((r) => {
9991
+ const entry = this.memories.get(r.id);
9992
+ return entry ? { entry, score: r.score * IMPORTANCE_WEIGHT[entry.importance] } : null;
9993
+ }).filter((r) => r !== null);
9994
+ scored.sort((a, b) => b.score - a.score);
9995
+ return scored.slice(0, opts.limit || 100).map((d) => d.entry);
9996
+ }
9997
+ return [];
9998
+ }
9999
+ results.sort((a, b) => {
10000
+ const weightDiff = IMPORTANCE_WEIGHT[b.importance] - IMPORTANCE_WEIGHT[a.importance];
10001
+ if (weightDiff !== 0) return weightDiff;
10002
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
10003
+ });
10004
+ return results.slice(0, opts.limit || 100);
10005
+ }
10006
+ /** Memories created within the last N hours for an agent. */
10007
+ async getRecentMemories(agentId, hours = 24) {
10008
+ const cutoff = new Date(Date.now() - hours * 36e5).toISOString();
10009
+ return this.getAgentMemories(agentId).filter((m) => m.createdAt >= cutoff).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10010
+ }
10011
+ // ─── Access Tracking ────────────────────────────────
10012
+ /** Bump access count + lastAccessedAt for a memory entry. */
10013
+ async recordAccess(memoryId) {
10014
+ const entry = this.memories.get(memoryId);
10015
+ if (!entry) return;
10016
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10017
+ entry.accessCount += 1;
10018
+ entry.lastAccessedAt = now;
10019
+ entry.updatedAt = now;
10020
+ this.dbRun(
10021
+ "UPDATE agent_memory SET access_count = ?, last_accessed_at = ?, updated_at = ? WHERE id = ?",
10022
+ [entry.accessCount, entry.lastAccessedAt, entry.updatedAt, memoryId]
10023
+ );
10024
+ }
10025
+ // ─── Context Generation ─────────────────────────────
10026
+ /**
10027
+ * Render an agent's memory as a markdown block for prompt injection.
10028
+ * Ranks entries by confidence × access × recency × importance, with a
10029
+ * BM25F relevance boost when a query is supplied, groups by category,
10030
+ * and truncates to ~maxTokens (estimated at 4 chars/token).
10031
+ */
10032
+ async generateMemoryContext(agentId, query, maxTokens = 1500) {
10033
+ const entries = this.getAgentMemories(agentId).filter((m) => m.confidence >= 0.1);
10034
+ if (entries.length === 0) return "";
10035
+ const now = Date.now();
10036
+ let relevanceMap;
10037
+ if (query) {
10038
+ const candidateIds = new Set(entries.map((e) => e.id));
10039
+ const searchResults = this.searchIndex.search(query, candidateIds);
10040
+ if (searchResults.length > 0) {
10041
+ relevanceMap = /* @__PURE__ */ new Map();
10042
+ const maxScore = searchResults[0].score;
10043
+ for (const r of searchResults) {
10044
+ relevanceMap.set(r.id, maxScore > 0 ? r.score / maxScore : 0);
10045
+ }
10046
+ }
10047
+ }
10048
+ const scored = entries.map((entry) => {
10049
+ const accessWeight = 1 + Math.log1p(entry.accessCount) * 0.3;
10050
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10051
+ const ageHours = Math.max(1, (now - new Date(lastTouch).getTime()) / 36e5);
10052
+ const recencyWeight = 1 / (1 + Math.log1p(ageHours / 24) * 0.2);
10053
+ let score = entry.confidence * accessWeight * recencyWeight;
10054
+ score *= IMPORTANCE_WEIGHT[entry.importance];
10055
+ if (relevanceMap) {
10056
+ const relevance = relevanceMap.get(entry.id) || 0;
10057
+ if (relevance > 0) score *= 1 + relevance * 3;
10058
+ }
10059
+ return { entry, score };
10060
+ });
10061
+ scored.sort((a, b) => b.score - a.score);
10062
+ const grouped = /* @__PURE__ */ new Map();
10063
+ for (const { entry } of scored) {
10064
+ const group = grouped.get(entry.category) || [];
10065
+ group.push(entry);
10066
+ grouped.set(entry.category, group);
10067
+ }
10068
+ const maxChars = maxTokens * 4;
10069
+ const lines = ["## Agent Memory", ""];
10070
+ let charCount = lines.join("\n").length;
10071
+ for (const [category, categoryEntries] of Array.from(grouped.entries())) {
10072
+ const meta = MEMORY_CATEGORIES[category];
10073
+ if (!meta) continue;
10074
+ const header2 = `### ${meta.label}`;
10075
+ if (charCount + header2.length + 2 > maxChars) break;
10076
+ lines.push(header2, "");
10077
+ charCount += header2.length + 2;
10078
+ for (const entry of categoryEntries) {
10079
+ const badge = entry.importance === "critical" ? "[CRITICAL] " : entry.importance === "high" ? "[HIGH] " : "";
10080
+ const entryLine = `- **${badge}${entry.title}**: ${entry.content}`;
10081
+ if (charCount + entryLine.length + 1 > maxChars) break;
10082
+ lines.push(entryLine);
10083
+ charCount += entryLine.length + 1;
10084
+ }
10085
+ lines.push("");
10086
+ charCount += 1;
10087
+ }
10088
+ return lines.join("\n").trim();
10089
+ }
10090
+ // ─── Memory Lifecycle ───────────────────────────────
10091
+ /** Decay confidence for entries unaccessed for 7+ days. Critical entries are exempt. */
10092
+ async decayConfidence(agentId, decayRate = 0.05) {
10093
+ const cutoff = new Date(Date.now() - 7 * 864e5).toISOString();
10094
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10095
+ let decayed = 0;
10096
+ for (const entry of this.getAgentMemories(agentId)) {
10097
+ if (entry.importance === "critical") continue;
10098
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10099
+ if (lastTouch >= cutoff) continue;
10100
+ const newConfidence = Math.max(0, entry.confidence - decayRate);
10101
+ if (newConfidence === entry.confidence) continue;
10102
+ entry.confidence = parseFloat(newConfidence.toFixed(4));
10103
+ entry.updatedAt = now;
10104
+ this.dbRun(
10105
+ "UPDATE agent_memory SET confidence = ?, updated_at = ? WHERE id = ?",
10106
+ [entry.confidence, now, entry.id]
10107
+ );
10108
+ decayed += 1;
10109
+ }
10110
+ return decayed;
10111
+ }
10112
+ /** Prune entries with confidence < 0.1 or past their expiresAt. */
10113
+ async pruneExpired(agentId) {
10114
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10115
+ const toDelete = [];
10116
+ const entries = agentId ? this.getAgentMemories(agentId) : Array.from(this.memories.values());
10117
+ for (const entry of entries) {
10118
+ const isLowConfidence = entry.confidence < 0.1;
10119
+ const isExpired = !!entry.expiresAt && entry.expiresAt <= now;
10120
+ if (isLowConfidence || isExpired) toDelete.push({ id: entry.id, agentId: entry.agentId });
10121
+ }
10122
+ for (const item of toDelete) {
10123
+ this.memories.delete(item.id);
10124
+ this.indexRemove(item.agentId, item.id);
10125
+ this.searchIndex.removeDocument(item.id);
10126
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [item.id]);
10127
+ }
10128
+ return toDelete.length;
10129
+ }
10130
+ // ─── Statistics ─────────────────────────────────────
10131
+ /** Aggregate statistics for a specific agent's memory. */
10132
+ async getStats(agentId) {
10133
+ return this.computeStats(this.getAgentMemories(agentId));
10134
+ }
10135
+ computeStats(entries) {
10136
+ const byCategory = {};
10137
+ const byImportance = {};
10138
+ const bySource = {};
10139
+ let totalConfidence = 0;
10140
+ for (const entry of entries) {
10141
+ byCategory[entry.category] = (byCategory[entry.category] || 0) + 1;
10142
+ byImportance[entry.importance] = (byImportance[entry.importance] || 0) + 1;
10143
+ bySource[entry.source] = (bySource[entry.source] || 0) + 1;
10144
+ totalConfidence += entry.confidence;
10145
+ }
10146
+ return {
10147
+ totalEntries: entries.length,
10148
+ byCategory,
10149
+ byImportance,
10150
+ bySource,
10151
+ avgConfidence: entries.length > 0 ? parseFloat((totalConfidence / entries.length).toFixed(4)) : 0
10152
+ };
10153
+ }
10154
+ // ─── Row Mapper ─────────────────────────────────────
10155
+ rowToEntry(row) {
10156
+ return {
10157
+ id: row.id,
10158
+ agentId: row.agent_id,
10159
+ category: row.category,
10160
+ title: row.title,
10161
+ content: row.content,
10162
+ source: row.source,
10163
+ importance: row.importance,
10164
+ confidence: row.confidence,
10165
+ accessCount: row.access_count || 0,
10166
+ lastAccessedAt: row.last_accessed_at || void 0,
10167
+ expiresAt: row.expires_at || void 0,
10168
+ tags: Array.isArray(sj(row.tags)) ? sj(row.tags) : [],
10169
+ metadata: sj(row.metadata || "{}"),
10170
+ createdAt: row.created_at,
10171
+ updatedAt: row.updated_at
10172
+ };
10173
+ }
10174
+ };
9292
10175
  export {
9293
10176
  AGENT_ROLES,
9294
10177
  AccountManager,
9295
10178
  AgentDeletionService,
10179
+ AgentMemoryManager,
9296
10180
  AgentMemoryStore,
9297
10181
  AgenticMailClient,
9298
10182
  BRIDGE_OPERATOR_LIVE_WINDOW_MS,
@@ -9309,8 +10193,10 @@ export {
9309
10193
  EmailSearchIndex,
9310
10194
  GatewayManager,
9311
10195
  InboxWatcher,
10196
+ MEMORY_CATEGORIES,
9312
10197
  MailReceiver,
9313
10198
  MailSender,
10199
+ MemorySearchIndex,
9314
10200
  PHONE_MAX_CONCURRENT_MISSIONS,
9315
10201
  PHONE_MIN_WEBHOOK_SECRET_LENGTH,
9316
10202
  PHONE_MISSION_STATES,
@@ -9401,7 +10287,9 @@ export {
9401
10287
  setTelemetryVersion,
9402
10288
  shouldSkipBridgeWakeForLiveOperator,
9403
10289
  startRelayBridge,
10290
+ stem,
9404
10291
  threadIdFor,
10292
+ tokenize,
9405
10293
  tryJoin,
9406
10294
  validateApiUrl,
9407
10295
  validatePhoneMissionPolicy,