metheus-governance-mcp-cli 0.2.282 → 0.2.284

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,
@@ -144,10 +145,10 @@ import {
144
145
  findEarlierProcessableArchiveDuplicate,
145
146
  findRecentTelegramMessageEnvelope,
146
147
  isTelegramLocalInboundEnvelopeForRoute,
147
- isInboundArchiveKind,
148
- normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
149
- normalizeArchiveCommentRecord,
150
- selectPendingArchiveComments,
148
+ isInboundArchiveKind,
149
+ normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
150
+ normalizeArchiveCommentRecord,
151
+ selectPendingArchiveComments,
151
152
  printRunnerResult,
152
153
  } from "./lib/runner-helpers.mjs";
153
154
  import {
@@ -4144,12 +4145,14 @@ function buildRunnerValidationAndDeliverySummary({
4144
4145
  responseContractValidationStatus = "",
4145
4146
  responseContractValidationReason = "",
4146
4147
  responseContractValidationTargets = [],
4147
- assignmentValidationStatus = "",
4148
- assignmentValidationReason = "",
4149
- assignmentValidationModes = [],
4150
- deliveryStatus = "",
4151
- archiveStatus = "",
4152
- transportError = "",
4148
+ assignmentValidationStatus = "",
4149
+ assignmentValidationReason = "",
4150
+ assignmentValidationModes = [],
4151
+ failureReplyClassification = "",
4152
+ failureFacts = {},
4153
+ deliveryStatus = "",
4154
+ archiveStatus = "",
4155
+ transportError = "",
4153
4156
  archiveError = "",
4154
4157
  sourceMessageEnvelope = {},
4155
4158
  lastReplyMessageEnvelope = {},
@@ -6381,12 +6384,14 @@ function markRunnerRequestLifecycle({
6381
6384
  responseContractValidationStatus = "",
6382
6385
  responseContractValidationReason = "",
6383
6386
  responseContractValidationTargets = [],
6384
- assignmentValidationStatus = "",
6385
- assignmentValidationReason = "",
6386
- assignmentValidationModes = [],
6387
- deliveryStatus = "",
6388
- archiveStatus = "",
6389
- transportError = "",
6387
+ assignmentValidationStatus = "",
6388
+ assignmentValidationReason = "",
6389
+ assignmentValidationModes = [],
6390
+ failureReplyClassification = "",
6391
+ failureFacts = {},
6392
+ deliveryStatus = "",
6393
+ archiveStatus = "",
6394
+ transportError = "",
6390
6395
  archiveError = "",
6391
6396
  lastReplyMessageID = 0,
6392
6397
  lastReplyMessageThreadID = 0,
@@ -6425,10 +6430,7 @@ function markRunnerRequestLifecycle({
6425
6430
  const authoritativeDecisionBundle = resolvedDecisionBundleValidation.ok === true
6426
6431
  ? safeObject(resolvedDecisionBundleValidation.bundle)
6427
6432
  : runnerRequestAuthoritativeDecisionBundle(existing);
6428
- const effectiveReplyToMessageID = intFromRawAllowZero(
6429
- replyToMessageID,
6430
- intFromRawAllowZero(existing.last_reply_to_message_id, 0),
6431
- );
6433
+ const effectiveReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
6432
6434
  const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
6433
6435
  sourceEnvelope: sourceMessageEnvelope,
6434
6436
  chatID: existing.chat_id,
@@ -6449,14 +6451,14 @@ function markRunnerRequestLifecycle({
6449
6451
  senderUsername: normalizedCurrentBotSelector,
6450
6452
  body: aiReplyPreview,
6451
6453
  });
6452
- const shouldRefreshAttemptedDeliveryEnvelope = (
6453
- aiReplyGenerated === true
6454
- || String(aiReplyPreview || "").trim().length > 0
6455
- || String(deliveryStatus || "").trim().length > 0
6456
- || String(transportError || "").trim().length > 0
6457
- || intFromRawAllowZero(replyToMessageID, 0) > 0
6458
- || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6459
- );
6454
+ const shouldRefreshAttemptedDeliveryEnvelope = (
6455
+ aiReplyGenerated === true
6456
+ || String(aiReplyPreview || "").trim().length > 0
6457
+ || String(deliveryStatus || "").trim().length > 0
6458
+ || String(transportError || "").trim().length > 0
6459
+ || intFromRawAllowZero(replyToMessageID, 0) > 0
6460
+ || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6461
+ );
6460
6462
  const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
6461
6463
  [
6462
6464
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets),
@@ -6497,6 +6499,15 @@ function markRunnerRequestLifecycle({
6497
6499
  || "",
6498
6500
  ).trim().toLowerCase();
6499
6501
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6502
+ const normalizedFailureReplyClassification = String(failureReplyClassification || "").trim().toLowerCase();
6503
+ const normalizedFailureFacts = safeObject(failureFacts);
6504
+ const shouldPersistReplyAnchor = (
6505
+ aiReplyGenerated === true
6506
+ || intFromRawAllowZero(lastReplyMessageID, 0) > 0
6507
+ || ["delivered", "dry_run", "archive_error", "failed_transport"].includes(normalizedDeliveryStatus)
6508
+ || String(transportError || "").trim().length > 0
6509
+ || ["replied", "delivery_failed_after_generation"].includes(normalizedOutcome)
6510
+ );
6500
6511
  const shouldRemainRunningAfterReply = authoritativeDecisionBundle.should_close_after_reply === true
6501
6512
  ? false
6502
6513
  : authoritativeDecisionBundle.should_close_after_reply === false
@@ -6511,6 +6522,18 @@ function markRunnerRequestLifecycle({
6511
6522
  || rootEffectiveNextExpectedResponders.length > 0
6512
6523
  || continuationSelectors.length > 0
6513
6524
  );
6525
+ const shouldRemainRunningAfterError = ["error", "execution_failed"].includes(normalizedOutcome)
6526
+ && (
6527
+ normalizedFailureFacts.retryable === true
6528
+ || normalizedFailureReplyClassification === "retryable_failure"
6529
+ )
6530
+ && authoritativeDecisionBundle.should_close_after_reply !== true
6531
+ && (
6532
+ nextExecutionContractType === "delegation"
6533
+ || rootEffectiveExecutionContractTargets.length > 0
6534
+ || rootEffectiveNextExpectedResponders.length > 0
6535
+ || continuationSelectors.length > 0
6536
+ );
6514
6537
  const nextConversationIntentMode = String(
6515
6538
  authoritativeDecisionBundle.conversation_intent_mode
6516
6539
  || conversationIntentMode
@@ -6547,7 +6570,7 @@ function markRunnerRequestLifecycle({
6547
6570
  || normalizedOutcome === "execution_failed"
6548
6571
  || normalizedOutcome === "policy_violation"
6549
6572
  ) {
6550
- return "closed";
6573
+ return shouldRemainRunningAfterError ? "running" : "closed";
6551
6574
  }
6552
6575
  return normalizeRunnerRequestStatus(existing.status);
6553
6576
  })();
@@ -6888,7 +6911,9 @@ function markRunnerRequestLifecycle({
6888
6911
  last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || existing.last_source_message_thread_id,
6889
6912
  last_reply_message_id: intFromRawAllowZero(lastReplyMessageID, 0) || existing.last_reply_message_id,
6890
6913
  last_reply_message_thread_id: intFromRawAllowZero(lastReplyMessageThreadID, 0) || existing.last_reply_message_thread_id,
6891
- last_reply_to_message_id: intFromRawAllowZero(replyToMessageID, 0) || existing.last_reply_to_message_id,
6914
+ last_reply_to_message_id: shouldPersistReplyAnchor
6915
+ ? effectiveReplyToMessageID
6916
+ : existing.last_reply_to_message_id,
6892
6917
  last_reply_message_envelope: persistSuccessfulReplyEnvelope
6893
6918
  ? lastReplyMessageEnvelope
6894
6919
  : safeObject(existing.last_reply_message_envelope),
@@ -19554,12 +19579,13 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
19554
19579
  push("runner_tui_frame_renders_route_statuses", false, String(err?.message || err));
19555
19580
  }
19556
19581
 
19557
- await runSelftestBotCommands(push, {
19558
- cliPath: fileURLToPath(import.meta.url),
19559
- parseSimpleEnvText,
19560
- resolveLocalAIExecutionModel,
19561
- suggestLocalAIModelDisplayName,
19562
- resolveGeminiReasoningConfig,
19582
+ await runSelftestBotCommands(push, {
19583
+ cliPath: fileURLToPath(import.meta.url),
19584
+ parseSimpleEnvText,
19585
+ resolveLocalAIExecutionModel,
19586
+ resolveGeminiHeadlessExecutionModel,
19587
+ suggestLocalAIModelDisplayName,
19588
+ resolveGeminiReasoningConfig,
19563
19589
  stripLocalOnlyToolArgs: (requestObj, toolName) =>
19564
19590
  stripLocalOnlyToolArgs(requestObj, toolName),
19565
19591
  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") {
@@ -13,6 +13,14 @@ function intFromRawAllowZero(value, fallback = 0) {
13
13
  return Number.isFinite(parsed) ? parsed : fallback;
14
14
  }
15
15
 
16
+ function buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID) {
17
+ return `reply anchor mismatch: expected ${String(expectedReplyToMessageID || 0)}, observed ${String(observedReplyToMessageID || 0)}`;
18
+ }
19
+
20
+ function buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID) {
21
+ return `message thread mismatch: expected ${String(expectedMessageThreadID || 0)}, observed ${String(observedMessageThreadID || 0)}`;
22
+ }
23
+
16
24
  function requireArchiveDependency(deps, key) {
17
25
  const candidate = deps?.[key];
18
26
  if (typeof candidate !== "function") {
@@ -83,11 +91,41 @@ export async function finalizeLocalBotDeliveryArchive({
83
91
  deliveredResult.message_id ?? deliveredBody.message_id ?? deliveredBody.ts,
84
92
  0,
85
93
  );
86
- const deliveredMessageThreadID = intFromRawAllowZero(
94
+ const observedMessageThreadID = intFromRawAllowZero(
87
95
  deliveredResult.message_thread_id ?? deliveredBody.message_thread_id ?? delivery.effectiveMessageThreadID,
88
96
  intFromRawAllowZero(messageThreadID, 0),
89
97
  );
90
- const archiveReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, replyToMessageID);
98
+ const expectedMessageThreadID = intFromRawAllowZero(messageThreadID, 0);
99
+ const expectedReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
100
+ const observedReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, 0);
101
+ if (
102
+ (expectedReplyToMessageID > 0 || observedReplyToMessageID > 0)
103
+ && observedReplyToMessageID !== expectedReplyToMessageID
104
+ ) {
105
+ return {
106
+ ok: false,
107
+ error: buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID),
108
+ reply_anchor_mismatch: true,
109
+ expected_reply_to_message_id: expectedReplyToMessageID,
110
+ observed_reply_to_message_id: observedReplyToMessageID,
111
+ thread_id: thread.threadID,
112
+ work_item_id: thread.workItemID,
113
+ };
114
+ }
115
+ if (
116
+ (expectedMessageThreadID > 0 || observedMessageThreadID > 0)
117
+ && observedMessageThreadID !== expectedMessageThreadID
118
+ ) {
119
+ return {
120
+ ok: false,
121
+ error: buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID),
122
+ message_thread_mismatch: true,
123
+ expected_message_thread_id: expectedMessageThreadID,
124
+ observed_message_thread_id: observedMessageThreadID,
125
+ thread_id: thread.threadID,
126
+ work_item_id: thread.workItemID,
127
+ };
128
+ }
91
129
  if (archiveDedupeOutbound && deliveredMessageID > 0) {
92
130
  const existingComments = await listThreadCommentsTail({
93
131
  siteBaseURL,
@@ -119,8 +157,8 @@ export async function finalizeLocalBotDeliveryArchive({
119
157
  destination,
120
158
  replyText: text,
121
159
  messageID: deliveredMessageID,
122
- messageThreadID: deliveredMessageThreadID,
123
- replyToMessageID: archiveReplyToMessageID,
160
+ messageThreadID: expectedMessageThreadID,
161
+ replyToMessageID: expectedReplyToMessageID,
124
162
  conversation: archiveConversation,
125
163
  });
126
164
  const createdComment = await createThreadComment({
@@ -314,6 +314,27 @@ export function buildTelegramMessageEnvelopeFromParsedArchive(parsedArchiveRaw,
314
314
  });
315
315
  }
316
316
 
317
+ export function resolveTelegramReplyAnchorMessageID({
318
+ replyToMessageID = 0,
319
+ sourceEnvelope: sourceEnvelopeRaw = {},
320
+ fallbackReplyToMessageID = 0,
321
+ } = {}) {
322
+ const explicitReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
323
+ if (explicitReplyToMessageID > 0) {
324
+ return explicitReplyToMessageID;
325
+ }
326
+ const sourceEnvelope = normalizeTelegramMessageEnvelope(sourceEnvelopeRaw);
327
+ const sourceMessageID = intFromRawAllowZero(sourceEnvelope.message_id, 0);
328
+ if (sourceMessageID > 0) {
329
+ return sourceMessageID;
330
+ }
331
+ const sourceReplyToMessageID = intFromRawAllowZero(sourceEnvelope.reply_to_message_id, 0);
332
+ if (sourceReplyToMessageID > 0) {
333
+ return sourceReplyToMessageID;
334
+ }
335
+ return intFromRawAllowZero(fallbackReplyToMessageID, 0);
336
+ }
337
+
317
338
  export function buildTelegramBotReplyEnvelope({
318
339
  sourceEnvelope: sourceEnvelopeRaw = {},
319
340
  chatID = "",
@@ -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,3 +1,7 @@
1
+ import {
2
+ resolveTelegramReplyAnchorMessageID,
3
+ } from "./runner-helpers.mjs";
4
+
1
5
  function safeObject(value) {
2
6
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3
7
  return {};
@@ -154,9 +158,20 @@ export function prepareRunnerSelectedRecordIngress({
154
158
  currentBotSelector,
155
159
  });
156
160
  const replyMessageThreadID = intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0);
157
- const replyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
161
+ const sourceMessageID = intFromRawAllowZero(sourceMessageEnvelope.message_id, 0);
162
+ const sourceReplyToMessageID = intFromRawAllowZero(sourceMessageEnvelope.reply_to_message_id, 0);
163
+ const replyToMessageID = resolveTelegramReplyAnchorMessageID({
164
+ replyToMessageID: sourceMessageID,
165
+ sourceEnvelope: sourceMessageEnvelope,
166
+ });
158
167
  const replyAnchorSource = String(sourceMessageEnvelope.source_origin || "").trim()
159
- || (replyToMessageID > 0 ? "source_message_envelope" : "");
168
+ || (replyToMessageID > 0
169
+ ? sourceMessageID > 0
170
+ ? "source_message_envelope"
171
+ : sourceReplyToMessageID > 0
172
+ ? "source_message_envelope_reply_to"
173
+ : ""
174
+ : "");
160
175
 
161
176
  return {
162
177
  handledResult: null,
@@ -29,6 +29,10 @@ function buildConversationSummaryDetail({
29
29
  ].filter(Boolean).join(" | ");
30
30
  }
31
31
 
32
+ function buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID) {
33
+ return `message thread mismatch: expected ${String(expectedMessageThreadID || 0)}, observed ${String(observedMessageThreadID || 0)}`;
34
+ }
35
+
32
36
  function buildSelectedRecordReplyOutcomeBase({
33
37
  routeKey,
34
38
  normalizedRoute,
@@ -354,11 +358,19 @@ export async function finalizeRunnerSelectedRecordReplyOutcome({
354
358
  deliveryBody.result?.message_id ?? deliveryBody.message_id,
355
359
  0,
356
360
  );
357
- const effectiveReplyMessageThreadID = intFromRawAllowZero(
361
+ const expectedReplyMessageThreadID = intFromRawAllowZero(replyMessageThreadID, 0);
362
+ const observedReplyMessageThreadID = intFromRawAllowZero(
358
363
  deliveryResult?.delivery?.effectiveMessageThreadID,
359
- replyMessageThreadID,
364
+ 0,
365
+ );
366
+ const messageThreadMismatch = (
367
+ (expectedReplyMessageThreadID > 0 || observedReplyMessageThreadID > 0)
368
+ && observedReplyMessageThreadID !== expectedReplyMessageThreadID
360
369
  );
361
- const effectiveReplyToMessageID = intFromRawAllowZero(deliveryResult?.delivery?.effectiveReplyToMessageID, replyToMessageID);
370
+ const messageThreadError = messageThreadMismatch
371
+ ? buildMessageThreadMismatchError(expectedReplyMessageThreadID, observedReplyMessageThreadID)
372
+ : "";
373
+ const effectiveReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
362
374
  const conversationDetail = buildConversationSummaryDetail({
363
375
  effectiveConversationContext,
364
376
  executionContract,
@@ -371,7 +383,7 @@ export async function finalizeRunnerSelectedRecordReplyOutcome({
371
383
  ...buildRunnerRouteStateFromComment(selectedRecord, {
372
384
  last_action: "replied",
373
385
  last_reply_message_id: replyMessageID,
374
- last_reply_message_thread_id: effectiveReplyMessageThreadID,
386
+ last_reply_message_thread_id: expectedReplyMessageThreadID,
375
387
  last_reply_anchor_source: replyAnchorSource,
376
388
  last_contract_validation_status: String(responseContractValidation?.status || "").trim(),
377
389
  last_contract_validation_reason: String(responseContractValidation?.reason || "").trim(),
@@ -445,17 +457,21 @@ export async function finalizeRunnerSelectedRecordReplyOutcome({
445
457
  evidence_ids: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).id || "").trim()).filter(Boolean),
446
458
  evidence_paths: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).path || "").trim()).filter(Boolean),
447
459
  last_reply_message_id: replyMessageID,
448
- last_reply_message_thread_id: effectiveReplyMessageThreadID,
460
+ last_reply_message_thread_id: expectedReplyMessageThreadID,
449
461
  reply_to_message_id: effectiveReplyToMessageID,
450
462
  last_reply_message_envelope: buildTelegramBotReplyEnvelope({
451
463
  sourceEnvelope: sourceMessageEnvelope,
452
464
  messageID: replyMessageID,
453
- messageThreadID: effectiveReplyMessageThreadID,
465
+ messageThreadID: expectedReplyMessageThreadID,
454
466
  replyToMessageID: effectiveReplyToMessageID,
455
467
  sender: bot?.username ? `@${String(bot.username || "").trim().replace(/^@+/, "")}` : String(bot?.name || "bot").trim(),
456
468
  senderUsername: normalizeMentionSelector(bot?.username || bot?.name),
457
469
  body: sanitizedReplyText,
458
470
  }),
471
+ message_thread_mismatch: messageThreadMismatch,
472
+ expected_message_thread_id: expectedReplyMessageThreadID || undefined,
473
+ observed_message_thread_id: observedReplyMessageThreadID || undefined,
474
+ message_thread_error: messageThreadError,
459
475
  reply_anchor_source: replyAnchorSource,
460
476
  reply_fallback_used: deliveryResult?.delivery?.replyFallbackUsed === true,
461
477
  delivery_status: deliveryResult?.delivery?.dryRun ? "dry_run" : "delivered",
@@ -36,14 +36,8 @@ export function prepareRunnerFailureReplyDeliveryHandoff({
36
36
  const sourceMessageEnvelope = Object.keys(safeObject(authoritativeSourceMessageEnvelope)).length > 0
37
37
  ? safeObject(authoritativeSourceMessageEnvelope)
38
38
  : safeObject(result.source_message_envelope);
39
- const replyToMessageID = intFromRawAllowZero(
40
- result.reply_to_message_id,
41
- intFromRawAllowZero(sourceMessageEnvelope.message_id, 0),
42
- );
43
- const messageThreadID = intFromRawAllowZero(
44
- result.reply_message_thread_id,
45
- intFromRawAllowZero(sourceMessageEnvelope.message_thread_id, 0),
46
- );
39
+ const replyToMessageID = intFromRawAllowZero(result.reply_to_message_id, 0);
40
+ const messageThreadID = intFromRawAllowZero(result.reply_message_thread_id, 0);
47
41
  return {
48
42
  siteBaseURL: normalizedRuntime.baseURL,
49
43
  token: normalizedRuntime.token,
@@ -11,6 +11,14 @@ function intFromRawAllowZero(value, fallback = 0) {
11
11
  return Number.isFinite(parsed) ? parsed : fallback;
12
12
  }
13
13
 
14
+ function buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID) {
15
+ return `reply anchor mismatch: expected ${String(expectedReplyToMessageID || 0)}, observed ${String(observedReplyToMessageID || 0)}`;
16
+ }
17
+
18
+ function buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID) {
19
+ return `message thread mismatch: expected ${String(expectedMessageThreadID || 0)}, observed ${String(observedMessageThreadID || 0)}`;
20
+ }
21
+
14
22
  export function finalizeRunnerFailureReplyDeliveryOutcome({
15
23
  processed = {},
16
24
  routeKey = "",
@@ -50,6 +58,24 @@ export function finalizeRunnerFailureReplyDeliveryOutcome({
50
58
  const delivery = safeObject(normalizedDeliveryResult.delivery);
51
59
  const archive = safeObject(normalizedDeliveryResult.archive);
52
60
  const handoff = safeObject(deliveryHandoff);
61
+ const expectedReplyToMessageID = intFromRawAllowZero(handoff.replyToMessageID, 0);
62
+ const observedReplyToMessageID = intFromRawAllowZero(delivery.effectiveReplyToMessageID, 0);
63
+ const expectedMessageThreadID = intFromRawAllowZero(handoff.messageThreadID, 0);
64
+ const observedMessageThreadID = intFromRawAllowZero(delivery.effectiveMessageThreadID, 0);
65
+ const replyAnchorMismatch = (
66
+ (expectedReplyToMessageID > 0 || observedReplyToMessageID > 0)
67
+ && observedReplyToMessageID !== expectedReplyToMessageID
68
+ );
69
+ const replyAnchorError = replyAnchorMismatch
70
+ ? buildReplyAnchorMismatchError(expectedReplyToMessageID, observedReplyToMessageID)
71
+ : "";
72
+ const messageThreadMismatch = (
73
+ (expectedMessageThreadID > 0 || observedMessageThreadID > 0)
74
+ && observedMessageThreadID !== expectedMessageThreadID
75
+ );
76
+ const messageThreadError = messageThreadMismatch
77
+ ? buildMessageThreadMismatchError(expectedMessageThreadID, observedMessageThreadID)
78
+ : "";
53
79
  const mergedResult = {
54
80
  ...result,
55
81
  failure_reply_sent: true,
@@ -69,14 +95,16 @@ export function finalizeRunnerFailureReplyDeliveryOutcome({
69
95
  safeObject(delivery.body).result?.message_id ?? safeObject(delivery.body).message_id,
70
96
  intFromRawAllowZero(result.last_reply_message_id, 0),
71
97
  ),
72
- last_reply_message_thread_id: intFromRawAllowZero(
73
- delivery.effectiveMessageThreadID,
74
- intFromRawAllowZero(handoff.messageThreadID, 0),
75
- ),
76
- reply_to_message_id: intFromRawAllowZero(
77
- delivery.effectiveReplyToMessageID,
78
- intFromRawAllowZero(handoff.replyToMessageID, 0),
79
- ),
98
+ last_reply_message_thread_id: expectedMessageThreadID,
99
+ reply_to_message_id: expectedReplyToMessageID,
100
+ reply_anchor_mismatch: replyAnchorMismatch,
101
+ expected_reply_to_message_id: expectedReplyToMessageID || undefined,
102
+ observed_reply_to_message_id: observedReplyToMessageID || undefined,
103
+ reply_anchor_error: replyAnchorError,
104
+ message_thread_mismatch: messageThreadMismatch,
105
+ expected_message_thread_id: expectedMessageThreadID || undefined,
106
+ observed_message_thread_id: observedMessageThreadID || undefined,
107
+ message_thread_error: messageThreadError,
80
108
  };
81
109
  if (normalizedRouteKey && typeof saveRunnerRouteState === "function") {
82
110
  const currentRouteState = typeof loadRouteState === "function"
@@ -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(),
@@ -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",