@integrity-labs/agt-cli 0.27.8 → 0.27.9-test.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/assets/impersonate-statusline.sh +7 -2
  2. package/dist/bin/agt.js +946 -129
  3. package/dist/bin/agt.js.map +1 -1
  4. package/dist/{chunk-YSBGIXJG.js → chunk-45I2X4M7.js} +361 -26
  5. package/dist/chunk-45I2X4M7.js.map +1 -0
  6. package/dist/{chunk-IB655E5U.js → chunk-N2EBL4PZ.js} +1086 -285
  7. package/dist/chunk-N2EBL4PZ.js.map +1 -0
  8. package/dist/{chunk-GN4XPQWJ.js → chunk-SJPAXXTW.js} +535 -94
  9. package/dist/chunk-SJPAXXTW.js.map +1 -0
  10. package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-OUGV74LY.js} +2 -2
  11. package/dist/lib/manager-worker.js +2071 -517
  12. package/dist/lib/manager-worker.js.map +1 -1
  13. package/dist/mcp/direct-chat-channel.js +49 -455
  14. package/dist/mcp/index.js +232 -137
  15. package/dist/mcp/slack-channel.js +1379 -856
  16. package/dist/mcp/teams-channel.js +16018 -0
  17. package/dist/mcp/telegram-channel.js +1264 -618
  18. package/dist/{persistent-session-ICYFLUAM.js → persistent-session-G3MR7JVI.js} +5 -3
  19. package/dist/responsiveness-probe-UHSBETBR.js +74 -0
  20. package/dist/responsiveness-probe-UHSBETBR.js.map +1 -0
  21. package/package.json +2 -2
  22. package/dist/chunk-GN4XPQWJ.js.map +0 -1
  23. package/dist/chunk-IB655E5U.js.map +0 -1
  24. package/dist/chunk-YSBGIXJG.js.map +0 -1
  25. package/dist/responsiveness-probe-WZNQ2762.js +0 -33
  26. package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
  27. /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-OUGV74LY.js.map} +0 -0
  28. /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-G3MR7JVI.js.map} +0 -0
@@ -6986,13 +6986,10 @@ __export(slack_block_kit_runtime_exports, {
6986
6986
  DEFAULT_ASK_USER_LIMITS: () => DEFAULT_ASK_USER_LIMITS,
6987
6987
  apiCall: () => apiCall,
6988
6988
  buildAskUserBlocks: () => buildAskUserBlocks,
6989
- buildRatingActionsBlock: () => buildRatingActionsBlock,
6990
6989
  createPendingInteraction: () => createPendingInteraction,
6991
6990
  decodeActionId: () => decodeActionId,
6992
- defaultRatingLabels: () => defaultRatingLabels,
6993
6991
  encodeActionId: () => encodeActionId,
6994
6992
  generateOptionToken: () => generateOptionToken,
6995
- ratingValuesForScale: () => ratingValuesForScale,
6996
6993
  recordSlackDelivery: () => recordSlackDelivery,
6997
6994
  resolveInteractive: () => resolveInteractive,
6998
6995
  updatePendingInteractionMessageTs: () => updatePendingInteractionMessageTs,
@@ -7159,22 +7156,6 @@ function buildAskUserBlocks(opts) {
7159
7156
  }
7160
7157
  ];
7161
7158
  }
7162
- function ratingValuesForScale(scale) {
7163
- return scale === "1-5" ? [1, 2, 3, 4, 5] : [-1, 0, 1];
7164
- }
7165
- function defaultRatingLabels(scale) {
7166
- return scale === "1-5" ? ["1", "2", "3", "4", "5"] : ["\u{1F44E}", "\u{1F914}", "\u{1F44D}"];
7167
- }
7168
- function buildRatingActionsBlock(opts) {
7169
- return {
7170
- type: "actions",
7171
- elements: opts.options.map(({ label, token }) => ({
7172
- type: "button",
7173
- text: { type: "plain_text", text: label },
7174
- action_id: encodeActionId(opts.callbackId, token)
7175
- }))
7176
- };
7177
- }
7178
7159
  async function resolveInteractive(cfg, input) {
7179
7160
  const res = await apiCall(cfg, "POST", "/host/interactive/resolve", {
7180
7161
  agent_id: cfg.agentId,
@@ -14293,13 +14274,13 @@ var StdioServerTransport = class {
14293
14274
 
14294
14275
  // src/telegram-channel.ts
14295
14276
  import https from "https";
14296
- import { createHash, randomUUID as randomUUID2 } from "crypto";
14277
+ import { createHash, randomUUID } from "crypto";
14297
14278
  import {
14298
14279
  createWriteStream,
14299
14280
  existsSync as existsSync2,
14300
14281
  mkdirSync as mkdirSync3,
14301
- readFileSync as readFileSync2,
14302
- readdirSync,
14282
+ readFileSync as readFileSync3,
14283
+ readdirSync as readdirSync2,
14303
14284
  renameSync as renameSync3,
14304
14285
  statSync,
14305
14286
  unlinkSync as unlinkSync3,
@@ -14307,7 +14288,7 @@ import {
14307
14288
  writeFileSync as writeFileSync3
14308
14289
  } from "fs";
14309
14290
  import { homedir as homedir2 } from "os";
14310
- import { join as join3 } from "path";
14291
+ import { join as join4 } from "path";
14311
14292
 
14312
14293
  // src/channel-attachments.ts
14313
14294
  import { homedir } from "os";
@@ -14545,23 +14526,33 @@ var DEFAULT_THROTTLE = {
14545
14526
  windowMs: 12e4,
14546
14527
  ringSize: 8
14547
14528
  };
14548
- function decideReplyThrottle(input) {
14529
+ var DEFAULT_REPLY_QUEUE_DEPTH = 2;
14530
+ function planReplyDelivery(input) {
14549
14531
  const cfg = input.config ?? DEFAULT_THROTTLE;
14532
+ const maxDepth = input.maxQueueDepth ?? DEFAULT_REPLY_QUEUE_DEPTH;
14550
14533
  const cutoff = input.now - cfg.windowMs;
14551
- const inWindow = input.recentReplyTimestamps.filter((t) => t >= cutoff);
14552
- if (inWindow.length >= cfg.threshold) {
14553
- return {
14554
- allow: false,
14555
- recentCount: inWindow.length,
14556
- reason: "throttle_threshold_exceeded"
14557
- };
14534
+ const inWindow = input.recentReplyTimestamps.filter((t) => t >= cutoff).sort((a, b) => a - b);
14535
+ const occupancy = inWindow.length + input.queuedCount;
14536
+ if (occupancy < cfg.threshold) {
14537
+ return { action: "send", recentCount: inWindow.length, reason: "under_threshold" };
14538
+ }
14539
+ if (input.queuedCount >= maxDepth) {
14540
+ return { action: "refuse", recentCount: inWindow.length, reason: "queue_full" };
14558
14541
  }
14542
+ const idx = Math.min(input.queuedCount, inWindow.length - 1);
14543
+ const opensAt = (inWindow[idx] ?? input.now) + cfg.windowMs;
14559
14544
  return {
14560
- allow: true,
14545
+ action: "queue",
14546
+ deliverAtMs: Math.max(opensAt, input.now),
14561
14547
  recentCount: inWindow.length,
14562
- reason: "under_threshold"
14548
+ reason: "queued_for_slot"
14563
14549
  };
14564
14550
  }
14551
+ function relaxedThrottleConfig(cfg, env = process.env, envVar = "TELEGRAM_PRIVATE_REPLY_THROTTLE_COUNT") {
14552
+ const raw = Number(env[envVar]);
14553
+ const threshold = Number.isFinite(raw) && raw > 0 ? raw : cfg.threshold * 2;
14554
+ return { ...cfg, threshold, ringSize: Math.max(cfg.ringSize, threshold + 2) };
14555
+ }
14565
14556
  var buffers = /* @__PURE__ */ new Map();
14566
14557
  function key(channelId, threadKey) {
14567
14558
  return `${channelId}${threadKey}`;
@@ -15163,8 +15154,316 @@ function createCrossTeamPeerAuditClient(args) {
15163
15154
  };
15164
15155
  }
15165
15156
 
15166
- // src/observed-chat-client.ts
15157
+ // src/diagnostic-event-client.ts
15167
15158
  var REQUEST_TIMEOUT_MS2 = 1e4;
15159
+ function createDiagnosticEventClient(args) {
15160
+ if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15161
+ const fetchImpl = args.fetchImpl ?? fetch;
15162
+ const log = args.log ?? (() => {
15163
+ });
15164
+ const base = args.agtHost.replace(/\/+$/, "");
15165
+ const agentId = args.agentId;
15166
+ const apiKey = args.agtApiKey;
15167
+ let cachedToken = null;
15168
+ let cachedTokenExpiresAt = 0;
15169
+ async function getToken() {
15170
+ if (cachedToken && Date.now() < cachedTokenExpiresAt) return cachedToken;
15171
+ const resp = await fetchImpl(`${base}/host/exchange`, {
15172
+ method: "POST",
15173
+ headers: { "Content-Type": "application/json" },
15174
+ body: JSON.stringify({ host_key: apiKey }),
15175
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15176
+ });
15177
+ if (!resp.ok) {
15178
+ const body = await resp.text().catch(() => "");
15179
+ throw new Error(`/host/exchange failed (${resp.status}): ${body.slice(0, 200)}`);
15180
+ }
15181
+ const data = await resp.json();
15182
+ cachedToken = data.token;
15183
+ cachedTokenExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() - 12e4 : Date.now() + 55 * 6e4;
15184
+ return cachedToken;
15185
+ }
15186
+ async function postOnce(event) {
15187
+ const token = await getToken();
15188
+ return fetchImpl(`${base}/host/diagnostic-command-event`, {
15189
+ method: "POST",
15190
+ headers: {
15191
+ Authorization: `Bearer ${token}`,
15192
+ "Content-Type": "application/json"
15193
+ },
15194
+ body: JSON.stringify({ agent_id: agentId, ...event }),
15195
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15196
+ });
15197
+ }
15198
+ async function emitAsync(event) {
15199
+ try {
15200
+ let resp = await postOnce(event);
15201
+ if (resp.status === 401) {
15202
+ cachedToken = null;
15203
+ cachedTokenExpiresAt = 0;
15204
+ resp = await postOnce(event);
15205
+ }
15206
+ if (!resp.ok) {
15207
+ const body = await resp.text().catch(() => "");
15208
+ log(
15209
+ `diagnostic-event: POST failed (${resp.status}) outcome=${event.outcome}: ${body.slice(0, 200)}`
15210
+ );
15211
+ }
15212
+ } catch (err) {
15213
+ log(`diagnostic-event: emit threw outcome=${event.outcome}: ${err.message}`);
15214
+ }
15215
+ }
15216
+ return {
15217
+ emit(event) {
15218
+ void emitAsync(event);
15219
+ }
15220
+ };
15221
+ }
15222
+
15223
+ // src/telegram-investigate.ts
15224
+ var INVESTIGATE_SYNTAX_RE = /^\/investigate(?:[-_]([A-Za-z0-9_-]{1,64}))?(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
15225
+ function isInvestigateSyntax(text) {
15226
+ return INVESTIGATE_SYNTAX_RE.test(text);
15227
+ }
15228
+ function classifyInvestigateText(text, opts) {
15229
+ const m = INVESTIGATE_SYNTAX_RE.exec(text);
15230
+ if (!m) return "ignore";
15231
+ const codeSuffix = m[1];
15232
+ if (codeSuffix) {
15233
+ const normalise = (s) => s.toLowerCase().replace(/_/g, "-");
15234
+ if (normalise(codeSuffix) !== normalise(opts.codeName)) return "ignore";
15235
+ }
15236
+ const botSuffix = m[2]?.toLowerCase();
15237
+ if (!botSuffix) return "act";
15238
+ if (!opts.botUsername) return "verification_failed";
15239
+ return botSuffix === opts.botUsername.toLowerCase() ? "act" : "ignore";
15240
+ }
15241
+ function evaluateInvestigateGate(opts) {
15242
+ if (opts.chatType !== "private") return { ok: false, reason: "not-dm" };
15243
+ if (opts.diagnosticChatIds.size === 0) return { ok: false, reason: "allowlist-empty" };
15244
+ if (!opts.senderId || !opts.diagnosticChatIds.has(opts.senderId)) {
15245
+ return { ok: false, reason: "not-allowed" };
15246
+ }
15247
+ return { ok: true };
15248
+ }
15249
+ function createSingleTailSlot() {
15250
+ let expiresAtMs = null;
15251
+ return {
15252
+ reserve(windowMs, nowMs = Date.now()) {
15253
+ if (expiresAtMs !== null && expiresAtMs > nowMs) return null;
15254
+ expiresAtMs = nowMs + windowMs;
15255
+ return expiresAtMs;
15256
+ },
15257
+ remainingMs(nowMs = Date.now()) {
15258
+ if (expiresAtMs === null) return 0;
15259
+ return Math.max(0, expiresAtMs - nowMs);
15260
+ },
15261
+ release() {
15262
+ expiresAtMs = null;
15263
+ }
15264
+ };
15265
+ }
15266
+
15267
+ // src/telegram-command-registry.ts
15268
+ var HELP = {
15269
+ command: "help",
15270
+ description: "Show available commands"
15271
+ };
15272
+ var RESTART = {
15273
+ command: "restart",
15274
+ description: "Restart this agent"
15275
+ };
15276
+ var INVESTIGATE = {
15277
+ command: "investigate",
15278
+ description: "Live tail of this agent's terminal pane (operators only)"
15279
+ };
15280
+ function buildCommandRegistrations() {
15281
+ return [
15282
+ { scope: { type: "default" }, commands: [HELP, RESTART] },
15283
+ { scope: { type: "all_private_chats" }, commands: [HELP, RESTART, INVESTIGATE] }
15284
+ ];
15285
+ }
15286
+
15287
+ // src/pane-tail.ts
15288
+ import { execFile } from "child_process";
15289
+ import { promisify } from "util";
15290
+ import { open, stat } from "fs/promises";
15291
+
15292
+ // src/session-probe-runtime.ts
15293
+ import { execFileSync } from "child_process";
15294
+ function agentTmuxSessionName(codeName) {
15295
+ return `agt-${codeName}`;
15296
+ }
15297
+ function escapePgrepRegex(value) {
15298
+ return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
15299
+ }
15300
+ function probeClaudeProcessInTmux(tmuxSession) {
15301
+ const escapedSession = escapePgrepRegex(tmuxSession);
15302
+ const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
15303
+ try {
15304
+ const out = execFileSync("pgrep", ["-f", "--", pattern], {
15305
+ encoding: "utf-8",
15306
+ timeout: 3e3
15307
+ }).trim();
15308
+ return out.length > 0 ? "alive" : "dead";
15309
+ } catch (err) {
15310
+ const e = err;
15311
+ if (e?.code === "ENOENT") return "unknown";
15312
+ return e?.status === 1 ? "dead" : "unknown";
15313
+ }
15314
+ }
15315
+ function probeTmuxSession(tmuxSession) {
15316
+ try {
15317
+ execFileSync("tmux", ["has-session", "-t", tmuxSession], {
15318
+ stdio: "ignore",
15319
+ timeout: 3e3
15320
+ });
15321
+ return "alive";
15322
+ } catch (err) {
15323
+ const e = err;
15324
+ if (e?.code === "ENOENT") return "unknown";
15325
+ return "dead";
15326
+ }
15327
+ }
15328
+ function probeAgentSession(codeName) {
15329
+ const session = agentTmuxSessionName(codeName);
15330
+ const tmux = probeTmuxSession(session);
15331
+ const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
15332
+ return { tmux, claude };
15333
+ }
15334
+ var probeCache = /* @__PURE__ */ new Map();
15335
+ var SESSION_PROBE_TTL_MS = 15e3;
15336
+ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
15337
+ const cached2 = probeCache.get(codeName);
15338
+ if (cached2 && now - cached2.at < ttlMs) return cached2.value;
15339
+ const value = probeAgentSession(codeName);
15340
+ probeCache.set(codeName, { at: now, value });
15341
+ return value;
15342
+ }
15343
+
15344
+ // src/pane-tail.ts
15345
+ var execFileAsync = promisify(execFile);
15346
+ var PANE_LOG_TAIL_BYTES = 64 * 1024;
15347
+ var TMUX_CAPTURE_TIMEOUT_MS = 2e3;
15348
+ async function capturePaneSnapshot(opts) {
15349
+ const scrollback = opts.scrollbackLines ?? 200;
15350
+ try {
15351
+ const { stdout } = await execFileAsync(
15352
+ "tmux",
15353
+ ["capture-pane", "-p", "-t", agentTmuxSessionName(opts.codeName), "-S", `-${scrollback}`],
15354
+ { timeout: TMUX_CAPTURE_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
15355
+ );
15356
+ return { source: "tmux", text: stdout };
15357
+ } catch {
15358
+ }
15359
+ if (!opts.agentDir) return null;
15360
+ const paneLogPath = `${opts.agentDir}/pane.log`;
15361
+ try {
15362
+ const tail = await readFileTail(paneLogPath, PANE_LOG_TAIL_BYTES);
15363
+ if (tail === null) return null;
15364
+ return { source: "pane-log", text: stripAnsi(tail) };
15365
+ } catch {
15366
+ return null;
15367
+ }
15368
+ }
15369
+ async function readFileTail(path, maxBytes) {
15370
+ const st = await stat(path);
15371
+ if (!st.isFile()) return null;
15372
+ const start = Math.max(0, st.size - maxBytes);
15373
+ const length = st.size - start;
15374
+ if (length === 0) return "";
15375
+ const handle = await open(path, "r");
15376
+ try {
15377
+ const buf = Buffer.alloc(length);
15378
+ await handle.read(buf, 0, length, start);
15379
+ let text = buf.toString("utf8");
15380
+ if (start > 0) {
15381
+ const nl = text.indexOf("\n");
15382
+ if (nl !== -1) text = text.slice(nl + 1);
15383
+ }
15384
+ return text;
15385
+ } finally {
15386
+ await handle.close();
15387
+ }
15388
+ }
15389
+ function stripAnsi(text) {
15390
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?<=>!]*[ -\/]*[@-~]/g, "").replace(/\x1b./g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
15391
+ }
15392
+ var SECRET_PATTERNS = [
15393
+ // Augmented host API keys
15394
+ { re: /tlk_[A-Za-z0-9]{8,}/g, label: "agt-api-key" },
15395
+ // Slack tokens: bot/app/user/refresh/config
15396
+ { re: /x(?:ox[abprs]|app)-[A-Za-z0-9-]{8,}/g, label: "slack-token" },
15397
+ // JWTs (three base64url segments, first one always starts with eyJ)
15398
+ { re: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, label: "jwt" },
15399
+ // OpenAI / Anthropic-style keys (sk-..., sk-ant-...)
15400
+ { re: /sk-[A-Za-z0-9_-]{16,}/g, label: "sk-key" },
15401
+ // AWS access key IDs
15402
+ { re: /(?:AKIA|ASIA)[0-9A-Z]{16}/g, label: "aws-key" },
15403
+ // GitHub tokens
15404
+ { re: /gh[pousr]_[A-Za-z0-9]{20,}/g, label: "github-token" }
15405
+ ];
15406
+ function redactPaneText(text) {
15407
+ let out = redactAugmentedPaths(text);
15408
+ for (const { re, label } of SECRET_PATTERNS) {
15409
+ out = out.replace(re, `<redacted:${label}>`);
15410
+ }
15411
+ return out;
15412
+ }
15413
+ var DEFAULT_MAX_CHARS = 3900;
15414
+ var REFLOW_COLS = 80;
15415
+ function formatPaneSnapshot(opts) {
15416
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
15417
+ const commandName = opts.commandName ?? "/debug";
15418
+ const header = opts.status.kind === "live" ? `\u{1F50E} Live pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 updates ~3s \xB7 expires in ${formatCountdown(opts.status.secondsRemaining)}` : `\u{1F50E} Pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 *expired* \u2014 run \`${commandName}\` to restart`;
15419
+ const fenceOverhead = header.length + "\n```\n".length + "\n```".length + 16;
15420
+ const contentBudget = Math.max(0, maxChars - fenceOverhead);
15421
+ const lines = reflowLines(sanitizeForCodeBlock(opts.text), REFLOW_COLS);
15422
+ const kept = [];
15423
+ let used = 0;
15424
+ for (let i = lines.length - 1; i >= 0; i--) {
15425
+ const cost = lines[i].length + 1;
15426
+ if (used + cost > contentBudget) break;
15427
+ kept.unshift(lines[i]);
15428
+ used += cost;
15429
+ }
15430
+ const truncated = kept.length < lines.length;
15431
+ const body = kept.join("\n").trimEnd();
15432
+ const block = body.length > 0 ? `\`\`\`
15433
+ ${truncated ? "\u2026\n" : ""}${body}
15434
+ \`\`\`` : "_(pane is empty)_";
15435
+ return `${header}
15436
+ ${block}`;
15437
+ }
15438
+ function formatCountdown(totalSeconds) {
15439
+ const s = Math.max(0, Math.floor(totalSeconds));
15440
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
15441
+ }
15442
+ function sanitizeForCodeBlock(text) {
15443
+ return text.replace(/```/g, "`\u200B`\u200B`");
15444
+ }
15445
+ function reflowLines(text, cols) {
15446
+ const out = [];
15447
+ for (const rawLine of text.split("\n")) {
15448
+ const line = rawLine.replace(/\s+$/, "");
15449
+ if (line.length <= cols) {
15450
+ out.push(line);
15451
+ continue;
15452
+ }
15453
+ for (let i = 0; i < line.length; i += cols) {
15454
+ out.push(line.slice(i, i + cols));
15455
+ }
15456
+ }
15457
+ const collapsed = [];
15458
+ for (const line of out) {
15459
+ if (line === "" && collapsed[collapsed.length - 1] === "") continue;
15460
+ collapsed.push(line);
15461
+ }
15462
+ return collapsed;
15463
+ }
15464
+
15465
+ // src/observed-chat-client.ts
15466
+ var REQUEST_TIMEOUT_MS3 = 1e4;
15168
15467
  function createObservedChatClient(args) {
15169
15468
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15170
15469
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15182,7 +15481,7 @@ function createObservedChatClient(args) {
15182
15481
  method: "POST",
15183
15482
  headers: { "Content-Type": "application/json" },
15184
15483
  body: JSON.stringify({ host_key: apiKey }),
15185
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15484
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15186
15485
  });
15187
15486
  if (!resp.ok) {
15188
15487
  const body = await resp.text().catch(() => "");
@@ -15199,7 +15498,7 @@ function createObservedChatClient(args) {
15199
15498
  method: "POST",
15200
15499
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15201
15500
  body: JSON.stringify({ agent_id: agentId, ...chat }),
15202
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15501
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15203
15502
  });
15204
15503
  }
15205
15504
  async function emitAsync(chat) {
@@ -15246,7 +15545,7 @@ function createObservedChatClient(args) {
15246
15545
  }
15247
15546
 
15248
15547
  // src/conversation-ingest-client.ts
15249
- var REQUEST_TIMEOUT_MS3 = 1e4;
15548
+ var REQUEST_TIMEOUT_MS4 = 1e4;
15250
15549
  function createConversationIngestClient(args) {
15251
15550
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15252
15551
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15263,7 +15562,7 @@ function createConversationIngestClient(args) {
15263
15562
  method: "POST",
15264
15563
  headers: { "Content-Type": "application/json" },
15265
15564
  body: JSON.stringify({ host_key: apiKey }),
15266
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15565
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15267
15566
  });
15268
15567
  if (!resp.ok) {
15269
15568
  const body = await resp.text().catch(() => "");
@@ -15286,7 +15585,7 @@ function createConversationIngestClient(args) {
15286
15585
  method: "POST",
15287
15586
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15288
15587
  body: JSON.stringify({ agent_id: agentId, ...payload }),
15289
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15588
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15290
15589
  });
15291
15590
  }
15292
15591
  function refKey(payload) {
@@ -15318,7 +15617,7 @@ function createConversationIngestClient(args) {
15318
15617
  }
15319
15618
 
15320
15619
  // src/inbound-context-client.ts
15321
- var REQUEST_TIMEOUT_MS4 = 1e4;
15620
+ var REQUEST_TIMEOUT_MS5 = 1e4;
15322
15621
  function createInboundContextClient(args) {
15323
15622
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15324
15623
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15335,7 +15634,7 @@ function createInboundContextClient(args) {
15335
15634
  method: "POST",
15336
15635
  headers: { "Content-Type": "application/json" },
15337
15636
  body: JSON.stringify({ host_key: apiKey }),
15338
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15637
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5)
15339
15638
  });
15340
15639
  if (!resp.ok) {
15341
15640
  const body = await resp.text().catch(() => "");
@@ -15352,7 +15651,7 @@ function createInboundContextClient(args) {
15352
15651
  method: "POST",
15353
15652
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15354
15653
  body: JSON.stringify(body),
15355
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15654
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5)
15356
15655
  });
15357
15656
  }
15358
15657
  async function emit(args2) {
@@ -15386,465 +15685,87 @@ function createInboundContextClient(args) {
15386
15685
  };
15387
15686
  }
15388
15687
 
15389
- // src/progress-tools.ts
15390
- import { randomUUID } from "crypto";
15391
-
15392
- // src/progress-handle.ts
15393
- async function startProgressHandle(opts) {
15394
- const now = opts.now ?? (() => Date.now());
15395
- const setTimer = opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
15396
- const clearTimer = opts.clearTimer ?? ((id) => clearTimeout(id));
15397
- const debounceMs = opts.debounceMs ?? 1e3;
15398
- const onPostTerminalUpdate = opts.onPostTerminalUpdate ?? defaultPostTerminalWarn;
15399
- const state = {
15400
- initialLabel: opts.initialLabel,
15401
- mode: opts.initialMode ?? "working",
15402
- lastChangeAt: now()
15403
- };
15404
- let lastFlushAt = 0;
15405
- let pendingTimer = null;
15406
- let inFlight = null;
15407
- let terminal = false;
15408
- async function doFlush() {
15409
- while (inFlight) await inFlight;
15410
- if (pendingTimer != null) {
15411
- clearTimer(pendingTimer);
15412
- pendingTimer = null;
15413
- }
15414
- lastFlushAt = now();
15415
- const promise = opts.flush(snapshotState());
15416
- inFlight = promise.then(
15417
- () => {
15418
- inFlight = null;
15419
- },
15420
- (err) => {
15421
- inFlight = null;
15422
- throw err;
15423
- }
15424
- );
15425
- await inFlight;
15426
- }
15427
- function snapshotState() {
15428
- return {
15429
- initialLabel: state.initialLabel,
15430
- mode: state.mode,
15431
- stepLabel: state.stepLabel,
15432
- stepNumber: state.stepNumber ? { ...state.stepNumber } : void 0,
15433
- detail: state.detail,
15434
- lastChangeAt: state.lastChangeAt,
15435
- terminal: state.terminal ? { ...state.terminal } : void 0
15436
- };
15437
- }
15438
- function applyUpdate(next) {
15439
- if (next.mode !== void 0) state.mode = next.mode;
15440
- if (next.stepLabel !== void 0) state.stepLabel = next.stepLabel;
15441
- if (next.stepNumber !== void 0) state.stepNumber = { ...next.stepNumber };
15442
- if (next.detail !== void 0) state.detail = next.detail;
15443
- state.lastChangeAt = now();
15444
- }
15445
- async function scheduleOrFlush() {
15446
- const elapsed = now() - lastFlushAt;
15447
- if (elapsed >= debounceMs) {
15448
- await doFlush();
15449
- return;
15450
- }
15451
- if (pendingTimer != null) return;
15452
- const remaining = debounceMs - elapsed;
15453
- pendingTimer = setTimer(() => {
15454
- pendingTimer = null;
15455
- void doFlush().catch(() => {
15456
- });
15457
- }, remaining);
15458
- }
15459
- lastFlushAt = now();
15460
- await opts.flush(snapshotState());
15688
+ // src/impersonation.ts
15689
+ var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
15690
+ var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
15691
+ function isImpersonating() {
15692
+ return impersonatedAgentId() !== null;
15693
+ }
15694
+ function impersonatedAgentId() {
15695
+ const raw = process.env[ENV_VAR];
15696
+ if (typeof raw !== "string") return null;
15697
+ const trimmed = raw.trim();
15698
+ return trimmed.length > 0 ? trimmed : null;
15699
+ }
15700
+ function channelsEnabledOverride() {
15701
+ const raw = process.env[OVERRIDE_ENV_VAR];
15702
+ if (typeof raw !== "string") return false;
15703
+ const trimmed = raw.trim().toLowerCase();
15704
+ return trimmed === "1" || trimmed === "true" || trimmed === "yes" || trimmed === "on";
15705
+ }
15706
+ var IMPERSONATION_REFUSAL_CODE = "CHANNEL.IMPERSONATION_DISABLED";
15707
+ var IMPERSONATION_REFUSAL_MESSAGE = "Posting as an agent under impersonation is disabled in v0 \u2014 quit impersonation with `agt impersonate exit` to send as yourself.";
15708
+ function buildImpersonationRefusal(toolName) {
15461
15709
  return {
15462
- snapshot: snapshotState,
15463
- async update(next) {
15464
- if (terminal) {
15465
- const peeked = snapshotState();
15466
- Object.assign(peeked, {
15467
- mode: next.mode ?? peeked.mode,
15468
- stepLabel: next.stepLabel ?? peeked.stepLabel,
15469
- stepNumber: next.stepNumber ?? peeked.stepNumber,
15470
- detail: next.detail ?? peeked.detail
15471
- });
15472
- try {
15473
- onPostTerminalUpdate(peeked);
15474
- } catch (err) {
15475
- console.warn(
15476
- "[progress-handle] onPostTerminalUpdate threw \u2014 swallowed to keep update() non-throwing:",
15477
- err
15478
- );
15479
- }
15480
- return;
15710
+ content: [
15711
+ {
15712
+ type: "text",
15713
+ text: JSON.stringify({
15714
+ error: {
15715
+ code: IMPERSONATION_REFUSAL_CODE,
15716
+ message: IMPERSONATION_REFUSAL_MESSAGE,
15717
+ tool: toolName
15718
+ }
15719
+ })
15481
15720
  }
15482
- applyUpdate(next);
15483
- await scheduleOrFlush();
15484
- },
15485
- async complete(summary) {
15486
- if (terminal) return;
15487
- terminal = true;
15488
- state.terminal = { kind: "completed", message: summary };
15489
- state.lastChangeAt = now();
15490
- await doFlush();
15491
- },
15492
- async fail(reason) {
15493
- if (terminal) return;
15494
- terminal = true;
15495
- state.terminal = { kind: "failed", message: reason };
15496
- state.lastChangeAt = now();
15497
- await doFlush();
15498
- },
15499
- isTerminal: () => terminal
15721
+ ],
15722
+ isError: true
15500
15723
  };
15501
15724
  }
15502
- function defaultPostTerminalWarn(state) {
15503
- console.warn(
15504
- `[progress-handle] update() called after terminal transition (${state.terminal?.kind}) \u2014 ignored.`
15505
- );
15506
- }
15507
15725
 
15508
- // src/progress-tools.ts
15509
- var ProgressToolRegistry = class {
15510
- constructor(opts) {
15511
- this.opts = opts;
15512
- this.toolNames = {
15513
- start: `${opts.prefix}_progress_start`,
15514
- update: `${opts.prefix}_progress_update`,
15515
- complete: `${opts.prefix}_progress_complete`,
15516
- fail: `${opts.prefix}_progress_fail`
15517
- };
15518
- }
15519
- handles = /* @__PURE__ */ new Map();
15520
- toolNames;
15521
- /** Tool definitions to splice into the MCP ListToolsRequest response. */
15522
- getDefinitions() {
15523
- const { surfaceDescription, startArgsSchema } = this.opts;
15524
- const debounceMs = this.opts.debounceMs ?? 1e3;
15525
- const debounceText = debounceMs === 1e3 ? "per second" : `every ${debounceMs}ms`;
15526
- return [
15527
- {
15528
- name: this.toolNames.start,
15529
- description: `Post a "still working" anchor to ${surfaceDescription} and return a progress_id. The anchor message updates in place as you call ${this.toolNames.update} \u2014 operators see one message that ticks forward instead of a flood of new ones. Always end with ${this.toolNames.complete} or ${this.toolNames.fail}; otherwise the anchor lingers as "working" until the stale-state sweep flips it. Opt-in \u2014 only call this for tasks that will take more than a few seconds.`,
15530
- inputSchema: {
15531
- type: "object",
15532
- properties: {
15533
- label: {
15534
- type: "string",
15535
- description: 'Short top-line label for the task (e.g. "diagnose vigil"). Stays constant for the lifetime of this progress anchor.'
15536
- },
15537
- initial_mode: {
15538
- type: "string",
15539
- enum: ["thinking", "working", "waiting"],
15540
- description: 'Initial mode emoji \u2014 defaults to "working".'
15541
- },
15542
- ...startArgsSchema.properties
15543
- },
15544
- required: ["label", ...startArgsSchema.required]
15545
- }
15546
- },
15547
- {
15548
- name: this.toolNames.update,
15549
- description: `Update the progress anchor in place. The state machine debounces calls to one ${surfaceDescription} API call ${debounceText}, so spamming this every step is safe \u2014 only the latest pending state will paint. Any subset of fields can be passed; omitted ones retain their previous value.`,
15550
- inputSchema: {
15551
- type: "object",
15552
- properties: {
15553
- progress_id: { type: "string", description: "The progress_id returned from start." },
15554
- step_label: { type: "string", description: 'Short label for the current step (e.g. "Querying CloudWatch logs").' },
15555
- step_current: { type: "number", description: "1-based step counter \u2014 current." },
15556
- step_total: { type: "number", description: "Total steps if known. Omit for open-ended progress." },
15557
- mode: { type: "string", enum: ["thinking", "working", "waiting"] },
15558
- detail: { type: "string", description: "Free-text detail line under the step label." }
15559
- },
15560
- required: ["progress_id"]
15561
- }
15562
- },
15563
- {
15564
- name: this.toolNames.complete,
15565
- description: `Mark the progress anchor as \u2705 completed. Flushes any pending debounced state before painting the terminal banner \u2014 the operator never sees a stale "Step N" beside the \u2705. After this, the handle is dead; further updates are no-ops.`,
15566
- inputSchema: {
15567
- type: "object",
15568
- properties: {
15569
- progress_id: { type: "string" },
15570
- summary: { type: "string", description: "Optional one-line summary of what was achieved." }
15571
- },
15572
- required: ["progress_id"]
15573
- }
15574
- },
15575
- {
15576
- name: this.toolNames.fail,
15577
- description: `Mark the progress anchor as \u274C failed. Always include a one-sentence reason \u2014 the operator can't tell *why* from emoji alone. Same flush-then-paint behaviour as complete.`,
15578
- inputSchema: {
15579
- type: "object",
15580
- properties: {
15581
- progress_id: { type: "string" },
15582
- reason: { type: "string", description: "One-sentence failure reason." }
15583
- },
15584
- required: ["progress_id", "reason"]
15585
- }
15586
- }
15587
- ];
15588
- }
15589
- /**
15590
- * Dispatch a CallToolRequest. Returns `undefined` if the tool name
15591
- * isn't one of ours (so the caller can keep walking its dispatch
15592
- * chain). Returns a tool result otherwise.
15593
- */
15594
- async handle(name, args) {
15595
- if (name === this.toolNames.start) return this.handleStart(args);
15596
- if (name === this.toolNames.update) return this.handleUpdate(args);
15597
- if (name === this.toolNames.complete) return this.handleComplete(args);
15598
- if (name === this.toolNames.fail) return this.handleFail(args);
15599
- return void 0;
15600
- }
15601
- /** Live count of in-flight handles. Exposed for tests + diagnostics. */
15602
- size() {
15603
- return this.handles.size;
15604
- }
15605
- async handleStart(args) {
15606
- const label = typeof args.label === "string" ? args.label : null;
15607
- if (!label) return errResult(`${this.toolNames.start}: 'label' must be a non-empty string`);
15608
- if ("initial_mode" in args && !isMode(args.initial_mode)) {
15609
- return errResult(
15610
- `${this.toolNames.start}: 'initial_mode' must be one of 'thinking', 'working', or 'waiting'`
15611
- );
15612
- }
15613
- for (const k of this.opts.startArgsSchema.required) {
15614
- if (typeof args[k] !== "string" || args[k].length === 0) {
15615
- return errResult(`${this.toolNames.start}: '${k}' must be a non-empty string`);
15616
- }
15617
- }
15618
- const initialMode = isMode(args.initial_mode) ? args.initial_mode : void 0;
15619
- const progressId = randomUUID();
15620
- try {
15621
- const flush = this.opts.createFlush(args);
15622
- const handle = await startProgressHandle({
15623
- initialLabel: label,
15624
- initialMode,
15625
- flush,
15626
- debounceMs: this.opts.debounceMs ?? 1e3
15627
- });
15628
- this.handles.set(progressId, handle);
15629
- return okResult(JSON.stringify({ progress_id: progressId }));
15630
- } catch (err) {
15631
- return errResult(`${this.toolNames.start} failed: ${err.message ?? String(err)}`);
15632
- }
15633
- }
15634
- async handleUpdate(args) {
15635
- const id = typeof args.progress_id === "string" ? args.progress_id : null;
15636
- if (!id) return errResult(`${this.toolNames.update}: 'progress_id' must be a string`);
15637
- const handle = this.handles.get(id);
15638
- if (!handle) return errResult(`${this.toolNames.update}: unknown progress_id (already terminated, or never started)`);
15639
- if ("mode" in args && !isMode(args.mode)) {
15640
- return errResult(
15641
- `${this.toolNames.update}: 'mode' must be one of 'thinking', 'working', or 'waiting'`
15642
- );
15643
- }
15644
- if (typeof args.step_total === "number" && typeof args.step_current !== "number") {
15645
- return errResult(
15646
- `${this.toolNames.update}: 'step_total' requires 'step_current'`
15647
- );
15648
- }
15649
- const next = {};
15650
- if (typeof args.step_label === "string") next.stepLabel = args.step_label;
15651
- if (typeof args.step_current === "number") {
15652
- next.stepNumber = {
15653
- current: args.step_current,
15654
- ...typeof args.step_total === "number" ? { total: args.step_total } : {}
15655
- };
15656
- }
15657
- if (isMode(args.mode)) next.mode = args.mode;
15658
- if (typeof args.detail === "string") next.detail = args.detail;
15659
- try {
15660
- await handle.update(next);
15661
- return okResult("ok");
15662
- } catch (err) {
15663
- return errResult(`${this.toolNames.update} failed: ${err.message ?? String(err)}`);
15664
- }
15726
+ // src/channel-egress-tools.ts
15727
+ var TELEGRAM_EGRESS_TOOLS = /* @__PURE__ */ new Set([
15728
+ "telegram.reply",
15729
+ "telegram.send_message",
15730
+ "telegram.react",
15731
+ "channel_request_input"
15732
+ ]);
15733
+
15734
+ // src/mcp-spawn-lock.ts
15735
+ import {
15736
+ existsSync,
15737
+ mkdirSync as mkdirSync2,
15738
+ readFileSync,
15739
+ renameSync as renameSync2,
15740
+ unlinkSync as unlinkSync2,
15741
+ writeFileSync as writeFileSync2
15742
+ } from "fs";
15743
+ import { join as join2 } from "path";
15744
+ function defaultIsPidAlive(pid) {
15745
+ if (!Number.isFinite(pid) || pid <= 0) return false;
15746
+ try {
15747
+ process.kill(pid, 0);
15748
+ return true;
15749
+ } catch (err) {
15750
+ const code = err.code;
15751
+ if (code === "ESRCH") return false;
15752
+ return true;
15665
15753
  }
15666
- async handleComplete(args) {
15667
- const id = typeof args.progress_id === "string" ? args.progress_id : null;
15668
- if (!id) return errResult(`${this.toolNames.complete}: 'progress_id' must be a string`);
15669
- const handle = this.handles.get(id);
15670
- if (!handle) return errResult(`${this.toolNames.complete}: unknown progress_id`);
15671
- const summary = typeof args.summary === "string" ? args.summary : void 0;
15672
- try {
15673
- await handle.complete(summary);
15674
- this.handles.delete(id);
15675
- return okResult("ok");
15676
- } catch (err) {
15677
- return errResult(`${this.toolNames.complete} failed: ${err.message ?? String(err)}`);
15754
+ }
15755
+ function acquireMcpSpawnLock(args) {
15756
+ const { agentDir, basename, options = {} } = args;
15757
+ if (!agentDir) return { kind: "no-agent-dir" };
15758
+ const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15759
+ const selfPid = options.selfPid ?? process.pid;
15760
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15761
+ const path = join2(agentDir, basename);
15762
+ const existing = readLockHolder(path);
15763
+ if (existing) {
15764
+ if (existing.pid === selfPid) {
15765
+ return { kind: "acquired", path };
15678
15766
  }
15679
- }
15680
- async handleFail(args) {
15681
- const id = typeof args.progress_id === "string" ? args.progress_id : null;
15682
- if (!id) return errResult(`${this.toolNames.fail}: 'progress_id' must be a string`);
15683
- const reason = typeof args.reason === "string" ? args.reason : null;
15684
- if (!reason) return errResult(`${this.toolNames.fail}: 'reason' must be a non-empty string`);
15685
- const handle = this.handles.get(id);
15686
- if (!handle) return errResult(`${this.toolNames.fail}: unknown progress_id`);
15687
- try {
15688
- await handle.fail(reason);
15689
- this.handles.delete(id);
15690
- return okResult("ok");
15691
- } catch (err) {
15692
- return errResult(`${this.toolNames.fail} failed: ${err.message ?? String(err)}`);
15693
- }
15694
- }
15695
- };
15696
- function okResult(text) {
15697
- return { content: [{ type: "text", text }] };
15698
- }
15699
- function errResult(text) {
15700
- return { content: [{ type: "text", text }], isError: true };
15701
- }
15702
- function isMode(value) {
15703
- return value === "thinking" || value === "working" || value === "waiting";
15704
- }
15705
-
15706
- // src/telegram-progress.ts
15707
- var MODE_EMOJI = {
15708
- thinking: "\u{1F4AD}",
15709
- working: "\u{1F6E0}\uFE0F",
15710
- waiting: "\u23F3"
15711
- };
15712
- var TERMINAL_EMOJI = {
15713
- completed: "\u2705",
15714
- failed: "\u274C"
15715
- };
15716
- function formatWallClock(ms, timeZone) {
15717
- const fmt = new Intl.DateTimeFormat("en-GB", {
15718
- hour: "2-digit",
15719
- minute: "2-digit",
15720
- second: "2-digit",
15721
- hour12: false,
15722
- timeZone,
15723
- timeZoneName: "short"
15724
- });
15725
- const parts = fmt.formatToParts(new Date(ms));
15726
- const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
15727
- return `${get("hour")}:${get("minute")}:${get("second")} ${get("timeZoneName")}`;
15728
- }
15729
- function escapeHtml(s) {
15730
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15731
- }
15732
- function renderProgressHtml(state) {
15733
- const lines = [];
15734
- if (state.terminal) {
15735
- const banner = state.terminal.kind === "completed" ? "Done" : "Failed";
15736
- lines.push(`${TERMINAL_EMOJI[state.terminal.kind]} <b>${banner}</b> \u2014 ${escapeHtml(state.initialLabel)}`);
15737
- if (state.terminal.message) lines.push(escapeHtml(state.terminal.message));
15738
- const verb = state.terminal.kind === "completed" ? "completed" : "failed";
15739
- lines.push(`<i>${verb}: ${formatWallClock(state.lastChangeAt)}</i>`);
15740
- return lines.join("\n");
15741
- }
15742
- lines.push(`${MODE_EMOJI[state.mode]} <b>Working on:</b> ${escapeHtml(state.initialLabel)}`);
15743
- if (state.stepNumber || state.stepLabel) {
15744
- const parts = [];
15745
- if (state.stepNumber) {
15746
- if (state.stepNumber.total != null) {
15747
- parts.push(`<b>Step ${state.stepNumber.current} of ${state.stepNumber.total}</b>`);
15748
- } else {
15749
- parts.push(`<b>Step ${state.stepNumber.current}</b>`);
15750
- }
15751
- }
15752
- if (state.stepLabel) parts.push(escapeHtml(state.stepLabel));
15753
- lines.push(parts.join(": "));
15754
- }
15755
- if (state.detail) lines.push(`<i>${escapeHtml(state.detail)}</i>`);
15756
- lines.push(`<i>last update: ${formatWallClock(state.lastChangeAt)}</i>`);
15757
- return lines.join("\n");
15758
- }
15759
- var TelegramApiError = class extends Error {
15760
- constructor(endpoint, apiError) {
15761
- super(`Telegram ${endpoint} failed: ${apiError}`);
15762
- this.endpoint = endpoint;
15763
- this.apiError = apiError;
15764
- this.name = "TelegramApiError";
15765
- }
15766
- };
15767
- function isMessageNotModified(description) {
15768
- if (!description) return false;
15769
- return /message is not modified/i.test(description);
15770
- }
15771
- function createTelegramProgressFlush(opts) {
15772
- let anchorMessageId = null;
15773
- return async function flush(state) {
15774
- const text = renderProgressHtml(state);
15775
- if (anchorMessageId == null) {
15776
- const body = {
15777
- chat_id: opts.chatId,
15778
- text,
15779
- parse_mode: "HTML",
15780
- disable_notification: true
15781
- };
15782
- if (opts.messageThreadId != null) body.message_thread_id = opts.messageThreadId;
15783
- if (opts.replyToMessageId != null) body.reply_to_message_id = opts.replyToMessageId;
15784
- const res = await opts.callImpl("sendMessage", body);
15785
- if (!res.ok || !res.result?.message_id) {
15786
- throw new TelegramApiError("sendMessage", res.description ?? "missing message_id");
15787
- }
15788
- anchorMessageId = res.result.message_id;
15789
- opts.onAnchorPosted?.(res.result.message_id);
15790
- return;
15791
- }
15792
- try {
15793
- const res = await opts.callImpl("editMessageText", {
15794
- chat_id: opts.chatId,
15795
- message_id: anchorMessageId,
15796
- text,
15797
- parse_mode: "HTML"
15798
- });
15799
- if (!res.ok) {
15800
- if (isMessageNotModified(res.description)) return;
15801
- throw new TelegramApiError("editMessageText", res.description ?? "unknown");
15802
- }
15803
- } catch (err) {
15804
- if (opts.onApiError && err instanceof TelegramApiError) {
15805
- await opts.onApiError(err);
15806
- return;
15807
- }
15808
- throw err;
15809
- }
15810
- };
15811
- }
15812
-
15813
- // src/mcp-spawn-lock.ts
15814
- import {
15815
- existsSync,
15816
- mkdirSync as mkdirSync2,
15817
- readFileSync,
15818
- renameSync as renameSync2,
15819
- unlinkSync as unlinkSync2,
15820
- writeFileSync as writeFileSync2
15821
- } from "fs";
15822
- import { join as join2 } from "path";
15823
- function defaultIsPidAlive(pid) {
15824
- if (!Number.isFinite(pid) || pid <= 0) return false;
15825
- try {
15826
- process.kill(pid, 0);
15827
- return true;
15828
- } catch (err) {
15829
- const code = err.code;
15830
- if (code === "ESRCH") return false;
15831
- return true;
15832
- }
15833
- }
15834
- function acquireMcpSpawnLock(args) {
15835
- const { agentDir, basename, options = {} } = args;
15836
- if (!agentDir) return { kind: "no-agent-dir" };
15837
- const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15838
- const selfPid = options.selfPid ?? process.pid;
15839
- const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15840
- const path = join2(agentDir, basename);
15841
- const existing = readLockHolder(path);
15842
- if (existing) {
15843
- if (existing.pid === selfPid) {
15844
- return { kind: "acquired", path };
15845
- }
15846
- if (isPidAlive(existing.pid)) {
15847
- return { kind: "blocked", path, holder: existing };
15767
+ if (isPidAlive(existing.pid)) {
15768
+ return { kind: "blocked", path, holder: existing };
15848
15769
  }
15849
15770
  }
15850
15771
  mkdirSync2(agentDir, { recursive: true, mode: 448 });
@@ -15879,19 +15800,133 @@ function readLockHolder(path) {
15879
15800
  }
15880
15801
  }
15881
15802
 
15803
+ // src/ack-reaction.ts
15804
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
15805
+ import { join as join3 } from "path";
15806
+ var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
15807
+ var ACK_STARTUP_GRACE_MS = 6e4;
15808
+ var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
15809
+ function decideAckReaction(i) {
15810
+ if (!i.hasTarget) return "none";
15811
+ if (!i.integrationReady) return "undeliverable";
15812
+ if (i.tmux === "dead") return "undeliverable";
15813
+ if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
15814
+ const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
15815
+ if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
15816
+ const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
15817
+ const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
15818
+ if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
15819
+ return "ack";
15820
+ }
15821
+ return "undeliverable";
15822
+ }
15823
+ return "ack";
15824
+ }
15825
+ function decideRecoveryHeal(i) {
15826
+ return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
15827
+ }
15828
+ var UNDELIVERABLE_NOTICE_THROTTLE_MS = 5 * 60 * 1e3;
15829
+ function shouldPostUndeliverableNotice(lastNoticeAtMs, nowMs, throttleMs = UNDELIVERABLE_NOTICE_THROTTLE_MS) {
15830
+ return lastNoticeAtMs == null || nowMs - lastNoticeAtMs >= throttleMs;
15831
+ }
15832
+ function undeliverableNoticeText() {
15833
+ return "\u23F3 I can't get to this right now. Please resend in a few minutes \u2014 I may not see this one.";
15834
+ }
15835
+ var GIVE_UP_SIGNAL_FILENAME = "watchdog-give-up.json";
15836
+ var GIVE_UP_SIGNAL_MAX_AGE_MS = 30 * 60 * 1e3;
15837
+ function readGiveUpSignalAtMs(path, now = Date.now()) {
15838
+ if (!path) return null;
15839
+ try {
15840
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
15841
+ if (typeof raw.gave_up_at !== "string") return null;
15842
+ const t = Date.parse(raw.gave_up_at);
15843
+ if (!Number.isFinite(t) || t > now) return null;
15844
+ return t;
15845
+ } catch {
15846
+ return null;
15847
+ }
15848
+ }
15849
+ function decideGiveUpNotice(input) {
15850
+ const maxAge = input.maxAgeMs ?? GIVE_UP_SIGNAL_MAX_AGE_MS;
15851
+ if (input.signalAtMs == null) return false;
15852
+ if (input.nowMs - input.signalAtMs > maxAge) return false;
15853
+ if (input.lastHandledAtMs != null && input.signalAtMs <= input.lastHandledAtMs) return false;
15854
+ return true;
15855
+ }
15856
+ function giveUpNoticeText() {
15857
+ return "\u26A0\uFE0F I couldn't read your last message \u2014 please resend it.";
15858
+ }
15859
+ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
15860
+ if (!dir) return null;
15861
+ let names;
15862
+ try {
15863
+ names = readdirSync(dir);
15864
+ } catch {
15865
+ return null;
15866
+ }
15867
+ let oldest = null;
15868
+ for (const name of names) {
15869
+ if (!name.endsWith(".json")) continue;
15870
+ let receivedAt;
15871
+ try {
15872
+ const raw = JSON.parse(readFileSync2(join3(dir, name), "utf-8"));
15873
+ receivedAt = raw.received_at;
15874
+ } catch {
15875
+ continue;
15876
+ }
15877
+ if (!receivedAt) continue;
15878
+ const t = Date.parse(receivedAt);
15879
+ if (Number.isNaN(t)) continue;
15880
+ const age = now - t;
15881
+ if (age < 0) continue;
15882
+ if (oldest == null || age > oldest) oldest = age;
15883
+ }
15884
+ return oldest;
15885
+ }
15886
+ function isPendingMarkerStale(receivedAt, nowMs, thresholdMs) {
15887
+ if (!receivedAt) return true;
15888
+ const t = Date.parse(receivedAt);
15889
+ if (!Number.isFinite(t)) return true;
15890
+ if (t > nowMs) return true;
15891
+ return nowMs - t > thresholdMs;
15892
+ }
15893
+ function channelOrphanMarkerMs() {
15894
+ const raw = parseInt(process.env.AGT_CHANNEL_ORPHAN_MARKER_MS ?? "", 10);
15895
+ return Number.isFinite(raw) && raw > 0 ? raw : 30 * 6e4;
15896
+ }
15897
+ var ORPHAN_SWEEP_INTERVAL_MS = 30 * 60 * 1e3;
15898
+ function orphanSweepIntervalMs() {
15899
+ return Math.max(6e4, Math.min(ORPHAN_SWEEP_INTERVAL_MS, channelOrphanMarkerMs()));
15900
+ }
15901
+ var MAX_MARKER_REPLAYS = 3;
15902
+ function channelReplayEnabled() {
15903
+ const v = process.env.AGT_CHANNEL_REPLAY_ENABLED;
15904
+ return v === "1" || v === "true";
15905
+ }
15906
+ function shouldReplayMarker(i) {
15907
+ if (!i.enabled) return false;
15908
+ if (!i.hasPayload) return false;
15909
+ if (!i.sessionAlive) return false;
15910
+ if (i.markerAgeMs < (i.minAgeMs ?? REPLY_WEDGED_THRESHOLD_MS)) return false;
15911
+ return i.replayCount < (i.maxReplays ?? MAX_MARKER_REPLAYS);
15912
+ }
15913
+
15882
15914
  // src/telegram-channel.ts
15883
15915
  function redactId(id) {
15884
15916
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
15885
15917
  }
15886
15918
  var BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
15887
15919
  var AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME ?? "unknown";
15888
- var TELEGRAM_AGENT_DIR = AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? join3(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15920
+ var TELEGRAM_AGENT_DIR = AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15889
15921
  var AGT_HOST = process.env.AGT_HOST ?? null;
15890
15922
  var AGT_API_KEY = process.env.AGT_API_KEY ?? null;
15891
15923
  var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
15892
15924
  var ALLOWED_CHATS = new Set(
15893
15925
  (process.env.TELEGRAM_ALLOWED_CHATS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15894
15926
  );
15927
+ var DIAGNOSTIC_CHAT_IDS = new Set(
15928
+ (process.env.TELEGRAM_DIAGNOSTIC_CHAT_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15929
+ );
15895
15930
  var PEER_CLASSIFIER_CONFIG = {
15896
15931
  peer_agent_mode: parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE),
15897
15932
  peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
@@ -15927,6 +15962,13 @@ var crossTeamPeerAuditClient = createCrossTeamPeerAuditClient({
15927
15962
  log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
15928
15963
  `)
15929
15964
  });
15965
+ var diagnosticEventClient = createDiagnosticEventClient({
15966
+ agtHost: AGT_HOST,
15967
+ agtApiKey: AGT_API_KEY,
15968
+ agentId: process.env.AGT_AGENT_ID ?? null,
15969
+ log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
15970
+ `)
15971
+ });
15930
15972
  var observedChatClient = createObservedChatClient({
15931
15973
  agtHost: AGT_HOST,
15932
15974
  agtApiKey: AGT_API_KEY,
@@ -15969,9 +16011,9 @@ if (!BOT_TOKEN) {
15969
16011
  var stderrLogStream = null;
15970
16012
  if (AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown") {
15971
16013
  try {
15972
- const logDir = join3(homedir2(), ".augmented", AGENT_CODE_NAME);
16014
+ const logDir = join4(homedir2(), ".augmented", AGENT_CODE_NAME);
15973
16015
  mkdirSync3(logDir, { recursive: true });
15974
- stderrLogStream = createWriteStream(join3(logDir, "telegram-channel-stderr.log"), {
16016
+ stderrLogStream = createWriteStream(join4(logDir, "telegram-channel-stderr.log"), {
15975
16017
  flags: "a",
15976
16018
  mode: 384
15977
16019
  });
@@ -16052,14 +16094,82 @@ async function setMessageReaction(chatId, messageId, emoji2) {
16052
16094
  );
16053
16095
  }
16054
16096
  }
16055
- var RESTART_FLAGS_DIR = join3(homedir2(), ".augmented", "restart-flags");
16097
+ var lastUndeliverableNoticeAt = /* @__PURE__ */ new Map();
16098
+ async function notifyUndeliverable(chatId, messageId) {
16099
+ const key2 = String(chatId);
16100
+ const now = Date.now();
16101
+ if (!shouldPostUndeliverableNotice(lastUndeliverableNoticeAt.get(key2), now)) return;
16102
+ lastUndeliverableNoticeAt.set(key2, now);
16103
+ try {
16104
+ const resp = await telegramApiCall(
16105
+ "sendMessage",
16106
+ {
16107
+ chat_id: chatId,
16108
+ // ENG-6025: shared copy with the Slack undeliverable notice.
16109
+ text: undeliverableNoticeText(),
16110
+ reply_to_message_id: Number(messageId),
16111
+ allow_sending_without_reply: true
16112
+ },
16113
+ 1e4
16114
+ );
16115
+ if (!resp.ok) {
16116
+ process.stderr.write(
16117
+ `telegram-channel(${AGENT_CODE_NAME}): undeliverable notice failed (chat=${redactId(chatId)}): ${resp.description ?? "unknown"}
16118
+ `
16119
+ );
16120
+ }
16121
+ } catch (err) {
16122
+ process.stderr.write(
16123
+ `telegram-channel(${AGENT_CODE_NAME}): undeliverable notice error: ${err.message}
16124
+ `
16125
+ );
16126
+ }
16127
+ }
16128
+ function __resetUndeliverableNoticeThrottle() {
16129
+ lastUndeliverableNoticeAt.clear();
16130
+ }
16131
+ var lastBackOnlineNoticeAt = /* @__PURE__ */ new Map();
16132
+ function notifyBackOnline(chatId) {
16133
+ const key2 = String(chatId);
16134
+ const now = Date.now();
16135
+ if (!shouldPostUndeliverableNotice(lastBackOnlineNoticeAt.get(key2), now)) return;
16136
+ lastBackOnlineNoticeAt.set(key2, now);
16137
+ void (async () => {
16138
+ try {
16139
+ const resp = await telegramApiCall(
16140
+ "sendMessage",
16141
+ {
16142
+ chat_id: chatId,
16143
+ text: "\u2705 Back online \u2014 catching up on what I missed."
16144
+ },
16145
+ 1e4
16146
+ );
16147
+ if (!resp.ok) {
16148
+ process.stderr.write(
16149
+ `telegram-channel(${AGENT_CODE_NAME}): back-online notice failed (chat=${redactId(chatId)}): ${resp.description ?? "unknown"}
16150
+ `
16151
+ );
16152
+ }
16153
+ } catch (err) {
16154
+ process.stderr.write(
16155
+ `telegram-channel(${AGENT_CODE_NAME}): back-online notice error: ${err.message}
16156
+ `
16157
+ );
16158
+ }
16159
+ })();
16160
+ }
16161
+ function __resetBackOnlineNoticeThrottle() {
16162
+ lastBackOnlineNoticeAt.clear();
16163
+ }
16164
+ var RESTART_FLAGS_DIR = join4(homedir2(), ".augmented", "restart-flags");
16056
16165
  function buildTelegramHelpMessage(codeName) {
16057
16166
  return [
16058
16167
  `\u{1F916} *Available commands for \`${codeName}\`*`,
16059
16168
  "",
16060
16169
  `_Type these in any chat where the bot is present (intercepted by the agent):_`,
16061
16170
  `\u2022 /help \u2014 show this help`,
16062
- `\u2022 /restart \u2014 restart this agent`
16171
+ `\u2022 /restart \u2014 restart this agent`,
16172
+ `\u2022 /investigate-${codeName} \u2014 live tail of this agent's terminal pane (DM only; team owners/admins and the agent's reports-to person)`
16063
16173
  ].join("\n");
16064
16174
  }
16065
16175
  async function handleHelpCommand(opts) {
@@ -16092,14 +16202,14 @@ async function handleRestartCommand(opts) {
16092
16202
  if (!existsSync2(RESTART_FLAGS_DIR)) {
16093
16203
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16094
16204
  }
16095
- const flagPath = join3(RESTART_FLAGS_DIR, `${AGENT_CODE_NAME}.flag`);
16205
+ const flagPath = join4(RESTART_FLAGS_DIR, `${AGENT_CODE_NAME}.flag`);
16096
16206
  const flag = {
16097
16207
  codeName: AGENT_CODE_NAME,
16098
16208
  source: "telegram",
16099
16209
  ts: Date.now(),
16100
16210
  reply: { chat_id: opts.chatId, message_id: opts.messageId }
16101
16211
  };
16102
- const tmpPath = `${flagPath}.${process.pid}.${randomUUID2()}.tmp`;
16212
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
16103
16213
  writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
16104
16214
  renameSync3(tmpPath, flagPath);
16105
16215
  process.stderr.write(
@@ -16154,6 +16264,261 @@ async function handleRestartCommand(opts) {
16154
16264
  }
16155
16265
  }
16156
16266
  }
16267
+ var INVESTIGATE_TAIL_WINDOW_MS = 12e4;
16268
+ var INVESTIGATE_TAIL_INTERVAL_MS = 3e3;
16269
+ var INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES = 4;
16270
+ var INVESTIGATE_MAX_CHARS = 3800;
16271
+ var investigateTailSlot = createSingleTailSlot();
16272
+ function investigateAuditLog(line) {
16273
+ process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): /investigate ${line}
16274
+ `);
16275
+ }
16276
+ function investigateNowUtcLabel() {
16277
+ return `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} UTC`;
16278
+ }
16279
+ function investigateSleep(ms) {
16280
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
16281
+ }
16282
+ async function classifyInvestigateCommand(text) {
16283
+ const needsUsername = /@[A-Za-z0-9_]{1,64}(?:\s|$)/.test(text);
16284
+ const ours = needsUsername ? await resolveBotUsername() : null;
16285
+ return classifyInvestigateText(text, { codeName: AGENT_CODE_NAME, botUsername: ours });
16286
+ }
16287
+ async function sendInvestigateReply(chatId, messageId, text) {
16288
+ try {
16289
+ const resp = await telegramApiCall(
16290
+ "sendMessage",
16291
+ {
16292
+ chat_id: chatId,
16293
+ text,
16294
+ parse_mode: "Markdown",
16295
+ reply_to_message_id: Number(messageId)
16296
+ },
16297
+ 1e4
16298
+ );
16299
+ if (!resp.ok) {
16300
+ investigateAuditLog(`reply rejected by Telegram: ${resp.description ?? "unknown"}`);
16301
+ }
16302
+ } catch (err) {
16303
+ investigateAuditLog(`reply send failed: ${redactAugmentedPaths(err.message)}`);
16304
+ }
16305
+ }
16306
+ async function runInvestigateTailLoop(opts) {
16307
+ const investigateCmd = `/investigate-${AGENT_CODE_NAME}`;
16308
+ let last = opts.initialFrame;
16309
+ let consecutiveFailures = 0;
16310
+ let delayMs = INVESTIGATE_TAIL_INTERVAL_MS;
16311
+ try {
16312
+ while (Date.now() + delayMs < opts.expiresAtMs) {
16313
+ await investigateSleep(delayMs);
16314
+ delayMs = INVESTIGATE_TAIL_INTERVAL_MS;
16315
+ const snapshot = await capturePaneSnapshot({
16316
+ codeName: AGENT_CODE_NAME,
16317
+ agentDir: TELEGRAM_AGENT_DIR
16318
+ });
16319
+ if (!snapshot) {
16320
+ consecutiveFailures++;
16321
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16322
+ investigateAuditLog(`tail aborted after ${consecutiveFailures} consecutive capture failures`);
16323
+ break;
16324
+ }
16325
+ continue;
16326
+ }
16327
+ const frame = {
16328
+ body: redactPaneText(snapshot.text),
16329
+ source: snapshot.source,
16330
+ capturedAtLabel: investigateNowUtcLabel()
16331
+ };
16332
+ if (frame.body === last.body) continue;
16333
+ const text = formatPaneSnapshot({
16334
+ codeName: AGENT_CODE_NAME,
16335
+ text: frame.body,
16336
+ source: frame.source,
16337
+ capturedAtLabel: frame.capturedAtLabel,
16338
+ status: {
16339
+ kind: "live",
16340
+ secondsRemaining: Math.max(0, Math.round((opts.expiresAtMs - Date.now()) / 1e3))
16341
+ },
16342
+ maxChars: INVESTIGATE_MAX_CHARS,
16343
+ commandName: investigateCmd
16344
+ });
16345
+ try {
16346
+ const resp = await telegramApiCall(
16347
+ "editMessageText",
16348
+ {
16349
+ chat_id: opts.chatId,
16350
+ message_id: opts.messageId,
16351
+ text,
16352
+ parse_mode: "Markdown"
16353
+ },
16354
+ 1e4
16355
+ );
16356
+ if (resp.ok) {
16357
+ last = frame;
16358
+ consecutiveFailures = 0;
16359
+ continue;
16360
+ }
16361
+ const retryAfter = resp.parameters?.retry_after;
16362
+ if (typeof retryAfter === "number" && retryAfter > 0) {
16363
+ delayMs = Math.max(INVESTIGATE_TAIL_INTERVAL_MS, retryAfter * 1e3);
16364
+ continue;
16365
+ }
16366
+ consecutiveFailures++;
16367
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16368
+ investigateAuditLog(
16369
+ `tail aborted after ${consecutiveFailures} consecutive update failures (last: ${resp.description ?? "unknown"})`
16370
+ );
16371
+ break;
16372
+ }
16373
+ } catch (err) {
16374
+ consecutiveFailures++;
16375
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16376
+ investigateAuditLog(
16377
+ `tail aborted after ${consecutiveFailures} consecutive update failures (last: ${redactAugmentedPaths(err.message)})`
16378
+ );
16379
+ break;
16380
+ }
16381
+ }
16382
+ }
16383
+ } finally {
16384
+ investigateTailSlot.release();
16385
+ try {
16386
+ await telegramApiCall(
16387
+ "editMessageText",
16388
+ {
16389
+ chat_id: opts.chatId,
16390
+ message_id: opts.messageId,
16391
+ text: formatPaneSnapshot({
16392
+ codeName: AGENT_CODE_NAME,
16393
+ text: last.body,
16394
+ source: last.source,
16395
+ capturedAtLabel: last.capturedAtLabel,
16396
+ status: { kind: "expired" },
16397
+ maxChars: INVESTIGATE_MAX_CHARS,
16398
+ commandName: investigateCmd
16399
+ }),
16400
+ parse_mode: "Markdown"
16401
+ },
16402
+ 1e4
16403
+ );
16404
+ } catch (err) {
16405
+ investigateAuditLog(`expired-edit failed: ${redactAugmentedPaths(err.message)}`);
16406
+ }
16407
+ }
16408
+ }
16409
+ async function handleInvestigateCommand(opts) {
16410
+ const investigateCmd = `/investigate-${AGENT_CODE_NAME}`;
16411
+ const emitAudit = (outcome, reason) => {
16412
+ diagnosticEventClient?.emit({
16413
+ channel: "telegram",
16414
+ command: "investigate",
16415
+ outcome,
16416
+ reason,
16417
+ chat_id: opts.chatId,
16418
+ sender_chat_id: opts.senderId
16419
+ });
16420
+ };
16421
+ const verdict = evaluateInvestigateGate({
16422
+ chatType: opts.chatType,
16423
+ senderId: opts.senderId,
16424
+ diagnosticChatIds: DIAGNOSTIC_CHAT_IDS
16425
+ });
16426
+ if (!verdict.ok) {
16427
+ investigateAuditLog(
16428
+ `denied reason=${verdict.reason} sender=${opts.senderId ? redactId(opts.senderId) : "unknown"}`
16429
+ );
16430
+ emitAudit("denied", verdict.reason);
16431
+ const denialText = verdict.reason === "not-dm" ? `\u26A0\uFE0F \`${investigateCmd}\` only works in a direct message with \`${AGENT_CODE_NAME}\` \u2014 it shows the agent's raw terminal, so it stays 1:1. Open a DM with the bot and run it there.` : verdict.reason === "allowlist-empty" ? `\u{1F6AB} \`${investigateCmd}\` is disabled for \`${AGENT_CODE_NAME}\` \u2014 no diagnostic allowlist reached this host. It is derived from team owners/admins and the agent's reports-to person; if that's you, link your Telegram in your Augmented Team contact preferences and wait for the next refresh.` : `\u{1F6AB} \`${investigateCmd}\` denied \u2014 it's limited to team owners/admins and the person this agent reports to. If that's you, link your Telegram in your Augmented Team contact preferences and wait for the next refresh.`;
16432
+ await sendInvestigateReply(opts.chatId, opts.messageId, denialText);
16433
+ return;
16434
+ }
16435
+ const reservedExpiresAtMs = investigateTailSlot.reserve(INVESTIGATE_TAIL_WINDOW_MS);
16436
+ if (reservedExpiresAtMs === null) {
16437
+ const remaining = formatCountdown(investigateTailSlot.remainingMs() / 1e3);
16438
+ investigateAuditLog(
16439
+ `not-started reason=tail-already-running sender=${redactId(opts.senderId ?? "unknown")} remaining=${remaining}`
16440
+ );
16441
+ emitAudit("not_started", "tail-already-running");
16442
+ await sendInvestigateReply(
16443
+ opts.chatId,
16444
+ opts.messageId,
16445
+ `\u23F3 A \`${investigateCmd}\` tail is already running for \`${AGENT_CODE_NAME}\` (one at a time) \u2014 it expires in ${remaining}.`
16446
+ );
16447
+ return;
16448
+ }
16449
+ const snapshot = await capturePaneSnapshot({
16450
+ codeName: AGENT_CODE_NAME,
16451
+ agentDir: TELEGRAM_AGENT_DIR
16452
+ });
16453
+ if (!snapshot) {
16454
+ investigateTailSlot.release();
16455
+ investigateAuditLog(`not-started reason=no-pane-output sender=${redactId(opts.senderId ?? "unknown")}`);
16456
+ emitAudit("not_started", "no-pane-output");
16457
+ await sendInvestigateReply(
16458
+ opts.chatId,
16459
+ opts.messageId,
16460
+ `\u26A0\uFE0F No pane output available for \`${AGENT_CODE_NAME}\` yet \u2014 the agent session may not have started. (If the whole host is wedged, \`${investigateCmd}\` can't reach it either \u2014 fall back to the SSM diagnostics runbook.)`
16461
+ );
16462
+ return;
16463
+ }
16464
+ investigateAuditLog(`granted sender=${redactId(opts.senderId ?? "unknown")} \u2014 starting live tail (source=${snapshot.source})`);
16465
+ emitAudit("granted", null);
16466
+ const initialFrame = {
16467
+ body: redactPaneText(snapshot.text),
16468
+ source: snapshot.source,
16469
+ capturedAtLabel: investigateNowUtcLabel()
16470
+ };
16471
+ let postedMessageId = null;
16472
+ try {
16473
+ const posted = await telegramApiCall(
16474
+ "sendMessage",
16475
+ {
16476
+ chat_id: opts.chatId,
16477
+ text: formatPaneSnapshot({
16478
+ codeName: AGENT_CODE_NAME,
16479
+ text: initialFrame.body,
16480
+ source: initialFrame.source,
16481
+ capturedAtLabel: initialFrame.capturedAtLabel,
16482
+ status: {
16483
+ kind: "live",
16484
+ secondsRemaining: Math.max(0, Math.round((reservedExpiresAtMs - Date.now()) / 1e3))
16485
+ },
16486
+ maxChars: INVESTIGATE_MAX_CHARS,
16487
+ commandName: investigateCmd
16488
+ }),
16489
+ parse_mode: "Markdown",
16490
+ reply_to_message_id: Number(opts.messageId)
16491
+ },
16492
+ 1e4
16493
+ );
16494
+ const result = posted.result;
16495
+ if (posted.ok && typeof result?.message_id === "number") {
16496
+ postedMessageId = result.message_id;
16497
+ } else {
16498
+ investigateAuditLog(`not-started reason=post-failed error=${posted.description ?? "unknown"}`);
16499
+ }
16500
+ } catch (err) {
16501
+ investigateAuditLog(`not-started reason=post-failed error=${redactAugmentedPaths(err.message)}`);
16502
+ }
16503
+ if (postedMessageId === null) {
16504
+ investigateTailSlot.release();
16505
+ await sendInvestigateReply(
16506
+ opts.chatId,
16507
+ opts.messageId,
16508
+ `\u274C Failed to post the pane snapshot. Try again in a moment.`
16509
+ );
16510
+ return;
16511
+ }
16512
+ void runInvestigateTailLoop({
16513
+ chatId: opts.chatId,
16514
+ messageId: postedMessageId,
16515
+ initialFrame,
16516
+ expiresAtMs: reservedExpiresAtMs
16517
+ }).catch((err) => {
16518
+ investigateTailSlot.release();
16519
+ investigateAuditLog(`tail loop crashed: ${redactAugmentedPaths(err.message)}`);
16520
+ });
16521
+ }
16157
16522
  var cachedBotUsername = null;
16158
16523
  var cachedBotId = null;
16159
16524
  async function refreshBotIdentity() {
@@ -16208,25 +16573,30 @@ async function classifyRestartCommand(text) {
16208
16573
  if (!ours) return "verification_failed";
16209
16574
  return target === ours ? "act" : "ignore";
16210
16575
  }
16211
- var AGENT_DIR = AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? join3(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
16212
- var PENDING_INBOUND_DIR = AGENT_DIR ? join3(AGENT_DIR, "telegram-pending-inbound") : null;
16213
- var RECOVERY_OUTBOX_DIR = AGENT_DIR ? join3(AGENT_DIR, "telegram-recovery-outbox") : null;
16576
+ var AGENT_DIR = AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
16577
+ var PENDING_INBOUND_DIR = AGENT_DIR ? join4(AGENT_DIR, "telegram-pending-inbound") : null;
16578
+ var RECOVERY_OUTBOX_DIR = AGENT_DIR ? join4(AGENT_DIR, "telegram-recovery-outbox") : null;
16214
16579
  function safeMarkerName(chatId, messageId) {
16215
16580
  const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
16216
16581
  return `${safe(chatId)}__${safe(messageId)}.json`;
16217
16582
  }
16218
16583
  function pendingInboundPath(chatId, messageId) {
16219
16584
  if (!PENDING_INBOUND_DIR) return null;
16220
- return join3(PENDING_INBOUND_DIR, safeMarkerName(chatId, messageId));
16585
+ return join4(PENDING_INBOUND_DIR, safeMarkerName(chatId, messageId));
16221
16586
  }
16222
- function writePendingInboundMarker(chatId, messageId, chatType) {
16587
+ function writePendingInboundMarker(chatId, messageId, chatType, undeliverable = false, payload) {
16223
16588
  const path = pendingInboundPath(chatId, messageId);
16224
16589
  if (!path || !PENDING_INBOUND_DIR) return;
16225
16590
  const marker = {
16226
16591
  chat_id: chatId,
16227
16592
  message_id: messageId,
16228
16593
  chat_type: chatType,
16229
- received_at: (/* @__PURE__ */ new Date()).toISOString()
16594
+ received_at: (/* @__PURE__ */ new Date()).toISOString(),
16595
+ // Only persist the flag when set — keeps healthy-path markers byte-identical.
16596
+ ...undeliverable ? { undeliverable: true } : {},
16597
+ // ENG-5969: carry the replay payload only when provided (the durable-replay
16598
+ // path). Absent ⇒ the marker can't be replayed (older / ack-only callers).
16599
+ ...payload ? { payload } : {}
16230
16600
  };
16231
16601
  try {
16232
16602
  mkdirSync3(PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
@@ -16238,14 +16608,28 @@ function writePendingInboundMarker(chatId, messageId, chatType) {
16238
16608
  );
16239
16609
  }
16240
16610
  }
16241
- function clearPendingInboundMarker(chatId, messageId) {
16242
- const path = pendingInboundPath(chatId, messageId);
16243
- if (!path) return;
16611
+ function clearTelegramMarkerFileWithHeal(fullPath) {
16612
+ let marker = null;
16613
+ try {
16614
+ marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
16615
+ } catch {
16616
+ }
16617
+ if (marker && decideRecoveryHeal({
16618
+ wasUndeliverable: marker.undeliverable === true,
16619
+ hasTarget: Boolean(marker.chat_id)
16620
+ }) === "heal") {
16621
+ notifyBackOnline(marker.chat_id);
16622
+ }
16244
16623
  try {
16245
- if (existsSync2(path)) unlinkSync3(path);
16624
+ if (existsSync2(fullPath)) unlinkSync3(fullPath);
16246
16625
  } catch {
16247
16626
  }
16248
16627
  }
16628
+ function clearPendingInboundMarker(chatId, messageId) {
16629
+ const path = pendingInboundPath(chatId, messageId);
16630
+ if (!path) return;
16631
+ clearTelegramMarkerFileWithHeal(path);
16632
+ }
16249
16633
  var MAX_RECOVERY_ATTEMPTS = 3;
16250
16634
  function nextRetryName(filename) {
16251
16635
  const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
@@ -16261,10 +16645,10 @@ function nextRetryName(filename) {
16261
16645
  async function processRecoveryOutboxFile(filename) {
16262
16646
  if (!RECOVERY_OUTBOX_DIR) return;
16263
16647
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
16264
- const fullPath = join3(RECOVERY_OUTBOX_DIR, filename);
16648
+ const fullPath = join4(RECOVERY_OUTBOX_DIR, filename);
16265
16649
  let payload;
16266
16650
  try {
16267
- const raw = readFileSync2(fullPath, "utf-8");
16651
+ const raw = readFileSync3(fullPath, "utf-8");
16268
16652
  payload = JSON.parse(raw);
16269
16653
  } catch (err) {
16270
16654
  process.stderr.write(
@@ -16327,7 +16711,7 @@ async function processRecoveryOutboxFile(filename) {
16327
16711
  const next = nextRetryName(filename);
16328
16712
  if (next) {
16329
16713
  try {
16330
- renameSync3(fullPath, join3(RECOVERY_OUTBOX_DIR, next.next));
16714
+ renameSync3(fullPath, join4(RECOVERY_OUTBOX_DIR, next.next));
16331
16715
  if (next.attempt >= MAX_RECOVERY_ATTEMPTS) {
16332
16716
  process.stderr.write(
16333
16717
  `telegram-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -16358,7 +16742,7 @@ function scanRecoveryRetries() {
16358
16742
  if (!RECOVERY_OUTBOX_DIR) return;
16359
16743
  let entries;
16360
16744
  try {
16361
- entries = readdirSync(RECOVERY_OUTBOX_DIR);
16745
+ entries = readdirSync2(RECOVERY_OUTBOX_DIR);
16362
16746
  } catch {
16363
16747
  return;
16364
16748
  }
@@ -16367,7 +16751,7 @@ function scanRecoveryRetries() {
16367
16751
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
16368
16752
  let mtimeMs;
16369
16753
  try {
16370
- mtimeMs = statSync(join3(RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16754
+ mtimeMs = statSync(join4(RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16371
16755
  } catch {
16372
16756
  continue;
16373
16757
  }
@@ -16388,7 +16772,7 @@ function startRecoveryOutboxWatcher() {
16388
16772
  return;
16389
16773
  }
16390
16774
  try {
16391
- for (const f of readdirSync(RECOVERY_OUTBOX_DIR)) {
16775
+ for (const f of readdirSync2(RECOVERY_OUTBOX_DIR)) {
16392
16776
  if (isFirstAttemptOutboxFile(f)) void processRecoveryOutboxFile(f);
16393
16777
  }
16394
16778
  } catch {
@@ -16397,7 +16781,7 @@ function startRecoveryOutboxWatcher() {
16397
16781
  const watcher = watch(RECOVERY_OUTBOX_DIR, (event, filename) => {
16398
16782
  if (event !== "rename" || !filename) return;
16399
16783
  if (!isFirstAttemptOutboxFile(filename)) return;
16400
- if (existsSync2(join3(RECOVERY_OUTBOX_DIR, filename))) {
16784
+ if (existsSync2(join4(RECOVERY_OUTBOX_DIR, filename))) {
16401
16785
  void processRecoveryOutboxFile(filename);
16402
16786
  }
16403
16787
  });
@@ -16413,15 +16797,15 @@ function startRecoveryOutboxWatcher() {
16413
16797
  }
16414
16798
  startRecoveryOutboxWatcher();
16415
16799
  var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
16416
- function trackPendingMessage(chatId, messageId, chatType) {
16417
- writePendingInboundMarker(chatId, messageId, chatType);
16800
+ function trackPendingMessage(chatId, messageId, chatType, undeliverable = false, payload) {
16801
+ writePendingInboundMarker(chatId, messageId, chatType, undeliverable, payload);
16418
16802
  }
16419
- function sweepTelegramStaleMarkersOnBoot() {
16803
+ function sweepTelegramStaleMarkers(thresholdMs) {
16420
16804
  if (!PENDING_INBOUND_DIR) return;
16421
16805
  if (!existsSync2(PENDING_INBOUND_DIR)) return;
16422
16806
  let filenames;
16423
16807
  try {
16424
- filenames = readdirSync(PENDING_INBOUND_DIR);
16808
+ filenames = readdirSync2(PENDING_INBOUND_DIR);
16425
16809
  } catch (err) {
16426
16810
  process.stderr.write(
16427
16811
  `telegram-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -16434,10 +16818,10 @@ function sweepTelegramStaleMarkersOnBoot() {
16434
16818
  for (const filename of filenames) {
16435
16819
  if (!filename.endsWith(".json")) continue;
16436
16820
  if (filename.endsWith(".tmp")) continue;
16437
- const fullPath = join3(PENDING_INBOUND_DIR, filename);
16821
+ const fullPath = join4(PENDING_INBOUND_DIR, filename);
16438
16822
  let marker;
16439
16823
  try {
16440
- marker = JSON.parse(readFileSync2(fullPath, "utf-8"));
16824
+ marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
16441
16825
  } catch (err) {
16442
16826
  process.stderr.write(
16443
16827
  `telegram-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactId(filename)}: ${err.message}
@@ -16451,31 +16835,152 @@ function sweepTelegramStaleMarkersOnBoot() {
16451
16835
  continue;
16452
16836
  }
16453
16837
  const { chat_id, message_id, received_at } = marker;
16454
- if (!chat_id || !message_id || !received_at) {
16838
+ if (!chat_id || !message_id || isPendingMarkerStale(received_at, now, thresholdMs)) {
16455
16839
  try {
16456
16840
  unlinkSync3(fullPath);
16457
16841
  } catch {
16458
16842
  }
16459
16843
  cleared++;
16460
- continue;
16461
16844
  }
16462
- const receivedAtMs = Date.parse(received_at);
16463
- if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
16845
+ }
16846
+ if (cleared > 0) {
16847
+ process.stderr.write(
16848
+ `telegram-channel(${AGENT_CODE_NAME}): stale-marker sweep cleared=${cleared}
16849
+ `
16850
+ );
16851
+ }
16852
+ }
16853
+ sweepTelegramStaleMarkers(STALE_MARKER_MS);
16854
+ var orphanSweepTimer = setInterval(() => {
16855
+ const probe = process.env.TMUX && AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
16856
+ const sessionAlive = probe.tmux === "alive" && probe.claude === "alive";
16857
+ sweepTelegramStaleMarkers(sessionAlive ? channelOrphanMarkerMs() : STALE_MARKER_MS);
16858
+ checkWatchdogGiveUpNotice();
16859
+ }, orphanSweepIntervalMs());
16860
+ orphanSweepTimer.unref?.();
16861
+ var lastGiveUpHandledAtMs = null;
16862
+ function listPendingInboundChatIds() {
16863
+ if (!PENDING_INBOUND_DIR || !existsSync2(PENDING_INBOUND_DIR)) return [];
16864
+ const chats = /* @__PURE__ */ new Set();
16865
+ try {
16866
+ for (const name of readdirSync2(PENDING_INBOUND_DIR)) {
16867
+ if (!name.endsWith(".json")) continue;
16464
16868
  try {
16465
- unlinkSync3(fullPath);
16869
+ const marker = JSON.parse(
16870
+ readFileSync3(join4(PENDING_INBOUND_DIR, name), "utf8")
16871
+ );
16872
+ if (typeof marker.chat_id === "string" && marker.chat_id) chats.add(marker.chat_id);
16466
16873
  } catch {
16467
16874
  }
16468
- cleared++;
16469
16875
  }
16876
+ } catch {
16877
+ return [];
16470
16878
  }
16471
- if (cleared > 0) {
16879
+ return [...chats];
16880
+ }
16881
+ async function notifyWatchdogGiveUp(chatId) {
16882
+ const now = Date.now();
16883
+ if (!shouldPostUndeliverableNotice(lastUndeliverableNoticeAt.get(chatId), now)) return;
16884
+ lastUndeliverableNoticeAt.set(chatId, now);
16885
+ try {
16886
+ const resp = await telegramApiCall(
16887
+ "sendMessage",
16888
+ { chat_id: chatId, text: giveUpNoticeText() },
16889
+ 1e4
16890
+ );
16891
+ if (!resp.ok) {
16892
+ process.stderr.write(
16893
+ `telegram-channel(${AGENT_CODE_NAME}): give-up notice failed (chat=${redactId(chatId)}): ${resp.description ?? "unknown"}
16894
+ `
16895
+ );
16896
+ return;
16897
+ }
16472
16898
  process.stderr.write(
16473
- `telegram-channel(${AGENT_CODE_NAME}): stale-marker sweep cleared=${cleared}
16899
+ `telegram-channel(${AGENT_CODE_NAME}): give-up notice posted chat=${redactId(chatId)}
16900
+ `
16901
+ );
16902
+ } catch (err) {
16903
+ process.stderr.write(
16904
+ `telegram-channel(${AGENT_CODE_NAME}): give-up notice error: ${err.message}
16905
+ `
16906
+ );
16907
+ }
16908
+ }
16909
+ function checkWatchdogGiveUpNotice() {
16910
+ if (!AGENT_DIR) return;
16911
+ const signalAtMs = readGiveUpSignalAtMs(join4(AGENT_DIR, GIVE_UP_SIGNAL_FILENAME));
16912
+ const act = decideGiveUpNotice({
16913
+ signalAtMs,
16914
+ lastHandledAtMs: lastGiveUpHandledAtMs,
16915
+ nowMs: Date.now()
16916
+ });
16917
+ if (signalAtMs != null) {
16918
+ lastGiveUpHandledAtMs = Math.max(lastGiveUpHandledAtMs ?? 0, signalAtMs);
16919
+ }
16920
+ if (!act) return;
16921
+ const chats = listPendingInboundChatIds();
16922
+ if (chats.length === 0) {
16923
+ process.stderr.write(
16924
+ `telegram-channel(${AGENT_CODE_NAME}): give-up signal seen but no pending markers \u2014 nothing to notice
16925
+ `
16926
+ );
16927
+ return;
16928
+ }
16929
+ for (const chatId of chats) {
16930
+ void notifyWatchdogGiveUp(chatId);
16931
+ }
16932
+ }
16933
+ function __resetGiveUpNoticeStateForTests() {
16934
+ lastGiveUpHandledAtMs = null;
16935
+ }
16936
+ var queuedReplyCounts = /* @__PURE__ */ new Map();
16937
+ function _resetQueuedReplyCountsForTests() {
16938
+ queuedReplyCounts.clear();
16939
+ }
16940
+ async function deliverQueuedReply(p) {
16941
+ const remaining = (queuedReplyCounts.get(p.chatId) ?? 1) - 1;
16942
+ if (remaining <= 0) queuedReplyCounts.delete(p.chatId);
16943
+ else queuedReplyCounts.set(p.chatId, remaining);
16944
+ try {
16945
+ const killed = await isThreadKilled({
16946
+ channelType: "telegram",
16947
+ channelId: p.chatId,
16948
+ threadTs: "",
16949
+ agtHost: AGT_HOST,
16950
+ agtApiKey: AGT_API_KEY
16951
+ });
16952
+ if (killed) {
16953
+ process.stderr.write(
16954
+ `telegram-channel(${AGENT_CODE_NAME}): reply_queue_dropped reason=killed chat=${redactId(p.chatId)}
16955
+ `
16956
+ );
16957
+ return;
16958
+ }
16959
+ const body = { chat_id: p.chatId, text: p.text };
16960
+ if (p.replyToMessageId) body.reply_to_message_id = Number(p.replyToMessageId);
16961
+ const data = await telegramApiCall("sendMessage", body, 15e3);
16962
+ if (!data.ok) {
16963
+ process.stderr.write(
16964
+ `telegram-channel(${AGENT_CODE_NAME}): reply_queue_failed chat=${redactId(p.chatId)} error=${data.description ?? "unknown"}
16965
+ `
16966
+ );
16967
+ return;
16968
+ }
16969
+ recordReply(p.chatId, "", Date.now(), p.throttleCfg);
16970
+ if (p.isReplyTool) {
16971
+ clearPendingMessage(p.chatId, p.replyToMessageId);
16972
+ }
16973
+ process.stderr.write(
16974
+ `telegram-channel(${AGENT_CODE_NAME}): reply_queue_delivered chat=${redactId(p.chatId)}
16975
+ `
16976
+ );
16977
+ } catch (err) {
16978
+ process.stderr.write(
16979
+ `telegram-channel(${AGENT_CODE_NAME}): reply_queue_failed chat=${redactId(p.chatId)} error=${err.message}
16474
16980
  `
16475
16981
  );
16476
16982
  }
16477
16983
  }
16478
- sweepTelegramStaleMarkersOnBoot();
16479
16984
  function clearPendingMessage(chatId, messageId) {
16480
16985
  if (messageId) {
16481
16986
  clearPendingInboundMarker(chatId, messageId);
@@ -16486,17 +16991,14 @@ function clearPendingMessage(chatId, messageId) {
16486
16991
  const prefix = `${safeChatId}__`;
16487
16992
  let filenames;
16488
16993
  try {
16489
- filenames = readdirSync(PENDING_INBOUND_DIR);
16994
+ filenames = readdirSync2(PENDING_INBOUND_DIR);
16490
16995
  } catch {
16491
16996
  return;
16492
16997
  }
16493
16998
  for (const filename of filenames) {
16494
16999
  if (!filename.startsWith(prefix)) continue;
16495
17000
  if (!filename.endsWith(".json")) continue;
16496
- try {
16497
- unlinkSync3(join3(PENDING_INBOUND_DIR, filename));
16498
- } catch {
16499
- }
17001
+ clearTelegramMarkerFileWithHeal(join4(PENDING_INBOUND_DIR, filename));
16500
17002
  }
16501
17003
  }
16502
17004
  function noteThreadActivity(chatId, messageId) {
@@ -16519,48 +17021,14 @@ var mcp = new Server(
16519
17021
  "CRITICAL: every response to a Telegram <channel> tag MUST go through telegram.reply with the chat_id from the tag. Text in your session WITHOUT a telegram.reply call never reaches the user.",
16520
17022
  'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">. Pass reply_to_message_id from the tag so the response lands as a quote-reply in busy chats.',
16521
17023
  "Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call telegram.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Caption arrives as channel content. Don't surface internal file-handling errors that don't affect the answer.",
16522
- 'For work >30s follow CLAUDE.md kanban flow: kanban.add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
17024
+ 'For work >30s follow CLAUDE.md kanban flow: kanban_add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
16523
17025
  "Address users by user_name; user is the numeric Telegram ID. Resolve ambiguous times against your own Timezone from CLAUDE.md \u2014 do not ask.",
16524
- "Reaction taxonomy (use telegram.react sparingly \u2014 prefer telegram.reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u{1F44D} or \u{1F389} = success. NEVER react to signal failure. On failure, telegram.reply with one sentence explaining what went wrong. Free-tier emoji: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} \u{1F494}."
17026
+ `Reaction taxonomy (use telegram.react sparingly \u2014 prefer telegram.reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u{1F44D} or \u{1F389} = success. NEVER react to signal failure of YOUR work. On failure, telegram.reply with one sentence explaining what went wrong. Free-tier emoji: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} \u{1F494}. (If a message arrived while you were offline you may see a system-posted "can't respond right now" notice \u2014 that's the runtime, not you; don't repeat or apologise for it.)`
16525
17027
  ].join(" ")
16526
17028
  }
16527
17029
  );
16528
- var TELEGRAM_PROGRESS_TIMEOUT_MS = 5e3;
16529
- var progressRegistry = new ProgressToolRegistry({
16530
- prefix: "telegram",
16531
- surfaceDescription: "a Telegram chat",
16532
- startArgsSchema: {
16533
- properties: {
16534
- chat_id: {
16535
- type: "string",
16536
- description: "Telegram chat id (from the `chat_id` attribute on the inbound <channel> tag). Numeric ids are passed as strings; the API accepts both."
16537
- },
16538
- message_thread_id: {
16539
- type: "string",
16540
- description: "Forum topic id (`message_thread_id`) \u2014 pass this if the inbound arrived in a forum topic so the anchor lands in the same topic. Omit for normal chats."
16541
- },
16542
- reply_to_message_id: {
16543
- type: "string",
16544
- description: "Optional message id to thread the anchor under (typically the inbound message_id from the <channel> tag)."
16545
- }
16546
- },
16547
- required: ["chat_id"]
16548
- },
16549
- createFlush: (args) => {
16550
- const chatId = args.chat_id;
16551
- const threadId = typeof args.message_thread_id === "string" && args.message_thread_id.length > 0 ? Number(args.message_thread_id) : void 0;
16552
- const replyTo = typeof args.reply_to_message_id === "string" && args.reply_to_message_id.length > 0 ? Number(args.reply_to_message_id) : void 0;
16553
- return createTelegramProgressFlush({
16554
- chatId,
16555
- messageThreadId: Number.isFinite(threadId) ? threadId : void 0,
16556
- replyToMessageId: Number.isFinite(replyTo) ? replyTo : void 0,
16557
- callImpl: (method, body) => telegramApiCall(method, body, TELEGRAM_PROGRESS_TIMEOUT_MS)
16558
- });
16559
- }
16560
- });
16561
17030
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16562
17031
  tools: [
16563
- ...progressRegistry.getDefinitions(),
16564
17032
  {
16565
17033
  name: "channel_request_input",
16566
17034
  description: "Generic Yes/No or multi-choice picker. Renders as a Telegram inline keyboard; the tool blocks until the user taps an option (or the timeout elapses) and returns the resolved value. Use this when you need a structured pick rather than freeform text \u2014 e.g. confirming a draft action or disambiguating an ambiguous fork.",
@@ -16652,7 +17120,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16652
17120
  },
16653
17121
  {
16654
17122
  name: "telegram.react",
16655
- description: "Add an emoji reaction to a Telegram message. Use sparingly \u2014 prefer a text reply via telegram.reply. Reaction taxonomy: \u{1F44D} or \u{1F389} = action completed successfully. NEVER react to signal failure \u2014 users can't tell why it failed from a reaction. On failure, call telegram.reply with one sentence explaining what went wrong instead. \u{1F440} is already auto-applied on inbound; do not duplicate. Only free-tier emoji reactions are available to bots (Premium-only emoji fail silently). Pass an empty string or omit emoji to clear the bot's reaction on that message.",
17123
+ description: `Add an emoji reaction to a Telegram message. Use sparingly \u2014 prefer a text reply via telegram.reply. Reaction taxonomy: \u{1F44D} or \u{1F389} = action completed successfully. NEVER react to signal failure of your work \u2014 users can't tell why it failed from a reaction. On failure, call telegram.reply with one sentence explaining what went wrong instead. \u{1F440} is already auto-applied on inbound; do not duplicate. Only free-tier emoji reactions are available to bots (Premium-only emoji fail silently). Pass an empty string or omit emoji to clear the bot's reaction on that message. (Note: when a message arrives while the agent is offline/wedged, the runtime \u2014 not you \u2014 posts a brief "can't respond right now" notice; you never need to add a failure reaction for that.)`,
16656
17124
  inputSchema: {
16657
17125
  type: "object",
16658
17126
  properties: {
@@ -16676,8 +17144,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16676
17144
  }));
16677
17145
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16678
17146
  const { name, arguments: args } = req.params;
16679
- const progressResult = await progressRegistry.handle(name, args ?? {});
16680
- if (progressResult !== void 0) return progressResult;
17147
+ if (isImpersonating() && !channelsEnabledOverride() && TELEGRAM_EGRESS_TOOLS.has(name)) {
17148
+ return buildImpersonationRefusal(name);
17149
+ }
16681
17150
  if (name === "channel_request_input") {
16682
17151
  return handleChannelRequestInput(args ?? {});
16683
17152
  }
@@ -16745,28 +17214,56 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16745
17214
  isError: true
16746
17215
  };
16747
17216
  }
16748
- const tgThrottleCfg = configFromEnv();
17217
+ const isPrivateChat = !chat_id.startsWith("-");
17218
+ const tgThrottleCfg = isPrivateChat ? relaxedThrottleConfig(configFromEnv()) : configFromEnv();
16749
17219
  const tgThrottleNow = Date.now();
16750
- const tgThrottleDecision = decideReplyThrottle({
17220
+ const tgPlan = planReplyDelivery({
16751
17221
  recentReplyTimestamps: getRecentReplies(chat_id, "", tgThrottleNow, tgThrottleCfg),
17222
+ queuedCount: queuedReplyCounts.get(chat_id) ?? 0,
16752
17223
  now: tgThrottleNow,
16753
17224
  config: tgThrottleCfg
16754
17225
  });
16755
- if (!tgThrottleDecision.allow) {
17226
+ if (tgPlan.action === "refuse") {
16756
17227
  process.stderr.write(
16757
- `telegram-channel(${AGENT_CODE_NAME}): reply_throttled chat=${redactId(chat_id)} count=${tgThrottleDecision.recentCount} window_ms=${tgThrottleCfg.windowMs} threshold=${tgThrottleCfg.threshold}
17228
+ `telegram-channel(${AGENT_CODE_NAME}): reply_throttled chat=${redactId(chat_id)} count=${tgPlan.recentCount} queued=${queuedReplyCounts.get(chat_id) ?? 0} window_ms=${tgThrottleCfg.windowMs} threshold=${tgThrottleCfg.threshold}
16758
17229
  `
16759
17230
  );
16760
17231
  return {
16761
17232
  content: [
16762
17233
  {
16763
17234
  type: "text",
16764
- text: `Reply throttled: ${tgThrottleDecision.recentCount} replies from this agent to this chat within the last ${Math.round(tgThrottleCfg.windowMs / 1e3)}s (threshold ${tgThrottleCfg.threshold}). The chat is paused for this agent until a human posts in it. Stop attempting to reply.`
17235
+ text: `Reply throttled: ${tgPlan.recentCount} replies from this agent to this chat within the last ${Math.round(tgThrottleCfg.windowMs / 1e3)}s (threshold ${tgThrottleCfg.threshold}) and the delivery queue is full. The chat is paused for this agent until a human posts in it. Stop attempting to reply.`
16765
17236
  }
16766
17237
  ],
16767
17238
  isError: true
16768
17239
  };
16769
17240
  }
17241
+ if (tgPlan.action === "queue" && tgPlan.deliverAtMs !== void 0) {
17242
+ const delayMs = Math.max(0, tgPlan.deliverAtMs - tgThrottleNow);
17243
+ queuedReplyCounts.set(chat_id, (queuedReplyCounts.get(chat_id) ?? 0) + 1);
17244
+ process.stderr.write(
17245
+ `telegram-channel(${AGENT_CODE_NAME}): reply_queued chat=${redactId(chat_id)} delay_ms=${delayMs} count=${tgPlan.recentCount} threshold=${tgThrottleCfg.threshold}
17246
+ `
17247
+ );
17248
+ const timer = setTimeout(() => {
17249
+ void deliverQueuedReply({
17250
+ chatId: chat_id,
17251
+ text,
17252
+ replyToMessageId: reply_to_message_id,
17253
+ isReplyTool: name === "telegram.reply",
17254
+ throttleCfg: tgThrottleCfg
17255
+ });
17256
+ }, delayMs);
17257
+ timer.unref();
17258
+ return {
17259
+ content: [
17260
+ {
17261
+ type: "text",
17262
+ text: `Reply queued, not yet sent: the reply-rate window is full (${tgPlan.recentCount} recent sends, threshold ${tgThrottleCfg.threshold}). It will be delivered automatically in ~${Math.ceil(delayMs / 1e3)}s. Do not re-send this message; avoid further sends to this chat until it delivers.`
17263
+ }
17264
+ ]
17265
+ };
17266
+ }
16770
17267
  try {
16771
17268
  const body = { chat_id, text };
16772
17269
  if (reply_to_message_id) body.reply_to_message_id = Number(reply_to_message_id);
@@ -16877,12 +17374,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16877
17374
  function channelRequestInputAvailable() {
16878
17375
  return Boolean(AGT_HOST && AGT_API_KEY && AGT_AGENT_ID);
16879
17376
  }
16880
- function errResult2(text) {
17377
+ function errResult(text) {
16881
17378
  return { content: [{ type: "text", text }], isError: true };
16882
17379
  }
16883
17380
  async function handleChannelRequestInput(args) {
16884
17381
  if (!channelRequestInputAvailable()) {
16885
- return errResult2(
17382
+ return errResult(
16886
17383
  "channel_request_input is disabled \u2014 missing AGT_HOST / AGT_API_KEY / AGT_AGENT_ID env wiring."
16887
17384
  );
16888
17385
  }
@@ -16890,12 +17387,12 @@ async function handleChannelRequestInput(args) {
16890
17387
  const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
16891
17388
  const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
16892
17389
  if (typeof thread_id !== "string" || !thread_id) {
16893
- return errResult2(
17390
+ return errResult(
16894
17391
  "thread_id is required (Telegram shape: `<chat_id>` or `<chat_id>:<reply_to_message_id>`)"
16895
17392
  );
16896
17393
  }
16897
17394
  if (typeof prompt_text !== "string" || !prompt_text) {
16898
- return errResult2("prompt_text is required");
17395
+ return errResult("prompt_text is required");
16899
17396
  }
16900
17397
  const optsValidation = validateAskUserOptions2(options, {
16901
17398
  maxOptions: 4,
@@ -16903,27 +17400,27 @@ async function handleChannelRequestInput(args) {
16903
17400
  maxValueChars: 2e3
16904
17401
  });
16905
17402
  if (!optsValidation.ok) {
16906
- return errResult2(
17403
+ return errResult(
16907
17404
  `Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
16908
17405
  );
16909
17406
  }
16910
17407
  const optionsArr = options;
16911
17408
  if (optionsArr.length < 2) {
16912
- return errResult2(
17409
+ return errResult(
16913
17410
  "options must contain at least 2 entries (this is a picker, not a notification)"
16914
17411
  );
16915
17412
  }
16916
17413
  if (!schema || typeof schema.type !== "string") {
16917
- return errResult2("schema is required with shape { type: 'yes_no' | 'choice' }");
17414
+ return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
16918
17415
  }
16919
17416
  if (schema.type !== "yes_no" && schema.type !== "choice") {
16920
- return errResult2("schema.type must be 'yes_no' or 'choice'");
17417
+ return errResult("schema.type must be 'yes_no' or 'choice'");
16921
17418
  }
16922
17419
  if (schema.type === "yes_no" && optionsArr.length !== 2) {
16923
- return errResult2("schema.type='yes_no' requires exactly 2 options");
17420
+ return errResult("schema.type='yes_no' requires exactly 2 options");
16924
17421
  }
16925
17422
  if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
16926
- return errResult2("request_id must be a non-empty string when provided");
17423
+ return errResult("request_id must be a non-empty string when provided");
16927
17424
  }
16928
17425
  const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
16929
17426
  const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
@@ -16945,10 +17442,10 @@ async function handleChannelRequestInput(args) {
16945
17442
  dispatchPayload = await res.json().catch(() => ({}));
16946
17443
  if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
16947
17444
  const reason = dispatchPayload.reason ?? `http_${res.status}`;
16948
- return errResult2(`channel_request_input dispatch failed: ${reason}`);
17445
+ return errResult(`channel_request_input dispatch failed: ${reason}`);
16949
17446
  }
16950
17447
  } catch (err) {
16951
- return errResult2(
17448
+ return errResult(
16952
17449
  `channel_request_input dispatch failed: ${err.message}`
16953
17450
  );
16954
17451
  }
@@ -17097,6 +17594,73 @@ async function ackCallbackQuery(callbackQueryId, text) {
17097
17594
  }
17098
17595
  }
17099
17596
  await mcp.connect(new StdioServerTransport());
17597
+ var REPLAY_SCAN_INTERVAL_MS = 6e4;
17598
+ async function replayPendingTelegramMarkers() {
17599
+ if (!channelReplayEnabled()) return;
17600
+ if (!PENDING_INBOUND_DIR || !existsSync2(PENDING_INBOUND_DIR)) return;
17601
+ const probe = process.env.TMUX && AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
17602
+ const sessionAlive = probe.tmux === "alive" && probe.claude === "alive";
17603
+ if (!sessionAlive) return;
17604
+ let filenames;
17605
+ try {
17606
+ filenames = readdirSync2(PENDING_INBOUND_DIR);
17607
+ } catch {
17608
+ return;
17609
+ }
17610
+ const now = Date.now();
17611
+ const entries = [];
17612
+ for (const name of filenames) {
17613
+ if (!name.endsWith(".json") || name.endsWith(".tmp")) continue;
17614
+ const fullPath = join4(PENDING_INBOUND_DIR, name);
17615
+ let marker;
17616
+ try {
17617
+ marker = JSON.parse(readFileSync3(fullPath, "utf-8"));
17618
+ } catch {
17619
+ continue;
17620
+ }
17621
+ const t = Date.parse(marker.received_at ?? "");
17622
+ const ageMs = Number.isFinite(t) ? now - t : 0;
17623
+ entries.push({ path: fullPath, marker, ageMs });
17624
+ }
17625
+ entries.sort((a, b) => b.ageMs - a.ageMs);
17626
+ for (const { path, marker, ageMs } of entries) {
17627
+ if (!shouldReplayMarker({
17628
+ enabled: true,
17629
+ hasPayload: Boolean(marker.payload),
17630
+ sessionAlive,
17631
+ markerAgeMs: ageMs,
17632
+ replayCount: marker.replay_count ?? 0
17633
+ })) {
17634
+ continue;
17635
+ }
17636
+ try {
17637
+ await mcp.notification({
17638
+ method: "notifications/claude/channel",
17639
+ params: marker.payload
17640
+ });
17641
+ } catch (err) {
17642
+ process.stderr.write(
17643
+ `telegram-channel(${AGENT_CODE_NAME}): replay push failed: ${err.message}
17644
+ `
17645
+ );
17646
+ continue;
17647
+ }
17648
+ try {
17649
+ if (existsSync2(path)) {
17650
+ const updated = {
17651
+ ...marker,
17652
+ replay_count: (marker.replay_count ?? 0) + 1
17653
+ };
17654
+ writeFileSync3(path, JSON.stringify(updated), { mode: 384 });
17655
+ }
17656
+ } catch {
17657
+ }
17658
+ }
17659
+ }
17660
+ var replayTimer = setInterval(() => {
17661
+ void replayPendingTelegramMarkers();
17662
+ }, REPLAY_SCAN_INTERVAL_MS);
17663
+ replayTimer.unref?.();
17100
17664
  var LONG_POLL_SECONDS = 50;
17101
17665
  var POLL_HTTP_TIMEOUT_MS = (LONG_POLL_SECONDS + 10) * 1e3;
17102
17666
  var TELEGRAM_DOWNLOAD_TIMEOUT_MS = 15e3;
@@ -17262,6 +17826,35 @@ async function pollLoop() {
17262
17826
  } catch (err) {
17263
17827
  process.stderr.write(
17264
17828
  `telegram-channel(${AGENT_CODE_NAME}): /restart verification-failed ack send failed: ${redactAugmentedPaths(err.message)}
17829
+ `
17830
+ );
17831
+ }
17832
+ }
17833
+ continue;
17834
+ }
17835
+ if (isInvestigateSyntax(trimmedContent)) {
17836
+ const disposition = await classifyInvestigateCommand(trimmedContent);
17837
+ if (disposition === "act") {
17838
+ await handleInvestigateCommand({
17839
+ chatId,
17840
+ messageId: String(msg.message_id),
17841
+ chatType: msg.chat.type ?? null,
17842
+ senderId: msg.from?.id != null ? String(msg.from.id) : null
17843
+ });
17844
+ } else if (disposition === "verification_failed") {
17845
+ try {
17846
+ await telegramApiCall(
17847
+ "sendMessage",
17848
+ {
17849
+ chat_id: chatId,
17850
+ text: `\u274C Couldn't verify the investigate target. Please retry in a few seconds.`,
17851
+ reply_to_message_id: Number(msg.message_id)
17852
+ },
17853
+ 1e4
17854
+ );
17855
+ } catch (err) {
17856
+ process.stderr.write(
17857
+ `telegram-channel(${AGENT_CODE_NAME}): /investigate verification-failed ack send failed: ${redactAugmentedPaths(err.message)}
17265
17858
  `
17266
17859
  );
17267
17860
  }
@@ -17370,8 +17963,29 @@ async function pollLoop() {
17370
17963
  }
17371
17964
  }
17372
17965
  const messageId = String(msg.message_id);
17373
- void setMessageReaction(chatId, messageId, ACK_EMOJI);
17374
- trackPendingMessage(chatId, messageId, msg.chat.type);
17966
+ const ackProbe = process.env.TMUX && AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
17967
+ let paneLogFreshAgeMs = null;
17968
+ if (AGENT_DIR) {
17969
+ try {
17970
+ const paneMtimeMs = statSync(join4(AGENT_DIR, "pane.log")).mtimeMs;
17971
+ paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
17972
+ } catch {
17973
+ }
17974
+ }
17975
+ const ackDecision = decideAckReaction({
17976
+ hasTarget: Boolean(chatId && messageId),
17977
+ integrationReady: Boolean(BOT_TOKEN),
17978
+ tmux: ackProbe.tmux,
17979
+ claude: ackProbe.claude,
17980
+ withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
17981
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(PENDING_INBOUND_DIR),
17982
+ paneLogFreshAgeMs
17983
+ });
17984
+ if (ackDecision === "ack") {
17985
+ void setMessageReaction(chatId, messageId, ACK_EMOJI);
17986
+ } else if (ackDecision === "undeliverable") {
17987
+ void notifyUndeliverable(chatId, messageId);
17988
+ }
17375
17989
  const fileMeta = [];
17376
17990
  for (const attachment of classifiedAttachments) {
17377
17991
  if (attachment.kind === "image") {
@@ -17399,30 +18013,38 @@ async function pollLoop() {
17399
18013
  }
17400
18014
  const downloadedImages = fileMeta.filter((f) => f.kind === "image" && typeof f.path === "string");
17401
18015
  const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
18016
+ const channelPayload = {
18017
+ content,
18018
+ meta: {
18019
+ source: "telegram",
18020
+ chat_id: chatId,
18021
+ chat_type: msg.chat.type,
18022
+ message_id: messageId,
18023
+ user: userId,
18024
+ user_name: userName,
18025
+ ts: String(msg.date),
18026
+ ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
18027
+ ...imagePath ? { image_path: imagePath } : {},
18028
+ // ENG-4902: peer-agent ingress carries distinct framing so the
18029
+ // runtime (MVP #5) can apply the peer-agent system preamble
18030
+ // and store the turn under a peer_agent memory role.
18031
+ ...peerAgentMeta ? {
18032
+ source_role: "agent",
18033
+ peer_code_name: peerAgentMeta.code_name,
18034
+ peer_agent_id: peerAgentMeta.agent_id
18035
+ } : {}
18036
+ }
18037
+ };
18038
+ trackPendingMessage(
18039
+ chatId,
18040
+ messageId,
18041
+ msg.chat.type,
18042
+ ackDecision === "undeliverable",
18043
+ channelPayload
18044
+ );
17402
18045
  await mcp.notification({
17403
18046
  method: "notifications/claude/channel",
17404
- params: {
17405
- content,
17406
- meta: {
17407
- source: "telegram",
17408
- chat_id: chatId,
17409
- chat_type: msg.chat.type,
17410
- message_id: messageId,
17411
- user: userId,
17412
- user_name: userName,
17413
- ts: String(msg.date),
17414
- ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
17415
- ...imagePath ? { image_path: imagePath } : {},
17416
- // ENG-4902: peer-agent ingress carries distinct framing so the
17417
- // runtime (MVP #5) can apply the peer-agent system preamble
17418
- // and store the turn under a peer_agent memory role.
17419
- ...peerAgentMeta ? {
17420
- source_role: "agent",
17421
- peer_code_name: peerAgentMeta.code_name,
17422
- peer_agent_id: peerAgentMeta.agent_id
17423
- } : {}
17424
- }
17425
- }
18047
+ params: channelPayload
17426
18048
  });
17427
18049
  conversationIngestClient?.ingest({
17428
18050
  channel: "telegram",
@@ -17490,6 +18112,24 @@ process.stderr.write(
17490
18112
  `telegram-channel(${AGENT_CODE_NAME}): started, long-polling getUpdates
17491
18113
  `
17492
18114
  );
18115
+ void (async () => {
18116
+ for (const registration of buildCommandRegistrations()) {
18117
+ try {
18118
+ const resp = await telegramApiCall("setMyCommands", registration, 1e4);
18119
+ if (!resp.ok) {
18120
+ process.stderr.write(
18121
+ `telegram-channel(${AGENT_CODE_NAME}): setMyCommands(${registration.scope.type}) rejected: ${resp.description ?? "unknown"}
18122
+ `
18123
+ );
18124
+ }
18125
+ } catch (err) {
18126
+ process.stderr.write(
18127
+ `telegram-channel(${AGENT_CODE_NAME}): setMyCommands(${registration.scope.type}) failed: ${redactAugmentedPaths(err.message)}
18128
+ `
18129
+ );
18130
+ }
18131
+ }
18132
+ })();
17493
18133
  pollLoop().catch((err) => {
17494
18134
  process.stderr.write(
17495
18135
  `telegram-channel(${AGENT_CODE_NAME}): poll loop died: ${err.message}
@@ -17497,3 +18137,9 @@ pollLoop().catch((err) => {
17497
18137
  );
17498
18138
  process.exit(1);
17499
18139
  });
18140
+ export {
18141
+ __resetBackOnlineNoticeThrottle,
18142
+ __resetGiveUpNoticeStateForTests,
18143
+ __resetUndeliverableNoticeThrottle,
18144
+ _resetQueuedReplyCountsForTests
18145
+ };