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 +333 -9
- package/lib/local-ai-adapters.mjs +117 -0
- package/lib/runner-orchestration.mjs +180 -1
- package/lib/selftest-runner-scenarios.mjs +270 -0
- package/package.json +1 -1
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:
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|