@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.
- package/dist/index.js +2 -2
- package/dist/{login-zwCTM_yW.js → login-8pfSiMc2.js} +2 -2
- package/dist/{login-zwCTM_yW.js.map → login-8pfSiMc2.js.map} +1 -1
- package/dist/{run-foreground-D_XxYTap.js → run-foreground-Cg2ewYjn.js} +424 -344
- package/dist/run-foreground-Cg2ewYjn.js.map +1 -0
- package/dist/run-foreground-DxSBKNXU.js +2 -0
- package/package.json +1 -1
- package/dist/run-foreground-CWZE-O3P.js +0 -2
- package/dist/run-foreground-D_XxYTap.js.map +0 -1
|
@@ -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
|
|
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
|
-
*
|
|
117343
|
-
*
|
|
117344
|
-
*
|
|
117345
|
-
*
|
|
117346
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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/
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
*
|
|
117489
|
-
*
|
|
117490
|
-
*
|
|
117491
|
-
*
|
|
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
|
-
|
|
117494
|
-
|
|
117495
|
-
|
|
117496
|
-
|
|
117497
|
-
|
|
117498
|
-
|
|
117499
|
-
|
|
117500
|
-
|
|
117501
|
-
|
|
117502
|
-
|
|
117503
|
-
|
|
117504
|
-
}
|
|
117505
|
-
|
|
117506
|
-
|
|
117507
|
-
|
|
117508
|
-
}
|
|
117509
|
-
|
|
117510
|
-
|
|
117511
|
-
|
|
117512
|
-
|
|
117513
|
-
|
|
117514
|
-
|
|
117515
|
-
|
|
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}
|
|
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
|
-
|
|
117719
|
-
this.
|
|
117720
|
-
|
|
117721
|
-
|
|
117722
|
-
|
|
117723
|
-
|
|
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
|
|
117740
|
-
*
|
|
117741
|
-
*
|
|
117742
|
-
*
|
|
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,
|
|
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
|
|
117753
|
-
* with an optional parse_mode +
|
|
117754
|
-
* message id for later in-place edits.
|
|
117755
|
-
*
|
|
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.
|
|
117572
|
+
...opts.replyTo !== void 0 ? { reply_parameters: {
|
|
117573
|
+
message_id: opts.replyTo,
|
|
117574
|
+
allow_sending_without_reply: true
|
|
117575
|
+
} } : {}
|
|
117766
117576
|
});
|
|
117767
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
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
|
-
|
|
137314
|
+
reply(`🛑 Stopped. ${muteNote}`);
|
|
137248
137315
|
return;
|
|
137249
137316
|
}
|
|
137250
137317
|
if (command.kind === "cancel") {
|
|
137251
137318
|
if (!inflight) {
|
|
137252
|
-
|
|
137319
|
+
reply("Nothing is running to cancel.");
|
|
137253
137320
|
return;
|
|
137254
137321
|
}
|
|
137255
137322
|
this.interruptTurn(target.agentId, acpSessionId, key, "cancel");
|
|
137256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
137807
|
-
*
|
|
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
|
-
|
|
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-
|
|
138640
|
+
//# sourceMappingURL=run-foreground-Cg2ewYjn.js.map
|