@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.
- package/CHANGELOG.md +116 -2
- package/README.md +7 -9
- package/bin/goodvibes +2 -2
- package/bin/launcher-support.js +8 -4
- package/docs/README.md +11 -2
- package/docs/foundation-artifacts/operator-contract.json +6403 -2347
- package/package.json +14 -25
- package/scripts/postinstall.js +35 -35
- package/src/audio/spoken-turn-model-routing.ts +1 -1
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completions/generate.ts +1 -1
- package/src/cli/doctor.ts +3 -3
- package/src/cli/entrypoint.ts +12 -7
- package/src/cli/help.ts +4 -5
- package/src/cli/hooks-command.ts +1 -1
- package/src/cli/index.ts +1 -4
- package/src/cli/launch-auto-update.ts +59 -7
- package/src/cli/management-commands.ts +6 -6
- package/src/cli/management-utils.ts +5 -5
- package/src/cli/management.ts +7 -7
- package/src/cli/network-posture.ts +1 -1
- package/src/cli/package-verification.ts +7 -6
- package/src/cli/plugin-bundles-command.ts +1 -1
- package/src/cli/plugin-command.ts +2 -2
- package/src/cli/service-command.ts +2 -2
- package/src/cli/service-posture.ts +4 -4
- package/src/cli/status.ts +9 -8
- package/src/cli/surface-command.ts +4 -4
- package/src/cli/tui-startup.ts +3 -3
- package/src/cli-flags.ts +3 -3
- package/src/config/goodvibes-home-audit.ts +0 -2
- package/src/config/secret-config.ts +37 -5
- package/src/config/tui-extension-settings.ts +1 -1
- package/src/core/approval-alert.ts +2 -2
- package/src/core/budget-breach-notifier.ts +3 -3
- package/src/core/conversation-compaction-render.ts +1 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-render-context.ts +1 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +10 -10
- package/src/core/long-task-notifier.ts +2 -2
- package/src/core/memory-diagnostics-gateway.ts +1 -1
- package/src/core/scriptable-statusline.ts +1 -1
- package/src/core/session-auto-titler.ts +1 -1
- package/src/core/session-resume-core.ts +2 -2
- package/src/core/stream-event-wiring.ts +24 -44
- package/src/core/terminal-notifier.ts +3 -3
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +5 -5
- package/src/input/command-registry.ts +6 -6
- package/src/input/commands/channel-pairing.ts +1 -1
- package/src/input/commands/cluster-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +1 -1
- package/src/input/commands/hosted-runtime.ts +330 -0
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup-transfer.ts +2 -2
- package/src/input/commands/local-setup.ts +3 -3
- package/src/input/commands/managed-runtime.ts +2 -2
- package/src/input/commands/operator-rpc.ts +24 -110
- package/src/input/commands/payment-card-intake.ts +1 -1
- package/src/input/commands/permissions-provenance.ts +2 -2
- package/src/input/commands/plugin-runtime.ts +1 -1
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/relay-runtime.ts +18 -9
- package/src/input/commands/rewind-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +1 -1
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +145 -55
- package/src/input/commands/update-runtime.ts +31 -28
- package/src/input/commands/work-plan-runtime.ts +2 -2
- package/src/input/commands/workstream-attempts.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +2 -0
- package/src/input/feature-unit-layout.ts +1 -1
- package/src/input/feed-context-factory.ts +3 -3
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +1 -1
- package/src/input/handler-feed.ts +3 -3
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
- package/src/input/handler-onboarding.ts +1 -1
- package/src/input/handler-types.ts +2 -2
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/handler.ts +4 -4
- package/src/input/input-history.ts +1 -1
- package/src/input/onboarding/onboarding-feature-units.ts +2 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
- package/src/input/payments-config.ts +6 -6
- package/src/input/search.ts +1 -1
- package/src/input/session-picker-modal.ts +46 -0
- package/src/input/settings-modal-activation.ts +1 -1
- package/src/input/settings-modal-behavior.ts +4 -4
- package/src/input/settings-modal-commit.ts +11 -0
- package/src/input/settings-modal-data.ts +13 -80
- package/src/input/settings-modal-mutations.ts +33 -0
- package/src/input/settings-modal-secrets.ts +34 -0
- package/src/input/settings-modal-subscriptions.ts +1 -1
- package/src/input/settings-modal-types.ts +15 -1
- package/src/input/settings-modal.ts +28 -3
- package/src/input/worktree-setup-config.ts +1 -1
- package/src/main.ts +11 -9
- package/src/panels/agent-inspector-shared.ts +2 -2
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/operations.ts +6 -8
- package/src/panels/builtin/session.ts +17 -0
- package/src/panels/builtin/shared.ts +5 -5
- package/src/panels/builtin-modals.ts +3 -3
- package/src/panels/confirm-state.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +4 -4
- package/src/panels/diff-panel.ts +2 -2
- package/src/panels/diff-review-panel.ts +2 -2
- package/src/panels/expandable-list-panel.ts +2 -2
- package/src/panels/fleet-acts.ts +1 -1
- package/src/panels/fleet-observed-render.ts +1 -1
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +1 -1
- package/src/panels/fleet-spawn.ts +1 -1
- package/src/panels/fleet-steer.ts +1 -1
- package/src/panels/fleet-transcript.ts +1 -1
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/hosted-session-feed.ts +352 -0
- package/src/panels/hosted-session-panel.ts +221 -0
- package/src/panels/local-auth-panel.ts +3 -3
- package/src/panels/modals/devices-modal.ts +1 -1
- package/src/panels/modals/local-auth-modal.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +1 -1
- package/src/panels/modals/services-modal.ts +1 -1
- package/src/panels/modals/settings-sync-modal.ts +1 -1
- package/src/panels/modals/subscription-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +1 -1
- package/src/panels/notifications-panel.ts +1 -1
- package/src/panels/panel-confirm-overlay.ts +1 -1
- package/src/panels/plugins-panel.ts +2 -2
- package/src/panels/polish-core.ts +2 -2
- package/src/panels/polish-tables.ts +1 -1
- package/src/panels/polish.ts +2 -2
- package/src/panels/scrollable-list-panel.ts +3 -3
- package/src/panels/skills-panel.ts +3 -3
- package/src/panels/token-budget-panel.ts +5 -5
- package/src/panels/types.ts +1 -2
- package/src/permissions/prompt.ts +1 -1
- package/src/renderer/autocomplete-overlay.ts +1 -1
- package/src/renderer/block-actions-overlay.ts +1 -1
- package/src/renderer/bookmark-modal.ts +1 -1
- package/src/renderer/buffer.ts +1 -1
- package/src/renderer/code-block.ts +1 -1
- package/src/renderer/composer-fragments.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/context-inspector.ts +1 -1
- package/src/renderer/conversation-layout.ts +2 -2
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/conversation-surface.ts +2 -2
- package/src/renderer/diff-view.ts +1 -1
- package/src/renderer/diff.ts +1 -1
- package/src/renderer/file-picker-overlay.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +1 -1
- package/src/renderer/fullscreen-primitives.ts +2 -2
- package/src/renderer/fullscreen-workspace.ts +1 -1
- package/src/renderer/header-line.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/history-search-overlay.ts +2 -2
- package/src/renderer/layout.ts +5 -8
- package/src/renderer/markdown-inline.ts +1 -1
- package/src/renderer/markdown-table.ts +1 -1
- package/src/renderer/markdown.ts +1 -1
- package/src/renderer/mcp-workspace.ts +1 -1
- package/src/renderer/modal-factory.ts +2 -2
- package/src/renderer/model-workspace.ts +2 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
- package/src/renderer/overlay-box.ts +1 -1
- package/src/renderer/panel-composite.ts +1 -1
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/profile-picker-modal.ts +1 -1
- package/src/renderer/search-overlay.ts +2 -2
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +124 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -2
- package/src/renderer/shell-surface.ts +2 -2
- package/src/renderer/system-message.ts +1 -1
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/theme-mode-config.ts +1 -1
- package/src/renderer/theme.ts +11 -12
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/tool-call.ts +2 -2
- package/src/renderer/tool-result-expanded-lines.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -8
- package/src/renderer/ui-factory.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -4
- package/src/runtime/bootstrap-command-parts.ts +5 -5
- package/src/runtime/bootstrap-core.ts +30 -30
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +128 -157
- package/src/runtime/client/approval-updates.ts +82 -0
- package/src/runtime/client/build-floors.ts +167 -0
- package/src/runtime/client/daemon-attach-handshake.ts +115 -0
- package/src/runtime/client/fleet-union.ts +147 -0
- package/src/runtime/client/host-service-status.ts +71 -0
- package/src/runtime/client/hosted-exit.ts +86 -0
- package/src/runtime/client/hosted-roster.ts +110 -0
- package/src/runtime/client/hosted-session-stream.ts +178 -0
- package/src/runtime/client/hosted-sessions.ts +187 -0
- package/src/runtime/client/operator-endpoint.ts +213 -0
- package/src/runtime/client/spine-adoption.ts +99 -0
- package/src/runtime/cloudflare-control-plane.ts +1 -1
- package/src/runtime/context-accounting-source.ts +1 -1
- package/src/runtime/context.ts +2 -2
- package/src/runtime/conversation-rewind-port.ts +42 -14
- package/src/runtime/daemon-attach-notices.ts +55 -19
- package/src/runtime/diagnostics/panels/ops.ts +1 -1
- package/src/runtime/disposal-wiring.ts +4 -24
- package/src/runtime/index.ts +119 -105
- package/src/runtime/install-self-check-startup.ts +2 -2
- package/src/runtime/interaction-seams.ts +1 -1
- package/src/runtime/knowledge-services.ts +8 -2
- package/src/runtime/legacy-daemon-migration.ts +4 -3
- package/src/runtime/legacy-daemon-reconcile.ts +3 -2
- package/src/runtime/memory-spine-transport.ts +37 -267
- package/src/runtime/notification-dispatch.ts +2 -2
- package/src/runtime/onboarding/apply.ts +2 -2
- package/src/runtime/onboarding/derivation.ts +2 -2
- package/src/runtime/onboarding/markers.ts +2 -2
- package/src/runtime/onboarding/progress.ts +2 -2
- package/src/runtime/onboarding/snapshot.ts +1 -1
- package/src/runtime/onboarding/state.ts +2 -2
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +1 -1
- package/src/runtime/path-shadow-startup.ts +28 -221
- package/src/runtime/power-keepawake-remote.ts +3 -2
- package/src/runtime/process-lifecycle.ts +2 -2
- package/src/runtime/provider-fallback.ts +10 -66
- package/src/runtime/recovery-autosave.ts +1 -1
- package/src/runtime/recovery-offer-wiring.ts +39 -23
- package/src/runtime/recovery-prompt.ts +31 -17
- package/src/runtime/relay-reachability-bridge.ts +53 -42
- package/src/runtime/release-artifacts.ts +2 -1
- package/src/runtime/runtime-services-types.ts +82 -35
- package/src/runtime/services.ts +305 -300
- package/src/runtime/session-resume-liveness-confirm.ts +1 -1
- package/src/runtime/ui-services.ts +7 -6
- package/src/runtime/update-check.ts +23 -43
- package/src/runtime/workspace-checkpointing.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +30 -7
- package/src/utils/clipboard.ts +1 -1
- package/src/verification/verification-ledger.ts +10 -8
- package/src/version.ts +1 -1
- package/bin/goodvibes-daemon +0 -90
- package/src/cli/config-overrides.ts +0 -267
- package/src/cli/endpoints.ts +0 -109
- package/src/cli/parser.ts +0 -395
- package/src/cli/redaction.ts +0 -170
- package/src/cli/types.ts +0 -84
- package/src/cluster/commands.ts +0 -382
- package/src/cluster/remote-daemon-target.ts +0 -225
- package/src/cluster/render.ts +0 -268
- package/src/config/atomic-write.ts +0 -70
- package/src/config/credential-availability.ts +0 -122
- package/src/config/goodvibes-home.ts +0 -149
- package/src/config/index.ts +0 -88
- package/src/config/provider-model.ts +0 -23
- package/src/config/read-versioned.ts +0 -228
- package/src/core/alert-gating.ts +0 -67
- package/src/core/consolidation-receipt.ts +0 -41
- package/src/core/focus-tracker.ts +0 -41
- package/src/core/history.ts +0 -45
- package/src/core/memory-status.ts +0 -118
- package/src/core/orchestrator.ts +0 -7
- package/src/core/pairing-devices.ts +0 -49
- package/src/core/pairing-handoff.ts +0 -101
- package/src/core/pairing-offers.ts +0 -83
- package/src/core/pairing-origin.ts +0 -78
- package/src/core/rewind-turn-anchors.ts +0 -311
- package/src/core/session-recovery.ts +0 -210
- package/src/core/stable-host.ts +0 -171
- package/src/core/transcript-journal.ts +0 -503
- package/src/daemon/cli.ts +0 -693
- package/src/daemon/fatal-boot-report.ts +0 -111
- package/src/daemon/handlers/context.ts +0 -29
- package/src/daemon/handlers/contracts.ts +0 -77
- package/src/daemon/handlers/credentials.ts +0 -139
- package/src/daemon/handlers/drafts/draft-store.ts +0 -427
- package/src/daemon/handlers/drafts/index.ts +0 -17
- package/src/daemon/handlers/drafts/register.ts +0 -331
- package/src/daemon/handlers/errors.ts +0 -18
- package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
- package/src/daemon/handlers/inbox/index.ts +0 -284
- package/src/daemon/handlers/inbox/mapping.ts +0 -192
- package/src/daemon/handlers/inbox/poller.ts +0 -212
- package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
- package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
- package/src/daemon/handlers/inbox/providers/email.ts +0 -152
- package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
- package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
- package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
- package/src/daemon/handlers/index.ts +0 -117
- package/src/daemon/handlers/register.ts +0 -168
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
- package/src/daemon/handlers/remote/backends/docker.ts +0 -79
- package/src/daemon/handlers/remote/backends/index.ts +0 -40
- package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
- package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
- package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
- package/src/daemon/handlers/remote/backends/types.ts +0 -97
- package/src/daemon/handlers/remote/dispatcher.ts +0 -181
- package/src/daemon/handlers/remote/index.ts +0 -119
- package/src/daemon/handlers/remote/peer-registry.ts +0 -357
- package/src/daemon/handlers/remote/service.ts +0 -191
- package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
- package/src/daemon/handlers/routing/index.ts +0 -261
- package/src/daemon/handlers/routing/route-store.ts +0 -319
- package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
- package/src/daemon/handlers/sqlite-store.ts +0 -278
- package/src/daemon/handlers/triage/index.ts +0 -57
- package/src/daemon/handlers/triage/integration.ts +0 -213
- package/src/daemon/handlers/triage/pipeline.ts +0 -274
- package/src/daemon/handlers/triage/scorer.ts +0 -287
- package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
- package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
- package/src/daemon/handlers/triage/tagger/index.ts +0 -184
- package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
- package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
- package/src/daemon/handlers/triage/types.ts +0 -50
- package/src/daemon/lifecycle.ts +0 -41
- package/src/daemon/provision-wake-model.ts +0 -81
- package/src/daemon/safe-serve.ts +0 -61
- package/src/daemon/send/channels.ts +0 -200
- package/src/daemon/send/command.ts +0 -332
- package/src/daemon/send/composition.ts +0 -99
- package/src/daemon/send/failure-text.ts +0 -93
- package/src/daemon/send/inert-text.ts +0 -225
- package/src/daemon/send/stdin.ts +0 -24
- package/src/daemon/service-commands.ts +0 -329
- package/src/export/cost-utils.ts +0 -319
- package/src/input/selection.ts +0 -135
- package/src/plugins/loader.ts +0 -15
- package/src/renderer/bottom-bar.ts +0 -58
- package/src/renderer/conversation-tree.ts +0 -228
- package/src/runtime/agent-graph-composition.ts +0 -78
- package/src/runtime/channel-composition.ts +0 -46
- package/src/runtime/cluster-composition.ts +0 -124
- package/src/runtime/cluster-group-composition.ts +0 -284
- package/src/runtime/code-index-services.ts +0 -167
- package/src/runtime/daemon-handler-composition.ts +0 -76
- package/src/runtime/device-posture-composition.ts +0 -110
- package/src/runtime/durability-housekeeping.ts +0 -183
- package/src/runtime/durability-services.ts +0 -155
- package/src/runtime/embedded-service-factories.ts +0 -74
- package/src/runtime/feature-settings.ts +0 -154
- package/src/runtime/fleet-needs-input-push.ts +0 -70
- package/src/runtime/fleet-services.ts +0 -41
- package/src/runtime/idle-power-services.ts +0 -85
- package/src/runtime/mail-composition.ts +0 -65
- package/src/runtime/memory-fold.ts +0 -55
- package/src/runtime/operator-token-cleanup.ts +0 -109
- package/src/runtime/path-shadow.ts +0 -418
- package/src/runtime/perf/panel-health-monitor.ts +0 -18
- package/src/runtime/provider-optimizer-wiring.ts +0 -55
- package/src/runtime/reachability-notice.ts +0 -100
- package/src/runtime/remote-execution-composition.ts +0 -62
- package/src/runtime/session-liveness-marker.ts +0 -257
- package/src/runtime/session-pointer-surface.ts +0 -22
- package/src/runtime/session-storage-services.ts +0 -41
- package/src/runtime/surface-feature-flags.ts +0 -96
- package/src/runtime/terminal-output-guard.ts +0 -235
- package/src/runtime/trust/workspace-registration.ts +0 -192
- package/src/runtime/trust/workspace-trust.ts +0 -229
- package/src/runtime/ui/index.ts +0 -39
- package/src/runtime/ui/model-picker/data-provider.ts +0 -182
- package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
- package/src/runtime/ui/model-picker/index.ts +0 -59
- package/src/runtime/ui/model-picker/types.ts +0 -153
- package/src/runtime/ui/provider-health/data-provider.ts +0 -244
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
- package/src/runtime/ui/provider-health/index.ts +0 -46
- package/src/runtime/ui/provider-health/types.ts +0 -146
- package/src/runtime/ui-events.ts +0 -1
- package/src/runtime/ui-read-model-helpers.ts +0 -1
- package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
- package/src/runtime/ui-read-models-observability-options.ts +0 -1
- package/src/runtime/ui-read-models-observability-remote.ts +0 -1
- package/src/runtime/ui-read-models-observability-security.ts +0 -1
- package/src/runtime/ui-read-models-observability-system.ts +0 -1
- package/src/runtime/ui-read-models-observability.ts +0 -1
- package/src/runtime/ui-service-queries.ts +0 -1
- package/src/runtime/voice-setup-services.ts +0 -98
- package/src/runtime/workstream-attempts-validation.ts +0 -77
- package/src/runtime/workstream-draft-edits.ts +0 -95
- package/src/runtime/workstream-draft-store.ts +0 -274
- package/src/runtime/workstream-draft-types.ts +0 -42
- package/src/runtime/workstream-services.ts +0 -373
- package/src/tools/index.ts +0 -1
- package/src/types/grid.ts +0 -48
- package/src/types/sql-js.d.ts +0 -15
- package/src/work-plans/work-plan-store.ts +0 -702
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-floors.ts — the two build floors this terminal and the daemon hold
|
|
3
|
+
* against each other.
|
|
4
|
+
*
|
|
5
|
+
* ── Forward: the daemon's floor on this client ────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* A daemon update swaps the daemon binary and nothing else. This terminal keeps
|
|
8
|
+
* running the build it started with: same rules, same bugs, still registered in
|
|
9
|
+
* the shared session store, still able to execute shared-session work. That is
|
|
10
|
+
* how a behavioral fix can land in the daemon, be verified present in the
|
|
11
|
+
* installed binaries, and still not change what the owner observes — an older
|
|
12
|
+
* terminal beside the new daemon simply keeps doing the old thing.
|
|
13
|
+
*
|
|
14
|
+
* So the daemon publishes the minimum client build it will let participate, as
|
|
15
|
+
* an `X-Goodvibes-Client-Floor` response header on `/status`. Below that floor
|
|
16
|
+
* this terminal says so plainly and stops taking shared-session work, rather
|
|
17
|
+
* than quietly executing under superseded rules until someone notices.
|
|
18
|
+
*
|
|
19
|
+
* ── Reverse: this client's floor on the daemon ────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* The exposure runs the other way too. This terminal is a pure client: it talks
|
|
22
|
+
* to a daemon it did not start, over a base URL that may point at another
|
|
23
|
+
* machine, and every capability it has is something the daemon performs on its
|
|
24
|
+
* behalf. A verb it depends on may simply not exist in the build that answers,
|
|
25
|
+
* and what the owner sees then is a 400 or a 404 on one call — which reads as a
|
|
26
|
+
* broken feature rather than as an old daemon. So this terminal declares the
|
|
27
|
+
* oldest daemon build it can work against and checks it on attach against the
|
|
28
|
+
* `version` `/status` already returns, refusing to adopt a daemon below it.
|
|
29
|
+
*
|
|
30
|
+
* ── Where these rules live ────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* Both comparisons — the header name, the verdict shapes, the version ordering
|
|
33
|
+
* — are owned by the SDK (platform/control-plane/client-compatibility.ts and
|
|
34
|
+
* daemon-compatibility.ts) and imported below rather than duplicated. What
|
|
35
|
+
* stays here is this process's own state: the forward latch, the reverse
|
|
36
|
+
* announce-once, and the daemon floor this product declares. The agent holds
|
|
37
|
+
* the same forward latch shape for its own process lifetime, which is why the
|
|
38
|
+
* latch is not a value the SDK ships.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
evaluateClientCompatibility,
|
|
43
|
+
evaluateDaemonStatusCompatibility,
|
|
44
|
+
type ClientCompatibilityVerdict,
|
|
45
|
+
type DaemonCompatibilityVerdict,
|
|
46
|
+
} from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The oldest daemon build this terminal will adopt.
|
|
50
|
+
*
|
|
51
|
+
* 1.28.0 is the daemon release in which the daemon became its own product and
|
|
52
|
+
* this terminal became a pure client of it: the session register, the inbound
|
|
53
|
+
* steer queue, the fleet needs-input push and the credential writes this build
|
|
54
|
+
* calls are all served there. An older daemon answers some of those and not
|
|
55
|
+
* others, which is the half-working state this floor exists to stop.
|
|
56
|
+
*
|
|
57
|
+
* Raise this only for a daemon behavior this terminal MUST have, and say in the
|
|
58
|
+
* release notes what it was raised for. It costs a daemon update for anyone
|
|
59
|
+
* running an older one.
|
|
60
|
+
*/
|
|
61
|
+
export const TUI_DAEMON_BUILD_FLOOR = '1.28.0';
|
|
62
|
+
|
|
63
|
+
export interface ClientBuildGuardOptions {
|
|
64
|
+
readonly clientVersion: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The live forward verdict for this process, updated by whatever reads the
|
|
69
|
+
* daemon's floor (the attach handshake) and consulted by whatever would execute
|
|
70
|
+
* shared-session work (the continuation runner).
|
|
71
|
+
*
|
|
72
|
+
* Latching is deliberate: once a daemon has told this process it is too old, a
|
|
73
|
+
* later read that fails to see the header (a restart, a truncated response)
|
|
74
|
+
* must not silently re-enable work.
|
|
75
|
+
*
|
|
76
|
+
* The notice sink is attached after construction — this guard is built in the
|
|
77
|
+
* services composition root, and the surface that renders its one line is wired
|
|
78
|
+
* in the bootstrap tail. A verdict reached before the sink attaches is held and
|
|
79
|
+
* delivered on attach, the same buffered-until-attach idiom the daemon receipt
|
|
80
|
+
* notices use, so a refusal is never the message nobody saw.
|
|
81
|
+
*/
|
|
82
|
+
export class ClientBuildGuard {
|
|
83
|
+
private verdict: ClientCompatibilityVerdict;
|
|
84
|
+
private announced = false;
|
|
85
|
+
private notify: ((verdict: ClientCompatibilityVerdict) => void) | null = null;
|
|
86
|
+
private pending: ClientCompatibilityVerdict | null = null;
|
|
87
|
+
|
|
88
|
+
constructor(private readonly options: ClientBuildGuardOptions) {
|
|
89
|
+
this.verdict = {
|
|
90
|
+
status: 'ok',
|
|
91
|
+
message: 'No daemon floor observed yet.',
|
|
92
|
+
clientVersion: options.clientVersion,
|
|
93
|
+
floor: undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Attach the surface that renders the one line telling the owner to restart.
|
|
99
|
+
* Flushes a verdict already reached before this call.
|
|
100
|
+
*/
|
|
101
|
+
onRestartRequired(notify: (verdict: ClientCompatibilityVerdict) => void): void {
|
|
102
|
+
this.notify = notify;
|
|
103
|
+
const held = this.pending;
|
|
104
|
+
this.pending = null;
|
|
105
|
+
if (held) notify(held);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Feed a floor read from a daemon `/status` response. */
|
|
109
|
+
observeFloor(floor: string | undefined): ClientCompatibilityVerdict {
|
|
110
|
+
if (this.verdict.status === 'restart-required') return this.verdict;
|
|
111
|
+
const next = evaluateClientCompatibility({ clientVersion: this.options.clientVersion, floor });
|
|
112
|
+
this.verdict = next;
|
|
113
|
+
if (next.status === 'restart-required' && !this.announced) {
|
|
114
|
+
this.announced = true;
|
|
115
|
+
if (this.notify) this.notify(next);
|
|
116
|
+
else this.pending = next;
|
|
117
|
+
}
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** False once this build has been judged too old for the live daemon. */
|
|
122
|
+
maySharedSessionWork(): boolean {
|
|
123
|
+
return this.verdict.status !== 'restart-required';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
current(): ClientCompatibilityVerdict {
|
|
127
|
+
return this.verdict;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The reverse half: this terminal's verdict on the daemon answering at a base
|
|
133
|
+
* URL, held across attaches so a reconnect loop against one old daemon states
|
|
134
|
+
* the problem once instead of on every poll.
|
|
135
|
+
*
|
|
136
|
+
* Unlatched, unlike the forward guard: a daemon CAN become new enough while
|
|
137
|
+
* this terminal runs — that is exactly what a daemon update does — and the
|
|
138
|
+
* attach that sees the newer build must adopt it. What is remembered is only
|
|
139
|
+
* which sentence has already been said.
|
|
140
|
+
*/
|
|
141
|
+
export class DaemonBuildFloor {
|
|
142
|
+
private announced: string | null = null;
|
|
143
|
+
|
|
144
|
+
constructor(private readonly floor: string = TUI_DAEMON_BUILD_FLOOR) {}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Judge a parsed `/status` body. `daemonLabel` names the peer in the sentence
|
|
148
|
+
* so an operator with two daemons on the LAN learns WHICH one is old.
|
|
149
|
+
*/
|
|
150
|
+
evaluate(statusPayload: unknown, daemonLabel: string): DaemonCompatibilityVerdict {
|
|
151
|
+
return evaluateDaemonStatusCompatibility(statusPayload, this.floor, daemonLabel);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The line to show for a verdict, or null when this exact sentence has
|
|
156
|
+
* already been shown (or there is nothing to say).
|
|
157
|
+
*/
|
|
158
|
+
noticeFor(verdict: DaemonCompatibilityVerdict): string | null {
|
|
159
|
+
if (verdict.status === 'ok') {
|
|
160
|
+
this.announced = null;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (this.announced === verdict.message) return null;
|
|
164
|
+
this.announced = verdict.message;
|
|
165
|
+
return verdict.message;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-attach-handshake.ts — attaching to a daemon is a handshake, not a
|
|
3
|
+
* pointer swap.
|
|
4
|
+
*
|
|
5
|
+
* ONE `/status` read carries the three things this terminal has to settle
|
|
6
|
+
* before it mirrors anything to a daemon, and as a pure client there is no
|
|
7
|
+
* in-process daemon handle to fold any of them from:
|
|
8
|
+
*
|
|
9
|
+
* - The DAEMON's own build. Every capability here is something the daemon
|
|
10
|
+
* performs on this terminal's behalf, so a daemon below this build's floor
|
|
11
|
+
* is refused rather than adopted — otherwise a verb it does not serve
|
|
12
|
+
* surfaces as one broken feature instead of as an old daemon, and the
|
|
13
|
+
* terminal keeps running half-working against a peer it has no reason to
|
|
14
|
+
* suspect. Refused means local-only, which is a state this app already
|
|
15
|
+
* renders honestly.
|
|
16
|
+
* - The minimum CLIENT build the daemon accepts. Below it the guard latches
|
|
17
|
+
* and the continuation runner stops taking shared-session work.
|
|
18
|
+
* - Its undelivered receipts (update applied, restarted after a crash,
|
|
19
|
+
* settings migrated), read from `/status?receipts=consume` where delivery is
|
|
20
|
+
* destructive — so the read that consumed them is the read that renders
|
|
21
|
+
* them.
|
|
22
|
+
*
|
|
23
|
+
* Every one of those is decided from the same response, which is why this is
|
|
24
|
+
* one function and not three callers each making their own read.
|
|
25
|
+
*
|
|
26
|
+
* A daemon that goes away and comes back gets the handshake again, off the
|
|
27
|
+
* liveness flip a caller already observes rather than a timer of this module's
|
|
28
|
+
* own: for an already-adopted daemon, coming back up is what its hourly
|
|
29
|
+
* self-update looks like from here, and the build that came back may not be the
|
|
30
|
+
* build that left. The flip an adoption's own first refresh causes runs the
|
|
31
|
+
* handshake once more too; that costs one /status GET and is the price of never
|
|
32
|
+
* having to decide which online flip is "really" a reconnect.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { HostServiceStatus } from '@/runtime/index.ts';
|
|
36
|
+
import { readExternalDaemonAttach } from '../daemon-attach-notices.ts';
|
|
37
|
+
import { ClientBuildGuard, DaemonBuildFloor } from './build-floors.ts';
|
|
38
|
+
|
|
39
|
+
export interface DaemonAttachHandshakeDeps {
|
|
40
|
+
/** The forward floor's latch — fed the floor this daemon announces. */
|
|
41
|
+
readonly clientBuildGuard: ClientBuildGuard;
|
|
42
|
+
/** The daemon this terminal is currently pointed at. Re-read on every attach. */
|
|
43
|
+
readonly readDaemonStatus: () => HostServiceStatus;
|
|
44
|
+
/**
|
|
45
|
+
* Record a refusal on the status every reader already consults, so the footer
|
|
46
|
+
* and `/status` report "a daemon is there and this build will not adopt it"
|
|
47
|
+
* rather than claiming a working mirror.
|
|
48
|
+
*/
|
|
49
|
+
readonly recordRefusal: (status: HostServiceStatus) => void;
|
|
50
|
+
/** Bring the session and memory spines up against this daemon (or take them down). */
|
|
51
|
+
readonly adopt: (status: HostServiceStatus, daemonToken: string) => void;
|
|
52
|
+
/** Where a daemon notice is shown to the owner. */
|
|
53
|
+
readonly notify: (text: string) => void;
|
|
54
|
+
/** This terminal's own floor on the daemon; defaults to the shipped one. */
|
|
55
|
+
readonly daemonFloor?: string | undefined;
|
|
56
|
+
/** Injectable `/status` read for tests. */
|
|
57
|
+
readonly read?: typeof readExternalDaemonAttach | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DaemonAttachHandshake {
|
|
61
|
+
/** Run the handshake against the current daemon status, then adopt or refuse. */
|
|
62
|
+
attach(daemonToken: string): Promise<void>;
|
|
63
|
+
/** Feed the adopted daemon's liveness flips; a flip to online re-handshakes. */
|
|
64
|
+
onLivenessTransition(online: boolean): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createDaemonAttachHandshake(deps: DaemonAttachHandshakeDeps): DaemonAttachHandshake {
|
|
68
|
+
const daemonBuildFloor = new DaemonBuildFloor(deps.daemonFloor);
|
|
69
|
+
const read = deps.read ?? readExternalDaemonAttach;
|
|
70
|
+
let lastDaemonToken: string | null = null;
|
|
71
|
+
// The forward floor's one line. The guard latches in the services graph,
|
|
72
|
+
// where the continuation runner reads it; this is the surface that tells the
|
|
73
|
+
// owner, and a verdict reached before now is held and delivered here.
|
|
74
|
+
deps.clientBuildGuard.onRestartRequired((verdict) => deps.notify(verdict.message));
|
|
75
|
+
|
|
76
|
+
const handshake: DaemonAttachHandshake = {
|
|
77
|
+
onLivenessTransition(online) {
|
|
78
|
+
if (online && lastDaemonToken !== null) void handshake.attach(lastDaemonToken);
|
|
79
|
+
},
|
|
80
|
+
async attach(daemonToken) {
|
|
81
|
+
lastDaemonToken = daemonToken;
|
|
82
|
+
const daemonStatus = deps.readDaemonStatus();
|
|
83
|
+
if (daemonStatus.mode !== 'external' || !daemonStatus.baseUrl) {
|
|
84
|
+
deps.adopt(daemonStatus, daemonToken);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const answer = await read({
|
|
88
|
+
baseUrl: daemonStatus.baseUrl,
|
|
89
|
+
authToken: daemonToken,
|
|
90
|
+
consumeReceipts: true,
|
|
91
|
+
});
|
|
92
|
+
// Nothing was read, so nothing is known — adopt as before and leave a
|
|
93
|
+
// daemon that is not answering to the spine's own reachability handling.
|
|
94
|
+
// Refusing on a failed read would turn one dropped request into a lost
|
|
95
|
+
// mirror.
|
|
96
|
+
if (!answer.answered) {
|
|
97
|
+
deps.adopt(daemonStatus, daemonToken);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const verdict = daemonBuildFloor.evaluate(answer.statusPayload, daemonStatus.baseUrl);
|
|
101
|
+
const notice = daemonBuildFloor.noticeFor(verdict);
|
|
102
|
+
if (notice) deps.notify(notice);
|
|
103
|
+
if (verdict.status === 'daemon-update-required') {
|
|
104
|
+
const refused: HostServiceStatus = { ...daemonStatus, mode: 'incompatible', reason: verdict.message };
|
|
105
|
+
deps.recordRefusal(refused);
|
|
106
|
+
deps.adopt(refused, daemonToken);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
deps.clientBuildGuard.observeFloor(answer.clientFloor);
|
|
110
|
+
deps.adopt(daemonStatus, daemonToken);
|
|
111
|
+
for (const receipt of answer.notices) deps.notify(receipt);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
return handshake;
|
|
115
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-union.ts — the Fleet panel shows everything running, not just what this
|
|
3
|
+
* terminal started.
|
|
4
|
+
*
|
|
5
|
+
* ── What moved to the SDK, and what stays here ────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The SDK's `@pellux/goodvibes-sdk/platform/runtime/client` now owns the
|
|
8
|
+
* policy every surface needs for this: `createDaemonFleetRowsPoller` (the poll
|
|
9
|
+
* cadence and last-known-rows retention when a poll fails), `mergeFleetNodes`
|
|
10
|
+
* (the local-wins dedupe by node id) and `daemonOnlyFleetActRefusal` (the
|
|
11
|
+
* reason string a steer/archive against a daemon-only row gets, rather than a
|
|
12
|
+
* bare false). This module is now a thin wrapper over that policy.
|
|
13
|
+
*
|
|
14
|
+
* What stays here is the panel binding: the TUI's own `FleetReadModel`
|
|
15
|
+
* interface (interrupt/resume/kill/steer/archive, subscribe, snapshots) and
|
|
16
|
+
* `buildFleetSnapshot`, the tree-builder + honest cost/token aggregator this
|
|
17
|
+
* repo's Fleet panel renders from. Rebuilding through the SAME builder the
|
|
18
|
+
* local view uses — rather than summing two halves — is what keeps the
|
|
19
|
+
* rollups, the cost/token totals and the blocked-on-user ordering computed
|
|
20
|
+
* once, over the whole fleet.
|
|
21
|
+
*
|
|
22
|
+
* ── Who wins ──────────────────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* Local rows are AUTHORITATIVE for processes this terminal spawned. They are
|
|
25
|
+
* live — the registry pushes on every state change, with sub-second latency —
|
|
26
|
+
* and they carry the capabilities that make a row actionable here (interrupt,
|
|
27
|
+
* resume, kill, steer all reach a real child process). The daemon's copy of
|
|
28
|
+
* the same row, arriving over a poll, is necessarily staler; where both
|
|
29
|
+
* describe the same node id, the local one is kept (`mergeFleetNodes`).
|
|
30
|
+
*
|
|
31
|
+
* ── Acting on a row you do not own ────────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* `interrupt`/`resume`/`kill`/`steer` reach this process's own children. A
|
|
34
|
+
* daemon row has no child here to signal, so those refuse — and `steer`,
|
|
35
|
+
* which has a reason channel, says why rather than returning a bare false
|
|
36
|
+
* that reads as "the agent ignored you". The panel's own act surface
|
|
37
|
+
* (fleet-gateway.ts) already drives the daemon's verbs for the acts the
|
|
38
|
+
* daemon serves.
|
|
39
|
+
*/
|
|
40
|
+
import {
|
|
41
|
+
createDaemonFleetRowsPoller,
|
|
42
|
+
daemonOnlyFleetActRefusal,
|
|
43
|
+
DEFAULT_FLEET_REFRESH_MS,
|
|
44
|
+
mergeFleetNodes,
|
|
45
|
+
readDaemonFleetRows,
|
|
46
|
+
type DaemonFleetRows,
|
|
47
|
+
type DaemonFleetRowsPoller,
|
|
48
|
+
type DaemonFleetRowsPollerOptions,
|
|
49
|
+
type DaemonVerbCaller,
|
|
50
|
+
} from '@pellux/goodvibes-sdk/platform/runtime/client';
|
|
51
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
52
|
+
import { buildFleetSnapshot, type FleetReadModel, type FleetSnapshot } from '../../panels/fleet-read-model.ts';
|
|
53
|
+
|
|
54
|
+
/** Re-exported so an importer that only needs the SDK's policy has one import site. */
|
|
55
|
+
export { DEFAULT_FLEET_REFRESH_MS, daemonOnlyFleetActRefusal, mergeFleetNodes, readDaemonFleetRows };
|
|
56
|
+
export type { DaemonFleetRows, DaemonFleetRowsPoller, DaemonFleetRowsPollerOptions };
|
|
57
|
+
|
|
58
|
+
export interface FleetUnionOptions {
|
|
59
|
+
/** This surface's own live registry view — authoritative for what it spawned. */
|
|
60
|
+
readonly local: FleetReadModel;
|
|
61
|
+
readonly verbs: DaemonVerbCaller;
|
|
62
|
+
readonly refreshIntervalMs?: number;
|
|
63
|
+
readonly log?: Pick<typeof logger, 'debug'>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FleetUnionReadModel extends FleetReadModel {
|
|
67
|
+
/** Stop the refresh timer. Idempotent. */
|
|
68
|
+
stop(): void;
|
|
69
|
+
/** Re-read the daemon's rows now. Never throws. */
|
|
70
|
+
refresh(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** How this terminal names itself in a daemon-only-row act refusal. */
|
|
74
|
+
const SURFACE_LABEL = 'this terminal';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wrap a local fleet read model so its snapshot also carries the adopted
|
|
78
|
+
* daemon's rows, over the SDK's poll/merge/refusal policy.
|
|
79
|
+
*
|
|
80
|
+
* Inert until the first poll lands: before then, and whenever the daemon
|
|
81
|
+
* cannot answer, the snapshot is exactly the local one — which is the honest
|
|
82
|
+
* answer, not a degraded one. A daemon that stops answering keeps its LAST
|
|
83
|
+
* known rows rather than dropping them (the poller's own retention), so a
|
|
84
|
+
* momentary blip does not make half the fleet blink out and back.
|
|
85
|
+
*/
|
|
86
|
+
export function createFleetUnionReadModel(options: FleetUnionOptions): FleetUnionReadModel {
|
|
87
|
+
const local = options.local;
|
|
88
|
+
const poller = createDaemonFleetRowsPoller({
|
|
89
|
+
verbs: options.verbs,
|
|
90
|
+
...(options.refreshIntervalMs === undefined ? {} : { refreshIntervalMs: options.refreshIntervalMs }),
|
|
91
|
+
...(options.log === undefined ? {} : { log: options.log }),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const listeners = new Set<() => void>();
|
|
95
|
+
const unsubscribePoller = poller.subscribe(() => {
|
|
96
|
+
for (const listener of listeners) listener();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** Ids this surface owns right now — the set the daemon's copy defers to. */
|
|
100
|
+
const localNodeIds = (snapshot: FleetSnapshot): Set<string> =>
|
|
101
|
+
new Set(snapshot.rows.map((row) => row.node.id));
|
|
102
|
+
const isLocalId = (id: string): boolean => localNodeIds(local.getSnapshot()).has(id);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
getSnapshot(): FleetSnapshot {
|
|
106
|
+
const localSnapshot = local.getSnapshot();
|
|
107
|
+
const daemonRows = poller.rows();
|
|
108
|
+
if (!daemonRows || daemonRows.nodes.length === 0) return localSnapshot;
|
|
109
|
+
const merged = mergeFleetNodes(localSnapshot.rows.map((row) => row.node), daemonRows.nodes);
|
|
110
|
+
// Rebuilt through the SAME builder the local view uses, so the rollups,
|
|
111
|
+
// the cost/token totals and the blocked-on-user ordering are computed
|
|
112
|
+
// once, over the whole fleet, rather than summed from two halves.
|
|
113
|
+
return buildFleetSnapshot(merged, Math.max(localSnapshot.capturedAt, daemonRows.capturedAt));
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
subscribe(listener: () => void): () => void {
|
|
117
|
+
listeners.add(listener);
|
|
118
|
+
const unsubscribeLocal = local.subscribe(listener);
|
|
119
|
+
return () => {
|
|
120
|
+
listeners.delete(listener);
|
|
121
|
+
unsubscribeLocal();
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
interrupt: (id) => local.interrupt(id),
|
|
126
|
+
resume: (id) => local.resume(id),
|
|
127
|
+
kill: (id, opts) => local.kill(id, opts),
|
|
128
|
+
steer: (id, text) => (isLocalId(id)
|
|
129
|
+
? local.steer(id, text)
|
|
130
|
+
: { queued: false, reason: daemonOnlyFleetActRefusal(id, SURFACE_LABEL) }),
|
|
131
|
+
subscribeConsumed: (listener) => local.subscribeConsumed(listener),
|
|
132
|
+
// The archive is this surface's own view state; the daemon keeps its own.
|
|
133
|
+
getArchivedSnapshot: () => local.getArchivedSnapshot(),
|
|
134
|
+
archive: (id) => (isLocalId(id)
|
|
135
|
+
? local.archive(id)
|
|
136
|
+
: { archived: false, count: 0, reason: daemonOnlyFleetActRefusal(id, SURFACE_LABEL) }),
|
|
137
|
+
archiveFinished: () => local.archiveFinished(),
|
|
138
|
+
unarchive: (id) => local.unarchive(id),
|
|
139
|
+
|
|
140
|
+
refresh: () => poller.refresh(),
|
|
141
|
+
stop(): void {
|
|
142
|
+
poller.stop();
|
|
143
|
+
unsubscribePoller();
|
|
144
|
+
listeners.clear();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host-service-status.ts — reading a background service's posture.
|
|
3
|
+
*
|
|
4
|
+
* Three questions the composition root asks about the daemon and the webhook
|
|
5
|
+
* listener, none of which need anything from the composition itself: what URL
|
|
6
|
+
* a configured host/port is reachable at, what to report before the discovery
|
|
7
|
+
* probe has finished, and whether a given verdict means "usable" or "the port
|
|
8
|
+
* is held and this terminal cannot have it".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { HostServiceStatus } from '@/runtime/index.ts';
|
|
12
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
|
+
|
|
14
|
+
/** Which of the two background services a question is about. */
|
|
15
|
+
export type HostServiceName = 'daemon' | 'httpListener';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The URL a configured host/port is actually reachable at. A wildcard bind is
|
|
19
|
+
* not an address anything can connect to, so `0.0.0.0` and `::` resolve to the
|
|
20
|
+
* loopback address of their family; a bare IPv6 literal is bracketed.
|
|
21
|
+
*/
|
|
22
|
+
export function formatHostServiceBaseUrl(host: string, port: number): string {
|
|
23
|
+
const normalized = host.trim().toLowerCase();
|
|
24
|
+
const probeHost = normalized === '0.0.0.0'
|
|
25
|
+
? '127.0.0.1'
|
|
26
|
+
: normalized === '::' || normalized === '[::]'
|
|
27
|
+
? '::1'
|
|
28
|
+
: host;
|
|
29
|
+
const urlHost = probeHost.includes(':') && !probeHost.startsWith('[') ? `[${probeHost}]` : probeHost;
|
|
30
|
+
return `http://${urlHost}:${port}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What to report about a service before the discovery probe has answered:
|
|
35
|
+
* where it WOULD be, and the honest reason nothing is known about it yet.
|
|
36
|
+
*/
|
|
37
|
+
export function createPendingServiceStatus(
|
|
38
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
39
|
+
service: HostServiceName,
|
|
40
|
+
): HostServiceStatus {
|
|
41
|
+
const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
|
|
42
|
+
const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
|
|
43
|
+
return {
|
|
44
|
+
mode: 'unavailable',
|
|
45
|
+
host,
|
|
46
|
+
port,
|
|
47
|
+
baseUrl: formatHostServiceBaseUrl(host, port),
|
|
48
|
+
reason: 'Background service startup has not completed yet',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A service this terminal can actually use. Called for both the daemon and the
|
|
54
|
+
* HTTP listener (see bootstrap.ts), so the 'embedded' arm stays live: the SDK
|
|
55
|
+
* always reports its in-process HTTP listener as 'embedded', even though this
|
|
56
|
+
* product's daemon status can never carry that mode (adoptOnly is hardcoded on
|
|
57
|
+
* every host-services call this app makes).
|
|
58
|
+
*/
|
|
59
|
+
export function hostServiceIsActive(status: HostServiceStatus): boolean {
|
|
60
|
+
return status.mode === 'embedded' || status.mode === 'external';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The configured port is held and unusable by this terminal — either by an
|
|
65
|
+
* unverified process ('blocked') or by a GoodVibes daemon this build refused to
|
|
66
|
+
* adopt ('incompatible': a wire-version mismatch, or a daemon below this
|
|
67
|
+
* build's own floor).
|
|
68
|
+
*/
|
|
69
|
+
export function hostServiceIsBlocked(status: HostServiceStatus): boolean {
|
|
70
|
+
return status.mode === 'blocked' || status.mode === 'incompatible';
|
|
71
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosted-exit.ts — leaving a hosted session because the terminal is closing.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists at all ────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The detach policy is applied when the LAST attached client detaches. A
|
|
7
|
+
* terminal that simply exits never detaches: the daemon keeps this window in
|
|
8
|
+
* `attachedClients` forever, so a `kill`-policy session — the shipped default,
|
|
9
|
+
* and the behavior closing a client has always had — would quietly NOT be
|
|
10
|
+
* killed, and would sit there attached to a process that no longer exists.
|
|
11
|
+
*
|
|
12
|
+
* That is not a cosmetic gap. The owner's ruling is that the capability lands
|
|
13
|
+
* while the familiar default is preserved: quitting ends the work unless the
|
|
14
|
+
* user asked for `survive`. Preserving it requires the quit to actually say so.
|
|
15
|
+
*
|
|
16
|
+
* ── Bounded, and never able to hold the exit open ─────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* The app's shutdown races a hard timeout; a detach is one small round trip to
|
|
19
|
+
* a daemon on loopback, but a daemon that has stopped answering must not add a
|
|
20
|
+
* hang to an exit that is already restoring the user's terminal. So this is
|
|
21
|
+
* bounded on its own and every failure is swallowed after being logged: the
|
|
22
|
+
* worst case is the session keeping a stale attachment, which the daemon's own
|
|
23
|
+
* housekeeping is what resolves, not this process.
|
|
24
|
+
*/
|
|
25
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
27
|
+
import { createDaemonVerbCaller } from './operator-endpoint.ts';
|
|
28
|
+
import { createHostedSessionsClient } from './hosted-sessions.ts';
|
|
29
|
+
import { getSharedHostedSessionFeed, type HostedSessionFeed } from '../../panels/hosted-session-feed.ts';
|
|
30
|
+
|
|
31
|
+
/** How long the exit will wait for the detach before giving up on it. */
|
|
32
|
+
export const HOSTED_DETACH_ON_EXIT_TIMEOUT_MS = 1_500;
|
|
33
|
+
|
|
34
|
+
export interface LeaveHostedSessionOnExitOptions {
|
|
35
|
+
readonly configManager: ConfigManager;
|
|
36
|
+
readonly homeDirectory: string | (() => string);
|
|
37
|
+
/** Defaults to the shared feed the `/hosted` command and the panel use. */
|
|
38
|
+
readonly feed?: HostedSessionFeed | undefined;
|
|
39
|
+
readonly timeoutMs?: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detach from whatever hosted session this terminal is attached to, so the
|
|
44
|
+
* daemon applies the policy the record has been advertising all along.
|
|
45
|
+
*
|
|
46
|
+
* Resolves to what happened, for a caller that wants to say so: `'none'` when
|
|
47
|
+
* nothing was attached, `'detached'` when the daemon answered, `'failed'` when
|
|
48
|
+
* it did not. Never throws and never runs past its own budget.
|
|
49
|
+
*/
|
|
50
|
+
export async function leaveHostedSessionOnExit(
|
|
51
|
+
options: LeaveHostedSessionOnExitOptions,
|
|
52
|
+
): Promise<'none' | 'detached' | 'failed'> {
|
|
53
|
+
const feed = options.feed ?? getSharedHostedSessionFeed();
|
|
54
|
+
const record = feed.getState().record;
|
|
55
|
+
// Closing the socket is this process's own business and is done either way —
|
|
56
|
+
// an already-terminated session has nothing to detach from, but the stream
|
|
57
|
+
// watching it is still open.
|
|
58
|
+
feed.closeStream();
|
|
59
|
+
if (!record || record.status === 'terminated') return 'none';
|
|
60
|
+
|
|
61
|
+
const client = createHostedSessionsClient(createDaemonVerbCaller({
|
|
62
|
+
configManager: options.configManager,
|
|
63
|
+
homeDirectory: options.homeDirectory,
|
|
64
|
+
}));
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? HOSTED_DETACH_ON_EXIT_TIMEOUT_MS;
|
|
66
|
+
try {
|
|
67
|
+
const outcome = await Promise.race([
|
|
68
|
+
client.detach(record.id).then(() => 'detached' as const),
|
|
69
|
+
new Promise<'failed'>((resolve) => {
|
|
70
|
+
const timer = setTimeout(() => resolve('failed'), timeoutMs);
|
|
71
|
+
timer.unref?.();
|
|
72
|
+
}),
|
|
73
|
+
]);
|
|
74
|
+
if (outcome === 'failed') {
|
|
75
|
+
logger.debug('[hosted-sessions] the detach-on-exit did not complete in time', {
|
|
76
|
+
sessionId: record.id, timeoutMs,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return outcome;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.debug('[hosted-sessions] the detach-on-exit was refused', {
|
|
82
|
+
sessionId: record.id, error: summarizeError(error),
|
|
83
|
+
});
|
|
84
|
+
return 'failed';
|
|
85
|
+
}
|
|
86
|
+
}
|