@songsid/agend 2.3.3 → 2.3.4-beta.1

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.
Files changed (134) hide show
  1. package/README.md +3 -3
  2. package/README.zh-TW.md +3 -3
  3. package/dist/agent-cli-instructions.md +1 -1
  4. package/dist/auth-api.d.ts +16 -3
  5. package/dist/auth-api.js +22 -6
  6. package/dist/auth-api.js.map +1 -1
  7. package/dist/authority/web-authority.d.ts +15 -1
  8. package/dist/authority/web-authority.js +27 -10
  9. package/dist/authority/web-authority.js.map +1 -1
  10. package/dist/backend/claude-code.js +11 -1
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +12 -0
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/types.d.ts +7 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/config-validator.js +18 -1
  17. package/dist/config-validator.js.map +1 -1
  18. package/dist/daemon.js +8 -1
  19. package/dist/daemon.js.map +1 -1
  20. package/dist/env-file.d.ts +2 -0
  21. package/dist/env-file.js +13 -8
  22. package/dist/env-file.js.map +1 -1
  23. package/dist/fleet-manager.d.ts +25 -1
  24. package/dist/fleet-manager.js +150 -21
  25. package/dist/fleet-manager.js.map +1 -1
  26. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -1
  27. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +4 -0
  28. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  29. package/dist/instance-config-impact.js +1 -0
  30. package/dist/instance-config-impact.js.map +1 -1
  31. package/dist/instance-lifecycle.d.ts +2 -0
  32. package/dist/instance-lifecycle.js +23 -7
  33. package/dist/instance-lifecycle.js.map +1 -1
  34. package/dist/instructions.d.ts +1 -1
  35. package/dist/instructions.js +1 -1
  36. package/dist/instructions.js.map +1 -1
  37. package/dist/interaction-observation.d.ts +1 -0
  38. package/dist/interaction-observation.js +1 -1
  39. package/dist/interaction-observation.js.map +1 -1
  40. package/dist/locale.js +48 -0
  41. package/dist/locale.js.map +1 -1
  42. package/dist/next-steps.d.ts +12 -2
  43. package/dist/next-steps.js +19 -3
  44. package/dist/next-steps.js.map +1 -1
  45. package/dist/office-share-service.d.ts +57 -0
  46. package/dist/office-share-service.js +76 -11
  47. package/dist/office-share-service.js.map +1 -1
  48. package/dist/office-share.d.ts +8 -0
  49. package/dist/office-share.js +34 -0
  50. package/dist/office-share.js.map +1 -1
  51. package/dist/outbound-schemas.js +1 -1
  52. package/dist/outbound-schemas.js.map +1 -1
  53. package/dist/page-share.d.ts +122 -0
  54. package/dist/page-share.js +199 -0
  55. package/dist/page-share.js.map +1 -0
  56. package/dist/public-preview.d.ts +4 -0
  57. package/dist/public-preview.js +5 -4
  58. package/dist/public-preview.js.map +1 -1
  59. package/dist/public-web-gateway.js +5 -0
  60. package/dist/public-web-gateway.js.map +1 -1
  61. package/dist/quickstart-api.d.ts +5 -0
  62. package/dist/quickstart-api.js +1 -1
  63. package/dist/quickstart-api.js.map +1 -1
  64. package/dist/quickstart.d.ts +40 -0
  65. package/dist/quickstart.js +174 -28
  66. package/dist/quickstart.js.map +1 -1
  67. package/dist/remote-card-answer.js +2 -0
  68. package/dist/remote-card-answer.js.map +1 -1
  69. package/dist/remote-card-prompt.js +4 -2
  70. package/dist/remote-card-prompt.js.map +1 -1
  71. package/dist/selection-card.d.ts +21 -0
  72. package/dist/selection-card.js +129 -0
  73. package/dist/selection-card.js.map +1 -0
  74. package/dist/settings-api.js +4 -0
  75. package/dist/settings-api.js.map +1 -1
  76. package/dist/settings-change.js +5 -0
  77. package/dist/settings-change.js.map +1 -1
  78. package/dist/settings-effect.js +2 -0
  79. package/dist/settings-effect.js.map +1 -1
  80. package/dist/setup-host.js +2 -2
  81. package/dist/setup-host.js.map +1 -1
  82. package/dist/setup-wizard.js +2 -2
  83. package/dist/setup-wizard.js.map +1 -1
  84. package/dist/types.d.ts +10 -0
  85. package/dist/ui/chat-store.js +8 -1
  86. package/dist/ui/chat-strings.js +36 -0
  87. package/dist/ui/page-share-dialog.js +191 -0
  88. package/dist/ui/panel-chat.js +14 -0
  89. package/dist/ui/panel-notices.js +98 -0
  90. package/dist/ui/panel-settings.js +9 -2
  91. package/dist/ui/preview-pages.js +41 -0
  92. package/dist/ui/preview-panel-actions.js +161 -0
  93. package/dist/ui/preview-panel.js +209 -28
  94. package/dist/ui/preview-versions.js +155 -0
  95. package/dist/ui/preview.js +88 -3
  96. package/dist/ui/settings-registry.js +1 -0
  97. package/dist/ui/settings-strings.js +4 -0
  98. package/dist/ui/shared/app-i18n.js +2 -2
  99. package/dist/ui/shared/app-route.js +4 -0
  100. package/dist/ui/shared/app-shell.js +5 -3
  101. package/dist/ui/shared/app-store.js +71 -1
  102. package/dist/ui/shared/app-stream.js +1 -1
  103. package/dist/ui/shared/app.css +43 -1
  104. package/dist/ui/shared/app.js +22 -1
  105. package/dist/ui/shared/signin.js +12 -1
  106. package/dist/ui/shared/ui-icons.js +3 -0
  107. package/dist/ui/shared/web-authorize.js +3 -1
  108. package/dist/web-api.d.ts +26 -0
  109. package/dist/web-api.js +180 -1
  110. package/dist/web-api.js.map +1 -1
  111. package/dist/web-auth.js +7 -1
  112. package/dist/web-auth.js.map +1 -1
  113. package/dist/web-login.d.ts +2 -0
  114. package/dist/web-login.js +3 -1
  115. package/dist/web-login.js.map +1 -1
  116. package/dist/web-notices.d.ts +51 -0
  117. package/dist/web-notices.js +118 -0
  118. package/dist/web-notices.js.map +1 -0
  119. package/dist/web-only-fleet.d.ts +22 -0
  120. package/dist/web-only-fleet.js +13 -0
  121. package/dist/web-only-fleet.js.map +1 -0
  122. package/dist/web-preview.d.ts +32 -3
  123. package/dist/web-preview.js +52 -19
  124. package/dist/web-preview.js.map +1 -1
  125. package/dist/web-session.d.ts +7 -0
  126. package/dist/web-session.js +2 -0
  127. package/dist/web-session.js.map +1 -1
  128. package/dist/web-settings-patch.d.ts +3 -1
  129. package/dist/web-settings-patch.js +4 -1
  130. package/dist/web-settings-patch.js.map +1 -1
  131. package/dist/web-shell-routes.d.ts +2 -0
  132. package/dist/web-shell-routes.js +3 -0
  133. package/dist/web-shell-routes.js.map +1 -1
  134. package/package.json +1 -1
@@ -9,3 +9,5 @@
9
9
  * - a key that appears twice: the last one wins.
10
10
  */
11
11
  export declare function parseEnvText(text: string): Map<string, string>;
12
+ /** The key a .env line sets, as parseEnvText reads it (`export KEY=…` included); null for a blank, comment or keyless line. */
13
+ export declare function envLineKey(line: string): string | null;
package/dist/env-file.js CHANGED
@@ -11,17 +11,22 @@
11
11
  export function parseEnvText(text) {
12
12
  const out = new Map();
13
13
  for (const line of text.split("\n")) {
14
- const trimmed = line.trim();
15
- if (!trimmed || trimmed.startsWith("#"))
16
- continue;
17
- const eqIdx = trimmed.indexOf("=");
18
- if (eqIdx < 0)
19
- continue;
20
- const key = trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim();
14
+ const key = envLineKey(line);
21
15
  if (!key)
22
16
  continue;
23
- out.set(key, trimmed.slice(eqIdx + 1).replace(/^["'](.*)["']$/, "$1"));
17
+ const trimmed = line.trim();
18
+ out.set(key, trimmed.slice(trimmed.indexOf("=") + 1).replace(/^["'](.*)["']$/, "$1"));
24
19
  }
25
20
  return out;
26
21
  }
22
+ /** The key a .env line sets, as parseEnvText reads it (`export KEY=…` included); null for a blank, comment or keyless line. */
23
+ export function envLineKey(line) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith("#"))
26
+ return null;
27
+ const eqIdx = trimmed.indexOf("=");
28
+ if (eqIdx < 0)
29
+ return null;
30
+ return trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim() || null;
31
+ }
27
32
  //# sourceMappingURL=env-file.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../src/env-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAClD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,GAAG,CAAC;YAAE,SAAS;QACxB,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrE,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../src/env-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+HAA+H;AAC/H,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;AAC1E,CAAC"}
@@ -4,6 +4,7 @@ import { AppIconStore } from "./web-app-icon.js";
4
4
  import { type NamedTestResult } from "./tunnel/named-link-test.js";
5
5
  import { OfficeShareService } from "./office-share-service.js";
6
6
  import { type OfficeShare } from "./office-share.js";
7
+ import type { PageShare } from "./page-share.js";
7
8
  import type { LoginCodeOwner } from "./web-login.js";
8
9
  import { type ProfileTicket } from "./runtime-cpu-profile.js";
9
10
  import type { CpuProfile } from "./cpu-profile.js";
@@ -100,6 +101,7 @@ import { type MemoryHog } from "./memory-hogs.js";
100
101
  import { type CacheReport } from "./cache-service.js";
101
102
  import { type WebCommandResult } from "./web-commands.js";
102
103
  import { type SessionRecord } from "./web-session.js";
104
+ import { WebNotices } from "./web-notices.js";
103
105
  import { VERIFIED_SESSION_HOURS, type HumanAuthority, type WebAuthorityRefusal, type WebAuthorityResult, type WebPeer } from "./authority/web-authority.js";
104
106
  import { type AuthorityState } from "./authority/authority-state.js";
105
107
  import { type SessionAuthorization } from "./authority/session-authorizations.js";
@@ -687,6 +689,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
687
689
  * `share` null with `count`: "Revoke all".
688
690
  */
689
691
  officeShareNotice(event: "created" | "revoked", who: string, share: OfficeShare | null, count: number): void;
692
+ /** #2911: the open page shares changed — the page hears it; each one handed out that ended is said in its channel. */
693
+ pageSharesChanged(_list: PageShare[], ended: PageShare[]): void;
694
+ /** #2911: a page share was made — said in the channel of the instance whose chat it came from (never its URL). */
695
+ pageShareNotice(who: string, share: PageShare): void;
690
696
  /** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
691
697
  private officeShareOwnerCurrent;
692
698
  private officeActsInstance?;
@@ -2289,7 +2295,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2289
2295
  private handleOutboundFromInstance;
2290
2296
  /** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
2291
2297
  /** No chat platform is configured at all: the web dashboard is the fleet's only surface. */
2292
- private isWebOnlyFleet;
2298
+ /** #2910: no Discord or Telegram connection at all (src/web-only-fleet.ts). */
2299
+ isWebOnlyFleet(): boolean;
2293
2300
  /** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
2294
2301
  private webOnlyPromptPlace;
2295
2302
  /** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
@@ -2852,6 +2859,16 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2852
2859
  notifyFleetError(text: string, opts?: {
2853
2860
  throttle?: boolean;
2854
2861
  }): boolean;
2862
+ private webNotices;
2863
+ /** #2910 D2 (web-api): the Notices on a web-only fleet; null on any other (its General says them). */
2864
+ webNoticesFeed(): WebNotices | null;
2865
+ /** #2910 D2: the web-only fleet's Notices (web-notices.json), read on first use. */
2866
+ webNoticeStore(): WebNotices | null;
2867
+ /**
2868
+ * #2910 D2: on a web-only fleet (no chat connection at all), keep what General or an agent's topic would have been
2869
+ * told as a Notice, and push it to the dashboard. False on any other fleet (its chat says it; nothing is kept).
2870
+ */
2871
+ private recordWebNotice;
2855
2872
  private static readonly FLEET_ERROR_THROTTLE_MS;
2856
2873
  private fleetErrorNotices;
2857
2874
  /**
@@ -3144,6 +3161,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
3144
3161
  ok: false;
3145
3162
  refusal: WebAuthorityRefusal;
3146
3163
  };
3164
+ /**
3165
+ * #2910 D1 (auth-api, at sign-in): a web-only fleet's host session — signed in with the host's own code (`agend web
3166
+ * --code`) on this computer — is the host operator, authorized from now for max_session_hours (as an account's
3167
+ * Confirm would be), under the current epoch: a revocation ends it, and only a new sign-in on this computer gives it
3168
+ * again. A fleet with a chat connection, another session, or another peer: nothing (§3 as before).
3169
+ */
3170
+ authorizeHostSession(session: SessionRecord, peer: WebPeer): void;
3147
3171
  private authorityStateStore;
3148
3172
  /**
3149
3173
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -28,6 +28,7 @@ import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, pub
28
28
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
29
29
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
30
30
  import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
31
+ import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
31
32
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
32
33
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
33
34
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
@@ -264,7 +265,8 @@ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCooki
264
265
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
265
266
  import { isWebPageNavigation } from "./web-shell-routes.js";
266
267
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
267
- import { authorityNow, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
268
+ import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
269
+ import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
268
270
  import { AuthorityStateStore } from "./authority/authority-state.js";
269
271
  import { SessionAuthorizations } from "./authority/session-authorizations.js";
270
272
  import { SessionAuthorizationRequests } from "./web-session-authorization.js";
@@ -1201,6 +1203,11 @@ export class FleetManager {
1201
1203
  officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
1202
1204
  ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
1203
1205
  changed: () => this.emitSseEvent("office_shares", {}),
1206
+ // #2911: page shares on the same tunnel. Their end (expired, stopped, the feature off, the maker no longer an admin,
1207
+ // the fleet stopping) is said in the page's own channel, as its start is (pageShareNotice); never its URL.
1208
+ pageConfig: () => this.fleetConfig?.web?.page_share,
1209
+ pageCdn: () => this.fleetConfig?.web?.preview_cdn === true,
1210
+ pagesChanged: (list, ended) => this.pageSharesChanged(list, ended),
1204
1211
  log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
1205
1212
  }));
1206
1213
  }
@@ -1215,6 +1222,17 @@ export class FleetManager {
1215
1222
  : share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
1216
1223
  this.notifyFleetError(text, { throttle: false });
1217
1224
  }
1225
+ /** #2911: the open page shares changed — the page hears it; each one handed out that ended is said in its channel. */
1226
+ pageSharesChanged(_list, ended) {
1227
+ this.emitSseEvent("page_shares", {});
1228
+ for (const s of ended)
1229
+ this.notifyInstanceTopic(s.instance, t("page_share.ended_notice", s.title || t("page_share.untitled")));
1230
+ }
1231
+ /** #2911: a page share was made — said in the channel of the instance whose chat it came from (never its URL). */
1232
+ pageShareNotice(who, share) {
1233
+ const minutes = Math.round((share.expiresAt - share.createdAt) / 60_000);
1234
+ this.notifyInstanceTopic(share.instance, t("page_share.created_notice", who, share.title || t("page_share.untitled"), minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`));
1235
+ }
1218
1236
  /** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
1219
1237
  officeShareOwnerCurrent(owner) {
1220
1238
  const m = /^person:([^:]+):(.+)$/.exec(owner);
@@ -1612,6 +1630,17 @@ export class FleetManager {
1612
1630
  const actor = verified();
1613
1631
  if (!actor)
1614
1632
  return null;
1633
+ // #2910 D1: a web-only fleet's host operator (its `agend web --code` session on this computer) confirms its own
1634
+ // change — the trust `agend settings confirm` has here; there is no account to be an admin of and no General to
1635
+ // ask. Asked again at execution: still the host operator now (this computer, not revoked or run out), still web-only.
1636
+ const hostOperator = (a) => !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId && this.isWebOnlyFleet();
1637
+ if (hostOperator(actor)) {
1638
+ return {
1639
+ label: t("settings_confirm.host_operator_label"),
1640
+ current: () => hostOperator(verified()),
1641
+ done: view => { this.logger.info({ id: view.id, state: view.state }, "Settings change confirmed from the web by this computer's admin"); },
1642
+ };
1643
+ }
1615
1644
  const chat = this.settingsChatAuthority(store, id);
1616
1645
  if (!chat)
1617
1646
  return null;
@@ -5690,8 +5719,7 @@ export class FleetManager {
5690
5719
  }
5691
5720
  /** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
5692
5721
  async startPersistedPausedInstance(name, transition) {
5693
- const topicMode = this.fleetConfig?.channel?.mode === "topic"
5694
- || !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
5722
+ const topicMode = daemonTopicMode(this.fleetConfig);
5695
5723
  const fleetConfig = this.fleetConfig?.instances[name];
5696
5724
  if (fleetConfig) {
5697
5725
  await this.startInstance(name, fleetConfig, topicMode, "fleet-topic", false, transition);
@@ -5902,8 +5930,7 @@ export class FleetManager {
5902
5930
  this.logger.info({ name, attempt: attempt + 1 }, "Startup retry deferred — tmux storm window is blocking spawns");
5903
5931
  return;
5904
5932
  }
5905
- const topicMode = this.fleetConfig?.channel?.mode === "topic"
5906
- || !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
5933
+ const topicMode = daemonTopicMode(this.fleetConfig);
5907
5934
  this.startupRetriesRunning.add(name);
5908
5935
  try {
5909
5936
  await this.spawnGate.run({
@@ -6426,7 +6453,7 @@ export class FleetManager {
6426
6453
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
6427
6454
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
6428
6455
  });
6429
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
6456
+ const topicMode = daemonTopicMode(this.fleetConfig);
6430
6457
  await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
6431
6458
  : this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
6432
6459
  this.stormWindow.markRecovered(name);
@@ -6729,6 +6756,7 @@ export class FleetManager {
6729
6756
  if (!publicPreviewWanted(this.fleetConfig?.web))
6730
6757
  return null;
6731
6758
  return new PublicPreview({ exposureId: id, expiresAt, dashboardOrigin, isCurrent, manager: this.getPreviewTunnel(),
6759
+ cdn: () => this.fleetConfig?.web?.preview_cdn === true,
6732
6760
  provider: () => new CloudflaredProvider({ binaryName: binary, protocol }),
6733
6761
  log: (event, detail) => this.logger.info({ event, exposureId: id, ...(detail ? { detail } : {}) }, "Public web link") });
6734
6762
  },
@@ -7690,7 +7718,8 @@ export class FleetManager {
7690
7718
  return;
7691
7719
  // The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
7692
7720
  // sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
7693
- this.notifyFleetError(t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
7721
+ // #2910 D2: a web-only fleet has no /web to revoke with — its Notice names the host's way (every browser signed out).
7722
+ this.notifyFleetError(t(this.webOnlyPromptPlace() ? "web.login_notice_web_only" : "web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
7694
7723
  }
7695
7724
  getDashboardAccess() {
7696
7725
  return { ready: this.healthServerListening, token: this.webToken };
@@ -8110,7 +8139,8 @@ export class FleetManager {
8110
8139
  // Must run before the first General/CLI spawn: pressure can hold startup.
8111
8140
  // Logs remain available even before the adapters and health listener exist.
8112
8141
  this.memoryPressure.start();
8113
- const topicMode = fleet.channel?.mode === "topic" || !!fleet.channels?.some(ch => ch.mode === "topic");
8142
+ // #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
8143
+ const topicMode = daemonTopicMode(fleet);
8114
8144
  // Set tmux socket isolation for custom AGEND_HOME
8115
8145
  const { getTmuxSocketName: getSocket } = await import("./paths.js");
8116
8146
  TmuxManager.setSocketName(getSocket());
@@ -8327,7 +8357,8 @@ export class FleetManager {
8327
8357
  }
8328
8358
  if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
8329
8359
  this.saveFleetConfig();
8330
- if (topicMode && (fleet.channel || fleet.channels?.length)) {
8360
+ // #2910: the scheduler (schedules, the task board, decisions) runs on a web-only fleet too; it needs no chat.
8361
+ if (topicMode) {
8331
8362
  const schedulerConfig = {
8332
8363
  ...DEFAULT_SCHEDULER_CONFIG,
8333
8364
  ...this.fleetConfig?.defaults.scheduler,
@@ -10836,7 +10867,10 @@ export class FleetManager {
10836
10867
  // else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
10837
10868
  const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
10838
10869
  if (this.worlds.size === 0 && !webOnlyReply) {
10839
- respond(null, "Channel adapters are not ready — retry shortly");
10870
+ // #2910: on a web-only fleet there will never be an adapter — say so, so the agent does not retry forever.
10871
+ respond(null, this.isWebOnlyFleet()
10872
+ ? `${tool} is not available on a web-only fleet (no Discord or Telegram connection); answer with reply`
10873
+ : "Channel adapters are not ready — retry shortly");
10840
10874
  return;
10841
10875
  }
10842
10876
  // Resolve threadId: use sender's topic_id if sender is a known fleet instance,
@@ -11036,9 +11070,9 @@ export class FleetManager {
11036
11070
  }
11037
11071
  /** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
11038
11072
  /** No chat platform is configured at all: the web dashboard is the fleet's only surface. */
11073
+ /** #2910: no Discord or Telegram connection at all (src/web-only-fleet.ts). */
11039
11074
  isWebOnlyFleet() {
11040
- const config = this.fleetConfig;
11041
- return config != null && !config.channel && !(config.channels?.length);
11075
+ return isWebOnlyFleetConfig(this.fleetConfig);
11042
11076
  }
11043
11077
  /** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
11044
11078
  webOnlyPromptPlace() {
@@ -11413,6 +11447,9 @@ export class FleetManager {
11413
11447
  // through the target's adapter, so a Telegram-created schedule for a
11414
11448
  // Discord target announced its failure into the wrong platform.
11415
11449
  const adapter = this.scheduleSourceAdapter(schedule);
11450
+ // #2910 D2: a web-only fleet has no chat to say it in: a Notice about the schedule's target.
11451
+ if (!adapter && this.recordWebNotice("schedule", `⏰ Schedule "${schedule.label ?? schedule.id}" trigger failed: instance ${schedule.target} is offline.`, schedule.target))
11452
+ return;
11416
11453
  if (!adapter) {
11417
11454
  this.logger.warn({ scheduleId: schedule.id, chatId: schedule.reply_chat_id, source: schedule.source }, "No adapter can reach the schedule's source chat — failure notice not sent");
11418
11455
  return;
@@ -14221,6 +14258,8 @@ export class FleetManager {
14221
14258
  * back to posting at the bare group id, which is the defect this replaced.
14222
14259
  */
14223
14260
  postDailySummary(text) {
14261
+ if (this.recordWebNotice("summary", text))
14262
+ return; // #2910 D2: a web-only fleet's Notices
14224
14263
  const target = this.fleetNoticeTarget();
14225
14264
  if (!this.adapter || !target) {
14226
14265
  this.logger.warn("Daily summary has no postable target — set a General topic or channel.options.general_channel_id");
@@ -14273,6 +14312,10 @@ export class FleetManager {
14273
14312
  dispatched = true;
14274
14313
  }
14275
14314
  }
14315
+ // #2910 D2: a web-only fleet has no General: the notice goes to the dashboard's Notices instead (and spends the
14316
+ // throttle window like a post would).
14317
+ if (!dispatched && this.recordWebNotice("fleet", body))
14318
+ dispatched = true;
14276
14319
  if (!dispatched) {
14277
14320
  // Do NOT record the throttle key. It used to be claimed before delivery
14278
14321
  // was attempted, so a message nobody could receive — every fleet error
@@ -14295,6 +14338,31 @@ export class FleetManager {
14295
14338
  }
14296
14339
  return true;
14297
14340
  }
14341
+ webNotices = null;
14342
+ /** #2910 D2 (web-api): the Notices on a web-only fleet; null on any other (its General says them). */
14343
+ webNoticesFeed() { return this.webOnlyPromptPlace() ? this.webNoticeStore() : null; }
14344
+ /** #2910 D2: the web-only fleet's Notices (web-notices.json), read on first use. */
14345
+ webNoticeStore() {
14346
+ if (!this.dataDir)
14347
+ return null;
14348
+ this.webNotices ??= new WebNotices(join(this.dataDir, WEB_NOTICES_FILE), this.logger);
14349
+ return this.webNotices;
14350
+ }
14351
+ /**
14352
+ * #2910 D2: on a web-only fleet (no chat connection at all), keep what General or an agent's topic would have been
14353
+ * told as a Notice, and push it to the dashboard. False on any other fleet (its chat says it; nothing is kept).
14354
+ */
14355
+ recordWebNotice(kind, text, instance) {
14356
+ if (!this.webOnlyPromptPlace())
14357
+ return false;
14358
+ const store = this.webNoticeStore();
14359
+ if (!store)
14360
+ return false;
14361
+ const known = instance !== undefined && Object.hasOwn(this.fleetConfig?.instances ?? {}, instance) ? instance : undefined;
14362
+ const notice = store.add({ kind, text: stripPlaceRefs(text), ...(known ? { instance: known } : {}) });
14363
+ this.emitSseEvent("notice", { notice, unread: store.unread() });
14364
+ return true;
14365
+ }
14298
14366
  static FLEET_ERROR_THROTTLE_MS = 10 * 60_000;
14299
14367
  fleetErrorNotices = new Map();
14300
14368
  /**
@@ -14308,6 +14376,9 @@ export class FleetManager {
14308
14376
  * would have to make this async and change every caller.
14309
14377
  */
14310
14378
  notifyInstanceTopic(instanceName, text, extraOpts) {
14379
+ // #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
14380
+ if (this.webOnlyPromptPlace())
14381
+ return this.recordWebNotice("instance", text, instanceName);
14311
14382
  const send = this.sendToInstanceTopic(instanceName, text, extraOpts);
14312
14383
  if (!send)
14313
14384
  return false;
@@ -15584,6 +15655,7 @@ export class FleetManager {
15584
15655
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
15585
15656
  authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
15586
15657
  policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
15658
+ hostOperator: this.isWebOnlyFleet(),
15587
15659
  });
15588
15660
  }
15589
15661
  sessionAuthorizations = null;
@@ -15616,6 +15688,33 @@ export class FleetManager {
15616
15688
  store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
15617
15689
  return { ok: true, ...store.authorize(session.handle, session.owner, now, state.epoch, now + policy.maxSessionMs, peer) };
15618
15690
  }
15691
+ /**
15692
+ * #2910 D1 (auth-api, at sign-in): a web-only fleet's host session — signed in with the host's own code (`agend web
15693
+ * --code`) on this computer — is the host operator, authorized from now for max_session_hours (as an account's
15694
+ * Confirm would be), under the current epoch: a revocation ends it, and only a new sign-in on this computer gives it
15695
+ * again. A fleet with a chat connection, another session, or another peer: nothing (§3 as before).
15696
+ */
15697
+ authorizeHostSession(session, peer) {
15698
+ const store = this.sessionAuthorizationStore();
15699
+ if (!store || !session.host || session.owner || !this.isWebOnlyFleet())
15700
+ return;
15701
+ const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
15702
+ const check = evaluateWebAuthority({
15703
+ session, gateway: false, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
15704
+ isAdmin: () => false,
15705
+ authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
15706
+ policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
15707
+ hostOperator: true,
15708
+ });
15709
+ if (check.refusal) {
15710
+ this.logger.info({ handle: session.handle, refusal: check.refusal }, "Host web session not authorized");
15711
+ return;
15712
+ }
15713
+ const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
15714
+ store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
15715
+ const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
15716
+ this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
15717
+ }
15619
15718
  authorityStateStore = null;
15620
15719
  /**
15621
15720
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -15671,7 +15770,10 @@ export class FleetManager {
15671
15770
  const sessions = new Map((this.webSessions?.list(undefined, () => true) ?? []).map(r => [r.handle, r]));
15672
15771
  const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
15673
15772
  .filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
15674
- .map(a => ({ handle: a.handle, label: sessions.get(a.handle).label, account: `${a.owner.adapterId}:${a.owner.userId}`, authorizedAt: a.authorizedAt,
15773
+ .map(a => ({ handle: a.handle, label: sessions.get(a.handle).label,
15774
+ // #2910 D1: the host operator's own session says whose it is, not "host:operator".
15775
+ account: a.owner.adapterId === HOST_OPERATOR.adapterId && a.owner.userId === HOST_OPERATOR.userId && this.isWebOnlyFleet()
15776
+ ? t("settings_confirm.host_operator_label") : `${a.owner.adapterId}:${a.owner.userId}`, authorizedAt: a.authorizedAt,
15675
15777
  until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry),
15676
15778
  // Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
15677
15779
  ...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
@@ -15781,6 +15883,8 @@ export class FleetManager {
15781
15883
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
15782
15884
  authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
15783
15885
  policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
15886
+ // #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
15887
+ hostOperator: this.isWebOnlyFleet(),
15784
15888
  });
15785
15889
  return r.refusal;
15786
15890
  }
@@ -16088,8 +16192,10 @@ export class FleetManager {
16088
16192
  ? [{ action: "resume", label: t("inst.session_oom_resume") }, { action: "fresh", label: t("inst.session_oom_fresh") }]
16089
16193
  : [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }];
16090
16194
  const extra = sessionOomRetry ? { sessionOomRetry, ...(current ? { confirmationCurrent: current } : {}) } : undefined;
16091
- this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
16092
16195
  const web = this.webOnlyPromptPlace();
16196
+ // A pointer to General's controls; on a web-only fleet (#2910 D2) the controls are in Needs you and nothing points there.
16197
+ if (!web)
16198
+ this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
16093
16199
  if (web) {
16094
16200
  await this.postNonceButtonPrompt({
16095
16201
  prefix, alertType: "exit_restart", instanceName, ...web,
@@ -16168,11 +16274,19 @@ export class FleetManager {
16168
16274
  * contain usernames or secret-adjacent text and stays in daemon.log.
16169
16275
  */
16170
16276
  async notifyInteractivePrompt(instanceName, kind) {
16171
- this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
16277
+ // #2910 D2: on a web-only fleet the assist prompt is in Needs you; there is no General to point to.
16278
+ if (!this.webOnlyPromptPlace())
16279
+ this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
16172
16280
  // Multi-world fleets can have one General per bot/platform. Route the assist
16173
16281
  // request to the General bound to the same adapter as the blocked instance.
16174
16282
  const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
16175
16283
  if (!generalName) {
16284
+ // #2910 D4: a web-only fleet with no General to ask for help — and a password or sudo prompt is not answered from a
16285
+ // web page: a Notice says where it waits (its View, where the person types it).
16286
+ if (this.webOnlyPromptPlace()) {
16287
+ this.recordWebNotice("instance", t("interactive.web_only_notice", instanceName, this.interactivePromptLabel(kind)), instanceName);
16288
+ return;
16289
+ }
16176
16290
  this.logger.warn({ instanceName }, "Interactive prompt has no General topic notification target");
16177
16291
  return;
16178
16292
  }
@@ -16331,6 +16445,10 @@ export class FleetManager {
16331
16445
  * was gone; this is the fallback).
16332
16446
  */
16333
16447
  async postRemoteCard(instanceName, card) {
16448
+ // #2910 D4: a CLI's own dialog read as a card (selection-card.ts) is put to people only on a web-only fleet — the
16449
+ // dashboard; a chat fleet keeps its parked notice and its General's controls, as before.
16450
+ if (card.kind === "question" && card.webOnly && !this.webOnlyPromptPlace())
16451
+ return;
16334
16452
  this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
16335
16453
  const place = this.remoteCardPlace(instanceName);
16336
16454
  if (!place) {
@@ -21624,7 +21742,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21624
21742
  this.reconcilePreviewListener();
21625
21743
  this.publicWebLink?.refresh();
21626
21744
  const newInstances = this.fleetConfig.instances;
21627
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
21745
+ const topicMode = daemonTopicMode(this.fleetConfig);
21628
21746
  // Only what a fresh process can adopt, and only relative to the signature
21629
21747
  // this process came up on: a Settings edit mutates this.fleetConfig in place
21630
21748
  // before the reload, so comparing the pre-load copy sees nothing at all.
@@ -23203,7 +23321,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23203
23321
  }
23204
23322
  const fleet = this.loadConfig(this.configPath);
23205
23323
  this.fleetConfig = fleet;
23206
- const topicMode = fleet.channel?.mode === "topic" || !!fleet.channels?.some(ch => ch.mode === "topic");
23324
+ const topicMode = daemonTopicMode(fleet);
23207
23325
  const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
23208
23326
  // Phase 1: generals first
23209
23327
  const restartEntries = Object.entries(fleet.instances);
@@ -23355,7 +23473,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23355
23473
  // Only notify when target is genuinely newer (semver), so a beta user on
23356
23474
  // 2.0.8-beta.16 is never told that stable 2.0.7 is "available".
23357
23475
  if (target && this.semverGt(target, currentVersion)) {
23358
- const generalId = this.findGeneralInstance();
23476
+ // #2910 D2 (#2925 review): a web-only fleet first — even with a General instance it has no chat for /update.
23477
+ const generalId = this.webOnlyPromptPlace() ? null : this.findGeneralInstance();
23359
23478
  if (generalId) {
23360
23479
  // No release URL — Discord's SuppressEmbeds proved unreliable and the
23361
23480
  // link preview looked bad. Version + /update instruction is enough.
@@ -23366,6 +23485,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23366
23485
  if (key)
23367
23486
  this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23368
23487
  }
23488
+ else if (this.webOnlyPromptPlace() && (fromChannel || updateNoticeKey(currentVersion, target))) {
23489
+ // #2910 D2: a web-only fleet: a Notice, once per version (the check runs daily); installed from this computer.
23490
+ const text = t("update.available_web_only", `v${target}`, `v${currentVersion}`);
23491
+ if (this.webNoticeStore()?.latest("update")?.text !== text)
23492
+ this.recordWebNotice("update", text);
23493
+ }
23369
23494
  }
23370
23495
  }
23371
23496
  catch { /* silent — network issues */ }
@@ -23679,7 +23804,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23679
23804
  if (decision.reason === "no-credential" && req.method === "GET"
23680
23805
  && String(req.headers.accept ?? "").includes("text/html")
23681
23806
  && isWebPageNavigation(requestPath)) {
23682
- serveSigninPage(res, 401);
23807
+ serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
23683
23808
  return;
23684
23809
  }
23685
23810
  res.writeHead(decision.status);
@@ -23902,7 +24027,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23902
24027
  (async () => {
23903
24028
  let held = false;
23904
24029
  try {
23905
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
24030
+ const topicMode = daemonTopicMode(this.fleetConfig);
23906
24031
  await withSpawnHoldReply(async (message) => {
23907
24032
  held = true;
23908
24033
  res.writeHead(202, { "Content-Type": "application/json" });
@@ -24137,7 +24262,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24137
24262
  }
24138
24263
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
24139
24264
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
24140
- extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
24265
+ extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; },
24266
+ // #2911 (7): CDN libraries in previews, as fleet.yaml says now (a change applies to the next preview started).
24267
+ cdn: () => this.fleetConfig?.web?.preview_cdn === true });
24141
24268
  this.previewListener = listener;
24142
24269
  listener.server.on("error", (err) => {
24143
24270
  if (this.previewListener !== listener)
@@ -24357,6 +24484,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24357
24484
  // #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
24358
24485
  // shown here rather than in chat). null when normal, unknown or advisory.
24359
24486
  host_memory: this.hostMemoryIndicator(),
24487
+ // #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
24488
+ ...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
24360
24489
  // #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
24361
24490
  ...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
24362
24491
  };