metheus-governance-mcp-cli 0.2.281 → 0.2.283

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
@@ -25,9 +25,10 @@ import {
25
25
  resolveRolePlannerAuditorModelDisplayName,
26
26
  resolveRolePlannerModelDisplayName,
27
27
  resolveRolePlannerRepairModelDisplayName,
28
- resolveResponderAdjudicatorModelDisplayName,
29
- resolveGeminiReasoningConfig,
30
- suggestLocalAIModelDisplayName,
28
+ resolveResponderAdjudicatorModelDisplayName,
29
+ resolveGeminiHeadlessExecutionModel,
30
+ resolveGeminiReasoningConfig,
31
+ suggestLocalAIModelDisplayName,
31
32
  SUPPORTED_LOCAL_AI_CLIENTS,
32
33
  normalizeLocalAIClientName,
33
34
  normalizeLocalAIPermissionMode,
@@ -3111,20 +3112,25 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
3111
3112
  entry.sourceMessageBotUsername,
3112
3113
  ]),
3113
3114
  });
3114
- normalized[requestKey] = {
3115
- request_key: requestKey,
3116
- project_id: String(entry.project_id || entry.projectID || "").trim(),
3117
- provider: String(entry.provider || "").trim(),
3118
- chat_id: firstNonEmptyString([
3119
- entry.chat_id,
3120
- entry.chatID,
3121
- normalizedSourceMessageEnvelope.chat_id,
3122
- ]),
3123
- source_message_id: intFromRawAllowZero(
3124
- entry.source_message_id
3125
- || entry.sourceMessageID
3126
- || normalizedSourceMessageEnvelope.message_id,
3127
- 0,
3115
+ normalized[requestKey] = {
3116
+ request_key: requestKey,
3117
+ project_id: String(entry.project_id || entry.projectID || "").trim(),
3118
+ provider: String(entry.provider || "").trim(),
3119
+ chat_id: firstNonEmptyString([
3120
+ entry.chat_id,
3121
+ entry.chatID,
3122
+ normalizedSourceMessageEnvelope.chat_id,
3123
+ ]),
3124
+ canonical_human_message_key: firstNonEmptyString([
3125
+ entry.canonical_human_message_key,
3126
+ entry.canonicalHumanMessageKey,
3127
+ normalizedSourceMessageEnvelope.canonical_human_message_key,
3128
+ ]),
3129
+ source_message_id: intFromRawAllowZero(
3130
+ entry.source_message_id
3131
+ || entry.sourceMessageID
3132
+ || normalizedSourceMessageEnvelope.message_id,
3133
+ 0,
3128
3134
  ) || undefined,
3129
3135
  source_message_thread_id: intFromRawAllowZero(
3130
3136
  entry.source_message_thread_id
@@ -3813,18 +3819,35 @@ function shouldBypassRunnerStartupLoopForContractFollowup({
3813
3819
  return true;
3814
3820
  }
3815
3821
 
3816
- function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
3817
- const chatID = String(selectors.chatID || "").trim();
3818
- const messageID = intFromRawAllowZero(selectors.messageID, 0);
3819
- if (!chatID || messageID <= 0) {
3820
- return [];
3821
- }
3822
- return findRunnerRequestsForScope(state, normalizedRoute, { chatID })
3823
- .filter((entry) => (
3824
- intFromRawAllowZero(entry.source_message_id, 0) === messageID
3825
- || intFromRawAllowZero(entry.last_source_message_id, 0) === messageID
3826
- ));
3827
- }
3822
+ function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
3823
+ const chatID = String(selectors.chatID || "").trim();
3824
+ const messageID = intFromRawAllowZero(selectors.messageID, 0);
3825
+ const canonicalHumanMessageKey = String(selectors.canonicalHumanMessageKey || "").trim();
3826
+ if (!chatID || (messageID <= 0 && !canonicalHumanMessageKey)) {
3827
+ return [];
3828
+ }
3829
+ return findRunnerRequestsForScope(state, normalizedRoute, { chatID })
3830
+ .filter((entryRaw) => {
3831
+ const entry = safeObject(entryRaw);
3832
+ if (canonicalHumanMessageKey) {
3833
+ const requestCanonicalHumanMessageKey = firstNonEmptyString([
3834
+ entry.canonical_human_message_key,
3835
+ safeObject(entry.source_message_envelope).canonical_human_message_key,
3836
+ buildRunnerCanonicalHumanInboundKey(safeObject(entry.source_message_envelope)),
3837
+ ]);
3838
+ if (requestCanonicalHumanMessageKey === canonicalHumanMessageKey) {
3839
+ return true;
3840
+ }
3841
+ }
3842
+ return (
3843
+ messageID > 0
3844
+ && (
3845
+ intFromRawAllowZero(entry.source_message_id, 0) === messageID
3846
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === messageID
3847
+ )
3848
+ );
3849
+ });
3850
+ }
3828
3851
 
3829
3852
  function sortRunnerRequestEntriesNewestFirst(entries = []) {
3830
3853
  return ensureArray(entries).slice().sort((leftRaw, rightRaw) => {
@@ -3839,24 +3862,26 @@ function sortRunnerRequestEntriesNewestFirst(entries = []) {
3839
3862
  });
3840
3863
  }
3841
3864
 
3842
- async function findServerRunnerRequestForMessageID({
3843
- normalizedRoute,
3844
- runtime,
3845
- chatID,
3846
- messageID,
3847
- }) {
3865
+ async function findServerRunnerRequestForMessageID({
3866
+ normalizedRoute,
3867
+ runtime,
3868
+ chatID,
3869
+ messageID,
3870
+ canonicalHumanMessageKey = "",
3871
+ }) {
3848
3872
  const projectID = String(normalizedRoute?.projectID || "").trim();
3849
3873
  const provider = String(normalizedRoute?.provider || "").trim();
3850
- const normalizedChatID = String(chatID || "").trim();
3851
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
3852
- if (
3853
- !projectID
3854
- || !provider
3855
- || !normalizedChatID
3856
- || normalizedMessageID <= 0
3857
- || !runtime?.baseURL
3858
- || !runtime?.token
3859
- ) {
3874
+ const normalizedChatID = String(chatID || "").trim();
3875
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
3876
+ const normalizedCanonicalHumanMessageKey = String(canonicalHumanMessageKey || "").trim();
3877
+ if (
3878
+ !projectID
3879
+ || !provider
3880
+ || !normalizedChatID
3881
+ || (normalizedMessageID <= 0 && !normalizedCanonicalHumanMessageKey)
3882
+ || !runtime?.baseURL
3883
+ || !runtime?.token
3884
+ ) {
3860
3885
  return null;
3861
3886
  }
3862
3887
  try {
@@ -3869,18 +3894,29 @@ async function findServerRunnerRequestForMessageID({
3869
3894
  limit: 500,
3870
3895
  offset: 0,
3871
3896
  });
3872
- const matched = sortRunnerRequestEntriesNewestFirst(serverRequests.filter((entryRaw) => {
3873
- const entry = safeObject(entryRaw);
3874
- return (
3875
- String(entry.project_id || "").trim() === projectID
3876
- && String(entry.provider || "").trim() === provider
3877
- && String(entry.chat_id || "").trim() === normalizedChatID
3878
- && (
3879
- intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
3880
- || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
3881
- )
3882
- );
3883
- }));
3897
+ const matched = sortRunnerRequestEntriesNewestFirst(serverRequests.filter((entryRaw) => {
3898
+ const entry = safeObject(entryRaw);
3899
+ const requestCanonicalHumanMessageKey = firstNonEmptyString([
3900
+ entry.canonical_human_message_key,
3901
+ safeObject(entry.source_message_envelope).canonical_human_message_key,
3902
+ buildRunnerCanonicalHumanInboundKey(safeObject(entry.source_message_envelope)),
3903
+ ]);
3904
+ return (
3905
+ String(entry.project_id || "").trim() === projectID
3906
+ && String(entry.provider || "").trim() === provider
3907
+ && String(entry.chat_id || "").trim() === normalizedChatID
3908
+ && (
3909
+ (normalizedCanonicalHumanMessageKey && requestCanonicalHumanMessageKey === normalizedCanonicalHumanMessageKey)
3910
+ || (
3911
+ normalizedMessageID > 0
3912
+ && (
3913
+ intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
3914
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
3915
+ )
3916
+ )
3917
+ )
3918
+ );
3919
+ }));
3884
3920
  return safeObject(matched[0]);
3885
3921
  } catch {
3886
3922
  return null;
@@ -4109,12 +4145,14 @@ function buildRunnerValidationAndDeliverySummary({
4109
4145
  responseContractValidationStatus = "",
4110
4146
  responseContractValidationReason = "",
4111
4147
  responseContractValidationTargets = [],
4112
- assignmentValidationStatus = "",
4113
- assignmentValidationReason = "",
4114
- assignmentValidationModes = [],
4115
- deliveryStatus = "",
4116
- archiveStatus = "",
4117
- transportError = "",
4148
+ assignmentValidationStatus = "",
4149
+ assignmentValidationReason = "",
4150
+ assignmentValidationModes = [],
4151
+ failureReplyClassification = "",
4152
+ failureFacts = {},
4153
+ deliveryStatus = "",
4154
+ archiveStatus = "",
4155
+ transportError = "",
4118
4156
  archiveError = "",
4119
4157
  sourceMessageEnvelope = {},
4120
4158
  lastReplyMessageEnvelope = {},
@@ -4321,25 +4359,27 @@ function pickRunnerSharedConversationSourceRequest(entries = [], excludeRequestK
4321
4359
  return safeObject(matched[0]);
4322
4360
  }
4323
4361
 
4324
- async function findServerRunnerConversationSourceRequestForMessageID({
4325
- normalizedRoute,
4326
- runtime,
4327
- chatID,
4328
- messageID,
4329
- excludeRequestKey = "",
4330
- }) {
4362
+ async function findServerRunnerConversationSourceRequestForMessageID({
4363
+ normalizedRoute,
4364
+ runtime,
4365
+ chatID,
4366
+ messageID,
4367
+ canonicalHumanMessageKey = "",
4368
+ excludeRequestKey = "",
4369
+ }) {
4331
4370
  const projectID = String(normalizedRoute?.projectID || "").trim();
4332
4371
  const provider = String(normalizedRoute?.provider || "").trim();
4333
- const normalizedChatID = String(chatID || "").trim();
4334
- const normalizedMessageID = intFromRawAllowZero(messageID, 0);
4335
- if (
4336
- !projectID
4337
- || !provider
4338
- || !normalizedChatID
4339
- || normalizedMessageID <= 0
4340
- || !runtime?.baseURL
4341
- || !runtime?.token
4342
- ) {
4372
+ const normalizedChatID = String(chatID || "").trim();
4373
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
4374
+ const normalizedCanonicalHumanMessageKey = String(canonicalHumanMessageKey || "").trim();
4375
+ if (
4376
+ !projectID
4377
+ || !provider
4378
+ || !normalizedChatID
4379
+ || (normalizedMessageID <= 0 && !normalizedCanonicalHumanMessageKey)
4380
+ || !runtime?.baseURL
4381
+ || !runtime?.token
4382
+ ) {
4343
4383
  return null;
4344
4384
  }
4345
4385
  try {
@@ -4352,18 +4392,29 @@ async function findServerRunnerConversationSourceRequestForMessageID({
4352
4392
  limit: 500,
4353
4393
  offset: 0,
4354
4394
  });
4355
- const matched = serverRequests.filter((entryRaw) => {
4356
- const entry = safeObject(entryRaw);
4357
- return (
4358
- String(entry.project_id || "").trim() === projectID
4359
- && String(entry.provider || "").trim() === provider
4360
- && String(entry.chat_id || "").trim() === normalizedChatID
4361
- && (
4362
- intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
4363
- || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
4364
- )
4365
- );
4366
- });
4395
+ const matched = serverRequests.filter((entryRaw) => {
4396
+ const entry = safeObject(entryRaw);
4397
+ const requestCanonicalHumanMessageKey = firstNonEmptyString([
4398
+ entry.canonical_human_message_key,
4399
+ safeObject(entry.source_message_envelope).canonical_human_message_key,
4400
+ buildRunnerCanonicalHumanInboundKey(safeObject(entry.source_message_envelope)),
4401
+ ]);
4402
+ return (
4403
+ String(entry.project_id || "").trim() === projectID
4404
+ && String(entry.provider || "").trim() === provider
4405
+ && String(entry.chat_id || "").trim() === normalizedChatID
4406
+ && (
4407
+ (normalizedCanonicalHumanMessageKey && requestCanonicalHumanMessageKey === normalizedCanonicalHumanMessageKey)
4408
+ || (
4409
+ normalizedMessageID > 0
4410
+ && (
4411
+ intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
4412
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
4413
+ )
4414
+ )
4415
+ )
4416
+ );
4417
+ });
4367
4418
  return pickRunnerSharedConversationSourceRequest(matched, excludeRequestKey);
4368
4419
  } catch {
4369
4420
  return null;
@@ -5026,13 +5077,14 @@ async function claimRunnerRequestForHumanComment({
5026
5077
  runtime,
5027
5078
  archiveThreadID,
5028
5079
  });
5029
- const replyChainContext = safeObject(replyChainResolution.replyChainContext);
5080
+ const replyChainContext = safeObject(replyChainResolution.replyChainContext);
5030
5081
  const referencedRequest = safeObject(replyChainContext.referencedRequest);
5031
5082
  const resolvedNormalizedIntent = resolveRunnerRequestClaimIntent({
5032
5083
  normalizedIntent,
5033
5084
  });
5034
- let stateForClaim = safeObject(replyChainResolution.state);
5035
- const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
5085
+ const canonicalHumanMessageKey = buildRunnerCanonicalHumanInboundKey(parsed);
5086
+ let stateForClaim = safeObject(replyChainResolution.state);
5087
+ const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
5036
5088
  const provisionalConversationID = String(
5037
5089
  parsed.conversationID
5038
5090
  || replyChainContext.conversationID
@@ -5067,29 +5119,31 @@ async function claimRunnerRequestForHumanComment({
5067
5119
  };
5068
5120
  }
5069
5121
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5070
- let sharedConversationSource = currentMessageID > 0
5071
- ? pickRunnerSharedConversationSourceRequest(
5072
- findRunnerRequestsForMessageID(stateForClaim, normalizedRoute, {
5073
- chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5074
- messageID: currentMessageID,
5075
- }),
5076
- provisionalRequestKey,
5077
- )
5078
- : {};
5122
+ let sharedConversationSource = currentMessageID > 0
5123
+ ? pickRunnerSharedConversationSourceRequest(
5124
+ findRunnerRequestsForMessageID(stateForClaim, normalizedRoute, {
5125
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5126
+ messageID: currentMessageID,
5127
+ canonicalHumanMessageKey,
5128
+ }),
5129
+ provisionalRequestKey,
5130
+ )
5131
+ : {};
5079
5132
  if (
5080
5133
  !Object.keys(sharedConversationSource).length
5081
5134
  && currentMessageID > 0
5082
5135
  && runtime?.baseURL
5083
5136
  && runtime?.token
5084
5137
  ) {
5085
- sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
5086
- normalizedRoute,
5087
- runtime,
5088
- chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5089
- messageID: currentMessageID,
5090
- excludeRequestKey: provisionalRequestKey,
5091
- }));
5092
- }
5138
+ sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
5139
+ normalizedRoute,
5140
+ runtime,
5141
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5142
+ messageID: currentMessageID,
5143
+ canonicalHumanMessageKey,
5144
+ excludeRequestKey: provisionalRequestKey,
5145
+ }));
5146
+ }
5093
5147
  const authorityContext = resolveRunnerHumanCommentAuthorityContext({
5094
5148
  normalizedRoute,
5095
5149
  selectedRecord,
@@ -5183,9 +5237,10 @@ async function claimRunnerRequestForHumanComment({
5183
5237
  provider: String(normalizedRoute?.provider || "").trim(),
5184
5238
  chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
5185
5239
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5186
- source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5187
- source_message_body: String(parsed.body || "").trim(),
5188
- source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
5240
+ source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5241
+ source_message_body: String(parsed.body || "").trim(),
5242
+ canonical_human_message_key: canonicalHumanMessageKey,
5243
+ source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
5189
5244
  source_message_route_key: String(sourceMessageEnvelope.source_route_key || "").trim(),
5190
5245
  source_message_bot_username: normalizeTelegramMentionUsername(sourceMessageEnvelope.source_bot_username),
5191
5246
  source_message_envelope: sourceMessageEnvelope,
@@ -6329,12 +6384,14 @@ function markRunnerRequestLifecycle({
6329
6384
  responseContractValidationStatus = "",
6330
6385
  responseContractValidationReason = "",
6331
6386
  responseContractValidationTargets = [],
6332
- assignmentValidationStatus = "",
6333
- assignmentValidationReason = "",
6334
- assignmentValidationModes = [],
6335
- deliveryStatus = "",
6336
- archiveStatus = "",
6337
- transportError = "",
6387
+ assignmentValidationStatus = "",
6388
+ assignmentValidationReason = "",
6389
+ assignmentValidationModes = [],
6390
+ failureReplyClassification = "",
6391
+ failureFacts = {},
6392
+ deliveryStatus = "",
6393
+ archiveStatus = "",
6394
+ transportError = "",
6338
6395
  archiveError = "",
6339
6396
  lastReplyMessageID = 0,
6340
6397
  lastReplyMessageThreadID = 0,
@@ -6445,6 +6502,8 @@ function markRunnerRequestLifecycle({
6445
6502
  || "",
6446
6503
  ).trim().toLowerCase();
6447
6504
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6505
+ const normalizedFailureReplyClassification = String(failureReplyClassification || "").trim().toLowerCase();
6506
+ const normalizedFailureFacts = safeObject(failureFacts);
6448
6507
  const shouldRemainRunningAfterReply = authoritativeDecisionBundle.should_close_after_reply === true
6449
6508
  ? false
6450
6509
  : authoritativeDecisionBundle.should_close_after_reply === false
@@ -6459,6 +6518,18 @@ function markRunnerRequestLifecycle({
6459
6518
  || rootEffectiveNextExpectedResponders.length > 0
6460
6519
  || continuationSelectors.length > 0
6461
6520
  );
6521
+ const shouldRemainRunningAfterError = ["error", "execution_failed"].includes(normalizedOutcome)
6522
+ && (
6523
+ normalizedFailureFacts.retryable === true
6524
+ || normalizedFailureReplyClassification === "retryable_failure"
6525
+ )
6526
+ && authoritativeDecisionBundle.should_close_after_reply !== true
6527
+ && (
6528
+ nextExecutionContractType === "delegation"
6529
+ || rootEffectiveExecutionContractTargets.length > 0
6530
+ || rootEffectiveNextExpectedResponders.length > 0
6531
+ || continuationSelectors.length > 0
6532
+ );
6462
6533
  const nextConversationIntentMode = String(
6463
6534
  authoritativeDecisionBundle.conversation_intent_mode
6464
6535
  || conversationIntentMode
@@ -6495,7 +6566,7 @@ function markRunnerRequestLifecycle({
6495
6566
  || normalizedOutcome === "execution_failed"
6496
6567
  || normalizedOutcome === "policy_violation"
6497
6568
  ) {
6498
- return "closed";
6569
+ return shouldRemainRunningAfterError ? "running" : "closed";
6499
6570
  }
6500
6571
  return normalizeRunnerRequestStatus(existing.status);
6501
6572
  })();
@@ -19502,12 +19573,13 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
19502
19573
  push("runner_tui_frame_renders_route_statuses", false, String(err?.message || err));
19503
19574
  }
19504
19575
 
19505
- await runSelftestBotCommands(push, {
19506
- cliPath: fileURLToPath(import.meta.url),
19507
- parseSimpleEnvText,
19508
- resolveLocalAIExecutionModel,
19509
- suggestLocalAIModelDisplayName,
19510
- resolveGeminiReasoningConfig,
19576
+ await runSelftestBotCommands(push, {
19577
+ cliPath: fileURLToPath(import.meta.url),
19578
+ parseSimpleEnvText,
19579
+ resolveLocalAIExecutionModel,
19580
+ resolveGeminiHeadlessExecutionModel,
19581
+ suggestLocalAIModelDisplayName,
19582
+ resolveGeminiReasoningConfig,
19511
19583
  stripLocalOnlyToolArgs: (requestObj, toolName) =>
19512
19584
  stripLocalOnlyToolArgs(requestObj, toolName),
19513
19585
  applyProxyResponsePatches: (params, deps = buildProxyResponsePipelineDeps()) =>
@@ -19,6 +19,7 @@ const GEMINI_HOME_SYNC_FILES = [
19
19
  ];
20
20
  const GEMINI_STDIN_BRIDGE_PROMPT = "Use the full task provided on standard input as the authoritative prompt. Follow it exactly and output only the final answer.";
21
21
  const GEMINI_CLI_TIMEOUT_MS = 90 * 1000;
22
+ const GEMINI_RUNNER_STABLE_EXECUTION_MODEL = "gemini-3-flash-preview";
22
23
  const LOCAL_AI_MODEL_MAPPINGS = {
23
24
  gpt: [
24
25
  {
@@ -880,7 +881,12 @@ function runLocalAIPromptRawText({
880
881
  const normalizedClient = normalizeLocalAIClientName(client);
881
882
  const normalizedPermissionMode = normalizeLocalAIPermissionMode(permissionMode);
882
883
  const normalizedReasoningEffort = normalizeLocalAIReasoningEffort(reasoningEffort, "low");
883
- const resolvedExecutionModel = resolveLocalAIExecutionModel(normalizedClient, model);
884
+ const resolvedExecutionModel = normalizedClient === "gemini"
885
+ ? resolveGeminiHeadlessExecutionModel(model, {
886
+ permissionMode: normalizedPermissionMode,
887
+ reasoningEffort: normalizedReasoningEffort,
888
+ })
889
+ : resolveLocalAIExecutionModel(normalizedClient, model);
884
890
  const resolvedWorkspaceDir = ensureWorkspaceDir(workspaceDir);
885
891
  const nextEnv = {
886
892
  ...process.env,
@@ -1386,6 +1392,23 @@ export function resolveLocalAIExecutionModel(clientName, rawModelValue = "") {
1386
1392
  return match ? String(match.execution || "").trim() : modelValue;
1387
1393
  }
1388
1394
 
1395
+ export function resolveGeminiHeadlessExecutionModel(
1396
+ rawModelValue = "",
1397
+ { permissionMode = "read_only", reasoningEffort = "low" } = {},
1398
+ ) {
1399
+ const resolvedExecutionModel = resolveLocalAIExecutionModel("gemini", rawModelValue);
1400
+ const normalizedExecutionModel = normalizeModelAliasText(resolvedExecutionModel);
1401
+ void normalizeLocalAIPermissionMode(permissionMode);
1402
+ void normalizeLocalAIReasoningEffort(reasoningEffort, "low");
1403
+ if (normalizedExecutionModel !== "auto-gemini-3") {
1404
+ return resolvedExecutionModel;
1405
+ }
1406
+ // Headless runner turns should not depend on Gemini CLI's internal auto-router.
1407
+ // Under heavier prompts it can escalate to capacity-constrained preview models,
1408
+ // which makes one bot path look flaky even though the routing logic is correct.
1409
+ return GEMINI_RUNNER_STABLE_EXECUTION_MODEL;
1410
+ }
1411
+
1389
1412
  function buildCodexArgs({ workspaceDir, model, permissionMode, reasoningEffort, outputPath }) {
1390
1413
  const args = ["exec"];
1391
1414
  if (model) {
@@ -1513,7 +1536,10 @@ function buildGeminiThinkingConfig(model, reasoningEffort) {
1513
1536
  }
1514
1537
 
1515
1538
  export function resolveGeminiReasoningConfig(rawModelValue = "", reasoningEffort = "medium") {
1516
- const executionModel = resolveLocalAIExecutionModel("gemini", rawModelValue);
1539
+ const executionModel = resolveGeminiHeadlessExecutionModel(rawModelValue, {
1540
+ permissionMode: "read_only",
1541
+ reasoningEffort,
1542
+ });
1517
1543
  if (!executionModel) {
1518
1544
  return null;
1519
1545
  }
@@ -3256,7 +3282,12 @@ export function runLocalAIClient({
3256
3282
  const normalizedClient = normalizeLocalAIClientName(client);
3257
3283
  const normalizedPermissionMode = normalizeLocalAIPermissionMode(permissionMode);
3258
3284
  const normalizedReasoningEffort = normalizeLocalAIReasoningEffort(reasoningEffort);
3259
- const resolvedExecutionModel = resolveLocalAIExecutionModel(normalizedClient, model);
3285
+ const resolvedExecutionModel = normalizedClient === "gemini"
3286
+ ? resolveGeminiHeadlessExecutionModel(model, {
3287
+ permissionMode: normalizedPermissionMode,
3288
+ reasoningEffort: normalizedReasoningEffort,
3289
+ })
3290
+ : resolveLocalAIExecutionModel(normalizedClient, model);
3260
3291
  const resolvedWorkspaceDir = ensureWorkspaceDir(workspaceDir);
3261
3292
  const promptText = buildLocalBotPrompt(inputPayload);
3262
3293
  if (normalizedClient === "sample") {
@@ -26,11 +26,14 @@ export function classifyExecutionFailureFacts(detail) {
26
26
  const normalizedDetail = String(detail || "").trim();
27
27
  const networkReset = /ECONNRESET|socket hang up|read ECONNRESET/i.test(normalizedDetail);
28
28
  const networkTimeout = /ETIMEDOUT|http timeout|ECONNABORTED|aborted/i.test(normalizedDetail);
29
- const retryable = networkReset || networkTimeout;
29
+ const providerCapacityExhausted = /MODEL_CAPACITY_EXHAUSTED|RESOURCE_EXHAUSTED|No capacity available for model|rateLimitExceeded/i.test(normalizedDetail);
30
+ const retryable = networkReset || networkTimeout || providerCapacityExhausted;
30
31
  const base = {
31
32
  stage: "execution",
32
33
  operation: "runner_execution",
33
- errorType: retryable
34
+ errorType: providerCapacityExhausted
35
+ ? "provider_capacity_exhausted"
36
+ : retryable
34
37
  ? (networkTimeout ? "network_timeout" : "network_reset")
35
38
  : "execution_failed",
36
39
  retryable,
@@ -42,6 +45,15 @@ export function classifyExecutionFailureFacts(detail) {
42
45
  if (!normalizedDetail) {
43
46
  return base;
44
47
  }
48
+ if (providerCapacityExhausted) {
49
+ return {
50
+ ...base,
51
+ stage: "provider_call",
52
+ operation: "local_ai_model_request",
53
+ errorType: "provider_capacity_exhausted",
54
+ retryable: true,
55
+ };
56
+ }
45
57
  if (/permission_mode=read_only|read[_ -]?only/i.test(normalizedDetail)) {
46
58
  return {
47
59
  ...base,
@@ -1,4 +1,5 @@
1
1
  import {
2
+ buildCanonicalHumanInboundKey,
2
3
  buildTelegramMessageEnvelopeFromParsedArchive,
3
4
  findRecentTelegramMessageEnvelope,
4
5
  isTelegramLocalInboundEnvelopeForRoute,
@@ -27,6 +28,20 @@ function normalizeMentionSelector(value) {
27
28
  return String(value || "").trim().replace(/^@+/, "").toLowerCase();
28
29
  }
29
30
 
31
+ function firstNonEmptyString(values) {
32
+ for (const value of ensureArray(values)) {
33
+ const normalized = String(value ?? "").trim();
34
+ if (normalized) {
35
+ return normalized;
36
+ }
37
+ }
38
+ return "";
39
+ }
40
+
41
+ function normalizeCanonicalHumanMessageKey(rawValue) {
42
+ return String(rawValue || "").trim();
43
+ }
44
+
30
45
  function uniqueOrdered(values) {
31
46
  const ordered = [];
32
47
  const seen = new Set();
@@ -82,6 +97,15 @@ function doesTelegramEnvelopeMatchMessage(rawEnvelope, {
82
97
  && intFromRawAllowZero(envelope.message_id, 0) === normalizedMessageID;
83
98
  }
84
99
 
100
+ function doesTelegramEnvelopeMatchCanonicalHumanMessage(rawEnvelope, canonicalHumanMessageKey = "") {
101
+ const normalizedCanonicalHumanMessageKey = normalizeCanonicalHumanMessageKey(canonicalHumanMessageKey);
102
+ if (!normalizedCanonicalHumanMessageKey) {
103
+ return false;
104
+ }
105
+ const envelope = normalizeTelegramMessageEnvelope(rawEnvelope);
106
+ return String(envelope.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey;
107
+ }
108
+
85
109
  function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
86
110
  const receipt = safeObject(rawReceipt);
87
111
  const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
@@ -89,7 +113,7 @@ function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
89
113
  if (!chatID || !(messageID > 0)) {
90
114
  return {};
91
115
  }
92
- return {
116
+ const normalized = {
93
117
  chat_id: chatID,
94
118
  message_id: messageID,
95
119
  ...(intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0) > 0
@@ -107,10 +131,16 @@ function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
107
131
  ...(normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "")
108
132
  ? { sender_username: normalizeMentionSelector(receipt.sender_username || receipt.senderUsername || "") }
109
133
  : {}),
134
+ ...(String(receipt.sender_id || receipt.senderID || "").trim()
135
+ ? { sender_id: String(receipt.sender_id || receipt.senderID || "").trim() }
136
+ : {}),
110
137
  sender_is_bot: receipt.sender_is_bot === true || receipt.senderIsBot === true,
111
138
  ...(String(receipt.body || "").trim()
112
139
  ? { body: String(receipt.body || "").trim() }
113
140
  : {}),
141
+ ...(String(receipt.occurred_at || receipt.occurredAt || "").trim()
142
+ ? { occurred_at: String(receipt.occurred_at || receipt.occurredAt || "").trim() }
143
+ : {}),
114
144
  ...(String(receipt.receipt_origin || receipt.receiptOrigin || "").trim()
115
145
  ? { receipt_origin: String(receipt.receipt_origin || receipt.receiptOrigin || "").trim().toLowerCase() }
116
146
  : {}),
@@ -121,16 +151,59 @@ function normalizeRunnerRecentLocalInboundReceipt(rawReceipt) {
121
151
  ? { receipt_bot_username: normalizeMentionSelector(receipt.receipt_bot_username || receipt.receiptBotUsername || "") }
122
152
  : {}),
123
153
  };
154
+ const canonicalHumanMessageKey = buildCanonicalHumanInboundKey({
155
+ chat_id: normalized.chat_id,
156
+ message_id: normalized.message_id,
157
+ message_thread_id: normalized.message_thread_id,
158
+ reply_to_message_id: normalized.reply_to_message_id,
159
+ kind: normalized.kind,
160
+ sender_id: normalized.sender_id,
161
+ sender_username: normalized.sender_username,
162
+ sender_is_bot: normalized.sender_is_bot === true,
163
+ body: normalized.body,
164
+ occurred_at: normalized.occurred_at,
165
+ canonical_human_message_key: firstNonEmptyString([
166
+ receipt.canonical_human_message_key,
167
+ receipt.canonicalHumanMessageKey,
168
+ ]),
169
+ });
170
+ if (canonicalHumanMessageKey) {
171
+ normalized.canonical_human_message_key = canonicalHumanMessageKey;
172
+ }
173
+ return normalized;
124
174
  }
125
175
 
126
- function findRecentTelegramInboundReceipt(rawMap, { chatID = "", messageID = 0 } = {}) {
176
+ function findRecentTelegramInboundReceipt(rawMap, {
177
+ chatID = "",
178
+ messageID = 0,
179
+ canonicalHumanMessageKey = "",
180
+ } = {}) {
127
181
  const normalizedChatID = String(chatID || "").trim();
128
182
  const normalizedMessageID = intFromRawAllowZero(messageID, 0);
129
- if (!normalizedChatID || !(normalizedMessageID > 0)) {
183
+ const normalizedCanonicalHumanMessageKey = normalizeCanonicalHumanMessageKey(canonicalHumanMessageKey);
184
+ if (!normalizedChatID || (!(normalizedMessageID > 0) && !normalizedCanonicalHumanMessageKey)) {
185
+ return {};
186
+ }
187
+ if (normalizedMessageID > 0) {
188
+ const key = `${normalizedChatID}:${normalizedMessageID}`;
189
+ const exactReceipt = normalizeRunnerRecentLocalInboundReceipt(safeObject(safeObject(rawMap)[key]));
190
+ if (Object.keys(exactReceipt).length > 0) {
191
+ return exactReceipt;
192
+ }
193
+ }
194
+ if (!normalizedCanonicalHumanMessageKey) {
130
195
  return {};
131
196
  }
132
- const key = `${normalizedChatID}:${normalizedMessageID}`;
133
- return normalizeRunnerRecentLocalInboundReceipt(safeObject(safeObject(rawMap)[key]));
197
+ for (const value of Object.values(safeObject(rawMap))) {
198
+ const normalizedReceipt = normalizeRunnerRecentLocalInboundReceipt(value);
199
+ if (
200
+ String(normalizedReceipt.chat_id || "").trim() === normalizedChatID
201
+ && String(normalizedReceipt.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey
202
+ ) {
203
+ return normalizedReceipt;
204
+ }
205
+ }
206
+ return {};
134
207
  }
135
208
 
136
209
  function buildTelegramMessageEnvelopeFromRecentReceipt(rawReceipt) {
@@ -154,6 +227,35 @@ function buildTelegramMessageEnvelopeFromRecentReceipt(rawReceipt) {
154
227
  });
155
228
  }
156
229
 
230
+ function findRecentTelegramMessageEnvelopeWithCanonicalFallback(rawMap, {
231
+ chatID = "",
232
+ messageID = 0,
233
+ canonicalHumanMessageKey = "",
234
+ } = {}) {
235
+ const exactEnvelope = findRecentTelegramMessageEnvelope(rawMap, {
236
+ chatID,
237
+ messageID,
238
+ });
239
+ if (Object.keys(safeObject(exactEnvelope)).length > 0) {
240
+ return exactEnvelope;
241
+ }
242
+ const normalizedChatID = String(chatID || "").trim();
243
+ const normalizedCanonicalHumanMessageKey = normalizeCanonicalHumanMessageKey(canonicalHumanMessageKey);
244
+ if (!normalizedChatID || !normalizedCanonicalHumanMessageKey) {
245
+ return {};
246
+ }
247
+ for (const rawEnvelope of Object.values(safeObject(rawMap))) {
248
+ const normalizedEnvelope = normalizeTelegramMessageEnvelope(rawEnvelope);
249
+ if (
250
+ String(normalizedEnvelope.chat_id || "").trim() === normalizedChatID
251
+ && String(normalizedEnvelope.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey
252
+ ) {
253
+ return normalizedEnvelope;
254
+ }
255
+ }
256
+ return {};
257
+ }
258
+
157
259
  function resolveRunnerTelegramSourceEnvelopeCandidates({
158
260
  routeState,
159
261
  persistedRequest,
@@ -173,19 +275,26 @@ function resolveRunnerTelegramSourceEnvelopeCandidates({
173
275
  archivedSourceEnvelope.message_id || archiveEnvelope.message_id,
174
276
  0,
175
277
  );
278
+ const archiveCanonicalHumanMessageKey = firstNonEmptyString([
279
+ archivedSourceEnvelope.canonical_human_message_key,
280
+ archiveEnvelope.canonical_human_message_key,
281
+ buildCanonicalHumanInboundKey(selectedRecord?.parsedArchive),
282
+ ]);
176
283
  const routeLocalReceipt = findRecentTelegramInboundReceipt(
177
284
  safeObject(routeState).recent_local_inbound_receipts,
178
285
  {
179
286
  chatID: archiveChatID,
180
287
  messageID: archiveMessageID,
288
+ canonicalHumanMessageKey: archiveCanonicalHumanMessageKey,
181
289
  },
182
290
  );
183
291
  const routeLocalReceiptEnvelope = buildTelegramMessageEnvelopeFromRecentReceipt(routeLocalReceipt);
184
- const routeLocalEnvelope = findRecentTelegramMessageEnvelope(
292
+ const routeLocalEnvelope = findRecentTelegramMessageEnvelopeWithCanonicalFallback(
185
293
  safeObject(routeState).recent_local_inbound_envelopes,
186
294
  {
187
295
  chatID: archiveChatID,
188
296
  messageID: archiveMessageID,
297
+ canonicalHumanMessageKey: archiveCanonicalHumanMessageKey,
189
298
  },
190
299
  );
191
300
  const persistedSourceEnvelope = normalizeTelegramMessageEnvelope(
@@ -197,6 +306,7 @@ function resolveRunnerTelegramSourceEnvelopeCandidates({
197
306
  archiveEnvelope,
198
307
  archiveChatID,
199
308
  archiveMessageID,
309
+ archiveCanonicalHumanMessageKey,
200
310
  routeLocalReceipt,
201
311
  routeLocalReceiptEnvelope,
202
312
  routeLocalEnvelope,
@@ -222,6 +332,7 @@ function resolveRunnerLocalSourceEnvelopeForRoute({
222
332
  archivedSourceEnvelope,
223
333
  archiveChatID,
224
334
  archiveMessageID,
335
+ archiveCanonicalHumanMessageKey,
225
336
  routeLocalReceiptEnvelope,
226
337
  routeLocalEnvelope,
227
338
  persistedSourceEnvelope,
@@ -251,10 +362,16 @@ function resolveRunnerLocalSourceEnvelopeForRoute({
251
362
  routeKey,
252
363
  botUsername: currentBotSelector,
253
364
  })
254
- && doesTelegramEnvelopeMatchMessage(persistedSourceEnvelope, {
255
- chatID: archiveChatID,
256
- messageID: archiveMessageID,
257
- })
365
+ && (
366
+ doesTelegramEnvelopeMatchMessage(persistedSourceEnvelope, {
367
+ chatID: archiveChatID,
368
+ messageID: archiveMessageID,
369
+ })
370
+ || doesTelegramEnvelopeMatchCanonicalHumanMessage(
371
+ persistedSourceEnvelope,
372
+ archiveCanonicalHumanMessageKey,
373
+ )
374
+ )
258
375
  ) {
259
376
  return {
260
377
  envelope: persistedSourceEnvelope,
@@ -267,10 +384,16 @@ function resolveRunnerLocalSourceEnvelopeForRoute({
267
384
  routeKey,
268
385
  botUsername: currentBotSelector,
269
386
  })
270
- && doesTelegramEnvelopeMatchMessage(archivedSourceEnvelope, {
271
- chatID: archiveChatID,
272
- messageID: archiveMessageID,
273
- })
387
+ && (
388
+ doesTelegramEnvelopeMatchMessage(archivedSourceEnvelope, {
389
+ chatID: archiveChatID,
390
+ messageID: archiveMessageID,
391
+ })
392
+ || doesTelegramEnvelopeMatchCanonicalHumanMessage(
393
+ archivedSourceEnvelope,
394
+ archiveCanonicalHumanMessageKey,
395
+ )
396
+ )
274
397
  ) {
275
398
  return {
276
399
  envelope: archivedSourceEnvelope,
@@ -289,14 +412,21 @@ function hasForeignRouteLocalEnvelopeForMessage({
289
412
  envelopes,
290
413
  archiveChatID,
291
414
  archiveMessageID,
415
+ archiveCanonicalHumanMessageKey,
292
416
  routeKey,
293
417
  currentBotSelector,
294
418
  }) {
295
419
  return ensureArray(envelopes).some((rawEnvelope) => (
296
- doesTelegramEnvelopeMatchMessage(rawEnvelope, {
297
- chatID: archiveChatID,
298
- messageID: archiveMessageID,
299
- })
420
+ (
421
+ doesTelegramEnvelopeMatchMessage(rawEnvelope, {
422
+ chatID: archiveChatID,
423
+ messageID: archiveMessageID,
424
+ })
425
+ || doesTelegramEnvelopeMatchCanonicalHumanMessage(
426
+ rawEnvelope,
427
+ archiveCanonicalHumanMessageKey,
428
+ )
429
+ )
300
430
  && String(safeObject(normalizeTelegramMessageEnvelope(rawEnvelope)).source_origin || "").trim().toLowerCase() === "local_telegram_inbound"
301
431
  && !isTelegramLocalInboundEnvelopeForRoute(rawEnvelope, {
302
432
  routeKey,
@@ -352,6 +482,7 @@ function resolveRunnerReceiptBackedHumanInboundVisibility({
352
482
  archivedSourceEnvelope,
353
483
  archiveChatID,
354
484
  archiveMessageID,
485
+ archiveCanonicalHumanMessageKey,
355
486
  routeLocalReceipt,
356
487
  persistedSourceEnvelope,
357
488
  } = safeObject(localMatch.candidates);
@@ -367,6 +498,7 @@ function resolveRunnerReceiptBackedHumanInboundVisibility({
367
498
  envelopes: [persistedSourceEnvelope, archivedSourceEnvelope],
368
499
  archiveChatID,
369
500
  archiveMessageID,
501
+ archiveCanonicalHumanMessageKey,
370
502
  routeKey,
371
503
  currentBotSelector,
372
504
  });
@@ -413,6 +545,7 @@ function buildRunnerDiagnosticTraceSummary({
413
545
  archivedSourceEnvelope,
414
546
  archiveChatID,
415
547
  archiveMessageID,
548
+ archiveCanonicalHumanMessageKey,
416
549
  persistedSourceEnvelope,
417
550
  routeLocalEnvelope,
418
551
  routeLocalReceipt,
@@ -436,6 +569,7 @@ function buildRunnerDiagnosticTraceSummary({
436
569
  envelopes: [persistedSourceEnvelope, archivedSourceEnvelope],
437
570
  archiveChatID,
438
571
  archiveMessageID,
572
+ archiveCanonicalHumanMessageKey,
439
573
  routeKey,
440
574
  currentBotSelector,
441
575
  });
@@ -88,6 +88,7 @@ export function buildRunnerProcessedLifecycleInput({
88
88
  const processed = safeObject(processedRaw);
89
89
  const result = safeObject(processed.result);
90
90
  const normalizedOutcome = normalizeRunnerProcessedLifecycleOutcome(processed);
91
+ const normalizedFailureFacts = safeObject(result.failure_facts);
91
92
  return {
92
93
  requestKey,
93
94
  selectedRecord,
@@ -95,6 +96,8 @@ export function buildRunnerProcessedLifecycleInput({
95
96
  outcome: normalizedOutcome,
96
97
  closedReason: normalizedOutcome === "skipped"
97
98
  ? String(processed.skippedRecord?.reason || result.detail || "skipped").trim() || "skipped"
99
+ : ["error", "execution_failed", "policy_violation"].includes(normalizedOutcome)
100
+ ? String(result.detail || "execution_error").trim() || "execution_error"
98
101
  : "",
99
102
  conversationIDRaw: String(result.conversation_id || "").trim(),
100
103
  conversationParticipants: ensureArray(result.conversation_participants),
@@ -126,6 +129,8 @@ export function buildRunnerProcessedLifecycleInput({
126
129
  assignmentValidationStatus: String(result.assignment_validation_status || "").trim(),
127
130
  assignmentValidationReason: String(result.assignment_validation_reason || "").trim(),
128
131
  assignmentValidationModes: ensureArray(result.assignment_validation_modes),
132
+ failureReplyClassification: String(result.failure_reply_classification || "").trim(),
133
+ failureFacts: normalizedFailureFacts,
129
134
  deliveryStatus: String(result.delivery_status || "").trim(),
130
135
  archiveStatus: String(result.archive_status || "").trim(),
131
136
  transportError: String(result.transport_error || "").trim(),
@@ -528,6 +528,27 @@ function normalizeRunnerRecentLocalInboundReceipt(rawReceipt, fallbackKey = "")
528
528
  if (chatTitle) {
529
529
  normalized.chat_title = chatTitle;
530
530
  }
531
+ const canonicalHumanMessageKey = String(
532
+ safeObject(normalizeTelegramMessageEnvelope({
533
+ chat_id: normalized.chat_id,
534
+ message_id: normalized.message_id,
535
+ message_thread_id: normalized.message_thread_id,
536
+ reply_to_message_id: normalized.reply_to_message_id,
537
+ kind: normalized.kind,
538
+ sender_id: normalized.sender_id,
539
+ sender_username: normalized.sender_username,
540
+ sender_is_bot: normalized.sender_is_bot === true,
541
+ body: normalized.body,
542
+ occurred_at: normalized.occurred_at,
543
+ canonical_human_message_key: firstNonEmptyString([
544
+ receipt.canonical_human_message_key,
545
+ receipt.canonicalHumanMessageKey,
546
+ ]),
547
+ })).canonical_human_message_key || "",
548
+ ).trim();
549
+ if (canonicalHumanMessageKey) {
550
+ normalized.canonical_human_message_key = canonicalHumanMessageKey;
551
+ }
531
552
  return [receiptKey, normalized];
532
553
  }
533
554
 
@@ -361,6 +361,7 @@ export async function runSelftestBotCommands(push, deps) {
361
361
  const cliPath = String(requireDependency(deps, "cliPath") || "").trim();
362
362
  const parseSimpleEnvText = requireDependency(deps, "parseSimpleEnvText");
363
363
  const resolveLocalAIExecutionModel = requireDependency(deps, "resolveLocalAIExecutionModel");
364
+ const resolveGeminiHeadlessExecutionModel = requireDependency(deps, "resolveGeminiHeadlessExecutionModel");
364
365
  const suggestLocalAIModelDisplayName = requireDependency(deps, "suggestLocalAIModelDisplayName");
365
366
  const resolveGeminiReasoningConfig = requireDependency(deps, "resolveGeminiReasoningConfig");
366
367
  const stripLocalOnlyToolArgs = requireDependency(deps, "stripLocalOnlyToolArgs");
@@ -392,6 +393,18 @@ export async function runSelftestBotCommands(push, deps) {
392
393
  ].join(" "),
393
394
  );
394
395
 
396
+ push(
397
+ "gemini_headless_runner_uses_explicit_stable_execution_model",
398
+ resolveGeminiHeadlessExecutionModel("gemini-3.1-pro", {
399
+ permissionMode: "read_only",
400
+ reasoningEffort: "low",
401
+ }) === "gemini-3-flash-preview",
402
+ `gemini_headless=${resolveGeminiHeadlessExecutionModel("gemini-3.1-pro", {
403
+ permissionMode: "read_only",
404
+ reasoningEffort: "low",
405
+ })}`,
406
+ );
407
+
395
408
  push(
396
409
  "blank_model_defaults_to_first_display_model_for_each_client",
397
410
  suggestLocalAIModelDisplayName("gpt", "") === "gpt-5.4"
@@ -409,7 +422,7 @@ export async function runSelftestBotCommands(push, deps) {
409
422
  const geminiHighReasoning = resolveGeminiReasoningConfig("gemini-3.1-pro", "high");
410
423
  push(
411
424
  "gemini_reasoning_effort_maps_to_runtime_settings_override",
412
- String(geminiLowReasoning?.model || "") === "auto-gemini-3"
425
+ String(geminiLowReasoning?.model || "") === "gemini-3-flash-preview"
413
426
  && String(safeObject(geminiLowReasoning?.thinkingConfig).thinkingLevel || "") === "LOW"
414
427
  && String(safeObject(geminiMediumReasoning?.thinkingConfig).thinkingLevel || "") === "THINKING_LEVEL_UNSPECIFIED"
415
428
  && String(safeObject(geminiHighReasoning?.thinkingConfig).thinkingLevel || "") === "HIGH",
@@ -2303,20 +2303,31 @@ export async function runSelftestRunnerScenarios(push, deps) {
2303
2303
  },
2304
2304
  });
2305
2305
  const sharedHumanState = loadBotRunnerState();
2306
- const sharedHumanRequestCount = Object.values(safeObject(sharedHumanState.requests))
2306
+ const sharedHumanRequests = Object.values(safeObject(sharedHumanState.requests))
2307
2307
  .filter((entryRaw) => {
2308
2308
  const entry = safeObject(entryRaw);
2309
2309
  return String(entry.chat_id || "") === "-100123"
2310
2310
  && String(entry.source_message_body || "") === sharedHumanBody;
2311
- })
2312
- .length;
2311
+ });
2312
+ const sharedHumanRequestCount = sharedHumanRequests.length;
2313
+ const sharedHumanCanonicalKey = buildArchivedInboundMessageKey({
2314
+ chatID: "-100123",
2315
+ messageID: 1189,
2316
+ messageThreadID: 0,
2317
+ kind: "telegram_message",
2318
+ senderID: "7001",
2319
+ senderIsBot: false,
2320
+ occurredAt: "2026-04-01T06:00:00.000Z",
2321
+ body: sharedHumanBody,
2322
+ }).replace(/^human:/, "");
2313
2323
  push(
2314
2324
  "runner_human_opening_request_identity_is_canonical_across_routes",
2315
2325
  sharedHumanClaimPrimary.ok === true
2316
2326
  && (sharedHumanClaimPeer.ok === true || String(sharedHumanClaimPeer.reason || "") === "request_already_claimed")
2317
2327
  && String(sharedHumanClaimPrimary.requestKey || "") === String(sharedHumanClaimPeer.requestKey || "")
2318
- && sharedHumanRequestCount === 1,
2319
- `primary=${String(sharedHumanClaimPrimary.requestKey || "(none)")} peer=${String(sharedHumanClaimPeer.requestKey || "(none)")} peer_reason=${String(sharedHumanClaimPeer.reason || "(none)")} count=${sharedHumanRequestCount}`,
2328
+ && sharedHumanRequestCount === 1
2329
+ && String(safeObject(sharedHumanRequests[0]).canonical_human_message_key || "") === sharedHumanCanonicalKey,
2330
+ `primary=${String(sharedHumanClaimPrimary.requestKey || "(none)")} peer=${String(sharedHumanClaimPeer.requestKey || "(none)")} peer_reason=${String(sharedHumanClaimPeer.reason || "(none)")} count=${sharedHumanRequestCount} canonical=${String(safeObject(sharedHumanRequests[0]).canonical_human_message_key || "(none)")}`,
2320
2331
  );
2321
2332
 
2322
2333
  const rootTaskRecord = {
@@ -3234,6 +3245,73 @@ export async function runSelftestRunnerScenarios(push, deps) {
3234
3245
  `status=${String(failedRequest?.status || "(none)")} reason=${String(failedRequest?.closed_reason || "(none)")} closed_at=${String(failedRequest?.closed_at || "(none)")}`,
3235
3246
  );
3236
3247
 
3248
+ saveBotRunnerState({
3249
+ routes: {
3250
+ [requestRouteKey]: {},
3251
+ },
3252
+ sharedInboxes: {},
3253
+ excludedComments: {},
3254
+ requests: {
3255
+ "request-key-2f": {
3256
+ request_key: "request-key-2f",
3257
+ project_id: selftestProjectID,
3258
+ provider: "telegram",
3259
+ chat_id: "-100123",
3260
+ source_message_id: 751,
3261
+ conversation_id: "conv-request-2f",
3262
+ execution_contract_type: "delegation",
3263
+ execution_contract_targets: ["ryoai3_bot"],
3264
+ next_expected_responders: ["ryoai3_bot"],
3265
+ authoritative_decision_bundle: {
3266
+ schema_version: "runner_conversation_decision.v1",
3267
+ decision_type: "reply_outcome",
3268
+ conversation_intent_mode: "delegated_single_lead",
3269
+ allowed_responders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
3270
+ initial_responders: ["ryoai_bot"],
3271
+ selected_bot_usernames: ["ryoai2_bot"],
3272
+ allow_bot_to_bot: true,
3273
+ execution_contract_type: "delegation",
3274
+ execution_contract_targets: ["ryoai3_bot"],
3275
+ next_expected_responders: ["ryoai3_bot"],
3276
+ should_close_after_reply: false,
3277
+ },
3278
+ decision_bundle_validation_status: "valid",
3279
+ status: "running",
3280
+ claimed_by_route: requestRouteKey,
3281
+ },
3282
+ },
3283
+ consumedComments: {},
3284
+ });
3285
+ const retryableFailedDelegation = markRunnerRequestLifecycle({
3286
+ normalizedRoute: requestRoute,
3287
+ requestKey: "request-key-2f",
3288
+ selectedRecord: {
3289
+ id: "comment-request-finish-2f",
3290
+ parsedArchive: {
3291
+ kind: "bot_reply",
3292
+ chatID: "-100123",
3293
+ messageID: 752,
3294
+ conversationID: "conv-request-2f",
3295
+ },
3296
+ },
3297
+ routeKey: requestRouteKey,
3298
+ outcome: "error",
3299
+ closedReason: "Gemini CLI timed out after 90s while waiting for a model response (No capacity available for model gemini-3.1-pro-preview on the server)",
3300
+ currentBotSelector: "@RyoAI2_bot",
3301
+ failureReplyClassification: "retryable_failure",
3302
+ failureFacts: {
3303
+ retryable: true,
3304
+ error_type: "provider_capacity_exhausted",
3305
+ },
3306
+ });
3307
+ push(
3308
+ "runner_request_lifecycle_retryable_delegated_error_stays_running",
3309
+ String(retryableFailedDelegation?.status || "") === "running"
3310
+ && ensureArray(retryableFailedDelegation?.next_expected_responders).includes("ryoai3_bot")
3311
+ && String(retryableFailedDelegation?.closed_reason || "").trim() === "",
3312
+ `status=${String(retryableFailedDelegation?.status || "(none)")} next=${ensureArray(retryableFailedDelegation?.next_expected_responders).join(",")} closed_reason=${String(retryableFailedDelegation?.closed_reason || "(none)")}`,
3313
+ );
3314
+
3237
3315
  saveBotRunnerState({
3238
3316
  routes: {
3239
3317
  [requestRouteKey]: {},
@@ -16534,6 +16612,97 @@ export async function runSelftestRunnerScenarios(push, deps) {
16534
16612
  }
16535
16613
  }
16536
16614
 
16615
+ try {
16616
+ const canonicalVisibility = resolveRunnerHumanInboundVisibility({
16617
+ routeState: {
16618
+ recent_local_inbound_envelopes: {},
16619
+ recent_local_inbound_receipts: {
16620
+ "-100123:5201": {
16621
+ chat_id: "-100123",
16622
+ message_id: 5201,
16623
+ sender_id: "7001",
16624
+ body: "@ryoai_bot canonical hello",
16625
+ occurred_at: "2026-04-02T00:00:00.000Z",
16626
+ receipt_origin: "local_telegram_inbound",
16627
+ receipt_route_key: "telegram-monitor-canonical-owner::project::telegram::monitor::dest::actor",
16628
+ receipt_bot_username: "ryoai_bot",
16629
+ received_at: "2026-04-02T00:00:01.000Z",
16630
+ },
16631
+ },
16632
+ },
16633
+ persistedRequest: null,
16634
+ selectedRecord: {
16635
+ id: "comment-canonical-visibility-1",
16636
+ parsedArchive: {
16637
+ kind: "telegram_message",
16638
+ chatID: "-100123",
16639
+ chatType: "supergroup",
16640
+ body: "@ryoai_bot canonical hello",
16641
+ messageID: 6301,
16642
+ senderID: "7001",
16643
+ senderIsBot: false,
16644
+ occurredAt: "2026-04-02T00:00:00.000Z",
16645
+ mentionUsernames: ["ryoai_bot"],
16646
+ },
16647
+ },
16648
+ routeKey: "telegram-monitor-canonical-owner::project::telegram::monitor::dest::actor",
16649
+ currentBotSelector: "ryoai_bot",
16650
+ triggerDecision: {
16651
+ shouldRespond: true,
16652
+ triggerEligible: true,
16653
+ candidateHintOnly: true,
16654
+ trigger: "mention",
16655
+ reason: "canonical receipt-backed mention trigger",
16656
+ candidateBotUsernames: ["ryoai_bot"],
16657
+ },
16658
+ });
16659
+ const canonicalSourceEnvelope = resolveRunnerDeliverySourceMessageEnvelope({
16660
+ routeState: {
16661
+ recent_local_inbound_envelopes: {},
16662
+ recent_local_inbound_receipts: {
16663
+ "-100123:5201": {
16664
+ chat_id: "-100123",
16665
+ message_id: 5201,
16666
+ sender_id: "7001",
16667
+ body: "@ryoai_bot canonical hello",
16668
+ occurred_at: "2026-04-02T00:00:00.000Z",
16669
+ receipt_origin: "local_telegram_inbound",
16670
+ receipt_route_key: "telegram-monitor-canonical-owner::project::telegram::monitor::dest::actor",
16671
+ receipt_bot_username: "ryoai_bot",
16672
+ received_at: "2026-04-02T00:00:01.000Z",
16673
+ },
16674
+ },
16675
+ },
16676
+ persistedRequest: null,
16677
+ selectedRecord: {
16678
+ id: "comment-canonical-visibility-1",
16679
+ parsedArchive: {
16680
+ kind: "telegram_message",
16681
+ chatID: "-100123",
16682
+ chatType: "supergroup",
16683
+ body: "@ryoai_bot canonical hello",
16684
+ messageID: 6301,
16685
+ senderID: "7001",
16686
+ senderIsBot: false,
16687
+ occurredAt: "2026-04-02T00:00:00.000Z",
16688
+ mentionUsernames: ["ryoai_bot"],
16689
+ },
16690
+ },
16691
+ routeKey: "telegram-monitor-canonical-owner::project::telegram::monitor::dest::actor",
16692
+ currentBotSelector: "ryoai_bot",
16693
+ });
16694
+ push(
16695
+ "runner_visibility_receipt_backed_human_opening_matches_canonical_key_across_route_local_copies",
16696
+ canonicalVisibility.applies === true
16697
+ && canonicalVisibility.executable === true
16698
+ && String(canonicalVisibility.visibilitySource || "") === "route_local_inbound_receipt"
16699
+ && Number(safeObject(canonicalSourceEnvelope).message_id || 0) === 5201,
16700
+ `mode=${String(canonicalVisibility.visibilityMode || "(none)")} status=${String(canonicalVisibility.visibilityStatus || "(none)")} source=${String(canonicalVisibility.visibilitySource || "(none)")} reply_message_id=${String(safeObject(canonicalSourceEnvelope).message_id || "(none)")}`,
16701
+ );
16702
+ } catch (err) {
16703
+ push("runner_visibility_receipt_backed_human_opening_matches_canonical_key_across_route_local_copies", false, String(err?.message || err));
16704
+ }
16705
+
16537
16706
  for (const [testName, triggerKind, messageID] of [
16538
16707
  [
16539
16708
  "runner_visibility_receipt_first_mention_without_exact_receipt_is_blocked",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.281",
3
+ "version": "0.2.283",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [