@graphorin/memory 0.5.0 → 0.6.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.
- package/CHANGELOG.md +90 -1
- package/README.md +31 -31
- package/dist/conflict/index.d.ts +3 -1
- package/dist/conflict/index.js +1 -1
- package/dist/conflict/locale-packs/en.d.ts +13 -0
- package/dist/conflict/locale-packs/en.d.ts.map +1 -0
- package/dist/conflict/locale-packs/en.js +1 -1
- package/dist/conflict/locale-packs/en.js.map +1 -1
- package/dist/conflict/pipeline.d.ts +2 -2
- package/dist/conflict/pipeline.js +3 -3
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/helpers.js +1 -1
- package/dist/conflict/stages/helpers.js.map +1 -1
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
- package/dist/conflict/types.d.ts +7 -7
- package/dist/conflict/types.js +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/budget.js +16 -3
- package/dist/consolidator/budget.js.map +1 -1
- package/dist/consolidator/decay.d.ts +13 -2
- package/dist/consolidator/decay.js +17 -5
- package/dist/consolidator/decay.js.map +1 -1
- package/dist/consolidator/dlq.js +1 -1
- package/dist/consolidator/dlq.js.map +1 -1
- package/dist/consolidator/errors.js +2 -2
- package/dist/consolidator/errors.js.map +1 -1
- package/dist/consolidator/index.js +8 -6
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +1 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/noise-filter.js.map +1 -1
- package/dist/consolidator/phases/deep.js +6 -2
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts +10 -10
- package/dist/consolidator/phases/induce.js +6 -6
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/learned-context.js +129 -0
- package/dist/consolidator/phases/learned-context.js.map +1 -0
- package/dist/consolidator/phases/light.js +2 -1
- package/dist/consolidator/phases/light.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +5 -4
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +37 -8
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +1 -1
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts +4 -4
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +73 -16
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +5 -5
- package/dist/consolidator/scheduler.js +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/triggers.js +7 -7
- package/dist/consolidator/triggers.js.map +1 -1
- package/dist/consolidator/types.d.ts +68 -20
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +16 -6
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/annotations.d.ts +9 -9
- package/dist/context-engine/annotations.js +3 -3
- package/dist/context-engine/annotations.js.map +1 -1
- package/dist/context-engine/auto-recall.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +26 -5
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.js +19 -6
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/index.js +2 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/compaction/thresholds.js.map +1 -1
- package/dist/context-engine/compaction/types.d.ts +47 -7
- package/dist/context-engine/compaction/types.d.ts.map +1 -1
- package/dist/context-engine/engine.d.ts +23 -4
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +69 -15
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +1 -0
- package/dist/context-engine/index.d.ts.map +1 -1
- package/dist/context-engine/index.js +2 -1
- package/dist/context-engine/locale-packs/en.js +14 -13
- package/dist/context-engine/locale-packs/en.js.map +1 -1
- package/dist/context-engine/locale-packs/types.d.ts +10 -10
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/metadata.d.ts +1 -1
- package/dist/context-engine/metadata.js +2 -2
- package/dist/context-engine/metadata.js.map +1 -1
- package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
- package/dist/context-engine/preambles/inbound-en.js +1 -1
- package/dist/context-engine/preambles/inbound-en.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +2 -2
- package/dist/context-engine/privacy-filter.js +1 -1
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/context-engine/templates/base-en.d.ts +1 -1
- package/dist/context-engine/templates/base-en.js +1 -1
- package/dist/context-engine/templates/base-en.js.map +1 -1
- package/dist/context-engine/templates/composer.d.ts +1 -1
- package/dist/context-engine/templates/composer.js.map +1 -1
- package/dist/context-engine/token-budget.d.ts +1 -1
- package/dist/context-engine/token-budget.js +1 -1
- package/dist/context-engine/token-budget.js.map +1 -1
- package/dist/context-engine/token-counter.d.ts +16 -7
- package/dist/context-engine/token-counter.d.ts.map +1 -1
- package/dist/context-engine/token-counter.js +31 -9
- package/dist/context-engine/token-counter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +44 -15
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +16 -8
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.d.ts +2 -2
- package/dist/graph/entity-resolver.js +2 -2
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/contextualize.js +2 -2
- package/dist/internal/contextualize.js.map +1 -1
- package/dist/internal/embedder-binding.js +1 -1
- package/dist/internal/embedder-binding.js.map +1 -1
- package/dist/internal/id.js +1 -1
- package/dist/internal/id.js.map +1 -1
- package/dist/internal/injection-heuristics.js +25 -2
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/spans.js +1 -1
- package/dist/internal/spans.js.map +1 -1
- package/dist/internal/storage-adapter.d.ts +71 -22
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/migration/embedder-migration.d.ts +7 -7
- package/dist/migration/embedder-migration.js.map +1 -1
- package/dist/search/explain.d.ts +7 -4
- package/dist/search/explain.d.ts.map +1 -1
- package/dist/search/explain.js +1 -1
- package/dist/search/explain.js.map +1 -1
- package/dist/search/fit-weights.d.ts +48 -0
- package/dist/search/fit-weights.d.ts.map +1 -0
- package/dist/search/fit-weights.js +70 -0
- package/dist/search/fit-weights.js.map +1 -0
- package/dist/search/graph-ppr.d.ts +36 -0
- package/dist/search/graph-ppr.d.ts.map +1 -0
- package/dist/search/graph-ppr.js +38 -0
- package/dist/search/graph-ppr.js.map +1 -0
- package/dist/search/index.d.ts +4 -1
- package/dist/search/index.js +4 -1
- package/dist/search/iterative.d.ts +15 -8
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +10 -9
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts +6 -6
- package/dist/search/query-transform.js +6 -6
- package/dist/search/query-transform.js.map +1 -1
- package/dist/search/rrf.d.ts +4 -4
- package/dist/search/rrf.js +4 -4
- package/dist/search/rrf.js.map +1 -1
- package/dist/search/trust.d.ts +30 -0
- package/dist/search/trust.d.ts.map +1 -0
- package/dist/search/trust.js +50 -0
- package/dist/search/trust.js.map +1 -0
- package/dist/search/types.d.ts +1 -1
- package/dist/tiers/episodic-memory.d.ts +6 -4
- package/dist/tiers/episodic-memory.d.ts.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -2
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +1 -1
- package/dist/tiers/index.js +1 -1
- package/dist/tiers/insight-memory.d.ts +4 -4
- package/dist/tiers/insight-memory.js +3 -3
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +29 -7
- package/dist/tiers/procedural-memory.d.ts.map +1 -1
- package/dist/tiers/procedural-memory.js +66 -6
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +111 -30
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +120 -27
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tiers/session-memory.d.ts +7 -7
- package/dist/tiers/session-memory.js +7 -7
- package/dist/tiers/session-memory.js.map +1 -1
- package/dist/tiers/shared-memory.d.ts +1 -1
- package/dist/tiers/shared-memory.js +1 -1
- package/dist/tiers/shared-memory.js.map +1 -1
- package/dist/tiers/working-memory.d.ts +3 -3
- package/dist/tiers/working-memory.js +2 -2
- package/dist/tiers/working-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +9 -9
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js +3 -3
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +48 -31
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +30 -18
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/index.d.ts +9 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +40 -20
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +29 -10
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +76 -0
- package/dist/tools/runbook-tools.d.ts.map +1 -0
- package/dist/tools/runbook-tools.js +53 -0
- package/dist/tools/runbook-tools.js.map +1 -0
- package/dist/tools/taint.js +33 -0
- package/dist/tools/taint.js.map +1 -0
- package/dist/tools/types.d.ts +1 -1
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-memory.js","names":["#store","#tracer","#embedder","#embedderIdProvider","#pipeline","#contextualMode","#queryTransformer","#entityResolver","#grader","#iterativeMaxIterations","#reranker","status: MemoryStatus","quarantineReason: 'injection' | 'synthesized' | undefined","fact: Fact","#resolveIndexText","decision: ConflictDecision","#commitFact","#linkEntities","#fetchExisting","#expandQueries","lists: Array<ReadonlyArray<MemoryHit<Fact>>>","listWeights: number[]","listLabels: string[]","#tryVectorSearch","#tryHyde","#tryExpandHops","reranker: ReRanker","#applyDecay","out: string[]","seedIds: string[]","rows: ReadonlyArray<{\n readonly id: string;\n readonly strength: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n }>"],"sources":["../../src/tiers/semantic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Fact,\n MemoryHit,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { md5 } from '@graphorin/core';\nimport type { ConflictDecision, ConflictPipeline } from '../conflict/index.js';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport type { EntityResolver } from '../graph/entity-resolver.js';\nimport { contextualize } from '../internal/contextualize.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport { explainRecall } from '../search/explain.js';\nimport {\n DEFAULT_MAX_ITERATIONS,\n type IterativeRetrievalResult,\n type RetrievalGrader,\n runIterativeRetrieval,\n} from '../search/iterative.js';\nimport type { QueryTransformer } from '../search/query-transform.js';\nimport { fuseRrf, fuseWeighted, WeightedRRFReranker } from '../search/rrf.js';\nimport type { ReRanker } from '../search/types.js';\n\n/**\n * Author-time fact payload accepted by {@link SemanticMemory.remember}.\n * The framework derives `id`, `kind: 'semantic'`, `userId`,\n * `createdAt`, `updatedAt`, `validFrom`, the optional `embedder_id`,\n * and the deduplication `hash` from this input.\n *\n * @stable\n */\nexport interface FactInput {\n readonly text: string;\n readonly subject?: string;\n readonly predicate?: string;\n readonly object?: string;\n readonly tags?: ReadonlyArray<string>;\n readonly confidence?: number;\n readonly sensitivity?: Sensitivity;\n readonly validFrom?: string;\n readonly validTo?: string;\n readonly supersedes?: string;\n /**\n * Trust-provenance tag (P1-4). Writers that synthesize memory pass\n * `'extraction'` / `'reflection'` so the fact lands quarantined;\n * first-party writers pass `'user'` / `'tool'` (or omit it — absent ⇒\n * treated as first-party `active`). The `status` is *derived* from\n * this tag plus the injection heuristics; it is never author-set.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Importance hint in `[0, 1]` (X-1 / MCON-12). Feeds the multi-signal\n * salience score that orders decay archiving and capacity eviction —\n * higher importance ⇒ evicted later. Values are clamped to `[0, 1]`;\n * non-finite values are dropped. The consolidator's extraction pass\n * fills this from the model's per-fact 1–10 rating\n * (`normalizeImportance`); absent ⇒ the neutral midpoint at scoring\n * time.\n */\n readonly importance?: number;\n}\n\n/**\n * Per-list weights for {@link FusionStrategy} `'weighted'` fusion (X-2),\n * keyed by retriever *kind* rather than position so they survive the\n * P2-3 multi-query fan-out (which appends extra FTS / vector candidate\n * lists). Each defaults to the neutral `1` (≡ RRF). The HyDE list is a\n * vector list and takes the `vector` weight.\n *\n * @stable\n */\nexport interface FusionWeights {\n /** Weight applied to every FTS5 (lexical) candidate list. Default `1`. */\n readonly fts?: number;\n /** Weight applied to every vector (incl. HyDE) candidate list. Default `1`. */\n readonly vector?: number;\n}\n\n/**\n * Score-fusion strategy for {@link SemanticMemory.search} (X-2).\n *\n * - `'rrf'` (the default when `fusion` is omitted) fuses the candidate\n * lists through the configured reranker — the zero-tuning\n * {@link RRFReranker} unless one was overridden.\n * - `'weighted'` fuses through {@link WeightedRRFReranker}, scaling each\n * list's reciprocal-rank contribution by its {@link FusionWeights}, for\n * callers who have calibrated retriever reliability against labels (the\n * P0-1 eval harness). At equal weights it reproduces RRF.\n *\n * @stable\n */\nexport type FusionStrategy =\n | { readonly strategy: 'rrf' }\n | {\n readonly strategy: 'weighted';\n readonly weights: FusionWeights;\n /** Override the RRF constant for the weighted fuse. Default `60`. */\n readonly k?: number;\n };\n\n/**\n * Per-call options accepted by {@link SemanticMemory.search}.\n *\n * @stable\n */\nexport interface FactSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n /** Override the per-list candidate count (default `60`). */\n readonly candidateTopK?: number;\n /**\n * Any-of tags filter (MRET-4). A fact matches when it carries at\n * least one of the requested tags; untagged facts never match.\n * Applied in-store on the FTS leg and as a record-level filter on\n * the fused result so every candidate leg (vector / HyDE / graph)\n * obeys it.\n */\n readonly tags?: ReadonlyArray<string>;\n /**\n * Point-in-time (\"as of\") read. When set, only facts whose\n * bi-temporal validity interval contains this instant are returned\n * (`valid_from <= asOf < valid_to`, open-ended bounds allowed),\n * applied at the store layer to both the FTS and vector candidate\n * lists. ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined facts in the result (P1-4). Defaults to\n * `false` — action-driving recall (`fact_search`, auto-recall) never\n * returns quarantined rows. Set `true` only for the validation /\n * inspector path that surfaces quarantined facts to a human for\n * promotion via {@link SemanticMemory.validate}.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n /**\n * Optional decay-aware ranking. When set, the reranker output is\n * post-multiplied by the per-fact retention curve\n * `score *= exp(-elapsedDays / tauDays)` so stale facts drop in\n * the result list without ever being hard-deleted (principle 8).\n * Requires the storage adapter to expose\n * `semantic.listForDecay?(...)` so the search can read\n * `strength` + `lastAccessedAt`; adapters without the surface\n * skip the boost silently.\n *\n * @stable\n */\n readonly decay?: {\n readonly tauDays: number;\n /** Override the wall clock (test seam). */\n readonly now?: () => number;\n };\n /**\n * Multi-query / RAG-Fusion (P2-3). When set to `N > 1` *and* a query\n * transformer is configured (`createMemory({ queryTransform })`), the\n * query is fanned into up to `N - 1` reworded variants via one cheap\n * LLM call; each variant is retrieved (FTS + vector) and **all** lists\n * are fused through the existing RRF reranker — recovering memories\n * whose stored wording differs from the user's phrasing. `N` bounds\n * the *total* query strings, including the original. Offline (no\n * transformer, or `N <= 1`) this is a **silent no-op**: search stays\n * single-shot and makes no provider call. Opt-in + retrieval-heavy, so\n * reserve it for deliberate recall rather than every search.\n *\n * @stable\n */\n readonly multiQuery?: number;\n /**\n * HyDE — Hypothetical Document Embeddings (arXiv:2212.10496), P2-3.\n * When `true` *and* both a query transformer and an embedder are\n * configured, generate a short hypothetical answer, embed it, and fuse\n * its vector neighbours into the result. Helps short / ambiguous\n * queries but adds a generate + embed round-trip and can drift — hence\n * opt-in. With no transformer (or no embedder) this is a silent no-op\n * and no provider call is made.\n *\n * @stable\n */\n readonly hyde?: boolean;\n /**\n * Score-fusion strategy (X-2). Omitted (the default) ⇒ RRF via the\n * configured reranker — behaviour is unchanged. `{ strategy:\n * 'weighted', weights }` fuses through {@link WeightedRRFReranker},\n * up-/down-weighting the FTS vs vector candidate lists per\n * {@link FusionWeights}; reserve it for callers who have calibrated the\n * weights against labels (the P0-1 eval harness). At equal weights it\n * reproduces RRF.\n *\n * @stable\n */\n readonly fusion?: FusionStrategy;\n /**\n * One-hop graph expansion (P2-1). With `1` *and* a graph-capable\n * storage adapter (`store.graph`), the facts retrieved by the lexical /\n * vector candidate pass are treated as seeds: facts sharing a canonical\n * entity (subject / object) are fetched via a recursive CTE and fused\n * in as an extra candidate list before rerank — surfacing connected\n * facts the query never matched directly (\"what did the person I met in\n * Tbilisi recommend?\"). `0` (the default) or a graph-less adapter ⇒ a\n * silent no-op; recall is unchanged. Opt-in + retrieval-heavy.\n *\n * @stable\n */\n readonly expandHops?: 0 | 1;\n}\n\n/**\n * Per-call options for {@link SemanticMemory.searchIterative} (P2-4) —\n * the gated grade-then-reformulate loop. Extends {@link FactSearchOptions}\n * (every base option applies to each retrieval pass); `topK` doubles as\n * the cap on the accumulated result count.\n *\n * @stable\n */\nexport interface IterativeSearchOptions extends FactSearchOptions {\n /**\n * Total-pass cap, clamped to `[1, 5]`. Omitted ⇒ the facade-configured\n * default (`createMemory({ iterativeRetrieval: { maxIterations } })`)\n * or `3`.\n */\n readonly maxIterations?: number;\n /**\n * Skip the heuristic difficulty gate and force the loop (still capped\n * and still a no-op without a grader). For deliberate \"deep recall\"\n * requests and tests.\n */\n readonly forceHard?: boolean;\n}\n\n/**\n * Outcome of {@link SemanticMemory.searchIterative}. Beyond the ranked\n * `hits`, `sufficient` / `abstained` tell the caller whether the memory\n * actually answered the query — `abstained: true` means it should say so\n * rather than confabulate.\n *\n * @stable\n */\nexport type IterativeRecallResult = IterativeRetrievalResult<MemoryHit<Fact>>;\n\n/**\n * Per-call options accepted by {@link SemanticMemory.remember}. The\n * Phase 10b pipeline writes one row to `fact_conflicts` (and\n * potentially one to `conflict_check_pending`) for every invocation;\n * pass `pipeline: 'off'` to bypass the pipeline for a single call\n * (useful for one-shot data imports).\n *\n * @stable\n */\nexport interface FactRememberOptions {\n readonly pipeline?: 'on' | 'off';\n /** Cancellation signal forwarded to the embedder + storage layers. */\n readonly signal?: AbortSignal;\n /**\n * Precomputed contextual-retrieval index text (P1-3, advanced). When\n * supplied it overrides the instance's `'late-chunk'` computation: the\n * embedding is computed from — and the FTS row indexed against — this\n * text, while the canonical `text` is stored unchanged. The background\n * consolidator passes this in its `'llm'` mode (the one place an LLM is\n * allowed to write the situating context); first-party callers normally\n * omit it and rely on the offline late-chunk default.\n */\n readonly indexText?: string;\n /**\n * Auto-promotion policy (MCON-2). When `true`, a *synthesized* write\n * (consolidator extraction) that is **clean** by the injection heuristics is\n * stored `active` instead of quarantined. Injection-flagged writes always\n * stay quarantined — the security gate is preserved. Off by default; the\n * consolidator passes it only when the operator opts in via\n * `autoPromoteExtraction`. No effect on non-synthesized writes.\n */\n readonly autoPromoteSynthesized?: boolean;\n}\n\n/**\n * Returned by {@link SemanticMemory.remember}. The `fact` is the\n * stored row (which may be the *existing* fact when the pipeline\n * dedups). The `decision` mirrors the pipeline outcome so callers can\n * distinguish silent dedups from active inserts.\n *\n * @stable\n */\nexport interface RememberOutcome {\n readonly fact: Fact;\n readonly decision: ConflictDecision;\n /**\n * Why this write landed quarantined, if it did (P1-4 / MRET-3).\n * `'injection'` — the offline injection heuristics flagged the text\n * (a memory-poisoning candidate). `'synthesized'` — a consolidator /\n * reflection / induction write awaiting validation. Absent when the\n * fact is `active` or when a dedup returned a pre-existing row.\n */\n readonly quarantineReason?: 'injection' | 'synthesized';\n}\n\n/**\n * `SemanticMemory` — long-term factual store. Hybrid (vector + FTS5)\n * search merges the two ranked lists through the configured\n * {@link ReRanker} (default {@link RRFReranker} with `k = 60`).\n *\n * Phase 10a wrote facts straight through with MD5 deduplication;\n * Phase 10b routes every `remember(...)` call through the multi-stage\n * conflict resolution pipeline (DEC-117 / ADR-018 ext / RB-02). The\n * pipeline can be disabled per-call (`pipeline: 'off'`) or per-`Memory`\n * instance (`createMemory({ conflictPipeline: { mode: 'off' } })`).\n *\n * @stable\n */\nexport class SemanticMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n readonly #pipeline: ConflictPipeline | null;\n readonly #contextualMode: 'off' | 'late-chunk';\n readonly #queryTransformer: QueryTransformer | null;\n readonly #entityResolver: EntityResolver | null;\n readonly #grader: RetrievalGrader | null;\n readonly #iterativeMaxIterations: number;\n #reranker: ReRanker;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n reranker: ReRanker;\n conflictPipeline?: ConflictPipeline;\n /**\n * Query transformer for multi-query / HyDE retrieval (P2-3). When\n * supplied, `search(..., { multiQuery })` / `{ hyde }` opt into one\n * cheap LLM call to rewrite / hypothesize the query; omitted (the\n * default) ⇒ those options are silent no-ops and search stays\n * offline + single-shot.\n */\n queryTransformer?: QueryTransformer;\n /**\n * Contextual-retrieval mode for the write path (P1-3). `'late-chunk'`\n * (default) prepends a deterministic situating context to the text\n * that is embedded + FTS-indexed, leaving the canonical `text`\n * untouched; `'off'` indexes the bare text. The hot write path never\n * makes an LLM call — the `'llm'` enrichment is confined to the\n * background consolidator, which supplies a precomputed `indexText`.\n */\n contextualRetrieval?: 'off' | 'late-chunk';\n /**\n * Entity resolver for the relation graph (P2-1). When supplied,\n * `remember(...)` resolves a fact's subject / object to canonical\n * entities and links them, enabling `search(..., { expandHops: 1 })`.\n * Omitted (the default) ⇒ writes carry s/p/o but form no entity\n * links, and the write path stays offline + unchanged.\n */\n entityResolver?: EntityResolver;\n /**\n * Retrieval grader for the gated iterative loop (P2-4). When\n * supplied, `searchIterative(...)` can grade a retrieved set and\n * reformulate on hard queries; omitted (the default) ⇒\n * `searchIterative` runs a single, difficulty-gated pass and makes no\n * provider call.\n */\n grader?: RetrievalGrader;\n /** Default total-pass cap for `searchIterative`. Default 3. */\n iterativeMaxIterations?: number;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n this.#reranker = args.reranker;\n this.#pipeline = args.conflictPipeline ?? null;\n this.#contextualMode = args.contextualRetrieval ?? 'late-chunk';\n this.#queryTransformer = args.queryTransformer ?? null;\n this.#entityResolver = args.entityResolver ?? null;\n this.#grader = args.grader ?? null;\n this.#iterativeMaxIterations = args.iterativeMaxIterations ?? DEFAULT_MAX_ITERATIONS;\n }\n\n /** Replace the active reranker. Returns the previous instance. */\n setReranker(reranker: ReRanker): ReRanker {\n const previous = this.#reranker;\n this.#reranker = reranker;\n return previous;\n }\n\n /** Currently active reranker. */\n reranker(): ReRanker {\n return this.#reranker;\n }\n\n /**\n * Persist a fact. Returns the stored record. Phase 10b routes every\n * call through the multi-stage conflict resolution pipeline; the\n * legacy straight-through path is reachable per-call via\n * `{ pipeline: 'off' }` (operators may disable the pipeline globally\n * via `createMemory({ conflictPipeline: { mode: 'off' } })`).\n */\n async remember(\n scope: SessionScope,\n input: FactInput,\n options: FactRememberOptions = {},\n ): Promise<Fact> {\n const { fact } = await this.rememberWithDecision(scope, input, options);\n return fact;\n }\n\n /**\n * Like {@link remember} but returns the pipeline `decision` alongside\n * the stored fact. Useful for callers that need to distinguish\n * silent dedups (`decision.kind === 'dedup'`) from fresh inserts.\n *\n * @stable\n */\n async rememberWithDecision(\n scope: SessionScope,\n input: FactInput,\n options: FactRememberOptions = {},\n ): Promise<RememberOutcome> {\n return withMemorySpan(this.#tracer, 'memory.write.semantic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const text = input.text;\n // P1-4: derive the retrieval-trust status. Synthesized writes\n // (consolidator extraction / reflection) and candidates that trip\n // the offline injection heuristics land quarantined — excluded\n // from default recall until a human validates them. First-party\n // writes (user / tool / imported / unset) stay active. `status`\n // is never author-set; it is always derived here.\n const provenance = input.provenance;\n const synthesized =\n provenance === 'extraction' || provenance === 'reflection' || provenance === 'induction';\n const injection = detectMemoryInjection(text);\n // MCON-2 auto-promotion: an opted-in, injection-clean synthesized write is\n // admitted `active`. Injection-flagged writes always stay quarantined.\n const autoPromote = options.autoPromoteSynthesized === true;\n const status: MemoryStatus = injection.flagged\n ? 'quarantined'\n : synthesized && !autoPromote\n ? 'quarantined'\n : 'active';\n // MRET-3: surface *why* a fresh write was quarantined so callers\n // (the fact_remember tool, harnesses) can tell an injection-flagged\n // poison candidate apart from a synthesized-but-clean consolidator\n // write. Injection takes precedence — it is the security-relevant\n // reason and gates promotion in validate().\n const quarantineReason: 'injection' | 'synthesized' | undefined =\n status === 'quarantined' ? (injection.flagged ? 'injection' : 'synthesized') : undefined;\n const fact: Fact = {\n id: newMemoryId('fact'),\n kind: 'semantic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n text,\n // P2-1: carry the s/p/o triple onto the persisted fact (it used to\n // be dropped here, surviving only in the late-chunk index text).\n // This is what activates the relation-graph substrate.\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n ...(input.confidence !== undefined ? { confidence: input.confidence } : {}),\n // X-1 / MCON-12: the importance hint that drives salience-ordered\n // forgetting. Clamped to [0, 1]; non-finite ⇒ unscored.\n ...(typeof input.importance === 'number' && Number.isFinite(input.importance)\n ? { importance: Math.min(1, Math.max(0, input.importance)) }\n : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.validFrom !== undefined ? { validFrom: input.validFrom } : { validFrom: now }),\n ...(input.validTo !== undefined ? { validTo: input.validTo } : {}),\n ...(input.supersedes !== undefined ? { supersedes: input.supersedes } : {}),\n ...(provenance !== undefined ? { provenance } : {}),\n status,\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n const embedder = this.#embedder;\n // P1-3: the text that is embedded + FTS-indexed. A caller-supplied\n // `indexText` (the consolidator's `'llm'` mode) wins; otherwise the\n // instance mode decides between the bare text (`'off'`) and the\n // offline late-chunk prefix derived from the *author-supplied*\n // signals on `input` (entities / timeframe / topics). The canonical\n // `fact.text` persisted below is never altered.\n const indexText = this.#resolveIndexText(input, fact.text, options.indexText);\n const pipelineEnabled = this.#pipeline !== null && (options.pipeline ?? 'on') === 'on';\n const decision: ConflictDecision =\n pipelineEnabled && this.#pipeline !== null\n ? await this.#pipeline.run(\n {\n store: this.#store,\n tracer: this.#tracer,\n embedder,\n embedderId,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n },\n fact,\n )\n : { kind: 'admit', stage: 'exact-dedup', reason: 'pipeline-skipped' };\n\n span.setAttributes({\n 'memory.semantic.text_length': text.length,\n 'memory.semantic.hash': md5(text),\n 'memory.semantic.pipeline.decision': decision.kind,\n 'memory.semantic.pipeline.stage': decision.stage,\n 'memory.semantic.pipeline.enabled': pipelineEnabled,\n 'memory.semantic.status': status,\n 'memory.semantic.contextualized': indexText !== text,\n ...(provenance !== undefined ? { 'memory.semantic.provenance': provenance } : {}),\n 'memory.semantic.injection_flagged': injection.flagged,\n ...(injection.flagged\n ? { 'memory.semantic.injection_markers': injection.markers.join(',') }\n : {}),\n });\n\n const reasonField = quarantineReason !== undefined ? { quarantineReason } : {};\n switch (decision.kind) {\n case 'admit':\n case 'pending': {\n await this.#commitFact(fact, embedder, embedderId, indexText);\n await this.#linkEntities(scope, fact, options.signal);\n return { fact, decision, ...reasonField };\n }\n case 'dedup': {\n const existing = await this.#fetchExisting(decision.existingId);\n // The candidate was never committed; the existing row's own\n // status is authoritative, so do not attach the candidate's\n // quarantine reason here.\n return { fact: existing ?? fact, decision };\n }\n case 'supersede': {\n await this.#commitFact(fact, embedder, embedderId, indexText);\n await this.#store.semantic.supersede(decision.existingId, fact, decision.reason);\n await this.#linkEntities(scope, fact, options.signal);\n return { fact, decision, ...reasonField };\n }\n }\n });\n }\n\n /**\n * Resolve the contextual-retrieval index text (P1-3). A caller\n * override (the consolidator's `'llm'` mode) takes precedence; then\n * the instance mode decides. Late-chunk derives the situating context\n * from the *author-supplied* signals on `input` (so an extraction's\n * subject/predicate/object survive even though the persisted `Fact`\n * drops them) and never from framework-defaulted fields, so a plain\n * `remember({ text })` write returns the canonical text unchanged.\n */\n #resolveIndexText(input: FactInput, canonicalText: string, override?: string): string {\n if (override !== undefined) return override;\n if (this.#contextualMode === 'off') return canonicalText;\n return contextualize({\n text: canonicalText,\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n ...(input.tags !== undefined ? { tags: input.tags } : {}),\n ...(input.validFrom !== undefined ? { validFrom: input.validFrom } : {}),\n });\n }\n\n async #commitFact(\n fact: Fact,\n embedder: EmbedderProvider | null,\n embedderId: string | null,\n indexText: string,\n ): Promise<void> {\n const adapterSupportsEmbeddedWrite =\n typeof this.#store.semantic.rememberWithEmbedding === 'function';\n const contextualized = indexText !== fact.text;\n const hasEmbeddingPath =\n embedder !== null && embedderId !== null && adapterSupportsEmbeddedWrite;\n if (hasEmbeddingPath) {\n // Embed the *contextual* text so the vector surface agrees with the\n // FTS index; the persisted `fact.text` stays canonical (P1-3). PS-10:\n // stored content is the `passage` role for asymmetric (E5) embedders.\n const [vector] = await embedder.embed([indexText], { taskType: 'passage' });\n if (vector !== undefined && this.#store.semantic.rememberWithEmbedding !== undefined) {\n await this.#store.semantic.rememberWithEmbedding(fact, {\n embedding: { embedderId, vector },\n ...(contextualized ? { indexText } : {}),\n });\n return;\n }\n }\n // No embedder, but contextualization still needs to reach the lexical\n // index — route through the extended write when the adapter supports\n // it. Plain (non-contextualized) writes keep the canonical fast path.\n if (\n contextualized &&\n adapterSupportsEmbeddedWrite &&\n this.#store.semantic.rememberWithEmbedding !== undefined\n ) {\n await this.#store.semantic.rememberWithEmbedding(fact, { indexText });\n return;\n }\n await this.#store.semantic.remember(fact);\n }\n\n async #fetchExisting(factId: string): Promise<Fact | null> {\n if (typeof this.#store.semantic.get !== 'function') return null;\n return this.#store.semantic.get(factId);\n }\n\n /**\n * P2-1: resolve the fact's subject / object to canonical entities and\n * link them so one-hop expansion can traverse this fact. A no-op when\n * no resolver is configured (the default) or the fact has no s/p/o, and\n * resilient — a resolution failure never breaks the write that just\n * committed.\n */\n async #linkEntities(scope: SessionScope, fact: Fact, signal?: AbortSignal): Promise<void> {\n if (this.#entityResolver === null) return;\n if (fact.subject === undefined && fact.object === undefined) return;\n try {\n await this.#entityResolver.linkFact(scope, fact, {\n ...(signal !== undefined ? { signal } : {}),\n });\n } catch {\n // Graph linking is a soft enrichment — never fail a committed write.\n }\n }\n\n /** Hybrid (vector + FTS5) search merged through the configured reranker. */\n async search(\n scope: SessionScope,\n query: string,\n opts: FactSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.semantic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const candidateTopK = opts.candidateTopK ?? 60;\n const finalTopK = opts.topK ?? 10;\n // X-2: when weighted fusion is opted-in, each candidate list is\n // weighted by its *kind* (FTS vs vector) so the weights survive\n // the P2-3 fan-out below; otherwise `listWeights` is unused and\n // fusion runs through the configured reranker (RRF by default).\n const weighted =\n opts.fusion !== undefined && opts.fusion.strategy === 'weighted' ? opts.fusion : null;\n const wFts = weighted?.weights.fts ?? 1;\n const wVector = weighted?.weights.vector ?? 1;\n // P2-3: fan the query into reworded variants when opted-in *and*\n // a transformer is configured; otherwise this is just `[query]`\n // (single-shot, no provider call — the offline default).\n const queries = await this.#expandQueries(query, opts);\n const lists: Array<ReadonlyArray<MemoryHit<Fact>>> = [];\n const listWeights: number[] = [];\n // MRET-13: retriever-kind labels in lockstep with `lists` so the\n // rrf.<label> explanation signals stay stable across fan-outs.\n const listLabels: string[] = [];\n let primaryFtsCount = 0;\n let primaryVectorCount = 0;\n let isPrimary = true;\n for (const q of queries) {\n const ftsHits = await this.#store.semantic.search(scope, {\n query: q,\n topK: candidateTopK,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: in-store any-of tags predicate on the FTS leg.\n ...(opts.tags !== undefined && opts.tags.length > 0 ? { tags: opts.tags } : {}),\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n lists.push(ftsHits);\n listWeights.push(wFts);\n listLabels.push(`fts_${queries.indexOf(q)}`);\n const vectorHits = await this.#tryVectorSearch(\n scope,\n q,\n candidateTopK,\n opts.asOf,\n opts.includeQuarantined,\n );\n if (vectorHits.length > 0) {\n lists.push(vectorHits);\n listWeights.push(wVector);\n listLabels.push(`vector_${queries.indexOf(q)}`);\n }\n if (isPrimary) {\n primaryFtsCount = ftsHits.length;\n primaryVectorCount = vectorHits.length;\n isPrimary = false;\n }\n }\n // P2-3 HyDE: embed a hypothetical answer and fuse its neighbours\n // (a vector list ⇒ it takes the `vector` weight).\n const hydeHits = await this.#tryHyde(scope, query, opts, candidateTopK);\n if (hydeHits.length > 0) {\n lists.push(hydeHits);\n listWeights.push(wVector);\n listLabels.push('hyde');\n }\n // P2-1: one-hop graph expansion — seed on the candidates gathered\n // so far and fuse in facts that share a canonical entity (a third\n // candidate kind ⇒ neutral fusion weight). A no-op without\n // `expandHops` / a graph adapter.\n const graphHits = await this.#tryExpandHops(scope, lists, opts, candidateTopK);\n if (graphHits.length > 0) {\n lists.push(graphHits);\n listWeights.push(1);\n listLabels.push('graph');\n }\n // X-2: weighted fusion runs through a per-call WeightedRRFReranker\n // built from the per-list weights; the default stays the\n // configured reranker so behaviour is unchanged when `fusion` is\n // omitted.\n const reranker: ReRanker =\n weighted !== null\n ? new WeightedRRFReranker({\n weights: listWeights,\n ...(weighted.k !== undefined ? { k: weighted.k } : {}),\n })\n : this.#reranker;\n // MRET-8: when decay re-ranking is requested, fuse the FULL\n // candidate pool (not just finalTopK) so a fresh fact sitting at\n // position topK+1 pre-decay can still enter the final page; the\n // finalTopK cut moves to AFTER decay + filters.\n const fusedTopK =\n opts.decay !== undefined\n ? Math.max(\n finalTopK,\n lists.reduce((n, l) => n + l.length, 0),\n )\n : finalTopK;\n const fused = await reranker.rerank(query, lists, {\n topK: fusedTopK,\n labels: listLabels,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const decayed = await this.#applyDecay(scope, fused, opts.decay);\n // MRET-4: the vector / HyDE / graph legs have no store-level tags\n // predicate — enforce the any-of filter on the fused records so\n // every leg obeys it.\n const filtered =\n opts.tags !== undefined && opts.tags.length > 0\n ? decayed.filter((h) => {\n const recordTags = h.record.tags;\n if (recordTags === undefined || recordTags.length === 0) return false;\n return opts.tags?.some((t) => recordTags.includes(t)) === true;\n })\n : decayed;\n const ranked = filtered.slice(0, finalTopK);\n // MRET-7: recall reinforces the recalled facts — stamp\n // last-accessed + bump strength so \"recently accessed facts decay\n // slower\" actually holds. Bookkeeping only: a failure here must\n // never break the read path.\n if (ranked.length > 0 && typeof this.#store.semantic.markAccessed === 'function') {\n try {\n await this.#store.semantic.markAccessed(ranked.map((h) => h.record.id));\n } catch {\n // Best-effort: decay reinforcement is advisory.\n }\n }\n const explanation = explainRecall(ranked, {\n query,\n rerankerId: reranker.id,\n });\n span.setAttributes({\n // `fts_count` / `vector_count` report the *original* query's\n // candidate lists, so the single-shot default reads exactly as\n // before; `query_count` (≥1) and `hyde_applied` cover the\n // P2-3 fan-out.\n 'memory.search.semantic.fts_count': primaryFtsCount,\n 'memory.search.semantic.vector_count': primaryVectorCount,\n 'memory.search.semantic.final_count': ranked.length,\n 'memory.search.semantic.reranker_id': reranker.id,\n 'memory.search.semantic.decay_applied': opts.decay !== undefined,\n 'memory.search.semantic.query_count': queries.length,\n 'memory.search.semantic.hyde_applied': hydeHits.length > 0,\n 'memory.search.semantic.expand_hops': opts.expandHops ?? 0,\n 'memory.search.semantic.graph_count': graphHits.length,\n // X-3: per-signal recall explanation (ids + scores + signals,\n // no query text — the query is surfaced only as `query_length`\n // above to keep traces free of recall content).\n 'memory.search.semantic.explain': JSON.stringify(explanation.results),\n ...(opts.asOf !== undefined ? { 'memory.search.semantic.as_of': opts.asOf } : {}),\n ...(opts.includeQuarantined === true\n ? { 'memory.search.semantic.include_quarantined': true }\n : {}),\n });\n return ranked;\n },\n );\n }\n\n /**\n * Gated, iterative (\"deep\") recall for hard queries (P2-4). A cheap\n * local heuristic ({@link assessQueryDifficulty}) decides whether the\n * query is even a loop candidate; simple lookups take exactly one\n * {@link search} pass and make no provider call. For a query judged\n * hard *and* with a grader configured\n * (`createMemory({ iterativeRetrieval })`), the retrieved set is graded\n * for sufficiency and, when weak, the query is reformulated and\n * retrieved again — **widening to one-hop graph expansion**\n * (`expandHops: 1`) on each reformulation pass — up to `maxIterations`\n * (hard-capped at 5). When still insufficient it returns\n * `abstained: true` so the caller can decline to answer instead of\n * confabulating.\n *\n * Without a grader (the offline default) this degrades to a single,\n * difficulty-gated `search` and never calls a provider.\n *\n * @stable\n */\n async searchIterative(\n scope: SessionScope,\n query: string,\n opts: IterativeSearchOptions = {},\n ): Promise<IterativeRecallResult> {\n // Reuse the `memory.search.semantic` span type (the inner per-pass\n // searches nest under it); iterative-specific data is namespaced under\n // the `…iterative.*` attribute prefix so no new core SpanType is\n // needed and P2-4 stays memory-only.\n return withMemorySpan(\n this.#tracer,\n 'memory.search.semantic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const result = await runIterativeRetrieval<MemoryHit<Fact>>(\n query,\n {\n // `maxIterations` / `forceHard` ride along harmlessly (search\n // ignores unknown keys); `expandHops: 1` widens recall to the\n // P2-1 graph on reformulation passes, and the per-pass signal\n // overrides any inherited one.\n retrieve: (q, widen, signal) =>\n this.search(scope, q, {\n ...opts,\n ...(widen ? { expandHops: 1 } : {}),\n ...(signal !== undefined ? { signal } : {}),\n }),\n snippetOf: (hit) => hit.record.text,\n idOf: (hit) => hit.record.id,\n grader: this.#grader,\n // MRET-2: re-fuse the per-pass lists with RRF before the\n // final topK cut so a reformulation-pass find can outrank\n // pass-1 noise (discovery order silently dropped it).\n fuse: (lists) => fuseRrf(lists, 60),\n },\n {\n maxIterations: opts.maxIterations ?? this.#iterativeMaxIterations,\n maxResults: opts.topK ?? 10,\n ...(opts.forceHard !== undefined ? { forceHard: opts.forceHard } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n },\n );\n span.setAttributes({\n 'memory.search.semantic.iterative.gate_hard': result.gateHard,\n 'memory.search.semantic.iterative.iterations': result.iterations,\n 'memory.search.semantic.iterative.sufficient': result.sufficient,\n 'memory.search.semantic.iterative.abstained': result.abstained,\n 'memory.search.semantic.iterative.query_count': result.queries.length,\n 'memory.search.semantic.iterative.final_count': result.hits.length,\n });\n return result;\n },\n );\n }\n\n /**\n * Raw vector KNN neighbours for the consolidator's reconcile\n * pre-filter (P0-3). Unlike {@link search} this skips FTS, reranking,\n * and decay so the cosine scores survive intact (the conflict-pipeline\n * zone thresholds are calibrated against them), and it **includes\n * quarantined facts** so prior synthesized memories are visible to\n * reconciliation. Returns `[]` when no embedder / vector adapter is\n * configured — the consolidator then treats every candidate as a\n * fresh `add`, degrading gracefully to the pre-reconcile behaviour.\n *\n * @stable\n */\n async neighbors(\n scope: SessionScope,\n text: string,\n opts: { readonly topK?: number } = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return this.#tryVectorSearch(scope, text, opts.topK ?? 10, undefined, true);\n }\n\n /** Lookup a single fact by id. Returns `null` for soft-deleted / missing. */\n async get(scope: SessionScope, factId: string): Promise<Fact | null> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.semantic',\n scope,\n { 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.get !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.get(...) requires a storage adapter that implements `semantic.get(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const fact = await this.#store.semantic.get(factId);\n span.setAttributes({ 'memory.read.semantic.found': fact !== null });\n return fact;\n },\n );\n }\n\n /**\n * Return the full bi-temporal supersede chain that `factId` belongs\n * to, oldest → newest, including superseded / soft-deleted rows so\n * callers can answer \"how did this fact change over time\". Requires\n * a storage adapter that implements\n * `SemanticMemoryStoreExt.historyOf(...)` — the default\n * `@graphorin/store-sqlite` adapter wires this through. P0-2.\n *\n * @stable\n */\n async history(scope: SessionScope, factId: string): Promise<ReadonlyArray<Fact>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.semantic',\n scope,\n { 'memory.semantic.action': 'history', 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.historyOf !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.history(...) requires a storage adapter that implements `semantic.historyOf(scope, id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const chain = await this.#store.semantic.historyOf(scope, factId);\n span.setAttributes({ 'memory.read.semantic.history_length': chain.length });\n return chain;\n },\n );\n }\n\n /**\n * Promote a quarantined fact to `active` (P1-4). The validation path\n * that admits a synthesized memory into action-driving recall once a\n * human (or trusted non-agent caller) has reviewed it. Writes a\n * `memory_history` audit row. Requires a storage adapter that\n * implements `SemanticMemoryStoreExt.setStatus(...)` — the default\n * `@graphorin/store-sqlite` adapter wires this through.\n *\n * MRET-3 / MST-1: promotion of a fact whose text still trips the\n * offline injection heuristics is **refused** with\n * {@link QuarantinePromotionRefusedError} — the model-facing\n * `fact_validate` tool calls this with no `force`, so a poisoned\n * memory can never be promoted by the agent itself (the one-turn\n * `fact_remember(poison)` → `fact_validate(id)` chain is closed). An\n * operator can override after review by passing `{ force: true }`\n * from a trusted (non-agent) context. Synthesized-but-clean writes\n * (consolidator / reflection) promote normally.\n *\n * @stable\n */\n async validate(\n scope: SessionScope,\n factId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'validate', 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.validate(...) requires a storage adapter that implements `semantic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n // Re-derive the injection verdict from the stored (immutable)\n // text. A poison candidate stays flagged forever, so this gate\n // does not depend on persisting a quarantine reason.\n const existing = await this.#fetchExisting(factId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({\n 'memory.semantic.validate.refused': true,\n 'memory.semantic.injection_markers': injection.markers.join(','),\n });\n throw new QuarantinePromotionRefusedError(factId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.semantic.validate.forced': force });\n await this.#store.semantic.setStatus(factId, 'active', reason);\n },\n );\n }\n\n /** Mark `oldId` superseded by a new fact. Returns the new record. */\n async supersede(\n scope: SessionScope,\n oldId: string,\n newInput: FactInput,\n reason?: string,\n ): Promise<{ readonly old: string; readonly new: Fact }> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'supersede' },\n async () => {\n // Bypass the conflict pipeline — the explicit supersede call\n // is the user's authoritative decision; a second pipeline\n // pass would race with itself (the new fact is by definition\n // a near-dup of the old one) and could silently reroute the\n // write into a `dedup` outcome.\n const newFact = await this.remember(\n scope,\n { ...newInput, supersedes: oldId },\n { pipeline: 'off' },\n );\n await this.#store.semantic.supersede(oldId, newFact, reason);\n return { old: oldId, new: newFact };\n },\n );\n }\n\n /** Soft-delete a fact (kept for replay; never hard-deleted). */\n async forget(scope: SessionScope, factId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'forget', 'memory.semantic.fact_id': factId },\n async () => {\n await this.#store.semantic.forget(factId, reason);\n },\n );\n }\n\n /**\n * Hard-delete a fact (GDPR path). Distinct from {@link forget}: the\n * record is removed from storage entirely instead of soft-archived.\n * Requires a storage adapter that implements\n * `SemanticMemoryStoreExt.purge(...)` — the default\n * `@graphorin/store-sqlite` adapter wires this through.\n */\n async purge(scope: SessionScope, factId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'purge', 'memory.semantic.fact_id': factId },\n async () => {\n if (typeof this.#store.semantic.purge !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.purge(...) requires a storage adapter that implements `semantic.purge(id)`. ' +\n 'For storage adapters without GDPR-grade hard-delete, prefer `SemanticMemory.forget(...)`.',\n );\n }\n await this.#store.semantic.purge(factId, reason);\n },\n );\n }\n\n /** Fuse multiple ranked lists outside of a `search()` call. */\n async fuse(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<Fact>>>,\n options: { topK?: number; signal?: AbortSignal } = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return this.#reranker.rerank(query, lists, options);\n }\n\n /** Pure-fusion helper — exposed for callers that already collected results. */\n static fuseRrf<TRecord extends Fact>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n k = 60,\n ): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseRrf(lists, k);\n }\n\n /**\n * Pure weighted-fusion helper (X-2) — like {@link SemanticMemory.fuseRrf}\n * but scales each list `i`'s reciprocal-rank contribution by\n * `weights[i]`. A missing / invalid entry defaults to `1`, so equal or\n * absent weights reproduce RRF.\n */\n static fuseWeighted<TRecord extends Fact>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n weights: ReadonlyArray<number> | undefined,\n k = 60,\n ): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseWeighted(lists, weights, k);\n }\n\n /**\n * Multi-query expansion (P2-3). Returns the original query followed by\n * up to `multiQuery - 1` deduped reworded variants. A no-op (`[query]`)\n * when `multiQuery` is unset / `<= 1` or no transformer is configured —\n * so the default path makes no provider call. A transformer failure\n * degrades to `[query]` rather than breaking recall.\n */\n async #expandQueries(query: string, opts: FactSearchOptions): Promise<ReadonlyArray<string>> {\n const n = opts.multiQuery;\n if (n === undefined || n <= 1 || this.#queryTransformer === null) return [query];\n try {\n const variants = await this.#queryTransformer.expand(query, n - 1, {\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const seen = new Set<string>([query.trim().toLowerCase()]);\n const out: string[] = [query];\n for (const variant of variants) {\n const trimmed = variant.trim();\n const key = trimmed.toLowerCase();\n if (trimmed.length === 0 || seen.has(key)) continue;\n seen.add(key);\n out.push(trimmed);\n if (out.length >= n) break;\n }\n return out;\n } catch {\n return [query];\n }\n }\n\n /**\n * HyDE retrieval (P2-3). Generates a hypothetical answer, embeds it,\n * and returns its vector neighbours to fuse into the result. A no-op\n * (`[]`) unless `hyde` is set, a transformer is configured, **and** an\n * embedder + vector surface exist — the embedder guard is checked\n * *first* so a missing embedder skips the LLM call entirely rather than\n * generating a passage that can never be embedded.\n */\n async #tryHyde(\n scope: SessionScope,\n query: string,\n opts: FactSearchOptions,\n topK: number,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n if (opts.hyde !== true || this.#queryTransformer === null) return [];\n if (\n this.#embedder === null ||\n this.#embedderIdProvider() === null ||\n typeof this.#store.semantic.searchVector !== 'function'\n ) {\n return [];\n }\n try {\n const pseudo = await this.#queryTransformer.hypothetical(query, {\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n if (pseudo === null || pseudo.trim().length === 0) return [];\n return this.#tryVectorSearch(scope, pseudo, topK, opts.asOf, opts.includeQuarantined);\n } catch {\n return [];\n }\n }\n\n /**\n * One-hop graph expansion (P2-1). Seeds on the unique fact ids already\n * gathered into `lists` and fuses in facts sharing a canonical entity,\n * via the adapter's recursive-CTE `graph.expandOneHop`. A no-op (`[]`)\n * unless `expandHops >= 1` and the adapter exposes `graph` — and\n * resilient: a traversal error degrades to no expansion rather than\n * breaking recall. Neighbours carry a `graph` signal so explanations\n * (X-3) can attribute a hit to the hop.\n */\n async #tryExpandHops(\n scope: SessionScope,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<Fact>>>,\n opts: FactSearchOptions,\n limit: number,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n const hops = opts.expandHops ?? 0;\n const graphStore = this.#store.graph;\n if (hops < 1 || graphStore === undefined || typeof graphStore.expandOneHop !== 'function') {\n return [];\n }\n const seedIds: string[] = [];\n const seen = new Set<string>();\n for (const list of lists) {\n for (const hit of list) {\n if (!seen.has(hit.record.id)) {\n seen.add(hit.record.id);\n seedIds.push(hit.record.id);\n }\n }\n }\n if (seedIds.length === 0) return [];\n try {\n const neighbours = await graphStore.expandOneHop(scope, seedIds, {\n maxHops: hops,\n limit,\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n });\n return neighbours.map((fact) => ({\n record: fact,\n score: 1,\n signals: Object.freeze({ graph: 1 }),\n }));\n } catch {\n return [];\n }\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.semantic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for asymmetric (E5) embedders.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.semantic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n );\n }\n\n async #applyDecay(\n scope: SessionScope,\n hits: ReadonlyArray<MemoryHit<Fact>>,\n decay: FactSearchOptions['decay'],\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n if (decay === undefined || hits.length === 0) return hits;\n // MRET-8: read decay columns ONLY for the hit ids (narrow IN-list)\n // instead of re-reading the scope's 1000 oldest rows per search —\n // the old window was O(scope) per query AND, being the LRU head,\n // could even miss the hits entirely.\n const semantic = this.#store.semantic;\n let rows: ReadonlyArray<{\n readonly id: string;\n readonly strength: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n }>;\n if (typeof semantic.listDecaySignals === 'function') {\n rows = await semantic.listDecaySignals(hits.map((h) => h.record.id));\n } else if (typeof semantic.listForDecay === 'function') {\n rows = await semantic.listForDecay(scope, 1000);\n } else {\n return hits;\n }\n if (rows.length === 0) return hits;\n const signals = new Map(\n rows.map((row) => [\n row.id,\n {\n strength: row.strength,\n lastAccessedAt: row.lastAccessedAt,\n createdAt: row.createdAt,\n },\n ]),\n );\n const now = (decay.now ?? Date.now)();\n const tauMs = Math.max(1, decay.tauDays) * 24 * 60 * 60 * 1000;\n const out = hits.map((hit) => {\n const sig = signals.get(hit.record.id);\n if (sig === undefined) return hit;\n const reference = sig.lastAccessedAt ?? sig.createdAt;\n const elapsed = Math.max(0, now - reference);\n const retention = Math.exp(-elapsed / (tauMs * Math.max(0.5, sig.strength)));\n // Record the decay multiplier as a signal so recall explanations\n // (X-3) can attribute the score drop to staleness, not just to\n // fusion. Hits with no decay row keep their signals untouched.\n return {\n ...hit,\n score: hit.score * retention,\n signals: Object.freeze({ ...(hit.signals ?? {}), decay: retention }),\n };\n });\n const sorted = [...out].sort((a, b) => b.score - a.score);\n return Object.freeze(sorted);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"semantic-memory.js","names":["#store","#tracer","#embedder","#embedderIdProvider","#pipeline","#contextualMode","#queryTransformer","#entityResolver","#grader","#iterativeMaxIterations","#trustWeights","#reranker","status: MemoryStatus","quarantineReason: 'injection' | 'synthesized' | undefined","fact: Fact","#resolveIndexText","decision: ConflictDecision","#commitFact","#linkEntities","#fetchExisting","#expandQueries","lists: Array<ReadonlyArray<MemoryHit<Fact>>>","listWeights: number[]","listLabels: string[]","#tryVectorSearch","#tryHyde","#tryExpandHops","#tryEntityMatch","reranker: ReRanker","#applyDecay","#applyTrustDiscount","out: string[]","seedIds: string[]","out: MemoryHit<Fact>[]","rows: ReadonlyArray<{\n readonly id: string;\n readonly strength: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n }>"],"sources":["../../src/tiers/semantic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Fact,\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { md5 } from '@graphorin/core';\nimport type { ConflictDecision, ConflictPipeline } from '../conflict/index.js';\nimport { DEFAULT_SALIENCE_WEIGHTS, type SalienceWeights } from '../consolidator/decay.js';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { type EntityResolver, normalizeEntityName } from '../graph/entity-resolver.js';\nimport { contextualize } from '../internal/contextualize.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport { explainRecall } from '../search/explain.js';\nimport { pprActivation } from '../search/graph-ppr.js';\nimport {\n DEFAULT_MAX_ITERATIONS,\n type IterativeRetrievalResult,\n type RetrievalGrader,\n runIterativeRetrieval,\n} from '../search/iterative.js';\nimport type { QueryTransformer } from '../search/query-transform.js';\nimport { fuseRrf, fuseWeighted, WeightedRRFReranker } from '../search/rrf.js';\nimport { trustDiscount } from '../search/trust.js';\nimport type { ReRanker } from '../search/types.js';\n\n/**\n * Author-time fact payload accepted by {@link SemanticMemory.remember}.\n * The framework derives `id`, `kind: 'semantic'`, `userId`,\n * `createdAt`, `updatedAt`, `validFrom`, the optional `embedder_id`,\n * and the deduplication `hash` from this input.\n *\n * @stable\n */\nexport interface FactInput {\n readonly text: string;\n readonly subject?: string;\n readonly predicate?: string;\n readonly object?: string;\n readonly tags?: ReadonlyArray<string>;\n readonly confidence?: number;\n readonly sensitivity?: Sensitivity;\n readonly validFrom?: string;\n readonly validTo?: string;\n readonly supersedes?: string;\n /**\n * Trust-provenance tag (P1-4). Writers that synthesize memory pass\n * `'extraction'` / `'reflection'` so the fact lands quarantined;\n * first-party writers pass `'user'` / `'tool'` (or omit it - absent ⇒\n * treated as first-party `active`). The `status` is *derived* from\n * this tag plus the injection heuristics; it is never author-set.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Importance hint in `[0, 1]` (X-1 / MCON-12). Feeds the multi-signal\n * salience score that orders decay archiving and capacity eviction -\n * higher importance ⇒ evicted later. Values are clamped to `[0, 1]`;\n * non-finite values are dropped. The consolidator's extraction pass\n * fills this from the model's per-fact 1-10 rating\n * (`normalizeImportance`); absent ⇒ the neutral midpoint at scoring\n * time.\n */\n readonly importance?: number;\n /**\n * Principal dimension (D3). The consolidator stamps `'agent'` on\n * synthesized writes; user-authored writes may pass `'user'`. Absent\n * (the default) leaves the column NULL - treated as `'user'` at\n * filter time. Never gates default recall.\n */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Per-list weights for {@link FusionStrategy} `'weighted'` fusion (X-2),\n * keyed by retriever *kind* rather than position so they survive the\n * P2-3 multi-query fan-out (which appends extra FTS / vector candidate\n * lists). Each defaults to the neutral `1` (≡ RRF). The HyDE list is a\n * vector list and takes the `vector` weight.\n *\n * @stable\n */\nexport interface FusionWeights {\n /** Weight applied to every FTS5 (lexical) candidate list. Default `1`. */\n readonly fts?: number;\n /** Weight applied to every vector (incl. HyDE) candidate list. Default `1`. */\n readonly vector?: number;\n /**\n * Weight applied to the graph-expansion candidate list (D5). Default\n * `1` (the previous neutral behaviour). Tune it once the graph leg's\n * reliability is calibrated against labels - the roadmap's \"graph as a\n * first-class tunable fusion weight\" (was hardcoded neutral).\n */\n readonly graph?: number;\n /** Weight applied to the exact entity-match candidate list (D5). Default `1`. */\n readonly entity?: number;\n}\n\n/**\n * Score-fusion strategy for {@link SemanticMemory.search} (X-2).\n *\n * - `'rrf'` (the default when `fusion` is omitted) fuses the candidate\n * lists through the configured reranker - the zero-tuning\n * {@link RRFReranker} unless one was overridden.\n * - `'weighted'` fuses through {@link WeightedRRFReranker}, scaling each\n * list's reciprocal-rank contribution by its {@link FusionWeights}, for\n * callers who have calibrated retriever reliability against labels (the\n * P0-1 eval harness). At equal weights it reproduces RRF.\n *\n * @stable\n */\nexport type FusionStrategy =\n | { readonly strategy: 'rrf' }\n | {\n readonly strategy: 'weighted';\n readonly weights: FusionWeights;\n /** Override the RRF constant for the weighted fuse. Default `60`. */\n readonly k?: number;\n };\n\n/**\n * Per-call options accepted by {@link SemanticMemory.search}.\n *\n * @stable\n */\nexport interface FactSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n /** Override the per-list candidate count (default `60`). */\n readonly candidateTopK?: number;\n /**\n * Any-of tags filter (MRET-4). A fact matches when it carries at\n * least one of the requested tags; untagged facts never match.\n * Applied in-store on the FTS leg and as a record-level filter on\n * the fused result so every candidate leg (vector / HyDE / graph)\n * obeys it.\n */\n readonly tags?: ReadonlyArray<string>;\n /**\n * Point-in-time (\"as of\") read. When set, only facts whose\n * bi-temporal validity interval contains this instant are returned\n * (`valid_from <= asOf < valid_to`, open-ended bounds allowed),\n * applied at the store layer to both the FTS and vector candidate\n * lists. ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined facts in the result (P1-4). Defaults to\n * `false` - action-driving recall (`fact_search`, auto-recall) never\n * returns quarantined rows. Set `true` only for the validation /\n * inspector path that surfaces quarantined facts to a human for\n * promotion via {@link SemanticMemory.validate}.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n /**\n * Rank-time trust discount (C5). `'on'` (default) multiplies each\n * hit's fused score by its trust factor - quarantined-but-included\n * rows by `1 - quarantine` (default 0.3x), foreign-provenance rows by\n * `1 - foreignProvenance` (default 0.8x); first-party active facts are\n * untouched. `'off'` restores pure similarity ranking (inspector /\n * calibration paths). Factors surface as the `trust` signal on hits\n * and in `explainRecall`.\n *\n * @stable\n */\n readonly trustWeighting?: 'on' | 'off';\n /**\n * Include superseded / validity-expired facts (memory-retrieval-01).\n * Defaults to `false`: a default read behaves as `asOf = now`, so a\n * fact whose validity interval was closed (e.g. by `supersede`)\n * never surfaces as current - what the `fact_supersede` tool\n * promises. Set `true` only for inspector / audit paths. Ignored\n * when an explicit {@link asOf} is supplied.\n *\n * @stable\n */\n readonly includeSuperseded?: boolean;\n /**\n * Optional decay-aware ranking. When set, the reranker output is\n * post-multiplied by the per-fact retention curve\n * `score *= exp(-elapsedDays / tauDays)` so stale facts drop in\n * the result list without ever being hard-deleted (principle 8).\n * Requires the storage adapter to expose\n * `semantic.listForDecay?(...)` so the search can read\n * `strength` + `lastAccessedAt`; adapters without the surface\n * skip the boost silently.\n *\n * @stable\n */\n readonly decay?: {\n readonly tauDays: number;\n /** Override the wall clock (test seam). */\n readonly now?: () => number;\n };\n /**\n * Multi-query / RAG-Fusion (P2-3). When set to `N > 1` *and* a query\n * transformer is configured (`createMemory({ queryTransform })`), the\n * query is fanned into up to `N - 1` reworded variants via one cheap\n * LLM call; each variant is retrieved (FTS + vector) and **all** lists\n * are fused through the existing RRF reranker - recovering memories\n * whose stored wording differs from the user's phrasing. `N` bounds\n * the *total* query strings, including the original. Offline (no\n * transformer, or `N <= 1`) this is a **silent no-op**: search stays\n * single-shot and makes no provider call. Opt-in + retrieval-heavy, so\n * reserve it for deliberate recall rather than every search.\n *\n * @stable\n */\n readonly multiQuery?: number;\n /**\n * HyDE - Hypothetical Document Embeddings (arXiv:2212.10496), P2-3.\n * When `true` *and* both a query transformer and an embedder are\n * configured, generate a short hypothetical answer, embed it, and fuse\n * its vector neighbours into the result. Helps short / ambiguous\n * queries but adds a generate + embed round-trip and can drift - hence\n * opt-in. With no transformer (or no embedder) this is a silent no-op\n * and no provider call is made.\n *\n * @stable\n */\n readonly hyde?: boolean;\n /**\n * Score-fusion strategy (X-2). Omitted (the default) ⇒ RRF via the\n * configured reranker - behaviour is unchanged. `{ strategy:\n * 'weighted', weights }` fuses through {@link WeightedRRFReranker},\n * up-/down-weighting the FTS vs vector candidate lists per\n * {@link FusionWeights}; reserve it for callers who have calibrated the\n * weights against labels (the P0-1 eval harness). At equal weights it\n * reproduces RRF.\n *\n * @stable\n */\n readonly fusion?: FusionStrategy;\n /**\n * One-hop graph expansion (P2-1). With `1` *and* a graph-capable\n * storage adapter (`store.graph`), the facts retrieved by the lexical /\n * vector candidate pass are treated as seeds: facts sharing a canonical\n * entity (subject / object) are fetched via a recursive CTE and fused\n * in as an extra candidate list before rerank - surfacing connected\n * facts the query never matched directly (\"what did the person I met in\n * Tbilisi recommend?\"). `0` (the default) or a graph-less adapter ⇒ a\n * silent no-op; recall is unchanged. Opt-in + retrieval-heavy. `2`\n * (D5) widens to two-hop expansion - pair with `graphScoring: 'ppr'`\n * so distant neighbours decay instead of tying with direct ones.\n *\n * @stable\n */\n readonly expandHops?: 0 | 1 | 2;\n /**\n * Graph-expansion scoring (D5). `'flat'` (the default) scores every\n * graph neighbour `1` (the pre-D5 behaviour). `'ppr'` uses PPR-lite\n * damped spreading activation (`damping^hopDepth`) via the store's\n * `expandActivation`, so a two-hop neighbour ranks below a direct one;\n * a no-op without `expandHops >= 1` or a graph-capable adapter.\n *\n * @stable\n */\n readonly graphScoring?: 'flat' | 'ppr';\n /**\n * Exact entity-match retriever (D5). When set, the query terms are\n * normalized to entity names and facts linked to a matching canonical\n * entity are fused in as a precise candidate leg (`entity` fusion\n * weight), distinct from the fuzzy FTS / vector legs. A no-op without\n * a graph-capable adapter. Opt-in.\n *\n * @stable\n */\n readonly entityMatch?: boolean;\n /**\n * Retrieval-time principal filter (D3). When set, only facts whose\n * `owner` is in the requested set are returned - `'user'` for\n * user-stated facts, `'agent'` for the agent's own inferences,\n * `'shared'` for multi-agent shared records. Rows written before the\n * feature (owner NULL) are treated as `'user'`. Applied in-store on\n * the FTS + vector legs and as a record-level filter on the fused\n * result so the HyDE / graph legs obey it too. Absent (the default)\n * ⇒ **no owner filter** - recall is byte-identical.\n *\n * @stable\n */\n readonly owner?: MemoryOwner | ReadonlyArray<MemoryOwner>;\n}\n\n/**\n * Per-call options for {@link SemanticMemory.searchIterative} (P2-4) -\n * the gated grade-then-reformulate loop. Extends {@link FactSearchOptions}\n * (every base option applies to each retrieval pass); `topK` doubles as\n * the cap on the accumulated result count.\n *\n * @stable\n */\nexport interface IterativeSearchOptions extends FactSearchOptions {\n /**\n * Total-pass cap, clamped to `[1, 5]`. Omitted ⇒ the facade-configured\n * default (`createMemory({ iterativeRetrieval: { maxIterations } })`)\n * or `3`.\n */\n readonly maxIterations?: number;\n /**\n * Skip the heuristic difficulty gate and force the loop (still capped\n * and still a no-op without a grader). For deliberate \"deep recall\"\n * requests and tests.\n */\n readonly forceHard?: boolean;\n}\n\n/**\n * Outcome of {@link SemanticMemory.searchIterative}. Beyond the ranked\n * `hits`, `sufficient` / `abstained` tell the caller whether the memory\n * actually answered the query - `abstained: true` means it should say so\n * rather than confabulate.\n *\n * @stable\n */\nexport type IterativeRecallResult = IterativeRetrievalResult<MemoryHit<Fact>>;\n\n/**\n * Per-call options accepted by {@link SemanticMemory.remember}. The\n * Phase 10b pipeline writes one row to `fact_conflicts` (and\n * potentially one to `conflict_check_pending`) for every invocation;\n * pass `pipeline: 'off'` to bypass the pipeline for a single call\n * (useful for one-shot data imports).\n *\n * @stable\n */\nexport interface FactRememberOptions {\n readonly pipeline?: 'on' | 'off';\n /** Cancellation signal forwarded to the embedder + storage layers. */\n readonly signal?: AbortSignal;\n /**\n * Precomputed contextual-retrieval index text (P1-3, advanced). When\n * supplied it overrides the instance's `'late-chunk'` computation: the\n * embedding is computed from - and the FTS row indexed against - this\n * text, while the canonical `text` is stored unchanged. The background\n * consolidator passes this in its `'llm'` mode (the one place an LLM is\n * allowed to write the situating context); first-party callers normally\n * omit it and rely on the offline late-chunk default.\n */\n readonly indexText?: string;\n /**\n * Auto-promotion policy (MCON-2). When `true`, a *synthesized* write\n * (consolidator extraction) that is **clean** by the injection heuristics is\n * stored `active` instead of quarantined. Injection-flagged writes always\n * stay quarantined - the security gate is preserved. Off by default; the\n * consolidator passes it only when the operator opts in via\n * `autoPromoteExtraction`. No effect on non-synthesized writes.\n */\n readonly autoPromoteSynthesized?: boolean;\n}\n\n/**\n * Returned by {@link SemanticMemory.remember}. The `fact` is the\n * stored row (which may be the *existing* fact when the pipeline\n * dedups). The `decision` mirrors the pipeline outcome so callers can\n * distinguish silent dedups from active inserts.\n *\n * @stable\n */\nexport interface RememberOutcome {\n readonly fact: Fact;\n readonly decision: ConflictDecision;\n /**\n * Why this write landed quarantined, if it did (P1-4 / MRET-3).\n * `'injection'` - the offline injection heuristics flagged the text\n * (a memory-poisoning candidate). `'synthesized'` - a consolidator /\n * reflection / induction write awaiting validation. Absent when the\n * fact is `active` or when a dedup returned a pre-existing row.\n */\n readonly quarantineReason?: 'injection' | 'synthesized';\n}\n\n/**\n * `SemanticMemory` - long-term factual store. Hybrid (vector + FTS5)\n * search merges the two ranked lists through the configured\n * {@link ReRanker} (default {@link RRFReranker} with `k = 60`).\n *\n * Phase 10a wrote facts straight through with MD5 deduplication;\n * Phase 10b routes every `remember(...)` call through the multi-stage\n * conflict resolution pipeline (DEC-117 / ADR-018 ext / RB-02). The\n * pipeline can be disabled per-call (`pipeline: 'off'`) or per-`Memory`\n * instance (`createMemory({ conflictPipeline: { mode: 'off' } })`).\n *\n * @stable\n */\nexport class SemanticMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n readonly #pipeline: ConflictPipeline | null;\n readonly #contextualMode: 'off' | 'late-chunk';\n readonly #queryTransformer: QueryTransformer | null;\n readonly #entityResolver: EntityResolver | null;\n readonly #grader: RetrievalGrader | null;\n readonly #iterativeMaxIterations: number;\n #reranker: ReRanker;\n readonly #trustWeights: SalienceWeights;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n reranker: ReRanker;\n conflictPipeline?: ConflictPipeline;\n /**\n * Query transformer for multi-query / HyDE retrieval (P2-3). When\n * supplied, `search(..., { multiQuery })` / `{ hyde }` opt into one\n * cheap LLM call to rewrite / hypothesize the query; omitted (the\n * default) ⇒ those options are silent no-ops and search stays\n * offline + single-shot.\n */\n queryTransformer?: QueryTransformer;\n /**\n * Contextual-retrieval mode for the write path (P1-3). `'late-chunk'`\n * (default) prepends a deterministic situating context to the text\n * that is embedded + FTS-indexed, leaving the canonical `text`\n * untouched; `'off'` indexes the bare text. The hot write path never\n * makes an LLM call - the `'llm'` enrichment is confined to the\n * background consolidator, which supplies a precomputed `indexText`.\n */\n contextualRetrieval?: 'off' | 'late-chunk';\n /**\n * Entity resolver for the relation graph (P2-1). When supplied,\n * `remember(...)` resolves a fact's subject / object to canonical\n * entities and links them, enabling `search(..., { expandHops: 1 })`.\n * Omitted (the default) ⇒ writes carry s/p/o but form no entity\n * links, and the write path stays offline + unchanged.\n */\n entityResolver?: EntityResolver;\n /**\n * Retrieval grader for the gated iterative loop (P2-4). When\n * supplied, `searchIterative(...)` can grade a retrieved set and\n * reformulate on hard queries; omitted (the default) ⇒\n * `searchIterative` runs a single, difficulty-gated pass and makes no\n * provider call.\n */\n grader?: RetrievalGrader;\n /** Default total-pass cap for `searchIterative`. Default 3. */\n iterativeMaxIterations?: number;\n /**\n * Weights for the rank-time trust discount (C5). Reuses the\n * eviction-path `SalienceWeights` semantics; defaults to\n * `DEFAULT_SALIENCE_WEIGHTS`.\n */\n trustWeights?: SalienceWeights;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n this.#reranker = args.reranker;\n this.#pipeline = args.conflictPipeline ?? null;\n this.#contextualMode = args.contextualRetrieval ?? 'late-chunk';\n this.#queryTransformer = args.queryTransformer ?? null;\n this.#entityResolver = args.entityResolver ?? null;\n this.#grader = args.grader ?? null;\n this.#iterativeMaxIterations = args.iterativeMaxIterations ?? DEFAULT_MAX_ITERATIONS;\n this.#trustWeights = args.trustWeights ?? DEFAULT_SALIENCE_WEIGHTS;\n }\n\n /** Replace the active reranker. Returns the previous instance. */\n setReranker(reranker: ReRanker): ReRanker {\n const previous = this.#reranker;\n this.#reranker = reranker;\n return previous;\n }\n\n /** Currently active reranker. */\n reranker(): ReRanker {\n return this.#reranker;\n }\n\n /**\n * Persist a fact. Returns the stored record. Phase 10b routes every\n * call through the multi-stage conflict resolution pipeline; the\n * legacy straight-through path is reachable per-call via\n * `{ pipeline: 'off' }` (operators may disable the pipeline globally\n * via `createMemory({ conflictPipeline: { mode: 'off' } })`).\n */\n async remember(\n scope: SessionScope,\n input: FactInput,\n options: FactRememberOptions = {},\n ): Promise<Fact> {\n const { fact } = await this.rememberWithDecision(scope, input, options);\n return fact;\n }\n\n /**\n * Like {@link remember} but returns the pipeline `decision` alongside\n * the stored fact. Useful for callers that need to distinguish\n * silent dedups (`decision.kind === 'dedup'`) from fresh inserts.\n *\n * @stable\n */\n async rememberWithDecision(\n scope: SessionScope,\n input: FactInput,\n options: FactRememberOptions = {},\n ): Promise<RememberOutcome> {\n return withMemorySpan(this.#tracer, 'memory.write.semantic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const text = input.text;\n // P1-4: derive the retrieval-trust status. Synthesized writes\n // (consolidator extraction / reflection) and candidates that trip\n // the offline injection heuristics land quarantined - excluded\n // from default recall until a human validates them. First-party\n // writes (user / tool / imported / unset) stay active. `status`\n // is never author-set; it is always derived here.\n const provenance = input.provenance;\n const synthesized =\n provenance === 'extraction' || provenance === 'reflection' || provenance === 'induction';\n const injection = detectMemoryInjection(text);\n // MCON-2 auto-promotion: an opted-in, injection-clean synthesized write is\n // admitted `active`. Injection-flagged writes always stay quarantined.\n const autoPromote = options.autoPromoteSynthesized === true;\n const status: MemoryStatus = injection.flagged\n ? 'quarantined'\n : synthesized && !autoPromote\n ? 'quarantined'\n : 'active';\n // MRET-3: surface *why* a fresh write was quarantined so callers\n // (the fact_remember tool, harnesses) can tell an injection-flagged\n // poison candidate apart from a synthesized-but-clean consolidator\n // write. Injection takes precedence - it is the security-relevant\n // reason and gates promotion in validate().\n const quarantineReason: 'injection' | 'synthesized' | undefined =\n status === 'quarantined' ? (injection.flagged ? 'injection' : 'synthesized') : undefined;\n const fact: Fact = {\n id: newMemoryId('fact'),\n kind: 'semantic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n text,\n // P2-1: carry the s/p/o triple onto the persisted fact (it used to\n // be dropped here, surviving only in the late-chunk index text).\n // This is what activates the relation-graph substrate.\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n ...(input.confidence !== undefined ? { confidence: input.confidence } : {}),\n // X-1 / MCON-12: the importance hint that drives salience-ordered\n // forgetting. Clamped to [0, 1]; non-finite ⇒ unscored.\n ...(typeof input.importance === 'number' && Number.isFinite(input.importance)\n ? { importance: Math.min(1, Math.max(0, input.importance)) }\n : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.validFrom !== undefined ? { validFrom: input.validFrom } : { validFrom: now }),\n ...(input.validTo !== undefined ? { validTo: input.validTo } : {}),\n ...(input.supersedes !== undefined ? { supersedes: input.supersedes } : {}),\n ...(provenance !== undefined ? { provenance } : {}),\n status,\n // D3: carry the principal dimension; absent ⇒ NULL (treated 'user').\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n const embedder = this.#embedder;\n // P1-3: the text that is embedded + FTS-indexed. A caller-supplied\n // `indexText` (the consolidator's `'llm'` mode) wins; otherwise the\n // instance mode decides between the bare text (`'off'`) and the\n // offline late-chunk prefix derived from the *author-supplied*\n // signals on `input` (entities / timeframe / topics). The canonical\n // `fact.text` persisted below is never altered.\n const indexText = this.#resolveIndexText(input, fact.text, options.indexText);\n const pipelineEnabled = this.#pipeline !== null && (options.pipeline ?? 'on') === 'on';\n const decision: ConflictDecision =\n pipelineEnabled && this.#pipeline !== null\n ? await this.#pipeline.run(\n {\n store: this.#store,\n tracer: this.#tracer,\n embedder,\n embedderId,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n },\n fact,\n )\n : { kind: 'admit', stage: 'exact-dedup', reason: 'pipeline-skipped' };\n\n span.setAttributes({\n 'memory.semantic.text_length': text.length,\n 'memory.semantic.hash': md5(text),\n 'memory.semantic.pipeline.decision': decision.kind,\n 'memory.semantic.pipeline.stage': decision.stage,\n 'memory.semantic.pipeline.enabled': pipelineEnabled,\n 'memory.semantic.status': status,\n 'memory.semantic.contextualized': indexText !== text,\n ...(provenance !== undefined ? { 'memory.semantic.provenance': provenance } : {}),\n 'memory.semantic.injection_flagged': injection.flagged,\n ...(injection.flagged\n ? { 'memory.semantic.injection_markers': injection.markers.join(',') }\n : {}),\n });\n\n const reasonField = quarantineReason !== undefined ? { quarantineReason } : {};\n switch (decision.kind) {\n case 'admit':\n case 'pending': {\n await this.#commitFact(fact, embedder, embedderId, indexText);\n await this.#linkEntities(scope, fact, options.signal);\n return { fact, decision, ...reasonField };\n }\n case 'dedup': {\n const existing = await this.#fetchExisting(decision.existingId);\n // The candidate was never committed; the existing row's own\n // status is authoritative, so do not attach the candidate's\n // quarantine reason here.\n return { fact: existing ?? fact, decision };\n }\n case 'supersede': {\n await this.#commitFact(fact, embedder, embedderId, indexText);\n await this.#store.semantic.supersede(decision.existingId, fact, decision.reason);\n await this.#linkEntities(scope, fact, options.signal);\n return { fact, decision, ...reasonField };\n }\n }\n });\n }\n\n /**\n * Resolve the contextual-retrieval index text (P1-3). A caller\n * override (the consolidator's `'llm'` mode) takes precedence; then\n * the instance mode decides. Late-chunk derives the situating context\n * from the *author-supplied* signals on `input` (so an extraction's\n * subject/predicate/object survive even though the persisted `Fact`\n * drops them) and never from framework-defaulted fields, so a plain\n * `remember({ text })` write returns the canonical text unchanged.\n */\n #resolveIndexText(input: FactInput, canonicalText: string, override?: string): string {\n if (override !== undefined) return override;\n if (this.#contextualMode === 'off') return canonicalText;\n return contextualize({\n text: canonicalText,\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n ...(input.tags !== undefined ? { tags: input.tags } : {}),\n ...(input.validFrom !== undefined ? { validFrom: input.validFrom } : {}),\n });\n }\n\n async #commitFact(\n fact: Fact,\n embedder: EmbedderProvider | null,\n embedderId: string | null,\n indexText: string,\n ): Promise<void> {\n const adapterSupportsEmbeddedWrite =\n typeof this.#store.semantic.rememberWithEmbedding === 'function';\n const contextualized = indexText !== fact.text;\n const hasEmbeddingPath =\n embedder !== null && embedderId !== null && adapterSupportsEmbeddedWrite;\n if (hasEmbeddingPath) {\n // Embed the *contextual* text so the vector surface agrees with the\n // FTS index; the persisted `fact.text` stays canonical (P1-3). PS-10:\n // stored content is the `passage` role for asymmetric (E5) embedders.\n const [vector] = await embedder.embed([indexText], { taskType: 'passage' });\n if (vector !== undefined && this.#store.semantic.rememberWithEmbedding !== undefined) {\n await this.#store.semantic.rememberWithEmbedding(fact, {\n embedding: { embedderId, vector },\n ...(contextualized ? { indexText } : {}),\n });\n return;\n }\n }\n // No embedder, but contextualization still needs to reach the lexical\n // index - route through the extended write when the adapter supports\n // it. Plain (non-contextualized) writes keep the canonical fast path.\n if (\n contextualized &&\n adapterSupportsEmbeddedWrite &&\n this.#store.semantic.rememberWithEmbedding !== undefined\n ) {\n await this.#store.semantic.rememberWithEmbedding(fact, { indexText });\n return;\n }\n await this.#store.semantic.remember(fact);\n }\n\n async #fetchExisting(factId: string): Promise<Fact | null> {\n if (typeof this.#store.semantic.get !== 'function') return null;\n return this.#store.semantic.get(factId);\n }\n\n /**\n * P2-1: resolve the fact's subject / object to canonical entities and\n * link them so one-hop expansion can traverse this fact. A no-op when\n * no resolver is configured (the default) or the fact has no s/p/o, and\n * resilient - a resolution failure never breaks the write that just\n * committed.\n */\n async #linkEntities(scope: SessionScope, fact: Fact, signal?: AbortSignal): Promise<void> {\n if (this.#entityResolver === null) return;\n if (fact.subject === undefined && fact.object === undefined) return;\n try {\n await this.#entityResolver.linkFact(scope, fact, {\n ...(signal !== undefined ? { signal } : {}),\n });\n } catch {\n // Graph linking is a soft enrichment - never fail a committed write.\n }\n }\n\n /** Hybrid (vector + FTS5) search merged through the configured reranker. */\n async search(\n scope: SessionScope,\n query: string,\n opts: FactSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.semantic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const candidateTopK = opts.candidateTopK ?? 60;\n const finalTopK = opts.topK ?? 10;\n // X-2: when weighted fusion is opted-in, each candidate list is\n // weighted by its *kind* (FTS vs vector) so the weights survive\n // the P2-3 fan-out below; otherwise `listWeights` is unused and\n // fusion runs through the configured reranker (RRF by default).\n const weighted =\n opts.fusion !== undefined && opts.fusion.strategy === 'weighted' ? opts.fusion : null;\n const wFts = weighted?.weights.fts ?? 1;\n const wVector = weighted?.weights.vector ?? 1;\n const wGraph = weighted?.weights.graph ?? 1;\n const wEntity = weighted?.weights.entity ?? 1;\n // P2-3: fan the query into reworded variants when opted-in *and*\n // a transformer is configured; otherwise this is just `[query]`\n // (single-shot, no provider call - the offline default).\n const queries = await this.#expandQueries(query, opts);\n const lists: Array<ReadonlyArray<MemoryHit<Fact>>> = [];\n const listWeights: number[] = [];\n // MRET-13: retriever-kind labels in lockstep with `lists` so the\n // rrf.<label> explanation signals stay stable across fan-outs.\n const listLabels: string[] = [];\n let primaryFtsCount = 0;\n let primaryVectorCount = 0;\n let isPrimary = true;\n for (const q of queries) {\n const ftsHits = await this.#store.semantic.search(scope, {\n query: q,\n topK: candidateTopK,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: in-store any-of tags predicate on the FTS leg.\n ...(opts.tags !== undefined && opts.tags.length > 0 ? { tags: opts.tags } : {}),\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n ...(opts.includeSuperseded === true ? { includeSuperseded: true } : {}),\n // D3: in-store principal filter on the FTS leg.\n ...(opts.owner !== undefined ? { owner: opts.owner } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n lists.push(ftsHits);\n listWeights.push(wFts);\n listLabels.push(`fts_${queries.indexOf(q)}`);\n const vectorHits = await this.#tryVectorSearch(\n scope,\n q,\n candidateTopK,\n opts.asOf,\n opts.includeQuarantined,\n opts.includeSuperseded,\n opts.owner,\n );\n if (vectorHits.length > 0) {\n lists.push(vectorHits);\n listWeights.push(wVector);\n listLabels.push(`vector_${queries.indexOf(q)}`);\n }\n if (isPrimary) {\n primaryFtsCount = ftsHits.length;\n primaryVectorCount = vectorHits.length;\n isPrimary = false;\n }\n }\n // P2-3 HyDE: embed a hypothetical answer and fuse its neighbours\n // (a vector list ⇒ it takes the `vector` weight).\n const hydeHits = await this.#tryHyde(scope, query, opts, candidateTopK);\n if (hydeHits.length > 0) {\n lists.push(hydeHits);\n listWeights.push(wVector);\n listLabels.push('hyde');\n }\n // P2-1: one-hop graph expansion - seed on the candidates gathered\n // so far and fuse in facts that share a canonical entity (a third\n // candidate kind ⇒ neutral fusion weight). A no-op without\n // `expandHops` / a graph adapter.\n const graphHits = await this.#tryExpandHops(scope, lists, opts, candidateTopK);\n if (graphHits.length > 0) {\n lists.push(graphHits);\n listWeights.push(wGraph);\n listLabels.push('graph');\n }\n // D5: exact entity-match leg - a precise \"facts about <entity>\"\n // candidate list, fused with its own weight.\n const entityHits = await this.#tryEntityMatch(scope, query, opts, candidateTopK);\n if (entityHits.length > 0) {\n lists.push(entityHits);\n listWeights.push(wEntity);\n listLabels.push('entity');\n }\n // X-2: weighted fusion runs through a per-call WeightedRRFReranker\n // built from the per-list weights; the default stays the\n // configured reranker so behaviour is unchanged when `fusion` is\n // omitted.\n const reranker: ReRanker =\n weighted !== null\n ? new WeightedRRFReranker({\n weights: listWeights,\n ...(weighted.k !== undefined ? { k: weighted.k } : {}),\n })\n : this.#reranker;\n // MRET-8: when decay re-ranking is requested, fuse the FULL\n // candidate pool (not just finalTopK) so a fresh fact sitting at\n // position topK+1 pre-decay can still enter the final page; the\n // finalTopK cut moves to AFTER decay + filters.\n // memory-retrieval-03: the tags filter also runs AFTER fusion\n // (the vector / HyDE / graph legs have no store-level tags\n // predicate), so a tagged search must widen the same way -\n // otherwise untagged candidates occupy fused ranks, get\n // filtered, and the search silently returns fewer than topK.\n const fusedTopK =\n opts.decay !== undefined ||\n (opts.tags?.length ?? 0) > 0 ||\n opts.owner !== undefined ||\n (opts.expandHops ?? 0) > 0 ||\n opts.entityMatch === true\n ? Math.max(\n finalTopK,\n lists.reduce((n, l) => n + l.length, 0),\n )\n : finalTopK;\n const fused = await reranker.rerank(query, lists, {\n topK: fusedTopK,\n labels: listLabels,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const decayed = await this.#applyDecay(scope, fused, opts.decay);\n // MRET-4: the vector / HyDE / graph legs have no store-level tags\n // predicate - enforce the any-of filter on the fused records so\n // every leg obeys it.\n const filtered =\n opts.tags !== undefined && opts.tags.length > 0\n ? decayed.filter((h) => {\n const recordTags = h.record.tags;\n if (recordTags === undefined || recordTags.length === 0) return false;\n return opts.tags?.some((t) => recordTags.includes(t)) === true;\n })\n : decayed;\n // D3: the HyDE / graph legs have no store-level owner predicate -\n // enforce the principal filter on the fused records so every leg\n // obeys it. Rows with no stored owner count as 'user'.\n const wantedOwners =\n opts.owner === undefined\n ? null\n : typeof opts.owner === 'string'\n ? [opts.owner]\n : opts.owner;\n const ownerFiltered =\n wantedOwners !== null\n ? filtered.filter((h) => wantedOwners.includes(h.record.owner ?? 'user'))\n : filtered;\n // C5: rank-time trust discount - quarantined-but-included and\n // foreign-provenance hits are down-weighted BEFORE the final cut,\n // mirroring the eviction-path securityFactor. First-party active\n // facts keep factor 1, so ordinary rankings are unchanged.\n const trusted =\n opts.trustWeighting === 'off' ? ownerFiltered : this.#applyTrustDiscount(ownerFiltered);\n const ranked = trusted.slice(0, finalTopK);\n // MRET-7: recall reinforces the recalled facts - stamp\n // last-accessed + bump strength so \"recently accessed facts decay\n // slower\" actually holds. Bookkeeping only: a failure here must\n // never break the read path.\n if (ranked.length > 0 && typeof this.#store.semantic.markAccessed === 'function') {\n try {\n await this.#store.semantic.markAccessed(ranked.map((h) => h.record.id));\n } catch {\n // Best-effort: decay reinforcement is advisory.\n }\n }\n const explanation = explainRecall(ranked, {\n query,\n rerankerId: reranker.id,\n });\n span.setAttributes({\n // `fts_count` / `vector_count` report the *original* query's\n // candidate lists, so the single-shot default reads exactly as\n // before; `query_count` (≥1) and `hyde_applied` cover the\n // P2-3 fan-out.\n 'memory.search.semantic.fts_count': primaryFtsCount,\n 'memory.search.semantic.vector_count': primaryVectorCount,\n 'memory.search.semantic.final_count': ranked.length,\n 'memory.search.semantic.reranker_id': reranker.id,\n 'memory.search.semantic.decay_applied': opts.decay !== undefined,\n 'memory.search.semantic.query_count': queries.length,\n 'memory.search.semantic.hyde_applied': hydeHits.length > 0,\n 'memory.search.semantic.expand_hops': opts.expandHops ?? 0,\n 'memory.search.semantic.graph_count': graphHits.length,\n // X-3: per-signal recall explanation (ids + scores + signals,\n // no query text - the query is surfaced only as `query_length`\n // above to keep traces free of recall content).\n 'memory.search.semantic.explain': JSON.stringify(explanation.results),\n ...(opts.asOf !== undefined ? { 'memory.search.semantic.as_of': opts.asOf } : {}),\n ...(opts.includeQuarantined === true\n ? { 'memory.search.semantic.include_quarantined': true }\n : {}),\n });\n return ranked;\n },\n );\n }\n\n /**\n * Gated, iterative (\"deep\") recall for hard queries (P2-4). A cheap\n * local heuristic ({@link assessQueryDifficulty}) decides whether the\n * query is even a loop candidate; simple lookups take exactly one\n * {@link search} pass and make no provider call. For a query judged\n * hard *and* with a grader configured\n * (`createMemory({ iterativeRetrieval })`), the retrieved set is graded\n * for sufficiency and, when weak, the query is reformulated and\n * retrieved again - **widening to one-hop graph expansion**\n * (`expandHops: 1`) on each reformulation pass - up to `maxIterations`\n * (hard-capped at 5). When still insufficient it returns\n * `abstained: true` so the caller can decline to answer instead of\n * confabulating.\n *\n * Without a grader (the offline default) this degrades to a single,\n * difficulty-gated `search` and never calls a provider.\n *\n * @stable\n */\n async searchIterative(\n scope: SessionScope,\n query: string,\n opts: IterativeSearchOptions = {},\n ): Promise<IterativeRecallResult> {\n // Reuse the `memory.search.semantic` span type (the inner per-pass\n // searches nest under it); iterative-specific data is namespaced under\n // the `…iterative.*` attribute prefix so no new core SpanType is\n // needed and P2-4 stays memory-only.\n return withMemorySpan(\n this.#tracer,\n 'memory.search.semantic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const result = await runIterativeRetrieval<MemoryHit<Fact>>(\n query,\n {\n // `maxIterations` / `forceHard` ride along harmlessly (search\n // ignores unknown keys); `expandHops: 1` widens recall to the\n // P2-1 graph on reformulation passes, and the per-pass signal\n // overrides any inherited one.\n retrieve: (q, widen, signal) =>\n this.search(scope, q, {\n ...opts,\n ...(widen ? { expandHops: 1 } : {}),\n ...(signal !== undefined ? { signal } : {}),\n }),\n snippetOf: (hit) => hit.record.text,\n idOf: (hit) => hit.record.id,\n grader: this.#grader,\n // MRET-2: re-fuse the per-pass lists with RRF before the\n // final topK cut so a reformulation-pass find can outrank\n // pass-1 noise (discovery order silently dropped it).\n fuse: (lists) => fuseRrf(lists, 60),\n },\n {\n maxIterations: opts.maxIterations ?? this.#iterativeMaxIterations,\n maxResults: opts.topK ?? 10,\n ...(opts.forceHard !== undefined ? { forceHard: opts.forceHard } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n },\n );\n span.setAttributes({\n 'memory.search.semantic.iterative.gate_hard': result.gateHard,\n 'memory.search.semantic.iterative.iterations': result.iterations,\n 'memory.search.semantic.iterative.sufficient': result.sufficient,\n 'memory.search.semantic.iterative.abstained': result.abstained,\n 'memory.search.semantic.iterative.query_count': result.queries.length,\n 'memory.search.semantic.iterative.final_count': result.hits.length,\n });\n return result;\n },\n );\n }\n\n /**\n * Raw vector KNN neighbours for the consolidator's reconcile\n * pre-filter (P0-3). Unlike {@link search} this skips FTS, reranking,\n * and decay so the cosine scores survive intact (the conflict-pipeline\n * zone thresholds are calibrated against them), and it **includes\n * quarantined facts** so prior synthesized memories are visible to\n * reconciliation. Returns `[]` when no embedder / vector adapter is\n * configured - the consolidator then treats every candidate as a\n * fresh `add`, degrading gracefully to the pre-reconcile behaviour.\n *\n * @stable\n */\n async neighbors(\n scope: SessionScope,\n text: string,\n opts: { readonly topK?: number } = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return this.#tryVectorSearch(scope, text, opts.topK ?? 10, undefined, true);\n }\n\n /** Lookup a single fact by id. Returns `null` for soft-deleted / missing. */\n async get(scope: SessionScope, factId: string): Promise<Fact | null> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.semantic',\n scope,\n { 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.get !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.get(...) requires a storage adapter that implements `semantic.get(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const fact = await this.#store.semantic.get(factId);\n span.setAttributes({ 'memory.read.semantic.found': fact !== null });\n return fact;\n },\n );\n }\n\n /**\n * Return the full bi-temporal supersede chain that `factId` belongs\n * to, oldest → newest, including superseded / soft-deleted rows so\n * callers can answer \"how did this fact change over time\". Requires\n * a storage adapter that implements\n * `SemanticMemoryStoreExt.historyOf(...)` - the default\n * `@graphorin/store-sqlite` adapter wires this through. P0-2.\n *\n * @stable\n */\n async history(scope: SessionScope, factId: string): Promise<ReadonlyArray<Fact>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.semantic',\n scope,\n { 'memory.semantic.action': 'history', 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.historyOf !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.history(...) requires a storage adapter that implements `semantic.historyOf(scope, id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const chain = await this.#store.semantic.historyOf(scope, factId);\n span.setAttributes({ 'memory.read.semantic.history_length': chain.length });\n return chain;\n },\n );\n }\n\n /**\n * Promote a quarantined fact to `active` (P1-4). The validation path\n * that admits a synthesized memory into action-driving recall once a\n * human (or trusted non-agent caller) has reviewed it. Writes a\n * `memory_history` audit row. Requires a storage adapter that\n * implements `SemanticMemoryStoreExt.setStatus(...)` - the default\n * `@graphorin/store-sqlite` adapter wires this through.\n *\n * MRET-3 / MST-1: promotion of a fact whose text still trips the\n * offline injection heuristics is **refused** with\n * {@link QuarantinePromotionRefusedError} - the model-facing\n * `fact_validate` tool calls this with no `force`, so a poisoned\n * memory can never be promoted by the agent itself (the one-turn\n * `fact_remember(poison)` → `fact_validate(id)` chain is closed). An\n * operator can override after review by passing `{ force: true }`\n * from a trusted (non-agent) context. Synthesized-but-clean writes\n * (consolidator / reflection) promote normally.\n *\n * @stable\n */\n async validate(\n scope: SessionScope,\n factId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'validate', 'memory.semantic.fact_id': factId },\n async (span) => {\n if (typeof this.#store.semantic.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.validate(...) requires a storage adapter that implements `semantic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n // Re-derive the injection verdict from the stored (immutable)\n // text. A poison candidate stays flagged forever, so this gate\n // does not depend on persisting a quarantine reason.\n const existing = await this.#fetchExisting(factId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({\n 'memory.semantic.validate.refused': true,\n 'memory.semantic.injection_markers': injection.markers.join(','),\n });\n throw new QuarantinePromotionRefusedError(factId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.semantic.validate.forced': force });\n await this.#store.semantic.setStatus(factId, 'active', reason);\n },\n );\n }\n\n /** Mark `oldId` superseded by a new fact. Returns the new record. */\n async supersede(\n scope: SessionScope,\n oldId: string,\n newInput: FactInput,\n reason?: string,\n ): Promise<{ readonly old: string; readonly new: Fact }> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'supersede' },\n async () => {\n // Bypass the conflict pipeline - the explicit supersede call\n // is the user's authoritative decision; a second pipeline\n // pass would race with itself (the new fact is by definition\n // a near-dup of the old one) and could silently reroute the\n // write into a `dedup` outcome.\n const newFact = await this.remember(\n scope,\n { ...newInput, supersedes: oldId },\n { pipeline: 'off' },\n );\n await this.#store.semantic.supersede(oldId, newFact, reason);\n return { old: oldId, new: newFact };\n },\n );\n }\n\n /** Soft-delete a fact (kept for replay; never hard-deleted). */\n async forget(scope: SessionScope, factId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'forget', 'memory.semantic.fact_id': factId },\n async () => {\n await this.#store.semantic.forget(factId, reason);\n },\n );\n }\n\n /**\n * Hard-delete a fact (GDPR path). Distinct from {@link forget}: the\n * record is removed from storage entirely instead of soft-archived.\n * Requires a storage adapter that implements\n * `SemanticMemoryStoreExt.purge(...)` - the default\n * `@graphorin/store-sqlite` adapter wires this through.\n */\n async purge(scope: SessionScope, factId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.semantic',\n scope,\n { 'memory.semantic.action': 'purge', 'memory.semantic.fact_id': factId },\n async () => {\n if (typeof this.#store.semantic.purge !== 'function') {\n throw new TypeError(\n '[graphorin/memory] SemanticMemory.purge(...) requires a storage adapter that implements `semantic.purge(id)`. ' +\n 'For storage adapters without GDPR-grade hard-delete, prefer `SemanticMemory.forget(...)`.',\n );\n }\n await this.#store.semantic.purge(factId, reason);\n },\n );\n }\n\n /** Fuse multiple ranked lists outside of a `search()` call. */\n async fuse(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<Fact>>>,\n options: { topK?: number; signal?: AbortSignal } = {},\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n return this.#reranker.rerank(query, lists, options);\n }\n\n /** Pure-fusion helper - exposed for callers that already collected results. */\n static fuseRrf<TRecord extends Fact>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n k = 60,\n ): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseRrf(lists, k);\n }\n\n /**\n * Pure weighted-fusion helper (X-2) - like {@link SemanticMemory.fuseRrf}\n * but scales each list `i`'s reciprocal-rank contribution by\n * `weights[i]`. A missing / invalid entry defaults to `1`, so equal or\n * absent weights reproduce RRF.\n */\n static fuseWeighted<TRecord extends Fact>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n weights: ReadonlyArray<number> | undefined,\n k = 60,\n ): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseWeighted(lists, weights, k);\n }\n\n /**\n * Multi-query expansion (P2-3). Returns the original query followed by\n * up to `multiQuery - 1` deduped reworded variants. A no-op (`[query]`)\n * when `multiQuery` is unset / `<= 1` or no transformer is configured -\n * so the default path makes no provider call. A transformer failure\n * degrades to `[query]` rather than breaking recall.\n */\n async #expandQueries(query: string, opts: FactSearchOptions): Promise<ReadonlyArray<string>> {\n const n = opts.multiQuery;\n if (n === undefined || n <= 1 || this.#queryTransformer === null) return [query];\n try {\n const variants = await this.#queryTransformer.expand(query, n - 1, {\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const seen = new Set<string>([query.trim().toLowerCase()]);\n const out: string[] = [query];\n for (const variant of variants) {\n const trimmed = variant.trim();\n const key = trimmed.toLowerCase();\n if (trimmed.length === 0 || seen.has(key)) continue;\n seen.add(key);\n out.push(trimmed);\n if (out.length >= n) break;\n }\n return out;\n } catch {\n return [query];\n }\n }\n\n /**\n * HyDE retrieval (P2-3). Generates a hypothetical answer, embeds it,\n * and returns its vector neighbours to fuse into the result. A no-op\n * (`[]`) unless `hyde` is set, a transformer is configured, **and** an\n * embedder + vector surface exist - the embedder guard is checked\n * *first* so a missing embedder skips the LLM call entirely rather than\n * generating a passage that can never be embedded.\n */\n async #tryHyde(\n scope: SessionScope,\n query: string,\n opts: FactSearchOptions,\n topK: number,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n if (opts.hyde !== true || this.#queryTransformer === null) return [];\n if (\n this.#embedder === null ||\n this.#embedderIdProvider() === null ||\n typeof this.#store.semantic.searchVector !== 'function'\n ) {\n return [];\n }\n try {\n const pseudo = await this.#queryTransformer.hypothetical(query, {\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n if (pseudo === null || pseudo.trim().length === 0) return [];\n return this.#tryVectorSearch(scope, pseudo, topK, opts.asOf, opts.includeQuarantined);\n } catch {\n return [];\n }\n }\n\n /**\n * One-hop graph expansion (P2-1). Seeds on the unique fact ids already\n * gathered into `lists` and fuses in facts sharing a canonical entity,\n * via the adapter's recursive-CTE `graph.expandOneHop`. A no-op (`[]`)\n * unless `expandHops >= 1` and the adapter exposes `graph` - and\n * resilient: a traversal error degrades to no expansion rather than\n * breaking recall. Neighbours carry a `graph` signal so explanations\n * (X-3) can attribute a hit to the hop.\n */\n async #tryExpandHops(\n scope: SessionScope,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<Fact>>>,\n opts: FactSearchOptions,\n limit: number,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n const hops = opts.expandHops ?? 0;\n const graphStore = this.#store.graph;\n if (hops < 1 || graphStore === undefined || typeof graphStore.expandOneHop !== 'function') {\n return [];\n }\n const seedIds: string[] = [];\n const seen = new Set<string>();\n for (const list of lists) {\n for (const hit of list) {\n if (!seen.has(hit.record.id)) {\n seen.add(hit.record.id);\n seedIds.push(hit.record.id);\n }\n }\n }\n if (seedIds.length === 0) return [];\n const expandOpts = {\n maxHops: hops,\n limit,\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n ...(opts.includeSuperseded === true ? { includeSuperseded: true } : {}),\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n };\n try {\n // D5: PPR-lite damped spreading activation when opted in AND the\n // store exposes the graded expansion; otherwise the flat one-hop.\n if (opts.graphScoring === 'ppr' && typeof graphStore.expandActivation === 'function') {\n const graded = await graphStore.expandActivation(scope, seedIds, expandOpts);\n const scores = pprActivation(graded);\n return graded.map((entry, i) => ({\n record: entry.fact,\n score: scores[i] ?? 0,\n signals: Object.freeze({ graph: scores[i] ?? 0 }),\n }));\n }\n const neighbours = await graphStore.expandOneHop(scope, seedIds, expandOpts);\n return neighbours.map((fact) => ({\n record: fact,\n score: 1,\n signals: Object.freeze({ graph: 1 }),\n }));\n } catch {\n return [];\n }\n }\n\n /**\n * D5 exact entity-match leg. Normalizes the query into candidate\n * entity names and fetches facts linked to a matching canonical\n * entity. A no-op without `entityMatch`, a graph adapter, or the\n * `factsForEntityName` store method.\n */\n async #tryEntityMatch(\n scope: SessionScope,\n query: string,\n opts: FactSearchOptions,\n limit: number,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n if (opts.entityMatch !== true) return [];\n const graphStore = this.#store.graph;\n if (graphStore === undefined || typeof graphStore.factsForEntityName !== 'function') {\n return [];\n }\n // Candidate entity names: the whole normalized query plus its\n // longer word n-grams (cheap, offline). The store's canonical index\n // makes each lookup O(1); most miss, the hit is precise.\n const normalized = normalizeEntityName(query);\n if (normalized.length === 0) return [];\n const candidates = new Set<string>([normalized]);\n const words = normalized.split(' ').filter((w) => w.length > 2);\n for (const w of words) candidates.add(w);\n const out: MemoryHit<Fact>[] = [];\n const seen = new Set<string>();\n try {\n for (const name of candidates) {\n const facts = await graphStore.factsForEntityName(scope, name, {\n limit,\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n ...(opts.includeSuperseded === true ? { includeSuperseded: true } : {}),\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n });\n for (const fact of facts) {\n if (seen.has(fact.id)) continue;\n seen.add(fact.id);\n out.push({ record: fact, score: 1, signals: Object.freeze({ entity: 1 }) });\n }\n }\n } catch {\n return [];\n }\n return out;\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n includeSuperseded?: boolean,\n owner?: MemoryOwner | ReadonlyArray<MemoryOwner>,\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.semantic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for asymmetric (E5) embedders.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.semantic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n includeSuperseded,\n owner,\n );\n }\n\n /**\n * C5: multiply each hit's fused score by its trust factor and re-sort.\n * No-ops (returns the same array) when every factor is 1.\n */\n #applyTrustDiscount(hits: ReadonlyArray<MemoryHit<Fact>>): ReadonlyArray<MemoryHit<Fact>> {\n let changed = false;\n const discounted = hits.map((hit) => {\n const factor = trustDiscount(hit.record, this.#trustWeights);\n if (factor >= 1) return hit;\n changed = true;\n return {\n record: hit.record,\n score: hit.score * factor,\n signals: Object.freeze({ ...(hit.signals ?? {}), trust: factor }),\n };\n });\n if (!changed) return hits;\n return [...discounted].sort((a, b) => b.score - a.score);\n }\n\n async #applyDecay(\n scope: SessionScope,\n hits: ReadonlyArray<MemoryHit<Fact>>,\n decay: FactSearchOptions['decay'],\n ): Promise<ReadonlyArray<MemoryHit<Fact>>> {\n if (decay === undefined || hits.length === 0) return hits;\n // MRET-8: read decay columns ONLY for the hit ids (narrow IN-list)\n // instead of re-reading the scope's 1000 oldest rows per search -\n // the old window was O(scope) per query AND, being the LRU head,\n // could even miss the hits entirely.\n const semantic = this.#store.semantic;\n let rows: ReadonlyArray<{\n readonly id: string;\n readonly strength: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n }>;\n if (typeof semantic.listDecaySignals === 'function') {\n rows = await semantic.listDecaySignals(hits.map((h) => h.record.id));\n } else if (typeof semantic.listForDecay === 'function') {\n rows = await semantic.listForDecay(scope, 1000);\n } else {\n return hits;\n }\n if (rows.length === 0) return hits;\n const signals = new Map(\n rows.map((row) => [\n row.id,\n {\n strength: row.strength,\n lastAccessedAt: row.lastAccessedAt,\n createdAt: row.createdAt,\n },\n ]),\n );\n const now = (decay.now ?? Date.now)();\n const tauMs = Math.max(1, decay.tauDays) * 24 * 60 * 60 * 1000;\n const out = hits.map((hit) => {\n const sig = signals.get(hit.record.id);\n if (sig === undefined) return hit;\n const reference = sig.lastAccessedAt ?? sig.createdAt;\n const elapsed = Math.max(0, now - reference);\n const retention = Math.exp(-elapsed / (tauMs * Math.max(0.5, sig.strength)));\n // Record the decay multiplier as a signal so recall explanations\n // (X-3) can attribute the score drop to staleness, not just to\n // fusion. Hits with no decay row keep their signals untouched.\n return {\n ...hit,\n score: hit.score * retention,\n signals: Object.freeze({ ...(hit.signals ?? {}), decay: retention }),\n };\n });\n const sorted = [...out].sort((a, b) => b.score - a.score);\n return Object.freeze(sorted);\n }\n}\n"],"mappings":"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"}
|
|
@@ -30,7 +30,7 @@ interface SessionCompactionResult {
|
|
|
30
30
|
readonly summary?: string;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* `SessionMemory`
|
|
33
|
+
* `SessionMemory` - append-only message log per session. Owns the
|
|
34
34
|
* `session_messages` storage by single-source-of-truth (DEC-147); the
|
|
35
35
|
* `@graphorin/sessions` package wraps this surface in Phase 11.
|
|
36
36
|
*
|
|
@@ -59,7 +59,7 @@ declare class SessionMemory {
|
|
|
59
59
|
signal?: AbortSignal;
|
|
60
60
|
}): Promise<ReadonlyArray<MemoryHit>>;
|
|
61
61
|
/**
|
|
62
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
62
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `{ flushed: 0 }` and
|
|
63
63
|
* performs no work. The consolidator pipeline (extraction → facts /
|
|
64
64
|
* episodes) superseded the planned "silent flush turn"; this method
|
|
65
65
|
* remains only for contract stability. Do not branch on its counter.
|
|
@@ -70,12 +70,12 @@ declare class SessionMemory {
|
|
|
70
70
|
flushed: number;
|
|
71
71
|
}>;
|
|
72
72
|
/**
|
|
73
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
73
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves
|
|
74
74
|
* `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.
|
|
75
75
|
* Session-context compaction is owned by the context engine
|
|
76
76
|
* (`memory.contextEngine.compactNow`, driven by the agent runtime);
|
|
77
77
|
* this tier-level method previously FABRICATED counts (it reported
|
|
78
|
-
* `total - keepLastN` as "removed" while removing nothing
|
|
78
|
+
* `total - keepLastN` as "removed" while removing nothing - with the
|
|
79
79
|
* default `keepLastN: 0` it claimed to have compacted the whole
|
|
80
80
|
* session). It now reports the truth until a real message splice
|
|
81
81
|
* exists at this layer.
|
|
@@ -88,9 +88,9 @@ declare class SessionMemory {
|
|
|
88
88
|
* `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The
|
|
89
89
|
* second argument can be either:
|
|
90
90
|
*
|
|
91
|
-
* - a `number`
|
|
91
|
+
* - a `number` - interpreted as the live `contextWindow` size in
|
|
92
92
|
* tokens (matches the documented memory-system spec signature);
|
|
93
|
-
* - an options bag
|
|
93
|
+
* - an options bag - `{ usedTokens?, contextWindow? }`. When
|
|
94
94
|
* `usedTokens` is supplied the call is purely arithmetic; when
|
|
95
95
|
* omitted, the storage adapter's per-message `token_count`
|
|
96
96
|
* cache (DEC-131) is consulted via `totalCachedTokens(scope)`,
|
|
@@ -104,7 +104,7 @@ declare class SessionMemory {
|
|
|
104
104
|
contextWindow?: number;
|
|
105
105
|
}): Promise<boolean>;
|
|
106
106
|
/**
|
|
107
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
107
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `[]`. The agent
|
|
108
108
|
* registry lives in `@graphorin/sessions` and has never been
|
|
109
109
|
* threaded into this tier; the previous JSDoc claimed the default
|
|
110
110
|
* sqlite adapter "resolves" registry rows here, which was false. Use
|
|
@@ -3,7 +3,7 @@ import { newMemoryId } from "../internal/id.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/tiers/session-memory.ts
|
|
5
5
|
/**
|
|
6
|
-
* `SessionMemory`
|
|
6
|
+
* `SessionMemory` - append-only message log per session. Owns the
|
|
7
7
|
* `session_messages` storage by single-source-of-truth (DEC-147); the
|
|
8
8
|
* `@graphorin/sessions` package wraps this surface in Phase 11.
|
|
9
9
|
*
|
|
@@ -72,7 +72,7 @@ var SessionMemory = class {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
75
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `{ flushed: 0 }` and
|
|
76
76
|
* performs no work. The consolidator pipeline (extraction → facts /
|
|
77
77
|
* episodes) superseded the planned "silent flush turn"; this method
|
|
78
78
|
* remains only for contract stability. Do not branch on its counter.
|
|
@@ -84,12 +84,12 @@ var SessionMemory = class {
|
|
|
84
84
|
}, async () => ({ flushed: 0 }));
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
87
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves
|
|
88
88
|
* `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.
|
|
89
89
|
* Session-context compaction is owned by the context engine
|
|
90
90
|
* (`memory.contextEngine.compactNow`, driven by the agent runtime);
|
|
91
91
|
* this tier-level method previously FABRICATED counts (it reported
|
|
92
|
-
* `total - keepLastN` as "removed" while removing nothing
|
|
92
|
+
* `total - keepLastN` as "removed" while removing nothing - with the
|
|
93
93
|
* default `keepLastN: 0` it claimed to have compacted the whole
|
|
94
94
|
* session). It now reports the truth until a real message splice
|
|
95
95
|
* exists at this layer.
|
|
@@ -114,9 +114,9 @@ var SessionMemory = class {
|
|
|
114
114
|
* `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The
|
|
115
115
|
* second argument can be either:
|
|
116
116
|
*
|
|
117
|
-
* - a `number`
|
|
117
|
+
* - a `number` - interpreted as the live `contextWindow` size in
|
|
118
118
|
* tokens (matches the documented memory-system spec signature);
|
|
119
|
-
* - an options bag
|
|
119
|
+
* - an options bag - `{ usedTokens?, contextWindow? }`. When
|
|
120
120
|
* `usedTokens` is supplied the call is purely arithmetic; when
|
|
121
121
|
* omitted, the storage adapter's per-message `token_count`
|
|
122
122
|
* cache (DEC-131) is consulted via `totalCachedTokens(scope)`,
|
|
@@ -140,7 +140,7 @@ var SessionMemory = class {
|
|
|
140
140
|
return used / window > ratio;
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
* NOT IMPLEMENTED (MRET-12)
|
|
143
|
+
* NOT IMPLEMENTED (MRET-12) - always resolves `[]`. The agent
|
|
144
144
|
* registry lives in `@graphorin/sessions` and has never been
|
|
145
145
|
* threaded into this tier; the previous JSDoc claimed the default
|
|
146
146
|
* sqlite adapter "resolves" registry rows here, which was false. Use
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-memory.js","names":["#store","#tracer","#compactionPolicy"],"sources":["../../src/tiers/session-memory.ts"],"sourcesContent":["import type {\n AgentRegistryEntry,\n MemoryHit,\n Message,\n MessageRef,\n SessionListOptions,\n SessionMessageWithMetadata,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { newMemoryId } from '../internal/id.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Per-session compaction policy. The default `0.9` matches DEC-104:\n * compaction kicks in once cached message tokens exceed\n * `0.9 * contextWindow`.\n *\n * @stable\n */\nexport interface SessionCompactionPolicy {\n /** Default `0.9`. */\n readonly compactAtRatio?: number;\n /** Default `8192`. */\n readonly contextWindowTokens?: number;\n}\n\n/**\n * Snapshot returned by {@link SessionMemory.compact}. The\n * minimum-viable rendering simply reports counts; the full\n * LLM-summarized cutoff (Phase 10c Consolidator) replaces this\n * implementation later.\n *\n * @stable\n */\nexport interface SessionCompactionResult {\n readonly removed: number;\n readonly summarized: number;\n readonly summary?: string;\n}\n\n/**\n * `SessionMemory` — append-only message log per session. Owns the\n * `session_messages` storage by single-source-of-truth (DEC-147); the\n * `@graphorin/sessions` package wraps this surface in Phase 11.\n *\n * @stable\n */\nexport class SessionMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #compactionPolicy: SessionCompactionPolicy;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n compactionPolicy?: SessionCompactionPolicy;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#compactionPolicy = args.compactionPolicy ?? {};\n }\n\n /** Persist a message. Returns the storage reference. */\n async push(scope: SessionScope, message: Message): Promise<MessageRef> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n {\n 'memory.session.message_role': message.role,\n ...(message.role === 'assistant' && message.agentId !== undefined\n ? { 'memory.session.agent_id': message.agentId }\n : {}),\n },\n async (span) => {\n const ref = await this.#store.session.push(scope, message);\n span.setAttributes({\n 'memory.session.sequence': ref.sequence,\n 'memory.session.message_id': ref.messageId,\n });\n return ref;\n },\n );\n }\n\n /** List messages for the supplied scope. */\n async list(scope: SessionScope, opts: SessionListOptions = {}): Promise<ReadonlyArray<Message>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.session',\n scope,\n {\n ...(opts.lastN !== undefined ? { 'memory.session.last_n': opts.lastN } : {}),\n ...(opts.agentId !== undefined ? { 'memory.session.agent_id': opts.agentId } : {}),\n ...(opts.role !== undefined ? { 'memory.session.role': opts.role } : {}),\n },\n async (span) => {\n const out = await this.#store.session.list(scope, opts);\n span.setAttributes({ 'memory.read.session.count': out.length });\n return out;\n },\n );\n }\n\n /**\n * RP-5: list messages with their persisted identity (stored id / sequence /\n * `createdAt`) so an exporter preserves message identity + chronology.\n * Delegates to the store when it supports the richer read.\n */\n async listWithMetadata(\n scope: SessionScope,\n opts: SessionListOptions = {},\n ): Promise<ReadonlyArray<SessionMessageWithMetadata>> {\n const store = this.#store.session;\n if (store.listWithMetadata !== undefined) {\n return store.listWithMetadata(scope, opts);\n }\n // A store without the richer read: fabricate a unique id + the current time\n // (the pre-RP-5 behaviour, just centralised here).\n const messages = await store.list(scope, opts);\n return messages.map((message, i) => ({\n message,\n messageId: newMemoryId('msg'),\n sequence: i + 1,\n createdAt: new Date().toISOString(),\n }));\n }\n\n /** Hybrid (FTS5) search over the session messages. */\n async search(\n scope: SessionScope,\n query: string,\n opts: { topK?: number; signal?: AbortSignal } = {},\n ): Promise<ReadonlyArray<MemoryHit>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.session',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const out = await this.#store.session.search(scope, query, {\n query,\n ...(opts.topK !== undefined ? { topK: opts.topK } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n span.setAttributes({ 'memory.search.session.count': out.length });\n return out;\n },\n );\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) — always resolves `{ flushed: 0 }` and\n * performs no work. The consolidator pipeline (extraction → facts /\n * episodes) superseded the planned \"silent flush turn\"; this method\n * remains only for contract stability. Do not branch on its counter.\n */\n async flushImportant(\n scope: SessionScope,\n opts: { silent?: boolean } = {},\n ): Promise<{ flushed: number }> {\n void opts;\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n { 'memory.session.action': 'flush-important', 'memory.session.implemented': false },\n async () => ({ flushed: 0 }),\n );\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) — always resolves\n * `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.\n * Session-context compaction is owned by the context engine\n * (`memory.contextEngine.compactNow`, driven by the agent runtime);\n * this tier-level method previously FABRICATED counts (it reported\n * `total - keepLastN` as \"removed\" while removing nothing — with the\n * default `keepLastN: 0` it claimed to have compacted the whole\n * session). It now reports the truth until a real message splice\n * exists at this layer.\n */\n async compact(\n scope: SessionScope,\n opts: { keepLastN?: number } = {},\n ): Promise<SessionCompactionResult> {\n void opts;\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n { 'memory.session.action': 'compact', 'memory.session.implemented': false },\n async (span) => {\n span.setAttributes({\n 'memory.session.compact.removed': 0,\n 'memory.session.compact.summarized': 0,\n });\n return { removed: 0, summarized: 0 };\n },\n );\n }\n\n /**\n * Returns `true` when the cached message tokens exceed\n * `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The\n * second argument can be either:\n *\n * - a `number` — interpreted as the live `contextWindow` size in\n * tokens (matches the documented memory-system spec signature);\n * - an options bag — `{ usedTokens?, contextWindow? }`. When\n * `usedTokens` is supplied the call is purely arithmetic; when\n * omitted, the storage adapter's per-message `token_count`\n * cache (DEC-131) is consulted via `totalCachedTokens(scope)`,\n * falling back to a heuristic (~4 chars/token) for cache\n * misses.\n *\n * @stable\n */\n async shouldCompact(\n scope: SessionScope,\n contextWindowOrOptions: number | { usedTokens?: number; contextWindow?: number } = {},\n ): Promise<boolean> {\n const options =\n typeof contextWindowOrOptions === 'number'\n ? { contextWindow: contextWindowOrOptions }\n : contextWindowOrOptions;\n const ratio = this.#compactionPolicy.compactAtRatio ?? 0.9;\n const window = options.contextWindow ?? this.#compactionPolicy.contextWindowTokens ?? 8192;\n if (options.usedTokens !== undefined) {\n return options.usedTokens / window > ratio;\n }\n if (typeof this.#store.session.totalCachedTokens === 'function') {\n const cached = await this.#store.session.totalCachedTokens(scope);\n if (cached !== null) {\n return cached / window > ratio;\n }\n }\n const messages = await this.#store.session.list(scope, {});\n let used = 0;\n for (const m of messages) used += approximateTokenLength(m);\n return used / window > ratio;\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) — always resolves `[]`. The agent\n * registry lives in `@graphorin/sessions` and has never been\n * threaded into this tier; the previous JSDoc claimed the default\n * sqlite adapter \"resolves\" registry rows here, which was false. Use\n * the sessions facade for participant attribution.\n */\n async attributedFor(scope: SessionScope): Promise<ReadonlyArray<AgentRegistryEntry>> {\n void scope;\n return [];\n }\n}\n\nfunction approximateTokenLength(message: Message): number {\n if (message.role === 'system') {\n return Math.ceil(message.content.length / 4);\n }\n if (typeof message.content === 'string') return Math.ceil(message.content.length / 4);\n let total = 0;\n for (const part of message.content) {\n if (part.type === 'text' || part.type === 'reasoning') {\n total += Math.ceil(part.text.length / 4);\n }\n }\n return total;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-memory.js","names":["#store","#tracer","#compactionPolicy"],"sources":["../../src/tiers/session-memory.ts"],"sourcesContent":["import type {\n AgentRegistryEntry,\n MemoryHit,\n Message,\n MessageRef,\n SessionListOptions,\n SessionMessageWithMetadata,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { newMemoryId } from '../internal/id.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Per-session compaction policy. The default `0.9` matches DEC-104:\n * compaction kicks in once cached message tokens exceed\n * `0.9 * contextWindow`.\n *\n * @stable\n */\nexport interface SessionCompactionPolicy {\n /** Default `0.9`. */\n readonly compactAtRatio?: number;\n /** Default `8192`. */\n readonly contextWindowTokens?: number;\n}\n\n/**\n * Snapshot returned by {@link SessionMemory.compact}. The\n * minimum-viable rendering simply reports counts; the full\n * LLM-summarized cutoff (Phase 10c Consolidator) replaces this\n * implementation later.\n *\n * @stable\n */\nexport interface SessionCompactionResult {\n readonly removed: number;\n readonly summarized: number;\n readonly summary?: string;\n}\n\n/**\n * `SessionMemory` - append-only message log per session. Owns the\n * `session_messages` storage by single-source-of-truth (DEC-147); the\n * `@graphorin/sessions` package wraps this surface in Phase 11.\n *\n * @stable\n */\nexport class SessionMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #compactionPolicy: SessionCompactionPolicy;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n compactionPolicy?: SessionCompactionPolicy;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#compactionPolicy = args.compactionPolicy ?? {};\n }\n\n /** Persist a message. Returns the storage reference. */\n async push(scope: SessionScope, message: Message): Promise<MessageRef> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n {\n 'memory.session.message_role': message.role,\n ...(message.role === 'assistant' && message.agentId !== undefined\n ? { 'memory.session.agent_id': message.agentId }\n : {}),\n },\n async (span) => {\n const ref = await this.#store.session.push(scope, message);\n span.setAttributes({\n 'memory.session.sequence': ref.sequence,\n 'memory.session.message_id': ref.messageId,\n });\n return ref;\n },\n );\n }\n\n /** List messages for the supplied scope. */\n async list(scope: SessionScope, opts: SessionListOptions = {}): Promise<ReadonlyArray<Message>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.session',\n scope,\n {\n ...(opts.lastN !== undefined ? { 'memory.session.last_n': opts.lastN } : {}),\n ...(opts.agentId !== undefined ? { 'memory.session.agent_id': opts.agentId } : {}),\n ...(opts.role !== undefined ? { 'memory.session.role': opts.role } : {}),\n },\n async (span) => {\n const out = await this.#store.session.list(scope, opts);\n span.setAttributes({ 'memory.read.session.count': out.length });\n return out;\n },\n );\n }\n\n /**\n * RP-5: list messages with their persisted identity (stored id / sequence /\n * `createdAt`) so an exporter preserves message identity + chronology.\n * Delegates to the store when it supports the richer read.\n */\n async listWithMetadata(\n scope: SessionScope,\n opts: SessionListOptions = {},\n ): Promise<ReadonlyArray<SessionMessageWithMetadata>> {\n const store = this.#store.session;\n if (store.listWithMetadata !== undefined) {\n return store.listWithMetadata(scope, opts);\n }\n // A store without the richer read: fabricate a unique id + the current time\n // (the pre-RP-5 behaviour, just centralised here).\n const messages = await store.list(scope, opts);\n return messages.map((message, i) => ({\n message,\n messageId: newMemoryId('msg'),\n sequence: i + 1,\n createdAt: new Date().toISOString(),\n }));\n }\n\n /** Hybrid (FTS5) search over the session messages. */\n async search(\n scope: SessionScope,\n query: string,\n opts: { topK?: number; signal?: AbortSignal } = {},\n ): Promise<ReadonlyArray<MemoryHit>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.session',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const out = await this.#store.session.search(scope, query, {\n query,\n ...(opts.topK !== undefined ? { topK: opts.topK } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n span.setAttributes({ 'memory.search.session.count': out.length });\n return out;\n },\n );\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) - always resolves `{ flushed: 0 }` and\n * performs no work. The consolidator pipeline (extraction → facts /\n * episodes) superseded the planned \"silent flush turn\"; this method\n * remains only for contract stability. Do not branch on its counter.\n */\n async flushImportant(\n scope: SessionScope,\n opts: { silent?: boolean } = {},\n ): Promise<{ flushed: number }> {\n void opts;\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n { 'memory.session.action': 'flush-important', 'memory.session.implemented': false },\n async () => ({ flushed: 0 }),\n );\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) - always resolves\n * `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.\n * Session-context compaction is owned by the context engine\n * (`memory.contextEngine.compactNow`, driven by the agent runtime);\n * this tier-level method previously FABRICATED counts (it reported\n * `total - keepLastN` as \"removed\" while removing nothing - with the\n * default `keepLastN: 0` it claimed to have compacted the whole\n * session). It now reports the truth until a real message splice\n * exists at this layer.\n */\n async compact(\n scope: SessionScope,\n opts: { keepLastN?: number } = {},\n ): Promise<SessionCompactionResult> {\n void opts;\n return withMemorySpan(\n this.#tracer,\n 'memory.write.session',\n scope,\n { 'memory.session.action': 'compact', 'memory.session.implemented': false },\n async (span) => {\n span.setAttributes({\n 'memory.session.compact.removed': 0,\n 'memory.session.compact.summarized': 0,\n });\n return { removed: 0, summarized: 0 };\n },\n );\n }\n\n /**\n * Returns `true` when the cached message tokens exceed\n * `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The\n * second argument can be either:\n *\n * - a `number` - interpreted as the live `contextWindow` size in\n * tokens (matches the documented memory-system spec signature);\n * - an options bag - `{ usedTokens?, contextWindow? }`. When\n * `usedTokens` is supplied the call is purely arithmetic; when\n * omitted, the storage adapter's per-message `token_count`\n * cache (DEC-131) is consulted via `totalCachedTokens(scope)`,\n * falling back to a heuristic (~4 chars/token) for cache\n * misses.\n *\n * @stable\n */\n async shouldCompact(\n scope: SessionScope,\n contextWindowOrOptions: number | { usedTokens?: number; contextWindow?: number } = {},\n ): Promise<boolean> {\n const options =\n typeof contextWindowOrOptions === 'number'\n ? { contextWindow: contextWindowOrOptions }\n : contextWindowOrOptions;\n const ratio = this.#compactionPolicy.compactAtRatio ?? 0.9;\n const window = options.contextWindow ?? this.#compactionPolicy.contextWindowTokens ?? 8192;\n if (options.usedTokens !== undefined) {\n return options.usedTokens / window > ratio;\n }\n if (typeof this.#store.session.totalCachedTokens === 'function') {\n const cached = await this.#store.session.totalCachedTokens(scope);\n if (cached !== null) {\n return cached / window > ratio;\n }\n }\n const messages = await this.#store.session.list(scope, {});\n let used = 0;\n for (const m of messages) used += approximateTokenLength(m);\n return used / window > ratio;\n }\n\n /**\n * NOT IMPLEMENTED (MRET-12) - always resolves `[]`. The agent\n * registry lives in `@graphorin/sessions` and has never been\n * threaded into this tier; the previous JSDoc claimed the default\n * sqlite adapter \"resolves\" registry rows here, which was false. Use\n * the sessions facade for participant attribution.\n */\n async attributedFor(scope: SessionScope): Promise<ReadonlyArray<AgentRegistryEntry>> {\n void scope;\n return [];\n }\n}\n\nfunction approximateTokenLength(message: Message): number {\n if (message.role === 'system') {\n return Math.ceil(message.content.length / 4);\n }\n if (typeof message.content === 'string') return Math.ceil(message.content.length / 4);\n let total = 0;\n for (const part of message.content) {\n if (part.type === 'text' || part.type === 'reasoning') {\n total += Math.ceil(part.text.length / 4);\n }\n }\n return total;\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { MemoryRecord, Tracer } from "@graphorin/core";
|
|
|
4
4
|
//#region src/tiers/shared-memory.d.ts
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* `SharedMemory`
|
|
7
|
+
* `SharedMemory` - many-to-many attach mode for blocks / facts /
|
|
8
8
|
* rules across multiple agents. Storage adapters represent
|
|
9
9
|
* attachments as a join table (`shared_attachments` in
|
|
10
10
|
* `@graphorin/store-sqlite`).
|
|
@@ -2,7 +2,7 @@ import { withMemorySpan } from "../internal/spans.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/tiers/shared-memory.ts
|
|
4
4
|
/**
|
|
5
|
-
* `SharedMemory`
|
|
5
|
+
* `SharedMemory` - many-to-many attach mode for blocks / facts /
|
|
6
6
|
* rules across multiple agents. Storage adapters represent
|
|
7
7
|
* attachments as a join table (`shared_attachments` in
|
|
8
8
|
* `@graphorin/store-sqlite`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/shared-memory.ts"],"sourcesContent":["import type { MemoryRecord, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * `SharedMemory`
|
|
1
|
+
{"version":3,"file":"shared-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/shared-memory.ts"],"sourcesContent":["import type { MemoryRecord, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * `SharedMemory` - many-to-many attach mode for blocks / facts /\n * rules across multiple agents. Storage adapters represent\n * attachments as a join table (`shared_attachments` in\n * `@graphorin/store-sqlite`).\n *\n * @stable\n */\nexport class SharedMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n\n constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n }\n\n /** Attach `recordId` to `agentId`. Idempotent. */\n async attach(recordId: string, agentId: string, userId: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.shared',\n { userId, agentId },\n { 'memory.shared.action': 'attach', 'memory.shared.record_id': recordId },\n async () => {\n await this.#store.shared.attach(recordId, agentId);\n },\n );\n }\n\n /** Detach `recordId` from `agentId`. Idempotent. */\n async detach(recordId: string, agentId: string, userId: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.shared',\n { userId, agentId },\n { 'memory.shared.action': 'detach', 'memory.shared.record_id': recordId },\n async () => {\n await this.#store.shared.detach(recordId, agentId);\n },\n );\n }\n\n /** List every attachment for `agentId`. */\n async listFor(agentId: string, userId: string): Promise<ReadonlyArray<MemoryRecord>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.shared',\n { userId, agentId },\n {},\n async (span) => {\n const out = await this.#store.shared.listFor(agentId);\n span.setAttributes({ 'memory.read.shared.count': out.length });\n return out;\n },\n );\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAa,eAAb,MAA0B;CACxB,CAASA;CACT,CAASC;CAET,YAAY,MAAqD;AAC/D,QAAKD,QAAS,KAAK;AACnB,QAAKC,SAAU,KAAK;;;CAItB,MAAM,OAAO,UAAkB,SAAiB,QAA+B;AAC7E,QAAM,eACJ,MAAKA,QACL,uBACA;GAAE;GAAQ;GAAS,EACnB;GAAE,wBAAwB;GAAU,2BAA2B;GAAU,EACzE,YAAY;AACV,SAAM,MAAKD,MAAO,OAAO,OAAO,UAAU,QAAQ;IAErD;;;CAIH,MAAM,OAAO,UAAkB,SAAiB,QAA+B;AAC7E,QAAM,eACJ,MAAKC,QACL,uBACA;GAAE;GAAQ;GAAS,EACnB;GAAE,wBAAwB;GAAU,2BAA2B;GAAU,EACzE,YAAY;AACV,SAAM,MAAKD,MAAO,OAAO,OAAO,UAAU,QAAQ;IAErD;;;CAIH,MAAM,QAAQ,SAAiB,QAAsD;AACnF,SAAO,eACL,MAAKC,QACL,sBACA;GAAE;GAAQ;GAAS,EACnB,EAAE,EACF,OAAO,SAAS;GACd,MAAM,MAAM,MAAM,MAAKD,MAAO,OAAO,QAAQ,QAAQ;AACrD,QAAK,cAAc,EAAE,4BAA4B,IAAI,QAAQ,CAAC;AAC9D,UAAO;IAEV"}
|