@songsid/agend 2.3.3-beta.9 → 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 (154) 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 +41 -1
  24. package/dist/fleet-manager.js +179 -22
  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 +56 -2
  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-link-progress.d.ts +5 -2
  57. package/dist/public-link-progress.js +4 -3
  58. package/dist/public-link-progress.js.map +1 -1
  59. package/dist/public-preview.d.ts +4 -0
  60. package/dist/public-preview.js +5 -4
  61. package/dist/public-preview.js.map +1 -1
  62. package/dist/public-web-gateway.js +5 -0
  63. package/dist/public-web-gateway.js.map +1 -1
  64. package/dist/public-web-link.d.ts +10 -0
  65. package/dist/public-web-link.js +24 -5
  66. package/dist/public-web-link.js.map +1 -1
  67. package/dist/quickstart-api.d.ts +5 -0
  68. package/dist/quickstart-api.js +1 -1
  69. package/dist/quickstart-api.js.map +1 -1
  70. package/dist/quickstart.d.ts +40 -0
  71. package/dist/quickstart.js +174 -28
  72. package/dist/quickstart.js.map +1 -1
  73. package/dist/remote-card-answer.js +2 -0
  74. package/dist/remote-card-answer.js.map +1 -1
  75. package/dist/remote-card-prompt.js +4 -2
  76. package/dist/remote-card-prompt.js.map +1 -1
  77. package/dist/selection-card.d.ts +21 -0
  78. package/dist/selection-card.js +129 -0
  79. package/dist/selection-card.js.map +1 -0
  80. package/dist/settings-api.d.ts +10 -0
  81. package/dist/settings-api.js +45 -0
  82. package/dist/settings-api.js.map +1 -1
  83. package/dist/settings-change.js +5 -0
  84. package/dist/settings-change.js.map +1 -1
  85. package/dist/settings-effect.js +2 -0
  86. package/dist/settings-effect.js.map +1 -1
  87. package/dist/setup-host.js +2 -2
  88. package/dist/setup-host.js.map +1 -1
  89. package/dist/setup-wizard.js +2 -2
  90. package/dist/setup-wizard.js.map +1 -1
  91. package/dist/tunnel/cloudflared.d.ts +5 -0
  92. package/dist/tunnel/cloudflared.js +32 -1
  93. package/dist/tunnel/cloudflared.js.map +1 -1
  94. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  95. package/dist/tunnel/named-diagnostics.js +4 -1
  96. package/dist/tunnel/named-diagnostics.js.map +1 -1
  97. package/dist/tunnel/named-link-test.d.ts +90 -0
  98. package/dist/tunnel/named-link-test.js +270 -0
  99. package/dist/tunnel/named-link-test.js.map +1 -0
  100. package/dist/tunnel/purpose-lane.d.ts +3 -1
  101. package/dist/tunnel/purpose-lane.js +2 -0
  102. package/dist/tunnel/purpose-lane.js.map +1 -1
  103. package/dist/types.d.ts +10 -0
  104. package/dist/ui/chat-store.js +8 -1
  105. package/dist/ui/chat-strings.js +36 -0
  106. package/dist/ui/page-share-dialog.js +191 -0
  107. package/dist/ui/panel-chat.js +14 -0
  108. package/dist/ui/panel-notices.js +98 -0
  109. package/dist/ui/panel-settings.js +14 -5
  110. package/dist/ui/preview-pages.js +41 -0
  111. package/dist/ui/preview-panel-actions.js +161 -0
  112. package/dist/ui/preview-panel.js +209 -28
  113. package/dist/ui/preview-versions.js +155 -0
  114. package/dist/ui/preview.js +88 -3
  115. package/dist/ui/settings-named-test.js +144 -0
  116. package/dist/ui/settings-registry.js +1 -0
  117. package/dist/ui/settings-strings.js +10 -6
  118. package/dist/ui/shared/app-i18n.js +2 -2
  119. package/dist/ui/shared/app-route.js +4 -0
  120. package/dist/ui/shared/app-shell.js +5 -3
  121. package/dist/ui/shared/app-store.js +71 -1
  122. package/dist/ui/shared/app-stream.js +1 -1
  123. package/dist/ui/shared/app.css +50 -1
  124. package/dist/ui/shared/app.js +22 -1
  125. package/dist/ui/shared/signin.js +12 -1
  126. package/dist/ui/shared/ui-icons.js +3 -0
  127. package/dist/ui/shared/web-authorize.js +5 -3
  128. package/dist/web-api.d.ts +26 -0
  129. package/dist/web-api.js +180 -1
  130. package/dist/web-api.js.map +1 -1
  131. package/dist/web-auth.js +7 -1
  132. package/dist/web-auth.js.map +1 -1
  133. package/dist/web-login.d.ts +2 -0
  134. package/dist/web-login.js +3 -1
  135. package/dist/web-login.js.map +1 -1
  136. package/dist/web-notices.d.ts +51 -0
  137. package/dist/web-notices.js +118 -0
  138. package/dist/web-notices.js.map +1 -0
  139. package/dist/web-only-fleet.d.ts +22 -0
  140. package/dist/web-only-fleet.js +13 -0
  141. package/dist/web-only-fleet.js.map +1 -0
  142. package/dist/web-preview.d.ts +32 -3
  143. package/dist/web-preview.js +52 -19
  144. package/dist/web-preview.js.map +1 -1
  145. package/dist/web-session.d.ts +7 -0
  146. package/dist/web-session.js +2 -0
  147. package/dist/web-session.js.map +1 -1
  148. package/dist/web-settings-patch.d.ts +3 -1
  149. package/dist/web-settings-patch.js +4 -1
  150. package/dist/web-settings-patch.js.map +1 -1
  151. package/dist/web-shell-routes.d.ts +2 -0
  152. package/dist/web-shell-routes.js +3 -0
  153. package/dist/web-shell-routes.js.map +1 -1
  154. 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"}
@@ -1,8 +1,10 @@
1
1
  import { type HumanOrigin } from "./human-delivery-receipts.js";
2
2
  import { SettingsExecution } from "./settings-transaction.js";
3
3
  import { AppIconStore } from "./web-app-icon.js";
4
+ import { type NamedTestResult } from "./tunnel/named-link-test.js";
4
5
  import { OfficeShareService } from "./office-share-service.js";
5
6
  import { type OfficeShare } from "./office-share.js";
7
+ import type { PageShare } from "./page-share.js";
6
8
  import type { LoginCodeOwner } from "./web-login.js";
7
9
  import { type ProfileTicket } from "./runtime-cpu-profile.js";
8
10
  import type { CpuProfile } from "./cpu-profile.js";
@@ -99,6 +101,7 @@ import { type MemoryHog } from "./memory-hogs.js";
99
101
  import { type CacheReport } from "./cache-service.js";
100
102
  import { type WebCommandResult } from "./web-commands.js";
101
103
  import { type SessionRecord } from "./web-session.js";
104
+ import { WebNotices } from "./web-notices.js";
102
105
  import { VERIFIED_SESSION_HOURS, type HumanAuthority, type WebAuthorityRefusal, type WebAuthorityResult, type WebPeer } from "./authority/web-authority.js";
103
106
  import { type AuthorityState } from "./authority/authority-state.js";
104
107
  import { type SessionAuthorization } from "./authority/session-authorizations.js";
@@ -686,6 +689,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
686
689
  * `share` null with `count`: "Revoke all".
687
690
  */
688
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;
689
696
  /** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
690
697
  private officeShareOwnerCurrent;
691
698
  private officeActsInstance?;
@@ -1642,6 +1649,21 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1642
1649
  private publicWebLink;
1643
1650
  /** #1723: the public link's preview tunnel — a manager and lease of its own, never the fleet's one tunnel lane. */
1644
1651
  private getPreviewTunnel;
1652
+ private namedLinkTestRunning;
1653
+ /**
1654
+ * #2906 (the user, 2026-11-01): Settings → Access → Test connection, on the fixed-address values as filled in (the
1655
+ * typed connector token, else the saved one). One at a time; the result is closed step codes only — the token is
1656
+ * never logged or returned. Admin only, never over the public link (settings-api.ts).
1657
+ */
1658
+ testNamedLink(input: {
1659
+ hostname: unknown;
1660
+ port: unknown;
1661
+ protocol: unknown;
1662
+ token?: unknown;
1663
+ current?: () => boolean;
1664
+ }): Promise<NamedTestResult | {
1665
+ busy: true;
1666
+ }>;
1645
1667
  private getTunnelLane;
1646
1668
  private publicOwnerCurrent;
1647
1669
  /** Exact owning General. No primary-adapter or name fallback participates in authorization. */
@@ -2273,7 +2295,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2273
2295
  private handleOutboundFromInstance;
2274
2296
  /** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
2275
2297
  /** No chat platform is configured at all: the web dashboard is the fleet's only surface. */
2276
- private isWebOnlyFleet;
2298
+ /** #2910: no Discord or Telegram connection at all (src/web-only-fleet.ts). */
2299
+ isWebOnlyFleet(): boolean;
2277
2300
  /** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
2278
2301
  private webOnlyPromptPlace;
2279
2302
  /** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
@@ -2836,6 +2859,16 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2836
2859
  notifyFleetError(text: string, opts?: {
2837
2860
  throttle?: boolean;
2838
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;
2839
2872
  private static readonly FLEET_ERROR_THROTTLE_MS;
2840
2873
  private fleetErrorNotices;
2841
2874
  /**
@@ -3128,6 +3161,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
3128
3161
  ok: false;
3129
3162
  refusal: WebAuthorityRefusal;
3130
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;
3131
3171
  private authorityStateStore;
3132
3172
  /**
3133
3173
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -27,6 +27,8 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
27
27
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
28
28
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
29
29
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
30
+ import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
31
+ import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
30
32
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
31
33
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
32
34
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
@@ -263,7 +265,8 @@ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCooki
263
265
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
264
266
  import { isWebPageNavigation } from "./web-shell-routes.js";
265
267
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
266
- 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";
267
270
  import { AuthorityStateStore } from "./authority/authority-state.js";
268
271
  import { SessionAuthorizations } from "./authority/session-authorizations.js";
269
272
  import { SessionAuthorizationRequests } from "./web-session-authorization.js";
@@ -1200,6 +1203,11 @@ export class FleetManager {
1200
1203
  officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
1201
1204
  ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
1202
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),
1203
1211
  log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
1204
1212
  }));
1205
1213
  }
@@ -1214,6 +1222,17 @@ export class FleetManager {
1214
1222
  : share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
1215
1223
  this.notifyFleetError(text, { throttle: false });
1216
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
+ }
1217
1236
  /** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
1218
1237
  officeShareOwnerCurrent(owner) {
1219
1238
  const m = /^person:([^:]+):(.+)$/.exec(owner);
@@ -1611,6 +1630,17 @@ export class FleetManager {
1611
1630
  const actor = verified();
1612
1631
  if (!actor)
1613
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
+ }
1614
1644
  const chat = this.settingsChatAuthority(store, id);
1615
1645
  if (!chat)
1616
1646
  return null;
@@ -5689,8 +5719,7 @@ export class FleetManager {
5689
5719
  }
5690
5720
  /** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
5691
5721
  async startPersistedPausedInstance(name, transition) {
5692
- const topicMode = this.fleetConfig?.channel?.mode === "topic"
5693
- || !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
5722
+ const topicMode = daemonTopicMode(this.fleetConfig);
5694
5723
  const fleetConfig = this.fleetConfig?.instances[name];
5695
5724
  if (fleetConfig) {
5696
5725
  await this.startInstance(name, fleetConfig, topicMode, "fleet-topic", false, transition);
@@ -5901,8 +5930,7 @@ export class FleetManager {
5901
5930
  this.logger.info({ name, attempt: attempt + 1 }, "Startup retry deferred — tmux storm window is blocking spawns");
5902
5931
  return;
5903
5932
  }
5904
- const topicMode = this.fleetConfig?.channel?.mode === "topic"
5905
- || !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
5933
+ const topicMode = daemonTopicMode(this.fleetConfig);
5906
5934
  this.startupRetriesRunning.add(name);
5907
5935
  try {
5908
5936
  await this.spawnGate.run({
@@ -6425,7 +6453,7 @@ export class FleetManager {
6425
6453
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
6426
6454
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
6427
6455
  });
6428
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
6456
+ const topicMode = daemonTopicMode(this.fleetConfig);
6429
6457
  await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
6430
6458
  : this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
6431
6459
  this.stormWindow.markRecovered(name);
@@ -6585,6 +6613,32 @@ export class FleetManager {
6585
6613
  return this.previewTunnel ??= new ManagedTunnel({ dataDir: join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR),
6586
6614
  log: () => this.logger.warn("Public preview tunnel cleanup requires attention") });
6587
6615
  }
6616
+ namedLinkTestRunning = false;
6617
+ /**
6618
+ * #2906 (the user, 2026-11-01): Settings → Access → Test connection, on the fixed-address values as filled in (the
6619
+ * typed connector token, else the saved one). One at a time; the result is closed step codes only — the token is
6620
+ * never logged or returned. Admin only, never over the public link (settings-api.ts).
6621
+ */
6622
+ async testNamedLink(input) {
6623
+ if (this.namedLinkTestRunning)
6624
+ return { busy: true };
6625
+ this.namedLinkTestRunning = true;
6626
+ try {
6627
+ const typed = typeof input.token === "string" && input.token.trim() ? input.token : null;
6628
+ const result = await runNamedLinkTest({ hostname: input.hostname, port: input.port, protocol: input.protocol, token: typed ?? process.env[PUBLIC_TUNNEL_TOKEN_ENV] ?? null }, {
6629
+ healthPort: this.fleetConfig?.health_port ?? 19280,
6630
+ ensureBinary: async (signal) => (await ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal })).path,
6631
+ liveMarker: values => this.publicWebLink?.liveNamedMarker(values) ?? null,
6632
+ reserve: namedTestLane(this.getTunnelLane()),
6633
+ current: input.current,
6634
+ });
6635
+ this.logger[result.cleanup ? "warn" : "info"]({ steps: result.steps.map(s => `${s.step}:${s.code}`), typedToken: !!typed, ...(result.cleanup ? { cleanup: result.cleanup } : {}) }, result.cleanup ? "Named link test: the test connector's stop could not be proven; the tunnel lane stays held" : "Named link test");
6636
+ return result;
6637
+ }
6638
+ finally {
6639
+ this.namedLinkTestRunning = false;
6640
+ }
6641
+ }
6588
6642
  getTunnelLane() {
6589
6643
  return this.tunnelPurposeLane ??= new TunnelPurposeLane(new ManagedTunnel({ dataDir: this.dataDir,
6590
6644
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
@@ -6702,6 +6756,7 @@ export class FleetManager {
6702
6756
  if (!publicPreviewWanted(this.fleetConfig?.web))
6703
6757
  return null;
6704
6758
  return new PublicPreview({ exposureId: id, expiresAt, dashboardOrigin, isCurrent, manager: this.getPreviewTunnel(),
6759
+ cdn: () => this.fleetConfig?.web?.preview_cdn === true,
6705
6760
  provider: () => new CloudflaredProvider({ binaryName: binary, protocol }),
6706
6761
  log: (event, detail) => this.logger.info({ event, exposureId: id, ...(detail ? { detail } : {}) }, "Public web link") });
6707
6762
  },
@@ -6960,7 +7015,8 @@ export class FleetManager {
6960
7015
  : "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
6961
7016
  + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
6962
7017
  const choices = [{ action: "local", label: t("dashboard.local_signin") },
6963
- ...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
7018
+ ...(publicLinkSettings(this.fleetConfig?.web).allowed
7019
+ ? [{ action: "public", label: t(publicLinkNamedHostname(this.fleetConfig?.web) ? "dashboard.public_open_named" : "dashboard.public_open") }] : []),
6964
7020
  ...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
6965
7021
  const posted = await this.postNonceButtonPrompt({
6966
7022
  prefix: "dashboard:", alertType: "login", instanceName: "dashboard", adapter, adapterId: owner.adapterId,
@@ -7662,7 +7718,8 @@ export class FleetManager {
7662
7718
  return;
7663
7719
  // The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
7664
7720
  // sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
7665
- 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)));
7666
7723
  }
7667
7724
  getDashboardAccess() {
7668
7725
  return { ready: this.healthServerListening, token: this.webToken };
@@ -8082,7 +8139,8 @@ export class FleetManager {
8082
8139
  // Must run before the first General/CLI spawn: pressure can hold startup.
8083
8140
  // Logs remain available even before the adapters and health listener exist.
8084
8141
  this.memoryPressure.start();
8085
- 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);
8086
8144
  // Set tmux socket isolation for custom AGEND_HOME
8087
8145
  const { getTmuxSocketName: getSocket } = await import("./paths.js");
8088
8146
  TmuxManager.setSocketName(getSocket());
@@ -8299,7 +8357,8 @@ export class FleetManager {
8299
8357
  }
8300
8358
  if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
8301
8359
  this.saveFleetConfig();
8302
- 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) {
8303
8362
  const schedulerConfig = {
8304
8363
  ...DEFAULT_SCHEDULER_CONFIG,
8305
8364
  ...this.fleetConfig?.defaults.scheduler,
@@ -10808,7 +10867,10 @@ export class FleetManager {
10808
10867
  // else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
10809
10868
  const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
10810
10869
  if (this.worlds.size === 0 && !webOnlyReply) {
10811
- 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");
10812
10874
  return;
10813
10875
  }
10814
10876
  // Resolve threadId: use sender's topic_id if sender is a known fleet instance,
@@ -11008,9 +11070,9 @@ export class FleetManager {
11008
11070
  }
11009
11071
  /** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
11010
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). */
11011
11074
  isWebOnlyFleet() {
11012
- const config = this.fleetConfig;
11013
- return config != null && !config.channel && !(config.channels?.length);
11075
+ return isWebOnlyFleetConfig(this.fleetConfig);
11014
11076
  }
11015
11077
  /** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
11016
11078
  webOnlyPromptPlace() {
@@ -11385,6 +11447,9 @@ export class FleetManager {
11385
11447
  // through the target's adapter, so a Telegram-created schedule for a
11386
11448
  // Discord target announced its failure into the wrong platform.
11387
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;
11388
11453
  if (!adapter) {
11389
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");
11390
11455
  return;
@@ -14193,6 +14258,8 @@ export class FleetManager {
14193
14258
  * back to posting at the bare group id, which is the defect this replaced.
14194
14259
  */
14195
14260
  postDailySummary(text) {
14261
+ if (this.recordWebNotice("summary", text))
14262
+ return; // #2910 D2: a web-only fleet's Notices
14196
14263
  const target = this.fleetNoticeTarget();
14197
14264
  if (!this.adapter || !target) {
14198
14265
  this.logger.warn("Daily summary has no postable target — set a General topic or channel.options.general_channel_id");
@@ -14245,6 +14312,10 @@ export class FleetManager {
14245
14312
  dispatched = true;
14246
14313
  }
14247
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;
14248
14319
  if (!dispatched) {
14249
14320
  // Do NOT record the throttle key. It used to be claimed before delivery
14250
14321
  // was attempted, so a message nobody could receive — every fleet error
@@ -14267,6 +14338,31 @@ export class FleetManager {
14267
14338
  }
14268
14339
  return true;
14269
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
+ }
14270
14366
  static FLEET_ERROR_THROTTLE_MS = 10 * 60_000;
14271
14367
  fleetErrorNotices = new Map();
14272
14368
  /**
@@ -14280,6 +14376,9 @@ export class FleetManager {
14280
14376
  * would have to make this async and change every caller.
14281
14377
  */
14282
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);
14283
14382
  const send = this.sendToInstanceTopic(instanceName, text, extraOpts);
14284
14383
  if (!send)
14285
14384
  return false;
@@ -15556,6 +15655,7 @@ export class FleetManager {
15556
15655
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
15557
15656
  authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
15558
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(),
15559
15659
  });
15560
15660
  }
15561
15661
  sessionAuthorizations = null;
@@ -15588,6 +15688,33 @@ export class FleetManager {
15588
15688
  store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
15589
15689
  return { ok: true, ...store.authorize(session.handle, session.owner, now, state.epoch, now + policy.maxSessionMs, peer) };
15590
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
+ }
15591
15718
  authorityStateStore = null;
15592
15719
  /**
15593
15720
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -15643,7 +15770,10 @@ export class FleetManager {
15643
15770
  const sessions = new Map((this.webSessions?.list(undefined, () => true) ?? []).map(r => [r.handle, r]));
15644
15771
  const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
15645
15772
  .filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
15646
- .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,
15647
15777
  until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry),
15648
15778
  // Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
15649
15779
  ...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
@@ -15753,6 +15883,8 @@ export class FleetManager {
15753
15883
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
15754
15884
  authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
15755
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(),
15756
15888
  });
15757
15889
  return r.refusal;
15758
15890
  }
@@ -16060,8 +16192,10 @@ export class FleetManager {
16060
16192
  ? [{ action: "resume", label: t("inst.session_oom_resume") }, { action: "fresh", label: t("inst.session_oom_fresh") }]
16061
16193
  : [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }];
16062
16194
  const extra = sessionOomRetry ? { sessionOomRetry, ...(current ? { confirmationCurrent: current } : {}) } : undefined;
16063
- this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
16064
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"));
16065
16199
  if (web) {
16066
16200
  await this.postNonceButtonPrompt({
16067
16201
  prefix, alertType: "exit_restart", instanceName, ...web,
@@ -16140,11 +16274,19 @@ export class FleetManager {
16140
16274
  * contain usernames or secret-adjacent text and stays in daemon.log.
16141
16275
  */
16142
16276
  async notifyInteractivePrompt(instanceName, kind) {
16143
- 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"));
16144
16280
  // Multi-world fleets can have one General per bot/platform. Route the assist
16145
16281
  // request to the General bound to the same adapter as the blocked instance.
16146
16282
  const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
16147
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
+ }
16148
16290
  this.logger.warn({ instanceName }, "Interactive prompt has no General topic notification target");
16149
16291
  return;
16150
16292
  }
@@ -16303,6 +16445,10 @@ export class FleetManager {
16303
16445
  * was gone; this is the fallback).
16304
16446
  */
16305
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;
16306
16452
  this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
16307
16453
  const place = this.remoteCardPlace(instanceName);
16308
16454
  if (!place) {
@@ -21596,7 +21742,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21596
21742
  this.reconcilePreviewListener();
21597
21743
  this.publicWebLink?.refresh();
21598
21744
  const newInstances = this.fleetConfig.instances;
21599
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
21745
+ const topicMode = daemonTopicMode(this.fleetConfig);
21600
21746
  // Only what a fresh process can adopt, and only relative to the signature
21601
21747
  // this process came up on: a Settings edit mutates this.fleetConfig in place
21602
21748
  // before the reload, so comparing the pre-load copy sees nothing at all.
@@ -23175,7 +23321,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23175
23321
  }
23176
23322
  const fleet = this.loadConfig(this.configPath);
23177
23323
  this.fleetConfig = fleet;
23178
- const topicMode = fleet.channel?.mode === "topic" || !!fleet.channels?.some(ch => ch.mode === "topic");
23324
+ const topicMode = daemonTopicMode(fleet);
23179
23325
  const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
23180
23326
  // Phase 1: generals first
23181
23327
  const restartEntries = Object.entries(fleet.instances);
@@ -23327,7 +23473,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23327
23473
  // Only notify when target is genuinely newer (semver), so a beta user on
23328
23474
  // 2.0.8-beta.16 is never told that stable 2.0.7 is "available".
23329
23475
  if (target && this.semverGt(target, currentVersion)) {
23330
- 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();
23331
23478
  if (generalId) {
23332
23479
  // No release URL — Discord's SuppressEmbeds proved unreliable and the
23333
23480
  // link preview looked bad. Version + /update instruction is enough.
@@ -23338,6 +23485,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23338
23485
  if (key)
23339
23486
  this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23340
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
+ }
23341
23494
  }
23342
23495
  }
23343
23496
  catch { /* silent — network issues */ }
@@ -23651,7 +23804,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23651
23804
  if (decision.reason === "no-credential" && req.method === "GET"
23652
23805
  && String(req.headers.accept ?? "").includes("text/html")
23653
23806
  && isWebPageNavigation(requestPath)) {
23654
- serveSigninPage(res, 401);
23807
+ serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
23655
23808
  return;
23656
23809
  }
23657
23810
  res.writeHead(decision.status);
@@ -23874,7 +24027,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23874
24027
  (async () => {
23875
24028
  let held = false;
23876
24029
  try {
23877
- const topicMode = this.fleetConfig?.channel?.mode === "topic";
24030
+ const topicMode = daemonTopicMode(this.fleetConfig);
23878
24031
  await withSpawnHoldReply(async (message) => {
23879
24032
  held = true;
23880
24033
  res.writeHead(202, { "Content-Type": "application/json" });
@@ -24109,7 +24262,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24109
24262
  }
24110
24263
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
24111
24264
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
24112
- 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 });
24113
24268
  this.previewListener = listener;
24114
24269
  listener.server.on("error", (err) => {
24115
24270
  if (this.previewListener !== listener)
@@ -24329,6 +24484,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24329
24484
  // #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
24330
24485
  // shown here rather than in chat). null when normal, unknown or advisory.
24331
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 } : {}),
24332
24489
  // #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
24333
24490
  ...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
24334
24491
  };