@songsid/agend 2.3.0-beta.1 → 2.3.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/dist/agent-cli.js +5 -5
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-file-read.d.ts +11 -6
- package/dist/agent-file-read.js +32 -17
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +10 -13
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +10 -5
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +13 -0
- package/dist/backend/types.js +10 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +15 -0
- package/dist/cache-service.js +26 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +9 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +8 -3
- package/dist/classic-channel-manager.js +10 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +7 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +5 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +5 -5
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.js +5 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +71 -3
- package/dist/fleet-manager.js +256 -45
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +38 -0
- package/dist/fleet-traffic.js +96 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +149 -5
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
- package/dist/locale.js +6 -4
- package/dist/locale.js.map +1 -1
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +4 -0
- package/dist/outbound-handlers.js +74 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +98 -0
- package/dist/outbound-schemas.js +56 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +12 -0
- package/dist/scheduler/scheduler.js +83 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -0
- package/dist/tool-permissions.js +15 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/ui/app-effects.js +33 -9
- package/dist/ui/chat-effects.js +62 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +13 -5
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +489 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +224 -0
- package/dist/ui/panel-chat.js +8 -2
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +17 -5
- package/dist/ui/panel-settings.js +3 -2
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +34 -18
- package/dist/ui/settings-dialogs.js +3 -6
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +6 -2
- package/dist/ui/settings-mods.js +4 -3
- package/dist/ui/settings-owner-effects.js +8 -8
- package/dist/ui/settings-strings.js +34 -38
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +1 -1
- package/dist/ui/shared/app-shell.js +23 -1
- package/dist/ui/shared/app-store.js +12 -1
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +163 -12
- package/dist/ui/shared/effect-app-runtime.js +1 -1
- package/dist/ui/shared/effect-frame.js +10 -5
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +2 -1
- package/dist/ui/shared/effect-registry.js +71 -17
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-stage.js +15 -9
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/identity-card.js +5 -2
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +35 -9
- package/dist/ui/shared/panel-view.js +2 -2
- package/dist/ui/shared/usage-dialog.js +14 -2
- package/dist/ui/shared/view-strings.js +22 -2
- package/dist/ui/signin.html +1 -1
- package/dist/usage/providers.d.ts +3 -0
- package/dist/usage/providers.js +4 -3
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +21 -0
- package/dist/view-api.js +28 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +5 -6
- package/dist/web-api.js +60 -11
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-view.d.ts +10 -0
- package/dist/web-appearance-view.js +77 -8
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-asset-parts.d.ts +3 -8
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-service.js +3 -1
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.js +4 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +4 -4
- package/dist/web-effect-trials.js +6 -6
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.d.ts +2 -4
- package/dist/web-effect-upload.js +3 -8
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect.d.ts +14 -0
- package/dist/web-effect.js +21 -0
- package/dist/web-effect.js.map +1 -1
- package/dist/web-guest-access.d.ts +67 -0
- package/dist/web-guest-access.js +126 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-mod-agent.js +5 -3
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +5 -0
- package/dist/web-mod-bundle.js +6 -0
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +5 -0
- package/dist/web-mod-store.js +8 -1
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +8 -0
- package/dist/web-prefs.js +9 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +15 -7
- package/dist/web-session.js +25 -8
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-tools.d.ts +69 -0
- package/dist/web-settings-tools.js +247 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
package/dist/fleet-manager.js
CHANGED
|
@@ -158,7 +158,7 @@ import { ReplyButtonsController } from "./reply-buttons-controller.js";
|
|
|
158
158
|
import { parsePoll } from "./reply-polls.js";
|
|
159
159
|
import { isUploadRequest, limitRequestReceive, publicAttachment, sweepOrphanedUploads, UPLOAD_REQUEST_TIMEOUT_MS, uploadLimits, WebFileLedger } from "./web-upload.js";
|
|
160
160
|
import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
|
|
161
|
-
import { cardPicture, handleViewRequest, humanCards, identityColors, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
161
|
+
import { cardPicture, handleViewRequest, humanCards, identityColors, isViewPath, personIdentityColour, profileIdentities, resolveInstanceIdentity, setPersonIdentityColour } from "./view-api.js";
|
|
162
162
|
import { autoIdentityColor } from "./web-identity-color.js";
|
|
163
163
|
import { AppearanceTools } from "./web-appearance-tools.js";
|
|
164
164
|
import { cardVersion, resolveCard } from "./web-card.js";
|
|
@@ -178,7 +178,10 @@ import { runOnLoginShell } from "./login-shell.js";
|
|
|
178
178
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
179
179
|
import { decideSlash } from "./slash-authz.js";
|
|
180
180
|
import { canonicalCommand, commandSpec, decideCommand, ruleFor } from "./command-table.js";
|
|
181
|
-
import { runVisibilityCommand } from "./cross-instance-notice.js";
|
|
181
|
+
import { crossInstanceVisibility, runVisibilityCommand } from "./cross-instance-notice.js";
|
|
182
|
+
import { WebSettingsTools } from "./web-settings-tools.js";
|
|
183
|
+
import { bindGuestRequest, guestRouteDecision } from "./web-guest-access.js";
|
|
184
|
+
import { TrafficLog, trafficItem } from "./fleet-traffic.js";
|
|
182
185
|
import { installedChannel, updateNoticeKey } from "./update-check.js";
|
|
183
186
|
import { resolveInstalledAgend } from "./update-dispatch.js";
|
|
184
187
|
import { resolveUpdateLaunch, openUpdateLog, updateLogPath, watchUpdateLaunch } from "./update-launch.js";
|
|
@@ -190,7 +193,7 @@ import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
|
190
193
|
import { recentChatContext } from "./log-tail.js";
|
|
191
194
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
192
195
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
193
|
-
import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
196
|
+
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
194
197
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
195
198
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
196
199
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
@@ -209,8 +212,9 @@ import { NeedsYouHub } from "./needs-you-hub.js";
|
|
|
209
212
|
import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
|
|
210
213
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
211
214
|
import { buildOrgChart } from "./web-org.js";
|
|
215
|
+
import { topMemoryHogs } from "./memory-hogs.js";
|
|
212
216
|
import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
|
|
213
|
-
import { CacheService,
|
|
217
|
+
import { CacheService, cacheWindow } from "./cache-service.js";
|
|
214
218
|
import { runWebCommand } from "./web-commands.js";
|
|
215
219
|
import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
216
220
|
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
@@ -234,7 +238,7 @@ import { PROVIDER_SECRET_SPECS, providerSecretSpec, providerRegistryEnvKeys, isR
|
|
|
234
238
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
235
239
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
236
240
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
237
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
241
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
238
242
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
239
243
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
240
244
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
@@ -504,6 +508,8 @@ function emojiListFilter(opts) {
|
|
|
504
508
|
withImageUrls: flag(opts.with_image_urls),
|
|
505
509
|
};
|
|
506
510
|
}
|
|
511
|
+
/** #1920: a guest's scope until phase 2 derives one: nothing. */
|
|
512
|
+
const NO_GUEST_SCOPE = new Set();
|
|
507
513
|
export class FleetManager {
|
|
508
514
|
dataDir;
|
|
509
515
|
static signalTarget = null;
|
|
@@ -774,6 +780,8 @@ export class FleetManager {
|
|
|
774
780
|
/** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
|
|
775
781
|
previewListener = null;
|
|
776
782
|
previewListening = false;
|
|
783
|
+
/** The preview listener could not open its port: "<port>: <error code>", for the page (#2061 follow-up); null otherwise. */
|
|
784
|
+
previewListenError = null;
|
|
777
785
|
/** The ports the preview listener was started for, and the inputs it was built from (a reload compares them). */
|
|
778
786
|
previewPorts = null;
|
|
779
787
|
previewInputs = "";
|
|
@@ -806,6 +814,17 @@ export class FleetManager {
|
|
|
806
814
|
startedAt = 0;
|
|
807
815
|
stormOpenNotifyTimer = null;
|
|
808
816
|
memoryLogAt = null;
|
|
817
|
+
/**
|
|
818
|
+
* #2015: while host memory is elevated or critical, the processes holding the most in the fleet's service and whose
|
|
819
|
+
* they are (an instance, the fleet, tmux), for the pressure log, /health and the dashboard — never chat (#1712).
|
|
820
|
+
* Rescanned at most once a minute (async, one at a time); cleared when pressure ends.
|
|
821
|
+
*/
|
|
822
|
+
memoryTop = null;
|
|
823
|
+
memoryTopScan = { at: null, inFlight: false, loggedAt: null };
|
|
824
|
+
/** The scan (a seam for tests: they give it a fake /proc). */
|
|
825
|
+
memoryHogScan = topMemoryHogs;
|
|
826
|
+
/** The fleet's own tmux session exists (only then are its panes read: a fleet that never started has none). */
|
|
827
|
+
tmuxSessionEnsured = false;
|
|
809
828
|
memoryDebugAt = null;
|
|
810
829
|
memoryLogLevel = null;
|
|
811
830
|
/** #1685: the host_memory value the last status frame carried, so a change re-sends the frame. */
|
|
@@ -1079,6 +1098,26 @@ export class FleetManager {
|
|
|
1079
1098
|
get ownerCss() {
|
|
1080
1099
|
return (this.ownerCssStore ??= new OwnerCssStore(this.dataDir, this.webPrefs));
|
|
1081
1100
|
}
|
|
1101
|
+
webSettingsToolsService;
|
|
1102
|
+
/**
|
|
1103
|
+
* #2056: Settings → Web appearance for a tool (General, a coordinator, full) — the page's own stores and checks, and the
|
|
1104
|
+
* page's announcement after a change (web-settings-tools.ts).
|
|
1105
|
+
*/
|
|
1106
|
+
get webSettings() {
|
|
1107
|
+
return (this.webSettingsToolsService ??= new WebSettingsTools({
|
|
1108
|
+
prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
|
|
1109
|
+
instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
|
|
1110
|
+
...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
|
|
1111
|
+
colour: (name) => personIdentityColour(this.dataDir, name),
|
|
1112
|
+
setColour: (name, patch) => { const w = setPersonIdentityColour(this.dataDir, name, patch); if (w.ok)
|
|
1113
|
+
this.identityChanged(); return w; },
|
|
1114
|
+
placements: (name) => this.themeStore.get(name).placements.effects.map((e) => (typeof e === "string" ? { id: e } : e)),
|
|
1115
|
+
announce: () => { try {
|
|
1116
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1117
|
+
}
|
|
1118
|
+
catch { /* the next status frame carries it */ } },
|
|
1119
|
+
}));
|
|
1120
|
+
}
|
|
1082
1121
|
get webPrefs() {
|
|
1083
1122
|
// #1664: a favorite must be an instance of this fleet now — fleet.yaml's (own keys) or a ClassicBot room's.
|
|
1084
1123
|
return (this.webPrefsStore ??= new WebPrefsStore(this.dataDir, {
|
|
@@ -1573,6 +1612,18 @@ export class FleetManager {
|
|
|
1573
1612
|
}
|
|
1574
1613
|
}
|
|
1575
1614
|
});
|
|
1615
|
+
// #1918: the org canvas's traffic — from the row this listener's events already name, never a probe of its own.
|
|
1616
|
+
// The row as it is now (it has already changed when the event comes): its state, not the event's, is the truth.
|
|
1617
|
+
outbox.on("state", (event) => {
|
|
1618
|
+
const row = event.deliveryId ? outbox.get(event.deliveryId) : undefined;
|
|
1619
|
+
if (row)
|
|
1620
|
+
this.publishTraffic(row);
|
|
1621
|
+
});
|
|
1622
|
+
outbox.on("consumed", (event) => {
|
|
1623
|
+
const row = event.deliveryId ? outbox.get(event.deliveryId) : undefined;
|
|
1624
|
+
if (row)
|
|
1625
|
+
this.publishTraffic(row, "consumed");
|
|
1626
|
+
});
|
|
1576
1627
|
outbox.on("generation_recovered", () => this.scheduleDeliveryOutboxPump());
|
|
1577
1628
|
this.replyObligationTimer = setInterval(() => measureSyncWork("fleet.replyObligationSweep", () => this.sweepReplyObligations()), REPLY_OBLIGATION_SWEEP_MS);
|
|
1578
1629
|
this.replyObligationTimer.unref?.();
|
|
@@ -1594,6 +1645,40 @@ export class FleetManager {
|
|
|
1594
1645
|
});
|
|
1595
1646
|
this.scheduleDeliveryOutboxPump();
|
|
1596
1647
|
}
|
|
1648
|
+
/** #1918: cross-instance traffic, as the org canvas draws it (fleet-traffic.ts). */
|
|
1649
|
+
trafficLog = new TrafficLog();
|
|
1650
|
+
trafficMemo = null;
|
|
1651
|
+
/** One delivery row's traffic item, kept for the snapshot and sent to the open pages when it is news. */
|
|
1652
|
+
publishTraffic(row, state) {
|
|
1653
|
+
try {
|
|
1654
|
+
const item = trafficItem(row, this.fleetConfig, new Date().toISOString(), state);
|
|
1655
|
+
const news = item ? this.trafficLog.note(item) : null;
|
|
1656
|
+
if (news) {
|
|
1657
|
+
this.trafficMemo = null;
|
|
1658
|
+
this.emitSseEvent("traffic", news);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
this.logger.debug({ err }, "traffic: an item was not published");
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* GET /ui/traffic: what is in flight now (the outbox's pending rows) and what finished in the last two minutes, at
|
|
1667
|
+
* most TRAFFIC_SNAPSHOT_MAX items. Worked out at most once every two seconds, so the public link's polling (or any
|
|
1668
|
+
* number of pages) costs one indexed read per two seconds. Visibility is applied as of now.
|
|
1669
|
+
*/
|
|
1670
|
+
trafficSnapshot() {
|
|
1671
|
+
const nowMs = performance.now();
|
|
1672
|
+
if (this.trafficMemo && nowMs - this.trafficMemo.atMs < 2_000)
|
|
1673
|
+
return this.trafficMemo.value;
|
|
1674
|
+
const now = new Date().toISOString();
|
|
1675
|
+
const pending = (this.deliveryOutbox?.listPending() ?? [])
|
|
1676
|
+
.map((r) => trafficItem(r, this.fleetConfig, r.createdAt ?? now)).filter((i) => i !== null);
|
|
1677
|
+
const visible = (name) => crossInstanceVisibility(this.fleetConfig, name) !== "hidden";
|
|
1678
|
+
const items = this.trafficLog.snapshot(pending).filter((i) => visible(i.from) && visible(i.to));
|
|
1679
|
+
this.trafficMemo = { atMs: nowMs, value: { now, items } };
|
|
1680
|
+
return this.trafficMemo.value;
|
|
1681
|
+
}
|
|
1597
1682
|
/** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
|
|
1598
1683
|
admitDurableDelivery(input) {
|
|
1599
1684
|
const outbox = this.deliveryOutbox;
|
|
@@ -1606,6 +1691,8 @@ export class FleetManager {
|
|
|
1606
1691
|
}
|
|
1607
1692
|
const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
|
|
1608
1693
|
const admitted = outbox.admit({ ...input, sourceDaemonBootId, sourceKey });
|
|
1694
|
+
if (admitted.inserted)
|
|
1695
|
+
this.publishTraffic(admitted.delivery);
|
|
1609
1696
|
if (admitted.inserted)
|
|
1610
1697
|
this.logger.info({
|
|
1611
1698
|
deliveryId: admitted.delivery.deliveryId,
|
|
@@ -1979,17 +2066,58 @@ export class FleetManager {
|
|
|
1979
2066
|
this.logger.info({ hostMemory: snapshot }, "Host memory recovered");
|
|
1980
2067
|
}
|
|
1981
2068
|
this.memoryLogLevel = snapshot.level;
|
|
2069
|
+
if (this.memoryTop) {
|
|
2070
|
+
this.memoryTop = null;
|
|
2071
|
+
this.memoryTopScan.loggedAt = null;
|
|
2072
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
2073
|
+
}
|
|
1982
2074
|
return;
|
|
1983
2075
|
}
|
|
2076
|
+
if (snapshot.level === "elevated" || snapshot.level === "critical")
|
|
2077
|
+
this.refreshMemoryTop(snapshot);
|
|
1984
2078
|
if (changed || this.memoryLogAt === null || now - this.memoryLogAt >= 10 * 60_000) {
|
|
1985
2079
|
this.memoryLogAt = now;
|
|
1986
|
-
this.logger.warn({ hostMemory: snapshot }, snapshot.level === "critical"
|
|
2080
|
+
this.logger.warn({ hostMemory: snapshot, ...(this.memoryTop ? { topProcesses: this.memoryTop.processes } : {}) }, snapshot.level === "critical"
|
|
1987
2081
|
? "Host memory critical — deferring new CLI spawns"
|
|
1988
2082
|
: snapshot.level === "elevated" ? "Host memory pressure — slowing new CLI spawns"
|
|
1989
2083
|
: "Host memory sample unavailable — slowing new CLI spawns");
|
|
1990
2084
|
}
|
|
1991
2085
|
this.memoryLogLevel = snapshot.level;
|
|
1992
2086
|
}
|
|
2087
|
+
/**
|
|
2088
|
+
* #2015: rescan the largest processes (at most once a minute, one at a time). The first result of an episode, and
|
|
2089
|
+
* then at most every 10 minutes, is logged with the pressure; /health and the dashboard read the latest. Never chat.
|
|
2090
|
+
*/
|
|
2091
|
+
refreshMemoryTop(snapshot) {
|
|
2092
|
+
const scan = this.memoryTopScan;
|
|
2093
|
+
const now = performance.now();
|
|
2094
|
+
if (scan.inFlight || (scan.at !== null && now - scan.at < 60_000))
|
|
2095
|
+
return;
|
|
2096
|
+
scan.inFlight = true;
|
|
2097
|
+
scan.at = now;
|
|
2098
|
+
void (async () => {
|
|
2099
|
+
const session = getTmuxSession();
|
|
2100
|
+
const [tmuxServerPid, panes] = this.tmuxSessionEnsured
|
|
2101
|
+
? await Promise.all([TmuxManager.getServerPid(session), TmuxManager.listPanePids(session)])
|
|
2102
|
+
: [null, []];
|
|
2103
|
+
const processes = await this.memoryHogScan({
|
|
2104
|
+
fleetPid: process.pid, tmuxServerPid, panes,
|
|
2105
|
+
isInstance: name => this.daemons.has(name) || Object.hasOwn(this.fleetConfig?.instances ?? {}, name),
|
|
2106
|
+
}, 3);
|
|
2107
|
+
// Pressure may have ended while the scan ran: nothing is kept then.
|
|
2108
|
+
if (this.shuttingDown || processes.length === 0 || this.hostMemoryIndicator() === null)
|
|
2109
|
+
return;
|
|
2110
|
+
const before = JSON.stringify(this.memoryTop?.processes ?? null);
|
|
2111
|
+
this.memoryTop = { at: new Date().toISOString(), processes };
|
|
2112
|
+
if (scan.loggedAt === null || performance.now() - scan.loggedAt >= 10 * 60_000) {
|
|
2113
|
+
scan.loggedAt = performance.now();
|
|
2114
|
+
this.logger.warn({ hostMemory: snapshot, topProcesses: processes }, "Host memory pressure — largest processes in the fleet");
|
|
2115
|
+
}
|
|
2116
|
+
if (JSON.stringify(processes) !== before)
|
|
2117
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
2118
|
+
})().catch(err => this.logger.debug({ err }, "Largest-process scan failed"))
|
|
2119
|
+
.finally(() => { scan.inFlight = false; });
|
|
2120
|
+
}
|
|
1993
2121
|
/** Wire the one fleet-wide storm into notification and recovery surfaces. */
|
|
1994
2122
|
bindStormWindowEvents() {
|
|
1995
2123
|
this.stormWindow.on("opened", (snapshot) => {
|
|
@@ -3436,7 +3564,8 @@ export class FleetManager {
|
|
|
3436
3564
|
* for `instance` (#1320 part B). Per entry: the channel's own adapter, no
|
|
3437
3565
|
* thread, mention suppression on. Returns the number of channels posted
|
|
3438
3566
|
* to. Failures are per-entry warn-and-continue — a failed echo never fails
|
|
3439
|
-
* the web send.
|
|
3567
|
+
* the web send. Only entries classicWebEchoOn admits are touched (#2054: unset follows web.echo_to_channel; an explicit
|
|
3568
|
+
* `web_echo: false` never echoes).
|
|
3440
3569
|
*
|
|
3441
3570
|
* Every copy is fenced at start time: an entry whose explicit world is
|
|
3442
3571
|
* gone is skipped (only legacy adapterId-less entries fall back to the
|
|
@@ -3463,7 +3592,7 @@ export class FleetManager {
|
|
|
3463
3592
|
}
|
|
3464
3593
|
}
|
|
3465
3594
|
const entry = (this.classicChannels?.getAll() ?? [])
|
|
3466
|
-
.find(candidate => candidate.instanceName === instance && candidate.webEcho
|
|
3595
|
+
.find(candidate => candidate.instanceName === instance && classicWebEchoOn(candidate.webEcho, this.fleetConfig?.web?.echo_to_channel)
|
|
3467
3596
|
&& !handled.has(`${candidate.channelId}#${candidate.adapterId ?? ""}`));
|
|
3468
3597
|
if (!entry)
|
|
3469
3598
|
break;
|
|
@@ -6266,6 +6395,7 @@ export class FleetManager {
|
|
|
6266
6395
|
const { getTmuxSocketName: getSocket } = await import("./paths.js");
|
|
6267
6396
|
TmuxManager.setSocketName(getSocket());
|
|
6268
6397
|
await TmuxManager.ensureSession(getTmuxSession());
|
|
6398
|
+
this.tmuxSessionEnsured = true;
|
|
6269
6399
|
// Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
|
|
6270
6400
|
// before instances spawn, so a CLI that comes up on its sign-in screen gets
|
|
6271
6401
|
// an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
|
|
@@ -7089,7 +7219,8 @@ export class FleetManager {
|
|
|
7089
7219
|
adapters: { total: this.adapterState.size, connected, states, details },
|
|
7090
7220
|
startupComplete: this.startupComplete,
|
|
7091
7221
|
memory: readFleetMemory(),
|
|
7092
|
-
hostMemory
|
|
7222
|
+
hostMemory: this.memoryTop && (hostMemory.level === "elevated" || hostMemory.level === "critical")
|
|
7223
|
+
? { ...hostMemory, topProcesses: this.memoryTop.processes, topProcessesAt: this.memoryTop.at } : hostMemory,
|
|
7093
7224
|
problems,
|
|
7094
7225
|
};
|
|
7095
7226
|
}
|
|
@@ -12497,8 +12628,14 @@ export class FleetManager {
|
|
|
12497
12628
|
* (cache-service.ts) — never a vendor call, never a synchronous transcript read.
|
|
12498
12629
|
*/
|
|
12499
12630
|
cacheReport(window) {
|
|
12500
|
-
|
|
12501
|
-
|
|
12631
|
+
return this.cacheServiceFor().report(cacheWindow(window));
|
|
12632
|
+
}
|
|
12633
|
+
/** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
|
|
12634
|
+
cacheReportPublished(window) {
|
|
12635
|
+
return this.cacheServiceFor().published(cacheWindow(window));
|
|
12636
|
+
}
|
|
12637
|
+
cacheServiceFor() {
|
|
12638
|
+
return this.cacheService ??= new CacheService({
|
|
12502
12639
|
instances: () => this.cacheInstances(),
|
|
12503
12640
|
claudeProjectsDir: () => join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects"),
|
|
12504
12641
|
claudeKey: claudeProjectKey,
|
|
@@ -12507,7 +12644,6 @@ export class FleetManager {
|
|
|
12507
12644
|
metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
|
|
12508
12645
|
log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
|
|
12509
12646
|
});
|
|
12510
|
-
return this.cacheService.report(w);
|
|
12511
12647
|
}
|
|
12512
12648
|
/** The web's Acknowledge (any item; a signed-in session is fleet-admin level). */
|
|
12513
12649
|
acknowledgeNeedsItem(id, principal) {
|
|
@@ -13029,11 +13165,12 @@ export class FleetManager {
|
|
|
13029
13165
|
};
|
|
13030
13166
|
};
|
|
13031
13167
|
const choicesFromCache = (instance) => {
|
|
13032
|
-
const
|
|
13168
|
+
const backend = this.backendNameForInstance(instance);
|
|
13169
|
+
const cached = this.readCliEnv(backend, this.catalogProfile(backend, instance)); // the cached catalog only: never a probe here
|
|
13033
13170
|
if (!cached?.models.length)
|
|
13034
13171
|
return null;
|
|
13035
13172
|
const current = this.resolveInstanceModel(instance).model;
|
|
13036
|
-
return { current, options: cached.models.map(o => ({ id: o.id, label: this.modelChoiceLabel(o, current) })) };
|
|
13173
|
+
return { current, options: this.withCurrentModel(cached.models, current).map(o => ({ id: o.id, label: this.modelChoiceLabel(o, current) })) };
|
|
13037
13174
|
};
|
|
13038
13175
|
return runWebCommand({
|
|
13039
13176
|
// Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
|
|
@@ -15812,7 +15949,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15812
15949
|
/** #1806: null on a switch INTO claude-code — the configured model then belongs to the backend being left. */
|
|
15813
15950
|
configured = this.fleetConfig?.instances[instanceName]?.model ?? this.fleetConfig?.defaults?.model) {
|
|
15814
15951
|
const aliases = ["default", "sonnet", "opus", "haiku", "opusplan", "Fable"];
|
|
15815
|
-
const api = (this.readCliEnv("claude-code")?.apiModels ?? []).map(m => m.id);
|
|
15952
|
+
const api = (this.readCliEnv("claude-code", this.catalogProfile("claude-code", instanceName))?.apiModels ?? []).map(m => m.id);
|
|
15816
15953
|
const lower = model.trim().toLowerCase();
|
|
15817
15954
|
const bare = lower.replace(/\[1m\]$/, "");
|
|
15818
15955
|
const known = aliases.some(a => a.toLowerCase() === bare)
|
|
@@ -15840,13 +15977,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15840
15977
|
: undefined;
|
|
15841
15978
|
return classic ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
15842
15979
|
}
|
|
15843
|
-
|
|
15844
|
-
|
|
15980
|
+
/**
|
|
15981
|
+
* #2072: a catalog is the account's, so a credential profile has its own. Only claude-code's probe reads an account
|
|
15982
|
+
* catalog through a profile's login (its `GET /v1/models` with the profile's token); other backends' probes read the
|
|
15983
|
+
* process environment, so their catalog stays the backend's own.
|
|
15984
|
+
*/
|
|
15985
|
+
catalogProfile(backend, instanceName) {
|
|
15986
|
+
return backend === "claude-code" && instanceName ? this.credentialProfileOf(instanceName, backend) : null;
|
|
15987
|
+
}
|
|
15988
|
+
cliEnvPath(backend, profile = null) {
|
|
15989
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
15990
|
+
return join(getAgendHome(), "cli-env", profile ? `${safe(backend)}@${safe(profile)}.json` : `${safe(backend)}.json`);
|
|
15845
15991
|
}
|
|
15846
|
-
/** Read the cached CLI env for a backend, or null if missing/stale/unparseable. */
|
|
15847
|
-
readCliEnv(backend) {
|
|
15992
|
+
/** Read the cached CLI env for a backend (a credential profile's own, when given), or null if missing/stale/unparseable. */
|
|
15993
|
+
readCliEnv(backend, profile = null) {
|
|
15848
15994
|
try {
|
|
15849
|
-
const env = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8"));
|
|
15995
|
+
const env = JSON.parse(readFileSync(this.cliEnvPath(backend, profile), "utf-8"));
|
|
15850
15996
|
if (typeof env?.probedAt === "number" && Date.now() - env.probedAt < CLI_ENV_TTL_MS)
|
|
15851
15997
|
return env;
|
|
15852
15998
|
}
|
|
@@ -15889,10 +16035,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15889
16035
|
* previous list, never stall the command that asked for it.
|
|
15890
16036
|
*/
|
|
15891
16037
|
async probeBackendBounded(backend, opts = {}) {
|
|
15892
|
-
// #725: single-flight per backend. Explicit vendor refreshes remain a
|
|
16038
|
+
// #725: single-flight per backend (#2072: per backend and credential profile). Explicit vendor refreshes remain a
|
|
15893
16039
|
// separate flight from an ordinary probe, while sysinfo can join either.
|
|
15894
|
-
const
|
|
15895
|
-
const
|
|
16040
|
+
const profile = opts.profile ?? null;
|
|
16041
|
+
const key = profile ? `${backend}@${profile}` : backend;
|
|
16042
|
+
const regularFlight = this.pendingCliEnvProbes.get(key);
|
|
16043
|
+
const vendorFlight = this.pendingVendorCliEnvProbes.get(key);
|
|
15896
16044
|
if (opts.refreshVendorCatalog && vendorFlight)
|
|
15897
16045
|
return vendorFlight;
|
|
15898
16046
|
if (!opts.refreshVendorCatalog && (vendorFlight || regularFlight))
|
|
@@ -15903,25 +16051,26 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15903
16051
|
const epoch = this.cliEnvProbeEpoch;
|
|
15904
16052
|
const input = {
|
|
15905
16053
|
mode: "env", backend, instanceDir,
|
|
15906
|
-
config: { workingDirectory: "", instanceDir, instanceName: `probe-${backend}`, mcpServers: {}
|
|
16054
|
+
config: { workingDirectory: "", instanceDir, instanceName: `probe-${backend}`, mcpServers: {},
|
|
16055
|
+
...(profile ? { credentialStoreHome: credentialProfileStoreHome(getAgendHome(), backend, profile) } : {}) },
|
|
15907
16056
|
refreshVendorCatalog: opts.refreshVendorCatalog === true,
|
|
15908
16057
|
};
|
|
15909
|
-
const bounded = this.runBackendProbeWorker(`global:${
|
|
16058
|
+
const bounded = this.runBackendProbeWorker(`global:${key}`, input).then(probed => {
|
|
15910
16059
|
if (!probed || Array.isArray(probed) || this.shuttingDown || epoch !== this.cliEnvProbeEpoch)
|
|
15911
16060
|
return null;
|
|
15912
16061
|
try {
|
|
15913
|
-
return this.persistCliEnvProbeResult(backend, probed);
|
|
16062
|
+
return this.persistCliEnvProbeResult(backend, probed, profile);
|
|
15914
16063
|
}
|
|
15915
16064
|
catch (err) {
|
|
15916
|
-
this.logger.warn({ backend, err }, "CLI env cache write failed");
|
|
16065
|
+
this.logger.warn({ backend, profile, err }, "CLI env cache write failed");
|
|
15917
16066
|
return null;
|
|
15918
16067
|
}
|
|
15919
16068
|
}).finally(() => {
|
|
15920
16069
|
const flights = opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes;
|
|
15921
|
-
if (flights.get(
|
|
15922
|
-
flights.delete(
|
|
16070
|
+
if (flights.get(key) === bounded)
|
|
16071
|
+
flights.delete(key);
|
|
15923
16072
|
});
|
|
15924
|
-
(opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes).set(
|
|
16073
|
+
(opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes).set(key, bounded);
|
|
15925
16074
|
return bounded;
|
|
15926
16075
|
}
|
|
15927
16076
|
stopCliEnvProbes() {
|
|
@@ -16036,12 +16185,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16036
16185
|
modelDisplayForInstance(instanceName) {
|
|
16037
16186
|
return this.resolveInstanceModel(instanceName).display;
|
|
16038
16187
|
}
|
|
16188
|
+
/**
|
|
16189
|
+
* #2072: the model an instance runs is always among its choices — one its CLI's catalog does not list (an older
|
|
16190
|
+
* name, a custom provider's, a typed one) is shown first, marked, so the menu never hides what is running.
|
|
16191
|
+
*/
|
|
16192
|
+
withCurrentModel(options, currentModel) {
|
|
16193
|
+
if (!currentModel || currentModel === "default" || options.some(o => o.id === currentModel))
|
|
16194
|
+
return options;
|
|
16195
|
+
return [{ id: currentModel, label: currentModel, description: t("model.not_listed") }, ...options];
|
|
16196
|
+
}
|
|
16039
16197
|
modelChoiceLabel(option, currentModel) {
|
|
16040
16198
|
const label = option.description ? `${option.label} — ${option.description}` : option.label;
|
|
16041
16199
|
return option.id === currentModel ? `✓ ${label}` : label;
|
|
16042
16200
|
}
|
|
16043
16201
|
/** Preserve the existing cache merge rules for worker probes. */
|
|
16044
|
-
persistCliEnvProbeResult(backend, probed) {
|
|
16202
|
+
persistCliEnvProbeResult(backend, probed, profile = null) {
|
|
16045
16203
|
const env = { backend, probedAt: Date.now(), ...probed };
|
|
16046
16204
|
// An empty result must never overwrite a catalog we already have. Some
|
|
16047
16205
|
// probes hit the network (`agy models` fetches, 5s cap), so a slow moment
|
|
@@ -16050,14 +16208,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16050
16208
|
// antigravity cache replaced by an empty one. Keep the known models and
|
|
16051
16209
|
// let the fresher currentModel/version through.
|
|
16052
16210
|
if (!env.models?.length) {
|
|
16053
|
-
const previous = this.readCliEnv(backend);
|
|
16211
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16054
16212
|
if (previous?.models?.length)
|
|
16055
16213
|
env.models = previous.models;
|
|
16056
16214
|
}
|
|
16057
16215
|
// Same protection for the extended catalog: one offline moment must not
|
|
16058
16216
|
// blank a good account list for the whole cache TTL.
|
|
16059
16217
|
if (!env.apiModels?.length) {
|
|
16060
|
-
const previous = this.readCliEnv(backend);
|
|
16218
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16061
16219
|
if (previous?.apiModels?.length)
|
|
16062
16220
|
env.apiModels = previous.apiModels;
|
|
16063
16221
|
}
|
|
@@ -16065,12 +16223,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16065
16223
|
// keeps the cached levels only for that same binary: both versions known and equal, cache still valid. A help
|
|
16066
16224
|
// that was read and lists none ([]) is an answer and is written as is, so the fallback applies.
|
|
16067
16225
|
if (env.effortLevels === undefined) {
|
|
16068
|
-
const previous = this.readCliEnv(backend);
|
|
16226
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16069
16227
|
if (previous?.effortLevels && previous.version && env.version && previous.version === env.version) {
|
|
16070
16228
|
env.effortLevels = previous.effortLevels;
|
|
16071
16229
|
}
|
|
16072
16230
|
}
|
|
16073
|
-
const path = this.cliEnvPath(backend);
|
|
16231
|
+
const path = this.cliEnvPath(backend, profile);
|
|
16074
16232
|
mkdirSync(dirname(path), { recursive: true });
|
|
16075
16233
|
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
16076
16234
|
return env;
|
|
@@ -16092,6 +16250,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16092
16250
|
// indefinitely — each is wrapped by CLI_ENV_PROBE_DEADLINE_MS.
|
|
16093
16251
|
for (const b of backends)
|
|
16094
16252
|
void this.probeBackendBounded(b);
|
|
16253
|
+
// #2072: each Claude credential profile in use has its own account catalog. Only fleet.yaml's agents are walked
|
|
16254
|
+
// here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
|
|
16255
|
+
const profiles = new Set();
|
|
16256
|
+
for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
|
|
16257
|
+
if (this.backendNameForInstance(name) !== "claude-code")
|
|
16258
|
+
continue;
|
|
16259
|
+
const profile = this.catalogProfile("claude-code", name);
|
|
16260
|
+
if (profile)
|
|
16261
|
+
profiles.add(profile);
|
|
16262
|
+
}
|
|
16263
|
+
for (const profile of profiles)
|
|
16264
|
+
void this.probeBackendBounded("claude-code", { profile });
|
|
16095
16265
|
}
|
|
16096
16266
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
|
16097
16267
|
async getModelOptions(instanceName, refresh = false, onLiveProbe) {
|
|
@@ -16105,7 +16275,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16105
16275
|
*/
|
|
16106
16276
|
async getModelOptionsWithSource(instanceName, opts = {}) {
|
|
16107
16277
|
const backendName = this.backendNameForInstance(instanceName);
|
|
16108
|
-
const
|
|
16278
|
+
const profile = this.catalogProfile(backendName, instanceName);
|
|
16279
|
+
const cached = this.readCliEnv(backendName, profile);
|
|
16109
16280
|
if (!opts.refresh && cached?.models.length && !this.cliEnvNeedsRefresh(cached)) {
|
|
16110
16281
|
return { models: cached.models, source: "cache" };
|
|
16111
16282
|
}
|
|
@@ -16116,7 +16287,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16116
16287
|
// Stale, missing, or a forced refresh → probe live (also refreshes the cache).
|
|
16117
16288
|
// A newly released model is invisible until this runs, which is why staleness
|
|
16118
16289
|
// triggers it rather than waiting for the 24h hard expiry or a cold start.
|
|
16119
|
-
const env = await this.probeBackendBounded(backendName, { refreshVendorCatalog: opts.refreshVendorCatalog });
|
|
16290
|
+
const env = await this.probeBackendBounded(backendName, { refreshVendorCatalog: opts.refreshVendorCatalog, profile });
|
|
16120
16291
|
if (env?.models.length)
|
|
16121
16292
|
return { models: env.models, source: "live" };
|
|
16122
16293
|
// Probe failed or timed out: the previous list is still the best answer.
|
|
@@ -16131,7 +16302,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16131
16302
|
modelMenuChoices(instanceName, nonce, options, currentModel) {
|
|
16132
16303
|
const isClaude = this.backendNameForInstance(instanceName) === "claude-code";
|
|
16133
16304
|
const choices = [{ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:__refresh__`, label: t("model.refresh") }];
|
|
16134
|
-
for (const o of options.slice(0, isClaude ? 23 : 24)) {
|
|
16305
|
+
for (const o of this.withCurrentModel(options, currentModel).slice(0, isClaude ? 23 : 24)) {
|
|
16135
16306
|
choices.push({ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`, label: this.modelChoiceLabel(o, currentModel) });
|
|
16136
16307
|
}
|
|
16137
16308
|
if (isClaude)
|
|
@@ -16538,16 +16709,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16538
16709
|
}
|
|
16539
16710
|
}
|
|
16540
16711
|
/** Cached-or-live account catalog behind "/model → more models" (claude). */
|
|
16541
|
-
async claudeApiModelOptions() {
|
|
16542
|
-
|
|
16712
|
+
async claudeApiModelOptions(instanceName) {
|
|
16713
|
+
// #2072: the account behind THIS instance — its credential profile's catalog, else the default login's.
|
|
16714
|
+
const profile = this.catalogProfile("claude-code", instanceName);
|
|
16715
|
+
const cached = this.readCliEnv("claude-code", profile);
|
|
16543
16716
|
if (cached?.apiModels?.length)
|
|
16544
16717
|
return cached.apiModels;
|
|
16545
|
-
const env = await this.probeBackendBounded("claude-code");
|
|
16718
|
+
const env = await this.probeBackendBounded("claude-code", { profile });
|
|
16546
16719
|
return env?.apiModels ?? [];
|
|
16547
16720
|
}
|
|
16548
16721
|
/** Replace a consumed "/model" menu with the full account catalog tier. */
|
|
16549
16722
|
async expandClaudeModelMenu(pending) {
|
|
16550
|
-
const expanded = await this.claudeApiModelOptions();
|
|
16723
|
+
const expanded = await this.claudeApiModelOptions(pending.instanceName);
|
|
16551
16724
|
if (!expanded.length) {
|
|
16552
16725
|
await pending.respond(t("model.more_unavailable")).catch(() => { });
|
|
16553
16726
|
return;
|
|
@@ -19842,6 +20015,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19842
20015
|
this.rejectedHostsLogged.add(name);
|
|
19843
20016
|
this.logger.warn({ host: name }, "Web request refused: Host is not allowed (add it to web.allowed_hosts if this is a proxy you run)");
|
|
19844
20017
|
}
|
|
20018
|
+
/**
|
|
20019
|
+
* #1920: the instances a guest session may see — derived on every request, never stored. Phase 2 derives it from
|
|
20020
|
+
* the account's chat access (and an admin's naming for an open connection); until then a guest sees nothing.
|
|
20021
|
+
*/
|
|
20022
|
+
guestScope(_owner) {
|
|
20023
|
+
return NO_GUEST_SCOPE;
|
|
20024
|
+
}
|
|
19845
20025
|
/** Shared handler dispatcher; gateway admission is enforced by its separate listener. */
|
|
19846
20026
|
dispatchWebHttp(req, res, port, closing = false) {
|
|
19847
20027
|
try {
|
|
@@ -19902,6 +20082,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19902
20082
|
res.end(JSON.stringify({ error: decision.message }));
|
|
19903
20083
|
return;
|
|
19904
20084
|
}
|
|
20085
|
+
// #1920: a guest reaches only GUEST_ROUTES, within the scope derived for it now — default deny, before any
|
|
20086
|
+
// handler, on this listener and the public link's gateway alike (both come through here).
|
|
20087
|
+
if (decision.via === "session" && decision.session.tier === "guest") {
|
|
20088
|
+
const owner = decision.session.owner;
|
|
20089
|
+
const verdict = guestRouteDecision(req.method ?? "GET", parsedUrl, this.guestScope(owner));
|
|
20090
|
+
if (verdict.kind === "deny") {
|
|
20091
|
+
res.writeHead(verdict.status);
|
|
20092
|
+
res.end(JSON.stringify({ error: verdict.error }));
|
|
20093
|
+
return;
|
|
20094
|
+
}
|
|
20095
|
+
// Phase 1b: the handlers that answer per recipient (the stream, the poll, a file) know it is a guest's, and
|
|
20096
|
+
// ask its scope anew each time.
|
|
20097
|
+
const admitted = decision.session;
|
|
20098
|
+
bindGuestRequest(req, { owner, scope: () => this.guestScope(owner),
|
|
20099
|
+
current: () => !!this.webSessions && !!this.webToken && this.webSessions.isCurrent(admitted, tokenEpoch(this.webToken)) });
|
|
20100
|
+
}
|
|
19905
20101
|
}
|
|
19906
20102
|
if (req.method === "GET" && req.url === "/health") {
|
|
19907
20103
|
const health = this.getFleetHealth();
|
|
@@ -20292,6 +20488,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20292
20488
|
this.logger.warn({ code: err.code, port: settings.port }, "Preview listener unavailable; HTML previews are off");
|
|
20293
20489
|
this.previewListening = false;
|
|
20294
20490
|
this.previewListener = null;
|
|
20491
|
+
this.previewListenError = `${settings.port}: ${err.code ?? err.message}`;
|
|
20295
20492
|
});
|
|
20296
20493
|
// An optional listener never takes the fleet down: a port Node refuses outright throws here, synchronously.
|
|
20297
20494
|
try {
|
|
@@ -20302,6 +20499,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20302
20499
|
this.logger.warn({ err: err.message, port: settings.port }, "Preview listener cannot listen; HTML previews are off");
|
|
20303
20500
|
this.previewListening = false;
|
|
20304
20501
|
this.previewListener = null;
|
|
20502
|
+
this.previewListenError = `${settings.port}: ${err.code ?? err.message}`;
|
|
20305
20503
|
}
|
|
20306
20504
|
}
|
|
20307
20505
|
/** What a reload may change hot: web.preview, and the dashboard names (the shim's allow-list and frame-ancestors). */
|
|
@@ -20322,6 +20520,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20322
20520
|
}
|
|
20323
20521
|
stopPreviewListener() {
|
|
20324
20522
|
this.previewListening = false;
|
|
20523
|
+
this.previewListenError = null;
|
|
20325
20524
|
this.previewListener?.close();
|
|
20326
20525
|
this.previewListener = null;
|
|
20327
20526
|
}
|
|
@@ -20329,10 +20528,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20329
20528
|
previewForUi(hostHeader, secure) {
|
|
20330
20529
|
const listener = this.previewListening ? this.previewListener : null;
|
|
20331
20530
|
const decided = previewAvailability(listener ? listener.settings : null, hostHeader, secure, listener ? listener.origins : undefined);
|
|
20531
|
+
// Previews are on, but no listener answers: say why — its port could not be opened (the error, never "turned off"),
|
|
20532
|
+
// or it is not listening yet.
|
|
20332
20533
|
if (!listener && this.fleetConfig?.web?.preview !== false)
|
|
20333
|
-
decided
|
|
20534
|
+
Object.assign(decided, this.previewNotListening());
|
|
20334
20535
|
return { ...decided, boot: listener ? listener.bootId : null };
|
|
20335
20536
|
}
|
|
20537
|
+
previewNotListening() {
|
|
20538
|
+
const failed = this.previewListenError;
|
|
20539
|
+
return failed
|
|
20540
|
+
? { code: "listenFailed", detail: failed, reason: `Previews are unavailable: the fleet could not open its preview port (${failed}). Another program may be using it — set web.preview_port, then restart AgEnD.` }
|
|
20541
|
+
: { code: "notListening", reason: "Previews are not available on this fleet right now." };
|
|
20542
|
+
}
|
|
20336
20543
|
/**
|
|
20337
20544
|
* #1723: for one page of the public link (exposure `exposureId`, at `dashboardOrigin`): its own preview tunnel's
|
|
20338
20545
|
* origin once up, "starting" until then, why it failed if it did — or a configured web.preview_origin, used instead.
|
|
@@ -20347,8 +20554,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20347
20554
|
const configured = typeof web?.preview_origin === "string" ? normalizeOrigin(web.preview_origin) : null;
|
|
20348
20555
|
if (configured) {
|
|
20349
20556
|
const listener = this.previewListening ? this.previewListener : null;
|
|
20350
|
-
if (!listener)
|
|
20351
|
-
|
|
20557
|
+
if (!listener) {
|
|
20558
|
+
const why = this.previewNotListening();
|
|
20559
|
+
return off(why.code, why.reason, why.detail ? { detail: why.detail } : {});
|
|
20560
|
+
}
|
|
20352
20561
|
if (!configured.startsWith("https:"))
|
|
20353
20562
|
return off("needHttps", "Previews need an https web.preview_origin when the dashboard is reached over https.");
|
|
20354
20563
|
return { dashboardOrigin, previewOrigin: configured, reason: null, code: null, boot: listener.bootId, frameSrc: `${configured}/frame` };
|
|
@@ -20488,6 +20697,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20488
20697
|
// #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
|
|
20489
20698
|
// shown here rather than in chat). null when normal, unknown or advisory.
|
|
20490
20699
|
host_memory: this.hostMemoryIndicator(),
|
|
20700
|
+
// #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
|
|
20701
|
+
...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
|
|
20491
20702
|
};
|
|
20492
20703
|
}
|
|
20493
20704
|
}
|