@songsid/agend 2.1.6-beta.4 → 2.1.6-beta.6

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 (59) hide show
  1. package/dist/backend/claude-code.js +24 -0
  2. package/dist/backend/claude-code.js.map +1 -1
  3. package/dist/backend/factory.js +4 -1
  4. package/dist/backend/factory.js.map +1 -1
  5. package/dist/backend/muse.d.ts +90 -0
  6. package/dist/backend/muse.js +455 -0
  7. package/dist/backend/muse.js.map +1 -0
  8. package/dist/backend/types.d.ts +2 -0
  9. package/dist/backend/types.js.map +1 -1
  10. package/dist/channel/access-manager.d.ts +20 -0
  11. package/dist/channel/access-manager.js +25 -0
  12. package/dist/channel/access-manager.js.map +1 -1
  13. package/dist/channel/adapters/discord.d.ts +3 -0
  14. package/dist/channel/adapters/discord.js +13 -10
  15. package/dist/channel/adapters/discord.js.map +1 -1
  16. package/dist/channel/markdown-chunk.d.ts +8 -3
  17. package/dist/channel/markdown-chunk.js +12 -3
  18. package/dist/channel/markdown-chunk.js.map +1 -1
  19. package/dist/channel/tool-router.js +12 -2
  20. package/dist/channel/tool-router.js.map +1 -1
  21. package/dist/config-validator.js +14 -3
  22. package/dist/config-validator.js.map +1 -1
  23. package/dist/daemon.js +7 -2
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/fleet-manager.d.ts +32 -0
  26. package/dist/fleet-manager.js +137 -12
  27. package/dist/fleet-manager.js.map +1 -1
  28. package/dist/instance-lifecycle.js +8 -4
  29. package/dist/instance-lifecycle.js.map +1 -1
  30. package/dist/locale.js +14 -0
  31. package/dist/locale.js.map +1 -1
  32. package/dist/logger.js +42 -25
  33. package/dist/logger.js.map +1 -1
  34. package/dist/outbound-schemas.d.ts +1 -0
  35. package/dist/outbound-schemas.js +1 -1
  36. package/dist/outbound-schemas.js.map +1 -1
  37. package/dist/quickstart-api.d.ts +3 -0
  38. package/dist/quickstart-api.js +1 -0
  39. package/dist/quickstart-api.js.map +1 -1
  40. package/dist/scheduler/db.js +17 -5
  41. package/dist/scheduler/db.js.map +1 -1
  42. package/dist/scheduler/db.test.js +34 -1
  43. package/dist/scheduler/db.test.js.map +1 -1
  44. package/dist/scheduler/types.d.ts +4 -0
  45. package/dist/scheduler/types.js.map +1 -1
  46. package/dist/setup-wizard.js +4 -0
  47. package/dist/setup-wizard.js.map +1 -1
  48. package/dist/steer-capability.js +4 -1
  49. package/dist/steer-capability.js.map +1 -1
  50. package/dist/tmux-manager.js +29 -4
  51. package/dist/tmux-manager.js.map +1 -1
  52. package/dist/topic-commands.js +1 -0
  53. package/dist/topic-commands.js.map +1 -1
  54. package/dist/web-api.js +2 -1
  55. package/dist/web-api.js.map +1 -1
  56. package/dist/web-terminal.d.ts +30 -1
  57. package/dist/web-terminal.js +86 -3
  58. package/dist/web-terminal.js.map +1 -1
  59. package/package.json +2 -1
@@ -573,6 +573,15 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
573
573
  * from restarting the same process twice; backendNameOf defines ownership.
574
574
  */
575
575
  private configuredBackendInstanceNames;
576
+ /**
577
+ * Probe the same executable set exposed by the web backend catalog.
578
+ *
579
+ * This deliberately has no cache: an `/install-cli` completion can add a
580
+ * binary to PATH while the fleet process remains alive, and the next bare
581
+ * `/login` must see it without requiring a restart or an explicit cache
582
+ * invalidation call.
583
+ */
584
+ private probeInstalledBackends;
576
585
  /**
577
586
  * One fleet-level notice per burst, not one per instance: a post-update herd
578
587
  * fails many instances within the same second. Two notices per incident at
@@ -757,6 +766,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
757
766
  * Pure: callers rely on being able to ask before claiming the dedup key.
758
767
  */
759
768
  private botMessageDropReason;
769
+ /**
770
+ * Say so when an adapter's access mode is coming from its state file rather
771
+ * than from fleet.yaml. The state file wins by design — a pairing done at
772
+ * runtime has to survive a restart — but that also means an edit to
773
+ * `access.mode` silently does nothing, which is indistinguishable from the
774
+ * fleet not having reloaded. Naming the file is what makes it fixable.
775
+ */
776
+ private warnIfAccessModeOverridden;
760
777
  private handleInboundMessage;
761
778
  /** Handle outbound tool calls from a daemon instance */
762
779
  /** Warn (but don't block) when rate limits are high. 30-min debounce per instance. */
@@ -1084,6 +1101,21 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1084
1101
  * (or an assist for one they stopped) must not stay clickable for the rest
1085
1102
  * of its 15 minutes.
1086
1103
  */
1104
+ /**
1105
+ * Collapse every still-armed button prompt, for a fleet that is going away.
1106
+ *
1107
+ * The map is in memory; the buttons are in the chat for up to 24 hours. A
1108
+ * restart therefore leaves them looking live — pressing one takes the stale
1109
+ * branch, which acknowledges the click and drops the dismissal without
1110
+ * saying so. Retiring them here means the user meets a spent prompt instead
1111
+ * of a live-looking dead one.
1112
+ *
1113
+ * Best effort and bounded. Each collapse is a platform call, a day of tips
1114
+ * can be many of them, and shutdown still has instances to stop. Whatever
1115
+ * has not finished by the deadline is abandoned — that is exactly today's
1116
+ * behaviour, so the budget can only leave things no worse than before.
1117
+ */
1118
+ private retirePendingNoncePrompts;
1087
1119
  clearNoncePromptsForInstance(instanceName: string): void;
1088
1120
  /** A clean exit is intentional from the CLI's perspective, but often not from
1089
1121
  * the operator's. Keep the instance notice passive and put the action in the
@@ -217,6 +217,8 @@ const CLEAR_CONFIRM_TIMEOUT_MS = 15_000;
217
217
  /** Default lifetime for long-lived nonce prompts (clear overrides this to 15s). */
218
218
  const NONCE_BUTTON_TIMEOUT_MS = 15 * 60_000;
219
219
  const TIP_BUTTON_TIMEOUT_MS = 24 * 60 * 60_000;
220
+ /** How long shutdown will spend retiring still-armed button prompts. */
221
+ const NONCE_RETIRE_BUDGET_MS = 5_000;
220
222
  const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // hard validity bound for the cached CLI env
221
223
  /**
222
224
  * How old the cached CLI env may be before `/model` re-probes it live.
@@ -2019,6 +2021,22 @@ export class FleetManager {
2019
2021
  names.add(channel.instanceName);
2020
2022
  return [...names];
2021
2023
  }
2024
+ /**
2025
+ * Probe the same executable set exposed by the web backend catalog.
2026
+ *
2027
+ * This deliberately has no cache: an `/install-cli` completion can add a
2028
+ * binary to PATH while the fleet process remains alive, and the next bare
2029
+ * `/login` must see it without requiring a restart or an explicit cache
2030
+ * invalidation call.
2031
+ */
2032
+ probeInstalledBackends() {
2033
+ const installed = new Set();
2034
+ for (const [backend, info] of Object.entries(BACKEND_INSTALLATION_INFO)) {
2035
+ if (checkBinaryInstalled(info.binary))
2036
+ installed.add(backend);
2037
+ }
2038
+ return installed;
2039
+ }
2022
2040
  /**
2023
2041
  * One fleet-level notice per burst, not one per instance: a post-update herd
2024
2042
  * fails many instances within the same second. Two notices per incident at
@@ -3167,7 +3185,9 @@ export class FleetManager {
3167
3185
  }
3168
3186
  const accessDir = join(this.dataDir, "access");
3169
3187
  mkdirSync(accessDir, { recursive: true });
3170
- const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, join(accessDir, "access.json"));
3188
+ const accessStatePath = join(accessDir, "access.json");
3189
+ const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
3190
+ this.warnIfAccessModeOverridden(accessManager, accessStatePath);
3171
3191
  this.accessManager = accessManager;
3172
3192
  const inboxDir = join(this.dataDir, "inbox");
3173
3193
  mkdirSync(inboxDir, { recursive: true });
@@ -3559,7 +3579,9 @@ export class FleetManager {
3559
3579
  }
3560
3580
  const accessDir = join(this.dataDir, "access");
3561
3581
  mkdirSync(accessDir, { recursive: true });
3562
- const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, join(accessDir, `access-${adapterId}.json`));
3582
+ const accessStatePath = join(accessDir, `access-${adapterId}.json`);
3583
+ const accessManager = new AccessManager(channelConfig.access ?? DEFAULT_OPEN_ACCESS, accessStatePath);
3584
+ this.warnIfAccessModeOverridden(accessManager, accessStatePath);
3563
3585
  const inboxDir = join(this.dataDir, "inbox");
3564
3586
  mkdirSync(inboxDir, { recursive: true });
3565
3587
  const adapter = await createAdapter(channelConfig, {
@@ -4314,13 +4336,36 @@ export class FleetManager {
4314
4336
  const target = this.routing.resolve(threadId);
4315
4337
  if (!target)
4316
4338
  return "fleet topic: no instance routed for this thread";
4317
- // Fleet topic: allow if collab enabled OR access mode is open
4339
+ // Fleet topic: this gate lets the copy through when the adapter is open OR
4340
+ // collab is on for the instance. "Through" is not "delivered" — access
4341
+ // control runs next, and the two arms are not symmetric there:
4342
+ //
4343
+ // open adapter → isAllowed() returns true outright: really admitted.
4344
+ // locked + collab → the bot's user id still has to be on the allowlist,
4345
+ // so the message is normally refused a few lines later
4346
+ // under "Access DENIED for non-allowed user".
4347
+ //
4348
+ // Collab alone therefore does not admit a bot on a locked adapter; it only
4349
+ // declines to drop the copy here and leaves the decision to access control.
4318
4350
  const isOpen = this.getChannelConfig(msg.adapterId)?.access?.mode === "open";
4319
4351
  if (!isOpen && !this.collabInstances.has(target.name)) {
4320
4352
  return `fleet topic: adapter not open and collab off for ${target.name}`;
4321
4353
  }
4322
4354
  return null;
4323
4355
  }
4356
+ /**
4357
+ * Say so when an adapter's access mode is coming from its state file rather
4358
+ * than from fleet.yaml. The state file wins by design — a pairing done at
4359
+ * runtime has to survive a restart — but that also means an edit to
4360
+ * `access.mode` silently does nothing, which is indistinguishable from the
4361
+ * fleet not having reloaded. Naming the file is what makes it fixable.
4362
+ */
4363
+ warnIfAccessModeOverridden(accessManager, statePath) {
4364
+ const configured = accessManager.overriddenConfigMode();
4365
+ if (!configured)
4366
+ return;
4367
+ this.logger.warn({ statePath, configured, inEffect: accessManager.getMode() }, `access mode "${accessManager.getMode()}" comes from the state file and overrides "${configured}" in fleet.yaml; delete ${statePath} to go back to the configured value`);
4368
+ }
4324
4369
  async handleInboundMessage(msg) {
4325
4370
  const threadId = msg.threadId || undefined;
4326
4371
  this.logger.debug({ source: msg.source, chatId: msg.chatId, threadId, userId: msg.userId, isBotMessage: msg.isBotMessage, textLen: (msg.text ?? "").length, text: (msg.text ?? "").slice(0, 80) }, "handleInboundMessage entry");
@@ -5227,6 +5272,15 @@ export class FleetManager {
5227
5272
  */
5228
5273
  scheduleSourceAdapter(schedule) {
5229
5274
  const chatId = String(schedule.reply_chat_id);
5275
+ // New schedules carry the adapter that owned the source chat at creation.
5276
+ // Keep that persona across source-instance rebinding, but never trust a
5277
+ // stale adapter after the chat has moved to another world.
5278
+ const persistedWorld = schedule.reply_adapter_id
5279
+ ? this.worlds.get(schedule.reply_adapter_id)
5280
+ : undefined;
5281
+ if (persistedWorld && String(persistedWorld.groupId) === chatId) {
5282
+ return persistedWorld.adapter;
5283
+ }
5230
5284
  const sourceAdapterId = schedule.source ? this.getInstanceAdapterId(schedule.source) : undefined;
5231
5285
  const sourceWorld = sourceAdapterId ? this.worlds.get(sourceAdapterId) : undefined;
5232
5286
  if (sourceWorld && String(sourceWorld.groupId) === chatId)
@@ -5337,6 +5391,7 @@ export class FleetManager {
5337
5391
  target: payload.target || instanceName,
5338
5392
  reply_chat_id: meta.chat_id,
5339
5393
  reply_thread_id: meta.thread_id || null,
5394
+ reply_adapter_id: meta.adapter_id || null,
5340
5395
  label: payload.label,
5341
5396
  timezone: payload.timezone,
5342
5397
  silent: !!(payload.silent),
@@ -7004,6 +7059,39 @@ export class FleetManager {
7004
7059
  * (or an assist for one they stopped) must not stay clickable for the rest
7005
7060
  * of its 15 minutes.
7006
7061
  */
7062
+ /**
7063
+ * Collapse every still-armed button prompt, for a fleet that is going away.
7064
+ *
7065
+ * The map is in memory; the buttons are in the chat for up to 24 hours. A
7066
+ * restart therefore leaves them looking live — pressing one takes the stale
7067
+ * branch, which acknowledges the click and drops the dismissal without
7068
+ * saying so. Retiring them here means the user meets a spent prompt instead
7069
+ * of a live-looking dead one.
7070
+ *
7071
+ * Best effort and bounded. Each collapse is a platform call, a day of tips
7072
+ * can be many of them, and shutdown still has instances to stop. Whatever
7073
+ * has not finished by the deadline is abandoned — that is exactly today's
7074
+ * behaviour, so the budget can only leave things no worse than before.
7075
+ */
7076
+ async retirePendingNoncePrompts(budgetMs = NONCE_RETIRE_BUDGET_MS) {
7077
+ const entries = [...this.pendingNonceButtons.values()];
7078
+ this.pendingNonceButtons.clear();
7079
+ for (const entry of entries)
7080
+ if (entry.timer)
7081
+ clearTimeout(entry.timer);
7082
+ const collapses = entries
7083
+ .filter(entry => entry.messageId && entry.adapter.editMessageRemoveButtons)
7084
+ .map(entry => entry.adapter.editMessageRemoveButtons(entry.chatId, entry.messageId, entry.expiredText, entry.threadId).catch(err => this.logger.debug({ err, instanceName: entry.instanceName, prefix: entry.prefix }, "Failed to retire button prompt during shutdown")));
7085
+ if (!collapses.length)
7086
+ return;
7087
+ await Promise.race([
7088
+ Promise.allSettled(collapses),
7089
+ new Promise(resolve => {
7090
+ const timer = setTimeout(resolve, budgetMs);
7091
+ timer.unref?.();
7092
+ }),
7093
+ ]);
7094
+ }
7007
7095
  clearNoncePromptsForInstance(instanceName) {
7008
7096
  for (const [nonce, entry] of this.pendingNonceButtons) {
7009
7097
  if (entry.instanceName !== instanceName)
@@ -7944,11 +8032,39 @@ export class FleetManager {
7944
8032
  for (const name of this.configuredBackendInstanceNames()) {
7945
8033
  configured.add(this.backendNameOf(name));
7946
8034
  }
7947
- const choices = Object.keys(LOGIN_FLOWS)
7948
- .filter(backend => configured.size === 0 || configured.has(backend))
7949
- .map(backend => ({ action: backend, label: backend }));
8035
+ const installed = this.probeInstalledBackends();
8036
+ const candidates = new Set([...installed, ...configured]);
8037
+ const unsupported = [];
8038
+ const choices = [...candidates].sort().flatMap(backend => {
8039
+ const flow = LOGIN_FLOWS[backend];
8040
+ const remoteLogin = !!flow && flow.remoteLogin !== "unsupported";
8041
+ const status = [];
8042
+ if (installed.has(backend))
8043
+ status.push(t("login.status_installed"));
8044
+ if (configured.has(backend))
8045
+ status.push(t("login.status_configured"));
8046
+ if (remoteLogin)
8047
+ status.push(t("login.status_auth"));
8048
+ else
8049
+ status.push(t("login.status_unsupported"));
8050
+ if (!remoteLogin) {
8051
+ unsupported.push({ backend, flow, status });
8052
+ return [];
8053
+ }
8054
+ return [{ action: backend, label: `${backend} · ${status.join(" · ")}` }];
8055
+ });
8056
+ if (unsupported.length) {
8057
+ const guidance = unsupported.map(({ backend, flow, status }) => `${backend} · ${status.join(" · ")} — ${flow?.remoteLogin === "unsupported"
8058
+ ? t("login.remote_unsupported_agent_cli", backend, flow.command)
8059
+ : backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
8060
+ await chat.adapter.sendText(chat.chatId, guidance, { threadId: chat.threadId })
8061
+ .catch(err => this.logger.warn({ err }, "Failed to post unsupported login guidance"));
8062
+ }
7950
8063
  if (choices.length === 0) {
7951
- await chat.adapter.sendText(chat.chatId, t("login.unsupported", [...configured].join(", ")), { threadId: chat.threadId });
8064
+ if (!unsupported.length) {
8065
+ await chat.adapter.sendText(chat.chatId, t("login.none_available"), { threadId: chat.threadId })
8066
+ .catch(err => this.logger.warn({ err }, "Failed to post empty login chooser guidance"));
8067
+ }
7952
8068
  return;
7953
8069
  }
7954
8070
  await this.postNonceButtonPrompt({
@@ -8537,6 +8653,12 @@ export class FleetManager {
8537
8653
  await chat.adapter.sendText(chat.chatId, t("install.success_no_login", backend), { threadId: chat.threadId }).catch(() => { });
8538
8654
  return;
8539
8655
  }
8656
+ // The button is deliberately best-effort: it is a short-lived
8657
+ // capability and can be lost during an adapter reconnect. Always
8658
+ // publish a durable completion line first so a successful install can
8659
+ // never look like it silently disappeared. Include the binary that
8660
+ // passed the fresh-login-shell verification and the exact next step.
8661
+ await chat.adapter.sendText(chat.chatId, t("install.success", backend, info.binary), { threadId: chat.threadId }).catch(err => this.logger.warn({ err, backend }, "Failed to send durable install success notification"));
8540
8662
  await this.postNonceButtonPrompt({
8541
8663
  prefix: INSTALL_LOGIN_CALLBACK_PREFIX,
8542
8664
  alertType: "login",
@@ -8788,6 +8910,8 @@ export class FleetManager {
8788
8910
  // Grok reads AGENTS.md project docs; agy reads .agents/agents.md — the
8789
8911
  // same files their writeConfig() appends fleet instructions to.
8790
8912
  "grok": "AGENTS.md",
8913
+ // muse init scaffolds AGENTS.md and the binary reads it as project rules.
8914
+ "muse": "AGENTS.md",
8791
8915
  "antigravity": ".agents/agents.md",
8792
8916
  "mock": "CLAUDE.md",
8793
8917
  };
@@ -8863,6 +8987,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
8863
8987
  // vendor-canonical location is .grok/skills.
8864
8988
  "opencode": [".agents", "skills"],
8865
8989
  "grok": [".grok", "skills"],
8990
+ // Live-verified on muse 1.3.0: `muse skills list --source project` sees
8991
+ // .agents/skills and ignores .muse/skills.
8992
+ "muse": [".agents", "skills"],
8866
8993
  "antigravity": [".agents", "skills"],
8867
8994
  };
8868
8995
  /** Copy general-knowledge steering + all role-eligible skills to General. */
@@ -10602,11 +10729,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10602
10729
  for (const pending of this.pendingClassicStarts.values())
10603
10730
  clearTimeout(pending.timer);
10604
10731
  this.pendingClassicStarts.clear();
10605
- for (const pending of this.pendingNonceButtons.values()) {
10606
- if (pending.timer)
10607
- clearTimeout(pending.timer);
10608
- }
10609
- this.pendingNonceButtons.clear();
10732
+ // Adapters are still connected here — they are stopped further down — so
10733
+ // this is the last moment the prompts can be collapsed.
10734
+ await this.retirePendingNoncePrompts();
10610
10735
  this.topicArchiver.stop();
10611
10736
  this.scheduler?.shutdown();
10612
10737
  // Stop instances in parallel batches to avoid long sequential waits.