@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
package/dist/fleet-manager.js
CHANGED
|
@@ -7,7 +7,7 @@ import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileRes
|
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
9
9
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
10
|
-
import { createPublicWebGateway } from "./public-web-gateway.js";
|
|
10
|
+
import { createPublicWebGateway, PUBLIC_GATEWAY_RECEIVE_MS } from "./public-web-gateway.js";
|
|
11
11
|
import { handleWebIconRequest } from "./web-icons.js";
|
|
12
12
|
import { trackWebServer } from "./web-server-drain.js";
|
|
13
13
|
import { renderPublicLinkProgress, ThrottledMessageEditor } from "./public-link-progress.js";
|
|
@@ -20,10 +20,9 @@ import { withinBudget } from "./monotonic-budget.js";
|
|
|
20
20
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
21
21
|
import { RuntimeCpuProfiler, ProfileBusyError, profileDuration } from "./runtime-cpu-profile.js";
|
|
22
22
|
import { ProfileControlServer } from "./profile-control.js";
|
|
23
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
|
|
23
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, constants as fsConstants, closeSync } from "node:fs";
|
|
24
24
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
25
25
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
26
|
-
import { spawnSync } from "node:child_process";
|
|
27
26
|
import { freemem, totalmem, cpus, homedir } from "node:os";
|
|
28
27
|
import { createServer } from "node:http";
|
|
29
28
|
import { Worker } from "node:worker_threads";
|
|
@@ -34,7 +33,7 @@ import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
|
34
33
|
import { fleetLabel } from "./fleet-label.js";
|
|
35
34
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
36
35
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
37
|
-
import { sdNotify, sdNotifyBlocking } from "./sd-notify.js";
|
|
36
|
+
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
|
|
38
37
|
import { readFleetMemory } from "./process-memory.js";
|
|
39
38
|
import { MemoryPressure } from "./memory-pressure.js";
|
|
40
39
|
import { replyDedupText, ReplyDeduper } from "./reply-dedup.js";
|
|
@@ -46,7 +45,7 @@ const __dirname = dirname(__filename);
|
|
|
46
45
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
47
46
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
48
47
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
49
|
-
import { loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
48
|
+
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
50
49
|
import { EventLog } from "./event-log.js";
|
|
51
50
|
import { binaryProbe } from "./binary-probe.js";
|
|
52
51
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
@@ -64,7 +63,9 @@ import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
|
64
63
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
65
64
|
import { createBackend } from "./backend/factory.js";
|
|
66
65
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
67
|
-
import {
|
|
66
|
+
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
67
|
+
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
68
|
+
import { CLI_ENV_TTL_MS, isModelCompatible, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
|
|
68
69
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
69
70
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
70
71
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
@@ -80,7 +81,8 @@ import { RoutingEngine } from "./routing-engine.js";
|
|
|
80
81
|
import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, checkBinaryInstalled, } from "./instance-lifecycle.js";
|
|
81
82
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
82
83
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
83
|
-
import { outboundHandlers } from "./outbound-handlers.js";
|
|
84
|
+
import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
|
|
85
|
+
import { backendArgs, backendStep, joinBackendArgs, modelNameCarried, planDisplay } from "./backend-command.js";
|
|
84
86
|
import { DeliveryOutbox, DEFAULT_REPLY_OBLIGATION_TTL_MS, isSilentScheduleDelivery } from "./delivery-outbox.js";
|
|
85
87
|
// The target Daemon can legitimately wait 30 minutes for a busy pane before
|
|
86
88
|
// deciding to defer before submission. This is an alert threshold only: the
|
|
@@ -137,12 +139,31 @@ import { LogService } from "./log-service.js";
|
|
|
137
139
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId } from "./web-chat-history.js";
|
|
138
140
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
139
141
|
import { SeenUsers, resolveMentions } from "./web-mentions.js";
|
|
140
|
-
import { WebPrefsStore } from "./web-prefs.js";
|
|
142
|
+
import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
|
|
143
|
+
import { agendVersion } from "./agent-whats-new.js";
|
|
144
|
+
import { OwnerCssStore } from "./web-owner-css-store.js";
|
|
145
|
+
import { ThemeStore } from "./web-theme-store.js";
|
|
146
|
+
import { WebAppearance } from "./web-appearance.js";
|
|
147
|
+
import { WebEffectLibrary } from "./web-effect-library.js";
|
|
148
|
+
import { WebEffectTrials } from "./web-effect-trials.js";
|
|
149
|
+
import { WebEffectService } from "./web-effect-service.js";
|
|
150
|
+
import { WebEffectProfiles } from "./web-effect-profiles.js";
|
|
151
|
+
import { WebEffectTools, WEB_EFFECT_TOOLS } from "./web-effect-tools.js";
|
|
152
|
+
import { resolveEffectParams } from "./web-effect.js";
|
|
153
|
+
import { cw1EffectChecker } from "./web-mod-bundle.js";
|
|
154
|
+
import { effectiveAppearanceLimits } from "./web-appearance-limits.js";
|
|
155
|
+
import { WEB_NAME_FALLBACK, safeWebName, validateWebName, webNameInUse, webPersonKey } from "./web-profile.js";
|
|
141
156
|
import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
|
|
142
157
|
import { ReplyButtonsController } from "./reply-buttons-controller.js";
|
|
143
|
-
import {
|
|
158
|
+
import { parsePoll } from "./reply-polls.js";
|
|
159
|
+
import { isUploadRequest, limitRequestReceive, publicAttachment, sweepOrphanedUploads, UPLOAD_REQUEST_TIMEOUT_MS, uploadLimits, WebFileLedger } from "./web-upload.js";
|
|
144
160
|
import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
|
|
145
|
-
import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
161
|
+
import { cardPicture, handleViewRequest, humanCards, identityColors, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
162
|
+
import { autoIdentityColor } from "./web-identity-color.js";
|
|
163
|
+
import { AppearanceTools } from "./web-appearance-tools.js";
|
|
164
|
+
import { cardVersion, resolveCard } from "./web-card.js";
|
|
165
|
+
import { ModStore } from "./web-mod-store.js";
|
|
166
|
+
import { AgentMods } from "./web-mod-agent.js";
|
|
146
167
|
import { downloadStickerImage } from "./channel/sticker-download.js";
|
|
147
168
|
import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
|
|
148
169
|
import { accentColor, avatarFor, ownBotConnections } from "./instance-avatar.js";
|
|
@@ -153,13 +174,14 @@ import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permis
|
|
|
153
174
|
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
154
175
|
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
155
176
|
import { LoginSession } from "./login-manager.js";
|
|
177
|
+
import { runOnLoginShell } from "./login-shell.js";
|
|
156
178
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
157
179
|
import { decideSlash } from "./slash-authz.js";
|
|
158
180
|
import { canonicalCommand, commandSpec, decideCommand, ruleFor } from "./command-table.js";
|
|
159
181
|
import { runVisibilityCommand } from "./cross-instance-notice.js";
|
|
160
|
-
import { installedChannel,
|
|
182
|
+
import { installedChannel, updateNoticeKey } from "./update-check.js";
|
|
161
183
|
import { resolveInstalledAgend } from "./update-dispatch.js";
|
|
162
|
-
import { resolveUpdateLaunch, watchUpdateLaunch } from "./update-launch.js";
|
|
184
|
+
import { resolveUpdateLaunch, openUpdateLog, updateLogPath, watchUpdateLaunch } from "./update-launch.js";
|
|
163
185
|
import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOGIN_RECOVERY_DEADLINE_MS } from "./login-controller.js";
|
|
164
186
|
import { runBeforeDeadline } from "./deadline.js";
|
|
165
187
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
@@ -170,10 +192,10 @@ import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet
|
|
|
170
192
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
171
193
|
import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
172
194
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
173
|
-
import { validateFleetConfig } from "./config-validator.js";
|
|
195
|
+
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
174
196
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
175
197
|
import { presentationState, interactionSummary, sameInteractionOwner } from "./interaction-observation.js";
|
|
176
|
-
import { readLastInboundAt } from "./daemon.js";
|
|
198
|
+
import { DEFAULT_STUCK_TIMEOUT_MS, readLastInboundAt } from "./daemon.js";
|
|
177
199
|
import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
|
|
178
200
|
import { readBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
179
201
|
import { DEFAULT_WARM_OVERFLOW, WakeCoordinator } from "./wake-coordinator.js";
|
|
@@ -184,11 +206,14 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
|
|
|
184
206
|
import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
|
|
185
207
|
import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
|
|
186
208
|
import { NeedsYouHub } from "./needs-you-hub.js";
|
|
187
|
-
import { WEB_CHAT_NOTICE,
|
|
209
|
+
import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
|
|
188
210
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
189
211
|
import { buildOrgChart } from "./web-org.js";
|
|
212
|
+
import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
|
|
190
213
|
import { CacheService, WINDOWS } from "./cache-service.js";
|
|
191
214
|
import { runWebCommand } from "./web-commands.js";
|
|
215
|
+
import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
216
|
+
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
192
217
|
import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
|
|
193
218
|
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
194
219
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
@@ -353,6 +378,7 @@ const CLASSIC_BACKEND_SELECTION_TIMEOUT_MS = 60_000;
|
|
|
353
378
|
const CLASSIC_BACKEND_CALLBACK_PREFIX = "classic-backend:";
|
|
354
379
|
const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
355
380
|
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
381
|
+
const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
|
|
356
382
|
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
357
383
|
const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
|
|
358
384
|
/** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
|
|
@@ -376,6 +402,7 @@ const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
|
|
|
376
402
|
const WEB_ONLY_REPLY_SINK = {
|
|
377
403
|
type: "web",
|
|
378
404
|
supportsReplyButtons: true, // #1266: the web chat shows a reply's buttons itself
|
|
405
|
+
supportsPolls: true, // #1823: …and a poll (its pages are told; there is nothing to edit)
|
|
379
406
|
sendText: async () => ({ chatId: "web", messageId: newWebMessageId() }),
|
|
380
407
|
sendFile: async () => ({ chatId: "web", messageId: newWebMessageId() }),
|
|
381
408
|
};
|
|
@@ -672,6 +699,8 @@ export class FleetManager {
|
|
|
672
699
|
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
673
700
|
pendingEffortSelects = new Map();
|
|
674
701
|
pendingModelSelects = new Map();
|
|
702
|
+
/** #1806: in-flight /backend steps, keyed by nonce (see handleBackendSelection). */
|
|
703
|
+
pendingBackendSelects = new Map();
|
|
675
704
|
/** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
|
|
676
705
|
pendingNonceButtons = new Map();
|
|
677
706
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
@@ -753,7 +782,6 @@ export class FleetManager {
|
|
|
753
782
|
updateProgressTimer = null;
|
|
754
783
|
updateProgressEditRunning = false;
|
|
755
784
|
lastUpdateProgressText = null;
|
|
756
|
-
updateCompletionTipText = null;
|
|
757
785
|
/**
|
|
758
786
|
* #1660: an adopted update whose retire outcome is still owed. The
|
|
759
787
|
* completion summary was (or will be) posted; this watch appends the
|
|
@@ -792,7 +820,8 @@ export class FleetManager {
|
|
|
792
820
|
/** #1565: the web chat history's files (set at fleet start, once the home and the instances are known). */
|
|
793
821
|
webChatStore = null;
|
|
794
822
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
795
|
-
|
|
823
|
+
// #1937: the upload limits are the config's, read on each use (agend reload applies a change to the next upload).
|
|
824
|
+
webFiles = new WebFileLedger({ limits: () => uploadLimits(this.fleetConfig?.web) });
|
|
796
825
|
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
797
826
|
webReplyOrder = new Map();
|
|
798
827
|
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
@@ -801,6 +830,255 @@ export class FleetManager {
|
|
|
801
830
|
seenUsers = new SeenUsers();
|
|
802
831
|
webPrefsStore;
|
|
803
832
|
/** #1665: the web preferences kept on the host (<home>/web-prefs.json), read on first use. */
|
|
833
|
+
/**
|
|
834
|
+
* #1827: a web person as agents and chats see them. Signed in as an account (a sign-in code DMed to it): that
|
|
835
|
+
* account's user id — still `via web`, and only for recognition — and the name they chose, else their account's name
|
|
836
|
+
* at sign-in (made safe). Otherwise (an `agend web` session): "web-user", as before, under a name the operator may set.
|
|
837
|
+
* Names are checked again now: one an instance or bot has taken since reads as the next choice down.
|
|
838
|
+
*/
|
|
839
|
+
webPerson(owner) {
|
|
840
|
+
const key = webPersonKey(owner);
|
|
841
|
+
let profile = null;
|
|
842
|
+
try {
|
|
843
|
+
profile = this.webPrefs.profile(key);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
this.logger.warn({ err: err.message }, "Web profile unavailable");
|
|
847
|
+
}
|
|
848
|
+
const reserved = (n) => this.webNameReserved(n);
|
|
849
|
+
const own = profile?.name !== undefined ? webNameInUse(profile.name, reserved) : WEB_NAME_FALLBACK;
|
|
850
|
+
const chosen = own !== WEB_NAME_FALLBACK;
|
|
851
|
+
const platformName = owner ? safeWebName(profile?.platformName, reserved) : undefined;
|
|
852
|
+
return {
|
|
853
|
+
key, bound: !!owner,
|
|
854
|
+
userId: owner ? owner.userId : WEB_NAME_FALLBACK,
|
|
855
|
+
// A poll ballot is the account's — the same key its votes on the platform have — or the operator's.
|
|
856
|
+
voterKey: owner ? `${owner.adapterId}:${owner.userId}` : "web:operator",
|
|
857
|
+
name: chosen ? own : platformName ?? WEB_NAME_FALLBACK,
|
|
858
|
+
set: chosen,
|
|
859
|
+
...(platformName !== undefined ? { platformName } : {}),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/** #1827: set (null: clear — back to their account's name) a web person's own name; validated in full. */
|
|
863
|
+
setWebPersonName(owner, raw) {
|
|
864
|
+
let name = null;
|
|
865
|
+
if (raw !== null) {
|
|
866
|
+
const v = validateWebName(raw, (n) => this.webNameReserved(n));
|
|
867
|
+
if (!v.ok)
|
|
868
|
+
return { ok: false, status: 400, error: v.error };
|
|
869
|
+
name = v.name;
|
|
870
|
+
}
|
|
871
|
+
const w = this.webPrefs.setProfileName(webPersonKey(owner), name);
|
|
872
|
+
return w.ok ? { ok: true, person: this.webPerson(owner) } : { ok: false, status: w.status, error: w.error };
|
|
873
|
+
}
|
|
874
|
+
/** #1827 (auth-api): a sign-in as this account — its display name then is the web name it starts with. */
|
|
875
|
+
noteWebSignIn(account) {
|
|
876
|
+
if (account.username)
|
|
877
|
+
this.webPrefs.setPlatformName(webPersonKey(account), account.username);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* #1827: a name that belongs to something here — an instance (or ClassicBot room), its display name, a bot. On the web
|
|
881
|
+
* send path: it never throws (a lookup that fails reserves nothing; the name was checked in full when it was set).
|
|
882
|
+
*/
|
|
883
|
+
webNameReserved(lowered) {
|
|
884
|
+
try {
|
|
885
|
+
return this.webNameTaken(lowered);
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
webNameTaken(lowered) {
|
|
892
|
+
const low = (s) => (typeof s === "string" ? s.normalize("NFC").toLocaleLowerCase("en-US") : "");
|
|
893
|
+
for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
894
|
+
if (low(name) === lowered || low(cfg.display_name) === lowered)
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
for (const ch of this.classicChannels?.getAll() ?? [])
|
|
898
|
+
if (low(ch.instanceName) === lowered || low(ch.displayName) === lowered)
|
|
899
|
+
return true;
|
|
900
|
+
for (const w of this.worlds.values())
|
|
901
|
+
if (w.botUsername && low(w.botUsername) === lowered)
|
|
902
|
+
return true;
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
themeStoreInstance;
|
|
906
|
+
webAppearanceService;
|
|
907
|
+
/** #1629: every instance's theme, placements, assets and lock (web-themes.json). */
|
|
908
|
+
get themeStore() {
|
|
909
|
+
// r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
|
|
910
|
+
return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits, effectsDropped: ids => this.effectLibrary.collectUnreferenced(ids) }));
|
|
911
|
+
}
|
|
912
|
+
/** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
|
|
913
|
+
get webAppearance() {
|
|
914
|
+
return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
|
|
915
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
916
|
+
}
|
|
917
|
+
catch { /* the next frame carries the rev */ } }));
|
|
918
|
+
}
|
|
919
|
+
effectLibraryInstance;
|
|
920
|
+
effectTrialsInstance;
|
|
921
|
+
/** Lazy, single-owner storage. Register every host reference source before any operation can collect. */
|
|
922
|
+
get effectLibrary() {
|
|
923
|
+
if (!this.effectLibraryInstance) {
|
|
924
|
+
const library = new WebEffectLibrary(this.dataDir);
|
|
925
|
+
// Providers are lazy; publish only after every registration succeeds.
|
|
926
|
+
library.registerReferenceSource("themes", () => {
|
|
927
|
+
if (!this.themeStore.readable())
|
|
928
|
+
throw new Error("themes_unreadable");
|
|
929
|
+
return this.themeStore.placedEffects().flatMap(row => row.ids.map(id => ({ id, holder: `instance:${row.name}` })));
|
|
930
|
+
});
|
|
931
|
+
library.registerReferenceSource("prefs", () => this.webPrefs.ownerEffectReferences());
|
|
932
|
+
library.registerReferenceSource("mods", () => {
|
|
933
|
+
const mods = this.mods.list();
|
|
934
|
+
if (mods.unreadable)
|
|
935
|
+
throw new Error("mods_unreadable");
|
|
936
|
+
return mods.mods.flatMap(mod => mod.effects.map(effect => ({ id: effect.id, holder: `mod:${mod.id}` })));
|
|
937
|
+
});
|
|
938
|
+
this.effectTrialsInstance = new WebEffectTrials({ library, known: name => this.isKnownInstance(name) });
|
|
939
|
+
this.effectLibraryInstance = library;
|
|
940
|
+
}
|
|
941
|
+
return this.effectLibraryInstance;
|
|
942
|
+
}
|
|
943
|
+
/** Volatile Try offers and session-bound runs share this library. */
|
|
944
|
+
get effectTrials() { if (!this.effectTrialsInstance)
|
|
945
|
+
void this.effectLibrary; return this.effectTrialsInstance; }
|
|
946
|
+
effectProfilesInstance;
|
|
947
|
+
get effectProfiles() { return (this.effectProfilesInstance ??= new WebEffectProfiles(this.dataDir)); }
|
|
948
|
+
effectServiceInstance;
|
|
949
|
+
get effectService() {
|
|
950
|
+
return (this.effectServiceInstance ??= new WebEffectService({ library: this.effectLibrary, themes: this.themeStore, profiles: this.effectProfiles, prefs: this.webPrefs, trials: this.effectTrials,
|
|
951
|
+
known: name => this.isKnownInstance(name), changed: () => { try {
|
|
952
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
953
|
+
}
|
|
954
|
+
catch { /* next status carries revision */ } } }));
|
|
955
|
+
}
|
|
956
|
+
effectToolsService;
|
|
957
|
+
/** Caller identity is supplied only by the owned IPC connection / verified agent endpoint token. */
|
|
958
|
+
get effectTools() {
|
|
959
|
+
return (this.effectToolsService ??= new WebEffectTools({
|
|
960
|
+
library: this.effectLibrary, limiter: this.webAppearance.limiter,
|
|
961
|
+
authorize: (name, buckets) => !this.isKnownInstance(name) ? { error: "unknown_instance" } : this.webAppearance.authorizeEffect(name, buckets),
|
|
962
|
+
placed: name => this.themeStore.get(name).placements.effects,
|
|
963
|
+
place: (name, effects) => {
|
|
964
|
+
const before = this.themeStore.get(name).placements.effects;
|
|
965
|
+
const explicit = new Set(effects.filter(e => typeof e !== "string").map(e => e.id));
|
|
966
|
+
const w = this.themeStore.setPlacements(name, { effects }, "agent", what => {
|
|
967
|
+
// An unchanged human/private placement may be retained, never exposed or newly acquired by an agent.
|
|
968
|
+
const r = explicit.has(what.id) ? this.effectLibrary.read(what.id, { kind: "agent", id: name })
|
|
969
|
+
: this.effectLibrary.readReferenced(what.id, `instance:${name}`);
|
|
970
|
+
if (!r.ok)
|
|
971
|
+
return r.error;
|
|
972
|
+
const params = explicit.has(what.id) ? what.params : before.find(e => e.id === what.id)?.params;
|
|
973
|
+
return resolveEffectParams(r.entry.manifest, params ?? {}).ok ? null : "effect_params";
|
|
974
|
+
});
|
|
975
|
+
return w.ok ? { ok: true, rev: w.rev } : { ok: false, error: w.error };
|
|
976
|
+
},
|
|
977
|
+
changed: () => { try {
|
|
978
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
979
|
+
}
|
|
980
|
+
catch { /* later status carries placements */ } },
|
|
981
|
+
// A nonmutating own/shared offer is not an appearance write. Only an explicit local-admin click may start it.
|
|
982
|
+
preview: async (instance, id) => {
|
|
983
|
+
const result = this.effectTrials.offer(instance, id);
|
|
984
|
+
if (!result.ok)
|
|
985
|
+
return { error: "not_found" };
|
|
986
|
+
const present = this.webChatHistory.list(instance).some(row => row.effectTrial?.offer === result.offer.offer);
|
|
987
|
+
if (!present && !this.publishEffectTrial(instance, result.offer))
|
|
988
|
+
return { error: "trial_unavailable" };
|
|
989
|
+
return { id, status: "offered", expiresAt: result.offer.expiresAt, message: "A trial card is in your web chat. A signed-in local admin can start it; nothing was applied." };
|
|
990
|
+
},
|
|
991
|
+
profiles: id => this.effectProfiles.read(id),
|
|
992
|
+
profileSummary: id => this.effectService.profileSummary(id),
|
|
993
|
+
profileSummaries: ids => this.effectService.profileSummaries(ids),
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
appearanceToolsService;
|
|
997
|
+
/** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
|
|
998
|
+
get appearanceTools() {
|
|
999
|
+
return (this.appearanceToolsService ??= new AppearanceTools({
|
|
1000
|
+
frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
|
|
1001
|
+
theme: (name) => { const r = this.themeStore.get(name); return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets }; },
|
|
1002
|
+
card: (name) => resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card),
|
|
1003
|
+
// #1958: the host's identity-colour settings, as the page reads them (an unreadable prefs file: the defaults).
|
|
1004
|
+
colorPrefs: () => { try {
|
|
1005
|
+
const v = this.webPrefs.read(OPERATOR).values;
|
|
1006
|
+
return { show: v["color.show"], off: v["color.off"] === true };
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
return {};
|
|
1010
|
+
} },
|
|
1011
|
+
cardPicture: (name, slot) => cardPicture(this, name, slot),
|
|
1012
|
+
themeAsset: (name, id) => this.themeStore.asset(name, id),
|
|
1013
|
+
profileAvatar: (name) => this.webImage("avatar", name),
|
|
1014
|
+
botAvatar: (_name, key) => this.webImage("bot", key),
|
|
1015
|
+
themeAnswer: (name) => this.webAppearance.getTheme(name),
|
|
1016
|
+
}));
|
|
1017
|
+
}
|
|
1018
|
+
/** #1958: a profile, card or colour write (view-api.ts) landed: every open page gets a fresh status frame. */
|
|
1019
|
+
identityChanged() {
|
|
1020
|
+
try {
|
|
1021
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1022
|
+
}
|
|
1023
|
+
catch { /* the next frame carries it */ }
|
|
1024
|
+
}
|
|
1025
|
+
/** A fleet.yaml instance (own key) or a ClassicBot room, now (#1629: a person's reset/lock names one). */
|
|
1026
|
+
isKnownInstance(name) {
|
|
1027
|
+
return Object.hasOwn(this.fleetConfig?.instances ?? {}, name) || (this.classicChannels?.getAll() ?? []).some((ch) => ch.instanceName === name);
|
|
1028
|
+
}
|
|
1029
|
+
modStore;
|
|
1030
|
+
/**
|
|
1031
|
+
* #1629 r4-5 / B2: installed mods use the same validated library and scoped holder claims as direct uploads.
|
|
1032
|
+
*/
|
|
1033
|
+
get mods() {
|
|
1034
|
+
return (this.modStore ??= new ModStore(this.dataDir, {
|
|
1035
|
+
themes: this.themeStore,
|
|
1036
|
+
prefs: this.webPrefs,
|
|
1037
|
+
ownerCss: this.ownerCss,
|
|
1038
|
+
effects: this.effectLibrary,
|
|
1039
|
+
context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
|
|
1040
|
+
isKnownInstance: (name) => this.isKnownInstance(name),
|
|
1041
|
+
prefsIdentity: OPERATOR,
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
1044
|
+
agentModsService;
|
|
1045
|
+
/** #1629 r4-5b: export_web_mod / propose_web_mod — the caller's own look as a file, and a proposal for a person. */
|
|
1046
|
+
get agentMods() {
|
|
1047
|
+
return (this.agentModsService ??= new AgentMods({
|
|
1048
|
+
themes: this.themeStore, mods: this.mods, limiter: this.webAppearance.limiter,
|
|
1049
|
+
webConfig: () => this.fleetConfig?.web ?? null,
|
|
1050
|
+
// The mod store's own: a proposal is checked exactly as the person's import of it will be.
|
|
1051
|
+
context: () => this.mods.checkContext(),
|
|
1052
|
+
readOwnEffect: (instance, id) => { const r = this.effectLibrary.read(id, { kind: "agent", id: instance }); return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null; },
|
|
1053
|
+
notice: (instance, text) => { try {
|
|
1054
|
+
this.emitSseEvent("message", { instance, sender: instance, role: "status", text, ts: new Date().toISOString() });
|
|
1055
|
+
}
|
|
1056
|
+
catch { /* the page reads it from Settings too */ } },
|
|
1057
|
+
changed: () => { try {
|
|
1058
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1059
|
+
}
|
|
1060
|
+
catch { /* the next frame */ } },
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* #1629 r4-5c: an effect as this person may carry it away in an export — their own claim, or one a person shared —
|
|
1065
|
+
* read with their principal (Sol's ACL). Being able to view or run a placed effect never grants packaging someone
|
|
1066
|
+
* else's private claim: the library's reference read is runtime access, not an export path. Null: not theirs.
|
|
1067
|
+
*/
|
|
1068
|
+
modPlacedEffect(person, id) {
|
|
1069
|
+
const r = this.effectLibrary.read(id, { kind: "human", id: person });
|
|
1070
|
+
return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null;
|
|
1071
|
+
}
|
|
1072
|
+
/** #1629 r4-5: an instance's card text as every viewer sees it (a person's fields over the agent's). */
|
|
1073
|
+
modCardText(name) {
|
|
1074
|
+
const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
|
|
1075
|
+
return { ...(card.tagline ? { tagline: card.tagline.text } : {}), ...(card.bio ? { bio: card.bio.text } : {}) };
|
|
1076
|
+
}
|
|
1077
|
+
ownerCssStore;
|
|
1078
|
+
/** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
|
|
1079
|
+
get ownerCss() {
|
|
1080
|
+
return (this.ownerCssStore ??= new OwnerCssStore(this.dataDir, this.webPrefs));
|
|
1081
|
+
}
|
|
804
1082
|
get webPrefs() {
|
|
805
1083
|
// #1664: a favorite must be an instance of this fleet now — fleet.yaml's (own keys) or a ClassicBot room's.
|
|
806
1084
|
return (this.webPrefsStore ??= new WebPrefsStore(this.dataDir, {
|
|
@@ -1149,6 +1427,8 @@ export class FleetManager {
|
|
|
1149
1427
|
mayClick: (set, userId) => this.mayAnswerReplyButtons(set.instance, set.adapterId, userId),
|
|
1150
1428
|
deliver: (set, button, by) => this.deliverReplyButtonChoice(set, button, by),
|
|
1151
1429
|
publish: (instance, view) => this.emitSseEvent("reply_buttons", { instance, buttons: view }),
|
|
1430
|
+
admitResults: (set, key, text) => this.admitPollResults(set, key, text),
|
|
1431
|
+
monotonic: () => performance.now(),
|
|
1152
1432
|
logger: this.logger,
|
|
1153
1433
|
setTimer: (fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; },
|
|
1154
1434
|
clearTimer: (h) => clearTimeout(h),
|
|
@@ -1157,6 +1437,21 @@ export class FleetManager {
|
|
|
1157
1437
|
return this.replyButtonsCtl;
|
|
1158
1438
|
}
|
|
1159
1439
|
replyButtonsStore = null;
|
|
1440
|
+
/**
|
|
1441
|
+
* #1823: a closed poll's results go to its instance through the delivery outbox, as a system notice keyed by the
|
|
1442
|
+
* poll: durable (held until the instance can take it) and admitted once, however often this is asked. Addressed to
|
|
1443
|
+
* the poll's chat, so the instance's answer (the right option, the scores) goes there.
|
|
1444
|
+
*/
|
|
1445
|
+
admitPollResults(set, key, text) {
|
|
1446
|
+
this.ensureDeliveryOutbox();
|
|
1447
|
+
const outbox = this.deliveryOutbox;
|
|
1448
|
+
if (!outbox)
|
|
1449
|
+
return false;
|
|
1450
|
+
outbox.admitSystemNotice(set.instance, key, text, new Date(), {
|
|
1451
|
+
chatId: set.chatId, threadId: set.threadId || undefined, adapterId: set.adapterId === "web" ? undefined : set.adapterId,
|
|
1452
|
+
});
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1160
1455
|
/**
|
|
1161
1456
|
* #1266: who may answer a reply's buttons on a platform — whoever may message that instance there: in a ClassicBot
|
|
1162
1457
|
* room anyone there (as a typed message), elsewhere someone the sending connection lets speak (#754/#1148), never a
|
|
@@ -1220,8 +1515,10 @@ export class FleetManager {
|
|
|
1220
1515
|
return false;
|
|
1221
1516
|
reactions.consume();
|
|
1222
1517
|
this.afterUserInboundDelivered(set.instance, inbound, deliveryEpoch);
|
|
1518
|
+
// The web chat shows the click as its button's label (a chip); `text` stays what the agent was given.
|
|
1223
1519
|
this.emitSseEvent("message", {
|
|
1224
1520
|
instance: set.instance, sender: by.username, role: "user", text: content, ts, ...(web ? { messageId } : {}),
|
|
1521
|
+
click: { label: button.label, of: set.id },
|
|
1225
1522
|
});
|
|
1226
1523
|
return true;
|
|
1227
1524
|
}
|
|
@@ -1564,6 +1861,7 @@ export class FleetManager {
|
|
|
1564
1861
|
content: payload.content,
|
|
1565
1862
|
delivery_id: claimed.deliveryId,
|
|
1566
1863
|
delivery_attempt: String(claimed.attemptNo),
|
|
1864
|
+
...(isSilentScheduleDelivery(claimed) ? { silent_schedule: true } : {}),
|
|
1567
1865
|
}, { waitForIdle: false, noInlineWake: this.deliveryWorkerMode(target) !== "off" });
|
|
1568
1866
|
}
|
|
1569
1867
|
else {
|
|
@@ -2203,7 +2501,7 @@ export class FleetManager {
|
|
|
2203
2501
|
async handleUpdateSlash(data, adapterId) {
|
|
2204
2502
|
const gate = this.fleetAdminGate(data.userId, adapterId);
|
|
2205
2503
|
if (gate !== "ok") {
|
|
2206
|
-
await data.respond(
|
|
2504
|
+
await data.respond(gate === "disabled" ? t("update.disabled", this.adminListKey(adapterId)) : t("not_authorized"));
|
|
2207
2505
|
return;
|
|
2208
2506
|
}
|
|
2209
2507
|
const messageId = await data.respond(t("update.progress.preparing", 0));
|
|
@@ -2218,24 +2516,29 @@ export class FleetManager {
|
|
|
2218
2516
|
// "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
|
|
2219
2517
|
const installed = await resolveInstalledAgend();
|
|
2220
2518
|
if (!installed.ok) {
|
|
2221
|
-
this.failUpdateProgress(
|
|
2519
|
+
this.failUpdateProgress(`${t("update.failure.shell")} ${t("update.failure.installed", installed.reason)}`);
|
|
2222
2520
|
return;
|
|
2223
2521
|
}
|
|
2224
2522
|
const origin = `slash /update by ${adapterId}:${data.userId}`;
|
|
2225
2523
|
recordInternalRequest(this.dataDir, "update", origin);
|
|
2226
2524
|
const launch = await resolveUpdateLaunch(installed.agend);
|
|
2227
2525
|
if (!launch.ok) {
|
|
2228
|
-
this.failUpdateProgress(
|
|
2526
|
+
this.failUpdateProgress(`${t("update.failure.shell")} ${t("update.failure.independent", launch.reason)}`);
|
|
2229
2527
|
return;
|
|
2230
2528
|
}
|
|
2231
2529
|
if (this.fleetAdminGate(data.userId, adapterId) !== "ok" || this.shuttingDown) {
|
|
2232
2530
|
this.failUpdateProgress(t("not_authorized"));
|
|
2233
2531
|
return;
|
|
2234
2532
|
}
|
|
2533
|
+
const logPath = updateLogPath(this.dataDir);
|
|
2534
|
+
const ts = new Date().toISOString();
|
|
2535
|
+
const logFd = openUpdateLog(logPath, `--- /update ${ts} ---`);
|
|
2235
2536
|
const child = spawn(launch.command, launch.args, {
|
|
2236
|
-
detached: true, stdio: "ignore", env: withOrigin(origin),
|
|
2537
|
+
detached: true, stdio: ["ignore", logFd ? logFd.fd : "ignore", logFd ? logFd.fd : "ignore"], env: withOrigin(origin),
|
|
2237
2538
|
});
|
|
2238
|
-
|
|
2539
|
+
if (logFd)
|
|
2540
|
+
closeSync(logFd.fd); // parent closes its copy; the child's fd stays open
|
|
2541
|
+
watchUpdateLaunch(child, message => this.failUpdateProgress(message), logFd ? logPath : undefined, logFd?.offset, installed.agend);
|
|
2239
2542
|
child.unref();
|
|
2240
2543
|
}
|
|
2241
2544
|
async handleProfileSlash(data, adapterId) {
|
|
@@ -2708,6 +3011,32 @@ export class FleetManager {
|
|
|
2708
3011
|
emit: (event, data) => this.emitSseEvent(event, data),
|
|
2709
3012
|
});
|
|
2710
3013
|
}
|
|
3014
|
+
/** Board 003648ac: a web /compact followed to its end, from the transcript the Log reads (none: nothing to follow). */
|
|
3015
|
+
compactWatchService = null;
|
|
3016
|
+
get compactWatch() {
|
|
3017
|
+
return this.compactWatchService ??= new CompactWatch({
|
|
3018
|
+
source: (name) => { const src = this.logSource(name); return { path: src.path, format: src.format }; },
|
|
3019
|
+
// The launch that runs it now (the same value the daemon sends with raw_paste_written).
|
|
3020
|
+
owner: (name) => {
|
|
3021
|
+
const daemon = this.lifecycle.daemons.get(name);
|
|
3022
|
+
try {
|
|
3023
|
+
return daemon ? launchIdentity(daemon.getInteractionSnapshot().owner) : null;
|
|
3024
|
+
}
|
|
3025
|
+
catch {
|
|
3026
|
+
return null;
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
paneFollow: (name) => {
|
|
3030
|
+
try {
|
|
3031
|
+
return this.lifecycle.daemons.get(name)?.compactPaneUi() != null;
|
|
3032
|
+
}
|
|
3033
|
+
catch {
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
changed: () => this.emitSseEvent("status", this.getUiStatus()),
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
2711
3040
|
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
2712
3041
|
logSteps(name, q) { return this.log.steps(name, q); }
|
|
2713
3042
|
/**
|
|
@@ -3117,7 +3446,8 @@ export class FleetManager {
|
|
|
3117
3446
|
* or a rebound adapter is never posted through a stale route. A copy
|
|
3118
3447
|
* already in flight may still land late — the contract permits that.
|
|
3119
3448
|
*/
|
|
3120
|
-
|
|
3449
|
+
/** `sender` (#1827): the web person's name, as the room log records them ("web-user" when none). */
|
|
3450
|
+
async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK) {
|
|
3121
3451
|
const guard = this.webChannelEchoGuards.get(instance);
|
|
3122
3452
|
const handled = new Set();
|
|
3123
3453
|
let posted = 0;
|
|
@@ -3148,7 +3478,7 @@ export class FleetManager {
|
|
|
3148
3478
|
if (!adapter?.sendText)
|
|
3149
3479
|
continue;
|
|
3150
3480
|
await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
|
|
3151
|
-
ClassicChannelManager.logMessage(instance,
|
|
3481
|
+
ClassicChannelManager.logMessage(instance, sender, text, new Date());
|
|
3152
3482
|
posted++;
|
|
3153
3483
|
}
|
|
3154
3484
|
catch (err) {
|
|
@@ -3475,6 +3805,20 @@ export class FleetManager {
|
|
|
3475
3805
|
hasFleetAdmins(adapterId) {
|
|
3476
3806
|
return (this.adminListOf(adapterId)?.length ?? 0) > 0;
|
|
3477
3807
|
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Where this adapter's fleet-admin list is in fleet.yaml, as the operator wrote it, for the "disabled" replies:
|
|
3810
|
+
* `channel.access.allowed_users` (the single-channel form), else the `channels:` entry — by its `id:`, or its
|
|
3811
|
+
* `type:` when it has none. Read from the raw file: loading turns `channel:` into `channels` and fills in ids.
|
|
3812
|
+
*/
|
|
3813
|
+
adminListKey(adapterId) {
|
|
3814
|
+
const raw = this.rawFleetConfig;
|
|
3815
|
+
if (!Array.isArray(raw.channels) && raw.channel)
|
|
3816
|
+
return "channel.access.allowed_users";
|
|
3817
|
+
const channels = Array.isArray(raw.channels) ? raw.channels : this.fleetConfig?.channels ?? [];
|
|
3818
|
+
const id = adapterId ?? this.getPrimaryAdapterId();
|
|
3819
|
+
const entry = channels.find(ch => (ch.id ?? ch.type) === id);
|
|
3820
|
+
return `channels[${entry?.id ? `id: ${entry.id}` : `type: ${entry?.type ?? id}`}].access.allowed_users`;
|
|
3821
|
+
}
|
|
3478
3822
|
/**
|
|
3479
3823
|
* The one question the privileged commands (/update, /doctor, /dashboard, /collab) ask: may THIS caller,
|
|
3480
3824
|
* through THIS adapter, run it. `disabled` is "nobody can" — an empty list — and is told apart from
|
|
@@ -3552,7 +3896,14 @@ export class FleetManager {
|
|
|
3552
3896
|
if (!decision.allow) {
|
|
3553
3897
|
this.logger.info({ command: data.command, adapterId, scope, reply: decision.reply[0] }, "Slash command refused by the command table");
|
|
3554
3898
|
const [key, ...args] = decision.reply;
|
|
3555
|
-
|
|
3899
|
+
// A command off for want of fleet admins names where they go (`{0}` in its disabled reply).
|
|
3900
|
+
if (decision.reply === spec.disabled)
|
|
3901
|
+
args.push(this.adminListKey(adapterId));
|
|
3902
|
+
// #1990: a refusal that sends the person to General links to it — not in a ClassicBot room, whose readers need
|
|
3903
|
+
// not be in the fleet's guild.
|
|
3904
|
+
const toGeneral = GENERAL_POINTING_REFUSALS.has(key) && scope !== "classic" && scope !== "none";
|
|
3905
|
+
const reply = toGeneral ? this.renderPlaces(adapterId, t(key, ...args) + placeSuffix({ kind: "general" }, "General")).text : t(key, ...args);
|
|
3906
|
+
await data.respond(reply).catch(() => { });
|
|
3556
3907
|
return;
|
|
3557
3908
|
}
|
|
3558
3909
|
if (data.command === "start") {
|
|
@@ -3611,6 +3962,9 @@ export class FleetManager {
|
|
|
3611
3962
|
else if (data.command === "model") {
|
|
3612
3963
|
await this.handleModelSlash(data, adapterId);
|
|
3613
3964
|
}
|
|
3965
|
+
else if (data.command === "backend") {
|
|
3966
|
+
await this.handleBackendSlash(data, adapterId);
|
|
3967
|
+
}
|
|
3614
3968
|
else if (data.command === "clear") {
|
|
3615
3969
|
await this.handleClearSlash(data, adapterId);
|
|
3616
3970
|
}
|
|
@@ -3704,7 +4058,7 @@ export class FleetManager {
|
|
|
3704
4058
|
await data.respond(result.durable ? t("dashboard.revoked", result.count) : t("dashboard.revoked_not_durable", result.count));
|
|
3705
4059
|
}
|
|
3706
4060
|
else {
|
|
3707
|
-
const owner = this.dashboardOwner(data.userId, adapterId, data.channelId);
|
|
4061
|
+
const owner = this.dashboardOwner(data.userId, adapterId, data.channelId, undefined, data.username);
|
|
3708
4062
|
if (!owner) {
|
|
3709
4063
|
await this.sendLocalDashboard(data, adapterId);
|
|
3710
4064
|
return;
|
|
@@ -5026,7 +5380,7 @@ export class FleetManager {
|
|
|
5026
5380
|
&& cfg.topic_id?.toString() === owner.threadId);
|
|
5027
5381
|
}
|
|
5028
5382
|
/** Exact owning General. No primary-adapter or name fallback participates in authorization. */
|
|
5029
|
-
dashboardOwner(userId, adapterId, address, threadId) {
|
|
5383
|
+
dashboardOwner(userId, adapterId, address, threadId, username) {
|
|
5030
5384
|
const world = this.worlds.get(adapterId);
|
|
5031
5385
|
if (!world || !this.hasFleetAdmins(adapterId) || !this.isFleetAdmin(userId, adapterId))
|
|
5032
5386
|
return null;
|
|
@@ -5041,7 +5395,7 @@ export class FleetManager {
|
|
|
5041
5395
|
const matched = dc ? topic === address || (group === address && topic === threadId)
|
|
5042
5396
|
: group === address && ((topic === "1" && (threadId === undefined || threadId === "1")) || topic === threadId);
|
|
5043
5397
|
if (group && matched)
|
|
5044
|
-
return { userId, adapterId, chatId: group, threadId: topic, binding: world.adapter };
|
|
5398
|
+
return { userId, adapterId, chatId: group, threadId: topic, binding: world.adapter, ...(username ? { username } : {}) };
|
|
5045
5399
|
}
|
|
5046
5400
|
return null;
|
|
5047
5401
|
}
|
|
@@ -5050,7 +5404,7 @@ export class FleetManager {
|
|
|
5050
5404
|
* — this bot's configured group and its one General topic. No address to match in a DM, so it must be unambiguous:
|
|
5051
5405
|
* no General, or more than one on this bot and group, and there is no owner (nothing is guessed).
|
|
5052
5406
|
*/
|
|
5053
|
-
dmDashboardOwner(userId, adapterId) {
|
|
5407
|
+
dmDashboardOwner(userId, adapterId, username) {
|
|
5054
5408
|
const world = this.worlds.get(adapterId);
|
|
5055
5409
|
if (!world || !this.hasFleetAdmins(adapterId) || !this.isFleetAdmin(userId, adapterId))
|
|
5056
5410
|
return null;
|
|
@@ -5064,7 +5418,7 @@ export class FleetManager {
|
|
|
5064
5418
|
.map(([, cfg]) => cfg.topic_id.toString());
|
|
5065
5419
|
if (topics.length !== 1)
|
|
5066
5420
|
return null;
|
|
5067
|
-
const owner = { userId, adapterId, chatId: group, threadId: topics[0], binding: world.adapter };
|
|
5421
|
+
const owner = { userId, adapterId, chatId: group, threadId: topics[0], binding: world.adapter, ...(username ? { username } : {}) };
|
|
5068
5422
|
return this.publicOwnerCurrent(owner) ? owner : null;
|
|
5069
5423
|
}
|
|
5070
5424
|
/**
|
|
@@ -5091,7 +5445,7 @@ export class FleetManager {
|
|
|
5091
5445
|
async startWebInDm(msg, adapter) {
|
|
5092
5446
|
if (!adapter)
|
|
5093
5447
|
return;
|
|
5094
|
-
const owner = msg.chatId === msg.userId ? this.dmDashboardOwner(msg.userId, msg.adapterId ?? "") : null;
|
|
5448
|
+
const owner = msg.chatId === msg.userId ? this.dmDashboardOwner(msg.userId, msg.adapterId ?? "", msg.username) : null;
|
|
5095
5449
|
if (!owner) {
|
|
5096
5450
|
await adapter.sendText(msg.chatId, t("dashboard.start_web_ack"));
|
|
5097
5451
|
return;
|
|
@@ -5144,10 +5498,11 @@ export class FleetManager {
|
|
|
5144
5498
|
/** Telegram's typed command enters through the same strict owner and nonce path as Discord. */
|
|
5145
5499
|
async dashboardMenu(msg) {
|
|
5146
5500
|
const id = msg.adapterId;
|
|
5147
|
-
const owner = id ? this.dashboardOwner(msg.userId, id, msg.chatId, msg.threadId) : null;
|
|
5501
|
+
const owner = id ? this.dashboardOwner(msg.userId, id, msg.chatId, msg.threadId, msg.username) : null;
|
|
5148
5502
|
if (!owner) {
|
|
5149
5503
|
const adapter = id ? this.adapters.get(id) : undefined;
|
|
5150
|
-
|
|
5504
|
+
const out = this.renderPlaces(id, t("dashboard.public_general") + placeSuffix({ kind: "general" }, "General"), { threadId: msg.threadId });
|
|
5505
|
+
await adapter?.sendText(msg.chatId, out.text, out.opts);
|
|
5151
5506
|
return;
|
|
5152
5507
|
}
|
|
5153
5508
|
if (!await this.showDashboardMenu(owner)) {
|
|
@@ -5177,7 +5532,9 @@ export class FleetManager {
|
|
|
5177
5532
|
return;
|
|
5178
5533
|
}
|
|
5179
5534
|
this.initializeWebSessions();
|
|
5180
|
-
|
|
5535
|
+
// #1827: the code goes privately to this account (a DM, or else this slash command's ephemeral answer): whoever
|
|
5536
|
+
// redeems it is recognised as that account. Recognition only — no owner (this is not an owning General's code).
|
|
5537
|
+
const login = this.webLoginCodes.issue({ epoch: tokenEpoch(token), recipient: { adapterId, userId: data.userId, ...(data.username ? { username: data.username } : {}) } });
|
|
5181
5538
|
const current = () => !this.shuttingDown && this.adapters.get(adapterId) === adapter
|
|
5182
5539
|
&& this.webToken === token && this.hasFleetAdmins(adapterId) && this.isFleetAdmin(data.userId, adapterId);
|
|
5183
5540
|
const privateText = `${text}\n${t("dashboard.code", login.display, Math.round(LOGIN_CODE_TTL_MS / 60_000))}`;
|
|
@@ -5289,13 +5646,12 @@ export class FleetManager {
|
|
|
5289
5646
|
const adapter = owner.binding;
|
|
5290
5647
|
const status = this.getPublicWebStatus();
|
|
5291
5648
|
const publicAllowed = publicLinkSettings(this.fleetConfig?.web).allowed;
|
|
5292
|
-
|
|
5293
|
-
const menuText = this.topicCommands.
|
|
5294
|
-
+ (publicAllowed ? "\n
|
|
5295
|
-
+ "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
const choices = [{ action: "local", label: t("dashboard.local") },
|
|
5649
|
+
// Show the action before any link exists. TG's Start instructions belong to confirmed DM refusal below.
|
|
5650
|
+
const menuText = this.topicCommands.getDashboardMenuText()
|
|
5651
|
+
+ (publicAllowed ? "\n" + t("dashboard.public_risk") : "")
|
|
5652
|
+
+ (status.state !== "closed" ? "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
5653
|
+
+ (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : "") : "");
|
|
5654
|
+
const choices = [{ action: "local", label: t("dashboard.local_signin") },
|
|
5299
5655
|
...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
|
|
5300
5656
|
...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
|
|
5301
5657
|
const posted = await this.postNonceButtonPrompt({
|
|
@@ -5371,9 +5727,11 @@ export class FleetManager {
|
|
|
5371
5727
|
return false;
|
|
5372
5728
|
this.initializeWebSessions();
|
|
5373
5729
|
const issued = this.webLoginCodes.issue({ epoch: tokenEpoch(token), audience: exposureId, owner });
|
|
5374
|
-
//
|
|
5375
|
-
const
|
|
5376
|
-
|
|
5730
|
+
// Only a public link has a link expiry; the code's five-minute lifetime applies to either packet.
|
|
5731
|
+
const minutes = Math.round(LOGIN_CODE_TTL_MS / 60_000);
|
|
5732
|
+
const text = expiresAt !== undefined
|
|
5733
|
+
? t("dashboard.private_public", url, issued.display, minutes, formatFleetTime(expiresAt))
|
|
5734
|
+
: t("dashboard.private_local", url, issued.display, minutes);
|
|
5377
5735
|
const deadline = performance.now() + 10_000;
|
|
5378
5736
|
const direct = owner.binding;
|
|
5379
5737
|
// The route the sign-in message took: the code-only follow-up takes the same one (#1586).
|
|
@@ -5562,7 +5920,6 @@ export class FleetManager {
|
|
|
5562
5920
|
messageId,
|
|
5563
5921
|
});
|
|
5564
5922
|
this.lastUpdateProgressText = null;
|
|
5565
|
-
this.updateCompletionTipText = null;
|
|
5566
5923
|
this.startUpdateProgressMonitor(adapter);
|
|
5567
5924
|
}
|
|
5568
5925
|
/** Persist the public response, wait for idle, then start the canonical service restart. */
|
|
@@ -5593,7 +5950,6 @@ export class FleetManager {
|
|
|
5593
5950
|
return false;
|
|
5594
5951
|
}
|
|
5595
5952
|
this.lastUpdateProgressText = null;
|
|
5596
|
-
this.updateCompletionTipText = null;
|
|
5597
5953
|
this.startUpdateProgressMonitor(adapter);
|
|
5598
5954
|
await this.waitForFullRestartIdleGrace();
|
|
5599
5955
|
// An update can begin while the idle wait yields. Never launch a second
|
|
@@ -5833,17 +6189,8 @@ export class FleetManager {
|
|
|
5833
6189
|
const adapter = this.adapters.get(target.adapterId) ?? (initialAdapter?.id === target.adapterId ? initialAdapter : undefined);
|
|
5834
6190
|
if (!adapter)
|
|
5835
6191
|
return;
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
// reach RestartProgress.finish(). Append the same optional tip here.
|
|
5839
|
-
if (marker.progress.stage === "complete" && this.tipsEnabled()) {
|
|
5840
|
-
if (this.updateCompletionTipText === null) {
|
|
5841
|
-
const tip = this.pickAvailableTip();
|
|
5842
|
-
this.updateCompletionTipText = tip ? this.formatTip(tip) : "";
|
|
5843
|
-
}
|
|
5844
|
-
if (this.updateCompletionTipText)
|
|
5845
|
-
text += `\n\n${this.updateCompletionTipText}`;
|
|
5846
|
-
}
|
|
6192
|
+
// #1854: no tip on an update's completion (either path); tips come on their own schedule and from /tips.
|
|
6193
|
+
const text = formatUpdateProgress(marker);
|
|
5847
6194
|
if (text === this.lastUpdateProgressText)
|
|
5848
6195
|
return;
|
|
5849
6196
|
this.updateProgressEditRunning = true;
|
|
@@ -5894,6 +6241,7 @@ export class FleetManager {
|
|
|
5894
6241
|
this.rotateFleetLogs();
|
|
5895
6242
|
const fleet = this.loadConfig(configPath);
|
|
5896
6243
|
this.slimFleetConfigAtStartup();
|
|
6244
|
+
this.logConfigWarningsAtStartup();
|
|
5897
6245
|
setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
|
|
5898
6246
|
// #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
|
|
5899
6247
|
const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
|
|
@@ -6125,7 +6473,7 @@ export class FleetManager {
|
|
|
6125
6473
|
...DEFAULT_SCHEDULER_CONFIG,
|
|
6126
6474
|
...this.fleetConfig?.defaults.scheduler,
|
|
6127
6475
|
};
|
|
6128
|
-
this.scheduler = new Scheduler(join(this.dataDir, "scheduler.db"), (schedule, runId, retry) => this.handleScheduleTrigger(schedule, runId, retry), schedulerConfig, (name) => this.fleetConfig?.instances?.[name] != null || !!this.classicChannels?.getAll().some(ch => ch.instanceName === name), (schedule, retry, reason) => this.scheduleRetryDropped(schedule, retry, reason));
|
|
6476
|
+
this.scheduler = new Scheduler(join(this.dataDir, "scheduler.db"), (schedule, runId, retry) => this.handleScheduleTrigger(schedule, runId, retry), schedulerConfig, (name) => this.fleetConfig?.instances?.[name] != null || !!this.classicChannels?.getAll().some(ch => ch.instanceName === name), (schedule, retry, reason) => this.scheduleRetryDropped(schedule, retry, reason), this.logger);
|
|
6129
6477
|
this.scheduler.init();
|
|
6130
6478
|
this.logger.info("Scheduler initialized");
|
|
6131
6479
|
// Tips share the daily-report clock but remain an independent internal
|
|
@@ -6229,6 +6577,7 @@ export class FleetManager {
|
|
|
6229
6577
|
// into the same broken state, and a user who has legitimately stopped every
|
|
6230
6578
|
// instance would get a restart loop. Those conditions surface through /health
|
|
6231
6579
|
// (which now returns 503) and through the General-topic notifications instead.
|
|
6580
|
+
setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
|
|
6232
6581
|
this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
|
|
6233
6582
|
// #1231: a stalled loop is what turns a slash command into "did not respond";
|
|
6234
6583
|
// put each long stall in the log instead of leaving it to be guessed at.
|
|
@@ -6347,12 +6696,6 @@ export class FleetManager {
|
|
|
6347
6696
|
pausedNames,
|
|
6348
6697
|
failedNames,
|
|
6349
6698
|
backgroundWorkEndedNames,
|
|
6350
|
-
tipText: pendingProgressOperation === "update" && this.tipsEnabled()
|
|
6351
|
-
? (() => {
|
|
6352
|
-
const tip = this.pickAvailableTip();
|
|
6353
|
-
return tip ? this.formatTip(tip) : undefined;
|
|
6354
|
-
})()
|
|
6355
|
-
: undefined,
|
|
6356
6699
|
...(cleanupOutcome && "render" in cleanupOutcome
|
|
6357
6700
|
? { warningText: cleanupOutcome.render.warning, warningCommand: cleanupOutcome.render.cleanupCommand }
|
|
6358
6701
|
: {}),
|
|
@@ -6403,6 +6746,7 @@ export class FleetManager {
|
|
|
6403
6746
|
// Health HTTP endpoint
|
|
6404
6747
|
this.startHealthServer(fleet.health_port ?? 19280);
|
|
6405
6748
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
6749
|
+
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
6406
6750
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
6407
6751
|
// Daily update check — first check after 1 hour, then every 24 hours
|
|
6408
6752
|
this.updateCheckTimer = setTimeout(() => {
|
|
@@ -7144,6 +7488,16 @@ export class FleetManager {
|
|
|
7144
7488
|
});
|
|
7145
7489
|
}
|
|
7146
7490
|
}
|
|
7491
|
+
else if (msg.type === "raw_paste_written") {
|
|
7492
|
+
// Board 003648ac: a web /compact's paste reached the pane (or never will): its watch starts, or ends as lost.
|
|
7493
|
+
if (typeof msg.id === "string")
|
|
7494
|
+
this.compactWatchService?.pasted(name, msg.id, msg.outcome === "written", { owner: msg.owner, checkpoint: msg.checkpoint, pane: msg.pane });
|
|
7495
|
+
}
|
|
7496
|
+
else if (msg.type === "compact_pane_result") {
|
|
7497
|
+
// Board 907c7899: kiro's pane said how that /compact ended.
|
|
7498
|
+
if (typeof msg.id === "string")
|
|
7499
|
+
this.compactWatchService?.paneResult(name, msg.id, msg.owner, msg.outcome);
|
|
7500
|
+
}
|
|
7147
7501
|
else if (msg.type === "runtime_switch_result") {
|
|
7148
7502
|
// #1690: what our own runtime /model came to (the daemon answered claude's "Switch model?" or saw the result).
|
|
7149
7503
|
const waiter = typeof msg.id === "string" ? this.runtimeSwitchWaiters.get(msg.id) : undefined;
|
|
@@ -7351,6 +7705,48 @@ export class FleetManager {
|
|
|
7351
7705
|
this.adapterRestarting.delete(id);
|
|
7352
7706
|
}
|
|
7353
7707
|
}
|
|
7708
|
+
/**
|
|
7709
|
+
* #1990: how a reader in world `worldId`'s group reaches a place token's target — undefined (plain text) for another
|
|
7710
|
+
* world's instance, an instance without a fleet topic (a ClassicBot room may be anywhere), or an id no link is built
|
|
7711
|
+
* from. Discord: the channel mention; Telegram: the topic URL (General's topic included).
|
|
7712
|
+
*/
|
|
7713
|
+
placeLink(worldId, target) {
|
|
7714
|
+
const world = this.worlds.get(worldId ?? "");
|
|
7715
|
+
if (!world)
|
|
7716
|
+
return undefined;
|
|
7717
|
+
let topic;
|
|
7718
|
+
if (target.kind === "general") {
|
|
7719
|
+
const notice = this.fleetNoticeTarget(world.id);
|
|
7720
|
+
if (!notice || notice.chatId !== world.groupId || notice.opts.threadId === undefined)
|
|
7721
|
+
return undefined;
|
|
7722
|
+
topic = String(notice.opts.threadId);
|
|
7723
|
+
}
|
|
7724
|
+
else {
|
|
7725
|
+
if (this.getInstanceAdapterId(target.name) !== world.id)
|
|
7726
|
+
return undefined;
|
|
7727
|
+
const topicId = this.fleetConfig?.instances[target.name]?.topic_id;
|
|
7728
|
+
if (topicId == null)
|
|
7729
|
+
return undefined;
|
|
7730
|
+
topic = String(topicId);
|
|
7731
|
+
}
|
|
7732
|
+
if (world.type === "discord")
|
|
7733
|
+
return SNOWFLAKE.test(topic) && SNOWFLAKE.test(world.groupId) ? { mention: `<#${topic}>` } : undefined;
|
|
7734
|
+
const url = topicUrl(worldPlaceOf(world), topic, { general: target.kind === "general" });
|
|
7735
|
+
return url ? { url } : undefined;
|
|
7736
|
+
}
|
|
7737
|
+
/** #1990: a message for world `worldId`'s group with its place tokens rendered (links as text or Telegram entities). */
|
|
7738
|
+
renderPlaces(worldId, text, opts) {
|
|
7739
|
+
if (!hasPlaceRefs(text))
|
|
7740
|
+
return { text, opts };
|
|
7741
|
+
const type = this.worlds.get(worldId ?? "")?.type;
|
|
7742
|
+
const surface = type === "discord" ? "discord" : type === "telegram" ? "telegram" : "plain";
|
|
7743
|
+
const out = renderPlaceRefs(text, surface, target => this.placeLink(worldId, target));
|
|
7744
|
+
return { text: out.text, opts: out.textLinks.length ? { ...opts, textLinks: out.textLinks } : opts };
|
|
7745
|
+
}
|
|
7746
|
+
/** #1990: renderPlaces for the world that owns `instanceName`'s topic. */
|
|
7747
|
+
renderPlacesFor(instanceName, text, opts) {
|
|
7748
|
+
return this.renderPlaces(this.getInstanceAdapterId(instanceName), text, opts);
|
|
7749
|
+
}
|
|
7354
7750
|
/** Handle inbound message — transcribe voice if present, then route */
|
|
7355
7751
|
findGeneralInstance(adapterId) {
|
|
7356
7752
|
if (!this.fleetConfig)
|
|
@@ -7938,6 +8334,21 @@ export class FleetManager {
|
|
|
7938
8334
|
}
|
|
7939
8335
|
return;
|
|
7940
8336
|
}
|
|
8337
|
+
// #1806: /backend — channel admin, as /model. A ClassicBot agent is not switched by it (it says so).
|
|
8338
|
+
if (text === "/backend" || text.startsWith("/backend ") || text.startsWith("/backend@")) {
|
|
8339
|
+
if (!this.isModelAdmin(msg.userId, chatId, msg.adapterId)) {
|
|
8340
|
+
await msgAdapter?.sendText(chatId, t("permission.denied"));
|
|
8341
|
+
return;
|
|
8342
|
+
}
|
|
8343
|
+
const backendInstance = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
8344
|
+
if (!backendInstance) {
|
|
8345
|
+
await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
|
|
8346
|
+
return;
|
|
8347
|
+
}
|
|
8348
|
+
if (msgAdapter)
|
|
8349
|
+
await this.runBackendCommand(backendInstance, text, msg.userId, chatId, msgAdapter, chatId, undefined, msg.adapterId);
|
|
8350
|
+
return;
|
|
8351
|
+
}
|
|
7941
8352
|
// Handle /compact command (admin only)
|
|
7942
8353
|
const classicCompact = parseCompactCommand(text);
|
|
7943
8354
|
if (classicCompact) {
|
|
@@ -8337,7 +8748,7 @@ export class FleetManager {
|
|
|
8337
8748
|
if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
|
|
8338
8749
|
continue;
|
|
8339
8750
|
try {
|
|
8340
|
-
await world.adapter.sendText(target.chatId, t("upgrade.web_chat"
|
|
8751
|
+
await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
|
|
8341
8752
|
this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
|
|
8342
8753
|
}
|
|
8343
8754
|
catch (err) {
|
|
@@ -8500,6 +8911,17 @@ export class FleetManager {
|
|
|
8500
8911
|
respond(null, `reply: ${parsedButtons.error}`);
|
|
8501
8912
|
return;
|
|
8502
8913
|
}
|
|
8914
|
+
// #1823: a poll is checked with its buttons. Where it cannot be shown it is refused, never sent as text: text
|
|
8915
|
+
// options would never come back as results.
|
|
8916
|
+
const parsedPoll = parsePoll(args.poll, parsedButtons && "buttons" in parsedButtons ? parsedButtons.buttons : null, args.text);
|
|
8917
|
+
if (parsedPoll && "error" in parsedPoll) {
|
|
8918
|
+
respond(null, `reply: ${parsedPoll.error}`);
|
|
8919
|
+
return;
|
|
8920
|
+
}
|
|
8921
|
+
if (parsedPoll && (!outAdapter.supportsPolls || !this.replyButtons())) {
|
|
8922
|
+
respond(null, `reply: a poll cannot be shown here${outAdapter.supportsPolls ? " (reply buttons are unavailable until AgEnD restarts)" : " — polls work in Discord and Telegram chats and the web chat"}; send the question without poll`);
|
|
8923
|
+
return;
|
|
8924
|
+
}
|
|
8503
8925
|
// Stickers (#1226) are checked before anything is sent: a refused one is the reply's error, not a gap.
|
|
8504
8926
|
const stickerProblem = await this.replyStickerProblem(outAdapter, args, threadId, contextAdapterId ?? this.getInstanceAdapterId(senderInstanceName ?? instanceName));
|
|
8505
8927
|
if (stickerProblem) {
|
|
@@ -8519,7 +8941,7 @@ export class FleetManager {
|
|
|
8519
8941
|
const buttons = outAdapter.supportsReplyButtons ? this.replyButtons() : null;
|
|
8520
8942
|
if (buttons) {
|
|
8521
8943
|
const outId = outAdapter === WEB_ONLY_REPLY_SINK ? "web" : outAdapter.id ?? contextAdapterId ?? "";
|
|
8522
|
-
replySet = buttons.prepare({ instance: instanceName, adapterId: outId, chatId: String(args.chat_id ?? ""), threadId }, parsedButtons.buttons);
|
|
8944
|
+
replySet = buttons.prepare({ instance: instanceName, adapterId: outId, chatId: String(args.chat_id ?? ""), threadId }, parsedButtons.buttons, parsedPoll && "poll" in parsedPoll ? parsedPoll.poll : null);
|
|
8523
8945
|
}
|
|
8524
8946
|
else {
|
|
8525
8947
|
args.text = `${String(args.text)}\n\n${replyButtonsFallbackText(parsedButtons.buttons)}`;
|
|
@@ -8539,6 +8961,9 @@ export class FleetManager {
|
|
|
8539
8961
|
this.replyButtonsCtl?.discard(replySet.id);
|
|
8540
8962
|
}
|
|
8541
8963
|
}
|
|
8964
|
+
// #1823: the agent learns its poll's id (the results name it).
|
|
8965
|
+
if (replySet?.poll && !error && result && typeof result === "object")
|
|
8966
|
+
result = { ...result, poll_id: replySet.id };
|
|
8542
8967
|
ticket.complete(result, error);
|
|
8543
8968
|
// Return the platform outcome first. Bookkeeping below must never turn
|
|
8544
8969
|
// a confirmed Discord/Telegram POST into a tool error if a secondary
|
|
@@ -8566,7 +8991,7 @@ export class FleetManager {
|
|
|
8566
8991
|
});
|
|
8567
8992
|
}
|
|
8568
8993
|
};
|
|
8569
|
-
if (routeToolCall(outAdapter, tool, args, threadId, respondAndRecord, replySet ? { replyButtons: replySet.callbacks } : {})) {
|
|
8994
|
+
if (routeToolCall(outAdapter, tool, args, threadId, respondAndRecord, replySet ? { replyButtons: replySet.callbacks, ...(replySet.poll ? { replyPoll: replySet.poll } : {}) } : {})) {
|
|
8570
8995
|
return;
|
|
8571
8996
|
}
|
|
8572
8997
|
if (replySet)
|
|
@@ -8994,6 +9419,19 @@ export class FleetManager {
|
|
|
8994
9419
|
}
|
|
8995
9420
|
dispatchTypedIpc(name, msg) {
|
|
8996
9421
|
const type = String(msg.type);
|
|
9422
|
+
if (type.startsWith("fleet_") && WEB_EFFECT_TOOLS.has(type.slice(6))) {
|
|
9423
|
+
const ipc = this.instanceIpcClients.get(name), fleetRequestId = msg.fleetRequestId;
|
|
9424
|
+
if (!ipc || typeof fleetRequestId !== "string")
|
|
9425
|
+
return;
|
|
9426
|
+
void this.handleWebThemeHttp(name, type.slice(6), (msg.payload ?? {})).then(result => {
|
|
9427
|
+
if (this.instanceIpcClients.get(name) === ipc)
|
|
9428
|
+
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
9429
|
+
}, err => {
|
|
9430
|
+
if (this.instanceIpcClients.get(name) === ipc)
|
|
9431
|
+
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err instanceof Error ? err.message : "Effect operation failed" });
|
|
9432
|
+
}).catch(err => this.logger.debug({ err }, "Effect tool response could not be written"));
|
|
9433
|
+
return;
|
|
9434
|
+
}
|
|
8997
9435
|
if (type.startsWith("fleet_schedule_")) {
|
|
8998
9436
|
this.handleScheduleCrud(name, msg);
|
|
8999
9437
|
return;
|
|
@@ -9019,6 +9457,36 @@ export class FleetManager {
|
|
|
9019
9457
|
this.handlePersonaEmoji(name, msg);
|
|
9020
9458
|
return;
|
|
9021
9459
|
}
|
|
9460
|
+
if (type === "fleet_get_web_theme" || type === "fleet_set_web_theme" || type === "fleet_set_web_theme_asset" || type === "fleet_reset_web_theme"
|
|
9461
|
+
|| type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
|
|
9462
|
+
|| type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
|
|
9463
|
+
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod") {
|
|
9464
|
+
this.handleWebTheme(name, msg);
|
|
9465
|
+
return;
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
/**
|
|
9469
|
+
* #1629: the caller's own web theme over IPC (the agent endpoint calls the same service). `name` is the instance this
|
|
9470
|
+
* IPC connection belongs to — never anything the message says. A refusal is a result the agent reads (its `error`
|
|
9471
|
+
* and what to do), not a transport error.
|
|
9472
|
+
*/
|
|
9473
|
+
handleWebTheme(instanceName, msg) {
|
|
9474
|
+
const fleetRequestId = msg.fleetRequestId;
|
|
9475
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
9476
|
+
if (!ipc)
|
|
9477
|
+
return;
|
|
9478
|
+
const tool = String(msg.type).replace(/^fleet_/, "");
|
|
9479
|
+
try {
|
|
9480
|
+
const args = (msg.payload ?? {});
|
|
9481
|
+
const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
|
|
9482
|
+
? this.appearanceTools.handle(instanceName, tool, args)
|
|
9483
|
+
: tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
|
|
9484
|
+
: this.webAppearance.handleTool(instanceName, tool, args);
|
|
9485
|
+
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
9486
|
+
}
|
|
9487
|
+
catch (err) {
|
|
9488
|
+
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err.message });
|
|
9489
|
+
}
|
|
9022
9490
|
}
|
|
9023
9491
|
/** `list_emojis` / `set_persona_emoji` / `preview_emojis` / `list_stickers` / `preview_stickers` over IPC; the agent endpoint calls the same methods. */
|
|
9024
9492
|
handlePersonaEmoji(instanceName, msg) {
|
|
@@ -9423,6 +9891,19 @@ export class FleetManager {
|
|
|
9423
9891
|
async handleSetPersonaEmojiHttp(instance, args) {
|
|
9424
9892
|
return this.setPersonaEmoji(instance, args);
|
|
9425
9893
|
}
|
|
9894
|
+
/** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
|
|
9895
|
+
async handleWebThemeHttp(instance, tool, args) {
|
|
9896
|
+
if (WEB_EFFECT_TOOLS.has(tool)) {
|
|
9897
|
+
if (!this.isKnownInstance(instance))
|
|
9898
|
+
return { error: "unknown_instance" };
|
|
9899
|
+
return this.effectTools.handle(instance, tool, args);
|
|
9900
|
+
}
|
|
9901
|
+
if (tool === "get_web_appearance" || tool === "preview_web_appearance")
|
|
9902
|
+
return this.appearanceTools.handle(instance, tool, args);
|
|
9903
|
+
if (tool === "export_web_mod" || tool === "propose_web_mod")
|
|
9904
|
+
return this.agentMods.handle(instance, tool, args);
|
|
9905
|
+
return this.webAppearance.handleTool(instance, tool, args);
|
|
9906
|
+
}
|
|
9426
9907
|
async handlePreviewEmojisHttp(instance, args) {
|
|
9427
9908
|
return this.previewEmojis(instance, args);
|
|
9428
9909
|
}
|
|
@@ -9929,6 +10410,21 @@ export class FleetManager {
|
|
|
9929
10410
|
? `Refusing to save fleet.yaml: it would be invalid (${introduced.map(e => `${e.path}: ${e.message}`).join("; ")})`
|
|
9930
10411
|
: null;
|
|
9931
10412
|
}
|
|
10413
|
+
/**
|
|
10414
|
+
* #1874 follow-up: the validator's warnings, once per start, in daemon.log — a setting fleet.yaml has that AgEnD
|
|
10415
|
+
* ignores (e.g. `web.access`) is otherwise silent unless someone runs `agend validate`.
|
|
10416
|
+
*/
|
|
10417
|
+
logConfigWarningsAtStartup() {
|
|
10418
|
+
let warnings = [];
|
|
10419
|
+
try {
|
|
10420
|
+
warnings = validateFleetConfig(this.rawFleetConfig).warnings;
|
|
10421
|
+
}
|
|
10422
|
+
catch (err) {
|
|
10423
|
+
this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
|
|
10424
|
+
}
|
|
10425
|
+
for (const w of warnings)
|
|
10426
|
+
this.logger.warn({ path: w.path }, `fleet.yaml: ${w.path ? `${w.path}: ` : ""}${w.message}`);
|
|
10427
|
+
}
|
|
9932
10428
|
/** One-time upgrade migration; invalid YAML is never rewritten. */
|
|
9933
10429
|
slimFleetConfigAtStartup() {
|
|
9934
10430
|
const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
|
|
@@ -10102,6 +10598,7 @@ export class FleetManager {
|
|
|
10102
10598
|
// must not be shown the old one's conversation, and deleted names must not pile up.
|
|
10103
10599
|
this.webChatHistory.forget(name);
|
|
10104
10600
|
this.webFiles.forget(name);
|
|
10601
|
+
this.compactWatchService?.forget(name);
|
|
10105
10602
|
// #1664: and it is nobody's favorite any more (every device sees that at its next status frame).
|
|
10106
10603
|
try {
|
|
10107
10604
|
this.webPrefs.forgetInstance(name);
|
|
@@ -10109,6 +10606,17 @@ export class FleetManager {
|
|
|
10109
10606
|
catch (err) {
|
|
10110
10607
|
this.logger.warn({ err: err.message, name }, "Dropping a removed instance from web favorites failed");
|
|
10111
10608
|
}
|
|
10609
|
+
try {
|
|
10610
|
+
this.themeStore.forget(name);
|
|
10611
|
+
this.webAppearance.forget(name);
|
|
10612
|
+
this.appearanceTools.forget(name);
|
|
10613
|
+
this.effectToolsService?.forget(name);
|
|
10614
|
+
this.mods.forgetInstance(name);
|
|
10615
|
+
this.agentMods.forget(name);
|
|
10616
|
+
}
|
|
10617
|
+
catch (err) {
|
|
10618
|
+
this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
|
|
10619
|
+
}
|
|
10112
10620
|
// Clean up statusline watcher + instance directory
|
|
10113
10621
|
this.statuslineWatcher.unwatch(name);
|
|
10114
10622
|
// instanceDirToClean and effectiveBackend are captured before lifecycle.remove
|
|
@@ -10309,10 +10817,12 @@ export class FleetManager {
|
|
|
10309
10817
|
return this.webImageCaches[kind];
|
|
10310
10818
|
}
|
|
10311
10819
|
/**
|
|
10312
|
-
*
|
|
10313
|
-
* whatever an instance still names — #1635 review)
|
|
10820
|
+
* The bots instances and ClassicBot rooms speak through, by connections configured now (a connection removed from
|
|
10821
|
+
* fleet.yaml is nobody's, whatever an instance still names — #1635 review). `own`: instance → its own bot (answering
|
|
10822
|
+
* for it alone; the primary too, when it serves one instance only). `speaking` (#1867): instance → the bot it speaks through — its own, a shared one,
|
|
10823
|
+
* or, with none configured for it, the primary.
|
|
10314
10824
|
*/
|
|
10315
|
-
|
|
10825
|
+
webBots() {
|
|
10316
10826
|
const instances = this.fleetConfig?.instances ?? {};
|
|
10317
10827
|
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
10318
10828
|
const configured = new Set(channels.map(ch => ch.id ?? ch.type).filter((id) => typeof id === "string"));
|
|
@@ -10322,8 +10832,17 @@ export class FleetManager {
|
|
|
10322
10832
|
if (!Object.hasOwn(instances, room.instanceName))
|
|
10323
10833
|
assignments.push([room.instanceName, assigned(room.instanceName)]);
|
|
10324
10834
|
}
|
|
10325
|
-
|
|
10835
|
+
const primary = this.getPrimaryAdapterId(), fallback = primary && configured.has(primary) ? primary : undefined;
|
|
10836
|
+
const speaking = new Map();
|
|
10837
|
+
for (const [instance, connection] of assignments) {
|
|
10838
|
+
const c = connection ?? fallback;
|
|
10839
|
+
if (c)
|
|
10840
|
+
speaking.set(instance, c);
|
|
10841
|
+
}
|
|
10842
|
+
return { own: ownBotConnections(assignments), speaking };
|
|
10326
10843
|
}
|
|
10844
|
+
/** instance → the connection that is its own bot (see webBots). */
|
|
10845
|
+
ownBots() { return this.webBots().own; }
|
|
10327
10846
|
/**
|
|
10328
10847
|
* The bot a connection is now: its running adapter and that bot's own user id. Everything cached for an own bot is
|
|
10329
10848
|
* keyed by both, so a bot put in its place (a token replaced under the same connection id) never shows the old one's
|
|
@@ -10366,8 +10885,11 @@ export class FleetManager {
|
|
|
10366
10885
|
},
|
|
10367
10886
|
};
|
|
10368
10887
|
}
|
|
10369
|
-
/**
|
|
10370
|
-
|
|
10888
|
+
/**
|
|
10889
|
+
* One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. The bot
|
|
10890
|
+
* is its own, else the one it speaks through (#1867) — one cache entry per bot, so a shared bot is fetched once.
|
|
10891
|
+
*/
|
|
10892
|
+
webIdentityImage(name, displayName, profileAvatar, bots) {
|
|
10371
10893
|
let profileVersion = null;
|
|
10372
10894
|
if (profileAvatar && imageMimeForFile(profileAvatar)) {
|
|
10373
10895
|
try {
|
|
@@ -10375,22 +10897,41 @@ export class FleetManager {
|
|
|
10375
10897
|
}
|
|
10376
10898
|
catch { /* removed */ }
|
|
10377
10899
|
}
|
|
10378
|
-
const
|
|
10900
|
+
const ownConnection = bots.own.get(name), connection = ownConnection ?? bots.speaking.get(name);
|
|
10901
|
+
const owner = connection ? this.botOwner(connection) : null;
|
|
10379
10902
|
const source = connection && owner?.adapter.fetchBotAvatar ? this.botAvatarSource(connection, owner) : null;
|
|
10380
10903
|
const bot = source ? this.webImageCache("avatars").peek(source.key, source.load) : null;
|
|
10381
10904
|
const cfg = this.ownInstanceConfig(name);
|
|
10382
10905
|
const avatar = avatarFor({ name, displayName, profileVersion, bot: bot && connection ? { connection, version: bot.version } : null,
|
|
10383
10906
|
personaEmoji: cfg?.status_emojis?.delivered });
|
|
10384
|
-
// The ring belongs to
|
|
10385
|
-
|
|
10386
|
-
|
|
10907
|
+
// The ring belongs to an own bot's face: only when that is what is shown (a shared bot's would ring everyone alike).
|
|
10908
|
+
// #1958: the own bot's colour is also the identity colour's role tier — whatever face is shown (never a shared bot's).
|
|
10909
|
+
const roleColor = ownConnection && owner
|
|
10910
|
+
? this.webAccent(ownConnection, owner, cfg ? (cfg.topic_id == null ? undefined : String(cfg.topic_id)) : this.classicChannels?.getChannelIdByInstance(name))
|
|
10387
10911
|
: null;
|
|
10388
|
-
|
|
10912
|
+
const accent = !profileVersion && bot && ownConnection && owner ? roleColor : null;
|
|
10913
|
+
// #1629 (first design §5, r2 (d)): the agent's theme may name its own picture and ring colour. Both come below a
|
|
10914
|
+
// person's upload (never when there is one) and ride beside the theme-free face: a viewer who turned agents'
|
|
10915
|
+
// themes off (fx.show), or a safe-mode load, shows `avatar` and `accent` as before.
|
|
10916
|
+
let theme = { v: 1 };
|
|
10917
|
+
try {
|
|
10918
|
+
theme = this.themeStore.get(name).theme;
|
|
10919
|
+
}
|
|
10920
|
+
catch { /* unreadable themes: none */ }
|
|
10921
|
+
const themeAvatar = !profileVersion && theme.avatar ? { kind: "image", src: `/ui/img/theme/${encodeURIComponent(name)}/${theme.avatar}` } : undefined;
|
|
10922
|
+
const themeAccent = !profileVersion && theme.accent ? theme.accent : undefined;
|
|
10923
|
+
return { avatar, ...(accent ? { accent } : {}), ...(themeAvatar ? { themeAvatar } : {}), ...(themeAccent ? { themeAccent } : {}),
|
|
10924
|
+
// #1958: the agent's own identity colour (theme.identity.color, else its accent) and where it would show it.
|
|
10925
|
+
...(roleColor ? { roleColor } : {}), ...(theme.identity?.color ?? theme.accent ? { agentColor: theme.identity?.color ?? theme.accent } : {}),
|
|
10926
|
+
...(theme.identity?.show ? { agentShow: theme.identity.show } : {}) };
|
|
10389
10927
|
}
|
|
10390
|
-
/**
|
|
10928
|
+
/**
|
|
10929
|
+
* GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only a connection some instance or room speaks
|
|
10930
|
+
* through (#1867), or a known instance's upload.
|
|
10931
|
+
*/
|
|
10391
10932
|
webImage(kind, key) {
|
|
10392
10933
|
if (kind === "bot") {
|
|
10393
|
-
if (![...this.
|
|
10934
|
+
if (![...this.webBots().speaking.values()].includes(key))
|
|
10394
10935
|
return null;
|
|
10395
10936
|
const owner = this.botOwner(key);
|
|
10396
10937
|
return owner ? this.webImageCache("avatars").read(this.botAvatarSource(key, owner).key) : null;
|
|
@@ -11469,7 +12010,8 @@ export class FleetManager {
|
|
|
11469
12010
|
// Discord, so every such fallback failed inside the catch handler.
|
|
11470
12011
|
const target = this.fleetNoticeTarget();
|
|
11471
12012
|
if (this.adapter && target) {
|
|
11472
|
-
this.
|
|
12013
|
+
const out = this.renderPlaces(this.getPrimaryAdapterId(), body, target.opts);
|
|
12014
|
+
this.adapter.sendText(target.chatId, out.text, out.opts)
|
|
11473
12015
|
.catch(err => this.logger.warn({ err }, "Failed to send fleet error notification"));
|
|
11474
12016
|
dispatched = true;
|
|
11475
12017
|
}
|
|
@@ -11481,7 +12023,7 @@ export class FleetManager {
|
|
|
11481
12023
|
// also suppressed the next ten minutes of identical ones. Leaving the key
|
|
11482
12024
|
// unspent lets the next caller, once an adapter is up, actually get
|
|
11483
12025
|
// through. The counter is not reset either: nothing was shown.
|
|
11484
|
-
this.logger.warn({ text: body }, "Fleet error could not be delivered (no adapter or no target yet) — not consuming the throttle window");
|
|
12026
|
+
this.logger.warn({ text: stripPlaceRefs(body) }, "Fleet error could not be delivered (no adapter or no target yet) — not consuming the throttle window");
|
|
11485
12027
|
return false;
|
|
11486
12028
|
}
|
|
11487
12029
|
if (!throttled)
|
|
@@ -11519,24 +12061,28 @@ export class FleetManager {
|
|
|
11519
12061
|
const groupId = channelCfg?.group_id;
|
|
11520
12062
|
// Fleet topic instance
|
|
11521
12063
|
const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
12064
|
+
const worldId = this.getInstanceAdapterId(instanceName);
|
|
11522
12065
|
if (threadId != null && groupId) {
|
|
11523
|
-
|
|
12066
|
+
const out = this.renderPlaces(worldId, text, { threadId: String(threadId), ...extraOpts });
|
|
12067
|
+
adapter.sendText(String(groupId), out.text, out.opts)
|
|
11524
12068
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
|
|
11525
12069
|
return true;
|
|
11526
12070
|
}
|
|
11527
12071
|
// Classic instance: find its channelId from the classic manager
|
|
11528
12072
|
const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
11529
12073
|
if (classicChatId) {
|
|
11530
|
-
|
|
12074
|
+
// #1990: a ClassicBot room's readers need not be in the fleet's group, so its notices name places as plain text.
|
|
12075
|
+
adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts)
|
|
11531
12076
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
|
|
11532
12077
|
return true;
|
|
11533
12078
|
}
|
|
11534
12079
|
// Fallback: the instance has neither a topic nor a classic channel, so post
|
|
11535
12080
|
// where fleet-wide notices go. Not the bare group id: on Discord that is a
|
|
11536
12081
|
// guild, and the send fails inside the catch handler.
|
|
11537
|
-
const target = this.fleetNoticeTarget(
|
|
12082
|
+
const target = this.fleetNoticeTarget(worldId);
|
|
11538
12083
|
if (target) {
|
|
11539
|
-
|
|
12084
|
+
const out = this.renderPlaces(worldId, text, { ...target.opts, ...extraOpts });
|
|
12085
|
+
adapter.sendText(target.chatId, out.text, out.opts)
|
|
11540
12086
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
|
|
11541
12087
|
return true;
|
|
11542
12088
|
}
|
|
@@ -11607,12 +12153,14 @@ export class FleetManager {
|
|
|
11607
12153
|
this.pendingNonceButtons.set(nonce, entry);
|
|
11608
12154
|
try {
|
|
11609
12155
|
const choices = opts.choices.map(c => ({ id: `${opts.prefix}${nonce}:${c.action}`, label: c.label }));
|
|
12156
|
+
// #1990: place names link for the group the prompt is posted in; the dashboard's copy is plain (for now).
|
|
12157
|
+
const posted = this.renderPlaces(opts.adapterId, opts.message, opts.threadId ? { threadId: opts.threadId } : undefined);
|
|
11610
12158
|
const sent = opts.deliver ? await opts.deliver(choices) : await opts.adapter.notifyAlert(opts.chatId, {
|
|
11611
12159
|
type: opts.alertType,
|
|
11612
12160
|
instanceName: opts.instanceName,
|
|
11613
|
-
message:
|
|
12161
|
+
message: posted.text,
|
|
11614
12162
|
choices,
|
|
11615
|
-
},
|
|
12163
|
+
}, posted.opts);
|
|
11616
12164
|
// Bind the nonce to the provider's canonical delivery address, not the
|
|
11617
12165
|
// logical routing input. Telegram, for example, represents General as
|
|
11618
12166
|
// AgEnD topic "1" on input but omits message_thread_id on the wire and in
|
|
@@ -11630,8 +12178,15 @@ export class FleetManager {
|
|
|
11630
12178
|
// Offered on the dashboard only once it is live on the platform (a failed post is disarmed above),
|
|
11631
12179
|
// and only if nothing claimed or expired it meanwhile.
|
|
11632
12180
|
if (WEB_MIRRORED_PROMPT_PREFIXES.has(opts.prefix) && this.pendingNonceButtons.get(nonce) === entry) {
|
|
12181
|
+
// #1990: on the dashboard an instance's name opens its terminal View (where the prompt is answered by hand).
|
|
12182
|
+
const web = renderPlaceRefs(opts.message, "web", target => {
|
|
12183
|
+
const route = target.kind === "instance" && Object.hasOwn(this.fleetConfig?.instances ?? {}, target.name) ? webRoute("view", target.name) : undefined;
|
|
12184
|
+
return route ? { url: route } : undefined;
|
|
12185
|
+
});
|
|
12186
|
+
const links = webTextLinks(web.textLinks, web.text);
|
|
11633
12187
|
entry.web = {
|
|
11634
|
-
text:
|
|
12188
|
+
text: web.text,
|
|
12189
|
+
...(links ? { links } : {}),
|
|
11635
12190
|
actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
|
|
11636
12191
|
expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
|
|
11637
12192
|
};
|
|
@@ -11827,6 +12382,8 @@ export class FleetManager {
|
|
|
11827
12382
|
return true;
|
|
11828
12383
|
if (await this.handleEffortSelection(data, adapterId))
|
|
11829
12384
|
return true;
|
|
12385
|
+
if (await this.handleBackendSelection(data, adapterId))
|
|
12386
|
+
return true;
|
|
11830
12387
|
if (await this.handleHangPrompt(data, adapterId, adapter))
|
|
11831
12388
|
return true;
|
|
11832
12389
|
if (data.callbackData.startsWith("cancel:")) {
|
|
@@ -11881,7 +12438,7 @@ export class FleetManager {
|
|
|
11881
12438
|
isClassic: name => classic.has(name),
|
|
11882
12439
|
place: name => {
|
|
11883
12440
|
const world = this.worlds.get(this.getInstanceAdapterId(name) ?? "");
|
|
11884
|
-
return world ?
|
|
12441
|
+
return world ? worldPlaceOf(world) : undefined;
|
|
11885
12442
|
},
|
|
11886
12443
|
// fleet.yaml's topic only. A ClassicBot room has none here, so no link: its channel may sit in any allowed
|
|
11887
12444
|
// guild, not necessarily its world's group.
|
|
@@ -12046,9 +12603,7 @@ export class FleetManager {
|
|
|
12046
12603
|
finishedAt: Date.parse(d.finishedAt) || 0,
|
|
12047
12604
|
})),
|
|
12048
12605
|
ownerOf,
|
|
12049
|
-
worlds: () => [...this.worlds.values()].map((w) => ({
|
|
12050
|
-
id: w.id, adapter: w.adapter, place: { type: w.type, ...(w.groupId ? { groupId: w.groupId } : {}) },
|
|
12051
|
-
})),
|
|
12606
|
+
worlds: () => [...this.worlds.values()].map((w) => ({ id: w.id, adapter: w.adapter, place: worldPlaceOf(w) })),
|
|
12052
12607
|
noticeTarget: world => {
|
|
12053
12608
|
const target = this.fleetNoticeTarget(world);
|
|
12054
12609
|
return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
|
|
@@ -12094,10 +12649,18 @@ export class FleetManager {
|
|
|
12094
12649
|
* The caller has passed the /ui gate (session, same origin, CSRF). Here: the prompt must be one offered
|
|
12095
12650
|
* on the dashboard, about the instance the page named, and the action one of its own buttons.
|
|
12096
12651
|
*/
|
|
12097
|
-
/** #
|
|
12098
|
-
|
|
12652
|
+
/** #1823: close_poll — `instance` closes one of its own polls before the deadline. */
|
|
12653
|
+
closePollEarly(instance, id) {
|
|
12654
|
+
const buttons = this.replyButtons();
|
|
12655
|
+
return buttons ? buttons.closeEarly(instance, id) : Promise.resolve({ ok: false, error: "polls are unavailable until AgEnD restarts" });
|
|
12656
|
+
}
|
|
12657
|
+
/**
|
|
12658
|
+
* #1266: a click on a reply's button in the web chat (the /ui gate is passed: a session, or the public link). On a
|
|
12659
|
+
* poll (#1823) it is `voter`'s vote: the page's session.
|
|
12660
|
+
*/
|
|
12661
|
+
clickWebReplyButton(instance, id, index, voter) {
|
|
12099
12662
|
const buttons = this.replyButtons();
|
|
12100
|
-
return buttons ? buttons.clickWeb(instance, id, index) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
|
|
12663
|
+
return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
|
|
12101
12664
|
}
|
|
12102
12665
|
async clickWebPrompt(instance, nonce, action) {
|
|
12103
12666
|
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z-]{0,23}$/.test(action))
|
|
@@ -12450,6 +13013,8 @@ export class FleetManager {
|
|
|
12450
13013
|
}
|
|
12451
13014
|
};
|
|
12452
13015
|
}
|
|
13016
|
+
/** #1806: the web's /backend confirmations — one per question, used once, for the arguments and backend asked about. */
|
|
13017
|
+
webBackendTokens = new Map();
|
|
12453
13018
|
/** #1269: the web's /clear confirmations — one token per question, used once, for the instance as it was asked about. */
|
|
12454
13019
|
webClearTokens = new Map();
|
|
12455
13020
|
/** #1269: an instance's chat command from the web chat (web-commands.ts); `publicLink`: a gateway request. */
|
|
@@ -12457,7 +13022,9 @@ export class FleetManager {
|
|
|
12457
13022
|
const webMeta = (instance) => {
|
|
12458
13023
|
const topicId = this.fleetConfig?.instances[instance]?.topic_id;
|
|
12459
13024
|
return {
|
|
12460
|
-
|
|
13025
|
+
// #1827: a /steer or /btw reaches the agent as the web person who wrote it.
|
|
13026
|
+
chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(),
|
|
13027
|
+
username: opts.person?.name ?? WEB_NAME_FALLBACK, userId: opts.person?.userId ?? WEB_NAME_FALLBACK,
|
|
12461
13028
|
threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web", humanOrigin: webHumanOrigin(true),
|
|
12462
13029
|
};
|
|
12463
13030
|
};
|
|
@@ -12477,7 +13044,22 @@ export class FleetManager {
|
|
|
12477
13044
|
return this.classicChannels?.getChannelIdByInstance(instance) !== undefined ? "classic" : null;
|
|
12478
13045
|
},
|
|
12479
13046
|
ctx: (instance) => this.topicCommands.getCtxText(instance),
|
|
12480
|
-
|
|
13047
|
+
// Board 003648ac: a watch for the paste, armed when the daemon says it was written (raw_paste_written); one
|
|
13048
|
+
// for a command that was not handed over is dropped.
|
|
13049
|
+
compact: async (instance, instructions) => {
|
|
13050
|
+
let watch = null;
|
|
13051
|
+
try {
|
|
13052
|
+
const r = this.topicCommands.compactInstance(instance, instructions, () => (watch = this.compactWatch.begin(instance)));
|
|
13053
|
+
if (!r.sent && watch)
|
|
13054
|
+
this.compactWatch.cancel(instance, watch);
|
|
13055
|
+
return { ...r, watch: r.sent ? watch : null };
|
|
13056
|
+
}
|
|
13057
|
+
catch (err) {
|
|
13058
|
+
if (watch)
|
|
13059
|
+
this.compactWatch.cancel(instance, watch);
|
|
13060
|
+
throw err;
|
|
13061
|
+
}
|
|
13062
|
+
},
|
|
12481
13063
|
applyModel: (instance, name) => this.applyModel(instance, name),
|
|
12482
13064
|
modelChoices: choicesFromCache,
|
|
12483
13065
|
applyEffort: (instance, level) => this.applyEffort(instance, level),
|
|
@@ -12488,6 +13070,41 @@ export class FleetManager {
|
|
|
12488
13070
|
const current = this.resolveInstanceEffort(instance).effort;
|
|
12489
13071
|
return { current, options: levels.map(l => ({ id: l, label: this.effortChoiceLabel(l, current) })) };
|
|
12490
13072
|
},
|
|
13073
|
+
backendStep: async (instance, args) => {
|
|
13074
|
+
const refusal = this.backendCommandRefusal(instance);
|
|
13075
|
+
if (refusal)
|
|
13076
|
+
return { error: refusal };
|
|
13077
|
+
// The cached model lists only, as the web's /model: never a live probe on this path.
|
|
13078
|
+
const step = await backendStep(this.backendCommandDeps(instance, { live: false }), instance, backendArgs(args));
|
|
13079
|
+
if (step.kind === "refuse")
|
|
13080
|
+
return step.noop ? { text: step.text } : { error: step.text };
|
|
13081
|
+
if (step.kind === "choose") {
|
|
13082
|
+
const id = (token) => joinBackendArgs([...step.args, token]);
|
|
13083
|
+
return { choices: { prompt: step.text, current: step.current === null ? null : id(step.current), options: step.choices.map(c => ({ id: id(c.token), label: c.label })) } };
|
|
13084
|
+
}
|
|
13085
|
+
const now = performance.now();
|
|
13086
|
+
for (const [k, v] of this.webBackendTokens)
|
|
13087
|
+
if (v.deadline < now)
|
|
13088
|
+
this.webBackendTokens.delete(k);
|
|
13089
|
+
const token = randomBytes(16).toString("hex");
|
|
13090
|
+
this.webBackendTokens.set(token, {
|
|
13091
|
+
instance, asked: joinBackendArgs(backendArgs(args)), args: step.args, from: step.from,
|
|
13092
|
+
target: this.fleetConfig.instances[instance], deadline: now + CLASSIC_BACKEND_SELECTION_TIMEOUT_MS,
|
|
13093
|
+
});
|
|
13094
|
+
return { confirm: { token, message: step.text } };
|
|
13095
|
+
},
|
|
13096
|
+
backendConfirm: async (instance, args, token) => {
|
|
13097
|
+
const entry = this.webBackendTokens.get(token);
|
|
13098
|
+
this.webBackendTokens.delete(token); // used once, whatever happens next
|
|
13099
|
+
if (!entry || entry.instance !== instance || entry.asked !== joinBackendArgs(backendArgs(args)) || performance.now() > entry.deadline) {
|
|
13100
|
+
return { status: 409, text: t("backend.selection_expired") };
|
|
13101
|
+
}
|
|
13102
|
+
// The instance it was asked about: one deleted and created again under the name, stopped or not, is another.
|
|
13103
|
+
const stillCurrent = () => this.backendTargetIs(instance, entry.target);
|
|
13104
|
+
if (!stillCurrent())
|
|
13105
|
+
return { status: 409, text: t("menu.click_stale") };
|
|
13106
|
+
return { status: 200, text: await this.applyBackendSwitch(instance, entry.args, entry.from, stillCurrent) };
|
|
13107
|
+
},
|
|
12491
13108
|
cancel: (instance) => this.cancelInstance(instance),
|
|
12492
13109
|
steer: (instance, text, originalBody) => this.topicCommands.sendSteer(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
12493
13110
|
btw: (instance, text, originalBody) => this.topicCommands.sendBtw(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
@@ -12516,7 +13133,8 @@ export class FleetManager {
|
|
|
12516
13133
|
// sendClear sends its first IPC synchronously: no await separates this check from the effect.
|
|
12517
13134
|
if (this.shuttingDown || !entry.fence())
|
|
12518
13135
|
return { status: 409, text: t("menu.click_stale") };
|
|
12519
|
-
|
|
13136
|
+
const r = this.topicCommands.clearInstance(instance);
|
|
13137
|
+
return r.sent ? { status: 200, text: r.text, sent: true } : { status: 409, text: r.text };
|
|
12520
13138
|
},
|
|
12521
13139
|
t: (key, ...args) => t(key, ...args),
|
|
12522
13140
|
}, input, opts);
|
|
@@ -12637,12 +13255,12 @@ export class FleetManager {
|
|
|
12637
13255
|
* the operator's. Keep the instance notice passive and put the action in the
|
|
12638
13256
|
* same-world General topic where an administrator can make the choice. */
|
|
12639
13257
|
async notifyNormalExit(instanceName) {
|
|
12640
|
-
this.notifyInstanceTopic(instanceName, t("exit.instance_notice", instanceName));
|
|
13258
|
+
this.notifyInstanceTopic(instanceName, t("exit.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
|
|
12641
13259
|
const web = this.webOnlyPromptPlace();
|
|
12642
13260
|
if (web) {
|
|
12643
13261
|
await this.postNonceButtonPrompt({
|
|
12644
13262
|
prefix: EXIT_RESTART_CALLBACK_PREFIX, alertType: "exit_restart", instanceName, ...web,
|
|
12645
|
-
message: t("exit.general_notice", instanceName),
|
|
13263
|
+
message: t("exit.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName)),
|
|
12646
13264
|
choices: [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }],
|
|
12647
13265
|
expiredText: t("exit.expired", instanceName),
|
|
12648
13266
|
});
|
|
@@ -12671,7 +13289,7 @@ export class FleetManager {
|
|
|
12671
13289
|
adapterId,
|
|
12672
13290
|
chatId,
|
|
12673
13291
|
threadId,
|
|
12674
|
-
message: t("exit.general_notice", instanceName),
|
|
13292
|
+
message: t("exit.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName)),
|
|
12675
13293
|
choices: [
|
|
12676
13294
|
{ action: "restart", label: t("exit.restart") },
|
|
12677
13295
|
{ action: "ignore", label: t("exit.ignore") },
|
|
@@ -12718,7 +13336,7 @@ export class FleetManager {
|
|
|
12718
13336
|
* contain usernames or secret-adjacent text and stays in daemon.log.
|
|
12719
13337
|
*/
|
|
12720
13338
|
async notifyInteractivePrompt(instanceName, kind) {
|
|
12721
|
-
this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName));
|
|
13339
|
+
this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
|
|
12722
13340
|
// Multi-world fleets can have one General per bot/platform. Route the assist
|
|
12723
13341
|
// request to the General bound to the same adapter as the blocked instance.
|
|
12724
13342
|
const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
|
|
@@ -12729,7 +13347,7 @@ export class FleetManager {
|
|
|
12729
13347
|
const label = this.interactivePromptLabel(kind);
|
|
12730
13348
|
const prompt = {
|
|
12731
13349
|
prefix: INTERACTIVE_ASSIST_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName,
|
|
12732
|
-
message: t("interactive.general_notice", instanceName, label),
|
|
13350
|
+
message: t("interactive.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName), label),
|
|
12733
13351
|
choices: [{ action: "confirm", label: t("interactive.confirm") }, { action: "cancel", label: t("interactive.cancel") }],
|
|
12734
13352
|
expiredText: t("interactive.expired", instanceName),
|
|
12735
13353
|
extra: { generalName, promptKind: kind, assistFor: this.interactionWaitKey(instanceName) },
|
|
@@ -14099,10 +14717,18 @@ export class FleetManager {
|
|
|
14099
14717
|
}
|
|
14100
14718
|
return;
|
|
14101
14719
|
}
|
|
14720
|
+
// Between the install ending and the sign-in starting there is no
|
|
14721
|
+
// session to cancel; this hand-off is what `/login cancel` stops — from
|
|
14722
|
+
// here on, as the login-shell lookup below is an await.
|
|
14723
|
+
const handoff = { backend, cancelled: false };
|
|
14724
|
+
this.installHandoff = handoff;
|
|
14102
14725
|
// The installer may only have added the binary to a profile PATH; a
|
|
14103
14726
|
// fresh login shell sees that, the fleet process's PATH may not.
|
|
14104
|
-
const installedAt = this.locateBinaryOnLoginShell(info.binary) ?? this.locateInInstallerBinDirs(info);
|
|
14727
|
+
const installedAt = (await this.locateBinaryOnLoginShell(info.binary)) ?? this.locateInInstallerBinDirs(info);
|
|
14728
|
+
if (handoff.cancelled || this.installHandoff !== handoff)
|
|
14729
|
+
return;
|
|
14105
14730
|
if (!installedAt) {
|
|
14731
|
+
this.installHandoff = null;
|
|
14106
14732
|
await chat.adapter.sendText(chat.chatId, t("install.verify_failed", backend, info.binary), { threadId: chat.threadId }).catch(() => { });
|
|
14107
14733
|
return;
|
|
14108
14734
|
}
|
|
@@ -14112,6 +14738,7 @@ export class FleetManager {
|
|
|
14112
14738
|
// installed, reported as verified, and then offered nowhere.
|
|
14113
14739
|
this.adoptBinaryDirectory(installedAt, backend);
|
|
14114
14740
|
if (!LOGIN_FLOWS[backend]) {
|
|
14741
|
+
this.installHandoff = null;
|
|
14115
14742
|
await chat.adapter.sendText(chat.chatId, t("install.success_no_login", backend), { threadId: chat.threadId }).catch(() => { });
|
|
14116
14743
|
return;
|
|
14117
14744
|
}
|
|
@@ -14120,10 +14747,6 @@ export class FleetManager {
|
|
|
14120
14747
|
// publish a durable completion line first so a successful install can
|
|
14121
14748
|
// never look like it silently disappeared. Include the binary that
|
|
14122
14749
|
// passed the fresh-login-shell verification and the exact next step.
|
|
14123
|
-
// Between the install ending and the sign-in starting there is no
|
|
14124
|
-
// session to cancel; this hand-off is what `/login cancel` stops.
|
|
14125
|
-
const handoff = { backend, cancelled: false };
|
|
14126
|
-
this.installHandoff = handoff;
|
|
14127
14750
|
await chat.adapter.sendText(chat.chatId, t("install.success", backend, info.binary), { threadId: chat.threadId }).catch(err => this.logger.warn({ err, backend }, "Failed to send durable install success notification"));
|
|
14128
14751
|
if (handoff.cancelled || this.installHandoff !== handoff)
|
|
14129
14752
|
return;
|
|
@@ -14182,16 +14805,14 @@ export class FleetManager {
|
|
|
14182
14805
|
* The absolute path a fresh login shell resolves `binary` to, or null. Only
|
|
14183
14806
|
* a real executable file counts: `command -v` also answers with an alias
|
|
14184
14807
|
* definition or a function name, neither of which a spawn could run.
|
|
14808
|
+
* Async (runOnLoginShell): a login file that backgrounds a process holding the shell's output once froze the fleet loop here.
|
|
14185
14809
|
*/
|
|
14186
|
-
locateBinaryOnLoginShell(binary) {
|
|
14187
|
-
return measureSyncWork("fleet.installLookup", () => this.locateBinaryOnLoginShellSync(binary));
|
|
14188
|
-
}
|
|
14189
|
-
locateBinaryOnLoginShellSync(binary) {
|
|
14810
|
+
async locateBinaryOnLoginShell(binary) {
|
|
14190
14811
|
try {
|
|
14191
|
-
const result =
|
|
14812
|
+
const result = await runOnLoginShell(`command -v ${binary}`);
|
|
14192
14813
|
if (result.status !== 0)
|
|
14193
14814
|
return null;
|
|
14194
|
-
const path =
|
|
14815
|
+
const path = result.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
14195
14816
|
if (!isAbsolute(path))
|
|
14196
14817
|
return null;
|
|
14197
14818
|
accessSync(path, fsConstants.X_OK);
|
|
@@ -14325,6 +14946,14 @@ export class FleetManager {
|
|
|
14325
14946
|
const room = this.classicChannels?.getAll().find(c => c.instanceName === name);
|
|
14326
14947
|
return room ? `room:${room.channelId}:${room.adapterId ?? ""}` : null;
|
|
14327
14948
|
}
|
|
14949
|
+
/** The only Try-card publisher: no arbitrary SSE sender/role can manufacture this metadata. */
|
|
14950
|
+
publishEffectTrial(instance, offer) {
|
|
14951
|
+
const row = this.webChatHistory.recordEffectTrial(instance, offer, new Date().toISOString());
|
|
14952
|
+
if (!row)
|
|
14953
|
+
return false;
|
|
14954
|
+
broadcastSseEvent(this.sseClients, "message", row, err => this.logger.debug({ err }, "SSE client write failed; evicting"), this.webChatHistory.cursorOf(row));
|
|
14955
|
+
return true;
|
|
14956
|
+
}
|
|
14328
14957
|
/** Push an SSE event to all connected Web UI clients. */
|
|
14329
14958
|
emitSseEvent(event, data) {
|
|
14330
14959
|
// #1386: a prompt opened or closed, or an instance's state moved — "Needs you" may have changed.
|
|
@@ -14340,8 +14969,13 @@ export class FleetManager {
|
|
|
14340
14969
|
messageId: typeof m.messageId === "string" ? m.messageId : undefined,
|
|
14341
14970
|
role: typeof m.role === "string" ? m.role : undefined,
|
|
14342
14971
|
buttons: m.buttons,
|
|
14972
|
+
click: m.click,
|
|
14973
|
+
links: m.links,
|
|
14343
14974
|
});
|
|
14344
14975
|
broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
|
|
14976
|
+
// #1629: live-only receipt after the recorded agent reply. Never persist/replay it or give it an SSE cursor.
|
|
14977
|
+
if (recorded.role === "agent")
|
|
14978
|
+
broadcastSseEvent(this.sseClients, "agent_reply_live", { v: 1, boot: recorded.boot, id: recorded.id, instance: recorded.instance }, onError);
|
|
14345
14979
|
return;
|
|
14346
14980
|
}
|
|
14347
14981
|
// #1266: a reply's buttons ended — the history shows it too (a later load, the public link's poll).
|
|
@@ -14391,7 +15025,7 @@ export class FleetManager {
|
|
|
14391
15025
|
const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
|
|
14392
15026
|
const configuredMinutes = instanceHangConfig?.timeout_minutes
|
|
14393
15027
|
?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
|
|
14394
|
-
?? 15
|
|
15028
|
+
?? DEFAULT_STUCK_TIMEOUT_MS / 60_000; // #1854: the detector's own default (10 min), not a stale 15
|
|
14395
15029
|
const unchangedMinutes = unchangedForMs == null
|
|
14396
15030
|
? configuredMinutes
|
|
14397
15031
|
: Math.max(1, Math.floor(unchangedForMs / 60_000));
|
|
@@ -14449,12 +15083,12 @@ export class FleetManager {
|
|
|
14449
15083
|
"codex": "AGENTS.md",
|
|
14450
15084
|
"opencode": "AGENTS.md",
|
|
14451
15085
|
"kiro-cli": ".kiro/steering/project.md",
|
|
14452
|
-
// Grok reads AGENTS.md project docs; agy reads .agents/
|
|
15086
|
+
// Grok reads AGENTS.md project docs; agy reads .agents/AGENTS.md (board 0340ec89: not the lowercase name) — the
|
|
14453
15087
|
// same files their writeConfig() appends fleet instructions to.
|
|
14454
15088
|
"grok": "AGENTS.md",
|
|
14455
15089
|
// muse init scaffolds AGENTS.md and the binary reads it as project rules.
|
|
14456
15090
|
"muse": "AGENTS.md",
|
|
14457
|
-
"antigravity": ".agents/
|
|
15091
|
+
"antigravity": ".agents/AGENTS.md",
|
|
14458
15092
|
"mock": "CLAUDE.md",
|
|
14459
15093
|
};
|
|
14460
15094
|
static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
|
|
@@ -14494,10 +15128,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14494
15128
|
ensureGeneralInstructionsSync(workDir, backendName, instanceName) {
|
|
14495
15129
|
const backend = backendName ?? "claude-code";
|
|
14496
15130
|
workDir = this.resolveKnowledgeWorkDir(workDir, backend, instanceName);
|
|
15131
|
+
// Board 0340ec89: General's file from before, under the lowercase name agy never read, becomes the one it reads.
|
|
15132
|
+
if (backend === "antigravity")
|
|
15133
|
+
migrateAgyAgentsFile(workDir);
|
|
14497
15134
|
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
14498
15135
|
const filePath = join(workDir, filename);
|
|
14499
15136
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
14500
|
-
if (
|
|
15137
|
+
if (backend === "antigravity" && danglingSymlink(filePath)) {
|
|
15138
|
+
// #1914 review: a person's dangling symlink there is kept and not followed (nothing created where it points).
|
|
15139
|
+
this.logger.warn({ filePath }, "General's agy instructions file is a dangling symlink; left as it is, not written");
|
|
15140
|
+
}
|
|
15141
|
+
else if (!existsSync(filePath)) {
|
|
14501
15142
|
writeFileSync(filePath, FleetManager.GENERAL_INSTRUCTIONS, "utf-8");
|
|
14502
15143
|
this.logger.info({ filePath }, "Created general instance instructions file");
|
|
14503
15144
|
}
|
|
@@ -14594,6 +15235,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14594
15235
|
}
|
|
14595
15236
|
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
14596
15237
|
const instructionsPath = join(workDir, filename);
|
|
15238
|
+
if (backend === "antigravity" && danglingSymlink(instructionsPath))
|
|
15239
|
+
return; // kept, not followed (#1914 review; logged above)
|
|
14597
15240
|
const body = files.map(f => readFileSync(join(srcSteering, f), "utf-8").trim()).join("\n\n");
|
|
14598
15241
|
const block = `${FleetManager.STEERING_BLOCK_BEGIN}\n${body}\n${FleetManager.STEERING_BLOCK_END}`;
|
|
14599
15242
|
let existing = "";
|
|
@@ -14614,6 +15257,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14614
15257
|
mkdirSync(dirname(instructionsPath), { recursive: true });
|
|
14615
15258
|
writeFileSync(instructionsPath, next);
|
|
14616
15259
|
}
|
|
15260
|
+
// Board 0340ec89: a legacy agy file that stays beside the new one (a person's own) loses only AgEnD's block.
|
|
15261
|
+
const legacy = backend === "antigravity" ? agyLegacyAgentsFile(workDir) : null;
|
|
15262
|
+
if (legacy) {
|
|
15263
|
+
try {
|
|
15264
|
+
const old = readFileSync(legacy, "utf-8");
|
|
15265
|
+
const b = old.indexOf(FleetManager.STEERING_BLOCK_BEGIN), e = old.indexOf(FleetManager.STEERING_BLOCK_END);
|
|
15266
|
+
if (b !== -1 && e > b) {
|
|
15267
|
+
const rest = (old.slice(0, b).trimEnd() + "\n" + old.slice(e + FleetManager.STEERING_BLOCK_END.length).trimStart()).trim();
|
|
15268
|
+
if (rest)
|
|
15269
|
+
writeFileSync(legacy, rest + "\n");
|
|
15270
|
+
else
|
|
15271
|
+
rmSync(legacy, { force: true });
|
|
15272
|
+
}
|
|
15273
|
+
}
|
|
15274
|
+
catch { /* best effort: the new file already has the block */ }
|
|
15275
|
+
}
|
|
14617
15276
|
}
|
|
14618
15277
|
static STEERING_BLOCK_BEGIN = "<!-- >>> agend:core-rules — managed by AgEnD; edits inside this block are overwritten -->";
|
|
14619
15278
|
static STEERING_BLOCK_END = "<!-- <<< agend:core-rules -->";
|
|
@@ -15149,10 +15808,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15149
15808
|
* accepts any string ("Set model to opsu"), and its "Switch model?" then names the typo. Static aliases (with [1m]),
|
|
15150
15809
|
* the cached account catalog, the configured model; without a cached catalog, any `claude-…` id too (cannot check).
|
|
15151
15810
|
*/
|
|
15152
|
-
claudeModelKnown(instanceName, model
|
|
15811
|
+
claudeModelKnown(instanceName, model,
|
|
15812
|
+
/** #1806: null on a switch INTO claude-code — the configured model then belongs to the backend being left. */
|
|
15813
|
+
configured = this.fleetConfig?.instances[instanceName]?.model ?? this.fleetConfig?.defaults?.model) {
|
|
15153
15814
|
const aliases = ["default", "sonnet", "opus", "haiku", "opusplan", "Fable"];
|
|
15154
15815
|
const api = (this.readCliEnv("claude-code")?.apiModels ?? []).map(m => m.id);
|
|
15155
|
-
const configured = this.fleetConfig?.instances[instanceName]?.model ?? this.fleetConfig?.defaults?.model;
|
|
15156
15816
|
const lower = model.trim().toLowerCase();
|
|
15157
15817
|
const bare = lower.replace(/\[1m\]$/, "");
|
|
15158
15818
|
const known = aliases.some(a => a.toLowerCase() === bare)
|
|
@@ -16131,7 +16791,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16131
16791
|
/** How this instance's backend applies an effort change. */
|
|
16132
16792
|
effortStrategyFor(instanceName) {
|
|
16133
16793
|
try {
|
|
16134
|
-
const { strategy, levels } =
|
|
16794
|
+
const { strategy, levels } = this.instanceEffortMetadata(instanceName);
|
|
16135
16795
|
return strategy !== "unsupported" && levels.length > 0 ? strategy : "unsupported";
|
|
16136
16796
|
}
|
|
16137
16797
|
catch {
|
|
@@ -16141,13 +16801,23 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16141
16801
|
/** Effort levels this instance's backend actually accepts (empty = unsupported). */
|
|
16142
16802
|
effortLevelsFor(instanceName) {
|
|
16143
16803
|
try {
|
|
16144
|
-
const { strategy, levels } =
|
|
16804
|
+
const { strategy, levels } = this.instanceEffortMetadata(instanceName);
|
|
16145
16805
|
return strategy === "unsupported" ? [] : levels;
|
|
16146
16806
|
}
|
|
16147
16807
|
catch {
|
|
16148
16808
|
return [];
|
|
16149
16809
|
}
|
|
16150
16810
|
}
|
|
16811
|
+
/** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
|
|
16812
|
+
instanceEffortMetadata(instanceName) {
|
|
16813
|
+
const backend = this.backendNameForInstance(instanceName);
|
|
16814
|
+
let model = null;
|
|
16815
|
+
if (backend === "antigravity") {
|
|
16816
|
+
const resolved = this.resolveInstanceModel(instanceName);
|
|
16817
|
+
model = resolved.source === "unresolved" ? null : resolved.model;
|
|
16818
|
+
}
|
|
16819
|
+
return readEffortMetadata(backend, this.getInstanceDir(instanceName), model);
|
|
16820
|
+
}
|
|
16151
16821
|
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
16152
16822
|
resolveInstanceEffort(instanceName) {
|
|
16153
16823
|
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
@@ -16299,6 +16969,377 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16299
16969
|
? `\n${t("effort.current_fleet", effort)}`
|
|
16300
16970
|
: `\n${t("effort.current", effort)}`;
|
|
16301
16971
|
}
|
|
16972
|
+
// ── #1806: /backend — another CLI backend, with its model and effort, in one write and one relaunch ──
|
|
16973
|
+
/** The steps' view of this fleet (backend-command.ts). `live`: a missing or stale model list may be probed (not on the web). */
|
|
16974
|
+
backendCommandDeps(instanceName, opts) {
|
|
16975
|
+
return {
|
|
16976
|
+
currentBackend: (name) => this.backendNameForInstance(name),
|
|
16977
|
+
currentDisplay: (name) => `${this.backendNameForInstance(name)} · ${this.resolveInstanceModel(name).display}`,
|
|
16978
|
+
installedBackends: () => KNOWN_BACKENDS.filter(b => b !== "mock" && this.backendInstalledHere(b)),
|
|
16979
|
+
models: (backend) => this.backendModelOptions(backend, opts),
|
|
16980
|
+
// #1808: the same rule update_instance_config's switch applies (backendModelVerdict).
|
|
16981
|
+
modelKnown: (backend, model) => this.backendModelVerdict(instanceName, backend, model).ok,
|
|
16982
|
+
effortLevels: (backend, model) => this.backendEffortLevels(instanceName, backend, model),
|
|
16983
|
+
inherited: (key) => {
|
|
16984
|
+
const value = getEffectiveInstanceDefaults(this.fleetConfig?.defaults)[key];
|
|
16985
|
+
return typeof value === "string" && value ? value : undefined;
|
|
16986
|
+
},
|
|
16987
|
+
modelCompatible: (backend, model) => isModelCompatible(backend, model),
|
|
16988
|
+
state: (name) => {
|
|
16989
|
+
const status = this.getInstanceStatus(name);
|
|
16990
|
+
return { running: status === "running", busy: status === "running" && !this.getInstanceIdle(name), status };
|
|
16991
|
+
},
|
|
16992
|
+
t: (key, ...args) => t(key, ...args),
|
|
16993
|
+
};
|
|
16994
|
+
}
|
|
16995
|
+
/**
|
|
16996
|
+
* `/effort`'s levels for a backend this instance may move to, for the model that will run there (codex's levels are
|
|
16997
|
+
* per model; null: its configured default). Empty: it has no effort setting. A file read, never a CLI.
|
|
16998
|
+
*/
|
|
16999
|
+
backendEffortLevels(instanceName, backend, model = null) {
|
|
17000
|
+
// codex: the model the NEXT launch runs, for the levels and the gate alike (#1807 review r2).
|
|
17001
|
+
let runs = model;
|
|
17002
|
+
if (backend === "codex") {
|
|
17003
|
+
const next = this.prospectiveCodexModel(instanceName, model);
|
|
17004
|
+
if (!next.proven)
|
|
17005
|
+
return [];
|
|
17006
|
+
runs = next.model;
|
|
17007
|
+
}
|
|
17008
|
+
let levels;
|
|
17009
|
+
try {
|
|
17010
|
+
const metadata = readEffortMetadata(backend, this.getInstanceDir(instanceName), runs);
|
|
17011
|
+
levels = metadata.strategy === "unsupported" ? [] : metadata.levels;
|
|
17012
|
+
}
|
|
17013
|
+
catch {
|
|
17014
|
+
return [];
|
|
17015
|
+
}
|
|
17016
|
+
// #1811: only the levels this host's launch of that backend will pass (launch-effort.ts, the gate the launch
|
|
17017
|
+
// itself applies) — an effort chosen here reaches the new CLI with the one relaunch, or is not offered.
|
|
17018
|
+
const takes = this.launchEffortGate(instanceName, backend, runs);
|
|
17019
|
+
return levels.filter(takes);
|
|
17020
|
+
}
|
|
17021
|
+
/**
|
|
17022
|
+
* #1808: whether a backend takes this model name — the one rule for /backend and update_instance_config's switch.
|
|
17023
|
+
* The name must be one its launch line can carry (modelNameCarried); then its offered list (the cached CLI env), and
|
|
17024
|
+
* claude-code's known names (without this instance's configured model, which belongs to the backend being left).
|
|
17025
|
+
* With no list known, the backend's usual name pattern decides.
|
|
17026
|
+
*/
|
|
17027
|
+
backendModelVerdict(instanceName, backend, model,
|
|
17028
|
+
/** The provider the backend will run on (default: this instance's own, else the fleet's). */
|
|
17029
|
+
opts = { provider: this.customProviderFor(instanceName, backend) }) {
|
|
17030
|
+
// A custom provider has its own catalog, which the CLI reads at launch and AgEnD cannot see before one (#1815
|
|
17031
|
+
// review): only the launch-line rule applies — never the account's list or the default provider's naming pattern.
|
|
17032
|
+
if (opts.provider)
|
|
17033
|
+
return modelNameCarried(backend, model) ? { ok: true } : { ok: false, choices: [] };
|
|
17034
|
+
const listed = this.readCliEnv(backend)?.models ?? [];
|
|
17035
|
+
const choices = listed.slice(0, 24).map(o => o.id);
|
|
17036
|
+
if (!modelNameCarried(backend, model))
|
|
17037
|
+
return { ok: false, choices };
|
|
17038
|
+
if (listed.some(o => o.id === model))
|
|
17039
|
+
return { ok: true };
|
|
17040
|
+
if (backend === "claude-code") {
|
|
17041
|
+
const check = this.claudeModelKnown(instanceName, model, null);
|
|
17042
|
+
return check.known ? { ok: true } : { ok: false, choices: check.names };
|
|
17043
|
+
}
|
|
17044
|
+
return listed.length === 0 && isModelCompatible(backend, model) ? { ok: true } : { ok: false, choices };
|
|
17045
|
+
}
|
|
17046
|
+
/**
|
|
17047
|
+
* #1807 review r2: the model codex's next launch of this instance runs. Every launch's writeConfig copies the SHARED
|
|
17048
|
+
* config.toml into the instance's codex home first, so that source decides — not the home's copy from an earlier
|
|
17049
|
+
* launch. Read-only; a model chosen here (`-c model=…`) outranks it; a project file that could change it, unproven.
|
|
17050
|
+
*/
|
|
17051
|
+
prospectiveCodexModel(instanceName, model) {
|
|
17052
|
+
const { sharedHome } = codexMetadataHomes(this.getInstanceDir(instanceName));
|
|
17053
|
+
return resolveCodexLaunchModel({
|
|
17054
|
+
isolatedHome: sharedHome, sharedHome, model,
|
|
17055
|
+
workingDirectory: this.fleetConfig?.instances[instanceName]?.working_directory,
|
|
17056
|
+
});
|
|
17057
|
+
}
|
|
17058
|
+
/** #1811: whether a launch of `backend` here would pass a level: the same gate its builder applies. */
|
|
17059
|
+
launchEffortGate(instanceName, backend, model) {
|
|
17060
|
+
if (backend === "claude-code" || backend === "grok" || backend === "antigravity") {
|
|
17061
|
+
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
17062
|
+
if (!binary)
|
|
17063
|
+
return () => false;
|
|
17064
|
+
const path = resolveBinary(binary);
|
|
17065
|
+
return level => flagEffort(backend, level, path).arg !== null;
|
|
17066
|
+
}
|
|
17067
|
+
if (backend === "codex") {
|
|
17068
|
+
// `model` is the next launch's, proven by the caller (prospectiveCodexModel); the catalog is codex's own cache.
|
|
17069
|
+
if (!model)
|
|
17070
|
+
return () => false;
|
|
17071
|
+
const catalog = readCodexModelEffortLevels({ ...codexMetadataHomes(this.getInstanceDir(instanceName)), model });
|
|
17072
|
+
return level => codexEffort(level, catalog).arg !== null;
|
|
17073
|
+
}
|
|
17074
|
+
// kiro-cli gates its own --effort by version at launch (a quiet console note otherwise); muse always passes it.
|
|
17075
|
+
return () => true;
|
|
17076
|
+
}
|
|
17077
|
+
/** Installed the way the launch finds a CLI (resolveBinary): on PATH or in a known install directory. */
|
|
17078
|
+
backendInstalledHere(backend) {
|
|
17079
|
+
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
17080
|
+
return !!binary && resolveBinary(binary) !== binary;
|
|
17081
|
+
}
|
|
17082
|
+
/** The models `/model` would offer on a backend: the cached CLI env, else (live) a bounded probe. Never throws. */
|
|
17083
|
+
async backendModelOptions(backend, opts) {
|
|
17084
|
+
const cached = this.readCliEnv(backend);
|
|
17085
|
+
if (!opts.live || (cached?.models.length && !this.cliEnvNeedsRefresh(cached)))
|
|
17086
|
+
return cached?.models ?? [];
|
|
17087
|
+
opts.onLiveProbe?.();
|
|
17088
|
+
const env = await this.probeBackendBounded(backend).catch(() => null);
|
|
17089
|
+
return env?.models.length ? env.models : cached?.models ?? [];
|
|
17090
|
+
}
|
|
17091
|
+
/**
|
|
17092
|
+
* #1806 review: the instance a `/backend` was asked about is still this config entry — not one deleted and created
|
|
17093
|
+
* again under the same name, or replaced by a Settings edit (both put a new object there), stopped or not.
|
|
17094
|
+
*/
|
|
17095
|
+
backendTargetIs(instanceName, target) {
|
|
17096
|
+
const instances = this.fleetConfig?.instances;
|
|
17097
|
+
return !!target && !!instances && Object.prototype.hasOwnProperty.call(instances, instanceName) && instances[instanceName] === target;
|
|
17098
|
+
}
|
|
17099
|
+
/** `/backend` is for fleet.yaml instances: update_instance_config does not manage a ClassicBot agent. */
|
|
17100
|
+
backendCommandRefusal(instanceName) {
|
|
17101
|
+
return this.fleetConfig?.instances && Object.prototype.hasOwnProperty.call(this.fleetConfig.instances, instanceName)
|
|
17102
|
+
? null : t("backend.classic_unsupported", instanceName);
|
|
17103
|
+
}
|
|
17104
|
+
/**
|
|
17105
|
+
* Apply a confirmed `/backend`: judged again first (a menu lives a minute — the backend may have moved, a CLI been
|
|
17106
|
+
* removed), then update_instance_config's write: backend, model and effort at once, refused and rolled back whole
|
|
17107
|
+
* (its credential-profile login gate included), then one fresh relaunch of a running instance.
|
|
17108
|
+
*/
|
|
17109
|
+
async applyBackendSwitch(instanceName, args, from, stillCurrent) {
|
|
17110
|
+
if (this.backendNameForInstance(instanceName) !== from)
|
|
17111
|
+
return t("backend.changed", instanceName, from);
|
|
17112
|
+
if (!stillCurrent())
|
|
17113
|
+
return t("menu.click_stale");
|
|
17114
|
+
const deps = this.backendCommandDeps(instanceName, { live: false });
|
|
17115
|
+
const step = await backendStep(deps, instanceName, args);
|
|
17116
|
+
if (step.kind !== "confirm")
|
|
17117
|
+
return step.kind === "refuse" ? step.text : t("backend.usage");
|
|
17118
|
+
// Asked again after the await, with nothing awaited between here and the write (applyInstanceConfigPatch writes
|
|
17119
|
+
// synchronously): two switches confirmed together, or an instance changed meanwhile, never both succeed.
|
|
17120
|
+
if (this.backendNameForInstance(instanceName) !== from)
|
|
17121
|
+
return t("backend.changed", instanceName, from);
|
|
17122
|
+
if (!stillCurrent())
|
|
17123
|
+
return t("menu.click_stale");
|
|
17124
|
+
const { plan } = step;
|
|
17125
|
+
const shown = planDisplay(deps, plan);
|
|
17126
|
+
const { value, error } = await new Promise(resolve => {
|
|
17127
|
+
applyInstanceConfigPatch(this, instanceName, { backend: plan.backend, model: plan.model, effort: plan.effort }, (value, error) => resolve({ value, error }));
|
|
17128
|
+
});
|
|
17129
|
+
if (error)
|
|
17130
|
+
return t("backend.refused", instanceName, error);
|
|
17131
|
+
const result = (value ?? {});
|
|
17132
|
+
if (result.warning)
|
|
17133
|
+
return t("backend.restart_failed", instanceName, plan.backend, result.warning);
|
|
17134
|
+
if (result.restarted)
|
|
17135
|
+
return t("backend.switched", instanceName, plan.backend, shown.model, shown.effort);
|
|
17136
|
+
return t("backend.switched_not_running", instanceName, plan.backend, shown.model, shown.effort, this.getInstanceStatus(instanceName));
|
|
17137
|
+
}
|
|
17138
|
+
/** A step's buttons: index callbacks (a model id can be longer than Telegram's 64-byte callback data allows). */
|
|
17139
|
+
backendStepButtons(instanceName, nonce, step) {
|
|
17140
|
+
if (step.kind === "choose")
|
|
17141
|
+
return step.choices.map((c, i) => ({ id: `${BACKEND_SELECT_CALLBACK_PREFIX}${nonce}:${i}`, label: c.label }));
|
|
17142
|
+
if (step.kind !== "confirm")
|
|
17143
|
+
return [];
|
|
17144
|
+
const running = this.getInstanceStatus(instanceName) === "running";
|
|
17145
|
+
return [
|
|
17146
|
+
{ id: `${BACKEND_SELECT_CALLBACK_PREFIX}${nonce}:0`, label: t(running ? "backend.confirm_button" : "backend.confirm_button_stopped") },
|
|
17147
|
+
{ id: `${BACKEND_SELECT_CALLBACK_PREFIX}${nonce}:1`, label: t("backend.cancel_button") },
|
|
17148
|
+
];
|
|
17149
|
+
}
|
|
17150
|
+
/**
|
|
17151
|
+
* Show a `/backend` step (a choice, or the confirmation) where the command was given: Discord edits its interaction
|
|
17152
|
+
* reply; Telegram retires the previous step's keyboard and posts the next. One pending entry per step, under a fresh
|
|
17153
|
+
* nonce: a click on an older step finds nothing.
|
|
17154
|
+
*/
|
|
17155
|
+
async presentBackendStep(place, step) {
|
|
17156
|
+
const nonce = randomBytes(6).toString("hex");
|
|
17157
|
+
const buttons = this.backendStepButtons(place.instanceName, nonce, step);
|
|
17158
|
+
const timer = setTimeout(() => {
|
|
17159
|
+
const p = this.pendingBackendSelects.get(nonce);
|
|
17160
|
+
if (!p)
|
|
17161
|
+
return;
|
|
17162
|
+
this.pendingBackendSelects.delete(nonce);
|
|
17163
|
+
void this.endBackendMenu(p, t("backend.selection_expired")).catch(() => { });
|
|
17164
|
+
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
17165
|
+
timer.unref?.();
|
|
17166
|
+
const pending = { ...place, step, timer };
|
|
17167
|
+
this.pendingBackendSelects.set(nonce, pending);
|
|
17168
|
+
try {
|
|
17169
|
+
if (step.kind === "confirm" && place.respondButtons) {
|
|
17170
|
+
await place.respondButtons(step.text, buttons);
|
|
17171
|
+
return;
|
|
17172
|
+
}
|
|
17173
|
+
if (place.respondChoices) {
|
|
17174
|
+
await place.respondChoices(step.text, buttons);
|
|
17175
|
+
return;
|
|
17176
|
+
}
|
|
17177
|
+
if (!place.adapter || !place.adapterChatId)
|
|
17178
|
+
throw new Error("no place to show the menu");
|
|
17179
|
+
if (place.menuMessageId && place.adapter.editMessageRemoveButtons) {
|
|
17180
|
+
const chosen = t("backend.chosen", joinBackendArgs(step.args));
|
|
17181
|
+
await place.adapter.editMessageRemoveButtons(place.adapterChatId, place.menuMessageId, chosen, place.adapterThreadId).catch(() => { });
|
|
17182
|
+
}
|
|
17183
|
+
const menuMessageId = await place.adapter.promptUser(place.adapterChatId, step.text, buttons, { threadId: place.adapterThreadId });
|
|
17184
|
+
const fresh = this.pendingBackendSelects.get(nonce);
|
|
17185
|
+
if (fresh)
|
|
17186
|
+
fresh.menuMessageId = menuMessageId;
|
|
17187
|
+
}
|
|
17188
|
+
catch (err) {
|
|
17189
|
+
this.pendingBackendSelects.delete(nonce);
|
|
17190
|
+
clearTimeout(timer);
|
|
17191
|
+
this.logger.warn({ err, instanceName: place.instanceName }, "backend menu failed");
|
|
17192
|
+
await place.respond(t("backend.usage")).catch(() => { });
|
|
17193
|
+
}
|
|
17194
|
+
}
|
|
17195
|
+
/** Close a `/backend` menu with a last word: its buttons go (Telegram edits them away), the text stays. Returns where it was said. */
|
|
17196
|
+
async endBackendMenu(pending, text) {
|
|
17197
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
17198
|
+
if (pending.menuMessageId && pending.adapter.editMessageRemoveButtons) {
|
|
17199
|
+
try {
|
|
17200
|
+
await pending.adapter.editMessageRemoveButtons(pending.adapterChatId, pending.menuMessageId, text, pending.adapterThreadId);
|
|
17201
|
+
return pending.menuMessageId;
|
|
17202
|
+
}
|
|
17203
|
+
catch { /* say it in a new message */ }
|
|
17204
|
+
}
|
|
17205
|
+
return (await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId })).messageId;
|
|
17206
|
+
}
|
|
17207
|
+
if (pending.retireButtons)
|
|
17208
|
+
await pending.retireButtons(text);
|
|
17209
|
+
else
|
|
17210
|
+
await pending.respond(text);
|
|
17211
|
+
return undefined;
|
|
17212
|
+
}
|
|
17213
|
+
/** Start `/backend` for a fleet instance from Discord or Telegram; the caller has checked the channel admin. */
|
|
17214
|
+
async beginBackendCommand(place, args) {
|
|
17215
|
+
const refusal = this.backendCommandRefusal(place.instanceName);
|
|
17216
|
+
if (refusal) {
|
|
17217
|
+
await place.respond(refusal);
|
|
17218
|
+
return;
|
|
17219
|
+
}
|
|
17220
|
+
if (!place.respondChoices && !(place.adapter && place.adapterChatId)) {
|
|
17221
|
+
await place.respond(t("backend.usage"));
|
|
17222
|
+
return;
|
|
17223
|
+
}
|
|
17224
|
+
place = { ...place, target: this.fleetConfig.instances[place.instanceName] };
|
|
17225
|
+
const deps = this.backendCommandDeps(place.instanceName, {
|
|
17226
|
+
live: true,
|
|
17227
|
+
onLiveProbe: () => { void place.respond(t("model.refreshing")).catch(() => { }); },
|
|
17228
|
+
});
|
|
17229
|
+
const step = await backendStep(deps, place.instanceName, args);
|
|
17230
|
+
if (step.kind === "refuse") {
|
|
17231
|
+
await place.respond(step.text);
|
|
17232
|
+
return;
|
|
17233
|
+
}
|
|
17234
|
+
await this.presentBackendStep(place, step);
|
|
17235
|
+
}
|
|
17236
|
+
/** `/backend` slash handler: channel admin, as `/model`. Options or typed text give the direct form. */
|
|
17237
|
+
async handleBackendSlash(data, adapterId) {
|
|
17238
|
+
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
17239
|
+
await data.respond(t("permission.denied"));
|
|
17240
|
+
return;
|
|
17241
|
+
}
|
|
17242
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
17243
|
+
if (!name) {
|
|
17244
|
+
await data.respond(t("classic.no_agent"));
|
|
17245
|
+
return;
|
|
17246
|
+
}
|
|
17247
|
+
const option = (key) => typeof data.options?.[key] === "string" ? data.options[key].trim() : "";
|
|
17248
|
+
const given = [option("backend"), option("model"), option("effort")];
|
|
17249
|
+
// A later option without the one before it says nothing that can be judged in order.
|
|
17250
|
+
if (given.some((value, i) => value && given.slice(0, i).some(before => !before))) {
|
|
17251
|
+
await data.respond(t("backend.usage"));
|
|
17252
|
+
return;
|
|
17253
|
+
}
|
|
17254
|
+
const args = given[0] ? given.filter(Boolean) : backendArgs(data.text ?? "");
|
|
17255
|
+
await this.beginBackendCommand({ instanceName: name, userId: data.userId, channelId: data.channelId, adapterId, respond: data.respond, respondChoices: data.respondChoices,
|
|
17256
|
+
respondButtons: data.respondButtons, retireButtons: data.retireButtons }, args);
|
|
17257
|
+
}
|
|
17258
|
+
/** Telegram `/backend` in a fleet topic (topic-commands) or a ClassicBot chat; the caller has checked the admin. */
|
|
17259
|
+
async runBackendCommand(instanceName, text, userId, channelId, adapter, chatId, threadId, adapterId) {
|
|
17260
|
+
const respond = async (reply) => (await adapter.sendText(chatId, reply, { threadId })).messageId;
|
|
17261
|
+
await this.beginBackendCommand({ instanceName, userId, channelId, adapterId, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId }, backendArgs(text.replace(/^\/backend(@\S+)?/, "")));
|
|
17262
|
+
}
|
|
17263
|
+
/** Consume a `/backend` click: the next step, or on the confirmation the switch itself. True for every backend-select id. */
|
|
17264
|
+
async handleBackendSelection(data, adapterId) {
|
|
17265
|
+
if (!data.callbackData.startsWith(BACKEND_SELECT_CALLBACK_PREFIX))
|
|
17266
|
+
return false;
|
|
17267
|
+
const match = data.callbackData.match(/^backend-select:([0-9a-f]+):(\d{1,2})$/);
|
|
17268
|
+
if (!match)
|
|
17269
|
+
return true;
|
|
17270
|
+
const pending = this.pendingBackendSelects.get(match[1]);
|
|
17271
|
+
if (!pending)
|
|
17272
|
+
return true;
|
|
17273
|
+
// As a /model click (#754 audit): the admin who opened it, through the same bot, in the same channel, still admin.
|
|
17274
|
+
const cbChannel = data.threadId ?? data.chatId;
|
|
17275
|
+
if (!data.userId || data.userId !== pending.userId
|
|
17276
|
+
|| (pending.adapterId !== undefined && pending.adapterId !== adapterId)
|
|
17277
|
+
|| (cbChannel !== pending.channelId && data.chatId !== pending.channelId)
|
|
17278
|
+
|| !this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)) {
|
|
17279
|
+
data.ack?.(t("buttons.admin_only"));
|
|
17280
|
+
return true;
|
|
17281
|
+
}
|
|
17282
|
+
this.pendingBackendSelects.delete(match[1]);
|
|
17283
|
+
clearTimeout(pending.timer);
|
|
17284
|
+
if (!pending.menuMessageId && data.messageId)
|
|
17285
|
+
pending.menuMessageId = data.messageId;
|
|
17286
|
+
const index = Number(match[2]);
|
|
17287
|
+
const { step } = pending;
|
|
17288
|
+
if (step.kind === "choose") {
|
|
17289
|
+
const choice = step.choices[index];
|
|
17290
|
+
if (!choice)
|
|
17291
|
+
return true;
|
|
17292
|
+
if (!this.backendTargetIs(pending.instanceName, pending.target)) {
|
|
17293
|
+
await this.endBackendMenu(pending, t("menu.click_stale")).catch(() => { });
|
|
17294
|
+
return true;
|
|
17295
|
+
}
|
|
17296
|
+
const next = await backendStep(this.backendCommandDeps(pending.instanceName, { live: true }), pending.instanceName, [...step.args, choice.token]);
|
|
17297
|
+
if (next.kind === "refuse") {
|
|
17298
|
+
await this.endBackendMenu(pending, next.text).catch(() => { });
|
|
17299
|
+
return true;
|
|
17300
|
+
}
|
|
17301
|
+
await this.presentBackendStep(pending, next);
|
|
17302
|
+
return true;
|
|
17303
|
+
}
|
|
17304
|
+
if (index === 1) {
|
|
17305
|
+
await this.endBackendMenu(pending, t("backend.cancelled")).catch(() => { });
|
|
17306
|
+
return true;
|
|
17307
|
+
}
|
|
17308
|
+
if (index !== 0)
|
|
17309
|
+
return true;
|
|
17310
|
+
// Confirmed: say so at once, then switch in the background (the relaunch takes a while) and say how it went.
|
|
17311
|
+
const progressId = await this.endBackendMenu(pending, t("backend.switching", pending.instanceName, step.plan.backend)).catch(() => undefined);
|
|
17312
|
+
const say = async (text) => {
|
|
17313
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
17314
|
+
if (progressId)
|
|
17315
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressId, text, pending.adapterThreadId);
|
|
17316
|
+
else
|
|
17317
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
17318
|
+
}
|
|
17319
|
+
else
|
|
17320
|
+
await pending.respond(text);
|
|
17321
|
+
};
|
|
17322
|
+
void (async () => {
|
|
17323
|
+
let result;
|
|
17324
|
+
try {
|
|
17325
|
+
// Asked again after the awaits above: the instance may have moved, or the admin lost admin.
|
|
17326
|
+
const stillCurrent = () => this.backendTargetIs(pending.instanceName, pending.target)
|
|
17327
|
+
&& this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId);
|
|
17328
|
+
result = stillCurrent()
|
|
17329
|
+
? await withSpawnHoldReply(say, () => this.applyBackendSwitch(pending.instanceName, step.args, step.from, stillCurrent))
|
|
17330
|
+
: t("menu.click_stale");
|
|
17331
|
+
}
|
|
17332
|
+
catch (err) {
|
|
17333
|
+
this.logger.error({ err, instance: pending.instanceName, backend: step.plan.backend }, "Backend switch failed");
|
|
17334
|
+
result = t("backend.failed", pending.instanceName, step.plan.backend, err instanceof Error ? err.message : String(err));
|
|
17335
|
+
}
|
|
17336
|
+
await say(result).catch(() => {
|
|
17337
|
+
if (pending.adapter && pending.adapterChatId)
|
|
17338
|
+
pending.adapter.sendText(pending.adapterChatId, result, { threadId: pending.adapterThreadId }).catch(() => { });
|
|
17339
|
+
});
|
|
17340
|
+
})();
|
|
17341
|
+
return true;
|
|
17342
|
+
}
|
|
16302
17343
|
/** Read recent chat log for agent context */
|
|
16303
17344
|
getRecentChatLog(instanceName, maxLines = 10) {
|
|
16304
17345
|
const logDir = ClassicChannelManager.chatLogDir(instanceName);
|
|
@@ -16602,6 +17643,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16602
17643
|
catch (err) {
|
|
16603
17644
|
this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room from web favorites failed");
|
|
16604
17645
|
}
|
|
17646
|
+
try {
|
|
17647
|
+
this.themeStore.forget(ch.instanceName);
|
|
17648
|
+
this.webAppearance.limiter.forget(ch.instanceName);
|
|
17649
|
+
this.effectToolsService?.forget(ch.instanceName);
|
|
17650
|
+
}
|
|
17651
|
+
catch (err) {
|
|
17652
|
+
this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room's web theme failed");
|
|
17653
|
+
}
|
|
16605
17654
|
}
|
|
16606
17655
|
this.instanceWorldBinding.delete(ch.instanceName);
|
|
16607
17656
|
// #1716: say so in the reply when background work was ended with the stop.
|
|
@@ -16711,6 +17760,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16711
17760
|
this.eventLogPruneTimer = null;
|
|
16712
17761
|
}
|
|
16713
17762
|
this.logService?.unwatchAll(); // #1218: a Log's watch timers end with the fleet
|
|
17763
|
+
this.compactWatchService?.stopAll();
|
|
16714
17764
|
if (this.outboxPruneTimer) {
|
|
16715
17765
|
clearInterval(this.outboxPruneTimer);
|
|
16716
17766
|
this.outboxPruneTimer = null;
|
|
@@ -18694,37 +19744,41 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18694
19744
|
const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
|
|
18695
19745
|
const latest = await npmVersion("@songsid/agend");
|
|
18696
19746
|
let target = latest;
|
|
19747
|
+
let fromChannel = false;
|
|
18697
19748
|
// The same channel rule as `agend update` (#1259): an alpha follows @alpha, any other prerelease @beta.
|
|
18698
19749
|
const channel = installedChannel(currentVersion);
|
|
18699
19750
|
if (channel !== "latest") {
|
|
18700
|
-
// Prerelease users track their own channel (never fall back to @latest,
|
|
18701
|
-
// which is older, and an alpha is never offered @beta), but should also
|
|
18702
|
-
// hear when a newer STABLE ships — pick whichever is the newest.
|
|
18703
19751
|
let pre = "";
|
|
18704
19752
|
try {
|
|
18705
19753
|
pre = await npmVersion(`@songsid/agend@${channel}`);
|
|
18706
19754
|
}
|
|
18707
19755
|
catch { /* no such tag */ }
|
|
18708
|
-
|
|
18709
|
-
|
|
19756
|
+
// #1926: a beta install hears about @beta only: beta users stay on the beta line after a stable ships, so
|
|
19757
|
+
// @latest is not their target. Taking the higher of the two made a same-core stable (2.2.0 over beta.16) the
|
|
19758
|
+
// target, which the same-core guard then dropped, so the newer beta.17 was never announced.
|
|
19759
|
+
target = pre;
|
|
19760
|
+
fromChannel = true;
|
|
19761
|
+
// An alpha is still told about a stable that passed its own minor (with --stable, #1259): only a stable of
|
|
19762
|
+
// another core that is newer than its channel's release. A same-core @latest (2.2.0 for 2.2.0-alpha.3) is not
|
|
19763
|
+
// taken from @latest and no longer hides a newer alpha; once the release points @alpha at it, it comes from
|
|
19764
|
+
// @alpha like any channel release.
|
|
19765
|
+
const core = (v) => v.replace(/^v/, "").split("-")[0];
|
|
19766
|
+
if (channel === "alpha" && latest && core(latest) !== core(currentVersion) && (!pre || this.semverGt(latest, pre))) {
|
|
18710
19767
|
target = latest;
|
|
19768
|
+
fromChannel = false;
|
|
19769
|
+
}
|
|
18711
19770
|
}
|
|
18712
|
-
// A beta already at/ahead of its matching stable must NOT be told to
|
|
18713
|
-
// "update" to that stable — e.g. 2.0.11-beta.41 already contains everything
|
|
18714
|
-
// in stable 2.0.11, so semverGt(2.0.11, 2.0.11-beta.41) being true (a stable
|
|
18715
|
-
// outranks a prerelease of the same core) is a false positive here. Suppress
|
|
18716
|
-
// only that same-core stable-vs-my-beta case; a higher stable core (2.0.12)
|
|
18717
|
-
// or a newer beta (2.0.11-beta.50) still notifies via semverGt below.
|
|
18718
|
-
const core = (v) => v.replace(/^v/, "").split("-")[0];
|
|
18719
|
-
const betaSupersedesStable = isPrereleaseVersion(currentVersion) && !isPrereleaseVersion(target) && core(target) === core(currentVersion);
|
|
18720
19771
|
// Only notify when target is genuinely newer (semver), so a beta user on
|
|
18721
19772
|
// 2.0.8-beta.16 is never told that stable 2.0.7 is "available".
|
|
18722
|
-
if (target &&
|
|
19773
|
+
if (target && this.semverGt(target, currentVersion)) {
|
|
18723
19774
|
const generalId = this.findGeneralInstance();
|
|
18724
19775
|
if (generalId) {
|
|
18725
19776
|
// No release URL — Discord's SuppressEmbeds proved unreliable and the
|
|
18726
19777
|
// link preview looked bad. Version + /update instruction is enough.
|
|
18727
|
-
|
|
19778
|
+
// From the install's own channel tag, this is what /update installs — even a stable the release pointed the
|
|
19779
|
+
// tag at (2.2.0 on @alpha / @beta): the plain /update text, never "/update stays on … use --stable" (#1928).
|
|
19780
|
+
const key = fromChannel ? "update.available_current" : updateNoticeKey(currentVersion, target);
|
|
19781
|
+
this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
|
|
18728
19782
|
}
|
|
18729
19783
|
}
|
|
18730
19784
|
}
|
|
@@ -18791,6 +19845,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18791
19845
|
/** Shared handler dispatcher; gateway admission is enforced by its separate listener. */
|
|
18792
19846
|
dispatchWebHttp(req, res, port, closing = false) {
|
|
18793
19847
|
try {
|
|
19848
|
+
// #1937: the listeners allow an upload long enough for a slow link; every other request keeps its old bound on
|
|
19849
|
+
// arriving whole — 10 s through the public link's gateway, Node's 300 s here.
|
|
19850
|
+
if (!isUploadRequest(req))
|
|
19851
|
+
limitRequestReceive(req, gatewayRequestContext(req) ? PUBLIC_GATEWAY_RECEIVE_MS : 300_000);
|
|
18794
19852
|
res.setHeader("Content-Type", "application/json");
|
|
18795
19853
|
// No Referer to a tunnel host, an upstream proxy, or any page linked from
|
|
18796
19854
|
// the panel — the dashboard URL is itself a credential-bearing address.
|
|
@@ -19132,6 +20190,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19132
20190
|
if (!this.webToken || !this.webSessions)
|
|
19133
20191
|
this.initializeWebAuthTokens();
|
|
19134
20192
|
const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
|
|
20193
|
+
// #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
|
|
20194
|
+
// more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
|
|
20195
|
+
// keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
|
|
20196
|
+
server.requestTimeout = UPLOAD_REQUEST_TIMEOUT_MS;
|
|
19135
20197
|
const drain = trackWebServer(server);
|
|
19136
20198
|
this.healthServer = server;
|
|
19137
20199
|
this.healthServerDrain = drain;
|
|
@@ -19318,13 +20380,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19318
20380
|
// /api/profiles (resolveInstanceIdentity) — the sidebar groups, searches and labels with it on every page.
|
|
19319
20381
|
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
19320
20382
|
const profiles = profileIdentities(this.dataDir);
|
|
20383
|
+
// #1629 r4 §1: the identity card's version per instance (the page reads the card when it moves).
|
|
20384
|
+
const cards = humanCards(this.dataDir);
|
|
20385
|
+
const colors = identityColors(this.dataDir);
|
|
19321
20386
|
// #1630: decorative — whatever goes wrong deciding an avatar never costs the frame (every instance gets its first character).
|
|
19322
|
-
let
|
|
20387
|
+
let bots = { own: new Map(), speaking: new Map() };
|
|
19323
20388
|
try {
|
|
19324
|
-
|
|
20389
|
+
bots = this.webBots();
|
|
19325
20390
|
}
|
|
19326
20391
|
catch (err) {
|
|
19327
|
-
this.logger.debug({ err }, "
|
|
20392
|
+
this.logger.debug({ err }, "bots unreadable (getUiStatus)");
|
|
19328
20393
|
}
|
|
19329
20394
|
const instances = names.map(name => {
|
|
19330
20395
|
const statusFile = join(this.getInstanceDir(name), "statusline.json");
|
|
@@ -19363,9 +20428,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19363
20428
|
const effort = (effortStrategy === "unsupported" || isAgy) ? null : effortResolved.effort;
|
|
19364
20429
|
const effort_source = (effortStrategy === "unsupported" || isAgy) ? null : effortResolved.source;
|
|
19365
20430
|
const identity = resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) });
|
|
20431
|
+
const compact = this.compactWatchService?.view(name) ?? null;
|
|
19366
20432
|
let image;
|
|
19367
20433
|
try {
|
|
19368
|
-
image = this.webIdentityImage(name, identity.display_name, profiles.get(name)?.avatar_path,
|
|
20434
|
+
image = this.webIdentityImage(name, identity.display_name, profiles.get(name)?.avatar_path, bots);
|
|
19369
20435
|
}
|
|
19370
20436
|
catch (err) {
|
|
19371
20437
|
this.logger.debug({ err, name }, "avatar unreadable (getUiStatus)");
|
|
@@ -19376,6 +20442,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19376
20442
|
display_name: identity.display_name || undefined,
|
|
19377
20443
|
avatar: image.avatar,
|
|
19378
20444
|
...(image.accent ? { accent: image.accent } : {}),
|
|
20445
|
+
// #1629: the agent's theme's picture and ring colour, for viewers who show agents' themes.
|
|
20446
|
+
...(image.themeAvatar ? { theme_avatar: image.themeAvatar } : {}),
|
|
20447
|
+
...(image.themeAccent ? { theme_accent: image.themeAccent } : {}),
|
|
20448
|
+
...(() => { try {
|
|
20449
|
+
const v = cardVersion(resolveCard(name, cards.get(name), this.themeStore.get(name).theme.card));
|
|
20450
|
+
return v ? { card_v: v } : {};
|
|
20451
|
+
}
|
|
20452
|
+
catch {
|
|
20453
|
+
return {};
|
|
20454
|
+
} })(),
|
|
20455
|
+
// #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
|
|
20456
|
+
color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
|
|
20457
|
+
...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
|
|
20458
|
+
// #1958 decision 2: where it shows — a person's choice for this instance, the agent's; else the viewer's host default.
|
|
20459
|
+
color_show: { ...(colors.get(name)?.show ? { person: colors.get(name).show } : {}), ...(image.agentShow ? { agent: image.agentShow } : {}) },
|
|
19379
20460
|
description: identity.description || undefined,
|
|
19380
20461
|
role: identity.role || undefined,
|
|
19381
20462
|
tags: identity.tags,
|
|
@@ -19390,6 +20471,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19390
20471
|
effort,
|
|
19391
20472
|
effort_source,
|
|
19392
20473
|
...this.instancePresentation(name),
|
|
20474
|
+
// Board 003648ac: the last web /compact, while it runs and briefly after (the chat's card follows it).
|
|
20475
|
+
...(compact ? { compact } : {}),
|
|
19393
20476
|
};
|
|
19394
20477
|
});
|
|
19395
20478
|
return {
|
|
@@ -19400,6 +20483,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19400
20483
|
connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
|
|
19401
20484
|
// #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
|
|
19402
20485
|
prefs_rev: this.webPrefs.rev(),
|
|
20486
|
+
// #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
|
|
20487
|
+
themes_rev: this.themeStore.rev(),
|
|
19403
20488
|
// #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
|
|
19404
20489
|
// shown here rather than in chat). null when normal, unknown or advisory.
|
|
19405
20490
|
host_memory: this.hostMemoryIndicator(),
|