metheus-governance-mcp-cli 0.2.274 → 0.2.275

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
@@ -8591,9 +8591,11 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8591
8591
  };
8592
8592
  }
8593
8593
 
8594
- function buildArchivedInboundMessageKey(chatID, messageID) {
8595
- return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8596
- }
8594
+ function buildArchivedInboundMessageKey(chatID, messageID, sourceBotUsername = "") {
8595
+ const baseKey = `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8596
+ const normalizedBotUsername = normalizeTelegramMentionUsername(sourceBotUsername);
8597
+ return normalizedBotUsername ? `${baseKey}::${normalizedBotUsername}` : baseKey;
8598
+ }
8597
8599
 
8598
8600
  function formatTelegramInboundArchiveComment(normalized) {
8599
8601
  const archiveSourceOrigin = String(normalized.archiveSourceOrigin || normalized.sourceOrigin || "").trim();
@@ -51,6 +51,56 @@ function intFromRawAllowZero(raw, fallback = 0) {
51
51
  return Number.isFinite(parsed) ? parsed : fallback;
52
52
  }
53
53
 
54
+ function escapeRegex(text) {
55
+ return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ }
57
+
58
+ function normalizeMentionSelector(value) {
59
+ return String(value || "").trim().replace(/^@+/, "").toLowerCase();
60
+ }
61
+
62
+ function buildRunnerInboundTargetSelectorSuffix(selector) {
63
+ const normalizedSelector = normalizeMentionSelector(selector);
64
+ return normalizedSelector ? `::${normalizedSelector}` : "";
65
+ }
66
+
67
+ function resolveRunnerArchiveSourceTargetSelector(recordRaw) {
68
+ const parsed = safeObject(safeObject(recordRaw).parsedArchive);
69
+ return normalizeMentionSelector(parsed.sourceBotUsername || parsed.source_bot_username);
70
+ }
71
+
72
+ function resolveRunnerLocalInboundReceiptTargetSelector(receiptRaw) {
73
+ const receipt = safeObject(receiptRaw);
74
+ const receiptBotSelector = normalizeMentionSelector(
75
+ receipt.receipt_bot_username
76
+ || receipt.receiptBotUsername
77
+ || receipt.source_bot_username
78
+ || receipt.sourceBotUsername,
79
+ );
80
+ if (!receiptBotSelector) {
81
+ return "";
82
+ }
83
+ const replyTargetSelector = normalizeMentionSelector(
84
+ receipt.reply_to_from_username
85
+ || receipt.replyToFromUsername
86
+ || receipt.reply_to_username
87
+ || receipt.replyToUsername,
88
+ );
89
+ const replyTargetIsBot = receipt.reply_to_from_is_bot === true
90
+ || receipt.replyToFromIsBot === true
91
+ || receipt.reply_to_sender_is_bot === true
92
+ || receipt.replyToSenderIsBot === true;
93
+ if (replyTargetIsBot && replyTargetSelector && replyTargetSelector === receiptBotSelector) {
94
+ return receiptBotSelector;
95
+ }
96
+ const body = String(receipt.body || receipt.text || "").trim();
97
+ if (!body) {
98
+ return "";
99
+ }
100
+ const mentionPattern = new RegExp(`(^|\\s)@${escapeRegex(receiptBotSelector)}(?=\\b|\\s|$)`, "i");
101
+ return mentionPattern.test(body) ? receiptBotSelector : "";
102
+ }
103
+
54
104
  function buildRunnerArchiveSourceMessageKey(recordRaw) {
55
105
  const parsed = safeObject(safeObject(recordRaw).parsedArchive);
56
106
  const chatID = String(parsed.chatID || parsed.chatId || "").trim();
@@ -58,7 +108,7 @@ function buildRunnerArchiveSourceMessageKey(recordRaw) {
58
108
  if (!chatID || !(messageID > 0)) {
59
109
  return "";
60
110
  }
61
- return `${chatID}:${messageID}`;
111
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerArchiveSourceTargetSelector(recordRaw))}`;
62
112
  }
63
113
 
64
114
  function buildRunnerLocalInboundReceiptKey(receiptRaw) {
@@ -68,7 +118,7 @@ function buildRunnerLocalInboundReceiptKey(receiptRaw) {
68
118
  if (!chatID || !(messageID > 0)) {
69
119
  return "";
70
120
  }
71
- return `${chatID}:${messageID}`;
121
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerLocalInboundReceiptTargetSelector(receiptRaw))}`;
72
122
  }
73
123
 
74
124
  function buildRunnerReceiptReplaySortTime(receiptRaw) {
@@ -157,10 +207,6 @@ function buildContextSpeakerType(parsedArchiveRaw) {
157
207
  return parsed.senderIsBot === true ? "bot" : "system";
158
208
  }
159
209
 
160
- function normalizeMentionSelector(value) {
161
- return String(value || "").trim().replace(/^@+/, "").toLowerCase();
162
- }
163
-
164
210
  function uniqueOrdered(values) {
165
211
  const seen = new Set();
166
212
  const output = [];
@@ -280,6 +280,34 @@ function currentRouteOwnsRunnerInboundUpdate({
280
280
  }).some((owner) => String(safeObject(owner).routeKey || "").trim() === normalizedRouteKey);
281
281
  }
282
282
 
283
+ function resolveRunnerInboundArchiveSourceBotUsername({
284
+ update,
285
+ route,
286
+ bot,
287
+ }) {
288
+ const normalizedUpdate = safeObject(update);
289
+ const currentBotSelectors = buildRouteBotUsernameCandidates(bot, route);
290
+ const currentBotSelector = ensureArray(currentBotSelectors)[0] || "";
291
+ if (!currentBotSelector) {
292
+ return "";
293
+ }
294
+ const explicitMentions = ensureArray(normalizedUpdate.mentionUsernames)
295
+ .map((value) => normalizeMentionSelector(value))
296
+ .filter(Boolean);
297
+ if (explicitMentions.includes(currentBotSelector)) {
298
+ return currentBotSelector;
299
+ }
300
+ const replyTargetSelector = normalizeMentionSelector(normalizedUpdate.replyToFromUsername);
301
+ if (
302
+ normalizedUpdate.replyToFromIsBot === true
303
+ && replyTargetSelector
304
+ && currentBotSelectors.includes(replyTargetSelector)
305
+ ) {
306
+ return replyTargetSelector;
307
+ }
308
+ return "";
309
+ }
310
+
283
311
  function managedConversationBotTargetsCurrentRoute({
284
312
  update,
285
313
  bot,
@@ -480,6 +508,18 @@ function normalizeRunnerRecentLocalInboundReceipt(rawReceipt, fallbackKey = "")
480
508
  if (replyToMessageID > 0) {
481
509
  normalized.reply_to_message_id = replyToMessageID;
482
510
  }
511
+ const replyToFromUsername = firstNonEmptyString([
512
+ receipt.reply_to_from_username,
513
+ receipt.replyToFromUsername,
514
+ receipt.reply_to_username,
515
+ receipt.replyToUsername,
516
+ ]);
517
+ if (replyToFromUsername) {
518
+ normalized.reply_to_from_username = replyToFromUsername;
519
+ }
520
+ if (receipt.reply_to_from_is_bot === true || receipt.replyToFromIsBot === true) {
521
+ normalized.reply_to_from_is_bot = true;
522
+ }
483
523
  const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
484
524
  if (chatType) {
485
525
  normalized.chat_type = chatType;
@@ -535,6 +575,8 @@ function buildRunnerLocalInboundArtifacts(updates, routeKey, route, bot, destina
535
575
  message_id: messageID,
536
576
  message_thread_id: intFromRawAllowZero(update.messageThreadID, 0),
537
577
  reply_to_message_id: intFromRawAllowZero(update.replyToMessageID, 0),
578
+ reply_to_from_username: update.replyToFromUsername,
579
+ reply_to_from_is_bot: update.replyToFromIsBot === true,
538
580
  kind: update.fromIsBot ? "bot_reply" : "telegram_message",
539
581
  sender_id: update.fromID,
540
582
  sender: update.fromName,
@@ -628,14 +670,16 @@ function buildRunnerRecentLocalInboundReceipts(routeStateRaw, localInboundArtifa
628
670
  const RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS = 10 * 60 * 1000;
629
671
  const runnerInboundArchiveReservations = new Map();
630
672
 
631
- function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID) {
673
+ function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername = "") {
632
674
  const normalizedThreadID = String(threadID || "").trim();
633
675
  const normalizedChatID = String(chatID || "").trim();
634
676
  const normalizedMessageID = intFromRawAllowZero(messageID, 0);
677
+ const normalizedSourceBotUsername = normalizeMentionSelector(sourceBotUsername);
635
678
  if (!normalizedThreadID || !normalizedChatID || !(normalizedMessageID > 0)) {
636
679
  return "";
637
680
  }
638
- return `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
681
+ const baseKey = `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
682
+ return normalizedSourceBotUsername ? `${baseKey}::${normalizedSourceBotUsername}` : baseKey;
639
683
  }
640
684
 
641
685
  function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
@@ -646,8 +690,8 @@ function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
646
690
  }
647
691
  }
648
692
 
649
- function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID) {
650
- const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID);
693
+ function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID, sourceBotUsername = "") {
694
+ const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername);
651
695
  if (!reservationKey) {
652
696
  return {
653
697
  ok: false,
@@ -777,7 +821,7 @@ async function loadRunnerExistingInboundArchiveKeys({
777
821
  .map((record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment))
778
822
  .map((record) => record.parsedArchive)
779
823
  .filter((parsed) => parsed && isInboundArchiveKind(parsed.kind) && parsed.chatID)
780
- .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID)),
824
+ .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID, parsed.sourceBotUsername)),
781
825
  );
782
826
  }
783
827
 
@@ -835,12 +879,22 @@ async function archiveRunnerTelegramInboundUpdates({
835
879
  ) {
836
880
  continue;
837
881
  }
838
- const dedupeKey = buildArchivedInboundMessageKey(update.chatID, update.messageID);
882
+ const archiveSourceBotUsername = resolveRunnerInboundArchiveSourceBotUsername({
883
+ update,
884
+ route,
885
+ bot,
886
+ });
887
+ const dedupeKey = buildArchivedInboundMessageKey(update.chatID, update.messageID, archiveSourceBotUsername);
839
888
  if (boolFromRaw(archivePolicy.dedupeInbound, true) && existingKeys.has(dedupeKey)) {
840
889
  continue;
841
890
  }
842
891
  const reservation = boolFromRaw(archivePolicy.dedupeInbound, true)
843
- ? reserveRunnerInboundArchiveMessage(archiveThread?.threadID, update.chatID, update.messageID)
892
+ ? reserveRunnerInboundArchiveMessage(
893
+ archiveThread?.threadID,
894
+ update.chatID,
895
+ update.messageID,
896
+ archiveSourceBotUsername,
897
+ )
844
898
  : { ok: true, reservationKey: "" };
845
899
  if (!reservation.ok) {
846
900
  continue;
@@ -851,6 +905,8 @@ async function archiveRunnerTelegramInboundUpdates({
851
905
  archiveBody = formatTelegramInboundArchiveComment({
852
906
  ...update,
853
907
  archiveSourceOrigin: "telegram_archive_context",
908
+ archiveSourceRouteKey: archiveSourceBotUsername ? String(routeKey || "").trim() : "",
909
+ archiveSourceBotUsername,
854
910
  });
855
911
  createdComment = await createThreadComment({
856
912
  siteBaseURL: runtime.baseURL,
@@ -19691,6 +19691,66 @@ export async function runSelftestRunnerScenarios(push, deps) {
19691
19691
  push("runner_entrypoint_pending_selection_stays_within_module_4_7_boundary", false, String(err?.message || err));
19692
19692
  }
19693
19693
 
19694
+ try {
19695
+ const foreignArchiveComment = {
19696
+ id: "comment-foreign-owner-1",
19697
+ createdAt: "2026-03-30T00:00:00.000Z",
19698
+ updatedAt: "2026-03-30T00:00:00.000Z",
19699
+ body: "@woobn_bot hi",
19700
+ parsedArchive: {
19701
+ kind: "telegram_message",
19702
+ messageID: 280,
19703
+ chatID: "-100999",
19704
+ chatType: "supergroup",
19705
+ sender: "tester",
19706
+ senderIsBot: false,
19707
+ body: "@woobn_bot hi",
19708
+ sourceBotUsername: "woobn_bot",
19709
+ },
19710
+ };
19711
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
19712
+ comments: [foreignArchiveComment],
19713
+ importOutcome: {
19714
+ importedCommentIDs: [],
19715
+ importedComments: [],
19716
+ currentPollLocalInboundReceipts: [
19717
+ {
19718
+ chat_id: "-100999",
19719
+ message_id: 280,
19720
+ body: "@RyoAI3_bot 하이",
19721
+ receipt_bot_username: "ryoai3_bot",
19722
+ occurred_at: "2026-03-31T00:10:00.000Z",
19723
+ },
19724
+ ],
19725
+ },
19726
+ refreshedState: {
19727
+ last_processed_comment_id: "comment-foreign-owner-1",
19728
+ last_processed_created_at: "2026-03-30T00:00:00.000Z",
19729
+ },
19730
+ mode: "continue",
19731
+ parseArchivedChatComment,
19732
+ deps: {
19733
+ normalizeArchiveCommentRecord: (record) => ({
19734
+ id: String(record?.id || "").trim(),
19735
+ body: String(record?.body || "").trim(),
19736
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
19737
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
19738
+ parsedArchive: safeObject(record?.parsedArchive),
19739
+ }),
19740
+ applyPendingAgeSelection: (selection) => selection,
19741
+ },
19742
+ pendingSelectionOptions: {},
19743
+ });
19744
+ push(
19745
+ "runner_entrypoint_receipt_replay_skips_foreign_bot_archive_collision",
19746
+ ensureArray(pendingWork.receiptBackedPending).length === 0
19747
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 0,
19748
+ `replay=${String(ensureArray(pendingWork.receiptBackedPending).length)} pending=${String(ensureArray(safeObject(pendingWork.pending).pending).length)}`,
19749
+ );
19750
+ } catch (err) {
19751
+ push("runner_entrypoint_receipt_replay_skips_foreign_bot_archive_collision", false, String(err?.message || err));
19752
+ }
19753
+
19694
19754
  try {
19695
19755
  const deliveryContext = await prepareLocalBotDeliveryContext({
19696
19756
  siteBaseURL: "https://example.test",
@@ -923,10 +923,10 @@ export async function runSelftestTelegramE2E(push, deps) {
923
923
  `envelope=${JSON.stringify(progressEnvelope)}`,
924
924
  );
925
925
  push(
926
- "telegram_archive_comment_is_context_only_without_route_ownership_fields",
926
+ "telegram_archive_comment_preserves_route_ownership_fields_for_explicit_targeting",
927
927
  progressCommentBody.includes("archive_source_origin: telegram_archive_context")
928
- && !progressCommentBody.includes("archive_source_route_key:")
929
- && !progressCommentBody.includes("archive_source_bot_username:"),
928
+ && progressCommentBody.includes("archive_source_route_key:")
929
+ && progressCommentBody.includes("archive_source_bot_username:"),
930
930
  `body=${progressCommentBody}`,
931
931
  );
932
932
  const progressParsedArchive = parseArchivedChatComment(progressCommentBody);
@@ -1657,6 +1657,93 @@ export async function runSelftestTelegramE2E(push, deps) {
1657
1657
  && ownershipBodies.length === 0,
1658
1658
  `owner=${JSON.stringify(ryoai2Receipt)} foreign=${JSON.stringify(ryoai3Receipt)} comments=${ownershipBodies.join(" || ")}`,
1659
1659
  );
1660
+
1661
+ telegramE2EServer.state.comments = [
1662
+ {
1663
+ id: "foreign-owner-archive-280",
1664
+ body: buildRunnerRuntimeDeps().formatTelegramInboundArchiveComment({
1665
+ eventName: "telegram.message.created",
1666
+ chatID: e2eDestination.chat_id,
1667
+ chatType: "supergroup",
1668
+ messageID: 280,
1669
+ fromID: "7001",
1670
+ fromName: "Operator",
1671
+ fromUsername: "operator_user",
1672
+ fromIsBot: false,
1673
+ mentionUsernames: ["woobn_bot"],
1674
+ text: "@WooBN_bot 하이",
1675
+ occurredAt: "2026-03-31T00:00:00.000Z",
1676
+ archiveSourceOrigin: "telegram_archive_context",
1677
+ archiveSourceRouteKey: "telegram-monitor-woobn-bot::project::telegram::monitor::dest::actor",
1678
+ archiveSourceBotUsername: "woobn_bot",
1679
+ }),
1680
+ created_at: "2026-03-31T00:00:01.000Z",
1681
+ updated_at: "2026-03-31T00:00:01.000Z",
1682
+ author_user_id: e2eActorUserID,
1683
+ },
1684
+ ];
1685
+ telegramE2EServer.state.updates = [
1686
+ {
1687
+ update_id: 404,
1688
+ message: {
1689
+ message_id: 280,
1690
+ date: Math.floor(Date.now() / 1000),
1691
+ chat: {
1692
+ id: Number(e2eDestination.chat_id),
1693
+ type: "supergroup",
1694
+ title: e2eDestination.label,
1695
+ },
1696
+ from: {
1697
+ id: 7002,
1698
+ is_bot: false,
1699
+ first_name: "Operator",
1700
+ username: "operator_user",
1701
+ },
1702
+ text: "@RyoAI3_bot 하이",
1703
+ entities: buildTelegramMentionEntities("@RyoAI3_bot 하이"),
1704
+ },
1705
+ },
1706
+ ];
1707
+ await archiveLocalTelegramMessagesForRoute({
1708
+ routeKey: routeRyoai3Key,
1709
+ route: routeRyoai3,
1710
+ routeState: {},
1711
+ runtime: {
1712
+ baseURL: telegramE2EServer.baseURL,
1713
+ timeoutSeconds: 10,
1714
+ token: e2eToken,
1715
+ actor: {
1716
+ user_id: e2eActorUserID,
1717
+ },
1718
+ },
1719
+ bot: {
1720
+ id: "88888888-8888-4888-8888-888888888883",
1721
+ name: "RyoAI3_bot",
1722
+ username: "RyoAI3_bot",
1723
+ role: "monitor",
1724
+ },
1725
+ destination: {
1726
+ chatID: e2eDestination.chat_id,
1727
+ },
1728
+ archiveThread: {
1729
+ threadID: e2eThreadID,
1730
+ },
1731
+ managedConversationBots: [
1732
+ {
1733
+ username: "RyoAI3_bot",
1734
+ route: routeRyoai3,
1735
+ bot: { username: "RyoAI3_bot", name: "RyoAI3_bot" },
1736
+ },
1737
+ ],
1738
+ deps: buildRunnerRuntimeDeps(),
1739
+ });
1740
+ const foreignCollisionBodies = telegramE2EServer.state.comments.map((item) => String(item.body || ""));
1741
+ push(
1742
+ "telegram_explicit_self_mention_reimports_when_same_message_id_exists_for_foreign_bot_archive",
1743
+ foreignCollisionBodies.filter((item) => item.includes("message_id: 280")).length === 2
1744
+ && foreignCollisionBodies.some((item) => item.includes("archive_source_bot_username: @ryoai3_bot")),
1745
+ `count=${foreignCollisionBodies.filter((item) => item.includes("message_id: 280")).length} bodies=${foreignCollisionBodies.join(" || ")}`,
1746
+ );
1660
1747
  } catch (err) {
1661
1748
  push("telegram_runner_e2e_local_mock", false, String(err?.message || err));
1662
1749
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.274",
3
+ "version": "0.2.275",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [