@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.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-AYSU35A4.js → chunk-WQJL6EGC.js} +162 -13
- package/dist/chunk-WQJL6EGC.js.map +1 -0
- package/dist/lib/manager-worker.js +91 -37
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/telegram-channel.js +563 -9
- package/package.json +1 -1
- package/dist/chunk-AYSU35A4.js.map +0 -1
package/mcp/telegram-channel.js
CHANGED
|
@@ -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
|
-
|
|
14422
|
-
|
|
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
|
|
14426
|
-
|
|
14427
|
-
|
|
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
|
|
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
|
|
14872
|
+
`telegram-channel(${AGENT_CODE_NAME}): getMe failed: ${redactAugmentedPaths(err.message)} \u2014 will retry on next lookup
|
|
14436
14873
|
`
|
|
14437
14874
|
);
|
|
14438
14875
|
}
|
|
14439
|
-
|
|
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
|
});
|