@songsid/agend 2.3.2-beta.4 → 2.3.2-beta.6
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/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/antigravity-usage-scan.d.ts +58 -0
- package/dist/antigravity-usage-scan.js +257 -0
- package/dist/antigravity-usage-scan.js.map +1 -0
- package/dist/antigravity-usage-worker.d.ts +1 -0
- package/dist/antigravity-usage-worker.js +10 -0
- package/dist/antigravity-usage-worker.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +45 -0
- package/dist/backend/kiro-identity.js +74 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +15 -0
- package/dist/backend/kiro.js +42 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-signin-guide.d.ts +1 -0
- package/dist/backend-signin-guide.js +9 -0
- package/dist/backend-signin-guide.js.map +1 -0
- package/dist/backend-signin-pending.d.ts +10 -0
- package/dist/backend-signin-pending.js +44 -0
- package/dist/backend-signin-pending.js.map +1 -0
- package/dist/browser-backend-signins.d.ts +56 -0
- package/dist/browser-backend-signins.js +189 -0
- package/dist/browser-backend-signins.js.map +1 -0
- package/dist/cache-ledger.d.ts +38 -2
- package/dist/cache-ledger.js +133 -8
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-prices.d.ts +2 -1
- package/dist/cache-prices.js +5 -2
- package/dist/cache-prices.js.map +1 -1
- package/dist/cache-service.d.ts +68 -0
- package/dist/cache-service.js +355 -13
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +50 -8
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +21 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +25 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -1
- package/dist/cpu-quota.d.ts +23 -0
- package/dist/cpu-quota.js +86 -0
- package/dist/cpu-quota.js.map +1 -0
- package/dist/daemon.js +6 -3
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +56 -5
- package/dist/fleet-manager.js +387 -144
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +20 -4
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +20 -4
- package/dist/instance-lifecycle.d.ts +9 -0
- package/dist/instance-lifecycle.js +27 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +5 -0
- package/dist/kiro-db-reader.js +18 -0
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +9 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-migrate.d.ts +49 -0
- package/dist/kiro-migrate.js +105 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +2 -0
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/kiro-transcript-worker.js +1 -0
- package/dist/kiro-transcript-worker.js.map +1 -1
- package/dist/kiro-usage-run.d.ts +7 -0
- package/dist/kiro-usage-run.js +39 -0
- package/dist/kiro-usage-run.js.map +1 -0
- package/dist/kiro-usage-scan.d.ts +49 -0
- package/dist/kiro-usage-scan.js +223 -0
- package/dist/kiro-usage-scan.js.map +1 -0
- package/dist/kiro-usage-worker.d.ts +1 -0
- package/dist/kiro-usage-worker.js +10 -0
- package/dist/kiro-usage-worker.js.map +1 -0
- package/dist/kiro-usage.d.ts +58 -0
- package/dist/kiro-usage.js +310 -0
- package/dist/kiro-usage.js.map +1 -0
- package/dist/locale.js +78 -2
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/login-url.d.ts +4 -0
- package/dist/login-url.js +22 -0
- package/dist/login-url.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +8 -2
- package/dist/needs-you.js +6 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/notice-delivery.d.ts +7 -6
- package/dist/notice-delivery.js +16 -18
- package/dist/notice-delivery.js.map +1 -1
- package/dist/office-share-page.d.ts +13 -0
- package/dist/office-share-page.js +82 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +89 -0
- package/dist/office-share-service.js +172 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +228 -0
- package/dist/office-share.js +489 -0
- package/dist/office-share.js.map +1 -0
- package/dist/opencode-usage-scan.d.ts +28 -0
- package/dist/opencode-usage-scan.js +92 -0
- package/dist/opencode-usage-scan.js.map +1 -0
- package/dist/opencode-usage-worker.d.ts +1 -0
- package/dist/opencode-usage-worker.js +10 -0
- package/dist/opencode-usage-worker.js.map +1 -0
- package/dist/outbound-handlers.js +22 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +4 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +6 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.js +13 -2
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.js +2 -3
- package/dist/quickstart.js.map +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +7 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +125 -7
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-signin-assets.d.ts +2 -0
- package/dist/setup-signin-assets.js +6 -0
- package/dist/setup-signin-assets.js.map +1 -0
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/structured-signin-transport.d.ts +8 -0
- package/dist/structured-signin-transport.js +79 -0
- package/dist/structured-signin-transport.js.map +1 -0
- package/dist/structured-signin.d.ts +131 -0
- package/dist/structured-signin.js +388 -0
- package/dist/structured-signin.js.map +1 -0
- package/dist/tool-permissions.js +2 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +4 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +26 -2
- package/dist/transcript-sources.js +87 -40
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +15 -5
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/fleet-office-pack.js +7 -2
- package/dist/ui/fleet-office-skin.js +5 -4
- package/dist/ui/fleet-office-zones.js +17 -6
- package/dist/ui/fleet-office.js +20 -10
- package/dist/ui/fleet-usage.js +19 -15
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +116 -0
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +4 -4
- package/dist/ui/panel-settings.js +12 -8
- package/dist/ui/settings-registry.js +2 -2
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app.css +29 -0
- package/dist/ui/structured-signin.js +115 -0
- package/dist/usage-report.d.ts +20 -0
- package/dist/usage-report.js +54 -17
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +9 -1
- package/dist/usage-rollup.js +20 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +15 -0
- package/dist/web-api.js +190 -0
- 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-backend-login.d.ts +1 -7
- package/dist/web-backend-login.js +2 -24
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-office-skins.d.ts +2 -1
- package/dist/web-office-skins.js +2 -1
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-settings-patch.js +1 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/working-recovery.d.ts +2 -2
- package/dist/working-recovery.js +43 -8
- package/dist/working-recovery.js.map +1 -1
- package/dist/working-state-journal.d.ts +2 -0
- package/dist/working-state-journal.js +16 -1
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +2 -1
- package/templates/windows-shortcuts.ps1 +7 -5
package/dist/fleet-manager.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
2
|
-
import { loginNoticeMode,
|
|
2
|
+
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
3
3
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
4
4
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
5
5
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
@@ -22,6 +22,9 @@ import { PublicLinkAutoOpen } from "./public-link-auto-open.js";
|
|
|
22
22
|
import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from "./public-link-owner.js";
|
|
23
23
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
24
24
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
25
|
+
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
26
|
+
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
27
|
+
import { ALL_OFFICES } from "./office-share.js";
|
|
25
28
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
26
29
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
27
30
|
import { withinBudget } from "./monotonic-budget.js";
|
|
@@ -31,13 +34,16 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
31
34
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, lstatSync, constants as fsConstants, closeSync } from "node:fs";
|
|
32
35
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
33
36
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
34
|
-
import { freemem, totalmem,
|
|
37
|
+
import { freemem, totalmem, homedir } from "node:os";
|
|
38
|
+
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
35
39
|
import { createServer } from "node:http";
|
|
36
40
|
import { Worker } from "node:worker_threads";
|
|
37
41
|
import { ProbeWorkerPool } from "./probe-worker-pool.js";
|
|
38
42
|
import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as resolvePath } from "node:path";
|
|
39
43
|
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { isDeepStrictEqual } from "node:util";
|
|
40
45
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
46
|
+
import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
|
|
41
47
|
import { fleetLabel } from "./fleet-label.js";
|
|
42
48
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
43
49
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
@@ -56,7 +62,7 @@ const __dirname = dirname(__filename);
|
|
|
56
62
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
57
63
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
58
64
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
59
|
-
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
65
|
+
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
60
66
|
import { EventLog } from "./event-log.js";
|
|
61
67
|
import { binaryProbe } from "./binary-probe.js";
|
|
62
68
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
|
|
@@ -193,7 +199,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
|
|
|
193
199
|
import { parseEnvText } from "./env-file.js";
|
|
194
200
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
195
201
|
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
196
|
-
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
202
|
+
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
197
203
|
import { LoginSession } from "./login-manager.js";
|
|
198
204
|
import { runOnLoginShell } from "./login-shell.js";
|
|
199
205
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
@@ -213,6 +219,10 @@ import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOG
|
|
|
213
219
|
import { WebBackendLogins, dashboardSignInSupported } from "./web-backend-login.js";
|
|
214
220
|
import { runBeforeDeadline } from "./deadline.js";
|
|
215
221
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
222
|
+
import { BrowserBackendSignIns } from "./browser-backend-signins.js";
|
|
223
|
+
import { createStructuredSignInTransport } from "./structured-signin-transport.js";
|
|
224
|
+
import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin-pending.js";
|
|
225
|
+
import { structuredSignInSupported } from "./structured-signin.js";
|
|
216
226
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
217
227
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
218
228
|
import { recentChatContext } from "./log-tail.js";
|
|
@@ -276,6 +286,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
276
286
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
277
287
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
278
288
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
289
|
+
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
279
290
|
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
280
291
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
281
292
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
@@ -473,6 +484,10 @@ const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
|
473
484
|
const NEEDS_PAUSE_TTL_MS = 10_000;
|
|
474
485
|
const HANG_CALLBACK_PREFIX = "hang:";
|
|
475
486
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
487
|
+
/** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
|
|
488
|
+
const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
|
|
489
|
+
/** Long enough for an admin to read the prompt; the move is judged again on the click. */
|
|
490
|
+
const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
476
491
|
/**
|
|
477
492
|
* The prompts the web dashboard also offers: the ones about an instance's own health, which a dashboard
|
|
478
493
|
* user — holding the full-fleet web credential — may answer exactly as a fleet admin on the platform may.
|
|
@@ -568,6 +583,7 @@ export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
|
|
|
568
583
|
* started ten CLIs together, saturated the CPU, and healthy starts then missed
|
|
569
584
|
* their startup budget — which used to cost the user their conversation. Cores
|
|
570
585
|
* bound how many can actually make progress; memory bounds how many fit.
|
|
586
|
+
* `cores` is what this process may use (cpuset and cgroup quota, #749), not the host's count.
|
|
571
587
|
*/
|
|
572
588
|
export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
573
589
|
const byMemory = Math.floor(freeMemMB / 300);
|
|
@@ -614,6 +630,7 @@ export class FleetManager {
|
|
|
614
630
|
workingRecovery = null;
|
|
615
631
|
workingRecoveryTimer = null;
|
|
616
632
|
workingRecoveryCause = "unknown_interruption";
|
|
633
|
+
workingRecoveryGroup = this.managerBootId;
|
|
617
634
|
recoveryEnabled(name) {
|
|
618
635
|
const own = this.fleetConfig?.instances[name]?.resume_interrupted_work;
|
|
619
636
|
return own ?? this.fleetConfig?.defaults?.resume_interrupted_work ?? true;
|
|
@@ -654,7 +671,16 @@ export class FleetManager {
|
|
|
654
671
|
const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
|
|
655
672
|
&& (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
|
|
656
673
|
.map(row => `${row.instance} (${row.reason})`);
|
|
657
|
-
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.`));
|
|
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
|
+
}
|
|
658
684
|
},
|
|
659
685
|
log: (episode, outcome, reason) => {
|
|
660
686
|
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
@@ -885,6 +911,8 @@ export class FleetManager {
|
|
|
885
911
|
pendingBackendSelects = new Map();
|
|
886
912
|
/** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
|
|
887
913
|
pendingNonceButtons = new Map();
|
|
914
|
+
/** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
|
|
915
|
+
pendingKiroMigrations = new Map();
|
|
888
916
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
889
917
|
needsYou = null;
|
|
890
918
|
/** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
|
|
@@ -1151,6 +1179,51 @@ export class FleetManager {
|
|
|
1151
1179
|
officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
|
|
1152
1180
|
/** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
|
|
1153
1181
|
officeWrites = new OfficeWriteLimiter();
|
|
1182
|
+
officeSharesInstance;
|
|
1183
|
+
/**
|
|
1184
|
+
* #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
|
|
1185
|
+
* their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
|
|
1186
|
+
*/
|
|
1187
|
+
get officeShares() {
|
|
1188
|
+
return (this.officeSharesInstance ??= new OfficeShareService({
|
|
1189
|
+
source: {
|
|
1190
|
+
status: () => this.getUiStatus(),
|
|
1191
|
+
org: () => this.orgChart(),
|
|
1192
|
+
offices: () => this.officeStore.list(),
|
|
1193
|
+
acts: () => this.officeActs.live(),
|
|
1194
|
+
traffic: () => this.trafficSnapshot(),
|
|
1195
|
+
needs: () => this.needsYouItems().map(i => i.instance),
|
|
1196
|
+
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
1197
|
+
skins: this.officeSkins,
|
|
1198
|
+
},
|
|
1199
|
+
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
1200
|
+
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
1201
|
+
provider: binary => new CloudflaredProvider({ binaryName: binary, protocol: this.fleetConfig?.web?.public_link?.protocol ?? "http2" }),
|
|
1202
|
+
config: () => this.fleetConfig?.web?.office_share,
|
|
1203
|
+
officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
|
|
1204
|
+
ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
|
|
1205
|
+
changed: () => this.emitSseEvent("office_shares", {}),
|
|
1206
|
+
log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* #2636: a share made or ended — said in General, as a sign-in is (who, which office, for how long; never its URL).
|
|
1211
|
+
* `share` null with `count`: "Revoke all".
|
|
1212
|
+
*/
|
|
1213
|
+
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 minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
|
|
1216
|
+
const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
|
|
1217
|
+
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
1218
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1219
|
+
}
|
|
1220
|
+
/** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
|
|
1221
|
+
officeShareOwnerCurrent(owner) {
|
|
1222
|
+
const m = /^person:([^:]+):(.+)$/.exec(owner);
|
|
1223
|
+
if (!m)
|
|
1224
|
+
return owner === "admin-session";
|
|
1225
|
+
return this.hasFleetAdmins(m[1]) && this.isFleetAdmin(m[2], m[1]);
|
|
1226
|
+
}
|
|
1154
1227
|
officeActsInstance;
|
|
1155
1228
|
/** #2578: the agents' acts in the Office view (office_view / office_set), memory only; every change is `office_acts`. */
|
|
1156
1229
|
get officeActs() {
|
|
@@ -1723,6 +1796,74 @@ export class FleetManager {
|
|
|
1723
1796
|
// No platform confirmation was reachable. Host inspection is still required.
|
|
1724
1797
|
this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
|
|
1725
1798
|
}
|
|
1799
|
+
kiroMigrateDeps() {
|
|
1800
|
+
return {
|
|
1801
|
+
agendHome: getAgendHome(),
|
|
1802
|
+
fleetConfig: () => this.fleetConfig,
|
|
1803
|
+
saveFleetConfig: () => this.saveFleetConfig(),
|
|
1804
|
+
status: name => this.getInstanceStatus(name),
|
|
1805
|
+
restart: name => this.restartSingleInstance(name, { explicit: true }),
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* #2598 B2, migrate_kiro_instance: judge the move, then ask in General for a fleet admin's Confirm. Nothing changes
|
|
1810
|
+
* here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
|
|
1811
|
+
*/
|
|
1812
|
+
async requestKiroMigration(name, startFresh) {
|
|
1813
|
+
const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
1814
|
+
if (!judged.ok)
|
|
1815
|
+
return judged;
|
|
1816
|
+
const general = this.findGeneralInstance();
|
|
1817
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
1818
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
1819
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
1820
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
1821
|
+
if (!general || !adapterId || !adapter || !chatId)
|
|
1822
|
+
return { ok: false, error: t("kiro_migrate.no_general") };
|
|
1823
|
+
const { plan } = judged;
|
|
1824
|
+
try {
|
|
1825
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
1826
|
+
prefix: KIRO_MIGRATE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name, adapter, adapterId, chatId,
|
|
1827
|
+
...(topic ? { threadId: topic } : {}),
|
|
1828
|
+
message: kiroMigrationPrompt(plan, this.getInstanceStatus(name) === "running"),
|
|
1829
|
+
choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
|
|
1830
|
+
expiredText: t("kiro_migrate.expired", name),
|
|
1831
|
+
timeoutMs: KIRO_MIGRATE_TIMEOUT_MS,
|
|
1832
|
+
});
|
|
1833
|
+
this.pendingKiroMigrations.set(nonce, plan);
|
|
1834
|
+
const expiry = setTimeout(() => this.pendingKiroMigrations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000);
|
|
1835
|
+
expiry.unref?.();
|
|
1836
|
+
}
|
|
1837
|
+
catch (err) {
|
|
1838
|
+
return { ok: false, error: `${t("kiro_migrate.no_general")} (${err.message})` };
|
|
1839
|
+
}
|
|
1840
|
+
return { ok: true, text: t("kiro_migrate.requested"), classic_conversation: plan.classicId };
|
|
1841
|
+
}
|
|
1842
|
+
/** A fleet admin's Confirm or Cancel on a kiro move. Any fleet admin (the default nonce authorization), in General. */
|
|
1843
|
+
async handleKiroMigrateCallback(data, adapterId, adapter) {
|
|
1844
|
+
const claimed = this.consumeNonceCallback(KIRO_MIGRATE_CALLBACK_PREFIX, /^kiro-migrate:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
|
|
1845
|
+
if (claimed === null)
|
|
1846
|
+
return false;
|
|
1847
|
+
if (claimed === "consumed")
|
|
1848
|
+
return true;
|
|
1849
|
+
const { entry, action } = claimed;
|
|
1850
|
+
const nonce = data.callbackData.slice(KIRO_MIGRATE_CALLBACK_PREFIX.length, KIRO_MIGRATE_CALLBACK_PREFIX.length + 32);
|
|
1851
|
+
const plan = this.pendingKiroMigrations.get(nonce);
|
|
1852
|
+
this.pendingKiroMigrations.delete(nonce);
|
|
1853
|
+
const messageId = entry.messageId ?? data.messageId;
|
|
1854
|
+
if (action === "cancel" || !plan) {
|
|
1855
|
+
await this.retireNonceButtons(entry, messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
this.eventLog?.insert(plan.name, "kiro_migrate", { classicId: plan.classicId, userId: data.userId });
|
|
1859
|
+
await this.retireNonceButtons(entry, messageId, t("kiro_migrate.applying", plan.name));
|
|
1860
|
+
const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
|
|
1861
|
+
this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
|
|
1862
|
+
this.notifyFleetError(outcome.text);
|
|
1863
|
+
if (outcome.ok)
|
|
1864
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1726
1867
|
async handleSettingsChangeCallback(data, adapterId, adapter) {
|
|
1727
1868
|
const result = this.consumeNonceCallback("settings-confirm:", /^settings-confirm:([0-9a-f]{32}):(confirm|reject)$/, data, adapterId, adapter, undefined, (userId, entry) => entry.confirmationAdmin?.(userId) === true);
|
|
1728
1869
|
if (!result)
|
|
@@ -2269,7 +2410,7 @@ export class FleetManager {
|
|
|
2269
2410
|
if (reason === "cancelled")
|
|
2270
2411
|
outbox.workingRecovery.cancel(name, observation.configuration, observation.sessionId);
|
|
2271
2412
|
if (entry.prior)
|
|
2272
|
-
outbox.workingRecovery.discover(name, entry.prior);
|
|
2413
|
+
outbox.workingRecovery.discover(name, entry.prior, this.workingRecoveryGroup);
|
|
2273
2414
|
const state = entry.current.observation.executionState ?? "unknown";
|
|
2274
2415
|
const counts = { tasks: entry.current.references.filter(r => r.kind === "task").length,
|
|
2275
2416
|
owed: entry.current.references.filter(r => r.kind === "owed").length,
|
|
@@ -2576,7 +2717,7 @@ export class FleetManager {
|
|
|
2576
2717
|
const explicit = this.fleetConfig?.defaults?.startup?.concurrency;
|
|
2577
2718
|
if (explicit != null)
|
|
2578
2719
|
return Math.max(1, Math.min(20, explicit));
|
|
2579
|
-
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)),
|
|
2720
|
+
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), effectiveCpuCount());
|
|
2580
2721
|
}
|
|
2581
2722
|
/** #1685: "elevated" or "critical" while host memory pressure slows or holds new agents; otherwise null (an
|
|
2582
2723
|
* unknown macOS reading — the advisory case — is unknown, so null too). */
|
|
@@ -2793,8 +2934,9 @@ export class FleetManager {
|
|
|
2793
2934
|
// the next tunnel and says so, it does not block the fleet.
|
|
2794
2935
|
this.announceToolPermissionsChange();
|
|
2795
2936
|
this.tightenInstanceDirectories();
|
|
2796
|
-
// #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way
|
|
2797
|
-
|
|
2937
|
+
// #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way (and
|
|
2938
|
+
// #2636's Office share tunnel's).
|
|
2939
|
+
for (const leaseDir of [this.dataDir, join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR), join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR)]) {
|
|
2798
2940
|
void reapStaleTunnel(leaseDir)
|
|
2799
2941
|
.then(outcome => {
|
|
2800
2942
|
if (outcome.kind === "manual")
|
|
@@ -5448,36 +5590,40 @@ export class FleetManager {
|
|
|
5448
5590
|
* config file races. Stagger applies between outer lifecycle admissions.
|
|
5449
5591
|
* TODO: per-instance startup timeout (existing issue, not introduced here)
|
|
5450
5592
|
*/
|
|
5451
|
-
async startInstancesWithConcurrency(entries, topicMode, onReady) {
|
|
5593
|
+
async startInstancesWithConcurrency(entries, topicMode, onReady, classic = []) {
|
|
5452
5594
|
// Persisted pauses are intentionally preserved across fleet restarts. Filter
|
|
5453
5595
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
5454
5596
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
5455
5597
|
// full stagger slot for every distinct working directory.
|
|
5456
5598
|
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
5599
|
+
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
5600
|
+
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
5601
|
+
const fleetNames = new Set(entries.map(([name]) => name));
|
|
5602
|
+
const runnableClassic = classic.filter(ch => !this.lifecycle.isPaused(ch.instanceName) && !fleetNames.has(ch.instanceName));
|
|
5457
5603
|
const pausedCount = entries.length - runnableEntries.length;
|
|
5458
5604
|
if (pausedCount > 0) {
|
|
5459
5605
|
this.logger.info({ pausedCount }, "Paused instances excluded from startup queue");
|
|
5460
5606
|
}
|
|
5461
|
-
if (runnableEntries.length === 0)
|
|
5607
|
+
if (runnableEntries.length === 0 && runnableClassic.length === 0)
|
|
5462
5608
|
return;
|
|
5463
5609
|
if (this.fleetConfig?.defaults?.startup?.concurrency == null) {
|
|
5464
5610
|
this.logger.info({
|
|
5465
5611
|
concurrency: this.spawnConcurrency(),
|
|
5466
5612
|
freeMemMB: Math.round(freemem() / (1024 * 1024)),
|
|
5467
|
-
totalInstances: runnableEntries.length,
|
|
5613
|
+
totalInstances: runnableEntries.length + runnableClassic.length,
|
|
5468
5614
|
}, "Adaptive startup concurrency");
|
|
5469
5615
|
}
|
|
5470
5616
|
const startedAt = performance.now();
|
|
5471
5617
|
let active = 0, peakConcurrentStarts = 0, readyInstances = 0;
|
|
5472
|
-
|
|
5618
|
+
const admit = (name, workingDirectory, start) => this.spawnGate.run({
|
|
5473
5619
|
instanceName: name,
|
|
5474
|
-
workingDirectory
|
|
5620
|
+
workingDirectory,
|
|
5475
5621
|
reason: "startup",
|
|
5476
5622
|
stage: "lifecycle",
|
|
5477
5623
|
}, async () => {
|
|
5478
5624
|
peakConcurrentStarts = Math.max(peakConcurrentStarts, ++active);
|
|
5479
5625
|
try {
|
|
5480
|
-
if (await
|
|
5626
|
+
if (await start()) {
|
|
5481
5627
|
readyInstances++;
|
|
5482
5628
|
onReady?.(name);
|
|
5483
5629
|
}
|
|
@@ -5485,9 +5631,13 @@ export class FleetManager {
|
|
|
5485
5631
|
finally {
|
|
5486
5632
|
active--;
|
|
5487
5633
|
}
|
|
5488
|
-
})
|
|
5634
|
+
});
|
|
5635
|
+
await Promise.all([
|
|
5636
|
+
...runnableEntries.map(([name, config]) => admit(name, config.working_directory, () => this.startInstanceUnattended(name, config, topicMode, "instance"))),
|
|
5637
|
+
...runnableClassic.map(ch => admit(ch.instanceName, join(getAgendHome(), "workspaces", ch.instanceName), () => this.startClassicInstanceUnattended(ch, "classic instance"))),
|
|
5638
|
+
]);
|
|
5489
5639
|
this.logger.info({
|
|
5490
|
-
totalInstances: runnableEntries.length, readyInstances,
|
|
5640
|
+
totalInstances: runnableEntries.length + runnableClassic.length, classicInstances: runnableClassic.length, readyInstances,
|
|
5491
5641
|
elapsedSeconds: Math.round((performance.now() - startedAt) / 100) / 10,
|
|
5492
5642
|
peakConcurrentStarts,
|
|
5493
5643
|
}, "Fleet startup batch completed");
|
|
@@ -6498,33 +6648,11 @@ export class FleetManager {
|
|
|
6498
6648
|
getPublicWebStatus() {
|
|
6499
6649
|
return this.publicWebLink?.status() ?? { state: "closed" };
|
|
6500
6650
|
}
|
|
6501
|
-
/** Notices use the first explicit admin of the owning connection; never a guest/paired account. */
|
|
6502
|
-
noticeAdmin(adapterId) {
|
|
6503
|
-
return this.adminListOf(adapterId)?.find(id => id.length > 0);
|
|
6504
|
-
}
|
|
6505
|
-
/** Best-effort private delivery. The caller retains its existing topic fallback and authority fences. */
|
|
6506
|
-
async tryNoticeDm(adapter, userId, text) {
|
|
6507
|
-
if (!userId || !adapter?.sendDirect)
|
|
6508
|
-
return false;
|
|
6509
|
-
try {
|
|
6510
|
-
await withinBudget(adapter.sendDirect(userId, text, { disablePreview: true }), performance.now() + 2_000);
|
|
6511
|
-
return true;
|
|
6512
|
-
}
|
|
6513
|
-
catch {
|
|
6514
|
-
this.logger.info({ adapterId: adapter.id }, "Fleet notice DM failed; falling back to the topic");
|
|
6515
|
-
return false;
|
|
6516
|
-
}
|
|
6517
|
-
}
|
|
6518
6651
|
async confirmPublicWebLogin(info, current) {
|
|
6519
6652
|
if (!current() || !this.publicOwnerCurrent(info.owner))
|
|
6520
6653
|
throw new Error("public owner unavailable");
|
|
6521
6654
|
const adapter = info.owner.binding;
|
|
6522
|
-
|
|
6523
|
-
if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, info.owner.userId, text)) {
|
|
6524
|
-
if (!current() || !this.publicOwnerCurrent(info.owner))
|
|
6525
|
-
throw new Error("public owner changed");
|
|
6526
|
-
await adapter.sendText(info.owner.chatId, text, { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
|
|
6527
|
-
}
|
|
6655
|
+
await adapter.sendText(info.owner.chatId, t("web.public_login_notice", info.label, info.handle.slice(0, 8)), { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
|
|
6528
6656
|
if (!current() || !this.publicOwnerCurrent(info.owner))
|
|
6529
6657
|
throw new Error("public owner changed");
|
|
6530
6658
|
}
|
|
@@ -6542,12 +6670,7 @@ export class FleetManager {
|
|
|
6542
6670
|
if (!open)
|
|
6543
6671
|
throw new Error("guest public sign-in unavailable");
|
|
6544
6672
|
const adapter = open.owner.binding;
|
|
6545
|
-
|
|
6546
|
-
if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, open.owner.userId, text)) {
|
|
6547
|
-
if (!admitted())
|
|
6548
|
-
throw new Error("guest public sign-in changed");
|
|
6549
|
-
await adapter.sendText(open.owner.chatId, text, { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
|
|
6550
|
-
}
|
|
6673
|
+
await adapter.sendText(open.owner.chatId, t("web.guest_public_login_notice", this.webPerson(info.account).name, info.label, this.guestScope(info.account).size, info.handle.slice(0, 8)), { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
|
|
6551
6674
|
if (!admitted())
|
|
6552
6675
|
throw new Error("guest public sign-in changed");
|
|
6553
6676
|
}
|
|
@@ -7385,26 +7508,18 @@ export class FleetManager {
|
|
|
7385
7508
|
}
|
|
7386
7509
|
/** Called by the sign-in endpoint: a login the operator did not make should be visible to them. */
|
|
7387
7510
|
onWebLogin(info) {
|
|
7388
|
-
//
|
|
7389
|
-
|
|
7390
|
-
if (
|
|
7391
|
-
|
|
7392
|
-
const text = guest
|
|
7393
|
-
? t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8))
|
|
7394
|
-
: t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8));
|
|
7395
|
-
if (loginNoticeMode(this.fleetConfig?.web) === "dm") {
|
|
7396
|
-
const adapterId = info.owner?.adapterId;
|
|
7397
|
-
const adapter = adapterId ? this.adapters.get(adapterId) : undefined;
|
|
7398
|
-
// A local guest's own account must never receive the admin's security notice.
|
|
7399
|
-
const userId = guest ? (adapterId ? this.noticeAdmin(adapterId) : undefined)
|
|
7400
|
-
: info.owner && this.isFleetAdmin(info.owner.userId, adapterId) ? info.owner.userId : undefined;
|
|
7401
|
-
void this.tryNoticeDm(adapter, userId, text).then(sent => { if (!sent)
|
|
7402
|
-
this.notifyFleetError(text); })
|
|
7403
|
-
.catch(() => this.logger.warn("Web sign-in notice could not be delivered"));
|
|
7511
|
+
// #1920 §2.3: a guest's sign-in is always said in General (web.notify_login is the operator's own sign-ins): who,
|
|
7512
|
+
// and how many agents they now reach — admins stay aware of everyone who is not one of them.
|
|
7513
|
+
if (info.tier === "guest" && info.owner) {
|
|
7514
|
+
this.notifyFleetError(t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8)));
|
|
7404
7515
|
return;
|
|
7405
7516
|
}
|
|
7406
|
-
//
|
|
7407
|
-
this.
|
|
7517
|
+
// Never by DM (hmsg-3b8ac2af: Telegram cannot DM a user who has not started the bot); a legacy "dm" reads as on.
|
|
7518
|
+
if (!loginNoticeMode(this.fleetConfig?.web))
|
|
7519
|
+
return;
|
|
7520
|
+
// The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
|
|
7521
|
+
// sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
|
|
7522
|
+
this.notifyFleetError(t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
|
|
7408
7523
|
}
|
|
7409
7524
|
getDashboardAccess() {
|
|
7410
7525
|
return { ready: this.healthServerListening, token: this.webToken };
|
|
@@ -7774,7 +7889,8 @@ export class FleetManager {
|
|
|
7774
7889
|
}
|
|
7775
7890
|
}
|
|
7776
7891
|
async startAllWithinDeadline(configPath, deadline) {
|
|
7777
|
-
this.
|
|
7892
|
+
if (!this.structuredLoginCleanupFailed)
|
|
7893
|
+
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
7778
7894
|
this.loginController?.reopen();
|
|
7779
7895
|
const startupStartedAt = Date.now();
|
|
7780
7896
|
FleetManager.signalTarget = this;
|
|
@@ -8035,12 +8151,7 @@ export class FleetManager {
|
|
|
8035
8151
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
8036
8152
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
8037
8153
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
8038
|
-
fleet.instances[name] =
|
|
8039
|
-
...DEFAULT_INSTANCE_CONFIG,
|
|
8040
|
-
working_directory: generalDir,
|
|
8041
|
-
general_topic: true,
|
|
8042
|
-
channel_id: need.adapterId,
|
|
8043
|
-
};
|
|
8154
|
+
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
8044
8155
|
generalsCreated = true;
|
|
8045
8156
|
}
|
|
8046
8157
|
if (generalsCreated)
|
|
@@ -8186,9 +8297,11 @@ export class FleetManager {
|
|
|
8186
8297
|
this.rotateAllInstanceLogs();
|
|
8187
8298
|
}, 24 * 60 * 60_000);
|
|
8188
8299
|
this.logRotateTimer.unref?.();
|
|
8189
|
-
// Phase 2: Start remaining instances with staggered concurrency
|
|
8190
|
-
|
|
8191
|
-
|
|
8300
|
+
// Phase 2: Start remaining instances, and the ClassicBot rooms with them, with staggered concurrency. A room needs no
|
|
8301
|
+
// chat connection to spawn: its open-list is set above (reregisterClassicChannels) and inbound queues until it is up.
|
|
8302
|
+
const classicAtBoot = topicMode && (fleet.channel || fleet.channels?.length) ? this.classicChannels?.getAll() ?? [] : [];
|
|
8303
|
+
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
8304
|
+
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
8192
8305
|
}
|
|
8193
8306
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
8194
8307
|
await adapterStartup;
|
|
@@ -8230,22 +8343,7 @@ export class FleetManager {
|
|
|
8230
8343
|
// IPC is already wired by startInstancesWithConcurrency → startInstance →
|
|
8231
8344
|
// connectIpcToInstance. The previous 3s sleep + connectToInstances loop
|
|
8232
8345
|
// was redundant.
|
|
8233
|
-
//
|
|
8234
|
-
if (this.classicChannels) {
|
|
8235
|
-
const fleetBackend = this.fleetConfig?.defaults?.backend;
|
|
8236
|
-
const channels = this.classicChannels.getAll()
|
|
8237
|
-
.filter(ch => !this.lifecycle.isPaused(ch.instanceName));
|
|
8238
|
-
const concurrency = 3;
|
|
8239
|
-
let idx = 0;
|
|
8240
|
-
while (idx < channels.length) {
|
|
8241
|
-
const batch = channels.slice(idx, idx + concurrency);
|
|
8242
|
-
await Promise.allSettled(batch.map(async (ch) => {
|
|
8243
|
-
if (await this.startClassicInstanceUnattended(ch, "classic instance"))
|
|
8244
|
-
startupProgress.markReady();
|
|
8245
|
-
}));
|
|
8246
|
-
idx += concurrency;
|
|
8247
|
-
}
|
|
8248
|
-
}
|
|
8346
|
+
// The ClassicBot rooms started in Phase 2, with the fleet.
|
|
8249
8347
|
for (const name of Object.keys(fleet.instances)) {
|
|
8250
8348
|
this.startStatuslineWatcher(name);
|
|
8251
8349
|
}
|
|
@@ -8512,25 +8610,43 @@ export class FleetManager {
|
|
|
8512
8610
|
if (channelConfigs.length === 0)
|
|
8513
8611
|
return;
|
|
8514
8612
|
// Start ALL adapters in parallel — any single failure doesn't block others.
|
|
8515
|
-
const
|
|
8613
|
+
const settled = channelConfigs.map(() => false);
|
|
8614
|
+
// Each connection is recorded (connected, or retrying with its retry scheduled) as its own login settles.
|
|
8615
|
+
const recorded = channelConfigs.map((cfg, i) => (i === 0
|
|
8516
8616
|
? this.startSingleAdapter(fleet, cfg)
|
|
8517
|
-
: this.startAdditionalAdapter(cfg)))
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
}
|
|
8617
|
+
: this.startAdditionalAdapter(cfg)).then(() => ({ status: "fulfilled" }), (reason) => ({ status: "rejected", reason }))
|
|
8618
|
+
.then(result => { settled[i] = true; this.recordAdapterStartup(cfg, result); })
|
|
8619
|
+
.catch(err => this.logger.error({ err }, "Recording a chat connection's startup failed")));
|
|
8620
|
+
// #2642: bounded. Startup continues past a connection still logging in after this long (ClassicBot instances,
|
|
8621
|
+
// READY); that connection is recorded, or retried, when its login settles, and joins the fleet as it would anyway.
|
|
8622
|
+
let timer;
|
|
8623
|
+
const inTime = await Promise.race([
|
|
8624
|
+
Promise.all(recorded).then(() => true),
|
|
8625
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), FleetManager.ADAPTER_STARTUP_WAIT_MS); timer.unref?.(); }),
|
|
8626
|
+
]).finally(() => clearTimeout(timer));
|
|
8627
|
+
if (!inTime) {
|
|
8628
|
+
this.logger.warn({
|
|
8629
|
+
pending: channelConfigs.filter((_, i) => !settled[i]).map(cfg => cfg.id ?? cfg.type),
|
|
8630
|
+
waitedMs: FleetManager.ADAPTER_STARTUP_WAIT_MS,
|
|
8631
|
+
}, "Chat connection still connecting — fleet startup continues without waiting for it");
|
|
8532
8632
|
}
|
|
8533
8633
|
}
|
|
8634
|
+
/** How long fleet startup waits for its chat connections before it continues without the slow ones (#2642). */
|
|
8635
|
+
static ADAPTER_STARTUP_WAIT_MS = 60_000;
|
|
8636
|
+
/** Track a connection's startup outcome; a failure gets the background retry. */
|
|
8637
|
+
recordAdapterStartup(cfg, result) {
|
|
8638
|
+
const adapterId = cfg.id ?? cfg.type;
|
|
8639
|
+
if (result.status === "fulfilled") {
|
|
8640
|
+
this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
|
|
8641
|
+
return;
|
|
8642
|
+
}
|
|
8643
|
+
const err = result.reason;
|
|
8644
|
+
this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
|
|
8645
|
+
this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
|
|
8646
|
+
this.scheduleAdapterRetry(adapterId);
|
|
8647
|
+
// Notify admin via whichever adapter is already up
|
|
8648
|
+
this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
|
|
8649
|
+
}
|
|
8534
8650
|
/** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
|
|
8535
8651
|
scheduleAdapterRetry(adapterId) {
|
|
8536
8652
|
const INITIAL_DELAY_MS = 5_000;
|
|
@@ -12170,10 +12286,11 @@ export class FleetManager {
|
|
|
12170
12286
|
const { office, ...dashboard } = patch.value;
|
|
12171
12287
|
return [{ ...patch, value: dashboard }, { path: [...path.slice(0, -1), "office_access"], value: office, remove: office === null }];
|
|
12172
12288
|
}
|
|
12289
|
+
// #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
|
|
12173
12290
|
if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
|
|
12174
|
-
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"],
|
|
12291
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], remove: true }];
|
|
12175
12292
|
if (patch.path.join(".") === "defaults.tips" && patch.value === "dm")
|
|
12176
|
-
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"],
|
|
12293
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], remove: true }];
|
|
12177
12294
|
return [patch];
|
|
12178
12295
|
});
|
|
12179
12296
|
// Re-read immediately before patching so an unrelated concurrent/manual
|
|
@@ -12251,9 +12368,46 @@ export class FleetManager {
|
|
|
12251
12368
|
},
|
|
12252
12369
|
});
|
|
12253
12370
|
this.rawFleetConfig = loadRawFleetConfig(this.configPath);
|
|
12371
|
+
// The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
|
|
12372
|
+
// leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
|
|
12373
|
+
// Keep the FleetConfig object itself: Settings and other request contexts hold it.
|
|
12374
|
+
const resolvedInstances = loadFleetConfig(this.configPath).instances;
|
|
12375
|
+
for (const [name, resolved] of Object.entries(resolvedInstances)) {
|
|
12376
|
+
const current = this.fleetConfig.instances[name];
|
|
12377
|
+
if (!current) {
|
|
12378
|
+
this.fleetConfig.instances[name] = resolved;
|
|
12379
|
+
continue;
|
|
12380
|
+
}
|
|
12381
|
+
// Lifecycle rollback fences hold these object identities across save. Refresh their values, not their owner.
|
|
12382
|
+
for (const key of Object.keys(current))
|
|
12383
|
+
if (!Object.hasOwn(resolved, key))
|
|
12384
|
+
delete current[key];
|
|
12385
|
+
Object.assign(current, resolved);
|
|
12386
|
+
}
|
|
12254
12387
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12388
|
+
this.refreshSavedDashboardPermissions();
|
|
12255
12389
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
12256
12390
|
}
|
|
12391
|
+
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
12392
|
+
refreshSavedDashboardPermissions() {
|
|
12393
|
+
if (!this.fleetConfig)
|
|
12394
|
+
return;
|
|
12395
|
+
const classicNames = new Set(this.classicChannels?.getAll().map(ch => ch.instanceName) ?? []);
|
|
12396
|
+
for (const [name, daemon] of this.daemons) {
|
|
12397
|
+
const next = this.fleetConfig.instances[name] ?? (classicNames.has(name) ? getEffectiveInstanceDefaults(this.fleetConfig.defaults) : undefined);
|
|
12398
|
+
const previous = daemon.getConfigSnapshot?.();
|
|
12399
|
+
if (!next || !previous)
|
|
12400
|
+
continue;
|
|
12401
|
+
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
|
|
12402
|
+
continue;
|
|
12403
|
+
// Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
|
|
12404
|
+
// 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 });
|
|
12406
|
+
if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
|
|
12407
|
+
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
12408
|
+
}
|
|
12409
|
+
}
|
|
12410
|
+
}
|
|
12257
12411
|
/** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
|
|
12258
12412
|
savedFleetConfigProblem(candidate) {
|
|
12259
12413
|
let after;
|
|
@@ -12292,12 +12446,13 @@ export class FleetManager {
|
|
|
12292
12446
|
}
|
|
12293
12447
|
/** One-time upgrade migration; invalid YAML is never rewritten. */
|
|
12294
12448
|
migrateNoticeDeliveryStorage() {
|
|
12295
|
-
if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
|
|
12449
|
+
if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
|
|
12450
|
+
|| this.fleetConfig.web?.notice_delivery !== undefined)
|
|
12296
12451
|
&& validateFleetConfig(this.rawFleetConfig).valid) {
|
|
12297
12452
|
try {
|
|
12298
12453
|
this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
|
|
12299
12454
|
this.saveFleetConfig();
|
|
12300
|
-
this.logger.info("
|
|
12455
|
+
this.logger.info("Notices no longer go by DM: stored them as topic booleans and dropped the old DM routing");
|
|
12301
12456
|
}
|
|
12302
12457
|
catch (err) {
|
|
12303
12458
|
this.logger.warn({ err }, "Could not migrate DM notice routing; keeping the original file");
|
|
@@ -14357,6 +14512,8 @@ export class FleetManager {
|
|
|
14357
14512
|
return true;
|
|
14358
14513
|
if (await this.handleSettingsChangeCallback(data, adapterId, adapter))
|
|
14359
14514
|
return true;
|
|
14515
|
+
if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
|
|
14516
|
+
return true;
|
|
14360
14517
|
if (await this.handleDashboardCallback(data, adapterId, adapter))
|
|
14361
14518
|
return true;
|
|
14362
14519
|
if (await this.handleClassicApproval(data, adapterId, adapter))
|
|
@@ -14477,6 +14634,7 @@ export class FleetManager {
|
|
|
14477
14634
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
14478
14635
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14479
14636
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
14637
|
+
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
14480
14638
|
}));
|
|
14481
14639
|
const taken = new Set(fleet.map(i => i.name));
|
|
14482
14640
|
const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
|
|
@@ -14486,6 +14644,7 @@ export class FleetManager {
|
|
|
14486
14644
|
ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
|
|
14487
14645
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
14488
14646
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14647
|
+
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14489
14648
|
}));
|
|
14490
14649
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
14491
14650
|
}
|
|
@@ -14506,8 +14665,19 @@ export class FleetManager {
|
|
|
14506
14665
|
return this.credentialProfileOf(name, "claude-code") ?? "default";
|
|
14507
14666
|
if (backend === "codex")
|
|
14508
14667
|
return "unrecorded";
|
|
14668
|
+
if (backend === "kiro-cli")
|
|
14669
|
+
return this.credentialProfileOf(name, "kiro-cli") ?? "default";
|
|
14670
|
+
if (backend === "grok" || backend === "muse" || backend === "opencode" || backend === "antigravity")
|
|
14671
|
+
return "default";
|
|
14509
14672
|
return undefined;
|
|
14510
14673
|
}
|
|
14674
|
+
/** #2122 §10: a Kiro instance with a credential profile writes its classic store in that profile's home. */
|
|
14675
|
+
kiroDbPathOf(name, backend) {
|
|
14676
|
+
if (backend !== "kiro-cli")
|
|
14677
|
+
return undefined;
|
|
14678
|
+
const profile = this.credentialProfileOf(name, "kiro-cli");
|
|
14679
|
+
return profile ? join(credentialProfileStoreHome(getAgendHome(), "kiro-cli", profile), "data.sqlite3") : undefined;
|
|
14680
|
+
}
|
|
14511
14681
|
/** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
|
|
14512
14682
|
usageZone() {
|
|
14513
14683
|
return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
@@ -14550,6 +14720,19 @@ export class FleetManager {
|
|
|
14550
14720
|
rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
|
|
14551
14721
|
metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
|
|
14552
14722
|
zone: () => this.usageZone(),
|
|
14723
|
+
// #2122 §10: Kiro's shared classic store (XDG_DATA_HOME, as kiro-cli resolves it) and its KIRO_HOME.
|
|
14724
|
+
kiroDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "kiro-cli", "data.sqlite3"),
|
|
14725
|
+
kiroHome: () => kiroHome(),
|
|
14726
|
+
// #2122 §10 step 3: grok keeps its sessions under ~/.grok (grok.ts reads them there too).
|
|
14727
|
+
grokSessionsDir: () => join(homedir(), ".grok", "sessions"),
|
|
14728
|
+
// #2122 §10 step 4: muse keeps sessions under $XDG_DATA_HOME/muse (else ~/.local/share/muse), as muse.ts reads them.
|
|
14729
|
+
// #2122 §10 step 5: OpenCode's database, under XDG_DATA_HOME as opencode resolves it.
|
|
14730
|
+
// #2122 §10 step 6: agy's conversations, under ~/.gemini as the antigravity backend reads its settings.
|
|
14731
|
+
antigravityDir: () => join(homedir(), ".gemini", "antigravity-cli", "conversations"),
|
|
14732
|
+
// The version AgEnD's CLI probe last recorded for agy: its cache file, whatever its age (never runs agy).
|
|
14733
|
+
antigravityVersion: () => this.lastRecordedCliVersion("antigravity"),
|
|
14734
|
+
opencodeDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "opencode", "opencode.db"),
|
|
14735
|
+
museSessionsDir: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "muse", "sessions"),
|
|
14553
14736
|
log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
|
|
14554
14737
|
});
|
|
14555
14738
|
}
|
|
@@ -14670,6 +14853,7 @@ export class FleetManager {
|
|
|
14670
14853
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
14671
14854
|
},
|
|
14672
14855
|
tasks: () => this.needsYouTaskItems(),
|
|
14856
|
+
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
14673
14857
|
taskAction: action => this.taskActionFromChat(action),
|
|
14674
14858
|
settings: () => ({
|
|
14675
14859
|
liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
|
|
@@ -14928,14 +15112,14 @@ export class FleetManager {
|
|
|
14928
15112
|
}
|
|
14929
15113
|
}
|
|
14930
15114
|
/** Post one fresh nonce-armed tip in a known General channel. */
|
|
14931
|
-
async promptTip(generalName, adapter, chatId, threadId
|
|
15115
|
+
async promptTip(generalName, adapter, chatId, threadId) {
|
|
14932
15116
|
const state = this.readTipState();
|
|
14933
15117
|
if (!state)
|
|
14934
15118
|
return "unavailable";
|
|
14935
15119
|
if (!state.advancedUnlocked
|
|
14936
15120
|
&& visibleTipLevels(true).has("advanced")
|
|
14937
15121
|
&& canUnlockAdvancedTips(state.dismissed)) {
|
|
14938
|
-
return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId
|
|
15122
|
+
return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId)
|
|
14939
15123
|
? "posted"
|
|
14940
15124
|
: "unavailable";
|
|
14941
15125
|
}
|
|
@@ -14951,7 +15135,6 @@ export class FleetManager {
|
|
|
14951
15135
|
chatId,
|
|
14952
15136
|
threadId,
|
|
14953
15137
|
message: this.formatTip(tip),
|
|
14954
|
-
...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, this.formatTip(tip), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
|
|
14955
15138
|
choices: [
|
|
14956
15139
|
{ action: "dismiss", label: t("tips.dismiss") },
|
|
14957
15140
|
{ action: "confused", label: t("tips.confused") },
|
|
@@ -14963,7 +15146,7 @@ export class FleetManager {
|
|
|
14963
15146
|
});
|
|
14964
15147
|
return nonce ? "posted" : "unavailable";
|
|
14965
15148
|
}
|
|
14966
|
-
async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId
|
|
15149
|
+
async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId) {
|
|
14967
15150
|
const nonce = await this.postNonceButtonPrompt({
|
|
14968
15151
|
prefix: TIP_UNLOCK_CALLBACK_PREFIX,
|
|
14969
15152
|
alertType: "tip",
|
|
@@ -14973,7 +15156,6 @@ export class FleetManager {
|
|
|
14973
15156
|
chatId,
|
|
14974
15157
|
threadId,
|
|
14975
15158
|
message: t("tips.advanced.unlock_prompt"),
|
|
14976
|
-
...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, t("tips.advanced.unlock_prompt"), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
|
|
14977
15159
|
choices: [{ action: "unlock", label: t("tips.advanced.unlock") }],
|
|
14978
15160
|
expiredText: t("tips.advanced.expired"),
|
|
14979
15161
|
// No allowAnyUser: unlocking changes a persistent setting, which typed and slash `/tips advanced on` reserve for a
|
|
@@ -14993,15 +15175,6 @@ export class FleetManager {
|
|
|
14993
15175
|
const topicId = this.fleetConfig?.instances[generalName]?.topic_id;
|
|
14994
15176
|
if (!adapter || !chatId)
|
|
14995
15177
|
return;
|
|
14996
|
-
if (this.fleetConfig && tipNoticeMode(this.fleetConfig) === "dm" && adapter.sendDirect) {
|
|
14997
|
-
const userId = this.noticeAdmin(adapter.id);
|
|
14998
|
-
if (userId) {
|
|
14999
|
-
const result = await this.promptTip(generalName, adapter, userId, undefined, userId);
|
|
15000
|
-
if (result !== "unavailable")
|
|
15001
|
-
return;
|
|
15002
|
-
// The failed prompt disarmed its nonce; a new topic prompt is safe.
|
|
15003
|
-
}
|
|
15004
|
-
}
|
|
15005
15178
|
const result = await this.promptTip(generalName, adapter, chatId, topicId != null ? String(topicId) : undefined);
|
|
15006
15179
|
if (result === "unavailable") {
|
|
15007
15180
|
this.logger.warn({ generalName }, "Daily tip could not be posted");
|
|
@@ -16685,6 +16858,55 @@ export class FleetManager {
|
|
|
16685
16858
|
}
|
|
16686
16859
|
return this.loginController;
|
|
16687
16860
|
}
|
|
16861
|
+
browserBackendSignInsInstance = null;
|
|
16862
|
+
structuredLoginCleanupFailed = false;
|
|
16863
|
+
pendingBackendSince = new Map();
|
|
16864
|
+
browserSignInOwnerCurrent(owner) {
|
|
16865
|
+
const record = owner;
|
|
16866
|
+
return !this.shuttingDown && typeof record.handle === "string" && this.webSessions?.findByHandle(record.handle) === owner
|
|
16867
|
+
&& this.isLocalAdminSession(record.handle);
|
|
16868
|
+
}
|
|
16869
|
+
/** #2599: default-account controls share the exact writer reservation with chat/profile/install. */
|
|
16870
|
+
get browserBackendSignIns() {
|
|
16871
|
+
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
16872
|
+
dataDir: this.dataDir, lock: this.loginWindow,
|
|
16873
|
+
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
16874
|
+
installed: async () => ONBOARDING_BACKENDS.filter(backend => this.isCliInstalled(backend)),
|
|
16875
|
+
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
16876
|
+
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
16877
|
+
transport: () => createStructuredSignInTransport(),
|
|
16878
|
+
changed: (event, owner, launched) => {
|
|
16879
|
+
try {
|
|
16880
|
+
this.eventLog?.insert("login", "browser_backend_signin", event);
|
|
16881
|
+
}
|
|
16882
|
+
catch { /* safe metadata only */ }
|
|
16883
|
+
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
|
|
16884
|
+
this.needsYou?.poke();
|
|
16885
|
+
if (!this.browserSignInOwnerCurrent(owner))
|
|
16886
|
+
return;
|
|
16887
|
+
if (event.phase === "starting") {
|
|
16888
|
+
const record = owner;
|
|
16889
|
+
this.notifyFleetError(t("login.web_dashboard_notice", event.backend, record.label, record.handle.slice(0, 8)));
|
|
16890
|
+
}
|
|
16891
|
+
if (event.phase === "done") {
|
|
16892
|
+
void this.backendAvailability.refresh([event.backend], { force: true }).catch(() => { });
|
|
16893
|
+
if (launched)
|
|
16894
|
+
void this.recoverBackendInstances(event.backend, undefined, null).catch(() => this.logger.warn({ backend: event.backend }, "Post sign-in recovery failed"));
|
|
16895
|
+
}
|
|
16896
|
+
},
|
|
16897
|
+
});
|
|
16898
|
+
}
|
|
16899
|
+
needsYouBackendSignIns() {
|
|
16900
|
+
const pending = readPendingBackendSignIns(this.dataDir).pending;
|
|
16901
|
+
for (const backend of this.pendingBackendSince?.keys() ?? [])
|
|
16902
|
+
if (!pending.includes(backend))
|
|
16903
|
+
this.pendingBackendSince.delete(backend);
|
|
16904
|
+
return pending.map(backend => {
|
|
16905
|
+
if (!this.pendingBackendSince.has(backend))
|
|
16906
|
+
this.pendingBackendSince.set(backend, Date.now());
|
|
16907
|
+
return { backend, since: this.pendingBackendSince.get(backend), browser: structuredSignInSupported(backend) };
|
|
16908
|
+
});
|
|
16909
|
+
}
|
|
16688
16910
|
/** #2233: sign-ins started from the web dashboard (local admin sessions), on the same controller as chat's. */
|
|
16689
16911
|
webBackendLoginsInstance = null;
|
|
16690
16912
|
get webBackendLogins() {
|
|
@@ -17037,6 +17259,14 @@ export class FleetManager {
|
|
|
17037
17259
|
// "cancelled" is the detail both legacy onDone handlers keep quiet about —
|
|
17038
17260
|
// a stopping fleet must not announce "login failed — fleet shutdown".
|
|
17039
17261
|
await Promise.all([
|
|
17262
|
+
bounded(this.browserBackendSignInsInstance?.close().then(confirmed => {
|
|
17263
|
+
if (confirmed)
|
|
17264
|
+
this.browserBackendSignInsInstance = null;
|
|
17265
|
+
else {
|
|
17266
|
+
this.structuredLoginCleanupFailed = true;
|
|
17267
|
+
this.logger.error("Structured login cleanup failed; sign-in admission remains closed until the host is restarted.");
|
|
17268
|
+
}
|
|
17269
|
+
}), "browser-controls"),
|
|
17040
17270
|
bounded(this.loginController?.shutdown(), "web-login"),
|
|
17041
17271
|
bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
|
|
17042
17272
|
]);
|
|
@@ -17053,6 +17283,13 @@ export class FleetManager {
|
|
|
17053
17283
|
this.installHandoff = null;
|
|
17054
17284
|
return t("login.cancelled", backend);
|
|
17055
17285
|
}
|
|
17286
|
+
const browserLogin = this.browserBackendSignInsInstance, browserBackend = browserLogin?.activeBackend;
|
|
17287
|
+
if (browserLogin && browserBackend) {
|
|
17288
|
+
const confirmed = await browserLogin.close();
|
|
17289
|
+
if (confirmed)
|
|
17290
|
+
this.browserBackendSignInsInstance = null;
|
|
17291
|
+
return confirmed ? t("login.cancelled", browserBackend) : t("login.browser_cleanup_failed");
|
|
17292
|
+
}
|
|
17056
17293
|
if (this.loginController?.isActive())
|
|
17057
17294
|
return this.loginController.cancel();
|
|
17058
17295
|
return t("login.no_session");
|
|
@@ -18564,6 +18801,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18564
18801
|
catch { /* missing / stale / corrupt */ }
|
|
18565
18802
|
return null;
|
|
18566
18803
|
}
|
|
18804
|
+
/**
|
|
18805
|
+
* #2122 §10: the version the CLI probe last recorded for a backend — its cache file read whatever its age (the usage
|
|
18806
|
+
* reader's version gate needs the last known version, not a fresh one); null when none was recorded. Never a probe.
|
|
18807
|
+
*/
|
|
18808
|
+
lastRecordedCliVersion(backend) {
|
|
18809
|
+
try {
|
|
18810
|
+
const v = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8")).version;
|
|
18811
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
18812
|
+
}
|
|
18813
|
+
catch {
|
|
18814
|
+
return null;
|
|
18815
|
+
}
|
|
18816
|
+
}
|
|
18567
18817
|
/** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
|
|
18568
18818
|
getBackendCliVersionSnapshot() {
|
|
18569
18819
|
const snapshot = {};
|
|
@@ -20501,8 +20751,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20501
20751
|
/** Signal-handler continuation, after successful graceful shutdown only. */
|
|
20502
20752
|
noteRequestedShutdown() {
|
|
20503
20753
|
const progress = readUpdateProgress(this.dataDir)?.progress;
|
|
20504
|
-
|
|
20505
|
-
|
|
20754
|
+
// Host CLI updates carry a generic marker with no chat progress target.
|
|
20755
|
+
const update = progress ? updateProgressOperation(progress) === "update"
|
|
20756
|
+
&& ["installed", "stopping", "starting"].includes(progress.stage) : isUpdateInProgress(this.dataDir);
|
|
20506
20757
|
this.recordWorkingRestartIntent(update ? "update" : "manual_restart");
|
|
20507
20758
|
}
|
|
20508
20759
|
recordWorkingRestartIntent(cause) {
|
|
@@ -20562,6 +20813,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20562
20813
|
const settingsControlStopped = settingsControl?.close();
|
|
20563
20814
|
this.publicLinkAutoOpen?.stop();
|
|
20564
20815
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
20816
|
+
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
20565
20817
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
20566
20818
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
20567
20819
|
const needsStopped = this.stopNeedsYou();
|
|
@@ -20600,6 +20852,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20600
20852
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
20601
20853
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
20602
20854
|
await publicStopped;
|
|
20855
|
+
await sharesStopped?.catch(() => { });
|
|
20603
20856
|
await profileStopped;
|
|
20604
20857
|
await needsStopped;
|
|
20605
20858
|
await settingsControlStopped;
|
|
@@ -22502,6 +22755,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22502
22755
|
this.logger.info("No instances to restart");
|
|
22503
22756
|
return;
|
|
22504
22757
|
}
|
|
22758
|
+
this.workingRecoveryGroup = randomUUID();
|
|
22505
22759
|
this.recordWorkingRestartIntent("manual_restart");
|
|
22506
22760
|
this.workingRecovery?.reset();
|
|
22507
22761
|
this.logger.info(`Graceful restart: waiting for ${instanceNames.length} instances to idle...`);
|
|
@@ -22577,29 +22831,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22577
22831
|
}
|
|
22578
22832
|
// General is ready again; now its topic can own the live progress message.
|
|
22579
22833
|
await restartProgress.start(progressTarget);
|
|
22580
|
-
|
|
22581
|
-
|
|
22834
|
+
// The ClassicBot rooms (killed during orphan cleanup) restart with the fleet instances, in the same queue; their
|
|
22835
|
+
// open-lists are refreshed first so a room's inbound is never dropped while it starts.
|
|
22836
|
+
const restartClassic = topicMode ? this.classicChannels?.getAll() ?? [] : [];
|
|
22837
|
+
if (restartClassic.length > 0)
|
|
22838
|
+
this.reregisterClassicChannels();
|
|
22839
|
+
if (restartOthers.length > 0 || restartClassic.length > 0) {
|
|
22840
|
+
await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady(), restartClassic);
|
|
22582
22841
|
}
|
|
22583
22842
|
if (topicMode) {
|
|
22584
22843
|
this.routing.rebuild(this.fleetConfig);
|
|
22585
22844
|
this.reregisterClassicChannels();
|
|
22586
22845
|
// startInstance already calls connectIpcToInstance, no need for connectToInstances here
|
|
22587
|
-
// Restart classic channel instances (killed during orphan cleanup)
|
|
22588
|
-
if (this.classicChannels) {
|
|
22589
|
-
const fleetBackend = this.fleetConfig?.defaults?.backend;
|
|
22590
|
-
const channels = this.classicChannels.getAll()
|
|
22591
|
-
.filter(ch => !this.lifecycle.isPaused(ch.instanceName));
|
|
22592
|
-
const concurrency = 3;
|
|
22593
|
-
let idx = 0;
|
|
22594
|
-
while (idx < channels.length) {
|
|
22595
|
-
const batch = channels.slice(idx, idx + concurrency);
|
|
22596
|
-
await Promise.allSettled(batch.map(async (ch) => {
|
|
22597
|
-
if (await this.startClassicInstanceUnattended(ch, "classic instance"))
|
|
22598
|
-
restartProgress.markReady();
|
|
22599
|
-
}));
|
|
22600
|
-
idx += concurrency;
|
|
22601
|
-
}
|
|
22602
|
-
}
|
|
22603
22846
|
for (const name of Object.keys(fleet.instances)) {
|
|
22604
22847
|
this.startStatuslineWatcher(name);
|
|
22605
22848
|
}
|