@pellux/goodvibes-tui 1.27.0 → 2.0.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 (404) hide show
  1. package/CHANGELOG.md +125 -2
  2. package/README.md +7 -9
  3. package/bin/goodvibes +2 -2
  4. package/bin/launcher-support.js +8 -4
  5. package/docs/README.md +11 -2
  6. package/docs/foundation-artifacts/operator-contract.json +6403 -2347
  7. package/package.json +14 -25
  8. package/scripts/postinstall.js +35 -35
  9. package/src/audio/spoken-turn-model-routing.ts +1 -1
  10. package/src/cli/bundle-command.ts +5 -5
  11. package/src/cli/completions/generate.ts +1 -1
  12. package/src/cli/doctor.ts +3 -3
  13. package/src/cli/entrypoint.ts +12 -7
  14. package/src/cli/help.ts +4 -5
  15. package/src/cli/hooks-command.ts +1 -1
  16. package/src/cli/index.ts +1 -4
  17. package/src/cli/launch-auto-update.ts +59 -7
  18. package/src/cli/management-commands.ts +6 -6
  19. package/src/cli/management-utils.ts +5 -5
  20. package/src/cli/management.ts +7 -7
  21. package/src/cli/network-posture.ts +1 -1
  22. package/src/cli/package-verification.ts +7 -6
  23. package/src/cli/plugin-bundles-command.ts +1 -1
  24. package/src/cli/plugin-command.ts +2 -2
  25. package/src/cli/service-command.ts +2 -2
  26. package/src/cli/service-posture.ts +4 -4
  27. package/src/cli/status.ts +9 -8
  28. package/src/cli/surface-command.ts +4 -4
  29. package/src/cli/tui-startup.ts +12 -3
  30. package/src/cli-flags.ts +3 -3
  31. package/src/config/goodvibes-home-audit.ts +0 -2
  32. package/src/config/secret-config.ts +37 -5
  33. package/src/config/tui-extension-settings.ts +1 -1
  34. package/src/core/approval-alert.ts +2 -2
  35. package/src/core/budget-breach-notifier.ts +3 -3
  36. package/src/core/conversation-compaction-render.ts +1 -1
  37. package/src/core/conversation-line-cache.ts +1 -1
  38. package/src/core/conversation-render-context.ts +1 -1
  39. package/src/core/conversation-rendering.ts +2 -2
  40. package/src/core/conversation.ts +10 -10
  41. package/src/core/long-task-notifier.ts +2 -2
  42. package/src/core/memory-diagnostics-gateway.ts +1 -1
  43. package/src/core/scriptable-statusline.ts +1 -1
  44. package/src/core/session-auto-titler.ts +1 -1
  45. package/src/core/session-resume-core.ts +2 -2
  46. package/src/core/stream-event-wiring.ts +24 -44
  47. package/src/core/terminal-notifier.ts +3 -3
  48. package/src/core/turn-cancellation.ts +1 -1
  49. package/src/core/turn-event-wiring.ts +5 -5
  50. package/src/input/command-registry.ts +6 -6
  51. package/src/input/commands/channel-pairing.ts +1 -1
  52. package/src/input/commands/cluster-runtime.ts +1 -1
  53. package/src/input/commands/codebase-runtime.ts +1 -1
  54. package/src/input/commands/devices-runtime.ts +1 -1
  55. package/src/input/commands/hosted-runtime.ts +330 -0
  56. package/src/input/commands/local-setup-review.ts +1 -1
  57. package/src/input/commands/local-setup-transfer.ts +2 -2
  58. package/src/input/commands/local-setup.ts +3 -3
  59. package/src/input/commands/managed-runtime.ts +2 -2
  60. package/src/input/commands/operator-rpc.ts +24 -110
  61. package/src/input/commands/payment-card-intake.ts +1 -1
  62. package/src/input/commands/permissions-provenance.ts +2 -2
  63. package/src/input/commands/plugin-runtime.ts +1 -1
  64. package/src/input/commands/provider.ts +1 -1
  65. package/src/input/commands/relay-runtime.ts +18 -9
  66. package/src/input/commands/rewind-runtime.ts +1 -1
  67. package/src/input/commands/runtime-services.ts +1 -1
  68. package/src/input/commands/services-runtime.ts +1 -1
  69. package/src/input/commands/session-workflow.ts +1 -1
  70. package/src/input/commands/settings-sync-runtime.ts +2 -2
  71. package/src/input/commands/share-runtime.ts +1 -1
  72. package/src/input/commands/tasks-runtime.ts +145 -55
  73. package/src/input/commands/update-runtime.ts +31 -28
  74. package/src/input/commands/work-plan-runtime.ts +2 -2
  75. package/src/input/commands/workstream-attempts.ts +1 -1
  76. package/src/input/commands/workstream-runtime.ts +2 -2
  77. package/src/input/commands.ts +2 -0
  78. package/src/input/feature-unit-layout.ts +1 -1
  79. package/src/input/feed-context-factory.ts +3 -3
  80. package/src/input/handler-content-actions.ts +2 -2
  81. package/src/input/handler-feed-routes.ts +1 -1
  82. package/src/input/handler-feed.ts +3 -3
  83. package/src/input/handler-modal-token-routes.ts +1 -1
  84. package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
  85. package/src/input/handler-onboarding.ts +1 -1
  86. package/src/input/handler-types.ts +2 -2
  87. package/src/input/handler-ui-state.ts +1 -1
  88. package/src/input/handler.ts +4 -4
  89. package/src/input/input-history.ts +1 -1
  90. package/src/input/onboarding/onboarding-feature-units.ts +2 -2
  91. package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
  92. package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
  93. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
  94. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
  95. package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
  96. package/src/input/payments-config.ts +6 -6
  97. package/src/input/search.ts +1 -1
  98. package/src/input/session-picker-modal.ts +46 -0
  99. package/src/input/settings-modal-activation.ts +1 -1
  100. package/src/input/settings-modal-behavior.ts +4 -4
  101. package/src/input/settings-modal-commit.ts +11 -0
  102. package/src/input/settings-modal-data.ts +13 -80
  103. package/src/input/settings-modal-mutations.ts +33 -0
  104. package/src/input/settings-modal-secrets.ts +34 -0
  105. package/src/input/settings-modal-subscriptions.ts +1 -1
  106. package/src/input/settings-modal-types.ts +15 -1
  107. package/src/input/settings-modal.ts +28 -3
  108. package/src/input/worktree-setup-config.ts +1 -1
  109. package/src/main.ts +11 -9
  110. package/src/panels/agent-inspector-shared.ts +2 -2
  111. package/src/panels/base-panel.ts +2 -2
  112. package/src/panels/builtin/operations.ts +6 -8
  113. package/src/panels/builtin/session.ts +17 -0
  114. package/src/panels/builtin/shared.ts +5 -5
  115. package/src/panels/builtin-modals.ts +3 -3
  116. package/src/panels/confirm-state.ts +1 -1
  117. package/src/panels/cost-tracker-panel.ts +4 -4
  118. package/src/panels/diff-panel.ts +2 -2
  119. package/src/panels/diff-review-panel.ts +2 -2
  120. package/src/panels/expandable-list-panel.ts +2 -2
  121. package/src/panels/fleet-acts.ts +1 -1
  122. package/src/panels/fleet-observed-render.ts +1 -1
  123. package/src/panels/fleet-panel-format.ts +1 -1
  124. package/src/panels/fleet-panel.ts +1 -1
  125. package/src/panels/fleet-spawn.ts +1 -1
  126. package/src/panels/fleet-steer.ts +1 -1
  127. package/src/panels/fleet-transcript.ts +1 -1
  128. package/src/panels/git-panel.ts +1 -1
  129. package/src/panels/hosted-session-feed.ts +352 -0
  130. package/src/panels/hosted-session-panel.ts +221 -0
  131. package/src/panels/local-auth-panel.ts +3 -3
  132. package/src/panels/modals/devices-modal.ts +1 -1
  133. package/src/panels/modals/local-auth-modal.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +1 -1
  135. package/src/panels/modals/services-modal.ts +1 -1
  136. package/src/panels/modals/settings-sync-modal.ts +1 -1
  137. package/src/panels/modals/subscription-modal.ts +1 -1
  138. package/src/panels/modals/work-plan-modal.ts +1 -1
  139. package/src/panels/notifications-panel.ts +1 -1
  140. package/src/panels/panel-confirm-overlay.ts +1 -1
  141. package/src/panels/plugins-panel.ts +2 -2
  142. package/src/panels/polish-core.ts +2 -2
  143. package/src/panels/polish-tables.ts +1 -1
  144. package/src/panels/polish.ts +2 -2
  145. package/src/panels/scrollable-list-panel.ts +3 -3
  146. package/src/panels/skills-panel.ts +3 -3
  147. package/src/panels/token-budget-panel.ts +5 -5
  148. package/src/panels/types.ts +1 -2
  149. package/src/permissions/prompt.ts +1 -1
  150. package/src/renderer/autocomplete-overlay.ts +1 -1
  151. package/src/renderer/block-actions-overlay.ts +1 -1
  152. package/src/renderer/bookmark-modal.ts +1 -1
  153. package/src/renderer/buffer.ts +1 -1
  154. package/src/renderer/code-block.ts +1 -1
  155. package/src/renderer/composer-fragments.ts +1 -1
  156. package/src/renderer/compositor.ts +2 -2
  157. package/src/renderer/config-modal.ts +1 -1
  158. package/src/renderer/context-inspector.ts +1 -1
  159. package/src/renderer/conversation-layout.ts +2 -2
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/conversation-surface.ts +2 -2
  162. package/src/renderer/diff-view.ts +1 -1
  163. package/src/renderer/diff.ts +1 -1
  164. package/src/renderer/file-picker-overlay.ts +1 -1
  165. package/src/renderer/fleet-tab-strip.ts +1 -1
  166. package/src/renderer/fullscreen-primitives.ts +2 -2
  167. package/src/renderer/fullscreen-workspace.ts +1 -1
  168. package/src/renderer/header-line.ts +1 -1
  169. package/src/renderer/help-overlay.ts +1 -1
  170. package/src/renderer/history-search-overlay.ts +2 -2
  171. package/src/renderer/layout.ts +5 -8
  172. package/src/renderer/markdown-inline.ts +1 -1
  173. package/src/renderer/markdown-table.ts +1 -1
  174. package/src/renderer/markdown.ts +1 -1
  175. package/src/renderer/mcp-workspace.ts +1 -1
  176. package/src/renderer/modal-factory.ts +2 -2
  177. package/src/renderer/model-workspace.ts +2 -2
  178. package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
  179. package/src/renderer/overlay-box.ts +1 -1
  180. package/src/renderer/panel-composite.ts +1 -1
  181. package/src/renderer/panel-workspace-bar.ts +1 -1
  182. package/src/renderer/process-indicator.ts +1 -1
  183. package/src/renderer/profile-picker-modal.ts +1 -1
  184. package/src/renderer/search-overlay.ts +2 -2
  185. package/src/renderer/selection-modal-overlay.ts +1 -1
  186. package/src/renderer/session-picker-modal.ts +124 -3
  187. package/src/renderer/settings-modal-helpers.ts +2 -0
  188. package/src/renderer/settings-modal.ts +2 -2
  189. package/src/renderer/shell-surface.ts +2 -2
  190. package/src/renderer/system-message.ts +1 -1
  191. package/src/renderer/tab-strip.ts +1 -1
  192. package/src/renderer/theme-mode-config.ts +1 -1
  193. package/src/renderer/theme.ts +11 -12
  194. package/src/renderer/thinking.ts +1 -1
  195. package/src/renderer/tool-call.ts +2 -2
  196. package/src/renderer/tool-result-expanded-lines.ts +1 -1
  197. package/src/renderer/turn-injection.ts +7 -8
  198. package/src/renderer/ui-factory.ts +3 -3
  199. package/src/runtime/bootstrap-command-context.ts +4 -4
  200. package/src/runtime/bootstrap-command-parts.ts +5 -5
  201. package/src/runtime/bootstrap-core.ts +30 -30
  202. package/src/runtime/bootstrap-shell.ts +4 -4
  203. package/src/runtime/bootstrap.ts +128 -157
  204. package/src/runtime/client/approval-updates.ts +82 -0
  205. package/src/runtime/client/build-floors.ts +167 -0
  206. package/src/runtime/client/daemon-attach-handshake.ts +115 -0
  207. package/src/runtime/client/fleet-union.ts +147 -0
  208. package/src/runtime/client/host-service-status.ts +71 -0
  209. package/src/runtime/client/hosted-exit.ts +86 -0
  210. package/src/runtime/client/hosted-roster.ts +110 -0
  211. package/src/runtime/client/hosted-session-stream.ts +178 -0
  212. package/src/runtime/client/hosted-sessions.ts +187 -0
  213. package/src/runtime/client/operator-endpoint.ts +213 -0
  214. package/src/runtime/client/spine-adoption.ts +99 -0
  215. package/src/runtime/cloudflare-control-plane.ts +1 -1
  216. package/src/runtime/context-accounting-source.ts +1 -1
  217. package/src/runtime/context.ts +2 -2
  218. package/src/runtime/conversation-rewind-port.ts +42 -14
  219. package/src/runtime/daemon-attach-notices.ts +55 -19
  220. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  221. package/src/runtime/disposal-wiring.ts +4 -24
  222. package/src/runtime/index.ts +119 -105
  223. package/src/runtime/install-self-check-startup.ts +2 -2
  224. package/src/runtime/interaction-seams.ts +1 -1
  225. package/src/runtime/knowledge-services.ts +8 -2
  226. package/src/runtime/legacy-daemon-migration.ts +15 -3
  227. package/src/runtime/legacy-daemon-reconcile.ts +3 -2
  228. package/src/runtime/memory-spine-transport.ts +37 -267
  229. package/src/runtime/notification-dispatch.ts +2 -2
  230. package/src/runtime/onboarding/apply.ts +2 -2
  231. package/src/runtime/onboarding/derivation.ts +2 -2
  232. package/src/runtime/onboarding/markers.ts +2 -2
  233. package/src/runtime/onboarding/progress.ts +2 -2
  234. package/src/runtime/onboarding/snapshot.ts +1 -1
  235. package/src/runtime/onboarding/state.ts +2 -2
  236. package/src/runtime/onboarding/types.ts +2 -2
  237. package/src/runtime/orchestrator-core-services.ts +1 -1
  238. package/src/runtime/path-shadow-startup.ts +28 -221
  239. package/src/runtime/power-keepawake-remote.ts +3 -2
  240. package/src/runtime/process-lifecycle.ts +2 -2
  241. package/src/runtime/provider-fallback.ts +10 -66
  242. package/src/runtime/recovery-autosave.ts +1 -1
  243. package/src/runtime/recovery-offer-wiring.ts +39 -23
  244. package/src/runtime/recovery-prompt.ts +31 -17
  245. package/src/runtime/relay-reachability-bridge.ts +53 -42
  246. package/src/runtime/release-artifacts.ts +2 -1
  247. package/src/runtime/runtime-services-types.ts +82 -35
  248. package/src/runtime/services.ts +305 -300
  249. package/src/runtime/session-resume-liveness-confirm.ts +1 -1
  250. package/src/runtime/ui-services.ts +7 -6
  251. package/src/runtime/update-check.ts +23 -43
  252. package/src/runtime/workspace-checkpointing.ts +1 -1
  253. package/src/runtime/wrfc-persistence.ts +2 -2
  254. package/src/shell/ui-openers.ts +30 -7
  255. package/src/utils/clipboard.ts +1 -1
  256. package/src/verification/verification-ledger.ts +10 -8
  257. package/src/version.ts +1 -1
  258. package/bin/goodvibes-daemon +0 -90
  259. package/src/cli/config-overrides.ts +0 -267
  260. package/src/cli/endpoints.ts +0 -109
  261. package/src/cli/parser.ts +0 -395
  262. package/src/cli/redaction.ts +0 -170
  263. package/src/cli/types.ts +0 -84
  264. package/src/cluster/commands.ts +0 -382
  265. package/src/cluster/remote-daemon-target.ts +0 -225
  266. package/src/cluster/render.ts +0 -268
  267. package/src/config/atomic-write.ts +0 -70
  268. package/src/config/credential-availability.ts +0 -122
  269. package/src/config/goodvibes-home.ts +0 -149
  270. package/src/config/index.ts +0 -88
  271. package/src/config/provider-model.ts +0 -23
  272. package/src/config/read-versioned.ts +0 -228
  273. package/src/core/alert-gating.ts +0 -67
  274. package/src/core/consolidation-receipt.ts +0 -41
  275. package/src/core/focus-tracker.ts +0 -41
  276. package/src/core/history.ts +0 -45
  277. package/src/core/memory-status.ts +0 -118
  278. package/src/core/orchestrator.ts +0 -7
  279. package/src/core/pairing-devices.ts +0 -49
  280. package/src/core/pairing-handoff.ts +0 -101
  281. package/src/core/pairing-offers.ts +0 -83
  282. package/src/core/pairing-origin.ts +0 -78
  283. package/src/core/rewind-turn-anchors.ts +0 -311
  284. package/src/core/session-recovery.ts +0 -210
  285. package/src/core/stable-host.ts +0 -171
  286. package/src/core/transcript-journal.ts +0 -503
  287. package/src/daemon/cli.ts +0 -641
  288. package/src/daemon/handlers/context.ts +0 -29
  289. package/src/daemon/handlers/contracts.ts +0 -77
  290. package/src/daemon/handlers/credentials.ts +0 -139
  291. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  292. package/src/daemon/handlers/drafts/index.ts +0 -17
  293. package/src/daemon/handlers/drafts/register.ts +0 -331
  294. package/src/daemon/handlers/errors.ts +0 -18
  295. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  296. package/src/daemon/handlers/inbox/index.ts +0 -284
  297. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  298. package/src/daemon/handlers/inbox/poller.ts +0 -212
  299. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  300. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  301. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  302. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  303. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  304. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  305. package/src/daemon/handlers/index.ts +0 -117
  306. package/src/daemon/handlers/register.ts +0 -168
  307. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  308. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  309. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  310. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  311. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  312. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  313. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  314. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  315. package/src/daemon/handlers/remote/index.ts +0 -119
  316. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  317. package/src/daemon/handlers/remote/service.ts +0 -191
  318. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  319. package/src/daemon/handlers/routing/index.ts +0 -261
  320. package/src/daemon/handlers/routing/route-store.ts +0 -319
  321. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  322. package/src/daemon/handlers/sqlite-store.ts +0 -278
  323. package/src/daemon/handlers/triage/index.ts +0 -57
  324. package/src/daemon/handlers/triage/integration.ts +0 -213
  325. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  326. package/src/daemon/handlers/triage/scorer.ts +0 -287
  327. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  328. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  329. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  330. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  331. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  332. package/src/daemon/handlers/triage/types.ts +0 -50
  333. package/src/daemon/lifecycle.ts +0 -41
  334. package/src/daemon/provision-wake-model.ts +0 -81
  335. package/src/daemon/safe-serve.ts +0 -61
  336. package/src/daemon/send/channels.ts +0 -200
  337. package/src/daemon/send/command.ts +0 -332
  338. package/src/daemon/send/composition.ts +0 -99
  339. package/src/daemon/send/failure-text.ts +0 -93
  340. package/src/daemon/send/inert-text.ts +0 -225
  341. package/src/daemon/send/stdin.ts +0 -24
  342. package/src/daemon/service-commands.ts +0 -329
  343. package/src/export/cost-utils.ts +0 -319
  344. package/src/input/selection.ts +0 -135
  345. package/src/plugins/loader.ts +0 -15
  346. package/src/renderer/bottom-bar.ts +0 -58
  347. package/src/renderer/conversation-tree.ts +0 -228
  348. package/src/runtime/agent-graph-composition.ts +0 -78
  349. package/src/runtime/channel-composition.ts +0 -46
  350. package/src/runtime/cluster-composition.ts +0 -124
  351. package/src/runtime/cluster-group-composition.ts +0 -284
  352. package/src/runtime/code-index-services.ts +0 -167
  353. package/src/runtime/daemon-handler-composition.ts +0 -76
  354. package/src/runtime/device-posture-composition.ts +0 -110
  355. package/src/runtime/durability-housekeeping.ts +0 -183
  356. package/src/runtime/durability-services.ts +0 -155
  357. package/src/runtime/embedded-service-factories.ts +0 -58
  358. package/src/runtime/feature-settings.ts +0 -154
  359. package/src/runtime/fleet-needs-input-push.ts +0 -70
  360. package/src/runtime/fleet-services.ts +0 -41
  361. package/src/runtime/idle-power-services.ts +0 -85
  362. package/src/runtime/mail-composition.ts +0 -65
  363. package/src/runtime/memory-fold.ts +0 -55
  364. package/src/runtime/operator-token-cleanup.ts +0 -109
  365. package/src/runtime/path-shadow.ts +0 -418
  366. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  367. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  368. package/src/runtime/reachability-notice.ts +0 -100
  369. package/src/runtime/remote-execution-composition.ts +0 -62
  370. package/src/runtime/session-liveness-marker.ts +0 -257
  371. package/src/runtime/session-pointer-surface.ts +0 -22
  372. package/src/runtime/session-storage-services.ts +0 -41
  373. package/src/runtime/surface-feature-flags.ts +0 -96
  374. package/src/runtime/terminal-output-guard.ts +0 -235
  375. package/src/runtime/trust/workspace-registration.ts +0 -192
  376. package/src/runtime/trust/workspace-trust.ts +0 -229
  377. package/src/runtime/ui/index.ts +0 -39
  378. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  379. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  380. package/src/runtime/ui/model-picker/index.ts +0 -59
  381. package/src/runtime/ui/model-picker/types.ts +0 -153
  382. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  383. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  384. package/src/runtime/ui/provider-health/index.ts +0 -46
  385. package/src/runtime/ui/provider-health/types.ts +0 -146
  386. package/src/runtime/ui-events.ts +0 -1
  387. package/src/runtime/ui-read-model-helpers.ts +0 -1
  388. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  393. package/src/runtime/ui-read-models-observability.ts +0 -1
  394. package/src/runtime/ui-service-queries.ts +0 -1
  395. package/src/runtime/voice-setup-services.ts +0 -98
  396. package/src/runtime/workstream-attempts-validation.ts +0 -77
  397. package/src/runtime/workstream-draft-edits.ts +0 -95
  398. package/src/runtime/workstream-draft-store.ts +0 -274
  399. package/src/runtime/workstream-draft-types.ts +0 -42
  400. package/src/runtime/workstream-services.ts +0 -373
  401. package/src/tools/index.ts +0 -1
  402. package/src/types/grid.ts +0 -48
  403. package/src/types/sql-js.d.ts +0 -15
  404. package/src/work-plans/work-plan-store.ts +0 -702
@@ -1,284 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Inbox surface: attaches the HOST handler to the SDK-registered
3
- // `channels.inbox.list` gateway descriptor.
4
- //
5
- // The SDK already declares the method id, input schema, output schema, scopes
6
- // (read:channels) and HTTP binding (GET /api/channels/inbox). This module does
7
- // NOT re-declare any of that — it looks the descriptor up via the catalog and
8
- // attaches an implementation with `registerCatalogHandler` ({ replace: true }).
9
- //
10
- // register(ctx, routing) =>
11
- // 1. registers built-in provider adapter factories (slack/discord/email)
12
- // 2. constructs adapters with the daemon credential store + a route resolver
13
- // bridged from the routing surface (best-effort, never throws)
14
- // 3. opens the inbox cursor store and seeds an initial poll
15
- // 4. starts the per-provider polling loops
16
- // 5. attaches the read-only `channels.inbox.list` handler (no confirm)
17
- // 6. returns an Unregister that detaches the handler, stops the poller, and
18
- // closes the store.
19
- //
20
- // The handler reads the persisted feed (filtered by provider/limit/since) and
21
- // maps the daemon-internal item shape onto the SDK CHANNEL_INBOX_ITEM_SCHEMA
22
- // wire shape. The redacted `fromDigest` is the only sender value emitted (as
23
- // `from`); raw sender ids and unredacted bodies never leave the daemon. The
24
- // monotonic cursor advances to max(receivedAt) in the returned window.
25
- // ---------------------------------------------------------------------------
26
-
27
- import type { HandlerContext } from '../context.ts';
28
- import type { Unregister } from '../register.ts';
29
- import { registerCatalogHandler } from '../register.ts';
30
- import type { RoutingRegistration } from '../index.ts';
31
- import {
32
- buildAdapters,
33
- registerAdapterFactory,
34
- type AdapterContext,
35
- type InboundChannelItem,
36
- type RouteResolver,
37
- } from './provider-adapter.ts';
38
- import { InboxCursorStore } from './cursor-store.ts';
39
- import { InboundPoller } from './poller.ts';
40
- import { createSlackAdapter, SLACK_PROVIDER_ID } from './providers/slack.ts';
41
- import { createDiscordAdapter, DISCORD_PROVIDER_ID } from './providers/discord.ts';
42
- import { createEmailAdapter, EMAIL_PROVIDER_ID } from './providers/email.ts';
43
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
44
-
45
- export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
46
- const DEFAULT_LIMIT = 50;
47
- const MAX_LIMIT = 500;
48
-
49
- /** SDK `channels.inbox.list` input (single optional provider per the schema). */
50
- export interface InboxListInput {
51
- provider?: string;
52
- limit?: number;
53
- since?: number;
54
- }
55
-
56
- /** One item in the SDK CHANNEL_INBOX_ITEM_SCHEMA wire shape. */
57
- export interface ChannelInboxItem {
58
- id: string;
59
- provider: string;
60
- kind: string;
61
- /** Redacted sender token (sha256First, 16 hex). Never the raw id. */
62
- from: string;
63
- subject?: string;
64
- bodyPreview: string;
65
- receivedAt: number;
66
- unread: boolean;
67
- routeId?: string;
68
- }
69
-
70
- /** SDK `channels.inbox.list` output (objectSchema: items/total/truncated/cursor?). */
71
- export interface InboxListOutput {
72
- items: ChannelInboxItem[];
73
- total: number;
74
- truncated: boolean;
75
- cursor?: string;
76
- }
77
-
78
- /** Start/stop control over the poll loops, handed to a leadership gate. */
79
- export interface InboxPollingControl {
80
- /** Seed one poll and arm the loops. Resolves once polling has begun. */
81
- start(): Promise<void>;
82
- /** Disarm the loops. Resolves once no further poll can run. */
83
- stop(): Promise<void>;
84
- }
85
-
86
- export interface RegisterInboxOptions {
87
- /** Override the cursor-store filename (tests). */
88
- storeFileName?: string;
89
- /** Skip the initial seed poll (tests that drive polling manually). */
90
- skipInitialPoll?: boolean;
91
- /** Register the built-in slack/discord/email adapters (default true). */
92
- registerBuiltins?: boolean;
93
- /**
94
- * Hand polling to a leadership gate instead of starting it here.
95
- *
96
- * When supplied, register() prepares the store and the handler but does NOT
97
- * poll: the callback receives start/stop control and something else decides
98
- * when this node is the one that should be fetching. When absent the loops
99
- * start immediately, which is the behaviour every existing caller and test
100
- * relies on.
101
- *
102
- * The READ path is never gated. `channels.inbox.list` serves the persisted
103
- * feed on every node — a node that is not fetching still answers questions
104
- * about what has already arrived.
105
- *
106
- * Called once PER PROVIDER, with that provider's id and a control that
107
- * starts and stops only its loop. Each inbox account is its own surface in
108
- * the LAN election, so the machine reading the work Slack account need not
109
- * be the machine reading the mailbox — and handing one account over must not
110
- * take the others down with it.
111
- */
112
- gatePolling?: (providerId: string, control: InboxPollingControl) => void;
113
- }
114
-
115
- function registerBuiltinAdapters(): void {
116
- registerAdapterFactory(SLACK_PROVIDER_ID, (ctx) => createSlackAdapter(ctx));
117
- registerAdapterFactory(DISCORD_PROVIDER_ID, (ctx) => createDiscordAdapter(ctx));
118
- registerAdapterFactory(EMAIL_PROVIDER_ID, (ctx) => createEmailAdapter(ctx));
119
- }
120
-
121
- function normalizeInput(body: unknown): { providers?: string[]; limit: number; since?: number } {
122
- const input = (body ?? {}) as InboxListInput;
123
- const provider = typeof input.provider === 'string' && input.provider.length > 0
124
- ? input.provider
125
- : undefined;
126
- let limit = typeof input.limit === 'number' && Number.isFinite(input.limit)
127
- ? Math.floor(input.limit)
128
- : DEFAULT_LIMIT;
129
- limit = Math.min(Math.max(1, limit), MAX_LIMIT);
130
- const since = typeof input.since === 'number' && Number.isFinite(input.since) && input.since >= 0
131
- ? Math.floor(input.since)
132
- : undefined;
133
- return {
134
- ...(provider ? { providers: [provider] } : {}),
135
- limit,
136
- ...(since !== undefined ? { since } : {}),
137
- };
138
- }
139
-
140
- /** Map a daemon-internal item onto the SDK CHANNEL_INBOX_ITEM_SCHEMA wire shape. */
141
- function toWireItem(item: InboundChannelItem): ChannelInboxItem {
142
- const wire: ChannelInboxItem = {
143
- id: item.id,
144
- provider: item.provider,
145
- kind: item.kind,
146
- from: item.fromDigest,
147
- bodyPreview: item.bodyPreview,
148
- receivedAt: item.receivedAt,
149
- unread: item.unread,
150
- };
151
- if (item.subjectPreview.length > 0) wire.subject = item.subjectPreview;
152
- if (item.routeId != null) wire.routeId = item.routeId;
153
- return wire;
154
- }
155
-
156
- /**
157
- * Bridge the routing surface's profile resolver into the adapter `RouteResolver`
158
- * seam. Resolution is by provider surface (best-effort wildcard); a resolved
159
- * profile id is surfaced as the item's routeId binding. Never throws.
160
- */
161
- function routeResolverFromRouting(routing: RoutingRegistration): RouteResolver {
162
- return ({ provider }) => {
163
- try {
164
- return routing.resolveProfileId(provider) ?? undefined;
165
- } catch {
166
- return undefined;
167
- }
168
- };
169
- }
170
-
171
- /**
172
- * Register the inbox surface. Attaches the `channels.inbox.list` handler to the
173
- * SDK catalog and returns an Unregister that detaches it, stops the poller, and
174
- * closes the store.
175
- */
176
- export function registerInboxMethods(
177
- ctx: HandlerContext,
178
- routing?: RoutingRegistration,
179
- options: RegisterInboxOptions = {},
180
- ): Unregister {
181
- if (options.registerBuiltins !== false) {
182
- registerBuiltinAdapters();
183
- }
184
-
185
- const adapterContext: AdapterContext = {
186
- credentials: ctx.credentials,
187
- logger: ctx.logger,
188
- ...(routing ? { resolveRouteId: routeResolverFromRouting(routing) } : {}),
189
- };
190
- const adapters = buildAdapters(adapterContext);
191
- // Retention runs inside the store (age TTL + count cap, at init and then on a
192
- // timer). Both hooks carry COUNTS ONLY — no sender ids, subjects or bodies.
193
- const store = new InboxCursorStore(ctx.workingDirectory, options.storeFileName, {
194
- onSweep: (summary) => {
195
- ctx.logger.info('inbox retention sweep reclaimed items', {
196
- expired: summary.expired,
197
- capped: summary.capped,
198
- remaining: summary.remaining,
199
- });
200
- },
201
- onSweepError: (message) => {
202
- ctx.logger.warn('inbox retention sweep failed', { error: message });
203
- },
204
- });
205
- const poller = new InboundPoller({ adapters, store, logger: ctx.logger });
206
-
207
- const gated = options.gatePolling !== undefined;
208
-
209
- // Async bootstrap: init store, and (ungated) seed one poll and start loops.
210
- // Failures are logged but never thrown out of register() — the handler still
211
- // serves the (possibly empty) persisted feed.
212
- const ready: Promise<void> = (async () => {
213
- await store.init();
214
- if (gated) return;
215
- if (!options.skipInitialPoll) {
216
- await poller.pollOnce();
217
- }
218
- poller.start();
219
- })().catch((error: unknown) => {
220
- ctx.logger.error('inbox surface bootstrap failed', {
221
- error: summarizeError(error),
222
- });
223
- });
224
-
225
- const gate = options.gatePolling;
226
- if (gate) {
227
- // One gate per provider, not one for the poller. Leadership is decided per
228
- // inbox account, so each account's loop has to be startable and stoppable
229
- // on its own — otherwise handing one account to another machine would stop
230
- // fetching for every account this node reads.
231
- for (const providerId of poller.providerIds()) {
232
- gate(providerId, {
233
- start: async () => {
234
- // The store must be ready before the first fetch, or the seed poll
235
- // would write cursors into an uninitialised store.
236
- await ready;
237
- if (!options.skipInitialPoll) {
238
- await poller.pollProviderOnce(providerId);
239
- }
240
- poller.startProvider(providerId);
241
- },
242
- // Synchronous underneath: it clears that provider's interval, so once
243
- // it returns no further poll of that account can be scheduled.
244
- // Declared async because the gate contract promises "resolves when
245
- // consumption has ceased", and a future adapter with an in-flight
246
- // request would need to await it.
247
- stop: async () => {
248
- poller.stopProvider(providerId);
249
- },
250
- });
251
- }
252
- }
253
-
254
- const unregisterMethod = registerCatalogHandler<InboxListInput, InboxListOutput>(
255
- ctx.catalog,
256
- INBOX_LIST_METHOD_ID,
257
- async (invocation) => {
258
- await ready;
259
- const { providers, limit, since } = normalizeInput(invocation.body);
260
- const internalItems = store.listItems({
261
- ...(providers ? { providers } : {}),
262
- ...(since !== undefined ? { since } : {}),
263
- limit,
264
- });
265
- const items = internalItems.map(toWireItem);
266
- const total = store.countItems(providers);
267
- const truncated = internalItems.length >= limit && total > internalItems.length;
268
- const maxReceived = store.maxReceivedAt(providers);
269
- const nextSince = Math.max(since ?? 0, maxReceived);
270
- const output: InboxListOutput = { items, total, truncated };
271
- if (nextSince > 0) output.cursor = String(nextSince);
272
- return output;
273
- },
274
- );
275
-
276
- return () => {
277
- try {
278
- unregisterMethod();
279
- } finally {
280
- poller.stop();
281
- void store.close();
282
- }
283
- };
284
- }
@@ -1,192 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Pure mapping + redaction helpers shared by every adapter.
3
- //
4
- // Rules (from the handoff contract):
5
- // - fromDigest = sha256First(senderExternalId, 16) (never the raw id)
6
- // - bodyPreview = plain-text, PII-stripped, truncated to 500 chars
7
- // - subjectPreview <= 200 chars
8
- // These are pure and deterministic so they are unit-testable in isolation.
9
- // ---------------------------------------------------------------------------
10
-
11
- import { createHash } from 'node:crypto';
12
-
13
- export const SUBJECT_PREVIEW_MAX = 200;
14
- export const BODY_PREVIEW_MAX = 500;
15
-
16
- /** First `hexChars` hex chars of the SHA-256 digest of `input` (utf-8). */
17
- export function sha256First(input: string, hexChars: number): string {
18
- const digest = createHash('sha256').update(input, 'utf-8').digest('hex');
19
- return digest.slice(0, Math.max(0, hexChars));
20
- }
21
-
22
- /**
23
- * Digest a sender's external id to a stable 16-hex-char token (the first 8
24
- * bytes of the SHA-256 digest). The raw id is never emitted.
25
- */
26
- export function digestSender(senderExternalId: string): string {
27
- return sha256First(senderExternalId, 16);
28
- }
29
-
30
- // PII patterns stripped from body previews before they ever leave the daemon.
31
- const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
32
- // E.164-ish and common separated phone numbers (>=7 digits with separators).
33
- const PHONE_RE = /(?:\+?\d[\d\s().-]{6,}\d)/g;
34
- // Long digit runs that look like card / account numbers (13-19 digits).
35
- const LONG_NUMBER_RE = /\b\d{13,19}\b/g;
36
- // IPv4 addresses.
37
- const IPV4_RE = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
38
-
39
- // --- Secret / token patterns -----------------------------------------------
40
- // These must run BEFORE the generic numeric/email scrubbers and are ordered
41
- // most-specific first so a long opaque secret is never partially redacted.
42
- //
43
- // `Authorization: Bearer <token>` / bare `Bearer <token>`.
44
- const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
45
- // JSON Web Tokens: three base64url segments separated by dots (header.payload.sig).
46
- const JWT_RE = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g;
47
- // Slack tokens: xoxb-/xoxp-/xoxa-/xoxr-/xoxs-... and legacy xox*-.
48
- const SLACK_TOKEN_RE = /\bxox[abeoprs]-[A-Za-z0-9-]{8,}/gi;
49
- // Common prefixed provider keys: OpenAI sk-/sk-proj-, GitHub gh[poursa]_,
50
- // Google AIza..., Stripe sk_live_/pk_live_/rk_live_, Slack-app xapp-, AWS AKIA...
51
- const PREFIXED_KEY_RE =
52
- /\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[poursa]_[A-Za-z0-9]{16,}|AIza[A-Za-z0-9_-]{16,}|(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|xapp-[A-Za-z0-9-]{8,}|AKIA[A-Z0-9]{16})/g;
53
- // `token=`, `api_key=`, `access_token: ...`, `secret = ...` style key/value pairs.
54
- const KV_SECRET_RE =
55
- /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret|token|password|passwd|pwd|authorization|auth)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9._~+/=-]{6,})/gi;
56
- // Generic high-entropy opaque blobs (>=24 chars of base64url/hex) not already
57
- // caught above — catches raw API keys pasted without a recognizable prefix.
58
- const OPAQUE_SECRET_RE = /\b[A-Za-z0-9_-]{24,}\b/g;
59
-
60
- /**
61
- * Replace PII and credentials with stable redaction tokens (does not alter the
62
- * length budget). Secrets/tokens are scrubbed before the email/phone/number
63
- * passes so an OAuth/bearer/API token can never leak into a preview.
64
- */
65
- export function stripPii(input: string): string {
66
- return input
67
- .replace(BEARER_RE, '[token]')
68
- .replace(JWT_RE, '[token]')
69
- .replace(SLACK_TOKEN_RE, '[token]')
70
- .replace(PREFIXED_KEY_RE, '[token]')
71
- .replace(KV_SECRET_RE, (match) => {
72
- const sep = match.includes('=') ? '=' : ':';
73
- const key = match.slice(0, match.indexOf(sep)).trimEnd();
74
- return `${key}${sep}[token]`;
75
- })
76
- .replace(EMAIL_RE, '[email]')
77
- .replace(IPV4_RE, '[ip]')
78
- .replace(LONG_NUMBER_RE, '[number]')
79
- .replace(PHONE_RE, (match) => {
80
- // Avoid eating short numeric tokens that survived LONG_NUMBER_RE; only
81
- // redact when there are at least 7 digits.
82
- const digits = match.replace(/\D/g, '');
83
- return digits.length >= 7 ? '[phone]' : match;
84
- })
85
- // Sweep any remaining long opaque blob (e.g. a bare API key) last so we do
86
- // not clobber the redaction tokens we just inserted.
87
- .replace(OPAQUE_SECRET_RE, (match) => (/^[A-Za-z]+$/.test(match) ? match : '[token]'));
88
- }
89
-
90
- // --- Markup / MIME de-structuring ------------------------------------------
91
- // Email BODY[TEXT] is frequently raw HTML and/or a multipart/MIME payload with
92
- // boundary lines and Content-* headers. Previews must be human-readable plain
93
- // text, so we de-MIME (prefer the text/plain part), strip tags, and decode the
94
- // handful of HTML entities that survive into previews. This is a no-op on text
95
- // that is already plain.
96
- const HTML_ENTITIES: Record<string, string> = {
97
- '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"',
98
- '&#39;': "'", '&apos;': "'", '&nbsp;': ' ',
99
- };
100
-
101
- /** True when the text looks like a MIME multipart payload (has boundary lines). */
102
- function isMultipart(text: string): boolean {
103
- return /^--[^\r\n]+\r?\n/m.test(text) && /content-type:/i.test(text);
104
- }
105
-
106
- /**
107
- * Given a multipart body, return the decoded text/plain part if present,
108
- * otherwise the text/html part, otherwise the original input. Strips the
109
- * per-part MIME headers so only the body bytes remain.
110
- */
111
- function extractMimePart(text: string): string {
112
- const boundaryMatch = /^--([^\r\n]+?)(?:--)?\r?$/m.exec(text);
113
- if (!boundaryMatch) return text;
114
- const boundary = boundaryMatch[1]!;
115
- const parts = text.split(new RegExp(`--${boundary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:--)?\r?\n?`));
116
- let htmlPart: string | undefined;
117
- for (const rawPart of parts) {
118
- const split = /\r?\n\r?\n/.exec(rawPart);
119
- if (!split) continue;
120
- const headers = rawPart.slice(0, split.index);
121
- const body = rawPart.slice(split.index + split[0].length);
122
- const ctype = /content-type:\s*([^\r\n;]+)/i.exec(headers)?.[1]?.trim().toLowerCase();
123
- if (!ctype) continue;
124
- if (ctype === 'text/plain') return body.trim();
125
- if (ctype === 'text/html' && htmlPart === undefined) htmlPart = body;
126
- }
127
- return (htmlPart ?? text).trim();
128
- }
129
-
130
- /** Strip stray top-level MIME/Content-* headers from a single-part body. */
131
- function stripMimeHeaders(text: string): string {
132
- return text.replace(
133
- /^(?:content-type|content-transfer-encoding|content-disposition|content-id|mime-version|--[^\r\n]+)\b[^\r\n]*\r?\n/gim,
134
- '',
135
- );
136
- }
137
-
138
- /** Decode the small set of HTML entities that matter for plain-text previews. */
139
- function decodeEntities(text: string): string {
140
- return text
141
- .replace(/&amp;|&lt;|&gt;|&quot;|&#39;|&apos;|&nbsp;/gi, (m) => HTML_ENTITIES[m.toLowerCase()] ?? m)
142
- .replace(/&#(\d{1,7});/g, (_m, dec: string) => {
143
- const code = Number.parseInt(dec, 10);
144
- return Number.isFinite(code) ? String.fromCodePoint(code) : _m;
145
- });
146
- }
147
-
148
- /**
149
- * Convert HTML/MIME body text into readable plain text. Drops <script>/<style>
150
- * blocks entirely, turns block-level tags into spaces, removes all remaining
151
- * tags, decodes entities, and de-MIMEs multipart payloads. Plain text passes
152
- * through unchanged (modulo entity decoding).
153
- */
154
- export function stripMarkup(input: string): string {
155
- let text = input;
156
- if (isMultipart(text)) {
157
- text = extractMimePart(text);
158
- }
159
- text = stripMimeHeaders(text);
160
- // Drop script/style contents before tag removal so their bodies never leak.
161
- text = text.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ');
162
- // Only treat as HTML if there is an actual tag; avoids mangling plain text
163
- // that merely contains a stray '<'.
164
- if (/<[a-z!/][^>]*>/i.test(text)) {
165
- text = text
166
- .replace(/<\/?(?:br|p|div|tr|li|h[1-6]|table|ul|ol|blockquote|hr)\b[^>]*>/gi, ' ')
167
- .replace(/<[^>]+>/g, '');
168
- }
169
- return decodeEntities(text);
170
- }
171
-
172
- /** Collapse whitespace and trim — keeps previews single-line and tidy. */
173
- export function normalizeWhitespace(input: string): string {
174
- return input.replace(/\s+/g, ' ').trim();
175
- }
176
-
177
- /** Build a display-safe subject preview (<=200 chars, no PII, single line). */
178
- export function toSubjectPreview(raw: string | undefined | null): string {
179
- const normalized = normalizeWhitespace(stripPii(raw ?? ''));
180
- return normalized.slice(0, SUBJECT_PREVIEW_MAX);
181
- }
182
-
183
- /**
184
- * Build a display-safe body preview (<=500 chars, plain text, PII-stripped,
185
- * single line). HTML/MIME is de-structured to readable text first so previews
186
- * of real-world (HTML/multipart) emails never leak tags or MIME headers.
187
- */
188
- export function toBodyPreview(raw: string | undefined | null): string {
189
- const plain = stripMarkup(raw ?? '');
190
- const normalized = normalizeWhitespace(stripPii(plain));
191
- return normalized.slice(0, BODY_PREVIEW_MAX);
192
- }
@@ -1,212 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Inbound provider poller.
3
- //
4
- // Runs one setInterval per provider at the provider's own cadence (Slack/
5
- // Discord 30s, email 60s, others 120s). Each tick:
6
- // 1. resolves the provider's persisted cursor (nextSince)
7
- // 2. calls adapter.poll({ since, limit })
8
- // 3. dedups + persists items into the cursor store (upsert)
9
- // 4. advances the cursor monotonically to max(receivedAt)
10
- // 5. records the last per-provider state for channels.inbox.list to report
11
- //
12
- // One bad provider can never crash the loop: adapter.poll() resolves with
13
- // state:'unavailable' instead of rejecting, and any thrown error is caught and
14
- // downgraded to an 'unavailable' status here.
15
- // ---------------------------------------------------------------------------
16
-
17
- import type { InboundProviderAdapter, ProviderState } from './provider-adapter.ts';
18
- import type { InboxCursorStore } from './cursor-store.ts';
19
- import type { HandlerLogger } from '../context.ts';
20
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
-
22
- export interface ProviderStatus {
23
- id: string;
24
- state: ProviderState;
25
- itemCount: number;
26
- error?: string;
27
- lastPolledAt?: number;
28
- }
29
-
30
- export interface PollerOptions {
31
- adapters: Map<string, InboundProviderAdapter>;
32
- store: InboxCursorStore;
33
- logger: HandlerLogger;
34
- /** Max items fetched per provider per tick. */
35
- perProviderLimit?: number;
36
- /** Inject a timer factory for tests (defaults to global setInterval). */
37
- setIntervalImpl?: typeof setInterval;
38
- clearIntervalImpl?: typeof clearInterval;
39
- }
40
-
41
- const DEFAULT_PER_PROVIDER_LIMIT = 50;
42
-
43
- export class InboundPoller {
44
- private readonly adapters: Map<string, InboundProviderAdapter>;
45
- private readonly store: InboxCursorStore;
46
- private readonly logger: HandlerLogger;
47
- private readonly perProviderLimit: number;
48
- private readonly setIntervalImpl: typeof setInterval;
49
- private readonly clearIntervalImpl: typeof clearInterval;
50
- private readonly timers = new Map<string, ReturnType<typeof setInterval>>();
51
- private readonly statuses = new Map<string, ProviderStatus>();
52
- private readonly inFlight = new Set<string>();
53
- private started = false;
54
- /**
55
- * Set by stop(), which the surface teardown calls and nothing else — unlike
56
- * stopProvider(), which a leadership handover uses and which must stay
57
- * resumable. Once the surface is released no interval may be armed again,
58
- * including by work that was already in flight: registerInboxMethods() starts
59
- * the store bootstrap without awaiting it, so a teardown during that window
60
- * would otherwise be followed by a start() that nothing is left to undo.
61
- */
62
- private released = false;
63
-
64
- constructor(options: PollerOptions) {
65
- this.adapters = options.adapters;
66
- this.store = options.store;
67
- this.logger = options.logger;
68
- this.perProviderLimit = options.perProviderLimit ?? DEFAULT_PER_PROVIDER_LIMIT;
69
- this.setIntervalImpl = options.setIntervalImpl ?? setInterval;
70
- this.clearIntervalImpl = options.clearIntervalImpl ?? clearInterval;
71
- for (const id of this.adapters.keys()) {
72
- this.statuses.set(id, { id, state: 'empty', itemCount: 0 });
73
- }
74
- }
75
-
76
- /** Begin per-provider interval loops. Idempotent. Does NOT poll immediately. */
77
- start(): void {
78
- if (this.released || this.started) return;
79
- this.started = true;
80
- for (const id of this.adapters.keys()) this.startProvider(id);
81
- }
82
-
83
- /**
84
- * Begin ONE provider's interval loop. Idempotent.
85
- *
86
- * Each inbox account is its own surface in the LAN election, so the machine
87
- * that reads the work Slack account may not be the machine that reads the
88
- * mailbox. A blanket start/stop cannot express that: it would take every
89
- * account down to hand one of them over. Everything below is therefore
90
- * addressable per provider, and the blanket calls are fan-outs over it.
91
- */
92
- startProvider(id: string): void {
93
- if (this.released || this.timers.has(id)) return;
94
- const adapter = this.adapters.get(id);
95
- if (!adapter) return;
96
- const handle = this.setIntervalImpl(() => {
97
- void this.pollProvider(id, adapter);
98
- }, adapter.pollIntervalMs);
99
- // Do not keep the event loop alive solely for polling (Bun/Node unref).
100
- (handle as unknown as { unref?: () => void }).unref?.();
101
- this.timers.set(id, handle);
102
- }
103
-
104
- /**
105
- * Stop ONE provider's interval loop. Idempotent.
106
- *
107
- * Synchronous underneath — clearing the interval means no further tick can
108
- * be scheduled — which is what lets the RESIGN that follows a handoff be an
109
- * honest claim that this node has stopped reading that account.
110
- */
111
- stopProvider(id: string): void {
112
- const handle = this.timers.get(id);
113
- if (handle === undefined) return;
114
- this.clearIntervalImpl(handle);
115
- this.timers.delete(id);
116
- if (this.timers.size === 0) this.started = false;
117
- }
118
-
119
- /** True when this node is polling the given provider right now. */
120
- isProviderRunning(id: string): boolean {
121
- return this.timers.has(id);
122
- }
123
-
124
- /** Every provider this node has an adapter for. */
125
- providerIds(): string[] {
126
- return [...this.adapters.keys()];
127
- }
128
-
129
- /** Run a single poll across all providers now (used on register + tests). */
130
- async pollOnce(): Promise<void> {
131
- await Promise.all(
132
- [...this.adapters.entries()].map(([id, adapter]) => this.pollProvider(id, adapter)),
133
- );
134
- }
135
-
136
- /** Run a single poll for ONE provider now. Never throws. */
137
- async pollProviderOnce(id: string): Promise<void> {
138
- const adapter = this.adapters.get(id);
139
- if (!adapter) return;
140
- await this.pollProvider(id, adapter);
141
- }
142
-
143
- /** Poll a single provider, dedup + persist, update status. Never throws. */
144
- async pollProvider(id: string, adapter: InboundProviderAdapter): Promise<void> {
145
- if (this.inFlight.has(id)) return; // skip overlapping ticks
146
- this.inFlight.add(id);
147
- const since = this.store.getCursor(id) || undefined;
148
- try {
149
- const result = await adapter.poll({ since, limit: this.perProviderLimit });
150
- if (result.state === 'unavailable') {
151
- this.setStatus(id, {
152
- id,
153
- state: 'unavailable',
154
- itemCount: 0,
155
- error: result.error ?? 'provider unavailable',
156
- lastPolledAt: Date.now(),
157
- });
158
- return;
159
- }
160
- const newCount = this.store.upsertItems(result.items);
161
- let maxReceived = since ?? 0;
162
- for (const item of result.items) {
163
- if (item.receivedAt > maxReceived) maxReceived = item.receivedAt;
164
- }
165
- if (maxReceived > 0) this.store.advanceCursor(id, maxReceived);
166
- await this.store.flush();
167
- this.setStatus(id, {
168
- id,
169
- state: result.items.length > 0 ? 'ready' : 'empty',
170
- itemCount: newCount,
171
- lastPolledAt: Date.now(),
172
- });
173
- } catch (error) {
174
- const message = summarizeError(error);
175
- this.logger.warn('inbound poll failed', { provider: id, error: message });
176
- this.setStatus(id, {
177
- id,
178
- state: 'unavailable',
179
- itemCount: 0,
180
- error: message,
181
- lastPolledAt: Date.now(),
182
- });
183
- } finally {
184
- this.inFlight.delete(id);
185
- }
186
- }
187
-
188
- /** Snapshot of the last known status for each provider. */
189
- snapshotStatuses(providerIds?: readonly string[]): ProviderStatus[] {
190
- const ids = providerIds && providerIds.length > 0
191
- ? providerIds.filter((id) => this.statuses.has(id))
192
- : [...this.statuses.keys()];
193
- return ids.map((id) => {
194
- const status = this.statuses.get(id)!;
195
- return { ...status };
196
- });
197
- }
198
-
199
- /** Release the poller for good: stop every interval loop, and refuse to arm another. Idempotent. */
200
- stop(): void {
201
- this.released = true;
202
- for (const handle of this.timers.values()) {
203
- this.clearIntervalImpl(handle);
204
- }
205
- this.timers.clear();
206
- this.started = false;
207
- }
208
-
209
- private setStatus(id: string, status: ProviderStatus): void {
210
- this.statuses.set(id, status);
211
- }
212
- }