metheus-governance-mcp-cli 0.2.277 → 0.2.278

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
@@ -135,6 +135,7 @@ import {
135
135
  } from "./lib/client-registration.mjs";
136
136
  import {
137
137
  applyPendingAgeSelection,
138
+ buildCanonicalHumanInboundKey as buildRunnerCanonicalHumanInboundKey,
138
139
  buildTelegramBotReplyEnvelope,
139
140
  buildTelegramMessageEnvelopeFromParsedArchive as buildRunnerTelegramMessageEnvelopeFromParsedArchive,
140
141
  buildRunnerRouteDuplicateStateFromComment,
@@ -3522,30 +3523,35 @@ function uniqueOrderedStrings(values, normalizer = (value) => String(value || ""
3522
3523
  return output;
3523
3524
  }
3524
3525
 
3525
- function buildRunnerRequestKey({
3526
- normalizedRoute,
3527
- selectedRecord,
3528
- selectedBotUsernames = [],
3529
- normalizedIntent = "",
3530
- conversationID = "",
3531
- }) {
3532
- const parsed = safeObject(selectedRecord?.parsedArchive);
3533
- const chatID = String(parsed.chatID || parsed.chatId || "").trim() || "-";
3534
- const messageID = intFromRawAllowZero(parsed.messageID, 0);
3535
- const kind = String(parsed.kind || "").trim().toLowerCase() || "-";
3536
- const responders = uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername).join(",") || "-";
3537
- const intent = String(normalizedIntent || "").trim().toLowerCase() || "-";
3538
- const resolvedConversationID = String(conversationID || parsed.conversationID || "").trim() || "-";
3539
- return [
3540
- String(normalizedRoute?.projectID || "").trim() || "-",
3541
- String(normalizedRoute?.provider || "").trim() || "-",
3542
- chatID,
3543
- String(messageID || "-"),
3544
- kind,
3545
- responders,
3546
- intent,
3547
- resolvedConversationID,
3548
- ].join("::");
3526
+ function buildRunnerRequestKey({
3527
+ normalizedRoute,
3528
+ selectedRecord,
3529
+ selectedBotUsernames = [],
3530
+ normalizedIntent = "",
3531
+ conversationID = "",
3532
+ }) {
3533
+ const parsed = safeObject(selectedRecord?.parsedArchive);
3534
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim() || "-";
3535
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
3536
+ const kind = String(parsed.kind || "").trim().toLowerCase() || "-";
3537
+ const canonicalHumanKey = buildRunnerCanonicalHumanInboundKey(parsed);
3538
+ const responders = canonicalHumanKey
3539
+ ? "-"
3540
+ : uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername).join(",") || "-";
3541
+ const intent = canonicalHumanKey
3542
+ ? "-"
3543
+ : String(normalizedIntent || "").trim().toLowerCase() || "-";
3544
+ const resolvedConversationID = String(conversationID || parsed.conversationID || "").trim() || "-";
3545
+ return [
3546
+ String(normalizedRoute?.projectID || "").trim() || "-",
3547
+ String(normalizedRoute?.provider || "").trim() || "-",
3548
+ chatID,
3549
+ canonicalHumanKey ? `human:${canonicalHumanKey}` : String(messageID || "-"),
3550
+ kind,
3551
+ responders,
3552
+ intent,
3553
+ resolvedConversationID,
3554
+ ].join("::");
3549
3555
  }
3550
3556
 
3551
3557
  function resolveRunnerRequestClaimIntent({
@@ -3614,15 +3620,19 @@ function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorM
3614
3620
  return `reply_chain:${provider}:${normalizedChatID}:${normalizedAnchorMessageID}`;
3615
3621
  }
3616
3622
 
3617
- function buildSyntheticHumanOpeningConversationID(normalizedRoute, chatID, messageID) {
3618
- const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
3619
- const normalizedChatID = String(chatID || "").trim() || "-";
3620
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
3621
- if (normalizedMessageID <= 0) {
3622
- return "";
3623
- }
3624
- return `human_opening:${provider}:${normalizedChatID}:${normalizedMessageID}`;
3625
- }
3623
+ function buildSyntheticHumanOpeningConversationID(normalizedRoute, chatID, messageID, canonicalHumanMessageKey = "") {
3624
+ const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
3625
+ const canonicalKey = String(canonicalHumanMessageKey || "").trim();
3626
+ if (canonicalKey) {
3627
+ return `human_opening:${provider}:${canonicalKey}`;
3628
+ }
3629
+ const normalizedChatID = String(chatID || "").trim() || "-";
3630
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
3631
+ if (normalizedMessageID <= 0) {
3632
+ return "";
3633
+ }
3634
+ return `human_opening:${provider}:${normalizedChatID}:${normalizedMessageID}`;
3635
+ }
3626
3636
 
3627
3637
  function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
3628
3638
  const requests = normalizeBotRunnerRequests(state?.requests);
@@ -4910,24 +4920,26 @@ function resolveRunnerHumanCommentAuthorityContext({
4910
4920
  const referenced = safeObject(referencedRequest);
4911
4921
  const sharedSource = safeObject(sharedConversationSource);
4912
4922
  const sharedIntent = safeObject(normalizedSharedHumanIntent);
4913
- const authoritySource = Object.keys(referenced).length
4914
- ? referenced
4915
- : Object.keys(sharedSource).length
4916
- ? sharedSource
4917
- : {};
4918
- const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4919
- const resolvedConversationID = firstNonEmptyString([
4920
- parsed.conversationID,
4921
- replyChain.conversationID,
4922
- authoritySource.conversation_id,
4923
- Object.keys(sharedIntent).length > 0
4924
- ? buildSyntheticHumanOpeningConversationID(
4925
- normalizedRoute,
4926
- String(parsed.chatID || parsed.chatId || "").trim(),
4927
- currentMessageID,
4928
- )
4929
- : "",
4930
- ]);
4923
+ const authoritySource = Object.keys(referenced).length
4924
+ ? referenced
4925
+ : Object.keys(sharedSource).length
4926
+ ? sharedSource
4927
+ : {};
4928
+ const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
4929
+ const canonicalHumanMessageKey = buildRunnerCanonicalHumanInboundKey(parsed);
4930
+ const resolvedConversationID = firstNonEmptyString([
4931
+ parsed.conversationID,
4932
+ replyChain.conversationID,
4933
+ authoritySource.conversation_id,
4934
+ Object.keys(sharedIntent).length > 0
4935
+ ? buildSyntheticHumanOpeningConversationID(
4936
+ normalizedRoute,
4937
+ String(parsed.chatID || parsed.chatId || "").trim(),
4938
+ currentMessageID,
4939
+ canonicalHumanMessageKey,
4940
+ )
4941
+ : "",
4942
+ ]);
4931
4943
  const explicitSelectedBotUsernames = uniqueOrderedStrings(
4932
4944
  selectedBotUsernames,
4933
4945
  normalizeTelegramMentionUsername,
@@ -8139,6 +8151,16 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8139
8151
  .map((value) => normalizeTelegramMentionUsername(value))
8140
8152
  .filter(Boolean),
8141
8153
  occurredAt: String(normalized?.occurredAt || "").trim(),
8154
+ canonicalHumanMessageKey: buildRunnerCanonicalHumanInboundKey({
8155
+ chatID: normalized?.chatID,
8156
+ kind,
8157
+ senderID: normalized?.fromID,
8158
+ senderIsBot: normalized?.fromIsBot === true,
8159
+ occurredAt: normalized?.occurredAt,
8160
+ messageThreadID: normalized?.messageThreadID,
8161
+ replyToMessageID: normalized?.replyToMessageID,
8162
+ body: normalized?.text,
8163
+ }),
8142
8164
  sourceOrigin: String(normalized?.archiveSourceOrigin || normalized?.sourceOrigin || "").trim().toLowerCase(),
8143
8165
  sourceRouteKey: String(normalized?.archiveSourceRouteKey || normalized?.sourceRouteKey || "").trim(),
8144
8166
  sourceBotUsername: normalizeTelegramMentionUsername(normalized?.archiveSourceBotUsername || normalized?.sourceBotUsername || ""),
@@ -8275,6 +8297,10 @@ function parseArchivedChatComment(rawBody) {
8275
8297
  .map((value) => normalizeTelegramMentionUsername(value))
8276
8298
  .filter(Boolean),
8277
8299
  occurredAt: firstNonEmptyString([safeObject(structuredPayload).occurredAt, metadata.occurred_at]),
8300
+ canonicalHumanMessageKey: firstNonEmptyString([
8301
+ safeObject(structuredPayload).canonicalHumanMessageKey,
8302
+ metadata.canonical_human_message_key,
8303
+ ]),
8278
8304
  sourceOrigin: firstNonEmptyString([safeObject(structuredPayload).sourceOrigin, metadata.archive_source_origin, metadata.source_origin]).toLowerCase(),
8279
8305
  sourceRouteKey: firstNonEmptyString([safeObject(structuredPayload).sourceRouteKey, metadata.archive_source_route_key, metadata.source_route_key]),
8280
8306
  sourceBotUsername: normalizeTelegramMentionUsername(
@@ -8609,9 +8635,35 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8609
8635
  };
8610
8636
  }
8611
8637
 
8612
- function buildArchivedInboundMessageKey(chatID, messageID, sourceBotUsername = "") {
8613
- const baseKey = `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8614
- const normalizedBotUsername = normalizeTelegramMentionUsername(sourceBotUsername);
8638
+ function buildArchivedInboundMessageKey(chatIDOrEnvelope, messageID = undefined, sourceBotUsername = "") {
8639
+ const rawEnvelope = messageID === undefined && typeof chatIDOrEnvelope === "object" && chatIDOrEnvelope !== null
8640
+ ? safeObject(chatIDOrEnvelope)
8641
+ : {
8642
+ chatID: chatIDOrEnvelope,
8643
+ messageID,
8644
+ sourceBotUsername,
8645
+ };
8646
+ const canonicalHumanKey = buildRunnerCanonicalHumanInboundKey(rawEnvelope);
8647
+ if (canonicalHumanKey) {
8648
+ return `human:${canonicalHumanKey}`;
8649
+ }
8650
+ const chatID = String(
8651
+ rawEnvelope.chatID
8652
+ || rawEnvelope.chatId
8653
+ || rawEnvelope.chat_id
8654
+ || "",
8655
+ ).trim();
8656
+ const normalizedMessageID = intFromRawAllowZero(
8657
+ rawEnvelope.messageID
8658
+ || rawEnvelope.message_id,
8659
+ 0,
8660
+ );
8661
+ const baseKey = `${chatID}:${normalizedMessageID}`;
8662
+ const normalizedBotUsername = normalizeTelegramMentionUsername(
8663
+ rawEnvelope.sourceBotUsername
8664
+ || rawEnvelope.source_bot_username
8665
+ || sourceBotUsername,
8666
+ );
8615
8667
  return normalizedBotUsername ? `${baseKey}::${normalizedBotUsername}` : baseKey;
8616
8668
  }
8617
8669
 
@@ -17931,7 +17983,8 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
17931
17983
  saveRunnerRouteState,
17932
17984
  mergeServerRunnerRequestLedgerIntoLocalState,
17933
17985
  buildRunnerStatusQueryLookup,
17934
- tryJsonParse,
17986
+ buildArchivedInboundMessageKey,
17987
+ tryJsonParse,
17935
17988
  safeObject,
17936
17989
  normalizeRunnerTriggerPolicy,
17937
17990
  evaluateTelegramRunnerTrigger,
@@ -1,4 +1,5 @@
1
1
  import process from "node:process";
2
+ import { createHash } from "node:crypto";
2
3
 
3
4
  function safeObject(value) {
4
5
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -11,6 +12,24 @@ function ensureArray(value) {
11
12
  return Array.isArray(value) ? value : [];
12
13
  }
13
14
 
15
+ function normalizeBodyForCanonicalKey(value) {
16
+ return String(value || "")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+
21
+ function normalizeOccurredAtForCanonicalKey(value) {
22
+ const text = String(value || "").trim();
23
+ if (!text) {
24
+ return "";
25
+ }
26
+ const parsed = Date.parse(text);
27
+ if (!Number.isFinite(parsed)) {
28
+ return text;
29
+ }
30
+ return new Date(Math.floor(parsed / 1000) * 1000).toISOString();
31
+ }
32
+
14
33
  function firstNonEmptyString(values) {
15
34
  for (const value of ensureArray(values)) {
16
35
  const text = String(value ?? "").trim();
@@ -31,6 +50,93 @@ function intFromRaw(raw, fallback) {
31
50
  return parsed > 0 ? parsed : fallback;
32
51
  }
33
52
 
53
+ function buildCanonicalHumanInboundSource(rawValue) {
54
+ const value = safeObject(rawValue);
55
+ const kind = String(value.kind || "").trim().toLowerCase();
56
+ const senderIsBotRaw = value.sender_is_bot
57
+ ?? value.senderIsBot
58
+ ?? value.fromIsBot
59
+ ?? value.from_is_bot;
60
+ const senderIsBot = senderIsBotRaw === true
61
+ || senderIsBotRaw === "true"
62
+ || senderIsBotRaw === 1
63
+ || senderIsBotRaw === "1"
64
+ || kind === "bot_reply";
65
+ if (senderIsBot) {
66
+ return null;
67
+ }
68
+ const chatID = String(
69
+ value.chat_id
70
+ || value.chatID
71
+ || value.chatId
72
+ || "",
73
+ ).trim();
74
+ const senderID = String(
75
+ value.sender_id
76
+ || value.senderID
77
+ || value.fromID
78
+ || value.from_id
79
+ || value.user_id
80
+ || value.userID
81
+ || value.sender_username
82
+ || value.senderUsername
83
+ || value.fromUsername
84
+ || value.from_username
85
+ || value.username
86
+ || "",
87
+ ).trim().toLowerCase();
88
+ const occurredAt = String(
89
+ value.occurred_at
90
+ || value.occurredAt
91
+ || "",
92
+ ).trim();
93
+ const body = normalizeBodyForCanonicalKey(
94
+ value.body
95
+ || value.text
96
+ || "",
97
+ );
98
+ if (!chatID || !senderID || !occurredAt || !body) {
99
+ return null;
100
+ }
101
+ const messageThreadID = intFromRawAllowZero(
102
+ value.message_thread_id
103
+ || value.messageThreadID,
104
+ 0,
105
+ );
106
+ const replyToMessageID = intFromRawAllowZero(
107
+ value.reply_to_message_id
108
+ || value.replyToMessageID,
109
+ 0,
110
+ );
111
+ return {
112
+ chatID,
113
+ senderID,
114
+ occurredAt: normalizeOccurredAtForCanonicalKey(occurredAt),
115
+ body,
116
+ messageThreadID: messageThreadID > 0 ? messageThreadID : 0,
117
+ replyToMessageID: replyToMessageID > 0 ? replyToMessageID : 0,
118
+ };
119
+ }
120
+
121
+ export function buildCanonicalHumanInboundKey(rawValue) {
122
+ const value = safeObject(rawValue);
123
+ const explicitKey = String(
124
+ value.canonical_human_message_key
125
+ || value.canonicalHumanMessageKey
126
+ || "",
127
+ ).trim();
128
+ if (explicitKey) {
129
+ return explicitKey;
130
+ }
131
+ const source = buildCanonicalHumanInboundSource(value);
132
+ if (!source) {
133
+ return "";
134
+ }
135
+ return createHash("sha1")
136
+ .update(JSON.stringify(source))
137
+ .digest("hex");
138
+ }
139
+
34
140
  export function normalizeTelegramMessageEnvelope(rawEnvelope) {
35
141
  const envelope = safeObject(rawEnvelope);
36
142
  const chatID = String(
@@ -61,6 +167,13 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
61
167
  envelope.sender,
62
168
  senderUsername ? `@${senderUsername}` : "",
63
169
  ]);
170
+ const senderID = String(
171
+ envelope.sender_id
172
+ || envelope.senderID
173
+ || envelope.fromID
174
+ || envelope.from_id
175
+ || "",
176
+ ).trim();
64
177
  const rawSenderIsBot = envelope.sender_is_bot ?? envelope.senderIsBot;
65
178
  const senderIsBot = rawSenderIsBot === true
66
179
  || rawSenderIsBot === "true"
@@ -68,6 +181,15 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
68
181
  || rawSenderIsBot === "1"
69
182
  || kind === "bot_reply";
70
183
  const body = String(envelope.body || envelope.text || "").trim();
184
+ const occurredAt = String(
185
+ envelope.occurred_at
186
+ || envelope.occurredAt
187
+ || "",
188
+ ).trim();
189
+ const mentionUsernames = uniqueNormalizedSelectors(
190
+ envelope.mention_usernames
191
+ || envelope.mentionUsernames,
192
+ );
71
193
  const sourceOrigin = String(
72
194
  envelope.source_origin
73
195
  || envelope.sourceOrigin
@@ -90,14 +212,29 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
90
212
  && !(replyToMessageID > 0)
91
213
  && !kind
92
214
  && !sender
215
+ && !senderID
93
216
  && !senderUsername
94
217
  && !body
218
+ && !occurredAt
219
+ && mentionUsernames.length === 0
95
220
  && !sourceOrigin
96
221
  && !sourceRouteKey
97
222
  && !sourceBotUsername
98
223
  ) {
99
224
  return {};
100
225
  }
226
+ const canonicalHumanMessageKey = buildCanonicalHumanInboundKey({
227
+ chat_id: chatID,
228
+ message_id: messageID,
229
+ message_thread_id: messageThreadID,
230
+ reply_to_message_id: replyToMessageID,
231
+ kind,
232
+ sender_id: senderID,
233
+ sender_username: senderUsername,
234
+ sender_is_bot: senderIsBot === true,
235
+ body,
236
+ occurred_at: occurredAt,
237
+ });
101
238
  return {
102
239
  ...(chatID ? { chat_id: chatID } : {}),
103
240
  ...(messageID > 0 ? { message_id: messageID } : {}),
@@ -105,9 +242,13 @@ export function normalizeTelegramMessageEnvelope(rawEnvelope) {
105
242
  ...(replyToMessageID > 0 ? { reply_to_message_id: replyToMessageID } : {}),
106
243
  ...(kind ? { kind } : {}),
107
244
  ...(sender ? { sender } : {}),
245
+ ...(senderID ? { sender_id: senderID } : {}),
108
246
  ...(senderUsername ? { sender_username: senderUsername } : {}),
109
247
  sender_is_bot: senderIsBot === true,
110
248
  ...(body ? { body } : {}),
249
+ ...(occurredAt ? { occurred_at: occurredAt } : {}),
250
+ ...(mentionUsernames.length > 0 ? { mention_usernames: mentionUsernames } : {}),
251
+ ...(canonicalHumanMessageKey ? { canonical_human_message_key: canonicalHumanMessageKey } : {}),
111
252
  ...(sourceOrigin ? { source_origin: sourceOrigin } : {}),
112
253
  ...(sourceRouteKey ? { source_route_key: sourceRouteKey } : {}),
113
254
  ...(sourceBotUsername ? { source_bot_username: sourceBotUsername } : {}),
@@ -126,6 +267,10 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
126
267
  message_thread_id: overrides.message_thread_id ?? parsedArchive.messageThreadID,
127
268
  reply_to_message_id: overrides.reply_to_message_id ?? parsedArchive.replyToMessageID,
128
269
  kind: firstNonEmptyString([overrides.kind, parsedArchive.kind]),
270
+ sender_id: firstNonEmptyString([
271
+ overrides.sender_id,
272
+ parsedArchive.senderID,
273
+ ]),
129
274
  sender: firstNonEmptyString([
130
275
  overrides.sender,
131
276
  parsedArchive.sender,
@@ -140,6 +285,17 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
140
285
  ]),
141
286
  sender_is_bot: overrides.sender_is_bot ?? parsedArchive.senderIsBot,
142
287
  body: firstNonEmptyString([overrides.body, parsedArchive.body]),
288
+ occurred_at: firstNonEmptyString([overrides.occurred_at, parsedArchive.occurredAt]),
289
+ mention_usernames: ensureArray(
290
+ overrides.mention_usernames
291
+ || overrides.mentionUsernames
292
+ || parsedArchive.mentionUsernames,
293
+ ),
294
+ canonical_human_message_key: firstNonEmptyString([
295
+ overrides.canonical_human_message_key,
296
+ overrides.canonicalHumanMessageKey,
297
+ parsedArchive.canonicalHumanMessageKey,
298
+ ]),
143
299
  source_origin: firstNonEmptyString([
144
300
  overrides.source_origin,
145
301
  parsedArchive.sourceOrigin,
@@ -451,6 +607,10 @@ export function buildProcessableArchiveLogicalKey(recordRaw) {
451
607
  if (!isRunnerProcessableArchiveKind(kind)) {
452
608
  return "";
453
609
  }
610
+ const canonicalHumanKey = buildCanonicalHumanInboundKey(parsed);
611
+ if (canonicalHumanKey) {
612
+ return `human:${canonicalHumanKey}`;
613
+ }
454
614
  const chatID = firstNonEmptyString([parsed.chatID, parsed.chatId]);
455
615
  const messageID = intFromRawAllowZero(parsed.messageID, 0);
456
616
  if (!chatID || !(messageID > 0)) {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ buildCanonicalHumanInboundKey,
2
3
  buildRunnerContextWindow,
3
4
  compareArchiveCommentRecords,
4
5
  dedupeProcessableArchiveComments,
@@ -103,6 +104,10 @@ function resolveRunnerLocalInboundReceiptTargetSelector(receiptRaw) {
103
104
 
104
105
  function buildRunnerArchiveSourceMessageKey(recordRaw) {
105
106
  const parsed = safeObject(safeObject(recordRaw).parsedArchive);
107
+ const canonicalHumanKey = buildCanonicalHumanInboundKey(parsed);
108
+ if (canonicalHumanKey) {
109
+ return `human:${canonicalHumanKey}`;
110
+ }
106
111
  const chatID = String(parsed.chatID || parsed.chatId || "").trim();
107
112
  const messageID = intFromRawAllowZero(parsed.messageID || parsed.messageId, 0);
108
113
  if (!chatID || !(messageID > 0)) {
@@ -113,6 +118,21 @@ function buildRunnerArchiveSourceMessageKey(recordRaw) {
113
118
 
114
119
  function buildRunnerLocalInboundReceiptKey(receiptRaw) {
115
120
  const receipt = safeObject(receiptRaw);
121
+ const canonicalHumanKey = buildCanonicalHumanInboundKey({
122
+ chat_id: receipt.chat_id,
123
+ message_id: receipt.message_id,
124
+ message_thread_id: receipt.message_thread_id,
125
+ reply_to_message_id: receipt.reply_to_message_id,
126
+ kind: receipt.kind,
127
+ sender_id: receipt.sender_id,
128
+ sender_username: receipt.sender_username,
129
+ sender_is_bot: receipt.sender_is_bot === true,
130
+ body: receipt.body,
131
+ occurred_at: receipt.occurred_at,
132
+ });
133
+ if (canonicalHumanKey) {
134
+ return `human:${canonicalHumanKey}`;
135
+ }
116
136
  const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
117
137
  const messageID = intFromRawAllowZero(receipt.message_id || receipt.messageID, 0);
118
138
  if (!chatID || !(messageID > 0)) {
@@ -539,10 +539,12 @@ function buildRunnerLocalInboundEnvelopeFromReceipt(rawReceipt) {
539
539
  message_thread_id: receipt.message_thread_id,
540
540
  reply_to_message_id: receipt.reply_to_message_id,
541
541
  kind: receipt.kind,
542
+ sender_id: receipt.sender_id,
542
543
  sender: receipt.sender,
543
544
  sender_username: receipt.sender_username,
544
545
  sender_is_bot: receipt.sender_is_bot === true,
545
546
  body: receipt.body,
547
+ occurred_at: receipt.occurred_at,
546
548
  source_origin: receipt.receipt_origin,
547
549
  source_route_key: receipt.receipt_route_key,
548
550
  source_bot_username: receipt.receipt_bot_username,
@@ -670,16 +672,13 @@ function buildRunnerRecentLocalInboundReceipts(routeStateRaw, localInboundArtifa
670
672
  const RUNNER_INBOUND_ARCHIVE_RESERVATION_TTL_MS = 10 * 60 * 1000;
671
673
  const runnerInboundArchiveReservations = new Map();
672
674
 
673
- function buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername = "") {
675
+ function buildRunnerInboundArchiveReservationKey(threadID, archiveMessageKey = "") {
674
676
  const normalizedThreadID = String(threadID || "").trim();
675
- const normalizedChatID = String(chatID || "").trim();
676
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
677
- const normalizedSourceBotUsername = normalizeMentionSelector(sourceBotUsername);
678
- if (!normalizedThreadID || !normalizedChatID || !(normalizedMessageID > 0)) {
677
+ const normalizedArchiveMessageKey = String(archiveMessageKey || "").trim();
678
+ if (!normalizedThreadID || !normalizedArchiveMessageKey) {
679
679
  return "";
680
680
  }
681
- const baseKey = `${normalizedThreadID}::${normalizedChatID}:${normalizedMessageID}`;
682
- return normalizedSourceBotUsername ? `${baseKey}::${normalizedSourceBotUsername}` : baseKey;
681
+ return `${normalizedThreadID}::${normalizedArchiveMessageKey}`;
683
682
  }
684
683
 
685
684
  function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
@@ -690,8 +689,8 @@ function cleanupRunnerInboundArchiveReservations(nowMs = Date.now()) {
690
689
  }
691
690
  }
692
691
 
693
- function reserveRunnerInboundArchiveMessage(threadID, chatID, messageID, sourceBotUsername = "") {
694
- const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, chatID, messageID, sourceBotUsername);
692
+ function reserveRunnerInboundArchiveMessage(threadID, archiveMessageKey = "") {
693
+ const reservationKey = buildRunnerInboundArchiveReservationKey(threadID, archiveMessageKey);
695
694
  if (!reservationKey) {
696
695
  return {
697
696
  ok: false,
@@ -821,7 +820,7 @@ async function loadRunnerExistingInboundArchiveKeys({
821
820
  .map((record) => normalizeArchiveCommentRecord(record, parseArchivedChatComment))
822
821
  .map((record) => record.parsedArchive)
823
822
  .filter((parsed) => parsed && isInboundArchiveKind(parsed.kind) && parsed.chatID)
824
- .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID, parsed.sourceBotUsername)),
823
+ .map((parsed) => buildArchivedInboundMessageKey(parsed)),
825
824
  );
826
825
  }
827
826
 
@@ -884,16 +883,27 @@ async function archiveRunnerTelegramInboundUpdates({
884
883
  route,
885
884
  bot,
886
885
  });
887
- const dedupeKey = buildArchivedInboundMessageKey(update.chatID, update.messageID, archiveSourceBotUsername);
886
+ const dedupeKey = buildArchivedInboundMessageKey({
887
+ chatID: update.chatID,
888
+ messageID: update.messageID,
889
+ messageThreadID: update.messageThreadID,
890
+ kind: update.fromIsBot ? "bot_reply" : "telegram_message",
891
+ senderID: update.fromID,
892
+ sender: update.fromName,
893
+ senderUsername: update.fromUsername,
894
+ senderIsBot: update.fromIsBot === true,
895
+ body: update.text,
896
+ occurredAt: update.occurredAt,
897
+ replyToMessageID: update.replyToMessageID,
898
+ sourceBotUsername: archiveSourceBotUsername,
899
+ });
888
900
  if (boolFromRaw(archivePolicy.dedupeInbound, true) && existingKeys.has(dedupeKey)) {
889
901
  continue;
890
902
  }
891
903
  const reservation = boolFromRaw(archivePolicy.dedupeInbound, true)
892
904
  ? reserveRunnerInboundArchiveMessage(
893
905
  archiveThread?.threadID,
894
- update.chatID,
895
- update.messageID,
896
- archiveSourceBotUsername,
906
+ dedupeKey,
897
907
  )
898
908
  : { ok: true, reservationKey: "" };
899
909
  if (!reservation.ok) {
@@ -299,6 +299,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
299
299
  const mergeServerRunnerRequestLedgerIntoLocalState = requireDependency(deps, "mergeServerRunnerRequestLedgerIntoLocalState");
300
300
  const buildRunnerStatusQueryLookup = requireDependency(deps, "buildRunnerStatusQueryLookup");
301
301
  const claimRunnerRequestForHumanComment = requireDependency(deps, "claimRunnerRequestForHumanComment");
302
+ const buildArchivedInboundMessageKey = requireDependency(deps, "buildArchivedInboundMessageKey");
302
303
  const markRunnerRequestLifecycle = requireDependency(deps, "markRunnerRequestLifecycle");
303
304
  const resolveRunnerContinuationRequestForBotReply = requireDependency(deps, "resolveRunnerContinuationRequestForBotReply");
304
305
  const prepareRunnerSelectedRecordRecoveryContext = requireDependency(deps, "prepareRunnerSelectedRecordRecoveryContext");
@@ -2180,17 +2181,142 @@ export async function runSelftestRunnerScenarios(push, deps) {
2180
2181
  const authoritativeSourceRequest = safeObject(
2181
2182
  safeObject(loadBotRunnerState().requests)[authoritativeSourceClaim.requestKey],
2182
2183
  );
2183
- push(
2184
- "runner_request_claim_preserves_authoritative_source_message_envelope",
2185
- authoritativeSourceClaim.ok === true
2186
- && String(authoritativeSourceRequest.source_message_origin || "") === "local_telegram_inbound"
2187
- && String(authoritativeSourceRequest.source_message_route_key || "") === requestRouteKey
2188
- && String(authoritativeSourceRequest.source_message_bot_username || "") === "ryoai_bot"
2189
- && Number(safeObject(authoritativeSourceRequest.source_message_envelope).message_id || 0) === 503,
2190
- `origin=${String(authoritativeSourceRequest.source_message_origin || "(none)")} route=${String(authoritativeSourceRequest.source_message_route_key || "(none)")} bot=${String(authoritativeSourceRequest.source_message_bot_username || "(none)")} message=${String(safeObject(authoritativeSourceRequest.source_message_envelope).message_id || "(none)")}`,
2191
- );
2192
-
2193
- const rootTaskRecord = {
2184
+ push(
2185
+ "runner_request_claim_preserves_authoritative_source_message_envelope",
2186
+ authoritativeSourceClaim.ok === true
2187
+ && String(authoritativeSourceRequest.source_message_origin || "") === "local_telegram_inbound"
2188
+ && String(authoritativeSourceRequest.source_message_route_key || "") === requestRouteKey
2189
+ && String(authoritativeSourceRequest.source_message_bot_username || "") === "ryoai_bot"
2190
+ && Number(safeObject(authoritativeSourceRequest.source_message_envelope).message_id || 0) === 503,
2191
+ `origin=${String(authoritativeSourceRequest.source_message_origin || "(none)")} route=${String(authoritativeSourceRequest.source_message_route_key || "(none)")} bot=${String(authoritativeSourceRequest.source_message_bot_username || "(none)")} message=${String(safeObject(authoritativeSourceRequest.source_message_envelope).message_id || "(none)")}`,
2192
+ );
2193
+
2194
+ const canonicalHumanArchiveKeyPrimary = buildArchivedInboundMessageKey({
2195
+ chatID: "-100123",
2196
+ messageID: 1189,
2197
+ messageThreadID: 0,
2198
+ kind: "telegram_message",
2199
+ senderID: "7001",
2200
+ senderIsBot: false,
2201
+ occurredAt: "2026-04-01T06:00:00.000Z",
2202
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의 해죠",
2203
+ sourceBotUsername: "ryoai_bot",
2204
+ });
2205
+ const canonicalHumanArchiveKeyPeer = buildArchivedInboundMessageKey({
2206
+ chatID: "-100123",
2207
+ messageID: 294,
2208
+ messageThreadID: 0,
2209
+ kind: "telegram_message",
2210
+ senderID: "7001",
2211
+ senderIsBot: false,
2212
+ occurredAt: "2026-04-01T06:00:00.000Z",
2213
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의 해죠",
2214
+ sourceBotUsername: "ryoai3_bot",
2215
+ });
2216
+ push(
2217
+ "runner_human_inbound_archive_identity_is_canonical_across_routes",
2218
+ canonicalHumanArchiveKeyPrimary === canonicalHumanArchiveKeyPeer
2219
+ && canonicalHumanArchiveKeyPrimary.startsWith("human:"),
2220
+ `primary=${canonicalHumanArchiveKeyPrimary} peer=${canonicalHumanArchiveKeyPeer}`,
2221
+ );
2222
+
2223
+ const sharedHumanRouteRyoai1 = normalizeRunnerRoute({
2224
+ ...requestRoute,
2225
+ name: "telegram-monitor-request-ledger-shared-human-ryoai1",
2226
+ });
2227
+ const sharedHumanRouteRyoai3 = normalizeRunnerRoute({
2228
+ ...requestRoute,
2229
+ name: "telegram-monitor-request-ledger-shared-human-ryoai3",
2230
+ });
2231
+ const sharedHumanRouteRyoai1Key = runnerRouteKey(sharedHumanRouteRyoai1);
2232
+ const sharedHumanRouteRyoai3Key = runnerRouteKey(sharedHumanRouteRyoai3);
2233
+ const sharedHumanBody = "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의 해죠";
2234
+ const sharedHumanRecordPrimary = {
2235
+ id: "comment-request-shared-human-1",
2236
+ createdAt: "2026-04-01T06:00:00.000Z",
2237
+ updatedAt: "2026-04-01T06:00:00.000Z",
2238
+ parsedArchive: {
2239
+ kind: "telegram_message",
2240
+ chatID: "-100123",
2241
+ chatType: "supergroup",
2242
+ body: sharedHumanBody,
2243
+ messageID: 1189,
2244
+ senderID: "7001",
2245
+ senderIsBot: false,
2246
+ occurredAt: "2026-04-01T06:00:00.000Z",
2247
+ mentionUsernames: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
2248
+ },
2249
+ };
2250
+ const sharedHumanRecordPeer = {
2251
+ id: "comment-request-shared-human-2",
2252
+ createdAt: "2026-04-01T06:00:00.000Z",
2253
+ updatedAt: "2026-04-01T06:00:00.000Z",
2254
+ parsedArchive: {
2255
+ kind: "telegram_message",
2256
+ chatID: "-100123",
2257
+ chatType: "supergroup",
2258
+ body: sharedHumanBody,
2259
+ messageID: 294,
2260
+ senderID: "7001",
2261
+ senderIsBot: false,
2262
+ occurredAt: "2026-04-01T06:00:00.000Z",
2263
+ mentionUsernames: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
2264
+ },
2265
+ };
2266
+ const sharedHumanClaimPrimary = await claimRunnerRequestForHumanComment({
2267
+ normalizedRoute: sharedHumanRouteRyoai1,
2268
+ routeKey: sharedHumanRouteRyoai1Key,
2269
+ selectedRecord: sharedHumanRecordPrimary,
2270
+ selectedBotUsernames: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
2271
+ normalizedIntent: "discussion_request",
2272
+ authoritativeSourceMessageEnvelope: {
2273
+ chat_id: "-100123",
2274
+ message_id: 1189,
2275
+ sender_id: "7001",
2276
+ sender_is_bot: false,
2277
+ occurred_at: "2026-04-01T06:00:00.000Z",
2278
+ body: sharedHumanBody,
2279
+ source_origin: "local_telegram_inbound",
2280
+ source_route_key: sharedHumanRouteRyoai1Key,
2281
+ source_bot_username: "ryoai_bot",
2282
+ },
2283
+ });
2284
+ const sharedHumanClaimPeer = await claimRunnerRequestForHumanComment({
2285
+ normalizedRoute: sharedHumanRouteRyoai3,
2286
+ routeKey: sharedHumanRouteRyoai3Key,
2287
+ selectedRecord: sharedHumanRecordPeer,
2288
+ selectedBotUsernames: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
2289
+ normalizedIntent: "discussion_request",
2290
+ authoritativeSourceMessageEnvelope: {
2291
+ chat_id: "-100123",
2292
+ message_id: 294,
2293
+ sender_id: "7001",
2294
+ sender_is_bot: false,
2295
+ occurred_at: "2026-04-01T06:00:00.000Z",
2296
+ body: sharedHumanBody,
2297
+ source_origin: "local_telegram_inbound",
2298
+ source_route_key: sharedHumanRouteRyoai3Key,
2299
+ source_bot_username: "ryoai3_bot",
2300
+ },
2301
+ });
2302
+ const sharedHumanState = loadBotRunnerState();
2303
+ const sharedHumanRequestCount = Object.values(safeObject(sharedHumanState.requests))
2304
+ .filter((entryRaw) => {
2305
+ const entry = safeObject(entryRaw);
2306
+ return String(entry.chat_id || "") === "-100123"
2307
+ && String(entry.source_message_body || "") === sharedHumanBody;
2308
+ })
2309
+ .length;
2310
+ push(
2311
+ "runner_human_opening_request_identity_is_canonical_across_routes",
2312
+ sharedHumanClaimPrimary.ok === true
2313
+ && (sharedHumanClaimPeer.ok === true || String(sharedHumanClaimPeer.reason || "") === "request_already_claimed")
2314
+ && String(sharedHumanClaimPrimary.requestKey || "") === String(sharedHumanClaimPeer.requestKey || "")
2315
+ && sharedHumanRequestCount === 1,
2316
+ `primary=${String(sharedHumanClaimPrimary.requestKey || "(none)")} peer=${String(sharedHumanClaimPeer.requestKey || "(none)")} peer_reason=${String(sharedHumanClaimPeer.reason || "(none)")} count=${sharedHumanRequestCount}`,
2317
+ );
2318
+
2319
+ const rootTaskRecord = {
2194
2320
  id: "comment-request-root-task-1",
2195
2321
  createdAt: "2026-03-22T00:05:00.000Z",
2196
2322
  updatedAt: "2026-03-22T00:05:00.000Z",
@@ -1599,6 +1599,123 @@ export async function runSelftestTelegramE2E(push, deps) {
1599
1599
  });
1600
1600
  const routeRyoai2Key = runnerRouteKey(routeRyoai2);
1601
1601
  const routeRyoai3Key = runnerRouteKey(routeRyoai3);
1602
+
1603
+ telegramE2EServer.state.comments = [];
1604
+ telegramE2EServer.state.updates = [
1605
+ {
1606
+ update_id: 402,
1607
+ message: {
1608
+ message_id: 84,
1609
+ date: Math.floor(Date.now() / 1000),
1610
+ chat: {
1611
+ id: Number(e2eDestination.chat_id),
1612
+ type: "supergroup",
1613
+ title: e2eDestination.label,
1614
+ },
1615
+ from: {
1616
+ id: 7001,
1617
+ is_bot: false,
1618
+ first_name: "Operator",
1619
+ username: "operator_user",
1620
+ },
1621
+ text: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의 해죠",
1622
+ entities: buildTelegramMentionEntities("@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의 해죠"),
1623
+ },
1624
+ },
1625
+ ];
1626
+ const sharedHumanRouteRuntime = {
1627
+ baseURL: telegramE2EServer.baseURL,
1628
+ timeoutSeconds: 10,
1629
+ token: e2eToken,
1630
+ actor: {
1631
+ user_id: e2eActorUserID,
1632
+ },
1633
+ };
1634
+ const sharedHumanManagedBots = [
1635
+ {
1636
+ username: "RyoAI_bot",
1637
+ route: routeRyoai1,
1638
+ bot: { username: "RyoAI_bot", name: "RyoAI_bot" },
1639
+ },
1640
+ {
1641
+ username: "RyoAI2_bot",
1642
+ route: routeRyoai2,
1643
+ bot: { username: "RyoAI2_bot", name: "RyoAI2_bot" },
1644
+ },
1645
+ {
1646
+ username: "RyoAI3_bot",
1647
+ route: routeRyoai3,
1648
+ bot: { username: "RyoAI3_bot", name: "RyoAI3_bot" },
1649
+ },
1650
+ ];
1651
+ await archiveLocalTelegramMessagesForRoute({
1652
+ routeKey: runnerRouteKey(routeRyoai1),
1653
+ route: routeRyoai1,
1654
+ routeState: {},
1655
+ runtime: sharedHumanRouteRuntime,
1656
+ bot: {
1657
+ id: "88888888-8888-4888-8888-888888888881",
1658
+ name: "RyoAI_bot",
1659
+ username: "RyoAI_bot",
1660
+ role: "monitor",
1661
+ },
1662
+ destination: {
1663
+ chatID: e2eDestination.chat_id,
1664
+ },
1665
+ archiveThread: {
1666
+ threadID: e2eThreadID,
1667
+ },
1668
+ managedConversationBots: sharedHumanManagedBots,
1669
+ deps: buildRunnerRuntimeDeps(),
1670
+ });
1671
+ await archiveLocalTelegramMessagesForRoute({
1672
+ routeKey: routeRyoai2Key,
1673
+ route: routeRyoai2,
1674
+ routeState: {},
1675
+ runtime: sharedHumanRouteRuntime,
1676
+ bot: {
1677
+ id: "88888888-8888-4888-8888-888888888882",
1678
+ name: "RyoAI2_bot",
1679
+ username: "RyoAI2_bot",
1680
+ role: "monitor",
1681
+ },
1682
+ destination: {
1683
+ chatID: e2eDestination.chat_id,
1684
+ },
1685
+ archiveThread: {
1686
+ threadID: e2eThreadID,
1687
+ },
1688
+ managedConversationBots: sharedHumanManagedBots,
1689
+ deps: buildRunnerRuntimeDeps(),
1690
+ });
1691
+ await archiveLocalTelegramMessagesForRoute({
1692
+ routeKey: routeRyoai3Key,
1693
+ route: routeRyoai3,
1694
+ routeState: {},
1695
+ runtime: sharedHumanRouteRuntime,
1696
+ bot: {
1697
+ id: "88888888-8888-4888-8888-888888888883",
1698
+ name: "RyoAI3_bot",
1699
+ username: "RyoAI3_bot",
1700
+ role: "monitor",
1701
+ },
1702
+ destination: {
1703
+ chatID: e2eDestination.chat_id,
1704
+ },
1705
+ archiveThread: {
1706
+ threadID: e2eThreadID,
1707
+ },
1708
+ managedConversationBots: sharedHumanManagedBots,
1709
+ deps: buildRunnerRuntimeDeps(),
1710
+ });
1711
+ const sharedHumanBodies = telegramE2EServer.state.comments.map((item) => String(item.body || ""));
1712
+ push(
1713
+ "telegram_multi_bot_human_opening_archives_once_across_routes",
1714
+ sharedHumanBodies.filter((item) => item.includes("message_id: 84")).length === 1,
1715
+ `count=${sharedHumanBodies.filter((item) => item.includes("message_id: 84")).length} bodies=${sharedHumanBodies.join(" || ")}`,
1716
+ );
1717
+
1718
+ telegramE2EServer.state.comments = [];
1602
1719
  await archiveLocalTelegramMessagesForRoute({
1603
1720
  routeKey: routeRyoai3Key,
1604
1721
  route: routeRyoai3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.277",
3
+ "version": "0.2.278",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [