@zaip-ai/cli 1.0.8 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/README.md +1 -1
  2. package/dist/services/adapters.d.ts +1 -1
  3. package/dist/services/adapters.js +1 -1
  4. package/dist/services/agent-adapters.js +3 -3
  5. package/dist/services/agent-adapters.js.map +1 -1
  6. package/dist/services/agent-adapters.test.js +7 -1
  7. package/dist/services/agent-adapters.test.js.map +1 -1
  8. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  9. package/dist/services/codex-pre-tool-hook-template.js +89 -7
  10. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  11. package/dist/services/codex-rules.d.ts +1 -1
  12. package/dist/services/codex-rules.d.ts.map +1 -1
  13. package/dist/services/codex-rules.js +7 -0
  14. package/dist/services/codex-rules.js.map +1 -1
  15. package/dist/services/install-agents-template.d.ts.map +1 -1
  16. package/dist/services/install-agents-template.js +1 -0
  17. package/dist/services/install-agents-template.js.map +1 -1
  18. package/dist/services/logger.test.js +2 -2
  19. package/dist/services/runtime-template.test.js +31 -7
  20. package/dist/services/runtime-template.test.js.map +1 -1
  21. package/dist/services/template-manifest.d.ts.map +1 -1
  22. package/dist/services/template-manifest.js +12 -3
  23. package/dist/services/template-manifest.js.map +1 -1
  24. package/package.json +1 -1
  25. package/templates/.agents/architecture/software-architecture.md +131 -0
  26. package/templates/.agents/branding/brandbook.docx +0 -0
  27. package/templates/.agents/branding/brandbook.md +60 -0
  28. package/templates/.agents/config/agents/catalog.yaml +7 -7
  29. package/templates/.agents/config/clients/codex/agents/zaip-agent-backend.toml +2 -2
  30. package/templates/.agents/config/clients/codex/agents/zaip-agent-designer.toml +2 -2
  31. package/templates/.agents/config/clients/codex/agents/zaip-agent-finalizer.toml +1 -1
  32. package/templates/.agents/config/clients/codex/agents/zaip-agent-frontend.toml +8 -8
  33. package/templates/.agents/config/clients/codex/agents/zaip-agent-orchestrator.toml +1 -1
  34. package/templates/.agents/dashboard/.tmp-run-list.json +254 -0
  35. package/templates/.agents/dashboard/.zaip-managed.json +457 -173
  36. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-Z60df2r-.js → CostCalculationGuidePage-ovxurkIk.js} +1 -1
  37. package/templates/.agents/dashboard/apps/frontend/dist/assets/DashboardHelpDialog-BgVgrfhQ.js +2310 -0
  38. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-Coc6pA-1.js → PricingSettingsDialog-vSvvOHQp.js} +1 -1
  39. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-BomxqCBU.css +1 -0
  40. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-zH758vfs.js +3 -0
  41. package/templates/.agents/dashboard/apps/frontend/dist/assets/TraceabilityWorkspace-DLn-Mz2M.js +1 -0
  42. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CaKwQP43.css → TraceabilityWorkspace-DvvavDLY.css} +1 -1
  43. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.js +68 -0
  44. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-PoJA2PI1.css +1 -0
  45. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-DFBuc_qp.js → icons-vendor-B_lDDHn1.js} +1 -1
  46. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-B31GMG5O.css +1 -0
  47. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-CZQlsLV9.js +2 -0
  48. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-CCQOHdVI.js → react-vendor-B3TJrD4O.js} +1 -1
  49. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-pC2_0J8x.js → traceability-7f1j5wx3.js} +1 -1
  50. package/templates/.agents/dashboard/apps/frontend/dist/index.html +7 -7
  51. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  52. package/templates/.agents/dashboard/apps/frontend/playwright.dom.config.ts +32 -0
  53. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +199 -140
  54. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +0 -21
  55. package/templates/.agents/dashboard/apps/frontend/src/DashboardAlternateContent.tsx +20 -11
  56. package/templates/.agents/dashboard/apps/frontend/src/ProjectSettingsPage.tsx +14 -7
  57. package/templates/.agents/dashboard/apps/frontend/src/ProjectStackEditor.tsx +4 -2
  58. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.dom.e2e.ts +56 -0
  59. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.test.ts +21 -2
  60. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.tsx +89 -19
  61. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +19 -4
  62. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +49 -3
  63. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +29 -10
  64. package/templates/.agents/dashboard/apps/frontend/src/operations-period-render.test.ts +4 -4
  65. package/templates/.agents/dashboard/apps/frontend/src/overview-status-page.test.ts +4 -2
  66. package/templates/.agents/dashboard/apps/frontend/src/project-settings-page.css +36 -26
  67. package/templates/.agents/dashboard/apps/frontend/src/project-settings.test.ts +2 -2
  68. package/templates/.agents/dashboard/apps/frontend/src/snapshot-banner-semantics.test.ts +47 -6
  69. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +59 -9
  70. package/templates/.agents/dashboard/apps/frontend/src/sqlite-page-export.test.ts +32 -0
  71. package/templates/.agents/dashboard/apps/frontend/src/sqlite-page-export.ts +21 -0
  72. package/templates/.agents/dashboard/apps/frontend/src/styles.css +19 -0
  73. package/templates/.agents/dashboard/apps/frontend/src/traceability-task-pagination.test.ts +32 -0
  74. package/templates/.agents/dashboard/apps/frontend/src/traceability-task-pagination.ts +36 -0
  75. package/templates/.agents/dashboard/apps/frontend/src/traceability-workspace.css +6 -2
  76. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.test.ts +137 -23
  77. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.ts +229 -35
  78. package/templates/.agents/dashboard/apps/frontend/src/use-app-effects.ts +61 -7
  79. package/templates/.agents/dashboard/apps/frontend/src/use-app-operations-pages.ts +3 -6
  80. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.ts +7 -1
  81. package/templates/.agents/dashboard/apps/frontend/src/use-app-work-list-page.test.ts +163 -0
  82. package/templates/.agents/dashboard/apps/frontend/src/use-app-work-list-page.ts +136 -0
  83. package/templates/.agents/dashboard/apps/frontend/src/use-operations-detail-export.ts +76 -0
  84. package/templates/.agents/dashboard/apps/frontend/src/work-list-load-dedup.test.ts +23 -0
  85. package/templates/.agents/dashboard/apps/frontend/src/work-list-load-dedup.ts +27 -0
  86. package/templates/.agents/dashboard/apps/frontend/src/work-list-pagination.test.ts +39 -15
  87. package/templates/.agents/dashboard/apps/frontend/src/work-list-pagination.ts +3 -4
  88. package/templates/.agents/dashboard/apps/frontend/vite.config.ts +4 -1
  89. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  90. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CompactLifecycleCostTable.test.ts +89 -19
  91. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CompactLifecycleCostTable.tsx +31 -16
  92. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostCharts.test.ts +43 -14
  93. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostCharts.tsx +5 -5
  94. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +60 -0
  95. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionTable.tsx +191 -0
  96. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.loading.test.ts +14 -0
  97. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.run-page.test.ts +18 -0
  98. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.tsx +18 -3
  99. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostModelsAuditTable.tsx +1 -1
  100. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorContent.tsx +75 -44
  101. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorLoadErrors.tsx +35 -0
  102. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +162 -40
  103. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +221 -79
  104. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTableLoading.test.ts +132 -0
  105. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTableLoading.tsx +70 -0
  106. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.test.ts +203 -155
  107. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.tsx +164 -125
  108. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-copy.ts +2 -0
  109. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-views.ts +3 -1
  110. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor.css +5 -2
  111. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-page-range.ts +35 -0
  112. package/templates/.agents/dashboard/modules/cost-monitor/src/react/index.ts +2 -2
  113. package/templates/.agents/dashboard/modules/cost-monitor/src/react/period-cost-groups.ts +2 -1
  114. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-compact-cost-page.ts +32 -9
  115. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-group-page.test.ts +37 -2
  116. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-group-page.ts +131 -9
  117. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-run-page.ts +34 -17
  118. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-monitor-projection.test.ts +0 -24
  119. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-projection.test.ts +225 -424
  120. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-projection.ts +24 -15
  121. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-terminal-day.test.ts +6 -1
  122. package/templates/.agents/dashboard/modules/cost-monitor/src/server/source-size-cost-server/build-cost-monitor-projection-agents.test.ts +31 -0
  123. package/templates/.agents/dashboard/modules/cost-monitor/src/server/source-size-cost-server/build-cost-summary-projection-duration.test.ts +414 -0
  124. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  125. package/templates/.agents/dashboard/modules/operations/src/contracts.ts +46 -26
  126. package/templates/.agents/dashboard/modules/operations/src/react/DeveloperAttributionPanel.test.ts +54 -0
  127. package/templates/.agents/dashboard/modules/operations/src/react/DeveloperAttributionPanel.tsx +247 -0
  128. package/templates/.agents/dashboard/modules/operations/src/react/EntityTables.tsx +31 -11
  129. package/templates/.agents/dashboard/modules/operations/src/react/OperationsMainContent.tsx +39 -13
  130. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx +107 -2
  131. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.macro-metrics.test.ts +55 -0
  132. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.test.ts +23 -29
  133. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +211 -184
  134. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.tsx +127 -0
  135. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +60 -51
  136. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPageNavigation.tsx +1 -4
  137. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPageOverview.tsx +3 -0
  138. package/templates/.agents/dashboard/modules/operations/src/react/PagedWorkList.test.tsx +43 -0
  139. package/templates/.agents/dashboard/modules/operations/src/react/PagedWorkList.tsx +181 -0
  140. package/templates/.agents/dashboard/modules/operations/src/react/PeriodSummaryTable.tsx +32 -10
  141. package/templates/.agents/dashboard/modules/operations/src/react/TaskDetailsDialog.tsx +9 -3
  142. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.tsx +13 -5
  143. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkList.test.ts +16 -6
  144. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkList.tsx +14 -4
  145. package/templates/.agents/dashboard/modules/operations/src/react/conversation-rows.ts +1 -2
  146. package/templates/.agents/dashboard/modules/operations/src/react/developer-identities.test.ts +18 -0
  147. package/templates/.agents/dashboard/modules/operations/src/react/developer-identities.ts +30 -0
  148. package/templates/.agents/dashboard/modules/operations/src/react/execution-metrics.ts +1 -0
  149. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +393 -4
  150. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +484 -61
  151. package/templates/.agents/dashboard/modules/operations/src/react/export/export-layout.test.ts +23 -1
  152. package/templates/.agents/dashboard/modules/operations/src/react/export/xlsx-runtime-e2e.test.ts +248 -0
  153. package/templates/.agents/dashboard/modules/operations/src/react/index.ts +1 -0
  154. package/templates/.agents/dashboard/modules/operations/src/react/operation-duration.test.ts +4 -4
  155. package/templates/.agents/dashboard/modules/operations/src/react/operation-duration.ts +6 -4
  156. package/templates/.agents/dashboard/modules/operations/src/react/operations-coverage-toast.test.ts +4 -3
  157. package/templates/.agents/dashboard/modules/operations/src/react/operations-date-filters.css +1 -1
  158. package/templates/.agents/dashboard/modules/operations/src/react/operations-global-filters.test.ts +4 -1
  159. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +22 -12
  160. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview.css +4 -0
  161. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview.status-cards.test.ts +1 -1
  162. package/templates/.agents/dashboard/modules/operations/src/react/operations-page-contract.ts +53 -6
  163. package/templates/.agents/dashboard/modules/operations/src/react/operations-view-order.test.ts +2 -4
  164. package/templates/.agents/dashboard/modules/operations/src/react/period-summary.test.ts +82 -6
  165. package/templates/.agents/dashboard/modules/operations/src/react/period-summary.ts +1 -0
  166. package/templates/.agents/dashboard/modules/operations/src/react/work-board-model.test.ts +2 -1
  167. package/templates/.agents/dashboard/modules/operations/src/react/work-board-model.ts +1 -1
  168. package/templates/.agents/dashboard/modules/operations/src/server/execution-relationship-counts.test.ts +19 -7
  169. package/templates/.agents/dashboard/modules/operations/src/server/execution-relationship-counts.ts +4 -1
  170. package/templates/.agents/dashboard/modules/operations/src/server/metric-coverage.ts +1 -0
  171. package/templates/.agents/dashboard/modules/operations/src/server/quick-daily-operations-projection.ts +11 -5
  172. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  173. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  174. package/templates/.agents/dashboard/packages/branding/src/index.ts +1 -1
  175. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  176. package/templates/.agents/dashboard/packages/contracts/src/index.ts +1 -0
  177. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  178. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +533 -338
  179. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +506 -311
  180. package/templates/.agents/dashboard/packages/local-runtime/gitignore.template +2 -0
  181. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  182. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-sqlite-dashboard.mjs +15 -7
  183. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-sqlite-runtime.mjs +44 -13
  184. package/templates/.agents/dashboard/packages/local-runtime/scripts/rebuild-sqlite-read-model.mjs +8 -3
  185. package/templates/.agents/dashboard/packages/local-runtime/scripts/source-size-runtime-scripts/sqlite-read-model-fixture.mjs +397 -0
  186. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-read-model.mjs +472 -145
  187. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-unix-launchers.mjs +15 -3
  188. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-sqlite-read-model.mjs +141 -33
  189. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +533 -338
  190. package/templates/.agents/dashboard/packages/local-runtime/src/project-settings.test.ts +6 -2
  191. package/templates/.agents/dashboard/packages/local-runtime/src/project-settings.ts +3 -3
  192. package/templates/.agents/dashboard/packages/local-runtime/src/server-board.test.ts +89 -16
  193. package/templates/.agents/dashboard/packages/local-runtime/src/server-board.ts +33 -16
  194. package/templates/.agents/dashboard/packages/local-runtime/src/server-classification-page.test.ts +87 -0
  195. package/templates/.agents/dashboard/packages/local-runtime/src/server-period-macro-metric.test.ts +44 -0
  196. package/templates/.agents/dashboard/packages/local-runtime/src/server-period-macro-metric.ts +125 -0
  197. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-refresh.test.ts +66 -0
  198. package/templates/.agents/dashboard/packages/local-runtime/src/server-request-authority.test.ts +61 -0
  199. package/templates/.agents/dashboard/packages/local-runtime/src/server-request-authority.ts +18 -0
  200. package/templates/.agents/dashboard/packages/local-runtime/src/server-run-projections.test.ts +42 -0
  201. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-refresh.ts +97 -0
  202. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot.test.ts +22 -5
  203. package/templates/.agents/dashboard/packages/local-runtime/src/server-sqlite-task-trace.ts +91 -0
  204. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +7 -3
  205. package/templates/.agents/dashboard/packages/local-runtime/src/server.test-fixtures.ts +36 -0
  206. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +230 -132
  207. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +224 -182
  208. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-artifact-counts.ts +28 -0
  209. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.test.ts +63 -0
  210. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +109 -0
  211. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-aggregates.ts +497 -0
  212. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-fast-page-parity.test-support.ts +65 -0
  213. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-page.test.ts +176 -0
  214. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-page.ts +119 -0
  215. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-projection.ts +41 -0
  216. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.test.ts +16 -0
  217. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.ts +29 -0
  218. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-cache.ts +355 -0
  219. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-persistence.ts +121 -0
  220. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-timing.ts +27 -0
  221. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-metric-contract.ts +16 -2
  222. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-projection.ts +15 -0
  223. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-work-list.test.ts +223 -0
  224. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-work-list.ts +304 -0
  225. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-cache.test-support.ts +23 -0
  226. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-first-fold-types.ts +89 -0
  227. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider-fixture.ts +227 -0
  228. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider.test.ts +691 -0
  229. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +81 -0
  230. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +1655 -567
  231. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +2168 -1268
  232. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-cache.ts +129 -0
  233. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-install.test.ts +26 -0
  234. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-install.ts +28 -0
  235. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +116 -0
  236. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +146 -0
  237. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +165 -0
  238. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-refresh.mjs +140 -0
  239. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +255 -289
  240. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +94 -88
  241. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  242. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +5 -2
  243. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  244. package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +4 -1
  245. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +142 -0
  246. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +232 -0
  247. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +743 -0
  248. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +189 -214
  249. package/templates/.agents/dashboard/zaip-dashboard-windows.exe +0 -0
  250. package/templates/.agents/design/.zaip-managed.json +3 -3
  251. package/templates/.agents/design/apps/catalog/package.json +1 -1
  252. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  253. package/templates/.agents/growth/estrategia-bootstrap-90-dias.md +169 -0
  254. package/templates/.agents/growth/strategy.md +27 -0
  255. package/templates/.agents/marketing/estrategia-marketing-zaip-90-dias.md +197 -0
  256. package/templates/.agents/marketing/strategy.md +29 -0
  257. package/templates/.agents/requirements/autonomous-assurance.md +6 -6
  258. package/templates/.agents/requirements/comparative-audit.md +1 -1
  259. package/templates/.agents/requirements/context-engine.md +1 -1
  260. package/templates/.agents/requirements/cost-observability.md +1 -1
  261. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  262. package/templates/.agents/requirements/data-export.md +1 -1
  263. package/templates/.agents/requirements/design-system.md +1 -1
  264. package/templates/.agents/requirements/execution-engine.md +3 -3
  265. package/templates/.agents/requirements/kanban-dashboard.md +10 -2
  266. package/templates/.agents/requirements/model-orchestration.md +1 -1
  267. package/templates/.agents/requirements/public-distribution.md +1 -1
  268. package/templates/.agents/requirements/software-architecture.md +1 -1
  269. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  270. package/templates/.agents/requirements/work-lifecycle.md +4 -4
  271. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  272. package/templates/.agents/rules/dashboard-ui-consistency.md +14 -0
  273. package/templates/.agents/rules/frontend-lite.md +4 -4
  274. package/templates/.agents/rules/run-lifecycle-closure.md +4 -4
  275. package/templates/.agents/runtime/VERSION +1 -1
  276. package/templates/.agents/runtime/bin/zaip.mjs +1142 -1041
  277. package/templates/.agents/runtime/checksums.sha256 +6 -6
  278. package/templates/.agents/runtime/manifest.json +1 -1
  279. package/templates/.agents/runtime/pnpm-lock.sha256 +1 -1
  280. package/templates/.agents/runtime/provenance.json +5 -5
  281. package/templates/.agents/runtime/sbom.cdx.json +3 -3
  282. package/templates/.agents/sales/playbook.md +32 -0
  283. package/templates/.agents/sales/roteiro-comercial-zaip.md +252 -0
  284. package/templates/.agents/skills/criar-pasta-obsidian/scripts/generate-obsidian-vault.test.mjs +20 -16
  285. package/templates/.agents/skills/zaip-aios/SKILL.md +1 -1
  286. package/templates/.agents/skills/zaip-skill-agent-catalog/SKILL.md +22 -0
  287. package/templates/.agents/skills/zaip-skill-agent-catalog/agents/openai.yaml +4 -0
  288. package/templates/.agents/skills/zaip-skill-author-skills/SKILL.md +3 -2
  289. package/templates/.agents/skills/zaip-skill-author-skills/scripts/audit-skill-catalog.mjs +49 -0
  290. package/templates/.agents/skills/zaip-skill-author-skills/scripts/author-skill-scripts.test.mjs +27 -0
  291. package/templates/.agents/skills/zaip-skill-author-skills/scripts/run-skill-tests.mjs +37 -0
  292. package/templates/.agents/skills/zaip-skill-backend/SKILL.md +1 -0
  293. package/templates/.agents/skills/zaip-skill-brand-governance/SKILL.md +9 -2
  294. package/templates/.agents/skills/zaip-skill-brand-governance/references/brandbook-framework.md +39 -0
  295. package/templates/.agents/skills/zaip-skill-brand-governance/references/brandbook-standards.md +1 -1
  296. package/templates/.agents/skills/zaip-skill-change/SKILL.md +1 -1
  297. package/templates/.agents/skills/zaip-skill-change/references/observable-evidence.md +83 -0
  298. package/templates/.agents/skills/zaip-skill-dashboard-diagnostics/SKILL.md +22 -0
  299. package/templates/.agents/skills/zaip-skill-dashboard-diagnostics/agents/openai.yaml +4 -0
  300. package/templates/.agents/skills/zaip-skill-enforce-architecture/SKILL.md +2 -0
  301. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +1 -1
  302. package/templates/.agents/skills/zaip-skill-frontend/SKILL.md +4 -3
  303. package/templates/.agents/skills/zaip-skill-frontend/agents/openai.yaml +2 -2
  304. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +10 -10
  305. package/templates/.agents/skills/zaip-skill-growth-strategy-revops/SKILL.md +6 -0
  306. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +27 -0
  307. package/templates/.agents/skills/zaip-skill-lifecycle-closure/agents/openai.yaml +4 -0
  308. package/templates/.agents/skills/zaip-skill-marketing/SKILL.md +6 -0
  309. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  310. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +111 -104
  311. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-explicit-recovery.mjs +223 -0
  312. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +55 -9
  313. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.test.mjs +145 -14
  314. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.mjs +19 -9
  315. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +82 -5
  316. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/source-size-telemetry/model-identity-reconciliation.mjs +77 -0
  317. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +20 -0
  318. package/templates/.agents/skills/zaip-skill-release-qualification/agents/openai.yaml +4 -0
  319. package/templates/.agents/skills/zaip-skill-review/SKILL.md +5 -0
  320. package/templates/.agents/skills/zaip-skill-rules/SKILL.md +1 -1
  321. package/templates/.agents/skills/zaip-skill-sales-methodology/SKILL.md +6 -0
  322. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  323. package/templates/.agents/skills/zaip-skill-sqlite-read-model/SKILL.md +22 -90
  324. package/templates/.agents/skills/zaip-skill-sqlite-read-model/scripts/ensure-node.mjs +28 -5
  325. package/templates/.agents/zaip-aios/INDEX.md +57 -22
  326. package/templates/.agents/zaip-aios/README.md +24 -13
  327. package/templates/.agents/zaip-aios/VERSION +1 -1
  328. package/templates/.agents/zaip-aios/guides/02-requirements.md +5 -0
  329. package/templates/.agents/zaip-aios/guides/06-execucao.md +13 -4
  330. package/templates/.agents/zaip-aios/guides/07-revisao-e-validacao.md +7 -0
  331. package/templates/.agents/zaip-aios/guides/10-seguranca.md +23 -0
  332. package/templates/.agents/zaip-aios/guides/11-observabilidade-e-custos.md +6 -6
  333. package/templates/.agents/zaip-aios/guides/13-boas-praticas.md +3 -0
  334. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +12 -3
  335. package/templates/.agents/zaip-aios/guides/17-como-o-custo-e-calculado.md +5 -5
  336. package/templates/.agents/zaip-aios/guides/18-catalogo-de-agentes.md +76 -0
  337. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +95 -0
  338. package/templates/.agents/zaip-aios/guides/20-instalacao-detalhada.md +366 -0
  339. package/templates/.agents/zaip-aios/guides/21-telemetria-monitor-custos.md +46 -0
  340. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +74 -0
  341. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +483 -0
  342. package/templates/.agents/zaip-aios/guides/24-calculo-de-custos-openai.md +85 -0
  343. package/templates/.agents/zaip-aios/guides/25-fontes-de-precos-openai.md +19 -0
  344. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +111 -0
  345. package/templates/.agents/zaip-aios/guides/27-telemetria-git-native.md +172 -0
  346. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +149 -0
  347. package/templates/.agents/zaip-aios/guides/assets/diagrams/lifecycles-task-spec-prd.png +0 -0
  348. package/templates/.agents/zaip-aios/guides/assets/diagrams/prd-lifecycle.png +0 -0
  349. package/templates/.agents/zaip-aios/guides/assets/diagrams/spec-lifecycle.png +0 -0
  350. package/templates/.agents/zaip-aios/guides/assets/diagrams/task-lifecycle.png +0 -0
  351. package/templates/template-manifest.json +756 -235
  352. package/templates/.agents/dashboard/apps/frontend/dist/assets/DashboardHelpDialog-Cs6dzDmW.js +0 -1505
  353. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-QE1k9yZB.css +0 -1
  354. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-_Pf5DA_A.js +0 -3
  355. package/templates/.agents/dashboard/apps/frontend/dist/assets/TraceabilityWorkspace-BVlz3DLZ.js +0 -1
  356. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-BZ5WdaMN.css +0 -1
  357. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Cf9ctPVt.js +0 -68
  358. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-8A4DcNXM.js +0 -2
  359. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DK_enB5Z.css +0 -1
  360. package/templates/.agents/dashboard/apps/frontend/src/use-idle-cost-prefetch.ts +0 -45
@@ -11,26 +11,78 @@ import {
11
11
  type PersistedTokenUsage,
12
12
  type PricingCatalog,
13
13
  pricingCatalogSchema,
14
+ resolveHistoricalDeveloperAttribution,
14
15
  } from "@zaip-ai/costs";
16
+ import type { CostSummarySource } from "@zaip-ai/dashboard-cost-monitor/server";
17
+ import { allocateSharedCreationValue } from "@zaip-ai/lifecycle-manifests";
15
18
  import {
16
19
  foldRunV2Records,
17
20
  parseRunV2JsonLines,
18
21
  type RunV2Fold,
19
22
  type UsageRecordedV2,
20
23
  } from "../../../../../packages/decisions/src/index.js";
24
+ import { artifactStructuralRowsSql, developerArtifactCountsCte } from "./sqlite-artifact-counts.js";
25
+ import {
26
+ costDimensionFactDateFilter,
27
+ isDateOnlyCostPage,
28
+ readDateOnlyCostDimensionPage,
29
+ } from "./sqlite-cost-agent-page.js";
30
+ import { buildCostDimensionAggregates } from "./sqlite-cost-aggregates.js";
31
+ import { type CostGroupKind, readCostGroupPage } from "./sqlite-cost-group-page.js";
32
+ import { materializeCostGroupDailyProjection } from "./sqlite-cost-group-projection.js";
33
+ import { filterCostInvocationProjection } from "./sqlite-cost-invocation-page.js";
34
+ import {
35
+ readFirstFoldAllTime,
36
+ readFirstFoldBoardAllTime,
37
+ readFirstFoldMonthlyQuick,
38
+ readFirstFoldTopTasksAllTime,
39
+ readFirstFoldViewport,
40
+ } from "./sqlite-first-fold-cache.js";
41
+ import {
42
+ hasRequiredFirstFoldPayloads,
43
+ persistFirstFoldPayloads,
44
+ } from "./sqlite-first-fold-persistence.js";
45
+ import { measureFirstFold } from "./sqlite-first-fold-timing.js";
46
+ import { materializeOperationsWorkProjection } from "./sqlite-operations-projection.js";
47
+ import {
48
+ attachOperationsWorkListDevelopers,
49
+ operationsWorkListPayloadPage,
50
+ operationsWorkListSqlPage,
51
+ operationsWorkListSqlRows,
52
+ operationsWorkRowsInRange,
53
+ } from "./sqlite-operations-work-list.js";
54
+ import {
55
+ installLegacyViewAliases,
56
+ materializeSnapshot,
57
+ mostRecentCompatibleSnapshot,
58
+ pruneSnapshotCache,
59
+ readSnapshotFileCount,
60
+ } from "./sqlite-snapshot-cache.js";
61
+
62
+ export {
63
+ type SqliteReadModelSourceCandidate,
64
+ sourceFingerprint,
65
+ } from "./sqlite-read-model-source-fingerprint.js";
66
+
67
+ import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
21
68
 
22
69
  const dbRelativePath = ".local/dashboard-sqlite/read-model.sqlite";
23
- const textExtensions = new Set([".json", ".jsonl", ".md", ".yaml", ".yml", ".txt"]);
24
70
  // Schema 58 aligns Operations invocation/day facts with the same factual
25
71
  // source day used by tokens, Cost Monitor and attributed Tempo. Seed reuse is
26
72
  // safe only when both source and this schema version match.
27
- const readModelSchemaVersion = "60";
73
+ // Allocation rows are materialized after attributed-time reconciliation; a
74
+ // prior seed contains the pre-reconciliation payload and is incompatible.
75
+ const readModelSchemaVersion = "70";
28
76
  const requiredPersistedTables = [
29
77
  "zaip_aios_activities",
30
78
  "zaip_dim_llm_model",
31
79
  "zaip_dim_provider",
32
80
  "zaip_dim_zaip_agent",
33
81
  "cost_invocation_dimension_facts",
82
+ "cost_group_daily_projection",
83
+ "cost_group_entity_projection",
84
+ "developer_cost_projection",
85
+ "operations_work_projection",
34
86
  ] as const;
35
87
  const requiredIndexes = [
36
88
  "zaip_activities_agent_run",
@@ -48,6 +100,9 @@ const requiredIndexes = [
48
100
  "cost_dimension_facts_day_model",
49
101
  "cost_dimension_facts_day_provider",
50
102
  "cost_dimension_facts_invocation",
103
+ "cost_group_daily_projection_lookup",
104
+ "cost_group_entity_projection_lookup",
105
+ "developer_cost_projection_lookup",
51
106
  ] as const;
52
107
  const requiredViews = [
53
108
  "zaip_vw_read_model_metadata",
@@ -59,26 +114,6 @@ const requiredViews = [
59
114
  ] as const;
60
115
  const MAX_RECONCILED_RUN_INTERVAL_MS = 2 * 3_600_000;
61
116
 
62
- type CostDimensionAggregateRow = {
63
- key: string;
64
- label: string;
65
- invocation_count: number;
66
- task_count: number;
67
- total_duration: number | null;
68
- source_event_count: number | null;
69
- total_tokens: number | null;
70
- total_cost_usd: number | null;
71
- total_cost_brl: number | null;
72
- };
73
-
74
- function observedDimensionValue(value: unknown): string | null {
75
- if (typeof value !== "string") return null;
76
- const normalized = value.trim();
77
- return normalized.length > 0 && !["unavailable", "unknown", "mixed"].includes(normalized.toLowerCase())
78
- ? normalized
79
- : null;
80
- }
81
-
82
117
  function saoPauloCurrentMonth(): { from: string; to: string } {
83
118
  const parts = new Intl.DateTimeFormat("en-CA", {
84
119
  timeZone: "America/Sao_Paulo",
@@ -91,161 +126,7 @@ function saoPauloCurrentMonth(): { from: string; to: string } {
91
126
  return { from: `${value.year}-${value.month}-01`, to: day };
92
127
  }
93
128
 
94
- /** Small, seed-time-only aggregates for the unfiltered Models/Providers tables. */
95
- function buildCostDimensionAggregates(
96
- database: DatabaseSync,
97
- ): Record<"model" | "provider", CostDimensionAggregateRow[]> {
98
- const dimensions = ["model", "provider"] as const;
99
- const allowedProviders = new Set(
100
- (database.prepare("SELECT provider FROM zaip_dim_provider").all() as Array<{ provider: string }>)
101
- .map((row) => row.provider),
102
- );
103
- const activities = database.prepare(`SELECT activity_row_id, agent_invocation_id, task_id, duration_ms,
104
- source_event_count, provider, model, total_tokens, cost_usd, cost_brl
105
- FROM zaip_aios_activities WHERE payload_type IS NULL`).all() as Array<Record<string, unknown>>;
106
- const segments = database.prepare(`SELECT agent_invocation_id, provider, model, total_tokens, cost_usd, cost_brl
107
- FROM invocation_usage_segments`).all() as Array<Record<string, unknown>>;
108
- const activityByInvocation = new Map(
109
- activities
110
- .filter((row) => typeof row.agent_invocation_id === "string")
111
- .map((row) => [String(row.agent_invocation_id), row] as const),
112
- );
113
- const rowsByDimension = new Map<"model" | "provider", Map<string, CostDimensionAggregateRow>>(
114
- dimensions.map((dimension) => [dimension, new Map()]),
115
- );
116
- const ensure = (dimension: "model" | "provider", key: string) => {
117
- const rows = rowsByDimension.get(dimension)!;
118
- const current = rows.get(key);
119
- if (current !== undefined) return current;
120
- const created: CostDimensionAggregateRow = {
121
- key, label: key, invocation_count: 0, task_count: 0, total_duration: null,
122
- source_event_count: null, total_tokens: null, total_cost_usd: null, total_cost_brl: null,
123
- };
124
- rows.set(key, created);
125
- return created;
126
- };
127
- const add = (target: CostDimensionAggregateRow, field: "total_tokens" | "total_cost_usd" | "total_cost_brl", value: unknown) => {
128
- if (typeof value === "number") target[field] = (target[field] ?? 0) + value;
129
- };
130
- const segmentsByInvocation = new Map<string, Array<Record<string, unknown>>>();
131
- for (const segment of segments) {
132
- const id = String(segment.agent_invocation_id ?? "");
133
- if (id.length === 0) continue;
134
- const invocationSegments = segmentsByInvocation.get(id);
135
- if (invocationSegments === undefined) segmentsByInvocation.set(id, [segment]);
136
- else invocationSegments.push(segment);
137
- }
138
- for (const [id, invocationSegments] of segmentsByInvocation) {
139
- const activity = activityByInvocation.get(id);
140
- if (activity === undefined) continue;
141
- for (const dimension of dimensions) {
142
- const financialSegments = invocationSegments.filter(
143
- (segment) =>
144
- observedDimensionValue(segment[dimension]) !== null &&
145
- observedDimensionValue(segment.provider) !== null &&
146
- allowedProviders.has(observedDimensionValue(segment.provider)!) &&
147
- (typeof segment.total_tokens === "number" ||
148
- typeof segment.cost_usd === "number" ||
149
- typeof segment.cost_brl === "number"),
150
- );
151
- const keys = [...new Set(financialSegments.map((row) => observedDimensionValue(row[dimension])!))];
152
- if (keys.length === 0) {
153
- // Keep auditable, dimensionless invocation facts visible so Models
154
- // and Providers reconcile to the Cost KPI instead of silently
155
- // dropping them.
156
- const target = ensure(dimension, "unavailable");
157
- target.invocation_count += 1;
158
- if (typeof activity.duration_ms === "number") target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
159
- if (typeof activity.source_event_count === "number") target.source_event_count = (target.source_event_count ?? 0) + activity.source_event_count;
160
- add(target, "total_tokens", activity.total_tokens);
161
- add(target, "total_cost_usd", activity.cost_usd);
162
- add(target, "total_cost_brl", activity.cost_brl);
163
- }
164
- for (const segment of financialSegments) {
165
- const target = ensure(dimension, observedDimensionValue(segment[dimension])!);
166
- add(target, "total_tokens", segment.total_tokens);
167
- add(target, "total_cost_usd", segment.cost_usd);
168
- add(target, "total_cost_brl", segment.cost_brl);
169
- }
170
- // An invocation's elapsed time is attributable only when its observed
171
- // financial segments agree on one dimension. Do not fabricate a
172
- // mixed bucket or split wall-clock time across models/providers.
173
- if (keys.length === 1) {
174
- const target = ensure(dimension, keys[0]!);
175
- target.invocation_count += 1;
176
- if (typeof activity.duration_ms === "number") target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
177
- if (typeof activity.source_event_count === "number") target.source_event_count = (target.source_event_count ?? 0) + activity.source_event_count;
178
- }
179
- }
180
- }
181
- const taskKeys = new Map<"model" | "provider", Map<string, Set<string>>>(
182
- dimensions.map((dimension) => [dimension, new Map()]),
183
- );
184
- for (const [id, invocationSegments] of segmentsByInvocation) {
185
- const taskId = activityByInvocation.get(id)?.task_id;
186
- if (typeof taskId !== "string") continue;
187
- for (const dimension of dimensions) {
188
- const byTask = taskKeys.get(dimension)!;
189
- const keys = byTask.get(taskId) ?? new Set<string>();
190
- for (const segment of invocationSegments) {
191
- const key = observedDimensionValue(segment[dimension]);
192
- const provider = observedDimensionValue(segment.provider);
193
- const hasFinancialFact =
194
- typeof segment.total_tokens === "number" ||
195
- typeof segment.cost_usd === "number" ||
196
- typeof segment.cost_brl === "number";
197
- if (key !== null && provider !== null && allowedProviders.has(provider) && hasFinancialFact)
198
- keys.add(key);
199
- }
200
- byTask.set(taskId, keys);
201
- }
202
- }
203
- for (const dimension of dimensions)
204
- for (const keys of taskKeys.get(dimension)!.values())
205
- for (const key of keys) ensure(dimension, key).task_count += 1;
206
- for (const activity of activities) {
207
- if (typeof activity.agent_invocation_id === "string") continue;
208
- const hasFinancialFact =
209
- typeof activity.total_tokens === "number" ||
210
- typeof activity.cost_usd === "number" ||
211
- typeof activity.cost_brl === "number";
212
- if (!hasFinancialFact) continue;
213
- const provider = observedDimensionValue(activity.provider);
214
- if (provider === null || !allowedProviders.has(provider)) continue;
215
- for (const dimension of dimensions) {
216
- const key = observedDimensionValue(activity[dimension]) ?? "unavailable";
217
- const target = ensure(dimension, key);
218
- target.invocation_count += 1;
219
- if (typeof activity.task_id === "string") target.task_count += 1;
220
- if (typeof activity.duration_ms === "number") target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
221
- if (typeof activity.source_event_count === "number") target.source_event_count = (target.source_event_count ?? 0) + activity.source_event_count;
222
- add(target, "total_tokens", activity.total_tokens);
223
- add(target, "total_cost_usd", activity.cost_usd);
224
- add(target, "total_cost_brl", activity.cost_brl);
225
- }
226
- }
227
- return Object.fromEntries(dimensions.map((dimension) => [dimension, [...rowsByDimension.get(dimension)!.values()].sort(
228
- (left, right) => (right.total_cost_brl ?? -1) - (left.total_cost_brl ?? -1) || left.key.localeCompare(right.key),
229
- )])) as Record<"model" | "provider", CostDimensionAggregateRow[]>;
230
- }
231
-
232
129
  /** Only canonical inputs consumed by the fact or its direct dimensions. */
233
- function isDashboardCanonicalSource(source: string): boolean {
234
- if (
235
- source === ".agents/config/project.yaml" ||
236
- source === ".agents/config/framework.lock.yaml" ||
237
- source === ".agents/config/agents/catalog.yaml" ||
238
- source === ".agents/config/clients.yaml"
239
- )
240
- return true;
241
- if (source.startsWith(".agents/config/pricing/")) return true;
242
- if (source.startsWith(".agents/records/runs/") && source.endsWith(".jsonl")) return true;
243
- if (/^\.agents\/records\/(tasks|specs|prds)\/.*\.md$/.test(source)) return true;
244
- if (/^\.agents\/records\/executions\/.*\/execution\.md$/.test(source)) return true;
245
- return /^\.agents\/records\/changes\/.*\/(change\.md|decisions\/.*\.md|artifacts\/(evidence|validations)\/.*\.md)$/.test(
246
- source,
247
- );
248
- }
249
130
 
250
131
  type AgentProfile = {
251
132
  catalogId: string;
@@ -260,15 +141,21 @@ type CanonicalEntity = {
260
141
  status: string | null;
261
142
  stage: string | null;
262
143
  risk: string | null;
144
+ specId: string | null;
145
+ changeId: string | null;
263
146
  occurredAt: string | null;
147
+ creationRunId: string | null;
148
+ completedRunId: string | null;
264
149
  };
265
150
  type CanonicalArtifact = {
266
151
  kind: "evidence" | "validation";
267
152
  id: string;
268
153
  occurredAt: string;
269
154
  occurredDaySp: string;
155
+ taskId: string | null;
156
+ changeId: string | null;
157
+ runId: string | null;
270
158
  };
271
-
272
159
  type CanonicalExecution = {
273
160
  id: string;
274
161
  status: string | null;
@@ -279,7 +166,6 @@ type CanonicalExecution = {
279
166
  finishedAt: string | null;
280
167
  sourcePath: string;
281
168
  };
282
-
283
169
  type CanonicalChange = {
284
170
  id: string;
285
171
  title: string;
@@ -296,7 +182,6 @@ type CanonicalChange = {
296
182
  createdAt: string;
297
183
  sourcePath: string;
298
184
  };
299
-
300
185
  type CanonicalDecision = {
301
186
  id: string;
302
187
  title: string;
@@ -306,7 +191,6 @@ type CanonicalDecision = {
306
191
  createdAt: string;
307
192
  sourcePath: string;
308
193
  };
309
-
310
194
  function frontMatterRecord(content: string): Record<string, unknown> | null {
311
195
  const source = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
312
196
  if (source === undefined) return null;
@@ -336,9 +220,7 @@ function stringValues(value: unknown): string[] {
336
220
  : [];
337
221
  }
338
222
 
339
- function readCanonicalLifecycleRecords(
340
- rows: Array<{ path: string; content: string }>,
341
- ): {
223
+ function readCanonicalLifecycleRecords(rows: Array<{ path: string; content: string }>): {
342
224
  executions: CanonicalExecution[];
343
225
  changes: CanonicalChange[];
344
226
  decisions: CanonicalDecision[];
@@ -462,7 +344,11 @@ function readCanonicalEntities(
462
344
  status: scalar("status"),
463
345
  stage: scalar("stage"),
464
346
  risk: scalar("risk"),
347
+ specId: scalar("spec_id"),
348
+ changeId: scalar("change_id"),
465
349
  occurredAt: scalar("created_at") ?? scalar("started_at") ?? scalar("recorded_at"),
350
+ creationRunId: scalar("creation_run_id"),
351
+ completedRunId: scalar("completed_run_id"),
466
352
  });
467
353
  }
468
354
  return entities;
@@ -490,7 +376,9 @@ function readAgentProfiles(
490
376
  // host records used `zaip-` forms of deprecated IDs. All of these are
491
377
  // explicit identities from the catalog; no UI fallback is involved.
492
378
  const identities = new Set([agent, catalogId]);
493
- for (const alias of block.matchAll(/^ {8}- kind:\s*(?:id|codex_name|codex_task_name)\s*\r?\n {10}value:\s*([^\s#]+)/gm)) {
379
+ for (const alias of block.matchAll(
380
+ /^ {8}- kind:\s*(?:id|codex_name|codex_task_name)\s*\r?\n {10}value:\s*([^\s#]+)/gm,
381
+ )) {
494
382
  const value = alias[1];
495
383
  if (value !== undefined) identities.add(value);
496
384
  }
@@ -586,7 +474,9 @@ type ResolvedSourceInterval = { start: string; end: string; duration: number };
586
474
  * happened. It wins over Run finalization elapsed time, which may include
587
475
  * orchestration/capture lag or be recorded much later.
588
476
  */
589
- function resolvedSourceInterval(records: readonly UsageRecordedV2[]): ResolvedSourceInterval | null {
477
+ function resolvedSourceInterval(
478
+ records: readonly UsageRecordedV2[],
479
+ ): ResolvedSourceInterval | null {
590
480
  const intervals = records.flatMap((record) => {
591
481
  const start = record.source_started_at;
592
482
  const end = record.source_finished_at;
@@ -677,7 +567,13 @@ function resolveDurations(
677
567
  }
678
568
  // Tokens and lifecycle timestamps are not a time source. Absence stays
679
569
  // absent: consumers must display unavailable rather than manufacture time.
680
- return { source: null, effective: null, provenance: null, sourceStartedAt: null, sourceFinishedAt: null };
570
+ return {
571
+ source: null,
572
+ effective: null,
573
+ provenance: null,
574
+ sourceStartedAt: null,
575
+ sourceFinishedAt: null,
576
+ };
681
577
  }
682
578
 
683
579
  function latestFxRate(rows: Array<{ path: string; content: string }>): FxRate | null {
@@ -857,7 +753,9 @@ function usagePricingAudit(
857
753
  const effectiveBrl =
858
754
  record.cost.brl ??
859
755
  actual?.estimated_brl ??
860
- (effectiveUsd !== null && fxRate !== null ? Math.round(effectiveUsd * fxRate.rate * 1_000_000) / 1_000_000 : null);
756
+ (effectiveUsd !== null && fxRate !== null
757
+ ? Math.round(effectiveUsd * fxRate.rate * 1_000_000) / 1_000_000
758
+ : null);
861
759
  return {
862
760
  provider,
863
761
  model,
@@ -946,8 +844,8 @@ export function allocateAttributedTokenActiveMs(intervals: readonly TemporalInte
946
844
  boundaries.add(interval.startMs);
947
845
  boundaries.add(interval.endMs);
948
846
  for (let cursor = interval.startMs; cursor < interval.endMs; ) {
949
- const nextDay = Date.parse(`${saoPauloDay(new Date(cursor).toISOString())}T03:00:00.000Z`) +
950
- 86_400_000;
847
+ const nextDay =
848
+ Date.parse(`${saoPauloDay(new Date(cursor).toISOString())}T03:00:00.000Z`) + 86_400_000;
951
849
  if (nextDay >= interval.endMs) break;
952
850
  boundaries.add(nextDay);
953
851
  cursor = nextDay;
@@ -958,7 +856,9 @@ export function allocateAttributedTokenActiveMs(intervals: readonly TemporalInte
958
856
  for (let index = 0; index < points.length - 1; index += 1) {
959
857
  const startMs = points[index]!;
960
858
  const endMs = points[index + 1]!;
961
- const active = intervals.filter((interval) => interval.startMs < endMs && interval.endMs > startMs);
859
+ const active = intervals.filter(
860
+ (interval) => interval.startMs < endMs && interval.endMs > startMs,
861
+ );
962
862
  if (active.length === 0 || endMs <= startMs) continue;
963
863
  const share = (endMs - startMs) / active.length;
964
864
  const day = saoPauloDay(new Date(startMs).toISOString());
@@ -982,34 +882,21 @@ function costMonitorFilterDay(value: string | null | undefined): string | null {
982
882
  return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : saoPauloDay(value);
983
883
  }
984
884
 
985
- function saoPauloLocalMidnight(day: string): number {
986
- const [year, month, date] = day.split("-").map(Number);
987
- const utcGuess = Date.UTC(year!, month! - 1, date!);
988
- const offset =
989
- new Intl.DateTimeFormat("en-US", {
990
- timeZone: "America/Sao_Paulo",
991
- timeZoneName: "longOffset",
992
- })
993
- .formatToParts(new Date(utcGuess))
994
- .find((part) => part.type === "timeZoneName")?.value ?? "GMT";
995
- const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offset);
996
- if (match === null) return utcGuess;
997
- const offsetMs =
998
- (Number(match[2]) * 60 + Number(match[3])) * 60_000 * (match[1] === "+" ? 1 : -1);
999
- return utcGuess - offsetMs;
1000
- }
1001
-
1002
- function nextSaoPauloDay(day: string): string {
1003
- const [year, month, date] = day.split("-").map(Number);
1004
- const next = new Date(Date.UTC(year!, month! - 1, date! + 1));
1005
- return next.toISOString().slice(0, 10);
885
+ function sumKnownDeveloperMetric(
886
+ rows: readonly Record<string, unknown>[],
887
+ field: string,
888
+ ): number | null {
889
+ const values = rows
890
+ .map((row) => row[field])
891
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
892
+ return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0);
1006
893
  }
1007
894
 
1008
895
  /** Extracts the small artifact projection while the canonical file is already being read. */
1009
896
  function readCanonicalArtifact(source: string, content: string): CanonicalArtifact | null {
1010
897
  const kind = source.includes("/artifacts/evidence/")
1011
898
  ? "evidence"
1012
- : source.includes("/artifacts/validations/")
899
+ : source.includes("/artifacts/validations/") || source.includes("/artifacts/tests/")
1013
900
  ? "validation"
1014
901
  : null;
1015
902
  if (kind === null) return null;
@@ -1028,6 +915,9 @@ function readCanonicalArtifact(source: string, content: string): CanonicalArtifa
1028
915
  id: scalar("id") ?? source.slice(source.lastIndexOf("/") + 1, -3),
1029
916
  occurredAt,
1030
917
  occurredDaySp: saoPauloDay(occurredAt),
918
+ taskId: scalar("task_id"),
919
+ changeId: scalar("change_id"),
920
+ runId: scalar("run_id"),
1031
921
  };
1032
922
  }
1033
923
 
@@ -1035,6 +925,7 @@ export function sqliteReadModelPath(workspaceRoot: string): string {
1035
925
  return resolve(process.env.ZAIP_SQLITE_DB_PATH ?? resolve(workspaceRoot, dbRelativePath));
1036
926
  }
1037
927
 
928
+ /** Atomically installs a worker-built immutable generation after its schema is validated. */
1038
929
  /**
1039
930
  * Refuses stale or incomplete SQLite generations before they are served. This
1040
931
  * is intentionally independent of source fingerprints: a matching cache key
@@ -1048,36 +939,54 @@ export function validateSqliteReadModelSchema(path: string): {
1048
939
  } {
1049
940
  const database = new DatabaseSync(path, { readOnly: true });
1050
941
  try {
1051
- const integrity = database.prepare("PRAGMA integrity_check").get() as { integrity_check?: string };
942
+ const integrity = database.prepare("PRAGMA integrity_check").get() as {
943
+ integrity_check?: string;
944
+ };
1052
945
  if (integrity.integrity_check !== "ok")
1053
- throw new Error(`SQLite integrity check failed for ${path}: ${integrity.integrity_check ?? "unknown"}`);
946
+ throw new Error(
947
+ `SQLite integrity check failed for ${path}: ${integrity.integrity_check ?? "unknown"}`,
948
+ );
1054
949
  const objects = database
1055
950
  .prepare("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index')")
1056
951
  .all() as Array<{ name: string; type: "table" | "view" | "index" }>;
1057
- const tables = objects.filter((object) => object.type === "table").map((object) => object.name).sort();
952
+ const tables = objects
953
+ .filter((object) => object.type === "table")
954
+ .map((object) => object.name)
955
+ .sort();
1058
956
  const expectedTables = [...requiredPersistedTables].sort();
1059
- if (tables.length !== expectedTables.length || tables.some((table, index) => table !== expectedTables[index]))
1060
- throw new Error(`SQLite schema ${readModelSchemaVersion} requires exactly five persisted tables; found ${tables.join(", ") || "none"}`);
957
+ if (
958
+ tables.length !== expectedTables.length ||
959
+ tables.some((table, index) => table !== expectedTables[index])
960
+ )
961
+ throw new Error(
962
+ `SQLite schema ${readModelSchemaVersion} requires the complete persisted table contract; found ${tables.join(", ") || "none"}`,
963
+ );
1061
964
  const names = new Set(objects.map((object) => object.name));
1062
965
  const missingViews = requiredViews.filter((view) => !names.has(view));
1063
- if (missingViews.length > 0) throw new Error(`SQLite schema is missing required views: ${missingViews.join(", ")}`);
966
+ if (missingViews.length > 0)
967
+ throw new Error(`SQLite schema is missing required views: ${missingViews.join(", ")}`);
1064
968
  const views = objects.filter((object) => object.type === "view").map((object) => object.name);
1065
969
  if (views.some((view) => !view.startsWith("zaip_vw_")))
1066
970
  throw new Error("SQLite persisted views must use the zaip_vw_ namespace");
1067
971
  const missingIndexes = requiredIndexes.filter((index) => !names.has(index));
1068
- if (missingIndexes.length > 0) throw new Error(`SQLite schema is missing required indexes: ${missingIndexes.join(", ")}`);
972
+ if (missingIndexes.length > 0)
973
+ throw new Error(`SQLite schema is missing required indexes: ${missingIndexes.join(", ")}`);
1069
974
  const schemaVersion = (
1070
975
  database
1071
976
  .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
1072
977
  .get("schema_version") as { value?: string } | undefined
1073
978
  )?.value;
1074
979
  if (schemaVersion !== readModelSchemaVersion)
1075
- throw new Error(`SQLite schema version must be ${readModelSchemaVersion}; found ${schemaVersion ?? "missing"}`);
980
+ throw new Error(
981
+ `SQLite schema version must be ${readModelSchemaVersion}; found ${schemaVersion ?? "missing"}`,
982
+ );
1076
983
  return {
1077
984
  schemaVersion,
1078
985
  tableCount: tables.length,
1079
986
  viewCount: views.length,
1080
- indexCount: objects.filter((object) => object.type === "index" && !object.name.startsWith("sqlite_")).length,
987
+ indexCount: objects.filter(
988
+ (object) => object.type === "index" && !object.name.startsWith("sqlite_"),
989
+ ).length,
1081
990
  };
1082
991
  } finally {
1083
992
  database.close();
@@ -1087,6 +996,7 @@ export function validateSqliteReadModelSchema(path: string): {
1087
996
  /** Recreates the disposable comparison read model from ZAIP records and configuration. */
1088
997
  export async function rebuildSqliteReadModel(
1089
998
  workspaceRoot: string,
999
+ options: { forceCanonical?: boolean } = {},
1090
1000
  ): Promise<{
1091
1001
  files: number;
1092
1002
  path: string;
@@ -1099,14 +1009,27 @@ export async function rebuildSqliteReadModel(
1099
1009
  const sourceSnapshot = await sourceFingerprint(workspaceRoot);
1100
1010
  const reportLaunchProgress = (phase: string, percent: number, detail: string) => {
1101
1011
  if (process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS !== "1") return;
1102
- process.stderr.write(`${JSON.stringify({ event: "dashboard_sqlite_rebuild_progress", phase, percent, detail })}\n`);
1012
+ process.stderr.write(
1013
+ `${JSON.stringify({ event: "dashboard_sqlite_rebuild_progress", phase, percent, detail })}\n`,
1014
+ );
1103
1015
  };
1104
1016
  const { fingerprint } = sourceSnapshot;
1105
1017
  const fingerprintMs = Math.round(performance.now() - fingerprintStartedAt);
1106
- const existingFiles = readSnapshotFileCount(path, fingerprint);
1018
+ const existingFiles = readSnapshotFileCount(
1019
+ path,
1020
+ fingerprint,
1021
+ readModelSchemaVersion,
1022
+ hasRequiredFirstFoldPayloads,
1023
+ );
1107
1024
  if (existingFiles !== null) {
1108
1025
  validateSqliteReadModelSchema(path);
1109
- return { files: existingFiles, path, cache: "generation", fingerprint_ms: fingerprintMs, materialize_ms: 0 };
1026
+ return {
1027
+ files: existingFiles,
1028
+ path,
1029
+ cache: "generation",
1030
+ fingerprint_ms: fingerprintMs,
1031
+ materialize_ms: 0,
1032
+ };
1110
1033
  }
1111
1034
 
1112
1035
  // The launcher intentionally gives every live server a private database path
@@ -1115,9 +1038,19 @@ export async function rebuildSqliteReadModel(
1115
1038
  // generation path. A normal fresh-port handoff can therefore be warm.
1116
1039
  const cachePath = join(
1117
1040
  process.env.ZAIP_SQLITE_CACHE_DIR ?? join(dirname(path), "cache"),
1118
- `v${readModelSchemaVersion}-${createHash("sha256").update(fingerprint).digest("hex")}.sqlite`,
1041
+ // The cache payload is part of the immutable generation contract. Keep
1042
+ // a separate namespace when that payload changes, so a schema-compatible
1043
+ // older seed cannot bypass the new all-time precomputation.
1044
+ // Keep the immutable name short enough that SQLite can create its
1045
+ // temporary journal beside deep Windows fixture/workspace paths.
1046
+ `v${readModelSchemaVersion}-ff9-${createHash("sha256").update(fingerprint).digest("hex")}.sqlite`,
1047
+ );
1048
+ const cachedFiles = readSnapshotFileCount(
1049
+ cachePath,
1050
+ fingerprint,
1051
+ readModelSchemaVersion,
1052
+ hasRequiredFirstFoldPayloads,
1119
1053
  );
1120
- const cachedFiles = readSnapshotFileCount(cachePath, fingerprint);
1121
1054
  if (cachedFiles !== null) {
1122
1055
  const materializeStartedAt = performance.now();
1123
1056
  await materializeSnapshot(cachePath, path);
@@ -1135,8 +1068,12 @@ export async function rebuildSqliteReadModel(
1135
1068
  // reconstruction merely because a record changed since the last snapshot.
1136
1069
  // The launcher labels this state as updating and starts an isolated refresh;
1137
1070
  // this process still serves only a schema-validated immutable seed.
1138
- if (process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1") {
1139
- const fallback = await mostRecentCompatibleSnapshot(dirname(cachePath));
1071
+ if (!options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1") {
1072
+ const fallback = await mostRecentCompatibleSnapshot(
1073
+ dirname(cachePath),
1074
+ readModelSchemaVersion,
1075
+ hasRequiredFirstFoldPayloads,
1076
+ );
1140
1077
  if (fallback !== null) {
1141
1078
  const materializeStartedAt = performance.now();
1142
1079
  await materializeSnapshot(fallback.path, path);
@@ -1150,19 +1087,23 @@ export async function rebuildSqliteReadModel(
1150
1087
  }
1151
1088
  }
1152
1089
 
1153
- reportLaunchProgress("source_inventory", 52, `Encontradas ${sourceSnapshot.candidates.length} fontes canônicas.`);
1090
+ reportLaunchProgress(
1091
+ "source_inventory",
1092
+ 52,
1093
+ `Encontradas ${sourceSnapshot.candidates.length} fontes canônicas.`,
1094
+ );
1154
1095
  await mkdir(dirname(cachePath), { recursive: true });
1155
- const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
1096
+ const temporaryPath = join(dirname(cachePath), `.snapshot-${randomUUID()}.tmp`);
1156
1097
  const database = new DatabaseSync(temporaryPath);
1157
1098
  let completedFiles = 0;
1158
1099
  try {
1159
1100
  database.exec("PRAGMA journal_mode=DELETE; BEGIN IMMEDIATE;");
1160
1101
  try {
1161
1102
  database.exec(`
1162
- CREATE TEMP TABLE files (path TEXT PRIMARY KEY, content TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, content_sha256 TEXT NOT NULL, artifact_kind TEXT, artifact_id TEXT, artifact_occurred_at TEXT, artifact_occurred_day_sp TEXT);
1103
+ CREATE TEMP TABLE files (path TEXT PRIMARY KEY, content TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, content_sha256 TEXT NOT NULL, artifact_kind TEXT, artifact_id TEXT, artifact_occurred_at TEXT, artifact_occurred_day_sp TEXT, artifact_task_id TEXT, artifact_change_id TEXT, artifact_run_id TEXT);
1163
1104
  CREATE TEMP TABLE read_model_source_state (source_path TEXT PRIMARY KEY, source_hash TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, source_kind TEXT NOT NULL, last_processed_at TEXT NOT NULL);
1164
1105
  CREATE TEMP TABLE read_model_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1165
- CREATE TEMP TABLE canonical_work_items (entity_kind TEXT NOT NULL, id TEXT NOT NULL, title TEXT, status TEXT, stage TEXT, risk TEXT, occurred_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, PRIMARY KEY(entity_kind, id));
1106
+ CREATE TEMP TABLE canonical_work_items (entity_kind TEXT NOT NULL, id TEXT NOT NULL, title TEXT, status TEXT, stage TEXT, risk TEXT, spec_id TEXT, change_id TEXT, completed_run_id TEXT, occurred_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, PRIMARY KEY(entity_kind, id));
1166
1107
  CREATE TEMP TABLE canonical_executions (id TEXT PRIMARY KEY, status TEXT, kind TEXT, origin_type TEXT, origin_id TEXT, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, finished_at TEXT, source_path TEXT NOT NULL);
1167
1108
  CREATE TEMP TABLE canonical_changes (id TEXT PRIMARY KEY, title TEXT NOT NULL, status TEXT NOT NULL, risk TEXT NOT NULL, source_type TEXT NOT NULL, source_id TEXT NOT NULL, source_spec_id TEXT, source_prd_id TEXT, requirement_ids_json TEXT NOT NULL, acceptance_ids_json TEXT NOT NULL, task_ids_json TEXT NOT NULL, result TEXT, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, source_path TEXT NOT NULL);
1168
1109
  CREATE TEMP TABLE canonical_decisions (id TEXT PRIMARY KEY, title TEXT NOT NULL, origin TEXT NOT NULL, scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, source_path TEXT NOT NULL);
@@ -1191,6 +1132,7 @@ export async function rebuildSqliteReadModel(
1191
1132
  occurred_at TEXT, occurred_day_sp TEXT, execution_kind TEXT, entrypoint TEXT, work_type TEXT, phase TEXT, purpose TEXT, status TEXT, activity_kind TEXT,
1192
1133
  zaip_agent TEXT, zaip_agent_persona TEXT, zaip_agent_job TEXT, zaip_agent_role TEXT, agent_catalog_id TEXT, agent_catalog_hash TEXT,
1193
1134
  client TEXT, provider TEXT, model TEXT, service_tier TEXT, reasoning_effort TEXT,
1135
+ developer_account_id TEXT, developer_login TEXT, developer_attribution_source TEXT,
1194
1136
  source_duration_ms REAL, source_started_at TEXT, source_finished_at TEXT, duration_ms REAL, duration_provenance TEXT, input_tokens REAL, cached_input_tokens REAL, output_tokens REAL, reasoning_tokens REAL, total_tokens REAL, token_source TEXT, usage_event_count INTEGER, llm_call_count INTEGER,
1195
1137
  cost_usd REAL, cost_brl REAL, cost_method TEXT, pricing_model TEXT, pricing_source TEXT, fx_rate REAL, source_event_count INTEGER,
1196
1138
  root_run_duration REAL, sub_run_duration REAL,
@@ -1217,7 +1159,7 @@ export async function rebuildSqliteReadModel(
1217
1159
  PRIMARY KEY(activity_row_id, occurred_day_sp)
1218
1160
  );`);
1219
1161
  const insert = database.prepare(
1220
- "INSERT INTO files(path, content, mtime_ms, size_bytes, content_sha256, artifact_kind, artifact_id, artifact_occurred_at, artifact_occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1162
+ "INSERT INTO files(path, content, mtime_ms, size_bytes, content_sha256, artifact_kind, artifact_id, artifact_occurred_at, artifact_occurred_day_sp, artifact_task_id, artifact_change_id, artifact_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1221
1163
  );
1222
1164
  const insertSourceState = database.prepare(
1223
1165
  "INSERT INTO read_model_source_state(source_path, source_hash, mtime_ms, size_bytes, source_kind, last_processed_at) VALUES (?, ?, ?, ?, ?, ?)",
@@ -1225,11 +1167,17 @@ export async function rebuildSqliteReadModel(
1225
1167
  let files = 0;
1226
1168
  const skipped = 0;
1227
1169
  const sourceReadBatchSize = 32;
1228
- for (let offset = 0; offset < sourceSnapshot.candidates.length; offset += sourceReadBatchSize) {
1170
+ for (
1171
+ let offset = 0;
1172
+ offset < sourceSnapshot.candidates.length;
1173
+ offset += sourceReadBatchSize
1174
+ ) {
1229
1175
  const batch = sourceSnapshot.candidates.slice(offset, offset + sourceReadBatchSize);
1230
1176
  // Parallel reads reduce cold I/O latency, while the following ordered
1231
1177
  // loop retains deterministic SQLite inserts and canonical projections.
1232
- const contents = await Promise.all(batch.map((candidate) => readFile(candidate.path, "utf8")));
1178
+ const contents = await Promise.all(
1179
+ batch.map((candidate) => readFile(candidate.path, "utf8")),
1180
+ );
1233
1181
  for (let index = 0; index < batch.length; index += 1) {
1234
1182
  const candidate = batch[index];
1235
1183
  const content = contents[index];
@@ -1240,7 +1188,7 @@ export async function rebuildSqliteReadModel(
1240
1188
  ? "run"
1241
1189
  : source.includes("/artifacts/evidence/")
1242
1190
  ? "evidence"
1243
- : source.includes("/artifacts/validations/")
1191
+ : source.includes("/artifacts/validations/") || source.includes("/artifacts/tests/")
1244
1192
  ? "validation"
1245
1193
  : source.startsWith(".agents/config/")
1246
1194
  ? "config"
@@ -1256,6 +1204,9 @@ export async function rebuildSqliteReadModel(
1256
1204
  artifact?.id ?? null,
1257
1205
  artifact?.occurredAt ?? null,
1258
1206
  artifact?.occurredDaySp ?? null,
1207
+ artifact?.taskId ?? null,
1208
+ artifact?.changeId ?? null,
1209
+ artifact?.runId ?? null,
1259
1210
  );
1260
1211
  insertSourceState.run(
1261
1212
  source,
@@ -1267,8 +1218,13 @@ export async function rebuildSqliteReadModel(
1267
1218
  );
1268
1219
  files += 1;
1269
1220
  }
1270
- const completedPercent = 52 + Math.round(((offset + batch.length) / sourceSnapshot.candidates.length) * 20);
1271
- reportLaunchProgress("source_import", completedPercent, `Lidas ${Math.min(offset + batch.length, sourceSnapshot.candidates.length)} de ${sourceSnapshot.candidates.length} fontes.`);
1221
+ const completedPercent =
1222
+ 52 + Math.round(((offset + batch.length) / sourceSnapshot.candidates.length) * 20);
1223
+ reportLaunchProgress(
1224
+ "source_import",
1225
+ completedPercent,
1226
+ `Lidas ${Math.min(offset + batch.length, sourceSnapshot.candidates.length)} de ${sourceSnapshot.candidates.length} fontes.`,
1227
+ );
1272
1228
  }
1273
1229
  reportLaunchProgress("projection_build", 74, "Organizando fatos e projeções do banco local.");
1274
1230
  const canonicalEntities = readCanonicalEntities(
@@ -1277,7 +1233,7 @@ export async function rebuildSqliteReadModel(
1277
1233
  .all() as Array<{ path: string; content: string }>,
1278
1234
  );
1279
1235
  const insertCanonicalWorkItem = database.prepare(
1280
- "INSERT INTO canonical_work_items(entity_kind, id, title, status, stage, risk, occurred_at, occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1236
+ "INSERT INTO canonical_work_items(entity_kind, id, title, status, stage, risk, spec_id, change_id, completed_run_id, occurred_at, occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1281
1237
  );
1282
1238
  for (const entity of canonicalEntities.values()) {
1283
1239
  if (
@@ -1293,6 +1249,9 @@ export async function rebuildSqliteReadModel(
1293
1249
  entity.status,
1294
1250
  entity.stage,
1295
1251
  entity.risk,
1252
+ entity.specId,
1253
+ entity.changeId,
1254
+ entity.completedRunId,
1296
1255
  entity.occurredAt,
1297
1256
  saoPauloDay(entity.occurredAt),
1298
1257
  );
@@ -1390,6 +1349,56 @@ export async function rebuildSqliteReadModel(
1390
1349
  "SELECT path, content, content_sha256 FROM files WHERE path LIKE '.agents/records/runs/%.jsonl' ORDER BY path",
1391
1350
  )
1392
1351
  .all() as Array<{ path: string; content: string; content_sha256: string }>;
1352
+ // Historical attribution is an append-only, self-verifying overlay. It
1353
+ // never changes a Run stream and therefore participates only after the
1354
+ // native identity carried by the Run itself.
1355
+ const historicalDeveloperManifests = (
1356
+ database
1357
+ .prepare(
1358
+ "SELECT content FROM files WHERE path LIKE '.agents/records/lifecycle/historical-developer-attribution/%.json'",
1359
+ )
1360
+ .all() as Array<{ content: string }>
1361
+ ).flatMap((row) => {
1362
+ try {
1363
+ return [JSON.parse(row.content)];
1364
+ } catch {
1365
+ return [];
1366
+ }
1367
+ });
1368
+ const resolveDeveloper = (row: { path: string; content: string }, identity: unknown) => {
1369
+ const native = recordValue(identity);
1370
+ if (
1371
+ native !== null &&
1372
+ typeof native.account_id === "string" &&
1373
+ typeof native.login_snapshot === "string"
1374
+ )
1375
+ return {
1376
+ accountId: native.account_id,
1377
+ login: native.login_snapshot,
1378
+ source:
1379
+ typeof native.attribution_source === "string" ? native.attribution_source : "native",
1380
+ };
1381
+ for (const candidate of historicalDeveloperManifests) {
1382
+ try {
1383
+ const resolved = resolveHistoricalDeveloperAttribution({
1384
+ manifest: candidate,
1385
+ run_id: String(JSON.parse(row.content.split(/\r?\n/u)[0] ?? "{}").run_id ?? ""),
1386
+ path: row.path,
1387
+ content: row.content,
1388
+ });
1389
+ if (resolved !== null)
1390
+ return {
1391
+ accountId: resolved.account_id,
1392
+ login: resolved.login_snapshot,
1393
+ source: resolved.attribution_source,
1394
+ };
1395
+ } catch {
1396
+ // A corrupt or non-matching historical manifest is not an
1397
+ // attribution source. The consumer remains fail-closed.
1398
+ }
1399
+ }
1400
+ return null;
1401
+ };
1393
1402
  const folds = new Map(
1394
1403
  runRows.map((row) => {
1395
1404
  const fold = foldRunV2Records(parseRunV2JsonLines(row.content));
@@ -1462,6 +1471,9 @@ export async function rebuildSqliteReadModel(
1462
1471
  "model",
1463
1472
  "service_tier",
1464
1473
  "reasoning_effort",
1474
+ "developer_account_id",
1475
+ "developer_login",
1476
+ "developer_attribution_source",
1465
1477
  "source_duration_ms",
1466
1478
  "source_started_at",
1467
1479
  "source_finished_at",
@@ -1500,10 +1512,11 @@ export async function rebuildSqliteReadModel(
1500
1512
  "INSERT INTO canonical_run_index(run_id, occurred_at, occurred_day_sp, task_id, phase, run_role, duration_ms, source_event_count, zaip_agent, provider, model, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1501
1513
  );
1502
1514
  let unassistedActivityCount = 0;
1503
- let excludedUnknownTokenRunCount = 0;
1515
+ const excludedUnknownTokenRunCount = 0;
1504
1516
  const temporalIntervals: TemporalInterval[] = [];
1505
1517
  for (const { row, fold } of folds.values()) {
1506
1518
  const started = fold.run;
1519
+ const developer = resolveDeveloper(row, started.developer_identity ?? null);
1507
1520
  const usage = fold.effective_usage_records;
1508
1521
  const agentRunId = started.agent_invocation_id ?? null;
1509
1522
  const isAgentRun = agentRunId !== null;
@@ -1550,13 +1563,9 @@ export async function rebuildSqliteReadModel(
1550
1563
  model,
1551
1564
  fold.status,
1552
1565
  );
1553
- // Keep the analytical fact restricted to measurable usage. Canonical
1554
- // Runs remain untouched and can enter after a later reconciliation.
1555
- // Known zero is measurable; only genuine unknown (NULL) is excluded.
1556
- if (totalTokens === null) {
1557
- excludedUnknownTokenRunCount += 1;
1558
- continue;
1559
- }
1566
+ // Preserve an attributed Run with absent telemetry as an analytical
1567
+ // fact with NULL measures. This makes coverage visibly unknown rather
1568
+ // than silently converting it to a known zero or erasing its owner.
1560
1569
  const profile = agentProfiles.get(zaipAgent) ?? agentProfiles.get(started.agent);
1561
1570
  const workItem =
1562
1571
  started.task_id === null
@@ -1645,6 +1654,9 @@ export async function rebuildSqliteReadModel(
1645
1654
  consolidatedValue(usage.map((record) => record.runtime?.reasoning_effort ?? null)) ??
1646
1655
  started.reasoning_effort ??
1647
1656
  null,
1657
+ developer_account_id: developer?.accountId ?? null,
1658
+ developer_login: developer?.login ?? null,
1659
+ developer_attribution_source: developer?.source ?? null,
1648
1660
  source_duration_ms: durations.source,
1649
1661
  source_started_at: durations.sourceStartedAt,
1650
1662
  source_finished_at: durations.sourceFinishedAt,
@@ -1671,9 +1683,15 @@ export async function rebuildSqliteReadModel(
1671
1683
  payload_json: null,
1672
1684
  } satisfies Record<(typeof columns)[number], string | number | null>;
1673
1685
  canonicalInsertActivity.run(...columns.map((column) => activity[column] ?? null));
1674
- if (totalTokens > 0 && durations.effective !== null && durations.effective >= 0) {
1686
+ if (
1687
+ totalTokens !== null &&
1688
+ totalTokens > 0 &&
1689
+ durations.effective !== null &&
1690
+ durations.effective >= 0
1691
+ ) {
1675
1692
  const startMs = Date.parse(durations.sourceStartedAt ?? started.occurred_at);
1676
- const endMs = Date.parse(durations.sourceFinishedAt ?? "") || startMs + durations.effective;
1693
+ const endMs =
1694
+ Date.parse(durations.sourceFinishedAt ?? "") || startMs + durations.effective;
1677
1695
  if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs)
1678
1696
  temporalIntervals.push({ activityRowId, startMs, endMs });
1679
1697
  else if (durations.effective === 0)
@@ -1726,6 +1744,188 @@ export async function rebuildSqliteReadModel(
1726
1744
  }
1727
1745
  for (const fact of allocateAttributedTokenActiveMs(temporalIntervals))
1728
1746
  insertTemporalFact.run(fact.activityRowId, fact.day, fact.value);
1747
+ // A single authoring sub-Run can create several sibling SPECs. Keep the
1748
+ // Run canonical and attach deterministic shares only to the macro read
1749
+ // model. Payload rows stay outside `activities`, preventing global
1750
+ // Run/Cost totals from counting that Run more than once.
1751
+ const creationSpecsByRun = new Map<string, CanonicalEntity[]>();
1752
+ for (const entity of canonicalEntities.values()) {
1753
+ if (entity.kind !== "spec" || entity.creationRunId === null) continue;
1754
+ creationSpecsByRun.set(entity.creationRunId, [
1755
+ ...(creationSpecsByRun.get(entity.creationRunId) ?? []),
1756
+ entity,
1757
+ ]);
1758
+ }
1759
+ const materializeSharedCreationAllocations = () => {
1760
+ for (const [runId, recipients] of creationSpecsByRun) {
1761
+ if (recipients.length === 0) continue;
1762
+ const source = folds.get(runId);
1763
+ if (source === undefined) continue;
1764
+ const { fold, row } = source;
1765
+ const started = fold.run;
1766
+ const usage = fold.effective_usage_records;
1767
+ const canonicalActivity = database
1768
+ .prepare(
1769
+ "SELECT duration_ms, duration_provenance, source_started_at, source_finished_at FROM zaip_aios_activities WHERE run_id=? AND payload_type IS NULL LIMIT 1",
1770
+ )
1771
+ .get(runId) as
1772
+ | {
1773
+ duration_ms: number | null;
1774
+ duration_provenance: string | null;
1775
+ source_started_at: string | null;
1776
+ source_finished_at: string | null;
1777
+ }
1778
+ | undefined;
1779
+ const cost = aggregateCost(usage, fold, pricingCatalog, fxRate);
1780
+ const recipientIds = recipients.map((recipient) => recipient.id);
1781
+ // The allocator owns the only partition. Read the already-materialized
1782
+ // canonical activity fact after attributed-token-active reconciliation;
1783
+ // never use the wider JSONL session span or a pre-divided value.
1784
+ const sharedCreationDuration = canonicalActivity?.duration_ms ?? null;
1785
+ const allocations = {
1786
+ source_duration_ms: allocateSharedCreationValue(sharedCreationDuration, recipientIds),
1787
+ // Shared creation attribution uses only the observed source interval;
1788
+ // never propagate an effective/legacy aggregate that can include
1789
+ // more than the creation segment.
1790
+ duration_ms: allocateSharedCreationValue(sharedCreationDuration, recipientIds),
1791
+ input_tokens: allocateSharedCreationValue(knownSum(usage, "input"), recipientIds),
1792
+ cached_input_tokens: allocateSharedCreationValue(
1793
+ knownSum(usage, "cached_input"),
1794
+ recipientIds,
1795
+ ),
1796
+ output_tokens: allocateSharedCreationValue(knownSum(usage, "output"), recipientIds),
1797
+ reasoning_tokens: allocateSharedCreationValue(
1798
+ knownSum(usage, "reasoning"),
1799
+ recipientIds,
1800
+ ),
1801
+ total_tokens: allocateSharedCreationValue(knownSum(usage, "total"), recipientIds),
1802
+ cost_usd: allocateSharedCreationValue(cost.usd, recipientIds),
1803
+ cost_brl: allocateSharedCreationValue(cost.brl, recipientIds),
1804
+ };
1805
+ for (const recipient of recipients) {
1806
+ const attributed =
1807
+ usage.find((record) => record.attributed_spec_id === recipient.id) ?? null;
1808
+ const attributedDuration =
1809
+ attributed?.source_started_at && attributed?.source_finished_at
1810
+ ? Math.max(
1811
+ 0,
1812
+ Date.parse(attributed.source_finished_at) -
1813
+ Date.parse(attributed.source_started_at),
1814
+ )
1815
+ : null;
1816
+ const activityRowId = `ACTV-${createHash("sha256")
1817
+ .update(`shared-creation:${runId}:${recipient.id}`)
1818
+ .digest("hex")
1819
+ .slice(0, 24)}`;
1820
+ const lastRecord = fold.records.at(-1);
1821
+ const allocation = {
1822
+ activity_row_id: activityRowId,
1823
+ activity_source_kind: "shared_creation_allocation",
1824
+ activity_key: `shared-creation:${runId}:${recipient.id}`,
1825
+ source_path: row.path,
1826
+ source_hash: row.content_sha256,
1827
+ source_occurred_at: started.occurred_at,
1828
+ recorded_at: lastRecord?.occurred_at ?? started.occurred_at,
1829
+ version_activity_id: null,
1830
+ source_last_event_id: lastRecord?.event_id ?? null,
1831
+ source_last_sequence: lastRecord?.sequence ?? null,
1832
+ work_item_kind: "spec",
1833
+ title: recipient.title,
1834
+ work_item_status: recipient.status,
1835
+ work_stage: recipient.stage,
1836
+ risk: recipient.risk,
1837
+ run_role: "shared_creation_allocation",
1838
+ root_run_id: rootRunId(fold),
1839
+ parent_run_id: started.parent_run_id,
1840
+ agent_run_id: null,
1841
+ root_agent_run_id: null,
1842
+ parent_agent_run_id: null,
1843
+ agent_run_role: null,
1844
+ delegation_depth: started.delegation_depth ?? null,
1845
+ agent_invocation_id: null,
1846
+ root_agent_invocation_id: null,
1847
+ parent_agent_invocation_id: null,
1848
+ invocation_kind: null,
1849
+ execution_id: started.execution_id,
1850
+ task_id: null,
1851
+ spec_id: recipient.id,
1852
+ prd_id: null,
1853
+ change_id: null,
1854
+ conversation_id: started.conversation_id,
1855
+ run_id: started.run_id,
1856
+ occurred_at: started.occurred_at,
1857
+ occurred_day_sp: saoPauloDay(started.occurred_at),
1858
+ execution_kind: started.execution_kind ?? null,
1859
+ entrypoint: started.entrypoint,
1860
+ work_type: started.work_type,
1861
+ phase: started.phase,
1862
+ purpose: started.run_purpose ?? null,
1863
+ status: fold.status,
1864
+ activity_kind: "planning",
1865
+ zaip_agent: started.agent_name ?? started.agent,
1866
+ zaip_agent_persona: null,
1867
+ zaip_agent_job: null,
1868
+ zaip_agent_role: null,
1869
+ agent_catalog_id: null,
1870
+ agent_catalog_hash: catalogHash,
1871
+ client: started.client,
1872
+ provider: null,
1873
+ model: null,
1874
+ service_tier: null,
1875
+ reasoning_effort: started.reasoning_effort ?? null,
1876
+ developer_account_id: null,
1877
+ developer_login: null,
1878
+ developer_attribution_source: null,
1879
+ source_duration_ms:
1880
+ attributedDuration ?? allocations.source_duration_ms.get(recipient.id) ?? null,
1881
+ source_started_at:
1882
+ attributed?.source_started_at ?? canonicalActivity?.source_started_at ?? null,
1883
+ source_finished_at:
1884
+ attributed?.source_finished_at ?? canonicalActivity?.source_finished_at ?? null,
1885
+ duration_ms: attributedDuration ?? allocations.duration_ms.get(recipient.id) ?? null,
1886
+ duration_provenance: attributed
1887
+ ? "explicit_spec_turn_recovery"
1888
+ : "shared_creation_allocation",
1889
+ input_tokens:
1890
+ attributed?.tokens.input ?? allocations.input_tokens.get(recipient.id) ?? null,
1891
+ cached_input_tokens:
1892
+ attributed?.tokens.cached_input ??
1893
+ allocations.cached_input_tokens.get(recipient.id) ??
1894
+ null,
1895
+ output_tokens:
1896
+ attributed?.tokens.output ?? allocations.output_tokens.get(recipient.id) ?? null,
1897
+ reasoning_tokens:
1898
+ attributed?.tokens.reasoning ??
1899
+ allocations.reasoning_tokens.get(recipient.id) ??
1900
+ null,
1901
+ total_tokens:
1902
+ attributed?.tokens.total ?? allocations.total_tokens.get(recipient.id) ?? null,
1903
+ token_source: consolidatedValue(usage.map((record) => record.tokens.source)),
1904
+ usage_event_count: usage.length,
1905
+ llm_call_count: null,
1906
+ cost_usd: attributed?.cost.usd ?? allocations.cost_usd.get(recipient.id) ?? null,
1907
+ cost_brl: attributed?.cost.brl ?? allocations.cost_brl.get(recipient.id) ?? null,
1908
+ cost_method: cost.method,
1909
+ pricing_model: cost.pricingModel,
1910
+ pricing_source: cost.pricingSource,
1911
+ fx_rate: cost.fxRate,
1912
+ source_event_count: fold.records.length,
1913
+ root_run_duration: null,
1914
+ sub_run_duration: null,
1915
+ payload_type: "shared_creation_allocation",
1916
+ payload_json: JSON.stringify({
1917
+ provenance: attributed
1918
+ ? "explicit_spec_turn_recovery"
1919
+ : "shared_creation_allocation",
1920
+ base_duration_provenance: canonicalActivity?.duration_provenance ?? null,
1921
+ creation_run_id: runId,
1922
+ spec_id: recipient.id,
1923
+ }),
1924
+ } satisfies Record<(typeof columns)[number], string | number | null>;
1925
+ canonicalInsertActivity.run(...columns.map((column) => allocation[column] ?? null));
1926
+ }
1927
+ }
1928
+ };
1729
1929
  database.exec(`UPDATE zaip_aios_activities
1730
1930
  SET duration_ms = (
1731
1931
  SELECT SUM(f.attributed_token_active_ms)
@@ -1734,7 +1934,9 @@ export async function rebuildSqliteReadModel(
1734
1934
  ),
1735
1935
  duration_provenance = CASE
1736
1936
  WHEN EXISTS (SELECT 1 FROM attributed_token_active_facts f WHERE f.activity_row_id=zaip_aios_activities.activity_row_id)
1737
- THEN 'attributed_token_active' ELSE NULL END;`);
1937
+ THEN 'attributed_token_active' ELSE NULL END
1938
+ WHERE payload_type IS NULL;`);
1939
+ materializeSharedCreationAllocations();
1738
1940
  const canonicalRunCount = folds.size;
1739
1941
  const invocationCount = (
1740
1942
  database
@@ -1815,7 +2017,7 @@ export async function rebuildSqliteReadModel(
1815
2017
  insertConfiguredModel.run(configuredModel.id);
1816
2018
  insertConfiguredProvider.run(configuredModel.provider);
1817
2019
  }
1818
- const costDimensionAggregates = buildCostDimensionAggregates(database);
2020
+ const costDimensionAggregates = buildCostDimensionAggregates(database, agentProfiles);
1819
2021
  // The rebuild scratch tables above are intentionally TEMP only. Persist
1820
2022
  // their technical inputs as typed payload activities so the on-disk
1821
2023
  // model has one fact plus its three declared dimensions, never hidden
@@ -1828,16 +2030,46 @@ export async function rebuildSqliteReadModel(
1828
2030
  'TECH-' || lower(hex(randomblob(16))), 'technical_payload', '${payloadType}:' || row_key,
1829
2031
  'sqlite://${payloadType}', '', datetime('now'), 'technical', 'technical', '${payloadType}', payload_json
1830
2032
  FROM (${selectSql});`);
1831
- persistPayload("metadata", "SELECT key AS row_key, json_object('key', key, 'value', value) AS payload_json FROM read_model_metadata");
1832
- persistPayload("source_state", "SELECT source_path AS row_key, json_object('source_path', source_path, 'source_hash', source_hash, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'source_kind', source_kind, 'last_processed_at', last_processed_at) AS payload_json FROM read_model_source_state");
1833
- persistPayload("file", "SELECT path AS row_key, json_object('path', path, 'content', content, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'content_sha256', content_sha256, 'artifact_kind', artifact_kind, 'artifact_id', artifact_id, 'artifact_occurred_at', artifact_occurred_at, 'artifact_occurred_day_sp', artifact_occurred_day_sp) AS payload_json FROM files");
1834
- persistPayload("work_item", "SELECT entity_kind || ':' || id AS row_key, json_object('entity_kind', entity_kind, 'id', id, 'title', title, 'status', status, 'stage', stage, 'risk', risk, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp) AS payload_json FROM canonical_work_items");
1835
- persistPayload("execution", "SELECT id AS row_key, json_object('id', id, 'status', status, 'kind', kind, 'origin_type', origin_type, 'origin_id', origin_id, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'finished_at', finished_at, 'source_path', source_path) AS payload_json FROM canonical_executions");
1836
- persistPayload("change", "SELECT id AS row_key, json_object('id', id, 'title', title, 'status', status, 'risk', risk, 'source_type', source_type, 'source_id', source_id, 'source_spec_id', source_spec_id, 'source_prd_id', source_prd_id, 'requirement_ids_json', requirement_ids_json, 'acceptance_ids_json', acceptance_ids_json, 'task_ids_json', task_ids_json, 'result', result, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'source_path', source_path) AS payload_json FROM canonical_changes");
1837
- persistPayload("decision", "SELECT id AS row_key, json_object('id', id, 'title', title, 'origin', origin, 'scope_type', scope_type, 'scope_id', scope_id, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'source_path', source_path) AS payload_json FROM canonical_decisions");
1838
- persistPayload("run_index", "SELECT run_id AS row_key, json_object('run_id', run_id, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp, 'task_id', task_id, 'phase', phase, 'run_role', run_role, 'duration_ms', duration_ms, 'source_event_count', source_event_count, 'zaip_agent', zaip_agent, 'provider', provider, 'model', model, 'status', status) AS payload_json FROM canonical_run_index");
1839
- persistPayload("usage_segment", "SELECT agent_invocation_id || ':' || rowid AS row_key, json_object('agent_invocation_id', agent_invocation_id, 'provider', provider, 'model', model, 'service_tier', service_tier, 'reasoning_effort', reasoning_effort, 'input_tokens', input_tokens, 'cached_input_tokens', cached_input_tokens, 'uncached_input_tokens', uncached_input_tokens, 'output_tokens', output_tokens, 'reasoning_tokens', reasoning_tokens, 'visible_output_tokens', visible_output_tokens, 'total_tokens', total_tokens, 'input_rate_usd_per_million', input_rate_usd_per_million, 'cached_input_rate_usd_per_million', cached_input_rate_usd_per_million, 'output_rate_usd_per_million', output_rate_usd_per_million, 'reasoning_rate_usd_per_million', reasoning_rate_usd_per_million, 'context_input_multiplier', context_input_multiplier, 'context_output_multiplier', context_output_multiplier, 'speed_cost_multiplier', speed_cost_multiplier, 'pricing_scenario', pricing_scenario, 'standard_cost_usd', standard_cost_usd, 'speed_uplift_usd', speed_uplift_usd, 'input_cost_usd', input_cost_usd, 'cached_input_cost_usd', cached_input_cost_usd, 'output_cost_usd', output_cost_usd, 'reasoning_cost_usd', reasoning_cost_usd, 'calculated_cost_usd', calculated_cost_usd, 'recorded_cost_usd', recorded_cost_usd, 'cost_variance_usd', cost_variance_usd, 'cost_usd', cost_usd, 'cost_brl', cost_brl, 'cost_method', cost_method, 'pricing_model', pricing_model, 'pricing_source', pricing_source, 'fx_rate', fx_rate) AS payload_json FROM invocation_usage_segments");
1840
- persistPayload("duration_allocation", "SELECT activity_row_id || ':' || occurred_day_sp AS row_key, json_object('activity_row_id', activity_row_id, 'occurred_day_sp', occurred_day_sp, 'attributed_token_active_ms', attributed_token_active_ms) AS payload_json FROM attributed_token_active_facts");
2033
+ persistPayload(
2034
+ "metadata",
2035
+ "SELECT key AS row_key, json_object('key', key, 'value', value) AS payload_json FROM read_model_metadata",
2036
+ );
2037
+ persistPayload(
2038
+ "source_state",
2039
+ "SELECT source_path AS row_key, json_object('source_path', source_path, 'source_hash', source_hash, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'source_kind', source_kind, 'last_processed_at', last_processed_at) AS payload_json FROM read_model_source_state",
2040
+ );
2041
+ persistPayload(
2042
+ "file",
2043
+ "SELECT path AS row_key, json_object('path', path, 'content', content, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'content_sha256', content_sha256, 'artifact_kind', artifact_kind, 'artifact_id', artifact_id, 'artifact_occurred_at', artifact_occurred_at, 'artifact_occurred_day_sp', artifact_occurred_day_sp, 'artifact_task_id', artifact_task_id, 'artifact_change_id', artifact_change_id, 'artifact_run_id', artifact_run_id) AS payload_json FROM files",
2044
+ );
2045
+ persistPayload(
2046
+ "work_item",
2047
+ "SELECT entity_kind || ':' || id AS row_key, json_object('entity_kind', entity_kind, 'id', id, 'title', title, 'status', status, 'stage', stage, 'risk', risk, 'spec_id', spec_id, 'change_id', change_id, 'completed_run_id', completed_run_id, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp) AS payload_json FROM canonical_work_items",
2048
+ );
2049
+ persistPayload(
2050
+ "execution",
2051
+ "SELECT id AS row_key, json_object('id', id, 'status', status, 'kind', kind, 'origin_type', origin_type, 'origin_id', origin_id, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'finished_at', finished_at, 'source_path', source_path) AS payload_json FROM canonical_executions",
2052
+ );
2053
+ persistPayload(
2054
+ "change",
2055
+ "SELECT id AS row_key, json_object('id', id, 'title', title, 'status', status, 'risk', risk, 'source_type', source_type, 'source_id', source_id, 'source_spec_id', source_spec_id, 'source_prd_id', source_prd_id, 'requirement_ids_json', requirement_ids_json, 'acceptance_ids_json', acceptance_ids_json, 'task_ids_json', task_ids_json, 'result', result, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'source_path', source_path) AS payload_json FROM canonical_changes",
2056
+ );
2057
+ persistPayload(
2058
+ "decision",
2059
+ "SELECT id AS row_key, json_object('id', id, 'title', title, 'origin', origin, 'scope_type', scope_type, 'scope_id', scope_id, 'created_at', created_at, 'occurred_day_sp', occurred_day_sp, 'source_path', source_path) AS payload_json FROM canonical_decisions",
2060
+ );
2061
+ persistPayload(
2062
+ "run_index",
2063
+ "SELECT run_id AS row_key, json_object('run_id', run_id, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp, 'task_id', task_id, 'phase', phase, 'run_role', run_role, 'duration_ms', duration_ms, 'source_event_count', source_event_count, 'zaip_agent', zaip_agent, 'provider', provider, 'model', model, 'status', status) AS payload_json FROM canonical_run_index",
2064
+ );
2065
+ persistPayload(
2066
+ "usage_segment",
2067
+ "SELECT agent_invocation_id || ':' || rowid AS row_key, json_object('agent_invocation_id', agent_invocation_id, 'provider', provider, 'model', model, 'service_tier', service_tier, 'reasoning_effort', reasoning_effort, 'input_tokens', input_tokens, 'cached_input_tokens', cached_input_tokens, 'uncached_input_tokens', uncached_input_tokens, 'output_tokens', output_tokens, 'reasoning_tokens', reasoning_tokens, 'visible_output_tokens', visible_output_tokens, 'total_tokens', total_tokens, 'input_rate_usd_per_million', input_rate_usd_per_million, 'cached_input_rate_usd_per_million', cached_input_rate_usd_per_million, 'output_rate_usd_per_million', output_rate_usd_per_million, 'reasoning_rate_usd_per_million', reasoning_rate_usd_per_million, 'context_input_multiplier', context_input_multiplier, 'context_output_multiplier', context_output_multiplier, 'speed_cost_multiplier', speed_cost_multiplier, 'pricing_scenario', pricing_scenario, 'standard_cost_usd', standard_cost_usd, 'speed_uplift_usd', speed_uplift_usd, 'input_cost_usd', input_cost_usd, 'cached_input_cost_usd', cached_input_cost_usd, 'output_cost_usd', output_cost_usd, 'reasoning_cost_usd', reasoning_cost_usd, 'calculated_cost_usd', calculated_cost_usd, 'recorded_cost_usd', recorded_cost_usd, 'cost_variance_usd', cost_variance_usd, 'cost_usd', cost_usd, 'cost_brl', cost_brl, 'cost_method', cost_method, 'pricing_model', pricing_model, 'pricing_source', pricing_source, 'fx_rate', fx_rate) AS payload_json FROM invocation_usage_segments",
2068
+ );
2069
+ persistPayload(
2070
+ "duration_allocation",
2071
+ "SELECT activity_row_id || ':' || occurred_day_sp AS row_key, json_object('activity_row_id', activity_row_id, 'occurred_day_sp', occurred_day_sp, 'attributed_token_active_ms', attributed_token_active_ms) AS payload_json FROM attributed_token_active_facts",
2072
+ );
1841
2073
  // Configuration-backed dimensions contain the current selectable
1842
2074
  // catalog even when no activity has used an option yet. Historical
1843
2075
  // values remain faithfully recorded in the fact without becoming
@@ -1870,6 +2102,27 @@ export async function rebuildSqliteReadModel(
1870
2102
  );
1871
2103
  }
1872
2104
  }
2105
+ for (const [dimension, projection] of Object.entries(costDimensionAggregates.remote)) {
2106
+ for (const row of projection.rows) {
2107
+ insertCostDimensionAggregate.run(
2108
+ `TECH-${randomUUID().toLowerCase()}`,
2109
+ `cost_dimension_aggregate_v1:${dimension}:${row.key}`,
2110
+ `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2111
+ JSON.stringify({ schema_version: 1, dimension, row_kind: "row", ...row }),
2112
+ );
2113
+ }
2114
+ insertCostDimensionAggregate.run(
2115
+ `TECH-${randomUUID().toLowerCase()}`,
2116
+ `cost_dimension_aggregate_v1:${dimension}:__totals__`,
2117
+ `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2118
+ JSON.stringify({
2119
+ schema_version: 1,
2120
+ dimension,
2121
+ row_kind: "totals",
2122
+ ...projection.totals,
2123
+ }),
2124
+ );
2125
+ }
1873
2126
  const viewSql = `
1874
2127
  CREATE INDEX zaip_activities_agent_run ON zaip_aios_activities(activity_source_kind, agent_run_id, occurred_day_sp, occurred_at DESC);
1875
2128
  CREATE INDEX zaip_activities_relationships ON zaip_aios_activities(task_id, spec_id, prd_id, execution_id, conversation_id);
@@ -1877,7 +2130,7 @@ export async function rebuildSqliteReadModel(
1877
2130
  CREATE INDEX zaip_activities_spec_day ON zaip_aios_activities(payload_type, spec_id, occurred_day_sp) WHERE spec_id IS NOT NULL;
1878
2131
  CREATE INDEX zaip_activities_prd_day ON zaip_aios_activities(payload_type, prd_id, occurred_day_sp) WHERE prd_id IS NOT NULL;
1879
2132
  CREATE INDEX zaip_activities_agent ON zaip_aios_activities(zaip_agent, zaip_agent_persona, zaip_agent_job, provider, model);
1880
- CREATE UNIQUE INDEX zaip_activities_run_id ON zaip_aios_activities(run_id);
2133
+ CREATE UNIQUE INDEX zaip_activities_run_id ON zaip_aios_activities(run_id) WHERE payload_type IS NULL;
1881
2134
  CREATE INDEX zaip_activities_run_hierarchy ON zaip_aios_activities(root_run_id, parent_run_id, run_role);
1882
2135
  CREATE INDEX zaip_activities_period ON zaip_aios_activities(occurred_day_sp, occurred_at DESC);
1883
2136
  -- Dimension residuals first restrict the visible period, then group
@@ -1887,17 +2140,17 @@ export async function rebuildSqliteReadModel(
1887
2140
  WHERE agent_invocation_id IS NOT NULL;
1888
2141
  CREATE INDEX zaip_activities_status ON zaip_aios_activities(activity_source_kind, status, phase);
1889
2142
  CREATE INDEX zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key);
1890
- CREATE VIEW files AS SELECT json_extract(payload_json, '$.path') AS path, json_extract(payload_json, '$.content') AS content, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.content_sha256') AS content_sha256, json_extract(payload_json, '$.artifact_kind') AS artifact_kind, json_extract(payload_json, '$.artifact_id') AS artifact_id, json_extract(payload_json, '$.artifact_occurred_at') AS artifact_occurred_at, json_extract(payload_json, '$.artifact_occurred_day_sp') AS artifact_occurred_day_sp FROM zaip_aios_activities WHERE payload_type='file';
2143
+ CREATE VIEW files AS SELECT json_extract(payload_json, '$.path') AS path, json_extract(payload_json, '$.content') AS content, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.content_sha256') AS content_sha256, json_extract(payload_json, '$.artifact_kind') AS artifact_kind, json_extract(payload_json, '$.artifact_id') AS artifact_id, json_extract(payload_json, '$.artifact_occurred_at') AS artifact_occurred_at, json_extract(payload_json, '$.artifact_occurred_day_sp') AS artifact_occurred_day_sp, json_extract(payload_json, '$.artifact_task_id') AS artifact_task_id, json_extract(payload_json, '$.artifact_change_id') AS artifact_change_id, json_extract(payload_json, '$.artifact_run_id') AS artifact_run_id FROM zaip_aios_activities WHERE payload_type='file';
1891
2144
  CREATE VIEW read_model_source_state AS SELECT json_extract(payload_json, '$.source_path') AS source_path, json_extract(payload_json, '$.source_hash') AS source_hash, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.source_kind') AS source_kind, json_extract(payload_json, '$.last_processed_at') AS last_processed_at FROM zaip_aios_activities WHERE payload_type='source_state';
1892
2145
  CREATE VIEW read_model_metadata AS SELECT json_extract(payload_json, '$.key') AS key, json_extract(payload_json, '$.value') AS value FROM zaip_aios_activities WHERE payload_type='metadata';
1893
- CREATE VIEW canonical_work_items AS SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind, json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.stage') AS stage, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp FROM zaip_aios_activities WHERE payload_type='work_item';
2146
+ CREATE VIEW canonical_work_items AS SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind, json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.stage') AS stage, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.spec_id') AS spec_id, json_extract(payload_json, '$.change_id') AS change_id, json_extract(payload_json, '$.completed_run_id') AS completed_run_id, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp FROM zaip_aios_activities WHERE payload_type='work_item';
1894
2147
  CREATE VIEW canonical_executions AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.kind') AS kind, json_extract(payload_json, '$.origin_type') AS origin_type, json_extract(payload_json, '$.origin_id') AS origin_id, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.finished_at') AS finished_at, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='execution';
1895
2148
  CREATE VIEW canonical_changes AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.source_type') AS source_type, json_extract(payload_json, '$.source_id') AS source_id, json_extract(payload_json, '$.source_spec_id') AS source_spec_id, json_extract(payload_json, '$.source_prd_id') AS source_prd_id, json_extract(payload_json, '$.requirement_ids_json') AS requirement_ids_json, json_extract(payload_json, '$.acceptance_ids_json') AS acceptance_ids_json, json_extract(payload_json, '$.task_ids_json') AS task_ids_json, json_extract(payload_json, '$.result') AS result, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='change';
1896
2149
  CREATE VIEW canonical_decisions AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.origin') AS origin, json_extract(payload_json, '$.scope_type') AS scope_type, json_extract(payload_json, '$.scope_id') AS scope_id, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='decision';
1897
2150
  CREATE VIEW canonical_run_index AS SELECT json_extract(payload_json, '$.run_id') AS run_id, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.task_id') AS task_id, json_extract(payload_json, '$.phase') AS phase, json_extract(payload_json, '$.run_role') AS run_role, json_extract(payload_json, '$.duration_ms') AS duration_ms, json_extract(payload_json, '$.source_event_count') AS source_event_count, json_extract(payload_json, '$.zaip_agent') AS zaip_agent, json_extract(payload_json, '$.provider') AS provider, json_extract(payload_json, '$.model') AS model, json_extract(payload_json, '$.status') AS status FROM zaip_aios_activities WHERE payload_type='run_index';
1898
2151
  CREATE VIEW invocation_usage_segments AS SELECT json_extract(payload_json, '$.agent_invocation_id') AS agent_invocation_id, json_extract(payload_json, '$.provider') AS provider, json_extract(payload_json, '$.model') AS model, json_extract(payload_json, '$.service_tier') AS service_tier, json_extract(payload_json, '$.reasoning_effort') AS reasoning_effort, json_extract(payload_json, '$.input_tokens') AS input_tokens, json_extract(payload_json, '$.cached_input_tokens') AS cached_input_tokens, json_extract(payload_json, '$.uncached_input_tokens') AS uncached_input_tokens, json_extract(payload_json, '$.output_tokens') AS output_tokens, json_extract(payload_json, '$.reasoning_tokens') AS reasoning_tokens, json_extract(payload_json, '$.visible_output_tokens') AS visible_output_tokens, json_extract(payload_json, '$.total_tokens') AS total_tokens, json_extract(payload_json, '$.input_rate_usd_per_million') AS input_rate_usd_per_million, json_extract(payload_json, '$.cached_input_rate_usd_per_million') AS cached_input_rate_usd_per_million, json_extract(payload_json, '$.output_rate_usd_per_million') AS output_rate_usd_per_million, json_extract(payload_json, '$.reasoning_rate_usd_per_million') AS reasoning_rate_usd_per_million, json_extract(payload_json, '$.context_input_multiplier') AS context_input_multiplier, json_extract(payload_json, '$.context_output_multiplier') AS context_output_multiplier, json_extract(payload_json, '$.speed_cost_multiplier') AS speed_cost_multiplier, json_extract(payload_json, '$.pricing_scenario') AS pricing_scenario, json_extract(payload_json, '$.standard_cost_usd') AS standard_cost_usd, json_extract(payload_json, '$.speed_uplift_usd') AS speed_uplift_usd, json_extract(payload_json, '$.input_cost_usd') AS input_cost_usd, json_extract(payload_json, '$.cached_input_cost_usd') AS cached_input_cost_usd, json_extract(payload_json, '$.output_cost_usd') AS output_cost_usd, json_extract(payload_json, '$.reasoning_cost_usd') AS reasoning_cost_usd, json_extract(payload_json, '$.calculated_cost_usd') AS calculated_cost_usd, json_extract(payload_json, '$.recorded_cost_usd') AS recorded_cost_usd, json_extract(payload_json, '$.cost_variance_usd') AS cost_variance_usd, json_extract(payload_json, '$.cost_usd') AS cost_usd, json_extract(payload_json, '$.cost_brl') AS cost_brl, json_extract(payload_json, '$.cost_method') AS cost_method, json_extract(payload_json, '$.pricing_model') AS pricing_model, json_extract(payload_json, '$.pricing_source') AS pricing_source, json_extract(payload_json, '$.fx_rate') AS fx_rate FROM zaip_aios_activities WHERE payload_type='usage_segment';
1899
2152
  CREATE VIEW attributed_token_active_facts AS SELECT json_extract(payload_json, '$.activity_row_id') AS activity_row_id, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.attributed_token_active_ms') AS attributed_token_active_ms FROM zaip_aios_activities WHERE payload_type='duration_allocation';
1900
- CREATE VIEW canonical_artifacts AS SELECT artifact_kind, artifact_id, artifact_occurred_at AS occurred_at, artifact_occurred_day_sp AS occurred_day_sp, path AS source_path FROM files WHERE artifact_kind IS NOT NULL AND artifact_occurred_at IS NOT NULL;
2153
+ CREATE VIEW canonical_artifacts AS SELECT artifact_kind, artifact_id, artifact_occurred_at AS occurred_at, artifact_occurred_day_sp AS occurred_day_sp, artifact_task_id AS task_id, artifact_change_id AS change_id, artifact_run_id AS run_id, path AS source_path FROM files WHERE artifact_kind IS NOT NULL AND artifact_occurred_at IS NOT NULL;
1901
2154
  CREATE VIEW activities AS
1902
2155
  SELECT a.*, f.attributed_token_active_ms
1903
2156
  FROM zaip_aios_activities a
@@ -1906,6 +2159,19 @@ export async function rebuildSqliteReadModel(
1906
2159
  FROM attributed_token_active_facts GROUP BY activity_row_id
1907
2160
  ) f USING(activity_row_id)
1908
2161
  WHERE a.payload_type IS NULL;
2162
+ -- Macro-only allocations are persisted as technical payloads to keep
2163
+ -- the global Run population unique. SPEC summaries opt into this
2164
+ -- projection explicitly; all other activity and cost views do not.
2165
+ CREATE VIEW shared_creation_allocations AS
2166
+ SELECT a.*, NULL AS attributed_token_active_ms
2167
+ FROM zaip_aios_activities a WHERE payload_type='shared_creation_allocation';
2168
+ CREATE VIEW macro_spec_activities AS
2169
+ SELECT a.* FROM activities a WHERE a.spec_id IS NOT NULL
2170
+ AND NOT EXISTS (
2171
+ SELECT 1 FROM shared_creation_allocations allocation
2172
+ WHERE allocation.spec_id=a.spec_id AND allocation.run_id=a.run_id
2173
+ )
2174
+ UNION ALL SELECT * FROM shared_creation_allocations;
1909
2175
  CREATE VIEW attributed_token_active AS
1910
2176
  SELECT f.activity_row_id, f.occurred_day_sp, f.attributed_token_active_ms,
1911
2177
  a.run_id, a.execution_id, a.task_id, a.spec_id, a.prd_id, a.conversation_id,
@@ -2041,7 +2307,10 @@ export async function rebuildSqliteReadModel(
2041
2307
  SUM(sub_run_duration) AS total_sub_run_duration,
2042
2308
  SUM(total_tokens) AS total_tokens,
2043
2309
  SUM(cost_usd) AS total_cost_usd,
2044
- SUM(cost_brl) AS total_cost_brl
2310
+ SUM(cost_brl) AS total_cost_brl,
2311
+ COUNT(total_tokens) AS token_known_runs,
2312
+ COUNT(cost_usd) AS usd_known_runs,
2313
+ COUNT(cost_brl) AS brl_known_runs
2045
2314
  FROM activities
2046
2315
  GROUP BY date(COALESCE(source_started_at, source_occurred_at, occurred_at), '-3 hours')
2047
2316
  ), cost_monitor_financial_daily AS (
@@ -2152,7 +2421,10 @@ export async function rebuildSqliteReadModel(
2152
2421
  o.total_duration AS total_duration,
2153
2422
  f.total_tokens,
2154
2423
  f.total_cost_usd,
2155
- f.total_cost_brl
2424
+ f.total_cost_brl,
2425
+ i.token_known_runs,
2426
+ i.usd_known_runs,
2427
+ i.brl_known_runs
2156
2428
  FROM days d
2157
2429
  LEFT JOIN work_item_daily w USING(day)
2158
2430
  LEFT JOIN artifact_daily a USING(day)
@@ -2181,7 +2453,7 @@ export async function rebuildSqliteReadModel(
2181
2453
  -- population rather than Operations' broader Run/activity totals.
2182
2454
  CREATE VIEW cost_monitor_overview AS SELECT period_kind, period_key, cost_monitor_invocation_count AS total_run_count, cost_monitor_invocation_count AS invocation_count, llm_call_count, source_event_count, cost_monitor_total_duration, total_tokens, total_cost_usd, total_cost_brl FROM overview;
2183
2455
  CREATE VIEW task_summary AS SELECT t.id, t.title, t.status, t.risk, t.occurred_at, t.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM tasks t LEFT JOIN activities i ON i.task_id = t.id GROUP BY t.id, t.title, t.status, t.risk, t.occurred_at, t.occurred_day_sp;
2184
- CREATE VIEW spec_summary AS SELECT s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM specs s LEFT JOIN activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2456
+ CREATE VIEW spec_summary AS SELECT s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM specs s LEFT JOIN macro_spec_activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2185
2457
  CREATE VIEW prd_summary AS SELECT p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM prds p LEFT JOIN activities i ON i.prd_id = p.id GROUP BY p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp;
2186
2458
  CREATE VIEW cost_monitor_group_daily AS SELECT * FROM cost_monitor_group_daily_cache;
2187
2459
  CREATE VIEW vw_overview AS SELECT * FROM overview;
@@ -2196,7 +2468,10 @@ export async function rebuildSqliteReadModel(
2196
2468
  CREATE VIEW vw_prd_summary_daily AS SELECT occurred_day_sp AS day, COUNT(*) AS prd_count, SUM(llm_call_count) AS llm_call_count FROM prd_summary GROUP BY occurred_day_sp;
2197
2469
  CREATE VIEW vw_task_status_daily AS SELECT occurred_day_sp AS day, status, COUNT(*) AS task_count FROM task_summary GROUP BY occurred_day_sp, status;
2198
2470
  CREATE VIEW vw_task_risk_daily AS SELECT occurred_day_sp AS day, risk, COUNT(*) AS task_count FROM task_summary GROUP BY occurred_day_sp, risk;
2199
- CREATE VIEW vw_entities AS SELECT 'task' AS entity_kind, id, title, status, risk, occurred_at, occurred_day_sp FROM task_summary UNION ALL SELECT 'spec', id, title, status, risk, occurred_at, occurred_day_sp FROM spec_summary UNION ALL SELECT 'prd', id, title, status, risk, occurred_at, occurred_day_sp FROM prd_summary;
2471
+ -- The Operations work-list export reads this public union directly.
2472
+ -- Keep the known aggregate facts from each entity summary so its
2473
+ -- snapshot is equivalent to the metrics shown by the work views.
2474
+ CREATE VIEW vw_entities AS SELECT 'task' AS entity_kind, id, title, status, risk, occurred_at, occurred_day_sp, invocation_count, total_duration, total_tokens, total_cost_usd, total_cost_brl FROM task_summary UNION ALL SELECT 'spec', id, title, status, risk, occurred_at, occurred_day_sp, invocation_count, total_duration, total_tokens, total_cost_usd, total_cost_brl FROM spec_summary UNION ALL SELECT 'prd', id, title, status, risk, occurred_at, occurred_day_sp, invocation_count, total_duration, total_tokens, total_cost_usd, total_cost_brl FROM prd_summary;
2200
2475
  CREATE VIEW vw_cost_group_daily AS SELECT * FROM cost_monitor_group_daily_cache;
2201
2476
  -- Narrow temporal Cost surfaces. Do not route the visible Cost
2202
2477
  -- tables through the three-grain overview union: it expands every
@@ -2302,7 +2577,7 @@ export async function rebuildSqliteReadModel(
2302
2577
  CREATE VIEW vw_cost_models AS WITH usage AS (SELECT model, agent_invocation_id, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl FROM invocation_usage GROUP BY model, agent_invocation_id), calls AS (SELECT i.agent_invocation_id, CASE WHEN (SELECT COUNT(DISTINCT m.model) FROM invocation_usage m WHERE m.agent_invocation_id = i.agent_invocation_id AND m.model IS NOT NULL) = 1 THEN i.llm_call_count END AS llm_call_count FROM invocations i) SELECT u.model, SUM(c.llm_call_count) AS llm_call_count, SUM(u.total_tokens) AS total_tokens, SUM(u.cost_usd) AS cost_usd, SUM(u.cost_brl) AS cost_brl, COUNT(*) AS invocation_count FROM usage u JOIN calls c USING(agent_invocation_id) GROUP BY u.model;
2303
2578
  CREATE VIEW vw_cost_providers AS WITH usage AS (SELECT provider, agent_invocation_id, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl FROM invocation_usage GROUP BY provider, agent_invocation_id), calls AS (SELECT i.agent_invocation_id, CASE WHEN (SELECT COUNT(DISTINCT p.provider) FROM invocation_usage p WHERE p.agent_invocation_id = i.agent_invocation_id AND p.provider IS NOT NULL) = 1 THEN i.llm_call_count END AS llm_call_count FROM invocations i) SELECT u.provider, SUM(c.llm_call_count) AS llm_call_count, SUM(u.total_tokens) AS total_tokens, SUM(u.cost_usd) AS cost_usd, SUM(u.cost_brl) AS cost_brl, COUNT(*) AS invocation_count FROM usage u JOIN calls c USING(agent_invocation_id) GROUP BY u.provider;
2304
2579
  CREATE VIEW vw_cost_tasks AS SELECT i.task_id, COALESCE(MAX(t.title), i.task_id) AS title, MAX(t.status) AS status, MAX(t.risk) AS risk, MIN(i.occurred_at) AS occurred_at, MIN(i.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(*) AS invocation_count FROM invocations i LEFT JOIN tasks t ON t.id = i.task_id WHERE i.task_id IS NOT NULL GROUP BY i.task_id;
2305
- CREATE VIEW vw_cost_specs AS SELECT s.id AS spec_id, s.title, s.status, s.risk, COALESCE(MIN(i.occurred_at), s.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), s.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.agent_invocation_id) AS invocation_count FROM specs s LEFT JOIN invocations i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2580
+ CREATE VIEW vw_cost_specs AS SELECT s.id AS spec_id, s.title, s.status, s.risk, COALESCE(MIN(i.occurred_at), s.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), s.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.run_id) AS invocation_count FROM specs s LEFT JOIN macro_spec_activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2306
2581
  CREATE VIEW vw_cost_prds AS SELECT p.id AS prd_id, p.title, p.status, p.risk, COALESCE(MIN(i.occurred_at), p.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), p.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.agent_invocation_id) AS invocation_count FROM prds p LEFT JOIN invocations i ON i.prd_id = p.id GROUP BY p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp;
2307
2582
  CREATE VIEW vw_cost_conversations AS SELECT i.conversation_id, i.conversation_id AS title, MAX(c.status) AS status, NULL AS risk, MIN(i.occurred_at) AS occurred_at, MIN(i.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(*) AS invocation_count FROM invocations i LEFT JOIN conversations c ON c.id = i.conversation_id WHERE i.conversation_id IS NOT NULL GROUP BY i.conversation_id;
2308
2583
  CREATE VIEW vw_operations_work_list AS SELECT * FROM vw_entities;
@@ -2325,7 +2600,7 @@ export async function rebuildSqliteReadModel(
2325
2600
  GROUP BY spec_id, prd_id
2326
2601
  ), entity_activity AS (
2327
2602
  SELECT 'task' AS entity_kind, task_id AS entity_id, * FROM activities WHERE task_id IS NOT NULL
2328
- UNION ALL SELECT 'spec', spec_id, * FROM activities WHERE spec_id IS NOT NULL
2603
+ UNION ALL SELECT 'spec', spec_id, * FROM macro_spec_activities
2329
2604
  UNION ALL SELECT 'prd', prd_id, * FROM activities WHERE prd_id IS NOT NULL
2330
2605
  UNION ALL
2331
2606
  SELECT 'task', l.task_id, a.*
@@ -2564,7 +2839,7 @@ export async function rebuildSqliteReadModel(
2564
2839
  SUM(total_tokens) AS total_tokens,
2565
2840
  SUM(cost_usd) AS total_cost_usd,
2566
2841
  SUM(cost_brl) AS total_cost_brl
2567
- FROM activities WHERE spec_id IS NOT NULL GROUP BY spec_id
2842
+ FROM macro_spec_activities GROUP BY spec_id
2568
2843
  )
2569
2844
  SELECT COUNT(*) AS entity_count,
2570
2845
  SUM(usage.total_run_count) AS total_run_count,
@@ -2609,7 +2884,7 @@ export async function rebuildSqliteReadModel(
2609
2884
  SUM(total_tokens) AS total_tokens,
2610
2885
  SUM(cost_usd) AS total_cost_usd,
2611
2886
  SUM(cost_brl) AS total_cost_brl
2612
- FROM activities WHERE spec_id IS NOT NULL GROUP BY spec_id
2887
+ FROM macro_spec_activities GROUP BY spec_id
2613
2888
  ), ranked AS (
2614
2889
  SELECT s.id AS entity_id, s.title, s.status, usage.total_run_count, usage.llm_call_count,
2615
2890
  usage.total_duration_ms, usage.total_tokens, usage.total_cost_usd, usage.total_cost_brl,
@@ -2740,7 +3015,72 @@ export async function rebuildSqliteReadModel(
2740
3015
  },
2741
3016
  );
2742
3017
  database.exec(deduplicatedViewSql);
2743
- reportLaunchProgress("projection_finalize", 80, "Finalizando índices e a cópia local do dashboard.");
3018
+ // One row per stable GitHub account. Costs are summed from the single
3019
+ // canonical Run fact, while delivery is joined exclusively through the
3020
+ // Task's completed_run_id (never inferred from a branch or latest Run).
3021
+ database.exec(`
3022
+ CREATE TABLE developer_cost_projection AS
3023
+ WITH completed AS (
3024
+ SELECT a.developer_account_id, COUNT(DISTINCT w.id) AS completed_task_count
3025
+ FROM zaip_vw_canonical_work_items w
3026
+ JOIN zaip_vw_activities a ON a.run_id=w.completed_run_id
3027
+ WHERE w.entity_kind='task' AND w.status='completed'
3028
+ AND a.developer_account_id IS NOT NULL
3029
+ GROUP BY a.developer_account_id
3030
+ )
3031
+ SELECT a.developer_account_id AS account_id,
3032
+ MAX(a.developer_login) AS login_snapshot,
3033
+ CASE WHEN COUNT(DISTINCT a.developer_attribution_source)=1 THEN MAX(a.developer_attribution_source) ELSE 'mixed' END AS attribution_source,
3034
+ COUNT(*) AS run_count,
3035
+ SUM(a.source_event_count) AS source_event_count,
3036
+ SUM(a.total_tokens) AS total_tokens,
3037
+ SUM(a.cost_usd) AS total_cost_usd,
3038
+ SUM(a.cost_brl) AS total_cost_brl,
3039
+ COUNT(a.total_tokens) AS token_covered_runs,
3040
+ COUNT(a.cost_usd) AS usd_covered_runs,
3041
+ COUNT(a.cost_brl) AS brl_covered_runs,
3042
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count
3043
+ FROM zaip_vw_activities a LEFT JOIN completed USING(developer_account_id)
3044
+ WHERE a.developer_account_id IS NOT NULL
3045
+ GROUP BY a.developer_account_id
3046
+ ORDER BY total_cost_brl DESC, account_id;
3047
+ CREATE INDEX developer_cost_projection_lookup ON developer_cost_projection(account_id);
3048
+ `);
3049
+ materializeCostGroupDailyProjection(database);
3050
+ materializeOperationsWorkProjection(database);
3051
+ // Persist the complete unfiltered invocation projection while the seed
3052
+ // still owns the compact canonical facts. Runtime pages can then slice
3053
+ // 10/100 rows without expanding the pricing view for every request.
3054
+ const costAgentInvocationProjection = database
3055
+ .prepare(
3056
+ `SELECT * FROM zaip_vw_cost_monitor_agent_invocations
3057
+ ORDER BY occurred_at DESC, agent_invocation_id DESC`,
3058
+ )
3059
+ .all() as Array<Record<string, unknown>>;
3060
+ const insertCostAgentInvocationProjection = database.prepare(
3061
+ `INSERT INTO zaip_aios_activities(
3062
+ activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
3063
+ recorded_at, occurred_day_sp, run_role, root_run_id, payload_type, payload_json
3064
+ ) VALUES (?, 'technical_payload', ?, 'sqlite://cost_agent_invocation_projection_v1', '',
3065
+ datetime('now'), ?, 'technical', 'technical', 'cost_agent_invocation_projection_v1', ?)`,
3066
+ );
3067
+ database.exec("BEGIN;");
3068
+ for (const row of costAgentInvocationProjection) {
3069
+ const invocationId = String(row.agent_invocation_id ?? "");
3070
+ if (invocationId.length === 0) continue;
3071
+ insertCostAgentInvocationProjection.run(
3072
+ `TECH-${randomUUID().toLowerCase()}`,
3073
+ `cost_agent_invocation_projection_v1:${invocationId}`,
3074
+ typeof row.occurred_day_sp === "string" ? row.occurred_day_sp : null,
3075
+ JSON.stringify(row),
3076
+ );
3077
+ }
3078
+ database.exec("COMMIT;");
3079
+ reportLaunchProgress(
3080
+ "projection_finalize",
3081
+ 80,
3082
+ "Finalizando índices e a cópia local do dashboard.",
3083
+ );
2744
3084
  completedFiles = files;
2745
3085
  } catch (error) {
2746
3086
  database.exec("ROLLBACK;");
@@ -2749,58 +3089,92 @@ export async function rebuildSqliteReadModel(
2749
3089
  } finally {
2750
3090
  database.close();
2751
3091
  }
2752
- try {
2753
- await rename(temporaryPath, cachePath);
2754
- } catch (error: unknown) {
2755
- // Another launcher may have built the same immutable cache entry while
2756
- // this process was reading. It is safe only if its fingerprint agrees.
2757
- if (readSnapshotFileCount(cachePath, fingerprint) === null) {
2758
- await rm(temporaryPath, { force: true });
2759
- throw error;
2760
- }
2761
- await rm(temporaryPath, { force: true });
2762
- }
2763
3092
  const materializeStartedAt = performance.now();
2764
- await materializeSnapshot(cachePath, path);
2765
- // Compute the exact canonical first-fold population once while rebuilding.
2766
- // Store it as a typed technical payload (not a sixth table) in both the
2767
- // immutable seed and this private generation, so first paint is O(1).
3093
+ const timed = <T>(phase: string, action: () => T) =>
3094
+ measureFirstFold(
3095
+ process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS === "1",
3096
+ reportLaunchProgress,
3097
+ phase,
3098
+ action,
3099
+ );
3100
+ // The shared cache remains private until every expensive first-fold payload
3101
+ // has been committed. A concurrent launcher may freely use `path`, its own
3102
+ // generation, but must never copy a half-published cache seed.
3103
+ await materializeSnapshot(temporaryPath, path);
2768
3104
  const firstFoldFilesystem = new SqliteReadModelFileSystem(workspaceRoot);
2769
- const firstFoldViewport = firstFoldFilesystem.firstFoldViewportAllTime();
3105
+ const firstFoldViewport = timed("first_fold_viewport", () =>
3106
+ firstFoldFilesystem.firstFoldViewportAllTime(),
3107
+ );
3108
+ const firstFoldAllTime = timed("first_fold_all_time", () =>
3109
+ firstFoldFilesystem.firstFoldAllTime(),
3110
+ );
3111
+ const firstFoldAllTimeBoard = timed("first_fold_board", () =>
3112
+ firstFoldFilesystem.firstFoldBoardAllTime(),
3113
+ );
3114
+ const costMonitorSummaryAllTime = timed("first_fold_cost_summary", () =>
3115
+ firstFoldFilesystem.costMonitorSummaryAggregate({ from: null, to: null }),
3116
+ );
2770
3117
  const quickMonth = saoPauloCurrentMonth();
3118
+ const costMonitorSummaryCurrentMonth = timed("first_fold_cost_summary_month", () =>
3119
+ firstFoldFilesystem.costMonitorSummaryAggregate(quickMonth),
3120
+ );
2771
3121
  const firstFoldMonthlyQuick: SqliteFirstFoldMonthlyQuick = {
2772
3122
  from_day: quickMonth.from,
2773
3123
  to_day: quickMonth.to,
2774
- source_latest_day: firstFoldFilesystem.firstFoldRelevantLatestDay(),
2775
- aggregate: firstFoldFilesystem.firstFoldAggregate(quickMonth),
3124
+ source_latest_day: timed("first_fold_latest_day", () =>
3125
+ firstFoldFilesystem.firstFoldRelevantLatestDay(),
3126
+ ),
3127
+ aggregate: timed("first_fold_month_aggregate", () =>
3128
+ firstFoldFilesystem.firstFoldAggregate(quickMonth),
3129
+ ),
2776
3130
  pages: {
2777
- task: firstFoldFilesystem.summaryPage("task", quickMonth, 0, 10_000).rows,
2778
- spec: firstFoldFilesystem.summaryPage("spec", quickMonth, 0, 10_000).rows,
2779
- prd: firstFoldFilesystem.summaryPage("prd", quickMonth, 0, 10_000).rows,
3131
+ task: operationsWorkRowsInRange(
3132
+ firstFoldAllTimeBoard.pages.task,
3133
+ quickMonth.from,
3134
+ quickMonth.to,
3135
+ ),
3136
+ spec: operationsWorkRowsInRange(
3137
+ firstFoldAllTimeBoard.pages.spec,
3138
+ quickMonth.from,
3139
+ quickMonth.to,
3140
+ ),
3141
+ prd: operationsWorkRowsInRange(
3142
+ firstFoldAllTimeBoard.pages.prd,
3143
+ quickMonth.from,
3144
+ quickMonth.to,
3145
+ ),
2780
3146
  },
2781
- top_tasks: firstFoldFilesystem.topTasks(quickMonth),
3147
+ top_tasks: timed("first_fold_month_top_tasks", () => firstFoldFilesystem.topTasks(quickMonth)),
2782
3148
  };
2783
3149
  firstFoldFilesystem.close();
2784
- const firstFoldPayload = JSON.stringify(firstFoldViewport);
2785
- const firstFoldMonthlyQuickPayload = JSON.stringify(firstFoldMonthlyQuick);
2786
- for (const target of [cachePath, path]) {
2787
- const targetDatabase = new DatabaseSync(target);
2788
- try {
2789
- targetDatabase.prepare(`INSERT INTO zaip_aios_activities(
2790
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2791
- recorded_at, run_role, root_run_id, payload_type, payload_json
2792
- ) VALUES (?, 'technical_payload', 'first_fold_viewport_cache:all_time',
2793
- 'sqlite://first_fold_viewport_cache/all_time', '', datetime('now'), 'technical', 'technical',
2794
- 'first_fold_viewport_cache', ?)`)
2795
- .run(`TECH-${randomUUID().toLowerCase()}`, firstFoldPayload);
2796
- targetDatabase.prepare(`INSERT INTO zaip_aios_activities(
2797
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2798
- recorded_at, run_role, root_run_id, payload_type, payload_json
2799
- ) VALUES (?, 'technical_payload', 'first_fold_monthly_quick_cache:default',
2800
- 'sqlite://first_fold_monthly_quick_cache/default', '', datetime('now'), 'technical', 'technical',
2801
- 'first_fold_monthly_quick_cache', ?)`)
2802
- .run(`TECH-${randomUUID().toLowerCase()}`, firstFoldMonthlyQuickPayload);
2803
- } finally { targetDatabase.close(); }
3150
+ persistFirstFoldPayloads([temporaryPath, path], {
3151
+ first_fold_viewport_cache: JSON.stringify(firstFoldViewport),
3152
+ first_fold_all_time_cache: JSON.stringify(firstFoldAllTime),
3153
+ first_fold_all_time_board_cache: JSON.stringify(firstFoldAllTimeBoard),
3154
+ cost_monitor_summary_cache: JSON.stringify(costMonitorSummaryAllTime),
3155
+ cost_monitor_summary_month_cache: JSON.stringify({
3156
+ ...quickMonth,
3157
+ aggregate: costMonitorSummaryCurrentMonth,
3158
+ }),
3159
+ first_fold_monthly_quick_cache: JSON.stringify(firstFoldMonthlyQuick),
3160
+ });
3161
+ try {
3162
+ await rename(temporaryPath, cachePath);
3163
+ } catch (error: unknown) {
3164
+ // A concurrent builder wins only when it published a complete seed.
3165
+ if (
3166
+ readSnapshotFileCount(
3167
+ cachePath,
3168
+ fingerprint,
3169
+ readModelSchemaVersion,
3170
+ hasRequiredFirstFoldPayloads,
3171
+ ) === null
3172
+ ) {
3173
+ await rm(temporaryPath, { force: true });
3174
+ throw error;
3175
+ }
3176
+ await rm(temporaryPath, { force: true });
3177
+ await materializeSnapshot(cachePath, path);
2804
3178
  }
2805
3179
  reportLaunchProgress("snapshot_ready", 86, "A cópia privada do banco local está pronta.");
2806
3180
  validateSqliteReadModelSchema(path);
@@ -2814,153 +3188,6 @@ export async function rebuildSqliteReadModel(
2814
3188
  };
2815
3189
  }
2816
3190
 
2817
- async function pruneSnapshotCache(directory: string, activePath: string): Promise<void> {
2818
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
2819
- const stale = await Promise.all(
2820
- entries
2821
- .filter(
2822
- (entry) =>
2823
- entry.isFile() &&
2824
- entry.name.endsWith(".sqlite") &&
2825
- resolve(directory, entry.name) !== activePath,
2826
- )
2827
- .map(async (entry) => ({
2828
- path: resolve(directory, entry.name),
2829
- mtime: (await stat(resolve(directory, entry.name))).mtimeMs,
2830
- })),
2831
- );
2832
- // Keep the most recent prior seed for a concurrent launcher; never remove tmp
2833
- // files, which may be an in-progress materialization by another process.
2834
- for (const entry of stale.sort((a, b) => b.mtime - a.mtime).slice(1))
2835
- await rm(entry.path, { force: true }).catch(() => {});
2836
- // Interrupted SQLite builds leave only cache-local temporary files. They
2837
- // cannot be valid immutable seeds and otherwise inflate the launch folder.
2838
- for (const entry of entries) {
2839
- if (!entry.isFile() || !entry.name.includes(".sqlite.") || !entry.name.includes(".tmp")) continue;
2840
- const candidate = resolve(directory, entry.name);
2841
- const age = Date.now() - (await stat(candidate)).mtimeMs;
2842
- if (age > 60_000) await rm(candidate, { force: true }).catch(() => {});
2843
- }
2844
- }
2845
-
2846
- function readSnapshotFileCount(path: string, fingerprint: string): number | null {
2847
- try {
2848
- const database = new DatabaseSync(path, { readOnly: true });
2849
- const previous = (
2850
- database
2851
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2852
- .get("source_fingerprint") as { value?: string } | undefined
2853
- )?.value;
2854
- const schema = (
2855
- database
2856
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2857
- .get("schema_version") as { value?: string } | undefined
2858
- )?.value;
2859
- const files = Number(
2860
- (
2861
- database
2862
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2863
- .get("source_file_count") as { value?: string } | undefined
2864
- )?.value ?? 0,
2865
- );
2866
- database.close();
2867
- return previous === fingerprint && schema === readModelSchemaVersion ? files : null;
2868
- } catch {
2869
- return null;
2870
- }
2871
- }
2872
-
2873
- async function mostRecentCompatibleSnapshot(directory: string): Promise<{ path: string; files: number } | null> {
2874
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
2875
- const candidates = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".sqlite"))
2876
- .map(async (entry) => ({ path: join(directory, entry.name), mtime: (await stat(join(directory, entry.name))).mtimeMs })));
2877
- for (const candidate of candidates.sort((left, right) => right.mtime - left.mtime)) {
2878
- const files = readCompatibleSnapshotFileCount(candidate.path);
2879
- if (files !== null) return { path: candidate.path, files };
2880
- }
2881
- return null;
2882
- }
2883
-
2884
- function readCompatibleSnapshotFileCount(path: string): number | null {
2885
- try {
2886
- const database = new DatabaseSync(path, { readOnly: true });
2887
- const schema = (database.prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?").get("schema_version") as { value?: string } | undefined)?.value;
2888
- const files = Number((database.prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?").get("source_file_count") as { value?: string } | undefined)?.value ?? 0);
2889
- database.close();
2890
- return schema === readModelSchemaVersion ? files : null;
2891
- } catch { return null; }
2892
- }
2893
-
2894
- /**
2895
- * The persisted schema exposes only zaip_vw_* names. The local runtime keeps
2896
- * concise historical names as connection-local TEMP aliases while callers are
2897
- * migrated; they are never written to the database file.
2898
- */
2899
- function installLegacyViewAliases(database: DatabaseSync): void {
2900
- const views = database
2901
- .prepare("SELECT name FROM sqlite_master WHERE type = 'view' AND name LIKE 'zaip_vw_%'")
2902
- .all() as Array<{ name: string }>;
2903
- for (const { name } of views) {
2904
- const alias = name.slice("zaip_vw_".length);
2905
- const quoted = (identifier: string) => `"${identifier.replaceAll('"', '""')}"`;
2906
- database.exec(`CREATE TEMP VIEW ${quoted(alias)} AS SELECT * FROM main.${quoted(name)}`);
2907
- }
2908
- }
2909
-
2910
- async function materializeSnapshot(source: string, destination: string): Promise<void> {
2911
- await mkdir(dirname(destination), { recursive: true });
2912
- const temporaryPath = `${destination}.${process.pid}.${randomUUID()}.tmp`;
2913
- await copyFile(source, temporaryPath);
2914
- try {
2915
- await rename(temporaryPath, destination);
2916
- } catch (error) {
2917
- await rm(temporaryPath, { force: true });
2918
- throw error;
2919
- }
2920
- }
2921
-
2922
- export type SqliteReadModelSourceCandidate = {
2923
- path: string;
2924
- source: string;
2925
- mtimeMs: number;
2926
- size: number;
2927
- };
2928
-
2929
- export async function sourceFingerprint(workspaceRoot: string): Promise<{
2930
- fingerprint: string;
2931
- candidates: SqliteReadModelSourceCandidate[];
2932
- }> {
2933
- const candidates = (
2934
- await Promise.all(
2935
- [".agents/records", ".agents/config"].map((root) => listFiles(resolve(workspaceRoot, root))),
2936
- )
2937
- )
2938
- .flat()
2939
- .filter(
2940
- (path) =>
2941
- textExtensions.has(path.slice(path.lastIndexOf(".")).toLowerCase()) &&
2942
- isDashboardCanonicalSource(relative(workspaceRoot, path).replaceAll("\\", "/")),
2943
- )
2944
- .sort();
2945
- const candidatesWithMetadata = await Promise.all(
2946
- candidates.map(async (candidate) => {
2947
- const metadata = await stat(candidate);
2948
- return {
2949
- path: candidate,
2950
- source: relative(workspaceRoot, candidate).replaceAll("\\", "/"),
2951
- mtimeMs: Math.trunc(metadata.mtimeMs),
2952
- size: metadata.size,
2953
- };
2954
- }),
2955
- );
2956
- return {
2957
- fingerprint: candidatesWithMetadata
2958
- .map((candidate) => `${candidate.source}:${candidate.mtimeMs}:${candidate.size}`)
2959
- .join("|"),
2960
- candidates: candidatesWithMetadata,
2961
- };
2962
- }
2963
-
2964
3191
  export type SqliteFirstFoldAggregate = {
2965
3192
  days: number;
2966
3193
  total_run_count: number;
@@ -2971,72 +3198,17 @@ export type SqliteFirstFoldAggregate = {
2971
3198
  cost_brl: number | null;
2972
3199
  execution_day_count: number;
2973
3200
  };
3201
+ export type {
3202
+ SqliteFirstFoldAllTime,
3203
+ SqliteFirstFoldAllTimeBoard,
3204
+ SqliteFirstFoldMonthlyQuick,
3205
+ } from "./sqlite-read-model-first-fold-types.js";
2974
3206
 
2975
- export type SqliteFirstFoldAllTime = {
2976
- aggregate: SqliteFirstFoldAggregate & {
2977
- start_day: string | null;
2978
- end_day: string | null;
2979
- task_count: number;
2980
- completed_task_count: number;
2981
- spec_count: number;
2982
- completed_spec_count: number;
2983
- prd_count: number;
2984
- completed_prd_count: number;
2985
- total_cost_usd: number | null;
2986
- latest_task_created_at: string | null;
2987
- };
2988
- task_status_counts: Record<string, number>;
2989
- task_risk_counts: Record<string, number>;
2990
- task_metrics: {
2991
- average_duration_ms_per_task: number | null;
2992
- average_tokens_per_task: number | null;
2993
- average_cost_usd_per_task: number | null;
2994
- average_cost_brl_per_task: number | null;
2995
- latest_task_created_at: string | null;
2996
- };
2997
- top_tasks: Array<{
2998
- rank: number;
2999
- task_id: string;
3000
- title: string;
3001
- status: string | null;
3002
- risk: string | null;
3003
- total_run_count: number;
3004
- llm_call_count: number | null;
3005
- total_duration_ms: number;
3006
- total_tokens: number | null;
3007
- total_cost_usd: number | null;
3008
- total_cost_brl: number | null;
3009
- duration_share: number;
3010
- }>;
3011
- macro_metrics: Record<"specs" | "prds", {
3012
- status_counts: Record<string, number>;
3013
- classification_counts: Record<string, number>;
3014
- entity_count: number;
3015
- total_run_count: number;
3016
- total_duration_ms: number | null;
3017
- total_tokens: number | null;
3018
- total_cost_usd: number | null;
3019
- total_cost_brl: number | null;
3020
- average_duration_ms: number | null;
3021
- average_tokens: number | null;
3022
- average_cost_usd: number | null;
3023
- average_cost_brl: number | null;
3024
- latest_created_at: string | null;
3025
- top_entities: Array<{
3026
- rank: number;
3027
- entity_id: string;
3028
- title: string;
3029
- status: string | null;
3030
- total_run_count: number;
3031
- llm_call_count: number | null;
3032
- total_duration_ms: number | null;
3033
- total_tokens: number | null;
3034
- total_cost_usd: number | null;
3035
- total_cost_brl: number | null;
3036
- duration_share: number;
3037
- }>;
3038
- }>;
3039
- };
3207
+ import type {
3208
+ SqliteFirstFoldAllTime,
3209
+ SqliteFirstFoldAllTimeBoard,
3210
+ SqliteFirstFoldMonthlyQuick,
3211
+ } from "./sqlite-read-model-first-fold-types.js";
3040
3212
 
3041
3213
  /** The only all-time facts needed before the Operations first fold is painted. */
3042
3214
  export type SqliteFirstFoldViewport = Pick<
@@ -3112,11 +3284,19 @@ export const sqliteViewMetadata = {
3112
3284
  first_fold_task_metrics_all_time: { date: null, id: null, order: "task_count DESC" },
3113
3285
  first_fold_top_tasks_all_time: { date: null, id: "task_id", order: "rank" },
3114
3286
  first_fold_spec_status_all_time: { date: null, id: "stage", order: "stage" },
3115
- first_fold_spec_classification_all_time: { date: null, id: "classification", order: "classification" },
3287
+ first_fold_spec_classification_all_time: {
3288
+ date: null,
3289
+ id: "classification",
3290
+ order: "classification",
3291
+ },
3116
3292
  first_fold_spec_metrics_all_time: { date: null, id: null, order: "entity_count DESC" },
3117
3293
  first_fold_top_specs_all_time: { date: null, id: "entity_id", order: "rank" },
3118
3294
  first_fold_prd_status_all_time: { date: null, id: "stage", order: "stage" },
3119
- first_fold_prd_classification_all_time: { date: null, id: "classification", order: "classification" },
3295
+ first_fold_prd_classification_all_time: {
3296
+ date: null,
3297
+ id: "classification",
3298
+ order: "classification",
3299
+ },
3120
3300
  first_fold_prd_metrics_all_time: { date: null, id: null, order: "entity_count DESC" },
3121
3301
  first_fold_top_prds_all_time: { date: null, id: "entity_id", order: "rank" },
3122
3302
  first_fold_task: { date: "occurred_day_sp", id: "id", order: "occurred_at DESC, id DESC" },
@@ -3142,51 +3322,24 @@ export type SqliteSummaryPage = {
3142
3322
  * immutable seed is built, rather than competing with the browser after the
3143
3323
  * launcher reports ready.
3144
3324
  */
3145
- export type SqliteFirstFoldMonthlyQuick = {
3146
- from_day: string;
3147
- to_day: string;
3148
- source_latest_day: string | null;
3149
- aggregate: SqliteFirstFoldAggregate;
3150
- pages: Record<SqliteSummaryKind, Array<Record<string, unknown>>>;
3151
- top_tasks: Array<{
3152
- task_id: string;
3153
- total_tokens: number | null;
3154
- cost_brl: number | null;
3155
- run_count: number;
3156
- duration_ms: number;
3157
- }>;
3158
- };
3159
-
3160
- async function listFiles(directory: string): Promise<string[]> {
3161
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
3162
- const paths = await Promise.all(
3163
- entries.map(async (entry) => {
3164
- const candidate = join(directory, entry.name);
3165
- return entry.isDirectory() ? listFiles(candidate) : entry.isFile() ? [candidate] : [];
3166
- }),
3167
- );
3168
- return paths.flat();
3169
- }
3170
-
3171
3325
  /** Read-only filesystem backed by the regenerated SQLite snapshot. */
3172
3326
  export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3173
3327
  private readonly executionDurationCache = new Map<string, Map<string, number>>();
3174
3328
  private dailyAggregateRowsCache: Array<Record<string, number | string | null>> | null = null;
3175
3329
  private firstFoldAllTimeCache: SqliteFirstFoldAllTime | null = null;
3330
+ private firstFoldAllTimeBoardCache: SqliteFirstFoldAllTimeBoard | null = null;
3176
3331
  private firstFoldTopTasksAllTimeCache: SqliteFirstFoldAllTime["top_tasks"] | null = null;
3177
3332
  private firstFoldViewportCache: SqliteFirstFoldViewport | null = null;
3178
3333
  private firstFoldMonthlyQuickCache: SqliteFirstFoldMonthlyQuick | null | undefined = undefined;
3179
3334
  private firstFoldViewportTimings: Record<string, number> = {};
3180
3335
  private metadataValues: Map<string, string> | null = null;
3181
3336
  private readonly unfilteredViewPageCache = new Map<string, Array<Record<string, unknown>>>();
3337
+ private readonly technicalPayloadRowsCache = new Map<string, Array<Record<string, unknown>>>();
3182
3338
  private readonly viewPageResultCache = new Map<string, Record<string, unknown>>();
3183
- // `operations_work_list` is a derived union of three aggregate views. The
3184
- // first page used to recompute that union (and its duration allocation) for
3185
- // both COUNT and SELECT, then repeat it for every offset. Keep the immutable
3186
- // generation-local result in the read-only runtime instead. The public SQL
3187
- // view and all filtered paths remain the compatibility seam.
3188
- private operationsWorkListPageCache: Array<Record<string, unknown>> | null = null;
3189
-
3339
+ // Summary cards and temporal Cost tables consume the same aggregate. Keep
3340
+ // exact filter results inside the immutable generation so a tab switch does
3341
+ // not repeat its multi-second SQL scan.
3342
+ private readonly costMonitorSummaryCache = new Map<string, CostSummarySource>();
3190
3343
  private dailyAggregateRows(): Array<Record<string, number | string | null>> {
3191
3344
  if (this.dailyAggregateRowsCache === null) {
3192
3345
  this.dailyAggregateRowsCache = this.database
@@ -3196,23 +3349,37 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3196
3349
  return this.dailyAggregateRowsCache;
3197
3350
  }
3198
3351
 
3199
- private operationsWorkListRows(): Array<Record<string, unknown>> {
3200
- this.operationsWorkListPageCache ??= this.database
3201
- .prepare("SELECT * FROM operations_work_list ORDER BY occurred_at DESC, id DESC")
3202
- .all() as Array<Record<string, unknown>>;
3203
- return this.operationsWorkListPageCache;
3204
- }
3205
-
3206
3352
  private unfilteredViewRows(view: SqliteViewName): Array<Record<string, unknown>> {
3207
3353
  const cached = this.unfilteredViewPageCache.get(view);
3208
3354
  if (cached !== undefined) return cached;
3209
3355
  const order = sqliteViewMetadata[view].order;
3210
- const rows = this.database
3211
- .prepare(`SELECT * FROM ${view} ORDER BY ${order}`)
3212
- .all() as Array<Record<string, unknown>>;
3356
+ const rows = this.database.prepare(`SELECT * FROM ${view} ORDER BY ${order}`).all() as Array<
3357
+ Record<string, unknown>
3358
+ >;
3213
3359
  this.unfilteredViewPageCache.set(view, rows);
3214
3360
  return rows;
3215
3361
  }
3362
+
3363
+ private technicalPayloadRows(payloadType: string): Array<Record<string, unknown>> {
3364
+ const cached = this.technicalPayloadRowsCache.get(payloadType);
3365
+ if (cached !== undefined) return cached;
3366
+ const rows = (
3367
+ this.database
3368
+ .prepare("SELECT payload_json FROM zaip_aios_activities WHERE payload_type=?")
3369
+ .all(payloadType) as Array<{ payload_json: string }>
3370
+ ).flatMap(({ payload_json }) => {
3371
+ try {
3372
+ const parsed = JSON.parse(payload_json) as unknown;
3373
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
3374
+ ? [parsed as Record<string, unknown>]
3375
+ : [];
3376
+ } catch {
3377
+ return [];
3378
+ }
3379
+ });
3380
+ this.technicalPayloadRowsCache.set(payloadType, rows);
3381
+ return rows;
3382
+ }
3216
3383
  private database: DatabaseSync;
3217
3384
 
3218
3385
  constructor(root: string) {
@@ -3287,6 +3454,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3287
3454
  this.executionDurationCache.clear();
3288
3455
  this.dailyAggregateRowsCache = null;
3289
3456
  this.firstFoldAllTimeCache = null;
3457
+ this.firstFoldAllTimeBoardCache = null;
3290
3458
  this.firstFoldTopTasksAllTimeCache = null;
3291
3459
  this.firstFoldViewportCache = null;
3292
3460
  this.firstFoldMonthlyQuickCache = undefined;
@@ -3295,6 +3463,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3295
3463
  // Public pages belong to the opened immutable SQLite generation. Never
3296
3464
  // carry them across refresh/rebuild, even when this filesystem instance is
3297
3465
  // reused by the launcher.
3466
+ this.unfilteredViewPageCache.clear();
3467
+ this.technicalPayloadRowsCache.clear();
3468
+ this.costMonitorSummaryCache.clear();
3298
3469
  this.viewPageResultCache.clear();
3299
3470
  }
3300
3471
 
@@ -3353,12 +3524,16 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3353
3524
  const providerValues = (filter.providers ?? []).filter((value) => value !== "unavailable");
3354
3525
  const modelValues = (filter.models ?? []).filter((value) => value !== "unavailable");
3355
3526
  const requestsUnavailable =
3356
- filter.providers?.includes("unavailable") === true || filter.models?.includes("unavailable") === true;
3527
+ filter.providers?.includes("unavailable") === true ||
3528
+ filter.models?.includes("unavailable") === true;
3357
3529
  if (requestsUnavailable) {
3358
3530
  const unavailableDimension = (column: "provider" | "model", selected?: readonly string[]) => {
3359
3531
  if (selected === undefined || selected.length === 0) return;
3360
3532
  const available = selected.filter((value) => value !== "unavailable");
3361
- const matches = available.length === 0 ? "0" : `EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=a.agent_run_id AND u.${column} IN (${available.map(() => "?").join(",")}))`;
3533
+ const matches =
3534
+ available.length === 0
3535
+ ? "0"
3536
+ : `EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=a.agent_run_id AND u.${column} IN (${available.map(() => "?").join(",")}))`;
3362
3537
  if (available.length > 0) values.push(...available);
3363
3538
  const missing = `NOT EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=a.agent_run_id AND u.${column} IS NOT NULL AND u.${column} <> 'unavailable')`;
3364
3539
  clauses.push(selected.includes("unavailable") ? `(${matches} OR ${missing})` : matches);
@@ -3377,7 +3552,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3377
3552
  }
3378
3553
  // Provider and model describe one observed usage segment. Independent
3379
3554
  // EXISTS clauses would let a crossed provider/model pair match a mixed Run.
3380
- clauses.push(`EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=a.agent_run_id AND ${segmentClauses.join(" AND ")})`);
3555
+ clauses.push(
3556
+ `EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=a.agent_run_id AND ${segmentClauses.join(" AND ")})`,
3557
+ );
3381
3558
  }
3382
3559
  const dimensions = [
3383
3560
  ["zaip_agent", filter.agents],
@@ -3415,12 +3592,16 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3415
3592
  const providerValues = (filter.providers ?? []).filter((value) => value !== "unavailable");
3416
3593
  const modelValues = (filter.models ?? []).filter((value) => value !== "unavailable");
3417
3594
  const requestsUnavailable =
3418
- filter.providers?.includes("unavailable") === true || filter.models?.includes("unavailable") === true;
3595
+ filter.providers?.includes("unavailable") === true ||
3596
+ filter.models?.includes("unavailable") === true;
3419
3597
  if (requestsUnavailable) {
3420
3598
  const unavailableDimension = (column: "provider" | "model", selected?: readonly string[]) => {
3421
3599
  if (selected === undefined || selected.length === 0) return;
3422
3600
  const available = selected.filter((value) => value !== "unavailable");
3423
- const matches = available.length === 0 ? "0" : `EXISTS (SELECT 1 FROM invocations i JOIN invocation_usage u USING(agent_invocation_id) WHERE i.run_id=canonical_run_index.run_id AND u.${column} IN (${available.map(() => "?").join(",")}))`;
3601
+ const matches =
3602
+ available.length === 0
3603
+ ? "0"
3604
+ : `EXISTS (SELECT 1 FROM invocations i JOIN invocation_usage u USING(agent_invocation_id) WHERE i.run_id=canonical_run_index.run_id AND u.${column} IN (${available.map(() => "?").join(",")}))`;
3424
3605
  if (available.length > 0) values.push(...available);
3425
3606
  const missing = `NOT EXISTS (SELECT 1 FROM invocations i JOIN invocation_usage u USING(agent_invocation_id) WHERE i.run_id=canonical_run_index.run_id AND u.${column} IS NOT NULL AND u.${column} <> 'unavailable')`;
3426
3607
  clauses.push(selected.includes("unavailable") ? `(${matches} OR ${missing})` : matches);
@@ -3437,7 +3618,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3437
3618
  segmentClauses.push(`u.model IN (${modelValues.map(() => "?").join(",")})`);
3438
3619
  values.push(...modelValues);
3439
3620
  }
3440
- clauses.push(`EXISTS (SELECT 1 FROM invocations i JOIN invocation_usage u USING(agent_invocation_id) WHERE i.run_id=canonical_run_index.run_id AND ${segmentClauses.join(" AND ")})`);
3621
+ clauses.push(
3622
+ `EXISTS (SELECT 1 FROM invocations i JOIN invocation_usage u USING(agent_invocation_id) WHERE i.run_id=canonical_run_index.run_id AND ${segmentClauses.join(" AND ")})`,
3623
+ );
3441
3624
  }
3442
3625
  const dimensions = [
3443
3626
  ["zaip_agent", filter.agents],
@@ -3450,7 +3633,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3450
3633
  values.push(...selected);
3451
3634
  }
3452
3635
  const rows = this.database
3453
- .prepare(`SELECT occurred_day_sp AS day, COUNT(*) AS count FROM canonical_run_index WHERE ${clauses.join(" AND ")} GROUP BY occurred_day_sp`)
3636
+ .prepare(
3637
+ `SELECT occurred_day_sp AS day, COUNT(*) AS count FROM canonical_run_index WHERE ${clauses.join(" AND ")} GROUP BY occurred_day_sp`,
3638
+ )
3454
3639
  .all(...values) as Array<{ day: string; count: number }>;
3455
3640
  return new Map(rows.map((row) => [row.day, Number(row.count)]));
3456
3641
  }
@@ -3469,33 +3654,36 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3469
3654
  days.map((day) => {
3470
3655
  const row = rowsByDay.get(day) ?? {};
3471
3656
  const totalRuns = Number(row.total_run_count ?? 0);
3472
- const knownUsageRuns = Number(row.activity_count ?? 0);
3473
- const unknownUsageRuns = Math.max(0, totalRuns - knownUsageRuns);
3657
+ const knownTokenRuns = Number(row.token_known_runs ?? 0);
3658
+ const knownCostRuns = Math.min(
3659
+ Number(row.usd_known_runs ?? 0),
3660
+ Number(row.brl_known_runs ?? 0),
3661
+ );
3474
3662
  return [
3475
- day,
3476
- {
3477
- prds: Number(row.prd_count ?? 0),
3478
- specs: Number(row.spec_count ?? 0),
3479
- tasks: Number(row.task_count ?? 0),
3480
- validations: Number(row.validation_count ?? 0),
3481
- evidences: Number(row.evidence_count ?? 0),
3482
- runs: Number(row.total_run_count ?? 0),
3483
- usage: {
3484
- total_tokens: {
3485
- known_sum: Number(row.total_tokens ?? 0),
3486
- unknown: unknownUsageRuns,
3663
+ day,
3664
+ {
3665
+ prds: Number(row.prd_count ?? 0),
3666
+ specs: Number(row.spec_count ?? 0),
3667
+ tasks: Number(row.task_count ?? 0),
3668
+ validations: Number(row.validation_count ?? 0),
3669
+ evidences: Number(row.evidence_count ?? 0),
3670
+ runs: Number(row.total_run_count ?? 0),
3671
+ usage: {
3672
+ total_tokens: {
3673
+ known_sum: Number(row.total_tokens ?? 0),
3674
+ unknown: Math.max(0, totalRuns - knownTokenRuns),
3675
+ },
3676
+ cost: {
3677
+ known_sum: Number(row.total_cost_brl ?? 0),
3678
+ unknown: Math.max(0, totalRuns - knownCostRuns),
3679
+ },
3487
3680
  },
3488
- cost: {
3489
- known_sum: Number(row.total_cost_brl ?? 0),
3490
- unknown: unknownUsageRuns,
3681
+ duration: {
3682
+ known_sum: durations.get(day) ?? 0,
3683
+ unknown: durations.has(day) ? 0 : Number(row.total_run_count ?? 0),
3491
3684
  },
3492
3685
  },
3493
- duration: {
3494
- known_sum: durations.get(day) ?? 0,
3495
- unknown: durations.has(day) ? 0 : Number(row.total_run_count ?? 0),
3496
- },
3497
- },
3498
- ];
3686
+ ];
3499
3687
  }),
3500
3688
  );
3501
3689
  }
@@ -3533,333 +3721,65 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3533
3721
  * A later month with no facts in this immutable generation is also an exact
3534
3722
  * empty result; every month that may contain a fact continues through SQL.
3535
3723
  */
3536
- firstFoldMonthlyQuick(filter: { from: string | null; to: string | null }): SqliteFirstFoldMonthlyQuick | null {
3537
- if (this.firstFoldMonthlyQuickCache === undefined) {
3538
- const persisted = this.database
3539
- .prepare("SELECT payload_json FROM zaip_aios_activities WHERE payload_type='first_fold_monthly_quick_cache' ORDER BY recorded_at DESC LIMIT 1")
3540
- .get() as { payload_json?: string } | undefined;
3541
- try {
3542
- const candidate = typeof persisted?.payload_json === "string"
3543
- ? JSON.parse(persisted.payload_json) as Partial<SqliteFirstFoldMonthlyQuick>
3544
- : null;
3545
- this.firstFoldMonthlyQuickCache =
3546
- candidate !== null &&
3547
- typeof candidate.from_day === "string" &&
3548
- typeof candidate.to_day === "string" &&
3549
- candidate.aggregate !== undefined &&
3550
- candidate.pages !== undefined &&
3551
- Array.isArray(candidate.top_tasks)
3552
- ? candidate as SqliteFirstFoldMonthlyQuick
3553
- : null;
3554
- } catch {
3555
- this.firstFoldMonthlyQuickCache = null;
3556
- }
3557
- }
3558
- const cached = this.firstFoldMonthlyQuickCache;
3559
- if (cached === null) return null;
3560
- const from = filter.from?.slice(0, 10) ?? null;
3561
- const to = filter.to?.slice(0, 10) ?? null;
3562
- const exactRange = from === cached.from_day && to === cached.to_day;
3563
- const sameMonthLaterDay =
3564
- from === cached.from_day &&
3565
- to !== null &&
3566
- to > cached.to_day &&
3567
- to.slice(0, 7) === cached.from_day.slice(0, 7);
3568
- const noUncoveredFacts =
3569
- cached.source_latest_day === null || cached.source_latest_day <= cached.to_day;
3570
- // The payload itself is exact for its own range even if the immutable
3571
- // generation also contains future-dated facts. Only an extension beyond
3572
- // its end day needs the no-uncovered-facts guard.
3573
- if (exactRange || (sameMonthLaterDay && noUncoveredFacts)) return cached;
3574
-
3575
- const requestedMonthIsLater =
3576
- from !== null &&
3577
- to !== null &&
3578
- from.slice(0, 7) >
3579
- (cached.source_latest_day === null
3580
- ? cached.from_day.slice(0, 7)
3581
- : cached.source_latest_day.slice(0, 7));
3582
- if (!requestedMonthIsLater) return null;
3583
- return {
3584
- from_day: from,
3585
- to_day: to,
3586
- source_latest_day: cached.source_latest_day,
3587
- aggregate: {
3588
- days: 0,
3589
- total_run_count: 0,
3590
- agent_invocation_count: 0,
3591
- llm_call_count: null,
3592
- duration_ms: null,
3593
- total_tokens: null,
3594
- cost_brl: null,
3595
- execution_day_count: 0,
3596
- },
3597
- pages: { task: [], spec: [], prd: [] },
3598
- top_tasks: [],
3599
- };
3724
+ firstFoldMonthlyQuick(filter: {
3725
+ from: string | null;
3726
+ to: string | null;
3727
+ }): SqliteFirstFoldMonthlyQuick | null {
3728
+ const read = readFirstFoldMonthlyQuick(this.database, this.firstFoldMonthlyQuickCache, filter);
3729
+ this.firstFoldMonthlyQuickCache = read.cache;
3730
+ return read.result;
3600
3731
  }
3601
3732
 
3602
3733
  firstFoldRelevantLatestDay(): string | null {
3603
- const row = this.database.prepare(`SELECT MAX(day) AS day FROM (
3734
+ const row = this.database
3735
+ .prepare(`SELECT MAX(day) AS day FROM (
3604
3736
  SELECT occurred_day_sp AS day FROM activities
3605
3737
  UNION ALL SELECT occurred_day_sp AS day FROM canonical_work_items
3606
3738
  UNION ALL SELECT occurred_day_sp AS day FROM canonical_artifacts
3607
3739
  UNION ALL SELECT occurred_day_sp AS day FROM canonical_run_index
3608
3740
  UNION ALL SELECT occurred_day_sp AS day FROM attributed_token_active_facts
3609
- )`).get() as { day?: string | null };
3741
+ )`)
3742
+ .get() as { day?: string | null };
3610
3743
  return typeof row.day === "string" ? row.day : null;
3611
3744
  }
3612
3745
 
3613
- /**
3614
- * Compact all-time first fold. A runtime reads an immutable private SQLite
3615
- * snapshot, so readiness can warm this object and first paint can reuse the
3616
- * same coherent source values without rescanning entity rows.
3617
- */
3618
3746
  firstFoldAllTime(): SqliteFirstFoldAllTime {
3619
3747
  if (this.firstFoldAllTimeCache !== null) return this.firstFoldAllTimeCache;
3620
- const row = this.database
3621
- .prepare("SELECT * FROM zaip_vw_first_fold_overview_all_time")
3622
- .get() as Record<string, unknown>;
3623
- const statusRows = this.database
3624
- .prepare("SELECT status, task_count FROM zaip_vw_first_fold_task_status_all_time")
3625
- .all() as Array<{ status: string; task_count: number }>;
3626
- const riskRows = this.database
3627
- .prepare("SELECT risk, task_count FROM zaip_vw_first_fold_task_risk_all_time")
3628
- .all() as Array<{ risk: string; task_count: number }>;
3629
- const metrics = this.database
3630
- .prepare("SELECT * FROM zaip_vw_first_fold_task_metrics_all_time")
3631
- .get() as Record<string, unknown>;
3632
- const topTasks = this.database
3633
- .prepare("SELECT * FROM zaip_vw_first_fold_top_tasks_all_time ORDER BY rank")
3634
- .all() as SqliteFirstFoldAllTime["top_tasks"];
3635
- const number = (value: unknown, fallback = 0) =>
3636
- typeof value === "number" ? value : fallback;
3637
- const nullableNumber = (value: unknown) => (typeof value === "number" ? value : null);
3638
- const readMacroMetrics = (kind: "spec" | "prd") => {
3639
- const statusRows = this.database
3640
- .prepare(`SELECT stage, entity_count FROM zaip_vw_first_fold_${kind}_status_all_time`)
3641
- .all() as Array<{ stage: string; entity_count: number }>;
3642
- const classificationRows = this.database
3643
- .prepare(
3644
- `SELECT classification, entity_count FROM zaip_vw_first_fold_${kind}_classification_all_time`,
3645
- )
3646
- .all() as Array<{ classification: string; entity_count: number }>;
3647
- const macro = this.database
3648
- .prepare(`SELECT * FROM zaip_vw_first_fold_${kind}_metrics_all_time`)
3649
- .get() as Record<string, unknown>;
3650
- const topEntities = this.database
3651
- .prepare(`SELECT * FROM zaip_vw_first_fold_top_${kind}s_all_time ORDER BY rank`)
3652
- .all() as SqliteFirstFoldAllTime["macro_metrics"]["specs"]["top_entities"];
3748
+ this.firstFoldAllTimeCache = readFirstFoldAllTime(
3749
+ this.database,
3750
+ () => this.dailyAggregateRows().length,
3751
+ this.firstFoldViewportAllTime(),
3752
+ );
3753
+ return this.firstFoldAllTimeCache;
3754
+ }
3755
+
3756
+ firstFoldBoardAllTime(): SqliteFirstFoldAllTimeBoard {
3757
+ if (this.firstFoldAllTimeBoardCache !== null) return this.firstFoldAllTimeBoardCache;
3758
+ this.firstFoldAllTimeBoardCache = readFirstFoldBoardAllTime(this.database, () => {
3759
+ const rows = operationsWorkListSqlRows(this.database);
3653
3760
  return {
3654
- status_counts: Object.fromEntries(
3655
- statusRows.map((status) => [status.stage, Number(status.entity_count)]),
3656
- ),
3657
- classification_counts: Object.fromEntries(
3658
- classificationRows.map((row) => [row.classification, Number(row.entity_count)]),
3659
- ),
3660
- entity_count: number(macro.entity_count),
3661
- total_run_count: number(macro.total_run_count),
3662
- total_duration_ms: nullableNumber(macro.total_duration_ms),
3663
- total_tokens: nullableNumber(macro.total_tokens),
3664
- total_cost_usd: nullableNumber(macro.total_cost_usd),
3665
- total_cost_brl: nullableNumber(macro.total_cost_brl),
3666
- average_duration_ms: nullableNumber(macro.average_duration_ms),
3667
- average_tokens: nullableNumber(macro.average_tokens),
3668
- average_cost_usd: nullableNumber(macro.average_cost_usd),
3669
- average_cost_brl: nullableNumber(macro.average_cost_brl),
3670
- latest_created_at:
3671
- typeof macro.latest_created_at === "string" ? macro.latest_created_at : null,
3672
- top_entities: topEntities,
3761
+ pages: {
3762
+ task: rows.filter((row) => row.kind === "task" || row.kind === "standalone-task"),
3763
+ spec: rows.filter((row) => row.kind === "spec"),
3764
+ prd: rows.filter((row) => row.kind === "prd"),
3765
+ },
3673
3766
  };
3674
- };
3675
- this.firstFoldAllTimeCache = {
3676
- aggregate: {
3677
- days:
3678
- typeof row.start_day === "string" && typeof row.end_day === "string"
3679
- ? this.dailyAggregateRows().length
3680
- : 0,
3681
- start_day: typeof row.start_day === "string" ? row.start_day : null,
3682
- end_day: typeof row.end_day === "string" ? row.end_day : null,
3683
- task_count: number(row.task_count),
3684
- completed_task_count: number(row.completed_task_count),
3685
- spec_count: number(row.spec_count),
3686
- completed_spec_count: number(row.completed_spec_count),
3687
- prd_count: number(row.prd_count),
3688
- completed_prd_count: number(row.completed_prd_count),
3689
- total_run_count: number(row.total_run_count),
3690
- agent_invocation_count: number(row.agent_invocation_count),
3691
- llm_call_count: nullableNumber(row.llm_call_count),
3692
- duration_ms: nullableNumber(row.total_duration_ms),
3693
- total_tokens: nullableNumber(row.total_tokens),
3694
- total_cost_usd: nullableNumber(row.total_cost_usd),
3695
- cost_brl: nullableNumber(row.total_cost_brl),
3696
- execution_day_count: number(row.execution_day_count),
3697
- latest_task_created_at:
3698
- typeof row.latest_task_created_at === "string" ? row.latest_task_created_at : null,
3699
- },
3700
- task_status_counts: Object.fromEntries(
3701
- statusRows.map((status) => [status.status, Number(status.task_count)]),
3702
- ),
3703
- task_risk_counts: Object.fromEntries(
3704
- riskRows.map((risk) => [risk.risk, Number(risk.task_count)]),
3705
- ),
3706
- task_metrics: {
3707
- average_duration_ms_per_task: nullableNumber(metrics.average_duration_ms_per_task),
3708
- average_tokens_per_task: nullableNumber(metrics.average_tokens_per_task),
3709
- average_cost_usd_per_task: nullableNumber(metrics.average_cost_usd_per_task),
3710
- average_cost_brl_per_task: nullableNumber(metrics.average_cost_brl_per_task),
3711
- latest_task_created_at:
3712
- typeof metrics.latest_task_created_at === "string"
3713
- ? metrics.latest_task_created_at
3714
- : null,
3715
- },
3716
- top_tasks: topTasks,
3717
- macro_metrics: {
3718
- specs: readMacroMetrics("spec"),
3719
- prds: readMacroMetrics("prd"),
3720
- },
3721
- };
3722
- return this.firstFoldAllTimeCache;
3767
+ });
3768
+ return this.firstFoldAllTimeBoardCache;
3723
3769
  }
3724
3770
 
3725
- /**
3726
- * Narrow all-time ranking for the Operations quick projection. This avoids
3727
- * loading macro metrics and extended aggregates solely to render Top 5.
3728
- */
3729
3771
  firstFoldTopTasksAllTime(): SqliteFirstFoldAllTime["top_tasks"] {
3730
3772
  if (this.firstFoldTopTasksAllTimeCache !== null) return this.firstFoldTopTasksAllTimeCache;
3731
- this.firstFoldTopTasksAllTimeCache = this.database
3732
- .prepare("SELECT * FROM zaip_vw_first_fold_top_tasks_all_time ORDER BY rank")
3733
- .all() as SqliteFirstFoldAllTime["top_tasks"];
3773
+ this.firstFoldTopTasksAllTimeCache = readFirstFoldTopTasksAllTime(this.database);
3734
3774
  return this.firstFoldTopTasksAllTimeCache;
3735
3775
  }
3736
3776
 
3737
- /**
3738
- * First-paint subset: six KPI facts plus the status and risk charts. Keep
3739
- * rankings, task averages and macro views out of readiness and scope=quick.
3740
- */
3741
3777
  firstFoldViewportAllTime(): SqliteFirstFoldViewport {
3742
3778
  if (this.firstFoldViewportCache !== null) return this.firstFoldViewportCache;
3743
- const persisted = this.database
3744
- .prepare("SELECT payload_json FROM zaip_aios_activities WHERE payload_type='first_fold_viewport_cache' ORDER BY recorded_at DESC LIMIT 1")
3745
- .get() as { payload_json?: string } | undefined;
3746
- if (typeof persisted?.payload_json === "string") {
3747
- this.firstFoldViewportCache = JSON.parse(persisted.payload_json) as SqliteFirstFoldViewport;
3748
- return this.firstFoldViewportCache;
3749
- }
3750
- // This is deliberately not the rich overview view: `task_summary` joins
3751
- // every activity and is appropriate for rankings, but status/risk charts
3752
- // need only the canonical work-item dimensions. Keep the three viewport
3753
- // SELECTs independently attributable and free of below-fold joins.
3754
- const profile = process.env.ZAIP_DASHBOARD_PROFILE_FIRST_FOLD === "1";
3755
- const timed = <T>(select: string, read: () => T): T => {
3756
- const startedAt = performance.now();
3757
- const value = read();
3758
- if (profile)
3759
- this.firstFoldViewportTimings[select] = Math.round((performance.now() - startedAt) * 10) / 10;
3760
- if (profile)
3761
- console.info(
3762
- JSON.stringify({
3763
- event: "dashboard_first_fold_select",
3764
- select,
3765
- elapsed_ms: Math.round((performance.now() - startedAt) * 10) / 10,
3766
- }),
3767
- );
3768
- return value;
3769
- };
3770
- const row = timed("viewport_overview", () => this.database
3771
- .prepare(`WITH totals AS (
3772
- SELECT SUM(CASE WHEN activity_source_kind='agent_run' THEN 1 ELSE 0 END) AS agent_invocation_count,
3773
- SUM(llm_call_count) AS llm_call_count,
3774
- SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd,
3775
- SUM(cost_brl) AS total_cost_brl FROM activities
3776
- ), run_totals AS (
3777
- SELECT COUNT(*) AS total_run_count FROM canonical_run_index
3778
- ), duration_totals AS (
3779
- SELECT ROUND(SUM(attributed_token_active_ms)) AS total_duration_ms,
3780
- COUNT(DISTINCT occurred_day_sp) AS execution_day_count
3781
- FROM attributed_token_active_facts
3782
- ), bounds AS (
3783
- SELECT MIN(day) AS start_day, MAX(day) AS end_day, COUNT(*) AS day_count FROM (
3784
- SELECT occurred_day_sp AS day FROM activities
3785
- UNION SELECT occurred_day_sp AS day FROM canonical_work_items
3786
- UNION SELECT occurred_day_sp AS day FROM canonical_artifacts
3787
- )
3788
- ), canonical_items AS MATERIALIZED (
3789
- SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind,
3790
- json_extract(payload_json, '$.id') AS id,
3791
- json_extract(payload_json, '$.status') AS status,
3792
- json_extract(payload_json, '$.risk') AS risk,
3793
- json_extract(payload_json, '$.occurred_at') AS occurred_at
3794
- FROM zaip_aios_activities WHERE payload_type='work_item'
3795
- ), items AS MATERIALIZED (
3796
- SELECT entity_kind, id, status, risk, occurred_at FROM canonical_items
3797
- UNION ALL SELECT 'task', a.task_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3798
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.task_id IS NOT NULL
3799
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='task' AND c.id=a.task_id)
3800
- GROUP BY task_id
3801
- UNION ALL SELECT 'spec', a.spec_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3802
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.spec_id IS NOT NULL
3803
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='spec' AND c.id=a.spec_id)
3804
- GROUP BY spec_id
3805
- UNION ALL SELECT 'prd', a.prd_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3806
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.prd_id IS NOT NULL
3807
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='prd' AND c.id=a.prd_id)
3808
- GROUP BY prd_id
3809
- ), item_totals AS (
3810
- SELECT
3811
- SUM(entity_kind='task') AS task_count,
3812
- SUM(entity_kind='task' AND status='completed') AS completed_task_count,
3813
- MAX(CASE WHEN entity_kind='task' THEN occurred_at END) AS latest_task_created_at,
3814
- SUM(entity_kind='spec') AS spec_count,
3815
- SUM(entity_kind='spec' AND status IN ('completed','delivered')) AS completed_spec_count,
3816
- SUM(entity_kind='prd') AS prd_count,
3817
- SUM(entity_kind='prd' AND status IN ('completed','delivered')) AS completed_prd_count
3818
- FROM items
3819
- ), task_status AS (
3820
- SELECT json_group_object(status, task_count) AS task_status_json FROM (
3821
- SELECT COALESCE(status, 'draft') AS status, COUNT(*) AS task_count
3822
- FROM items WHERE entity_kind='task' GROUP BY COALESCE(status, 'draft')
3823
- )
3824
- ), task_risk AS (
3825
- SELECT json_group_object(risk, task_count) AS task_risk_json FROM (
3826
- SELECT COALESCE(risk, 'standard') AS risk, COUNT(*) AS task_count
3827
- FROM items WHERE entity_kind='task' GROUP BY COALESCE(risk, 'standard')
3828
- )
3829
- ) SELECT totals.*, run_totals.*, duration_totals.*, bounds.*, item_totals.*, task_status.*, task_risk.*
3830
- FROM totals CROSS JOIN run_totals CROSS JOIN duration_totals CROSS JOIN bounds
3831
- CROSS JOIN item_totals CROSS JOIN task_status CROSS JOIN task_risk`)
3832
- .get() as Record<string, unknown>);
3833
- const number = (value: unknown, fallback = 0) =>
3834
- typeof value === "number" ? value : fallback;
3835
- const nullableNumber = (value: unknown) => (typeof value === "number" ? value : null);
3836
- this.firstFoldViewportCache = {
3837
- aggregate: {
3838
- days: number(row.day_count),
3839
- start_day: typeof row.start_day === "string" ? row.start_day : null,
3840
- end_day: typeof row.end_day === "string" ? row.end_day : null,
3841
- task_count: number(row.task_count),
3842
- completed_task_count: number(row.completed_task_count),
3843
- spec_count: number(row.spec_count),
3844
- completed_spec_count: number(row.completed_spec_count),
3845
- prd_count: number(row.prd_count),
3846
- completed_prd_count: number(row.completed_prd_count),
3847
- total_run_count: number(row.total_run_count),
3848
- agent_invocation_count: number(row.agent_invocation_count),
3849
- llm_call_count: nullableNumber(row.llm_call_count),
3850
- duration_ms: nullableNumber(row.total_duration_ms),
3851
- total_tokens: nullableNumber(row.total_tokens),
3852
- total_cost_usd: nullableNumber(row.total_cost_usd),
3853
- cost_brl: nullableNumber(row.total_cost_brl),
3854
- execution_day_count: number(row.execution_day_count),
3855
- latest_task_created_at:
3856
- typeof row.latest_task_created_at === "string" ? row.latest_task_created_at : null,
3857
- },
3858
- task_status_counts:
3859
- typeof row.task_status_json === "string" ? JSON.parse(row.task_status_json) : {},
3860
- task_risk_counts:
3861
- typeof row.task_risk_json === "string" ? JSON.parse(row.task_risk_json) : {},
3862
- };
3779
+ this.firstFoldViewportCache = readFirstFoldViewport(
3780
+ this.database,
3781
+ this.firstFoldViewportTimings,
3782
+ );
3863
3783
  return this.firstFoldViewportCache;
3864
3784
  }
3865
3785
 
@@ -3889,7 +3809,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3889
3809
  AND (? IS NULL OR f.occurred_day_sp >= ?) AND (? IS NULL OR f.occurred_day_sp <= ?)
3890
3810
  GROUP BY a.task_id`)
3891
3811
  .all(from, from, to, to) as Array<{ task_id: string; duration_ms: number }>;
3892
- const durationByTask = new Map(durationRows.map((row) => [row.task_id, Number(row.duration_ms)]));
3812
+ const durationByTask = new Map(
3813
+ durationRows.map((row) => [row.task_id, Number(row.duration_ms)]),
3814
+ );
3893
3815
  return facts
3894
3816
  .map((row) => ({
3895
3817
  ...row,
@@ -3935,14 +3857,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3935
3857
  }>;
3936
3858
  const knownDurationRuns = runs.filter(
3937
3859
  (run) =>
3938
- run.run_role === "root" &&
3939
- run.phase === "execution" &&
3940
- typeof run.duration_ms === "number",
3860
+ run.run_role === "root" && run.phase === "execution" && typeof run.duration_ms === "number",
3941
3861
  ).length;
3942
- const durationByDay = this.executionDurationByDay(
3943
- { from: null, to: null },
3944
- taskId,
3945
- );
3862
+ const durationByDay = this.executionDurationByDay({ from: null, to: null }, taskId);
3946
3863
  const duration =
3947
3864
  durationByDay.size === 0
3948
3865
  ? null
@@ -4018,11 +3935,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4018
3935
  const to = filter.to?.slice(0, 10) ?? null;
4019
3936
  // Financial periods are source-session periods when available. This keeps
4020
3937
  // a retrospective capture on its observed day rather than its write day.
4021
- const periodDay = "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
4022
- const clauses = [
4023
- `(? IS NULL OR ${periodDay} >= ?)`,
4024
- `(? IS NULL OR ${periodDay} <= ?)`,
4025
- ];
3938
+ const periodDay =
3939
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
3940
+ const clauses = [`(? IS NULL OR ${periodDay} >= ?)`, `(? IS NULL OR ${periodDay} <= ?)`];
4026
3941
  const values: Array<string | number | null> = [from, from, to, to];
4027
3942
  const add = (column: string, items?: readonly string[]) => {
4028
3943
  if (items !== undefined && items.length > 0) {
@@ -4041,7 +3956,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4041
3956
  const providerValues = (filter.providers ?? []).filter((item) => item !== "unavailable");
4042
3957
  const modelValues = (filter.models ?? []).filter((item) => item !== "unavailable");
4043
3958
  const requestsUnavailable =
4044
- filter.providers?.includes("unavailable") === true || filter.models?.includes("unavailable") === true;
3959
+ filter.providers?.includes("unavailable") === true ||
3960
+ filter.models?.includes("unavailable") === true;
4045
3961
  if (requestsUnavailable) {
4046
3962
  // Keep the pre-existing explicit-unavailable behaviour; it describes a
4047
3963
  // missing invocation attribute rather than a provider/model pair.
@@ -4069,7 +3985,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4069
3985
  segmentClauses.push(`f.model IN (${modelValues.map(() => "?").join(",")})`);
4070
3986
  values.push(...modelValues);
4071
3987
  }
4072
- clauses.push(`EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id = i.agent_invocation_id AND ${segmentClauses.join(" AND ")})`);
3988
+ clauses.push(
3989
+ `EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id = i.agent_invocation_id AND ${segmentClauses.join(" AND ")})`,
3990
+ );
4073
3991
  }
4074
3992
  return this.database
4075
3993
  .prepare(`SELECT i.*, CASE COUNT(DISTINCT s.provider) WHEN 0 THEN NULL WHEN 1 THEN MIN(s.provider) ELSE 'mixed' END AS provider, CASE COUNT(DISTINCT s.model) WHEN 0 THEN NULL WHEN 1 THEN MIN(s.model) ELSE 'mixed' END AS model,
@@ -4090,7 +4008,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4090
4008
  */
4091
4009
  costMonitorFactualDurationByDay(
4092
4010
  filter: Parameters<SqliteReadModelFileSystem["invocationCostRows"]>[0],
4093
- financialRows: readonly Record<string, unknown>[] = this.costMonitorFinancialActivityRows(filter),
4011
+ financialRows: readonly Record<string, unknown>[] = this.costMonitorFinancialActivityRows(
4012
+ filter,
4013
+ ),
4094
4014
  ): Map<string, { known_sum: number; eligible_runs: number; known_runs: number }> {
4095
4015
  const rows = financialRows;
4096
4016
  const activityIds = new Set<string>();
@@ -4105,7 +4025,10 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4105
4025
  ? row.occurred_at
4106
4026
  : null;
4107
4027
  if (timestamp === null) continue;
4108
- const hasFinancialFact = typeof row.total_tokens === "number" || typeof row.cost_brl === "number" || typeof row.cost_usd === "number";
4028
+ const hasFinancialFact =
4029
+ typeof row.total_tokens === "number" ||
4030
+ typeof row.cost_brl === "number" ||
4031
+ typeof row.cost_usd === "number";
4109
4032
  if (!hasFinancialFact) continue;
4110
4033
  // SQLite exposes the durable activity key as an integer; normalize it
4111
4034
  // before joining it to the JSON-backed temporal fact key.
@@ -4122,7 +4045,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4122
4045
  // local session can be refreshed instead of failing its Cost Monitor.
4123
4046
  const durationFactTable =
4124
4047
  this.database
4125
- .prepare("SELECT 1 AS present FROM sqlite_master WHERE name = 'attributed_token_active_facts'")
4048
+ .prepare(
4049
+ "SELECT 1 AS present FROM sqlite_master WHERE name = 'attributed_token_active_facts'",
4050
+ )
4126
4051
  .get() !== undefined
4127
4052
  ? "attributed_token_active_facts"
4128
4053
  : "zaip_vw_attributed_token_active_facts";
@@ -4168,7 +4093,10 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4168
4093
  ids.add(activityId);
4169
4094
  unavailableByDay.set(day, ids);
4170
4095
  }
4171
- const byDay = new Map<string, { known_sum: number; eligible_runs: number; known_runs: number }>();
4096
+ const byDay = new Map<
4097
+ string,
4098
+ { known_sum: number; eligible_runs: number; known_runs: number }
4099
+ >();
4172
4100
  for (const day of new Set([...factsByDay.keys(), ...unavailableByDay.keys()])) {
4173
4101
  const facts = factsByDay.get(day) ?? { known_sum: 0, activityIds: new Set<string>() };
4174
4102
  const unavailableIds = unavailableByDay.get(day) ?? new Set<string>();
@@ -4227,7 +4155,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4227
4155
  }
4228
4156
 
4229
4157
  /** Cost Monitor's historical financial population, independent of Agent-only views. */
4230
- costMonitorFinancialActivityRows(filter: Parameters<SqliteReadModelFileSystem["activityCostRows"]>[0]) {
4158
+ costMonitorFinancialActivityRows(
4159
+ filter: Parameters<SqliteReadModelFileSystem["activityCostRows"]>[0],
4160
+ ) {
4231
4161
  // Activity capture day can differ from the observed source session day.
4232
4162
  // Keep that source-session population in SQL: the previous implementation
4233
4163
  // read every activity into JS and issued a usage lookup for each filtered
@@ -4235,7 +4165,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4235
4165
  const { from, to, providers = [], models = [] } = filter;
4236
4166
  const sourceFrom = costMonitorFilterDay(from);
4237
4167
  const sourceTo = costMonitorFilterDay(to);
4238
- const sourceDay = "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
4168
+ const sourceDay =
4169
+ "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
4239
4170
  const clauses = [
4240
4171
  "(a.total_tokens IS NOT NULL OR a.cost_brl IS NOT NULL OR a.cost_usd IS NOT NULL)",
4241
4172
  `(? IS NULL OR ${sourceDay} >= ?)`,
@@ -4259,15 +4190,25 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4259
4190
  if (providers.length > 0 || models.length > 0) {
4260
4191
  const usageClauses: string[] = [];
4261
4192
  if (providers.length > 0) {
4262
- usageClauses.push(`COALESCE(u.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`);
4193
+ usageClauses.push(
4194
+ `COALESCE(u.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`,
4195
+ );
4263
4196
  }
4264
4197
  if (models.length > 0) {
4265
- usageClauses.push(`COALESCE(u.model, 'unavailable') IN (${models.map(() => "?").join(",")})`);
4198
+ usageClauses.push(
4199
+ `COALESCE(u.model, 'unavailable') IN (${models.map(() => "?").join(",")})`,
4200
+ );
4266
4201
  }
4267
4202
  const usageValues = [...providers, ...models];
4268
4203
  const directClauses: string[] = [];
4269
- if (providers.length > 0) directClauses.push(`COALESCE(a.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`);
4270
- if (models.length > 0) directClauses.push(`COALESCE(a.model, 'unavailable') IN (${models.map(() => "?").join(",")})`);
4204
+ if (providers.length > 0)
4205
+ directClauses.push(
4206
+ `COALESCE(a.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`,
4207
+ );
4208
+ if (models.length > 0)
4209
+ directClauses.push(
4210
+ `COALESCE(a.model, 'unavailable') IN (${models.map(() => "?").join(",")})`,
4211
+ );
4271
4212
  // Rows without an Agent Invocation keep their activity dimensions;
4272
4213
  // Agent rows retain the segment-level provider/model semantics.
4273
4214
  clauses.push(
@@ -4302,13 +4243,283 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4302
4243
  * per selected day plus conversation/planning/execution and total rows; no
4303
4244
  * activity-shaped record is returned to the server or serialized to HTTP.
4304
4245
  */
4246
+ developerCostAggregate(filter: {
4247
+ from: string | null;
4248
+ to: string | null;
4249
+ accountIds?: readonly string[];
4250
+ }) {
4251
+ const from = costMonitorFilterDay(filter.from);
4252
+ const to = costMonitorFilterDay(filter.to);
4253
+ const clauses = [
4254
+ "a.developer_account_id IS NOT NULL",
4255
+ "(? IS NULL OR a.occurred_day_sp >= ?)",
4256
+ "(? IS NULL OR a.occurred_day_sp <= ?)",
4257
+ ];
4258
+ const values: Array<string | null> = [from, from, to, to];
4259
+ if (filter.accountIds !== undefined && filter.accountIds.length > 0) {
4260
+ clauses.push(`a.developer_account_id IN (${filter.accountIds.map(() => "?").join(",")})`);
4261
+ values.push(...filter.accountIds);
4262
+ }
4263
+ const where = clauses.join(" AND ");
4264
+ const rows = this.database
4265
+ .prepare(`
4266
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where}), completed AS (
4267
+ SELECT a.developer_account_id, COUNT(DISTINCT w.id) AS completed_task_count
4268
+ FROM zaip_vw_canonical_work_items w JOIN selected a ON a.run_id=w.completed_run_id
4269
+ WHERE w.entity_kind='task' AND w.status='completed' GROUP BY a.developer_account_id
4270
+ ) ${developerArtifactCountsCte()}
4271
+ SELECT a.developer_account_id AS account_id, MAX(a.developer_login) AS login_snapshot,
4272
+ CASE WHEN COUNT(DISTINCT a.developer_attribution_source)=1 THEN MAX(a.developer_attribution_source) ELSE 'mixed' END AS attribution_source,
4273
+ COUNT(DISTINCT a.prd_id) AS prd_count, COUNT(DISTINCT a.spec_id) AS spec_count,
4274
+ COUNT(DISTINCT a.task_id) AS task_count, COUNT(*) AS run_count,
4275
+ COALESCE(artifacts.test_count, 0) AS test_count,
4276
+ COALESCE(artifacts.evidence_count, 0) AS evidence_count,
4277
+ SUM(a.attributed_token_active_ms) AS total_duration_ms,
4278
+ COUNT(a.attributed_token_active_ms) AS duration_covered_runs,
4279
+ SUM(a.source_event_count) AS source_event_count,
4280
+ SUM(a.total_tokens) AS total_tokens, SUM(a.cost_usd) AS total_cost_usd, SUM(a.cost_brl) AS total_cost_brl,
4281
+ COUNT(a.total_tokens) AS token_covered_runs, COUNT(a.cost_usd) AS usd_covered_runs, COUNT(a.cost_brl) AS brl_covered_runs,
4282
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count
4283
+ FROM selected a
4284
+ LEFT JOIN completed USING(developer_account_id)
4285
+ LEFT JOIN artifacts USING(developer_account_id)
4286
+ GROUP BY a.developer_account_id ORDER BY total_cost_brl DESC, account_id`)
4287
+ .all(...values, from, from, to, to) as Array<Record<string, unknown>>;
4288
+ // A delivery is factual only when the canonical Task points at the exact
4289
+ // completed Run. Contributors are the distinct attributed GitHub accounts
4290
+ // on that same Run; no timestamp, commit, or deployment inference enters.
4291
+ const delivered = this.database
4292
+ .prepare(`
4293
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where})
4294
+ SELECT selected.developer_account_id AS delivered_by_account_id, w.id, w.title, w.status, w.completed_run_id,
4295
+ GROUP_CONCAT(DISTINCT contributor.developer_account_id || char(31) || COALESCE(contributor.developer_login, '')) AS contributor_pairs
4296
+ FROM zaip_vw_canonical_work_items w
4297
+ JOIN selected ON selected.run_id=w.completed_run_id
4298
+ LEFT JOIN zaip_vw_activities contributor ON contributor.task_id=w.id
4299
+ AND contributor.developer_account_id IS NOT NULL
4300
+ WHERE w.entity_kind='task' AND w.status='completed'
4301
+ GROUP BY selected.developer_account_id, w.id, w.title, w.status, w.completed_run_id
4302
+ ORDER BY w.id`)
4303
+ .all(...values) as Array<Record<string, unknown>>;
4304
+ const deliveriesByAccount = new Map<string, Array<Record<string, unknown>>>();
4305
+ for (const task of delivered) {
4306
+ const accountId = String(task.delivered_by_account_id);
4307
+ const list = deliveriesByAccount.get(accountId) ?? [];
4308
+ list.push({
4309
+ id: task.id,
4310
+ title: task.title,
4311
+ status: task.status,
4312
+ completed_run_id: task.completed_run_id,
4313
+ delivered_by: { account_id: accountId },
4314
+ contributors: String(task.contributor_pairs ?? "")
4315
+ .split(",")
4316
+ .filter(Boolean)
4317
+ .map((pair) => {
4318
+ const [account_id, login_snapshot] = pair.split(String.fromCharCode(31));
4319
+ return { account_id, login_snapshot: login_snapshot || null };
4320
+ }),
4321
+ });
4322
+ deliveriesByAccount.set(accountId, list);
4323
+ }
4324
+ const qualificationForSource: Record<
4325
+ string,
4326
+ "verified" | "cached_fallback" | "human_assigned" | "mixed"
4327
+ > = {
4328
+ verified_local_account: "verified",
4329
+ cached_fallback: "cached_fallback",
4330
+ human_assigned: "human_assigned",
4331
+ mixed: "mixed",
4332
+ };
4333
+ for (const row of rows) {
4334
+ row.completed_tasks = deliveriesByAccount.get(String(row.account_id)) ?? [];
4335
+ row.qualification = qualificationForSource[String(row.attribution_source)] ?? "unqualified";
4336
+ }
4337
+ return {
4338
+ data_source: "sqlite_read_model" as const,
4339
+ filters: { from, to, account_ids: filter.accountIds ?? [] },
4340
+ rows,
4341
+ totals: {
4342
+ run_count: rows.reduce((sum, row) => sum + Number(row.run_count ?? 0), 0),
4343
+ completed_task_count: rows.reduce(
4344
+ (sum, row) => sum + Number(row.completed_task_count ?? 0),
4345
+ 0,
4346
+ ),
4347
+ total_cost_usd: sumKnownDeveloperMetric(rows, "total_cost_usd"),
4348
+ total_cost_brl: sumKnownDeveloperMetric(rows, "total_cost_brl"),
4349
+ },
4350
+ };
4351
+ }
4352
+
4353
+ /**
4354
+ * Operations-only developer projection. It deliberately keeps the Cost
4355
+ * Monitor aggregate unchanged: status, kind and risk are derived solely
4356
+ * from canonical Task records linked to the developer's observed Task ids.
4357
+ */
4358
+ operationsDeveloperAggregate(filter: {
4359
+ from: string | null;
4360
+ to: string | null;
4361
+ accountIds?: readonly string[];
4362
+ }) {
4363
+ const from = costMonitorFilterDay(filter.from);
4364
+ const to = costMonitorFilterDay(filter.to);
4365
+ const clauses = [
4366
+ "a.developer_account_id IS NOT NULL",
4367
+ "(? IS NULL OR a.occurred_day_sp >= ?)",
4368
+ "(? IS NULL OR a.occurred_day_sp <= ?)",
4369
+ ];
4370
+ const values: Array<string | null> = [from, from, to, to];
4371
+ if (filter.accountIds !== undefined && filter.accountIds.length > 0) {
4372
+ clauses.push(`a.developer_account_id IN (${filter.accountIds.map(() => "?").join(",")})`);
4373
+ values.push(...filter.accountIds);
4374
+ }
4375
+ const where = clauses.join(" AND ");
4376
+ const rows = this.database
4377
+ .prepare(`
4378
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where}),
4379
+ developer_tasks AS (
4380
+ SELECT DISTINCT developer_account_id AS account_id, task_id
4381
+ FROM selected WHERE task_id IS NOT NULL
4382
+ ), canonical_task_facts AS (
4383
+ SELECT developer_tasks.account_id, work.status, work.entity_kind, work.risk
4384
+ FROM developer_tasks JOIN zaip_vw_canonical_work_items work
4385
+ ON work.entity_kind='task' AND work.id=developer_tasks.task_id
4386
+ ), status_counts AS (
4387
+ SELECT account_id, json_group_object(status, task_count) AS status_counts_json
4388
+ FROM (SELECT account_id, status, COUNT(*) AS task_count FROM canonical_task_facts
4389
+ WHERE status IS NOT NULL GROUP BY account_id, status) GROUP BY account_id
4390
+ ), type_counts AS (
4391
+ SELECT account_id, json_group_object(entity_kind, task_count) AS type_counts_json
4392
+ FROM (SELECT account_id, entity_kind, COUNT(*) AS task_count FROM canonical_task_facts
4393
+ WHERE entity_kind IS NOT NULL GROUP BY account_id, entity_kind) GROUP BY account_id
4394
+ ), risk_counts AS (
4395
+ SELECT account_id, json_group_object(risk, task_count) AS risk_counts_json
4396
+ FROM (SELECT account_id, risk, COUNT(*) AS task_count FROM canonical_task_facts
4397
+ WHERE risk IS NOT NULL GROUP BY account_id, risk) GROUP BY account_id
4398
+ ), completed AS (
4399
+ SELECT account_id, COUNT(*) AS completed_task_count
4400
+ FROM canonical_task_facts WHERE status='completed'
4401
+ GROUP BY account_id
4402
+ )
4403
+ -- task_count is the distinct, observed Task-id population attributed
4404
+ -- to this developer in the selected period. Canonical records do not
4405
+ -- retain a creator identity, so this projection never infers one.
4406
+ SELECT selected.developer_account_id AS account_id, MAX(selected.developer_login) AS login_snapshot,
4407
+ COUNT(DISTINCT selected.task_id) AS task_count,
4408
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count,
4409
+ SUM(selected.attributed_token_active_ms) AS total_duration_ms,
4410
+ SUM(selected.total_tokens) AS total_tokens,
4411
+ SUM(selected.cost_brl) AS total_cost_brl,
4412
+ status_counts.status_counts_json, type_counts.type_counts_json, risk_counts.risk_counts_json
4413
+ FROM selected
4414
+ LEFT JOIN completed ON completed.account_id=selected.developer_account_id
4415
+ LEFT JOIN status_counts ON status_counts.account_id=selected.developer_account_id
4416
+ LEFT JOIN type_counts ON type_counts.account_id=selected.developer_account_id
4417
+ LEFT JOIN risk_counts ON risk_counts.account_id=selected.developer_account_id
4418
+ GROUP BY selected.developer_account_id
4419
+ ORDER BY total_cost_brl DESC, account_id`)
4420
+ .all(...values) as Array<Record<string, unknown>>;
4421
+ const distribution = (value: unknown): Record<string, number> | null => {
4422
+ if (typeof value !== "string") return null;
4423
+ try {
4424
+ const parsed = JSON.parse(value) as Record<string, unknown>;
4425
+ return Object.fromEntries(
4426
+ Object.entries(parsed).flatMap(([key, count]) =>
4427
+ typeof count === "number" && Number.isFinite(count) ? [[key, count]] : [],
4428
+ ),
4429
+ );
4430
+ } catch {
4431
+ return null;
4432
+ }
4433
+ };
4434
+ for (const row of rows) {
4435
+ row.status_counts = distribution(row.status_counts_json);
4436
+ row.type_counts = distribution(row.type_counts_json);
4437
+ row.risk_counts = distribution(row.risk_counts_json);
4438
+ delete row.status_counts_json;
4439
+ delete row.type_counts_json;
4440
+ delete row.risk_counts_json;
4441
+ }
4442
+ return {
4443
+ data_source: "sqlite_read_model" as const,
4444
+ filters: { from, to, account_ids: filter.accountIds ?? [] },
4445
+ rows,
4446
+ };
4447
+ }
4448
+
4305
4449
  costMonitorSummaryAggregate(
4306
4450
  filter: Parameters<SqliteReadModelFileSystem["activityCostRows"]>[0],
4307
- ) {
4451
+ ): CostSummarySource {
4308
4452
  const { from, to, providers = [], models = [] } = filter;
4309
4453
  const sourceFrom = costMonitorFilterDay(from);
4310
4454
  const sourceTo = costMonitorFilterDay(to);
4311
- const sourceDay = "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
4455
+ const cacheKey = JSON.stringify({ ...filter, from: sourceFrom, to: sourceTo });
4456
+ const generationCached = this.costMonitorSummaryCache.get(cacheKey);
4457
+ if (generationCached !== undefined) return structuredClone(generationCached);
4458
+ const remember = (value: CostSummarySource) => {
4459
+ if (this.costMonitorSummaryCache.size >= 64) {
4460
+ const oldest = this.costMonitorSummaryCache.keys().next().value as string | undefined;
4461
+ if (oldest !== undefined) this.costMonitorSummaryCache.delete(oldest);
4462
+ }
4463
+ this.costMonitorSummaryCache.set(cacheKey, structuredClone(value));
4464
+ return structuredClone(value);
4465
+ };
4466
+ const unfilteredAllTime =
4467
+ sourceFrom === null &&
4468
+ sourceTo === null &&
4469
+ [
4470
+ providers,
4471
+ models,
4472
+ filter.agents,
4473
+ filter.phases,
4474
+ filter.statuses,
4475
+ filter.tasks,
4476
+ filter.specs,
4477
+ filter.prds,
4478
+ filter.executions,
4479
+ filter.changes,
4480
+ ].every((values) => values === undefined || values.length === 0);
4481
+ if (unfilteredAllTime) {
4482
+ const cached = this.database
4483
+ .prepare(
4484
+ "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='cost_monitor_summary_cache' ORDER BY recorded_at DESC LIMIT 1",
4485
+ )
4486
+ .get() as { payload_json?: string } | undefined;
4487
+ if (typeof cached?.payload_json === "string")
4488
+ return remember(JSON.parse(cached.payload_json) as CostSummarySource);
4489
+ }
4490
+ if (
4491
+ sourceFrom !== null &&
4492
+ sourceTo !== null &&
4493
+ [
4494
+ providers,
4495
+ models,
4496
+ filter.agents,
4497
+ filter.phases,
4498
+ filter.statuses,
4499
+ filter.tasks,
4500
+ filter.specs,
4501
+ filter.prds,
4502
+ filter.executions,
4503
+ filter.changes,
4504
+ ].every((values) => values === undefined || values.length === 0)
4505
+ ) {
4506
+ const cached = this.database
4507
+ .prepare(
4508
+ "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='cost_monitor_summary_month_cache' ORDER BY recorded_at DESC LIMIT 1",
4509
+ )
4510
+ .get() as { payload_json?: string } | undefined;
4511
+ if (typeof cached?.payload_json === "string") {
4512
+ const payload = JSON.parse(cached.payload_json) as {
4513
+ from: string;
4514
+ to: string;
4515
+ aggregate: CostSummarySource;
4516
+ };
4517
+ if (payload.from === sourceFrom && payload.to === sourceTo)
4518
+ return remember(payload.aggregate);
4519
+ }
4520
+ }
4521
+ const sourceDay =
4522
+ "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
4312
4523
  const clauses = [
4313
4524
  "(a.total_tokens IS NOT NULL OR a.cost_brl IS NOT NULL OR a.cost_usd IS NOT NULL)",
4314
4525
  `(? IS NULL OR ${sourceDay} >= ?)`,
@@ -4334,14 +4545,22 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4334
4545
  const usageValues: string[] = [];
4335
4546
  const directValues: string[] = [];
4336
4547
  if (providers.length > 0) {
4337
- usageClauses.push(`COALESCE(u.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`);
4338
- directClauses.push(`COALESCE(a.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`);
4548
+ usageClauses.push(
4549
+ `COALESCE(u.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`,
4550
+ );
4551
+ directClauses.push(
4552
+ `COALESCE(a.provider, 'unavailable') IN (${providers.map(() => "?").join(",")})`,
4553
+ );
4339
4554
  usageValues.push(...providers);
4340
4555
  directValues.push(...providers);
4341
4556
  }
4342
4557
  if (models.length > 0) {
4343
- usageClauses.push(`COALESCE(u.model, 'unavailable') IN (${models.map(() => "?").join(",")})`);
4344
- directClauses.push(`COALESCE(a.model, 'unavailable') IN (${models.map(() => "?").join(",")})`);
4558
+ usageClauses.push(
4559
+ `COALESCE(u.model, 'unavailable') IN (${models.map(() => "?").join(",")})`,
4560
+ );
4561
+ directClauses.push(
4562
+ `COALESCE(a.model, 'unavailable') IN (${models.map(() => "?").join(",")})`,
4563
+ );
4345
4564
  usageValues.push(...models);
4346
4565
  directValues.push(...models);
4347
4566
  }
@@ -4387,21 +4606,34 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4387
4606
  FROM selected GROUP BY day, phase_key ORDER BY day, phase_key`)
4388
4607
  .all(...values) as Array<Record<string, unknown>>;
4389
4608
  const nullableColumns = [
4390
- "duration_ms", "input_tokens", "cached_input_tokens", "output_tokens",
4391
- "reasoning_tokens", "total_tokens", "cost_usd", "cost_brl",
4609
+ "duration_ms",
4610
+ "input_tokens",
4611
+ "cached_input_tokens",
4612
+ "output_tokens",
4613
+ "reasoning_tokens",
4614
+ "total_tokens",
4615
+ "cost_usd",
4616
+ "cost_brl",
4392
4617
  ] as const;
4393
4618
  const additiveColumns = [
4394
- "eligible_runs", "events", "duration_known", "tokens_known", "usd_known", "brl_known",
4619
+ "eligible_runs",
4620
+ "events",
4621
+ "duration_known",
4622
+ "tokens_known",
4623
+ "usd_known",
4624
+ "brl_known",
4395
4625
  ] as const;
4396
4626
  const rollup = (key: string, rows: readonly Record<string, unknown>[]) => {
4397
4627
  const result: Record<string, unknown> = { key };
4398
4628
  for (const column of additiveColumns)
4399
- result[column] = rows.reduce((sum, row) => sum + (typeof row[column] === "number" ? Number(row[column]) : 0), 0);
4629
+ result[column] = rows.reduce(
4630
+ (sum, row) => sum + (typeof row[column] === "number" ? Number(row[column]) : 0),
4631
+ 0,
4632
+ );
4400
4633
  for (const column of nullableColumns) {
4401
4634
  const present = rows.filter((row) => typeof row[column] === "number");
4402
- result[column] = present.length === 0
4403
- ? null
4404
- : present.reduce((sum, row) => sum + Number(row[column]), 0);
4635
+ result[column] =
4636
+ present.length === 0 ? null : present.reduce((sum, row) => sum + Number(row[column]), 0);
4405
4637
  }
4406
4638
  const taskIds = new Set<string>();
4407
4639
  for (const row of rows) {
@@ -4413,10 +4645,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4413
4645
  return result;
4414
4646
  };
4415
4647
  const phaseKeys = ["conversation", "planning", "execution"] as const;
4416
- const phaseRows = phaseKeys.map((key) => rollup(key, factRows.filter((row) => row.phase_key === key)));
4648
+ const phaseRows = phaseKeys.map((key) =>
4649
+ rollup(
4650
+ key,
4651
+ factRows.filter((row) => row.phase_key === key),
4652
+ ),
4653
+ );
4417
4654
  phaseRows.push(rollup("total", factRows));
4418
4655
  const dailyRows = [...new Set(factRows.map((row) => String(row.day)))].map((day) =>
4419
- rollup(day, factRows.filter((row) => String(row.day) === day)),
4656
+ rollup(
4657
+ day,
4658
+ factRows.filter((row) => String(row.day) === day),
4659
+ ),
4420
4660
  );
4421
4661
  const structuralRows = this.database
4422
4662
  .prepare(`WITH canonical_ids AS MATERIALIZED (
@@ -4449,16 +4689,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4449
4689
  AND (? IS NULL OR day >= ?)
4450
4690
  AND (? IS NULL OR day <= ?)
4451
4691
  GROUP BY day
4452
- UNION ALL
4453
- SELECT occurred_day_sp,
4454
- 0, 0, 0,
4455
- SUM(CASE WHEN artifact_kind='validation' THEN 1 ELSE 0 END),
4456
- SUM(CASE WHEN artifact_kind='evidence' THEN 1 ELSE 0 END)
4457
- FROM canonical_artifacts
4458
- WHERE occurred_day_sp IS NOT NULL
4459
- AND (? IS NULL OR occurred_day_sp >= ?)
4460
- AND (? IS NULL OR occurred_day_sp <= ?)
4461
- GROUP BY occurred_day_sp
4692
+ ${artifactStructuralRowsSql()}
4462
4693
  )
4463
4694
  SELECT day,
4464
4695
  SUM(prd_count) AS prd_count,
@@ -4467,7 +4698,16 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4467
4698
  SUM(validation_count) AS validation_count,
4468
4699
  SUM(evidence_count) AS evidence_count
4469
4700
  FROM structural GROUP BY day ORDER BY day`)
4470
- .all(sourceFrom, sourceFrom, sourceTo, sourceTo, sourceFrom, sourceFrom, sourceTo, sourceTo) as Array<Record<string, unknown>>;
4701
+ .all(
4702
+ sourceFrom,
4703
+ sourceFrom,
4704
+ sourceTo,
4705
+ sourceTo,
4706
+ sourceFrom,
4707
+ sourceFrom,
4708
+ sourceTo,
4709
+ sourceTo,
4710
+ ) as Array<Record<string, unknown>>;
4471
4711
  const number = (value: unknown) => (typeof value === "number" ? value : 0);
4472
4712
  const nullable = (value: unknown) => (typeof value === "number" ? value : null);
4473
4713
  const metric = (row?: Record<string, unknown>) => {
@@ -4588,7 +4828,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4588
4828
  // phases and daily rows. Calling the paginated view here was tempting but
4589
4829
  // silently loses multi-value and lineage filters (Task/SPEC/PRD/etc.).
4590
4830
  // This remains one bounded SQL aggregate, without transporting Runs.
4591
- const summaryModelRows = this.database.prepare(`${selected},
4831
+ const summaryModelRows = this.database
4832
+ .prepare(`${selected},
4592
4833
  segment_totals AS (
4593
4834
  SELECT agent_invocation_id, SUM(total_tokens) AS total_tokens,
4594
4835
  SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
@@ -4630,7 +4871,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4630
4871
  NULL AS total_duration, 0 AS duration_unknown, 0 AS duration_known,
4631
4872
  0 AS source_event_count, SUM(total_tokens) AS total_tokens,
4632
4873
  SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl
4633
- FROM components GROUP BY key ORDER BY total_cost_brl DESC, key`).all(...values) as Array<Record<string, unknown>>;
4874
+ FROM components GROUP BY key ORDER BY total_cost_brl DESC, key`)
4875
+ .all(...values) as Array<Record<string, unknown>>;
4634
4876
  const model_groups = summaryModelRows.map((row) => {
4635
4877
  const duration = nullable(row.total_duration);
4636
4878
  const durationUnknown = number(row.duration_unknown);
@@ -4659,7 +4901,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4659
4901
  coverage: { events: 0, tokens: 0, usd: 0, brl: 0 },
4660
4902
  };
4661
4903
  });
4662
- return {
4904
+ return remember({
4663
4905
  periods: { daily: periods },
4664
4906
  aggregate_usage: {
4665
4907
  conversation,
@@ -4673,7 +4915,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4673
4915
  model_groups,
4674
4916
  cost_monitor_daily_duration: durations,
4675
4917
  daily_rows: compactDailyRows,
4676
- };
4918
+ });
4677
4919
  }
4678
4920
 
4679
4921
  /** Keyset cursor or LIMIT/OFFSET pagination for materialized work summaries. */
@@ -4696,7 +4938,11 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4696
4938
  const from = filter.from?.slice(0, 10) ?? null;
4697
4939
  const to = filter.to?.slice(0, 10) ?? null;
4698
4940
  const cursorValue = parseSummaryCursor(cursor);
4699
- const clauses = ["entity_kind = ?", "(? IS NULL OR occurred_day_sp >= ?)", "(? IS NULL OR occurred_day_sp <= ?)"];
4941
+ const clauses = [
4942
+ "entity_kind = ?",
4943
+ "(? IS NULL OR occurred_day_sp >= ?)",
4944
+ "(? IS NULL OR occurred_day_sp <= ?)",
4945
+ ];
4700
4946
  const values: Array<string | number | null> = [kind, from, from, to, to];
4701
4947
  if (filter.status) {
4702
4948
  clauses.push("status = ?");
@@ -4748,6 +4994,21 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4748
4994
  };
4749
4995
  }
4750
4996
 
4997
+ /** Canonical page for the Board: keeps telemetry-only historical rows out. */
4998
+ canonicalWorkPage(
4999
+ kind: SqliteSummaryKind,
5000
+ filter: { from: string | null; to: string | null },
5001
+ offset: number,
5002
+ limit: number,
5003
+ ) {
5004
+ return this.viewPage("operations_work_list", {
5005
+ ...filter,
5006
+ kind: kind === "task" ? "all-tasks" : kind,
5007
+ offset,
5008
+ limit,
5009
+ });
5010
+ }
5011
+
4751
5012
  overviewPage(period: "day" | "month" | "year", offset: number, limit: number) {
4752
5013
  const total = Number(
4753
5014
  (
@@ -4780,25 +5041,30 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4780
5041
  };
4781
5042
  }
4782
5043
 
4783
- /**
4784
- * Generation-local cache of the complete public page response. It caches
4785
- * post-enrichment rows and totals, never raw facts, so the daily Operations
4786
- * duration and Cost Monitor pre-pagination subtotals retain their contracts.
4787
- */
4788
5044
  viewPage(
4789
5045
  view: SqliteViewName,
4790
5046
  input: {
4791
- from?: string | null; to?: string | null; query?: string; status?: string; risk?: string;
4792
- provider?: string; model?: string; agent?: string; phase?: string; origin?: string;
4793
- workType?: string; priority?: string; offset?: number; limit?: number;
5047
+ from?: string | null;
5048
+ to?: string | null;
5049
+ query?: string;
5050
+ status?: string;
5051
+ risk?: string;
5052
+ provider?: string;
5053
+ model?: string;
5054
+ agent?: string;
5055
+ phase?: string;
5056
+ origin?: string;
5057
+ workType?: string;
5058
+ priority?: string;
5059
+ kind?: "all" | "all-tasks" | "task" | "standalone-task" | "prd" | "spec";
5060
+ offset?: number;
5061
+ limit?: number;
4794
5062
  },
4795
5063
  ): ReturnType<SqliteReadModelFileSystem["uncachedViewPage"]> {
4796
- // The key includes every filter and pagination input. The cache is scoped
4797
- // to the immutable opened SQLite generation and reopen() clears it, so a
4798
- // launcher refresh cannot expose a page from the previous snapshot.
4799
5064
  const key = `${view}:${JSON.stringify(input)}`;
4800
5065
  const cached = this.viewPageResultCache.get(key);
4801
- if (cached !== undefined) return structuredClone(cached) as ReturnType<SqliteReadModelFileSystem["uncachedViewPage"]>;
5066
+ if (cached !== undefined)
5067
+ return structuredClone(cached) as ReturnType<SqliteReadModelFileSystem["uncachedViewPage"]>;
4802
5068
  const result = this.uncachedViewPage(view, input);
4803
5069
  if (this.viewPageResultCache.size >= 256) {
4804
5070
  const oldest = this.viewPageResultCache.keys().next().value as string | undefined;
@@ -4824,10 +5090,151 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4824
5090
  origin?: string;
4825
5091
  workType?: string;
4826
5092
  priority?: string;
5093
+ kind?: "all" | "all-tasks" | "task" | "standalone-task" | "prd" | "spec";
4827
5094
  offset?: number;
4828
5095
  limit?: number;
4829
5096
  },
4830
5097
  ) {
5098
+ const isUnfilteredAllTimeCostPage =
5099
+ !input.from &&
5100
+ !input.to &&
5101
+ !input.query &&
5102
+ !input.status &&
5103
+ !input.risk &&
5104
+ !input.provider &&
5105
+ !input.model &&
5106
+ !input.agent &&
5107
+ !input.phase &&
5108
+ !input.origin &&
5109
+ !input.workType &&
5110
+ !input.priority;
5111
+ const isDateOnlyCostInvocationPage =
5112
+ view === "cost_monitor_agent_invocations" &&
5113
+ !input.query &&
5114
+ !input.status &&
5115
+ !input.risk &&
5116
+ !input.provider &&
5117
+ !input.model &&
5118
+ !input.agent &&
5119
+ !input.phase &&
5120
+ !input.origin &&
5121
+ !input.workType &&
5122
+ !input.priority;
5123
+ if (isDateOnlyCostInvocationPage) {
5124
+ const projection = filterCostInvocationProjection(
5125
+ this.technicalPayloadRows("cost_agent_invocation_projection_v1"),
5126
+ input,
5127
+ );
5128
+ if (projection.length > 0) {
5129
+ const limit = Math.min(200, Math.max(1, input.limit ?? 100));
5130
+ const offset = Math.max(0, input.offset ?? 0);
5131
+ const rows = projection.slice(offset, offset + limit);
5132
+ return {
5133
+ data_source: "sqlite_read_model" as const,
5134
+ view,
5135
+ offset,
5136
+ limit,
5137
+ total_count: projection.length,
5138
+ has_more: offset + rows.length < projection.length,
5139
+ rows,
5140
+ };
5141
+ }
5142
+ }
5143
+ const remoteAggregateDimensions: Partial<Record<SqliteViewName, string>> = {
5144
+ cost_monitor_zaip_agents: "agent",
5145
+ cost_monitor_executions: "execution",
5146
+ cost_monitor_tasks: "task",
5147
+ cost_monitor_conversations: "conversation",
5148
+ };
5149
+ const remoteAggregateDimension = remoteAggregateDimensions[view];
5150
+ if (isUnfilteredAllTimeCostPage && remoteAggregateDimension !== undefined) {
5151
+ const projection = this.technicalPayloadRows("cost_dimension_aggregate_v1").filter(
5152
+ (row) => row.dimension === remoteAggregateDimension,
5153
+ );
5154
+ const rows = projection
5155
+ .filter((row) => row.row_kind === "row")
5156
+ .map((row) =>
5157
+ view === "cost_monitor_zaip_agents"
5158
+ ? {
5159
+ key: row.key,
5160
+ label: row.label,
5161
+ persona: row.persona,
5162
+ job: row.job,
5163
+ latest_started_at: row.latest_started_at,
5164
+ invocation_count: row.invocation_count,
5165
+ task_count: row.task_count,
5166
+ total_duration: row.total_duration,
5167
+ duration_unknown: row.duration_unknown,
5168
+ duration_known: row.duration_known,
5169
+ source_event_count: row.source_event_count,
5170
+ total_tokens: row.total_tokens,
5171
+ total_cost_usd: row.total_cost_usd,
5172
+ total_cost_brl: row.total_cost_brl,
5173
+ }
5174
+ : {
5175
+ id: row.id,
5176
+ key: row.key,
5177
+ title: row.title,
5178
+ status: row.status,
5179
+ risk: row.risk,
5180
+ first_day: row.first_day,
5181
+ last_day: row.last_day,
5182
+ occurred_at: row.occurred_at,
5183
+ invocation_count: row.invocation_count,
5184
+ llm_call_count: row.llm_call_count,
5185
+ total_duration: row.total_duration,
5186
+ total_input_tokens: row.total_input_tokens,
5187
+ total_cached_input_tokens: row.total_cached_input_tokens,
5188
+ total_output_tokens: row.total_output_tokens,
5189
+ total_reasoning_tokens: row.total_reasoning_tokens,
5190
+ total_tokens: row.total_tokens,
5191
+ total_cost_usd: row.total_cost_usd,
5192
+ total_cost_brl: row.total_cost_brl,
5193
+ },
5194
+ )
5195
+ .sort(
5196
+ (left, right) =>
5197
+ Number(right.total_cost_brl ?? -1) - Number(left.total_cost_brl ?? -1) ||
5198
+ String(left.key ?? "").localeCompare(String(right.key ?? "")),
5199
+ ) as Array<Record<string, unknown>>;
5200
+ const totals = projection.find((row) => row.row_kind === "totals");
5201
+ if (rows.length > 0 && totals !== undefined) {
5202
+ const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5203
+ const offset = Math.max(0, input.offset ?? 0);
5204
+ const page = rows.slice(offset, offset + limit);
5205
+ const publicTotals =
5206
+ view !== "cost_monitor_zaip_agents"
5207
+ ? {
5208
+ entity_count: totals.entity_count,
5209
+ invocation_count: totals.invocation_count,
5210
+ total_duration: totals.total_duration,
5211
+ total_tokens: totals.total_tokens,
5212
+ total_cost_usd: totals.total_cost_usd,
5213
+ total_cost_brl: totals.total_cost_brl,
5214
+ }
5215
+ : {
5216
+ invocation_count: totals.invocation_count,
5217
+ task_count: totals.task_count,
5218
+ total_duration: totals.total_duration,
5219
+ duration_unknown: totals.duration_unknown,
5220
+ duration_known: totals.duration_known,
5221
+ source_event_count: totals.source_event_count,
5222
+ total_tokens: totals.total_tokens,
5223
+ total_cost_usd: totals.total_cost_usd,
5224
+ total_cost_brl: totals.total_cost_brl,
5225
+ };
5226
+ return {
5227
+ data_source: "sqlite_read_model" as const,
5228
+ view,
5229
+ offset,
5230
+ limit,
5231
+ total_count: rows.length,
5232
+ has_more: offset + page.length < rows.length,
5233
+ totals: publicTotals,
5234
+ rows: page,
5235
+ };
5236
+ }
5237
+ }
4831
5238
  const cacheableUnfilteredViews: readonly SqliteViewName[] = [
4832
5239
  "operations_work_kanban",
4833
5240
  "operations_executions",
@@ -4836,9 +5243,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4836
5243
  ];
4837
5244
  if (
4838
5245
  cacheableUnfilteredViews.includes(view) &&
4839
- !input.from && !input.to && !input.query && !input.status && !input.risk &&
4840
- !input.provider && !input.model && !input.agent && !input.phase &&
4841
- !input.origin && !input.workType && !input.priority
5246
+ !input.from &&
5247
+ !input.to &&
5248
+ !input.query &&
5249
+ !input.status &&
5250
+ !input.risk &&
5251
+ !input.provider &&
5252
+ !input.model &&
5253
+ !input.agent &&
5254
+ !input.phase &&
5255
+ !input.origin &&
5256
+ !input.workType &&
5257
+ !input.priority
4842
5258
  ) {
4843
5259
  const rows = this.unfilteredViewRows(view);
4844
5260
  const limit = Math.min(200, Math.max(1, input.limit ?? 100));
@@ -4854,39 +5270,24 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4854
5270
  rows: page,
4855
5271
  };
4856
5272
  }
4857
- // This is the common unfiltered pagination flow used by Operations. Cache
4858
- // the complete, deterministic result once per private SQLite generation so
4859
- // a 974-row list cannot re-run its aggregate SQL for every browser page.
4860
- // Filtered requests deliberately retain the database path below because
4861
- // SQLite owns their LIKE/filter semantics.
4862
- if (
4863
- view === "operations_work_list" &&
4864
- !input.from &&
4865
- !input.to &&
4866
- !input.query &&
4867
- !input.status &&
4868
- !input.risk
4869
- ) {
4870
- const rows = this.operationsWorkListRows();
4871
- const limit = Math.min(200, Math.max(1, input.limit ?? 50));
4872
- const offset = Math.max(0, input.offset ?? 0);
4873
- const page = rows.slice(offset, offset + limit);
5273
+ if (view === "operations_work_list") {
5274
+ const payload =
5275
+ input.from || input.to
5276
+ ? this.firstFoldMonthlyQuick({ from: input.from ?? null, to: input.to ?? null })
5277
+ : this.technicalPayloadRows("first_fold_all_time_board_cache")[0];
5278
+ const cached = operationsWorkListPayloadPage(input, payload);
5279
+ const result = cached ?? operationsWorkListSqlPage(this.database, input);
4874
5280
  return {
4875
5281
  data_source: "sqlite_read_model" as const,
4876
5282
  view,
4877
- offset,
4878
- limit,
4879
- total_count: rows.length,
4880
- has_more: offset + page.length < rows.length,
4881
- rows: page,
5283
+ ...result,
5284
+ rows: attachOperationsWorkListDevelopers(this.database, result.rows, input),
4882
5285
  };
4883
5286
  }
4884
5287
  if (view === "cost_monitor_agent_invocations") {
4885
- const sourceDay = "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
4886
- const clauses = [
4887
- `(? IS NULL OR ${sourceDay} >= ?)`,
4888
- `(? IS NULL OR ${sourceDay} <= ?)`,
4889
- ];
5288
+ const sourceDay =
5289
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
5290
+ const clauses = [`(? IS NULL OR ${sourceDay} >= ?)`, `(? IS NULL OR ${sourceDay} <= ?)`];
4890
5291
  const from = costMonitorFilterDay(input.from);
4891
5292
  const to = costMonitorFilterDay(input.to);
4892
5293
  const values: Array<string | number | null> = [from, from, to, to];
@@ -4902,7 +5303,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4902
5303
  const usageSegmentClauses: string[] = [];
4903
5304
  if (input.provider) {
4904
5305
  if (input.provider === "unavailable") {
4905
- clauses.push("((i.agent_invocation_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')) OR (i.agent_invocation_id IS NULL AND (i.provider IS NULL OR i.provider = 'unavailable')))");
5306
+ clauses.push(
5307
+ "((i.agent_invocation_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')) OR (i.agent_invocation_id IS NULL AND (i.provider IS NULL OR i.provider = 'unavailable')))",
5308
+ );
4906
5309
  } else {
4907
5310
  usageSegmentClauses.push("f.provider=?");
4908
5311
  values.push(input.provider);
@@ -4910,14 +5313,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4910
5313
  }
4911
5314
  if (input.model) {
4912
5315
  if (input.model === "unavailable") {
4913
- clauses.push("((i.agent_invocation_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')) OR (i.agent_invocation_id IS NULL AND (i.model IS NULL OR i.model = 'unavailable')))");
5316
+ clauses.push(
5317
+ "((i.agent_invocation_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')) OR (i.agent_invocation_id IS NULL AND (i.model IS NULL OR i.model = 'unavailable')))",
5318
+ );
4914
5319
  } else {
4915
5320
  usageSegmentClauses.push("f.model=?");
4916
5321
  values.push(input.model);
4917
5322
  }
4918
5323
  }
4919
5324
  if (usageSegmentClauses.length > 0)
4920
- clauses.push(`EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${usageSegmentClauses.join(" AND ")})`);
5325
+ clauses.push(
5326
+ `EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${usageSegmentClauses.join(" AND ")})`,
5327
+ );
4921
5328
  if (input.query) {
4922
5329
  clauses.push("(i.agent_invocation_id LIKE ? OR i.task_id LIKE ? OR i.execution_id LIKE ?)");
4923
5330
  values.push(`%${input.query}%`, `%${input.query}%`, `%${input.query}%`);
@@ -4925,16 +5332,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4925
5332
  const where = clauses.join(" AND ");
4926
5333
  const limit = Math.min(200, Math.max(1, input.limit ?? 100));
4927
5334
  const offset = Math.max(0, input.offset ?? 0);
4928
- let total_count = Number(
5335
+ const total_count = Number(
4929
5336
  (
4930
5337
  this.database
4931
- .prepare(`SELECT COUNT(*) AS count FROM cost_agent_invocations i WHERE ${where}`)
5338
+ .prepare(
5339
+ `SELECT COUNT(*) AS count FROM cost_monitor_agent_invocations i WHERE ${where}`,
5340
+ )
4932
5341
  .get(...values) as { count: number }
4933
5342
  ).count,
4934
5343
  );
4935
- let rows = this.database
5344
+ const rows = this.database
4936
5345
  .prepare(
4937
- `SELECT i.* FROM cost_agent_invocations i WHERE ${where} ORDER BY i.occurred_at DESC, i.agent_invocation_id DESC LIMIT ? OFFSET ?`,
5346
+ `SELECT i.* FROM cost_monitor_agent_invocations i WHERE ${where} ORDER BY i.occurred_at DESC, i.agent_invocation_id DESC LIMIT ? OFFSET ?`,
4938
5347
  )
4939
5348
  .all(...values, limit, offset) as Array<Record<string, unknown>>;
4940
5349
  return {
@@ -4958,14 +5367,53 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4958
5367
  cost_monitor_providers: "provider",
4959
5368
  };
4960
5369
  const filteredCostGroup = filteredCostGroups[view];
5370
+ if (
5371
+ (filteredCostGroup === "agent" ||
5372
+ filteredCostGroup === "model" ||
5373
+ filteredCostGroup === "provider") &&
5374
+ isDateOnlyCostPage(input)
5375
+ ) {
5376
+ const from = costMonitorFilterDay(input.from) ?? "0000-01-01";
5377
+ const to = costMonitorFilterDay(input.to) ?? "9999-12-31";
5378
+ const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5379
+ const offset = Math.max(0, input.offset ?? 0);
5380
+ const fast = readDateOnlyCostDimensionPage(
5381
+ this.database,
5382
+ filteredCostGroup,
5383
+ from,
5384
+ to,
5385
+ limit,
5386
+ offset,
5387
+ );
5388
+ return {
5389
+ data_source: "sqlite_read_model" as const,
5390
+ view,
5391
+ offset,
5392
+ limit,
5393
+ total_count: fast.total_count,
5394
+ has_more: offset + fast.rows.length < fast.total_count,
5395
+ totals: fast.totals,
5396
+ rows: fast.rows,
5397
+ };
5398
+ }
4961
5399
  const isUnfilteredCostDimension =
4962
5400
  (filteredCostGroup === "model" || filteredCostGroup === "provider") &&
4963
- !input.from && !input.to && !input.query && !input.status && !input.risk &&
4964
- !input.provider && !input.model && !input.agent && !input.phase &&
4965
- !input.origin && !input.workType && !input.priority;
5401
+ !input.from &&
5402
+ !input.to &&
5403
+ !input.query &&
5404
+ !input.status &&
5405
+ !input.risk &&
5406
+ !input.provider &&
5407
+ !input.model &&
5408
+ !input.agent &&
5409
+ !input.phase &&
5410
+ !input.origin &&
5411
+ !input.workType &&
5412
+ !input.priority;
4966
5413
  if (isUnfilteredCostDimension) {
4967
5414
  const dimension = filteredCostGroup;
4968
- const payloadRows = this.database.prepare(`SELECT
5415
+ const payloadRows = this.database
5416
+ .prepare(`SELECT
4969
5417
  json_extract(payload_json, '$.key') AS key,
4970
5418
  json_extract(payload_json, '$.label') AS label,
4971
5419
  json_extract(payload_json, '$.invocation_count') AS invocation_count,
@@ -4978,7 +5426,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4978
5426
  FROM zaip_aios_activities
4979
5427
  WHERE payload_type='cost_dimension_aggregate_v1'
4980
5428
  AND json_extract(payload_json, '$.dimension')=?
4981
- ORDER BY total_cost_brl DESC, key`).all(dimension) as Array<Record<string, unknown>>;
5429
+ ORDER BY total_cost_brl DESC, key`)
5430
+ .all(dimension) as Array<Record<string, unknown>>;
4982
5431
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
4983
5432
  const offset = Math.max(0, input.offset ?? 0);
4984
5433
  const rows = payloadRows.slice(offset, offset + limit);
@@ -4995,17 +5444,46 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4995
5444
  if (filteredCostGroup === "agent") {
4996
5445
  const clauses = ["i.zaip_agent IS NOT NULL"];
4997
5446
  const values: Array<string | number> = [];
4998
- if (input.from) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?"); values.push(input.from.slice(0, 10)); }
4999
- if (input.to) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?"); values.push(input.to.slice(0, 10)); }
5000
- if (input.agent) { clauses.push("i.zaip_agent = ?"); values.push(input.agent); }
5001
- if (input.phase) { clauses.push("i.phase = ?"); values.push(input.phase); }
5002
- if (input.status) { clauses.push("i.status = ?"); values.push(input.status); }
5447
+ if (input.from) {
5448
+ clauses.push(
5449
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
5450
+ );
5451
+ values.push(input.from.slice(0, 10));
5452
+ }
5453
+ if (input.to) {
5454
+ clauses.push(
5455
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?",
5456
+ );
5457
+ values.push(input.to.slice(0, 10));
5458
+ }
5459
+ if (input.agent) {
5460
+ clauses.push("i.zaip_agent = ?");
5461
+ values.push(input.agent);
5462
+ }
5463
+ if (input.phase) {
5464
+ clauses.push("i.phase = ?");
5465
+ values.push(input.phase);
5466
+ }
5467
+ if (input.status) {
5468
+ clauses.push("i.status = ?");
5469
+ values.push(input.status);
5470
+ }
5003
5471
  // This read model does not materialize canonical Task priority. Keep
5004
5472
  // the filtered view safe and avoid substituting a different field.
5005
5473
  if (input.priority) clauses.push("0");
5006
- if (input.risk) { clauses.push("t.risk = ?"); values.push(input.risk); }
5474
+ if (input.risk) {
5475
+ clauses.push("t.risk = ?");
5476
+ values.push(input.risk);
5477
+ }
5007
5478
  if (input.origin) {
5008
- const origin = input.origin === "prd" ? "i.prd_id IS NOT NULL" : input.origin === "spec" ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL" : input.origin === "task" ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL" : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL";
5479
+ const origin =
5480
+ input.origin === "prd"
5481
+ ? "i.prd_id IS NOT NULL"
5482
+ : input.origin === "spec"
5483
+ ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL"
5484
+ : input.origin === "task"
5485
+ ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL"
5486
+ : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL";
5009
5487
  clauses.push(origin);
5010
5488
  }
5011
5489
  // SQLite invocation facts do not contain canonical work_type. A work-type
@@ -5013,33 +5491,82 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5013
5491
  if (input.workType) clauses.push("0");
5014
5492
  const segment: string[] = [];
5015
5493
  if (input.provider) {
5016
- if (input.provider === "unavailable") clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')");
5017
- else { segment.push("f.provider = ?"); values.push(input.provider); }
5494
+ if (input.provider === "unavailable")
5495
+ clauses.push(
5496
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')",
5497
+ );
5498
+ else {
5499
+ segment.push("f.provider = ?");
5500
+ values.push(input.provider);
5501
+ }
5018
5502
  }
5019
5503
  if (input.model) {
5020
- if (input.model === "unavailable") clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')");
5021
- else { segment.push("f.model = ?"); values.push(input.model); }
5504
+ if (input.model === "unavailable")
5505
+ clauses.push(
5506
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')",
5507
+ );
5508
+ else {
5509
+ segment.push("f.model = ?");
5510
+ values.push(input.model);
5511
+ }
5022
5512
  }
5023
- if (segment.length > 0) clauses.push(`EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`);
5513
+ if (segment.length > 0)
5514
+ clauses.push(
5515
+ `EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`,
5516
+ );
5024
5517
  if (input.query) {
5025
5518
  const query = `%${input.query}%`;
5026
- clauses.push("(i.zaip_agent LIKE ? OR i.zaip_agent_persona LIKE ? OR i.zaip_agent_job LIKE ?)");
5519
+ clauses.push(
5520
+ "(i.zaip_agent LIKE ? OR i.zaip_agent_persona LIKE ? OR i.zaip_agent_job LIKE ?)",
5521
+ );
5027
5522
  values.push(query, query, query);
5028
5523
  }
5029
5524
  // This is deliberately the same financial fact population as the Cost
5030
5525
  // Monitor summary, grouped by the factual activity.zaip_agent. It is
5031
5526
  // not an invocation-only approximation.
5032
- const sourceDay = "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
5033
- const activityClauses = ["a.activity_source_kind <> 'agent_run'", "a.zaip_agent IS NOT NULL", "(a.total_tokens IS NOT NULL OR a.cost_brl IS NOT NULL OR a.cost_usd IS NOT NULL)"];
5527
+ const sourceDay =
5528
+ "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
5529
+ const activityClauses = [
5530
+ "a.activity_source_kind <> 'agent_run'",
5531
+ "a.zaip_agent IS NOT NULL",
5532
+ "(a.total_tokens IS NOT NULL OR a.cost_brl IS NOT NULL OR a.cost_usd IS NOT NULL)",
5533
+ ];
5034
5534
  const activityValues: Array<string | number> = [];
5035
- if (input.from) { activityClauses.push(`${sourceDay} >= ?`); activityValues.push(input.from.slice(0, 10)); }
5036
- if (input.to) { activityClauses.push(`${sourceDay} <= ?`); activityValues.push(input.to.slice(0, 10)); }
5037
- if (input.agent) { activityClauses.push("a.zaip_agent = ?"); activityValues.push(input.agent); }
5038
- if (input.phase) { activityClauses.push("a.phase = ?"); activityValues.push(input.phase); }
5039
- if (input.status) { activityClauses.push("a.status = ?"); activityValues.push(input.status); }
5535
+ if (input.from) {
5536
+ activityClauses.push(`${sourceDay} >= ?`);
5537
+ activityValues.push(input.from.slice(0, 10));
5538
+ }
5539
+ if (input.to) {
5540
+ activityClauses.push(`${sourceDay} <= ?`);
5541
+ activityValues.push(input.to.slice(0, 10));
5542
+ }
5543
+ if (input.agent) {
5544
+ activityClauses.push("a.zaip_agent = ?");
5545
+ activityValues.push(input.agent);
5546
+ }
5547
+ if (input.phase) {
5548
+ activityClauses.push("a.phase = ?");
5549
+ activityValues.push(input.phase);
5550
+ }
5551
+ if (input.status) {
5552
+ activityClauses.push("a.status = ?");
5553
+ activityValues.push(input.status);
5554
+ }
5040
5555
  if (input.priority || input.workType) activityClauses.push("0");
5041
- if (input.risk) { activityClauses.push("activity_task.risk = ?"); activityValues.push(input.risk); }
5042
- if (input.origin) activityClauses.push(input.origin === "prd" ? "a.prd_id IS NOT NULL" : input.origin === "spec" ? "a.prd_id IS NULL AND a.spec_id IS NOT NULL" : input.origin === "task" ? "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NOT NULL" : "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NULL");
5556
+ if (input.risk) {
5557
+ activityClauses.push("activity_task.risk = ?");
5558
+ activityValues.push(input.risk);
5559
+ }
5560
+ if (input.origin)
5561
+ activityClauses.push(
5562
+ input.origin === "prd"
5563
+ ? "a.prd_id IS NOT NULL"
5564
+ : input.origin === "spec"
5565
+ ? "a.prd_id IS NULL AND a.spec_id IS NOT NULL"
5566
+ : input.origin === "task"
5567
+ ? "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NOT NULL"
5568
+ : "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NULL",
5569
+ );
5043
5570
  if (input.provider || input.model) {
5044
5571
  const direct: string[] = [];
5045
5572
  const usage: string[] = [];
@@ -5053,30 +5580,86 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5053
5580
  usage.push("COALESCE(u.model, 'unavailable') = ?");
5054
5581
  activityValues.push(input.model);
5055
5582
  }
5056
- activityClauses.push(`((a.agent_run_id IS NULL AND ${direct.join(" AND ")}) OR (a.agent_run_id IS NOT NULL AND EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id = a.agent_run_id AND ${usage.join(" AND ")})))`);
5583
+ activityClauses.push(
5584
+ `((a.agent_run_id IS NULL AND ${direct.join(" AND ")}) OR (a.agent_run_id IS NOT NULL AND EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id = a.agent_run_id AND ${usage.join(" AND ")})))`,
5585
+ );
5057
5586
  if (input.provider) activityValues.push(input.provider);
5058
5587
  if (input.model) activityValues.push(input.model);
5059
5588
  }
5060
- if (input.query) { const query = `%${input.query}%`; activityClauses.push("(a.zaip_agent LIKE ? OR a.zaip_agent_persona LIKE ? OR a.zaip_agent_job LIKE ?)"); activityValues.push(query, query, query); }
5589
+ if (input.query) {
5590
+ const query = `%${input.query}%`;
5591
+ activityClauses.push(
5592
+ "(a.zaip_agent LIKE ? OR a.zaip_agent_persona LIKE ? OR a.zaip_agent_job LIKE ?)",
5593
+ );
5594
+ activityValues.push(query, query, query);
5595
+ }
5061
5596
  const where = clauses.join(" AND ");
5062
- const invocationClauses = ["i.zaip_agent IS NOT NULL", "(i.total_tokens IS NOT NULL OR i.cost_brl IS NOT NULL OR i.cost_usd IS NOT NULL)"];
5597
+ const invocationClauses = ["i.zaip_agent IS NOT NULL"];
5063
5598
  const invocationValues: Array<string | number> = [];
5064
- const invocationSourceDay = "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
5065
- if (input.from) { invocationClauses.push(`${invocationSourceDay} >= ?`); invocationValues.push(input.from.slice(0, 10)); }
5066
- if (input.to) { invocationClauses.push(`${invocationSourceDay} <= ?`); invocationValues.push(input.to.slice(0, 10)); }
5067
- if (input.agent) { invocationClauses.push("i.zaip_agent = ?"); invocationValues.push(input.agent); }
5068
- if (input.phase) { invocationClauses.push("i.phase = ?"); invocationValues.push(input.phase); }
5069
- if (input.status) { invocationClauses.push("i.status = ?"); invocationValues.push(input.status); }
5599
+ const invocationSourceDay =
5600
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
5601
+ if (input.from) {
5602
+ invocationClauses.push(`${invocationSourceDay} >= ?`);
5603
+ invocationValues.push(input.from.slice(0, 10));
5604
+ // Keep the source-day predicate for historical correctness, while
5605
+ // using the materialized day key to seek the immutable fact first.
5606
+ invocationClauses.push("i.occurred_day_sp >= ?");
5607
+ invocationValues.push(input.from.slice(0, 10));
5608
+ }
5609
+ if (input.to) {
5610
+ invocationClauses.push(`${invocationSourceDay} <= ?`);
5611
+ invocationValues.push(input.to.slice(0, 10));
5612
+ invocationClauses.push("i.occurred_day_sp <= ?");
5613
+ invocationValues.push(input.to.slice(0, 10));
5614
+ }
5615
+ if (input.agent) {
5616
+ invocationClauses.push("i.zaip_agent = ?");
5617
+ invocationValues.push(input.agent);
5618
+ }
5619
+ if (input.phase) {
5620
+ invocationClauses.push("i.phase = ?");
5621
+ invocationValues.push(input.phase);
5622
+ }
5623
+ if (input.status) {
5624
+ invocationClauses.push("i.status = ?");
5625
+ invocationValues.push(input.status);
5626
+ }
5070
5627
  if (input.priority || input.workType) invocationClauses.push("0");
5071
- if (input.risk) { invocationClauses.push("invocation_task.risk = ?"); invocationValues.push(input.risk); }
5072
- if (input.origin) invocationClauses.push(input.origin === "prd" ? "i.prd_id IS NOT NULL" : input.origin === "spec" ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL" : input.origin === "task" ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL" : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL");
5628
+ if (input.risk) {
5629
+ invocationClauses.push("invocation_task.risk = ?");
5630
+ invocationValues.push(input.risk);
5631
+ }
5632
+ if (input.origin)
5633
+ invocationClauses.push(
5634
+ input.origin === "prd"
5635
+ ? "i.prd_id IS NOT NULL"
5636
+ : input.origin === "spec"
5637
+ ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL"
5638
+ : input.origin === "task"
5639
+ ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL"
5640
+ : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL",
5641
+ );
5073
5642
  if (input.provider || input.model) {
5074
5643
  const segment: string[] = [];
5075
- if (input.provider) { segment.push("COALESCE(u.provider, 'unavailable') = ?"); invocationValues.push(input.provider); }
5076
- if (input.model) { segment.push("COALESCE(u.model, 'unavailable') = ?"); invocationValues.push(input.model); }
5077
- invocationClauses.push(`EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`);
5644
+ if (input.provider) {
5645
+ segment.push("COALESCE(u.provider, 'unavailable') = ?");
5646
+ invocationValues.push(input.provider);
5647
+ }
5648
+ if (input.model) {
5649
+ segment.push("COALESCE(u.model, 'unavailable') = ?");
5650
+ invocationValues.push(input.model);
5651
+ }
5652
+ invocationClauses.push(
5653
+ `EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`,
5654
+ );
5655
+ }
5656
+ if (input.query) {
5657
+ const query = `%${input.query}%`;
5658
+ invocationClauses.push(
5659
+ "(i.zaip_agent LIKE ? OR i.zaip_agent_persona LIKE ? OR i.zaip_agent_job LIKE ?)",
5660
+ );
5661
+ invocationValues.push(query, query, query);
5078
5662
  }
5079
- if (input.query) { const query = `%${input.query}%`; invocationClauses.push("(i.zaip_agent LIKE ? OR i.zaip_agent_persona LIKE ? OR i.zaip_agent_job LIKE ?)"); invocationValues.push(query, query, query); }
5080
5663
  const groupedFacts = `WITH agent_facts AS (
5081
5664
  SELECT i.zaip_agent AS key, i.zaip_agent_persona AS persona, i.zaip_agent_job AS job, d.persona AS catalog_persona, d.job AS catalog_job,
5082
5665
  i.occurred_at, i.task_id, i.duration_ms, i.source_event_count, i.total_tokens, i.cost_usd, i.cost_brl
@@ -5090,63 +5673,171 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5090
5673
  )`;
5091
5674
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5092
5675
  const offset = Math.max(0, input.offset ?? 0);
5093
- const total_count = Number((this.database.prepare(`${groupedFacts} SELECT COUNT(DISTINCT key) AS count FROM agent_facts`).get(...invocationValues, ...activityValues) as { count: number }).count);
5676
+ const total_count = Number(
5677
+ (
5678
+ this.database
5679
+ .prepare(`${groupedFacts} SELECT COUNT(DISTINCT key) AS count FROM agent_facts`)
5680
+ .get(...invocationValues, ...activityValues) as { count: number }
5681
+ ).count,
5682
+ );
5094
5683
  // Activity facts retain the catalog snapshot used when they were
5095
5684
  // materialized. The current dimension is a safe fallback for older
5096
5685
  // snapshots that predate those fields: it supplies only catalog display
5097
5686
  // metadata, never tokens, cost, duration, or attribution.
5098
- const totals = this.database.prepare(`${groupedFacts} SELECT COUNT(*) AS invocation_count, COUNT(DISTINCT task_id) AS task_count, SUM(duration_ms) AS total_duration, SUM(CASE WHEN duration_ms IS NULL THEN 1 ELSE 0 END) AS duration_unknown, SUM(CASE WHEN duration_ms IS NULL THEN 0 ELSE duration_ms END) AS duration_known, SUM(source_event_count) AS source_event_count, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd, SUM(cost_brl) AS total_cost_brl FROM agent_facts`).get(...invocationValues, ...activityValues) as Record<string, unknown>;
5099
- const rows = this.database.prepare(`${groupedFacts} SELECT key, key AS label, COALESCE(MAX(persona), MAX(catalog_persona)) AS persona, COALESCE(MAX(job), MAX(catalog_job)) AS job, MAX(occurred_at) AS latest_started_at, COUNT(*) AS invocation_count, COUNT(DISTINCT task_id) AS task_count, SUM(duration_ms) AS total_duration, SUM(CASE WHEN duration_ms IS NULL THEN 1 ELSE 0 END) AS duration_unknown, SUM(CASE WHEN duration_ms IS NULL THEN 0 ELSE duration_ms END) AS duration_known, SUM(source_event_count) AS source_event_count, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd, SUM(cost_brl) AS total_cost_brl FROM agent_facts GROUP BY key ORDER BY total_cost_brl DESC, key LIMIT ? OFFSET ?`).all(...invocationValues, ...activityValues, limit, offset) as Array<Record<string, unknown>>;
5100
- return { data_source: "sqlite_read_model" as const, view, offset, limit, total_count, has_more: offset + rows.length < total_count, totals, rows };
5687
+ const totals = this.database
5688
+ .prepare(
5689
+ `${groupedFacts} SELECT COUNT(*) AS invocation_count, COUNT(DISTINCT task_id) AS task_count, SUM(duration_ms) AS total_duration, SUM(CASE WHEN duration_ms IS NULL THEN 1 ELSE 0 END) AS duration_unknown, SUM(CASE WHEN duration_ms IS NULL THEN 0 ELSE duration_ms END) AS duration_known, SUM(source_event_count) AS source_event_count, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd, SUM(cost_brl) AS total_cost_brl FROM agent_facts`,
5690
+ )
5691
+ .get(...invocationValues, ...activityValues) as Record<string, unknown>;
5692
+ const rows = this.database
5693
+ .prepare(
5694
+ `${groupedFacts} SELECT key, key AS label, COALESCE(MAX(persona), MAX(catalog_persona)) AS persona, COALESCE(MAX(job), MAX(catalog_job)) AS job, MAX(occurred_at) AS latest_started_at, COUNT(*) AS invocation_count, COUNT(DISTINCT task_id) AS task_count, SUM(duration_ms) AS total_duration, SUM(CASE WHEN duration_ms IS NULL THEN 1 ELSE 0 END) AS duration_unknown, SUM(CASE WHEN duration_ms IS NULL THEN 0 ELSE duration_ms END) AS duration_known, SUM(source_event_count) AS source_event_count, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd, SUM(cost_brl) AS total_cost_brl FROM agent_facts GROUP BY key ORDER BY total_cost_brl DESC, key LIMIT ? OFFSET ?`,
5695
+ )
5696
+ .all(...invocationValues, ...activityValues, limit, offset) as Array<
5697
+ Record<string, unknown>
5698
+ >;
5699
+ return {
5700
+ data_source: "sqlite_read_model" as const,
5701
+ view,
5702
+ offset,
5703
+ limit,
5704
+ total_count,
5705
+ has_more: offset + rows.length < total_count,
5706
+ totals,
5707
+ rows,
5708
+ };
5101
5709
  }
5102
5710
  if (filteredCostGroup === "model") {
5103
5711
  const clauses = ["1=1"];
5104
5712
  const values: Array<string | number> = [];
5105
- if (input.from) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?"); values.push(input.from.slice(0, 10)); }
5106
- if (input.to) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?"); values.push(input.to.slice(0, 10)); }
5107
- if (input.agent) { clauses.push("i.zaip_agent = ?"); values.push(input.agent); }
5108
- if (input.phase) { clauses.push("i.phase = ?"); values.push(input.phase); }
5109
- if (input.status) { clauses.push("i.status = ?"); values.push(input.status); }
5713
+ const dimensionDateFilter = costDimensionFactDateFilter(input);
5714
+ if (input.from) {
5715
+ clauses.push(
5716
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
5717
+ );
5718
+ values.push(input.from.slice(0, 10));
5719
+ }
5720
+ if (input.to) {
5721
+ clauses.push(
5722
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?",
5723
+ );
5724
+ values.push(input.to.slice(0, 10));
5725
+ }
5726
+ if (input.agent) {
5727
+ clauses.push("i.zaip_agent = ?");
5728
+ values.push(input.agent);
5729
+ }
5730
+ if (input.phase) {
5731
+ clauses.push("i.phase = ?");
5732
+ values.push(input.phase);
5733
+ }
5734
+ if (input.status) {
5735
+ clauses.push("i.status = ?");
5736
+ values.push(input.status);
5737
+ }
5110
5738
  if (input.priority || input.workType) clauses.push("0");
5111
- if (input.risk) { clauses.push("t.risk = ?"); values.push(input.risk); }
5739
+ if (input.risk) {
5740
+ clauses.push("t.risk = ?");
5741
+ values.push(input.risk);
5742
+ }
5112
5743
  if (input.origin) {
5113
- clauses.push(input.origin === "prd" ? "i.prd_id IS NOT NULL" : input.origin === "spec" ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL" : input.origin === "task" ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL" : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL");
5744
+ clauses.push(
5745
+ input.origin === "prd"
5746
+ ? "i.prd_id IS NOT NULL"
5747
+ : input.origin === "spec"
5748
+ ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL"
5749
+ : input.origin === "task"
5750
+ ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL"
5751
+ : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL",
5752
+ );
5114
5753
  }
5115
5754
  if (input.provider) {
5116
- if (input.provider === "unavailable") clauses.push("s.provider IS NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')");
5117
- else { clauses.push("s.provider = ?"); values.push(input.provider); }
5755
+ if (input.provider === "unavailable")
5756
+ clauses.push(
5757
+ "s.provider IS NULL AND NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')",
5758
+ );
5759
+ else {
5760
+ clauses.push("s.provider = ?");
5761
+ values.push(input.provider);
5762
+ }
5118
5763
  }
5119
5764
  if (input.model) {
5120
5765
  if (input.model === "unavailable") clauses.push("s.model_key = 'unavailable'");
5121
- else { clauses.push("s.model_key = ?"); values.push(input.model); }
5766
+ else {
5767
+ clauses.push("s.model_key = ?");
5768
+ values.push(input.model);
5769
+ }
5770
+ }
5771
+ if (input.query) {
5772
+ clauses.push("s.model_key LIKE ?");
5773
+ values.push(`%${input.query}%`);
5122
5774
  }
5123
- if (input.query) { clauses.push("s.model_key LIKE ?"); values.push(`%${input.query}%`); }
5124
5775
  const where = clauses.join(" AND ");
5125
5776
  const activityClauses = ["a.activity_source_kind <> 'agent_run'"];
5126
5777
  const activityValues: Array<string | number> = [];
5127
- if (input.from) { activityClauses.push("a.occurred_day_sp >= ?"); activityValues.push(input.from.slice(0, 10)); }
5128
- if (input.to) { activityClauses.push("a.occurred_day_sp <= ?"); activityValues.push(input.to.slice(0, 10)); }
5129
- if (input.agent) { activityClauses.push("a.zaip_agent = ?"); activityValues.push(input.agent); }
5130
- if (input.phase) { activityClauses.push("a.phase = ?"); activityValues.push(input.phase); }
5131
- if (input.status) { activityClauses.push("a.status = ?"); activityValues.push(input.status); }
5778
+ if (input.from) {
5779
+ activityClauses.push("a.occurred_day_sp >= ?");
5780
+ activityValues.push(input.from.slice(0, 10));
5781
+ }
5782
+ if (input.to) {
5783
+ activityClauses.push("a.occurred_day_sp <= ?");
5784
+ activityValues.push(input.to.slice(0, 10));
5785
+ }
5786
+ if (input.agent) {
5787
+ activityClauses.push("a.zaip_agent = ?");
5788
+ activityValues.push(input.agent);
5789
+ }
5790
+ if (input.phase) {
5791
+ activityClauses.push("a.phase = ?");
5792
+ activityValues.push(input.phase);
5793
+ }
5794
+ if (input.status) {
5795
+ activityClauses.push("a.status = ?");
5796
+ activityValues.push(input.status);
5797
+ }
5132
5798
  if (input.priority || input.workType) activityClauses.push("0");
5133
- if (input.risk) { activityClauses.push("activity_task.risk = ?"); activityValues.push(input.risk); }
5134
- if (input.origin) activityClauses.push(input.origin === "prd" ? "a.prd_id IS NOT NULL" : input.origin === "spec" ? "a.prd_id IS NULL AND a.spec_id IS NOT NULL" : input.origin === "task" ? "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NOT NULL" : "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NULL");
5799
+ if (input.risk) {
5800
+ activityClauses.push("activity_task.risk = ?");
5801
+ activityValues.push(input.risk);
5802
+ }
5803
+ if (input.origin)
5804
+ activityClauses.push(
5805
+ input.origin === "prd"
5806
+ ? "a.prd_id IS NOT NULL"
5807
+ : input.origin === "spec"
5808
+ ? "a.prd_id IS NULL AND a.spec_id IS NOT NULL"
5809
+ : input.origin === "task"
5810
+ ? "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NOT NULL"
5811
+ : "a.prd_id IS NULL AND a.spec_id IS NULL AND a.task_id IS NULL",
5812
+ );
5135
5813
  if (input.provider) {
5136
- if (input.provider === "unavailable") activityClauses.push("a.provider IS NULL OR trim(a.provider) = '' OR a.provider = 'unavailable'");
5137
- else { activityClauses.push("a.provider = ?"); activityValues.push(input.provider); }
5814
+ if (input.provider === "unavailable")
5815
+ activityClauses.push(
5816
+ "a.provider IS NULL OR trim(a.provider) = '' OR a.provider = 'unavailable'",
5817
+ );
5818
+ else {
5819
+ activityClauses.push("a.provider = ?");
5820
+ activityValues.push(input.provider);
5821
+ }
5138
5822
  }
5139
5823
  if (input.model) {
5140
- if (input.model === "unavailable") activityClauses.push("a.model IS NULL OR trim(a.model) = '' OR a.model = 'unavailable'");
5141
- else { activityClauses.push("a.model = ?"); activityValues.push(input.model); }
5824
+ if (input.model === "unavailable")
5825
+ activityClauses.push("a.model IS NULL OR trim(a.model) = '' OR a.model = 'unavailable'");
5826
+ else {
5827
+ activityClauses.push("a.model = ?");
5828
+ activityValues.push(input.model);
5829
+ }
5830
+ }
5831
+ if (input.query) {
5832
+ activityClauses.push("COALESCE(a.model, 'unavailable') LIKE ?");
5833
+ activityValues.push(`%${input.query}%`);
5142
5834
  }
5143
- if (input.query) { activityClauses.push("COALESCE(a.model, 'unavailable') LIKE ?"); activityValues.push(`%${input.query}%`); }
5144
5835
  const ctes = `WITH segment_totals AS (
5145
5836
  SELECT agent_invocation_id, SUM(input_tokens) AS input_tokens, SUM(cached_input_tokens) AS cached_input_tokens,
5146
5837
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5147
5838
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5148
5839
  FROM cost_invocation_dimension_facts
5149
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
5840
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5150
5841
  GROUP BY agent_invocation_id
5151
5842
  ), segments AS (
5152
5843
  SELECT agent_invocation_id,
@@ -5156,7 +5847,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5156
5847
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5157
5848
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5158
5849
  FROM cost_invocation_dimension_facts
5159
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
5850
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5160
5851
  GROUP BY agent_invocation_id, model_key, provider
5161
5852
  UNION ALL
5162
5853
  -- Preserve every factual residual from the consolidated invocation:
@@ -5223,7 +5914,13 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5223
5914
  )`;
5224
5915
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5225
5916
  const offset = Math.max(0, input.offset ?? 0);
5226
- const total_count = Number((this.database.prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM keys`).get(...values, ...activityValues) as { count: number }).count);
5917
+ const total_count = Number(
5918
+ (
5919
+ this.database
5920
+ .prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM keys`)
5921
+ .get(...values, ...activityValues) as { count: number }
5922
+ ).count,
5923
+ );
5227
5924
  // Dimension membership is intentionally non-additive: one Task can use
5228
5925
  // several concrete models. Expose the distinct population separately so
5229
5926
  // a footer never reports the inflated sum of row memberships as total.
@@ -5244,30 +5941,90 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5244
5941
  SUM(cost_brl) AS total_cost_brl
5245
5942
  FROM filtered_segments`)
5246
5943
  .get(...values, ...activityValues) as Record<string, unknown>;
5247
- const rows = this.database.prepare(`${ctes} ${facts} SELECT keys.key, keys.key AS label, COALESCE(population.invocation_count, 0) AS invocation_count, COALESCE(task_population.task_count, 0) AS task_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, keys.key LIMIT ? OFFSET ?`).all(...values, ...activityValues, limit, offset) as Array<Record<string, unknown>>;
5248
- return { data_source: "sqlite_read_model" as const, view, offset, limit, total_count, has_more: offset + rows.length < total_count, totals, rows };
5944
+ const rows = this.database
5945
+ .prepare(
5946
+ `${ctes} ${facts} SELECT keys.key, keys.key AS label, COALESCE(population.invocation_count, 0) AS invocation_count, COALESCE(task_population.task_count, 0) AS task_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, keys.key LIMIT ? OFFSET ?`,
5947
+ )
5948
+ .all(...values, ...activityValues, limit, offset) as Array<Record<string, unknown>>;
5949
+ return {
5950
+ data_source: "sqlite_read_model" as const,
5951
+ view,
5952
+ offset,
5953
+ limit,
5954
+ total_count,
5955
+ has_more: offset + rows.length < total_count,
5956
+ totals,
5957
+ rows,
5958
+ };
5249
5959
  }
5250
5960
  if (filteredCostGroup === "provider") {
5251
5961
  const clauses = ["1=1"];
5252
5962
  const values: Array<string | number> = [];
5253
- if (input.from) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?"); values.push(input.from.slice(0, 10)); }
5254
- if (input.to) { clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?"); values.push(input.to.slice(0, 10)); }
5255
- if (input.agent) { clauses.push("i.zaip_agent = ?"); values.push(input.agent); }
5256
- if (input.phase) { clauses.push("i.phase = ?"); values.push(input.phase); }
5257
- if (input.status) { clauses.push("i.status = ?"); values.push(input.status); }
5963
+ const dimensionDateFilter = costDimensionFactDateFilter(input);
5964
+ if (input.from) {
5965
+ clauses.push(
5966
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
5967
+ );
5968
+ values.push(input.from.slice(0, 10));
5969
+ }
5970
+ if (input.to) {
5971
+ clauses.push(
5972
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?",
5973
+ );
5974
+ values.push(input.to.slice(0, 10));
5975
+ }
5976
+ if (input.agent) {
5977
+ clauses.push("i.zaip_agent = ?");
5978
+ values.push(input.agent);
5979
+ }
5980
+ if (input.phase) {
5981
+ clauses.push("i.phase = ?");
5982
+ values.push(input.phase);
5983
+ }
5984
+ if (input.status) {
5985
+ clauses.push("i.status = ?");
5986
+ values.push(input.status);
5987
+ }
5258
5988
  if (input.priority || input.workType) clauses.push("0");
5259
- if (input.risk) { clauses.push("t.risk = ?"); values.push(input.risk); }
5260
- if (input.origin) clauses.push(input.origin === "prd" ? "i.prd_id IS NOT NULL" : input.origin === "spec" ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL" : input.origin === "task" ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL" : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL");
5261
- if (input.provider) { if (input.provider === "unavailable") clauses.push("s.provider_key = 'unavailable'"); else { clauses.push("s.provider_key = ?"); values.push(input.provider); } }
5262
- if (input.model) { if (input.model === "unavailable") clauses.push("s.model_key = 'unavailable'"); else { clauses.push("s.model_key = ?"); values.push(input.model); } }
5263
- if (input.query) { clauses.push("s.provider_key LIKE ?"); values.push(`%${input.query}%`); }
5989
+ if (input.risk) {
5990
+ clauses.push("t.risk = ?");
5991
+ values.push(input.risk);
5992
+ }
5993
+ if (input.origin)
5994
+ clauses.push(
5995
+ input.origin === "prd"
5996
+ ? "i.prd_id IS NOT NULL"
5997
+ : input.origin === "spec"
5998
+ ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL"
5999
+ : input.origin === "task"
6000
+ ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL"
6001
+ : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL",
6002
+ );
6003
+ if (input.provider) {
6004
+ if (input.provider === "unavailable") clauses.push("s.provider_key = 'unavailable'");
6005
+ else {
6006
+ clauses.push("s.provider_key = ?");
6007
+ values.push(input.provider);
6008
+ }
6009
+ }
6010
+ if (input.model) {
6011
+ if (input.model === "unavailable") clauses.push("s.model_key = 'unavailable'");
6012
+ else {
6013
+ clauses.push("s.model_key = ?");
6014
+ values.push(input.model);
6015
+ }
6016
+ }
6017
+ if (input.query) {
6018
+ clauses.push("s.provider_key LIKE ?");
6019
+ values.push(`%${input.query}%`);
6020
+ }
5264
6021
  const where = clauses.join(" AND ");
5265
6022
  const ctes = `WITH segment_totals AS (
5266
6023
  SELECT agent_invocation_id, SUM(input_tokens) AS input_tokens, SUM(cached_input_tokens) AS cached_input_tokens,
5267
6024
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5268
6025
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5269
6026
  FROM cost_invocation_dimension_facts
5270
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
6027
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5271
6028
  GROUP BY agent_invocation_id
5272
6029
  ), segments AS (
5273
6030
  SELECT agent_invocation_id,
@@ -5277,7 +6034,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5277
6034
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5278
6035
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5279
6036
  FROM cost_invocation_dimension_facts
5280
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
6037
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5281
6038
  GROUP BY agent_invocation_id, provider_key, model_key
5282
6039
  UNION ALL
5283
6040
  -- See the equivalent Models branch: attribute only the segment part;
@@ -5319,8 +6076,15 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5319
6076
  SELECT provider_key AS key, COUNT(*) AS task_count
5320
6077
  FROM task_providers GROUP BY provider_key
5321
6078
  ), keys AS (SELECT key FROM financial UNION SELECT key FROM population UNION SELECT key FROM task_population)`;
5322
- const limit = Math.min(200, Math.max(1, input.limit ?? 50)); const offset = Math.max(0, input.offset ?? 0);
5323
- const total_count = Number((this.database.prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM keys`).get(...values) as { count: number }).count);
6079
+ const limit = Math.min(200, Math.max(1, input.limit ?? 50));
6080
+ const offset = Math.max(0, input.offset ?? 0);
6081
+ const total_count = Number(
6082
+ (
6083
+ this.database
6084
+ .prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM keys`)
6085
+ .get(...values) as { count: number }
6086
+ ).count,
6087
+ );
5324
6088
  const totals = this.database
5325
6089
  .prepare(`${ctes} ${facts} SELECT
5326
6090
  (SELECT COUNT(*) FROM population_ranked WHERE provider_rank = 1) AS invocation_count,
@@ -5338,21 +6102,38 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5338
6102
  SUM(cost_brl) AS total_cost_brl
5339
6103
  FROM filtered_segments`)
5340
6104
  .get(...values) as Record<string, unknown>;
5341
- const rows = this.database.prepare(`${ctes} ${facts} SELECT keys.key, keys.key AS label, COALESCE(population.invocation_count,0) AS invocation_count, COALESCE(task_population.task_count,0) AS task_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, keys.key LIMIT ? OFFSET ?`).all(...values, limit, offset) as Array<Record<string, unknown>>;
5342
- return { data_source: "sqlite_read_model" as const, view, offset, limit, total_count, has_more: offset + rows.length < total_count, totals, rows };
6105
+ const rows = this.database
6106
+ .prepare(
6107
+ `${ctes} ${facts} SELECT keys.key, keys.key AS label, COALESCE(population.invocation_count,0) AS invocation_count, COALESCE(task_population.task_count,0) AS task_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, keys.key LIMIT ? OFFSET ?`,
6108
+ )
6109
+ .all(...values, limit, offset) as Array<Record<string, unknown>>;
6110
+ return {
6111
+ data_source: "sqlite_read_model" as const,
6112
+ view,
6113
+ offset,
6114
+ limit,
6115
+ total_count,
6116
+ has_more: offset + rows.length < total_count,
6117
+ totals,
6118
+ rows,
6119
+ };
5343
6120
  }
5344
6121
  const temporalCostGroup =
5345
6122
  filteredCostGroup === "day" || filteredCostGroup === "month" || filteredCostGroup === "year";
5346
6123
  const hasDimensionalCostFilter = Boolean(
5347
- input.provider || input.model || input.agent || input.phase || input.status || input.origin || input.priority || input.risk || input.workType,
6124
+ input.provider ||
6125
+ input.model ||
6126
+ input.agent ||
6127
+ input.phase ||
6128
+ input.status ||
6129
+ input.origin ||
6130
+ input.priority ||
6131
+ input.risk ||
6132
+ input.workType,
5348
6133
  );
5349
6134
  if (filteredCostGroup !== undefined && temporalCostGroup && !hasDimensionalCostFilter) {
5350
6135
  const from = costMonitorFilterDay(input.from);
5351
6136
  const to = costMonitorFilterDay(input.to);
5352
- // Cost temporal tables reuse the same aggregate-only projection as the
5353
- // summary cards. This intentionally bypasses daily_aggregate_cache,
5354
- // whose recursive all-dashboard view used to expand the complete fact
5355
- // graph before returning a handful of visible columns.
5356
6137
  const aggregate = this.costMonitorSummaryAggregate({ from, to });
5357
6138
  const financialRows = aggregate.daily_rows as Array<Record<string, unknown>>;
5358
6139
  const financialByDay = new Map(financialRows.map((row) => [String(row.key), row]));
@@ -5400,12 +6181,14 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5400
6181
  for (const column of additive) {
5401
6182
  const value = financial?.[column];
5402
6183
  if (typeof value === "number")
5403
- target[column] = (typeof target[column] === "number" ? Number(target[column]) : 0) + value;
6184
+ target[column] =
6185
+ (typeof target[column] === "number" ? Number(target[column]) : 0) + value;
5404
6186
  }
5405
6187
  const duration = financial?.total_duration;
5406
6188
  if (typeof duration === "number") {
5407
6189
  target.total_duration =
5408
- (typeof target.total_duration === "number" ? Number(target.total_duration) : 0) + duration;
6190
+ (typeof target.total_duration === "number" ? Number(target.total_duration) : 0) +
6191
+ duration;
5409
6192
  }
5410
6193
  if (typeof financial?.duration_unknown === "number" && financial.duration_unknown > 0)
5411
6194
  target.duration_incomplete = true;
@@ -5440,7 +6223,20 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5440
6223
  .filter((row) => query === null || String(row.key).toLocaleLowerCase().includes(query))
5441
6224
  .sort((left, right) => String(right.key).localeCompare(String(left.key)));
5442
6225
  const totals = allRows.reduce<Record<string, number | null>>((result, row) => {
5443
- for (const key of ["prd_count", "spec_count", "task_count", "total_run_count", "validation_count", "evidence_count", "total_duration", "duration_known", "duration_unknown", "total_tokens", "total_cost_usd", "total_cost_brl"] as const) {
6226
+ for (const key of [
6227
+ "prd_count",
6228
+ "spec_count",
6229
+ "task_count",
6230
+ "total_run_count",
6231
+ "validation_count",
6232
+ "evidence_count",
6233
+ "total_duration",
6234
+ "duration_known",
6235
+ "duration_unknown",
6236
+ "total_tokens",
6237
+ "total_cost_usd",
6238
+ "total_cost_brl",
6239
+ ] as const) {
5444
6240
  const value = row[key];
5445
6241
  if (typeof value === "number") result[key] = (result[key] ?? 0) + value;
5446
6242
  else if (!(key in result)) result[key] = null;
@@ -5465,7 +6261,15 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5465
6261
  if (temporalCostGroup && input.workType) {
5466
6262
  // No canonical work_type exists at invocation grain, including for
5467
6263
  // duration/Run completion backfills. Do not emit unrelated buckets.
5468
- return { data_source: "sqlite_read_model" as const, view, offset: Math.max(0, input.offset ?? 0), limit: Math.min(200, Math.max(1, input.limit ?? 50)), total_count: 0, has_more: false, rows: [] };
6264
+ return {
6265
+ data_source: "sqlite_read_model" as const,
6266
+ view,
6267
+ offset: Math.max(0, input.offset ?? 0),
6268
+ limit: Math.min(200, Math.max(1, input.limit ?? 50)),
6269
+ total_count: 0,
6270
+ has_more: false,
6271
+ rows: [],
6272
+ };
5469
6273
  }
5470
6274
  const groupExpression =
5471
6275
  filteredCostGroup === "day"
@@ -5481,17 +6285,23 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5481
6285
  ];
5482
6286
  const values: Array<string | number> = [];
5483
6287
  if (input.from) {
5484
- clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?");
6288
+ clauses.push(
6289
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
6290
+ );
5485
6291
  values.push(costMonitorFilterDay(input.from)!);
5486
6292
  }
5487
6293
  if (input.to) {
5488
- clauses.push("date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?");
6294
+ clauses.push(
6295
+ "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') <= ?",
6296
+ );
5489
6297
  values.push(costMonitorFilterDay(input.to)!);
5490
6298
  }
5491
6299
  const usageSegmentClauses: string[] = [];
5492
6300
  if (input.provider) {
5493
6301
  if (input.provider === "unavailable") {
5494
- clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')");
6302
+ clauses.push(
6303
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')",
6304
+ );
5495
6305
  } else {
5496
6306
  usageSegmentClauses.push("f.provider=?");
5497
6307
  values.push(input.provider);
@@ -5499,14 +6309,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5499
6309
  }
5500
6310
  if (input.model) {
5501
6311
  if (input.model === "unavailable") {
5502
- clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')");
6312
+ clauses.push(
6313
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')",
6314
+ );
5503
6315
  } else {
5504
6316
  usageSegmentClauses.push("f.model=?");
5505
6317
  values.push(input.model);
5506
6318
  }
5507
6319
  }
5508
6320
  if (usageSegmentClauses.length > 0)
5509
- clauses.push(`EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${usageSegmentClauses.join(" AND ")})`);
6321
+ clauses.push(
6322
+ `EXISTS (SELECT 1 FROM invocation_usage f WHERE f.agent_invocation_id=i.agent_invocation_id AND ${usageSegmentClauses.join(" AND ")})`,
6323
+ );
5510
6324
  if (input.agent) {
5511
6325
  clauses.push("i.zaip_agent = ?");
5512
6326
  values.push(input.agent);
@@ -5602,19 +6416,43 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5602
6416
  const factualDuration = this.costMonitorFactualDurationByDay(canonicalFilter);
5603
6417
  // Lifecycle counts are canonical facts keyed by their own occurrence
5604
6418
  // date. They deliberately do not inherit financial dimensions.
5605
- const hasUnsupportedLifecycleAttribution = Boolean(input.origin || input.priority || input.risk || input.workType);
6419
+ const hasUnsupportedLifecycleAttribution = Boolean(
6420
+ input.origin || input.priority || input.risk || input.workType,
6421
+ );
5606
6422
  const lifecycleRows = hasUnsupportedLifecycleAttribution
5607
6423
  ? []
5608
- : this.database
6424
+ : (this.database
5609
6425
  .prepare(`SELECT day, prd_count, spec_count, task_count, validation_count, evidence_count
5610
6426
  FROM daily_aggregate_cache WHERE (? IS NULL OR day >= ?) AND (? IS NULL OR day <= ?)`)
5611
- .all(costMonitorFilterDay(input.from), costMonitorFilterDay(input.from), costMonitorFilterDay(input.to), costMonitorFilterDay(input.to)) as Array<Record<string, unknown>>;
6427
+ .all(
6428
+ costMonitorFilterDay(input.from),
6429
+ costMonitorFilterDay(input.from),
6430
+ costMonitorFilterDay(input.to),
6431
+ costMonitorFilterDay(input.to),
6432
+ ) as Array<Record<string, unknown>>);
5612
6433
  const lifecycleByKey = new Map<string, Record<string, number>>();
5613
6434
  for (const row of lifecycleRows) {
5614
6435
  const day = String(row.day);
5615
- const key = filteredCostGroup === "day" ? day : filteredCostGroup === "month" ? day.slice(0, 7) : day.slice(0, 4);
5616
- const target = lifecycleByKey.get(key) ?? { prd_count: 0, spec_count: 0, task_count: 0, validation_count: 0, evidence_count: 0 };
5617
- for (const column of ["prd_count", "spec_count", "task_count", "validation_count", "evidence_count"] as const)
6436
+ const key =
6437
+ filteredCostGroup === "day"
6438
+ ? day
6439
+ : filteredCostGroup === "month"
6440
+ ? day.slice(0, 7)
6441
+ : day.slice(0, 4);
6442
+ const target = lifecycleByKey.get(key) ?? {
6443
+ prd_count: 0,
6444
+ spec_count: 0,
6445
+ task_count: 0,
6446
+ validation_count: 0,
6447
+ evidence_count: 0,
6448
+ };
6449
+ for (const column of [
6450
+ "prd_count",
6451
+ "spec_count",
6452
+ "task_count",
6453
+ "validation_count",
6454
+ "evidence_count",
6455
+ ] as const)
5618
6456
  target[column] = (target[column] ?? 0) + Number(row[column] ?? 0);
5619
6457
  lifecycleByKey.set(key, target);
5620
6458
  }
@@ -5622,7 +6460,12 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5622
6460
  const canonicalKeys = new Set(
5623
6461
  (input.query
5624
6462
  ? []
5625
- : [...new Set([...factualDuration.keys(), ...lifecycleRows.map((row) => String(row.day))])]
6463
+ : [
6464
+ ...new Set([
6465
+ ...factualDuration.keys(),
6466
+ ...lifecycleRows.map((row) => String(row.day)),
6467
+ ]),
6468
+ ]
5626
6469
  ).map((day) =>
5627
6470
  filteredCostGroup === "day"
5628
6471
  ? day
@@ -5653,12 +6496,26 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5653
6496
  const durationMatches = [...factualDuration].filter(([day]) => dayMatches(day));
5654
6497
  return {
5655
6498
  ...financial,
5656
- ...(lifecycleByKey.get(key) ?? (hasUnsupportedLifecycleAttribution
5657
- ? { prd_count: null, spec_count: null, task_count: null, validation_count: null, evidence_count: null }
5658
- : { prd_count: 0, spec_count: 0, task_count: 0, validation_count: 0, evidence_count: 0 })),
6499
+ ...(lifecycleByKey.get(key) ??
6500
+ (hasUnsupportedLifecycleAttribution
6501
+ ? {
6502
+ prd_count: null,
6503
+ spec_count: null,
6504
+ task_count: null,
6505
+ validation_count: null,
6506
+ evidence_count: null,
6507
+ }
6508
+ : {
6509
+ prd_count: 0,
6510
+ spec_count: 0,
6511
+ task_count: 0,
6512
+ validation_count: 0,
6513
+ evidence_count: 0,
6514
+ })),
5659
6515
  total_run_count: Number(financial.invocation_count ?? 0),
5660
6516
  total_duration:
5661
- durationMatches.length === 0 || durationMatches.some(([, duration]) => duration.eligible_runs !== duration.known_runs)
6517
+ durationMatches.length === 0 ||
6518
+ durationMatches.some(([, duration]) => duration.eligible_runs !== duration.known_runs)
5662
6519
  ? null
5663
6520
  : durationMatches.reduce((sum, [, duration]) => sum + duration.known_sum, 0),
5664
6521
  };
@@ -5701,8 +6558,13 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5701
6558
  // or belongs to another period. Financial measures stay nullable.
5702
6559
  const isCanonicalMacro = groupKind === "spec" || groupKind === "prd";
5703
6560
  const hasInvocationOnlyFilter = Boolean(
5704
- input.provider || input.model || input.agent || input.phase ||
5705
- input.origin || input.workType || input.priority,
6561
+ input.provider ||
6562
+ input.model ||
6563
+ input.agent ||
6564
+ input.phase ||
6565
+ input.origin ||
6566
+ input.workType ||
6567
+ input.priority,
5706
6568
  );
5707
6569
  if (isCanonicalMacro && !hasInvocationOnlyFilter) {
5708
6570
  const entityTable = groupKind === "spec" ? "specs" : "prds";
@@ -5736,7 +6598,8 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5736
6598
  entityValues.push(`%${input.query}%`, `%${input.query}%`);
5737
6599
  }
5738
6600
  const entityWhere = entityClauses.length === 0 ? "1" : entityClauses.join(" AND ");
5739
- const invocationJoin = invocationClauses.length === 0 ? "" : ` AND ${invocationClauses.join(" AND ")}`;
6601
+ const invocationJoin =
6602
+ invocationClauses.length === 0 ? "" : ` AND ${invocationClauses.join(" AND ")}`;
5740
6603
  const ctes = `WITH grouped AS (
5741
6604
  SELECT entity.id AS group_value, entity.title, entity.status, entity.risk,
5742
6605
  COALESCE(MIN(i.occurred_day_sp), entity.occurred_day_sp) AS first_day,
@@ -5758,42 +6621,135 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5758
6621
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5759
6622
  const offset = Math.max(0, input.offset ?? 0);
5760
6623
  const values = [...invocationValues, ...entityValues];
5761
- const total_count = Number((this.database.prepare(`${ctes} SELECT COUNT(*) AS count FROM grouped`).get(...values) as { count: number }).count);
5762
- const totals = this.database.prepare(`${ctes} SELECT COUNT(*) AS entity_count, COALESCE(SUM(invocation_count), 0) AS invocation_count, ROUND(SUM(total_duration)) AS total_duration, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl FROM grouped`).get(...values) as Record<string, unknown>;
5763
- const rows = this.database.prepare(`${ctes} SELECT group_value AS id, group_value AS key, title, status, risk, first_day, last_day, occurred_at, invocation_count, llm_call_count, total_duration, total_input_tokens, total_cached_input_tokens, total_output_tokens, total_reasoning_tokens, total_tokens, total_cost_usd, total_cost_brl FROM grouped ORDER BY total_cost_brl DESC, group_value LIMIT ? OFFSET ?`).all(...values, limit, offset) as Array<Record<string, unknown>>;
5764
- return { data_source: "sqlite_read_model" as const, view, offset, limit, total_count, has_more: offset + rows.length < total_count, totals, rows };
6624
+ const total_count = Number(
6625
+ (
6626
+ this.database
6627
+ .prepare(`${ctes} SELECT COUNT(*) AS count FROM grouped`)
6628
+ .get(...values) as { count: number }
6629
+ ).count,
6630
+ );
6631
+ const totals = this.database
6632
+ .prepare(
6633
+ `${ctes} SELECT COUNT(*) AS entity_count, COALESCE(SUM(invocation_count), 0) AS invocation_count, ROUND(SUM(total_duration)) AS total_duration, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl FROM grouped`,
6634
+ )
6635
+ .get(...values) as Record<string, unknown>;
6636
+ const rows = this.database
6637
+ .prepare(
6638
+ `${ctes} SELECT group_value AS id, group_value AS key, title, status, risk, first_day, last_day, occurred_at, invocation_count, llm_call_count, total_duration, total_input_tokens, total_cached_input_tokens, total_output_tokens, total_reasoning_tokens, total_tokens, total_cost_usd, total_cost_brl FROM grouped ORDER BY total_cost_brl DESC, group_value LIMIT ? OFFSET ?`,
6639
+ )
6640
+ .all(...values, limit, offset) as Array<Record<string, unknown>>;
6641
+ return {
6642
+ data_source: "sqlite_read_model" as const,
6643
+ view,
6644
+ offset,
6645
+ limit,
6646
+ total_count,
6647
+ has_more: offset + rows.length < total_count,
6648
+ totals,
6649
+ rows,
6650
+ };
5765
6651
  }
5766
6652
  const hasDimensionalFilter = Boolean(
5767
- input.provider || input.model || input.agent || input.phase || input.status ||
5768
- input.origin || input.workType || input.priority || input.risk,
6653
+ input.provider ||
6654
+ input.model ||
6655
+ input.agent ||
6656
+ input.phase ||
6657
+ input.status ||
6658
+ input.origin ||
6659
+ input.workType ||
6660
+ input.priority ||
6661
+ input.risk,
5769
6662
  );
5770
6663
  if (hasDimensionalFilter) {
5771
6664
  const column = {
5772
- task: "i.task_id", spec: "i.spec_id", prd: "i.prd_id",
5773
- execution: "i.execution_id", conversation: "i.conversation_id",
6665
+ task: "i.task_id",
6666
+ spec: "i.spec_id",
6667
+ prd: "i.prd_id",
6668
+ execution: "i.execution_id",
6669
+ conversation: "i.conversation_id",
5774
6670
  }[groupKind];
5775
- const entity = groupKind === "task" ? "tasks" : groupKind === "spec" ? "specs" : groupKind === "prd" ? "prds" : null;
6671
+ const entity =
6672
+ groupKind === "task"
6673
+ ? "tasks"
6674
+ : groupKind === "spec"
6675
+ ? "specs"
6676
+ : groupKind === "prd"
6677
+ ? "prds"
6678
+ : null;
5776
6679
  const clauses = [`${column} IS NOT NULL`];
5777
6680
  const values: Array<string | number> = [];
5778
- if (input.from) { clauses.push("i.occurred_day_sp >= ?"); values.push(input.from.slice(0, 10)); }
5779
- if (input.to) { clauses.push("i.occurred_day_sp <= ?"); values.push(input.to.slice(0, 10)); }
5780
- if (input.agent) { clauses.push("i.zaip_agent = ?"); values.push(input.agent); }
5781
- if (input.phase) { clauses.push("i.phase = ?"); values.push(input.phase); }
5782
- if (input.status) { clauses.push("i.status = ?"); values.push(input.status); }
5783
- if (input.risk) { clauses.push("task_entity.risk = ?"); values.push(input.risk); }
5784
- if (input.origin) clauses.push(input.origin === "prd" ? "i.prd_id IS NOT NULL" : input.origin === "spec" ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL" : input.origin === "task" ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL" : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL");
6681
+ if (input.from) {
6682
+ clauses.push("i.occurred_day_sp >= ?");
6683
+ values.push(input.from.slice(0, 10));
6684
+ }
6685
+ if (input.to) {
6686
+ clauses.push("i.occurred_day_sp <= ?");
6687
+ values.push(input.to.slice(0, 10));
6688
+ }
6689
+ if (input.agent) {
6690
+ clauses.push("i.zaip_agent = ?");
6691
+ values.push(input.agent);
6692
+ }
6693
+ if (input.phase) {
6694
+ clauses.push("i.phase = ?");
6695
+ values.push(input.phase);
6696
+ }
6697
+ if (input.status) {
6698
+ clauses.push("i.status = ?");
6699
+ values.push(input.status);
6700
+ }
6701
+ if (input.risk) {
6702
+ clauses.push("task_entity.risk = ?");
6703
+ values.push(input.risk);
6704
+ }
6705
+ if (input.origin)
6706
+ clauses.push(
6707
+ input.origin === "prd"
6708
+ ? "i.prd_id IS NOT NULL"
6709
+ : input.origin === "spec"
6710
+ ? "i.prd_id IS NULL AND i.spec_id IS NOT NULL"
6711
+ : input.origin === "task"
6712
+ ? "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NOT NULL"
6713
+ : "i.prd_id IS NULL AND i.spec_id IS NULL AND i.task_id IS NULL",
6714
+ );
5785
6715
  // Compact lifecycle rows do not materialize canonical work type or Task priority.
5786
6716
  // Return a safe empty result instead of substituting another attribution.
5787
6717
  if (input.workType || input.priority) clauses.push("0");
5788
6718
  const segment: string[] = [];
5789
- if (input.provider) { if (input.provider === "unavailable") clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')"); else { segment.push("u.provider = ?"); values.push(input.provider); } }
5790
- if (input.model) { if (input.model === "unavailable") clauses.push("NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')"); else { segment.push("u.model = ?"); values.push(input.model); } }
5791
- if (segment.length > 0) clauses.push(`EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`);
6719
+ if (input.provider) {
6720
+ if (input.provider === "unavailable")
6721
+ clauses.push(
6722
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_provider WHERE known_provider.agent_invocation_id=i.agent_invocation_id AND known_provider.provider IS NOT NULL AND known_provider.provider <> 'unavailable')",
6723
+ );
6724
+ else {
6725
+ segment.push("u.provider = ?");
6726
+ values.push(input.provider);
6727
+ }
6728
+ }
6729
+ if (input.model) {
6730
+ if (input.model === "unavailable")
6731
+ clauses.push(
6732
+ "NOT EXISTS (SELECT 1 FROM invocation_usage known_model WHERE known_model.agent_invocation_id=i.agent_invocation_id AND known_model.model IS NOT NULL AND known_model.model <> 'unavailable')",
6733
+ );
6734
+ else {
6735
+ segment.push("u.model = ?");
6736
+ values.push(input.model);
6737
+ }
6738
+ }
6739
+ if (segment.length > 0)
6740
+ clauses.push(
6741
+ `EXISTS (SELECT 1 FROM invocation_usage u WHERE u.agent_invocation_id=i.agent_invocation_id AND ${segment.join(" AND ")})`,
6742
+ );
5792
6743
  if (input.query) {
5793
- clauses.push(entity === null ? `${column} LIKE ?` : `(${column} LIKE ? OR COALESCE(entity.title, '') LIKE ?)`);
6744
+ clauses.push(
6745
+ entity === null
6746
+ ? `${column} LIKE ?`
6747
+ : `(${column} LIKE ? OR COALESCE(entity.title, '') LIKE ?)`,
6748
+ );
5794
6749
  values.push(`%${input.query}%`, ...(entity === null ? [] : [`%${input.query}%`]));
5795
6750
  }
5796
- const entityJoin = entity === null ? "" : `LEFT JOIN ${entity} entity ON entity.id=${column}`;
6751
+ const entityJoin =
6752
+ entity === null ? "" : `LEFT JOIN ${entity} entity ON entity.id=${column}`;
5797
6753
  const taskJoin = "LEFT JOIN tasks task_entity ON task_entity.id=i.task_id";
5798
6754
  const join = `${entityJoin} ${taskJoin}`;
5799
6755
  const where = clauses.join(" AND ");
@@ -5802,115 +6758,35 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5802
6758
  const ctes = `WITH grouped AS (SELECT ${column} AS group_value, ${title} AS title, ${status} AS status, MAX(task_entity.risk) AS risk, MAX(i.occurred_at) AS occurred_at, COUNT(*) AS invocation_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM invocations i ${join} WHERE ${where} GROUP BY ${column})`;
5803
6759
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5804
6760
  const offset = Math.max(0, input.offset ?? 0);
5805
- const total_count = Number((this.database.prepare(`${ctes} SELECT COUNT(*) AS count FROM grouped`).get(...values) as { count: number }).count);
5806
- const totals = this.database.prepare(`${ctes} SELECT COUNT(*) AS entity_count, COALESCE(SUM(invocation_count), 0) AS invocation_count, ROUND(SUM(total_duration)) AS total_duration, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl FROM grouped`).get(...values) as Record<string, unknown>;
5807
- const rows = this.database.prepare(`${ctes} SELECT group_value AS id, group_value AS key, title, status, risk, occurred_at, invocation_count, total_duration, total_tokens, total_cost_usd, total_cost_brl FROM grouped ORDER BY total_cost_brl DESC, group_value LIMIT ? OFFSET ?`).all(...values, limit, offset) as Array<Record<string, unknown>>;
5808
- return { data_source: "sqlite_read_model" as const, view, offset, limit, total_count, has_more: offset + rows.length < total_count, totals, rows };
5809
- }
5810
- const entityMetadata = {
5811
- task: { table: "tasks", title: "entity.title", status: "entity.status", risk: "entity.risk" },
5812
- spec: { table: "specs", title: "entity.title", status: "entity.status", risk: "entity.risk" },
5813
- prd: { table: "prds", title: "entity.title", status: "entity.status", risk: "entity.risk" },
5814
- execution: { table: "executions", title: "grouped.group_value", status: "entity.status", risk: "NULL" },
5815
- conversation: { table: "conversations", title: "grouped.group_value", status: "entity.status", risk: "NULL" },
5816
- } as const;
5817
- const metadata = entityMetadata[groupKind as keyof typeof entityMetadata];
5818
- const clauses = ["group_kind = ?", "group_value IS NOT NULL"];
5819
- const values: Array<string | number> = [groupKind];
5820
- if (input.from) {
5821
- clauses.push("day >= ?");
5822
- values.push(input.from.slice(0, 10));
5823
- }
5824
- if (input.to) {
5825
- clauses.push("day <= ?");
5826
- values.push(input.to.slice(0, 10));
5827
- }
5828
- if (input.query) {
5829
- clauses.push("group_value LIKE ?");
5830
- values.push(`%${input.query}%`);
5831
- }
5832
- const groupFilter =
5833
- groupKind === "provider"
5834
- ? input.provider
5835
- : groupKind === "model"
5836
- ? input.model
5837
- : groupKind === "zaip_agent"
5838
- ? input.agent
5839
- : undefined;
5840
- if (groupFilter) {
5841
- clauses.push("group_value = ?");
5842
- values.push(groupFilter);
6761
+ const total_count = Number(
6762
+ (
6763
+ this.database
6764
+ .prepare(`${ctes} SELECT COUNT(*) AS count FROM grouped`)
6765
+ .get(...values) as { count: number }
6766
+ ).count,
6767
+ );
6768
+ const totals = this.database
6769
+ .prepare(
6770
+ `${ctes} SELECT COUNT(*) AS entity_count, COALESCE(SUM(invocation_count), 0) AS invocation_count, ROUND(SUM(total_duration)) AS total_duration, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl FROM grouped`,
6771
+ )
6772
+ .get(...values) as Record<string, unknown>;
6773
+ const rows = this.database
6774
+ .prepare(
6775
+ `${ctes} SELECT group_value AS id, group_value AS key, title, status, risk, occurred_at, invocation_count, total_duration, total_tokens, total_cost_usd, total_cost_brl FROM grouped ORDER BY total_cost_brl DESC, group_value LIMIT ? OFFSET ?`,
6776
+ )
6777
+ .all(...values, limit, offset) as Array<Record<string, unknown>>;
6778
+ return {
6779
+ data_source: "sqlite_read_model" as const,
6780
+ view,
6781
+ offset,
6782
+ limit,
6783
+ total_count,
6784
+ has_more: offset + rows.length < total_count,
6785
+ totals,
6786
+ rows,
6787
+ };
5843
6788
  }
5844
- const where = clauses.join(" AND ");
5845
- const total_count = Number(
5846
- (
5847
- this.database
5848
- .prepare(
5849
- `SELECT COUNT(DISTINCT group_value) AS count FROM cost_monitor_group_daily_cache WHERE ${where}`,
5850
- )
5851
- .get(...values) as { count: number }
5852
- ).count,
5853
- );
5854
- // This aggregate deliberately precedes LIMIT/OFFSET: a compact page is
5855
- // only a window into the selected entity population, never its subtotal.
5856
- // SUM preserves a factual 0 and leaves a wholly unavailable metric NULL.
5857
- const totals = this.database
5858
- .prepare(
5859
- `SELECT COUNT(DISTINCT group_value) AS entity_count,
5860
- COALESCE(SUM(invocation_count), 0) AS invocation_count,
5861
- ROUND(SUM(total_duration)) AS total_duration,
5862
- SUM(total_tokens) AS total_tokens,
5863
- SUM(total_cost_usd) AS total_cost_usd,
5864
- SUM(total_cost_brl) AS total_cost_brl
5865
- FROM cost_monitor_group_daily_cache
5866
- WHERE ${where}`,
5867
- )
5868
- .get(...values) as Record<string, unknown>;
5869
- const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5870
- const offset = Math.max(0, input.offset ?? 0);
5871
- const rows = this.database
5872
- .prepare(
5873
- `WITH grouped AS (
5874
- SELECT group_value, MIN(day) AS first_day, MAX(day) AS last_day,
5875
- SUM(invocation_count) AS invocation_count,
5876
- SUM(llm_call_count) AS llm_call_count,
5877
- SUM(total_duration) AS total_duration,
5878
- SUM(total_input_tokens) AS total_input_tokens,
5879
- SUM(total_cached_input_tokens) AS total_cached_input_tokens,
5880
- SUM(total_output_tokens) AS total_output_tokens,
5881
- SUM(total_reasoning_tokens) AS total_reasoning_tokens,
5882
- SUM(total_tokens) AS total_tokens,
5883
- SUM(total_cost_usd) AS total_cost_usd,
5884
- SUM(total_cost_brl) AS total_cost_brl
5885
- FROM cost_monitor_group_daily_cache
5886
- WHERE ${where}
5887
- GROUP BY group_value
5888
- )
5889
- SELECT grouped.group_value AS id, grouped.group_value AS key,
5890
- COALESCE(${metadata.title}, grouped.group_value) AS title,
5891
- ${metadata.status} AS status, ${metadata.risk} AS risk,
5892
- grouped.first_day, grouped.last_day,
5893
- grouped.last_day || 'T12:00:00.000Z' AS occurred_at,
5894
- grouped.invocation_count, grouped.llm_call_count, grouped.total_duration,
5895
- grouped.total_input_tokens, grouped.total_cached_input_tokens,
5896
- grouped.total_output_tokens, grouped.total_reasoning_tokens,
5897
- grouped.total_tokens, grouped.total_cost_usd, grouped.total_cost_brl
5898
- FROM grouped
5899
- LEFT JOIN ${metadata.table} entity ON entity.id = grouped.group_value
5900
- ORDER BY grouped.total_cost_brl DESC, grouped.group_value
5901
- LIMIT ? OFFSET ?`,
5902
- )
5903
- .all(...values, limit, offset) as Array<Record<string, unknown>>;
5904
- return {
5905
- data_source: "sqlite_read_model" as const,
5906
- view,
5907
- offset,
5908
- limit,
5909
- total_count,
5910
- has_more: offset + rows.length < total_count,
5911
- totals,
5912
- rows,
5913
- };
6789
+ return readCostGroupPage(this.database, view, groupKind as CostGroupKind, input);
5914
6790
  }
5915
6791
  const meta = sqliteViewMetadata[view];
5916
6792
  const clauses: string[] = [];
@@ -5918,8 +6794,10 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5918
6794
  // Period rows use a coarse key (YYYY-MM/YYYY), so a partial daily range
5919
6795
  // must intersect start/end days instead of comparing that key directly.
5920
6796
  const overlaps =
5921
- view === "cost_monitor_monthly" || view === "cost_monitor_annual" ||
5922
- view === "operations_monthly" || view === "operations_annual";
6797
+ view === "cost_monitor_monthly" ||
6798
+ view === "cost_monitor_annual" ||
6799
+ view === "operations_monthly" ||
6800
+ view === "operations_annual";
5923
6801
  if (overlaps && input.from) {
5924
6802
  clauses.push("end_day >= ?");
5925
6803
  values.push(input.from.slice(0, 10));
@@ -6068,8 +6946,13 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
6068
6946
  const partialFromDay = input.from?.slice(0, 10) ?? null;
6069
6947
  const partialToDay = input.to?.slice(0, 10) ?? null;
6070
6948
  const dailyRows = partialCoarse
6071
- ? this.database.prepare(`SELECT * FROM operations_daily WHERE (? IS NULL OR day >= ?) AND (? IS NULL OR day <= ?)`)
6072
- .all(partialFromDay, partialFromDay, partialToDay, partialToDay) as Array<Record<string, unknown>>
6949
+ ? (this.database
6950
+ .prepare(
6951
+ `SELECT * FROM operations_daily WHERE (? IS NULL OR day >= ?) AND (? IS NULL OR day <= ?)`,
6952
+ )
6953
+ .all(partialFromDay, partialFromDay, partialToDay, partialToDay) as Array<
6954
+ Record<string, unknown>
6955
+ >)
6073
6956
  : [];
6074
6957
  const daily = this.executionDurationByDay({ from: input.from ?? null, to: input.to ?? null });
6075
6958
  for (const row of rows) {
@@ -6079,8 +6962,23 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
6079
6962
  const matchingDays = matching.map((day) => String(day.day)).sort();
6080
6963
  row.start_day = matchingDays[0] ?? null;
6081
6964
  row.end_day = matchingDays.at(-1) ?? null;
6082
- for (const column of ["prd_count", "spec_count", "task_count", "validation_count", "evidence_count", "total_run_count", "invocation_count", "llm_call_count", "total_tokens", "total_cost_usd", "total_cost_brl"] as const)
6083
- row[column] = matching.reduce((sum, day) => sum + (typeof day[column] === "number" ? Number(day[column]) : 0), 0);
6965
+ for (const column of [
6966
+ "prd_count",
6967
+ "spec_count",
6968
+ "task_count",
6969
+ "validation_count",
6970
+ "evidence_count",
6971
+ "total_run_count",
6972
+ "invocation_count",
6973
+ "llm_call_count",
6974
+ "total_tokens",
6975
+ "total_cost_usd",
6976
+ "total_cost_brl",
6977
+ ] as const)
6978
+ row[column] = matching.reduce(
6979
+ (sum, day) => sum + (typeof day[column] === "number" ? Number(day[column]) : 0),
6980
+ 0,
6981
+ );
6084
6982
  }
6085
6983
  row.operations_total_duration = [...daily]
6086
6984
  .filter(([day]) => (view === "operations_daily" ? day === key : day.startsWith(key)))
@@ -6112,10 +7010,12 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
6112
7010
  } {
6113
7011
  if (this.metadataValues === null) {
6114
7012
  this.metadataValues = new Map(
6115
- (this.database.prepare("SELECT key, value FROM read_model_metadata").all() as Array<{
6116
- key: string;
6117
- value: string;
6118
- }>).map((row) => [row.key, row.value]),
7013
+ (
7014
+ this.database.prepare("SELECT key, value FROM read_model_metadata").all() as Array<{
7015
+ key: string;
7016
+ value: string;
7017
+ }>
7018
+ ).map((row) => [row.key, row.value]),
6119
7019
  );
6120
7020
  }
6121
7021
  const value = (key: string) => Number(this.metadataValues?.get(key) ?? 0);