@graphorin/memory 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/dist/conflict/index.d.ts +3 -0
  5. package/dist/conflict/index.js +7 -0
  6. package/dist/conflict/locale-packs/en.js +262 -0
  7. package/dist/conflict/locale-packs/en.js.map +1 -0
  8. package/dist/conflict/locale-packs/index.js +4 -0
  9. package/dist/conflict/locale-packs/types.d.ts +103 -0
  10. package/dist/conflict/locale-packs/types.d.ts.map +1 -0
  11. package/dist/conflict/locale-packs/types.js +55 -0
  12. package/dist/conflict/locale-packs/types.js.map +1 -0
  13. package/dist/conflict/pipeline.d.ts +35 -0
  14. package/dist/conflict/pipeline.d.ts.map +1 -0
  15. package/dist/conflict/pipeline.js +257 -0
  16. package/dist/conflict/pipeline.js.map +1 -0
  17. package/dist/conflict/stages/helpers.js +66 -0
  18. package/dist/conflict/stages/helpers.js.map +1 -0
  19. package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
  20. package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
  21. package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
  22. package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
  23. package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
  24. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
  25. package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
  26. package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
  27. package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
  28. package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
  29. package/dist/conflict/types.d.ts +222 -0
  30. package/dist/conflict/types.d.ts.map +1 -0
  31. package/dist/conflict/types.js +50 -0
  32. package/dist/conflict/types.js.map +1 -0
  33. package/dist/consolidator/budget.d.ts +1 -0
  34. package/dist/consolidator/budget.js +214 -0
  35. package/dist/consolidator/budget.js.map +1 -0
  36. package/dist/consolidator/decay.d.ts +35 -0
  37. package/dist/consolidator/decay.d.ts.map +1 -0
  38. package/dist/consolidator/decay.js +91 -0
  39. package/dist/consolidator/decay.js.map +1 -0
  40. package/dist/consolidator/dlq.js +58 -0
  41. package/dist/consolidator/dlq.js.map +1 -0
  42. package/dist/consolidator/errors.d.ts +54 -0
  43. package/dist/consolidator/errors.d.ts.map +1 -0
  44. package/dist/consolidator/errors.js +67 -0
  45. package/dist/consolidator/errors.js.map +1 -0
  46. package/dist/consolidator/idempotency.d.ts +1 -0
  47. package/dist/consolidator/idempotency.js +22 -0
  48. package/dist/consolidator/idempotency.js.map +1 -0
  49. package/dist/consolidator/index.d.ts +34 -0
  50. package/dist/consolidator/index.d.ts.map +1 -0
  51. package/dist/consolidator/index.js +163 -0
  52. package/dist/consolidator/index.js.map +1 -0
  53. package/dist/consolidator/lock.d.ts +2 -0
  54. package/dist/consolidator/lock.js +89 -0
  55. package/dist/consolidator/lock.js.map +1 -0
  56. package/dist/consolidator/noise-filter.d.ts +2 -0
  57. package/dist/consolidator/noise-filter.js +173 -0
  58. package/dist/consolidator/noise-filter.js.map +1 -0
  59. package/dist/consolidator/phases/deep.d.ts +4 -0
  60. package/dist/consolidator/phases/deep.js +198 -0
  61. package/dist/consolidator/phases/deep.js.map +1 -0
  62. package/dist/consolidator/phases/induce.d.ts +154 -0
  63. package/dist/consolidator/phases/induce.d.ts.map +1 -0
  64. package/dist/consolidator/phases/induce.js +253 -0
  65. package/dist/consolidator/phases/induce.js.map +1 -0
  66. package/dist/consolidator/phases/light.d.ts +4 -0
  67. package/dist/consolidator/phases/light.js +99 -0
  68. package/dist/consolidator/phases/light.js.map +1 -0
  69. package/dist/consolidator/phases/reflect.js +287 -0
  70. package/dist/consolidator/phases/reflect.js.map +1 -0
  71. package/dist/consolidator/phases/standard.d.ts +8 -0
  72. package/dist/consolidator/phases/standard.js +499 -0
  73. package/dist/consolidator/phases/standard.js.map +1 -0
  74. package/dist/consolidator/reconcile.js +192 -0
  75. package/dist/consolidator/reconcile.js.map +1 -0
  76. package/dist/consolidator/runtime.d.ts +62 -0
  77. package/dist/consolidator/runtime.d.ts.map +1 -0
  78. package/dist/consolidator/runtime.js +580 -0
  79. package/dist/consolidator/runtime.js.map +1 -0
  80. package/dist/consolidator/scheduler.d.ts +111 -0
  81. package/dist/consolidator/scheduler.d.ts.map +1 -0
  82. package/dist/consolidator/scheduler.js +71 -0
  83. package/dist/consolidator/scheduler.js.map +1 -0
  84. package/dist/consolidator/triggers.d.ts +40 -0
  85. package/dist/consolidator/triggers.d.ts.map +1 -0
  86. package/dist/consolidator/triggers.js +94 -0
  87. package/dist/consolidator/triggers.js.map +1 -0
  88. package/dist/consolidator/types.d.ts +397 -0
  89. package/dist/consolidator/types.d.ts.map +1 -0
  90. package/dist/consolidator/types.js +117 -0
  91. package/dist/consolidator/types.js.map +1 -0
  92. package/dist/context-engine/annotations.d.ts +118 -0
  93. package/dist/context-engine/annotations.d.ts.map +1 -0
  94. package/dist/context-engine/annotations.js +102 -0
  95. package/dist/context-engine/annotations.js.map +1 -0
  96. package/dist/context-engine/auto-recall.d.ts +52 -0
  97. package/dist/context-engine/auto-recall.d.ts.map +1 -0
  98. package/dist/context-engine/auto-recall.js +41 -0
  99. package/dist/context-engine/auto-recall.js.map +1 -0
  100. package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
  101. package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
  102. package/dist/context-engine/compaction/clear-tool-results.js +97 -0
  103. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
  104. package/dist/context-engine/compaction/compactor.d.ts +52 -0
  105. package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
  106. package/dist/context-engine/compaction/compactor.js +196 -0
  107. package/dist/context-engine/compaction/compactor.js.map +1 -0
  108. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
  109. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
  110. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
  111. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
  112. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
  113. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
  114. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
  115. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
  116. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
  117. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
  118. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
  119. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
  120. package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
  121. package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
  122. package/dist/context-engine/compaction/index.d.ts +9 -0
  123. package/dist/context-engine/compaction/index.js +9 -0
  124. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
  125. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
  126. package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
  127. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
  128. package/dist/context-engine/compaction/thresholds.d.ts +68 -0
  129. package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
  130. package/dist/context-engine/compaction/thresholds.js +63 -0
  131. package/dist/context-engine/compaction/thresholds.js.map +1 -0
  132. package/dist/context-engine/compaction/types.d.ts +202 -0
  133. package/dist/context-engine/compaction/types.d.ts.map +1 -0
  134. package/dist/context-engine/engine.d.ts +258 -0
  135. package/dist/context-engine/engine.d.ts.map +1 -0
  136. package/dist/context-engine/engine.js +424 -0
  137. package/dist/context-engine/engine.js.map +1 -0
  138. package/dist/context-engine/index.d.ts +68 -0
  139. package/dist/context-engine/index.d.ts.map +1 -0
  140. package/dist/context-engine/index.js +24 -0
  141. package/dist/context-engine/locale-packs/en.d.ts +14 -0
  142. package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
  143. package/dist/context-engine/locale-packs/en.js +87 -0
  144. package/dist/context-engine/locale-packs/en.js.map +1 -0
  145. package/dist/context-engine/locale-packs/index.js +4 -0
  146. package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
  147. package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
  148. package/dist/context-engine/locale-packs/resolver.js +119 -0
  149. package/dist/context-engine/locale-packs/resolver.js.map +1 -0
  150. package/dist/context-engine/locale-packs/types.d.ts +119 -0
  151. package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
  152. package/dist/context-engine/locale-packs/types.js +24 -0
  153. package/dist/context-engine/locale-packs/types.js.map +1 -0
  154. package/dist/context-engine/metadata.d.ts +40 -0
  155. package/dist/context-engine/metadata.d.ts.map +1 -0
  156. package/dist/context-engine/metadata.js +66 -0
  157. package/dist/context-engine/metadata.js.map +1 -0
  158. package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
  159. package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
  160. package/dist/context-engine/preambles/inbound-en.js +23 -0
  161. package/dist/context-engine/preambles/inbound-en.js.map +1 -0
  162. package/dist/context-engine/privacy-filter.d.ts +108 -0
  163. package/dist/context-engine/privacy-filter.d.ts.map +1 -0
  164. package/dist/context-engine/privacy-filter.js +113 -0
  165. package/dist/context-engine/privacy-filter.js.map +1 -0
  166. package/dist/context-engine/templates/base-en.d.ts +19 -0
  167. package/dist/context-engine/templates/base-en.d.ts.map +1 -0
  168. package/dist/context-engine/templates/base-en.js +22 -0
  169. package/dist/context-engine/templates/base-en.js.map +1 -0
  170. package/dist/context-engine/templates/composer.d.ts +55 -0
  171. package/dist/context-engine/templates/composer.d.ts.map +1 -0
  172. package/dist/context-engine/templates/composer.js +51 -0
  173. package/dist/context-engine/templates/composer.js.map +1 -0
  174. package/dist/context-engine/token-budget.d.ts +99 -0
  175. package/dist/context-engine/token-budget.d.ts.map +1 -0
  176. package/dist/context-engine/token-budget.js +211 -0
  177. package/dist/context-engine/token-budget.js.map +1 -0
  178. package/dist/context-engine/token-counter.d.ts +54 -0
  179. package/dist/context-engine/token-counter.d.ts.map +1 -0
  180. package/dist/context-engine/token-counter.js +81 -0
  181. package/dist/context-engine/token-counter.js.map +1 -0
  182. package/dist/errors/index.d.ts +171 -0
  183. package/dist/errors/index.d.ts.map +1 -0
  184. package/dist/errors/index.js +206 -0
  185. package/dist/errors/index.js.map +1 -0
  186. package/dist/facade.d.ts +278 -0
  187. package/dist/facade.d.ts.map +1 -0
  188. package/dist/facade.js +265 -0
  189. package/dist/facade.js.map +1 -0
  190. package/dist/graph/entity-resolver.d.ts +135 -0
  191. package/dist/graph/entity-resolver.d.ts.map +1 -0
  192. package/dist/graph/entity-resolver.js +202 -0
  193. package/dist/graph/entity-resolver.js.map +1 -0
  194. package/dist/graph/index.d.ts +1 -0
  195. package/dist/index.d.ts +103 -0
  196. package/dist/index.d.ts.map +1 -0
  197. package/dist/index.js +95 -0
  198. package/dist/index.js.map +1 -0
  199. package/dist/internal/contextualize.d.ts +14 -0
  200. package/dist/internal/contextualize.d.ts.map +1 -0
  201. package/dist/internal/contextualize.js +110 -0
  202. package/dist/internal/contextualize.js.map +1 -0
  203. package/dist/internal/embedder-binding.js +50 -0
  204. package/dist/internal/embedder-binding.js.map +1 -0
  205. package/dist/internal/id.js +24 -0
  206. package/dist/internal/id.js.map +1 -0
  207. package/dist/internal/injection-heuristics.js +64 -0
  208. package/dist/internal/injection-heuristics.js.map +1 -0
  209. package/dist/internal/spans.js +35 -0
  210. package/dist/internal/spans.js.map +1 -0
  211. package/dist/internal/storage-adapter.d.ts +742 -0
  212. package/dist/internal/storage-adapter.d.ts.map +1 -0
  213. package/dist/migration/embedder-migration.d.ts +124 -0
  214. package/dist/migration/embedder-migration.d.ts.map +1 -0
  215. package/dist/migration/embedder-migration.js +109 -0
  216. package/dist/migration/embedder-migration.js.map +1 -0
  217. package/dist/migration/index.d.ts +2 -0
  218. package/dist/migration/index.js +3 -0
  219. package/dist/search/explain.d.ts +74 -0
  220. package/dist/search/explain.d.ts.map +1 -0
  221. package/dist/search/explain.js +50 -0
  222. package/dist/search/explain.js.map +1 -0
  223. package/dist/search/index.d.ts +6 -0
  224. package/dist/search/index.js +6 -0
  225. package/dist/search/iterative.d.ts +223 -0
  226. package/dist/search/iterative.d.ts.map +1 -0
  227. package/dist/search/iterative.js +330 -0
  228. package/dist/search/iterative.js.map +1 -0
  229. package/dist/search/query-transform.d.ts +112 -0
  230. package/dist/search/query-transform.d.ts.map +1 -0
  231. package/dist/search/query-transform.js +184 -0
  232. package/dist/search/query-transform.js.map +1 -0
  233. package/dist/search/rrf.d.ts +95 -0
  234. package/dist/search/rrf.d.ts.map +1 -0
  235. package/dist/search/rrf.js +166 -0
  236. package/dist/search/rrf.js.map +1 -0
  237. package/dist/search/types.d.ts +50 -0
  238. package/dist/search/types.d.ts.map +1 -0
  239. package/dist/tiers/episodic-memory.d.ts +142 -0
  240. package/dist/tiers/episodic-memory.d.ts.map +1 -0
  241. package/dist/tiers/episodic-memory.js +254 -0
  242. package/dist/tiers/episodic-memory.js.map +1 -0
  243. package/dist/tiers/index.d.ts +8 -0
  244. package/dist/tiers/index.js +9 -0
  245. package/dist/tiers/insight-memory.d.ts +71 -0
  246. package/dist/tiers/insight-memory.d.ts.map +1 -0
  247. package/dist/tiers/insight-memory.js +133 -0
  248. package/dist/tiers/insight-memory.js.map +1 -0
  249. package/dist/tiers/procedural-memory.d.ts +147 -0
  250. package/dist/tiers/procedural-memory.d.ts.map +1 -0
  251. package/dist/tiers/procedural-memory.js +267 -0
  252. package/dist/tiers/procedural-memory.js.map +1 -0
  253. package/dist/tiers/semantic-memory.d.ts +461 -0
  254. package/dist/tiers/semantic-memory.d.ts.map +1 -0
  255. package/dist/tiers/semantic-memory.js +631 -0
  256. package/dist/tiers/semantic-memory.js.map +1 -0
  257. package/dist/tiers/session-memory.d.ts +117 -0
  258. package/dist/tiers/session-memory.d.ts.map +1 -0
  259. package/dist/tiers/session-memory.js +163 -0
  260. package/dist/tiers/session-memory.js.map +1 -0
  261. package/dist/tiers/shared-memory.d.ts +29 -0
  262. package/dist/tiers/shared-memory.d.ts.map +1 -0
  263. package/dist/tiers/shared-memory.js +58 -0
  264. package/dist/tiers/shared-memory.js.map +1 -0
  265. package/dist/tiers/working-memory.d.ts +129 -0
  266. package/dist/tiers/working-memory.d.ts.map +1 -0
  267. package/dist/tiers/working-memory.js +229 -0
  268. package/dist/tiers/working-memory.js.map +1 -0
  269. package/dist/tools/block-tools.d.ts +98 -0
  270. package/dist/tools/block-tools.d.ts.map +1 -0
  271. package/dist/tools/block-tools.js +114 -0
  272. package/dist/tools/block-tools.js.map +1 -0
  273. package/dist/tools/fact-tools.d.ts +294 -0
  274. package/dist/tools/fact-tools.d.ts.map +1 -0
  275. package/dist/tools/fact-tools.js +293 -0
  276. package/dist/tools/fact-tools.js.map +1 -0
  277. package/dist/tools/index.d.ts +35 -0
  278. package/dist/tools/index.d.ts.map +1 -0
  279. package/dist/tools/index.js +36 -0
  280. package/dist/tools/index.js.map +1 -0
  281. package/dist/tools/recall-tools.d.ts +209 -0
  282. package/dist/tools/recall-tools.d.ts.map +1 -0
  283. package/dist/tools/recall-tools.js +170 -0
  284. package/dist/tools/recall-tools.js.map +1 -0
  285. package/dist/tools/types.d.ts +40 -0
  286. package/dist/tools/types.d.ts.map +1 -0
  287. package/package.json +119 -0
@@ -0,0 +1,222 @@
1
+ import { LocalePack } from "./locale-packs/types.js";
2
+ import { ConflictMemoryStoreExt, MemoryStoreAdapter } from "../internal/storage-adapter.js";
3
+ import { EmbedderProvider, Fact, MemoryHit, Tracer } from "@graphorin/core";
4
+
5
+ //#region src/conflict/types.d.ts
6
+
7
+ /**
8
+ * Stable lowercase identifier of the pipeline stage. Mirrored
9
+ * byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`
10
+ * so `fact_conflicts` / `conflict_check_pending` rows can be grouped
11
+ * deterministically.
12
+ *
13
+ * @stable
14
+ */
15
+ type ConflictStage = 'exact-dedup' | 'embedding-three-zone' | 'heuristic-regex' | 'subject-predicate' | 'defer-to-deep';
16
+ /**
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
19
+ * production values for the default `Xenova/multilingual-e5-base`
20
+ * embedder (DEC-130).
21
+ *
22
+ * @stable
23
+ */
24
+ interface ConflictThresholds {
25
+ readonly hot: number;
26
+ readonly nearDup: number;
27
+ readonly cold: number;
28
+ }
29
+ /**
30
+ * Hard-coded defaults for the three-zone thresholds. Imported by the
31
+ * pipeline + the test suite so call sites stay aligned.
32
+ *
33
+ * @stable
34
+ */
35
+ declare const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds;
36
+ /**
37
+ * Final pipeline outcome — discriminated union returned by
38
+ * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 — every
39
+ * variant carries the originating `stage` so audit + replay tooling
40
+ * can pattern-match without inspecting the message.
41
+ *
42
+ * @stable
43
+ */
44
+ type ConflictDecision = {
45
+ readonly kind: 'admit';
46
+ readonly stage: ConflictStage;
47
+ readonly reason?: string;
48
+ } | {
49
+ readonly kind: 'dedup';
50
+ readonly stage: ConflictStage;
51
+ readonly existingId: string;
52
+ readonly similarity?: number;
53
+ readonly reason?: string;
54
+ } | {
55
+ readonly kind: 'supersede';
56
+ readonly stage: ConflictStage;
57
+ readonly existingId: string;
58
+ readonly reason: string;
59
+ } | {
60
+ readonly kind: 'pending';
61
+ readonly stage: ConflictStage;
62
+ /** The candidate id that was admitted as `pending`. */
63
+ readonly candidateId: string;
64
+ /** Top-K conflicting existing fact ids from Stage 2's vector search. */
65
+ readonly conflictingIds: ReadonlyArray<string>;
66
+ readonly similarity?: number;
67
+ readonly reason?: string;
68
+ };
69
+ /**
70
+ * Pipeline configuration accepted by `createMemory({ conflictPipeline:
71
+ * ... })` and surfaced through {@link createConflictPipeline}.
72
+ *
73
+ * The `mode` field is the master switch:
74
+ *
75
+ * - `'on'` (default) — the multi-stage pipeline runs on every
76
+ * `SemanticMemory.remember(...)` call.
77
+ * - `'off'` — bypass the pipeline and fall back to 10a's straight-
78
+ * through write. Emits a one-shot WARN (per process) so operators
79
+ * notice the regression risk.
80
+ *
81
+ * @stable
82
+ */
83
+ interface ConflictPipelineOptions {
84
+ readonly mode?: 'on' | 'off';
85
+ readonly thresholds?: Partial<ConflictThresholds>;
86
+ readonly localePack?: LocalePack;
87
+ /** Per-list candidate count fed into Stage 2. Default `5` (RB-02 §8.1). */
88
+ readonly stage2TopK?: number;
89
+ /** Override the audit / pending sink. Defaults to `store.conflicts` when present. */
90
+ readonly conflictStore?: ConflictMemoryStoreExt;
91
+ /** Inject a deterministic clock. Defaults to `() => new Date().toISOString()`. */
92
+ readonly now?: () => string;
93
+ }
94
+ /**
95
+ * Inputs the orchestrator hands every stage. The `existing` array is
96
+ * populated during Stage 2 (vector search top-K); Stage 1 receives an
97
+ * empty array because the dedup hash is computed off the candidate
98
+ * alone.
99
+ *
100
+ * @stable
101
+ */
102
+ interface StageContext {
103
+ readonly candidate: Fact;
104
+ readonly existing: ReadonlyArray<MemoryHit<Fact>>;
105
+ readonly localePack: LocalePack;
106
+ readonly thresholds: ConflictThresholds;
107
+ }
108
+ /**
109
+ * Per-stage outcome surfaced to the orchestrator. `'admit'` means the
110
+ * stage decided not to short-circuit — the pipeline continues to the
111
+ * next stage. Every other variant terminates the pipeline.
112
+ *
113
+ * @stable
114
+ */
115
+ type StageOutcome = {
116
+ readonly kind: 'continue';
117
+ } | {
118
+ readonly kind: 'admit';
119
+ readonly reason?: string;
120
+ } | {
121
+ readonly kind: 'dedup';
122
+ readonly existingId: string;
123
+ readonly similarity?: number;
124
+ readonly reason?: string;
125
+ } | {
126
+ readonly kind: 'supersede';
127
+ readonly existingId: string;
128
+ readonly reason: string;
129
+ } | {
130
+ readonly kind: 'pending';
131
+ readonly conflictingIds: ReadonlyArray<string>;
132
+ readonly similarity?: number;
133
+ readonly reason?: string;
134
+ };
135
+ /**
136
+ * Stage interface every step implements. The orchestrator visits
137
+ * stages in declaration order and stops at the first non-`'continue'`
138
+ * outcome.
139
+ *
140
+ * @stable
141
+ */
142
+ interface PipelineStage {
143
+ readonly id: ConflictStage;
144
+ evaluate(ctx: StageContext): Promise<StageOutcome>;
145
+ }
146
+ /**
147
+ * Pre-built pipeline returned by {@link createConflictPipeline}. The
148
+ * `run` method is what `SemanticMemory.remember(...)` calls to make
149
+ * the conflict decision.
150
+ *
151
+ * @stable
152
+ */
153
+ interface ConflictPipeline {
154
+ readonly mode: 'on' | 'off';
155
+ readonly localePack: LocalePack;
156
+ readonly thresholds: ConflictThresholds;
157
+ run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision>;
158
+ }
159
+ /**
160
+ * Per-call dependency bag handed to the pipeline by `SemanticMemory`.
161
+ *
162
+ * @stable
163
+ */
164
+ interface ConflictPipelineDeps {
165
+ readonly store: MemoryStoreAdapter;
166
+ readonly tracer: Tracer;
167
+ readonly embedder: EmbedderProvider | null;
168
+ readonly embedderId: string | null;
169
+ /** Optional cancellation signal forwarded to embedder + searchVector. */
170
+ readonly signal?: AbortSignal;
171
+ }
172
+ /**
173
+ * The four actions the neighbour-aware reconcile loop (P0-3) may choose
174
+ * for a candidate fact once the LLM has the most-similar existing
175
+ * memories in view. The de-facto-standard memory write loop
176
+ * (Mem0 / LangMem / Letta), with Graphorin's twist: `update` and
177
+ * `conflict` route through a **bi-temporal supersede** (close the old
178
+ * interval, insert the new) rather than a destructive delete.
179
+ *
180
+ * @stable
181
+ */
182
+ type ReconcileAction = 'add' | 'update' | 'noop' | 'conflict';
183
+ /**
184
+ * Outcome of {@link reconcileCandidate}. `add` is independent / unsure
185
+ * (a fresh fact); `noop` is a duplicate that adds nothing; `update` is
186
+ * a newer version of `targetId`; `conflict` contradicts `targetId` and
187
+ * closes it. The three neighbour-referencing variants carry the
188
+ * `targetId` of the existing fact they resolve against.
189
+ *
190
+ * @stable
191
+ */
192
+ type ReconcileDecision = {
193
+ readonly action: 'add';
194
+ readonly reason?: string;
195
+ } | {
196
+ readonly action: 'update';
197
+ readonly targetId: string;
198
+ readonly reason: string;
199
+ } | {
200
+ readonly action: 'noop';
201
+ readonly targetId: string;
202
+ readonly reason?: string;
203
+ } | {
204
+ readonly action: 'conflict';
205
+ readonly targetId: string;
206
+ readonly reason: string;
207
+ };
208
+ /**
209
+ * Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}
210
+ * so reconcile outcomes land in `fact_conflicts` through the same audit
211
+ * path as the inline pipeline (no new stage / schema): `add` → `admit`,
212
+ * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
213
+ * decisions are stamped with the `defer-to-deep` stage — the reconcile
214
+ * loop is the consolidator's replacement for the deferred deep-judge
215
+ * step it supersedes.
216
+ *
217
+ * @stable
218
+ */
219
+ declare function reconcileToConflictDecision(decision: ReconcileDecision): ConflictDecision;
220
+ //#endregion
221
+ export { ConflictDecision, ConflictPipeline, ConflictPipelineDeps, ConflictPipelineOptions, ConflictStage, ConflictThresholds, DEFAULT_CONFLICT_THRESHOLDS, PipelineStage, ReconcileAction, ReconcileDecision, StageContext, StageOutcome, reconcileToConflictDecision };
222
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/conflict/types.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAwGA;;AAEwB,KAvFZ,aAAA,GAuFY,aAAA,GAAA,sBAAA,GAAA,iBAAA,GAAA,mBAAA,GAAA,eAAA;;;;AAkBxB;;;;;AAGuB,UA7FN,kBAAA,CA6FM;EACA,SAAA,GAAA,EAAA,MAAA;EAAkB,SAAA,OAAA,EAAA,MAAA;EAU7B,SAAA,IAAA,EAAY,MAAA;AA4BxB;;;;;;AAYA;AAEuB,cAtIV,2BAsIU,EAtImB,kBAsInB;;;;;;;AAUvB;;AAEmB,KApIP,gBAAA,GAoIO;EACE,SAAA,IAAA,EAAA,OAAA;EAGD,SAAA,KAAA,EArIE,aAqIF;EAAW,SAAA,MAAA,CAAA,EAAA,MAAA;AAa/B,CAAA,GAAY;EAWA,SAAA,IAAA,EAAA,OAAiB;EAiBb,SAAA,KAAA,EAzKM,aAyKN;;;;;;kBAlKM;;;;;kBAMA;;;;2BAIS;;;;;;;;;;;;;;;;;;UAmBd,uBAAA;;wBAEO,QAAQ;wBACR;;;;2BAIG;;;;;;;;;;;;UAaV,YAAA;sBACK;qBACD,cAAc,UAAU;uBACtB;uBACA;;;;;;;;;KAUX,YAAA;;;;;;;;;;;;;;;;2BAgBmB;;;;;;;;;;;UAYd,aAAA;eACF;gBACC,eAAe,QAAQ;;;;;;;;;UAUtB,gBAAA;;uBAEM;uBACA;YACX,iCAAiC,OAAO,QAAQ;;;;;;;UAQ3C,oBAAA;kBACC;mBACC;qBACE;;;oBAGD;;;;;;;;;;;;KAaR,eAAA;;;;;;;;;;KAWA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBI,2BAAA,WAAsC,oBAAoB"}
@@ -0,0 +1,50 @@
1
+ //#region src/conflict/types.ts
2
+ /**
3
+ * Hard-coded defaults for the three-zone thresholds. Imported by the
4
+ * pipeline + the test suite so call sites stay aligned.
5
+ *
6
+ * @stable
7
+ */
8
+ const DEFAULT_CONFLICT_THRESHOLDS = Object.freeze({
9
+ hot: .95,
10
+ nearDup: .85,
11
+ cold: .4
12
+ });
13
+ /**
14
+ * Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}
15
+ * so reconcile outcomes land in `fact_conflicts` through the same audit
16
+ * path as the inline pipeline (no new stage / schema): `add` → `admit`,
17
+ * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
18
+ * decisions are stamped with the `defer-to-deep` stage — the reconcile
19
+ * loop is the consolidator's replacement for the deferred deep-judge
20
+ * step it supersedes.
21
+ *
22
+ * @stable
23
+ */
24
+ function reconcileToConflictDecision(decision) {
25
+ const stage = "defer-to-deep";
26
+ switch (decision.action) {
27
+ case "add": return {
28
+ kind: "admit",
29
+ stage,
30
+ ...decision.reason !== void 0 ? { reason: decision.reason } : {}
31
+ };
32
+ case "noop": return {
33
+ kind: "dedup",
34
+ stage,
35
+ existingId: decision.targetId,
36
+ ...decision.reason !== void 0 ? { reason: decision.reason } : {}
37
+ };
38
+ case "update":
39
+ case "conflict": return {
40
+ kind: "supersede",
41
+ stage,
42
+ existingId: decision.targetId,
43
+ reason: decision.reason
44
+ };
45
+ }
46
+ }
47
+
48
+ //#endregion
49
+ export { DEFAULT_CONFLICT_THRESHOLDS, reconcileToConflictDecision };
50
+ //# sourceMappingURL=types.js.map
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ import "./types.js";
@@ -0,0 +1,214 @@
1
+ import { BudgetExceededError } from "./errors.js";
2
+
3
+ //#region src/consolidator/budget.ts
4
+ /**
5
+ * Cost-budget enforcement for the consolidator. The tracker is a
6
+ * pure in-memory state machine: it owns the day-bucketed counters
7
+ * (token + cost), the UTC reset semantics, and the `onExceed`
8
+ * dispatch.
9
+ *
10
+ * The tracker does not perform any I/O — operators surface the live
11
+ * counters via `Consolidator.status()` and the persisted snapshot is
12
+ * derived from `consolidator_runs` rows.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /**
17
+ * Per-instance budget tracker. The runtime creates one tracker per
18
+ * consolidator and resets the day counters lazily on the next phase
19
+ * invocation.
20
+ *
21
+ * @stable
22
+ */
23
+ var BudgetTracker = class {
24
+ #now;
25
+ #resetSemantics;
26
+ #onExceed;
27
+ #maxTokensPerDay;
28
+ #maxCostPerDay;
29
+ #bucketStart;
30
+ #tokens = 0;
31
+ #cost = 0;
32
+ #paused = false;
33
+ #pausedReason = null;
34
+ /**
35
+ * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to
36
+ * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the
37
+ * counters reflect a true rolling window instead of being zeroed on every
38
+ * check (MCON-3). Unused — and never appended to — under `utc` / `local`.
39
+ */
40
+ #ledger = [];
41
+ constructor(opts) {
42
+ this.#now = opts.now ?? Date.now;
43
+ this.#resetSemantics = opts.resetSemantics;
44
+ this.#onExceed = opts.onExceed;
45
+ this.#maxTokensPerDay = opts.maxTokensPerDay;
46
+ this.#maxCostPerDay = opts.maxCostPerDay;
47
+ this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
48
+ }
49
+ /**
50
+ * Replace the active ceilings. Used by `Consolidator.setTier(...)`.
51
+ *
52
+ * @stable
53
+ */
54
+ reconfigure(opts) {
55
+ this.#maxTokensPerDay = opts.maxTokensPerDay;
56
+ this.#maxCostPerDay = opts.maxCostPerDay;
57
+ if (this.#paused) {
58
+ const tokensOk = this.#tokens <= this.#maxTokensPerDay;
59
+ const costOk = this.#cost <= this.#maxCostPerDay;
60
+ if (tokensOk && costOk) {
61
+ this.#paused = false;
62
+ this.#pausedReason = null;
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Return whether the supplied phase may run right now.
68
+ *
69
+ * @stable
70
+ */
71
+ precheck(phase) {
72
+ this.#maybeReset();
73
+ if (phase === "light") return { allowed: true };
74
+ if (this.#paused) return {
75
+ allowed: false,
76
+ reason: this.#pausedReason ?? "cost-exceeded"
77
+ };
78
+ if (this.#maxTokensPerDay <= 0) return {
79
+ allowed: false,
80
+ reason: "tokens-exceeded"
81
+ };
82
+ if (this.#maxCostPerDay <= 0) return {
83
+ allowed: false,
84
+ reason: "cost-exceeded"
85
+ };
86
+ return { allowed: true };
87
+ }
88
+ /**
89
+ * Record consumption. Returns the post-record state — `paused` is
90
+ * `true` when the spend pushed past a ceiling under
91
+ * `onExceed: 'pause'`.
92
+ *
93
+ * Throws {@link BudgetExceededError} when the configured behaviour
94
+ * is `'throw'`.
95
+ *
96
+ * @stable
97
+ */
98
+ record(args) {
99
+ this.#maybeReset();
100
+ const tokens = Math.max(0, args.tokens);
101
+ const cost = Math.max(0, args.costUsd);
102
+ if (this.#resetSemantics === "sliding-24h") this.#ledger.push({
103
+ at: this.#now(),
104
+ tokens,
105
+ cost
106
+ });
107
+ this.#tokens += tokens;
108
+ this.#cost += cost;
109
+ if (this.#tokens > this.#maxTokensPerDay) this.#handleBreach(args.phase, "tokens", this.#tokens, this.#maxTokensPerDay);
110
+ if (this.#cost > this.#maxCostPerDay) this.#handleBreach(args.phase, "cost", this.#cost, this.#maxCostPerDay);
111
+ return this.snapshot();
112
+ }
113
+ /**
114
+ * Read-only snapshot. Surfaced through `Consolidator.status()`.
115
+ *
116
+ * @stable
117
+ */
118
+ snapshot() {
119
+ this.#maybeReset();
120
+ const resetAt = this.#resetSemantics === "sliding-24h" ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS : nextBucketStart(this.#bucketStart, this.#resetSemantics);
121
+ return Object.freeze({
122
+ tokensUsedToday: this.#tokens,
123
+ costUsedToday: this.#cost,
124
+ tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),
125
+ costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),
126
+ resetAt: new Date(resetAt).toISOString(),
127
+ paused: this.#paused
128
+ });
129
+ }
130
+ /**
131
+ * Force a reset. Used by tests + manual operator action.
132
+ *
133
+ * @stable
134
+ */
135
+ reset() {
136
+ this.#tokens = 0;
137
+ this.#cost = 0;
138
+ this.#paused = false;
139
+ this.#pausedReason = null;
140
+ this.#ledger = [];
141
+ this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
142
+ }
143
+ #handleBreach(phase, resource, actual, budget) {
144
+ if (this.#onExceed === "throw") throw new BudgetExceededError({
145
+ phase,
146
+ resource,
147
+ actual,
148
+ budget
149
+ });
150
+ if (this.#onExceed === "pause") {
151
+ this.#paused = true;
152
+ this.#pausedReason = resource === "tokens" ? "tokens-exceeded" : "cost-exceeded";
153
+ }
154
+ }
155
+ #maybeReset() {
156
+ const now = this.#now();
157
+ if (this.#resetSemantics === "sliding-24h") {
158
+ const cutoff = now - DAY_MS;
159
+ if (this.#ledger.length > 0 && this.#ledger[0] !== void 0 && this.#ledger[0].at <= cutoff) this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);
160
+ this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);
161
+ this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);
162
+ if (this.#paused && this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) {
163
+ this.#paused = false;
164
+ this.#pausedReason = null;
165
+ }
166
+ return;
167
+ }
168
+ const currentBucket = bucketStart(now, this.#resetSemantics);
169
+ if (currentBucket !== this.#bucketStart) {
170
+ this.#bucketStart = currentBucket;
171
+ this.#tokens = 0;
172
+ this.#cost = 0;
173
+ this.#paused = false;
174
+ this.#pausedReason = null;
175
+ }
176
+ }
177
+ };
178
+ const DAY_MS = 1440 * 60 * 1e3;
179
+ /**
180
+ * Compute the start of the current bucket (ms epoch) for the
181
+ * supplied reset semantics. UTC midnight is the production default
182
+ * per ADR-038.
183
+ *
184
+ * @internal
185
+ */
186
+ function bucketStart(now, semantics) {
187
+ switch (semantics) {
188
+ case "utc": {
189
+ const d = new Date(now);
190
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
191
+ }
192
+ case "local": {
193
+ const d = new Date(now);
194
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
195
+ }
196
+ case "sliding-24h": return now;
197
+ }
198
+ }
199
+ /**
200
+ * Compute the next reset boundary (ms epoch).
201
+ *
202
+ * @internal
203
+ */
204
+ function nextBucketStart(bucket, semantics) {
205
+ switch (semantics) {
206
+ case "utc":
207
+ case "local": return bucket + DAY_MS;
208
+ case "sliding-24h": return bucket + DAY_MS;
209
+ }
210
+ }
211
+
212
+ //#endregion
213
+ export { BudgetTracker, bucketStart, nextBucketStart };
214
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,35 @@
1
+ //#region src/consolidator/decay.d.ts
2
+
3
+ /**
4
+ * Tunable weights for the multi-signal {@link salience} score (X-1).
5
+ * Each weight is the *magnitude* of the corresponding signal's pull on
6
+ * the retention curve; all default to values chosen so the ordering is
7
+ * sensible without ever inverting it.
8
+ *
9
+ * @stable
10
+ */
11
+ interface SalienceWeights {
12
+ /**
13
+ * How strongly importance (P1-2) stretches retention. At the default
14
+ * `0.6`, importance `1.0` multiplies retention by `1.3` and importance
15
+ * `0.0` by `0.7`; neutral importance leaves it unchanged.
16
+ */
17
+ readonly importance: number;
18
+ /**
19
+ * Penalty applied to a **quarantined** fact (P1-4) — the explicit
20
+ * security-risk negative term. At the default `0.7`, a quarantined
21
+ * fact keeps only `0.3` of its retention, so it is evicted first under
22
+ * capacity pressure. Never a hard delete: the fact is archived,
23
+ * recoverable, and quarantine still gates it out of recall meanwhile.
24
+ */
25
+ readonly quarantine: number;
26
+ /**
27
+ * Mild penalty for a fact with non-first-party provenance (P1-4) —
28
+ * e.g. `'tool'` / `'imported'` content that did not originate with the
29
+ * user. At the default `0.2` such a fact keeps `0.8` of its retention.
30
+ */
31
+ readonly foreignProvenance: number;
32
+ }
33
+ //#endregion
34
+ export { SalienceWeights };
35
+ //# sourceMappingURL=decay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decay.d.ts","names":[],"sources":["../../src/consolidator/decay.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UA6FiB,eAAA"}
@@ -0,0 +1,91 @@
1
+ //#region src/consolidator/decay.ts
2
+ /**
3
+ * Compute the retention score for a fact given its age, last-access
4
+ * recency, and accumulated `strength`. Larger `strength` means the
5
+ * fact has been accessed more often, so the retention curve flattens
6
+ * — `score = base * exp(-elapsedDays / (tau * strength))`.
7
+ *
8
+ * @stable
9
+ */
10
+ function retention(args) {
11
+ const reference = args.lastAccessedAt ?? args.createdAt;
12
+ const elapsedDays = Math.max(0, args.now - reference) / (1e3 * 60 * 60 * 24);
13
+ const tau = Math.max(.1, args.tauDays * Math.max(.5, args.strength));
14
+ return Math.exp(-elapsedDays / tau);
15
+ }
16
+ /**
17
+ * Neutral importance used when a fact carries no importance hint —
18
+ * the midpoint of the `[0, 1]` range, so an unscored fact contributes
19
+ * an importance factor of exactly `1.0` (`salience === retention`).
20
+ *
21
+ * @stable
22
+ */
23
+ const NEUTRAL_IMPORTANCE = .5;
24
+ /**
25
+ * Default {@link SalienceWeights}. Chosen so that an active,
26
+ * first-party, unscored fact has `salience === retention` (the X-1
27
+ * change is invisible until a fact carries an importance hint, is
28
+ * quarantined, or has foreign provenance).
29
+ *
30
+ * @stable
31
+ */
32
+ const DEFAULT_SALIENCE_WEIGHTS = Object.freeze({
33
+ importance: .6,
34
+ quarantine: .7,
35
+ foreignProvenance: .2
36
+ });
37
+ /**
38
+ * Multi-signal salience for capacity-bounded forgetting (X-1). Combines
39
+ * the Ebbinghaus {@link retention} curve (temporal relevance + access
40
+ * frequency via `strength`) with the P1-2 importance hint and a P1-4
41
+ * security-risk negative term:
42
+ *
43
+ * ```
44
+ * salience = retention × importanceFactor × securityFactor
45
+ * ```
46
+ *
47
+ * Sold as **cost / staleness control, not accuracy**: it only orders
48
+ * what gets archived first when storage is bounded — it never gates
49
+ * recall. With neutral importance, an active fact, and first-party
50
+ * provenance the factors are all `1`, so `salience === retention`.
51
+ *
52
+ * @stable
53
+ */
54
+ function salience(args) {
55
+ const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;
56
+ const base = retention({
57
+ now: args.now,
58
+ lastAccessedAt: args.lastAccessedAt,
59
+ createdAt: args.createdAt,
60
+ strength: args.strength,
61
+ tauDays: args.tauDays
62
+ });
63
+ const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);
64
+ const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));
65
+ 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);
67
+ }
68
+ /**
69
+ * Capacity-bounded eviction selector (X-1). Given facts scored by
70
+ * {@link salience}, return the ids of the lowest-salience ones to
71
+ * archive so that at most `capacity` remain. Pure and deterministic:
72
+ * ties break by id so a given batch always evicts the same set.
73
+ *
74
+ * Returns `[]` when the batch already fits (`length <= capacity`).
75
+ * `capacity <= 0` evicts the whole batch.
76
+ *
77
+ * @stable
78
+ */
79
+ function selectForCapacityEviction(scored, capacity) {
80
+ const evictCount = scored.length - Math.max(0, capacity);
81
+ if (evictCount <= 0) return [];
82
+ return [...scored].sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).slice(0, evictCount).map((row) => row.id);
83
+ }
84
+ function clamp01(value) {
85
+ if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;
86
+ return Math.min(1, Math.max(0, value));
87
+ }
88
+
89
+ //#endregion
90
+ export { DEFAULT_SALIENCE_WEIGHTS, NEUTRAL_IMPORTANCE, retention, salience, selectForCapacityEviction };
91
+ //# sourceMappingURL=decay.js.map