metheus-governance-mcp-cli 0.2.276 → 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,
@@ -5770,7 +5782,21 @@ async function finalizePreparedRunnerSelectedRecordExecutionContext({
5770
5782
  };
5771
5783
  }
5772
5784
  if (mode === "continuation_finalize") {
5773
- const requestClaim = normalizePreparedRunnerRequestClaim(requestPreparation.requestClaim);
5785
+ const requestClaim = normalizePreparedRunnerRequestClaim(resolveRunnerContinuationRequestForBotReplyImpl({
5786
+ normalizedRoute,
5787
+ routeKey,
5788
+ selectedRecord,
5789
+ persist: true,
5790
+ deps: buildRunnerRecoveryDeps(),
5791
+ }));
5792
+ if (!requestClaim.ok) {
5793
+ return finalizeRunnerRequestClaimFailureRecorderState({
5794
+ normalizedRoute,
5795
+ runtime,
5796
+ requestClaim,
5797
+ syncRunnerRequestLedgerForProjectToServer,
5798
+ });
5799
+ }
5774
5800
  const finalizedRequest = await finalizePreparedRunnerRequestForExecution({
5775
5801
  normalizedRoute,
5776
5802
  routeKey,
@@ -8125,6 +8151,16 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8125
8151
  .map((value) => normalizeTelegramMentionUsername(value))
8126
8152
  .filter(Boolean),
8127
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
+ }),
8128
8164
  sourceOrigin: String(normalized?.archiveSourceOrigin || normalized?.sourceOrigin || "").trim().toLowerCase(),
8129
8165
  sourceRouteKey: String(normalized?.archiveSourceRouteKey || normalized?.sourceRouteKey || "").trim(),
8130
8166
  sourceBotUsername: normalizeTelegramMentionUsername(normalized?.archiveSourceBotUsername || normalized?.sourceBotUsername || ""),
@@ -8261,6 +8297,10 @@ function parseArchivedChatComment(rawBody) {
8261
8297
  .map((value) => normalizeTelegramMentionUsername(value))
8262
8298
  .filter(Boolean),
8263
8299
  occurredAt: firstNonEmptyString([safeObject(structuredPayload).occurredAt, metadata.occurred_at]),
8300
+ canonicalHumanMessageKey: firstNonEmptyString([
8301
+ safeObject(structuredPayload).canonicalHumanMessageKey,
8302
+ metadata.canonical_human_message_key,
8303
+ ]),
8264
8304
  sourceOrigin: firstNonEmptyString([safeObject(structuredPayload).sourceOrigin, metadata.archive_source_origin, metadata.source_origin]).toLowerCase(),
8265
8305
  sourceRouteKey: firstNonEmptyString([safeObject(structuredPayload).sourceRouteKey, metadata.archive_source_route_key, metadata.source_route_key]),
8266
8306
  sourceBotUsername: normalizeTelegramMentionUsername(
@@ -8595,9 +8635,35 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8595
8635
  };
8596
8636
  }
8597
8637
 
8598
- function buildArchivedInboundMessageKey(chatID, messageID, sourceBotUsername = "") {
8599
- const baseKey = `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
8600
- 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
+ );
8601
8667
  return normalizedBotUsername ? `${baseKey}::${normalizedBotUsername}` : baseKey;
8602
8668
  }
8603
8669
 
@@ -17917,7 +17983,8 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
17917
17983
  saveRunnerRouteState,
17918
17984
  mergeServerRunnerRequestLedgerIntoLocalState,
17919
17985
  buildRunnerStatusQueryLookup,
17920
- tryJsonParse,
17986
+ buildArchivedInboundMessageKey,
17987
+ tryJsonParse,
17921
17988
  safeObject,
17922
17989
  normalizeRunnerTriggerPolicy,
17923
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)) {
@@ -16,6 +16,10 @@ function ensureArray(value) {
16
16
  return Array.isArray(value) ? value : [];
17
17
  }
18
18
 
19
+ function normalizeMentionSelector(rawValue) {
20
+ return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
21
+ }
22
+
19
23
  function intFromRawAllowZero(raw, fallback = 0) {
20
24
  if (raw === null || raw === undefined || raw === "") {
21
25
  return fallback;
@@ -48,6 +52,45 @@ function buildRunnerContinuationContractTriggerDecision(triggerDecision) {
48
52
  };
49
53
  }
50
54
 
55
+ function isRunnerBotReplyContinuationAuthorizedForCurrentBot({
56
+ selectedRecord,
57
+ persistedRequest = null,
58
+ currentBotSelector = "",
59
+ }) {
60
+ const normalizedCurrentBotSelector = normalizeMentionSelector(currentBotSelector);
61
+ if (!normalizedCurrentBotSelector) {
62
+ return false;
63
+ }
64
+ const parsed = safeObject(selectedRecord?.parsedArchive);
65
+ const normalizedSenderSelectors = ensureArray([
66
+ parsed.botUsername,
67
+ parsed.senderUsername,
68
+ parsed.sender,
69
+ ])
70
+ .map((value) => normalizeMentionSelector(value))
71
+ .filter(Boolean);
72
+ if (normalizedSenderSelectors.includes(normalizedCurrentBotSelector)) {
73
+ return false;
74
+ }
75
+ const explicitReplyTargets = ensureArray([
76
+ ...(Array.isArray(parsed.mentionUsernames) ? parsed.mentionUsernames : []),
77
+ parsed.replyToBotUsername,
78
+ parsed.targetBotUsername,
79
+ parsed.assignBotUsername,
80
+ parsed.nextResponderBotUsername,
81
+ ])
82
+ .map((value) => normalizeMentionSelector(value))
83
+ .filter(Boolean);
84
+ if (explicitReplyTargets.includes(normalizedCurrentBotSelector)) {
85
+ return true;
86
+ }
87
+ const request = safeObject(persistedRequest);
88
+ const nextExpectedResponders = ensureArray(request.next_expected_responders)
89
+ .map((value) => normalizeMentionSelector(value))
90
+ .filter(Boolean);
91
+ return nextExpectedResponders.includes(normalizedCurrentBotSelector);
92
+ }
93
+
51
94
  export async function resolveRunnerPrecomputedResponderAdjudication({
52
95
  selectedRecord,
53
96
  pendingOrdered,
@@ -189,6 +232,11 @@ export async function resolveRunnerPrecomputedBotReplyAuthorityContext({
189
232
  persistedRequest: resolveRunnerContinuationLinkedRequest(normalizedRequestClaim),
190
233
  triggerDecision,
191
234
  });
235
+ const continuationAuthorizedForCurrentBot = isRunnerBotReplyContinuationAuthorizedForCurrentBot({
236
+ selectedRecord,
237
+ persistedRequest: resolveRunnerContinuationLinkedRequest(normalizedRequestClaim),
238
+ currentBotSelector,
239
+ });
192
240
  const currentBotSelected = ensureArray(continuationAdjudication?.selected_bot_usernames)
193
241
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
194
242
  .includes(currentBotSelector);
@@ -204,6 +252,18 @@ export async function resolveRunnerPrecomputedBotReplyAuthorityContext({
204
252
  sharedHumanIntentContext: null,
205
253
  };
206
254
  }
255
+ if (!continuationAuthorizedForCurrentBot) {
256
+ return {
257
+ shouldSkip: true,
258
+ requestless: false,
259
+ skipAction: "adjudication_skipped",
260
+ skipReason: "bot_reply_not_explicitly_handed_off",
261
+ skipTrigger: "request_contract",
262
+ skipRecordPatch: {},
263
+ adjudication: continuationAdjudication,
264
+ sharedHumanIntentContext: null,
265
+ };
266
+ }
207
267
  return {
208
268
  shouldSkip: false,
209
269
  requestless: false,
@@ -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)) {
@@ -431,6 +431,7 @@ export function peekRunnerContinuationRequestForBotReply({
431
431
  export function recoverRunnerContinuationRequestForBotReply({
432
432
  normalizedRoute,
433
433
  continuationPreview,
434
+ persist = true,
434
435
  deps = {},
435
436
  }) {
436
437
  const upsertRunnerRequest = requireDependency(deps, "upsertRunnerRequest");
@@ -506,6 +507,18 @@ export function recoverRunnerContinuationRequestForBotReply({
506
507
  || "",
507
508
  ).trim().toLowerCase(),
508
509
  };
510
+ if (persist !== true) {
511
+ return {
512
+ ok: true,
513
+ linkedRequestKey: fallbackRequestKey,
514
+ linkedRequest: seedRequest,
515
+ currentState,
516
+ conversationID: String(preview.conversationID || "").trim(),
517
+ commentKind: String(preview.commentKind || "").trim().toLowerCase(),
518
+ linkResolutionKind: "recovered_request_link",
519
+ linkRecovered: true,
520
+ };
521
+ }
509
522
  const seededRequest = upsertRunnerRequest(currentState, fallbackRequestKey, seedRequest);
510
523
  currentState.requests = seededRequest.requests;
511
524
  saveBotRunnerState({
@@ -531,6 +544,7 @@ export function resolveRunnerContinuationRequestForBotReply({
531
544
  normalizedRoute,
532
545
  routeKey,
533
546
  selectedRecord,
547
+ persist = true,
534
548
  deps = {},
535
549
  }) {
536
550
  const upsertRunnerConsumedComment = requireDependency(deps, "upsertRunnerConsumedComment");
@@ -547,12 +561,16 @@ export function resolveRunnerContinuationRequestForBotReply({
547
561
  continuation = recoverRunnerContinuationRequestForBotReply({
548
562
  normalizedRoute,
549
563
  continuationPreview: continuation,
564
+ persist,
550
565
  deps,
551
566
  });
552
567
  }
553
568
  if (!continuation.ok) {
554
569
  return continuation;
555
570
  }
571
+ if (persist !== true) {
572
+ return continuation;
573
+ }
556
574
  const {
557
575
  currentState,
558
576
  linkedRequest,
@@ -609,6 +627,7 @@ export function prepareRunnerBotReplyContinuationContext({
609
627
  normalizedRoute,
610
628
  routeKey,
611
629
  selectedRecord,
630
+ persist: false,
612
631
  deps,
613
632
  })
614
633
  : continuationPreview;
@@ -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",
@@ -16340,11 +16466,82 @@ export async function runSelftestRunnerScenarios(push, deps) {
16340
16466
  && !Object.prototype.hasOwnProperty.call(continuation, "request"),
16341
16467
  `linked_key=${String(continuation.linkedRequestKey || "(none)")} has_request=${String(Object.prototype.hasOwnProperty.call(continuation, "request"))}`,
16342
16468
  );
16343
- } finally {
16344
- process.env.HOME = previousHome;
16345
- process.env.USERPROFILE = previousUserProfile;
16346
- try {
16347
- fs.rmSync(provenanceTempRoot, { recursive: true, force: true });
16469
+ saveBotRunnerState({
16470
+ routes: {
16471
+ [provenanceRouteKey]: {
16472
+ conversation_sessions: {
16473
+ "conversation-provenance-seed": {
16474
+ status: "open",
16475
+ started_at: "2026-03-27T00:00:00.000Z",
16476
+ participants: ["ryoai_bot"],
16477
+ initial_responders: ["ryoai_bot"],
16478
+ allowed_responders: ["ryoai_bot"],
16479
+ intent_mode: "single_bot",
16480
+ allow_bot_to_bot: false,
16481
+ },
16482
+ },
16483
+ recent_local_inbound_receipts: {
16484
+ "-100123:779": {
16485
+ chat_id: "-100123",
16486
+ message_id: 779,
16487
+ receipt_origin: "local_telegram_inbound",
16488
+ receipt_route_key: provenanceRouteKey,
16489
+ receipt_bot_username: "ryoai_bot",
16490
+ source_origin: "local_telegram_inbound",
16491
+ source_route_key: provenanceRouteKey,
16492
+ source_bot_username: "ryoai_bot",
16493
+ body: "hello provenance",
16494
+ message_thread_id: 43,
16495
+ },
16496
+ },
16497
+ last_followup_source_message_envelope: {
16498
+ message_id: 779,
16499
+ message_thread_id: 43,
16500
+ body: "hello provenance",
16501
+ source_origin: "local_telegram_inbound",
16502
+ source_route_key: provenanceRouteKey,
16503
+ source_bot_username: "ryoai_bot",
16504
+ },
16505
+ },
16506
+ },
16507
+ sharedInboxes: {},
16508
+ excludedComments: {},
16509
+ requests: {},
16510
+ consumedComments: {},
16511
+ });
16512
+ const stateBeforePreviewOnlyContinuation = loadBotRunnerState();
16513
+ const previewOnlyRecoveryContext = prepareRunnerSelectedRecordRecoveryContext({
16514
+ normalizedRoute: provenanceRoute,
16515
+ routeKey: provenanceRouteKey,
16516
+ selectedRecord: {
16517
+ id: "bot-reply-preview-only-seed",
16518
+ parsedArchive: {
16519
+ kind: "bot_reply",
16520
+ chatID: "-100123",
16521
+ messageID: 881,
16522
+ senderIsBot: true,
16523
+ conversationID: "conversation-provenance-seed",
16524
+ botUsername: "@ryoai_bot",
16525
+ },
16526
+ },
16527
+ });
16528
+ const stateAfterPreviewOnlyContinuation = loadBotRunnerState();
16529
+ const previewRequestsBefore = Object.keys(normalizeBotRunnerRequests(stateBeforePreviewOnlyContinuation.requests)).length;
16530
+ const previewRequestsAfter = Object.keys(normalizeBotRunnerRequests(stateAfterPreviewOnlyContinuation.requests)).length;
16531
+ const previewConsumedBefore = Object.keys(safeObject(stateBeforePreviewOnlyContinuation.consumedComments || stateBeforePreviewOnlyContinuation.consumed_comments)).length;
16532
+ const previewConsumedAfter = Object.keys(safeObject(stateAfterPreviewOnlyContinuation.consumedComments || stateAfterPreviewOnlyContinuation.consumed_comments)).length;
16533
+ push(
16534
+ "runner_continuation_preview_does_not_write_request_or_consumed_state",
16535
+ safeObject(previewOnlyRecoveryContext.botReplyContinuationContext).continuationLinkRecoverable === true
16536
+ && previewRequestsBefore === previewRequestsAfter
16537
+ && previewConsumedBefore === previewConsumedAfter,
16538
+ `recoverable=${String(safeObject(previewOnlyRecoveryContext.botReplyContinuationContext).continuationLinkRecoverable)} requests=${String(previewRequestsBefore)}->${String(previewRequestsAfter)} consumed=${String(previewConsumedBefore)}->${String(previewConsumedAfter)}`,
16539
+ );
16540
+ } finally {
16541
+ process.env.HOME = previousHome;
16542
+ process.env.USERPROFILE = previousUserProfile;
16543
+ try {
16544
+ fs.rmSync(provenanceTempRoot, { recursive: true, force: true });
16348
16545
  } catch {}
16349
16546
  }
16350
16547
  } catch (err) {
@@ -17447,6 +17644,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
17447
17644
  linkedRequest: {
17448
17645
  request_key: "req-1",
17449
17646
  selected_bot_usernames: ["alpha_bot"],
17647
+ next_expected_responders: ["alpha_bot"],
17450
17648
  },
17451
17649
  },
17452
17650
  },
@@ -17493,6 +17691,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
17493
17691
  linkedRequest: {
17494
17692
  request_key: "req-1",
17495
17693
  selected_bot_usernames: ["alpha_bot"],
17694
+ next_expected_responders: ["alpha_bot"],
17496
17695
  },
17497
17696
  },
17498
17697
  },
@@ -17529,6 +17728,69 @@ export async function runSelftestRunnerScenarios(push, deps) {
17529
17728
  push("runner_selected_record_execution_context_promotes_continuation_only_after_recovery", false, String(err?.message || err));
17530
17729
  }
17531
17730
 
17731
+ try {
17732
+ const preparation = await resolveRunnerPrecomputedSelectedRecordExecutionContext({
17733
+ selectedRecord: {
17734
+ id: "comment-continuation-no-explicit-handoff",
17735
+ parsedArchive: {
17736
+ kind: "bot_reply",
17737
+ chatID: "-100123",
17738
+ chatType: "supergroup",
17739
+ messageID: 3504,
17740
+ body: "foreign bot follow up",
17741
+ sender: "other bot",
17742
+ senderIsBot: true,
17743
+ botUsername: "@other_bot",
17744
+ mentionUsernames: [],
17745
+ },
17746
+ },
17747
+ recoveryContext: {
17748
+ selectedRecordKind: "bot_reply",
17749
+ botReplyContinuationContext: {
17750
+ continuationLinkRecoverable: true,
17751
+ continuationLinkResolution: {
17752
+ ok: true,
17753
+ linkedRequestKey: "req-no-explicit-handoff",
17754
+ linkedRequest: {
17755
+ request_key: "req-no-explicit-handoff",
17756
+ selected_bot_usernames: ["alpha_bot"],
17757
+ next_expected_responders: [],
17758
+ },
17759
+ },
17760
+ },
17761
+ },
17762
+ normalizedRoute: {
17763
+ provider: "telegram",
17764
+ triggerPolicy: {
17765
+ replyToBotMessages: true,
17766
+ },
17767
+ },
17768
+ routeKey: "telegram-monitor-alpha::project::telegram::monitor::dest::actor",
17769
+ routeState: {},
17770
+ fallbackRouteState: {},
17771
+ bot: {
17772
+ username: "alpha_bot",
17773
+ name: "alpha_bot",
17774
+ },
17775
+ executionPlan: {},
17776
+ pendingOrdered: [],
17777
+ currentBotSelector: "alpha_bot",
17778
+ routingExecutionDeps: {
17779
+ managedConversationBots: [],
17780
+ },
17781
+ });
17782
+ push(
17783
+ "runner_selected_record_execution_context_requires_explicit_handoff_for_bot_reply_continuation",
17784
+ preparation.kind === "skip"
17785
+ && String(preparation?.skipAction || "") === "adjudication_skipped"
17786
+ && String(preparation?.skipReason || "") === "bot_reply_not_explicitly_handed_off"
17787
+ && String(preparation?.skipTrigger || "") === "request_contract",
17788
+ `kind=${String(preparation?.kind || "(none)")} action=${String(preparation?.skipAction || "(none)")} reason=${String(preparation?.skipReason || "(none)")} trigger=${String(preparation?.skipTrigger || "(none)")}`,
17789
+ );
17790
+ } catch (err) {
17791
+ push("runner_selected_record_execution_context_requires_explicit_handoff_for_bot_reply_continuation", false, String(err?.message || err));
17792
+ }
17793
+
17532
17794
  try {
17533
17795
  const preparation = await resolveRunnerPrecomputedSelectedRecordExecutionContext({
17534
17796
  selectedRecord: {
@@ -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.276",
3
+ "version": "0.2.278",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [