metheus-governance-mcp-cli 0.2.294 → 0.2.295

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");
181
294
  }
295
+ for (const receiptRaw of Object.values(normalizeRunnerRecentLocalInboundReceiptMap(routeStateLocalInboundReceipts))) {
296
+ registerReceipt(receiptRaw, "route_state");
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,156 @@ 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 replyOccurredAtMs = Date.parse(occurredAt);
387
+ const maxPendingAgeMs = intFromRawAllowZero(safeObject(pendingSelectionOptions).maxPendingAgeMs, 0);
388
+ const staleAfterAt = maxPendingAgeMs > 0 && Number.isFinite(replyOccurredAtMs)
389
+ ? new Date(replyOccurredAtMs + maxPendingAgeMs).toISOString()
390
+ : "";
391
+ const syntheticID = `request-followup:${String(request.request_key || "").trim()}:${messageID}:${normalizedCurrentBotSelector || "route"}`;
392
+ return {
393
+ id: syntheticID,
394
+ body: String(replyEnvelope.body || "").trim(),
395
+ createdAt: occurredAt,
396
+ updatedAt: occurredAt,
397
+ sourceOccurredAt: occurredAt,
398
+ ...(staleAfterAt ? { staleAfterAt } : {}),
399
+ replayTriggeredByRequestFollowup: true,
400
+ parsedArchive: {
401
+ kind: "bot_reply",
402
+ chatID,
403
+ messageID,
404
+ ...(intFromRawAllowZero(replyEnvelope.message_thread_id, 0) > 0
405
+ ? { messageThreadID: intFromRawAllowZero(replyEnvelope.message_thread_id, 0) }
406
+ : {}),
407
+ ...(intFromRawAllowZero(replyEnvelope.reply_to_message_id, 0) > 0
408
+ ? { replyToMessageID: intFromRawAllowZero(replyEnvelope.reply_to_message_id, 0) }
409
+ : {}),
410
+ sender: String(replyEnvelope.sender || (senderSelector ? `@${senderSelector}` : "@bot")).trim(),
411
+ senderID: String(replyEnvelope.sender_id || "").trim(),
412
+ senderIsBot: true,
413
+ username: senderSelector,
414
+ botUsername: senderSelector,
415
+ mentionUsernames: ensureArray(replyEnvelope.mention_usernames),
416
+ occurredAt,
417
+ body: String(replyEnvelope.body || "").trim(),
418
+ conversationID: String(request.conversation_id || "").trim(),
419
+ conversationMode: String(request.conversation_intent_mode || "").trim(),
420
+ conversationStage: "bot_reply",
421
+ conversationAllowBotToBot: request.conversation_allow_bot_to_bot === true,
422
+ conversationLeadBotUsername: normalizeMentionSelector(request.conversation_lead_bot),
423
+ conversationSummaryBotUsername: normalizeMentionSelector(request.conversation_summary_bot),
424
+ conversationTargetBotUsername: nextResponders.length === 1 ? nextResponders[0] : "",
425
+ conversationParticipants: participants,
426
+ conversationInitialResponders: initialResponders,
427
+ conversationAllowedResponders: allowedResponders,
428
+ conversationReplyExpectation: String(request.conversation_reply_expectation || "").trim().toLowerCase(),
429
+ executionContractType: String(
430
+ request.execution_contract_type || request.normalized_execution_contract_type || "",
431
+ ).trim().toLowerCase(),
432
+ executionContractActionable: request.execution_contract_actionable === true,
433
+ executionContractAssignments: executionTargets.map((targetBot) => ({
434
+ targetBot,
435
+ })),
436
+ executionContractNextResponders: nextResponders,
437
+ sourceOrigin: "request_followup_replay",
438
+ sourceRouteKey: String(request.claimed_by_route || "").trim(),
439
+ sourceBotUsername: senderSelector,
440
+ canonicalHumanMessageKey: String(request.canonical_human_message_key || "").trim(),
441
+ },
442
+ };
443
+ }
444
+
445
+ function buildRunnerRequestFollowupPendingArchiveComments({
446
+ orderedComments,
447
+ routeState,
448
+ existingPendingIDs,
449
+ followupRequests,
450
+ currentBotSelector,
451
+ pendingSelectionOptions,
452
+ }) {
453
+ const pendingIDs = new Set(ensureArray(existingPendingIDs).map((value) => String(value || "").trim()).filter(Boolean));
454
+ const replayCandidates = [];
455
+ for (const requestRaw of ensureArray(followupRequests)) {
456
+ const request = safeObject(requestRaw);
457
+ const syntheticRecord = buildRunnerRequestFollowupSyntheticRecord(
458
+ request,
459
+ currentBotSelector,
460
+ pendingSelectionOptions,
461
+ );
462
+ if (!syntheticRecord) {
463
+ continue;
464
+ }
465
+ if (pendingIDs.has(String(syntheticRecord.id || "").trim())) {
466
+ continue;
467
+ }
468
+ const parsedSynthetic = safeObject(syntheticRecord.parsedArchive);
469
+ const archiveCollision = ensureArray(orderedComments).some((recordRaw) => {
470
+ const parsed = safeObject(safeObject(recordRaw).parsedArchive);
471
+ return String(parsed.kind || "").trim().toLowerCase() === "bot_reply"
472
+ && String(parsed.chatID || parsed.chatId || "").trim() === String(parsedSynthetic.chatID || "").trim()
473
+ && intFromRawAllowZero(parsed.messageID || parsed.messageId, 0) === intFromRawAllowZero(parsedSynthetic.messageID, 0);
474
+ });
475
+ if (archiveCollision) {
476
+ continue;
477
+ }
478
+ if (!isArchiveRecordAfterState(syntheticRecord, routeState)) {
479
+ continue;
480
+ }
481
+ pendingIDs.add(String(syntheticRecord.id || "").trim());
482
+ replayCandidates.push(syntheticRecord);
483
+ }
484
+ return replayCandidates.sort(compareArchiveCommentRecords);
485
+ }
486
+
198
487
  function buildContextSpeakerType(parsedArchiveRaw) {
199
488
  const parsed = safeObject(parsedArchiveRaw);
200
489
  const kind = String(parsed.kind || "").trim();
@@ -944,6 +1233,8 @@ export function selectRunnerPendingWork({
944
1233
  parseArchivedChatComment,
945
1234
  deps,
946
1235
  pendingSelectionOptions,
1236
+ followupRequests = [],
1237
+ currentBotSelector = "",
947
1238
  }) {
948
1239
  const normalizeArchiveCommentRecord = requireDependency(deps, "normalizeArchiveCommentRecord");
949
1240
  const applyPendingAgeSelection = requireDependency(deps, "applyPendingAgeSelection");
@@ -1007,14 +1298,31 @@ export function selectRunnerPendingWork({
1007
1298
  const receiptBackedPending = buildRunnerReceiptBackedPendingArchiveComments({
1008
1299
  orderedComments,
1009
1300
  currentPollLocalInboundReceipts: ensureArray(importOutcome?.currentPollLocalInboundReceipts),
1301
+ routeStateLocalInboundReceipts: safeObject(refreshedState).recent_local_inbound_receipts,
1302
+ routeState: refreshedState,
1010
1303
  existingPendingIDs: ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
1011
1304
  pendingSelectionOptions,
1012
1305
  });
1013
- const finalPending = ensureArray(safeObject(pending).pending).length === 0 && receiptBackedPending.length > 0
1306
+ const requestFollowupPending = buildRunnerRequestFollowupPendingArchiveComments({
1307
+ orderedComments,
1308
+ routeState: refreshedState,
1309
+ existingPendingIDs: [
1310
+ ...ensureArray(safeObject(pending).pending).map((record) => String(safeObject(record).id || "").trim()),
1311
+ ...ensureArray(receiptBackedPending).map((record) => String(safeObject(record).id || "").trim()),
1312
+ ],
1313
+ followupRequests,
1314
+ currentBotSelector,
1315
+ pendingSelectionOptions,
1316
+ });
1317
+ const replayFallbackPending = [
1318
+ ...ensureArray(receiptBackedPending),
1319
+ ...ensureArray(requestFollowupPending),
1320
+ ].sort(compareArchiveCommentRecords);
1321
+ const finalPending = ensureArray(safeObject(pending).pending).length === 0 && replayFallbackPending.length > 0
1014
1322
  ? applyPendingAgeSelection({
1015
1323
  ...safeObject(pending),
1016
1324
  shouldPrime: false,
1017
- pending: receiptBackedPending,
1325
+ pending: replayFallbackPending,
1018
1326
  }, pendingSelectionOptions)
1019
1327
  : pending;
1020
1328
  return {
@@ -1022,6 +1330,7 @@ export function selectRunnerPendingWork({
1022
1330
  inboundComments,
1023
1331
  importedRecords,
1024
1332
  receiptBackedPending,
1333
+ requestFollowupPending,
1025
1334
  pending: finalPending,
1026
1335
  };
1027
1336
  }
@@ -22062,6 +22062,254 @@ 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
+ && ensureArray(safeObject(safeObject(ensureArray(pendingWork.requestFollowupPending)[0]).parsedArchive).executionContractNextResponders).includes("ryoai_bot"),
22240
+ `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)"}`,
22241
+ );
22242
+ } catch (err) {
22243
+ push("runner_entrypoint_replays_request_followup_for_next_expected_responder", false, String(err?.message || err));
22244
+ }
22245
+
22246
+ try {
22247
+ const pendingWork = selectRunnerPendingWorkEntrypoint({
22248
+ comments: [],
22249
+ importOutcome: {
22250
+ importedCommentIDs: [],
22251
+ importedComments: [],
22252
+ currentPollLocalInboundReceipts: [],
22253
+ },
22254
+ refreshedState: {
22255
+ last_processed_comment_id: "request-followup:request-followup-2:499:ryoai_bot",
22256
+ last_processed_created_at: "2026-04-06T00:11:00.000Z",
22257
+ },
22258
+ mode: "continue",
22259
+ parseArchivedChatComment,
22260
+ deps: {
22261
+ normalizeArchiveCommentRecord: (record) => ({
22262
+ id: String(record?.id || "").trim(),
22263
+ body: String(record?.body || "").trim(),
22264
+ createdAt: String(record?.created_at || record?.createdAt || record?.updated_at || record?.updatedAt || "").trim(),
22265
+ updatedAt: String(record?.updated_at || record?.updatedAt || "").trim(),
22266
+ parsedArchive: safeObject(record?.parsedArchive),
22267
+ }),
22268
+ applyPendingAgeSelection: (selection) => selection,
22269
+ },
22270
+ pendingSelectionOptions: {},
22271
+ followupRequests: [
22272
+ {
22273
+ request_key: "request-followup-2",
22274
+ status: "running",
22275
+ project_id: "project-followup-1",
22276
+ provider: "telegram",
22277
+ chat_id: "-100999",
22278
+ conversation_id: "conversation-followup-2",
22279
+ conversation_intent_mode: "multi_bot_direct",
22280
+ conversation_participants: ["ryoai_bot", "ryoai2_bot"],
22281
+ conversation_initial_responders: ["ryoai_bot", "ryoai2_bot"],
22282
+ conversation_allowed_responders: ["ryoai_bot", "ryoai2_bot"],
22283
+ execution_contract_type: "direct_result",
22284
+ execution_contract_actionable: true,
22285
+ execution_contract_targets: ["ryoai2_bot"],
22286
+ next_expected_responders: ["ryoai_bot"],
22287
+ updated_at: "2026-04-06T00:11:00.000Z",
22288
+ last_reply_message_envelope: {
22289
+ chat_id: "-100999",
22290
+ message_id: 499,
22291
+ reply_to_message_id: 497,
22292
+ body: "하이!",
22293
+ sender: "@RyoAI2_bot",
22294
+ sender_username: "ryoai2_bot",
22295
+ sender_is_bot: true,
22296
+ occurred_at: "2026-04-06T00:11:00.000Z",
22297
+ canonical_human_message_key: "shared-human-key-499",
22298
+ },
22299
+ },
22300
+ ],
22301
+ currentBotSelector: "ryoai_bot",
22302
+ });
22303
+ push(
22304
+ "runner_entrypoint_does_not_replay_request_followup_after_route_cursor",
22305
+ ensureArray(pendingWork.requestFollowupPending).length === 0
22306
+ && ensureArray(safeObject(pendingWork.pending).pending).length === 0,
22307
+ `replay=${ensureArray(pendingWork.requestFollowupPending).length} pending=${ensureArray(safeObject(pendingWork.pending).pending).length}`,
22308
+ );
22309
+ } catch (err) {
22310
+ push("runner_entrypoint_does_not_replay_request_followup_after_route_cursor", false, String(err?.message || err));
22311
+ }
22312
+
22065
22313
  try {
22066
22314
  const deliveryContext = await prepareLocalBotDeliveryContext({
22067
22315
  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.295",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [