@integrity-labs/agt-cli 0.19.17 → 0.19.19

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.
@@ -14211,6 +14211,410 @@ async function isThreadKilled(opts) {
14211
14211
  }
14212
14212
  }
14213
14213
 
14214
+ // src/telegram-peer-classifier.ts
14215
+ function classifyPeerMessage(msg, cfg, self) {
14216
+ if (!msg.from?.is_bot) return { kind: "human" };
14217
+ if (self.bot_id !== null && msg.from.id === self.bot_id) {
14218
+ return { kind: "self" };
14219
+ }
14220
+ if (cfg.peer_agent_mode === "off") {
14221
+ return { kind: "drop", reason: "mode_off" };
14222
+ }
14223
+ const chatId = String(msg.chat.id);
14224
+ if (cfg.peer_group_ids.length === 0) {
14225
+ if (msg.chat.type !== "private") {
14226
+ return { kind: "drop", reason: "chat_not_allowed" };
14227
+ }
14228
+ } else if (!cfg.peer_group_ids.includes(chatId)) {
14229
+ return { kind: "drop", reason: "chat_not_allowed" };
14230
+ }
14231
+ const peer = cfg.peers.find((p) => p.bot_id === msg.from.id);
14232
+ if (!peer) {
14233
+ return { kind: "drop", reason: "unknown_peer" };
14234
+ }
14235
+ if (self.bot_id === null || self.bot_username === null) {
14236
+ return { kind: "drop", reason: "self_resolution_pending" };
14237
+ }
14238
+ const addressing = classifyAddressing(msg, self.bot_id, self.bot_username);
14239
+ if (!addressing.addressed) {
14240
+ return { kind: "drop", reason: "not_addressed" };
14241
+ }
14242
+ if (addressing.viaReplyOnly) {
14243
+ const inAllowedGroup = cfg.peer_group_ids.length > 0 && cfg.peer_group_ids.includes(chatId);
14244
+ const respondMode = cfg.peer_agent_mode === "respond";
14245
+ if (!(inAllowedGroup && respondMode)) {
14246
+ return { kind: "drop", reason: "not_addressed" };
14247
+ }
14248
+ }
14249
+ return { kind: "peer-ingress", peer };
14250
+ }
14251
+ function classifyAddressing(msg, ourBotId, ourBotUsername) {
14252
+ let viaMentionOrCommand = false;
14253
+ const text = msg.text ?? msg.caption ?? "";
14254
+ const entities = msg.entities ?? msg.caption_entities ?? [];
14255
+ const usAt = "@" + ourBotUsername;
14256
+ for (const entity of entities) {
14257
+ const slice = text.slice(entity.offset, entity.offset + entity.length).toLowerCase();
14258
+ if (entity.type === "bot_command") {
14259
+ const at = slice.indexOf("@");
14260
+ if (at < 0 || slice.slice(at + 1) === ourBotUsername) {
14261
+ viaMentionOrCommand = true;
14262
+ break;
14263
+ }
14264
+ } else if (entity.type === "mention" && slice === usAt) {
14265
+ viaMentionOrCommand = true;
14266
+ break;
14267
+ }
14268
+ }
14269
+ const viaReply = msg.reply_to_message?.from?.id === ourBotId;
14270
+ if (viaMentionOrCommand) return { addressed: true, viaReplyOnly: false };
14271
+ if (viaReply) return { addressed: true, viaReplyOnly: true };
14272
+ return { addressed: false, viaReplyOnly: false };
14273
+ }
14274
+ var CODE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14275
+ function parsePeersEnv(raw) {
14276
+ if (!raw || raw.trim().length === 0) return [];
14277
+ let parsed;
14278
+ try {
14279
+ parsed = JSON.parse(raw);
14280
+ } catch {
14281
+ return [];
14282
+ }
14283
+ if (!Array.isArray(parsed)) return [];
14284
+ const out = [];
14285
+ for (const entry of parsed) {
14286
+ if (entry && typeof entry === "object" && typeof entry.code_name === "string" && CODE_NAME_RE.test(entry.code_name) && typeof entry.agent_id === "string" && typeof entry.bot_id === "number" && Number.isInteger(entry.bot_id) && entry.bot_id > 0) {
14287
+ const e = entry;
14288
+ out.push({ code_name: e.code_name, bot_id: e.bot_id, agent_id: e.agent_id });
14289
+ }
14290
+ }
14291
+ return out;
14292
+ }
14293
+ function parsePeerGroupIdsEnv(raw) {
14294
+ if (!raw) return [];
14295
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
14296
+ }
14297
+ function parsePeerAgentModeEnv(raw) {
14298
+ if (raw === "listen" || raw === "respond") return raw;
14299
+ return "off";
14300
+ }
14301
+
14302
+ // src/telegram-peer-rate-limiter.ts
14303
+ var SECOND_MS = 1e3;
14304
+ var MINUTE_MS = 60 * SECOND_MS;
14305
+ var HOUR_MS = 60 * MINUTE_MS;
14306
+ var DAY_MS = 24 * HOUR_MS;
14307
+ var DEFAULT_RATE_LIMITER_CONFIG = Object.freeze({
14308
+ pairBurstLimit: 1,
14309
+ pairBurstWindowMs: 10 * SECOND_MS,
14310
+ pairWindowLimit: 5,
14311
+ pairWindowMs: 5 * MINUTE_MS,
14312
+ chatLimit: 10,
14313
+ chatWindowMs: 5 * MINUTE_MS,
14314
+ agentDailyLimit: 200,
14315
+ agentDailyWindowMs: DAY_MS,
14316
+ dailyBudgetLimit: 500,
14317
+ dailyBudgetWindowMs: DAY_MS
14318
+ });
14319
+ function readSliding(window, windowMs, now) {
14320
+ if (!window) return 0;
14321
+ const cutoff = now - windowMs;
14322
+ let i = 0;
14323
+ while (i < window.timestamps.length && window.timestamps[i] <= cutoff) i++;
14324
+ if (i > 0) window.timestamps.splice(0, i);
14325
+ return window.timestamps.length;
14326
+ }
14327
+ function bumpSliding(window, now) {
14328
+ window.timestamps.push(now);
14329
+ }
14330
+ function createPeerRateLimiter(config2 = DEFAULT_RATE_LIMITER_CONFIG) {
14331
+ const counters = {
14332
+ pairBurst: /* @__PURE__ */ new Map(),
14333
+ pairWindow: /* @__PURE__ */ new Map(),
14334
+ chat: /* @__PURE__ */ new Map(),
14335
+ agentDaily: { timestamps: [] },
14336
+ dailyBudget: { timestamps: [] }
14337
+ };
14338
+ let humanInbound = 0;
14339
+ function pairKey(chatId, peerBotId) {
14340
+ return `${chatId}:${peerBotId}`;
14341
+ }
14342
+ function readMap(map, key2, windowMs, now) {
14343
+ return readSliding(map.get(key2), windowMs, now);
14344
+ }
14345
+ function bumpMap(map, key2, now) {
14346
+ let entry = map.get(key2);
14347
+ if (!entry) {
14348
+ entry = { timestamps: [] };
14349
+ map.set(key2, entry);
14350
+ }
14351
+ bumpSliding(entry, now);
14352
+ }
14353
+ return {
14354
+ recordInboundPeer(chatId, peerBotId, now) {
14355
+ const pk = pairKey(chatId, peerBotId);
14356
+ if (readMap(counters.pairBurst, pk, config2.pairBurstWindowMs, now) >= config2.pairBurstLimit) {
14357
+ return { decision: "rate_limit_pair", dailyBudgetWarn: false };
14358
+ }
14359
+ if (readMap(counters.pairWindow, pk, config2.pairWindowMs, now) >= config2.pairWindowLimit) {
14360
+ return { decision: "rate_limit_pair", dailyBudgetWarn: false };
14361
+ }
14362
+ if (readMap(counters.chat, chatId, config2.chatWindowMs, now) >= config2.chatLimit) {
14363
+ return { decision: "rate_limit_chat", dailyBudgetWarn: false };
14364
+ }
14365
+ if (readSliding(counters.agentDaily, config2.agentDailyWindowMs, now) >= config2.agentDailyLimit) {
14366
+ return { decision: "rate_limit_agent_daily", dailyBudgetWarn: false };
14367
+ }
14368
+ const budgetSoFar = readSliding(counters.dailyBudget, config2.dailyBudgetWindowMs, now);
14369
+ if (budgetSoFar >= config2.dailyBudgetLimit) {
14370
+ return { decision: "daily_budget_exhausted", dailyBudgetWarn: false };
14371
+ }
14372
+ bumpMap(counters.pairBurst, pk, now);
14373
+ bumpMap(counters.pairWindow, pk, now);
14374
+ bumpMap(counters.chat, chatId, now);
14375
+ bumpSliding(counters.agentDaily, now);
14376
+ bumpSliding(counters.dailyBudget, now);
14377
+ const newBudget = budgetSoFar + 1;
14378
+ const warn80 = newBudget >= Math.ceil(config2.dailyBudgetLimit * 0.8) && newBudget < config2.dailyBudgetLimit;
14379
+ return { decision: "ok", dailyBudgetWarn: warn80 };
14380
+ },
14381
+ recordInboundHuman(_chatId, _now) {
14382
+ humanInbound += 1;
14383
+ },
14384
+ snapshot(now) {
14385
+ const perChat = /* @__PURE__ */ new Map();
14386
+ for (const [k, v] of counters.chat) {
14387
+ perChat.set(k, readSliding(v, config2.chatWindowMs, now));
14388
+ }
14389
+ const perPair = /* @__PURE__ */ new Map();
14390
+ for (const [k, v] of counters.pairWindow) {
14391
+ perPair.set(k, readSliding(v, config2.pairWindowMs, now));
14392
+ }
14393
+ return {
14394
+ perAgentDaily: readSliding(counters.agentDaily, config2.agentDailyWindowMs, now),
14395
+ dailyBudget: readSliding(counters.dailyBudget, config2.dailyBudgetWindowMs, now),
14396
+ perChat,
14397
+ perPair,
14398
+ humanInbound
14399
+ };
14400
+ }
14401
+ };
14402
+ }
14403
+ function createPresenceTracker() {
14404
+ const lastHumanByChat = /* @__PURE__ */ new Map();
14405
+ return {
14406
+ noteInbound(chatId, kind, now) {
14407
+ if (kind === "human") lastHumanByChat.set(chatId, now);
14408
+ },
14409
+ shouldDropOutgress(chatId, now, maxAgeMs = 30 * MINUTE_MS) {
14410
+ const last = lastHumanByChat.get(chatId);
14411
+ if (last === void 0) return "no_human_recent";
14412
+ if (now - last >= maxAgeMs) return "no_human_recent";
14413
+ return null;
14414
+ }
14415
+ };
14416
+ }
14417
+
14418
+ // src/telegram-peer-rate-limiter-db.ts
14419
+ function createDbBackedPeerRateLimiter(deps) {
14420
+ const config2 = deps.config ?? DEFAULT_RATE_LIMITER_CONFIG;
14421
+ const readCacheMs = deps.readCacheMs ?? 2e3;
14422
+ const log = deps.log ?? (() => {
14423
+ });
14424
+ const caches = /* @__PURE__ */ new Map();
14425
+ let humanInbound = 0;
14426
+ const pendingWrites = /* @__PURE__ */ new Set();
14427
+ function trackPending(write) {
14428
+ pendingWrites.add(write);
14429
+ void write.finally(() => pendingWrites.delete(write));
14430
+ }
14431
+ function cacheKey2(layer, chatId, peerBotId) {
14432
+ return `${layer}|${chatId}|${peerBotId ?? ""}`;
14433
+ }
14434
+ const localBumpTimes = /* @__PURE__ */ new Map();
14435
+ function bumpLocal(layer, chatId, peerBotId, now) {
14436
+ const key2 = cacheKey2(layer, chatId, peerBotId);
14437
+ const list = localBumpTimes.get(key2);
14438
+ if (list) list.push(now);
14439
+ else localBumpTimes.set(key2, [now]);
14440
+ }
14441
+ function localBumpsSince(key2, fetchedAt) {
14442
+ const list = localBumpTimes.get(key2);
14443
+ if (!list) return 0;
14444
+ let n = 0;
14445
+ for (let i = list.length - 1; i >= 0; i--) {
14446
+ if (list[i] >= fetchedAt) n++;
14447
+ else break;
14448
+ }
14449
+ return n;
14450
+ }
14451
+ function pruneLocalBumps(retentionMs, now) {
14452
+ const cutoff = now - retentionMs;
14453
+ for (const [key2, list] of localBumpTimes) {
14454
+ let i = 0;
14455
+ while (i < list.length && list[i] < cutoff) i++;
14456
+ if (i > 0) list.splice(0, i);
14457
+ if (list.length === 0) localBumpTimes.delete(key2);
14458
+ }
14459
+ }
14460
+ async function readCount(layer, chatId, peerBotId, windowMs, now) {
14461
+ const key2 = cacheKey2(layer, chatId, peerBotId);
14462
+ const cached2 = caches.get(key2);
14463
+ if (cached2 && now - cached2.fetchedAt < readCacheMs) {
14464
+ return cached2.count + localBumpsSince(key2, cached2.fetchedAt);
14465
+ }
14466
+ const since = new Date(now - windowMs).toISOString();
14467
+ let count;
14468
+ try {
14469
+ const args = {
14470
+ since,
14471
+ traffic_class: layer
14472
+ };
14473
+ if (layer === "pair_burst" || layer === "pair_window") {
14474
+ args.chat_id = chatId;
14475
+ args.peer_bot_id = peerBotId ?? void 0;
14476
+ } else if (layer === "chat") {
14477
+ args.chat_id = chatId;
14478
+ }
14479
+ count = await deps.api.countEvents(args);
14480
+ } catch (err) {
14481
+ log(`peer_rate_event read failed (layer=${layer}): ${err.message} \u2014 fail-open`);
14482
+ count = 0;
14483
+ }
14484
+ const fetchedAt = now;
14485
+ caches.set(key2, { fetchedAt, count });
14486
+ return count + localBumpsSince(key2, fetchedAt);
14487
+ }
14488
+ async function decide(chatId, peerBotId, now) {
14489
+ const layers = [
14490
+ { layer: "pair_burst", chatId, peerBotId, limit: config2.pairBurstLimit, windowMs: config2.pairBurstWindowMs, reason: "rate_limit_pair" },
14491
+ { layer: "pair_window", chatId, peerBotId, limit: config2.pairWindowLimit, windowMs: config2.pairWindowMs, reason: "rate_limit_pair" },
14492
+ { layer: "chat", chatId, peerBotId: null, limit: config2.chatLimit, windowMs: config2.chatWindowMs, reason: "rate_limit_chat" },
14493
+ { layer: "agent_daily", chatId: "", peerBotId: null, limit: config2.agentDailyLimit, windowMs: config2.agentDailyWindowMs, reason: "rate_limit_agent_daily" },
14494
+ { layer: "daily_budget", chatId: "", peerBotId: null, limit: config2.dailyBudgetLimit, windowMs: config2.dailyBudgetWindowMs, reason: "daily_budget_exhausted" }
14495
+ ];
14496
+ for (const l of layers) {
14497
+ const count = await readCount(l.layer, l.chatId, l.peerBotId, l.windowMs, now);
14498
+ if (count >= l.limit) {
14499
+ return { decision: l.reason, dailyBudgetWarn: false };
14500
+ }
14501
+ }
14502
+ for (const l of layers) bumpLocal(l.layer, l.chatId, l.peerBotId, now);
14503
+ pruneLocalBumps(Math.max(config2.pairBurstWindowMs, 6e4), now);
14504
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: peerBotId, traffic_class: "pair_burst" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14505
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: peerBotId, traffic_class: "pair_window" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14506
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "chat" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14507
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "agent_daily" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14508
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "daily_budget" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14509
+ const dbKey = cacheKey2("daily_budget", "", null);
14510
+ const dbCached = caches.get(dbKey);
14511
+ const dbBudget = (dbCached?.count ?? 0) + localBumpsSince(dbKey, dbCached?.fetchedAt ?? 0);
14512
+ const warn80 = dbBudget >= Math.ceil(config2.dailyBudgetLimit * 0.8) && dbBudget < config2.dailyBudgetLimit;
14513
+ return { decision: "ok", dailyBudgetWarn: warn80 };
14514
+ }
14515
+ return {
14516
+ recordInboundPeer(chatId, peerBotId, now) {
14517
+ return decide(chatId, peerBotId, now);
14518
+ },
14519
+ recordInboundHuman(_chatId, _now) {
14520
+ humanInbound += 1;
14521
+ },
14522
+ snapshot(now) {
14523
+ const perChat = /* @__PURE__ */ new Map();
14524
+ const perPair = /* @__PURE__ */ new Map();
14525
+ let perAgentDaily = 0;
14526
+ let dailyBudget = 0;
14527
+ for (const [k, v] of caches) {
14528
+ const local = localBumpsSince(k, v.fetchedAt);
14529
+ const layer = k.split("|")[0];
14530
+ if (layer === "pair_window") {
14531
+ const chatId = k.split("|")[1];
14532
+ const peerBotId = k.split("|")[2];
14533
+ perPair.set(`${chatId}:${peerBotId}`, v.count + local);
14534
+ } else if (layer === "chat") {
14535
+ const chatId = k.split("|")[1];
14536
+ perChat.set(chatId, v.count + local);
14537
+ } else if (layer === "agent_daily") {
14538
+ perAgentDaily = v.count + local;
14539
+ } else if (layer === "daily_budget") {
14540
+ dailyBudget = v.count + local;
14541
+ }
14542
+ }
14543
+ void now;
14544
+ return { perAgentDaily, dailyBudget, perChat, perPair, humanInbound };
14545
+ },
14546
+ async flushPending() {
14547
+ await Promise.all([...pendingWrites]);
14548
+ }
14549
+ };
14550
+ }
14551
+ function createDefaultPeerRateApiClient(args) {
14552
+ if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
14553
+ const fetchImpl = args.fetchImpl ?? fetch;
14554
+ const base = args.agtHost.replace(/\/+$/, "");
14555
+ let cachedToken = null;
14556
+ let cachedTokenExpiresAt = 0;
14557
+ async function getToken() {
14558
+ if (cachedToken && Date.now() < cachedTokenExpiresAt) return cachedToken;
14559
+ const resp = await fetchImpl(`${base}/host/exchange`, {
14560
+ method: "POST",
14561
+ headers: { "Content-Type": "application/json" },
14562
+ body: JSON.stringify({ host_key: args.agtApiKey })
14563
+ });
14564
+ if (!resp.ok) {
14565
+ const body = await resp.text().catch(() => "");
14566
+ throw new Error(`/host/exchange failed (${resp.status}): ${body.slice(0, 200)}`);
14567
+ }
14568
+ const data = await resp.json();
14569
+ cachedToken = data.token;
14570
+ cachedTokenExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() - 12e4 : Date.now() + 55 * 6e4;
14571
+ return cachedToken;
14572
+ }
14573
+ async function authedFetch(path, init) {
14574
+ const headers = {
14575
+ Authorization: `Bearer ${await getToken()}`
14576
+ };
14577
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
14578
+ let resp = await fetchImpl(`${base}${path}`, { method: init.method, headers, body: init.body });
14579
+ if (resp.status === 401) {
14580
+ cachedToken = null;
14581
+ cachedTokenExpiresAt = 0;
14582
+ const retryHeaders = {
14583
+ Authorization: `Bearer ${await getToken()}`
14584
+ };
14585
+ if (init.body !== void 0) retryHeaders["Content-Type"] = "application/json";
14586
+ resp = await fetchImpl(`${base}${path}`, { method: init.method, headers: retryHeaders, body: init.body });
14587
+ }
14588
+ return resp;
14589
+ }
14590
+ return {
14591
+ async recordEvent({ chat_id, peer_bot_id, traffic_class }) {
14592
+ const resp = await authedFetch("/host/peer-rate-events", {
14593
+ method: "POST",
14594
+ body: JSON.stringify({ agent_id: args.agentId, chat_id, peer_bot_id, traffic_class })
14595
+ });
14596
+ if (!resp.ok) {
14597
+ throw new Error(`POST /host/peer-rate-events failed: ${resp.status} ${await resp.text()}`);
14598
+ }
14599
+ },
14600
+ async countEvents({ since, chat_id, peer_bot_id, traffic_class }) {
14601
+ const params = new URLSearchParams({
14602
+ agent_id: args.agentId,
14603
+ since
14604
+ });
14605
+ if (chat_id !== void 0) params.set("chat_id", chat_id);
14606
+ if (peer_bot_id !== void 0) params.set("peer_bot_id", String(peer_bot_id));
14607
+ if (traffic_class !== void 0) params.set("traffic_class", traffic_class);
14608
+ const resp = await authedFetch(`/host/peer-rate-events?${params}`, { method: "GET" });
14609
+ if (!resp.ok) {
14610
+ throw new Error(`GET /host/peer-rate-events failed: ${resp.status} ${await resp.text()}`);
14611
+ }
14612
+ const json = await resp.json();
14613
+ return typeof json.count === "number" ? json.count : 0;
14614
+ }
14615
+ };
14616
+ }
14617
+
14214
14618
  // src/telegram-channel.ts
14215
14619
  function redactId(id) {
14216
14620
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
@@ -14222,6 +14626,33 @@ var AGT_API_KEY = process.env.AGT_API_KEY ?? null;
14222
14626
  var ALLOWED_CHATS = new Set(
14223
14627
  (process.env.TELEGRAM_ALLOWED_CHATS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
14224
14628
  );
14629
+ var PEER_CLASSIFIER_CONFIG = {
14630
+ peer_agent_mode: parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE),
14631
+ peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
14632
+ peers: parsePeersEnv(process.env.TELEGRAM_PEERS)
14633
+ };
14634
+ var PEER_DISABLED_GLOBAL = (() => {
14635
+ const v = (process.env.TELEGRAM_PEER_DISABLED ?? "").toLowerCase();
14636
+ return v === "1" || v === "true" || v === "yes";
14637
+ })();
14638
+ if (PEER_DISABLED_GLOBAL) {
14639
+ process.stderr.write(
14640
+ `telegram-channel(${AGENT_CODE_NAME}): TELEGRAM_PEER_DISABLED=true \u2014 peer ingress + outgress short-circuited
14641
+ `
14642
+ );
14643
+ }
14644
+ var peerRateApiClient = createDefaultPeerRateApiClient({
14645
+ agtHost: AGT_HOST,
14646
+ agtApiKey: AGT_API_KEY,
14647
+ agentId: process.env.AGT_AGENT_ID ?? null
14648
+ });
14649
+ var peerRateLimiter = peerRateApiClient && parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE) !== "off" ? createDbBackedPeerRateLimiter({
14650
+ api: peerRateApiClient,
14651
+ log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
14652
+ `)
14653
+ }) : createPeerRateLimiter();
14654
+ var peerPresence = createPresenceTracker();
14655
+ var dailyBudgetWarned = false;
14225
14656
  if (!BOT_TOKEN) {
14226
14657
  process.stderr.write(
14227
14658
  "telegram-channel: Missing TELEGRAM_BOT_TOKEN. Cannot start.\n"
@@ -14418,25 +14849,41 @@ async function handleRestartCommand(opts) {
14418
14849
  }
14419
14850
  }
14420
14851
  var cachedBotUsername = null;
14421
- async function resolveBotUsername() {
14422
- if (cachedBotUsername !== null) return cachedBotUsername;
14852
+ var cachedBotId = null;
14853
+ async function refreshBotIdentity() {
14423
14854
  try {
14424
14855
  const resp = await telegramApiCall("getMe", {}, 5e3);
14425
- if (resp.ok && resp.result && typeof resp.result.username === "string") {
14426
- cachedBotUsername = resp.result.username.toLowerCase();
14427
- return cachedBotUsername;
14856
+ if (resp.ok && resp.result) {
14857
+ const result = resp.result;
14858
+ if (typeof result.username === "string") {
14859
+ cachedBotUsername = result.username.toLowerCase();
14860
+ }
14861
+ if (typeof result.id === "number") {
14862
+ cachedBotId = result.id;
14863
+ }
14864
+ if (cachedBotUsername !== null && cachedBotId !== null) return;
14428
14865
  }
14429
14866
  process.stderr.write(
14430
- `telegram-channel(${AGENT_CODE_NAME}): getMe rejected: ${resp.description ?? "unknown"} \u2014 will retry on next /restart@<bot>
14867
+ `telegram-channel(${AGENT_CODE_NAME}): getMe rejected: ${resp.description ?? "unknown"} \u2014 will retry on next lookup
14431
14868
  `
14432
14869
  );
14433
14870
  } catch (err) {
14434
14871
  process.stderr.write(
14435
- `telegram-channel(${AGENT_CODE_NAME}): getMe failed: ${redactAugmentedPaths(err.message)} \u2014 will retry on next /restart@<bot>
14872
+ `telegram-channel(${AGENT_CODE_NAME}): getMe failed: ${redactAugmentedPaths(err.message)} \u2014 will retry on next lookup
14436
14873
  `
14437
14874
  );
14438
14875
  }
14439
- return null;
14876
+ }
14877
+ async function resolveBotUsername() {
14878
+ if (cachedBotUsername !== null) return cachedBotUsername;
14879
+ await refreshBotIdentity();
14880
+ return cachedBotUsername;
14881
+ }
14882
+ async function resolveBotIdentity() {
14883
+ if (cachedBotId === null || cachedBotUsername === null) {
14884
+ await refreshBotIdentity();
14885
+ }
14886
+ return { bot_id: cachedBotId, bot_username: cachedBotUsername };
14440
14887
  }
14441
14888
  var RESTART_SYNTAX_RE = /^\/restart(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
14442
14889
  var HELP_SYNTAX_RE = /^\/help(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
@@ -14842,6 +15289,40 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14842
15289
  isError: true
14843
15290
  };
14844
15291
  }
15292
+ const peerActiveOnChat = PEER_CLASSIFIER_CONFIG.peer_agent_mode !== "off" && PEER_CLASSIFIER_CONFIG.peer_group_ids.length > 0 && PEER_CLASSIFIER_CONFIG.peer_group_ids.includes(chat_id);
15293
+ if (peerActiveOnChat) {
15294
+ if (PEER_DISABLED_GLOBAL) {
15295
+ process.stderr.write(
15296
+ `telegram-channel(${AGENT_CODE_NAME}): outgress_blocked reason=peer_disabled_global chat=${redactId(chat_id)}
15297
+ `
15298
+ );
15299
+ return {
15300
+ content: [
15301
+ {
15302
+ type: "text",
15303
+ text: "Multi-agent collaboration is disabled (TELEGRAM_PEER_DISABLED). Stop attempting to reply on this chat."
15304
+ }
15305
+ ],
15306
+ isError: true
15307
+ };
15308
+ }
15309
+ const stale = peerPresence.shouldDropOutgress(chat_id, Date.now());
15310
+ if (stale === "no_human_recent") {
15311
+ process.stderr.write(
15312
+ `telegram-channel(${AGENT_CODE_NAME}): outgress_blocked reason=no_human_recent chat=${redactId(chat_id)}
15313
+ `
15314
+ );
15315
+ return {
15316
+ content: [
15317
+ {
15318
+ type: "text",
15319
+ text: "No human message in this chat in the last 30 minutes. Peer-agent collaboration is paused for this chat until a human posts."
15320
+ }
15321
+ ],
15322
+ isError: true
15323
+ };
15324
+ }
15325
+ }
14845
15326
  const killed = await isThreadKilled({
14846
15327
  channelType: "telegram",
14847
15328
  channelId: chat_id,
@@ -15148,9 +15629,74 @@ async function pollLoop() {
15148
15629
  const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
15149
15630
  const userName = msg.from?.username || [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || userId;
15150
15631
  const isFromBot = !!msg.from?.is_bot;
15632
+ const nowMs = Date.now();
15151
15633
  if (chatId && !isFromBot) {
15152
15634
  resetThread(chatId, "");
15153
15635
  }
15636
+ peerPresence.noteInbound(chatId, isFromBot ? "bot" : "human", nowMs);
15637
+ if (!isFromBot) {
15638
+ peerRateLimiter.recordInboundHuman(chatId, nowMs);
15639
+ }
15640
+ let peerAgentMeta = null;
15641
+ if (isFromBot) {
15642
+ if (PEER_DISABLED_GLOBAL) {
15643
+ process.stderr.write(
15644
+ `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=peer_disabled_global chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15645
+ `
15646
+ );
15647
+ continue;
15648
+ }
15649
+ const classifierMsg = {
15650
+ message_id: msg.message_id,
15651
+ text: msg.text,
15652
+ caption: msg.caption,
15653
+ from: msg.from ? { id: msg.from.id, username: msg.from.username, is_bot: msg.from.is_bot } : void 0,
15654
+ chat: { id: msg.chat.id, type: msg.chat.type },
15655
+ reply_to_message: msg.reply_to_message ? {
15656
+ message_id: msg.reply_to_message.message_id,
15657
+ from: msg.reply_to_message.from ? { id: msg.reply_to_message.from.id, is_bot: msg.reply_to_message.from.is_bot } : void 0
15658
+ } : void 0,
15659
+ entities: msg.entities,
15660
+ caption_entities: msg.caption_entities
15661
+ };
15662
+ const self = await resolveBotIdentity();
15663
+ const classification = classifyPeerMessage(classifierMsg, PEER_CLASSIFIER_CONFIG, self);
15664
+ if (classification.kind === "self") {
15665
+ continue;
15666
+ }
15667
+ if (classification.kind === "drop") {
15668
+ process.stderr.write(
15669
+ `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${classification.reason} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15670
+ `
15671
+ );
15672
+ continue;
15673
+ }
15674
+ if (classification.kind === "peer-ingress") {
15675
+ const limit = await peerRateLimiter.recordInboundPeer(
15676
+ chatId,
15677
+ classification.peer.bot_id,
15678
+ nowMs
15679
+ );
15680
+ if (limit.decision !== "ok") {
15681
+ process.stderr.write(
15682
+ `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${limit.decision} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15683
+ `
15684
+ );
15685
+ continue;
15686
+ }
15687
+ if (limit.dailyBudgetWarn && !dailyBudgetWarned) {
15688
+ dailyBudgetWarned = true;
15689
+ process.stderr.write(
15690
+ `telegram-channel(${AGENT_CODE_NAME}): peer_warn reason=daily_budget_80pct
15691
+ `
15692
+ );
15693
+ }
15694
+ peerAgentMeta = {
15695
+ code_name: classification.peer.code_name,
15696
+ agent_id: classification.peer.agent_id
15697
+ };
15698
+ }
15699
+ }
15154
15700
  const messageId = String(msg.message_id);
15155
15701
  void setMessageReaction(chatId, messageId, ACK_EMOJI);
15156
15702
  trackPendingMessage(chatId, messageId, msg.chat.type);
@@ -15194,7 +15740,15 @@ async function pollLoop() {
15194
15740
  user_name: userName,
15195
15741
  ts: String(msg.date),
15196
15742
  ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
15197
- ...imagePath ? { image_path: imagePath } : {}
15743
+ ...imagePath ? { image_path: imagePath } : {},
15744
+ // ENG-4902: peer-agent ingress carries distinct framing so the
15745
+ // runtime (MVP #5) can apply the peer-agent system preamble
15746
+ // and store the turn under a peer_agent memory role.
15747
+ ...peerAgentMeta ? {
15748
+ source_role: "agent",
15749
+ peer_code_name: peerAgentMeta.code_name,
15750
+ peer_agent_id: peerAgentMeta.agent_id
15751
+ } : {}
15198
15752
  }
15199
15753
  }
15200
15754
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.19.17",
3
+ "version": "0.19.19",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {