@integrity-labs/agt-cli 0.27.94 → 0.27.95

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.
@@ -15154,8 +15154,296 @@ function createCrossTeamPeerAuditClient(args) {
15154
15154
  };
15155
15155
  }
15156
15156
 
15157
- // src/observed-chat-client.ts
15157
+ // src/diagnostic-event-client.ts
15158
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/pane-tail.ts
15268
+ import { execFile } from "child_process";
15269
+ import { promisify } from "util";
15270
+ import { open, stat } from "fs/promises";
15271
+
15272
+ // src/session-probe-runtime.ts
15273
+ import { execFileSync } from "child_process";
15274
+ function agentTmuxSessionName(codeName) {
15275
+ return `agt-${codeName}`;
15276
+ }
15277
+ function escapePgrepRegex(value) {
15278
+ return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
15279
+ }
15280
+ function probeClaudeProcessInTmux(tmuxSession) {
15281
+ const escapedSession = escapePgrepRegex(tmuxSession);
15282
+ const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
15283
+ try {
15284
+ const out = execFileSync("pgrep", ["-f", "--", pattern], {
15285
+ encoding: "utf-8",
15286
+ timeout: 3e3
15287
+ }).trim();
15288
+ return out.length > 0 ? "alive" : "dead";
15289
+ } catch (err) {
15290
+ const e = err;
15291
+ if (e?.code === "ENOENT") return "unknown";
15292
+ return e?.status === 1 ? "dead" : "unknown";
15293
+ }
15294
+ }
15295
+ function probeTmuxSession(tmuxSession) {
15296
+ try {
15297
+ execFileSync("tmux", ["has-session", "-t", tmuxSession], {
15298
+ stdio: "ignore",
15299
+ timeout: 3e3
15300
+ });
15301
+ return "alive";
15302
+ } catch (err) {
15303
+ const e = err;
15304
+ if (e?.code === "ENOENT") return "unknown";
15305
+ return "dead";
15306
+ }
15307
+ }
15308
+ function probeAgentSession(codeName) {
15309
+ const session = agentTmuxSessionName(codeName);
15310
+ const tmux = probeTmuxSession(session);
15311
+ const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
15312
+ return { tmux, claude };
15313
+ }
15314
+ var probeCache = /* @__PURE__ */ new Map();
15315
+ var SESSION_PROBE_TTL_MS = 15e3;
15316
+ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
15317
+ const cached2 = probeCache.get(codeName);
15318
+ if (cached2 && now - cached2.at < ttlMs) return cached2.value;
15319
+ const value = probeAgentSession(codeName);
15320
+ probeCache.set(codeName, { at: now, value });
15321
+ return value;
15322
+ }
15323
+
15324
+ // src/pane-tail.ts
15325
+ var execFileAsync = promisify(execFile);
15326
+ var PANE_LOG_TAIL_BYTES = 64 * 1024;
15327
+ var TMUX_CAPTURE_TIMEOUT_MS = 2e3;
15328
+ async function capturePaneSnapshot(opts) {
15329
+ const scrollback = opts.scrollbackLines ?? 200;
15330
+ try {
15331
+ const { stdout } = await execFileAsync(
15332
+ "tmux",
15333
+ ["capture-pane", "-p", "-t", agentTmuxSessionName(opts.codeName), "-S", `-${scrollback}`],
15334
+ { timeout: TMUX_CAPTURE_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
15335
+ );
15336
+ return { source: "tmux", text: stdout };
15337
+ } catch {
15338
+ }
15339
+ if (!opts.agentDir) return null;
15340
+ const paneLogPath = `${opts.agentDir}/pane.log`;
15341
+ try {
15342
+ const tail = await readFileTail(paneLogPath, PANE_LOG_TAIL_BYTES);
15343
+ if (tail === null) return null;
15344
+ return { source: "pane-log", text: stripAnsi(tail) };
15345
+ } catch {
15346
+ return null;
15347
+ }
15348
+ }
15349
+ async function readFileTail(path, maxBytes) {
15350
+ const st = await stat(path);
15351
+ if (!st.isFile()) return null;
15352
+ const start = Math.max(0, st.size - maxBytes);
15353
+ const length = st.size - start;
15354
+ if (length === 0) return "";
15355
+ const handle = await open(path, "r");
15356
+ try {
15357
+ const buf = Buffer.alloc(length);
15358
+ await handle.read(buf, 0, length, start);
15359
+ let text = buf.toString("utf8");
15360
+ if (start > 0) {
15361
+ const nl = text.indexOf("\n");
15362
+ if (nl !== -1) text = text.slice(nl + 1);
15363
+ }
15364
+ return text;
15365
+ } finally {
15366
+ await handle.close();
15367
+ }
15368
+ }
15369
+ function stripAnsi(text) {
15370
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?<=>!]*[ -\/]*[@-~]/g, "").replace(/\x1b./g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
15371
+ }
15372
+ var SECRET_PATTERNS = [
15373
+ // Augmented host API keys
15374
+ { re: /tlk_[A-Za-z0-9]{8,}/g, label: "agt-api-key" },
15375
+ // Slack tokens: bot/app/user/refresh/config
15376
+ { re: /x(?:ox[abprs]|app)-[A-Za-z0-9-]{8,}/g, label: "slack-token" },
15377
+ // JWTs (three base64url segments, first one always starts with eyJ)
15378
+ { re: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, label: "jwt" },
15379
+ // OpenAI / Anthropic-style keys (sk-..., sk-ant-...)
15380
+ { re: /sk-[A-Za-z0-9_-]{16,}/g, label: "sk-key" },
15381
+ // AWS access key IDs
15382
+ { re: /(?:AKIA|ASIA)[0-9A-Z]{16}/g, label: "aws-key" },
15383
+ // GitHub tokens
15384
+ { re: /gh[pousr]_[A-Za-z0-9]{20,}/g, label: "github-token" }
15385
+ ];
15386
+ function redactPaneText(text) {
15387
+ let out = redactAugmentedPaths(text);
15388
+ for (const { re, label } of SECRET_PATTERNS) {
15389
+ out = out.replace(re, `<redacted:${label}>`);
15390
+ }
15391
+ return out;
15392
+ }
15393
+ var DEFAULT_MAX_CHARS = 3900;
15394
+ var REFLOW_COLS = 80;
15395
+ function formatPaneSnapshot(opts) {
15396
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
15397
+ const commandName = opts.commandName ?? "/debug";
15398
+ 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`;
15399
+ const fenceOverhead = header.length + "\n```\n".length + "\n```".length + 16;
15400
+ const contentBudget = Math.max(0, maxChars - fenceOverhead);
15401
+ const lines = reflowLines(sanitizeForCodeBlock(opts.text), REFLOW_COLS);
15402
+ const kept = [];
15403
+ let used = 0;
15404
+ for (let i = lines.length - 1; i >= 0; i--) {
15405
+ const cost = lines[i].length + 1;
15406
+ if (used + cost > contentBudget) break;
15407
+ kept.unshift(lines[i]);
15408
+ used += cost;
15409
+ }
15410
+ const truncated = kept.length < lines.length;
15411
+ const body = kept.join("\n").trimEnd();
15412
+ const block = body.length > 0 ? `\`\`\`
15413
+ ${truncated ? "\u2026\n" : ""}${body}
15414
+ \`\`\`` : "_(pane is empty)_";
15415
+ return `${header}
15416
+ ${block}`;
15417
+ }
15418
+ function formatCountdown(totalSeconds) {
15419
+ const s = Math.max(0, Math.floor(totalSeconds));
15420
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
15421
+ }
15422
+ function sanitizeForCodeBlock(text) {
15423
+ return text.replace(/```/g, "`\u200B`\u200B`");
15424
+ }
15425
+ function reflowLines(text, cols) {
15426
+ const out = [];
15427
+ for (const rawLine of text.split("\n")) {
15428
+ const line = rawLine.replace(/\s+$/, "");
15429
+ if (line.length <= cols) {
15430
+ out.push(line);
15431
+ continue;
15432
+ }
15433
+ for (let i = 0; i < line.length; i += cols) {
15434
+ out.push(line.slice(i, i + cols));
15435
+ }
15436
+ }
15437
+ const collapsed = [];
15438
+ for (const line of out) {
15439
+ if (line === "" && collapsed[collapsed.length - 1] === "") continue;
15440
+ collapsed.push(line);
15441
+ }
15442
+ return collapsed;
15443
+ }
15444
+
15445
+ // src/observed-chat-client.ts
15446
+ var REQUEST_TIMEOUT_MS3 = 1e4;
15159
15447
  function createObservedChatClient(args) {
15160
15448
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15161
15449
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15173,7 +15461,7 @@ function createObservedChatClient(args) {
15173
15461
  method: "POST",
15174
15462
  headers: { "Content-Type": "application/json" },
15175
15463
  body: JSON.stringify({ host_key: apiKey }),
15176
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15464
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15177
15465
  });
15178
15466
  if (!resp.ok) {
15179
15467
  const body = await resp.text().catch(() => "");
@@ -15190,7 +15478,7 @@ function createObservedChatClient(args) {
15190
15478
  method: "POST",
15191
15479
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15192
15480
  body: JSON.stringify({ agent_id: agentId, ...chat }),
15193
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
15481
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15194
15482
  });
15195
15483
  }
15196
15484
  async function emitAsync(chat) {
@@ -15237,7 +15525,7 @@ function createObservedChatClient(args) {
15237
15525
  }
15238
15526
 
15239
15527
  // src/conversation-ingest-client.ts
15240
- var REQUEST_TIMEOUT_MS3 = 1e4;
15528
+ var REQUEST_TIMEOUT_MS4 = 1e4;
15241
15529
  function createConversationIngestClient(args) {
15242
15530
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15243
15531
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15254,7 +15542,7 @@ function createConversationIngestClient(args) {
15254
15542
  method: "POST",
15255
15543
  headers: { "Content-Type": "application/json" },
15256
15544
  body: JSON.stringify({ host_key: apiKey }),
15257
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15545
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15258
15546
  });
15259
15547
  if (!resp.ok) {
15260
15548
  const body = await resp.text().catch(() => "");
@@ -15277,7 +15565,7 @@ function createConversationIngestClient(args) {
15277
15565
  method: "POST",
15278
15566
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15279
15567
  body: JSON.stringify({ agent_id: agentId, ...payload }),
15280
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
15568
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15281
15569
  });
15282
15570
  }
15283
15571
  function refKey(payload) {
@@ -15309,7 +15597,7 @@ function createConversationIngestClient(args) {
15309
15597
  }
15310
15598
 
15311
15599
  // src/inbound-context-client.ts
15312
- var REQUEST_TIMEOUT_MS4 = 1e4;
15600
+ var REQUEST_TIMEOUT_MS5 = 1e4;
15313
15601
  function createInboundContextClient(args) {
15314
15602
  if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
15315
15603
  const fetchImpl = args.fetchImpl ?? fetch;
@@ -15326,7 +15614,7 @@ function createInboundContextClient(args) {
15326
15614
  method: "POST",
15327
15615
  headers: { "Content-Type": "application/json" },
15328
15616
  body: JSON.stringify({ host_key: apiKey }),
15329
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15617
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5)
15330
15618
  });
15331
15619
  if (!resp.ok) {
15332
15620
  const body = await resp.text().catch(() => "");
@@ -15343,7 +15631,7 @@ function createInboundContextClient(args) {
15343
15631
  method: "POST",
15344
15632
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
15345
15633
  body: JSON.stringify(body),
15346
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS4)
15634
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5)
15347
15635
  });
15348
15636
  }
15349
15637
  async function emit(args2) {
@@ -15579,58 +15867,6 @@ function shouldReplayMarker(i) {
15579
15867
  return i.replayCount < (i.maxReplays ?? MAX_MARKER_REPLAYS);
15580
15868
  }
15581
15869
 
15582
- // src/session-probe-runtime.ts
15583
- import { execFileSync } from "child_process";
15584
- function agentTmuxSessionName(codeName) {
15585
- return `agt-${codeName}`;
15586
- }
15587
- function escapePgrepRegex(value) {
15588
- return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
15589
- }
15590
- function probeClaudeProcessInTmux(tmuxSession) {
15591
- const escapedSession = escapePgrepRegex(tmuxSession);
15592
- const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
15593
- try {
15594
- const out = execFileSync("pgrep", ["-f", "--", pattern], {
15595
- encoding: "utf-8",
15596
- timeout: 3e3
15597
- }).trim();
15598
- return out.length > 0 ? "alive" : "dead";
15599
- } catch (err) {
15600
- const e = err;
15601
- if (e?.code === "ENOENT") return "unknown";
15602
- return e?.status === 1 ? "dead" : "unknown";
15603
- }
15604
- }
15605
- function probeTmuxSession(tmuxSession) {
15606
- try {
15607
- execFileSync("tmux", ["has-session", "-t", tmuxSession], {
15608
- stdio: "ignore",
15609
- timeout: 3e3
15610
- });
15611
- return "alive";
15612
- } catch (err) {
15613
- const e = err;
15614
- if (e?.code === "ENOENT") return "unknown";
15615
- return "dead";
15616
- }
15617
- }
15618
- function probeAgentSession(codeName) {
15619
- const session = agentTmuxSessionName(codeName);
15620
- const tmux = probeTmuxSession(session);
15621
- const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
15622
- return { tmux, claude };
15623
- }
15624
- var probeCache = /* @__PURE__ */ new Map();
15625
- var SESSION_PROBE_TTL_MS = 15e3;
15626
- function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
15627
- const cached2 = probeCache.get(codeName);
15628
- if (cached2 && now - cached2.at < ttlMs) return cached2.value;
15629
- const value = probeAgentSession(codeName);
15630
- probeCache.set(codeName, { at: now, value });
15631
- return value;
15632
- }
15633
-
15634
15870
  // src/telegram-channel.ts
15635
15871
  function redactId(id) {
15636
15872
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
@@ -15644,6 +15880,9 @@ var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
15644
15880
  var ALLOWED_CHATS = new Set(
15645
15881
  (process.env.TELEGRAM_ALLOWED_CHATS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15646
15882
  );
15883
+ var DIAGNOSTIC_CHAT_IDS = new Set(
15884
+ (process.env.TELEGRAM_DIAGNOSTIC_CHAT_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
15885
+ );
15647
15886
  var PEER_CLASSIFIER_CONFIG = {
15648
15887
  peer_agent_mode: parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE),
15649
15888
  peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
@@ -15679,6 +15918,13 @@ var crossTeamPeerAuditClient = createCrossTeamPeerAuditClient({
15679
15918
  log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
15680
15919
  `)
15681
15920
  });
15921
+ var diagnosticEventClient = createDiagnosticEventClient({
15922
+ agtHost: AGT_HOST,
15923
+ agtApiKey: AGT_API_KEY,
15924
+ agentId: process.env.AGT_AGENT_ID ?? null,
15925
+ log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
15926
+ `)
15927
+ });
15682
15928
  var observedChatClient = createObservedChatClient({
15683
15929
  agtHost: AGT_HOST,
15684
15930
  agtApiKey: AGT_API_KEY,
@@ -15878,7 +16124,8 @@ function buildTelegramHelpMessage(codeName) {
15878
16124
  "",
15879
16125
  `_Type these in any chat where the bot is present (intercepted by the agent):_`,
15880
16126
  `\u2022 /help \u2014 show this help`,
15881
- `\u2022 /restart \u2014 restart this agent`
16127
+ `\u2022 /restart \u2014 restart this agent`,
16128
+ `\u2022 /investigate-${codeName} \u2014 live tail of this agent's terminal pane (DM only; team owners/admins and the agent's reports-to person)`
15882
16129
  ].join("\n");
15883
16130
  }
15884
16131
  async function handleHelpCommand(opts) {
@@ -15973,6 +16220,261 @@ async function handleRestartCommand(opts) {
15973
16220
  }
15974
16221
  }
15975
16222
  }
16223
+ var INVESTIGATE_TAIL_WINDOW_MS = 12e4;
16224
+ var INVESTIGATE_TAIL_INTERVAL_MS = 3e3;
16225
+ var INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES = 4;
16226
+ var INVESTIGATE_MAX_CHARS = 3800;
16227
+ var investigateTailSlot = createSingleTailSlot();
16228
+ function investigateAuditLog(line) {
16229
+ process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): /investigate ${line}
16230
+ `);
16231
+ }
16232
+ function investigateNowUtcLabel() {
16233
+ return `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} UTC`;
16234
+ }
16235
+ function investigateSleep(ms) {
16236
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
16237
+ }
16238
+ async function classifyInvestigateCommand(text) {
16239
+ const needsUsername = /@[A-Za-z0-9_]{1,64}(?:\s|$)/.test(text);
16240
+ const ours = needsUsername ? await resolveBotUsername() : null;
16241
+ return classifyInvestigateText(text, { codeName: AGENT_CODE_NAME, botUsername: ours });
16242
+ }
16243
+ async function sendInvestigateReply(chatId, messageId, text) {
16244
+ try {
16245
+ const resp = await telegramApiCall(
16246
+ "sendMessage",
16247
+ {
16248
+ chat_id: chatId,
16249
+ text,
16250
+ parse_mode: "Markdown",
16251
+ reply_to_message_id: Number(messageId)
16252
+ },
16253
+ 1e4
16254
+ );
16255
+ if (!resp.ok) {
16256
+ investigateAuditLog(`reply rejected by Telegram: ${resp.description ?? "unknown"}`);
16257
+ }
16258
+ } catch (err) {
16259
+ investigateAuditLog(`reply send failed: ${redactAugmentedPaths(err.message)}`);
16260
+ }
16261
+ }
16262
+ async function runInvestigateTailLoop(opts) {
16263
+ const investigateCmd = `/investigate-${AGENT_CODE_NAME}`;
16264
+ let last = opts.initialFrame;
16265
+ let consecutiveFailures = 0;
16266
+ let delayMs = INVESTIGATE_TAIL_INTERVAL_MS;
16267
+ try {
16268
+ while (Date.now() + delayMs < opts.expiresAtMs) {
16269
+ await investigateSleep(delayMs);
16270
+ delayMs = INVESTIGATE_TAIL_INTERVAL_MS;
16271
+ const snapshot = await capturePaneSnapshot({
16272
+ codeName: AGENT_CODE_NAME,
16273
+ agentDir: TELEGRAM_AGENT_DIR
16274
+ });
16275
+ if (!snapshot) {
16276
+ consecutiveFailures++;
16277
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16278
+ investigateAuditLog(`tail aborted after ${consecutiveFailures} consecutive capture failures`);
16279
+ break;
16280
+ }
16281
+ continue;
16282
+ }
16283
+ const frame = {
16284
+ body: redactPaneText(snapshot.text),
16285
+ source: snapshot.source,
16286
+ capturedAtLabel: investigateNowUtcLabel()
16287
+ };
16288
+ if (frame.body === last.body) continue;
16289
+ const text = formatPaneSnapshot({
16290
+ codeName: AGENT_CODE_NAME,
16291
+ text: frame.body,
16292
+ source: frame.source,
16293
+ capturedAtLabel: frame.capturedAtLabel,
16294
+ status: {
16295
+ kind: "live",
16296
+ secondsRemaining: Math.max(0, Math.round((opts.expiresAtMs - Date.now()) / 1e3))
16297
+ },
16298
+ maxChars: INVESTIGATE_MAX_CHARS,
16299
+ commandName: investigateCmd
16300
+ });
16301
+ try {
16302
+ const resp = await telegramApiCall(
16303
+ "editMessageText",
16304
+ {
16305
+ chat_id: opts.chatId,
16306
+ message_id: opts.messageId,
16307
+ text,
16308
+ parse_mode: "Markdown"
16309
+ },
16310
+ 1e4
16311
+ );
16312
+ if (resp.ok) {
16313
+ last = frame;
16314
+ consecutiveFailures = 0;
16315
+ continue;
16316
+ }
16317
+ const retryAfter = resp.parameters?.retry_after;
16318
+ if (typeof retryAfter === "number" && retryAfter > 0) {
16319
+ delayMs = Math.max(INVESTIGATE_TAIL_INTERVAL_MS, retryAfter * 1e3);
16320
+ continue;
16321
+ }
16322
+ consecutiveFailures++;
16323
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16324
+ investigateAuditLog(
16325
+ `tail aborted after ${consecutiveFailures} consecutive update failures (last: ${resp.description ?? "unknown"})`
16326
+ );
16327
+ break;
16328
+ }
16329
+ } catch (err) {
16330
+ consecutiveFailures++;
16331
+ if (consecutiveFailures >= INVESTIGATE_TAIL_MAX_CONSECUTIVE_FAILURES) {
16332
+ investigateAuditLog(
16333
+ `tail aborted after ${consecutiveFailures} consecutive update failures (last: ${redactAugmentedPaths(err.message)})`
16334
+ );
16335
+ break;
16336
+ }
16337
+ }
16338
+ }
16339
+ } finally {
16340
+ investigateTailSlot.release();
16341
+ try {
16342
+ await telegramApiCall(
16343
+ "editMessageText",
16344
+ {
16345
+ chat_id: opts.chatId,
16346
+ message_id: opts.messageId,
16347
+ text: formatPaneSnapshot({
16348
+ codeName: AGENT_CODE_NAME,
16349
+ text: last.body,
16350
+ source: last.source,
16351
+ capturedAtLabel: last.capturedAtLabel,
16352
+ status: { kind: "expired" },
16353
+ maxChars: INVESTIGATE_MAX_CHARS,
16354
+ commandName: investigateCmd
16355
+ }),
16356
+ parse_mode: "Markdown"
16357
+ },
16358
+ 1e4
16359
+ );
16360
+ } catch (err) {
16361
+ investigateAuditLog(`expired-edit failed: ${redactAugmentedPaths(err.message)}`);
16362
+ }
16363
+ }
16364
+ }
16365
+ async function handleInvestigateCommand(opts) {
16366
+ const investigateCmd = `/investigate-${AGENT_CODE_NAME}`;
16367
+ const emitAudit = (outcome, reason) => {
16368
+ diagnosticEventClient?.emit({
16369
+ channel: "telegram",
16370
+ command: "investigate",
16371
+ outcome,
16372
+ reason,
16373
+ chat_id: opts.chatId,
16374
+ sender_chat_id: opts.senderId
16375
+ });
16376
+ };
16377
+ const verdict = evaluateInvestigateGate({
16378
+ chatType: opts.chatType,
16379
+ senderId: opts.senderId,
16380
+ diagnosticChatIds: DIAGNOSTIC_CHAT_IDS
16381
+ });
16382
+ if (!verdict.ok) {
16383
+ investigateAuditLog(
16384
+ `denied reason=${verdict.reason} sender=${opts.senderId ? redactId(opts.senderId) : "unknown"}`
16385
+ );
16386
+ emitAudit("denied", verdict.reason);
16387
+ 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.`;
16388
+ await sendInvestigateReply(opts.chatId, opts.messageId, denialText);
16389
+ return;
16390
+ }
16391
+ const reservedExpiresAtMs = investigateTailSlot.reserve(INVESTIGATE_TAIL_WINDOW_MS);
16392
+ if (reservedExpiresAtMs === null) {
16393
+ const remaining = formatCountdown(investigateTailSlot.remainingMs() / 1e3);
16394
+ investigateAuditLog(
16395
+ `not-started reason=tail-already-running sender=${redactId(opts.senderId ?? "unknown")} remaining=${remaining}`
16396
+ );
16397
+ emitAudit("not_started", "tail-already-running");
16398
+ await sendInvestigateReply(
16399
+ opts.chatId,
16400
+ opts.messageId,
16401
+ `\u23F3 A \`${investigateCmd}\` tail is already running for \`${AGENT_CODE_NAME}\` (one at a time) \u2014 it expires in ${remaining}.`
16402
+ );
16403
+ return;
16404
+ }
16405
+ const snapshot = await capturePaneSnapshot({
16406
+ codeName: AGENT_CODE_NAME,
16407
+ agentDir: TELEGRAM_AGENT_DIR
16408
+ });
16409
+ if (!snapshot) {
16410
+ investigateTailSlot.release();
16411
+ investigateAuditLog(`not-started reason=no-pane-output sender=${redactId(opts.senderId ?? "unknown")}`);
16412
+ emitAudit("not_started", "no-pane-output");
16413
+ await sendInvestigateReply(
16414
+ opts.chatId,
16415
+ opts.messageId,
16416
+ `\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.)`
16417
+ );
16418
+ return;
16419
+ }
16420
+ investigateAuditLog(`granted sender=${redactId(opts.senderId ?? "unknown")} \u2014 starting live tail (source=${snapshot.source})`);
16421
+ emitAudit("granted", null);
16422
+ const initialFrame = {
16423
+ body: redactPaneText(snapshot.text),
16424
+ source: snapshot.source,
16425
+ capturedAtLabel: investigateNowUtcLabel()
16426
+ };
16427
+ let postedMessageId = null;
16428
+ try {
16429
+ const posted = await telegramApiCall(
16430
+ "sendMessage",
16431
+ {
16432
+ chat_id: opts.chatId,
16433
+ text: formatPaneSnapshot({
16434
+ codeName: AGENT_CODE_NAME,
16435
+ text: initialFrame.body,
16436
+ source: initialFrame.source,
16437
+ capturedAtLabel: initialFrame.capturedAtLabel,
16438
+ status: {
16439
+ kind: "live",
16440
+ secondsRemaining: Math.max(0, Math.round((reservedExpiresAtMs - Date.now()) / 1e3))
16441
+ },
16442
+ maxChars: INVESTIGATE_MAX_CHARS,
16443
+ commandName: investigateCmd
16444
+ }),
16445
+ parse_mode: "Markdown",
16446
+ reply_to_message_id: Number(opts.messageId)
16447
+ },
16448
+ 1e4
16449
+ );
16450
+ const result = posted.result;
16451
+ if (posted.ok && typeof result?.message_id === "number") {
16452
+ postedMessageId = result.message_id;
16453
+ } else {
16454
+ investigateAuditLog(`not-started reason=post-failed error=${posted.description ?? "unknown"}`);
16455
+ }
16456
+ } catch (err) {
16457
+ investigateAuditLog(`not-started reason=post-failed error=${redactAugmentedPaths(err.message)}`);
16458
+ }
16459
+ if (postedMessageId === null) {
16460
+ investigateTailSlot.release();
16461
+ await sendInvestigateReply(
16462
+ opts.chatId,
16463
+ opts.messageId,
16464
+ `\u274C Failed to post the pane snapshot. Try again in a moment.`
16465
+ );
16466
+ return;
16467
+ }
16468
+ void runInvestigateTailLoop({
16469
+ chatId: opts.chatId,
16470
+ messageId: postedMessageId,
16471
+ initialFrame,
16472
+ expiresAtMs: reservedExpiresAtMs
16473
+ }).catch((err) => {
16474
+ investigateTailSlot.release();
16475
+ investigateAuditLog(`tail loop crashed: ${redactAugmentedPaths(err.message)}`);
16476
+ });
16477
+ }
15976
16478
  var cachedBotUsername = null;
15977
16479
  var cachedBotId = null;
15978
16480
  async function refreshBotIdentity() {
@@ -17204,6 +17706,35 @@ async function pollLoop() {
17204
17706
  } catch (err) {
17205
17707
  process.stderr.write(
17206
17708
  `telegram-channel(${AGENT_CODE_NAME}): /restart verification-failed ack send failed: ${redactAugmentedPaths(err.message)}
17709
+ `
17710
+ );
17711
+ }
17712
+ }
17713
+ continue;
17714
+ }
17715
+ if (isInvestigateSyntax(trimmedContent)) {
17716
+ const disposition = await classifyInvestigateCommand(trimmedContent);
17717
+ if (disposition === "act") {
17718
+ await handleInvestigateCommand({
17719
+ chatId,
17720
+ messageId: String(msg.message_id),
17721
+ chatType: msg.chat.type ?? null,
17722
+ senderId: msg.from?.id != null ? String(msg.from.id) : null
17723
+ });
17724
+ } else if (disposition === "verification_failed") {
17725
+ try {
17726
+ await telegramApiCall(
17727
+ "sendMessage",
17728
+ {
17729
+ chat_id: chatId,
17730
+ text: `\u274C Couldn't verify the investigate target. Please retry in a few seconds.`,
17731
+ reply_to_message_id: Number(msg.message_id)
17732
+ },
17733
+ 1e4
17734
+ );
17735
+ } catch (err) {
17736
+ process.stderr.write(
17737
+ `telegram-channel(${AGENT_CODE_NAME}): /investigate verification-failed ack send failed: ${redactAugmentedPaths(err.message)}
17207
17738
  `
17208
17739
  );
17209
17740
  }