@graphorin/memory 0.5.0 → 0.6.1

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 (236) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/README.md +54 -37
  3. package/dist/conflict/index.d.ts +3 -1
  4. package/dist/conflict/index.js +1 -1
  5. package/dist/conflict/locale-packs/en.d.ts +13 -0
  6. package/dist/conflict/locale-packs/en.d.ts.map +1 -0
  7. package/dist/conflict/locale-packs/en.js +1 -1
  8. package/dist/conflict/locale-packs/en.js.map +1 -1
  9. package/dist/conflict/pipeline.d.ts +2 -2
  10. package/dist/conflict/pipeline.js +3 -3
  11. package/dist/conflict/pipeline.js.map +1 -1
  12. package/dist/conflict/stages/helpers.js +1 -1
  13. package/dist/conflict/stages/helpers.js.map +1 -1
  14. package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
  15. package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
  16. package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
  17. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  18. package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
  19. package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
  20. package/dist/conflict/types.d.ts +7 -7
  21. package/dist/conflict/types.js +1 -1
  22. package/dist/conflict/types.js.map +1 -1
  23. package/dist/consolidator/budget.js +16 -3
  24. package/dist/consolidator/budget.js.map +1 -1
  25. package/dist/consolidator/decay.d.ts +13 -2
  26. package/dist/consolidator/decay.js +17 -5
  27. package/dist/consolidator/decay.js.map +1 -1
  28. package/dist/consolidator/dlq.js +1 -1
  29. package/dist/consolidator/dlq.js.map +1 -1
  30. package/dist/consolidator/errors.js +2 -2
  31. package/dist/consolidator/errors.js.map +1 -1
  32. package/dist/consolidator/index.js +8 -6
  33. package/dist/consolidator/index.js.map +1 -1
  34. package/dist/consolidator/lock.js +1 -1
  35. package/dist/consolidator/lock.js.map +1 -1
  36. package/dist/consolidator/noise-filter.js.map +1 -1
  37. package/dist/consolidator/phases/deep.js +6 -2
  38. package/dist/consolidator/phases/deep.js.map +1 -1
  39. package/dist/consolidator/phases/induce.d.ts +10 -10
  40. package/dist/consolidator/phases/induce.js +6 -6
  41. package/dist/consolidator/phases/induce.js.map +1 -1
  42. package/dist/consolidator/phases/learned-context.js +129 -0
  43. package/dist/consolidator/phases/learned-context.js.map +1 -0
  44. package/dist/consolidator/phases/light.js +2 -1
  45. package/dist/consolidator/phases/light.js.map +1 -1
  46. package/dist/consolidator/phases/reflect.js +5 -4
  47. package/dist/consolidator/phases/reflect.js.map +1 -1
  48. package/dist/consolidator/phases/standard.js +37 -8
  49. package/dist/consolidator/phases/standard.js.map +1 -1
  50. package/dist/consolidator/reconcile.js +1 -1
  51. package/dist/consolidator/reconcile.js.map +1 -1
  52. package/dist/consolidator/runtime.d.ts +4 -4
  53. package/dist/consolidator/runtime.d.ts.map +1 -1
  54. package/dist/consolidator/runtime.js +73 -16
  55. package/dist/consolidator/runtime.js.map +1 -1
  56. package/dist/consolidator/scheduler.d.ts +5 -5
  57. package/dist/consolidator/scheduler.js +1 -1
  58. package/dist/consolidator/scheduler.js.map +1 -1
  59. package/dist/consolidator/triggers.js +7 -7
  60. package/dist/consolidator/triggers.js.map +1 -1
  61. package/dist/consolidator/types.d.ts +68 -20
  62. package/dist/consolidator/types.d.ts.map +1 -1
  63. package/dist/consolidator/types.js +16 -6
  64. package/dist/consolidator/types.js.map +1 -1
  65. package/dist/context-engine/annotations.d.ts +9 -9
  66. package/dist/context-engine/annotations.js +3 -3
  67. package/dist/context-engine/annotations.js.map +1 -1
  68. package/dist/context-engine/auto-recall.js.map +1 -1
  69. package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
  70. package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
  71. package/dist/context-engine/compaction/clear-tool-results.js +26 -5
  72. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  73. package/dist/context-engine/compaction/compactor.js +19 -6
  74. package/dist/context-engine/compaction/compactor.js.map +1 -1
  75. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
  76. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
  77. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
  78. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
  79. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
  80. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
  81. package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
  82. package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
  83. package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
  84. package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
  85. package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
  86. package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
  87. package/dist/context-engine/compaction/index.d.ts +1 -0
  88. package/dist/context-engine/compaction/index.js +2 -1
  89. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
  90. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  91. package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
  92. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  93. package/dist/context-engine/compaction/thresholds.js.map +1 -1
  94. package/dist/context-engine/compaction/types.d.ts +47 -7
  95. package/dist/context-engine/compaction/types.d.ts.map +1 -1
  96. package/dist/context-engine/engine.d.ts +23 -4
  97. package/dist/context-engine/engine.d.ts.map +1 -1
  98. package/dist/context-engine/engine.js +69 -15
  99. package/dist/context-engine/engine.js.map +1 -1
  100. package/dist/context-engine/index.d.ts +1 -0
  101. package/dist/context-engine/index.d.ts.map +1 -1
  102. package/dist/context-engine/index.js +2 -1
  103. package/dist/context-engine/locale-packs/en.js +14 -13
  104. package/dist/context-engine/locale-packs/en.js.map +1 -1
  105. package/dist/context-engine/locale-packs/types.d.ts +10 -10
  106. package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
  107. package/dist/context-engine/locale-packs/types.js.map +1 -1
  108. package/dist/context-engine/metadata.d.ts +1 -1
  109. package/dist/context-engine/metadata.js +2 -2
  110. package/dist/context-engine/metadata.js.map +1 -1
  111. package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
  112. package/dist/context-engine/preambles/inbound-en.js +1 -1
  113. package/dist/context-engine/preambles/inbound-en.js.map +1 -1
  114. package/dist/context-engine/privacy-filter.d.ts +2 -2
  115. package/dist/context-engine/privacy-filter.js +1 -1
  116. package/dist/context-engine/privacy-filter.js.map +1 -1
  117. package/dist/context-engine/templates/base-en.d.ts +1 -1
  118. package/dist/context-engine/templates/base-en.js +1 -1
  119. package/dist/context-engine/templates/base-en.js.map +1 -1
  120. package/dist/context-engine/templates/composer.d.ts +1 -1
  121. package/dist/context-engine/templates/composer.js.map +1 -1
  122. package/dist/context-engine/token-budget.d.ts +1 -1
  123. package/dist/context-engine/token-budget.js +1 -1
  124. package/dist/context-engine/token-budget.js.map +1 -1
  125. package/dist/context-engine/token-counter.d.ts +16 -7
  126. package/dist/context-engine/token-counter.d.ts.map +1 -1
  127. package/dist/context-engine/token-counter.js +31 -9
  128. package/dist/context-engine/token-counter.js.map +1 -1
  129. package/dist/errors/index.d.ts +2 -2
  130. package/dist/errors/index.js +4 -4
  131. package/dist/errors/index.js.map +1 -1
  132. package/dist/facade.d.ts +44 -15
  133. package/dist/facade.d.ts.map +1 -1
  134. package/dist/facade.js +16 -8
  135. package/dist/facade.js.map +1 -1
  136. package/dist/graph/entity-resolver.d.ts +2 -2
  137. package/dist/graph/entity-resolver.js +2 -2
  138. package/dist/graph/entity-resolver.js.map +1 -1
  139. package/dist/index.d.ts +9 -6
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +11 -5
  142. package/dist/index.js.map +1 -1
  143. package/dist/internal/contextualize.js +2 -2
  144. package/dist/internal/contextualize.js.map +1 -1
  145. package/dist/internal/embedder-binding.js +1 -1
  146. package/dist/internal/embedder-binding.js.map +1 -1
  147. package/dist/internal/id.js +1 -1
  148. package/dist/internal/id.js.map +1 -1
  149. package/dist/internal/injection-heuristics.js +25 -2
  150. package/dist/internal/injection-heuristics.js.map +1 -1
  151. package/dist/internal/spans.js +1 -1
  152. package/dist/internal/spans.js.map +1 -1
  153. package/dist/internal/storage-adapter.d.ts +71 -22
  154. package/dist/internal/storage-adapter.d.ts.map +1 -1
  155. package/dist/migration/embedder-migration.d.ts +7 -7
  156. package/dist/migration/embedder-migration.js.map +1 -1
  157. package/dist/package.js +6 -0
  158. package/dist/package.js.map +1 -0
  159. package/dist/search/explain.d.ts +7 -4
  160. package/dist/search/explain.d.ts.map +1 -1
  161. package/dist/search/explain.js +1 -1
  162. package/dist/search/explain.js.map +1 -1
  163. package/dist/search/fit-weights.d.ts +48 -0
  164. package/dist/search/fit-weights.d.ts.map +1 -0
  165. package/dist/search/fit-weights.js +70 -0
  166. package/dist/search/fit-weights.js.map +1 -0
  167. package/dist/search/graph-ppr.d.ts +36 -0
  168. package/dist/search/graph-ppr.d.ts.map +1 -0
  169. package/dist/search/graph-ppr.js +38 -0
  170. package/dist/search/graph-ppr.js.map +1 -0
  171. package/dist/search/index.d.ts +4 -1
  172. package/dist/search/index.js +4 -1
  173. package/dist/search/iterative.d.ts +15 -8
  174. package/dist/search/iterative.d.ts.map +1 -1
  175. package/dist/search/iterative.js +10 -9
  176. package/dist/search/iterative.js.map +1 -1
  177. package/dist/search/query-transform.d.ts +6 -6
  178. package/dist/search/query-transform.js +6 -6
  179. package/dist/search/query-transform.js.map +1 -1
  180. package/dist/search/rrf.d.ts +4 -4
  181. package/dist/search/rrf.js +4 -4
  182. package/dist/search/rrf.js.map +1 -1
  183. package/dist/search/trust.d.ts +30 -0
  184. package/dist/search/trust.d.ts.map +1 -0
  185. package/dist/search/trust.js +50 -0
  186. package/dist/search/trust.js.map +1 -0
  187. package/dist/search/types.d.ts +1 -1
  188. package/dist/tiers/episodic-memory.d.ts +6 -4
  189. package/dist/tiers/episodic-memory.d.ts.map +1 -1
  190. package/dist/tiers/episodic-memory.js +3 -2
  191. package/dist/tiers/episodic-memory.js.map +1 -1
  192. package/dist/tiers/index.d.ts +1 -1
  193. package/dist/tiers/index.js +1 -1
  194. package/dist/tiers/insight-memory.d.ts +4 -4
  195. package/dist/tiers/insight-memory.js +3 -3
  196. package/dist/tiers/insight-memory.js.map +1 -1
  197. package/dist/tiers/procedural-memory.d.ts +29 -7
  198. package/dist/tiers/procedural-memory.d.ts.map +1 -1
  199. package/dist/tiers/procedural-memory.js +66 -6
  200. package/dist/tiers/procedural-memory.js.map +1 -1
  201. package/dist/tiers/semantic-memory.d.ts +111 -30
  202. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  203. package/dist/tiers/semantic-memory.js +120 -27
  204. package/dist/tiers/semantic-memory.js.map +1 -1
  205. package/dist/tiers/session-memory.d.ts +7 -7
  206. package/dist/tiers/session-memory.js +7 -7
  207. package/dist/tiers/session-memory.js.map +1 -1
  208. package/dist/tiers/shared-memory.d.ts +1 -1
  209. package/dist/tiers/shared-memory.js +1 -1
  210. package/dist/tiers/shared-memory.js.map +1 -1
  211. package/dist/tiers/working-memory.d.ts +3 -3
  212. package/dist/tiers/working-memory.js +2 -2
  213. package/dist/tiers/working-memory.js.map +1 -1
  214. package/dist/tools/block-tools.d.ts +3 -3
  215. package/dist/tools/block-tools.js +3 -3
  216. package/dist/tools/block-tools.js.map +1 -1
  217. package/dist/tools/fact-tools.d.ts +48 -31
  218. package/dist/tools/fact-tools.d.ts.map +1 -1
  219. package/dist/tools/fact-tools.js +30 -18
  220. package/dist/tools/fact-tools.js.map +1 -1
  221. package/dist/tools/index.d.ts +9 -2
  222. package/dist/tools/index.d.ts.map +1 -1
  223. package/dist/tools/index.js +4 -2
  224. package/dist/tools/index.js.map +1 -1
  225. package/dist/tools/recall-tools.d.ts +40 -20
  226. package/dist/tools/recall-tools.d.ts.map +1 -1
  227. package/dist/tools/recall-tools.js +29 -10
  228. package/dist/tools/recall-tools.js.map +1 -1
  229. package/dist/tools/runbook-tools.d.ts +76 -0
  230. package/dist/tools/runbook-tools.d.ts.map +1 -0
  231. package/dist/tools/runbook-tools.js +53 -0
  232. package/dist/tools/runbook-tools.js.map +1 -0
  233. package/dist/tools/taint.js +33 -0
  234. package/dist/tools/taint.js.map +1 -0
  235. package/dist/tools/types.d.ts +1 -1
  236. package/package.json +8 -8
@@ -31,7 +31,7 @@ function normaliseSubject(input, pack) {
31
31
  }
32
32
  /**
33
33
  * Naive subject / predicate / object split. The locale pack supplies
34
- * the verb tokens used as predicate normalisers the first matching
34
+ * the verb tokens used as predicate normalisers - the first matching
35
35
  * verb in the sentence becomes the predicate and splits the rest
36
36
  * into subject (left side) and object (right side). The split is
37
37
  * intentionally simple: anything more clever lives behind the entity-
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","names":[],"sources":["../../../src/conflict/stages/helpers.ts"],"sourcesContent":["/**\n * Internal helpers shared across the five pipeline stages.\n *\n * @packageDocumentation\n * @internal\n */\n\nimport { md5 } from '@graphorin/core';\nimport type { LocalePack } from '../locale-packs/index.js';\n\n/**\n * Lowercase + collapse whitespace + trim. Used by Stage 1 (exact\n * dedup) to compute the canonical fact body that feeds the MD5\n * comparison.\n */\nexport function canonicaliseFactBody(text: string): string {\n return text.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\n/**\n * Stable hash for a canonical fact body. We re-use the framework's\n * MD5 helper to stay consistent with the storage layer's existing\n * dedup hash column (see `facts.hash`).\n */\nexport function fastFactHash(text: string): string {\n return md5(text);\n}\n\n/** Strip leading articles + trim a candidate subject token. */\nexport function normaliseSubject(input: string, pack: LocalePack): string {\n const tokens = canonicaliseFactBody(input).split(' ');\n const stop = new Set(pack.subjectStopWords.map((s) => s.toLowerCase()));\n return tokens.filter((token) => token.length > 0 && !stop.has(token)).join(' ');\n}\n\n/**\n * Naive subject / predicate / object split. The locale pack supplies\n * the verb tokens used as predicate normalisers the first matching\n * verb in the sentence becomes the predicate and splits the rest\n * into subject (left side) and object (right side). The split is\n * intentionally simple: anything more clever lives behind the entity-\n * linking layer (post-MVP).\n */\nexport function splitSubjectPredicateObject(\n text: string,\n pack: LocalePack,\n): { readonly subject: string; readonly predicate: string; readonly object: string } | null {\n const canonical = canonicaliseFactBody(text);\n if (canonical.length === 0) return null;\n const tokens = canonical.split(' ');\n const verbs = new Set(pack.predicateNormalisers.map((v) => v.toLowerCase()));\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token === undefined) continue;\n const stripped = token.replace(/[.,!?;:]/g, '');\n if (verbs.has(stripped)) {\n const subject = normaliseSubject(tokens.slice(0, i).join(' '), pack);\n const predicate = stripped;\n const object = canonicaliseFactBody(tokens.slice(i + 1).join(' '));\n if (subject.length === 0 || object.length === 0) continue;\n return { subject, predicate, object };\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,qBAAqB,MAAsB;AACzD,QAAO,KAAK,QAAQ,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa;;;;;;;AAQvD,SAAgB,aAAa,MAAsB;AACjD,QAAO,IAAI,KAAK;;;AAIlB,SAAgB,iBAAiB,OAAe,MAA0B;CACxE,MAAM,SAAS,qBAAqB,MAAM,CAAC,MAAM,IAAI;CACrD,MAAM,OAAO,IAAI,IAAI,KAAK,iBAAiB,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;AACvE,QAAO,OAAO,QAAQ,UAAU,MAAM,SAAS,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;AAWjF,SAAgB,4BACd,MACA,MAC0F;CAC1F,MAAM,YAAY,qBAAqB,KAAK;AAC5C,KAAI,UAAU,WAAW,EAAG,QAAO;CACnC,MAAM,SAAS,UAAU,MAAM,IAAI;CACnC,MAAM,QAAQ,IAAI,IAAI,KAAK,qBAAqB,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;AAC5E,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,QAAQ,OAAO;AACrB,MAAI,UAAU,OAAW;EACzB,MAAM,WAAW,MAAM,QAAQ,aAAa,GAAG;AAC/C,MAAI,MAAM,IAAI,SAAS,EAAE;GACvB,MAAM,UAAU,iBAAiB,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,KAAK;GACpE,MAAM,YAAY;GAClB,MAAM,SAAS,qBAAqB,OAAO,MAAM,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAClE,OAAI,QAAQ,WAAW,KAAK,OAAO,WAAW,EAAG;AACjD,UAAO;IAAE;IAAS;IAAW;IAAQ;;;AAGzC,QAAO"}
1
+ {"version":3,"file":"helpers.js","names":[],"sources":["../../../src/conflict/stages/helpers.ts"],"sourcesContent":["/**\n * Internal helpers shared across the five pipeline stages.\n *\n * @packageDocumentation\n * @internal\n */\n\nimport { md5 } from '@graphorin/core';\nimport type { LocalePack } from '../locale-packs/index.js';\n\n/**\n * Lowercase + collapse whitespace + trim. Used by Stage 1 (exact\n * dedup) to compute the canonical fact body that feeds the MD5\n * comparison.\n */\nexport function canonicaliseFactBody(text: string): string {\n return text.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\n/**\n * Stable hash for a canonical fact body. We re-use the framework's\n * MD5 helper to stay consistent with the storage layer's existing\n * dedup hash column (see `facts.hash`).\n */\nexport function fastFactHash(text: string): string {\n return md5(text);\n}\n\n/** Strip leading articles + trim a candidate subject token. */\nexport function normaliseSubject(input: string, pack: LocalePack): string {\n const tokens = canonicaliseFactBody(input).split(' ');\n const stop = new Set(pack.subjectStopWords.map((s) => s.toLowerCase()));\n return tokens.filter((token) => token.length > 0 && !stop.has(token)).join(' ');\n}\n\n/**\n * Naive subject / predicate / object split. The locale pack supplies\n * the verb tokens used as predicate normalisers - the first matching\n * verb in the sentence becomes the predicate and splits the rest\n * into subject (left side) and object (right side). The split is\n * intentionally simple: anything more clever lives behind the entity-\n * linking layer (post-MVP).\n */\nexport function splitSubjectPredicateObject(\n text: string,\n pack: LocalePack,\n): { readonly subject: string; readonly predicate: string; readonly object: string } | null {\n const canonical = canonicaliseFactBody(text);\n if (canonical.length === 0) return null;\n const tokens = canonical.split(' ');\n const verbs = new Set(pack.predicateNormalisers.map((v) => v.toLowerCase()));\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token === undefined) continue;\n const stripped = token.replace(/[.,!?;:]/g, '');\n if (verbs.has(stripped)) {\n const subject = normaliseSubject(tokens.slice(0, i).join(' '), pack);\n const predicate = stripped;\n const object = canonicaliseFactBody(tokens.slice(i + 1).join(' '));\n if (subject.length === 0 || object.length === 0) continue;\n return { subject, predicate, object };\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,qBAAqB,MAAsB;AACzD,QAAO,KAAK,QAAQ,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa;;;;;;;AAQvD,SAAgB,aAAa,MAAsB;AACjD,QAAO,IAAI,KAAK;;;AAIlB,SAAgB,iBAAiB,OAAe,MAA0B;CACxE,MAAM,SAAS,qBAAqB,MAAM,CAAC,MAAM,IAAI;CACrD,MAAM,OAAO,IAAI,IAAI,KAAK,iBAAiB,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;AACvE,QAAO,OAAO,QAAQ,UAAU,MAAM,SAAS,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;AAWjF,SAAgB,4BACd,MACA,MAC0F;CAC1F,MAAM,YAAY,qBAAqB,KAAK;AAC5C,KAAI,UAAU,WAAW,EAAG,QAAO;CACnC,MAAM,SAAS,UAAU,MAAM,IAAI;CACnC,MAAM,QAAQ,IAAI,IAAI,KAAK,qBAAqB,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;AAC5E,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,QAAQ,OAAO;AACrB,MAAI,UAAU,OAAW;EACzB,MAAM,WAAW,MAAM,QAAQ,aAAa,GAAG;AAC/C,MAAI,MAAM,IAAI,SAAS,EAAE;GACvB,MAAM,UAAU,iBAAiB,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,KAAK;GACpE,MAAM,YAAY;GAClB,MAAM,SAAS,qBAAqB,OAAO,MAAM,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAClE,OAAI,QAAQ,WAAW,KAAK,OAAO,WAAW,EAAG;AACjD,UAAO;IAAE;IAAS;IAAW;IAAQ;;;AAGzC,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"stage1-exact-dedup.js","names":["stage1ExactDedup: PipelineStage"],"sources":["../../../src/conflict/stages/stage1-exact-dedup.ts"],"sourcesContent":["/**\n * Stage 1 Exact dedup. Computes an MD5 hash on the canonical fact\n * body (lowercase + trimmed + collapsed whitespace) and short-circuits\n * to `dedup` when an existing fact in the supplied vector hit list\n * shares the same canonical form.\n *\n * Stage 1 only inspects `existing` when Stage 2 already populated it\n * for the orchestrator (re-using the embedding round-trip). When the\n * orchestrator calls Stage 1 with an empty list the stage falls\n * through to `'continue'` Stage 2 will gather candidates and Stage 1\n * is folded into Stage 2's HOT zone via the same canonical-form\n * comparison.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage } from '../types.js';\nimport { canonicaliseFactBody, fastFactHash } from './helpers.js';\n\nexport const stage1ExactDedup: PipelineStage = {\n id: 'exact-dedup',\n async evaluate(ctx) {\n const candidateHash = fastFactHash(canonicaliseFactBody(ctx.candidate.text));\n if (ctx.existing.length === 0) {\n return { kind: 'continue' };\n }\n for (const hit of ctx.existing) {\n const existingHash = fastFactHash(canonicaliseFactBody(hit.record.text));\n if (existingHash === candidateHash) {\n return {\n kind: 'dedup',\n existingId: hit.record.id,\n similarity: 1,\n reason: 'exact-hash-match',\n };\n }\n }\n return { kind: 'continue' };\n },\n};\n"],"mappings":";;;AAmBA,MAAaA,mBAAkC;CAC7C,IAAI;CACJ,MAAM,SAAS,KAAK;EAClB,MAAM,gBAAgB,aAAa,qBAAqB,IAAI,UAAU,KAAK,CAAC;AAC5E,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO,EAAE,MAAM,YAAY;AAE7B,OAAK,MAAM,OAAO,IAAI,SAEpB,KADqB,aAAa,qBAAqB,IAAI,OAAO,KAAK,CAAC,KACnD,cACnB,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,YAAY;GACZ,QAAQ;GACT;AAGL,SAAO,EAAE,MAAM,YAAY;;CAE9B"}
1
+ {"version":3,"file":"stage1-exact-dedup.js","names":["stage1ExactDedup: PipelineStage"],"sources":["../../../src/conflict/stages/stage1-exact-dedup.ts"],"sourcesContent":["/**\n * Stage 1 - Exact dedup. Computes an MD5 hash on the canonical fact\n * body (lowercase + trimmed + collapsed whitespace) and short-circuits\n * to `dedup` when an existing fact in the supplied vector hit list\n * shares the same canonical form.\n *\n * Stage 1 only inspects `existing` when Stage 2 already populated it\n * for the orchestrator (re-using the embedding round-trip). When the\n * orchestrator calls Stage 1 with an empty list the stage falls\n * through to `'continue'` - Stage 2 will gather candidates and Stage 1\n * is folded into Stage 2's HOT zone via the same canonical-form\n * comparison.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage } from '../types.js';\nimport { canonicaliseFactBody, fastFactHash } from './helpers.js';\n\nexport const stage1ExactDedup: PipelineStage = {\n id: 'exact-dedup',\n async evaluate(ctx) {\n const candidateHash = fastFactHash(canonicaliseFactBody(ctx.candidate.text));\n if (ctx.existing.length === 0) {\n return { kind: 'continue' };\n }\n for (const hit of ctx.existing) {\n const existingHash = fastFactHash(canonicaliseFactBody(hit.record.text));\n if (existingHash === candidateHash) {\n return {\n kind: 'dedup',\n existingId: hit.record.id,\n similarity: 1,\n reason: 'exact-hash-match',\n };\n }\n }\n return { kind: 'continue' };\n },\n};\n"],"mappings":";;;AAmBA,MAAaA,mBAAkC;CAC7C,IAAI;CACJ,MAAM,SAAS,KAAK;EAClB,MAAM,gBAAgB,aAAa,qBAAqB,IAAI,UAAU,KAAK,CAAC;AAC5E,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO,EAAE,MAAM,YAAY;AAE7B,OAAK,MAAM,OAAO,IAAI,SAEpB,KADqB,aAAa,qBAAqB,IAAI,OAAO,KAAK,CAAC,KACnD,cACnB,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,YAAY;GACZ,QAAQ;GACT;AAGL,SAAO,EAAE,MAAM,YAAY;;CAE9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"stage2-embedding-three-zone.js","names":["stage2EmbeddingThreeZone: PipelineStage"],"sources":["../../../src/conflict/stages/stage2-embedding-three-zone.ts"],"sourcesContent":["/**\n * Stage 2 Embedding three-zone classifier (RB-02 §8.1).\n *\n * Inspects the candidate similarity against the top-K nearest\n * neighbours surfaced by the semantic vector store and routes the\n * decision into one of four zones:\n *\n * - HOT (`sim ≥ thresholds.hot`): silent dedup the existing fact is\n * semantically identical, no audit-worthy conflict.\n * - NEAR-DUP (`thresholds.nearDup ≤ sim < thresholds.hot`): same\n * fact phrased differently dedup with `near-dup` zone label so\n * operators can spot the case in `fact_conflicts`.\n * - CONFLICT-CHECK (`thresholds.cold < sim < thresholds.nearDup`):\n * candidate sounds related; defer decision to Stages 3 → 4 → 5.\n * - COLD (`sim ≤ thresholds.cold`): no conflict admit the candidate.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage2EmbeddingThreeZone: PipelineStage = {\n id: 'embedding-three-zone',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const similarity = top.score;\n if (similarity >= ctx.thresholds.hot) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-hot-zone',\n };\n }\n if (similarity >= ctx.thresholds.nearDup) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-near-dup-zone',\n };\n }\n if (similarity > ctx.thresholds.cold) {\n return { kind: 'continue' };\n }\n return { kind: 'admit', reason: 'embedding-cold-zone' };\n },\n};\n"],"mappings":";AAqBA,MAAaA,2BAA0C;CACrD,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,aAAa,IAAI;AACvB,MAAI,cAAc,IAAI,WAAW,IAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,cAAc,IAAI,WAAW,QAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,aAAa,IAAI,WAAW,KAC9B,QAAO,EAAE,MAAM,YAAY;AAE7B,SAAO;GAAE,MAAM;GAAS,QAAQ;GAAuB;;CAE1D"}
1
+ {"version":3,"file":"stage2-embedding-three-zone.js","names":["stage2EmbeddingThreeZone: PipelineStage"],"sources":["../../../src/conflict/stages/stage2-embedding-three-zone.ts"],"sourcesContent":["/**\n * Stage 2 - Embedding three-zone classifier (RB-02 §8.1).\n *\n * Inspects the candidate similarity against the top-K nearest\n * neighbours surfaced by the semantic vector store and routes the\n * decision into one of four zones:\n *\n * - HOT (`sim ≥ thresholds.hot`): silent dedup - the existing fact is\n * semantically identical, no audit-worthy conflict.\n * - NEAR-DUP (`thresholds.nearDup ≤ sim < thresholds.hot`): same\n * fact phrased differently - dedup with `near-dup` zone label so\n * operators can spot the case in `fact_conflicts`.\n * - CONFLICT-CHECK (`thresholds.cold < sim < thresholds.nearDup`):\n * candidate sounds related; defer decision to Stages 3 → 4 → 5.\n * - COLD (`sim ≤ thresholds.cold`): no conflict - admit the candidate.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage2EmbeddingThreeZone: PipelineStage = {\n id: 'embedding-three-zone',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const similarity = top.score;\n if (similarity >= ctx.thresholds.hot) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-hot-zone',\n };\n }\n if (similarity >= ctx.thresholds.nearDup) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-near-dup-zone',\n };\n }\n if (similarity > ctx.thresholds.cold) {\n return { kind: 'continue' };\n }\n return { kind: 'admit', reason: 'embedding-cold-zone' };\n },\n};\n"],"mappings":";AAqBA,MAAaA,2BAA0C;CACrD,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,aAAa,IAAI;AACvB,MAAI,cAAc,IAAI,WAAW,IAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,cAAc,IAAI,WAAW,QAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,aAAa,IAAI,WAAW,KAC9B,QAAO,EAAE,MAAM,YAAY;AAE7B,SAAO;GAAE,MAAM;GAAS,QAAQ;GAAuB;;CAE1D"}
@@ -3,7 +3,7 @@ import "../locale-packs/index.js";
3
3
 
4
4
  //#region src/conflict/stages/stage3-heuristic-regex.ts
5
5
  /**
6
- * Stage 3 Heuristic regex (per-locale, pluggable).
6
+ * Stage 3 - Heuristic regex (per-locale, pluggable).
7
7
  *
8
8
  * Inspects the candidate fact body for explicit supersede or negation
9
9
  * markers from the active locale pack (English by default). When a
@@ -1 +1 @@
1
- {"version":3,"file":"stage3-heuristic-regex.js","names":["stage3HeuristicRegex: PipelineStage"],"sources":["../../../src/conflict/stages/stage3-heuristic-regex.ts"],"sourcesContent":["/**\n * Stage 3 Heuristic regex (per-locale, pluggable).\n *\n * Inspects the candidate fact body for explicit supersede or negation\n * markers from the active locale pack (English by default). When a\n * marker fires the stage emits a `supersede` outcome against the\n * highest-scoring CONFLICT-CHECK candidate from Stage 2.\n *\n * @packageDocumentation\n */\n\nimport { evaluateMarkers } from '../locale-packs/index.js';\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage3HeuristicRegex: PipelineStage = {\n id: 'heuristic-regex',\n async evaluate(ctx): Promise<StageOutcome> {\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'continue' };\n }\n const candidateText = ctx.candidate.text;\n const supersede = evaluateMarkers(candidateText, ctx.localePack.supersedeMarkers);\n if (supersede.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-supersede-marker', supersede),\n };\n }\n const negation = evaluateMarkers(candidateText, ctx.localePack.negationMarkers);\n if (negation.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-negation', negation),\n };\n }\n return { kind: 'continue' };\n },\n};\n\nfunction formatReason(\n base: string,\n match: { kind?: string | undefined; excerpt?: string | undefined },\n): string {\n const parts = [base];\n if (match.kind !== undefined) parts.push(`kind=${match.kind}`);\n if (match.excerpt !== undefined) parts.push(`marker=${match.excerpt}`);\n return parts.join('; ');\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAaA,uBAAsC;CACjD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO,EAAE,MAAM,YAAY;EAE7B,MAAM,gBAAgB,IAAI,UAAU;EACpC,MAAM,YAAY,gBAAgB,eAAe,IAAI,WAAW,iBAAiB;AACjF,MAAI,UAAU,QACZ,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,0BAA0B,UAAU;GAC1D;EAEH,MAAM,WAAW,gBAAgB,eAAe,IAAI,WAAW,gBAAgB;AAC/E,MAAI,SAAS,QACX,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,kBAAkB,SAAS;GACjD;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B;AAED,SAAS,aACP,MACA,OACQ;CACR,MAAM,QAAQ,CAAC,KAAK;AACpB,KAAI,MAAM,SAAS,OAAW,OAAM,KAAK,QAAQ,MAAM,OAAO;AAC9D,KAAI,MAAM,YAAY,OAAW,OAAM,KAAK,UAAU,MAAM,UAAU;AACtE,QAAO,MAAM,KAAK,KAAK"}
1
+ {"version":3,"file":"stage3-heuristic-regex.js","names":["stage3HeuristicRegex: PipelineStage"],"sources":["../../../src/conflict/stages/stage3-heuristic-regex.ts"],"sourcesContent":["/**\n * Stage 3 - Heuristic regex (per-locale, pluggable).\n *\n * Inspects the candidate fact body for explicit supersede or negation\n * markers from the active locale pack (English by default). When a\n * marker fires the stage emits a `supersede` outcome against the\n * highest-scoring CONFLICT-CHECK candidate from Stage 2.\n *\n * @packageDocumentation\n */\n\nimport { evaluateMarkers } from '../locale-packs/index.js';\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage3HeuristicRegex: PipelineStage = {\n id: 'heuristic-regex',\n async evaluate(ctx): Promise<StageOutcome> {\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'continue' };\n }\n const candidateText = ctx.candidate.text;\n const supersede = evaluateMarkers(candidateText, ctx.localePack.supersedeMarkers);\n if (supersede.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-supersede-marker', supersede),\n };\n }\n const negation = evaluateMarkers(candidateText, ctx.localePack.negationMarkers);\n if (negation.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-negation', negation),\n };\n }\n return { kind: 'continue' };\n },\n};\n\nfunction formatReason(\n base: string,\n match: { kind?: string | undefined; excerpt?: string | undefined },\n): string {\n const parts = [base];\n if (match.kind !== undefined) parts.push(`kind=${match.kind}`);\n if (match.excerpt !== undefined) parts.push(`marker=${match.excerpt}`);\n return parts.join('; ');\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAaA,uBAAsC;CACjD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO,EAAE,MAAM,YAAY;EAE7B,MAAM,gBAAgB,IAAI,UAAU;EACpC,MAAM,YAAY,gBAAgB,eAAe,IAAI,WAAW,iBAAiB;AACjF,MAAI,UAAU,QACZ,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,0BAA0B,UAAU;GAC1D;EAEH,MAAM,WAAW,gBAAgB,eAAe,IAAI,WAAW,gBAAgB;AAC/E,MAAI,SAAS,QACX,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,kBAAkB,SAAS;GACjD;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B;AAED,SAAS,aACP,MACA,OACQ;CACR,MAAM,QAAQ,CAAC,KAAK;AACpB,KAAI,MAAM,SAAS,OAAW,OAAM,KAAK,QAAQ,MAAM,OAAO;AAC9D,KAAI,MAAM,YAAY,OAAW,OAAM,KAAK,UAAU,MAAM,UAAU;AACtE,QAAO,MAAM,KAAK,KAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"stage4-subject-predicate.js","names":["stage4SubjectPredicate: PipelineStage"],"sources":["../../../src/conflict/stages/stage4-subject-predicate.ts"],"sourcesContent":["/**\n * Stage 4 Subject / predicate match.\n *\n * Naively splits each fact body into `(subject, predicate, object)`\n * triples using the locale pack's predicate normalisers. When the\n * candidate and an existing CONFLICT-CHECK fact share the same\n * subject + predicate but differ on the object, Stage 4 marks the\n * existing fact as superseded.\n *\n * The split is intentionally simple: anything more sophisticated\n * (full entity linking) lives in the post-MVP knowledge-graph layer.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\nimport { splitSubjectPredicateObject } from './helpers.js';\n\nexport const stage4SubjectPredicate: PipelineStage = {\n id: 'subject-predicate',\n async evaluate(ctx): Promise<StageOutcome> {\n const candidateTriple = splitSubjectPredicateObject(ctx.candidate.text, ctx.localePack);\n if (candidateTriple === null) {\n return { kind: 'continue' };\n }\n for (const hit of ctx.existing) {\n const existingTriple = splitSubjectPredicateObject(hit.record.text, ctx.localePack);\n if (existingTriple === null) continue;\n if (candidateTriple.subject !== existingTriple.subject) continue;\n if (candidateTriple.predicate !== existingTriple.predicate) continue;\n if (candidateTriple.object === existingTriple.object) {\n return {\n kind: 'dedup',\n existingId: hit.record.id,\n reason: 'subject-predicate-match-same-object',\n };\n }\n return {\n kind: 'supersede',\n existingId: hit.record.id,\n reason: `subject-predicate-match; subject=${candidateTriple.subject}; predicate=${candidateTriple.predicate}`,\n };\n }\n return { kind: 'continue' };\n },\n};\n"],"mappings":";;;AAkBA,MAAaA,yBAAwC;CACnD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,kBAAkB,4BAA4B,IAAI,UAAU,MAAM,IAAI,WAAW;AACvF,MAAI,oBAAoB,KACtB,QAAO,EAAE,MAAM,YAAY;AAE7B,OAAK,MAAM,OAAO,IAAI,UAAU;GAC9B,MAAM,iBAAiB,4BAA4B,IAAI,OAAO,MAAM,IAAI,WAAW;AACnF,OAAI,mBAAmB,KAAM;AAC7B,OAAI,gBAAgB,YAAY,eAAe,QAAS;AACxD,OAAI,gBAAgB,cAAc,eAAe,UAAW;AAC5D,OAAI,gBAAgB,WAAW,eAAe,OAC5C,QAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ;IACT;AAEH,UAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ,oCAAoC,gBAAgB,QAAQ,cAAc,gBAAgB;IACnG;;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B"}
1
+ {"version":3,"file":"stage4-subject-predicate.js","names":["stage4SubjectPredicate: PipelineStage"],"sources":["../../../src/conflict/stages/stage4-subject-predicate.ts"],"sourcesContent":["/**\n * Stage 4 - Subject / predicate match.\n *\n * Naively splits each fact body into `(subject, predicate, object)`\n * triples using the locale pack's predicate normalisers. When the\n * candidate and an existing CONFLICT-CHECK fact share the same\n * subject + predicate but differ on the object, Stage 4 marks the\n * existing fact as superseded.\n *\n * The split is intentionally simple: anything more sophisticated\n * (full entity linking) lives in the post-MVP knowledge-graph layer.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\nimport { splitSubjectPredicateObject } from './helpers.js';\n\nexport const stage4SubjectPredicate: PipelineStage = {\n id: 'subject-predicate',\n async evaluate(ctx): Promise<StageOutcome> {\n const candidateTriple = splitSubjectPredicateObject(ctx.candidate.text, ctx.localePack);\n if (candidateTriple === null) {\n return { kind: 'continue' };\n }\n for (const hit of ctx.existing) {\n const existingTriple = splitSubjectPredicateObject(hit.record.text, ctx.localePack);\n if (existingTriple === null) continue;\n if (candidateTriple.subject !== existingTriple.subject) continue;\n if (candidateTriple.predicate !== existingTriple.predicate) continue;\n if (candidateTriple.object === existingTriple.object) {\n return {\n kind: 'dedup',\n existingId: hit.record.id,\n reason: 'subject-predicate-match-same-object',\n };\n }\n return {\n kind: 'supersede',\n existingId: hit.record.id,\n reason: `subject-predicate-match; subject=${candidateTriple.subject}; predicate=${candidateTriple.predicate}`,\n };\n }\n return { kind: 'continue' };\n },\n};\n"],"mappings":";;;AAkBA,MAAaA,yBAAwC;CACnD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,kBAAkB,4BAA4B,IAAI,UAAU,MAAM,IAAI,WAAW;AACvF,MAAI,oBAAoB,KACtB,QAAO,EAAE,MAAM,YAAY;AAE7B,OAAK,MAAM,OAAO,IAAI,UAAU;GAC9B,MAAM,iBAAiB,4BAA4B,IAAI,OAAO,MAAM,IAAI,WAAW;AACnF,OAAI,mBAAmB,KAAM;AAC7B,OAAI,gBAAgB,YAAY,eAAe,QAAS;AACxD,OAAI,gBAAgB,cAAc,eAAe,UAAW;AAC5D,OAAI,gBAAgB,WAAW,eAAe,OAC5C,QAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ;IACT;AAEH,UAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ,oCAAoC,gBAAgB,QAAQ,cAAc,gBAAgB;IACnG;;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"stage5-defer-to-deep.js","names":["stage5DeferToDeep: PipelineStage"],"sources":["../../../src/conflict/stages/stage5-defer-to-deep.ts"],"sourcesContent":["/**\n * Stage 5 Defer to deep LLM judge.\n *\n * Reached only when Stages 14 left the decision unresolved. The\n * stage records every CONFLICT-CHECK candidate id surfaced by Stage 2\n * so the orchestrator can hand the queue write to the storage layer\n * (the actual `conflict_check_pending` insert is the orchestrator's\n * responsibility Stage 5 only computes the payload).\n *\n * The candidate fact itself is admitted with a `pending` flag so it\n * remains searchable; the deep phase (Phase 10c) drains the queue and\n * applies the resolution later.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage5DeferToDeep: PipelineStage = {\n id: 'defer-to-deep',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'stage5-no-candidates' };\n }\n const conflictingIds = ctx.existing.map((hit) => hit.record.id);\n const top = ctx.existing[0];\n return {\n kind: 'pending',\n conflictingIds,\n ...(top !== undefined ? { similarity: top.score } : {}),\n reason: 'awaiting-deep-llm-judge',\n };\n },\n};\n"],"mappings":";AAkBA,MAAaA,oBAAmC;CAC9C,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAAwB;EAE1D,MAAM,iBAAiB,IAAI,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG;EAC/D,MAAM,MAAM,IAAI,SAAS;AACzB,SAAO;GACL,MAAM;GACN;GACA,GAAI,QAAQ,SAAY,EAAE,YAAY,IAAI,OAAO,GAAG,EAAE;GACtD,QAAQ;GACT;;CAEJ"}
1
+ {"version":3,"file":"stage5-defer-to-deep.js","names":["stage5DeferToDeep: PipelineStage"],"sources":["../../../src/conflict/stages/stage5-defer-to-deep.ts"],"sourcesContent":["/**\n * Stage 5 - Defer to deep LLM judge.\n *\n * Reached only when Stages 1-4 left the decision unresolved. The\n * stage records every CONFLICT-CHECK candidate id surfaced by Stage 2\n * so the orchestrator can hand the queue write to the storage layer\n * (the actual `conflict_check_pending` insert is the orchestrator's\n * responsibility - Stage 5 only computes the payload).\n *\n * The candidate fact itself is admitted with a `pending` flag so it\n * remains searchable; the deep phase (Phase 10c) drains the queue and\n * applies the resolution later.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage5DeferToDeep: PipelineStage = {\n id: 'defer-to-deep',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'stage5-no-candidates' };\n }\n const conflictingIds = ctx.existing.map((hit) => hit.record.id);\n const top = ctx.existing[0];\n return {\n kind: 'pending',\n conflictingIds,\n ...(top !== undefined ? { similarity: top.score } : {}),\n reason: 'awaiting-deep-llm-judge',\n };\n },\n};\n"],"mappings":";AAkBA,MAAaA,oBAAmC;CAC9C,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAAwB;EAE1D,MAAM,iBAAiB,IAAI,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG;EAC/D,MAAM,MAAM,IAAI,SAAS;AACzB,SAAO;GACL,MAAM;GACN;GACA,GAAI,QAAQ,SAAY,EAAE,YAAY,IAAI,OAAO,GAAG,EAAE;GACtD,QAAQ;GACT;;CAEJ"}
@@ -15,7 +15,7 @@ import { EmbedderProvider, Fact, MemoryHit, Tracer } from "@graphorin/core";
15
15
  type ConflictStage = 'exact-dedup' | 'embedding-three-zone' | 'heuristic-regex' | 'subject-predicate' | 'defer-to-deep';
16
16
  /**
17
17
  * Configurable similarity thresholds for Stage 2 (embedding three-
18
- * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 the
18
+ * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 - the
19
19
  * production values for the default `Xenova/multilingual-e5-base`
20
20
  * embedder (DEC-130).
21
21
  *
@@ -34,8 +34,8 @@ interface ConflictThresholds {
34
34
  */
35
35
  declare const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds;
36
36
  /**
37
- * Final pipeline outcome discriminated union returned by
38
- * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 every
37
+ * Final pipeline outcome - discriminated union returned by
38
+ * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 - every
39
39
  * variant carries the originating `stage` so audit + replay tooling
40
40
  * can pattern-match without inspecting the message.
41
41
  *
@@ -72,9 +72,9 @@ type ConflictDecision = {
72
72
  *
73
73
  * The `mode` field is the master switch:
74
74
  *
75
- * - `'on'` (default) the multi-stage pipeline runs on every
75
+ * - `'on'` (default) - the multi-stage pipeline runs on every
76
76
  * `SemanticMemory.remember(...)` call.
77
- * - `'off'` bypass the pipeline and fall back to 10a's straight-
77
+ * - `'off'` - bypass the pipeline and fall back to 10a's straight-
78
78
  * through write. Emits a one-shot WARN (per process) so operators
79
79
  * notice the regression risk.
80
80
  *
@@ -107,7 +107,7 @@ interface StageContext {
107
107
  }
108
108
  /**
109
109
  * Per-stage outcome surfaced to the orchestrator. `'admit'` means the
110
- * stage decided not to short-circuit the pipeline continues to the
110
+ * stage decided not to short-circuit - the pipeline continues to the
111
111
  * next stage. Every other variant terminates the pipeline.
112
112
  *
113
113
  * @stable
@@ -210,7 +210,7 @@ type ReconcileDecision = {
210
210
  * so reconcile outcomes land in `fact_conflicts` through the same audit
211
211
  * path as the inline pipeline (no new stage / schema): `add` → `admit`,
212
212
  * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
213
- * decisions are stamped with the `defer-to-deep` stage the reconcile
213
+ * decisions are stamped with the `defer-to-deep` stage - the reconcile
214
214
  * loop is the consolidator's replacement for the deferred deep-judge
215
215
  * step it supersedes.
216
216
  *
@@ -15,7 +15,7 @@ const DEFAULT_CONFLICT_THRESHOLDS = Object.freeze({
15
15
  * so reconcile outcomes land in `fact_conflicts` through the same audit
16
16
  * path as the inline pipeline (no new stage / schema): `add` → `admit`,
17
17
  * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
18
- * decisions are stamped with the `defer-to-deep` stage the reconcile
18
+ * decisions are stamped with the `defer-to-deep` stage - the reconcile
19
19
  * loop is the consolidator's replacement for the deferred deep-judge
20
20
  * step it supersedes.
21
21
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds","stage: ConflictStage"],"sources":["../../src/conflict/types.ts"],"sourcesContent":["/**\n * Public types for the multi-stage conflict resolution pipeline shipped\n * in `@graphorin/memory` Phase 10b (DEC-117 / ADR-018 ext / RB-02).\n *\n * @packageDocumentation\n */\n\nimport type { EmbedderProvider, Fact, MemoryHit, Tracer } from '@graphorin/core';\nimport type { ConflictMemoryStoreExt, MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport type { LocalePack } from './locale-packs/index.js';\n\n/**\n * Stable lowercase identifier of the pipeline stage. Mirrored\n * byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`\n * so `fact_conflicts` / `conflict_check_pending` rows can be grouped\n * deterministically.\n *\n * @stable\n */\nexport type ConflictStage =\n | 'exact-dedup'\n | 'embedding-three-zone'\n | 'heuristic-regex'\n | 'subject-predicate'\n | 'defer-to-deep';\n\n/**\n * Configurable similarity thresholds for Stage 2 (embedding three-\n * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 the\n * production values for the default `Xenova/multilingual-e5-base`\n * embedder (DEC-130).\n *\n * @stable\n */\nexport interface ConflictThresholds {\n readonly hot: number;\n readonly nearDup: number;\n readonly cold: number;\n}\n\n/**\n * Hard-coded defaults for the three-zone thresholds. Imported by the\n * pipeline + the test suite so call sites stay aligned.\n *\n * @stable\n */\nexport const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds = Object.freeze({\n hot: 0.95,\n nearDup: 0.85,\n cold: 0.4,\n});\n\n/**\n * Final pipeline outcome discriminated union returned by\n * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 every\n * variant carries the originating `stage` so audit + replay tooling\n * can pattern-match without inspecting the message.\n *\n * @stable\n */\nexport type ConflictDecision =\n | {\n readonly kind: 'admit';\n readonly stage: ConflictStage;\n readonly reason?: string;\n }\n | {\n readonly kind: 'dedup';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly stage: ConflictStage;\n /** The candidate id that was admitted as `pending`. */\n readonly candidateId: string;\n /** Top-K conflicting existing fact ids from Stage 2's vector search. */\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Pipeline configuration accepted by `createMemory({ conflictPipeline:\n * ... })` and surfaced through {@link createConflictPipeline}.\n *\n * The `mode` field is the master switch:\n *\n * - `'on'` (default) the multi-stage pipeline runs on every\n * `SemanticMemory.remember(...)` call.\n * - `'off'` bypass the pipeline and fall back to 10a's straight-\n * through write. Emits a one-shot WARN (per process) so operators\n * notice the regression risk.\n *\n * @stable\n */\nexport interface ConflictPipelineOptions {\n readonly mode?: 'on' | 'off';\n readonly thresholds?: Partial<ConflictThresholds>;\n readonly localePack?: LocalePack;\n /** Per-list candidate count fed into Stage 2. Default `5` (RB-02 §8.1). */\n readonly stage2TopK?: number;\n /** Override the audit / pending sink. Defaults to `store.conflicts` when present. */\n readonly conflictStore?: ConflictMemoryStoreExt;\n /** Inject a deterministic clock. Defaults to `() => new Date().toISOString()`. */\n readonly now?: () => string;\n}\n\n/**\n * Inputs the orchestrator hands every stage. The `existing` array is\n * populated during Stage 2 (vector search top-K); Stage 1 receives an\n * empty array because the dedup hash is computed off the candidate\n * alone.\n *\n * @stable\n */\nexport interface StageContext {\n readonly candidate: Fact;\n readonly existing: ReadonlyArray<MemoryHit<Fact>>;\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n}\n\n/**\n * Per-stage outcome surfaced to the orchestrator. `'admit'` means the\n * stage decided not to short-circuit the pipeline continues to the\n * next stage. Every other variant terminates the pipeline.\n *\n * @stable\n */\nexport type StageOutcome =\n | { readonly kind: 'continue' }\n | { readonly kind: 'admit'; readonly reason?: string }\n | {\n readonly kind: 'dedup';\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Stage interface every step implements. The orchestrator visits\n * stages in declaration order and stops at the first non-`'continue'`\n * outcome.\n *\n * @stable\n */\nexport interface PipelineStage {\n readonly id: ConflictStage;\n evaluate(ctx: StageContext): Promise<StageOutcome>;\n}\n\n/**\n * Pre-built pipeline returned by {@link createConflictPipeline}. The\n * `run` method is what `SemanticMemory.remember(...)` calls to make\n * the conflict decision.\n *\n * @stable\n */\nexport interface ConflictPipeline {\n readonly mode: 'on' | 'off';\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision>;\n}\n\n/**\n * Per-call dependency bag handed to the pipeline by `SemanticMemory`.\n *\n * @stable\n */\nexport interface ConflictPipelineDeps {\n readonly store: MemoryStoreAdapter;\n readonly tracer: Tracer;\n readonly embedder: EmbedderProvider | null;\n readonly embedderId: string | null;\n /** Optional cancellation signal forwarded to embedder + searchVector. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The four actions the neighbour-aware reconcile loop (P0-3) may choose\n * for a candidate fact once the LLM has the most-similar existing\n * memories in view. The de-facto-standard memory write loop\n * (Mem0 / LangMem / Letta), with Graphorin's twist: `update` and\n * `conflict` route through a **bi-temporal supersede** (close the old\n * interval, insert the new) rather than a destructive delete.\n *\n * @stable\n */\nexport type ReconcileAction = 'add' | 'update' | 'noop' | 'conflict';\n\n/**\n * Outcome of {@link reconcileCandidate}. `add` is independent / unsure\n * (a fresh fact); `noop` is a duplicate that adds nothing; `update` is\n * a newer version of `targetId`; `conflict` contradicts `targetId` and\n * closes it. The three neighbour-referencing variants carry the\n * `targetId` of the existing fact they resolve against.\n *\n * @stable\n */\nexport type ReconcileDecision =\n | { readonly action: 'add'; readonly reason?: string }\n | { readonly action: 'update'; readonly targetId: string; readonly reason: string }\n | { readonly action: 'noop'; readonly targetId: string; readonly reason?: string }\n | { readonly action: 'conflict'; readonly targetId: string; readonly reason: string };\n\n/**\n * Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}\n * so reconcile outcomes land in `fact_conflicts` through the same audit\n * path as the inline pipeline (no new stage / schema): `add` → `admit`,\n * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile\n * decisions are stamped with the `defer-to-deep` stage the reconcile\n * loop is the consolidator's replacement for the deferred deep-judge\n * step it supersedes.\n *\n * @stable\n */\nexport function reconcileToConflictDecision(decision: ReconcileDecision): ConflictDecision {\n const stage: ConflictStage = 'defer-to-deep';\n switch (decision.action) {\n case 'add':\n return {\n kind: 'admit',\n stage,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'noop':\n return {\n kind: 'dedup',\n stage,\n existingId: decision.targetId,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'update':\n case 'conflict':\n return {\n kind: 'supersede',\n stage,\n existingId: decision.targetId,\n reason: decision.reason,\n };\n }\n}\n"],"mappings":";;;;;;;AA8CA,MAAaA,8BAAkD,OAAO,OAAO;CAC3E,KAAK;CACL,SAAS;CACT,MAAM;CACP,CAAC;;;;;;;;;;;;AA2LF,SAAgB,4BAA4B,UAA+C;CACzF,MAAMC,QAAuB;AAC7B,SAAQ,SAAS,QAAjB;EACE,KAAK,MACH,QAAO;GACL,MAAM;GACN;GACA,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK,OACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK;EACL,KAAK,WACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,QAAQ,SAAS;GAClB"}
1
+ {"version":3,"file":"types.js","names":["DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds","stage: ConflictStage"],"sources":["../../src/conflict/types.ts"],"sourcesContent":["/**\n * Public types for the multi-stage conflict resolution pipeline shipped\n * in `@graphorin/memory` Phase 10b (DEC-117 / ADR-018 ext / RB-02).\n *\n * @packageDocumentation\n */\n\nimport type { EmbedderProvider, Fact, MemoryHit, Tracer } from '@graphorin/core';\nimport type { ConflictMemoryStoreExt, MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport type { LocalePack } from './locale-packs/index.js';\n\n/**\n * Stable lowercase identifier of the pipeline stage. Mirrored\n * byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`\n * so `fact_conflicts` / `conflict_check_pending` rows can be grouped\n * deterministically.\n *\n * @stable\n */\nexport type ConflictStage =\n | 'exact-dedup'\n | 'embedding-three-zone'\n | 'heuristic-regex'\n | 'subject-predicate'\n | 'defer-to-deep';\n\n/**\n * Configurable similarity thresholds for Stage 2 (embedding three-\n * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 - the\n * production values for the default `Xenova/multilingual-e5-base`\n * embedder (DEC-130).\n *\n * @stable\n */\nexport interface ConflictThresholds {\n readonly hot: number;\n readonly nearDup: number;\n readonly cold: number;\n}\n\n/**\n * Hard-coded defaults for the three-zone thresholds. Imported by the\n * pipeline + the test suite so call sites stay aligned.\n *\n * @stable\n */\nexport const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds = Object.freeze({\n hot: 0.95,\n nearDup: 0.85,\n cold: 0.4,\n});\n\n/**\n * Final pipeline outcome - discriminated union returned by\n * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 - every\n * variant carries the originating `stage` so audit + replay tooling\n * can pattern-match without inspecting the message.\n *\n * @stable\n */\nexport type ConflictDecision =\n | {\n readonly kind: 'admit';\n readonly stage: ConflictStage;\n readonly reason?: string;\n }\n | {\n readonly kind: 'dedup';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly stage: ConflictStage;\n /** The candidate id that was admitted as `pending`. */\n readonly candidateId: string;\n /** Top-K conflicting existing fact ids from Stage 2's vector search. */\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Pipeline configuration accepted by `createMemory({ conflictPipeline:\n * ... })` and surfaced through {@link createConflictPipeline}.\n *\n * The `mode` field is the master switch:\n *\n * - `'on'` (default) - the multi-stage pipeline runs on every\n * `SemanticMemory.remember(...)` call.\n * - `'off'` - bypass the pipeline and fall back to 10a's straight-\n * through write. Emits a one-shot WARN (per process) so operators\n * notice the regression risk.\n *\n * @stable\n */\nexport interface ConflictPipelineOptions {\n readonly mode?: 'on' | 'off';\n readonly thresholds?: Partial<ConflictThresholds>;\n readonly localePack?: LocalePack;\n /** Per-list candidate count fed into Stage 2. Default `5` (RB-02 §8.1). */\n readonly stage2TopK?: number;\n /** Override the audit / pending sink. Defaults to `store.conflicts` when present. */\n readonly conflictStore?: ConflictMemoryStoreExt;\n /** Inject a deterministic clock. Defaults to `() => new Date().toISOString()`. */\n readonly now?: () => string;\n}\n\n/**\n * Inputs the orchestrator hands every stage. The `existing` array is\n * populated during Stage 2 (vector search top-K); Stage 1 receives an\n * empty array because the dedup hash is computed off the candidate\n * alone.\n *\n * @stable\n */\nexport interface StageContext {\n readonly candidate: Fact;\n readonly existing: ReadonlyArray<MemoryHit<Fact>>;\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n}\n\n/**\n * Per-stage outcome surfaced to the orchestrator. `'admit'` means the\n * stage decided not to short-circuit - the pipeline continues to the\n * next stage. Every other variant terminates the pipeline.\n *\n * @stable\n */\nexport type StageOutcome =\n | { readonly kind: 'continue' }\n | { readonly kind: 'admit'; readonly reason?: string }\n | {\n readonly kind: 'dedup';\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Stage interface every step implements. The orchestrator visits\n * stages in declaration order and stops at the first non-`'continue'`\n * outcome.\n *\n * @stable\n */\nexport interface PipelineStage {\n readonly id: ConflictStage;\n evaluate(ctx: StageContext): Promise<StageOutcome>;\n}\n\n/**\n * Pre-built pipeline returned by {@link createConflictPipeline}. The\n * `run` method is what `SemanticMemory.remember(...)` calls to make\n * the conflict decision.\n *\n * @stable\n */\nexport interface ConflictPipeline {\n readonly mode: 'on' | 'off';\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision>;\n}\n\n/**\n * Per-call dependency bag handed to the pipeline by `SemanticMemory`.\n *\n * @stable\n */\nexport interface ConflictPipelineDeps {\n readonly store: MemoryStoreAdapter;\n readonly tracer: Tracer;\n readonly embedder: EmbedderProvider | null;\n readonly embedderId: string | null;\n /** Optional cancellation signal forwarded to embedder + searchVector. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The four actions the neighbour-aware reconcile loop (P0-3) may choose\n * for a candidate fact once the LLM has the most-similar existing\n * memories in view. The de-facto-standard memory write loop\n * (Mem0 / LangMem / Letta), with Graphorin's twist: `update` and\n * `conflict` route through a **bi-temporal supersede** (close the old\n * interval, insert the new) rather than a destructive delete.\n *\n * @stable\n */\nexport type ReconcileAction = 'add' | 'update' | 'noop' | 'conflict';\n\n/**\n * Outcome of {@link reconcileCandidate}. `add` is independent / unsure\n * (a fresh fact); `noop` is a duplicate that adds nothing; `update` is\n * a newer version of `targetId`; `conflict` contradicts `targetId` and\n * closes it. The three neighbour-referencing variants carry the\n * `targetId` of the existing fact they resolve against.\n *\n * @stable\n */\nexport type ReconcileDecision =\n | { readonly action: 'add'; readonly reason?: string }\n | { readonly action: 'update'; readonly targetId: string; readonly reason: string }\n | { readonly action: 'noop'; readonly targetId: string; readonly reason?: string }\n | { readonly action: 'conflict'; readonly targetId: string; readonly reason: string };\n\n/**\n * Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}\n * so reconcile outcomes land in `fact_conflicts` through the same audit\n * path as the inline pipeline (no new stage / schema): `add` → `admit`,\n * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile\n * decisions are stamped with the `defer-to-deep` stage - the reconcile\n * loop is the consolidator's replacement for the deferred deep-judge\n * step it supersedes.\n *\n * @stable\n */\nexport function reconcileToConflictDecision(decision: ReconcileDecision): ConflictDecision {\n const stage: ConflictStage = 'defer-to-deep';\n switch (decision.action) {\n case 'add':\n return {\n kind: 'admit',\n stage,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'noop':\n return {\n kind: 'dedup',\n stage,\n existingId: decision.targetId,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'update':\n case 'conflict':\n return {\n kind: 'supersede',\n stage,\n existingId: decision.targetId,\n reason: decision.reason,\n };\n }\n}\n"],"mappings":";;;;;;;AA8CA,MAAaA,8BAAkD,OAAO,OAAO;CAC3E,KAAK;CACL,SAAS;CACT,MAAM;CACP,CAAC;;;;;;;;;;;;AA2LF,SAAgB,4BAA4B,UAA+C;CACzF,MAAMC,QAAuB;AAC7B,SAAQ,SAAS,QAAjB;EACE,KAAK,MACH,QAAO;GACL,MAAM;GACN;GACA,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK,OACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK;EACL,KAAK,WACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,QAAQ,SAAS;GAClB"}
@@ -7,7 +7,7 @@ import { BudgetExceededError } from "./errors.js";
7
7
  * (token + cost), the UTC reset semantics, and the `onExceed`
8
8
  * dispatch.
9
9
  *
10
- * The tracker does not perform any I/O operators surface the live
10
+ * The tracker does not perform any I/O - operators surface the live
11
11
  * counters via `Consolidator.status()` and the persisted snapshot is
12
12
  * derived from `consolidator_runs` rows.
13
13
  *
@@ -24,6 +24,7 @@ var BudgetTracker = class {
24
24
  #now;
25
25
  #resetSemantics;
26
26
  #onExceed;
27
+ #logger;
27
28
  #maxTokensPerDay;
28
29
  #maxCostPerDay;
29
30
  #bucketStart;
@@ -31,17 +32,22 @@ var BudgetTracker = class {
31
32
  #cost = 0;
32
33
  #paused = false;
33
34
  #pausedReason = null;
35
+ /** Resources already WARNed this window under `onExceed: 'log'` (memory-consolidation-02). */
36
+ #warnedThisWindow = /* @__PURE__ */ new Set();
34
37
  /**
35
38
  * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to
36
39
  * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the
37
40
  * counters reflect a true rolling window instead of being zeroed on every
38
- * check (MCON-3). Unused and never appended to under `utc` / `local`.
41
+ * check (MCON-3). Unused - and never appended to - under `utc` / `local`.
39
42
  */
40
43
  #ledger = [];
41
44
  constructor(opts) {
42
45
  this.#now = opts.now ?? Date.now;
43
46
  this.#resetSemantics = opts.resetSemantics;
44
47
  this.#onExceed = opts.onExceed;
48
+ this.#logger = opts.logger ?? ((message) => {
49
+ process.stderr.write(`${message}\n`);
50
+ });
45
51
  this.#maxTokensPerDay = opts.maxTokensPerDay;
46
52
  this.#maxCostPerDay = opts.maxCostPerDay;
47
53
  this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
@@ -86,7 +92,7 @@ var BudgetTracker = class {
86
92
  return { allowed: true };
87
93
  }
88
94
  /**
89
- * Record consumption. Returns the post-record state `paused` is
95
+ * Record consumption. Returns the post-record state - `paused` is
90
96
  * `true` when the spend pushed past a ceiling under
91
97
  * `onExceed: 'pause'`.
92
98
  *
@@ -138,6 +144,7 @@ var BudgetTracker = class {
138
144
  this.#paused = false;
139
145
  this.#pausedReason = null;
140
146
  this.#ledger = [];
147
+ this.#warnedThisWindow.clear();
141
148
  this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
142
149
  }
143
150
  #handleBreach(phase, resource, actual, budget) {
@@ -151,6 +158,10 @@ var BudgetTracker = class {
151
158
  this.#paused = true;
152
159
  this.#pausedReason = resource === "tokens" ? "tokens-exceeded" : "cost-exceeded";
153
160
  }
161
+ if (this.#onExceed === "log" && !this.#warnedThisWindow.has(resource)) {
162
+ this.#warnedThisWindow.add(resource);
163
+ this.#logger(`[graphorin/memory] consolidator ${resource} budget exceeded in phase '${phase}': ${resource === "cost" ? `$${actual.toFixed(4)} > $${budget.toFixed(4)}` : `${actual} > ${budget}`} (onExceed: 'log' - continuing; switch to 'pause'/'throw' to enforce).`);
164
+ }
154
165
  }
155
166
  #maybeReset() {
156
167
  const now = this.#now();
@@ -163,6 +174,7 @@ var BudgetTracker = class {
163
174
  this.#paused = false;
164
175
  this.#pausedReason = null;
165
176
  }
177
+ if (this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) this.#warnedThisWindow.clear();
166
178
  return;
167
179
  }
168
180
  const currentBucket = bucketStart(now, this.#resetSemantics);
@@ -172,6 +184,7 @@ var BudgetTracker = class {
172
184
  this.#cost = 0;
173
185
  this.#paused = false;
174
186
  this.#pausedReason = null;
187
+ this.#warnedThisWindow.clear();
175
188
  }
176
189
  }
177
190
  };
@@ -1 +1 @@
1
- {"version":3,"file":"budget.js","names":["#now","#resetSemantics","#onExceed","#maxTokensPerDay","#maxCostPerDay","#bucketStart","#paused","#tokens","#cost","#pausedReason","#maybeReset","#ledger","#handleBreach"],"sources":["../../src/consolidator/budget.ts"],"sourcesContent":["/**\n * Cost-budget enforcement for the consolidator. The tracker is a\n * pure in-memory state machine: it owns the day-bucketed counters\n * (token + cost), the UTC reset semantics, and the `onExceed`\n * dispatch.\n *\n * The tracker does not perform any I/O — operators surface the live\n * counters via `Consolidator.status()` and the persisted snapshot is\n * derived from `consolidator_runs` rows.\n *\n * @packageDocumentation\n */\n\nimport { BudgetExceededError } from './errors.js';\nimport type { ConsolidatorPhase, OnBudgetExceed } from './types.js';\n\n/**\n * @stable\n */\nexport interface BudgetSnapshot {\n readonly tokensUsedToday: number;\n readonly costUsedToday: number;\n readonly tokensRemaining: number;\n readonly costRemaining: number;\n readonly resetAt: string;\n readonly paused: boolean;\n}\n\n/**\n * @stable\n */\nexport interface BudgetCheck {\n readonly allowed: boolean;\n readonly reason?: 'paused' | 'tokens-exceeded' | 'cost-exceeded';\n}\n\n/**\n * @stable\n */\nexport interface BudgetTrackerOptions {\n readonly maxTokensPerDay: number;\n readonly maxCostPerDay: number;\n readonly onExceed: OnBudgetExceed;\n readonly resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly now?: () => number;\n}\n\n/**\n * Per-instance budget tracker. The runtime creates one tracker per\n * consolidator and resets the day counters lazily on the next phase\n * invocation.\n *\n * @stable\n */\nexport class BudgetTracker {\n readonly #now: () => number;\n readonly #resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly #onExceed: OnBudgetExceed;\n #maxTokensPerDay: number;\n #maxCostPerDay: number;\n #bucketStart: number;\n #tokens = 0;\n #cost = 0;\n #paused = false;\n #pausedReason: 'tokens-exceeded' | 'cost-exceeded' | null = null;\n /**\n * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to\n * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the\n * counters reflect a true rolling window instead of being zeroed on every\n * check (MCON-3). Unused — and never appended to — under `utc` / `local`.\n */\n #ledger: Array<{ at: number; tokens: number; cost: number }> = [];\n\n constructor(opts: BudgetTrackerOptions) {\n this.#now = opts.now ?? Date.now;\n this.#resetSemantics = opts.resetSemantics;\n this.#onExceed = opts.onExceed;\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n /**\n * Replace the active ceilings. Used by `Consolidator.setTier(...)`.\n *\n * @stable\n */\n reconfigure(opts: { maxTokensPerDay: number; maxCostPerDay: number }): void {\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n if (this.#paused) {\n const tokensOk = this.#tokens <= this.#maxTokensPerDay;\n const costOk = this.#cost <= this.#maxCostPerDay;\n if (tokensOk && costOk) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n }\n\n /**\n * Return whether the supplied phase may run right now.\n *\n * @stable\n */\n precheck(phase: ConsolidatorPhase): BudgetCheck {\n this.#maybeReset();\n if (phase === 'light') return { allowed: true };\n if (this.#paused) {\n const reason = this.#pausedReason ?? 'cost-exceeded';\n return { allowed: false, reason };\n }\n if (this.#maxTokensPerDay <= 0) {\n return { allowed: false, reason: 'tokens-exceeded' };\n }\n if (this.#maxCostPerDay <= 0) {\n return { allowed: false, reason: 'cost-exceeded' };\n }\n return { allowed: true };\n }\n\n /**\n * Record consumption. Returns the post-record state — `paused` is\n * `true` when the spend pushed past a ceiling under\n * `onExceed: 'pause'`.\n *\n * Throws {@link BudgetExceededError} when the configured behaviour\n * is `'throw'`.\n *\n * @stable\n */\n record(args: { phase: ConsolidatorPhase; tokens: number; costUsd: number }): BudgetSnapshot {\n this.#maybeReset();\n const tokens = Math.max(0, args.tokens);\n const cost = Math.max(0, args.costUsd);\n if (this.#resetSemantics === 'sliding-24h') {\n this.#ledger.push({ at: this.#now(), tokens, cost });\n }\n this.#tokens += tokens;\n this.#cost += cost;\n if (this.#tokens > this.#maxTokensPerDay) {\n this.#handleBreach(args.phase, 'tokens', this.#tokens, this.#maxTokensPerDay);\n }\n if (this.#cost > this.#maxCostPerDay) {\n this.#handleBreach(args.phase, 'cost', this.#cost, this.#maxCostPerDay);\n }\n return this.snapshot();\n }\n\n /**\n * Read-only snapshot. Surfaced through `Consolidator.status()`.\n *\n * @stable\n */\n snapshot(): BudgetSnapshot {\n this.#maybeReset();\n // For the rolling window the \"reset\" is continuous — the boundary is when\n // the oldest in-window spend ages out (24h after it landed).\n const resetAt =\n this.#resetSemantics === 'sliding-24h'\n ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS\n : nextBucketStart(this.#bucketStart, this.#resetSemantics);\n return Object.freeze({\n tokensUsedToday: this.#tokens,\n costUsedToday: this.#cost,\n tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),\n costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),\n resetAt: new Date(resetAt).toISOString(),\n paused: this.#paused,\n });\n }\n\n /**\n * Force a reset. Used by tests + manual operator action.\n *\n * @stable\n */\n reset(): void {\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#ledger = [];\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n #handleBreach(\n phase: ConsolidatorPhase,\n resource: 'tokens' | 'cost',\n actual: number,\n budget: number,\n ): void {\n if (this.#onExceed === 'throw') {\n throw new BudgetExceededError({ phase, resource, actual, budget });\n }\n if (this.#onExceed === 'pause') {\n this.#paused = true;\n this.#pausedReason = resource === 'tokens' ? 'tokens-exceeded' : 'cost-exceeded';\n }\n }\n\n #maybeReset(): void {\n const now = this.#now();\n if (this.#resetSemantics === 'sliding-24h') {\n // Rolling window: drop spends older than the trailing 24h and recompute\n // the totals from what remains, rather than zeroing on every check.\n const cutoff = now - DAY_MS;\n if (\n this.#ledger.length > 0 &&\n this.#ledger[0] !== undefined &&\n this.#ledger[0].at <= cutoff\n ) {\n this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);\n }\n this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);\n this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);\n // Auto-unpause once the window has dropped back under both ceilings.\n if (\n this.#paused &&\n this.#tokens <= this.#maxTokensPerDay &&\n this.#cost <= this.#maxCostPerDay\n ) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n return;\n }\n // utc / local: zero the counters when the calendar bucket rolls over.\n const currentBucket = bucketStart(now, this.#resetSemantics);\n if (currentBucket !== this.#bucketStart) {\n this.#bucketStart = currentBucket;\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Compute the start of the current bucket (ms epoch) for the\n * supplied reset semantics. UTC midnight is the production default\n * per ADR-038.\n *\n * @internal\n */\nexport function bucketStart(now: number, semantics: 'utc' | 'local' | 'sliding-24h'): number {\n switch (semantics) {\n case 'utc': {\n const d = new Date(now);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());\n }\n case 'local': {\n const d = new Date(now);\n return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n }\n case 'sliding-24h':\n return now;\n }\n}\n\n/**\n * Compute the next reset boundary (ms epoch).\n *\n * @internal\n */\nexport function nextBucketStart(\n bucket: number,\n semantics: 'utc' | 'local' | 'sliding-24h',\n): number {\n switch (semantics) {\n case 'utc':\n case 'local':\n return bucket + DAY_MS;\n case 'sliding-24h':\n return bucket + DAY_MS;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"budget.js","names":["#now","#resetSemantics","#onExceed","#logger","#maxTokensPerDay","#maxCostPerDay","#bucketStart","#paused","#tokens","#cost","#pausedReason","#maybeReset","#ledger","#handleBreach","#warnedThisWindow"],"sources":["../../src/consolidator/budget.ts"],"sourcesContent":["/**\n * Cost-budget enforcement for the consolidator. The tracker is a\n * pure in-memory state machine: it owns the day-bucketed counters\n * (token + cost), the UTC reset semantics, and the `onExceed`\n * dispatch.\n *\n * The tracker does not perform any I/O - operators surface the live\n * counters via `Consolidator.status()` and the persisted snapshot is\n * derived from `consolidator_runs` rows.\n *\n * @packageDocumentation\n */\n\nimport { BudgetExceededError } from './errors.js';\nimport type { ConsolidatorPhase, OnBudgetExceed } from './types.js';\n\n/**\n * @stable\n */\nexport interface BudgetSnapshot {\n readonly tokensUsedToday: number;\n readonly costUsedToday: number;\n readonly tokensRemaining: number;\n readonly costRemaining: number;\n readonly resetAt: string;\n readonly paused: boolean;\n}\n\n/**\n * @stable\n */\nexport interface BudgetCheck {\n readonly allowed: boolean;\n readonly reason?: 'paused' | 'tokens-exceeded' | 'cost-exceeded';\n}\n\n/**\n * @stable\n */\nexport interface BudgetTrackerOptions {\n readonly maxTokensPerDay: number;\n readonly maxCostPerDay: number;\n readonly onExceed: OnBudgetExceed;\n readonly resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly now?: () => number;\n /**\n * Sink for the `onExceed: 'log'` WARN (memory-consolidation-02).\n * Defaults to `process.stderr`. One WARN per resource per budget\n * window - the shipped standard/full presets use `'log'`, so without\n * it a breached ceiling was completely silent.\n */\n readonly logger?: (message: string) => void;\n}\n\n/**\n * Per-instance budget tracker. The runtime creates one tracker per\n * consolidator and resets the day counters lazily on the next phase\n * invocation.\n *\n * @stable\n */\nexport class BudgetTracker {\n readonly #now: () => number;\n readonly #resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly #onExceed: OnBudgetExceed;\n readonly #logger: (message: string) => void;\n #maxTokensPerDay: number;\n #maxCostPerDay: number;\n #bucketStart: number;\n #tokens = 0;\n #cost = 0;\n #paused = false;\n #pausedReason: 'tokens-exceeded' | 'cost-exceeded' | null = null;\n /** Resources already WARNed this window under `onExceed: 'log'` (memory-consolidation-02). */\n #warnedThisWindow = new Set<'tokens' | 'cost'>();\n /**\n * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to\n * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the\n * counters reflect a true rolling window instead of being zeroed on every\n * check (MCON-3). Unused - and never appended to - under `utc` / `local`.\n */\n #ledger: Array<{ at: number; tokens: number; cost: number }> = [];\n\n constructor(opts: BudgetTrackerOptions) {\n this.#now = opts.now ?? Date.now;\n this.#resetSemantics = opts.resetSemantics;\n this.#onExceed = opts.onExceed;\n this.#logger =\n opts.logger ??\n ((message: string): void => {\n process.stderr.write(`${message}\\n`);\n });\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n /**\n * Replace the active ceilings. Used by `Consolidator.setTier(...)`.\n *\n * @stable\n */\n reconfigure(opts: { maxTokensPerDay: number; maxCostPerDay: number }): void {\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n if (this.#paused) {\n const tokensOk = this.#tokens <= this.#maxTokensPerDay;\n const costOk = this.#cost <= this.#maxCostPerDay;\n if (tokensOk && costOk) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n }\n\n /**\n * Return whether the supplied phase may run right now.\n *\n * @stable\n */\n precheck(phase: ConsolidatorPhase): BudgetCheck {\n this.#maybeReset();\n if (phase === 'light') return { allowed: true };\n if (this.#paused) {\n const reason = this.#pausedReason ?? 'cost-exceeded';\n return { allowed: false, reason };\n }\n if (this.#maxTokensPerDay <= 0) {\n return { allowed: false, reason: 'tokens-exceeded' };\n }\n if (this.#maxCostPerDay <= 0) {\n return { allowed: false, reason: 'cost-exceeded' };\n }\n return { allowed: true };\n }\n\n /**\n * Record consumption. Returns the post-record state - `paused` is\n * `true` when the spend pushed past a ceiling under\n * `onExceed: 'pause'`.\n *\n * Throws {@link BudgetExceededError} when the configured behaviour\n * is `'throw'`.\n *\n * @stable\n */\n record(args: { phase: ConsolidatorPhase; tokens: number; costUsd: number }): BudgetSnapshot {\n this.#maybeReset();\n const tokens = Math.max(0, args.tokens);\n const cost = Math.max(0, args.costUsd);\n if (this.#resetSemantics === 'sliding-24h') {\n this.#ledger.push({ at: this.#now(), tokens, cost });\n }\n this.#tokens += tokens;\n this.#cost += cost;\n if (this.#tokens > this.#maxTokensPerDay) {\n this.#handleBreach(args.phase, 'tokens', this.#tokens, this.#maxTokensPerDay);\n }\n if (this.#cost > this.#maxCostPerDay) {\n this.#handleBreach(args.phase, 'cost', this.#cost, this.#maxCostPerDay);\n }\n return this.snapshot();\n }\n\n /**\n * Read-only snapshot. Surfaced through `Consolidator.status()`.\n *\n * @stable\n */\n snapshot(): BudgetSnapshot {\n this.#maybeReset();\n // For the rolling window the \"reset\" is continuous - the boundary is when\n // the oldest in-window spend ages out (24h after it landed).\n const resetAt =\n this.#resetSemantics === 'sliding-24h'\n ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS\n : nextBucketStart(this.#bucketStart, this.#resetSemantics);\n return Object.freeze({\n tokensUsedToday: this.#tokens,\n costUsedToday: this.#cost,\n tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),\n costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),\n resetAt: new Date(resetAt).toISOString(),\n paused: this.#paused,\n });\n }\n\n /**\n * Force a reset. Used by tests + manual operator action.\n *\n * @stable\n */\n reset(): void {\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#ledger = [];\n this.#warnedThisWindow.clear();\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n #handleBreach(\n phase: ConsolidatorPhase,\n resource: 'tokens' | 'cost',\n actual: number,\n budget: number,\n ): void {\n if (this.#onExceed === 'throw') {\n throw new BudgetExceededError({ phase, resource, actual, budget });\n }\n if (this.#onExceed === 'pause') {\n this.#paused = true;\n this.#pausedReason = resource === 'tokens' ? 'tokens-exceeded' : 'cost-exceeded';\n }\n if (this.#onExceed === 'log' && !this.#warnedThisWindow.has(resource)) {\n // memory-consolidation-02: the type doc always promised \"'log'\n // keeps running with a WARN\" - the WARN finally exists. Once per\n // resource per budget window, so a breached daily ceiling is\n // visible without flooding.\n this.#warnedThisWindow.add(resource);\n this.#logger(\n `[graphorin/memory] consolidator ${resource} budget exceeded in phase '${phase}': ` +\n `${resource === 'cost' ? `$${actual.toFixed(4)} > $${budget.toFixed(4)}` : `${actual} > ${budget}`} ` +\n `(onExceed: 'log' - continuing; switch to 'pause'/'throw' to enforce).`,\n );\n }\n }\n\n #maybeReset(): void {\n const now = this.#now();\n if (this.#resetSemantics === 'sliding-24h') {\n // Rolling window: drop spends older than the trailing 24h and recompute\n // the totals from what remains, rather than zeroing on every check.\n const cutoff = now - DAY_MS;\n if (\n this.#ledger.length > 0 &&\n this.#ledger[0] !== undefined &&\n this.#ledger[0].at <= cutoff\n ) {\n this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);\n }\n this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);\n this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);\n // Auto-unpause once the window has dropped back under both ceilings.\n if (\n this.#paused &&\n this.#tokens <= this.#maxTokensPerDay &&\n this.#cost <= this.#maxCostPerDay\n ) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n if (this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) {\n this.#warnedThisWindow.clear();\n }\n return;\n }\n // utc / local: zero the counters when the calendar bucket rolls over.\n const currentBucket = bucketStart(now, this.#resetSemantics);\n if (currentBucket !== this.#bucketStart) {\n this.#bucketStart = currentBucket;\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#warnedThisWindow.clear();\n }\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Compute the start of the current bucket (ms epoch) for the\n * supplied reset semantics. UTC midnight is the production default\n * per ADR-038.\n *\n * @internal\n */\nexport function bucketStart(now: number, semantics: 'utc' | 'local' | 'sliding-24h'): number {\n switch (semantics) {\n case 'utc': {\n const d = new Date(now);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());\n }\n case 'local': {\n const d = new Date(now);\n return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n }\n case 'sliding-24h':\n return now;\n }\n}\n\n/**\n * Compute the next reset boundary (ms epoch).\n *\n * @internal\n */\nexport function nextBucketStart(\n bucket: number,\n semantics: 'utc' | 'local' | 'sliding-24h',\n): number {\n switch (semantics) {\n case 'utc':\n case 'local':\n return bucket + DAY_MS;\n case 'sliding-24h':\n return bucket + DAY_MS;\n }\n}\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ interface SalienceWeights {
16
16
  */
17
17
  readonly importance: number;
18
18
  /**
19
- * Penalty applied to a **quarantined** fact (P1-4) the explicit
19
+ * Penalty applied to a **quarantined** fact (P1-4) - the explicit
20
20
  * security-risk negative term. At the default `0.7`, a quarantined
21
21
  * fact keeps only `0.3` of its retention, so it is evicted first under
22
22
  * capacity pressure. Never a hard delete: the fact is archived,
@@ -24,11 +24,22 @@ interface SalienceWeights {
24
24
  */
25
25
  readonly quarantine: number;
26
26
  /**
27
- * Mild penalty for a fact with non-first-party provenance (P1-4)
27
+ * Mild penalty for a fact with non-first-party provenance (P1-4) -
28
28
  * e.g. `'tool'` / `'imported'` content that did not originate with the
29
29
  * user. At the default `0.2` such a fact keeps `0.8` of its retention.
30
30
  */
31
31
  readonly foreignProvenance: number;
32
+ /**
33
+ * Retrieval-frequency reinforcement (D3) - the use-it-or-lose-it
34
+ * signal. How strongly the monotonic access counter stretches
35
+ * retention: the factor is
36
+ * `1 + weight * min(1, log1p(count) / log1p(saturation))`, saturating
37
+ * at {@link ACCESS_REINFORCEMENT_SATURATION} accesses. At the default
38
+ * `0` the factor is exactly `1` - behaviour is byte-identical until an
39
+ * operator opts in (e.g. `0.3` ⇒ a heavily-used fact keeps up to 1.3x
40
+ * its retention). Optional so existing weight literals stay valid.
41
+ */
42
+ readonly accessReinforcement?: number;
32
43
  }
33
44
  //#endregion
34
45
  export { SalienceWeights };
@@ -3,7 +3,7 @@
3
3
  * Compute the retention score for a fact given its age, last-access
4
4
  * recency, and accumulated `strength`. Larger `strength` means the
5
5
  * fact has been accessed more often, so the retention curve flattens
6
- * `score = base * exp(-elapsedDays / (tau * strength))`.
6
+ * - `score = base * exp(-elapsedDays / (tau * strength))`.
7
7
  *
8
8
  * @stable
9
9
  */
@@ -14,7 +14,7 @@ function retention(args) {
14
14
  return Math.exp(-elapsedDays / tau);
15
15
  }
16
16
  /**
17
- * Neutral importance used when a fact carries no importance hint
17
+ * Neutral importance used when a fact carries no importance hint -
18
18
  * the midpoint of the `[0, 1]` range, so an unscored fact contributes
19
19
  * an importance factor of exactly `1.0` (`salience === retention`).
20
20
  *
@@ -22,6 +22,14 @@ function retention(args) {
22
22
  */
23
23
  const NEUTRAL_IMPORTANCE = .5;
24
24
  /**
25
+ * Access count at which retrieval-frequency reinforcement saturates
26
+ * (D3). `log1p`-scaled, so the first few accesses matter most and
27
+ * anything past this count contributes the full weight.
28
+ *
29
+ * @stable
30
+ */
31
+ const ACCESS_REINFORCEMENT_SATURATION = 32;
32
+ /**
25
33
  * Default {@link SalienceWeights}. Chosen so that an active,
26
34
  * first-party, unscored fact has `salience === retention` (the X-1
27
35
  * change is invisible until a fact carries an importance hint, is
@@ -32,7 +40,8 @@ const NEUTRAL_IMPORTANCE = .5;
32
40
  const DEFAULT_SALIENCE_WEIGHTS = Object.freeze({
33
41
  importance: .6,
34
42
  quarantine: .7,
35
- foreignProvenance: .2
43
+ foreignProvenance: .2,
44
+ accessReinforcement: 0
36
45
  });
37
46
  /**
38
47
  * Multi-signal salience for capacity-bounded forgetting (X-1). Combines
@@ -45,7 +54,7 @@ const DEFAULT_SALIENCE_WEIGHTS = Object.freeze({
45
54
  * ```
46
55
  *
47
56
  * Sold as **cost / staleness control, not accuracy**: it only orders
48
- * what gets archived first when storage is bounded it never gates
57
+ * what gets archived first when storage is bounded - it never gates
49
58
  * recall. With neutral importance, an active fact, and first-party
50
59
  * provenance the factors are all `1`, so `salience === retention`.
51
60
  *
@@ -63,7 +72,10 @@ function salience(args) {
63
72
  const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);
64
73
  const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));
65
74
  const securityFactor = args.quarantined ? Math.max(0, 1 - weights.quarantine) : args.foreignProvenance ? Math.max(0, 1 - weights.foreignProvenance) : 1;
66
- return Math.max(0, base * importanceFactor * securityFactor);
75
+ const reinforcementWeight = weights.accessReinforcement ?? 0;
76
+ const count = Math.max(0, args.accessCount ?? 0);
77
+ const reinforcementFactor = reinforcementWeight === 0 ? 1 : 1 + reinforcementWeight * Math.min(1, Math.log1p(count) / Math.log1p(ACCESS_REINFORCEMENT_SATURATION));
78
+ return Math.max(0, base * importanceFactor * securityFactor * reinforcementFactor);
67
79
  }
68
80
  /**
69
81
  * Capacity-bounded eviction selector (X-1). Given facts scored by
@@ -1 +1 @@
1
- {"version":3,"file":"decay.js","names":["DEFAULT_SALIENCE_WEIGHTS: SalienceWeights"],"sources":["../../src/consolidator/decay.ts"],"sourcesContent":["/**\n * Ebbinghaus retention curve for the light phase. Pure functions so\n * the formula is testable without touching storage.\n *\n * @packageDocumentation\n */\n\n/**\n * Default time constant for the retention curve. The plan calls for\n * `tau = 7` days; consumers may override per-consolidator via\n * `decayTauDays`.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_TAU_DAYS = 7;\n\n/**\n * Default archive threshold — facts whose retention falls below this\n * value are soft-archived in the light phase. `0.05` matches the\n * documented forgetting policy for the memory system.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_ARCHIVE_THRESHOLD = 0.05;\n\n/**\n * Compute the retention score for a fact given its age, last-access\n * recency, and accumulated `strength`. Larger `strength` means the\n * fact has been accessed more often, so the retention curve flattens\n * — `score = base * exp(-elapsedDays / (tau * strength))`.\n *\n * @stable\n */\nexport function retention(args: {\n /** Reference time (ms epoch). */\n readonly now: number;\n /** Last access timestamp (ms epoch); falls back to `createdAt` when null. */\n readonly lastAccessedAt: number | null;\n /** Creation timestamp (ms epoch). */\n readonly createdAt: number;\n /** Strength multiplier (default 1.0). */\n readonly strength: number;\n /** Time constant in days. */\n readonly tauDays: number;\n}): number {\n const reference = args.lastAccessedAt ?? args.createdAt;\n const elapsedMs = Math.max(0, args.now - reference);\n const elapsedDays = elapsedMs / (1000 * 60 * 60 * 24);\n const tau = Math.max(0.1, args.tauDays * Math.max(0.5, args.strength));\n return Math.exp(-elapsedDays / tau);\n}\n\n/**\n * `true` when the fact's retention curve has fallen below the\n * archive threshold.\n *\n * @stable\n */\nexport function shouldArchive(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n readonly archiveThreshold: number;\n}): boolean {\n const score = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n return score < args.archiveThreshold;\n}\n\n/**\n * Neutral importance used when a fact carries no importance hint —\n * the midpoint of the `[0, 1]` range, so an unscored fact contributes\n * an importance factor of exactly `1.0` (`salience === retention`).\n *\n * @stable\n */\nexport const NEUTRAL_IMPORTANCE = 0.5;\n\n/**\n * Tunable weights for the multi-signal {@link salience} score (X-1).\n * Each weight is the *magnitude* of the corresponding signal's pull on\n * the retention curve; all default to values chosen so the ordering is\n * sensible without ever inverting it.\n *\n * @stable\n */\nexport interface SalienceWeights {\n /**\n * How strongly importance (P1-2) stretches retention. At the default\n * `0.6`, importance `1.0` multiplies retention by `1.3` and importance\n * `0.0` by `0.7`; neutral importance leaves it unchanged.\n */\n readonly importance: number;\n /**\n * Penalty applied to a **quarantined** fact (P1-4) — the explicit\n * security-risk negative term. At the default `0.7`, a quarantined\n * fact keeps only `0.3` of its retention, so it is evicted first under\n * capacity pressure. Never a hard delete: the fact is archived,\n * recoverable, and quarantine still gates it out of recall meanwhile.\n */\n readonly quarantine: number;\n /**\n * Mild penalty for a fact with non-first-party provenance (P1-4) —\n * e.g. `'tool'` / `'imported'` content that did not originate with the\n * user. At the default `0.2` such a fact keeps `0.8` of its retention.\n */\n readonly foreignProvenance: number;\n}\n\n/**\n * Default {@link SalienceWeights}. Chosen so that an active,\n * first-party, unscored fact has `salience === retention` (the X-1\n * change is invisible until a fact carries an importance hint, is\n * quarantined, or has foreign provenance).\n *\n * @stable\n */\nexport const DEFAULT_SALIENCE_WEIGHTS: SalienceWeights = Object.freeze({\n importance: 0.6,\n quarantine: 0.7,\n foreignProvenance: 0.2,\n});\n\n/**\n * Multi-signal salience for capacity-bounded forgetting (X-1). Combines\n * the Ebbinghaus {@link retention} curve (temporal relevance + access\n * frequency via `strength`) with the P1-2 importance hint and a P1-4\n * security-risk negative term:\n *\n * ```\n * salience = retention × importanceFactor × securityFactor\n * ```\n *\n * Sold as **cost / staleness control, not accuracy**: it only orders\n * what gets archived first when storage is bounded — it never gates\n * recall. With neutral importance, an active fact, and first-party\n * provenance the factors are all `1`, so `salience === retention`.\n *\n * @stable\n */\nexport function salience(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n /** Importance hint in `[0, 1]`; `null` → {@link NEUTRAL_IMPORTANCE}. */\n readonly importance: number | null;\n /** P1-4: a quarantined fact gets the {@link SalienceWeights.quarantine} penalty. */\n readonly quarantined: boolean;\n /** P1-4: a non-first-party fact gets the {@link SalienceWeights.foreignProvenance} penalty. */\n readonly foreignProvenance: boolean;\n /** Defaults to {@link DEFAULT_SALIENCE_WEIGHTS}. */\n readonly weights?: SalienceWeights;\n}): number {\n const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;\n const base = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);\n const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));\n const securityFactor = args.quarantined\n ? Math.max(0, 1 - weights.quarantine)\n : args.foreignProvenance\n ? Math.max(0, 1 - weights.foreignProvenance)\n : 1;\n return Math.max(0, base * importanceFactor * securityFactor);\n}\n\n/**\n * Capacity-bounded eviction selector (X-1). Given facts scored by\n * {@link salience}, return the ids of the lowest-salience ones to\n * archive so that at most `capacity` remain. Pure and deterministic:\n * ties break by id so a given batch always evicts the same set.\n *\n * Returns `[]` when the batch already fits (`length <= capacity`).\n * `capacity <= 0` evicts the whole batch.\n *\n * @stable\n */\nexport function selectForCapacityEviction(\n scored: ReadonlyArray<{ readonly id: string; readonly salience: number }>,\n capacity: number,\n): ReadonlyArray<string> {\n const evictCount = scored.length - Math.max(0, capacity);\n if (evictCount <= 0) return [];\n return [...scored]\n .sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n .slice(0, evictCount)\n .map((row) => row.id);\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;\n return Math.min(1, Math.max(0, value));\n}\n"],"mappings":";;;;;;;;;AAiCA,SAAgB,UAAU,MAWf;CACT,MAAM,YAAY,KAAK,kBAAkB,KAAK;CAE9C,MAAM,cADY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,IAClB,MAAO,KAAK,KAAK;CAClD,MAAM,MAAM,KAAK,IAAI,IAAK,KAAK,UAAU,KAAK,IAAI,IAAK,KAAK,SAAS,CAAC;AACtE,QAAO,KAAK,IAAI,CAAC,cAAc,IAAI;;;;;;;;;AAkCrC,MAAa,qBAAqB;;;;;;;;;AAyClC,MAAaA,2BAA4C,OAAO,OAAO;CACrE,YAAY;CACZ,YAAY;CACZ,mBAAmB;CACpB,CAAC;;;;;;;;;;;;;;;;;;AAmBF,SAAgB,SAAS,MAcd;CACT,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,OAAO,UAAU;EACrB,KAAK,KAAK;EACV,gBAAgB,KAAK;EACrB,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,SAAS,KAAK;EACf,CAAC;CACF,MAAM,aAAa,QAAQ,KAAK,cAAc,mBAAmB;CACjE,MAAM,mBAAmB,KAAK,IAAI,GAAG,IAAI,QAAQ,cAAc,aAAa,oBAAoB;CAChG,MAAM,iBAAiB,KAAK,cACxB,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW,GACnC,KAAK,oBACH,KAAK,IAAI,GAAG,IAAI,QAAQ,kBAAkB,GAC1C;AACN,QAAO,KAAK,IAAI,GAAG,OAAO,mBAAmB,eAAe;;;;;;;;;;;;;AAc9D,SAAgB,0BACd,QACA,UACuB;CACvB,MAAM,aAAa,OAAO,SAAS,KAAK,IAAI,GAAG,SAAS;AACxD,KAAI,cAAc,EAAG,QAAO,EAAE;AAC9B,QAAO,CAAC,GAAG,OAAO,CACf,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,CACnF,MAAM,GAAG,WAAW,CACpB,KAAK,QAAQ,IAAI,GAAG;;AAGzB,SAAS,QAAQ,OAAuB;AACtC,KAAI,OAAO,MAAM,MAAM,CAAE,QAAO;AAChC,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC"}
1
+ {"version":3,"file":"decay.js","names":["DEFAULT_SALIENCE_WEIGHTS: SalienceWeights"],"sources":["../../src/consolidator/decay.ts"],"sourcesContent":["/**\n * Ebbinghaus retention curve for the light phase. Pure functions so\n * the formula is testable without touching storage.\n *\n * @packageDocumentation\n */\n\n/**\n * Default time constant for the retention curve. The plan calls for\n * `tau = 7` days; consumers may override per-consolidator via\n * `decayTauDays`.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_TAU_DAYS = 7;\n\n/**\n * Default archive threshold - facts whose retention falls below this\n * value are soft-archived in the light phase. `0.05` matches the\n * documented forgetting policy for the memory system.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_ARCHIVE_THRESHOLD = 0.05;\n\n/**\n * Compute the retention score for a fact given its age, last-access\n * recency, and accumulated `strength`. Larger `strength` means the\n * fact has been accessed more often, so the retention curve flattens\n * - `score = base * exp(-elapsedDays / (tau * strength))`.\n *\n * @stable\n */\nexport function retention(args: {\n /** Reference time (ms epoch). */\n readonly now: number;\n /** Last access timestamp (ms epoch); falls back to `createdAt` when null. */\n readonly lastAccessedAt: number | null;\n /** Creation timestamp (ms epoch). */\n readonly createdAt: number;\n /** Strength multiplier (default 1.0). */\n readonly strength: number;\n /** Time constant in days. */\n readonly tauDays: number;\n}): number {\n const reference = args.lastAccessedAt ?? args.createdAt;\n const elapsedMs = Math.max(0, args.now - reference);\n const elapsedDays = elapsedMs / (1000 * 60 * 60 * 24);\n const tau = Math.max(0.1, args.tauDays * Math.max(0.5, args.strength));\n return Math.exp(-elapsedDays / tau);\n}\n\n/**\n * `true` when the fact's retention curve has fallen below the\n * archive threshold.\n *\n * @stable\n */\nexport function shouldArchive(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n readonly archiveThreshold: number;\n}): boolean {\n const score = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n return score < args.archiveThreshold;\n}\n\n/**\n * Neutral importance used when a fact carries no importance hint -\n * the midpoint of the `[0, 1]` range, so an unscored fact contributes\n * an importance factor of exactly `1.0` (`salience === retention`).\n *\n * @stable\n */\nexport const NEUTRAL_IMPORTANCE = 0.5;\n\n/**\n * Tunable weights for the multi-signal {@link salience} score (X-1).\n * Each weight is the *magnitude* of the corresponding signal's pull on\n * the retention curve; all default to values chosen so the ordering is\n * sensible without ever inverting it.\n *\n * @stable\n */\nexport interface SalienceWeights {\n /**\n * How strongly importance (P1-2) stretches retention. At the default\n * `0.6`, importance `1.0` multiplies retention by `1.3` and importance\n * `0.0` by `0.7`; neutral importance leaves it unchanged.\n */\n readonly importance: number;\n /**\n * Penalty applied to a **quarantined** fact (P1-4) - the explicit\n * security-risk negative term. At the default `0.7`, a quarantined\n * fact keeps only `0.3` of its retention, so it is evicted first under\n * capacity pressure. Never a hard delete: the fact is archived,\n * recoverable, and quarantine still gates it out of recall meanwhile.\n */\n readonly quarantine: number;\n /**\n * Mild penalty for a fact with non-first-party provenance (P1-4) -\n * e.g. `'tool'` / `'imported'` content that did not originate with the\n * user. At the default `0.2` such a fact keeps `0.8` of its retention.\n */\n readonly foreignProvenance: number;\n /**\n * Retrieval-frequency reinforcement (D3) - the use-it-or-lose-it\n * signal. How strongly the monotonic access counter stretches\n * retention: the factor is\n * `1 + weight * min(1, log1p(count) / log1p(saturation))`, saturating\n * at {@link ACCESS_REINFORCEMENT_SATURATION} accesses. At the default\n * `0` the factor is exactly `1` - behaviour is byte-identical until an\n * operator opts in (e.g. `0.3` ⇒ a heavily-used fact keeps up to 1.3x\n * its retention). Optional so existing weight literals stay valid.\n */\n readonly accessReinforcement?: number;\n}\n\n/**\n * Access count at which retrieval-frequency reinforcement saturates\n * (D3). `log1p`-scaled, so the first few accesses matter most and\n * anything past this count contributes the full weight.\n *\n * @stable\n */\nexport const ACCESS_REINFORCEMENT_SATURATION = 32;\n\n/**\n * Default {@link SalienceWeights}. Chosen so that an active,\n * first-party, unscored fact has `salience === retention` (the X-1\n * change is invisible until a fact carries an importance hint, is\n * quarantined, or has foreign provenance).\n *\n * @stable\n */\nexport const DEFAULT_SALIENCE_WEIGHTS: SalienceWeights = Object.freeze({\n importance: 0.6,\n quarantine: 0.7,\n foreignProvenance: 0.2,\n // D3: reinforcement is opt-in - the default weight 0 keeps salience\n // byte-identical for every fact regardless of its access count.\n accessReinforcement: 0,\n});\n\n/**\n * Multi-signal salience for capacity-bounded forgetting (X-1). Combines\n * the Ebbinghaus {@link retention} curve (temporal relevance + access\n * frequency via `strength`) with the P1-2 importance hint and a P1-4\n * security-risk negative term:\n *\n * ```\n * salience = retention × importanceFactor × securityFactor\n * ```\n *\n * Sold as **cost / staleness control, not accuracy**: it only orders\n * what gets archived first when storage is bounded - it never gates\n * recall. With neutral importance, an active fact, and first-party\n * provenance the factors are all `1`, so `salience === retention`.\n *\n * @stable\n */\nexport function salience(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n /** Importance hint in `[0, 1]`; `null` → {@link NEUTRAL_IMPORTANCE}. */\n readonly importance: number | null;\n /** P1-4: a quarantined fact gets the {@link SalienceWeights.quarantine} penalty. */\n readonly quarantined: boolean;\n /** P1-4: a non-first-party fact gets the {@link SalienceWeights.foreignProvenance} penalty. */\n readonly foreignProvenance: boolean;\n /**\n * Monotonic retrieval-access count (D3); `null` / absent ⇒ `0`.\n * Contributes only when {@link SalienceWeights.accessReinforcement}\n * is non-zero (the default `0` keeps salience unchanged).\n */\n readonly accessCount?: number | null;\n /** Defaults to {@link DEFAULT_SALIENCE_WEIGHTS}. */\n readonly weights?: SalienceWeights;\n}): number {\n const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;\n const base = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);\n const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));\n const securityFactor = args.quarantined\n ? Math.max(0, 1 - weights.quarantine)\n : args.foreignProvenance\n ? Math.max(0, 1 - weights.foreignProvenance)\n : 1;\n // D3: use-it-or-lose-it reinforcement. log1p-saturating in the access\n // count so early accesses matter most; identity at the default weight 0.\n const reinforcementWeight = weights.accessReinforcement ?? 0;\n const count = Math.max(0, args.accessCount ?? 0);\n const reinforcementFactor =\n reinforcementWeight === 0\n ? 1\n : 1 +\n reinforcementWeight *\n Math.min(1, Math.log1p(count) / Math.log1p(ACCESS_REINFORCEMENT_SATURATION));\n return Math.max(0, base * importanceFactor * securityFactor * reinforcementFactor);\n}\n\n/**\n * Capacity-bounded eviction selector (X-1). Given facts scored by\n * {@link salience}, return the ids of the lowest-salience ones to\n * archive so that at most `capacity` remain. Pure and deterministic:\n * ties break by id so a given batch always evicts the same set.\n *\n * Returns `[]` when the batch already fits (`length <= capacity`).\n * `capacity <= 0` evicts the whole batch.\n *\n * @stable\n */\nexport function selectForCapacityEviction(\n scored: ReadonlyArray<{ readonly id: string; readonly salience: number }>,\n capacity: number,\n): ReadonlyArray<string> {\n const evictCount = scored.length - Math.max(0, capacity);\n if (evictCount <= 0) return [];\n return [...scored]\n .sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n .slice(0, evictCount)\n .map((row) => row.id);\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;\n return Math.min(1, Math.max(0, value));\n}\n"],"mappings":";;;;;;;;;AAiCA,SAAgB,UAAU,MAWf;CACT,MAAM,YAAY,KAAK,kBAAkB,KAAK;CAE9C,MAAM,cADY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,IAClB,MAAO,KAAK,KAAK;CAClD,MAAM,MAAM,KAAK,IAAI,IAAK,KAAK,UAAU,KAAK,IAAI,IAAK,KAAK,SAAS,CAAC;AACtE,QAAO,KAAK,IAAI,CAAC,cAAc,IAAI;;;;;;;;;AAkCrC,MAAa,qBAAqB;;;;;;;;AAmDlC,MAAa,kCAAkC;;;;;;;;;AAU/C,MAAaA,2BAA4C,OAAO,OAAO;CACrE,YAAY;CACZ,YAAY;CACZ,mBAAmB;CAGnB,qBAAqB;CACtB,CAAC;;;;;;;;;;;;;;;;;;AAmBF,SAAgB,SAAS,MAoBd;CACT,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,OAAO,UAAU;EACrB,KAAK,KAAK;EACV,gBAAgB,KAAK;EACrB,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,SAAS,KAAK;EACf,CAAC;CACF,MAAM,aAAa,QAAQ,KAAK,cAAc,mBAAmB;CACjE,MAAM,mBAAmB,KAAK,IAAI,GAAG,IAAI,QAAQ,cAAc,aAAa,oBAAoB;CAChG,MAAM,iBAAiB,KAAK,cACxB,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW,GACnC,KAAK,oBACH,KAAK,IAAI,GAAG,IAAI,QAAQ,kBAAkB,GAC1C;CAGN,MAAM,sBAAsB,QAAQ,uBAAuB;CAC3D,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,eAAe,EAAE;CAChD,MAAM,sBACJ,wBAAwB,IACpB,IACA,IACA,sBACE,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAG,KAAK,MAAM,gCAAgC,CAAC;AACpF,QAAO,KAAK,IAAI,GAAG,OAAO,mBAAmB,iBAAiB,oBAAoB;;;;;;;;;;;;;AAcpF,SAAgB,0BACd,QACA,UACuB;CACvB,MAAM,aAAa,OAAO,SAAS,KAAK,IAAI,GAAG,SAAS;AACxD,KAAI,cAAc,EAAG,QAAO,EAAE;AAC9B,QAAO,CAAC,GAAG,OAAO,CACf,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,CACnF,MAAM,GAAG,WAAW,CACpB,KAAK,QAAQ,IAAI,GAAG;;AAGzB,SAAS,QAAQ,OAAuB;AACtC,KAAI,OAAO,MAAM,MAAM,CAAE,QAAO;AAChC,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC"}
@@ -1,6 +1,6 @@
1
1
  //#region src/consolidator/dlq.ts
2
2
  /**
3
- * Dead-letter queue helpers pure functions used by the standard
3
+ * Dead-letter queue helpers - pure functions used by the standard
4
4
  * + deep phases. The actual persistence lives in the storage
5
5
  * adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dlq.js","names":[],"sources":["../../src/consolidator/dlq.ts"],"sourcesContent":["/**\n * Dead-letter queue helpers pure functions used by the standard\n * + deep phases. The actual persistence lives in the storage\n * adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.\n *\n * @packageDocumentation\n */\n\n/**\n * Compute the next retry delay given the failure count + retry\n * policy. Implements full-jitter exponential backoff capped by\n * `maxBackoffMs`.\n *\n * The formula is `delay = random(0, min(maxBackoffMs, baseMs * 2^retryCount))`\n * where `random` is the optional `jitter` callback (defaults to\n * `Math.random()`).\n *\n * @stable\n */\nexport function nextBackoffMs(args: {\n readonly retryCount: number;\n readonly baseMs: number;\n readonly maxMs: number;\n readonly jitter?: () => number;\n}): number {\n const cap = Math.min(args.maxMs, args.baseMs * 2 ** Math.max(0, args.retryCount));\n const jitter = args.jitter ?? Math.random;\n return Math.max(0, Math.floor(cap * jitter()));\n}\n\n/**\n * Classify a thrown error into the lowercase `error_kind` value\n * recorded against `consolidator_failed_batches`. Used by the phases\n * so the DLQ row carries a stable discriminator instead of free-form\n * prose.\n *\n * @stable\n */\nexport function classifyError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown';\n const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n if (message.includes('rate limit') || message.includes('429')) return 'rate_limit';\n if (message.includes('5xx') || message.includes('500') || message.includes('502')) return '5xx';\n if (message.includes('timeout')) return 'timeout';\n if (message.includes('embedder') || message.includes('embedding')) return 'embedder_failed';\n if (message.includes('invalid') || message.includes('parse')) return 'invalid_response';\n if (message.includes('budget')) return 'budget';\n return 'unknown';\n}\n\n/**\n * Best-effort error message extraction.\n *\n * @internal\n */\nexport function describeError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown error';\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return String(err);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cAAc,MAKnB;CACT,MAAM,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,KAAK,KAAK,IAAI,GAAG,KAAK,WAAW,CAAC;CACjF,MAAM,SAAS,KAAK,UAAU,KAAK;AACnC,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,QAAQ,CAAC,CAAC;;;;;;;;;;AAWhD,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;CAC9C,MAAM,UAAU,eAAe,QAAQ,IAAI,QAAQ,aAAa,GAAG,OAAO,IAAI,CAAC,aAAa;AAC5F,KAAI,QAAQ,SAAS,aAAa,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AACtE,KAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AAC1F,KAAI,QAAQ,SAAS,UAAU,CAAE,QAAO;AACxC,KAAI,QAAQ,SAAS,WAAW,IAAI,QAAQ,SAAS,YAAY,CAAE,QAAO;AAC1E,KAAI,QAAQ,SAAS,UAAU,IAAI,QAAQ,SAAS,QAAQ,CAAE,QAAO;AACrE,KAAI,QAAQ,SAAS,SAAS,CAAE,QAAO;AACvC,QAAO;;;;;;;AAQT,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,KAAI,eAAe,MAAO,QAAO,IAAI;AACrC,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAO,OAAO,IAAI"}
1
+ {"version":3,"file":"dlq.js","names":[],"sources":["../../src/consolidator/dlq.ts"],"sourcesContent":["/**\n * Dead-letter queue helpers - pure functions used by the standard\n * + deep phases. The actual persistence lives in the storage\n * adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.\n *\n * @packageDocumentation\n */\n\n/**\n * Compute the next retry delay given the failure count + retry\n * policy. Implements full-jitter exponential backoff capped by\n * `maxBackoffMs`.\n *\n * The formula is `delay = random(0, min(maxBackoffMs, baseMs * 2^retryCount))`\n * where `random` is the optional `jitter` callback (defaults to\n * `Math.random()`).\n *\n * @stable\n */\nexport function nextBackoffMs(args: {\n readonly retryCount: number;\n readonly baseMs: number;\n readonly maxMs: number;\n readonly jitter?: () => number;\n}): number {\n const cap = Math.min(args.maxMs, args.baseMs * 2 ** Math.max(0, args.retryCount));\n const jitter = args.jitter ?? Math.random;\n return Math.max(0, Math.floor(cap * jitter()));\n}\n\n/**\n * Classify a thrown error into the lowercase `error_kind` value\n * recorded against `consolidator_failed_batches`. Used by the phases\n * so the DLQ row carries a stable discriminator instead of free-form\n * prose.\n *\n * @stable\n */\nexport function classifyError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown';\n const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n if (message.includes('rate limit') || message.includes('429')) return 'rate_limit';\n if (message.includes('5xx') || message.includes('500') || message.includes('502')) return '5xx';\n if (message.includes('timeout')) return 'timeout';\n if (message.includes('embedder') || message.includes('embedding')) return 'embedder_failed';\n if (message.includes('invalid') || message.includes('parse')) return 'invalid_response';\n if (message.includes('budget')) return 'budget';\n return 'unknown';\n}\n\n/**\n * Best-effort error message extraction.\n *\n * @internal\n */\nexport function describeError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown error';\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return String(err);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cAAc,MAKnB;CACT,MAAM,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,KAAK,KAAK,IAAI,GAAG,KAAK,WAAW,CAAC;CACjF,MAAM,SAAS,KAAK,UAAU,KAAK;AACnC,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,QAAQ,CAAC,CAAC;;;;;;;;;;AAWhD,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;CAC9C,MAAM,UAAU,eAAe,QAAQ,IAAI,QAAQ,aAAa,GAAG,OAAO,IAAI,CAAC,aAAa;AAC5F,KAAI,QAAQ,SAAS,aAAa,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AACtE,KAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AAC1F,KAAI,QAAQ,SAAS,UAAU,CAAE,QAAO;AACxC,KAAI,QAAQ,SAAS,WAAW,IAAI,QAAQ,SAAS,YAAY,CAAE,QAAO;AAC1E,KAAI,QAAQ,SAAS,UAAU,IAAI,QAAQ,SAAS,QAAQ,CAAE,QAAO;AACrE,KAAI,QAAQ,SAAS,SAAS,CAAE,QAAO;AACvC,QAAO;;;;;;;AAQT,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,KAAI,eAAe,MAAO,QAAO,IAAI;AACrC,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAO,OAAO,IAAI"}
@@ -22,7 +22,7 @@ var BudgetExceededError = class extends GraphorinMemoryError {
22
22
  actual;
23
23
  resource;
24
24
  constructor(args) {
25
- super(`[graphorin/memory] consolidator budget exceeded phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`, { hint: "Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings." });
25
+ super(`[graphorin/memory] consolidator budget exceeded - phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`, { hint: "Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings." });
26
26
  this.phase = args.phase;
27
27
  this.budget = args.budget;
28
28
  this.actual = args.actual;
@@ -41,7 +41,7 @@ var CustomTierMisconfiguredError = class extends GraphorinMemoryError {
41
41
  kind = "custom-tier-misconfigured";
42
42
  missing;
43
43
  constructor(missing) {
44
- super(`[graphorin/memory] tier='custom' requires explicit ceilings missing: ${missing.join(", ")}`, { hint: "Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled)." });
44
+ super(`[graphorin/memory] tier='custom' requires explicit ceilings - missing: ${missing.join(", ")}`, { hint: "Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled)." });
45
45
  this.missing = Object.freeze([...missing]);
46
46
  }
47
47
  };
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/consolidator/errors.ts"],"sourcesContent":["/**\n * Typed errors raised by the Phase 10c consolidator runtime. Every\n * error carries a stable lowercase `kind` discriminator so downstream\n * tooling can pattern-match without parsing the prose.\n *\n * @packageDocumentation\n */\n\nimport { GraphorinMemoryError } from '../errors/index.js';\nimport type { ConsolidatorPhase } from './types.js';\n\n/**\n * Raised when the consolidator's daily budget envelope is exhausted\n * and `onExceed: 'throw'` is in effect.\n *\n * @stable\n */\nexport class BudgetExceededError extends GraphorinMemoryError {\n override readonly name = 'BudgetExceededError';\n readonly kind = 'budget-exceeded' as const;\n readonly phase: ConsolidatorPhase;\n readonly budget: number;\n readonly actual: number;\n readonly resource: 'tokens' | 'cost';\n\n constructor(args: {\n phase: ConsolidatorPhase;\n budget: number;\n actual: number;\n resource: 'tokens' | 'cost';\n }) {\n super(\n `[graphorin/memory] consolidator budget exceeded phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`,\n { hint: 'Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings.' },\n );\n this.phase = args.phase;\n this.budget = args.budget;\n this.actual = args.actual;\n this.resource = args.resource;\n }\n}\n\n/**\n * Raised when `tier: 'custom'` is selected without explicit\n * `ceilings.maxTokensPerDay` / `ceilings.maxCostPerDay`. Custom is\n * an explicit-only escape hatch.\n *\n * @stable\n */\nexport class CustomTierMisconfiguredError extends GraphorinMemoryError {\n override readonly name = 'CustomTierMisconfiguredError';\n readonly kind = 'custom-tier-misconfigured' as const;\n readonly missing: ReadonlyArray<string>;\n\n constructor(missing: ReadonlyArray<string>) {\n super(\n `[graphorin/memory] tier='custom' requires explicit ceilings missing: ${missing.join(', ')}`,\n {\n hint: 'Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled).',\n },\n );\n this.missing = Object.freeze([...missing]);\n }\n}\n\n/**\n * Raised when the standard / deep phase attempts an LLM call without\n * a configured provider. Surfaces as a typed failure that the DLQ\n * pipeline can attribute correctly.\n *\n * @stable\n */\nexport class ProviderNotConfiguredError extends GraphorinMemoryError {\n override readonly name = 'ProviderNotConfiguredError';\n readonly kind = 'provider-not-configured' as const;\n readonly phase: ConsolidatorPhase;\n\n constructor(phase: ConsolidatorPhase) {\n super(\n `[graphorin/memory] consolidator phase '${phase}' requires a provider but none was supplied.`,\n {\n hint: 'Pass `provider` to createConsolidator({...}) or pin tier=\"free\" to skip LLM phases.',\n },\n );\n this.phase = phase;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAa,sBAAb,cAAyC,qBAAqB;CAC5D,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAKT;AACD,QACE,2DAA2D,KAAK,MAAM,YAAY,KAAK,SAAS,UAAU,KAAK,OAAO,UAAU,KAAK,UACrI,EAAE,MAAM,2EAAyE,CAClF;AACD,OAAK,QAAQ,KAAK;AAClB,OAAK,SAAS,KAAK;AACnB,OAAK,SAAS,KAAK;AACnB,OAAK,WAAW,KAAK;;;;;;;;;;AAWzB,IAAa,+BAAb,cAAkD,qBAAqB;CACrE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,SAAgC;AAC1C,QACE,0EAA0E,QAAQ,KAAK,KAAK,IAC5F,EACE,MAAM,2HACP,CACF;AACD,OAAK,UAAU,OAAO,OAAO,CAAC,GAAG,QAAQ,CAAC;;;;;;;;;;AAW9C,IAAa,6BAAb,cAAgD,qBAAqB;CACnE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,OAA0B;AACpC,QACE,0CAA0C,MAAM,+CAChD,EACE,MAAM,yFACP,CACF;AACD,OAAK,QAAQ"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/consolidator/errors.ts"],"sourcesContent":["/**\n * Typed errors raised by the Phase 10c consolidator runtime. Every\n * error carries a stable lowercase `kind` discriminator so downstream\n * tooling can pattern-match without parsing the prose.\n *\n * @packageDocumentation\n */\n\nimport { GraphorinMemoryError } from '../errors/index.js';\nimport type { ConsolidatorPhase } from './types.js';\n\n/**\n * Raised when the consolidator's daily budget envelope is exhausted\n * and `onExceed: 'throw'` is in effect.\n *\n * @stable\n */\nexport class BudgetExceededError extends GraphorinMemoryError {\n override readonly name = 'BudgetExceededError';\n readonly kind = 'budget-exceeded' as const;\n readonly phase: ConsolidatorPhase;\n readonly budget: number;\n readonly actual: number;\n readonly resource: 'tokens' | 'cost';\n\n constructor(args: {\n phase: ConsolidatorPhase;\n budget: number;\n actual: number;\n resource: 'tokens' | 'cost';\n }) {\n super(\n `[graphorin/memory] consolidator budget exceeded - phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`,\n { hint: 'Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings.' },\n );\n this.phase = args.phase;\n this.budget = args.budget;\n this.actual = args.actual;\n this.resource = args.resource;\n }\n}\n\n/**\n * Raised when `tier: 'custom'` is selected without explicit\n * `ceilings.maxTokensPerDay` / `ceilings.maxCostPerDay`. Custom is\n * an explicit-only escape hatch.\n *\n * @stable\n */\nexport class CustomTierMisconfiguredError extends GraphorinMemoryError {\n override readonly name = 'CustomTierMisconfiguredError';\n readonly kind = 'custom-tier-misconfigured' as const;\n readonly missing: ReadonlyArray<string>;\n\n constructor(missing: ReadonlyArray<string>) {\n super(\n `[graphorin/memory] tier='custom' requires explicit ceilings - missing: ${missing.join(', ')}`,\n {\n hint: 'Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled).',\n },\n );\n this.missing = Object.freeze([...missing]);\n }\n}\n\n/**\n * Raised when the standard / deep phase attempts an LLM call without\n * a configured provider. Surfaces as a typed failure that the DLQ\n * pipeline can attribute correctly.\n *\n * @stable\n */\nexport class ProviderNotConfiguredError extends GraphorinMemoryError {\n override readonly name = 'ProviderNotConfiguredError';\n readonly kind = 'provider-not-configured' as const;\n readonly phase: ConsolidatorPhase;\n\n constructor(phase: ConsolidatorPhase) {\n super(\n `[graphorin/memory] consolidator phase '${phase}' requires a provider but none was supplied.`,\n {\n hint: 'Pass `provider` to createConsolidator({...}) or pin tier=\"free\" to skip LLM phases.',\n },\n );\n this.phase = phase;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAa,sBAAb,cAAyC,qBAAqB;CAC5D,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAKT;AACD,QACE,2DAA2D,KAAK,MAAM,YAAY,KAAK,SAAS,UAAU,KAAK,OAAO,UAAU,KAAK,UACrI,EAAE,MAAM,2EAAyE,CAClF;AACD,OAAK,QAAQ,KAAK;AAClB,OAAK,SAAS,KAAK;AACnB,OAAK,SAAS,KAAK;AACnB,OAAK,WAAW,KAAK;;;;;;;;;;AAWzB,IAAa,+BAAb,cAAkD,qBAAqB;CACrE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,SAAgC;AAC1C,QACE,0EAA0E,QAAQ,KAAK,KAAK,IAC5F,EACE,MAAM,2HACP,CACF;AACD,OAAK,UAAU,OAAO,OAAO,CAAC,GAAG,QAAQ,CAAC;;;;;;;;;;AAW9C,IAAa,6BAAb,cAAgD,qBAAqB;CACnE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,OAA0B;AACpC,QACE,0CAA0C,MAAM,+CAChD,EACE,MAAM,yFACP,CACF;AACD,OAAK,QAAQ"}