metheus-governance-mcp-cli 0.2.200 → 0.2.201

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
@@ -2553,6 +2553,76 @@ async function findServerRunnerRequestForMessageID({
2553
2553
  }
2554
2554
  }
2555
2555
 
2556
+ async function loadRunnerArchiveThreadMessageIndex({
2557
+ runtime,
2558
+ threadID,
2559
+ cache = null,
2560
+ }) {
2561
+ const normalizedThreadID = String(threadID || "").trim();
2562
+ if (
2563
+ cache
2564
+ && String(cache.threadID || "").trim() === normalizedThreadID
2565
+ && cache.messageIndex instanceof Map
2566
+ ) {
2567
+ return cache;
2568
+ }
2569
+ if (!normalizedThreadID || !runtime?.baseURL || !runtime?.token) {
2570
+ return {
2571
+ threadID: normalizedThreadID,
2572
+ messageIndex: new Map(),
2573
+ };
2574
+ }
2575
+ try {
2576
+ const comments = await listThreadCommentsTail({
2577
+ siteBaseURL: runtime.baseURL,
2578
+ threadID: normalizedThreadID,
2579
+ token: runtime.token,
2580
+ timeoutSeconds: runtime.timeoutSeconds,
2581
+ actorUserID: runtime.actor?.user_id,
2582
+ tailLimit: 300,
2583
+ scanLimit: 1200,
2584
+ });
2585
+ const messageIndex = new Map();
2586
+ for (const commentRaw of ensureArray(comments)) {
2587
+ const comment = safeObject(commentRaw);
2588
+ const parsedArchive = safeObject(comment.parsedArchive || parseArchivedChatComment(comment.body));
2589
+ const messageID = intFromRawAllowZero(parsedArchive.messageID, 0);
2590
+ if (messageID > 0 && !messageIndex.has(messageID)) {
2591
+ messageIndex.set(messageID, {
2592
+ ...comment,
2593
+ parsedArchive,
2594
+ });
2595
+ }
2596
+ }
2597
+ return {
2598
+ threadID: normalizedThreadID,
2599
+ messageIndex,
2600
+ };
2601
+ } catch {
2602
+ return {
2603
+ threadID: normalizedThreadID,
2604
+ messageIndex: new Map(),
2605
+ };
2606
+ }
2607
+ }
2608
+
2609
+ async function findRunnerArchiveThreadMessageByID({
2610
+ runtime,
2611
+ threadID,
2612
+ messageID,
2613
+ cache = null,
2614
+ }) {
2615
+ const nextCache = await loadRunnerArchiveThreadMessageIndex({
2616
+ runtime,
2617
+ threadID,
2618
+ cache,
2619
+ });
2620
+ return {
2621
+ cache: nextCache,
2622
+ record: safeObject(nextCache.messageIndex.get(intFromRawAllowZero(messageID, 0))),
2623
+ };
2624
+ }
2625
+
2556
2626
  function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
2557
2627
  const parsed = safeObject(selectedRecord?.parsedArchive);
2558
2628
  const explicitConversationID = String(parsed.conversationID || "").trim();
@@ -2600,6 +2670,8 @@ async function resolveRunnerReplyChainConversationContextWithServerFallback({
2600
2670
  normalizedRoute,
2601
2671
  selectedRecord,
2602
2672
  runtime,
2673
+ archiveThreadID = "",
2674
+ hydrationAttempted = false,
2603
2675
  }) {
2604
2676
  const initialState = safeObject(state);
2605
2677
  const initialContext = resolveRunnerReplyChainConversationContext(initialState, normalizedRoute, selectedRecord);
@@ -2611,11 +2683,11 @@ async function resolveRunnerReplyChainConversationContextWithServerFallback({
2611
2683
  };
2612
2684
  }
2613
2685
  const parsed = safeObject(selectedRecord?.parsedArchive);
2614
- const replyToMessageID = intFromRawAllowZero(
2686
+ const initialReplyToMessageID = intFromRawAllowZero(
2615
2687
  parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
2616
2688
  0,
2617
2689
  );
2618
- if (replyToMessageID <= 0 || !runtime?.baseURL || !runtime?.token) {
2690
+ if (initialReplyToMessageID <= 0) {
2619
2691
  return {
2620
2692
  state: initialState,
2621
2693
  replyChainContext: initialContext,
@@ -2623,43 +2695,142 @@ async function resolveRunnerReplyChainConversationContextWithServerFallback({
2623
2695
  };
2624
2696
  }
2625
2697
  const chatID = String(parsed.chatID || parsed.chatId || "").trim();
2626
- const serverReferencedRequest = await findServerRunnerRequestForMessageID({
2627
- normalizedRoute,
2628
- runtime,
2629
- chatID,
2630
- messageID: replyToMessageID,
2631
- });
2632
- if (serverReferencedRequest.request_key) {
2633
- const requestIndex = normalizeBotRunnerRequests(initialState.requests);
2634
- requestIndex[String(serverReferencedRequest.request_key || "").trim()] = serverReferencedRequest;
2635
- const anchorMessageID = intFromRawAllowZero(serverReferencedRequest.source_message_id, 0) || replyToMessageID;
2698
+ const normalizedArchiveThreadID = firstNonEmptyString([
2699
+ archiveThreadID,
2700
+ selectedRecord?.thread_id,
2701
+ selectedRecord?.threadID,
2702
+ selectedRecord?.threadId,
2703
+ ]);
2704
+ let stateForLookup = initialState;
2705
+ let replyToMessageID = initialReplyToMessageID;
2706
+ let archiveThreadCache = null;
2707
+ let lastAnchorMessageID = intFromRawAllowZero(safeObject(initialContext).anchorMessageID, 0) || initialReplyToMessageID;
2708
+ const visitedMessageIDs = new Set();
2709
+
2710
+ const buildContextFromRequest = (referencedRequestRaw, reasonSuffix = "") => {
2711
+ const referencedRequest = safeObject(referencedRequestRaw);
2712
+ const anchorMessageID = intFromRawAllowZero(referencedRequest.source_message_id, 0) || replyToMessageID;
2636
2713
  return {
2637
- state: {
2638
- ...initialState,
2639
- requests: requestIndex,
2640
- },
2714
+ state: stateForLookup,
2641
2715
  replyChainContext: {
2642
- conversationID: String(serverReferencedRequest.conversation_id || "").trim()
2716
+ conversationID: String(referencedRequest.conversation_id || "").trim()
2643
2717
  || buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID),
2644
- replyToMessageID,
2718
+ replyToMessageID: initialReplyToMessageID,
2645
2719
  anchorMessageID,
2646
- reason: String(serverReferencedRequest.conversation_id || "").trim()
2647
- ? "reply_request_conversation_server"
2648
- : "reply_request_synthetic_server",
2649
- referencedRequest: serverReferencedRequest,
2720
+ reason: String(referencedRequest.conversation_id || "").trim()
2721
+ ? `reply_request_conversation${reasonSuffix}`
2722
+ : `reply_request_synthetic${reasonSuffix}`,
2723
+ referencedRequest,
2650
2724
  },
2651
- hydrated: false,
2725
+ hydrated: hydrationAttempted,
2652
2726
  };
2727
+ };
2728
+
2729
+ for (let hop = 0; hop < 8 && replyToMessageID > 0; hop += 1) {
2730
+ if (visitedMessageIDs.has(replyToMessageID)) {
2731
+ break;
2732
+ }
2733
+ visitedMessageIDs.add(replyToMessageID);
2734
+
2735
+ const localReferencedRequest = safeObject(findRunnerRequestsForMessageID(stateForLookup, normalizedRoute, {
2736
+ chatID,
2737
+ messageID: replyToMessageID,
2738
+ })[0]);
2739
+ if (localReferencedRequest.request_key) {
2740
+ return buildContextFromRequest(localReferencedRequest, hop > 0 ? "_chain" : "");
2741
+ }
2742
+
2743
+ if (runtime?.baseURL && runtime?.token) {
2744
+ const serverReferencedRequest = await findServerRunnerRequestForMessageID({
2745
+ normalizedRoute,
2746
+ runtime,
2747
+ chatID,
2748
+ messageID: replyToMessageID,
2749
+ });
2750
+ if (serverReferencedRequest.request_key) {
2751
+ const requestIndex = normalizeBotRunnerRequests(stateForLookup.requests);
2752
+ requestIndex[String(serverReferencedRequest.request_key || "").trim()] = serverReferencedRequest;
2753
+ stateForLookup = {
2754
+ ...stateForLookup,
2755
+ requests: requestIndex,
2756
+ };
2757
+ return buildContextFromRequest(serverReferencedRequest, hop > 0 ? "_chain_server" : "_server");
2758
+ }
2759
+ }
2760
+
2761
+ if (!normalizedArchiveThreadID || !runtime?.baseURL || !runtime?.token) {
2762
+ break;
2763
+ }
2764
+ const archivedMessageLookup = await findRunnerArchiveThreadMessageByID({
2765
+ runtime,
2766
+ threadID: normalizedArchiveThreadID,
2767
+ messageID: replyToMessageID,
2768
+ cache: archiveThreadCache,
2769
+ });
2770
+ archiveThreadCache = archivedMessageLookup.cache;
2771
+ const archivedRecord = safeObject(archivedMessageLookup.record);
2772
+ const archivedParsed = safeObject(archivedRecord.parsedArchive || parseArchivedChatComment(archivedRecord.body));
2773
+ const archivedMessageID = intFromRawAllowZero(archivedParsed.messageID, 0) || replyToMessageID;
2774
+ if (archivedMessageID > 0) {
2775
+ lastAnchorMessageID = archivedMessageID;
2776
+ }
2777
+ const archivedConversationID = String(archivedParsed.conversationID || "").trim();
2778
+ if (archivedConversationID) {
2779
+ return {
2780
+ state: stateForLookup,
2781
+ replyChainContext: {
2782
+ conversationID: archivedConversationID,
2783
+ replyToMessageID: initialReplyToMessageID,
2784
+ anchorMessageID: archivedMessageID,
2785
+ reason: "reply_chain_archive_conversation",
2786
+ referencedRequest: null,
2787
+ },
2788
+ hydrated: hydrationAttempted,
2789
+ };
2790
+ }
2791
+ const nextReplyToMessageID = intFromRawAllowZero(archivedParsed.replyToMessageID, 0);
2792
+ if (nextReplyToMessageID <= 0) {
2793
+ return {
2794
+ state: stateForLookup,
2795
+ replyChainContext: {
2796
+ conversationID: buildSyntheticReplyChainConversationID(normalizedRoute, chatID, lastAnchorMessageID || archivedMessageID),
2797
+ replyToMessageID: initialReplyToMessageID,
2798
+ anchorMessageID: lastAnchorMessageID || archivedMessageID,
2799
+ reason: "reply_chain_archive_synthetic",
2800
+ referencedRequest: null,
2801
+ },
2802
+ hydrated: hydrationAttempted,
2803
+ };
2804
+ }
2805
+ replyToMessageID = nextReplyToMessageID;
2653
2806
  }
2654
- const hydratedState = await hydrateRunnerRequestLedgerFromServer({
2655
- normalizedRoute,
2656
- runtime,
2657
- });
2658
- const hydratedContext = resolveRunnerReplyChainConversationContext(hydratedState, normalizedRoute, selectedRecord);
2807
+
2808
+ if (!hydrationAttempted && runtime?.baseURL && runtime?.token) {
2809
+ const hydratedState = await hydrateRunnerRequestLedgerFromServer({
2810
+ normalizedRoute,
2811
+ runtime,
2812
+ });
2813
+ const hydratedResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
2814
+ state: hydratedState,
2815
+ normalizedRoute,
2816
+ selectedRecord,
2817
+ runtime,
2818
+ archiveThreadID: normalizedArchiveThreadID,
2819
+ hydrationAttempted: true,
2820
+ });
2821
+ return {
2822
+ state: hydratedResolution.state,
2823
+ replyChainContext: hydratedResolution.replyChainContext,
2824
+ hydrated: true,
2825
+ };
2826
+ }
2827
+
2659
2828
  return {
2660
- state: hydratedState,
2661
- replyChainContext: hydratedContext,
2662
- hydrated: true,
2829
+ state: stateForLookup,
2830
+ replyChainContext: hydrationAttempted
2831
+ ? resolveRunnerReplyChainConversationContext(stateForLookup, normalizedRoute, selectedRecord)
2832
+ : initialContext,
2833
+ hydrated: hydrationAttempted,
2663
2834
  };
2664
2835
  }
2665
2836
 
@@ -2709,6 +2880,7 @@ async function claimRunnerRequestForHumanComment({
2709
2880
  selectedBotUsernames = [],
2710
2881
  normalizedIntent = "",
2711
2882
  runtime = null,
2883
+ archiveThreadID = "",
2712
2884
  }) {
2713
2885
  const parsed = safeObject(selectedRecord?.parsedArchive);
2714
2886
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
@@ -2724,6 +2896,7 @@ async function claimRunnerRequestForHumanComment({
2724
2896
  normalizedRoute,
2725
2897
  selectedRecord,
2726
2898
  runtime,
2899
+ archiveThreadID,
2727
2900
  });
2728
2901
  const replyChainContext = safeObject(replyChainResolution.replyChainContext);
2729
2902
  const referencedRequest = safeObject(replyChainContext.referencedRequest);
@@ -5431,9 +5604,6 @@ function formatTelegramInboundArchiveComment(normalized) {
5431
5604
  if (normalized.mentionUsernames.length > 0) {
5432
5605
  headerLines.push(`mention_usernames: ${normalized.mentionUsernames.map((item) => `@${item}`).join(", ")}`);
5433
5606
  }
5434
- if (normalized.messageThreadID) {
5435
- headerLines.push(`telegram_topic_id: ${normalized.messageThreadID}`);
5436
- }
5437
5607
  if (normalized.replyToMessageID > 0) {
5438
5608
  headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
5439
5609
  headerLines.push(`reply_to_sender_is_bot: ${normalized.replyToFromIsBot ? "true" : "false"}`);
@@ -6186,6 +6356,29 @@ function buildRunnerSmallTalkReply({ route, executionPlan } = {}) {
6186
6356
  return templatePool[stableTextModulo(`${displayName}:${roleProfileName}`, templatePool.length)];
6187
6357
  }
6188
6358
 
6359
+ function buildInformationalMiniExecutionOverride({ route, executionPlan } = {}) {
6360
+ const safeRoute = safeObject(route);
6361
+ const safeExecutionPlan = safeObject(executionPlan);
6362
+ const roleProfileName = normalizeRunnerRoleProfileName(
6363
+ safeExecutionPlan.roleProfileName || safeRoute.roleProfile || safeRoute.role || "monitor",
6364
+ ) || "monitor";
6365
+ const lightweightModel = String(
6366
+ resolveResponderAdjudicatorModelDisplayName({ client: "gpt", env: process.env }) || "gpt-5.3-codex-spark",
6367
+ ).trim() || "gpt-5.3-codex-spark";
6368
+ return {
6369
+ mode: "role_profile",
6370
+ role_profile_name: roleProfileName,
6371
+ role_profile: normalizeRunnerRoleProfile(roleProfileName, {
6372
+ client: "gpt",
6373
+ model: lightweightModel,
6374
+ permission_mode: "read_only",
6375
+ reasoning_effort: "low",
6376
+ }),
6377
+ workspace_dir: String(safeExecutionPlan.workspaceDir || safeRoute.workspaceDir || "").trim(),
6378
+ workspace_source: String(safeExecutionPlan.workspaceSource || "").trim(),
6379
+ };
6380
+ }
6381
+
6189
6382
  function summarizeRunnerRequestForStatusLookup(entryRaw) {
6190
6383
  const entry = safeObject(entryRaw);
6191
6384
  return {
@@ -6411,11 +6604,21 @@ async function resolveInformationalQueryReply({
6411
6604
  }) {
6412
6605
  const normalizedIntentType = String(intentType || "").trim();
6413
6606
  const messageText = String(safeObject(selectedRecord?.parsedArchive).body || "").trim();
6607
+ const executionOverride = buildInformationalMiniExecutionOverride({ route, executionPlan });
6414
6608
  if (normalizedIntentType === "small_talk") {
6415
6609
  return {
6416
6610
  handled: true,
6417
- reply: buildRunnerSmallTalkReply({ route, executionPlan }),
6611
+ response_mode: "lookup_only",
6612
+ reply: "",
6418
6613
  source: "small_talk",
6614
+ lookup: {
6615
+ intent_type: "small_talk",
6616
+ user_message: messageText,
6617
+ bot_name: firstNonEmptyString([route?.botName, route?.serverBotName, route?.server_bot_name, route?.name]),
6618
+ bot_role: String(route?.role || route?.roleProfile || "").trim(),
6619
+ proposed_summary: buildRunnerSmallTalkReply({ route, executionPlan }),
6620
+ },
6621
+ execution_override: executionOverride,
6419
6622
  };
6420
6623
  }
6421
6624
  if (false && normalizedIntentType === "small_talk") {
@@ -6433,9 +6636,14 @@ async function resolveInformationalQueryReply({
6433
6636
  : "현재 이 프로젝트는 project-workspaces.json에 로컬 작업 폴더가 바인딩되어 있지 않습니다.";
6434
6637
  return {
6435
6638
  handled: true,
6436
- reply,
6639
+ response_mode: "lookup_only",
6640
+ reply: "",
6437
6641
  source: "project.workspace",
6438
- lookup: workspace,
6642
+ lookup: {
6643
+ ...workspace,
6644
+ proposed_summary: reply,
6645
+ },
6646
+ execution_override: executionOverride,
6439
6647
  };
6440
6648
  }
6441
6649
  if (normalizedIntentType === "bot_role_query") {
@@ -6447,9 +6655,14 @@ async function resolveInformationalQueryReply({
6447
6655
  });
6448
6656
  return {
6449
6657
  handled: true,
6450
- reply: buildProjectBotRolesText(payload),
6658
+ response_mode: "lookup_only",
6659
+ reply: "",
6451
6660
  source: "project.bot_roles",
6452
- lookup: payload,
6661
+ lookup: {
6662
+ ...payload,
6663
+ proposed_summary: buildProjectBotRolesText(payload),
6664
+ },
6665
+ execution_override: executionOverride,
6453
6666
  };
6454
6667
  }
6455
6668
  if (normalizedIntentType === "status_query") {
@@ -6512,9 +6725,14 @@ async function resolveInformationalQueryReply({
6512
6725
  });
6513
6726
  return {
6514
6727
  handled: true,
6515
- reply: buildProjectFileLocateText(payload),
6728
+ response_mode: "lookup_only",
6729
+ reply: "",
6516
6730
  source: "project.file.locate",
6517
- lookup: payload,
6731
+ lookup: {
6732
+ ...payload,
6733
+ proposed_summary: buildProjectFileLocateText(payload),
6734
+ },
6735
+ execution_override: executionOverride,
6518
6736
  };
6519
6737
  }
6520
6738
  return null;
@@ -7142,6 +7360,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
7142
7360
  selectedRecord,
7143
7361
  selectedBotUsernames: selectedResponderSelectors,
7144
7362
  runtime,
7363
+ archiveThreadID: archiveThread.threadID,
7145
7364
  });
7146
7365
  };
7147
7366
  if (deferExecution) {
@@ -14190,6 +14409,44 @@ async function runSelftest(flags = {}) {
14190
14409
  && monitorGreeting !== reviewGreeting,
14191
14410
  `monitor=${monitorGreeting} review=${reviewGreeting}`,
14192
14411
  );
14412
+ const informationalMiniReply = await resolveInformationalQueryReply({
14413
+ intentType: "small_talk",
14414
+ route: { botName: "RyoAI_bot", role: "monitor", roleProfile: "monitor" },
14415
+ routeState: {},
14416
+ selectedRecord: {
14417
+ parsedArchive: {
14418
+ body: "@RyoAI_bot 하이",
14419
+ },
14420
+ },
14421
+ runtime: {},
14422
+ executionPlan: {
14423
+ mode: "role_profile",
14424
+ roleProfileName: "monitor",
14425
+ roleProfile: {
14426
+ client: "gpt",
14427
+ model: "gpt-5.4",
14428
+ permissionMode: "workspace_write",
14429
+ reasoningEffort: "medium",
14430
+ },
14431
+ workspaceDir: "C:\\selftest-workspace",
14432
+ workspaceSource: "selftest",
14433
+ },
14434
+ });
14435
+ const expectedInformationalModel = resolveResponderAdjudicatorModelDisplayName({
14436
+ client: "gpt",
14437
+ env: process.env,
14438
+ });
14439
+ push(
14440
+ "small_talk_reply_uses_lookup_only_mini_override",
14441
+ safeObject(informationalMiniReply).handled === true
14442
+ && String(safeObject(informationalMiniReply).response_mode || "") === "lookup_only"
14443
+ && String(safeObject(informationalMiniReply).reply || "") === ""
14444
+ && String(safeObject(safeObject(informationalMiniReply).execution_override).role_profile?.client || "") === "gpt"
14445
+ && String(safeObject(safeObject(informationalMiniReply).execution_override).role_profile?.model || "") === String(expectedInformationalModel || "")
14446
+ && String(safeObject(safeObject(informationalMiniReply).execution_override).role_profile?.permissionMode || "") === "read_only"
14447
+ && String(safeObject(safeObject(informationalMiniReply).execution_override).role_profile?.reasoningEffort || "") === "low",
14448
+ JSON.stringify(safeObject(informationalMiniReply).execution_override || {}),
14449
+ );
14193
14450
  const telegramEnvV2Parsed = parseSimpleEnvText(`
14194
14451
  TELEGRAM_API_BASE_URL=http://127.0.0.1:8999/api
14195
14452
  TELEGRAM_AUTO_CLEAR_WEBHOOK=false
@@ -827,6 +827,7 @@ export async function discoverArchiveThreadForDestination(
827
827
  const providerEnvConfig = requireDependency(deps, "providerEnvConfig");
828
828
  const parseArchivedChatComment = requireDependency(deps, "parseArchivedChatComment");
829
829
  const providerLabel = providerEnvConfig(provider).label;
830
+ const hasConfiguredArchiveWorkItem = archiveWorkItemID && isUUID(archiveWorkItemID);
830
831
 
831
832
  if (archiveThreadID && isUUID(archiveThreadID)) {
832
833
  return {
@@ -837,8 +838,7 @@ export async function discoverArchiveThreadForDestination(
837
838
  }
838
839
 
839
840
  const candidateWorkItemIDs = [];
840
- let reusableWorkItemID = "";
841
- if (archiveWorkItemID && isUUID(archiveWorkItemID)) {
841
+ if (hasConfiguredArchiveWorkItem) {
842
842
  candidateWorkItemIDs.push(archiveWorkItemID);
843
843
  } else {
844
844
  const workItems = await listProjectWorkItems({
@@ -864,11 +864,9 @@ export async function discoverArchiveThreadForDestination(
864
864
  token,
865
865
  timeoutSeconds,
866
866
  }, deps);
867
- let fallbackThreadID = "";
868
867
  for (const thread of threads) {
869
868
  const threadID = String(thread.id || "").trim();
870
869
  if (!threadID) continue;
871
- if (!fallbackThreadID) fallbackThreadID = threadID;
872
870
  const comments = await listThreadComments({
873
871
  siteBaseURL,
874
872
  threadID,
@@ -889,15 +887,8 @@ export async function discoverArchiveThreadForDestination(
889
887
  };
890
888
  }
891
889
  }
892
- if (fallbackThreadID) {
893
- return {
894
- threadID: fallbackThreadID,
895
- workItemID,
896
- source: "fallback-thread",
897
- };
898
- }
899
- if (!reusableWorkItemID) {
900
- reusableWorkItemID = workItemID;
890
+ if (hasConfiguredArchiveWorkItem) {
891
+ break;
901
892
  }
902
893
  }
903
894
 
@@ -907,7 +898,9 @@ export async function discoverArchiveThreadForDestination(
907
898
  const archiveDescription = `Auto-created archive work item for ${providerLabel} destination ${destinationLabel}.`;
908
899
  try {
909
900
  const threadBody = `Auto-created archive thread for ${providerLabel} destination ${destinationLabel} (chat_id=${destination.chatID}).`;
910
- let resolvedWorkItemID = String(reusableWorkItemID || "").trim();
901
+ let resolvedWorkItemID = hasConfiguredArchiveWorkItem
902
+ ? String(archiveWorkItemID || "").trim()
903
+ : "";
911
904
  if (!resolvedWorkItemID) {
912
905
  const createdWorkItem = await createProjectWorkItem(
913
906
  {
@@ -941,7 +934,9 @@ export async function discoverArchiveThreadForDestination(
941
934
  return {
942
935
  threadID: createdThreadID,
943
936
  workItemID: resolvedWorkItemID,
944
- source: reusableWorkItemID ? "auto-created-thread-on-existing-work-item" : "auto-created-thread",
937
+ source: hasConfiguredArchiveWorkItem
938
+ ? "auto-created-thread-on-configured-work-item"
939
+ : "auto-created-thread",
945
940
  };
946
941
  }
947
942
  }
@@ -653,6 +653,62 @@ function isInformationalHumanIntentType(intentType) {
653
653
  ].includes(normalizeHumanIntentType(intentType));
654
654
  }
655
655
 
656
+ function resolveInformationalQueryExecutionPlan(executionPlan, directInformationalReply, route) {
657
+ const basePlan = safeObject(executionPlan);
658
+ const override = safeObject(safeObject(directInformationalReply).execution_override);
659
+ const overrideRoleProfile = safeObject(override.role_profile || override.roleProfile);
660
+ if (String(override.mode || "").trim().toLowerCase() !== "role_profile") {
661
+ return basePlan;
662
+ }
663
+ if (!String(overrideRoleProfile.client || "").trim()) {
664
+ return basePlan;
665
+ }
666
+ return {
667
+ ...basePlan,
668
+ mode: "role_profile",
669
+ roleProfileName: String(
670
+ override.role_profile_name
671
+ || override.roleProfileName
672
+ || basePlan.roleProfileName
673
+ || route?.roleProfile
674
+ || route?.role
675
+ || "",
676
+ ).trim(),
677
+ roleProfile: {
678
+ ...safeObject(basePlan.roleProfile),
679
+ client: String(overrideRoleProfile.client || safeObject(basePlan.roleProfile).client || "").trim(),
680
+ model: String(overrideRoleProfile.model || safeObject(basePlan.roleProfile).model || "").trim(),
681
+ permissionMode: String(
682
+ overrideRoleProfile.permission_mode
683
+ || overrideRoleProfile.permissionMode
684
+ || safeObject(basePlan.roleProfile).permissionMode
685
+ || "",
686
+ ).trim(),
687
+ reasoningEffort: String(
688
+ overrideRoleProfile.reasoning_effort
689
+ || overrideRoleProfile.reasoningEffort
690
+ || safeObject(basePlan.roleProfile).reasoningEffort
691
+ || "",
692
+ ).trim(),
693
+ },
694
+ workspaceDir: String(
695
+ override.workspace_dir
696
+ || override.workspaceDir
697
+ || basePlan.workspaceDir
698
+ || route?.workspaceDir
699
+ || "",
700
+ ).trim(),
701
+ workspaceSource: String(
702
+ override.workspace_source
703
+ || override.workspaceSource
704
+ || basePlan.workspaceSource
705
+ || "",
706
+ ).trim(),
707
+ usedCommandFallback: false,
708
+ fallbackReason: "",
709
+ };
710
+ }
711
+
656
712
  function buildHumanIntentContextWindowOptions(intentType) {
657
713
  const normalizedIntentType = normalizeHumanIntentType(intentType);
658
714
  if (normalizedIntentType === "small_talk") {
@@ -3168,7 +3224,7 @@ async function resolvePublicConversationContext({
3168
3224
  managedMentions,
3169
3225
  peerMap,
3170
3226
  deps,
3171
- executionPlan,
3227
+ executionPlan: effectiveExecutionPlan,
3172
3228
  });
3173
3229
  const participantSelectors = uniqueOrdered(ensureArray(humanIntent.participantSelectors));
3174
3230
  const initialResponderSelectors = uniqueOrdered(
@@ -4263,6 +4319,11 @@ export async function processRunnerSelectedRecord({
4263
4319
  };
4264
4320
  }
4265
4321
  }
4322
+ const effectiveExecutionPlan = resolveInformationalQueryExecutionPlan(
4323
+ executionPlan,
4324
+ directInformationalReply,
4325
+ normalizedRoute,
4326
+ );
4266
4327
  const aiPayload = buildRunnerInputPayload({
4267
4328
  route: normalizedRoute,
4268
4329
  bot: {
@@ -4277,7 +4338,7 @@ export async function processRunnerSelectedRecord({
4277
4338
  selectedRecord,
4278
4339
  contextWindow,
4279
4340
  projectContextItems,
4280
- executionPlan,
4341
+ executionPlan: effectiveExecutionPlan,
4281
4342
  serializedTriggerPolicy: serializeRunnerTriggerPolicy(normalizedRoute.triggerPolicy),
4282
4343
  serializedArchivePolicy: serializeRunnerArchivePolicy(normalizedRoute.archivePolicy),
4283
4344
  triggerDecision: effectiveTriggerDecision,
@@ -4402,7 +4463,7 @@ export async function processRunnerSelectedRecord({
4402
4463
  bot,
4403
4464
  destination,
4404
4465
  archiveThread,
4405
- executionPlan,
4466
+ executionPlan: effectiveExecutionPlan,
4406
4467
  runtime,
4407
4468
  executionDeps,
4408
4469
  triggerDecision: effectiveTriggerDecision,
@@ -4426,7 +4487,7 @@ export async function processRunnerSelectedRecord({
4426
4487
  inputPayload: aiPayload,
4427
4488
  route: normalizedRoute,
4428
4489
  destination,
4429
- executionPlan,
4490
+ executionPlan: effectiveExecutionPlan,
4430
4491
  deps: executionDeps,
4431
4492
  });
4432
4493
  }
@@ -4482,12 +4543,12 @@ export async function processRunnerSelectedRecord({
4482
4543
 
4483
4544
  const artifactValidation = artifactValidationOverride && Object.keys(artifactValidationOverride).length > 0
4484
4545
  ? artifactValidationOverride
4485
- : validateWorkspaceArtifacts && String(executionPlan.workspaceDir || "").trim()
4546
+ : validateWorkspaceArtifacts && String(effectiveExecutionPlan.workspaceDir || "").trim()
4486
4547
  ? validateWorkspaceArtifacts(
4487
4548
  ensureArray(aiResult?.artifacts),
4488
- executionPlan.workspaceDir,
4549
+ effectiveExecutionPlan.workspaceDir,
4489
4550
  {
4490
- permissionMode: String(executionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
4551
+ permissionMode: String(effectiveExecutionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
4491
4552
  },
4492
4553
  )
4493
4554
  : {
@@ -4520,7 +4581,7 @@ export async function processRunnerSelectedRecord({
4520
4581
  last_artifact_paths: summarizeValidatedArtifactPaths(artifactValidation),
4521
4582
  last_artifact_errors: artifactErrors,
4522
4583
  last_boundary_violations: boundaryViolations,
4523
- last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
4584
+ last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
4524
4585
  ...intentStatePatch,
4525
4586
  }),
4526
4587
  );
@@ -4541,8 +4602,8 @@ export async function processRunnerSelectedRecord({
4541
4602
  conversation_lead_bot: String(conversationContext?.leadBotUsername || "").trim(),
4542
4603
  conversation_summary_bot: String(conversationContext?.summaryBotUsername || "").trim(),
4543
4604
  conversation_allowed_responders: ensureArray(conversationContext?.allowedResponderSelectors),
4544
- execution_mode: executionPlan.mode,
4545
- role_profile: executionPlan.roleProfileName,
4605
+ execution_mode: effectiveExecutionPlan.mode,
4606
+ role_profile: effectiveExecutionPlan.roleProfileName,
4546
4607
  artifact_validation: String(artifactValidation.status || "").trim() || "policy_violation",
4547
4608
  artifact_paths: summarizeValidatedArtifactPaths(artifactValidation),
4548
4609
  artifact_errors: artifactErrors,
@@ -4615,7 +4676,7 @@ export async function processRunnerSelectedRecord({
4615
4676
  botReplyText: String(aiResult.reply || "").trim(),
4616
4677
  currentBotUsername: String(bot?.username || bot?.name || "").trim(),
4617
4678
  managedBots: buildConversationParticipantViews(Array.from(auditPeerMap.keys()), auditPeerMap),
4618
- workspaceDir: String(executionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
4679
+ workspaceDir: String(effectiveExecutionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
4619
4680
  });
4620
4681
  if (audit?.requires_actionable_contract === true && audit?.reply_satisfies_request !== true) {
4621
4682
  requiresActionableContract = true;
@@ -4661,7 +4722,7 @@ export async function processRunnerSelectedRecord({
4661
4722
  inputPayload: forcedContractPayload,
4662
4723
  route: normalizedRoute,
4663
4724
  destination,
4664
- executionPlan,
4725
+ executionPlan: effectiveExecutionPlan,
4665
4726
  deps: executionDeps,
4666
4727
  });
4667
4728
  executionContract = conversationContext?.mode === "public_multi_bot"
@@ -4727,7 +4788,7 @@ export async function processRunnerSelectedRecord({
4727
4788
  last_reason: reason,
4728
4789
  last_conversation_id: String(conversationContext?.id || "").trim(),
4729
4790
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
4730
- last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
4791
+ last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
4731
4792
  ...intentStatePatch,
4732
4793
  }),
4733
4794
  );
@@ -5022,7 +5083,7 @@ export async function processRunnerSelectedRecord({
5022
5083
  ? "error"
5023
5084
  : "",
5024
5085
  last_context_suggestion_error: String(projectContextSuggestion?.error || "").trim(),
5025
- last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
5086
+ last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
5026
5087
  last_execution_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
5027
5088
  ...intentStatePatch,
5028
5089
  }),
@@ -5036,7 +5097,7 @@ export async function processRunnerSelectedRecord({
5036
5097
  route_name: normalizedRoute.name,
5037
5098
  outcome: deliveryResult.delivery.dryRun ? "dry_run" : "replied",
5038
5099
  detail: [
5039
- `${deliveryResult.delivery.dryRun ? "dry-run prepared" : "replied"} as ${bot.name || bot.id}${executionPlan.usedCommandFallback ? " (legacy command fallback)" : ""}`,
5100
+ `${deliveryResult.delivery.dryRun ? "dry-run prepared" : "replied"} as ${bot.name || bot.id}${effectiveExecutionPlan.usedCommandFallback ? " (legacy command fallback)" : ""}`,
5040
5101
  effectiveTriggerDecision?.trigger ? `trigger=${String(effectiveTriggerDecision.trigger || "").trim()}` : "",
5041
5102
  effectiveConversationContext?.mode === "public_multi_bot"
5042
5103
  ? [
@@ -5082,8 +5143,8 @@ export async function processRunnerSelectedRecord({
5082
5143
  evidence_ids: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).id || "").trim()).filter(Boolean),
5083
5144
  evidence_paths: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).path || "").trim()).filter(Boolean),
5084
5145
  reply_chars: String(sanitizedReplyText || "").length,
5085
- execution_mode: executionPlan.mode,
5086
- role_profile: executionPlan.roleProfileName,
5146
+ execution_mode: effectiveExecutionPlan.mode,
5147
+ role_profile: effectiveExecutionPlan.roleProfileName,
5087
5148
  executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
5088
5149
  archive_status: deliveryResult.archive?.dry_run
5089
5150
  ? "dry_run"
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import http from "node:http";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
@@ -22,6 +23,67 @@ function ensureArray(value) {
22
23
  return Array.isArray(value) ? value : [];
23
24
  }
24
25
 
26
+ async function startReplyChainSelftestServer({
27
+ projectID,
28
+ threadID,
29
+ comments = [],
30
+ }) {
31
+ const state = {
32
+ comments: ensureArray(comments).slice(),
33
+ };
34
+ const writeJSON = (res, statusCode, payload) => {
35
+ const body = `${JSON.stringify(payload)}\n`;
36
+ res.writeHead(statusCode, {
37
+ "content-type": "application/json",
38
+ "content-length": Buffer.byteLength(body),
39
+ connection: "close",
40
+ });
41
+ res.end(body);
42
+ };
43
+ const server = http.createServer((req, res) => {
44
+ const requestURL = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
45
+ const pathname = requestURL.pathname;
46
+ if (req.method === "GET" && pathname === `/api/v1/projects/${encodeURIComponent(projectID)}/runner-requests`) {
47
+ writeJSON(res, 200, []);
48
+ return;
49
+ }
50
+ if (req.method === "GET" && pathname === `/api/v1/projects/${encodeURIComponent(projectID)}/runner-request-comments`) {
51
+ writeJSON(res, 200, []);
52
+ return;
53
+ }
54
+ if (req.method === "GET" && pathname === `/api/v1/threads/${encodeURIComponent(threadID)}/comments`) {
55
+ writeJSON(res, 200, state.comments);
56
+ return;
57
+ }
58
+ writeJSON(res, 404, { error: "not_found" });
59
+ });
60
+ await new Promise((resolve, reject) => {
61
+ server.listen(0, "127.0.0.1", (error) => {
62
+ if (error) {
63
+ reject(error);
64
+ return;
65
+ }
66
+ resolve();
67
+ });
68
+ });
69
+ const address = server.address();
70
+ const port = typeof address === "object" && address ? address.port : 0;
71
+ return {
72
+ baseURL: `http://127.0.0.1:${port}`,
73
+ async close() {
74
+ await new Promise((resolve, reject) => {
75
+ server.close((error) => {
76
+ if (error) {
77
+ reject(error);
78
+ return;
79
+ }
80
+ resolve();
81
+ });
82
+ });
83
+ },
84
+ };
85
+ }
86
+
25
87
  export async function runSelftestRunnerScenarios(push, deps) {
26
88
  const selftestProjectID = String(requireValue(deps, "selftestProjectID"));
27
89
  const allowLegacyRunnerCommandEnvKey = String(requireValue(deps, "allowLegacyRunnerCommandEnvKey"));
@@ -569,8 +631,11 @@ export async function runSelftestRunnerScenarios(push, deps) {
569
631
  },
570
632
  postJSONWithAuthHeaders: async (url, _timeoutSeconds, _token, payload) => {
571
633
  createdCalls.push({ url, payload });
572
- if (url.includes("/api/v1/workitems/existing-workitem-1/threads")) {
573
- return JSON.stringify({ id: "thread-existing-1" });
634
+ if (url.endsWith("/api/v1/workitems")) {
635
+ return JSON.stringify({ work_item_id: "workitem-new-1" });
636
+ }
637
+ if (url.includes("/api/v1/workitems/workitem-new-1/threads")) {
638
+ return JSON.stringify({ id: "thread-new-1" });
574
639
  }
575
640
  throw new Error(`unexpected url ${url}`);
576
641
  },
@@ -585,16 +650,72 @@ export async function runSelftestRunnerScenarios(push, deps) {
585
650
  },
586
651
  );
587
652
  push(
588
- "archive_thread_reuses_existing_work_item_without_thread",
589
- archiveThread.threadID === "thread-existing-1"
590
- && archiveThread.workItemID === "existing-workitem-1"
591
- && archiveThread.source === "auto-created-thread-on-existing-work-item"
653
+ "archive_thread_does_not_reuse_title_only_work_item_without_chat_match",
654
+ archiveThread.threadID === "thread-new-1"
655
+ && archiveThread.workItemID === "workitem-new-1"
656
+ && archiveThread.source === "auto-created-thread"
592
657
  && workItemListHeaders[0]?.["X-Actor-User-Id"] === "99999999-9999-9999-9999-999999999999"
593
- && createdCalls.length === 1,
658
+ && createdCalls.length === 2,
594
659
  `thread=${archiveThread.threadID} work_item=${archiveThread.workItemID} created_calls=${createdCalls.length} actor_header=${String(workItemListHeaders[0]?.["X-Actor-User-Id"] || "")}`,
595
660
  );
596
661
  } catch (err) {
597
- push("archive_thread_reuses_existing_work_item_without_thread", false, String(err?.message || err));
662
+ push("archive_thread_does_not_reuse_title_only_work_item_without_chat_match", false, String(err?.message || err));
663
+ }
664
+
665
+ try {
666
+ const createdCalls = [];
667
+ const archiveThread = await discoverArchiveThreadForDestination(
668
+ {
669
+ siteBaseURL: "https://example.test",
670
+ projectID: selftestProjectID,
671
+ provider: "telegram",
672
+ destination: {
673
+ id: "dest-1",
674
+ provider: "telegram",
675
+ label: "Main Room",
676
+ chatID: "-1001",
677
+ isActive: true,
678
+ },
679
+ token: "test-token",
680
+ timeoutSeconds: 10,
681
+ actorUserID: "99999999-9999-9999-9999-999999999999",
682
+ archiveWorkItemID: "12345678-1234-1234-1234-1234567890ab",
683
+ },
684
+ {
685
+ getJSONWithAuth: async () => [],
686
+ getJSONWithAuthHeaders: async (url) => {
687
+ if (url.includes("/api/v1/workitems/12345678-1234-1234-1234-1234567890ab/threads")) {
688
+ return [];
689
+ }
690
+ return [];
691
+ },
692
+ postJSONWithAuthHeaders: async (url, _timeoutSeconds, _token, payload) => {
693
+ createdCalls.push({ url, payload });
694
+ if (url.includes("/api/v1/workitems/12345678-1234-1234-1234-1234567890ab/threads")) {
695
+ return JSON.stringify({ id: "thread-configured-1" });
696
+ }
697
+ throw new Error(`unexpected url ${url}`);
698
+ },
699
+ parseJSONText: JSON.parse,
700
+ normalizeChatDestination: (value) => value,
701
+ normalizeBotProvider: (value) => value,
702
+ normalizeBotRole: (value) => value,
703
+ normalizeRunnerRoute: (value) => value,
704
+ providerEnvConfig: () => ({ label: "Telegram" }),
705
+ parseArchivedChatComment: () => null,
706
+ isUUID: (value) => /^[0-9a-f-]{36}$/i.test(String(value || "")),
707
+ },
708
+ );
709
+ push(
710
+ "archive_thread_reuses_only_configured_archive_work_item_without_chat_match",
711
+ archiveThread.threadID === "thread-configured-1"
712
+ && archiveThread.workItemID === "12345678-1234-1234-1234-1234567890ab"
713
+ && archiveThread.source === "auto-created-thread-on-configured-work-item"
714
+ && createdCalls.length === 1,
715
+ `thread=${archiveThread.threadID} work_item=${archiveThread.workItemID} created_calls=${createdCalls.length}`,
716
+ );
717
+ } catch (err) {
718
+ push("archive_thread_reuses_only_configured_archive_work_item_without_chat_match", false, String(err?.message || err));
598
719
  }
599
720
 
600
721
  const aliasMaps = buildToolAliasMaps([
@@ -1716,6 +1837,103 @@ export async function runSelftestRunnerScenarios(push, deps) {
1716
1837
  `conversation=${String(replyChainStatusLoopLookup.resolved_conversation_id || "(none)")} source_message=${String(safeObject(replyChainStatusLoopLookup.related_request).source_message_id || "(none)")} intent=${String(safeObject(replyChainStatusLoopLookup.related_request).normalized_intent || "(none)")} root_work_item=${String(safeObject(replyChainStatusLoopLookup.root_work_item).id || "(none)")}`,
1717
1838
  );
1718
1839
 
1840
+ const multihopConversationID = "reply_chain:telegram:-100123:701";
1841
+ const multihopThreadID = "reply-chain-thread-701";
1842
+ const multihopBotReplyBody = formatBotReplyArchiveComment({
1843
+ provider: "telegram",
1844
+ bot: {
1845
+ id: "bot-ryoai",
1846
+ name: "RyoAI_bot",
1847
+ username: "RyoAI_bot",
1848
+ role: "monitor",
1849
+ },
1850
+ destination: {
1851
+ chatID: "-100123",
1852
+ label: "Main Room",
1853
+ },
1854
+ replyText: "Still working on the original task.",
1855
+ messageID: 702,
1856
+ replyToMessageID: 701,
1857
+ });
1858
+ const multihopServer = await startReplyChainSelftestServer({
1859
+ projectID: selftestProjectID,
1860
+ threadID: multihopThreadID,
1861
+ comments: [
1862
+ {
1863
+ id: "comment-bot-reply-702",
1864
+ body: multihopBotReplyBody,
1865
+ },
1866
+ ],
1867
+ });
1868
+ try {
1869
+ saveBotRunnerState({
1870
+ routes: {},
1871
+ sharedInboxes: {},
1872
+ excludedComments: {},
1873
+ requests: {
1874
+ "request-key-701": {
1875
+ request_key: "request-key-701",
1876
+ project_id: selftestProjectID,
1877
+ provider: "telegram",
1878
+ chat_id: "-100123",
1879
+ source_message_id: 701,
1880
+ conversation_id: "",
1881
+ selected_bot_usernames: ["ryoai_bot"],
1882
+ normalized_intent: "ctxpack_mutation",
1883
+ status: "running",
1884
+ claimed_by_route: requestRouteKey,
1885
+ root_work_item_id: "root-work-item-701",
1886
+ root_work_item_title: "Root reply-chain task",
1887
+ root_work_item_status: "doing",
1888
+ root_thread_id: "root-thread-701",
1889
+ updated_at: "2026-03-22T02:03:00.000Z",
1890
+ },
1891
+ },
1892
+ consumedComments: {},
1893
+ });
1894
+ const multihopReplyClaim = await claimRunnerRequestForHumanComment({
1895
+ normalizedRoute: requestRoute,
1896
+ routeKey: requestRouteKey,
1897
+ selectedRecord: {
1898
+ id: "comment-request-root-task-5",
1899
+ createdAt: "2026-03-22T00:07:00.000Z",
1900
+ updatedAt: "2026-03-22T00:07:00.000Z",
1901
+ parsedArchive: {
1902
+ kind: "telegram_message",
1903
+ chatID: "-100123",
1904
+ chatType: "supergroup",
1905
+ body: "@RyoAI_bot is the original task done now?",
1906
+ messageID: 704,
1907
+ replyToMessageID: 702,
1908
+ senderIsBot: false,
1909
+ mentionUsernames: ["ryoai_bot"],
1910
+ },
1911
+ },
1912
+ selectedBotUsernames: ["ryoai_bot"],
1913
+ normalizedIntent: "status_query",
1914
+ runtime: {
1915
+ baseURL: multihopServer.baseURL,
1916
+ token: "selftest-token",
1917
+ timeoutSeconds: 5,
1918
+ actor: { user_id: "selftest-user" },
1919
+ },
1920
+ archiveThreadID: multihopThreadID,
1921
+ });
1922
+ const multihopClaimedRequest = safeObject(
1923
+ safeObject(loadBotRunnerState().requests)[multihopReplyClaim.requestKey],
1924
+ );
1925
+ push(
1926
+ "runner_request_claim_resolves_multihop_reply_chain_anchor",
1927
+ multihopReplyClaim.ok === true
1928
+ && String(multihopClaimedRequest.conversation_id || "") === multihopConversationID
1929
+ && String(multihopClaimedRequest.root_work_item_id || "") === "root-work-item-701"
1930
+ && String(multihopClaimedRequest.root_thread_id || "") === "root-thread-701",
1931
+ `conversation=${String(multihopClaimedRequest.conversation_id || "(none)")} root_work_item=${String(multihopClaimedRequest.root_work_item_id || "(none)")} root_thread=${String(multihopClaimedRequest.root_thread_id || "(none)")}`,
1932
+ );
1933
+ } finally {
1934
+ await multihopServer.close();
1935
+ }
1936
+
1719
1937
  const stateBeforeCleanup = loadBotRunnerState();
1720
1938
  saveBotRunnerState({
1721
1939
  ...stateBeforeCleanup,
@@ -8401,6 +8619,159 @@ export async function runSelftestRunnerScenarios(push, deps) {
8401
8619
  push("small_talk_direct_mention_skips_dynamic_execution_plan", false, String(err?.message || err));
8402
8620
  }
8403
8621
 
8622
+ try {
8623
+ let capturedExecutionPlan = null;
8624
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-small-talk-mini-"));
8625
+ const processed = await processRunnerSelectedRecord({
8626
+ routeKey: "small-talk-mini-execution-key",
8627
+ normalizedRoute: normalizeRunnerRoute({
8628
+ name: "telegram-monitor-small-talk-mini",
8629
+ project_id: selftestProjectID,
8630
+ provider: "telegram",
8631
+ role: "monitor",
8632
+ role_profile: "monitor",
8633
+ destination_id: "dest-1",
8634
+ destination_label: "Main Room",
8635
+ server_bot_name: "RyoAI_bot",
8636
+ server_bot_id: "bot-1",
8637
+ trigger_policy: {
8638
+ mentions_only: true,
8639
+ direct_messages: true,
8640
+ reply_to_bot_messages: true,
8641
+ },
8642
+ archive_policy: {
8643
+ mirror_replies: true,
8644
+ dedupe_inbound: true,
8645
+ dedupe_outbound: true,
8646
+ skip_bot_messages: true,
8647
+ },
8648
+ dry_run_delivery: true,
8649
+ }),
8650
+ selectedRecord: {
8651
+ id: "comment-small-talk-mini-execution",
8652
+ createdAt: "2026-03-24T00:11:00.000Z",
8653
+ parsedArchive: {
8654
+ kind: "telegram_message",
8655
+ chatID: "-100123",
8656
+ chatType: "supergroup",
8657
+ body: "@RyoAI_bot 하이",
8658
+ messageID: 212,
8659
+ sender: "human",
8660
+ senderIsBot: false,
8661
+ mentionUsernames: ["ryoai_bot"],
8662
+ },
8663
+ },
8664
+ pendingOrdered: [],
8665
+ bot: {
8666
+ id: "bot-1",
8667
+ name: "RyoAI_bot",
8668
+ username: "RyoAI_bot",
8669
+ role: "monitor",
8670
+ provider: "telegram",
8671
+ },
8672
+ destination: {
8673
+ id: "dest-1",
8674
+ label: "Main Room",
8675
+ provider: "telegram",
8676
+ chatID: "-100123",
8677
+ },
8678
+ archiveThread: {
8679
+ threadID: "thread-1",
8680
+ workItemID: "work-item-1",
8681
+ },
8682
+ executionPlan: {
8683
+ mode: "role_profile",
8684
+ roleProfileName: "monitor",
8685
+ roleProfile: {
8686
+ client: "gpt",
8687
+ model: "gpt-5.4",
8688
+ permissionMode: "workspace_write",
8689
+ reasoningEffort: "medium",
8690
+ },
8691
+ workspaceDir,
8692
+ workspaceSource: "selftest",
8693
+ usedCommandFallback: false,
8694
+ },
8695
+ runtime: {
8696
+ baseURL: "https://example.test",
8697
+ token: "selftest-token",
8698
+ timeoutSeconds: 30,
8699
+ actor: { user_id: "user-1" },
8700
+ },
8701
+ deps: {
8702
+ saveRunnerRouteState: () => {},
8703
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
8704
+ runRunnerAIExecution: async ({ executionPlan }) => {
8705
+ capturedExecutionPlan = safeObject(executionPlan);
8706
+ return {
8707
+ skip: false,
8708
+ reply: "안녕하세요.",
8709
+ replyToMessageID: 212,
8710
+ };
8711
+ },
8712
+ performLocalBotDelivery: async () => ({
8713
+ delivery: { dryRun: true, body: {} },
8714
+ archive: {},
8715
+ }),
8716
+ serializeRunnerTriggerPolicy: (value) => value,
8717
+ serializeRunnerArchivePolicy: (value) => value,
8718
+ buildRunnerExecutionDeps: () => ({
8719
+ resolveInformationalQueryReply: async () => ({
8720
+ handled: true,
8721
+ response_mode: "lookup_only",
8722
+ reply: "",
8723
+ source: "small_talk",
8724
+ lookup: { intent_type: "small_talk" },
8725
+ execution_override: {
8726
+ mode: "role_profile",
8727
+ role_profile_name: "monitor",
8728
+ role_profile: {
8729
+ client: "gpt",
8730
+ model: "gpt-5-mini",
8731
+ permissionMode: "read_only",
8732
+ reasoningEffort: "low",
8733
+ },
8734
+ workspace_dir: workspaceDir,
8735
+ workspace_source: "selftest",
8736
+ },
8737
+ }),
8738
+ planRoleExecutionWithAI: async () => ({
8739
+ requiresExecution: true,
8740
+ summaryRole: "worker",
8741
+ steps: [{ role: "worker", goal: "unexpected", artifactsRequired: true }],
8742
+ }),
8743
+ resolveRunnerExecutionPlanForRole: () => ({
8744
+ mode: "role_profile",
8745
+ roleProfileName: "worker",
8746
+ roleProfile: {
8747
+ client: "sample",
8748
+ model: "",
8749
+ permissionMode: "workspace_write",
8750
+ reasoningEffort: "medium",
8751
+ },
8752
+ workspaceDir,
8753
+ workspaceSource: "selftest",
8754
+ usedCommandFallback: false,
8755
+ }),
8756
+ }),
8757
+ buildRunnerDeliveryDeps: () => ({}),
8758
+ buildRunnerRuntimeDeps: () => ({}),
8759
+ resolveConversationPeerBots: () => [],
8760
+ },
8761
+ });
8762
+ push(
8763
+ "small_talk_lookup_only_uses_mini_execution_plan",
8764
+ processed.kind === "replied"
8765
+ && String(safeObject(capturedExecutionPlan).roleProfile?.client || "") === "gpt"
8766
+ && ["gpt-5-mini", "gpt-5.3-codex-spark"].includes(String(safeObject(capturedExecutionPlan).roleProfile?.model || ""))
8767
+ && String(safeObject(capturedExecutionPlan).roleProfile?.permissionMode || "") === "read_only"
8768
+ && String(safeObject(capturedExecutionPlan).roleProfile?.reasoningEffort || "") === "low",
8769
+ JSON.stringify(capturedExecutionPlan || {}),
8770
+ );
8771
+ } catch (err) {
8772
+ push("small_talk_lookup_only_uses_mini_execution_plan", false, String(err?.message || err));
8773
+ }
8774
+
8404
8775
  try {
8405
8776
  let aiCalls = 0;
8406
8777
  let capturedInputPayload = null;
@@ -868,6 +868,57 @@ export async function runSelftestTelegramE2E(push, deps) {
868
868
  `count=${telegramE2EServer.state.comments.filter((item) => String(item.body || "").includes("message_id: 353")).length}`,
869
869
  );
870
870
 
871
+ telegramE2EServer.state.comments = [];
872
+ telegramE2EServer.state.updates = [
873
+ {
874
+ update_id: 354,
875
+ message: {
876
+ message_id: 354,
877
+ message_thread_id: 999,
878
+ date: Math.floor(Date.now() / 1000),
879
+ chat: {
880
+ id: Number(e2eDestination.chat_id),
881
+ type: "supergroup",
882
+ title: e2eDestination.label,
883
+ },
884
+ from: {
885
+ id: 5001,
886
+ is_bot: false,
887
+ first_name: "Operator",
888
+ username: "operator_user",
889
+ },
890
+ text: "topic metadata should not be archived",
891
+ },
892
+ },
893
+ ];
894
+ await archiveLocalTelegramMessagesForRoute({
895
+ routeKey: pruneRouteKey,
896
+ route: pruneRoute,
897
+ routeState: safeObject(loadBotRunnerState().routes[pruneRouteKey]),
898
+ runtime: {
899
+ baseURL: telegramE2EServer.baseURL,
900
+ timeoutSeconds: 10,
901
+ token: e2eToken,
902
+ actor: {
903
+ user_id: e2eActorUserID,
904
+ },
905
+ },
906
+ bot: e2eBot,
907
+ destination: {
908
+ chatID: e2eDestination.chat_id,
909
+ },
910
+ archiveThread: {
911
+ threadID: e2eThreadID,
912
+ },
913
+ deps: buildRunnerRuntimeDeps(),
914
+ });
915
+ const topicArchiveBody = String(safeObject(telegramE2EServer.state.comments[0]).body || "");
916
+ push(
917
+ "telegram_inbound_archive_omits_unused_topic_metadata",
918
+ topicArchiveBody.includes("message_id: 354") && !topicArchiveBody.includes("telegram_topic_id:"),
919
+ topicArchiveBody,
920
+ );
921
+
871
922
  telegramE2EServer.state.comments = [];
872
923
  telegramE2EServer.state.updates = [
873
924
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.200",
3
+ "version": "0.2.201",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [