metheus-governance-mcp-cli 0.2.294 → 0.2.296

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
@@ -10632,10 +10632,10 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10632
10632
  latestRunnerState.consumedComments || latestRunnerState.consumed_comments,
10633
10633
  );
10634
10634
  const requests = normalizeBotRunnerRequests(latestRunnerState.requests);
10635
- const commentsForPending = ensureArray(comments).filter((comment) => {
10636
- const normalizedComment = normalizeArchiveCommentRecord(comment, parseArchivedChatComment);
10637
- const commentID = String(normalizedComment.id || "").trim();
10638
- const parsed = safeObject(normalizedComment.parsedArchive);
10635
+ const commentsForPending = ensureArray(comments).filter((comment) => {
10636
+ const normalizedComment = normalizeArchiveCommentRecord(comment, parseArchivedChatComment);
10637
+ const commentID = String(normalizedComment.id || "").trim();
10638
+ const parsed = safeObject(normalizedComment.parsedArchive);
10639
10639
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
10640
10640
  if (commentID && safeObject(excludedComments)[commentID]) {
10641
10641
  return false;
@@ -10663,22 +10663,45 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10663
10663
  }
10664
10664
  const sessionMatch = findScopedConversationSessionState(latestRunnerState, normalizedRoute, conversationID);
10665
10665
  return sessionAllowsConversationResponder(sessionMatch.session, currentBotSelector);
10666
- }
10667
- return true;
10668
- });
10666
+ }
10667
+ return true;
10668
+ });
10669
+ const followupRequestsForPending = Object.values(requests).filter((entryRaw) => {
10670
+ const entry = safeObject(entryRaw);
10671
+ const nextExpectedResponders = ensureArray(entry.next_expected_responders)
10672
+ .map((value) => normalizeTelegramMentionUsername(value))
10673
+ .filter(Boolean);
10674
+ if (!nextExpectedResponders.includes(currentBotSelector)) {
10675
+ return false;
10676
+ }
10677
+ if (!isActiveRunnerRequestStatus(entry.status)) {
10678
+ return false;
10679
+ }
10680
+ if (
10681
+ String(entry.project_id || "").trim() !== String(normalizedRoute.projectID || "").trim()
10682
+ || String(entry.provider || "").trim() !== String(normalizedRoute.provider || "").trim()
10683
+ || String(entry.chat_id || "").trim() !== String(destination.chatID || "").trim()
10684
+ ) {
10685
+ return false;
10686
+ }
10687
+ const lastReplyEnvelope = safeObject(entry.last_reply_message_envelope);
10688
+ return intFromRawAllowZero(lastReplyEnvelope.message_id || lastReplyEnvelope.messageID, 0) > 0;
10689
+ });
10669
10690
  const pendingWork = selectRunnerPendingWork({
10670
10691
  comments: commentsForPending,
10671
10692
  importOutcome,
10672
10693
  refreshedState,
10673
- mode,
10694
+ mode,
10674
10695
  parseArchivedChatComment,
10675
10696
  pendingSelectionOptions: {
10676
10697
  maxPendingAgeMs: BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS,
10677
10698
  },
10678
- deps: {
10679
- applyPendingAgeSelection,
10680
- normalizeArchiveCommentRecord,
10681
- },
10699
+ deps: {
10700
+ applyPendingAgeSelection,
10701
+ normalizeArchiveCommentRecord,
10702
+ },
10703
+ followupRequests: followupRequestsForPending,
10704
+ currentBotSelector,
10682
10705
  });
10683
10706
  const pending = pendingWork.pending;
10684
10707
  if (pending.staleSkippedLatest) {
@@ -5,6 +5,7 @@ import {
5
5
  dedupeProcessableArchiveComments,
6
6
  isArchiveRecordAfterState,
7
7
  isInboundArchiveKind,
8
+ normalizeTelegramMessageEnvelope,
8
9
  selectPendingArchiveComments,
9
10
  } from "./runner-helpers.mjs";
10
11
  import {
@@ -13,6 +14,7 @@ import {
13
14
  } from "./runner-trigger.mjs";
14
15
  import {
15
16
  buildRunnerLocalInboundReceiptKey,
17
+ normalizeRunnerRecentLocalInboundReceiptMap,
16
18
  } from "./runner-local-inbound-receipts.mjs";
17
19
 
18
20
  function safeObject(value) {
@@ -130,6 +132,101 @@ function buildRunnerReceiptReplaySortTime(receiptRaw) {
130
132
  return Number.isFinite(receiptTime) ? receiptTime : 0;
131
133
  }
132
134
 
135
+ function extractReceiptMentionSelectors(text) {
136
+ const matches = Array.from(String(text || "").matchAll(/@([A-Za-z0-9_]+)/g));
137
+ return Array.from(new Set(matches.map((match) => normalizeMentionSelector(match[1] || "")).filter(Boolean)));
138
+ }
139
+
140
+ function buildRunnerReceiptReplayRouteState(routeStateRaw) {
141
+ const routeState = safeObject(routeStateRaw);
142
+ return {
143
+ ...routeState,
144
+ last_processed_comment_id: String(routeState.active_comment_id || "").trim()
145
+ || String(routeState.last_processed_comment_id || "").trim(),
146
+ last_processed_created_at: String(routeState.active_comment_created_at || "").trim()
147
+ || String(routeState.last_processed_created_at || "").trim(),
148
+ };
149
+ }
150
+
151
+ function buildRunnerReceiptBackedSyntheticRecord(receiptRaw, receiptKey) {
152
+ const receipt = safeObject(receiptRaw);
153
+ const chatID = String(receipt.chat_id || "").trim();
154
+ const messageID = intFromRawAllowZero(receipt.message_id, 0);
155
+ const occurredAt = firstNonEmptyString([
156
+ receipt.occurred_at,
157
+ receipt.occurredAt,
158
+ receipt.received_at,
159
+ receipt.receivedAt,
160
+ ]);
161
+ if (!chatID || !(messageID > 0) || !occurredAt) {
162
+ return null;
163
+ }
164
+ const body = String(receipt.body || receipt.text || "").trim();
165
+ const parsedArchive = {
166
+ kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
167
+ chatID,
168
+ messageID,
169
+ sender: String(receipt.sender || "").trim(),
170
+ senderID: String(receipt.sender_id || "").trim(),
171
+ senderIsBot: receipt.sender_is_bot === true,
172
+ username: String(receipt.sender_username || "").trim(),
173
+ mentionUsernames: extractReceiptMentionSelectors(body),
174
+ occurredAt,
175
+ canonicalHumanMessageKey: String(receipt.canonical_human_message_key || "").trim(),
176
+ sourceOrigin: firstNonEmptyString([receipt.receipt_origin, "local_telegram_inbound"]).toLowerCase(),
177
+ sourceRouteKey: String(receipt.receipt_route_key || "").trim(),
178
+ sourceBotUsername: normalizeMentionSelector(receipt.receipt_bot_username || ""),
179
+ body,
180
+ };
181
+ const messageThreadID = intFromRawAllowZero(receipt.message_thread_id, 0);
182
+ if (messageThreadID > 0) {
183
+ parsedArchive.messageThreadID = messageThreadID;
184
+ }
185
+ const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id, 0);
186
+ if (replyToMessageID > 0) {
187
+ parsedArchive.replyToMessageID = replyToMessageID;
188
+ }
189
+ const replyToUsername = String(receipt.reply_to_from_username || "").trim();
190
+ if (replyToUsername) {
191
+ parsedArchive.replyToUsername = replyToUsername;
192
+ }
193
+ if (receipt.reply_to_from_is_bot === true) {
194
+ parsedArchive.replyToSenderIsBot = true;
195
+ }
196
+ const chatType = String(receipt.chat_type || "").trim();
197
+ if (chatType) {
198
+ parsedArchive.chatType = chatType;
199
+ }
200
+ return {
201
+ id: `receipt-replay:${receiptKey}`,
202
+ body,
203
+ createdAt: occurredAt,
204
+ updatedAt: occurredAt,
205
+ sourceOccurredAt: occurredAt,
206
+ parsedArchive,
207
+ };
208
+ }
209
+
210
+ function findRunnerReceiptArchiveCollisionRecord(orderedComments, receiptRaw, expectedReceiptKey = "") {
211
+ const receipt = safeObject(receiptRaw);
212
+ const chatID = String(receipt.chat_id || "").trim();
213
+ const messageID = intFromRawAllowZero(receipt.message_id, 0);
214
+ if (!chatID || !(messageID > 0)) {
215
+ return null;
216
+ }
217
+ return ensureArray(orderedComments).find((recordRaw) => {
218
+ const record = safeObject(recordRaw);
219
+ const parsed = safeObject(record.parsedArchive);
220
+ if (
221
+ firstNonEmptyString([parsed.chatID, parsed.chatId]) !== chatID
222
+ || intFromRawAllowZero(parsed.messageID, 0) !== messageID
223
+ ) {
224
+ return false;
225
+ }
226
+ return buildRunnerArchiveSourceMessageKey(record) !== String(expectedReceiptKey || "").trim();
227
+ }) || null;
228
+ }
229
+
133
230
  function buildRunnerReceiptBackedReplayRecord(recordRaw, receiptRaw, pendingSelectionOptions = {}) {
134
231
  const record = safeObject(recordRaw);
135
232
  const receipt = safeObject(receiptRaw);
@@ -161,33 +258,75 @@ function buildRunnerReceiptBackedReplayRecord(recordRaw, receiptRaw, pendingSele
161
258
  function buildRunnerReceiptBackedPendingArchiveComments({
162
259
  orderedComments,
163
260
  currentPollLocalInboundReceipts,
261
+ routeStateLocalInboundReceipts,
262
+ routeState,
164
263
  existingPendingIDs,
165
264
  pendingSelectionOptions,
166
265
  }) {
167
266
  const pendingIDs = new Set(ensureArray(existingPendingIDs).map((value) => String(value || "").trim()).filter(Boolean));
168
267
  const latestReceiptsByKey = new Map();
169
- for (const receiptRaw of ensureArray(currentPollLocalInboundReceipts)) {
268
+ const registerReceipt = (receiptRaw, source) => {
170
269
  const receipt = safeObject(receiptRaw);
171
270
  const receiptKey = buildRunnerLocalInboundReceiptKey(receipt, {
172
271
  resolveTargetSelector: resolveRunnerLocalInboundReceiptTargetSelector,
173
272
  });
174
273
  if (!receiptKey) {
175
- continue;
274
+ return;
176
275
  }
177
- const previous = safeObject(latestReceiptsByKey.get(receiptKey));
178
- if (buildRunnerReceiptReplaySortTime(receipt) >= buildRunnerReceiptReplaySortTime(previous)) {
179
- latestReceiptsByKey.set(receiptKey, receipt);
276
+ const previousEntry = safeObject(latestReceiptsByKey.get(receiptKey));
277
+ const previousReceipt = safeObject(previousEntry.receipt);
278
+ const previousSource = String(previousEntry.source || "").trim();
279
+ const nextSortTime = buildRunnerReceiptReplaySortTime(receipt);
280
+ const previousSortTime = buildRunnerReceiptReplaySortTime(previousReceipt);
281
+ if (
282
+ nextSortTime > previousSortTime
283
+ || (
284
+ nextSortTime === previousSortTime
285
+ && source === "current_poll"
286
+ && previousSource !== "current_poll"
287
+ )
288
+ ) {
289
+ latestReceiptsByKey.set(receiptKey, { receipt, source });
180
290
  }
291
+ };
292
+ for (const receiptRaw of ensureArray(currentPollLocalInboundReceipts)) {
293
+ registerReceipt(receiptRaw, "current_poll");
294
+ }
295
+ for (const receiptRaw of Object.values(normalizeRunnerRecentLocalInboundReceiptMap(routeStateLocalInboundReceipts))) {
296
+ registerReceipt(receiptRaw, "route_state");
181
297
  }
298
+ const replayRouteState = buildRunnerReceiptReplayRouteState(routeState);
182
299
  const replayCandidates = [];
183
- for (const [receiptKey, receipt] of latestReceiptsByKey.entries()) {
300
+ for (const [receiptKey, receiptEntryRaw] of latestReceiptsByKey.entries()) {
301
+ const receiptEntry = safeObject(receiptEntryRaw);
302
+ const receipt = safeObject(receiptEntry.receipt);
303
+ const receiptSource = String(receiptEntry.source || "").trim();
184
304
  const matchedRecord = orderedComments.find((record) => buildRunnerArchiveSourceMessageKey(record) === receiptKey);
185
- const matchedID = String(safeObject(matchedRecord).id || "").trim();
305
+ const collisionRecord = matchedRecord
306
+ ? null
307
+ : findRunnerReceiptArchiveCollisionRecord(orderedComments, receipt, receiptKey);
308
+ const selectedRecord = matchedRecord || buildRunnerReceiptBackedSyntheticRecord(receipt, receiptKey);
309
+ if (
310
+ collisionRecord
311
+ || !selectedRecord
312
+ || (
313
+ !matchedRecord
314
+ && !String(safeObject(selectedRecord.parsedArchive).canonicalHumanMessageKey || "").trim()
315
+ && !String(safeObject(selectedRecord.parsedArchive).canonical_human_message_key || "").trim()
316
+ )
317
+ || (
318
+ receiptSource !== "current_poll"
319
+ && !isArchiveRecordAfterState(selectedRecord, replayRouteState)
320
+ )
321
+ ) {
322
+ continue;
323
+ }
324
+ const matchedID = String(safeObject(selectedRecord).id || "").trim();
186
325
  if (!matchedID || pendingIDs.has(matchedID)) {
187
326
  continue;
188
327
  }
189
328
  replayCandidates.push(buildRunnerReceiptBackedReplayRecord(
190
- matchedRecord,
329
+ selectedRecord,
191
330
  receipt,
192
331
  pendingSelectionOptions,
193
332
  ));
@@ -195,6 +334,175 @@ function buildRunnerReceiptBackedPendingArchiveComments({
195
334
  return replayCandidates.sort(compareArchiveCommentRecords);
196
335
  }
197
336
 
337
+ function buildRunnerRequestFollowupSyntheticRecord(requestRaw, currentBotSelector, pendingSelectionOptions = {}) {
338
+ const request = safeObject(requestRaw);
339
+ const replyEnvelope = safeObject(normalizeTelegramMessageEnvelope(request.last_reply_message_envelope));
340
+ const chatID = String(request.chat_id || replyEnvelope.chat_id || "").trim();
341
+ const messageID = intFromRawAllowZero(replyEnvelope.message_id, 0);
342
+ const occurredAt = firstNonEmptyString([
343
+ replyEnvelope.occurred_at,
344
+ request.updated_at,
345
+ request.claimed_at,
346
+ request.created_at,
347
+ ]);
348
+ if (!chatID || !(messageID > 0) || !occurredAt) {
349
+ return null;
350
+ }
351
+ const normalizedCurrentBotSelector = normalizeMentionSelector(currentBotSelector);
352
+ const senderSelector = normalizeMentionSelector(
353
+ replyEnvelope.sender_username
354
+ || replyEnvelope.bot_username
355
+ || replyEnvelope.username
356
+ || replyEnvelope.sender,
357
+ );
358
+ if (normalizedCurrentBotSelector && senderSelector && normalizedCurrentBotSelector === senderSelector) {
359
+ return null;
360
+ }
361
+ const participants = Array.from(new Set(
362
+ ensureArray(request.conversation_participants)
363
+ .map((value) => normalizeMentionSelector(value))
364
+ .filter(Boolean),
365
+ ));
366
+ const initialResponders = Array.from(new Set(
367
+ ensureArray(request.conversation_initial_responders)
368
+ .map((value) => normalizeMentionSelector(value))
369
+ .filter(Boolean),
370
+ ));
371
+ const allowedResponders = Array.from(new Set(
372
+ ensureArray(request.conversation_allowed_responders)
373
+ .map((value) => normalizeMentionSelector(value))
374
+ .filter(Boolean),
375
+ ));
376
+ const nextResponders = Array.from(new Set(
377
+ ensureArray(request.next_expected_responders)
378
+ .map((value) => normalizeMentionSelector(value))
379
+ .filter(Boolean),
380
+ ));
381
+ const executionTargets = Array.from(new Set(
382
+ ensureArray(request.execution_contract_targets)
383
+ .map((value) => normalizeMentionSelector(value))
384
+ .filter(Boolean),
385
+ ));
386
+ const normalizedExecutionContractType = String(
387
+ request.execution_contract_type || request.normalized_execution_contract_type || "",
388
+ ).trim().toLowerCase() || "direct_result";
389
+ const normalizedSummaryBotSelector = normalizeMentionSelector(request.conversation_summary_bot);
390
+ const executionContract = {
391
+ type: normalizedExecutionContractType,
392
+ actionable: request.execution_contract_actionable === true,
393
+ assignments: executionTargets.map((targetBot) => ({
394
+ target_bot: targetBot,
395
+ })),
396
+ ...(normalizedSummaryBotSelector ? { summary_bot: normalizedSummaryBotSelector } : {}),
397
+ ...(nextResponders.length > 0 ? { next_responders: nextResponders } : {}),
398
+ };
399
+ const replyOccurredAtMs = Date.parse(occurredAt);
400
+ const maxPendingAgeMs = intFromRawAllowZero(safeObject(pendingSelectionOptions).maxPendingAgeMs, 0);
401
+ const staleAfterAt = maxPendingAgeMs > 0 && Number.isFinite(replyOccurredAtMs)
402
+ ? new Date(replyOccurredAtMs + maxPendingAgeMs).toISOString()
403
+ : "";
404
+ const syntheticID = `request-followup:${String(request.request_key || "").trim()}:${messageID}:${normalizedCurrentBotSelector || "route"}`;
405
+ return {
406
+ id: syntheticID,
407
+ body: String(replyEnvelope.body || "").trim(),
408
+ createdAt: occurredAt,
409
+ updatedAt: occurredAt,
410
+ sourceOccurredAt: occurredAt,
411
+ ...(staleAfterAt ? { staleAfterAt } : {}),
412
+ replayTriggeredByRequestFollowup: true,
413
+ parsedArchive: {
414
+ kind: "bot_reply",
415
+ chatID,
416
+ messageID,
417
+ ...(intFromRawAllowZero(replyEnvelope.message_thread_id, 0) > 0
418
+ ? { messageThreadID: intFromRawAllowZero(replyEnvelope.message_thread_id, 0) }
419
+ : {}),
420
+ ...(intFromRawAllowZero(replyEnvelope.reply_to_message_id, 0) > 0
421
+ ? { replyToMessageID: intFromRawAllowZero(replyEnvelope.reply_to_message_id, 0) }
422
+ : {}),
423
+ sender: String(replyEnvelope.sender || (senderSelector ? `@${senderSelector}` : "@bot")).trim(),
424
+ senderID: String(replyEnvelope.sender_id || "").trim(),
425
+ senderIsBot: true,
426
+ username: senderSelector,
427
+ botUsername: senderSelector,
428
+ mentionUsernames: uniqueOrdered([
429
+ ...ensureArray(replyEnvelope.mention_usernames)
430
+ .map((value) => normalizeMentionSelector(value))
431
+ .filter(Boolean),
432
+ ...nextResponders,
433
+ ]),
434
+ occurredAt,
435
+ body: String(replyEnvelope.body || "").trim(),
436
+ conversationID: String(request.conversation_id || "").trim(),
437
+ conversationMode: "public_multi_bot",
438
+ conversationStage: "bot_reply",
439
+ conversationIntentMode: String(request.conversation_intent_mode || "").trim(),
440
+ conversationAllowBotToBot: request.conversation_allow_bot_to_bot === true,
441
+ conversationLeadBotUsername: normalizeMentionSelector(request.conversation_lead_bot),
442
+ conversationSummaryBotUsername: normalizedSummaryBotSelector,
443
+ conversationTargetBotUsername: nextResponders.length === 1 ? nextResponders[0] : "",
444
+ conversationParticipants: participants,
445
+ conversationInitialResponders: initialResponders,
446
+ conversationAllowedResponders: allowedResponders,
447
+ conversationReplyExpectation: String(request.conversation_reply_expectation || "").trim().toLowerCase(),
448
+ executionContract,
449
+ executionContractType: normalizedExecutionContractType,
450
+ executionContractActionable: request.execution_contract_actionable === true,
451
+ executionContractAssignments: executionTargets.map((targetBot) => ({
452
+ targetBot,
453
+ })),
454
+ executionContractSummaryBot: normalizedSummaryBotSelector,
455
+ executionContractNextResponders: nextResponders,
456
+ sourceOrigin: "request_followup_replay",
457
+ sourceRouteKey: String(request.claimed_by_route || "").trim(),
458
+ sourceBotUsername: senderSelector,
459
+ canonicalHumanMessageKey: String(request.canonical_human_message_key || "").trim(),
460
+ },
461
+ };
462
+ }
463
+
464
+ function buildRunnerRequestFollowupPendingArchiveComments({
465
+ orderedComments,
466
+ routeState,
467
+ existingPendingIDs,
468
+ followupRequests,
469
+ currentBotSelector,
470
+ pendingSelectionOptions,
471
+ }) {
472
+ const pendingIDs = new Set(ensureArray(existingPendingIDs).map((value) => String(value || "").trim()).filter(Boolean));
473
+ const replayCandidates = [];
474
+ for (const requestRaw of ensureArray(followupRequests)) {
475
+ const request = safeObject(requestRaw);
476
+ const syntheticRecord = buildRunnerRequestFollowupSyntheticRecord(
477
+ request,
478
+ currentBotSelector,
479
+ pendingSelectionOptions,
480
+ );
481
+ if (!syntheticRecord) {
482
+ continue;
483
+ }
484
+ if (pendingIDs.has(String(syntheticRecord.id || "").trim())) {
485
+ continue;
486
+ }
487
+ const parsedSynthetic = safeObject(syntheticRecord.parsedArchive);
488
+ const archiveCollision = ensureArray(orderedComments).some((recordRaw) => {
489
+ const parsed = safeObject(safeObject(recordRaw).parsedArchive);
490
+ return String(parsed.kind || "").trim().toLowerCase() === "bot_reply"
491
+ && String(parsed.chatID || parsed.chatId || "").trim() === String(parsedSynthetic.chatID || "").trim()
492
+ && intFromRawAllowZero(parsed.messageID || parsed.messageId, 0) === intFromRawAllowZero(parsedSynthetic.messageID, 0);
493
+ });
494
+ if (archiveCollision) {
495
+ continue;
496
+ }
497
+ if (!isArchiveRecordAfterState(syntheticRecord, routeState)) {
498
+ continue;
499
+ }
500
+ pendingIDs.add(String(syntheticRecord.id || "").trim());
501
+ replayCandidates.push(syntheticRecord);
502
+ }
503
+ return replayCandidates.sort(compareArchiveCommentRecords);
504
+ }
505
+
198
506
  function buildContextSpeakerType(parsedArchiveRaw) {
199
507
  const parsed = safeObject(parsedArchiveRaw);
200
508
  const kind = String(parsed.kind || "").trim();
@@ -944,6 +1252,8 @@ export function selectRunnerPendingWork({
944
1252
  parseArchivedChatComment,
945
1253
  deps,
946
1254
  pendingSelectionOptions,
1255
+ followupRequests = [],
1256
+ currentBotSelector = "",
947
1257
  }) {
948
1258
  const normalizeArchiveCommentRecord = requireDependency(deps, "normalizeArchiveCommentRecord");
949
1259
  const applyPendingAgeSelection = requireDependency(deps, "applyPendingAgeSelection");
@@ -1007,14 +1317,31 @@ export function selectRunnerPendingWork({
1007
1317
  const receiptBackedPending = buildRunnerReceiptBackedPendingArchiveComments({
1008
1318
  orderedComments,
1009
1319
  currentPollLocalInboundReceipts: ensureArray(importOutcome?.currentPollLocalInboundReceipts),
1320
+ routeStateLocalInboundReceipts: safeObject(refreshedState).recent_local_inbound_receipts,
1321
+ routeState: refreshedState,
1010
1322
  existingPendingIDs: ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
1011
1323
  pendingSelectionOptions,
1012
1324
  });
1013
- const finalPending = ensureArray(safeObject(pending).pending).length === 0 && receiptBackedPending.length > 0
1325
+ const requestFollowupPending = buildRunnerRequestFollowupPendingArchiveComments({
1326
+ orderedComments,
1327
+ routeState: refreshedState,
1328
+ existingPendingIDs: [
1329
+ ...ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
1330
+ ...ensureArray(receiptBackedPending).map((record) => String(safeObject(record).id || "").trim()),
1331
+ ],
1332
+ followupRequests,
1333
+ currentBotSelector,
1334
+ pendingSelectionOptions,
1335
+ });
1336
+ const replayFallbackPending = [
1337
+ ...ensureArray(receiptBackedPending),
1338
+ ...ensureArray(requestFollowupPending),
1339
+ ].sort(compareArchiveCommentRecords);
1340
+ const finalPending = ensureArray(safeObject(pending).pending).length === 0 && replayFallbackPending.length > 0
1014
1341
  ? applyPendingAgeSelection({
1015
1342
  ...safeObject(pending),
1016
1343
  shouldPrime: false,
1017
- pending: receiptBackedPending,
1344
+ pending: replayFallbackPending,
1018
1345
  }, pendingSelectionOptions)
1019
1346
  : pending;
1020
1347
  return {
@@ -1022,6 +1349,7 @@ export function selectRunnerPendingWork({
1022
1349
  inboundComments,
1023
1350
  importedRecords,
1024
1351
  receiptBackedPending,
1352
+ requestFollowupPending,
1025
1353
  pending: finalPending,
1026
1354
  };
1027
1355
  }
@@ -22062,6 +22062,425 @@ export async function runSelftestRunnerScenarios(push, deps) {
22062
22062
  push("runner_entrypoint_receipt_replay_skips_foreign_bot_archive_collision", false, String(err?.message || err));
22063
22063
  }
22064
22064
 
22065
+ try {
22066
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22067
+ comments: [
22068
+ {
22069
+ id: "comment-earlier-cursor-1",
22070
+ createdAt: "2026-04-06T00:00:00.000Z",
22071
+ updatedAt: "2026-04-06T00:00:00.000Z",
22072
+ body: "@RyoAI_bot earlier",
22073
+ parsedArchive: {
22074
+ kind: "telegram_message",
22075
+ messageID: 485,
22076
+ chatID: "-100999",
22077
+ chatType: "supergroup",
22078
+ sender: "tester",
22079
+ senderIsBot: false,
22080
+ body: "@RyoAI_bot earlier",
22081
+ },
22082
+ },
22083
+ ],
22084
+ importOutcome: {
22085
+ importedCommentIDs: [],
22086
+ importedComments: [],
22087
+ currentPollLocalInboundReceipts: [],
22088
+ },
22089
+ refreshedState: {
22090
+ last_processed_comment_id: "comment-earlier-cursor-1",
22091
+ last_processed_created_at: "2026-04-06T00:00:00.000Z",
22092
+ recent_local_inbound_receipts: {
22093
+ "-100999:486": {
22094
+ chat_id: "-100999",
22095
+ message_id: 486,
22096
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 하이",
22097
+ receipt_bot_username: "ryoai_bot",
22098
+ occurred_at: "2026-04-06T00:00:06.000Z",
22099
+ canonical_human_message_key: "shared-human-key-486",
22100
+ },
22101
+ },
22102
+ },
22103
+ mode: "continue",
22104
+ parseArchivedChatComment,
22105
+ deps: {
22106
+ normalizeArchiveCommentRecord: (record) => ({
22107
+ id: String(record?.id || "").trim(),
22108
+ body: String(record?.body || "").trim(),
22109
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22110
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22111
+ parsedArchive: safeObject(record?.parsedArchive),
22112
+ }),
22113
+ applyPendingAgeSelection: (selection) => selection,
22114
+ },
22115
+ pendingSelectionOptions: {},
22116
+ });
22117
+ push(
22118
+ "runner_entrypoint_replays_route_state_local_receipt_after_shared_human_fanout",
22119
+ ensureArray(pendingWork.receiptBackedPending).length === 1
22120
+ && String(safeObject(ensureArray(pendingWork.receiptBackedPending)[0]).id || "").startsWith("receipt-replay:human:shared-human-key-486")
22121
+ && safeObject(safeObject(ensureArray(pendingWork.receiptBackedPending)[0]).parsedArchive).messageID === 486
22122
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 1
22123
+ && String(safeObject(ensureArray(safeObject(pendingWork.pending).pending)[0]).id || "").startsWith("receipt-replay:human:shared-human-key-486"),
22124
+ `replay=${ensureArray(pendingWork.receiptBackedPending).map((item) => String(safeObject(item).id || "")).join(",") || "(none)"} pending=${ensureArray(safeObject(pendingWork.pending).pending).map((item) => String(safeObject(item).id || "")).join(",") || "(none)"}`,
22125
+ );
22126
+ } catch (err) {
22127
+ push("runner_entrypoint_replays_route_state_local_receipt_after_shared_human_fanout", false, String(err?.message || err));
22128
+ }
22129
+
22130
+ try {
22131
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22132
+ comments: [],
22133
+ importOutcome: {
22134
+ importedCommentIDs: [],
22135
+ importedComments: [],
22136
+ currentPollLocalInboundReceipts: [],
22137
+ },
22138
+ refreshedState: {
22139
+ last_processed_comment_id: "comment-shared-human-opening-processed",
22140
+ last_processed_created_at: "2026-04-06T00:00:07.000Z",
22141
+ recent_local_inbound_receipts: {
22142
+ "-100999:487": {
22143
+ chat_id: "-100999",
22144
+ message_id: 487,
22145
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 하이",
22146
+ receipt_bot_username: "ryoai3_bot",
22147
+ occurred_at: "2026-04-06T00:00:06.000Z",
22148
+ canonical_human_message_key: "shared-human-key-487",
22149
+ },
22150
+ },
22151
+ },
22152
+ mode: "continue",
22153
+ parseArchivedChatComment,
22154
+ deps: {
22155
+ normalizeArchiveCommentRecord: (record) => ({
22156
+ id: String(record?.id || "").trim(),
22157
+ body: String(record?.body || "").trim(),
22158
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22159
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22160
+ parsedArchive: safeObject(record?.parsedArchive),
22161
+ }),
22162
+ applyPendingAgeSelection: (selection) => selection,
22163
+ },
22164
+ pendingSelectionOptions: {},
22165
+ });
22166
+ push(
22167
+ "runner_entrypoint_does_not_replay_route_state_local_receipt_after_route_cursor",
22168
+ ensureArray(pendingWork.receiptBackedPending).length === 0
22169
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 0,
22170
+ `replay=${ensureArray(pendingWork.receiptBackedPending).length} pending=${ensureArray(safeObject(pendingWork.pending).pending).length}`,
22171
+ );
22172
+ } catch (err) {
22173
+ push("runner_entrypoint_does_not_replay_route_state_local_receipt_after_route_cursor", false, String(err?.message || err));
22174
+ }
22175
+
22176
+ try {
22177
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22178
+ comments: [],
22179
+ importOutcome: {
22180
+ importedCommentIDs: [],
22181
+ importedComments: [],
22182
+ currentPollLocalInboundReceipts: [],
22183
+ },
22184
+ refreshedState: {
22185
+ last_processed_comment_id: "comment-before-followup-1",
22186
+ last_processed_created_at: "2026-04-06T00:00:00.000Z",
22187
+ },
22188
+ mode: "continue",
22189
+ parseArchivedChatComment,
22190
+ deps: {
22191
+ normalizeArchiveCommentRecord: (record) => ({
22192
+ id: String(record?.id || "").trim(),
22193
+ body: String(record?.body || "").trim(),
22194
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22195
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22196
+ parsedArchive: safeObject(record?.parsedArchive),
22197
+ }),
22198
+ applyPendingAgeSelection: (selection) => selection,
22199
+ },
22200
+ pendingSelectionOptions: {},
22201
+ followupRequests: [
22202
+ {
22203
+ request_key: "request-followup-1",
22204
+ status: "running",
22205
+ project_id: "project-followup-1",
22206
+ provider: "telegram",
22207
+ chat_id: "-100999",
22208
+ conversation_id: "conversation-followup-1",
22209
+ conversation_intent_mode: "multi_bot_direct",
22210
+ conversation_participants: ["ryoai_bot", "ryoai2_bot"],
22211
+ conversation_initial_responders: ["ryoai_bot", "ryoai2_bot"],
22212
+ conversation_allowed_responders: ["ryoai_bot", "ryoai2_bot"],
22213
+ execution_contract_type: "direct_result",
22214
+ execution_contract_actionable: true,
22215
+ execution_contract_targets: ["ryoai2_bot"],
22216
+ next_expected_responders: ["ryoai_bot"],
22217
+ updated_at: "2026-04-06T00:10:00.000Z",
22218
+ last_reply_message_envelope: {
22219
+ chat_id: "-100999",
22220
+ message_id: 498,
22221
+ reply_to_message_id: 497,
22222
+ body: "하이!",
22223
+ sender: "@RyoAI2_bot",
22224
+ sender_username: "ryoai2_bot",
22225
+ sender_is_bot: true,
22226
+ occurred_at: "2026-04-06T00:10:00.000Z",
22227
+ canonical_human_message_key: "shared-human-key-497",
22228
+ },
22229
+ },
22230
+ ],
22231
+ currentBotSelector: "ryoai_bot",
22232
+ });
22233
+ push(
22234
+ "runner_entrypoint_replays_request_followup_for_next_expected_responder",
22235
+ ensureArray(pendingWork.requestFollowupPending).length === 1
22236
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 1
22237
+ && String(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).id || "").startsWith("request-followup:request-followup-1:498:ryoai_bot")
22238
+ && safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).kind === "bot_reply"
22239
+ && String(safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).conversationMode || "") === "public_multi_bot"
22240
+ && String(safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).conversationIntentMode || "") === "multi_bot_direct"
22241
+ && ensureArray(safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).executionContractNextResponders).includes("ryoai_bot")
22242
+ && ensureArray(safeObject(safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).executionContract).next_responders).includes("ryoai_bot"),
22243
+ `replay=${ensureArray(pendingWork.requestFollowupPending).map((item) => String(safeObject(item).id || "")).join(",") || "(none)"} pending=${ensureArray(safeObject(pendingWork.pending).pending).map((item) => String(safeObject(item).id || "")).join(",") || "(none)"}`,
22244
+ );
22245
+ } catch (err) {
22246
+ push("runner_entrypoint_replays_request_followup_for_next_expected_responder", false, String(err?.message || err));
22247
+ }
22248
+
22249
+ try {
22250
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22251
+ comments: [],
22252
+ importOutcome: {
22253
+ importedCommentIDs: [],
22254
+ importedComments: [],
22255
+ currentPollLocalInboundReceipts: [],
22256
+ },
22257
+ refreshedState: {
22258
+ last_processed_comment_id: "request-followup:request-followup-2:499:ryoai_bot",
22259
+ last_processed_created_at: "2026-04-06T00:11:00.000Z",
22260
+ },
22261
+ mode: "continue",
22262
+ parseArchivedChatComment,
22263
+ deps: {
22264
+ normalizeArchiveCommentRecord: (record) => ({
22265
+ id: String(record?.id || "").trim(),
22266
+ body: String(record?.body || "").trim(),
22267
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22268
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22269
+ parsedArchive: safeObject(record?.parsedArchive),
22270
+ }),
22271
+ applyPendingAgeSelection: (selection) => selection,
22272
+ },
22273
+ pendingSelectionOptions: {},
22274
+ followupRequests: [
22275
+ {
22276
+ request_key: "request-followup-2",
22277
+ status: "running",
22278
+ project_id: "project-followup-1",
22279
+ provider: "telegram",
22280
+ chat_id: "-100999",
22281
+ conversation_id: "conversation-followup-2",
22282
+ conversation_intent_mode: "multi_bot_direct",
22283
+ conversation_participants: ["ryoai_bot", "ryoai2_bot"],
22284
+ conversation_initial_responders: ["ryoai_bot", "ryoai2_bot"],
22285
+ conversation_allowed_responders: ["ryoai_bot", "ryoai2_bot"],
22286
+ execution_contract_type: "direct_result",
22287
+ execution_contract_actionable: true,
22288
+ execution_contract_targets: ["ryoai2_bot"],
22289
+ next_expected_responders: ["ryoai_bot"],
22290
+ updated_at: "2026-04-06T00:11:00.000Z",
22291
+ last_reply_message_envelope: {
22292
+ chat_id: "-100999",
22293
+ message_id: 499,
22294
+ reply_to_message_id: 497,
22295
+ body: "하이!",
22296
+ sender: "@RyoAI2_bot",
22297
+ sender_username: "ryoai2_bot",
22298
+ sender_is_bot: true,
22299
+ occurred_at: "2026-04-06T00:11:00.000Z",
22300
+ canonical_human_message_key: "shared-human-key-499",
22301
+ },
22302
+ },
22303
+ ],
22304
+ currentBotSelector: "ryoai_bot",
22305
+ });
22306
+ push(
22307
+ "runner_entrypoint_does_not_replay_request_followup_after_route_cursor",
22308
+ ensureArray(pendingWork.requestFollowupPending).length === 0
22309
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 0,
22310
+ `replay=${ensureArray(pendingWork.requestFollowupPending).length} pending=${ensureArray(safeObject(pendingWork.pending).pending).length}`,
22311
+ );
22312
+ } catch (err) {
22313
+ push("runner_entrypoint_does_not_replay_request_followup_after_route_cursor", false, String(err?.message || err));
22314
+ }
22315
+
22316
+ try {
22317
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22318
+ comments: [],
22319
+ importOutcome: {
22320
+ importedCommentIDs: [],
22321
+ importedComments: [],
22322
+ currentPollLocalInboundReceipts: [],
22323
+ },
22324
+ refreshedState: {
22325
+ last_processed_comment_id: "comment-before-followup-3",
22326
+ last_processed_created_at: "2026-04-06T00:12:00.000Z",
22327
+ },
22328
+ mode: "continue",
22329
+ parseArchivedChatComment,
22330
+ deps: {
22331
+ normalizeArchiveCommentRecord: (record) => ({
22332
+ id: String(record?.id || "").trim(),
22333
+ body: String(record?.body || "").trim(),
22334
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22335
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22336
+ parsedArchive: safeObject(record?.parsedArchive),
22337
+ }),
22338
+ applyPendingAgeSelection: (selection) => selection,
22339
+ },
22340
+ pendingSelectionOptions: {},
22341
+ followupRequests: [
22342
+ {
22343
+ request_key: "request-followup-3",
22344
+ status: "running",
22345
+ project_id: selftestProjectID,
22346
+ provider: "telegram",
22347
+ chat_id: "-100126",
22348
+ conversation_id: "conversation-followup-3",
22349
+ conversation_intent_mode: "multi_bot_direct",
22350
+ conversation_allow_bot_to_bot: true,
22351
+ conversation_participants: ["ryoai2_bot", "ryoai3_bot"],
22352
+ conversation_initial_responders: ["ryoai2_bot", "ryoai3_bot"],
22353
+ conversation_allowed_responders: ["ryoai2_bot", "ryoai3_bot"],
22354
+ execution_contract_type: "direct_result",
22355
+ execution_contract_actionable: true,
22356
+ execution_contract_targets: ["ryoai2_bot", "ryoai3_bot"],
22357
+ next_expected_responders: ["ryoai3_bot"],
22358
+ updated_at: "2026-04-06T00:12:00.000Z",
22359
+ last_reply_message_envelope: {
22360
+ chat_id: "-100126",
22361
+ message_id: 515,
22362
+ reply_to_message_id: 514,
22363
+ body: "안녕하세요, RyoAI2_bot입니다.",
22364
+ sender: "@RyoAI2_bot",
22365
+ sender_username: "ryoai2_bot",
22366
+ sender_is_bot: true,
22367
+ occurred_at: "2026-04-06T00:12:00.000Z",
22368
+ canonical_human_message_key: "shared-human-key-515",
22369
+ },
22370
+ },
22371
+ ],
22372
+ currentBotSelector: "ryoai3_bot",
22373
+ });
22374
+ let aiCalls = 0;
22375
+ const deliveredConversation = [];
22376
+ const processed = await processRunnerSelectedRecord({
22377
+ routeKey: "runner-followup-public-multi-bot-peer-key",
22378
+ normalizedRoute: normalizeRunnerRoute({
22379
+ name: "telegram-monitor-runner-followup-public-multi-bot-peer",
22380
+ project_id: selftestProjectID,
22381
+ provider: "telegram",
22382
+ role: "monitor",
22383
+ role_profile: "monitor",
22384
+ destination_id: "dest-1",
22385
+ destination_label: "Main Room",
22386
+ server_bot_name: "RyoAI3_bot",
22387
+ server_bot_id: "bot-peer-2",
22388
+ trigger_policy: {
22389
+ mentions_only: true,
22390
+ direct_messages: true,
22391
+ reply_to_bot_messages: true,
22392
+ },
22393
+ archive_policy: {
22394
+ mirror_replies: true,
22395
+ dedupe_inbound: true,
22396
+ dedupe_outbound: true,
22397
+ skip_bot_messages: true,
22398
+ },
22399
+ dry_run_delivery: true,
22400
+ }),
22401
+ selectedRecord: ensureArray(pendingWork.requestFollowupPending)[0],
22402
+ pendingOrdered: ensureArray(pendingWork.pending?.pending),
22403
+ bot: {
22404
+ id: "bot-peer-2",
22405
+ name: "RyoAI3_bot",
22406
+ username: "RyoAI3_bot",
22407
+ role: "monitor",
22408
+ provider: "telegram",
22409
+ },
22410
+ destination: {
22411
+ id: "dest-1",
22412
+ label: "Main Room",
22413
+ provider: "telegram",
22414
+ chatID: "-100126",
22415
+ },
22416
+ archiveThread: {
22417
+ threadID: "thread-1",
22418
+ workItemID: "work-item-1",
22419
+ },
22420
+ executionPlan: {
22421
+ mode: "role_profile",
22422
+ roleProfileName: "monitor",
22423
+ roleProfile: {
22424
+ client: "sample",
22425
+ model: "",
22426
+ permissionMode: "read_only",
22427
+ reasoningEffort: "low",
22428
+ },
22429
+ workspaceDir: path.join(os.tmpdir(), "metheus-runner-selftest-followup-public-multi-bot-peer"),
22430
+ workspaceSource: "selftest",
22431
+ usedCommandFallback: false,
22432
+ },
22433
+ runtime: {
22434
+ baseURL: "https://example.test",
22435
+ token: "selftest-token",
22436
+ timeoutSeconds: 30,
22437
+ actor: { user_id: "user-1" },
22438
+ },
22439
+ deps: {
22440
+ saveRunnerRouteState: () => {},
22441
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
22442
+ runRunnerAIExecution: async () => {
22443
+ aiCalls += 1;
22444
+ return {
22445
+ skip: false,
22446
+ reply: "안녕하세요, RyoAI3_bot입니다.",
22447
+ };
22448
+ },
22449
+ performLocalBotDelivery: async ({ archiveConversation, archiveConversationContext, archiveExecutionContract }) => {
22450
+ deliveredConversation.push(buildSelftestArchiveConversation({
22451
+ archiveConversation,
22452
+ archiveConversationContext,
22453
+ archiveExecutionContract,
22454
+ }));
22455
+ return {
22456
+ delivery: { dryRun: true, body: {} },
22457
+ archive: {},
22458
+ };
22459
+ },
22460
+ serializeRunnerTriggerPolicy: (value) => value,
22461
+ serializeRunnerArchivePolicy: (value) => value,
22462
+ buildRunnerExecutionDeps: () => ({}),
22463
+ buildRunnerDeliveryDeps: () => ({}),
22464
+ buildRunnerRuntimeDeps: () => ({}),
22465
+ resolveConversationPeerBots: () => [
22466
+ { id: "bot-peer-1", name: "RyoAI2_bot" },
22467
+ { id: "bot-peer-2", name: "RyoAI3_bot" },
22468
+ ],
22469
+ },
22470
+ });
22471
+ push(
22472
+ "runner_request_followup_public_multi_bot_continuation_is_authorized",
22473
+ processed.kind === "replied"
22474
+ && aiCalls === 1
22475
+ && String(deliveredConversation[0]?.mode || "") === "public_multi_bot"
22476
+ && String(deliveredConversation[0]?.intentMode || "") === "multi_bot_direct"
22477
+ && ensureArray(deliveredConversation[0]?.allowedResponderSelectors).includes("ryoai3_bot"),
22478
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} mode=${String(deliveredConversation[0]?.mode || "(none)")} intent=${String(deliveredConversation[0]?.intentMode || "(none)")} reason=${String(processed.skippedRecord?.reason || "(none)")}`,
22479
+ );
22480
+ } catch (err) {
22481
+ push("runner_request_followup_public_multi_bot_continuation_is_authorized", false, String(err?.message || err));
22482
+ }
22483
+
22065
22484
  try {
22066
22485
  const deliveryContext = await prepareLocalBotDeliveryContext({
22067
22486
  siteBaseURL: "https://example.test",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.294",
3
+ "version": "0.2.296",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [