metheus-governance-mcp-cli 0.2.273 → 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
@@ -3333,37 +3333,46 @@ function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
3333
3333
  return normalized;
3334
3334
  }
3335
3335
 
3336
- function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
3337
- const commentID = String(commentIDRaw || "").trim();
3338
- if (!commentID) return "";
3339
- const routeKey = String(routeKeyRaw || "").trim();
3340
- const commentKind = String(commentKindRaw || "").trim().toLowerCase();
3341
- if (commentKind === "bot_reply" && routeKey) {
3342
- return `${commentID}::${routeKey}`;
3343
- }
3344
- return commentID;
3345
- }
3346
-
3347
- function findRunnerConsumedCommentEntry(rawConsumed, commentIDRaw, { routeKey = "", commentKind = "" } = {}) {
3348
- const commentID = String(commentIDRaw || "").trim();
3349
- if (!commentID) return {};
3350
- const consumedComments = normalizeBotRunnerConsumedComments(rawConsumed);
3351
- const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, routeKey, commentKind);
3352
- const directEntry = safeObject(consumedComments[ledgerKey]);
3353
- if (Object.keys(directEntry).length) {
3354
- return directEntry;
3355
- }
3356
- const normalizedKind = String(commentKind || "").trim().toLowerCase();
3357
- if (normalizedKind === "bot_reply" && String(routeKey || "").trim()) {
3358
- const routeEntry = Object.values(consumedComments).find((entryRaw) => {
3359
- const entry = safeObject(entryRaw);
3360
- return String(entry.comment_id || "").trim() === commentID
3361
- && String(entry.route_key || "").trim() === String(routeKey || "").trim();
3362
- });
3363
- return safeObject(routeEntry);
3364
- }
3365
- return safeObject(consumedComments[commentID]);
3366
- }
3336
+ function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
3337
+ const commentID = String(commentIDRaw || "").trim();
3338
+ if (!commentID) return "";
3339
+ const routeKey = String(routeKeyRaw || "").trim();
3340
+ if (routeKey) {
3341
+ return `${commentID}::${routeKey}`;
3342
+ }
3343
+ return commentID;
3344
+ }
3345
+
3346
+ function findRunnerConsumedCommentEntry(rawConsumed, commentIDRaw, { routeKey = "", commentKind = "" } = {}) {
3347
+ const commentID = String(commentIDRaw || "").trim();
3348
+ if (!commentID) return {};
3349
+ const consumedComments = normalizeBotRunnerConsumedComments(rawConsumed);
3350
+ const normalizedRouteKey = String(routeKey || "").trim();
3351
+ const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, normalizedRouteKey, commentKind);
3352
+ const directEntry = safeObject(consumedComments[ledgerKey]);
3353
+ if (Object.keys(directEntry).length) {
3354
+ return directEntry;
3355
+ }
3356
+ if (normalizedRouteKey) {
3357
+ const routeEntry = Object.values(consumedComments).find((entryRaw) => {
3358
+ const entry = safeObject(entryRaw);
3359
+ return String(entry.comment_id || "").trim() === commentID
3360
+ && String(entry.route_key || "").trim() === normalizedRouteKey;
3361
+ });
3362
+ if (Object.keys(safeObject(routeEntry)).length > 0) {
3363
+ return safeObject(routeEntry);
3364
+ }
3365
+ }
3366
+ const legacyGlobalEntry = safeObject(consumedComments[commentID]);
3367
+ if (!Object.keys(legacyGlobalEntry).length) {
3368
+ return {};
3369
+ }
3370
+ const legacyRouteKey = String(legacyGlobalEntry.route_key || "").trim();
3371
+ if (normalizedRouteKey && legacyRouteKey && legacyRouteKey !== normalizedRouteKey) {
3372
+ return {};
3373
+ }
3374
+ return legacyGlobalEntry;
3375
+ }
3367
3376
 
3368
3377
  function runnerRouteMatchesProjectConversationScope(candidateRouteRaw, normalizedRouteRaw) {
3369
3378
  const candidateRoute = normalizeRunnerRoute(candidateRouteRaw);
@@ -8582,9 +8591,11 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8582
8591
  };
8583
8592
  }
8584
8593
 
8585
- function buildArchivedInboundMessageKey(chatID, messageID) {
8586
- return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8587
- }
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
+ }
8588
8599
 
8589
8600
  function formatTelegramInboundArchiveComment(normalized) {
8590
8601
  const archiveSourceOrigin = String(normalized.archiveSourceOrigin || normalized.sourceOrigin || "").trim();
@@ -51,6 +51,150 @@ 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
+
104
+ function buildRunnerArchiveSourceMessageKey(recordRaw) {
105
+ const parsed = safeObject(safeObject(recordRaw).parsedArchive);
106
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
107
+ const messageID = intFromRawAllowZero(parsed.messageID || parsed.messageId, 0);
108
+ if (!chatID || !(messageID > 0)) {
109
+ return "";
110
+ }
111
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerArchiveSourceTargetSelector(recordRaw))}`;
112
+ }
113
+
114
+ function buildRunnerLocalInboundReceiptKey(receiptRaw) {
115
+ const receipt = safeObject(receiptRaw);
116
+ const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
117
+ const messageID = intFromRawAllowZero(receipt.message_id || receipt.messageID, 0);
118
+ if (!chatID || !(messageID > 0)) {
119
+ return "";
120
+ }
121
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(resolveRunnerLocalInboundReceiptTargetSelector(receiptRaw))}`;
122
+ }
123
+
124
+ function buildRunnerReceiptReplaySortTime(receiptRaw) {
125
+ const receipt = safeObject(receiptRaw);
126
+ const receiptTime = Date.parse(firstNonEmptyString([
127
+ receipt.occurred_at,
128
+ receipt.occurredAt,
129
+ receipt.received_at,
130
+ receipt.receivedAt,
131
+ ]));
132
+ return Number.isFinite(receiptTime) ? receiptTime : 0;
133
+ }
134
+
135
+ function buildRunnerReceiptBackedReplayRecord(recordRaw, receiptRaw, pendingSelectionOptions = {}) {
136
+ const record = safeObject(recordRaw);
137
+ const receipt = safeObject(receiptRaw);
138
+ const replayOccurredAt = firstNonEmptyString([
139
+ receipt.occurred_at,
140
+ receipt.occurredAt,
141
+ receipt.received_at,
142
+ receipt.receivedAt,
143
+ record.sourceOccurredAt,
144
+ safeObject(record.parsedArchive).occurredAt,
145
+ safeObject(record.parsedArchive).occurred_at,
146
+ record.createdAt,
147
+ record.updatedAt,
148
+ ]);
149
+ let staleAfterAt = String(record.staleAfterAt || "").trim();
150
+ const maxPendingAgeMs = intFromRawAllowZero(safeObject(pendingSelectionOptions).maxPendingAgeMs, 0);
151
+ const replayOccurredAtMs = Date.parse(replayOccurredAt);
152
+ if (!staleAfterAt && maxPendingAgeMs > 0 && Number.isFinite(replayOccurredAtMs)) {
153
+ staleAfterAt = new Date(replayOccurredAtMs + maxPendingAgeMs).toISOString();
154
+ }
155
+ return {
156
+ ...record,
157
+ sourceOccurredAt: replayOccurredAt,
158
+ staleAfterAt,
159
+ replayTriggeredByLocalReceipt: true,
160
+ };
161
+ }
162
+
163
+ function buildRunnerReceiptBackedPendingArchiveComments({
164
+ orderedComments,
165
+ currentPollLocalInboundReceipts,
166
+ existingPendingIDs,
167
+ pendingSelectionOptions,
168
+ }) {
169
+ const pendingIDs = new Set(ensureArray(existingPendingIDs).map((value) => String(value || "").trim()).filter(Boolean));
170
+ const latestReceiptsByKey = new Map();
171
+ for (const receiptRaw of ensureArray(currentPollLocalInboundReceipts)) {
172
+ const receipt = safeObject(receiptRaw);
173
+ const receiptKey = buildRunnerLocalInboundReceiptKey(receipt);
174
+ if (!receiptKey) {
175
+ continue;
176
+ }
177
+ const previous = safeObject(latestReceiptsByKey.get(receiptKey));
178
+ if (buildRunnerReceiptReplaySortTime(receipt) >= buildRunnerReceiptReplaySortTime(previous)) {
179
+ latestReceiptsByKey.set(receiptKey, receipt);
180
+ }
181
+ }
182
+ const replayCandidates = [];
183
+ for (const [receiptKey, receipt] of latestReceiptsByKey.entries()) {
184
+ const matchedRecord = orderedComments.find((record) => buildRunnerArchiveSourceMessageKey(record) === receiptKey);
185
+ const matchedID = String(safeObject(matchedRecord).id || "").trim();
186
+ if (!matchedID || pendingIDs.has(matchedID)) {
187
+ continue;
188
+ }
189
+ replayCandidates.push(buildRunnerReceiptBackedReplayRecord(
190
+ matchedRecord,
191
+ receipt,
192
+ pendingSelectionOptions,
193
+ ));
194
+ }
195
+ return replayCandidates.sort(compareArchiveCommentRecords);
196
+ }
197
+
54
198
  function buildContextSpeakerType(parsedArchiveRaw) {
55
199
  const parsed = safeObject(parsedArchiveRaw);
56
200
  const kind = String(parsed.kind || "").trim();
@@ -63,10 +207,6 @@ function buildContextSpeakerType(parsedArchiveRaw) {
63
207
  return parsed.senderIsBot === true ? "bot" : "system";
64
208
  }
65
209
 
66
- function normalizeMentionSelector(value) {
67
- return String(value || "").trim().replace(/^@+/, "").toLowerCase();
68
- }
69
-
70
210
  function uniqueOrdered(values) {
71
211
  const seen = new Set();
72
212
  const output = [];
@@ -862,10 +1002,24 @@ export function selectRunnerPendingWork({
862
1002
  pending: importedPendingAfterCursor,
863
1003
  }, pendingSelectionOptions)
864
1004
  : fullPending;
1005
+ const receiptBackedPending = buildRunnerReceiptBackedPendingArchiveComments({
1006
+ orderedComments,
1007
+ currentPollLocalInboundReceipts: ensureArray(importOutcome?.currentPollLocalInboundReceipts),
1008
+ existingPendingIDs: ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
1009
+ pendingSelectionOptions,
1010
+ });
1011
+ const finalPending = ensureArray(safeObject(pending).pending).length === 0 && receiptBackedPending.length > 0
1012
+ ? applyPendingAgeSelection({
1013
+ ...safeObject(pending),
1014
+ shouldPrime: false,
1015
+ pending: receiptBackedPending,
1016
+ }, pendingSelectionOptions)
1017
+ : pending;
865
1018
  return {
866
1019
  orderedComments,
867
1020
  inboundComments,
868
1021
  importedRecords,
869
- pending,
1022
+ receiptBackedPending,
1023
+ pending: finalPending,
870
1024
  };
871
1025
  }
@@ -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,
@@ -575,6 +617,14 @@ function groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts) {
575
617
  return Object.fromEntries(grouped.entries());
576
618
  }
577
619
 
620
+ function buildRunnerCurrentPollLocalInboundReceipts(localInboundArtifactsByRoute, routeKey) {
621
+ return ensureArray(safeObject(localInboundArtifactsByRoute)[String(routeKey || "").trim()])
622
+ .map((artifactRaw) => ensureArray(safeObject(artifactRaw).receiptEntry))
623
+ .filter((entry) => entry.length === 2)
624
+ .map((entry) => safeObject(entry[1]))
625
+ .filter((receipt) => Object.keys(receipt).length > 0);
626
+ }
627
+
578
628
  function buildRunnerRecentLocalInboundEnvelopes(routeStateRaw, recentLocalInboundReceipts) {
579
629
  const routeState = safeObject(routeStateRaw);
580
630
  const relevantEnvelopes = Object.values(safeObject(recentLocalInboundReceipts))
@@ -620,14 +670,16 @@ function buildRunnerRecentLocalInboundReceipts(routeStateRaw, localInboundArtifa
620
670
  const RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS = 10 * 60 * 1000;
621
671
  const runnerInboundArchiveReservations = new Map();
622
672
 
623
- function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID) {
673
+ function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername = "") {
624
674
  const normalizedThreadID = String(threadID || "").trim();
625
675
  const normalizedChatID = String(chatID || "").trim();
626
676
  const normalizedMessageID = intFromRawAllowZero(messageID, 0);
677
+ const normalizedSourceBotUsername = normalizeMentionSelector(sourceBotUsername);
627
678
  if (!normalizedThreadID || !normalizedChatID || !(normalizedMessageID > 0)) {
628
679
  return "";
629
680
  }
630
- return `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
681
+ const baseKey = `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
682
+ return normalizedSourceBotUsername ? `${baseKey}::${normalizedSourceBotUsername}` : baseKey;
631
683
  }
632
684
 
633
685
  function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
@@ -638,8 +690,8 @@ function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
638
690
  }
639
691
  }
640
692
 
641
- function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID) {
642
- const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID);
693
+ function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID, sourceBotUsername = "") {
694
+ const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername);
643
695
  if (!reservationKey) {
644
696
  return {
645
697
  ok: false,
@@ -769,7 +821,7 @@ async function loadRunnerExistingInboundArchiveKeys({
769
821
  .map((record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment))
770
822
  .map((record) => record.parsedArchive)
771
823
  .filter((parsed) => parsed && isInboundArchiveKind(parsed.kind) && parsed.chatID)
772
- .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID)),
824
+ .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID, parsed.sourceBotUsername)),
773
825
  );
774
826
  }
775
827
 
@@ -827,12 +879,22 @@ async function archiveRunnerTelegramInboundUpdates({
827
879
  ) {
828
880
  continue;
829
881
  }
830
- 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);
831
888
  if (boolFromRaw(archivePolicy.dedupeInbound, true) && existingKeys.has(dedupeKey)) {
832
889
  continue;
833
890
  }
834
891
  const reservation = boolFromRaw(archivePolicy.dedupeInbound, true)
835
- ? reserveRunnerInboundArchiveMessage(archiveThread?.threadID, update.chatID, update.messageID)
892
+ ? reserveRunnerInboundArchiveMessage(
893
+ archiveThread?.threadID,
894
+ update.chatID,
895
+ update.messageID,
896
+ archiveSourceBotUsername,
897
+ )
836
898
  : { ok: true, reservationKey: "" };
837
899
  if (!reservation.ok) {
838
900
  continue;
@@ -843,6 +905,8 @@ async function archiveRunnerTelegramInboundUpdates({
843
905
  archiveBody = formatTelegramInboundArchiveComment({
844
906
  ...update,
845
907
  archiveSourceOrigin: "telegram_archive_context",
908
+ archiveSourceRouteKey: archiveSourceBotUsername ? String(routeKey || "").trim() : "",
909
+ archiveSourceBotUsername,
846
910
  });
847
911
  createdComment = await createThreadComment({
848
912
  siteBaseURL: runtime.baseURL,
@@ -1144,6 +1208,10 @@ export async function archiveLocalTelegramMessagesForRoute({
1144
1208
  managedConversationBots,
1145
1209
  );
1146
1210
  const localInboundArtifactsByRoute = groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts);
1211
+ const currentPollLocalInboundReceipts = buildRunnerCurrentPollLocalInboundReceipts(
1212
+ localInboundArtifactsByRoute,
1213
+ routeKey,
1214
+ );
1147
1215
  const recentLocalInboundReceipts = buildRunnerRecentLocalInboundReceipts(
1148
1216
  routeState,
1149
1217
  ensureArray(localInboundArtifactsByRoute[routeKey]),
@@ -1205,7 +1273,9 @@ export async function archiveLocalTelegramMessagesForRoute({
1205
1273
  persistPollingProgress([], lastUpdateID);
1206
1274
  return {
1207
1275
  importedCommentIDs: [],
1276
+ importedComments: [],
1208
1277
  importedCount: 0,
1278
+ currentPollLocalInboundReceipts,
1209
1279
  lastUpdateID,
1210
1280
  };
1211
1281
  }
@@ -1249,6 +1319,7 @@ export async function archiveLocalTelegramMessagesForRoute({
1249
1319
  importedCommentIDs,
1250
1320
  importedComments,
1251
1321
  importedCount: importedCommentIDs.length,
1322
+ currentPollLocalInboundReceipts,
1252
1323
  lastUpdateID: Math.max(lastUpdateID, handledUpdateID),
1253
1324
  };
1254
1325
  }
@@ -1007,6 +1007,63 @@ export async function runSelftestRunnerScenarios(push, deps) {
1007
1007
  );
1008
1008
  }
1009
1009
 
1010
+ try {
1011
+ const pendingWork = selectRunnerPendingWork({
1012
+ comments: [
1013
+ {
1014
+ id: "archived-current-message",
1015
+ createdAt: "2026-03-18T00:00:01.000Z",
1016
+ updatedAt: "2026-03-18T00:00:01.000Z",
1017
+ parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 280, body: "@RyoAI3_bot 하이" },
1018
+ },
1019
+ {
1020
+ id: "cursor-comment",
1021
+ createdAt: "2026-03-18T00:04:00.000Z",
1022
+ updatedAt: "2026-03-18T00:04:00.000Z",
1023
+ parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 281, body: "@RyoAI_bot already processed" },
1024
+ },
1025
+ ],
1026
+ importOutcome: {
1027
+ importedCommentIDs: [],
1028
+ currentPollLocalInboundReceipts: [
1029
+ {
1030
+ chat_id: "-1001",
1031
+ message_id: 280,
1032
+ occurred_at: "2026-03-18T00:05:00.000Z",
1033
+ received_at: "2026-03-18T00:05:00.000Z",
1034
+ update_id: 2001,
1035
+ },
1036
+ ],
1037
+ },
1038
+ refreshedState: {
1039
+ last_processed_comment_id: "cursor-comment",
1040
+ last_processed_created_at: "2026-03-18T00:04:00.000Z",
1041
+ },
1042
+ mode: "start",
1043
+ parseArchivedChatComment: () => null,
1044
+ deps: {
1045
+ normalizeArchiveCommentRecord: (record) => record,
1046
+ applyPendingAgeSelection: (selection) => selection,
1047
+ },
1048
+ pendingSelectionOptions: {
1049
+ maxPendingAgeMs: 15 * 60 * 1000,
1050
+ },
1051
+ });
1052
+ push(
1053
+ "runner_pending_selection_replays_current_poll_local_receipt_against_existing_archive_comment",
1054
+ pendingWork.pending.pending.length === 1
1055
+ && String(pendingWork.pending.pending[0]?.id || "").trim() === "archived-current-message"
1056
+ && pendingWork.pending.pending[0]?.replayTriggeredByLocalReceipt === true,
1057
+ `pending=${pendingWork.pending.pending.map((item) => `${item.id}:${String(item.replayTriggeredByLocalReceipt === true)}`).join(",") || "(none)"}`,
1058
+ );
1059
+ } catch (err) {
1060
+ push(
1061
+ "runner_pending_selection_replays_current_poll_local_receipt_against_existing_archive_comment",
1062
+ false,
1063
+ String(err?.message || err),
1064
+ );
1065
+ }
1066
+
1010
1067
  try {
1011
1068
  const selected = selectProjectChatDestination(
1012
1069
  [
@@ -2042,10 +2099,16 @@ export async function runSelftestRunnerScenarios(push, deps) {
2042
2099
  selectedRecord: humanRecord,
2043
2100
  selectedBotUsernames: ["ryoai_bot"],
2044
2101
  normalizedIntent: "coordination_request",
2045
- });
2046
- const claimedState = loadBotRunnerState();
2047
- const claimedRequest = safeObject(safeObject(claimedState.requests)[claimed.requestKey]);
2048
- const claimedConsumed = safeObject(safeObject(claimedState.consumedComments)[humanRecord.id]);
2102
+ });
2103
+ const claimedState = loadBotRunnerState();
2104
+ const claimedRequest = safeObject(safeObject(claimedState.requests)[claimed.requestKey]);
2105
+ const claimedConsumed = safeObject(
2106
+ Object.values(safeObject(claimedState.consumedComments)).find((entryRaw) => {
2107
+ const entry = safeObject(entryRaw);
2108
+ return String(entry.comment_id || "").trim() === humanRecord.id
2109
+ && String(entry.route_key || "").trim() === requestRouteKey;
2110
+ }),
2111
+ );
2049
2112
  push(
2050
2113
  "runner_request_claim_persists_request_and_consumed_comment",
2051
2114
  claimed.ok === true
@@ -19628,6 +19691,66 @@ export async function runSelftestRunnerScenarios(push, deps) {
19628
19691
  push("runner_entrypoint_pending_selection_stays_within_module_4_7_boundary", false, String(err?.message || err));
19629
19692
  }
19630
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
+
19631
19754
  try {
19632
19755
  const deliveryContext = await prepareLocalBotDeliveryContext({
19633
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.273",
3
+ "version": "0.2.275",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [