@songsid/agend 2.3.0-beta.25 → 2.3.0-beta.26
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/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.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.js +29 -4
- 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.js +15 -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 +18 -4
- 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 +73 -3
- 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.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 +76 -6
- package/dist/fleet-manager.js +479 -145
- 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 +33 -11
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +42 -13
- package/dist/host-memory.js.map +1 -1
- 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 +22 -0
- 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 +4 -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/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 +20 -1
- package/dist/ui/chat-render.js +1 -15
- package/dist/ui/chat-store.js +171 -61
- package/dist/ui/chat-strings.js +7 -2
- 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 +14 -13
- 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 +12 -0
- package/dist/ui/shared/app-store.js +1 -0
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +132 -3
- 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/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/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 +15 -2
- package/dist/web-api.js +174 -25
- 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";
|
|
@@ -235,12 +239,13 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
|
235
239
|
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
236
240
|
import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
|
|
237
241
|
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
242
|
+
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
238
243
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
239
244
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
|
|
240
|
-
import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
|
|
245
|
+
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
241
246
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
242
247
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
243
|
-
import { authorityNow,
|
|
248
|
+
import { authorityNow, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
244
249
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
245
250
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
246
251
|
import { SessionAuthorizationRequests } from "./web-session-authorization.js";
|
|
@@ -267,6 +272,7 @@ import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
|
267
272
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
268
273
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
269
274
|
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
275
|
+
import { FleetStartup } from "./fleet-startup.js";
|
|
270
276
|
import { launchFullRestartHelper } from "./full-restart.js";
|
|
271
277
|
import { fullReloadExitCode } from "./service-owner.js";
|
|
272
278
|
import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
|
|
@@ -814,6 +820,7 @@ export class FleetManager {
|
|
|
814
820
|
// Adapter isolation: track state per adapter for retry + visibility
|
|
815
821
|
/** #2175: this boot's progress message, whose completion may still be owed by its background retry. */
|
|
816
822
|
startupProgress = null;
|
|
823
|
+
startupDashboard = null;
|
|
817
824
|
adapterState = new Map();
|
|
818
825
|
adapterRetryInFlight = new Map();
|
|
819
826
|
/** Web Settings secret rotation is deliberately separate from reconnect
|
|
@@ -895,6 +902,7 @@ export class FleetManager {
|
|
|
895
902
|
/** Days of event/activity history to keep. */
|
|
896
903
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
897
904
|
watchdogTimer = null;
|
|
905
|
+
startupMemoryDeadline = null;
|
|
898
906
|
eventLoopWatch = null;
|
|
899
907
|
startedAt = 0;
|
|
900
908
|
stormOpenNotifyTimer = null;
|
|
@@ -930,6 +938,28 @@ export class FleetManager {
|
|
|
930
938
|
webFiles = new WebFileLedger({ limits: () => uploadLimits(this.fleetConfig?.web) });
|
|
931
939
|
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
932
940
|
webReplyOrder = new Map();
|
|
941
|
+
/**
|
|
942
|
+
* #1774: an instance name's reply generation — moved on when a removal of it starts, so replies sent from then on (a
|
|
943
|
+
* same-name replacement's) are told from the removed instance's. `webChatRemovals`: once a removal succeeded, the
|
|
944
|
+
* generations and the daemon whose replies are no longer anyone's to show.
|
|
945
|
+
*/
|
|
946
|
+
webChatGenerations = new Map();
|
|
947
|
+
webChatRemovals = new Map();
|
|
948
|
+
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
949
|
+
webChatRemovalsPending = new Map();
|
|
950
|
+
webChatGeneration(name) { return this.webChatGenerations.get(name) ?? 0; }
|
|
951
|
+
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
952
|
+
webChatIdentity(name) { return { generation: this.webChatGeneration(name), daemon: this.daemons.get(name) }; }
|
|
953
|
+
/** #1774: the reply's instance was not removed (a removal that failed changes nothing). */
|
|
954
|
+
webChatIdentityCurrent(name, id) {
|
|
955
|
+
const removed = this.webChatRemovals.get(name);
|
|
956
|
+
return !removed || (id.generation >= removed.below && (removed.daemon === undefined || id.daemon !== removed.daemon));
|
|
957
|
+
}
|
|
958
|
+
/** #1774: the removal still deciding this reply's fate (it was sent by what is being removed), or null. */
|
|
959
|
+
webChatRemovalPending(name, id) {
|
|
960
|
+
const pending = this.webChatRemovalsPending.get(name);
|
|
961
|
+
return pending && (id.generation < pending.below || (pending.daemon !== undefined && id.daemon === pending.daemon)) ? pending.done : null;
|
|
962
|
+
}
|
|
933
963
|
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
934
964
|
webReplyMaxBytes = REPLY_FILES_MAX_BYTES;
|
|
935
965
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
@@ -1015,6 +1045,11 @@ export class FleetManager {
|
|
|
1015
1045
|
// r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
|
|
1016
1046
|
return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits, effectsDropped: ids => this.effectLibrary.collectUnreferenced(ids) }));
|
|
1017
1047
|
}
|
|
1048
|
+
officeStoreInstance;
|
|
1049
|
+
/** Fleet → Org chart → Office: the offices people made (web-offices.json, beside web-themes.json). */
|
|
1050
|
+
get officeStore() { return (this.officeStoreInstance ??= new OfficeStore(this.dataDir)); }
|
|
1051
|
+
/** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
|
|
1052
|
+
officeWrites = new OfficeWriteLimiter();
|
|
1018
1053
|
/** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
|
|
1019
1054
|
get webAppearance() {
|
|
1020
1055
|
return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
|
|
@@ -1362,7 +1397,13 @@ export class FleetManager {
|
|
|
1362
1397
|
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
1363
1398
|
if (auth.kind !== "ok")
|
|
1364
1399
|
return null;
|
|
1365
|
-
|
|
1400
|
+
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
1401
|
+
if (gatewayRequestContext(req))
|
|
1402
|
+
return null;
|
|
1403
|
+
// …and never a change that turns the named-link option on: that one needs a General's card or the host.
|
|
1404
|
+
if (!store.selfConfirmable(id))
|
|
1405
|
+
return null;
|
|
1406
|
+
const record = auth.session, gateway = !!gatewayRequestContext(req), peer = webPeerOf(req);
|
|
1366
1407
|
const verified = () => {
|
|
1367
1408
|
const now = this.webAuthority({ session: record, gateway, peer });
|
|
1368
1409
|
// A refusal carries no authority (kind "none"); the handle pins the session, and with it its one account.
|
|
@@ -1629,6 +1670,7 @@ export class FleetManager {
|
|
|
1629
1670
|
this.spawnGate = new SpawnGate({
|
|
1630
1671
|
storm: this.stormWindow,
|
|
1631
1672
|
memoryPressure: this.memoryPressure,
|
|
1673
|
+
onStartupMemoryWait: () => this.startupMemoryDeadline?.check(),
|
|
1632
1674
|
concurrency: () => this.spawnConcurrency(),
|
|
1633
1675
|
staggerMs: () => this.fleetConfig?.defaults?.startup?.stagger_delay_ms ?? 500,
|
|
1634
1676
|
});
|
|
@@ -2674,6 +2716,7 @@ export class FleetManager {
|
|
|
2674
2716
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
2675
2717
|
this.warnAboutRemovedLoginMode(loaded);
|
|
2676
2718
|
this.enforceGuestSwitch();
|
|
2719
|
+
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
2677
2720
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
2678
2721
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
2679
2722
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -3280,7 +3323,8 @@ export class FleetManager {
|
|
|
3280
3323
|
instancePresentation(name) {
|
|
3281
3324
|
const execution_state = this.getInstanceExecutionState(name);
|
|
3282
3325
|
const interaction = this.getInstanceInteraction(name);
|
|
3283
|
-
|
|
3326
|
+
const startup_phase = !this.lifecycle.isPaused(name) ? this.startupDashboard?.phase(name) : undefined;
|
|
3327
|
+
return { ...(startup_phase ? { startup_phase } : {}), state: presentationState(execution_state, interaction), execution_state,
|
|
3284
3328
|
interaction, interaction_summary: interactionSummary(interaction) };
|
|
3285
3329
|
}
|
|
3286
3330
|
/**
|
|
@@ -3832,6 +3876,10 @@ export class FleetManager {
|
|
|
3832
3876
|
// #2305: the copies' own message ids, so an instance's reaction to the web message is put on them too.
|
|
3833
3877
|
if (copies.length)
|
|
3834
3878
|
this.rememberWebEchoCopies(messageId, copies);
|
|
3879
|
+
// #2281: and where they are, for a later web reply to this message to be a native reply there.
|
|
3880
|
+
const posts = copies.flatMap(c => c.adapterId ? [{ adapterId: c.adapterId, chatId: c.chatId, ...(c.threadId ? { threadId: c.threadId } : {}), messageId: c.messageId }] : []);
|
|
3881
|
+
if (posts.length)
|
|
3882
|
+
this.webChatHistory.addPosts(instance, messageId, posts);
|
|
3835
3883
|
const m = this.webChatHistory.markChannel(instance, messageId);
|
|
3836
3884
|
if (m)
|
|
3837
3885
|
broadcastToGuests(this.sseClients, "message", m, (err) => this.logger.debug({ err }, "SSE client write failed; evicting"));
|
|
@@ -3882,7 +3930,11 @@ export class FleetManager {
|
|
|
3882
3930
|
return { ok: true, mirrored };
|
|
3883
3931
|
}
|
|
3884
3932
|
/** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
|
|
3885
|
-
|
|
3933
|
+
/**
|
|
3934
|
+
* `reply` (#2281): where the message a web reply quotes is on a platform. A room that has it gets the reply as a
|
|
3935
|
+
* native reply to it (`reply.text`: without the quoted line); every other room gets `text`, as before.
|
|
3936
|
+
*/
|
|
3937
|
+
async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK, copies, reply) {
|
|
3886
3938
|
const guard = this.webChannelEchoGuards.get(instance);
|
|
3887
3939
|
const handled = new Set();
|
|
3888
3940
|
let posted = 0;
|
|
@@ -3912,9 +3964,11 @@ export class FleetManager {
|
|
|
3912
3964
|
try {
|
|
3913
3965
|
if (!adapter?.sendText)
|
|
3914
3966
|
continue;
|
|
3915
|
-
|
|
3967
|
+
// A Telegram room is its chat; a Discord room is the guild's channel (an inbound's thread there).
|
|
3968
|
+
const native = reply?.posts.find(p => p.adapterId === adapter.id && (p.chatId === entry.channelId || p.threadId === entry.channelId));
|
|
3969
|
+
const sent = await adapter.sendText(entry.channelId, native ? reply.text : text, { format: "text", allowedMentions: { parse: [] }, ...(native ? { replyTo: native.messageId } : {}) });
|
|
3916
3970
|
if (copies && sent && typeof sent.messageId === "string" && sent.messageId)
|
|
3917
|
-
copies.push({ adapter, chatId: entry.channelId, messageId: sent.messageId });
|
|
3971
|
+
copies.push({ adapter, adapterId: adapter.id, chatId: entry.channelId, messageId: sent.messageId });
|
|
3918
3972
|
ClassicChannelManager.logMessage(instance, sender, text, new Date());
|
|
3919
3973
|
posted++;
|
|
3920
3974
|
}
|
|
@@ -4971,66 +5025,92 @@ export class FleetManager {
|
|
|
4971
5025
|
this.logger.info({ name }, "Instance already running, skipping");
|
|
4972
5026
|
return;
|
|
4973
5027
|
}
|
|
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"));
|
|
5028
|
+
await this.trackStartupInstance(name, async () => {
|
|
5029
|
+
// An explicit start (CLI/API) begins a fresh idle window; boot and
|
|
5030
|
+
// reconcile starts keep the persisted last-inbound seed (Phase 2a).
|
|
5031
|
+
if (resumePaused)
|
|
5032
|
+
this.lifecycle.markActivitySeedNow(name);
|
|
5033
|
+
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5034
|
+
if (config.general_topic) {
|
|
5035
|
+
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
4989
5036
|
}
|
|
4990
|
-
|
|
4991
|
-
//
|
|
4992
|
-
//
|
|
4993
|
-
// fleet
|
|
4994
|
-
|
|
5037
|
+
else if (kind === "fleet-topic") {
|
|
5038
|
+
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
5039
|
+
// deliberately excluded: their workspace and conversation lifecycle are
|
|
5040
|
+
// managed independently from fleet-topic workers.
|
|
5041
|
+
try {
|
|
5042
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
5043
|
+
measureSyncWork("fleet.workerSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "worker"));
|
|
5044
|
+
}
|
|
5045
|
+
catch (err) {
|
|
5046
|
+
// Skill publishing is additive. A read-only or temporarily unavailable
|
|
5047
|
+
// workspace must not turn an otherwise valid worker startup into a
|
|
5048
|
+
// fleet outage.
|
|
5049
|
+
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
5050
|
+
}
|
|
4995
5051
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5052
|
+
else if (kind === "classic") {
|
|
5053
|
+
// #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
|
|
5054
|
+
// coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
|
|
5055
|
+
// room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
|
|
5056
|
+
// reads any bundled skill on demand.
|
|
5057
|
+
try {
|
|
5058
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
5059
|
+
if (insideManagedWorkspaces(skillsWorkDir))
|
|
5060
|
+
measureSyncWork("fleet.classicSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "classic"));
|
|
5061
|
+
else
|
|
5062
|
+
this.logger.debug({ name, skillsWorkDir }, "Classic workspace is not AgEnD-managed — no skills published");
|
|
5063
|
+
}
|
|
5064
|
+
catch (err) {
|
|
5065
|
+
this.logger.warn({ err, name, backend }, "Failed to sync classic skills — continuing startup");
|
|
5066
|
+
}
|
|
5008
5067
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5068
|
+
const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
|
|
5069
|
+
const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
|
|
5070
|
+
if (receipt) {
|
|
5071
|
+
await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
|
|
5072
|
+
// Only this transition's actual publication can acquire its replacement.
|
|
5073
|
+
receipt.daemon = daemon;
|
|
5074
|
+
});
|
|
5011
5075
|
}
|
|
5076
|
+
else
|
|
5077
|
+
await this.lifecycle.start(name, config, topicMode, identity, transition, execution);
|
|
5078
|
+
execution?.assert();
|
|
5079
|
+
// Only clear a stale process status after a real start succeeded. Clearing
|
|
5080
|
+
// it before lifecycle.start() can turn a crash-loop daemon's dead pane into
|
|
5081
|
+
// a falsely running instance when lifecycle.start() returns early.
|
|
5082
|
+
this.instanceProcessStatus.delete(name);
|
|
5083
|
+
// Recovery intent belongs to Daemon.start. lifecycle.start can return
|
|
5084
|
+
// before reaching it, so an await here is not proof the marker was read.
|
|
5085
|
+
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
5086
|
+
await this.connectIpcToInstance(name);
|
|
5087
|
+
execution?.assert();
|
|
5088
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
5089
|
+
});
|
|
5090
|
+
}
|
|
5091
|
+
/** A boot's observation cannot settle a replacement boot or a newer attempt of the same instance. */
|
|
5092
|
+
async trackStartupInstance(name, start) {
|
|
5093
|
+
const boot = this.startupDashboard;
|
|
5094
|
+
const owner = boot?.begin(name);
|
|
5095
|
+
if (owner)
|
|
5096
|
+
this.publishStartupStatus();
|
|
5097
|
+
let ready = false;
|
|
5098
|
+
try {
|
|
5099
|
+
await start();
|
|
5100
|
+
ready = this.daemons.has(name);
|
|
5012
5101
|
}
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5102
|
+
finally {
|
|
5103
|
+
if (boot && owner && this.startupDashboard === boot && boot.finish(name, owner, ready))
|
|
5104
|
+
this.publishStartupStatus();
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
publishStartupStatus() {
|
|
5108
|
+
try {
|
|
5109
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
5110
|
+
}
|
|
5111
|
+
catch (err) {
|
|
5112
|
+
this.logger.debug({ err }, "Startup status unavailable; the next status frame retries");
|
|
5020
5113
|
}
|
|
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
5114
|
}
|
|
5035
5115
|
/** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
|
|
5036
5116
|
async startPersistedPausedInstance(name, transition) {
|
|
@@ -5972,6 +6052,11 @@ export class FleetManager {
|
|
|
5972
6052
|
revoke: id => {
|
|
5973
6053
|
this.webLoginCodes?.revokeAudience(id);
|
|
5974
6054
|
this.webGuestLoginCodes?.revokeAudience(id);
|
|
6055
|
+
// #2453 (§0.2): the link's sessions end, and the authorizations kept for them go with them (housekeeping: their
|
|
6056
|
+
// handles can never match again).
|
|
6057
|
+
const ended = new Set(this.webSessions?.list(undefined, r => r.exposureId === id).map(r => r.handle) ?? []);
|
|
6058
|
+
if (ended.size)
|
|
6059
|
+
this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
|
|
5975
6060
|
const result = this.webSessions?.revokeExposure(id);
|
|
5976
6061
|
if (result && !result.durable)
|
|
5977
6062
|
this.logger.warn("Public session revocation could not be saved; exposure remains closed");
|
|
@@ -5992,6 +6077,66 @@ export class FleetManager {
|
|
|
5992
6077
|
},
|
|
5993
6078
|
});
|
|
5994
6079
|
}
|
|
6080
|
+
publicLinkAutoOpen = null;
|
|
6081
|
+
/**
|
|
6082
|
+
* #named-link-ttl: the admin who last opened a named-tunnel link from /web, with the live binding — only while they
|
|
6083
|
+
* still may own one (publicOwnerCurrent: a fleet admin of a live bot, the same General). Else null.
|
|
6084
|
+
*/
|
|
6085
|
+
rememberedPublicOwner() {
|
|
6086
|
+
const saved = readPublicLinkOwner(this.dataDir);
|
|
6087
|
+
if (!saved)
|
|
6088
|
+
return null;
|
|
6089
|
+
const owner = { ...saved, binding: this.adapters.get(saved.adapterId) };
|
|
6090
|
+
return this.publicOwnerCurrent(owner) ? owner : null;
|
|
6091
|
+
}
|
|
6092
|
+
/** A named-tunnel link was privately delivered: auto_open reopens it for this owner after a restart. */
|
|
6093
|
+
rememberPublicLinkOwner(owner) {
|
|
6094
|
+
if (!publicLinkNamedHostname(this.fleetConfig?.web))
|
|
6095
|
+
return;
|
|
6096
|
+
try {
|
|
6097
|
+
writePublicLinkOwner(this.dataDir, owner);
|
|
6098
|
+
}
|
|
6099
|
+
catch (err) {
|
|
6100
|
+
this.logger.warn({ err: err?.message }, "Could not remember the public link's owner; auto_open will not reopen it");
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
/** Closed on purpose (Close, /web revoke): a restart must not reopen it. The next /web remembers again. */
|
|
6104
|
+
forgetPublicLinkOwner() {
|
|
6105
|
+
try {
|
|
6106
|
+
clearPublicLinkOwner(this.dataDir);
|
|
6107
|
+
}
|
|
6108
|
+
catch (err) {
|
|
6109
|
+
this.logger.warn({ err: err?.message }, "Could not forget the public link's owner");
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
/**
|
|
6113
|
+
* #named-link-ttl: `web.public_link.auto_open` — once per start, after the web listener and the chat connections are
|
|
6114
|
+
* up. Opens the named link with nothing posted (sign-in still needs a code from /web or a returning device); a final
|
|
6115
|
+
* failure goes to its owner once, by DM, drawn as a failed /web is.
|
|
6116
|
+
*/
|
|
6117
|
+
startPublicLinkAutoOpen() {
|
|
6118
|
+
this.publicLinkAutoOpen?.stop();
|
|
6119
|
+
const runner = this.publicLinkAutoOpen = new PublicLinkAutoOpen({
|
|
6120
|
+
web: () => this.fleetConfig?.web,
|
|
6121
|
+
ready: () => !this.shuttingDown && this.healthServerListening && this.publicLinkAutoOpen === runner,
|
|
6122
|
+
owner: () => this.rememberedPublicOwner(),
|
|
6123
|
+
open: (owner, onProgress) => this.getPublicWebLink().openUnattended(owner, onProgress),
|
|
6124
|
+
notifyFailed: async (owner, last) => {
|
|
6125
|
+
const adapter = owner.binding;
|
|
6126
|
+
if (this.shuttingDown || !adapter?.sendDirect || !this.publicOwnerCurrent(owner))
|
|
6127
|
+
return;
|
|
6128
|
+
const outcome = t("dashboard.public_auto_failed");
|
|
6129
|
+
try {
|
|
6130
|
+
await withinBudget(adapter.sendDirect(owner.userId, last ? renderPublicLinkProgress(last, performance.now(), outcome) : outcome, { disablePreview: true }), performance.now() + 5_000);
|
|
6131
|
+
}
|
|
6132
|
+
catch (err) {
|
|
6133
|
+
this.logDashboardDmFailure(err);
|
|
6134
|
+
}
|
|
6135
|
+
},
|
|
6136
|
+
log: (event, fields) => this.logger.info({ event, ...fields }, "Public web link"),
|
|
6137
|
+
});
|
|
6138
|
+
runner.start();
|
|
6139
|
+
}
|
|
5995
6140
|
getPublicWebStatus() {
|
|
5996
6141
|
return this.publicWebLink?.status() ?? { state: "closed" };
|
|
5997
6142
|
}
|
|
@@ -6175,8 +6320,9 @@ export class FleetManager {
|
|
|
6175
6320
|
// Show the action before any link exists. TG's Start instructions belong to confirmed DM refusal below.
|
|
6176
6321
|
const menuText = this.topicCommands.getDashboardMenuText()
|
|
6177
6322
|
+ (publicAllowed ? "\n" + t("dashboard.public_risk") : "")
|
|
6178
|
-
+ (status.state
|
|
6179
|
-
|
|
6323
|
+
+ (status.state === "closed" ? "" : status.noExpiry ? "\n" + t("dashboard.public_status_no_expiry", status.state)
|
|
6324
|
+
: "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
6325
|
+
+ (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
|
|
6180
6326
|
const choices = [{ action: "local", label: t("dashboard.local_signin") },
|
|
6181
6327
|
...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
|
|
6182
6328
|
...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
|
|
@@ -6223,6 +6369,7 @@ export class FleetManager {
|
|
|
6223
6369
|
if (!entry.publicExposureId)
|
|
6224
6370
|
return true;
|
|
6225
6371
|
const result = await this.publicWebLink?.close("admin close", entry.publicExposureId);
|
|
6372
|
+
this.forgetPublicLinkOwner();
|
|
6226
6373
|
if (result?.confirmed === false)
|
|
6227
6374
|
data.ack?.(t("dashboard.public_cleanup"));
|
|
6228
6375
|
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("dashboard.public_closed"), data.editClicked);
|
|
@@ -6253,10 +6400,11 @@ export class FleetManager {
|
|
|
6253
6400
|
return false;
|
|
6254
6401
|
this.initializeWebSessions();
|
|
6255
6402
|
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
|
|
6403
|
+
// Only a public link has a link expiry (null: none — a named tunnel's ttl_minutes 0); the code's five-minute
|
|
6404
|
+
// lifetime applies to either packet.
|
|
6257
6405
|
const minutes = Math.round(LOGIN_CODE_TTL_MS / 60_000);
|
|
6258
6406
|
const text = expiresAt !== undefined
|
|
6259
|
-
? t("dashboard.private_public", url, issued.display, minutes, formatFleetTime(expiresAt))
|
|
6407
|
+
? t("dashboard.private_public", url, issued.display, minutes, expiresAt === null ? t("dashboard.public_no_expiry") : formatFleetTime(expiresAt))
|
|
6260
6408
|
: t("dashboard.private_local", url, issued.display, minutes);
|
|
6261
6409
|
const deadline = performance.now() + 10_000;
|
|
6262
6410
|
const direct = owner.binding;
|
|
@@ -6319,6 +6467,8 @@ export class FleetManager {
|
|
|
6319
6467
|
// Still closes the step editor below and tells the clicker: a throw must not leave the menu refreshing.
|
|
6320
6468
|
this.logger.warn({ err }, "Dashboard sign-in delivery failed");
|
|
6321
6469
|
}
|
|
6470
|
+
if (ok && action === "public")
|
|
6471
|
+
this.rememberPublicLinkOwner(owner);
|
|
6322
6472
|
// Only safe, static words enter General; never link, code or platform error text.
|
|
6323
6473
|
// #1742: Telegram's failure walks through opening the DM (never a bare /start, which starts a ClassicBot agent),
|
|
6324
6474
|
// with the "先開私訊 →" button when the bot's username is known; and for a while a bare /start there counts as
|
|
@@ -6343,6 +6493,7 @@ export class FleetManager {
|
|
|
6343
6493
|
revokeWebSessions() {
|
|
6344
6494
|
this.initializeWebSessions();
|
|
6345
6495
|
void this.publicWebLink?.close("dashboard revoke");
|
|
6496
|
+
this.forgetPublicLinkOwner();
|
|
6346
6497
|
this.webLoginCodes.revoke();
|
|
6347
6498
|
this.webGuestLoginCodes.revokeAll();
|
|
6348
6499
|
this.guestRevokes++; // and every guest /web still on its way
|
|
@@ -7030,11 +7181,46 @@ export class FleetManager {
|
|
|
7030
7181
|
}
|
|
7031
7182
|
/** Start all instances from fleet config */
|
|
7032
7183
|
async startAll(configPath) {
|
|
7184
|
+
this.startupMemoryDeadline?.stop();
|
|
7185
|
+
const deadline = new StartupMemoryDeadline({
|
|
7186
|
+
current: () => this.startupMemoryDeadline === deadline && !this.shuttingDown,
|
|
7187
|
+
waiting: () => this.spawnGate.isStartupMemoryDeferred(),
|
|
7188
|
+
notify: sdNotify,
|
|
7189
|
+
log: message => this.logger.info(message),
|
|
7190
|
+
});
|
|
7191
|
+
this.startupMemoryDeadline = deadline;
|
|
7192
|
+
// A held first General must not postpone event-loop liveness monitoring.
|
|
7193
|
+
// No independent heartbeat: a genuinely frozen main loop still stops pinging.
|
|
7194
|
+
setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
|
|
7195
|
+
if (this.watchdogTimer)
|
|
7196
|
+
clearInterval(this.watchdogTimer);
|
|
7197
|
+
this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
|
|
7198
|
+
this.eventLoopWatch?.stop();
|
|
7199
|
+
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
7200
|
+
try {
|
|
7201
|
+
await this.startAllWithinDeadline(configPath, deadline);
|
|
7202
|
+
}
|
|
7203
|
+
finally {
|
|
7204
|
+
deadline.stop();
|
|
7205
|
+
if (this.startupMemoryDeadline === deadline) {
|
|
7206
|
+
this.startupMemoryDeadline = null;
|
|
7207
|
+
if (!this.startupComplete) {
|
|
7208
|
+
if (this.watchdogTimer)
|
|
7209
|
+
clearInterval(this.watchdogTimer);
|
|
7210
|
+
this.watchdogTimer = null;
|
|
7211
|
+
this.eventLoopWatch?.stop();
|
|
7212
|
+
this.eventLoopWatch = null;
|
|
7213
|
+
}
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
async startAllWithinDeadline(configPath, deadline) {
|
|
7033
7218
|
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
7034
7219
|
this.loginController?.reopen();
|
|
7035
7220
|
const startupStartedAt = Date.now();
|
|
7036
7221
|
FleetManager.signalTarget = this;
|
|
7037
7222
|
this.startupComplete = false;
|
|
7223
|
+
this.startupDashboard = null;
|
|
7038
7224
|
// Cleared here, not at the end of doStopAll: a stop has an async tail, and
|
|
7039
7225
|
// anything arriving during it is still part of the stop.
|
|
7040
7226
|
this.shuttingDown = false;
|
|
@@ -7286,7 +7472,7 @@ export class FleetManager {
|
|
|
7286
7472
|
continue;
|
|
7287
7473
|
this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
|
|
7288
7474
|
const generalDir = join(getAgendHome(), name);
|
|
7289
|
-
mkdirSync(generalDir, { recursive: true });
|
|
7475
|
+
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
7290
7476
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
7291
7477
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
7292
7478
|
fleet.instances[name] = {
|
|
@@ -7337,6 +7523,13 @@ export class FleetManager {
|
|
|
7337
7523
|
const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
7338
7524
|
const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
|
|
7339
7525
|
this.startupProgress = startupProgress;
|
|
7526
|
+
// Auth, restored chat, Classic roster and the task DB are ready. Listening is not service readiness:
|
|
7527
|
+
// the dashboard observes startup, while /health and READY retain their existing completion boundary.
|
|
7528
|
+
this.startupDashboard = new FleetStartup(this.configuredStartupInstanceNames(fleet, topicMode)
|
|
7529
|
+
.filter(name => !this.lifecycle.isPaused(name)));
|
|
7530
|
+
await this.startHealthServer(fleet.health_port ?? 19280);
|
|
7531
|
+
if (this.shuttingDown)
|
|
7532
|
+
return;
|
|
7340
7533
|
if (generals.length > 0) {
|
|
7341
7534
|
for (const [name, cfg] of generals) {
|
|
7342
7535
|
try {
|
|
@@ -7401,22 +7594,6 @@ export class FleetManager {
|
|
|
7401
7594
|
return startupProgress.start(this.restartProgressTarget());
|
|
7402
7595
|
});
|
|
7403
7596
|
}
|
|
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
7597
|
// EventLog.prune() existed but was never called, so `events` and `activity`
|
|
7421
7598
|
// grew without bound for the life of the install. Prune once at startup and
|
|
7422
7599
|
// daily after that; the timer is unref'd so it never holds the loop open.
|
|
@@ -7592,9 +7769,9 @@ export class FleetManager {
|
|
|
7592
7769
|
}
|
|
7593
7770
|
// After "fleet ready", so the notice is not the first thing people see of a restart.
|
|
7594
7771
|
void this.announceWebChatOnce(agendVersion);
|
|
7772
|
+
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
7773
|
+
this.startPublicLinkAutoOpen();
|
|
7595
7774
|
}
|
|
7596
|
-
// Health HTTP endpoint
|
|
7597
|
-
this.startHealthServer(fleet.health_port ?? 19280);
|
|
7598
7775
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
7599
7776
|
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
7600
7777
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
@@ -7611,7 +7788,11 @@ export class FleetManager {
|
|
|
7611
7788
|
// A SIGHUP may arrive after the PID/general is available but before the
|
|
7612
7789
|
// rest of startup finishes. Replay one coalesced reload only after all
|
|
7613
7790
|
// startup-owned lifecycle work and signal handlers are in place.
|
|
7791
|
+
if (this.shuttingDown || this.startupMemoryDeadline !== deadline)
|
|
7792
|
+
return;
|
|
7614
7793
|
this.finishStartup(bootUpdateMarker);
|
|
7794
|
+
this.startupDashboard = null;
|
|
7795
|
+
this.publishStartupStatus();
|
|
7615
7796
|
// Tell systemd we are ready only now. This used to fire right after the
|
|
7616
7797
|
// generals started — before adapters, classic instances, topic creation and the
|
|
7617
7798
|
// health server — so `systemctl start` returned success while the fleet was
|
|
@@ -8035,12 +8216,12 @@ export class FleetManager {
|
|
|
8035
8216
|
return;
|
|
8036
8217
|
}
|
|
8037
8218
|
const accessDir = join(this.dataDir, "access");
|
|
8038
|
-
mkdirSync(accessDir, { recursive: true });
|
|
8219
|
+
mkdirSync(accessDir, { recursive: true, mode: 0o700 });
|
|
8039
8220
|
const accessStatePath = join(accessDir, "access.json");
|
|
8040
8221
|
const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
|
|
8041
8222
|
this.warnIfAccessModeOverridden(accessManager, accessStatePath);
|
|
8042
8223
|
const inboxDir = join(this.dataDir, "inbox");
|
|
8043
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
8224
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
8044
8225
|
const adapterId = channelConfig.id ?? channelConfig.type;
|
|
8045
8226
|
const adapter = await createAdapter(channelConfig, {
|
|
8046
8227
|
id: adapterId,
|
|
@@ -8186,12 +8367,12 @@ export class FleetManager {
|
|
|
8186
8367
|
return;
|
|
8187
8368
|
}
|
|
8188
8369
|
const accessDir = join(this.dataDir, "access");
|
|
8189
|
-
mkdirSync(accessDir, { recursive: true });
|
|
8370
|
+
mkdirSync(accessDir, { recursive: true, mode: 0o700 });
|
|
8190
8371
|
const accessStatePath = join(accessDir, `access-${adapterId}.json`);
|
|
8191
8372
|
const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
|
|
8192
8373
|
this.warnIfAccessModeOverridden(accessManager, accessStatePath);
|
|
8193
8374
|
const inboxDir = join(this.dataDir, "inbox");
|
|
8194
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
8375
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
8195
8376
|
const adapter = await createAdapter(channelConfig, {
|
|
8196
8377
|
id: adapterId,
|
|
8197
8378
|
botToken,
|
|
@@ -9623,9 +9804,14 @@ export class FleetManager {
|
|
|
9623
9804
|
* history (#1565). #1920 §5.2: said in the instance's topic, General or room, so it is that channel's, by that account.
|
|
9624
9805
|
*/
|
|
9625
9806
|
emitPlatformInbound(instance, msg, text) {
|
|
9807
|
+
// #2281: where it is on the platform — a web reply to it is then a native reply there. The platform thread it came
|
|
9808
|
+
// from (#1085: a ClassicBot's `threadId` may be its registry key).
|
|
9809
|
+
const thread = msg.transportThreadId ?? msg.threadId;
|
|
9810
|
+
const posts = msg.adapterId && msg.chatId && msg.messageId
|
|
9811
|
+
? [{ adapterId: msg.adapterId, chatId: msg.chatId, ...(thread && thread !== msg.chatId ? { threadId: thread } : {}), messageId: msg.messageId }] : undefined;
|
|
9626
9812
|
this.emitSseEvent("message", {
|
|
9627
9813
|
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),
|
|
9814
|
+
channel: true, from: webChatFrom(msg.adapterId, msg.userId), ...(posts ? { posts } : {}),
|
|
9629
9815
|
});
|
|
9630
9816
|
}
|
|
9631
9817
|
/** Handle outbound tool calls from a daemon instance */
|
|
@@ -9735,6 +9921,13 @@ export class FleetManager {
|
|
|
9735
9921
|
this.logger.warn({ sink, instance: instanceName, profile, tool, enforced: true }, "tool-permissions: refused");
|
|
9736
9922
|
return { allowed: false, message: toolRefusedMessage(profile, tool) };
|
|
9737
9923
|
}
|
|
9924
|
+
// The person's dashboard switches (dashboard-permissions.ts), on top of the profile, as configured now.
|
|
9925
|
+
// Its own switches, else the defaults' — a ClassicBot identity has no instances entry and runs on the defaults (#2391 review).
|
|
9926
|
+
const switchedOff = dashboardRefusalIn(this.fleetConfig, instanceName, tool);
|
|
9927
|
+
if (switchedOff) {
|
|
9928
|
+
this.logger.warn({ sink, instance: instanceName, profile, tool, enforced: true, dashboard: true }, "tool-permissions: refused");
|
|
9929
|
+
return { allowed: false, message: switchedOff };
|
|
9930
|
+
}
|
|
9738
9931
|
this.logger.debug({ sink, instance: instanceName, profile, tool, enforced: true }, "tool-permissions: allowed");
|
|
9739
9932
|
return { allowed: true, message: "" };
|
|
9740
9933
|
}
|
|
@@ -9742,6 +9935,9 @@ export class FleetManager {
|
|
|
9742
9935
|
this.touchActivity(instanceName);
|
|
9743
9936
|
this.setTopicIcon(instanceName, "green");
|
|
9744
9937
|
const tool = msg.tool;
|
|
9938
|
+
// #1774: who sends this — taken before anything here awaits (sticker checks, the platform send): a reply that waits
|
|
9939
|
+
// must never be read as a same-name replacement's (removed and re-registered meanwhile).
|
|
9940
|
+
const sentBy = tool === "reply" ? this.webChatIdentity(instanceName) : undefined;
|
|
9745
9941
|
const args = (msg.args ?? {});
|
|
9746
9942
|
const requestId = msg.requestId;
|
|
9747
9943
|
const fleetRequestId = msg.fleetRequestId;
|
|
@@ -9921,6 +10117,7 @@ export class FleetManager {
|
|
|
9921
10117
|
args.text = `${String(args.text)}\n\n${replyButtonsFallbackText(parsedButtons.buttons)}`;
|
|
9922
10118
|
}
|
|
9923
10119
|
}
|
|
10120
|
+
const replyIdentity = sentBy; // #1774: taken at the handler's entry
|
|
9924
10121
|
const original = respond;
|
|
9925
10122
|
const respondAndRecord = (result, error) => {
|
|
9926
10123
|
let buttonsView = null;
|
|
@@ -9952,12 +10149,13 @@ export class FleetManager {
|
|
|
9952
10149
|
try {
|
|
9953
10150
|
// #1920 §5.2: shown to the instance's guests only when it landed in the instance's own topic or room.
|
|
9954
10151
|
let inChannel = false;
|
|
10152
|
+
const replyAdapterId = contextAdapterId ?? this.getInstanceAdapterId(instanceName);
|
|
9955
10153
|
try {
|
|
9956
10154
|
inChannel = outAdapter !== WEB_ONLY_REPLY_SINK
|
|
9957
|
-
&& this.landedInOwnChannel(instanceName,
|
|
10155
|
+
&& this.landedInOwnChannel(instanceName, replyAdapterId, result);
|
|
9958
10156
|
}
|
|
9959
10157
|
catch { /* not known: not the channel's (fail closed), and the bookkeeping below still runs */ }
|
|
9960
|
-
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView, inChannel);
|
|
10158
|
+
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView, inChannel, replyAdapterId ? { adapterId: replyAdapterId, result } : undefined, replyIdentity);
|
|
9961
10159
|
}
|
|
9962
10160
|
catch (err) {
|
|
9963
10161
|
this.logger.warn({ err, instanceName }, "Reply delivered but post-delivery bookkeeping failed");
|
|
@@ -10034,7 +10232,12 @@ export class FleetManager {
|
|
|
10034
10232
|
return false;
|
|
10035
10233
|
}
|
|
10036
10234
|
}
|
|
10037
|
-
afterReplyRouted(instanceName, args, senderSessionName, buttons = null, inChannel = false) {
|
|
10235
|
+
afterReplyRouted(instanceName, args, senderSessionName, buttons = null, inChannel = false, sentAs, identity = this.webChatIdentity(instanceName)) {
|
|
10236
|
+
// #2281: where the reply is, when it landed in the instance's own place — a web reply to it is then a native reply
|
|
10237
|
+
// there. A reply elsewhere (another chat, the web-only sink) records none.
|
|
10238
|
+
const sent = (sentAs?.result && typeof sentAs.result === "object" ? sentAs.result : {});
|
|
10239
|
+
const post = inChannel && sentAs && typeof sent.messageId === "string" && typeof sent.chatId === "string"
|
|
10240
|
+
? { adapterId: sentAs.adapterId, chatId: sent.chatId, ...(typeof sent.threadId === "string" && sent.threadId ? { threadId: sent.threadId } : {}), messageId: sent.messageId } : undefined;
|
|
10038
10241
|
// A reply is NOT proof the turn is over (#410) — but it is not proof of
|
|
10039
10242
|
// more work either. Split the difference: an instance that is clearly
|
|
10040
10243
|
// idle loses the button now; one that looks busy keeps it (re-posted
|
|
@@ -10061,7 +10264,16 @@ export class FleetManager {
|
|
|
10061
10264
|
// copies are done, and an instance's replies wait for the one before them: they reach the chat in order.
|
|
10062
10265
|
const replyFiles = Array.isArray(args.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
10063
10266
|
const ts = new Date().toISOString();
|
|
10064
|
-
|
|
10267
|
+
// #1774: the instance this reply is for — removed while its files were being copied (a same-name replacement
|
|
10268
|
+
// registered meanwhile included), the reply is no longer its to show: no chat made again, none of the new one's
|
|
10269
|
+
// touched, and the copies made for it go. A replacement's own replies are its own.
|
|
10270
|
+
// Its identity was taken when the reply was sent (identity), never re-read after the platform answered.
|
|
10271
|
+
const current = () => this.webChatIdentityCurrent(instanceName, identity);
|
|
10272
|
+
const show = (attachments, reserved) => {
|
|
10273
|
+
if (!current()) {
|
|
10274
|
+
this.webFiles.dropServed(attachments.map(a => a.id));
|
|
10275
|
+
return;
|
|
10276
|
+
}
|
|
10065
10277
|
// The one place a delivered agent reply reaches the web chat: the server marks it `agent` (#1306) — the only
|
|
10066
10278
|
// role that may get HTML preview cards. Never inferred from the sender name or the text.
|
|
10067
10279
|
this.emitSseEvent("message", {
|
|
@@ -10071,18 +10283,25 @@ export class FleetManager {
|
|
|
10071
10283
|
...(attachments.length ? { attachments } : {}),
|
|
10072
10284
|
...(buttons ? { buttons } : {}), // #1266
|
|
10073
10285
|
...(inChannel ? { channel: true } : {}), // #1920 §5.2
|
|
10286
|
+
...(post ? { posts: [post] } : {}), // #2281
|
|
10287
|
+
...(reserved !== undefined ? { reserved } : {}), // #1774
|
|
10074
10288
|
});
|
|
10075
10289
|
};
|
|
10076
10290
|
const before = this.webReplyOrder.get(instanceName);
|
|
10077
|
-
|
|
10291
|
+
// #1774: a removal of this instance still going decides whether a reply sent before it began is shown — it waits.
|
|
10292
|
+
const removal = this.webChatRemovalPending(instanceName, identity);
|
|
10293
|
+
if (!replyFiles.length && !before && !removal) {
|
|
10078
10294
|
show([]);
|
|
10079
10295
|
return Promise.resolve();
|
|
10080
10296
|
}
|
|
10297
|
+
// #1774: its place in the chat is now — a person's message sent while the files are copied comes after it.
|
|
10298
|
+
const reserved = this.webChatHistory.reserve();
|
|
10081
10299
|
const done = (before ?? Promise.resolve())
|
|
10082
|
-
.then(() => this.webReplyAttachments(instanceName, replyFiles))
|
|
10300
|
+
.then(() => this.webReplyAttachments(instanceName, replyFiles, current))
|
|
10301
|
+
.then(async (attachments) => { await this.webChatRemovalPending(instanceName, identity); return attachments; })
|
|
10083
10302
|
.catch((err) => { this.logger.warn({ err, instanceName }, "Reply files could not be prepared for the web chat"); return []; })
|
|
10084
10303
|
.then((attachments) => { try {
|
|
10085
|
-
show(attachments);
|
|
10304
|
+
show(attachments, reserved);
|
|
10086
10305
|
}
|
|
10087
10306
|
catch (err) {
|
|
10088
10307
|
this.logger.warn({ err, instanceName }, "Reply could not be shown in the web chat");
|
|
@@ -10097,11 +10316,13 @@ export class FleetManager {
|
|
|
10097
10316
|
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
10098
10317
|
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
10099
10318
|
*/
|
|
10100
|
-
async webReplyAttachments(instanceName, paths) {
|
|
10319
|
+
async webReplyAttachments(instanceName, paths, current = () => true) {
|
|
10101
10320
|
const home = getAgendHome();
|
|
10102
10321
|
const dir = webFilesDir(home, instanceName);
|
|
10103
10322
|
const out = [];
|
|
10104
10323
|
for (const path of paths) {
|
|
10324
|
+
if (!current())
|
|
10325
|
+
break; // #1774: removed meanwhile — copy no more for it
|
|
10105
10326
|
const id = randomBytes(16).toString("hex");
|
|
10106
10327
|
const got = dir ? await snapshotReplyFile(path, dir, id, { stateDir: home, maxDirBytes: this.webReplyMaxBytes }) : null;
|
|
10107
10328
|
if (got && "evicted" in got && got.evicted.length)
|
|
@@ -11353,6 +11574,7 @@ export class FleetManager {
|
|
|
11353
11574
|
saveFleetConfig(explicitPatches = []) {
|
|
11354
11575
|
this.publicWebLink?.refresh();
|
|
11355
11576
|
this.enforceGuestSwitch(); // #1920: a Settings save turning guests off ends their sessions
|
|
11577
|
+
this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
|
|
11356
11578
|
if (!this.fleetConfig || !this.configPath)
|
|
11357
11579
|
return;
|
|
11358
11580
|
if (!this.savedFleetConfigSnapshot)
|
|
@@ -11643,10 +11865,27 @@ export class FleetManager {
|
|
|
11643
11865
|
// Capture instance dir BEFORE lifecycle.remove, which deletes the instance
|
|
11644
11866
|
// from fleetConfig. (The backend would be unresolvable afterwards.)
|
|
11645
11867
|
const instanceDirToClean = this.getInstanceDir(name);
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11868
|
+
// #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
|
|
11869
|
+
// sent before wait for the outcome before they are shown.
|
|
11870
|
+
const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
|
|
11871
|
+
this.webChatGenerations.set(name, removedBelow);
|
|
11872
|
+
let settleRemoval;
|
|
11873
|
+
const removalPending = { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}), done: new Promise(r => { settleRemoval = r; }) };
|
|
11874
|
+
this.webChatRemovalsPending.set(name, removalPending);
|
|
11875
|
+
try {
|
|
11876
|
+
if (execution)
|
|
11877
|
+
await this.lifecycle.remove(name, authorization, execution, transition);
|
|
11878
|
+
else
|
|
11879
|
+
await this.lifecycle.remove(name, authorization);
|
|
11880
|
+
// #1774: removed — its replies still copying their files are no one's to show, replaced or not (a failed removal
|
|
11881
|
+
// throws above: its replies stay its own).
|
|
11882
|
+
this.webChatRemovals.set(name, { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}) });
|
|
11883
|
+
}
|
|
11884
|
+
finally {
|
|
11885
|
+
if (this.webChatRemovalsPending.get(name) === removalPending)
|
|
11886
|
+
this.webChatRemovalsPending.delete(name);
|
|
11887
|
+
settleRemoval();
|
|
11888
|
+
}
|
|
11650
11889
|
// A later same-name registration owns its files and caches.
|
|
11651
11890
|
// Own keys only: an instance named "constructor" is removed as any other (its inherited namesake is not a
|
|
11652
11891
|
// re-registration) — #1677 review.
|
|
@@ -12342,7 +12581,7 @@ export class FleetManager {
|
|
|
12342
12581
|
/** A preview under the inbox, named by a hash of the id (a Telegram file_id is long), pruned after a day. */
|
|
12343
12582
|
storeStickerPreview(id, picture) {
|
|
12344
12583
|
const dir = join(this.dataDir, "inbox", "sticker-previews");
|
|
12345
|
-
mkdirSync(dir, { recursive: true });
|
|
12584
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
12346
12585
|
const now = Date.now();
|
|
12347
12586
|
for (const name of readdirSync(dir)) {
|
|
12348
12587
|
try {
|
|
@@ -12408,7 +12647,7 @@ export class FleetManager {
|
|
|
12408
12647
|
if (!/^\d{15,25}$/.test(id))
|
|
12409
12648
|
throw new Error("not a Discord emoji id");
|
|
12410
12649
|
const dir = join(this.dataDir, "inbox", "emoji-previews");
|
|
12411
|
-
mkdirSync(dir, { recursive: true });
|
|
12650
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
12412
12651
|
const now = Date.now();
|
|
12413
12652
|
for (const name of readdirSync(dir)) {
|
|
12414
12653
|
try {
|
|
@@ -13043,7 +13282,7 @@ export class FleetManager {
|
|
|
13043
13282
|
const board = this.scheduler?.db.board;
|
|
13044
13283
|
if (!board)
|
|
13045
13284
|
return;
|
|
13046
|
-
const read = board.summaryRead(since.toISOString(), until.toISOString());
|
|
13285
|
+
const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
|
|
13047
13286
|
const projects = board.listProjects(true);
|
|
13048
13287
|
const stats = board.projectStats();
|
|
13049
13288
|
const names = new Map(projects.map((p) => [p.id, p.name]));
|
|
@@ -13823,9 +14062,9 @@ export class FleetManager {
|
|
|
13823
14062
|
return classic ? { chatId: classic } : null;
|
|
13824
14063
|
}
|
|
13825
14064
|
/**
|
|
13826
|
-
* #2352: the instance whose own channel lists a waiting card —
|
|
13827
|
-
*
|
|
13828
|
-
*
|
|
14065
|
+
* #2352: the instance whose own channel lists a waiting card — its project's lead — when that channel can be used now
|
|
14066
|
+
* (a fleet instance of a live world, with a topic or room). undefined: its assignee's world's General, as before.
|
|
14067
|
+
* #2434: only a card the person handed out waits for them, so the instance that handed one out is never its lane.
|
|
13829
14068
|
*/
|
|
13830
14069
|
needsYouLane(card, leads) {
|
|
13831
14070
|
const usable = (name) => {
|
|
@@ -13834,9 +14073,6 @@ export class FleetManager {
|
|
|
13834
14073
|
const world = this.getInstanceAdapterId(name);
|
|
13835
14074
|
return world !== undefined && this.worlds.has(world) && this.instanceChannelTarget(name) !== null;
|
|
13836
14075
|
};
|
|
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
14076
|
const lead = card.projectId != null ? leads.get(card.projectId) : null;
|
|
13841
14077
|
return usable(lead) ? lead : undefined;
|
|
13842
14078
|
}
|
|
@@ -13844,12 +14080,24 @@ export class FleetManager {
|
|
|
13844
14080
|
* #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
14081
|
* or a reply, never from the last render. null: it no longer waits for the person.
|
|
13846
14082
|
*/
|
|
14083
|
+
/**
|
|
14084
|
+
* #2425 follow-up: an instance that handed out a card and is no longer in the fleet — neither a configured instance nor
|
|
14085
|
+
* a ClassicBot identity (deleted). Its blocked and in-review cards fall back to the person (Needs you, the summary).
|
|
14086
|
+
* Not known before the config and the ClassicBot rooms are loaded: then no one is gone (a live dispatcher's cards never
|
|
14087
|
+
* flash into the person's list at startup).
|
|
14088
|
+
*/
|
|
14089
|
+
dispatcherGone = (name) => {
|
|
14090
|
+
const instances = this.fleetConfig?.instances, classic = this.classicChannels;
|
|
14091
|
+
if (!instances || !classic)
|
|
14092
|
+
return false;
|
|
14093
|
+
return !Object.hasOwn(instances, name) && !classic.getAll().some((ch) => ch.instanceName === name);
|
|
14094
|
+
};
|
|
13847
14095
|
needsYouRoute(code) {
|
|
13848
14096
|
const board = this.scheduler?.db.board;
|
|
13849
14097
|
const card = board?.findCard(code);
|
|
13850
14098
|
// #2425 (Fable): the same rule as the list — a button rendered before the fix (or before the opt-in went off) for a
|
|
13851
14099
|
// 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))
|
|
14100
|
+
if (!board || !card || this.fleetConfig?.needs_you?.task_board !== true || !waitsForPerson(card, this.dispatcherGone))
|
|
13853
14101
|
return null;
|
|
13854
14102
|
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13855
14103
|
const lane = this.needsYouLane(card, leads);
|
|
@@ -13857,7 +14105,7 @@ export class FleetManager {
|
|
|
13857
14105
|
}
|
|
13858
14106
|
/**
|
|
13859
14107
|
* #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
|
|
14108
|
+
* its lane's channel (#2352: its project's lead; #2434: never the instance that handed it out), else in General.
|
|
13861
14109
|
*/
|
|
13862
14110
|
needsYouTaskItems() {
|
|
13863
14111
|
// #2425: the task board's cards join Needs you only when the fleet asks for them (needs_you.task_board, off by
|
|
@@ -13869,7 +14117,7 @@ export class FleetManager {
|
|
|
13869
14117
|
if (!board)
|
|
13870
14118
|
return [];
|
|
13871
14119
|
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13872
|
-
return board.waitingForPerson().map(({ card, since }) => {
|
|
14120
|
+
return board.waitingForPerson(this.dispatcherGone).map(({ card, since }) => {
|
|
13873
14121
|
const gap = card.status === "review" ? wrapUpGap(card) : null;
|
|
13874
14122
|
const lane = this.needsYouLane(card, leads);
|
|
13875
14123
|
return {
|
|
@@ -14304,7 +14552,7 @@ export class FleetManager {
|
|
|
14304
14552
|
...(input.instance !== undefined ? { target: { adapterId: this.getInstanceAdapterId(input.instance) } } : {}),
|
|
14305
14553
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14306
14554
|
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(),
|
|
14555
|
+
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now: Date.now(),
|
|
14308
14556
|
});
|
|
14309
14557
|
}
|
|
14310
14558
|
sessionAuthorizations = null;
|
|
@@ -14329,7 +14577,7 @@ export class FleetManager {
|
|
|
14329
14577
|
session, gateway, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
14330
14578
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14331
14579
|
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,
|
|
14580
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now,
|
|
14333
14581
|
});
|
|
14334
14582
|
if (check.refusal)
|
|
14335
14583
|
return { ok: false, refusal: check.refusal };
|
|
@@ -14344,9 +14592,30 @@ export class FleetManager {
|
|
|
14344
14592
|
*/
|
|
14345
14593
|
webAuthorityState() {
|
|
14346
14594
|
if (!this.dataDir)
|
|
14347
|
-
return { enabled: false, loopbackOnly: true, epoch: 0 };
|
|
14595
|
+
return { enabled: false, loopbackOnly: true, namedLink: false, epoch: 0 };
|
|
14348
14596
|
this.authorityStateStore ??= new AuthorityStateStore(join(this.dataDir, "web-authority.json"), this.logger, undefined, undefined, () => { this.ensureDeliveryOutbox?.(); return this.deliveryOutbox?.maxWebAuthorityEpoch() ?? 0; });
|
|
14349
|
-
|
|
14597
|
+
const state = this.authorityStateStore.current(this.fleetConfig?.web?.verified_authority);
|
|
14598
|
+
// #2453 (§0.2): the named-link option seen going off — the authority of every session over the link ends now, for
|
|
14599
|
+
// good: their authorizations go (a later "on" can never bring one back without its account's Confirm).
|
|
14600
|
+
if (state.namedLinkClosed)
|
|
14601
|
+
this.sessionAuthorizationStore()?.prune(a => this.webSessions?.findByHandle(a.handle)?.surface === "local");
|
|
14602
|
+
return state;
|
|
14603
|
+
}
|
|
14604
|
+
/**
|
|
14605
|
+
* #2453 review: the web-authority state read on every configuration change (a Settings save, the host's confirm, a
|
|
14606
|
+
* reload) — so a switch turned off and on again between two requests is still seen off: its epoch moves, and the
|
|
14607
|
+
* named-link sessions' authorizations are dropped. Reads that only happen on a request would miss it.
|
|
14608
|
+
*/
|
|
14609
|
+
observeWebAuthority() {
|
|
14610
|
+
// Only once the state is in use: any authorization was given through a read that made it (and a first read at
|
|
14611
|
+
// start-up still sees what changed while the fleet was down). Nothing new is opened during a load.
|
|
14612
|
+
try {
|
|
14613
|
+
if (this.dataDir && this.authorityStateStore)
|
|
14614
|
+
this.webAuthorityState();
|
|
14615
|
+
}
|
|
14616
|
+
catch (err) {
|
|
14617
|
+
this.logger.warn({ err: err?.message }, "Web authority state could not be read after a configuration change");
|
|
14618
|
+
}
|
|
14350
14619
|
}
|
|
14351
14620
|
/**
|
|
14352
14621
|
* #1939 S2a: Settings → Web → Web authority — the state in force (the switch counts as off while its state cannot be
|
|
@@ -14361,8 +14630,10 @@ export class FleetManager {
|
|
|
14361
14630
|
const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
|
|
14362
14631
|
.filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
|
|
14363
14632
|
.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
|
-
|
|
14633
|
+
until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry),
|
|
14634
|
+
...(sessions.get(a.handle).surface === "gateway" ? { named: true } : {}) })) : [];
|
|
14635
|
+
const host = this.fleetConfig?.web?.public_link?.hostname;
|
|
14636
|
+
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null };
|
|
14366
14637
|
}
|
|
14367
14638
|
authorizationRequests = null;
|
|
14368
14639
|
/**
|
|
@@ -14375,7 +14646,9 @@ export class FleetManager {
|
|
|
14375
14646
|
const adapterOf = (adapterId) => this.adapters.get(adapterId) ?? this.worlds.get(adapterId)?.adapter;
|
|
14376
14647
|
// The host's clock, with its zone: the person may read the DM on a phone in another one.
|
|
14377
14648
|
const when = (ms) => new Date(ms).toLocaleString(undefined, { timeZoneName: "short" });
|
|
14378
|
-
const where = (peer) =>
|
|
14649
|
+
const where = (peer) => peer === "named_link"
|
|
14650
|
+
? t("web_auth.peer_named_link", String(this.fleetConfig?.web?.public_link?.hostname ?? "?")) // §0.2: the internet
|
|
14651
|
+
: t(peer === "loopback" ? "web_auth.peer_loopback" : "web_auth.peer_lan");
|
|
14379
14652
|
return this.authorizationRequests = new SessionAuthorizationRequests({
|
|
14380
14653
|
now: () => performance.now(), wallNow: () => Date.now(),
|
|
14381
14654
|
maxSessionMs: () => webAuthorityPolicy(this.fleetConfig?.web).maxSessionMs,
|
|
@@ -14409,7 +14682,8 @@ export class FleetManager {
|
|
|
14409
14682
|
const record = this.webSessions?.findByHandle(handle);
|
|
14410
14683
|
if (!record)
|
|
14411
14684
|
return { ok: false, refusal: "session_gone" };
|
|
14412
|
-
|
|
14685
|
+
// §0.2: a named-link session is judged as the gateway request it is (its exposure still the current one).
|
|
14686
|
+
const r = this.authorizeWebSession(record, peer, peer === "named_link");
|
|
14413
14687
|
// What the card says: the end of the authorization, or the session's own end when that comes first.
|
|
14414
14688
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
14415
14689
|
},
|
|
@@ -14455,7 +14729,7 @@ export class FleetManager {
|
|
|
14455
14729
|
session: s, gateway: input.gateway, peer: input.peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
14456
14730
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
14457
14731
|
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,
|
|
14732
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink }, epoch: state.epoch, now,
|
|
14459
14733
|
});
|
|
14460
14734
|
return r.refusal;
|
|
14461
14735
|
}
|
|
@@ -16566,8 +16840,12 @@ export class FleetManager {
|
|
|
16566
16840
|
from: m.from,
|
|
16567
16841
|
replyTo: m.replyTo,
|
|
16568
16842
|
cards: m.cards,
|
|
16843
|
+
posts: m.posts, // #2281: kept beside the row, never broadcast (not a row field)
|
|
16844
|
+
reserved: m.reserved, // #1774: an agent reply's place, taken when it was sent
|
|
16569
16845
|
});
|
|
16570
|
-
|
|
16846
|
+
// A sending page's own id for its pending bubble rides on the live copy only: never recorded, never replayed.
|
|
16847
|
+
const clientId = typeof data.clientId === "string" ? data.clientId : "";
|
|
16848
|
+
broadcastSseEvent(this.sseClients, event, clientId ? { ...recorded, clientId } : recorded, onError, this.webChatHistory.cursorOf(recorded));
|
|
16571
16849
|
this.searchIndex?.record(chatDoc(recorded));
|
|
16572
16850
|
// #1629: live-only receipt after the recorded agent reply. Never persist/replay it or give it an SSE cursor.
|
|
16573
16851
|
if (recorded.role === "agent")
|
|
@@ -16603,7 +16881,8 @@ export class FleetManager {
|
|
|
16603
16881
|
if (this.fleetConfig?.needs_you?.live_message === false)
|
|
16604
16882
|
return false;
|
|
16605
16883
|
const card = db.board.findCard(uuid);
|
|
16606
|
-
|
|
16884
|
+
// #2434: only while it waits for the person — a card that does not is in no Needs you, so no buttons anywhere.
|
|
16885
|
+
if (!card || !waitsForPerson(card, this.dispatcherGone))
|
|
16607
16886
|
return false;
|
|
16608
16887
|
const leads = new Map(db.board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
16609
16888
|
return this.needsYouLane(card, leads) === instance;
|
|
@@ -17356,7 +17635,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17356
17635
|
try {
|
|
17357
17636
|
const tmpPath = await dlAdapter.downloadAttachment(att.fileId);
|
|
17358
17637
|
const inboxDir = join(getAgendHome(), "workspaces", instanceName, "inbox");
|
|
17359
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
17638
|
+
mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
17360
17639
|
const dest = join(inboxDir, basename(tmpPath));
|
|
17361
17640
|
// Copy to destination — failure means this attachment is skipped
|
|
17362
17641
|
try {
|
|
@@ -17768,7 +18047,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17768
18047
|
}
|
|
17769
18048
|
}
|
|
17770
18049
|
const path = this.cliEnvPath(backend, profile);
|
|
17771
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
18050
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17772
18051
|
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
17773
18052
|
return env;
|
|
17774
18053
|
}
|
|
@@ -19467,8 +19746,27 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19467
19746
|
}
|
|
19468
19747
|
tmuxExecHelperTimer = null;
|
|
19469
19748
|
stopAllInFlight = null;
|
|
19749
|
+
/** Stops release memory. Reassess between batches without waiting for recovery. */
|
|
19750
|
+
async stopInstancesInBatches(entries, stop) {
|
|
19751
|
+
let index = 0;
|
|
19752
|
+
while (index < entries.length) {
|
|
19753
|
+
// Linux procfs can still be read after the periodic sampler is stopped.
|
|
19754
|
+
// Do not revive a stopped Darwin probe/worker just to size a quit batch;
|
|
19755
|
+
// its full shutdown uses the conservative batch size instead.
|
|
19756
|
+
const level = this.memoryPressure.platform === "darwin"
|
|
19757
|
+
? this.shuttingDown ? "elevated" : this.memoryPressure.snapshot().level
|
|
19758
|
+
: this.memoryPressure.sample().level;
|
|
19759
|
+
const size = fleetStopConcurrency(entries.length, level);
|
|
19760
|
+
const batch = entries.slice(index, index + size);
|
|
19761
|
+
index += batch.length;
|
|
19762
|
+
await Promise.allSettled(batch.map(stop));
|
|
19763
|
+
}
|
|
19764
|
+
}
|
|
19470
19765
|
async doStopAll() {
|
|
19766
|
+
this.startupMemoryDeadline?.stop();
|
|
19767
|
+
this.startupMemoryDeadline = null;
|
|
19471
19768
|
this.startupComplete = false;
|
|
19769
|
+
this.startupDashboard = null;
|
|
19472
19770
|
this.reloadPending = false;
|
|
19473
19771
|
// Before anything is stopped: everything that dies from here on dies
|
|
19474
19772
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
@@ -19480,6 +19778,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19480
19778
|
const settingsControl = this.settingsControl;
|
|
19481
19779
|
this.settingsControl = null;
|
|
19482
19780
|
const settingsControlStopped = settingsControl?.close();
|
|
19781
|
+
this.publicLinkAutoOpen?.stop();
|
|
19483
19782
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
19484
19783
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
19485
19784
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
@@ -19630,21 +19929,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19630
19929
|
// tmux ops, while small fleets stay conservative to avoid overwhelming the
|
|
19631
19930
|
// tmux server.
|
|
19632
19931
|
const entries = [...this.daemons.entries()];
|
|
19633
|
-
const STOP_CONCURRENCY = entries.length > 30 ? 15 : entries.length >= 10 ? 10 : 5;
|
|
19634
19932
|
for (const [name] of entries)
|
|
19635
19933
|
this.ipcStoppingInstances.add(name);
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
}
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
this.daemons.delete(name);
|
|
19646
|
-
}));
|
|
19647
|
-
}
|
|
19934
|
+
await this.stopInstancesInBatches(entries, async ([name, daemon]) => {
|
|
19935
|
+
try {
|
|
19936
|
+
await daemon.stop();
|
|
19937
|
+
}
|
|
19938
|
+
catch (err) {
|
|
19939
|
+
this.logger.warn({ name, err }, "Stop failed");
|
|
19940
|
+
}
|
|
19941
|
+
this.daemons.delete(name);
|
|
19942
|
+
});
|
|
19648
19943
|
// Close IPC clients in parallel — serial close over a large fleet adds
|
|
19649
19944
|
// noticeable latency.
|
|
19650
19945
|
await Promise.all([...this.instanceIpcClients.values()].map(ipc => Promise.resolve(ipc.close()).catch(() => { })));
|
|
@@ -19855,6 +20150,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19855
20150
|
this.rawFleetConfig = previousRawConfig;
|
|
19856
20151
|
this.rawFleetDocument = previousRawDocument;
|
|
19857
20152
|
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
20153
|
+
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
19858
20154
|
this.logger.error({
|
|
19859
20155
|
oldCount,
|
|
19860
20156
|
newCount,
|
|
@@ -19963,6 +20259,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19963
20259
|
const daemon = this.daemons.get(name);
|
|
19964
20260
|
const runtimeConfig = daemon.getConfigSnapshot?.() ?? oldConfig.instances[name];
|
|
19965
20261
|
const change = classifyInstanceChange(runtimeConfig, config);
|
|
20262
|
+
// (b) The person switched its task board on: tell it (its tool list shows the `task` tool at once).
|
|
20263
|
+
if (taskBoardSwitchedOn(runtimeConfig.dashboard, config.dashboard)) {
|
|
20264
|
+
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
20265
|
+
}
|
|
19966
20266
|
if (change === "restart") {
|
|
19967
20267
|
this.logger.info({ name }, "Instance config changed — restarting");
|
|
19968
20268
|
observe?.(name, "restart", "running");
|
|
@@ -21436,7 +21736,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21436
21736
|
await ipc.close();
|
|
21437
21737
|
}
|
|
21438
21738
|
this.instanceIpcClients.clear();
|
|
21439
|
-
await
|
|
21739
|
+
await this.stopInstancesInBatches(instanceNames, name => this.stopInstance(name));
|
|
21440
21740
|
// Kill remaining orphan windows to prevent stale state on restart
|
|
21441
21741
|
try {
|
|
21442
21742
|
const agendNames = new Set(instanceNames);
|
|
@@ -21757,6 +22057,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21757
22057
|
if (this.guestScope(owner).size)
|
|
21758
22058
|
return "ok";
|
|
21759
22059
|
if (owner && this.fleetConfig?.web?.guests?.enabled === true) {
|
|
22060
|
+
// Before adapters initialize, topic scope is unknown, not revoked. Keep the returning cookie but grant no
|
|
22061
|
+
// request until its configured world exists. Removed connections and initialized denied access stay final.
|
|
22062
|
+
const channels = this.fleetConfig.channels ?? (this.fleetConfig.channel ? [this.fleetConfig.channel] : []);
|
|
22063
|
+
if (this.startupDashboard && !this.worlds.has(owner.adapterId)
|
|
22064
|
+
&& channels.some(channel => (channel.id ?? channel.type) === owner.adapterId))
|
|
22065
|
+
return "pending";
|
|
21760
22066
|
const members = this.guestRooms(), rooms = guestGroupRooms(this.classicChannels?.getAll() ?? [], owner);
|
|
21761
22067
|
if (rooms.some((room) => members.pending(room, owner.userId))) {
|
|
21762
22068
|
void this.refreshGuestRooms(owner, { mode: "triggered" }); // rate-limited per room (30 s), single-flight
|
|
@@ -21855,7 +22161,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21855
22161
|
res.end(JSON.stringify({ error: WEB_HOST_REJECTED_MESSAGE }));
|
|
21856
22162
|
return;
|
|
21857
22163
|
}
|
|
21858
|
-
if (closing) {
|
|
22164
|
+
if (closing || this.shuttingDown) {
|
|
21859
22165
|
res.writeHead(503, { Connection: "close", "Retry-After": "10" });
|
|
21860
22166
|
res.end(JSON.stringify({ error: "fleet stopping" }));
|
|
21861
22167
|
return;
|
|
@@ -21903,6 +22209,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21903
22209
|
// #1920: a guest reaches only GUEST_ROUTES, within the scope derived for it now — default deny, before any
|
|
21904
22210
|
// handler, on this listener and the public link's gateway alike (both come through here).
|
|
21905
22211
|
if (decision.via === "session" && decision.session.tier === "guest") {
|
|
22212
|
+
// A browser opening a page gets a page in its language, not the JSON an API caller gets (#2399 smoke).
|
|
22213
|
+
const navigating = req.method === "GET" && String(req.headers.accept ?? "").includes("text/html") && isWebPageNavigation(requestPath);
|
|
21906
22214
|
const owner = decision.session.owner;
|
|
21907
22215
|
const admitted = decision.session;
|
|
21908
22216
|
const admission = this.guestAdmission(admitted);
|
|
@@ -21913,12 +22221,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21913
22221
|
return;
|
|
21914
22222
|
}
|
|
21915
22223
|
if (admission === "gone") {
|
|
22224
|
+
if (navigating) {
|
|
22225
|
+
serveGuestRefusedPage(res, 401, "ended");
|
|
22226
|
+
return;
|
|
22227
|
+
}
|
|
21916
22228
|
res.writeHead(401);
|
|
21917
22229
|
res.end(JSON.stringify({ error: "guest_scope_empty" }));
|
|
21918
22230
|
return;
|
|
21919
22231
|
}
|
|
21920
22232
|
const verdict = guestRouteDecision(req.method ?? "GET", parsedUrl, this.guestScope(owner));
|
|
21921
22233
|
if (verdict.kind === "deny") {
|
|
22234
|
+
if (navigating) {
|
|
22235
|
+
serveGuestRefusedPage(res, verdict.status, "members");
|
|
22236
|
+
return;
|
|
22237
|
+
}
|
|
21922
22238
|
res.writeHead(verdict.status);
|
|
21923
22239
|
res.end(JSON.stringify({ error: verdict.error }));
|
|
21924
22240
|
return;
|
|
@@ -21951,6 +22267,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21951
22267
|
res.end(JSON.stringify(health));
|
|
21952
22268
|
return;
|
|
21953
22269
|
}
|
|
22270
|
+
// Threat: lifecycle/config/chat writes racing boot-owned starts and reconciliation. Reads stay live;
|
|
22271
|
+
// authentication keeps its own checks so a browser can sign in or sign out while agents start.
|
|
22272
|
+
if (this.startupDashboard && !["GET", "HEAD", "OPTIONS"].includes(req.method ?? "GET")
|
|
22273
|
+
&& !requestPath.startsWith("/auth/")) {
|
|
22274
|
+
res.writeHead(503, { "Retry-After": "2", "Cache-Control": "no-store", Connection: "close" });
|
|
22275
|
+
res.end(JSON.stringify({ code: "fleet_starting", error: t("fleet.starting_retry") }));
|
|
22276
|
+
return;
|
|
22277
|
+
}
|
|
21954
22278
|
if (req.method === "GET" && req.url === "/status") {
|
|
21955
22279
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => {
|
|
21956
22280
|
const statusFile = join(this.getInstanceDir(name), "statusline.json");
|
|
@@ -22221,6 +22545,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22221
22545
|
}
|
|
22222
22546
|
}
|
|
22223
22547
|
startHealthServer(port) {
|
|
22548
|
+
let settled;
|
|
22549
|
+
const listening = new Promise(resolve => { settled = resolve; });
|
|
22224
22550
|
this.startedAt = Date.now();
|
|
22225
22551
|
this.healthServerListening = false;
|
|
22226
22552
|
this.healthPortRetried = false;
|
|
@@ -22235,6 +22561,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22235
22561
|
const drain = trackWebServer(server);
|
|
22236
22562
|
this.healthServer = server;
|
|
22237
22563
|
this.healthServerDrain = drain;
|
|
22564
|
+
server.once("close", settled);
|
|
22238
22565
|
const markListening = (afterTakeover = false) => {
|
|
22239
22566
|
this.healthServerListening = true;
|
|
22240
22567
|
this.logger.info({ port }, afterTakeover
|
|
@@ -22248,6 +22575,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22248
22575
|
// #1306: the preview listener starts once the web listener is bound — its frame-ancestors name the real port.
|
|
22249
22576
|
const bound = this.healthServer?.address();
|
|
22250
22577
|
this.startPreviewListener(port, bound && typeof bound === "object" ? bound.port : port);
|
|
22578
|
+
settled();
|
|
22251
22579
|
};
|
|
22252
22580
|
this.healthServer.on("error", (err) => {
|
|
22253
22581
|
this.healthServerListening = false;
|
|
@@ -22255,6 +22583,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22255
22583
|
if (this.healthPortRetried) {
|
|
22256
22584
|
this.logger.error({ err, port }, "Health port still in use after takeover — dashboard disabled");
|
|
22257
22585
|
this.notifyFleetError(t("dashboard.port_in_use", port));
|
|
22586
|
+
settled();
|
|
22258
22587
|
return;
|
|
22259
22588
|
}
|
|
22260
22589
|
this.healthPortRetried = true;
|
|
@@ -22290,16 +22619,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22290
22619
|
this.logger.debug({ err }, "Old fleet process kill skipped (already gone or no permission)");
|
|
22291
22620
|
}
|
|
22292
22621
|
setTimeout(() => {
|
|
22293
|
-
if (
|
|
22622
|
+
if (this.healthServer !== server || this.shuttingDown) {
|
|
22623
|
+
settled();
|
|
22294
22624
|
return;
|
|
22295
|
-
|
|
22625
|
+
}
|
|
22626
|
+
server.listen(port, "127.0.0.1", () => markListening(true));
|
|
22296
22627
|
}, 1500);
|
|
22297
22628
|
return;
|
|
22298
22629
|
}
|
|
22299
22630
|
this.logger.error({ err, port }, "Health server error");
|
|
22300
22631
|
this.notifyFleetError(t("dashboard.server_failed", err.message));
|
|
22632
|
+
settled();
|
|
22301
22633
|
});
|
|
22302
22634
|
this.healthServer.listen(port, "127.0.0.1", () => markListening());
|
|
22635
|
+
return listening;
|
|
22303
22636
|
}
|
|
22304
22637
|
/**
|
|
22305
22638
|
* #1306: the preview listener, beside the web listener and closed with it. When it cannot listen (the port is
|
|
@@ -22528,6 +22861,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22528
22861
|
return {
|
|
22529
22862
|
canvas_worker: this.canvasWorkerEnabled(),
|
|
22530
22863
|
instances,
|
|
22864
|
+
startup: this.startupDashboard?.snapshot() ?? null,
|
|
22531
22865
|
publicLink: this.getPublicWebStatus(),
|
|
22532
22866
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
22533
22867
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|