@songsid/agend 2.3.2-beta.8 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh-TW.md +6 -0
- 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.js +3 -0
- 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.js +23 -1
- 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 +344 -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 -1
- package/dist/backend/kiro.js +553 -29
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +102 -0
- 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 +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +17 -8
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +66 -0
- package/dist/daemon.js +421 -19
- 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/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 +90 -3
- package/dist/fleet-manager.js +569 -41
- 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/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 +1 -1
- 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 +60 -18
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +60 -18
- 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 +7 -0
- package/dist/instance-lifecycle.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 +198 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +2 -2
- 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 +80 -13
- package/dist/office-share.js +145 -25
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +41 -2
- 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 +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.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 -0
- package/dist/settings-api.js +18 -12
- 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 -4
- 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.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.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 +10 -0
- 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 +87 -10
- 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 +18 -3
- package/dist/ui/fleet-office-pack.js +217 -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 +323 -50
- 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 +50 -1
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- 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 -0
- package/dist/ui/settings-strings.js +16 -4
- 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 +74 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- 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-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 +149 -15
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
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,6 +47,7 @@ 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";
|
|
@@ -83,7 +88,7 @@ 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";
|
|
@@ -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";
|
|
@@ -248,7 +255,7 @@ 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
257
|
import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
|
|
251
|
-
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
258
|
+
import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
|
|
252
259
|
import { buildOrgChart } from "./web-org.js";
|
|
253
260
|
import { topMemoryHogs } from "./memory-hogs.js";
|
|
254
261
|
import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
|
|
@@ -478,6 +485,13 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
|
478
485
|
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
479
486
|
const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
|
|
480
487
|
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
488
|
+
/** #2675: a card a CLI shows in place of its composer (kiro V3's Question / MCP input), posted with its answers. */
|
|
489
|
+
const REMOTE_CARD_CALLBACK_PREFIX = "card-answer:";
|
|
490
|
+
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)$/;
|
|
491
|
+
/** A posted card's buttons stay this long; the card itself waits in the pane for as long as it takes. */
|
|
492
|
+
const REMOTE_CARD_TIMEOUT_MS = 12 * 60 * 60_000;
|
|
493
|
+
/** A reply already taken as a card's answer is remembered this long, so another bot's copy of it is dropped (#2675). */
|
|
494
|
+
const REMOTE_CARD_TAKEN_TTL_MS = 10 * 60_000;
|
|
481
495
|
const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
|
|
482
496
|
const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
483
497
|
/** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
|
|
@@ -486,6 +500,11 @@ const HANG_CALLBACK_PREFIX = "hang:";
|
|
|
486
500
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
487
501
|
/** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
|
|
488
502
|
const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
|
|
503
|
+
const KIRO_RECONCILE_CALLBACK_PREFIX = "kiro-reconcile:";
|
|
504
|
+
/** #2692: a fleet admin's Approve / Reject on an agent's pending skin-pack change, in General. */
|
|
505
|
+
const OFFICE_PACK_CALLBACK_PREFIX = "office-pack:";
|
|
506
|
+
/** #2692: the skin-pack tools' IPC types (answered as web theme tools). */
|
|
507
|
+
const OFFICE_PACK_IPC = new Set(OFFICE_PACK_TOOLS.map(tool => `fleet_${tool}`));
|
|
489
508
|
/** Long enough for an admin to read the prompt; the move is judged again on the click. */
|
|
490
509
|
const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
491
510
|
/**
|
|
@@ -496,6 +515,7 @@ const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
|
496
515
|
*/
|
|
497
516
|
const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
|
|
498
517
|
HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
|
|
518
|
+
REMOTE_CARD_CALLBACK_PREFIX,
|
|
499
519
|
]);
|
|
500
520
|
/**
|
|
501
521
|
* Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
|
|
@@ -913,6 +933,8 @@ export class FleetManager {
|
|
|
913
933
|
pendingNonceButtons = new Map();
|
|
914
934
|
/** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
|
|
915
935
|
pendingKiroMigrations = new Map();
|
|
936
|
+
pendingKiroReconciliations = new Map();
|
|
937
|
+
kiroMigrationRuns = new Map();
|
|
916
938
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
917
939
|
needsYou = null;
|
|
918
940
|
/** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
|
|
@@ -925,6 +947,12 @@ export class FleetManager {
|
|
|
925
947
|
* adapter emits — whatever fields its payload carries — can claim a dashboard click's authority.
|
|
926
948
|
*/
|
|
927
949
|
webPromptClicks = new WeakSet();
|
|
950
|
+
/** #2675: a dashboard click's typed answer, for the click it came with. */
|
|
951
|
+
webPromptTexts = new WeakMap();
|
|
952
|
+
/** #2675: the nonce of each instance's posted card. */
|
|
953
|
+
remoteCardPosts = new Map();
|
|
954
|
+
/** #2675: replies taken as a card's answer (`<chatId>|<messageId>` → monotonic expiry), so another bot's copy is dropped. */
|
|
955
|
+
takenCardReplies = new Map();
|
|
928
956
|
/** The web clicks consumeNonceCallback actually claimed (the others were refused or lost a race). */
|
|
929
957
|
webPromptClaims = new WeakSet();
|
|
930
958
|
// Model failover state
|
|
@@ -1195,6 +1223,7 @@ export class FleetManager {
|
|
|
1195
1223
|
needs: () => this.needsYouItems().map(i => i.instance),
|
|
1196
1224
|
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
1197
1225
|
skins: this.officeSkins,
|
|
1226
|
+
levels: () => this.officeLevelsSnapshot(),
|
|
1198
1227
|
},
|
|
1199
1228
|
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
1200
1229
|
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
@@ -1211,7 +1240,7 @@ export class FleetManager {
|
|
|
1211
1240
|
* `share` null with `count`: "Revoke all".
|
|
1212
1241
|
*/
|
|
1213
1242
|
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;
|
|
1243
|
+
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
1244
|
const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
|
|
1216
1245
|
const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
|
|
1217
1246
|
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
@@ -1235,6 +1264,98 @@ export class FleetManager {
|
|
|
1235
1264
|
changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
|
|
1236
1265
|
}));
|
|
1237
1266
|
}
|
|
1267
|
+
officePackToolsInstance;
|
|
1268
|
+
/** #2692: the agents' skin-pack tools — their log, the admins' approval mode and the pending changes (memory only). */
|
|
1269
|
+
get officePackTools() {
|
|
1270
|
+
return (this.officePackToolsInstance ??= new OfficePackTools({
|
|
1271
|
+
skins: this.officeSkins, offices: this.officeStore,
|
|
1272
|
+
allowedAgents: () => this.officePackAgents(),
|
|
1273
|
+
notice: (text, pending) => { void this.officePackNotice(text, pending); },
|
|
1274
|
+
settled: (id, text) => this.settleOfficePackPrompt(id, text),
|
|
1275
|
+
changed: what => {
|
|
1276
|
+
if (what.skins !== undefined)
|
|
1277
|
+
this.emitSseEvent("office_skins", { rev: what.skins });
|
|
1278
|
+
if (what.offices !== undefined)
|
|
1279
|
+
this.emitSseEvent("offices", { rev: what.offices });
|
|
1280
|
+
if (what.changes)
|
|
1281
|
+
this.emitSseEvent("office_pack_changes", {});
|
|
1282
|
+
},
|
|
1283
|
+
logger: this.logger,
|
|
1284
|
+
}));
|
|
1285
|
+
}
|
|
1286
|
+
/** #2692: the instances whose `office_packs` feature is on (their own switch, else the defaults'): the Office page lists them. */
|
|
1287
|
+
officePackAgents() {
|
|
1288
|
+
const names = new Set([...Object.keys(this.fleetConfig?.instances ?? {}), ...(this.classicChannels?.getAll() ?? []).map(ch => ch.instanceName)]);
|
|
1289
|
+
return [...names].filter(name => dashboardRefusalIn(this.fleetConfig, name, "office_pack_upload") === null);
|
|
1290
|
+
}
|
|
1291
|
+
/** #2692: the nonce of each pending pack change's prompt in General (its buttons are retired when it is decided). */
|
|
1292
|
+
officePackPrompts = new Map();
|
|
1293
|
+
/**
|
|
1294
|
+
* #2692: tell the fleet's admins of an agent's pack change — one short message in General (notifyFleetError, never a
|
|
1295
|
+
* sign-in notice). A pending one comes with Approve / Reject (a fleet admin's, as a kiro move); where no General can
|
|
1296
|
+
* carry buttons it is the plain message, and the Office page decides it.
|
|
1297
|
+
*/
|
|
1298
|
+
async officePackNotice(text, pending) {
|
|
1299
|
+
if (!pending) {
|
|
1300
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
const general = this.findGeneralInstance();
|
|
1304
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
1305
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
1306
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
1307
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
1308
|
+
if (general && adapterId && adapter && chatId) {
|
|
1309
|
+
try {
|
|
1310
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
1311
|
+
prefix: OFFICE_PACK_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: pending.instance, adapter, adapterId, chatId,
|
|
1312
|
+
...(topic ? { threadId: topic } : {}), message: text,
|
|
1313
|
+
choices: [{ action: "approve", label: t("office_packs.approve_button") }, { action: "reject", label: t("office_packs.reject_button") }],
|
|
1314
|
+
expiredText: t("office_packs.expired", ""), timeoutMs: Math.max(1_000, pending.expires_at - Date.now()),
|
|
1315
|
+
});
|
|
1316
|
+
if (this.officePackTools.pendingViews().some(v => v.id === pending.id))
|
|
1317
|
+
this.officePackPrompts.set(pending.id, nonce);
|
|
1318
|
+
else
|
|
1319
|
+
this.retireOfficePackNonce(nonce, t("office_packs.gone"));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
this.logger.info({ err: err.message }, "Office pack approval prompt unavailable; the plain notice and the Office page remain");
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1327
|
+
}
|
|
1328
|
+
retireOfficePackNonce(nonce, text) {
|
|
1329
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
1330
|
+
if (!entry || entry.prefix !== OFFICE_PACK_CALLBACK_PREFIX)
|
|
1331
|
+
return;
|
|
1332
|
+
this.pendingNonceButtons.delete(nonce);
|
|
1333
|
+
clearTimeout(entry.timer);
|
|
1334
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
1335
|
+
}
|
|
1336
|
+
/** #2692: a pending change was decided (here or on the page) or expired: its prompt says so and loses its buttons. */
|
|
1337
|
+
settleOfficePackPrompt(id, text) {
|
|
1338
|
+
const nonce = this.officePackPrompts.get(id);
|
|
1339
|
+
this.officePackPrompts.delete(id);
|
|
1340
|
+
if (nonce)
|
|
1341
|
+
this.retireOfficePackNonce(nonce, text);
|
|
1342
|
+
}
|
|
1343
|
+
/** #2692: Approve / Reject on a pending pack change. Any fleet admin (the default nonce authorization), in General. */
|
|
1344
|
+
async handleOfficePackCallback(data, adapterId, adapter) {
|
|
1345
|
+
const claimed = this.consumeNonceCallback(OFFICE_PACK_CALLBACK_PREFIX, /^office-pack:([0-9a-f]{32}):(approve|reject)$/, data, adapterId, adapter);
|
|
1346
|
+
if (claimed === null)
|
|
1347
|
+
return false;
|
|
1348
|
+
if (claimed === "consumed")
|
|
1349
|
+
return true;
|
|
1350
|
+
const { entry, action } = claimed;
|
|
1351
|
+
const nonce = data.callbackData.slice(OFFICE_PACK_CALLBACK_PREFIX.length, OFFICE_PACK_CALLBACK_PREFIX.length + 32);
|
|
1352
|
+
const id = [...this.officePackPrompts].find(([, n]) => n === nonce)?.[0];
|
|
1353
|
+
if (id)
|
|
1354
|
+
this.officePackPrompts.delete(id);
|
|
1355
|
+
const decided = id ? this.officePackTools.decide(id, action, t("settings_confirm.admin_label", data.userId ?? "")) : null;
|
|
1356
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, decided?.text ?? t("office_packs.gone"));
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1238
1359
|
/**
|
|
1239
1360
|
* #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
|
|
1240
1361
|
* frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
|
|
@@ -1245,7 +1366,10 @@ export class FleetManager {
|
|
|
1245
1366
|
needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
|
|
1246
1367
|
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
1247
1368
|
let profiles = null;
|
|
1369
|
+
// #2661: the levels as last worked out (never waits; none while off or not worked out yet).
|
|
1370
|
+
const levels = this.officeLevelsSnapshot();
|
|
1248
1371
|
return {
|
|
1372
|
+
level: name => (levels && Object.prototype.hasOwnProperty.call(levels, name) ? levels[name].level : undefined),
|
|
1249
1373
|
stateOf: name => {
|
|
1250
1374
|
if (!this.isKnownInstance(name))
|
|
1251
1375
|
return "missing";
|
|
@@ -1797,12 +1921,96 @@ export class FleetManager {
|
|
|
1797
1921
|
this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
|
|
1798
1922
|
}
|
|
1799
1923
|
kiroMigrateDeps() {
|
|
1924
|
+
let token;
|
|
1925
|
+
const current = (name) => {
|
|
1926
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1927
|
+
return !!token && run?.token === token && !this.shuttingDown
|
|
1928
|
+
&& (this.explicitStopGeneration.get(name) ?? 0) === run.stopGeneration;
|
|
1929
|
+
};
|
|
1800
1930
|
return {
|
|
1801
|
-
agendHome: getAgendHome(),
|
|
1802
|
-
fleetConfig: () => this.fleetConfig,
|
|
1803
|
-
saveFleetConfig: () => this.saveFleetConfig(),
|
|
1931
|
+
agendHome: getAgendHome(), fleetConfig: () => this.fleetConfig, saveFleetConfig: () => this.saveFleetConfig(),
|
|
1804
1932
|
status: name => this.getInstanceStatus(name),
|
|
1805
|
-
|
|
1933
|
+
reserve: name => {
|
|
1934
|
+
if (this.kiroMigrationRuns.has(name) || this.restartsInFlight.has(name) || this.stopsInFlight.has(name))
|
|
1935
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
1936
|
+
token = Symbol(name);
|
|
1937
|
+
this.kiroMigrationRuns.set(name, { token, stopGeneration: this.explicitStopGeneration.get(name) ?? 0 });
|
|
1938
|
+
return { current: () => current(name), release: () => {
|
|
1939
|
+
if (this.kiroMigrationRuns.get(name)?.token === token)
|
|
1940
|
+
this.kiroMigrationRuns.delete(name);
|
|
1941
|
+
this.scheduleDeliveryOutboxPump();
|
|
1942
|
+
} };
|
|
1943
|
+
},
|
|
1944
|
+
preflight: async (name, plan) => {
|
|
1945
|
+
const compat = getCachedKiroCliCompatibility(resolveBinary("kiro-cli"));
|
|
1946
|
+
if (!supportsVerifiedKiroMigration(compat))
|
|
1947
|
+
throw new Error(t("kiro_migrate.version_required"));
|
|
1948
|
+
const daemon = this.daemons.get(name);
|
|
1949
|
+
if (this.getInstanceStatus(name) === "running" && (!daemon || !await daemon.kiroMigrationIdle()))
|
|
1950
|
+
throw new Error(t("kiro_migrate.idle_required"));
|
|
1951
|
+
if (!current(name) || daemon && this.daemons.get(name) !== daemon)
|
|
1952
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1953
|
+
const inst = this.fleetConfig?.instances[name];
|
|
1954
|
+
const model = daemon?.getModelOverride() ?? inst?.model ?? (plan.classicId ? readKiroV1SavedModel({ id: plan.classicId,
|
|
1955
|
+
workingDirectory: plan.workingDirectory, dbPath: kiroV1DbPath(plan.credentialProfile) }) : undefined);
|
|
1956
|
+
if (!model && plan.classicId)
|
|
1957
|
+
throw new Error(t("kiro_migrate.model_unknown"));
|
|
1958
|
+
return model ?? "";
|
|
1959
|
+
},
|
|
1960
|
+
restart: async (name) => {
|
|
1961
|
+
if (!current(name))
|
|
1962
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1963
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1964
|
+
const expected = run.stopGeneration + 1;
|
|
1965
|
+
// This restart owns exactly one stop. A person's concurrent stop must not be hidden by the workflow.
|
|
1966
|
+
try {
|
|
1967
|
+
await this.restartSingleInstance(name, { explicit: true, kiroMigration: token });
|
|
1968
|
+
}
|
|
1969
|
+
finally {
|
|
1970
|
+
// Even a failed replacement owns its completed stop; retain authority to clean up/pause it.
|
|
1971
|
+
if (this.kiroMigrationRuns.get(name) === run && this.explicitStopGeneration.get(name) === expected)
|
|
1972
|
+
run.stopGeneration = expected;
|
|
1973
|
+
}
|
|
1974
|
+
if (this.explicitStopGeneration.get(name) !== expected)
|
|
1975
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1976
|
+
},
|
|
1977
|
+
verify: async (name, id, model, archived) => {
|
|
1978
|
+
const daemon = this.daemons.get(name), deadline = performance.now() + 60_000;
|
|
1979
|
+
if (!daemon)
|
|
1980
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1981
|
+
while (performance.now() < deadline) {
|
|
1982
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1983
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1984
|
+
const observed = await daemon.kiroMigrationObservation();
|
|
1985
|
+
if (observed) {
|
|
1986
|
+
if (observed.sessionId !== id || observed.model !== model || resolvePath(observed.transcript) !== resolvePath(kiroV2SessionsDir(), id + ".jsonl"))
|
|
1987
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1988
|
+
const inst = this.fleetConfig.instances[name];
|
|
1989
|
+
const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), migration: {
|
|
1990
|
+
agendHome: getAgendHome(), instance: name, workingDirectory: inst.working_directory, credentialProfile: instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli"),
|
|
1991
|
+
sessionId: id, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli")),
|
|
1992
|
+
model, archived, outboxDbPath: join(this.dataDir, "delivery-outbox.db"), knownInstances: Object.keys(this.fleetConfig.instances),
|
|
1993
|
+
} });
|
|
1994
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1995
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1996
|
+
if (reply.kind !== "ok" || !reply.verification)
|
|
1997
|
+
throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_migrate.verification_failed"));
|
|
1998
|
+
return reply.verification;
|
|
1999
|
+
}
|
|
2000
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2001
|
+
}
|
|
2002
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
2003
|
+
},
|
|
2004
|
+
stop: async (name) => { try {
|
|
2005
|
+
await this.stopInstance(name);
|
|
2006
|
+
}
|
|
2007
|
+
finally {
|
|
2008
|
+
writePausedMarker(this.getInstanceDir(name), Date.now(), "error");
|
|
2009
|
+
} },
|
|
2010
|
+
readClassicStore: async (workingDirectory, credentialProfile) => {
|
|
2011
|
+
const dbPath = kiroV1DbPath(credentialProfile);
|
|
2012
|
+
return listKiroV1Sessions(workingDirectory, dbPath, await readKiroClassicLinks(workingDirectory, dbPath));
|
|
2013
|
+
},
|
|
1806
2014
|
};
|
|
1807
2015
|
}
|
|
1808
2016
|
/**
|
|
@@ -1810,7 +2018,7 @@ export class FleetManager {
|
|
|
1810
2018
|
* here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
|
|
1811
2019
|
*/
|
|
1812
2020
|
async requestKiroMigration(name, startFresh) {
|
|
1813
|
-
const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
2021
|
+
const judged = await planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
1814
2022
|
if (!judged.ok)
|
|
1815
2023
|
return judged;
|
|
1816
2024
|
const general = this.findGeneralInstance();
|
|
@@ -1860,8 +2068,77 @@ export class FleetManager {
|
|
|
1860
2068
|
const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
|
|
1861
2069
|
this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
|
|
1862
2070
|
this.notifyFleetError(outcome.text);
|
|
1863
|
-
|
|
1864
|
-
|
|
2071
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2074
|
+
kiroReconcileDeps() {
|
|
2075
|
+
return { ...this.kiroMigrateDeps(), store: async (input, plan, actor, rollbackReceipt) => {
|
|
2076
|
+
const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: input.sessionsDir, reconciliation: { input, plan, actor, rollbackReceipt } });
|
|
2077
|
+
if (reply.kind !== "ok" || !reply.reconciliation)
|
|
2078
|
+
throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_reconcile.changed"));
|
|
2079
|
+
return reply.reconciliation;
|
|
2080
|
+
} };
|
|
2081
|
+
}
|
|
2082
|
+
async requestKiroReconciliation(name, keep, archive, rollbackReceipt) {
|
|
2083
|
+
try {
|
|
2084
|
+
if (!this.eventLog)
|
|
2085
|
+
return { ok: false, error: t("kiro_reconcile.audit_failed") };
|
|
2086
|
+
const plan = await planKiroReconciliation(this.kiroReconcileDeps(), name, keep, archive, rollbackReceipt);
|
|
2087
|
+
const general = this.findGeneralInstance(), adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
2088
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined, chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
2089
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
2090
|
+
if (!general || !adapterId || !adapter || !chatId)
|
|
2091
|
+
return { ok: false, error: t("kiro_migrate.no_general") };
|
|
2092
|
+
const nonce = await this.postNonceButtonPromptOrThrow({ prefix: KIRO_RECONCILE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name,
|
|
2093
|
+
adapter, adapterId, chatId, ...(topic ? { threadId: topic } : {}), message: kiroReconciliationPrompt(plan),
|
|
2094
|
+
choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
|
|
2095
|
+
expiredText: t("kiro_migrate.expired", name), timeoutMs: KIRO_MIGRATE_TIMEOUT_MS });
|
|
2096
|
+
this.pendingKiroReconciliations.set(nonce, plan);
|
|
2097
|
+
setTimeout(() => this.pendingKiroReconciliations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000).unref?.();
|
|
2098
|
+
this.eventLog?.insert(name, "kiro_reconcile", { phase: "requested", keep, archive, rollbackReceipt, message: kiroReconciliationPrompt(plan) });
|
|
2099
|
+
return { ok: true, text: t("kiro_migrate.requested") };
|
|
2100
|
+
}
|
|
2101
|
+
catch (error) {
|
|
2102
|
+
return { ok: false, error: error instanceof SyntaxError ? "Malformed reconciliation data" : error.message };
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async handleKiroReconcileCallback(data, adapterId, adapter) {
|
|
2106
|
+
const claimed = this.consumeNonceCallback(KIRO_RECONCILE_CALLBACK_PREFIX, /^kiro-reconcile:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
|
|
2107
|
+
if (claimed === null)
|
|
2108
|
+
return false;
|
|
2109
|
+
if (claimed === "consumed")
|
|
2110
|
+
return true;
|
|
2111
|
+
const { entry, action } = claimed;
|
|
2112
|
+
const nonce = data.callbackData.slice(KIRO_RECONCILE_CALLBACK_PREFIX.length, KIRO_RECONCILE_CALLBACK_PREFIX.length + 32);
|
|
2113
|
+
const plan = this.pendingKiroReconciliations.get(nonce);
|
|
2114
|
+
this.pendingKiroReconciliations.delete(nonce);
|
|
2115
|
+
if (action === "cancel" || !plan) {
|
|
2116
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
|
|
2117
|
+
this.eventLog?.insert(entry.instanceName, "kiro_reconcile", { phase: "cancelled_or_expired", userId: data.userId });
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
let outcome;
|
|
2121
|
+
try {
|
|
2122
|
+
if (!this.eventLog)
|
|
2123
|
+
throw new Error(t("kiro_reconcile.audit_failed"));
|
|
2124
|
+
this.eventLog.insert(plan.name, "kiro_reconcile", { phase: "confirmed", userId: data.userId, keep: plan.input.sessionId, archive: plan.input.archiveId });
|
|
2125
|
+
}
|
|
2126
|
+
catch {
|
|
2127
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.audit_failed"));
|
|
2128
|
+
this.notifyFleetError(t("kiro_reconcile.audit_failed"));
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.applying", plan.name));
|
|
2132
|
+
outcome = await applyKiroReconciliation(this.kiroReconcileDeps(), plan, `${adapterId}:${data.userId}`);
|
|
2133
|
+
try {
|
|
2134
|
+
this.eventLog.insert(plan.name, "kiro_reconcile", { phase: outcome.ok ? "completed" : "failed", userId: data.userId, receipt: outcome.receipt, message: outcome.text });
|
|
2135
|
+
}
|
|
2136
|
+
catch {
|
|
2137
|
+
this.logger.error({ name: plan.name, receipt: outcome.receipt }, "Kiro reconciliation final Log entry failed; confirmed intent remains in the audit and archive");
|
|
2138
|
+
}
|
|
2139
|
+
this.logger.info({ name: plan.name, ok: outcome.ok, receipt: outcome.receipt }, "Kiro explicit reconciliation");
|
|
2140
|
+
this.notifyFleetError(outcome.text);
|
|
2141
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
1865
2142
|
return true;
|
|
1866
2143
|
}
|
|
1867
2144
|
async handleSettingsChangeCallback(data, adapterId, adapter) {
|
|
@@ -5441,7 +5718,10 @@ export class FleetManager {
|
|
|
5441
5718
|
*/
|
|
5442
5719
|
resumePaused = false,
|
|
5443
5720
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
5444
|
-
transition, execution) {
|
|
5721
|
+
transition, execution, kiroMigration) {
|
|
5722
|
+
const migration = this.kiroMigrationRuns?.get(name);
|
|
5723
|
+
if (migration && migration.token !== kiroMigration)
|
|
5724
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
5445
5725
|
execution?.assert();
|
|
5446
5726
|
if (this.sessionOomStopped(name)) {
|
|
5447
5727
|
if (resumePaused)
|
|
@@ -5937,6 +6217,11 @@ export class FleetManager {
|
|
|
5937
6217
|
stopOnUnsupportedCli(name, err, what) {
|
|
5938
6218
|
if (!(err instanceof UnsupportedCliError))
|
|
5939
6219
|
return false;
|
|
6220
|
+
if (err instanceof ResumeBlockedError) {
|
|
6221
|
+
this.logger.error({ name, reason: err.message }, `Not starting ${what}: retained session needs store repair`);
|
|
6222
|
+
void this.notifyFleetError(t("fleet.resume_blocked", name, err.message, err.remedy));
|
|
6223
|
+
return true;
|
|
6224
|
+
}
|
|
5940
6225
|
this.logger.error({ name, reason: err.message }, `Not starting ${what}: the installed CLI cannot run it as configured`);
|
|
5941
6226
|
this.queueUnsupportedCliNotice(name, err.message);
|
|
5942
6227
|
this.queueKiroIncompatNotice(name, err.message);
|
|
@@ -6136,6 +6421,9 @@ export class FleetManager {
|
|
|
6136
6421
|
}
|
|
6137
6422
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
6138
6423
|
async restartSingleInstance(name, opts) {
|
|
6424
|
+
const migration = this.kiroMigrationRuns?.get(name);
|
|
6425
|
+
if (migration && migration.token !== opts?.kiroMigration)
|
|
6426
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
6139
6427
|
if (opts?.recoveryCurrent && !opts.recoveryCurrent())
|
|
6140
6428
|
throw new Error(t("exit.expired", name));
|
|
6141
6429
|
const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
|
|
@@ -6196,7 +6484,7 @@ export class FleetManager {
|
|
|
6196
6484
|
return run;
|
|
6197
6485
|
}
|
|
6198
6486
|
isInstanceRestarting(name) {
|
|
6199
|
-
return this.restartsInFlight.has(name);
|
|
6487
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
6200
6488
|
}
|
|
6201
6489
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
6202
6490
|
restartInFlight(name) {
|
|
@@ -6301,7 +6589,8 @@ export class FleetManager {
|
|
|
6301
6589
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
|
|
6302
6590
|
});
|
|
6303
6591
|
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)
|
|
6592
|
+
await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
|
|
6593
|
+
: this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
|
|
6305
6594
|
this.stormWindow.markRecovered(name);
|
|
6306
6595
|
return;
|
|
6307
6596
|
}
|
|
@@ -7057,9 +7346,22 @@ export class FleetManager {
|
|
|
7057
7346
|
...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
|
|
7058
7347
|
}).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
|
|
7059
7348
|
// #2348: `explicit` — grants are the scope (default deny); false: the derived scope of #1920, until grants are saved.
|
|
7060
|
-
|
|
7349
|
+
// #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
|
|
7350
|
+
// with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
|
|
7351
|
+
const profiles = profileIdentities(this.dataDir);
|
|
7352
|
+
const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
|
|
7353
|
+
.map((r) => {
|
|
7354
|
+
const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
|
|
7355
|
+
return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
|
|
7356
|
+
})
|
|
7357
|
+
.sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
|
|
7358
|
+
return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
|
|
7061
7359
|
audit: this.guestAudit() };
|
|
7062
7360
|
}
|
|
7361
|
+
/** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
|
|
7362
|
+
classicRoomNames() {
|
|
7363
|
+
return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
|
|
7364
|
+
}
|
|
7063
7365
|
/**
|
|
7064
7366
|
* #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
|
|
7065
7367
|
* (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
|
|
@@ -8082,8 +8384,8 @@ export class FleetManager {
|
|
|
8082
8384
|
const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
8083
8385
|
this.warnUnboundGeneralChannelIds(fleet);
|
|
8084
8386
|
const primaryAdapterId = channelConfigs[0] ? (channelConfigs[0].id ?? channelConfigs[0].type) : undefined;
|
|
8387
|
+
let generalsCreated = adoptGeneralByName(fleet, this.logger, dir => insideManagedWorkspaces(dir) || resolvePath(dir) === join(getAgendHome(), GENERAL_NAME));
|
|
8085
8388
|
const generalInstances = Object.entries(fleet.instances).filter(([, inst]) => inst.general_topic === true);
|
|
8086
|
-
let generalsCreated = false;
|
|
8087
8389
|
// Collect unbound generals (no channel_id set) for auto-assignment
|
|
8088
8390
|
const unboundGenerals = generalInstances.filter(([, inst]) => !inst.channel_id);
|
|
8089
8391
|
// Track which adapters still need a general
|
|
@@ -8143,9 +8445,16 @@ export class FleetManager {
|
|
|
8143
8445
|
}
|
|
8144
8446
|
// Phase 3: Create new generals for any remaining adapters
|
|
8145
8447
|
for (const need of needsGeneral) {
|
|
8146
|
-
|
|
8147
|
-
if (fleet.instances[name])
|
|
8448
|
+
let name = channelConfigs.length > 1 ? `general-${need.adapterId}` : GENERAL_NAME;
|
|
8449
|
+
if (fleet.instances[name]?.general_topic)
|
|
8148
8450
|
continue;
|
|
8451
|
+
if (fleet.instances[name]) {
|
|
8452
|
+
// M-2069: the name is an agent's that is not the General (adoptGeneralByName left it: it works in a project
|
|
8453
|
+
// of the user's) — the fleet still gets its General, under a free name, never none.
|
|
8454
|
+
const taken = name;
|
|
8455
|
+
name = freeGeneralName(fleet.instances, taken);
|
|
8456
|
+
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");
|
|
8457
|
+
}
|
|
8149
8458
|
this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
|
|
8150
8459
|
const generalDir = join(getAgendHome(), name);
|
|
8151
8460
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
@@ -9935,6 +10244,10 @@ export class FleetManager {
|
|
|
9935
10244
|
}))
|
|
9936
10245
|
return;
|
|
9937
10246
|
}
|
|
10247
|
+
// #2675: a reply to a posted question card is its typed answer (judged by who may talk to that instance), not a
|
|
10248
|
+
// message for the instance — which could not take one while its card is up anyway.
|
|
10249
|
+
if (await this.takeRemoteCardReply(msg))
|
|
10250
|
+
return;
|
|
9938
10251
|
// Access control — classic channels are open to all, others require an allowed
|
|
9939
10252
|
// user. This runs before the dedup claim, and is judged by the adapter that
|
|
9940
10253
|
// owns the topic rather than the one that happened to receive the message:
|
|
@@ -11326,7 +11639,7 @@ export class FleetManager {
|
|
|
11326
11639
|
if (!ipc || !fleetRequestId)
|
|
11327
11640
|
return;
|
|
11328
11641
|
const type = String(msg.type);
|
|
11329
|
-
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
|
|
11642
|
+
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" || OFFICE_PACK_IPC.has(type) ? "fleet_web_theme_response"
|
|
11330
11643
|
: type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
|
|
11331
11644
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
11332
11645
|
: type === "fleet_task" ? "fleet_task_response"
|
|
@@ -11381,7 +11694,10 @@ export class FleetManager {
|
|
|
11381
11694
|
|| type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
|
|
11382
11695
|
|| type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
|
|
11383
11696
|
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11384
|
-
|| type === "fleet_office_view" || type === "fleet_office_set"
|
|
11697
|
+
|| type === "fleet_office_view" || type === "fleet_office_set"
|
|
11698
|
+
// #2692: the skin packs (OFFICE_PACK_IPC), answered as web theme tools.
|
|
11699
|
+
|| type === "fleet_office_pack_check" || type === "fleet_office_pack_upload" || type === "fleet_office_pack_delete"
|
|
11700
|
+
|| type === "fleet_office_pack_restore" || type === "fleet_office_pack_list" || type === "fleet_office_set_style") {
|
|
11385
11701
|
this.handleWebTheme(name, msg);
|
|
11386
11702
|
return;
|
|
11387
11703
|
}
|
|
@@ -11403,7 +11719,8 @@ export class FleetManager {
|
|
|
11403
11719
|
? this.appearanceTools.handle(instanceName, tool, args)
|
|
11404
11720
|
: tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
|
|
11405
11721
|
: tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
|
|
11406
|
-
: this.
|
|
11722
|
+
: OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
|
|
11723
|
+
: this.webAppearance.handleTool(instanceName, tool, args);
|
|
11407
11724
|
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
11408
11725
|
}
|
|
11409
11726
|
catch (err) {
|
|
@@ -11841,6 +12158,8 @@ export class FleetManager {
|
|
|
11841
12158
|
return this.agentMods.handle(instance, tool, args);
|
|
11842
12159
|
if (tool === "office_view" || tool === "office_set")
|
|
11843
12160
|
return this.officeActs.handle(instance, tool, args); // #2578
|
|
12161
|
+
if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
|
|
12162
|
+
return this.officePackTools.handle(instance, tool, args); // #2692
|
|
11844
12163
|
return this.webAppearance.handleTool(instance, tool, args);
|
|
11845
12164
|
}
|
|
11846
12165
|
async handlePreviewEmojisHttp(instance, args) {
|
|
@@ -12279,12 +12598,21 @@ export class FleetManager {
|
|
|
12279
12598
|
normalizeOfficeStorage(this.fleetConfig);
|
|
12280
12599
|
explicitPatches = explicitPatches.flatMap(patch => {
|
|
12281
12600
|
const path = patch.path;
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12601
|
+
// #2601 (and #2692's office_packs): a separately kept feature is written under its own key, never in `dashboard`.
|
|
12602
|
+
const separate = path.at(-2) === "dashboard" ? SEPARATE_FEATURE_KEYS[path.at(-1)] : undefined;
|
|
12603
|
+
if (separate && !patch.remove) {
|
|
12604
|
+
return [{ ...patch, path: [...path.slice(0, -2), separate] }, { path, remove: true }];
|
|
12605
|
+
}
|
|
12606
|
+
if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.keys(SEPARATE_FEATURE_KEYS).some(f => Object.hasOwn(patch.value, f))) {
|
|
12607
|
+
const dashboard = { ...patch.value }, moved = [];
|
|
12608
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12609
|
+
if (!Object.hasOwn(dashboard, feature))
|
|
12610
|
+
continue;
|
|
12611
|
+
const value = dashboard[feature];
|
|
12612
|
+
delete dashboard[feature];
|
|
12613
|
+
moved.push({ path: [...path.slice(0, -1), key], value, remove: value === null });
|
|
12614
|
+
}
|
|
12615
|
+
return [{ ...patch, value: dashboard }, ...moved];
|
|
12288
12616
|
}
|
|
12289
12617
|
// #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
|
|
12290
12618
|
if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
|
|
@@ -12398,11 +12726,11 @@ export class FleetManager {
|
|
|
12398
12726
|
const previous = daemon.getConfigSnapshot?.();
|
|
12399
12727
|
if (!next || !previous)
|
|
12400
12728
|
continue;
|
|
12401
|
-
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
|
|
12729
|
+
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access && previous.office_pack_access === next.office_pack_access)
|
|
12402
12730
|
continue;
|
|
12403
12731
|
// Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
|
|
12404
12732
|
// 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 });
|
|
12733
|
+
daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null, office_pack_access: next.office_pack_access ?? null });
|
|
12406
12734
|
if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
|
|
12407
12735
|
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
12408
12736
|
}
|
|
@@ -12436,7 +12764,7 @@ export class FleetManager {
|
|
|
12436
12764
|
logConfigWarningsAtStartup() {
|
|
12437
12765
|
let warnings = [];
|
|
12438
12766
|
try {
|
|
12439
|
-
warnings = validateFleetConfig(this.rawFleetConfig).warnings;
|
|
12767
|
+
warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
|
|
12440
12768
|
}
|
|
12441
12769
|
catch (err) {
|
|
12442
12770
|
this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
|
|
@@ -12467,11 +12795,13 @@ export class FleetManager {
|
|
|
12467
12795
|
const patches = [];
|
|
12468
12796
|
const raw = this.rawFleetConfig;
|
|
12469
12797
|
for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12798
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12799
|
+
if (!item?.dashboard || !Object.hasOwn(item.dashboard, feature))
|
|
12800
|
+
continue;
|
|
12801
|
+
patches.push({ path: [...path, "dashboard", feature], remove: true });
|
|
12802
|
+
if (!Object.hasOwn(item, key) && typeof item.dashboard[feature] === "boolean")
|
|
12803
|
+
patches.push({ path: [...path, key], value: item.dashboard[feature] });
|
|
12804
|
+
}
|
|
12475
12805
|
}
|
|
12476
12806
|
this.saveFleetConfig(patches);
|
|
12477
12807
|
this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
|
|
@@ -12706,6 +13036,7 @@ export class FleetManager {
|
|
|
12706
13036
|
this.mods.forgetInstance(name);
|
|
12707
13037
|
this.agentMods.forget(name);
|
|
12708
13038
|
this.officeActsInstance?.forget(name);
|
|
13039
|
+
this.officePackToolsInstance?.forget(name);
|
|
12709
13040
|
}
|
|
12710
13041
|
catch (err) {
|
|
12711
13042
|
this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
|
|
@@ -14073,7 +14404,7 @@ export class FleetManager {
|
|
|
14073
14404
|
return;
|
|
14074
14405
|
const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
|
|
14075
14406
|
const projects = board.listProjects(true);
|
|
14076
|
-
const stats = board.projectStats();
|
|
14407
|
+
const stats = board.projectStats(this.dispatcherGone);
|
|
14077
14408
|
const names = new Map(projects.map((p) => [p.id, p.name]));
|
|
14078
14409
|
const configured = this.getPrimaryAdapterId();
|
|
14079
14410
|
const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
|
|
@@ -14337,7 +14668,7 @@ export class FleetManager {
|
|
|
14337
14668
|
entry.web = {
|
|
14338
14669
|
text: web.text,
|
|
14339
14670
|
...(links ? { links } : {}),
|
|
14340
|
-
actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
|
|
14671
|
+
actions: [...opts.choices.map(c => ({ id: c.action, label: c.label })), ...(opts.webInput ? [{ id: opts.webInput.action, label: opts.webInput.label, input: true }] : [])],
|
|
14341
14672
|
expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
|
|
14342
14673
|
};
|
|
14343
14674
|
this.emitSseEvent("prompt", { instance: entry.instanceName, nonce, ...entry.web });
|
|
@@ -14514,6 +14845,10 @@ export class FleetManager {
|
|
|
14514
14845
|
return true;
|
|
14515
14846
|
if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
|
|
14516
14847
|
return true;
|
|
14848
|
+
if (await this.handleKiroReconcileCallback(data, adapterId, adapter))
|
|
14849
|
+
return true;
|
|
14850
|
+
if (await this.handleOfficePackCallback(data, adapterId, adapter))
|
|
14851
|
+
return true;
|
|
14517
14852
|
if (await this.handleDashboardCallback(data, adapterId, adapter))
|
|
14518
14853
|
return true;
|
|
14519
14854
|
if (await this.handleClassicApproval(data, adapterId, adapter))
|
|
@@ -14530,6 +14865,8 @@ export class FleetManager {
|
|
|
14530
14865
|
return true;
|
|
14531
14866
|
if (await this.handleInteractivePromptAssist(data, adapterId, adapter))
|
|
14532
14867
|
return true;
|
|
14868
|
+
if (await this.handleRemoteCardClick(data, adapterId, adapter))
|
|
14869
|
+
return true;
|
|
14533
14870
|
if (await this.handleClassicBackendSelection(data))
|
|
14534
14871
|
return true;
|
|
14535
14872
|
if (await this.handleModelSelection(data, adapterId))
|
|
@@ -14706,6 +15043,34 @@ export class FleetManager {
|
|
|
14706
15043
|
messages: messages.filter(m => keep(m.instance)),
|
|
14707
15044
|
});
|
|
14708
15045
|
}
|
|
15046
|
+
/** #2661: the Office levels as last worked out, and the work in flight (officeLevels). */
|
|
15047
|
+
officeLevelsMemo = null;
|
|
15048
|
+
officeLevelsRun = null;
|
|
15049
|
+
/**
|
|
15050
|
+
* #2661: every instance's Office level (office-levels.ts) — worked out by the cache service from the usage store, at
|
|
15051
|
+
* most once per OFFICE_LEVELS_TTL_MS however many pages and share links ask (one run at a time). Null when
|
|
15052
|
+
* web.office_levels is false.
|
|
15053
|
+
*/
|
|
15054
|
+
async officeLevels() {
|
|
15055
|
+
if (this.fleetConfig?.web?.office_levels === false)
|
|
15056
|
+
return null;
|
|
15057
|
+
const { OFFICE_LEVELS_TTL_MS } = await import("./office-levels.js");
|
|
15058
|
+
const memo = this.officeLevelsMemo;
|
|
15059
|
+
if (memo && performance.now() - memo.at < OFFICE_LEVELS_TTL_MS)
|
|
15060
|
+
return memo.levels;
|
|
15061
|
+
this.officeLevelsRun ??= this.cacheServiceFor().officeLevels(Date.now()).then((levels) => { this.officeLevelsMemo = { at: performance.now(), levels }; return levels; }).finally(() => { this.officeLevelsRun = null; });
|
|
15062
|
+
return this.officeLevelsRun;
|
|
15063
|
+
}
|
|
15064
|
+
/**
|
|
15065
|
+
* #2661: the levels as last worked out, for a synchronous reader (a share link's gateway): never waits; a stale or
|
|
15066
|
+
* missing set starts the next run. Null when web.office_levels is false or none has been worked out yet.
|
|
15067
|
+
*/
|
|
15068
|
+
officeLevelsSnapshot() {
|
|
15069
|
+
if (this.fleetConfig?.web?.office_levels === false)
|
|
15070
|
+
return null;
|
|
15071
|
+
void this.officeLevels().catch(() => { });
|
|
15072
|
+
return this.officeLevelsMemo?.levels ?? null;
|
|
15073
|
+
}
|
|
14709
15074
|
/** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
|
|
14710
15075
|
cacheReportPublished(window) {
|
|
14711
15076
|
return this.cacheServiceFor().published(cacheWindow(window));
|
|
@@ -15003,8 +15368,12 @@ export class FleetManager {
|
|
|
15003
15368
|
const buttons = this.replyButtons();
|
|
15004
15369
|
return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
|
|
15005
15370
|
}
|
|
15006
|
-
async clickWebPrompt(instance, nonce, action) {
|
|
15007
|
-
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-
|
|
15371
|
+
async clickWebPrompt(instance, nonce, action, text) {
|
|
15372
|
+
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z0-9-]{0,23}$/.test(action))
|
|
15373
|
+
return { status: 400, error: "Malformed prompt answer" };
|
|
15374
|
+
// #2675: only an action the page shows a text box for takes text, and it must say something.
|
|
15375
|
+
const input = this.pendingNonceButtons.get(nonce)?.web?.actions.find(a => a.id === action)?.input === true;
|
|
15376
|
+
if (input !== (text !== undefined) || (input && (!text.trim() || text.length > REMOTE_CARD_TEXT_MAX)))
|
|
15008
15377
|
return { status: 400, error: "Malformed prompt answer" };
|
|
15009
15378
|
const entry = this.pendingNonceButtons.get(nonce);
|
|
15010
15379
|
// Unknown is the same answer as answered or expired: the page drops the buttons either way.
|
|
@@ -15026,6 +15395,8 @@ export class FleetManager {
|
|
|
15026
15395
|
notice = n; },
|
|
15027
15396
|
};
|
|
15028
15397
|
this.webPromptClicks.add(data);
|
|
15398
|
+
if (text !== undefined)
|
|
15399
|
+
this.webPromptTexts.set(data, text);
|
|
15029
15400
|
await this.dispatchAdapterCallback(data, entry.adapterId, entry.adapter);
|
|
15030
15401
|
if (this.webPromptClaims.has(data))
|
|
15031
15402
|
return { status: 200 };
|
|
@@ -16083,6 +16454,163 @@ export class FleetManager {
|
|
|
16083
16454
|
}
|
|
16084
16455
|
return true;
|
|
16085
16456
|
}
|
|
16457
|
+
// ── #2675: a card a CLI shows in place of its composer, answered from the chat ─────────────────────────────────
|
|
16458
|
+
/**
|
|
16459
|
+
* Where an instance's card is put to people: its own place — its topic, or its ClassicBot room — or, on a fleet with
|
|
16460
|
+
* no chat platform, the dashboard. Never General: the answer is the instance's conversation.
|
|
16461
|
+
*/
|
|
16462
|
+
remoteCardPlace(instanceName) {
|
|
16463
|
+
const web = this.webOnlyPromptPlace();
|
|
16464
|
+
if (web)
|
|
16465
|
+
return web;
|
|
16466
|
+
const room = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
16467
|
+
if (room) {
|
|
16468
|
+
const adapterId = this.classicChannels?.getAdapterIdByInstance(instanceName) ?? this.getPrimaryAdapterId();
|
|
16469
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16470
|
+
return adapter && adapterId ? { adapter, adapterId, chatId: room } : null;
|
|
16471
|
+
}
|
|
16472
|
+
const adapterId = this.getInstanceAdapterId(instanceName);
|
|
16473
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16474
|
+
const chatId = this.getGroupIdForInstance(instanceName);
|
|
16475
|
+
const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
16476
|
+
if (!adapter || !adapterId || !chatId || topicId == null)
|
|
16477
|
+
return null;
|
|
16478
|
+
return { adapter, adapterId, chatId, threadId: String(topicId) };
|
|
16479
|
+
}
|
|
16480
|
+
/**
|
|
16481
|
+
* The daemon saw a card held still on `instanceName`'s screen (remote_card): put it to the people who talk to that
|
|
16482
|
+
* instance, with a button per answer. A card already posted for the instance is retired first (the daemon said it
|
|
16483
|
+
* was gone; this is the fallback).
|
|
16484
|
+
*/
|
|
16485
|
+
async postRemoteCard(instanceName, card) {
|
|
16486
|
+
this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
|
|
16487
|
+
const place = this.remoteCardPlace(instanceName);
|
|
16488
|
+
if (!place) {
|
|
16489
|
+
this.logger.warn({ instanceName }, "A card is waiting on screen but the instance has no place to post it — the parked notice stands");
|
|
16490
|
+
return;
|
|
16491
|
+
}
|
|
16492
|
+
const prompt = remoteCardPrompt(instanceName, card);
|
|
16493
|
+
const nonce = await this.postNonceButtonPrompt({
|
|
16494
|
+
prefix: REMOTE_CARD_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName, ...place,
|
|
16495
|
+
message: prompt.message, choices: prompt.choices, webInput: prompt.webInput,
|
|
16496
|
+
expiredText: t("card.expired", instanceName), timeoutMs: REMOTE_CARD_TIMEOUT_MS,
|
|
16497
|
+
extra: { remoteCard: { identity: card.identity, kind: card.kind, options: card.kind === "question" ? card.options.map(o => ({ n: o.n, title: o.title })) : [] } },
|
|
16498
|
+
});
|
|
16499
|
+
if (nonce)
|
|
16500
|
+
this.remoteCardPosts.set(instanceName, nonce);
|
|
16501
|
+
}
|
|
16502
|
+
/**
|
|
16503
|
+
* The card is gone from the screen (remote_card_gone, `identity`; null: whichever is posted): its buttons collapse to
|
|
16504
|
+
* `text` if nobody answered them here.
|
|
16505
|
+
*/
|
|
16506
|
+
retireRemoteCard(instanceName, identity, text = t("card.answered_in_pane")) {
|
|
16507
|
+
const nonce = this.remoteCardPosts.get(instanceName);
|
|
16508
|
+
if (!nonce)
|
|
16509
|
+
return;
|
|
16510
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
16511
|
+
if (entry && identity !== null && entry.remoteCard?.identity !== identity)
|
|
16512
|
+
return;
|
|
16513
|
+
this.remoteCardPosts.delete(instanceName);
|
|
16514
|
+
if (!entry)
|
|
16515
|
+
return;
|
|
16516
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16517
|
+
if (entry.timer)
|
|
16518
|
+
clearTimeout(entry.timer);
|
|
16519
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
16520
|
+
}
|
|
16521
|
+
/** Who may answer an instance's card: whoever may already talk to it there (a ClassicBot room is open to all). */
|
|
16522
|
+
mayAnswerRemoteCard(userId, entry) {
|
|
16523
|
+
if (this.classicChannels?.getChannelIdByInstance(entry.instanceName) === entry.chatId)
|
|
16524
|
+
return true;
|
|
16525
|
+
const am = this.worlds.get(entry.adapterId)?.accessManager ?? this.accessManager;
|
|
16526
|
+
return !am || am.isAllowed(userId);
|
|
16527
|
+
}
|
|
16528
|
+
/** A click on a card's button (platform or dashboard). */
|
|
16529
|
+
async handleRemoteCardClick(data, callbackAdapterId, receivingAdapter) {
|
|
16530
|
+
if (!data.callbackData.startsWith(REMOTE_CARD_CALLBACK_PREFIX))
|
|
16531
|
+
return false;
|
|
16532
|
+
// A typed answer from the dashboard must come with its text; refused before the claim, so the buttons stay.
|
|
16533
|
+
const match = REMOTE_CARD_CALLBACK_RE.exec(data.callbackData);
|
|
16534
|
+
if (match?.[2] === "text" && remoteCardAction("text", this.webPromptTexts.get(data)) === null) {
|
|
16535
|
+
data.ack?.(t("buttons.stale_notice"));
|
|
16536
|
+
return true;
|
|
16537
|
+
}
|
|
16538
|
+
const claimed = this.consumeNonceCallback(REMOTE_CARD_CALLBACK_PREFIX, REMOTE_CARD_CALLBACK_RE, data, callbackAdapterId, receivingAdapter, undefined, (userId, entry) => this.mayAnswerRemoteCard(userId, entry));
|
|
16539
|
+
if (claimed === null)
|
|
16540
|
+
return false;
|
|
16541
|
+
if (claimed === "consumed")
|
|
16542
|
+
return true;
|
|
16543
|
+
const action = remoteCardAction(claimed.action, this.webPromptTexts.get(data));
|
|
16544
|
+
const who = this.webPromptClicks.has(data) ? t("card.someone_on_dashboard") : (data.username ?? data.userId ?? "?");
|
|
16545
|
+
await this.answerRemoteCardFor(claimed.entry, action, who, data.messageId, data.editClicked);
|
|
16546
|
+
return true;
|
|
16547
|
+
}
|
|
16548
|
+
/**
|
|
16549
|
+
* An inbound message that replies to a posted question card, by someone who may answer it: its text is the typed
|
|
16550
|
+
* answer, and the message goes no further (true). Another bot's copy of a reply already taken is dropped (true).
|
|
16551
|
+
*/
|
|
16552
|
+
async takeRemoteCardReply(msg) {
|
|
16553
|
+
if (msg.isBotMessage || !msg.replyTo)
|
|
16554
|
+
return false;
|
|
16555
|
+
const now = performance.now();
|
|
16556
|
+
for (const [key, until] of this.takenCardReplies)
|
|
16557
|
+
if (until <= now)
|
|
16558
|
+
this.takenCardReplies.delete(key);
|
|
16559
|
+
const key = `${msg.chatId}|${msg.messageId}`;
|
|
16560
|
+
if (this.takenCardReplies.has(key))
|
|
16561
|
+
return true;
|
|
16562
|
+
for (const [nonce, entry] of this.pendingNonceButtons) {
|
|
16563
|
+
if (entry.prefix !== REMOTE_CARD_CALLBACK_PREFIX || entry.remoteCard?.kind !== "question")
|
|
16564
|
+
continue;
|
|
16565
|
+
// By place, whichever of the fleet's bots received the reply (a sibling's copy can arrive first, #2680 review), on
|
|
16566
|
+
// the post's own platform; who may answer is judged by the post's world (mayAnswerRemoteCard), as Needs you does.
|
|
16567
|
+
if (entry.chatId !== msg.chatId || entry.messageId !== msg.replyTo || entry.adapter.type !== msg.source)
|
|
16568
|
+
continue;
|
|
16569
|
+
if (!msg.userId || !this.mayAnswerRemoteCard(msg.userId, entry))
|
|
16570
|
+
return false; // not theirs to answer: it is just a message
|
|
16571
|
+
const action = remoteCardAction("text", msg.text ?? "");
|
|
16572
|
+
if (!action)
|
|
16573
|
+
return false;
|
|
16574
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16575
|
+
if (entry.timer)
|
|
16576
|
+
clearTimeout(entry.timer);
|
|
16577
|
+
this.webPromptGone(entry);
|
|
16578
|
+
this.takenCardReplies.set(key, now + REMOTE_CARD_TAKEN_TTL_MS);
|
|
16579
|
+
await this.answerRemoteCardFor(entry, action, msg.username ?? msg.userId, entry.messageId ?? msg.replyTo);
|
|
16580
|
+
return true;
|
|
16581
|
+
}
|
|
16582
|
+
return false;
|
|
16583
|
+
}
|
|
16584
|
+
/**
|
|
16585
|
+
* Key a claimed answer into the instance's card (the daemon checks it is still that card and walks a verified
|
|
16586
|
+
* cursor), then collapse the post to what happened. When the card is still waiting and was not answered — refused
|
|
16587
|
+
* or stopped part-way — it is posted again, so it can still be answered from here.
|
|
16588
|
+
*/
|
|
16589
|
+
async answerRemoteCardFor(entry, action, who, messageId, editClicked) {
|
|
16590
|
+
if (this.remoteCardPosts.get(entry.instanceName) === entry.nonce)
|
|
16591
|
+
this.remoteCardPosts.delete(entry.instanceName);
|
|
16592
|
+
const daemon = this.daemons.get(entry.instanceName);
|
|
16593
|
+
const card = entry.remoteCard;
|
|
16594
|
+
let outcome;
|
|
16595
|
+
if (!card || !action)
|
|
16596
|
+
outcome = { outcome: "refused", reason: "malformed" };
|
|
16597
|
+
else if (!daemon?.answerRemoteCard)
|
|
16598
|
+
outcome = { outcome: "stale" };
|
|
16599
|
+
else {
|
|
16600
|
+
try {
|
|
16601
|
+
outcome = await daemon.answerRemoteCard(card.identity, action);
|
|
16602
|
+
}
|
|
16603
|
+
catch (err) {
|
|
16604
|
+
this.logger.warn({ err: err?.message, instanceName: entry.instanceName }, "Answering a card from the chat failed");
|
|
16605
|
+
outcome = { outcome: "stopped", step: "error" };
|
|
16606
|
+
}
|
|
16607
|
+
}
|
|
16608
|
+
this.eventLog?.insert(entry.instanceName, "remote_card_answer", { action: action?.kind ?? null, outcome: outcome.outcome });
|
|
16609
|
+
const choice = card && action ? remoteCardChoiceLabel(card, action) : "?";
|
|
16610
|
+
await this.retireNonceButtons(entry, messageId, remoteCardOutcomeText(entry.instanceName, who, choice, outcome), editClicked);
|
|
16611
|
+
if (outcome.outcome === "refused" || outcome.outcome === "stopped")
|
|
16612
|
+
daemon?.repostRemoteCard?.();
|
|
16613
|
+
}
|
|
16086
16614
|
// ── Cancel button ────────────────────────────────────────────────────
|
|
16087
16615
|
// Sent after delivering a user message to an instance; clicking it (or
|
|
16088
16616
|
// /cancel) sends Escape to the instance's pane to interrupt generation.
|