tracelattice 1.2.9 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +83 -13
  2. package/dist/ServerConfig.d.ts +60 -0
  3. package/dist/ServerConfig.d.ts.map +1 -1
  4. package/dist/ServerConfig.js +27 -1
  5. package/dist/ServerConfig.js.map +1 -1
  6. package/dist/__tests__/calibrator/Calibrator.test.d.ts +6 -0
  7. package/dist/__tests__/calibrator/Calibrator.test.d.ts.map +1 -0
  8. package/dist/__tests__/compression/CompressionService.test.d.ts +5 -0
  9. package/dist/__tests__/compression/CompressionService.test.d.ts.map +1 -0
  10. package/dist/__tests__/compression/DehydrationPolicy.test.d.ts +5 -0
  11. package/dist/__tests__/compression/DehydrationPolicy.test.d.ts.map +1 -0
  12. package/dist/__tests__/compression/InMemorySummaryStore.test.d.ts +5 -0
  13. package/dist/__tests__/compression/InMemorySummaryStore.test.d.ts.map +1 -0
  14. package/dist/__tests__/config/ConfigLoader.features.test.d.ts +2 -0
  15. package/dist/__tests__/config/ConfigLoader.features.test.d.ts.map +1 -0
  16. package/dist/__tests__/core/HistoryManager.dag.test.d.ts +9 -0
  17. package/dist/__tests__/core/HistoryManager.dag.test.d.ts.map +1 -0
  18. package/dist/__tests__/core/HistoryManager.edge-persistence.test.d.ts +12 -0
  19. package/dist/__tests__/core/HistoryManager.edge-persistence.test.d.ts.map +1 -0
  20. package/dist/__tests__/core/HistoryManager.toolInvocation.test.d.ts +2 -0
  21. package/dist/__tests__/core/HistoryManager.toolInvocation.test.d.ts.map +1 -0
  22. package/dist/__tests__/core/ThoughtProcessor.newThoughtTypes.test.d.ts +2 -0
  23. package/dist/__tests__/core/ThoughtProcessor.newThoughtTypes.test.d.ts.map +1 -0
  24. package/dist/__tests__/core/ThoughtProcessor.toolInterleave.test.d.ts +2 -0
  25. package/dist/__tests__/core/ThoughtProcessor.toolInterleave.test.d.ts.map +1 -0
  26. package/dist/__tests__/core/graph/Edge.test.d.ts +5 -0
  27. package/dist/__tests__/core/graph/Edge.test.d.ts.map +1 -0
  28. package/dist/__tests__/core/graph/EdgeStore.test.d.ts +5 -0
  29. package/dist/__tests__/core/graph/EdgeStore.test.d.ts.map +1 -0
  30. package/dist/__tests__/core/graph/GraphView.test.d.ts +5 -0
  31. package/dist/__tests__/core/graph/GraphView.test.d.ts.map +1 -0
  32. package/dist/__tests__/core/reasoning/OutcomeRecorder.test.d.ts +5 -0
  33. package/dist/__tests__/core/reasoning/OutcomeRecorder.test.d.ts.map +1 -0
  34. package/dist/__tests__/core/reasoning/strategies/TreeOfThought.newTypes.test.d.ts +2 -0
  35. package/dist/__tests__/core/reasoning/strategies/TreeOfThought.newTypes.test.d.ts.map +1 -0
  36. package/dist/__tests__/core/retraction.test.d.ts +2 -0
  37. package/dist/__tests__/core/retraction.test.d.ts.map +1 -0
  38. package/dist/__tests__/core/thought-id.test.d.ts +13 -0
  39. package/dist/__tests__/core/thought-id.test.d.ts.map +1 -0
  40. package/dist/__tests__/core/tools/InMemorySuspensionStore.test.d.ts +2 -0
  41. package/dist/__tests__/core/tools/InMemorySuspensionStore.test.d.ts.map +1 -0
  42. package/dist/__tests__/eval/fixtures/scenarios.d.ts +37 -0
  43. package/dist/__tests__/eval/fixtures/scenarios.d.ts.map +1 -0
  44. package/dist/__tests__/eval/totVsSequential.eval.d.ts +16 -0
  45. package/dist/__tests__/eval/totVsSequential.eval.d.ts.map +1 -0
  46. package/dist/__tests__/evaluator/EvaluatorBackwardCompat.test.d.ts +15 -0
  47. package/dist/__tests__/evaluator/EvaluatorBackwardCompat.test.d.ts.map +1 -0
  48. package/dist/__tests__/helpers/factories.d.ts +2 -0
  49. package/dist/__tests__/helpers/factories.d.ts.map +1 -1
  50. package/dist/__tests__/integration/CompressionAutoTrigger.test.d.ts +11 -0
  51. package/dist/__tests__/integration/CompressionAutoTrigger.test.d.ts.map +1 -0
  52. package/dist/__tests__/integration/CompressionPersistence.test.d.ts +17 -0
  53. package/dist/__tests__/integration/CompressionPersistence.test.d.ts.map +1 -0
  54. package/dist/__tests__/integration/StrategyIntegration.test.d.ts +10 -0
  55. package/dist/__tests__/integration/StrategyIntegration.test.d.ts.map +1 -0
  56. package/dist/__tests__/integration/ToTStrategyIntegration.test.d.ts +18 -0
  57. package/dist/__tests__/integration/ToTStrategyIntegration.test.d.ts.map +1 -0
  58. package/dist/__tests__/integration/dag-edges.test.d.ts +17 -0
  59. package/dist/__tests__/integration/dag-edges.test.d.ts.map +1 -0
  60. package/dist/__tests__/strategies/SequentialStrategy.test.d.ts +2 -0
  61. package/dist/__tests__/strategies/SequentialStrategy.test.d.ts.map +1 -0
  62. package/dist/__tests__/strategies/StrategyContract.test.d.ts +18 -0
  63. package/dist/__tests__/strategies/StrategyContract.test.d.ts.map +1 -0
  64. package/dist/__tests__/strategies/TreeOfThoughtStrategy.test.d.ts +9 -0
  65. package/dist/__tests__/strategies/TreeOfThoughtStrategy.test.d.ts.map +1 -0
  66. package/dist/__tests__/strategies/plateau.test.d.ts +2 -0
  67. package/dist/__tests__/strategies/plateau.test.d.ts.map +1 -0
  68. package/dist/__tests__/strategies/totScoring.test.d.ts +2 -0
  69. package/dist/__tests__/strategies/totScoring.test.d.ts.map +1 -0
  70. package/dist/cli.js +9 -9
  71. package/dist/cli.js.map +1 -1
  72. package/dist/config/ConfigLoader.d.ts +37 -0
  73. package/dist/config/ConfigLoader.d.ts.map +1 -1
  74. package/dist/config/ConfigLoader.js +52 -2
  75. package/dist/config/ConfigLoader.js.map +1 -1
  76. package/dist/context/RequestContext.d.ts +2 -46
  77. package/dist/context/RequestContext.d.ts.map +1 -1
  78. package/dist/context/RequestContext.js +1 -10
  79. package/dist/context/RequestContext.js.map +1 -1
  80. package/dist/contracts/calibrator.d.ts +88 -0
  81. package/dist/contracts/calibrator.d.ts.map +1 -0
  82. package/dist/contracts/calibrator.js +1 -0
  83. package/dist/contracts/index.d.ts +5 -1
  84. package/dist/contracts/index.d.ts.map +1 -1
  85. package/dist/contracts/interfaces.d.ts +114 -35
  86. package/dist/contracts/interfaces.d.ts.map +1 -1
  87. package/dist/contracts/strategy.d.ts +106 -0
  88. package/dist/contracts/strategy.d.ts.map +1 -0
  89. package/dist/contracts/strategy.js +1 -0
  90. package/dist/contracts/summary.d.ts +104 -0
  91. package/dist/contracts/summary.d.ts.map +1 -0
  92. package/dist/contracts/summary.js +1 -0
  93. package/dist/contracts/suspension.d.ts +181 -0
  94. package/dist/contracts/suspension.d.ts.map +1 -0
  95. package/dist/contracts/suspension.js +1 -0
  96. package/dist/core/HistoryManager.d.ts +54 -441
  97. package/dist/core/HistoryManager.d.ts.map +1 -1
  98. package/dist/core/HistoryManager.js +132 -145
  99. package/dist/core/HistoryManager.js.map +1 -1
  100. package/dist/core/IHistoryManager.d.ts +17 -0
  101. package/dist/core/IHistoryManager.d.ts.map +1 -1
  102. package/dist/core/InputNormalizer.d.ts.map +1 -1
  103. package/dist/core/InputNormalizer.js +13 -24
  104. package/dist/core/InputNormalizer.js.map +1 -1
  105. package/dist/core/PersistenceBuffer.d.ts +110 -0
  106. package/dist/core/PersistenceBuffer.d.ts.map +1 -0
  107. package/dist/core/PersistenceBuffer.js +141 -0
  108. package/dist/core/PersistenceBuffer.js.map +1 -0
  109. package/dist/core/SessionManager.d.ts +58 -0
  110. package/dist/core/SessionManager.d.ts.map +1 -0
  111. package/dist/core/SessionManager.js +65 -0
  112. package/dist/core/SessionManager.js.map +1 -0
  113. package/dist/core/ThoughtEvaluator.d.ts +14 -99
  114. package/dist/core/ThoughtEvaluator.d.ts.map +1 -1
  115. package/dist/core/ThoughtEvaluator.js +66 -331
  116. package/dist/core/ThoughtEvaluator.js.map +1 -1
  117. package/dist/core/ThoughtFormatter.d.ts.map +1 -1
  118. package/dist/core/ThoughtFormatter.js +23 -1
  119. package/dist/core/ThoughtFormatter.js.map +1 -1
  120. package/dist/core/ThoughtProcessor.d.ts +68 -50
  121. package/dist/core/ThoughtProcessor.d.ts.map +1 -1
  122. package/dist/core/ThoughtProcessor.js +159 -7
  123. package/dist/core/ThoughtProcessor.js.map +1 -1
  124. package/dist/core/compression/CompressionService.d.ts +74 -0
  125. package/dist/core/compression/CompressionService.d.ts.map +1 -0
  126. package/dist/core/compression/CompressionService.js +159 -0
  127. package/dist/core/compression/CompressionService.js.map +1 -0
  128. package/dist/core/compression/DehydrationPolicy.d.ts +74 -0
  129. package/dist/core/compression/DehydrationPolicy.d.ts.map +1 -0
  130. package/dist/core/compression/DehydrationPolicy.js +45 -0
  131. package/dist/core/compression/DehydrationPolicy.js.map +1 -0
  132. package/dist/core/compression/InMemorySummaryStore.d.ts +130 -0
  133. package/dist/core/compression/InMemorySummaryStore.d.ts.map +1 -0
  134. package/dist/core/compression/InMemorySummaryStore.js +55 -0
  135. package/dist/core/compression/InMemorySummaryStore.js.map +1 -0
  136. package/dist/core/compression/Summary.d.ts +55 -0
  137. package/dist/core/compression/Summary.d.ts.map +1 -0
  138. package/dist/core/compression/Summary.js +1 -0
  139. package/dist/core/evaluator/Aggregator.d.ts +40 -0
  140. package/dist/core/evaluator/Aggregator.d.ts.map +1 -0
  141. package/dist/core/evaluator/Aggregator.js +32 -0
  142. package/dist/core/evaluator/Aggregator.js.map +1 -0
  143. package/dist/core/evaluator/Calibrator.d.ts +49 -0
  144. package/dist/core/evaluator/Calibrator.d.ts.map +1 -0
  145. package/dist/core/evaluator/Calibrator.js +172 -0
  146. package/dist/core/evaluator/Calibrator.js.map +1 -0
  147. package/dist/core/evaluator/PatternDetector.d.ts +69 -0
  148. package/dist/core/evaluator/PatternDetector.d.ts.map +1 -0
  149. package/dist/core/evaluator/PatternDetector.js +188 -0
  150. package/dist/core/evaluator/PatternDetector.js.map +1 -0
  151. package/dist/core/evaluator/SignalComputer.d.ts +116 -0
  152. package/dist/core/evaluator/SignalComputer.d.ts.map +1 -0
  153. package/dist/core/evaluator/SignalComputer.js +106 -0
  154. package/dist/core/evaluator/SignalComputer.js.map +1 -0
  155. package/dist/core/evaluator/internals.d.ts +36 -0
  156. package/dist/core/evaluator/internals.d.ts.map +1 -0
  157. package/dist/core/evaluator/internals.js +43 -0
  158. package/dist/core/evaluator/internals.js.map +1 -0
  159. package/dist/core/graph/Edge.d.ts +51 -0
  160. package/dist/core/graph/Edge.d.ts.map +1 -0
  161. package/dist/core/graph/Edge.js +1 -0
  162. package/dist/core/graph/EdgeEmitter.d.ts +64 -0
  163. package/dist/core/graph/EdgeEmitter.d.ts.map +1 -0
  164. package/dist/core/graph/EdgeEmitter.js +99 -0
  165. package/dist/core/graph/EdgeEmitter.js.map +1 -0
  166. package/dist/core/graph/EdgeStore.d.ts +104 -0
  167. package/dist/core/graph/EdgeStore.d.ts.map +1 -0
  168. package/dist/core/graph/EdgeStore.js +78 -0
  169. package/dist/core/graph/EdgeStore.js.map +1 -0
  170. package/dist/core/graph/GraphView.d.ts +156 -0
  171. package/dist/core/graph/GraphView.d.ts.map +1 -0
  172. package/dist/core/graph/GraphView.js +149 -0
  173. package/dist/core/graph/GraphView.js.map +1 -0
  174. package/dist/core/ids.d.ts +33 -0
  175. package/dist/core/ids.d.ts.map +1 -0
  176. package/dist/core/ids.js +11 -0
  177. package/dist/core/ids.js.map +1 -0
  178. package/dist/core/reasoning/OutcomeRecorder.d.ts +78 -0
  179. package/dist/core/reasoning/OutcomeRecorder.d.ts.map +1 -0
  180. package/dist/core/reasoning/OutcomeRecorder.js +36 -0
  181. package/dist/core/reasoning/OutcomeRecorder.js.map +1 -0
  182. package/dist/core/reasoning/strategies/SequentialStrategy.d.ts +54 -0
  183. package/dist/core/reasoning/strategies/SequentialStrategy.d.ts.map +1 -0
  184. package/dist/core/reasoning/strategies/SequentialStrategy.js +22 -0
  185. package/dist/core/reasoning/strategies/SequentialStrategy.js.map +1 -0
  186. package/dist/core/reasoning/strategies/StrategyFactory.d.ts +25 -0
  187. package/dist/core/reasoning/strategies/StrategyFactory.d.ts.map +1 -0
  188. package/dist/core/reasoning/strategies/StrategyFactory.js +15 -0
  189. package/dist/core/reasoning/strategies/StrategyFactory.js.map +1 -0
  190. package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.d.ts +58 -0
  191. package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.d.ts.map +1 -0
  192. package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js +98 -0
  193. package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js.map +1 -0
  194. package/dist/core/reasoning/strategies/plateau.d.ts +18 -0
  195. package/dist/core/reasoning/strategies/plateau.d.ts.map +1 -0
  196. package/dist/core/reasoning/strategies/plateau.js +12 -0
  197. package/dist/core/reasoning/strategies/plateau.js.map +1 -0
  198. package/dist/core/reasoning/strategies/totScoring.d.ts +90 -0
  199. package/dist/core/reasoning/strategies/totScoring.d.ts.map +1 -0
  200. package/dist/core/reasoning/strategies/totScoring.js +79 -0
  201. package/dist/core/reasoning/strategies/totScoring.js.map +1 -0
  202. package/dist/core/reasoning.d.ts +23 -3
  203. package/dist/core/reasoning.d.ts.map +1 -1
  204. package/dist/core/thought.d.ts +36 -0
  205. package/dist/core/thought.d.ts.map +1 -1
  206. package/dist/core/tools/InMemorySuspensionStore.d.ts +59 -0
  207. package/dist/core/tools/InMemorySuspensionStore.d.ts.map +1 -0
  208. package/dist/core/tools/InMemorySuspensionStore.js +91 -0
  209. package/dist/core/tools/InMemorySuspensionStore.js.map +1 -0
  210. package/dist/di/ServiceRegistry.d.ts +14 -0
  211. package/dist/di/ServiceRegistry.d.ts.map +1 -1
  212. package/dist/errors.d.ts +117 -0
  213. package/dist/errors.d.ts.map +1 -1
  214. package/dist/errors.js +40 -1
  215. package/dist/errors.js.map +1 -1
  216. package/dist/health/HealthChecker.d.ts.map +1 -1
  217. package/dist/health/HealthChecker.js +3 -2
  218. package/dist/health/HealthChecker.js.map +1 -1
  219. package/dist/lib.d.ts +0 -5
  220. package/dist/lib.d.ts.map +1 -1
  221. package/dist/lib.js +67 -12
  222. package/dist/lib.js.map +1 -1
  223. package/dist/persistence/FilePersistence.d.ts +55 -0
  224. package/dist/persistence/FilePersistence.d.ts.map +1 -1
  225. package/dist/persistence/FilePersistence.js +120 -1
  226. package/dist/persistence/FilePersistence.js.map +1 -1
  227. package/dist/persistence/MemoryPersistence.d.ts +40 -0
  228. package/dist/persistence/MemoryPersistence.d.ts.map +1 -1
  229. package/dist/persistence/MemoryPersistence.js +33 -0
  230. package/dist/persistence/MemoryPersistence.js.map +1 -1
  231. package/dist/persistence/PersistenceBackend.d.ts +40 -0
  232. package/dist/persistence/PersistenceBackend.d.ts.map +1 -1
  233. package/dist/persistence/SqlitePersistence.d.ts +41 -0
  234. package/dist/persistence/SqlitePersistence.d.ts.map +1 -1
  235. package/dist/persistence/SqlitePersistence.js +102 -0
  236. package/dist/persistence/SqlitePersistence.js.map +1 -1
  237. package/dist/registry/BaseRegistry.d.ts.map +1 -1
  238. package/dist/registry/BaseRegistry.js +4 -3
  239. package/dist/registry/BaseRegistry.js.map +1 -1
  240. package/dist/registry/SkillRegistry.d.ts +1 -1
  241. package/dist/registry/SkillRegistry.js.map +1 -1
  242. package/dist/schema.d.ts +27 -1
  243. package/dist/schema.d.ts.map +1 -1
  244. package/dist/schema.js +41 -9
  245. package/dist/schema.js.map +1 -1
  246. package/dist/transport/BaseTransport.d.ts +1 -20
  247. package/dist/transport/BaseTransport.d.ts.map +1 -1
  248. package/dist/transport/BaseTransport.js +1 -2
  249. package/dist/transport/BaseTransport.js.map +1 -1
  250. package/dist/transport/HttpHelpers.d.ts +0 -7
  251. package/dist/transport/HttpHelpers.d.ts.map +1 -1
  252. package/dist/transport/HttpHelpers.js +1 -7
  253. package/dist/transport/HttpHelpers.js.map +1 -1
  254. package/dist/transport/HttpTransport.d.ts.map +1 -1
  255. package/dist/transport/HttpTransport.js +2 -1
  256. package/dist/transport/HttpTransport.js.map +1 -1
  257. package/dist/transport/StreamableHttpTransport.d.ts +0 -6
  258. package/dist/transport/StreamableHttpTransport.d.ts.map +1 -1
  259. package/dist/transport/StreamableHttpTransport.js +4 -13
  260. package/dist/transport/StreamableHttpTransport.js.map +1 -1
  261. package/dist/watchers/SkillWatcher.d.ts.map +1 -1
  262. package/dist/watchers/SkillWatcher.js +3 -2
  263. package/dist/watchers/SkillWatcher.js.map +1 -1
  264. package/dist/watchers/ToolWatcher.d.ts.map +1 -1
  265. package/dist/watchers/ToolWatcher.js +2 -1
  266. package/dist/watchers/ToolWatcher.js.map +1 -1
  267. package/package.json +4 -4
  268. package/dist/__tests__/request-context.test.d.ts +0 -2
  269. package/dist/__tests__/request-context.test.d.ts.map +0 -1
  270. package/dist/__tests__/worker-manager-cov.test.d.ts +0 -2
  271. package/dist/__tests__/worker-manager-cov.test.d.ts.map +0 -1
  272. package/dist/__tests__/worker-manager.test.d.ts +0 -2
  273. package/dist/__tests__/worker-manager.test.d.ts.map +0 -1
  274. package/dist/cluster/WorkerManager.d.ts +0 -166
  275. package/dist/cluster/WorkerManager.d.ts.map +0 -1
  276. package/dist/cluster/WorkerManager.js +0 -202
  277. package/dist/cluster/WorkerManager.js.map +0 -1
  278. package/dist/cluster/worker.d.ts +0 -11
  279. package/dist/cluster/worker.d.ts.map +0 -1
  280. package/dist/cluster/worker.js +0 -36
  281. package/dist/cluster/worker.js.map +0 -1
  282. package/dist/telemetry/Telemetry.d.ts +0 -36
  283. package/dist/telemetry/Telemetry.d.ts.map +0 -1
  284. package/dist/telemetry/Telemetry.js +0 -68
  285. package/dist/telemetry/Telemetry.js.map +0 -1
  286. package/dist/telemetry/__tests__/Telemetry.test.d.ts +0 -2
  287. package/dist/telemetry/__tests__/Telemetry.test.d.ts.map +0 -1
@@ -1,12 +1,10 @@
1
1
  import { ValidationError } from "../errors.js";
2
2
  import { sanitizeString } from "../sanitize.js";
3
- const DEFAULT_TOOL_CONFIDENCE = 0.5;
4
- const DEFAULT_TOOL_PRIORITY = 999;
5
- const DEFAULT_TOOL_RATIONALE = '';
3
+ import { MAX_SESSION_ID_LENGTH, SESSION_ID_PATTERN, generateUlid } from "./ids.js";
4
+ const DEFAULT_RECOMMENDATION_CONFIDENCE = 0.5;
5
+ const DEFAULT_RECOMMENDATION_PRIORITY = 999;
6
+ const DEFAULT_RECOMMENDATION_RATIONALE = '';
6
7
  const DEFAULT_STEP_OUTCOME = '';
7
- const DEFAULT_SKILL_CONFIDENCE = 0.5;
8
- const DEFAULT_SKILL_PRIORITY = 999;
9
- const DEFAULT_SKILL_RATIONALE = '';
10
8
  function sanitizeRecursive(value) {
11
9
  if (null == value) return value;
12
10
  if ('string' == typeof value) return sanitizeString(value);
@@ -21,32 +19,22 @@ function sanitizeRecursive(value) {
21
19
  return value;
22
20
  }
23
21
  const BRANCH_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
24
- const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]{1,100}$/;
25
22
  function sanitizeBranchId(branchId) {
26
23
  if (!BRANCH_ID_PATTERN.test(branchId)) throw new ValidationError('branch_id', 'Invalid format - must be 1-64 alphanumeric characters, hyphens, or underscores only');
27
24
  return branchId;
28
25
  }
29
26
  function sanitizeSessionId(sessionId) {
30
27
  const cleaned = sanitizeString(sessionId);
31
- if (!SESSION_ID_PATTERN.test(cleaned)) return;
28
+ if (cleaned.length > MAX_SESSION_ID_LENGTH || !SESSION_ID_PATTERN.test(cleaned)) return;
32
29
  return cleaned;
33
30
  }
34
- function normalizeToolRecommendation(tool) {
31
+ function normalizeRecommendation(rec) {
35
32
  const normalized = {
36
- ...tool
33
+ ...rec
37
34
  };
38
- if (!('confidence' in normalized) || void 0 === normalized.confidence) normalized.confidence = DEFAULT_TOOL_CONFIDENCE;
39
- if (!('priority' in normalized) || void 0 === normalized.priority) normalized.priority = DEFAULT_TOOL_PRIORITY;
40
- if (!('rationale' in normalized) || void 0 === normalized.rationale) normalized.rationale = DEFAULT_TOOL_RATIONALE;
41
- return normalized;
42
- }
43
- function normalizeSkillRecommendation(skill) {
44
- const normalized = {
45
- ...skill
46
- };
47
- if (!('confidence' in normalized) || void 0 === normalized.confidence) normalized.confidence = DEFAULT_SKILL_CONFIDENCE;
48
- if (!('priority' in normalized) || void 0 === normalized.priority) normalized.priority = DEFAULT_SKILL_PRIORITY;
49
- if (!('rationale' in normalized) || void 0 === normalized.rationale) normalized.rationale = DEFAULT_SKILL_RATIONALE;
35
+ if (!('confidence' in normalized) || void 0 === normalized.confidence) normalized.confidence = DEFAULT_RECOMMENDATION_CONFIDENCE;
36
+ if (!('priority' in normalized) || void 0 === normalized.priority) normalized.priority = DEFAULT_RECOMMENDATION_PRIORITY;
37
+ if (!('rationale' in normalized) || void 0 === normalized.rationale) normalized.rationale = DEFAULT_RECOMMENDATION_RATIONALE;
50
38
  return normalized;
51
39
  }
52
40
  function normalizeStepRecommendation(step, lenient) {
@@ -61,8 +49,8 @@ function normalizeStepRecommendation(step, lenient) {
61
49
  normalized.recommended_skills = normalized.recommended_skill;
62
50
  delete normalized.recommended_skill;
63
51
  }
64
- if (Array.isArray(normalized.recommended_tools)) normalized.recommended_tools = normalized.recommended_tools.map((tool)=>'object' == typeof tool && null !== tool ? normalizeToolRecommendation(tool) : tool);
65
- if (Array.isArray(normalized.recommended_skills)) normalized.recommended_skills = normalized.recommended_skills.map((skill)=>'object' == typeof skill && null !== skill ? normalizeSkillRecommendation(skill) : skill);
52
+ if (Array.isArray(normalized.recommended_tools)) normalized.recommended_tools = normalized.recommended_tools.map((tool)=>'object' == typeof tool && null !== tool ? normalizeRecommendation(tool) : tool);
53
+ if (Array.isArray(normalized.recommended_skills)) normalized.recommended_skills = normalized.recommended_skills.map((skill)=>'object' == typeof skill && null !== skill ? normalizeRecommendation(skill) : skill);
66
54
  if (lenient && !('expected_outcome' in normalized)) normalized.expected_outcome = DEFAULT_STEP_OUTCOME;
67
55
  return normalized;
68
56
  }
@@ -92,6 +80,7 @@ function normalizeInput(input) {
92
80
  if (void 0 === sanitized) delete normalized.session_id;
93
81
  else normalized.session_id = sanitized;
94
82
  }
83
+ if (!normalized.id || 'string' != typeof normalized.id) normalized.id = generateUlid();
95
84
  normalizeReasoningFields(normalized);
96
85
  const sanitized = sanitizeRecursive(normalized);
97
86
  return sanitized;
@@ -1 +1 @@
1
- {"version":3,"file":"core/InputNormalizer.js","sources":["../../src/core/InputNormalizer.ts"],"sourcesContent":["/**\n * Input normalization for common LLM field name mistakes.\n *\n * This module provides normalization logic to handle common mistakes\n * that LLMs make when generating field names, such as using singular\n * instead of plural forms (e.g., `recommended_tool` vs `recommended_tools`).\n *\n * It also fills in sensible defaults for missing fields in `previous_steps`,\n * which LLMs naturally provide as partial/skeletal data (historical context).\n *\n * @module processor\n */\n\nimport { ValidationError } from '../errors.js';\nimport { sanitizeString } from '../sanitize.js';\nimport type { ThoughtData } from './thought.js';\n\n/**\n * Default values for missing partial tool recommendation fields.\n */\nconst DEFAULT_TOOL_CONFIDENCE = 0.5;\nconst DEFAULT_TOOL_PRIORITY = 999;\nconst DEFAULT_TOOL_RATIONALE = '';\nconst DEFAULT_STEP_OUTCOME = '';\n\n/**\n * Default values for missing skill recommendation fields.\n */\nconst DEFAULT_SKILL_CONFIDENCE = 0.5;\nconst DEFAULT_SKILL_PRIORITY = 999;\nconst DEFAULT_SKILL_RATIONALE = '';\n\n/**\n * Recursively sanitizes all string values within an unknown structure.\n * Walks into plain objects and arrays to reach deeply nested strings.\n * Non-plain objects (Date, RegExp, etc.) are returned as-is.\n *\n * @param value - The value to sanitize recursively\n * @returns The sanitized value with all nested strings cleaned\n *\n * @example\n * ```typescript\n * sanitizeRecursive('<script>x</script>'); // 'x'\n * sanitizeRecursive({ a: { b: '<iframe>y' } }); // { a: { b: 'y' } }\n * sanitizeRecursive(['a\\x00b', 42]); // ['ab', 42]\n * ```\n */\nexport function sanitizeRecursive(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn value;\n\t}\n\tif (typeof value === 'string') {\n\t\treturn sanitizeString(value);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((item) => sanitizeRecursive(item));\n\t}\n\tif (typeof value === 'object') {\n\t\t// Only recurse into plain objects — skip Date, RegExp, Map, Set, etc.\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\tif (proto !== Object.prototype && proto !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n\t\t\tresult[key] = sanitizeRecursive(val);\n\t\t}\n\t\treturn result;\n\t}\n\treturn value;\n}\n\n/**\n * Valid branch ID pattern: alphanumeric, hyphens, underscores only.\n * Prevents path traversal attacks by rejecting special characters like / . \\ etc.\n */\nconst BRANCH_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;\n\n/**\n * Valid session ID pattern: alphanumeric, hyphens, underscores, 1-100 chars.\n * Same as branch_id but with longer max length to allow compound identifiers.\n */\nconst SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]{1,100}$/;\n\n/**\n * Sanitizes and validates a branch ID to prevent path traversal attacks.\n *\n * @param branchId - The branch ID to sanitize\n * @returns The sanitized branch ID\n * @throws ValidationError if the branch ID contains invalid characters\n *\n * @example\n * ```typescript\n * sanitizeBranchId('my-branch_01'); // 'my-branch_01'\n * sanitizeBranchId('../etc/passwd'); // throws ValidationError\n * ```\n */\nexport function sanitizeBranchId(branchId: string): string {\n\t// Validate format\n\tif (!BRANCH_ID_PATTERN.test(branchId)) {\n\t\tthrow new ValidationError(\n\t\t\t'branch_id',\n\t\t\t'Invalid format - must be 1-64 alphanumeric characters, hyphens, or underscores only'\n\t\t);\n\t}\n\treturn branchId;\n}\n\n/**\n * Sanitizes and validates a session ID.\n *\n * @param sessionId - The session ID to sanitize\n * @returns The sanitized session ID, or undefined if invalid after sanitization\n *\n * @example\n * ```typescript\n * sanitizeSessionId('analysis-task-42'); // 'analysis-task-42'\n * sanitizeSessionId('bad session!'); // undefined (stripped)\n * ```\n */\nexport function sanitizeSessionId(sessionId: string): string | undefined {\n\t// First sanitize control characters\n\tconst cleaned = sanitizeString(sessionId);\n\t// Validate format after sanitization\n\tif (!SESSION_ID_PATTERN.test(cleaned)) {\n\t\treturn undefined;\n\t}\n\treturn cleaned;\n}\n\n/**\n * Normalizes tool recommendation objects with default values.\n * Normalizes tool recommendation objects with default values.\n *\n * Fills in sensible defaults for missing optional fields:\n * - `confidence`: 0.5\n * - `priority`: 999\n * - `rationale`: empty string\n *\n * @param tool - The tool recommendation to normalize\n * @returns The normalized tool recommendation with defaults filled in\n *\n * @example\n * ```typescript\n * const input = { tool_name: 'Read', rationale: 'Read the file' };\n * const normalized = normalizeToolRecommendation(input);\n * // { tool_name: 'Read', rationale: 'Read the file', confidence: 0.5, priority: 999 }\n * ```\n */\nfunction normalizeToolRecommendation(tool: Record<string, unknown>): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...tool };\n\n\t// Fill in default confidence if missing\n\tif (!('confidence' in normalized) || normalized.confidence === undefined) {\n\t\tnormalized.confidence = DEFAULT_TOOL_CONFIDENCE;\n\t}\n\n\t// Fill in default priority if missing\n\tif (!('priority' in normalized) || normalized.priority === undefined) {\n\t\tnormalized.priority = DEFAULT_TOOL_PRIORITY;\n\t}\n\n\t// Fill in default rationale if missing\n\tif (!('rationale' in normalized) || normalized.rationale === undefined) {\n\t\tnormalized.rationale = DEFAULT_TOOL_RATIONALE;\n\t}\n\n\treturn normalized;\n}\n\n/**\n * Normalizes skill recommendation objects with default values.\n *\n * Fills in sensible defaults for missing optional fields:\n * - `confidence`: 0.5\n * - `priority`: 999\n * - `rationale`: empty string\n *\n * @param skill - The skill recommendation to normalize\n * @returns The normalized skill recommendation with defaults filled in\n *\n * @example\n * ```typescript\n * const input = { skill_name: 'ast-grep' };\n * const normalized = normalizeSkillRecommendation(input);\n * // { skill_name: 'ast-grep', confidence: 0.5, priority: 999, rationale: '' }\n * ```\n */\nfunction normalizeSkillRecommendation(skill: Record<string, unknown>): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...skill };\n\n\t// Fill in default confidence if missing\n\tif (!('confidence' in normalized) || normalized.confidence === undefined) {\n\t\tnormalized.confidence = DEFAULT_SKILL_CONFIDENCE;\n\t}\n\n\t// Fill in default priority if missing\n\tif (!('priority' in normalized) || normalized.priority === undefined) {\n\t\tnormalized.priority = DEFAULT_SKILL_PRIORITY;\n\t}\n\n\t// Fill in default rationale if missing\n\tif (!('rationale' in normalized) || normalized.rationale === undefined) {\n\t\tnormalized.rationale = DEFAULT_SKILL_RATIONALE;\n\t}\n\n\treturn normalized;\n}\n/**\n * Normalizes step recommendation objects.\n *\n * Handles common field name mistakes:\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n *\n * Also normalizes tool recommendations within the step to fill in defaults.\n *\n * @param step - The step recommendation to normalize\n * @param lenient - Whether to use lenient mode (fill in defaults for missing fields)\n * @returns The normalized step recommendation\n *\n * @example\n * ```typescript\n * // Strict mode (for current_step)\n * const input = {\n * step_description: 'Analyze data',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data analyzed'\n * };\n * const normalized = normalizeStepRecommendation(input, false);\n * // normalized.recommended_tools exists (plural form)\n *\n * // Lenient mode (for previous_steps)\n * const partialInput = {\n * step_description: 'Read file',\n * recommended_tools: [{ tool_name: 'Read', rationale: 'Read file' }]\n * };\n * const normalized = normalizeStepRecommendation(partialInput, true);\n * // confidence: 0.5, priority: 999, expected_outcome: '' filled in\n * ```\n */\nfunction normalizeStepRecommendation(\n\tstep: Record<string, unknown>,\n\tlenient: boolean\n): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...step };\n\n\t// Transform `recommended_tool` (singular) → `recommended_tools` (plural)\n\tif ('recommended_tool' in normalized && !('recommended_tools' in normalized)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tool;\n\t\tdelete normalized.recommended_tool;\n\t}\n\n\t// Transform `recommended_skill` (singular) → `recommended_skills` (plural)\n\tif ('recommended_skill' in normalized && !('recommended_skills' in normalized)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skill;\n\t\tdelete normalized.recommended_skill;\n\t}\n\n\t// Normalize recommended_tools array if present\n\tif (Array.isArray(normalized.recommended_tools)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tools.map((tool) =>\n\t\t\ttypeof tool === 'object' && tool !== null\n\t\t\t\t? normalizeToolRecommendation(tool as Record<string, unknown>)\n\t\t\t\t: tool\n\t\t);\n\t}\n\n\t// Normalize recommended_skills array if present\n\tif (Array.isArray(normalized.recommended_skills)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skills.map((skill) =>\n\t\t\ttypeof skill === 'object' && skill !== null\n\t\t\t\t? normalizeSkillRecommendation(skill as Record<string, unknown>)\n\t\t\t\t: skill\n\t\t);\n\t}\n\t// In lenient mode, fill in default expected_outcome if missing\n\tif (lenient && !('expected_outcome' in normalized)) {\n\t\tnormalized.expected_outcome = DEFAULT_STEP_OUTCOME;\n\t}\n\n\treturn normalized;\n}\n\n\n/**\n * Normalizes reasoning-specific fields on a thought input object.\n * Always applies reasoning normalization — reasoning is the default pipeline.\n * Applies the following normalization rules:\n * - Defaults `thought_type` to `'regular'` if not provided\n * - Clamps `quality_score` to [0, 1] range\n * - Clamps `confidence` to [0, 1] range\n * - Sanitizes `hypothesis_id` using `sanitizeBranchId` pattern\n * - Filters `synthesis_sources` to positive integers only\n * - Filters `merge_from_thoughts` to positive integers only\n * - Sanitizes each entry in `merge_branch_ids`\n * - Defaults `reasoning_depth` to `'moderate'` for hypothesis/verification types\n *\n * @param input - The mutable normalized input object to apply reasoning defaults to\n *\n * @example\n * ```typescript\n * const input: Record<string, unknown> = { thought_type: 'hypothesis', quality_score: 1.5 };\n * normalizeReasoningFields(input);\n * // input.quality_score === 1, input.reasoning_depth === 'moderate'\n * ```\n */\nexport function normalizeReasoningFields(input: Record<string, unknown>): void {\n\t// Always apply reasoning field normalization — reasoning is the default pipeline\n\t// Default thought_type to 'regular'\n\n\tif (!('thought_type' in input) || input.thought_type === undefined) {\n\t\tinput.thought_type = 'regular';\n\t}\n\n\t// Clamp quality_score to [0, 1]\n\tif (typeof input.quality_score === 'number') {\n\t\tinput.quality_score = Math.max(0, Math.min(1, input.quality_score));\n\t}\n\n\t// Clamp confidence to [0, 1]\n\tif (typeof input.confidence === 'number') {\n\t\tinput.confidence = Math.max(0, Math.min(1, input.confidence));\n\t}\n\n\t// Sanitize hypothesis_id (same rules as branch_id)\n\tif (typeof input.hypothesis_id === 'string') {\n\t\tinput.hypothesis_id = sanitizeBranchId(input.hypothesis_id);\n\t}\n\n\t// Filter synthesis_sources to positive integers only\n\tif (Array.isArray(input.synthesis_sources)) {\n\t\tinput.synthesis_sources = input.synthesis_sources.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Filter merge_from_thoughts to positive integers only\n\tif (Array.isArray(input.merge_from_thoughts)) {\n\t\tinput.merge_from_thoughts = input.merge_from_thoughts.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Sanitize merge_branch_ids entries\n\tif (Array.isArray(input.merge_branch_ids)) {\n\t\tinput.merge_branch_ids = input.merge_branch_ids.map((id: unknown) => {\n\t\t\tif (typeof id === 'string') {\n\t\t\t\treturn sanitizeBranchId(id);\n\t\t\t}\n\t\t\treturn id;\n\t\t});\n\t}\n\n\t// Default reasoning_depth to 'moderate' for hypothesis/verification types\n\tif (\n\t\t(input.thought_type === 'hypothesis' || input.thought_type === 'verification') &&\n\t\t!('reasoning_depth' in input)\n\t) {\n\t\tinput.reasoning_depth = 'moderate';\n\t}\n}\n\n/**\n * Normalizes thought input data by fixing common LLM field name mistakes.\n *\n * This function handles cases where LLMs incorrectly use singular forms\n * of field names that should be plural. It applies normalization to both\n * `current_step` and `previous_steps` fields.\n *\n * The normalization is applied BEFORE schema validation, allowing the\n * strict Valibot schema to remain correct while still being tolerant\n * of common LLM mistakes.\n *\n * @param input - The raw thought input data to normalize\n * @returns Normalized thought data with correct field names\n *\n * @remarks\n * **Normalization Rules:**\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n * - Applied to `current_step` if present (strict mode)\n * - Applied to all items in `previous_steps` if present (lenient mode with defaults)\n *\n * **Design Rationale:**\n * LLMs sometimes use singular field names even when the schema explicitly\n * defines plural forms. Rather than forcing the LLM to be perfect (which\n * leads to cryptic validation errors), we normalize the input to handle\n * these common mistakes gracefully.\n *\n * Additionally, LLMs naturally provide complete data for `current_step`\n * but only partial/skeletal data for `previous_steps` (historical context).\n * The lenient mode for `previous_steps` fills in sensible defaults:\n * - `confidence`: 0.5 for missing tool recommendation confidence\n * - `priority`: 999 for missing tool recommendation priority\n * - `rationale`: empty string for missing tool recommendation rationale\n * - `expected_outcome`: empty string for missing step expected outcome\n *\n * @example\n * ```typescript\n * const input = {\n * thought: 'I need to analyze the data',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Read the data file',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data loaded'\n * },\n * previous_steps: [{\n * step_description: 'Previous step',\n * recommended_tools: [{ tool_name: 'Grep', rationale: 'Search code' }]\n * }]\n * };\n *\n * const normalized = normalizeInput(input);\n * // current_step: recommended_tools exists (plural form)\n * // previous_steps[0]: confidence=0.5, priority=999, expected_outcome='' filled in\n * ```\n */\nexport function normalizeInput(input: unknown): ThoughtData {\n\tif (typeof input !== 'object' || input === null) {\n\t\treturn input as ThoughtData;\n\t}\n\n\tconst normalized = { ...input } as Record<string, unknown>;\n\n\t// Normalize current_step if present (strict mode - no defaults)\n\tif (normalized.current_step && typeof normalized.current_step === 'object') {\n\t\tnormalized.current_step = normalizeStepRecommendation(\n\t\t\tnormalized.current_step as Record<string, unknown>,\n\t\t\tfalse // strict mode\n\t\t);\n\t}\n\n\n\t// Normalize all items in previous_steps if present (lenient mode - with defaults)\n\tif (Array.isArray(normalized.previous_steps) && normalized.previous_steps.length > 0) {\n\t\tnormalized.previous_steps = normalized.previous_steps.map((step) =>\n\t\t\ttypeof step === 'object' && step !== null\n\t\t\t\t? normalizeStepRecommendation(step as Record<string, unknown>, true) // lenient mode\n\t\t\t\t: step\n\t\t);\n\t}\n\n\t// Sanitize branch_id to prevent path traversal attacks\n\tif (typeof normalized.branch_id === 'string') {\n\t\tnormalized.branch_id = sanitizeBranchId(normalized.branch_id);\n\t}\n\n\t// Sanitize session_id (same pattern as branch_id but allows 1-100 chars)\n\tif (typeof normalized.session_id === 'string') {\n\t\tconst sanitized = sanitizeSessionId(normalized.session_id);\n\t\tif (sanitized === undefined) {\n\t\t\tdelete normalized.session_id;\n\t\t} else {\n\t\t\tnormalized.session_id = sanitized;\n\t\t}\n\t}\n\n\t// Normalize reasoning fields\n\tnormalizeReasoningFields(normalized);\n\n\n\t// Sanitize all free-text string fields recursively (dangerous HTML tags + null bytes)\n\t// This was moved from schema transforms because v.transform() cannot be converted to JSON Schema\n\tconst sanitized = sanitizeRecursive(normalized);\n\n\treturn sanitized as unknown as ThoughtData;\n}\n"],"names":["DEFAULT_TOOL_CONFIDENCE","DEFAULT_TOOL_PRIORITY","DEFAULT_TOOL_RATIONALE","DEFAULT_STEP_OUTCOME","DEFAULT_SKILL_CONFIDENCE","DEFAULT_SKILL_PRIORITY","DEFAULT_SKILL_RATIONALE","sanitizeRecursive","value","sanitizeString","Array","item","proto","Object","result","key","val","BRANCH_ID_PATTERN","SESSION_ID_PATTERN","sanitizeBranchId","branchId","ValidationError","sanitizeSessionId","sessionId","cleaned","normalizeToolRecommendation","tool","normalized","undefined","normalizeSkillRecommendation","skill","normalizeStepRecommendation","step","lenient","normalizeReasoningFields","input","Math","v","Number","id","normalizeInput","sanitized"],"mappings":"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"}
1
+ {"version":3,"file":"core/InputNormalizer.js","sources":["../../src/core/InputNormalizer.ts"],"sourcesContent":["/**\n * Input normalization for common LLM field name mistakes.\n *\n * This module provides normalization logic to handle common mistakes\n * that LLMs make when generating field names, such as using singular\n * instead of plural forms (e.g., `recommended_tool` vs `recommended_tools`).\n *\n * It also fills in sensible defaults for missing fields in `previous_steps`,\n * which LLMs naturally provide as partial/skeletal data (historical context).\n *\n * @module processor\n */\n\nimport { ValidationError } from '../errors.js';\nimport { sanitizeString } from '../sanitize.js';\nimport type { ThoughtData } from './thought.js';\nimport { generateUlid, SESSION_ID_PATTERN, MAX_SESSION_ID_LENGTH } from './ids.js';\n\n/**\n * Default values for missing partial recommendation fields.\n *\n * Shared between tool and skill recommendations (identical defaults).\n */\nconst DEFAULT_RECOMMENDATION_CONFIDENCE = 0.5;\nconst DEFAULT_RECOMMENDATION_PRIORITY = 999;\nconst DEFAULT_RECOMMENDATION_RATIONALE = '';\nconst DEFAULT_STEP_OUTCOME = '';\n\n/**\n * Recursively sanitizes all string values within an unknown structure.\n * Walks into plain objects and arrays to reach deeply nested strings.\n * Non-plain objects (Date, RegExp, etc.) are returned as-is.\n *\n * @param value - The value to sanitize recursively\n * @returns The sanitized value with all nested strings cleaned\n *\n * @example\n * ```typescript\n * sanitizeRecursive('<script>x</script>'); // 'x'\n * sanitizeRecursive({ a: { b: '<iframe>y' } }); // { a: { b: 'y' } }\n * sanitizeRecursive(['a\\x00b', 42]); // ['ab', 42]\n * ```\n */\nexport function sanitizeRecursive(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn value;\n\t}\n\tif (typeof value === 'string') {\n\t\treturn sanitizeString(value);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((item) => sanitizeRecursive(item));\n\t}\n\tif (typeof value === 'object') {\n\t\t// Only recurse into plain objects — skip Date, RegExp, Map, Set, etc.\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\tif (proto !== Object.prototype && proto !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n\t\t\tresult[key] = sanitizeRecursive(val);\n\t\t}\n\t\treturn result;\n\t}\n\treturn value;\n}\n\n/**\n * Valid branch ID pattern: alphanumeric, hyphens, underscores only.\n * Prevents path traversal attacks by rejecting special characters like / . \\ etc.\n */\nconst BRANCH_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;\n\n\n\n/**\n * Sanitizes and validates a branch ID to prevent path traversal attacks.\n *\n * @param branchId - The branch ID to sanitize\n * @returns The sanitized branch ID\n * @throws ValidationError if the branch ID contains invalid characters\n *\n * @example\n * ```typescript\n * sanitizeBranchId('my-branch_01'); // 'my-branch_01'\n * sanitizeBranchId('../etc/passwd'); // throws ValidationError\n * ```\n */\nexport function sanitizeBranchId(branchId: string): string {\n\t// Validate format\n\tif (!BRANCH_ID_PATTERN.test(branchId)) {\n\t\tthrow new ValidationError(\n\t\t\t'branch_id',\n\t\t\t'Invalid format - must be 1-64 alphanumeric characters, hyphens, or underscores only'\n\t\t);\n\t}\n\treturn branchId;\n}\n\n/**\n * Sanitizes and validates a session ID.\n *\n * @param sessionId - The session ID to sanitize\n * @returns The sanitized session ID, or undefined if invalid after sanitization\n *\n * @example\n * ```typescript\n * sanitizeSessionId('analysis-task-42'); // 'analysis-task-42'\n * sanitizeSessionId('bad session!'); // undefined (stripped)\n * ```\n */\nexport function sanitizeSessionId(sessionId: string): string | undefined {\n\t// First sanitize control characters\n\tconst cleaned = sanitizeString(sessionId);\n\t// Validate format after sanitization\n\tif (cleaned.length > MAX_SESSION_ID_LENGTH || !SESSION_ID_PATTERN.test(cleaned)) {\n\t\treturn undefined;\n\t}\n\treturn cleaned;\n}\n\n\n/**\n * Normalizes a recommendation object (tool or skill) with default values.\n *\n * Fills in sensible defaults for missing optional fields:\n * - `confidence`: 0.5\n * - `priority`: 999\n * - `rationale`: empty string\n *\n * @param rec - The recommendation object to normalize\n * @returns The normalized recommendation with defaults filled in\n *\n * @example\n * ```typescript\n * const input = { tool_name: 'Read', rationale: 'Read the file' };\n * const normalized = normalizeRecommendation(input);\n * // { tool_name: 'Read', rationale: 'Read the file', confidence: 0.5, priority: 999 }\n * ```\n */\nfunction normalizeRecommendation(rec: Record<string, unknown>): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...rec };\n\n\t// Fill in default confidence if missing\n\tif (!('confidence' in normalized) || normalized.confidence === undefined) {\n\t\tnormalized.confidence = DEFAULT_RECOMMENDATION_CONFIDENCE;\n\t}\n\n\t// Fill in default priority if missing\n\tif (!('priority' in normalized) || normalized.priority === undefined) {\n\t\tnormalized.priority = DEFAULT_RECOMMENDATION_PRIORITY;\n\t}\n\n\t// Fill in default rationale if missing\n\tif (!('rationale' in normalized) || normalized.rationale === undefined) {\n\t\tnormalized.rationale = DEFAULT_RECOMMENDATION_RATIONALE;\n\t}\n\n\treturn normalized;\n}\n/**\n * Normalizes step recommendation objects.\n *\n * Handles common field name mistakes:\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n *\n * Also normalizes tool recommendations within the step to fill in defaults.\n *\n * @param step - The step recommendation to normalize\n * @param lenient - Whether to use lenient mode (fill in defaults for missing fields)\n * @returns The normalized step recommendation\n *\n * @example\n * ```typescript\n * // Strict mode (for current_step)\n * const input = {\n * step_description: 'Analyze data',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data analyzed'\n * };\n * const normalized = normalizeStepRecommendation(input, false);\n * // normalized.recommended_tools exists (plural form)\n *\n * // Lenient mode (for previous_steps)\n * const partialInput = {\n * step_description: 'Read file',\n * recommended_tools: [{ tool_name: 'Read', rationale: 'Read file' }]\n * };\n * const normalized = normalizeStepRecommendation(partialInput, true);\n * // confidence: 0.5, priority: 999, expected_outcome: '' filled in\n * ```\n */\nfunction normalizeStepRecommendation(\n\tstep: Record<string, unknown>,\n\tlenient: boolean\n): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...step };\n\n\t// Transform `recommended_tool` (singular) → `recommended_tools` (plural)\n\tif ('recommended_tool' in normalized && !('recommended_tools' in normalized)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tool;\n\t\tdelete normalized.recommended_tool;\n\t}\n\n\t// Transform `recommended_skill` (singular) → `recommended_skills` (plural)\n\tif ('recommended_skill' in normalized && !('recommended_skills' in normalized)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skill;\n\t\tdelete normalized.recommended_skill;\n\t}\n\n\t// Normalize recommended_tools array if present\n\tif (Array.isArray(normalized.recommended_tools)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tools.map((tool) =>\n\t\t\ttypeof tool === 'object' && tool !== null\n\t\t\t\t? normalizeRecommendation(tool as Record<string, unknown>)\n\t\t\t\t: tool\n\t\t);\n\t}\n\n\t// Normalize recommended_skills array if present\n\tif (Array.isArray(normalized.recommended_skills)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skills.map((skill) =>\n\t\t\ttypeof skill === 'object' && skill !== null\n\t\t\t\t? normalizeRecommendation(skill as Record<string, unknown>)\n\t\t\t\t: skill\n\t\t);\n\t}\n\t// In lenient mode, fill in default expected_outcome if missing\n\tif (lenient && !('expected_outcome' in normalized)) {\n\t\tnormalized.expected_outcome = DEFAULT_STEP_OUTCOME;\n\t}\n\n\treturn normalized;\n}\n\n\n/**\n * Normalizes reasoning-specific fields on a thought input object.\n * Always applies reasoning normalization — reasoning is the default pipeline.\n * Applies the following normalization rules:\n * - Defaults `thought_type` to `'regular'` if not provided\n * - Clamps `quality_score` to [0, 1] range\n * - Clamps `confidence` to [0, 1] range\n * - Sanitizes `hypothesis_id` using `sanitizeBranchId` pattern\n * - Filters `synthesis_sources` to positive integers only\n * - Filters `merge_from_thoughts` to positive integers only\n * - Sanitizes each entry in `merge_branch_ids`\n * - Defaults `reasoning_depth` to `'moderate'` for hypothesis/verification types\n *\n * @param input - The mutable normalized input object to apply reasoning defaults to\n *\n * @example\n * ```typescript\n * const input: Record<string, unknown> = { thought_type: 'hypothesis', quality_score: 1.5 };\n * normalizeReasoningFields(input);\n * // input.quality_score === 1, input.reasoning_depth === 'moderate'\n * ```\n */\nexport function normalizeReasoningFields(input: Record<string, unknown>): void {\n\t// Always apply reasoning field normalization — reasoning is the default pipeline\n\t// Default thought_type to 'regular'\n\n\tif (!('thought_type' in input) || input.thought_type === undefined) {\n\t\tinput.thought_type = 'regular';\n\t}\n\n\t// Clamp quality_score to [0, 1]\n\tif (typeof input.quality_score === 'number') {\n\t\tinput.quality_score = Math.max(0, Math.min(1, input.quality_score));\n\t}\n\n\t// Clamp confidence to [0, 1]\n\tif (typeof input.confidence === 'number') {\n\t\tinput.confidence = Math.max(0, Math.min(1, input.confidence));\n\t}\n\n\t// Sanitize hypothesis_id (same rules as branch_id)\n\tif (typeof input.hypothesis_id === 'string') {\n\t\tinput.hypothesis_id = sanitizeBranchId(input.hypothesis_id);\n\t}\n\n\t// Filter synthesis_sources to positive integers only\n\tif (Array.isArray(input.synthesis_sources)) {\n\t\tinput.synthesis_sources = input.synthesis_sources.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Filter merge_from_thoughts to positive integers only\n\tif (Array.isArray(input.merge_from_thoughts)) {\n\t\tinput.merge_from_thoughts = input.merge_from_thoughts.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Sanitize merge_branch_ids entries\n\tif (Array.isArray(input.merge_branch_ids)) {\n\t\tinput.merge_branch_ids = input.merge_branch_ids.map((id: unknown) => {\n\t\t\tif (typeof id === 'string') {\n\t\t\t\treturn sanitizeBranchId(id);\n\t\t\t}\n\t\t\treturn id;\n\t\t});\n\t}\n\n\t// Default reasoning_depth to 'moderate' for hypothesis/verification types\n\tif (\n\t\t(input.thought_type === 'hypothesis' || input.thought_type === 'verification') &&\n\t\t!('reasoning_depth' in input)\n\t) {\n\t\tinput.reasoning_depth = 'moderate';\n\t}\n}\n\n/**\n * Normalizes thought input data by fixing common LLM field name mistakes.\n *\n * This function handles cases where LLMs incorrectly use singular forms\n * of field names that should be plural. It applies normalization to both\n * `current_step` and `previous_steps` fields.\n *\n * The normalization is applied BEFORE schema validation, allowing the\n * strict Valibot schema to remain correct while still being tolerant\n * of common LLM mistakes.\n *\n * @param input - The raw thought input data to normalize\n * @returns Normalized thought data with correct field names\n *\n * @remarks\n * **Normalization Rules:**\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n * - Applied to `current_step` if present (strict mode)\n * - Applied to all items in `previous_steps` if present (lenient mode with defaults)\n *\n * **Design Rationale:**\n * LLMs sometimes use singular field names even when the schema explicitly\n * defines plural forms. Rather than forcing the LLM to be perfect (which\n * leads to cryptic validation errors), we normalize the input to handle\n * these common mistakes gracefully.\n *\n * Additionally, LLMs naturally provide complete data for `current_step`\n * but only partial/skeletal data for `previous_steps` (historical context).\n * The lenient mode for `previous_steps` fills in sensible defaults:\n * - `confidence`: 0.5 for missing tool recommendation confidence\n * - `priority`: 999 for missing tool recommendation priority\n * - `rationale`: empty string for missing tool recommendation rationale\n * - `expected_outcome`: empty string for missing step expected outcome\n *\n * @example\n * ```typescript\n * const input = {\n * thought: 'I need to analyze the data',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Read the data file',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data loaded'\n * },\n * previous_steps: [{\n * step_description: 'Previous step',\n * recommended_tools: [{ tool_name: 'Grep', rationale: 'Search code' }]\n * }]\n * };\n *\n * const normalized = normalizeInput(input);\n * // current_step: recommended_tools exists (plural form)\n * // previous_steps[0]: confidence=0.5, priority=999, expected_outcome='' filled in\n * ```\n */\nexport function normalizeInput(input: unknown): ThoughtData {\n\tif (typeof input !== 'object' || input === null) {\n\t\treturn input as ThoughtData;\n\t}\n\n\tconst normalized = { ...input } as Record<string, unknown>;\n\n\t// Normalize current_step if present (strict mode - no defaults)\n\tif (normalized.current_step && typeof normalized.current_step === 'object') {\n\t\tnormalized.current_step = normalizeStepRecommendation(\n\t\t\tnormalized.current_step as Record<string, unknown>,\n\t\t\tfalse // strict mode\n\t\t);\n\t}\n\n\n\t// Normalize all items in previous_steps if present (lenient mode - with defaults)\n\tif (Array.isArray(normalized.previous_steps) && normalized.previous_steps.length > 0) {\n\t\tnormalized.previous_steps = normalized.previous_steps.map((step) =>\n\t\t\ttypeof step === 'object' && step !== null\n\t\t\t\t? normalizeStepRecommendation(step as Record<string, unknown>, true) // lenient mode\n\t\t\t\t: step\n\t\t);\n\t}\n\n\t// Sanitize branch_id to prevent path traversal attacks\n\tif (typeof normalized.branch_id === 'string') {\n\t\tnormalized.branch_id = sanitizeBranchId(normalized.branch_id);\n\t}\n\n\t// Sanitize session_id (same pattern as branch_id but allows 1-100 chars)\n\tif (typeof normalized.session_id === 'string') {\n\t\tconst sanitized = sanitizeSessionId(normalized.session_id);\n\t\tif (sanitized === undefined) {\n\t\t\tdelete normalized.session_id;\n\t\t} else {\n\t\t\tnormalized.session_id = sanitized;\n\t\t}\n\t}\n\n\t// Auto-generate id if not provided (for DAG node identity)\n\tif (!normalized.id || typeof normalized.id !== 'string') {\n\t\tnormalized.id = generateUlid();\n\t}\n\n\t// Normalize reasoning fields\n\tnormalizeReasoningFields(normalized);\n\n\n\t// Sanitize all free-text string fields recursively (dangerous HTML tags + null bytes)\n\t// This was moved from schema transforms because v.transform() cannot be converted to JSON Schema\n\tconst sanitized = sanitizeRecursive(normalized);\n\n\treturn sanitized as unknown as ThoughtData;\n}\n"],"names":["DEFAULT_RECOMMENDATION_CONFIDENCE","DEFAULT_RECOMMENDATION_PRIORITY","DEFAULT_RECOMMENDATION_RATIONALE","DEFAULT_STEP_OUTCOME","sanitizeRecursive","value","sanitizeString","Array","item","proto","Object","result","key","val","BRANCH_ID_PATTERN","sanitizeBranchId","branchId","ValidationError","sanitizeSessionId","sessionId","cleaned","MAX_SESSION_ID_LENGTH","SESSION_ID_PATTERN","normalizeRecommendation","rec","normalized","undefined","normalizeStepRecommendation","step","lenient","tool","skill","normalizeReasoningFields","input","Math","v","Number","id","normalizeInput","sanitized","generateUlid"],"mappings":";;;AAuBA,MAAMA,oCAAoC;AAC1C,MAAMC,kCAAkC;AACxC,MAAMC,mCAAmC;AACzC,MAAMC,uBAAuB;AAiBtB,SAASC,kBAAkBC,KAAc;IAC/C,IAAIA,QAAAA,OACH,OAAOA;IAER,IAAI,AAAiB,YAAjB,OAAOA,OACV,OAAOC,eAAeD;IAEvB,IAAIE,MAAM,OAAO,CAACF,QACjB,OAAOA,MAAM,GAAG,CAAC,CAACG,OAASJ,kBAAkBI;IAE9C,IAAI,AAAiB,YAAjB,OAAOH,OAAoB;QAE9B,MAAMI,QAAQC,OAAO,cAAc,CAACL;QACpC,IAAII,UAAUC,OAAO,SAAS,IAAID,AAAU,SAAVA,OACjC,OAAOJ;QAER,MAAMM,SAAkC,CAAC;QACzC,KAAK,MAAM,CAACC,KAAKC,IAAI,IAAIH,OAAO,OAAO,CAACL,OACvCM,MAAM,CAACC,IAAI,GAAGR,kBAAkBS;QAEjC,OAAOF;IACR;IACA,OAAON;AACR;AAMA,MAAMS,oBAAoB;AAiBnB,SAASC,iBAAiBC,QAAgB;IAEhD,IAAI,CAACF,kBAAkB,IAAI,CAACE,WAC3B,MAAM,IAAIC,gBACT,aACA;IAGF,OAAOD;AACR;AAcO,SAASE,kBAAkBC,SAAiB;IAElD,MAAMC,UAAUd,eAAea;IAE/B,IAAIC,QAAQ,MAAM,GAAGC,yBAAyB,CAACC,mBAAmB,IAAI,CAACF,UACtE;IAED,OAAOA;AACR;AAqBA,SAASG,wBAAwBC,GAA4B;IAC5D,MAAMC,aAAsC;QAAE,GAAGD,GAAG;IAAC;IAGrD,IAAI,CAAE,iBAAgBC,UAAS,KAAMA,AAA0BC,WAA1BD,WAAW,UAAU,EACzDA,WAAW,UAAU,GAAGzB;IAIzB,IAAI,CAAE,eAAcyB,UAAS,KAAMA,AAAwBC,WAAxBD,WAAW,QAAQ,EACrDA,WAAW,QAAQ,GAAGxB;IAIvB,IAAI,CAAE,gBAAewB,UAAS,KAAMA,AAAyBC,WAAzBD,WAAW,SAAS,EACvDA,WAAW,SAAS,GAAGvB;IAGxB,OAAOuB;AACR;AAkCA,SAASE,4BACRC,IAA6B,EAC7BC,OAAgB;IAEhB,MAAMJ,aAAsC;QAAE,GAAGG,IAAI;IAAC;IAGtD,IAAI,sBAAsBH,cAAc,CAAE,wBAAuBA,UAAS,GAAI;QAC7EA,WAAW,iBAAiB,GAAGA,WAAW,gBAAgB;QAC1D,OAAOA,WAAW,gBAAgB;IACnC;IAGA,IAAI,uBAAuBA,cAAc,CAAE,yBAAwBA,UAAS,GAAI;QAC/EA,WAAW,kBAAkB,GAAGA,WAAW,iBAAiB;QAC5D,OAAOA,WAAW,iBAAiB;IACpC;IAGA,IAAIlB,MAAM,OAAO,CAACkB,WAAW,iBAAiB,GAC7CA,WAAW,iBAAiB,GAAGA,WAAW,iBAAiB,CAAC,GAAG,CAAC,CAACK,OAChE,AAAgB,YAAhB,OAAOA,QAAqBA,AAAS,SAATA,OACzBP,wBAAwBO,QACxBA;IAKL,IAAIvB,MAAM,OAAO,CAACkB,WAAW,kBAAkB,GAC9CA,WAAW,kBAAkB,GAAGA,WAAW,kBAAkB,CAAC,GAAG,CAAC,CAACM,QAClE,AAAiB,YAAjB,OAAOA,SAAsBA,AAAU,SAAVA,QAC1BR,wBAAwBQ,SACxBA;IAIL,IAAIF,WAAW,CAAE,uBAAsBJ,UAAS,GAC/CA,WAAW,gBAAgB,GAAGtB;IAG/B,OAAOsB;AACR;AAyBO,SAASO,yBAAyBC,KAA8B;IAItE,IAAI,CAAE,mBAAkBA,KAAI,KAAMA,AAAuBP,WAAvBO,MAAM,YAAY,EACnDA,MAAM,YAAY,GAAG;IAItB,IAAI,AAA+B,YAA/B,OAAOA,MAAM,aAAa,EAC7BA,MAAM,aAAa,GAAGC,KAAK,GAAG,CAAC,GAAGA,KAAK,GAAG,CAAC,GAAGD,MAAM,aAAa;IAIlE,IAAI,AAA4B,YAA5B,OAAOA,MAAM,UAAU,EAC1BA,MAAM,UAAU,GAAGC,KAAK,GAAG,CAAC,GAAGA,KAAK,GAAG,CAAC,GAAGD,MAAM,UAAU;IAI5D,IAAI,AAA+B,YAA/B,OAAOA,MAAM,aAAa,EAC7BA,MAAM,aAAa,GAAGlB,iBAAiBkB,MAAM,aAAa;IAI3D,IAAI1B,MAAM,OAAO,CAAC0B,MAAM,iBAAiB,GACxCA,MAAM,iBAAiB,GAAGA,MAAM,iBAAiB,CAAC,MAAM,CACvD,CAACE,IAAe,AAAa,YAAb,OAAOA,KAAkBC,OAAO,SAAS,CAACD,MAAMA,IAAI;IAKtE,IAAI5B,MAAM,OAAO,CAAC0B,MAAM,mBAAmB,GAC1CA,MAAM,mBAAmB,GAAGA,MAAM,mBAAmB,CAAC,MAAM,CAC3D,CAACE,IAAe,AAAa,YAAb,OAAOA,KAAkBC,OAAO,SAAS,CAACD,MAAMA,IAAI;IAKtE,IAAI5B,MAAM,OAAO,CAAC0B,MAAM,gBAAgB,GACvCA,MAAM,gBAAgB,GAAGA,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAACI;QACpD,IAAI,AAAc,YAAd,OAAOA,IACV,OAAOtB,iBAAiBsB;QAEzB,OAAOA;IACR;IAID,IACEJ,AAAAA,CAAAA,AAAuB,iBAAvBA,MAAM,YAAY,IAAqBA,AAAuB,mBAAvBA,MAAM,YAAY,AAAkB,KAC5E,CAAE,sBAAqBA,KAAI,GAE3BA,MAAM,eAAe,GAAG;AAE1B;AA4DO,SAASK,eAAeL,KAAc;IAC5C,IAAI,AAAiB,YAAjB,OAAOA,SAAsBA,AAAU,SAAVA,OAChC,OAAOA;IAGR,MAAMR,aAAa;QAAE,GAAGQ,KAAK;IAAC;IAG9B,IAAIR,WAAW,YAAY,IAAI,AAAmC,YAAnC,OAAOA,WAAW,YAAY,EAC5DA,WAAW,YAAY,GAAGE,4BACzBF,WAAW,YAAY,EACvB;IAMF,IAAIlB,MAAM,OAAO,CAACkB,WAAW,cAAc,KAAKA,WAAW,cAAc,CAAC,MAAM,GAAG,GAClFA,WAAW,cAAc,GAAGA,WAAW,cAAc,CAAC,GAAG,CAAC,CAACG,OAC1D,AAAgB,YAAhB,OAAOA,QAAqBA,AAAS,SAATA,OACzBD,4BAA4BC,MAAiC,QAC7DA;IAKL,IAAI,AAAgC,YAAhC,OAAOH,WAAW,SAAS,EAC9BA,WAAW,SAAS,GAAGV,iBAAiBU,WAAW,SAAS;IAI7D,IAAI,AAAiC,YAAjC,OAAOA,WAAW,UAAU,EAAe;QAC9C,MAAMc,YAAYrB,kBAAkBO,WAAW,UAAU;QACzD,IAAIc,AAAcb,WAAda,WACH,OAAOd,WAAW,UAAU;aAE5BA,WAAW,UAAU,GAAGc;IAE1B;IAGA,IAAI,CAACd,WAAW,EAAE,IAAI,AAAyB,YAAzB,OAAOA,WAAW,EAAE,EACzCA,WAAW,EAAE,GAAGe;IAIjBR,yBAAyBP;IAKzB,MAAMc,YAAYnC,kBAAkBqB;IAEpC,OAAOc;AACR"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * PersistenceBuffer — owns the periodic flush timer, retry/backoff logic, and
3
+ * concurrent-flush guarding for HistoryManager's persistence write buffer.
4
+ *
5
+ * Extracted from HistoryManager. The session Map and write buffers remain
6
+ * owned by HistoryManager (passed by reference) so public access patterns
7
+ * (e.g. `manager._sessions`) are preserved.
8
+ *
9
+ * @module PersistenceBuffer
10
+ */
11
+ import type { IEdgeStore } from '../contracts/interfaces.js';
12
+ import type { Logger } from '../logger/StructuredLogger.js';
13
+ import type { PersistenceBackend } from '../persistence/PersistenceBackend.js';
14
+ import type { ThoughtData } from './thought.js';
15
+ /** Minimal session view: anything that owns a `writeBuffer`. */
16
+ export interface BufferedSession {
17
+ writeBuffer: ThoughtData[];
18
+ }
19
+ /** Event emitter contract for persistence error events. */
20
+ export interface PersistenceEventEmitter {
21
+ emit(event: 'persistenceError', payload: {
22
+ operation: string;
23
+ error: Error;
24
+ }): boolean;
25
+ }
26
+ /** Configuration options for PersistenceBuffer. */
27
+ export interface PersistenceBufferConfig<S extends BufferedSession> {
28
+ persistence: PersistenceBackend;
29
+ bufferSize: number;
30
+ flushInterval: number;
31
+ maxRetries: number;
32
+ defaultSessionId: string;
33
+ /** Returns the live session map (called on each flush). */
34
+ getSessions: () => Map<string, S>;
35
+ /** Returns the requeue target session (default-session buffer). */
36
+ getDefaultSession: () => S;
37
+ /** Optional EdgeStore for flushing edges alongside thoughts. */
38
+ edgeStore?: IEdgeStore;
39
+ /** Optional emitter for `persistenceError` events. */
40
+ eventEmitter?: PersistenceEventEmitter | null;
41
+ logger?: Logger;
42
+ }
43
+ /**
44
+ * Manages buffered persistence writes with periodic flushing, exponential
45
+ * backoff retries, concurrent-flush guarding, and edge store integration.
46
+ */
47
+ export declare class PersistenceBuffer<S extends BufferedSession> {
48
+ private readonly _persistence;
49
+ private readonly _bufferSize;
50
+ private readonly _flushInterval;
51
+ private readonly _maxRetries;
52
+ private readonly _defaultSessionId;
53
+ private readonly _getSessions;
54
+ private readonly _getDefaultSession;
55
+ private readonly _edgeStore?;
56
+ private _eventEmitter;
57
+ private readonly _logger;
58
+ private _flushTimer;
59
+ private _isFlushing;
60
+ private _flushRetryCount;
61
+ constructor(config: PersistenceBufferConfig<S>);
62
+ /** Returns the underlying flush timer (for test introspection). */
63
+ get timer(): ReturnType<typeof setInterval> | null;
64
+ /** Returns true when a flush is in progress. */
65
+ get isFlushing(): boolean;
66
+ /** Sets / replaces the persistence error event emitter. */
67
+ setEventEmitter(emitter: PersistenceEventEmitter | null): void;
68
+ /**
69
+ * Buffers a thought into the given session's write buffer. Triggers an
70
+ * immediate flush when the buffer reaches `bufferSize`.
71
+ */
72
+ bufferThought(session: BufferedSession, thought: ThoughtData): void;
73
+ /**
74
+ * Starts the periodic flush timer. No-op if already started.
75
+ * The timer is unref'd so it does not block process exit.
76
+ */
77
+ startFlushTimer(): void;
78
+ /** Stops the periodic flush timer. */
79
+ stopFlushTimer(): void;
80
+ /**
81
+ * Flushes the write buffer to the persistence backend.
82
+ *
83
+ * Collects all buffered thoughts across all sessions and saves them
84
+ * individually with retry logic. On persistent failure (all retries
85
+ * exhausted), emits a `persistenceError` event and re-queues failed items
86
+ * into the default session's buffer.
87
+ *
88
+ * Safe to call concurrently — duplicate calls are skipped.
89
+ */
90
+ flush(): Promise<void>;
91
+ /**
92
+ * Flushes edges for all known sessions to the persistence backend.
93
+ * No-op when EdgeStore is unavailable.
94
+ */
95
+ private _flushEdges;
96
+ /**
97
+ * Flushes a single thought to persistence with exponential backoff retry.
98
+ * @returns true if saved successfully, false otherwise
99
+ */
100
+ private _flushSingleThought;
101
+ /**
102
+ * Handles the result of a flush operation, re-queuing failures into the
103
+ * default session's buffer and emitting a `persistenceError` event when
104
+ * any items failed.
105
+ */
106
+ private _handleFlushResult;
107
+ /** Returns a promise that resolves after the specified delay. */
108
+ private _delay;
109
+ }
110
+ //# sourceMappingURL=PersistenceBuffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersistenceBuffer.d.ts","sourceRoot":"","sources":["../../src/core/PersistenceBuffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC/B,WAAW,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACvC,IAAI,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC;CACvF;AAED,mDAAmD;AACnD,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,eAAe;IACjE,WAAW,EAAE,kBAAkB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAClC,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAC,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,sDAAsD;IACtD,YAAY,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,qBAAa,iBAAiB,CAAC,CAAC,SAAS,eAAe;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAuB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAU;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAK;gBAEjB,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAa9C,mEAAmE;IACnE,IAAW,KAAK,IAAI,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAExD;IAED,gDAAgD;IAChD,IAAW,UAAU,IAAI,OAAO,CAE/B;IAED,2DAA2D;IACpD,eAAe,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI;IAIrE;;;OAGG;IACI,aAAa,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAgB1E;;;OAGG;IACI,eAAe,IAAI,IAAI;IAU9B,sCAAsC;IAC/B,cAAc,IAAI,IAAI;IAO7B;;;;;;;;;OASG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCnC;;;OAGG;YACW,WAAW;IAkBzB;;;OAGG;YACW,mBAAmB;IA4BjC;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAwB1B,iEAAiE;IACjE,OAAO,CAAC,MAAM;CAGd"}
@@ -0,0 +1,141 @@
1
+ import { getErrorMessage } from "../errors.js";
2
+ import { NullLogger } from "../logger/NullLogger.js";
3
+ class PersistenceBuffer {
4
+ _persistence;
5
+ _bufferSize;
6
+ _flushInterval;
7
+ _maxRetries;
8
+ _defaultSessionId;
9
+ _getSessions;
10
+ _getDefaultSession;
11
+ _edgeStore;
12
+ _eventEmitter;
13
+ _logger;
14
+ _flushTimer = null;
15
+ _isFlushing = false;
16
+ _flushRetryCount = 0;
17
+ constructor(config){
18
+ this._persistence = config.persistence;
19
+ this._bufferSize = config.bufferSize;
20
+ this._flushInterval = config.flushInterval;
21
+ this._maxRetries = config.maxRetries;
22
+ this._defaultSessionId = config.defaultSessionId;
23
+ this._getSessions = config.getSessions;
24
+ this._getDefaultSession = config.getDefaultSession;
25
+ this._edgeStore = config.edgeStore;
26
+ this._eventEmitter = config.eventEmitter ?? null;
27
+ this._logger = config.logger ?? new NullLogger();
28
+ }
29
+ get timer() {
30
+ return this._flushTimer;
31
+ }
32
+ get isFlushing() {
33
+ return this._isFlushing;
34
+ }
35
+ setEventEmitter(emitter) {
36
+ this._eventEmitter = emitter;
37
+ }
38
+ bufferThought(session, thought) {
39
+ if (session.writeBuffer.length >= this._bufferSize && this._isFlushing) this._logger.info('Write buffer full and flush in progress, applying backpressure', {
40
+ bufferSize: session.writeBuffer.length,
41
+ maxSize: this._bufferSize
42
+ });
43
+ session.writeBuffer.push(thought);
44
+ if (session.writeBuffer.length >= this._bufferSize) this.flush();
45
+ }
46
+ startFlushTimer() {
47
+ if (null !== this._flushTimer) return;
48
+ this._flushTimer = setInterval(()=>{
49
+ this.flush();
50
+ }, this._flushInterval);
51
+ if (this._flushTimer && 'object' == typeof this._flushTimer && 'unref' in this._flushTimer) this._flushTimer.unref();
52
+ }
53
+ stopFlushTimer() {
54
+ if (null !== this._flushTimer) {
55
+ clearInterval(this._flushTimer);
56
+ this._flushTimer = null;
57
+ }
58
+ }
59
+ async flush() {
60
+ if (this._isFlushing) return;
61
+ const allPending = [];
62
+ for (const session of this._getSessions().values())if (session.writeBuffer.length > 0) allPending.push(...session.writeBuffer.splice(0));
63
+ if (0 === allPending.length) return;
64
+ this._isFlushing = true;
65
+ const failedItems = [];
66
+ try {
67
+ for (const thought of allPending){
68
+ const saved = await this._flushSingleThought(thought);
69
+ if (!saved) failedItems.push(thought);
70
+ }
71
+ this._handleFlushResult(failedItems, allPending.length);
72
+ if (this._edgeStore) await this._flushEdges();
73
+ } finally{
74
+ this._isFlushing = false;
75
+ }
76
+ }
77
+ async _flushEdges() {
78
+ if (!this._edgeStore) return;
79
+ const sessionKeys = new Set(this._getSessions().keys());
80
+ sessionKeys.add(this._defaultSessionId);
81
+ for (const sessionId of sessionKeys){
82
+ const edges = this._edgeStore.edgesForSession(sessionId);
83
+ if (0 !== edges.length) try {
84
+ await this._persistence.saveEdges(sessionId, edges);
85
+ } catch (err) {
86
+ this._logger.info('Failed to persist edges for session', {
87
+ sessionId,
88
+ error: getErrorMessage(err)
89
+ });
90
+ }
91
+ }
92
+ }
93
+ async _flushSingleThought(thought) {
94
+ const backoffDelays = [
95
+ 100,
96
+ 500,
97
+ 2000
98
+ ];
99
+ for(let attempt = 0; attempt <= this._maxRetries; attempt++)try {
100
+ await this._persistence.saveThought(thought);
101
+ return true;
102
+ } catch (err) {
103
+ if (attempt < this._maxRetries) {
104
+ const delay = backoffDelays[attempt] ?? backoffDelays[backoffDelays.length - 1];
105
+ this._logger.info(`Persistence retry ${attempt + 1}/${this._maxRetries}`, {
106
+ thoughtNumber: thought.thought_number,
107
+ delay,
108
+ error: getErrorMessage(err)
109
+ });
110
+ await this._delay(delay);
111
+ } else this._logger.info('All persistence retries exhausted for thought', {
112
+ thoughtNumber: thought.thought_number,
113
+ error: getErrorMessage(err)
114
+ });
115
+ }
116
+ return false;
117
+ }
118
+ _handleFlushResult(failedItems, totalCount) {
119
+ if (failedItems.length > 0) {
120
+ const defaultSession = this._getDefaultSession();
121
+ defaultSession.writeBuffer.unshift(...failedItems);
122
+ this._flushRetryCount++;
123
+ const error = new Error(`Failed to persist ${failedItems.length} thoughts after ${this._maxRetries} retries`);
124
+ this._eventEmitter?.emit('persistenceError', {
125
+ operation: 'flushBuffer',
126
+ error
127
+ });
128
+ this._logger.info('Flush completed with failures', {
129
+ failed: failedItems.length,
130
+ total: totalCount,
131
+ consecutiveFailures: this._flushRetryCount
132
+ });
133
+ } else this._flushRetryCount = 0;
134
+ }
135
+ _delay(ms) {
136
+ return new Promise((resolve)=>setTimeout(resolve, ms));
137
+ }
138
+ }
139
+ export { PersistenceBuffer };
140
+
141
+ //# sourceMappingURL=PersistenceBuffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core/PersistenceBuffer.js","sources":["../../src/core/PersistenceBuffer.ts"],"sourcesContent":["/**\n * PersistenceBuffer — owns the periodic flush timer, retry/backoff logic, and\n * concurrent-flush guarding for HistoryManager's persistence write buffer.\n *\n * Extracted from HistoryManager. The session Map and write buffers remain\n * owned by HistoryManager (passed by reference) so public access patterns\n * (e.g. `manager._sessions`) are preserved.\n *\n * @module PersistenceBuffer\n */\n\nimport type { IEdgeStore } from '../contracts/interfaces.js';\nimport { getErrorMessage } from '../errors.js';\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\nimport type { PersistenceBackend } from '../persistence/PersistenceBackend.js';\nimport type { ThoughtData } from './thought.js';\n\n/** Minimal session view: anything that owns a `writeBuffer`. */\nexport interface BufferedSession {\n\twriteBuffer: ThoughtData[];\n}\n\n/** Event emitter contract for persistence error events. */\nexport interface PersistenceEventEmitter {\n\temit(event: 'persistenceError', payload: { operation: string; error: Error }): boolean;\n}\n\n/** Configuration options for PersistenceBuffer. */\nexport interface PersistenceBufferConfig<S extends BufferedSession> {\n\tpersistence: PersistenceBackend;\n\tbufferSize: number;\n\tflushInterval: number;\n\tmaxRetries: number;\n\tdefaultSessionId: string;\n\t/** Returns the live session map (called on each flush). */\n\tgetSessions: () => Map<string, S>;\n\t/** Returns the requeue target session (default-session buffer). */\n\tgetDefaultSession: () => S;\n\t/** Optional EdgeStore for flushing edges alongside thoughts. */\n\tedgeStore?: IEdgeStore;\n\t/** Optional emitter for `persistenceError` events. */\n\teventEmitter?: PersistenceEventEmitter | null;\n\tlogger?: Logger;\n}\n\n/**\n * Manages buffered persistence writes with periodic flushing, exponential\n * backoff retries, concurrent-flush guarding, and edge store integration.\n */\nexport class PersistenceBuffer<S extends BufferedSession> {\n\tprivate readonly _persistence: PersistenceBackend;\n\tprivate readonly _bufferSize: number;\n\tprivate readonly _flushInterval: number;\n\tprivate readonly _maxRetries: number;\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _getSessions: () => Map<string, S>;\n\tprivate readonly _getDefaultSession: () => S;\n\tprivate readonly _edgeStore?: IEdgeStore;\n\tprivate _eventEmitter: PersistenceEventEmitter | null;\n\tprivate readonly _logger: Logger;\n\n\tprivate _flushTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate _isFlushing = false;\n\tprivate _flushRetryCount = 0;\n\n\tconstructor(config: PersistenceBufferConfig<S>) {\n\t\tthis._persistence = config.persistence;\n\t\tthis._bufferSize = config.bufferSize;\n\t\tthis._flushInterval = config.flushInterval;\n\t\tthis._maxRetries = config.maxRetries;\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._getSessions = config.getSessions;\n\t\tthis._getDefaultSession = config.getDefaultSession;\n\t\tthis._edgeStore = config.edgeStore;\n\t\tthis._eventEmitter = config.eventEmitter ?? null;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying flush timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._flushTimer;\n\t}\n\n\t/** Returns true when a flush is in progress. */\n\tpublic get isFlushing(): boolean {\n\t\treturn this._isFlushing;\n\t}\n\n\t/** Sets / replaces the persistence error event emitter. */\n\tpublic setEventEmitter(emitter: PersistenceEventEmitter | null): void {\n\t\tthis._eventEmitter = emitter;\n\t}\n\n\t/**\n\t * Buffers a thought into the given session's write buffer. Triggers an\n\t * immediate flush when the buffer reaches `bufferSize`.\n\t */\n\tpublic bufferThought(session: BufferedSession, thought: ThoughtData): void {\n\t\t// Backpressure: if buffer is full and flush is in progress, log warning\n\t\tif (session.writeBuffer.length >= this._bufferSize && this._isFlushing) {\n\t\t\tthis._logger.info('Write buffer full and flush in progress, applying backpressure', {\n\t\t\t\tbufferSize: session.writeBuffer.length,\n\t\t\t\tmaxSize: this._bufferSize,\n\t\t\t});\n\t\t}\n\n\t\tsession.writeBuffer.push(thought);\n\n\t\tif (session.writeBuffer.length >= this._bufferSize) {\n\t\t\tvoid this.flush();\n\t\t}\n\t}\n\n\t/**\n\t * Starts the periodic flush timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startFlushTimer(): void {\n\t\tif (this._flushTimer !== null) return;\n\t\tthis._flushTimer = setInterval(() => {\n\t\t\tvoid this.flush();\n\t\t}, this._flushInterval);\n\t\tif (this._flushTimer && typeof this._flushTimer === 'object' && 'unref' in this._flushTimer) {\n\t\t\tthis._flushTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic flush timer. */\n\tpublic stopFlushTimer(): void {\n\t\tif (this._flushTimer !== null) {\n\t\t\tclearInterval(this._flushTimer);\n\t\t\tthis._flushTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes the write buffer to the persistence backend.\n\t *\n\t * Collects all buffered thoughts across all sessions and saves them\n\t * individually with retry logic. On persistent failure (all retries\n\t * exhausted), emits a `persistenceError` event and re-queues failed items\n\t * into the default session's buffer.\n\t *\n\t * Safe to call concurrently — duplicate calls are skipped.\n\t */\n\tpublic async flush(): Promise<void> {\n\t\tif (this._isFlushing) return;\n\n\t\t// Collect all pending writes from all sessions\n\t\tconst allPending: ThoughtData[] = [];\n\t\tfor (const session of this._getSessions().values()) {\n\t\t\tif (session.writeBuffer.length > 0) {\n\t\t\t\tallPending.push(...session.writeBuffer.splice(0));\n\t\t\t}\n\t\t}\n\n\t\tif (allPending.length === 0) return;\n\n\t\tthis._isFlushing = true;\n\t\tconst failedItems: ThoughtData[] = [];\n\n\t\ttry {\n\t\t\tfor (const thought of allPending) {\n\t\t\t\tconst saved = await this._flushSingleThought(thought);\n\t\t\t\tif (!saved) {\n\t\t\t\t\tfailedItems.push(thought);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis._handleFlushResult(failedItems, allPending.length);\n\n\t\t\tif (this._edgeStore) {\n\t\t\t\tawait this._flushEdges();\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._isFlushing = false;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes edges for all known sessions to the persistence backend.\n\t * No-op when EdgeStore is unavailable.\n\t */\n\tprivate async _flushEdges(): Promise<void> {\n\t\tif (!this._edgeStore) return;\n\t\tconst sessionKeys = new Set<string>(this._getSessions().keys());\n\t\tsessionKeys.add(this._defaultSessionId);\n\t\tfor (const sessionId of sessionKeys) {\n\t\t\tconst edges = this._edgeStore.edgesForSession(sessionId);\n\t\t\tif (edges.length === 0) continue;\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveEdges(sessionId, edges);\n\t\t\t} catch (err) {\n\t\t\t\tthis._logger.info('Failed to persist edges for session', {\n\t\t\t\t\tsessionId,\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Flushes a single thought to persistence with exponential backoff retry.\n\t * @returns true if saved successfully, false otherwise\n\t */\n\tprivate async _flushSingleThought(thought: ThoughtData): Promise<boolean> {\n\t\tconst backoffDelays = [100, 500, 2000];\n\n\t\tfor (let attempt = 0; attempt <= this._maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveThought(thought);\n\t\t\t\treturn true;\n\t\t\t} catch (err) {\n\t\t\t\tif (attempt < this._maxRetries) {\n\t\t\t\t\tconst delay = backoffDelays[attempt] ?? backoffDelays[backoffDelays.length - 1]!;\n\t\t\t\t\tthis._logger.info(`Persistence retry ${attempt + 1}/${this._maxRetries}`, {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\tdelay,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t\tawait this._delay(delay);\n\t\t\t\t} else {\n\t\t\t\t\tthis._logger.info('All persistence retries exhausted for thought', {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Handles the result of a flush operation, re-queuing failures into the\n\t * default session's buffer and emitting a `persistenceError` event when\n\t * any items failed.\n\t */\n\tprivate _handleFlushResult(failedItems: ThoughtData[], totalCount: number): void {\n\t\tif (failedItems.length > 0) {\n\t\t\tconst defaultSession = this._getDefaultSession();\n\t\t\tdefaultSession.writeBuffer.unshift(...failedItems);\n\t\t\tthis._flushRetryCount++;\n\n\t\t\tconst error = new Error(\n\t\t\t\t`Failed to persist ${failedItems.length} thoughts after ${this._maxRetries} retries`\n\t\t\t);\n\t\t\tthis._eventEmitter?.emit('persistenceError', {\n\t\t\t\toperation: 'flushBuffer',\n\t\t\t\terror,\n\t\t\t});\n\n\t\t\tthis._logger.info('Flush completed with failures', {\n\t\t\t\tfailed: failedItems.length,\n\t\t\t\ttotal: totalCount,\n\t\t\t\tconsecutiveFailures: this._flushRetryCount,\n\t\t\t});\n\t\t} else {\n\t\t\tthis._flushRetryCount = 0;\n\t\t}\n\t}\n\n\t/** Returns a promise that resolves after the specified delay. */\n\tprivate _delay(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n"],"names":["PersistenceBuffer","config","NullLogger","emitter","session","thought","setInterval","clearInterval","allPending","failedItems","saved","sessionKeys","Set","sessionId","edges","err","getErrorMessage","backoffDelays","attempt","delay","totalCount","defaultSession","error","Error","ms","Promise","resolve","setTimeout"],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SessionManager — owns periodic session cleanup timer and TTL/LRU eviction logic.
3
+ *
4
+ * Extracted from HistoryManager. The session Map remains owned by HistoryManager
5
+ * (passed by reference) so that public access patterns are preserved.
6
+ *
7
+ * @module SessionManager
8
+ */
9
+ import type { Logger } from '../logger/StructuredLogger.js';
10
+ /** Minimal session contract — anything with a `lastAccessedAt` timestamp. */
11
+ export interface SessionLike {
12
+ lastAccessedAt: number;
13
+ }
14
+ /** Configuration options for SessionManager. */
15
+ export interface SessionManagerConfig {
16
+ /** Default session key that must never be evicted. */
17
+ defaultSessionId: string;
18
+ /** TTL for inactive sessions in milliseconds. */
19
+ sessionTtlMs: number;
20
+ /** Periodic cleanup interval in milliseconds. */
21
+ cleanupIntervalMs: number;
22
+ /** Returns the current MAX_SESSIONS limit (callable so tests can mutate). */
23
+ getMaxSessions: () => number;
24
+ logger?: Logger;
25
+ }
26
+ /**
27
+ * Manages session lifecycle: periodic stale-session cleanup and LRU eviction
28
+ * when the session count exceeds the configured maximum.
29
+ */
30
+ export declare class SessionManager<S extends SessionLike> {
31
+ private readonly _defaultSessionId;
32
+ private readonly _sessionTtlMs;
33
+ private readonly _cleanupIntervalMs;
34
+ private readonly _getMaxSessions;
35
+ private readonly _logger;
36
+ private _cleanupTimer;
37
+ constructor(config: SessionManagerConfig);
38
+ /** Returns the underlying cleanup timer (for test introspection). */
39
+ get timer(): ReturnType<typeof setInterval> | null;
40
+ /**
41
+ * Starts the periodic session cleanup timer. No-op if already started.
42
+ * The timer is unref'd so it does not block process exit.
43
+ */
44
+ startCleanupTimer(sessions: Map<string, S>): void;
45
+ /** Stops the periodic session cleanup timer. */
46
+ stopCleanupTimer(): void;
47
+ /**
48
+ * Evicts sessions that have been inactive longer than `sessionTtlMs`.
49
+ * The default session is never evicted.
50
+ */
51
+ cleanupStaleSessions(sessions: Map<string, S>): void;
52
+ /**
53
+ * Evicts oldest sessions when the configured maximum is exceeded (LRU).
54
+ * The default session is never evicted.
55
+ */
56
+ evictExcessSessions(sessions: Map<string, S>): void;
57
+ }
58
+ //# sourceMappingURL=SessionManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../src/core/SessionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC3B,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACpC,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,qBAAa,cAAc,CAAC,CAAC,SAAS,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAe;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,aAAa,CAA+C;gBAExD,MAAM,EAAE,oBAAoB;IAQxC,qEAAqE;IACrE,IAAW,KAAK,IAAI,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAExD;IAED;;;OAGG;IACI,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;IAcxD,gDAAgD;IACzC,gBAAgB,IAAI,IAAI;IAO/B;;;OAGG;IACI,oBAAoB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;IAW3D;;;OAGG;IACI,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;CAoB1D"}
@@ -0,0 +1,65 @@
1
+ import { NullLogger } from "../logger/NullLogger.js";
2
+ class SessionManager {
3
+ _defaultSessionId;
4
+ _sessionTtlMs;
5
+ _cleanupIntervalMs;
6
+ _getMaxSessions;
7
+ _logger;
8
+ _cleanupTimer = null;
9
+ constructor(config){
10
+ this._defaultSessionId = config.defaultSessionId;
11
+ this._sessionTtlMs = config.sessionTtlMs;
12
+ this._cleanupIntervalMs = config.cleanupIntervalMs;
13
+ this._getMaxSessions = config.getMaxSessions;
14
+ this._logger = config.logger ?? new NullLogger();
15
+ }
16
+ get timer() {
17
+ return this._cleanupTimer;
18
+ }
19
+ startCleanupTimer(sessions) {
20
+ if (null !== this._cleanupTimer) return;
21
+ this._cleanupTimer = setInterval(()=>{
22
+ this.cleanupStaleSessions(sessions);
23
+ }, this._cleanupIntervalMs);
24
+ if (this._cleanupTimer && 'object' == typeof this._cleanupTimer && 'unref' in this._cleanupTimer) this._cleanupTimer.unref();
25
+ }
26
+ stopCleanupTimer() {
27
+ if (null !== this._cleanupTimer) {
28
+ clearInterval(this._cleanupTimer);
29
+ this._cleanupTimer = null;
30
+ }
31
+ }
32
+ cleanupStaleSessions(sessions) {
33
+ const now = Date.now();
34
+ for (const [key, session] of sessions)if (key !== this._defaultSessionId) {
35
+ if (now - session.lastAccessedAt > this._sessionTtlMs) {
36
+ sessions.delete(key);
37
+ this._logger.info('Evicted stale session', {
38
+ sessionId: key
39
+ });
40
+ }
41
+ }
42
+ }
43
+ evictExcessSessions(sessions) {
44
+ const max = this._getMaxSessions();
45
+ while(sessions.size > max){
46
+ let oldestKey = null;
47
+ let oldestTime = 1 / 0;
48
+ for (const [key, session] of sessions)if (key !== this._defaultSessionId) {
49
+ if (session.lastAccessedAt < oldestTime) {
50
+ oldestTime = session.lastAccessedAt;
51
+ oldestKey = key;
52
+ }
53
+ }
54
+ if (null !== oldestKey) {
55
+ sessions.delete(oldestKey);
56
+ this._logger.info('Evicted oldest session (LRU)', {
57
+ sessionId: oldestKey
58
+ });
59
+ } else break;
60
+ }
61
+ }
62
+ }
63
+ export { SessionManager };
64
+
65
+ //# sourceMappingURL=SessionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core/SessionManager.js","sources":["../../src/core/SessionManager.ts"],"sourcesContent":["/**\n * SessionManager — owns periodic session cleanup timer and TTL/LRU eviction logic.\n *\n * Extracted from HistoryManager. The session Map remains owned by HistoryManager\n * (passed by reference) so that public access patterns are preserved.\n *\n * @module SessionManager\n */\n\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\n\n/** Minimal session contract — anything with a `lastAccessedAt` timestamp. */\nexport interface SessionLike {\n\tlastAccessedAt: number;\n}\n\n/** Configuration options for SessionManager. */\nexport interface SessionManagerConfig {\n\t/** Default session key that must never be evicted. */\n\tdefaultSessionId: string;\n\t/** TTL for inactive sessions in milliseconds. */\n\tsessionTtlMs: number;\n\t/** Periodic cleanup interval in milliseconds. */\n\tcleanupIntervalMs: number;\n\t/** Returns the current MAX_SESSIONS limit (callable so tests can mutate). */\n\tgetMaxSessions: () => number;\n\tlogger?: Logger;\n}\n\n/**\n * Manages session lifecycle: periodic stale-session cleanup and LRU eviction\n * when the session count exceeds the configured maximum.\n */\nexport class SessionManager<S extends SessionLike> {\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _sessionTtlMs: number;\n\tprivate readonly _cleanupIntervalMs: number;\n\tprivate readonly _getMaxSessions: () => number;\n\tprivate readonly _logger: Logger;\n\tprivate _cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(config: SessionManagerConfig) {\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._sessionTtlMs = config.sessionTtlMs;\n\t\tthis._cleanupIntervalMs = config.cleanupIntervalMs;\n\t\tthis._getMaxSessions = config.getMaxSessions;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying cleanup timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._cleanupTimer;\n\t}\n\n\t/**\n\t * Starts the periodic session cleanup timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startCleanupTimer(sessions: Map<string, S>): void {\n\t\tif (this._cleanupTimer !== null) return;\n\t\tthis._cleanupTimer = setInterval(() => {\n\t\t\tthis.cleanupStaleSessions(sessions);\n\t\t}, this._cleanupIntervalMs);\n\t\tif (\n\t\t\tthis._cleanupTimer &&\n\t\t\ttypeof this._cleanupTimer === 'object' &&\n\t\t\t'unref' in this._cleanupTimer\n\t\t) {\n\t\t\tthis._cleanupTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic session cleanup timer. */\n\tpublic stopCleanupTimer(): void {\n\t\tif (this._cleanupTimer !== null) {\n\t\t\tclearInterval(this._cleanupTimer);\n\t\t\tthis._cleanupTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Evicts sessions that have been inactive longer than `sessionTtlMs`.\n\t * The default session is never evicted.\n\t */\n\tpublic cleanupStaleSessions(sessions: Map<string, S>): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, session] of sessions) {\n\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\tif (now - session.lastAccessedAt > this._sessionTtlMs) {\n\t\t\t\tsessions.delete(key);\n\t\t\t\tthis._logger.info('Evicted stale session', { sessionId: key });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Evicts oldest sessions when the configured maximum is exceeded (LRU).\n\t * The default session is never evicted.\n\t */\n\tpublic evictExcessSessions(sessions: Map<string, S>): void {\n\t\tconst max = this._getMaxSessions();\n\t\twhile (sessions.size > max) {\n\t\t\tlet oldestKey: string | null = null;\n\t\t\tlet oldestTime = Infinity;\n\t\t\tfor (const [key, session] of sessions) {\n\t\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\t\tif (session.lastAccessedAt < oldestTime) {\n\t\t\t\t\toldestTime = session.lastAccessedAt;\n\t\t\t\t\toldestKey = key;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (oldestKey !== null) {\n\t\t\t\tsessions.delete(oldestKey);\n\t\t\t\tthis._logger.info('Evicted oldest session (LRU)', { sessionId: oldestKey });\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"],"names":["SessionManager","config","NullLogger","sessions","setInterval","clearInterval","now","Date","key","session","max","oldestKey","oldestTime","Infinity"],"mappings":";AAkCO,MAAMA;IACK,kBAA0B;IAC1B,cAAsB;IACtB,mBAA2B;IAC3B,gBAA8B;IAC9B,QAAgB;IACzB,gBAAuD,KAAK;IAEpE,YAAYC,MAA4B,CAAE;QACzC,IAAI,CAAC,iBAAiB,GAAGA,OAAO,gBAAgB;QAChD,IAAI,CAAC,aAAa,GAAGA,OAAO,YAAY;QACxC,IAAI,CAAC,kBAAkB,GAAGA,OAAO,iBAAiB;QAClD,IAAI,CAAC,eAAe,GAAGA,OAAO,cAAc;QAC5C,IAAI,CAAC,OAAO,GAAGA,OAAO,MAAM,IAAI,IAAIC;IACrC;IAGA,IAAW,QAA+C;QACzD,OAAO,IAAI,CAAC,aAAa;IAC1B;IAMO,kBAAkBC,QAAwB,EAAQ;QACxD,IAAI,AAAuB,SAAvB,IAAI,CAAC,aAAa,EAAW;QACjC,IAAI,CAAC,aAAa,GAAGC,YAAY;YAChC,IAAI,CAAC,oBAAoB,CAACD;QAC3B,GAAG,IAAI,CAAC,kBAAkB;QAC1B,IACC,IAAI,CAAC,aAAa,IAClB,AAA8B,YAA9B,OAAO,IAAI,CAAC,aAAa,IACzB,WAAW,IAAI,CAAC,aAAa,EAE7B,IAAI,CAAC,aAAa,CAAC,KAAK;IAE1B;IAGO,mBAAyB;QAC/B,IAAI,AAAuB,SAAvB,IAAI,CAAC,aAAa,EAAW;YAChCE,cAAc,IAAI,CAAC,aAAa;YAChC,IAAI,CAAC,aAAa,GAAG;QACtB;IACD;IAMO,qBAAqBF,QAAwB,EAAQ;QAC3D,MAAMG,MAAMC,KAAK,GAAG;QACpB,KAAK,MAAM,CAACC,KAAKC,QAAQ,IAAIN,SAC5B,IAAIK,QAAQ,IAAI,CAAC,iBAAiB,EAClC;YAAA,IAAIF,MAAMG,QAAQ,cAAc,GAAG,IAAI,CAAC,aAAa,EAAE;gBACtDN,SAAS,MAAM,CAACK;gBAChB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,yBAAyB;oBAAE,WAAWA;gBAAI;YAC7D;QAAA;IAEF;IAMO,oBAAoBL,QAAwB,EAAQ;QAC1D,MAAMO,MAAM,IAAI,CAAC,eAAe;QAChC,MAAOP,SAAS,IAAI,GAAGO,IAAK;YAC3B,IAAIC,YAA2B;YAC/B,IAAIC,aAAaC;YACjB,KAAK,MAAM,CAACL,KAAKC,QAAQ,IAAIN,SAC5B,IAAIK,QAAQ,IAAI,CAAC,iBAAiB,EAClC;gBAAA,IAAIC,QAAQ,cAAc,GAAGG,YAAY;oBACxCA,aAAaH,QAAQ,cAAc;oBACnCE,YAAYH;gBACb;YAAA;YAED,IAAIG,AAAc,SAAdA,WAAoB;gBACvBR,SAAS,MAAM,CAACQ;gBAChB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,gCAAgC;oBAAE,WAAWA;gBAAU;YAC1E,OACC;QAEF;IACD;AACD"}