@songsid/agend 2.3.3-beta.8 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/backend/claude-code.js +17 -0
  2. package/dist/backend/claude-code.js.map +1 -1
  3. package/dist/backend/codex.d.ts +15 -0
  4. package/dist/backend/codex.js +67 -18
  5. package/dist/backend/codex.js.map +1 -1
  6. package/dist/backend/types.d.ts +7 -0
  7. package/dist/backend/types.js.map +1 -1
  8. package/dist/channel/adapters/discord.js +60 -17
  9. package/dist/channel/adapters/discord.js.map +1 -1
  10. package/dist/daemon.d.ts +17 -0
  11. package/dist/daemon.js +85 -9
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/fleet-manager.d.ts +31 -0
  14. package/dist/fleet-manager.js +106 -12
  15. package/dist/fleet-manager.js.map +1 -1
  16. package/dist/instance-lifecycle.d.ts +13 -0
  17. package/dist/instance-lifecycle.js +30 -6
  18. package/dist/instance-lifecycle.js.map +1 -1
  19. package/dist/locale.js +10 -2
  20. package/dist/locale.js.map +1 -1
  21. package/dist/parked-notices.d.ts +87 -0
  22. package/dist/parked-notices.js +194 -0
  23. package/dist/parked-notices.js.map +1 -0
  24. package/dist/public-link-progress.d.ts +5 -2
  25. package/dist/public-link-progress.js +4 -3
  26. package/dist/public-link-progress.js.map +1 -1
  27. package/dist/public-web-link.d.ts +10 -0
  28. package/dist/public-web-link.js +24 -5
  29. package/dist/public-web-link.js.map +1 -1
  30. package/dist/settings-api.d.ts +10 -0
  31. package/dist/settings-api.js +41 -0
  32. package/dist/settings-api.js.map +1 -1
  33. package/dist/tunnel/cloudflared.d.ts +5 -0
  34. package/dist/tunnel/cloudflared.js +32 -1
  35. package/dist/tunnel/cloudflared.js.map +1 -1
  36. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  37. package/dist/tunnel/named-diagnostics.js +4 -1
  38. package/dist/tunnel/named-diagnostics.js.map +1 -1
  39. package/dist/tunnel/named-link-test.d.ts +90 -0
  40. package/dist/tunnel/named-link-test.js +270 -0
  41. package/dist/tunnel/named-link-test.js.map +1 -0
  42. package/dist/tunnel/purpose-lane.d.ts +3 -1
  43. package/dist/tunnel/purpose-lane.js +2 -0
  44. package/dist/tunnel/purpose-lane.js.map +1 -1
  45. package/dist/ui/panel-settings.js +5 -3
  46. package/dist/ui/settings-named-test.js +144 -0
  47. package/dist/ui/settings-strings.js +6 -6
  48. package/dist/ui/shared/app.css +7 -0
  49. package/dist/ui/shared/web-authorize.js +2 -2
  50. package/package.json +1 -1
@@ -1,6 +1,7 @@
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";
6
7
  import type { LoginCodeOwner } from "./web-login.js";
@@ -1642,6 +1643,21 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1642
1643
  private publicWebLink;
1643
1644
  /** #1723: the public link's preview tunnel — a manager and lease of its own, never the fleet's one tunnel lane. */
1644
1645
  private getPreviewTunnel;
1646
+ private namedLinkTestRunning;
1647
+ /**
1648
+ * #2906 (the user, 2026-11-01): Settings → Access → Test connection, on the fixed-address values as filled in (the
1649
+ * typed connector token, else the saved one). One at a time; the result is closed step codes only — the token is
1650
+ * never logged or returned. Admin only, never over the public link (settings-api.ts).
1651
+ */
1652
+ testNamedLink(input: {
1653
+ hostname: unknown;
1654
+ port: unknown;
1655
+ protocol: unknown;
1656
+ token?: unknown;
1657
+ current?: () => boolean;
1658
+ }): Promise<NamedTestResult | {
1659
+ busy: true;
1660
+ }>;
1645
1661
  private getTunnelLane;
1646
1662
  private publicOwnerCurrent;
1647
1663
  /** Exact owning General. No primary-adapter or name fallback participates in authorization. */
@@ -2849,6 +2865,21 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2849
2865
  * would have to make this async and change every caller.
2850
2866
  */
2851
2867
  notifyInstanceTopic(instanceName: string, text: string, extraOpts?: import("./channel/types.js").SendOpts): boolean;
2868
+ /**
2869
+ * notifyInstanceTopic's routing, handing back the send itself — and which world sent it — for a caller that must
2870
+ * edit or delete the message later (#2897). Null when there is no adapter or no postable target (already logged).
2871
+ */
2872
+ private sendToInstanceTopic;
2873
+ private parkedNoticesStore;
2874
+ private get parkedNotices();
2875
+ /**
2876
+ * A held CLI dialog's notices: the instance-topic one and the General one, each edited in place while the
2877
+ * dialog stays (one live message per place, never a pile), and taken back by resolveParkedNotice.
2878
+ */
2879
+ showParkedNotice(instanceName: string, topicText: string | null, generalText: string | null): void;
2880
+ /** The held dialog is resolved, or the instance stopped or restarted: delete its notices (no-op when none). */
2881
+ resolveParkedNotice(instanceName: string): void;
2882
+ private postParkedNotice;
2852
2883
  /**
2853
2884
  * Post decision buttons whose callback ids are `<prefix><nonce>:<action>`.
2854
2885
  * The entry is registered before the send and rolled back if the send
@@ -27,6 +27,7 @@ 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";
30
31
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
31
32
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
32
33
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
@@ -80,6 +81,7 @@ import { TmuxManager } from "./tmux-manager.js";
80
81
  import { AccessManager } from "./channel/access-manager.js";
81
82
  import { IpcClient } from "./channel/ipc-bridge.js";
82
83
  import { usesChannelAddress } from "./channel/mcp-tools.js";
84
+ import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
83
85
  import { formatFleetTime } from "./tz-utils.js";
84
86
  import { createAdapter } from "./channel/factory.js";
85
87
  import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
@@ -3079,6 +3081,7 @@ export class FleetManager {
3079
3081
  if (this.reloadPending)
3080
3082
  this.scheduleReconcile();
3081
3083
  void this.sweepOrphanedCancelButtons();
3084
+ void this.parkedNotices.sweep();
3082
3085
  }
3083
3086
  /**
3084
3087
  * We are the post-update fleet: the update is over by definition. Clearing it here (rather than in the update
@@ -6583,6 +6586,32 @@ export class FleetManager {
6583
6586
  return this.previewTunnel ??= new ManagedTunnel({ dataDir: join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR),
6584
6587
  log: () => this.logger.warn("Public preview tunnel cleanup requires attention") });
6585
6588
  }
6589
+ namedLinkTestRunning = false;
6590
+ /**
6591
+ * #2906 (the user, 2026-11-01): Settings → Access → Test connection, on the fixed-address values as filled in (the
6592
+ * typed connector token, else the saved one). One at a time; the result is closed step codes only — the token is
6593
+ * never logged or returned. Admin only, never over the public link (settings-api.ts).
6594
+ */
6595
+ async testNamedLink(input) {
6596
+ if (this.namedLinkTestRunning)
6597
+ return { busy: true };
6598
+ this.namedLinkTestRunning = true;
6599
+ try {
6600
+ const typed = typeof input.token === "string" && input.token.trim() ? input.token : null;
6601
+ const result = await runNamedLinkTest({ hostname: input.hostname, port: input.port, protocol: input.protocol, token: typed ?? process.env[PUBLIC_TUNNEL_TOKEN_ENV] ?? null }, {
6602
+ healthPort: this.fleetConfig?.health_port ?? 19280,
6603
+ ensureBinary: async (signal) => (await ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal })).path,
6604
+ liveMarker: values => this.publicWebLink?.liveNamedMarker(values) ?? null,
6605
+ reserve: namedTestLane(this.getTunnelLane()),
6606
+ current: input.current,
6607
+ });
6608
+ 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");
6609
+ return result;
6610
+ }
6611
+ finally {
6612
+ this.namedLinkTestRunning = false;
6613
+ }
6614
+ }
6586
6615
  getTunnelLane() {
6587
6616
  return this.tunnelPurposeLane ??= new TunnelPurposeLane(new ManagedTunnel({ dataDir: this.dataDir,
6588
6617
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
@@ -6958,7 +6987,8 @@ export class FleetManager {
6958
6987
  : "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
6959
6988
  + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
6960
6989
  const choices = [{ action: "local", label: t("dashboard.local_signin") },
6961
- ...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
6990
+ ...(publicLinkSettings(this.fleetConfig?.web).allowed
6991
+ ? [{ action: "public", label: t(publicLinkNamedHostname(this.fleetConfig?.web) ? "dashboard.public_open_named" : "dashboard.public_open") }] : []),
6962
6992
  ...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
6963
6993
  const posted = await this.postNonceButtonPrompt({
6964
6994
  prefix: "dashboard:", alertType: "login", instanceName: "dashboard", adapter, adapterId: owner.adapterId,
@@ -14278,11 +14308,22 @@ export class FleetManager {
14278
14308
  * would have to make this async and change every caller.
14279
14309
  */
14280
14310
  notifyInstanceTopic(instanceName, text, extraOpts) {
14311
+ const send = this.sendToInstanceTopic(instanceName, text, extraOpts);
14312
+ if (!send)
14313
+ return false;
14314
+ send.sent.catch(e => this.logger.warn({ err: e, instanceName }, send.failure));
14315
+ return true;
14316
+ }
14317
+ /**
14318
+ * notifyInstanceTopic's routing, handing back the send itself — and which world sent it — for a caller that must
14319
+ * edit or delete the message later (#2897). Null when there is no adapter or no postable target (already logged).
14320
+ */
14321
+ sendToInstanceTopic(instanceName, text, extraOpts) {
14281
14322
  const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
14282
14323
  if (!adapter) {
14283
14324
  // Early startup: adapters are created well after the fleet object exists.
14284
14325
  this.logger.warn({ instanceName }, "No adapter yet — instance topic notification not sent");
14285
- return false;
14326
+ return null;
14286
14327
  }
14287
14328
  const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
14288
14329
  const groupId = channelCfg?.group_id;
@@ -14291,17 +14332,13 @@ export class FleetManager {
14291
14332
  const worldId = this.getInstanceAdapterId(instanceName);
14292
14333
  if (threadId != null && groupId) {
14293
14334
  const out = this.renderPlaces(worldId, text, { threadId: String(threadId), ...extraOpts });
14294
- adapter.sendText(String(groupId), out.text, out.opts)
14295
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
14296
- return true;
14335
+ return { adapterId: worldId, sent: adapter.sendText(String(groupId), out.text, out.opts), failure: "Failed to send instance topic notification" };
14297
14336
  }
14298
14337
  // Classic instance: find its channelId from the classic manager
14299
14338
  const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
14300
14339
  if (classicChatId) {
14301
14340
  // #1990: a ClassicBot room's readers need not be in the fleet's group, so its notices name places as plain text.
14302
- adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts)
14303
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
14304
- return true;
14341
+ return { adapterId: worldId, sent: adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts), failure: "Failed to send classic notification" };
14305
14342
  }
14306
14343
  // Fallback: the instance has neither a topic nor a classic channel, so post
14307
14344
  // where fleet-wide notices go. Not the bare group id: on Discord that is a
@@ -14309,12 +14346,67 @@ export class FleetManager {
14309
14346
  const target = this.fleetNoticeTarget(worldId);
14310
14347
  if (target) {
14311
14348
  const out = this.renderPlaces(worldId, text, { ...target.opts, ...extraOpts });
14312
- adapter.sendText(target.chatId, out.text, out.opts)
14313
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
14314
- return true;
14349
+ return { adapterId: worldId, sent: adapter.sendText(target.chatId, out.text, out.opts), failure: "Failed to send notification (no topic)" };
14315
14350
  }
14316
14351
  this.logger.warn({ instanceName }, "No postable target — instance topic notification not sent");
14317
- return false;
14352
+ return null;
14353
+ }
14354
+ // ── Parked-dialog notices (#2897) ──
14355
+ parkedNoticesStore = null;
14356
+ get parkedNotices() {
14357
+ this.parkedNoticesStore ??= new ParkedNotices({
14358
+ file: join(this.dataDir, PARKED_NOTICE_LEDGER_FILE),
14359
+ logger: this.logger,
14360
+ post: (instanceName, place, text) => this.postParkedNotice(instanceName, place, text),
14361
+ adapterFor: adapterId => this.worlds.get(adapterId)?.adapter
14362
+ ?? (adapterId === this.getPrimaryAdapterId() ? this.adapter ?? undefined : undefined),
14363
+ resolvedText: () => t("fleet.dialog_parked_resolved"),
14364
+ // A ClassicBot room names places as plain text (#1990), as its post did.
14365
+ render: (row, text) => row.place === "topic" && this.classicChannels?.getChannelIdByInstance(row.instanceName) === row.chatId
14366
+ ? stripPlaceRefs(text) : this.renderPlaces(row.adapterId, text).text,
14367
+ });
14368
+ return this.parkedNoticesStore;
14369
+ }
14370
+ /**
14371
+ * A held CLI dialog's notices: the instance-topic one and the General one, each edited in place while the
14372
+ * dialog stays (one live message per place, never a pile), and taken back by resolveParkedNotice.
14373
+ */
14374
+ showParkedNotice(instanceName, topicText, generalText) {
14375
+ if (topicText !== null)
14376
+ void this.parkedNotices.show(instanceName, "topic", topicText);
14377
+ if (generalText !== null)
14378
+ void this.parkedNotices.show(instanceName, "general", generalText);
14379
+ }
14380
+ /** The held dialog is resolved, or the instance stopped or restarted: delete its notices (no-op when none). */
14381
+ resolveParkedNotice(instanceName) {
14382
+ void this.parkedNotices.resolve(instanceName);
14383
+ }
14384
+ async postParkedNotice(instanceName, place, text) {
14385
+ let send;
14386
+ if (place === "topic") {
14387
+ send = this.sendToInstanceTopic(instanceName, text);
14388
+ }
14389
+ else {
14390
+ // Where notifyFleetError posts, without its text throttle: the ledger is what keeps this to one message.
14391
+ const general = Object.entries(this.fleetConfig?.instances ?? {})
14392
+ .find(([, config]) => config.general_topic === true)?.[0];
14393
+ const target = general ? null : this.fleetNoticeTarget();
14394
+ if (general)
14395
+ send = this.sendToInstanceTopic(general, text);
14396
+ else if (this.adapter && target) {
14397
+ const out = this.renderPlaces(this.getPrimaryAdapterId(), text, target.opts);
14398
+ send = { adapterId: this.getPrimaryAdapterId(), sent: this.adapter.sendText(target.chatId, out.text, out.opts) };
14399
+ }
14400
+ else
14401
+ send = null;
14402
+ }
14403
+ if (!send)
14404
+ return null;
14405
+ const sent = await send.sent;
14406
+ const adapterId = send.adapterId ?? this.getPrimaryAdapterId();
14407
+ if (!adapterId)
14408
+ return null;
14409
+ return { adapterId, chatId: sent.chatId, ...(sent.threadId !== undefined ? { threadId: sent.threadId } : {}), messageId: sent.messageId };
14318
14410
  }
14319
14411
  // ── Nonce-armed button prompts (hang / assist / exit / clear) ──
14320
14412
  //
@@ -21215,6 +21307,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21215
21307
  for (const timer of this.cancelButtonIdleRetireTimers.values())
21216
21308
  clearTimeout(timer);
21217
21309
  this.cancelButtonIdleRetireTimers.clear();
21310
+ // #2897: notices still being retired stay in the ledger; the next fleet's sweep finishes them.
21311
+ this.parkedNoticesStore?.dispose();
21218
21312
  if (this.topicCleanupTimer) {
21219
21313
  clearInterval(this.topicCleanupTimer);
21220
21314
  this.topicCleanupTimer = null;