@songsid/agend 2.3.0-beta.2 → 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-whats-new.js +2 -0
- 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 +5 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +5 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -0
- 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/delivery-outbox.js +5 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +68 -3
- package/dist/fleet-manager.js +247 -41
- 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 +13 -5
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +39 -4
- 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 +2 -0
- package/dist/outbound-handlers.js +40 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +88 -0
- package/dist/outbound-schemas.js +50 -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/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.js +4 -0
- 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-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 +152 -10
- 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 +20 -4
- package/dist/ui/shared/panel-view.js +2 -2
- package/dist/ui/shared/view-strings.js +16 -0
- 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 +57 -10
- 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-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-trials.d.ts +2 -2
- package/dist/web-effect-trials.js +3 -3
- package/dist/web-effect-trials.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-store.d.ts +5 -0
- package/dist/web-mod-store.js +6 -0
- 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/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,6 +212,7 @@ 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
217
|
import { CacheService, cacheWindow } from "./cache-service.js";
|
|
214
218
|
import { runWebCommand } from "./web-commands.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
|
}
|
|
@@ -13034,11 +13165,12 @@ export class FleetManager {
|
|
|
13034
13165
|
};
|
|
13035
13166
|
};
|
|
13036
13167
|
const choicesFromCache = (instance) => {
|
|
13037
|
-
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
|
|
13038
13170
|
if (!cached?.models.length)
|
|
13039
13171
|
return null;
|
|
13040
13172
|
const current = this.resolveInstanceModel(instance).model;
|
|
13041
|
-
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) })) };
|
|
13042
13174
|
};
|
|
13043
13175
|
return runWebCommand({
|
|
13044
13176
|
// Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
|
|
@@ -15817,7 +15949,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15817
15949
|
/** #1806: null on a switch INTO claude-code — the configured model then belongs to the backend being left. */
|
|
15818
15950
|
configured = this.fleetConfig?.instances[instanceName]?.model ?? this.fleetConfig?.defaults?.model) {
|
|
15819
15951
|
const aliases = ["default", "sonnet", "opus", "haiku", "opusplan", "Fable"];
|
|
15820
|
-
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);
|
|
15821
15953
|
const lower = model.trim().toLowerCase();
|
|
15822
15954
|
const bare = lower.replace(/\[1m\]$/, "");
|
|
15823
15955
|
const known = aliases.some(a => a.toLowerCase() === bare)
|
|
@@ -15845,13 +15977,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15845
15977
|
: undefined;
|
|
15846
15978
|
return classic ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
15847
15979
|
}
|
|
15848
|
-
|
|
15849
|
-
|
|
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`);
|
|
15850
15991
|
}
|
|
15851
|
-
/** Read the cached CLI env for a backend, or null if missing/stale/unparseable. */
|
|
15852
|
-
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) {
|
|
15853
15994
|
try {
|
|
15854
|
-
const env = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8"));
|
|
15995
|
+
const env = JSON.parse(readFileSync(this.cliEnvPath(backend, profile), "utf-8"));
|
|
15855
15996
|
if (typeof env?.probedAt === "number" && Date.now() - env.probedAt < CLI_ENV_TTL_MS)
|
|
15856
15997
|
return env;
|
|
15857
15998
|
}
|
|
@@ -15894,10 +16035,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15894
16035
|
* previous list, never stall the command that asked for it.
|
|
15895
16036
|
*/
|
|
15896
16037
|
async probeBackendBounded(backend, opts = {}) {
|
|
15897
|
-
// #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
|
|
15898
16039
|
// separate flight from an ordinary probe, while sysinfo can join either.
|
|
15899
|
-
const
|
|
15900
|
-
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);
|
|
15901
16044
|
if (opts.refreshVendorCatalog && vendorFlight)
|
|
15902
16045
|
return vendorFlight;
|
|
15903
16046
|
if (!opts.refreshVendorCatalog && (vendorFlight || regularFlight))
|
|
@@ -15908,25 +16051,26 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
15908
16051
|
const epoch = this.cliEnvProbeEpoch;
|
|
15909
16052
|
const input = {
|
|
15910
16053
|
mode: "env", backend, instanceDir,
|
|
15911
|
-
config: { workingDirectory: "", instanceDir, instanceName: `probe-${backend}`, mcpServers: {}
|
|
16054
|
+
config: { workingDirectory: "", instanceDir, instanceName: `probe-${backend}`, mcpServers: {},
|
|
16055
|
+
...(profile ? { credentialStoreHome: credentialProfileStoreHome(getAgendHome(), backend, profile) } : {}) },
|
|
15912
16056
|
refreshVendorCatalog: opts.refreshVendorCatalog === true,
|
|
15913
16057
|
};
|
|
15914
|
-
const bounded = this.runBackendProbeWorker(`global:${
|
|
16058
|
+
const bounded = this.runBackendProbeWorker(`global:${key}`, input).then(probed => {
|
|
15915
16059
|
if (!probed || Array.isArray(probed) || this.shuttingDown || epoch !== this.cliEnvProbeEpoch)
|
|
15916
16060
|
return null;
|
|
15917
16061
|
try {
|
|
15918
|
-
return this.persistCliEnvProbeResult(backend, probed);
|
|
16062
|
+
return this.persistCliEnvProbeResult(backend, probed, profile);
|
|
15919
16063
|
}
|
|
15920
16064
|
catch (err) {
|
|
15921
|
-
this.logger.warn({ backend, err }, "CLI env cache write failed");
|
|
16065
|
+
this.logger.warn({ backend, profile, err }, "CLI env cache write failed");
|
|
15922
16066
|
return null;
|
|
15923
16067
|
}
|
|
15924
16068
|
}).finally(() => {
|
|
15925
16069
|
const flights = opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes;
|
|
15926
|
-
if (flights.get(
|
|
15927
|
-
flights.delete(
|
|
16070
|
+
if (flights.get(key) === bounded)
|
|
16071
|
+
flights.delete(key);
|
|
15928
16072
|
});
|
|
15929
|
-
(opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes).set(
|
|
16073
|
+
(opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes).set(key, bounded);
|
|
15930
16074
|
return bounded;
|
|
15931
16075
|
}
|
|
15932
16076
|
stopCliEnvProbes() {
|
|
@@ -16041,12 +16185,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16041
16185
|
modelDisplayForInstance(instanceName) {
|
|
16042
16186
|
return this.resolveInstanceModel(instanceName).display;
|
|
16043
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
|
+
}
|
|
16044
16197
|
modelChoiceLabel(option, currentModel) {
|
|
16045
16198
|
const label = option.description ? `${option.label} — ${option.description}` : option.label;
|
|
16046
16199
|
return option.id === currentModel ? `✓ ${label}` : label;
|
|
16047
16200
|
}
|
|
16048
16201
|
/** Preserve the existing cache merge rules for worker probes. */
|
|
16049
|
-
persistCliEnvProbeResult(backend, probed) {
|
|
16202
|
+
persistCliEnvProbeResult(backend, probed, profile = null) {
|
|
16050
16203
|
const env = { backend, probedAt: Date.now(), ...probed };
|
|
16051
16204
|
// An empty result must never overwrite a catalog we already have. Some
|
|
16052
16205
|
// probes hit the network (`agy models` fetches, 5s cap), so a slow moment
|
|
@@ -16055,14 +16208,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16055
16208
|
// antigravity cache replaced by an empty one. Keep the known models and
|
|
16056
16209
|
// let the fresher currentModel/version through.
|
|
16057
16210
|
if (!env.models?.length) {
|
|
16058
|
-
const previous = this.readCliEnv(backend);
|
|
16211
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16059
16212
|
if (previous?.models?.length)
|
|
16060
16213
|
env.models = previous.models;
|
|
16061
16214
|
}
|
|
16062
16215
|
// Same protection for the extended catalog: one offline moment must not
|
|
16063
16216
|
// blank a good account list for the whole cache TTL.
|
|
16064
16217
|
if (!env.apiModels?.length) {
|
|
16065
|
-
const previous = this.readCliEnv(backend);
|
|
16218
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16066
16219
|
if (previous?.apiModels?.length)
|
|
16067
16220
|
env.apiModels = previous.apiModels;
|
|
16068
16221
|
}
|
|
@@ -16070,12 +16223,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16070
16223
|
// keeps the cached levels only for that same binary: both versions known and equal, cache still valid. A help
|
|
16071
16224
|
// that was read and lists none ([]) is an answer and is written as is, so the fallback applies.
|
|
16072
16225
|
if (env.effortLevels === undefined) {
|
|
16073
|
-
const previous = this.readCliEnv(backend);
|
|
16226
|
+
const previous = this.readCliEnv(backend, profile);
|
|
16074
16227
|
if (previous?.effortLevels && previous.version && env.version && previous.version === env.version) {
|
|
16075
16228
|
env.effortLevels = previous.effortLevels;
|
|
16076
16229
|
}
|
|
16077
16230
|
}
|
|
16078
|
-
const path = this.cliEnvPath(backend);
|
|
16231
|
+
const path = this.cliEnvPath(backend, profile);
|
|
16079
16232
|
mkdirSync(dirname(path), { recursive: true });
|
|
16080
16233
|
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
16081
16234
|
return env;
|
|
@@ -16097,6 +16250,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16097
16250
|
// indefinitely — each is wrapped by CLI_ENV_PROBE_DEADLINE_MS.
|
|
16098
16251
|
for (const b of backends)
|
|
16099
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 });
|
|
16100
16265
|
}
|
|
16101
16266
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
|
16102
16267
|
async getModelOptions(instanceName, refresh = false, onLiveProbe) {
|
|
@@ -16110,7 +16275,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16110
16275
|
*/
|
|
16111
16276
|
async getModelOptionsWithSource(instanceName, opts = {}) {
|
|
16112
16277
|
const backendName = this.backendNameForInstance(instanceName);
|
|
16113
|
-
const
|
|
16278
|
+
const profile = this.catalogProfile(backendName, instanceName);
|
|
16279
|
+
const cached = this.readCliEnv(backendName, profile);
|
|
16114
16280
|
if (!opts.refresh && cached?.models.length && !this.cliEnvNeedsRefresh(cached)) {
|
|
16115
16281
|
return { models: cached.models, source: "cache" };
|
|
16116
16282
|
}
|
|
@@ -16121,7 +16287,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16121
16287
|
// Stale, missing, or a forced refresh → probe live (also refreshes the cache).
|
|
16122
16288
|
// A newly released model is invisible until this runs, which is why staleness
|
|
16123
16289
|
// triggers it rather than waiting for the 24h hard expiry or a cold start.
|
|
16124
|
-
const env = await this.probeBackendBounded(backendName, { refreshVendorCatalog: opts.refreshVendorCatalog });
|
|
16290
|
+
const env = await this.probeBackendBounded(backendName, { refreshVendorCatalog: opts.refreshVendorCatalog, profile });
|
|
16125
16291
|
if (env?.models.length)
|
|
16126
16292
|
return { models: env.models, source: "live" };
|
|
16127
16293
|
// Probe failed or timed out: the previous list is still the best answer.
|
|
@@ -16136,7 +16302,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16136
16302
|
modelMenuChoices(instanceName, nonce, options, currentModel) {
|
|
16137
16303
|
const isClaude = this.backendNameForInstance(instanceName) === "claude-code";
|
|
16138
16304
|
const choices = [{ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:__refresh__`, label: t("model.refresh") }];
|
|
16139
|
-
for (const o of options.slice(0, isClaude ? 23 : 24)) {
|
|
16305
|
+
for (const o of this.withCurrentModel(options, currentModel).slice(0, isClaude ? 23 : 24)) {
|
|
16140
16306
|
choices.push({ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`, label: this.modelChoiceLabel(o, currentModel) });
|
|
16141
16307
|
}
|
|
16142
16308
|
if (isClaude)
|
|
@@ -16543,16 +16709,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16543
16709
|
}
|
|
16544
16710
|
}
|
|
16545
16711
|
/** Cached-or-live account catalog behind "/model → more models" (claude). */
|
|
16546
|
-
async claudeApiModelOptions() {
|
|
16547
|
-
|
|
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);
|
|
16548
16716
|
if (cached?.apiModels?.length)
|
|
16549
16717
|
return cached.apiModels;
|
|
16550
|
-
const env = await this.probeBackendBounded("claude-code");
|
|
16718
|
+
const env = await this.probeBackendBounded("claude-code", { profile });
|
|
16551
16719
|
return env?.apiModels ?? [];
|
|
16552
16720
|
}
|
|
16553
16721
|
/** Replace a consumed "/model" menu with the full account catalog tier. */
|
|
16554
16722
|
async expandClaudeModelMenu(pending) {
|
|
16555
|
-
const expanded = await this.claudeApiModelOptions();
|
|
16723
|
+
const expanded = await this.claudeApiModelOptions(pending.instanceName);
|
|
16556
16724
|
if (!expanded.length) {
|
|
16557
16725
|
await pending.respond(t("model.more_unavailable")).catch(() => { });
|
|
16558
16726
|
return;
|
|
@@ -19847,6 +20015,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19847
20015
|
this.rejectedHostsLogged.add(name);
|
|
19848
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)");
|
|
19849
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
|
+
}
|
|
19850
20025
|
/** Shared handler dispatcher; gateway admission is enforced by its separate listener. */
|
|
19851
20026
|
dispatchWebHttp(req, res, port, closing = false) {
|
|
19852
20027
|
try {
|
|
@@ -19907,6 +20082,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19907
20082
|
res.end(JSON.stringify({ error: decision.message }));
|
|
19908
20083
|
return;
|
|
19909
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
|
+
}
|
|
19910
20101
|
}
|
|
19911
20102
|
if (req.method === "GET" && req.url === "/health") {
|
|
19912
20103
|
const health = this.getFleetHealth();
|
|
@@ -20297,6 +20488,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20297
20488
|
this.logger.warn({ code: err.code, port: settings.port }, "Preview listener unavailable; HTML previews are off");
|
|
20298
20489
|
this.previewListening = false;
|
|
20299
20490
|
this.previewListener = null;
|
|
20491
|
+
this.previewListenError = `${settings.port}: ${err.code ?? err.message}`;
|
|
20300
20492
|
});
|
|
20301
20493
|
// An optional listener never takes the fleet down: a port Node refuses outright throws here, synchronously.
|
|
20302
20494
|
try {
|
|
@@ -20307,6 +20499,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20307
20499
|
this.logger.warn({ err: err.message, port: settings.port }, "Preview listener cannot listen; HTML previews are off");
|
|
20308
20500
|
this.previewListening = false;
|
|
20309
20501
|
this.previewListener = null;
|
|
20502
|
+
this.previewListenError = `${settings.port}: ${err.code ?? err.message}`;
|
|
20310
20503
|
}
|
|
20311
20504
|
}
|
|
20312
20505
|
/** What a reload may change hot: web.preview, and the dashboard names (the shim's allow-list and frame-ancestors). */
|
|
@@ -20327,6 +20520,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20327
20520
|
}
|
|
20328
20521
|
stopPreviewListener() {
|
|
20329
20522
|
this.previewListening = false;
|
|
20523
|
+
this.previewListenError = null;
|
|
20330
20524
|
this.previewListener?.close();
|
|
20331
20525
|
this.previewListener = null;
|
|
20332
20526
|
}
|
|
@@ -20334,10 +20528,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20334
20528
|
previewForUi(hostHeader, secure) {
|
|
20335
20529
|
const listener = this.previewListening ? this.previewListener : null;
|
|
20336
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.
|
|
20337
20533
|
if (!listener && this.fleetConfig?.web?.preview !== false)
|
|
20338
|
-
decided
|
|
20534
|
+
Object.assign(decided, this.previewNotListening());
|
|
20339
20535
|
return { ...decided, boot: listener ? listener.bootId : null };
|
|
20340
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
|
+
}
|
|
20341
20543
|
/**
|
|
20342
20544
|
* #1723: for one page of the public link (exposure `exposureId`, at `dashboardOrigin`): its own preview tunnel's
|
|
20343
20545
|
* origin once up, "starting" until then, why it failed if it did — or a configured web.preview_origin, used instead.
|
|
@@ -20352,8 +20554,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20352
20554
|
const configured = typeof web?.preview_origin === "string" ? normalizeOrigin(web.preview_origin) : null;
|
|
20353
20555
|
if (configured) {
|
|
20354
20556
|
const listener = this.previewListening ? this.previewListener : null;
|
|
20355
|
-
if (!listener)
|
|
20356
|
-
|
|
20557
|
+
if (!listener) {
|
|
20558
|
+
const why = this.previewNotListening();
|
|
20559
|
+
return off(why.code, why.reason, why.detail ? { detail: why.detail } : {});
|
|
20560
|
+
}
|
|
20357
20561
|
if (!configured.startsWith("https:"))
|
|
20358
20562
|
return off("needHttps", "Previews need an https web.preview_origin when the dashboard is reached over https.");
|
|
20359
20563
|
return { dashboardOrigin, previewOrigin: configured, reason: null, code: null, boot: listener.bootId, frameSrc: `${configured}/frame` };
|
|
@@ -20493,6 +20697,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20493
20697
|
// #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
|
|
20494
20698
|
// shown here rather than in chat). null when normal, unknown or advisory.
|
|
20495
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 } : {}),
|
|
20496
20702
|
};
|
|
20497
20703
|
}
|
|
20498
20704
|
}
|