@vellumai/assistant 0.12.1 → 0.12.2-staging.2
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +360 -51
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +250 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/cu-unified-flow.test.ts +14 -4
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/heartbeat-service.test.ts +56 -40
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-cu-proxy.test.ts +596 -0
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/oauth-commands-routes.test.ts +168 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/service-account-token.test.ts +247 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/unicode.test.ts +13 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/agent/loop.ts +1 -0
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/events/notification-intent.ts +2 -0
- package/src/api/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/avatar/identity-avatar.ts +7 -2
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +155 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +92 -72
- package/src/calls/voice-triage-escalate.ts +178 -59
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/bash.test.ts +53 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
- package/src/config/bundled-skills/schedule/SKILL.md +3 -4
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +33 -13
- package/src/config/loader.ts +2 -0
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/credential-health/credential-health-service.ts +11 -17
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +20 -7
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +21 -10
- package/src/daemon/conversation-tool-setup.ts +45 -59
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +224 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/orphan-reaper.ts +1 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/reaction-history-render.ts +2 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- package/src/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/protocol.test.ts +39 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/__tests__/session-controls.test.ts +79 -0
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +297 -699
- package/src/live-voice/protocol.ts +60 -2
- package/src/live-voice/session-controls.ts +111 -0
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +100 -0
- package/src/oauth/connection-resolver.ts +120 -3
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +314 -27
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +4 -2
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/host-observe.test.ts +40 -1
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +90 -2
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +20 -14
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- package/src/runtime/routes/index.ts +4 -2
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +6 -20
- package/src/runtime/routes/oauth-commands-routes.ts +50 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/security/untrusted-content.ts +3 -1
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/util/unicode.ts +15 -1
- package/src/watch/watch-retro.ts +6 -8
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand assistant desktop: Xtigervnc (VNC on loopback only, so the
|
|
3
|
+
* authenticated `/v1/desktop/stream` upgrade is the sole way in), openbox, the
|
|
4
|
+
* xcompmgr compositor, the Plank dock, the tigervncconfig clipboard bridge and
|
|
5
|
+
* Google Chrome, started by the first viewer and lingering after the
|
|
6
|
+
* last one leaves so a reconnect is instant.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { readAvatarState } from "../avatar/avatar-manifest.js";
|
|
13
|
+
import { resolveNotificationAccentHex } from "../avatar/notification-avatar.js";
|
|
14
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
15
|
+
import { terminateProcessTree } from "../util/host-process.js";
|
|
16
|
+
import { getLogger } from "../util/logger.js";
|
|
17
|
+
import { getDataDir } from "../util/platform.js";
|
|
18
|
+
import { sleep } from "../util/retry.js";
|
|
19
|
+
import { writeDesktopChromePolicy } from "./desktop-chrome-policy.js";
|
|
20
|
+
import { shouldRestoreDesktopChromeSession } from "./desktop-chrome-session.js";
|
|
21
|
+
import {
|
|
22
|
+
desktopChromePath,
|
|
23
|
+
resolveDesktopBinaries,
|
|
24
|
+
} from "./desktop-dependencies.js";
|
|
25
|
+
import { writeDesktopPanelConfig } from "./desktop-panel-config.js";
|
|
26
|
+
import { renderCurrentDesktopWallpaper } from "./desktop-wallpaper.js";
|
|
27
|
+
import { DESKTOP_WINDOW_DRAG_SCRIPT } from "./desktop-window-drag.js";
|
|
28
|
+
import { writeDesktopWindowManagerConfig } from "./desktop-window-manager-config.js";
|
|
29
|
+
import { writeDesktopWindowTheme } from "./desktop-window-theme.js";
|
|
30
|
+
|
|
31
|
+
const log = getLogger("desktop-session");
|
|
32
|
+
|
|
33
|
+
const DESKTOP_DISPLAY = ":99";
|
|
34
|
+
export const DESKTOP_VNC_PORT = 5999;
|
|
35
|
+
const DESKTOP_WIDTH = 1600;
|
|
36
|
+
const DESKTOP_HEIGHT = 900;
|
|
37
|
+
const DESKTOP_GEOMETRY = `${DESKTOP_WIDTH}x${DESKTOP_HEIGHT}`;
|
|
38
|
+
const DESKTOP_LINGER_MS = 5 * 60_000;
|
|
39
|
+
const VNC_READY_DEADLINE_MS = 10_000;
|
|
40
|
+
const VNC_PROBE_INTERVAL_MS = 100;
|
|
41
|
+
const KILL_GRACE_MS = 2_000;
|
|
42
|
+
const PANEL_RESTART_LIMIT = 3;
|
|
43
|
+
const PANEL_RESTART_DELAY_MS = 1_000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the desktop children see. Deliberately not `buildSanitizedEnv()`: its
|
|
47
|
+
* kata chroot PATH and LD_LIBRARY_PATH overlay would give this Chrome
|
|
48
|
+
* different libraries than the browser tool's, which launches with the plain
|
|
49
|
+
* process env.
|
|
50
|
+
*/
|
|
51
|
+
const DESKTOP_ENV_KEYS = [
|
|
52
|
+
"PATH",
|
|
53
|
+
"HOME",
|
|
54
|
+
"LANG",
|
|
55
|
+
"LC_ALL",
|
|
56
|
+
"XDG_RUNTIME_DIR",
|
|
57
|
+
"TMPDIR",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Close codes for `/v1/desktop/stream`. Application-range values cannot
|
|
62
|
+
* collide with velay's own 1013 or be remapped by the gateway's velay bridge.
|
|
63
|
+
*/
|
|
64
|
+
export const DESKTOP_CLOSE = {
|
|
65
|
+
goingAway: 1001,
|
|
66
|
+
unavailable: 4008,
|
|
67
|
+
failed: 4011,
|
|
68
|
+
busy: 4013,
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
export type DesktopCloseCode =
|
|
72
|
+
(typeof DESKTOP_CLOSE)[keyof typeof DESKTOP_CLOSE];
|
|
73
|
+
|
|
74
|
+
const SHUTTING_DOWN_LOSS: DesktopLoss = {
|
|
75
|
+
code: DESKTOP_CLOSE.goingAway,
|
|
76
|
+
reason: "The assistant is shutting down",
|
|
77
|
+
};
|
|
78
|
+
export const START_FAILED_LOSS: DesktopLoss = {
|
|
79
|
+
code: DESKTOP_CLOSE.failed,
|
|
80
|
+
reason: "Desktop failed to start",
|
|
81
|
+
};
|
|
82
|
+
const BUSY_LOSS: DesktopLoss = {
|
|
83
|
+
code: DESKTOP_CLOSE.busy,
|
|
84
|
+
reason: "Desktop is in use by another viewer",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type DesktopChildRole =
|
|
88
|
+
| "x-server"
|
|
89
|
+
| "window-manager"
|
|
90
|
+
| "compositor"
|
|
91
|
+
| "panel"
|
|
92
|
+
| "wallpaper"
|
|
93
|
+
| "clipboard"
|
|
94
|
+
| "browser";
|
|
95
|
+
|
|
96
|
+
/** Optional desktop decoration processes. */
|
|
97
|
+
const COSMETIC_ROLES: ReadonlySet<DesktopChildRole> = new Set([
|
|
98
|
+
"compositor",
|
|
99
|
+
"wallpaper",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/** The slice of `Bun.Subprocess` the manager drives, so tests can fake it. */
|
|
103
|
+
export interface DesktopChild {
|
|
104
|
+
readonly pid: number;
|
|
105
|
+
readonly exited: Promise<number>;
|
|
106
|
+
kill(signal?: NodeJS.Signals | number): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DesktopSpawnRequest {
|
|
110
|
+
readonly cmd: readonly string[];
|
|
111
|
+
readonly env: Record<string, string>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Why a viewer cannot have the desktop, as the close code it relays. */
|
|
115
|
+
export interface DesktopLoss {
|
|
116
|
+
readonly code: DesktopCloseCode;
|
|
117
|
+
readonly reason: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Callback surface of the socket holding the viewer slot. */
|
|
121
|
+
export interface DesktopViewer {
|
|
122
|
+
onDesktopLost(loss: DesktopLoss): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A start that failed, carrying the loss for a viewer `onDesktopLost` missed. */
|
|
126
|
+
export class DesktopStartError extends Error {
|
|
127
|
+
constructor(
|
|
128
|
+
readonly loss: DesktopLoss,
|
|
129
|
+
cause: unknown,
|
|
130
|
+
) {
|
|
131
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
132
|
+
this.name = "DesktopStartError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DesktopTcpSocket {
|
|
137
|
+
/** Returns the bytes accepted; fewer than offered means backpressure. */
|
|
138
|
+
write(data: Uint8Array): number;
|
|
139
|
+
end(): void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface DesktopTcpHandlers {
|
|
143
|
+
onData(data: Uint8Array): void;
|
|
144
|
+
onDrain(): void;
|
|
145
|
+
onClose(): void;
|
|
146
|
+
onError(err: Error): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type DesktopSignal = "SIGTERM" | "SIGKILL";
|
|
150
|
+
|
|
151
|
+
type ViewerSlotResult =
|
|
152
|
+
| { readonly ok: true }
|
|
153
|
+
| { readonly ok: false; readonly loss: DesktopLoss };
|
|
154
|
+
|
|
155
|
+
interface DesktopSessionManagerOptions {
|
|
156
|
+
readonly spawn?: (
|
|
157
|
+
role: DesktopChildRole,
|
|
158
|
+
request: DesktopSpawnRequest,
|
|
159
|
+
) => DesktopChild;
|
|
160
|
+
/** PATH lookup for the desktop binaries; `null` when one is missing. */
|
|
161
|
+
readonly which?: (binary: string) => string | null;
|
|
162
|
+
/** Whether the VNC server accepts connections on `port`. */
|
|
163
|
+
readonly probeVncPort?: (port: number) => Promise<boolean>;
|
|
164
|
+
/** Path of the installed Google Chrome binary. */
|
|
165
|
+
readonly resolveChromePath?: () => Promise<string>;
|
|
166
|
+
readonly killProcessGroup?: (
|
|
167
|
+
child: DesktopChild,
|
|
168
|
+
signal: DesktopSignal,
|
|
169
|
+
) => void;
|
|
170
|
+
readonly lingerMs?: number;
|
|
171
|
+
readonly readyDeadlineMs?: number;
|
|
172
|
+
readonly killGraceMs?: number;
|
|
173
|
+
readonly profileDir?: string;
|
|
174
|
+
/** Directory for dock config, launchers, icons and wallpaper. */
|
|
175
|
+
readonly panelConfigDir?: string;
|
|
176
|
+
readonly renderWallpaper?: (
|
|
177
|
+
width: number,
|
|
178
|
+
height: number,
|
|
179
|
+
) => Promise<Buffer | null>;
|
|
180
|
+
/** Where the children's allowlisted env is read from. */
|
|
181
|
+
readonly sourceEnv?: NodeJS.ProcessEnv;
|
|
182
|
+
readonly panelRestartDelayMs?: number;
|
|
183
|
+
readonly writeWindowManagerConfig?: (configDir: string) => string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type DesktopBinaries = ReturnType<typeof resolveDesktopBinaries>;
|
|
187
|
+
|
|
188
|
+
export class DesktopSessionManager {
|
|
189
|
+
private readonly children = new Map<DesktopChildRole, DesktopChild>();
|
|
190
|
+
private starting: Promise<void> | null = null;
|
|
191
|
+
/** The kill of the last tree, which a new start waits out. */
|
|
192
|
+
private tearingDown: Promise<void> | null = null;
|
|
193
|
+
private running = false;
|
|
194
|
+
/** Bumped on every teardown so an in-flight start notices it lost its tree. */
|
|
195
|
+
private generation = 0;
|
|
196
|
+
private viewer: DesktopViewer | null = null;
|
|
197
|
+
private lingerTimer: ReturnType<typeof setTimeout> | null = null;
|
|
198
|
+
private ingressClosed = false;
|
|
199
|
+
/** Resolved for the current tree, and read again when the dock comes up. */
|
|
200
|
+
private binaries: DesktopBinaries | null = null;
|
|
201
|
+
private panelStarted = false;
|
|
202
|
+
private panelRestartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
203
|
+
private panelRestartAttempts = 0;
|
|
204
|
+
private panelLaunch: {
|
|
205
|
+
chromiumPath: string;
|
|
206
|
+
env: Record<string, string>;
|
|
207
|
+
} | null = null;
|
|
208
|
+
private readonly retiredPanels = new Set<DesktopChild>();
|
|
209
|
+
private wallpaperStarting: {
|
|
210
|
+
generation: number;
|
|
211
|
+
refreshQueued: boolean;
|
|
212
|
+
} | null = null;
|
|
213
|
+
|
|
214
|
+
private readonly spawn: NonNullable<DesktopSessionManagerOptions["spawn"]>;
|
|
215
|
+
private readonly which: NonNullable<DesktopSessionManagerOptions["which"]>;
|
|
216
|
+
private readonly probeVncPort: NonNullable<
|
|
217
|
+
DesktopSessionManagerOptions["probeVncPort"]
|
|
218
|
+
>;
|
|
219
|
+
private readonly resolveChromePath: NonNullable<
|
|
220
|
+
DesktopSessionManagerOptions["resolveChromePath"]
|
|
221
|
+
>;
|
|
222
|
+
private readonly killProcessGroup: NonNullable<
|
|
223
|
+
DesktopSessionManagerOptions["killProcessGroup"]
|
|
224
|
+
>;
|
|
225
|
+
private readonly lingerMs: number;
|
|
226
|
+
private readonly readyDeadlineMs: number;
|
|
227
|
+
private readonly killGraceMs: number;
|
|
228
|
+
private readonly profileDir: string;
|
|
229
|
+
private readonly panelConfigDir: string;
|
|
230
|
+
private readonly renderWallpaper: NonNullable<
|
|
231
|
+
DesktopSessionManagerOptions["renderWallpaper"]
|
|
232
|
+
>;
|
|
233
|
+
private readonly sourceEnv: NodeJS.ProcessEnv;
|
|
234
|
+
private readonly panelRestartDelayMs: number;
|
|
235
|
+
private readonly writeWindowManagerConfig: (configDir: string) => string;
|
|
236
|
+
|
|
237
|
+
constructor(options: DesktopSessionManagerOptions = {}) {
|
|
238
|
+
this.spawn = options.spawn ?? spawnDetached;
|
|
239
|
+
this.which = options.which ?? Bun.which;
|
|
240
|
+
this.probeVncPort = options.probeVncPort ?? probeLoopbackPort;
|
|
241
|
+
this.resolveChromePath =
|
|
242
|
+
options.resolveChromePath ??
|
|
243
|
+
(async () => {
|
|
244
|
+
if (process.platform === "linux" && getIsContainerized()) {
|
|
245
|
+
// Google Chrome reads policies from /etc even when its deb is extracted.
|
|
246
|
+
try {
|
|
247
|
+
writeDesktopChromePolicy("/etc/opt/chrome/policies/managed");
|
|
248
|
+
} catch (err) {
|
|
249
|
+
log.warn({ err }, "Desktop Chrome policy could not be applied");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return desktopChromePath();
|
|
253
|
+
});
|
|
254
|
+
this.killProcessGroup = options.killProcessGroup ?? killProcessGroup;
|
|
255
|
+
this.lingerMs = options.lingerMs ?? DESKTOP_LINGER_MS;
|
|
256
|
+
this.readyDeadlineMs = options.readyDeadlineMs ?? VNC_READY_DEADLINE_MS;
|
|
257
|
+
this.killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
|
|
258
|
+
this.profileDir =
|
|
259
|
+
options.profileDir ?? join(getDataDir(), "desktop-profile");
|
|
260
|
+
this.panelConfigDir =
|
|
261
|
+
options.panelConfigDir ?? join(getDataDir(), "desktop-panel");
|
|
262
|
+
this.sourceEnv = options.sourceEnv ?? process.env;
|
|
263
|
+
this.panelRestartDelayMs =
|
|
264
|
+
options.panelRestartDelayMs ?? PANEL_RESTART_DELAY_MS;
|
|
265
|
+
this.writeWindowManagerConfig =
|
|
266
|
+
options.writeWindowManagerConfig ??
|
|
267
|
+
((configDir) => {
|
|
268
|
+
let sourcePath: string | undefined;
|
|
269
|
+
try {
|
|
270
|
+
sourcePath = writeDesktopWindowTheme(
|
|
271
|
+
configDir,
|
|
272
|
+
this.sourceEnv.HOME,
|
|
273
|
+
resolveNotificationAccentHex(readAvatarState()),
|
|
274
|
+
);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log.warn({ err }, "Desktop window theme could not be applied");
|
|
277
|
+
}
|
|
278
|
+
return writeDesktopWindowManagerConfig(
|
|
279
|
+
configDir,
|
|
280
|
+
this.sourceEnv.HOME,
|
|
281
|
+
sourcePath,
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
this.renderWallpaper =
|
|
285
|
+
options.renderWallpaper ?? renderCurrentDesktopWallpaper;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Viewer slot ────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
acquireViewerSlot(viewer: DesktopViewer): ViewerSlotResult {
|
|
291
|
+
if (this.ingressClosed) {
|
|
292
|
+
return { ok: false, loss: SHUTTING_DOWN_LOSS };
|
|
293
|
+
}
|
|
294
|
+
if (this.viewer) {
|
|
295
|
+
return { ok: false, loss: BUSY_LOSS };
|
|
296
|
+
}
|
|
297
|
+
this.viewer = viewer;
|
|
298
|
+
this.clearLinger();
|
|
299
|
+
return { ok: true };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Give the slot back. Only the holder can release it, so a socket that was
|
|
304
|
+
* turned away as busy never disturbs the viewer that turned it away.
|
|
305
|
+
*/
|
|
306
|
+
releaseViewerSlot(viewer: DesktopViewer): void {
|
|
307
|
+
if (this.viewer !== viewer) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.viewer = null;
|
|
311
|
+
if (this.running || this.starting) {
|
|
312
|
+
this.armLinger();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Process tree ───────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Start the desktop if needed. Concurrent callers share one start; it
|
|
320
|
+
* rejects with a `DesktopStartError`.
|
|
321
|
+
*/
|
|
322
|
+
ensureDesktopRunning(): Promise<void> {
|
|
323
|
+
if (this.ingressClosed) {
|
|
324
|
+
return Promise.reject(ingressClosedError());
|
|
325
|
+
}
|
|
326
|
+
if (this.running) {
|
|
327
|
+
void this.refreshWallpaper(this.childEnv(), this.generation);
|
|
328
|
+
return Promise.resolve();
|
|
329
|
+
}
|
|
330
|
+
this.starting ??= this.startDesktop().finally(() => {
|
|
331
|
+
this.starting = null;
|
|
332
|
+
});
|
|
333
|
+
return this.starting;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Refuse new viewers and kill the process tree. Resolves once every child
|
|
338
|
+
* has exited or been hard-killed after the grace period. Safe to repeat.
|
|
339
|
+
*/
|
|
340
|
+
destroy(): Promise<void> {
|
|
341
|
+
this.ingressClosed = true;
|
|
342
|
+
return this.teardown(SHUTTING_DOWN_LOSS);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async startDesktop(): Promise<void> {
|
|
346
|
+
// The last tree may still be in its kill grace; its X server holds the
|
|
347
|
+
// display and port until it is gone.
|
|
348
|
+
await this.tearingDown;
|
|
349
|
+
if (this.ingressClosed) {
|
|
350
|
+
throw ingressClosedError();
|
|
351
|
+
}
|
|
352
|
+
const generation = this.generation;
|
|
353
|
+
let env: Record<string, string>;
|
|
354
|
+
try {
|
|
355
|
+
this.binaries = resolveDesktopBinaries(this.which);
|
|
356
|
+
env = this.childEnv();
|
|
357
|
+
this.launch("x-server", xServerCommand(this.binaries.xServer), env);
|
|
358
|
+
const ready = await this.waitForVnc(generation);
|
|
359
|
+
if (this.generation !== generation) {
|
|
360
|
+
throw new Error("Desktop was torn down while starting");
|
|
361
|
+
}
|
|
362
|
+
if (!ready) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Desktop VNC server not ready on port ${DESKTOP_VNC_PORT} after ${this.readyDeadlineMs}ms`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const windowManagerConfig = this.writeWindowManagerConfig(
|
|
368
|
+
this.panelConfigDir,
|
|
369
|
+
);
|
|
370
|
+
this.launch(
|
|
371
|
+
"window-manager",
|
|
372
|
+
[
|
|
373
|
+
this.binaries.python,
|
|
374
|
+
"-c",
|
|
375
|
+
DESKTOP_WINDOW_DRAG_SCRIPT,
|
|
376
|
+
this.binaries.windowManager,
|
|
377
|
+
"--sm-disable",
|
|
378
|
+
"--config-file",
|
|
379
|
+
windowManagerConfig,
|
|
380
|
+
],
|
|
381
|
+
env,
|
|
382
|
+
);
|
|
383
|
+
// Before the dock, which only gets the ARGB visual its rounded corners
|
|
384
|
+
// and translucency need if a compositor is already running.
|
|
385
|
+
this.launchCosmetic("compositor", [this.binaries.compositor], env);
|
|
386
|
+
this.launch("clipboard", [this.binaries.clipboard, "-nowin"], env);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (this.generation === generation) {
|
|
389
|
+
void this.teardown(START_FAILED_LOSS);
|
|
390
|
+
}
|
|
391
|
+
throw new DesktopStartError(START_FAILED_LOSS, err);
|
|
392
|
+
}
|
|
393
|
+
this.running = true;
|
|
394
|
+
log.info({ display: DESKTOP_DISPLAY }, "Desktop started");
|
|
395
|
+
void this.refreshWallpaper(env, generation);
|
|
396
|
+
void this.startBrowser(env, generation);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async refreshWallpaper(
|
|
400
|
+
env: Record<string, string>,
|
|
401
|
+
generation: number,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
if (this.wallpaperStarting?.generation === generation) {
|
|
404
|
+
this.wallpaperStarting.refreshQueued = true;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const pending = { generation, refreshQueued: false };
|
|
408
|
+
this.wallpaperStarting = pending;
|
|
409
|
+
try {
|
|
410
|
+
const png = await this.renderWallpaper(DESKTOP_WIDTH, DESKTOP_HEIGHT);
|
|
411
|
+
if (
|
|
412
|
+
!png ||
|
|
413
|
+
pending.refreshQueued ||
|
|
414
|
+
this.generation !== generation ||
|
|
415
|
+
!this.binaries
|
|
416
|
+
) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
mkdirSync(this.panelConfigDir, { recursive: true });
|
|
420
|
+
const path = join(this.panelConfigDir, "wallpaper.png");
|
|
421
|
+
writeFileSync(path, png);
|
|
422
|
+
this.launchCosmetic(
|
|
423
|
+
"wallpaper",
|
|
424
|
+
[this.binaries.wallpaper, "--no-fehbg", "--bg-fill", path],
|
|
425
|
+
env,
|
|
426
|
+
);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
log.warn({ err }, "Desktop wallpaper could not be applied");
|
|
429
|
+
} finally {
|
|
430
|
+
if (this.wallpaperStarting === pending) {
|
|
431
|
+
this.wallpaperStarting = null;
|
|
432
|
+
if (
|
|
433
|
+
pending.refreshQueued &&
|
|
434
|
+
this.generation === generation &&
|
|
435
|
+
this.running
|
|
436
|
+
) {
|
|
437
|
+
void this.refreshWallpaper(env, generation);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private async waitForVnc(generation: number): Promise<boolean> {
|
|
444
|
+
const deadline = Date.now() + this.readyDeadlineMs;
|
|
445
|
+
while (this.generation === generation) {
|
|
446
|
+
if (await this.probeVncPort(DESKTOP_VNC_PORT)) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (Date.now() >= deadline) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
await sleep(VNC_PROBE_INTERVAL_MS);
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Launch Chrome and its dock once the X server is ready. */
|
|
458
|
+
private async startBrowser(
|
|
459
|
+
env: Record<string, string>,
|
|
460
|
+
generation: number,
|
|
461
|
+
): Promise<void> {
|
|
462
|
+
try {
|
|
463
|
+
const executable = await this.resolveChromePath();
|
|
464
|
+
if (this.generation !== generation) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
mkdirSync(this.profileDir, { recursive: true });
|
|
468
|
+
this.startPanel(executable, env);
|
|
469
|
+
this.launch("browser", browserCommand(executable, this.profileDir), env);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
log.warn({ err }, "Desktop browser failed to launch");
|
|
472
|
+
if (this.generation === generation) {
|
|
473
|
+
void this.teardown({
|
|
474
|
+
code: DESKTOP_CLOSE.failed,
|
|
475
|
+
reason: "Desktop browser failed to start",
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Start the dock after its Chrome launcher has an executable. */
|
|
482
|
+
private startPanel(chromiumPath: string, env: Record<string, string>): void {
|
|
483
|
+
const binaries = this.binaries;
|
|
484
|
+
if (this.panelStarted || !binaries) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.panelStarted = true;
|
|
488
|
+
this.panelLaunch = { chromiumPath, env };
|
|
489
|
+
try {
|
|
490
|
+
writeDesktopPanelConfig({
|
|
491
|
+
configDir: this.panelConfigDir,
|
|
492
|
+
chromiumPath,
|
|
493
|
+
chromiumProfileDir: this.profileDir,
|
|
494
|
+
terminalPath: binaries.terminal,
|
|
495
|
+
});
|
|
496
|
+
this.launch("panel", [binaries.panelSession, "--", binaries.panel], {
|
|
497
|
+
...env,
|
|
498
|
+
XDG_CONFIG_HOME: this.panelConfigDir,
|
|
499
|
+
XDG_DATA_HOME: this.panelConfigDir,
|
|
500
|
+
GSETTINGS_BACKEND: "keyfile",
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
log.warn({ err }, "Desktop dock failed to start");
|
|
504
|
+
this.schedulePanelRestart();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private schedulePanelRestart(): void {
|
|
509
|
+
if (!this.running || this.panelRestartTimer || !this.panelLaunch) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (this.panelRestartAttempts >= PANEL_RESTART_LIMIT) {
|
|
513
|
+
log.warn("Desktop dock restart limit reached");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.panelRestartAttempts += 1;
|
|
517
|
+
const generation = this.generation;
|
|
518
|
+
const { chromiumPath, env } = this.panelLaunch;
|
|
519
|
+
this.panelRestartTimer = setTimeout(() => {
|
|
520
|
+
this.panelRestartTimer = null;
|
|
521
|
+
if (generation === this.generation && this.running) {
|
|
522
|
+
this.panelStarted = false;
|
|
523
|
+
this.startPanel(chromiumPath, env);
|
|
524
|
+
}
|
|
525
|
+
}, this.panelRestartDelayMs);
|
|
526
|
+
this.panelRestartTimer.unref?.();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Spawn a child the desktop looks worse without but works fine without. */
|
|
530
|
+
private launchCosmetic(
|
|
531
|
+
role: DesktopChildRole,
|
|
532
|
+
cmd: readonly string[],
|
|
533
|
+
env: Record<string, string>,
|
|
534
|
+
): void {
|
|
535
|
+
try {
|
|
536
|
+
this.launch(role, cmd, env);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
log.warn({ err, role }, "Desktop child failed to start");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private launch(
|
|
543
|
+
role: DesktopChildRole,
|
|
544
|
+
cmd: readonly string[],
|
|
545
|
+
env: Record<string, string>,
|
|
546
|
+
): void {
|
|
547
|
+
const stale = this.children.get(role);
|
|
548
|
+
if (stale) {
|
|
549
|
+
log.warn({ role, pid: stale.pid }, "Desktop process already running");
|
|
550
|
+
void this.killAll(new Map([[role, stale]]));
|
|
551
|
+
}
|
|
552
|
+
const child = this.spawn(role, { cmd, env });
|
|
553
|
+
this.children.set(role, child);
|
|
554
|
+
child.exited.then(
|
|
555
|
+
(code) => this.onChildExit(role, child, code),
|
|
556
|
+
(err: unknown) => this.onChildExit(role, child, err),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private onChildExit(
|
|
561
|
+
role: DesktopChildRole,
|
|
562
|
+
child: DesktopChild,
|
|
563
|
+
outcome: unknown,
|
|
564
|
+
): void {
|
|
565
|
+
if (this.children.get(role) !== child) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.children.delete(role);
|
|
569
|
+
if (role === "panel") {
|
|
570
|
+
// Keep dock-launched applications alive until desktop teardown.
|
|
571
|
+
this.retiredPanels.add(child);
|
|
572
|
+
log.warn({ outcome }, "Desktop dock exited, scheduling restart");
|
|
573
|
+
this.schedulePanelRestart();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (role === "wallpaper" && outcome === 0) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (role === "browser") {
|
|
580
|
+
log.info({ outcome }, "Desktop browser exited");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (COSMETIC_ROLES.has(role)) {
|
|
584
|
+
// Cosmetic failures leave the interactive desktop available.
|
|
585
|
+
log.warn({ role, outcome }, "Desktop child exited");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
log.warn({ role, outcome }, "Desktop process exited, tearing down");
|
|
589
|
+
void this.teardown({
|
|
590
|
+
code: DESKTOP_CLOSE.failed,
|
|
591
|
+
reason: `Desktop stopped: ${role} exited`,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Kill the tree. The viewer, if any, hears `loss` before the kill starts;
|
|
597
|
+
* the linger path passes none since nobody is watching by then.
|
|
598
|
+
*/
|
|
599
|
+
private teardown(loss?: DesktopLoss): Promise<void> {
|
|
600
|
+
this.clearLinger();
|
|
601
|
+
this.generation += 1;
|
|
602
|
+
this.running = false;
|
|
603
|
+
this.binaries = null;
|
|
604
|
+
this.panelStarted = false;
|
|
605
|
+
if (this.panelRestartTimer) {
|
|
606
|
+
clearTimeout(this.panelRestartTimer);
|
|
607
|
+
this.panelRestartTimer = null;
|
|
608
|
+
}
|
|
609
|
+
this.panelRestartAttempts = 0;
|
|
610
|
+
this.panelLaunch = null;
|
|
611
|
+
const retiredPanels = [...this.retiredPanels];
|
|
612
|
+
this.retiredPanels.clear();
|
|
613
|
+
const children = new Map(this.children);
|
|
614
|
+
this.children.clear();
|
|
615
|
+
const viewer = this.viewer;
|
|
616
|
+
this.viewer = null;
|
|
617
|
+
if (viewer && loss) {
|
|
618
|
+
viewer.onDesktopLost(loss);
|
|
619
|
+
}
|
|
620
|
+
// Chain onto an earlier kill still in its grace so a start waits for both.
|
|
621
|
+
const done: Promise<void> = Promise.all([
|
|
622
|
+
this.tearingDown,
|
|
623
|
+
this.killAll(children),
|
|
624
|
+
...retiredPanels.map((child) =>
|
|
625
|
+
this.killAll(new Map([["panel", child]])),
|
|
626
|
+
),
|
|
627
|
+
])
|
|
628
|
+
.then(() => undefined)
|
|
629
|
+
.finally(() => {
|
|
630
|
+
if (this.tearingDown === done) {
|
|
631
|
+
this.tearingDown = null;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
this.tearingDown = done;
|
|
635
|
+
return done;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* SIGTERM so X and Chrome exit cleanly, then SIGKILL whatever is left and
|
|
640
|
+
* wait for it too: a killed X server holds the display and port until it is
|
|
641
|
+
* reaped. Both waits are bounded by the grace so shutdown cannot hang.
|
|
642
|
+
*/
|
|
643
|
+
private async killAll(
|
|
644
|
+
children: ReadonlyMap<DesktopChildRole, DesktopChild>,
|
|
645
|
+
): Promise<void> {
|
|
646
|
+
if (children.size === 0) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const alive = new Map(children);
|
|
650
|
+
const exits = Promise.all(
|
|
651
|
+
[...children].map(([role, child]) => {
|
|
652
|
+
this.killProcessGroup(child, "SIGTERM");
|
|
653
|
+
return child.exited.catch(() => 0).then(() => alive.delete(role));
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
const panel = children.get("panel");
|
|
657
|
+
await this.waitForExits(
|
|
658
|
+
panel ? Promise.all([exits, sleep(this.killGraceMs)]) : exits,
|
|
659
|
+
);
|
|
660
|
+
for (const [role, child] of children) {
|
|
661
|
+
// A reaped panel does not prove its application process group is empty.
|
|
662
|
+
if (role === "panel" || alive.has(role)) {
|
|
663
|
+
this.killProcessGroup(child, "SIGKILL");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
await this.waitForExits(exits);
|
|
667
|
+
if (alive.size > 0) {
|
|
668
|
+
const survivors = [...alive].map(([role, child]) => ({
|
|
669
|
+
role,
|
|
670
|
+
pid: child.pid,
|
|
671
|
+
}));
|
|
672
|
+
log.warn({ survivors }, "Desktop processes still alive after SIGKILL");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private waitForExits(exits: Promise<unknown>): Promise<void> {
|
|
677
|
+
let grace: ReturnType<typeof setTimeout> | undefined;
|
|
678
|
+
return Promise.race([
|
|
679
|
+
exits.then(() => undefined),
|
|
680
|
+
new Promise<void>((resolve) => {
|
|
681
|
+
grace = setTimeout(resolve, this.killGraceMs);
|
|
682
|
+
}),
|
|
683
|
+
]).finally(() => clearTimeout(grace));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private armLinger(): void {
|
|
687
|
+
this.clearLinger();
|
|
688
|
+
this.lingerTimer = setTimeout(() => {
|
|
689
|
+
this.lingerTimer = null;
|
|
690
|
+
log.info("Desktop idle, tearing down");
|
|
691
|
+
void this.teardown();
|
|
692
|
+
}, this.lingerMs);
|
|
693
|
+
this.lingerTimer.unref?.();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private clearLinger(): void {
|
|
697
|
+
if (this.lingerTimer) {
|
|
698
|
+
clearTimeout(this.lingerTimer);
|
|
699
|
+
this.lingerTimer = null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private childEnv(): Record<string, string> {
|
|
704
|
+
const env: Record<string, string> = {};
|
|
705
|
+
for (const key of DESKTOP_ENV_KEYS) {
|
|
706
|
+
const value = this.sourceEnv[key];
|
|
707
|
+
if (value != null) {
|
|
708
|
+
env[key] = value;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
env.DISPLAY = DESKTOP_DISPLAY;
|
|
712
|
+
return env;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function ingressClosedError(): DesktopStartError {
|
|
717
|
+
return new DesktopStartError(
|
|
718
|
+
SHUTTING_DOWN_LOSS,
|
|
719
|
+
new Error("Desktop ingress is closed"),
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
// Commands
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
function xServerCommand(executable: string): string[] {
|
|
728
|
+
return [
|
|
729
|
+
executable,
|
|
730
|
+
DESKTOP_DISPLAY,
|
|
731
|
+
"-localhost",
|
|
732
|
+
"-SecurityTypes",
|
|
733
|
+
"None",
|
|
734
|
+
"-rfbport",
|
|
735
|
+
String(DESKTOP_VNC_PORT),
|
|
736
|
+
"-geometry",
|
|
737
|
+
DESKTOP_GEOMETRY,
|
|
738
|
+
// Keep the dock and wallpaper anchored to a stable display.
|
|
739
|
+
"-AcceptSetDesktopSize=0",
|
|
740
|
+
"-depth",
|
|
741
|
+
"24",
|
|
742
|
+
"-desktop",
|
|
743
|
+
"Vellum",
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function browserCommand(executable: string, profileDir: string): string[] {
|
|
748
|
+
// Explicit window bounds override Chrome's maximized startup state.
|
|
749
|
+
return [
|
|
750
|
+
executable,
|
|
751
|
+
"--no-sandbox",
|
|
752
|
+
"--no-first-run",
|
|
753
|
+
"--disable-dev-shm-usage",
|
|
754
|
+
// Crash recovery requires both flags; clean exits use normal startup.
|
|
755
|
+
...(shouldRestoreDesktopChromeSession(profileDir)
|
|
756
|
+
? ["--restore-last-session", "--hide-crash-restore-bubble"]
|
|
757
|
+
: []),
|
|
758
|
+
"--start-maximized",
|
|
759
|
+
`--user-data-dir=${profileDir}`,
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
// Default collaborators
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
|
|
767
|
+
function spawnDetached(
|
|
768
|
+
_role: DesktopChildRole,
|
|
769
|
+
request: DesktopSpawnRequest,
|
|
770
|
+
): DesktopChild {
|
|
771
|
+
// Each child leads its own process group so teardown can kill everything
|
|
772
|
+
// it forked (Chrome's renderers, openbox's autostart) in one signal.
|
|
773
|
+
return Bun.spawn([...request.cmd], {
|
|
774
|
+
env: request.env,
|
|
775
|
+
detached: true,
|
|
776
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
777
|
+
windowsHide: true,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function killProcessGroup(child: DesktopChild, signal: DesktopSignal): void {
|
|
782
|
+
if (signal === "SIGKILL") {
|
|
783
|
+
terminateProcessTree(child);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
process.kill(-child.pid, signal);
|
|
788
|
+
} catch {
|
|
789
|
+
// Not a group leader, or already gone.
|
|
790
|
+
child.kill(signal);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Dial a loopback TCP port; resolves on connect, rejects when refused. */
|
|
795
|
+
export function connectLoopback(
|
|
796
|
+
port: number,
|
|
797
|
+
handlers: DesktopTcpHandlers,
|
|
798
|
+
): Promise<DesktopTcpSocket> {
|
|
799
|
+
return new Promise((resolve, reject) => {
|
|
800
|
+
Bun.connect({
|
|
801
|
+
hostname: "127.0.0.1",
|
|
802
|
+
port,
|
|
803
|
+
socket: {
|
|
804
|
+
open: (socket) => resolve(socket),
|
|
805
|
+
data: (_socket, data) => handlers.onData(data),
|
|
806
|
+
drain: () => handlers.onDrain(),
|
|
807
|
+
close: () => handlers.onClose(),
|
|
808
|
+
error: (_socket, err) => handlers.onError(err),
|
|
809
|
+
connectError: (_socket, err) => reject(err),
|
|
810
|
+
},
|
|
811
|
+
}).catch(reject);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const silentHandlers: DesktopTcpHandlers = {
|
|
816
|
+
onData() {},
|
|
817
|
+
onDrain() {},
|
|
818
|
+
onClose() {},
|
|
819
|
+
onError() {},
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
async function probeLoopbackPort(port: number): Promise<boolean> {
|
|
823
|
+
try {
|
|
824
|
+
const socket = await connectLoopback(port, silentHandlers);
|
|
825
|
+
socket.end();
|
|
826
|
+
return true;
|
|
827
|
+
} catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
// Singleton
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
let sharedManager: DesktopSessionManager | null = null;
|
|
837
|
+
|
|
838
|
+
/** One instance because there is one display and one VNC port. */
|
|
839
|
+
export function getDesktopSessionManager(): DesktopSessionManager {
|
|
840
|
+
sharedManager ??= new DesktopSessionManager();
|
|
841
|
+
return sharedManager;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Shut the shared desktop down. Latches even when none was ever served, so a
|
|
846
|
+
* socket arriving mid-shutdown cannot start a tree that nothing would kill.
|
|
847
|
+
*/
|
|
848
|
+
export function destroyDesktopSessionManager(): Promise<void> {
|
|
849
|
+
return getDesktopSessionManager().destroy();
|
|
850
|
+
}
|