metheus-governance-mcp-cli 0.2.204 → 0.2.206

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
@@ -184,6 +184,7 @@ import {
184
184
  } from "./lib/runner-execution.mjs";
185
185
  import {
186
186
  processRunnerSelectedRecord,
187
+ resolveHumanIntentContext,
187
188
  resolveRunnerResponderAdjudication,
188
189
  resolveRunnerStartupLoopAdjudication,
189
190
  selectRunnerPendingWork,
@@ -2064,14 +2065,176 @@ function saveBotRunnerState(nextState) {
2064
2065
  try {
2065
2066
  current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
2066
2067
  } catch {}
2068
+ const stateEntryTimestampMs = (...values) => {
2069
+ for (const value of values) {
2070
+ const ms = Date.parse(String(value || "").trim());
2071
+ if (Number.isFinite(ms)) {
2072
+ return ms;
2073
+ }
2074
+ }
2075
+ return 0;
2076
+ };
2077
+ const mergeRunnerStateRoutes = (currentRoutesRaw, nextRoutesRaw) => {
2078
+ const currentRoutes = safeObject(currentRoutesRaw);
2079
+ const nextRoutes = safeObject(nextRoutesRaw);
2080
+ const merged = {
2081
+ ...currentRoutes,
2082
+ };
2083
+ const mergeConversationSessions = (currentSessionsRaw, nextSessionsRaw) => {
2084
+ const currentSessions = safeObject(currentSessionsRaw);
2085
+ const nextSessions = safeObject(nextSessionsRaw);
2086
+ const mergedSessions = {
2087
+ ...currentSessions,
2088
+ };
2089
+ for (const [conversationID, nextSessionRaw] of Object.entries(nextSessions)) {
2090
+ const currentSession = safeObject(currentSessions[conversationID]);
2091
+ const nextSession = safeObject(nextSessionRaw);
2092
+ if (!Object.keys(currentSession).length) {
2093
+ mergedSessions[conversationID] = nextSession;
2094
+ continue;
2095
+ }
2096
+ const currentMs = stateEntryTimestampMs(
2097
+ currentSession.updated_at,
2098
+ currentSession.last_activity_at,
2099
+ currentSession.closed_at,
2100
+ currentSession.started_at,
2101
+ currentSession.expires_at,
2102
+ );
2103
+ const nextMs = stateEntryTimestampMs(
2104
+ nextSession.updated_at,
2105
+ nextSession.last_activity_at,
2106
+ nextSession.closed_at,
2107
+ nextSession.started_at,
2108
+ nextSession.expires_at,
2109
+ );
2110
+ mergedSessions[conversationID] = nextMs >= currentMs
2111
+ ? {
2112
+ ...currentSession,
2113
+ ...nextSession,
2114
+ }
2115
+ : {
2116
+ ...nextSession,
2117
+ ...currentSession,
2118
+ };
2119
+ }
2120
+ return mergedSessions;
2121
+ };
2122
+ for (const [routeKey, nextRouteRaw] of Object.entries(nextRoutes)) {
2123
+ const currentRoute = safeObject(currentRoutes[routeKey]);
2124
+ const nextRoute = safeObject(nextRouteRaw);
2125
+ if (!Object.keys(currentRoute).length) {
2126
+ merged[routeKey] = nextRoute;
2127
+ continue;
2128
+ }
2129
+ const preferredRoute = prefersRunnerStateRecord(nextRoute, currentRoute) ? nextRoute : currentRoute;
2130
+ const fallbackRoute = preferredRoute === nextRoute ? currentRoute : nextRoute;
2131
+ merged[routeKey] = cleanupRunnerStateRecord({
2132
+ ...mergeRunnerStateRecords(preferredRoute, fallbackRoute),
2133
+ conversation_sessions: mergeConversationSessions(
2134
+ currentRoute.conversation_sessions,
2135
+ nextRoute.conversation_sessions,
2136
+ ),
2137
+ });
2138
+ }
2139
+ return merged;
2140
+ };
2141
+ const mergeRunnerStateRequests = (currentRequestsRaw, nextRequestsRaw) => {
2142
+ const currentRequests = normalizeBotRunnerRequests(currentRequestsRaw);
2143
+ const nextRequests = normalizeBotRunnerRequests(nextRequestsRaw);
2144
+ const merged = {
2145
+ ...currentRequests,
2146
+ };
2147
+ for (const [requestKey, nextRequestRaw] of Object.entries(nextRequests)) {
2148
+ const currentRequest = safeObject(currentRequests[requestKey]);
2149
+ const nextRequest = safeObject(nextRequestRaw);
2150
+ const currentMs = stateEntryTimestampMs(
2151
+ currentRequest.updated_at,
2152
+ currentRequest.completed_at,
2153
+ currentRequest.closed_at,
2154
+ currentRequest.claimed_at,
2155
+ );
2156
+ const nextMs = stateEntryTimestampMs(
2157
+ nextRequest.updated_at,
2158
+ nextRequest.completed_at,
2159
+ nextRequest.closed_at,
2160
+ nextRequest.claimed_at,
2161
+ );
2162
+ if (!Object.keys(currentRequest).length) {
2163
+ merged[requestKey] = nextRequest;
2164
+ continue;
2165
+ }
2166
+ merged[requestKey] = nextMs >= currentMs
2167
+ ? {
2168
+ ...currentRequest,
2169
+ ...nextRequest,
2170
+ }
2171
+ : currentRequest;
2172
+ }
2173
+ return normalizeBotRunnerRequests(merged);
2174
+ };
2175
+ const mergeRunnerStateExcludedComments = (currentExcludedRaw, nextExcludedRaw) => {
2176
+ const currentExcluded = normalizeBotRunnerExcludedComments(currentExcludedRaw);
2177
+ const nextExcluded = normalizeBotRunnerExcludedComments(nextExcludedRaw);
2178
+ const merged = {
2179
+ ...currentExcluded,
2180
+ };
2181
+ for (const [commentID, nextEntryRaw] of Object.entries(nextExcluded)) {
2182
+ const currentEntry = safeObject(currentExcluded[commentID]);
2183
+ const nextEntry = safeObject(nextEntryRaw);
2184
+ const currentMs = stateEntryTimestampMs(currentEntry.excluded_at);
2185
+ const nextMs = stateEntryTimestampMs(nextEntry.excluded_at);
2186
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2187
+ merged[commentID] = {
2188
+ ...currentEntry,
2189
+ ...nextEntry,
2190
+ };
2191
+ }
2192
+ }
2193
+ return normalizeBotRunnerExcludedComments(merged);
2194
+ };
2195
+ const mergeRunnerStateConsumedComments = (currentConsumedRaw, nextConsumedRaw) => {
2196
+ const currentConsumed = normalizeBotRunnerConsumedComments(currentConsumedRaw);
2197
+ const nextConsumed = normalizeBotRunnerConsumedComments(nextConsumedRaw);
2198
+ const merged = {
2199
+ ...currentConsumed,
2200
+ };
2201
+ for (const [commentID, nextEntryRaw] of Object.entries(nextConsumed)) {
2202
+ const currentEntry = safeObject(currentConsumed[commentID]);
2203
+ const nextEntry = safeObject(nextEntryRaw);
2204
+ const currentMs = stateEntryTimestampMs(currentEntry.consumed_at);
2205
+ const nextMs = stateEntryTimestampMs(nextEntry.consumed_at);
2206
+ if (!Object.keys(currentEntry).length || nextMs >= currentMs) {
2207
+ merged[commentID] = {
2208
+ ...currentEntry,
2209
+ ...nextEntry,
2210
+ };
2211
+ }
2212
+ }
2213
+ return normalizeBotRunnerConsumedComments(merged);
2214
+ };
2067
2215
  const payload = {
2068
2216
  version: 1,
2069
2217
  updated_at: new Date().toISOString(),
2070
- routes: safeObject(nextState?.routes ?? current.routes),
2071
- shared_inboxes: safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes ?? current.shared_inboxes ?? current.sharedInboxes),
2072
- excluded_comments: normalizeBotRunnerExcludedComments(nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments),
2073
- requests: normalizeBotRunnerRequests(nextState?.requests ?? current.requests),
2074
- consumed_comments: normalizeBotRunnerConsumedComments(nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments),
2218
+ routes: mergeRunnerStateRoutes(
2219
+ current.routes,
2220
+ nextState?.routes ?? current.routes,
2221
+ ),
2222
+ shared_inboxes: {
2223
+ ...safeObject(current.shared_inboxes ?? current.sharedInboxes),
2224
+ ...safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes),
2225
+ },
2226
+ excluded_comments: mergeRunnerStateExcludedComments(
2227
+ current.excluded_comments ?? current.excludedComments,
2228
+ nextState?.excludedComments ?? nextState?.excluded_comments ?? current.excluded_comments ?? current.excludedComments,
2229
+ ),
2230
+ requests: mergeRunnerStateRequests(
2231
+ current.requests,
2232
+ nextState?.requests ?? current.requests,
2233
+ ),
2234
+ consumed_comments: mergeRunnerStateConsumedComments(
2235
+ current.consumed_comments ?? current.consumedComments,
2236
+ nextState?.consumedComments ?? nextState?.consumed_comments ?? current.consumed_comments ?? current.consumedComments,
2237
+ ),
2075
2238
  };
2076
2239
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
2077
2240
  fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -2506,6 +2669,89 @@ function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2506
2669
  });
2507
2670
  }
2508
2671
 
2672
+ function findScopedConversationSessionState(state, normalizedRoute, conversationIDRaw = "") {
2673
+ const conversationID = String(conversationIDRaw || "").trim();
2674
+ if (!conversationID) {
2675
+ return {};
2676
+ }
2677
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
2678
+ const nextRoutes = safeObject(state?.routes);
2679
+ const candidates = [];
2680
+ const seenRouteKeys = new Set();
2681
+ const pushCandidateRoute = (routeRaw) => {
2682
+ const routeObject = safeObject(routeRaw);
2683
+ if (!Object.keys(routeObject).length) {
2684
+ return;
2685
+ }
2686
+ const candidateRoute = normalizeRunnerRoute(routeObject);
2687
+ const candidateRouteKey = runnerRouteKey(candidateRoute);
2688
+ if (seenRouteKeys.has(candidateRouteKey)) {
2689
+ return;
2690
+ }
2691
+ seenRouteKeys.add(candidateRouteKey);
2692
+ candidates.push({
2693
+ route: candidateRoute,
2694
+ routeKey: candidateRouteKey,
2695
+ routeState: safeObject(nextRoutes[candidateRouteKey]),
2696
+ });
2697
+ };
2698
+ pushCandidateRoute(normalizedRoute);
2699
+ for (const candidateRouteRaw of ensureArray(config.routes)) {
2700
+ if (!runnerRouteMatchesProjectConversationScope(candidateRouteRaw, normalizedRoute)) {
2701
+ continue;
2702
+ }
2703
+ pushCandidateRoute(candidateRouteRaw);
2704
+ }
2705
+ const ranked = candidates
2706
+ .map((candidate) => {
2707
+ const session = safeObject(safeObject(candidate.routeState.conversation_sessions)[conversationID]);
2708
+ if (!Object.keys(session).length) {
2709
+ return null;
2710
+ }
2711
+ const status = String(session.status || "").trim().toLowerCase();
2712
+ const lastActivity = firstNonEmptyString([session.last_activity_at, session.closed_at, session.started_at]);
2713
+ return {
2714
+ ...candidate,
2715
+ session,
2716
+ status,
2717
+ lastActivity,
2718
+ };
2719
+ })
2720
+ .filter(Boolean)
2721
+ .sort((left, right) => {
2722
+ if (left.status !== right.status) {
2723
+ if (left.status === "open") return -1;
2724
+ if (right.status === "open") return 1;
2725
+ }
2726
+ if (left.lastActivity && right.lastActivity && left.lastActivity !== right.lastActivity) {
2727
+ return left.lastActivity < right.lastActivity ? 1 : -1;
2728
+ }
2729
+ return String(left.routeKey || "").localeCompare(String(right.routeKey || ""));
2730
+ });
2731
+ return safeObject(ranked[0]);
2732
+ }
2733
+
2734
+ function sessionAllowsConversationResponder(sessionRaw, responderSelectorRaw = "") {
2735
+ const session = safeObject(sessionRaw);
2736
+ const responderSelector = normalizeTelegramMentionUsername(responderSelectorRaw);
2737
+ if (!responderSelector) {
2738
+ return false;
2739
+ }
2740
+ if (String(session.status || "").trim().toLowerCase() !== "open") {
2741
+ return false;
2742
+ }
2743
+ const nextExpectedResponders = ensureArray(session.next_expected_responders)
2744
+ .map((value) => normalizeTelegramMentionUsername(value))
2745
+ .filter(Boolean);
2746
+ if (nextExpectedResponders.length > 0) {
2747
+ return nextExpectedResponders.includes(responderSelector);
2748
+ }
2749
+ const allowedResponders = ensureArray(session.allowed_responders)
2750
+ .map((value) => normalizeTelegramMentionUsername(value))
2751
+ .filter(Boolean);
2752
+ return allowedResponders.includes(responderSelector);
2753
+ }
2754
+
2509
2755
  function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
2510
2756
  const chatID = String(selectors.chatID || "").trim();
2511
2757
  const messageID = intFromRawAllowZero(selectors.messageID, 0);
@@ -2986,6 +3232,7 @@ async function claimRunnerRequestForHumanComment({
2986
3232
  selectedRecord,
2987
3233
  selectedBotUsernames = [],
2988
3234
  normalizedIntent = "",
3235
+ sharedHumanIntent = null,
2989
3236
  runtime = null,
2990
3237
  archiveThreadID = "",
2991
3238
  }) {
@@ -3039,6 +3286,7 @@ async function claimRunnerRequestForHumanComment({
3039
3286
  }
3040
3287
  const requests = normalizeBotRunnerRequests(stateForClaim.requests);
3041
3288
  const existing = safeObject(requests[requestKey]);
3289
+ const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
3042
3290
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
3043
3291
  let sharedConversationSource = currentMessageID > 0
3044
3292
  ? pickRunnerSharedConversationSourceRequest(
@@ -3097,20 +3345,22 @@ async function claimRunnerRequestForHumanComment({
3097
3345
  conversation_id: resolvedConversationID,
3098
3346
  selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
3099
3347
  conversation_intent_mode: String(
3100
- existing.conversation_intent_mode || sharedConversationSource.conversation_intent_mode || referencedRequest.conversation_intent_mode || "",
3348
+ existing.conversation_intent_mode || sharedConversationSource.conversation_intent_mode || referencedRequest.conversation_intent_mode || normalizedSharedHumanIntent.intentMode || "",
3101
3349
  ).trim().toLowerCase(),
3102
3350
  conversation_lead_bot: normalizeTelegramMentionUsername(
3103
- existing.conversation_lead_bot || sharedConversationSource.conversation_lead_bot || referencedRequest.conversation_lead_bot,
3351
+ existing.conversation_lead_bot || sharedConversationSource.conversation_lead_bot || referencedRequest.conversation_lead_bot || normalizedSharedHumanIntent.leadBotSelector,
3104
3352
  ),
3105
3353
  conversation_summary_bot: normalizeTelegramMentionUsername(
3106
- existing.conversation_summary_bot || sharedConversationSource.conversation_summary_bot || referencedRequest.conversation_summary_bot,
3354
+ existing.conversation_summary_bot || sharedConversationSource.conversation_summary_bot || referencedRequest.conversation_summary_bot || normalizedSharedHumanIntent.summaryBotSelector,
3107
3355
  ),
3108
3356
  conversation_participants: uniqueOrderedStrings(
3109
3357
  ensureArray(existing.conversation_participants).length
3110
3358
  ? existing.conversation_participants
3111
3359
  : ensureArray(sharedConversationSource.conversation_participants).length
3112
3360
  ? sharedConversationSource.conversation_participants
3113
- : referencedRequest.conversation_participants,
3361
+ : ensureArray(referencedRequest.conversation_participants).length
3362
+ ? referencedRequest.conversation_participants
3363
+ : normalizedSharedHumanIntent.participantSelectors,
3114
3364
  normalizeTelegramMentionUsername,
3115
3365
  ),
3116
3366
  conversation_initial_responders: uniqueOrderedStrings(
@@ -3118,7 +3368,9 @@ async function claimRunnerRequestForHumanComment({
3118
3368
  ? existing.conversation_initial_responders
3119
3369
  : ensureArray(sharedConversationSource.conversation_initial_responders).length
3120
3370
  ? sharedConversationSource.conversation_initial_responders
3121
- : referencedRequest.conversation_initial_responders,
3371
+ : ensureArray(referencedRequest.conversation_initial_responders).length
3372
+ ? referencedRequest.conversation_initial_responders
3373
+ : normalizedSharedHumanIntent.initialResponderSelectors,
3122
3374
  normalizeTelegramMentionUsername,
3123
3375
  ),
3124
3376
  conversation_allowed_responders: uniqueOrderedStrings(
@@ -3126,14 +3378,17 @@ async function claimRunnerRequestForHumanComment({
3126
3378
  ? existing.conversation_allowed_responders
3127
3379
  : ensureArray(sharedConversationSource.conversation_allowed_responders).length
3128
3380
  ? sharedConversationSource.conversation_allowed_responders
3129
- : referencedRequest.conversation_allowed_responders,
3381
+ : ensureArray(referencedRequest.conversation_allowed_responders).length
3382
+ ? referencedRequest.conversation_allowed_responders
3383
+ : normalizedSharedHumanIntent.allowedResponderSelectors,
3130
3384
  normalizeTelegramMentionUsername,
3131
3385
  ),
3132
3386
  conversation_allow_bot_to_bot: existing.conversation_allow_bot_to_bot === true
3133
3387
  || sharedConversationSource.conversation_allow_bot_to_bot === true
3134
- || referencedRequest.conversation_allow_bot_to_bot === true,
3388
+ || referencedRequest.conversation_allow_bot_to_bot === true
3389
+ || normalizedSharedHumanIntent.allowBotToBot === true,
3135
3390
  conversation_reply_expectation: String(
3136
- existing.conversation_reply_expectation || sharedConversationSource.conversation_reply_expectation || referencedRequest.conversation_reply_expectation || "",
3391
+ existing.conversation_reply_expectation || sharedConversationSource.conversation_reply_expectation || referencedRequest.conversation_reply_expectation || normalizedSharedHumanIntent.replyExpectation || "",
3137
3392
  ).trim().toLowerCase(),
3138
3393
  execution_contract_type: String(
3139
3394
  existing.execution_contract_type || sharedConversationSource.execution_contract_type || referencedRequest.execution_contract_type || "",
@@ -3154,7 +3409,9 @@ async function claimRunnerRequestForHumanComment({
3154
3409
  ? existing.next_expected_responders
3155
3410
  : ensureArray(sharedConversationSource.next_expected_responders).length
3156
3411
  ? sharedConversationSource.next_expected_responders
3157
- : referencedRequest.next_expected_responders,
3412
+ : ensureArray(referencedRequest.next_expected_responders).length
3413
+ ? referencedRequest.next_expected_responders
3414
+ : normalizedSharedHumanIntent.initialResponderSelectors,
3158
3415
  normalizeTelegramMentionUsername,
3159
3416
  ),
3160
3417
  normalized_intent: resolvedNormalizedIntent,
@@ -3883,11 +4140,80 @@ function resolveRunnerContinuationRequestForBotReply({
3883
4140
  };
3884
4141
  }
3885
4142
  const currentState = loadBotRunnerState();
3886
- const requests = findRunnerRequestsForScope(currentState, normalizedRoute, {
4143
+ let requests = findRunnerRequestsForScope(currentState, normalizedRoute, {
3887
4144
  conversationID,
3888
4145
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3889
4146
  }).filter((entry) => isActiveRunnerRequestStatus(entry.status));
3890
- const request = safeObject(requests[0]);
4147
+ let request = safeObject(requests[0]);
4148
+ if (!Object.keys(request).length) {
4149
+ const sessionMatch = findScopedConversationSessionState(currentState, normalizedRoute, conversationID);
4150
+ const session = safeObject(sessionMatch.session);
4151
+ const fallbackRequestKey = String(
4152
+ safeObject(sessionMatch.routeState).active_request_key
4153
+ || safeObject(sessionMatch.routeState).last_request_key
4154
+ || "",
4155
+ ).trim();
4156
+ if (
4157
+ fallbackRequestKey
4158
+ && String(session.status || "").trim().toLowerCase() === "open"
4159
+ ) {
4160
+ const fallbackRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[fallbackRequestKey]);
4161
+ const nowISO = new Date().toISOString();
4162
+ const seedRequest = Object.keys(fallbackRequest).length
4163
+ ? fallbackRequest
4164
+ : {
4165
+ request_key: fallbackRequestKey,
4166
+ project_id: String(normalizedRoute?.projectID || "").trim(),
4167
+ provider: String(normalizedRoute?.provider || "").trim(),
4168
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
4169
+ conversation_id: conversationID,
4170
+ selected_bot_usernames: ensureArray(session.participants),
4171
+ conversation_allowed_responders: ensureArray(session.allowed_responders),
4172
+ conversation_intent_mode: String(session.intent_mode || "").trim().toLowerCase(),
4173
+ conversation_lead_bot: normalizeTelegramMentionUsername(session.lead_bot_username),
4174
+ conversation_summary_bot: normalizeTelegramMentionUsername(session.summary_bot_username),
4175
+ conversation_participants: ensureArray(session.participants),
4176
+ conversation_initial_responders: ensureArray(session.initial_responders),
4177
+ conversation_allow_bot_to_bot: session.allow_bot_to_bot === true,
4178
+ conversation_reply_expectation: "",
4179
+ execution_contract_type: String(session.last_execution_contract_type || "").trim().toLowerCase(),
4180
+ execution_contract_actionable: session.last_execution_contract_actionable === true,
4181
+ execution_contract_targets: ensureArray(session.last_execution_contract_targets),
4182
+ next_expected_responders: ensureArray(session.next_expected_responders),
4183
+ normalized_intent: String(safeObject(sessionMatch.routeState).last_intent_type || "").trim().toLowerCase(),
4184
+ status: "running",
4185
+ claimed_by_route: String(sessionMatch.routeKey || "").trim(),
4186
+ claimed_at: firstNonEmptyString([session.started_at, nowISO]),
4187
+ started_at: firstNonEmptyString([session.started_at, nowISO]),
4188
+ root_work_item_id: String(
4189
+ safeObject(sessionMatch.routeState).active_root_work_item_id
4190
+ || safeObject(sessionMatch.routeState).last_root_work_item_id
4191
+ || "",
4192
+ ).trim(),
4193
+ root_work_item_title: String(
4194
+ safeObject(sessionMatch.routeState).active_root_work_item_title
4195
+ || safeObject(sessionMatch.routeState).last_root_work_item_title
4196
+ || "",
4197
+ ).trim(),
4198
+ root_work_item_status: String(
4199
+ safeObject(sessionMatch.routeState).active_root_work_item_status
4200
+ || safeObject(sessionMatch.routeState).last_root_work_item_status
4201
+ || "",
4202
+ ).trim().toLowerCase(),
4203
+ };
4204
+ const seededRequest = upsertRunnerRequest(currentState, fallbackRequestKey, seedRequest);
4205
+ currentState.requests = seededRequest.requests;
4206
+ request = safeObject(seededRequest.request);
4207
+ saveBotRunnerState({
4208
+ routes: currentState.routes,
4209
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
4210
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
4211
+ requests: currentState.requests,
4212
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
4213
+ });
4214
+ requests = [request];
4215
+ }
4216
+ }
3891
4217
  if (!Object.keys(request).length) {
3892
4218
  return {
3893
4219
  ok: false,
@@ -4137,7 +4463,10 @@ function cleanupBotRunnerRequestState({
4137
4463
  && String(entry.conversation_id || "").trim() === String(conversationID || "").trim()
4138
4464
  && isActiveRunnerRequestStatus(entry.status)
4139
4465
  ));
4140
- if (!expired && activeRequests.length > 0) {
4466
+ const pendingContinuationResponders = ensureArray(session.next_expected_responders)
4467
+ .map((value) => normalizeTelegramMentionUsername(value))
4468
+ .filter(Boolean);
4469
+ if (!expired && (activeRequests.length > 0 || pendingContinuationResponders.length > 0)) {
4141
4470
  continue;
4142
4471
  }
4143
4472
  const closedReason = expired ? "expired_session" : "orphaned_open_session";
@@ -7475,7 +7804,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7475
7804
  && String(entry.conversation_id || "").trim() === conversationID
7476
7805
  && isActiveRunnerRequestStatus(entry.status)
7477
7806
  ));
7478
- return activeRequests.length > 0;
7807
+ if (activeRequests.length > 0) {
7808
+ return true;
7809
+ }
7810
+ const sessionMatch = findScopedConversationSessionState(latestRunnerState, normalizedRoute, conversationID);
7811
+ return sessionAllowsConversationResponder(sessionMatch.session, currentBotSelector);
7479
7812
  }
7480
7813
  return true;
7481
7814
  });
@@ -7592,7 +7925,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7592
7925
  }
7593
7926
  return null;
7594
7927
  };
7595
- const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = []) => {
7928
+ const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = [], sharedHumanIntent = null) => {
7596
7929
  const parsed = safeObject(selectedRecord?.parsedArchive);
7597
7930
  const kind = String(parsed.kind || "").trim().toLowerCase();
7598
7931
  if (kind === "bot_reply") {
@@ -7622,6 +7955,8 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7622
7955
  routeKey,
7623
7956
  selectedRecord,
7624
7957
  selectedBotUsernames: selectedResponderSelectors,
7958
+ normalizedIntent: String(safeObject(sharedHumanIntent).intentType || "").trim().toLowerCase(),
7959
+ sharedHumanIntent,
7625
7960
  runtime,
7626
7961
  archiveThreadID: archiveThread.threadID,
7627
7962
  });
@@ -7679,17 +8014,26 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7679
8014
  skippedRecords.push(startupLoopSkipped.skippedRecord);
7680
8015
  continue;
7681
8016
  }
8017
+ const routingExecutionDeps = {
8018
+ ...buildRunnerExecutionDeps(),
8019
+ managedConversationBots,
8020
+ resolveConversationPeerBots: resolveRunnerConversationPeers,
8021
+ };
8022
+ const sharedHumanIntentContext = await resolveHumanIntentContext({
8023
+ selectedRecord,
8024
+ normalizedRoute,
8025
+ bot,
8026
+ executionPlan,
8027
+ deps: routingExecutionDeps,
8028
+ });
7682
8029
  const adjudication = await resolveRunnerResponderAdjudication({
7683
8030
  selectedRecord,
7684
8031
  pendingOrdered: pending.ordered,
7685
8032
  normalizedRoute,
7686
8033
  bot,
7687
8034
  executionPlan,
7688
- deps: {
7689
- ...buildRunnerExecutionDeps(),
7690
- managedConversationBots,
7691
- resolveConversationPeerBots: resolveRunnerConversationPeers,
7692
- },
8035
+ deps: routingExecutionDeps,
8036
+ precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
7693
8037
  });
7694
8038
  const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
7695
8039
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
@@ -7710,7 +8054,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7710
8054
  });
7711
8055
  continue;
7712
8056
  }
7713
- const requestClaim = await prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
8057
+ const requestClaim = await prepareRunnerRequestClaim(
8058
+ selectedRecord,
8059
+ adjudication.selected_bot_usernames,
8060
+ safeObject(sharedHumanIntentContext).humanIntent || null,
8061
+ );
7714
8062
  if (!requestClaim.ok) {
7715
8063
  await syncRunnerRequestLedgerForProjectToServer({
7716
8064
  normalizedRoute,
@@ -7836,6 +8184,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7836
8184
  requestKey: String(requestClaim.requestKey || "").trim(),
7837
8185
  triggerDecision,
7838
8186
  responderAdjudication: adjudication,
8187
+ humanIntentContext: sharedHumanIntentContext,
7839
8188
  },
7840
8189
  };
7841
8190
  }
@@ -7918,17 +8267,26 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7918
8267
  skippedRecords.push(startupLoopSkipped.skippedRecord);
7919
8268
  continue;
7920
8269
  }
8270
+ const routingExecutionDeps = {
8271
+ ...buildRunnerExecutionDeps(),
8272
+ managedConversationBots,
8273
+ resolveConversationPeerBots: resolveRunnerConversationPeers,
8274
+ };
8275
+ const sharedHumanIntentContext = await resolveHumanIntentContext({
8276
+ selectedRecord,
8277
+ normalizedRoute,
8278
+ bot,
8279
+ executionPlan,
8280
+ deps: routingExecutionDeps,
8281
+ });
7921
8282
  const inlineAdjudication = await resolveRunnerResponderAdjudication({
7922
8283
  selectedRecord,
7923
8284
  pendingOrdered: pending.ordered,
7924
8285
  normalizedRoute,
7925
8286
  bot,
7926
8287
  executionPlan,
7927
- deps: {
7928
- ...buildRunnerExecutionDeps(),
7929
- managedConversationBots,
7930
- resolveConversationPeerBots: resolveRunnerConversationPeers,
7931
- },
8288
+ deps: routingExecutionDeps,
8289
+ precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
7932
8290
  });
7933
8291
  const inlineCurrentBotSelected = ensureArray(inlineAdjudication.selected_bot_usernames)
7934
8292
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
@@ -7950,7 +8308,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7950
8308
  continue;
7951
8309
  }
7952
8310
  const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
7953
- const requestClaim = await prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
8311
+ const requestClaim = await prepareRunnerRequestClaim(
8312
+ selectedRecord,
8313
+ inlineAdjudication.selected_bot_usernames,
8314
+ safeObject(sharedHumanIntentContext).humanIntent || null,
8315
+ );
7954
8316
  if (!requestClaim.ok) {
7955
8317
  await syncRunnerRequestLedgerForProjectToServer({
7956
8318
  normalizedRoute,
@@ -8096,6 +8458,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8096
8458
  triggerDecision,
8097
8459
  responderAdjudication: inlineAdjudication,
8098
8460
  persistedHumanIntentRequest: claimedRequest,
8461
+ precomputedHumanIntentContext: sharedHumanIntentContext,
8099
8462
  deps: {
8100
8463
  saveRunnerRouteState,
8101
8464
  startRunnerTypingHeartbeat,
@@ -10465,6 +10828,7 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
10465
10828
  triggerDecision: deferredExecution.triggerDecision,
10466
10829
  responderAdjudication: deferredExecution.responderAdjudication,
10467
10830
  persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
10831
+ precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
10468
10832
  deps: {
10469
10833
  saveRunnerRouteState,
10470
10834
  startRunnerTypingHeartbeat,
@@ -14951,6 +15315,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
14951
15315
  safeObject,
14952
15316
  normalizeRunnerTriggerPolicy,
14953
15317
  evaluateTelegramRunnerTrigger,
15318
+ resolveRunnerResponderAdjudication,
14954
15319
  selectPendingArchiveComments,
14955
15320
  selectRunnerPendingWork,
14956
15321
  processRunnerSelectedRecord,
@@ -2067,7 +2067,7 @@ function buildConversationIntentAnalysisPrompt({
2067
2067
  "You are a conversation intent contract parser for a public Telegram room with managed bots.",
2068
2068
  "Infer the human's intended bot participation contract from the human message only.",
2069
2069
  "Do not infer from prior bot replies. Do not invent bots outside managed_bots.",
2070
- "Be conservative. If the request is ambiguous, choose single_bot and keep only the directly addressed bot as the responder.",
2070
+ "Be conservative only when the human truly does not indicate collaboration. If the message clearly asks multiple managed bots to discuss, review, brainstorm, compare perspectives, or talk together, do not collapse it to single_bot.",
2071
2071
  "Also decide whether the human is asking for immediate execution/work now or only asking for information/explanation.",
2072
2072
  "Also classify the current message intent type.",
2073
2073
  "",
@@ -2092,12 +2092,13 @@ function buildConversationIntentAnalysisPrompt({
2092
2092
  "- intent_type=bot_role_query for asking about bot roles, bot ownership, or how managed bots are meant to collaborate.",
2093
2093
  "- intent_type=workspace_query for asking about the current workspace, project folder, or working directory.",
2094
2094
  "- intent_type=artifact_location_query for asking where a file, document, pdf, guide, or artifact is located.",
2095
- "- intent_type=explanation_query for explanation, clarification, or informational questions that do not require execution.",
2095
+ "- intent_type=explanation_query for explanation, clarification, informational questions, discussion requests, review requests, brainstorming requests, or requests for bots to talk together about a topic when no concrete work execution is explicitly requested.",
2096
2096
  "- intent_type=ctxpack_mutation for requests to create/update project guidance, instructions, rules, policies, or other ctxpack-backed source documents now.",
2097
2097
  "- intent_type=workitem_mutation for requests to create/update actionable work items, backlog tasks, or task breakdowns now.",
2098
- "- intent_type=general_execution for other concrete work requests that should execute now.",
2098
+ "- intent_type=general_execution only for concrete work requests that should execute now, such as creating/updating files, running tools/commands, modifying project artifacts, or producing validated deliverables.",
2099
+ "- If the human asks bots to discuss, debate, review, brainstorm, compare opinions, or hold a conversation about a topic, default to reply_expectation=informational unless they explicitly ask for concrete execution/output now.",
2099
2100
  "- reply_expectation=actionable when the human is asking the bot(s) to actually do work now, produce concrete results, create/update files, delegate concrete tasks, or otherwise execute immediately.",
2100
- "- reply_expectation=informational when the human is only asking for explanation, status, location, clarification, or other non-execution information.",
2101
+ "- reply_expectation=informational when the human is only asking for explanation, status, location, clarification, discussion, review, brainstorming, or other non-execution information.",
2101
2102
  "",
2102
2103
  `managed_bots=${JSON.stringify(bots)}`,
2103
2104
  `human_message=${JSON.stringify(String(messageText || "").trim())}`,
@@ -1004,7 +1004,7 @@ function buildHumanIntentFromPersistedRunnerRequest({
1004
1004
  };
1005
1005
  }
1006
1006
 
1007
- async function resolveHumanIntentContext({
1007
+ export async function resolveHumanIntentContext({
1008
1008
  selectedRecord,
1009
1009
  normalizedRoute,
1010
1010
  bot,
@@ -3957,6 +3957,7 @@ export async function resolveRunnerResponderAdjudication({
3957
3957
  bot,
3958
3958
  executionPlan,
3959
3959
  deps,
3960
+ precomputedHumanIntent = null,
3960
3961
  }) {
3961
3962
  const cacheKey = buildRunnerResponderAdjudicationCacheKey({ normalizedRoute, selectedRecord });
3962
3963
  if (runnerResponderAdjudicationPromises.has(cacheKey)) {
@@ -3972,6 +3973,11 @@ export async function resolveRunnerResponderAdjudication({
3972
3973
  bot,
3973
3974
  deps,
3974
3975
  });
3976
+ const managedBotSelectors = new Set(
3977
+ managedBots
3978
+ .map((entry) => normalizeMentionSelector(entry.username))
3979
+ .filter(Boolean),
3980
+ );
3975
3981
  const triggerFacts = {
3976
3982
  message_kind: String(safeObject(selectedRecord?.parsedArchive).kind || "").trim(),
3977
3983
  chat_type: String(safeObject(selectedRecord?.parsedArchive).chatType || "").trim(),
@@ -3987,6 +3993,47 @@ export async function resolveRunnerResponderAdjudication({
3987
3993
  trigger_reason: String(entry.trigger_reason || "").trim(),
3988
3994
  })),
3989
3995
  };
3996
+ const humanIntent = safeObject(precomputedHumanIntent);
3997
+ const contractInitialResponders = uniqueOrdered(
3998
+ ensureArray(humanIntent.initialResponderSelectors)
3999
+ .map((value) => normalizeMentionSelector(value))
4000
+ .filter((value) => value && managedBotSelectors.has(value)),
4001
+ );
4002
+ const contractAllowedResponders = uniqueOrdered(
4003
+ ensureArray(humanIntent.allowedResponderSelectors)
4004
+ .map((value) => normalizeMentionSelector(value))
4005
+ .filter((value) => value && managedBotSelectors.has(value)),
4006
+ );
4007
+ const contractParticipants = uniqueOrdered(
4008
+ ensureArray(humanIntent.participantSelectors)
4009
+ .map((value) => normalizeMentionSelector(value))
4010
+ .filter((value) => value && managedBotSelectors.has(value)),
4011
+ );
4012
+ const contractLeadBot = normalizeMentionSelector(humanIntent.leadBotSelector);
4013
+ const selectedFromContract = uniqueOrdered(
4014
+ (
4015
+ contractInitialResponders.length
4016
+ ? contractInitialResponders
4017
+ : contractAllowedResponders.length
4018
+ ? contractAllowedResponders
4019
+ : contractParticipants.length
4020
+ ? contractParticipants
4021
+ : contractLeadBot && managedBotSelectors.has(contractLeadBot)
4022
+ ? [contractLeadBot]
4023
+ : []
4024
+ ).filter(Boolean),
4025
+ );
4026
+ if (selectedFromContract.length > 0) {
4027
+ return {
4028
+ decision: selectedFromContract.length > 1 ? "multiple_responders" : "single_responder",
4029
+ selected_bot_usernames: selectedFromContract,
4030
+ referenced_bot_usernames: triggerFacts.mentioned_bot_usernames,
4031
+ confidence: "high",
4032
+ reason_code: "precomputed_human_intent_contract",
4033
+ clarification: "",
4034
+ managed_bots: managedBots,
4035
+ };
4036
+ }
3990
4037
  if (!adjudicator) {
3991
4038
  const fallbackSelected = managedBots
3992
4039
  .filter((entry) => entry.trigger_eligible === true)
@@ -4109,6 +4156,7 @@ export async function processRunnerSelectedRecord({
4109
4156
  triggerDecision: precomputedTriggerDecision = null,
4110
4157
  responderAdjudication: precomputedResponderAdjudication = null,
4111
4158
  persistedHumanIntentRequest = null,
4159
+ precomputedHumanIntentContext = null,
4112
4160
  deps,
4113
4161
  }) {
4114
4162
  const saveRunnerRouteState = requireDependency(deps, "saveRunnerRouteState");
@@ -4128,6 +4176,8 @@ export async function processRunnerSelectedRecord({
4128
4176
  ...safeObject(buildRunnerExecutionDeps()),
4129
4177
  ...safeObject(deps),
4130
4178
  };
4179
+ const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4180
+ const normalizedPrecomputedHumanIntent = safeObject(normalizedPrecomputedHumanIntentContext.humanIntent);
4131
4181
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
4132
4182
  ? executionDeps.validateWorkspaceArtifacts
4133
4183
  : null;
@@ -4172,6 +4222,7 @@ export async function processRunnerSelectedRecord({
4172
4222
  bot,
4173
4223
  executionPlan,
4174
4224
  deps: executionDeps,
4225
+ precomputedHumanIntent: normalizedPrecomputedHumanIntent,
4175
4226
  });
4176
4227
  const selectedResponderSelectors = ensureArray(responderAdjudication.selected_bot_usernames)
4177
4228
  .map((value) => normalizeMentionSelector(value))
@@ -4201,14 +4252,16 @@ export async function processRunnerSelectedRecord({
4201
4252
  };
4202
4253
  }
4203
4254
 
4204
- const humanIntentContext = await resolveHumanIntentContext({
4205
- selectedRecord,
4206
- normalizedRoute,
4207
- bot,
4208
- executionPlan,
4209
- deps: executionDeps,
4210
- persistedRequest: persistedHumanIntentRequest,
4211
- });
4255
+ const humanIntentContext = Object.keys(normalizedPrecomputedHumanIntentContext).length > 0
4256
+ ? normalizedPrecomputedHumanIntentContext
4257
+ : await resolveHumanIntentContext({
4258
+ selectedRecord,
4259
+ normalizedRoute,
4260
+ bot,
4261
+ executionPlan,
4262
+ deps: executionDeps,
4263
+ persistedRequest: persistedHumanIntentRequest,
4264
+ });
4212
4265
  const precomputedIntentType = normalizeHumanIntentType(
4213
4266
  safeObject(safeObject(humanIntentContext).humanIntent).intentType,
4214
4267
  );
@@ -118,6 +118,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
118
118
  const safeObject = requireDependency(deps, "safeObject");
119
119
  const normalizeRunnerTriggerPolicy = requireDependency(deps, "normalizeRunnerTriggerPolicy");
120
120
  const evaluateTelegramRunnerTrigger = requireDependency(deps, "evaluateTelegramRunnerTrigger");
121
+ const resolveRunnerResponderAdjudication = requireDependency(deps, "resolveRunnerResponderAdjudication");
121
122
  const selectPendingArchiveComments = requireDependency(deps, "selectPendingArchiveComments");
122
123
  const selectRunnerPendingWork = requireDependency(deps, "selectRunnerPendingWork");
123
124
  const processRunnerSelectedRecord = requireDependency(deps, "processRunnerSelectedRecord");
@@ -2228,6 +2229,134 @@ export async function runSelftestRunnerScenarios(push, deps) {
2228
2229
  }
2229
2230
  }
2230
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
+
2231
2360
  const defaultMonitorTriggerPolicy = normalizeRunnerTriggerPolicy({}, { role: "monitor" });
2232
2361
  push(
2233
2362
  "bot_runner_default_monitor_trigger_policy",
@@ -2935,6 +3064,108 @@ export async function runSelftestRunnerScenarios(push, deps) {
2935
3064
  push("public_multi_bot_human_intent_is_computed_once_per_message", false, String(err?.message || err));
2936
3065
  }
2937
3066
 
3067
+ try {
3068
+ let adjudicatorCalls = 0;
3069
+ const adjudication = await resolveRunnerResponderAdjudication({
3070
+ selectedRecord: {
3071
+ id: "comment-precomputed-human-intent-adjudication",
3072
+ parsedArchive: {
3073
+ kind: "telegram_message",
3074
+ chatID: "-100123",
3075
+ chatType: "supergroup",
3076
+ senderIsBot: false,
3077
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot discuss this project together.",
3078
+ mentionUsernames: ["RyoAI_bot", "RyoAI2_bot", "RyoAI3_bot"],
3079
+ messageID: 1193,
3080
+ },
3081
+ },
3082
+ pendingOrdered: [],
3083
+ normalizedRoute: normalizeRunnerRoute({
3084
+ name: "telegram-monitor-precomputed-human-intent-adjudication",
3085
+ project_id: selftestProjectID,
3086
+ provider: "telegram",
3087
+ role: "monitor",
3088
+ role_profile: "monitor",
3089
+ destination_id: "dest-1",
3090
+ destination_label: "Main Room",
3091
+ server_bot_name: "RyoAI_bot",
3092
+ server_bot_id: "bot-lead-1",
3093
+ trigger_policy: {
3094
+ mentions_only: true,
3095
+ direct_messages: true,
3096
+ reply_to_bot_messages: true,
3097
+ },
3098
+ archive_policy: {
3099
+ mirror_replies: true,
3100
+ dedupe_inbound: true,
3101
+ dedupe_outbound: true,
3102
+ skip_bot_messages: true,
3103
+ },
3104
+ dry_run_delivery: true,
3105
+ }),
3106
+ bot: {
3107
+ id: "bot-lead-1",
3108
+ name: "RyoAI_bot",
3109
+ username: "RyoAI_bot",
3110
+ role: "monitor",
3111
+ provider: "telegram",
3112
+ },
3113
+ executionPlan: {
3114
+ mode: "role_profile",
3115
+ roleProfileName: "monitor",
3116
+ roleProfile: {
3117
+ client: "sample",
3118
+ model: "",
3119
+ permissionMode: "read_only",
3120
+ reasoningEffort: "low",
3121
+ },
3122
+ workspaceDir: path.join(os.tmpdir(), "metheus-runner-selftest-precomputed-human-intent-adjudication"),
3123
+ workspaceSource: "selftest",
3124
+ usedCommandFallback: false,
3125
+ },
3126
+ precomputedHumanIntent: {
3127
+ intentMode: "delegated_single_lead",
3128
+ intentType: "explanation_query",
3129
+ participantSelectors: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
3130
+ initialResponderSelectors: ["ryoai_bot"],
3131
+ allowedResponderSelectors: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
3132
+ leadBotSelector: "ryoai_bot",
3133
+ summaryBotSelector: "ryoai_bot",
3134
+ allowBotToBot: true,
3135
+ replyExpectation: "informational",
3136
+ },
3137
+ deps: {
3138
+ adjudicateRunnerRespondersWithAI: async () => {
3139
+ adjudicatorCalls += 1;
3140
+ return {
3141
+ decision: "multiple_responders",
3142
+ selected_bot_usernames: ["ryoai2_bot", "ryoai3_bot"],
3143
+ };
3144
+ },
3145
+ managedConversationBots: [
3146
+ { route: { serverBotName: "RyoAI_bot" }, bot: { username: "RyoAI_bot", name: "RyoAI_bot" } },
3147
+ { route: { serverBotName: "RyoAI2_bot" }, bot: { username: "RyoAI2_bot", name: "RyoAI2_bot" } },
3148
+ { route: { serverBotName: "RyoAI3_bot" }, bot: { username: "RyoAI3_bot", name: "RyoAI3_bot" } },
3149
+ ],
3150
+ resolveConversationPeerBots: () => [
3151
+ { id: "bot-lead-1", name: "RyoAI_bot" },
3152
+ { id: "bot-peer-1", name: "RyoAI2_bot" },
3153
+ { id: "bot-peer-2", name: "RyoAI3_bot" },
3154
+ ],
3155
+ },
3156
+ });
3157
+ push(
3158
+ "precomputed_human_intent_contract_drives_responder_selection",
3159
+ adjudicatorCalls === 0
3160
+ && ensureArray(adjudication.selected_bot_usernames).length === 1
3161
+ && String(ensureArray(adjudication.selected_bot_usernames)[0] || "") === "ryoai_bot"
3162
+ && String(adjudication.reason_code || "") === "precomputed_human_intent_contract",
3163
+ `adjudicator_calls=${adjudicatorCalls} selected=${JSON.stringify(ensureArray(adjudication.selected_bot_usernames))} reason=${String(adjudication.reason_code || "(none)")}`,
3164
+ );
3165
+ } catch (err) {
3166
+ push("precomputed_human_intent_contract_drives_responder_selection", false, String(err?.message || err));
3167
+ }
3168
+
2938
3169
  try {
2939
3170
  let humanIntentCalls = 0;
2940
3171
  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.204",
3
+ "version": "0.2.206",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [