@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,49 +0,0 @@
1
- /**
2
- * pairing-devices.ts — shared rendering for the paired-device list.
3
- *
4
- * Both the /devices command family and the settings device-management modal
5
- * render each per-device token the same way (name · created · last-seen · id),
6
- * so the two surfaces never drift. Pure over its inputs and a `now` clock, so
7
- * the formatting is testable without real time.
8
- */
9
- import type { PublicPairingToken } from '@pellux/goodvibes-sdk/platform/pairing';
10
-
11
- /** A short, human-quotable id prefix used to target rename/revoke. */
12
- export function shortTokenId(id: string): string {
13
- return id.slice(0, 8);
14
- }
15
-
16
- /** Relative last-seen, or "never" when the device has not authenticated yet. */
17
- export function formatLastSeen(ms: number | undefined, now: number = Date.now()): string {
18
- if (ms === undefined) return 'never';
19
- const delta = Math.max(0, now - ms);
20
- if (delta < 60_000) return 'just now';
21
- const mins = Math.floor(delta / 60_000);
22
- if (mins < 60) return `${mins}m ago`;
23
- const hours = Math.floor(mins / 60);
24
- if (hours < 24) return `${hours}h ago`;
25
- return `${Math.floor(hours / 24)}d ago`;
26
- }
27
-
28
- /** Creation date as YYYY-MM-DD. */
29
- export function formatCreated(ms: number): string {
30
- return new Date(ms).toISOString().slice(0, 10);
31
- }
32
-
33
- /** One device line: name · created · last-seen · short id. */
34
- export function formatDeviceLine(token: PublicPairingToken, now: number = Date.now()): string {
35
- return `${token.name} · created ${formatCreated(token.createdAt)} · last seen ${formatLastSeen(token.lastSeenAt, now)} · ${shortTokenId(token.id)}`;
36
- }
37
-
38
- /** Resolve a token by exact id or unambiguous id-prefix; returns a resolution result. */
39
- export function resolveTokenByIdPrefix(
40
- tokens: readonly PublicPairingToken[],
41
- idOrPrefix: string,
42
- ): { readonly ok: true; readonly token: PublicPairingToken } | { readonly ok: false; readonly reason: 'not-found' | 'ambiguous' } {
43
- const exact = tokens.find((t) => t.id === idOrPrefix);
44
- if (exact) return { ok: true, token: exact };
45
- const matches = tokens.filter((t) => t.id.startsWith(idOrPrefix));
46
- if (matches.length === 1) return { ok: true, token: matches[0]! };
47
- if (matches.length === 0) return { ok: false, reason: 'not-found' };
48
- return { ok: false, reason: 'ambiguous' };
49
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * pairing-handoff.ts — the one place a TUI pairing producer mints a link.
3
- *
4
- * Every producer (the `gv pair` CLI block, the pairing modal QR, the /qrcode
5
- * flow) routes through mintPairingHandoff, which mints a fresh per-device token
6
- * and encodes the SDK's canonical `#pair=<token>` deep link — the exact shape
7
- * the web app consumes, byte-for-byte the same as the `pairing.handoff.create`
8
- * gateway verb's handler (mint + buildPairingHandoffLink). No producer encodes a
9
- * raw JSON connection blob any more: a camera scan of the QR opens the web app
10
- * already carrying the one-time token, so the device lands signed in.
11
- *
12
- * The token rides in the URL fragment, never the query, so it is never sent to a
13
- * server (no access-log / Referer exposure). The offer set rides alongside so a
14
- * bundle-aware surface can present the notifications/relay/passkey steps.
15
- */
16
- import {
17
- buildPairingHandoffFragment,
18
- buildPairingHandoffLink,
19
- describeOriginPosture,
20
- type MintedPairingToken,
21
- type OriginPosture,
22
- type PairingHandoffOfferKind,
23
- } from '@pellux/goodvibes-sdk/platform/pairing';
24
-
25
- /** The token-minting surface a handoff needs — satisfied by the SDK's PairingTokenManager. */
26
- export interface PairingTokenMinter {
27
- mint(input: { readonly name: string }): MintedPairingToken;
28
- }
29
-
30
- export interface PairingHandoff {
31
- readonly token: MintedPairingToken;
32
- readonly offers: readonly PairingHandoffOfferKind[];
33
- /** `#pair=<token>` (with an `offers=` key when offers are present). */
34
- readonly fragment: string;
35
- /** `<webOrigin>/#pair=<token>` — present only when a web origin is known. */
36
- readonly deepLink?: string | undefined;
37
- /**
38
- * The honest TLS/capability posture of the web origin the deep link opens —
39
- * the same field the `pairing.handoff.create` gateway verb carries (both
40
- * computed by the SDK's describeOriginPosture). Present only when a web origin
41
- * is known; a surface renders its one honest LAN line and its labeled
42
- * capability list from here, never from a locally-authored string.
43
- */
44
- readonly posture?: OriginPosture | undefined;
45
- }
46
-
47
- export interface MintPairingHandoffInput {
48
- readonly pairingTokens: PairingTokenMinter;
49
- /** The device/token name shown to the user and stored on the token; editable later. */
50
- readonly name: string;
51
- readonly offers: readonly PairingHandoffOfferKind[];
52
- /** The web-app origin the deep link points at; absent ⇒ fragment-only. */
53
- readonly webOrigin?: string | undefined;
54
- }
55
-
56
- /**
57
- * A default name for a freshly-minted pairing token when the user did not supply
58
- * one. Names are shown and editable later in the device management surface, so a
59
- * date-stamped default is enough to tell two devices apart at a glance.
60
- */
61
- export function defaultPairingTokenName(now: Date = new Date()): string {
62
- const stamp = now.toISOString().slice(0, 16).replace('T', ' ');
63
- return `paired device (${stamp})`;
64
- }
65
-
66
- export function mintPairingHandoff(input: MintPairingHandoffInput): PairingHandoff {
67
- const token = input.pairingTokens.mint({ name: input.name });
68
- const fragment = buildPairingHandoffFragment({ token: token.token, offers: input.offers });
69
- const deepLink = input.webOrigin
70
- ? buildPairingHandoffLink({ webOrigin: input.webOrigin, token: token.token, offers: input.offers })
71
- : undefined;
72
- // The posture is described from the SAME web origin the deep link opens — the
73
- // identical computation the gateway verb performs — so a locally-minted handoff
74
- // carries the same honest posture as one minted over the wire.
75
- const posture = input.webOrigin ? describeOriginPosture(input.webOrigin) : undefined;
76
- return { token, offers: input.offers, fragment, ...(deepLink ? { deepLink } : {}), ...(posture ? { posture } : {}) };
77
- }
78
-
79
- /** The link content a QR should encode: the full deep link when known, else the fragment. */
80
- export function pairingQrContent(handoff: PairingHandoff): string {
81
- return handoff.deepLink ?? handoff.fragment;
82
- }
83
-
84
- export interface PairingOfferAvailability {
85
- /** Whether the rendezvous relay is configured ⇒ the relay offer is presentable. */
86
- readonly relayEnabled: boolean;
87
- /** Whether a step-up (passkey) ceremony is wired ⇒ the passkey offer is presentable. */
88
- readonly stepUpAvailable: boolean;
89
- }
90
-
91
- /**
92
- * The offers this daemon can satisfy right now, in canonical order. Notifications
93
- * are always available (browser push needs no server-side prerequisite beyond
94
- * the VAPID key the daemon mints on demand); relay and passkey are gated.
95
- */
96
- export function availablePairingOffers(availability: PairingOfferAvailability): PairingHandoffOfferKind[] {
97
- const offers: PairingHandoffOfferKind[] = ['notifications'];
98
- if (availability.relayEnabled) offers.push('relay');
99
- if (availability.stepUpAvailable) offers.push('passkey');
100
- return offers;
101
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * pairing-offers.ts — plain-language copy for the pairing offer set, the one
3
- * honest LAN-posture line, and the labeled browser-capability list.
4
- *
5
- * Each offer names, in plain terms, what accepting it does and what declining it
6
- * costs — no security jargon, just the concrete consequence. Every offer is
7
- * independently declinable; a pairing without any accepted offer still pairs the
8
- * device (it just signs in with none of the extras).
9
- *
10
- * The one honest LAN line and the per-capability availability both come from the
11
- * SDK posture (describeOriginPosture / the `posture` field on a handoff), so
12
- * every surface renders the SAME truth: the exact LAN_PLAIN_HTTP_NOTICE wording
13
- * only when the posture carries it, and each browser-gated capability labeled
14
- * with whether the paired device gets it (and why not, when it does not).
15
- */
16
- import type {
17
- BrowserGatedCapability,
18
- OriginPosture,
19
- PairingHandoffOfferKind,
20
- } from '@pellux/goodvibes-sdk/platform/pairing';
21
-
22
- export interface PairingOfferCopy {
23
- readonly title: string;
24
- /** One plain-language line: what accepting does, and what declining leaves you with. */
25
- readonly consequence: string;
26
- }
27
-
28
- export const PAIRING_OFFER_COPY: Record<PairingHandoffOfferKind, PairingOfferCopy> = {
29
- notifications: {
30
- title: 'Notifications',
31
- consequence: 'This device gets a push when an agent needs your input or a run finishes. Decline and it pairs without push.',
32
- },
33
- relay: {
34
- title: 'Relay',
35
- consequence: 'Reach this daemon from off the LAN through the rendezvous relay, which sees only ciphertext and connection metadata. Decline to stay LAN-only.',
36
- },
37
- passkey: {
38
- title: 'Passkey',
39
- consequence: 'Register a passkey on this device to confirm sensitive actions with a tap. Decline and sensitive actions ask another way.',
40
- },
41
- };
42
-
43
- /** The offer set as `label — consequence` lines, in the given order. */
44
- export function formatPairingOffers(offers: readonly PairingHandoffOfferKind[]): string[] {
45
- return offers.map((kind) => {
46
- const copy = PAIRING_OFFER_COPY[kind];
47
- return ` ${copy.title} — ${copy.consequence}`;
48
- });
49
- }
50
-
51
- /**
52
- * Plain-language label for each browser-gated capability the posture reports —
53
- * what the paired device would actually get, named without jargon.
54
- */
55
- export const POSTURE_CAPABILITY_LABEL: Record<BrowserGatedCapability, string> = {
56
- 'service-worker': 'Offline app (installable / background sync)',
57
- push: 'Push notifications',
58
- microphone: 'Voice input (microphone)',
59
- };
60
-
61
- /**
62
- * The one honest LAN line to render, or null. This is the SDK posture's own
63
- * `notice` field verbatim (LAN_PLAIN_HTTP_NOTICE) — present ONLY for the
64
- * plain-http-on-LAN posture, absent (and so never a nag) everywhere else.
65
- */
66
- export function pairingPostureNotice(posture: OriginPosture | undefined): string | null {
67
- return posture?.notice ?? null;
68
- }
69
-
70
- /**
71
- * The labeled capability list from the posture: each browser-gated capability as
72
- * a `Label — available` / `Label — <reason>` line, so the pairing surface lists
73
- * what the paired device will get instead of hiding a dead button. Empty when no
74
- * posture is known.
75
- */
76
- export function formatPostureCapabilities(posture: OriginPosture | undefined): string[] {
77
- if (!posture) return [];
78
- return posture.capabilities.map((cap) => {
79
- const label = POSTURE_CAPABILITY_LABEL[cap.capability as BrowserGatedCapability] ?? cap.capability;
80
- const status = cap.available ? 'available' : (cap.reason ?? 'unavailable');
81
- return ` ${label} — ${status}`;
82
- });
83
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * pairing-origin.ts — the web-app origin a pairing deep link points at, and the
3
- * one-time write of web.publicBaseUrl from the stable-name resolution.
4
- *
5
- * The origin is the WEB endpoint's public URL (the surface the `#pair=<token>`
6
- * link opens), not the control-plane daemon URL. A user-set web.publicBaseUrl is
7
- * authoritative and is never re-derived or clobbered; only when it is empty do
8
- * we fall back to `http://<stable-host>:<web-port>` using the same ladder the
9
- * printed/QR link uses, so the link and the persisted origin always agree.
10
- */
11
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
12
- import { resolveRuntimeEndpointBinding } from '../cli/endpoints.ts';
13
- import { stableUrlHostForBindHost, type ResolvedStableHost, type StableHostInputs } from './stable-host.ts';
14
-
15
- export interface PairingWebOrigin {
16
- readonly origin: string;
17
- readonly resolvedHost: ResolvedStableHost;
18
- /** True when the origin is http:// on a non-loopback host — the honest LAN-posture case. */
19
- readonly httpOnLan: boolean;
20
- /** True when the origin came verbatim from a user-set web.publicBaseUrl. */
21
- readonly fromPublicBaseUrl: boolean;
22
- }
23
-
24
- function trimTrailingSlash(value: string): string {
25
- return value.replace(/\/+$/, '');
26
- }
27
-
28
- /** http:// on anything other than loopback is served in the clear on the LAN. */
29
- export function isHttpOnLan(origin: string): boolean {
30
- if (!origin.startsWith('http://')) return false;
31
- let host: string;
32
- try {
33
- host = new URL(origin).hostname;
34
- } catch {
35
- return false;
36
- }
37
- return host !== '127.0.0.1' && host !== 'localhost' && host !== '::1';
38
- }
39
-
40
- export function resolvePairingWebOrigin(
41
- configManager: Pick<ConfigManager, 'get'>,
42
- probe?: () => StableHostInputs,
43
- ): PairingWebOrigin {
44
- const publicBaseUrl = trimTrailingSlash(String(configManager.get('web.publicBaseUrl') ?? '').trim());
45
- if (publicBaseUrl) {
46
- return { origin: publicBaseUrl, resolvedHost: { host: hostnameOf(publicBaseUrl), kind: 'gateway-interface', stable: true }, httpOnLan: isHttpOnLan(publicBaseUrl), fromPublicBaseUrl: true };
47
- }
48
- const binding = resolveRuntimeEndpointBinding(configManager, 'web');
49
- const resolvedHost = stableUrlHostForBindHost(binding.host, probe);
50
- const origin = `http://${resolvedHost.host}:${binding.port}`;
51
- return { origin, resolvedHost, httpOnLan: isHttpOnLan(origin), fromPublicBaseUrl: false };
52
- }
53
-
54
- function hostnameOf(origin: string): string {
55
- try {
56
- return new URL(origin).hostname;
57
- } catch {
58
- return origin;
59
- }
60
- }
61
-
62
- /**
63
- * Persist web.publicBaseUrl once, from the stable-name resolution, and only when
64
- * it is empty AND the resolution produced a stable name (a DHCP-bound address is
65
- * not worth freezing into config). Returns the resolved origin either way. Never
66
- * overwrites a user-set value.
67
- */
68
- export function ensurePublicBaseUrl(
69
- configManager: Pick<ConfigManager, 'get' | 'setDynamic'>,
70
- probe?: () => StableHostInputs,
71
- ): PairingWebOrigin {
72
- const resolved = resolvePairingWebOrigin(configManager, probe);
73
- if (!resolved.fromPublicBaseUrl && resolved.resolvedHost.stable) {
74
- configManager.setDynamic('web.publicBaseUrl', resolved.origin);
75
- return { ...resolved, fromPublicBaseUrl: true };
76
- }
77
- return resolved;
78
- }
@@ -1,311 +0,0 @@
1
- /**
2
- * rewind-turn-anchors.ts — the session-scoped join key between the conversation
3
- * and the workspace-checkpoint store, for message-anchored rewind.
4
- *
5
- * The unified rewind service (SDK platform/rewind) anchors a rewind to a
6
- * `{ sessionId, turnId }`. Files-scope resolves that turnId against the
7
- * workspace checkpoints the turn engine already stamps with the same turnId.
8
- * Conversation-scope, however, needs to know HOW MANY conversation messages
9
- * existed at that turn boundary — a mapping the SDK's checkpoint store does not
10
- * carry (TUI turn-checkpoints are not stamped with a conversation-message
11
- * count). This registry supplies exactly that mapping: at every TURN_COMPLETED
12
- * the TUI records the turnId together with the live `conversation.getMessageCount()`,
13
- * so a later rewind to that turnId can truncate the conversation to precisely
14
- * the boundary the files checkpoint captured.
15
- *
16
- * Scope + lifetime: in-memory, keyed by sessionId. The registry is the live
17
- * working copy; it is mirrored to a small per-session sidecar file next to the
18
- * session's JSONL (`<sessionsDir>/<sessionId>.anchors.json`) at every turn, and
19
- * `restoreTurnAnchors` reloads that sidecar on resume so message-anchored
20
- * /rewind works identically before and after a resume. The sidecar's
21
- * `messageCount` boundaries stay valid because a resume rehydrates the same
22
- * message history the anchors were recorded against.
23
- */
24
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
25
- import { join } from 'node:path';
26
- import type { SessionSurface } from '@/runtime/index.ts';
27
-
28
-
29
- /** One recorded turn boundary — the rewind coordinator's per-turn anchor. */
30
- export interface TurnAnchor {
31
- /** The turn engine's turn id, shared with the workspace checkpoint's `turnId`. */
32
- readonly turnId: string;
33
- /** A short human label (the truncated user prompt) for the recent-turns picker. */
34
- readonly label: string;
35
- /** `conversation.getMessageCount()` captured at this turn's completion — the conversation truncation boundary. */
36
- readonly messageCount: number;
37
- /** Wall-clock ms at capture, for ordering + age display. */
38
- readonly at: number;
39
- }
40
-
41
- /** Hard cap so a very long session cannot grow this registry without bound. */
42
- const MAX_ANCHORS_PER_SESSION = 500;
43
-
44
- const registry = new Map<string, TurnAnchor[]>();
45
-
46
- /** Trim a user prompt to a single compact line for the picker. */
47
- export function summarizeTurnLabel(text: string | null | undefined, max = 72): string {
48
- const oneLine = (text ?? '').replace(/\s+/g, ' ').trim();
49
- if (oneLine.length === 0) return '(no prompt text)';
50
- return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
51
- }
52
-
53
- /**
54
- * Record a completed turn's anchor. Idempotent per turnId: a repeated turnId
55
- * updates the existing entry in place (the checkpoint engine can re-snapshot a
56
- * turn) rather than duplicating it.
57
- */
58
- export function recordTurnAnchor(sessionId: string, anchor: TurnAnchor): void {
59
- if (!sessionId || !anchor.turnId) return;
60
- const list = registry.get(sessionId) ?? [];
61
- const existingIndex = list.findIndex((a) => a.turnId === anchor.turnId);
62
- if (existingIndex >= 0) {
63
- list[existingIndex] = anchor;
64
- } else {
65
- list.push(anchor);
66
- if (list.length > MAX_ANCHORS_PER_SESSION) list.splice(0, list.length - MAX_ANCHORS_PER_SESSION);
67
- }
68
- registry.set(sessionId, list);
69
- }
70
-
71
- /** All recorded anchors for a session, oldest first (chronological). */
72
- export function getTurnAnchors(sessionId: string): readonly TurnAnchor[] {
73
- return registry.get(sessionId) ?? [];
74
- }
75
-
76
- /** Resolve an anchor by exact turnId, or null when this run never recorded it. */
77
- export function resolveTurnAnchor(sessionId: string, turnId: string): TurnAnchor | null {
78
- return registry.get(sessionId)?.find((a) => a.turnId === turnId) ?? null;
79
- }
80
-
81
- /** Drop a session's anchors. Exposed for tests and session reset. */
82
- export function clearTurnAnchors(sessionId: string): void {
83
- registry.delete(sessionId);
84
- }
85
-
86
- // --- Cross-resume persistence -------------------------------------------------
87
- //
88
- // The TUI mirrors the in-memory registry to a sidecar next to the session file.
89
- // The directory comes off the caller's SessionSurface (`surface.sessionsDir`),
90
- // the same handle the session JSONL itself is written through — so the sidecar
91
- // and its session can never end up in different directories.
92
- const ANCHOR_SIDECAR_VERSION = 1;
93
-
94
- /** Absolute path to a session's anchor sidecar, or null when inputs are unusable. */
95
- const ANCHOR_SIDECAR_SUFFIX = '.anchors.json';
96
-
97
- function anchorSidecarPath(sessionId: string, surface: SessionSurface): string | null {
98
- if (!sessionId || !surface.sessionsDir) return null;
99
- // A sessionId is a filename component; refuse anything with path separators so
100
- // a malformed id can never escape the sessions directory.
101
- if (/[\\/]/.test(sessionId)) return null;
102
- return join(surface.sessionsDir, `${sessionId}${ANCHOR_SIDECAR_SUFFIX}`);
103
- }
104
-
105
- function isTurnAnchor(value: unknown): value is TurnAnchor {
106
- if (typeof value !== 'object' || value === null) return false;
107
- const a = value as Record<string, unknown>;
108
- return (
109
- typeof a.turnId === 'string' && a.turnId.length > 0 &&
110
- typeof a.label === 'string' &&
111
- typeof a.messageCount === 'number' && Number.isFinite(a.messageCount) &&
112
- typeof a.at === 'number' && Number.isFinite(a.at)
113
- );
114
- }
115
-
116
- /**
117
- * Write the current in-memory anchors for a session to its sidecar so they
118
- * survive a resume. Best-effort and atomic (temp file + rename): a failed or
119
- * torn write must never break the turn that triggered it. Called after each
120
- * `recordTurnAnchor` at TURN_COMPLETED.
121
- */
122
- export function persistTurnAnchors(sessionId: string, surface: SessionSurface): void {
123
- const path = anchorSidecarPath(sessionId, surface);
124
- if (!path) return;
125
- const list = registry.get(sessionId) ?? [];
126
- if (list.length === 0) return; // nothing to mirror yet
127
- try {
128
- mkdirSync(surface.sessionsDir, { recursive: true });
129
- const payload = JSON.stringify({ version: ANCHOR_SIDECAR_VERSION, sessionId, anchors: list });
130
- const tmp = `${path}${ANCHOR_SIDECAR_TMP_MARKER}${process.pid}`;
131
- writeFileSync(tmp, payload);
132
- renameSync(tmp, path);
133
- } catch {
134
- /* best-effort; a rewind-anchor persist miss must never break the turn */
135
- }
136
- }
137
-
138
- /**
139
- * Reload a session's persisted anchors into the in-memory registry on resume.
140
- * Returns the number of anchors restored (0 when no sidecar exists or it is
141
- * unreadable). Idempotent via `recordTurnAnchor`'s per-turnId dedup, so a resume
142
- * that later re-records the same turn keeps a single entry.
143
- */
144
- export function restoreTurnAnchors(sessionId: string, surface: SessionSurface): number {
145
- const path = anchorSidecarPath(sessionId, surface);
146
- if (!path || !existsSync(path)) return 0;
147
- try {
148
- const parsed = JSON.parse(readFileSync(path, 'utf8')) as { anchors?: unknown };
149
- if (!parsed || !Array.isArray(parsed.anchors)) return 0;
150
- let restored = 0;
151
- for (const candidate of parsed.anchors) {
152
- if (isTurnAnchor(candidate)) {
153
- recordTurnAnchor(sessionId, candidate);
154
- restored++;
155
- }
156
- }
157
- return restored;
158
- } catch {
159
- return 0;
160
- }
161
- }
162
-
163
- // ─── Reaping orphaned sidecars ────────────────────────────────────────────────
164
- //
165
- // A sidecar's OWNER is the session JSONL it sits beside. Deleting a session
166
- // removes the JSONL but nothing ever removed the sidecar, so anchors for
167
- // sessions that no longer exist accumulate in the sessions directory forever.
168
- // The sweep below reclaims them, plus the `.tmp-<pid>` staging files a crash
169
- // between write and rename leaves behind.
170
-
171
- /** Infix for the atomic-write staging file, before the writing process's pid. */
172
- const ANCHOR_SIDECAR_TMP_MARKER = '.tmp-';
173
-
174
- /**
175
- * How long a staging file is tolerated before it is treated as crash residue:
176
- * 1 hour. `persistTurnAnchors` renames within microseconds of writing, so
177
- * anything this old was interrupted, and the completed sidecar (if the write
178
- * ever finished) is a separate file.
179
- */
180
- export const ANCHOR_TMP_MAX_AGE_MS = 60 * 60 * 1000;
181
-
182
- /**
183
- * How settled a sidecar must be before this sweep will delete it: 1 hour.
184
- *
185
- * Another instance can be persisting anchors for a session this process knows
186
- * nothing about, and the window between reading a sidecar's content and
187
- * unlinking it is not atomic. Requiring the file to have been untouched for an
188
- * hour means a sidecar that some other instance is actively rewriting is never
189
- * a candidate, while genuine residue — whose writer is long gone — always is.
190
- * The sweep repeats, so the delay costs nothing.
191
- */
192
- export const ANCHOR_SIDECAR_SETTLE_MS = 60 * 60 * 1000;
193
-
194
- export interface AnchorSidecarReapResult {
195
- /** Sidecar and staging files examined this sweep. */
196
- readonly scanned: number;
197
- /** Files deleted this sweep. */
198
- readonly reaped: number;
199
- }
200
-
201
- export interface AnchorSidecarReapOptions {
202
- /** The session this process is using right now; its sidecar is never reaped. */
203
- readonly currentSessionId?: string | null;
204
- readonly now?: () => number;
205
- /** Override the staging-file age window (tests). */
206
- readonly tmpMaxAgeMs?: number;
207
- /** Override how long a sidecar must be untouched before it can be reaped (tests). */
208
- readonly settleMs?: number;
209
- }
210
-
211
- /**
212
- * Delete anchor sidecars whose owning session file is gone, sidecars that hold
213
- * nothing readable, and abandoned staging files.
214
- *
215
- * A sidecar survives when `<sessionsDir>/<sessionId>.jsonl` still exists AND
216
- * the sidecar itself parses into at least one usable anchor — content, not
217
- * mere existence, because a sidecar truncated by a crash restores nothing and
218
- * would otherwise sit there indefinitely looking like valid state. It also
219
- * survives while it is still fresh (see `ANCHOR_SIDECAR_SETTLE_MS`), which
220
- * keeps a sidecar another instance is mid-rewrite out of reach.
221
- *
222
- * The current session's sidecar is never touched, and an unreadable or absent
223
- * sessions directory simply reclaims nothing. Idempotent and concurrency-safe:
224
- * a file another sweeper unlinked first (ENOENT) counts as reaped.
225
- */
226
- export function reapOrphanedAnchorSidecars(
227
- surface: SessionSurface,
228
- options: AnchorSidecarReapOptions = {},
229
- ): AnchorSidecarReapResult {
230
- const dir = surface.sessionsDir;
231
- if (!dir) return { scanned: 0, reaped: 0 };
232
-
233
- let names: string[];
234
- try {
235
- names = readdirSync(dir).filter(
236
- (n) => n.endsWith(ANCHOR_SIDECAR_SUFFIX) || n.includes(`${ANCHOR_SIDECAR_SUFFIX}${ANCHOR_SIDECAR_TMP_MARKER}`),
237
- );
238
- } catch {
239
- return { scanned: 0, reaped: 0 };
240
- }
241
-
242
- const now = options.now?.() ?? Date.now();
243
- const tmpMaxAgeMs = options.tmpMaxAgeMs ?? ANCHOR_TMP_MAX_AGE_MS;
244
- const settleMs = options.settleMs ?? ANCHOR_SIDECAR_SETTLE_MS;
245
- let reaped = 0;
246
-
247
- for (const name of names) {
248
- const path = join(dir, name);
249
-
250
- if (!name.endsWith(ANCHOR_SIDECAR_SUFFIX)) {
251
- // A `<sessionId>.anchors.json.tmp-<pid>` staging file. Age alone decides:
252
- // a young one may belong to another instance's in-flight write.
253
- let mtimeMs: number;
254
- try {
255
- mtimeMs = statSync(path).mtimeMs;
256
- } catch {
257
- continue;
258
- }
259
- if (now - mtimeMs > tmpMaxAgeMs && unlinkAnchorFile(path)) reaped++;
260
- continue;
261
- }
262
-
263
- const sessionId = name.slice(0, name.length - ANCHOR_SIDECAR_SUFFIX.length);
264
- if (sessionId.length === 0) continue;
265
- if (options.currentSessionId && sessionId === options.currentSessionId) continue;
266
-
267
- // A sidecar written moments ago belongs to a writer that is still around —
268
- // possibly another instance whose session this process cannot see.
269
- let sidecarMtimeMs: number;
270
- try {
271
- sidecarMtimeMs = statSync(path).mtimeMs;
272
- } catch {
273
- continue;
274
- }
275
- if (now - sidecarMtimeMs <= settleMs) continue;
276
-
277
- if (!existsSync(join(dir, `${sessionId}.jsonl`))) {
278
- // The owning session is gone — this sidecar can never be used again.
279
- if (unlinkAnchorFile(path)) reaped++;
280
- continue;
281
- }
282
- if (!sidecarHoldsAnchors(path)) {
283
- // The session survives but the sidecar is empty or torn: it restores
284
- // nothing, so keeping it only hides the loss.
285
- if (unlinkAnchorFile(path)) reaped++;
286
- }
287
- }
288
-
289
- return { scanned: names.length, reaped };
290
- }
291
-
292
- /** Content check: does this sidecar parse into at least one usable anchor? */
293
- function sidecarHoldsAnchors(path: string): boolean {
294
- try {
295
- const text = readFileSync(path, 'utf8');
296
- if (text.trim().length === 0) return false;
297
- const parsed = JSON.parse(text) as { anchors?: unknown };
298
- return Array.isArray(parsed?.anchors) && parsed.anchors.some(isTurnAnchor);
299
- } catch {
300
- return false;
301
- }
302
- }
303
-
304
- function unlinkAnchorFile(path: string): boolean {
305
- try {
306
- unlinkSync(path);
307
- return true;
308
- } catch (err) {
309
- return (err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';
310
- }
311
- }