metheus-governance-mcp-cli 0.2.261 → 0.2.263

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
@@ -13302,21 +13302,27 @@ async function maybeDeliverRunnerExecutionFailureAfterRecord({
13302
13302
 
13303
13303
  async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, processed) {
13304
13304
  if (String(deferredExecution?.requestKey || "").trim()) {
13305
- const resolvedIntentType = String(
13306
- safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
13307
- ).trim();
13308
- markRunnerRequestLifecycle({
13309
- normalizedRoute: deferredExecution.normalizedRoute,
13310
- requestKey: deferredExecution.requestKey,
13311
- selectedRecord: deferredExecution.selectedRecord,
13312
- routeKey: deferredExecution.routeKey,
13313
- outcome: processed.kind === "delivery_failed"
13314
- ? "delivery_failed_after_generation"
13315
- : String(processed.result?.outcome || "replied").trim().toLowerCase(),
13316
- conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
13317
- conversationParticipants: ensureArray(processed.result?.conversation_participants),
13318
- conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
13319
- allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
13305
+ const resolvedIntentType = String(
13306
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
13307
+ ).trim();
13308
+ const normalizedDeferredOutcome = processed.kind === "delivery_failed"
13309
+ ? "delivery_failed_after_generation"
13310
+ : processed.kind === "skipped"
13311
+ ? "skipped"
13312
+ : String(processed.result?.outcome || "replied").trim().toLowerCase();
13313
+ markRunnerRequestLifecycle({
13314
+ normalizedRoute: deferredExecution.normalizedRoute,
13315
+ requestKey: deferredExecution.requestKey,
13316
+ selectedRecord: deferredExecution.selectedRecord,
13317
+ routeKey: deferredExecution.routeKey,
13318
+ outcome: normalizedDeferredOutcome,
13319
+ closedReason: processed.kind === "skipped"
13320
+ ? String(processed.skippedRecord?.reason || processed.result?.detail || "skipped").trim()
13321
+ : "",
13322
+ conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
13323
+ conversationParticipants: ensureArray(processed.result?.conversation_participants),
13324
+ conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
13325
+ allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
13320
13326
  conversationLeadBot: String(processed.result?.conversation_lead_bot || "").trim(),
13321
13327
  conversationSummaryBot: String(processed.result?.conversation_summary_bot || "").trim(),
13322
13328
  conversationAllowBotToBot: processed.result?.conversation_allow_bot_to_bot === true,
@@ -13349,17 +13355,17 @@ async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, proce
13349
13355
  lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
13350
13356
  lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
13351
13357
  replyToMessageID: intFromRawAllowZero(processed.result?.reply_to_message_id, 0),
13352
- replyFallbackUsed: processed.result?.reply_fallback_used === true,
13353
- authoritativeSourceMessageEnvelope: safeObject(
13354
- safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13355
- ),
13356
- });
13357
- if (processed.kind !== "delivery_failed") {
13358
- await ensureRunnerRootWorkItemForRequest({
13359
- normalizedRoute: deferredExecution.normalizedRoute,
13360
- routeKey: deferredExecution.routeKey,
13361
- selectedRecord: deferredExecution.selectedRecord,
13362
- runtime: deferredExecution.runtime,
13358
+ replyFallbackUsed: processed.result?.reply_fallback_used === true,
13359
+ authoritativeSourceMessageEnvelope: safeObject(
13360
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13361
+ ),
13362
+ });
13363
+ if (processed.kind !== "delivery_failed" && processed.kind !== "skipped") {
13364
+ await ensureRunnerRootWorkItemForRequest({
13365
+ normalizedRoute: deferredExecution.normalizedRoute,
13366
+ routeKey: deferredExecution.routeKey,
13367
+ selectedRecord: deferredExecution.selectedRecord,
13368
+ runtime: deferredExecution.runtime,
13363
13369
  requestKey: deferredExecution.requestKey,
13364
13370
  });
13365
13371
  }
@@ -187,6 +187,17 @@ function assertLocalTelegramReplyAnchor({
187
187
  }
188
188
  }
189
189
 
190
+ function buildLocalDeliveryError(message, code, extra = {}) {
191
+ const error = new Error(message);
192
+ error.code = String(code || "").trim();
193
+ Object.assign(error, safeObject(extra));
194
+ return error;
195
+ }
196
+
197
+ function isTelegramReplyTargetMissingError(detail) {
198
+ return /message to be replied not found/i.test(String(detail || ""));
199
+ }
200
+
190
201
  function throwLocalDeliveryFailure({
191
202
  provider,
192
203
  delivery,
@@ -198,6 +209,17 @@ function throwLocalDeliveryFailure({
198
209
  const errorDetail =
199
210
  String(responseJSON.description || responseJSON.error || JSON.stringify(delivery.body || "")).trim()
200
211
  || `${normalizedProvider} api status ${delivery.statusCode}`;
212
+ if (normalizedProvider === "telegram" && isTelegramReplyTargetMissingError(errorDetail)) {
213
+ throw buildLocalDeliveryError(
214
+ `local ${normalizedProvider} delivery skipped stale reply anchor (${errorDetail})`,
215
+ "TELEGRAM_STALE_REPLY_ANCHOR",
216
+ {
217
+ staleReplyAnchor: true,
218
+ provider: normalizedProvider,
219
+ detail: errorDetail,
220
+ },
221
+ );
222
+ }
201
223
  if (delivery.statusCode === 401 || /unauthorized|invalid_auth/i.test(errorDetail)) {
202
224
  throw new Error(
203
225
  `local ${normalizedProvider} delivery failed (Unauthorized). Check ${String(providerEnv.tokenKey || providerEnvConfig(normalizedProvider).tokenKey)} in ${providerEnv.filePath}`,
@@ -7362,6 +7362,91 @@ export async function processRunnerSelectedRecord({
7362
7362
  });
7363
7363
  } catch (err) {
7364
7364
  const transportError = String(err?.message || err).trim() || "delivery_failed";
7365
+ const staleReplyAnchor = err?.staleReplyAnchor === true
7366
+ || String(err?.code || "").trim() === "TELEGRAM_STALE_REPLY_ANCHOR";
7367
+ if (staleReplyAnchor) {
7368
+ saveRunnerRouteState(
7369
+ routeKey,
7370
+ buildRunnerRouteStateFromComment(selectedRecord, {
7371
+ last_action: "stale_reply_anchor_skipped",
7372
+ last_reason: transportError,
7373
+ last_trigger: String(effectiveTriggerDecision.trigger || "").trim(),
7374
+ last_conversation_id: String(effectiveConversationContext?.id || "").trim(),
7375
+ last_conversation_stage: String(effectiveConversationContext?.stage || "").trim(),
7376
+ last_contract_validation_status: String(responseContractValidation.status || "").trim(),
7377
+ last_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
7378
+ last_contract_validation_targets: ensureArray(responseContractValidation.targets),
7379
+ last_assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
7380
+ last_assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
7381
+ last_speaker_bot_username: normalizeMentionSelector(bot?.username || bot?.name),
7382
+ last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
7383
+ ...responderStatePatch,
7384
+ ...intentStatePatch,
7385
+ ...visibilityStatePatch,
7386
+ }),
7387
+ );
7388
+ return {
7389
+ kind: "skipped",
7390
+ skippedRecord: {
7391
+ id: selectedRecord.id,
7392
+ reason: "stale_reply_anchor",
7393
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
7394
+ diagnosticType: "delivery",
7395
+ },
7396
+ result: {
7397
+ route_key: routeKey,
7398
+ route_name: normalizedRoute.name,
7399
+ outcome: "skipped",
7400
+ detail: `skipped stale reply anchor (${transportError})`,
7401
+ thread_id: archiveThread.threadID,
7402
+ comment_id: selectedRecord.id,
7403
+ trigger_kind: String(effectiveTriggerDecision.trigger || "").trim(),
7404
+ conversation_id: String(effectiveConversationContext?.id || "").trim(),
7405
+ conversation_stage: String(effectiveConversationContext?.stage || "").trim(),
7406
+ conversation_intent_mode: String(effectiveConversationContext?.intentMode || "").trim(),
7407
+ conversation_lead_bot: String(effectiveConversationContext?.leadBotUsername || "").trim(),
7408
+ conversation_summary_bot: String(effectiveConversationContext?.summaryBotUsername || "").trim(),
7409
+ conversation_participants: ensureArray(effectiveConversationContext?.participantSelectors),
7410
+ conversation_initial_responders: ensureArray(effectiveConversationContext?.initialResponderSelectors),
7411
+ conversation_allowed_responders: ensureArray(effectiveConversationContext?.allowedResponderSelectors),
7412
+ conversation_allow_bot_to_bot: effectiveConversationContext?.allowBotToBot === true,
7413
+ conversation_reply_expectation: String(effectiveConversationContext?.replyExpectation || "").trim(),
7414
+ execution_contract_type: String(executionContract?.type || "").trim(),
7415
+ execution_contract_actionable: executionContract?.actionable === true,
7416
+ execution_contract_targets: ensureArray(executionContract?.assignments).map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean),
7417
+ next_expected_responders: collectExecutionContractNextResponders(executionContract),
7418
+ normalized_execution_contract_type: normalizedExecutionContractType,
7419
+ normalized_execution_contract_targets: normalizedExecutionTargets,
7420
+ normalized_execution_next_responders: normalizedExecutionNextResponders,
7421
+ artifact_validation: String(artifactValidation.status || "").trim() || "none",
7422
+ artifact_paths: summarizeValidatedArtifactPaths(artifactValidation),
7423
+ ai_reply_generated: true,
7424
+ ai_reply_generated_at: aiReplyGeneratedAt,
7425
+ ai_reply_preview: aiReplyPreview,
7426
+ source_message_envelope: sourceMessageEnvelope,
7427
+ attempted_delivery_envelope: attemptedDeliveryEnvelope,
7428
+ visibility_status: String(humanInboundVisibility.visibilityStatus || "").trim(),
7429
+ visibility_reason: String(humanInboundVisibility.visibilityReason || "").trim(),
7430
+ visibility_source: String(humanInboundVisibility.visibilitySource || "").trim(),
7431
+ reply_to_message_id: replyToMessageID,
7432
+ reply_message_thread_id: replyMessageThreadID,
7433
+ delivery_status: "skipped_stale_reply_anchor",
7434
+ archive_status: "not_attempted",
7435
+ transport_error: transportError,
7436
+ archive_error: "",
7437
+ response_contract_validation_status: String(responseContractValidation.status || "").trim(),
7438
+ response_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
7439
+ response_contract_validation_targets: ensureArray(responseContractValidation.targets),
7440
+ assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
7441
+ assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
7442
+ assignment_validation_modes: ensureArray(assignmentExecutionValidation.assignmentModes),
7443
+ reply_chars: String(sanitizedReplyText || "").length,
7444
+ execution_mode: effectiveExecutionPlan.mode,
7445
+ role_profile: effectiveExecutionPlan.roleProfileName,
7446
+ executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
7447
+ },
7448
+ };
7449
+ }
7365
7450
  saveRunnerRouteState(
7366
7451
  routeKey,
7367
7452
  buildRunnerRouteStateFromComment(selectedRecord, {
@@ -51,6 +51,38 @@ function normalizeMentionSelector(rawValue) {
51
51
  return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
52
52
  }
53
53
 
54
+ function buildRunnerRouteOwnershipKey(routeRaw) {
55
+ const route = safeObject(routeRaw);
56
+ const name = String(route.name || "").trim() || "-";
57
+ const projectID = String(route.projectID || route.project_id || "").trim() || "-";
58
+ const provider = String(route.provider || "").trim().toLowerCase() || "-";
59
+ const role = String(route.role || route.bot_role || route.botRole || "").trim().toLowerCase() || "-";
60
+ const botID = String(
61
+ route.botID
62
+ || route.bot_id
63
+ || route.server_bot_id
64
+ || route.serverBotID
65
+ || "",
66
+ ).trim();
67
+ const botName = String(
68
+ route.botName
69
+ || route.bot_name
70
+ || route.server_bot_name
71
+ || route.serverBotName
72
+ || "",
73
+ ).trim();
74
+ const destinationID = String(route.destinationID || route.destination_id || "").trim();
75
+ const destinationLabel = String(route.destinationLabel || route.destination_label || "").trim();
76
+ return [
77
+ name,
78
+ projectID,
79
+ provider,
80
+ role,
81
+ botID || botName || "-",
82
+ destinationID || destinationLabel || "-",
83
+ ].join("::");
84
+ }
85
+
54
86
  function normalizeRunnerSharedInboxKey({ provider, bot, route }) {
55
87
  const normalizedProvider = String(provider || route?.provider || "").trim().toLowerCase() || "telegram";
56
88
  const botSelector = firstNonEmptyString([
@@ -98,6 +130,130 @@ function buildManagedConversationBotSelectors(managedConversationBots) {
98
130
  );
99
131
  }
100
132
 
133
+ function buildRunnerLocalInboundOwnerMap({
134
+ routeKey,
135
+ route,
136
+ bot,
137
+ managedConversationBots,
138
+ }) {
139
+ const owners = new Map();
140
+ const appendOwner = (selectorRaw, ownerRaw = {}) => {
141
+ const selector = normalizeMentionSelector(selectorRaw);
142
+ const ownerRouteKey = String(
143
+ ownerRaw.routeKey
144
+ || ownerRaw.route_key
145
+ || buildRunnerRouteOwnershipKey(ownerRaw.route),
146
+ ).trim();
147
+ const ownerBotUsername = normalizeMentionSelector(
148
+ ownerRaw.botUsername
149
+ || ownerRaw.bot_username
150
+ || ownerRaw.username
151
+ || safeObject(ownerRaw.bot).username
152
+ || safeObject(ownerRaw.bot).name
153
+ || safeObject(ownerRaw.route).botName
154
+ || safeObject(ownerRaw.route).bot_name
155
+ || safeObject(ownerRaw.route).serverBotName
156
+ || safeObject(ownerRaw.route).server_bot_name
157
+ || selector,
158
+ );
159
+ if (!selector || !ownerRouteKey) {
160
+ return;
161
+ }
162
+ owners.set(selector, {
163
+ selector,
164
+ routeKey: ownerRouteKey,
165
+ botUsername: ownerBotUsername || selector,
166
+ });
167
+ };
168
+
169
+ appendOwner(
170
+ bot?.username
171
+ || bot?.name
172
+ || route?.botName
173
+ || route?.bot_name
174
+ || route?.serverBotName
175
+ || route?.server_bot_name,
176
+ {
177
+ routeKey,
178
+ botUsername:
179
+ bot?.username
180
+ || bot?.name
181
+ || route?.botName
182
+ || route?.bot_name
183
+ || route?.serverBotName
184
+ || route?.server_bot_name,
185
+ },
186
+ );
187
+
188
+ for (const entryRaw of ensureArray(managedConversationBots)) {
189
+ const entry = safeObject(entryRaw);
190
+ appendOwner(
191
+ entry.username
192
+ || entry.display_name
193
+ || safeObject(entry.bot).username
194
+ || safeObject(entry.bot).name
195
+ || safeObject(entry.route).botName
196
+ || safeObject(entry.route).bot_name
197
+ || safeObject(entry.route).serverBotName
198
+ || safeObject(entry.route).server_bot_name,
199
+ entry,
200
+ );
201
+ }
202
+
203
+ return owners;
204
+ }
205
+
206
+ function resolveRunnerLocalInboundArtifactOwners({
207
+ update,
208
+ routeKey,
209
+ route,
210
+ bot,
211
+ managedConversationBots,
212
+ }) {
213
+ const normalizedUpdate = safeObject(update);
214
+ const ownersBySelector = buildRunnerLocalInboundOwnerMap({
215
+ routeKey,
216
+ route,
217
+ bot,
218
+ managedConversationBots,
219
+ });
220
+ const currentBotSelector = normalizeMentionSelector(
221
+ bot?.username
222
+ || bot?.name
223
+ || route?.botName
224
+ || route?.bot_name
225
+ || route?.serverBotName
226
+ || route?.server_bot_name,
227
+ );
228
+ const currentOwner = currentBotSelector ? ownersBySelector.get(currentBotSelector) : null;
229
+
230
+ const explicitMentions = Array.from(new Set(
231
+ ensureArray(normalizedUpdate.mentionUsernames)
232
+ .map((value) => normalizeMentionSelector(value))
233
+ .filter(Boolean),
234
+ ));
235
+ if (explicitMentions.length > 0) {
236
+ const explicitOwners = explicitMentions
237
+ .map((selector) => ownersBySelector.get(selector))
238
+ .filter((owner) => owner && owner.routeKey);
239
+ if (explicitOwners.length > 0) {
240
+ return explicitOwners;
241
+ }
242
+ return [];
243
+ }
244
+
245
+ const replyTargetSelector = normalizeMentionSelector(normalizedUpdate.replyToFromUsername);
246
+ if (
247
+ normalizedUpdate.replyToFromIsBot === true
248
+ && replyTargetSelector
249
+ && ownersBySelector.has(replyTargetSelector)
250
+ ) {
251
+ return [ownersBySelector.get(replyTargetSelector)];
252
+ }
253
+
254
+ return currentOwner ? [currentOwner] : [];
255
+ }
256
+
101
257
  function managedConversationBotTargetsCurrentRoute({
102
258
  update,
103
259
  bot,
@@ -327,49 +483,72 @@ function buildRunnerLocalInboundEnvelopeFromReceipt(rawReceipt) {
327
483
  });
328
484
  }
329
485
 
330
- function buildRunnerLocalInboundArtifacts(updates, routeKey, bot, destination) {
331
- const currentBotSelector = normalizeMentionSelector(
332
- bot?.username
333
- || bot?.name
334
- || "",
335
- );
486
+ function buildRunnerLocalInboundArtifacts(updates, routeKey, route, bot, destination, managedConversationBots = []) {
336
487
  const normalizedRouteKey = String(routeKey || "").trim();
337
488
  const destinationChatID = String(destination?.chatID || "").trim();
338
489
  return ensureArray(updates)
339
490
  .filter((update) => String(update.chatID || "").trim() === destinationChatID)
340
491
  .filter((update) => String(update.text || "").trim())
341
- .map((update) => {
342
- const chatID = String(update.chatID || "").trim();
343
- const messageID = intFromRawAllowZero(update.messageID, 0);
344
- const receiptKey = buildRunnerRecentLocalInboundReceiptKey(chatID, messageID);
345
- const receiptEntry = normalizeRunnerRecentLocalInboundReceipt({
346
- update_id: intFromRawAllowZero(update.updateID, 0),
347
- chat_id: chatID,
348
- chat_type: update.chatType,
349
- chat_title: update.chatTitle,
350
- message_id: messageID,
351
- message_thread_id: intFromRawAllowZero(update.messageThreadID, 0),
352
- reply_to_message_id: intFromRawAllowZero(update.replyToMessageID, 0),
353
- kind: update.fromIsBot ? "bot_reply" : "telegram_message",
354
- sender_id: update.fromID,
355
- sender: update.fromName,
356
- sender_username: update.fromUsername,
357
- sender_is_bot: update.fromIsBot === true,
358
- body: update.text,
359
- occurred_at: update.occurredAt,
360
- receipt_origin: "local_telegram_inbound",
361
- receipt_route_key: normalizedRouteKey,
362
- receipt_bot_username: currentBotSelector,
363
- received_at: firstNonEmptyString([update.occurredAt, new Date().toISOString()]),
364
- }, receiptKey);
365
- return { receiptEntry };
492
+ .flatMap((update) => {
493
+ const owners = resolveRunnerLocalInboundArtifactOwners({
494
+ update,
495
+ routeKey: normalizedRouteKey,
496
+ route,
497
+ bot,
498
+ managedConversationBots,
499
+ });
500
+ return owners.map((owner) => {
501
+ const chatID = String(update.chatID || "").trim();
502
+ const messageID = intFromRawAllowZero(update.messageID, 0);
503
+ const receiptKey = buildRunnerRecentLocalInboundReceiptKey(chatID, messageID);
504
+ const receiptEntry = normalizeRunnerRecentLocalInboundReceipt({
505
+ update_id: intFromRawAllowZero(update.updateID, 0),
506
+ chat_id: chatID,
507
+ chat_type: update.chatType,
508
+ chat_title: update.chatTitle,
509
+ message_id: messageID,
510
+ message_thread_id: intFromRawAllowZero(update.messageThreadID, 0),
511
+ reply_to_message_id: intFromRawAllowZero(update.replyToMessageID, 0),
512
+ kind: update.fromIsBot ? "bot_reply" : "telegram_message",
513
+ sender_id: update.fromID,
514
+ sender: update.fromName,
515
+ sender_username: update.fromUsername,
516
+ sender_is_bot: update.fromIsBot === true,
517
+ body: update.text,
518
+ occurred_at: update.occurredAt,
519
+ receipt_origin: "local_telegram_inbound",
520
+ receipt_route_key: owner.routeKey,
521
+ receipt_bot_username: owner.botUsername,
522
+ received_at: firstNonEmptyString([update.occurredAt, new Date().toISOString()]),
523
+ }, receiptKey);
524
+ return {
525
+ ownerRouteKey: String(owner.routeKey || "").trim(),
526
+ receiptEntry,
527
+ };
528
+ });
366
529
  })
367
530
  .filter((artifact) => {
368
531
  const normalizedEntry = ensureArray(safeObject(artifact).receiptEntry);
369
- return normalizedEntry.length === 2;
532
+ return normalizedEntry.length === 2 && String(safeObject(artifact).ownerRouteKey || "").trim();
370
533
  });
371
534
  }
372
535
 
536
+ function groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts) {
537
+ const grouped = new Map();
538
+ for (const artifactRaw of ensureArray(localInboundArtifacts)) {
539
+ const artifact = safeObject(artifactRaw);
540
+ const ownerRouteKey = String(artifact.ownerRouteKey || "").trim();
541
+ const normalizedEntry = ensureArray(artifact.receiptEntry);
542
+ if (!ownerRouteKey || normalizedEntry.length !== 2) {
543
+ continue;
544
+ }
545
+ const routeArtifacts = grouped.get(ownerRouteKey) || [];
546
+ routeArtifacts.push({ receiptEntry: normalizedEntry });
547
+ grouped.set(ownerRouteKey, routeArtifacts);
548
+ }
549
+ return Object.fromEntries(grouped.entries());
550
+ }
551
+
373
552
  function buildRunnerRecentLocalInboundEnvelopes(routeStateRaw, recentLocalInboundReceipts) {
374
553
  const routeState = safeObject(routeStateRaw);
375
554
  const relevantEnvelopes = Object.values(safeObject(recentLocalInboundReceipts))
@@ -904,12 +1083,15 @@ export async function archiveLocalTelegramMessagesForRoute({
904
1083
  const localInboundArtifacts = buildRunnerLocalInboundArtifacts(
905
1084
  updates,
906
1085
  routeKey,
1086
+ route,
907
1087
  bot,
908
1088
  destination,
1089
+ managedConversationBots,
909
1090
  );
1091
+ const localInboundArtifactsByRoute = groupRunnerLocalInboundArtifactsByRoute(localInboundArtifacts);
910
1092
  const recentLocalInboundReceipts = buildRunnerRecentLocalInboundReceipts(
911
1093
  routeState,
912
- localInboundArtifacts,
1094
+ ensureArray(localInboundArtifactsByRoute[routeKey]),
913
1095
  );
914
1096
  const recentLocalInboundEnvelopes = buildRunnerRecentLocalInboundEnvelopes(
915
1097
  routeState,
@@ -926,13 +1108,36 @@ export async function archiveLocalTelegramMessagesForRoute({
926
1108
  if (sharedInboxPatch && saveBotRunnerState) {
927
1109
  saveBotRunnerState(sharedInboxPatch);
928
1110
  }
929
- saveRunnerRouteState(
1111
+ const inputRouteKeys = Array.from(new Set([
930
1112
  routeKey,
931
- buildRunnerRouteInputStatePatch({
932
- recentLocalInboundEnvelopes,
933
- recentLocalInboundReceipts,
934
- }),
935
- );
1113
+ ...Object.keys(safeObject(localInboundArtifactsByRoute)),
1114
+ ]));
1115
+ for (const targetRouteKey of inputRouteKeys) {
1116
+ const fullState = loadBotRunnerState ? safeObject(loadBotRunnerState()) : {};
1117
+ const targetRouteState = safeObject(
1118
+ safeObject(fullState.routes)[targetRouteKey]
1119
+ || (targetRouteKey === routeKey ? routeState : {}),
1120
+ );
1121
+ const targetRecentLocalInboundReceipts = targetRouteKey === routeKey
1122
+ ? recentLocalInboundReceipts
1123
+ : buildRunnerRecentLocalInboundReceipts(
1124
+ targetRouteState,
1125
+ ensureArray(localInboundArtifactsByRoute[targetRouteKey]),
1126
+ );
1127
+ const targetRecentLocalInboundEnvelopes = targetRouteKey === routeKey
1128
+ ? recentLocalInboundEnvelopes
1129
+ : buildRunnerRecentLocalInboundEnvelopes(
1130
+ targetRouteState,
1131
+ targetRecentLocalInboundReceipts,
1132
+ );
1133
+ saveRunnerRouteState(
1134
+ targetRouteKey,
1135
+ buildRunnerRouteInputStatePatch({
1136
+ recentLocalInboundEnvelopes: targetRecentLocalInboundEnvelopes,
1137
+ recentLocalInboundReceipts: targetRecentLocalInboundReceipts,
1138
+ }),
1139
+ );
1140
+ }
936
1141
  saveRunnerRouteState(
937
1142
  routeKey,
938
1143
  buildRunnerRoutePollingStatePatch({
@@ -13246,14 +13246,139 @@ export async function runSelftestRunnerScenarios(push, deps) {
13246
13246
  && /ECONNRESET/i.test(String(processed.result?.transport_error || "")),
13247
13247
  `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_generated=${String(processed.result?.ai_reply_generated || false)} delivery=${String(processed.result?.delivery_status || "(none)")} transport=${String(processed.result?.transport_error || "(none)")}`,
13248
13248
  );
13249
- } catch (err) {
13250
- push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
13251
- }
13252
-
13249
+ } catch (err) {
13250
+ push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
13251
+ }
13252
+
13253
+ try {
13254
+ const processed = await processRunnerSelectedRecord({
13255
+ routeKey: "delivery-stale-reply-anchor-key",
13256
+ normalizedRoute: normalizeRunnerRoute({
13257
+ name: "telegram-monitor-delivery-stale-reply-anchor",
13258
+ project_id: selftestProjectID,
13259
+ provider: "telegram",
13260
+ role: "monitor",
13261
+ role_profile: "monitor",
13262
+ destination_id: "dest-1",
13263
+ destination_label: "Main Room",
13264
+ server_bot_name: "RyoAI_bot",
13265
+ server_bot_id: "bot-1",
13266
+ trigger_policy: {
13267
+ mentions_only: true,
13268
+ direct_messages: true,
13269
+ reply_to_bot_messages: true,
13270
+ },
13271
+ archive_policy: {
13272
+ mirror_replies: true,
13273
+ dedupe_inbound: true,
13274
+ dedupe_outbound: true,
13275
+ skip_bot_messages: true,
13276
+ },
13277
+ dry_run_delivery: false,
13278
+ }),
13279
+ selectedRecord: {
13280
+ id: "comment-delivery-stale-reply-anchor",
13281
+ createdAt: "2026-03-17T00:00:22.000Z",
13282
+ parsedArchive: {
13283
+ kind: "telegram_message",
13284
+ chatID: "-100123",
13285
+ chatType: "supergroup",
13286
+ body: "@RyoAI_bot hi",
13287
+ messageID: 127,
13288
+ sender: "human",
13289
+ senderIsBot: false,
13290
+ mentionUsernames: ["ryoai_bot"],
13291
+ },
13292
+ },
13293
+ pendingOrdered: [],
13294
+ bot: {
13295
+ id: "bot-1",
13296
+ name: "RyoAI_bot",
13297
+ username: "RyoAI_bot",
13298
+ role: "monitor",
13299
+ provider: "telegram",
13300
+ },
13301
+ destination: {
13302
+ id: "dest-1",
13303
+ label: "Main Room",
13304
+ provider: "telegram",
13305
+ chatID: "-100123",
13306
+ },
13307
+ archiveThread: {
13308
+ threadID: "thread-1",
13309
+ workItemID: "work-item-1",
13310
+ },
13311
+ executionPlan: {
13312
+ mode: "role_profile",
13313
+ roleProfileName: "monitor",
13314
+ roleProfile: {
13315
+ client: "sample",
13316
+ model: "",
13317
+ permissionMode: "read_only",
13318
+ reasoningEffort: "low",
13319
+ },
13320
+ workspaceDir: "",
13321
+ workspaceSource: "selftest",
13322
+ usedCommandFallback: false,
13323
+ },
13324
+ runtime: {
13325
+ baseURL: "https://example.test",
13326
+ token: "selftest-token",
13327
+ timeoutSeconds: 30,
13328
+ actor: { user_id: "user-1" },
13329
+ },
13330
+ deps: {
13331
+ saveRunnerRouteState: () => {},
13332
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
13333
+ runRunnerAIExecution: async () => ({
13334
+ skip: false,
13335
+ reply: "Hello from RyoAI_bot.",
13336
+ }),
13337
+ performLocalBotDelivery: async () => {
13338
+ const err = new Error("local telegram delivery skipped stale reply anchor (Bad Request: message to be replied not found)");
13339
+ err.code = "TELEGRAM_STALE_REPLY_ANCHOR";
13340
+ err.staleReplyAnchor = true;
13341
+ throw err;
13342
+ },
13343
+ serializeRunnerTriggerPolicy: (value) => value,
13344
+ serializeRunnerArchivePolicy: (value) => value,
13345
+ buildRunnerExecutionDeps: () => ({
13346
+ validateWorkspaceArtifacts,
13347
+ analyzeHumanConversationIntentWithAI: async () => ({
13348
+ mode: "single_bot",
13349
+ lead_bot: "ryoai_bot",
13350
+ participants: ["ryoai_bot"],
13351
+ initial_responders: ["ryoai_bot"],
13352
+ allowed_responders: ["ryoai_bot"],
13353
+ summary_bot: "",
13354
+ allow_bot_to_bot: false,
13355
+ reply_expectation: "informational",
13356
+ intent_type: "small_talk",
13357
+ }),
13358
+ }),
13359
+ buildRunnerDeliveryDeps: () => ({}),
13360
+ buildRunnerRuntimeDeps: () => ({}),
13361
+ resolveConversationPeerBots: () => [],
13362
+ },
13363
+ });
13364
+ push(
13365
+ "runner_stale_reply_anchor_skips_generated_reply_without_delivery_failure",
13366
+ processed.kind === "skipped"
13367
+ && String(processed.skippedRecord?.reason || "") === "stale_reply_anchor"
13368
+ && String(processed.result?.outcome || "") === "skipped"
13369
+ && processed.result?.ai_reply_generated === true
13370
+ && String(processed.result?.delivery_status || "") === "skipped_stale_reply_anchor"
13371
+ && /message to be replied not found/i.test(String(processed.result?.transport_error || "")),
13372
+ `kind=${String(processed.kind || "(none)")} reason=${String(processed.skippedRecord?.reason || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} delivery=${String(processed.result?.delivery_status || "(none)")} transport=${String(processed.result?.transport_error || "(none)")}`,
13373
+ );
13374
+ } catch (err) {
13375
+ push("runner_stale_reply_anchor_skips_generated_reply_without_delivery_failure", false, String(err?.message || err));
13376
+ }
13377
+
13253
13378
  try {
13254
13379
  let capturedReplyToMessageID = 0;
13255
13380
  let capturedMessageThreadID = 0;
13256
- let capturedSourceMessageEnvelope = {};
13381
+ let capturedSourceMessageEnvelope = {};
13257
13382
  const processed = await processRunnerSelectedRecord({
13258
13383
  routeKey: "delivery-prefers-route-local-inbound-envelope-key",
13259
13384
  normalizedRoute: normalizeRunnerRoute({
@@ -17,6 +17,10 @@ function safeObject(value) {
17
17
  return value;
18
18
  }
19
19
 
20
+ function ensureArray(value) {
21
+ return Array.isArray(value) ? value : [];
22
+ }
23
+
20
24
  function buildTelegramMentionEntities(text) {
21
25
  const entities = [];
22
26
  const source = String(text || "");
@@ -58,7 +62,8 @@ async function startLocalTelegramRunnerSelftestServer({
58
62
  // on Windows CI shells.
59
63
  sendMessageTransientFailuresRemaining: 0,
60
64
  sendChatActionTransientFailuresRemaining: 0,
61
- updates: [
65
+ missingReplyTargetMessageIDs: [],
66
+ updates: [
62
67
  {
63
68
  update_id: 11,
64
69
  message: {
@@ -294,13 +299,24 @@ async function startLocalTelegramRunnerSelftestServer({
294
299
  state.sendMessageTransientFailuresRemaining -= 1;
295
300
  req.socket.destroy(new Error("socket hang up"));
296
301
  return;
297
- }
298
- const payload = await readJSONBody(req);
299
- const messageID = state.nextTelegramMessageID++;
300
- state.sentMessages.push({
301
- ...payload,
302
- message_id: messageID,
303
- });
302
+ }
303
+ const payload = await readJSONBody(req);
304
+ const replyToMessageID = intFromRawAllowZero(payload.reply_to_message_id, 0);
305
+ if (
306
+ replyToMessageID > 0
307
+ && ensureArray(state.missingReplyTargetMessageIDs).includes(replyToMessageID)
308
+ ) {
309
+ writeJSON(res, 400, {
310
+ ok: false,
311
+ description: "Bad Request: message to be replied not found",
312
+ });
313
+ return;
314
+ }
315
+ const messageID = state.nextTelegramMessageID++;
316
+ state.sentMessages.push({
317
+ ...payload,
318
+ message_id: messageID,
319
+ });
304
320
  writeJSON(res, 200, {
305
321
  ok: true,
306
322
  result: {
@@ -581,6 +597,45 @@ export async function runSelftestTelegramE2E(push, deps) {
581
597
  `error=${unsafeAnchorError || "(none)"} sent=${telegramE2EServer.state.sentMessages.length}`,
582
598
  );
583
599
 
600
+ telegramE2EServer.state.missingReplyTargetMessageIDs = [999];
601
+ const sentCountBeforeStaleAnchor = telegramE2EServer.state.sentMessages.length;
602
+ let staleAnchorError = "";
603
+ try {
604
+ await performLocalBotDelivery({
605
+ siteBaseURL: telegramE2EServer.baseURL,
606
+ token: e2eToken,
607
+ timeoutSeconds: 10,
608
+ actorUserID: e2eActorUserID,
609
+ bot: e2eBot,
610
+ projectID: selftestProjectID,
611
+ provider: "telegram",
612
+ destinationSelectors: {
613
+ destinationLabel: e2eDestination.label,
614
+ },
615
+ text: "stale reply anchor",
616
+ replyToMessageID: 999,
617
+ sourceMessageEnvelope: {
618
+ chat_id: e2eDestination.chat_id,
619
+ message_id: 999,
620
+ source_origin: "local_telegram_inbound",
621
+ },
622
+ archiveReplies: false,
623
+ dryRun: false,
624
+ deps: buildRunnerDeliveryDeps(),
625
+ });
626
+ } catch (err) {
627
+ staleAnchorError = String(err?.message || err);
628
+ } finally {
629
+ telegramE2EServer.state.missingReplyTargetMessageIDs = [];
630
+ }
631
+ push(
632
+ "telegram_delivery_classifies_missing_reply_target_as_stale_anchor",
633
+ /stale reply anchor/i.test(staleAnchorError)
634
+ && /message to be replied not found/i.test(staleAnchorError)
635
+ && telegramE2EServer.state.sentMessages.length === sentCountBeforeStaleAnchor,
636
+ `error=${staleAnchorError || "(none)"} sent=${telegramE2EServer.state.sentMessages.length}`,
637
+ );
638
+
584
639
  const sentCountBeforeDirectDelivery = telegramE2EServer.state.sentMessages.length;
585
640
  const commentCountBeforeDirectDelivery = telegramE2EServer.state.comments.length;
586
641
  const requestCountBeforeDirectDelivery = telegramE2EServer.state.runnerRequests.length;
@@ -1436,6 +1491,113 @@ export async function runSelftestTelegramE2E(push, deps) {
1436
1491
  telegramE2EServer.state.comments.length === 0,
1437
1492
  `comments=${telegramE2EServer.state.comments.length}`,
1438
1493
  );
1494
+
1495
+ telegramE2EServer.state.comments = [];
1496
+ telegramE2EServer.state.updates = [
1497
+ {
1498
+ update_id: 403,
1499
+ message: {
1500
+ message_id: 83,
1501
+ date: Math.floor(Date.now() / 1000),
1502
+ chat: {
1503
+ id: Number(e2eDestination.chat_id),
1504
+ type: "supergroup",
1505
+ title: e2eDestination.label,
1506
+ },
1507
+ from: {
1508
+ id: 7001,
1509
+ is_bot: false,
1510
+ first_name: "Operator",
1511
+ username: "operator_user",
1512
+ },
1513
+ text: "@RyoAI2_bot 하이",
1514
+ entities: buildTelegramMentionEntities("@RyoAI2_bot 하이"),
1515
+ },
1516
+ },
1517
+ ];
1518
+ const routeRyoai1 = normalizeRunnerRoute({
1519
+ ...e2eRoute,
1520
+ name: "selftest-runner-e2e-ownership-ryoai1",
1521
+ server_bot_name: "RyoAI_bot",
1522
+ server_bot_id: "88888888-8888-4888-8888-888888888881",
1523
+ destination_id: "dest-telegram",
1524
+ destination_label: e2eDestination.label,
1525
+ });
1526
+ const routeRyoai2 = normalizeRunnerRoute({
1527
+ ...e2eRoute,
1528
+ name: "selftest-runner-e2e-ownership-ryoai2",
1529
+ server_bot_name: "RyoAI2_bot",
1530
+ server_bot_id: "88888888-8888-4888-8888-888888888882",
1531
+ destination_id: "dest-telegram",
1532
+ destination_label: e2eDestination.label,
1533
+ });
1534
+ const routeRyoai3 = normalizeRunnerRoute({
1535
+ ...e2eRoute,
1536
+ name: "selftest-runner-e2e-ownership-ryoai3",
1537
+ server_bot_name: "RyoAI3_bot",
1538
+ server_bot_id: "88888888-8888-4888-8888-888888888883",
1539
+ destination_id: "dest-telegram",
1540
+ destination_label: e2eDestination.label,
1541
+ });
1542
+ const routeRyoai2Key = runnerRouteKey(routeRyoai2);
1543
+ const routeRyoai3Key = runnerRouteKey(routeRyoai3);
1544
+ await archiveLocalTelegramMessagesForRoute({
1545
+ routeKey: routeRyoai3Key,
1546
+ route: routeRyoai3,
1547
+ routeState: {},
1548
+ runtime: {
1549
+ baseURL: telegramE2EServer.baseURL,
1550
+ timeoutSeconds: 10,
1551
+ token: e2eToken,
1552
+ actor: {
1553
+ user_id: e2eActorUserID,
1554
+ },
1555
+ },
1556
+ bot: {
1557
+ id: "88888888-8888-4888-8888-888888888883",
1558
+ name: "RyoAI3_bot",
1559
+ username: "RyoAI3_bot",
1560
+ role: "monitor",
1561
+ },
1562
+ destination: {
1563
+ chatID: e2eDestination.chat_id,
1564
+ },
1565
+ archiveThread: {
1566
+ threadID: e2eThreadID,
1567
+ },
1568
+ managedConversationBots: [
1569
+ {
1570
+ username: "RyoAI_bot",
1571
+ route: routeRyoai1,
1572
+ bot: { username: "RyoAI_bot", name: "RyoAI_bot" },
1573
+ },
1574
+ {
1575
+ username: "RyoAI2_bot",
1576
+ route: routeRyoai2,
1577
+ bot: { username: "RyoAI2_bot", name: "RyoAI2_bot" },
1578
+ },
1579
+ {
1580
+ username: "RyoAI3_bot",
1581
+ route: routeRyoai3,
1582
+ bot: { username: "RyoAI3_bot", name: "RyoAI3_bot" },
1583
+ },
1584
+ ],
1585
+ deps: buildRunnerRuntimeDeps(),
1586
+ });
1587
+ const ownershipState = loadBotRunnerState();
1588
+ const ryoai2Receipt = safeObject(
1589
+ safeObject(safeObject(ownershipState.routes)[routeRyoai2Key]).recent_local_inbound_receipts?.[`${String(e2eDestination.chat_id)}:83`],
1590
+ );
1591
+ const ryoai3Receipt = safeObject(
1592
+ safeObject(safeObject(ownershipState.routes)[routeRyoai3Key]).recent_local_inbound_receipts?.[`${String(e2eDestination.chat_id)}:83`],
1593
+ );
1594
+ push(
1595
+ "telegram_local_inbound_receipt_is_attributed_to_explicit_mention_owner_route",
1596
+ String(ryoai2Receipt.receipt_route_key || "") === routeRyoai2Key
1597
+ && String(ryoai2Receipt.receipt_bot_username || "") === "ryoai2_bot"
1598
+ && !String(ryoai3Receipt.receipt_route_key || "").trim(),
1599
+ `owner=${JSON.stringify(ryoai2Receipt)} foreign=${JSON.stringify(ryoai3Receipt)}`,
1600
+ );
1439
1601
  } catch (err) {
1440
1602
  push("telegram_runner_e2e_local_mock", false, String(err?.message || err));
1441
1603
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.261",
3
+ "version": "0.2.263",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [