@integrity-labs/agt-cli 0.15.7 → 0.15.9

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.
@@ -13863,7 +13863,21 @@ var StdioServerTransport = class {
13863
13863
 
13864
13864
  // src/telegram-channel.ts
13865
13865
  import https from "https";
13866
- import { createHash } from "crypto";
13866
+ import { createHash, randomUUID } from "crypto";
13867
+ import {
13868
+ createWriteStream,
13869
+ existsSync,
13870
+ mkdirSync as mkdirSync2,
13871
+ readFileSync,
13872
+ readdirSync,
13873
+ renameSync as renameSync2,
13874
+ statSync,
13875
+ unlinkSync as unlinkSync2,
13876
+ watch,
13877
+ writeFileSync as writeFileSync2
13878
+ } from "fs";
13879
+ import { homedir as homedir2 } from "os";
13880
+ import { join as join2 } from "path";
13867
13881
 
13868
13882
  // src/channel-attachments.ts
13869
13883
  import { homedir } from "os";
@@ -14110,6 +14124,28 @@ if (!BOT_TOKEN) {
14110
14124
  );
14111
14125
  process.exit(1);
14112
14126
  }
14127
+ var stderrLogStream = null;
14128
+ if (AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown") {
14129
+ try {
14130
+ const logDir = join2(homedir2(), ".augmented", AGENT_CODE_NAME);
14131
+ mkdirSync2(logDir, { recursive: true });
14132
+ stderrLogStream = createWriteStream(join2(logDir, "telegram-channel-stderr.log"), {
14133
+ flags: "a",
14134
+ mode: 384
14135
+ });
14136
+ const origWrite = process.stderr.write.bind(process.stderr);
14137
+ process.stderr.write = (chunk, ...rest) => {
14138
+ try {
14139
+ const buf = typeof chunk === "string" ? chunk : String(chunk);
14140
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
14141
+ stderrLogStream?.write(`[${ts}] ${buf}${buf.endsWith("\n") ? "" : "\n"}`);
14142
+ } catch {
14143
+ }
14144
+ return origWrite(chunk, ...rest);
14145
+ };
14146
+ } catch {
14147
+ }
14148
+ }
14113
14149
  function telegramApiCall(method, body, timeoutMs) {
14114
14150
  return new Promise((resolve2, reject) => {
14115
14151
  const postData = JSON.stringify(body);
@@ -14150,8 +14186,7 @@ function telegramApiCall(method, body, timeoutMs) {
14150
14186
  });
14151
14187
  }
14152
14188
  var ACK_EMOJI = "\u{1F440}";
14153
- var TIMEOUT_EMOJI = "\u{1F494}";
14154
- var RESPONSE_TIMEOUT_MS = 12e4;
14189
+ var RESPONSE_TIMEOUT_MS = 3e5;
14155
14190
  async function setMessageReaction(chatId, messageId, emoji2) {
14156
14191
  try {
14157
14192
  const resp = await telegramApiCall(
@@ -14176,14 +14211,343 @@ async function setMessageReaction(chatId, messageId, emoji2) {
14176
14211
  );
14177
14212
  }
14178
14213
  }
14214
+ var RESTART_FLAGS_DIR = join2(homedir2(), ".augmented", "restart-flags");
14215
+ async function handleRestartCommand(opts) {
14216
+ try {
14217
+ if (!existsSync(RESTART_FLAGS_DIR)) {
14218
+ mkdirSync2(RESTART_FLAGS_DIR, { recursive: true });
14219
+ }
14220
+ const flagPath = join2(RESTART_FLAGS_DIR, `${AGENT_CODE_NAME}.flag`);
14221
+ const flag = {
14222
+ codeName: AGENT_CODE_NAME,
14223
+ source: "telegram",
14224
+ ts: Date.now(),
14225
+ reply: { chat_id: opts.chatId, message_id: opts.messageId }
14226
+ };
14227
+ const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
14228
+ writeFileSync2(tmpPath, JSON.stringify(flag) + "\n", "utf8");
14229
+ renameSync2(tmpPath, flagPath);
14230
+ process.stderr.write(
14231
+ `telegram-channel(${AGENT_CODE_NAME}): /restart queued from chat ${redactId(opts.chatId)}
14232
+ `
14233
+ );
14234
+ try {
14235
+ const ack = await telegramApiCall(
14236
+ "sendMessage",
14237
+ {
14238
+ chat_id: opts.chatId,
14239
+ text: `\u23F3 Restart queued for \`${AGENT_CODE_NAME}\`. The session will reconnect in a few seconds.`,
14240
+ reply_to_message_id: Number(opts.messageId),
14241
+ parse_mode: "Markdown"
14242
+ },
14243
+ 1e4
14244
+ );
14245
+ if (!ack.ok) {
14246
+ process.stderr.write(
14247
+ `telegram-channel(${AGENT_CODE_NAME}): /restart ack rejected by Telegram (chat ${redactId(opts.chatId)}): ${ack.description ?? "unknown"}
14248
+ `
14249
+ );
14250
+ }
14251
+ } catch (err) {
14252
+ process.stderr.write(
14253
+ `telegram-channel(${AGENT_CODE_NAME}): /restart ack send failed: ${redactAugmentedPaths(err.message)}
14254
+ `
14255
+ );
14256
+ }
14257
+ } catch (err) {
14258
+ process.stderr.write(
14259
+ `telegram-channel(${AGENT_CODE_NAME}): /restart flag write failed: ${redactAugmentedPaths(err.message)}
14260
+ `
14261
+ );
14262
+ try {
14263
+ const errAck = await telegramApiCall(
14264
+ "sendMessage",
14265
+ {
14266
+ chat_id: opts.chatId,
14267
+ text: `\u274C Failed to queue restart for ${AGENT_CODE_NAME}. Please try again in a few seconds.`,
14268
+ reply_to_message_id: Number(opts.messageId)
14269
+ },
14270
+ 1e4
14271
+ );
14272
+ if (!errAck.ok) {
14273
+ process.stderr.write(
14274
+ `telegram-channel(${AGENT_CODE_NAME}): /restart error-ack rejected by Telegram (chat ${redactId(opts.chatId)}): ${errAck.description ?? "unknown"}
14275
+ `
14276
+ );
14277
+ }
14278
+ } catch {
14279
+ }
14280
+ }
14281
+ }
14282
+ var cachedBotUsername = null;
14283
+ async function resolveBotUsername() {
14284
+ if (cachedBotUsername !== null) return cachedBotUsername;
14285
+ try {
14286
+ const resp = await telegramApiCall("getMe", {}, 5e3);
14287
+ if (resp.ok && resp.result && typeof resp.result.username === "string") {
14288
+ cachedBotUsername = resp.result.username.toLowerCase();
14289
+ return cachedBotUsername;
14290
+ }
14291
+ process.stderr.write(
14292
+ `telegram-channel(${AGENT_CODE_NAME}): getMe rejected: ${resp.description ?? "unknown"} \u2014 will retry on next /restart@<bot>
14293
+ `
14294
+ );
14295
+ } catch (err) {
14296
+ process.stderr.write(
14297
+ `telegram-channel(${AGENT_CODE_NAME}): getMe failed: ${redactAugmentedPaths(err.message)} \u2014 will retry on next /restart@<bot>
14298
+ `
14299
+ );
14300
+ }
14301
+ return null;
14302
+ }
14303
+ var RESTART_SYNTAX_RE = /^\/restart(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
14304
+ function isRestartSyntax(text) {
14305
+ return RESTART_SYNTAX_RE.test(text);
14306
+ }
14307
+ async function classifyRestartCommand(text) {
14308
+ const m = RESTART_SYNTAX_RE.exec(text);
14309
+ if (!m) return "ignore";
14310
+ const target = m[1]?.toLowerCase();
14311
+ if (!target) return "act";
14312
+ const ours = await resolveBotUsername();
14313
+ if (!ours) return "verification_failed";
14314
+ return target === ours ? "act" : "ignore";
14315
+ }
14179
14316
  var pendingMessages = /* @__PURE__ */ new Map();
14317
+ var AGENT_DIR = AGENT_CODE_NAME && AGENT_CODE_NAME !== "unknown" ? join2(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
14318
+ var PENDING_INBOUND_DIR = AGENT_DIR ? join2(AGENT_DIR, "telegram-pending-inbound") : null;
14319
+ var RECOVERY_OUTBOX_DIR = AGENT_DIR ? join2(AGENT_DIR, "telegram-recovery-outbox") : null;
14320
+ function safeMarkerName(chatId, messageId) {
14321
+ const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
14322
+ return `${safe(chatId)}__${safe(messageId)}.json`;
14323
+ }
14324
+ function pendingInboundPath(chatId, messageId) {
14325
+ if (!PENDING_INBOUND_DIR) return null;
14326
+ return join2(PENDING_INBOUND_DIR, safeMarkerName(chatId, messageId));
14327
+ }
14328
+ function writePendingInboundMarker(chatId, messageId, chatType) {
14329
+ const path = pendingInboundPath(chatId, messageId);
14330
+ if (!path || !PENDING_INBOUND_DIR) return;
14331
+ const marker = {
14332
+ chat_id: chatId,
14333
+ message_id: messageId,
14334
+ chat_type: chatType,
14335
+ received_at: (/* @__PURE__ */ new Date()).toISOString()
14336
+ };
14337
+ try {
14338
+ mkdirSync2(PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
14339
+ writeFileSync2(path, JSON.stringify(marker), { mode: 384 });
14340
+ } catch (err) {
14341
+ process.stderr.write(
14342
+ `telegram-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
14343
+ `
14344
+ );
14345
+ }
14346
+ }
14347
+ function clearPendingInboundMarker(chatId, messageId) {
14348
+ const path = pendingInboundPath(chatId, messageId);
14349
+ if (!path) return;
14350
+ try {
14351
+ if (existsSync(path)) unlinkSync2(path);
14352
+ } catch {
14353
+ }
14354
+ }
14355
+ var MAX_RECOVERY_ATTEMPTS = 3;
14356
+ function nextRetryName(filename) {
14357
+ const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
14358
+ if (!match) return null;
14359
+ const base = match[1];
14360
+ const prev = match[2] ? parseInt(match[2], 10) : 0;
14361
+ const attempt = prev + 1;
14362
+ if (attempt >= MAX_RECOVERY_ATTEMPTS) {
14363
+ return { next: `${base}.retry-${attempt}.poison.json`, attempt };
14364
+ }
14365
+ return { next: `${base}.retry-${attempt}.json`, attempt };
14366
+ }
14367
+ async function processRecoveryOutboxFile(filename) {
14368
+ if (!RECOVERY_OUTBOX_DIR) return;
14369
+ if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
14370
+ const fullPath = join2(RECOVERY_OUTBOX_DIR, filename);
14371
+ let payload;
14372
+ try {
14373
+ const raw = readFileSync(fullPath, "utf-8");
14374
+ payload = JSON.parse(raw);
14375
+ } catch (err) {
14376
+ process.stderr.write(
14377
+ `telegram-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
14378
+ `
14379
+ );
14380
+ try {
14381
+ renameSync2(fullPath, `${fullPath}.parse-error.poison`);
14382
+ } catch {
14383
+ }
14384
+ return;
14385
+ }
14386
+ if (!payload.chat_id || !payload.text) {
14387
+ process.stderr.write(
14388
+ `telegram-channel(${AGENT_CODE_NAME}): recovery outbox malformed (${filename}): missing chat_id or text
14389
+ `
14390
+ );
14391
+ try {
14392
+ renameSync2(fullPath, `${fullPath}.malformed.poison`);
14393
+ } catch {
14394
+ }
14395
+ return;
14396
+ }
14397
+ const text = `[recovered reply] ${payload.text}`;
14398
+ const body = {
14399
+ chat_id: payload.chat_id,
14400
+ text,
14401
+ allow_sending_without_reply: true
14402
+ };
14403
+ if (payload.message_id) body.reply_to_message_id = Number(payload.message_id);
14404
+ let sendSucceeded = false;
14405
+ try {
14406
+ const resp = await telegramApiCall("sendMessage", body, 15e3);
14407
+ if (resp.ok) {
14408
+ sendSucceeded = true;
14409
+ process.stderr.write(
14410
+ `telegram-channel(${AGENT_CODE_NAME}): ghost-reply recovery sent (chat=${redactId(payload.chat_id)} msg=${redactId(payload.message_id ?? "")})
14411
+ `
14412
+ );
14413
+ if (payload.message_id) clearPendingInboundMarker(payload.chat_id, payload.message_id);
14414
+ } else {
14415
+ process.stderr.write(
14416
+ `telegram-channel(${AGENT_CODE_NAME}): ghost-reply recovery failed (chat=${redactId(payload.chat_id)}): ${resp.description ?? "unknown"}
14417
+ `
14418
+ );
14419
+ }
14420
+ } catch (err) {
14421
+ process.stderr.write(
14422
+ `telegram-channel(${AGENT_CODE_NAME}): ghost-reply recovery error (chat=${redactId(payload.chat_id)}): ${err.message}
14423
+ `
14424
+ );
14425
+ }
14426
+ if (sendSucceeded) {
14427
+ try {
14428
+ unlinkSync2(fullPath);
14429
+ } catch {
14430
+ }
14431
+ return;
14432
+ }
14433
+ const next = nextRetryName(filename);
14434
+ if (next) {
14435
+ try {
14436
+ renameSync2(fullPath, join2(RECOVERY_OUTBOX_DIR, next.next));
14437
+ if (next.attempt >= MAX_RECOVERY_ATTEMPTS) {
14438
+ process.stderr.write(
14439
+ `telegram-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
14440
+ `
14441
+ );
14442
+ }
14443
+ } catch {
14444
+ }
14445
+ }
14446
+ }
14447
+ function isFirstAttemptOutboxFile(filename) {
14448
+ if (!filename.endsWith(".json")) return false;
14449
+ if (filename.includes(".retry-")) return false;
14450
+ if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return false;
14451
+ if (filename.startsWith(".")) return false;
14452
+ return true;
14453
+ }
14454
+ var RETRY_SCAN_INTERVAL_MS = 6e4;
14455
+ var RETRY_BACKOFF_BASE_MS = 6e4;
14456
+ function shouldRetryNow(filename, mtimeMs) {
14457
+ const match = filename.match(/\.retry-(\d+)\.json$/);
14458
+ if (!match) return false;
14459
+ const attempt = parseInt(match[1], 10);
14460
+ const required2 = attempt * RETRY_BACKOFF_BASE_MS;
14461
+ return Date.now() - mtimeMs >= required2;
14462
+ }
14463
+ function scanRecoveryRetries() {
14464
+ if (!RECOVERY_OUTBOX_DIR) return;
14465
+ let entries;
14466
+ try {
14467
+ entries = readdirSync(RECOVERY_OUTBOX_DIR);
14468
+ } catch {
14469
+ return;
14470
+ }
14471
+ for (const f of entries) {
14472
+ if (!f.endsWith(".json")) continue;
14473
+ if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
14474
+ let mtimeMs;
14475
+ try {
14476
+ mtimeMs = statSync(join2(RECOVERY_OUTBOX_DIR, f)).mtimeMs;
14477
+ } catch {
14478
+ continue;
14479
+ }
14480
+ if (shouldRetryNow(f, mtimeMs)) {
14481
+ void processRecoveryOutboxFile(f);
14482
+ }
14483
+ }
14484
+ }
14485
+ function startRecoveryOutboxWatcher() {
14486
+ if (!RECOVERY_OUTBOX_DIR) return;
14487
+ try {
14488
+ mkdirSync2(RECOVERY_OUTBOX_DIR, { recursive: true, mode: 448 });
14489
+ } catch (err) {
14490
+ process.stderr.write(
14491
+ `telegram-channel(${AGENT_CODE_NAME}): recovery outbox mkdir failed: ${err.message}
14492
+ `
14493
+ );
14494
+ return;
14495
+ }
14496
+ try {
14497
+ for (const f of readdirSync(RECOVERY_OUTBOX_DIR)) {
14498
+ if (isFirstAttemptOutboxFile(f)) void processRecoveryOutboxFile(f);
14499
+ }
14500
+ } catch {
14501
+ }
14502
+ try {
14503
+ const watcher = watch(RECOVERY_OUTBOX_DIR, (event, filename) => {
14504
+ if (event !== "rename" || !filename) return;
14505
+ if (!isFirstAttemptOutboxFile(filename)) return;
14506
+ if (existsSync(join2(RECOVERY_OUTBOX_DIR, filename))) {
14507
+ void processRecoveryOutboxFile(filename);
14508
+ }
14509
+ });
14510
+ watcher.unref?.();
14511
+ } catch (err) {
14512
+ process.stderr.write(
14513
+ `telegram-channel(${AGENT_CODE_NAME}): recovery outbox watch failed: ${err.message}
14514
+ `
14515
+ );
14516
+ }
14517
+ const retryTimer = setInterval(scanRecoveryRetries, RETRY_SCAN_INTERVAL_MS);
14518
+ retryTimer.unref?.();
14519
+ }
14520
+ startRecoveryOutboxWatcher();
14180
14521
  function trackPendingMessage(chatId, messageId, chatType) {
14181
14522
  const key = `${chatId}:${messageId}`;
14182
14523
  const existing = pendingMessages.get(key);
14183
14524
  if (existing) clearTimeout(existing.timer);
14184
14525
  const timer = setTimeout(() => {
14185
14526
  pendingMessages.delete(key);
14186
- void setMessageReaction(chatId, messageId, TIMEOUT_EMOJI);
14527
+ clearPendingInboundMarker(chatId, messageId);
14528
+ void setMessageReaction(chatId, messageId, null);
14529
+ void telegramApiCall(
14530
+ "sendMessage",
14531
+ {
14532
+ chat_id: chatId,
14533
+ text: "Sorry \u2014 I didn't get a response back to you within 5 minutes, so this one fell on the floor. Please re-send if it's still relevant.",
14534
+ reply_to_message_id: Number(messageId),
14535
+ allow_sending_without_reply: true
14536
+ },
14537
+ 1e4
14538
+ ).then((resp) => {
14539
+ if (!resp.ok) {
14540
+ process.stderr.write(
14541
+ `telegram-channel(${AGENT_CODE_NAME}): timeout sendMessage failed for chat ${redactId(chatId)} message ${redactId(messageId)}: ${resp.description ?? "unknown"}
14542
+ `
14543
+ );
14544
+ }
14545
+ }).catch((err) => {
14546
+ process.stderr.write(
14547
+ `telegram-channel(${AGENT_CODE_NAME}): timeout sendMessage error for chat ${redactId(chatId)} message ${redactId(messageId)}: ${err.message}
14548
+ `
14549
+ );
14550
+ });
14187
14551
  process.stderr.write(
14188
14552
  `telegram-channel(${AGENT_CODE_NAME}): response timeout for message ${redactId(messageId)} in chat ${redactId(chatId)}
14189
14553
  `
@@ -14191,6 +14555,7 @@ function trackPendingMessage(chatId, messageId, chatType) {
14191
14555
  }, RESPONSE_TIMEOUT_MS);
14192
14556
  timer.unref?.();
14193
14557
  pendingMessages.set(key, { timer, chatType });
14558
+ writePendingInboundMarker(chatId, messageId, chatType);
14194
14559
  }
14195
14560
  function clearPendingMessage(chatId, messageId) {
14196
14561
  if (messageId) {
@@ -14199,15 +14564,30 @@ function clearPendingMessage(chatId, messageId) {
14199
14564
  if (entry) {
14200
14565
  clearTimeout(entry.timer);
14201
14566
  pendingMessages.delete(key);
14567
+ clearPendingInboundMarker(chatId, messageId);
14202
14568
  }
14203
14569
  return;
14204
14570
  }
14205
14571
  const prefix = `${chatId}:`;
14572
+ let clearedPrivate = false;
14206
14573
  for (const [key, entry] of pendingMessages) {
14207
14574
  if (!key.startsWith(prefix)) continue;
14208
14575
  if (entry.chatType !== "private") continue;
14209
14576
  clearTimeout(entry.timer);
14210
14577
  pendingMessages.delete(key);
14578
+ const msgId = key.slice(prefix.length);
14579
+ clearPendingInboundMarker(chatId, msgId);
14580
+ clearedPrivate = true;
14581
+ }
14582
+ if (clearedPrivate) return;
14583
+ for (const [key, entry] of pendingMessages) {
14584
+ if (!key.startsWith(prefix)) continue;
14585
+ if (entry.chatType === "private") continue;
14586
+ clearTimeout(entry.timer);
14587
+ pendingMessages.delete(key);
14588
+ const msgId = key.slice(prefix.length);
14589
+ clearPendingInboundMarker(chatId, msgId);
14590
+ return;
14211
14591
  }
14212
14592
  }
14213
14593
  var mcp = new Server(
@@ -14221,13 +14601,14 @@ var mcp = new Server(
14221
14601
  // Attachments rules live near the top because they get chopped otherwise
14222
14602
  // and the agent silently loses attachment-handling guidance.
14223
14603
  instructions: [
14224
- 'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">.',
14225
- "Inbound attachments: the <channel> tag's `files` attribute is a JSON-serialised array \u2014 JSON.parse it before iterating. If a file entry has a `path`, the image is ALREADY DOWNLOADED locally \u2014 Read that path directly, do NOT call telegram.download_attachment. That tool is only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim (never paraphrase), then Read the returned path. Single-image messages also get a top-level `image_path` convenience attribute; multi-image messages omit it. Caption (no text) arrives as the channel content. Never tell the user about internal file-handling failures that don't affect the answer.",
14226
- "The user reads Telegram, not this session. Every response goes through telegram.reply with the chat_id from the tag \u2014 clarifying questions, errors, partial answers, status updates, all of it.",
14227
- 'For work >30s follow CLAUDE.md kanban flow: kanban.add, reply with "On it \u2014 tracking here: <kanban URL>", move to in_progress, do the work, reply with the result. Simple lookups skip kanban but still reply.',
14228
- "Address users by user_name; user is the numeric Telegram ID. Reply-to a specific message with reply_to_message_id, otherwise omit.",
14229
- "Resolve ambiguous times against your own Timezone from CLAUDE.md \u2014 do not ask the user.",
14230
- `Reaction taxonomy (use telegram.react sparingly \u2014 prefer a text reply via telegram.reply): \u{1F440} = ack/working on it (already auto-added on inbound, do not duplicate); \u{1F44D} or \u{1F389} = action completed successfully; \u{1F494} = you TRIED to execute the requested action and it FAILED with an error (Telegram's free-tier stand-in for Slack's \u274C; the literal \u274C is not on the bot-allowed emoji list). Do NOT use \u{1F494} for "skipped", "disagree", "n/a", or "no response needed" \u2014 those need either a text reply or no reaction. Free-tier emoji set: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} \u{1F494} (premium-only fail silently).`
14604
+ // Highest-priority lines first Claude Code truncates this string at
14605
+ // 2048 chars, so anything appended late silently disappears.
14606
+ "CRITICAL: every response to a Telegram <channel> tag MUST go through telegram.reply with the chat_id from the tag. Text in your session WITHOUT a telegram.reply call never reaches the user.",
14607
+ 'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">. Pass reply_to_message_id from the tag so the response lands as a quote-reply in busy chats.',
14608
+ "Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call telegram.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Caption arrives as channel content. Don't surface internal file-handling errors that don't affect the answer.",
14609
+ 'For work >30s follow CLAUDE.md kanban flow: kanban.add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
14610
+ "Address users by user_name; user is the numeric Telegram ID. Resolve ambiguous times against your own Timezone from CLAUDE.md \u2014 do not ask.",
14611
+ "Reaction taxonomy (use telegram.react sparingly \u2014 prefer telegram.reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u{1F44D} or \u{1F389} = success. NEVER react to signal failure. On failure, telegram.reply with one sentence explaining what went wrong. Free-tier emoji: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} \u{1F494}."
14231
14612
  ].join(" ")
14232
14613
  }
14233
14614
  );
@@ -14287,7 +14668,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
14287
14668
  },
14288
14669
  {
14289
14670
  name: "telegram.react",
14290
- description: `Add an emoji reaction to a Telegram message. Use sparingly \u2014 prefer a text reply via telegram.reply. Reaction taxonomy: \u{1F44D} or \u{1F389} = action completed successfully; \u{1F494} = you TRIED to execute the requested action and it FAILED with an error (free-tier stand-in for Slack's \u274C). Do NOT use \u{1F494} for "skipped", "disagree", or "noted" \u2014 those need a text reply or no reaction at all. \u{1F440} is already auto-applied on inbound; do not duplicate. Only free-tier emoji reactions are available to bots (Premium-only emoji fail silently). Pass an empty string or omit emoji to clear the bot's reaction on that message.`,
14671
+ description: "Add an emoji reaction to a Telegram message. Use sparingly \u2014 prefer a text reply via telegram.reply. Reaction taxonomy: \u{1F44D} or \u{1F389} = action completed successfully. NEVER react to signal failure \u2014 users can't tell why it failed from a reaction. On failure, call telegram.reply with one sentence explaining what went wrong instead. \u{1F440} is already auto-applied on inbound; do not duplicate. Only free-tier emoji reactions are available to bots (Premium-only emoji fail silently). Pass an empty string or omit emoji to clear the bot's reaction on that message.",
14291
14672
  inputSchema: {
14292
14673
  type: "object",
14293
14674
  properties: {
@@ -14539,6 +14920,34 @@ async function pollLoop() {
14539
14920
  if (content.length === 0 && classifiedAttachments.length === 0) continue;
14540
14921
  const chatId = String(msg.chat.id);
14541
14922
  if (ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(chatId)) continue;
14923
+ const trimmedContent = content.trim();
14924
+ if (isRestartSyntax(trimmedContent)) {
14925
+ const disposition = await classifyRestartCommand(trimmedContent);
14926
+ if (disposition === "act") {
14927
+ await handleRestartCommand({
14928
+ chatId,
14929
+ messageId: String(msg.message_id)
14930
+ });
14931
+ } else if (disposition === "verification_failed") {
14932
+ try {
14933
+ await telegramApiCall(
14934
+ "sendMessage",
14935
+ {
14936
+ chat_id: chatId,
14937
+ text: `\u274C Couldn't verify the restart target. Please retry /restart in a few seconds.`,
14938
+ reply_to_message_id: Number(msg.message_id)
14939
+ },
14940
+ 1e4
14941
+ );
14942
+ } catch (err) {
14943
+ process.stderr.write(
14944
+ `telegram-channel(${AGENT_CODE_NAME}): /restart verification-failed ack send failed: ${redactAugmentedPaths(err.message)}
14945
+ `
14946
+ );
14947
+ }
14948
+ }
14949
+ continue;
14950
+ }
14542
14951
  const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
14543
14952
  const userName = msg.from?.username || [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || userId;
14544
14953
  const messageId = String(msg.message_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {