@songsid/agend 2.3.0-beta.25 → 2.3.0-beta.27
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/README.md +7 -5
- package/README.zh-TW.md +7 -5
- package/dist/adapter-world.js.map +1 -1
- package/dist/agend-home.d.ts +5 -0
- package/dist/agend-home.js +7 -0
- package/dist/agend-home.js.map +1 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +8 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +1 -1
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +12 -2
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/apply-job.js.map +1 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +5 -0
- package/dist/auth-api.js +21 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +17 -8
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +14 -6
- package/dist/authority/web-authority.js +19 -5
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.d.ts +2 -2
- package/dist/backend/antigravity.js +2 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +1 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-kas-store.js.map +1 -1
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/mock.js.map +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/session-signals.js.map +1 -1
- package/dist/backend-availability.js.map +1 -1
- package/dist/backend-command.js.map +1 -1
- package/dist/backend-outage.js.map +1 -1
- package/dist/binary-probe.js.map +1 -1
- package/dist/bug-report.js.map +1 -1
- package/dist/bundled-skills.js.map +1 -1
- package/dist/cache-service.js +1 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +40 -5
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -0
- package/dist/channel/adapters/telegram.js +54 -14
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +10 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/sticker-download.js.map +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.js +3 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli-entry.js.map +1 -1
- package/dist/cli-flags.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +44 -9
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.js.map +1 -1
- package/dist/completion-install.js.map +1 -1
- package/dist/completion.js.map +1 -1
- package/dist/config-validator.js +22 -6
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.js.map +1 -1
- package/dist/context-guardian.js.map +1 -1
- package/dist/coredump-filter.js.map +1 -1
- package/dist/cpu-profile.js.map +1 -1
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +76 -4
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.js.map +1 -1
- package/dist/darwin-memory.js.map +1 -1
- package/dist/dashboard-live.d.ts +8 -0
- package/dist/dashboard-live.js +35 -0
- package/dist/dashboard-live.js.map +1 -0
- package/dist/dashboard-permissions.d.ts +69 -0
- package/dist/dashboard-permissions.js +138 -0
- package/dist/dashboard-permissions.js.map +1 -0
- package/dist/decision-list.js.map +1 -1
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/diagnostic-redaction.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +10 -0
- package/dist/doctor.js.map +1 -1
- package/dist/e2e-isolation.js.map +1 -1
- package/dist/event-log.js.map +1 -1
- package/dist/export-import.js +3 -2
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-label.js.map +1 -1
- package/dist/fleet-lock.js +3 -2
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +84 -7
- package/dist/fleet-manager.js +562 -158
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.js +3 -2
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/fleet-startup.d.ts +14 -0
- package/dist/fleet-startup.js +30 -0
- package/dist/fleet-startup.js.map +1 -0
- package/dist/fleet-traffic.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +0 -20
- package/dist/forged-envelope-scan.js +3 -2
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +43 -21
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +52 -23
- package/dist/host-memory.js.map +1 -1
- package/dist/idle-gate-log.d.ts +19 -0
- package/dist/idle-gate-log.js +15 -0
- package/dist/idle-gate-log.js.map +1 -0
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +1 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +6 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.js.map +1 -1
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-log-gate.js.map +1 -1
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +40 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.js +3 -2
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/memory-pressure.js.map +1 -1
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.js.map +1 -1
- package/dist/network-family.js.map +1 -1
- package/dist/operator-control-paths.js +3 -4
- package/dist/operator-control-paths.js.map +1 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +68 -68
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.js.map +1 -1
- package/dist/pane-context-cache.js.map +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/private-dir.js.map +1 -1
- package/dist/probe-worker-pool.js.map +1 -1
- package/dist/profile-control.js.map +1 -1
- package/dist/prompt-file-ref.js.map +1 -1
- package/dist/provider-probe.js.map +1 -1
- package/dist/provider-secret-registry.js.map +1 -1
- package/dist/public-link-auto-open.d.ts +32 -0
- package/dist/public-link-auto-open.js +86 -0
- package/dist/public-link-auto-open.js.map +1 -0
- package/dist/public-link-owner.d.ts +10 -0
- package/dist/public-link-owner.js +36 -0
- package/dist/public-link-owner.js.map +1 -0
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +7 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +35 -1
- package/dist/public-web-link.js +142 -43
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +5 -0
- package/dist/quickstart.js +25 -16
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/resource-report.js.map +1 -1
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.js.map +1 -1
- package/dist/runtime-cpu-profile.js.map +1 -1
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/task-board-text.js +16 -11
- package/dist/scheduler/task-board-text.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +39 -11
- package/dist/scheduler/task-board.js +123 -45
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/secret-file.js.map +1 -1
- package/dist/secret-store.js.map +1 -1
- package/dist/service-installer.d.ts +7 -1
- package/dist/service-installer.js +13 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +33 -4
- package/dist/service-plan.js +177 -8
- package/dist/service-plan.js.map +1 -1
- package/dist/session-oom.d.ts +21 -0
- package/dist/session-oom.js +139 -0
- package/dist/session-oom.js.map +1 -0
- package/dist/settings-api.js +3 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +71 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +4 -0
- package/dist/settings-confirmation.js +2 -0
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-control.js.map +1 -1
- package/dist/settings-effect.js +4 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +13 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-host.d.ts +5 -1
- package/dist/setup-host.js +8 -1
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/spawn-gate.d.ts +4 -0
- package/dist/spawn-gate.js +15 -5
- package/dist/spawn-gate.js.map +1 -1
- package/dist/start-attempt.d.ts +1 -0
- package/dist/start-attempt.js +18 -0
- package/dist/start-attempt.js.map +1 -0
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +20 -0
- package/dist/start-failure-notice.js.map +1 -0
- package/dist/start-failure.d.ts +147 -0
- package/dist/start-failure.js +281 -0
- package/dist/start-failure.js.map +1 -0
- package/dist/startup-memory-deadline.d.ts +23 -0
- package/dist/startup-memory-deadline.js +55 -0
- package/dist/startup-memory-deadline.js.map +1 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/storm-window.js.map +1 -1
- package/dist/systemd-peer-bus.js.map +1 -1
- package/dist/target-queue-worker.js.map +1 -1
- package/dist/task-board-summary.js.map +1 -1
- package/dist/task-board-tool.js +3 -2
- package/dist/task-board-tool.js.map +1 -1
- package/dist/task-board-waker.js +8 -4
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -0
- package/dist/tmux-manager.js +2 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-archiver.js.map +1 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.js.map +1 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.js +3 -2
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/tunnel/public-fetch.js.map +1 -1
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/ui/chat-render.js +7 -17
- package/dist/ui/chat-store.js +171 -61
- package/dist/ui/chat-strings.js +9 -4
- package/dist/ui/chat-thread.js +48 -1
- package/dist/ui/fleet-canvas-layout.js +3 -0
- package/dist/ui/fleet-canvas.js +23 -20
- package/dist/ui/fleet-office-editor.js +149 -0
- package/dist/ui/fleet-office-model.js +358 -0
- package/dist/ui/fleet-office-pack.js +199 -0
- package/dist/ui/fleet-office-skin.js +474 -0
- package/dist/ui/fleet-office-store.js +49 -0
- package/dist/ui/fleet-office-zones.js +177 -0
- package/dist/ui/fleet-office.js +434 -0
- package/dist/ui/fleet-org.js +18 -5
- package/dist/ui/guest-refused.html +41 -0
- package/dist/ui/image-lightbox.js +9 -3
- package/dist/ui/panel-chat.js +35 -22
- package/dist/ui/panel-settings.js +24 -7
- package/dist/ui/settings-dialogs.js +11 -2
- package/dist/ui/settings-model.js +20 -0
- package/dist/ui/settings-strings.js +36 -0
- package/dist/ui/settings-web-authority.js +7 -2
- package/dist/ui/shared/agend-auth.js +2 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +2 -0
- package/dist/ui/shared/app-shell.js +62 -9
- package/dist/ui/shared/app-store.js +1 -0
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +164 -15
- package/dist/ui/shared/guest-refused.js +35 -0
- package/dist/ui/shared/header-tools.js +2 -2
- package/dist/ui/shared/instance-nav.js +25 -8
- package/dist/ui/shared/tokens.css +4 -0
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/usage-dialog.js +152 -6
- package/dist/ui/shared/view-strings.js +20 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/update-check.js.map +1 -1
- package/dist/update-dispatch.js.map +1 -1
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.js.map +1 -1
- package/dist/update-preflight.js.map +1 -1
- package/dist/update-progress.d.ts +3 -1
- package/dist/update-progress.js +3 -1
- package/dist/update-progress.js.map +1 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/providers.d.ts +8 -0
- package/dist/usage/providers.js +6 -2
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/usage/usage-api.js +2 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.js +1 -1
- package/dist/usage-rollup.js.map +1 -1
- package/dist/user-systemd.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-ambient.js.map +1 -1
- package/dist/web-api.d.ts +19 -2
- package/dist/web-api.js +180 -28
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon.js.map +1 -1
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.js +5 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +41 -11
- package/dist/web-chat-history.js +93 -22
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +2 -2
- package/dist/web-effect-library.js.map +1 -1
- package/dist/web-effect-playback.js.map +1 -1
- package/dist/web-effect-profiles.d.ts +16 -16
- package/dist/web-effect-profiles.js +3 -2
- package/dist/web-effect-profiles.js.map +1 -1
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect.d.ts +7 -7
- package/dist/web-effect.js.map +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.js +1 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-guest-rooms.js.map +1 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-icons.js.map +1 -1
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-images.js.map +1 -1
- package/dist/web-libavoid.js.map +1 -1
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.js.map +1 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-office-store.d.ts +104 -0
- package/dist/web-office-store.js +236 -0
- package/dist/web-office-store.js.map +1 -0
- package/dist/web-owner-css-store.js.map +1 -1
- package/dist/web-owner-css.js.map +1 -1
- package/dist/web-png.js.map +1 -1
- package/dist/web-prefs.js +2 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-profile.js.map +1 -1
- package/dist/web-reactions.d.ts +2 -1
- package/dist/web-request-context.d.ts +5 -0
- package/dist/web-request-context.js.map +1 -1
- package/dist/web-returning-device.js +1 -1
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-schedules.js.map +1 -1
- package/dist/web-send-dedupe.d.ts +36 -0
- package/dist/web-send-dedupe.js +69 -0
- package/dist/web-send-dedupe.js.map +1 -0
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.js +1 -1
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal-http.js.map +1 -1
- package/dist/web-terminal.js.map +1 -1
- package/dist/web-theme-store.js.map +1 -1
- package/dist/web-upload.d.ts +5 -1
- package/dist/web-upload.js +6 -4
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.js.map +1 -1
- package/dist/webhook-emitter.js.map +1 -1
- package/dist/windows-shortcuts.js +1 -1
- package/dist/windows-shortcuts.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/dist/ws-server.js.map +1 -1
- package/dist/yaml-shared-nodes.js.map +1 -1
- package/launcher/agend-install.cjs +281 -6
- package/launcher/private-home.cjs +43 -0
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -13,7 +13,9 @@ import { handleWebIconRequest, useAppIconStore } from "./web-icons.js";
|
|
|
13
13
|
import { AppIconStore } from "./web-app-icon.js";
|
|
14
14
|
import { trackWebServer } from "./web-server-drain.js";
|
|
15
15
|
import { renderPublicLinkProgress, ThrottledMessageEditor } from "./public-link-progress.js";
|
|
16
|
-
import { PublicWebLink, publicLinkSettings } from "./public-web-link.js";
|
|
16
|
+
import { PublicWebLink, publicLinkNamedHostname, publicLinkSettings } from "./public-web-link.js";
|
|
17
|
+
import { PublicLinkAutoOpen } from "./public-link-auto-open.js";
|
|
18
|
+
import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from "./public-link-owner.js";
|
|
17
19
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
18
20
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
19
21
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
@@ -36,6 +38,7 @@ import { fleetLabel } from "./fleet-label.js";
|
|
|
36
38
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
37
39
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
38
40
|
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
|
|
41
|
+
import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
|
|
39
42
|
import { readFleetMemory } from "./process-memory.js";
|
|
40
43
|
import { MemoryPressure } from "./memory-pressure.js";
|
|
41
44
|
import { replyDedupText, ReplyDeduper } from "./reply-dedup.js";
|
|
@@ -144,7 +147,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
144
147
|
}
|
|
145
148
|
return tasks;
|
|
146
149
|
}
|
|
147
|
-
import { handleWebRequest, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
150
|
+
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
148
151
|
import { LogService } from "./log-service.js";
|
|
149
152
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
150
153
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
@@ -154,6 +157,7 @@ import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
|
|
|
154
157
|
import { agendVersion } from "./agent-whats-new.js";
|
|
155
158
|
import { OwnerCssStore } from "./web-owner-css-store.js";
|
|
156
159
|
import { ThemeStore } from "./web-theme-store.js";
|
|
160
|
+
import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
157
161
|
import { WebAppearance } from "./web-appearance.js";
|
|
158
162
|
import { WebEffectLibrary } from "./web-effect-library.js";
|
|
159
163
|
import { WebEffectTrials } from "./web-effect-trials.js";
|
|
@@ -213,6 +217,7 @@ import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
|
213
217
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
214
218
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
215
219
|
import { presentationState, interactionSummary, sameInteractionOwner } from "./interaction-observation.js";
|
|
220
|
+
import { idleGateTimeoutLog } from "./idle-gate-log.js";
|
|
216
221
|
import { DEFAULT_STUCK_TIMEOUT_MS, readLastInboundAt } from "./daemon.js";
|
|
217
222
|
import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
|
|
218
223
|
import { readBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
@@ -235,12 +240,13 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
|
235
240
|
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
236
241
|
import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
|
|
237
242
|
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
243
|
+
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
238
244
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
239
245
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
|
|
240
|
-
import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
|
|
246
|
+
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
241
247
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
242
248
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
243
|
-
import { authorityNow,
|
|
249
|
+
import { authorityNow, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
244
250
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
245
251
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
246
252
|
import { SessionAuthorizationRequests } from "./web-session-authorization.js";
|
|
@@ -267,6 +273,7 @@ import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
|
267
273
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
268
274
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
269
275
|
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
276
|
+
import { FleetStartup } from "./fleet-startup.js";
|
|
270
277
|
import { launchFullRestartHelper } from "./full-restart.js";
|
|
271
278
|
import { fullReloadExitCode } from "./service-owner.js";
|
|
272
279
|
import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
|
|
@@ -671,7 +678,8 @@ export class FleetManager {
|
|
|
671
678
|
]);
|
|
672
679
|
logger = createLogger("info");
|
|
673
680
|
/** Report each bundled skill collision once per fleet, across instance starts. */
|
|
674
|
-
|
|
681
|
+
/** #2482: per skills directory, the skipped set (names + their content) last said — this process's view of the record. */
|
|
682
|
+
skippedSkillsSaid = new Map();
|
|
675
683
|
topicCommands;
|
|
676
684
|
// sessionName → instanceName mapping for external sessions
|
|
677
685
|
sessionRegistry = new Map();
|
|
@@ -814,6 +822,7 @@ export class FleetManager {
|
|
|
814
822
|
// Adapter isolation: track state per adapter for retry + visibility
|
|
815
823
|
/** #2175: this boot's progress message, whose completion may still be owed by its background retry. */
|
|
816
824
|
startupProgress = null;
|
|
825
|
+
startupDashboard = null;
|
|
817
826
|
adapterState = new Map();
|
|
818
827
|
adapterRetryInFlight = new Map();
|
|
819
828
|
/** Web Settings secret rotation is deliberately separate from reconnect
|
|
@@ -895,6 +904,7 @@ export class FleetManager {
|
|
|
895
904
|
/** Days of event/activity history to keep. */
|
|
896
905
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
897
906
|
watchdogTimer = null;
|
|
907
|
+
startupMemoryDeadline = null;
|
|
898
908
|
eventLoopWatch = null;
|
|
899
909
|
startedAt = 0;
|
|
900
910
|
stormOpenNotifyTimer = null;
|
|
@@ -930,6 +940,28 @@ export class FleetManager {
|
|
|
930
940
|
webFiles = new WebFileLedger({ limits: () => uploadLimits(this.fleetConfig?.web) });
|
|
931
941
|
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
932
942
|
webReplyOrder = new Map();
|
|
943
|
+
/**
|
|
944
|
+
* #1774: an instance name's reply generation — moved on when a removal of it starts, so replies sent from then on (a
|
|
945
|
+
* same-name replacement's) are told from the removed instance's. `webChatRemovals`: once a removal succeeded, the
|
|
946
|
+
* generations and the daemon whose replies are no longer anyone's to show.
|
|
947
|
+
*/
|
|
948
|
+
webChatGenerations = new Map();
|
|
949
|
+
webChatRemovals = new Map();
|
|
950
|
+
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
951
|
+
webChatRemovalsPending = new Map();
|
|
952
|
+
webChatGeneration(name) { return this.webChatGenerations.get(name) ?? 0; }
|
|
953
|
+
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
954
|
+
webChatIdentity(name) { return { generation: this.webChatGeneration(name), daemon: this.daemons.get(name) }; }
|
|
955
|
+
/** #1774: the reply's instance was not removed (a removal that failed changes nothing). */
|
|
956
|
+
webChatIdentityCurrent(name, id) {
|
|
957
|
+
const removed = this.webChatRemovals.get(name);
|
|
958
|
+
return !removed || (id.generation >= removed.below && (removed.daemon === undefined || id.daemon !== removed.daemon));
|
|
959
|
+
}
|
|
960
|
+
/** #1774: the removal still deciding this reply's fate (it was sent by what is being removed), or null. */
|
|
961
|
+
webChatRemovalPending(name, id) {
|
|
962
|
+
const pending = this.webChatRemovalsPending.get(name);
|
|
963
|
+
return pending && (id.generation < pending.below || (pending.daemon !== undefined && id.daemon === pending.daemon)) ? pending.done : null;
|
|
964
|
+
}
|
|
933
965
|
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
934
966
|
webReplyMaxBytes = REPLY_FILES_MAX_BYTES;
|
|
935
967
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
@@ -1015,6 +1047,11 @@ export class FleetManager {
|
|
|
1015
1047
|
// r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
|
|
1016
1048
|
return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits, effectsDropped: ids => this.effectLibrary.collectUnreferenced(ids) }));
|
|
1017
1049
|
}
|
|
1050
|
+
officeStoreInstance;
|
|
1051
|
+
/** Fleet → Org chart → Office: the offices people made (web-offices.json, beside web-themes.json). */
|
|
1052
|
+
get officeStore() { return (this.officeStoreInstance ??= new OfficeStore(this.dataDir)); }
|
|
1053
|
+
/** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
|
|
1054
|
+
officeWrites = new OfficeWriteLimiter();
|
|
1018
1055
|
/** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
|
|
1019
1056
|
get webAppearance() {
|
|
1020
1057
|
return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
|
|
@@ -1362,7 +1399,13 @@ export class FleetManager {
|
|
|
1362
1399
|
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
1363
1400
|
if (auth.kind !== "ok")
|
|
1364
1401
|
return null;
|
|
1365
|
-
|
|
1402
|
+
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
1403
|
+
if (gatewayRequestContext(req))
|
|
1404
|
+
return null;
|
|
1405
|
+
// …and never a change that turns the named-link option on: that one needs a General's card or the host.
|
|
1406
|
+
if (!store.selfConfirmable(id))
|
|
1407
|
+
return null;
|
|
1408
|
+
const record = auth.session, gateway = !!gatewayRequestContext(req), peer = webPeerOf(req);
|
|
1366
1409
|
const verified = () => {
|
|
1367
1410
|
const now = this.webAuthority({ session: record, gateway, peer });
|
|
1368
1411
|
// A refusal carries no authority (kind "none"); the handle pins the session, and with it its one account.
|
|
@@ -1629,6 +1672,7 @@ export class FleetManager {
|
|
|
1629
1672
|
this.spawnGate = new SpawnGate({
|
|
1630
1673
|
storm: this.stormWindow,
|
|
1631
1674
|
memoryPressure: this.memoryPressure,
|
|
1675
|
+
onStartupMemoryWait: () => this.startupMemoryDeadline?.check(),
|
|
1632
1676
|
concurrency: () => this.spawnConcurrency(),
|
|
1633
1677
|
staggerMs: () => this.fleetConfig?.defaults?.startup?.stagger_delay_ms ?? 500,
|
|
1634
1678
|
});
|
|
@@ -2674,6 +2718,7 @@ export class FleetManager {
|
|
|
2674
2718
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
2675
2719
|
this.warnAboutRemovedLoginMode(loaded);
|
|
2676
2720
|
this.enforceGuestSwitch();
|
|
2721
|
+
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
2677
2722
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
2678
2723
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
2679
2724
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -3280,7 +3325,8 @@ export class FleetManager {
|
|
|
3280
3325
|
instancePresentation(name) {
|
|
3281
3326
|
const execution_state = this.getInstanceExecutionState(name);
|
|
3282
3327
|
const interaction = this.getInstanceInteraction(name);
|
|
3283
|
-
|
|
3328
|
+
const startup_phase = !this.lifecycle.isPaused(name) ? this.startupDashboard?.phase(name) : undefined;
|
|
3329
|
+
return { ...(startup_phase ? { startup_phase } : {}), state: presentationState(execution_state, interaction), execution_state,
|
|
3284
3330
|
interaction, interaction_summary: interactionSummary(interaction) };
|
|
3285
3331
|
}
|
|
3286
3332
|
/**
|
|
@@ -3714,7 +3760,9 @@ export class FleetManager {
|
|
|
3714
3760
|
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
3715
3761
|
return false;
|
|
3716
3762
|
if (!idle) {
|
|
3717
|
-
|
|
3763
|
+
// #2482: a busy target is the normal case (its CLI queues the message): INFO; a WARN only for a likely anomaly.
|
|
3764
|
+
const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
|
|
3765
|
+
this.logger[log.level](log.fields, log.message);
|
|
3718
3766
|
}
|
|
3719
3767
|
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent);
|
|
3720
3768
|
if (!sent)
|
|
@@ -3832,6 +3880,10 @@ export class FleetManager {
|
|
|
3832
3880
|
// #2305: the copies' own message ids, so an instance's reaction to the web message is put on them too.
|
|
3833
3881
|
if (copies.length)
|
|
3834
3882
|
this.rememberWebEchoCopies(messageId, copies);
|
|
3883
|
+
// #2281: and where they are, for a later web reply to this message to be a native reply there.
|
|
3884
|
+
const posts = copies.flatMap(c => c.adapterId ? [{ adapterId: c.adapterId, chatId: c.chatId, ...(c.threadId ? { threadId: c.threadId } : {}), messageId: c.messageId }] : []);
|
|
3885
|
+
if (posts.length)
|
|
3886
|
+
this.webChatHistory.addPosts(instance, messageId, posts);
|
|
3835
3887
|
const m = this.webChatHistory.markChannel(instance, messageId);
|
|
3836
3888
|
if (m)
|
|
3837
3889
|
broadcastToGuests(this.sseClients, "message", m, (err) => this.logger.debug({ err }, "SSE client write failed; evicting"));
|
|
@@ -3882,7 +3934,11 @@ export class FleetManager {
|
|
|
3882
3934
|
return { ok: true, mirrored };
|
|
3883
3935
|
}
|
|
3884
3936
|
/** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
|
|
3885
|
-
|
|
3937
|
+
/**
|
|
3938
|
+
* `reply` (#2281): where the message a web reply quotes is on a platform. A room that has it gets the reply as a
|
|
3939
|
+
* native reply to it (`reply.text`: without the quoted line); every other room gets `text`, as before.
|
|
3940
|
+
*/
|
|
3941
|
+
async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK, copies, reply) {
|
|
3886
3942
|
const guard = this.webChannelEchoGuards.get(instance);
|
|
3887
3943
|
const handled = new Set();
|
|
3888
3944
|
let posted = 0;
|
|
@@ -3912,9 +3968,11 @@ export class FleetManager {
|
|
|
3912
3968
|
try {
|
|
3913
3969
|
if (!adapter?.sendText)
|
|
3914
3970
|
continue;
|
|
3915
|
-
|
|
3971
|
+
// A Telegram room is its chat; a Discord room is the guild's channel (an inbound's thread there).
|
|
3972
|
+
const native = reply?.posts.find(p => p.adapterId === adapter.id && (p.chatId === entry.channelId || p.threadId === entry.channelId));
|
|
3973
|
+
const sent = await adapter.sendText(entry.channelId, native ? reply.text : text, { format: "text", allowedMentions: { parse: [] }, ...(native ? { replyTo: native.messageId } : {}) });
|
|
3916
3974
|
if (copies && sent && typeof sent.messageId === "string" && sent.messageId)
|
|
3917
|
-
copies.push({ adapter, chatId: entry.channelId, messageId: sent.messageId });
|
|
3975
|
+
copies.push({ adapter, adapterId: adapter.id, chatId: entry.channelId, messageId: sent.messageId });
|
|
3918
3976
|
ClassicChannelManager.logMessage(instance, sender, text, new Date());
|
|
3919
3977
|
posted++;
|
|
3920
3978
|
}
|
|
@@ -4952,7 +5010,7 @@ export class FleetManager {
|
|
|
4952
5010
|
return;
|
|
4953
5011
|
}
|
|
4954
5012
|
if (this.lifecycle.isPaused(name)) {
|
|
4955
|
-
this.logger.
|
|
5013
|
+
this.logger.debug({ name }, "Persisted paused instance — skipping startup"); // #2482: every start and reload
|
|
4956
5014
|
return;
|
|
4957
5015
|
}
|
|
4958
5016
|
if (this.lifecycle.daemons.has(name)) {
|
|
@@ -4971,66 +5029,92 @@ export class FleetManager {
|
|
|
4971
5029
|
this.logger.info({ name }, "Instance already running, skipping");
|
|
4972
5030
|
return;
|
|
4973
5031
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
else if (kind === "fleet-topic") {
|
|
4983
|
-
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
4984
|
-
// deliberately excluded: their workspace and conversation lifecycle are
|
|
4985
|
-
// managed independently from fleet-topic workers.
|
|
4986
|
-
try {
|
|
4987
|
-
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
4988
|
-
measureSyncWork("fleet.workerSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "worker"));
|
|
5032
|
+
await this.trackStartupInstance(name, async () => {
|
|
5033
|
+
// An explicit start (CLI/API) begins a fresh idle window; boot and
|
|
5034
|
+
// reconcile starts keep the persisted last-inbound seed (Phase 2a).
|
|
5035
|
+
if (resumePaused)
|
|
5036
|
+
this.lifecycle.markActivitySeedNow(name);
|
|
5037
|
+
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5038
|
+
if (config.general_topic) {
|
|
5039
|
+
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
4989
5040
|
}
|
|
4990
|
-
|
|
4991
|
-
//
|
|
4992
|
-
//
|
|
4993
|
-
// fleet
|
|
4994
|
-
|
|
5041
|
+
else if (kind === "fleet-topic") {
|
|
5042
|
+
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
5043
|
+
// deliberately excluded: their workspace and conversation lifecycle are
|
|
5044
|
+
// managed independently from fleet-topic workers.
|
|
5045
|
+
try {
|
|
5046
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
5047
|
+
measureSyncWork("fleet.workerSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "worker"));
|
|
5048
|
+
}
|
|
5049
|
+
catch (err) {
|
|
5050
|
+
// Skill publishing is additive. A read-only or temporarily unavailable
|
|
5051
|
+
// workspace must not turn an otherwise valid worker startup into a
|
|
5052
|
+
// fleet outage.
|
|
5053
|
+
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
5054
|
+
}
|
|
4995
5055
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5056
|
+
else if (kind === "classic") {
|
|
5057
|
+
// #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
|
|
5058
|
+
// coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
|
|
5059
|
+
// room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
|
|
5060
|
+
// reads any bundled skill on demand.
|
|
5061
|
+
try {
|
|
5062
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
5063
|
+
if (insideManagedWorkspaces(skillsWorkDir))
|
|
5064
|
+
measureSyncWork("fleet.classicSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "classic"));
|
|
5065
|
+
else
|
|
5066
|
+
this.logger.debug({ name, skillsWorkDir }, "Classic workspace is not AgEnD-managed — no skills published");
|
|
5067
|
+
}
|
|
5068
|
+
catch (err) {
|
|
5069
|
+
this.logger.warn({ err, name, backend }, "Failed to sync classic skills — continuing startup");
|
|
5070
|
+
}
|
|
5008
5071
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5072
|
+
const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
|
|
5073
|
+
const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
|
|
5074
|
+
if (receipt) {
|
|
5075
|
+
await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
|
|
5076
|
+
// Only this transition's actual publication can acquire its replacement.
|
|
5077
|
+
receipt.daemon = daemon;
|
|
5078
|
+
});
|
|
5011
5079
|
}
|
|
5080
|
+
else
|
|
5081
|
+
await this.lifecycle.start(name, config, topicMode, identity, transition, execution);
|
|
5082
|
+
execution?.assert();
|
|
5083
|
+
// Only clear a stale process status after a real start succeeded. Clearing
|
|
5084
|
+
// it before lifecycle.start() can turn a crash-loop daemon's dead pane into
|
|
5085
|
+
// a falsely running instance when lifecycle.start() returns early.
|
|
5086
|
+
this.instanceProcessStatus.delete(name);
|
|
5087
|
+
// Recovery intent belongs to Daemon.start. lifecycle.start can return
|
|
5088
|
+
// before reaching it, so an await here is not proof the marker was read.
|
|
5089
|
+
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
5090
|
+
await this.connectIpcToInstance(name);
|
|
5091
|
+
execution?.assert();
|
|
5092
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
5093
|
+
});
|
|
5094
|
+
}
|
|
5095
|
+
/** A boot's observation cannot settle a replacement boot or a newer attempt of the same instance. */
|
|
5096
|
+
async trackStartupInstance(name, start) {
|
|
5097
|
+
const boot = this.startupDashboard;
|
|
5098
|
+
const owner = boot?.begin(name);
|
|
5099
|
+
if (owner)
|
|
5100
|
+
this.publishStartupStatus();
|
|
5101
|
+
let ready = false;
|
|
5102
|
+
try {
|
|
5103
|
+
await start();
|
|
5104
|
+
ready = this.daemons.has(name);
|
|
5012
5105
|
}
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5106
|
+
finally {
|
|
5107
|
+
if (boot && owner && this.startupDashboard === boot && boot.finish(name, owner, ready))
|
|
5108
|
+
this.publishStartupStatus();
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
publishStartupStatus() {
|
|
5112
|
+
try {
|
|
5113
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
5114
|
+
}
|
|
5115
|
+
catch (err) {
|
|
5116
|
+
this.logger.debug({ err }, "Startup status unavailable; the next status frame retries");
|
|
5020
5117
|
}
|
|
5021
|
-
else
|
|
5022
|
-
await this.lifecycle.start(name, config, topicMode, identity, transition, execution);
|
|
5023
|
-
execution?.assert();
|
|
5024
|
-
// Only clear a stale process status after a real start succeeded. Clearing
|
|
5025
|
-
// it before lifecycle.start() can turn a crash-loop daemon's dead pane into
|
|
5026
|
-
// a falsely running instance when lifecycle.start() returns early.
|
|
5027
|
-
this.instanceProcessStatus.delete(name);
|
|
5028
|
-
// Recovery intent belongs to Daemon.start. lifecycle.start can return
|
|
5029
|
-
// before reaching it, so an await here is not proof the marker was read.
|
|
5030
|
-
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
5031
|
-
await this.connectIpcToInstance(name);
|
|
5032
|
-
execution?.assert();
|
|
5033
|
-
this.requestDiscordUsagePresenceRefresh();
|
|
5034
5118
|
}
|
|
5035
5119
|
/** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
|
|
5036
5120
|
async startPersistedPausedInstance(name, transition) {
|
|
@@ -5972,6 +6056,11 @@ export class FleetManager {
|
|
|
5972
6056
|
revoke: id => {
|
|
5973
6057
|
this.webLoginCodes?.revokeAudience(id);
|
|
5974
6058
|
this.webGuestLoginCodes?.revokeAudience(id);
|
|
6059
|
+
// #2453 (§0.2): the link's sessions end, and the authorizations kept for them go with them (housekeeping: their
|
|
6060
|
+
// handles can never match again).
|
|
6061
|
+
const ended = new Set(this.webSessions?.list(undefined, r => r.exposureId === id).map(r => r.handle) ?? []);
|
|
6062
|
+
if (ended.size)
|
|
6063
|
+
this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
|
|
5975
6064
|
const result = this.webSessions?.revokeExposure(id);
|
|
5976
6065
|
if (result && !result.durable)
|
|
5977
6066
|
this.logger.warn("Public session revocation could not be saved; exposure remains closed");
|
|
@@ -5992,6 +6081,66 @@ export class FleetManager {
|
|
|
5992
6081
|
},
|
|
5993
6082
|
});
|
|
5994
6083
|
}
|
|
6084
|
+
publicLinkAutoOpen = null;
|
|
6085
|
+
/**
|
|
6086
|
+
* #named-link-ttl: the admin who last opened a named-tunnel link from /web, with the live binding — only while they
|
|
6087
|
+
* still may own one (publicOwnerCurrent: a fleet admin of a live bot, the same General). Else null.
|
|
6088
|
+
*/
|
|
6089
|
+
rememberedPublicOwner() {
|
|
6090
|
+
const saved = readPublicLinkOwner(this.dataDir);
|
|
6091
|
+
if (!saved)
|
|
6092
|
+
return null;
|
|
6093
|
+
const owner = { ...saved, binding: this.adapters.get(saved.adapterId) };
|
|
6094
|
+
return this.publicOwnerCurrent(owner) ? owner : null;
|
|
6095
|
+
}
|
|
6096
|
+
/** A named-tunnel link was privately delivered: auto_open reopens it for this owner after a restart. */
|
|
6097
|
+
rememberPublicLinkOwner(owner) {
|
|
6098
|
+
if (!publicLinkNamedHostname(this.fleetConfig?.web))
|
|
6099
|
+
return;
|
|
6100
|
+
try {
|
|
6101
|
+
writePublicLinkOwner(this.dataDir, owner);
|
|
6102
|
+
}
|
|
6103
|
+
catch (err) {
|
|
6104
|
+
this.logger.warn({ err: err?.message }, "Could not remember the public link's owner; auto_open will not reopen it");
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
/** Closed on purpose (Close, /web revoke): a restart must not reopen it. The next /web remembers again. */
|
|
6108
|
+
forgetPublicLinkOwner() {
|
|
6109
|
+
try {
|
|
6110
|
+
clearPublicLinkOwner(this.dataDir);
|
|
6111
|
+
}
|
|
6112
|
+
catch (err) {
|
|
6113
|
+
this.logger.warn({ err: err?.message }, "Could not forget the public link's owner");
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
/**
|
|
6117
|
+
* #named-link-ttl: `web.public_link.auto_open` — once per start, after the web listener and the chat connections are
|
|
6118
|
+
* up. Opens the named link with nothing posted (sign-in still needs a code from /web or a returning device); a final
|
|
6119
|
+
* failure goes to its owner once, by DM, drawn as a failed /web is.
|
|
6120
|
+
*/
|
|
6121
|
+
startPublicLinkAutoOpen() {
|
|
6122
|
+
this.publicLinkAutoOpen?.stop();
|
|
6123
|
+
const runner = this.publicLinkAutoOpen = new PublicLinkAutoOpen({
|
|
6124
|
+
web: () => this.fleetConfig?.web,
|
|
6125
|
+
ready: () => !this.shuttingDown && this.healthServerListening && this.publicLinkAutoOpen === runner,
|
|
6126
|
+
owner: () => this.rememberedPublicOwner(),
|
|
6127
|
+
open: (owner, onProgress) => this.getPublicWebLink().openUnattended(owner, onProgress),
|
|
6128
|
+
notifyFailed: async (owner, last) => {
|
|
6129
|
+
const adapter = owner.binding;
|
|
6130
|
+
if (this.shuttingDown || !adapter?.sendDirect || !this.publicOwnerCurrent(owner))
|
|
6131
|
+
return;
|
|
6132
|
+
const outcome = t("dashboard.public_auto_failed");
|
|
6133
|
+
try {
|
|
6134
|
+
await withinBudget(adapter.sendDirect(owner.userId, last ? renderPublicLinkProgress(last, performance.now(), outcome) : outcome, { disablePreview: true }), performance.now() + 5_000);
|
|
6135
|
+
}
|
|
6136
|
+
catch (err) {
|
|
6137
|
+
this.logDashboardDmFailure(err);
|
|
6138
|
+
}
|
|
6139
|
+
},
|
|
6140
|
+
log: (event, fields) => this.logger.info({ event, ...fields }, "Public web link"),
|
|
6141
|
+
});
|
|
6142
|
+
runner.start();
|
|
6143
|
+
}
|
|
5995
6144
|
getPublicWebStatus() {
|
|
5996
6145
|
return this.publicWebLink?.status() ?? { state: "closed" };
|
|
5997
6146
|
}
|
|
@@ -6175,8 +6324,9 @@ export class FleetManager {
|
|
|
6175
6324
|
// Show the action before any link exists. TG's Start instructions belong to confirmed DM refusal below.
|
|
6176
6325
|
const menuText = this.topicCommands.getDashboardMenuText()
|
|
6177
6326
|
+ (publicAllowed ? "\n" + t("dashboard.public_risk") : "")
|
|
6178
|
-
+ (status.state
|
|
6179
|
-
|
|
6327
|
+
+ (status.state === "closed" ? "" : status.noExpiry ? "\n" + t("dashboard.public_status_no_expiry", status.state)
|
|
6328
|
+
: "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
6329
|
+
+ (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
|
|
6180
6330
|
const choices = [{ action: "local", label: t("dashboard.local_signin") },
|
|
6181
6331
|
...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
|
|
6182
6332
|
...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
|
|
@@ -6223,6 +6373,7 @@ export class FleetManager {
|
|
|
6223
6373
|
if (!entry.publicExposureId)
|
|
6224
6374
|
return true;
|
|
6225
6375
|
const result = await this.publicWebLink?.close("admin close", entry.publicExposureId);
|
|
6376
|
+
this.forgetPublicLinkOwner();
|
|
6226
6377
|
if (result?.confirmed === false)
|
|
6227
6378
|
data.ack?.(t("dashboard.public_cleanup"));
|
|
6228
6379
|
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("dashboard.public_closed"), data.editClicked);
|
|
@@ -6253,10 +6404,11 @@ export class FleetManager {
|
|
|
6253
6404
|
return false;
|
|
6254
6405
|
this.initializeWebSessions();
|
|
6255
6406
|
const issued = this.webLoginCodes.issue({ epoch: tokenEpoch(token), audience: exposureId, owner });
|
|
6256
|
-
// Only a public link has a link expiry; the code's five-minute
|
|
6407
|
+
// Only a public link has a link expiry (null: none — a named tunnel's ttl_minutes 0); the code's five-minute
|
|
6408
|
+
// lifetime applies to either packet.
|
|
6257
6409
|
const minutes = Math.round(LOGIN_CODE_TTL_MS / 60_000);
|
|
6258
6410
|
const text = expiresAt !== undefined
|
|
6259
|
-
? t("dashboard.private_public", url, issued.display, minutes, formatFleetTime(expiresAt))
|
|
6411
|
+
? t("dashboard.private_public", url, issued.display, minutes, expiresAt === null ? t("dashboard.public_no_expiry") : formatFleetTime(expiresAt))
|
|
6260
6412
|
: t("dashboard.private_local", url, issued.display, minutes);
|
|
6261
6413
|
const deadline = performance.now() + 10_000;
|
|
6262
6414
|
const direct = owner.binding;
|
|
@@ -6319,6 +6471,8 @@ export class FleetManager {
|
|
|
6319
6471
|
// Still closes the step editor below and tells the clicker: a throw must not leave the menu refreshing.
|
|
6320
6472
|
this.logger.warn({ err }, "Dashboard sign-in delivery failed");
|
|
6321
6473
|
}
|
|
6474
|
+
if (ok && action === "public")
|
|
6475
|
+
this.rememberPublicLinkOwner(owner);
|
|
6322
6476
|
// Only safe, static words enter General; never link, code or platform error text.
|
|
6323
6477
|
// #1742: Telegram's failure walks through opening the DM (never a bare /start, which starts a ClassicBot agent),
|
|
6324
6478
|
// with the "先開私訊 →" button when the bot's username is known; and for a while a bare /start there counts as
|
|
@@ -6343,6 +6497,7 @@ export class FleetManager {
|
|
|
6343
6497
|
revokeWebSessions() {
|
|
6344
6498
|
this.initializeWebSessions();
|
|
6345
6499
|
void this.publicWebLink?.close("dashboard revoke");
|
|
6500
|
+
this.forgetPublicLinkOwner();
|
|
6346
6501
|
this.webLoginCodes.revoke();
|
|
6347
6502
|
this.webGuestLoginCodes.revokeAll();
|
|
6348
6503
|
this.guestRevokes++; // and every guest /web still on its way
|
|
@@ -6944,7 +7099,7 @@ export class FleetManager {
|
|
|
6944
7099
|
if (this.updateProgressTimer)
|
|
6945
7100
|
clearInterval(this.updateProgressTimer);
|
|
6946
7101
|
const tick = async () => {
|
|
6947
|
-
if (this.updateProgressEditRunning)
|
|
7102
|
+
if (this.updateProgressEditRunning || this.shuttingDown)
|
|
6948
7103
|
return;
|
|
6949
7104
|
const marker = readUpdateProgress(this.dataDir);
|
|
6950
7105
|
if (!marker) {
|
|
@@ -6995,6 +7150,11 @@ export class FleetManager {
|
|
|
6995
7150
|
return;
|
|
6996
7151
|
}
|
|
6997
7152
|
}
|
|
7153
|
+
// #2476: from "starting" onward RestartProgress in the NEW process owns this
|
|
7154
|
+
// message (resume → N/M → ✅ finish). A late CLI-stage edit of "installed" is
|
|
7155
|
+
// what made "✅ 安裝完成" land after "啟動中 N/M". Failure still edits here.
|
|
7156
|
+
if (marker.progress.stage === "starting")
|
|
7157
|
+
return;
|
|
6998
7158
|
const target = marker.progress.target;
|
|
6999
7159
|
const adapter = this.adapters.get(target.adapterId) ?? (initialAdapter?.id === target.adapterId ? initialAdapter : undefined);
|
|
7000
7160
|
if (!adapter)
|
|
@@ -7030,11 +7190,46 @@ export class FleetManager {
|
|
|
7030
7190
|
}
|
|
7031
7191
|
/** Start all instances from fleet config */
|
|
7032
7192
|
async startAll(configPath) {
|
|
7193
|
+
this.startupMemoryDeadline?.stop();
|
|
7194
|
+
const deadline = new StartupMemoryDeadline({
|
|
7195
|
+
current: () => this.startupMemoryDeadline === deadline && !this.shuttingDown,
|
|
7196
|
+
waiting: () => this.spawnGate.isStartupMemoryDeferred(),
|
|
7197
|
+
notify: sdNotify,
|
|
7198
|
+
log: message => this.logger.info(message),
|
|
7199
|
+
});
|
|
7200
|
+
this.startupMemoryDeadline = deadline;
|
|
7201
|
+
// A held first General must not postpone event-loop liveness monitoring.
|
|
7202
|
+
// No independent heartbeat: a genuinely frozen main loop still stops pinging.
|
|
7203
|
+
setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
|
|
7204
|
+
if (this.watchdogTimer)
|
|
7205
|
+
clearInterval(this.watchdogTimer);
|
|
7206
|
+
this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
|
|
7207
|
+
this.eventLoopWatch?.stop();
|
|
7208
|
+
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
7209
|
+
try {
|
|
7210
|
+
await this.startAllWithinDeadline(configPath, deadline);
|
|
7211
|
+
}
|
|
7212
|
+
finally {
|
|
7213
|
+
deadline.stop();
|
|
7214
|
+
if (this.startupMemoryDeadline === deadline) {
|
|
7215
|
+
this.startupMemoryDeadline = null;
|
|
7216
|
+
if (!this.startupComplete) {
|
|
7217
|
+
if (this.watchdogTimer)
|
|
7218
|
+
clearInterval(this.watchdogTimer);
|
|
7219
|
+
this.watchdogTimer = null;
|
|
7220
|
+
this.eventLoopWatch?.stop();
|
|
7221
|
+
this.eventLoopWatch = null;
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
}
|
|
7226
|
+
async startAllWithinDeadline(configPath, deadline) {
|
|
7033
7227
|
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
7034
7228
|
this.loginController?.reopen();
|
|
7035
7229
|
const startupStartedAt = Date.now();
|
|
7036
7230
|
FleetManager.signalTarget = this;
|
|
7037
7231
|
this.startupComplete = false;
|
|
7232
|
+
this.startupDashboard = null;
|
|
7038
7233
|
// Cleared here, not at the end of doStopAll: a stop has an async tail, and
|
|
7039
7234
|
// anything arriving during it is still part of the stop.
|
|
7040
7235
|
this.shuttingDown = false;
|
|
@@ -7286,7 +7481,7 @@ export class FleetManager {
|
|
|
7286
7481
|
continue;
|
|
7287
7482
|
this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
|
|
7288
7483
|
const generalDir = join(getAgendHome(), name);
|
|
7289
|
-
mkdirSync(generalDir, { recursive: true });
|
|
7484
|
+
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
7290
7485
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
7291
7486
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
7292
7487
|
fleet.instances[name] = {
|
|
@@ -7337,6 +7532,13 @@ export class FleetManager {
|
|
|
7337
7532
|
const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
7338
7533
|
const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
|
|
7339
7534
|
this.startupProgress = startupProgress;
|
|
7535
|
+
// Auth, restored chat, Classic roster and the task DB are ready. Listening is not service readiness:
|
|
7536
|
+
// the dashboard observes startup, while /health and READY retain their existing completion boundary.
|
|
7537
|
+
this.startupDashboard = new FleetStartup(this.configuredStartupInstanceNames(fleet, topicMode)
|
|
7538
|
+
.filter(name => !this.lifecycle.isPaused(name)));
|
|
7539
|
+
await this.startHealthServer(fleet.health_port ?? 19280);
|
|
7540
|
+
if (this.shuttingDown)
|
|
7541
|
+
return;
|
|
7340
7542
|
if (generals.length > 0) {
|
|
7341
7543
|
for (const [name, cfg] of generals) {
|
|
7342
7544
|
try {
|
|
@@ -7401,22 +7603,6 @@ export class FleetManager {
|
|
|
7401
7603
|
return startupProgress.start(this.restartProgressTarget());
|
|
7402
7604
|
});
|
|
7403
7605
|
}
|
|
7404
|
-
// The systemd watchdog answers exactly one question: is this process still
|
|
7405
|
-
// turning its event loop? Pinging from a timer proves that, and after the
|
|
7406
|
-
// blocking child-process calls were made async it is a meaningful signal —
|
|
7407
|
-
// a deadlocked or frozen fleet stops pinging and systemd restarts it.
|
|
7408
|
-
//
|
|
7409
|
-
// It deliberately does NOT gate on fleet health. "No adapter connected" or
|
|
7410
|
-
// "an instance crashed" must not kill the process: the fleet would be restarted
|
|
7411
|
-
// into the same broken state, and a user who has legitimately stopped every
|
|
7412
|
-
// instance would get a restart loop. Those conditions surface through /health
|
|
7413
|
-
// (which now returns 503) and through the General-topic notifications instead.
|
|
7414
|
-
setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
|
|
7415
|
-
this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
|
|
7416
|
-
// #1231: a stalled loop is what turns a slash command into "did not respond";
|
|
7417
|
-
// put each long stall in the log instead of leaving it to be guessed at.
|
|
7418
|
-
this.eventLoopWatch?.stop();
|
|
7419
|
-
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
7420
7606
|
// EventLog.prune() existed but was never called, so `events` and `activity`
|
|
7421
7607
|
// grew without bound for the life of the install. Prune once at startup and
|
|
7422
7608
|
// daily after that; the timer is unref'd so it never holds the loop open.
|
|
@@ -7592,9 +7778,9 @@ export class FleetManager {
|
|
|
7592
7778
|
}
|
|
7593
7779
|
// After "fleet ready", so the notice is not the first thing people see of a restart.
|
|
7594
7780
|
void this.announceWebChatOnce(agendVersion);
|
|
7781
|
+
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
7782
|
+
this.startPublicLinkAutoOpen();
|
|
7595
7783
|
}
|
|
7596
|
-
// Health HTTP endpoint
|
|
7597
|
-
this.startHealthServer(fleet.health_port ?? 19280);
|
|
7598
7784
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
7599
7785
|
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
7600
7786
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
@@ -7611,12 +7797,26 @@ export class FleetManager {
|
|
|
7611
7797
|
// A SIGHUP may arrive after the PID/general is available but before the
|
|
7612
7798
|
// rest of startup finishes. Replay one coalesced reload only after all
|
|
7613
7799
|
// startup-owned lifecycle work and signal handlers are in place.
|
|
7800
|
+
if (this.shuttingDown || this.startupMemoryDeadline !== deadline)
|
|
7801
|
+
return;
|
|
7614
7802
|
this.finishStartup(bootUpdateMarker);
|
|
7803
|
+
this.startupDashboard = null;
|
|
7804
|
+
this.publishStartupStatus();
|
|
7615
7805
|
// Tell systemd we are ready only now. This used to fire right after the
|
|
7616
7806
|
// generals started — before adapters, classic instances, topic creation and the
|
|
7617
7807
|
// health server — so `systemctl start` returned success while the fleet was
|
|
7618
7808
|
// still deaf: no path existed for a user message to arrive.
|
|
7619
7809
|
sdNotify("READY=1");
|
|
7810
|
+
// #2481: this version runs — the CLI's start-failure banner stops, and this is the version to go back to later.
|
|
7811
|
+
try {
|
|
7812
|
+
const { ownPackage, recordStarted } = await import("./start-failure.js");
|
|
7813
|
+
const version = ownPackage().version;
|
|
7814
|
+
if (version)
|
|
7815
|
+
recordStarted(this.dataDir, version, process.pid, Date.now());
|
|
7816
|
+
}
|
|
7817
|
+
catch (err) {
|
|
7818
|
+
this.logger.debug({ err }, "fleet-started record not written");
|
|
7819
|
+
}
|
|
7620
7820
|
const health = this.getFleetHealth();
|
|
7621
7821
|
if (health.status !== "ok") {
|
|
7622
7822
|
this.logger.warn({ health }, "Fleet started with problems — see /health");
|
|
@@ -8035,12 +8235,12 @@ export class FleetManager {
|
|
|
8035
8235
|
return;
|
|
8036
8236
|
}
|
|
8037
8237
|
const accessDir = join(this.dataDir, "access");
|
|
8038
|
-
mkdirSync(accessDir, { recursive: true });
|
|
8238
|
+
mkdirSync(accessDir, { recursive: true, mode: 0o700 });
|
|
8039
8239
|
const accessStatePath = join(accessDir, "access.json");
|
|
8040
8240
|
const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
|
|
8041
8241
|
this.warnIfAccessModeOverridden(accessManager, accessStatePath);
|
|
8042
8242
|
const inboxDir = join(this.dataDir, "inbox");
|
|
8043
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
8243
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
8044
8244
|
const adapterId = channelConfig.id ?? channelConfig.type;
|
|
8045
8245
|
const adapter = await createAdapter(channelConfig, {
|
|
8046
8246
|
id: adapterId,
|
|
@@ -8186,12 +8386,12 @@ export class FleetManager {
|
|
|
8186
8386
|
return;
|
|
8187
8387
|
}
|
|
8188
8388
|
const accessDir = join(this.dataDir, "access");
|
|
8189
|
-
mkdirSync(accessDir, { recursive: true });
|
|
8389
|
+
mkdirSync(accessDir, { recursive: true, mode: 0o700 });
|
|
8190
8390
|
const accessStatePath = join(accessDir, `access-${adapterId}.json`);
|
|
8191
8391
|
const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
|
|
8192
8392
|
this.warnIfAccessModeOverridden(accessManager, accessStatePath);
|
|
8193
8393
|
const inboxDir = join(this.dataDir, "inbox");
|
|
8194
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
8394
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
8195
8395
|
const adapter = await createAdapter(channelConfig, {
|
|
8196
8396
|
id: adapterId,
|
|
8197
8397
|
botToken,
|
|
@@ -9623,9 +9823,14 @@ export class FleetManager {
|
|
|
9623
9823
|
* history (#1565). #1920 §5.2: said in the instance's topic, General or room, so it is that channel's, by that account.
|
|
9624
9824
|
*/
|
|
9625
9825
|
emitPlatformInbound(instance, msg, text) {
|
|
9826
|
+
// #2281: where it is on the platform — a web reply to it is then a native reply there. The platform thread it came
|
|
9827
|
+
// from (#1085: a ClassicBot's `threadId` may be its registry key).
|
|
9828
|
+
const thread = msg.transportThreadId ?? msg.threadId;
|
|
9829
|
+
const posts = msg.adapterId && msg.chatId && msg.messageId
|
|
9830
|
+
? [{ adapterId: msg.adapterId, chatId: msg.chatId, ...(thread && thread !== msg.chatId ? { threadId: thread } : {}), messageId: msg.messageId }] : undefined;
|
|
9626
9831
|
this.emitSseEvent("message", {
|
|
9627
9832
|
instance, sender: msg.username, role: "user", text: text.slice(0, WEB_CHAT_TEXT_MAX), ts: new Date().toISOString(),
|
|
9628
|
-
channel: true, from: webChatFrom(msg.adapterId, msg.userId),
|
|
9833
|
+
channel: true, from: webChatFrom(msg.adapterId, msg.userId), ...(posts ? { posts } : {}),
|
|
9629
9834
|
});
|
|
9630
9835
|
}
|
|
9631
9836
|
/** Handle outbound tool calls from a daemon instance */
|
|
@@ -9735,6 +9940,13 @@ export class FleetManager {
|
|
|
9735
9940
|
this.logger.warn({ sink, instance: instanceName, profile, tool, enforced: true }, "tool-permissions: refused");
|
|
9736
9941
|
return { allowed: false, message: toolRefusedMessage(profile, tool) };
|
|
9737
9942
|
}
|
|
9943
|
+
// The person's dashboard switches (dashboard-permissions.ts), on top of the profile, as configured now.
|
|
9944
|
+
// Its own switches, else the defaults' — a ClassicBot identity has no instances entry and runs on the defaults (#2391 review).
|
|
9945
|
+
const switchedOff = dashboardRefusalIn(this.fleetConfig, instanceName, tool);
|
|
9946
|
+
if (switchedOff) {
|
|
9947
|
+
this.logger.warn({ sink, instance: instanceName, profile, tool, enforced: true, dashboard: true }, "tool-permissions: refused");
|
|
9948
|
+
return { allowed: false, message: switchedOff };
|
|
9949
|
+
}
|
|
9738
9950
|
this.logger.debug({ sink, instance: instanceName, profile, tool, enforced: true }, "tool-permissions: allowed");
|
|
9739
9951
|
return { allowed: true, message: "" };
|
|
9740
9952
|
}
|
|
@@ -9742,6 +9954,9 @@ export class FleetManager {
|
|
|
9742
9954
|
this.touchActivity(instanceName);
|
|
9743
9955
|
this.setTopicIcon(instanceName, "green");
|
|
9744
9956
|
const tool = msg.tool;
|
|
9957
|
+
// #1774: who sends this — taken before anything here awaits (sticker checks, the platform send): a reply that waits
|
|
9958
|
+
// must never be read as a same-name replacement's (removed and re-registered meanwhile).
|
|
9959
|
+
const sentBy = tool === "reply" ? this.webChatIdentity(instanceName) : undefined;
|
|
9745
9960
|
const args = (msg.args ?? {});
|
|
9746
9961
|
const requestId = msg.requestId;
|
|
9747
9962
|
const fleetRequestId = msg.fleetRequestId;
|
|
@@ -9921,6 +10136,7 @@ export class FleetManager {
|
|
|
9921
10136
|
args.text = `${String(args.text)}\n\n${replyButtonsFallbackText(parsedButtons.buttons)}`;
|
|
9922
10137
|
}
|
|
9923
10138
|
}
|
|
10139
|
+
const replyIdentity = sentBy; // #1774: taken at the handler's entry
|
|
9924
10140
|
const original = respond;
|
|
9925
10141
|
const respondAndRecord = (result, error) => {
|
|
9926
10142
|
let buttonsView = null;
|
|
@@ -9952,12 +10168,13 @@ export class FleetManager {
|
|
|
9952
10168
|
try {
|
|
9953
10169
|
// #1920 §5.2: shown to the instance's guests only when it landed in the instance's own topic or room.
|
|
9954
10170
|
let inChannel = false;
|
|
10171
|
+
const replyAdapterId = contextAdapterId ?? this.getInstanceAdapterId(instanceName);
|
|
9955
10172
|
try {
|
|
9956
10173
|
inChannel = outAdapter !== WEB_ONLY_REPLY_SINK
|
|
9957
|
-
&& this.landedInOwnChannel(instanceName,
|
|
10174
|
+
&& this.landedInOwnChannel(instanceName, replyAdapterId, result);
|
|
9958
10175
|
}
|
|
9959
10176
|
catch { /* not known: not the channel's (fail closed), and the bookkeeping below still runs */ }
|
|
9960
|
-
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView, inChannel);
|
|
10177
|
+
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView, inChannel, replyAdapterId ? { adapterId: replyAdapterId, result } : undefined, replyIdentity);
|
|
9961
10178
|
}
|
|
9962
10179
|
catch (err) {
|
|
9963
10180
|
this.logger.warn({ err, instanceName }, "Reply delivered but post-delivery bookkeeping failed");
|
|
@@ -10034,7 +10251,12 @@ export class FleetManager {
|
|
|
10034
10251
|
return false;
|
|
10035
10252
|
}
|
|
10036
10253
|
}
|
|
10037
|
-
afterReplyRouted(instanceName, args, senderSessionName, buttons = null, inChannel = false) {
|
|
10254
|
+
afterReplyRouted(instanceName, args, senderSessionName, buttons = null, inChannel = false, sentAs, identity = this.webChatIdentity(instanceName)) {
|
|
10255
|
+
// #2281: where the reply is, when it landed in the instance's own place — a web reply to it is then a native reply
|
|
10256
|
+
// there. A reply elsewhere (another chat, the web-only sink) records none.
|
|
10257
|
+
const sent = (sentAs?.result && typeof sentAs.result === "object" ? sentAs.result : {});
|
|
10258
|
+
const post = inChannel && sentAs && typeof sent.messageId === "string" && typeof sent.chatId === "string"
|
|
10259
|
+
? { adapterId: sentAs.adapterId, chatId: sent.chatId, ...(typeof sent.threadId === "string" && sent.threadId ? { threadId: sent.threadId } : {}), messageId: sent.messageId } : undefined;
|
|
10038
10260
|
// A reply is NOT proof the turn is over (#410) — but it is not proof of
|
|
10039
10261
|
// more work either. Split the difference: an instance that is clearly
|
|
10040
10262
|
// idle loses the button now; one that looks busy keeps it (re-posted
|
|
@@ -10061,7 +10283,16 @@ export class FleetManager {
|
|
|
10061
10283
|
// copies are done, and an instance's replies wait for the one before them: they reach the chat in order.
|
|
10062
10284
|
const replyFiles = Array.isArray(args.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
10063
10285
|
const ts = new Date().toISOString();
|
|
10064
|
-
|
|
10286
|
+
// #1774: the instance this reply is for — removed while its files were being copied (a same-name replacement
|
|
10287
|
+
// registered meanwhile included), the reply is no longer its to show: no chat made again, none of the new one's
|
|
10288
|
+
// touched, and the copies made for it go. A replacement's own replies are its own.
|
|
10289
|
+
// Its identity was taken when the reply was sent (identity), never re-read after the platform answered.
|
|
10290
|
+
const current = () => this.webChatIdentityCurrent(instanceName, identity);
|
|
10291
|
+
const show = (attachments, reserved) => {
|
|
10292
|
+
if (!current()) {
|
|
10293
|
+
this.webFiles.dropServed(attachments.map(a => a.id));
|
|
10294
|
+
return;
|
|
10295
|
+
}
|
|
10065
10296
|
// The one place a delivered agent reply reaches the web chat: the server marks it `agent` (#1306) — the only
|
|
10066
10297
|
// role that may get HTML preview cards. Never inferred from the sender name or the text.
|
|
10067
10298
|
this.emitSseEvent("message", {
|
|
@@ -10071,18 +10302,25 @@ export class FleetManager {
|
|
|
10071
10302
|
...(attachments.length ? { attachments } : {}),
|
|
10072
10303
|
...(buttons ? { buttons } : {}), // #1266
|
|
10073
10304
|
...(inChannel ? { channel: true } : {}), // #1920 §5.2
|
|
10305
|
+
...(post ? { posts: [post] } : {}), // #2281
|
|
10306
|
+
...(reserved !== undefined ? { reserved } : {}), // #1774
|
|
10074
10307
|
});
|
|
10075
10308
|
};
|
|
10076
10309
|
const before = this.webReplyOrder.get(instanceName);
|
|
10077
|
-
|
|
10310
|
+
// #1774: a removal of this instance still going decides whether a reply sent before it began is shown — it waits.
|
|
10311
|
+
const removal = this.webChatRemovalPending(instanceName, identity);
|
|
10312
|
+
if (!replyFiles.length && !before && !removal) {
|
|
10078
10313
|
show([]);
|
|
10079
10314
|
return Promise.resolve();
|
|
10080
10315
|
}
|
|
10316
|
+
// #1774: its place in the chat is now — a person's message sent while the files are copied comes after it.
|
|
10317
|
+
const reserved = this.webChatHistory.reserve();
|
|
10081
10318
|
const done = (before ?? Promise.resolve())
|
|
10082
|
-
.then(() => this.webReplyAttachments(instanceName, replyFiles))
|
|
10319
|
+
.then(() => this.webReplyAttachments(instanceName, replyFiles, current))
|
|
10320
|
+
.then(async (attachments) => { await this.webChatRemovalPending(instanceName, identity); return attachments; })
|
|
10083
10321
|
.catch((err) => { this.logger.warn({ err, instanceName }, "Reply files could not be prepared for the web chat"); return []; })
|
|
10084
10322
|
.then((attachments) => { try {
|
|
10085
|
-
show(attachments);
|
|
10323
|
+
show(attachments, reserved);
|
|
10086
10324
|
}
|
|
10087
10325
|
catch (err) {
|
|
10088
10326
|
this.logger.warn({ err, instanceName }, "Reply could not be shown in the web chat");
|
|
@@ -10097,11 +10335,13 @@ export class FleetManager {
|
|
|
10097
10335
|
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
10098
10336
|
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
10099
10337
|
*/
|
|
10100
|
-
async webReplyAttachments(instanceName, paths) {
|
|
10338
|
+
async webReplyAttachments(instanceName, paths, current = () => true) {
|
|
10101
10339
|
const home = getAgendHome();
|
|
10102
10340
|
const dir = webFilesDir(home, instanceName);
|
|
10103
10341
|
const out = [];
|
|
10104
10342
|
for (const path of paths) {
|
|
10343
|
+
if (!current())
|
|
10344
|
+
break; // #1774: removed meanwhile — copy no more for it
|
|
10105
10345
|
const id = randomBytes(16).toString("hex");
|
|
10106
10346
|
const got = dir ? await snapshotReplyFile(path, dir, id, { stateDir: home, maxDirBytes: this.webReplyMaxBytes }) : null;
|
|
10107
10347
|
if (got && "evicted" in got && got.evicted.length)
|
|
@@ -11353,6 +11593,7 @@ export class FleetManager {
|
|
|
11353
11593
|
saveFleetConfig(explicitPatches = []) {
|
|
11354
11594
|
this.publicWebLink?.refresh();
|
|
11355
11595
|
this.enforceGuestSwitch(); // #1920: a Settings save turning guests off ends their sessions
|
|
11596
|
+
this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
|
|
11356
11597
|
if (!this.fleetConfig || !this.configPath)
|
|
11357
11598
|
return;
|
|
11358
11599
|
if (!this.savedFleetConfigSnapshot)
|
|
@@ -11643,10 +11884,27 @@ export class FleetManager {
|
|
|
11643
11884
|
// Capture instance dir BEFORE lifecycle.remove, which deletes the instance
|
|
11644
11885
|
// from fleetConfig. (The backend would be unresolvable afterwards.)
|
|
11645
11886
|
const instanceDirToClean = this.getInstanceDir(name);
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11887
|
+
// #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
|
|
11888
|
+
// sent before wait for the outcome before they are shown.
|
|
11889
|
+
const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
|
|
11890
|
+
this.webChatGenerations.set(name, removedBelow);
|
|
11891
|
+
let settleRemoval;
|
|
11892
|
+
const removalPending = { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}), done: new Promise(r => { settleRemoval = r; }) };
|
|
11893
|
+
this.webChatRemovalsPending.set(name, removalPending);
|
|
11894
|
+
try {
|
|
11895
|
+
if (execution)
|
|
11896
|
+
await this.lifecycle.remove(name, authorization, execution, transition);
|
|
11897
|
+
else
|
|
11898
|
+
await this.lifecycle.remove(name, authorization);
|
|
11899
|
+
// #1774: removed — its replies still copying their files are no one's to show, replaced or not (a failed removal
|
|
11900
|
+
// throws above: its replies stay its own).
|
|
11901
|
+
this.webChatRemovals.set(name, { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}) });
|
|
11902
|
+
}
|
|
11903
|
+
finally {
|
|
11904
|
+
if (this.webChatRemovalsPending.get(name) === removalPending)
|
|
11905
|
+
this.webChatRemovalsPending.delete(name);
|
|
11906
|
+
settleRemoval();
|
|
11907
|
+
}
|
|
11650
11908
|
// A later same-name registration owns its files and caches.
|
|
11651
11909
|
// Own keys only: an instance named "constructor" is removed as any other (its inherited namesake is not a
|
|
11652
11910
|
// re-registration) — #1677 review.
|
|
@@ -12342,7 +12600,7 @@ export class FleetManager {
|
|
|
12342
12600
|
/** A preview under the inbox, named by a hash of the id (a Telegram file_id is long), pruned after a day. */
|
|
12343
12601
|
storeStickerPreview(id, picture) {
|
|
12344
12602
|
const dir = join(this.dataDir, "inbox", "sticker-previews");
|
|
12345
|
-
mkdirSync(dir, { recursive: true });
|
|
12603
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
12346
12604
|
const now = Date.now();
|
|
12347
12605
|
for (const name of readdirSync(dir)) {
|
|
12348
12606
|
try {
|
|
@@ -12408,7 +12666,7 @@ export class FleetManager {
|
|
|
12408
12666
|
if (!/^\d{15,25}$/.test(id))
|
|
12409
12667
|
throw new Error("not a Discord emoji id");
|
|
12410
12668
|
const dir = join(this.dataDir, "inbox", "emoji-previews");
|
|
12411
|
-
mkdirSync(dir, { recursive: true });
|
|
12669
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
12412
12670
|
const now = Date.now();
|
|
12413
12671
|
for (const name of readdirSync(dir)) {
|
|
12414
12672
|
try {
|
|
@@ -13043,7 +13301,7 @@ export class FleetManager {
|
|
|
13043
13301
|
const board = this.scheduler?.db.board;
|
|
13044
13302
|
if (!board)
|
|
13045
13303
|
return;
|
|
13046
|
-
const read = board.summaryRead(since.toISOString(), until.toISOString());
|
|
13304
|
+
const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
|
|
13047
13305
|
const projects = board.listProjects(true);
|
|
13048
13306
|
const stats = board.projectStats();
|
|
13049
13307
|
const names = new Map(projects.map((p) => [p.id, p.name]));
|
|
@@ -13823,9 +14081,9 @@ export class FleetManager {
|
|
|
13823
14081
|
return classic ? { chatId: classic } : null;
|
|
13824
14082
|
}
|
|
13825
14083
|
/**
|
|
13826
|
-
* #2352: the instance whose own channel lists a waiting card —
|
|
13827
|
-
*
|
|
13828
|
-
*
|
|
14084
|
+
* #2352: the instance whose own channel lists a waiting card — its project's lead — when that channel can be used now
|
|
14085
|
+
* (a fleet instance of a live world, with a topic or room). undefined: its assignee's world's General, as before.
|
|
14086
|
+
* #2434: only a card the person handed out waits for them, so the instance that handed one out is never its lane.
|
|
13829
14087
|
*/
|
|
13830
14088
|
needsYouLane(card, leads) {
|
|
13831
14089
|
const usable = (name) => {
|
|
@@ -13834,9 +14092,6 @@ export class FleetManager {
|
|
|
13834
14092
|
const world = this.getInstanceAdapterId(name);
|
|
13835
14093
|
return world !== undefined && this.worlds.has(world) && this.instanceChannelTarget(name) !== null;
|
|
13836
14094
|
};
|
|
13837
|
-
const dispatcher = card.dispatcher?.kind === "instance" && card.dispatcher.name !== card.assignee ? card.dispatcher.name : null;
|
|
13838
|
-
if (dispatcher)
|
|
13839
|
-
return usable(dispatcher) ? dispatcher : undefined;
|
|
13840
14095
|
const lead = card.projectId != null ? leads.get(card.projectId) : null;
|
|
13841
14096
|
return usable(lead) ? lead : undefined;
|
|
13842
14097
|
}
|
|
@@ -13844,12 +14099,24 @@ export class FleetManager {
|
|
|
13844
14099
|
* #2355 review: where a card's Needs you buttons belong now — read from the board and the fleet at the moment of a click
|
|
13845
14100
|
* or a reply, never from the last render. null: it no longer waits for the person.
|
|
13846
14101
|
*/
|
|
14102
|
+
/**
|
|
14103
|
+
* #2425 follow-up: an instance that handed out a card and is no longer in the fleet — neither a configured instance nor
|
|
14104
|
+
* a ClassicBot identity (deleted). Its blocked and in-review cards fall back to the person (Needs you, the summary).
|
|
14105
|
+
* Not known before the config and the ClassicBot rooms are loaded: then no one is gone (a live dispatcher's cards never
|
|
14106
|
+
* flash into the person's list at startup).
|
|
14107
|
+
*/
|
|
14108
|
+
dispatcherGone = (name) => {
|
|
14109
|
+
const instances = this.fleetConfig?.instances, classic = this.classicChannels;
|
|
14110
|
+
if (!instances || !classic)
|
|
14111
|
+
return false;
|
|
14112
|
+
return !Object.hasOwn(instances, name) && !classic.getAll().some((ch) => ch.instanceName === name);
|
|
14113
|
+
};
|
|
13847
14114
|
needsYouRoute(code) {
|
|
13848
14115
|
const board = this.scheduler?.db.board;
|
|
13849
14116
|
const card = board?.findCard(code);
|
|
13850
14117
|
// #2425 (Fable): the same rule as the list — a button rendered before the fix (or before the opt-in went off) for a
|
|
13851
14118
|
// card that no longer waits for the person answers "no longer waits".
|
|
13852
|
-
if (!board || !card || this.fleetConfig?.needs_you?.task_board !== true || !waitsForPerson(card))
|
|
14119
|
+
if (!board || !card || this.fleetConfig?.needs_you?.task_board !== true || !waitsForPerson(card, this.dispatcherGone))
|
|
13853
14120
|
return null;
|
|
13854
14121
|
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13855
14122
|
const lane = this.needsYouLane(card, leads);
|
|
@@ -13857,7 +14124,7 @@ export class FleetManager {
|
|
|
13857
14124
|
}
|
|
13858
14125
|
/**
|
|
13859
14126
|
* #2246 step 3: the task board's cards that wait for the person, for Needs you — under each card's assignee, listed in
|
|
13860
|
-
* its lane's channel (#2352: the instance that handed it out
|
|
14127
|
+
* its lane's channel (#2352: its project's lead; #2434: never the instance that handed it out), else in General.
|
|
13861
14128
|
*/
|
|
13862
14129
|
needsYouTaskItems() {
|
|
13863
14130
|
// #2425: the task board's cards join Needs you only when the fleet asks for them (needs_you.task_board, off by
|
|
@@ -13869,7 +14136,7 @@ export class FleetManager {
|
|
|
13869
14136
|
if (!board)
|
|
13870
14137
|
return [];
|
|
13871
14138
|
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13872
|
-
return board.waitingForPerson().map(({ card, since }) => {
|
|
14139
|
+
return board.waitingForPerson(this.dispatcherGone).map(({ card, since }) => {
|
|
13873
14140
|
const gap = card.status === "review" ? wrapUpGap(card) : null;
|
|
13874
14141
|
const lane = this.needsYouLane(card, leads);
|
|
13875
14142
|
return {
|
|
@@ -14304,7 +14571,7 @@ export class FleetManager {
|
|
|
14304
14571
|
...(input.instance !== undefined ? { target: { adapterId: this.getInstanceAdapterId(input.instance) } } : {}),
|
|
14305
14572
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14306
14573
|
authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
|
|
14307
|
-
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now: Date.now(),
|
|
14574
|
+
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now: Date.now(),
|
|
14308
14575
|
});
|
|
14309
14576
|
}
|
|
14310
14577
|
sessionAuthorizations = null;
|
|
@@ -14329,7 +14596,7 @@ export class FleetManager {
|
|
|
14329
14596
|
session, gateway, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
14330
14597
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14331
14598
|
authorization: { handle: session.handle, owner: session.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
|
|
14332
|
-
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now,
|
|
14599
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now,
|
|
14333
14600
|
});
|
|
14334
14601
|
if (check.refusal)
|
|
14335
14602
|
return { ok: false, refusal: check.refusal };
|
|
@@ -14344,9 +14611,30 @@ export class FleetManager {
|
|
|
14344
14611
|
*/
|
|
14345
14612
|
webAuthorityState() {
|
|
14346
14613
|
if (!this.dataDir)
|
|
14347
|
-
return { enabled: false, loopbackOnly: true, epoch: 0 };
|
|
14614
|
+
return { enabled: false, loopbackOnly: true, namedLink: false, epoch: 0 };
|
|
14348
14615
|
this.authorityStateStore ??= new AuthorityStateStore(join(this.dataDir, "web-authority.json"), this.logger, undefined, undefined, () => { this.ensureDeliveryOutbox?.(); return this.deliveryOutbox?.maxWebAuthorityEpoch() ?? 0; });
|
|
14349
|
-
|
|
14616
|
+
const state = this.authorityStateStore.current(this.fleetConfig?.web?.verified_authority);
|
|
14617
|
+
// #2453 (§0.2): the named-link option seen going off — the authority of every session over the link ends now, for
|
|
14618
|
+
// good: their authorizations go (a later "on" can never bring one back without its account's Confirm).
|
|
14619
|
+
if (state.namedLinkClosed)
|
|
14620
|
+
this.sessionAuthorizationStore()?.prune(a => this.webSessions?.findByHandle(a.handle)?.surface === "local");
|
|
14621
|
+
return state;
|
|
14622
|
+
}
|
|
14623
|
+
/**
|
|
14624
|
+
* #2453 review: the web-authority state read on every configuration change (a Settings save, the host's confirm, a
|
|
14625
|
+
* reload) — so a switch turned off and on again between two requests is still seen off: its epoch moves, and the
|
|
14626
|
+
* named-link sessions' authorizations are dropped. Reads that only happen on a request would miss it.
|
|
14627
|
+
*/
|
|
14628
|
+
observeWebAuthority() {
|
|
14629
|
+
// Only once the state is in use: any authorization was given through a read that made it (and a first read at
|
|
14630
|
+
// start-up still sees what changed while the fleet was down). Nothing new is opened during a load.
|
|
14631
|
+
try {
|
|
14632
|
+
if (this.dataDir && this.authorityStateStore)
|
|
14633
|
+
this.webAuthorityState();
|
|
14634
|
+
}
|
|
14635
|
+
catch (err) {
|
|
14636
|
+
this.logger.warn({ err: err?.message }, "Web authority state could not be read after a configuration change");
|
|
14637
|
+
}
|
|
14350
14638
|
}
|
|
14351
14639
|
/**
|
|
14352
14640
|
* #1939 S2a: Settings → Web → Web authority — the state in force (the switch counts as off while its state cannot be
|
|
@@ -14361,8 +14649,10 @@ export class FleetManager {
|
|
|
14361
14649
|
const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
|
|
14362
14650
|
.filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
|
|
14363
14651
|
.map(a => ({ handle: a.handle, label: sessions.get(a.handle).label, account: `${a.owner.adapterId}:${a.owner.userId}`, authorizedAt: a.authorizedAt,
|
|
14364
|
-
until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry)
|
|
14365
|
-
|
|
14652
|
+
until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry),
|
|
14653
|
+
...(sessions.get(a.handle).surface === "gateway" ? { named: true } : {}) })) : [];
|
|
14654
|
+
const host = this.fleetConfig?.web?.public_link?.hostname;
|
|
14655
|
+
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null };
|
|
14366
14656
|
}
|
|
14367
14657
|
authorizationRequests = null;
|
|
14368
14658
|
/**
|
|
@@ -14375,7 +14665,9 @@ export class FleetManager {
|
|
|
14375
14665
|
const adapterOf = (adapterId) => this.adapters.get(adapterId) ?? this.worlds.get(adapterId)?.adapter;
|
|
14376
14666
|
// The host's clock, with its zone: the person may read the DM on a phone in another one.
|
|
14377
14667
|
const when = (ms) => new Date(ms).toLocaleString(undefined, { timeZoneName: "short" });
|
|
14378
|
-
const where = (peer) =>
|
|
14668
|
+
const where = (peer) => peer === "named_link"
|
|
14669
|
+
? t("web_auth.peer_named_link", String(this.fleetConfig?.web?.public_link?.hostname ?? "?")) // §0.2: the internet
|
|
14670
|
+
: t(peer === "loopback" ? "web_auth.peer_loopback" : "web_auth.peer_lan");
|
|
14379
14671
|
return this.authorizationRequests = new SessionAuthorizationRequests({
|
|
14380
14672
|
now: () => performance.now(), wallNow: () => Date.now(),
|
|
14381
14673
|
maxSessionMs: () => webAuthorityPolicy(this.fleetConfig?.web).maxSessionMs,
|
|
@@ -14409,7 +14701,8 @@ export class FleetManager {
|
|
|
14409
14701
|
const record = this.webSessions?.findByHandle(handle);
|
|
14410
14702
|
if (!record)
|
|
14411
14703
|
return { ok: false, refusal: "session_gone" };
|
|
14412
|
-
|
|
14704
|
+
// §0.2: a named-link session is judged as the gateway request it is (its exposure still the current one).
|
|
14705
|
+
const r = this.authorizeWebSession(record, peer, peer === "named_link");
|
|
14413
14706
|
// What the card says: the end of the authorization, or the session's own end when that comes first.
|
|
14414
14707
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
14415
14708
|
},
|
|
@@ -14455,7 +14748,7 @@ export class FleetManager {
|
|
|
14455
14748
|
session: s, gateway: input.gateway, peer: input.peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
14456
14749
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14457
14750
|
authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
|
|
14458
|
-
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now,
|
|
14751
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now,
|
|
14459
14752
|
});
|
|
14460
14753
|
return r.refusal;
|
|
14461
14754
|
}
|
|
@@ -15211,7 +15504,7 @@ export class FleetManager {
|
|
|
15211
15504
|
}
|
|
15212
15505
|
}
|
|
15213
15506
|
this.persistCancelButtons();
|
|
15214
|
-
this.logger.
|
|
15507
|
+
this.logger.debug({ instanceName, messageId: sent.messageId }, "Cancel button sent"); // #2482: per-turn bookkeeping
|
|
15215
15508
|
// Retirement is normally edge-triggered, but the edge (or an explicit
|
|
15216
15509
|
// reply/cancel clear) may have happened while notifyAlert was in flight.
|
|
15217
15510
|
// Reconcile the level after publication so a late button cannot resurrect
|
|
@@ -15477,7 +15770,8 @@ export class FleetManager {
|
|
|
15477
15770
|
this.deleteButtonMessage(entry)
|
|
15478
15771
|
.then(() => {
|
|
15479
15772
|
this.discardButton(entry);
|
|
15480
|
-
this.logger.
|
|
15773
|
+
this.logger.debug(// #2482: per-turn bookkeeping
|
|
15774
|
+
{ instanceName: entry.instanceName, messageId: entry.messageId, historyPreserved: Boolean(entry.toolProgress) }, "Cancel button retired");
|
|
15481
15775
|
})
|
|
15482
15776
|
.catch((err) => this.scheduleButtonRetry(entry, err));
|
|
15483
15777
|
}
|
|
@@ -16566,8 +16860,12 @@ export class FleetManager {
|
|
|
16566
16860
|
from: m.from,
|
|
16567
16861
|
replyTo: m.replyTo,
|
|
16568
16862
|
cards: m.cards,
|
|
16863
|
+
posts: m.posts, // #2281: kept beside the row, never broadcast (not a row field)
|
|
16864
|
+
reserved: m.reserved, // #1774: an agent reply's place, taken when it was sent
|
|
16569
16865
|
});
|
|
16570
|
-
|
|
16866
|
+
// A sending page's own id for its pending bubble rides on the live copy only: never recorded, never replayed.
|
|
16867
|
+
const clientId = typeof data.clientId === "string" ? data.clientId : "";
|
|
16868
|
+
broadcastSseEvent(this.sseClients, event, clientId ? { ...recorded, clientId } : recorded, onError, this.webChatHistory.cursorOf(recorded));
|
|
16571
16869
|
this.searchIndex?.record(chatDoc(recorded));
|
|
16572
16870
|
// #1629: live-only receipt after the recorded agent reply. Never persist/replay it or give it an SSE cursor.
|
|
16573
16871
|
if (recorded.role === "agent")
|
|
@@ -16603,7 +16901,8 @@ export class FleetManager {
|
|
|
16603
16901
|
if (this.fleetConfig?.needs_you?.live_message === false)
|
|
16604
16902
|
return false;
|
|
16605
16903
|
const card = db.board.findCard(uuid);
|
|
16606
|
-
|
|
16904
|
+
// #2434: only while it waits for the person — a card that does not is in no Needs you, so no buttons anywhere.
|
|
16905
|
+
if (!card || !waitsForPerson(card, this.dispatcherGone))
|
|
16607
16906
|
return false;
|
|
16608
16907
|
const leads = new Map(db.board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
16609
16908
|
return this.needsYouLane(card, leads) === instance;
|
|
@@ -16952,6 +17251,47 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16952
17251
|
* migration exception is the pre-manifest Kiro General layout explicitly
|
|
16953
17252
|
* selected by adoptLegacyUnmanaged.
|
|
16954
17253
|
*/
|
|
17254
|
+
/**
|
|
17255
|
+
* #2482: the user's own skills that shadow bundled ones, said once per directory and change — not once per name on
|
|
17256
|
+
* every start (11 lines a start on the host). The record (`skipped-skills.json` in the AgEnD home) keeps what was said
|
|
17257
|
+
* across restarts; a new name or an edited SKILL.md is said again.
|
|
17258
|
+
*/
|
|
17259
|
+
reportSkippedSkills(destSkills, names) {
|
|
17260
|
+
const signature = names.map(name => {
|
|
17261
|
+
let content = "";
|
|
17262
|
+
try {
|
|
17263
|
+
content = readFileSync(join(destSkills, name, "SKILL.md"), "utf-8");
|
|
17264
|
+
}
|
|
17265
|
+
catch { /* gone: its name alone */ }
|
|
17266
|
+
return `${name}:${createHash("sha256").update(content).digest("hex").slice(0, 16)}`;
|
|
17267
|
+
}).join(",");
|
|
17268
|
+
if (this.skippedSkillsSaid.get(destSkills) === signature)
|
|
17269
|
+
return;
|
|
17270
|
+
this.skippedSkillsSaid.set(destSkills, signature);
|
|
17271
|
+
const path = join(this.dataDir, "skipped-skills.json");
|
|
17272
|
+
// Only a log's memory: whatever is in the file (missing, corrupt, any other shape) never stands in the sync's way
|
|
17273
|
+
// (#2493 review) — a record that cannot be read is no record, and the warning is said.
|
|
17274
|
+
const record = Object.create(null);
|
|
17275
|
+
try {
|
|
17276
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
17277
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17278
|
+
for (const [key, value] of Object.entries(parsed))
|
|
17279
|
+
if (typeof value === "string")
|
|
17280
|
+
record[key] = value;
|
|
17281
|
+
}
|
|
17282
|
+
}
|
|
17283
|
+
catch { /* none yet, or unreadable */ }
|
|
17284
|
+
if (Object.hasOwn(record, destSkills) && record[destSkills] === signature) {
|
|
17285
|
+
this.logger.debug({ skills: names, destSkills }, "Skipping bundled skills — unchanged since it was last said");
|
|
17286
|
+
return;
|
|
17287
|
+
}
|
|
17288
|
+
record[destSkills] = signature;
|
|
17289
|
+
try {
|
|
17290
|
+
writeFileSync(path, JSON.stringify(record) + "\n");
|
|
17291
|
+
}
|
|
17292
|
+
catch { /* said this time; said again next start */ }
|
|
17293
|
+
this.logger.warn({ skills: names, destSkills }, "Skipping bundled skills — skills of these names exist but were not published by AgEnD");
|
|
17294
|
+
}
|
|
16955
17295
|
syncManagedSkills(destSkills, srcSkills, role, adoptLegacyUnmanaged = false, confined) {
|
|
16956
17296
|
if (!existsSync(srcSkills))
|
|
16957
17297
|
return;
|
|
@@ -17001,16 +17341,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17001
17341
|
return role === "general" || roles.includes(role);
|
|
17002
17342
|
});
|
|
17003
17343
|
const managed = [];
|
|
17344
|
+
const skipped = [];
|
|
17004
17345
|
for (const name of eligible) {
|
|
17005
17346
|
const destDir = join(destSkills, name);
|
|
17006
17347
|
const dest = join(destDir, "SKILL.md");
|
|
17007
17348
|
const isOurs = previouslyManaged.includes(name) || !existsSync(destDir);
|
|
17008
17349
|
if (!isOurs) {
|
|
17009
|
-
// The user's skill still wins on every sync; only the warning is deduped
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
this.logger.warn({ skill: name, destSkills }, "Skipping bundled skill — a skill of this name exists but was not published by AgEnD");
|
|
17013
|
-
}
|
|
17350
|
+
// The user's skill still wins on every sync; only the warning is deduped (#2482: one line per directory, and
|
|
17351
|
+
// only when what is skipped there changed since it was last said — across restarts too).
|
|
17352
|
+
skipped.push(name);
|
|
17014
17353
|
continue;
|
|
17015
17354
|
}
|
|
17016
17355
|
managed.push(name);
|
|
@@ -17025,6 +17364,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17025
17364
|
mkdirSync(destDir, { recursive: true });
|
|
17026
17365
|
writeFileSync(dest, newContent);
|
|
17027
17366
|
}
|
|
17367
|
+
if (skipped.length)
|
|
17368
|
+
this.reportSkippedSkills(destSkills, skipped);
|
|
17028
17369
|
// Remove managed skills that are no longer bundled OR no longer eligible
|
|
17029
17370
|
// for this role. This makes a shared → general-only metadata change take
|
|
17030
17371
|
// effect on the next worker startup instead of leaving stale capability.
|
|
@@ -17356,7 +17697,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17356
17697
|
try {
|
|
17357
17698
|
const tmpPath = await dlAdapter.downloadAttachment(att.fileId);
|
|
17358
17699
|
const inboxDir = join(getAgendHome(), "workspaces", instanceName, "inbox");
|
|
17359
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
17700
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
17360
17701
|
const dest = join(inboxDir, basename(tmpPath));
|
|
17361
17702
|
// Copy to destination — failure means this attachment is skipped
|
|
17362
17703
|
try {
|
|
@@ -17768,7 +18109,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17768
18109
|
}
|
|
17769
18110
|
}
|
|
17770
18111
|
const path = this.cliEnvPath(backend, profile);
|
|
17771
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
18112
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17772
18113
|
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
17773
18114
|
return env;
|
|
17774
18115
|
}
|
|
@@ -19467,19 +19808,43 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19467
19808
|
}
|
|
19468
19809
|
tmuxExecHelperTimer = null;
|
|
19469
19810
|
stopAllInFlight = null;
|
|
19811
|
+
/** Stops release memory. Reassess between batches without waiting for recovery. */
|
|
19812
|
+
async stopInstancesInBatches(entries, stop) {
|
|
19813
|
+
let index = 0;
|
|
19814
|
+
while (index < entries.length) {
|
|
19815
|
+
// Linux procfs can still be read after the periodic sampler is stopped.
|
|
19816
|
+
// Do not revive a stopped Darwin probe/worker just to size a quit batch;
|
|
19817
|
+
// its full shutdown uses the conservative batch size instead.
|
|
19818
|
+
const level = this.memoryPressure.platform === "darwin"
|
|
19819
|
+
? this.shuttingDown ? "elevated" : this.memoryPressure.snapshot().level
|
|
19820
|
+
: this.memoryPressure.sample().level;
|
|
19821
|
+
const size = fleetStopConcurrency(entries.length, level);
|
|
19822
|
+
const batch = entries.slice(index, index + size);
|
|
19823
|
+
index += batch.length;
|
|
19824
|
+
await Promise.allSettled(batch.map(stop));
|
|
19825
|
+
}
|
|
19826
|
+
}
|
|
19470
19827
|
async doStopAll() {
|
|
19828
|
+
this.startupMemoryDeadline?.stop();
|
|
19829
|
+
this.startupMemoryDeadline = null;
|
|
19471
19830
|
this.startupComplete = false;
|
|
19831
|
+
this.startupDashboard = null;
|
|
19472
19832
|
this.reloadPending = false;
|
|
19473
19833
|
// Before anything is stopped: everything that dies from here on dies
|
|
19474
19834
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
19475
19835
|
// await in the same tick as the signal handler, so no event can slip in.
|
|
19476
19836
|
this.shuttingDown = true;
|
|
19837
|
+
if (this.updateProgressTimer) {
|
|
19838
|
+
clearInterval(this.updateProgressTimer);
|
|
19839
|
+
this.updateProgressTimer = null;
|
|
19840
|
+
}
|
|
19477
19841
|
this.settingsGeneration++;
|
|
19478
19842
|
this.settingsConfirmation?.store.close();
|
|
19479
19843
|
this.settingsConfirmation = null;
|
|
19480
19844
|
const settingsControl = this.settingsControl;
|
|
19481
19845
|
this.settingsControl = null;
|
|
19482
19846
|
const settingsControlStopped = settingsControl?.close();
|
|
19847
|
+
this.publicLinkAutoOpen?.stop();
|
|
19483
19848
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
19484
19849
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
19485
19850
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
@@ -19630,21 +19995,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19630
19995
|
// tmux ops, while small fleets stay conservative to avoid overwhelming the
|
|
19631
19996
|
// tmux server.
|
|
19632
19997
|
const entries = [...this.daemons.entries()];
|
|
19633
|
-
const STOP_CONCURRENCY = entries.length > 30 ? 15 : entries.length >= 10 ? 10 : 5;
|
|
19634
19998
|
for (const [name] of entries)
|
|
19635
19999
|
this.ipcStoppingInstances.add(name);
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
}
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
this.daemons.delete(name);
|
|
19646
|
-
}));
|
|
19647
|
-
}
|
|
20000
|
+
await this.stopInstancesInBatches(entries, async ([name, daemon]) => {
|
|
20001
|
+
try {
|
|
20002
|
+
await daemon.stop();
|
|
20003
|
+
}
|
|
20004
|
+
catch (err) {
|
|
20005
|
+
this.logger.warn({ name, err }, "Stop failed");
|
|
20006
|
+
}
|
|
20007
|
+
this.daemons.delete(name);
|
|
20008
|
+
});
|
|
19648
20009
|
// Close IPC clients in parallel — serial close over a large fleet adds
|
|
19649
20010
|
// noticeable latency.
|
|
19650
20011
|
await Promise.all([...this.instanceIpcClients.values()].map(ipc => Promise.resolve(ipc.close()).catch(() => { })));
|
|
@@ -19855,6 +20216,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19855
20216
|
this.rawFleetConfig = previousRawConfig;
|
|
19856
20217
|
this.rawFleetDocument = previousRawDocument;
|
|
19857
20218
|
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
20219
|
+
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
19858
20220
|
this.logger.error({
|
|
19859
20221
|
oldCount,
|
|
19860
20222
|
newCount,
|
|
@@ -19953,8 +20315,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19953
20315
|
// so an old/new diff alone can miss the live daemon's stale value.
|
|
19954
20316
|
for (const [name, config] of Object.entries(newInstances)) {
|
|
19955
20317
|
if (!this.daemons.has(name)) {
|
|
19956
|
-
// New instance — startInstance already calls connectIpcToInstance
|
|
19957
|
-
|
|
20318
|
+
// New instance — startInstance already calls connectIpcToInstance. #2482: a paused one is not started (startInstance
|
|
20319
|
+
// returns at its pause); say so, quietly, instead of "starting" on every reload.
|
|
20320
|
+
if (this.lifecycle.isPaused(name))
|
|
20321
|
+
this.logger.debug({ name }, "Paused instance in config — not started");
|
|
20322
|
+
else
|
|
20323
|
+
this.logger.info({ name }, "New instance in config — starting");
|
|
19958
20324
|
observe?.(name, "restart", "running");
|
|
19959
20325
|
await this.startInstanceUnattended(name, config, topicMode, "new instance");
|
|
19960
20326
|
observe?.(name, "restart", "done");
|
|
@@ -19963,6 +20329,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19963
20329
|
const daemon = this.daemons.get(name);
|
|
19964
20330
|
const runtimeConfig = daemon.getConfigSnapshot?.() ?? oldConfig.instances[name];
|
|
19965
20331
|
const change = classifyInstanceChange(runtimeConfig, config);
|
|
20332
|
+
// (b) The person switched its task board on: tell it (its tool list shows the `task` tool at once).
|
|
20333
|
+
if (taskBoardSwitchedOn(runtimeConfig.dashboard, config.dashboard)) {
|
|
20334
|
+
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
20335
|
+
}
|
|
19966
20336
|
if (change === "restart") {
|
|
19967
20337
|
this.logger.info({ name }, "Instance config changed — restarting");
|
|
19968
20338
|
observe?.(name, "restart", "running");
|
|
@@ -21436,7 +21806,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21436
21806
|
await ipc.close();
|
|
21437
21807
|
}
|
|
21438
21808
|
this.instanceIpcClients.clear();
|
|
21439
|
-
await
|
|
21809
|
+
await this.stopInstancesInBatches(instanceNames, name => this.stopInstance(name));
|
|
21440
21810
|
// Kill remaining orphan windows to prevent stale state on restart
|
|
21441
21811
|
try {
|
|
21442
21812
|
const agendNames = new Set(instanceNames);
|
|
@@ -21757,6 +22127,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21757
22127
|
if (this.guestScope(owner).size)
|
|
21758
22128
|
return "ok";
|
|
21759
22129
|
if (owner && this.fleetConfig?.web?.guests?.enabled === true) {
|
|
22130
|
+
// Before adapters initialize, topic scope is unknown, not revoked. Keep the returning cookie but grant no
|
|
22131
|
+
// request until its configured world exists. Removed connections and initialized denied access stay final.
|
|
22132
|
+
const channels = this.fleetConfig.channels ?? (this.fleetConfig.channel ? [this.fleetConfig.channel] : []);
|
|
22133
|
+
if (this.startupDashboard && !this.worlds.has(owner.adapterId)
|
|
22134
|
+
&& channels.some(channel => (channel.id ?? channel.type) === owner.adapterId))
|
|
22135
|
+
return "pending";
|
|
21760
22136
|
const members = this.guestRooms(), rooms = guestGroupRooms(this.classicChannels?.getAll() ?? [], owner);
|
|
21761
22137
|
if (rooms.some((room) => members.pending(room, owner.userId))) {
|
|
21762
22138
|
void this.refreshGuestRooms(owner, { mode: "triggered" }); // rate-limited per room (30 s), single-flight
|
|
@@ -21855,7 +22231,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21855
22231
|
res.end(JSON.stringify({ error: WEB_HOST_REJECTED_MESSAGE }));
|
|
21856
22232
|
return;
|
|
21857
22233
|
}
|
|
21858
|
-
if (closing) {
|
|
22234
|
+
if (closing || this.shuttingDown) {
|
|
21859
22235
|
res.writeHead(503, { Connection: "close", "Retry-After": "10" });
|
|
21860
22236
|
res.end(JSON.stringify({ error: "fleet stopping" }));
|
|
21861
22237
|
return;
|
|
@@ -21903,6 +22279,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21903
22279
|
// #1920: a guest reaches only GUEST_ROUTES, within the scope derived for it now — default deny, before any
|
|
21904
22280
|
// handler, on this listener and the public link's gateway alike (both come through here).
|
|
21905
22281
|
if (decision.via === "session" && decision.session.tier === "guest") {
|
|
22282
|
+
// A browser opening a page gets a page in its language, not the JSON an API caller gets (#2399 smoke).
|
|
22283
|
+
const navigating = req.method === "GET" && String(req.headers.accept ?? "").includes("text/html") && isWebPageNavigation(requestPath);
|
|
21906
22284
|
const owner = decision.session.owner;
|
|
21907
22285
|
const admitted = decision.session;
|
|
21908
22286
|
const admission = this.guestAdmission(admitted);
|
|
@@ -21913,12 +22291,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21913
22291
|
return;
|
|
21914
22292
|
}
|
|
21915
22293
|
if (admission === "gone") {
|
|
22294
|
+
if (navigating) {
|
|
22295
|
+
serveGuestRefusedPage(res, 401, "ended");
|
|
22296
|
+
return;
|
|
22297
|
+
}
|
|
21916
22298
|
res.writeHead(401);
|
|
21917
22299
|
res.end(JSON.stringify({ error: "guest_scope_empty" }));
|
|
21918
22300
|
return;
|
|
21919
22301
|
}
|
|
21920
22302
|
const verdict = guestRouteDecision(req.method ?? "GET", parsedUrl, this.guestScope(owner));
|
|
21921
22303
|
if (verdict.kind === "deny") {
|
|
22304
|
+
if (navigating) {
|
|
22305
|
+
serveGuestRefusedPage(res, verdict.status, "members");
|
|
22306
|
+
return;
|
|
22307
|
+
}
|
|
21922
22308
|
res.writeHead(verdict.status);
|
|
21923
22309
|
res.end(JSON.stringify({ error: verdict.error }));
|
|
21924
22310
|
return;
|
|
@@ -21951,6 +22337,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21951
22337
|
res.end(JSON.stringify(health));
|
|
21952
22338
|
return;
|
|
21953
22339
|
}
|
|
22340
|
+
// Threat: lifecycle/config/chat writes racing boot-owned starts and reconciliation. Reads stay live;
|
|
22341
|
+
// authentication keeps its own checks so a browser can sign in or sign out while agents start.
|
|
22342
|
+
if (this.startupDashboard && !["GET", "HEAD", "OPTIONS"].includes(req.method ?? "GET")
|
|
22343
|
+
&& !requestPath.startsWith("/auth/")) {
|
|
22344
|
+
res.writeHead(503, { "Retry-After": "2", "Cache-Control": "no-store", Connection: "close" });
|
|
22345
|
+
res.end(JSON.stringify({ code: "fleet_starting", error: t("fleet.starting_retry") }));
|
|
22346
|
+
return;
|
|
22347
|
+
}
|
|
21954
22348
|
if (req.method === "GET" && req.url === "/status") {
|
|
21955
22349
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => {
|
|
21956
22350
|
const statusFile = join(this.getInstanceDir(name), "statusline.json");
|
|
@@ -22221,6 +22615,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22221
22615
|
}
|
|
22222
22616
|
}
|
|
22223
22617
|
startHealthServer(port) {
|
|
22618
|
+
let settled;
|
|
22619
|
+
const listening = new Promise(resolve => { settled = resolve; });
|
|
22224
22620
|
this.startedAt = Date.now();
|
|
22225
22621
|
this.healthServerListening = false;
|
|
22226
22622
|
this.healthPortRetried = false;
|
|
@@ -22235,6 +22631,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22235
22631
|
const drain = trackWebServer(server);
|
|
22236
22632
|
this.healthServer = server;
|
|
22237
22633
|
this.healthServerDrain = drain;
|
|
22634
|
+
server.once("close", settled);
|
|
22238
22635
|
const markListening = (afterTakeover = false) => {
|
|
22239
22636
|
this.healthServerListening = true;
|
|
22240
22637
|
this.logger.info({ port }, afterTakeover
|
|
@@ -22248,6 +22645,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22248
22645
|
// #1306: the preview listener starts once the web listener is bound — its frame-ancestors name the real port.
|
|
22249
22646
|
const bound = this.healthServer?.address();
|
|
22250
22647
|
this.startPreviewListener(port, bound && typeof bound === "object" ? bound.port : port);
|
|
22648
|
+
settled();
|
|
22251
22649
|
};
|
|
22252
22650
|
this.healthServer.on("error", (err) => {
|
|
22253
22651
|
this.healthServerListening = false;
|
|
@@ -22255,6 +22653,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22255
22653
|
if (this.healthPortRetried) {
|
|
22256
22654
|
this.logger.error({ err, port }, "Health port still in use after takeover — dashboard disabled");
|
|
22257
22655
|
this.notifyFleetError(t("dashboard.port_in_use", port));
|
|
22656
|
+
settled();
|
|
22258
22657
|
return;
|
|
22259
22658
|
}
|
|
22260
22659
|
this.healthPortRetried = true;
|
|
@@ -22290,16 +22689,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22290
22689
|
this.logger.debug({ err }, "Old fleet process kill skipped (already gone or no permission)");
|
|
22291
22690
|
}
|
|
22292
22691
|
setTimeout(() => {
|
|
22293
|
-
if (
|
|
22692
|
+
if (this.healthServer !== server || this.shuttingDown) {
|
|
22693
|
+
settled();
|
|
22294
22694
|
return;
|
|
22295
|
-
|
|
22695
|
+
}
|
|
22696
|
+
server.listen(port, "127.0.0.1", () => markListening(true));
|
|
22296
22697
|
}, 1500);
|
|
22297
22698
|
return;
|
|
22298
22699
|
}
|
|
22299
22700
|
this.logger.error({ err, port }, "Health server error");
|
|
22300
22701
|
this.notifyFleetError(t("dashboard.server_failed", err.message));
|
|
22702
|
+
settled();
|
|
22301
22703
|
});
|
|
22302
22704
|
this.healthServer.listen(port, "127.0.0.1", () => markListening());
|
|
22705
|
+
return listening;
|
|
22303
22706
|
}
|
|
22304
22707
|
/**
|
|
22305
22708
|
* #1306: the preview listener, beside the web listener and closed with it. When it cannot listen (the port is
|
|
@@ -22528,6 +22931,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22528
22931
|
return {
|
|
22529
22932
|
canvas_worker: this.canvasWorkerEnabled(),
|
|
22530
22933
|
instances,
|
|
22934
|
+
startup: this.startupDashboard?.snapshot() ?? null,
|
|
22531
22935
|
publicLink: this.getPublicWebStatus(),
|
|
22532
22936
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
22533
22937
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|