@integrity-labs/agt-cli 0.27.8-test.9 → 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.
@@ -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,
@@ -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" };
14558
14538
  }
14539
+ if (input.queuedCount >= maxDepth) {
14540
+ return { action: "refuse", recentCount: inWindow.length, reason: "queue_full" };
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) {
@@ -15506,6 +15805,7 @@ import { readdirSync, readFileSync as readFileSync2 } from "fs";
15506
15805
  import { join as join3 } from "path";
15507
15806
  var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
15508
15807
  var ACK_STARTUP_GRACE_MS = 6e4;
15808
+ var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
15509
15809
  function decideAckReaction(i) {
15510
15810
  if (!i.hasTarget) return "none";
15511
15811
  if (!i.integrationReady) return "undeliverable";
@@ -15513,6 +15813,11 @@ function decideAckReaction(i) {
15513
15813
  if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
15514
15814
  const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
15515
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
+ }
15516
15821
  return "undeliverable";
15517
15822
  }
15518
15823
  return "ack";
@@ -15520,6 +15825,37 @@ function decideAckReaction(i) {
15520
15825
  function decideRecoveryHeal(i) {
15521
15826
  return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
15522
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
+ }
15523
15859
  function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
15524
15860
  if (!dir) return null;
15525
15861
  let names;
@@ -15547,57 +15883,32 @@ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
15547
15883
  }
15548
15884
  return oldest;
15549
15885
  }
15550
-
15551
- // src/session-probe-runtime.ts
15552
- import { execFileSync } from "child_process";
15553
- function agentTmuxSessionName(codeName) {
15554
- return `agt-${codeName}`;
15555
- }
15556
- function escapePgrepRegex(value) {
15557
- return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
15558
- }
15559
- function probeClaudeProcessInTmux(tmuxSession) {
15560
- const escapedSession = escapePgrepRegex(tmuxSession);
15561
- const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
15562
- try {
15563
- const out = execFileSync("pgrep", ["-f", "--", pattern], {
15564
- encoding: "utf-8",
15565
- timeout: 3e3
15566
- }).trim();
15567
- return out.length > 0 ? "alive" : "dead";
15568
- } catch (err) {
15569
- const e = err;
15570
- if (e?.code === "ENOENT") return "unknown";
15571
- return e?.status === 1 ? "dead" : "unknown";
15572
- }
15573
- }
15574
- function probeTmuxSession(tmuxSession) {
15575
- try {
15576
- execFileSync("tmux", ["has-session", "-t", tmuxSession], {
15577
- stdio: "ignore",
15578
- timeout: 3e3
15579
- });
15580
- return "alive";
15581
- } catch (err) {
15582
- const e = err;
15583
- if (e?.code === "ENOENT") return "unknown";
15584
- return "dead";
15585
- }
15586
- }
15587
- function probeAgentSession(codeName) {
15588
- const session = agentTmuxSessionName(codeName);
15589
- const tmux = probeTmuxSession(session);
15590
- const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
15591
- return { tmux, claude };
15592
- }
15593
- var probeCache = /* @__PURE__ */ new Map();
15594
- var SESSION_PROBE_TTL_MS = 15e3;
15595
- function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
15596
- const cached2 = probeCache.get(codeName);
15597
- if (cached2 && now - cached2.at < ttlMs) return cached2.value;
15598
- const value = probeAgentSession(codeName);
15599
- probeCache.set(codeName, { at: now, value });
15600
- return value;
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);
15601
15912
  }
15602
15913
 
15603
15914
  // src/telegram-channel.ts
@@ -15613,6 +15924,9 @@ var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
15613
15924
  var ALLOWED_CHATS = new Set(
15614
15925
  (process.env.TELEGRAM_ALLOWED_CHATS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15615
15926
  );
15927
+ var DIAGNOSTIC_CHAT_IDS = new Set(
15928
+ (process.env.TELEGRAM_DIAGNOSTIC_CHAT_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15929
+ );
15616
15930
  var PEER_CLASSIFIER_CONFIG = {
15617
15931
  peer_agent_mode: parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE),
15618
15932
  peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
@@ -15648,6 +15962,13 @@ var crossTeamPeerAuditClient = createCrossTeamPeerAuditClient({
15648
15962
  log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
15649
15963
  `)
15650
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
+ });
15651
15972
  var observedChatClient = createObservedChatClient({
15652
15973
  agtHost: AGT_HOST,
15653
15974
  agtApiKey: AGT_API_KEY,
@@ -15773,20 +16094,19 @@ async function setMessageReaction(chatId, messageId, emoji2) {
15773
16094
  );
15774
16095
  }
15775
16096
  }
15776
- var UNDELIVERABLE_NOTICE_THROTTLE_MS = 5 * 60 * 1e3;
15777
16097
  var lastUndeliverableNoticeAt = /* @__PURE__ */ new Map();
15778
16098
  async function notifyUndeliverable(chatId, messageId) {
15779
16099
  const key2 = String(chatId);
15780
16100
  const now = Date.now();
15781
- const last = lastUndeliverableNoticeAt.get(key2);
15782
- if (last != null && now - last < UNDELIVERABLE_NOTICE_THROTTLE_MS) return;
16101
+ if (!shouldPostUndeliverableNotice(lastUndeliverableNoticeAt.get(key2), now)) return;
15783
16102
  lastUndeliverableNoticeAt.set(key2, now);
15784
16103
  try {
15785
16104
  const resp = await telegramApiCall(
15786
16105
  "sendMessage",
15787
16106
  {
15788
16107
  chat_id: chatId,
15789
- text: "\u26A0\uFE0F I can't respond right now \u2014 I'll follow up once I'm back.",
16108
+ // ENG-6025: shared copy with the Slack undeliverable notice.
16109
+ text: undeliverableNoticeText(),
15790
16110
  reply_to_message_id: Number(messageId),
15791
16111
  allow_sending_without_reply: true
15792
16112
  },
@@ -15812,8 +16132,7 @@ var lastBackOnlineNoticeAt = /* @__PURE__ */ new Map();
15812
16132
  function notifyBackOnline(chatId) {
15813
16133
  const key2 = String(chatId);
15814
16134
  const now = Date.now();
15815
- const last = lastBackOnlineNoticeAt.get(key2);
15816
- if (last != null && now - last < UNDELIVERABLE_NOTICE_THROTTLE_MS) return;
16135
+ if (!shouldPostUndeliverableNotice(lastBackOnlineNoticeAt.get(key2), now)) return;
15817
16136
  lastBackOnlineNoticeAt.set(key2, now);
15818
16137
  void (async () => {
15819
16138
  try {
@@ -15849,7 +16168,8 @@ function buildTelegramHelpMessage(codeName) {
15849
16168
  "",
15850
16169
  `_Type these in any chat where the bot is present (intercepted by the agent):_`,
15851
16170
  `\u2022 /help \u2014 show this help`,
15852
- `\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)`
15853
16173
  ].join("\n");
15854
16174
  }
15855
16175
  async function handleHelpCommand(opts) {
@@ -15944,6 +16264,261 @@ async function handleRestartCommand(opts) {
15944
16264
  }
15945
16265
  }
15946
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
+ }
15947
16522
  var cachedBotUsername = null;
15948
16523
  var cachedBotId = null;
15949
16524
  async function refreshBotIdentity() {
@@ -16009,7 +16584,7 @@ function pendingInboundPath(chatId, messageId) {
16009
16584
  if (!PENDING_INBOUND_DIR) return null;
16010
16585
  return join4(PENDING_INBOUND_DIR, safeMarkerName(chatId, messageId));
16011
16586
  }
16012
- function writePendingInboundMarker(chatId, messageId, chatType, undeliverable = false) {
16587
+ function writePendingInboundMarker(chatId, messageId, chatType, undeliverable = false, payload) {
16013
16588
  const path = pendingInboundPath(chatId, messageId);
16014
16589
  if (!path || !PENDING_INBOUND_DIR) return;
16015
16590
  const marker = {
@@ -16018,7 +16593,10 @@ function writePendingInboundMarker(chatId, messageId, chatType, undeliverable =
16018
16593
  chat_type: chatType,
16019
16594
  received_at: (/* @__PURE__ */ new Date()).toISOString(),
16020
16595
  // Only persist the flag when set — keeps healthy-path markers byte-identical.
16021
- ...undeliverable ? { undeliverable: true } : {}
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 } : {}
16022
16600
  };
16023
16601
  try {
16024
16602
  mkdirSync3(PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
@@ -16219,10 +16797,10 @@ function startRecoveryOutboxWatcher() {
16219
16797
  }
16220
16798
  startRecoveryOutboxWatcher();
16221
16799
  var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
16222
- function trackPendingMessage(chatId, messageId, chatType, undeliverable = false) {
16223
- writePendingInboundMarker(chatId, messageId, chatType, undeliverable);
16800
+ function trackPendingMessage(chatId, messageId, chatType, undeliverable = false, payload) {
16801
+ writePendingInboundMarker(chatId, messageId, chatType, undeliverable, payload);
16224
16802
  }
16225
- function sweepTelegramStaleMarkersOnBoot() {
16803
+ function sweepTelegramStaleMarkers(thresholdMs) {
16226
16804
  if (!PENDING_INBOUND_DIR) return;
16227
16805
  if (!existsSync2(PENDING_INBOUND_DIR)) return;
16228
16806
  let filenames;
@@ -16257,31 +16835,152 @@ function sweepTelegramStaleMarkersOnBoot() {
16257
16835
  continue;
16258
16836
  }
16259
16837
  const { chat_id, message_id, received_at } = marker;
16260
- if (!chat_id || !message_id || !received_at) {
16838
+ if (!chat_id || !message_id || isPendingMarkerStale(received_at, now, thresholdMs)) {
16261
16839
  try {
16262
16840
  unlinkSync3(fullPath);
16263
16841
  } catch {
16264
16842
  }
16265
16843
  cleared++;
16266
- continue;
16267
16844
  }
16268
- const receivedAtMs = Date.parse(received_at);
16269
- 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;
16270
16868
  try {
16271
- 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);
16272
16873
  } catch {
16273
16874
  }
16274
- cleared++;
16275
16875
  }
16876
+ } catch {
16877
+ return [];
16276
16878
  }
16277
- 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
+ }
16278
16898
  process.stderr.write(
16279
- `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}
16280
16980
  `
16281
16981
  );
16282
16982
  }
16283
16983
  }
16284
- sweepTelegramStaleMarkersOnBoot();
16285
16984
  function clearPendingMessage(chatId, messageId) {
16286
16985
  if (messageId) {
16287
16986
  clearPendingInboundMarker(chatId, messageId);
@@ -16515,28 +17214,56 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16515
17214
  isError: true
16516
17215
  };
16517
17216
  }
16518
- const tgThrottleCfg = configFromEnv();
17217
+ const isPrivateChat = !chat_id.startsWith("-");
17218
+ const tgThrottleCfg = isPrivateChat ? relaxedThrottleConfig(configFromEnv()) : configFromEnv();
16519
17219
  const tgThrottleNow = Date.now();
16520
- const tgThrottleDecision = decideReplyThrottle({
17220
+ const tgPlan = planReplyDelivery({
16521
17221
  recentReplyTimestamps: getRecentReplies(chat_id, "", tgThrottleNow, tgThrottleCfg),
17222
+ queuedCount: queuedReplyCounts.get(chat_id) ?? 0,
16522
17223
  now: tgThrottleNow,
16523
17224
  config: tgThrottleCfg
16524
17225
  });
16525
- if (!tgThrottleDecision.allow) {
17226
+ if (tgPlan.action === "refuse") {
16526
17227
  process.stderr.write(
16527
- `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}
16528
17229
  `
16529
17230
  );
16530
17231
  return {
16531
17232
  content: [
16532
17233
  {
16533
17234
  type: "text",
16534
- 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.`
16535
17236
  }
16536
17237
  ],
16537
17238
  isError: true
16538
17239
  };
16539
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
+ }
16540
17267
  try {
16541
17268
  const body = { chat_id, text };
16542
17269
  if (reply_to_message_id) body.reply_to_message_id = Number(reply_to_message_id);
@@ -16867,6 +17594,73 @@ async function ackCallbackQuery(callbackQueryId, text) {
16867
17594
  }
16868
17595
  }
16869
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?.();
16870
17664
  var LONG_POLL_SECONDS = 50;
16871
17665
  var POLL_HTTP_TIMEOUT_MS = (LONG_POLL_SECONDS + 10) * 1e3;
16872
17666
  var TELEGRAM_DOWNLOAD_TIMEOUT_MS = 15e3;
@@ -17032,6 +17826,35 @@ async function pollLoop() {
17032
17826
  } catch (err) {
17033
17827
  process.stderr.write(
17034
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)}
17035
17858
  `
17036
17859
  );
17037
17860
  }
@@ -17141,20 +17964,28 @@ async function pollLoop() {
17141
17964
  }
17142
17965
  const messageId = String(msg.message_id);
17143
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
+ }
17144
17975
  const ackDecision = decideAckReaction({
17145
17976
  hasTarget: Boolean(chatId && messageId),
17146
17977
  integrationReady: Boolean(BOT_TOKEN),
17147
17978
  tmux: ackProbe.tmux,
17148
17979
  claude: ackProbe.claude,
17149
17980
  withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
17150
- oldestPendingAgeMs: oldestPendingMarkerAgeMs(PENDING_INBOUND_DIR)
17981
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(PENDING_INBOUND_DIR),
17982
+ paneLogFreshAgeMs
17151
17983
  });
17152
17984
  if (ackDecision === "ack") {
17153
17985
  void setMessageReaction(chatId, messageId, ACK_EMOJI);
17154
17986
  } else if (ackDecision === "undeliverable") {
17155
17987
  void notifyUndeliverable(chatId, messageId);
17156
17988
  }
17157
- trackPendingMessage(chatId, messageId, msg.chat.type, ackDecision === "undeliverable");
17158
17989
  const fileMeta = [];
17159
17990
  for (const attachment of classifiedAttachments) {
17160
17991
  if (attachment.kind === "image") {
@@ -17182,30 +18013,38 @@ async function pollLoop() {
17182
18013
  }
17183
18014
  const downloadedImages = fileMeta.filter((f) => f.kind === "image" && typeof f.path === "string");
17184
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
+ );
17185
18045
  await mcp.notification({
17186
18046
  method: "notifications/claude/channel",
17187
- params: {
17188
- content,
17189
- meta: {
17190
- source: "telegram",
17191
- chat_id: chatId,
17192
- chat_type: msg.chat.type,
17193
- message_id: messageId,
17194
- user: userId,
17195
- user_name: userName,
17196
- ts: String(msg.date),
17197
- ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
17198
- ...imagePath ? { image_path: imagePath } : {},
17199
- // ENG-4902: peer-agent ingress carries distinct framing so the
17200
- // runtime (MVP #5) can apply the peer-agent system preamble
17201
- // and store the turn under a peer_agent memory role.
17202
- ...peerAgentMeta ? {
17203
- source_role: "agent",
17204
- peer_code_name: peerAgentMeta.code_name,
17205
- peer_agent_id: peerAgentMeta.agent_id
17206
- } : {}
17207
- }
17208
- }
18047
+ params: channelPayload
17209
18048
  });
17210
18049
  conversationIngestClient?.ingest({
17211
18050
  channel: "telegram",
@@ -17273,6 +18112,24 @@ process.stderr.write(
17273
18112
  `telegram-channel(${AGENT_CODE_NAME}): started, long-polling getUpdates
17274
18113
  `
17275
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
+ })();
17276
18133
  pollLoop().catch((err) => {
17277
18134
  process.stderr.write(
17278
18135
  `telegram-channel(${AGENT_CODE_NAME}): poll loop died: ${err.message}
@@ -17282,5 +18139,7 @@ pollLoop().catch((err) => {
17282
18139
  });
17283
18140
  export {
17284
18141
  __resetBackOnlineNoticeThrottle,
17285
- __resetUndeliverableNoticeThrottle
18142
+ __resetGiveUpNoticeStateForTests,
18143
+ __resetUndeliverableNoticeThrottle,
18144
+ _resetQueuedReplyCountsForTests
17286
18145
  };