@songsid/agend 2.2.0-beta.10 → 2.2.0-beta.11

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 (98) hide show
  1. package/dist/agent-endpoint.js +3 -0
  2. package/dist/agent-endpoint.js.map +1 -1
  3. package/dist/auth-api.d.ts +3 -1
  4. package/dist/auth-api.js +15 -1
  5. package/dist/auth-api.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +5 -0
  7. package/dist/backend/antigravity.js +27 -13
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/effort-metadata.d.ts +36 -0
  10. package/dist/backend/effort-metadata.js +83 -13
  11. package/dist/backend/effort-metadata.js.map +1 -1
  12. package/dist/backend/launch-effort.d.ts +29 -1
  13. package/dist/backend/launch-effort.js +53 -6
  14. package/dist/backend/launch-effort.js.map +1 -1
  15. package/dist/backend/types.d.ts +14 -0
  16. package/dist/backend/types.js +10 -0
  17. package/dist/backend/types.js.map +1 -1
  18. package/dist/channel/adapters/discord.d.ts +12 -0
  19. package/dist/channel/adapters/discord.js +39 -2
  20. package/dist/channel/adapters/discord.js.map +1 -1
  21. package/dist/channel/adapters/telegram.d.ts +18 -0
  22. package/dist/channel/adapters/telegram.js +24 -3
  23. package/dist/channel/adapters/telegram.js.map +1 -1
  24. package/dist/channel/mcp-tools.js +2 -0
  25. package/dist/channel/mcp-tools.js.map +1 -1
  26. package/dist/channel/tool-router.d.ts +2 -0
  27. package/dist/channel/tool-router.js +1 -1
  28. package/dist/channel/tool-router.js.map +1 -1
  29. package/dist/channel/types.d.ts +16 -0
  30. package/dist/cli.js +34 -11
  31. package/dist/cli.js.map +1 -1
  32. package/dist/daemon.js +9 -0
  33. package/dist/daemon.js.map +1 -1
  34. package/dist/delivery-outbox.d.ts +8 -1
  35. package/dist/delivery-outbox.js +6 -2
  36. package/dist/delivery-outbox.js.map +1 -1
  37. package/dist/fleet-manager.d.ts +59 -3
  38. package/dist/fleet-manager.js +157 -22
  39. package/dist/fleet-manager.js.map +1 -1
  40. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  41. package/dist/instructions.js +2 -2
  42. package/dist/instructions.js.map +1 -1
  43. package/dist/locale.js +26 -0
  44. package/dist/locale.js.map +1 -1
  45. package/dist/outbound-handlers.js +21 -1
  46. package/dist/outbound-handlers.js.map +1 -1
  47. package/dist/outbound-schemas.d.ts +12 -0
  48. package/dist/outbound-schemas.js +12 -1
  49. package/dist/outbound-schemas.js.map +1 -1
  50. package/dist/public-web-gateway.js +3 -0
  51. package/dist/public-web-gateway.js.map +1 -1
  52. package/dist/reply-buttons-controller.d.ts +86 -5
  53. package/dist/reply-buttons-controller.js +271 -7
  54. package/dist/reply-buttons-controller.js.map +1 -1
  55. package/dist/reply-buttons.d.ts +48 -3
  56. package/dist/reply-buttons.js +105 -10
  57. package/dist/reply-buttons.js.map +1 -1
  58. package/dist/reply-polls.d.ts +85 -0
  59. package/dist/reply-polls.js +154 -0
  60. package/dist/reply-polls.js.map +1 -0
  61. package/dist/tool-permissions.js +3 -3
  62. package/dist/tool-permissions.js.map +1 -1
  63. package/dist/topic-commands.js +9 -4
  64. package/dist/topic-commands.js.map +1 -1
  65. package/dist/ui/chat-render.js +19 -2
  66. package/dist/ui/chat-store.js +35 -1
  67. package/dist/ui/chat-strings.js +2 -0
  68. package/dist/ui/chat-thread.js +42 -0
  69. package/dist/ui/panel-chat.js +2 -0
  70. package/dist/ui/panel-settings.js +48 -1
  71. package/dist/ui/settings-strings.js +10 -0
  72. package/dist/ui/shared/app-stream.js +4 -1
  73. package/dist/ui/shared/app.css +8 -0
  74. package/dist/update-check.d.ts +31 -0
  75. package/dist/update-check.js +47 -0
  76. package/dist/update-check.js.map +1 -1
  77. package/dist/update-launch.d.ts +46 -1
  78. package/dist/update-launch.js +146 -3
  79. package/dist/update-launch.js.map +1 -1
  80. package/dist/web-api.d.ts +40 -2
  81. package/dist/web-api.js +130 -14
  82. package/dist/web-api.js.map +1 -1
  83. package/dist/web-chat-history.d.ts +12 -1
  84. package/dist/web-chat-history.js +29 -7
  85. package/dist/web-chat-history.js.map +1 -1
  86. package/dist/web-login.d.ts +14 -0
  87. package/dist/web-login.js +4 -2
  88. package/dist/web-login.js.map +1 -1
  89. package/dist/web-prefs.d.ts +18 -0
  90. package/dist/web-prefs.js +36 -4
  91. package/dist/web-prefs.js.map +1 -1
  92. package/dist/web-profile.d.ts +32 -0
  93. package/dist/web-profile.js +132 -0
  94. package/dist/web-profile.js.map +1 -0
  95. package/dist/web-session.d.ts +14 -0
  96. package/dist/web-session.js +10 -0
  97. package/dist/web-session.js.map +1 -1
  98. package/package.json +1 -1
@@ -20,7 +20,7 @@ import { withinBudget } from "./monotonic-budget.js";
20
20
  import { measureSyncWork } from "./sync-work-attribution.js";
21
21
  import { RuntimeCpuProfiler, ProfileBusyError, profileDuration } from "./runtime-cpu-profile.js";
22
22
  import { ProfileControlServer } from "./profile-control.js";
23
- import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
23
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, constants as fsConstants, closeSync } from "node:fs";
24
24
  import { atomicWriteFileSync } from "./atomic-write.js";
25
25
  import { createHash, randomBytes, randomUUID } from "node:crypto";
26
26
  import { spawnSync } from "node:child_process";
@@ -141,8 +141,10 @@ import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId } fr
141
141
  import { WebChatDiskStore } from "./web-chat-store.js";
142
142
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
143
143
  import { WebPrefsStore } from "./web-prefs.js";
144
+ import { WEB_NAME_FALLBACK, safeWebName, validateWebName, webNameInUse, webPersonKey } from "./web-profile.js";
144
145
  import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
145
146
  import { ReplyButtonsController } from "./reply-buttons-controller.js";
147
+ import { parsePoll } from "./reply-polls.js";
146
148
  import { publicAttachment, sweepOrphanedUploads, WebFileLedger } from "./web-upload.js";
147
149
  import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
148
150
  import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
@@ -162,7 +164,7 @@ import { canonicalCommand, commandSpec, decideCommand, ruleFor } from "./command
162
164
  import { runVisibilityCommand } from "./cross-instance-notice.js";
163
165
  import { installedChannel, isPrereleaseVersion, updateNoticeKey } from "./update-check.js";
164
166
  import { resolveInstalledAgend } from "./update-dispatch.js";
165
- import { resolveUpdateLaunch, watchUpdateLaunch } from "./update-launch.js";
167
+ import { resolveUpdateLaunch, openUpdateLog, updateLogPath, watchUpdateLaunch } from "./update-launch.js";
166
168
  import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOGIN_RECOVERY_DEADLINE_MS } from "./login-controller.js";
167
169
  import { runBeforeDeadline } from "./deadline.js";
168
170
  import { LoginWindowLock } from "./login-window-lock.js";
@@ -380,6 +382,7 @@ const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
380
382
  const WEB_ONLY_REPLY_SINK = {
381
383
  type: "web",
382
384
  supportsReplyButtons: true, // #1266: the web chat shows a reply's buttons itself
385
+ supportsPolls: true, // #1823: …and a poll (its pages are told; there is nothing to edit)
383
386
  sendText: async () => ({ chatId: "web", messageId: newWebMessageId() }),
384
387
  sendFile: async () => ({ chatId: "web", messageId: newWebMessageId() }),
385
388
  };
@@ -807,6 +810,78 @@ export class FleetManager {
807
810
  seenUsers = new SeenUsers();
808
811
  webPrefsStore;
809
812
  /** #1665: the web preferences kept on the host (<home>/web-prefs.json), read on first use. */
813
+ /**
814
+ * #1827: a web person as agents and chats see them. Signed in as an account (a sign-in code DMed to it): that
815
+ * account's user id — still `via web`, and only for recognition — and the name they chose, else their account's name
816
+ * at sign-in (made safe). Otherwise (an `agend web` session): "web-user", as before, under a name the operator may set.
817
+ * Names are checked again now: one an instance or bot has taken since reads as the next choice down.
818
+ */
819
+ webPerson(owner) {
820
+ const key = webPersonKey(owner);
821
+ let profile = null;
822
+ try {
823
+ profile = this.webPrefs.profile(key);
824
+ }
825
+ catch (err) {
826
+ this.logger.warn({ err: err.message }, "Web profile unavailable");
827
+ }
828
+ const reserved = (n) => this.webNameReserved(n);
829
+ const own = profile?.name !== undefined ? webNameInUse(profile.name, reserved) : WEB_NAME_FALLBACK;
830
+ const chosen = own !== WEB_NAME_FALLBACK;
831
+ const platformName = owner ? safeWebName(profile?.platformName, reserved) : undefined;
832
+ return {
833
+ key, bound: !!owner,
834
+ userId: owner ? owner.userId : WEB_NAME_FALLBACK,
835
+ // A poll ballot is the account's — the same key its votes on the platform have — or the operator's.
836
+ voterKey: owner ? `${owner.adapterId}:${owner.userId}` : "web:operator",
837
+ name: chosen ? own : platformName ?? WEB_NAME_FALLBACK,
838
+ set: chosen,
839
+ ...(platformName !== undefined ? { platformName } : {}),
840
+ };
841
+ }
842
+ /** #1827: set (null: clear — back to their account's name) a web person's own name; validated in full. */
843
+ setWebPersonName(owner, raw) {
844
+ let name = null;
845
+ if (raw !== null) {
846
+ const v = validateWebName(raw, (n) => this.webNameReserved(n));
847
+ if (!v.ok)
848
+ return { ok: false, status: 400, error: v.error };
849
+ name = v.name;
850
+ }
851
+ const w = this.webPrefs.setProfileName(webPersonKey(owner), name);
852
+ return w.ok ? { ok: true, person: this.webPerson(owner) } : { ok: false, status: w.status, error: w.error };
853
+ }
854
+ /** #1827 (auth-api): a sign-in as this account — its display name then is the web name it starts with. */
855
+ noteWebSignIn(account) {
856
+ if (account.username)
857
+ this.webPrefs.setPlatformName(webPersonKey(account), account.username);
858
+ }
859
+ /**
860
+ * #1827: a name that belongs to something here — an instance (or ClassicBot room), its display name, a bot. On the web
861
+ * send path: it never throws (a lookup that fails reserves nothing; the name was checked in full when it was set).
862
+ */
863
+ webNameReserved(lowered) {
864
+ try {
865
+ return this.webNameTaken(lowered);
866
+ }
867
+ catch {
868
+ return false;
869
+ }
870
+ }
871
+ webNameTaken(lowered) {
872
+ const low = (s) => (typeof s === "string" ? s.normalize("NFC").toLocaleLowerCase("en-US") : "");
873
+ for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
874
+ if (low(name) === lowered || low(cfg.display_name) === lowered)
875
+ return true;
876
+ }
877
+ for (const ch of this.classicChannels?.getAll() ?? [])
878
+ if (low(ch.instanceName) === lowered || low(ch.displayName) === lowered)
879
+ return true;
880
+ for (const w of this.worlds.values())
881
+ if (w.botUsername && low(w.botUsername) === lowered)
882
+ return true;
883
+ return false;
884
+ }
810
885
  get webPrefs() {
811
886
  // #1664: a favorite must be an instance of this fleet now — fleet.yaml's (own keys) or a ClassicBot room's.
812
887
  return (this.webPrefsStore ??= new WebPrefsStore(this.dataDir, {
@@ -1155,6 +1230,8 @@ export class FleetManager {
1155
1230
  mayClick: (set, userId) => this.mayAnswerReplyButtons(set.instance, set.adapterId, userId),
1156
1231
  deliver: (set, button, by) => this.deliverReplyButtonChoice(set, button, by),
1157
1232
  publish: (instance, view) => this.emitSseEvent("reply_buttons", { instance, buttons: view }),
1233
+ admitResults: (set, key, text) => this.admitPollResults(set, key, text),
1234
+ monotonic: () => performance.now(),
1158
1235
  logger: this.logger,
1159
1236
  setTimer: (fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; },
1160
1237
  clearTimer: (h) => clearTimeout(h),
@@ -1163,6 +1240,21 @@ export class FleetManager {
1163
1240
  return this.replyButtonsCtl;
1164
1241
  }
1165
1242
  replyButtonsStore = null;
1243
+ /**
1244
+ * #1823: a closed poll's results go to its instance through the delivery outbox, as a system notice keyed by the
1245
+ * poll: durable (held until the instance can take it) and admitted once, however often this is asked. Addressed to
1246
+ * the poll's chat, so the instance's answer (the right option, the scores) goes there.
1247
+ */
1248
+ admitPollResults(set, key, text) {
1249
+ this.ensureDeliveryOutbox();
1250
+ const outbox = this.deliveryOutbox;
1251
+ if (!outbox)
1252
+ return false;
1253
+ outbox.admitSystemNotice(set.instance, key, text, new Date(), {
1254
+ chatId: set.chatId, threadId: set.threadId || undefined, adapterId: set.adapterId === "web" ? undefined : set.adapterId,
1255
+ });
1256
+ return true;
1257
+ }
1166
1258
  /**
1167
1259
  * #1266: who may answer a reply's buttons on a platform — whoever may message that instance there: in a ClassicBot
1168
1260
  * room anyone there (as a typed message), elsewhere someone the sending connection lets speak (#754/#1148), never a
@@ -2238,10 +2330,15 @@ export class FleetManager {
2238
2330
  this.failUpdateProgress(t("not_authorized"));
2239
2331
  return;
2240
2332
  }
2333
+ const logPath = updateLogPath(this.dataDir);
2334
+ const ts = new Date().toISOString();
2335
+ const logFd = openUpdateLog(logPath, `--- /update ${ts} ---`);
2241
2336
  const child = spawn(launch.command, launch.args, {
2242
- detached: true, stdio: "ignore", env: withOrigin(origin),
2337
+ detached: true, stdio: ["ignore", logFd ? logFd.fd : "ignore", logFd ? logFd.fd : "ignore"], env: withOrigin(origin),
2243
2338
  });
2244
- watchUpdateLaunch(child, message => this.failUpdateProgress(message));
2339
+ if (logFd)
2340
+ closeSync(logFd.fd); // parent closes its copy; the child's fd stays open
2341
+ watchUpdateLaunch(child, message => this.failUpdateProgress(message), logFd ? logPath : undefined, logFd?.offset, installed.agend);
2245
2342
  child.unref();
2246
2343
  }
2247
2344
  async handleProfileSlash(data, adapterId) {
@@ -3123,7 +3220,8 @@ export class FleetManager {
3123
3220
  * or a rebound adapter is never posted through a stale route. A copy
3124
3221
  * already in flight may still land late — the contract permits that.
3125
3222
  */
3126
- async sendClassicWebEcho(instance, text) {
3223
+ /** `sender` (#1827): the web person's name, as the room log records them ("web-user" when none). */
3224
+ async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK) {
3127
3225
  const guard = this.webChannelEchoGuards.get(instance);
3128
3226
  const handled = new Set();
3129
3227
  let posted = 0;
@@ -3154,7 +3252,7 @@ export class FleetManager {
3154
3252
  if (!adapter?.sendText)
3155
3253
  continue;
3156
3254
  await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
3157
- ClassicChannelManager.logMessage(instance, "web-user", text, new Date());
3255
+ ClassicChannelManager.logMessage(instance, sender, text, new Date());
3158
3256
  posted++;
3159
3257
  }
3160
3258
  catch (err) {
@@ -3713,7 +3811,7 @@ export class FleetManager {
3713
3811
  await data.respond(result.durable ? t("dashboard.revoked", result.count) : t("dashboard.revoked_not_durable", result.count));
3714
3812
  }
3715
3813
  else {
3716
- const owner = this.dashboardOwner(data.userId, adapterId, data.channelId);
3814
+ const owner = this.dashboardOwner(data.userId, adapterId, data.channelId, undefined, data.username);
3717
3815
  if (!owner) {
3718
3816
  await this.sendLocalDashboard(data, adapterId);
3719
3817
  return;
@@ -5035,7 +5133,7 @@ export class FleetManager {
5035
5133
  && cfg.topic_id?.toString() === owner.threadId);
5036
5134
  }
5037
5135
  /** Exact owning General. No primary-adapter or name fallback participates in authorization. */
5038
- dashboardOwner(userId, adapterId, address, threadId) {
5136
+ dashboardOwner(userId, adapterId, address, threadId, username) {
5039
5137
  const world = this.worlds.get(adapterId);
5040
5138
  if (!world || !this.hasFleetAdmins(adapterId) || !this.isFleetAdmin(userId, adapterId))
5041
5139
  return null;
@@ -5050,7 +5148,7 @@ export class FleetManager {
5050
5148
  const matched = dc ? topic === address || (group === address && topic === threadId)
5051
5149
  : group === address && ((topic === "1" && (threadId === undefined || threadId === "1")) || topic === threadId);
5052
5150
  if (group && matched)
5053
- return { userId, adapterId, chatId: group, threadId: topic, binding: world.adapter };
5151
+ return { userId, adapterId, chatId: group, threadId: topic, binding: world.adapter, ...(username ? { username } : {}) };
5054
5152
  }
5055
5153
  return null;
5056
5154
  }
@@ -5059,7 +5157,7 @@ export class FleetManager {
5059
5157
  * — this bot's configured group and its one General topic. No address to match in a DM, so it must be unambiguous:
5060
5158
  * no General, or more than one on this bot and group, and there is no owner (nothing is guessed).
5061
5159
  */
5062
- dmDashboardOwner(userId, adapterId) {
5160
+ dmDashboardOwner(userId, adapterId, username) {
5063
5161
  const world = this.worlds.get(adapterId);
5064
5162
  if (!world || !this.hasFleetAdmins(adapterId) || !this.isFleetAdmin(userId, adapterId))
5065
5163
  return null;
@@ -5073,7 +5171,7 @@ export class FleetManager {
5073
5171
  .map(([, cfg]) => cfg.topic_id.toString());
5074
5172
  if (topics.length !== 1)
5075
5173
  return null;
5076
- const owner = { userId, adapterId, chatId: group, threadId: topics[0], binding: world.adapter };
5174
+ const owner = { userId, adapterId, chatId: group, threadId: topics[0], binding: world.adapter, ...(username ? { username } : {}) };
5077
5175
  return this.publicOwnerCurrent(owner) ? owner : null;
5078
5176
  }
5079
5177
  /**
@@ -5100,7 +5198,7 @@ export class FleetManager {
5100
5198
  async startWebInDm(msg, adapter) {
5101
5199
  if (!adapter)
5102
5200
  return;
5103
- const owner = msg.chatId === msg.userId ? this.dmDashboardOwner(msg.userId, msg.adapterId ?? "") : null;
5201
+ const owner = msg.chatId === msg.userId ? this.dmDashboardOwner(msg.userId, msg.adapterId ?? "", msg.username) : null;
5104
5202
  if (!owner) {
5105
5203
  await adapter.sendText(msg.chatId, t("dashboard.start_web_ack"));
5106
5204
  return;
@@ -5153,7 +5251,7 @@ export class FleetManager {
5153
5251
  /** Telegram's typed command enters through the same strict owner and nonce path as Discord. */
5154
5252
  async dashboardMenu(msg) {
5155
5253
  const id = msg.adapterId;
5156
- const owner = id ? this.dashboardOwner(msg.userId, id, msg.chatId, msg.threadId) : null;
5254
+ const owner = id ? this.dashboardOwner(msg.userId, id, msg.chatId, msg.threadId, msg.username) : null;
5157
5255
  if (!owner) {
5158
5256
  const adapter = id ? this.adapters.get(id) : undefined;
5159
5257
  await adapter?.sendText(msg.chatId, t("dashboard.public_general"), { threadId: msg.threadId });
@@ -5186,7 +5284,9 @@ export class FleetManager {
5186
5284
  return;
5187
5285
  }
5188
5286
  this.initializeWebSessions();
5189
- const login = this.webLoginCodes.issue({ epoch: tokenEpoch(token) });
5287
+ // #1827: the code goes privately to this account (a DM, or else this slash command's ephemeral answer): whoever
5288
+ // redeems it is recognised as that account. Recognition only — no owner (this is not an owning General's code).
5289
+ const login = this.webLoginCodes.issue({ epoch: tokenEpoch(token), recipient: { adapterId, userId: data.userId, ...(data.username ? { username: data.username } : {}) } });
5190
5290
  const current = () => !this.shuttingDown && this.adapters.get(adapterId) === adapter
5191
5291
  && this.webToken === token && this.hasFleetAdmins(adapterId) && this.isFleetAdmin(data.userId, adapterId);
5192
5292
  const privateText = `${text}\n${t("dashboard.code", login.display, Math.round(LOGIN_CODE_TTL_MS / 60_000))}`;
@@ -6412,6 +6512,7 @@ export class FleetManager {
6412
6512
  // Health HTTP endpoint
6413
6513
  this.startHealthServer(fleet.health_port ?? 19280);
6414
6514
  // #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
6515
+ // #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
6415
6516
  void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
6416
6517
  // Daily update check — first check after 1 hour, then every 24 hours
6417
6518
  this.updateCheckTimer = setTimeout(() => {
@@ -8524,6 +8625,17 @@ export class FleetManager {
8524
8625
  respond(null, `reply: ${parsedButtons.error}`);
8525
8626
  return;
8526
8627
  }
8628
+ // #1823: a poll is checked with its buttons. Where it cannot be shown it is refused, never sent as text: text
8629
+ // options would never come back as results.
8630
+ const parsedPoll = parsePoll(args.poll, parsedButtons && "buttons" in parsedButtons ? parsedButtons.buttons : null, args.text);
8631
+ if (parsedPoll && "error" in parsedPoll) {
8632
+ respond(null, `reply: ${parsedPoll.error}`);
8633
+ return;
8634
+ }
8635
+ if (parsedPoll && (!outAdapter.supportsPolls || !this.replyButtons())) {
8636
+ respond(null, `reply: a poll cannot be shown here${outAdapter.supportsPolls ? " (reply buttons are unavailable until AgEnD restarts)" : " — polls work in Discord and Telegram chats and the web chat"}; send the question without poll`);
8637
+ return;
8638
+ }
8527
8639
  // Stickers (#1226) are checked before anything is sent: a refused one is the reply's error, not a gap.
8528
8640
  const stickerProblem = await this.replyStickerProblem(outAdapter, args, threadId, contextAdapterId ?? this.getInstanceAdapterId(senderInstanceName ?? instanceName));
8529
8641
  if (stickerProblem) {
@@ -8543,7 +8655,7 @@ export class FleetManager {
8543
8655
  const buttons = outAdapter.supportsReplyButtons ? this.replyButtons() : null;
8544
8656
  if (buttons) {
8545
8657
  const outId = outAdapter === WEB_ONLY_REPLY_SINK ? "web" : outAdapter.id ?? contextAdapterId ?? "";
8546
- replySet = buttons.prepare({ instance: instanceName, adapterId: outId, chatId: String(args.chat_id ?? ""), threadId }, parsedButtons.buttons);
8658
+ replySet = buttons.prepare({ instance: instanceName, adapterId: outId, chatId: String(args.chat_id ?? ""), threadId }, parsedButtons.buttons, parsedPoll && "poll" in parsedPoll ? parsedPoll.poll : null);
8547
8659
  }
8548
8660
  else {
8549
8661
  args.text = `${String(args.text)}\n\n${replyButtonsFallbackText(parsedButtons.buttons)}`;
@@ -8563,6 +8675,9 @@ export class FleetManager {
8563
8675
  this.replyButtonsCtl?.discard(replySet.id);
8564
8676
  }
8565
8677
  }
8678
+ // #1823: the agent learns its poll's id (the results name it).
8679
+ if (replySet?.poll && !error && result && typeof result === "object")
8680
+ result = { ...result, poll_id: replySet.id };
8566
8681
  ticket.complete(result, error);
8567
8682
  // Return the platform outcome first. Bookkeeping below must never turn
8568
8683
  // a confirmed Discord/Telegram POST into a tool error if a secondary
@@ -8590,7 +8705,7 @@ export class FleetManager {
8590
8705
  });
8591
8706
  }
8592
8707
  };
8593
- if (routeToolCall(outAdapter, tool, args, threadId, respondAndRecord, replySet ? { replyButtons: replySet.callbacks } : {})) {
8708
+ if (routeToolCall(outAdapter, tool, args, threadId, respondAndRecord, replySet ? { replyButtons: replySet.callbacks, ...(replySet.poll ? { replyPoll: replySet.poll } : {}) } : {})) {
8594
8709
  return;
8595
8710
  }
8596
8711
  if (replySet)
@@ -12120,10 +12235,18 @@ export class FleetManager {
12120
12235
  * The caller has passed the /ui gate (session, same origin, CSRF). Here: the prompt must be one offered
12121
12236
  * on the dashboard, about the instance the page named, and the action one of its own buttons.
12122
12237
  */
12123
- /** #1266: a click on a reply's button in the web chat (the /ui gate is passed: a session, or the public link). */
12124
- clickWebReplyButton(instance, id, index) {
12238
+ /** #1823: close_poll — `instance` closes one of its own polls before the deadline. */
12239
+ closePollEarly(instance, id) {
12240
+ const buttons = this.replyButtons();
12241
+ return buttons ? buttons.closeEarly(instance, id) : Promise.resolve({ ok: false, error: "polls are unavailable until AgEnD restarts" });
12242
+ }
12243
+ /**
12244
+ * #1266: a click on a reply's button in the web chat (the /ui gate is passed: a session, or the public link). On a
12245
+ * poll (#1823) it is `voter`'s vote: the page's session.
12246
+ */
12247
+ clickWebReplyButton(instance, id, index, voter) {
12125
12248
  const buttons = this.replyButtons();
12126
- return buttons ? buttons.clickWeb(instance, id, index) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
12249
+ return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
12127
12250
  }
12128
12251
  async clickWebPrompt(instance, nonce, action) {
12129
12252
  if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z-]{0,23}$/.test(action))
@@ -12485,7 +12608,9 @@ export class FleetManager {
12485
12608
  const webMeta = (instance) => {
12486
12609
  const topicId = this.fleetConfig?.instances[instance]?.topic_id;
12487
12610
  return {
12488
- chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(), username: "web-user", userId: "web-user",
12611
+ // #1827: a /steer or /btw reaches the agent as the web person who wrote it.
12612
+ chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(),
12613
+ username: opts.person?.name ?? WEB_NAME_FALLBACK, userId: opts.person?.userId ?? WEB_NAME_FALLBACK,
12489
12614
  threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web", humanOrigin: webHumanOrigin(true),
12490
12615
  };
12491
12616
  };
@@ -16195,7 +16320,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16195
16320
  /** How this instance's backend applies an effort change. */
16196
16321
  effortStrategyFor(instanceName) {
16197
16322
  try {
16198
- const { strategy, levels } = readEffortMetadata(this.backendNameForInstance(instanceName), this.getInstanceDir(instanceName));
16323
+ const { strategy, levels } = this.instanceEffortMetadata(instanceName);
16199
16324
  return strategy !== "unsupported" && levels.length > 0 ? strategy : "unsupported";
16200
16325
  }
16201
16326
  catch {
@@ -16205,13 +16330,23 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16205
16330
  /** Effort levels this instance's backend actually accepts (empty = unsupported). */
16206
16331
  effortLevelsFor(instanceName) {
16207
16332
  try {
16208
- const { strategy, levels } = readEffortMetadata(this.backendNameForInstance(instanceName), this.getInstanceDir(instanceName));
16333
+ const { strategy, levels } = this.instanceEffortMetadata(instanceName);
16209
16334
  return strategy === "unsupported" ? [] : levels;
16210
16335
  }
16211
16336
  catch {
16212
16337
  return [];
16213
16338
  }
16214
16339
  }
16340
+ /** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
16341
+ instanceEffortMetadata(instanceName) {
16342
+ const backend = this.backendNameForInstance(instanceName);
16343
+ let model = null;
16344
+ if (backend === "antigravity") {
16345
+ const resolved = this.resolveInstanceModel(instanceName);
16346
+ model = resolved.source === "unresolved" ? null : resolved.model;
16347
+ }
16348
+ return readEffortMetadata(backend, this.getInstanceDir(instanceName), model);
16349
+ }
16215
16350
  /** Configured effort for an instance: per-instance, else fleet default, else none. */
16216
16351
  resolveInstanceEffort(instanceName) {
16217
16352
  const own = this.fleetConfig?.instances[instanceName]?.effort;