@songsid/agend 2.3.2-beta.9 → 2.3.3-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 +6 -0
- package/README.zh-TW.md +6 -0
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-cli.js +48 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +6 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +21 -0
- package/dist/agent-whats-new.js +53 -21
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +13 -1
- package/dist/backend/claude-code.js +43 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +57 -0
- package/dist/backend/kiro-classic-archive.js +622 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +100 -1
- package/dist/backend/kiro-identity.js +262 -31
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +351 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +35 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +31 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +124 -6
- package/dist/backend/kiro.js +553 -39
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +109 -4
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +40 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +6 -0
- package/dist/classic-channel-manager.js +25 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +20 -9
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +10 -5
- package/dist/cross-instance-envelope.js +20 -17
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +125 -63
- package/dist/daemon.js +694 -330
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +15 -4
- package/dist/dashboard-permissions.js +49 -23
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-outbox.d.ts +27 -22
- package/dist/delivery-outbox.js +109 -161
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +10 -25
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +94 -15
- package/dist/fleet-manager.js +597 -133
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
- package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +2 -2
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +28 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +6 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-migrate.d.ts +19 -2
- package/dist/kiro-migrate.js +133 -35
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +88 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +19 -0
- package/dist/kiro-tui-forensic.js +124 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/locale.js +202 -16
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +3 -3
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.d.ts +3 -1
- package/dist/needs-you.js +4 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/office-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +6 -3
- package/dist/office-share-service.js +12 -8
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +45 -8
- package/dist/office-share.js +75 -15
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +21 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +42 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +39 -1
- package/dist/quickstart-api.js +115 -17
- package/dist/quickstart-api.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +44 -17
- package/dist/scheduler/task-board.js +95 -36
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +13 -14
- package/dist/sd-notify.js +62 -28
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.d.ts +2 -1
- package/dist/settings-api.js +19 -26
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +10 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -6
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +2 -2
- package/dist/setup-form.js +139 -93
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.d.ts +25 -0
- package/dist/setup-i18n.js +226 -0
- package/dist/setup-i18n.js.map +1 -0
- package/dist/task-board-waker.d.ts +40 -1
- package/dist/task-board-waker.js +115 -9
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -1
- package/dist/tmux-control.js +3 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.js +18 -16
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +2 -0
- package/dist/tmux-read.js +7 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -0
- package/dist/topic-commands.js +2 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +20 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/dist/ui/backend-signin.js +4 -3
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +81 -9
- package/dist/ui/fleet-office-layout.js +56 -11
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +15 -3
- package/dist/ui/fleet-office-pack.js +177 -23
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +74 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +102 -19
- package/dist/ui/fleet-office.js +308 -44
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +1 -0
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- package/dist/ui/settings-appearance.js +3 -35
- package/dist/ui/settings-discord.js +3 -2
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-registry.js +1 -1
- package/dist/ui/settings-strings.js +18 -6
- package/dist/ui/settings-token.js +3 -2
- package/dist/ui/settings-wizard.js +25 -9
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +70 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- package/dist/update-check.d.ts +13 -4
- package/dist/update-check.js +17 -4
- package/dist/update-check.js.map +1 -1
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +207 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +6 -2
- package/dist/web-experimental.js +8 -4
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +9 -4
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +241 -0
- package/dist/web-office-pack-tools.js +785 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +68 -4
- package/dist/web-office-skins.js +324 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +47 -4
- package/dist/web-office-store.js +124 -23
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -2
- package/dist/web-settings-patch.js +7 -5
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +3 -3
- package/dist/working-recovery.d.ts +0 -2
- package/dist/working-recovery.js +2 -23
- package/dist/working-recovery.js.map +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
- package/dist/upgrade-notices.d.ts +0 -17
- package/dist/upgrade-notices.js +0 -93
- package/dist/upgrade-notices.js.map +0 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
2
|
+
import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
|
|
3
|
+
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
4
|
+
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
5
|
+
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
2
6
|
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
3
7
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
4
8
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
@@ -24,7 +28,7 @@ import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, pub
|
|
|
24
28
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
25
29
|
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
26
30
|
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
27
|
-
import { ALL_OFFICES } from "./office-share.js";
|
|
31
|
+
import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
|
|
28
32
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
29
33
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
30
34
|
import { withinBudget } from "./monotonic-budget.js";
|
|
@@ -43,11 +47,12 @@ import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as re
|
|
|
43
47
|
import { fileURLToPath } from "node:url";
|
|
44
48
|
import { isDeepStrictEqual } from "node:util";
|
|
45
49
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
50
|
+
import { planKiroReconciliation, applyKiroReconciliation, kiroReconciliationPrompt } from "./kiro-reconcile.js";
|
|
46
51
|
import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
|
|
47
52
|
import { fleetLabel } from "./fleet-label.js";
|
|
48
53
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
49
54
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
50
|
-
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
|
|
55
|
+
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
|
|
51
56
|
import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
|
|
52
57
|
import { readFleetMemory } from "./process-memory.js";
|
|
53
58
|
import { MemoryPressure } from "./memory-pressure.js";
|
|
@@ -83,13 +88,13 @@ import { createBackend } from "./backend/factory.js";
|
|
|
83
88
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
84
89
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
85
90
|
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
86
|
-
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
|
|
91
|
+
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
87
92
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
88
93
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
89
94
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
90
95
|
import { Scheduler } from "./scheduler/index.js";
|
|
91
96
|
import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
|
|
92
|
-
import { TaskBoardWaker } from "./task-board-waker.js";
|
|
97
|
+
import { TaskBoardWaker, namesCard } from "./task-board-waker.js";
|
|
93
98
|
import { TaskBoardError, waitsForPerson, wrapUpGap } from "./scheduler/task-board.js";
|
|
94
99
|
import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
|
|
95
100
|
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
@@ -170,6 +175,7 @@ import { ThemeStore } from "./web-theme-store.js";
|
|
|
170
175
|
import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
171
176
|
import { OfficeSkinStore } from "./web-office-skins.js";
|
|
172
177
|
import { OfficeActs } from "./web-office-acts.js";
|
|
178
|
+
import { OFFICE_PACK_TOOLS, OfficePackTools } from "./web-office-pack-tools.js";
|
|
173
179
|
import { WebAppearance } from "./web-appearance.js";
|
|
174
180
|
import { WebEffectLibrary } from "./web-effect-library.js";
|
|
175
181
|
import { WebEffectTrials } from "./web-effect-trials.js";
|
|
@@ -225,10 +231,11 @@ import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin
|
|
|
225
231
|
import { structuredSignInSupported } from "./structured-signin.js";
|
|
226
232
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
227
233
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
234
|
+
import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteCardPrompt, REMOTE_CARD_TEXT_MAX } from "./remote-card-prompt.js";
|
|
228
235
|
import { recentChatContext } from "./log-tail.js";
|
|
229
236
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
230
237
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
231
|
-
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
238
|
+
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
232
239
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
233
240
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
234
241
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
@@ -247,8 +254,7 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
|
|
|
247
254
|
import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
|
|
248
255
|
import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
|
|
249
256
|
import { NeedsYouHub } from "./needs-you-hub.js";
|
|
250
|
-
import {
|
|
251
|
-
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
257
|
+
import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
|
|
252
258
|
import { buildOrgChart } from "./web-org.js";
|
|
253
259
|
import { topMemoryHogs } from "./memory-hogs.js";
|
|
254
260
|
import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
|
|
@@ -478,6 +484,13 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
|
478
484
|
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
479
485
|
const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
|
|
480
486
|
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
487
|
+
/** #2675: a card a CLI shows in place of its composer (kiro V3's Question / MCP input), posted with its answers. */
|
|
488
|
+
const REMOTE_CARD_CALLBACK_PREFIX = "card-answer:";
|
|
489
|
+
const REMOTE_CARD_CALLBACK_RE = /^card-answer:([0-9a-f]{32}):(opt-\d{1,2}|sub-all|sub-\d{1,2}|back|skip|decline|cancel|text)$/;
|
|
490
|
+
/** A posted card's buttons stay this long; the card itself waits in the pane for as long as it takes. */
|
|
491
|
+
const REMOTE_CARD_TIMEOUT_MS = 12 * 60 * 60_000;
|
|
492
|
+
/** A reply already taken as a card's answer is remembered this long, so another bot's copy of it is dropped (#2675). */
|
|
493
|
+
const REMOTE_CARD_TAKEN_TTL_MS = 10 * 60_000;
|
|
481
494
|
const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
|
|
482
495
|
const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
483
496
|
/** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
|
|
@@ -486,6 +499,11 @@ const HANG_CALLBACK_PREFIX = "hang:";
|
|
|
486
499
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
487
500
|
/** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
|
|
488
501
|
const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
|
|
502
|
+
const KIRO_RECONCILE_CALLBACK_PREFIX = "kiro-reconcile:";
|
|
503
|
+
/** #2692: a fleet admin's Approve / Reject on an agent's pending skin-pack change, in General. */
|
|
504
|
+
const OFFICE_PACK_CALLBACK_PREFIX = "office-pack:";
|
|
505
|
+
/** #2692: the skin-pack tools' IPC types (answered as web theme tools). */
|
|
506
|
+
const OFFICE_PACK_IPC = new Set(OFFICE_PACK_TOOLS.map(tool => `fleet_${tool}`));
|
|
489
507
|
/** Long enough for an admin to read the prompt; the move is judged again on the click. */
|
|
490
508
|
const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
491
509
|
/**
|
|
@@ -496,6 +514,7 @@ const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
|
496
514
|
*/
|
|
497
515
|
const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
|
|
498
516
|
HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
|
|
517
|
+
REMOTE_CARD_CALLBACK_PREFIX,
|
|
499
518
|
]);
|
|
500
519
|
/**
|
|
501
520
|
* Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
|
|
@@ -592,7 +611,6 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
|
592
611
|
/** #926: how often reply obligations are checked, the owner-reminder grace, and the default overdue window. */
|
|
593
612
|
const REPLY_OBLIGATION_SWEEP_MS = 30_000;
|
|
594
613
|
const REPLY_REMINDER_GRACE_MS = 60_000;
|
|
595
|
-
const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
|
|
596
614
|
/**
|
|
597
615
|
* The filter `list_emojis` and `list_stickers` take (#1226). Values may come from the agent CLI as strings, so a
|
|
598
616
|
* limit is read as a number and the flags accept "true". No limit means all of them — never zero.
|
|
@@ -656,32 +674,6 @@ export class FleetManager {
|
|
|
656
674
|
laneFree: name => !this.isInstanceRestarting(name) && !this.activeDurableTargets.has(name)
|
|
657
675
|
&& outbox.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending"]) === 0,
|
|
658
676
|
admit: (episode, tasks, advisory) => outbox.admitWorkingRecovery(episode, buildWorkingRecoveryPrompt(tasks, this.workingRecoveryCause, advisory), advisory),
|
|
659
|
-
summary: (group, rows) => {
|
|
660
|
-
const general = this.findGeneralInstance();
|
|
661
|
-
if (!general || !this.daemons.has(general))
|
|
662
|
-
return;
|
|
663
|
-
// Our own informational notice must not supersede General's recovery
|
|
664
|
-
// as unrelated pending work before it crosses the input boundary.
|
|
665
|
-
const generalEpisode = store.get(general);
|
|
666
|
-
if (generalEpisode?.outcome === "admitted" && generalEpisode.deliveryId) {
|
|
667
|
-
const state = outbox.get(generalEpisode.deliveryId)?.state;
|
|
668
|
-
if (!state || !["delivered", "uncertain", "failed", "cancelled"].includes(state))
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
|
|
672
|
-
&& (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
|
|
673
|
-
.map(row => `${row.instance} (${row.reason})`);
|
|
674
|
-
const posted = store.summary(group, () => outbox.admitSystemNotice(general, `working-recovery-summary:${group}`, `[system:working-recovery-summary] Observed cause: unknown interruption${this.workingRecoveryCause === "unknown_interruption" ? "" : `; prior requested action: ${this.workingRecoveryCause}`}. ${rows.length} interrupted instances; continuation queued ${rows.filter(r => r.outcome === "admitted").length}, excluded ${rows.filter(r => r.outcome === "excluded").length}, still deferred ${rows.filter(r => r.outcome === "deferred").length}. ${rows.filter(r => r.reason === "advisory_background_unknown").length ? `Advisory, background unknown: ${rows.filter(r => r.reason === "advisory_background_unknown").map(r => r.instance).join(", ")}. ` : ""}${deferredAdvisories.length ? `Deferred advisory candidates (background unknown; all other gates still apply): ${deferredAdvisories.join(", ")}. ` : ""}Details are in the Log. Deferred work remains eligible; no action or confirmation is requested.`));
|
|
675
|
-
if (posted) {
|
|
676
|
-
const counts = { interrupted: rows.length, queued: rows.filter(row => row.outcome === "admitted").length,
|
|
677
|
-
excluded: rows.filter(row => row.outcome === "excluded").length, deferred: rows.filter(row => row.outcome === "deferred").length };
|
|
678
|
-
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
679
|
-
this.logger.info({ group, ...counts, observedCause: "unknown_interruption", requestedAction }, "Interrupted work recovery summary queued for General");
|
|
680
|
-
this.eventLog?.insert(general, "working_state", { reason: "recovery_summary", group, ...counts,
|
|
681
|
-
observedCause: "unknown_interruption", requestedAction,
|
|
682
|
-
message: `Interrupted work summary queued for General: ${counts.interrupted} interrupted; ${counts.queued} queued, ${counts.excluded} excluded, ${counts.deferred} deferred.` });
|
|
683
|
-
}
|
|
684
|
-
},
|
|
685
677
|
log: (episode, outcome, reason) => {
|
|
686
678
|
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
687
679
|
this.logger.info({ name: episode.instance, episode: episode.key, outcome, reason,
|
|
@@ -913,6 +905,8 @@ export class FleetManager {
|
|
|
913
905
|
pendingNonceButtons = new Map();
|
|
914
906
|
/** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
|
|
915
907
|
pendingKiroMigrations = new Map();
|
|
908
|
+
pendingKiroReconciliations = new Map();
|
|
909
|
+
kiroMigrationRuns = new Map();
|
|
916
910
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
917
911
|
needsYou = null;
|
|
918
912
|
/** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
|
|
@@ -925,6 +919,12 @@ export class FleetManager {
|
|
|
925
919
|
* adapter emits — whatever fields its payload carries — can claim a dashboard click's authority.
|
|
926
920
|
*/
|
|
927
921
|
webPromptClicks = new WeakSet();
|
|
922
|
+
/** #2675: a dashboard click's typed answer, for the click it came with. */
|
|
923
|
+
webPromptTexts = new WeakMap();
|
|
924
|
+
/** #2675: the nonce of each instance's posted card. */
|
|
925
|
+
remoteCardPosts = new Map();
|
|
926
|
+
/** #2675: replies taken as a card's answer (`<chatId>|<messageId>` → monotonic expiry), so another bot's copy is dropped. */
|
|
927
|
+
takenCardReplies = new Map();
|
|
928
928
|
/** The web clicks consumeNonceCallback actually claimed (the others were refused or lost a race). */
|
|
929
929
|
webPromptClaims = new WeakSet();
|
|
930
930
|
// Model failover state
|
|
@@ -1195,6 +1195,7 @@ export class FleetManager {
|
|
|
1195
1195
|
needs: () => this.needsYouItems().map(i => i.instance),
|
|
1196
1196
|
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
1197
1197
|
skins: this.officeSkins,
|
|
1198
|
+
levels: () => this.officeLevelsSnapshot(),
|
|
1198
1199
|
},
|
|
1199
1200
|
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
1200
1201
|
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
@@ -1211,7 +1212,7 @@ export class FleetManager {
|
|
|
1211
1212
|
* `share` null with `count`: "Revoke all".
|
|
1212
1213
|
*/
|
|
1213
1214
|
officeShareNotice(event, who, share, count) {
|
|
1214
|
-
const office = !share ? "" : share.office === ALL_OFFICES ? t("office_share.all") : this.officeStore.list().offices.find(o => o.id === share.office)?.name ?? share.office;
|
|
1215
|
+
const office = !share ? "" : share.office === ALL_OFFICES ? t("office_share.all") : share.office === WHOLE_FLEET ? t("office_share.whole") : this.officeStore.list().offices.find(o => o.id === share.office)?.name ?? share.office;
|
|
1215
1216
|
const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
|
|
1216
1217
|
const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
|
|
1217
1218
|
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
@@ -1235,6 +1236,98 @@ export class FleetManager {
|
|
|
1235
1236
|
changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
|
|
1236
1237
|
}));
|
|
1237
1238
|
}
|
|
1239
|
+
officePackToolsInstance;
|
|
1240
|
+
/** #2692: the agents' skin-pack tools — their log, the admins' approval mode and the pending changes (memory only). */
|
|
1241
|
+
get officePackTools() {
|
|
1242
|
+
return (this.officePackToolsInstance ??= new OfficePackTools({
|
|
1243
|
+
skins: this.officeSkins, offices: this.officeStore,
|
|
1244
|
+
allowedAgents: () => this.officePackAgents(),
|
|
1245
|
+
notice: (text, pending) => { void this.officePackNotice(text, pending); },
|
|
1246
|
+
settled: (id, text) => this.settleOfficePackPrompt(id, text),
|
|
1247
|
+
changed: what => {
|
|
1248
|
+
if (what.skins !== undefined)
|
|
1249
|
+
this.emitSseEvent("office_skins", { rev: what.skins });
|
|
1250
|
+
if (what.offices !== undefined)
|
|
1251
|
+
this.emitSseEvent("offices", { rev: what.offices });
|
|
1252
|
+
if (what.changes)
|
|
1253
|
+
this.emitSseEvent("office_pack_changes", {});
|
|
1254
|
+
},
|
|
1255
|
+
logger: this.logger,
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
/** #2692: the instances whose `office_packs` feature is on (their own switch, else the defaults'): the Office page lists them. */
|
|
1259
|
+
officePackAgents() {
|
|
1260
|
+
const names = new Set([...Object.keys(this.fleetConfig?.instances ?? {}), ...(this.classicChannels?.getAll() ?? []).map(ch => ch.instanceName)]);
|
|
1261
|
+
return [...names].filter(name => dashboardRefusalIn(this.fleetConfig, name, "office_pack_upload") === null);
|
|
1262
|
+
}
|
|
1263
|
+
/** #2692: the nonce of each pending pack change's prompt in General (its buttons are retired when it is decided). */
|
|
1264
|
+
officePackPrompts = new Map();
|
|
1265
|
+
/**
|
|
1266
|
+
* #2692: tell the fleet's admins of an agent's pack change — one short message in General (notifyFleetError, never a
|
|
1267
|
+
* sign-in notice). A pending one comes with Approve / Reject (a fleet admin's, as a kiro move); where no General can
|
|
1268
|
+
* carry buttons it is the plain message, and the Office page decides it.
|
|
1269
|
+
*/
|
|
1270
|
+
async officePackNotice(text, pending) {
|
|
1271
|
+
if (!pending) {
|
|
1272
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const general = this.findGeneralInstance();
|
|
1276
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
1277
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
1278
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
1279
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
1280
|
+
if (general && adapterId && adapter && chatId) {
|
|
1281
|
+
try {
|
|
1282
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
1283
|
+
prefix: OFFICE_PACK_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: pending.instance, adapter, adapterId, chatId,
|
|
1284
|
+
...(topic ? { threadId: topic } : {}), message: text,
|
|
1285
|
+
choices: [{ action: "approve", label: t("office_packs.approve_button") }, { action: "reject", label: t("office_packs.reject_button") }],
|
|
1286
|
+
expiredText: t("office_packs.expired", ""), timeoutMs: Math.max(1_000, pending.expires_at - Date.now()),
|
|
1287
|
+
});
|
|
1288
|
+
if (this.officePackTools.pendingViews().some(v => v.id === pending.id))
|
|
1289
|
+
this.officePackPrompts.set(pending.id, nonce);
|
|
1290
|
+
else
|
|
1291
|
+
this.retireOfficePackNonce(nonce, t("office_packs.gone"));
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
catch (err) {
|
|
1295
|
+
this.logger.info({ err: err.message }, "Office pack approval prompt unavailable; the plain notice and the Office page remain");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1299
|
+
}
|
|
1300
|
+
retireOfficePackNonce(nonce, text) {
|
|
1301
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
1302
|
+
if (!entry || entry.prefix !== OFFICE_PACK_CALLBACK_PREFIX)
|
|
1303
|
+
return;
|
|
1304
|
+
this.pendingNonceButtons.delete(nonce);
|
|
1305
|
+
clearTimeout(entry.timer);
|
|
1306
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
1307
|
+
}
|
|
1308
|
+
/** #2692: a pending change was decided (here or on the page) or expired: its prompt says so and loses its buttons. */
|
|
1309
|
+
settleOfficePackPrompt(id, text) {
|
|
1310
|
+
const nonce = this.officePackPrompts.get(id);
|
|
1311
|
+
this.officePackPrompts.delete(id);
|
|
1312
|
+
if (nonce)
|
|
1313
|
+
this.retireOfficePackNonce(nonce, text);
|
|
1314
|
+
}
|
|
1315
|
+
/** #2692: Approve / Reject on a pending pack change. Any fleet admin (the default nonce authorization), in General. */
|
|
1316
|
+
async handleOfficePackCallback(data, adapterId, adapter) {
|
|
1317
|
+
const claimed = this.consumeNonceCallback(OFFICE_PACK_CALLBACK_PREFIX, /^office-pack:([0-9a-f]{32}):(approve|reject)$/, data, adapterId, adapter);
|
|
1318
|
+
if (claimed === null)
|
|
1319
|
+
return false;
|
|
1320
|
+
if (claimed === "consumed")
|
|
1321
|
+
return true;
|
|
1322
|
+
const { entry, action } = claimed;
|
|
1323
|
+
const nonce = data.callbackData.slice(OFFICE_PACK_CALLBACK_PREFIX.length, OFFICE_PACK_CALLBACK_PREFIX.length + 32);
|
|
1324
|
+
const id = [...this.officePackPrompts].find(([, n]) => n === nonce)?.[0];
|
|
1325
|
+
if (id)
|
|
1326
|
+
this.officePackPrompts.delete(id);
|
|
1327
|
+
const decided = id ? this.officePackTools.decide(id, action, t("settings_confirm.admin_label", data.userId ?? "")) : null;
|
|
1328
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, decided?.text ?? t("office_packs.gone"));
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1238
1331
|
/**
|
|
1239
1332
|
* #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
|
|
1240
1333
|
* frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
|
|
@@ -1245,7 +1338,10 @@ export class FleetManager {
|
|
|
1245
1338
|
needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
|
|
1246
1339
|
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
1247
1340
|
let profiles = null;
|
|
1341
|
+
// #2661: the levels as last worked out (never waits; none while off or not worked out yet).
|
|
1342
|
+
const levels = this.officeLevelsSnapshot();
|
|
1248
1343
|
return {
|
|
1344
|
+
level: name => (levels && Object.prototype.hasOwnProperty.call(levels, name) ? levels[name].level : undefined),
|
|
1249
1345
|
stateOf: name => {
|
|
1250
1346
|
if (!this.isKnownInstance(name))
|
|
1251
1347
|
return "missing";
|
|
@@ -1797,12 +1893,96 @@ export class FleetManager {
|
|
|
1797
1893
|
this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
|
|
1798
1894
|
}
|
|
1799
1895
|
kiroMigrateDeps() {
|
|
1896
|
+
let token;
|
|
1897
|
+
const current = (name) => {
|
|
1898
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1899
|
+
return !!token && run?.token === token && !this.shuttingDown
|
|
1900
|
+
&& (this.explicitStopGeneration.get(name) ?? 0) === run.stopGeneration;
|
|
1901
|
+
};
|
|
1800
1902
|
return {
|
|
1801
|
-
agendHome: getAgendHome(),
|
|
1802
|
-
fleetConfig: () => this.fleetConfig,
|
|
1803
|
-
saveFleetConfig: () => this.saveFleetConfig(),
|
|
1903
|
+
agendHome: getAgendHome(), fleetConfig: () => this.fleetConfig, saveFleetConfig: () => this.saveFleetConfig(),
|
|
1804
1904
|
status: name => this.getInstanceStatus(name),
|
|
1805
|
-
|
|
1905
|
+
reserve: name => {
|
|
1906
|
+
if (this.kiroMigrationRuns.has(name) || this.restartsInFlight.has(name) || this.stopsInFlight.has(name))
|
|
1907
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
1908
|
+
token = Symbol(name);
|
|
1909
|
+
this.kiroMigrationRuns.set(name, { token, stopGeneration: this.explicitStopGeneration.get(name) ?? 0 });
|
|
1910
|
+
return { current: () => current(name), release: () => {
|
|
1911
|
+
if (this.kiroMigrationRuns.get(name)?.token === token)
|
|
1912
|
+
this.kiroMigrationRuns.delete(name);
|
|
1913
|
+
this.scheduleDeliveryOutboxPump();
|
|
1914
|
+
} };
|
|
1915
|
+
},
|
|
1916
|
+
preflight: async (name, plan) => {
|
|
1917
|
+
const compat = getCachedKiroCliCompatibility(resolveBinary("kiro-cli"));
|
|
1918
|
+
if (!supportsVerifiedKiroMigration(compat))
|
|
1919
|
+
throw new Error(t("kiro_migrate.version_required"));
|
|
1920
|
+
const daemon = this.daemons.get(name);
|
|
1921
|
+
if (this.getInstanceStatus(name) === "running" && (!daemon || !await daemon.kiroMigrationIdle()))
|
|
1922
|
+
throw new Error(t("kiro_migrate.idle_required"));
|
|
1923
|
+
if (!current(name) || daemon && this.daemons.get(name) !== daemon)
|
|
1924
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1925
|
+
const inst = this.fleetConfig?.instances[name];
|
|
1926
|
+
const model = daemon?.getModelOverride() ?? inst?.model ?? (plan.classicId ? readKiroV1SavedModel({ id: plan.classicId,
|
|
1927
|
+
workingDirectory: plan.workingDirectory, dbPath: kiroV1DbPath(plan.credentialProfile) }) : undefined);
|
|
1928
|
+
if (!model && plan.classicId)
|
|
1929
|
+
throw new Error(t("kiro_migrate.model_unknown"));
|
|
1930
|
+
return model ?? "";
|
|
1931
|
+
},
|
|
1932
|
+
restart: async (name) => {
|
|
1933
|
+
if (!current(name))
|
|
1934
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1935
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1936
|
+
const expected = run.stopGeneration + 1;
|
|
1937
|
+
// This restart owns exactly one stop. A person's concurrent stop must not be hidden by the workflow.
|
|
1938
|
+
try {
|
|
1939
|
+
await this.restartSingleInstance(name, { explicit: true, kiroMigration: token });
|
|
1940
|
+
}
|
|
1941
|
+
finally {
|
|
1942
|
+
// Even a failed replacement owns its completed stop; retain authority to clean up/pause it.
|
|
1943
|
+
if (this.kiroMigrationRuns.get(name) === run && this.explicitStopGeneration.get(name) === expected)
|
|
1944
|
+
run.stopGeneration = expected;
|
|
1945
|
+
}
|
|
1946
|
+
if (this.explicitStopGeneration.get(name) !== expected)
|
|
1947
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1948
|
+
},
|
|
1949
|
+
verify: async (name, id, model, archived) => {
|
|
1950
|
+
const daemon = this.daemons.get(name), deadline = performance.now() + 60_000;
|
|
1951
|
+
if (!daemon)
|
|
1952
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1953
|
+
while (performance.now() < deadline) {
|
|
1954
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1955
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1956
|
+
const observed = await daemon.kiroMigrationObservation();
|
|
1957
|
+
if (observed) {
|
|
1958
|
+
if (observed.sessionId !== id || observed.model !== model || resolvePath(observed.transcript) !== resolvePath(kiroV2SessionsDir(), id + ".jsonl"))
|
|
1959
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1960
|
+
const inst = this.fleetConfig.instances[name];
|
|
1961
|
+
const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), migration: {
|
|
1962
|
+
agendHome: getAgendHome(), instance: name, workingDirectory: inst.working_directory, credentialProfile: instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli"),
|
|
1963
|
+
sessionId: id, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli")),
|
|
1964
|
+
model, archived, outboxDbPath: join(this.dataDir, "delivery-outbox.db"), knownInstances: Object.keys(this.fleetConfig.instances),
|
|
1965
|
+
} });
|
|
1966
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1967
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1968
|
+
if (reply.kind !== "ok" || !reply.verification)
|
|
1969
|
+
throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_migrate.verification_failed"));
|
|
1970
|
+
return reply.verification;
|
|
1971
|
+
}
|
|
1972
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1973
|
+
}
|
|
1974
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1975
|
+
},
|
|
1976
|
+
stop: async (name) => { try {
|
|
1977
|
+
await this.stopInstance(name);
|
|
1978
|
+
}
|
|
1979
|
+
finally {
|
|
1980
|
+
writePausedMarker(this.getInstanceDir(name), Date.now(), "error");
|
|
1981
|
+
} },
|
|
1982
|
+
readClassicStore: async (workingDirectory, credentialProfile) => {
|
|
1983
|
+
const dbPath = kiroV1DbPath(credentialProfile);
|
|
1984
|
+
return listKiroV1Sessions(workingDirectory, dbPath, await readKiroClassicLinks(workingDirectory, dbPath));
|
|
1985
|
+
},
|
|
1806
1986
|
};
|
|
1807
1987
|
}
|
|
1808
1988
|
/**
|
|
@@ -1810,7 +1990,7 @@ export class FleetManager {
|
|
|
1810
1990
|
* here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
|
|
1811
1991
|
*/
|
|
1812
1992
|
async requestKiroMigration(name, startFresh) {
|
|
1813
|
-
const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
1993
|
+
const judged = await planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
1814
1994
|
if (!judged.ok)
|
|
1815
1995
|
return judged;
|
|
1816
1996
|
const general = this.findGeneralInstance();
|
|
@@ -1860,8 +2040,77 @@ export class FleetManager {
|
|
|
1860
2040
|
const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
|
|
1861
2041
|
this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
|
|
1862
2042
|
this.notifyFleetError(outcome.text);
|
|
1863
|
-
|
|
1864
|
-
|
|
2043
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
2044
|
+
return true;
|
|
2045
|
+
}
|
|
2046
|
+
kiroReconcileDeps() {
|
|
2047
|
+
return { ...this.kiroMigrateDeps(), store: async (input, plan, actor, rollbackReceipt) => {
|
|
2048
|
+
const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: input.sessionsDir, reconciliation: { input, plan, actor, rollbackReceipt } });
|
|
2049
|
+
if (reply.kind !== "ok" || !reply.reconciliation)
|
|
2050
|
+
throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_reconcile.changed"));
|
|
2051
|
+
return reply.reconciliation;
|
|
2052
|
+
} };
|
|
2053
|
+
}
|
|
2054
|
+
async requestKiroReconciliation(name, keep, archive, rollbackReceipt) {
|
|
2055
|
+
try {
|
|
2056
|
+
if (!this.eventLog)
|
|
2057
|
+
return { ok: false, error: t("kiro_reconcile.audit_failed") };
|
|
2058
|
+
const plan = await planKiroReconciliation(this.kiroReconcileDeps(), name, keep, archive, rollbackReceipt);
|
|
2059
|
+
const general = this.findGeneralInstance(), adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
2060
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined, chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
2061
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
2062
|
+
if (!general || !adapterId || !adapter || !chatId)
|
|
2063
|
+
return { ok: false, error: t("kiro_migrate.no_general") };
|
|
2064
|
+
const nonce = await this.postNonceButtonPromptOrThrow({ prefix: KIRO_RECONCILE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name,
|
|
2065
|
+
adapter, adapterId, chatId, ...(topic ? { threadId: topic } : {}), message: kiroReconciliationPrompt(plan),
|
|
2066
|
+
choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
|
|
2067
|
+
expiredText: t("kiro_migrate.expired", name), timeoutMs: KIRO_MIGRATE_TIMEOUT_MS });
|
|
2068
|
+
this.pendingKiroReconciliations.set(nonce, plan);
|
|
2069
|
+
setTimeout(() => this.pendingKiroReconciliations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000).unref?.();
|
|
2070
|
+
this.eventLog?.insert(name, "kiro_reconcile", { phase: "requested", keep, archive, rollbackReceipt, message: kiroReconciliationPrompt(plan) });
|
|
2071
|
+
return { ok: true, text: t("kiro_migrate.requested") };
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
return { ok: false, error: error instanceof SyntaxError ? "Malformed reconciliation data" : error.message };
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
async handleKiroReconcileCallback(data, adapterId, adapter) {
|
|
2078
|
+
const claimed = this.consumeNonceCallback(KIRO_RECONCILE_CALLBACK_PREFIX, /^kiro-reconcile:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
|
|
2079
|
+
if (claimed === null)
|
|
2080
|
+
return false;
|
|
2081
|
+
if (claimed === "consumed")
|
|
2082
|
+
return true;
|
|
2083
|
+
const { entry, action } = claimed;
|
|
2084
|
+
const nonce = data.callbackData.slice(KIRO_RECONCILE_CALLBACK_PREFIX.length, KIRO_RECONCILE_CALLBACK_PREFIX.length + 32);
|
|
2085
|
+
const plan = this.pendingKiroReconciliations.get(nonce);
|
|
2086
|
+
this.pendingKiroReconciliations.delete(nonce);
|
|
2087
|
+
if (action === "cancel" || !plan) {
|
|
2088
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
|
|
2089
|
+
this.eventLog?.insert(entry.instanceName, "kiro_reconcile", { phase: "cancelled_or_expired", userId: data.userId });
|
|
2090
|
+
return true;
|
|
2091
|
+
}
|
|
2092
|
+
let outcome;
|
|
2093
|
+
try {
|
|
2094
|
+
if (!this.eventLog)
|
|
2095
|
+
throw new Error(t("kiro_reconcile.audit_failed"));
|
|
2096
|
+
this.eventLog.insert(plan.name, "kiro_reconcile", { phase: "confirmed", userId: data.userId, keep: plan.input.sessionId, archive: plan.input.archiveId });
|
|
2097
|
+
}
|
|
2098
|
+
catch {
|
|
2099
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.audit_failed"));
|
|
2100
|
+
this.notifyFleetError(t("kiro_reconcile.audit_failed"));
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.applying", plan.name));
|
|
2104
|
+
outcome = await applyKiroReconciliation(this.kiroReconcileDeps(), plan, `${adapterId}:${data.userId}`);
|
|
2105
|
+
try {
|
|
2106
|
+
this.eventLog.insert(plan.name, "kiro_reconcile", { phase: outcome.ok ? "completed" : "failed", userId: data.userId, receipt: outcome.receipt, message: outcome.text });
|
|
2107
|
+
}
|
|
2108
|
+
catch {
|
|
2109
|
+
this.logger.error({ name: plan.name, receipt: outcome.receipt }, "Kiro reconciliation final Log entry failed; confirmed intent remains in the audit and archive");
|
|
2110
|
+
}
|
|
2111
|
+
this.logger.info({ name: plan.name, ok: outcome.ok, receipt: outcome.receipt }, "Kiro explicit reconciliation");
|
|
2112
|
+
this.notifyFleetError(outcome.text);
|
|
2113
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
1865
2114
|
return true;
|
|
1866
2115
|
}
|
|
1867
2116
|
async handleSettingsChangeCallback(data, adapterId, adapter) {
|
|
@@ -2327,27 +2576,8 @@ export class FleetManager {
|
|
|
2327
2576
|
const daemon = this.daemons.get(name);
|
|
2328
2577
|
if (daemon?.bootId === daemonBootId)
|
|
2329
2578
|
this.listenForWorkingState(name, daemon);
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
return;
|
|
2333
|
-
for (const prior of outbox.getUnansweredAccepted(name, daemonBootId)) {
|
|
2334
|
-
try {
|
|
2335
|
-
outbox.admitPostRestartOutcomeNotice(prior, daemonBootId);
|
|
2336
|
-
}
|
|
2337
|
-
catch (err) {
|
|
2338
|
-
this.logger.error({ err, source: name, operationId: prior.operationId }, "Could not persist post-restart delivery outcome notice");
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
// #926: a restart kills the turn that was working on a request, and a
|
|
2342
|
-
// resumed CLI does not pick it back up. Remind the owner once per restart.
|
|
2343
|
-
try {
|
|
2344
|
-
const now = new Date();
|
|
2345
|
-
this.expireReplyObligations(now);
|
|
2346
|
-
outbox.remindReplyObligations(name, { reason: "restart", graceMs: 0, now, since: now, restartId: daemonBootId });
|
|
2347
|
-
}
|
|
2348
|
-
catch (err) {
|
|
2349
|
-
this.logger.error({ err, owner: name }, "Could not persist the post-restart reply reminder");
|
|
2350
|
-
}
|
|
2579
|
+
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
2580
|
+
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
2351
2581
|
this.scheduleDeliveryOutboxPump();
|
|
2352
2582
|
}
|
|
2353
2583
|
workingStateListeners;
|
|
@@ -2431,9 +2661,8 @@ export class FleetManager {
|
|
|
2431
2661
|
}
|
|
2432
2662
|
}
|
|
2433
2663
|
/**
|
|
2434
|
-
* #926:
|
|
2435
|
-
*
|
|
2436
|
-
* notice when the owner is not working and nothing came back in time.
|
|
2664
|
+
* #926: idle owners with an unanswered request get one reminder (after the grace). #2802: the only reply notice —
|
|
2665
|
+
* requesters are no longer told about overdue or expired asks (they can check delivery_status / describe_instance).
|
|
2437
2666
|
*/
|
|
2438
2667
|
sweepReplyObligations(now = new Date()) {
|
|
2439
2668
|
const outbox = this.deliveryOutbox;
|
|
@@ -2444,10 +2673,8 @@ export class FleetManager {
|
|
|
2444
2673
|
this.expireReplyObligations(now);
|
|
2445
2674
|
for (const name of this.daemons.keys()) {
|
|
2446
2675
|
if (stateOf(name) === "idle")
|
|
2447
|
-
outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS
|
|
2676
|
+
outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS });
|
|
2448
2677
|
}
|
|
2449
|
-
const minutes = this.fleetConfig?.defaults?.reply_overdue_minutes ?? DEFAULT_REPLY_OVERDUE_MINUTES;
|
|
2450
|
-
outbox.notifyOverdueReplyObligations({ now, overdueMs: minutes * 60_000, ownerIdle: name => stateOf(name) !== "working" });
|
|
2451
2678
|
}
|
|
2452
2679
|
catch (err) {
|
|
2453
2680
|
this.logger.error({ err }, "Reply-obligation sweep failed");
|
|
@@ -5441,7 +5668,10 @@ export class FleetManager {
|
|
|
5441
5668
|
*/
|
|
5442
5669
|
resumePaused = false,
|
|
5443
5670
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
5444
|
-
transition, execution) {
|
|
5671
|
+
transition, execution, kiroMigration) {
|
|
5672
|
+
const migration = this.kiroMigrationRuns?.get(name);
|
|
5673
|
+
if (migration && migration.token !== kiroMigration)
|
|
5674
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
5445
5675
|
execution?.assert();
|
|
5446
5676
|
if (this.sessionOomStopped(name)) {
|
|
5447
5677
|
if (resumePaused)
|
|
@@ -5937,6 +6167,11 @@ export class FleetManager {
|
|
|
5937
6167
|
stopOnUnsupportedCli(name, err, what) {
|
|
5938
6168
|
if (!(err instanceof UnsupportedCliError))
|
|
5939
6169
|
return false;
|
|
6170
|
+
if (err instanceof ResumeBlockedError) {
|
|
6171
|
+
this.logger.error({ name, reason: err.message }, `Not starting ${what}: retained session needs store repair`);
|
|
6172
|
+
void this.notifyFleetError(t("fleet.resume_blocked", name, err.message, err.remedy));
|
|
6173
|
+
return true;
|
|
6174
|
+
}
|
|
5940
6175
|
this.logger.error({ name, reason: err.message }, `Not starting ${what}: the installed CLI cannot run it as configured`);
|
|
5941
6176
|
this.queueUnsupportedCliNotice(name, err.message);
|
|
5942
6177
|
this.queueKiroIncompatNotice(name, err.message);
|
|
@@ -6136,6 +6371,9 @@ export class FleetManager {
|
|
|
6136
6371
|
}
|
|
6137
6372
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
6138
6373
|
async restartSingleInstance(name, opts) {
|
|
6374
|
+
const migration = this.kiroMigrationRuns?.get(name);
|
|
6375
|
+
if (migration && migration.token !== opts?.kiroMigration)
|
|
6376
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
6139
6377
|
if (opts?.recoveryCurrent && !opts.recoveryCurrent())
|
|
6140
6378
|
throw new Error(t("exit.expired", name));
|
|
6141
6379
|
const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
|
|
@@ -6196,7 +6434,7 @@ export class FleetManager {
|
|
|
6196
6434
|
return run;
|
|
6197
6435
|
}
|
|
6198
6436
|
isInstanceRestarting(name) {
|
|
6199
|
-
return this.restartsInFlight.has(name);
|
|
6437
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
6200
6438
|
}
|
|
6201
6439
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
6202
6440
|
restartInFlight(name) {
|
|
@@ -6301,7 +6539,8 @@ export class FleetManager {
|
|
|
6301
6539
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
|
|
6302
6540
|
});
|
|
6303
6541
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
6304
|
-
await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition)
|
|
6542
|
+
await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
|
|
6543
|
+
: this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
|
|
6305
6544
|
this.stormWindow.markRecovered(name);
|
|
6306
6545
|
return;
|
|
6307
6546
|
}
|
|
@@ -7057,9 +7296,22 @@ export class FleetManager {
|
|
|
7057
7296
|
...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
|
|
7058
7297
|
}).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
|
|
7059
7298
|
// #2348: `explicit` — grants are the scope (default deny); false: the derived scope of #1920, until grants are saved.
|
|
7060
|
-
|
|
7299
|
+
// #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
|
|
7300
|
+
// with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
|
|
7301
|
+
const profiles = profileIdentities(this.dataDir);
|
|
7302
|
+
const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
|
|
7303
|
+
.map((r) => {
|
|
7304
|
+
const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
|
|
7305
|
+
return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
|
|
7306
|
+
})
|
|
7307
|
+
.sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
|
|
7308
|
+
return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
|
|
7061
7309
|
audit: this.guestAudit() };
|
|
7062
7310
|
}
|
|
7311
|
+
/** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
|
|
7312
|
+
classicRoomNames() {
|
|
7313
|
+
return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
|
|
7314
|
+
}
|
|
7063
7315
|
/**
|
|
7064
7316
|
* #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
|
|
7065
7317
|
* (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
|
|
@@ -7868,7 +8120,7 @@ export class FleetManager {
|
|
|
7868
8120
|
setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
|
|
7869
8121
|
if (this.watchdogTimer)
|
|
7870
8122
|
clearInterval(this.watchdogTimer);
|
|
7871
|
-
this.watchdogTimer =
|
|
8123
|
+
this.watchdogTimer = startSdWatchdog();
|
|
7872
8124
|
this.eventLoopWatch?.stop();
|
|
7873
8125
|
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
7874
8126
|
try {
|
|
@@ -8082,8 +8334,8 @@ export class FleetManager {
|
|
|
8082
8334
|
const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
8083
8335
|
this.warnUnboundGeneralChannelIds(fleet);
|
|
8084
8336
|
const primaryAdapterId = channelConfigs[0] ? (channelConfigs[0].id ?? channelConfigs[0].type) : undefined;
|
|
8337
|
+
let generalsCreated = adoptGeneralByName(fleet, this.logger, dir => insideManagedWorkspaces(dir) || resolvePath(dir) === join(getAgendHome(), GENERAL_NAME));
|
|
8085
8338
|
const generalInstances = Object.entries(fleet.instances).filter(([, inst]) => inst.general_topic === true);
|
|
8086
|
-
let generalsCreated = false;
|
|
8087
8339
|
// Collect unbound generals (no channel_id set) for auto-assignment
|
|
8088
8340
|
const unboundGenerals = generalInstances.filter(([, inst]) => !inst.channel_id);
|
|
8089
8341
|
// Track which adapters still need a general
|
|
@@ -8143,9 +8395,16 @@ export class FleetManager {
|
|
|
8143
8395
|
}
|
|
8144
8396
|
// Phase 3: Create new generals for any remaining adapters
|
|
8145
8397
|
for (const need of needsGeneral) {
|
|
8146
|
-
|
|
8147
|
-
if (fleet.instances[name])
|
|
8398
|
+
let name = channelConfigs.length > 1 ? `general-${need.adapterId}` : GENERAL_NAME;
|
|
8399
|
+
if (fleet.instances[name]?.general_topic)
|
|
8148
8400
|
continue;
|
|
8401
|
+
if (fleet.instances[name]) {
|
|
8402
|
+
// M-2069: the name is an agent's that is not the General (adoptGeneralByName left it: it works in a project
|
|
8403
|
+
// of the user's) — the fleet still gets its General, under a free name, never none.
|
|
8404
|
+
const taken = name;
|
|
8405
|
+
name = freeGeneralName(fleet.instances, taken);
|
|
8406
|
+
this.logger.warn({ taken, name }, "An agent is named like the General but is not one — creating the General under a free name; rename that agent to clear this");
|
|
8407
|
+
}
|
|
8149
8408
|
this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
|
|
8150
8409
|
const generalDir = join(getAgendHome(), name);
|
|
8151
8410
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
@@ -8425,8 +8684,6 @@ export class FleetManager {
|
|
|
8425
8684
|
}
|
|
8426
8685
|
await this.sendFleetStartCompletionFallback(String(fleet.channel.group_id), text, generalThreadId != null ? String(generalThreadId) : undefined);
|
|
8427
8686
|
}
|
|
8428
|
-
// After "fleet ready", so the notice is not the first thing people see of a restart.
|
|
8429
|
-
void this.announceWebChatOnce(agendVersion);
|
|
8430
8687
|
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
8431
8688
|
this.startPublicLinkAutoOpen();
|
|
8432
8689
|
}
|
|
@@ -9935,6 +10192,10 @@ export class FleetManager {
|
|
|
9935
10192
|
}))
|
|
9936
10193
|
return;
|
|
9937
10194
|
}
|
|
10195
|
+
// #2675: a reply to a posted question card is its typed answer (judged by who may talk to that instance), not a
|
|
10196
|
+
// message for the instance — which could not take one while its card is up anyway.
|
|
10197
|
+
if (await this.takeRemoteCardReply(msg))
|
|
10198
|
+
return;
|
|
9938
10199
|
// Access control — classic channels are open to all, others require an allowed
|
|
9939
10200
|
// user. This runs before the dedup claim, and is judged by the adapter that
|
|
9940
10201
|
// owns the topic rather than the one that happened to receive the message:
|
|
@@ -10557,34 +10818,6 @@ export class FleetManager {
|
|
|
10557
10818
|
* their fleet is organised. Both stay theirs — this only makes sure they are
|
|
10558
10819
|
* not discovered by an agent failing at three in the morning.
|
|
10559
10820
|
*/
|
|
10560
|
-
/**
|
|
10561
|
-
* #1366: the first time a fleet runs 2.2 or later, tell each chat platform's General — once — that its agents can
|
|
10562
|
-
* now be talked to from a browser. Claimed in upgrade-notices.json before the send, released if the send fails.
|
|
10563
|
-
* Platforms with nowhere to post fleet notices (no group, or a Discord fleet with no General channel) are skipped
|
|
10564
|
-
* and not recorded, so they are told once they have one.
|
|
10565
|
-
*/
|
|
10566
|
-
async announceWebChatOnce(version) {
|
|
10567
|
-
if (!hasWebChat(version))
|
|
10568
|
-
return;
|
|
10569
|
-
const path = upgradeNoticesPath(this.dataDir);
|
|
10570
|
-
for (const [adapterId, world] of this.worlds) {
|
|
10571
|
-
if (this.shuttingDown)
|
|
10572
|
-
return;
|
|
10573
|
-
const target = this.fleetNoticeTarget(adapterId);
|
|
10574
|
-
if (!target)
|
|
10575
|
-
continue;
|
|
10576
|
-
if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
|
|
10577
|
-
continue;
|
|
10578
|
-
try {
|
|
10579
|
-
await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
|
|
10580
|
-
this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
|
|
10581
|
-
}
|
|
10582
|
-
catch (err) {
|
|
10583
|
-
const released = releaseNotice(path, WEB_CHAT_NOTICE, adapterId);
|
|
10584
|
-
this.logger.warn({ err, adapterId, released }, "Could not announce web chat in General — will try at the next start");
|
|
10585
|
-
}
|
|
10586
|
-
}
|
|
10587
|
-
}
|
|
10588
10821
|
announceToolPermissionsChange() {
|
|
10589
10822
|
try {
|
|
10590
10823
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 19).replace("T", " ");
|
|
@@ -11326,7 +11559,7 @@ export class FleetManager {
|
|
|
11326
11559
|
if (!ipc || !fleetRequestId)
|
|
11327
11560
|
return;
|
|
11328
11561
|
const type = String(msg.type);
|
|
11329
|
-
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
|
|
11562
|
+
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" || OFFICE_PACK_IPC.has(type) ? "fleet_web_theme_response"
|
|
11330
11563
|
: type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
|
|
11331
11564
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
11332
11565
|
: type === "fleet_task" ? "fleet_task_response"
|
|
@@ -11381,7 +11614,10 @@ export class FleetManager {
|
|
|
11381
11614
|
|| type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
|
|
11382
11615
|
|| type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
|
|
11383
11616
|
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11384
|
-
|| type === "fleet_office_view" || type === "fleet_office_set"
|
|
11617
|
+
|| type === "fleet_office_view" || type === "fleet_office_set"
|
|
11618
|
+
// #2692: the skin packs (OFFICE_PACK_IPC), answered as web theme tools.
|
|
11619
|
+
|| type === "fleet_office_pack_check" || type === "fleet_office_pack_upload" || type === "fleet_office_pack_delete"
|
|
11620
|
+
|| type === "fleet_office_pack_restore" || type === "fleet_office_pack_list" || type === "fleet_office_set_style") {
|
|
11385
11621
|
this.handleWebTheme(name, msg);
|
|
11386
11622
|
return;
|
|
11387
11623
|
}
|
|
@@ -11403,7 +11639,8 @@ export class FleetManager {
|
|
|
11403
11639
|
? this.appearanceTools.handle(instanceName, tool, args)
|
|
11404
11640
|
: tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
|
|
11405
11641
|
: tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
|
|
11406
|
-
: this.
|
|
11642
|
+
: OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
|
|
11643
|
+
: this.webAppearance.handleTool(instanceName, tool, args);
|
|
11407
11644
|
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
11408
11645
|
}
|
|
11409
11646
|
catch (err) {
|
|
@@ -11841,6 +12078,8 @@ export class FleetManager {
|
|
|
11841
12078
|
return this.agentMods.handle(instance, tool, args);
|
|
11842
12079
|
if (tool === "office_view" || tool === "office_set")
|
|
11843
12080
|
return this.officeActs.handle(instance, tool, args); // #2578
|
|
12081
|
+
if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
|
|
12082
|
+
return this.officePackTools.handle(instance, tool, args); // #2692
|
|
11844
12083
|
return this.webAppearance.handleTool(instance, tool, args);
|
|
11845
12084
|
}
|
|
11846
12085
|
async handlePreviewEmojisHttp(instance, args) {
|
|
@@ -12279,12 +12518,21 @@ export class FleetManager {
|
|
|
12279
12518
|
normalizeOfficeStorage(this.fleetConfig);
|
|
12280
12519
|
explicitPatches = explicitPatches.flatMap(patch => {
|
|
12281
12520
|
const path = patch.path;
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12521
|
+
// #2601 (and #2692's office_packs): a separately kept feature is written under its own key, never in `dashboard`.
|
|
12522
|
+
const separate = path.at(-2) === "dashboard" ? SEPARATE_FEATURE_KEYS[path.at(-1)] : undefined;
|
|
12523
|
+
if (separate && !patch.remove) {
|
|
12524
|
+
return [{ ...patch, path: [...path.slice(0, -2), separate] }, { path, remove: true }];
|
|
12525
|
+
}
|
|
12526
|
+
if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.keys(SEPARATE_FEATURE_KEYS).some(f => Object.hasOwn(patch.value, f))) {
|
|
12527
|
+
const dashboard = { ...patch.value }, moved = [];
|
|
12528
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12529
|
+
if (!Object.hasOwn(dashboard, feature))
|
|
12530
|
+
continue;
|
|
12531
|
+
const value = dashboard[feature];
|
|
12532
|
+
delete dashboard[feature];
|
|
12533
|
+
moved.push({ path: [...path.slice(0, -1), key], value, remove: value === null });
|
|
12534
|
+
}
|
|
12535
|
+
return [{ ...patch, value: dashboard }, ...moved];
|
|
12288
12536
|
}
|
|
12289
12537
|
// #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
|
|
12290
12538
|
if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
|
|
@@ -12398,11 +12646,11 @@ export class FleetManager {
|
|
|
12398
12646
|
const previous = daemon.getConfigSnapshot?.();
|
|
12399
12647
|
if (!next || !previous)
|
|
12400
12648
|
continue;
|
|
12401
|
-
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
|
|
12649
|
+
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access && previous.office_pack_access === next.office_pack_access)
|
|
12402
12650
|
continue;
|
|
12403
12651
|
// Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
|
|
12404
12652
|
// detects that they require a restart. MCP menus also watch the raw file; this updates the next spawn's env.
|
|
12405
|
-
daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null });
|
|
12653
|
+
daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null, office_pack_access: next.office_pack_access ?? null });
|
|
12406
12654
|
if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
|
|
12407
12655
|
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
12408
12656
|
}
|
|
@@ -12436,7 +12684,7 @@ export class FleetManager {
|
|
|
12436
12684
|
logConfigWarningsAtStartup() {
|
|
12437
12685
|
let warnings = [];
|
|
12438
12686
|
try {
|
|
12439
|
-
warnings = validateFleetConfig(this.rawFleetConfig).warnings;
|
|
12687
|
+
warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
|
|
12440
12688
|
}
|
|
12441
12689
|
catch (err) {
|
|
12442
12690
|
this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
|
|
@@ -12467,11 +12715,13 @@ export class FleetManager {
|
|
|
12467
12715
|
const patches = [];
|
|
12468
12716
|
const raw = this.rawFleetConfig;
|
|
12469
12717
|
for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12718
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12719
|
+
if (!item?.dashboard || !Object.hasOwn(item.dashboard, feature))
|
|
12720
|
+
continue;
|
|
12721
|
+
patches.push({ path: [...path, "dashboard", feature], remove: true });
|
|
12722
|
+
if (!Object.hasOwn(item, key) && typeof item.dashboard[feature] === "boolean")
|
|
12723
|
+
patches.push({ path: [...path, key], value: item.dashboard[feature] });
|
|
12724
|
+
}
|
|
12475
12725
|
}
|
|
12476
12726
|
this.saveFleetConfig(patches);
|
|
12477
12727
|
this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
|
|
@@ -12706,6 +12956,7 @@ export class FleetManager {
|
|
|
12706
12956
|
this.mods.forgetInstance(name);
|
|
12707
12957
|
this.agentMods.forget(name);
|
|
12708
12958
|
this.officeActsInstance?.forget(name);
|
|
12959
|
+
this.officePackToolsInstance?.forget(name);
|
|
12709
12960
|
}
|
|
12710
12961
|
catch (err) {
|
|
12711
12962
|
this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
|
|
@@ -14073,7 +14324,7 @@ export class FleetManager {
|
|
|
14073
14324
|
return;
|
|
14074
14325
|
const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
|
|
14075
14326
|
const projects = board.listProjects(true);
|
|
14076
|
-
const stats = board.projectStats();
|
|
14327
|
+
const stats = board.projectStats(this.dispatcherGone);
|
|
14077
14328
|
const names = new Map(projects.map((p) => [p.id, p.name]));
|
|
14078
14329
|
const configured = this.getPrimaryAdapterId();
|
|
14079
14330
|
const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
|
|
@@ -14337,7 +14588,7 @@ export class FleetManager {
|
|
|
14337
14588
|
entry.web = {
|
|
14338
14589
|
text: web.text,
|
|
14339
14590
|
...(links ? { links } : {}),
|
|
14340
|
-
actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
|
|
14591
|
+
actions: [...opts.choices.map(c => ({ id: c.action, label: c.label })), ...(opts.webInput ? [{ id: opts.webInput.action, label: opts.webInput.label, input: true }] : [])],
|
|
14341
14592
|
expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
|
|
14342
14593
|
};
|
|
14343
14594
|
this.emitSseEvent("prompt", { instance: entry.instanceName, nonce, ...entry.web });
|
|
@@ -14514,6 +14765,10 @@ export class FleetManager {
|
|
|
14514
14765
|
return true;
|
|
14515
14766
|
if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
|
|
14516
14767
|
return true;
|
|
14768
|
+
if (await this.handleKiroReconcileCallback(data, adapterId, adapter))
|
|
14769
|
+
return true;
|
|
14770
|
+
if (await this.handleOfficePackCallback(data, adapterId, adapter))
|
|
14771
|
+
return true;
|
|
14517
14772
|
if (await this.handleDashboardCallback(data, adapterId, adapter))
|
|
14518
14773
|
return true;
|
|
14519
14774
|
if (await this.handleClassicApproval(data, adapterId, adapter))
|
|
@@ -14530,6 +14785,8 @@ export class FleetManager {
|
|
|
14530
14785
|
return true;
|
|
14531
14786
|
if (await this.handleInteractivePromptAssist(data, adapterId, adapter))
|
|
14532
14787
|
return true;
|
|
14788
|
+
if (await this.handleRemoteCardClick(data, adapterId, adapter))
|
|
14789
|
+
return true;
|
|
14533
14790
|
if (await this.handleClassicBackendSelection(data))
|
|
14534
14791
|
return true;
|
|
14535
14792
|
if (await this.handleModelSelection(data, adapterId))
|
|
@@ -14706,6 +14963,34 @@ export class FleetManager {
|
|
|
14706
14963
|
messages: messages.filter(m => keep(m.instance)),
|
|
14707
14964
|
});
|
|
14708
14965
|
}
|
|
14966
|
+
/** #2661: the Office levels as last worked out, and the work in flight (officeLevels). */
|
|
14967
|
+
officeLevelsMemo = null;
|
|
14968
|
+
officeLevelsRun = null;
|
|
14969
|
+
/**
|
|
14970
|
+
* #2661: every instance's Office level (office-levels.ts) — worked out by the cache service from the usage store, at
|
|
14971
|
+
* most once per OFFICE_LEVELS_TTL_MS however many pages and share links ask (one run at a time). Null when
|
|
14972
|
+
* web.office_levels is false.
|
|
14973
|
+
*/
|
|
14974
|
+
async officeLevels() {
|
|
14975
|
+
if (this.fleetConfig?.web?.office_levels === false)
|
|
14976
|
+
return null;
|
|
14977
|
+
const { OFFICE_LEVELS_TTL_MS } = await import("./office-levels.js");
|
|
14978
|
+
const memo = this.officeLevelsMemo;
|
|
14979
|
+
if (memo && performance.now() - memo.at < OFFICE_LEVELS_TTL_MS)
|
|
14980
|
+
return memo.levels;
|
|
14981
|
+
this.officeLevelsRun ??= this.cacheServiceFor().officeLevels(Date.now()).then((levels) => { this.officeLevelsMemo = { at: performance.now(), levels }; return levels; }).finally(() => { this.officeLevelsRun = null; });
|
|
14982
|
+
return this.officeLevelsRun;
|
|
14983
|
+
}
|
|
14984
|
+
/**
|
|
14985
|
+
* #2661: the levels as last worked out, for a synchronous reader (a share link's gateway): never waits; a stale or
|
|
14986
|
+
* missing set starts the next run. Null when web.office_levels is false or none has been worked out yet.
|
|
14987
|
+
*/
|
|
14988
|
+
officeLevelsSnapshot() {
|
|
14989
|
+
if (this.fleetConfig?.web?.office_levels === false)
|
|
14990
|
+
return null;
|
|
14991
|
+
void this.officeLevels().catch(() => { });
|
|
14992
|
+
return this.officeLevelsMemo?.levels ?? null;
|
|
14993
|
+
}
|
|
14709
14994
|
/** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
|
|
14710
14995
|
cacheReportPublished(window) {
|
|
14711
14996
|
return this.cacheServiceFor().published(cacheWindow(window));
|
|
@@ -15003,8 +15288,12 @@ export class FleetManager {
|
|
|
15003
15288
|
const buttons = this.replyButtons();
|
|
15004
15289
|
return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
|
|
15005
15290
|
}
|
|
15006
|
-
async clickWebPrompt(instance, nonce, action) {
|
|
15007
|
-
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-
|
|
15291
|
+
async clickWebPrompt(instance, nonce, action, text) {
|
|
15292
|
+
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z0-9-]{0,23}$/.test(action))
|
|
15293
|
+
return { status: 400, error: "Malformed prompt answer" };
|
|
15294
|
+
// #2675: only an action the page shows a text box for takes text, and it must say something.
|
|
15295
|
+
const input = this.pendingNonceButtons.get(nonce)?.web?.actions.find(a => a.id === action)?.input === true;
|
|
15296
|
+
if (input !== (text !== undefined) || (input && (!text.trim() || text.length > REMOTE_CARD_TEXT_MAX)))
|
|
15008
15297
|
return { status: 400, error: "Malformed prompt answer" };
|
|
15009
15298
|
const entry = this.pendingNonceButtons.get(nonce);
|
|
15010
15299
|
// Unknown is the same answer as answered or expired: the page drops the buttons either way.
|
|
@@ -15026,6 +15315,8 @@ export class FleetManager {
|
|
|
15026
15315
|
notice = n; },
|
|
15027
15316
|
};
|
|
15028
15317
|
this.webPromptClicks.add(data);
|
|
15318
|
+
if (text !== undefined)
|
|
15319
|
+
this.webPromptTexts.set(data, text);
|
|
15029
15320
|
await this.dispatchAdapterCallback(data, entry.adapterId, entry.adapter);
|
|
15030
15321
|
if (this.webPromptClaims.has(data))
|
|
15031
15322
|
return { status: 200 };
|
|
@@ -16083,6 +16374,163 @@ export class FleetManager {
|
|
|
16083
16374
|
}
|
|
16084
16375
|
return true;
|
|
16085
16376
|
}
|
|
16377
|
+
// ── #2675: a card a CLI shows in place of its composer, answered from the chat ─────────────────────────────────
|
|
16378
|
+
/**
|
|
16379
|
+
* Where an instance's card is put to people: its own place — its topic, or its ClassicBot room — or, on a fleet with
|
|
16380
|
+
* no chat platform, the dashboard. Never General: the answer is the instance's conversation.
|
|
16381
|
+
*/
|
|
16382
|
+
remoteCardPlace(instanceName) {
|
|
16383
|
+
const web = this.webOnlyPromptPlace();
|
|
16384
|
+
if (web)
|
|
16385
|
+
return web;
|
|
16386
|
+
const room = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
16387
|
+
if (room) {
|
|
16388
|
+
const adapterId = this.classicChannels?.getAdapterIdByInstance(instanceName) ?? this.getPrimaryAdapterId();
|
|
16389
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16390
|
+
return adapter && adapterId ? { adapter, adapterId, chatId: room } : null;
|
|
16391
|
+
}
|
|
16392
|
+
const adapterId = this.getInstanceAdapterId(instanceName);
|
|
16393
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16394
|
+
const chatId = this.getGroupIdForInstance(instanceName);
|
|
16395
|
+
const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
16396
|
+
if (!adapter || !adapterId || !chatId || topicId == null)
|
|
16397
|
+
return null;
|
|
16398
|
+
return { adapter, adapterId, chatId, threadId: String(topicId) };
|
|
16399
|
+
}
|
|
16400
|
+
/**
|
|
16401
|
+
* The daemon saw a card held still on `instanceName`'s screen (remote_card): put it to the people who talk to that
|
|
16402
|
+
* instance, with a button per answer. A card already posted for the instance is retired first (the daemon said it
|
|
16403
|
+
* was gone; this is the fallback).
|
|
16404
|
+
*/
|
|
16405
|
+
async postRemoteCard(instanceName, card) {
|
|
16406
|
+
this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
|
|
16407
|
+
const place = this.remoteCardPlace(instanceName);
|
|
16408
|
+
if (!place) {
|
|
16409
|
+
this.logger.warn({ instanceName }, "A card is waiting on screen but the instance has no place to post it — the parked notice stands");
|
|
16410
|
+
return;
|
|
16411
|
+
}
|
|
16412
|
+
const prompt = remoteCardPrompt(instanceName, card);
|
|
16413
|
+
const nonce = await this.postNonceButtonPrompt({
|
|
16414
|
+
prefix: REMOTE_CARD_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName, ...place,
|
|
16415
|
+
message: prompt.message, choices: prompt.choices, webInput: prompt.webInput,
|
|
16416
|
+
expiredText: t("card.expired", instanceName), timeoutMs: REMOTE_CARD_TIMEOUT_MS,
|
|
16417
|
+
extra: { remoteCard: { identity: card.identity, kind: card.kind, options: card.kind === "question" ? card.options.map(o => ({ n: o.n, title: o.title })) : [] } },
|
|
16418
|
+
});
|
|
16419
|
+
if (nonce)
|
|
16420
|
+
this.remoteCardPosts.set(instanceName, nonce);
|
|
16421
|
+
}
|
|
16422
|
+
/**
|
|
16423
|
+
* The card is gone from the screen (remote_card_gone, `identity`; null: whichever is posted): its buttons collapse to
|
|
16424
|
+
* `text` if nobody answered them here.
|
|
16425
|
+
*/
|
|
16426
|
+
retireRemoteCard(instanceName, identity, text = t("card.answered_in_pane")) {
|
|
16427
|
+
const nonce = this.remoteCardPosts.get(instanceName);
|
|
16428
|
+
if (!nonce)
|
|
16429
|
+
return;
|
|
16430
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
16431
|
+
if (entry && identity !== null && entry.remoteCard?.identity !== identity)
|
|
16432
|
+
return;
|
|
16433
|
+
this.remoteCardPosts.delete(instanceName);
|
|
16434
|
+
if (!entry)
|
|
16435
|
+
return;
|
|
16436
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16437
|
+
if (entry.timer)
|
|
16438
|
+
clearTimeout(entry.timer);
|
|
16439
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
16440
|
+
}
|
|
16441
|
+
/** Who may answer an instance's card: whoever may already talk to it there (a ClassicBot room is open to all). */
|
|
16442
|
+
mayAnswerRemoteCard(userId, entry) {
|
|
16443
|
+
if (this.classicChannels?.getChannelIdByInstance(entry.instanceName) === entry.chatId)
|
|
16444
|
+
return true;
|
|
16445
|
+
const am = this.worlds.get(entry.adapterId)?.accessManager ?? this.accessManager;
|
|
16446
|
+
return !am || am.isAllowed(userId);
|
|
16447
|
+
}
|
|
16448
|
+
/** A click on a card's button (platform or dashboard). */
|
|
16449
|
+
async handleRemoteCardClick(data, callbackAdapterId, receivingAdapter) {
|
|
16450
|
+
if (!data.callbackData.startsWith(REMOTE_CARD_CALLBACK_PREFIX))
|
|
16451
|
+
return false;
|
|
16452
|
+
// A typed answer from the dashboard must come with its text; refused before the claim, so the buttons stay.
|
|
16453
|
+
const match = REMOTE_CARD_CALLBACK_RE.exec(data.callbackData);
|
|
16454
|
+
if (match?.[2] === "text" && remoteCardAction("text", this.webPromptTexts.get(data)) === null) {
|
|
16455
|
+
data.ack?.(t("buttons.stale_notice"));
|
|
16456
|
+
return true;
|
|
16457
|
+
}
|
|
16458
|
+
const claimed = this.consumeNonceCallback(REMOTE_CARD_CALLBACK_PREFIX, REMOTE_CARD_CALLBACK_RE, data, callbackAdapterId, receivingAdapter, undefined, (userId, entry) => this.mayAnswerRemoteCard(userId, entry));
|
|
16459
|
+
if (claimed === null)
|
|
16460
|
+
return false;
|
|
16461
|
+
if (claimed === "consumed")
|
|
16462
|
+
return true;
|
|
16463
|
+
const action = remoteCardAction(claimed.action, this.webPromptTexts.get(data));
|
|
16464
|
+
const who = this.webPromptClicks.has(data) ? t("card.someone_on_dashboard") : (data.username ?? data.userId ?? "?");
|
|
16465
|
+
await this.answerRemoteCardFor(claimed.entry, action, who, data.messageId, data.editClicked);
|
|
16466
|
+
return true;
|
|
16467
|
+
}
|
|
16468
|
+
/**
|
|
16469
|
+
* An inbound message that replies to a posted question card, by someone who may answer it: its text is the typed
|
|
16470
|
+
* answer, and the message goes no further (true). Another bot's copy of a reply already taken is dropped (true).
|
|
16471
|
+
*/
|
|
16472
|
+
async takeRemoteCardReply(msg) {
|
|
16473
|
+
if (msg.isBotMessage || !msg.replyTo)
|
|
16474
|
+
return false;
|
|
16475
|
+
const now = performance.now();
|
|
16476
|
+
for (const [key, until] of this.takenCardReplies)
|
|
16477
|
+
if (until <= now)
|
|
16478
|
+
this.takenCardReplies.delete(key);
|
|
16479
|
+
const key = `${msg.chatId}|${msg.messageId}`;
|
|
16480
|
+
if (this.takenCardReplies.has(key))
|
|
16481
|
+
return true;
|
|
16482
|
+
for (const [nonce, entry] of this.pendingNonceButtons) {
|
|
16483
|
+
if (entry.prefix !== REMOTE_CARD_CALLBACK_PREFIX || entry.remoteCard?.kind !== "question")
|
|
16484
|
+
continue;
|
|
16485
|
+
// By place, whichever of the fleet's bots received the reply (a sibling's copy can arrive first, #2680 review), on
|
|
16486
|
+
// the post's own platform; who may answer is judged by the post's world (mayAnswerRemoteCard), as Needs you does.
|
|
16487
|
+
if (entry.chatId !== msg.chatId || entry.messageId !== msg.replyTo || entry.adapter.type !== msg.source)
|
|
16488
|
+
continue;
|
|
16489
|
+
if (!msg.userId || !this.mayAnswerRemoteCard(msg.userId, entry))
|
|
16490
|
+
return false; // not theirs to answer: it is just a message
|
|
16491
|
+
const action = remoteCardAction("text", msg.text ?? "");
|
|
16492
|
+
if (!action)
|
|
16493
|
+
return false;
|
|
16494
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16495
|
+
if (entry.timer)
|
|
16496
|
+
clearTimeout(entry.timer);
|
|
16497
|
+
this.webPromptGone(entry);
|
|
16498
|
+
this.takenCardReplies.set(key, now + REMOTE_CARD_TAKEN_TTL_MS);
|
|
16499
|
+
await this.answerRemoteCardFor(entry, action, msg.username ?? msg.userId, entry.messageId ?? msg.replyTo);
|
|
16500
|
+
return true;
|
|
16501
|
+
}
|
|
16502
|
+
return false;
|
|
16503
|
+
}
|
|
16504
|
+
/**
|
|
16505
|
+
* Key a claimed answer into the instance's card (the daemon checks it is still that card and walks a verified
|
|
16506
|
+
* cursor), then collapse the post to what happened. When the card is still waiting and was not answered — refused
|
|
16507
|
+
* or stopped part-way — it is posted again, so it can still be answered from here.
|
|
16508
|
+
*/
|
|
16509
|
+
async answerRemoteCardFor(entry, action, who, messageId, editClicked) {
|
|
16510
|
+
if (this.remoteCardPosts.get(entry.instanceName) === entry.nonce)
|
|
16511
|
+
this.remoteCardPosts.delete(entry.instanceName);
|
|
16512
|
+
const daemon = this.daemons.get(entry.instanceName);
|
|
16513
|
+
const card = entry.remoteCard;
|
|
16514
|
+
let outcome;
|
|
16515
|
+
if (!card || !action)
|
|
16516
|
+
outcome = { outcome: "refused", reason: "malformed" };
|
|
16517
|
+
else if (!daemon?.answerRemoteCard)
|
|
16518
|
+
outcome = { outcome: "stale" };
|
|
16519
|
+
else {
|
|
16520
|
+
try {
|
|
16521
|
+
outcome = await daemon.answerRemoteCard(card.identity, action);
|
|
16522
|
+
}
|
|
16523
|
+
catch (err) {
|
|
16524
|
+
this.logger.warn({ err: err?.message, instanceName: entry.instanceName }, "Answering a card from the chat failed");
|
|
16525
|
+
outcome = { outcome: "stopped", step: "error" };
|
|
16526
|
+
}
|
|
16527
|
+
}
|
|
16528
|
+
this.eventLog?.insert(entry.instanceName, "remote_card_answer", { action: action?.kind ?? null, outcome: outcome.outcome });
|
|
16529
|
+
const choice = card && action ? remoteCardChoiceLabel(card, action) : "?";
|
|
16530
|
+
await this.retireNonceButtons(entry, messageId, remoteCardOutcomeText(entry.instanceName, who, choice, outcome), editClicked);
|
|
16531
|
+
if (outcome.outcome === "refused" || outcome.outcome === "stopped")
|
|
16532
|
+
daemon?.repostRemoteCard?.();
|
|
16533
|
+
}
|
|
16086
16534
|
// ── Cancel button ────────────────────────────────────────────────────
|
|
16087
16535
|
// Sent after delivering a user message to an instance; clicking it (or
|
|
16088
16536
|
// /cancel) sends Escape to the instance's pane to interrupt generation.
|
|
@@ -17793,8 +18241,8 @@ export class FleetManager {
|
|
|
17793
18241
|
* The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
|
|
17794
18242
|
* revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
|
|
17795
18243
|
* must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
|
|
17796
|
-
* one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
|
|
17797
|
-
* (task-board-waker.ts).
|
|
18244
|
+
* one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken, or
|
|
18245
|
+
* withdrawn once its cards changed again (#2801; task-board-waker.ts).
|
|
17798
18246
|
*/
|
|
17799
18247
|
attachTaskBoard(db) {
|
|
17800
18248
|
this.taskBoardWaker?.stop();
|
|
@@ -17803,6 +18251,19 @@ export class FleetManager {
|
|
|
17803
18251
|
const waker = this.taskBoardWaker = new TaskBoardWaker({
|
|
17804
18252
|
deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
|
|
17805
18253
|
isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
|
|
18254
|
+
// #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
|
|
18255
|
+
messagedAbout: (from, to, codes, sinceMs) => {
|
|
18256
|
+
const outbox = this.deliveryOutbox;
|
|
18257
|
+
if (!outbox?.isOpen)
|
|
18258
|
+
return false;
|
|
18259
|
+
return outbox.recentTextsFrom(to, from, new Date(sinceMs).toISOString()).some((text) => codes.some((code) => namesCard(text, code)));
|
|
18260
|
+
},
|
|
18261
|
+
// #2801: a notice still waiting whose cards have all changed again is not delivered.
|
|
18262
|
+
withdraw: (prefix, stale, reason) => {
|
|
18263
|
+
const outbox = this.deliveryOutbox;
|
|
18264
|
+
return outbox?.isOpen ? outbox.withdrawWaitingNotices(prefix, stale, reason).length : 0;
|
|
18265
|
+
},
|
|
18266
|
+
changedAt: (uuid) => db.board.lastChangeAt(uuid),
|
|
17806
18267
|
holds: (wake) => db.board.wakeHolds(wake),
|
|
17807
18268
|
card: (uuid) => db.board.findCard(uuid),
|
|
17808
18269
|
// #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
|
|
@@ -17819,12 +18280,13 @@ export class FleetManager {
|
|
|
17819
18280
|
},
|
|
17820
18281
|
log: (level, message, extra) => this.logger[level](extra ?? {}, message),
|
|
17821
18282
|
});
|
|
17822
|
-
db.board.onChange(({ rev, ids, projects, wakes }) => {
|
|
18283
|
+
db.board.onChange(({ rev, ids, projects, wakes, changed }) => {
|
|
17823
18284
|
// Refresh both old and new owners on reassignment; wakes are not an ownership index.
|
|
17824
18285
|
if (ids.length)
|
|
17825
18286
|
for (const name of this.daemons.keys())
|
|
17826
18287
|
this.observeWorkingState(name, "tasks");
|
|
17827
18288
|
this.emitSseEvent("tasks", { rev, ids, projects });
|
|
18289
|
+
waker.changed(changed);
|
|
17828
18290
|
waker.note(wakes);
|
|
17829
18291
|
// step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
|
|
17830
18292
|
if (ids.length || projects)
|
|
@@ -22978,7 +23440,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22978
23440
|
// From the install's own channel tag, this is what /update installs — even a stable the release pointed the
|
|
22979
23441
|
// tag at (2.2.0 on @alpha / @beta): the plain /update text, never "/update stays on … use --stable" (#1928).
|
|
22980
23442
|
const key = fromChannel ? "update.available_current" : updateNoticeKey(currentVersion, target);
|
|
22981
|
-
|
|
23443
|
+
// #2775: null = a beta install and a stable target; beta users are never told to switch.
|
|
23444
|
+
if (key)
|
|
23445
|
+
this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
|
|
22982
23446
|
}
|
|
22983
23447
|
}
|
|
22984
23448
|
}
|