metheus-governance-mcp-cli 0.2.262 → 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, {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.262",
3
+ "version": "0.2.263",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [