@graphorin/memory 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +16 -13
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -5
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +1 -1
  89. package/dist/package.js.map +1 -1
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Cost-budget enforcement for the consolidator. The tracker is a
3
+ * pure in-memory state machine: it owns the day-bucketed counters
4
+ * (token + cost), the UTC reset semantics, and the `onExceed`
5
+ * dispatch.
6
+ *
7
+ * The tracker does not perform any I/O - operators surface the live
8
+ * counters via `Consolidator.status()` and the persisted snapshot is
9
+ * derived from `consolidator_runs` rows.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import { BudgetExceededError } from './errors.js';
15
+ import type { ConsolidatorPhase, OnBudgetExceed } from './types.js';
16
+
17
+ /**
18
+ * @stable
19
+ */
20
+ export interface BudgetSnapshot {
21
+ readonly tokensUsedToday: number;
22
+ readonly costUsedToday: number;
23
+ readonly tokensRemaining: number;
24
+ readonly costRemaining: number;
25
+ readonly resetAt: string;
26
+ readonly paused: boolean;
27
+ }
28
+
29
+ /**
30
+ * @stable
31
+ */
32
+ export interface BudgetCheck {
33
+ readonly allowed: boolean;
34
+ readonly reason?: 'paused' | 'tokens-exceeded' | 'cost-exceeded';
35
+ }
36
+
37
+ /**
38
+ * @stable
39
+ */
40
+ export interface BudgetTrackerOptions {
41
+ readonly maxTokensPerDay: number;
42
+ readonly maxCostPerDay: number;
43
+ readonly onExceed: OnBudgetExceed;
44
+ readonly resetSemantics: 'utc' | 'local' | 'sliding-24h';
45
+ readonly now?: () => number;
46
+ /**
47
+ * Sink for the `onExceed: 'log'` WARN (memory-consolidation-02).
48
+ * Defaults to `process.stderr`. One WARN per resource per budget
49
+ * window - the shipped standard/full presets use `'log'`, so without
50
+ * it a breached ceiling was completely silent.
51
+ */
52
+ readonly logger?: (message: string) => void;
53
+ }
54
+
55
+ /**
56
+ * Per-instance budget tracker. The runtime creates one tracker per
57
+ * consolidator and resets the day counters lazily on the next phase
58
+ * invocation.
59
+ *
60
+ * @stable
61
+ */
62
+ export class BudgetTracker {
63
+ readonly #now: () => number;
64
+ readonly #resetSemantics: 'utc' | 'local' | 'sliding-24h';
65
+ readonly #onExceed: OnBudgetExceed;
66
+ readonly #logger: (message: string) => void;
67
+ #maxTokensPerDay: number;
68
+ #maxCostPerDay: number;
69
+ #bucketStart: number;
70
+ #tokens = 0;
71
+ #cost = 0;
72
+ #paused = false;
73
+ #pausedReason: 'tokens-exceeded' | 'cost-exceeded' | null = null;
74
+ /** Resources already WARNed this window under `onExceed: 'log'` (memory-consolidation-02). */
75
+ #warnedThisWindow = new Set<'tokens' | 'cost'>();
76
+ /**
77
+ * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to
78
+ * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the
79
+ * counters reflect a true rolling window instead of being zeroed on every
80
+ * check (MCON-3). Unused - and never appended to - under `utc` / `local`.
81
+ */
82
+ #ledger: Array<{ at: number; tokens: number; cost: number }> = [];
83
+
84
+ constructor(opts: BudgetTrackerOptions) {
85
+ this.#now = opts.now ?? Date.now;
86
+ this.#resetSemantics = opts.resetSemantics;
87
+ this.#onExceed = opts.onExceed;
88
+ this.#logger =
89
+ opts.logger ??
90
+ ((message: string): void => {
91
+ process.stderr.write(`${message}\n`);
92
+ });
93
+ this.#maxTokensPerDay = opts.maxTokensPerDay;
94
+ this.#maxCostPerDay = opts.maxCostPerDay;
95
+ this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
96
+ }
97
+
98
+ /**
99
+ * Replace the active ceilings. Used by `Consolidator.setTier(...)`.
100
+ *
101
+ * @stable
102
+ */
103
+ reconfigure(opts: { maxTokensPerDay: number; maxCostPerDay: number }): void {
104
+ this.#maxTokensPerDay = opts.maxTokensPerDay;
105
+ this.#maxCostPerDay = opts.maxCostPerDay;
106
+ if (this.#paused) {
107
+ const tokensOk = this.#tokens <= this.#maxTokensPerDay;
108
+ const costOk = this.#cost <= this.#maxCostPerDay;
109
+ if (tokensOk && costOk) {
110
+ this.#paused = false;
111
+ this.#pausedReason = null;
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Return whether the supplied phase may run right now.
118
+ *
119
+ * @stable
120
+ */
121
+ precheck(phase: ConsolidatorPhase): BudgetCheck {
122
+ this.#maybeReset();
123
+ if (phase === 'light') return { allowed: true };
124
+ if (this.#paused) {
125
+ const reason = this.#pausedReason ?? 'cost-exceeded';
126
+ return { allowed: false, reason };
127
+ }
128
+ if (this.#maxTokensPerDay <= 0) {
129
+ return { allowed: false, reason: 'tokens-exceeded' };
130
+ }
131
+ if (this.#maxCostPerDay <= 0) {
132
+ return { allowed: false, reason: 'cost-exceeded' };
133
+ }
134
+ return { allowed: true };
135
+ }
136
+
137
+ /**
138
+ * Record consumption. Returns the post-record state - `paused` is
139
+ * `true` when the spend pushed past a ceiling under
140
+ * `onExceed: 'pause'`.
141
+ *
142
+ * Throws {@link BudgetExceededError} when the configured behaviour
143
+ * is `'throw'`.
144
+ *
145
+ * @stable
146
+ */
147
+ record(args: { phase: ConsolidatorPhase; tokens: number; costUsd: number }): BudgetSnapshot {
148
+ this.#maybeReset();
149
+ const tokens = Math.max(0, args.tokens);
150
+ const cost = Math.max(0, args.costUsd);
151
+ if (this.#resetSemantics === 'sliding-24h') {
152
+ this.#ledger.push({ at: this.#now(), tokens, cost });
153
+ }
154
+ this.#tokens += tokens;
155
+ this.#cost += cost;
156
+ if (this.#tokens > this.#maxTokensPerDay) {
157
+ this.#handleBreach(args.phase, 'tokens', this.#tokens, this.#maxTokensPerDay);
158
+ }
159
+ if (this.#cost > this.#maxCostPerDay) {
160
+ this.#handleBreach(args.phase, 'cost', this.#cost, this.#maxCostPerDay);
161
+ }
162
+ return this.snapshot();
163
+ }
164
+
165
+ /**
166
+ * Read-only snapshot. Surfaced through `Consolidator.status()`.
167
+ *
168
+ * @stable
169
+ */
170
+ snapshot(): BudgetSnapshot {
171
+ this.#maybeReset();
172
+ // For the rolling window the "reset" is continuous - the boundary is when
173
+ // the oldest in-window spend ages out (24h after it landed).
174
+ const resetAt =
175
+ this.#resetSemantics === 'sliding-24h'
176
+ ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS
177
+ : nextBucketStart(this.#bucketStart, this.#resetSemantics);
178
+ return Object.freeze({
179
+ tokensUsedToday: this.#tokens,
180
+ costUsedToday: this.#cost,
181
+ tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),
182
+ costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),
183
+ resetAt: new Date(resetAt).toISOString(),
184
+ paused: this.#paused,
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Force a reset. Used by tests + manual operator action.
190
+ *
191
+ * @stable
192
+ */
193
+ reset(): void {
194
+ this.#tokens = 0;
195
+ this.#cost = 0;
196
+ this.#paused = false;
197
+ this.#pausedReason = null;
198
+ this.#ledger = [];
199
+ this.#warnedThisWindow.clear();
200
+ this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
201
+ }
202
+
203
+ #handleBreach(
204
+ phase: ConsolidatorPhase,
205
+ resource: 'tokens' | 'cost',
206
+ actual: number,
207
+ budget: number,
208
+ ): void {
209
+ if (this.#onExceed === 'throw') {
210
+ throw new BudgetExceededError({ phase, resource, actual, budget });
211
+ }
212
+ if (this.#onExceed === 'pause') {
213
+ this.#paused = true;
214
+ this.#pausedReason = resource === 'tokens' ? 'tokens-exceeded' : 'cost-exceeded';
215
+ }
216
+ if (this.#onExceed === 'log' && !this.#warnedThisWindow.has(resource)) {
217
+ // memory-consolidation-02: the type doc always promised "'log'
218
+ // keeps running with a WARN" - the WARN finally exists. Once per
219
+ // resource per budget window, so a breached daily ceiling is
220
+ // visible without flooding.
221
+ this.#warnedThisWindow.add(resource);
222
+ this.#logger(
223
+ `[graphorin/memory] consolidator ${resource} budget exceeded in phase '${phase}': ` +
224
+ `${resource === 'cost' ? `$${actual.toFixed(4)} > $${budget.toFixed(4)}` : `${actual} > ${budget}`} ` +
225
+ `(onExceed: 'log' - continuing; switch to 'pause'/'throw' to enforce).`,
226
+ );
227
+ }
228
+ }
229
+
230
+ #maybeReset(): void {
231
+ const now = this.#now();
232
+ if (this.#resetSemantics === 'sliding-24h') {
233
+ // Rolling window: drop spends older than the trailing 24h and recompute
234
+ // the totals from what remains, rather than zeroing on every check.
235
+ const cutoff = now - DAY_MS;
236
+ if (
237
+ this.#ledger.length > 0 &&
238
+ this.#ledger[0] !== undefined &&
239
+ this.#ledger[0].at <= cutoff
240
+ ) {
241
+ this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);
242
+ }
243
+ this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);
244
+ this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);
245
+ // Auto-unpause once the window has dropped back under both ceilings.
246
+ if (
247
+ this.#paused &&
248
+ this.#tokens <= this.#maxTokensPerDay &&
249
+ this.#cost <= this.#maxCostPerDay
250
+ ) {
251
+ this.#paused = false;
252
+ this.#pausedReason = null;
253
+ }
254
+ if (this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) {
255
+ this.#warnedThisWindow.clear();
256
+ }
257
+ return;
258
+ }
259
+ // utc / local: zero the counters when the calendar bucket rolls over.
260
+ const currentBucket = bucketStart(now, this.#resetSemantics);
261
+ if (currentBucket !== this.#bucketStart) {
262
+ this.#bucketStart = currentBucket;
263
+ this.#tokens = 0;
264
+ this.#cost = 0;
265
+ this.#paused = false;
266
+ this.#pausedReason = null;
267
+ this.#warnedThisWindow.clear();
268
+ }
269
+ }
270
+ }
271
+
272
+ const DAY_MS = 24 * 60 * 60 * 1000;
273
+
274
+ /**
275
+ * Compute the start of the current bucket (ms epoch) for the
276
+ * supplied reset semantics. UTC midnight is the production default
277
+ * per ADR-038.
278
+ *
279
+ * @internal
280
+ */
281
+ export function bucketStart(now: number, semantics: 'utc' | 'local' | 'sliding-24h'): number {
282
+ switch (semantics) {
283
+ case 'utc': {
284
+ const d = new Date(now);
285
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
286
+ }
287
+ case 'local': {
288
+ const d = new Date(now);
289
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
290
+ }
291
+ case 'sliding-24h':
292
+ return now;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Compute the next reset boundary (ms epoch).
298
+ *
299
+ * @internal
300
+ */
301
+ export function nextBucketStart(
302
+ bucket: number,
303
+ semantics: 'utc' | 'local' | 'sliding-24h',
304
+ ): number {
305
+ switch (semantics) {
306
+ case 'utc':
307
+ case 'local':
308
+ return bucket + DAY_MS;
309
+ case 'sliding-24h':
310
+ return bucket + DAY_MS;
311
+ }
312
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Ebbinghaus retention curve for the light phase. Pure functions so
3
+ * the formula is testable without touching storage.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ /**
9
+ * Default time constant for the retention curve. The plan calls for
10
+ * `tau = 7` days; consumers may override per-consolidator via
11
+ * `decayTauDays`.
12
+ *
13
+ * @stable
14
+ */
15
+ export const DEFAULT_DECAY_TAU_DAYS = 7;
16
+
17
+ /**
18
+ * Default archive threshold - facts whose retention falls below this
19
+ * value are soft-archived in the light phase. `0.05` matches the
20
+ * documented forgetting policy for the memory system.
21
+ *
22
+ * @stable
23
+ */
24
+ export const DEFAULT_DECAY_ARCHIVE_THRESHOLD = 0.05;
25
+
26
+ /**
27
+ * Compute the retention score for a fact given its age, last-access
28
+ * recency, and accumulated `strength`. Larger `strength` means the
29
+ * fact has been accessed more often, so the retention curve flattens
30
+ * - `score = base * exp(-elapsedDays / (tau * strength))`.
31
+ *
32
+ * @stable
33
+ */
34
+ export function retention(args: {
35
+ /** Reference time (ms epoch). */
36
+ readonly now: number;
37
+ /** Last access timestamp (ms epoch); falls back to `createdAt` when null. */
38
+ readonly lastAccessedAt: number | null;
39
+ /** Creation timestamp (ms epoch). */
40
+ readonly createdAt: number;
41
+ /** Strength multiplier (default 1.0). */
42
+ readonly strength: number;
43
+ /** Time constant in days. */
44
+ readonly tauDays: number;
45
+ }): number {
46
+ const reference = args.lastAccessedAt ?? args.createdAt;
47
+ const elapsedMs = Math.max(0, args.now - reference);
48
+ const elapsedDays = elapsedMs / (1000 * 60 * 60 * 24);
49
+ const tau = Math.max(0.1, args.tauDays * Math.max(0.5, args.strength));
50
+ return Math.exp(-elapsedDays / tau);
51
+ }
52
+
53
+ /**
54
+ * `true` when the fact's retention curve has fallen below the
55
+ * archive threshold.
56
+ *
57
+ * @stable
58
+ */
59
+ export function shouldArchive(args: {
60
+ readonly now: number;
61
+ readonly lastAccessedAt: number | null;
62
+ readonly createdAt: number;
63
+ readonly strength: number;
64
+ readonly tauDays: number;
65
+ readonly archiveThreshold: number;
66
+ }): boolean {
67
+ const score = retention({
68
+ now: args.now,
69
+ lastAccessedAt: args.lastAccessedAt,
70
+ createdAt: args.createdAt,
71
+ strength: args.strength,
72
+ tauDays: args.tauDays,
73
+ });
74
+ return score < args.archiveThreshold;
75
+ }
76
+
77
+ /**
78
+ * Neutral importance used when a fact carries no importance hint -
79
+ * the midpoint of the `[0, 1]` range, so an unscored fact contributes
80
+ * an importance factor of exactly `1.0` (`salience === retention`).
81
+ *
82
+ * @stable
83
+ */
84
+ export const NEUTRAL_IMPORTANCE = 0.5;
85
+
86
+ /**
87
+ * Tunable weights for the multi-signal {@link salience} score (X-1).
88
+ * Each weight is the *magnitude* of the corresponding signal's pull on
89
+ * the retention curve; all default to values chosen so the ordering is
90
+ * sensible without ever inverting it.
91
+ *
92
+ * @stable
93
+ */
94
+ export interface SalienceWeights {
95
+ /**
96
+ * How strongly importance (P1-2) stretches retention. At the default
97
+ * `0.6`, importance `1.0` multiplies retention by `1.3` and importance
98
+ * `0.0` by `0.7`; neutral importance leaves it unchanged.
99
+ */
100
+ readonly importance: number;
101
+ /**
102
+ * Penalty applied to a **quarantined** fact (P1-4) - the explicit
103
+ * security-risk negative term. At the default `0.7`, a quarantined
104
+ * fact keeps only `0.3` of its retention, so it is evicted first under
105
+ * capacity pressure. Never a hard delete: the fact is archived,
106
+ * recoverable, and quarantine still gates it out of recall meanwhile.
107
+ */
108
+ readonly quarantine: number;
109
+ /**
110
+ * Mild penalty for a fact with non-first-party provenance (P1-4) -
111
+ * e.g. `'tool'` / `'imported'` content that did not originate with the
112
+ * user. At the default `0.2` such a fact keeps `0.8` of its retention.
113
+ */
114
+ readonly foreignProvenance: number;
115
+ /**
116
+ * Retrieval-frequency reinforcement (D3) - the use-it-or-lose-it
117
+ * signal. How strongly the monotonic access counter stretches
118
+ * retention: the factor is
119
+ * `1 + weight * min(1, log1p(count) / log1p(saturation))`, saturating
120
+ * at {@link ACCESS_REINFORCEMENT_SATURATION} accesses. At the default
121
+ * `0` the factor is exactly `1` - behaviour is byte-identical until an
122
+ * operator opts in (e.g. `0.3` ⇒ a heavily-used fact keeps up to 1.3x
123
+ * its retention). Optional so existing weight literals stay valid.
124
+ */
125
+ readonly accessReinforcement?: number;
126
+ }
127
+
128
+ /**
129
+ * Access count at which retrieval-frequency reinforcement saturates
130
+ * (D3). `log1p`-scaled, so the first few accesses matter most and
131
+ * anything past this count contributes the full weight.
132
+ *
133
+ * @stable
134
+ */
135
+ export const ACCESS_REINFORCEMENT_SATURATION = 32;
136
+
137
+ /**
138
+ * Default {@link SalienceWeights}. Chosen so that an active,
139
+ * first-party, unscored fact has `salience === retention` (the X-1
140
+ * change is invisible until a fact carries an importance hint, is
141
+ * quarantined, or has foreign provenance).
142
+ *
143
+ * @stable
144
+ */
145
+ export const DEFAULT_SALIENCE_WEIGHTS: SalienceWeights = Object.freeze({
146
+ importance: 0.6,
147
+ quarantine: 0.7,
148
+ foreignProvenance: 0.2,
149
+ // D3: reinforcement is opt-in - the default weight 0 keeps salience
150
+ // byte-identical for every fact regardless of its access count.
151
+ accessReinforcement: 0,
152
+ });
153
+
154
+ /**
155
+ * Multi-signal salience for capacity-bounded forgetting (X-1). Combines
156
+ * the Ebbinghaus {@link retention} curve (temporal relevance + access
157
+ * frequency via `strength`) with the P1-2 importance hint and a P1-4
158
+ * security-risk negative term:
159
+ *
160
+ * ```
161
+ * salience = retention × importanceFactor × securityFactor
162
+ * ```
163
+ *
164
+ * Sold as **cost / staleness control, not accuracy**: it only orders
165
+ * what gets archived first when storage is bounded - it never gates
166
+ * recall. With neutral importance, an active fact, and first-party
167
+ * provenance the factors are all `1`, so `salience === retention`.
168
+ *
169
+ * @stable
170
+ */
171
+ export function salience(args: {
172
+ readonly now: number;
173
+ readonly lastAccessedAt: number | null;
174
+ readonly createdAt: number;
175
+ readonly strength: number;
176
+ readonly tauDays: number;
177
+ /** Importance hint in `[0, 1]`; `null` → {@link NEUTRAL_IMPORTANCE}. */
178
+ readonly importance: number | null;
179
+ /** P1-4: a quarantined fact gets the {@link SalienceWeights.quarantine} penalty. */
180
+ readonly quarantined: boolean;
181
+ /** P1-4: a non-first-party fact gets the {@link SalienceWeights.foreignProvenance} penalty. */
182
+ readonly foreignProvenance: boolean;
183
+ /**
184
+ * Monotonic retrieval-access count (D3); `null` / absent ⇒ `0`.
185
+ * Contributes only when {@link SalienceWeights.accessReinforcement}
186
+ * is non-zero (the default `0` keeps salience unchanged).
187
+ */
188
+ readonly accessCount?: number | null;
189
+ /** Defaults to {@link DEFAULT_SALIENCE_WEIGHTS}. */
190
+ readonly weights?: SalienceWeights;
191
+ }): number {
192
+ const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;
193
+ const base = retention({
194
+ now: args.now,
195
+ lastAccessedAt: args.lastAccessedAt,
196
+ createdAt: args.createdAt,
197
+ strength: args.strength,
198
+ tauDays: args.tauDays,
199
+ });
200
+ const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);
201
+ const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));
202
+ const securityFactor = args.quarantined
203
+ ? Math.max(0, 1 - weights.quarantine)
204
+ : args.foreignProvenance
205
+ ? Math.max(0, 1 - weights.foreignProvenance)
206
+ : 1;
207
+ // D3: use-it-or-lose-it reinforcement. log1p-saturating in the access
208
+ // count so early accesses matter most; identity at the default weight 0.
209
+ const reinforcementWeight = weights.accessReinforcement ?? 0;
210
+ const count = Math.max(0, args.accessCount ?? 0);
211
+ const reinforcementFactor =
212
+ reinforcementWeight === 0
213
+ ? 1
214
+ : 1 +
215
+ reinforcementWeight *
216
+ Math.min(1, Math.log1p(count) / Math.log1p(ACCESS_REINFORCEMENT_SATURATION));
217
+ return Math.max(0, base * importanceFactor * securityFactor * reinforcementFactor);
218
+ }
219
+
220
+ /**
221
+ * Capacity-bounded eviction selector (X-1). Given facts scored by
222
+ * {@link salience}, return the ids of the lowest-salience ones to
223
+ * archive so that at most `capacity` remain. Pure and deterministic:
224
+ * ties break by id so a given batch always evicts the same set.
225
+ *
226
+ * Returns `[]` when the batch already fits (`length <= capacity`).
227
+ * `capacity <= 0` evicts the whole batch.
228
+ *
229
+ * @stable
230
+ */
231
+ export function selectForCapacityEviction(
232
+ scored: ReadonlyArray<{ readonly id: string; readonly salience: number }>,
233
+ capacity: number,
234
+ ): ReadonlyArray<string> {
235
+ const evictCount = scored.length - Math.max(0, capacity);
236
+ if (evictCount <= 0) return [];
237
+ return [...scored]
238
+ .sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
239
+ .slice(0, evictCount)
240
+ .map((row) => row.id);
241
+ }
242
+
243
+ function clamp01(value: number): number {
244
+ if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;
245
+ return Math.min(1, Math.max(0, value));
246
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Dead-letter queue helpers - pure functions used by the standard
3
+ * + deep phases. The actual persistence lives in the storage
4
+ * adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ /**
10
+ * Compute the next retry delay given the failure count + retry
11
+ * policy. Implements full-jitter exponential backoff capped by
12
+ * `maxBackoffMs`.
13
+ *
14
+ * The formula is `delay = random(0, min(maxBackoffMs, baseMs * 2^retryCount))`
15
+ * where `random` is the optional `jitter` callback (defaults to
16
+ * `Math.random()`).
17
+ *
18
+ * @stable
19
+ */
20
+ export function nextBackoffMs(args: {
21
+ readonly retryCount: number;
22
+ readonly baseMs: number;
23
+ readonly maxMs: number;
24
+ readonly jitter?: () => number;
25
+ }): number {
26
+ const cap = Math.min(args.maxMs, args.baseMs * 2 ** Math.max(0, args.retryCount));
27
+ const jitter = args.jitter ?? Math.random;
28
+ return Math.max(0, Math.floor(cap * jitter()));
29
+ }
30
+
31
+ /**
32
+ * Classify a thrown error into the lowercase `error_kind` value
33
+ * recorded against `consolidator_failed_batches`. Used by the phases
34
+ * so the DLQ row carries a stable discriminator instead of free-form
35
+ * prose.
36
+ *
37
+ * @stable
38
+ */
39
+ export function classifyError(err: unknown): string {
40
+ if (err === null || err === undefined) return 'unknown';
41
+ const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
42
+ if (message.includes('rate limit') || message.includes('429')) return 'rate_limit';
43
+ if (message.includes('5xx') || message.includes('500') || message.includes('502')) return '5xx';
44
+ if (message.includes('timeout')) return 'timeout';
45
+ if (message.includes('embedder') || message.includes('embedding')) return 'embedder_failed';
46
+ if (message.includes('invalid') || message.includes('parse')) return 'invalid_response';
47
+ if (message.includes('budget')) return 'budget';
48
+ return 'unknown';
49
+ }
50
+
51
+ /**
52
+ * Best-effort error message extraction.
53
+ *
54
+ * @internal
55
+ */
56
+ export function describeError(err: unknown): string {
57
+ if (err === null || err === undefined) return 'unknown error';
58
+ if (err instanceof Error) return err.message;
59
+ if (typeof err === 'string') return err;
60
+ return String(err);
61
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Typed errors raised by the Phase 10c consolidator runtime. Every
3
+ * error carries a stable lowercase `kind` discriminator so downstream
4
+ * tooling can pattern-match without parsing the prose.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import { GraphorinMemoryError } from '../errors/index.js';
10
+ import type { ConsolidatorPhase } from './types.js';
11
+
12
+ /**
13
+ * Raised when the consolidator's daily budget envelope is exhausted
14
+ * and `onExceed: 'throw'` is in effect.
15
+ *
16
+ * @stable
17
+ */
18
+ export class BudgetExceededError extends GraphorinMemoryError {
19
+ override readonly name = 'BudgetExceededError';
20
+ readonly kind = 'budget-exceeded' as const;
21
+ readonly phase: ConsolidatorPhase;
22
+ readonly budget: number;
23
+ readonly actual: number;
24
+ readonly resource: 'tokens' | 'cost';
25
+
26
+ constructor(args: {
27
+ phase: ConsolidatorPhase;
28
+ budget: number;
29
+ actual: number;
30
+ resource: 'tokens' | 'cost';
31
+ }) {
32
+ super(
33
+ `[graphorin/memory] consolidator budget exceeded - phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`,
34
+ { hint: 'Pass `onExceed: "log"` to keep running, or upgrade the tier ceilings.' },
35
+ );
36
+ this.phase = args.phase;
37
+ this.budget = args.budget;
38
+ this.actual = args.actual;
39
+ this.resource = args.resource;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Raised when `tier: 'custom'` is selected without explicit
45
+ * `ceilings.maxTokensPerDay` / `ceilings.maxCostPerDay`. Custom is
46
+ * an explicit-only escape hatch.
47
+ *
48
+ * @stable
49
+ */
50
+ export class CustomTierMisconfiguredError extends GraphorinMemoryError {
51
+ override readonly name = 'CustomTierMisconfiguredError';
52
+ readonly kind = 'custom-tier-misconfigured' as const;
53
+ readonly missing: ReadonlyArray<string>;
54
+
55
+ constructor(missing: ReadonlyArray<string>) {
56
+ super(
57
+ `[graphorin/memory] tier='custom' requires explicit ceilings - missing: ${missing.join(', ')}`,
58
+ {
59
+ hint: 'Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled).',
60
+ },
61
+ );
62
+ this.missing = Object.freeze([...missing]);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Raised when the standard / deep phase attempts an LLM call without
68
+ * a configured provider. Surfaces as a typed failure that the DLQ
69
+ * pipeline can attribute correctly.
70
+ *
71
+ * @stable
72
+ */
73
+ export class ProviderNotConfiguredError extends GraphorinMemoryError {
74
+ override readonly name = 'ProviderNotConfiguredError';
75
+ readonly kind = 'provider-not-configured' as const;
76
+ readonly phase: ConsolidatorPhase;
77
+
78
+ constructor(phase: ConsolidatorPhase) {
79
+ super(
80
+ `[graphorin/memory] consolidator phase '${phase}' requires a provider but none was supplied.`,
81
+ {
82
+ hint: 'Pass `provider` to createConsolidator({...}) or pin tier="free" to skip LLM phases.',
83
+ },
84
+ );
85
+ this.phase = phase;
86
+ }
87
+ }