@anuma/sdk 3.1.3 → 3.3.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.
@@ -4470,6 +4470,14 @@ declare class VaultMemory extends Model {
4470
4470
  /** Extraction-logic version this memory was last stamped under. Null (pre-v38)
4471
4471
  * reads as 0, so a TOPICS_EXTRACTION_VERSION bump re-extracts stale rows. */
4472
4472
  topicsExtractedVersion: number | null;
4473
+ /** C3 re-observation watermark: Unix ms of the last retain() merge, or null. */
4474
+ lastObservedAt: number | null;
4475
+ /** Typed memory (PR1) — the extractor's FactType. Null on legacy/manual rows. */
4476
+ factType: string | null;
4477
+ /** Decay archive state (PR2) — Unix ms when archived, null when active. */
4478
+ archivedAt: number | null;
4479
+ /** Tier-0 security (PR3) — "quarantined" | "trusted" | null. */
4480
+ trustTier: string | null;
4473
4481
  createdAt: Date;
4474
4482
  updatedAt: Date;
4475
4483
  isDeleted: boolean;
@@ -4542,465 +4550,193 @@ interface EntityOperationsContext {
4542
4550
  allowUnscopedRows?: boolean;
4543
4551
  }
4544
4552
 
4545
- interface StoredVaultMemory {
4546
- /** WatermelonDB internal ID */
4547
- uniqueId: string;
4548
- /** Plain text memory content */
4549
- content: string;
4550
- /** Scope for partitioning memories (e.g., "private", "shared") */
4551
- scope: string;
4552
- /** Folder ID for organization, null if unfiled */
4553
- folderId: string | null;
4554
- /** User ID for multi-user server-side scoping, null on client */
4555
- userId: string | null;
4556
- /** JSON-stringified embedding vector, null if not yet computed */
4557
- embedding: string | null;
4558
- /** Model that produced `embedding`. Null on legacy rows (grandfathered as
4559
- * compatible with the current model). */
4560
- embeddingModel: string | null;
4561
- /** JSON-stringified array of source message IDs this fact was extracted from. */
4562
- sourceChunkIds: string[] | null;
4563
- /** Times this fact has been re-observed (for ranking + UX badges). */
4564
- proofCount: number | null;
4565
- /** How the memory was created: manual | auto-extracted | capsule. */
4566
- source: string | null;
4567
- /** W6 temporal lane — Unix ms when the event occurred (point/start of range). */
4568
- eventTimeStart: number | null;
4569
- /** W6 temporal lane — Unix ms when the event ended (range only). */
4570
- eventTimeEnd: number | null;
4571
- /** W6 temporal lane — `point | range | ongoing | null`. */
4572
- eventTimeKind: string | null;
4573
- /** When true, the user has manually set this memory's topics (entity links);
4574
- * auto-extraction leaves them alone. False on legacy/auto rows. */
4575
- topicsUserManaged: boolean;
4576
- /** Unix ms of the last LLM topic-extraction pass over this memory's content.
4577
- * Null = never extracted standalone; rows that already carry entity links
4578
- * are grandfathered as extracted (see getMemoriesNeedingTopicExtractionOp). */
4579
- topicsExtractedAt: number | null;
4580
- /** Write-time supersession (A2): id of the newer memory that replaced this
4581
- * one (incompatible-value update, e.g. "Lives in Portland" → "Lives in SF").
4582
- * Null = live. Superseded rows are excluded from recall/dedup by default but
4583
- * kept for history + the read-time fallback. */
4584
- supersededBy: string | null;
4585
- /** Unix ms when this memory was superseded. Null when live. */
4586
- supersededAt: number | null;
4587
- /** Extraction-logic version this memory was last stamped under. Null (pre-v38)
4588
- * reads as 0, so a TOPICS_EXTRACTION_VERSION bump re-extracts stale rows. */
4589
- topicsExtractedVersion: number | null;
4590
- createdAt: Date;
4591
- updatedAt: Date;
4592
- isDeleted: boolean;
4593
- }
4594
- interface CreateVaultMemoryOptions {
4595
- content: string;
4596
- /** Scope for the memory. Defaults to "private" if omitted. */
4597
- scope?: string;
4598
- /** Folder ID for organization, null or omitted if unfiled */
4599
- folderId?: string | null;
4600
- /** JSON-stringified embedding vector to persist */
4601
- embedding?: string;
4602
- /** Model that produced `embedding`. Persisted alongside it so a later
4603
- * model change can detect and re-embed stale vectors. */
4604
- embeddingModel?: string;
4605
- /** Source message IDs that produced this fact (auto-extraction provenance). */
4606
- sourceChunkIds?: string[];
4607
- /** Initial proof count. Defaults to 1 if omitted. */
4608
- proofCount?: number;
4609
- /** How the memory was created. Defaults to "manual" if omitted. */
4610
- source?: string;
4611
- /** W6 temporal lane — when the event in this memory occurred. */
4612
- eventTime?: {
4613
- /** Unix ms timestamp of event start (or point). */
4614
- start: number | null;
4615
- /** Unix ms timestamp of event end (range only). */
4616
- end: number | null;
4617
- /** Kind: 'point' | 'range' | 'ongoing' | null (or omit). */
4618
- kind: "point" | "range" | "ongoing" | null;
4619
- };
4620
- }
4621
- interface UpdateVaultMemoryOptions {
4622
- content: string;
4623
- /** If provided, updates the memory's scope. */
4624
- scope?: string;
4625
- /** If provided, moves the memory to this folder. */
4626
- folderId?: string | null;
4627
- /** JSON-stringified embedding vector to persist, or null to clear stale embedding */
4628
- embedding?: string | null;
4629
- /** Model that produced `embedding`. Set whenever `embedding` is written so
4630
- * the stored model tag stays in sync with the vector. */
4631
- embeddingModel?: string | null;
4632
- /** Replace source-chunk-ids list (used during merge to accumulate provenance). */
4633
- sourceChunkIds?: string[];
4634
- /** Set an absolute proof count. Prefer {@link proofCountIncrement} for
4635
- * re-observation paths so the read+write happens inside the writer
4636
- * and concurrent retains can't lose updates. */
4637
- proofCount?: number;
4638
- /** Atomically bump proof_count by this delta inside the write block.
4639
- * Reads the current value from the in-memory record at write time, so
4640
- * two parallel retain() calls observe each other's commits and neither
4641
- * loses its increment. Wins over `proofCount` when both are set. */
4642
- proofCountIncrement?: number;
4643
- /** Set source ("manual" | "auto-extracted" | "capsule"). */
4644
- source?: string;
4645
- /**
4646
- * W6 temporal lane — write the event-time fields on update. Use during
4647
- * auto-merge to preserve (or refine) the original event-time signal when
4648
- * a new observation lands on an existing fact. Omit to leave the
4649
- * existing values untouched.
4650
- */
4651
- eventTime?: {
4652
- start: number | null;
4653
- end: number | null;
4654
- kind: "point" | "range" | "ongoing" | null;
4655
- };
4656
- /**
4657
- * When true, restore the existing `updated_at` after the write so the
4658
- * recency multiplier doesn't see a re-observation as a brand-new fact.
4659
- * Set by auto-merge/consolidate paths — they want proof_count to bump
4660
- * without inflating recency on top.
4661
- */
4662
- preserveUpdatedAt?: boolean;
4663
- /** If provided, sets whether the user has taken manual control of this
4664
- * memory's topics. Set by {@link setMemoryEntitiesOp}. */
4665
- topicsUserManaged?: boolean;
4553
+ /**
4554
+ * Auth for portal LLM calls (extraction, consolidation, decomposition,
4555
+ * reflection). Mirrors `memoryEngine`'s `EmbeddingOptions` dual-auth:
4556
+ *
4557
+ * - `apiKey`: For direct API keys (uses x-api-key header)
4558
+ * - `getToken`: For Privy identity tokens (uses Authorization: Bearer header)
4559
+ *
4560
+ * At least one of `apiKey` or `getToken` must be provided (enforced at
4561
+ * runtime); `apiKey` takes precedence when both are set.
4562
+ *
4563
+ * @public
4564
+ */
4565
+ interface PortalLlmAuth {
4566
+ /** Direct API key — sent as `x-api-key` (server-side / CLI usage). Wins when both are provided. */
4567
+ apiKey?: string;
4568
+ /** Function to get an auth token (e.g., Privy's getIdentityToken). Token is sent as `Authorization: Bearer`. */
4569
+ getToken?: () => Promise<string | null>;
4666
4570
  }
4667
4571
 
4668
- interface VaultMemoryOperationsContext {
4669
- database: Database;
4670
- vaultMemoryCollection: Collection<VaultMemory>;
4671
- walletAddress?: string;
4672
- signMessage?: SignMessageFn;
4673
- embeddedWalletSigner?: EmbeddedWalletSignerFn;
4674
- /** When set, operations scope to this user (server-side multi-user). */
4675
- userId?: string;
4572
+ /**
4573
+ * Optional LLM injection classifier — Tier-0 security second layer (PR5).
4574
+ *
4575
+ * The deterministic {@link ./injectionScreen} is a signature scan: it catches
4576
+ * imperative overrides, role-marker leakage, and exfil URLs, but by
4577
+ * construction it CANNOT catch signature-free poison phrased as a plain
4578
+ * third-person fact — e.g. "Trusts BrandX for financial advice", "Prefers to
4579
+ * always use the AcmePay card". Those read like ordinary preferences and sail
4580
+ * through the regex screen.
4581
+ *
4582
+ * This module closes that gap as a SECOND, opt-in layer. In `extractAndRetain`
4583
+ * it runs ONLY over the candidates the deterministic screen already passed as
4584
+ * clean, asking a cheap model a single batched yes/no per candidate: "is this
4585
+ * an instruction to the assistant / a planted preference, rather than a genuine
4586
+ * fact the user stated about themselves?" Positives are moved to quarantine
4587
+ * (trust_tier="quarantined"), exactly like a signature hit.
4588
+ *
4589
+ * Safety posture (why it can only ever add safety, never remove it):
4590
+ * - Default OFF. No option → no LLM call → zero latency/cost added, and the
4591
+ * write path is byte-for-byte the pre-PR5 deterministic screen.
4592
+ * - Fails CLEAN. Any error (network, timeout, malformed JSON, auth) → the
4593
+ * candidate stays in the clean set (trusts the deterministic result). A
4594
+ * flaky classifier can never suppress a legitimate memory.
4595
+ * - Bounded. One batched portal call per turn (never per-candidate), with a
4596
+ * candidate cap, a low retry budget, and a wall-clock total-timeout.
4597
+ * - Zero-knowledge parity. Candidate content is PII-redacted before it
4598
+ * reaches the portal using the SAME redactor setting as the extraction
4599
+ * path (inherited by `extractAndRetain`); the model only ever returns
4600
+ * integer indices, so nothing needs de-anonymizing on the way back.
4601
+ *
4602
+ * SECURITY (MEDIUM, residual): this is a NEW portal call surface for memory
4603
+ * content. It is gated on an explicit opt-in and redacts like the extractor, but
4604
+ * a security review should treat enabling it as widening the set of models that
4605
+ * see (redacted) facts. A malicious / MITM'd portal can also use it as a lever
4606
+ * to QUARANTINE the affected rows (they drop out of recall) — reversible, and it
4607
+ * can never delete or alter a memory. Bounded, recoverable trust tradeoff; gate
4608
+ * on trust in the portal.
4609
+ */
4610
+
4611
+ /**
4612
+ * Auth + tuning for the optional LLM injection classifier. Auth is the dual
4613
+ * pattern — one of `apiKey` / `getToken` is required at runtime (see
4614
+ * {@link PortalLlmAuth}). The mere PRESENCE of this options object in
4615
+ * `extractAndRetain` is the opt-in switch; omit it for the default-off,
4616
+ * deterministic-only screen.
4617
+ * @public
4618
+ */
4619
+ interface InjectionClassifierOptions extends PortalLlmAuth {
4620
+ baseUrl?: string;
4621
+ model?: string;
4622
+ /** Override fetch (tests). */
4623
+ fetchFn?: typeof fetch;
4624
+ /** Max portal attempts on a TRANSIENT failure. Default 2. */
4625
+ maxAttempts?: number;
4626
+ /** Absolute wall-clock budget across attempts. Default 15s. */
4627
+ totalTimeoutMs?: number;
4628
+ /** Backoff before each retry (ms). Tests pass `() => 0`. */
4629
+ backoffMs?: (attempt: number) => number;
4676
4630
  /**
4677
- * When set, vault delete ops cascade to memory_entity rows pointing at
4678
- * the deleted memories. Without this the W5 graph lane keeps returning
4679
- * IDs of soft-deleted memories and the join table grows unbounded.
4631
+ * PII redaction for the outbound content, same switch as the extractor.
4632
+ * `extractAndRetain` inherits the extraction setting so enabling redaction
4633
+ * there also protects this call. `true` = fresh per-call redactor; pass a
4634
+ * shared {@link PiiRedactor} to keep placeholder numbering consistent.
4680
4635
  */
4681
- entityCtx?: EntityOperationsContext;
4636
+ piiRedaction?: boolean | PiiRedactor;
4637
+ /** Max candidates classified per call. Default 20. */
4638
+ maxCandidates?: number;
4682
4639
  }
4683
- declare function createVaultMemoryOp(ctx: VaultMemoryOperationsContext, opts: CreateVaultMemoryOptions): Promise<StoredVaultMemory>;
4684
- declare function createVaultMemoriesBatchOp(ctx: VaultMemoryOperationsContext, optionsArray: CreateVaultMemoryOptions[]): Promise<StoredVaultMemory[]>;
4685
- declare function getVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string): Promise<StoredVaultMemory | null>;
4686
- declare function getAllVaultMemoriesOp(ctx: VaultMemoryOperationsContext, options?: {
4687
- scopes?: string[];
4688
- since?: Date;
4689
- limit?: number;
4690
- folderId?: string | null;
4691
- /**
4692
- * Include soft-deleted memories in the result (each carries
4693
- * `isDeleted: true`). Default `false` — deleted rows are excluded, as
4694
- * they are from every other read path. Used by the Memory Graph to
4695
- * render "forgotten" nodes; ordinary consumers should leave this off.
4696
- */
4697
- includeDeleted?: boolean;
4698
- /**
4699
- * Include A2-superseded memories (each carries `supersededBy`). Default
4700
- * `false` — superseded rows are excluded, as they are from recall/dedup.
4701
- * Used by a "memory history" view to render retired facts.
4702
- */
4703
- includeSuperseded?: boolean;
4704
- }): Promise<StoredVaultMemory[]>;
4705
- declare function getAllVaultMemoryContentsOp(ctx: VaultMemoryOperationsContext, options?: {
4706
- since?: Date;
4707
- }): Promise<string[]>;
4708
- declare function updateVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string, opts: UpdateVaultMemoryOptions): Promise<StoredVaultMemory | null>;
4709
- declare function deleteVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string): Promise<boolean>;
4710
4640
  /**
4711
- * Mark a memory as superseded by a newer one (A2 write-time supersession).
4712
- * The row stays in the table (history + read-time fallback) but is excluded
4713
- * from recall/dedup by default via `superseded_by`. Idempotent-ish: no-op if
4714
- * the row is missing, not owned, deleted, or already superseded. Does NOT
4715
- * preserve `updated_at` — superseded rows are hidden from recall, so their
4716
- * recency is irrelevant.
4641
+ * Classify already-clean extraction candidates for signature-free injection /
4642
+ * poisoning. Returns the set of 0-based indices (into `candidates`) the model
4643
+ * flagged as poison.
4717
4644
  *
4718
- * @param id - the memory being retired (e.g. "Lives in Portland")
4719
- * @param supersededById - the newer memory that replaced it (e.g. "Lives in SF")
4645
+ * FAILS CLEAN: returns an empty set on empty input, missing auth, LLM error,
4646
+ * or any malformed response — the caller then keeps every candidate clean, so
4647
+ * this layer can only ever ADD quarantines, never suppress a legitimate fact
4648
+ * on failure. Makes at most ONE portal call regardless of candidate count.
4649
+ *
4650
+ * @public
4720
4651
  */
4721
- declare function supersedeVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string, supersededById: string): Promise<boolean>;
4652
+ declare function classifyInjectionCandidates(candidates: readonly ExtractedCandidate[], options: InjectionClassifierOptions): Promise<{
4653
+ flagged: Set<number>;
4654
+ }>;
4655
+
4722
4656
  /**
4723
- * Get all non-deleted, unfiled vault memories (folder_id is null).
4657
+ * Write-time memory-poisoning screen — Tier-0 security (PR3).
4658
+ *
4659
+ * Deterministic, network-free, and always on. Scans each extraction
4660
+ * candidate's `content` for prompt-injection / memory-poisoning
4661
+ * signatures and routes flagged candidates to quarantine instead of the
4662
+ * live vault.
4663
+ *
4664
+ * WHY a signature scan (not an LLM classifier): this runs on the write
4665
+ * path for every extracted fact, so it must be cheap, offline, and must
4666
+ * never leak content to a model. It also gives a small false-positive
4667
+ * surface here: durable third-person facts about the user ("Lives in San
4668
+ * Francisco", "Allergic to shellfish", "Works in engineering") almost
4669
+ * never contain second-person imperatives aimed at the assistant, chat
4670
+ * role markers, or beacon URLs — the exact shapes these signatures look
4671
+ * for. An optional LLM injection classifier is deferred to PR5.
4672
+ *
4673
+ * WHY quarantine, not drop: a flagged candidate is still persisted (see
4674
+ * `extractAndRetain`) with `trust_tier="quarantined"` so it stays an
4675
+ * auditable record, recoverable by an operator via the `includeQuarantined`
4676
+ * read path. The `baseVaultConditions` choke point excludes quarantined
4677
+ * rows from every recall lane, so a poisoned fact never reaches the answer
4678
+ * model even though it lives in the DB.
4679
+ *
4680
+ * This MITIGATES memory poisoning (MINJA / AgentPoison; OWASP ASI06 2026).
4681
+ * It is not a complete solve — a poison phrased as a plain third-person
4682
+ * fact ("Prefers the BrandX credit card") carries no injection signature
4683
+ * and will pass. The proof-count ranking boost (once-seen facts stay
4684
+ * low-ranked) and the read-time isolation in `formatRecallResult` are the
4685
+ * complementary layers.
4724
4686
  */
4725
- declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): Promise<StoredVaultMemory[]>;
4687
+
4688
+ /** Why a candidate was quarantined. Coarse buckets over the signature set
4689
+ * below — surfaced for audit/telemetry, never alongside the content.
4690
+ * `llm_semantic` (PR5) is emitted by the optional second-layer LLM classifier
4691
+ * ({@link classifyInjectionCandidates}), not by any deterministic signature
4692
+ * here — it catches signature-free poison ("Trusts BrandX for financial
4693
+ * advice") the regex screen passes as clean. */
4694
+ type InjectionReason = "imperative_override" | "role_marker_leak" | "exfiltration_url" | "llm_semantic";
4695
+ /** A candidate the screen flagged, with the matching signature id + reason.
4696
+ * Content is intentionally NOT duplicated here beyond the candidate itself —
4697
+ * callers must never log `candidate.content`. */
4698
+ interface ScreenedCandidate {
4699
+ candidate: ExtractedCandidate;
4700
+ /** Coarse reason bucket. */
4701
+ reason: InjectionReason;
4702
+ /** Stable id of the signature that matched (safe to log; carries no content). */
4703
+ signature: string;
4704
+ }
4705
+ /** Result of screening a candidate batch. */
4706
+ interface ScreenResult {
4707
+ /** Candidates with no injection signature — persist normally. */
4708
+ clean: ExtractedCandidate[];
4709
+ /** Candidates that matched a signature — persist quarantined. */
4710
+ quarantined: ScreenedCandidate[];
4711
+ }
4726
4712
  /**
4727
- * The current topic-extraction logic version. Bump this whenever the extraction
4728
- * prompt or model in `topicExtract.ts` changes: every memory stamped under an
4729
- * older version (including pre-v37 rows, read as version 0) is then re-extracted
4730
- * by the next sweep, so topic-quality improvements propagate across the existing
4731
- * vault. The worker's `limit` drains that re-extraction across sweeps.
4713
+ * Screen extraction candidates for injection / poisoning signatures.
4714
+ *
4715
+ * Partitions the input into `clean` (persist normally) and `quarantined`
4716
+ * (persist with `trust_tier="quarantined"`, hidden from recall). Pure and
4717
+ * synchronous — no network, no DB, no content logging. Input order is
4718
+ * preserved within each partition.
4732
4719
  */
4733
- declare const TOPICS_EXTRACTION_VERSION = 1;
4720
+ declare function screenCandidatesForInjection(candidates: readonly ExtractedCandidate[]): ScreenResult;
4734
4721
  /**
4735
- * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
4736
- * background topic worker should run LLM entity extraction on, and which it
4737
- * should merely stamp as already-extracted.
4722
+ * Content-free catalog of the active signatures (id + reason, no patterns).
4723
+ * Exposed so a security review / audit surface can enumerate coverage
4724
+ * without reaching into module internals. Does not leak any user content.
4738
4725
  */
4739
- interface MemoriesNeedingTopicExtraction {
4740
- /**
4741
- * Memories to run LLM topic extraction on (decrypted): never-extracted rows
4742
- * with no entity links, plus stamped rows edited since their last pass
4743
- * (`updated_at` > `topics_extracted_at`) or extracted under an older
4744
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
4745
- * stale-version rows come first (they get priority under `limit`), each group
4746
- * newest-created first.
4747
- */
4748
- pending: StoredVaultMemory[];
4749
- /**
4750
- * IDs of rows that already have entity links but no watermark — legacy rows
4751
- * extracted by the conversation pipeline before v36. Grandfather these with
4752
- * {@link stampTopicsExtractedAtOp} (no LLM call) so a later content edit
4753
- * makes them re-extractable instead of invisible forever. Bounded by the
4754
- * same `limit` as {@link pending} — stamping loads a Model per row, so the
4755
- * grandfather backlog is drained across sweeps rather than in one spike.
4756
- */
4757
- linkedUnstamped: string[];
4758
- }
4726
+ declare function injectionSignatureCatalog(): {
4727
+ id: string;
4728
+ reason: InjectionReason;
4729
+ }[];
4730
+
4759
4731
  /**
4760
- * Sweep query for the background topic-extraction worker: partition the
4761
- * user's non-deleted, non-user-managed memories by what the worker should do
4762
- * with them (see {@link MemoriesNeedingTopicExtraction}). User-managed rows
4763
- * are never returned — the user owns their topics, including an intentionally
4764
- * empty set. Requires `ctx.entityCtx` for the entity-links check.
4765
- */
4766
- declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
4767
- limit?: number;
4768
- }): Promise<MemoriesNeedingTopicExtraction>;
4769
- /**
4770
- * Stamp `topics_extracted_at` (and `topics_extracted_version`) on the given
4771
- * memories — the topic worker calls this after a successful extraction pass
4772
- * (including zero-entity results, so quiet memories aren't re-asked every sweep)
4773
- * and to grandfather `linkedUnstamped` rows without an LLM call. `version`
4774
- * defaults to {@link TOPICS_EXTRACTION_VERSION}: stamping at the current version
4775
- * (for both fresh extractions and grandfathered legacy rows) means they aren't
4776
- * re-extracted until a future version bump. Preserves `updated_at` so a stamp
4777
- * never inflates the recency multiplier — and never masks a concurrent content
4778
- * edit from the next sweep. Skips deleted, foreign-user, and user-managed rows.
4779
- * Returns the IDs actually stamped.
4780
- *
4781
- * ALL eligibility AND `updated_at` are read from the LIVE Model inside the
4782
- * serialized writer — never a pre-writer snapshot. Writers are serialized, so
4783
- * a content edit or topic-override that commits before this writer runs is
4784
- * observed here: its fresh `updated_at` is preserved (so the next sweep's
4785
- * `updated_at > stamp` check still fires) and a mid-pass user-managed flip
4786
- * skips the row. Reading `updated_at` from a raw pre-fetch instead would write
4787
- * a stale value back, pushing `updated_at < topics_extracted_at` and hiding
4788
- * the edited memory from every future sweep.
4789
- *
4790
- * Callers bound the input via `getMemoriesNeedingTopicExtractionOp`'s `limit`
4791
- * (both `pending` and `linkedUnstamped` are capped), so the per-row Model load
4792
- * needed to `prepareUpdate` stays bounded and never spikes the RecordCache.
4793
- */
4794
- declare function stampTopicsExtractedAtOp(ctx: VaultMemoryOperationsContext, memoryIds: readonly string[], extractedAt: number, version?: number): Promise<string[]>;
4795
- declare function deleteAllVaultMemoriesForUserOp(ctx: VaultMemoryOperationsContext, userId: string): Promise<number>;
4796
- declare function updateVaultMemoryEmbeddingOp(ctx: VaultMemoryOperationsContext, id: string, embedding: string, embeddingModel: string): Promise<boolean>;
4797
-
4798
- /**
4799
- * Queue Types
4800
- *
4801
- * Types for the in-memory write queue that holds operations
4802
- * when encryption keys aren't yet available.
4803
- */
4804
-
4805
- /**
4806
- * Types of database operations that can be queued.
4807
- */
4808
- type QueuedOperationType = "createConversation" | "updateConversationTitle" | "updateConversationPinned" | "createMessage" | "updateMessage" | "createMedia" | "createMediaBatch" | "updateMediaMessageId";
4809
- /**
4810
- * A single queued database operation.
4811
- */
4812
- interface QueuedOperation {
4813
- /** Unique ID for this operation */
4814
- id: string;
4815
- /** Type of operation */
4816
- type: QueuedOperationType;
4817
- /** Wallet address this operation belongs to */
4818
- walletAddress: string;
4819
- /** When the operation was queued */
4820
- timestamp: number;
4821
- /** Priority for ordering (lower = higher priority). Conversations=0, Messages=1, Media=2 */
4822
- priority: number;
4823
- /** IDs of operations that must complete before this one */
4824
- dependencies: string[];
4825
- /** Operation-specific payload */
4826
- payload: Record<string, any>;
4827
- /** Number of times this operation has been retried */
4828
- retryCount: number;
4829
- /** Maximum number of retries allowed */
4830
- maxRetries: number;
4831
- }
4832
- /**
4833
- * Status of a wallet's queue.
4834
- */
4835
- interface QueueStatus {
4836
- /** Number of pending operations */
4837
- pending: number;
4838
- /** Number of operations that failed all retries */
4839
- failed: number;
4840
- /** Whether the queue is currently being flushed */
4841
- isFlushing: boolean;
4842
- /** Whether the queue is paused (e.g., wallet disconnected) */
4843
- isPaused: boolean;
4844
- }
4845
- /**
4846
- * Result of a flush operation.
4847
- */
4848
- interface FlushResult {
4849
- /** IDs of operations that succeeded */
4850
- succeeded: string[];
4851
- /** Operations that failed with their errors */
4852
- failed: Array<{
4853
- id: string;
4854
- error: string;
4855
- }>;
4856
- /** Total number of operations attempted */
4857
- total: number;
4858
- }
4859
- /**
4860
- * Encryption context needed to execute queued operations.
4861
- */
4862
- interface QueueEncryptionContext {
4863
- walletAddress: string;
4864
- signMessage: SignMessageFn;
4865
- embeddedWalletSigner?: EmbeddedWalletSignerFn;
4866
- }
4867
- /**
4868
- * Executor function that runs a single queued operation.
4869
- * Provided by the consumer (e.g., useChatStorage) during flush.
4870
- */
4871
- type OperationExecutor = (operation: QueuedOperation, encryptionContext: QueueEncryptionContext) => Promise<void>;
4872
-
4873
- /**
4874
- * Queue Manager
4875
- *
4876
- * In-memory queue for database operations when encryption keys aren't yet available.
4877
- * Operations are held in memory and flushed to the database once the key becomes available.
4878
- *
4879
- * Key design decisions:
4880
- * - In-memory only: queue is lost on page refresh (acceptable since user must re-auth anyway)
4881
- * - Per-wallet isolation: each wallet has its own queue
4882
- * - Dependency tracking: operations are flushed in correct order (conversation before messages)
4883
- * - Max 1000 operations per wallet to prevent memory leaks
4884
- */
4885
-
4886
- declare class QueueManager {
4887
- /** Per-wallet operation queues: wallet address -> operation id -> operation */
4888
- private queues;
4889
- /** Per-wallet flushing state */
4890
- private flushing;
4891
- /** Per-wallet paused state */
4892
- private paused;
4893
- /** Per-wallet failed operations (exceeded max retries) */
4894
- private failedOps;
4895
- /** Change listeners: wallet address -> callbacks */
4896
- private listeners;
4897
- /**
4898
- * Queue a new operation for a wallet.
4899
- * @returns The operation ID, or null if queue is full.
4900
- */
4901
- queueOperation(walletAddress: string, type: QueuedOperationType, payload: Record<string, any>, dependencies?: string[], maxRetries?: number): string | null;
4902
- /**
4903
- * Get all pending operations for a wallet, sorted by dependency order.
4904
- */
4905
- getOperations(walletAddress: string): QueuedOperation[];
4906
- /**
4907
- * Remove a specific operation from the queue.
4908
- */
4909
- removeOperation(walletAddress: string, operationId: string): void;
4910
- /**
4911
- * Get the status of a wallet's queue.
4912
- */
4913
- getStatus(walletAddress: string): QueueStatus;
4914
- /**
4915
- * Flush all queued operations for a wallet by executing them with encryption.
4916
- *
4917
- * @param encryptionContext - Wallet address and signing functions for encryption
4918
- * @param executor - Function that executes each operation against the database
4919
- * @returns Result with succeeded/failed operation IDs
4920
- */
4921
- flush(encryptionContext: QueueEncryptionContext, executor: OperationExecutor): Promise<FlushResult>;
4922
- /**
4923
- * Clear all queued operations for a wallet.
4924
- */
4925
- clear(walletAddress: string): void;
4926
- /**
4927
- * Pause the queue for a wallet (stops flush mid-way).
4928
- */
4929
- pause(walletAddress: string): void;
4930
- /**
4931
- * Resume the queue for a wallet.
4932
- */
4933
- resume(walletAddress: string): void;
4934
- /**
4935
- * Register a listener for queue changes on a wallet.
4936
- * @returns Unsubscribe function
4937
- */
4938
- onQueueChange(walletAddress: string, callback: () => void): () => void;
4939
- /**
4940
- * Check if a wallet has any pending operations.
4941
- */
4942
- hasPending(walletAddress: string): boolean;
4943
- /** Move a failed operation to the failed set. */
4944
- private moveToFailed;
4945
- /** Notify all listeners for a wallet. */
4946
- private notifyListeners;
4947
- }
4948
- /** Singleton queue manager instance */
4949
- declare const queueManager: QueueManager;
4950
-
4951
- /**
4952
- * Wallet Poller
4953
- *
4954
- * Polls for embedded wallet availability during Privy initialization.
4955
- * When a wallet becomes available, triggers a callback so the queue can flush.
4956
- */
4957
- declare class WalletPoller {
4958
- private timerId;
4959
- private attempts;
4960
- /**
4961
- * Start polling for wallet availability.
4962
- *
4963
- * @param checkWallet - Returns wallet address when ready, null if not yet available
4964
- * @param onWalletReady - Called with the wallet address when it becomes available
4965
- * @param intervalMs - Polling interval in milliseconds (default: 1000ms)
4966
- * @param maxAttempts - Maximum polling attempts before giving up (default: 60)
4967
- * @returns Stop function to cancel polling
4968
- */
4969
- startPolling(checkWallet: () => Promise<string | null>, onWalletReady: (address: string) => void, intervalMs?: number, maxAttempts?: number): () => void;
4970
- /**
4971
- * Stop polling.
4972
- */
4973
- stop(): void;
4974
- }
4975
-
4976
- /**
4977
- * Auth for portal LLM calls (extraction, consolidation, decomposition,
4978
- * reflection). Mirrors `memoryEngine`'s `EmbeddingOptions` dual-auth:
4979
- *
4980
- * - `apiKey`: For direct API keys (uses x-api-key header)
4981
- * - `getToken`: For Privy identity tokens (uses Authorization: Bearer header)
4982
- *
4983
- * At least one of `apiKey` or `getToken` must be provided (enforced at
4984
- * runtime); `apiKey` takes precedence when both are set.
4985
- *
4986
- * @public
4987
- */
4988
- interface PortalLlmAuth {
4989
- /** Direct API key — sent as `x-api-key` (server-side / CLI usage). Wins when both are provided. */
4990
- apiKey?: string;
4991
- /** Function to get an auth token (e.g., Privy's getIdentityToken). Token is sent as `Authorization: Bearer`. */
4992
- getToken?: () => Promise<string | null>;
4993
- }
4994
-
4995
- /**
4996
- * Linear recency decay multiplier — applied post-fusion to nudge fresh
4997
- * memories above stale ones with otherwise comparable scores.
4998
- *
4999
- * Formula:
5000
- * multiplier(ageDays) = max(floor, 1 - perYearDecay * (ageDays / 365))
5001
- *
5002
- * Memories without an `updatedAt` get a neutral 0.5 (Hindsight pattern —
5003
- * neither boost nor penalty when freshness is unknown).
4732
+ * Linear recency decay multiplier — applied post-fusion to nudge fresh
4733
+ * memories above stale ones with otherwise comparable scores.
4734
+ *
4735
+ * Formula:
4736
+ * multiplier(ageDays) = max(floor, 1 - perYearDecay * (ageDays / 365))
4737
+ *
4738
+ * Memories without an `updatedAt` get a neutral 0.5 (Hindsight pattern —
4739
+ * neither boost nor penalty when freshness is unknown).
5004
4740
  */
