@graphorin/memory 0.6.1 → 0.7.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.
Files changed (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +16 -13
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -5
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +1 -1
  89. package/dist/package.js.map +1 -1
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -1 +1 @@
1
- {"version":3,"file":"insight-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/insight-memory.ts"],"sourcesContent":["/**\n * `InsightMemory` - the thin, read-only surface over the reflection\n * insights the consolidator synthesizes (P1-1). Insights are *derived*\n * (quarantined + `provenance: 'reflection'`), carry mandatory citations,\n * and are retrieval-ranked **below the primary facts they cite**. The\n * write + salience-management paths live in the consolidator's\n * reflection pass; this tier only lists / searches for the validation /\n * inspector path.\n *\n * Search is FTS-only - insights are a soft inspector surface, not\n * primary recall, so no embedder is wired here.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, Insight, MemoryHit, SessionScope, Tracer } from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/** Per-call options accepted by {@link InsightMemory.search}. */\nexport interface InsightSearchOptions {\n readonly topK?: number;\n /**\n * Include quarantined insights (P1-4). Defaults to `false`. Since\n * reflection-synthesized insights *always* land quarantined, set this\n * `true` to surface them for the validation / inspector path - never\n * for auto-recall fed back into the model.\n */\n readonly includeQuarantined?: boolean;\n readonly signal?: AbortSignal;\n}\n\n/** Per-call options accepted by {@link InsightMemory.list}. */\nexport interface InsightListOptions {\n readonly limit?: number;\n /** Include quarantined insights (validation / inspector path). */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `InsightMemory` - list / search reflection insights. A no-op (returns\n * empty) when the storage adapter does not expose the optional\n * `insights` surface.\n *\n * @stable\n */\nexport class InsightMemory {\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 /** FTS keyword search over insight text. */\n async search(\n scope: SessionScope,\n query: string,\n opts: InsightSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Insight>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.insight',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.search.insight.count': 0 });\n return [];\n }\n const hits = await store.search(scope, query, {\n topK: opts.topK ?? 10,\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n // MCON-16: retrieval reinforces (ExpeL) - a recalled insight gains\n // +1 salience, offsetting the per-reflection-pass -1 decay, so\n // used insights persist while unused ones slide to the prune\n // floor. Best-effort bookkeeping: never break the read path.\n for (const hit of hits) {\n try {\n await store.bumpSalience(hit.record.id, 1, 'retrieval');\n } catch {\n // Advisory only.\n }\n }\n span.setAttributes({ 'memory.search.insight.count': hits.length });\n return hits;\n },\n );\n }\n\n /** Most-recent insights for the scope (newest first). */\n async list(scope: SessionScope, opts: InsightListOptions = {}): Promise<ReadonlyArray<Insight>> {\n return withMemorySpan(this.#tracer, 'memory.read.insight', scope, {}, async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.read.insight.count': 0 });\n return [];\n }\n const out = await store.list(scope, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n span.setAttributes({ 'memory.read.insight.count': out.length });\n return out;\n });\n }\n\n /** Lookup a single insight by id. */\n async get(id: string): Promise<Insight | null> {\n const store = this.#store.insights;\n if (store === undefined || typeof store.get !== 'function') return null;\n return store.get(id);\n }\n\n /**\n * Promote a quarantined insight out of quarantine (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored text and **refuses** promotion of an injection-flagged insight\n * unless an operator passes `{ force: true }` from a trusted context.\n */\n async validate(\n scope: SessionScope,\n insightId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.insight',\n scope,\n { 'memory.insight.action': 'validate', 'memory.insight.insight_id': insightId },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined || typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = typeof store.get === 'function' ? await store.get(insightId) : null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.insight.validate.refused': true });\n throw new QuarantinePromotionRefusedError(insightId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.insight.validate.forced': force });\n await store.setStatus(insightId, 'active', reason);\n },\n );\n }\n}\n\n/** Tie-break margin keeping a capped insight strictly below its cited fact. */\nconst RANK_CAP_EPSILON = 1e-6;\n\n/**\n * Enforce the P1-1 rank ceiling: an insight may never outrank a primary\n * fact **it cites**. For each insight hit, if any fact it cites is\n * present in `factHits`, its score is lowered to strictly below that\n * cited fact's score - so concatenating the two lists and sorting by\n * score descending can never place the insight above the evidence it\n * was synthesized from. Insights whose cited facts are absent from\n * `factHits` are returned unchanged; this is a relative, not a global,\n * cap (per the execution plan: \"never outrank the primary facts they\n * cite\").\n *\n * Pure + deterministic - does not mutate its inputs.\n *\n * @stable\n */\nexport function capInsightsBelowFacts(\n factHits: ReadonlyArray<MemoryHit<Fact>>,\n insightHits: ReadonlyArray<MemoryHit<Insight>>,\n): ReadonlyArray<MemoryHit<Insight>> {\n if (factHits.length === 0 || insightHits.length === 0) return insightHits;\n const factScoreById = new Map<string, number>();\n for (const hit of factHits) {\n const prev = factScoreById.get(hit.record.id);\n if (prev === undefined || hit.score > prev) factScoreById.set(hit.record.id, hit.score);\n }\n return insightHits.map((hit) => {\n let citedMax = Number.NEGATIVE_INFINITY;\n for (const id of hit.record.cites) {\n const score = factScoreById.get(id);\n if (score !== undefined && score > citedMax) citedMax = score;\n }\n // No cited fact present in the fused set, or already below it ⇒ keep.\n if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;\n const capped = citedMax - RANK_CAP_EPSILON;\n return {\n ...hit,\n score: capped,\n signals: Object.freeze({ ...(hit.signals ?? {}), 'rank-cap': capped }),\n };\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"insight-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/insight-memory.ts"],"sourcesContent":["/**\n * `InsightMemory` - the thin, read-only surface over the reflection\n * insights the consolidator synthesizes (P1-1). Insights are *derived*\n * (quarantined + `provenance: 'reflection'`), carry mandatory citations,\n * and are retrieval-ranked **below the primary facts they cite**. The\n * write + salience-management paths live in the consolidator's\n * reflection pass; this tier only lists / searches for the validation /\n * inspector path.\n *\n * Search is FTS-only - insights are a soft inspector surface, not\n * primary recall, so no embedder is wired here.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, Insight, MemoryHit, SessionScope, Tracer } from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/** Per-call options accepted by {@link InsightMemory.search}. */\nexport interface InsightSearchOptions {\n readonly topK?: number;\n /**\n * Include quarantined insights (P1-4). Defaults to `false`. Since\n * reflection-synthesized insights *always* land quarantined, set this\n * `true` to surface them for the validation / inspector path - never\n * for auto-recall fed back into the model.\n */\n readonly includeQuarantined?: boolean;\n readonly signal?: AbortSignal;\n}\n\n/** Per-call options accepted by {@link InsightMemory.list}. */\nexport interface InsightListOptions {\n readonly limit?: number;\n /** Include quarantined insights (validation / inspector path). */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `InsightMemory` - list / search reflection insights. A no-op (returns\n * empty) when the storage adapter does not expose the optional\n * `insights` surface.\n *\n * @stable\n */\nexport class InsightMemory {\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 /** FTS keyword search over insight text. */\n async search(\n scope: SessionScope,\n query: string,\n opts: InsightSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Insight>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.insight',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.search.insight.count': 0 });\n return [];\n }\n const hits = await store.search(scope, query, {\n topK: opts.topK ?? 10,\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n // MCON-16: retrieval reinforces (ExpeL) - a recalled insight gains\n // +1 salience, offsetting the per-reflection-pass -1 decay, so\n // used insights persist while unused ones slide to the prune\n // floor. Best-effort bookkeeping: never break the read path.\n for (const hit of hits) {\n try {\n await store.bumpSalience(hit.record.id, 1, 'retrieval');\n } catch {\n // Advisory only.\n }\n }\n span.setAttributes({ 'memory.search.insight.count': hits.length });\n return hits;\n },\n );\n }\n\n /** Most-recent insights for the scope (newest first). */\n async list(scope: SessionScope, opts: InsightListOptions = {}): Promise<ReadonlyArray<Insight>> {\n return withMemorySpan(this.#tracer, 'memory.read.insight', scope, {}, async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.read.insight.count': 0 });\n return [];\n }\n const out = await store.list(scope, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n span.setAttributes({ 'memory.read.insight.count': out.length });\n return out;\n });\n }\n\n /** Lookup a single insight by id. */\n async get(id: string): Promise<Insight | null> {\n const store = this.#store.insights;\n if (store === undefined || typeof store.get !== 'function') return null;\n return store.get(id);\n }\n\n /**\n * Promote a quarantined insight out of quarantine (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored text and **refuses** promotion of an injection-flagged insight\n * unless an operator passes `{ force: true }` from a trusted context.\n */\n async validate(\n scope: SessionScope,\n insightId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.insight',\n scope,\n { 'memory.insight.action': 'validate', 'memory.insight.insight_id': insightId },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined || typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = typeof store.get === 'function' ? await store.get(insightId) : null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.insight.validate.refused': true });\n throw new QuarantinePromotionRefusedError(insightId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.insight.validate.forced': force });\n await store.setStatus(insightId, 'active', reason, scope);\n },\n );\n }\n}\n\n/** Tie-break margin keeping a capped insight strictly below its cited fact. */\nconst RANK_CAP_EPSILON = 1e-6;\n\n/**\n * Enforce the P1-1 rank ceiling: an insight may never outrank a primary\n * fact **it cites**. For each insight hit, if any fact it cites is\n * present in `factHits`, its score is lowered to strictly below that\n * cited fact's score - so concatenating the two lists and sorting by\n * score descending can never place the insight above the evidence it\n * was synthesized from. Insights whose cited facts are absent from\n * `factHits` are returned unchanged; this is a relative, not a global,\n * cap (per the execution plan: \"never outrank the primary facts they\n * cite\").\n *\n * Pure + deterministic - does not mutate its inputs.\n *\n * @stable\n */\nexport function capInsightsBelowFacts(\n factHits: ReadonlyArray<MemoryHit<Fact>>,\n insightHits: ReadonlyArray<MemoryHit<Insight>>,\n): ReadonlyArray<MemoryHit<Insight>> {\n if (factHits.length === 0 || insightHits.length === 0) return insightHits;\n const factScoreById = new Map<string, number>();\n for (const hit of factHits) {\n const prev = factScoreById.get(hit.record.id);\n if (prev === undefined || hit.score > prev) factScoreById.set(hit.record.id, hit.score);\n }\n return insightHits.map((hit) => {\n let citedMax = Number.NEGATIVE_INFINITY;\n for (const id of hit.record.cites) {\n const score = factScoreById.get(id);\n if (score !== undefined && score > citedMax) citedMax = score;\n }\n // No cited fact present in the fused set, or already below it ⇒ keep.\n if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;\n const capped = citedMax - RANK_CAP_EPSILON;\n return {\n ...hit,\n score: capped,\n signals: Object.freeze({ ...(hit.signals ?? {}), 'rank-cap': capped }),\n };\n });\n}\n"],"mappings":"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"}
@@ -44,7 +44,7 @@ interface RuleInput {
44
44
  interface InduceOptions {
45
45
  /** Sensitivity of the stored procedure. Default `'internal'`. */
46
46
  readonly sensitivity?: Sensitivity;
47
- /** Priority of the stored procedure. Default {@link INDUCED_PRIORITY} (40). */
47
+ /** Priority of the stored procedure. Default `INDUCED_PRIORITY` (40). */
48
48
  readonly priority?: number;
49
49
  }
50
50
  /**
@@ -1,8 +1,8 @@
1
1
  import { withMemorySpan } from "../internal/spans.js";
2
2
  import { ProcedureInductionNotConfiguredError, QuarantinePromotionRefusedError } from "../errors/index.js";
3
+ import { detectMemoryInjection } from "../internal/injection-heuristics.js";
3
4
  import { runWorkflowInduction, trajectoryFromRunState } from "../consolidator/phases/induce.js";
4
5
  import { newMemoryId } from "../internal/id.js";
5
- import { detectMemoryInjection } from "../internal/injection-heuristics.js";
6
6
 
7
7
  //#region src/tiers/procedural-memory.ts
8
8
  /** Default priority for an *induced* procedure - below the author default
@@ -297,7 +297,7 @@ var ProceduralMemory = class {
297
297
  }
298
298
  }
299
299
  span.setAttributes({ "memory.procedural.validate.forced": force });
300
- await store.setStatus(ruleId, "active", reason);
300
+ await store.setStatus(ruleId, "active", reason, scope);
301
301
  });
302
302
  }
303
303
  };
@@ -1 +1 @@
1
- {"version":3,"file":"procedural-memory.js","names":["#store","#tracer","#inducer","#promoteAfterSuccesses","rule: Rule"],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":["import type {\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Rule,\n RunState,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport {\n type InducedProcedure,\n runWorkflowInduction,\n type Trajectory,\n trajectoryFromRunState,\n type WorkflowInducer,\n} from '../consolidator/phases/induce.js';\nimport {\n ProcedureInductionNotConfiguredError,\n QuarantinePromotionRefusedError,\n} from '../errors/index.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';\n\n/** Default priority for an *induced* procedure - below the author default\n * (50) because an induced, still-quarantined procedure is provisional. */\nconst INDUCED_PRIORITY = 40;\n\n/**\n * Author-time rule payload accepted by {@link ProceduralMemory.define}.\n *\n * @stable\n */\nexport interface RuleInput {\n readonly text: string;\n /**\n * Free-form predicate evaluated by {@link ProceduralMemory.activate}.\n * The predicate language is intentionally narrow in v0.1: either\n * the literal string `'always'`, or a `'topic=...'` / `'tag=...'`\n * shorthand. Custom predicates should be expressed as a callable\n * matched in `activate(...)`'s `customMatchers` argument.\n */\n readonly condition?: string;\n /** Default `'public'` per DEC-126 - rules are NOT user data. */\n readonly sensitivity?: Sensitivity;\n readonly priority?: number;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Optional structured workflow payload (P2-2). Usually set by\n * {@link ProceduralMemory.induce}, but accepted here so an author can\n * round-trip a hand-written procedure. See {@link Rule.steps}.\n */\n readonly steps?: ReadonlyArray<string>;\n /** Variable names abstracted into {@link RuleInput.steps} (P2-2). */\n readonly variables?: ReadonlyArray<string>;\n /** Verifiable success criteria stored with the procedure (P2-2). */\n readonly successCriteria?: ReadonlyArray<string>;\n /** Principal dimension (D3). Absent ⇒ NULL (treated `'user'`). */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Options for {@link ProceduralMemory.induce}.\n *\n * @stable\n */\nexport interface InduceOptions {\n /** Sensitivity of the stored procedure. Default `'internal'`. */\n readonly sensitivity?: Sensitivity;\n /** Priority of the stored procedure. Default {@link INDUCED_PRIORITY} (40). */\n readonly priority?: number;\n}\n\n/**\n * Predicate context passed to {@link ProceduralMemory.activate}.\n *\n * @stable\n */\nexport interface RuleActivationContext {\n readonly topic?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * `ProceduralMemory` - standing orders activated when the agent's\n * current context matches the rule's predicate. The activation rules\n * are deterministic so the agent runtime + ContextEngine can render\n * the active set into the system prompt every step.\n *\n * P2-2 adds {@link ProceduralMemory.induce}: distil a reusable workflow\n * from a successful agent trajectory and store it **quarantined** (it must\n * not drive actions until validated). Quarantined procedures are excluded\n * from {@link ProceduralMemory.activate} but remain visible to\n * {@link ProceduralMemory.list}.\n *\n * @stable\n */\nexport class ProceduralMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n /** Opt-in workflow inducer (P2-2). `null` ⇒ {@link induce} throws. */\n readonly #inducer: WorkflowInducer | null;\n /**\n * Promotion-by-demonstrated-success threshold (MCON-2 part 4).\n * `null` ⇒ outcomes are counted but never auto-promote.\n */\n readonly #promoteAfterSuccesses: number | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n inducer?: WorkflowInducer | null;\n promoteAfterSuccesses?: number | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#inducer = args.inducer ?? null;\n const k = args.promoteAfterSuccesses;\n this.#promoteAfterSuccesses =\n typeof k === 'number' && Number.isFinite(k) && k >= 1 ? Math.floor(k) : null;\n }\n\n /**\n * Record the outcome of one demonstrated reuse of a procedure\n * (MCON-2 part 4). A success increments the rule's persistent\n * `successCount`; when `procedurePromotion.afterSuccesses` is\n * configured and a QUARANTINED procedure reaches the threshold it is\n * promoted through {@link validate} - the injection gate still\n * applies, so a flagged text refuses promotion (surfaced as\n * `refused: true`) no matter how many successes accumulate.\n * Failures are observed but not persisted (no negative counter yet).\n *\n * Callers decide what \"success\" means - typically\n * `checkSuccessCriteria(...)` over the procedure's stored\n * `successCriteria`, or an operator's judgement.\n *\n * @stable\n */\n async recordOutcome(\n scope: SessionScope,\n ruleId: string,\n succeeded: boolean,\n ): Promise<{\n readonly successCount: number;\n readonly promoted: boolean;\n readonly refused: boolean;\n }> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'record-outcome', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (!succeeded || typeof store.recordSuccess !== 'function') {\n span.setAttributes({ 'memory.procedural.outcome.counted': false });\n return { successCount: 0, promoted: false, refused: false };\n }\n const successCount = await store.recordSuccess(ruleId);\n span.setAttributes({ 'memory.procedural.outcome.success_count': successCount });\n const threshold = this.#promoteAfterSuccesses;\n if (threshold === null || successCount < threshold) {\n return { successCount, promoted: false, refused: false };\n }\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing === null || existing.status !== 'quarantined') {\n return { successCount, promoted: false, refused: false };\n }\n try {\n await this.validate(\n scope,\n ruleId,\n `demonstrated-success: ${successCount} >= ${threshold}`,\n );\n span.setAttributes({ 'memory.procedural.outcome.promoted': true });\n return { successCount, promoted: true, refused: false };\n } catch (err) {\n if (err instanceof QuarantinePromotionRefusedError) {\n span.setAttributes({ 'memory.procedural.outcome.refused': true });\n return { successCount, promoted: false, refused: true };\n }\n throw err;\n }\n },\n );\n }\n\n /** Persist a rule. Returns the stored record. */\n async define(scope: SessionScope, input: RuleInput): Promise<Rule> {\n return withMemorySpan(this.#tracer, 'memory.write.procedural', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'public',\n text: input.text,\n ...(input.condition !== undefined ? { condition: input.condition } : {}),\n priority: input.priority ?? 50,\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.steps !== undefined ? { steps: Object.freeze([...input.steps]) } : {}),\n ...(input.variables !== undefined\n ? { variables: Object.freeze([...input.variables]) }\n : {}),\n ...(input.successCriteria !== undefined\n ? { successCriteria: Object.freeze([...input.successCriteria]) }\n : {}),\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.priority': rule.priority,\n 'memory.procedural.text_length': input.text.length,\n });\n return rule;\n });\n }\n\n /**\n * Induce a reusable procedure (P2-2) from a successful agent trajectory\n * and store it **quarantined** + `provenance: 'induction'` (P1-4). Returns\n * the stored {@link Rule}, or `null` when the trajectory was unsuccessful /\n * empty or the inducer produced nothing inducible.\n *\n * Throws {@link ProcedureInductionNotConfiguredError} when no inducer was\n * configured (`createMemory({ procedureInduction: { provider } })`).\n */\n async induce(\n scope: SessionScope,\n trajectory: Trajectory,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n const inducer = this.#inducer;\n if (inducer === null) throw new ProcedureInductionNotConfiguredError();\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'induce' },\n async (span) => {\n const induced = await runWorkflowInduction(trajectory, inducer, {});\n if (induced === null) {\n span.setAttributes({ 'memory.procedural.induced': 0 });\n return null;\n }\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n // Induced from a run ⇒ user-derived, so 'internal' (not the public\n // default for author-defined rules).\n sensitivity: opts.sensitivity ?? 'internal',\n text: renderProcedureText(induced),\n priority: opts.priority ?? INDUCED_PRIORITY,\n steps: Object.freeze([...induced.steps]),\n variables: Object.freeze([...induced.variables]),\n successCriteria: Object.freeze([...induced.successCriteria]),\n // Highest-poisoning-risk write in the system - procedures drive\n // actions, so it lands quarantined + provenance-tagged (P1-4).\n provenance: 'induction' satisfies MemoryProvenance,\n status: 'quarantined' satisfies MemoryStatus,\n // D3: an induced procedure is the agent's own inference.\n owner: 'agent',\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.induced': 1,\n 'memory.procedural.steps': induced.steps.length,\n 'memory.procedural.variables': induced.variables.length,\n });\n return rule;\n },\n );\n }\n\n /**\n * Convenience over {@link induce}: distil the {@link Trajectory} from a\n * completed {@link RunState} (the agent's already-emitted run state) and\n * induce a procedure. The success signal is `status === 'completed'`.\n */\n async induceFromRun(\n scope: SessionScope,\n run: RunState,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n return this.induce(scope, trajectoryFromRunState(run), opts);\n }\n\n /** Soft-delete a rule. */\n async remove(scope: SessionScope, ruleId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'remove', 'memory.procedural.rule_id': ruleId },\n async () => {\n await this.#store.procedural.remove(ruleId, reason);\n },\n );\n }\n\n /** List every active (non-deleted) rule for the supplied scope. */\n async list(scope: SessionScope): Promise<ReadonlyArray<Rule>> {\n return withMemorySpan(this.#tracer, 'memory.read.procedural', scope, {}, async (span) => {\n const out = await this.#store.procedural.list(scope);\n span.setAttributes({ 'memory.read.procedural.count': out.length });\n return out;\n });\n }\n\n /**\n * Return the rules active under `context`. Rules without a\n * `condition` are always active; the bundled predicate vocabulary\n * supports the literals `'always'`, `'topic=<topic>'`, and\n * `'tag=<tag>'`. Anything outside that grammar is treated as\n * always-active so callers do not silently lose rules.\n *\n * **Quarantined procedures are excluded** (P1-4 / P2-2): an induced\n * procedure must not drive actions until validated, so activation - which\n * feeds the system prompt - never surfaces it.\n */\n async activate(\n scope: SessionScope,\n context: RuleActivationContext = {},\n ): Promise<ReadonlyArray<Rule>> {\n const rules = await this.list(scope);\n return rules\n .filter((rule) => rule.status !== 'quarantined' && predicateMatches(rule, context))\n .sort((a, b) => b.priority - a.priority);\n }\n\n /**\n * Runbook content search (D3): \"find the procedure for this task\" -\n * lexical recall over rule text, as opposed to predicate\n * {@link activate}. Returns **whole validated procedures** (the full\n * {@link Rule} incl. steps / variables / success criteria) so a match\n * can be followed file-style rather than re-synthesized from\n * fragments. Quarantined (unvalidated induced) procedures are\n * excluded - they must not drive actions - unless the inspector opts\n * in via `includeQuarantined`.\n *\n * Uses the storage adapter's FTS surface when available\n * (`procedural.search`, the default `@graphorin/store-sqlite` adapter\n * implements it via migration 028); otherwise degrades to an offline\n * in-memory token-overlap scan over {@link list}, so custom adapters\n * keep working without the index.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: { readonly topK?: number; readonly includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<MemoryHit<Rule>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.procedural',\n scope,\n { 'memory.procedural.action': 'search' },\n async (span) => {\n const topK = opts.topK ?? 5;\n const store = this.#store.procedural;\n if (typeof store.search === 'function') {\n const hits = await store.search(scope, query, {\n topK,\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n });\n span.setAttributes({\n 'memory.read.procedural.count': hits.length,\n 'memory.read.procedural.search_backend': 'store',\n });\n return hits;\n }\n // Fallback: offline token-overlap scoring over list(). Coarse by\n // design - it exists so the runbook surface degrades instead of\n // disappearing on adapters without an FTS index.\n const tokens = tokenizeForRunbook(query);\n const rules = await this.list(scope);\n const scored = rules\n .filter((rule) => opts.includeQuarantined === true || rule.status !== 'quarantined')\n .map((rule) => {\n const haystack = tokenizeForRunbook(\n [rule.text, ...(rule.steps ?? []), ...(rule.tags ?? [])].join(' '),\n );\n const overlap = tokens.filter((t) => haystack.includes(t)).length;\n return { rule, overlap };\n })\n .filter((entry) => entry.overlap > 0)\n .sort((a, b) => b.overlap - a.overlap || (a.rule.id < b.rule.id ? -1 : 1))\n .slice(0, topK);\n span.setAttributes({\n 'memory.read.procedural.count': scored.length,\n 'memory.read.procedural.search_backend': 'fallback',\n });\n return scored.map((entry) => ({\n record: entry.rule,\n score: tokens.length === 0 ? 0 : entry.overlap / tokens.length,\n signals: { lexical: entry.overlap },\n }));\n },\n );\n }\n\n /**\n * Promote a quarantined (induced) procedure into `activate()` (MCON-2).\n * Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict\n * from the stored rule text and **refuses** promotion of an injection-flagged\n * procedure unless an operator passes `{ force: true }`. Induced procedures\n * drive *actions*, so this gate matters most for them.\n */\n async validate(\n scope: SessionScope,\n ruleId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'validate', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] ProceduralMemory.validate(...) requires a storage adapter that implements `procedural.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via ProceduralMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n // No store get-by-id for rules - list() already surfaces quarantined.\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.procedural.validate.refused': true });\n throw new QuarantinePromotionRefusedError(ruleId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.procedural.validate.forced': force });\n await store.setStatus(ruleId, 'active', reason);\n },\n );\n }\n}\n\n/** Render an induced procedure into the human-readable `Rule.text`. */\nfunction renderProcedureText(procedure: InducedProcedure): string {\n const lines = [procedure.title, ...procedure.steps.map((step, i) => `${i + 1}. ${step}`)];\n return lines.join('\\n');\n}\n\n/** Lowercased word tokens for the offline runbook-search fallback (D3). */\nfunction tokenizeForRunbook(text: string): ReadonlyArray<string> {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((token) => token.length > 1);\n}\n\nfunction predicateMatches(rule: Rule, context: RuleActivationContext): boolean {\n if (rule.condition === undefined || rule.condition === 'always') return true;\n if (rule.condition.startsWith('topic=')) {\n const wanted = rule.condition.slice('topic='.length).trim();\n return context.topic === wanted;\n }\n if (rule.condition.startsWith('tag=')) {\n const wanted = rule.condition.slice('tag='.length).trim();\n return (context.tags ?? []).includes(wanted);\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"procedural-memory.js","names":["#store","#tracer","#inducer","#promoteAfterSuccesses","rule: Rule"],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":["import type {\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Rule,\n RunState,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport {\n type InducedProcedure,\n runWorkflowInduction,\n type Trajectory,\n trajectoryFromRunState,\n type WorkflowInducer,\n} from '../consolidator/phases/induce.js';\nimport {\n ProcedureInductionNotConfiguredError,\n QuarantinePromotionRefusedError,\n} from '../errors/index.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';\n\n/** Default priority for an *induced* procedure - below the author default\n * (50) because an induced, still-quarantined procedure is provisional. */\nconst INDUCED_PRIORITY = 40;\n\n/**\n * Author-time rule payload accepted by {@link ProceduralMemory.define}.\n *\n * @stable\n */\nexport interface RuleInput {\n readonly text: string;\n /**\n * Free-form predicate evaluated by {@link ProceduralMemory.activate}.\n * The predicate language is intentionally narrow in v0.1: either\n * the literal string `'always'`, or a `'topic=...'` / `'tag=...'`\n * shorthand. Custom predicates should be expressed as a callable\n * matched in `activate(...)`'s `customMatchers` argument.\n */\n readonly condition?: string;\n /** Default `'public'` per DEC-126 - rules are NOT user data. */\n readonly sensitivity?: Sensitivity;\n readonly priority?: number;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Optional structured workflow payload (P2-2). Usually set by\n * {@link ProceduralMemory.induce}, but accepted here so an author can\n * round-trip a hand-written procedure. See {@link Rule.steps}.\n */\n readonly steps?: ReadonlyArray<string>;\n /** Variable names abstracted into {@link RuleInput.steps} (P2-2). */\n readonly variables?: ReadonlyArray<string>;\n /** Verifiable success criteria stored with the procedure (P2-2). */\n readonly successCriteria?: ReadonlyArray<string>;\n /** Principal dimension (D3). Absent ⇒ NULL (treated `'user'`). */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Options for {@link ProceduralMemory.induce}.\n *\n * @stable\n */\nexport interface InduceOptions {\n /** Sensitivity of the stored procedure. Default `'internal'`. */\n readonly sensitivity?: Sensitivity;\n /** Priority of the stored procedure. Default `INDUCED_PRIORITY` (40). */\n readonly priority?: number;\n}\n\n/**\n * Predicate context passed to {@link ProceduralMemory.activate}.\n *\n * @stable\n */\nexport interface RuleActivationContext {\n readonly topic?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * `ProceduralMemory` - standing orders activated when the agent's\n * current context matches the rule's predicate. The activation rules\n * are deterministic so the agent runtime + ContextEngine can render\n * the active set into the system prompt every step.\n *\n * P2-2 adds {@link ProceduralMemory.induce}: distil a reusable workflow\n * from a successful agent trajectory and store it **quarantined** (it must\n * not drive actions until validated). Quarantined procedures are excluded\n * from {@link ProceduralMemory.activate} but remain visible to\n * {@link ProceduralMemory.list}.\n *\n * @stable\n */\nexport class ProceduralMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n /** Opt-in workflow inducer (P2-2). `null` ⇒ {@link induce} throws. */\n readonly #inducer: WorkflowInducer | null;\n /**\n * Promotion-by-demonstrated-success threshold (MCON-2 part 4).\n * `null` ⇒ outcomes are counted but never auto-promote.\n */\n readonly #promoteAfterSuccesses: number | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n inducer?: WorkflowInducer | null;\n promoteAfterSuccesses?: number | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#inducer = args.inducer ?? null;\n const k = args.promoteAfterSuccesses;\n this.#promoteAfterSuccesses =\n typeof k === 'number' && Number.isFinite(k) && k >= 1 ? Math.floor(k) : null;\n }\n\n /**\n * Record the outcome of one demonstrated reuse of a procedure\n * (MCON-2 part 4). A success increments the rule's persistent\n * `successCount`; when `procedurePromotion.afterSuccesses` is\n * configured and a QUARANTINED procedure reaches the threshold it is\n * promoted through {@link validate} - the injection gate still\n * applies, so a flagged text refuses promotion (surfaced as\n * `refused: true`) no matter how many successes accumulate.\n * Failures are observed but not persisted (no negative counter yet).\n *\n * Callers decide what \"success\" means - typically\n * `checkSuccessCriteria(...)` over the procedure's stored\n * `successCriteria`, or an operator's judgement.\n *\n * @stable\n */\n async recordOutcome(\n scope: SessionScope,\n ruleId: string,\n succeeded: boolean,\n ): Promise<{\n readonly successCount: number;\n readonly promoted: boolean;\n readonly refused: boolean;\n }> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'record-outcome', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (!succeeded || typeof store.recordSuccess !== 'function') {\n span.setAttributes({ 'memory.procedural.outcome.counted': false });\n return { successCount: 0, promoted: false, refused: false };\n }\n const successCount = await store.recordSuccess(ruleId);\n span.setAttributes({ 'memory.procedural.outcome.success_count': successCount });\n const threshold = this.#promoteAfterSuccesses;\n if (threshold === null || successCount < threshold) {\n return { successCount, promoted: false, refused: false };\n }\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing === null || existing.status !== 'quarantined') {\n return { successCount, promoted: false, refused: false };\n }\n try {\n await this.validate(\n scope,\n ruleId,\n `demonstrated-success: ${successCount} >= ${threshold}`,\n );\n span.setAttributes({ 'memory.procedural.outcome.promoted': true });\n return { successCount, promoted: true, refused: false };\n } catch (err) {\n if (err instanceof QuarantinePromotionRefusedError) {\n span.setAttributes({ 'memory.procedural.outcome.refused': true });\n return { successCount, promoted: false, refused: true };\n }\n throw err;\n }\n },\n );\n }\n\n /** Persist a rule. Returns the stored record. */\n async define(scope: SessionScope, input: RuleInput): Promise<Rule> {\n return withMemorySpan(this.#tracer, 'memory.write.procedural', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'public',\n text: input.text,\n ...(input.condition !== undefined ? { condition: input.condition } : {}),\n priority: input.priority ?? 50,\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.steps !== undefined ? { steps: Object.freeze([...input.steps]) } : {}),\n ...(input.variables !== undefined\n ? { variables: Object.freeze([...input.variables]) }\n : {}),\n ...(input.successCriteria !== undefined\n ? { successCriteria: Object.freeze([...input.successCriteria]) }\n : {}),\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.priority': rule.priority,\n 'memory.procedural.text_length': input.text.length,\n });\n return rule;\n });\n }\n\n /**\n * Induce a reusable procedure (P2-2) from a successful agent trajectory\n * and store it **quarantined** + `provenance: 'induction'` (P1-4). Returns\n * the stored {@link Rule}, or `null` when the trajectory was unsuccessful /\n * empty or the inducer produced nothing inducible.\n *\n * Throws {@link ProcedureInductionNotConfiguredError} when no inducer was\n * configured (`createMemory({ procedureInduction: { provider } })`).\n */\n async induce(\n scope: SessionScope,\n trajectory: Trajectory,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n const inducer = this.#inducer;\n if (inducer === null) throw new ProcedureInductionNotConfiguredError();\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'induce' },\n async (span) => {\n const induced = await runWorkflowInduction(trajectory, inducer, {});\n if (induced === null) {\n span.setAttributes({ 'memory.procedural.induced': 0 });\n return null;\n }\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n // Induced from a run ⇒ user-derived, so 'internal' (not the public\n // default for author-defined rules).\n sensitivity: opts.sensitivity ?? 'internal',\n text: renderProcedureText(induced),\n priority: opts.priority ?? INDUCED_PRIORITY,\n steps: Object.freeze([...induced.steps]),\n variables: Object.freeze([...induced.variables]),\n successCriteria: Object.freeze([...induced.successCriteria]),\n // Highest-poisoning-risk write in the system - procedures drive\n // actions, so it lands quarantined + provenance-tagged (P1-4).\n provenance: 'induction' satisfies MemoryProvenance,\n status: 'quarantined' satisfies MemoryStatus,\n // D3: an induced procedure is the agent's own inference.\n owner: 'agent',\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.induced': 1,\n 'memory.procedural.steps': induced.steps.length,\n 'memory.procedural.variables': induced.variables.length,\n });\n return rule;\n },\n );\n }\n\n /**\n * Convenience over {@link induce}: distil the {@link Trajectory} from a\n * completed {@link RunState} (the agent's already-emitted run state) and\n * induce a procedure. The success signal is `status === 'completed'`.\n */\n async induceFromRun(\n scope: SessionScope,\n run: RunState,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n return this.induce(scope, trajectoryFromRunState(run), opts);\n }\n\n /** Soft-delete a rule. */\n async remove(scope: SessionScope, ruleId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'remove', 'memory.procedural.rule_id': ruleId },\n async () => {\n await this.#store.procedural.remove(ruleId, reason);\n },\n );\n }\n\n /** List every active (non-deleted) rule for the supplied scope. */\n async list(scope: SessionScope): Promise<ReadonlyArray<Rule>> {\n return withMemorySpan(this.#tracer, 'memory.read.procedural', scope, {}, async (span) => {\n const out = await this.#store.procedural.list(scope);\n span.setAttributes({ 'memory.read.procedural.count': out.length });\n return out;\n });\n }\n\n /**\n * Return the rules active under `context`. Rules without a\n * `condition` are always active; the bundled predicate vocabulary\n * supports the literals `'always'`, `'topic=<topic>'`, and\n * `'tag=<tag>'`. Anything outside that grammar is treated as\n * always-active so callers do not silently lose rules.\n *\n * **Quarantined procedures are excluded** (P1-4 / P2-2): an induced\n * procedure must not drive actions until validated, so activation - which\n * feeds the system prompt - never surfaces it.\n */\n async activate(\n scope: SessionScope,\n context: RuleActivationContext = {},\n ): Promise<ReadonlyArray<Rule>> {\n const rules = await this.list(scope);\n return rules\n .filter((rule) => rule.status !== 'quarantined' && predicateMatches(rule, context))\n .sort((a, b) => b.priority - a.priority);\n }\n\n /**\n * Runbook content search (D3): \"find the procedure for this task\" -\n * lexical recall over rule text, as opposed to predicate\n * {@link activate}. Returns **whole validated procedures** (the full\n * {@link Rule} incl. steps / variables / success criteria) so a match\n * can be followed file-style rather than re-synthesized from\n * fragments. Quarantined (unvalidated induced) procedures are\n * excluded - they must not drive actions - unless the inspector opts\n * in via `includeQuarantined`.\n *\n * Uses the storage adapter's FTS surface when available\n * (`procedural.search`, the default `@graphorin/store-sqlite` adapter\n * implements it via migration 028); otherwise degrades to an offline\n * in-memory token-overlap scan over {@link list}, so custom adapters\n * keep working without the index.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: { readonly topK?: number; readonly includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<MemoryHit<Rule>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.read.procedural',\n scope,\n { 'memory.procedural.action': 'search' },\n async (span) => {\n const topK = opts.topK ?? 5;\n const store = this.#store.procedural;\n if (typeof store.search === 'function') {\n const hits = await store.search(scope, query, {\n topK,\n ...(opts.includeQuarantined === true ? { includeQuarantined: true } : {}),\n });\n span.setAttributes({\n 'memory.read.procedural.count': hits.length,\n 'memory.read.procedural.search_backend': 'store',\n });\n return hits;\n }\n // Fallback: offline token-overlap scoring over list(). Coarse by\n // design - it exists so the runbook surface degrades instead of\n // disappearing on adapters without an FTS index.\n const tokens = tokenizeForRunbook(query);\n const rules = await this.list(scope);\n const scored = rules\n .filter((rule) => opts.includeQuarantined === true || rule.status !== 'quarantined')\n .map((rule) => {\n const haystack = tokenizeForRunbook(\n [rule.text, ...(rule.steps ?? []), ...(rule.tags ?? [])].join(' '),\n );\n const overlap = tokens.filter((t) => haystack.includes(t)).length;\n return { rule, overlap };\n })\n .filter((entry) => entry.overlap > 0)\n .sort((a, b) => b.overlap - a.overlap || (a.rule.id < b.rule.id ? -1 : 1))\n .slice(0, topK);\n span.setAttributes({\n 'memory.read.procedural.count': scored.length,\n 'memory.read.procedural.search_backend': 'fallback',\n });\n return scored.map((entry) => ({\n record: entry.rule,\n score: tokens.length === 0 ? 0 : entry.overlap / tokens.length,\n signals: { lexical: entry.overlap },\n }));\n },\n );\n }\n\n /**\n * Promote a quarantined (induced) procedure into `activate()` (MCON-2).\n * Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict\n * from the stored rule text and **refuses** promotion of an injection-flagged\n * procedure unless an operator passes `{ force: true }`. Induced procedures\n * drive *actions*, so this gate matters most for them.\n */\n async validate(\n scope: SessionScope,\n ruleId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'validate', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] ProceduralMemory.validate(...) requires a storage adapter that implements `procedural.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via ProceduralMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n // No store get-by-id for rules - list() already surfaces quarantined.\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.procedural.validate.refused': true });\n throw new QuarantinePromotionRefusedError(ruleId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.procedural.validate.forced': force });\n await store.setStatus(ruleId, 'active', reason, scope);\n },\n );\n }\n}\n\n/** Render an induced procedure into the human-readable `Rule.text`. */\nfunction renderProcedureText(procedure: InducedProcedure): string {\n const lines = [procedure.title, ...procedure.steps.map((step, i) => `${i + 1}. ${step}`)];\n return lines.join('\\n');\n}\n\n/** Lowercased word tokens for the offline runbook-search fallback (D3). */\nfunction tokenizeForRunbook(text: string): ReadonlyArray<string> {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((token) => token.length > 1);\n}\n\nfunction predicateMatches(rule: Rule, context: RuleActivationContext): boolean {\n if (rule.condition === undefined || rule.condition === 'always') return true;\n if (rule.condition.startsWith('topic=')) {\n const wanted = rule.condition.slice('topic='.length).trim();\n return context.topic === wanted;\n }\n if (rule.condition.startsWith('tag=')) {\n const wanted = rule.condition.slice('tag='.length).trim();\n return (context.tags ?? []).includes(wanted);\n }\n return true;\n}\n"],"mappings":"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"}
@@ -105,6 +105,20 @@ type FusionStrategy = {
105
105
  *
106
106
  * @stable
107
107
  */
108
+ /**
109
+ * Search options an operator may default at construction time via
110
+ * `createMemory({ searchDefaults })` (W-086) - the advanced-retrieval
111
+ * switches (fan-out, HyDE, graph expansion, fusion, decay) that the
112
+ * model-facing surfaces (`fact_search`, auto-recall, `deep_recall`)
113
+ * cannot reach per-call. Deliberately a `Pick` that EXCLUDES the
114
+ * trust-sensitive predicates (`includeQuarantined`, `includeSuperseded`,
115
+ * `trustWeighting`, `owner`): configuration must not be able to silently
116
+ * weaken trust gates for every caller. Per-call options always win
117
+ * key-by-key.
118
+ *
119
+ * @stable
120
+ */
121
+ type SemanticSearchDefaults = Pick<FactSearchOptions, 'multiQuery' | 'hyde' | 'expandHops' | 'entityMatch' | 'graphScoring' | 'fusion' | 'decay' | 'candidateTopK'>;
108
122
  interface FactSearchOptions {
109
123
  readonly topK?: number;
110
124
  readonly signal?: AbortSignal;
@@ -287,6 +301,14 @@ interface IterativeSearchOptions extends FactSearchOptions {
287
301
  * requests and tests.
288
302
  */
289
303
  readonly forceHard?: boolean;
304
+ /**
305
+ * Difficulty-gate threshold in `[0, 1]` for THIS call (W-088). The
306
+ * gate's signal lexicon is **English-only** - for non-English
307
+ * deployments lower the threshold or use {@link forceHard}. Omitted ⇒
308
+ * the facade default (`iterativeRetrieval.difficultyThreshold`) or the
309
+ * built-in `0.5`.
310
+ */
311
+ readonly difficultyThreshold?: number;
290
312
  }
291
313
  /**
292
314
  * Outcome of {@link SemanticMemory.searchIterative}. Beyond the ranked
@@ -407,12 +429,27 @@ declare class SemanticMemory {
407
429
  grader?: RetrievalGrader;
408
430
  /** Default total-pass cap for `searchIterative`. Default 3. */
409
431
  iterativeMaxIterations?: number;
432
+ /**
433
+ * Default difficulty-gate threshold for `searchIterative` (W-088).
434
+ * Omitted ⇒ the gate's built-in `0.5`. Per-call
435
+ * `difficultyThreshold` overrides it.
436
+ */
437
+ iterativeDifficultyThreshold?: number;
410
438
  /**
411
439
  * Weights for the rank-time trust discount (C5). Reuses the
412
440
  * eviction-path `SalienceWeights` semantics; defaults to
413
441
  * `DEFAULT_SALIENCE_WEIGHTS`.
414
442
  */
415
443
  trustWeights?: SalienceWeights;
444
+ /**
445
+ * Construction-time retrieval defaults (W-086) merged under every
446
+ * `search(...)` call - see {@link SemanticSearchDefaults}. Because
447
+ * the merge happens inside `search()`, the model-facing surfaces
448
+ * (`fact_search`, auto-recall, `deep_recall`) inherit them without
449
+ * any per-surface wiring; per-call options override key-by-key (so
450
+ * e.g. `deep_recall`'s widen-pass `expandHops` still wins).
451
+ */
452
+ searchDefaults?: SemanticSearchDefaults;
416
453
  });
417
454
  /** Replace the active reranker. Returns the previous instance. */
418
455
  setReranker(reranker: ReRanker): ReRanker;
@@ -435,7 +472,7 @@ declare class SemanticMemory {
435
472
  */
436
473
  rememberWithDecision(scope: SessionScope, input: FactInput, options?: FactRememberOptions): Promise<RememberOutcome>;
437
474
  /** Hybrid (vector + FTS5) search merged through the configured reranker. */
438
- search(scope: SessionScope, query: string, opts?: FactSearchOptions): Promise<ReadonlyArray<MemoryHit<Fact>>>;
475
+ search(scope: SessionScope, query: string, callOpts?: FactSearchOptions): Promise<ReadonlyArray<MemoryHit<Fact>>>;
439
476
  /**
440
477
  * Gated, iterative ("deep") recall for hard queries (P2-4). A cheap
441
478
  * local heuristic ({@link assessQueryDifficulty}) decides whether the
@@ -507,8 +544,25 @@ declare class SemanticMemory {
507
544
  validate(scope: SessionScope, factId: string, reason?: string, options?: {
508
545
  readonly force?: boolean;
509
546
  }): Promise<void>;
510
- /** Mark `oldId` superseded by a new fact. Returns the new record. */
511
- supersede(scope: SessionScope, oldId: string, newInput: FactInput, reason?: string): Promise<{
547
+ /**
548
+ * Mark `oldId` superseded by a new fact. Returns the new record.
549
+ *
550
+ * W-019 (security-first, knowledge-preserving): when the successor
551
+ * lands QUARANTINED (the default for `extraction`/synthesized
552
+ * provenance), the old ACTIVE fact's validity interval is NOT closed
553
+ * - default recall keeps returning the old knowledge until the
554
+ * successor passes {@link validate}, which completes the closure.
555
+ * The link is recorded on the successor's `supersedes` field. With
556
+ * `autoPromoteSynthesized` (threaded from the consolidator's
557
+ * `autoPromoteExtraction` escape hatch) an injection-clean successor
558
+ * is active immediately and the interval closes right away - the
559
+ * pre-W-019 behaviour. Inverting the default (auto-activating the
560
+ * successor) would hand a MINJA attacker instant active memory via
561
+ * any text the reconciler classifies as an 'update'.
562
+ */
563
+ supersede(scope: SessionScope, oldId: string, newInput: FactInput, reason?: string, options?: {
564
+ readonly autoPromoteSynthesized?: boolean;
565
+ }): Promise<{
512
566
  readonly old: string;
513
567
  readonly new: Fact;
514
568
  }>;
@@ -538,5 +592,5 @@ declare class SemanticMemory {
538
592
  static fuseWeighted<TRecord extends Fact>(lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>, weights: ReadonlyArray<number> | undefined, k?: number): ReadonlyArray<MemoryHit<TRecord>>;
539
593
  }
540
594
  //#endregion
541
- export { FactInput, FactRememberOptions, FactSearchOptions, FusionStrategy, FusionWeights, IterativeRecallResult, IterativeSearchOptions, RememberOutcome, SemanticMemory };
595
+ export { FactInput, FactRememberOptions, FactSearchOptions, FusionStrategy, FusionWeights, IterativeRecallResult, IterativeSearchOptions, RememberOutcome, SemanticMemory, SemanticSearchDefaults };
542
596
  //# sourceMappingURL=semantic-memory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-memory.d.ts","names":[],"sources":["../../src/tiers/semantic-memory.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"semantic-memory.d.ts","names":[],"sources":["../../src/tiers/semantic-memory.ts"],"sourcesContent":[],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { withMemorySpan } from "../internal/spans.js";
2
2
  import { DEFAULT_SALIENCE_WEIGHTS } from "../consolidator/decay.js";
3
3
  import { QuarantinePromotionRefusedError } from "../errors/index.js";
4
+ import { detectMemoryInjection } from "../internal/injection-heuristics.js";
4
5
  import { contextualize } from "../internal/contextualize.js";
5
6
  import { newMemoryId } from "../internal/id.js";
6
7
  import { normalizeEntityName } from "../graph/entity-resolver.js";
7
8
  import { DEFAULT_MAX_ITERATIONS, runIterativeRetrieval } from "../search/iterative.js";
8
9
  import { WeightedRRFReranker, fuseRrf, fuseWeighted } from "../search/rrf.js";
9
- import { detectMemoryInjection } from "../internal/injection-heuristics.js";
10
10
  import { explainRecall } from "../search/explain.js";
11
11
  import { pprActivation } from "../search/graph-ppr.js";
12
12
  import { trustDiscount } from "../search/trust.js";
@@ -37,8 +37,10 @@ var SemanticMemory = class {
37
37
  #entityResolver;
38
38
  #grader;
39
39
  #iterativeMaxIterations;
40
+ #iterativeDifficultyThreshold;
40
41
  #reranker;
41
42
  #trustWeights;
43
+ #searchDefaults;
42
44
  constructor(args) {
43
45
  this.#store = args.store;
44
46
  this.#tracer = args.tracer;
@@ -51,7 +53,9 @@ var SemanticMemory = class {
51
53
  this.#entityResolver = args.entityResolver ?? null;
52
54
  this.#grader = args.grader ?? null;
53
55
  this.#iterativeMaxIterations = args.iterativeMaxIterations ?? DEFAULT_MAX_ITERATIONS;
56
+ this.#iterativeDifficultyThreshold = args.iterativeDifficultyThreshold;
54
57
  this.#trustWeights = args.trustWeights ?? DEFAULT_SALIENCE_WEIGHTS;
58
+ this.#searchDefaults = args.searchDefaults ?? {};
55
59
  }
56
60
  /** Replace the active reranker. Returns the previous instance. */
57
61
  setReranker(reranker) {
@@ -230,7 +234,11 @@ var SemanticMemory = class {
230
234
  } catch {}
231
235
  }
232
236
  /** Hybrid (vector + FTS5) search merged through the configured reranker. */
233
- async search(scope, query, opts = {}) {
237
+ async search(scope, query, callOpts = {}) {
238
+ const opts = {
239
+ ...this.#searchDefaults,
240
+ ...callOpts
241
+ };
234
242
  return withMemorySpan(this.#tracer, "memory.search.semantic", scope, { "memory.search.query_length": query.length }, async (span) => {
235
243
  const candidateTopK = opts.candidateTopK ?? 60;
236
244
  const finalTopK = opts.topK ?? 10;
@@ -240,6 +248,8 @@ var SemanticMemory = class {
240
248
  const wGraph = weighted?.weights.graph ?? 1;
241
249
  const wEntity = weighted?.weights.entity ?? 1;
242
250
  const queries = await this.#expandQueries(query, opts);
251
+ const variantVectors = await this.#tryBatchEmbedQueries(queries);
252
+ const hydeHitsPromise = this.#tryHyde(scope, query, opts, candidateTopK);
243
253
  const lists = [];
244
254
  const listWeights = [];
245
255
  const listLabels = [];
@@ -260,7 +270,8 @@ var SemanticMemory = class {
260
270
  lists.push(ftsHits);
261
271
  listWeights.push(wFts);
262
272
  listLabels.push(`fts_${queries.indexOf(q)}`);
263
- const vectorHits = await this.#tryVectorSearch(scope, q, candidateTopK, opts.asOf, opts.includeQuarantined, opts.includeSuperseded, opts.owner);
273
+ const preVector = variantVectors?.get(q);
274
+ const vectorHits = preVector !== void 0 ? await this.#vectorSearchWithVector(scope, preVector, candidateTopK, opts.asOf, opts.includeQuarantined, opts.includeSuperseded, opts.owner) : await this.#tryVectorSearch(scope, q, candidateTopK, opts.asOf, opts.includeQuarantined, opts.includeSuperseded, opts.owner);
264
275
  if (vectorHits.length > 0) {
265
276
  lists.push(vectorHits);
266
277
  listWeights.push(wVector);
@@ -272,7 +283,7 @@ var SemanticMemory = class {
272
283
  isPrimary = false;
273
284
  }
274
285
  }
275
- const hydeHits = await this.#tryHyde(scope, query, opts, candidateTopK);
286
+ const hydeHits = await hydeHitsPromise;
276
287
  if (hydeHits.length > 0) {
277
288
  lists.push(hydeHits);
278
289
  listWeights.push(wVector);
@@ -294,7 +305,7 @@ var SemanticMemory = class {
294
305
  weights: listWeights,
295
306
  ...weighted.k !== void 0 ? { k: weighted.k } : {}
296
307
  }) : this.#reranker;
297
- const fusedTopK = opts.decay !== void 0 || (opts.tags?.length ?? 0) > 0 || opts.owner !== void 0 || (opts.expandHops ?? 0) > 0 || opts.entityMatch === true ? Math.max(finalTopK, lists.reduce((n, l) => n + l.length, 0)) : finalTopK;
308
+ const fusedTopK = opts.decay !== void 0 || (opts.tags?.length ?? 0) > 0 || opts.owner !== void 0 || (opts.expandHops ?? 0) > 0 || opts.entityMatch === true || opts.trustWeighting !== "off" || opts.includeQuarantined === true ? Math.max(finalTopK, lists.reduce((n, l) => n + l.length, 0)) : finalTopK;
298
309
  const fused = await reranker.rerank(query, lists, {
299
310
  topK: fusedTopK,
300
311
  labels: listLabels,
@@ -310,7 +321,7 @@ var SemanticMemory = class {
310
321
  const ownerFiltered = wantedOwners !== null ? filtered.filter((h) => wantedOwners.includes(h.record.owner ?? "user")) : filtered;
311
322
  const ranked = (opts.trustWeighting === "off" ? ownerFiltered : this.#applyTrustDiscount(ownerFiltered)).slice(0, finalTopK);
312
323
  if (ranked.length > 0 && typeof this.#store.semantic.markAccessed === "function") try {
313
- await this.#store.semantic.markAccessed(ranked.map((h) => h.record.id));
324
+ await this.#store.semantic.markAccessed(ranked.map((h) => h.record.id), void 0, scope);
314
325
  } catch {}
315
326
  const explanation = explainRecall(ranked, {
316
327
  query,
@@ -364,12 +375,16 @@ var SemanticMemory = class {
364
375
  idOf: (hit) => hit.record.id,
365
376
  grader: this.#grader,
366
377
  fuse: (lists) => fuseRrf(lists, 60)
367
- }, {
368
- maxIterations: opts.maxIterations ?? this.#iterativeMaxIterations,
369
- maxResults: opts.topK ?? 10,
370
- ...opts.forceHard !== void 0 ? { forceHard: opts.forceHard } : {},
371
- ...opts.signal !== void 0 ? { signal: opts.signal } : {}
372
- });
378
+ }, (() => {
379
+ const difficultyThreshold = opts.difficultyThreshold ?? this.#iterativeDifficultyThreshold;
380
+ return {
381
+ maxIterations: opts.maxIterations ?? this.#iterativeMaxIterations,
382
+ maxResults: opts.topK ?? 10,
383
+ ...opts.forceHard !== void 0 ? { forceHard: opts.forceHard } : {},
384
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {},
385
+ ...difficultyThreshold !== void 0 ? { difficulty: { threshold: difficultyThreshold } } : {}
386
+ };
387
+ })());
373
388
  span.setAttributes({
374
389
  "memory.search.semantic.iterative.gate_hard": result.gateHard,
375
390
  "memory.search.semantic.iterative.iterations": result.iterations,
@@ -465,16 +480,51 @@ var SemanticMemory = class {
465
480
  }
466
481
  }
467
482
  span.setAttributes({ "memory.semantic.validate.forced": force });
468
- await this.#store.semantic.setStatus(factId, "active", reason);
483
+ await this.#store.semantic.setStatus(factId, "active", reason, scope);
484
+ if (existing !== null && existing.supersedes !== void 0) {
485
+ const oldFact = await this.#fetchExisting(existing.supersedes);
486
+ if (oldFact !== null && oldFact.supersededBy === void 0 && oldFact.validTo === void 0) {
487
+ span.setAttributes({ "memory.semantic.validate.completed_supersede": true });
488
+ await this.#store.semantic.supersede(existing.supersedes, {
489
+ ...existing,
490
+ status: "active"
491
+ }, reason ?? "validated-supersede");
492
+ }
493
+ }
469
494
  });
470
495
  }
471
- /** Mark `oldId` superseded by a new fact. Returns the new record. */
472
- async supersede(scope, oldId, newInput, reason) {
473
- return withMemorySpan(this.#tracer, "memory.write.semantic", scope, { "memory.semantic.action": "supersede" }, async () => {
496
+ /**
497
+ * Mark `oldId` superseded by a new fact. Returns the new record.
498
+ *
499
+ * W-019 (security-first, knowledge-preserving): when the successor
500
+ * lands QUARANTINED (the default for `extraction`/synthesized
501
+ * provenance), the old ACTIVE fact's validity interval is NOT closed
502
+ * - default recall keeps returning the old knowledge until the
503
+ * successor passes {@link validate}, which completes the closure.
504
+ * The link is recorded on the successor's `supersedes` field. With
505
+ * `autoPromoteSynthesized` (threaded from the consolidator's
506
+ * `autoPromoteExtraction` escape hatch) an injection-clean successor
507
+ * is active immediately and the interval closes right away - the
508
+ * pre-W-019 behaviour. Inverting the default (auto-activating the
509
+ * successor) would hand a MINJA attacker instant active memory via
510
+ * any text the reconciler classifies as an 'update'.
511
+ */
512
+ async supersede(scope, oldId, newInput, reason, options) {
513
+ return withMemorySpan(this.#tracer, "memory.write.semantic", scope, { "memory.semantic.action": "supersede" }, async (span) => {
474
514
  const newFact = await this.remember(scope, {
475
515
  ...newInput,
476
516
  supersedes: oldId
477
- }, { pipeline: "off" });
517
+ }, {
518
+ pipeline: "off",
519
+ ...options?.autoPromoteSynthesized === true ? { autoPromoteSynthesized: true } : {}
520
+ });
521
+ if (newFact.status === "quarantined") {
522
+ span.setAttributes({ "memory.semantic.supersede.pending": true });
523
+ return {
524
+ old: oldId,
525
+ new: newFact
526
+ };
527
+ }
478
528
  await this.#store.semantic.supersede(oldId, newFact, reason);
479
529
  return {
480
530
  old: oldId,
@@ -488,7 +538,7 @@ var SemanticMemory = class {
488
538
  "memory.semantic.action": "forget",
489
539
  "memory.semantic.fact_id": factId
490
540
  }, async () => {
491
- await this.#store.semantic.forget(factId, reason);
541
+ await this.#store.semantic.forget(factId, reason, scope);
492
542
  });
493
543
  }
494
544
  /**
@@ -504,7 +554,7 @@ var SemanticMemory = class {
504
554
  "memory.semantic.fact_id": factId
505
555
  }, async () => {
506
556
  if (typeof this.#store.semantic.purge !== "function") throw new TypeError("[graphorin/memory] SemanticMemory.purge(...) requires a storage adapter that implements `semantic.purge(id)`. For storage adapters without GDPR-grade hard-delete, prefer `SemanticMemory.forget(...)`.");
507
- await this.#store.semantic.purge(factId, reason);
557
+ await this.#store.semantic.purge(factId, reason, scope);
508
558
  });
509
559
  }
510
560
  /** Fuse multiple ranked lists outside of a `search()` call. */
@@ -565,7 +615,7 @@ var SemanticMemory = class {
565
615
  try {
566
616
  const pseudo = await this.#queryTransformer.hypothetical(query, { ...opts.signal !== void 0 ? { signal: opts.signal } : {} });
567
617
  if (pseudo === null || pseudo.trim().length === 0) return [];
568
- return this.#tryVectorSearch(scope, pseudo, topK, opts.asOf, opts.includeQuarantined);
618
+ return this.#tryVectorSearch(scope, pseudo, topK, opts.asOf, opts.includeQuarantined, opts.includeSuperseded, opts.owner);
569
619
  } catch {
570
620
  return [];
571
621
  }
@@ -661,9 +711,42 @@ var SemanticMemory = class {
661
711
  if (this.#embedder === null || embedderId === null || typeof this.#store.semantic.searchVector !== "function") return [];
662
712
  const [vector] = await this.#embedder.embed([query], { taskType: "query" });
663
713
  if (vector === void 0) return [];
714
+ return this.#vectorSearchWithVector(scope, vector, topK, asOf, includeQuarantined, includeSuperseded, owner);
715
+ }
716
+ /**
717
+ * W-087: the store half of {@link SemanticMemory.#tryVectorSearch} -
718
+ * runs the KNN read against an ALREADY-COMPUTED query vector so the
719
+ * multi-query fan-out can batch its N variant embeddings into one
720
+ * `embed([q1..qN])` call instead of N provider round-trips.
721
+ */
722
+ async #vectorSearchWithVector(scope, vector, topK, asOf, includeQuarantined, includeSuperseded, owner) {
723
+ const embedderId = this.#embedderIdProvider();
724
+ if (embedderId === null || typeof this.#store.semantic.searchVector !== "function") return [];
664
725
  return this.#store.semantic.searchVector(scope, vector, embedderId, topK, asOf, includeQuarantined, includeSuperseded, owner);
665
726
  }
666
727
  /**
728
+ * W-087: embed every fan-out variant in ONE embedder call. Returns
729
+ * `null` - and the caller degrades to the per-variant path - when
730
+ * there is no fan-out, no vector surface, or the batch call fails; a
731
+ * batching failure must never fail the search.
732
+ */
733
+ async #tryBatchEmbedQueries(queries) {
734
+ if (queries.length <= 1) return null;
735
+ if (this.#embedder === null || this.#embedderIdProvider() === null || typeof this.#store.semantic.searchVector !== "function") return null;
736
+ try {
737
+ const vectors = await this.#embedder.embed([...queries], { taskType: "query" });
738
+ if (vectors.length !== queries.length) return null;
739
+ const byQuery = /* @__PURE__ */ new Map();
740
+ queries.forEach((q, i) => {
741
+ const v = vectors[i];
742
+ if (v !== void 0) byQuery.set(q, v);
743
+ });
744
+ return byQuery;
745
+ } catch {
746
+ return null;
747
+ }
748
+ }
749
+ /**
667
750
  * C5: multiply each hit's fused score by its trust factor and re-sort.
668
751
  * No-ops (returns the same array) when every factor is 1.
669
752
  */