metheus-governance-mcp-cli 0.2.195 → 0.2.197

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/README.md CHANGED
@@ -664,6 +664,11 @@ Runner command contract:
664
664
  Notes:
665
665
  - `runner once` processes the most recent pending archived inbound message
666
666
  - `runner start` keeps polling and stores per-route cursor state in `~/.metheus/bot-runner-state.json`
667
+ - request execution is request-ledger first: one human root message becomes one `request_key`, and request status is the real execution gate
668
+ - `conversation_session` is advisory metadata only; participants, last speaker, and next expected responders stay there, but session `open` alone must not revive old work
669
+ - bot replies and public bot-to-bot delegation replies do not create a fresh request on their own; they continue only when an active request already exists for that conversation
670
+ - consumed archived comments are recorded in the runner request comment ledger so the same bot reply does not get re-queued later as new work
671
+ - startup cleanup closes stale open sessions without an active request and excludes their archived bot replies before pending selection runs
667
672
  - first start primes the cursor to the latest inbound message and does not reply to old backlog
668
673
  - when inline filters match a configured route in `~/.metheus/bot-runner.json`, the runner reuses that route's canonical name/destination and state cursor instead of creating a new anonymous route key
669
674
  - stale anonymous route keys in `~/.metheus/bot-runner-state.json` are auto-migrated to the matching configured route when possible; `doctor` warns if ambiguous legacy keys still remain
package/cli.mjs CHANGED
@@ -2363,6 +2363,16 @@ function buildRunnerRequestKey({
2363
2363
  ].join("::");
2364
2364
  }
2365
2365
 
2366
+ function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID) {
2367
+ const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
2368
+ const normalizedChatID = String(chatID || "").trim() || "-";
2369
+ const normalizedAnchorMessageID = intFromRawAllowZero(anchorMessageID, 0);
2370
+ if (normalizedAnchorMessageID <= 0) {
2371
+ return "";
2372
+ }
2373
+ return `reply_chain:${provider}:${normalizedChatID}:${normalizedAnchorMessageID}`;
2374
+ }
2375
+
2366
2376
  function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2367
2377
  const requests = normalizeBotRunnerRequests(state?.requests);
2368
2378
  const projectID = String(normalizedRoute?.projectID || "").trim();
@@ -2386,6 +2396,61 @@ function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2386
2396
  });
2387
2397
  }
2388
2398
 
2399
+ function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
2400
+ const chatID = String(selectors.chatID || "").trim();
2401
+ const messageID = intFromRawAllowZero(selectors.messageID, 0);
2402
+ if (!chatID || messageID <= 0) {
2403
+ return [];
2404
+ }
2405
+ return findRunnerRequestsForScope(state, normalizedRoute, { chatID })
2406
+ .filter((entry) => (
2407
+ intFromRawAllowZero(entry.source_message_id, 0) === messageID
2408
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === messageID
2409
+ ));
2410
+ }
2411
+
2412
+ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
2413
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2414
+ const explicitConversationID = String(parsed.conversationID || "").trim();
2415
+ if (explicitConversationID) {
2416
+ return {
2417
+ conversationID: explicitConversationID,
2418
+ replyToMessageID: intFromRawAllowZero(parsed.replyToMessageID, 0),
2419
+ anchorMessageID: 0,
2420
+ reason: "archive_conversation",
2421
+ referencedRequest: null,
2422
+ };
2423
+ }
2424
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
2425
+ const replyToMessageID = intFromRawAllowZero(parsed.replyToMessageID, 0);
2426
+ if (!chatID || replyToMessageID <= 0) {
2427
+ return {
2428
+ conversationID: "",
2429
+ replyToMessageID,
2430
+ anchorMessageID: 0,
2431
+ reason: "",
2432
+ referencedRequest: null,
2433
+ };
2434
+ }
2435
+ const referencedRequest = safeObject(findRunnerRequestsForMessageID(state, normalizedRoute, {
2436
+ chatID,
2437
+ messageID: replyToMessageID,
2438
+ })[0]);
2439
+ const referencedConversationID = String(referencedRequest.conversation_id || "").trim();
2440
+ const anchorMessageID = intFromRawAllowZero(referencedRequest.source_message_id, 0) || replyToMessageID;
2441
+ return {
2442
+ conversationID: referencedConversationID || buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID),
2443
+ replyToMessageID,
2444
+ anchorMessageID,
2445
+ reason: referencedConversationID
2446
+ ? "reply_request_conversation"
2447
+ : Object.keys(referencedRequest).length > 0
2448
+ ? "reply_request_synthetic"
2449
+ : "reply_message_synthetic",
2450
+ referencedRequest: Object.keys(referencedRequest).length > 0 ? referencedRequest : null,
2451
+ };
2452
+ }
2453
+
2389
2454
  function upsertRunnerRequest(state, requestKey, patch = {}) {
2390
2455
  const currentState = safeObject(state);
2391
2456
  const requests = normalizeBotRunnerRequests(currentState.requests);
@@ -2447,7 +2512,24 @@ function claimRunnerRequestForHumanComment({
2447
2512
  normalizedIntent,
2448
2513
  });
2449
2514
  const currentState = loadBotRunnerState();
2450
- const requests = normalizeBotRunnerRequests(currentState.requests);
2515
+ const replyChainContext = resolveRunnerReplyChainConversationContext(currentState, normalizedRoute, selectedRecord);
2516
+ const conversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
2517
+ let stateForClaim = currentState;
2518
+ if (
2519
+ replyChainContext.referencedRequest
2520
+ && conversationID
2521
+ && !String(replyChainContext.referencedRequest.conversation_id || "").trim()
2522
+ && String(replyChainContext.referencedRequest.request_key || "").trim()
2523
+ ) {
2524
+ const backfilled = upsertRunnerRequest(stateForClaim, replyChainContext.referencedRequest.request_key, {
2525
+ conversation_id: conversationID,
2526
+ });
2527
+ stateForClaim = {
2528
+ ...stateForClaim,
2529
+ requests: backfilled.requests,
2530
+ };
2531
+ }
2532
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
2451
2533
  const existing = safeObject(requests[requestKey]);
2452
2534
  if (isFinalRunnerRequestStatus(existing.status)) {
2453
2535
  return {
@@ -2468,14 +2550,14 @@ function claimRunnerRequestForHumanComment({
2468
2550
  };
2469
2551
  }
2470
2552
  const nowISO = new Date().toISOString();
2471
- const { requests: nextRequests, request } = upsertRunnerRequest(currentState, requestKey, {
2553
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
2472
2554
  project_id: String(normalizedRoute?.projectID || "").trim(),
2473
2555
  provider: String(normalizedRoute?.provider || "").trim(),
2474
2556
  chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
2475
2557
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2476
2558
  root_comment_id: String(selectedRecord?.id || "").trim(),
2477
2559
  root_comment_kind: commentKind,
2478
- conversation_id: String(parsed.conversationID || "").trim(),
2560
+ conversation_id: conversationID,
2479
2561
  selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
2480
2562
  normalized_intent: String(normalizedIntent || "").trim().toLowerCase(),
2481
2563
  status: "claimed",
@@ -2485,20 +2567,20 @@ function claimRunnerRequestForHumanComment({
2485
2567
  last_comment_kind: commentKind,
2486
2568
  last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2487
2569
  });
2488
- const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(currentState, selectedRecord?.id, {
2570
+ const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
2489
2571
  project_id: String(normalizedRoute?.projectID || "").trim(),
2490
2572
  provider: String(normalizedRoute?.provider || "").trim(),
2491
2573
  request_key: requestKey,
2492
2574
  route_key: String(routeKey || "").trim(),
2493
- conversation_id: String(parsed.conversationID || "").trim(),
2575
+ conversation_id: conversationID,
2494
2576
  source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2495
2577
  comment_kind: commentKind,
2496
2578
  request_status: "claimed",
2497
2579
  });
2498
2580
  saveBotRunnerState({
2499
- routes: currentState.routes,
2500
- sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
2501
- excludedComments: currentState.excludedComments || currentState.excluded_comments,
2581
+ routes: stateForClaim.routes,
2582
+ sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
2583
+ excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
2502
2584
  requests: nextRequests,
2503
2585
  consumedComments: nextConsumedComments,
2504
2586
  });
@@ -2579,6 +2661,11 @@ function markRunnerRequestLifecycle({
2579
2661
  outcome,
2580
2662
  conversationIDRaw = "",
2581
2663
  allowedResponders = [],
2664
+ executionContractType = "",
2665
+ executionContractTargets = [],
2666
+ nextExpectedResponders = [],
2667
+ currentBotSelector = "",
2668
+ conversationIntentMode = "",
2582
2669
  normalizedIntent = "",
2583
2670
  closedReason = "",
2584
2671
  }) {
@@ -2589,20 +2676,34 @@ function markRunnerRequestLifecycle({
2589
2676
  if (!Object.keys(existing).length) return null;
2590
2677
  const parsed = safeObject(selectedRecord?.parsedArchive);
2591
2678
  const conversationID = String(conversationIDRaw || existing.conversation_id || parsed.conversationID || "").trim();
2592
- const conversationFacts = conversationID
2593
- ? collectBotRunnerConversationSessionFacts(normalizedRoute, conversationID)
2594
- : {};
2679
+ const normalizedCurrentBotSelector = normalizeTelegramMentionUsername(
2680
+ currentBotSelector
2681
+ || safeObject(currentState.routes)[String(routeKey || "").trim()]?.last_speaker_bot_username
2682
+ || "",
2683
+ );
2684
+ const continuationSelectors = uniqueOrderedStrings(
2685
+ [
2686
+ ...ensureArray(executionContractTargets).length
2687
+ ? ensureArray(executionContractTargets)
2688
+ : ensureArray(existing.execution_contract_targets),
2689
+ ...ensureArray(nextExpectedResponders).length
2690
+ ? ensureArray(nextExpectedResponders)
2691
+ : ensureArray(existing.next_expected_responders),
2692
+ ],
2693
+ normalizeTelegramMentionUsername,
2694
+ ).filter((selector) => selector && selector !== normalizedCurrentBotSelector);
2695
+ const shouldRemainRunningAfterReply = continuationSelectors.length > 0;
2595
2696
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
2596
2697
  const nextStatus = (() => {
2597
2698
  if (normalizedOutcome === "claimed") return "claimed";
2598
2699
  if (normalizedOutcome === "running") return "running";
2599
2700
  if (normalizedOutcome === "replied") {
2600
- return conversationFacts.any_open ? "running" : "completed";
2701
+ return shouldRemainRunningAfterReply ? "running" : "completed";
2601
2702
  }
2602
2703
  if (normalizedOutcome === "loop_closed") return "loop_closed";
2603
2704
  if (normalizedOutcome === "expired") return "expired";
2604
2705
  if (normalizedOutcome === "error" || normalizedOutcome === "skipped" || normalizedOutcome === "closed") {
2605
- return conversationFacts.any_open ? "running" : "closed";
2706
+ return "closed";
2606
2707
  }
2607
2708
  return normalizeRunnerRequestStatus(existing.status);
2608
2709
  })();
@@ -2613,6 +2714,24 @@ function markRunnerRequestLifecycle({
2613
2714
  ensureArray(allowedResponders).length ? allowedResponders : existing.conversation_allowed_responders,
2614
2715
  normalizeTelegramMentionUsername,
2615
2716
  ),
2717
+ conversation_intent_mode: String(
2718
+ conversationIntentMode
2719
+ || existing.conversation_intent_mode
2720
+ || "",
2721
+ ).trim().toLowerCase(),
2722
+ execution_contract_type: String(
2723
+ executionContractType
2724
+ || existing.execution_contract_type
2725
+ || "",
2726
+ ).trim().toLowerCase(),
2727
+ execution_contract_targets: uniqueOrderedStrings(
2728
+ ensureArray(executionContractTargets).length ? executionContractTargets : existing.execution_contract_targets,
2729
+ normalizeTelegramMentionUsername,
2730
+ ),
2731
+ next_expected_responders: uniqueOrderedStrings(
2732
+ ensureArray(nextExpectedResponders).length ? nextExpectedResponders : existing.next_expected_responders,
2733
+ normalizeTelegramMentionUsername,
2734
+ ),
2616
2735
  normalized_intent: String(normalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
2617
2736
  status: nextStatus,
2618
2737
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -5107,6 +5226,170 @@ function buildRunnerSmallTalkReply({ route, executionPlan } = {}) {
5107
5226
  return templatePool[stableTextModulo(`${displayName}:${roleProfileName}`, templatePool.length)];
5108
5227
  }
5109
5228
 
5229
+ function summarizeRunnerRequestForStatusLookup(entryRaw) {
5230
+ const entry = safeObject(entryRaw);
5231
+ return {
5232
+ status: String(entry.status || "").trim(),
5233
+ normalized_intent: String(entry.normalized_intent || "").trim(),
5234
+ conversation_id: String(entry.conversation_id || "").trim(),
5235
+ closed_reason: String(entry.closed_reason || "").trim(),
5236
+ claimed_at: firstNonEmptyString([entry.claimed_at, entry.started_at]),
5237
+ started_at: firstNonEmptyString([entry.started_at, entry.claimed_at]),
5238
+ updated_at: String(entry.updated_at || "").trim(),
5239
+ source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
5240
+ last_source_message_id: intFromRawAllowZero(entry.last_source_message_id, 0) || undefined,
5241
+ selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
5242
+ .map((value) => normalizeTelegramMentionUsername(value))
5243
+ .filter(Boolean),
5244
+ };
5245
+ }
5246
+
5247
+ function isInformationalRunnerRequestIntent(intentType) {
5248
+ return new Set([
5249
+ "status_query",
5250
+ "bot_role_query",
5251
+ "workspace_query",
5252
+ "explanation_query",
5253
+ "small_talk",
5254
+ ]).has(String(intentType || "").trim().toLowerCase());
5255
+ }
5256
+
5257
+ function requestEligibleForStatusLookup(entryRaw, routeKey, selfBotUsername, currentMessageID) {
5258
+ const entry = safeObject(entryRaw);
5259
+ const sourceMessageID = intFromRawAllowZero(entry.source_message_id, 0);
5260
+ const lastSourceMessageID = intFromRawAllowZero(entry.last_source_message_id, 0);
5261
+ if (
5262
+ (currentMessageID > 0 && sourceMessageID === currentMessageID)
5263
+ || (currentMessageID > 0 && lastSourceMessageID === currentMessageID)
5264
+ ) {
5265
+ return false;
5266
+ }
5267
+ if (String(entry.claimed_by_route || "").trim() === routeKey) {
5268
+ return true;
5269
+ }
5270
+ if (!selfBotUsername) {
5271
+ return true;
5272
+ }
5273
+ return ensureArray(entry.selected_bot_usernames)
5274
+ .map((value) => normalizeTelegramMentionUsername(value))
5275
+ .filter(Boolean)
5276
+ .includes(selfBotUsername);
5277
+ }
5278
+
5279
+ function pickPreferredStatusLookupRequest(entries = []) {
5280
+ const candidates = ensureArray(entries).map((entry) => safeObject(entry)).filter((entry) => Object.keys(entry).length > 0);
5281
+ if (!candidates.length) {
5282
+ return null;
5283
+ }
5284
+ const activeNonInformational = candidates.filter((entry) => (
5285
+ isActiveRunnerRequestStatus(entry.status)
5286
+ && !isInformationalRunnerRequestIntent(entry.normalized_intent)
5287
+ ));
5288
+ if (activeNonInformational.length) {
5289
+ return activeNonInformational[0];
5290
+ }
5291
+ const nonInformational = candidates.filter((entry) => !isInformationalRunnerRequestIntent(entry.normalized_intent));
5292
+ if (nonInformational.length) {
5293
+ return nonInformational[0];
5294
+ }
5295
+ const activeAny = candidates.filter((entry) => isActiveRunnerRequestStatus(entry.status));
5296
+ if (activeAny.length) {
5297
+ return activeAny[0];
5298
+ }
5299
+ return candidates[0];
5300
+ }
5301
+
5302
+ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5303
+ const parsed = safeObject(selectedRecord?.parsedArchive);
5304
+ const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5305
+ const currentChatID = String(parsed.chatID || parsed.chatId || "").trim();
5306
+ const routeKey = runnerRouteKey(route);
5307
+ const selfBotUsername = normalizeTelegramMentionUsername(firstNonEmptyString([
5308
+ route?.server_bot_name,
5309
+ route?.serverBotName,
5310
+ route?.botName,
5311
+ route?.name,
5312
+ ]));
5313
+ const activeExecution = resolveRunnerActiveExecutionState(routeState);
5314
+ const activeSourceMessageID = intFromRawAllowZero(activeExecution.sourceMessageID, 0);
5315
+ const selfBusyFiltered = Boolean(
5316
+ activeExecution.active
5317
+ && currentMessageID > 0
5318
+ && activeSourceMessageID > 0
5319
+ && currentMessageID === activeSourceMessageID
5320
+ );
5321
+ let runnerState = { requests: {} };
5322
+ try {
5323
+ runnerState = loadBotRunnerState();
5324
+ } catch {}
5325
+ const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
5326
+ const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
5327
+ const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
5328
+ entry,
5329
+ routeKey,
5330
+ selfBotUsername,
5331
+ currentMessageID,
5332
+ );
5333
+ let relatedActiveRequest = null;
5334
+ let relatedRequest = null;
5335
+ const selectors = currentConversationID
5336
+ ? { conversationID: currentConversationID, chatID: currentChatID }
5337
+ : { chatID: currentChatID };
5338
+ let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
5339
+ if (!scopedRequests.length && currentConversationID) {
5340
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
5341
+ }
5342
+ const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
5343
+ relatedActiveRequest = eligibleScopedRequests
5344
+ .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
5345
+ relatedRequest = pickPreferredStatusLookupRequest(
5346
+ eligibleScopedRequests.length
5347
+ ? eligibleScopedRequests
5348
+ : (replyChainContext.referencedRequest ? [replyChainContext.referencedRequest] : []).filter(requestMatchesCurrentRoute),
5349
+ );
5350
+ const lastAction = String(safeObject(routeState).last_action || "").trim();
5351
+ const lastReason = String(safeObject(routeState).last_reason || "").trim();
5352
+ const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
5353
+ const routeConversationID = String(safeObject(routeState).last_conversation_id || "").trim();
5354
+ const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
5355
+ const routeWorkItemTitles = ensureArray(safeObject(routeState).last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
5356
+ return {
5357
+ kind: "runner_status",
5358
+ status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
5359
+ ? "running"
5360
+ : String(safeObject(relatedRequest).status || "").trim() || "idle",
5361
+ resolved_conversation_id: currentConversationID,
5362
+ reply_chain_resolution: {
5363
+ reason: String(replyChainContext.reason || "").trim(),
5364
+ reply_to_message_id: intFromRawAllowZero(replyChainContext.replyToMessageID, 0) || undefined,
5365
+ anchor_message_id: intFromRawAllowZero(replyChainContext.anchorMessageID, 0) || undefined,
5366
+ },
5367
+ self_busy_filtered: selfBusyFiltered,
5368
+ active_execution: activeExecution.active && !selfBusyFiltered
5369
+ ? {
5370
+ started_at: String(activeExecution.startedAt || "").trim(),
5371
+ age_seconds: intFromRawAllowZero(activeExecution.ageSeconds, 0),
5372
+ stale: activeExecution.stale === true,
5373
+ stuck: activeExecution.stuck === true,
5374
+ warning: String(activeExecution.warning || "").trim(),
5375
+ }
5376
+ : null,
5377
+ related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
5378
+ related_request: relatedRequest ? summarizeRunnerRequestForStatusLookup(relatedRequest) : null,
5379
+ route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
5380
+ ? {
5381
+ ids: routeWorkItemIDs,
5382
+ titles: routeWorkItemTitles,
5383
+ }
5384
+ : null,
5385
+ last_route_result: {
5386
+ action: lastAction,
5387
+ reason: lastReason,
5388
+ intent_type: lastIntentType,
5389
+ },
5390
+ };
5391
+ }
5392
+
5110
5393
  async function resolveInformationalQueryReply({
5111
5394
  intentType,
5112
5395
  route,
@@ -5159,6 +5442,17 @@ async function resolveInformationalQueryReply({
5159
5442
  };
5160
5443
  }
5161
5444
  if (normalizedIntentType === "status_query") {
5445
+ return {
5446
+ handled: true,
5447
+ source: "runner.status",
5448
+ response_mode: "lookup_only",
5449
+ reply: "",
5450
+ lookup: buildRunnerStatusQueryLookup({
5451
+ route,
5452
+ routeState,
5453
+ selectedRecord,
5454
+ }),
5455
+ };
5162
5456
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
5163
5457
  if (activeExecution.active) {
5164
5458
  const startedAt = String(activeExecution.startedAt || "").trim();
@@ -5839,6 +6133,24 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
5839
6133
  skippedRecords.push(duplicateArchivedSkip);
5840
6134
  continue;
5841
6135
  }
6136
+ const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
6137
+ if (triggerDecision.shouldRespond !== true) {
6138
+ saveRunnerRouteState(
6139
+ routeKey,
6140
+ buildRunnerRouteStateFromComment(selectedRecord, {
6141
+ last_action: "trigger_skipped",
6142
+ last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
6143
+ last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
6144
+ }),
6145
+ );
6146
+ skippedRecords.push({
6147
+ id: selectedRecord.id,
6148
+ reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
6149
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
6150
+ suppressDiagnostic: true,
6151
+ });
6152
+ continue;
6153
+ }
5842
6154
  const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
5843
6155
  routeKey,
5844
6156
  normalizedRoute,
@@ -5947,6 +6259,8 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
5947
6259
  runtime,
5948
6260
  managedConversationBots,
5949
6261
  requestKey: String(requestClaim.requestKey || "").trim(),
6262
+ triggerDecision,
6263
+ responderAdjudication: adjudication,
5950
6264
  },
5951
6265
  };
5952
6266
  }
@@ -5992,6 +6306,24 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
5992
6306
  skippedRecords.push(duplicateArchivedSkip);
5993
6307
  continue;
5994
6308
  }
6309
+ const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
6310
+ if (triggerDecision.shouldRespond !== true) {
6311
+ saveRunnerRouteState(
6312
+ routeKey,
6313
+ buildRunnerRouteStateFromComment(selectedRecord, {
6314
+ last_action: "trigger_skipped",
6315
+ last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
6316
+ last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
6317
+ }),
6318
+ );
6319
+ skippedRecords.push({
6320
+ id: selectedRecord.id,
6321
+ reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
6322
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
6323
+ suppressDiagnostic: true,
6324
+ });
6325
+ continue;
6326
+ }
5995
6327
  const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
5996
6328
  routeKey,
5997
6329
  normalizedRoute,
@@ -6099,6 +6431,8 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6099
6431
  archiveThread,
6100
6432
  executionPlan,
6101
6433
  runtime,
6434
+ triggerDecision,
6435
+ responderAdjudication: inlineAdjudication,
6102
6436
  deps: {
6103
6437
  saveRunnerRouteState,
6104
6438
  startRunnerTypingHeartbeat,
@@ -6140,6 +6474,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6140
6474
  outcome: String(processed.result?.outcome || "replied").trim().toLowerCase(),
6141
6475
  conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
6142
6476
  allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
6477
+ executionContractType: String(processed.result?.execution_contract_type || "").trim(),
6478
+ executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
6479
+ nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
6480
+ currentBotSelector,
6481
+ conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
6143
6482
  normalizedIntent: String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim(),
6144
6483
  });
6145
6484
  await syncRunnerRequestLedgerForProjectToServer({
@@ -8403,6 +8742,8 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8403
8742
  archiveThread: deferredExecution.archiveThread,
8404
8743
  executionPlan: deferredExecution.executionPlan,
8405
8744
  runtime: deferredExecution.runtime,
8745
+ triggerDecision: deferredExecution.triggerDecision,
8746
+ responderAdjudication: deferredExecution.responderAdjudication,
8406
8747
  deps: {
8407
8748
  saveRunnerRouteState,
8408
8749
  startRunnerTypingHeartbeat,
@@ -8473,6 +8814,13 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8473
8814
  outcome: String(processed.result?.outcome || "replied").trim().toLowerCase(),
8474
8815
  conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
8475
8816
  allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
8817
+ executionContractType: String(processed.result?.execution_contract_type || "").trim(),
8818
+ executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
8819
+ nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
8820
+ currentBotSelector: normalizeTelegramMentionUsername(
8821
+ deferredExecution.bot?.username || deferredExecution.bot?.name,
8822
+ ),
8823
+ conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8476
8824
  normalizedIntent: String(safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "").trim(),
8477
8825
  });
8478
8826
  await syncRunnerRequestLedgerForProjectToServer({
@@ -12782,6 +13130,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
12782
13130
  runnerRouteLogicalSignature,
12783
13131
  loadBotRunnerState,
12784
13132
  saveBotRunnerState,
13133
+ buildRunnerStatusQueryLookup,
12785
13134
  tryJsonParse,
12786
13135
  safeObject,
12787
13136
  normalizeRunnerTriggerPolicy,
@@ -12791,6 +13140,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
12791
13140
  processRunnerSelectedRecord,
12792
13141
  resolveRunnerStartupLoopAdjudication,
12793
13142
  claimRunnerRequestForHumanComment,
13143
+ markRunnerRequestLifecycle,
12794
13144
  resolveRunnerContinuationRequestForBotReply,
12795
13145
  cleanupBotRunnerRequestState,
12796
13146
  runRunnerAIExecution,
@@ -1703,6 +1703,10 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1703
1703
  const queryLookup = safePayload.query_lookup && typeof safePayload.query_lookup === "object"
1704
1704
  ? safePayload.query_lookup
1705
1705
  : null;
1706
+ const queryLookupFacts = queryLookup && Object.prototype.hasOwnProperty.call(queryLookup, "facts")
1707
+ ? queryLookup.facts
1708
+ : null;
1709
+ const queryLookupResponseMode = String(queryLookup?.response_mode || "").trim().toLowerCase();
1706
1710
  const currentTurnPurpose = inferCurrentTurnPurpose({
1707
1711
  trigger,
1708
1712
  conversation,
@@ -1777,13 +1781,25 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1777
1781
  "",
1778
1782
  ];
1779
1783
  if (queryLookup?.handled === true) {
1780
- lines.push(
1781
- "Tool-assisted factual lookup for this turn:",
1782
- `- Source: ${String(queryLookup.source || "").trim() || "-"}`,
1783
- `- Proposed factual reply: ${String(queryLookup.proposed_reply || "").trim() || "-"}`,
1784
- "If you decide this bot should answer, prefer this factual reply and refine it only as needed.",
1785
- "",
1786
- );
1784
+ lines.push("Tool-assisted factual lookup for this turn:");
1785
+ lines.push(`- Source: ${String(queryLookup.source || "").trim() || "-"}`);
1786
+ if (queryLookupResponseMode) {
1787
+ lines.push(`- Response mode: ${queryLookupResponseMode}`);
1788
+ }
1789
+ if (String(queryLookup.proposed_reply || "").trim()) {
1790
+ lines.push(`- Proposed factual reply: ${String(queryLookup.proposed_reply || "").trim()}`);
1791
+ }
1792
+ if (queryLookupFacts != null) {
1793
+ lines.push("Structured lookup facts:");
1794
+ lines.push(JSON.stringify(queryLookupFacts, null, 2));
1795
+ }
1796
+ if (queryLookupResponseMode === "lookup_only") {
1797
+ lines.push("Use these facts to answer naturally in the user's language.");
1798
+ lines.push("Do not expose raw internal ids, request keys, or exact ISO timestamps unless the user explicitly asks for debug details.");
1799
+ } else {
1800
+ lines.push("If you decide this bot should answer, prefer this factual reply and refine it only as needed.");
1801
+ }
1802
+ lines.push("");
1787
1803
  }
1788
1804
  if (isInternalExecutionStep) {
1789
1805
  const stepIndex = intFromRawAllowZero(executionStep.index, 0);