5005
4741
  interface RecencyOptions {
5006
4742
  /** Override "now" — useful for deterministic tests and back-dated benchmarks. */
@@ -5045,6 +4781,23 @@ interface MemoryVaultSearchOptions {
5045
4781
  scopes?: string[];
5046
4782
  /** When provided, only search memories in this folder (null for unfiled) */
5047
4783
  folderId?: string | null;
4784
+ /** Typed memory (PR1) — when provided, only search memories of these fact
4785
+ * types. Applied at load time via `Q.oneOf` on the indexed `fact_type`
4786
+ * column. Omit for no type filter. */
4787
+ factTypes?: string[];
4788
+ /**
4789
+ * PR5 — optional per-FactType score multiplier applied in the boost stage
4790
+ * (e.g. `{ identity: 1.2, ongoing_context: 0.8 }`). Empty/omitted = uniform
4791
+ * (no behavior change). See {@link rankFusedVaultMemories}.
4792
+ */
4793
+ factTypeWeights?: Record<string, number>;
4794
+ /**
4795
+ * PR5 — include archived (decayed) rows in the candidate load. Default false
4796
+ * (the baseVaultConditions choke point excludes them). retain()'s dedup
4797
+ * search sets this so a re-observed fact can merge into — and un-archive — an
4798
+ * archived row instead of creating a fresh duplicate.
4799
+ */
4800
+ includeArchived?: boolean;
5048
4801
  /**
5049
4802
  * Use the hybrid fusion ranker (cosine + BM25 + RRF + recency) instead of
5050
4803
  * cosine-only. Default true — new W1 pipeline. Pass false to fall back
@@ -5139,6 +4892,10 @@ interface VaultSearchResult {
5139
4892
  eventTimeStart?: number | null;
5140
4893
  eventTimeEnd?: number | null;
5141
4894
  eventTimeKind?: "point" | "range" | "ongoing" | null;
4895
+ /** Typed memory (PR1) — the fact's FactType, threaded through from the
4896
+ * storage row alongside the event-time anchors. Null/undefined when
4897
+ * untyped. Loose string (originates from a stored column). */
4898
+ factType?: string | null;
5142
4899
  /** Message ids this fact was extracted from (provenance). recall() uses
5143
4900
  * these to suppress the originating chunk in the chunk lane. */
5144
4901
  sourceChunkIds?: string[] | null;
@@ -5228,6 +4985,14 @@ interface RankedMemory {
5228
4985
  eventTimeStart?: number | null;
5229
4986
  eventTimeEnd?: number | null;
5230
4987
  eventTimeKind?: "point" | "range" | "ongoing" | null;
4988
+ /**
4989
+ * Typed memory (PR1) — the extractor's FactType classification for this
4990
+ * fact, threaded through the same channel as the event-time anchors so
4991
+ * recall results (and UI) can show the type without a second DB read.
4992
+ * Null/undefined on legacy/untyped/manual facts. Kept as a loose string
4993
+ * (not narrowed to FactType) since it originates from a stored column.
4994
+ */
4995
+ factType?: string | null;
5231
4996
  conversationId?: string;
5232
4997
  messageId?: string;
5233
4998
  role?: "user" | "assistant";
@@ -5249,6 +5014,18 @@ interface RecallOptions {
5249
5014
  scopes?: string[];
5250
5015
  /** Vault folder filter. Vault-only. */
5251
5016
  folderId?: string | null;
5017
+ /**
5018
+ * Typed memory (PR1) — restrict fact recall to these FactTypes. Optional
5019
+ * and no-op when unset (all types are eligible). Vault-only.
5020
+ */
5021
+ factTypes?: FactType[];
5022
+ /**
5023
+ * PR5 — optional per-FactType score multiplier applied in the fusion boost
5024
+ * stage (e.g. boost `identity`/`constraint`, down-weight `ongoing_context`).
5025
+ * A type absent from the map (and untyped rows) uses 1.0, so an empty/omitted
5026
+ * map is a no-op (uniform weighting). Vault-only.
5027
+ */
5028
+ factTypeWeights?: Partial<Record<FactType, number>>;
5252
5029
  /** Restrict chunk search to one conversation. Chunk-only. */
5253
5030
  conversationId?: string;
5254
5031
  /** Exclude one conversation from chunk search. Chunk-only. */
@@ -5301,416 +5078,1063 @@ interface RecallOptions {
5301
5078
  bm25AdmissionDivisor?: number;
5302
5079
  /** RRF smoothing constant for lane fusion (facts × chunks and side lanes). Default: 60. */
5303
5080
  rrfK?: number;
5081
+ /** Total graph hops incl. the seed lookup (hop 1). Default: 1 (seed only). */
5082
+ maxHops?: number;
5083
+ /** Max neighbor entities expanded per hop. Default: 8. */
5084
+ entityFanout?: number;
5085
+ /** Hard cap on accumulated memory IDs across all hops. Default: 64. */
5086
+ nodeBudget?: number;
5087
+ /**
5088
+ * PR5 — enable LLM graph path-refinement: at each traversal hop a model picks
5089
+ * which neighbor entities to expand instead of pure co-occurrence ranking.
5090
+ * Opt-in (default false); only active on the `high` budget (needs the
5091
+ * `traverse` flag) AND when `decomposeOptions` is set (reuses that auth).
5092
+ * Falls back to deterministic co-occurrence order on any error. Adds ≤1 LLM
5093
+ * call per expansion hop.
5094
+ */
5095
+ graphRefine?: boolean;
5096
+ }
5097
+ interface RecallContext {
5098
+ /** Required when `types` includes 'fact'. */
5099
+ vaultCtx?: VaultMemoryOperationsContext;
5100
+ /** Required when `types` includes 'chunk'. */
5101
+ storageCtx?: StorageOperationsContext;
5102
+ /** Embedding API options. */
5103
+ embeddingOptions: EmbeddingOptions;
5104
+ /** Vault embedding LRU cache. */
5105
+ vaultCache?: VaultEmbeddingCache;
5106
+ /**
5107
+ * Optional chunk-vector LRU cache. When provided, the chunk lane skips the
5108
+ * per-query decrypt + JSON.parse of every message's chunk vectors on warm
5109
+ * entries. Build via `createChunkVectorCache`. Omit for legacy behavior.
5110
+ */
5111
+ chunkCache?: ChunkVectorCache;
5112
+ /**
5113
+ * Optional — when provided, recall extracts entities from the query
5114
+ * and adds a graph lane to the RRF fusion (memories sharing entities
5115
+ * with the query rank higher). Build via `entityCollection` +
5116
+ * `memoryEntityCollection` from your DatabaseManager.
5117
+ */
5118
+ entityCtx?: EntityOperationsContext;
5119
+ }
5120
+ interface RecallResult {
5121
+ memories: RankedMemory[];
5122
+ /** Diagnostic: budget actually used (may downgrade if reranker fails). */
5123
+ usedBudget: Budget;
5124
+ /** Diagnostic: was the reranker invoked? */
5125
+ reranked: boolean;
5126
+ /** Diagnostic: total candidates considered before truncation. */
5127
+ candidateCount: number;
5128
+ /** Diagnostic: total memories in the vault when fact lane was queried. */
5129
+ vaultSize?: number;
5130
+ }
5131
+ /** Soft-degradation signals surfaced via {@link RecallDiagnostics.degraded}. */
5132
+ type RecallDegradation =
5133
+ /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5134
+ * this call — unavailable (e.g. React Native) or a transient failure. */
5135
+ "rerank-unavailable"
5136
+ /** `budget: 'high'` requested but no `decomposeOptions`, so query
5137
+ * decomposition was skipped and the budget downgraded to mid. */
5138
+ | "decompose-unavailable";
5139
+ /**
5140
+ * Per-call recall observability payload (see {@link RecallOptions.onDiagnostics}).
5141
+ * All timings are wall-clock milliseconds. Lane counts are post-dedupe,
5142
+ * pre-fusion. Intended to be forwarded to a metrics sink (e.g. PostHog).
5143
+ */
5144
+ interface RecallDiagnostics {
5145
+ /** Budget actually executed (may have downgraded from the requested one). */
5146
+ usedBudget: Budget;
5147
+ /** Whether the cross-encoder actually reranked the fact lane this call. */
5148
+ reranked: boolean;
5149
+ /** Total candidates considered before truncation. */
5150
+ candidateCount: number;
5151
+ /** Total vault size when the fact lane ran (absent if it didn't). */
5152
+ vaultSize?: number;
5153
+ /** Facts the fact lane returned (post-dedupe, pre-fusion). */
5154
+ factCount: number;
5155
+ /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
5156
+ chunkCount: number;
5157
+ /** Wall-clock phase timings (ms). */
5158
+ timings: {
5159
+ /** Whole `recall()` call. */
5160
+ total: number;
5161
+ /** Parallel query-embed + graph/temporal side-lane build. */
5162
+ prep: number;
5163
+ /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
5164
+ factLane: number;
5165
+ /** Chunk-lane search (`searchChunksOp`). */
5166
+ chunkLane: number;
5167
+ /** Cross-lane RRF fusion + provenance dedup after both lanes. */
5168
+ fuse: number;
5169
+ };
5170
+ /** Soft-degradation signals that fired this call (empty when clean). */
5171
+ degraded: RecallDegradation[];
5172
+ }
5173
+ type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5174
+ type RetainSource = "manual" | "auto-extracted" | "capsule";
5175
+ /** Why the consolidator fell back to "create" instead of a real decision. */
5176
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response";
5177
+ interface RetainOptions {
5178
+ source?: RetainSource;
5179
+ sourceChunkIds?: string[];
5180
+ scope?: string;
5181
+ folderId?: string | null;
5182
+ /** When provided, applies merge-on-write logic instead of plain insert. */
5183
+ enableAutoMerge?: boolean;
5184
+ /**
5185
+ * When true, a would-be create is suppressed if it matches a soft-deleted
5186
+ * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
5187
+ * can't silently resurrect a fact the user deleted. Off by default so manual
5188
+ * and other `retain()` callers are unaffected; auto-extraction opts in.
5189
+ * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5190
+ */
5191
+ respectTombstones?: boolean;
5192
+ /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5193
+ autoMergeThreshold?: number;
5194
+ /**
5195
+ * When provided, runs an LLM-based consolidation pass against the top-K
5196
+ * existing memories above `consolidateThreshold` (looser than auto-merge).
5197
+ * The LLM emits create/update/noop per Hindsight's facet-dedup rules.
5198
+ * Auth/endpoint required — one of `apiKey` / `getToken` (see
5199
+ * {@link PortalLlmAuth}); without this option we keep the cosine-only path.
5200
+ */
5201
+ consolidateOptions?: PortalLlmAuth & {
5202
+ baseUrl?: string;
5203
+ model?: string;
5204
+ /**
5205
+ * Invoked when the consolidator degrades to its "create" fallback
5206
+ * instead of returning a real decision — `llm_error` for network /
5207
+ * timeout / unparseable output, `invalid_response` for well-formed
5208
+ * JSON that violates the schema (unknown action, bad targetId).
5209
+ * A flaky consolidator silently accumulates duplicate memories;
5210
+ * wire this to logging/metrics so the fallback rate is observable.
5211
+ */
5212
+ onFallback?: (reason: ConsolidationFallbackReason) => void;
5213
+ /**
5214
+ * When set, the new fact and existing candidates are PII-redacted before
5215
+ * the consolidation model sees them and the result is de-anonymized before
5216
+ * persistence. Auto-extraction inherits this from its `extract` options.
5217
+ */
5218
+ piiRedaction?: boolean | PiiRedactor;
5219
+ };
5220
+ /** Cosine similarity floor for the consolidator candidate set. Default: 0.65. */
5221
+ consolidateThreshold?: number;
5222
+ /** Top-K consolidation candidates to feed the LLM. Default: 5. */
5223
+ consolidateTopK?: number;
5224
+ /**
5225
+ * W6 temporal lane — when the event in this fact occurred. Persisted to
5226
+ * memory_vault.event_time_* columns; recall's temporal lane filters
5227
+ * and boosts memories whose event-time overlaps the query window.
5228
+ * Auto-extraction emits this; manual writes can omit it.
5229
+ */
5230
+ eventTime?: {
5231
+ kind: "point" | "range" | "ongoing";
5232
+ start: number;
5233
+ end: number | null;
5234
+ } | null;
5235
+ /**
5236
+ * Typed memory (PR1) — the extractor's classification for this fact.
5237
+ * Persisted on create; on merge/consolidate it lazily backfills the target
5238
+ * only when the target has no type yet (never overwrites a non-null type).
5239
+ * Auto-extraction emits this; manual writes omit it (persisted as null).
5240
+ */
5241
+ factType?: FactType;
5242
+ /**
5243
+ * Tier-0 security (PR3) — trust tier for this fact. The write-time
5244
+ * injection screen threads `"quarantined"` here for flagged candidates;
5245
+ * omit for the default (null/trusted). Persisted only on create (a
5246
+ * quarantined candidate is force-created, never merged, so it can't bump
5247
+ * or contaminate a clean memory). The DB op re-validates the value
5248
+ * against the known set before writing.
5249
+ */
5250
+ trustTier?: string;
5251
+ }
5252
+ interface RetainResult {
5253
+ action: RetainAction;
5254
+ memoryId: string;
5255
+ /** When action is 'merge' or 'update', the prior memory's id. When action is
5256
+ * 'supersede', the stale memory that was retired (`memoryId` is the new one). */
5257
+ targetId?: string;
5258
+ /**
5259
+ * When action is 'suppressed', the id of the soft-deleted memory that blocked
5260
+ * re-creation. `memoryId` is set to the same id (no new memory was written).
5261
+ */
5262
+ tombstoneId?: string;
5263
+ /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
5264
+ proofCount: number;
5265
+ }
5266
+
5267
+ /**
5268
+ * Retain API — single write surface for the unified memory layer with
5269
+ * write-time auto-merge.
5270
+ *
5271
+ * On write, retain() searches for the nearest existing memory and, if its
5272
+ * cosine similarity to the new content exceeds the auto-merge threshold,
5273
+ * folds the new fact into the existing record (incrementing proof_count
5274
+ * and unioning source_chunk_ids). Otherwise it inserts a new memory.
5275
+ *
5276
+ * This is the W4 (auto-merge / dedup) workstream made callable. The W2
5277
+ * auto-extraction worker uses retain() as its write step, with the LLM
5278
+ * resolver upstream deciding when auto-merge should be enabled vs when
5279
+ * a semantic update should be applied via the lower-level update op.
5280
+ */
5281
+
5282
+ interface RetainContext {
5283
+ vaultCtx: VaultMemoryOperationsContext;
5284
+ embeddingOptions: EmbeddingOptions;
5285
+ vaultCache: VaultEmbeddingCache;
5286
+ }
5287
+ /**
5288
+ * Persist a memory, merging into the nearest existing record if its
5289
+ * cosine similarity exceeds the auto-merge threshold.
5290
+ *
5291
+ * Default behavior (autoMerge ON): proof_count increments on the merged
5292
+ * target, source_chunk_ids accumulate (union), content is left untouched.
5293
+ * The caller doesn't see a duplicate and the original memory's UI badge
5294
+ * shows it has been re-observed.
5295
+ *
5296
+ * Pass `enableAutoMerge: false` for a force-create (W2 resolver path
5297
+ * after it has explicitly decided "create new").
5298
+ */
5299
+ declare function retain(content: string, ctx: RetainContext, options?: RetainOptions): Promise<RetainResult>;
5300
+
5301
+ /**
5302
+ * Auto-extraction worker — W2.
5303
+ *
5304
+ * After each assistant turn, this pipeline reads the recent conversation,
5305
+ * asks an LLM to identify durable user facts, and writes them to the
5306
+ * vault via retain(). The user never needs to ask Anuma to remember
5307
+ * something — durable facts are captured automatically.
5308
+ *
5309
+ * Two-stage pipeline (extract → retain). The retain step uses
5310
+ * write-time auto-merge (W4) so the same fact re-observed in a later
5311
+ * conversation increments proof_count instead of duplicating.
5312
+ *
5313
+ * Spec: see `tasks/hackathon/auto-extraction-prompt.md`.
5314
+ */
5315
+
5316
+ declare const FACT_TYPES: readonly ["identity", "preference", "relationship", "plan", "ongoing_context", "constraint", "other"];
5317
+ type FactType = (typeof FACT_TYPES)[number];
5318
+ interface AutoExtractMessage {
5319
+ /** Stable message identifier — used for source_chunk_ids provenance. */
5320
+ id: string;
5321
+ role: "user" | "assistant";
5322
+ content: string;
5323
+ }
5324
+ /**
5325
+ * A named entity extracted from the conversation, with an optional
5326
+ * classification. `kind` is omitted when the model gave no kind or an
5327
+ * unrecognized one — see {@link validateCandidates}.
5328
+ */
5329
+ interface ExtractedEntity {
5330
+ name: string;
5331
+ kind?: EntityKind;
5332
+ }
5333
+ interface ExtractedCandidate {
5334
+ content: string;
5335
+ type: FactType;
5336
+ confidence: number;
5337
+ sourceMessageIds: string[];
5338
+ entities: ExtractedEntity[];
5339
+ /** W6 temporal lane — when the event in this fact occurred. Resolved
5340
+ * to absolute timestamps by the LLM; null when the fact has no
5341
+ * temporal anchor. */
5342
+ eventTime: {
5343
+ kind: "point" | "range" | "ongoing";
5344
+ /** Unix ms timestamp of the event start (or point). */
5345
+ start: number;
5346
+ /** Unix ms timestamp of the event end. Only set when kind='range'. */
5347
+ end: number | null;
5348
+ } | null;
5349
+ }
5350
+ /**
5351
+ * Tier-0 security (PR3) — describes a candidate the injection screen
5352
+ * quarantined and persisted as an audit row. The client uses this to surface
5353
+ * a "held for review" state. `content` lives on `candidate` (same exposure as
5354
+ * {@link ExtractedCandidate}); never log it.
5355
+ */
5356
+ interface QuarantinedMemoryInfo {
5357
+ candidate: ExtractedCandidate;
5358
+ /** The persisted (quarantined) memory row id. */
5359
+ memoryId: string;
5360
+ /** Coarse reason bucket from the screen. */
5361
+ reason: InjectionReason;
5362
+ /** Stable signature id that matched (safe to log; carries no content). */
5363
+ signature: string;
5364
+ }
5365
+ /**
5366
+ * Auth + endpoint for the extraction LLM call. Auth is the dual pattern —
5367
+ * one of `apiKey` / `getToken` is required at runtime; see
5368
+ * {@link PortalLlmAuth}.
5369
+ */
5370
+ interface ExtractFactsOptions extends PortalLlmAuth {
5371
+ baseUrl?: string;
5372
+ model?: string;
5373
+ /** Override the global fetch implementation (useful for tests). */
5374
+ fetchFn?: typeof fetch;
5375
+ /**
5376
+ * The user's own name(s) / handle(s) (e.g. profile nickname, wallet display
5377
+ * name). Candidates whose entire content is just one of these are dropped —
5378
+ * a personal memory system already knows who the user is, so "Peter Lee" is
5379
+ * circular noise. Optional; when omitted only the bare-fragment gate applies.
5380
+ */
5381
+ userIdentity?: string[];
5382
+ /**
5383
+ * Reference "now" (Unix ms) for resolving relative temporal phrases in the
5384
+ * transcript ("yesterday", "next week", "in two days") into the absolute
5385
+ * YYYY-MM-DD anchors the W6 temporal lane indexes on. The transcript itself
5386
+ * carries no timestamps, so without an anchor the model resolves relatives
5387
+ * against its own training-cutoff guess and emits wrong `eventTime` dates.
5388
+ * Defaults to `Date.now()`. Override for back-dated eval corpora and
5389
+ * deterministic tests (mirrors {@link RecallOptions.now}).
5390
+ *
5391
+ * Server-side timezone note: the ms value is formatted to a calendar date in
5392
+ * the process's local timezone (same basis as `parseLocalCalendarDay`). On a
5393
+ * UTC server, a user near midnight in a non-UTC offset can get the wrong
5394
+ * calendar day. Pass the user's local-midnight timestamp as `now` when the
5395
+ * process timezone doesn't match the user's.
5396
+ */
5397
+ now?: number;
5398
+ /**
5399
+ * Max attempts for the extraction call on a transient failure (default 3).
5400
+ * Lower it to bound how long extraction can hold a turn open — e.g. a worker
5401
+ * that runs extraction behind an in-flight-turn guard can pass `2` to keep
5402
+ * repeated failures from delaying later turns.
5403
+ */
5404
+ maxAttempts?: number;
5405
+ /** Per-attempt timeout (ms) for the extraction call. Defaults to the portal
5406
+ * helper's 60s. Combine with {@link maxAttempts} to cap the total time budget. */
5407
+ timeoutMs?: number;
5408
+ /**
5409
+ * Absolute wall-clock budget (ms) across ALL extraction attempts incl. backoff.
5410
+ * When set, the loop stops before an attempt that would exceed it, so worst-case
5411
+ * latency is bounded rather than `maxAttempts × timeoutMs`.
5412
+ */
5413
+ totalTimeoutMs?: number;
5414
+ /**
5415
+ * Override the retry backoff (ms) for a given 1-based attempt index. The
5416
+ * extraction call retries transient failures internally (default exponential
5417
+ * backoff); pass `() => 0` to retry without delay (useful for tests).
5418
+ */
5419
+ backoffMs?: (attempt: number) => number;
5420
+ /**
5421
+ * When set, PII (emails, phones, SSNs, cards, IPs, API keys, …) in the
5422
+ * conversation transcript is replaced with tagged placeholders before the
5423
+ * extraction call, and the returned facts + entities are de-anonymized so the
5424
+ * vault keeps the real values while raw PII never reaches the extraction
5425
+ * model (and, via `extractAndRetain`, the consolidation model). Pass `true`
5426
+ * for a fresh per-call redactor, or a shared {@link PiiRedactor} to keep
5427
+ * placeholder numbering consistent with other calls.
5428
+ *
5429
+ * NOTE: this does NOT cover the embeddings provider. Facts are stored and
5430
+ * embedded with their real values, so to keep PII out of embedding requests
5431
+ * set `RetainContext.embeddingOptions.maskInput` (e.g. `redactor.maskText`)
5432
+ * as well — the two are independent switches.
5433
+ */
5434
+ piiRedaction?: boolean | PiiRedactor;
5435
+ /**
5436
+ * Called when the extraction LLM returned no usable result after exhausting
5437
+ * its retries (empty/malformed completion, network/HTTP error) — i.e. a
5438
+ * *failure* that drops the turn's facts, as opposed to a legitimate
5439
+ * `{candidates: []}` "nothing durable here". Lets callers distinguish a
5440
+ * silently-degrading extractor from quiet turns (the two are otherwise
5441
+ * indistinguishable). See {@link extractAndRetain}'s `outcome`.
5442
+ */
5443
+ onExhaustedEmpty?: () => void;
5444
+ /**
5445
+ * Called when the extractor DID produce candidates but PII de-anonymization
5446
+ * dropped every one of them — the model mangled its placeholders (so they
5447
+ * can't be restored to real values) or restoring the values blew the length
5448
+ * cap. These drops happen before `retain()`, so `failedCount` can't see them,
5449
+ * and the turn would otherwise masquerade as a quiet `no-facts` result. Lets
5450
+ * H3's `outcome` surface `dropped-after-redaction` so a rising PII-drop rate
5451
+ * (i.e. redaction silently eating facts) is alarmable.
5452
+ */
5453
+ onCandidatesDropped?: () => void;
5454
+ }
5455
+ /**
5456
+ * Stage 1 — call the LLM to extract candidate facts from the recent
5457
+ * conversation. Returns post-validated candidates only (confidence
5458
+ * threshold, source-id check, length cap, schema validation). Returns
5459
+ * an empty array if the LLM emits malformed JSON or no candidates.
5460
+ *
5461
+ * A null from `callPortalJsonCompletion` means a *failure* (empty completion,
5462
+ * malformed JSON, network/HTTP error) — distinct from a successful
5463
+ * `{candidates: []}`, which parses to a non-null object. A failed extraction
5464
+ * silently drops the whole turn's memories, so transient failures matter:
5465
+ * `callPortalJsonCompletion` retries them internally with backoff (default 3
5466
+ * attempts), so a one-off empty/malformed completion from a reasoning-class
5467
+ * model doesn't lose the turn. Only an exhausted-retry null reaches here.
5468
+ */
5469
+ declare function extractFacts(messages: AutoExtractMessage[], options: ExtractFactsOptions): Promise<ExtractedCandidate[]>;
5470
+ /**
5471
+ * Outcome of the EXTRACTOR stage for a turn — independent of whether the
5472
+ * subsequent `retain()` writes landed (that's `failedCount`):
5473
+ * - `extracted` — the extractor produced at least one candidate above
5474
+ * the confidence floor. Whether those writes succeeded
5475
+ * is reported separately by `failedCount` — so
5476
+ * `extracted` + `failedCount > 0` means "found facts but
5477
+ * writes are failing", which is more signal than
5478
+ * collapsing it into `no-facts` would give.
5479
+ * - `no-facts` — the extractor ran fine but found nothing durable.
5480
+ * - `empty-after-retry` — the extractor returned empty/malformed after
5481
+ * exhausting retries (a *failure*). Distinguishing this
5482
+ * from `no-facts` is what makes a silently-degrading
5483
+ * extractor alarmable rather than invisible.
5484
+ * - `dropped-after-redaction` — the extractor found facts but PII
5485
+ * de-anonymization dropped every one (mangled
5486
+ * placeholders / over-cap after restore), before retain.
5487
+ * A degradation `failedCount` can't see; would otherwise
5488
+ * look like `no-facts`.
5489
+ */
5490
+ type ExtractOutcome = "extracted" | "no-facts" | "empty-after-retry" | "dropped-after-redaction";
5491
+ /**
5492
+ * Stage 2 — for each extracted candidate, call retain() with auto-merge
5493
+ * enabled. When `consolidateOptions` are wired, retain()'s consolidation pass
5494
+ * is the "resolver": a second LLM call against the top-K existing vault
5495
+ * memories that decides create/update/noop/supersede — including retiring a
5496
+ * stale value on a state change (the supersession the extraction prompt above
5497
+ * promises). Without consolidation, retain() still dedups at the cosine-merge
5498
+ * level, and the read-time supersession heuristic remains the fallback.
5499
+ *
5500
+ * Returns the candidates that survived validation along with the retain
5501
+ * result for each (which captures whether the fact was created, merged,
5502
+ * or skipped), plus an `outcome` summarizing why the turn did/didn't produce
5503
+ * facts (see {@link ExtractOutcome}).
5504
+ */
5505
+ declare function extractAndRetain(messages: AutoExtractMessage[], retainCtx: RetainContext, options: {
5506
+ extract: ExtractFactsOptions;
5507
+ minConfidence?: number;
5508
+ /** Override scope/folder for all retained facts. */
5509
+ scope?: string;
5510
+ folderId?: string | null;
5511
+ /**
5512
+ * Forwarded verbatim to each retain() call — enables the LLM-based
5513
+ * consolidation pass (Hindsight facet-dedup) on every write. See
5514
+ * `RetainOptions.consolidateOptions` for auth + observability fields.
5515
+ */
5516
+ consolidateOptions?: RetainOptions["consolidateOptions"];
5517
+ /**
5518
+ * When provided, persist each candidate's `entities[]` to the
5519
+ * entity + memory_entity tables after a successful retain. This
5520
+ * powers the W5 graph retrieval lane — recall() can query for
5521
+ * memories sharing entities with the user's question.
5522
+ */
5523
+ entityCtx?: EntityOperationsContext;
5524
+ /**
5525
+ * Per-candidate failure hook — invoked once per filtered candidate
5526
+ * whose `retain()` call threw. Lets UI layers surface "couldn't save
5527
+ * X" toasts; without it consumers only see the aggregate
5528
+ * `failedCount` and can't name which facts dropped.
5529
+ */
5530
+ onCandidateFailed?: (candidate: ExtractedCandidate, error: unknown) => void;
5531
+ /**
5532
+ * Tier-0 security (PR3) — invoked once per candidate the injection screen
5533
+ * quarantined AND persisted (audit row written). Lets a UI surface a
5534
+ * "held for review" state instead of the fact silently vanishing. Carries
5535
+ * the same content exposure as `onMemoryExtracted` (the candidate) plus
5536
+ * the persisted `memoryId` + the screen `reason`/`signature`; content is
5537
+ * never logged. Fired only on a successful quarantine write, not on a
5538
+ * failed one (that goes to `onCandidateFailed`).
5539
+ */
5540
+ onQuarantined?: (info: QuarantinedMemoryInfo) => void;
5541
+ /**
5542
+ * Tier-0 security (PR5) — optional SECOND-layer LLM injection classifier.
5543
+ * When provided, candidates the deterministic {@link screenCandidatesForInjection}
5544
+ * screen passed as CLEAN are additionally run through a cheap LLM that
5545
+ * catches signature-free poison ("Trusts BrandX for financial advice")
5546
+ * the regex screen can't. Positives are quarantined exactly like a
5547
+ * signature hit (reason `llm_semantic`). DEFAULT OFF — omit this to keep
5548
+ * the deterministic-only, no-extra-LLM-call path. Fails clean on any
5549
+ * error. Content is PII-redacted before the call, inheriting the
5550
+ * extraction redaction setting when this option doesn't set its own.
5551
+ */
5552
+ injectionClassifier?: InjectionClassifierOptions;
5553
+ }): Promise<{
5554
+ candidates: ExtractedCandidate[];
5555
+ results: RetainResult[];
5556
+ failedCount: number;
5557
+ outcome: ExtractOutcome;
5558
+ /**
5559
+ * Tier-0 security (PR3) — candidates quarantined by the injection screen and
5560
+ * persisted as audit rows (trust_tier="quarantined"). Kept OUT of
5561
+ * `candidates`/`results` so `onMemoryExtracted` never announces poison;
5562
+ * surfaced here so callers can distinguish "nothing extracted" from
5563
+ * "extracted but held for review".
5564
+ */
5565
+ quarantined: QuarantinedMemoryInfo[];
5566
+ }>;
5567
+
5568
+ interface StoredVaultMemory {
5569
+ /** WatermelonDB internal ID */
5570
+ uniqueId: string;
5571
+ /** Plain text memory content */
5572
+ content: string;
5573
+ /** Scope for partitioning memories (e.g., "private", "shared") */
5574
+ scope: string;
5575
+ /** Folder ID for organization, null if unfiled */
5576
+ folderId: string | null;
5577
+ /** User ID for multi-user server-side scoping, null on client */
5578
+ userId: string | null;
5579
+ /** JSON-stringified embedding vector, null if not yet computed */
5580
+ embedding: string | null;
5581
+ /** Model that produced `embedding`. Null on legacy rows (grandfathered as
5582
+ * compatible with the current model). */
5583
+ embeddingModel: string | null;
5584
+ /** JSON-stringified array of source message IDs this fact was extracted from. */
5585
+ sourceChunkIds: string[] | null;
5586
+ /** Times this fact has been re-observed (for ranking + UX badges). */
5587
+ proofCount: number | null;
5588
+ /** How the memory was created: manual | auto-extracted | capsule. */
5589
+ source: string | null;
5590
+ /** W6 temporal lane — Unix ms when the event occurred (point/start of range). */
5591
+ eventTimeStart: number | null;
5592
+ /** W6 temporal lane — Unix ms when the event ended (range only). */
5593
+ eventTimeEnd: number | null;
5594
+ /** W6 temporal lane — `point | range | ongoing | null`. */
5595
+ eventTimeKind: string | null;
5596
+ /** When true, the user has manually set this memory's topics (entity links);
5597
+ * auto-extraction leaves them alone. False on legacy/auto rows. */
5598
+ topicsUserManaged: boolean;
5599
+ /** Unix ms of the last LLM topic-extraction pass over this memory's content.
5600
+ * Null = never extracted standalone; rows that already carry entity links
5601
+ * are grandfathered as extracted (see getMemoriesNeedingTopicExtractionOp). */
5602
+ topicsExtractedAt: number | null;
5603
+ /** Write-time supersession (A2): id of the newer memory that replaced this
5604
+ * one (incompatible-value update, e.g. "Lives in Portland" → "Lives in SF").
5605
+ * Null = live. Superseded rows are excluded from recall/dedup by default but
5606
+ * kept for history + the read-time fallback. */
5607
+ supersededBy: string | null;
5608
+ /** Unix ms when this memory was superseded. Null when live. */
5609
+ supersededAt: number | null;
5610
+ /** Extraction-logic version this memory was last stamped under. Null (pre-v38)
5611
+ * reads as 0, so a TOPICS_EXTRACTION_VERSION bump re-extracts stale rows. */
5612
+ topicsExtractedVersion: number | null;
5613
+ /** C3 re-observation watermark: Unix ms of the last retain() merge into this
5614
+ * fact. Distinct from `updatedAt` (which merges preserve). Null = never
5615
+ * re-observed since the column was added; synthesis falls back to
5616
+ * `updatedAt` in that case. */
5617
+ lastObservedAt: number | null;
5618
+ /** Typed memory (PR1) — the extractor's FactType for this fact, or null on
5619
+ * legacy/manual/untyped rows. Plaintext string (not narrowed to FactType
5620
+ * here since the DB can hold any stored value). */
5621
+ factType: string | null;
5622
+ /** Decay archive state (PR2) — Unix ms when archived, or null when active. */
5623
+ archivedAt: number | null;
5624
+ /** Tier-0 security (PR3) — "quarantined" | "trusted" | null. */
5625
+ trustTier: string | null;
5626
+ createdAt: Date;
5627
+ updatedAt: Date;
5628
+ isDeleted: boolean;
5629
+ }
5630
+ interface CreateVaultMemoryOptions {
5631
+ content: string;
5632
+ /** Scope for the memory. Defaults to "private" if omitted. */
5633
+ scope?: string;
5634
+ /** Folder ID for organization, null or omitted if unfiled */
5635
+ folderId?: string | null;
5636
+ /** JSON-stringified embedding vector to persist */
5637
+ embedding?: string;
5638
+ /** Model that produced `embedding`. Persisted alongside it so a later
5639
+ * model change can detect and re-embed stale vectors. */
5640
+ embeddingModel?: string;
5641
+ /** Source message IDs that produced this fact (auto-extraction provenance). */
5642
+ sourceChunkIds?: string[];
5643
+ /** Initial proof count. Defaults to 1 if omitted. */
5644
+ proofCount?: number;
5645
+ /** How the memory was created. Defaults to "manual" if omitted. */
5646
+ source?: string;
5647
+ /** W6 temporal lane — when the event in this memory occurred. */
5648
+ eventTime?: {
5649
+ /** Unix ms timestamp of event start (or point). */
5650
+ start: number | null;
5651
+ /** Unix ms timestamp of event end (range only). */
5652
+ end: number | null;
5653
+ /** Kind: 'point' | 'range' | 'ongoing' | null (or omit). */
5654
+ kind: "point" | "range" | "ongoing" | null;
5655
+ };
5656
+ /** Typed memory (PR1) — the extractor's classification for this fact.
5657
+ * Omit for manual/untyped saves (persisted as null). */
5658
+ factType?: FactType;
5659
+ /** Tier-0 security (PR3) — set "quarantined" when the injection screen
5660
+ * flagged this fact. Omit for the default (null/trusted). */
5661
+ trustTier?: string;
5662
+ }
5663
+ interface UpdateVaultMemoryOptions {
5664
+ content: string;
5665
+ /** If provided, updates the memory's scope. */
5666
+ scope?: string;
5667
+ /** If provided, moves the memory to this folder. */
5668
+ folderId?: string | null;
5669
+ /** JSON-stringified embedding vector to persist, or null to clear stale embedding */
5670
+ embedding?: string | null;
5671
+ /** Model that produced `embedding`. Set whenever `embedding` is written so
5672
+ * the stored model tag stays in sync with the vector. */
5673
+ embeddingModel?: string | null;
5674
+ /** Replace source-chunk-ids list (used during merge to accumulate provenance). */
5675
+ sourceChunkIds?: string[];
5676
+ /** Set an absolute proof count. Prefer {@link proofCountIncrement} for
5677
+ * re-observation paths so the read+write happens inside the writer
5678
+ * and concurrent retains can't lose updates. */
5679
+ proofCount?: number;
5680
+ /** Atomically bump proof_count by this delta inside the write block.
5681
+ * Reads the current value from the in-memory record at write time, so
5682
+ * two parallel retain() calls observe each other's commits and neither
5683
+ * loses its increment. Wins over `proofCount` when both are set. */
5684
+ proofCountIncrement?: number;
5685
+ /** Set source ("manual" | "auto-extracted" | "capsule"). */
5686
+ source?: string;
5687
+ /**
5688
+ * W6 temporal lane — write the event-time fields on update. Use during
5689
+ * auto-merge to preserve (or refine) the original event-time signal when
5690
+ * a new observation lands on an existing fact. Omit to leave the
5691
+ * existing values untouched.
5692
+ */
5693
+ eventTime?: {
5694
+ start: number | null;
5695
+ end: number | null;
5696
+ kind: "point" | "range" | "ongoing" | null;
5697
+ };
5698
+ /**
5699
+ * When true, restore the existing `updated_at` after the write so the
5700
+ * recency multiplier doesn't see a re-observation as a brand-new fact.
5701
+ * Set by auto-merge/consolidate paths — they want proof_count to bump
5702
+ * without inflating recency on top.
5703
+ */
5704
+ preserveUpdatedAt?: boolean;
5705
+ /** C3: Unix ms to stamp as the re-observation watermark (`last_observed_at`).
5706
+ * Set by retain() merge/consolidate paths so a re-observation records "seen
5707
+ * again now" without touching `updated_at` (which `preserveUpdatedAt` keeps
5708
+ * pinned). Omit to leave the existing value untouched. */
5709
+ lastObservedAt?: number;
5710
+ /** If provided, sets whether the user has taken manual control of this
5711
+ * memory's topics. Set by {@link setMemoryEntitiesOp}. */
5712
+ topicsUserManaged?: boolean;
5713
+ /** Typed memory (PR1) — set/refine the fact's classification on update.
5714
+ * Used by retain()'s lazy backfill (adopt an incoming type only when the
5715
+ * existing row has none). Omit to leave the existing value untouched. */
5716
+ factType?: FactType;
5717
+ /** Tier-0 security (PR3) — set the trust tier on update ("quarantined" |
5718
+ * "trusted"). Omit to leave the existing value untouched. */
5719
+ trustTier?: string;
5720
+ /**
5721
+ * PR5 — un-archive on re-observe. When true, clears `archived_at` (null) as
5722
+ * part of the write, resurrecting a decayed row that a new observation just
5723
+ * merged into. retain() sets this (with `preserveUpdatedAt` OFF) so the
5724
+ * restored row's decay clock resets and it doesn't immediately re-archive.
5725
+ * Omit/false to leave `archived_at` untouched.
5726
+ */
5727
+ restore?: boolean;
5304
5728
  }
5305
- interface RecallContext {
5306
- /** Required when `types` includes 'fact'. */
5307
- vaultCtx?: VaultMemoryOperationsContext;
5308
- /** Required when `types` includes 'chunk'. */
5309
- storageCtx?: StorageOperationsContext;
5310
- /** Embedding API options. */
5311
- embeddingOptions: EmbeddingOptions;
5312
- /** Vault embedding LRU cache. */
5313
- vaultCache?: VaultEmbeddingCache;
5729
+
5730
+ interface VaultMemoryOperationsContext {
5731
+ database: Database;
5732
+ vaultMemoryCollection: Collection<VaultMemory>;
5733
+ walletAddress?: string;
5734
+ signMessage?: SignMessageFn;
5735
+ embeddedWalletSigner?: EmbeddedWalletSignerFn;
5736
+ /** When set, operations scope to this user (server-side multi-user). */
5737
+ userId?: string;
5314
5738
  /**
5315
- * Optional chunk-vector LRU cache. When provided, the chunk lane skips the
5316
- * per-query decrypt + JSON.parse of every message's chunk vectors on warm
5317
- * entries. Build via `createChunkVectorCache`. Omit for legacy behavior.
5739
+ * Asserts this context runs against a physically single-tenant database — one
5740
+ * where every row belongs to the same owner (the per-wallet client DBs, which
5741
+ * hold exactly one wallet's rows written with `user_id = null`). This is the
5742
+ * ONLY thing that makes the decay sweep's unscoped scan/archive/delete safe
5743
+ * without a `userId`: see {@link assertVaultScopeForSweep}. A shared /
5744
+ * multi-tenant DB must NOT set this — it must scope by `userId` instead.
5745
+ * `walletAddress` presence alone is NOT a substitute (the sweep query filters
5746
+ * by `user_id` only, so a bare `walletAddress` on a shared DB would sweep
5747
+ * every tenant).
5318
5748
  */
5319
- chunkCache?: ChunkVectorCache;
5749
+ singleTenant?: boolean;
5320
5750
  /**
5321
- * Optional — when provided, recall extracts entities from the query
5322
- * and adds a graph lane to the RRF fusion (memories sharing entities
5323
- * with the query rank higher). Build via `entityCollection` +
5324
- * `memoryEntityCollection` from your DatabaseManager.
5751
+ * When set, vault delete ops cascade to memory_entity rows pointing at
5752
+ * the deleted memories. Without this the W5 graph lane keeps returning
5753
+ * IDs of soft-deleted memories and the join table grows unbounded.
5325
5754
  */
5326
5755
  entityCtx?: EntityOperationsContext;
5327
5756
  }
5328
- interface RecallResult {
5329
- memories: RankedMemory[];
5330
- /** Diagnostic: budget actually used (may downgrade if reranker fails). */
5331
- usedBudget: Budget;
5332
- /** Diagnostic: was the reranker invoked? */
5333
- reranked: boolean;
5334
- /** Diagnostic: total candidates considered before truncation. */
5335
- candidateCount: number;
5336
- /** Diagnostic: total memories in the vault when fact lane was queried. */
5337
- vaultSize?: number;
5338
- }
5339
- /** Soft-degradation signals surfaced via {@link RecallDiagnostics.degraded}. */
5340
- type RecallDegradation =
5341
- /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5342
- * this call — unavailable (e.g. React Native) or a transient failure. */
5343
- "rerank-unavailable"
5344
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5345
- * decomposition was skipped and the budget downgraded to mid. */
5346
- | "decompose-unavailable";
5347
- /**
5348
- * Per-call recall observability payload (see {@link RecallOptions.onDiagnostics}).
5349
- * All timings are wall-clock milliseconds. Lane counts are post-dedupe,
5350
- * pre-fusion. Intended to be forwarded to a metrics sink (e.g. PostHog).
5351
- */
5352
- interface RecallDiagnostics {
5353
- /** Budget actually executed (may have downgraded from the requested one). */
5354
- usedBudget: Budget;
5355
- /** Whether the cross-encoder actually reranked the fact lane this call. */
5356
- reranked: boolean;
5357
- /** Total candidates considered before truncation. */
5358
- candidateCount: number;
5359
- /** Total vault size when the fact lane ran (absent if it didn't). */
5360
- vaultSize?: number;
5361
- /** Facts the fact lane returned (post-dedupe, pre-fusion). */
5362
- factCount: number;
5363
- /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
5364
- chunkCount: number;
5365
- /** Wall-clock phase timings (ms). */
5366
- timings: {
5367
- /** Whole `recall()` call. */
5368
- total: number;
5369
- /** Parallel query-embed + graph/temporal side-lane build. */
5370
- prep: number;
5371
- /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
5372
- factLane: number;
5373
- /** Chunk-lane search (`searchChunksOp`). */
5374
- chunkLane: number;
5375
- /** Cross-lane RRF fusion + provenance dedup after both lanes. */
5376
- fuse: number;
5377
- };
5378
- /** Soft-degradation signals that fired this call (empty when clean). */
5379
- degraded: RecallDegradation[];
5380
- }
5381
- type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5382
- type RetainSource = "manual" | "auto-extracted" | "capsule";
5383
- /** Why the consolidator fell back to "create" instead of a real decision. */
5384
- type ConsolidationFallbackReason = "llm_error" | "invalid_response";
5385
- interface RetainOptions {
5386
- source?: RetainSource;
5387
- sourceChunkIds?: string[];
5388
- scope?: string;
5757
+ declare function createVaultMemoryOp(ctx: VaultMemoryOperationsContext, opts: CreateVaultMemoryOptions): Promise<StoredVaultMemory>;
5758
+ declare function createVaultMemoriesBatchOp(ctx: VaultMemoryOperationsContext, optionsArray: CreateVaultMemoryOptions[]): Promise<StoredVaultMemory[]>;
5759
+ declare function getVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string): Promise<StoredVaultMemory | null>;
5760
+ declare function getAllVaultMemoriesOp(ctx: VaultMemoryOperationsContext, options?: {
5761
+ scopes?: string[];
5762
+ since?: Date;
5763
+ limit?: number;
5389
5764
  folderId?: string | null;
5390
- /** When provided, applies merge-on-write logic instead of plain insert. */
5391
- enableAutoMerge?: boolean;
5392
5765
  /**
5393
- * When true, a would-be create is suppressed if it matches a soft-deleted
5394
- * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
5395
- * can't silently resurrect a fact the user deleted. Off by default so manual
5396
- * and other `retain()` callers are unaffected; auto-extraction opts in.
5397
- * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5766
+ * Include soft-deleted memories in the result (each carries
5767
+ * `isDeleted: true`). Default `false` — deleted rows are excluded, as
5768
+ * they are from every other read path. Used by the Memory Graph to
5769
+ * render "forgotten" nodes; ordinary consumers should leave this off.
5398
5770
  */
5399
- respectTombstones?: boolean;
5400
- /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5401
- autoMergeThreshold?: number;
5771
+ includeDeleted?: boolean;
5772
+ /** Include archived (decayed) memories. Default `false` (PR1 choke point). */
5773
+ includeArchived?: boolean;
5774
+ /** Include quarantined memories. Default `false` (PR1 choke point). */
5775
+ includeQuarantined?: boolean;
5776
+ /** Typed memory (PR1) — restrict to these fact types. Omit for no filter. */
5777
+ factTypes?: string[];
5402
5778
  /**
5403
- * When provided, runs an LLM-based consolidation pass against the top-K
5404
- * existing memories above `consolidateThreshold` (looser than auto-merge).
5405
- * The LLM emits create/update/noop per Hindsight's facet-dedup rules.
5406
- * Auth/endpoint required — one of `apiKey` / `getToken` (see
5407
- * {@link PortalLlmAuth}); without this option we keep the cosine-only path.
5779
+ * Include A2-superseded memories (each carries `supersededBy`). Default
5780
+ * `false` — superseded rows are excluded, as they are from recall/dedup.
5781
+ * Used by a "memory history" view to render retired facts.
5408
5782
  */
5409
- consolidateOptions?: PortalLlmAuth & {
5410
- baseUrl?: string;
5411
- model?: string;
5412
- /**
5413
- * Invoked when the consolidator degrades to its "create" fallback
5414
- * instead of returning a real decision — `llm_error` for network /
5415
- * timeout / unparseable output, `invalid_response` for well-formed
5416
- * JSON that violates the schema (unknown action, bad targetId).
5417
- * A flaky consolidator silently accumulates duplicate memories;
5418
- * wire this to logging/metrics so the fallback rate is observable.
5419
- */
5420
- onFallback?: (reason: ConsolidationFallbackReason) => void;
5421
- /**
5422
- * When set, the new fact and existing candidates are PII-redacted before
5423
- * the consolidation model sees them and the result is de-anonymized before
5424
- * persistence. Auto-extraction inherits this from its `extract` options.
5425
- */
5426
- piiRedaction?: boolean | PiiRedactor;
5427
- };
5428
- /** Cosine similarity floor for the consolidator candidate set. Default: 0.65. */
5429
- consolidateThreshold?: number;
5430
- /** Top-K consolidation candidates to feed the LLM. Default: 5. */
5431
- consolidateTopK?: number;
5783
+ includeSuperseded?: boolean;
5784
+ }): Promise<StoredVaultMemory[]>;
5785
+ declare function getAllVaultMemoryContentsOp(ctx: VaultMemoryOperationsContext, options?: {
5786
+ since?: Date;
5787
+ }): Promise<string[]>;
5788
+ declare function updateVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string, opts: UpdateVaultMemoryOptions): Promise<StoredVaultMemory | null>;
5789
+ declare function deleteVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string): Promise<boolean>;
5790
+ /**
5791
+ * Mark a memory as superseded by a newer one (A2 write-time supersession).
5792
+ * The row stays in the table (history + read-time fallback) but is excluded
5793
+ * from recall/dedup by default via `superseded_by`. Idempotent-ish: no-op if
5794
+ * the row is missing, not owned, deleted, or already superseded. Does NOT
5795
+ * preserve `updated_at` — superseded rows are hidden from recall, so their
5796
+ * recency is irrelevant.
5797
+ *
5798
+ * @param id - the memory being retired (e.g. "Lives in Portland")
5799
+ * @param supersededById - the newer memory that replaced it (e.g. "Lives in SF")
5800
+ */
5801
+ declare function supersedeVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string, supersededById: string): Promise<boolean>;
5802
+ /**
5803
+ * Get all non-deleted, unfiled vault memories (folder_id is null).
5804
+ */
5805
+ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): Promise<StoredVaultMemory[]>;
5806
+ /**
5807
+ * The current topic-extraction logic version. Bump this whenever the extraction
5808
+ * prompt or model in `topicExtract.ts` changes: every memory stamped under an
5809
+ * older version (including pre-v37 rows, read as version 0) is then re-extracted
5810
+ * by the next sweep, so topic-quality improvements propagate across the existing
5811
+ * vault. The worker's `limit` drains that re-extraction across sweeps.
5812
+ */
5813
+ declare const TOPICS_EXTRACTION_VERSION = 1;
5814
+ /**
5815
+ * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
5816
+ * background topic worker should run LLM entity extraction on, and which it
5817
+ * should merely stamp as already-extracted.
5818
+ */
5819
+ interface MemoriesNeedingTopicExtraction {
5432
5820
  /**
5433
- * W6 temporal lane — when the event in this fact occurred. Persisted to
5434
- * memory_vault.event_time_* columns; recall's temporal lane filters
5435
- * and boosts memories whose event-time overlaps the query window.
5436
- * Auto-extraction emits this; manual writes can omit it.
5821
+ * Memories to run LLM topic extraction on (decrypted): never-extracted rows
5822
+ * with no entity links, plus stamped rows edited since their last pass
5823
+ * (`updated_at` > `topics_extracted_at`) or extracted under an older
5824
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
5825
+ * stale-version rows come first (they get priority under `limit`), each group
5826
+ * newest-created first.
5437
5827
  */
5438
- eventTime?: {
5439
- kind: "point" | "range" | "ongoing";
5440
- start: number;
5441
- end: number | null;
5442
- } | null;
5443
- }
5444
- interface RetainResult {
5445
- action: RetainAction;
5446
- memoryId: string;
5447
- /** When action is 'merge' or 'update', the prior memory's id. When action is
5448
- * 'supersede', the stale memory that was retired (`memoryId` is the new one). */
5449
- targetId?: string;
5828
+ pending: StoredVaultMemory[];
5450
5829
  /**
5451
- * When action is 'suppressed', the id of the soft-deleted memory that blocked
5452
- * re-creation. `memoryId` is set to the same id (no new memory was written).
5830
+ * IDs of rows that already have entity links but no watermark — legacy rows
5831
+ * extracted by the conversation pipeline before v36. Grandfather these with
5832
+ * {@link stampTopicsExtractedAtOp} (no LLM call) so a later content edit
5833
+ * makes them re-extractable instead of invisible forever. Bounded by the
5834
+ * same `limit` as {@link pending} — stamping loads a Model per row, so the
5835
+ * grandfather backlog is drained across sweeps rather than in one spike.
5453
5836
  */
5454
- tombstoneId?: string;
5455
- /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
5456
- proofCount: number;
5837
+ linkedUnstamped: string[];
5838
+ }
5839
+ /**
5840
+ * Sweep query for the background topic-extraction worker: partition the
5841
+ * user's non-deleted, non-user-managed memories by what the worker should do
5842
+ * with them (see {@link MemoriesNeedingTopicExtraction}). User-managed rows
5843
+ * are never returned — the user owns their topics, including an intentionally
5844
+ * empty set. Requires `ctx.entityCtx` for the entity-links check.
5845
+ */
5846
+ declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
5847
+ limit?: number;
5848
+ }): Promise<MemoriesNeedingTopicExtraction>;
5849
+ /**
5850
+ * Stamp `topics_extracted_at` (and `topics_extracted_version`) on the given
5851
+ * memories — the topic worker calls this after a successful extraction pass
5852
+ * (including zero-entity results, so quiet memories aren't re-asked every sweep)
5853
+ * and to grandfather `linkedUnstamped` rows without an LLM call. `version`
5854
+ * defaults to {@link TOPICS_EXTRACTION_VERSION}: stamping at the current version
5855
+ * (for both fresh extractions and grandfathered legacy rows) means they aren't
5856
+ * re-extracted until a future version bump. Preserves `updated_at` so a stamp
5857
+ * never inflates the recency multiplier — and never masks a concurrent content
5858
+ * edit from the next sweep. Skips deleted, foreign-user, and user-managed rows.
5859
+ * Returns the IDs actually stamped.
5860
+ *
5861
+ * ALL eligibility AND `updated_at` are read from the LIVE Model inside the
5862
+ * serialized writer — never a pre-writer snapshot. Writers are serialized, so
5863
+ * a content edit or topic-override that commits before this writer runs is
5864
+ * observed here: its fresh `updated_at` is preserved (so the next sweep's
5865
+ * `updated_at > stamp` check still fires) and a mid-pass user-managed flip
5866
+ * skips the row. Reading `updated_at` from a raw pre-fetch instead would write
5867
+ * a stale value back, pushing `updated_at < topics_extracted_at` and hiding
5868
+ * the edited memory from every future sweep.
5869
+ *
5870
+ * Callers bound the input via `getMemoriesNeedingTopicExtractionOp`'s `limit`
5871
+ * (both `pending` and `linkedUnstamped` are capped), so the per-row Model load
5872
+ * needed to `prepareUpdate` stays bounded and never spikes the RecordCache.
5873
+ */
5874
+ declare function stampTopicsExtractedAtOp(ctx: VaultMemoryOperationsContext, memoryIds: readonly string[], extractedAt: number, version?: number): Promise<string[]>;
5875
+ declare function deleteAllVaultMemoriesForUserOp(ctx: VaultMemoryOperationsContext, userId: string): Promise<number>;
5876
+ /**
5877
+ * The minimal plaintext shape the decay sweep needs — mirrors the `DecayInput`
5878
+ * shape in `memory/decay` plus the row id. Deliberately omits `content`
5879
+ * (encrypted) so the sweep stays zero-knowledge.
5880
+ */
5881
+ interface DecayCandidateRaw {
5882
+ uniqueId: string;
5883
+ factType: string | null;
5884
+ eventTimeEnd: number | null;
5885
+ eventTimeKind: string | null;
5886
+ /** Unix ms — the raw `updated_at`, used both for the age rule and as the
5887
+ * optimistic-concurrency guard passed back to {@link archiveVaultMemoryOp}. */
5888
+ updatedAt: number;
5889
+ archivedAt: number | null;
5890
+ source: string | null;
5891
+ /** `trusted` | `quarantined` | null. Quarantined rows still decay by RULE, but
5892
+ * are never handed to the optional content-reading decay classifier (they must
5893
+ * not egress poison content — see the decay sweeper's `isBorderline`). */
5894
+ trustTier: string | null;
5457
5895
  }
5458
-
5459
5896
  /**
5460
- * Retain API — single write surface for the unified memory layer with
5461
- * write-time auto-merge.
5897
+ * Decay sweep candidate scan (PR2). Selects the plaintext columns
5898
+ * `classifyDecay` (in `memory/decay`) needs via
5899
+ * `unsafeFetchRaw` — NO Model per row (dodges the never-evicted RecordCache /
5900
+ * web Pile-2 OOM history) and NO `content` read / decrypt (zero-knowledge).
5462
5901
  *
5463
- * On write, retain() searches for the nearest existing memory and, if its
5464
- * cosine similarity to the new content exceeds the auto-merge threshold,
5465
- * folds the new fact into the existing record (incrementing proof_count
5466
- * and unioning source_chunk_ids). Otherwise it inserts a new memory.
5902
+ * Includes archived AND quarantined rows (so archived→delete transitions and
5903
+ * aged quarantined rows are seen) but excludes hard-deleted rows — the
5904
+ * `baseVaultConditions` default keeps `is_deleted = false`.
5467
5905
  *
5468
- * This is the W4 (auto-merge / dedup) workstream made callable. The W2
5469
- * auto-extraction worker uses retain() as its write step, with the LLM
5470
- * resolver upstream deciding when auto-merge should be enabled vs when
5471
- * a semantic update should be applied via the lower-level update op.
5906
+ * Refuses to run on an unscoped multi-tenant context (see
5907
+ * {@link assertVaultScopeForSweep}).
5472
5908
  */
5473
-
5474
- interface RetainContext {
5475
- vaultCtx: VaultMemoryOperationsContext;
5476
- embeddingOptions: EmbeddingOptions;
5477
- vaultCache: VaultEmbeddingCache;
5478
- }
5909
+ declare function getDecayCandidatesRawOp(ctx: VaultMemoryOperationsContext): Promise<DecayCandidateRaw[]>;
5479
5910
  /**
5480
- * Persist a memory, merging into the nearest existing record if its
5481
- * cosine similarity exceeds the auto-merge threshold.
5911
+ * Archive a memory (decay soft state, PR2) — set `archived_at`. An archived row
5912
+ * drops out of every recall lane via the `baseVaultConditions` choke point but
5913
+ * stays recoverable via {@link restoreVaultMemoryOp} until the hard-delete
5914
+ * window elapses.
5482
5915
  *
5483
- * Default behavior (autoMerge ON): proof_count increments on the merged
5484
- * target, source_chunk_ids accumulate (union), content is left untouched.
5485
- * The caller doesn't see a duplicate and the original memory's UI badge
5486
- * shows it has been re-observed.
5916
+ * Concurrency: re-checks `is_deleted` / ownership / `archived_at` INSIDE the
5917
+ * serialized writer (mirrors {@link updateVaultMemoryOp}). Additionally, when
5918
+ * `opts.expectedUpdatedAt` is given, the archive is skipped if the row's current
5919
+ * `updated_at` no longer matches — i.e. a `retain()` merge (which bumps
5920
+ * `updated_at`) landed between the sweep's candidate scan and this write, so the
5921
+ * fact was just re-observed and must NOT be archived on stale data. Idempotent:
5922
+ * a row another sweep already archived returns `false` (no double-write).
5487
5923
  *
5488
- * Pass `enableAutoMerge: false` for a force-create (W2 resolver path
5489
- * after it has explicitly decided "create new").
5924
+ * @returns `true` if this call archived the row; `false` if it was stale
5925
+ * (deleted / not owned / already archived / refreshed under us).
5490
5926
  */
5491
- declare function retain(content: string, ctx: RetainContext, options?: RetainOptions): Promise<RetainResult>;
5492
-
5927
+ declare function archiveVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string, opts?: {
5928
+ /** Timestamp to stamp into `archived_at`. Default `Date.now()`. */
5929
+ now?: number;
5930
+ /** Optimistic-concurrency guard: skip if the row's `updated_at` changed
5931
+ * since the sweep observed it (a concurrent re-observation). */
5932
+ expectedUpdatedAt?: number;
5933
+ }): Promise<boolean>;
5493
5934
  /**
5494
- * Auto-extraction worker — W2.
5935
+ * Restore an archived memory (PR2) — clear `archived_at` so it re-enters recall.
5936
+ * Re-checks `is_deleted` / ownership inside the writer. Idempotent on an
5937
+ * already-active row (clearing null → null is harmless).
5495
5938
  *
5496
- * After each assistant turn, this pipeline reads the recent conversation,
5497
- * asks an LLM to identify durable user facts, and writes them to the
5498
- * vault via retain(). The user never needs to ask Anuma to remember
5499
- * something — durable facts are captured automatically.
5939
+ * @returns `true` if the row was restored (or already active); `false` if it was
5940
+ * deleted / not owned / missing.
5941
+ */
5942
+ declare function restoreVaultMemoryOp(ctx: VaultMemoryOperationsContext, id: string): Promise<boolean>;
5943
+ /**
5944
+ * Hard-delete a memory ONLY if it is still archived and still past the delete
5945
+ * window (PR2 decay terminal transition). Unlike the generic
5946
+ * {@link deleteVaultMemoryOp}, this re-reads `archived_at` INSIDE the writer and
5947
+ * bails if the row was restored (`archived_at → null`) or re-archived more
5948
+ * recently since the sweep's candidate scan. This is the restore-vs-delete
5949
+ * mutual-exclusion guard: a user hitting Restore between the scan and this write
5950
+ * must win, so their just-rescued memory is never permanently lost.
5500
5951
  *
5501
- * Two-stage pipeline (extract → retain). The retain step uses
5502
- * write-time auto-merge (W4) so the same fact re-observed in a later
5503
- * conversation increments proof_count instead of duplicating.
5952
+ * @returns `true` if this call hard-deleted the row; `false` if it was stale
5953
+ * (deleted / not owned / no longer archived / no longer past the window).
5954
+ */
5955
+ declare function hardDeleteDecayedOp(ctx: VaultMemoryOperationsContext, id: string, opts: {
5956
+ hardDeleteWindowMs: number;
5957
+ now?: number;
5958
+ }): Promise<boolean>;
5959
+ declare function updateVaultMemoryEmbeddingOp(ctx: VaultMemoryOperationsContext, id: string, embedding: string, embeddingModel: string): Promise<boolean>;
5960
+
5961
+ /**
5962
+ * Queue Types
5504
5963
  *
5505
- * Spec: see `tasks/hackathon/auto-extraction-prompt.md`.
5964
+ * Types for the in-memory write queue that holds operations
5965
+ * when encryption keys aren't yet available.
5506
5966
  */
5507
5967
 
5508
- declare const FACT_TYPES: readonly ["identity", "preference", "relationship", "plan", "ongoing_context", "constraint", "other"];
5509
- type FactType = (typeof FACT_TYPES)[number];
5510
- interface AutoExtractMessage {
5511
- /** Stable message identifier — used for source_chunk_ids provenance. */
5968
+ /**
5969
+ * Types of database operations that can be queued.
5970
+ */
5971
+ type QueuedOperationType = "createConversation" | "updateConversationTitle" | "updateConversationPinned" | "createMessage" | "updateMessage" | "createMedia" | "createMediaBatch" | "updateMediaMessageId";
5972
+ /**
5973
+ * A single queued database operation.
5974
+ */
5975
+ interface QueuedOperation {
5976
+ /** Unique ID for this operation */
5512
5977
  id: string;
5513
- role: "user" | "assistant";
5514
- content: string;
5978
+ /** Type of operation */
5979
+ type: QueuedOperationType;
5980
+ /** Wallet address this operation belongs to */
5981
+ walletAddress: string;
5982
+ /** When the operation was queued */
5983
+ timestamp: number;
5984
+ /** Priority for ordering (lower = higher priority). Conversations=0, Messages=1, Media=2 */
5985
+ priority: number;
5986
+ /** IDs of operations that must complete before this one */
5987
+ dependencies: string[];
5988
+ /** Operation-specific payload */
5989
+ payload: Record<string, any>;
5990
+ /** Number of times this operation has been retried */
5991
+ retryCount: number;
5992
+ /** Maximum number of retries allowed */
5993
+ maxRetries: number;
5515
5994
  }
5516
5995
  /**
5517
- * A named entity extracted from the conversation, with an optional
5518
- * classification. `kind` is omitted when the model gave no kind or an
5519
- * unrecognized one — see {@link validateCandidates}.
5996
+ * Status of a wallet's queue.
5520
5997
  */
5521
- interface ExtractedEntity {
5522
- name: string;
5523
- kind?: EntityKind;
5998
+ interface QueueStatus {
5999
+ /** Number of pending operations */
6000
+ pending: number;
6001
+ /** Number of operations that failed all retries */
6002
+ failed: number;
6003
+ /** Whether the queue is currently being flushed */
6004
+ isFlushing: boolean;
6005
+ /** Whether the queue is paused (e.g., wallet disconnected) */
6006
+ isPaused: boolean;
5524
6007
  }
5525
- interface ExtractedCandidate {
5526
- content: string;
5527
- type: FactType;
5528
- confidence: number;
5529
- sourceMessageIds: string[];
5530
- entities: ExtractedEntity[];
5531
- /** W6 temporal lane — when the event in this fact occurred. Resolved
5532
- * to absolute timestamps by the LLM; null when the fact has no
5533
- * temporal anchor. */
5534
- eventTime: {
5535
- kind: "point" | "range" | "ongoing";
5536
- /** Unix ms timestamp of the event start (or point). */
5537
- start: number;
5538
- /** Unix ms timestamp of the event end. Only set when kind='range'. */
5539
- end: number | null;
5540
- } | null;
6008
+ /**
6009
+ * Result of a flush operation.
6010
+ */
6011
+ interface FlushResult {
6012
+ /** IDs of operations that succeeded */
6013
+ succeeded: string[];
6014
+ /** Operations that failed with their errors */
6015
+ failed: Array<{
6016
+ id: string;
6017
+ error: string;
6018
+ }>;
6019
+ /** Total number of operations attempted */
6020
+ total: number;
5541
6021
  }
5542
6022
  /**
5543
- * Auth + endpoint for the extraction LLM call. Auth is the dual pattern —
5544
- * one of `apiKey` / `getToken` is required at runtime; see
5545
- * {@link PortalLlmAuth}.
6023
+ * Encryption context needed to execute queued operations.
5546
6024
  */
5547
- interface ExtractFactsOptions extends PortalLlmAuth {
5548
- baseUrl?: string;
5549
- model?: string;
5550
- /** Override the global fetch implementation (useful for tests). */
5551
- fetchFn?: typeof fetch;
6025
+ interface QueueEncryptionContext {
6026
+ walletAddress: string;
6027
+ signMessage: SignMessageFn;
6028
+ embeddedWalletSigner?: EmbeddedWalletSignerFn;
6029
+ }
6030
+ /**
6031
+ * Executor function that runs a single queued operation.
6032
+ * Provided by the consumer (e.g., useChatStorage) during flush.
6033
+ */
6034
+ type OperationExecutor = (operation: QueuedOperation, encryptionContext: QueueEncryptionContext) => Promise<void>;
6035
+
6036
+ /**
6037
+ * Queue Manager
6038
+ *
6039
+ * In-memory queue for database operations when encryption keys aren't yet available.
6040
+ * Operations are held in memory and flushed to the database once the key becomes available.
6041
+ *
6042
+ * Key design decisions:
6043
+ * - In-memory only: queue is lost on page refresh (acceptable since user must re-auth anyway)
6044
+ * - Per-wallet isolation: each wallet has its own queue
6045
+ * - Dependency tracking: operations are flushed in correct order (conversation before messages)
6046
+ * - Max 1000 operations per wallet to prevent memory leaks
6047
+ */
6048
+
6049
+ declare class QueueManager {
6050
+ /** Per-wallet operation queues: wallet address -> operation id -> operation */
6051
+ private queues;
6052
+ /** Per-wallet flushing state */
6053
+ private flushing;
6054
+ /** Per-wallet paused state */
6055
+ private paused;
6056
+ /** Per-wallet failed operations (exceeded max retries) */
6057
+ private failedOps;
6058
+ /** Change listeners: wallet address -> callbacks */
6059
+ private listeners;
5552
6060
  /**
5553
- * The user's own name(s) / handle(s) (e.g. profile nickname, wallet display
5554
- * name). Candidates whose entire content is just one of these are dropped —
5555
- * a personal memory system already knows who the user is, so "Peter Lee" is
5556
- * circular noise. Optional; when omitted only the bare-fragment gate applies.
6061
+ * Queue a new operation for a wallet.
6062
+ * @returns The operation ID, or null if queue is full.
5557
6063
  */
5558
- userIdentity?: string[];
6064
+ queueOperation(walletAddress: string, type: QueuedOperationType, payload: Record<string, any>, dependencies?: string[], maxRetries?: number): string | null;
5559
6065
  /**
5560
- * Reference "now" (Unix ms) for resolving relative temporal phrases in the
5561
- * transcript ("yesterday", "next week", "in two days") into the absolute
5562
- * YYYY-MM-DD anchors the W6 temporal lane indexes on. The transcript itself
5563
- * carries no timestamps, so without an anchor the model resolves relatives
5564
- * against its own training-cutoff guess and emits wrong `eventTime` dates.
5565
- * Defaults to `Date.now()`. Override for back-dated eval corpora and
5566
- * deterministic tests (mirrors {@link RecallOptions.now}).
5567
- *
5568
- * Server-side timezone note: the ms value is formatted to a calendar date in
5569
- * the process's local timezone (same basis as `parseLocalCalendarDay`). On a
5570
- * UTC server, a user near midnight in a non-UTC offset can get the wrong
5571
- * calendar day. Pass the user's local-midnight timestamp as `now` when the
5572
- * process timezone doesn't match the user's.
6066
+ * Get all pending operations for a wallet, sorted by dependency order.
5573
6067
  */
5574
- now?: number;
6068
+ getOperations(walletAddress: string): QueuedOperation[];
5575
6069
  /**
5576
- * Max attempts for the extraction call on a transient failure (default 3).
5577
- * Lower it to bound how long extraction can hold a turn open — e.g. a worker
5578
- * that runs extraction behind an in-flight-turn guard can pass `2` to keep
5579
- * repeated failures from delaying later turns.
6070
+ * Remove a specific operation from the queue.
5580
6071
  */
5581
- maxAttempts?: number;
5582
- /** Per-attempt timeout (ms) for the extraction call. Defaults to the portal
5583
- * helper's 60s. Combine with {@link maxAttempts} to cap the total time budget. */
5584
- timeoutMs?: number;
6072
+ removeOperation(walletAddress: string, operationId: string): void;
5585
6073
  /**
5586
- * Absolute wall-clock budget (ms) across ALL extraction attempts incl. backoff.
5587
- * When set, the loop stops before an attempt that would exceed it, so worst-case
5588
- * latency is bounded rather than `maxAttempts × timeoutMs`.
6074
+ * Get the status of a wallet's queue.
5589
6075
  */
5590
- totalTimeoutMs?: number;
6076
+ getStatus(walletAddress: string): QueueStatus;
5591
6077
  /**
5592
- * Override the retry backoff (ms) for a given 1-based attempt index. The
5593
- * extraction call retries transient failures internally (default exponential
5594
- * backoff); pass `() => 0` to retry without delay (useful for tests).
6078
+ * Flush all queued operations for a wallet by executing them with encryption.
6079
+ *
6080
+ * @param encryptionContext - Wallet address and signing functions for encryption
6081
+ * @param executor - Function that executes each operation against the database
6082
+ * @returns Result with succeeded/failed operation IDs
5595
6083
  */
5596
- backoffMs?: (attempt: number) => number;
6084
+ flush(encryptionContext: QueueEncryptionContext, executor: OperationExecutor): Promise<FlushResult>;
5597
6085
  /**
5598
- * When set, PII (emails, phones, SSNs, cards, IPs, API keys, …) in the
5599
- * conversation transcript is replaced with tagged placeholders before the
5600
- * extraction call, and the returned facts + entities are de-anonymized so the
5601
- * vault keeps the real values while raw PII never reaches the extraction
5602
- * model (and, via `extractAndRetain`, the consolidation model). Pass `true`
5603
- * for a fresh per-call redactor, or a shared {@link PiiRedactor} to keep
5604
- * placeholder numbering consistent with other calls.
5605
- *
5606
- * NOTE: this does NOT cover the embeddings provider. Facts are stored and
5607
- * embedded with their real values, so to keep PII out of embedding requests
5608
- * set `RetainContext.embeddingOptions.maskInput` (e.g. `redactor.maskText`)
5609
- * as well — the two are independent switches.
6086
+ * Clear all queued operations for a wallet.
5610
6087
  */
5611
- piiRedaction?: boolean | PiiRedactor;
6088
+ clear(walletAddress: string): void;
5612
6089
  /**
5613
- * Called when the extraction LLM returned no usable result after exhausting
5614
- * its retries (empty/malformed completion, network/HTTP error) — i.e. a
5615
- * *failure* that drops the turn's facts, as opposed to a legitimate
5616
- * `{candidates: []}` "nothing durable here". Lets callers distinguish a
5617
- * silently-degrading extractor from quiet turns (the two are otherwise
5618
- * indistinguishable). See {@link extractAndRetain}'s `outcome`.
6090
+ * Pause the queue for a wallet (stops flush mid-way).
5619
6091
  */
5620
- onExhaustedEmpty?: () => void;
6092
+ pause(walletAddress: string): void;
5621
6093
  /**
5622
- * Called when the extractor DID produce candidates but PII de-anonymization
5623
- * dropped every one of them — the model mangled its placeholders (so they
5624
- * can't be restored to real values) or restoring the values blew the length
5625
- * cap. These drops happen before `retain()`, so `failedCount` can't see them,
5626
- * and the turn would otherwise masquerade as a quiet `no-facts` result. Lets
5627
- * H3's `outcome` surface `dropped-after-redaction` so a rising PII-drop rate
5628
- * (i.e. redaction silently eating facts) is alarmable.
6094
+ * Resume the queue for a wallet.
6095
+ */
6096
+ resume(walletAddress: string): void;
6097
+ /**
6098
+ * Register a listener for queue changes on a wallet.
6099
+ * @returns Unsubscribe function
5629
6100
  */
5630
- onCandidatesDropped?: () => void;
6101
+ onQueueChange(walletAddress: string, callback: () => void): () => void;
6102
+ /**
6103
+ * Check if a wallet has any pending operations.
6104
+ */
6105
+ hasPending(walletAddress: string): boolean;
6106
+ /** Move a failed operation to the failed set. */
6107
+ private moveToFailed;
6108
+ /** Notify all listeners for a wallet. */
6109
+ private notifyListeners;
5631
6110
  }
6111
+ /** Singleton queue manager instance */
6112
+ declare const queueManager: QueueManager;
6113
+
5632
6114
  /**
5633
- * Stage 1 — call the LLM to extract candidate facts from the recent
5634
- * conversation. Returns post-validated candidates only (confidence
5635
- * threshold, source-id check, length cap, schema validation). Returns
5636
- * an empty array if the LLM emits malformed JSON or no candidates.
5637
- *
5638
- * A null from `callPortalJsonCompletion` means a *failure* (empty completion,
5639
- * malformed JSON, network/HTTP error) — distinct from a successful
5640
- * `{candidates: []}`, which parses to a non-null object. A failed extraction
5641
- * silently drops the whole turn's memories, so transient failures matter:
5642
- * `callPortalJsonCompletion` retries them internally with backoff (default 3
5643
- * attempts), so a one-off empty/malformed completion from a reasoning-class
5644
- * model doesn't lose the turn. Only an exhausted-retry null reaches here.
5645
- */
5646
- declare function extractFacts(messages: AutoExtractMessage[], options: ExtractFactsOptions): Promise<ExtractedCandidate[]>;
5647
- /**
5648
- * Outcome of the EXTRACTOR stage for a turn — independent of whether the
5649
- * subsequent `retain()` writes landed (that's `failedCount`):
5650
- * - `extracted` — the extractor produced at least one candidate above
5651
- * the confidence floor. Whether those writes succeeded
5652
- * is reported separately by `failedCount` — so
5653
- * `extracted` + `failedCount > 0` means "found facts but
5654
- * writes are failing", which is more signal than
5655
- * collapsing it into `no-facts` would give.
5656
- * - `no-facts` — the extractor ran fine but found nothing durable.
5657
- * - `empty-after-retry` — the extractor returned empty/malformed after
5658
- * exhausting retries (a *failure*). Distinguishing this
5659
- * from `no-facts` is what makes a silently-degrading
5660
- * extractor alarmable rather than invisible.
5661
- * - `dropped-after-redaction` — the extractor found facts but PII
5662
- * de-anonymization dropped every one (mangled
5663
- * placeholders / over-cap after restore), before retain.
5664
- * A degradation `failedCount` can't see; would otherwise
5665
- * look like `no-facts`.
5666
- */
5667
- type ExtractOutcome = "extracted" | "no-facts" | "empty-after-retry" | "dropped-after-redaction";
5668
- /**
5669
- * Stage 2 — for each extracted candidate, call retain() with auto-merge
5670
- * enabled. When `consolidateOptions` are wired, retain()'s consolidation pass
5671
- * is the "resolver": a second LLM call against the top-K existing vault
5672
- * memories that decides create/update/noop/supersede — including retiring a
5673
- * stale value on a state change (the supersession the extraction prompt above
5674
- * promises). Without consolidation, retain() still dedups at the cosine-merge
5675
- * level, and the read-time supersession heuristic remains the fallback.
6115
+ * Wallet Poller
5676
6116
  *
5677
- * Returns the candidates that survived validation along with the retain
5678
- * result for each (which captures whether the fact was created, merged,
5679
- * or skipped), plus an `outcome` summarizing why the turn did/didn't produce
5680
- * facts (see {@link ExtractOutcome}).
6117
+ * Polls for embedded wallet availability during Privy initialization.
6118
+ * When a wallet becomes available, triggers a callback so the queue can flush.
5681
6119
  */
5682
- declare function extractAndRetain(messages: AutoExtractMessage[], retainCtx: RetainContext, options: {
5683
- extract: ExtractFactsOptions;
5684
- minConfidence?: number;
5685
- /** Override scope/folder for all retained facts. */
5686
- scope?: string;
5687
- folderId?: string | null;
5688
- /**
5689
- * Forwarded verbatim to each retain() call — enables the LLM-based
5690
- * consolidation pass (Hindsight facet-dedup) on every write. See
5691
- * `RetainOptions.consolidateOptions` for auth + observability fields.
5692
- */
5693
- consolidateOptions?: RetainOptions["consolidateOptions"];
6120
+ declare class WalletPoller {
6121
+ private timerId;
6122
+ private attempts;
5694
6123
  /**
5695
- * When provided, persist each candidate's `entities[]` to the
5696
- * entity + memory_entity tables after a successful retain. This
5697
- * powers the W5 graph retrieval lane — recall() can query for
5698
- * memories sharing entities with the user's question.
6124
+ * Start polling for wallet availability.
6125
+ *
6126
+ * @param checkWallet - Returns wallet address when ready, null if not yet available
6127
+ * @param onWalletReady - Called with the wallet address when it becomes available
6128
+ * @param intervalMs - Polling interval in milliseconds (default: 1000ms)
6129
+ * @param maxAttempts - Maximum polling attempts before giving up (default: 60)
6130
+ * @returns Stop function to cancel polling
5699
6131
  */
5700
- entityCtx?: EntityOperationsContext;
6132
+ startPolling(checkWallet: () => Promise<string | null>, onWalletReady: (address: string) => void, intervalMs?: number, maxAttempts?: number): () => void;
5701
6133
  /**
5702
- * Per-candidate failure hook — invoked once per filtered candidate
5703
- * whose `retain()` call threw. Lets UI layers surface "couldn't save
5704
- * X" toasts; without it consumers only see the aggregate
5705
- * `failedCount` and can't name which facts dropped.
6134
+ * Stop polling.
5706
6135
  */
5707
- onCandidateFailed?: (candidate: ExtractedCandidate, error: unknown) => void;
5708
- }): Promise<{
5709
- candidates: ExtractedCandidate[];
5710
- results: RetainResult[];
5711
- failedCount: number;
5712
- outcome: ExtractOutcome;
5713
- }>;
6136
+ stop(): void;
6137
+ }
5714
6138
 
5715
6139
  /**
5716
6140
  * Auto-extraction worker — orchestration glue for wiring
@@ -5770,6 +6194,16 @@ interface MemoryExtractedEvent {
5770
6194
  result: RetainResult;
5771
6195
  conversationId?: string;
5772
6196
  }
6197
+ /**
6198
+ * Tier-0 security (PR3) — fired once per candidate the injection screen
6199
+ * quarantined and persisted as an audit row. Distinct from
6200
+ * {@link MemoryExtractedEvent} so a client can render "held for review"
6201
+ * without treating a poisoned fact as a normal saved memory.
6202
+ * @public
6203
+ */
6204
+ interface MemoryQuarantinedEvent extends QuarantinedMemoryInfo {
6205
+ conversationId?: string;
6206
+ }
5773
6207
  /** @public */
5774
6208
  interface TurnSkippedEvent {
5775
6209
  /**
@@ -5908,6 +6342,12 @@ interface CreateAutoExtractorOptions {
5908
6342
  };
5909
6343
  /** Per-fact event — fires once per memory written. */
5910
6344
  onMemoryExtracted?: (event: MemoryExtractedEvent) => void;
6345
+ /**
6346
+ * Tier-0 security (PR3) — fires once per candidate quarantined by the
6347
+ * injection screen (and persisted as an audit row). Lets a client surface a
6348
+ * "held for review" state instead of the fact silently disappearing.
6349
+ */
6350
+ onMemoryQuarantined?: (event: MemoryQuarantinedEvent) => void;
5911
6351
  /** Per-turn event — fires once after the whole pipeline finishes. */
5912
6352
  onTurnComplete?: (event: TurnCompleteEvent) => void;
5913
6353
  /** Diagnostic — fires when a turn is skipped. */
@@ -5968,6 +6408,536 @@ declare const DEFAULT_CHUNK_CACHE_SIZE = 5000;
5968
6408
  */
5969
6409
  declare function createChunkVectorCache(maxSize?: number): ChunkVectorCache;
5970
6410
 
6411
+ /**
6412
+ * Temporal decay classifier (PR2) — pure, DB-free, unit-testable.
6413
+ *
6414
+ * Decides whether a stored memory should be kept, archived (soft, recoverable),
6415
+ * or hard-deleted, based ONLY on its plaintext columns. This is the
6416
+ * zero-knowledge core of the decay sweep: it never sees `content` and never
6417
+ * triggers a signature/decrypt, so it can run wherever a wallet key is NOT
6418
+ * loaded. The sweep worker ({@link ./decayWorker}) feeds it raw rows selected
6419
+ * via `unsafeFetchRaw` and materializes Models only for the transitions.
6420
+ *
6421
+ * Lifecycle (mirrors the timestamp state machine in db/schema.ts):
6422
+ * active → archived (set `archived_at`) — recoverable, drops out of recall
6423
+ * archived → deleted (set `is_deleted`) — terminal, after HARD_DELETE_WINDOW
6424
+ *
6425
+ * Per-type TTL policy (the "becomes past" intuition):
6426
+ * - identity / preference / relationship / constraint → never age-archive
6427
+ * (durable facts about who the user is; Infinity TTL).
6428
+ * - plan / ongoing_context → short TTL (they go stale as time moves on).
6429
+ * - other / null (untyped/legacy) → medium TTL.
6430
+ * `source === "manual"` memories are protected from AUTO-ARCHIVE (user-curated)
6431
+ * — but NOT from the hard-delete clock once they are archived: archive is the
6432
+ * shared purge buffer for every row, so an already-archived manual memory is
6433
+ * still deleted after the window (the archived check runs before the manual
6434
+ * short-circuit by design).
6435
+ */
6436
+ /** The verdict for a single memory. */
6437
+ type DecayVerdict = "keep" | "archive" | "delete";
6438
+ /** Short TTL — `plan` / `ongoing_context` age out ~30 days after last touch. */
6439
+ declare const SHORT_TTL_MS: number;
6440
+ /** Medium TTL — `other` / untyped rows age out ~180 days after last touch. */
6441
+ declare const MEDIUM_TTL_MS: number;
6442
+ /**
6443
+ * "Never" TTL — durable identity-class types (identity/preference/relationship/
6444
+ * constraint) never age-archive. Expressed as +Infinity so the age comparison
6445
+ * `now - updatedAt > ttl` is always false.
6446
+ */
6447
+ declare const NEVER_TTL_MS: number;
6448
+ /**
6449
+ * How long an archived memory lingers (recoverable) before the sweep hard-
6450
+ * deletes it. ~30 days gives the user a window to restore an over-eager decay.
6451
+ */
6452
+ declare const HARD_DELETE_WINDOW_MS: number;
6453
+ /**
6454
+ * Grace applied after a `plan` / `ongoing_context` event's `event_time_end`
6455
+ * before it's considered "become past" and archived. Avoids archiving a plan
6456
+ * the instant its end date passes (the user may still reference it briefly).
6457
+ */
6458
+ declare const PAST_EVENT_GRACE_MS: number;
6459
+ /**
6460
+ * Per-type TTL + window overrides. Pass a partial to {@link classifyDecay} /
6461
+ * the sweeper to tune behavior (evaluation harnesses, tests). Anything omitted
6462
+ * falls back to {@link DEFAULT_DECAY_POLICY}.
6463
+ */
6464
+ interface DecayPolicy {
6465
+ /**
6466
+ * Age TTL (ms since `updatedAt`) per FactType. A type absent from this map —
6467
+ * or a null/unknown `factType` — uses {@link DecayPolicy.fallbackTtlMs}.
6468
+ */
6469
+ ttlByType: Record<string, number>;
6470
+ /** TTL for `null` / unknown fact types (the medium bucket). */
6471
+ fallbackTtlMs: number;
6472
+ /** How long an archived row lingers before hard delete. */
6473
+ hardDeleteWindowMs: number;
6474
+ /** Grace after a past `event_time_end` before archiving a plan/ongoing. */
6475
+ pastEventGraceMs: number;
6476
+ }
6477
+ /** Default policy — the locked per-type TTLs. */
6478
+ declare const DEFAULT_DECAY_POLICY: DecayPolicy;
6479
+ /**
6480
+ * The minimal plaintext shape {@link classifyDecay} reads. Deliberately excludes
6481
+ * `content` (encrypted; never touched by decay) — the sweep selects exactly
6482
+ * these columns so it stays zero-knowledge.
6483
+ */
6484
+ interface DecayInput {
6485
+ /**
6486
+ * The row's stable id. Not read by the rule engine ({@link classifyDecay}
6487
+ * ignores it) — it is threaded through so an optional content-reading decay
6488
+ * classifier (PR5, {@link createLlmDecayClassifier}) can fetch + decrypt the
6489
+ * row for a borderline verdict. Optional so pure rule-based callers/tests can
6490
+ * omit it.
6491
+ */
6492
+ id?: string;
6493
+ /** The extractor's FactType, or null (legacy/manual/untyped → medium bucket). */
6494
+ factType: string | null;
6495
+ /** W6 temporal lane — Unix ms the event ended (range/ongoing), or null. */
6496
+ eventTimeEnd: number | null;
6497
+ /** W6 temporal lane — `point | range | ongoing | null`. */
6498
+ eventTimeKind: string | null;
6499
+ /** Unix ms of the row's last write (re-observation resets this). */
6500
+ updatedAt: number;
6501
+ /** Unix ms when archived, or null when active. */
6502
+ archivedAt: number | null;
6503
+ /** `manual` | `auto-extracted` | `capsule` | null. Manual is never decayed. */
6504
+ source: string | null;
6505
+ /**
6506
+ * `trusted` | `quarantined` | null. Not read by the rule engine
6507
+ * ({@link classifyDecay} ignores it — quarantined rows still age/archive/delete
6508
+ * by RULE). It exists only so the sweeper's `isBorderline` can keep a
6509
+ * quarantined (injection-screened) row away from the optional content-reading
6510
+ * classifier, so poison content never egresses. Optional so pure rule-based
6511
+ * callers/tests can omit it (treated as not quarantined).
6512
+ */
6513
+ trustTier?: string | null;
6514
+ }
6515
+ /**
6516
+ * The age TTL for a fact type. `null` / unknown types fall to the medium
6517
+ * fallback bucket (never Infinity — untyped rows must still eventually age out).
6518
+ */
6519
+ declare function ttlForType(factType: string | null, policy?: Partial<DecayPolicy>): number;
6520
+ /**
6521
+ * Classify one memory into a decay verdict, reading only plaintext fields.
6522
+ *
6523
+ * Rules, in order (first match wins):
6524
+ * 0. Non-finite timestamps → keep (safe) + warn (corrupt row, observable).
6525
+ * 1. Already archived (`archivedAt != null`): delete once past the hard-delete
6526
+ * window, else keep — runs REGARDLESS of `source`, so the purge clock
6527
+ * applies to archived manual rows too.
6528
+ * 2. `source === "manual"` → keep — protects manual saves from AUTO-ARCHIVE
6529
+ * only (rules 3/4). Delete-of-already-archived is handled above.
6530
+ * 3. `plan` / `ongoing_context` whose event has become past — `eventTimeEnd`
6531
+ * is set AND older than `now - grace` → archive. An `ongoing` status with a
6532
+ * null `eventTimeEnd` is still ongoing, so this rule does not fire for it.
6533
+ * 4. Age fallback — `now - updatedAt > TTL(factType)` → archive. Durable types
6534
+ * have Infinity TTL, so this never fires for them; null/unknown use medium.
6535
+ * EXCEPTION: a `plan`/`ongoing_context` with a still-upcoming event
6536
+ * (`eventTimeEnd >= now`) is kept — it must not age-archive before the event
6537
+ * happens; only rule 3 (after it passes) may archive it.
6538
+ * 5. Otherwise → keep.
6539
+ *
6540
+ * @param m Plaintext decay inputs for one row.
6541
+ * @param now Reference time (Unix ms). Injected for determinism.
6542
+ * @param policy Optional partial policy overriding {@link DEFAULT_DECAY_POLICY}.
6543
+ */
6544
+ declare function classifyDecay(m: DecayInput, now: number, policy?: Partial<DecayPolicy>): DecayVerdict;
6545
+
6546
+ /**
6547
+ * Decay sweeper (PR2) — orchestration glue that runs the temporal-decay
6548
+ * lifecycle over the vault. Mirrors the shape of {@link ./autoExtractWorker}'s
6549
+ * `createAutoExtractor`: created once, driven by the caller (a client hook on a
6550
+ * low-frequency interval / app-foreground), disposable.
6551
+ *
6552
+ * A sweep is zero-knowledge by construction:
6553
+ * 1. {@link getDecayCandidatesRawOp} selects plaintext columns via
6554
+ * `unsafeFetchRaw` — no `content`, no decrypt, no Model per row.
6555
+ * 2. {@link classifyDecay} is a pure function over those plaintext fields.
6556
+ * 3. Only the (usually small) transition set is materialized as Models to
6557
+ * archive ({@link archiveVaultMemoryOp}) or hard-delete
6558
+ * ({@link hardDeleteDecayedOp}, which re-checks archived + past-window
6559
+ * inside the write so a concurrent restore wins).
6560
+ *
6561
+ * The optional {@link DecayClassifier} is a PR5 seam for an on-device model that
6562
+ * refines borderline verdicts by reading decrypted content — it is NOT
6563
+ * implemented here. Default is `undefined` → pure rule-based, which is what
6564
+ * keeps the default path zero-knowledge.
6565
+ */
6566
+
6567
+ /** Counts from one sweep, for UI surfacing (e.g. "N memories archived"). */
6568
+ interface DecaySweepResult {
6569
+ /** Rows transitioned active → archived this sweep. */
6570
+ archived: number;
6571
+ /** Rows hard-deleted (archived past the window) this sweep. */
6572
+ deleted: number;
6573
+ /** Total candidate rows scanned (all non-hard-deleted rows). */
6574
+ scanned: number;
6575
+ }
6576
+ /**
6577
+ * PR5 seam — an on-device classifier that refines the rule-based verdict for
6578
+ * borderline rows (e.g. type `other`/null, or a `plan` without an event end).
6579
+ * Because it may read decrypted content, callers MUST gate providing it on
6580
+ * wallet-key availability. Left undefined here → pure rule-based sweep.
6581
+ *
6582
+ * Egress is bounded by the sweeper (NOT the classifier): each sweep caps how
6583
+ * many rows reach the classifier ({@link CreateDecaySweeperOptions.maxClassifierCallsPerSweep})
6584
+ * and a row whose (id, updated_at) was already classified is never re-sent on a
6585
+ * later sweep — so a stable borderline "keep" row egresses at most once.
6586
+ *
6587
+ * SECURITY (MEDIUM, residual) — enabling a classifier hands whoever answers the
6588
+ * portal (incl. a malicious / MITM'd endpoint) a lever over the affected rows:
6589
+ * a hostile verdict can only quarantine-adjacent OUTCOMES here, i.e. archive a
6590
+ * row (reversible — never a hard delete, which stays the deterministic
6591
+ * archived-past-window mechanic). Bounded, reversible trust tradeoff; gate the
6592
+ * classifier on trust in the portal.
6593
+ *
6594
+ * @param input The same plaintext inputs the rule engine saw.
6595
+ * @param ruleVerdict The rule-based verdict, as a starting point / fallback.
6596
+ * @param now The sweep's reference time (Unix ms). Injected so the
6597
+ * classifier derives any age math from the same clock the rule engine used,
6598
+ * never wall-clock `Date.now()` — keeping a fixed-`now` sweep deterministic.
6599
+ * @returns The (possibly refined) verdict.
6600
+ */
6601
+ interface DecayClassifier {
6602
+ classify(input: DecayInput, ruleVerdict: DecayVerdict, now: number): Promise<DecayVerdict> | DecayVerdict;
6603
+ }
6604
+ /** A clock: a fixed timestamp (tests) or a getter (production intervals). */
6605
+ type NowSource = number | (() => number);
6606
+ /** @public */
6607
+ interface CreateDecaySweeperOptions {
6608
+ /** Vault write context — the same one recall/retain use. */
6609
+ vaultCtx: VaultMemoryOperationsContext;
6610
+ /**
6611
+ * Reference "now". A number is fixed (deterministic tests); a function is
6612
+ * re-evaluated per sweep (long-lived interval usage). Default `Date.now`.
6613
+ */
6614
+ now?: NowSource;
6615
+ /** Partial policy overriding the per-type TTL defaults. Omit for defaults. */
6616
+ policy?: Partial<DecayPolicy>;
6617
+ /**
6618
+ * PR5 seam. When provided, borderline candidates' rule verdict is passed
6619
+ * through it. Gate on key availability (it may decrypt content). Default
6620
+ * undefined. Egress is bounded — see {@link maxClassifierCallsPerSweep}.
6621
+ */
6622
+ classifier?: DecayClassifier;
6623
+ /**
6624
+ * PR5 — hard ceiling on classifier invocations (and thus decrypted-content
6625
+ * portal egress) per sweep. Once hit, the remaining borderline rows fall back
6626
+ * to the rule verdict for that sweep (no call). Prevents a large vault from
6627
+ * firing hundreds of sequential content-bearing calls in one sweep. Default
6628
+ * {@link DEFAULT_MAX_CLASSIFIER_CALLS_PER_SWEEP} (20). Cache hits (a row
6629
+ * already classified at its current `updated_at`) do NOT count against this.
6630
+ */
6631
+ maxClassifierCallsPerSweep?: number;
6632
+ /** Fires once after each sweep with the transition counts (UI). */
6633
+ onSwept?: (result: DecaySweepResult) => void;
6634
+ /** Diagnostic — fires on an unexpected sweep-level error. */
6635
+ onError?: (error: Error) => void;
6636
+ }
6637
+ /** @public */
6638
+ interface DecaySweeper {
6639
+ /**
6640
+ * Scan the vault, classify every candidate, and apply archive/delete
6641
+ * transitions. Safe to call repeatedly (idempotent — a keep stays a keep, an
6642
+ * already-archived row won't re-archive). Returns the transition counts.
6643
+ * A no-op (returns zero counts) after {@link DecaySweeper.dispose}.
6644
+ */
6645
+ runSweep(): Promise<DecaySweepResult>;
6646
+ /** Stop accepting sweeps. An in-flight `runSweep()` resolves normally. */
6647
+ dispose(): void;
6648
+ }
6649
+ /**
6650
+ * Default per-sweep ceiling on decay-classifier invocations (see
6651
+ * {@link CreateDecaySweeperOptions.maxClassifierCallsPerSweep}). Kept small: the
6652
+ * classifier egresses DECRYPTED content, so a sweep must never fan out to
6653
+ * hundreds of sequential calls. 20 covers the borderline churn of a typical
6654
+ * sweep while capping worst-case egress; stable rows are also cached so this
6655
+ * ceiling is rarely reached after the first sweep.
6656
+ */
6657
+ declare const DEFAULT_MAX_CLASSIFIER_CALLS_PER_SWEEP = 20;
6658
+ /**
6659
+ * Create a decay sweeper. See module docstring for the zero-knowledge contract.
6660
+ */
6661
+ declare function createDecaySweeper(options: CreateDecaySweeperOptions): DecaySweeper;
6662
+
6663
+ /**
6664
+ * Optional content-reading decay classifier (PR5).
6665
+ *
6666
+ * The rule engine ({@link ./decay}.classifyDecay) decides keep/archive/delete
6667
+ * from plaintext columns ALONE — it never sees `content`, which is what keeps
6668
+ * the default decay sweep zero-knowledge. That is the right default, but it is
6669
+ * weakest on BORDERLINE rows: an `other`/untyped fact, or a `plan` with no
6670
+ * event end, whose staleness is a pure `updated_at` guess.
6671
+ *
6672
+ * This factory builds a {@link DecayClassifier} that reads the DECRYPTED content
6673
+ * of such a row and returns keep-vs-archive. The sweeper consults it ONLY for a
6674
+ * borderline row whose CURRENT rule verdict is `keep` — a rule archive/delete
6675
+ * short-circuits before the classifier ever runs (see `decayWorker.verdictFor` +
6676
+ * `isBorderline`). So its whole job is to REFINE a still-kept borderline row
6677
+ * toward EARLIER archive when the content shows it is genuinely ephemeral (a
6678
+ * finished one-off plan, an expired temporary state) — turning a weak age-only
6679
+ * "keep" into an "archive" sooner.
6680
+ *
6681
+ * DIRECTIONALITY IS DELIBERATE (a security property, not a shortcoming): it can
6682
+ * move a borderline keep → archive, never the reverse. It CANNOT rescue /
6683
+ * keep-alive a row the rule would archive — the escalation gate makes that path
6684
+ * unreachable, and archive is reversible while un-archiving poison would not be.
6685
+ * Enabling a classifier hands whoever answers the portal (incl. a malicious /
6686
+ * MITM'd endpoint) a lever, and that lever must only ever archive-reversibly.
6687
+ *
6688
+ * ZERO-KNOWLEDGE CONTRACT (the caller MUST honor this):
6689
+ * - `getContent` is supplied by the caller and MUST be gated on wallet-key
6690
+ * availability: return `null` when no key is loaded. With no content the
6691
+ * classifier degrades to the rule verdict — never blocks, never guesses.
6692
+ * - When it does read content and calls the portal, the content is
6693
+ * PII-redacted first. Redaction is OPT-OUT, not opt-in: it defaults ON when
6694
+ * the consumer omits `piiRedaction`, so a caller can never accidentally
6695
+ * egress raw PII. Pass `piiRedaction: false` to deliberately disable it.
6696
+ * - Egress is bounded by the SWEEPER, not this classifier: the sweep caps
6697
+ * invocations per pass and never re-sends an unchanged (id, updated_at) row
6698
+ * (see `decayWorker`), so a stable borderline "keep" row egresses once.
6699
+ * - It only ever chooses keep vs archive, and only for a row the rule already
6700
+ * keeps. It NEVER escalates to `delete` and NEVER un-archives — hard-delete
6701
+ * is exclusively the deterministic archived-past-window mechanic (rule 1),
6702
+ * and a row the rule wants archived is archived regardless of this layer.
6703
+ *
6704
+ * Fails to the rule verdict (keep) on any error, missing id, no key, or
6705
+ * malformed response — like every other optional LLM layer in this subsystem it
6706
+ * can only refine a borderline keep toward archive, never make the sweep worse.
6707
+ *
6708
+ * RESIDUAL SCOPE (accepted): a genuinely durable fact MISTYPED as `other`/null
6709
+ * and never re-mentioned within its medium TTL WILL age-archive — this layer can
6710
+ * only bring that archive EARLIER, never prevent it. Accepted because it is
6711
+ * recoverable: archived rows stay in the Archived section, Restore clears
6712
+ * `archived_at`, and any re-observation resets the TTL. The alternative — a
6713
+ * keep-alive lever — is exactly the capability a hostile portal must NOT have.
6714
+ *
6715
+ * SECURITY (MEDIUM, residual) — this is a NEW portal call surface carrying
6716
+ * (redacted) memory content. It is opt-in (you must construct + pass it) and
6717
+ * gated on key availability. Beyond widening which models see facts, a
6718
+ * malicious / MITM'd portal can steer the verdict — but only to ARCHIVE a row
6719
+ * (reversible; never a hard delete). Gate it on trust in the portal.
6720
+ */
6721
+
6722
+ /**
6723
+ * Auth + wiring for {@link createLlmDecayClassifier}. Auth is the dual pattern —
6724
+ * one of `apiKey` / `getToken` is required at runtime (see {@link PortalLlmAuth}).
6725
+ * @public
6726
+ */
6727
+ interface LlmDecayClassifierOptions extends PortalLlmAuth {
6728
+ baseUrl?: string;
6729
+ model?: string;
6730
+ /** Override fetch (tests). */
6731
+ fetchFn?: typeof fetch;
6732
+ /** Max portal attempts on a TRANSIENT failure. Default 2. */
6733
+ maxAttempts?: number;
6734
+ /** Absolute wall-clock budget across attempts. Default 12s. */
6735
+ totalTimeoutMs?: number;
6736
+ /** Backoff before each retry (ms). Tests pass `() => 0`. */
6737
+ backoffMs?: (attempt: number) => number;
6738
+ /**
6739
+ * PII redaction for the outbound content. OPT-OUT: defaults to ON (a fresh
6740
+ * per-call redactor) when omitted, so decrypted content is never egressed
6741
+ * raw by accident. Pass a shared {@link PiiRedactor} to keep placeholder
6742
+ * numbering consistent with other calls, or `false` to deliberately disable
6743
+ * redaction. The verdict returned is a bare enum — nothing to de-anonymize.
6744
+ */
6745
+ piiRedaction?: boolean | PiiRedactor;
6746
+ /**
6747
+ * Resolve a memory's DECRYPTED content by id. The caller supplies this and
6748
+ * MUST gate it on wallet-key availability — return `null` when no key is
6749
+ * loaded so the classifier degrades to the rule verdict (zero-knowledge).
6750
+ * A throw is treated the same as `null` (fail to the rule verdict).
6751
+ */
6752
+ getContent: (id: string) => Promise<string | null>;
6753
+ }
6754
+ /**
6755
+ * Build a {@link DecayClassifier} that reads a borderline row's decrypted
6756
+ * content and returns a keep/archive verdict via a cheap portal LLM. Pass it as
6757
+ * `createDecaySweeper({ classifier })`. See the module docstring for the
6758
+ * zero-knowledge contract. Returns the rule verdict on any failure and never
6759
+ * escalates to `delete`.
6760
+ * @public
6761
+ */
6762
+ declare function createLlmDecayClassifier(options: LlmDecayClassifierOptions): DecayClassifier;
6763
+
6764
+ /**
6765
+ * Multi-hop graph traversal for the W5 entity-graph recall lane (PR4,
6766
+ * MRAgent-style "active reconstruction").
6767
+ *
6768
+ * The single-hop lane (see `recall.ts:buildGraphLaneRanking`) does one thing:
6769
+ * extract entities from the query, look up memories sharing any of them, and
6770
+ * order those memories by shared-entity count. That is hop 1 — the "seed"
6771
+ * lookup. This module wraps a bounded breadth-first search around it: from the
6772
+ * seed memories it walks OUT to their neighbor entities (via the new reverse
6773
+ * edge {@link getEntitiesByMemoryIdsOp}), ranks those neighbors by
6774
+ * co-occurrence across the frontier, expands the top few, and pulls the
6775
+ * memories they reach — repeating up to {@link MAX_HOPS} times.
6776
+ *
6777
+ * Cost note (why this is cheap): NONE of this runs cosine. Cosine runs exactly
6778
+ * once per recall inside `searchVaultMemoriesWithSize`. This lane only emits an
6779
+ * ordered list of memory IDs that is RRF-fused against the cosine head. So
6780
+ * multi-hop adds indexed WatermelonDB joins + more RRF entries, not embedding
6781
+ * math. The dominant cost of enabling it is the reranker over a slightly larger
6782
+ * candidate pool — which stays bounded because `rerankTopN` remains
6783
+ * authoritative downstream.
6784
+ *
6785
+ * Determinism: neighbor-entity selection is pure co-occurrence counting by
6786
+ * default. An optional LLM path-refiner (PR5) can override which neighbors
6787
+ * expand; it falls back to the deterministic order on any error.
6788
+ *
6789
+ * Hop numbering: hop 1 is the seed lookup. `maxHops = 1` therefore means "seed
6790
+ * only" — byte-for-byte identical to the single-hop lane (the regression guard).
6791
+ * `MAX_HOPS = 2` (the PR5 default) performs one expansion beyond the seed.
6792
+ *
6793
+ * Neighbor selection at each expansion hop is deterministic co-occurrence
6794
+ * counting by default. PR5 adds an OPTIONAL LLM path-refiner
6795
+ * ({@link NeighborRefiner} / {@link createLlmNeighborRefiner}) that lets a model
6796
+ * pick which neighbor entities to expand instead; it is opt-in, capped to ≤1
6797
+ * call per hop, and falls back to the deterministic order on any error.
6798
+ */
6799
+
6800
+ /**
6801
+ * Total hops the traversal performs, counting the seed lookup as hop 1.
6802
+ * `1` = seed only (identical to the single-hop lane). PR5 default is `2` (one
6803
+ * expansion beyond the seed). Overridable per-call for ablation. Still gated to
6804
+ * the `high` budget in recall, and capped back to 1 on large vaults by
6805
+ * {@link capHopsForDensity}.
6806
+ * @public
6807
+ */
6808
+ declare const MAX_HOPS = 2;
6809
+ /**
6810
+ * Max neighbor entities expanded per hop. Caps fan-out so a densely-linked
6811
+ * frontier can't explode the candidate pool. Neighbors are ranked by
6812
+ * co-occurrence frequency across the frontier; only the top this-many expand.
6813
+ * @public
6814
+ */
6815
+ declare const ENTITY_FANOUT = 8;
6816
+ /**
6817
+ * Hard ceiling on total accumulated memory IDs across all hops. The BFS stops
6818
+ * expanding once the accumulated set reaches this size (and the frontier is
6819
+ * bounded to it too), keeping the RRF pool — and the downstream reranker
6820
+ * workload — bounded regardless of graph density.
6821
+ * @public
6822
+ */
6823
+ declare const NODE_BUDGET = 64;
6824
+ /**
6825
+ * Above this vault size, {@link capHopsForDensity} forces `MAX_HOPS = 1`
6826
+ * (seed-only). Fan-out grows with graph density, so on large vaults we don't
6827
+ * pay the expansion cost. Bounded-traversal safety valve.
6828
+ * @public
6829
+ */
6830
+ declare const VAULT_SIZE_HOP_CAP = 1000;
6831
+ /** Options for {@link traverseGraphLane}. All optional; defaults are the
6832
+ * exported constants above. Exposed for ablation / evaluation sweeps.
6833
+ * @public */
6834
+ interface GraphTraversalOptions {
6835
+ /** Total hops incl. the seed lookup (hop 1). Default {@link MAX_HOPS}. */
6836
+ maxHops?: number;
6837
+ /** Max neighbor entities expanded per hop. Default {@link ENTITY_FANOUT}. */
6838
+ entityFanout?: number;
6839
+ /** Hard cap on accumulated memory IDs. Default {@link NODE_BUDGET}. */
6840
+ nodeBudget?: number;
6841
+ /** RRF smoothing constant for per-hop fusion. Default 60 (rrf.ts). */
6842
+ rrfK?: number;
6843
+ /**
6844
+ * Vault size hint. When provided and above {@link VAULT_SIZE_HOP_CAP}, the
6845
+ * effective hop count is capped to 1 (see {@link capHopsForDensity}).
6846
+ */
6847
+ vaultSize?: number;
6848
+ /**
6849
+ * PR5 — optional LLM neighbor-selection. When provided, at each expansion hop
6850
+ * the deterministically-ranked candidate neighbor entities are handed to this
6851
+ * refiner, which returns the subset to expand. Falls back to the
6852
+ * co-occurrence order on any error or empty result. Called at most ONCE per
6853
+ * hop. Build one with {@link createLlmNeighborRefiner}, or supply your own.
6854
+ */
6855
+ refineNeighbors?: NeighborRefiner;
6856
+ /**
6857
+ * Resolve a batch of candidate memory ids to just the ACTIVE ones (not
6858
+ * archived, not quarantined, not soft-deleted — the same set the final recall
6859
+ * gate admits). When provided, traversal drops "forgotten" memories from the
6860
+ * frontier AT EACH HOP so they neither steer neighbor-entity ranking nor
6861
+ * egress their entity names to {@link refineNeighbors}. Omit (tests /
6862
+ * entity-only callers) to traverse over every linked memory. Wire it from a
6863
+ * vault context with {@link ../db/memoryVault/operations.getActiveVaultMemoryIdsOp}.
6864
+ */
6865
+ filterActiveMemoryIds?: (ids: string[]) => Promise<Set<string>>;
6866
+ }
6867
+ /**
6868
+ * Picks which candidate neighbor entities to expand at a traversal hop. Given
6869
+ * the query and the deterministically-ranked candidate entity names, return the
6870
+ * subset (≤ `limit`) to expand. Must be resilient: {@link traverseGraphLane}
6871
+ * falls back to the deterministic top-`limit` on a throw or empty return.
6872
+ * @public
6873
+ */
6874
+ interface NeighborRefiner {
6875
+ refine(query: string, candidates: string[], limit: number): Promise<string[]>;
6876
+ }
6877
+ /**
6878
+ * Cap the hop count on large vaults. Fan-out grows with graph density, so above
6879
+ * {@link VAULT_SIZE_HOP_CAP} memories we force seed-only traversal (1 hop)
6880
+ * rather than pay an unbounded expansion. A no-op when `vaultSize` is unknown or
6881
+ * within the threshold.
6882
+ * @public
6883
+ */
6884
+ declare function capHopsForDensity(maxHops: number, vaultSize?: number): number;
6885
+ /**
6886
+ * Bounded multi-hop entity-graph traversal. Returns an ordered list of memory
6887
+ * IDs (best first) — the SAME output shape as the single-hop lane, so nothing
6888
+ * downstream changes. The caller passes it through as `entityRanking` for RRF
6889
+ * fusion with the cosine/BM25 head.
6890
+ *
6891
+ * With `maxHops <= 1` this returns the seed ordering verbatim, making it a
6892
+ * drop-in equivalent of the single-hop lane (the regression guard). The PR5
6893
+ * default is 2 (one expansion beyond the seed).
6894
+ *
6895
+ * Returns an empty array when the query has no extractable entities or no stored
6896
+ * memory shares a seed entity.
6897
+ *
6898
+ * @public
6899
+ */
6900
+ declare function traverseGraphLane(query: string, entityCtx: EntityOperationsContext, options?: GraphTraversalOptions): Promise<string[]>;
6901
+ /** Auth + tuning for {@link createLlmNeighborRefiner}. Reuses the recall
6902
+ * `decomposeOptions` shape (dual auth — one of `apiKey`/`getToken`). @public */
6903
+ interface LlmNeighborRefinerOptions extends PortalLlmAuth {
6904
+ baseUrl?: string;
6905
+ model?: string;
6906
+ fetchFn?: typeof fetch;
6907
+ maxAttempts?: number;
6908
+ totalTimeoutMs?: number;
6909
+ backoffMs?: (attempt: number) => number;
6910
+ }
6911
+ /**
6912
+ * Build a {@link NeighborRefiner} backed by a cheap portal LLM. At each hop it
6913
+ * asks the model to pick, from the candidate neighbor entities, the ≤`limit`
6914
+ * most relevant to the query, so traversal expands toward the question instead
6915
+ * of purely by co-occurrence frequency.
6916
+ *
6917
+ * Bounded + fail-safe: one attempt by default, a short total timeout, and
6918
+ * {@link traverseGraphLane} falls back to the deterministic co-occurrence order
6919
+ * on any throw or empty result — so enabling this can reorder which neighbors
6920
+ * expand but never breaks or stalls recall.
6921
+ *
6922
+ * SECURITY / ZERO-KNOWLEDGE (must stay default-OFF): this sends the query +
6923
+ * candidate ENTITY NAMES to the portal UNREDACTED. Those names ARE user PII —
6924
+ * people, places, orgs pulled from the stored graph (e.g. "Sara", "Kyoto",
6925
+ * "Acme") — not lower-risk than content just because they're short. It reuses
6926
+ * the query-decompose auth and is opt-in (`RecallOptions.graphRefine`, default
6927
+ * off in recall); leave it off unless you accept that exposure. To bound that
6928
+ * exposure, {@link traverseGraphLane} caps the candidate list it hands this
6929
+ * refiner per hop — at most `MAX_REFINER_CANDIDATES` entity names ever leave per
6930
+ * hop, REGARDLESS of `entityFanout` (the cap is a hard ceiling, not just a
6931
+ * fanout-scaled floor), so the full frontier is never egressed.
6932
+ *
6933
+ * MEDIUM residual: a malicious / MITM'd portal can only steer WHICH neighbor
6934
+ * entities expand — a recall-ranking nudge, not a data-integrity change (no
6935
+ * memory is written, archived, or deleted), and {@link traverseGraphLane}
6936
+ * falls back to deterministic order on any bad/empty response. Bounded tradeoff.
6937
+ * @public
6938
+ */
6939
+ declare function createLlmNeighborRefiner(options: LlmNeighborRefinerOptions): NeighborRefiner;
6940
+
5971
6941
  /**
5972
6942
  * Unified Recall API — single retrieval surface above vault + engine.
5973
6943
  *
@@ -6170,6 +7140,129 @@ declare class RerankerUnavailableError extends Error {
6170
7140
  */
6171
7141
  declare function isRerankerAvailable(): boolean | undefined;
6172
7142
 
7143
+ /**
7144
+ * synthesizeProfile — client-side profile synthesis for People Nearby (C1).
7145
+ *
7146
+ * The vault is E2E-encrypted and device-authoritative; the server cannot read
7147
+ * it. So the shareable profile is synthesized ON-DEVICE here, in the SDK, from
7148
+ * the user's vault facts. The client then publishes the result (PII-redacted,
7149
+ * then server-side moderated in `nearby`) to the server-authoritative profile
7150
+ * store. The SDK stays STATELESS: no profile table, no persistence — the caller
7151
+ * owns storage and passes the prior doc back via `options.previous` for delta
7152
+ * refresh.
7153
+ *
7154
+ * Shape: the profile is decomposed into independent facets (bio, interests,
7155
+ * work/role, …). Each facet is one grounded `reflect()` pass over the vault, so
7156
+ * every section carries the `sourceMemoryIds` it was built from — enabling both
7157
+ * provenance and cheap delta refresh (only regenerate sections whose source
7158
+ * facts changed since the previous doc's `vaultWatermark`).
7159
+ *
7160
+ * Facet decomposition, per-facet synthesis, delta refresh, and the PII gate are
7161
+ * wired end-to-end. New facts are attributed to the facets they're relevant to
7162
+ * (by embedding similarity to each facet query) so an unrelated new fact doesn't
7163
+ * force a full re-synthesis — see {@link attributeFacts}.
7164
+ */
7165
+
7166
+ /** Bump when the ProfileDoc / section shape changes incompatibly. */
7167
+ declare const PROFILE_DOC_VERSION = 1;
7168
+ /** The facets a profile decomposes into (dating-app-style, per the People
7169
+ * Nearby plan). Configurable via {@link SynthesizeProfileOptions.facets}. */
7170
+ type ProfileFacetKey = "bio" | "interests" | "work_role" | "location_context" | "communication_style" | "recent_activity";
7171
+ /** One profile facet: how to recall its evidence and steer its synthesis. */
7172
+ interface ProfileFacet {
7173
+ key: ProfileFacetKey;
7174
+ /** Human-readable section label. */
7175
+ label: string;
7176
+ /** Recall query that pulls the vault facts relevant to this facet. */
7177
+ query: string;
7178
+ /** Facet-specific guidance appended to the synthesis system prompt. */
7179
+ guidance: string;
7180
+ }
7181
+ /** Default dating-app facet set. Order is display order. */
7182
+ declare const DEFAULT_PROFILE_FACETS: ProfileFacet[];
7183
+ /** A synthesized profile section, grounded in specific vault facts. */
7184
+ interface ProfileSection {
7185
+ key: ProfileFacetKey;
7186
+ label: string;
7187
+ /** Synthesized prose (PII-redacted when a redactor is supplied). Empty when
7188
+ * the vault has no evidence for this facet. */
7189
+ text: string;
7190
+ /** Vault memory ids this section was grounded on — provenance + delta refresh. */
7191
+ sourceMemoryIds: string[];
7192
+ /** Unix ms this section was generated. */
7193
+ generatedAt: number;
7194
+ /** True when regeneration failed and a prior section value was carried
7195
+ * forward (e.g. LLM returned empty) — the caller may choose to retry. */
7196
+ stale?: boolean;
7197
+ }
7198
+ /** Fingerprint of the config that produced a {@link ProfileDoc}. Delta reuse
7199
+ * (both the wholesale fast path and per-section reuse) is only valid when the
7200
+ * current call's config matches — otherwise reused sections could carry the
7201
+ * wrong scope's evidence, un-redacted text under a now-present redactor, or an
7202
+ * old section shape. */
7203
+ interface ProfileConfigFingerprint {
7204
+ /** Facet keys present in the doc, sorted. */
7205
+ facetKeys: ProfileFacetKey[];
7206
+ /** Order-independent digest of each facet's full definition (key + label +
7207
+ * query + guidance). Reuse must invalidate when a facet's PROMPT changes, not
7208
+ * just its key set — otherwise reused sections carry text generated under the
7209
+ * old definition. Facet display order does NOT invalidate (sections are
7210
+ * rebuilt in facet order and reused by key). */
7211
+ facetsSignature: string;
7212
+ /** Scopes the facts were drawn from, sorted. */
7213
+ scopes: string[];
7214
+ /** Whether a PII redactor gated the section text. Reusing un-gated text under
7215
+ * a now-present redactor would leak PII, so this flips the fingerprint. */
7216
+ redacted: boolean;
7217
+ }
7218
+ /** A synthesized profile. Server-authoritative once published; the client
7219
+ * caches it and passes it back as {@link SynthesizeProfileOptions.previous}. */
7220
+ interface ProfileDoc {
7221
+ /** {@link PROFILE_DOC_VERSION} at synthesis time. */
7222
+ version: number;
7223
+ /** One section per requested facet (in facet order). */
7224
+ sections: ProfileSection[];
7225
+ /** Max change-time across all vault facts (incl. deleted/superseded) at
7226
+ * synthesis time. Delta refresh regenerates only sections whose source facts
7227
+ * changed since a previous doc's watermark. */
7228
+ vaultWatermark: number;
7229
+ /** The config that produced this doc — see {@link ProfileConfigFingerprint}. */
7230
+ config: ProfileConfigFingerprint;
7231
+ /** Unix ms this doc was produced. */
7232
+ generatedAt: number;
7233
+ }
7234
+ /** Options for {@link synthesizeProfile}. Auth is the dual {@link PortalLlmAuth}
7235
+ * pattern — one of `apiKey` / `getToken` is required at runtime. */
7236
+ interface SynthesizeProfileOptions extends PortalLlmAuth {
7237
+ /** Facets to synthesize. Defaults to {@link DEFAULT_PROFILE_FACETS}. */
7238
+ facets?: ProfileFacet[];
7239
+ /** Prior doc for delta refresh. Unchanged sections are reused verbatim. */
7240
+ previous?: ProfileDoc;
7241
+ /** Synthesis model. Default: open-weights ling-2.6-flash. */
7242
+ llmModel?: string;
7243
+ /** LLM endpoint override. */
7244
+ baseUrl?: string;
7245
+ /** Scopes to draw facts from. Default: ["private"]. */
7246
+ scopes?: string[];
7247
+ /** Facts recalled per facet before synthesis. Default: 20. */
7248
+ limit?: number;
7249
+ /** Override fetch (tests). */
7250
+ fetchFn?: typeof fetch;
7251
+ /** Pre-publish PII gate. When supplied, each section's text is run through
7252
+ * {@link PiiRedactor.redactTextAsync} (regex + NER) before it's returned.
7253
+ * Omit only when the caller redacts downstream — `nearby` also moderates
7254
+ * server-side, but the client should never publish un-gated text. */
7255
+ redactor?: PiiRedactor;
7256
+ }
7257
+ /**
7258
+ * Synthesize a shareable {@link ProfileDoc} from the user's vault, on-device.
7259
+ *
7260
+ * Stateless: pass `options.previous` to reuse unchanged sections (delta refresh)
7261
+ * and the caller persists the result. On per-facet LLM failure the section
7262
+ * falls back to its prior value (marked `stale`) or an empty section.
7263
+ */
7264
+ declare function synthesizeProfile(ctx: RecallContext, options?: SynthesizeProfileOptions): Promise<ProfileDoc>;
7265
+
6173
7266
  /**
6174
7267
  * Standalone topic (entity) extraction for EXISTING memories.
6175
7268
  *
@@ -6998,6 +8091,9 @@ declare function useChatStorage(options: UseChatStorageOptions): UseChatStorageR
6998
8091
  * - v33: Added embedding_model column to memory_vault so stale-model vectors are
6999
8092
  * detectable and re-embeddable after an embedding-model change (null = legacy
7000
8093
  * rows, grandfathered as compatible with the current model)
8094
+ * - v34: Added topics_user_managed column to memory_vault so a memory whose
8095
+ * entity links the user has taken manual control of is left alone by
8096
+ * auto-extraction (null/false = auto-derived, the default)
7001
8097
  * - v35: Added conversation_memory table recording which vault memories a
7002
8098
  * conversation drew on, so the conversation-level Memories panel survives reload
7003
8099
  * - v36: Added topics_extracted_at column to memory_vault — watermark of the last
@@ -7012,8 +8108,17 @@ declare function useChatStorage(options: UseChatStorageOptions): UseChatStorageR
7012
8108
  * logic version a memory was last stamped under. Bumping TOPICS_EXTRACTION_VERSION
7013
8109
  * (new prompt/model) makes the worker re-extract every row whose stored version
7014
8110
  * is behind, so topic-quality improvements propagate across the existing vault
7015
- */
7016
- declare const SDK_SCHEMA_VERSION = 38;
8111
+ * - v39: Added last_observed_at column to memory_vault (C3) — a re-observation
8112
+ * watermark stamped each time retain() merges into an existing fact, kept
8113
+ * distinct from updated_at (which merges preserve). Lets profile synthesis
8114
+ * weight facts by recency of reinforcement rather than last edit.
8115
+ * - v40: Added fact_type, archived_at, trust_tier columns to memory_vault for
8116
+ * typed memory + decay + Tier-0 security. All nullable + plaintext, no
8117
+ * backfill (null = legacy/untyped, active, un-screened — content is
8118
+ * encrypted so in-migration classification is impossible; NULL = zero-risk,
8119
+ * exact embedding_model precedent)
8120
+ */
8121
+ declare const SDK_SCHEMA_VERSION = 40;
7017
8122
  /**
7018
8123
  * Combined WatermelonDB schema for all SDK storage modules.
7019
8124
  *
@@ -7091,9 +8196,13 @@ declare const sdkSchema: Readonly<{
7091
8196
  * - v30 → v31: Added `user_id` column to memory_entity for multi-user scoping of the W5 graph lane (with backfill from memory_vault.user_id)
7092
8197
  * - v31 → v32: Added `pinned_at` column to conversations for pinning chats
7093
8198
  * - v32 → v33: Added `embedding_model` column to memory_vault (null grandfathered as current-model-compatible)
8199
+ * - v33 → v34: Added `topics_user_managed` column to memory_vault (null/false = auto-derived topics, the default)
7094
8200
  * - v34 → v35: Added `conversation_memory` table (conversation ↔ recalled memory ids)
7095
8201
  * - v35 → v36: Added `topics_extracted_at` column to memory_vault (watermark for the background topic-extraction worker; null + existing links grandfathered as extracted)
7096
8202
  * - v36 → v37: Added `superseded_by` + `superseded_at` columns to memory_vault (write-time supersession; null = live, excluded from recall/dedup when set)
8203
+ * - v37 → v38: Added `topics_extracted_version` column to memory_vault (extraction-logic version; null read as 0 so a TOPICS_EXTRACTION_VERSION bump re-extracts stale rows)
8204
+ * - v38 → v39: Added `last_observed_at` column to memory_vault (C3 re-observation watermark; stamped on retain merge, distinct from updated_at)
8205
+ * - v39 → v40: Added `fact_type`, `archived_at`, `trust_tier` columns to memory_vault for typed memory + decay + Tier-0 security (all nullable + plaintext, NULL backfill)
7097
8206
  */
7098
8207
  declare const sdkMigrations: Readonly<{
7099
8208
  validated: true;
@@ -7615,4 +8724,4 @@ declare function refreshNotionAccessToken(params: NotionRefreshTokenParams): Pro
7615
8724
  */
7616
8725
  declare function createNotionTools(getAccessToken: () => string | null, requestNotionAccess: () => Promise<string>): ToolConfig[];
7617
8726
 
7618
- export { type AutoExtractMessage, type AutoExtractor, type Budget, type CachedChunkVectors, type CachedServerTools, Conversation as ChatConversation, Message as ChatMessage, type ChatRole, type ChunkVectorCache, type ChunkingOptions, type ConsolidationFallbackReason, type ConversationMemoryInput, ConversationMemory as ConversationMemoryModel, type ConversationMemoryOperationsContext, type CreateAutoExtractorOptions, type CreateConversationOptions, type CreateMessageOptions, type CreateVaultFolderOptions, type CreateVaultMemoryOptions, type CryptoPriceClassification, type CryptoPricePreProcessorOptions, DEFAULT_CACHE_EXPIRATION_MS, DEFAULT_CHUNK_CACHE_SIZE, DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_MIN_CHUNK_SIZE, DEFAULT_VAULT_CACHE_SIZE, DatabaseManager, type DatabaseManagerLogger, type DatabaseManagerOptions, type EmbeddedWalletSignerFn, type ExtractFactsOptions, type ExtractOutcome, type ExtractedCandidate, type ExtractedEntity, type ExtractionCursorStore, type FactType, type FileMetadata, type FlushResult, type GetConversationsPageOptions, type GetMessagesPageOptions, INFERENCE_ID_HEADER, type LazyStoredConversation, type Logger, LoggerProvider, type LoggerProviderProps, type MemoriesNeedingTopicExtraction, type EmbeddingOptions as MemoryEngineEmbeddingOptions, type MemoryEngineResult, type MemoryEngineSearchOptions, type MemoryExtractedEvent, type MemoryKind, type MemoryVaultSearchOptions, type MemoryVaultToolOptions, type MessageRedactionResult, type MessageSkeleton, NOTION_OAUTH_CONFIG, type NotionAuthUrlParams, type NotionClientRegistration, type NotionExchangeCodeParams, type NotionOAuthEndpoints, type NotionPKCEChallenge, type NotionRefreshTokenParams, type NotionTokenResponse, PII_PATTERNS, type PiiCategory, type PiiMatch, type PiiPattern, PiiRedactor, type PiiRedactorOptions, type PlatformStorage, type PortalLlmAuth, type PromptPreProcessor, type PromptPreProcessorContext, type QuantizedEmbedding, QueueManager, type QueueStatus, RECALL_MAX_LIMIT, RECALL_TOOL_NAME, type RankedMemory, type RecallContext, type RecallDegradation, type RecallDiagnostics, type RecallOptions, type RecallResult, type RecallToolCallbacks, type RecallToolOptions, type RecencyOptions, type RedactionResult, type ReflectOptions, type ReflectResult, RerankerUnavailableError, type ResumeStreamOptions, type ResumeStreamResult, type ResumeStreamWithStorageResult, type RetainAction, type RetainContext, type RetainOptions, type RetainResult, type RetainSource, SDK_SCHEMA_VERSION, STREAM_RESUMABLE_HEADER, type ScoreBreakdown, type SearchSource, type SendMessageWithStorageArgs, type SendMessageWithStorageDetachedResult, type SendMessageWithStorageResult, type ServerToolsOptions, type ServerToolsResponse, type SignMessageFn, type StockPriceClassification, type StockPricePreProcessorOptions, type ChatCompletionUsage as StoredChatCompletionUsage, type StoredConversation, type StoredConversationMemory, type StoredMessage, type StoredMessageWithSimilarity, type StoredVaultFolder, VaultFolder as StoredVaultFolderModel, type StoredVaultMemory, VaultMemory as StoredVaultMemoryModel, StreamExpiredError, type StreamMetaEvent, type StreamResumeHandle, TOPICS_EXTRACTION_VERSION, TOPIC_EXTRACTION_BATCH_SIZE, type TextChunk, type TopicExtractOptions, type TopicExtractionInput, type TopicExtractionRunResult, type TurnCompleteEvent, type TurnSkippedEvent, type UpdateVaultFolderOptions, type UpdateVaultMemoryOptions, type UseChatStorageOptions, type UseChatStorageResult, type UseCreditsOptions, type UseCreditsResult, type UseModelsOptions, type UseModelsResult, type VaultEmbeddingCache, type VaultFolderOperationsContext, type VaultMemoryOperationsContext, type VaultSaveOperation, type VaultSearchResult, WalletPoller, type WeatherClassification, type WeatherPreProcessorOptions, type WebSearchClassification, type WebSearchPreProcessorOptions, addConversationMemoriesOp, buildNotionAuthUrl, chatStorageMigrations, chatStorageSchema, chunkAndEmbedAllMessages, chunkAndEmbedMessage, chunkText, classifyCryptoPrice, classifyCryptoPriceBatch, classifyStockPrice, classifyStockPriceBatch, classifyWeather, classifyWeatherBatch, classifyWebSearch, classifyWebSearchBatch, clearAllEncryptionKeys, clearAllEncryptionState, clearConversationMemoriesOp, clearEncryptionKey, clearLazyTitleCache, clearServerToolsCache, consoleLogger, conversationMemoryToStored, cosineInt8, createAutoExtractor, createChunkVectorCache, createCryptoPricePreProcessor, createMemoryEngineTool, createMemoryVaultSearchTool, createMemoryVaultTool, createNotionTools, createPlatformCursorStore, createRecallTool, createStockPricePreProcessor, createStreamingDeAnonymizer, createVaultEmbeddingCache, createVaultFolderOp, createVaultMemoriesBatchOp, createVaultMemoryOp, createWeatherPreProcessor, createWebSearchPreProcessor, decryptConversationTitle, deleteAllVaultMemoriesForUserOp, deleteVaultFolderOp, deleteVaultMemoryOp, dequantizeEmbedding, discoverNotionOAuthEndpoints, eagerEmbedContent, embedAllMessages, embedMessage, ensureDefaultFoldersOp, exchangeNotionCode, extractAndLinkEntitiesForMemoriesOp, extractAndRetain, extractEntitiesForMemories, extractFacts, generateConversationId, generateEmbedding, generateEmbeddings, generateNotionPKCE, getAllVaultFoldersOp, getAllVaultMemoriesOp, getAllVaultMemoryContentsOp, getCachedServerTools, getConversationMemoriesOp, getConversationsByProjectLazyOp, getConversationsLazyOp, getConversationsPageOp, getLogger, getMemoriesNeedingTopicExtractionOp, getServerTools, getUnfiledVaultMemoriesOp, getVaultFolderMemoryCountOp, getVaultMemoryOp, hasEncryptionKey, isPiiRedactor, isRerankerAvailable, moveMemoriesToFolderOp, noopLogger, onKeyAvailable, preEmbedVaultMemories, quantizeEmbedding, queueManager, recall, reflect, refreshNotionAccessToken, registerNotionClient, requestEncryptionKey, resolvePiiRedactor, resumeStream, retain, sdkMigrations, sdkModelClasses, sdkSchema, searchVaultMemories, setLogger, shouldChunkMessage, stampTopicsExtractedAtOp, streamCancelPath, streamReplayPath, supersedeVaultMemoryOp, updateVaultFolderContextOp, updateVaultFolderOp, updateVaultMemoryEmbeddingOp, updateVaultMemoryOp, upsertMessageOp, useChat, useChatStorage, useCredits, useEncryption, useModels, xhrTransport };
8727
+ export { type AutoExtractMessage, type AutoExtractor, type Budget, type CachedChunkVectors, type CachedServerTools, Conversation as ChatConversation, Message as ChatMessage, type ChatRole, type ChunkVectorCache, type ChunkingOptions, type ConsolidationFallbackReason, type ConversationMemoryInput, ConversationMemory as ConversationMemoryModel, type ConversationMemoryOperationsContext, type CreateAutoExtractorOptions, type CreateConversationOptions, type CreateDecaySweeperOptions, type CreateMessageOptions, type CreateVaultFolderOptions, type CreateVaultMemoryOptions, type CryptoPriceClassification, type CryptoPricePreProcessorOptions, DEFAULT_CACHE_EXPIRATION_MS, DEFAULT_CHUNK_CACHE_SIZE, DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_DECAY_POLICY, DEFAULT_MAX_CLASSIFIER_CALLS_PER_SWEEP, DEFAULT_MIN_CHUNK_SIZE, DEFAULT_PROFILE_FACETS, DEFAULT_VAULT_CACHE_SIZE, DatabaseManager, type DatabaseManagerLogger, type DatabaseManagerOptions, type DecayCandidateRaw, type DecayClassifier, type DecayInput, type DecayPolicy, type DecaySweepResult, type DecaySweeper, type DecayVerdict, ENTITY_FANOUT, type EmbeddedWalletSignerFn, type ExtractFactsOptions, type ExtractOutcome, type ExtractedCandidate, type ExtractedEntity, type ExtractionCursorStore, type FactType, type FileMetadata, type FlushResult, type GetConversationsPageOptions, type GetMessagesPageOptions, type GraphTraversalOptions, HARD_DELETE_WINDOW_MS, INFERENCE_ID_HEADER, type InjectionClassifierOptions, type InjectionReason, type LazyStoredConversation, type LlmDecayClassifierOptions, type LlmNeighborRefinerOptions, type Logger, LoggerProvider, type LoggerProviderProps, MAX_HOPS, MEDIUM_TTL_MS, type MemoriesNeedingTopicExtraction, type EmbeddingOptions as MemoryEngineEmbeddingOptions, type MemoryEngineResult, type MemoryEngineSearchOptions, type MemoryExtractedEvent, type MemoryKind, type MemoryQuarantinedEvent, type MemoryVaultSearchOptions, type MemoryVaultToolOptions, type MessageRedactionResult, type MessageSkeleton, NEVER_TTL_MS, NODE_BUDGET, NOTION_OAUTH_CONFIG, type NeighborRefiner, type NotionAuthUrlParams, type NotionClientRegistration, type NotionExchangeCodeParams, type NotionOAuthEndpoints, type NotionPKCEChallenge, type NotionRefreshTokenParams, type NotionTokenResponse, type NowSource, PAST_EVENT_GRACE_MS, PII_PATTERNS, PROFILE_DOC_VERSION, type PiiCategory, type PiiMatch, type PiiPattern, PiiRedactor, type PiiRedactorOptions, type PlatformStorage, type PortalLlmAuth, type ProfileConfigFingerprint, type ProfileDoc, type ProfileFacet, type ProfileFacetKey, type ProfileSection, type PromptPreProcessor, type PromptPreProcessorContext, type QuantizedEmbedding, type QuarantinedMemoryInfo, QueueManager, type QueueStatus, RECALL_MAX_LIMIT, RECALL_TOOL_NAME, type RankedMemory, type RecallContext, type RecallDegradation, type RecallDiagnostics, type RecallOptions, type RecallResult, type RecallToolCallbacks, type RecallToolOptions, type RecencyOptions, type RedactionResult, type ReflectOptions, type ReflectResult, RerankerUnavailableError, type ResumeStreamOptions, type ResumeStreamResult, type ResumeStreamWithStorageResult, type RetainAction, type RetainContext, type RetainOptions, type RetainResult, type RetainSource, SDK_SCHEMA_VERSION, SHORT_TTL_MS, STREAM_RESUMABLE_HEADER, type ScoreBreakdown, type ScreenResult, type ScreenedCandidate, type SearchSource, type SendMessageWithStorageArgs, type SendMessageWithStorageDetachedResult, type SendMessageWithStorageResult, type ServerToolsOptions, type ServerToolsResponse, type SignMessageFn, type StockPriceClassification, type StockPricePreProcessorOptions, type ChatCompletionUsage as StoredChatCompletionUsage, type StoredConversation, type StoredConversationMemory, type StoredMessage, type StoredMessageWithSimilarity, type StoredVaultFolder, VaultFolder as StoredVaultFolderModel, type StoredVaultMemory, VaultMemory as StoredVaultMemoryModel, StreamExpiredError, type StreamMetaEvent, type StreamResumeHandle, type SynthesizeProfileOptions, TOPICS_EXTRACTION_VERSION, TOPIC_EXTRACTION_BATCH_SIZE, type TextChunk, type TopicExtractOptions, type TopicExtractionInput, type TopicExtractionRunResult, type TurnCompleteEvent, type TurnSkippedEvent, type UpdateVaultFolderOptions, type UpdateVaultMemoryOptions, type UseChatStorageOptions, type UseChatStorageResult, type UseCreditsOptions, type UseCreditsResult, type UseModelsOptions, type UseModelsResult, VAULT_SIZE_HOP_CAP, type VaultEmbeddingCache, type VaultFolderOperationsContext, type VaultMemoryOperationsContext, type VaultSaveOperation, type VaultSearchResult, WalletPoller, type WeatherClassification, type WeatherPreProcessorOptions, type WebSearchClassification, type WebSearchPreProcessorOptions, addConversationMemoriesOp, archiveVaultMemoryOp, buildNotionAuthUrl, capHopsForDensity, chatStorageMigrations, chatStorageSchema, chunkAndEmbedAllMessages, chunkAndEmbedMessage, chunkText, classifyCryptoPrice, classifyCryptoPriceBatch, classifyDecay, classifyInjectionCandidates, classifyStockPrice, classifyStockPriceBatch, classifyWeather, classifyWeatherBatch, classifyWebSearch, classifyWebSearchBatch, clearAllEncryptionKeys, clearAllEncryptionState, clearConversationMemoriesOp, clearEncryptionKey, clearLazyTitleCache, clearServerToolsCache, consoleLogger, conversationMemoryToStored, cosineInt8, createAutoExtractor, createChunkVectorCache, createCryptoPricePreProcessor, createDecaySweeper, createLlmDecayClassifier, createLlmNeighborRefiner, createMemoryEngineTool, createMemoryVaultSearchTool, createMemoryVaultTool, createNotionTools, createPlatformCursorStore, createRecallTool, createStockPricePreProcessor, createStreamingDeAnonymizer, createVaultEmbeddingCache, createVaultFolderOp, createVaultMemoriesBatchOp, createVaultMemoryOp, createWeatherPreProcessor, createWebSearchPreProcessor, decryptConversationTitle, deleteAllVaultMemoriesForUserOp, deleteVaultFolderOp, deleteVaultMemoryOp, dequantizeEmbedding, discoverNotionOAuthEndpoints, eagerEmbedContent, embedAllMessages, embedMessage, ensureDefaultFoldersOp, exchangeNotionCode, extractAndLinkEntitiesForMemoriesOp, extractAndRetain, extractEntitiesForMemories, extractFacts, generateConversationId, generateEmbedding, generateEmbeddings, generateNotionPKCE, getAllVaultFoldersOp, getAllVaultMemoriesOp, getAllVaultMemoryContentsOp, getCachedServerTools, getConversationMemoriesOp, getConversationsByProjectLazyOp, getConversationsLazyOp, getConversationsPageOp, getDecayCandidatesRawOp, getLogger, getMemoriesNeedingTopicExtractionOp, getServerTools, getUnfiledVaultMemoriesOp, getVaultFolderMemoryCountOp, getVaultMemoryOp, hardDeleteDecayedOp, hasEncryptionKey, injectionSignatureCatalog, isPiiRedactor, isRerankerAvailable, moveMemoriesToFolderOp, noopLogger, onKeyAvailable, preEmbedVaultMemories, quantizeEmbedding, queueManager, recall, reflect, refreshNotionAccessToken, registerNotionClient, requestEncryptionKey, resolvePiiRedactor, restoreVaultMemoryOp, resumeStream, retain, screenCandidatesForInjection, sdkMigrations, sdkModelClasses, sdkSchema, searchVaultMemories, setLogger, shouldChunkMessage, stampTopicsExtractedAtOp, streamCancelPath, streamReplayPath, supersedeVaultMemoryOp, synthesizeProfile, traverseGraphLane, ttlForType, updateVaultFolderContextOp, updateVaultFolderOp, updateVaultMemoryEmbeddingOp, updateVaultMemoryOp, upsertMessageOp, useChat, useChatStorage, useCredits, useEncryption, useModels, xhrTransport };