principles-disciple 1.8.1 → 1.8.2

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 (470) hide show
  1. package/ADVANCED_CONFIG_ZH.md +97 -0
  2. package/AGENT_INSTALL.md +173 -0
  3. package/AGENT_INSTALL_EN.md +173 -0
  4. package/INSTALL.md +256 -0
  5. package/SKILL.md +63 -0
  6. package/docs/COMMAND_REFERENCE.md +76 -0
  7. package/docs/COMMAND_REFERENCE_EN.md +79 -0
  8. package/esbuild.config.js +75 -0
  9. package/openclaw.plugin.json +4 -4
  10. package/package.json +11 -13
  11. package/scripts/build-web.mjs +46 -0
  12. package/scripts/install-dependencies.cjs +47 -0
  13. package/scripts/sync-plugin.mjs +802 -0
  14. package/scripts/verify-build.mjs +109 -0
  15. package/src/agents/nocturnal-dreamer.md +152 -0
  16. package/src/agents/nocturnal-philosopher.md +138 -0
  17. package/src/agents/nocturnal-reflector.md +126 -0
  18. package/src/agents/nocturnal-scribe.md +164 -0
  19. package/src/commands/capabilities.ts +85 -0
  20. package/{dist/commands/context.js → src/commands/context.ts} +78 -38
  21. package/src/commands/evolution-status.ts +146 -0
  22. package/src/commands/export.ts +111 -0
  23. package/src/commands/focus.ts +533 -0
  24. package/src/commands/nocturnal-review.ts +311 -0
  25. package/src/commands/nocturnal-rollout.ts +763 -0
  26. package/src/commands/nocturnal-train.ts +1002 -0
  27. package/{dist/commands/pain.js → src/commands/pain.ts} +68 -49
  28. package/src/commands/principle-rollback.ts +27 -0
  29. package/{dist/commands/rollback.js → src/commands/rollback.ts} +44 -12
  30. package/src/commands/samples.ts +60 -0
  31. package/src/commands/strategy.ts +38 -0
  32. package/{dist/commands/thinking-os.js → src/commands/thinking-os.ts} +59 -36
  33. package/src/commands/workflow-debug.ts +128 -0
  34. package/{dist/config/defaults/runtime.js → src/config/defaults/runtime.ts} +12 -5
  35. package/src/config/errors.ts +163 -0
  36. package/{dist/config/index.d.ts → src/config/index.ts} +2 -1
  37. package/src/constants/diagnostician.ts +66 -0
  38. package/src/constants/tools.ts +62 -0
  39. package/src/core/adaptive-thresholds.ts +476 -0
  40. package/{dist/core/config-service.js → src/core/config-service.ts} +7 -4
  41. package/{dist/core/config.js → src/core/config.ts} +158 -46
  42. package/src/core/control-ui-db.ts +435 -0
  43. package/{dist/core/detection-funnel.js → src/core/detection-funnel.ts} +36 -21
  44. package/{dist/core/detection-service.js → src/core/detection-service.ts} +7 -4
  45. package/{dist/core/dictionary-service.js → src/core/dictionary-service.ts} +7 -4
  46. package/{dist/core/dictionary.js → src/core/dictionary.ts} +57 -34
  47. package/src/core/empathy-keyword-matcher.ts +327 -0
  48. package/src/core/empathy-types.ts +218 -0
  49. package/src/core/event-log.ts +544 -0
  50. package/src/core/evolution-engine.ts +612 -0
  51. package/src/core/evolution-logger.ts +353 -0
  52. package/src/core/evolution-migration.ts +77 -0
  53. package/src/core/evolution-reducer.ts +731 -0
  54. package/src/core/evolution-types.ts +456 -0
  55. package/src/core/external-training-contract.ts +527 -0
  56. package/src/core/focus-history.ts +1458 -0
  57. package/src/core/hygiene/tracker.ts +117 -0
  58. package/{dist/core/init.js → src/core/init.ts} +39 -26
  59. package/src/core/local-worker-routing.ts +617 -0
  60. package/{dist/core/migration.js → src/core/migration.ts} +18 -11
  61. package/src/core/model-deployment-registry.ts +722 -0
  62. package/src/core/model-training-registry.ts +813 -0
  63. package/src/core/nocturnal-arbiter.ts +706 -0
  64. package/src/core/nocturnal-candidate-scoring.ts +392 -0
  65. package/src/core/nocturnal-compliance.ts +1075 -0
  66. package/src/core/nocturnal-dataset.ts +668 -0
  67. package/src/core/nocturnal-executability.ts +428 -0
  68. package/src/core/nocturnal-export.ts +390 -0
  69. package/{dist/core/nocturnal-paths.js → src/core/nocturnal-paths.ts} +49 -23
  70. package/src/core/nocturnal-trajectory-extractor.ts +484 -0
  71. package/src/core/nocturnal-trinity.ts +1384 -0
  72. package/src/core/pain.ts +122 -0
  73. package/{dist/core/path-resolver.js → src/core/path-resolver.ts} +157 -36
  74. package/{dist/core/paths.js → src/core/paths.ts} +13 -4
  75. package/src/core/principle-training-state.ts +450 -0
  76. package/src/core/profile.ts +226 -0
  77. package/src/core/promotion-gate.ts +822 -0
  78. package/{dist/core/risk-calculator.js → src/core/risk-calculator.ts} +42 -16
  79. package/{dist/core/session-tracker.js → src/core/session-tracker.ts} +175 -62
  80. package/src/core/shadow-observation-registry.ts +534 -0
  81. package/{dist/core/system-logger.js → src/core/system-logger.ts} +9 -5
  82. package/src/core/thinking-models.ts +217 -0
  83. package/src/core/training-program.ts +630 -0
  84. package/src/core/trajectory-types.ts +243 -0
  85. package/src/core/trajectory.ts +1673 -0
  86. package/{dist/core/workspace-context.js → src/core/workspace-context.ts} +57 -32
  87. package/src/hooks/bash-risk.ts +171 -0
  88. package/src/hooks/edit-verification.ts +295 -0
  89. package/src/hooks/gate-block-helper.ts +160 -0
  90. package/src/hooks/gate.ts +210 -0
  91. package/src/hooks/gfi-gate.ts +177 -0
  92. package/src/hooks/lifecycle.ts +326 -0
  93. package/{dist/hooks/llm.js → src/hooks/llm.ts} +160 -80
  94. package/src/hooks/message-sanitize.ts +45 -0
  95. package/src/hooks/pain.ts +384 -0
  96. package/src/hooks/progressive-trust-gate.ts +174 -0
  97. package/src/hooks/prompt.ts +920 -0
  98. package/src/hooks/subagent.ts +207 -0
  99. package/src/hooks/thinking-checkpoint.ts +73 -0
  100. package/src/hooks/trajectory-collector.ts +290 -0
  101. package/src/http/principles-console-route.ts +716 -0
  102. package/src/i18n/commands.ts +117 -0
  103. package/src/index.ts +694 -0
  104. package/src/service/central-database.ts +831 -0
  105. package/src/service/control-ui-query-service.ts +888 -0
  106. package/src/service/evolution-query-service.ts +405 -0
  107. package/src/service/evolution-worker.ts +1646 -0
  108. package/src/service/health-query-service.ts +836 -0
  109. package/{dist/service/nocturnal-runtime.js → src/service/nocturnal-runtime.ts} +235 -79
  110. package/src/service/nocturnal-service.ts +1015 -0
  111. package/src/service/nocturnal-target-selector.ts +532 -0
  112. package/src/service/phase3-input-filter.ts +237 -0
  113. package/src/service/runtime-summary-service.ts +757 -0
  114. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +513 -0
  115. package/{dist/service/subagent-workflow/empathy-observer-workflow-manager.js → src/service/subagent-workflow/empathy-observer-workflow-manager.ts} +240 -117
  116. package/src/service/subagent-workflow/index.ts +51 -0
  117. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +856 -0
  118. package/src/service/subagent-workflow/runtime-direct-driver.ts +166 -0
  119. package/{dist/service/subagent-workflow/types.d.ts → src/service/subagent-workflow/types.ts} +137 -18
  120. package/src/service/subagent-workflow/workflow-store.ts +328 -0
  121. package/src/service/trajectory-service.ts +15 -0
  122. package/{dist/tools/critique-prompt.js → src/tools/critique-prompt.ts} +25 -8
  123. package/src/tools/deep-reflect.ts +349 -0
  124. package/{dist/tools/model-index.js → src/tools/model-index.ts} +33 -17
  125. package/src/types/event-types.ts +453 -0
  126. package/src/types/hygiene-types.ts +31 -0
  127. package/src/types/principle-tree-schema.ts +244 -0
  128. package/src/types/runtime-summary.ts +49 -0
  129. package/src/types.ts +74 -0
  130. package/src/utils/file-lock.ts +391 -0
  131. package/{dist/utils/glob-match.js → src/utils/glob-match.ts} +21 -20
  132. package/{dist/utils/hashing.js → src/utils/hashing.ts} +6 -4
  133. package/src/utils/io.ts +110 -0
  134. package/{dist/utils/nlp.js → src/utils/nlp.ts} +19 -12
  135. package/{dist/utils/plugin-logger.js → src/utils/plugin-logger.ts} +33 -8
  136. package/src/utils/subagent-probe.ts +94 -0
  137. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +70 -1
  138. package/templates/pain_settings.json +2 -1
  139. package/tests/README.md +120 -0
  140. package/tests/build-artifacts.test.ts +111 -0
  141. package/tests/commands/evolution-status.test.ts +222 -0
  142. package/tests/commands/evolver.test.ts +22 -0
  143. package/tests/commands/export.test.ts +78 -0
  144. package/tests/commands/nocturnal-review.test.ts +448 -0
  145. package/tests/commands/nocturnal-train.test.ts +97 -0
  146. package/tests/commands/pain.test.ts +108 -0
  147. package/tests/commands/samples.test.ts +65 -0
  148. package/tests/commands/strategy.test.ts +34 -0
  149. package/tests/commands/thinking-os.test.ts +88 -0
  150. package/tests/core/adaptive-thresholds.test.ts +261 -0
  151. package/tests/core/config-service.test.ts +89 -0
  152. package/tests/core/config.test.ts +90 -0
  153. package/tests/core/control-ui-db.test.ts +75 -0
  154. package/tests/core/core-template-guidance.test.ts +21 -0
  155. package/tests/core/detection-funnel.test.ts +63 -0
  156. package/tests/core/detection-service.test.ts +50 -0
  157. package/tests/core/dictionary-service.test.ts +116 -0
  158. package/tests/core/dictionary.test.ts +168 -0
  159. package/tests/core/empathy-keyword-matcher.test.ts +209 -0
  160. package/tests/core/event-log.test.ts +181 -0
  161. package/tests/core/evolution-e2e.test.ts +58 -0
  162. package/tests/core/evolution-engine-gate-integration.test.ts +543 -0
  163. package/tests/core/evolution-engine.test.ts +562 -0
  164. package/tests/core/evolution-logger.test.ts +148 -0
  165. package/tests/core/evolution-migration.test.ts +50 -0
  166. package/tests/core/evolution-paths.test.ts +21 -0
  167. package/tests/core/evolution-reducer.detector-metadata.test.ts +602 -0
  168. package/tests/core/evolution-reducer.test.ts +180 -0
  169. package/tests/core/evolution-types-loop.test.ts +48 -0
  170. package/tests/core/evolution-user-stories.e2e.test.ts +249 -0
  171. package/tests/core/external-training-contract.test.ts +463 -0
  172. package/tests/core/focus-history.test.ts +682 -0
  173. package/tests/core/init-flatten.test.ts +69 -0
  174. package/tests/core/init-refactor.test.ts +87 -0
  175. package/tests/core/init-v1.3.test.ts +46 -0
  176. package/tests/core/init.test.ts +190 -0
  177. package/tests/core/local-worker-routing.test.ts +757 -0
  178. package/tests/core/migration.test.ts +84 -0
  179. package/tests/core/model-deployment-registry.test.ts +845 -0
  180. package/tests/core/model-training-registry.test.ts +889 -0
  181. package/tests/core/nocturnal-arbiter.test.ts +494 -0
  182. package/tests/core/nocturnal-candidate-scoring.test.ts +400 -0
  183. package/tests/core/nocturnal-compliance.test.ts +646 -0
  184. package/tests/core/nocturnal-dataset.test.ts +892 -0
  185. package/tests/core/nocturnal-executability.test.ts +357 -0
  186. package/tests/core/nocturnal-export.test.ts +462 -0
  187. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +428 -0
  188. package/tests/core/nocturnal-trajectory-extractor.test.ts +634 -0
  189. package/tests/core/nocturnal-trinity.test.ts +953 -0
  190. package/tests/core/pain.test.ts +33 -0
  191. package/tests/core/path-resolver.test.ts +57 -0
  192. package/tests/core/paths-refactor.test.ts +42 -0
  193. package/tests/core/phase7-rollout-integration.test.ts +477 -0
  194. package/tests/core/principle-training-state.test.ts +712 -0
  195. package/tests/core/profile.test.ts +56 -0
  196. package/tests/core/promotion-gate.test.ts +556 -0
  197. package/tests/core/risk-calculator.test.ts +168 -0
  198. package/tests/core/session-tracker.test.ts +191 -0
  199. package/tests/core/training-program.test.ts +472 -0
  200. package/tests/core/trajectory.test.ts +265 -0
  201. package/tests/core/workspace-context-factory.test.ts +18 -0
  202. package/tests/core/workspace-context.test.ts +134 -0
  203. package/tests/fixtures/nocturnal-reviewed-subset.json +183 -0
  204. package/tests/fixtures/production-compatibility.test.ts +147 -0
  205. package/tests/fixtures/production-mock-generator.ts +282 -0
  206. package/tests/hooks/bash-risk-integration.test.ts +137 -0
  207. package/tests/hooks/bash-risk.test.ts +81 -0
  208. package/tests/hooks/edit-verification.test.ts +678 -0
  209. package/tests/hooks/gate-edit-verification-p1.test.ts +632 -0
  210. package/tests/hooks/gate-edit-verification.test.ts +435 -0
  211. package/tests/hooks/gate-pipeline-integration.test.ts +404 -0
  212. package/tests/hooks/gate.test.ts +271 -0
  213. package/tests/hooks/gfi-gate-unit.test.ts +422 -0
  214. package/tests/hooks/gfi-gate.test.ts +669 -0
  215. package/tests/hooks/lifecycle.test.ts +248 -0
  216. package/tests/hooks/llm.test.ts +308 -0
  217. package/tests/hooks/message-sanitize.test.ts +36 -0
  218. package/tests/hooks/pain.test.ts +141 -0
  219. package/tests/hooks/progressive-trust-gate.test.ts +277 -0
  220. package/tests/hooks/prompt.test.ts +1411 -0
  221. package/tests/hooks/subagent.test.ts +467 -0
  222. package/tests/hooks/thinking-gate.test.ts +313 -0
  223. package/tests/http/principles-console-route.test.ts +140 -0
  224. package/tests/hygiene-tracker.test.ts +77 -0
  225. package/tests/index.integration.test.ts +179 -0
  226. package/tests/index.shadow-routing.integration.test.ts +140 -0
  227. package/tests/index.test.ts +9 -0
  228. package/tests/integration/empathy-workflow-integration.test.ts +627 -0
  229. package/tests/service/control-ui-query-service.test.ts +121 -0
  230. package/tests/service/empathy-observer-workflow-manager.test.ts +176 -0
  231. package/tests/service/evolution-worker.test.ts +585 -0
  232. package/tests/service/nocturnal-runtime.test.ts +470 -0
  233. package/tests/service/nocturnal-service.test.ts +577 -0
  234. package/tests/service/nocturnal-target-selector.test.ts +615 -0
  235. package/tests/service/nocturnal-workflow-manager.test.ts +439 -0
  236. package/tests/service/phase3-input-filter.test.ts +289 -0
  237. package/tests/service/runtime-summary-service.test.ts +919 -0
  238. package/tests/task-compliance.test.ts +166 -0
  239. package/tests/test-utils.ts +48 -0
  240. package/tests/tools/critique-prompt.test.ts +260 -0
  241. package/tests/tools/deep-reflect.test.ts +232 -0
  242. package/tests/tools/model-index.test.ts +246 -0
  243. package/tests/ui/app.test.tsx +114 -0
  244. package/tests/utils/file-lock.test.ts +407 -0
  245. package/tests/utils/hashing.test.ts +32 -0
  246. package/tests/utils/io.test.ts +39 -0
  247. package/tests/utils/nlp.test.ts +53 -0
  248. package/tests/utils/plugin-logger.test.ts +156 -0
  249. package/tsconfig.json +16 -0
  250. package/tsconfig.tsbuildinfo +1 -0
  251. package/ui/src/App.tsx +45 -0
  252. package/ui/src/api.ts +216 -0
  253. package/ui/src/charts.tsx +586 -0
  254. package/ui/src/components/ErrorState.tsx +6 -0
  255. package/ui/src/components/Loading.tsx +13 -0
  256. package/ui/src/components/ProtectedRoute.tsx +12 -0
  257. package/ui/src/components/Shell.tsx +91 -0
  258. package/ui/src/components/WorkspaceConfig.tsx +146 -0
  259. package/ui/src/components/index.ts +5 -0
  260. package/ui/src/context/auth.tsx +80 -0
  261. package/ui/src/context/theme.tsx +66 -0
  262. package/ui/src/hooks/useAutoRefresh.ts +39 -0
  263. package/ui/src/i18n/ui.ts +363 -0
  264. package/ui/src/main.tsx +16 -0
  265. package/ui/src/pages/EvolutionPage.tsx +352 -0
  266. package/ui/src/pages/FeedbackPage.tsx +140 -0
  267. package/ui/src/pages/GateMonitorPage.tsx +136 -0
  268. package/ui/src/pages/LoginPage.tsx +88 -0
  269. package/ui/src/pages/OverviewPage.tsx +238 -0
  270. package/ui/src/pages/SamplesPage.tsx +174 -0
  271. package/ui/src/pages/ThinkingModelsPage.tsx +127 -0
  272. package/ui/src/styles.css +1661 -0
  273. package/ui/src/types.ts +368 -0
  274. package/ui/src/utils/format.ts +15 -0
  275. package/vitest.config.ts +23 -0
  276. package/dist/commands/capabilities.d.ts +0 -3
  277. package/dist/commands/capabilities.js +0 -73
  278. package/dist/commands/context.d.ts +0 -5
  279. package/dist/commands/evolution-status.d.ts +0 -4
  280. package/dist/commands/evolution-status.js +0 -117
  281. package/dist/commands/evolver.d.ts +0 -9
  282. package/dist/commands/evolver.js +0 -26
  283. package/dist/commands/export.d.ts +0 -2
  284. package/dist/commands/export.js +0 -98
  285. package/dist/commands/focus.d.ts +0 -14
  286. package/dist/commands/focus.js +0 -457
  287. package/dist/commands/nocturnal-review.d.ts +0 -24
  288. package/dist/commands/nocturnal-review.js +0 -265
  289. package/dist/commands/nocturnal-rollout.d.ts +0 -27
  290. package/dist/commands/nocturnal-rollout.js +0 -671
  291. package/dist/commands/nocturnal-train.d.ts +0 -25
  292. package/dist/commands/nocturnal-train.js +0 -919
  293. package/dist/commands/pain.d.ts +0 -5
  294. package/dist/commands/principle-rollback.d.ts +0 -4
  295. package/dist/commands/principle-rollback.js +0 -22
  296. package/dist/commands/rollback.d.ts +0 -19
  297. package/dist/commands/samples.d.ts +0 -2
  298. package/dist/commands/samples.js +0 -55
  299. package/dist/commands/strategy.d.ts +0 -3
  300. package/dist/commands/strategy.js +0 -29
  301. package/dist/commands/thinking-os.d.ts +0 -2
  302. package/dist/config/defaults/runtime.d.ts +0 -40
  303. package/dist/config/errors.d.ts +0 -84
  304. package/dist/config/errors.js +0 -94
  305. package/dist/config/index.js +0 -7
  306. package/dist/constants/diagnostician.d.ts +0 -12
  307. package/dist/constants/diagnostician.js +0 -56
  308. package/dist/constants/tools.d.ts +0 -17
  309. package/dist/constants/tools.js +0 -54
  310. package/dist/core/adaptive-thresholds.d.ts +0 -186
  311. package/dist/core/adaptive-thresholds.js +0 -300
  312. package/dist/core/config-service.d.ts +0 -15
  313. package/dist/core/config.d.ts +0 -129
  314. package/dist/core/control-ui-db.d.ts +0 -95
  315. package/dist/core/control-ui-db.js +0 -292
  316. package/dist/core/detection-funnel.d.ts +0 -33
  317. package/dist/core/detection-service.d.ts +0 -15
  318. package/dist/core/dictionary-service.d.ts +0 -15
  319. package/dist/core/dictionary.d.ts +0 -38
  320. package/dist/core/event-log.d.ts +0 -82
  321. package/dist/core/event-log.js +0 -463
  322. package/dist/core/evolution-engine.d.ts +0 -118
  323. package/dist/core/evolution-engine.js +0 -464
  324. package/dist/core/evolution-logger.d.ts +0 -137
  325. package/dist/core/evolution-logger.js +0 -256
  326. package/dist/core/evolution-migration.d.ts +0 -5
  327. package/dist/core/evolution-migration.js +0 -65
  328. package/dist/core/evolution-reducer.d.ts +0 -98
  329. package/dist/core/evolution-reducer.js +0 -465
  330. package/dist/core/evolution-types.d.ts +0 -287
  331. package/dist/core/evolution-types.js +0 -78
  332. package/dist/core/external-training-contract.d.ts +0 -276
  333. package/dist/core/external-training-contract.js +0 -269
  334. package/dist/core/focus-history.d.ts +0 -210
  335. package/dist/core/focus-history.js +0 -1185
  336. package/dist/core/hygiene/tracker.d.ts +0 -22
  337. package/dist/core/hygiene/tracker.js +0 -106
  338. package/dist/core/init.d.ts +0 -12
  339. package/dist/core/local-worker-routing.d.ts +0 -175
  340. package/dist/core/local-worker-routing.js +0 -525
  341. package/dist/core/migration.d.ts +0 -6
  342. package/dist/core/model-deployment-registry.d.ts +0 -218
  343. package/dist/core/model-deployment-registry.js +0 -503
  344. package/dist/core/model-training-registry.d.ts +0 -295
  345. package/dist/core/model-training-registry.js +0 -475
  346. package/dist/core/nocturnal-arbiter.d.ts +0 -159
  347. package/dist/core/nocturnal-arbiter.js +0 -534
  348. package/dist/core/nocturnal-candidate-scoring.d.ts +0 -137
  349. package/dist/core/nocturnal-candidate-scoring.js +0 -266
  350. package/dist/core/nocturnal-compliance.d.ts +0 -175
  351. package/dist/core/nocturnal-compliance.js +0 -824
  352. package/dist/core/nocturnal-dataset.d.ts +0 -224
  353. package/dist/core/nocturnal-dataset.js +0 -443
  354. package/dist/core/nocturnal-executability.d.ts +0 -85
  355. package/dist/core/nocturnal-executability.js +0 -331
  356. package/dist/core/nocturnal-export.d.ts +0 -124
  357. package/dist/core/nocturnal-export.js +0 -275
  358. package/dist/core/nocturnal-paths.d.ts +0 -124
  359. package/dist/core/nocturnal-trajectory-extractor.d.ts +0 -242
  360. package/dist/core/nocturnal-trajectory-extractor.js +0 -307
  361. package/dist/core/nocturnal-trinity.d.ts +0 -311
  362. package/dist/core/nocturnal-trinity.js +0 -880
  363. package/dist/core/pain.d.ts +0 -4
  364. package/dist/core/pain.js +0 -70
  365. package/dist/core/path-resolver.d.ts +0 -46
  366. package/dist/core/paths.d.ts +0 -65
  367. package/dist/core/principle-training-state.d.ts +0 -121
  368. package/dist/core/principle-training-state.js +0 -321
  369. package/dist/core/profile.d.ts +0 -62
  370. package/dist/core/profile.js +0 -210
  371. package/dist/core/promotion-gate.d.ts +0 -238
  372. package/dist/core/promotion-gate.js +0 -529
  373. package/dist/core/risk-calculator.d.ts +0 -22
  374. package/dist/core/session-tracker.d.ts +0 -101
  375. package/dist/core/shadow-observation-registry.d.ts +0 -217
  376. package/dist/core/shadow-observation-registry.js +0 -308
  377. package/dist/core/system-logger.d.ts +0 -8
  378. package/dist/core/thinking-models.d.ts +0 -38
  379. package/dist/core/thinking-models.js +0 -170
  380. package/dist/core/training-program.d.ts +0 -233
  381. package/dist/core/training-program.js +0 -433
  382. package/dist/core/trajectory.d.ts +0 -411
  383. package/dist/core/trajectory.js +0 -1307
  384. package/dist/core/workspace-context.d.ts +0 -71
  385. package/dist/hooks/bash-risk.d.ts +0 -57
  386. package/dist/hooks/bash-risk.js +0 -137
  387. package/dist/hooks/edit-verification.d.ts +0 -62
  388. package/dist/hooks/edit-verification.js +0 -256
  389. package/dist/hooks/gate-block-helper.d.ts +0 -44
  390. package/dist/hooks/gate-block-helper.js +0 -119
  391. package/dist/hooks/gate.d.ts +0 -24
  392. package/dist/hooks/gate.js +0 -173
  393. package/dist/hooks/gfi-gate.d.ts +0 -40
  394. package/dist/hooks/gfi-gate.js +0 -113
  395. package/dist/hooks/lifecycle.d.ts +0 -5
  396. package/dist/hooks/lifecycle.js +0 -284
  397. package/dist/hooks/llm.d.ts +0 -13
  398. package/dist/hooks/message-sanitize.d.ts +0 -3
  399. package/dist/hooks/message-sanitize.js +0 -37
  400. package/dist/hooks/pain.d.ts +0 -5
  401. package/dist/hooks/pain.js +0 -301
  402. package/dist/hooks/progressive-trust-gate.d.ts +0 -52
  403. package/dist/hooks/progressive-trust-gate.js +0 -134
  404. package/dist/hooks/prompt.d.ts +0 -49
  405. package/dist/hooks/prompt.js +0 -905
  406. package/dist/hooks/subagent.d.ts +0 -10
  407. package/dist/hooks/subagent.js +0 -387
  408. package/dist/hooks/thinking-checkpoint.d.ts +0 -37
  409. package/dist/hooks/thinking-checkpoint.js +0 -51
  410. package/dist/hooks/trajectory-collector.d.ts +0 -32
  411. package/dist/hooks/trajectory-collector.js +0 -256
  412. package/dist/http/principles-console-route.d.ts +0 -9
  413. package/dist/http/principles-console-route.js +0 -681
  414. package/dist/i18n/commands.d.ts +0 -26
  415. package/dist/i18n/commands.js +0 -116
  416. package/dist/index.d.ts +0 -7
  417. package/dist/index.js +0 -581
  418. package/dist/service/central-database.d.ts +0 -104
  419. package/dist/service/central-database.js +0 -649
  420. package/dist/service/control-ui-query-service.d.ts +0 -221
  421. package/dist/service/control-ui-query-service.js +0 -543
  422. package/dist/service/empathy-observer-manager.d.ts +0 -88
  423. package/dist/service/empathy-observer-manager.js +0 -414
  424. package/dist/service/evolution-query-service.d.ts +0 -155
  425. package/dist/service/evolution-query-service.js +0 -258
  426. package/dist/service/evolution-worker.d.ts +0 -101
  427. package/dist/service/evolution-worker.js +0 -975
  428. package/dist/service/health-query-service.d.ts +0 -170
  429. package/dist/service/health-query-service.js +0 -662
  430. package/dist/service/nocturnal-runtime.d.ts +0 -183
  431. package/dist/service/nocturnal-service.d.ts +0 -163
  432. package/dist/service/nocturnal-service.js +0 -787
  433. package/dist/service/nocturnal-target-selector.d.ts +0 -145
  434. package/dist/service/nocturnal-target-selector.js +0 -315
  435. package/dist/service/phase3-input-filter.d.ts +0 -73
  436. package/dist/service/phase3-input-filter.js +0 -172
  437. package/dist/service/runtime-summary-service.d.ts +0 -122
  438. package/dist/service/runtime-summary-service.js +0 -485
  439. package/dist/service/subagent-workflow/empathy-observer-workflow-manager.d.ts +0 -48
  440. package/dist/service/subagent-workflow/index.d.ts +0 -4
  441. package/dist/service/subagent-workflow/index.js +0 -3
  442. package/dist/service/subagent-workflow/runtime-direct-driver.d.ts +0 -77
  443. package/dist/service/subagent-workflow/runtime-direct-driver.js +0 -75
  444. package/dist/service/subagent-workflow/types.js +0 -11
  445. package/dist/service/subagent-workflow/workflow-store.d.ts +0 -26
  446. package/dist/service/subagent-workflow/workflow-store.js +0 -165
  447. package/dist/service/trajectory-service.d.ts +0 -2
  448. package/dist/service/trajectory-service.js +0 -15
  449. package/dist/tools/critique-prompt.d.ts +0 -14
  450. package/dist/tools/deep-reflect.d.ts +0 -39
  451. package/dist/tools/deep-reflect.js +0 -350
  452. package/dist/tools/model-index.d.ts +0 -9
  453. package/dist/types/event-types.d.ts +0 -306
  454. package/dist/types/event-types.js +0 -106
  455. package/dist/types/hygiene-types.d.ts +0 -20
  456. package/dist/types/hygiene-types.js +0 -12
  457. package/dist/types/runtime-summary.d.ts +0 -47
  458. package/dist/types/runtime-summary.js +0 -1
  459. package/dist/types.d.ts +0 -50
  460. package/dist/types.js +0 -22
  461. package/dist/utils/file-lock.d.ts +0 -71
  462. package/dist/utils/file-lock.js +0 -309
  463. package/dist/utils/glob-match.d.ts +0 -28
  464. package/dist/utils/hashing.d.ts +0 -9
  465. package/dist/utils/io.d.ts +0 -6
  466. package/dist/utils/io.js +0 -106
  467. package/dist/utils/nlp.d.ts +0 -9
  468. package/dist/utils/plugin-logger.d.ts +0 -39
  469. package/dist/utils/subagent-probe.d.ts +0 -34
  470. package/dist/utils/subagent-probe.js +0 -81
@@ -0,0 +1,1673 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { withLock } from '../utils/file-lock.js';
6
+ import { resolvePdPath } from './paths.js';
7
+ import { SampleNotFoundError } from '../config/index.js';
8
+ import type {
9
+ CorrectionSampleReviewStatus,
10
+ CorrectionExportMode,
11
+ TrajectoryDataStats,
12
+ TrajectoryAssistantTurnInput,
13
+ TrajectoryUserTurnInput,
14
+ TrajectoryToolCallInput,
15
+ TrajectoryPainEventInput,
16
+ TrajectoryGateBlockInput,
17
+ DailyMetricRow,
18
+ TrajectoryTrustChangeInput,
19
+ TrajectoryPrincipleEventInput,
20
+ TrajectoryTaskOutcomeInput,
21
+ TrajectorySessionInput,
22
+ TaskKind,
23
+ TaskPriority,
24
+ EvolutionTaskInput,
25
+ EvolutionTaskInputV2,
26
+ EvolutionEventInput,
27
+ EvolutionTaskRecord,
28
+ EvolutionEventRecord,
29
+ EvolutionTaskFilters,
30
+ AssistantTurnRecord,
31
+ CorrectionSampleRecord,
32
+ TrajectoryExportResult,
33
+ TrajectoryDatabaseOptions,
34
+ } from './trajectory-types.js';
35
+
36
+ export type {
37
+ CorrectionSampleReviewStatus,
38
+ CorrectionExportMode,
39
+ TrajectoryDataStats,
40
+ TrajectoryAssistantTurnInput,
41
+ TrajectoryUserTurnInput,
42
+ TrajectoryToolCallInput,
43
+ TrajectoryPainEventInput,
44
+ TrajectoryGateBlockInput,
45
+ DailyMetricRow,
46
+ TrajectoryTrustChangeInput,
47
+ TrajectoryPrincipleEventInput,
48
+ TrajectoryTaskOutcomeInput,
49
+ TrajectorySessionInput,
50
+ TaskKind,
51
+ TaskPriority,
52
+ EvolutionTaskInput,
53
+ EvolutionTaskInputV2,
54
+ EvolutionEventInput,
55
+ EvolutionTaskRecord,
56
+ EvolutionEventRecord,
57
+ EvolutionTaskFilters,
58
+ AssistantTurnRecord,
59
+ CorrectionSampleRecord,
60
+ TrajectoryExportResult,
61
+ TrajectoryDatabaseOptions,
62
+ } from './trajectory-types.js';
63
+
64
+ /**
65
+ * Trajectory database stores HISTORICAL and ANALYTICS data.
66
+ *
67
+ * PURPOSE: Track task outcomes, trust changes, and evolution progress over time.
68
+ * USAGE: Insights, trends, and Phase 3 supporting evidence (where explicitly allowed).
69
+ * NOT FOR: Control decisions, Phase 3 eligibility, or real-time operations.
70
+ *
71
+ * Runtime truth comes from: queue state, workspace trust scorecard, active sessions
72
+ */
73
+
74
+ const DEFAULT_INLINE_THRESHOLD = 16 * 1024;
75
+ const DEFAULT_BUSY_TIMEOUT_MS = 5000;
76
+ const DEFAULT_ORPHAN_BLOB_GRACE_DAYS = 7;
77
+ const SCHEMA_VERSION = 1;
78
+
79
+ function nowIso(): string {
80
+ return new Date().toISOString();
81
+ }
82
+
83
+ function safeJson(value: unknown): string {
84
+ return JSON.stringify(value ?? {});
85
+ }
86
+
87
+ function fileSizeIfExists(filePath: string): number {
88
+ try {
89
+ return fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
90
+ } catch {
91
+ return 0;
92
+ }
93
+ }
94
+
95
+ function summarizeForDiff(text: string): string {
96
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
97
+ }
98
+
99
+ function redactText(text: string): string {
100
+ return text
101
+ .replace(/[A-Za-z]:\\[^\s"'`]+/g, '<WINDOWS_PATH>')
102
+ .replace(/\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+/g, '<PATH>')
103
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '<EMAIL>')
104
+ .replace(/\b(sk|rk|pk)_[A-Za-z0-9]+\b/g, '<TOKEN>');
105
+ }
106
+
107
+ export class TrajectoryDatabase {
108
+ private readonly workspaceDir: string;
109
+ private readonly stateDir: string;
110
+ private readonly dbPath: string;
111
+ private readonly blobDir: string;
112
+ private readonly exportDir: string;
113
+ private readonly blobInlineThresholdBytes: number;
114
+ private readonly orphanBlobGraceMs: number;
115
+ private readonly db: Database.Database;
116
+
117
+ constructor(opts: TrajectoryDatabaseOptions) {
118
+ this.workspaceDir = path.resolve(opts.workspaceDir);
119
+ this.stateDir = resolvePdPath(this.workspaceDir, 'STATE_DIR');
120
+ this.dbPath = resolvePdPath(this.workspaceDir, 'TRAJECTORY_DB');
121
+ this.blobDir = resolvePdPath(this.workspaceDir, 'TRAJECTORY_BLOBS_DIR');
122
+ this.exportDir = resolvePdPath(this.workspaceDir, 'EXPORTS_DIR');
123
+ this.blobInlineThresholdBytes = opts.blobInlineThresholdBytes ?? DEFAULT_INLINE_THRESHOLD;
124
+ this.orphanBlobGraceMs = Math.max(0, (opts.orphanBlobGraceDays ?? DEFAULT_ORPHAN_BLOB_GRACE_DAYS) * 24 * 60 * 60 * 1000);
125
+
126
+ fs.mkdirSync(this.stateDir, { recursive: true });
127
+ fs.mkdirSync(this.blobDir, { recursive: true });
128
+ fs.mkdirSync(this.exportDir, { recursive: true });
129
+
130
+ this.db = new Database(this.dbPath);
131
+ this.db.pragma('journal_mode = WAL');
132
+ this.db.pragma('foreign_keys = ON');
133
+ this.db.pragma('synchronous = NORMAL');
134
+ this.db.pragma(`busy_timeout = ${Math.max(0, opts.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS)}`);
135
+ this.initSchema();
136
+ this.importLegacyArtifacts();
137
+ this.pruneUnreferencedBlobs();
138
+ }
139
+
140
+ dispose(): void {
141
+ this.db.close();
142
+ }
143
+
144
+ recordSession(input: TrajectorySessionInput): void {
145
+ const startedAt = input.startedAt ?? nowIso();
146
+ this.withWrite(() => {
147
+ this.db.prepare(`
148
+ INSERT INTO sessions (session_id, started_at, updated_at)
149
+ VALUES (?, ?, ?)
150
+ ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at
151
+ `).run(input.sessionId, startedAt, nowIso());
152
+ });
153
+ }
154
+
155
+ recordAssistantTurn(input: TrajectoryAssistantTurnInput): number {
156
+ this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
157
+ const rawStorage = this.storeRawText('assistant', input.rawText);
158
+ const createdAt = input.createdAt ?? nowIso();
159
+
160
+ return this.withWrite(() => {
161
+ const result = this.db.prepare(`
162
+ INSERT INTO assistant_turns (
163
+ session_id, run_id, provider, model, raw_text, sanitized_text, usage_json,
164
+ empathy_signal_json, blob_ref, raw_excerpt, created_at
165
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
166
+ `).run(
167
+ input.sessionId,
168
+ input.runId,
169
+ input.provider,
170
+ input.model,
171
+ rawStorage.inlineText,
172
+ input.sanitizedText,
173
+ safeJson(input.usageJson),
174
+ safeJson(input.empathySignalJson),
175
+ rawStorage.blobRef,
176
+ rawStorage.excerpt,
177
+ createdAt,
178
+ );
179
+ return Number(result.lastInsertRowid);
180
+ });
181
+ }
182
+
183
+ recordUserTurn(input: TrajectoryUserTurnInput): number {
184
+ this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
185
+ const rawStorage = this.storeRawText('user', input.rawText);
186
+ const createdAt = input.createdAt ?? nowIso();
187
+ return this.withWrite(() => {
188
+ const result = this.db.prepare(`
189
+ INSERT INTO user_turns (
190
+ session_id, turn_index, raw_text, blob_ref, raw_excerpt,
191
+ correction_detected, correction_cue, references_assistant_turn_id, created_at
192
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
193
+ `).run(
194
+ input.sessionId,
195
+ input.turnIndex,
196
+ rawStorage.inlineText,
197
+ rawStorage.blobRef,
198
+ rawStorage.excerpt,
199
+ input.correctionDetected ? 1 : 0,
200
+ input.correctionCue ?? null,
201
+ input.referencesAssistantTurnId ?? null,
202
+ createdAt,
203
+ );
204
+ return Number(result.lastInsertRowid);
205
+ });
206
+ }
207
+
208
+ recordToolCall(input: TrajectoryToolCallInput): number {
209
+ this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
210
+ const createdAt = input.createdAt ?? nowIso();
211
+ // Extract filePath from paramsJson if provided and is an object with filePath
212
+ const paramsObj = input.paramsJson as Record<string, unknown> | undefined;
213
+ const filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
214
+ const rowId = this.withWrite(() => {
215
+ const result = this.db.prepare(`
216
+ INSERT INTO tool_calls (
217
+ session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message,
218
+ gfi_before, gfi_after, params_json, created_at
219
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
220
+ `).run(
221
+ input.sessionId,
222
+ input.toolName,
223
+ input.outcome,
224
+ input.durationMs ?? null,
225
+ input.exitCode ?? null,
226
+ input.errorType ?? null,
227
+ input.errorMessage ?? null,
228
+ input.gfiBefore ?? null,
229
+ input.gfiAfter ?? null,
230
+ safeJson(input.paramsJson),
231
+ createdAt,
232
+ );
233
+ return Number(result.lastInsertRowid);
234
+ });
235
+
236
+ if (input.outcome === 'success') {
237
+ this.maybeCreateCorrectionSample(input.sessionId);
238
+ }
239
+ return rowId;
240
+ }
241
+
242
+ recordPainEvent(input: TrajectoryPainEventInput): void {
243
+ this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
244
+ this.withWrite(() => {
245
+ this.db.prepare(`
246
+ INSERT INTO pain_events (
247
+ session_id, source, score, reason, severity, origin, confidence, text, created_at
248
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
249
+ `).run(
250
+ input.sessionId,
251
+ input.source,
252
+ input.score,
253
+ input.reason ?? null,
254
+ input.severity ?? null,
255
+ input.origin ?? null,
256
+ input.confidence ?? null,
257
+ input.text ?? null,
258
+ input.createdAt ?? nowIso(),
259
+ );
260
+
261
+ // Maintain FTS5 index: insert text into pain_events_fts if text is provided (MEM-03, MEM-04)
262
+ if (input.text) {
263
+ const lastId = this.db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
264
+ this.db.prepare(`
265
+ INSERT INTO pain_events_fts (text, pain_event_id) VALUES (?, ?)
266
+ `).run(input.text, lastId.id);
267
+ }
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Search pain_events using FTS5 full-text search (MEM-04).
273
+ * Returns pain events matching the query, ordered by relevance.
274
+ */
275
+ searchPainEvents(query: string, limit: number = 10): Array<{
276
+ id: number;
277
+ sessionId: string;
278
+ source: string;
279
+ score: number;
280
+ reason: string | null;
281
+ severity: string | null;
282
+ origin: string | null;
283
+ confidence: number | null;
284
+ text: string | null;
285
+ createdAt: string;
286
+ }> {
287
+ if (!query || query.trim().length === 0) {
288
+ return [];
289
+ }
290
+
291
+ // Escape FTS5 special characters and format query for porter tokenizer
292
+ const ftsQuery = query.trim().split(/\s+/).map(term => `"${term.replace(/"/g, '""')}"`).join(' ');
293
+
294
+ try {
295
+ const results = this.db.prepare(`
296
+ SELECT pe.*
297
+ FROM pain_events_fts pf
298
+ JOIN pain_events pe ON pe.id = pf.pain_event_id
299
+ WHERE pain_events_fts MATCH ?
300
+ ORDER BY bm25(pain_events_fts) DESC
301
+ LIMIT ?
302
+ `).all(ftsQuery, limit) as Array<{
303
+ id: number;
304
+ session_id: string;
305
+ source: string;
306
+ score: number;
307
+ reason: string | null;
308
+ severity: string | null;
309
+ origin: string | null;
310
+ confidence: number | null;
311
+ text: string | null;
312
+ created_at: string;
313
+ }>;
314
+
315
+ return results.map(row => ({
316
+ id: row.id,
317
+ sessionId: row.session_id,
318
+ source: row.source,
319
+ score: row.score,
320
+ reason: row.reason,
321
+ severity: row.severity,
322
+ origin: row.origin,
323
+ confidence: row.confidence,
324
+ text: row.text,
325
+ createdAt: row.created_at,
326
+ }));
327
+ } catch (err) {
328
+ // If FTS5 query fails (e.g., syntax error), return empty results
329
+ console.warn(`[PD:TrajectoryDatabase] FTS5 search failed: ${String(err)}`);
330
+ return [];
331
+ }
332
+ }
333
+
334
+ recordGateBlock(input: TrajectoryGateBlockInput): void {
335
+ this.withWrite(() => {
336
+ this.db.prepare(`
337
+ INSERT INTO gate_blocks (session_id, tool_name, file_path, reason, plan_status, created_at)
338
+ VALUES (?, ?, ?, ?, ?, ?)
339
+ `).run(
340
+ input.sessionId ?? null,
341
+ input.toolName,
342
+ input.filePath ?? null,
343
+ input.reason,
344
+ input.planStatus ?? null,
345
+ input.createdAt ?? nowIso(),
346
+ );
347
+ });
348
+ }
349
+
350
+ recordTrustChange(input: TrajectoryTrustChangeInput): void {
351
+ this.withWrite(() => {
352
+ this.db.prepare(`
353
+ INSERT INTO trust_changes (session_id, previous_score, new_score, delta, reason, created_at)
354
+ VALUES (?, ?, ?, ?, ?, ?)
355
+ `).run(
356
+ input.sessionId ?? null,
357
+ input.previousScore,
358
+ input.newScore,
359
+ input.delta,
360
+ input.reason,
361
+ input.createdAt ?? nowIso(),
362
+ );
363
+ });
364
+ }
365
+
366
+ recordPrincipleEvent(input: TrajectoryPrincipleEventInput): void {
367
+ this.withWrite(() => {
368
+ this.db.prepare(`
369
+ INSERT INTO principle_events (principle_id, event_type, payload_json, created_at)
370
+ VALUES (?, ?, ?, ?)
371
+ `).run(
372
+ input.principleId ?? null,
373
+ input.eventType,
374
+ safeJson(input.payload),
375
+ input.createdAt ?? nowIso(),
376
+ );
377
+ });
378
+ }
379
+
380
+ recordTaskOutcome(input: TrajectoryTaskOutcomeInput): void {
381
+ this.withWrite(() => {
382
+ this.db.prepare(`
383
+ INSERT INTO task_outcomes (session_id, task_id, outcome, summary, principle_ids_json, created_at)
384
+ VALUES (?, ?, ?, ?, ?, ?)
385
+ `).run(
386
+ input.sessionId,
387
+ input.taskId ?? null,
388
+ input.outcome,
389
+ input.summary ?? null,
390
+ safeJson(input.principleIdsJson),
391
+ input.createdAt ?? nowIso(),
392
+ );
393
+ });
394
+ }
395
+
396
+ recordEvolutionTask(input: EvolutionTaskInput): void {
397
+ const now = nowIso();
398
+ // Cast to V2 to access new fields
399
+ const v2 = input as EvolutionTaskInputV2;
400
+ this.withWrite(() => {
401
+ this.db.prepare(`
402
+ INSERT INTO evolution_tasks (
403
+ task_id, trace_id, source, reason, score, status,
404
+ enqueued_at, started_at, completed_at, resolution, created_at, updated_at,
405
+ task_kind, priority, retry_count, max_retries, last_error, result_ref
406
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
407
+ ON CONFLICT(task_id) DO UPDATE SET
408
+ status = excluded.status,
409
+ started_at = excluded.started_at,
410
+ completed_at = excluded.completed_at,
411
+ resolution = excluded.resolution,
412
+ updated_at = excluded.updated_at,
413
+ task_kind = excluded.task_kind,
414
+ priority = excluded.priority,
415
+ retry_count = excluded.retry_count,
416
+ max_retries = excluded.max_retries,
417
+ last_error = excluded.last_error,
418
+ result_ref = excluded.result_ref
419
+ `).run(
420
+ input.taskId,
421
+ input.traceId,
422
+ input.source,
423
+ input.reason ?? null,
424
+ input.score ?? 0,
425
+ input.status ?? 'pending',
426
+ input.enqueuedAt ?? null,
427
+ input.startedAt ?? null,
428
+ input.completedAt ?? null,
429
+ input.resolution ?? null,
430
+ input.createdAt ?? now,
431
+ input.updatedAt ?? now,
432
+ v2.taskKind ?? null,
433
+ v2.priority ?? null,
434
+ v2.retryCount ?? null,
435
+ v2.maxRetries ?? null,
436
+ v2.lastError ?? null,
437
+ v2.resultRef ?? null,
438
+ );
439
+ });
440
+ }
441
+
442
+ updateEvolutionTask(taskId: string, updates: Partial<Omit<EvolutionTaskInput, 'taskId' | 'traceId' | 'source'>>): void {
443
+ const now = nowIso();
444
+ // Cast to V2 to access new fields
445
+ const v2Updates = updates as Partial<Omit<EvolutionTaskInputV2, 'taskId' | 'traceId' | 'source'>>;
446
+ this.withWrite(() => {
447
+ const setClauses: string[] = ['updated_at = ?'];
448
+ const values: unknown[] = [now];
449
+
450
+ if (updates.status !== undefined) {
451
+ setClauses.push('status = ?');
452
+ values.push(updates.status);
453
+ }
454
+ if (updates.startedAt !== undefined) {
455
+ setClauses.push('started_at = ?');
456
+ values.push(updates.startedAt);
457
+ }
458
+ if (updates.completedAt !== undefined) {
459
+ setClauses.push('completed_at = ?');
460
+ values.push(updates.completedAt);
461
+ }
462
+ if (updates.resolution !== undefined) {
463
+ setClauses.push('resolution = ?');
464
+ values.push(updates.resolution);
465
+ }
466
+ if (updates.score !== undefined) {
467
+ setClauses.push('score = ?');
468
+ values.push(updates.score);
469
+ }
470
+ // V2 fields
471
+ if (v2Updates.taskKind !== undefined) {
472
+ setClauses.push('task_kind = ?');
473
+ values.push(v2Updates.taskKind);
474
+ }
475
+ if (v2Updates.priority !== undefined) {
476
+ setClauses.push('priority = ?');
477
+ values.push(v2Updates.priority);
478
+ }
479
+ if (v2Updates.retryCount !== undefined) {
480
+ setClauses.push('retry_count = ?');
481
+ values.push(v2Updates.retryCount);
482
+ }
483
+ if (v2Updates.maxRetries !== undefined) {
484
+ setClauses.push('max_retries = ?');
485
+ values.push(v2Updates.maxRetries);
486
+ }
487
+ if (v2Updates.lastError !== undefined) {
488
+ setClauses.push('last_error = ?');
489
+ values.push(v2Updates.lastError);
490
+ }
491
+ if (v2Updates.resultRef !== undefined) {
492
+ setClauses.push('result_ref = ?');
493
+ values.push(v2Updates.resultRef);
494
+ }
495
+
496
+ values.push(taskId);
497
+ this.db.prepare(`
498
+ UPDATE evolution_tasks SET ${setClauses.join(', ')} WHERE task_id = ?
499
+ `).run(...values);
500
+ });
501
+ }
502
+
503
+ recordEvolutionEvent(input: EvolutionEventInput): void {
504
+ this.withWrite(() => {
505
+ this.db.prepare(`
506
+ INSERT INTO evolution_events (trace_id, task_id, stage, level, message, summary, metadata_json, created_at)
507
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
508
+ `).run(
509
+ input.traceId,
510
+ input.taskId ?? null,
511
+ input.stage,
512
+ input.level ?? 'info',
513
+ input.message,
514
+ input.summary ?? null,
515
+ safeJson(input.metadata),
516
+ input.createdAt ?? nowIso(),
517
+ );
518
+ });
519
+ }
520
+
521
+ /**
522
+ * List evolution tasks with optional filtering.
523
+ *
524
+ * Returns: Analytics data aggregated from trajectory database.
525
+ * Not: Runtime truth or real-time queue state.
526
+ */
527
+ listEvolutionTasks(filters: EvolutionTaskFilters = {}): EvolutionTaskRecord[] {
528
+ const conditions: string[] = [];
529
+ const values: unknown[] = [];
530
+
531
+ if (filters.status) {
532
+ conditions.push('status = ?');
533
+ values.push(filters.status);
534
+ }
535
+ if (filters.dateFrom) {
536
+ conditions.push('created_at >= ?');
537
+ values.push(filters.dateFrom);
538
+ }
539
+ if (filters.dateTo) {
540
+ conditions.push('created_at <= ?');
541
+ values.push(filters.dateTo);
542
+ }
543
+
544
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
545
+ const limit = filters.limit ?? 50;
546
+ const offset = filters.offset ?? 0;
547
+
548
+ const rows = this.db.prepare(`
549
+ SELECT id, task_id, trace_id, source, reason, score, status,
550
+ enqueued_at, started_at, completed_at, resolution, created_at, updated_at,
551
+ task_kind, priority, retry_count, max_retries, last_error, result_ref
552
+ FROM evolution_tasks
553
+ ${whereClause}
554
+ ORDER BY created_at DESC
555
+ LIMIT ? OFFSET ?
556
+ `).all(...values, limit, offset) as Array<Record<string, unknown>>;
557
+
558
+ return rows.map((row) => ({
559
+ id: Number(row.id),
560
+ taskId: String(row.task_id),
561
+ traceId: String(row.trace_id),
562
+ source: String(row.source),
563
+ reason: row.reason ? String(row.reason) : null,
564
+ score: Number(row.score ?? 0),
565
+ status: String(row.status),
566
+ enqueuedAt: row.enqueued_at ? String(row.enqueued_at) : null,
567
+ startedAt: row.started_at ? String(row.started_at) : null,
568
+ completedAt: row.completed_at ? String(row.completed_at) : null,
569
+ resolution: row.resolution ? String(row.resolution) : null,
570
+ createdAt: String(row.created_at),
571
+ updatedAt: String(row.updated_at),
572
+ taskKind: row.task_kind ? (row.task_kind as TaskKind) : null,
573
+ priority: row.priority ? (row.priority as TaskPriority) : null,
574
+ retryCount: row.retry_count != null ? Number(row.retry_count) : null,
575
+ maxRetries: row.max_retries != null ? Number(row.max_retries) : null,
576
+ lastError: row.last_error ? String(row.last_error) : null,
577
+ resultRef: row.result_ref ? String(row.result_ref) : null,
578
+ }));
579
+ }
580
+
581
+ /**
582
+ * List evolution events for a trace or globally.
583
+ *
584
+ * Returns: Analytics data aggregated from trajectory database.
585
+ * Not: Runtime truth or real-time queue state.
586
+ */
587
+ listEvolutionEvents(traceId?: string, filters: { limit?: number; offset?: number } = {}): EvolutionEventRecord[] {
588
+ const limit = filters.limit ?? 100;
589
+ const offset = filters.offset ?? 0;
590
+
591
+ let rows: Array<Record<string, unknown>>;
592
+ if (traceId) {
593
+ rows = this.db.prepare(`
594
+ SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
595
+ FROM evolution_events
596
+ WHERE trace_id = ?
597
+ ORDER BY created_at ASC
598
+ LIMIT ? OFFSET ?
599
+ `).all(traceId, limit, offset) as Array<Record<string, unknown>>;
600
+ } else {
601
+ rows = this.db.prepare(`
602
+ SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
603
+ FROM evolution_events
604
+ ORDER BY created_at DESC
605
+ LIMIT ? OFFSET ?
606
+ `).all(limit, offset) as Array<Record<string, unknown>>;
607
+ }
608
+
609
+ return rows.map((row) => ({
610
+ id: Number(row.id),
611
+ traceId: String(row.trace_id),
612
+ taskId: row.task_id ? String(row.task_id) : null,
613
+ stage: String(row.stage),
614
+ level: String(row.level ?? 'info'),
615
+ message: String(row.message),
616
+ summary: row.summary ? String(row.summary) : null,
617
+ metadata: JSON.parse(String(row.metadata_json ?? '{}')),
618
+ createdAt: String(row.created_at),
619
+ }));
620
+ }
621
+
622
+ /**
623
+ * Get evolution task by trace ID.
624
+ *
625
+ * Returns: Analytics data aggregated from trajectory database.
626
+ * Not: Runtime truth or real-time queue state.
627
+ */
628
+ getEvolutionTaskByTraceId(traceId: string): EvolutionTaskRecord | null {
629
+ const row = this.db.prepare(`
630
+ SELECT id, task_id, trace_id, source, reason, score, status,
631
+ enqueued_at, started_at, completed_at, resolution, created_at, updated_at,
632
+ task_kind, priority, retry_count, max_retries, last_error, result_ref
633
+ FROM evolution_tasks
634
+ WHERE trace_id = ?
635
+ LIMIT 1
636
+ `).get(traceId) as Record<string, unknown> | undefined;
637
+
638
+ if (!row) return null;
639
+
640
+ return {
641
+ id: Number(row.id),
642
+ taskId: String(row.task_id),
643
+ traceId: String(row.trace_id),
644
+ source: String(row.source),
645
+ reason: row.reason ? String(row.reason) : null,
646
+ score: Number(row.score ?? 0),
647
+ status: String(row.status),
648
+ enqueuedAt: row.enqueued_at ? String(row.enqueued_at) : null,
649
+ startedAt: row.started_at ? String(row.started_at) : null,
650
+ completedAt: row.completed_at ? String(row.completed_at) : null,
651
+ resolution: row.resolution ? String(row.resolution) : null,
652
+ createdAt: String(row.created_at),
653
+ updatedAt: String(row.updated_at),
654
+ taskKind: row.task_kind ? (row.task_kind as TaskKind) : null,
655
+ priority: row.priority ? (row.priority as TaskPriority) : null,
656
+ retryCount: row.retry_count != null ? Number(row.retry_count) : null,
657
+ maxRetries: row.max_retries != null ? Number(row.max_retries) : null,
658
+ lastError: row.last_error ? String(row.last_error) : null,
659
+ resultRef: row.result_ref ? String(row.result_ref) : null,
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Get evolution task statistics.
665
+ *
666
+ * Returns: Analytics data aggregated from trajectory database.
667
+ * Not: Runtime truth or real-time queue state.
668
+ */
669
+ getEvolutionStats(): { total: number; pending: number; inProgress: number; completed: number; failed: number } {
670
+ const rows = this.db.prepare(`
671
+ SELECT status, COUNT(*) as count FROM evolution_tasks GROUP BY status
672
+ `).all() as Array<{ status: string; count: number }>;
673
+
674
+ const stats = { total: 0, pending: 0, inProgress: 0, completed: 0, failed: 0 };
675
+ for (const row of rows) {
676
+ stats.total += row.count;
677
+ if (row.status === 'pending') stats.pending = row.count;
678
+ else if (row.status === 'in_progress') stats.inProgress = row.count;
679
+ else if (row.status === 'completed') stats.completed = row.count;
680
+ else if (row.status === 'failed') stats.failed = row.count;
681
+ }
682
+ return stats;
683
+ }
684
+
685
+ /**
686
+ * List recent sessions from the trajectory database.
687
+ *
688
+ * Returns: Recent session records ordered by most recently updated.
689
+ *
690
+ * @param options.limit - Maximum number of sessions to return (default: 20)
691
+ * @param options.dateFrom - Only return sessions updated after this date
692
+ * @param options.dateTo - Only return sessions updated before this date
693
+ */
694
+ listRecentSessions(options: { limit?: number; dateFrom?: string; dateTo?: string } = {}): Array<{ sessionId: string; startedAt: string; updatedAt: string }> {
695
+ const conditions: string[] = [];
696
+ const values: unknown[] = [];
697
+
698
+ if (options.dateFrom) {
699
+ conditions.push('updated_at >= ?');
700
+ values.push(options.dateFrom);
701
+ }
702
+ if (options.dateTo) {
703
+ conditions.push('updated_at <= ?');
704
+ values.push(options.dateTo);
705
+ }
706
+
707
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
708
+ const limit = options.limit ?? 20;
709
+
710
+ const rows = this.db.prepare(`
711
+ SELECT session_id, started_at, updated_at
712
+ FROM sessions
713
+ ${whereClause}
714
+ ORDER BY updated_at DESC
715
+ LIMIT ?
716
+ `).all(...values, limit) as Array<Record<string, unknown>>;
717
+
718
+ return rows.map((row) => ({
719
+ sessionId: String(row.session_id),
720
+ startedAt: String(row.started_at),
721
+ updatedAt: String(row.updated_at),
722
+ }));
723
+ }
724
+
725
+ /**
726
+ * List assistant turns for a session.
727
+ *
728
+ * Returns: Analytics data aggregated from trajectory database.
729
+ * Not: Runtime truth or real-time queue state.
730
+ */
731
+ listAssistantTurns(sessionId: string): AssistantTurnRecord[] {
732
+ const rows = this.db.prepare(`
733
+ SELECT id, session_id, run_id, provider, model, raw_text, sanitized_text, blob_ref, created_at
734
+ FROM assistant_turns
735
+ WHERE session_id = ?
736
+ ORDER BY id ASC
737
+ `).all(sessionId) as Array<Record<string, unknown>>;
738
+
739
+ return rows.map((row) => ({
740
+ id: Number(row.id),
741
+ sessionId: String(row.session_id),
742
+ runId: String(row.run_id),
743
+ provider: String(row.provider),
744
+ model: String(row.model),
745
+ rawText: this.restoreRawText(row.raw_text as string | null, row.blob_ref as string | null),
746
+ sanitizedText: String(row.sanitized_text ?? ''),
747
+ blobRef: row.blob_ref ? String(row.blob_ref) : null,
748
+ createdAt: String(row.created_at),
749
+ }));
750
+ }
751
+
752
+ /**
753
+ * List tool calls for a session.
754
+ *
755
+ * Returns: Analytics data aggregated from trajectory database.
756
+ * Not: Runtime truth or real-time queue state.
757
+ */
758
+ listToolCallsForSession(sessionId: string): Array<{
759
+ id: number;
760
+ toolName: string;
761
+ outcome: string;
762
+ filePath: string | null;
763
+ durationMs: number | null;
764
+ exitCode: number | null;
765
+ errorType: string | null;
766
+ errorMessage: string | null;
767
+ gfiBefore: number | null;
768
+ gfiAfter: number | null;
769
+ createdAt: string;
770
+ }> {
771
+ const rows = this.db.prepare(`
772
+ SELECT id, tool_name, outcome, params_json, duration_ms, exit_code, error_type, error_message,
773
+ gfi_before, gfi_after, created_at
774
+ FROM tool_calls
775
+ WHERE session_id = ?
776
+ ORDER BY id ASC
777
+ `).all(sessionId) as Array<Record<string, unknown>>;
778
+
779
+ return rows.map((row) => {
780
+ // Extract filePath from params_json if present
781
+ let filePath: string | null = null;
782
+ if (row.params_json && typeof row.params_json === 'string') {
783
+ try {
784
+ const params = JSON.parse(row.params_json);
785
+ if (params && typeof params.filePath === 'string') {
786
+ filePath = params.filePath;
787
+ }
788
+ } catch {
789
+ // Ignore malformed JSON
790
+ }
791
+ }
792
+ return {
793
+ id: Number(row.id),
794
+ toolName: String(row.tool_name),
795
+ outcome: String(row.outcome),
796
+ filePath,
797
+ durationMs: row.duration_ms != null ? Number(row.duration_ms) : null,
798
+ exitCode: row.exit_code != null ? Number(row.exit_code) : null,
799
+ errorType: row.error_type ? String(row.error_type) : null,
800
+ errorMessage: row.error_message ? String(row.error_message) : null,
801
+ gfiBefore: row.gfi_before != null ? Number(row.gfi_before) : null,
802
+ gfiAfter: row.gfi_after != null ? Number(row.gfi_after) : null,
803
+ createdAt: String(row.created_at),
804
+ };
805
+ });
806
+ }
807
+
808
+ /**
809
+ * List pain events for a session.
810
+ *
811
+ * Returns: Analytics data aggregated from trajectory database.
812
+ * Not: Runtime truth or real-time queue state.
813
+ */
814
+ listPainEventsForSession(sessionId: string): Array<{
815
+ id: number;
816
+ source: string;
817
+ score: number;
818
+ reason: string | null;
819
+ severity: string | null;
820
+ origin: string | null;
821
+ confidence: number | null;
822
+ createdAt: string;
823
+ }> {
824
+ const rows = this.db.prepare(`
825
+ SELECT id, source, score, reason, severity, origin, confidence, created_at
826
+ FROM pain_events
827
+ WHERE session_id = ?
828
+ ORDER BY created_at ASC
829
+ `).all(sessionId) as Array<Record<string, unknown>>;
830
+
831
+ return rows.map((row) => ({
832
+ id: Number(row.id),
833
+ source: String(row.source),
834
+ score: Number(row.score),
835
+ reason: row.reason ? String(row.reason) : null,
836
+ severity: row.severity ? String(row.severity) : null,
837
+ origin: row.origin ? String(row.origin) : null,
838
+ confidence: row.confidence != null ? Number(row.confidence) : null,
839
+ createdAt: String(row.created_at),
840
+ }));
841
+ }
842
+
843
+ /**
844
+ * List user turns for a session.
845
+ * Returns sanitized/reduced fields for nocturnal use — NO raw text.
846
+ */
847
+ listUserTurnsForSession(sessionId: string): Array<{
848
+ id: number;
849
+ turnIndex: number;
850
+ correctionDetected: boolean;
851
+ correctionCue: string | null;
852
+ createdAt: string;
853
+ }> {
854
+ const rows = this.db.prepare(`
855
+ SELECT id, turn_index, correction_detected, correction_cue, created_at
856
+ FROM user_turns
857
+ WHERE session_id = ?
858
+ ORDER BY turn_index ASC
859
+ `).all(sessionId) as Array<Record<string, unknown>>;
860
+
861
+ return rows.map((row) => ({
862
+ id: Number(row.id),
863
+ turnIndex: Number(row.turn_index),
864
+ correctionDetected: Boolean(row.correction_detected),
865
+ correctionCue: row.correction_cue ? String(row.correction_cue) : null,
866
+ createdAt: String(row.created_at),
867
+ }));
868
+ }
869
+
870
+ /**
871
+ * List correction samples with optional review status filter.
872
+ *
873
+ * Returns: Analytics data aggregated from trajectory database.
874
+ * Not: Runtime truth or real-time queue state.
875
+ */
876
+ listCorrectionSamples(status: CorrectionSampleReviewStatus = 'pending'): CorrectionSampleRecord[] {
877
+ const rows = this.db.prepare(`
878
+ SELECT sample_id, session_id, bad_assistant_turn_id, user_correction_turn_id,
879
+ recovery_tool_span_json, diff_excerpt, principle_ids_json, quality_score,
880
+ review_status, export_mode, created_at, updated_at
881
+ FROM correction_samples
882
+ WHERE review_status = ?
883
+ ORDER BY created_at DESC
884
+ `).all(status) as Array<Record<string, unknown>>;
885
+
886
+ return rows.map((row) => ({
887
+ sampleId: String(row.sample_id),
888
+ sessionId: String(row.session_id),
889
+ badAssistantTurnId: Number(row.bad_assistant_turn_id),
890
+ userCorrectionTurnId: Number(row.user_correction_turn_id),
891
+ recoveryToolSpanJson: String(row.recovery_tool_span_json),
892
+ diffExcerpt: String(row.diff_excerpt ?? ''),
893
+ principleIdsJson: String(row.principle_ids_json ?? '[]'),
894
+ qualityScore: Number(row.quality_score),
895
+ reviewStatus: row.review_status as CorrectionSampleReviewStatus,
896
+ exportMode: row.export_mode as CorrectionExportMode,
897
+ createdAt: String(row.created_at),
898
+ updatedAt: String(row.updated_at),
899
+ }));
900
+ }
901
+
902
+ /**
903
+ * List gate blocks for a session.
904
+ * Returns minimal fields for nocturnal use — no raw text.
905
+ */
906
+ listGateBlocksForSession(sessionId: string): Array<{
907
+ id: number;
908
+ toolName: string;
909
+ filePath: string | null;
910
+ reason: string;
911
+ planStatus: string | null;
912
+ createdAt: string;
913
+ }> {
914
+ const rows = this.db.prepare(`
915
+ SELECT id, tool_name, file_path, reason, plan_status, created_at
916
+ FROM gate_blocks
917
+ WHERE session_id = ?
918
+ ORDER BY id ASC
919
+ `).all(sessionId) as Array<Record<string, unknown>>;
920
+
921
+ return rows.map((row) => ({
922
+ id: Number(row.id),
923
+ toolName: String(row.tool_name),
924
+ filePath: row.file_path ? String(row.file_path) : null,
925
+ reason: String(row.reason),
926
+ planStatus: row.plan_status ? String(row.plan_status) : null,
927
+ createdAt: String(row.created_at),
928
+ }));
929
+ }
930
+
931
+ reviewCorrectionSample(sampleId: string, status: Exclude<CorrectionSampleReviewStatus, 'pending'>, note?: string): CorrectionSampleRecord {
932
+ const updatedAt = nowIso();
933
+ const updated = this.withWrite(() => {
934
+ const updateResult = this.db.prepare(`
935
+ UPDATE correction_samples
936
+ SET review_status = ?, updated_at = ?
937
+ WHERE sample_id = ?
938
+ `).run(status, updatedAt, sampleId);
939
+ if (updateResult.changes === 0) {
940
+ return false;
941
+ }
942
+ this.db.prepare(`
943
+ INSERT INTO sample_reviews (sample_id, review_status, note, created_at)
944
+ VALUES (?, ?, ?, ?)
945
+ `).run(sampleId, status, note ?? null, updatedAt);
946
+ return true;
947
+ });
948
+ if (!updated) {
949
+ throw new SampleNotFoundError(sampleId);
950
+ }
951
+
952
+ const record = this.db.prepare(`
953
+ SELECT sample_id, session_id, bad_assistant_turn_id, user_correction_turn_id,
954
+ recovery_tool_span_json, diff_excerpt, principle_ids_json, quality_score,
955
+ review_status, export_mode, created_at, updated_at
956
+ FROM correction_samples
957
+ WHERE sample_id = ?
958
+ `).get(sampleId) as Record<string, unknown>;
959
+ if (!record) {
960
+ throw new SampleNotFoundError(`${sampleId} (after update)`);
961
+ }
962
+
963
+ return {
964
+ sampleId: String(record.sample_id),
965
+ sessionId: String(record.session_id),
966
+ badAssistantTurnId: Number(record.bad_assistant_turn_id),
967
+ userCorrectionTurnId: Number(record.user_correction_turn_id),
968
+ recoveryToolSpanJson: String(record.recovery_tool_span_json),
969
+ diffExcerpt: String(record.diff_excerpt ?? ''),
970
+ principleIdsJson: String(record.principle_ids_json ?? '[]'),
971
+ qualityScore: Number(record.quality_score),
972
+ reviewStatus: record.review_status as CorrectionSampleReviewStatus,
973
+ exportMode: record.export_mode as CorrectionExportMode,
974
+ createdAt: String(record.created_at),
975
+ updatedAt: String(record.updated_at),
976
+ };
977
+ }
978
+
979
+ /**
980
+ * Export correction samples to JSONL file.
981
+ *
982
+ * Returns: Analytics data aggregated from trajectory database.
983
+ * Not: Runtime truth or real-time queue state.
984
+ */
985
+ exportCorrections(opts: { mode: CorrectionExportMode; approvedOnly: boolean }): TrajectoryExportResult {
986
+ const rows = this.db.prepare(`
987
+ SELECT cs.sample_id, cs.session_id, cs.recovery_tool_span_json, cs.diff_excerpt, cs.quality_score,
988
+ at.raw_text AS assistant_raw_text, at.blob_ref AS assistant_blob_ref, at.sanitized_text,
989
+ ut.raw_text AS user_raw_text, ut.blob_ref AS user_blob_ref, ut.correction_cue
990
+ FROM correction_samples cs
991
+ JOIN assistant_turns at ON at.id = cs.bad_assistant_turn_id
992
+ JOIN user_turns ut ON ut.id = cs.user_correction_turn_id
993
+ WHERE (? = 0 OR cs.review_status = 'approved')
994
+ ORDER BY cs.created_at ASC
995
+ `).all(opts.approvedOnly ? 1 : 0) as Array<Record<string, unknown>>;
996
+
997
+ const exportPath = path.join(this.exportDir, `corrections-${Date.now()}-${opts.mode}.jsonl`);
998
+ const lines = rows.map((row) => {
999
+ const assistantRaw = this.restoreRawText(row.assistant_raw_text as string | null, row.assistant_blob_ref as string | null);
1000
+ const userRaw = this.restoreRawText(row.user_raw_text as string | null, row.user_blob_ref as string | null);
1001
+ const assistantText = opts.mode === 'redacted' ? redactText(assistantRaw) : assistantRaw;
1002
+ const userText = opts.mode === 'redacted' ? redactText(userRaw) : userRaw;
1003
+ return JSON.stringify({
1004
+ sample_id: row.sample_id,
1005
+ session_id: row.session_id,
1006
+ instruction: userText,
1007
+ input_context: assistantText,
1008
+ bad_attempt_summary: String(row.diff_excerpt ?? ''),
1009
+ preferred_response: userText,
1010
+ labels: {
1011
+ correction_cue: row.correction_cue,
1012
+ quality_score: row.quality_score,
1013
+ },
1014
+ metadata: {
1015
+ mode: opts.mode,
1016
+ recovery_tool_span_json: row.recovery_tool_span_json,
1017
+ },
1018
+ });
1019
+ });
1020
+
1021
+ fs.writeFileSync(exportPath, `${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`, 'utf8');
1022
+ this.recordExportAudit('corrections', opts.mode, opts.approvedOnly, exportPath, rows.length);
1023
+ return { filePath: exportPath, count: rows.length, mode: opts.mode };
1024
+ }
1025
+
1026
+ /**
1027
+ * Export analytics data to JSON file.
1028
+ *
1029
+ * Returns: Analytics data aggregated from trajectory database.
1030
+ * Not: Runtime truth or real-time queue state.
1031
+ */
1032
+ exportAnalytics(): TrajectoryExportResult {
1033
+ const payload = {
1034
+ generatedAt: nowIso(),
1035
+ stats: this.getDataStats(),
1036
+ dailyMetrics: this.dailyMetrics(),
1037
+ errorClusters: this.db.prepare('SELECT * FROM v_error_clusters').all(),
1038
+ principleEffectiveness: this.db.prepare('SELECT * FROM v_principle_effectiveness').all(),
1039
+ sampleQueue: this.db.prepare('SELECT * FROM v_sample_queue').all(),
1040
+ };
1041
+ const exportPath = path.join(this.exportDir, `analytics-${Date.now()}.json`);
1042
+ fs.writeFileSync(exportPath, JSON.stringify(payload, null, 2), 'utf8');
1043
+ this.recordExportAudit('analytics', 'raw', true, exportPath, Array.isArray(payload.dailyMetrics) ? payload.dailyMetrics.length : 0);
1044
+ return { filePath: exportPath, count: Array.isArray(payload.dailyMetrics) ? payload.dailyMetrics.length : 0 };
1045
+ }
1046
+
1047
+ /**
1048
+ * Get trajectory database statistics.
1049
+ *
1050
+ * Returns: Analytics data aggregated from trajectory database.
1051
+ * Not: Runtime truth or real-time queue state.
1052
+ */
1053
+ getDataStats(): TrajectoryDataStats {
1054
+ const getCount = (table: string, where?: string) => {
1055
+ const sql = where ? `SELECT COUNT(*) as count FROM ${table} WHERE ${where}` : `SELECT COUNT(*) as count FROM ${table}`;
1056
+ return Number((this.db.prepare(sql).get() as { count: number }).count);
1057
+ };
1058
+ const lastIngest = this.db.prepare(`
1059
+ SELECT MAX(ts) AS ts FROM (
1060
+ SELECT MAX(created_at) AS ts FROM assistant_turns
1061
+ UNION ALL SELECT MAX(created_at) AS ts FROM user_turns
1062
+ UNION ALL SELECT MAX(created_at) AS ts FROM tool_calls
1063
+ UNION ALL SELECT MAX(created_at) AS ts FROM pain_events
1064
+ )
1065
+ `).get() as { ts: string | null };
1066
+ return {
1067
+ dbPath: this.dbPath,
1068
+ dbSizeBytes: fileSizeIfExists(this.dbPath),
1069
+ assistantTurns: getCount('assistant_turns'),
1070
+ userTurns: getCount('user_turns'),
1071
+ toolCalls: getCount('tool_calls'),
1072
+ painEvents: getCount('pain_events'),
1073
+ pendingSamples: getCount('correction_samples', `review_status = 'pending'`),
1074
+ approvedSamples: getCount('correction_samples', `review_status = 'approved'`),
1075
+ blobBytes: this.computeBlobBytes(),
1076
+ lastIngestAt: lastIngest.ts ?? null,
1077
+ };
1078
+ }
1079
+
1080
+ cleanupBlobStorage(): { removedFiles: number; reclaimedBytes: number } {
1081
+ return this.pruneUnreferencedBlobs();
1082
+ }
1083
+
1084
+ private initSchema(): void {
1085
+ this.db.exec(`
1086
+ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
1087
+ CREATE TABLE IF NOT EXISTS ingest_checkpoint (
1088
+ source_key TEXT PRIMARY KEY,
1089
+ imported_at TEXT NOT NULL
1090
+ );
1091
+ CREATE TABLE IF NOT EXISTS sessions (
1092
+ session_id TEXT PRIMARY KEY,
1093
+ started_at TEXT NOT NULL,
1094
+ updated_at TEXT NOT NULL
1095
+ );
1096
+ CREATE TABLE IF NOT EXISTS assistant_turns (
1097
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1098
+ session_id TEXT NOT NULL,
1099
+ run_id TEXT NOT NULL,
1100
+ provider TEXT NOT NULL,
1101
+ model TEXT NOT NULL,
1102
+ raw_text TEXT,
1103
+ sanitized_text TEXT NOT NULL,
1104
+ usage_json TEXT NOT NULL,
1105
+ empathy_signal_json TEXT NOT NULL,
1106
+ blob_ref TEXT,
1107
+ raw_excerpt TEXT,
1108
+ created_at TEXT NOT NULL
1109
+ );
1110
+ CREATE TABLE IF NOT EXISTS user_turns (
1111
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1112
+ session_id TEXT NOT NULL,
1113
+ turn_index INTEGER NOT NULL,
1114
+ raw_text TEXT,
1115
+ blob_ref TEXT,
1116
+ raw_excerpt TEXT,
1117
+ correction_detected INTEGER NOT NULL DEFAULT 0,
1118
+ correction_cue TEXT,
1119
+ references_assistant_turn_id INTEGER,
1120
+ created_at TEXT NOT NULL
1121
+ );
1122
+ CREATE TABLE IF NOT EXISTS tool_calls (
1123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1124
+ session_id TEXT NOT NULL,
1125
+ tool_name TEXT NOT NULL,
1126
+ outcome TEXT NOT NULL,
1127
+ duration_ms INTEGER,
1128
+ exit_code INTEGER,
1129
+ error_type TEXT,
1130
+ error_message TEXT,
1131
+ gfi_before REAL,
1132
+ gfi_after REAL,
1133
+ params_json TEXT NOT NULL,
1134
+ created_at TEXT NOT NULL
1135
+ );
1136
+ CREATE TABLE IF NOT EXISTS pain_events (
1137
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1138
+ session_id TEXT NOT NULL,
1139
+ source TEXT NOT NULL,
1140
+ score REAL NOT NULL,
1141
+ reason TEXT,
1142
+ severity TEXT,
1143
+ origin TEXT,
1144
+ confidence REAL,
1145
+ text TEXT,
1146
+ created_at TEXT NOT NULL
1147
+ );
1148
+ CREATE TABLE IF NOT EXISTS gate_blocks (
1149
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1150
+ session_id TEXT,
1151
+ tool_name TEXT NOT NULL,
1152
+ file_path TEXT,
1153
+ reason TEXT NOT NULL,
1154
+ plan_status TEXT,
1155
+ created_at TEXT NOT NULL
1156
+ );
1157
+ CREATE TABLE IF NOT EXISTS trust_changes (
1158
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1159
+ session_id TEXT,
1160
+ previous_score REAL NOT NULL,
1161
+ new_score REAL NOT NULL,
1162
+ delta REAL NOT NULL,
1163
+ reason TEXT NOT NULL,
1164
+ created_at TEXT NOT NULL
1165
+ );
1166
+ CREATE TABLE IF NOT EXISTS principle_events (
1167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1168
+ principle_id TEXT,
1169
+ event_type TEXT NOT NULL,
1170
+ payload_json TEXT NOT NULL,
1171
+ created_at TEXT NOT NULL
1172
+ );
1173
+ CREATE TABLE IF NOT EXISTS task_outcomes (
1174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1175
+ session_id TEXT NOT NULL,
1176
+ task_id TEXT,
1177
+ outcome TEXT NOT NULL,
1178
+ summary TEXT,
1179
+ principle_ids_json TEXT NOT NULL,
1180
+ created_at TEXT NOT NULL
1181
+ );
1182
+ CREATE TABLE IF NOT EXISTS correction_samples (
1183
+ sample_id TEXT PRIMARY KEY,
1184
+ session_id TEXT NOT NULL,
1185
+ bad_assistant_turn_id INTEGER NOT NULL,
1186
+ user_correction_turn_id INTEGER NOT NULL,
1187
+ recovery_tool_span_json TEXT NOT NULL,
1188
+ diff_excerpt TEXT NOT NULL,
1189
+ principle_ids_json TEXT NOT NULL,
1190
+ quality_score REAL NOT NULL,
1191
+ review_status TEXT NOT NULL,
1192
+ export_mode TEXT NOT NULL,
1193
+ created_at TEXT NOT NULL,
1194
+ updated_at TEXT NOT NULL
1195
+ );
1196
+ CREATE TABLE IF NOT EXISTS sample_reviews (
1197
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1198
+ sample_id TEXT NOT NULL,
1199
+ review_status TEXT NOT NULL,
1200
+ note TEXT,
1201
+ created_at TEXT NOT NULL
1202
+ );
1203
+ CREATE TABLE IF NOT EXISTS exports_audit (
1204
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1205
+ export_kind TEXT NOT NULL,
1206
+ mode TEXT NOT NULL,
1207
+ approved_only INTEGER NOT NULL,
1208
+ file_path TEXT NOT NULL,
1209
+ row_count INTEGER NOT NULL,
1210
+ created_at TEXT NOT NULL
1211
+ );
1212
+ CREATE TABLE IF NOT EXISTS evolution_tasks (
1213
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1214
+ task_id TEXT UNIQUE NOT NULL,
1215
+ trace_id TEXT NOT NULL,
1216
+ source TEXT NOT NULL,
1217
+ reason TEXT,
1218
+ score INTEGER DEFAULT 0,
1219
+ status TEXT DEFAULT 'pending',
1220
+ enqueued_at TEXT,
1221
+ started_at TEXT,
1222
+ completed_at TEXT,
1223
+ resolution TEXT,
1224
+ created_at TEXT NOT NULL,
1225
+ updated_at TEXT NOT NULL
1226
+ );
1227
+ CREATE TABLE IF NOT EXISTS evolution_events (
1228
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1229
+ trace_id TEXT NOT NULL,
1230
+ task_id TEXT,
1231
+ stage TEXT NOT NULL,
1232
+ level TEXT DEFAULT 'info',
1233
+ message TEXT NOT NULL,
1234
+ summary TEXT,
1235
+ metadata_json TEXT,
1236
+ created_at TEXT NOT NULL
1237
+ );
1238
+ `);
1239
+
1240
+ // Migration: Add text column to pain_events if it doesn't exist (MEM-01)
1241
+ // SQLite doesn't support IF NOT EXISTS for ADD COLUMN, so we use try/catch
1242
+ try {
1243
+ this.db.exec(`ALTER TABLE pain_events ADD COLUMN text TEXT`);
1244
+ } catch (err: unknown) {
1245
+ const message = err instanceof Error ? err.message : String(err);
1246
+ if (!message.includes('duplicate column name') && !message.includes('no column named')) {
1247
+ // Re-throw unexpected errors — silently swallowing migration failures is dangerous
1248
+ throw err;
1249
+ }
1250
+ }
1251
+
1252
+ // Create FTS5 virtual table for pain_events text search (MEM-04)
1253
+ this.db.exec(`
1254
+ CREATE VIRTUAL TABLE IF NOT EXISTS pain_events_fts USING fts5(
1255
+ text,
1256
+ pain_event_id UNINDEXED,
1257
+ tokenize='porter unicode61'
1258
+ )
1259
+ `);
1260
+
1261
+ // V2 migration: Add V2 columns to evolution_tasks if they don't exist
1262
+ // SQLite does not support IF NOT EXISTS for ADD COLUMN, so we must check manually
1263
+ // before each ALTER to avoid "duplicate column name" errors on existing DBs
1264
+ const v2Columns = [
1265
+ { name: 'task_kind', type: 'TEXT' },
1266
+ { name: 'priority', type: 'TEXT' },
1267
+ { name: 'retry_count', type: 'INTEGER' },
1268
+ { name: 'max_retries', type: 'INTEGER' },
1269
+ { name: 'last_error', type: 'TEXT' },
1270
+ { name: 'result_ref', type: 'TEXT' },
1271
+ ];
1272
+ for (const col of v2Columns) {
1273
+ const exists = this.db.prepare(`PRAGMA table_info(evolution_tasks)`).all()
1274
+ .some((row: any) => row.name === col.name);
1275
+ if (!exists) {
1276
+ this.db.exec(`ALTER TABLE evolution_tasks ADD COLUMN ${col.name} ${col.type}`);
1277
+ }
1278
+ }
1279
+
1280
+ this.db.exec(`
1281
+ CREATE VIEW IF NOT EXISTS v_error_clusters AS
1282
+ SELECT tool_name, COALESCE(error_type, 'unknown') AS error_type, COUNT(*) AS occurrences
1283
+ FROM tool_calls
1284
+ WHERE outcome = 'failure'
1285
+ GROUP BY tool_name, COALESCE(error_type, 'unknown')
1286
+ ORDER BY occurrences DESC;
1287
+ CREATE VIEW IF NOT EXISTS v_principle_effectiveness AS
1288
+ SELECT event_type, COUNT(*) AS total
1289
+ FROM principle_events
1290
+ GROUP BY event_type
1291
+ ORDER BY total DESC;
1292
+ CREATE VIEW IF NOT EXISTS v_sample_queue AS
1293
+ SELECT review_status, COUNT(*) AS total
1294
+ FROM correction_samples
1295
+ GROUP BY review_status;
1296
+ CREATE INDEX IF NOT EXISTS idx_assistant_turns_session_id ON assistant_turns(session_id);
1297
+ CREATE INDEX IF NOT EXISTS idx_assistant_turns_created_at ON assistant_turns(created_at);
1298
+ CREATE INDEX IF NOT EXISTS idx_assistant_turns_provider_model ON assistant_turns(provider, model);
1299
+ CREATE INDEX IF NOT EXISTS idx_user_turns_session_id ON user_turns(session_id);
1300
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session_id ON tool_calls(session_id);
1301
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_created_at ON tool_calls(created_at);
1302
+ CREATE INDEX IF NOT EXISTS idx_pain_events_session_id ON pain_events(session_id);
1303
+ CREATE INDEX IF NOT EXISTS idx_correction_samples_review_status ON correction_samples(review_status);
1304
+ CREATE INDEX IF NOT EXISTS idx_evolution_tasks_trace_id ON evolution_tasks(trace_id);
1305
+ CREATE INDEX IF NOT EXISTS idx_evolution_tasks_status ON evolution_tasks(status);
1306
+ CREATE INDEX IF NOT EXISTS idx_evolution_tasks_created_at ON evolution_tasks(created_at);
1307
+ CREATE INDEX IF NOT EXISTS idx_evolution_events_trace_id ON evolution_events(trace_id);
1308
+ CREATE INDEX IF NOT EXISTS idx_evolution_events_created_at ON evolution_events(created_at);
1309
+ `);
1310
+
1311
+ const row = this.db.prepare('SELECT version FROM schema_version LIMIT 1').get() as { version?: number } | undefined;
1312
+ this.migrateSchema(row?.version);
1313
+ if (!row) {
1314
+ this.db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
1315
+ } else if (row.version !== SCHEMA_VERSION) {
1316
+ this.db.prepare('UPDATE schema_version SET version = ?').run(SCHEMA_VERSION);
1317
+ }
1318
+ }
1319
+
1320
+ private importLegacyArtifacts(): void {
1321
+ this.importLegacySessions();
1322
+ this.importLegacyEvents();
1323
+ this.importLegacyEvolution();
1324
+ }
1325
+
1326
+ private migrateSchema(_fromVersion?: number): void {
1327
+ this.db.exec(`
1328
+ DROP VIEW IF EXISTS v_daily_metrics;
1329
+ CREATE VIEW IF NOT EXISTS v_daily_metrics AS
1330
+ WITH tool_daily AS (
1331
+ SELECT
1332
+ substr(created_at, 1, 10) AS day,
1333
+ COUNT(*) AS tool_calls,
1334
+ SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) AS failures
1335
+ FROM tool_calls
1336
+ GROUP BY substr(created_at, 1, 10)
1337
+ ),
1338
+ correction_daily AS (
1339
+ SELECT
1340
+ substr(created_at, 1, 10) AS day,
1341
+ SUM(CASE WHEN correction_detected = 1 THEN 1 ELSE 0 END) AS user_corrections
1342
+ FROM user_turns
1343
+ GROUP BY substr(created_at, 1, 10)
1344
+ )
1345
+ SELECT
1346
+ tool_daily.day AS day,
1347
+ tool_daily.tool_calls AS tool_calls,
1348
+ tool_daily.failures AS failures,
1349
+ COALESCE(correction_daily.user_corrections, 0) AS user_corrections
1350
+ FROM tool_daily
1351
+ LEFT JOIN correction_daily ON correction_daily.day = tool_daily.day;
1352
+ `);
1353
+ }
1354
+
1355
+ /**
1356
+ * Get daily metrics for analytics.
1357
+ *
1358
+ * Returns: Analytics data aggregated from trajectory database.
1359
+ * Not: Runtime truth or real-time queue state.
1360
+ */
1361
+ private dailyMetrics(): DailyMetricRow[] {
1362
+ return this.db.prepare('SELECT * FROM v_daily_metrics ORDER BY day ASC').all() as DailyMetricRow[];
1363
+ }
1364
+
1365
+ private importLegacySessions(): void {
1366
+ const key = 'legacy:sessions';
1367
+ if (this.isImported(key)) return;
1368
+ const sessionDir = resolvePdPath(this.workspaceDir, 'SESSION_DIR');
1369
+ if (!fs.existsSync(sessionDir)) return;
1370
+ for (const file of fs.readdirSync(sessionDir).filter((entry) => entry.endsWith('.json'))) {
1371
+ try {
1372
+ const content = JSON.parse(fs.readFileSync(path.join(sessionDir, file), 'utf8')) as { sessionId?: string; lastActivityAt?: number };
1373
+ if (content.sessionId) {
1374
+ const startedAt = typeof content.lastActivityAt === 'number'
1375
+ ? new Date(content.lastActivityAt).toISOString()
1376
+ : nowIso();
1377
+ this.recordSession({ sessionId: content.sessionId, startedAt });
1378
+ }
1379
+ } catch {
1380
+ // Ignore malformed legacy sessions.
1381
+ }
1382
+ }
1383
+ this.markImported(key);
1384
+ }
1385
+
1386
+ private importLegacyEvents(): void {
1387
+ const key = 'legacy:events';
1388
+ if (this.isImported(key)) return;
1389
+ const eventsPath = path.join(this.stateDir, 'logs', 'events.jsonl');
1390
+ if (!fs.existsSync(eventsPath)) return;
1391
+ const raw = fs.readFileSync(eventsPath, 'utf8').trim();
1392
+ if (!raw) {
1393
+ this.markImported(key);
1394
+ return;
1395
+ }
1396
+ for (const line of raw.split('\n')) {
1397
+ try {
1398
+ const event = JSON.parse(line) as { type?: string; sessionId?: string; data?: Record<string, unknown>; ts?: string };
1399
+ if (event.type === 'pain_signal' && event.sessionId) {
1400
+ this.recordPainEvent({
1401
+ sessionId: event.sessionId,
1402
+ source: String(event.data?.source ?? 'legacy'),
1403
+ score: Number(event.data?.score ?? 0),
1404
+ reason: typeof event.data?.reason === 'string' ? event.data.reason : null,
1405
+ severity: typeof event.data?.severity === 'string' ? event.data.severity : null,
1406
+ origin: typeof event.data?.origin === 'string' ? event.data.origin : null,
1407
+ confidence: typeof event.data?.confidence === 'number' ? event.data.confidence : null,
1408
+ createdAt: event.ts,
1409
+ });
1410
+ }
1411
+ if (event.type === 'trust_change') {
1412
+ this.recordTrustChange({
1413
+ sessionId: event.sessionId,
1414
+ previousScore: Number(event.data?.previousScore ?? 0),
1415
+ newScore: Number(event.data?.newScore ?? 0),
1416
+ delta: Number(event.data?.delta ?? 0),
1417
+ reason: String(event.data?.reason ?? 'legacy'),
1418
+ createdAt: event.ts,
1419
+ });
1420
+ }
1421
+ if (event.type === 'gate_block') {
1422
+ this.recordGateBlock({
1423
+ sessionId: event.sessionId,
1424
+ toolName: String(event.data?.toolName ?? 'unknown'),
1425
+ filePath: typeof event.data?.filePath === 'string' ? event.data.filePath : null,
1426
+ reason: String(event.data?.reason ?? 'legacy'),
1427
+ planStatus: typeof event.data?.planStatus === 'string' ? event.data.planStatus : null,
1428
+ createdAt: event.ts,
1429
+ });
1430
+ }
1431
+ } catch {
1432
+ // Ignore malformed legacy events.
1433
+ }
1434
+ }
1435
+ this.markImported(key);
1436
+ }
1437
+
1438
+ private importLegacyEvolution(): void {
1439
+ const key = 'legacy:evolution';
1440
+ if (this.isImported(key)) return;
1441
+ const evolutionPath = resolvePdPath(this.workspaceDir, 'EVOLUTION_STREAM');
1442
+ if (!fs.existsSync(evolutionPath)) return;
1443
+ const raw = fs.readFileSync(evolutionPath, 'utf8').trim();
1444
+ if (!raw) {
1445
+ this.markImported(key);
1446
+ return;
1447
+ }
1448
+ for (const line of raw.split('\n')) {
1449
+ try {
1450
+ const event = JSON.parse(line) as { type?: string; data?: Record<string, unknown>; ts?: string };
1451
+ this.recordPrincipleEvent({
1452
+ principleId: typeof event.data?.principleId === 'string' ? event.data.principleId : null,
1453
+ eventType: String(event.type ?? 'legacy'),
1454
+ payload: event.data ?? {},
1455
+ createdAt: event.ts,
1456
+ });
1457
+ } catch {
1458
+ // Ignore malformed legacy evolution events.
1459
+ }
1460
+ }
1461
+ this.markImported(key);
1462
+ }
1463
+
1464
+ private markImported(sourceKey: string): void {
1465
+ this.withWrite(() => {
1466
+ this.db.prepare(`
1467
+ INSERT INTO ingest_checkpoint (source_key, imported_at)
1468
+ VALUES (?, ?)
1469
+ ON CONFLICT(source_key) DO UPDATE SET imported_at = excluded.imported_at
1470
+ `).run(sourceKey, nowIso());
1471
+ });
1472
+ }
1473
+
1474
+ private isImported(sourceKey: string): boolean {
1475
+ const row = this.db.prepare('SELECT source_key FROM ingest_checkpoint WHERE source_key = ?').get(sourceKey);
1476
+ return Boolean(row);
1477
+ }
1478
+
1479
+ private maybeCreateCorrectionSample(sessionId: string): void {
1480
+ const pending = this.db.prepare(`
1481
+ SELECT sample_id FROM correction_samples
1482
+ WHERE session_id = ? AND review_status = 'pending'
1483
+ ORDER BY created_at DESC
1484
+ LIMIT 1
1485
+ `).get(sessionId) as { sample_id?: string } | undefined;
1486
+ if (pending?.sample_id) return;
1487
+
1488
+ const correctionTurn = this.db.prepare(`
1489
+ SELECT id, references_assistant_turn_id, correction_cue, raw_text, blob_ref
1490
+ FROM user_turns
1491
+ WHERE session_id = ? AND correction_detected = 1
1492
+ ORDER BY id DESC
1493
+ LIMIT 1
1494
+ `).get(sessionId) as Record<string, unknown> | undefined;
1495
+ if (!correctionTurn || !correctionTurn.references_assistant_turn_id) return;
1496
+
1497
+ const failedCall = this.db.prepare(`
1498
+ SELECT id, tool_name, error_type, error_message
1499
+ FROM tool_calls
1500
+ WHERE session_id = ? AND outcome = 'failure'
1501
+ ORDER BY id DESC
1502
+ LIMIT 1
1503
+ `).get(sessionId) as Record<string, unknown> | undefined;
1504
+ if (!failedCall) return;
1505
+
1506
+ const successfulCalls = this.db.prepare(`
1507
+ SELECT id, tool_name
1508
+ FROM tool_calls
1509
+ WHERE session_id = ? AND outcome = 'success'
1510
+ ORDER BY id DESC
1511
+ LIMIT 3
1512
+ `).all(sessionId) as Array<Record<string, unknown>>;
1513
+ if (successfulCalls.length === 0) return;
1514
+
1515
+ const sampleId = `sample_${crypto.createHash('md5').update(`${sessionId}:${correctionTurn.id}:${successfulCalls[0].id}`).digest('hex').slice(0, 12)}`;
1516
+ const userRawText = this.restoreRawText(correctionTurn.raw_text as string | null, correctionTurn.blob_ref as string | null);
1517
+ const qualityScore = [
1518
+ correctionTurn.references_assistant_turn_id ? 35 : 0,
1519
+ correctionTurn.correction_cue ? 20 : 0,
1520
+ failedCall ? 20 : 0,
1521
+ successfulCalls.length > 0 ? 25 : 0,
1522
+ ].reduce((sum, value) => sum + value, 0);
1523
+
1524
+ this.withWrite(() => {
1525
+ this.db.prepare(`
1526
+ INSERT OR IGNORE INTO correction_samples (
1527
+ sample_id, session_id, bad_assistant_turn_id, user_correction_turn_id,
1528
+ recovery_tool_span_json, diff_excerpt, principle_ids_json, quality_score,
1529
+ review_status, export_mode, created_at, updated_at
1530
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'raw', ?, ?)
1531
+ `).run(
1532
+ sampleId,
1533
+ sessionId,
1534
+ Number(correctionTurn.references_assistant_turn_id),
1535
+ Number(correctionTurn.id),
1536
+ safeJson(successfulCalls.map((call) => ({ id: call.id, toolName: call.tool_name }))),
1537
+ summarizeForDiff(userRawText || String(failedCall.error_message ?? failedCall.error_type ?? failedCall.tool_name)),
1538
+ '[]',
1539
+ qualityScore,
1540
+ nowIso(),
1541
+ nowIso(),
1542
+ );
1543
+ });
1544
+ }
1545
+
1546
+ private recordExportAudit(
1547
+ exportKind: string,
1548
+ mode: CorrectionExportMode,
1549
+ approvedOnly: boolean,
1550
+ filePath: string,
1551
+ rowCount: number,
1552
+ ): void {
1553
+ this.withWrite(() => {
1554
+ this.db.prepare(`
1555
+ INSERT INTO exports_audit (export_kind, mode, approved_only, file_path, row_count, created_at)
1556
+ VALUES (?, ?, ?, ?, ?, ?)
1557
+ `).run(exportKind, mode, approvedOnly ? 1 : 0, filePath, rowCount, nowIso());
1558
+ });
1559
+ }
1560
+
1561
+ private storeRawText(kind: 'assistant' | 'user', text: string): { inlineText: string | null; blobRef: string | null; excerpt: string } {
1562
+ const excerpt = text.length > 200 ? `${text.slice(0, 197)}...` : text;
1563
+ const bytes = Buffer.byteLength(text, 'utf8');
1564
+ if (bytes <= this.blobInlineThresholdBytes) {
1565
+ return { inlineText: text, blobRef: null, excerpt };
1566
+ }
1567
+ const hash = crypto.createHash('sha256').update(text).digest('hex');
1568
+ const relativePath = `${kind}-${hash}.txt`;
1569
+ const fullPath = path.join(this.blobDir, relativePath);
1570
+ if (!fs.existsSync(fullPath)) {
1571
+ fs.writeFileSync(fullPath, text, 'utf8');
1572
+ }
1573
+ return { inlineText: null, blobRef: relativePath, excerpt };
1574
+ }
1575
+
1576
+ private restoreRawText(inlineText: string | null, blobRef: string | null): string {
1577
+ if (inlineText) return inlineText;
1578
+ if (!blobRef) return '';
1579
+ const fullPath = path.join(this.blobDir, blobRef);
1580
+ return fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
1581
+ }
1582
+
1583
+ private computeBlobBytes(): number {
1584
+ if (!fs.existsSync(this.blobDir)) return 0;
1585
+ return fs.readdirSync(this.blobDir).reduce((sum, file) => sum + fileSizeIfExists(path.join(this.blobDir, file)), 0);
1586
+ }
1587
+
1588
+ private pruneUnreferencedBlobs(): { removedFiles: number; reclaimedBytes: number } {
1589
+ if (!fs.existsSync(this.blobDir)) {
1590
+ return { removedFiles: 0, reclaimedBytes: 0 };
1591
+ }
1592
+
1593
+ const referenced = new Set<string>();
1594
+ const rows = this.db.prepare(`
1595
+ SELECT blob_ref FROM assistant_turns WHERE blob_ref IS NOT NULL
1596
+ UNION
1597
+ SELECT blob_ref FROM user_turns WHERE blob_ref IS NOT NULL
1598
+ `).all() as Array<{ blob_ref?: string | null }>;
1599
+ for (const row of rows) {
1600
+ if (row.blob_ref) referenced.add(String(row.blob_ref));
1601
+ }
1602
+
1603
+ const now = Date.now();
1604
+ let removedFiles = 0;
1605
+ let reclaimedBytes = 0;
1606
+
1607
+ for (const entry of fs.readdirSync(this.blobDir)) {
1608
+ if (referenced.has(entry)) continue;
1609
+ const fullPath = path.join(this.blobDir, entry);
1610
+ let stat: fs.Stats;
1611
+ try {
1612
+ stat = fs.statSync(fullPath);
1613
+ } catch {
1614
+ continue;
1615
+ }
1616
+ if (!stat.isFile()) continue;
1617
+ if (this.orphanBlobGraceMs > 0 && now - stat.mtimeMs < this.orphanBlobGraceMs) continue;
1618
+ reclaimedBytes += stat.size;
1619
+ removedFiles += 1;
1620
+ fs.rmSync(fullPath, { force: true });
1621
+ }
1622
+
1623
+ return { removedFiles, reclaimedBytes };
1624
+ }
1625
+
1626
+ private withWrite<T>(fn: () => T): T {
1627
+ return withLock(this.dbPath, fn, { lockSuffix: '.trajectory.lock', lockStaleMs: 30000 });
1628
+ }
1629
+ }
1630
+
1631
+ export class TrajectoryRegistry {
1632
+ private static instances = new Map<string, TrajectoryDatabase>();
1633
+
1634
+ static get(workspaceDir: string, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): TrajectoryDatabase {
1635
+ const normalized = path.resolve(workspaceDir);
1636
+ const existing = this.instances.get(normalized);
1637
+ if (existing) return existing;
1638
+ const created = new TrajectoryDatabase({ workspaceDir: normalized, ...opts });
1639
+ this.instances.set(normalized, created);
1640
+ return created;
1641
+ }
1642
+
1643
+ static dispose(workspaceDir: string): void {
1644
+ const normalized = path.resolve(workspaceDir);
1645
+ const instance = this.instances.get(normalized);
1646
+ if (instance) {
1647
+ instance.dispose();
1648
+ this.instances.delete(normalized);
1649
+ }
1650
+ }
1651
+
1652
+ static clear(): void {
1653
+ for (const instance of this.instances.values()) {
1654
+ instance.dispose();
1655
+ }
1656
+ this.instances.clear();
1657
+ }
1658
+
1659
+ static use<T>(workspaceDir: string, fn: (db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
1660
+ const normalized = path.resolve(workspaceDir);
1661
+ const existing = this.instances.get(normalized);
1662
+ if (existing) {
1663
+ return fn(existing);
1664
+ }
1665
+
1666
+ const transient = new TrajectoryDatabase({ workspaceDir: normalized, ...opts });
1667
+ try {
1668
+ return fn(transient);
1669
+ } finally {
1670
+ transient.dispose();
1671
+ }
1672
+ }
1673
+ }