@pellux/goodvibes-tui 1.27.1 → 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 (405) hide show
  1. package/CHANGELOG.md +116 -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 +3 -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 +4 -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 -693
  288. package/src/daemon/fatal-boot-report.ts +0 -111
  289. package/src/daemon/handlers/context.ts +0 -29
  290. package/src/daemon/handlers/contracts.ts +0 -77
  291. package/src/daemon/handlers/credentials.ts +0 -139
  292. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  293. package/src/daemon/handlers/drafts/index.ts +0 -17
  294. package/src/daemon/handlers/drafts/register.ts +0 -331
  295. package/src/daemon/handlers/errors.ts +0 -18
  296. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  297. package/src/daemon/handlers/inbox/index.ts +0 -284
  298. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  299. package/src/daemon/handlers/inbox/poller.ts +0 -212
  300. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  301. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  302. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  303. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  304. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  305. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  306. package/src/daemon/handlers/index.ts +0 -117
  307. package/src/daemon/handlers/register.ts +0 -168
  308. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  309. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  310. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  311. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  312. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  313. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  314. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  315. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  316. package/src/daemon/handlers/remote/index.ts +0 -119
  317. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  318. package/src/daemon/handlers/remote/service.ts +0 -191
  319. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  320. package/src/daemon/handlers/routing/index.ts +0 -261
  321. package/src/daemon/handlers/routing/route-store.ts +0 -319
  322. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  323. package/src/daemon/handlers/sqlite-store.ts +0 -278
  324. package/src/daemon/handlers/triage/index.ts +0 -57
  325. package/src/daemon/handlers/triage/integration.ts +0 -213
  326. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  327. package/src/daemon/handlers/triage/scorer.ts +0 -287
  328. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  329. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  330. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  331. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  332. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  333. package/src/daemon/handlers/triage/types.ts +0 -50
  334. package/src/daemon/lifecycle.ts +0 -41
  335. package/src/daemon/provision-wake-model.ts +0 -81
  336. package/src/daemon/safe-serve.ts +0 -61
  337. package/src/daemon/send/channels.ts +0 -200
  338. package/src/daemon/send/command.ts +0 -332
  339. package/src/daemon/send/composition.ts +0 -99
  340. package/src/daemon/send/failure-text.ts +0 -93
  341. package/src/daemon/send/inert-text.ts +0 -225
  342. package/src/daemon/send/stdin.ts +0 -24
  343. package/src/daemon/service-commands.ts +0 -329
  344. package/src/export/cost-utils.ts +0 -319
  345. package/src/input/selection.ts +0 -135
  346. package/src/plugins/loader.ts +0 -15
  347. package/src/renderer/bottom-bar.ts +0 -58
  348. package/src/renderer/conversation-tree.ts +0 -228
  349. package/src/runtime/agent-graph-composition.ts +0 -78
  350. package/src/runtime/channel-composition.ts +0 -46
  351. package/src/runtime/cluster-composition.ts +0 -124
  352. package/src/runtime/cluster-group-composition.ts +0 -284
  353. package/src/runtime/code-index-services.ts +0 -167
  354. package/src/runtime/daemon-handler-composition.ts +0 -76
  355. package/src/runtime/device-posture-composition.ts +0 -110
  356. package/src/runtime/durability-housekeeping.ts +0 -183
  357. package/src/runtime/durability-services.ts +0 -155
  358. package/src/runtime/embedded-service-factories.ts +0 -74
  359. package/src/runtime/feature-settings.ts +0 -154
  360. package/src/runtime/fleet-needs-input-push.ts +0 -70
  361. package/src/runtime/fleet-services.ts +0 -41
  362. package/src/runtime/idle-power-services.ts +0 -85
  363. package/src/runtime/mail-composition.ts +0 -65
  364. package/src/runtime/memory-fold.ts +0 -55
  365. package/src/runtime/operator-token-cleanup.ts +0 -109
  366. package/src/runtime/path-shadow.ts +0 -418
  367. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  368. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  369. package/src/runtime/reachability-notice.ts +0 -100
  370. package/src/runtime/remote-execution-composition.ts +0 -62
  371. package/src/runtime/session-liveness-marker.ts +0 -257
  372. package/src/runtime/session-pointer-surface.ts +0 -22
  373. package/src/runtime/session-storage-services.ts +0 -41
  374. package/src/runtime/surface-feature-flags.ts +0 -96
  375. package/src/runtime/terminal-output-guard.ts +0 -235
  376. package/src/runtime/trust/workspace-registration.ts +0 -192
  377. package/src/runtime/trust/workspace-trust.ts +0 -229
  378. package/src/runtime/ui/index.ts +0 -39
  379. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  380. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  381. package/src/runtime/ui/model-picker/index.ts +0 -59
  382. package/src/runtime/ui/model-picker/types.ts +0 -153
  383. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  384. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  385. package/src/runtime/ui/provider-health/index.ts +0 -46
  386. package/src/runtime/ui/provider-health/types.ts +0 -146
  387. package/src/runtime/ui-events.ts +0 -1
  388. package/src/runtime/ui-read-model-helpers.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  393. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  394. package/src/runtime/ui-read-models-observability.ts +0 -1
  395. package/src/runtime/ui-service-queries.ts +0 -1
  396. package/src/runtime/voice-setup-services.ts +0 -98
  397. package/src/runtime/workstream-attempts-validation.ts +0 -77
  398. package/src/runtime/workstream-draft-edits.ts +0 -95
  399. package/src/runtime/workstream-draft-store.ts +0 -274
  400. package/src/runtime/workstream-draft-types.ts +0 -42
  401. package/src/runtime/workstream-services.ts +0 -373
  402. package/src/tools/index.ts +0 -1
  403. package/src/types/grid.ts +0 -48
  404. package/src/types/sql-js.d.ts +0 -15
  405. 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
- }