@agentconnect.md/daemon 1.0.0-rc.229 → 1.0.0-rc.230

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.
@@ -5728,6 +5728,17 @@ var LocalStore = class {
5728
5728
  threadTranscript(channel, thread) {
5729
5729
  return this.db.prepare("SELECT * FROM transcript WHERE channel = ? AND thread = ? ORDER BY seq ASC").all(channel, thread);
5730
5730
  }
5731
+ /**
5732
+ * The session thread a Telegram message belongs to, recovered from the transcript
5733
+ * (every conversational `text` row carries its platform message id in `ts`, and
5734
+ * Telegram message ids are unique per chat). Backs reply-based session continuity:
5735
+ * a human reply to a bot message (`reply_to_message.message_id`) resolves to the
5736
+ * session that bot message was posted in. Undefined when the id was never recorded
5737
+ * as text (e.g. a reply to transient chrome, or an unknown message).
5738
+ */
5739
+ telegramThreadForMessage(channel, messageId) {
5740
+ return this.db.prepare("SELECT thread FROM transcript WHERE channel = ? AND ts = ? AND kind = 'text' ORDER BY seq DESC LIMIT 1").get(channel, messageId)?.thread;
5741
+ }
5731
5742
  openSessionAgents(channel, thread) {
5732
5743
  return this.db.prepare("SELECT agentId FROM sessions WHERE channel = ? AND thread = ? AND state != 'closed'").all(channel, thread).map((r) => r.agentId);
5733
5744
  }
@@ -10151,23 +10162,26 @@ function routeRules(msg, rules, threadOwner, explicitAgentId) {
10151
10162
  }
10152
10163
  //#endregion
10153
10164
  //#region src/commands/commands.ts
10154
- /** Accepted command prefixes (Slack uses `!`; `/` is for future platforms). */
10165
+ /** Accepted command prefixes (Slack uses `!`; `/` is the Telegram/Discord surface). */
10155
10166
  const COMMAND_PREFIXES = ["!", "/"];
10156
10167
  const STOP_WORDS = /* @__PURE__ */ new Set(["stop"]);
10157
10168
  const CANCEL_WORDS = /* @__PURE__ */ new Set(["cancel"]);
10158
10169
  const QUEUE_WORDS = /* @__PURE__ */ new Set(["queue"]);
10170
+ const STATUS_WORDS = /* @__PURE__ */ new Set(["status"]);
10171
+ const FAST_WORDS = /* @__PURE__ */ new Set(["fast"]);
10159
10172
  /**
10160
10173
  * Parse a leading control command from a message's text. Returns `null` when the
10161
10174
  * text is not a recognized command (so it flows to the agent unchanged). The
10162
10175
  * prefix must be the first non-whitespace character and be followed immediately by
10163
10176
  * a known command word, so ordinary text like `hello!` or `! note` is never a
10164
- * command.
10177
+ * command. An optional `@botname` right after the word (Telegram group addressing)
10178
+ * is stripped before the argument is read.
10165
10179
  */
10166
10180
  function parseCommand(raw) {
10167
10181
  const text = raw.trimStart();
10168
10182
  const prefix = COMMAND_PREFIXES.find((p) => text.startsWith(p));
10169
10183
  if (!prefix) return null;
10170
- const m = /^([a-zA-Z]+)([\s\S]*)$/.exec(text.slice(prefix.length));
10184
+ const m = /^([a-zA-Z]+)(?:@[A-Za-z0-9_]+)?([\s\S]*)$/.exec(text.slice(prefix.length));
10171
10185
  if (!m) return null;
10172
10186
  const word = m[1].toLowerCase();
10173
10187
  const arg = (m[2] ?? "").trim();
@@ -10177,6 +10191,14 @@ function parseCommand(raw) {
10177
10191
  kind: "queue",
10178
10192
  text: arg
10179
10193
  };
10194
+ if (STATUS_WORDS.has(word)) return { kind: "status" };
10195
+ if (FAST_WORDS.has(word)) {
10196
+ const a = arg.toLowerCase();
10197
+ return {
10198
+ kind: "fast",
10199
+ enable: a === "on" ? true : a === "off" ? false : null
10200
+ };
10201
+ }
10180
10202
  return null;
10181
10203
  }
10182
10204
  //#endregion
@@ -117339,15 +117361,16 @@ function photoToAttachment(sizes) {
117339
117361
  };
117340
117362
  }
117341
117363
  /**
117342
- * Which Telegram concept maps to a Slack "thread" (drives session keying):
117343
- * forum-topic message_thread_id → reply_to_message chain → channel-root (none).
117344
- * Channel-root deliberately returns undefined so the whole chat is ONE session
117345
- * (unlike Slack, whose own-ts fallback makes every top-level message its own
117346
- * thread) — the intended Telegram continuity model.
117364
+ * The raw Telegram `message_thread_id` of a message, or undefined. This id means one of
117365
+ * two DIFFERENT things depending on `is_topic_message`:
117366
+ * - in a forum supergroup (is_topic_message) it's the forum TOPIC id, and
117367
+ * - in a plain supergroup it's the REPLY-THREAD ROOT (Telegram auto-threads replies).
117368
+ * Normalize splits these into `telegramTopicId` vs `telegramThreadRoot` because only the
117369
+ * forum-topic id may be sent back as `message_thread_id` when posting — a plain reply
117370
+ * thread is continued with `reply_parameters`, not `message_thread_id`.
117347
117371
  */
117348
117372
  function telegramThread(msg) {
117349
- if (msg.message_thread_id != null) return String(msg.message_thread_id);
117350
- if (msg.reply_to_message?.message_id != null) return String(msg.reply_to_message.message_id);
117373
+ return msg.message_thread_id != null ? String(msg.message_thread_id) : void 0;
117351
117374
  }
117352
117375
  /** Distinct bot/user handles mentioned in a message — @usernames (without the '@')
117353
117376
  * from `mention` entities, plus numeric ids from `text_mention` entities. The
@@ -117369,14 +117392,18 @@ function normalizeTelegramMessage(msg, ctx) {
117369
117392
  if (photo) attachments.push(photo);
117370
117393
  const doc = documentToAttachment(msg.document);
117371
117394
  if (doc) attachments.push(doc);
117372
- const thread = telegramThread(msg);
117395
+ const threadId = telegramThread(msg);
117396
+ const isForumTopic = msg.is_topic_message === true;
117397
+ const replyTo = msg.reply_to_message?.message_id != null ? String(msg.reply_to_message.message_id) : void 0;
117373
117398
  return {
117374
117399
  msgId: `telegram:${msg.chat.id}:${msg.message_id}`,
117375
117400
  traceId: ctx.traceId,
117376
117401
  source: "user",
117377
117402
  platform: "telegram",
117378
117403
  channel: String(msg.chat.id),
117379
- ...thread !== void 0 ? { thread } : {},
117404
+ ...isForumTopic && threadId !== void 0 ? { telegramTopicId: threadId } : {},
117405
+ ...!isForumTopic && threadId !== void 0 ? { telegramThreadRoot: threadId } : {},
117406
+ ...replyTo !== void 0 ? { replyTo } : {},
117380
117407
  sender: {
117381
117408
  id: msg.from ? String(msg.from.id) : "unknown",
117382
117409
  isBot: Boolean(msg.from?.is_bot)
@@ -117388,254 +117415,42 @@ function normalizeTelegramMessage(msg, ctx) {
117388
117415
  };
117389
117416
  }
117390
117417
  //#endregion
117391
- //#region src/telegram/render.ts
117392
- /** Telegram single-message hard cap (4096 chars) — vs Slack's 12000-char block. */
117393
- const TELEGRAM_MESSAGE_LIMIT = 4096;
117394
- const MAX_LABEL = 100;
117395
- const MAX_REASONING = 2800;
117396
- const MAX_TOOL_OUTPUT = 2800;
117397
- function clampTo(s, max) {
117398
- const t = s.trim();
117399
- return t.length > max ? `${t.slice(0, max - 1)}…` : t;
117400
- }
117401
- /** Escape the three characters Telegram's HTML parse_mode is sensitive to. */
117402
- function escapeHtml(s) {
117403
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
117404
- }
117405
- /** An HTML inline code span (monospace, verbatim). */
117406
- function htmlCode(s) {
117407
- return `<code>${escapeHtml(s)}</code>`;
117408
- }
117409
- /** An HTML preformatted block (monospace, multi-line). */
117410
- function htmlPre(s) {
117411
- return `<pre>${escapeHtml(s)}</pre>`;
117412
- }
117413
- /** Head-clamp a finished tool's output (kept under the message cap so the whole
117414
- * block posts as one message); head-kept since the start is usually most useful. */
117415
- function capOutput(s) {
117416
- const t = s.trim();
117417
- return t.length > MAX_TOOL_OUTPUT ? `${t.slice(0, MAX_TOOL_OUTPUT - 1)}…` : t;
117418
- }
117419
- /** Pull human-readable output text out of an ACP tool_call/_update (content[] text
117420
- * blocks preferred; string rawOutput fallback). Mirrors slack/render's extractor. */
117421
- function extractToolOutput(update) {
117422
- const content = update.content;
117423
- if (Array.isArray(content)) {
117424
- const parts = [];
117425
- for (const item of content) {
117426
- if (!item || typeof item !== "object") continue;
117427
- if (item.type !== "content") continue;
117428
- const block = item.content;
117429
- if (block?.type === "text" && block.text) parts.push(block.text);
117430
- }
117431
- if (parts.length) return parts.join("\n").trim();
117432
- }
117433
- return typeof update.rawOutput === "string" ? update.rawOutput.trim() : "";
117434
- }
117435
- function planIcon(status) {
117436
- if (status === "completed") return "✅";
117437
- if (status === "in_progress") return "⏳";
117438
- return "⬜";
117439
- }
117440
- /** Render an ACP plan (full entry list, resent each update) as a compact HTML summary. */
117441
- function renderPlan(entries) {
117442
- return ["📋 <b>Plan</b>", ...entries.map((e) => `${planIcon(e.status)} ${escapeHtml(clampTo(e.content ?? "", MAX_LABEL))}`)].join("\n");
117443
- }
117444
- /** Render the accumulated reasoning trace (high mode) tail-clamped, as HTML. */
117445
- function renderReasoning(buf) {
117446
- const trimmed = buf.trim();
117447
- return `💭 <b>Thinking</b>\n${escapeHtml(trimmed.length > MAX_REASONING ? `…${trimmed.slice(-2800)}` : trimmed)}`;
117448
- }
117449
- function compactCount(n) {
117450
- if (n < 1e3) return String(n);
117451
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
117452
- return `${(n / 1e6).toFixed(1)}M`;
117453
- }
117454
- /** The compact status line (HTML), e.g. `📊 <b>opus-4.8</b> · fast · ctx 120k/200k (60%) · 45.2k tok`. */
117455
- function renderStatusText(info) {
117456
- const parts = [];
117457
- if (info.model) parts.push(`<b>${escapeHtml(info.model)}</b>`);
117458
- if (info.fastMode) parts.push("fast");
117459
- if (info.contextUsed !== void 0 && info.contextSize !== void 0 && info.contextSize > 0) {
117460
- const pct = Math.round(info.contextUsed / info.contextSize * 100);
117461
- parts.push(`ctx ${compactCount(info.contextUsed)}/${compactCount(info.contextSize)} (${pct}%)`);
117462
- } else if (info.contextUsed !== void 0) parts.push(`ctx ${compactCount(info.contextUsed)}`);
117463
- if (info.totalTokens !== void 0) parts.push(`${compactCount(info.totalTokens)} tok`);
117464
- return `📊 ${parts.length ? parts.join(" · ") : "—"}`;
117465
- }
117466
- /** callback_data verbs for the status keyboard. Telegram caps callback_data at 64
117467
- * bytes and cannot fit a full session key, so the verb is stateless — the
117468
- * connection maps the message (chat+message_id) back to its session key. */
117469
- const TG_CALLBACK = {
117470
- cancel: "ac_cancel",
117471
- fastOn: "ac_fast_on",
117472
- fastOff: "ac_fast_off"
117473
- };
117474
- /** Parse a status-keyboard callback_data verb into a status action (null if unknown). */
117475
- function parseTelegramCallback(data) {
117476
- if (data === TG_CALLBACK.cancel) return { kind: "cancel" };
117477
- if (data === TG_CALLBACK.fastOn) return {
117478
- kind: "set-fast",
117479
- fastMode: true
117480
- };
117481
- if (data === TG_CALLBACK.fastOff) return {
117482
- kind: "set-fast",
117483
- fastMode: false
117484
- };
117485
- return null;
117486
- }
117418
+ //#region src/telegram/connection.ts
117487
117419
  /**
117488
- * Build the per-turn status inline keyboard: a Cancel button, an optional Fast
117489
- * on/off toggle (when the model advertises one), and a View-session URL button
117490
- * when a deep link is known. Model/effort selection isn't offered here — inline
117491
- * keyboards can't do dropdowns; that's a future Mini-App. Pure — unit-testable.
117420
+ * §Telegram edge unit. Mirrors slack/connection.ts but over grammY long-polling
117421
+ * (getUpdates) — an OUTBOUND long-lived connection, so it works behind NAT just
117422
+ * like Slack Socket Mode. One connection per bot token.
117423
+ *
117424
+ * Control surface: unlike Slack (interactive status bar + modal), Telegram exposes
117425
+ * session state and controls via slash commands — `/status`, `/stop`, `/cancel`,
117426
+ * `/fast`, `/queue`, parsed by the daemon like any inbound text. start() registers
117427
+ * them with BotFather (setMyCommands) so they autocomplete in the Telegram UI.
117492
117428
  */
117493
- function buildStatusKeyboard(info, link) {
117494
- const row = [{
117495
- text: "🛑 Cancel",
117496
- callback_data: TG_CALLBACK.cancel
117497
- }];
117498
- if (info.fastModeAvailable) row.push(info.fastMode ? {
117499
- text: "⚡ Fast: on",
117500
- callback_data: TG_CALLBACK.fastOff
117501
- } : {
117502
- text: "⚡ Fast: off",
117503
- callback_data: TG_CALLBACK.fastOn
117504
- });
117505
- if (link) row.push({
117506
- text: "🔗 View session",
117507
- url: link
117508
- });
117509
- return { inline_keyboard: [row] };
117510
- }
117511
- var TelegramConverger = class {
117512
- mode;
117513
- buf = "";
117514
- reasoningBuf = "";
117515
- reasoningDirty = false;
117516
- toolTitles = /* @__PURE__ */ new Map();
117517
- toolOutputs = /* @__PURE__ */ new Map();
117518
- emittedOutput = /* @__PURE__ */ new Set();
117519
- constructor(mode) {
117520
- this.mode = mode;
117521
- }
117522
- /** True while body text OR reasoning is pending — the daemon (re)arms the idle-flush timer on it. */
117523
- hasBuffered() {
117524
- return this.buf.trim().length > 0 || this.reasoningDirty;
117525
- }
117526
- /** Idle-timer flush: one in-place reasoning update (high) placed ABOVE the body, then the body. */
117527
- flushBuffered() {
117528
- return [...this.drainReasoning(), ...this.flush()];
117529
- }
117530
- drainReasoning() {
117531
- if (!this.reasoningDirty) return [];
117532
- this.reasoningDirty = false;
117533
- return [{
117534
- kind: "reasoning",
117535
- text: renderReasoning(this.reasoningBuf),
117536
- parseMode: "HTML"
117537
- }];
117538
- }
117539
- flush() {
117540
- const text = this.buf;
117541
- this.buf = "";
117542
- if (!text.trim()) return [];
117543
- return splitIntoSections(text, TELEGRAM_MESSAGE_LIMIT).map((t) => ({
117544
- kind: "post",
117545
- text: t
117546
- }));
117547
- }
117548
- toolLabel(update) {
117549
- const id = update.toolCallId;
117550
- if (update.title) {
117551
- if (id) this.toolTitles.set(id, update.title);
117552
- return update.title;
117553
- }
117554
- return (id && this.toolTitles.get(id)) ?? id ?? "tool";
117555
- }
117556
- drainToolOutput(update) {
117557
- const id = update.toolCallId;
117558
- if (!id) return [];
117559
- if (update.content !== void 0 || update.rawOutput !== void 0) {
117560
- const out = extractToolOutput(update);
117561
- if (out) this.toolOutputs.set(id, out);
117562
- else this.toolOutputs.delete(id);
117563
- }
117564
- if (!(update.status === "completed" || update.status === "failed") || this.emittedOutput.has(id)) return [];
117565
- const text = this.toolOutputs.get(id);
117566
- if (!text) return [];
117567
- this.emittedOutput.add(id);
117568
- this.toolOutputs.delete(id);
117569
- return [{
117570
- kind: "tool-output",
117571
- text: `${update.status === "failed" ? "❌" : "📄"}\n${htmlPre(capOutput(text))}`,
117572
- parseMode: "HTML"
117573
- }];
117574
- }
117575
- onUpdate(update) {
117576
- switch (update.sessionUpdate) {
117577
- case "agent_message_chunk": {
117578
- const content = update.content;
117579
- if (content?.type === "text") this.buf += content.text ?? "";
117580
- return [];
117581
- }
117582
- case "agent_thought_chunk":
117583
- if (this.mode === "high") {
117584
- const text = update.content?.text ?? "";
117585
- if (text) {
117586
- this.reasoningBuf += text;
117587
- if (this.reasoningBuf.length > MAX_REASONING * 2) this.reasoningBuf = this.reasoningBuf.slice(-2800 * 2);
117588
- this.reasoningDirty = true;
117589
- }
117590
- }
117591
- return [...this.flush(), { kind: "typing" }];
117592
- case "tool_call":
117593
- case "tool_call_update": {
117594
- const u = update;
117595
- const label = this.toolLabel(u);
117596
- if (this.mode === "low") return [...this.flush(), { kind: "typing" }];
117597
- const actions = [
117598
- ...this.flush(),
117599
- { kind: "typing" },
117600
- {
117601
- kind: "progress",
117602
- text: `🔨 ${htmlCode(label)}`,
117603
- parseMode: "HTML"
117604
- }
117605
- ];
117606
- if (this.mode === "high") actions.push(...this.drainToolOutput(u));
117607
- return actions;
117608
- }
117609
- case "plan": {
117610
- const entries = update.entries ?? [];
117611
- if (this.mode === "low") return [...this.flush(), { kind: "typing" }];
117612
- return [...this.flush(), {
117613
- kind: "plan",
117614
- text: renderPlan(entries),
117615
- parseMode: "HTML"
117616
- }];
117617
- }
117618
- default: return [];
117619
- }
117620
- }
117621
- /** Turn end: flush remaining body; in medium/high append a done footer with the deep link. */
117622
- onFinal(link) {
117623
- if (this.mode === "low") return [...this.flush()];
117624
- const reasoning = this.drainReasoning();
117625
- const footer = link ? [{
117626
- kind: "notice",
117627
- text: `✅ done — <a href="${escapeHtml(link)}">details</a>`,
117628
- parseMode: "HTML"
117629
- }] : [];
117630
- return [
117631
- ...reasoning,
117632
- ...this.flush(),
117633
- ...footer
117634
- ];
117429
+ /** The bot commands advertised to Telegram (setMyCommands) so they autocomplete.
117430
+ * The daemon parses the actual invocations (commands/commands.ts); this list is
117431
+ * purely the client-side menu. */
117432
+ const BOT_COMMANDS = [
117433
+ {
117434
+ command: "status",
117435
+ description: "Show session model, context, and token usage"
117436
+ },
117437
+ {
117438
+ command: "stop",
117439
+ description: "Stop the current turn and mute until you @mention me"
117440
+ },
117441
+ {
117442
+ command: "cancel",
117443
+ description: "Cancel the current turn (keep the session live)"
117444
+ },
117445
+ {
117446
+ command: "fast",
117447
+ description: "Toggle fast mode — /fast on | off"
117448
+ },
117449
+ {
117450
+ command: "queue",
117451
+ description: "Queue a message to run when idle — /queue <text>"
117635
117452
  }
117636
- };
117637
- //#endregion
117638
- //#region src/telegram/connection.ts
117453
+ ];
117639
117454
  /** §6.1 analog: group integrations by bot token (one grammY Bot per token). */
117640
117455
  function consolidateTelegram(agents) {
117641
117456
  const groups = /* @__PURE__ */ new Map();
@@ -117670,9 +117485,6 @@ function defaultFactory(token) {
117670
117485
  onMessage(handler) {
117671
117486
  bot.on("message", (ctx) => handler(ctx.message));
117672
117487
  },
117673
- onCallbackQuery(handler) {
117674
- bot.on("callback_query:data", (ctx) => handler(ctx.callbackQuery));
117675
- },
117676
117488
  start(onStart) {
117677
117489
  bot.start(onStart ? { onStart: () => onStart() } : void 0);
117678
117490
  },
@@ -117686,7 +117498,6 @@ var TelegramConnection = class {
117686
117498
  deps;
117687
117499
  bot;
117688
117500
  queue;
117689
- statusKeys = /* @__PURE__ */ new Map();
117690
117501
  /** The bot token this connection authenticated with (used to detect a swap). */
117691
117502
  botToken;
117692
117503
  /** The bot's numeric user id (as string), resolved at start() via getMe. */
@@ -117712,48 +117523,44 @@ var TelegramConnection = class {
117712
117523
  log?.debug(`telegram: getMe ok → bot @${this.botUsername || "?"} (id ${this.botUserId || "n/a"})`);
117713
117524
  this.bot.onMessage((message) => {
117714
117525
  const msg = normalizeTelegramMessage(message, { traceId: this.deps.newTraceId() });
117715
- log?.debug(`telegram: inbound ch=${msg.channel} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} thread=${msg.thread ?? "-"} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`);
117526
+ log?.debug(`telegram: inbound ch=${msg.channel} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} topic=${msg.telegramTopicId ?? "-"} root=${msg.telegramThreadRoot ?? "-"} replyTo=${msg.replyTo ?? "-"} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`);
117716
117527
  this.deps.onMessage(msg);
117717
117528
  });
117718
- this.bot.onCallbackQuery((q) => {
117719
- this.answerCallbackQuery(q.id);
117720
- const action = q.data ? parseTelegramCallback(q.data) : null;
117721
- if (!action || !q.message) return;
117722
- const sessionKey = this.statusKeys.get(`${q.message.chat.id}:${q.message.message_id}`);
117723
- if (!sessionKey) return;
117724
- if (action.kind === "cancel") this.deps.onStatusAction?.({
117725
- kind: "cancel",
117726
- sessionKey
117727
- });
117728
- else this.deps.onStatusAction?.({
117729
- kind: "set-fast",
117730
- sessionKey,
117731
- fastMode: action.fastMode
117732
- });
117733
- });
117529
+ try {
117530
+ await this.bot.api.setMyCommands(BOT_COMMANDS);
117531
+ log?.debug("telegram: setMyCommands ok");
117532
+ } catch (err) {
117533
+ log?.debug(`telegram: setMyCommands failed: ${err.message}`);
117534
+ }
117734
117535
  log?.debug("telegram: bot.start → opening long-poll (getUpdates)…");
117735
117536
  this.bot.start(() => log?.debug("telegram: long-poll established"));
117736
117537
  }
117737
117538
  /**
117738
117539
  * Post a message. `threadTs` (when numeric) is treated as a forum-topic
117739
- * message_thread_id. Returns the resulting message id as a string.
117740
- *
117741
- * Stage 3: sent as plain text (no parse_mode). MarkdownV2 conversion + the
117742
- * serialized send-queue arrive with telegram/render.ts.
117540
+ * message_thread_id — the daemon's reply-based session threads are non-numeric
117541
+ * (`tg:<id>` / `dm`), so they correctly post to the chat root here. `replyTo`
117542
+ * (a message id) threads the post as a reply, which is how the bot anchors its
117543
+ * turn to the triggering message and keeps the reply chain intact. Returns the
117544
+ * resulting message id as a string.
117743
117545
  */
117744
- async postMessage(channel, text, threadTs) {
117546
+ async postMessage(channel, text, threadTs, replyTo) {
117745
117547
  const thread = threadTs != null && /^\d+$/.test(threadTs) ? Number(threadTs) : void 0;
117746
117548
  return this.queue.enqueue(async () => {
117747
- const res = await this.bot.api.sendMessage(channel, text, thread !== void 0 ? { message_thread_id: thread } : {});
117549
+ const res = await this.bot.api.sendMessage(channel, text, {
117550
+ ...thread !== void 0 ? { message_thread_id: thread } : {},
117551
+ ...replyTo !== void 0 ? { reply_parameters: {
117552
+ message_id: replyTo,
117553
+ allow_sending_without_reply: true
117554
+ } } : {}
117555
+ });
117748
117556
  return res?.message_id != null ? String(res.message_id) : void 0;
117749
117557
  });
117750
117558
  }
117751
117559
  /**
117752
- * Post a "chrome" message (progress / plan / reasoning / tool-output / status bar)
117753
- * with an optional parse_mode + inline keyboard + forum topic. Returns the new
117754
- * message id for later in-place edits. When `sessionKey` is given (the status
117755
- * bar), the message is registered so its keyboard callbacks resolve back to the
117756
- * session. Best-effort: swallows send errors (chrome must never break a turn).
117560
+ * Post a "chrome" message (progress / plan / reasoning / tool-output / a `/status`
117561
+ * reply) with an optional parse_mode + forum topic + reply target. Returns the new
117562
+ * message id for later in-place edits. Best-effort: swallows send errors (chrome
117563
+ * must never break a turn).
117757
117564
  */
117758
117565
  async postChrome(channel, text, opts = {}) {
117759
117566
  const thread = opts.threadTs != null && /^\d+$/.test(opts.threadTs) ? Number(opts.threadTs) : void 0;
@@ -117762,11 +117569,12 @@ var TelegramConnection = class {
117762
117569
  const res = await this.bot.api.sendMessage(channel, text, {
117763
117570
  ...thread !== void 0 ? { message_thread_id: thread } : {},
117764
117571
  ...opts.parseMode ? { parse_mode: opts.parseMode } : {},
117765
- ...opts.keyboard ? { reply_markup: opts.keyboard } : {}
117572
+ ...opts.replyTo !== void 0 ? { reply_parameters: {
117573
+ message_id: opts.replyTo,
117574
+ allow_sending_without_reply: true
117575
+ } } : {}
117766
117576
  });
117767
- const id = res?.message_id != null ? String(res.message_id) : void 0;
117768
- if (id && opts.sessionKey) this.statusKeys.set(`${channel}:${id}`, opts.sessionKey);
117769
- return id;
117577
+ return res?.message_id != null ? String(res.message_id) : void 0;
117770
117578
  } catch (err) {
117771
117579
  this.deps.log?.debug(`telegram: sendMessage (chrome) failed (ch=${channel}): ${err.message}`);
117772
117580
  return;
@@ -117777,10 +117585,7 @@ var TelegramConnection = class {
117777
117585
  async updateMessage(channel, messageId, text, opts = {}) {
117778
117586
  await this.queue.enqueue(async () => {
117779
117587
  try {
117780
- await this.bot.api.editMessageText(channel, Number(messageId), text, {
117781
- ...opts.parseMode ? { parse_mode: opts.parseMode } : {},
117782
- ...opts.keyboard ? { reply_markup: opts.keyboard } : {}
117783
- });
117588
+ await this.bot.api.editMessageText(channel, Number(messageId), text, { ...opts.parseMode ? { parse_mode: opts.parseMode } : {} });
117784
117589
  } catch (err) {
117785
117590
  this.deps.log?.debug(`telegram: editMessageText failed (ch=${channel} id=${messageId}): ${err.message}`);
117786
117591
  }
@@ -117795,14 +117600,6 @@ var TelegramConnection = class {
117795
117600
  this.deps.log?.debug(`telegram: sendChatAction failed (ch=${channel}): ${err.message}`);
117796
117601
  }
117797
117602
  }
117798
- /** Ack a callback_query so Telegram stops showing the button spinner. Best-effort. */
117799
- async answerCallbackQuery(id, text) {
117800
- try {
117801
- await this.bot.api.answerCallbackQuery(id, text ? { text } : {});
117802
- } catch (err) {
117803
- this.deps.log?.debug(`telegram: answerCallbackQuery failed: ${err.message}`);
117804
- }
117805
- }
117806
117603
  /**
117807
117604
  * Download a Telegram file by its `file_id` (carried in Attachment.sourceUrl):
117808
117605
  * getFile → file_path → GET https://api.telegram.org/file/bot<token>/<path>, up
@@ -117988,6 +117785,217 @@ var SlackNameResolver = class {
117988
117785
  }
117989
117786
  };
117990
117787
  //#endregion
117788
+ //#region src/telegram/render.ts
117789
+ /** Telegram single-message hard cap (4096 chars) — vs Slack's 12000-char block. */
117790
+ const TELEGRAM_MESSAGE_LIMIT = 4096;
117791
+ const MAX_LABEL = 100;
117792
+ const MAX_REASONING = 2800;
117793
+ const MAX_TOOL_OUTPUT = 2800;
117794
+ function clampTo(s, max) {
117795
+ const t = s.trim();
117796
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
117797
+ }
117798
+ /** Escape the three characters Telegram's HTML parse_mode is sensitive to. */
117799
+ function escapeHtml(s) {
117800
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
117801
+ }
117802
+ /** An HTML inline code span (monospace, verbatim). */
117803
+ function htmlCode(s) {
117804
+ return `<code>${escapeHtml(s)}</code>`;
117805
+ }
117806
+ /** An HTML preformatted block (monospace, multi-line). */
117807
+ function htmlPre(s) {
117808
+ return `<pre>${escapeHtml(s)}</pre>`;
117809
+ }
117810
+ /** Head-clamp a finished tool's output (kept under the message cap so the whole
117811
+ * block posts as one message); head-kept since the start is usually most useful. */
117812
+ function capOutput(s) {
117813
+ const t = s.trim();
117814
+ return t.length > MAX_TOOL_OUTPUT ? `${t.slice(0, MAX_TOOL_OUTPUT - 1)}…` : t;
117815
+ }
117816
+ /** Pull human-readable output text out of an ACP tool_call/_update (content[] text
117817
+ * blocks preferred; string rawOutput fallback). Mirrors slack/render's extractor. */
117818
+ function extractToolOutput(update) {
117819
+ const content = update.content;
117820
+ if (Array.isArray(content)) {
117821
+ const parts = [];
117822
+ for (const item of content) {
117823
+ if (!item || typeof item !== "object") continue;
117824
+ if (item.type !== "content") continue;
117825
+ const block = item.content;
117826
+ if (block?.type === "text" && block.text) parts.push(block.text);
117827
+ }
117828
+ if (parts.length) return parts.join("\n").trim();
117829
+ }
117830
+ return typeof update.rawOutput === "string" ? update.rawOutput.trim() : "";
117831
+ }
117832
+ function planIcon(status) {
117833
+ if (status === "completed") return "✅";
117834
+ if (status === "in_progress") return "⏳";
117835
+ return "⬜";
117836
+ }
117837
+ /** Render an ACP plan (full entry list, resent each update) as a compact HTML summary. */
117838
+ function renderPlan(entries) {
117839
+ return ["📋 <b>Plan</b>", ...entries.map((e) => `${planIcon(e.status)} ${escapeHtml(clampTo(e.content ?? "", MAX_LABEL))}`)].join("\n");
117840
+ }
117841
+ /** Render the accumulated reasoning trace (high mode) tail-clamped, as HTML. */
117842
+ function renderReasoning(buf) {
117843
+ const trimmed = buf.trim();
117844
+ return `💭 <b>Thinking</b>\n${escapeHtml(trimmed.length > MAX_REASONING ? `…${trimmed.slice(-2800)}` : trimmed)}`;
117845
+ }
117846
+ function compactCount(n) {
117847
+ if (n < 1e3) return String(n);
117848
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
117849
+ return `${(n / 1e6).toFixed(1)}M`;
117850
+ }
117851
+ /** The compact status line (HTML), e.g. `📊 <b>opus-4.8</b> · fast · ctx 120k/200k (60%) · 45.2k tok`. */
117852
+ function renderStatusText(info) {
117853
+ const parts = [];
117854
+ if (info.model) parts.push(`<b>${escapeHtml(info.model)}</b>`);
117855
+ if (info.fastMode) parts.push("fast");
117856
+ if (info.contextUsed !== void 0 && info.contextSize !== void 0 && info.contextSize > 0) {
117857
+ const pct = Math.round(info.contextUsed / info.contextSize * 100);
117858
+ parts.push(`ctx ${compactCount(info.contextUsed)}/${compactCount(info.contextSize)} (${pct}%)`);
117859
+ } else if (info.contextUsed !== void 0) parts.push(`ctx ${compactCount(info.contextUsed)}`);
117860
+ if (info.totalTokens !== void 0) parts.push(`${compactCount(info.totalTokens)} tok`);
117861
+ return `📊 ${parts.length ? parts.join(" · ") : "—"}`;
117862
+ }
117863
+ /**
117864
+ * The `/status` command reply (HTML): the compact status line, plus a View-session
117865
+ * link line when a deep link is known. Sent as Telegram chrome (parse_mode HTML),
117866
+ * so the link renders as a tappable anchor. Pure — unit-testable.
117867
+ */
117868
+ function renderStatusReply(info, link) {
117869
+ const line = renderStatusText(info);
117870
+ return link ? `${line}\n🔗 <a href="${escapeHtml(link)}">View session</a>` : line;
117871
+ }
117872
+ var TelegramConverger = class {
117873
+ mode;
117874
+ buf = "";
117875
+ reasoningBuf = "";
117876
+ reasoningDirty = false;
117877
+ toolTitles = /* @__PURE__ */ new Map();
117878
+ toolOutputs = /* @__PURE__ */ new Map();
117879
+ emittedOutput = /* @__PURE__ */ new Set();
117880
+ constructor(mode) {
117881
+ this.mode = mode;
117882
+ }
117883
+ /** True while body text OR reasoning is pending — the daemon (re)arms the idle-flush timer on it. */
117884
+ hasBuffered() {
117885
+ return this.buf.trim().length > 0 || this.reasoningDirty;
117886
+ }
117887
+ /** Idle-timer flush: one in-place reasoning update (high) placed ABOVE the body, then the body. */
117888
+ flushBuffered() {
117889
+ return [...this.drainReasoning(), ...this.flush()];
117890
+ }
117891
+ drainReasoning() {
117892
+ if (!this.reasoningDirty) return [];
117893
+ this.reasoningDirty = false;
117894
+ return [{
117895
+ kind: "reasoning",
117896
+ text: renderReasoning(this.reasoningBuf),
117897
+ parseMode: "HTML"
117898
+ }];
117899
+ }
117900
+ flush() {
117901
+ const text = this.buf;
117902
+ this.buf = "";
117903
+ if (!text.trim()) return [];
117904
+ return splitIntoSections(text, TELEGRAM_MESSAGE_LIMIT).map((t) => ({
117905
+ kind: "post",
117906
+ text: t
117907
+ }));
117908
+ }
117909
+ toolLabel(update) {
117910
+ const id = update.toolCallId;
117911
+ if (update.title) {
117912
+ if (id) this.toolTitles.set(id, update.title);
117913
+ return update.title;
117914
+ }
117915
+ return (id && this.toolTitles.get(id)) ?? id ?? "tool";
117916
+ }
117917
+ drainToolOutput(update) {
117918
+ const id = update.toolCallId;
117919
+ if (!id) return [];
117920
+ if (update.content !== void 0 || update.rawOutput !== void 0) {
117921
+ const out = extractToolOutput(update);
117922
+ if (out) this.toolOutputs.set(id, out);
117923
+ else this.toolOutputs.delete(id);
117924
+ }
117925
+ if (!(update.status === "completed" || update.status === "failed") || this.emittedOutput.has(id)) return [];
117926
+ const text = this.toolOutputs.get(id);
117927
+ if (!text) return [];
117928
+ this.emittedOutput.add(id);
117929
+ this.toolOutputs.delete(id);
117930
+ return [{
117931
+ kind: "tool-output",
117932
+ text: `${update.status === "failed" ? "❌" : "📄"}\n${htmlPre(capOutput(text))}`,
117933
+ parseMode: "HTML"
117934
+ }];
117935
+ }
117936
+ onUpdate(update) {
117937
+ switch (update.sessionUpdate) {
117938
+ case "agent_message_chunk": {
117939
+ const content = update.content;
117940
+ if (content?.type === "text") this.buf += content.text ?? "";
117941
+ return [];
117942
+ }
117943
+ case "agent_thought_chunk":
117944
+ if (this.mode === "high") {
117945
+ const text = update.content?.text ?? "";
117946
+ if (text) {
117947
+ this.reasoningBuf += text;
117948
+ if (this.reasoningBuf.length > MAX_REASONING * 2) this.reasoningBuf = this.reasoningBuf.slice(-2800 * 2);
117949
+ this.reasoningDirty = true;
117950
+ }
117951
+ }
117952
+ return [...this.flush(), { kind: "typing" }];
117953
+ case "tool_call":
117954
+ case "tool_call_update": {
117955
+ const u = update;
117956
+ const label = this.toolLabel(u);
117957
+ if (this.mode === "low") return [...this.flush(), { kind: "typing" }];
117958
+ const actions = [
117959
+ ...this.flush(),
117960
+ { kind: "typing" },
117961
+ {
117962
+ kind: "progress",
117963
+ text: `🔨 ${htmlCode(label)}`,
117964
+ parseMode: "HTML"
117965
+ }
117966
+ ];
117967
+ if (this.mode === "high") actions.push(...this.drainToolOutput(u));
117968
+ return actions;
117969
+ }
117970
+ case "plan": {
117971
+ const entries = update.entries ?? [];
117972
+ if (this.mode === "low") return [...this.flush(), { kind: "typing" }];
117973
+ return [...this.flush(), {
117974
+ kind: "plan",
117975
+ text: renderPlan(entries),
117976
+ parseMode: "HTML"
117977
+ }];
117978
+ }
117979
+ default: return [];
117980
+ }
117981
+ }
117982
+ /** Turn end: flush remaining body; in medium/high append a done footer with the deep link. */
117983
+ onFinal(link) {
117984
+ if (this.mode === "low") return [...this.flush()];
117985
+ const reasoning = this.drainReasoning();
117986
+ const footer = link ? [{
117987
+ kind: "notice",
117988
+ text: `✅ done — <a href="${escapeHtml(link)}">details</a>`,
117989
+ parseMode: "HTML"
117990
+ }] : [];
117991
+ return [
117992
+ ...reasoning,
117993
+ ...this.flush(),
117994
+ ...footer
117995
+ ];
117996
+ }
117997
+ };
117998
+ //#endregion
117991
117999
  //#region ../../node_modules/.pnpm/croner@10.0.1/node_modules/croner/dist/croner.js
117992
118000
  function T(s) {
117993
118001
  return Date.UTC(s.y, s.m - 1, s.d, s.h, s.i, s.s);
@@ -136793,7 +136801,6 @@ var Daemon = class Daemon {
136793
136801
  group,
136794
136802
  newTraceId: () => randomUUID(),
136795
136803
  onMessage: (msg) => this.onInbound(msg),
136796
- onStatusAction: (a) => this.handleStatusAction(a),
136797
136804
  log: this.log
136798
136805
  });
136799
136806
  try {
@@ -136918,6 +136925,59 @@ var Daemon = class Daemon {
136918
136925
  this.hosts.set(agentId, host);
136919
136926
  return host;
136920
136927
  }
136928
+ /** The bare Telegram message id from a normalized `telegram:<chatId>:<messageId>`
136929
+ * msgId (the last segment; chat ids may be negative, so split, don't parse). */
136930
+ telegramMessageId(msg) {
136931
+ const parts = msg.msgId.split(":");
136932
+ return parts[parts.length - 1] ?? "";
136933
+ }
136934
+ /**
136935
+ * Resolve the session thread for a Telegram message and set it on `msg.thread` in
136936
+ * place (normalize leaves it unset for Telegram). Ladder:
136937
+ * - forum topic → the topic id (a real native thread; numeric, so it also
136938
+ * drives the post's `message_thread_id`)
136939
+ * - native reply-root → `tg:<root>` from Telegram's own `message_thread_id` in a
136940
+ * plain supergroup (Telegram auto-threads replies to the root)
136941
+ * - DM → one continuous session per chat (`dm`)
136942
+ * - reply that maps → the session the replied-to message belongs to, resolved from
136943
+ * the transcript (basic groups carry no `message_thread_id`)
136944
+ * - otherwise → a fresh session rooted at this message (`tg:<msgId>`) — a new
136945
+ * @mention, or a reply we can't place
136946
+ * A fresh @mention (message N, no thread root) keys `tg:N`; every later reply in that
136947
+ * thread carries `message_thread_id = N`, so it keys `tg:N` too — the two meet with no
136948
+ * lookup. The non-topic keys are deliberately NON-numeric so posting never mistakes
136949
+ * them for a forum `message_thread_id` (see TelegramConnection.postMessage). No-op for
136950
+ * other platforms or when the thread is already set.
136951
+ */
136952
+ canonicalizeTelegramThread(msg) {
136953
+ if (msg.platform !== "telegram" || msg.thread !== void 0) return;
136954
+ if (msg.telegramTopicId !== void 0) {
136955
+ msg.thread = msg.telegramTopicId;
136956
+ return;
136957
+ }
136958
+ if (msg.telegramThreadRoot !== void 0) {
136959
+ msg.thread = `tg:${msg.telegramThreadRoot}`;
136960
+ return;
136961
+ }
136962
+ if (msg.isDm) {
136963
+ msg.thread = "dm";
136964
+ return;
136965
+ }
136966
+ if (msg.replyTo) {
136967
+ msg.thread = this.store.telegramThreadForMessage(msg.channel, msg.replyTo) ?? `tg:${msg.replyTo}`;
136968
+ return;
136969
+ }
136970
+ msg.thread = `tg:${this.telegramMessageId(msg)}`;
136971
+ }
136972
+ /** Telegram reply target for a turn/command triggered by `msg`: the triggering
136973
+ * message's own id, so the bot's posts reply to it (req: reply to the last message
136974
+ * in the session, and keep the reply chain resolvable). Undefined off Telegram or
136975
+ * when the id can't be recovered as a positive integer. */
136976
+ telegramReplyTarget(msg) {
136977
+ if (msg.platform !== "telegram") return void 0;
136978
+ const n = Number(this.telegramMessageId(msg));
136979
+ return Number.isInteger(n) && n > 0 ? n : void 0;
136980
+ }
136921
136981
  seenMsgIds = /* @__PURE__ */ new Set();
136922
136982
  onInbound(msg) {
136923
136983
  if (this.draining) {
@@ -136934,6 +136994,7 @@ var Daemon = class Daemon {
136934
136994
  this.log.debug(`routing: dropping own bot echo ${msg.msgId}`);
136935
136995
  return;
136936
136996
  }
136997
+ this.canonicalizeTelegramThread(msg);
136937
136998
  const command = parseCommand(msg.text);
136938
136999
  if (command) {
136939
137000
  this.handleCommand(command, msg);
@@ -137236,28 +137297,62 @@ var Daemon = class Daemon {
137236
137297
  const rec = this.store.getSession(key);
137237
137298
  const acpSessionId = rec?.acpSessionId;
137238
137299
  const inflight = !!(acpSessionId && this.pending.has(acpSessionId));
137300
+ const tgReplyTo = this.telegramReplyTarget(msg);
137301
+ const reply = (text) => {
137302
+ if (!conn) return;
137303
+ if (msg.platform === "telegram") conn.postMessage(msg.channel, text, thread, tgReplyTo);
137304
+ else conn.postMessage(msg.channel, text, thread);
137305
+ };
137239
137306
  if (command.kind === "stop") {
137240
137307
  if (rec) this.store.setSessionMuted(key, true);
137241
137308
  const muteNote = "Muted in this thread — @mention me to resume.";
137242
137309
  if (!inflight) {
137243
- conn?.postMessage(msg.channel, rec ? `🔇 Nothing is running. ${muteNote}` : "Nothing is running to stop.", thread);
137310
+ reply(rec ? `🔇 Nothing is running. ${muteNote}` : "Nothing is running to stop.");
137244
137311
  return;
137245
137312
  }
137246
137313
  this.interruptTurn(target.agentId, acpSessionId, key, "stop");
137247
- conn?.postMessage(msg.channel, `🛑 Stopped. ${muteNote}`, thread);
137314
+ reply(`🛑 Stopped. ${muteNote}`);
137248
137315
  return;
137249
137316
  }
137250
137317
  if (command.kind === "cancel") {
137251
137318
  if (!inflight) {
137252
- conn?.postMessage(msg.channel, "Nothing is running to cancel.", thread);
137319
+ reply("Nothing is running to cancel.");
137253
137320
  return;
137254
137321
  }
137255
137322
  this.interruptTurn(target.agentId, acpSessionId, key, "cancel");
137256
- conn?.postMessage(msg.channel, "🛑 Cancelled.", thread);
137323
+ reply("🛑 Cancelled.");
137324
+ return;
137325
+ }
137326
+ if (command.kind === "status") {
137327
+ if (!rec) {
137328
+ reply("No active session here yet — send me a message to start one.");
137329
+ return;
137330
+ }
137331
+ const info = this.statusInfoFrom(target.agentId, key, acpSessionId ?? void 0);
137332
+ const link = acpSessionId ? this.sessionLink(acpSessionId) : void 0;
137333
+ if (msg.platform === "telegram") conn?.postChrome(msg.channel, renderStatusReply(info, link), {
137334
+ parseMode: "HTML",
137335
+ threadTs: thread,
137336
+ replyTo: tgReplyTo
137337
+ });
137338
+ else reply(link ? `${renderStatusBar(info)} · <${link}|View session>` : renderStatusBar(info));
137339
+ return;
137340
+ }
137341
+ if (command.kind === "fast") {
137342
+ if (!rec) {
137343
+ reply("No active session here to configure.");
137344
+ return;
137345
+ }
137346
+ if (command.enable === null) {
137347
+ reply("Usage: `/fast on` or `/fast off`.");
137348
+ return;
137349
+ }
137350
+ this.setFastByKey(key, command.enable);
137351
+ reply(command.enable ? "⚡ Fast mode on." : "🐢 Fast mode off.");
137257
137352
  return;
137258
137353
  }
137259
137354
  if (!command.text) {
137260
- conn?.postMessage(msg.channel, "Usage: `!queue <message>` — runs when the current turn finishes.", thread);
137355
+ reply("Usage: `!queue <message>` — runs when the current turn finishes.");
137261
137356
  return;
137262
137357
  }
137263
137358
  const payload = {
@@ -137272,13 +137367,13 @@ var Daemon = class Daemon {
137272
137367
  const q = this.queued.get(acpSessionId) ?? [];
137273
137368
  if (q.length >= MAX_QUEUED_PER_SESSION) {
137274
137369
  this.log.warn(`command: queue → agent "${target.agentId}" session ${acpSessionId} full (${q.length}), rejected`);
137275
- conn?.postMessage(msg.channel, `Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`, thread);
137370
+ reply(`Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`);
137276
137371
  return;
137277
137372
  }
137278
137373
  q.push(payload);
137279
137374
  this.queued.set(acpSessionId, q);
137280
137375
  this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
137281
- conn?.postMessage(msg.channel, `📥 Queued (#${q.length}) — will run when the current turn finishes.`, thread);
137376
+ reply(`📥 Queued (#${q.length}) — will run when the current turn finishes.`);
137282
137377
  }
137283
137378
  /** Drain one buffered message for a session whose turn just finished (FIFO). */
137284
137379
  flushQueued(agentId, sessionId, integrationId) {
@@ -137387,6 +137482,7 @@ var Daemon = class Daemon {
137387
137482
  acpSessionId: sessionId,
137388
137483
  channel: msg.channel,
137389
137484
  thread: msg.thread,
137485
+ ...this.telegramReplyTarget(msg) !== void 0 ? { tgReplyTo: this.telegramReplyTarget(msg) } : {},
137390
137486
  statusThread,
137391
137487
  conn: replyConn,
137392
137488
  applyChain: Promise.resolve(),
@@ -137595,7 +137691,6 @@ var Daemon = class Daemon {
137595
137691
  * - notice / tool-output → posted (HTML) but NOT recorded (chrome).
137596
137692
  * - typing → a transient chat-action ("typing…").
137597
137693
  * - progress / plan / reasoning → the single in-place message of that kind.
137598
- * - status-bar → the per-turn status line + inline keyboard (Cancel / Fast / View).
137599
137694
  */
137600
137695
  async applyTelegramAction(p, action) {
137601
137696
  const conn = p.conn;
@@ -137605,7 +137700,7 @@ var Daemon = class Daemon {
137605
137700
  await conn.sendChatAction(p.channel);
137606
137701
  return;
137607
137702
  case "post": {
137608
- const id = await conn.postMessage(p.channel, action.text, p.thread);
137703
+ const id = await conn.postMessage(p.channel, action.text, p.thread, p.tgReplyTo);
137609
137704
  this.store.appendTranscript({
137610
137705
  channel: p.channel,
137611
137706
  thread: p.statusThread,
@@ -137620,7 +137715,8 @@ var Daemon = class Daemon {
137620
137715
  case "tool-output":
137621
137716
  await conn.postChrome(p.channel, action.text, {
137622
137717
  parseMode: action.parseMode,
137623
- threadTs: p.thread
137718
+ threadTs: p.thread,
137719
+ replyTo: p.tgReplyTo
137624
137720
  });
137625
137721
  return;
137626
137722
  case "progress":
@@ -137629,7 +137725,8 @@ var Daemon = class Daemon {
137629
137725
  p.progressAttempted = true;
137630
137726
  p.progressTs = await conn.postChrome(p.channel, action.text, {
137631
137727
  parseMode: action.parseMode,
137632
- threadTs: p.thread
137728
+ threadTs: p.thread,
137729
+ replyTo: p.tgReplyTo
137633
137730
  });
137634
137731
  }
137635
137732
  return;
@@ -137639,7 +137736,8 @@ var Daemon = class Daemon {
137639
137736
  p.planAttempted = true;
137640
137737
  p.planTs = await conn.postChrome(p.channel, action.text, {
137641
137738
  parseMode: action.parseMode,
137642
- threadTs: p.thread
137739
+ threadTs: p.thread,
137740
+ replyTo: p.tgReplyTo
137643
137741
  });
137644
137742
  }
137645
137743
  return;
@@ -137648,23 +137746,9 @@ var Daemon = class Daemon {
137648
137746
  else if (!p.reasoningAttempted) {
137649
137747
  p.reasoningAttempted = true;
137650
137748
  p.reasoningTs = await conn.postChrome(p.channel, action.text, {
137651
- parseMode: action.parseMode,
137652
- threadTs: p.thread
137653
- });
137654
- }
137655
- return;
137656
- case "status-bar":
137657
- if (p.statusBarTs) await conn.updateMessage(p.channel, p.statusBarTs, action.text, {
137658
- parseMode: action.parseMode,
137659
- keyboard: action.keyboard
137660
- });
137661
- else if (!p.statusBarAttempted) {
137662
- p.statusBarAttempted = true;
137663
- p.statusBarTs = await conn.postChrome(p.channel, action.text, {
137664
137749
  parseMode: action.parseMode,
137665
137750
  threadTs: p.thread,
137666
- keyboard: action.keyboard,
137667
- sessionKey: p.sessionKey
137751
+ replyTo: p.tgReplyTo
137668
137752
  });
137669
137753
  }
137670
137754
  return;
@@ -137803,8 +137887,9 @@ var Daemon = class Daemon {
137803
137887
  /** Emit/refresh the session's status bar (model / context / tokens / cost). Called at
137804
137888
  * turn start (model + carried-over usage), on each `usage_update` (live context/cost),
137805
137889
  * and at turn end (token totals). Deduped against the last snapshot so an unchanged
137806
- * update is a no-op. Webchat rides a status-only `webchat/output` frame; every other
137807
- * (Slack / headless) target gets the in-place `status-bar` action — headless no-ops in
137890
+ * update is a no-op. Webchat rides a status-only `webchat/output` frame; Slack gets
137891
+ * the in-place `status-bar` action. Telegram has NO status bar — state is queried on
137892
+ * demand via `/status` (see handleCommand) — so it's skipped here. Headless no-ops in
137808
137893
  * applyAction (no connection), which is fine. */
137809
137894
  emitStatusBar(p) {
137810
137895
  const info = this.buildStatusInfo(p);
@@ -137820,16 +137905,11 @@ var Daemon = class Daemon {
137820
137905
  index: wc.index++,
137821
137906
  status: info
137822
137907
  });
137823
- } else {
137908
+ } else if (p.platform === "telegram") p.lastStatusBar = key;
137909
+ else {
137824
137910
  p.lastStatusBar = key;
137825
137911
  const link = this.sessionLink(p.acpSessionId);
137826
- if (p.platform === "telegram") this.enqueueApply(p, {
137827
- kind: "status-bar",
137828
- text: renderStatusText(info),
137829
- parseMode: "HTML",
137830
- keyboard: buildStatusKeyboard(info, link)
137831
- });
137832
- else this.enqueueApply(p, {
137912
+ this.enqueueApply(p, {
137833
137913
  kind: "status-bar",
137834
137914
  text: renderStatusBar(info),
137835
137915
  blocks: buildStatusBlocks(info, p.sessionKey, link)
@@ -138557,4 +138637,4 @@ async function runForeground(opts, deps = {}) {
138557
138637
  //#endregion
138558
138638
  export { ClientTransport as n, runForeground as t };
138559
138639
 
138560
- //# sourceMappingURL=run-foreground-D_XxYTap.js.map
138640
+ //# sourceMappingURL=run-foreground-Cg2ewYjn.js.map