metheus-governance-mcp-cli 0.2.249 → 0.2.250

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,7 @@ import {
140
140
  buildRunnerRouteStateFromComment,
141
141
  buildProcessableArchiveLogicalKey,
142
142
  findEarlierProcessableArchiveDuplicate,
143
+ findRecentTelegramMessageEnvelope,
143
144
  isInboundArchiveKind,
144
145
  normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
145
146
  normalizeArchiveCommentRecord,
@@ -1989,6 +1990,7 @@ function mergeRunnerStateRecords(preferred, fallback) {
1989
1990
  last_followup_source_message_envelope: pickObjectField("last_followup_source_message_envelope"),
1990
1991
  last_followup_last_reply_message_envelope: pickObjectField("last_followup_last_reply_message_envelope"),
1991
1992
  last_followup_attempted_delivery_envelope: pickObjectField("last_followup_attempted_delivery_envelope"),
1993
+ recent_local_inbound_envelopes: pickObjectField("recent_local_inbound_envelopes"),
1992
1994
  last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
1993
1995
  last_normalized_execution_contract_targets: pickArrayField("last_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
1994
1996
  last_normalized_execution_next_responders: pickArrayField("last_normalized_execution_next_responders", normalizeTelegramMentionUsername),
@@ -2642,6 +2644,42 @@ function normalizeRunnerReplyChainContext(rawContext) {
2642
2644
  return normalized;
2643
2645
  }
2644
2646
 
2647
+ function findRunnerRouteLocalInboundEnvelope(routeStateRaw, parsedArchiveRaw) {
2648
+ const routeState = safeObject(routeStateRaw);
2649
+ const parsedArchive = safeObject(parsedArchiveRaw);
2650
+ const chatID = String(parsedArchive.chatID || parsedArchive.chatId || "").trim();
2651
+ const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2652
+ if (!chatID || !(messageID > 0)) {
2653
+ return {};
2654
+ }
2655
+ return findRecentTelegramMessageEnvelope(routeState.recent_local_inbound_envelopes, {
2656
+ chatID,
2657
+ messageID,
2658
+ });
2659
+ }
2660
+
2661
+ function buildRunnerSourceMessageEnvelope({
2662
+ routeState = {},
2663
+ routeKey = "",
2664
+ normalizedRoute = null,
2665
+ parsedArchive = null,
2666
+ }) {
2667
+ const localEnvelope = findRunnerRouteLocalInboundEnvelope(routeState, parsedArchive);
2668
+ if (String(localEnvelope.source_origin || "").trim().toLowerCase() === "local_telegram_inbound") {
2669
+ return localEnvelope;
2670
+ }
2671
+ const fallbackBotSelector = normalizeTelegramMentionUsername(
2672
+ normalizedRoute?.botName
2673
+ || normalizedRoute?.serverBotName
2674
+ || "",
2675
+ );
2676
+ return buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsedArchive, {
2677
+ source_origin: "archive_reconstructed",
2678
+ source_route_key: String(routeKey || "").trim(),
2679
+ source_bot_username: fallbackBotSelector,
2680
+ });
2681
+ }
2682
+
2645
2683
  function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2646
2684
  const normalized = {};
2647
2685
  for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
@@ -2676,6 +2714,7 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2676
2714
  body: entry.source_message_body || entry.sourceMessageBody,
2677
2715
  sender: "human",
2678
2716
  sender_is_bot: false,
2717
+ source_origin: "archive_reconstructed",
2679
2718
  }
2680
2719
  : {}),
2681
2720
  ),
@@ -4408,9 +4447,10 @@ async function claimRunnerRequestForHumanComment({
4408
4447
  reason: "non_human_comment_cannot_create_request",
4409
4448
  };
4410
4449
  }
4411
- const currentState = loadBotRunnerState();
4412
- const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4413
- state: currentState,
4450
+ const currentState = loadBotRunnerState();
4451
+ const currentRouteState = safeObject(safeObject(currentState.routes)[String(routeKey || "").trim()]);
4452
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
4453
+ state: currentState,
4414
4454
  normalizedRoute,
4415
4455
  selectedRecord,
4416
4456
  runtime,
@@ -4530,14 +4570,19 @@ async function claimRunnerRequestForHumanComment({
4530
4570
  };
4531
4571
  }
4532
4572
  const nowISO = new Date().toISOString();
4533
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4573
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
4534
4574
  project_id: String(normalizedRoute?.projectID || "").trim(),
4535
4575
  provider: String(normalizedRoute?.provider || "").trim(),
4536
4576
  chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4537
4577
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
4538
4578
  source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
4539
4579
  source_message_body: String(parsed.body || "").trim(),
4540
- source_message_envelope: buildRunnerTelegramMessageEnvelopeFromParsedArchive(parsed),
4580
+ source_message_envelope: buildRunnerSourceMessageEnvelope({
4581
+ routeState: currentRouteState,
4582
+ routeKey,
4583
+ normalizedRoute,
4584
+ parsedArchive: parsed,
4585
+ }),
4541
4586
  root_comment_id: String(selectedRecord?.id || "").trim(),
4542
4587
  root_comment_kind: commentKind,
4543
4588
  conversation_id: resolvedConversationID,
@@ -7552,11 +7597,11 @@ function parseArchivedChatComment(rawBody) {
7552
7597
  .filter(Boolean),
7553
7598
  }
7554
7599
  : null;
7555
- return {
7556
- kind,
7557
- header,
7558
- metadata,
7559
- body,
7600
+ return {
7601
+ kind,
7602
+ header,
7603
+ metadata,
7604
+ body,
7560
7605
  chatID: String(metadata.chat_id || "").trim(),
7561
7606
  chatType: String(metadata.chat_type || "").trim().toLowerCase(),
7562
7607
  messageID: intFromRawAllowZero(metadata.message_id, 0),
@@ -7569,8 +7614,11 @@ function parseArchivedChatComment(rawBody) {
7569
7614
  .split(",")
7570
7615
  .map((value) => normalizeTelegramMentionUsername(value))
7571
7616
  .filter(Boolean),
7572
- occurredAt: String(metadata.occurred_at || "").trim(),
7573
- replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7617
+ occurredAt: String(metadata.occurred_at || "").trim(),
7618
+ sourceOrigin: String(metadata.source_origin || "").trim().toLowerCase(),
7619
+ sourceRouteKey: String(metadata.source_route_key || "").trim(),
7620
+ sourceBotUsername: normalizeTelegramMentionUsername(metadata.source_bot_username || ""),
7621
+ replyToMessageID: intFromRawAllowZero(metadata.reply_to_message_id, 0),
7574
7622
  replyToSender: String(metadata.reply_to_sender || "").trim(),
7575
7623
  replyToUsername: String(metadata.reply_to_telegram_username || "").trim(),
7576
7624
  replyToSenderIsBot: boolFromRaw(metadata.reply_to_sender_is_bot, false),
@@ -7886,18 +7934,27 @@ function buildArchivedInboundMessageKey(chatID, messageID) {
7886
7934
  return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
7887
7935
  }
7888
7936
 
7889
- function formatTelegramInboundArchiveComment(normalized) {
7890
- const headerLines = [
7937
+ function formatTelegramInboundArchiveComment(normalized) {
7938
+ const headerLines = [
7891
7939
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
7892
7940
  `chat_id: ${normalized.chatID || "<missing>"}`,
7893
7941
  `chat_type: ${normalized.chatType || "unknown"}`,
7894
7942
  `message_id: ${normalized.messageID || "<missing>"}`,
7895
7943
  `occurred_at: ${normalized.occurredAt || new Date().toISOString()}`,
7896
7944
  `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) {
7945
+ `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
7946
+ `sender_is_bot: ${normalized.fromIsBot ? "true" : "false"}`,
7947
+ ];
7948
+ if (String(normalized.sourceOrigin || "").trim()) {
7949
+ headerLines.push(`source_origin: ${String(normalized.sourceOrigin || "").trim()}`);
7950
+ }
7951
+ if (String(normalized.sourceRouteKey || "").trim()) {
7952
+ headerLines.push(`source_route_key: ${String(normalized.sourceRouteKey || "").trim()}`);
7953
+ }
7954
+ if (String(normalized.sourceBotUsername || "").trim()) {
7955
+ headerLines.push(`source_bot_username: @${String(normalized.sourceBotUsername || "").trim().replace(/^@+/, "")}`);
7956
+ }
7957
+ if (normalized.fromUsername) {
7901
7958
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
7902
7959
  }
7903
7960
  if (normalized.mentionUsernames.length > 0) {
@@ -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);
@@ -7,6 +7,7 @@ import {
7
7
  buildRunnerRouteStateFromComment,
8
8
  compareArchiveCommentRecords,
9
9
  dedupeProcessableArchiveComments,
10
+ findRecentTelegramMessageEnvelope,
10
11
  isInboundArchiveKind,
11
12
  normalizeTelegramMessageEnvelope,
12
13
  selectPendingArchiveComments,
@@ -376,11 +377,54 @@ function normalizeBoundaryViolations(rawViolations) {
376
377
  .filter(Boolean);
377
378
  }
378
379
 
379
- function normalizeMentionSelector(value) {
380
- return String(value || "").trim().replace(/^@+/, "").toLowerCase();
381
- }
382
-
383
- function escapeRegExp(text) {
380
+ function normalizeMentionSelector(value) {
381
+ return String(value || "").trim().replace(/^@+/, "").toLowerCase();
382
+ }
383
+
384
+ function resolveRunnerDeliverySourceMessageEnvelope({
385
+ routeState,
386
+ persistedRequest,
387
+ selectedRecord,
388
+ routeKey,
389
+ currentBotSelector,
390
+ }) {
391
+ const archiveEnvelope = buildTelegramMessageEnvelopeFromParsedArchive(selectedRecord?.parsedArchive, {
392
+ source_origin: "archive_reconstructed",
393
+ source_route_key: String(routeKey || "").trim(),
394
+ source_bot_username: currentBotSelector,
395
+ });
396
+ const archiveChatID = String(archiveEnvelope.chat_id || "").trim();
397
+ const archiveMessageID = intFromRawAllowZero(archiveEnvelope.message_id, 0);
398
+ const routeLocalEnvelope = findRecentTelegramMessageEnvelope(
399
+ safeObject(routeState).recent_local_inbound_envelopes,
400
+ {
401
+ chatID: archiveChatID,
402
+ messageID: archiveMessageID,
403
+ },
404
+ );
405
+ if (String(routeLocalEnvelope.source_origin || "").trim().toLowerCase() === "local_telegram_inbound") {
406
+ return routeLocalEnvelope;
407
+ }
408
+ const persistedSourceEnvelope = normalizeTelegramMessageEnvelope(
409
+ safeObject(persistedRequest).source_message_envelope
410
+ || safeObject(persistedRequest).sourceMessageEnvelope,
411
+ );
412
+ const persistedOrigin = String(persistedSourceEnvelope.source_origin || "").trim().toLowerCase();
413
+ const persistedChatID = String(persistedSourceEnvelope.chat_id || "").trim();
414
+ const persistedMessageID = intFromRawAllowZero(persistedSourceEnvelope.message_id, 0);
415
+ if (
416
+ persistedOrigin === "local_telegram_inbound"
417
+ && archiveChatID
418
+ && archiveChatID === persistedChatID
419
+ && archiveMessageID > 0
420
+ && archiveMessageID === persistedMessageID
421
+ ) {
422
+ return persistedSourceEnvelope;
423
+ }
424
+ return archiveEnvelope;
425
+ }
426
+
427
+ function escapeRegExp(text) {
384
428
  return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
385
429
  }
386
430
 
@@ -4720,10 +4764,18 @@ export async function processRunnerSelectedRecord({
4720
4764
  safeObject(persistedHumanIntentRequest).reply_chain_context
4721
4765
  || safeObject(persistedHumanIntentRequest).replyChainContext,
4722
4766
  );
4723
- const sourceMessageEnvelope = buildTelegramMessageEnvelopeFromParsedArchive(selectedRecord?.parsedArchive);
4767
+ const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
4768
+ const sourceMessageEnvelope = resolveRunnerDeliverySourceMessageEnvelope({
4769
+ routeState,
4770
+ persistedRequest: persistedHumanIntentRequest,
4771
+ selectedRecord,
4772
+ routeKey,
4773
+ currentBotSelector,
4774
+ });
4724
4775
  const replyMessageThreadID = intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0);
4725
4776
  const replyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
4726
- const replyAnchorSource = replyToMessageID > 0 ? "source_message_envelope" : "";
4777
+ const replyAnchorSource = String(sourceMessageEnvelope.source_origin || "").trim()
4778
+ || (replyToMessageID > 0 ? "source_message_envelope" : "");
4727
4779
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4728
4780
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
4729
4781
  ? executionDeps.validateWorkspaceArtifacts
@@ -4734,7 +4786,6 @@ export async function processRunnerSelectedRecord({
4734
4786
  const resolveInformationalQueryReply = typeof executionDeps.resolveInformationalQueryReply === "function"
4735
4787
  ? executionDeps.resolveInformationalQueryReply
4736
4788
  : null;
4737
- const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
4738
4789
  const triggerDecision = safeObject(precomputedTriggerDecision);
4739
4790
  const effectiveTriggerDecision = typeof triggerDecision.shouldRespond === "boolean"
4740
4791
  ? triggerDecision
@@ -5592,6 +5643,7 @@ export async function processRunnerSelectedRecord({
5592
5643
  disableWebPagePreview: true,
5593
5644
  messageThreadID: replyMessageThreadID,
5594
5645
  replyToMessageID,
5646
+ sourceMessageEnvelope: sourceMessageEnvelope,
5595
5647
  archiveReplies: normalizedRoute.archivePolicy.mirrorReplies,
5596
5648
  archiveDedupeOutbound: normalizedRoute.archivePolicy.dedupeOutbound,
5597
5649
  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);
@@ -12829,6 +12829,163 @@ export async function runSelftestRunnerScenarios(push, deps) {
12829
12829
  push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
12830
12830
  }
12831
12831
 
12832
+ try {
12833
+ let capturedReplyToMessageID = 0;
12834
+ let capturedMessageThreadID = 0;
12835
+ let capturedSourceMessageEnvelope = {};
12836
+ const processed = await processRunnerSelectedRecord({
12837
+ routeKey: "delivery-prefers-route-local-inbound-envelope-key",
12838
+ normalizedRoute: normalizeRunnerRoute({
12839
+ name: "telegram-monitor-delivery-prefers-route-local-inbound-envelope",
12840
+ project_id: selftestProjectID,
12841
+ provider: "telegram",
12842
+ role: "monitor",
12843
+ role_profile: "monitor",
12844
+ destination_id: "dest-1",
12845
+ destination_label: "Main Room",
12846
+ server_bot_name: "RyoAI_bot",
12847
+ server_bot_id: "bot-1",
12848
+ trigger_policy: {
12849
+ mentions_only: true,
12850
+ direct_messages: true,
12851
+ reply_to_bot_messages: true,
12852
+ },
12853
+ archive_policy: {
12854
+ mirror_replies: true,
12855
+ dedupe_inbound: true,
12856
+ dedupe_outbound: true,
12857
+ skip_bot_messages: true,
12858
+ },
12859
+ dry_run_delivery: false,
12860
+ }),
12861
+ routeState: {
12862
+ recent_local_inbound_envelopes: {
12863
+ "-100123:128": {
12864
+ chat_id: "-100123",
12865
+ message_id: 128,
12866
+ message_thread_id: 912,
12867
+ reply_to_message_id: 127,
12868
+ kind: "telegram_message",
12869
+ sender: "human",
12870
+ sender_is_bot: false,
12871
+ body: "@RyoAI_bot hi",
12872
+ source_origin: "local_telegram_inbound",
12873
+ source_route_key: "delivery-prefers-route-local-inbound-envelope-key",
12874
+ source_bot_username: "ryoai_bot",
12875
+ },
12876
+ },
12877
+ },
12878
+ selectedRecord: {
12879
+ id: "comment-delivery-prefers-route-local-inbound-envelope",
12880
+ createdAt: "2026-03-27T00:00:00.000Z",
12881
+ parsedArchive: {
12882
+ kind: "telegram_message",
12883
+ chatID: "-100123",
12884
+ chatType: "supergroup",
12885
+ body: "@RyoAI_bot hi",
12886
+ messageID: 128,
12887
+ messageThreadID: 912,
12888
+ replyToMessageID: 127,
12889
+ sender: "human",
12890
+ senderIsBot: false,
12891
+ mentionUsernames: ["ryoai_bot"],
12892
+ },
12893
+ },
12894
+ pendingOrdered: [],
12895
+ bot: {
12896
+ id: "bot-1",
12897
+ name: "RyoAI_bot",
12898
+ username: "RyoAI_bot",
12899
+ role: "monitor",
12900
+ provider: "telegram",
12901
+ },
12902
+ destination: {
12903
+ id: "dest-1",
12904
+ label: "Main Room",
12905
+ provider: "telegram",
12906
+ chatID: "-100123",
12907
+ },
12908
+ archiveThread: {
12909
+ threadID: "thread-1",
12910
+ workItemID: "work-item-1",
12911
+ },
12912
+ executionPlan: {
12913
+ mode: "role_profile",
12914
+ roleProfileName: "monitor",
12915
+ roleProfile: {
12916
+ client: "sample",
12917
+ model: "",
12918
+ permissionMode: "read_only",
12919
+ reasoningEffort: "low",
12920
+ },
12921
+ workspaceDir: "",
12922
+ workspaceSource: "selftest",
12923
+ usedCommandFallback: false,
12924
+ },
12925
+ runtime: {
12926
+ baseURL: "https://example.test",
12927
+ token: "selftest-token",
12928
+ timeoutSeconds: 30,
12929
+ actor: { user_id: "user-1" },
12930
+ },
12931
+ deps: {
12932
+ saveRunnerRouteState: () => {},
12933
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
12934
+ runRunnerAIExecution: async () => ({
12935
+ skip: false,
12936
+ reply: "Hello from RyoAI_bot.",
12937
+ }),
12938
+ performLocalBotDelivery: async ({ replyToMessageID, messageThreadID, sourceMessageEnvelope }) => {
12939
+ capturedReplyToMessageID = Number(replyToMessageID || 0);
12940
+ capturedMessageThreadID = Number(messageThreadID || 0);
12941
+ capturedSourceMessageEnvelope = safeObject(sourceMessageEnvelope);
12942
+ return {
12943
+ delivery: {
12944
+ dryRun: false,
12945
+ body: {
12946
+ result: {
12947
+ message_id: 9001,
12948
+ message_thread_id: Number(messageThreadID || 0) || undefined,
12949
+ },
12950
+ },
12951
+ },
12952
+ archive: {},
12953
+ };
12954
+ },
12955
+ serializeRunnerTriggerPolicy: (value) => value,
12956
+ serializeRunnerArchivePolicy: (value) => value,
12957
+ buildRunnerExecutionDeps: () => ({
12958
+ validateWorkspaceArtifacts,
12959
+ analyzeHumanConversationIntentWithAI: async () => ({
12960
+ mode: "single_bot",
12961
+ lead_bot: "ryoai_bot",
12962
+ participants: ["ryoai_bot"],
12963
+ initial_responders: ["ryoai_bot"],
12964
+ allowed_responders: ["ryoai_bot"],
12965
+ summary_bot: "",
12966
+ allow_bot_to_bot: false,
12967
+ reply_expectation: "informational",
12968
+ intent_type: "small_talk",
12969
+ }),
12970
+ }),
12971
+ buildRunnerDeliveryDeps: () => ({}),
12972
+ buildRunnerRuntimeDeps: () => ({}),
12973
+ resolveConversationPeerBots: () => [],
12974
+ },
12975
+ });
12976
+ push(
12977
+ "runner_delivery_prefers_route_local_inbound_provenance_envelope",
12978
+ processed.kind === "replied"
12979
+ && capturedReplyToMessageID === 128
12980
+ && capturedMessageThreadID === 912
12981
+ && String(capturedSourceMessageEnvelope.source_origin || "") === "local_telegram_inbound"
12982
+ && Number(capturedSourceMessageEnvelope.message_id || 0) === 128,
12983
+ `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)")}`,
12984
+ );
12985
+ } catch (err) {
12986
+ push("runner_delivery_prefers_route_local_inbound_provenance_envelope", false, String(err?.message || err));
12987
+ }
12988
+
12832
12989
  try {
12833
12990
  const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-observed-artifacts-"));
12834
12991
  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.250",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [