@winspan/claude-forge 8.50.6 → 8.51.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 (367) hide show
  1. package/CLAUDE.md +7 -7
  2. package/dist/claudemd/claudemd-generator.d.ts.map +1 -1
  3. package/dist/claudemd/claudemd-generator.js +27 -237
  4. package/dist/claudemd/claudemd-generator.js.map +1 -1
  5. package/dist/claudemd/resume-manager.js +1 -1
  6. package/dist/claudemd/resume-manager.js.map +1 -1
  7. package/dist/claudemd/templates/swarm-protocol.md +222 -0
  8. package/dist/cli/commands/daemon.js +6 -6
  9. package/dist/cli/commands/daemon.js.map +1 -1
  10. package/dist/cli/commands/executions.d.ts.map +1 -1
  11. package/dist/cli/commands/executions.js +4 -3
  12. package/dist/cli/commands/executions.js.map +1 -1
  13. package/dist/cli/commands/init.js +2 -2
  14. package/dist/cli/commands/init.js.map +1 -1
  15. package/dist/cli/commands/logs.js.map +1 -1
  16. package/dist/cli/commands/mcp.d.ts.map +1 -1
  17. package/dist/cli/commands/mcp.js +3 -5
  18. package/dist/cli/commands/mcp.js.map +1 -1
  19. package/dist/cli/commands/menu.d.ts.map +1 -1
  20. package/dist/cli/commands/menu.js +4 -3
  21. package/dist/cli/commands/menu.js.map +1 -1
  22. package/dist/cli/commands/stats.d.ts.map +1 -1
  23. package/dist/cli/commands/stats.js +2 -3
  24. package/dist/cli/commands/stats.js.map +1 -1
  25. package/dist/cli/commands/status.js +2 -2
  26. package/dist/cli/commands/status.js.map +1 -1
  27. package/dist/cli/commands/trace.d.ts.map +1 -1
  28. package/dist/cli/commands/trace.js +11 -23
  29. package/dist/cli/commands/trace.js.map +1 -1
  30. package/dist/cli/init/hook-manager.d.ts.map +1 -1
  31. package/dist/cli/init/hook-manager.js +2 -2
  32. package/dist/cli/init/hook-manager.js.map +1 -1
  33. package/dist/core/ai/provider.js +2 -2
  34. package/dist/core/ai/provider.js.map +1 -1
  35. package/dist/core/constants.d.ts +12 -1
  36. package/dist/core/constants.d.ts.map +1 -1
  37. package/dist/core/constants.js +15 -1
  38. package/dist/core/constants.js.map +1 -1
  39. package/dist/core/event-fields.d.ts +16 -0
  40. package/dist/core/event-fields.d.ts.map +1 -0
  41. package/dist/core/event-fields.js +19 -0
  42. package/dist/core/event-fields.js.map +1 -0
  43. package/dist/core/queue/index.d.ts.map +1 -1
  44. package/dist/core/queue/index.js +3 -4
  45. package/dist/core/queue/index.js.map +1 -1
  46. package/dist/core/storage/base.d.ts +36 -3
  47. package/dist/core/storage/base.d.ts.map +1 -1
  48. package/dist/core/storage/base.js +101 -58
  49. package/dist/core/storage/base.js.map +1 -1
  50. package/dist/core/storage/events.d.ts +92 -3
  51. package/dist/core/storage/events.d.ts.map +1 -1
  52. package/dist/core/storage/events.js +147 -0
  53. package/dist/core/storage/events.js.map +1 -1
  54. package/dist/core/storage/routing.d.ts +54 -1
  55. package/dist/core/storage/routing.d.ts.map +1 -1
  56. package/dist/core/storage/routing.js +99 -1
  57. package/dist/core/storage/routing.js.map +1 -1
  58. package/dist/core/storage/schema.sql +12 -2
  59. package/dist/core/storage/sessions.d.ts +20 -0
  60. package/dist/core/storage/sessions.d.ts.map +1 -1
  61. package/dist/core/storage/sessions.js +59 -0
  62. package/dist/core/storage/sessions.js.map +1 -1
  63. package/dist/core/storage/skills.d.ts +23 -0
  64. package/dist/core/storage/skills.d.ts.map +1 -1
  65. package/dist/core/storage/skills.js +47 -0
  66. package/dist/core/storage/skills.js.map +1 -1
  67. package/dist/core/storage/sqlite.d.ts +35 -2
  68. package/dist/core/storage/sqlite.d.ts.map +1 -1
  69. package/dist/core/storage/sqlite.js +93 -4
  70. package/dist/core/storage/sqlite.js.map +1 -1
  71. package/dist/core/storage/tasks.d.ts +49 -0
  72. package/dist/core/storage/tasks.d.ts.map +1 -1
  73. package/dist/core/storage/tasks.js +143 -1
  74. package/dist/core/storage/tasks.js.map +1 -1
  75. package/dist/core/storage/token-usage.d.ts +1 -1
  76. package/dist/core/storage/token-usage.d.ts.map +1 -1
  77. package/dist/core/storage/token-usage.js +1 -1
  78. package/dist/core/storage/token-usage.js.map +1 -1
  79. package/dist/core/types.d.ts +24 -3
  80. package/dist/core/types.d.ts.map +1 -1
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/core/utils/error-handler.d.ts.map +1 -1
  83. package/dist/core/utils/error-handler.js +3 -2
  84. package/dist/core/utils/error-handler.js.map +1 -1
  85. package/dist/core/utils/git.d.ts +10 -0
  86. package/dist/core/utils/git.d.ts.map +1 -0
  87. package/dist/core/utils/git.js +24 -0
  88. package/dist/core/utils/git.js.map +1 -0
  89. package/dist/core/utils/logger.d.ts.map +1 -1
  90. package/dist/core/utils/logger.js +15 -1
  91. package/dist/core/utils/logger.js.map +1 -1
  92. package/dist/core/utils/lru-cache.d.ts +1 -0
  93. package/dist/core/utils/lru-cache.d.ts.map +1 -1
  94. package/dist/core/utils/lru-cache.js +3 -0
  95. package/dist/core/utils/lru-cache.js.map +1 -1
  96. package/dist/core/utils/token-tracker.js +1 -1
  97. package/dist/core/utils/token-tracker.js.map +1 -1
  98. package/dist/daemon/event-parser.d.ts.map +1 -1
  99. package/dist/daemon/event-parser.js +2 -1
  100. package/dist/daemon/event-parser.js.map +1 -1
  101. package/dist/daemon/handlers/history-exporter.js.map +1 -1
  102. package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
  103. package/dist/daemon/handlers/post-tool-use.js +7 -3
  104. package/dist/daemon/handlers/post-tool-use.js.map +1 -1
  105. package/dist/daemon/handlers/stop.d.ts +4 -0
  106. package/dist/daemon/handlers/stop.d.ts.map +1 -1
  107. package/dist/daemon/handlers/stop.js +23 -35
  108. package/dist/daemon/handlers/stop.js.map +1 -1
  109. package/dist/daemon/handlers/user-prompt.d.ts +3 -3
  110. package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
  111. package/dist/daemon/handlers/user-prompt.js +12 -22
  112. package/dist/daemon/handlers/user-prompt.js.map +1 -1
  113. package/dist/daemon/hook-sync.d.ts +17 -0
  114. package/dist/daemon/hook-sync.d.ts.map +1 -0
  115. package/dist/daemon/hook-sync.js +74 -0
  116. package/dist/daemon/hook-sync.js.map +1 -0
  117. package/dist/daemon/index.d.ts.map +1 -1
  118. package/dist/daemon/index.js +33 -9
  119. package/dist/daemon/index.js.map +1 -1
  120. package/dist/daemon/lifecycle.js +3 -4
  121. package/dist/daemon/lifecycle.js.map +1 -1
  122. package/dist/daemon/server.d.ts +6 -4
  123. package/dist/daemon/server.d.ts.map +1 -1
  124. package/dist/daemon/server.js +76 -85
  125. package/dist/daemon/server.js.map +1 -1
  126. package/dist/daemon/services/task-segmenter.js +1 -1
  127. package/dist/daemon/services/task-segmenter.js.map +1 -1
  128. package/dist/hooks/hook-lib.sh +37 -0
  129. package/dist/hooks/notification.sh +2 -2
  130. package/dist/hooks/post-tool-use.sh +2 -2
  131. package/dist/hooks/pre-tool-use.sh +2 -2
  132. package/dist/hooks/stop.sh +9 -6
  133. package/dist/hooks/user-prompt-submit.sh +2 -2
  134. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
  135. package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
  136. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
  137. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
  138. package/dist/web/analytics/drift-detector.d.ts.map +1 -0
  139. package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
  140. package/dist/web/analytics/drift-detector.js.map +1 -0
  141. package/dist/web/analytics/weekly-report.d.ts.map +1 -0
  142. package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
  143. package/dist/web/analytics/weekly-report.js.map +1 -0
  144. package/dist/web/auth-middleware.d.ts.map +1 -1
  145. package/dist/web/auth-middleware.js +1 -2
  146. package/dist/web/auth-middleware.js.map +1 -1
  147. package/dist/web/routes/_helpers.d.ts +16 -0
  148. package/dist/web/routes/_helpers.d.ts.map +1 -0
  149. package/dist/web/routes/_helpers.js +32 -0
  150. package/dist/web/routes/_helpers.js.map +1 -0
  151. package/dist/web/routes/drift.js +1 -1
  152. package/dist/web/routes/drift.js.map +1 -1
  153. package/dist/web/routes/insights.js +1 -1
  154. package/dist/web/routes/insights.js.map +1 -1
  155. package/dist/web/routes/reports.js +1 -1
  156. package/dist/web/routes/reports.js.map +1 -1
  157. package/dist/web/routes/rules.d.ts +3 -0
  158. package/dist/web/routes/rules.d.ts.map +1 -1
  159. package/dist/web/routes/rules.js +28 -52
  160. package/dist/web/routes/rules.js.map +1 -1
  161. package/dist/web/routes/sessions.d.ts.map +1 -1
  162. package/dist/web/routes/sessions.js +16 -30
  163. package/dist/web/routes/sessions.js.map +1 -1
  164. package/dist/web/routes/skill-stats.d.ts +2 -0
  165. package/dist/web/routes/skill-stats.d.ts.map +1 -1
  166. package/dist/web/routes/skill-stats.js +28 -64
  167. package/dist/web/routes/skill-stats.js.map +1 -1
  168. package/dist/web/routes/skills.d.ts.map +1 -1
  169. package/dist/web/routes/skills.js +5 -4
  170. package/dist/web/routes/skills.js.map +1 -1
  171. package/dist/web/routes/stats.d.ts +4 -0
  172. package/dist/web/routes/stats.d.ts.map +1 -1
  173. package/dist/web/routes/stats.js +19 -21
  174. package/dist/web/routes/stats.js.map +1 -1
  175. package/dist/web/routes/tasks.d.ts.map +1 -1
  176. package/dist/web/routes/tasks.js +17 -42
  177. package/dist/web/routes/tasks.js.map +1 -1
  178. package/dist/web/routes/trace.d.ts.map +1 -1
  179. package/dist/web/routes/trace.js +7 -17
  180. package/dist/web/routes/trace.js.map +1 -1
  181. package/dist/web/routes/types.d.ts.map +1 -1
  182. package/dist/web/routes/types.js +4 -3
  183. package/dist/web/routes/types.js.map +1 -1
  184. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
  185. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
  186. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
  187. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
  188. package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
  189. package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
  190. package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
  191. package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
  192. package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
  193. package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
  194. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
  195. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
  196. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
  197. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
  198. package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
  199. package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
  200. package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
  201. package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
  202. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
  203. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
  204. package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
  205. package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
  206. package/dist/web/static/assets/index-CBX47X8l.js +3 -0
  207. package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
  208. package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
  209. package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
  210. package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
  211. package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
  212. package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
  213. package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
  214. package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
  215. package/dist/web/static/index.html +4 -4
  216. package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
  217. package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
  218. package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
  219. package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
  220. package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
  221. package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
  222. package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
  223. package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
  224. package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
  225. package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
  226. package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
  227. package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
  228. package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
  229. package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
  230. package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
  231. package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
  232. package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
  233. package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
  234. package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
  235. package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
  236. package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
  237. package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
  238. package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
  239. package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
  240. package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
  241. package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
  242. package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
  243. package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
  244. package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
  245. package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
  246. package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
  247. package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
  248. package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
  249. package/docs/reviews/task-title-summary.md +92 -0
  250. package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
  251. package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
  252. package/package.json +2 -2
  253. package/src/claudemd/claudemd-generator.ts +29 -238
  254. package/src/claudemd/resume-manager.ts +1 -1
  255. package/src/claudemd/templates/swarm-protocol.md +222 -0
  256. package/src/cli/commands/daemon.ts +6 -6
  257. package/src/cli/commands/executions.ts +4 -3
  258. package/src/cli/commands/init.ts +2 -2
  259. package/src/cli/commands/logs.ts +1 -1
  260. package/src/cli/commands/mcp.ts +3 -5
  261. package/src/cli/commands/menu.ts +4 -3
  262. package/src/cli/commands/stats.ts +2 -3
  263. package/src/cli/commands/status.ts +2 -2
  264. package/src/cli/commands/trace.ts +10 -26
  265. package/src/cli/init/hook-manager.ts +2 -2
  266. package/src/core/ai/provider.ts +2 -2
  267. package/src/core/constants.ts +18 -1
  268. package/src/core/event-fields.ts +32 -0
  269. package/src/core/queue/index.ts +3 -4
  270. package/src/core/storage/base.ts +132 -56
  271. package/src/core/storage/events.ts +183 -4
  272. package/src/core/storage/routing.ts +129 -1
  273. package/src/core/storage/schema.sql +12 -2
  274. package/src/core/storage/sessions.ts +64 -0
  275. package/src/core/storage/skills.ts +69 -0
  276. package/src/core/storage/sqlite.ts +103 -4
  277. package/src/core/storage/tasks.ts +149 -1
  278. package/src/core/storage/token-usage.ts +1 -1
  279. package/src/core/types.ts +30 -3
  280. package/src/core/utils/error-handler.ts +3 -2
  281. package/src/core/utils/git.ts +23 -0
  282. package/src/core/utils/logger.ts +16 -1
  283. package/src/core/utils/lru-cache.ts +4 -0
  284. package/src/core/utils/token-tracker.ts +1 -1
  285. package/src/daemon/event-parser.ts +4 -3
  286. package/src/daemon/handlers/history-exporter.ts +1 -1
  287. package/src/daemon/handlers/post-tool-use.ts +7 -3
  288. package/src/daemon/handlers/stop.ts +32 -39
  289. package/src/daemon/handlers/user-prompt.ts +12 -22
  290. package/src/daemon/hook-sync.ts +91 -0
  291. package/src/daemon/index.ts +34 -10
  292. package/src/daemon/lifecycle.ts +3 -3
  293. package/src/daemon/server.ts +76 -89
  294. package/src/daemon/services/task-segmenter.ts +1 -1
  295. package/src/hooks/hook-lib.sh +37 -0
  296. package/src/hooks/notification.sh +2 -2
  297. package/src/hooks/post-tool-use.sh +2 -2
  298. package/src/hooks/pre-tool-use.sh +2 -2
  299. package/src/hooks/stop.sh +9 -6
  300. package/src/hooks/user-prompt-submit.sh +2 -2
  301. package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
  302. package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
  303. package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
  304. package/src/web/auth-middleware.ts +1 -2
  305. package/src/web/routes/_helpers.ts +34 -0
  306. package/src/web/routes/drift.ts +1 -1
  307. package/src/web/routes/insights.ts +1 -1
  308. package/src/web/routes/reports.ts +1 -1
  309. package/src/web/routes/rules.ts +31 -56
  310. package/src/web/routes/sessions.ts +18 -30
  311. package/src/web/routes/skill-stats.ts +29 -69
  312. package/src/web/routes/skills.ts +5 -4
  313. package/src/web/routes/stats.ts +19 -29
  314. package/src/web/routes/tasks.ts +17 -42
  315. package/src/web/routes/trace.ts +7 -19
  316. package/src/web/routes/types.ts +4 -3
  317. package/tests/integration/claudemd-generator.test.ts +90 -0
  318. package/tests/integration/web-analytics.integration.test.ts +133 -0
  319. package/tests/integration/web-stats.integration.test.ts +135 -0
  320. package/tests/integration/web-trace.integration.test.ts +175 -0
  321. package/tests/unit/core/forge-paths.test.ts +99 -0
  322. package/tests/unit/daemon/hook-sync.test.ts +71 -0
  323. package/tests/unit/daemon/post-tool-use.test.ts +121 -0
  324. package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
  325. package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
  326. package/tests/unit/event-fields.test.ts +88 -0
  327. package/tests/unit/event-parser.test.ts +55 -0
  328. package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
  329. package/tests/unit/socket-server.test.ts +183 -0
  330. package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
  331. package/tests/unit/storage/migration-idempotent.test.ts +304 -0
  332. package/tests/unit/storage/routing-aggregates.test.ts +276 -0
  333. package/tests/unit/storage/routing.test.ts +117 -0
  334. package/tests/unit/storage/schema-missing.test.ts +81 -0
  335. package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
  336. package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
  337. package/tests/unit/storage/skills-aggregates.test.ts +104 -0
  338. package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
  339. package/tests/unit/storage/task-operations-counts.test.ts +46 -0
  340. package/tests/unit/storage/tasks-getById.test.ts +343 -0
  341. package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
  342. package/tests/unit/token-usage.test.ts +6 -6
  343. package/tests/unit/web/navigation-back-contract.test.ts +134 -0
  344. package/tests/unit/web/routes-rules.test.ts +182 -0
  345. package/tests/unit/web/routes-tasks.test.ts +34 -0
  346. package/tests/unit/web/task-title-contract.test.ts +210 -0
  347. package/tests/unit/web/tasks-component-contract.test.ts +179 -0
  348. package/vitest.config.ts +1 -1
  349. package/web/src/pages/TaskDetail.tsx +9 -5
  350. package/web/src/pages/Tasks.tsx +315 -50
  351. package/web/src/utils/navigation.ts +25 -0
  352. package/web/src/utils/task-title.ts +49 -0
  353. package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
  354. package/dist/daemon/services/drift-detector.d.ts.map +0 -1
  355. package/dist/daemon/services/drift-detector.js.map +0 -1
  356. package/dist/daemon/services/weekly-report.d.ts.map +0 -1
  357. package/dist/daemon/services/weekly-report.js.map +0 -1
  358. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
  359. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
  360. package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
  361. package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
  362. package/dist/web/static/assets/index-D8AKj26b.css +0 -1
  363. package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
  364. package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
  365. package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
  366. /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
  367. /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
@@ -16,10 +16,6 @@ import { z } from 'zod';
16
16
  import type { ForgeEvent } from '../types.js';
17
17
  import { logger } from '../utils/logger.js';
18
18
 
19
- interface UpsertCapableEmitter extends EventEmitter {
20
- upsertSession(event: ForgeEvent): void;
21
- }
22
-
23
19
  // Zod schema for runtime validation
24
20
  const ForgeEventSchema = z.object({
25
21
  event_id: z.string().uuid().optional(),
@@ -218,6 +214,189 @@ export class EventOperations {
218
214
  return rows.map((row) => this.rowToEvent(row));
219
215
  }
220
216
 
217
+ // ── H2: 聚合 / 计数方法(消除 routes / handlers / analytics 越权 SQL)─────
218
+
219
+ /** Total event row count. */
220
+ countAllEvents(): number {
221
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM events').get() as { cnt: number };
222
+ return row.cnt;
223
+ }
224
+
225
+ /**
226
+ * Tool usage distribution (tool_name → count).
227
+ *
228
+ * - `since`: ISO timestamp, inclusive lower bound on `timestamp`. Omit for all-time.
229
+ * - `hook_type`: filter on hook_type (e.g. 'PreToolUse') — used by weekly-report.
230
+ * - `limit`: max distinct tools returned (default 100).
231
+ */
232
+ aggregateToolUsage(opts: {
233
+ since?: string;
234
+ limit?: number;
235
+ hook_type?: 'PreToolUse' | 'PostToolUse';
236
+ } = {}): Array<{ tool_name: string; count: number }> {
237
+ const conditions: string[] = [`tool_name IS NOT NULL`, `tool_name != ''`];
238
+ const params: unknown[] = [];
239
+ if (opts.since !== undefined) {
240
+ conditions.push('timestamp >= ?');
241
+ params.push(opts.since);
242
+ }
243
+ if (opts.hook_type) {
244
+ conditions.push('hook_type = ?');
245
+ params.push(opts.hook_type);
246
+ }
247
+ const limit = opts.limit ?? 100;
248
+ const sql = `SELECT tool_name, COUNT(*) as count FROM events
249
+ WHERE ${conditions.join(' AND ')}
250
+ GROUP BY tool_name ORDER BY count DESC LIMIT ?`;
251
+ params.push(limit);
252
+ return this.db.prepare(sql).all(...params) as Array<{ tool_name: string; count: number }>;
253
+ }
254
+
255
+ /** Per-day event counts (date(timestamp) → count) within [since, until). */
256
+ aggregateDailyEventCounts(opts: { since: string; until?: string }): Array<{ date: string; count: number }> {
257
+ const conditions: string[] = ['timestamp >= ?'];
258
+ const params: unknown[] = [opts.since];
259
+ if (opts.until !== undefined) {
260
+ conditions.push('timestamp < ?');
261
+ params.push(opts.until);
262
+ }
263
+ const sql = `SELECT date(timestamp) as date, COUNT(*) as count FROM events
264
+ WHERE ${conditions.join(' AND ')}
265
+ GROUP BY date ORDER BY date`;
266
+ return this.db.prepare(sql).all(...params) as Array<{ date: string; count: number }>;
267
+ }
268
+
269
+ /** Hook type distribution for a single session. */
270
+ aggregateHookTypeBySession(session_id: string): Array<{ hook_type: string; count: number }> {
271
+ return this.db.prepare(
272
+ `SELECT hook_type, COUNT(*) as count FROM events
273
+ WHERE session_id = ? GROUP BY hook_type`
274
+ ).all(session_id) as Array<{ hook_type: string; count: number }>;
275
+ }
276
+
277
+ /** Agent/Task subagent_type distribution for a single session (json_extract from tool_input). */
278
+ aggregateAgentTypeBySession(session_id: string): Array<{ agent_type: string | null; count: number }> {
279
+ return this.db.prepare(
280
+ `SELECT json_extract(tool_input, '$.subagent_type') as agent_type, COUNT(*) as count
281
+ FROM events
282
+ WHERE session_id = ? AND tool_name IN ('Agent', 'Task') AND tool_input IS NOT NULL
283
+ GROUP BY agent_type`
284
+ ).all(session_id) as Array<{ agent_type: string | null; count: number }>;
285
+ }
286
+
287
+ /** Tool usage distribution for a single session (non-null tool_name only). */
288
+ aggregateToolUsageBySession(session_id: string): Array<{ tool_name: string; count: number }> {
289
+ return this.db.prepare(
290
+ `SELECT tool_name, COUNT(*) as count FROM events
291
+ WHERE session_id = ? AND tool_name IS NOT NULL
292
+ GROUP BY tool_name ORDER BY count DESC`
293
+ ).all(session_id) as Array<{ tool_name: string; count: number }>;
294
+ }
295
+
296
+ /** Count of distinct active days (date(timestamp)) in [since, until). */
297
+ countActiveDays(opts: { since: string; until?: string }): number {
298
+ const conditions: string[] = ['timestamp >= ?'];
299
+ const params: unknown[] = [opts.since];
300
+ if (opts.until !== undefined) {
301
+ conditions.push('timestamp < ?');
302
+ params.push(opts.until);
303
+ }
304
+ const sql = `SELECT COUNT(DISTINCT date(timestamp)) as cnt FROM events
305
+ WHERE ${conditions.join(' AND ')}`;
306
+ const row = this.db.prepare(sql).get(...params) as { cnt: number } | undefined;
307
+ return row?.cnt ?? 0;
308
+ }
309
+
310
+ /** Composite overview within [since, until): event count + distinct session count + distinct day count. */
311
+ aggregateOverviewByRange(opts: { since: string; until: string }): {
312
+ event_count: number;
313
+ session_count: number;
314
+ day_count: number;
315
+ } {
316
+ const row = this.db.prepare(
317
+ `SELECT COUNT(*) as cnt, COUNT(DISTINCT session_id) as sessionCount,
318
+ COUNT(DISTINCT date(timestamp)) as dayCount
319
+ FROM events
320
+ WHERE timestamp >= ? AND timestamp < ?`
321
+ ).get(opts.since, opts.until) as { cnt: number; sessionCount: number; dayCount: number } | undefined;
322
+ return {
323
+ event_count: row?.cnt ?? 0,
324
+ session_count: row?.sessionCount ?? 0,
325
+ day_count: row?.dayCount ?? 0,
326
+ };
327
+ }
328
+
329
+ /** Distinct, non-empty project_path values within [since, until). */
330
+ queryDistinctProjects(opts: { since: string; until: string }): string[] {
331
+ const rows = this.db.prepare(
332
+ `SELECT DISTINCT project_path FROM events
333
+ WHERE timestamp >= ? AND timestamp < ?
334
+ AND project_path IS NOT NULL AND project_path != ''`
335
+ ).all(opts.since, opts.until) as Array<{ project_path: string }>;
336
+ return rows.map(r => r.project_path);
337
+ }
338
+
339
+ /**
340
+ * PostToolUse failure rate within [since, until).
341
+ *
342
+ * Failure detection uses three LIKE branches on tool_output (preserved from
343
+ * weekly-report.ts original SQL):
344
+ * - '%"error"%'
345
+ * - '%"is_error":true%'
346
+ * - '%"isError":true%'
347
+ */
348
+ aggregateToolFailureRate(opts: { since: string; until: string }): { post_total: number; failed: number } {
349
+ const totalRow = this.db.prepare(
350
+ `SELECT COUNT(*) as total FROM events
351
+ WHERE timestamp >= ? AND timestamp < ?
352
+ AND hook_type = 'PostToolUse'`
353
+ ).get(opts.since, opts.until) as { total: number } | undefined;
354
+
355
+ const failRow = this.db.prepare(
356
+ `SELECT COUNT(*) as failed FROM events
357
+ WHERE timestamp >= ? AND timestamp < ?
358
+ AND hook_type = 'PostToolUse'
359
+ AND tool_output IS NOT NULL
360
+ AND (tool_output LIKE '%"error"%'
361
+ OR tool_output LIKE '%"is_error":true%'
362
+ OR tool_output LIKE '%"isError":true%')`
363
+ ).get(opts.since, opts.until) as { failed: number } | undefined;
364
+
365
+ return {
366
+ post_total: totalRow?.total ?? 0,
367
+ failed: failRow?.failed ?? 0,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Raw tool_input JSON strings for file-edit tools within [since, until).
373
+ * Caller is responsible for JSON.parse / file_path extraction.
374
+ * Internal IN-list expansion of `tool_names`.
375
+ */
376
+ queryFileEditInputs(opts: { since: string; until: string; tool_names: string[] }): Array<{ tool_input: string }> {
377
+ if (opts.tool_names.length === 0) return [];
378
+ const placeholders = opts.tool_names.map(() => '?').join(',');
379
+ const sql = `SELECT tool_input FROM events
380
+ WHERE timestamp >= ? AND timestamp < ?
381
+ AND hook_type = 'PreToolUse'
382
+ AND tool_name IN (${placeholders})
383
+ AND tool_input IS NOT NULL`;
384
+ return this.db.prepare(sql).all(opts.since, opts.until, ...opts.tool_names) as Array<{ tool_input: string }>;
385
+ }
386
+
387
+ /** Full events in [since, until?), ascending by timestamp (used by anti-pattern-detector). */
388
+ queryEventsByTimeRange(opts: { since: string; until?: string }): ForgeEvent[] {
389
+ const conditions: string[] = ['timestamp >= ?'];
390
+ const params: unknown[] = [opts.since];
391
+ if (opts.until !== undefined) {
392
+ conditions.push('timestamp < ?');
393
+ params.push(opts.until);
394
+ }
395
+ const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY timestamp ASC`;
396
+ const rows = this.db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
397
+ return rows.map(r => this.rowToEvent(r));
398
+ }
399
+
221
400
  rowToEvent(row: Record<string, unknown>): ForgeEvent {
222
401
  // Runtime validation of database row
223
402
  try {
@@ -127,7 +127,7 @@ export class RoutingOperations {
127
127
  }
128
128
 
129
129
  /** Pending events with NULL obeyed within the last N seconds; used by daemon restart recovery. */
130
- findPendingRoutingEvents(ageMs: number): RoutingEventRow[] {
130
+ queryPendingRoutingEvents(ageMs: number): RoutingEventRow[] {
131
131
  const cutoff = Date.now() - ageMs;
132
132
  return this.db.prepare(
133
133
  `SELECT * FROM routing_events WHERE obeyed IS NULL AND ts >= ? ORDER BY ts DESC`
@@ -145,6 +145,30 @@ export class RoutingOperations {
145
145
  return row ?? null;
146
146
  }
147
147
 
148
+ /**
149
+ * Get the most recent PENDING routing_event for a session (obeyed IS NULL).
150
+ *
151
+ * Used by PostToolUseHandler to associate Agent invocations with the
152
+ * still-unfilled routing_event from the current user prompt. Returning only
153
+ * pending rows prevents the handler from re-overwriting an already-completed
154
+ * event (e.g. when 2+ Agents fire within the same prompt, or when an old
155
+ * obeyed=1 row leaks past a missed UserPromptSubmit).
156
+ *
157
+ * Trade-off: in the "same-prompt multi-Agent" case, the 2nd+ Agent will not
158
+ * find a pending row and will be silently skipped. Recording every Agent
159
+ * call is M7 out-of-scope (would require INSERT in PostToolUse rather than
160
+ * UPDATE).
161
+ */
162
+ getRecentPendingRoutingEvent(sessionId: string): { id: number } | null {
163
+ const row = this.db.prepare(`
164
+ SELECT id FROM routing_events
165
+ WHERE session_id = ? AND obeyed IS NULL
166
+ ORDER BY ts DESC
167
+ LIMIT 1
168
+ `).get(sessionId) as { id: number } | undefined;
169
+ return row ?? null;
170
+ }
171
+
148
172
  // ── Experiment Assignments ──────────────────────────────────────────────
149
173
 
150
174
  getExperimentAssignment(sessionId: string, experimentId: string): string | null {
@@ -161,6 +185,110 @@ export class RoutingOperations {
161
185
  ).run(sessionId, experimentId, groupId);
162
186
  }
163
187
 
188
+ // ── Aggregates (H1: SQL-side GROUP BY for routes/rules/skill-stats) ───
189
+ //
190
+ // 这些 aggregate* 方法把原本 routes 层做的"全表 SELECT + JS reduce"下沉到
191
+ // SQL 端,避免单进程内存放大。命名约定(spec H1 固化):
192
+ // - aggregate* —— SQL 端 GROUP BY / COUNT,返回小结果
193
+ // - query* —— 原始行返回(保留旧 API)
194
+ //
195
+ // 注意:所有方法都用 `ts > 0` 兜底,防止 strftime 在 ts<=0 时返回 NULL。
196
+
197
+ /**
198
+ * 一次性返回 rules + skill-stats 共用的 routing 汇总(4 个独立聚合,
199
+ * 各走自己的索引)。不走 UNION ALL(SQLite 无 GROUPING SETS)。
200
+ */
201
+ aggregateRoutingStats(filter: { since_ts: number; project_path?: string }): {
202
+ total: number;
203
+ obeyed: number;
204
+ refused: number;
205
+ unknown: number;
206
+ by_type: Array<{ type: 'agent' | 'skill' | 'none'; count: number }>;
207
+ by_agent: Array<{ agent: string; count: number }>;
208
+ by_skill_routed: Array<{ skill: string; count: number }>;
209
+ } {
210
+ const projectCond = filter.project_path ? 'AND project_path = @project' : '';
211
+ const params = {
212
+ since: filter.since_ts,
213
+ project: filter.project_path ?? null,
214
+ };
215
+
216
+ const totalRow = this.db.prepare(`
217
+ SELECT COUNT(*) AS total,
218
+ SUM(CASE WHEN obeyed=1 THEN 1 ELSE 0 END) AS obeyed,
219
+ SUM(CASE WHEN obeyed=0 THEN 1 ELSE 0 END) AS refused,
220
+ SUM(CASE WHEN obeyed IS NULL THEN 1 ELSE 0 END) AS unknown
221
+ FROM routing_events
222
+ WHERE ts > 0 AND ts >= @since ${projectCond}
223
+ `).get(params) as {
224
+ total: number | null;
225
+ obeyed: number | null;
226
+ refused: number | null;
227
+ unknown: number | null;
228
+ };
229
+
230
+ const byTypeRows = this.db.prepare(`
231
+ SELECT COALESCE(routed_to_type, 'none') AS type, COUNT(*) AS count
232
+ FROM routing_events
233
+ WHERE ts > 0 AND ts >= @since ${projectCond}
234
+ GROUP BY COALESCE(routed_to_type, 'none')
235
+ ORDER BY count DESC
236
+ `).all(params) as Array<{ type: 'agent' | 'skill' | 'none'; count: number }>;
237
+
238
+ const byAgentRows = this.db.prepare(`
239
+ SELECT routed_to_name AS agent, COUNT(*) AS count
240
+ FROM routing_events
241
+ WHERE ts > 0 AND ts >= @since AND obeyed = 1 AND routed_to_name IS NOT NULL ${projectCond}
242
+ GROUP BY routed_to_name
243
+ ORDER BY count DESC
244
+ `).all(params) as Array<{ agent: string; count: number }>;
245
+
246
+ const bySkillRoutedRows = this.db.prepare(`
247
+ SELECT routed_to_name AS skill, COUNT(*) AS count
248
+ FROM routing_events
249
+ WHERE ts > 0 AND ts >= @since AND routed_to_type = 'skill' AND routed_to_name IS NOT NULL ${projectCond}
250
+ GROUP BY routed_to_name
251
+ ORDER BY count DESC
252
+ `).all(params) as Array<{ skill: string; count: number }>;
253
+
254
+ return {
255
+ total: totalRow.total ?? 0,
256
+ obeyed: totalRow.obeyed ?? 0,
257
+ refused: totalRow.refused ?? 0,
258
+ unknown: totalRow.unknown ?? 0,
259
+ by_type: byTypeRows,
260
+ by_agent: byAgentRows,
261
+ by_skill_routed: bySkillRoutedRows,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * 按天聚合 routing 事件,用于 skill-stats trend chart。
267
+ * 时区固定 UTC(strftime + unixepoch),与旧实现的 `new Date(ts).toISOString().split('T')[0]` 一致。
268
+ */
269
+ aggregateRoutingTrendByDay(filter: { since_ts: number }): Array<{
270
+ day: string;
271
+ total: number;
272
+ skill: number;
273
+ }> {
274
+ const rows = this.db.prepare(`
275
+ SELECT strftime('%Y-%m-%d', ts / 1000, 'unixepoch') AS day,
276
+ COUNT(*) AS total,
277
+ SUM(CASE WHEN routed_to_type = 'skill' THEN 1 ELSE 0 END) AS skill
278
+ FROM routing_events
279
+ WHERE ts > 0 AND ts >= @since
280
+ GROUP BY day
281
+ ORDER BY day ASC
282
+ `).all({ since: filter.since_ts }) as Array<{
283
+ day: string | null;
284
+ total: number;
285
+ skill: number | null;
286
+ }>;
287
+ return rows
288
+ .filter(r => r.day !== null)
289
+ .map(r => ({ day: r.day as string, total: r.total, skill: r.skill ?? 0 }));
290
+ }
291
+
164
292
  queryExperimentStats(experimentId: string): Array<{
165
293
  group_id: string;
166
294
  total: number;
@@ -17,7 +17,10 @@
17
17
  -- └─ skill_invocations.session_id → events.session_id (logical, no FK)
18
18
  -- └─ skill_invocations.route_request_id → routing_events.route_request_id (logical, no FK)
19
19
  --
20
- -- Why no FK on routing_events / skill_invocations:
20
+ -- token_usage (LLM token accounting)
21
+ -- └─ token_usage.session_id → events.session_id (logical, no FK)
22
+ --
23
+ -- Why no FK on routing_events / skill_invocations / token_usage:
21
24
  -- 1. routing_events.route_request_id is nullable and not UNIQUE — cannot be FK target
22
25
  -- 2. Write order is not guaranteed: skill_invocations may be written before routing_events
23
26
  -- 3. session_id references are maintained by application logic; sessions is an aggregate
@@ -152,7 +155,7 @@ CREATE INDEX IF NOT EXISTS idx_routing_events_experiment ON routing_events(exper
152
155
 
153
156
  CREATE TABLE IF NOT EXISTS token_usage (
154
157
  id INTEGER PRIMARY KEY AUTOINCREMENT,
155
- session_id TEXT NOT NULL,
158
+ session_id TEXT NOT NULL, -- logical ref → events.session_id; no FK due to write-order race (sessions is aggregate, may lag)
156
159
  timestamp INTEGER NOT NULL,
157
160
  input_tokens INTEGER NOT NULL,
158
161
  output_tokens INTEGER NOT NULL,
@@ -212,3 +215,10 @@ CREATE INDEX IF NOT EXISTS idx_events_session_hook ON events(session_id, hook_ty
212
215
 
213
216
  -- injections: optimize session + handler composite queries
214
217
  CREATE INDEX IF NOT EXISTS idx_injections_session_handler ON injections(session_id, source_handler);
218
+
219
+ -- routing_events: optimize routed_to_type distribution (H1 aggregate path)
220
+ CREATE INDEX IF NOT EXISTS idx_routing_events_type_ts ON routing_events(routed_to_type, ts DESC);
221
+
222
+ -- skill_invocations: optimize workflow/phase and feature_slug queries (H4 dedup: previously migration-only)
223
+ CREATE INDEX IF NOT EXISTS idx_skill_invocations_workflow ON skill_invocations(workflow, phase);
224
+ CREATE INDEX IF NOT EXISTS idx_skill_invocations_feature ON skill_invocations(feature_slug);
@@ -101,4 +101,68 @@ export class SessionOperations {
101
101
  end_time: r.end_time as string,
102
102
  }));
103
103
  }
104
+
105
+ // ── H2: 聚合 / 计数方法 ───────────────────────────────────────────────
106
+
107
+ /** Total session row count. */
108
+ countAllSessions(): number {
109
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM sessions').get() as { cnt: number };
110
+ return row.cnt;
111
+ }
112
+
113
+ /** Per-day session counts (date(start_time) → count) within [since, until). */
114
+ aggregateDailySessionCounts(opts: { since: string; until?: string }): Array<{ date: string; count: number }> {
115
+ const conditions: string[] = ['start_time >= ?'];
116
+ const params: unknown[] = [opts.since];
117
+ if (opts.until !== undefined) {
118
+ conditions.push('start_time < ?');
119
+ params.push(opts.until);
120
+ }
121
+ const sql = `SELECT date(start_time) as date, COUNT(*) as count FROM sessions
122
+ WHERE ${conditions.join(' AND ')}
123
+ GROUP BY date ORDER BY date`;
124
+ return this.db.prepare(sql).all(...params) as Array<{ date: string; count: number }>;
125
+ }
126
+
127
+ /** Sessions with start_time in [since, until?), ordered by start_time DESC. */
128
+ querySessionsByTimeRange(opts: { since: string; until?: string }): SessionSummary[] {
129
+ const conditions: string[] = ['start_time >= ?'];
130
+ const params: unknown[] = [opts.since];
131
+ if (opts.until !== undefined) {
132
+ conditions.push('start_time < ?');
133
+ params.push(opts.until);
134
+ }
135
+ const sql = `
136
+ SELECT
137
+ session_id,
138
+ first_prompt,
139
+ event_count,
140
+ start_time,
141
+ COALESCE(end_time, last_event_time, start_time) AS end_time
142
+ FROM sessions
143
+ WHERE ${conditions.join(' AND ')}
144
+ ORDER BY start_time DESC
145
+ `;
146
+ const rows = this.db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
147
+ return rows.map(r => ({
148
+ session_id: r.session_id as string,
149
+ first_prompt: (r.first_prompt as string) || '',
150
+ event_count: (r.event_count as number) ?? 0,
151
+ start_time: r.start_time as string,
152
+ end_time: r.end_time as string,
153
+ }));
154
+ }
155
+
156
+ /** Count of sessions with start_time in [since, until?). */
157
+ countSessionsByRange(opts: { since: string; until?: string }): number {
158
+ const conditions: string[] = ['start_time >= ?'];
159
+ const params: unknown[] = [opts.since];
160
+ if (opts.until !== undefined) {
161
+ conditions.push('start_time < ?');
162
+ params.push(opts.until);
163
+ }
164
+ const sql = `SELECT COUNT(*) as cnt FROM sessions WHERE ${conditions.join(' AND ')}`;
165
+ const row = this.db.prepare(sql).get(...params) as { cnt: number } | undefined;
166
+ return row?.cnt ?? 0;
167
+ }
104
168
  }
@@ -70,6 +70,43 @@ export class SkillOperations {
70
70
  return this.db.prepare(sql).all(...params) as SkillInvocationRow[];
71
71
  }
72
72
 
73
+ /**
74
+ * H1: 按 skill_id 聚合调用总数 / 成功 / 失败。
75
+ *
76
+ * 取代 routes 层的"全表 querySkillInvocations + JS reduce"。
77
+ * 走 `idx_skill_invocations_timestamp`。
78
+ */
79
+ aggregateSkillInvocationsBySkill(filter: { since: number; limit?: number }): Array<{
80
+ skill_id: string;
81
+ total: number;
82
+ success: number;
83
+ failed: number;
84
+ }> {
85
+ const limit = filter.limit ?? 1000;
86
+ const rows = this.db.prepare(`
87
+ SELECT skill_id,
88
+ COUNT(*) AS total,
89
+ SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success,
90
+ SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS failed
91
+ FROM skill_invocations
92
+ WHERE timestamp >= ?
93
+ GROUP BY skill_id
94
+ ORDER BY total DESC
95
+ LIMIT ?
96
+ `).all(filter.since, limit) as Array<{
97
+ skill_id: string;
98
+ total: number;
99
+ success: number | null;
100
+ failed: number | null;
101
+ }>;
102
+ return rows.map(r => ({
103
+ skill_id: r.skill_id,
104
+ total: r.total,
105
+ success: r.success ?? 0,
106
+ failed: r.failed ?? 0,
107
+ }));
108
+ }
109
+
73
110
  /**
74
111
  * 按 route_request_id 聚合工作流进度。
75
112
  *
@@ -161,4 +198,36 @@ export class SkillOperations {
161
198
  };
162
199
  });
163
200
  }
201
+
202
+ // ── H2: 计数 / 列表方法 ──────────────────────────────────────────────
203
+
204
+ /** Total skill_invocations row count. */
205
+ countAllSkillInvocations(): number {
206
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM skill_invocations').get() as { cnt: number };
207
+ return row.cnt;
208
+ }
209
+
210
+ /** Skill invocation count for a single session. */
211
+ countSkillInvocationsBySession(session_id: string): number {
212
+ const row = this.db.prepare(
213
+ `SELECT COUNT(*) as cnt FROM skill_invocations WHERE session_id = ?`
214
+ ).get(session_id) as { cnt: number } | undefined;
215
+ return row?.cnt ?? 0;
216
+ }
217
+
218
+ /** Distinct skill_id count since `since_ms` (unix ms). */
219
+ countDistinctSkillsSince(since_ms: number): number {
220
+ const row = this.db.prepare(
221
+ `SELECT COUNT(DISTINCT skill_id) as cnt FROM skill_invocations WHERE timestamp >= ?`
222
+ ).get(since_ms) as { cnt: number } | undefined;
223
+ return row?.cnt ?? 0;
224
+ }
225
+
226
+ /** Distinct skill_id values for a single session. */
227
+ queryDistinctSkillIdsBySession(session_id: string): string[] {
228
+ const rows = this.db.prepare(
229
+ `SELECT DISTINCT skill_id FROM skill_invocations WHERE session_id = ?`
230
+ ).all(session_id) as Array<{ skill_id: string }>;
231
+ return rows.map(r => r.skill_id);
232
+ }
164
233
  }