@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.3
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +10 -7
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +141 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +14 -2
- package/dist/backend/kiro.js +84 -9
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +4 -2
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -1
- package/dist/channel/adapters/discord.js +23 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +19 -1
- package/dist/channel/adapters/telegram.js +60 -9
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +23 -12
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +14 -2
- package/dist/cli.js +55 -16
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +49 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +27 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +48 -0
- package/dist/daemon.js +250 -22
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +17 -2
- package/dist/delivery-outbox.js +13 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-manager.d.ts +63 -1
- package/dist/fleet-manager.js +314 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +3 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +10 -7
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +36 -26
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +28 -15
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +10 -2
- package/dist/outbound-schemas.js +15 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/service-activation.js +4 -3
- package/dist/service-activation.js.map +1 -1
- package/dist/settings-api.js +71 -21
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +70 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +26 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +8 -0
- package/dist/transcript-monitor.js +26 -2
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +54 -0
- package/dist/transcript-sources.js +188 -27
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-render.js +12 -0
- package/dist/ui/chat-store.js +1 -1
- package/dist/ui/chat-strings.js +12 -12
- package/dist/ui/chat-thread.js +23 -4
- package/dist/ui/panel-chat.js +16 -3
- package/dist/ui/panel-needs.js +5 -2
- package/dist/ui/panel-settings.js +129 -31
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +44 -4
- package/dist/ui/shared/app-i18n.js +37 -15
- package/dist/ui/shared/app-prefs.js +249 -0
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +17 -4
- package/dist/ui/shared/app-store.js +3 -1
- package/dist/ui/shared/app.css +55 -0
- package/dist/ui/shared/app.js +6 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +169 -32
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +13 -4
- package/dist/ui/shared/view-strings.js +22 -4
- package/dist/ui/signin.html +1 -1
- package/dist/update-install.d.ts +15 -1
- package/dist/update-install.js +6 -0
- package/dist/update-install.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +21 -1
- package/dist/web-api.js +157 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +7 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-prefs.d.ts +63 -0
- package/dist/web-prefs.js +263 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-session.d.ts +36 -1
- package/dist/web-session.js +53 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +21 -0
- package/dist/web-settings-patch.js +74 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -51,6 +51,7 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
|
51
51
|
import { TmuxManager } from "./tmux-manager.js";
|
|
52
52
|
import { AccessManager } from "./channel/access-manager.js";
|
|
53
53
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
54
|
+
import { usesChannelAddress } from "./channel/mcp-tools.js";
|
|
54
55
|
import { formatFleetTime } from "./tz-utils.js";
|
|
55
56
|
import { createAdapter } from "./channel/factory.js";
|
|
56
57
|
import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
@@ -74,7 +75,7 @@ import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, che
|
|
|
74
75
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
75
76
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
76
77
|
import { outboundHandlers } from "./outbound-handlers.js";
|
|
77
|
-
import { DeliveryOutbox } from "./delivery-outbox.js";
|
|
78
|
+
import { DeliveryOutbox, isSilentScheduleDelivery } from "./delivery-outbox.js";
|
|
78
79
|
// The target Daemon can legitimately wait 30 minutes for a busy pane before
|
|
79
80
|
// deciding to defer before submission. This is an alert threshold only: the
|
|
80
81
|
// active manager/target generation pair owns the lane until the daemon reports
|
|
@@ -128,10 +129,14 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
128
129
|
import { handleWebRequest, broadcastSseEvent, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
129
130
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId } from "./web-chat-history.js";
|
|
130
131
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
132
|
+
import { WebPrefsStore } from "./web-prefs.js";
|
|
131
133
|
import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
|
|
132
134
|
import { ReplyButtonsController } from "./reply-buttons-controller.js";
|
|
133
135
|
import { publicAttachment, sweepOrphanedUploads, WebFileLedger } from "./web-upload.js";
|
|
134
136
|
import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
137
|
+
import { downloadStickerImage } from "./channel/sticker-download.js";
|
|
138
|
+
import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
|
|
139
|
+
import { accentColor, avatarFor, ownBotConnections } from "./instance-avatar.js";
|
|
135
140
|
import { envFileKeys } from "./token-env-name.js";
|
|
136
141
|
import { discordGeneralChannelId } from "./discord-general.js";
|
|
137
142
|
import { parseEnvText } from "./env-file.js";
|
|
@@ -180,7 +185,7 @@ import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefu
|
|
|
180
185
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken } from "./web-auth.js";
|
|
181
186
|
import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
|
|
182
187
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
183
|
-
import { tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
188
|
+
import { localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
184
189
|
import { WebLoginCodes, LOGIN_CODE_TTL_MS } from "./web-login.js";
|
|
185
190
|
import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
|
|
186
191
|
import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
|
|
@@ -758,6 +763,15 @@ export class FleetManager {
|
|
|
758
763
|
webChatStore = null;
|
|
759
764
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
760
765
|
webFiles = new WebFileLedger();
|
|
766
|
+
webPrefsStore;
|
|
767
|
+
/** #1665: the web preferences kept on the host (<home>/web-prefs.json), read on first use. */
|
|
768
|
+
get webPrefs() {
|
|
769
|
+
// #1664: a favorite must be an instance of this fleet now — fleet.yaml's (own keys) or a ClassicBot room's.
|
|
770
|
+
return (this.webPrefsStore ??= new WebPrefsStore(this.dataDir, {
|
|
771
|
+
exists: (name) => Object.hasOwn(this.fleetConfig?.instances ?? {}, name)
|
|
772
|
+
|| (this.classicChannels?.getAll() ?? []).some((ch) => ch.instanceName === name),
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
761
775
|
/**
|
|
762
776
|
* Read from disk on every access rather than cached at startup: `agend
|
|
763
777
|
* web-token rotate` runs in a separate process, and a cached copy would keep
|
|
@@ -1215,8 +1229,13 @@ export class FleetManager {
|
|
|
1215
1229
|
if (event.deliveryId && (event.state === "failed" || event.state === "uncertain")) {
|
|
1216
1230
|
const row = outbox.get(event.deliveryId);
|
|
1217
1231
|
if (row && row.kind !== "delivery_outcome_notice" && row.kind !== "post_restart_outcome_notice") {
|
|
1218
|
-
|
|
1219
|
-
|
|
1232
|
+
// #1647: the cause goes in the log (the notice points there), and a silent schedule's paste says what it is.
|
|
1233
|
+
const silent = isSilentScheduleDelivery(row);
|
|
1234
|
+
const notice = silent
|
|
1235
|
+
? t(`delivery.fleet_${row.state}_silent_schedule`, row.targetInstance, row.operationId, row.deliveryId, row.lastError ?? "unknown")
|
|
1236
|
+
: t(`delivery.fleet_${row.state}`, row.targetInstance, row.operationId, row.deliveryId);
|
|
1237
|
+
this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
|
|
1238
|
+
cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
|
|
1220
1239
|
this.notifyFleetError(notice);
|
|
1221
1240
|
}
|
|
1222
1241
|
}
|
|
@@ -1274,7 +1293,20 @@ export class FleetManager {
|
|
|
1274
1293
|
}
|
|
1275
1294
|
queryDurableDeliveryStatus(callerInstance, selector) {
|
|
1276
1295
|
this.ensureDeliveryOutbox();
|
|
1277
|
-
|
|
1296
|
+
const page = this.deliveryOutbox.queryStatusForInstance(callerInstance, selector);
|
|
1297
|
+
// #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
|
|
1298
|
+
return {
|
|
1299
|
+
...page,
|
|
1300
|
+
items: page.items.map(item => (item.state === "queued" || item.state === "retry_wait")
|
|
1301
|
+
? { ...item, waiting_reason: this.deliveryWaitingReason(item.target_instance) }
|
|
1302
|
+
: item),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
/** #1641: why a target is not taking queued work right now, or null when nothing known holds it. */
|
|
1306
|
+
deliveryWaitingReason(target) {
|
|
1307
|
+
if (this.lifecycle.isPaused(target))
|
|
1308
|
+
return t("delivery.hold_state", "paused");
|
|
1309
|
+
return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
|
|
1278
1310
|
}
|
|
1279
1311
|
getDaemonBootId(instanceName) {
|
|
1280
1312
|
return this.daemons.get(instanceName)?.bootId;
|
|
@@ -3003,13 +3035,38 @@ export class FleetManager {
|
|
|
3003
3035
|
}
|
|
3004
3036
|
return posted;
|
|
3005
3037
|
}
|
|
3006
|
-
|
|
3038
|
+
/**
|
|
3039
|
+
* #1666: a delivered web chat message starts the same cancel/progress lifecycle in the instance's topic as a platform
|
|
3040
|
+
* inbound (afterUserInboundDelivered): the same bubble, ticker and retire rules. Not its ✅ tracking — the web message
|
|
3041
|
+
* id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
|
|
3042
|
+
*/
|
|
3043
|
+
startWebChatProgress(instanceName) {
|
|
3044
|
+
if (this.shuttingDown)
|
|
3045
|
+
return Promise.resolve();
|
|
3046
|
+
return this.sendCancelButton(instanceName);
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* `copy: false` (#1666/#1668): an entry that is not a copy of a message — the web send's Cancel/progress bubble. It
|
|
3050
|
+
* keeps its place in the order, but the lane's copy rules do not apply to it: its 5 s budget starts when its turn
|
|
3051
|
+
* starts (waiting behind a slow echo is not its delay), its own timeout only releases the lane (the copies queued
|
|
3052
|
+
* behind it are not "behind an ambiguous copy"), and another entry's timeout never drops it. A copy reserved while
|
|
3053
|
+
* any non-copy is still open starts its own budget when the last of them finishes: a slow bubble is no copy's delay,
|
|
3054
|
+
* however far back the copy is. And a timeout only ever drops copies reserved AFTER the entry that timed out — with
|
|
3055
|
+
* deferred budgets, deadlines no longer grow with the order, so that rule is stated, not implied (#1668 r3).
|
|
3056
|
+
*/
|
|
3057
|
+
reserveWebChannelEcho(instanceName, sendEcho, opts = {}) {
|
|
3058
|
+
const copy = opts.copy !== false;
|
|
3007
3059
|
const epoch = this.getDeliveryEpoch(instanceName);
|
|
3008
|
-
const
|
|
3060
|
+
const queued = this.webChannelEchoTails.get(instanceName);
|
|
3061
|
+
// The last open non-copy ahead of this copy: its budget starts when that one finishes.
|
|
3062
|
+
const nonCopyAhead = copy && (queued?.nonCopiesOpen ?? 0) > 0 ? queued?.lastNonCopyDone : undefined;
|
|
3063
|
+
let deadlineAt = copy && !nonCopyAhead ? performance.now() + 5_000 : Number.POSITIVE_INFINITY;
|
|
3009
3064
|
let decide;
|
|
3010
3065
|
const admission = new Promise(resolve => { decide = resolve; });
|
|
3011
|
-
const queue =
|
|
3066
|
+
const queue = queued
|
|
3012
3067
|
?? { tail: Promise.resolve(), pending: new Set() };
|
|
3068
|
+
const seq = queue.nextSeq ?? 0;
|
|
3069
|
+
queue.nextSeq = seq + 1;
|
|
3013
3070
|
const previous = queue.tail;
|
|
3014
3071
|
let resolveDone;
|
|
3015
3072
|
const tail = new Promise(resolve => { resolveDone = resolve; });
|
|
@@ -3020,6 +3077,8 @@ export class FleetManager {
|
|
|
3020
3077
|
if (done)
|
|
3021
3078
|
return;
|
|
3022
3079
|
done = true;
|
|
3080
|
+
if (!copy)
|
|
3081
|
+
queue.nonCopiesOpen = Math.max(0, (queue.nonCopiesOpen ?? 0) - 1);
|
|
3023
3082
|
clearTimeout(timer);
|
|
3024
3083
|
queue.pending.delete(entry);
|
|
3025
3084
|
if (guard && this.webChannelEchoGuards.get(instanceName) === guard) {
|
|
@@ -3027,7 +3086,7 @@ export class FleetManager {
|
|
|
3027
3086
|
}
|
|
3028
3087
|
resolveDone();
|
|
3029
3088
|
};
|
|
3030
|
-
const entry = { started: false, drop: () => {
|
|
3089
|
+
const entry = { seq, started: false, copy, drop: () => {
|
|
3031
3090
|
this.logger.warn({ instanceName }, "Queued web channel echo dropped after ordering timeout");
|
|
3032
3091
|
finish();
|
|
3033
3092
|
} };
|
|
@@ -3037,14 +3096,16 @@ export class FleetManager {
|
|
|
3037
3096
|
expired = true;
|
|
3038
3097
|
this.logger.warn({ instanceName, inFlight: entry.started }, "Web channel echo ordering timed out");
|
|
3039
3098
|
finish();
|
|
3040
|
-
// Do not post copies that were queued behind an ambiguous platform send.
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3099
|
+
// Do not post copies that were queued behind an ambiguous platform send. A non-copy that timed out was no copy
|
|
3100
|
+
// at all (nothing behind it is ambiguous), and a non-copy is never dropped as one.
|
|
3101
|
+
if (copy)
|
|
3102
|
+
for (const pending of queue.pending)
|
|
3103
|
+
if (pending.seq > seq && !pending.started && pending.copy)
|
|
3104
|
+
pending.drop();
|
|
3044
3105
|
};
|
|
3045
3106
|
const checkDeadline = () => {
|
|
3046
|
-
if (done)
|
|
3047
|
-
return;
|
|
3107
|
+
if (done || deadlineAt === Number.POSITIVE_INFINITY)
|
|
3108
|
+
return; // a non-copy's budget has not started yet
|
|
3048
3109
|
const remaining = deadlineAt - performance.now();
|
|
3049
3110
|
if (remaining <= 0)
|
|
3050
3111
|
expire();
|
|
@@ -3053,8 +3114,19 @@ export class FleetManager {
|
|
|
3053
3114
|
};
|
|
3054
3115
|
queue.pending.add(entry);
|
|
3055
3116
|
queue.tail = tail;
|
|
3117
|
+
if (!copy) {
|
|
3118
|
+
queue.nonCopiesOpen = (queue.nonCopiesOpen ?? 0) + 1;
|
|
3119
|
+
queue.lastNonCopyDone = tail;
|
|
3120
|
+
}
|
|
3056
3121
|
this.webChannelEchoTails.set(instanceName, queue);
|
|
3057
3122
|
checkDeadline();
|
|
3123
|
+
// Registered before the admission below (which waits on `previous`, itself after the non-copy ahead), so the budget
|
|
3124
|
+
// is set by the time that runs.
|
|
3125
|
+
if (nonCopyAhead)
|
|
3126
|
+
void nonCopyAhead.then(() => { if (!done) {
|
|
3127
|
+
deadlineAt = performance.now() + 5_000;
|
|
3128
|
+
checkDeadline();
|
|
3129
|
+
} });
|
|
3058
3130
|
void Promise.all([previous, admission]).then(([, accepted]) => {
|
|
3059
3131
|
if (done)
|
|
3060
3132
|
return;
|
|
@@ -3063,11 +3135,15 @@ export class FleetManager {
|
|
|
3063
3135
|
return;
|
|
3064
3136
|
}
|
|
3065
3137
|
// A delayed timer callback must not admit an already expired copy.
|
|
3066
|
-
if (performance.now() >= deadlineAt) {
|
|
3138
|
+
if (copy && performance.now() >= deadlineAt) {
|
|
3067
3139
|
expire();
|
|
3068
3140
|
return;
|
|
3069
3141
|
}
|
|
3070
3142
|
entry.started = true;
|
|
3143
|
+
if (!copy) {
|
|
3144
|
+
deadlineAt = performance.now() + 5_000;
|
|
3145
|
+
checkDeadline();
|
|
3146
|
+
}
|
|
3071
3147
|
guard = { epoch, deadlineAt };
|
|
3072
3148
|
this.webChannelEchoGuards.set(instanceName, guard);
|
|
3073
3149
|
let request;
|
|
@@ -4152,6 +4228,33 @@ export class FleetManager {
|
|
|
4152
4228
|
names.add(channel.instanceName);
|
|
4153
4229
|
return [...names];
|
|
4154
4230
|
}
|
|
4231
|
+
/** #1624: migration cannot infer ownership from the running daemon roster.
|
|
4232
|
+
* Stopped/paused ClassicBot entries count too; their workspace is exactly
|
|
4233
|
+
* the one startClassicInstance builds, not this FleetManager's dataDir.
|
|
4234
|
+
*/
|
|
4235
|
+
kiroPeerWorkingDirectories(name) {
|
|
4236
|
+
if (!this.fleetConfig || !this.classicChannels)
|
|
4237
|
+
throw new Error("Kiro peer configuration is not loaded");
|
|
4238
|
+
const peers = [];
|
|
4239
|
+
for (const [peer, cfg] of Object.entries(this.fleetConfig.instances)) {
|
|
4240
|
+
if (peer === name || (cfg.backend ?? this.fleetConfig.defaults?.backend ?? "claude-code") !== "kiro-cli")
|
|
4241
|
+
continue;
|
|
4242
|
+
if (typeof cfg.working_directory !== "string" || !cfg.working_directory)
|
|
4243
|
+
throw new Error("Kiro peer cwd is unknown");
|
|
4244
|
+
peers.push(cfg.working_directory);
|
|
4245
|
+
}
|
|
4246
|
+
for (const ch of this.classicChannels.getAll()) {
|
|
4247
|
+
if (ch.instanceName === name) {
|
|
4248
|
+
if (Object.hasOwn(this.fleetConfig.instances, name))
|
|
4249
|
+
throw new Error("Kiro instance name has two configurations");
|
|
4250
|
+
continue;
|
|
4251
|
+
}
|
|
4252
|
+
if (this.classicChannels.getBackendByInstance(ch.instanceName, this.fleetConfig.defaults?.backend) === "kiro-cli") {
|
|
4253
|
+
peers.push(join(getAgendHome(), "workspaces", ch.instanceName));
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
return peers;
|
|
4257
|
+
}
|
|
4155
4258
|
/**
|
|
4156
4259
|
* Probe the same executable set exposed by the web backend catalog.
|
|
4157
4260
|
*
|
|
@@ -4549,6 +4652,9 @@ export class FleetManager {
|
|
|
4549
4652
|
this.webSessions = new WebSessionStore({
|
|
4550
4653
|
dataDir: this.dataDir,
|
|
4551
4654
|
onWarn: message => this.logger.warn(message),
|
|
4655
|
+
// #1620: fleet.yaml's web.session.local, read at every check — a Settings save or a reload applies at once
|
|
4656
|
+
// (shortening ends the sessions it now outlives). The public link's policy stays fixed.
|
|
4657
|
+
localPolicy: () => localSessionPolicy(this.fleetConfig?.web?.session?.local),
|
|
4552
4658
|
});
|
|
4553
4659
|
}
|
|
4554
4660
|
if (!this.returningDevices) {
|
|
@@ -7710,10 +7816,12 @@ export class FleetManager {
|
|
|
7710
7816
|
respond(null, "No adapter available");
|
|
7711
7817
|
return;
|
|
7712
7818
|
}
|
|
7819
|
+
// Only channel-addressing schemas receive ClassicBot routing metadata.
|
|
7820
|
+
// Non-channel calls (notably strict delivery_status) retain their arguments.
|
|
7713
7821
|
// For classic instances: force chat_id to channelId and clear thread_id
|
|
7714
7822
|
// (daemon may have set chat_id to guild_id which is wrong for DC; TG may have set thread_id which causes 'thread not found')
|
|
7715
7823
|
const classicChannelId = this.classicChannels?.getChannelIdByInstance(senderInstanceName ?? instanceName);
|
|
7716
|
-
if (classicChannelId) {
|
|
7824
|
+
if (classicChannelId && usesChannelAddress(tool)) {
|
|
7717
7825
|
args.chat_id = classicChannelId;
|
|
7718
7826
|
// #1085: a Telegram forum ClassicBot replies in the topic it was asked
|
|
7719
7827
|
// in. Any other thread_id (threadless Telegram, where it echoes the chat
|
|
@@ -9301,7 +9409,9 @@ export class FleetManager {
|
|
|
9301
9409
|
else
|
|
9302
9410
|
await this.lifecycle.remove(name, authorization);
|
|
9303
9411
|
// A later same-name registration owns its files and caches.
|
|
9304
|
-
|
|
9412
|
+
// Own keys only: an instance named "constructor" is removed as any other (its inherited namesake is not a
|
|
9413
|
+
// re-registration) — #1677 review.
|
|
9414
|
+
if (Object.hasOwn(this.fleetConfig?.instances ?? {}, name) || this.daemons.has(name))
|
|
9305
9415
|
return;
|
|
9306
9416
|
execution?.assert();
|
|
9307
9417
|
// Clean up per-instance tracking maps so they don't grow unbounded
|
|
@@ -9313,6 +9423,13 @@ export class FleetManager {
|
|
|
9313
9423
|
// must not be shown the old one's conversation, and deleted names must not pile up.
|
|
9314
9424
|
this.webChatHistory.forget(name);
|
|
9315
9425
|
this.webFiles.forget(name);
|
|
9426
|
+
// #1664: and it is nobody's favorite any more (every device sees that at its next status frame).
|
|
9427
|
+
try {
|
|
9428
|
+
this.webPrefs.forgetInstance(name);
|
|
9429
|
+
}
|
|
9430
|
+
catch (err) {
|
|
9431
|
+
this.logger.warn({ err: err.message, name }, "Dropping a removed instance from web favorites failed");
|
|
9432
|
+
}
|
|
9316
9433
|
// Clean up statusline watcher + instance directory
|
|
9317
9434
|
this.statuslineWatcher.unwatch(name);
|
|
9318
9435
|
// instanceDirToClean and effectiveBackend are captured before lifecycle.remove
|
|
@@ -9502,6 +9619,151 @@ export class FleetManager {
|
|
|
9502
9619
|
/** Keyed `<connection>\0<server id>`. */
|
|
9503
9620
|
guildEmojiCache = new Map();
|
|
9504
9621
|
guildEmojiFetches = new Map();
|
|
9622
|
+
// ── #1630: what stands for an instance in the web app, and Discord server emoji in web chat ──
|
|
9623
|
+
webImageCaches = null;
|
|
9624
|
+
webImageCache(kind) {
|
|
9625
|
+
const onError = (key, err) => this.logger.debug({ err: err?.message, key }, "web image fetch failed (kept what was cached)");
|
|
9626
|
+
this.webImageCaches ??= {
|
|
9627
|
+
avatars: new WebImageCache({ dir: join(this.dataDir, "cache", "avatars"), onError }),
|
|
9628
|
+
emoji: new WebImageCache({ dir: join(this.dataDir, "cache", "emoji"), onError }),
|
|
9629
|
+
};
|
|
9630
|
+
return this.webImageCaches[kind];
|
|
9631
|
+
}
|
|
9632
|
+
/**
|
|
9633
|
+
* instance → the connection that is its own bot: configured now (a connection removed from fleet.yaml is nobody's,
|
|
9634
|
+
* whatever an instance still names — #1635 review), not the primary, and answering for it alone.
|
|
9635
|
+
*/
|
|
9636
|
+
ownBots() {
|
|
9637
|
+
const instances = this.fleetConfig?.instances ?? {};
|
|
9638
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
9639
|
+
const configured = new Set(channels.map(ch => ch.id ?? ch.type).filter((id) => typeof id === "string"));
|
|
9640
|
+
const assigned = (name) => { const c = this.getInstanceAdapterId(name); return c && configured.has(c) ? c : undefined; };
|
|
9641
|
+
const assignments = Object.keys(instances).map(n => [n, assigned(n)]);
|
|
9642
|
+
for (const room of this.classicChannels?.getAll() ?? []) {
|
|
9643
|
+
if (!Object.hasOwn(instances, room.instanceName))
|
|
9644
|
+
assignments.push([room.instanceName, assigned(room.instanceName)]);
|
|
9645
|
+
}
|
|
9646
|
+
return ownBotConnections(this.getPrimaryAdapterId(), assignments);
|
|
9647
|
+
}
|
|
9648
|
+
/**
|
|
9649
|
+
* The bot a connection is now: its running adapter and that bot's own user id. Everything cached for an own bot is
|
|
9650
|
+
* keyed by both, so a bot put in its place (a token replaced under the same connection id) never shows the old one's
|
|
9651
|
+
* face or colour, and the old one's late answers land under the old key (#1635 review). null while it has no id.
|
|
9652
|
+
*/
|
|
9653
|
+
botOwner(connection) {
|
|
9654
|
+
const adapter = this.adapters.get(connection), id = adapter?.getBotUserId?.();
|
|
9655
|
+
return adapter && id ? { adapter, id } : null;
|
|
9656
|
+
}
|
|
9657
|
+
/** Still the same bot behind `connection` (after an await): the same adapter object and the same bot id. */
|
|
9658
|
+
botStillOwns(connection, owner) {
|
|
9659
|
+
return this.adapters.get(connection) === owner.adapter && owner.adapter.getBotUserId?.() === owner.id;
|
|
9660
|
+
}
|
|
9661
|
+
/** keyed `<connection>\0<bot id>\0<channel>`: the bot's colour there, refreshed at most daily (monotonic), one read at a time. */
|
|
9662
|
+
webAccents = new Map();
|
|
9663
|
+
webAccentReads = new Set();
|
|
9664
|
+
webAccent(connection, owner, channelId) {
|
|
9665
|
+
const adapter = owner.adapter;
|
|
9666
|
+
if (!adapter.botAccentColor)
|
|
9667
|
+
return null;
|
|
9668
|
+
const key = `${connection}\0${owner.id}\0${channelId ?? ""}`, now = performance.now(), cached = this.webAccents.get(key);
|
|
9669
|
+
if ((!cached || now >= cached.until) && !this.webAccentReads.has(key)) {
|
|
9670
|
+
this.webAccentReads.add(key);
|
|
9671
|
+
adapter.botAccentColor(channelId)
|
|
9672
|
+
// Keyed by the bot's id: a replaced bot's late answer lands under its own key, which the new bot never reads.
|
|
9673
|
+
.then(value => { this.webAccents.set(key, { value: accentColor(value), until: performance.now() + WEB_IMAGE_MAX_AGE_MS }); }, () => { this.webAccents.set(key, { value: cached?.value ?? null, until: performance.now() + WEB_IMAGE_RETRY_MS }); })
|
|
9674
|
+
.finally(() => this.webAccentReads.delete(key));
|
|
9675
|
+
}
|
|
9676
|
+
return cached?.value ?? null;
|
|
9677
|
+
}
|
|
9678
|
+
/** The cache key of an own bot's avatar, and its loader (which refuses an answer from a bot no longer there). */
|
|
9679
|
+
botAvatarSource(connection, owner) {
|
|
9680
|
+
return {
|
|
9681
|
+
key: `bot:${connection}:${owner.id}`,
|
|
9682
|
+
load: async () => {
|
|
9683
|
+
const got = await owner.adapter.fetchBotAvatar();
|
|
9684
|
+
if (!this.botStillOwns(connection, owner))
|
|
9685
|
+
throw new Error("the bot was replaced while its avatar was fetched");
|
|
9686
|
+
return got;
|
|
9687
|
+
},
|
|
9688
|
+
};
|
|
9689
|
+
}
|
|
9690
|
+
/** One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. */
|
|
9691
|
+
webIdentityImage(name, displayName, profileAvatar, own) {
|
|
9692
|
+
let profileVersion = null;
|
|
9693
|
+
if (profileAvatar && imageMimeForFile(profileAvatar)) {
|
|
9694
|
+
try {
|
|
9695
|
+
profileVersion = Math.floor(statSync(join(this.dataDir, "avatars", basename(profileAvatar))).mtimeMs).toString(36);
|
|
9696
|
+
}
|
|
9697
|
+
catch { /* removed */ }
|
|
9698
|
+
}
|
|
9699
|
+
const connection = own.get(name), owner = connection ? this.botOwner(connection) : null;
|
|
9700
|
+
const source = connection && owner?.adapter.fetchBotAvatar ? this.botAvatarSource(connection, owner) : null;
|
|
9701
|
+
const bot = source ? this.webImageCache("avatars").peek(source.key, source.load) : null;
|
|
9702
|
+
const cfg = this.ownInstanceConfig(name);
|
|
9703
|
+
const avatar = avatarFor({ name, displayName, profileVersion, bot: bot && connection ? { connection, version: bot.version } : null,
|
|
9704
|
+
personaEmoji: cfg?.status_emojis?.delivered });
|
|
9705
|
+
// The ring belongs to the bot's own face: only when that is what is shown.
|
|
9706
|
+
const accent = !profileVersion && bot && connection && owner
|
|
9707
|
+
? this.webAccent(connection, owner, cfg ? (cfg.topic_id == null ? undefined : String(cfg.topic_id)) : this.classicChannels?.getChannelIdByInstance(name))
|
|
9708
|
+
: null;
|
|
9709
|
+
return accent ? { avatar, accent } : { avatar };
|
|
9710
|
+
}
|
|
9711
|
+
/** GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only an own-bot connection, or a known instance's upload. */
|
|
9712
|
+
webImage(kind, key) {
|
|
9713
|
+
if (kind === "bot") {
|
|
9714
|
+
if (![...this.ownBots().values()].includes(key))
|
|
9715
|
+
return null;
|
|
9716
|
+
const owner = this.botOwner(key);
|
|
9717
|
+
return owner ? this.webImageCache("avatars").read(this.botAvatarSource(key, owner).key) : null;
|
|
9718
|
+
}
|
|
9719
|
+
const known = Object.hasOwn(this.fleetConfig?.instances ?? {}, key) || this.classicChannels?.getChannelIdByInstance(key) !== undefined;
|
|
9720
|
+
if (!known)
|
|
9721
|
+
return null;
|
|
9722
|
+
const file = profileIdentities(this.dataDir).get(key)?.avatar_path;
|
|
9723
|
+
const mime = file ? imageMimeForFile(file) : null;
|
|
9724
|
+
if (!file || !mime)
|
|
9725
|
+
return null;
|
|
9726
|
+
try {
|
|
9727
|
+
const path = join(this.dataDir, "avatars", basename(file));
|
|
9728
|
+
return { bytes: readFileSync(path), mime, version: Math.floor(statSync(path).mtimeMs).toString(36) };
|
|
9729
|
+
}
|
|
9730
|
+
catch {
|
|
9731
|
+
return null;
|
|
9732
|
+
}
|
|
9733
|
+
}
|
|
9734
|
+
/** GET /ui/emojis: the custom emoji of every server the fleet's Discord bots are in (the Settings picker's cache). */
|
|
9735
|
+
async webEmojiList() {
|
|
9736
|
+
const adopted = [];
|
|
9737
|
+
for (const [id, world] of [...this.worlds]) {
|
|
9738
|
+
if (world.type !== "discord")
|
|
9739
|
+
continue;
|
|
9740
|
+
const listed = await this.listGuildEmojis(id);
|
|
9741
|
+
if (listed.ok)
|
|
9742
|
+
adopted.push({ id, world, emojis: listed.emojis });
|
|
9743
|
+
}
|
|
9744
|
+
// After the LAST await (#1635 review r2): only the lists whose world is still the one that was read — a connection
|
|
9745
|
+
// removed or replaced while a later list was awaited takes its servers along, whenever it went.
|
|
9746
|
+
const out = new Map();
|
|
9747
|
+
for (const a of adopted) {
|
|
9748
|
+
if (this.worlds.get(a.id) !== a.world)
|
|
9749
|
+
continue;
|
|
9750
|
+
for (const e of a.emojis)
|
|
9751
|
+
if (!out.has(e.id))
|
|
9752
|
+
out.set(e.id, { id: e.id, name: e.name, animated: e.animated });
|
|
9753
|
+
}
|
|
9754
|
+
return [...out.values()];
|
|
9755
|
+
}
|
|
9756
|
+
/** GET /ui/img/emoji/<id>: only an emoji of those servers; fetched once from Discord's CDN, then from the host cache. */
|
|
9757
|
+
async webEmoji(id) {
|
|
9758
|
+
if (!/^\d{15,25}$/.test(id))
|
|
9759
|
+
return null;
|
|
9760
|
+
const emoji = (await this.webEmojiList()).find(e => e.id === id);
|
|
9761
|
+
if (!emoji)
|
|
9762
|
+
return null;
|
|
9763
|
+
const image = await this.webImageCache("emoji").get(`emoji:${id}`, () => downloadStickerImage(`https://cdn.discordapp.com/emojis/${id}.${emoji.animated ? "gif" : "png"}?size=64`, { maxBytes: WEB_IMAGE_MAX_BYTES }));
|
|
9764
|
+
// Still an emoji of the fleet's servers once it is here (a connection removed meanwhile takes its servers along).
|
|
9765
|
+
return image && (await this.webEmojiList()).some(e => e.id === id) ? image : null;
|
|
9766
|
+
}
|
|
9505
9767
|
/**
|
|
9506
9768
|
* What `instanceName` may pick as its persona emoji (the `list_emojis`
|
|
9507
9769
|
* tool): its current status set with where each value comes from, the
|
|
@@ -10693,7 +10955,8 @@ export class FleetManager {
|
|
|
10693
10955
|
// on the sign-in DM, armed by the first tap). A double tap, or a late second one from an old keyboard, lands
|
|
10694
10956
|
// here with the spent nonce — and collapsing would wipe the message's text and the live buttons. The clicker is
|
|
10695
10957
|
// told; the message is left alone.
|
|
10696
|
-
|
|
10958
|
+
// #1617: and of the same bot — two bots' DM message ids can collide; another bot's live prompt says nothing here.
|
|
10959
|
+
if ([...this.pendingNonceButtons.values()].some(e => e.prefix === prefix && e.adapterId === callbackAdapterId && e.chatId === data.chatId && e.messageId === data.messageId)) {
|
|
10697
10960
|
data.ack?.(t("buttons.stale_notice"));
|
|
10698
10961
|
return "consumed";
|
|
10699
10962
|
}
|
|
@@ -15444,6 +15707,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15444
15707
|
const ch = this.classicChannels.unregister(channelId, adapterId);
|
|
15445
15708
|
if (!ch)
|
|
15446
15709
|
return t("classic.no_agent");
|
|
15710
|
+
// #1664: the room is gone (not paused or stopped): nobody's favorite any more — unless the name is still an
|
|
15711
|
+
// instance (fleet.yaml's, or another room's), so a later room of the same name does not inherit an old star.
|
|
15712
|
+
if (!Object.hasOwn(this.fleetConfig?.instances ?? {}, ch.instanceName)
|
|
15713
|
+
&& !this.classicChannels.getAll().some((c) => c.instanceName === ch.instanceName)) {
|
|
15714
|
+
try {
|
|
15715
|
+
this.webPrefs.forgetInstance(ch.instanceName);
|
|
15716
|
+
}
|
|
15717
|
+
catch (err) {
|
|
15718
|
+
this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room from web favorites failed");
|
|
15719
|
+
}
|
|
15720
|
+
}
|
|
15447
15721
|
this.instanceWorldBinding.delete(ch.instanceName);
|
|
15448
15722
|
await this.stopInstance(ch.instanceName).catch(err => this.logger.warn({ err, instanceName: ch.instanceName }, "Failed to stop classic instance"));
|
|
15449
15723
|
clearPausedMarker(this.getInstanceDir(ch.instanceName));
|
|
@@ -17940,7 +18214,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17940
18214
|
// Never the token: fleet.log is readable by anything that can read the
|
|
17941
18215
|
// data dir, is copied into bug reports, and is tailed in shared terminals.
|
|
17942
18216
|
// `/dashboard` and `agend web` are the ways to get an authorized link.
|
|
17943
|
-
this.logger.info({ url: `http://localhost:${port}/ui` }, "Web UI available (open it with /
|
|
18217
|
+
this.logger.info({ url: `http://localhost:${port}/ui` }, "Web UI available (open it with /web or `agend web`)");
|
|
17944
18218
|
this.logger.info({ url: `http://localhost:${port}/view` }, "Web View available");
|
|
17945
18219
|
// #1306: the preview listener starts once the web listener is bound — its frame-ancestors name the real port.
|
|
17946
18220
|
const bound = this.healthServer?.address();
|
|
@@ -18082,6 +18356,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18082
18356
|
// /api/profiles (resolveInstanceIdentity) — the sidebar groups, searches and labels with it on every page.
|
|
18083
18357
|
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
18084
18358
|
const profiles = profileIdentities(this.dataDir);
|
|
18359
|
+
// #1630: decorative — whatever goes wrong deciding an avatar never costs the frame (every instance gets its first character).
|
|
18360
|
+
let ownBots = new Map();
|
|
18361
|
+
try {
|
|
18362
|
+
ownBots = this.ownBots();
|
|
18363
|
+
}
|
|
18364
|
+
catch (err) {
|
|
18365
|
+
this.logger.debug({ err }, "own bots unreadable (getUiStatus)");
|
|
18366
|
+
}
|
|
18085
18367
|
const instances = names.map(name => {
|
|
18086
18368
|
const statusFile = join(this.getInstanceDir(name), "statusline.json");
|
|
18087
18369
|
let cost = 0;
|
|
@@ -18119,9 +18401,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18119
18401
|
const effort = (effortStrategy === "unsupported" || isAgy) ? null : effortResolved.effort;
|
|
18120
18402
|
const effort_source = (effortStrategy === "unsupported" || isAgy) ? null : effortResolved.source;
|
|
18121
18403
|
const identity = resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) });
|
|
18404
|
+
let image;
|
|
18405
|
+
try {
|
|
18406
|
+
image = this.webIdentityImage(name, identity.display_name, profiles.get(name)?.avatar_path, ownBots);
|
|
18407
|
+
}
|
|
18408
|
+
catch (err) {
|
|
18409
|
+
this.logger.debug({ err, name }, "avatar unreadable (getUiStatus)");
|
|
18410
|
+
image = { avatar: avatarFor({ name, displayName: identity.display_name }) };
|
|
18411
|
+
}
|
|
18122
18412
|
return {
|
|
18123
18413
|
name,
|
|
18124
18414
|
display_name: identity.display_name || undefined,
|
|
18415
|
+
avatar: image.avatar,
|
|
18416
|
+
...(image.accent ? { accent: image.accent } : {}),
|
|
18125
18417
|
description: identity.description || undefined,
|
|
18126
18418
|
role: identity.role || undefined,
|
|
18127
18419
|
tags: identity.tags,
|
|
@@ -18144,6 +18436,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18144
18436
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
18145
18437
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|
|
18146
18438
|
connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
|
|
18439
|
+
// #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
|
|
18440
|
+
prefs_rev: this.webPrefs.rev(),
|
|
18147
18441
|
};
|
|
18148
18442
|
}
|
|
18149
18443
|
}
|