@pellux/goodvibes-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/.goodvibes/GOODVIBES.md +35 -0
  2. package/.goodvibes/agents/reviewer.md +48 -0
  3. package/.goodvibes/skills/add-provider/SKILL.md +199 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +74 -0
  6. package/bin/goodvibes-agent.ts +2 -0
  7. package/docs/README.md +23 -0
  8. package/docs/deployment-and-services.md +57 -0
  9. package/docs/getting-started.md +53 -0
  10. package/docs/release-and-publishing.md +46 -0
  11. package/package.json +134 -0
  12. package/scripts/check-bun.sh +20 -0
  13. package/src/audio/player.ts +156 -0
  14. package/src/audio/spoken-turn-controller.ts +203 -0
  15. package/src/audio/spoken-turn-model-routing.ts +117 -0
  16. package/src/audio/spoken-turn-wiring.ts +44 -0
  17. package/src/audio/text-chunker.ts +110 -0
  18. package/src/cli/bundle-command.ts +227 -0
  19. package/src/cli/completion.ts +90 -0
  20. package/src/cli/config-overrides.ts +159 -0
  21. package/src/cli/endpoints.ts +63 -0
  22. package/src/cli/entrypoint.ts +172 -0
  23. package/src/cli/help.ts +299 -0
  24. package/src/cli/index.ts +11 -0
  25. package/src/cli/management-commands.ts +426 -0
  26. package/src/cli/management.ts +744 -0
  27. package/src/cli/network-posture.ts +46 -0
  28. package/src/cli/package-verification.ts +123 -0
  29. package/src/cli/parser.ts +369 -0
  30. package/src/cli/provider-auth-routes.ts +22 -0
  31. package/src/cli/provider-classification.ts +107 -0
  32. package/src/cli/redaction.ts +105 -0
  33. package/src/cli/service-command.ts +26 -0
  34. package/src/cli/service-posture.ts +482 -0
  35. package/src/cli/status.ts +383 -0
  36. package/src/cli/surface-command.ts +247 -0
  37. package/src/cli/tui-startup.ts +32 -0
  38. package/src/cli/types.ts +69 -0
  39. package/src/cli-flags.ts +21 -0
  40. package/src/config/goodvibes-home-audit.ts +465 -0
  41. package/src/config/index.ts +57 -0
  42. package/src/config/provider-model.ts +23 -0
  43. package/src/config/secret-config.ts +119 -0
  44. package/src/config/secrets.ts +71 -0
  45. package/src/config/surface.ts +1 -0
  46. package/src/core/composer-state.ts +61 -0
  47. package/src/core/conversation-rendering.ts +359 -0
  48. package/src/core/conversation.ts +551 -0
  49. package/src/core/history.ts +45 -0
  50. package/src/core/orchestrator.ts +7 -0
  51. package/src/core/system-message-router.ts +171 -0
  52. package/src/daemon/cli.ts +55 -0
  53. package/src/daemon/safe-serve.ts +61 -0
  54. package/src/input/agent-workspace.ts +428 -0
  55. package/src/input/autocomplete.ts +96 -0
  56. package/src/input/bookmark-modal.ts +115 -0
  57. package/src/input/command-args-hint.ts +36 -0
  58. package/src/input/command-registry.ts +329 -0
  59. package/src/input/commands/agent-externalized-tui.ts +73 -0
  60. package/src/input/commands/agent-workspace-runtime.ts +17 -0
  61. package/src/input/commands/branch-runtime.ts +72 -0
  62. package/src/input/commands/cloudflare-runtime.ts +370 -0
  63. package/src/input/commands/config.ts +18 -0
  64. package/src/input/commands/control-room-runtime.ts +255 -0
  65. package/src/input/commands/conversation-runtime.ts +207 -0
  66. package/src/input/commands/discovery-runtime.ts +52 -0
  67. package/src/input/commands/eval.ts +204 -0
  68. package/src/input/commands/experience-runtime.ts +278 -0
  69. package/src/input/commands/guidance-runtime.ts +106 -0
  70. package/src/input/commands/health-runtime.ts +434 -0
  71. package/src/input/commands/hooks-runtime.ts +148 -0
  72. package/src/input/commands/incident-runtime.ts +95 -0
  73. package/src/input/commands/integration-runtime.ts +394 -0
  74. package/src/input/commands/intelligence-runtime.ts +223 -0
  75. package/src/input/commands/knowledge.ts +531 -0
  76. package/src/input/commands/local-auth-runtime.ts +105 -0
  77. package/src/input/commands/local-provider-runtime.ts +170 -0
  78. package/src/input/commands/local-runtime.ts +392 -0
  79. package/src/input/commands/local-setup-review.ts +199 -0
  80. package/src/input/commands/local-setup-transfer.ts +135 -0
  81. package/src/input/commands/local-setup.ts +282 -0
  82. package/src/input/commands/managed-runtime.ts +209 -0
  83. package/src/input/commands/marketplace-runtime.ts +290 -0
  84. package/src/input/commands/mcp-runtime.ts +432 -0
  85. package/src/input/commands/memory-product-runtime.ts +111 -0
  86. package/src/input/commands/memory.ts +151 -0
  87. package/src/input/commands/notify-runtime.ts +83 -0
  88. package/src/input/commands/onboarding-runtime.ts +14 -0
  89. package/src/input/commands/operator-panel-runtime.ts +146 -0
  90. package/src/input/commands/operator-runtime.ts +392 -0
  91. package/src/input/commands/planning-runtime.ts +205 -0
  92. package/src/input/commands/platform-access-runtime.ts +422 -0
  93. package/src/input/commands/platform-services-runtime.ts +246 -0
  94. package/src/input/commands/policy-dispatch.ts +339 -0
  95. package/src/input/commands/policy.ts +17 -0
  96. package/src/input/commands/product-runtime.ts +351 -0
  97. package/src/input/commands/profile-sync-runtime.ts +99 -0
  98. package/src/input/commands/provider-accounts-runtime.ts +113 -0
  99. package/src/input/commands/provider.ts +363 -0
  100. package/src/input/commands/qrcode-runtime.ts +20 -0
  101. package/src/input/commands/quit-shared.ts +162 -0
  102. package/src/input/commands/recall-bundle.ts +132 -0
  103. package/src/input/commands/recall-capture.ts +152 -0
  104. package/src/input/commands/recall-query.ts +229 -0
  105. package/src/input/commands/recall-review.ts +98 -0
  106. package/src/input/commands/recall-shared.ts +22 -0
  107. package/src/input/commands/remote-runtime-pool.ts +106 -0
  108. package/src/input/commands/remote-runtime-setup.ts +199 -0
  109. package/src/input/commands/remote-runtime.ts +431 -0
  110. package/src/input/commands/replay-runtime.ts +18 -0
  111. package/src/input/commands/runtime-services.ts +291 -0
  112. package/src/input/commands/schedule-runtime.ts +91 -0
  113. package/src/input/commands/services-runtime.ts +209 -0
  114. package/src/input/commands/session-content.ts +408 -0
  115. package/src/input/commands/session-workflow.ts +464 -0
  116. package/src/input/commands/session.ts +375 -0
  117. package/src/input/commands/settings-sync-runtime.ts +174 -0
  118. package/src/input/commands/share-runtime.ts +119 -0
  119. package/src/input/commands/shell-core.ts +307 -0
  120. package/src/input/commands/skills-runtime.ts +221 -0
  121. package/src/input/commands/subscription-runtime.ts +434 -0
  122. package/src/input/commands/tasks-runtime.ts +230 -0
  123. package/src/input/commands/teamwork-runtime.ts +339 -0
  124. package/src/input/commands/teleport-runtime.ts +57 -0
  125. package/src/input/commands/tts-runtime.ts +29 -0
  126. package/src/input/commands/work-plan-runtime.ts +169 -0
  127. package/src/input/commands.ts +131 -0
  128. package/src/input/feed-context-factory.ts +254 -0
  129. package/src/input/file-picker.ts +192 -0
  130. package/src/input/handler-command-route.ts +180 -0
  131. package/src/input/handler-content-actions.ts +497 -0
  132. package/src/input/handler-feed-routes.ts +648 -0
  133. package/src/input/handler-feed.ts +452 -0
  134. package/src/input/handler-interactions.ts +281 -0
  135. package/src/input/handler-modal-routes.ts +418 -0
  136. package/src/input/handler-modal-stack.ts +263 -0
  137. package/src/input/handler-modal-token-routes.ts +329 -0
  138. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  139. package/src/input/handler-onboarding.ts +620 -0
  140. package/src/input/handler-picker-routes.ts +472 -0
  141. package/src/input/handler-prompt-buffer.ts +320 -0
  142. package/src/input/handler-shortcuts.ts +213 -0
  143. package/src/input/handler-ui-state.ts +372 -0
  144. package/src/input/handler.ts +729 -0
  145. package/src/input/input-history.ts +297 -0
  146. package/src/input/keybindings.ts +292 -0
  147. package/src/input/mcp-workspace.ts +554 -0
  148. package/src/input/model-picker-provider-filter.ts +28 -0
  149. package/src/input/model-picker-types.ts +137 -0
  150. package/src/input/model-picker.ts +797 -0
  151. package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
  152. package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
  153. package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
  154. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
  155. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
  156. package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
  157. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
  158. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
  159. package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
  160. package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
  161. package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
  162. package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
  163. package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
  164. package/src/input/onboarding/onboarding-wizard.ts +711 -0
  165. package/src/input/panel-integration-actions.ts +78 -0
  166. package/src/input/profile-picker-modal.ts +222 -0
  167. package/src/input/search.ts +100 -0
  168. package/src/input/selection-modal.ts +163 -0
  169. package/src/input/selection.ts +135 -0
  170. package/src/input/session-picker-modal.ts +136 -0
  171. package/src/input/settings-modal-behavior.ts +37 -0
  172. package/src/input/settings-modal-secrets.ts +41 -0
  173. package/src/input/settings-modal-subscriptions.ts +95 -0
  174. package/src/input/settings-modal-types.ts +91 -0
  175. package/src/input/settings-modal.ts +793 -0
  176. package/src/input/submission-intent.ts +17 -0
  177. package/src/input/submission-router.ts +59 -0
  178. package/src/input/tts-settings-actions.ts +100 -0
  179. package/src/main.ts +792 -0
  180. package/src/mcp/runtime-reload.ts +81 -0
  181. package/src/panels/agent-inspector-panel.ts +521 -0
  182. package/src/panels/agent-inspector-shared.ts +94 -0
  183. package/src/panels/agent-logs-panel.ts +559 -0
  184. package/src/panels/agent-logs-shared.ts +129 -0
  185. package/src/panels/approval-panel.ts +150 -0
  186. package/src/panels/automation-control-panel.ts +212 -0
  187. package/src/panels/base-panel.ts +254 -0
  188. package/src/panels/builtin/agent.ts +117 -0
  189. package/src/panels/builtin/development.ts +31 -0
  190. package/src/panels/builtin/knowledge.ts +26 -0
  191. package/src/panels/builtin/operations.ts +349 -0
  192. package/src/panels/builtin/session.ts +129 -0
  193. package/src/panels/builtin/shared.ts +274 -0
  194. package/src/panels/builtin-panels.ts +23 -0
  195. package/src/panels/cockpit-panel.ts +183 -0
  196. package/src/panels/communication-panel.ts +153 -0
  197. package/src/panels/confirm-state.ts +61 -0
  198. package/src/panels/context-visualizer-panel.ts +204 -0
  199. package/src/panels/control-plane-panel.ts +211 -0
  200. package/src/panels/cost-tracker-panel.ts +444 -0
  201. package/src/panels/debug-panel.ts +432 -0
  202. package/src/panels/diff-panel.ts +520 -0
  203. package/src/panels/docs-panel.ts +283 -0
  204. package/src/panels/eval-panel.ts +399 -0
  205. package/src/panels/file-explorer-panel.ts +584 -0
  206. package/src/panels/file-preview-panel.ts +434 -0
  207. package/src/panels/forensics-panel.ts +364 -0
  208. package/src/panels/git-panel.ts +638 -0
  209. package/src/panels/hooks-panel.ts +239 -0
  210. package/src/panels/incident-review-panel.ts +197 -0
  211. package/src/panels/index.ts +46 -0
  212. package/src/panels/intelligence-panel.ts +176 -0
  213. package/src/panels/knowledge-panel.ts +345 -0
  214. package/src/panels/local-auth-panel.ts +130 -0
  215. package/src/panels/marketplace-panel.ts +212 -0
  216. package/src/panels/memory-panel.ts +225 -0
  217. package/src/panels/ops-control-panel.ts +150 -0
  218. package/src/panels/ops-strategy-panel.ts +235 -0
  219. package/src/panels/orchestration-panel.ts +273 -0
  220. package/src/panels/panel-list-panel.ts +509 -0
  221. package/src/panels/panel-manager.ts +570 -0
  222. package/src/panels/panel-picker.ts +106 -0
  223. package/src/panels/plan-dashboard-panel.ts +274 -0
  224. package/src/panels/plugins-panel.ts +178 -0
  225. package/src/panels/policy-panel.ts +308 -0
  226. package/src/panels/polish.ts +717 -0
  227. package/src/panels/project-planning-panel.ts +711 -0
  228. package/src/panels/provider-account-snapshot.ts +259 -0
  229. package/src/panels/provider-accounts-panel.ts +218 -0
  230. package/src/panels/provider-health-domains.ts +215 -0
  231. package/src/panels/provider-health-panel.ts +727 -0
  232. package/src/panels/provider-health-tracker.ts +115 -0
  233. package/src/panels/provider-stats-panel.ts +366 -0
  234. package/src/panels/qr-panel.ts +182 -0
  235. package/src/panels/remote-panel.ts +449 -0
  236. package/src/panels/routes-panel.ts +178 -0
  237. package/src/panels/sandbox-panel.ts +283 -0
  238. package/src/panels/schedule-panel.ts +329 -0
  239. package/src/panels/scrollable-list-panel.ts +491 -0
  240. package/src/panels/search-focus.ts +32 -0
  241. package/src/panels/security-panel.ts +295 -0
  242. package/src/panels/services-panel.ts +231 -0
  243. package/src/panels/session-browser-panel.ts +400 -0
  244. package/src/panels/session-maintenance.ts +125 -0
  245. package/src/panels/settings-sync-panel.ts +120 -0
  246. package/src/panels/skills-panel.ts +431 -0
  247. package/src/panels/subscription-panel.ts +263 -0
  248. package/src/panels/symbol-outline-panel.ts +486 -0
  249. package/src/panels/system-messages-panel.ts +230 -0
  250. package/src/panels/tasks-panel.ts +399 -0
  251. package/src/panels/thinking-panel.ts +304 -0
  252. package/src/panels/token-budget-panel.ts +475 -0
  253. package/src/panels/tool-inspector-panel.ts +429 -0
  254. package/src/panels/types.ts +54 -0
  255. package/src/panels/watchers-panel.ts +193 -0
  256. package/src/panels/work-plan-panel.ts +175 -0
  257. package/src/panels/worktree-panel.ts +182 -0
  258. package/src/panels/wrfc-panel.ts +609 -0
  259. package/src/permissions/prompt.ts +165 -0
  260. package/src/planning/project-planning-coordinator.ts +543 -0
  261. package/src/plugins/loader.ts +15 -0
  262. package/src/renderer/agent-detail-modal.ts +331 -0
  263. package/src/renderer/agent-workspace.ts +238 -0
  264. package/src/renderer/ansi-sanitize.ts +76 -0
  265. package/src/renderer/autocomplete-overlay.ts +154 -0
  266. package/src/renderer/block-actions.ts +76 -0
  267. package/src/renderer/bookmark-modal.ts +101 -0
  268. package/src/renderer/bottom-bar.ts +58 -0
  269. package/src/renderer/buffer.ts +113 -0
  270. package/src/renderer/code-block.ts +373 -0
  271. package/src/renderer/compositor.ts +283 -0
  272. package/src/renderer/context-inspector.ts +219 -0
  273. package/src/renderer/conversation-layout.ts +67 -0
  274. package/src/renderer/conversation-overlays.ts +140 -0
  275. package/src/renderer/conversation-surface.ts +260 -0
  276. package/src/renderer/diff-view.ts +132 -0
  277. package/src/renderer/diff.ts +130 -0
  278. package/src/renderer/file-picker-overlay.ts +101 -0
  279. package/src/renderer/file-tree.ts +153 -0
  280. package/src/renderer/fullscreen-primitives.ts +130 -0
  281. package/src/renderer/fullscreen-workspace.ts +199 -0
  282. package/src/renderer/git-status.ts +89 -0
  283. package/src/renderer/help-overlay.ts +267 -0
  284. package/src/renderer/history-search-overlay.ts +73 -0
  285. package/src/renderer/layout-engine.ts +97 -0
  286. package/src/renderer/layout.ts +32 -0
  287. package/src/renderer/live-tail-modal.ts +156 -0
  288. package/src/renderer/markdown.ts +635 -0
  289. package/src/renderer/mcp-workspace.ts +237 -0
  290. package/src/renderer/modal-factory.ts +467 -0
  291. package/src/renderer/modal-utils.ts +24 -0
  292. package/src/renderer/model-picker-overlay.ts +473 -0
  293. package/src/renderer/model-workspace.ts +488 -0
  294. package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
  295. package/src/renderer/overlay-box.ts +146 -0
  296. package/src/renderer/overlay-viewport.ts +104 -0
  297. package/src/renderer/panel-composite.ts +158 -0
  298. package/src/renderer/panel-picker-overlay.ts +202 -0
  299. package/src/renderer/panel-tab-bar.ts +69 -0
  300. package/src/renderer/panel-workspace-bar.ts +42 -0
  301. package/src/renderer/process-indicator.ts +96 -0
  302. package/src/renderer/process-modal.ts +656 -0
  303. package/src/renderer/process-summary.ts +67 -0
  304. package/src/renderer/profile-picker-modal.ts +129 -0
  305. package/src/renderer/progress.ts +98 -0
  306. package/src/renderer/qr-renderer.ts +120 -0
  307. package/src/renderer/search-overlay.ts +54 -0
  308. package/src/renderer/selection-modal-overlay.ts +214 -0
  309. package/src/renderer/semantic-diff.ts +369 -0
  310. package/src/renderer/session-picker-modal.ts +127 -0
  311. package/src/renderer/settings-modal-helpers.ts +193 -0
  312. package/src/renderer/settings-modal.ts +537 -0
  313. package/src/renderer/shell-surface.ts +88 -0
  314. package/src/renderer/status-glyphs.ts +21 -0
  315. package/src/renderer/status-token.ts +67 -0
  316. package/src/renderer/surface-layout.ts +101 -0
  317. package/src/renderer/syntax-highlighter.ts +542 -0
  318. package/src/renderer/system-message.ts +83 -0
  319. package/src/renderer/tab-strip.ts +108 -0
  320. package/src/renderer/text-layout.ts +31 -0
  321. package/src/renderer/thinking.ts +17 -0
  322. package/src/renderer/tool-call.ts +234 -0
  323. package/src/renderer/ui-factory.ts +524 -0
  324. package/src/renderer/ui-primitives.ts +96 -0
  325. package/src/runtime/bootstrap-command-context.ts +278 -0
  326. package/src/runtime/bootstrap-command-parts.ts +386 -0
  327. package/src/runtime/bootstrap-core.ts +540 -0
  328. package/src/runtime/bootstrap-hook-bridge.ts +112 -0
  329. package/src/runtime/bootstrap-shell.ts +283 -0
  330. package/src/runtime/bootstrap.ts +575 -0
  331. package/src/runtime/cloudflare-control-plane.ts +349 -0
  332. package/src/runtime/context.ts +142 -0
  333. package/src/runtime/diagnostics/panels/index.ts +24 -0
  334. package/src/runtime/diagnostics/panels/ops.ts +156 -0
  335. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  336. package/src/runtime/diagnostics/panels/policy.ts +177 -0
  337. package/src/runtime/index.ts +662 -0
  338. package/src/runtime/onboarding/apply.ts +642 -0
  339. package/src/runtime/onboarding/derivation.ts +534 -0
  340. package/src/runtime/onboarding/index.ts +7 -0
  341. package/src/runtime/onboarding/markers.ts +148 -0
  342. package/src/runtime/onboarding/snapshot.ts +406 -0
  343. package/src/runtime/onboarding/state.ts +141 -0
  344. package/src/runtime/onboarding/types.ts +404 -0
  345. package/src/runtime/onboarding/verify.ts +171 -0
  346. package/src/runtime/operator-token-cleanup.ts +27 -0
  347. package/src/runtime/perf/panel-contracts.ts +32 -0
  348. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  349. package/src/runtime/sandbox-public-gaps.ts +358 -0
  350. package/src/runtime/services.ts +670 -0
  351. package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
  352. package/src/runtime/store/domains/index.ts +222 -0
  353. package/src/runtime/store/domains/panels.ts +117 -0
  354. package/src/runtime/store/domains/ui-perf.ts +103 -0
  355. package/src/runtime/store/index.ts +305 -0
  356. package/src/runtime/store/selectors/index.ts +359 -0
  357. package/src/runtime/store/state.ts +145 -0
  358. package/src/runtime/surface-feature-flags.ts +65 -0
  359. package/src/runtime/terminal-output-guard.ts +228 -0
  360. package/src/runtime/ui/index.ts +39 -0
  361. package/src/runtime/ui/model-picker/data-provider.ts +182 -0
  362. package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
  363. package/src/runtime/ui/model-picker/index.ts +59 -0
  364. package/src/runtime/ui/model-picker/types.ts +149 -0
  365. package/src/runtime/ui/provider-health/data-provider.ts +244 -0
  366. package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
  367. package/src/runtime/ui/provider-health/index.ts +46 -0
  368. package/src/runtime/ui/provider-health/types.ts +146 -0
  369. package/src/runtime/ui-events.ts +1 -0
  370. package/src/runtime/ui-read-model-helpers.ts +1 -0
  371. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
  372. package/src/runtime/ui-read-models-observability-options.ts +1 -0
  373. package/src/runtime/ui-read-models-observability-remote.ts +1 -0
  374. package/src/runtime/ui-read-models-observability-security.ts +1 -0
  375. package/src/runtime/ui-read-models-observability-system.ts +1 -0
  376. package/src/runtime/ui-read-models-observability.ts +1 -0
  377. package/src/runtime/ui-read-models.ts +61 -0
  378. package/src/runtime/ui-service-queries.ts +1 -0
  379. package/src/runtime/ui-services.ts +190 -0
  380. package/src/scripts/process-messages.ts +42 -0
  381. package/src/shell/blocking-input.ts +98 -0
  382. package/src/shell/service-settings-sync.ts +273 -0
  383. package/src/shell/ui-openers.ts +352 -0
  384. package/src/tools/index.ts +1 -0
  385. package/src/tools/wrfc-agent-guard.ts +49 -0
  386. package/src/types/grid.ts +48 -0
  387. package/src/types/sql-js.d.ts +15 -0
  388. package/src/utils/clipboard.ts +22 -0
  389. package/src/utils/splash-lines.ts +46 -0
  390. package/src/utils/terminal-width.ts +185 -0
  391. package/src/verification/live-verifier.ts +430 -0
  392. package/src/verification/verification-ledger.ts +242 -0
  393. package/src/version.ts +17 -0
  394. package/src/widget/index.ts +2 -0
  395. package/src/widget/types.ts +9 -0
  396. package/src/widget/widget.ts +8 -0
  397. package/src/work-plans/work-plan-store.ts +374 -0
  398. package/tsconfig.json +18 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Health enrichment for model picker entries.
3
+ *
4
+ * Joins ModelDefinition records from the provider registry with health
5
+ * telemetry from ProviderHealthDomainState to produce enriched
6
+ * ModelPickerEntry objects ready for UI consumption.
7
+ */
8
+ import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
9
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
10
+ import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
11
+ import type { ProviderHealthDomainState, ProviderHealthRecord } from '@/runtime/index.ts';
12
+ import type { ModelDomainState } from '@/runtime/index.ts';
13
+ import { detectFamily, tierToCategoryFilter } from '../../../input/model-picker.ts';
14
+ import { getQualityTier, getQualityTierFromScore, compositeScore } from '@pellux/goodvibes-sdk/platform/providers';
15
+ import type {
16
+ ModelPickerEntry,
17
+ ModelPickerGroup,
18
+ ProviderHealthContext,
19
+ CapabilityFlags,
20
+ ProviderLatencyStats,
21
+ } from '@/runtime/index.ts';
22
+
23
+ /** Status sort priority (lower = shown first). */
24
+ const STATUS_ORDER: Record<string, number> = {
25
+ healthy: 0,
26
+ unknown: 1,
27
+ degraded: 2,
28
+ rate_limited: 3,
29
+ auth_error: 4,
30
+ unavailable: 5,
31
+ };
32
+
33
+ /**
34
+ * Derive ProviderHealthContext from a ProviderHealthRecord.
35
+ * Returns a safe default when the record is absent (provider not yet observed).
36
+ */
37
+ function buildHealthContext(record: ProviderHealthRecord | undefined): ProviderHealthContext {
38
+ if (!record) {
39
+ return {
40
+ status: 'unknown',
41
+ isConfigured: false,
42
+ };
43
+ }
44
+
45
+ let latency: ProviderLatencyStats | undefined;
46
+ if (record.stats.totalCalls > 0) {
47
+ latency = {
48
+ avgMs: record.stats.avgLatencyMs,
49
+ p95Ms: record.stats.maxLatencyMs,
50
+ minMs: record.stats.minLatencyMs,
51
+ };
52
+ }
53
+
54
+ return {
55
+ status: record.status,
56
+ latency,
57
+ cacheHitRate: record.cacheMetrics?.hitRate,
58
+ isConfigured: record.isConfigured,
59
+ rateLimitResetAt: record.rateLimitResetAt,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Derive CapabilityFlags from a ModelDefinition.
65
+ */
66
+ function buildCapabilityFlags(model: ModelDefinition): CapabilityFlags {
67
+ return {
68
+ reasoning: model.capabilities?.reasoning ?? false,
69
+ caching: false, // caching capability is provider-level; enriched separately if needed
70
+ toolCalling: model.capabilities?.toolCalling ?? false,
71
+ multimodal: model.capabilities?.multimodal ?? false,
72
+ codeEditing: model.capabilities?.codeEditing ?? false,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Derive quality tier and benchmark score for a model.
78
+ * Handles synthetic models (catalog-backed composite scores) and standard models.
79
+ */
80
+ function buildQualityInfo(
81
+ model: ModelDefinition,
82
+ benchmarkStore: Pick<BenchmarkStore, 'getBenchmarks'>,
83
+ providerRegistry: Pick<ProviderRegistry, 'getSyntheticModelInfoFromCatalog'>,
84
+ ): { qualityTier?: string; benchmarkScore?: number } {
85
+ if (model.provider === 'synthetic') {
86
+ const info = providerRegistry.getSyntheticModelInfoFromCatalog(model.id);
87
+ if (info?.bestCompositeScore != null) {
88
+ return {
89
+ qualityTier: getQualityTierFromScore(info.bestCompositeScore),
90
+ benchmarkScore: info.bestCompositeScore,
91
+ };
92
+ }
93
+ return {};
94
+ }
95
+
96
+ const benchmarks = benchmarkStore.getBenchmarks(model.id) ?? benchmarkStore.getBenchmarks(model.displayName);
97
+ if (!benchmarks) return {};
98
+
99
+ return {
100
+ qualityTier: getQualityTier(benchmarks.benchmarks),
101
+ benchmarkScore: compositeScore(benchmarks.benchmarks) ?? undefined,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Build the set of model IDs in the current fallback chain.
107
+ * Returns a Map from modelId to its position (0 = primary).
108
+ */
109
+ function buildFallbackPositionMap(modelState: ModelDomainState): Map<string, number> {
110
+ const map = new Map<string, number>();
111
+ // Position 0 is always the primary active model
112
+ map.set(modelState.activeModelId, 0);
113
+ for (let i = 0; i < modelState.fallbackChain.length; i++) {
114
+ const entry = modelState.fallbackChain[i];
115
+ if (!map.has(entry.modelId)) {
116
+ map.set(entry.modelId, i + 1);
117
+ }
118
+ }
119
+ return map;
120
+ }
121
+
122
+ /**
123
+ * Enrich a flat list of ModelDefinitions with health and state data.
124
+ *
125
+ * @param models - All selectable models from the registry.
126
+ * @param healthState - Current provider health domain state.
127
+ * @param modelState - Current model domain state.
128
+ * @param pinnedIds - Set of pinned/favorited model IDs.
129
+ * @returns Sorted, enriched ModelPickerEntry array.
130
+ */
131
+ export function enrichModelEntries(
132
+ models: readonly ModelDefinition[],
133
+ healthState: ProviderHealthDomainState,
134
+ modelState: ModelDomainState,
135
+ pinnedIds: ReadonlySet<string>,
136
+ benchmarkStore: Pick<BenchmarkStore, 'getBenchmarks'>,
137
+ providerRegistry: Pick<ProviderRegistry, 'getSyntheticModelInfoFromCatalog' | 'getContextWindowForModel'>,
138
+ ): ModelPickerEntry[] {
139
+ const fallbackPositions = buildFallbackPositionMap(modelState);
140
+
141
+ const entries: ModelPickerEntry[] = models.map((model) => {
142
+ const record = healthState.providers.get(model.provider);
143
+ const health = buildHealthContext(record);
144
+ const { qualityTier, benchmarkScore } = buildQualityInfo(model, benchmarkStore, providerRegistry);
145
+ const isProviderDegraded =
146
+ health.status === 'degraded' || health.status === 'rate_limited';
147
+ const isProviderUnavailable =
148
+ health.status === 'unavailable' || health.status === 'auth_error';
149
+
150
+ const fallbackPosition = fallbackPositions.get(model.id);
151
+
152
+ // Resolve effective context window and determine display source label.
153
+ const effectiveContextWindow = providerRegistry.getContextWindowForModel(model);
154
+ // Determine source: custom/local providers carry provenance on ModelDefinition;
155
+ // for catalog models, if getContextWindowForModel returned more than the
156
+ // static contextWindow it came from OpenRouter, else it's the registry value.
157
+ let contextWindowSource: ModelPickerEntry['contextWindowSource'];
158
+ if (model.contextWindowProvenance) {
159
+ contextWindowSource = model.contextWindowProvenance;
160
+ } else if (effectiveContextWindow !== model.contextWindow) {
161
+ contextWindowSource = 'openrouter';
162
+ } else {
163
+ contextWindowSource = 'registry';
164
+ }
165
+
166
+ return {
167
+ modelId: model.id,
168
+ providerId: model.provider,
169
+ displayName: model.displayName,
170
+ family: detectFamily(model),
171
+ pricingTier: tierToCategoryFilter(model.tier),
172
+ qualityTier,
173
+ benchmarkScore,
174
+ capabilities: buildCapabilityFlags(model),
175
+ health,
176
+ contextWindow: effectiveContextWindow,
177
+ contextWindowSource,
178
+ isPinned: pinnedIds.has(model.id),
179
+ isActive: model.id === modelState.activeModelId,
180
+ isProviderDegraded,
181
+ isProviderUnavailable,
182
+ isInFallbackChain: fallbackPosition !== undefined,
183
+ fallbackPosition,
184
+ };
185
+ });
186
+
187
+ // Sort: healthy providers first, then degraded, then unavailable.
188
+ // Within each status bucket: pinned first, then active, then by display name.
189
+ entries.sort((a, b) => {
190
+ const statusDiff =
191
+ (STATUS_ORDER[a.health.status] ?? 1) - (STATUS_ORDER[b.health.status] ?? 1);
192
+ if (statusDiff !== 0) return statusDiff;
193
+
194
+ // Pinned before unpinned
195
+ if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
196
+ // Active before inactive
197
+ if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
198
+
199
+ return a.displayName.localeCompare(b.displayName);
200
+ });
201
+
202
+ return entries;
203
+ }
204
+
205
+ /**
206
+ * Group a sorted list of ModelPickerEntry records by their provider ID.
207
+ * Preserves the sort order of entries within each group.
208
+ *
209
+ * @param entries - Pre-sorted enriched entries.
210
+ * @returns Groups ordered by first appearance of each provider in entries.
211
+ */
212
+ export function groupEntriesByProvider(entries: readonly ModelPickerEntry[]): ModelPickerGroup[] {
213
+ const groupMap = new Map<string, ModelPickerEntry[]>();
214
+ const groupOrder: string[] = [];
215
+
216
+ for (const entry of entries) {
217
+ if (!groupMap.has(entry.providerId)) {
218
+ groupMap.set(entry.providerId, []);
219
+ groupOrder.push(entry.providerId);
220
+ }
221
+ groupMap.get(entry.providerId)!.push(entry);
222
+ }
223
+
224
+ return groupOrder.map((providerId) => ({
225
+ label: providerId,
226
+ entries: groupMap.get(providerId)!,
227
+ }));
228
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Model picker UI data surface barrel.
3
+ *
4
+ * Re-exports all types and the ModelPickerDataProvider class.
5
+ * Also provides the createModelPickerData() factory for one-shot snapshots.
6
+ */
7
+ export type {
8
+ CapabilityFlags,
9
+ ProviderLatencyStats,
10
+ ProviderHealthContext,
11
+ ModelPickerEntry,
12
+ ModelPickerGroup,
13
+ ModelPickerData,
14
+ } from '@/runtime/index.ts';
15
+ // ProviderStatus re-exported from types for convenience
16
+ export type { ProviderStatus } from '@/runtime/index.ts';
17
+
18
+ export { ModelPickerDataProvider } from '@/runtime/index.ts';
19
+ export type { ModelPickerDataProviderOptions } from '@/runtime/index.ts';
20
+
21
+ import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
22
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
23
+ import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
24
+ import type { ProviderHealthDomainState } from '@/runtime/index.ts';
25
+ import type { ModelDomainState } from '@/runtime/index.ts';
26
+ import type { ModelPickerData } from '@/runtime/index.ts';
27
+ import { ModelPickerDataProvider } from '@/runtime/index.ts';
28
+
29
+ /**
30
+ * Produce a one-shot ModelPickerData snapshot without creating a long-lived provider.
31
+ *
32
+ * Use this when you need a single render pass and do not require change subscriptions.
33
+ * For reactive/subscription-based UIs, prefer ModelPickerDataProvider.
34
+ *
35
+ * @param models - All selectable models from the registry.
36
+ * @param healthState - Current provider health domain state.
37
+ * @param modelState - Current model domain state.
38
+ * @param pinnedIds - Set of pinned/favorited model IDs.
39
+ * @returns Immutable ModelPickerData snapshot.
40
+ */
41
+ export function createModelPickerData(
42
+ models: readonly ModelDefinition[],
43
+ healthState: ProviderHealthDomainState,
44
+ modelState: ModelDomainState,
45
+ benchmarkStore: Pick<BenchmarkStore, 'getBenchmarks'>,
46
+ providerRegistry: Pick<ProviderRegistry, 'getSyntheticModelInfoFromCatalog' | 'getContextWindowForModel'>,
47
+ pinnedIds: ReadonlySet<string> = new Set(),
48
+ ): ModelPickerData {
49
+ // Delegate to the data provider for consistent derivation logic,
50
+ // then dispose immediately since no subscriptions are needed.
51
+ const dp = new ModelPickerDataProvider(models, healthState, modelState, {
52
+ pinnedIds,
53
+ benchmarkStore,
54
+ providerRegistry,
55
+ });
56
+ const snapshot = dp.getSnapshot();
57
+ dp.dispose();
58
+ return snapshot;
59
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Model picker UI data types.
3
+ *
4
+ * Purely data-oriented — no rendering logic. These types are produced by
5
+ * ModelPickerDataProvider and consumed by renderers/components.
6
+ */
7
+ import type { ProviderStatus } from '@/runtime/index.ts';
8
+ import type { ModelFamily, CategoryFilter } from '../../../input/model-picker.ts';
9
+
10
+ // Re-export for convenience
11
+ export type { ProviderStatus };
12
+
13
+ /**
14
+ * Capability flags indicating what a model can do.
15
+ */
16
+ export interface CapabilityFlags {
17
+ /** Model supports extended reasoning / chain-of-thought. */
18
+ readonly reasoning: boolean;
19
+ /** Model supports prompt caching. */
20
+ readonly caching: boolean;
21
+ /** Model supports tool/function calling. */
22
+ readonly toolCalling: boolean;
23
+ /** Model supports vision/multimodal inputs. */
24
+ readonly multimodal: boolean;
25
+ /** Model supports code editing operations. */
26
+ readonly codeEditing: boolean;
27
+ }
28
+
29
+ /**
30
+ * Latency statistics for a provider, derived from health telemetry.
31
+ */
32
+ export interface ProviderLatencyStats {
33
+ /** Moving average latency in ms (last N calls). */
34
+ readonly avgMs: number;
35
+ /** Approximate 95th-percentile latency in ms (max of recent observations). */
36
+ readonly p95Ms: number;
37
+ /** Minimum observed latency in ms. */
38
+ readonly minMs: number;
39
+ }
40
+
41
+ /**
42
+ * Health context for a single provider, enriched into model picker entries.
43
+ */
44
+ export interface ProviderHealthContext {
45
+ /** Current health status of the provider. */
46
+ readonly status: ProviderStatus;
47
+ /** Latency stats. Present only when the provider has call history. */
48
+ readonly latency?: ProviderLatencyStats;
49
+ /** Cache hit rate (0–1). Present only when cache metrics are available. */
50
+ readonly cacheHitRate?: number;
51
+ /** True when the provider is configured with a valid API key. */
52
+ readonly isConfigured: boolean;
53
+ /** Rate limit reset time (epoch ms), if the provider is currently rate-limited. */
54
+ readonly rateLimitResetAt?: number;
55
+ }
56
+
57
+ /**
58
+ * A single entry in the model picker list, enriched with health and benchmark data.
59
+ */
60
+ export interface ModelPickerEntry {
61
+ // ── Identity ──────────────────────────────────────────────────────────────
62
+ /** Unique model identifier. */
63
+ readonly modelId: string;
64
+ /** Provider identifier. */
65
+ readonly providerId: string;
66
+ /** Human-readable display name. */
67
+ readonly displayName: string;
68
+ /** Model family (GPT, Claude, Gemini, …). */
69
+ readonly family: ModelFamily;
70
+ /** Pricing tier bucket. */
71
+ readonly pricingTier: CategoryFilter;
72
+
73
+ // ── Quality ───────────────────────────────────────────────────────────────
74
+ /** Quality tier badge (S/A/B/C), derived from benchmark composite score. */
75
+ readonly qualityTier?: string;
76
+ /** Benchmark composite score (0–1). */
77
+ readonly benchmarkScore?: number;
78
+
79
+ // ── Capabilities ──────────────────────────────────────────────────────────
80
+ /** Capability flags for this model. */
81
+ readonly capabilities: CapabilityFlags;
82
+
83
+ // ── Health ────────────────────────────────────────────────────────────────
84
+ /** Health context for the model's provider. */
85
+ readonly health: ProviderHealthContext;
86
+
87
+ // ── Context window ─────────────────────────────────────────────────────
88
+ /**
89
+ * Effective context window in tokens.
90
+ * Use this for display and budgeting — it is the authoritative value.
91
+ */
92
+ readonly contextWindow: number;
93
+ /**
94
+ * How `contextWindow` was determined.
95
+ * - `provider_api` — reported by the provider's /v1/models endpoint
96
+ * - `configured_cap` — set explicitly in the provider config file
97
+ * - `fallback` — default constant (no config or API source)
98
+ * - `openrouter` — sourced from OpenRouter model data (built-in catalog models)
99
+ * - `registry` — static value in the built-in model registry
100
+ */
101
+ readonly contextWindowSource:
102
+ | 'provider_api'
103
+ | 'configured_cap'
104
+ | 'fallback'
105
+ | 'openrouter'
106
+ | 'registry';
107
+
108
+ // ── Display state ─────────────────────────────────────────────────────────
109
+ /** True if this model is in the user's favorites/pinned list. */
110
+ readonly isPinned: boolean;
111
+ /** True if this is the currently active model. */
112
+ readonly isActive: boolean;
113
+ /** True when the provider is currently in a degraded or critical state. */
114
+ readonly isProviderDegraded: boolean;
115
+ /** True when the provider is unavailable or in auth error. */
116
+ readonly isProviderUnavailable: boolean;
117
+ /** True if this model is part of the current fallback chain. */
118
+ readonly isInFallbackChain: boolean;
119
+ /** Position in the fallback chain (0 = primary, 1+ = fallback index). */
120
+ readonly fallbackPosition?: number;
121
+ }
122
+
123
+ /**
124
+ * A visual group in the model picker list.
125
+ */
126
+ export interface ModelPickerGroup {
127
+ /** Group label (e.g. provider name, family name, quality tier). */
128
+ readonly label: string;
129
+ /** Entries belonging to this group, sorted by health then favorites. */
130
+ readonly entries: readonly ModelPickerEntry[];
131
+ }
132
+
133
+ /**
134
+ * Complete data snapshot produced by ModelPickerDataProvider.
135
+ */
136
+ export interface ModelPickerData {
137
+ /** All enriched model entries (flat, pre-sorted). */
138
+ readonly entries: readonly ModelPickerEntry[];
139
+ /** Grouped view for rendering with section headers. */
140
+ readonly groups: readonly ModelPickerGroup[];
141
+ /** IDs of providers that are currently degraded. */
142
+ readonly degradedProviderIds: readonly string[];
143
+ /** IDs of providers that are currently unavailable. */
144
+ readonly unavailableProviderIds: readonly string[];
145
+ /** Model ID currently at the head of the fallback chain (active model). */
146
+ readonly activeModelId: string;
147
+ /** Epoch ms when this snapshot was produced. */
148
+ readonly snapshotAt: number;
149
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * ProviderHealthDataProvider — enriched provider health data surface.
3
+ *
4
+ * Combines ProviderHealthDomainState and ModelDomainState into a single,
5
+ * sorted ProviderHealthData snapshot for UI consumption.
6
+ *
7
+ * This class is a data provider only — it contains no rendering logic.
8
+ * Subscribe to change notifications and call getSnapshot() to render.
9
+ */
10
+ import type { ProviderHealthDomainState, ProviderHealthRecord } from '@/runtime/index.ts';
11
+ import type { ModelDomainState } from '@/runtime/index.ts';
12
+ import { buildFallbackChainData } from '@/runtime/index.ts';
13
+ import type {
14
+ ProviderHealthData,
15
+ ProviderHealthEntry,
16
+ HealthTimeline,
17
+ HealthTimelinePoint,
18
+ } from '@/runtime/index.ts';
19
+
20
+ /**
21
+ * Number of timeline points retained per provider.
22
+ * Each point represents a snapshot at the time of a state update.
23
+ */
24
+ const TIMELINE_MAX_POINTS = 60;
25
+
26
+ /** Status sort priority (lower = shown first in list). */
27
+ const STATUS_ORDER: Record<string, number> = {
28
+ degraded: 0,
29
+ rate_limited: 1,
30
+ auth_error: 2,
31
+ unavailable: 3,
32
+ unknown: 4,
33
+ healthy: 5,
34
+ };
35
+
36
+ /**
37
+ * Internal mutable timeline buffer for a provider.
38
+ */
39
+ interface TimelineBuffer {
40
+ readonly points: HealthTimelinePoint[];
41
+ }
42
+
43
+ /**
44
+ * ProviderHealthDataProvider produces enriched provider health data snapshots.
45
+ *
46
+ * Usage:
47
+ * ```ts
48
+ * const provider = new ProviderHealthDataProvider(healthState, modelState);
49
+ * const unsub = provider.subscribe(() => {
50
+ * const data = provider.getSnapshot();
51
+ * // render data.entries, data.fallbackChain, etc.
52
+ * });
53
+ * // When state changes:
54
+ * provider.updateHealthState(newHealthState);
55
+ * provider.updateModelState(newModelState);
56
+ * // Cleanup:
57
+ * unsub();
58
+ * provider.dispose();
59
+ * ```
60
+ */
61
+ export class ProviderHealthDataProvider {
62
+ private _healthState: ProviderHealthDomainState;
63
+ private _modelState: ModelDomainState;
64
+ private _snapshot: ProviderHealthData;
65
+ private readonly _subscribers = new Set<() => void>();
66
+ /** Per-provider timeline buffers, keyed by providerId. */
67
+ private readonly _timelines = new Map<string, TimelineBuffer>();
68
+
69
+ constructor(healthState: ProviderHealthDomainState, modelState: ModelDomainState) {
70
+ this._healthState = healthState;
71
+ this._modelState = modelState;
72
+ this._seedTimelines(healthState);
73
+ this._snapshot = this._buildSnapshot();
74
+ }
75
+
76
+ /**
77
+ * Return the current enriched provider health data snapshot.
78
+ * Updated synchronously when state changes via update methods.
79
+ */
80
+ public getSnapshot(): ProviderHealthData {
81
+ return this._snapshot;
82
+ }
83
+
84
+ /**
85
+ * Register a callback invoked whenever the snapshot changes.
86
+ * @returns An unsubscribe function.
87
+ */
88
+ public subscribe(callback: () => void): () => void {
89
+ this._subscribers.add(callback);
90
+ return () => this._subscribers.delete(callback);
91
+ }
92
+
93
+ /**
94
+ * Update provider health state and rebuild the snapshot.
95
+ * Appends a new timeline point for each provider.
96
+ */
97
+ public updateHealthState(healthState: ProviderHealthDomainState): void {
98
+ this._healthState = healthState;
99
+ this._appendTimelinePoints(healthState);
100
+ this._rebuild();
101
+ }
102
+
103
+ /**
104
+ * Update model domain state (e.g. active model or fallback chain change).
105
+ * Triggers a snapshot rebuild.
106
+ */
107
+ public updateModelState(modelState: ModelDomainState): void {
108
+ this._modelState = modelState;
109
+ this._rebuild();
110
+ }
111
+
112
+ /**
113
+ * Release all subscriber references.
114
+ * Does not clear internal state — getSnapshot() remains usable after disposal.
115
+ */
116
+ public dispose(): void {
117
+ this._subscribers.clear();
118
+ }
119
+
120
+ // ── Private ────────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Seed timeline buffers from the initial health state.
124
+ * Appends one point per known provider to initialize the timeline.
125
+ */
126
+ private _seedTimelines(healthState: ProviderHealthDomainState): void {
127
+ for (const [id, record] of healthState.providers) {
128
+ const buffer: TimelineBuffer = { points: [] };
129
+ this._timelines.set(id, buffer);
130
+ this._appendPoint(buffer, record);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Append a new timeline point for every provider in the updated state.
136
+ * Creates a new buffer for providers seen for the first time.
137
+ */
138
+ private _appendTimelinePoints(healthState: ProviderHealthDomainState): void {
139
+ for (const [id, record] of healthState.providers) {
140
+ if (!this._timelines.has(id)) {
141
+ this._timelines.set(id, { points: [] });
142
+ }
143
+ this._appendPoint(this._timelines.get(id)!, record);
144
+ }
145
+ }
146
+
147
+ /** Append a single timeline point to a buffer, capping at TIMELINE_MAX_POINTS. */
148
+ private _appendPoint(buffer: TimelineBuffer, record: ProviderHealthRecord): void {
149
+ const total = record.stats.totalCalls;
150
+ const successRate = total > 0 ? record.stats.successCalls / total : 1;
151
+ const errorRate = total > 0 ? record.stats.errorCalls / total : 0;
152
+
153
+ buffer.points.push({
154
+ ts: Date.now(),
155
+ successRate,
156
+ avgLatencyMs: record.stats.avgLatencyMs,
157
+ errorRate,
158
+ });
159
+
160
+ if (buffer.points.length > TIMELINE_MAX_POINTS) {
161
+ // Array.shift() is O(n) but acceptable: TIMELINE_MAX_POINTS is capped at 60,
162
+ // so the cost is negligible and the simplicity outweighs a ring-buffer implementation.
163
+ buffer.points.shift();
164
+ }
165
+ }
166
+
167
+ /** Build a HealthTimeline from a buffer for a given provider. */
168
+ private _buildTimeline(providerId: string): HealthTimeline {
169
+ const buffer = this._timelines.get(providerId);
170
+ const points: readonly HealthTimelinePoint[] = buffer ? [...buffer.points] : [];
171
+ return {
172
+ providerId,
173
+ points,
174
+ length: points.length,
175
+ };
176
+ }
177
+
178
+ private _rebuild(): void {
179
+ this._snapshot = this._buildSnapshot();
180
+ this._notify();
181
+ }
182
+
183
+ private _buildSnapshot(): ProviderHealthData {
184
+ const entries: ProviderHealthEntry[] = [];
185
+
186
+ for (const [id, record] of this._healthState.providers) {
187
+ const total = record.stats.totalCalls;
188
+ const successRate = total > 0 ? record.stats.successCalls / total : 1;
189
+ const errorRate = total > 0 ? record.stats.errorCalls / total : 0;
190
+
191
+ entries.push({
192
+ providerId: id,
193
+ displayName: record.displayName,
194
+ status: record.status,
195
+ isActive: record.isActive,
196
+ isConfigured: record.isConfigured,
197
+ successRate,
198
+ errorRate,
199
+ p95LatencyMs: record.stats.maxLatencyMs,
200
+ avgLatencyMs: record.stats.avgLatencyMs,
201
+ totalCalls: total,
202
+ // cacheHitRate is populated only when cache-capability is wired to the provider record.
203
+ // Until then this will always be undefined — intentionally unsupported at this stage.
204
+ cacheHitRate: record.cacheMetrics?.hitRate,
205
+ cacheReadTokens: record.cacheMetrics?.cacheReadTokens,
206
+ cacheWriteTokens: record.cacheMetrics?.cacheWriteTokens,
207
+ lastSuccessAt: record.stats.lastSuccessAt,
208
+ lastErrorAt: record.stats.lastErrorAt,
209
+ lastErrorMessage: record.stats.lastErrorMessage,
210
+ lastCheckedAt: record.lastCheckedAt,
211
+ rateLimitResetAt: record.rateLimitResetAt,
212
+ timeline: this._buildTimeline(id),
213
+ });
214
+ }
215
+
216
+ // Sort: degraded/unavailable first (needs attention), then healthy, then unknown
217
+ entries.sort((a, b) => {
218
+ const diff = (STATUS_ORDER[a.status] ?? 4) - (STATUS_ORDER[b.status] ?? 4);
219
+ return diff !== 0 ? diff : a.displayName.localeCompare(b.displayName);
220
+ });
221
+
222
+ const fallbackChain = buildFallbackChainData(this._modelState, this._healthState);
223
+
224
+ return {
225
+ entries,
226
+ compositeStatus: this._healthState.compositeStatus,
227
+ degradedCount: this._healthState.degradedCount,
228
+ unavailableCount: this._healthState.unavailableCount,
229
+ fallbackChain,
230
+ warnings: this._healthState.warnings,
231
+ snapshotAt: Date.now(),
232
+ };
233
+ }
234
+
235
+ private _notify(): void {
236
+ for (const cb of this._subscribers) {
237
+ try {
238
+ cb();
239
+ } catch {
240
+ // Non-fatal: subscriber errors must not crash the provider
241
+ }
242
+ }
243
+ }
244
+ }