metheus-governance-mcp-cli 0.2.249 → 0.2.252

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/cli.mjs CHANGED
@@ -140,6 +140,8 @@ import {
140
140
  buildRunnerRouteStateFromComment,
141
141
  buildProcessableArchiveLogicalKey,
142
142
  findEarlierProcessableArchiveDuplicate,
143
+ findRecentTelegramMessageEnvelope,
144
+ isTelegramLocalInboundEnvelopeForRoute,
143
145
  isInboundArchiveKind,
144
146
  normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
145
147
  normalizeArchiveCommentRecord,
@@ -1989,6 +1991,7 @@ function mergeRunnerStateRecords(preferred, fallback) {
1989
1991
  last_followup_source_message_envelope: pickObjectField("last_followup_source_message_envelope"),
1990
1992
  last_followup_last_reply_message_envelope: pickObjectField("last_followup_last_reply_message_envelope"),
1991
1993
  last_followup_attempted_delivery_envelope: pickObjectField("last_followup_attempted_delivery_envelope"),
1994
+ recent_local_inbound_envelopes: pickObjectField("recent_local_inbound_envelopes"),
1992
1995
  last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
1993
1996
  last_normalized_execution_contract_targets: pickArrayField("last_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
1994
1997
  last_normalized_execution_next_responders: pickArrayField("last_normalized_execution_next_responders", normalizeTelegramMentionUsername),
@@ -2642,6 +2645,45 @@ function normalizeRunnerReplyChainContext(rawContext) {
2642
2645
  return normalized;
2643
2646
  }
2644
2647
 
2648
+ function findRunnerRouteLocalInboundEnvelope(routeStateRaw, parsedArchiveRaw) {
2649
+ const routeState = safeObject(routeStateRaw);
2650
+ const parsedArchive = safeObject(parsedArchiveRaw);
2651
+ const chatID = String(parsedArchive.chatID || parsedArchive.chatId || "").trim();
2652
+ const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2653
+ if (!chatID || !(messageID > 0)) {
2654
+ return {};
2655
+ }
2656
+ return findRecentTelegramMessageEnvelope(routeState.recent_local_inbound_envelopes, {
2657
+ chatID,
2658
+ messageID,
2659
+ });
2660
+ }
2661
+
2662
+ function buildRunnerSourceMessageEnvelope({
2663
+ routeState = {},
2664
+ routeKey = "",
2665
+ normalizedRoute = null,
2666
+ parsedArchive = null,
2667
+ }) {
2668
+ const localEnvelope = findRunnerRouteLocalInboundEnvelope(routeState, parsedArchive);
2669
+ const fallbackBotSelector = normalizeTelegramMentionUsername(
2670
+ normalizedRoute?.botName
2671
+ || normalizedRoute?.serverBotName
2672
+ || "",
2673
+ );
2674
+ if (isTelegramLocalInboundEnvelopeForRoute(localEnvelope, {
2675
+ routeKey,
2676
+ botUsername: fallbackBotSelector,
2677
+ })) {
2678
+ return localEnvelope;
2679
+ }
2680
+ return buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, {
2681
+ source_origin: "archive_reconstructed",
2682
+ source_route_key: String(routeKey || "").trim(),
2683
+ source_bot_username: fallbackBotSelector,
2684
+ });
2685
+ }
2686
+
2645
2687
  function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2646
2688
  const normalized = {};
2647
2689
  for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
@@ -2676,6 +2718,7 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2676
2718
  body: entry.source_message_body || entry.sourceMessageBody,
2677
2719
  sender: "human",
2678
2720
  sender_is_bot: false,
2721
+ source_origin: "archive_reconstructed",
2679
2722
  }
2680
2723
  : {}),
2681
2724
  ),
@@ -4408,9 +4451,10 @@ async function claimRunnerRequestForHumanComment({
4408
4451
  reason: "non_human_comment_cannot_create_request",
4409
4452
  };
4410
4453
  }
4411
- const currentState = loadBotRunnerState();
4412
- const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4413
- state: currentState,
4454
+ const currentState = loadBotRunnerState();
4455
+ const currentRouteState = safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]);
4456
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4457
+ state: currentState,
4414
4458
  normalizedRoute,
4415
4459
  selectedRecord,
4416
4460
  runtime,
@@ -4530,14 +4574,19 @@ async function claimRunnerRequestForHumanComment({
4530
4574
  };
4531
4575
  }
4532
4576
  const nowISO = new Date().toISOString();
4533
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4577
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4534
4578
  project_id: String(normalizedRoute?.projectID || "").trim(),
4535
4579
  provider: String(normalizedRoute?.provider || "").trim(),
4536
4580
  chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4537
4581
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4538
4582
  source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4539
4583
  source_message_body: String(parsed.body || "").trim(),
4540
- source_message_envelope: buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsed),
4584
+ source_message_envelope: buildRunnerSourceMessageEnvelope({
4585
+ routeState: currentRouteState,
4586
+ routeKey,
4587
+ normalizedRoute,
4588
+ parsedArchive: parsed,
4589
+ }),
4541
4590
  root_comment_id: String(selectedRecord?.id || "").trim(),
4542
4591
  root_comment_kind: commentKind,
4543
4592
  conversation_id: resolvedConversationID,
@@ -7552,11 +7601,11 @@ function parseArchivedChatComment(rawBody) {
7552
7601
  .filter(Boolean),
7553
7602
  }
7554
7603
  : null;
7555
- return {
7556
- kind,
7557
- header,
7558
- metadata,
7559
- body,
7604
+ return {
7605
+ kind,
7606
+ header,
7607
+ metadata,
7608
+ body,
7560
7609
  chatID: String(metadata.chat_id || "").trim(),
7561
7610
  chatType: String(metadata.chat_type || "").trim().toLowerCase(),
7562
7611
  messageID: intFromRawAllowZero(metadata.message_id, 0),
@@ -7569,8 +7618,11 @@ function parseArchivedChatComment(rawBody) {
7569
7618
  .split(",")
7570
7619
  .map((value) => normalizeTelegramMentionUsername(value))
7571
7620
  .filter(Boolean),
7572
- occurredAt: String(metadata.occurred_at || "").trim(),
7573
- replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7621
+ occurredAt: String(metadata.occurred_at || "").trim(),
7622
+ sourceOrigin: String(metadata.source_origin || "").trim().toLowerCase(),
7623
+ sourceRouteKey: String(metadata.source_route_key || "").trim(),
7624
+ sourceBotUsername: normalizeTelegramMentionUsername(metadata.source_bot_username || ""),
7625
+ replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7574
7626
  replyToSender: String(metadata.reply_to_sender || "").trim(),
7575
7627
  replyToUsername: String(metadata.reply_to_telegram_username || "").trim(),
7576
7628
  replyToSenderIsBot: boolFromRaw(metadata.reply_to_sender_is_bot, false),
@@ -7817,30 +7869,23 @@ function extractTelegramEntityText(text, entity) {
7817
7869
  return body.slice(offset, offset + length);
7818
7870
  }
7819
7871
 
7820
- function extractTelegramMentionUsernames(text, entities) {
7821
- const set = new Set();
7822
- for (const entityRaw of ensureArray(entities)) {
7823
- const entity = safeObject(entityRaw);
7872
+ function extractTelegramMentionUsernames(text, entities) {
7873
+ const set = new Set();
7874
+ for (const entityRaw of ensureArray(entities)) {
7875
+ const entity = safeObject(entityRaw);
7824
7876
  const type = String(entity.type || "").trim().toLowerCase();
7825
7877
  if (type === "mention") {
7826
7878
  const username = normalizeTelegramMentionUsername(extractTelegramEntityText(text, entity));
7827
7879
  if (username) set.add(username);
7828
7880
  continue;
7829
7881
  }
7830
- if (type === "text_mention") {
7831
- const username = normalizeTelegramMentionUsername(entity.user?.username);
7832
- if (username) set.add(username);
7833
- }
7834
- }
7835
- const regex = /(^|[^A-Za-z0-9_])@([A-Za-z0-9_]{3,})\b/g;
7836
- let match = regex.exec(String(text || ""));
7837
- while (match) {
7838
- const username = normalizeTelegramMentionUsername(match[2]);
7839
- if (username) set.add(username);
7840
- match = regex.exec(String(text || ""));
7841
- }
7842
- return Array.from(set);
7843
- }
7882
+ if (type === "text_mention") {
7883
+ const username = normalizeTelegramMentionUsername(entity.user?.username);
7884
+ if (username) set.add(username);
7885
+ }
7886
+ }
7887
+ return Array.from(set);
7888
+ }
7844
7889
 
7845
7890
  function normalizeLocalTelegramUpdate(rawUpdate) {
7846
7891
  const update = safeObject(rawUpdate);
@@ -7886,18 +7931,27 @@ function buildArchivedInboundMessageKey(chatID, messageID) {
7886
7931
  return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
7887
7932
  }
7888
7933
 
7889
- function formatTelegramInboundArchiveComment(normalized) {
7890
- const headerLines = [
7934
+ function formatTelegramInboundArchiveComment(normalized) {
7935
+ const headerLines = [
7891
7936
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
7892
7937
  `chat_id: ${normalized.chatID || "<missing>"}`,
7893
7938
  `chat_type: ${normalized.chatType || "unknown"}`,
7894
7939
  `message_id: ${normalized.messageID || "<missing>"}`,
7895
7940
  `occurred_at: ${normalized.occurredAt || new Date().toISOString()}`,
7896
7941
  `sender_id: ${normalized.fromID || "<missing>"}`,
7897
- `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
7898
- `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
7899
- ];
7900
- if (normalized.fromUsername) {
7942
+ `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
7943
+ `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
7944
+ ];
7945
+ if (String(normalized.sourceOrigin || "").trim()) {
7946
+ headerLines.push(`source_origin: ${String(normalized.sourceOrigin || "").trim()}`);
7947
+ }
7948
+ if (String(normalized.sourceRouteKey || "").trim()) {
7949
+ headerLines.push(`source_route_key: ${String(normalized.sourceRouteKey || "").trim()}`);
7950
+ }
7951
+ if (String(normalized.sourceBotUsername || "").trim()) {
7952
+ headerLines.push(`source_bot_username: @${String(normalized.sourceBotUsername || "").trim().replace(/^@+/, "")}`);
7953
+ }
7954
+ if (normalized.fromUsername) {
7901
7955
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
7902
7956
  }
7903
7957
  if (normalized.mentionUsernames.length > 0) {
@@ -17482,11 +17536,12 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
17482
17536
  mergeServerRunnerRequestLedgerIntoLocalState,
17483
17537
  buildRunnerStatusQueryLookup,
17484
17538
  tryJsonParse,
17485
- safeObject,
17486
- normalizeRunnerTriggerPolicy,
17487
- evaluateTelegramRunnerTrigger,
17488
- resolveRunnerResponderAdjudication,
17489
- selectPendingArchiveComments,
17539
+ safeObject,
17540
+ normalizeRunnerTriggerPolicy,
17541
+ evaluateTelegramRunnerTrigger,
17542
+ resolveHumanIntentContext,
17543
+ resolveRunnerResponderAdjudication,
17544
+ selectPendingArchiveComments,
17490
17545
  selectRunnerPendingWork,
17491
17546
  processRunnerSelectedRecord,
17492
17547
  resolveRunnerStartupLoopAdjudication,
@@ -2669,7 +2669,7 @@ function suggestedAIModelsForClient(clientName) {
2669
2669
  { value: "Opus 4.6", label: "Opus 4.6", description: "display label; runs as opus" },
2670
2670
  ];
2671
2671
  }
2672
- if (normalizedClient === "gemini") {
2672
+ if (normalizedClient === "gemini") {
2673
2673
  return [
2674
2674
  { value: "gemini-3.1-pro", label: "gemini-3.1-pro", description: "display label; runs as auto-gemini-3" },
2675
2675
  ];
@@ -18,6 +18,7 @@ const GEMINI_HOME_SYNC_FILES = [
18
18
  "settings.json",
19
19
  ];
20
20
  const GEMINI_STDIN_BRIDGE_PROMPT = "Use the full task provided on standard input as the authoritative prompt. Follow it exactly and output only the final answer.";
21
+ const GEMINI_CLI_TIMEOUT_MS = 90 * 1000;
21
22
  const LOCAL_AI_MODEL_MAPPINGS = {
22
23
  gpt: [
23
24
  {
@@ -769,13 +770,36 @@ function runGeminiRawText({ promptText, workspaceDir, model, permissionMode, rea
769
770
  env: runtime.env,
770
771
  input: String(promptText || ""),
771
772
  maxBuffer: 8 * 1024 * 1024,
773
+ timeout: GEMINI_CLI_TIMEOUT_MS,
772
774
  },
773
775
  );
774
776
  if (result.error) {
777
+ const errorCode = String(result.error?.code || "").trim().toUpperCase();
778
+ if (errorCode === "ETIMEDOUT") {
779
+ const stderrText = String(result.stderr || "").trim();
780
+ const stdoutText = String(result.stdout || "").trim();
781
+ const details = [stderrText, stdoutText]
782
+ .filter(Boolean)
783
+ .map((value) => value.replace(/\s+/g, " ").trim())
784
+ .filter(Boolean)
785
+ .slice(0, 2)
786
+ .join(" | ");
787
+ throw new Error(
788
+ details
789
+ ? `Gemini CLI timed out after ${Math.round(GEMINI_CLI_TIMEOUT_MS / 1000)}s while waiting for a model response (${details})`
790
+ : `Gemini CLI timed out after ${Math.round(GEMINI_CLI_TIMEOUT_MS / 1000)}s while waiting for a model response`,
791
+ );
792
+ }
775
793
  throw new Error(String(result.error?.message || result.error));
776
794
  }
777
795
  if (result.status !== 0) {
778
- throw new Error(String(result.stderr || result.stdout || `gemini exited with status ${result.status}`));
796
+ const stderrText = String(result.stderr || "");
797
+ const stdoutText = String(result.stdout || "");
798
+ const combinedText = `${stderrText}\n${stdoutText}`.trim();
799
+ if (/MODEL_CAPACITY_EXHAUSTED|No capacity available for model/i.test(combinedText)) {
800
+ throw new Error(`Gemini model capacity is currently unavailable for ${String(model || "").trim() || "the configured model"}`);
801
+ }
802
+ throw new Error(combinedText || `gemini exited with status ${result.status}`);
779
803
  }
780
804
  return String(result.stdout || "");
781
805
  } finally {
@@ -1377,7 +1401,8 @@ function buildGeminiArgs({ model, permissionMode }) {
1377
1401
  "--allowed-mcp-server-names",
1378
1402
  "none",
1379
1403
  ];
1380
- if (model) {
1404
+ const normalizedModel = normalizeModelAliasText(model);
1405
+ if (model && normalizedModel !== "auto-gemini-3") {
1381
1406
  args.push("--model", model);
1382
1407
  }
1383
1408
  return args;
@@ -1433,6 +1458,9 @@ function buildGeminiThinkingConfig(model, reasoningEffort) {
1433
1458
 
1434
1459
  export function resolveGeminiReasoningConfig(rawModelValue = "", reasoningEffort = "medium") {
1435
1460
  const executionModel = resolveLocalAIExecutionModel("gemini", rawModelValue);
1461
+ if (!executionModel || executionModel === "auto-gemini-3") {
1462
+ return null;
1463
+ }
1436
1464
  const thinkingConfig = buildGeminiThinkingConfig(executionModel, reasoningEffort);
1437
1465
  if (!thinkingConfig) {
1438
1466
  return null;
@@ -1,5 +1,6 @@
1
1
  import { deliverLocalProviderMessage } from "./provider-local-transport.mjs";
2
2
  import { normalizeBotProviderName } from "./provider-support.mjs";
3
+ import { normalizeTelegramMessageEnvelope } from "./runner-helpers.mjs";
3
4
 
4
5
  function safeObject(value) {
5
6
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -71,6 +72,12 @@ function normalizeArchiveCommentRecord(rawComment, parseArchivedChatComment) {
71
72
  };
72
73
  }
73
74
 
75
+ function telegramReplySourceEnvelopeIsLocal(rawEnvelope) {
76
+ const envelope = normalizeTelegramMessageEnvelope(rawEnvelope);
77
+ return String(envelope.source_origin || "").trim().toLowerCase() === "local_telegram_inbound"
78
+ && intFromRawAllowZero(envelope.message_id, 0) > 0;
79
+ }
80
+
74
81
  function normalizeExecutionContractForArchive(rawContract) {
75
82
  const contract = safeObject(rawContract);
76
83
  if (!Object.keys(contract).length) {
@@ -274,6 +281,7 @@ export async function performLocalBotDelivery({
274
281
  disableWebPagePreview,
275
282
  messageThreadID,
276
283
  replyToMessageID,
284
+ sourceMessageEnvelope = {},
277
285
  archiveReplies = true,
278
286
  archiveDedupeOutbound = true,
279
287
  archiveThreadID = "",
@@ -295,6 +303,7 @@ export async function performLocalBotDelivery({
295
303
  const parseArchivedChatComment = requireDependency(deps, "parseArchivedChatComment");
296
304
 
297
305
  const normalizedProvider = normalizeBotProviderName(provider, "telegram");
306
+ const normalizedSourceMessageEnvelope = normalizeTelegramMessageEnvelope(sourceMessageEnvelope);
298
307
  const destinations = await listProjectChatDestinations({
299
308
  siteBaseURL,
300
309
  projectID,
@@ -332,6 +341,14 @@ export async function performLocalBotDelivery({
332
341
  replySupported: normalizedProvider === "telegram",
333
342
  };
334
343
  } else {
344
+ if (
345
+ normalizedProvider === "telegram"
346
+ && intFromRawAllowZero(replyToMessageID, 0) > 0
347
+ && !telegramReplySourceEnvelopeIsLocal(normalizedSourceMessageEnvelope)
348
+ ) {
349
+ const origin = String(normalizedSourceMessageEnvelope.source_origin || "unknown").trim() || "unknown";
350
+ throw new Error(`telegram reply anchor is not local to this route (${origin})`);
351
+ }
335
352
  delivery = await deliverLocalProviderMessage({
336
353
  provider: normalizedProvider,
337
354
  token: providerEnv.token,
@@ -68,6 +68,21 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
68
68
  || rawSenderIsBot === "1"
69
69
  || kind === "bot_reply";
70
70
  const body = String(envelope.body || envelope.text || "").trim();
71
+ const sourceOrigin = String(
72
+ envelope.source_origin
73
+ || envelope.sourceOrigin
74
+ || "",
75
+ ).trim().toLowerCase();
76
+ const sourceRouteKey = String(
77
+ envelope.source_route_key
78
+ || envelope.sourceRouteKey
79
+ || "",
80
+ ).trim();
81
+ const sourceBotUsername = normalizeMentionSelector(
82
+ envelope.source_bot_username
83
+ || envelope.sourceBotUsername
84
+ || "",
85
+ );
71
86
  if (
72
87
  !chatID
73
88
  && !(messageID > 0)
@@ -77,6 +92,9 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
77
92
  && !sender
78
93
  && !senderUsername
79
94
  && !body
95
+ && !sourceOrigin
96
+ && !sourceRouteKey
97
+ && !sourceBotUsername
80
98
  ) {
81
99
  return {};
82
100
  }
@@ -90,6 +108,9 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
90
108
  ...(senderUsername ? { sender_username: senderUsername } : {}),
91
109
  sender_is_bot: senderIsBot === true,
92
110
  ...(body ? { body } : {}),
111
+ ...(sourceOrigin ? { source_origin: sourceOrigin } : {}),
112
+ ...(sourceRouteKey ? { source_route_key: sourceRouteKey } : {}),
113
+ ...(sourceBotUsername ? { source_bot_username: sourceBotUsername } : {}),
93
114
  };
94
115
  }
95
116
 
@@ -119,6 +140,21 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
119
140
  ]),
120
141
  sender_is_bot: overrides.sender_is_bot ?? parsedArchive.senderIsBot,
121
142
  body: firstNonEmptyString([overrides.body, parsedArchive.body]),
143
+ source_origin: firstNonEmptyString([
144
+ overrides.source_origin,
145
+ parsedArchive.sourceOrigin,
146
+ parsedArchive.metadata?.source_origin,
147
+ ]),
148
+ source_route_key: firstNonEmptyString([
149
+ overrides.source_route_key,
150
+ parsedArchive.sourceRouteKey,
151
+ parsedArchive.metadata?.source_route_key,
152
+ ]),
153
+ source_bot_username: firstNonEmptyString([
154
+ overrides.source_bot_username,
155
+ parsedArchive.sourceBotUsername,
156
+ parsedArchive.metadata?.source_bot_username,
157
+ ]),
122
158
  });
123
159
  }
124
160
 
@@ -155,6 +191,60 @@ export function buildTelegramBotReplyEnvelope({
155
191
  });
156
192
  }
157
193
 
194
+ export function buildTelegramMessageEnvelopeKey(rawEnvelopeOrChatID, rawMessageID = undefined) {
195
+ const envelope = rawMessageID === undefined
196
+ ? normalizeTelegramMessageEnvelope(rawEnvelopeOrChatID)
197
+ : normalizeTelegramMessageEnvelope({
198
+ chat_id: rawEnvelopeOrChatID,
199
+ message_id: rawMessageID,
200
+ });
201
+ const chatID = String(envelope.chat_id || "").trim();
202
+ const messageID = intFromRawAllowZero(envelope.message_id, 0);
203
+ if (!chatID || !(messageID > 0)) {
204
+ return "";
205
+ }
206
+ return `${chatID}:${messageID}`;
207
+ }
208
+
209
+ export function findRecentTelegramMessageEnvelope(rawMap, { chatID = "", messageID = 0 } = {}) {
210
+ const envelopeKey = buildTelegramMessageEnvelopeKey(chatID, messageID);
211
+ if (!envelopeKey) {
212
+ return {};
213
+ }
214
+ return normalizeTelegramMessageEnvelope(safeObject(safeObject(rawMap)[envelopeKey]));
215
+ }
216
+
217
+ export function mergeRecentTelegramMessageEnvelopes(rawMap, incomingEnvelopes, limit = 128) {
218
+ const merged = new Map();
219
+ for (const [key, value] of Object.entries(safeObject(rawMap))) {
220
+ const normalized = normalizeTelegramMessageEnvelope(value);
221
+ const normalizedKey = buildTelegramMessageEnvelopeKey(normalized) || String(key || "").trim();
222
+ if (!normalizedKey || !Object.keys(normalized).length) {
223
+ continue;
224
+ }
225
+ merged.set(normalizedKey, normalized);
226
+ }
227
+ for (const rawEnvelope of ensureArray(incomingEnvelopes)) {
228
+ const normalized = normalizeTelegramMessageEnvelope(rawEnvelope);
229
+ const normalizedKey = buildTelegramMessageEnvelopeKey(normalized);
230
+ if (!normalizedKey || !Object.keys(normalized).length) {
231
+ continue;
232
+ }
233
+ if (merged.has(normalizedKey)) {
234
+ merged.delete(normalizedKey);
235
+ }
236
+ merged.set(normalizedKey, normalized);
237
+ }
238
+ while (merged.size > Math.max(1, intFromRawAllowZero(limit, 128))) {
239
+ const oldestKey = merged.keys().next().value;
240
+ if (!oldestKey) {
241
+ break;
242
+ }
243
+ merged.delete(oldestKey);
244
+ }
245
+ return Object.fromEntries(merged.entries());
246
+ }
247
+
158
248
  function normalizePendingSelectionOptions(rawOptions) {
159
249
  const options = safeObject(rawOptions);
160
250
  const maxPendingAgeMs = intFromRawAllowZero(options.maxPendingAgeMs, 0);
@@ -194,6 +284,28 @@ function normalizeMentionSelector(rawValue) {
194
284
  return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
195
285
  }
196
286
 
287
+ export function isTelegramLocalInboundEnvelopeForRoute(rawEnvelope, {
288
+ routeKey = "",
289
+ botUsername = "",
290
+ } = {}) {
291
+ const envelope = normalizeTelegramMessageEnvelope(rawEnvelope);
292
+ const sourceOrigin = String(envelope.source_origin || "").trim().toLowerCase();
293
+ if (sourceOrigin !== "local_telegram_inbound") {
294
+ return false;
295
+ }
296
+ const expectedRouteKey = String(routeKey || "").trim();
297
+ const actualRouteKey = String(envelope.source_route_key || "").trim();
298
+ if (expectedRouteKey && expectedRouteKey !== actualRouteKey) {
299
+ return false;
300
+ }
301
+ const expectedBotUsername = normalizeMentionSelector(botUsername);
302
+ const actualBotUsername = normalizeMentionSelector(envelope.source_bot_username || "");
303
+ if (expectedBotUsername && expectedBotUsername !== actualBotUsername) {
304
+ return false;
305
+ }
306
+ return true;
307
+ }
308
+
197
309
  function uniqueNormalizedSelectors(values) {
198
310
  return Array.from(new Set(
199
311
  ensureArray(values)
@@ -7,6 +7,8 @@ import {
7
7
  buildRunnerRouteStateFromComment,
8
8
  compareArchiveCommentRecords,
9
9
  dedupeProcessableArchiveComments,
10
+ findRecentTelegramMessageEnvelope,
11
+ isTelegramLocalInboundEnvelopeForRoute,
10
12
  isInboundArchiveKind,
11
13
  normalizeTelegramMessageEnvelope,
12
14
  selectPendingArchiveComments,
@@ -376,11 +378,59 @@ function normalizeBoundaryViolations(rawViolations) {
376
378
  .filter(Boolean);
377
379
  }
378
380
 
379
- function normalizeMentionSelector(value) {
380
- return String(value || "").trim().replace(/^@+/, "").toLowerCase();
381
- }
382
-
383
- function escapeRegExp(text) {
381
+ function normalizeMentionSelector(value) {
382
+ return String(value || "").trim().replace(/^@+/, "").toLowerCase();
383
+ }
384
+
385
+ function resolveRunnerDeliverySourceMessageEnvelope({
386
+ routeState,
387
+ persistedRequest,
388
+ selectedRecord,
389
+ routeKey,
390
+ currentBotSelector,
391
+ }) {
392
+ const archiveEnvelope = buildTelegramMessageEnvelopeFromParsedArchive(selectedRecord?.parsedArchive, {
393
+ source_origin: "archive_reconstructed",
394
+ source_route_key: String(routeKey || "").trim(),
395
+ source_bot_username: currentBotSelector,
396
+ });
397
+ const archiveChatID = String(archiveEnvelope.chat_id || "").trim();
398
+ const archiveMessageID = intFromRawAllowZero(archiveEnvelope.message_id, 0);
399
+ const routeLocalEnvelope = findRecentTelegramMessageEnvelope(
400
+ safeObject(routeState).recent_local_inbound_envelopes,
401
+ {
402
+ chatID: archiveChatID,
403
+ messageID: archiveMessageID,
404
+ },
405
+ );
406
+ if (isTelegramLocalInboundEnvelopeForRoute(routeLocalEnvelope, {
407
+ routeKey,
408
+ botUsername: currentBotSelector,
409
+ })) {
410
+ return routeLocalEnvelope;
411
+ }
412
+ const persistedSourceEnvelope = normalizeTelegramMessageEnvelope(
413
+ safeObject(persistedRequest).source_message_envelope
414
+ || safeObject(persistedRequest).sourceMessageEnvelope,
415
+ );
416
+ const persistedChatID = String(persistedSourceEnvelope.chat_id || "").trim();
417
+ const persistedMessageID = intFromRawAllowZero(persistedSourceEnvelope.message_id, 0);
418
+ if (
419
+ isTelegramLocalInboundEnvelopeForRoute(persistedSourceEnvelope, {
420
+ routeKey,
421
+ botUsername: currentBotSelector,
422
+ })
423
+ && archiveChatID
424
+ && archiveChatID === persistedChatID
425
+ && archiveMessageID > 0
426
+ && archiveMessageID === persistedMessageID
427
+ ) {
428
+ return persistedSourceEnvelope;
429
+ }
430
+ return archiveEnvelope;
431
+ }
432
+
433
+ function escapeRegExp(text) {
384
434
  return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
385
435
  }
386
436
 
@@ -3338,21 +3388,22 @@ async function maybeExecuteDynamicRolePlan({
3338
3388
  };
3339
3389
  }
3340
3390
 
3341
- function buildConversationPeerMap(bot, normalizedRoute, deps) {
3342
- const peers = typeof deps?.resolveConversationPeerBots === "function"
3343
- ? ensureArray(deps.resolveConversationPeerBots(normalizedRoute))
3344
- : [];
3345
- const output = new Map();
3346
- const register = (peerRaw) => {
3347
- const peer = safeObject(peerRaw);
3348
- const displayName = String(peer.name || peer.username || peer.id || "").trim();
3349
- const selectors = uniqueOrdered([
3350
- normalizeMentionSelector(peer.username),
3351
- normalizeMentionSelector(peer.name),
3352
- ]);
3353
- for (const selector of selectors) {
3354
- output.set(selector, {
3355
- selector,
3391
+ function buildConversationPeerMap(bot, normalizedRoute, deps) {
3392
+ const peers = typeof deps?.resolveConversationPeerBots === "function"
3393
+ ? ensureArray(deps.resolveConversationPeerBots(normalizedRoute))
3394
+ : [];
3395
+ const output = new Map();
3396
+ const register = (peerRaw) => {
3397
+ const peer = safeObject(peerRaw);
3398
+ const displayName = String(peer.name || peer.username || peer.id || "").trim();
3399
+ const explicitUsername = normalizeMentionSelector(peer.username);
3400
+ const selectors = uniqueOrdered([
3401
+ explicitUsername,
3402
+ explicitUsername ? "" : normalizeMentionSelector(peer.name),
3403
+ ]);
3404
+ for (const selector of selectors) {
3405
+ output.set(selector, {
3406
+ selector,
3356
3407
  displayName: displayName || selector,
3357
3408
  id: String(peer.id || "").trim(),
3358
3409
  });
@@ -4720,10 +4771,18 @@ export async function processRunnerSelectedRecord({
4720
4771
  safeObject(persistedHumanIntentRequest).reply_chain_context
4721
4772
  || safeObject(persistedHumanIntentRequest).replyChainContext,
4722
4773
  );
4723
- const sourceMessageEnvelope = buildTelegramMessageEnvelopeFromParsedArchive(selectedRecord?.parsedArchive);
4774
+ const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
4775
+ const sourceMessageEnvelope = resolveRunnerDeliverySourceMessageEnvelope({
4776
+ routeState,
4777
+ persistedRequest: persistedHumanIntentRequest,
4778
+ selectedRecord,
4779
+ routeKey,
4780
+ currentBotSelector,
4781
+ });
4724
4782
  const replyMessageThreadID = intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0);
4725
4783
  const replyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
4726
- const replyAnchorSource = replyToMessageID > 0 ? "source_message_envelope" : "";
4784
+ const replyAnchorSource = String(sourceMessageEnvelope.source_origin || "").trim()
4785
+ || (replyToMessageID > 0 ? "source_message_envelope" : "");
4727
4786
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4728
4787
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
4729
4788
  ? executionDeps.validateWorkspaceArtifacts
@@ -4734,7 +4793,6 @@ export async function processRunnerSelectedRecord({
4734
4793
  const resolveInformationalQueryReply = typeof executionDeps.resolveInformationalQueryReply === "function"
4735
4794
  ? executionDeps.resolveInformationalQueryReply
4736
4795
  : null;
4737
- const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
4738
4796
  const triggerDecision = safeObject(precomputedTriggerDecision);
4739
4797
  const effectiveTriggerDecision = typeof triggerDecision.shouldRespond === "boolean"
4740
4798
  ? triggerDecision
@@ -4884,7 +4942,10 @@ export async function processRunnerSelectedRecord({
4884
4942
  && !precomputedConversationResponderSelection;
4885
4943
  if (!currentBotSelected && !shouldDeferBotReplyConversationAuthorization) {
4886
4944
  const adjudicationDecision = String(responderAdjudication.decision || "").trim() || "no_responder";
4887
- const adjudicationReason = String(responderAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator";
4945
+ const rawAdjudicationReason = String(responderAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator";
4946
+ const adjudicationReason = rawAdjudicationReason === "precomputed_human_intent_contract" && selectedResponderSelectors.length > 0
4947
+ ? "precomputed_human_intent_contract_selected_other_responder"
4948
+ : rawAdjudicationReason;
4888
4949
  saveRunnerRouteState(
4889
4950
  routeKey,
4890
4951
  buildRunnerRouteStateFromComment(selectedRecord, {
@@ -5592,6 +5653,7 @@ export async function processRunnerSelectedRecord({
5592
5653
  disableWebPagePreview: true,
5593
5654
  messageThreadID: replyMessageThreadID,
5594
5655
  replyToMessageID,
5656
+ sourceMessageEnvelope: sourceMessageEnvelope,
5595
5657
  archiveReplies: normalizedRoute.archivePolicy.mirrorReplies,
5596
5658
  archiveDedupeOutbound: normalizedRoute.archivePolicy.dedupeOutbound,
5597
5659
  archiveThreadID: archiveThread.threadID,
@@ -1,3 +1,8 @@
1
+ import {
2
+ mergeRecentTelegramMessageEnvelopes,
3
+ normalizeTelegramMessageEnvelope,
4
+ } from "./runner-helpers.mjs";
5
+
1
6
  function safeObject(value) {
2
7
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3
8
  return {};
@@ -198,6 +203,40 @@ function mergeRunnerSharedInboxUpdates(existingUpdates, incomingUpdates) {
198
203
  .slice(-500);
199
204
  }
200
205
 
206
+ const RUNNER_RECENT_LOCAL_INBOUND_ENVELOPE_LIMIT = 200;
207
+
208
+ function buildRunnerRecentLocalInboundEnvelopes(routeStateRaw, updates, routeKey, bot, destination) {
209
+ const routeState = safeObject(routeStateRaw);
210
+ const currentBotSelector = normalizeMentionSelector(
211
+ bot?.username
212
+ || bot?.name
213
+ || "",
214
+ );
215
+ const relevantEnvelopes = ensureArray(updates)
216
+ .filter((update) => String(update.chatID || "").trim() === String(destination?.chatID || "").trim())
217
+ .filter((update) => String(update.text || "").trim())
218
+ .map((update) => normalizeTelegramMessageEnvelope({
219
+ chat_id: update.chatID,
220
+ message_id: update.messageID,
221
+ message_thread_id: update.messageThreadID,
222
+ reply_to_message_id: update.replyToMessageID,
223
+ kind: update.fromIsBot ? "bot_reply" : "telegram_message",
224
+ sender: update.fromName,
225
+ sender_username: update.fromUsername,
226
+ sender_is_bot: update.fromIsBot === true,
227
+ body: update.text,
228
+ source_origin: "local_telegram_inbound",
229
+ source_route_key: String(routeKey || "").trim(),
230
+ source_bot_username: currentBotSelector,
231
+ }))
232
+ .filter((envelope) => Object.keys(envelope).length > 0);
233
+ return mergeRecentTelegramMessageEnvelopes(
234
+ routeState.recent_local_inbound_envelopes,
235
+ relevantEnvelopes,
236
+ RUNNER_RECENT_LOCAL_INBOUND_ENVELOPE_LIMIT,
237
+ );
238
+ }
239
+
201
240
  const RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS = 10 * 60 * 1000;
202
241
  const runnerInboundArchiveReservations = new Map();
203
242
 
@@ -470,6 +509,13 @@ export async function archiveLocalTelegramMessagesForRoute({
470
509
  .sort((left, right) => intFromRawAllowZero(left.updateID, 0) - intFromRawAllowZero(right.updateID, 0));
471
510
  let handledUpdateID = lastUpdateID;
472
511
  const mergedSharedUpdates = mergeRunnerSharedInboxUpdates(sharedInbox.updates, updates);
512
+ const recentLocalInboundEnvelopes = buildRunnerRecentLocalInboundEnvelopes(
513
+ routeState,
514
+ mergedSharedUpdates,
515
+ routeKey,
516
+ bot,
517
+ destination,
518
+ );
473
519
 
474
520
  const persistPollingProgress = (remainingSharedUpdates = []) => {
475
521
  if (sharedInboxKey && saveBotRunnerState) {
@@ -489,6 +535,7 @@ export async function archiveLocalTelegramMessagesForRoute({
489
535
  local_telegram_polling_ready: true,
490
536
  last_provider_update_id: handledUpdateID,
491
537
  last_local_poll_at: new Date().toISOString(),
538
+ recent_local_inbound_envelopes: recentLocalInboundEnvelopes,
492
539
  });
493
540
  };
494
541
 
@@ -564,7 +611,12 @@ export async function archiveLocalTelegramMessagesForRoute({
564
611
  timeoutSeconds: runtime.timeoutSeconds,
565
612
  threadID: archiveThread.threadID,
566
613
  actorUserID: runtime.actor.user_id,
567
- body: formatTelegramInboundArchiveComment(update),
614
+ body: formatTelegramInboundArchiveComment({
615
+ ...update,
616
+ sourceOrigin: "local_telegram_inbound",
617
+ sourceRouteKey: String(routeKey || "").trim(),
618
+ sourceBotUsername: normalizeMentionSelector(bot?.username || bot?.name),
619
+ }),
568
620
  });
569
621
  } catch (err) {
570
622
  releaseRunnerInboundArchiveMessageReservation(reservation.reservationKey);
@@ -35,19 +35,11 @@ function normalizeTelegramMentionUsername(rawValue) {
35
35
  }
36
36
 
37
37
  function listTelegramMentionUsernames(parsedArchive) {
38
- const usernames = new Set(
38
+ return uniqueOrdered(
39
39
  ensureArray(parsedArchive?.mentionUsernames)
40
40
  .map((value) => normalizeTelegramMentionUsername(value))
41
41
  .filter(Boolean),
42
42
  );
43
- const text = String(parsedArchive?.body || "").trim().toLowerCase();
44
- for (const match of text.matchAll(/@([a-z0-9_]{3,})/gi)) {
45
- const normalized = normalizeTelegramMentionUsername(match?.[1]);
46
- if (normalized) {
47
- usernames.add(normalized);
48
- }
49
- }
50
- return Array.from(usernames);
51
43
  }
52
44
 
53
45
  function inferTelegramArchiveChatType(parsedArchive) {
@@ -60,10 +52,13 @@ function inferTelegramArchiveChatType(parsedArchive) {
60
52
 
61
53
  function buildTelegramBotUsernameCandidates(bot, route) {
62
54
  const set = new Set();
55
+ const explicitUsername = normalizeTelegramMentionUsername(bot?.username);
56
+ const fallbackDisplayCandidates = explicitUsername
57
+ ? []
58
+ : [bot?.name, route?.botName];
63
59
  for (const candidate of [
64
- bot?.username,
65
- bot?.name,
66
- route?.botName,
60
+ explicitUsername,
61
+ ...fallbackDisplayCandidates,
67
62
  ]) {
68
63
  const normalized = normalizeTelegramMentionUsername(candidate);
69
64
  if (normalized) {
@@ -74,13 +69,11 @@ function buildTelegramBotUsernameCandidates(bot, route) {
74
69
  }
75
70
 
76
71
  function doesTelegramArchiveMentionBot(parsedArchive, bot, route) {
77
- const text = String(parsedArchive?.body || "").trim().toLowerCase();
78
72
  const mentions = new Set(listTelegramMentionUsernames(parsedArchive));
79
73
  const candidates = buildTelegramBotUsernameCandidates(bot, route);
80
74
  for (const username of candidates) {
81
75
  if (!username) continue;
82
76
  if (mentions.has(username)) return true;
83
- if (text.includes(`@${username}`)) return true;
84
77
  }
85
78
  return false;
86
79
  }
@@ -117,7 +117,8 @@ export async function runSelftestRunnerScenarios(push, deps) {
117
117
  const tryJsonParse = requireDependency(deps, "tryJsonParse");
118
118
  const safeObject = requireDependency(deps, "safeObject");
119
119
  const normalizeRunnerTriggerPolicy = requireDependency(deps, "normalizeRunnerTriggerPolicy");
120
- const evaluateTelegramRunnerTrigger = requireDependency(deps, "evaluateTelegramRunnerTrigger");
120
+ const evaluateTelegramRunnerTrigger = requireDependency(deps, "evaluateTelegramRunnerTrigger");
121
+ const resolveHumanIntentContext = requireDependency(deps, "resolveHumanIntentContext");
121
122
  const resolveRunnerResponderAdjudication = requireDependency(deps, "resolveRunnerResponderAdjudication");
122
123
  const selectPendingArchiveComments = requireDependency(deps, "selectPendingArchiveComments");
123
124
  const selectRunnerPendingWork = requireDependency(deps, "selectRunnerPendingWork");
@@ -2464,13 +2465,80 @@ export async function runSelftestRunnerScenarios(push, deps) {
2464
2465
  mentionOnlyRoute,
2465
2466
  { name: "ServerProtocolMonitorBot", role: "monitor" },
2466
2467
  );
2467
- push(
2468
- "telegram_trigger_mentions_only_accepts_bot_mention",
2469
- mentionOnlyMatched.shouldRespond === true && mentionOnlyMatched.trigger === "mention",
2470
- `shouldRespond=${mentionOnlyMatched.shouldRespond} trigger=${mentionOnlyMatched.trigger}`,
2471
- );
2472
-
2473
- const mentionOverridesReplyForUnmentionedBot = evaluateTelegramRunnerTrigger(
2468
+ push(
2469
+ "telegram_trigger_mentions_only_accepts_bot_mention",
2470
+ mentionOnlyMatched.shouldRespond === true && mentionOnlyMatched.trigger === "mention",
2471
+ `shouldRespond=${mentionOnlyMatched.shouldRespond} trigger=${mentionOnlyMatched.trigger}`,
2472
+ );
2473
+
2474
+ const mentionOnlyAliasCandidate = evaluateTelegramRunnerTrigger(
2475
+ {
2476
+ id: "comment-2a",
2477
+ parsedArchive: {
2478
+ kind: "telegram_message",
2479
+ chatID: "-100123",
2480
+ chatType: "supergroup",
2481
+ body: "hello @RyoAI_bot2",
2482
+ mentionUsernames: [],
2483
+ replyToSenderIsBot: false,
2484
+ },
2485
+ },
2486
+ mentionOnlyRoute,
2487
+ { username: "ryoai2_bot", name: "RyoAI_bot2", role: "monitor" },
2488
+ );
2489
+ push(
2490
+ "telegram_trigger_does_not_treat_raw_alias_text_as_authoritative_mention",
2491
+ mentionOnlyAliasCandidate.shouldRespond === true
2492
+ && mentionOnlyAliasCandidate.trigger === "mentions_only_unaddressed_candidate",
2493
+ `shouldRespond=${mentionOnlyAliasCandidate.shouldRespond} trigger=${mentionOnlyAliasCandidate.trigger}`,
2494
+ );
2495
+
2496
+ try {
2497
+ const aliasHumanIntentContext = await resolveHumanIntentContext({
2498
+ selectedRecord: {
2499
+ id: "comment-alias-managed-mention",
2500
+ parsedArchive: {
2501
+ kind: "telegram_message",
2502
+ body: "@RyoAI_bot2 hi",
2503
+ senderIsBot: false,
2504
+ },
2505
+ },
2506
+ normalizedRoute: {
2507
+ name: "telegram-monitor-ryoai2-bot-2",
2508
+ },
2509
+ bot: {
2510
+ username: "ryoai2_bot",
2511
+ name: "RyoAI_bot2",
2512
+ },
2513
+ executionPlan: {},
2514
+ deps: {
2515
+ resolveConversationPeerBots: () => [],
2516
+ },
2517
+ persistedRequest: {
2518
+ conversation_intent_mode: "single_bot",
2519
+ conversation_reply_expectation: "informational",
2520
+ conversation_initial_responders: ["ryoai2_bot"],
2521
+ conversation_allowed_responders: ["ryoai2_bot"],
2522
+ conversation_participants: ["ryoai2_bot"],
2523
+ conversation_lead_bot: "ryoai2_bot",
2524
+ conversation_summary_bot: "ryoai2_bot",
2525
+ },
2526
+ });
2527
+ push(
2528
+ "runner_human_intent_context_ignores_raw_alias_selector_for_managed_mentions",
2529
+ ensureArray(aliasHumanIntentContext?.managedMentions).length === 0
2530
+ && aliasHumanIntentContext?.reusedPersistedContract === true,
2531
+ `managedMentions=${JSON.stringify(ensureArray(aliasHumanIntentContext?.managedMentions))} reused=${String(aliasHumanIntentContext?.reusedPersistedContract)}`,
2532
+ );
2533
+ } catch (err) {
2534
+ push(
2535
+ "runner_human_intent_context_ignores_raw_alias_selector_for_managed_mentions",
2536
+ false,
2537
+ String(err?.message || err),
2538
+ );
2539
+ }
2540
+
2541
+ const mentionOverridesReplyForUnmentionedBot = evaluateTelegramRunnerTrigger(
2474
2542
  {
2475
2543
  id: "comment-2b",
2476
2544
  parsedArchive: {
@@ -12829,6 +12897,298 @@ export async function runSelftestRunnerScenarios(push, deps) {
12829
12897
  push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
12830
12898
  }
12831
12899
 
12900
+ try {
12901
+ let capturedReplyToMessageID = 0;
12902
+ let capturedMessageThreadID = 0;
12903
+ let capturedSourceMessageEnvelope = {};
12904
+ const processed = await processRunnerSelectedRecord({
12905
+ routeKey: "delivery-prefers-route-local-inbound-envelope-key",
12906
+ normalizedRoute: normalizeRunnerRoute({
12907
+ name: "telegram-monitor-delivery-prefers-route-local-inbound-envelope",
12908
+ project_id: selftestProjectID,
12909
+ provider: "telegram",
12910
+ role: "monitor",
12911
+ role_profile: "monitor",
12912
+ destination_id: "dest-1",
12913
+ destination_label: "Main Room",
12914
+ server_bot_name: "RyoAI_bot",
12915
+ server_bot_id: "bot-1",
12916
+ trigger_policy: {
12917
+ mentions_only: true,
12918
+ direct_messages: true,
12919
+ reply_to_bot_messages: true,
12920
+ },
12921
+ archive_policy: {
12922
+ mirror_replies: true,
12923
+ dedupe_inbound: true,
12924
+ dedupe_outbound: true,
12925
+ skip_bot_messages: true,
12926
+ },
12927
+ dry_run_delivery: false,
12928
+ }),
12929
+ routeState: {
12930
+ recent_local_inbound_envelopes: {
12931
+ "-100123:128": {
12932
+ chat_id: "-100123",
12933
+ message_id: 128,
12934
+ message_thread_id: 912,
12935
+ reply_to_message_id: 127,
12936
+ kind: "telegram_message",
12937
+ sender: "human",
12938
+ sender_is_bot: false,
12939
+ body: "@RyoAI_bot hi",
12940
+ source_origin: "local_telegram_inbound",
12941
+ source_route_key: "delivery-prefers-route-local-inbound-envelope-key",
12942
+ source_bot_username: "ryoai_bot",
12943
+ },
12944
+ },
12945
+ },
12946
+ selectedRecord: {
12947
+ id: "comment-delivery-prefers-route-local-inbound-envelope",
12948
+ createdAt: "2026-03-27T00:00:00.000Z",
12949
+ parsedArchive: {
12950
+ kind: "telegram_message",
12951
+ chatID: "-100123",
12952
+ chatType: "supergroup",
12953
+ body: "@RyoAI_bot hi",
12954
+ messageID: 128,
12955
+ messageThreadID: 912,
12956
+ replyToMessageID: 127,
12957
+ sender: "human",
12958
+ senderIsBot: false,
12959
+ mentionUsernames: ["ryoai_bot"],
12960
+ },
12961
+ },
12962
+ pendingOrdered: [],
12963
+ bot: {
12964
+ id: "bot-1",
12965
+ name: "RyoAI_bot",
12966
+ username: "RyoAI_bot",
12967
+ role: "monitor",
12968
+ provider: "telegram",
12969
+ },
12970
+ destination: {
12971
+ id: "dest-1",
12972
+ label: "Main Room",
12973
+ provider: "telegram",
12974
+ chatID: "-100123",
12975
+ },
12976
+ archiveThread: {
12977
+ threadID: "thread-1",
12978
+ workItemID: "work-item-1",
12979
+ },
12980
+ executionPlan: {
12981
+ mode: "role_profile",
12982
+ roleProfileName: "monitor",
12983
+ roleProfile: {
12984
+ client: "sample",
12985
+ model: "",
12986
+ permissionMode: "read_only",
12987
+ reasoningEffort: "low",
12988
+ },
12989
+ workspaceDir: "",
12990
+ workspaceSource: "selftest",
12991
+ usedCommandFallback: false,
12992
+ },
12993
+ runtime: {
12994
+ baseURL: "https://example.test",
12995
+ token: "selftest-token",
12996
+ timeoutSeconds: 30,
12997
+ actor: { user_id: "user-1" },
12998
+ },
12999
+ deps: {
13000
+ saveRunnerRouteState: () => {},
13001
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
13002
+ runRunnerAIExecution: async () => ({
13003
+ skip: false,
13004
+ reply: "Hello from RyoAI_bot.",
13005
+ }),
13006
+ performLocalBotDelivery: async ({ replyToMessageID, messageThreadID, sourceMessageEnvelope }) => {
13007
+ capturedReplyToMessageID = Number(replyToMessageID || 0);
13008
+ capturedMessageThreadID = Number(messageThreadID || 0);
13009
+ capturedSourceMessageEnvelope = safeObject(sourceMessageEnvelope);
13010
+ return {
13011
+ delivery: {
13012
+ dryRun: false,
13013
+ body: {
13014
+ result: {
13015
+ message_id: 9001,
13016
+ message_thread_id: Number(messageThreadID || 0) || undefined,
13017
+ },
13018
+ },
13019
+ },
13020
+ archive: {},
13021
+ };
13022
+ },
13023
+ serializeRunnerTriggerPolicy: (value) => value,
13024
+ serializeRunnerArchivePolicy: (value) => value,
13025
+ buildRunnerExecutionDeps: () => ({
13026
+ validateWorkspaceArtifacts,
13027
+ analyzeHumanConversationIntentWithAI: async () => ({
13028
+ mode: "single_bot",
13029
+ lead_bot: "ryoai_bot",
13030
+ participants: ["ryoai_bot"],
13031
+ initial_responders: ["ryoai_bot"],
13032
+ allowed_responders: ["ryoai_bot"],
13033
+ summary_bot: "",
13034
+ allow_bot_to_bot: false,
13035
+ reply_expectation: "informational",
13036
+ intent_type: "small_talk",
13037
+ }),
13038
+ }),
13039
+ buildRunnerDeliveryDeps: () => ({}),
13040
+ buildRunnerRuntimeDeps: () => ({}),
13041
+ resolveConversationPeerBots: () => [],
13042
+ },
13043
+ });
13044
+ push(
13045
+ "runner_delivery_prefers_route_local_inbound_provenance_envelope",
13046
+ processed.kind === "replied"
13047
+ && capturedReplyToMessageID === 128
13048
+ && capturedMessageThreadID === 912
13049
+ && String(capturedSourceMessageEnvelope.source_origin || "") === "local_telegram_inbound"
13050
+ && Number(capturedSourceMessageEnvelope.message_id || 0) === 128,
13051
+ `kind=${String(processed.kind || "(none)")} reply_to=${String(capturedReplyToMessageID || 0)} thread=${String(capturedMessageThreadID || 0)} origin=${String(capturedSourceMessageEnvelope.source_origin || "(none)")} message=${String(capturedSourceMessageEnvelope.message_id || "(none)")}`,
13052
+ );
13053
+ } catch (err) {
13054
+ push("runner_delivery_prefers_route_local_inbound_provenance_envelope", false, String(err?.message || err));
13055
+ }
13056
+
13057
+ try {
13058
+ let capturedSourceMessageEnvelope = {};
13059
+ const processed = await processRunnerSelectedRecord({
13060
+ routeKey: "telegram-monitor-ryoai2-bot-2::project::telegram::monitor::dest::actor",
13061
+ normalizedRoute: {
13062
+ name: "telegram-monitor-ryoai2-bot-2",
13063
+ provider: "telegram",
13064
+ role: "monitor",
13065
+ roleProfile: "monitor",
13066
+ botName: "RyoAI2_bot",
13067
+ archivePolicy: { mirrorReplies: true, dedupeOutbound: true },
13068
+ triggerPolicy: {
13069
+ directMessages: true,
13070
+ mentionsOnly: false,
13071
+ replyToBotMessages: true,
13072
+ ignoreEditedMessages: true,
13073
+ },
13074
+ },
13075
+ routeState: {
13076
+ recent_local_inbound_envelopes: {},
13077
+ },
13078
+ selectedRecord: {
13079
+ id: "comment-foreign-provenance",
13080
+ threadID: "thread-1",
13081
+ parsedArchive: {
13082
+ kind: "telegram_message",
13083
+ chatID: "-100123",
13084
+ chatType: "supergroup",
13085
+ body: "@RyoAI2_bot hi",
13086
+ messageID: 228,
13087
+ sender: "human",
13088
+ senderIsBot: false,
13089
+ mentionUsernames: ["ryoai2_bot"],
13090
+ },
13091
+ },
13092
+ persistedHumanIntentRequest: {
13093
+ source_message_envelope: {
13094
+ chat_id: "-100123",
13095
+ message_id: 228,
13096
+ source_origin: "local_telegram_inbound",
13097
+ source_route_key: "telegram-monitor-ryoai-bot-2::project::telegram::monitor::dest::actor",
13098
+ source_bot_username: "ryoai_bot",
13099
+ },
13100
+ },
13101
+ pendingOrdered: [],
13102
+ bot: {
13103
+ id: "bot-2",
13104
+ name: "RyoAI2_bot",
13105
+ username: "RyoAI2_bot",
13106
+ role: "monitor",
13107
+ provider: "telegram",
13108
+ },
13109
+ destination: {
13110
+ id: "dest-1",
13111
+ label: "Main Room",
13112
+ provider: "telegram",
13113
+ chatID: "-100123",
13114
+ },
13115
+ archiveThread: {
13116
+ threadID: "thread-1",
13117
+ workItemID: "work-item-1",
13118
+ },
13119
+ executionPlan: {
13120
+ mode: "role_profile",
13121
+ roleProfileName: "monitor",
13122
+ roleProfile: {
13123
+ client: "sample",
13124
+ model: "",
13125
+ permissionMode: "read_only",
13126
+ reasoningEffort: "low",
13127
+ },
13128
+ workspaceDir: "",
13129
+ workspaceSource: "selftest",
13130
+ usedCommandFallback: false,
13131
+ },
13132
+ runtime: {
13133
+ baseURL: "https://example.test",
13134
+ token: "selftest-token",
13135
+ timeoutSeconds: 30,
13136
+ actor: { user_id: "user-1" },
13137
+ },
13138
+ deps: {
13139
+ saveRunnerRouteState: () => {},
13140
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
13141
+ runRunnerAIExecution: async () => ({
13142
+ skip: false,
13143
+ reply: "Hello from RyoAI2_bot.",
13144
+ }),
13145
+ performLocalBotDelivery: async ({ sourceMessageEnvelope }) => {
13146
+ capturedSourceMessageEnvelope = safeObject(sourceMessageEnvelope);
13147
+ return {
13148
+ delivery: {
13149
+ dryRun: false,
13150
+ body: {
13151
+ result: {
13152
+ message_id: 9002,
13153
+ },
13154
+ },
13155
+ },
13156
+ archive: {},
13157
+ };
13158
+ },
13159
+ serializeRunnerTriggerPolicy: (value) => value,
13160
+ serializeRunnerArchivePolicy: (value) => value,
13161
+ buildRunnerExecutionDeps: () => ({
13162
+ validateWorkspaceArtifacts,
13163
+ analyzeHumanConversationIntentWithAI: async () => ({
13164
+ mode: "single_bot",
13165
+ lead_bot: "ryoai2_bot",
13166
+ participants: ["ryoai2_bot"],
13167
+ initial_responders: ["ryoai2_bot"],
13168
+ allowed_responders: ["ryoai2_bot"],
13169
+ summary_bot: "",
13170
+ allow_bot_to_bot: false,
13171
+ reply_expectation: "informational",
13172
+ intent_type: "small_talk",
13173
+ }),
13174
+ }),
13175
+ buildRunnerDeliveryDeps: () => ({}),
13176
+ buildRunnerRuntimeDeps: () => ({}),
13177
+ resolveConversationPeerBots: () => [],
13178
+ },
13179
+ });
13180
+ push(
13181
+ "runner_delivery_rejects_foreign_route_local_provenance",
13182
+ processed.kind === "replied"
13183
+ && String(capturedSourceMessageEnvelope.source_origin || "") === "archive_reconstructed"
13184
+ && String(capturedSourceMessageEnvelope.source_route_key || "").includes("telegram-monitor-ryoai2-bot-2")
13185
+ && String(capturedSourceMessageEnvelope.source_bot_username || "") === "ryoai2_bot",
13186
+ `kind=${String(processed.kind || "(none)")} origin=${String(capturedSourceMessageEnvelope.source_origin || "(none)")} route=${String(capturedSourceMessageEnvelope.source_route_key || "(none)")} bot=${String(capturedSourceMessageEnvelope.source_bot_username || "(none)")}`,
13187
+ );
13188
+ } catch (err) {
13189
+ push("runner_delivery_rejects_foreign_route_local_provenance", false, String(err?.message || err));
13190
+ }
13191
+
12832
13192
  try {
12833
13193
  const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-observed-artifacts-"));
12834
13194
  const scriptDir = path.join(workspaceDir, ".metheus", "runner-runtime", "local-ai-scratch");
@@ -503,17 +503,51 @@ export async function runSelftestTelegramE2E(push, deps) {
503
503
  dryRun: true,
504
504
  deps: buildRunnerDeliveryDeps(),
505
505
  });
506
- push(
507
- "telegram_delivery_dry_run_skips_send_and_archive",
508
- Boolean(dryRunResult.delivery?.dryRun)
509
- && String(dryRunResult.archive?.reason || "") === "dry_run_delivery"
510
- && telegramE2EServer.state.sentMessages.length === sentCountBeforeDryRun
511
- && telegramE2EServer.state.comments.length === commentCountBeforeDryRun,
512
- `dry_run=${String(dryRunResult.delivery?.dryRun || false)} sent=${telegramE2EServer.state.sentMessages.length} comments=${telegramE2EServer.state.comments.length}`,
513
- );
514
-
515
- telegramE2EServer.state.comments = [];
516
- telegramE2EServer.state.updates = [
506
+ push(
507
+ "telegram_delivery_dry_run_skips_send_and_archive",
508
+ Boolean(dryRunResult.delivery?.dryRun)
509
+ && String(dryRunResult.archive?.reason || "") === "dry_run_delivery"
510
+ && telegramE2EServer.state.sentMessages.length === sentCountBeforeDryRun
511
+ && telegramE2EServer.state.comments.length === commentCountBeforeDryRun,
512
+ `dry_run=${String(dryRunResult.delivery?.dryRun || false)} sent=${telegramE2EServer.state.sentMessages.length} comments=${telegramE2EServer.state.comments.length}`,
513
+ );
514
+ const sentCountBeforeUnsafeAnchor = telegramE2EServer.state.sentMessages.length;
515
+ let unsafeAnchorError = "";
516
+ try {
517
+ await performLocalBotDelivery({
518
+ siteBaseURL: telegramE2EServer.baseURL,
519
+ token: e2eToken,
520
+ timeoutSeconds: 10,
521
+ actorUserID: e2eActorUserID,
522
+ bot: e2eBot,
523
+ projectID: selftestProjectID,
524
+ provider: "telegram",
525
+ destinationSelectors: {
526
+ destinationLabel: e2eDestination.label,
527
+ },
528
+ text: "unsafe archive anchor",
529
+ replyToMessageID: 41,
530
+ sourceMessageEnvelope: {
531
+ chat_id: e2eDestination.chat_id,
532
+ message_id: 41,
533
+ source_origin: "archive_reconstructed",
534
+ },
535
+ archiveReplies: false,
536
+ dryRun: false,
537
+ deps: buildRunnerDeliveryDeps(),
538
+ });
539
+ } catch (err) {
540
+ unsafeAnchorError = String(err?.message || err);
541
+ }
542
+ push(
543
+ "telegram_delivery_rejects_archive_reconstructed_reply_anchor",
544
+ /telegram reply anchor is not local to this route/i.test(unsafeAnchorError)
545
+ && telegramE2EServer.state.sentMessages.length === sentCountBeforeUnsafeAnchor,
546
+ `error=${unsafeAnchorError || "(none)"} sent=${telegramE2EServer.state.sentMessages.length}`,
547
+ );
548
+
549
+ telegramE2EServer.state.comments = [];
550
+ telegramE2EServer.state.updates = [
517
551
  {
518
552
  update_id: 201,
519
553
  message: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.249",
3
+ "version": "0.2.252",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [