metheus-governance-mcp-cli 0.2.205 → 0.2.207

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
@@ -16,6 +16,7 @@ import {
16
16
  analyzeHumanConversationIntentWithAI,
17
17
  auditRoleExecutionPlanWithAI,
18
18
  auditDirectHumanReplyWithAI,
19
+ explainExecutionFailureWithAI,
19
20
  normalizeExecutionArtifacts,
20
21
  planRoleExecutionWithAI,
21
22
  repairRoleExecutionPlanWithAI,
@@ -2065,14 +2066,176 @@ function saveBotRunnerState(nextState) {
2065
2066
  try {
2066
2067
  current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2067
2068
  } catch {}
2069
+ const stateEntryTimestampMs = (...values) => {
2070
+ for (const value of values) {
2071
+ const ms = Date.parse(String(value || "").trim());
2072
+ if (Number.isFinite(ms)) {
2073
+ return ms;
2074
+ }
2075
+ }
2076
+ return 0;
2077
+ };
2078
+ const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2079
+ const currentRoutes = safeObject(currentRoutesRaw);
2080
+ const nextRoutes = safeObject(nextRoutesRaw);
2081
+ const merged = {
2082
+ ...currentRoutes,
2083
+ };
2084
+ const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2085
+ const currentSessions = safeObject(currentSessionsRaw);
2086
+ const nextSessions = safeObject(nextSessionsRaw);
2087
+ const mergedSessions = {
2088
+ ...currentSessions,
2089
+ };
2090
+ for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2091
+ const currentSession = safeObject(currentSessions[conversationID]);
2092
+ const nextSession = safeObject(nextSessionRaw);
2093
+ if (!Object.keys(currentSession).length) {
2094
+ mergedSessions[conversationID] = nextSession;
2095
+ continue;
2096
+ }
2097
+ const currentMs = stateEntryTimestampMs(
2098
+ currentSession.updated_at,
2099
+ currentSession.last_activity_at,
2100
+ currentSession.closed_at,
2101
+ currentSession.started_at,
2102
+ currentSession.expires_at,
2103
+ );
2104
+ const nextMs = stateEntryTimestampMs(
2105
+ nextSession.updated_at,
2106
+ nextSession.last_activity_at,
2107
+ nextSession.closed_at,
2108
+ nextSession.started_at,
2109
+ nextSession.expires_at,
2110
+ );
2111
+ mergedSessions[conversationID] = nextMs >= currentMs
2112
+ ? {
2113
+ ...currentSession,
2114
+ ...nextSession,
2115
+ }
2116
+ : {
2117
+ ...nextSession,
2118
+ ...currentSession,
2119
+ };
2120
+ }
2121
+ return mergedSessions;
2122
+ };
2123
+ for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2124
+ const currentRoute = safeObject(currentRoutes[routeKey]);
2125
+ const nextRoute = safeObject(nextRouteRaw);
2126
+ if (!Object.keys(currentRoute).length) {
2127
+ merged[routeKey] = nextRoute;
2128
+ continue;
2129
+ }
2130
+ const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2131
+ const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2132
+ merged[routeKey] = cleanupRunnerStateRecord({
2133
+ ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2134
+ conversation_sessions: mergeConversationSessions(
2135
+ currentRoute.conversation_sessions,
2136
+ nextRoute.conversation_sessions,
2137
+ ),
2138
+ });
2139
+ }
2140
+ return merged;
2141
+ };
2142
+ const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2143
+ const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2144
+ const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2145
+ const merged = {
2146
+ ...currentRequests,
2147
+ };
2148
+ for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2149
+ const currentRequest = safeObject(currentRequests[requestKey]);
2150
+ const nextRequest = safeObject(nextRequestRaw);
2151
+ const currentMs = stateEntryTimestampMs(
2152
+ currentRequest.updated_at,
2153
+ currentRequest.completed_at,
2154
+ currentRequest.closed_at,
2155
+ currentRequest.claimed_at,
2156
+ );
2157
+ const nextMs = stateEntryTimestampMs(
2158
+ nextRequest.updated_at,
2159
+ nextRequest.completed_at,
2160
+ nextRequest.closed_at,
2161
+ nextRequest.claimed_at,
2162
+ );
2163
+ if (!Object.keys(currentRequest).length) {
2164
+ merged[requestKey] = nextRequest;
2165
+ continue;
2166
+ }
2167
+ merged[requestKey] = nextMs >= currentMs
2168
+ ? {
2169
+ ...currentRequest,
2170
+ ...nextRequest,
2171
+ }
2172
+ : currentRequest;
2173
+ }
2174
+ return normalizeBotRunnerRequests(merged);
2175
+ };
2176
+ const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2177
+ const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2178
+ const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2179
+ const merged = {
2180
+ ...currentExcluded,
2181
+ };
2182
+ for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2183
+ const currentEntry = safeObject(currentExcluded[commentID]);
2184
+ const nextEntry = safeObject(nextEntryRaw);
2185
+ const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2186
+ const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2187
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2188
+ merged[commentID] = {
2189
+ ...currentEntry,
2190
+ ...nextEntry,
2191
+ };
2192
+ }
2193
+ }
2194
+ return normalizeBotRunnerExcludedComments(merged);
2195
+ };
2196
+ const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2197
+ const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2198
+ const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2199
+ const merged = {
2200
+ ...currentConsumed,
2201
+ };
2202
+ for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2203
+ const currentEntry = safeObject(currentConsumed[commentID]);
2204
+ const nextEntry = safeObject(nextEntryRaw);
2205
+ const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2206
+ const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2207
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2208
+ merged[commentID] = {
2209
+ ...currentEntry,
2210
+ ...nextEntry,
2211
+ };
2212
+ }
2213
+ }
2214
+ return normalizeBotRunnerConsumedComments(merged);
2215
+ };
2068
2216
  const payload = {
2069
2217
  version: 1,
2070
2218
  updated_at: new Date().toISOString(),
2071
- routes: safeObject(nextState?.routes ?? current.routes),
2072
- shared_inboxes: safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes ?? current.shared_inboxes ?? current.sharedInboxes),
2073
- excluded_comments: normalizeBotRunnerExcludedComments(nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments),
2074
- requests: normalizeBotRunnerRequests(nextState?.requests ?? current.requests),
2075
- consumed_comments: normalizeBotRunnerConsumedComments(nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments),
2219
+ routes: mergeRunnerStateRoutes(
2220
+ current.routes,
2221
+ nextState?.routes ?? current.routes,
2222
+ ),
2223
+ shared_inboxes: {
2224
+ ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2225
+ ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2226
+ },
2227
+ excluded_comments: mergeRunnerStateExcludedComments(
2228
+ current.excluded_comments ?? current.excludedComments,
2229
+ nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2230
+ ),
2231
+ requests: mergeRunnerStateRequests(
2232
+ current.requests,
2233
+ nextState?.requests ?? current.requests,
2234
+ ),
2235
+ consumed_comments: mergeRunnerStateConsumedComments(
2236
+ current.consumed_comments ?? current.consumedComments,
2237
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2238
+ ),
2076
2239
  };
2077
2240
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
2078
2241
  fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -2507,6 +2670,89 @@ function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2507
2670
  });
2508
2671
  }
2509
2672
 
2673
+ function findScopedConversationSessionState(state, normalizedRoute, conversationIDRaw = "") {
2674
+ const conversationID = String(conversationIDRaw || "").trim();
2675
+ if (!conversationID) {
2676
+ return {};
2677
+ }
2678
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
2679
+ const nextRoutes = safeObject(state?.routes);
2680
+ const candidates = [];
2681
+ const seenRouteKeys = new Set();
2682
+ const pushCandidateRoute = (routeRaw) => {
2683
+ const routeObject = safeObject(routeRaw);
2684
+ if (!Object.keys(routeObject).length) {
2685
+ return;
2686
+ }
2687
+ const candidateRoute = normalizeRunnerRoute(routeObject);
2688
+ const candidateRouteKey = runnerRouteKey(candidateRoute);
2689
+ if (seenRouteKeys.has(candidateRouteKey)) {
2690
+ return;
2691
+ }
2692
+ seenRouteKeys.add(candidateRouteKey);
2693
+ candidates.push({
2694
+ route: candidateRoute,
2695
+ routeKey: candidateRouteKey,
2696
+ routeState: safeObject(nextRoutes[candidateRouteKey]),
2697
+ });
2698
+ };
2699
+ pushCandidateRoute(normalizedRoute);
2700
+ for (const candidateRouteRaw of ensureArray(config.routes)) {
2701
+ if (!runnerRouteMatchesProjectConversationScope(candidateRouteRaw, normalizedRoute)) {
2702
+ continue;
2703
+ }
2704
+ pushCandidateRoute(candidateRouteRaw);
2705
+ }
2706
+ const ranked = candidates
2707
+ .map((candidate) => {
2708
+ const session = safeObject(safeObject(candidate.routeState.conversation_sessions)[conversationID]);
2709
+ if (!Object.keys(session).length) {
2710
+ return null;
2711
+ }
2712
+ const status = String(session.status || "").trim().toLowerCase();
2713
+ const lastActivity = firstNonEmptyString([session.last_activity_at, session.closed_at, session.started_at]);
2714
+ return {
2715
+ ...candidate,
2716
+ session,
2717
+ status,
2718
+ lastActivity,
2719
+ };
2720
+ })
2721
+ .filter(Boolean)
2722
+ .sort((left, right) => {
2723
+ if (left.status !== right.status) {
2724
+ if (left.status === "open") return -1;
2725
+ if (right.status === "open") return 1;
2726
+ }
2727
+ if (left.lastActivity && right.lastActivity && left.lastActivity !== right.lastActivity) {
2728
+ return left.lastActivity < right.lastActivity ? 1 : -1;
2729
+ }
2730
+ return String(left.routeKey || "").localeCompare(String(right.routeKey || ""));
2731
+ });
2732
+ return safeObject(ranked[0]);
2733
+ }
2734
+
2735
+ function sessionAllowsConversationResponder(sessionRaw, responderSelectorRaw = "") {
2736
+ const session = safeObject(sessionRaw);
2737
+ const responderSelector = normalizeTelegramMentionUsername(responderSelectorRaw);
2738
+ if (!responderSelector) {
2739
+ return false;
2740
+ }
2741
+ if (String(session.status || "").trim().toLowerCase() !== "open") {
2742
+ return false;
2743
+ }
2744
+ const nextExpectedResponders = ensureArray(session.next_expected_responders)
2745
+ .map((value) => normalizeTelegramMentionUsername(value))
2746
+ .filter(Boolean);
2747
+ if (nextExpectedResponders.length > 0) {
2748
+ return nextExpectedResponders.includes(responderSelector);
2749
+ }
2750
+ const allowedResponders = ensureArray(session.allowed_responders)
2751
+ .map((value) => normalizeTelegramMentionUsername(value))
2752
+ .filter(Boolean);
2753
+ return allowedResponders.includes(responderSelector);
2754
+ }
2755
+
2510
2756
  function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
2511
2757
  const chatID = String(selectors.chatID || "").trim();
2512
2758
  const messageID = intFromRawAllowZero(selectors.messageID, 0);
@@ -3895,11 +4141,80 @@ function resolveRunnerContinuationRequestForBotReply({
3895
4141
  };
3896
4142
  }
3897
4143
  const currentState = loadBotRunnerState();
3898
- const requests = findRunnerRequestsForScope(currentState, normalizedRoute, {
4144
+ let requests = findRunnerRequestsForScope(currentState, normalizedRoute, {
3899
4145
  conversationID,
3900
4146
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3901
4147
  }).filter((entry) => isActiveRunnerRequestStatus(entry.status));
3902
- const request = safeObject(requests[0]);
4148
+ let request = safeObject(requests[0]);
4149
+ if (!Object.keys(request).length) {
4150
+ const sessionMatch = findScopedConversationSessionState(currentState, normalizedRoute, conversationID);
4151
+ const session = safeObject(sessionMatch.session);
4152
+ const fallbackRequestKey = String(
4153
+ safeObject(sessionMatch.routeState).active_request_key
4154
+ || safeObject(sessionMatch.routeState).last_request_key
4155
+ || "",
4156
+ ).trim();
4157
+ if (
4158
+ fallbackRequestKey
4159
+ && String(session.status || "").trim().toLowerCase() === "open"
4160
+ ) {
4161
+ const fallbackRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[fallbackRequestKey]);
4162
+ const nowISO = new Date().toISOString();
4163
+ const seedRequest = Object.keys(fallbackRequest).length
4164
+ ? fallbackRequest
4165
+ : {
4166
+ request_key: fallbackRequestKey,
4167
+ project_id: String(normalizedRoute?.projectID || "").trim(),
4168
+ provider: String(normalizedRoute?.provider || "").trim(),
4169
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4170
+ conversation_id: conversationID,
4171
+ selected_bot_usernames: ensureArray(session.participants),
4172
+ conversation_allowed_responders: ensureArray(session.allowed_responders),
4173
+ conversation_intent_mode: String(session.intent_mode || "").trim().toLowerCase(),
4174
+ conversation_lead_bot: normalizeTelegramMentionUsername(session.lead_bot_username),
4175
+ conversation_summary_bot: normalizeTelegramMentionUsername(session.summary_bot_username),
4176
+ conversation_participants: ensureArray(session.participants),
4177
+ conversation_initial_responders: ensureArray(session.initial_responders),
4178
+ conversation_allow_bot_to_bot: session.allow_bot_to_bot === true,
4179
+ conversation_reply_expectation: "",
4180
+ execution_contract_type: String(session.last_execution_contract_type || "").trim().toLowerCase(),
4181
+ execution_contract_actionable: session.last_execution_contract_actionable === true,
4182
+ execution_contract_targets: ensureArray(session.last_execution_contract_targets),
4183
+ next_expected_responders: ensureArray(session.next_expected_responders),
4184
+ normalized_intent: String(safeObject(sessionMatch.routeState).last_intent_type || "").trim().toLowerCase(),
4185
+ status: "running",
4186
+ claimed_by_route: String(sessionMatch.routeKey || "").trim(),
4187
+ claimed_at: firstNonEmptyString([session.started_at, nowISO]),
4188
+ started_at: firstNonEmptyString([session.started_at, nowISO]),
4189
+ root_work_item_id: String(
4190
+ safeObject(sessionMatch.routeState).active_root_work_item_id
4191
+ || safeObject(sessionMatch.routeState).last_root_work_item_id
4192
+ || "",
4193
+ ).trim(),
4194
+ root_work_item_title: String(
4195
+ safeObject(sessionMatch.routeState).active_root_work_item_title
4196
+ || safeObject(sessionMatch.routeState).last_root_work_item_title
4197
+ || "",
4198
+ ).trim(),
4199
+ root_work_item_status: String(
4200
+ safeObject(sessionMatch.routeState).active_root_work_item_status
4201
+ || safeObject(sessionMatch.routeState).last_root_work_item_status
4202
+ || "",
4203
+ ).trim().toLowerCase(),
4204
+ };
4205
+ const seededRequest = upsertRunnerRequest(currentState, fallbackRequestKey, seedRequest);
4206
+ currentState.requests = seededRequest.requests;
4207
+ request = safeObject(seededRequest.request);
4208
+ saveBotRunnerState({
4209
+ routes: currentState.routes,
4210
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
4211
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
4212
+ requests: currentState.requests,
4213
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
4214
+ });
4215
+ requests = [request];
4216
+ }
4217
+ }
3903
4218
  if (!Object.keys(request).length) {
3904
4219
  return {
3905
4220
  ok: false,
@@ -4149,7 +4464,10 @@ function cleanupBotRunnerRequestState({
4149
4464
  && String(entry.conversation_id || "").trim() === String(conversationID || "").trim()
4150
4465
  && isActiveRunnerRequestStatus(entry.status)
4151
4466
  ));
4152
- if (!expired && activeRequests.length > 0) {
4467
+ const pendingContinuationResponders = ensureArray(session.next_expected_responders)
4468
+ .map((value) => normalizeTelegramMentionUsername(value))
4469
+ .filter(Boolean);
4470
+ if (!expired && (activeRequests.length > 0 || pendingContinuationResponders.length > 0)) {
4153
4471
  continue;
4154
4472
  }
4155
4473
  const closedReason = expired ? "expired_session" : "orphaned_open_session";
@@ -7487,7 +7805,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7487
7805
  && String(entry.conversation_id || "").trim() === conversationID
7488
7806
  && isActiveRunnerRequestStatus(entry.status)
7489
7807
  ));
7490
- return activeRequests.length > 0;
7808
+ if (activeRequests.length > 0) {
7809
+ return true;
7810
+ }
7811
+ const sessionMatch = findScopedConversationSessionState(latestRunnerState, normalizedRoute, conversationID);
7812
+ return sessionAllowsConversationResponder(sessionMatch.session, currentBotSelector);
7491
7813
  }
7492
7814
  return true;
7493
7815
  });
@@ -8142,6 +8464,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8142
8464
  saveRunnerRouteState,
8143
8465
  startRunnerTypingHeartbeat,
8144
8466
  runRunnerAIExecution,
8467
+ explainExecutionFailureWithAI,
8145
8468
  performLocalBotDelivery,
8146
8469
  serializeRunnerTriggerPolicy,
8147
8470
  serializeRunnerArchivePolicy,
@@ -10512,6 +10835,7 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
10512
10835
  saveRunnerRouteState,
10513
10836
  startRunnerTypingHeartbeat,
10514
10837
  runRunnerAIExecution,
10838
+ explainExecutionFailureWithAI,
10515
10839
  performLocalBotDelivery,
10516
10840
  serializeRunnerTriggerPolicy,
10517
10841
  serializeRunnerArchivePolicy,
@@ -1269,6 +1269,29 @@ export function resolveResponderAdjudicatorModelDisplayName({
1269
1269
  ).trim();
1270
1270
  }
1271
1271
 
1272
+ export function resolveFailureExplainerModelDisplayName({
1273
+ client = "",
1274
+ model = "",
1275
+ env = process.env,
1276
+ } = {}) {
1277
+ const explainerClient = normalizeLocalAIClientName(
1278
+ String(
1279
+ client
1280
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
1281
+ || env?.METHEUS_INTENT_PARSER_CLIENT
1282
+ || "",
1283
+ ).trim(),
1284
+ "gpt",
1285
+ );
1286
+ return String(
1287
+ model
1288
+ || env?.METHEUS_FAILURE_EXPLAINER_MODEL
1289
+ || env?.METHEUS_INTENT_PARSER_MODEL
1290
+ || defaultAdjudicationModelForClient(explainerClient, env)
1291
+ || "",
1292
+ ).trim();
1293
+ }
1294
+
1272
1295
  export function resolveLocalAIExecutionModel(clientName, rawModelValue = "") {
1273
1296
  const modelValue = String(rawModelValue || "").trim();
1274
1297
  if (!modelValue) return "";
@@ -2492,6 +2515,100 @@ export function analyzeHumanConversationIntentWithAI({
2492
2515
  };
2493
2516
  }
2494
2517
 
2518
+ function buildExecutionFailureExplanationPrompt({
2519
+ botName = "",
2520
+ userMessageText = "",
2521
+ failureFacts = null,
2522
+ }) {
2523
+ const compactUserMessage = String(userMessageText || "").trim() || "(not available)";
2524
+ const compactBotName = String(botName || "").trim() || "the bot";
2525
+ return [
2526
+ "You explain runner execution failures to the user.",
2527
+ `The active bot name is: ${compactBotName}`,
2528
+ "Use only the supplied failure facts. Do not invent success, recovered work items, or missing facts.",
2529
+ "Classify the outcome and explain it briefly in the same language as the user's latest message when possible.",
2530
+ "If retryable is true, mention that a retry is reasonable.",
2531
+ "Return a JSON object only with keys:",
2532
+ '{"classification":"failed|retryable_failure|partial_success|needs_user_input|blocked","reply":"string","next_action":"string"}',
2533
+ "",
2534
+ "Latest user message:",
2535
+ compactUserMessage,
2536
+ "",
2537
+ "Failure facts JSON:",
2538
+ JSON.stringify(safeObject(failureFacts), null, 2),
2539
+ ].join("\n");
2540
+ }
2541
+
2542
+ export function explainExecutionFailureWithAI({
2543
+ failureFacts = null,
2544
+ userMessageText = "",
2545
+ botName = "",
2546
+ workspaceDir,
2547
+ client = "",
2548
+ model = "",
2549
+ env = process.env,
2550
+ }) {
2551
+ const explainerClient = normalizeLocalAIClientName(
2552
+ String(
2553
+ client
2554
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
2555
+ || env?.METHEUS_INTENT_PARSER_CLIENT
2556
+ || "",
2557
+ ).trim(),
2558
+ "gpt",
2559
+ );
2560
+ const explainerModel = resolveFailureExplainerModelDisplayName({
2561
+ client: explainerClient,
2562
+ model,
2563
+ env,
2564
+ });
2565
+ const rawText = runLocalAIPromptRawText({
2566
+ client: explainerClient,
2567
+ promptText: buildExecutionFailureExplanationPrompt({
2568
+ botName,
2569
+ userMessageText,
2570
+ failureFacts,
2571
+ }),
2572
+ workspaceDir,
2573
+ model: explainerModel,
2574
+ permissionMode: "read_only",
2575
+ reasoningEffort: String(env?.METHEUS_FAILURE_EXPLAINER_REASONING_EFFORT || "low").trim() || "low",
2576
+ env,
2577
+ });
2578
+ const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
2579
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2580
+ const classificationRaw = String(parsed.classification || "").trim().toLowerCase();
2581
+ const classification = [
2582
+ "failed",
2583
+ "retryable_failure",
2584
+ "partial_success",
2585
+ "needs_user_input",
2586
+ "blocked",
2587
+ ].includes(classificationRaw) ? classificationRaw : "failed";
2588
+ const reply = String(parsed.reply || parsed.message || "").trim();
2589
+ const nextAction = String(parsed.next_action || parsed.nextAction || "").trim();
2590
+ if (!reply) {
2591
+ throw new Error("failure explainer did not return reply text");
2592
+ }
2593
+ return {
2594
+ classification,
2595
+ reply,
2596
+ next_action: nextAction,
2597
+ raw: parsed,
2598
+ };
2599
+ }
2600
+ const plainReply = String(rawText || "").trim();
2601
+ if (!plainReply) {
2602
+ throw new Error("failure explainer returned empty output");
2603
+ }
2604
+ return {
2605
+ classification: "failed",
2606
+ reply: plainReply,
2607
+ next_action: "",
2608
+ raw: null,
2609
+ };
2610
+ }
2611
+
2495
2612
  function normalizeResponderAdjudicationSelectorList(values, allowedSelectors) {
2496
2613
  const allowed = new Set(ensureArray(allowedSelectors).map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase()).filter(Boolean));
2497
2614
  return uniqueOrdered(
@@ -431,6 +431,155 @@ function shouldSendExecutionFailureReply({ triggerDecision, selectedRecord }) {
431
431
  && safeObject(selectedRecord?.parsedArchive).senderIsBot !== true;
432
432
  }
433
433
 
434
+ function classifyExecutionFailureFacts(detail) {
435
+ const normalizedDetail = String(detail || "").trim();
436
+ const networkReset = /ECONNRESET|socket hang up|read ECONNRESET/i.test(normalizedDetail);
437
+ const networkTimeout = /ETIMEDOUT|http timeout|ECONNABORTED|aborted/i.test(normalizedDetail);
438
+ const retryable = networkReset || networkTimeout;
439
+ const base = {
440
+ stage: "execution",
441
+ operation: "runner_execution",
442
+ errorType: retryable
443
+ ? (networkTimeout ? "network_timeout" : "network_reset")
444
+ : "execution_failed",
445
+ retryable,
446
+ artifactCreated: null,
447
+ workItemCreated: null,
448
+ ctxpackUpdated: null,
449
+ partialSuccess: false,
450
+ };
451
+ if (!normalizedDetail) {
452
+ return base;
453
+ }
454
+ if (/permission_mode=read_only|read[_ -]?only/i.test(normalizedDetail)) {
455
+ return {
456
+ ...base,
457
+ stage: "permission_check",
458
+ operation: "route_permission_check",
459
+ errorType: "read_only_route",
460
+ retryable: false,
461
+ };
462
+ }
463
+ if (/reply did not produce an actionable execution contract/i.test(normalizedDetail)) {
464
+ return {
465
+ ...base,
466
+ stage: "execution_contract",
467
+ operation: "response_contract_validation",
468
+ errorType: "missing_actionable_contract",
469
+ retryable: false,
470
+ };
471
+ }
472
+ if (/failed to create work item/i.test(normalizedDetail)) {
473
+ return {
474
+ ...base,
475
+ stage: "work_item_create",
476
+ operation: "workitem.push",
477
+ errorType: retryable
478
+ ? (networkTimeout ? "network_timeout" : "network_reset")
479
+ : "work_item_create_failed",
480
+ workItemCreated: false,
481
+ };
482
+ }
483
+ if (/governance work items/i.test(normalizedDetail)) {
484
+ return {
485
+ ...base,
486
+ stage: "work_item_create",
487
+ operation: "governance_work_item_validation",
488
+ errorType: "governance_work_items_missing",
489
+ retryable: false,
490
+ workItemCreated: false,
491
+ };
492
+ }
493
+ if (/validated project artifacts|reported project artifacts that were not observed|artifact path does not exist/i.test(normalizedDetail)) {
494
+ return {
495
+ ...base,
496
+ stage: "artifact_validation",
497
+ operation: "workspace_artifact_validation",
498
+ errorType: "artifact_validation_failed",
499
+ retryable: false,
500
+ artifactCreated: false,
501
+ };
502
+ }
503
+ if (/thread not found/i.test(normalizedDetail)) {
504
+ return {
505
+ ...base,
506
+ stage: "archive_thread",
507
+ operation: "archive_thread_lookup",
508
+ errorType: "archive_thread_missing",
509
+ retryable,
510
+ };
511
+ }
512
+ if (/ctxpack version_id is missing/i.test(normalizedDetail)) {
513
+ return {
514
+ ...base,
515
+ stage: "ctxpack_update",
516
+ operation: "ctxpack.update",
517
+ errorType: "ctxpack_version_missing",
518
+ retryable: false,
519
+ ctxpackUpdated: false,
520
+ };
521
+ }
522
+ if (/ctxpack\.update requires project ctxpack write access|ctxpack write access|ctxpack update permission|forbidden/i.test(normalizedDetail) && /ctxpack/i.test(normalizedDetail)) {
523
+ return {
524
+ ...base,
525
+ stage: "ctxpack_update",
526
+ operation: "ctxpack.update",
527
+ errorType: "ctxpack_permission_denied",
528
+ retryable: false,
529
+ ctxpackUpdated: false,
530
+ };
531
+ }
532
+ if (/ctxpack/i.test(normalizedDetail) && retryable) {
533
+ return {
534
+ ...base,
535
+ stage: "ctxpack_update",
536
+ operation: "ctxpack.update",
537
+ errorType: networkTimeout ? "network_timeout" : "network_reset",
538
+ retryable: true,
539
+ ctxpackUpdated: false,
540
+ };
541
+ }
542
+ if (/ctxpack/i.test(normalizedDetail)) {
543
+ return {
544
+ ...base,
545
+ stage: "ctxpack_update",
546
+ operation: "ctxpack.update",
547
+ errorType: "ctxpack_update_failed",
548
+ retryable: false,
549
+ ctxpackUpdated: false,
550
+ };
551
+ }
552
+ return base;
553
+ }
554
+
555
+ function buildExecutionFailureFacts(detail, options = {}) {
556
+ const normalizedDetail = String(detail || "").trim();
557
+ const compactDetail = normalizedDetail.replace(/\s+/g, " ").trim();
558
+ const normalizedOptions = safeObject(options);
559
+ const intentType = normalizeHumanIntentType(normalizedOptions.intentType);
560
+ const classified = classifyExecutionFailureFacts(compactDetail);
561
+ return {
562
+ stage: classified.stage,
563
+ operation: classified.operation,
564
+ status: "failed",
565
+ error_type: classified.errorType,
566
+ error_message: compactDetail.slice(0, 400),
567
+ retryable: classified.retryable === true,
568
+ artifact_created: classified.artifactCreated,
569
+ work_item_created: classified.workItemCreated,
570
+ ctxpack_updated: classified.ctxpackUpdated,
571
+ partial_success: classified.partialSuccess === true,
572
+ intent_type: intentType,
573
+ informational_request: isInformationalHumanIntentType(intentType),
574
+ execution_mode: String(normalizedOptions.executionMode || "").trim(),
575
+ role_profile: String(normalizedOptions.roleProfileName || "").trim(),
576
+ conversation_id: String(normalizedOptions.conversationID || "").trim(),
577
+ root_work_item_id: String(normalizedOptions.rootWorkItemID || "").trim(),
578
+ request_message_id: intFromRawAllowZero(normalizedOptions.messageID, 0),
579
+ request_text: String(normalizedOptions.userMessageText || "").trim(),
580
+ };
581
+ }
582
+
434
583
  function buildExecutionFailureReplyText(detail, options = {}) {
435
584
  const normalizedDetail = String(detail || "").trim();
436
585
  const intentType = normalizeHumanIntentType(safeObject(options).intentType);
@@ -4176,6 +4325,9 @@ export async function processRunnerSelectedRecord({
4176
4325
  ...safeObject(buildRunnerExecutionDeps()),
4177
4326
  ...safeObject(deps),
4178
4327
  };
4328
+ const explainExecutionFailureWithAI = typeof executionDeps.explainExecutionFailureWithAI === "function"
4329
+ ? executionDeps.explainExecutionFailureWithAI
4330
+ : null;
4179
4331
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4180
4332
  const normalizedPrecomputedHumanIntent = safeObject(normalizedPrecomputedHumanIntentContext.humanIntent);
4181
4333
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
@@ -4413,9 +4565,36 @@ export async function processRunnerSelectedRecord({
4413
4565
  if (!shouldSendExecutionFailureReply({ triggerDecision: effectiveTriggerDecision, selectedRecord })) {
4414
4566
  return null;
4415
4567
  }
4416
- const replyText = buildExecutionFailureReplyText(detail, {
4568
+ const failureFacts = buildExecutionFailureFacts(detail, {
4417
4569
  intentType: resolvedIntentType,
4570
+ executionMode: effectiveExecutionPlan.mode,
4571
+ roleProfileName: effectiveExecutionPlan.roleProfileName,
4572
+ conversationID: String(conversationContext?.id || "").trim(),
4573
+ rootWorkItemID: firstNonEmptyString([
4574
+ routeState?.active_root_work_item_id,
4575
+ routeState?.last_root_work_item_id,
4576
+ ]),
4577
+ messageID: selectedRecord?.parsedArchive?.messageID,
4578
+ userMessageText: selectedRecord?.parsedArchive?.body,
4418
4579
  });
4580
+ let replyText = "";
4581
+ if (explainExecutionFailureWithAI) {
4582
+ try {
4583
+ const explanation = await Promise.resolve(explainExecutionFailureWithAI({
4584
+ failureFacts,
4585
+ userMessageText: String(selectedRecord?.parsedArchive?.body || "").trim(),
4586
+ botName: String(bot?.name || bot?.username || bot?.id || "").trim(),
4587
+ workspaceDir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
4588
+ env: process.env,
4589
+ }));
4590
+ replyText = String(safeObject(explanation).reply || safeObject(explanation).message || "").trim();
4591
+ } catch {}
4592
+ }
4593
+ if (!replyText) {
4594
+ replyText = buildExecutionFailureReplyText(detail, {
4595
+ intentType: resolvedIntentType,
4596
+ });
4597
+ }
4419
4598
  if (!replyText) {
4420
4599
  return null;
4421
4600
  }
@@ -2229,6 +2229,134 @@ export async function runSelftestRunnerScenarios(push, deps) {
2229
2229
  }
2230
2230
  }
2231
2231
 
2232
+ const originalStateMergeHome = process.env.HOME;
2233
+ const originalStateMergeUserProfile = process.env.USERPROFILE;
2234
+ let runnerStateMergeTempRoot = "";
2235
+ try {
2236
+ runnerStateMergeTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-state-merge-selftest-"));
2237
+ const runnerStateMergeHome = path.join(runnerStateMergeTempRoot, "home");
2238
+ fs.mkdirSync(path.join(runnerStateMergeHome, ".metheus"), { recursive: true });
2239
+ process.env.HOME = runnerStateMergeHome;
2240
+ process.env.USERPROFILE = runnerStateMergeHome;
2241
+ const mergeRoute = normalizeRunnerRoute({
2242
+ name: "telegram-monitor-state-merge",
2243
+ project_id: selftestProjectID,
2244
+ provider: "telegram",
2245
+ role: "monitor",
2246
+ destination_label: "Main Room",
2247
+ });
2248
+ const mergeRouteKey = runnerRouteKey(mergeRoute);
2249
+ saveBotRunnerState({
2250
+ routes: {
2251
+ [mergeRouteKey]: {
2252
+ updated_at: "2026-03-24T06:29:05.000Z",
2253
+ last_request_key: "request-key-597",
2254
+ last_action: "running",
2255
+ conversation_sessions: {
2256
+ "conversation-597": {
2257
+ conversation_id: "conversation-597",
2258
+ status: "open",
2259
+ updated_at: "2026-03-24T06:29:05.000Z",
2260
+ last_activity_at: "2026-03-24T06:29:05.000Z",
2261
+ last_execution_contract_type: "delegation",
2262
+ next_expected_responders: ["ryoai2_bot", "ryoai3_bot"],
2263
+ },
2264
+ },
2265
+ },
2266
+ },
2267
+ sharedInboxes: {},
2268
+ excludedComments: {},
2269
+ requests: {
2270
+ "request-key-597": {
2271
+ request_key: "request-key-597",
2272
+ project_id: selftestProjectID,
2273
+ provider: "telegram",
2274
+ chat_id: "-100123",
2275
+ source_message_id: 597,
2276
+ conversation_id: "conversation-597",
2277
+ normalized_intent: "explanation_query",
2278
+ execution_contract_type: "delegation",
2279
+ execution_contract_actionable: true,
2280
+ next_expected_responders: ["ryoai2_bot", "ryoai3_bot"],
2281
+ status: "running",
2282
+ claimed_by_route: mergeRouteKey,
2283
+ root_work_item_id: "root-work-item-597",
2284
+ root_thread_id: "root-thread-597",
2285
+ updated_at: "2026-03-24T06:29:05.000Z",
2286
+ },
2287
+ },
2288
+ consumedComments: {},
2289
+ });
2290
+ saveBotRunnerState({
2291
+ routes: {
2292
+ [mergeRouteKey]: {
2293
+ updated_at: "2026-03-24T06:29:06.000Z",
2294
+ last_request_key: "request-key-597",
2295
+ last_action: "replied",
2296
+ conversation_sessions: {
2297
+ "conversation-597": {
2298
+ conversation_id: "conversation-597",
2299
+ status: "open",
2300
+ updated_at: "2026-03-24T06:29:06.000Z",
2301
+ },
2302
+ },
2303
+ },
2304
+ },
2305
+ sharedInboxes: {},
2306
+ excludedComments: {},
2307
+ requests: {
2308
+ "request-key-597": {
2309
+ request_key: "request-key-597",
2310
+ project_id: selftestProjectID,
2311
+ provider: "telegram",
2312
+ chat_id: "-100123",
2313
+ source_message_id: 597,
2314
+ conversation_id: "conversation-597",
2315
+ normalized_intent: "general_execution",
2316
+ status: "claimed",
2317
+ claimed_by_route: mergeRouteKey,
2318
+ root_work_item_id: "",
2319
+ root_thread_id: "",
2320
+ next_expected_responders: [],
2321
+ updated_at: "2026-03-24T06:28:30.000Z",
2322
+ },
2323
+ },
2324
+ consumedComments: {},
2325
+ });
2326
+ const mergedState = loadBotRunnerState();
2327
+ const mergedRouteState = safeObject(safeObject(mergedState.routes)[mergeRouteKey]);
2328
+ const mergedSession = safeObject(safeObject(mergedRouteState.conversation_sessions)["conversation-597"]);
2329
+ const mergedRequest = safeObject(safeObject(mergedState.requests)["request-key-597"]);
2330
+ push(
2331
+ "runner_state_save_preserves_newer_request_and_conversation_session",
2332
+ String(mergedRequest.status || "") === "running"
2333
+ && String(mergedRequest.root_work_item_id || "") === "root-work-item-597"
2334
+ && ensureArray(mergedRequest.next_expected_responders).includes("ryoai2_bot")
2335
+ && String(mergedSession.status || "") === "open"
2336
+ && String(mergedSession.last_execution_contract_type || "") === "delegation"
2337
+ && ensureArray(mergedSession.next_expected_responders).includes("ryoai3_bot"),
2338
+ `request_status=${String(mergedRequest.status || "(none)")} root_work_item=${String(mergedRequest.root_work_item_id || "(none)")} session_contract=${String(mergedSession.last_execution_contract_type || "(none)")} next=${ensureArray(mergedSession.next_expected_responders).join(",")}`,
2339
+ );
2340
+ } catch (err) {
2341
+ push("runner_state_save_preserves_newer_request_and_conversation_session", false, String(err?.message || err));
2342
+ } finally {
2343
+ if (typeof originalStateMergeHome === "string") {
2344
+ process.env.HOME = originalStateMergeHome;
2345
+ } else {
2346
+ delete process.env.HOME;
2347
+ }
2348
+ if (typeof originalStateMergeUserProfile === "string") {
2349
+ process.env.USERPROFILE = originalStateMergeUserProfile;
2350
+ } else {
2351
+ delete process.env.USERPROFILE;
2352
+ }
2353
+ if (runnerStateMergeTempRoot) {
2354
+ try {
2355
+ fs.rmSync(runnerStateMergeTempRoot, { recursive: true, force: true });
2356
+ } catch {}
2357
+ }
2358
+ }
2359
+
2232
2360
  const defaultMonitorTriggerPolicy = normalizeRunnerTriggerPolicy({}, { role: "monitor" });
2233
2361
  push(
2234
2362
  "bot_runner_default_monitor_trigger_policy",
@@ -5079,6 +5207,148 @@ export async function runSelftestRunnerScenarios(push, deps) {
5079
5207
  push("single_bot_human_work_request_requires_actionable_contract", false, String(err?.message || err));
5080
5208
  }
5081
5209
 
5210
+ try {
5211
+ let aiCalls = 0;
5212
+ let deliveryCalls = 0;
5213
+ let deliveredText = "";
5214
+ let capturedFailureFacts = null;
5215
+ const processed = await processRunnerSelectedRecord({
5216
+ routeKey: "single-bot-human-work-request-ai-failure-explainer-key",
5217
+ normalizedRoute: normalizeRunnerRoute({
5218
+ name: "telegram-monitor-single-bot-human-work-request-ai-failure-explainer",
5219
+ project_id: selftestProjectID,
5220
+ provider: "telegram",
5221
+ role: "monitor",
5222
+ role_profile: "monitor",
5223
+ destination_id: "dest-1",
5224
+ destination_label: "Main Room",
5225
+ server_bot_name: "RyoAI_bot",
5226
+ server_bot_id: "bot-lead-1",
5227
+ trigger_policy: {
5228
+ mentions_only: true,
5229
+ direct_messages: true,
5230
+ reply_to_bot_messages: true,
5231
+ },
5232
+ archive_policy: {
5233
+ mirror_replies: true,
5234
+ dedupe_inbound: true,
5235
+ dedupe_outbound: true,
5236
+ skip_bot_messages: true,
5237
+ },
5238
+ dry_run_delivery: true,
5239
+ }),
5240
+ selectedRecord: {
5241
+ id: "comment-single-bot-human-work-request-ai-failure-explainer",
5242
+ createdAt: "2026-03-16T00:02:05.500Z",
5243
+ parsedArchive: {
5244
+ kind: "telegram_message",
5245
+ chatID: "-100123",
5246
+ chatType: "supergroup",
5247
+ senderIsBot: false,
5248
+ body: "@RyoAI_bot update the implementation guide now.",
5249
+ mentionUsernames: ["RyoAI_bot"],
5250
+ messageID: 1206,
5251
+ },
5252
+ },
5253
+ pendingOrdered: [],
5254
+ bot: {
5255
+ id: "bot-lead-1",
5256
+ name: "RyoAI_bot",
5257
+ username: "RyoAI_bot",
5258
+ role: "monitor",
5259
+ provider: "telegram",
5260
+ },
5261
+ destination: {
5262
+ id: "dest-1",
5263
+ label: "Main Room",
5264
+ provider: "telegram",
5265
+ chatID: "-100123",
5266
+ },
5267
+ archiveThread: {
5268
+ threadID: "thread-1",
5269
+ workItemID: "work-item-1",
5270
+ },
5271
+ executionPlan: {
5272
+ mode: "role_profile",
5273
+ roleProfileName: "monitor",
5274
+ roleProfile: {
5275
+ client: "sample",
5276
+ model: "",
5277
+ permissionMode: "read_only",
5278
+ reasoningEffort: "low",
5279
+ },
5280
+ workspaceDir: process.cwd(),
5281
+ workspaceSource: "selftest",
5282
+ usedCommandFallback: false,
5283
+ },
5284
+ runtime: {
5285
+ baseURL: "https://example.test",
5286
+ token: "selftest-token",
5287
+ timeoutSeconds: 30,
5288
+ actor: { user_id: "user-1" },
5289
+ },
5290
+ deps: {
5291
+ saveRunnerRouteState: () => {},
5292
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
5293
+ runRunnerAIExecution: async () => {
5294
+ aiCalls += 1;
5295
+ return {
5296
+ skip: false,
5297
+ reply: "I reviewed the request but did not produce a contract.",
5298
+ contract: null,
5299
+ };
5300
+ },
5301
+ explainExecutionFailureWithAI: ({ failureFacts }) => {
5302
+ capturedFailureFacts = safeObject(failureFacts);
5303
+ return {
5304
+ classification: "failed",
5305
+ reply: "AI failure summary",
5306
+ next_action: "retry",
5307
+ };
5308
+ },
5309
+ performLocalBotDelivery: async ({ text }) => {
5310
+ deliveryCalls += 1;
5311
+ deliveredText = String(text || "");
5312
+ return {
5313
+ delivery: { dryRun: true, body: {} },
5314
+ archive: {},
5315
+ };
5316
+ },
5317
+ serializeRunnerTriggerPolicy: (value) => value,
5318
+ serializeRunnerArchivePolicy: (value) => value,
5319
+ buildRunnerExecutionDeps: () => ({
5320
+ analyzeHumanConversationIntentWithAI: async () => ({
5321
+ mode: "single_bot",
5322
+ lead_bot: "ryoai_bot",
5323
+ participants: ["ryoai_bot"],
5324
+ initial_responders: ["ryoai_bot"],
5325
+ allowed_responders: ["ryoai_bot"],
5326
+ summary_bot: "",
5327
+ allow_bot_to_bot: false,
5328
+ reply_expectation: "actionable",
5329
+ }),
5330
+ }),
5331
+ buildRunnerDeliveryDeps: () => ({}),
5332
+ buildRunnerRuntimeDeps: () => ({}),
5333
+ resolveConversationPeerBots: () => [
5334
+ { id: "bot-lead-1", name: "RyoAI_bot" },
5335
+ ],
5336
+ },
5337
+ });
5338
+ push(
5339
+ "single_bot_execution_failure_uses_ai_failure_explainer_when_available",
5340
+ processed.kind === "error"
5341
+ && aiCalls >= 1
5342
+ && deliveryCalls === 1
5343
+ && deliveredText === "AI failure summary"
5344
+ && String(capturedFailureFacts?.error_type || "") === "missing_actionable_contract"
5345
+ && String(capturedFailureFacts?.stage || "") === "execution_contract",
5346
+ `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} delivered=${deliveredText} failure_type=${String(capturedFailureFacts?.error_type || "(none)")} stage=${String(capturedFailureFacts?.stage || "(none)")}`,
5347
+ );
5348
+ } catch (err) {
5349
+ push("single_bot_execution_failure_uses_ai_failure_explainer_when_available", false, String(err?.message || err));
5350
+ }
5351
+
5082
5352
  try {
5083
5353
  let aiCalls = 0;
5084
5354
  const processed = await processRunnerSelectedRecord({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.205",
3
+ "version": "0.2.207",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [