@integrity-labs/agt-cli 0.19.18 → 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.
@@ -14235,13 +14235,21 @@ function classifyPeerMessage(msg, cfg, self) {
14235
14235
  if (self.bot_id === null || self.bot_username === null) {
14236
14236
  return { kind: "drop", reason: "self_resolution_pending" };
14237
14237
  }
14238
- if (isAddressedToUs(msg, self.bot_id, self.bot_username)) {
14239
- return { kind: "peer-ingress", peer };
14238
+ const addressing = classifyAddressing(msg, self.bot_id, self.bot_username);
14239
+ if (!addressing.addressed) {
14240
+ return { kind: "drop", reason: "not_addressed" };
14240
14241
  }
14241
- return { kind: "drop", reason: "not_addressed" };
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 };
14242
14250
  }
14243
- function isAddressedToUs(msg, ourBotId, ourBotUsername) {
14244
- if (msg.reply_to_message?.from?.id === ourBotId) return true;
14251
+ function classifyAddressing(msg, ourBotId, ourBotUsername) {
14252
+ let viaMentionOrCommand = false;
14245
14253
  const text = msg.text ?? msg.caption ?? "";
14246
14254
  const entities = msg.entities ?? msg.caption_entities ?? [];
14247
14255
  const usAt = "@" + ourBotUsername;
@@ -14249,15 +14257,19 @@ function isAddressedToUs(msg, ourBotId, ourBotUsername) {
14249
14257
  const slice = text.slice(entity.offset, entity.offset + entity.length).toLowerCase();
14250
14258
  if (entity.type === "bot_command") {
14251
14259
  const at = slice.indexOf("@");
14252
- if (at < 0) {
14253
- return true;
14260
+ if (at < 0 || slice.slice(at + 1) === ourBotUsername) {
14261
+ viaMentionOrCommand = true;
14262
+ break;
14254
14263
  }
14255
- if (slice.slice(at + 1) === ourBotUsername) return true;
14256
14264
  } else if (entity.type === "mention" && slice === usAt) {
14257
- return true;
14265
+ viaMentionOrCommand = true;
14266
+ break;
14258
14267
  }
14259
14268
  }
14260
- return false;
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 };
14261
14273
  }
14262
14274
  var CODE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14263
14275
  function parsePeersEnv(raw) {
@@ -14287,6 +14299,322 @@ function parsePeerAgentModeEnv(raw) {
14287
14299
  return "off";
14288
14300
  }
14289
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
+
14290
14618
  // src/telegram-channel.ts
14291
14619
  function redactId(id) {
14292
14620
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
@@ -14303,6 +14631,28 @@ var PEER_CLASSIFIER_CONFIG = {
14303
14631
  peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
14304
14632
  peers: parsePeersEnv(process.env.TELEGRAM_PEERS)
14305
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;
14306
14656
  if (!BOT_TOKEN) {
14307
14657
  process.stderr.write(
14308
14658
  "telegram-channel: Missing TELEGRAM_BOT_TOKEN. Cannot start.\n"
@@ -14939,6 +15289,40 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14939
15289
  isError: true
14940
15290
  };
14941
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
+ }
14942
15326
  const killed = await isThreadKilled({
14943
15327
  channelType: "telegram",
14944
15328
  channelId: chat_id,
@@ -15245,11 +15629,23 @@ async function pollLoop() {
15245
15629
  const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
15246
15630
  const userName = msg.from?.username || [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || userId;
15247
15631
  const isFromBot = !!msg.from?.is_bot;
15632
+ const nowMs = Date.now();
15248
15633
  if (chatId && !isFromBot) {
15249
15634
  resetThread(chatId, "");
15250
15635
  }
15636
+ peerPresence.noteInbound(chatId, isFromBot ? "bot" : "human", nowMs);
15637
+ if (!isFromBot) {
15638
+ peerRateLimiter.recordInboundHuman(chatId, nowMs);
15639
+ }
15251
15640
  let peerAgentMeta = null;
15252
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
+ }
15253
15649
  const classifierMsg = {
15254
15650
  message_id: msg.message_id,
15255
15651
  text: msg.text,
@@ -15276,7 +15672,29 @@ async function pollLoop() {
15276
15672
  continue;
15277
15673
  }
15278
15674
  if (classification.kind === "peer-ingress") {
15279
- peerAgentMeta = { code_name: classification.peer.code_name, agent_id: classification.peer.agent_id };
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
+ };
15280
15698
  }
15281
15699
  }
15282
15700
  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.19.18",
3
+ "version": "0.19.19",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {