@anuma/sdk 6.21.0 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ import {
11
11
  ENTITY_FANOUT,
12
12
  ENTITY_KINDS,
13
13
  EncryptionKeyMissingError,
14
+ ExtractionJob,
14
15
  LRUCache,
15
16
  MAX_HOPS,
16
17
  NODE_BUDGET,
@@ -52,6 +53,7 @@ import {
52
53
  decryptJsonField,
53
54
  deleteAllVaultMemoriesForUserOp,
54
55
  deleteConversationOp,
56
+ deleteMessageOp,
55
57
  deleteVaultMemoryOp,
56
58
  eagerEmbedContent,
57
59
  embedAllMessages,
@@ -133,7 +135,7 @@ import {
133
135
  upsertMessageOp,
134
136
  useEncryption,
135
137
  vaultMemoryToStored
136
- } from "./chunk-CIMZZ7GQ.mjs";
138
+ } from "./chunk-6BULYLRY.mjs";
137
139
  import {
138
140
  PII_PATTERNS,
139
141
  PiiRedactor,
@@ -5802,7 +5804,8 @@ function classifyDecay(m, now, policy) {
5802
5804
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
5803
5805
  if (m.eventTimeEnd >= now) return "keep";
5804
5806
  }
5805
- if (now - m.updatedAt > ttlForType(m.factType, resolved)) return "archive";
5807
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
5808
+ if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
5806
5809
  return "keep";
5807
5810
  }
5808
5811
 
@@ -6707,6 +6710,7 @@ async function retain(content, ctx, options = {}) {
6707
6710
  const updated = await updateVaultMemoryOp(ctx.vaultCtx, targetId, {
6708
6711
  content: existing.content,
6709
6712
  proofCountIncrement: 1,
6713
+ observationSourceIds: options.sourceChunkIds,
6710
6714
  sourceChunkIds: mergedSourceIds,
6711
6715
  // resurrect encodes the decay gate: ACTIVE target → { preserveUpdatedAt:
6712
6716
  // true } (main's normal re-observation path — bump proof_count without
@@ -6950,6 +6954,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6950
6954
  const updated = await updateVaultMemoryOp(ctx.vaultCtx, decision.targetId, {
6951
6955
  content: existing.content,
6952
6956
  proofCountIncrement: 1,
6957
+ observationSourceIds: options.sourceChunkIds,
6953
6958
  sourceChunkIds: mergedSourceIds,
6954
6959
  // ACTIVE target → { preserveUpdatedAt: true } (main's normal re-observation
6955
6960
  // path); ARCHIVED (non-superseded, non-deleted) → { restore: true } and no
@@ -6999,6 +7004,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6999
7004
  const updated = await updateVaultMemoryOp(ctx.vaultCtx, decision.targetId, {
7000
7005
  content: decision.content,
7001
7006
  proofCountIncrement: 1,
7007
+ observationSourceIds: options.sourceChunkIds,
7002
7008
  sourceChunkIds: mergedSourceIds,
7003
7009
  embedding: JSON.stringify(newEmbedding),
7004
7010
  embeddingModel: consolidatedModel,
@@ -7635,7 +7641,7 @@ function createAutoExtractor(options) {
7635
7641
  onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
7636
7642
  }
7637
7643
  });
7638
- if (outcome !== "empty-after-retry") {
7644
+ if (outcome !== "empty-after-retry" && failedCount === 0) {
7639
7645
  const advancedTo = window[window.length - 1].id;
7640
7646
  stateFor(conversationId).watermark = advancedTo;
7641
7647
  persistCursor(conversationId, messages, advancedTo, contiguous);
@@ -7790,6 +7796,7 @@ function toDecayInput(c) {
7790
7796
  eventTimeEnd: c.eventTimeEnd,
7791
7797
  eventTimeKind: c.eventTimeKind,
7792
7798
  updatedAt: c.updatedAt,
7799
+ lastObservedAt: c.lastObservedAt,
7793
7800
  archivedAt: c.archivedAt,
7794
7801
  source: c.source,
7795
7802
  trustTier: c.trustTier
@@ -7871,7 +7878,8 @@ function createDecaySweeper(options) {
7871
7878
  for (const c of toArchive) {
7872
7879
  const ok = await archiveVaultMemoryOp(vaultCtx, c.uniqueId, {
7873
7880
  now,
7874
- expectedUpdatedAt: c.updatedAt
7881
+ expectedUpdatedAt: c.updatedAt,
7882
+ expectedLastObservedAt: c.lastObservedAt ?? null
7875
7883
  });
7876
7884
  if (ok) archived++;
7877
7885
  }
@@ -8116,6 +8124,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
8116
8124
  budget: defaultBudget,
8117
8125
  ...toolOptions?.minScore !== void 0 && { minScore: toolOptions.minScore },
8118
8126
  ...toolOptions?.scopes && { scopes: toolOptions.scopes },
8127
+ ...toolOptions?.memoryIds !== void 0 && { memoryIds: toolOptions.memoryIds },
8119
8128
  ...toolOptions?.folderId !== void 0 && { folderId: toolOptions.folderId },
8120
8129
  ...toolOptions?.excludeConversationId && {
8121
8130
  excludeConversationId: toolOptions.excludeConversationId
@@ -8520,13 +8529,25 @@ async function synthesizeProfile(ctx, options = {}) {
8520
8529
  watermark,
8521
8530
  options.reviewedMemoryIds
8522
8531
  );
8532
+ const memoriesById = new Map(memories.map((memory) => [memory.uniqueId, memory]));
8533
+ const fallbackPriors = /* @__PURE__ */ new Map();
8534
+ if (previous) {
8535
+ for (const section of previous.sections) {
8536
+ if (section.sourceMemoryIds.length > 0 && section.sourceMemoryIds.every((id) => {
8537
+ const memory = memoriesById.get(id);
8538
+ return memory && !memory.isDeleted && !memory.supersededBy && scopes.includes(memory.scope) && (options.reviewedMemoryIds === void 0 || options.reviewedMemoryIds.includes(id)) && changeTime(memory) <= previous.vaultWatermark;
8539
+ })) {
8540
+ fallbackPriors.set(section.key, section);
8541
+ }
8542
+ }
8543
+ }
8523
8544
  const settled = await Promise.allSettled(
8524
8545
  facets.map(async (facet) => {
8525
8546
  const prior = previous?.sections.find((s) => s.key === facet.key);
8526
8547
  if (prior && !staleKeys.has(facet.key)) {
8527
8548
  return prior;
8528
8549
  }
8529
- return synthesizeFacet(facet, ctx, options, prior);
8550
+ return synthesizeFacet(facet, ctx, options, fallbackPriors.get(facet.key));
8530
8551
  })
8531
8552
  );
8532
8553
  const sections = settled.map((r, i) => {
@@ -8539,10 +8560,7 @@ async function synthesizeProfile(ctx, options = {}) {
8539
8560
  error: r.reason instanceof Error ? r.reason.message : String(r.reason)
8540
8561
  }
8541
8562
  );
8542
- return fallbackSection(
8543
- facet,
8544
- previous?.sections.find((s) => s.key === facet.key)
8545
- );
8563
+ return fallbackSection(facet, fallbackPriors.get(facet.key));
8546
8564
  });
8547
8565
  return {
8548
8566
  version: PROFILE_DOC_VERSION,
@@ -8730,7 +8748,7 @@ async function synthesizeFacet(facet, ctx, options, prior) {
8730
8748
  const { text: text12, legitimateEmpty } = extractFacetText(result.structuredOutput);
8731
8749
  if (!text12 && !legitimateEmpty && !noEvidence) {
8732
8750
  getLogger().warn(
8733
- "[memory/synthesizeProfile] facet synthesis returned degraded-empty; keeping prior section",
8751
+ "[memory/synthesizeProfile] facet synthesis returned degraded-empty; using fallback section",
8734
8752
  { facet: facet.key, recalledCount: result.basedOn.memoryIds.length }
8735
8753
  );
8736
8754
  return fallbackSection(facet, prior);
@@ -9296,6 +9314,462 @@ function createMessageSourceResolver(ctx) {
9296
9314
  };
9297
9315
  }
9298
9316
 
9317
+ // src/lib/memory/context.ts
9318
+ function shouldRecallMemory(query) {
9319
+ const normalized = query.trim().toLowerCase().replace(/[.!?。!?¡¿]+$/u, "").replace(/^[¡¿]+/u, "");
9320
+ return normalized.length > 0 && !/^(hi|hi there|hello|hey|yes|no|yep|nope|sure|k|ok|okay|thanks|thank you|thank you very much|thanks a lot|cool|great|got it|sounds good|hola|buenos días|buenas|sí|si|gracias|muchas gracias|vale|bonjour|salut|oui|non|merci|merci beaucoup|d'accord|hallo|guten tag|ja|nein|danke|danke schön|alles klar|olá|obrigado|obrigada|ciao|grazie|sì|你好|谢谢|好的|こんにちは|ありがとう|안녕하세요|감사합니다)$/u.test(
9321
+ normalized
9322
+ );
9323
+ }
9324
+ async function assembleMemoryContext(options) {
9325
+ const { query, recall: recall2, loadFacts, loadSessionRefs } = options;
9326
+ const memoryIds = options.memoryIds ?? options.recallOptions?.memoryIds;
9327
+ const ranked = shouldRecallMemory(query);
9328
+ const degraded = [];
9329
+ const guard = async (lane, run, fallback) => {
9330
+ try {
9331
+ return await run();
9332
+ } catch (error) {
9333
+ degraded.push(lane);
9334
+ try {
9335
+ options.onDegraded?.(lane, error);
9336
+ } catch {
9337
+ }
9338
+ return fallback;
9339
+ }
9340
+ };
9341
+ const scoped = memoryIds !== void 0 || options.recallOptions?.scopes !== void 0 || options.recallOptions?.folderId !== void 0;
9342
+ const allowed = memoryIds !== void 0 ? new Set(memoryIds) : void 0;
9343
+ const filters = {
9344
+ ...options.recallOptions?.scopes && { scopes: options.recallOptions.scopes },
9345
+ ...options.recallOptions?.folderId !== void 0 && {
9346
+ folderId: options.recallOptions.folderId
9347
+ }
9348
+ };
9349
+ const active = (m) => !m.isDeleted && !m.archivedAt && !m.supersededBy && m.trustTier !== "quarantined" && (!allowed || allowed.has(m.uniqueId));
9350
+ const asItem = (m, lane, score = 1) => ({
9351
+ id: m.uniqueId,
9352
+ content: m.content,
9353
+ score,
9354
+ lane,
9355
+ sourceIds: m.sourceChunkIds ?? []
9356
+ });
9357
+ const empty = {
9358
+ memories: [],
9359
+ usedBudget: options.recallOptions?.budget ?? "low",
9360
+ reranked: false,
9361
+ candidateCount: 0
9362
+ };
9363
+ const [facts, episodes, profile, session] = await Promise.all([
9364
+ ranked ? guard(
9365
+ "fact",
9366
+ () => recall2(query, { ...options.recallOptions, types: ["fact"], limit: 20, memoryIds }),
9367
+ empty
9368
+ ) : empty,
9369
+ ranked && !scoped && options.includeEpisodes !== false ? guard(
9370
+ "episode",
9371
+ () => recall2(query, {
9372
+ ...options.recallOptions,
9373
+ types: ["chunk"],
9374
+ limit: 4,
9375
+ minScore: 0.5,
9376
+ onDiagnostics: void 0
9377
+ }),
9378
+ empty
9379
+ ) : empty,
9380
+ loadFacts ? guard(
9381
+ "profile",
9382
+ async () => {
9383
+ const stable = await loadFacts({
9384
+ ...filters,
9385
+ memoryIds,
9386
+ factTypes: ["identity", "preference", "constraint", "relationship"],
9387
+ limit: 8
9388
+ });
9389
+ const dynamic = await loadFacts({
9390
+ ...filters,
9391
+ memoryIds,
9392
+ factTypes: ["ongoing_context", "plan"],
9393
+ limit: 4
9394
+ });
9395
+ return [...stable, ...dynamic].filter(active).map((m) => asItem(m, "profile"));
9396
+ },
9397
+ []
9398
+ ) : [],
9399
+ loadFacts && loadSessionRefs ? guard(
9400
+ "session",
9401
+ async () => {
9402
+ const refs = (await loadSessionRefs()).filter((r) => !allowed || allowed.has(r.memoryId)).sort((a, b) => b.score - a.score).slice(0, 10);
9403
+ if (!refs.length) return [];
9404
+ const rows = await loadFacts({ ...filters, memoryIds: refs.map((r) => r.memoryId) });
9405
+ const byId = new Map(rows.filter(active).map((m) => [m.uniqueId, m]));
9406
+ return refs.flatMap((ref) => {
9407
+ const m = byId.get(ref.memoryId);
9408
+ return m ? [asItem(m, "session", ref.score)] : [];
9409
+ });
9410
+ },
9411
+ []
9412
+ ) : []
9413
+ ]);
9414
+ const toItems = (result, lane) => result.memories.filter((m) => !allowed || allowed.has(m.id)).map((m) => ({
9415
+ id: m.id,
9416
+ content: m.content,
9417
+ score: m.score,
9418
+ lane,
9419
+ ...lane === "fact" && { recalled: true },
9420
+ sourceIds: m.sourceChunkIds ?? (m.messageId ? [m.messageId] : [])
9421
+ }));
9422
+ const recalledFacts = toItems(facts, "fact");
9423
+ const recalledScores = /* @__PURE__ */ new Map();
9424
+ const recalledContentScores = /* @__PURE__ */ new Map();
9425
+ for (const item of recalledFacts) {
9426
+ recalledScores.set(item.id, Math.max(recalledScores.get(item.id) ?? -Infinity, item.score));
9427
+ const content = item.content.trim();
9428
+ recalledContentScores.set(
9429
+ content,
9430
+ Math.max(recalledContentScores.get(content) ?? -Infinity, item.score)
9431
+ );
9432
+ }
9433
+ const rankedProfile = profile.map((item) => {
9434
+ const score = Math.max(
9435
+ recalledScores.get(item.id) ?? -Infinity,
9436
+ recalledContentScores.get(item.content.trim()) ?? -Infinity
9437
+ );
9438
+ return score === -Infinity ? item : { ...item, score, recalled: true };
9439
+ });
9440
+ const lanes = [
9441
+ [rankedProfile, 2e3],
9442
+ [recalledFacts, 6e3],
9443
+ [toItems(episodes, "episode"), 3e3],
9444
+ [session, 1e3]
9445
+ ];
9446
+ const items = [];
9447
+ const seenIds = /* @__PURE__ */ new Set();
9448
+ const seenContent = /* @__PURE__ */ new Set();
9449
+ let remaining = Math.max(0, options.maxChars ?? 12e3);
9450
+ let truncated = false;
9451
+ for (const [candidates, laneBudget] of lanes) {
9452
+ let laneRemaining = Math.min(laneBudget, remaining);
9453
+ for (const item of candidates) {
9454
+ const content = item.content?.trim();
9455
+ const key = `${item.lane === "episode" ? "episode" : "fact"}:${item.id}`;
9456
+ if (!content || seenIds.has(key) || seenContent.has(content)) continue;
9457
+ if (content.length > laneRemaining) {
9458
+ truncated = true;
9459
+ continue;
9460
+ }
9461
+ items.push({ ...item, content });
9462
+ seenIds.add(key);
9463
+ seenContent.add(content);
9464
+ remaining -= content.length;
9465
+ laneRemaining -= content.length;
9466
+ }
9467
+ }
9468
+ return { items, ranked, rankedCount: recalledFacts.length, degraded, truncated };
9469
+ }
9470
+
9471
+ // src/lib/memory/durableExtraction.ts
9472
+ import { Q as Q4 } from "@nozbe/watermelondb";
9473
+ var MAX_JOBS_PER_PASS = 3;
9474
+ var MAX_ATTEMPTS = 3;
9475
+ function idsOf(job) {
9476
+ return JSON.parse(String(job._getRaw("message_ids")));
9477
+ }
9478
+ function seqOf(job) {
9479
+ const raw = job._getRaw("watermark_seq");
9480
+ return typeof raw === "number" && raw > 0 ? raw : void 0;
9481
+ }
9482
+ function createDurableAutoExtractor(options) {
9483
+ const vault = options.retainCtx.vaultCtx;
9484
+ const database = vault.database;
9485
+ const owner = vault.userId ?? vault.walletAddress;
9486
+ if (!owner) throw new Error("Durable extraction requires a user or wallet owner");
9487
+ const jobs = database.get(ExtractionJob.table);
9488
+ const storage = {
9489
+ database,
9490
+ messagesCollection: database.get("history"),
9491
+ conversationsCollection: database.get("conversations"),
9492
+ walletAddress: vault.walletAddress,
9493
+ signMessage: vault.signMessage,
9494
+ embeddedWalletSigner: vault.embeddedWalletSigner
9495
+ };
9496
+ const folderId = options.folderId ?? null;
9497
+ let disposed = false;
9498
+ let running = false;
9499
+ let wakeRequested = false;
9500
+ let timer;
9501
+ let writes = Promise.resolve();
9502
+ let storeFailures = 0;
9503
+ const attempts = /* @__PURE__ */ new Map();
9504
+ const reportError = (error, conversationId) => {
9505
+ try {
9506
+ options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
9507
+ } catch {
9508
+ }
9509
+ };
9510
+ const currentScope = () => {
9511
+ try {
9512
+ const value = typeof options.scope === "function" ? options.scope() : options.scope;
9513
+ return value ?? "private";
9514
+ } catch (error) {
9515
+ reportError(error);
9516
+ return "private";
9517
+ }
9518
+ };
9519
+ function schedule(delay) {
9520
+ if (disposed) return;
9521
+ if (running || timer !== void 0) {
9522
+ if (running) wakeRequested = true;
9523
+ return;
9524
+ }
9525
+ timer = setTimeout(() => {
9526
+ timer = void 0;
9527
+ void drain();
9528
+ }, delay);
9529
+ }
9530
+ async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
9531
+ let watchdog;
9532
+ try {
9533
+ return await Promise.race([
9534
+ new Promise((resolve, reject) => {
9535
+ const worker = createAutoExtractor({
9536
+ ...options,
9537
+ retainCtx: {
9538
+ ...options.retainCtx,
9539
+ vaultCtx: {
9540
+ ...vault,
9541
+ canWrite: async () => !disposed && await storage.conversationsCollection.query(Q4.where("conversation_id", conversationId), Q4.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(Q4.where("conversation_id", conversationId), Q4.where("id", Q4.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
9542
+ }
9543
+ },
9544
+ // Never publish a private queued observation after a mode flip.
9545
+ scope: jobScope,
9546
+ folderId: jobFolderId,
9547
+ cursorStore: void 0,
9548
+ windowSize: 20,
9549
+ maxWindowSize: 20,
9550
+ onTurnComplete: resolve,
9551
+ onError: reject
9552
+ });
9553
+ worker.processTurn(messages, conversationId);
9554
+ worker.dispose();
9555
+ }),
9556
+ new Promise((_resolve, reject) => {
9557
+ watchdog = setTimeout(
9558
+ () => reject(new Error("Extraction did not settle in time; retained for retry")),
9559
+ options.batchTimeoutMs ?? 18e4
9560
+ );
9561
+ })
9562
+ ]);
9563
+ } finally {
9564
+ if (watchdog !== void 0) clearTimeout(watchdog);
9565
+ }
9566
+ }
9567
+ async function drain() {
9568
+ if (disposed || running) return;
9569
+ running = true;
9570
+ wakeRequested = false;
9571
+ let retry = false;
9572
+ let scheduleAfterSuccess = false;
9573
+ try {
9574
+ await writes;
9575
+ const pending = await jobs.query(Q4.where("owner_key", owner), Q4.where("message_ids", Q4.notEq("[]"))).fetch();
9576
+ storeFailures = 0;
9577
+ let processed = 0;
9578
+ for (const job of pending) {
9579
+ if (disposed) break;
9580
+ const attempted = attempts.get(job.id) ?? 0;
9581
+ if (attempted >= MAX_ATTEMPTS) continue;
9582
+ if (processed >= MAX_JOBS_PER_PASS) {
9583
+ scheduleAfterSuccess = true;
9584
+ break;
9585
+ }
9586
+ processed++;
9587
+ const conversationId = String(job._getRaw("conversation_id"));
9588
+ try {
9589
+ const conversations = await storage.conversationsCollection.query(Q4.where("conversation_id", conversationId), Q4.where("is_deleted", false)).fetchCount();
9590
+ if (!conversations) {
9591
+ await database.write(() => job.destroyPermanently());
9592
+ continue;
9593
+ }
9594
+ const ids = idsOf(job).slice(0, 20);
9595
+ const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
9596
+ const missing = ids.filter((_id, index) => loaded[index] === null);
9597
+ if (missing.length) {
9598
+ if (attempted < MAX_ATTEMPTS - 1)
9599
+ throw new Error("Source messages not yet available; retained for retry");
9600
+ const dropped = new Set(missing);
9601
+ await database.write(
9602
+ () => job.update(
9603
+ (r) => r._setRaw(
9604
+ "message_ids",
9605
+ JSON.stringify(idsOf(job).filter((id) => !dropped.has(id)))
9606
+ )
9607
+ )
9608
+ );
9609
+ attempts.delete(job.id);
9610
+ scheduleAfterSuccess = true;
9611
+ reportError(
9612
+ new Error(
9613
+ `Dropped ${missing.length} unresolvable source id(s) to unblock extraction`
9614
+ ),
9615
+ conversationId
9616
+ );
9617
+ continue;
9618
+ }
9619
+ if (loaded.some(
9620
+ (message) => message && (message.decryptionStatus || isEncrypted(message.content))
9621
+ )) {
9622
+ throw new Error("Source messages are locked; retained for retry");
9623
+ }
9624
+ const messages = loaded.flatMap(
9625
+ (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
9626
+ );
9627
+ let success = messages.length === 0;
9628
+ if (messages.length) {
9629
+ const result = await extractBatch(
9630
+ messages,
9631
+ conversationId,
9632
+ ids,
9633
+ currentScope() === "private" ? "private" : String(job._getRaw("scope")),
9634
+ job._getRaw("folder_id") ?? null
9635
+ );
9636
+ success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
9637
+ options.onTurnComplete?.(result);
9638
+ }
9639
+ if (!success) throw new Error("Extraction batch incomplete; retained for retry");
9640
+ attempts.delete(job.id);
9641
+ const sequences = /* @__PURE__ */ new Map();
9642
+ for (const message of loaded)
9643
+ if (message && message.messageId > 0)
9644
+ sequences.set(message.uniqueId, message.messageId);
9645
+ await database.write(async () => {
9646
+ const current = idsOf(job);
9647
+ const acknowledged = ids.filter((id) => current.includes(id));
9648
+ if (!acknowledged.length) return;
9649
+ const done = new Set(acknowledged);
9650
+ const remaining = current.filter((id) => !done.has(id));
9651
+ if (remaining.length) {
9652
+ await job.update(
9653
+ (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
9654
+ );
9655
+ } else await job.update((r) => r._setRaw("message_ids", "[]"));
9656
+ const newest = acknowledged[acknowledged.length - 1];
9657
+ await job.update((r) => {
9658
+ r._setRaw("watermark", newest);
9659
+ const sequence = sequences.get(newest);
9660
+ if (sequence !== void 0) {
9661
+ const prior = seqOf(job);
9662
+ r._setRaw(
9663
+ "watermark_seq",
9664
+ prior === void 0 ? sequence : Math.max(prior, sequence)
9665
+ );
9666
+ }
9667
+ });
9668
+ });
9669
+ scheduleAfterSuccess = true;
9670
+ } catch (error) {
9671
+ attempts.set(job.id, attempted + 1);
9672
+ retry = true;
9673
+ reportError(error, conversationId);
9674
+ }
9675
+ }
9676
+ } catch (error) {
9677
+ retry = ++storeFailures < 3;
9678
+ reportError(error);
9679
+ } finally {
9680
+ running = false;
9681
+ const woken = wakeRequested;
9682
+ wakeRequested = false;
9683
+ if (retry || scheduleAfterSuccess || woken)
9684
+ schedule(retry ? options.retryDelayMs ?? 3e4 : 0);
9685
+ }
9686
+ }
9687
+ schedule(0);
9688
+ return {
9689
+ processTurn(messages, conversationId) {
9690
+ if (disposed || !conversationId || !messages.length) return false;
9691
+ const ids = messages.map((m) => m.id);
9692
+ const scope = currentScope();
9693
+ writes = writes.then(
9694
+ () => database.write(async () => {
9695
+ const existing = await jobs.query(Q4.where("owner_key", owner), Q4.where("conversation_id", conversationId)).fetch();
9696
+ const job = existing.find(
9697
+ (candidate) => candidate._getRaw("scope") === scope && (candidate._getRaw("folder_id") ?? null) === folderId
9698
+ );
9699
+ const positions = new Map(ids.map((id, index) => [id, index]));
9700
+ const rows = await storage.messagesCollection.query(Q4.where("id", Q4.oneOf(ids))).fetch();
9701
+ const sequences = /* @__PURE__ */ new Map();
9702
+ for (const row of rows) {
9703
+ const sequence = Number(row._getRaw("message_id"));
9704
+ if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
9705
+ }
9706
+ let boundary = -1;
9707
+ let observedSeq = 0;
9708
+ const foreignSources = /* @__PURE__ */ new Set();
9709
+ for (const prior of existing) {
9710
+ const sources = [...idsOf(prior), String(prior._getRaw("watermark") ?? "")];
9711
+ for (const id of sources) {
9712
+ boundary = Math.max(boundary, positions.get(id) ?? -1);
9713
+ if (prior !== job) foreignSources.add(id);
9714
+ }
9715
+ observedSeq = Math.max(observedSeq, seqOf(prior) ?? 0);
9716
+ for (const id of idsOf(prior))
9717
+ observedSeq = Math.max(observedSeq, sequences.get(id) ?? 0);
9718
+ }
9719
+ if (observedSeq > 0)
9720
+ ids.forEach((id, index) => {
9721
+ const sequence = sequences.get(id);
9722
+ if (sequence !== void 0 && sequence <= observedSeq)
9723
+ boundary = Math.max(boundary, index);
9724
+ });
9725
+ if (!existing.length && options.cursorStore) {
9726
+ try {
9727
+ const cursor = options.cursorStore.get(conversationId);
9728
+ if (cursor) boundary = Math.max(boundary, positions.get(cursor) ?? -1);
9729
+ } catch (error) {
9730
+ reportError(error, conversationId);
9731
+ }
9732
+ }
9733
+ if (boundary < 0 && existing.some((prior) => idsOf(prior).length > 0 || seqOf(prior) !== void 0)) {
9734
+ reportError(
9735
+ new Error("Extraction boundary unresolved; skipped rather than re-observing"),
9736
+ conversationId
9737
+ );
9738
+ return;
9739
+ }
9740
+ const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
9741
+ if (start >= ids.length) return;
9742
+ const ownWatermark = job?._getRaw("watermark");
9743
+ const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
9744
+ const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
9745
+ const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
9746
+ if (!fresh.length) return;
9747
+ if (job) {
9748
+ await job.update(
9749
+ (r) => r._setRaw("message_ids", JSON.stringify([.../* @__PURE__ */ new Set([...idsOf(job), ...fresh])]))
9750
+ );
9751
+ attempts.delete(job.id);
9752
+ } else {
9753
+ await jobs.create((r) => {
9754
+ r._setRaw("owner_key", owner);
9755
+ r._setRaw("conversation_id", conversationId);
9756
+ r._setRaw("scope", scope);
9757
+ r._setRaw("folder_id", folderId);
9758
+ r._setRaw("message_ids", JSON.stringify([...new Set(fresh)]));
9759
+ });
9760
+ }
9761
+ })
9762
+ ).then(() => schedule(options.debounceMs ?? 2e4)).catch((error) => reportError(error, conversationId));
9763
+ return true;
9764
+ },
9765
+ isProcessing: () => running,
9766
+ dispose() {
9767
+ disposed = true;
9768
+ if (timer !== void 0) clearTimeout(timer);
9769
+ }
9770
+ };
9771
+ }
9772
+
9299
9773
  // src/lib/memoryEngine/quantization.ts
9300
9774
  var INT8_MAX = 127;
9301
9775
  function quantizeEmbedding(v) {
@@ -12697,10 +13171,25 @@ __decorateClass([
12697
13171
  ], VaultFolder.prototype, "context", 2);
12698
13172
 
12699
13173
  // src/lib/db/schema.ts
12700
- var SDK_SCHEMA_VERSION = 45;
13174
+ var SDK_SCHEMA_VERSION = 46;
12701
13175
  var sdkSchema = appSchema2({
12702
13176
  version: SDK_SCHEMA_VERSION,
12703
13177
  tables: [
13178
+ tableSchema2({
13179
+ name: "memory_extraction_jobs",
13180
+ columns: [
13181
+ { name: "owner_key", type: "string", isIndexed: true },
13182
+ { name: "conversation_id", type: "string", isIndexed: true },
13183
+ { name: "scope", type: "string" },
13184
+ { name: "message_ids", type: "string" },
13185
+ { name: "watermark", type: "string", isOptional: true },
13186
+ // Deletion-proof anchor: history.message_id of the newest acknowledged
13187
+ // source. `watermark` alone is an id, and deleting that message erased
13188
+ // the anchor — see the column note in extractionJobs/models.ts.
13189
+ { name: "watermark_seq", type: "number", isOptional: true },
13190
+ { name: "folder_id", type: "string", isOptional: true }
13191
+ ]
13192
+ }),
12704
13193
  // Chat storage tables
12705
13194
  tableSchema2({
12706
13195
  name: "history",
@@ -13705,10 +14194,28 @@ var sdkMigrations = schemaMigrations2({
13705
14194
  columns: [{ name: "media", type: "string", isOptional: true }]
13706
14195
  })
13707
14196
  ]
14197
+ },
14198
+ {
14199
+ toVersion: 46,
14200
+ steps: [
14201
+ createTable({
14202
+ name: "memory_extraction_jobs",
14203
+ columns: [
14204
+ { name: "owner_key", type: "string", isIndexed: true },
14205
+ { name: "conversation_id", type: "string", isIndexed: true },
14206
+ { name: "scope", type: "string" },
14207
+ { name: "message_ids", type: "string" },
14208
+ { name: "watermark", type: "string", isOptional: true },
14209
+ { name: "watermark_seq", type: "number", isOptional: true },
14210
+ { name: "folder_id", type: "string", isOptional: true }
14211
+ ]
14212
+ })
14213
+ ]
13708
14214
  }
13709
14215
  ]
13710
14216
  });
13711
14217
  var sdkModelClasses = [
14218
+ ExtractionJob,
13712
14219
  Message,
13713
14220
  Conversation,
13714
14221
  ConversationSummary,
@@ -13886,7 +14393,7 @@ var DatabaseManager = class {
13886
14393
  };
13887
14394
 
13888
14395
  // src/lib/db/conversationMemory/operations.ts
13889
- import { Q as Q4 } from "@nozbe/watermelondb";
14396
+ import { Q as Q5 } from "@nozbe/watermelondb";
13890
14397
  var MAX_PER_CONVERSATION = 200;
13891
14398
  function conversationMemoryToStored(row) {
13892
14399
  return {
@@ -13901,7 +14408,7 @@ async function addConversationMemoriesOp(ctx, conversationId, items) {
13901
14408
  if (!conversationId || items.length === 0) return;
13902
14409
  const now = Date.now();
13903
14410
  await ctx.database.write(async () => {
13904
- const existing = await ctx.conversationMemoryCollection.query(Q4.where("conversation_id", conversationId), Q4.sortBy("created_at", Q4.asc)).fetch();
14411
+ const existing = await ctx.conversationMemoryCollection.query(Q5.where("conversation_id", conversationId), Q5.sortBy("created_at", Q5.asc)).fetch();
13905
14412
  const seen = new Set(existing.map((r) => r.memoryId));
13906
14413
  const batchSeen = /* @__PURE__ */ new Set();
13907
14414
  const deduped = [];
@@ -13936,20 +14443,20 @@ async function addConversationMemoriesOp(ctx, conversationId, items) {
13936
14443
  }
13937
14444
  async function getConversationMemoriesOp(ctx, conversationId) {
13938
14445
  if (!conversationId) return [];
13939
- const rows = await ctx.conversationMemoryCollection.query(Q4.where("conversation_id", conversationId), Q4.sortBy("created_at", Q4.asc)).fetch();
14446
+ const rows = await ctx.conversationMemoryCollection.query(Q5.where("conversation_id", conversationId), Q5.sortBy("created_at", Q5.asc)).fetch();
13940
14447
  return rows.map(conversationMemoryToStored);
13941
14448
  }
13942
14449
  async function clearConversationMemoriesOp(ctx, conversationId) {
13943
14450
  if (!conversationId) return;
13944
14451
  await ctx.database.write(async () => {
13945
- const rows = await ctx.conversationMemoryCollection.query(Q4.where("conversation_id", conversationId)).fetch();
14452
+ const rows = await ctx.conversationMemoryCollection.query(Q5.where("conversation_id", conversationId)).fetch();
13946
14453
  if (rows.length === 0) return;
13947
14454
  await ctx.database.batch(...rows.map((r) => r.prepareDestroyPermanently()));
13948
14455
  });
13949
14456
  }
13950
14457
 
13951
14458
  // src/lib/db/vaultFolders/operations.ts
13952
- import { Q as Q5 } from "@nozbe/watermelondb";
14459
+ import { Q as Q6 } from "@nozbe/watermelondb";
13953
14460
  function folderToStored(folder) {
13954
14461
  return {
13955
14462
  uniqueId: folder.id,
@@ -13974,7 +14481,7 @@ async function createVaultFolderOp(ctx, opts) {
13974
14481
  return folderToStored(created);
13975
14482
  }
13976
14483
  async function getAllVaultFoldersOp(ctx) {
13977
- const results = await ctx.vaultFolderCollection.query(Q5.where("is_deleted", false), Q5.sortBy("created_at", Q5.desc)).fetch();
14484
+ const results = await ctx.vaultFolderCollection.query(Q6.where("is_deleted", false), Q6.sortBy("created_at", Q6.desc)).fetch();
13978
14485
  return results.map(folderToStored);
13979
14486
  }
13980
14487
  async function updateVaultFolderOp(ctx, id, opts) {
@@ -13983,7 +14490,7 @@ async function updateVaultFolderOp(ctx, id, opts) {
13983
14490
  if (record.isDeleted) return null;
13984
14491
  const scopeChanged = opts.scope !== void 0 && opts.scope !== record.scope;
13985
14492
  const updated = await ctx.database.write(async () => {
13986
- const memories = scopeChanged ? await ctx.vaultMemoryCollection.query(Q5.where("folder_id", id), Q5.where("is_deleted", false)).fetch() : [];
14493
+ const memories = scopeChanged ? await ctx.vaultMemoryCollection.query(Q6.where("folder_id", id), Q6.where("is_deleted", false)).fetch() : [];
13987
14494
  const updates = [];
13988
14495
  updates.push(
13989
14496
  record.prepareUpdate((r) => {
@@ -14011,7 +14518,7 @@ async function deleteVaultFolderOp(ctx, id) {
14011
14518
  const record = await ctx.vaultFolderCollection.find(id);
14012
14519
  if (record.isDeleted) return false;
14013
14520
  await ctx.database.write(async () => {
14014
- const memories = await ctx.vaultMemoryCollection.query(Q5.where("folder_id", id), Q5.where("is_deleted", false)).fetch();
14521
+ const memories = await ctx.vaultMemoryCollection.query(Q6.where("folder_id", id), Q6.where("is_deleted", false)).fetch();
14015
14522
  const preparedMemories = memories.map(
14016
14523
  (memory) => memory.prepareUpdate((r) => {
14017
14524
  r._setRaw("folder_id", null);
@@ -14080,7 +14587,7 @@ async function updateVaultFolderContextOp(ctx, id, context) {
14080
14587
  }
14081
14588
  }
14082
14589
  async function getVaultFolderMemoryCountOp(ctx, folderId) {
14083
- return await ctx.vaultMemoryCollection.query(Q5.where("folder_id", folderId), Q5.where("is_deleted", false)).fetchCount();
14590
+ return await ctx.vaultMemoryCollection.query(Q6.where("folder_id", folderId), Q6.where("is_deleted", false)).fetchCount();
14084
14591
  }
14085
14592
 
14086
14593
  // src/lib/db/vaultFolders/defaults.ts
@@ -15088,6 +15595,7 @@ export {
15088
15595
  WalletPoller,
15089
15596
  addConversationMemoriesOp,
15090
15597
  archiveVaultMemoryOp,
15598
+ assembleMemoryContext,
15091
15599
  backfillMemoryTopicsOp,
15092
15600
  buildNotionAuthUrl,
15093
15601
  capHopsForDensity,
@@ -15120,6 +15628,7 @@ export {
15120
15628
  createChunkVectorCache,
15121
15629
  createCryptoPricePreProcessor,
15122
15630
  createDecaySweeper,
15631
+ createDurableAutoExtractor,
15123
15632
  createLlmDecayClassifier,
15124
15633
  createLlmNeighborRefiner,
15125
15634
  createMemoryEngineTool,
@@ -15140,6 +15649,7 @@ export {
15140
15649
  decodeChunkVector,
15141
15650
  decryptConversationTitle,
15142
15651
  deleteAllVaultMemoriesForUserOp,
15652
+ deleteMessageOp,
15143
15653
  deleteVaultFolderOp,
15144
15654
  deleteVaultMemoryOp,
15145
15655
  dequantizeEmbedding,
@@ -15212,6 +15722,7 @@ export {
15212
15722
  setLogger,
15213
15723
  setMemoryVisibilityOp,
15214
15724
  shouldChunkMessage,
15725
+ shouldRecallMemory,
15215
15726
  stampTopicsExtractedAtOp,
15216
15727
  streamCancelPath,
15217
15728
  streamReplayPath,