metheus-governance-mcp-cli 0.2.262 → 0.2.264

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
@@ -831,9 +831,17 @@ function botRunnerWorkspaceRegistryFilePath() {
831
831
  return resolveHomeFilePath(BOT_RUNNER_WORKSPACE_REGISTRY_RELATIVE_PATH);
832
832
  }
833
833
 
834
- function botRunnerStateFilePath() {
835
- return resolveHomeFilePath(BOT_RUNNER_STATE_RELATIVE_PATH);
836
- }
834
+ function botRunnerStateFilePath() {
835
+ return resolveHomeFilePath(BOT_RUNNER_STATE_RELATIVE_PATH);
836
+ }
837
+
838
+ function botRunnerStateBackupFilePath(filePath = botRunnerStateFilePath()) {
839
+ const normalizedPath = String(filePath || "").trim();
840
+ if (!normalizedPath) {
841
+ return "";
842
+ }
843
+ return `${normalizedPath}.bak`;
844
+ }
837
845
 
838
846
  function botRunnerProcessesFilePath() {
839
847
  return resolveHomeFilePath(BOT_RUNNER_PROCESSES_RELATIVE_PATH);
@@ -2200,25 +2208,37 @@ function migrateBotRunnerStateRoutes(routes, runnerConfig) {
2200
2208
  };
2201
2209
  }
2202
2210
 
2203
- function loadBotRunnerState() {
2204
- const filePath = botRunnerStateFilePath();
2205
- waitForBotRunnerStateLockRelease(filePath);
2206
- try {
2207
- if (!fs.existsSync(filePath)) {
2208
- return {
2209
- filePath,
2210
- routes: {},
2211
- sharedInboxes: {},
2212
- excludedComments: {},
2213
- requests: {},
2214
- consumedComments: {},
2215
- migrated: false,
2216
- migratedKeys: [],
2217
- remainingAnonymousKeys: [],
2218
- };
2219
- }
2220
- const parsed = tryJsonParse(fs.readFileSync(filePath, "utf8"));
2221
- const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
2211
+ function loadBotRunnerState() {
2212
+ const filePath = botRunnerStateFilePath();
2213
+ const backupPath = botRunnerStateBackupFilePath(filePath);
2214
+ waitForBotRunnerStateLockRelease(filePath);
2215
+ const emptyState = {
2216
+ filePath,
2217
+ routes: {},
2218
+ sharedInboxes: {},
2219
+ excludedComments: {},
2220
+ requests: {},
2221
+ consumedComments: {},
2222
+ migrated: false,
2223
+ migratedKeys: [],
2224
+ remainingAnonymousKeys: [],
2225
+ };
2226
+ try {
2227
+ if (!fs.existsSync(filePath)) {
2228
+ return emptyState;
2229
+ }
2230
+ let parsed = tryJsonParse(fs.readFileSync(filePath, "utf8"));
2231
+ if ((!parsed || typeof parsed !== "object" || Array.isArray(parsed)) && backupPath && fs.existsSync(backupPath)) {
2232
+ const backupParsed = tryJsonParse(fs.readFileSync(backupPath, "utf8"));
2233
+ if (backupParsed && typeof backupParsed === "object" && !Array.isArray(backupParsed)) {
2234
+ parsed = backupParsed;
2235
+ writeTextFileAtomic(filePath, `${JSON.stringify(backupParsed, null, 2)}\n`);
2236
+ }
2237
+ }
2238
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2239
+ return emptyState;
2240
+ }
2241
+ const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
2222
2242
  const migratedState = migrateBotRunnerStateRoutes(safeObject(parsed?.routes), runnerConfig);
2223
2243
  if (migratedState.changed) {
2224
2244
  saveBotRunnerState({
@@ -2241,24 +2261,14 @@ function loadBotRunnerState() {
2241
2261
  excludedComments: normalizeBotRunnerExcludedComments(parsed?.excluded_comments || parsed?.excludedComments),
2242
2262
  requests: normalizeBotRunnerRequests(parsed?.requests),
2243
2263
  consumedComments: normalizeBotRunnerConsumedComments(parsed?.consumed_comments || parsed?.consumedComments),
2244
- migrated: migratedState.changed,
2245
- migratedKeys: migratedState.migratedKeys,
2246
- remainingAnonymousKeys: migratedState.remainingAnonymousKeys,
2247
- };
2248
- } catch {
2249
- return {
2250
- filePath,
2251
- routes: {},
2252
- sharedInboxes: {},
2253
- excludedComments: {},
2254
- requests: {},
2255
- consumedComments: {},
2256
- migrated: false,
2257
- migratedKeys: [],
2258
- remainingAnonymousKeys: [],
2259
- };
2260
- }
2261
- }
2264
+ migrated: migratedState.changed,
2265
+ migratedKeys: migratedState.migratedKeys,
2266
+ remainingAnonymousKeys: migratedState.remainingAnonymousKeys,
2267
+ };
2268
+ } catch {
2269
+ return emptyState;
2270
+ }
2271
+ }
2262
2272
 
2263
2273
  function sleepSyncMs(delayMs) {
2264
2274
  const ms = Number(delayMs) || 0;
@@ -2408,13 +2418,19 @@ function writeTextFileAtomic(filePath, text) {
2408
2418
  }
2409
2419
  }
2410
2420
 
2411
- function saveBotRunnerState(nextState) {
2412
- const filePath = botRunnerStateFilePath();
2413
- return withBotRunnerStateFileLock(filePath, () => {
2414
- let current = {};
2415
- try {
2416
- current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2417
- } catch {}
2421
+ function saveBotRunnerState(nextState) {
2422
+ const filePath = botRunnerStateFilePath();
2423
+ const backupPath = botRunnerStateBackupFilePath(filePath);
2424
+ return withBotRunnerStateFileLock(filePath, () => {
2425
+ let current = {};
2426
+ try {
2427
+ current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2428
+ } catch {}
2429
+ if (!Object.keys(current).length && backupPath && fs.existsSync(backupPath)) {
2430
+ try {
2431
+ current = safeObject(tryJsonParse(fs.readFileSync(backupPath, "utf8")));
2432
+ } catch {}
2433
+ }
2418
2434
  const stateEntryTimestampMs = (...values) => {
2419
2435
  for (const value of values) {
2420
2436
  const ms = Date.parse(String(value || "").trim());
@@ -2562,7 +2578,7 @@ function saveBotRunnerState(nextState) {
2562
2578
  }
2563
2579
  return normalizeBotRunnerConsumedComments(merged);
2564
2580
  };
2565
- const payload = {
2581
+ const payload = {
2566
2582
  version: 1,
2567
2583
  updated_at: new Date().toISOString(),
2568
2584
  routes: mergeRunnerStateRoutes(
@@ -2581,15 +2597,30 @@ function saveBotRunnerState(nextState) {
2581
2597
  current.requests,
2582
2598
  nextState?.requests ?? current.requests,
2583
2599
  ),
2584
- consumed_comments: mergeRunnerStateConsumedComments(
2585
- current.consumed_comments ?? current.consumedComments,
2586
- nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2587
- ),
2588
- };
2589
- writeTextFileAtomic(filePath, `${JSON.stringify(payload, null, 2)}\n`);
2590
- return filePath;
2591
- });
2592
- }
2600
+ consumed_comments: mergeRunnerStateConsumedComments(
2601
+ current.consumed_comments ?? current.consumedComments,
2602
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2603
+ ),
2604
+ };
2605
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
2606
+ writeTextFileAtomic(filePath, serialized);
2607
+ const verifiedPayload = tryJsonParse(fs.readFileSync(filePath, "utf8"));
2608
+ if (!verifiedPayload || typeof verifiedPayload !== "object" || Array.isArray(verifiedPayload)) {
2609
+ if (backupPath && fs.existsSync(backupPath)) {
2610
+ const backupRaw = fs.readFileSync(backupPath, "utf8");
2611
+ const backupParsed = tryJsonParse(backupRaw);
2612
+ if (backupParsed && typeof backupParsed === "object" && !Array.isArray(backupParsed)) {
2613
+ writeTextFileAtomic(filePath, backupRaw);
2614
+ }
2615
+ }
2616
+ throw new Error("bot runner state write verification failed");
2617
+ }
2618
+ if (backupPath) {
2619
+ writeTextFileAtomic(backupPath, serialized);
2620
+ }
2621
+ return filePath;
2622
+ });
2623
+ }
2593
2624
 
2594
2625
  function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
2595
2626
  const normalized = {};
@@ -13302,21 +13333,27 @@ async function maybeDeliverRunnerExecutionFailureAfterRecord({
13302
13333
 
13303
13334
  async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, processed) {
13304
13335
  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),
13336
+ const resolvedIntentType = String(
13337
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
13338
+ ).trim();
13339
+ const normalizedDeferredOutcome = processed.kind === "delivery_failed"
13340
+ ? "delivery_failed_after_generation"
13341
+ : processed.kind === "skipped"
13342
+ ? "skipped"
13343
+ : String(processed.result?.outcome || "replied").trim().toLowerCase();
13344
+ markRunnerRequestLifecycle({
13345
+ normalizedRoute: deferredExecution.normalizedRoute,
13346
+ requestKey: deferredExecution.requestKey,
13347
+ selectedRecord: deferredExecution.selectedRecord,
13348
+ routeKey: deferredExecution.routeKey,
13349
+ outcome: normalizedDeferredOutcome,
13350
+ closedReason: processed.kind === "skipped"
13351
+ ? String(processed.skippedRecord?.reason || processed.result?.detail || "skipped").trim()
13352
+ : "",
13353
+ conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
13354
+ conversationParticipants: ensureArray(processed.result?.conversation_participants),
13355
+ conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
13356
+ allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
13320
13357
  conversationLeadBot: String(processed.result?.conversation_lead_bot || "").trim(),
13321
13358
  conversationSummaryBot: String(processed.result?.conversation_summary_bot || "").trim(),
13322
13359
  conversationAllowBotToBot: processed.result?.conversation_allow_bot_to_bot === true,
@@ -13349,17 +13386,17 @@ async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, proce
13349
13386
  lastReplyMessageID: intFromRawAllowZero(processed.result?.last_reply_message_id, 0),
13350
13387
  lastReplyMessageThreadID: intFromRawAllowZero(processed.result?.last_reply_message_thread_id, 0),
13351
13388
  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,
13389
+ replyFallbackUsed: processed.result?.reply_fallback_used === true,
13390
+ authoritativeSourceMessageEnvelope: safeObject(
13391
+ safeObject(deferredExecution.humanInboundVisibility).sourceMessageEnvelope,
13392
+ ),
13393
+ });
13394
+ if (processed.kind !== "delivery_failed" && processed.kind !== "skipped") {
13395
+ await ensureRunnerRootWorkItemForRequest({
13396
+ normalizedRoute: deferredExecution.normalizedRoute,
13397
+ routeKey: deferredExecution.routeKey,
13398
+ selectedRecord: deferredExecution.selectedRecord,
13399
+ runtime: deferredExecution.runtime,
13363
13400
  requestKey: deferredExecution.requestKey,
13364
13401
  });
13365
13402
  }
@@ -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}`,
@@ -1809,7 +1809,7 @@ export async function resolveRunnerPrecomputedSelectedRecordExecutionContext({
1809
1809
  };
1810
1810
  }
1811
1811
 
1812
- function resolveRunnerDeliverySourceMessageEnvelope({
1812
+ export function resolveRunnerDeliverySourceMessageEnvelope({
1813
1813
  routeState,
1814
1814
  persistedRequest,
1815
1815
  selectedRecord,
@@ -1826,7 +1826,7 @@ function resolveRunnerDeliverySourceMessageEnvelope({
1826
1826
  if (Object.keys(safeObject(localMatch.envelope)).length > 0) {
1827
1827
  return localMatch.envelope;
1828
1828
  }
1829
- return safeObject(localMatch.candidates).archiveEnvelope || {};
1829
+ return {};
1830
1830
  }
1831
1831
 
1832
1832
  function escapeRegExp(text) {
@@ -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, {
@@ -2,7 +2,10 @@
2
2
  import http from "node:http";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import process from "node:process";
5
+ import process from "node:process";
6
+ import {
7
+ resolveRunnerDeliverySourceMessageEnvelope,
8
+ } from "./runner-orchestration.mjs";
6
9
 
7
10
  function requireDependency(deps, key) {
8
11
  const candidate = deps?.[key];
@@ -2745,6 +2748,66 @@ export async function runSelftestRunnerScenarios(push, deps) {
2745
2748
  }
2746
2749
  }
2747
2750
 
2751
+ const originalStateBackupHome = process.env.HOME;
2752
+ const originalStateBackupUserProfile = process.env.USERPROFILE;
2753
+ let stateBackupTempRoot = "";
2754
+ try {
2755
+ stateBackupTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-state-backup-"));
2756
+ process.env.HOME = stateBackupTempRoot;
2757
+ process.env.USERPROFILE = stateBackupTempRoot;
2758
+ const metheusDir = path.join(stateBackupTempRoot, ".metheus");
2759
+ fs.mkdirSync(metheusDir, { recursive: true });
2760
+ const statePath = path.join(metheusDir, "bot-runner-state.json");
2761
+ const backupPath = `${statePath}.bak`;
2762
+ fs.writeFileSync(
2763
+ backupPath,
2764
+ `${JSON.stringify({
2765
+ version: 1,
2766
+ updated_at: "2026-03-29T00:00:00.000Z",
2767
+ routes: {
2768
+ "telegram-monitor-state-backup::project::telegram::monitor::dest::actor": {
2769
+ last_action: "idle",
2770
+ },
2771
+ },
2772
+ shared_inboxes: {},
2773
+ excluded_comments: {},
2774
+ requests: {},
2775
+ consumed_comments: {},
2776
+ }, null, 2)}\n`,
2777
+ "utf8",
2778
+ );
2779
+ fs.writeFileSync(statePath, "{ malformed json", "utf8");
2780
+ const recoveredState = loadBotRunnerState();
2781
+ const recoveredRouteState = safeObject(
2782
+ safeObject(recoveredState.routes)["telegram-monitor-state-backup::project::telegram::monitor::dest::actor"],
2783
+ );
2784
+ const repairedPrimary = safeObject(tryJsonParse(fs.readFileSync(statePath, "utf8")));
2785
+ push(
2786
+ "runner_state_load_restores_primary_from_backup_when_primary_is_malformed",
2787
+ String(recoveredRouteState.last_action || "") === "idle"
2788
+ && String(safeObject(safeObject(repairedPrimary.routes)["telegram-monitor-state-backup::project::telegram::monitor::dest::actor"]).last_action || "") === "idle",
2789
+ `recovered=${String(recoveredRouteState.last_action || "(none)")} repaired=${String(safeObject(safeObject(repairedPrimary.routes)["telegram-monitor-state-backup::project::telegram::monitor::dest::actor"]).last_action || "(none)")}`,
2790
+ );
2791
+ } catch (err) {
2792
+ push("runner_state_load_restores_primary_from_backup_when_primary_is_malformed", false, String(err?.message || err));
2793
+ } finally {
2794
+ if (typeof originalStateBackupHome === "string") {
2795
+ process.env.HOME = originalStateBackupHome;
2796
+ } else {
2797
+ delete process.env.HOME;
2798
+ }
2799
+ if (typeof originalStateBackupUserProfile === "string") {
2800
+ process.env.USERPROFILE = originalStateBackupUserProfile;
2801
+ } else {
2802
+ delete process.env.USERPROFILE;
2803
+ }
2804
+ if (stateBackupTempRoot) {
2805
+ try {
2806
+ fs.rmSync(stateBackupTempRoot, { recursive: true, force: true });
2807
+ } catch {}
2808
+ }
2809
+ }
2810
+
2748
2811
  const defaultMonitorTriggerPolicy = normalizeRunnerTriggerPolicy({}, { role: "monitor" });
2749
2812
  push(
2750
2813
  "bot_runner_default_monitor_trigger_policy",
@@ -13246,14 +13309,139 @@ export async function runSelftestRunnerScenarios(push, deps) {
13246
13309
  && /ECONNRESET/i.test(String(processed.result?.transport_error || "")),
13247
13310
  `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
13311
  );
13249
- } catch (err) {
13250
- push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
13251
- }
13252
-
13312
+ } catch (err) {
13313
+ push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
13314
+ }
13315
+
13316
+ try {
13317
+ const processed = await processRunnerSelectedRecord({
13318
+ routeKey: "delivery-stale-reply-anchor-key",
13319
+ normalizedRoute: normalizeRunnerRoute({
13320
+ name: "telegram-monitor-delivery-stale-reply-anchor",
13321
+ project_id: selftestProjectID,
13322
+ provider: "telegram",
13323
+ role: "monitor",
13324
+ role_profile: "monitor",
13325
+ destination_id: "dest-1",
13326
+ destination_label: "Main Room",
13327
+ server_bot_name: "RyoAI_bot",
13328
+ server_bot_id: "bot-1",
13329
+ trigger_policy: {
13330
+ mentions_only: true,
13331
+ direct_messages: true,
13332
+ reply_to_bot_messages: true,
13333
+ },
13334
+ archive_policy: {
13335
+ mirror_replies: true,
13336
+ dedupe_inbound: true,
13337
+ dedupe_outbound: true,
13338
+ skip_bot_messages: true,
13339
+ },
13340
+ dry_run_delivery: false,
13341
+ }),
13342
+ selectedRecord: {
13343
+ id: "comment-delivery-stale-reply-anchor",
13344
+ createdAt: "2026-03-17T00:00:22.000Z",
13345
+ parsedArchive: {
13346
+ kind: "telegram_message",
13347
+ chatID: "-100123",
13348
+ chatType: "supergroup",
13349
+ body: "@RyoAI_bot hi",
13350
+ messageID: 127,
13351
+ sender: "human",
13352
+ senderIsBot: false,
13353
+ mentionUsernames: ["ryoai_bot"],
13354
+ },
13355
+ },
13356
+ pendingOrdered: [],
13357
+ bot: {
13358
+ id: "bot-1",
13359
+ name: "RyoAI_bot",
13360
+ username: "RyoAI_bot",
13361
+ role: "monitor",
13362
+ provider: "telegram",
13363
+ },
13364
+ destination: {
13365
+ id: "dest-1",
13366
+ label: "Main Room",
13367
+ provider: "telegram",
13368
+ chatID: "-100123",
13369
+ },
13370
+ archiveThread: {
13371
+ threadID: "thread-1",
13372
+ workItemID: "work-item-1",
13373
+ },
13374
+ executionPlan: {
13375
+ mode: "role_profile",
13376
+ roleProfileName: "monitor",
13377
+ roleProfile: {
13378
+ client: "sample",
13379
+ model: "",
13380
+ permissionMode: "read_only",
13381
+ reasoningEffort: "low",
13382
+ },
13383
+ workspaceDir: "",
13384
+ workspaceSource: "selftest",
13385
+ usedCommandFallback: false,
13386
+ },
13387
+ runtime: {
13388
+ baseURL: "https://example.test",
13389
+ token: "selftest-token",
13390
+ timeoutSeconds: 30,
13391
+ actor: { user_id: "user-1" },
13392
+ },
13393
+ deps: {
13394
+ saveRunnerRouteState: () => {},
13395
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
13396
+ runRunnerAIExecution: async () => ({
13397
+ skip: false,
13398
+ reply: "Hello from RyoAI_bot.",
13399
+ }),
13400
+ performLocalBotDelivery: async () => {
13401
+ const err = new Error("local telegram delivery skipped stale reply anchor (Bad Request: message to be replied not found)");
13402
+ err.code = "TELEGRAM_STALE_REPLY_ANCHOR";
13403
+ err.staleReplyAnchor = true;
13404
+ throw err;
13405
+ },
13406
+ serializeRunnerTriggerPolicy: (value) => value,
13407
+ serializeRunnerArchivePolicy: (value) => value,
13408
+ buildRunnerExecutionDeps: () => ({
13409
+ validateWorkspaceArtifacts,
13410
+ analyzeHumanConversationIntentWithAI: async () => ({
13411
+ mode: "single_bot",
13412
+ lead_bot: "ryoai_bot",
13413
+ participants: ["ryoai_bot"],
13414
+ initial_responders: ["ryoai_bot"],
13415
+ allowed_responders: ["ryoai_bot"],
13416
+ summary_bot: "",
13417
+ allow_bot_to_bot: false,
13418
+ reply_expectation: "informational",
13419
+ intent_type: "small_talk",
13420
+ }),
13421
+ }),
13422
+ buildRunnerDeliveryDeps: () => ({}),
13423
+ buildRunnerRuntimeDeps: () => ({}),
13424
+ resolveConversationPeerBots: () => [],
13425
+ },
13426
+ });
13427
+ push(
13428
+ "runner_stale_reply_anchor_skips_generated_reply_without_delivery_failure",
13429
+ processed.kind === "skipped"
13430
+ && String(processed.skippedRecord?.reason || "") === "stale_reply_anchor"
13431
+ && String(processed.result?.outcome || "") === "skipped"
13432
+ && processed.result?.ai_reply_generated === true
13433
+ && String(processed.result?.delivery_status || "") === "skipped_stale_reply_anchor"
13434
+ && /message to be replied not found/i.test(String(processed.result?.transport_error || "")),
13435
+ `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)")}`,
13436
+ );
13437
+ } catch (err) {
13438
+ push("runner_stale_reply_anchor_skips_generated_reply_without_delivery_failure", false, String(err?.message || err));
13439
+ }
13440
+
13253
13441
  try {
13254
13442
  let capturedReplyToMessageID = 0;
13255
13443
  let capturedMessageThreadID = 0;
13256
- let capturedSourceMessageEnvelope = {};
13444
+ let capturedSourceMessageEnvelope = {};
13257
13445
  const processed = await processRunnerSelectedRecord({
13258
13446
  routeKey: "delivery-prefers-route-local-inbound-envelope-key",
13259
13447
  normalizedRoute: normalizeRunnerRoute({
@@ -15075,9 +15263,43 @@ export async function runSelftestRunnerScenarios(push, deps) {
15075
15263
  && String(capturedSourceMessageEnvelope.source_route_key || "") === "telegram-monitor-ryoai2-bot-2::project::telegram::monitor::dest::actor",
15076
15264
  `kind=${String(processed.kind || "(none)")} reply_to=${capturedReplyToMessageID} origin=${String(capturedSourceMessageEnvelope.source_origin || "(none)")} route=${String(capturedSourceMessageEnvelope.source_route_key || "(none)")}`,
15077
15265
  );
15078
- } catch (err) {
15079
- push("runner_delivery_accepts_archived_local_route_provenance", false, String(err?.message || err));
15080
- }
15266
+ } catch (err) {
15267
+ push("runner_delivery_accepts_archived_local_route_provenance", false, String(err?.message || err));
15268
+ }
15269
+
15270
+ try {
15271
+ const envelope = resolveRunnerDeliverySourceMessageEnvelope({
15272
+ routeState: {
15273
+ recent_local_inbound_receipts: {},
15274
+ recent_local_inbound_envelopes: {},
15275
+ },
15276
+ persistedRequest: {},
15277
+ selectedRecord: {
15278
+ parsedArchive: {
15279
+ kind: "telegram_message",
15280
+ chatID: "-100123",
15281
+ chatType: "supergroup",
15282
+ body: "@RyoAI2_bot hi without local provenance",
15283
+ messageID: 331,
15284
+ sender: "human",
15285
+ senderIsBot: false,
15286
+ mentionUsernames: ["ryoai2_bot"],
15287
+ sourceOrigin: "archive_reconstructed",
15288
+ sourceRouteKey: "telegram-monitor-ryoai2-bot-2::project::telegram::monitor::dest::actor",
15289
+ sourceBotUsername: "ryoai2_bot",
15290
+ },
15291
+ },
15292
+ routeKey: "telegram-monitor-ryoai2-bot-2::project::telegram::monitor::dest::actor",
15293
+ currentBotSelector: "ryoai2_bot",
15294
+ });
15295
+ push(
15296
+ "runner_delivery_drops_archive_reconstructed_reply_anchor_before_delivery",
15297
+ Object.keys(safeObject(envelope)).length === 0,
15298
+ `origin=${String(safeObject(envelope).source_origin || "(none)")} message=${String(safeObject(envelope).message_id || "(none)")}`,
15299
+ );
15300
+ } catch (err) {
15301
+ push("runner_delivery_drops_archive_reconstructed_reply_anchor_before_delivery", false, String(err?.message || err));
15302
+ }
15081
15303
 
15082
15304
  try {
15083
15305
  const provenanceTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-provenance-selftest-"));
@@ -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.264",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [