@graphorin/memory 0.6.0 → 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 +83 -0
  2. package/README.md +24 -7
  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 +17 -15
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -7
  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 +6 -0
  89. package/dist/package.js.map +1 -0
  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,27 @@
1
+ /**
2
+ * Idempotency cursor helpers. The runtime reads the persisted state
3
+ * row directly via {@link ConsolidatorMemoryStoreExt.getState}; this
4
+ * file centralises the pure helpers around the cursor advance.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { SessionMessageRecord } from '../internal/storage-adapter.js';
10
+
11
+ /**
12
+ * Advance the cursor to the highest sequence number observed in the
13
+ * batch. Returns `null` when the batch is empty.
14
+ *
15
+ * @stable
16
+ */
17
+ export function tipMessageId(batch: ReadonlyArray<SessionMessageRecord>): string | null {
18
+ if (batch.length === 0) return null;
19
+ let bestIdx = 0;
20
+ for (let i = 1; i < batch.length; i++) {
21
+ const candidate = batch[i];
22
+ const champion = batch[bestIdx];
23
+ if (candidate === undefined || champion === undefined) continue;
24
+ if (candidate.sequence > champion.sequence) bestIdx = i;
25
+ }
26
+ return batch[bestIdx]?.id ?? null;
27
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * `@graphorin/memory/consolidator` - Phase 10c runtime that turns
3
+ * raw conversation turns into long-lived facts and episodes. The
4
+ * module ships:
5
+ *
6
+ * - {@link createConsolidator} - the production factory that wires
7
+ * triggers, the budget envelope, the wait-then-defer lock, the
8
+ * idempotent cursor, the DLQ, and the three phases (light /
9
+ * standard / deep) into a single `Consolidator` handle.
10
+ * - {@link createConsolidatorPlaceholder} - kept for back-compat
11
+ * with consumers that wired the no-op stub during Phase 10a; it
12
+ * honours the same shape but does no background work.
13
+ * - Pure helpers (`noise-filter`, `decay`, `budget`, `lock`, `dlq`,
14
+ * `idempotency`, `triggers`) and the per-phase orchestrators -
15
+ * exported so tests + the standalone server (Phase 14) can compose
16
+ * them without re-implementing the algorithm.
17
+ *
18
+ * The `Consolidator` interface is **stable** - Phase 14 wires the
19
+ * runtime into the lifecycle hooks, and Phase 15 surfaces every
20
+ * method through `graphorin consolidator …` CLI commands.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+
25
+ import { DEFAULT_SALIENCE_WEIGHTS } from './decay.js';
26
+ import type { Consolidator } from './runtime.js';
27
+ import type {
28
+ ConsolidatorConfig,
29
+ ConsolidatorPhase,
30
+ ConsolidatorStatus,
31
+ ConsolidatorTier,
32
+ ConsolidatorTriggerSpec,
33
+ PhaseListener,
34
+ PhaseOutcome,
35
+ } from './types.js';
36
+
37
+ export {
38
+ type BudgetCheck,
39
+ type BudgetSnapshot,
40
+ BudgetTracker,
41
+ type BudgetTrackerOptions,
42
+ bucketStart,
43
+ nextBucketStart,
44
+ } from './budget.js';
45
+ export {
46
+ DEFAULT_DECAY_ARCHIVE_THRESHOLD,
47
+ DEFAULT_DECAY_TAU_DAYS,
48
+ DEFAULT_SALIENCE_WEIGHTS,
49
+ NEUTRAL_IMPORTANCE,
50
+ retention,
51
+ type SalienceWeights,
52
+ salience,
53
+ selectForCapacityEviction,
54
+ shouldArchive,
55
+ } from './decay.js';
56
+ export {
57
+ classifyError,
58
+ describeError,
59
+ nextBackoffMs,
60
+ } from './dlq.js';
61
+ export {
62
+ BudgetExceededError,
63
+ CustomTierMisconfiguredError,
64
+ ProviderNotConfiguredError,
65
+ } from './errors.js';
66
+ export { tipMessageId } from './idempotency.js';
67
+ export { LockManager, type LockManagerOptions, type LockOutcome } from './lock.js';
68
+ export {
69
+ applyNoiseFilters,
70
+ type NoiseFilterPreset,
71
+ type NoiseFilterResult,
72
+ } from './noise-filter.js';
73
+ export { type DeepPhaseDeps, runDeepPhase } from './phases/deep.js';
74
+ export {
75
+ buildInductionRequest,
76
+ checkSuccessCriteria,
77
+ createProviderWorkflowInducer,
78
+ DEFAULT_INDUCTION_MAX_TOKENS,
79
+ INDUCTION_SYSTEM_PROMPT,
80
+ type InducedProcedure,
81
+ MAX_PROCEDURE_STEPS,
82
+ MAX_TRAJECTORY_STEPS_SHOWN,
83
+ normalizeInducedProcedure,
84
+ parseInducedProcedure,
85
+ runWorkflowInduction,
86
+ type Trajectory,
87
+ type TrajectoryStep,
88
+ trajectoryFromRunState,
89
+ type VerificationResult,
90
+ type WorkflowInducer,
91
+ type WorkflowInductionOptions,
92
+ } from './phases/induce.js';
93
+ export {
94
+ type LightPhaseDeps,
95
+ runLightPhase,
96
+ } from './phases/light.js';
97
+ export {
98
+ parseExtraction,
99
+ runStandardPhase,
100
+ type StandardPhaseDeps,
101
+ } from './phases/standard.js';
102
+ export { type Consolidator, createConsolidator } from './runtime.js';
103
+ export {
104
+ type ConsolidatorCatchupPolicy,
105
+ type RegisterTriggersOptions,
106
+ type RegisterTriggersResult,
107
+ registerConsolidatorTriggers,
108
+ type SchedulerLike,
109
+ type TriggerDeclarationLike,
110
+ } from './scheduler.js';
111
+ export {
112
+ type ParsedTrigger,
113
+ parseTriggerSpec,
114
+ reasonFromTrigger,
115
+ } from './triggers.js';
116
+ export type {
117
+ ConsolidatorBudgetSnapshot,
118
+ ConsolidatorCeilings,
119
+ ConsolidatorConfig,
120
+ ConsolidatorLastRuns,
121
+ ConsolidatorPhase,
122
+ ConsolidatorStatus,
123
+ ConsolidatorTier,
124
+ ConsolidatorTriggerReason,
125
+ ConsolidatorTriggerSpec,
126
+ CreateConsolidatorOptions,
127
+ OnBudgetExceed,
128
+ PhaseListener,
129
+ PhaseOutcome,
130
+ } from './types.js';
131
+ export { CONSOLIDATOR_TIER_DEFAULTS } from './types.js';
132
+
133
+ /**
134
+ * Build a no-op consolidator that honours the full {@link Consolidator}
135
+ * interface but performs no background work. Useful for consumers
136
+ * that want the typed shape (e.g., unit tests of higher tiers) but
137
+ * do not pay the runtime cost of triggers / locking / DLQ.
138
+ *
139
+ * Phase 10c's {@link createConsolidator} is the production factory.
140
+ *
141
+ * @stable
142
+ */
143
+ export function createConsolidatorPlaceholder(
144
+ options: {
145
+ readonly triggers?: ReadonlyArray<ConsolidatorTriggerSpec>;
146
+ readonly tier?: ConsolidatorTier;
147
+ } = {},
148
+ ): Consolidator {
149
+ let running = false;
150
+ let paused = false;
151
+ let tier: ConsolidatorTier = options.tier ?? 'free';
152
+ const triggers = Object.freeze([
153
+ // Matches the real runtime default (MCON-4): idle drives light/standard,
154
+ // the daily cron makes the deep phase reachable; turn:N is consumer-emitted
155
+ // so it is not a scheduler default.
156
+ ...(options.triggers ?? (['idle:5m', 'cron:0 4 * * *'] as ConsolidatorTriggerSpec[])),
157
+ ]) as ReadonlyArray<ConsolidatorTriggerSpec>;
158
+ const listeners = new Set<PhaseListener>();
159
+
160
+ const config: ConsolidatorConfig = Object.freeze({
161
+ triggers,
162
+ tier,
163
+ phases: Object.freeze(['light' as ConsolidatorPhase]),
164
+ ceilings: {
165
+ maxTokensPerDay: 0,
166
+ maxCostPerDay: 0,
167
+ maxConcurrentRuns: 1,
168
+ maxRunDurationMs: 5 * 60 * 1000,
169
+ cooldownMs: 60_000,
170
+ },
171
+ onExceed: 'pause',
172
+ cheapModel: null,
173
+ deepModel: null,
174
+ budgetResetSemantics: 'utc',
175
+ noiseFilters: Object.freeze(['default' as const]),
176
+ lockWaitMs: 30_000,
177
+ decayTauDays: 7,
178
+ decayArchiveThreshold: 0.05,
179
+ decayCapacity: null,
180
+ salienceWeights: DEFAULT_SALIENCE_WEIGHTS,
181
+ maxStandardBatchSize: 50,
182
+ maxTranscriptChars: 60_000,
183
+ maxDeepConflictsPerRun: 20,
184
+ dlqMaxRetries: 5,
185
+ dlqBaseBackoffMs: 60_000,
186
+ dlqMaxBackoffMs: 60 * 60 * 1000,
187
+ formEpisodes: false,
188
+ importanceScoring: false,
189
+ autoPromoteExtraction: false,
190
+ reflection: false,
191
+ importanceThreshold: 3,
192
+ reflectionMaxQuestions: 3,
193
+ reflectionMaxQuarantinedInsights: 100,
194
+ contextualRetrieval: 'late-chunk',
195
+ learnedContext: false,
196
+ learnedContextMaxChars: 1200,
197
+ });
198
+
199
+ const status = async (): Promise<ConsolidatorStatus> =>
200
+ Object.freeze({
201
+ tier,
202
+ triggers,
203
+ phases: config.phases,
204
+ running,
205
+ paused,
206
+ pendingConflicts: 0,
207
+ queueDepth: 0,
208
+ dlqSize: 0,
209
+ deferredRuns: 0,
210
+ emptyExtractions: 0,
211
+ lastRuns: Object.freeze({}),
212
+ budget: {
213
+ tokensUsedToday: 0,
214
+ costUsedToday: 0,
215
+ tokensRemaining: 0,
216
+ costRemaining: 0,
217
+ resetAt: new Date(Date.now() + 86_400_000).toISOString(),
218
+ },
219
+ budgetRemaining: {
220
+ tokens: 0,
221
+ costUsd: 0,
222
+ },
223
+ });
224
+
225
+ return {
226
+ async start() {
227
+ running = true;
228
+ },
229
+ async stop() {
230
+ running = false;
231
+ },
232
+ async trigger() {
233
+ return null;
234
+ },
235
+ async fireNow(): Promise<PhaseOutcome | null> {
236
+ return null;
237
+ },
238
+ async setTier(next: ConsolidatorTier) {
239
+ tier = next;
240
+ },
241
+ async pause() {
242
+ paused = true;
243
+ },
244
+ async resume() {
245
+ paused = false;
246
+ },
247
+ recordExternalSpend(): void {
248
+ // Placeholder - no budget to record against.
249
+ },
250
+ onPhaseFinished(listener) {
251
+ listeners.add(listener);
252
+ return () => listeners.delete(listener);
253
+ },
254
+ config() {
255
+ return config;
256
+ },
257
+ async registerWithScheduler() {
258
+ // No-op consolidator: nothing to schedule.
259
+ return Object.freeze({ registered: Object.freeze([]), skipped: Object.freeze([]) });
260
+ },
261
+ isFree() {
262
+ return tier === 'free';
263
+ },
264
+ async drainDlq() {
265
+ return 0;
266
+ },
267
+ status,
268
+ };
269
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Wait-then-defer lock manager. The lock state lives in the storage
3
+ * layer's `consolidator_state` row so concurrent processes (lib +
4
+ * server) coordinate through SQLite. The manager waits up to
5
+ * `lockWaitMs` for the lock to clear; if the wait expires it returns
6
+ * `'deferred'` so the trigger is recorded but no phase runs.
7
+ *
8
+ * The lock honours DEC-114 ("memory does not block the agent loop")
9
+ * by capping the wait - the agent loop never observes a memory
10
+ * blocker.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import type { SessionScope } from '@graphorin/core';
16
+ import type { ConsolidatorMemoryStoreExt } from '../internal/storage-adapter.js';
17
+
18
+ /** Outcome surfaced by {@link LockManager.acquire}. */
19
+ export type LockOutcome =
20
+ | { readonly kind: 'acquired'; readonly runId: string }
21
+ | { readonly kind: 'deferred'; readonly heldBy: string | null };
22
+
23
+ /** Options accepted by {@link LockManager}. */
24
+ export interface LockManagerOptions {
25
+ readonly store: ConsolidatorMemoryStoreExt | null;
26
+ readonly waitMs: number;
27
+ readonly maxRunDurationMs: number;
28
+ readonly now?: () => number;
29
+ readonly sleep?: (ms: number) => Promise<void>;
30
+ readonly randomId?: () => string;
31
+ }
32
+
33
+ const DEFAULT_POLL_MS = 100;
34
+
35
+ /** @stable */
36
+ export class LockManager {
37
+ readonly #store: ConsolidatorMemoryStoreExt | null;
38
+ readonly #waitMs: number;
39
+ readonly #maxAge: number;
40
+ readonly #now: () => number;
41
+ readonly #sleep: (ms: number) => Promise<void>;
42
+ readonly #randomId: () => string;
43
+ // Per-process fallback when the storage adapter does not expose a
44
+ // consolidator surface (lib mode + in-memory test doubles).
45
+ readonly #localLocks = new Map<string, { runId: string; acquiredAt: number }>();
46
+
47
+ constructor(opts: LockManagerOptions) {
48
+ this.#store = opts.store;
49
+ this.#waitMs = Math.max(0, opts.waitMs);
50
+ this.#maxAge = Math.max(0, opts.maxRunDurationMs);
51
+ this.#now = opts.now ?? Date.now;
52
+ this.#sleep =
53
+ opts.sleep ??
54
+ ((ms): Promise<void> => new Promise((resolve) => globalThis.setTimeout(resolve, ms)));
55
+ this.#randomId =
56
+ opts.randomId ??
57
+ ((): string => {
58
+ const a = Math.floor(Math.random() * 1e9).toString(36);
59
+ const b = Math.floor(Math.random() * 1e9).toString(36);
60
+ return `run_${a}${b}`;
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Try to acquire the per-scope lock. Returns the run id on success
66
+ * or `'deferred'` after the wait window elapses.
67
+ *
68
+ * @stable
69
+ */
70
+ async acquire(scope: SessionScope): Promise<LockOutcome> {
71
+ const runId = this.#randomId();
72
+ const start = this.#now();
73
+ const pollMs = Math.min(DEFAULT_POLL_MS, Math.max(10, this.#waitMs / 5 || DEFAULT_POLL_MS));
74
+ while (true) {
75
+ const ok = await this.#tryAcquire(scope, runId);
76
+ if (ok) return { kind: 'acquired', runId };
77
+ const elapsed = this.#now() - start;
78
+ if (elapsed >= this.#waitMs) {
79
+ const heldBy = await this.#heldBy(scope);
80
+ return { kind: 'deferred', heldBy };
81
+ }
82
+ await this.#sleep(pollMs);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Release the lock. Idempotent - releasing an already-released
88
+ * lock is a no-op.
89
+ *
90
+ * @stable
91
+ */
92
+ async release(scope: SessionScope, runId: string): Promise<void> {
93
+ if (this.#store !== null) {
94
+ await this.#store.releaseLock(scope, runId);
95
+ }
96
+ const key = scopeKey(scope);
97
+ const local = this.#localLocks.get(key);
98
+ if (local !== undefined && local.runId === runId) {
99
+ this.#localLocks.delete(key);
100
+ }
101
+ }
102
+
103
+ async #tryAcquire(scope: SessionScope, runId: string): Promise<boolean> {
104
+ const now = this.#now();
105
+ if (this.#store !== null) {
106
+ return this.#store.acquireLock(scope, runId, now, this.#maxAge);
107
+ }
108
+ // Fall back to per-process map.
109
+ const key = scopeKey(scope);
110
+ const existing = this.#localLocks.get(key);
111
+ if (existing === undefined) {
112
+ this.#localLocks.set(key, { runId, acquiredAt: now });
113
+ return true;
114
+ }
115
+ if (existing.runId === runId) return true;
116
+ if (this.#maxAge > 0 && now - existing.acquiredAt > this.#maxAge) {
117
+ this.#localLocks.set(key, { runId, acquiredAt: now });
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ async #heldBy(scope: SessionScope): Promise<string | null> {
124
+ if (this.#store !== null) {
125
+ const state = await this.#store.getState(scope);
126
+ return state?.activeLockHeldBy ?? null;
127
+ }
128
+ const local = this.#localLocks.get(scopeKey(scope));
129
+ return local?.runId ?? null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Canonical string key for a consolidation scope. The lock manager and
135
+ * the runtime's per-scope dispatch bookkeeping (W-142) must agree on
136
+ * this shape: the lock serialises work per scope, so any state keyed by
137
+ * `scopeKey` holds at most one live entry per scope.
138
+ */
139
+ export function scopeKey(scope: SessionScope): string {
140
+ return `${scope.userId}|${scope.sessionId ?? ''}|${scope.agentId ?? ''}`;
141
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Built-in noise filter pipeline. Runs before LLM extraction in the
3
+ * standard phase + every light-phase invocation. The defaults are
4
+ * heuristic-only (no LLM); adapters may extend the pipeline through
5
+ * `noiseFilters: [...customFilters]` once the public surface is wired
6
+ * (post-MVP - DEC-134 carve-out).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type { Message } from '@graphorin/core';
12
+ import type { SessionMessageRecord } from '../internal/storage-adapter.js';
13
+
14
+ /**
15
+ * Bundle level surfaced by {@link applyNoiseFilters}. The runtime
16
+ * uses these to log + report `noise_filtered_count` per phase.
17
+ *
18
+ * @stable
19
+ */
20
+ export type NoiseFilterPreset = 'default' | 'minimal' | 'none';
21
+
22
+ const MIN_LENGTH_DEFAULT = 10;
23
+ const MIN_LENGTH_MINIMAL = 4;
24
+ const MAX_LENGTH = 10_000;
25
+ const STOP_WORD_RATIO = 0.8;
26
+ const STOP_WORDS = new Set([
27
+ 'a',
28
+ 'an',
29
+ 'and',
30
+ 'are',
31
+ 'as',
32
+ 'at',
33
+ 'be',
34
+ 'but',
35
+ 'by',
36
+ 'can',
37
+ 'do',
38
+ 'does',
39
+ 'for',
40
+ 'from',
41
+ 'had',
42
+ 'has',
43
+ 'have',
44
+ 'he',
45
+ 'her',
46
+ 'his',
47
+ 'i',
48
+ 'if',
49
+ 'in',
50
+ 'is',
51
+ 'it',
52
+ 'its',
53
+ 'me',
54
+ 'my',
55
+ 'no',
56
+ 'not',
57
+ 'of',
58
+ 'on',
59
+ 'or',
60
+ 'our',
61
+ 'she',
62
+ 'so',
63
+ 'that',
64
+ 'the',
65
+ 'their',
66
+ 'them',
67
+ 'they',
68
+ 'this',
69
+ 'to',
70
+ 'too',
71
+ 'us',
72
+ 'was',
73
+ 'we',
74
+ 'were',
75
+ 'what',
76
+ 'when',
77
+ 'where',
78
+ 'which',
79
+ 'who',
80
+ 'will',
81
+ 'with',
82
+ 'would',
83
+ 'yes',
84
+ 'you',
85
+ 'your',
86
+ ]);
87
+
88
+ /**
89
+ * Result returned by {@link applyNoiseFilters}.
90
+ *
91
+ * @stable
92
+ */
93
+ export interface NoiseFilterResult {
94
+ readonly kept: ReadonlyArray<SessionMessageRecord>;
95
+ readonly droppedCount: number;
96
+ readonly droppedByReason: Readonly<Record<string, number>>;
97
+ }
98
+
99
+ /**
100
+ * Run the configured filter chain against the supplied messages.
101
+ * Filters short-circuit at the first hit so the cheapest checks
102
+ * always run first.
103
+ *
104
+ * @stable
105
+ */
106
+ export function applyNoiseFilters(
107
+ messages: ReadonlyArray<SessionMessageRecord>,
108
+ presets: ReadonlyArray<NoiseFilterPreset> = ['default'],
109
+ ): NoiseFilterResult {
110
+ if (presets.length === 0 || presets.every((p) => p === 'none')) {
111
+ return { kept: messages, droppedCount: 0, droppedByReason: Object.freeze({}) };
112
+ }
113
+ const minimal = presets.includes('minimal') && !presets.includes('default');
114
+ const minLength = minimal ? MIN_LENGTH_MINIMAL : MIN_LENGTH_DEFAULT;
115
+ const droppedByReason: Record<string, number> = {};
116
+ const kept: SessionMessageRecord[] = [];
117
+ const recentTexts: string[] = [];
118
+
119
+ for (const record of messages) {
120
+ const reason = classify(record.message, recentTexts, minLength, minimal);
121
+ if (reason === null) {
122
+ kept.push(record);
123
+ const text = renderText(record.message);
124
+ if (text.length > 0) {
125
+ recentTexts.push(text);
126
+ if (recentTexts.length > 5) recentTexts.shift();
127
+ }
128
+ } else {
129
+ droppedByReason[reason] = (droppedByReason[reason] ?? 0) + 1;
130
+ }
131
+ }
132
+ return {
133
+ kept,
134
+ droppedCount: messages.length - kept.length,
135
+ droppedByReason: Object.freeze({ ...droppedByReason }),
136
+ };
137
+ }
138
+
139
+ function classify(
140
+ message: Message,
141
+ recentTexts: ReadonlyArray<string>,
142
+ minLength: number,
143
+ minimal: boolean,
144
+ ): string | null {
145
+ if (message.role === 'system') return 'role-system';
146
+ if (message.role === 'tool' && minimal) return null;
147
+ const text = renderText(message);
148
+ const trimmed = text.trim();
149
+ if (trimmed.length === 0) return 'empty';
150
+ if (trimmed.length < minLength) return 'too-short';
151
+ if (trimmed.length > MAX_LENGTH) return 'too-long';
152
+ if (looksLikeCodeOrJson(trimmed)) return 'code-block';
153
+ if (!minimal && stopWordRatio(trimmed) > STOP_WORD_RATIO) return 'stop-word-ratio';
154
+ if (!minimal && isNearDuplicate(trimmed, recentTexts)) return 'near-duplicate';
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Render a message as a single text blob for the heuristic filters.
160
+ *
161
+ * @internal
162
+ */
163
+ export function renderText(message: Message): string {
164
+ if (message.role === 'system') return message.content;
165
+ const c = message.content;
166
+ if (typeof c === 'string') return c;
167
+ let out = '';
168
+ for (const part of c) {
169
+ if (part.type === 'text' || part.type === 'reasoning') {
170
+ out += `${part.text} `;
171
+ }
172
+ }
173
+ return out.trim();
174
+ }
175
+
176
+ function looksLikeCodeOrJson(text: string): boolean {
177
+ if (text.startsWith('```')) return true;
178
+ const trimmed = text.trim();
179
+ if (
180
+ (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
181
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))
182
+ ) {
183
+ try {
184
+ JSON.parse(trimmed);
185
+ return true;
186
+ } catch {
187
+ // Not actually JSON - fall through.
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+
193
+ function stopWordRatio(text: string): number {
194
+ const tokens = text
195
+ .toLowerCase()
196
+ .split(/[^a-z0-9]+/u)
197
+ .filter((t) => t.length > 0);
198
+ if (tokens.length === 0) return 0;
199
+ let stop = 0;
200
+ for (const t of tokens) if (STOP_WORDS.has(t)) stop += 1;
201
+ return stop / tokens.length;
202
+ }
203
+
204
+ function isNearDuplicate(text: string, recent: ReadonlyArray<string>): boolean {
205
+ const normalised = text.replace(/\s+/gu, ' ').toLowerCase().trim();
206
+ for (const prior of recent) {
207
+ const priorNorm = prior.replace(/\s+/gu, ' ').toLowerCase().trim();
208
+ if (priorNorm.length === 0) continue;
209
+ if (priorNorm === normalised) return true;
210
+ const similarity = trigramSimilarity(priorNorm, normalised);
211
+ if (similarity >= 0.9) return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+ function trigramSimilarity(a: string, b: string): number {
217
+ const setA = trigramSet(a);
218
+ const setB = trigramSet(b);
219
+ if (setA.size === 0 || setB.size === 0) return 0;
220
+ let inter = 0;
221
+ for (const t of setA) if (setB.has(t)) inter += 1;
222
+ return inter / Math.max(setA.size, setB.size);
223
+ }
224
+
225
+ function trigramSet(text: string): Set<string> {
226
+ const out = new Set<string>();
227
+ if (text.length < 3) {
228
+ out.add(text);
229
+ return out;
230
+ }
231
+ for (let i = 0; i < text.length - 2; i++) {
232
+ out.add(text.slice(i, i + 3));
233
+ }
234
+ return out;
235
+ }