metheus-governance-mcp-cli 0.2.197 → 0.2.198

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
@@ -158,11 +158,13 @@ import {
158
158
  listProjectCtxpackFiles as listProjectCtxpackFilesImpl,
159
159
  listProjectRunnerRequestCommentStates as listProjectRunnerRequestCommentStatesImpl,
160
160
  listProjectRunnerRequests as listProjectRunnerRequestsImpl,
161
+ listWorkItemThreads as listWorkItemThreadsImpl,
161
162
  listThreadComments as listThreadCommentsImpl,
162
163
  listThreadCommentsTail as listThreadCommentsTailImpl,
163
164
  listUserBotsForRunner as listUserBotsForRunnerImpl,
164
165
  selectProjectChatDestination as selectProjectChatDestinationImpl,
165
166
  selectRunnerBot as selectRunnerBotImpl,
167
+ transitionProjectWorkItem as transitionProjectWorkItemImpl,
166
168
  upsertProjectRunnerRequest as upsertProjectRunnerRequestImpl,
167
169
  upsertProjectRunnerRequestCommentState as upsertProjectRunnerRequestCommentStateImpl,
168
170
  updateProjectContextItem as updateProjectContextItemImpl,
@@ -1914,9 +1916,17 @@ function mergeRunnerStateRecords(preferred, fallback) {
1914
1916
  active_comment_id: pickString(primary.active_comment_id, secondary.active_comment_id),
1915
1917
  active_comment_created_at: pickString(primary.active_comment_created_at, secondary.active_comment_created_at),
1916
1918
  active_source_message_id: pickNumber(primary.active_source_message_id, secondary.active_source_message_id) || undefined,
1919
+ active_request_key: pickString(primary.active_request_key, secondary.active_request_key),
1917
1920
  active_started_at: pickString(primary.active_started_at, secondary.active_started_at),
1921
+ active_root_work_item_id: pickString(primary.active_root_work_item_id, secondary.active_root_work_item_id),
1922
+ active_root_work_item_title: pickString(primary.active_root_work_item_title, secondary.active_root_work_item_title),
1923
+ active_root_work_item_status: pickString(primary.active_root_work_item_status, secondary.active_root_work_item_status),
1918
1924
  active_runner_pid: pickNumber(primary.active_runner_pid, secondary.active_runner_pid) || undefined,
1919
1925
  active_execution_token: pickString(primary.active_execution_token, secondary.active_execution_token),
1926
+ last_request_key: pickString(primary.last_request_key, secondary.last_request_key),
1927
+ last_root_work_item_id: pickString(primary.last_root_work_item_id, secondary.last_root_work_item_id),
1928
+ last_root_work_item_title: pickString(primary.last_root_work_item_title, secondary.last_root_work_item_title),
1929
+ last_root_work_item_status: pickString(primary.last_root_work_item_status, secondary.last_root_work_item_status),
1920
1930
  conversation_sessions: {
1921
1931
  ...safeObject(secondary.conversation_sessions),
1922
1932
  ...safeObject(primary.conversation_sessions),
@@ -2123,6 +2133,13 @@ function isActiveRunnerRequestStatus(rawStatus) {
2123
2133
  return status === "planned" || status === "claimed" || status === "running";
2124
2134
  }
2125
2135
 
2136
+ function normalizeRunnerWorkItemStatus(rawStatus) {
2137
+ const status = String(rawStatus || "").trim().toLowerCase();
2138
+ return ["backlog", "doing", "review", "done", "canceled"].includes(status)
2139
+ ? status
2140
+ : "";
2141
+ }
2142
+
2126
2143
  function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2127
2144
  const normalized = {};
2128
2145
  for (const [requestKeyRaw, entryRaw] of Object.entries(safeObject(rawRequests))) {
@@ -2158,6 +2175,12 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2158
2175
  completed_at: firstNonEmptyString([entry.completed_at, entry.completedAt]),
2159
2176
  closed_at: firstNonEmptyString([entry.closed_at, entry.closedAt]),
2160
2177
  closed_reason: String(entry.closed_reason || entry.closedReason || "").trim(),
2178
+ root_work_item_id: String(entry.root_work_item_id || entry.rootWorkItemID || "").trim(),
2179
+ root_work_item_title: String(entry.root_work_item_title || entry.rootWorkItemTitle || "").trim(),
2180
+ root_work_item_status: normalizeRunnerWorkItemStatus(entry.root_work_item_status || entry.rootWorkItemStatus),
2181
+ root_thread_id: String(entry.root_thread_id || entry.rootThreadID || "").trim(),
2182
+ root_work_item_created_at: firstNonEmptyString([entry.root_work_item_created_at, entry.rootWorkItemCreatedAt]),
2183
+ root_work_item_last_error: String(entry.root_work_item_last_error || entry.rootWorkItemLastError || "").trim(),
2161
2184
  last_comment_id: String(entry.last_comment_id || entry.lastCommentID || "").trim(),
2162
2185
  last_comment_kind: String(entry.last_comment_kind || entry.lastCommentKind || "").trim().toLowerCase(),
2163
2186
  last_source_message_id: intFromRawAllowZero(entry.last_source_message_id || entry.lastSourceMessageID, 0) || undefined,
@@ -2409,6 +2432,67 @@ function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {})
2409
2432
  ));
2410
2433
  }
2411
2434
 
2435
+ function sortRunnerRequestEntriesNewestFirst(entries = []) {
2436
+ return ensureArray(entries).slice().sort((leftRaw, rightRaw) => {
2437
+ const left = safeObject(leftRaw);
2438
+ const right = safeObject(rightRaw);
2439
+ const leftTime = firstNonEmptyString([left.updated_at, left.completed_at, left.closed_at, left.claimed_at]);
2440
+ const rightTime = firstNonEmptyString([right.updated_at, right.completed_at, right.closed_at, right.claimed_at]);
2441
+ if (leftTime && rightTime && leftTime !== rightTime) {
2442
+ return leftTime < rightTime ? 1 : -1;
2443
+ }
2444
+ return String(left.request_key || "").localeCompare(String(right.request_key || ""));
2445
+ });
2446
+ }
2447
+
2448
+ async function findServerRunnerRequestForMessageID({
2449
+ normalizedRoute,
2450
+ runtime,
2451
+ chatID,
2452
+ messageID,
2453
+ }) {
2454
+ const projectID = String(normalizedRoute?.projectID || "").trim();
2455
+ const provider = String(normalizedRoute?.provider || "").trim();
2456
+ const normalizedChatID = String(chatID || "").trim();
2457
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2458
+ if (
2459
+ !projectID
2460
+ || !provider
2461
+ || !normalizedChatID
2462
+ || normalizedMessageID <= 0
2463
+ || !runtime?.baseURL
2464
+ || !runtime?.token
2465
+ ) {
2466
+ return null;
2467
+ }
2468
+ try {
2469
+ const serverRequests = await listProjectRunnerRequests({
2470
+ siteBaseURL: runtime.baseURL,
2471
+ projectID,
2472
+ token: runtime.token,
2473
+ timeoutSeconds: runtime.timeoutSeconds,
2474
+ actorUserID: runtime.actor?.user_id,
2475
+ limit: 500,
2476
+ offset: 0,
2477
+ });
2478
+ const matched = sortRunnerRequestEntriesNewestFirst(serverRequests.filter((entryRaw) => {
2479
+ const entry = safeObject(entryRaw);
2480
+ return (
2481
+ String(entry.project_id || "").trim() === projectID
2482
+ && String(entry.provider || "").trim() === provider
2483
+ && String(entry.chat_id || "").trim() === normalizedChatID
2484
+ && (
2485
+ intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
2486
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
2487
+ )
2488
+ );
2489
+ }));
2490
+ return safeObject(matched[0]);
2491
+ } catch {
2492
+ return null;
2493
+ }
2494
+ }
2495
+
2412
2496
  function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
2413
2497
  const parsed = safeObject(selectedRecord?.parsedArchive);
2414
2498
  const explicitConversationID = String(parsed.conversationID || "").trim();
@@ -2451,6 +2535,74 @@ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, sele
2451
2535
  };
2452
2536
  }
2453
2537
 
2538
+ async function resolveRunnerReplyChainConversationContextWithServerFallback({
2539
+ state,
2540
+ normalizedRoute,
2541
+ selectedRecord,
2542
+ runtime,
2543
+ }) {
2544
+ const initialState = safeObject(state);
2545
+ const initialContext = resolveRunnerReplyChainConversationContext(initialState, normalizedRoute, selectedRecord);
2546
+ if (safeObject(initialContext.referencedRequest).request_key) {
2547
+ return {
2548
+ state: initialState,
2549
+ replyChainContext: initialContext,
2550
+ hydrated: false,
2551
+ };
2552
+ }
2553
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2554
+ const replyToMessageID = intFromRawAllowZero(
2555
+ parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
2556
+ 0,
2557
+ );
2558
+ if (replyToMessageID <= 0 || !runtime?.baseURL || !runtime?.token) {
2559
+ return {
2560
+ state: initialState,
2561
+ replyChainContext: initialContext,
2562
+ hydrated: false,
2563
+ };
2564
+ }
2565
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
2566
+ const serverReferencedRequest = await findServerRunnerRequestForMessageID({
2567
+ normalizedRoute,
2568
+ runtime,
2569
+ chatID,
2570
+ messageID: replyToMessageID,
2571
+ });
2572
+ if (serverReferencedRequest.request_key) {
2573
+ const requestIndex = normalizeBotRunnerRequests(initialState.requests);
2574
+ requestIndex[String(serverReferencedRequest.request_key || "").trim()] = serverReferencedRequest;
2575
+ const anchorMessageID = intFromRawAllowZero(serverReferencedRequest.source_message_id, 0) || replyToMessageID;
2576
+ return {
2577
+ state: {
2578
+ ...initialState,
2579
+ requests: requestIndex,
2580
+ },
2581
+ replyChainContext: {
2582
+ conversationID: String(serverReferencedRequest.conversation_id || "").trim()
2583
+ || buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID),
2584
+ replyToMessageID,
2585
+ anchorMessageID,
2586
+ reason: String(serverReferencedRequest.conversation_id || "").trim()
2587
+ ? "reply_request_conversation_server"
2588
+ : "reply_request_synthetic_server",
2589
+ referencedRequest: serverReferencedRequest,
2590
+ },
2591
+ hydrated: false,
2592
+ };
2593
+ }
2594
+ const hydratedState = await hydrateRunnerRequestLedgerFromServer({
2595
+ normalizedRoute,
2596
+ runtime,
2597
+ });
2598
+ const hydratedContext = resolveRunnerReplyChainConversationContext(hydratedState, normalizedRoute, selectedRecord);
2599
+ return {
2600
+ state: hydratedState,
2601
+ replyChainContext: hydratedContext,
2602
+ hydrated: true,
2603
+ };
2604
+ }
2605
+
2454
2606
  function upsertRunnerRequest(state, requestKey, patch = {}) {
2455
2607
  const currentState = safeObject(state);
2456
2608
  const requests = normalizeBotRunnerRequests(currentState.requests);
@@ -2490,12 +2642,13 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
2490
2642
  };
2491
2643
  }
2492
2644
 
2493
- function claimRunnerRequestForHumanComment({
2645
+ async function claimRunnerRequestForHumanComment({
2494
2646
  normalizedRoute,
2495
2647
  routeKey,
2496
2648
  selectedRecord,
2497
2649
  selectedBotUsernames = [],
2498
2650
  normalizedIntent = "",
2651
+ runtime = null,
2499
2652
  }) {
2500
2653
  const parsed = safeObject(selectedRecord?.parsedArchive);
2501
2654
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
@@ -2512,16 +2665,23 @@ function claimRunnerRequestForHumanComment({
2512
2665
  normalizedIntent,
2513
2666
  });
2514
2667
  const currentState = loadBotRunnerState();
2515
- const replyChainContext = resolveRunnerReplyChainConversationContext(currentState, normalizedRoute, selectedRecord);
2668
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
2669
+ state: currentState,
2670
+ normalizedRoute,
2671
+ selectedRecord,
2672
+ runtime,
2673
+ });
2674
+ const replyChainContext = safeObject(replyChainResolution.replyChainContext);
2675
+ const referencedRequest = safeObject(replyChainContext.referencedRequest);
2516
2676
  const conversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
2517
- let stateForClaim = currentState;
2677
+ let stateForClaim = safeObject(replyChainResolution.state);
2518
2678
  if (
2519
- replyChainContext.referencedRequest
2679
+ Object.keys(referencedRequest).length > 0
2520
2680
  && conversationID
2521
- && !String(replyChainContext.referencedRequest.conversation_id || "").trim()
2522
- && String(replyChainContext.referencedRequest.request_key || "").trim()
2681
+ && !String(referencedRequest.conversation_id || "").trim()
2682
+ && String(referencedRequest.request_key || "").trim()
2523
2683
  ) {
2524
- const backfilled = upsertRunnerRequest(stateForClaim, replyChainContext.referencedRequest.request_key, {
2684
+ const backfilled = upsertRunnerRequest(stateForClaim, referencedRequest.request_key, {
2525
2685
  conversation_id: conversationID,
2526
2686
  });
2527
2687
  stateForClaim = {
@@ -2563,6 +2723,17 @@ function claimRunnerRequestForHumanComment({
2563
2723
  status: "claimed",
2564
2724
  claimed_by_route: String(routeKey || "").trim(),
2565
2725
  claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
2726
+ root_work_item_id: String(existing.root_work_item_id || referencedRequest.root_work_item_id || "").trim(),
2727
+ root_work_item_title: String(existing.root_work_item_title || referencedRequest.root_work_item_title || "").trim(),
2728
+ root_work_item_status: normalizeRunnerWorkItemStatus(
2729
+ existing.root_work_item_status || referencedRequest.root_work_item_status,
2730
+ ),
2731
+ root_thread_id: String(existing.root_thread_id || referencedRequest.root_thread_id || "").trim(),
2732
+ root_work_item_created_at: firstNonEmptyString([
2733
+ existing.root_work_item_created_at,
2734
+ referencedRequest.root_work_item_created_at,
2735
+ ]),
2736
+ root_work_item_last_error: String(existing.root_work_item_last_error || "").trim(),
2566
2737
  last_comment_id: String(selectedRecord?.id || "").trim(),
2567
2738
  last_comment_kind: commentKind,
2568
2739
  last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
@@ -2591,6 +2762,562 @@ function claimRunnerRequestForHumanComment({
2591
2762
  };
2592
2763
  }
2593
2764
 
2765
+ function isActionableRunnerRequestIntent(rawIntent) {
2766
+ const normalizedIntent = String(rawIntent || "").trim().toLowerCase();
2767
+ return Boolean(normalizedIntent) && !isInformationalRunnerRequestIntent(normalizedIntent);
2768
+ }
2769
+
2770
+ function truncateRunnerWorkItemTitleText(rawText, maxLength = 96) {
2771
+ const text = String(rawText || "").replace(/\s+/g, " ").trim();
2772
+ if (!text) {
2773
+ return "";
2774
+ }
2775
+ if (text.length <= maxLength) {
2776
+ return text;
2777
+ }
2778
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trim()}...`;
2779
+ }
2780
+
2781
+ function buildRunnerRootWorkItemTitle({ selectedRecord, request }) {
2782
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2783
+ const intent = String(safeObject(request).normalized_intent || "").trim().toLowerCase();
2784
+ const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 84);
2785
+ const prefix = intent === "ctxpack_mutation"
2786
+ ? "Ctxpack request"
2787
+ : intent === "workitem_mutation"
2788
+ ? "Work item request"
2789
+ : "Runner request";
2790
+ if (requestBody) {
2791
+ return `${prefix}: ${requestBody}`;
2792
+ }
2793
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
2794
+ return messageID > 0 ? `${prefix} #${messageID}` : prefix;
2795
+ }
2796
+
2797
+ function buildRunnerRootWorkItemDescription({
2798
+ normalizedRoute,
2799
+ routeKey,
2800
+ selectedRecord,
2801
+ request,
2802
+ }) {
2803
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2804
+ const lines = [
2805
+ `Source request: ${String(parsed.body || "").trim() || "(empty)"}`,
2806
+ `Intent: ${String(safeObject(request).normalized_intent || "").trim() || "unknown"}`,
2807
+ `Route: ${String(normalizedRoute?.name || routeKey || "").trim() || "-"}`,
2808
+ `Provider: ${String(normalizedRoute?.provider || "").trim() || "-"}`,
2809
+ `Chat ID: ${String(parsed.chatID || parsed.chatId || "").trim() || "-"}`,
2810
+ `Message ID: ${intFromRawAllowZero(parsed.messageID, 0) || "-"}`,
2811
+ `Request key: ${String(safeObject(request).request_key || "").trim() || "-"}`,
2812
+ ];
2813
+ const conversationID = String(safeObject(request).conversation_id || parsed.conversationID || "").trim();
2814
+ if (conversationID) {
2815
+ lines.push(`Conversation ID: ${conversationID}`);
2816
+ }
2817
+ const selectedBots = ensureArray(safeObject(request).selected_bot_usernames)
2818
+ .map((item) => normalizeTelegramMentionUsername(item))
2819
+ .filter(Boolean);
2820
+ if (selectedBots.length > 0) {
2821
+ lines.push(`Selected bots: ${selectedBots.join(", ")}`);
2822
+ }
2823
+ return lines.join("\n").trim();
2824
+ }
2825
+
2826
+ function buildRunnerRootWorkItemThreadTitle({ selectedRecord }) {
2827
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2828
+ const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 72);
2829
+ if (requestBody) {
2830
+ return `Request Context: ${requestBody}`;
2831
+ }
2832
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
2833
+ return messageID > 0 ? `Request Context #${messageID}` : "Request Context";
2834
+ }
2835
+
2836
+ function buildRunnerRootWorkItemThreadBody({
2837
+ normalizedRoute,
2838
+ routeKey,
2839
+ selectedRecord,
2840
+ request,
2841
+ }) {
2842
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2843
+ const lines = [
2844
+ "Runner root request context thread.",
2845
+ buildRunnerRootWorkItemDescription({
2846
+ normalizedRoute,
2847
+ routeKey,
2848
+ selectedRecord,
2849
+ request,
2850
+ }),
2851
+ ];
2852
+ if (String(selectedRecord?.id || "").trim()) {
2853
+ lines.push(`Archive comment ID: ${String(selectedRecord.id || "").trim()}`);
2854
+ }
2855
+ const occurredAt = String(parsed.occurredAt || parsed.occurred_at || "").trim();
2856
+ if (occurredAt) {
2857
+ lines.push(`Occurred at: ${occurredAt}`);
2858
+ }
2859
+ return lines.filter(Boolean).join("\n").trim();
2860
+ }
2861
+
2862
+ async function ensureRunnerRootThreadForRequest({
2863
+ normalizedRoute,
2864
+ routeKey,
2865
+ selectedRecord,
2866
+ runtime,
2867
+ requestKey,
2868
+ }) {
2869
+ const key = String(requestKey || "").trim();
2870
+ if (!key) {
2871
+ return {
2872
+ ok: false,
2873
+ reason: "request_key_missing",
2874
+ };
2875
+ }
2876
+ const currentState = loadBotRunnerState();
2877
+ const request = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
2878
+ const rootWorkItemID = String(request.root_work_item_id || "").trim();
2879
+ if (!Object.keys(request).length) {
2880
+ return {
2881
+ ok: false,
2882
+ reason: "request_not_found",
2883
+ requestKey: key,
2884
+ };
2885
+ }
2886
+ if (!rootWorkItemID) {
2887
+ return {
2888
+ ok: true,
2889
+ requestKey: key,
2890
+ request,
2891
+ skipped: true,
2892
+ reason: "root_work_item_missing",
2893
+ };
2894
+ }
2895
+ if (String(request.root_thread_id || "").trim()) {
2896
+ return {
2897
+ ok: true,
2898
+ requestKey: key,
2899
+ request,
2900
+ reused: true,
2901
+ };
2902
+ }
2903
+ if (!runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
2904
+ return {
2905
+ ok: false,
2906
+ reason: "governance_runtime_unavailable",
2907
+ requestKey: key,
2908
+ request,
2909
+ };
2910
+ }
2911
+ try {
2912
+ let rootThreadID = "";
2913
+ const existingThreads = ensureArray(await listWorkItemThreads({
2914
+ siteBaseURL: runtime.baseURL,
2915
+ token: runtime.token,
2916
+ timeoutSeconds: runtime.timeoutSeconds,
2917
+ workItemID: rootWorkItemID,
2918
+ status: "",
2919
+ }));
2920
+ rootThreadID = String(
2921
+ safeObject(existingThreads[0]).id
2922
+ || safeObject(existingThreads[0]).thread_id
2923
+ || safeObject(existingThreads[0]).threadID
2924
+ || "",
2925
+ ).trim();
2926
+ if (!rootThreadID) {
2927
+ const createdThread = safeObject(await createWorkItemThread({
2928
+ siteBaseURL: runtime.baseURL,
2929
+ token: runtime.token,
2930
+ timeoutSeconds: runtime.timeoutSeconds,
2931
+ actorUserID: runtime.actor.user_id,
2932
+ workItemID: rootWorkItemID,
2933
+ title: buildRunnerRootWorkItemThreadTitle({
2934
+ selectedRecord,
2935
+ }),
2936
+ body: buildRunnerRootWorkItemThreadBody({
2937
+ normalizedRoute,
2938
+ routeKey,
2939
+ selectedRecord,
2940
+ request,
2941
+ }),
2942
+ }));
2943
+ rootThreadID = String(createdThread.thread_id || createdThread.threadID || createdThread.id || "").trim();
2944
+ }
2945
+ if (!rootThreadID) {
2946
+ throw new Error("root thread creation returned no id");
2947
+ }
2948
+ const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, key, {
2949
+ root_thread_id: rootThreadID,
2950
+ });
2951
+ saveBotRunnerState({
2952
+ routes: currentState.routes,
2953
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
2954
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
2955
+ requests: nextRequests,
2956
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
2957
+ });
2958
+ return {
2959
+ ok: true,
2960
+ requestKey: key,
2961
+ request: nextRequest,
2962
+ created: true,
2963
+ };
2964
+ } catch (err) {
2965
+ return {
2966
+ ok: false,
2967
+ reason: "root_thread_create_failed",
2968
+ requestKey: key,
2969
+ request,
2970
+ error: String(err?.message || err).trim() || "failed to create root thread",
2971
+ };
2972
+ }
2973
+ }
2974
+
2975
+ async function inheritRunnerReferenceRootWorkItemForRequest({
2976
+ normalizedRoute,
2977
+ selectedRecord,
2978
+ runtime,
2979
+ requestKey,
2980
+ }) {
2981
+ const key = String(requestKey || "").trim();
2982
+ if (!key) {
2983
+ return {
2984
+ ok: false,
2985
+ reason: "request_key_missing",
2986
+ };
2987
+ }
2988
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2989
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
2990
+ const replyToMessageID = intFromRawAllowZero(parsed.replyToMessageID, 0);
2991
+ if (!chatID || replyToMessageID <= 0) {
2992
+ return {
2993
+ ok: true,
2994
+ requestKey: key,
2995
+ skipped: true,
2996
+ reason: "reply_reference_missing",
2997
+ };
2998
+ }
2999
+ const currentState = loadBotRunnerState();
3000
+ const currentRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3001
+ if (!Object.keys(currentRequest).length) {
3002
+ return {
3003
+ ok: false,
3004
+ requestKey: key,
3005
+ reason: "request_not_found",
3006
+ };
3007
+ }
3008
+ if (String(currentRequest.root_work_item_id || "").trim()) {
3009
+ return {
3010
+ ok: true,
3011
+ requestKey: key,
3012
+ request: currentRequest,
3013
+ reused: true,
3014
+ };
3015
+ }
3016
+ const serverReferencedRequest = await findServerRunnerRequestForMessageID({
3017
+ normalizedRoute,
3018
+ runtime,
3019
+ chatID,
3020
+ messageID: replyToMessageID,
3021
+ });
3022
+ if (!String(serverReferencedRequest.root_work_item_id || "").trim()) {
3023
+ return {
3024
+ ok: true,
3025
+ requestKey: key,
3026
+ request: currentRequest,
3027
+ skipped: true,
3028
+ reason: "referenced_root_work_item_missing",
3029
+ };
3030
+ }
3031
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3032
+ root_work_item_id: String(serverReferencedRequest.root_work_item_id || "").trim(),
3033
+ root_work_item_title: String(serverReferencedRequest.root_work_item_title || "").trim(),
3034
+ root_work_item_status: normalizeRunnerWorkItemStatus(serverReferencedRequest.root_work_item_status),
3035
+ root_thread_id: String(serverReferencedRequest.root_thread_id || "").trim(),
3036
+ root_work_item_created_at: firstNonEmptyString([serverReferencedRequest.root_work_item_created_at]),
3037
+ root_work_item_last_error: String(serverReferencedRequest.root_work_item_last_error || "").trim(),
3038
+ });
3039
+ saveBotRunnerState({
3040
+ routes: currentState.routes,
3041
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3042
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3043
+ requests: nextRequests,
3044
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3045
+ });
3046
+ return {
3047
+ ok: true,
3048
+ requestKey: key,
3049
+ request,
3050
+ inherited: true,
3051
+ };
3052
+ }
3053
+
3054
+ async function ensureRunnerRootWorkItemForRequest({
3055
+ normalizedRoute,
3056
+ routeKey,
3057
+ selectedRecord,
3058
+ runtime,
3059
+ requestKey,
3060
+ }) {
3061
+ const key = String(requestKey || "").trim();
3062
+ if (!key) {
3063
+ return {
3064
+ ok: false,
3065
+ reason: "request_key_missing",
3066
+ };
3067
+ }
3068
+ const currentState = loadBotRunnerState();
3069
+ const existing = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3070
+ if (!Object.keys(existing).length) {
3071
+ return {
3072
+ ok: false,
3073
+ reason: "request_not_found",
3074
+ requestKey: key,
3075
+ };
3076
+ }
3077
+ if (!isActionableRunnerRequestIntent(existing.normalized_intent)) {
3078
+ return {
3079
+ ok: true,
3080
+ requestKey: key,
3081
+ request: existing,
3082
+ skipped: true,
3083
+ reason: "informational_request",
3084
+ };
3085
+ }
3086
+ if (String(existing.root_work_item_id || "").trim()) {
3087
+ const rootThreadClaim = await ensureRunnerRootThreadForRequest({
3088
+ normalizedRoute,
3089
+ routeKey,
3090
+ selectedRecord,
3091
+ runtime,
3092
+ requestKey: key,
3093
+ });
3094
+ return {
3095
+ ok: true,
3096
+ requestKey: key,
3097
+ request: safeObject(rootThreadClaim.request || existing),
3098
+ reused: true,
3099
+ root_thread_created: rootThreadClaim.created === true,
3100
+ root_thread_error: String(rootThreadClaim.error || "").trim(),
3101
+ };
3102
+ }
3103
+ if (!runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
3104
+ return {
3105
+ ok: false,
3106
+ reason: "governance_runtime_unavailable",
3107
+ requestKey: key,
3108
+ };
3109
+ }
3110
+ try {
3111
+ const actorBotID = firstNonEmptyString([
3112
+ normalizedRoute?.botID,
3113
+ normalizedRoute?.botId,
3114
+ normalizedRoute?.serverBotID,
3115
+ normalizedRoute?.server_bot_id,
3116
+ ]);
3117
+ const actorBotName = firstNonEmptyString([
3118
+ normalizedRoute?.botName,
3119
+ normalizedRoute?.bot_name,
3120
+ normalizedRoute?.serverBotName,
3121
+ normalizedRoute?.server_bot_name,
3122
+ ]);
3123
+ const title = buildRunnerRootWorkItemTitle({
3124
+ selectedRecord,
3125
+ request: existing,
3126
+ });
3127
+ const description = buildRunnerRootWorkItemDescription({
3128
+ normalizedRoute,
3129
+ routeKey,
3130
+ selectedRecord,
3131
+ request: existing,
3132
+ });
3133
+ const created = safeObject(await createProjectWorkItem({
3134
+ siteBaseURL: runtime.baseURL,
3135
+ token: runtime.token,
3136
+ timeoutSeconds: runtime.timeoutSeconds,
3137
+ actorUserID: runtime.actor.user_id,
3138
+ actorBotID: String(actorBotID || "").trim(),
3139
+ actorBotName: String(actorBotName || "").trim(),
3140
+ projectID: String(normalizedRoute?.projectID || "").trim(),
3141
+ title,
3142
+ description,
3143
+ }));
3144
+ const rootWorkItemID = String(created.id || created.work_item_id || created.workItemID || "").trim();
3145
+ if (!rootWorkItemID) {
3146
+ throw new Error("work item creation returned no id");
3147
+ }
3148
+ const rootWorkItemStatus = normalizeRunnerWorkItemStatus(created.status || "backlog") || "backlog";
3149
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3150
+ root_work_item_id: rootWorkItemID,
3151
+ root_work_item_title: String(created.title || title).trim() || title,
3152
+ root_work_item_status: rootWorkItemStatus,
3153
+ root_work_item_created_at: new Date().toISOString(),
3154
+ root_work_item_last_error: "",
3155
+ });
3156
+ saveBotRunnerState({
3157
+ routes: currentState.routes,
3158
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3159
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3160
+ requests: nextRequests,
3161
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3162
+ });
3163
+ const rootThreadClaim = await ensureRunnerRootThreadForRequest({
3164
+ normalizedRoute,
3165
+ routeKey,
3166
+ selectedRecord,
3167
+ runtime,
3168
+ requestKey: key,
3169
+ });
3170
+ return {
3171
+ ok: true,
3172
+ requestKey: key,
3173
+ request: safeObject(rootThreadClaim.request || request),
3174
+ created: true,
3175
+ root_thread_created: rootThreadClaim.created === true,
3176
+ root_thread_error: String(rootThreadClaim.error || "").trim(),
3177
+ };
3178
+ } catch (err) {
3179
+ const errorText = String(err?.message || err).trim() || "failed to create root work item";
3180
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3181
+ root_work_item_last_error: errorText,
3182
+ });
3183
+ saveBotRunnerState({
3184
+ routes: currentState.routes,
3185
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3186
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3187
+ requests: nextRequests,
3188
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3189
+ });
3190
+ return {
3191
+ ok: false,
3192
+ reason: "root_work_item_create_failed",
3193
+ requestKey: key,
3194
+ request,
3195
+ error: errorText,
3196
+ };
3197
+ }
3198
+ }
3199
+
3200
+ function deriveRunnerRootWorkItemTargetStatus(rawRequestStatus) {
3201
+ const status = normalizeRunnerRequestStatus(rawRequestStatus);
3202
+ if (status === "running") {
3203
+ return "doing";
3204
+ }
3205
+ if (status === "completed" || status === "loop_closed") {
3206
+ return "done";
3207
+ }
3208
+ if (status === "closed" || status === "expired") {
3209
+ return "canceled";
3210
+ }
3211
+ return "";
3212
+ }
3213
+
3214
+ function buildRunnerRootWorkItemTransitionPath(currentStatusRaw, targetStatusRaw) {
3215
+ const currentStatus = normalizeRunnerWorkItemStatus(currentStatusRaw) || "backlog";
3216
+ const targetStatus = normalizeRunnerWorkItemStatus(targetStatusRaw);
3217
+ if (!targetStatus || currentStatus === targetStatus) {
3218
+ return [];
3219
+ }
3220
+ if (targetStatus === "doing") {
3221
+ return currentStatus === "backlog" ? ["doing"] : [];
3222
+ }
3223
+ if (targetStatus === "done") {
3224
+ if (currentStatus === "backlog") return ["doing", "review", "done"];
3225
+ if (currentStatus === "doing") return ["review", "done"];
3226
+ if (currentStatus === "review") return ["done"];
3227
+ return [];
3228
+ }
3229
+ if (targetStatus === "canceled") {
3230
+ if (currentStatus === "backlog" || currentStatus === "doing" || currentStatus === "review") {
3231
+ return ["canceled"];
3232
+ }
3233
+ }
3234
+ return [];
3235
+ }
3236
+
3237
+ async function syncRunnerRequestRootWorkItemForOutcome({
3238
+ normalizedRoute,
3239
+ runtime,
3240
+ requestKey,
3241
+ }) {
3242
+ const key = String(requestKey || "").trim();
3243
+ if (!key) {
3244
+ return {
3245
+ ok: false,
3246
+ reason: "request_key_missing",
3247
+ };
3248
+ }
3249
+ const currentState = loadBotRunnerState();
3250
+ const request = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3251
+ const rootWorkItemID = String(request.root_work_item_id || "").trim();
3252
+ if (!rootWorkItemID) {
3253
+ return {
3254
+ ok: true,
3255
+ skipped: true,
3256
+ reason: "root_work_item_missing",
3257
+ request,
3258
+ };
3259
+ }
3260
+ const targetStatus = deriveRunnerRootWorkItemTargetStatus(request.status);
3261
+ if (!targetStatus || !runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
3262
+ return {
3263
+ ok: true,
3264
+ skipped: true,
3265
+ reason: !targetStatus ? "no_target_status" : "governance_runtime_unavailable",
3266
+ request,
3267
+ };
3268
+ }
3269
+ let currentStatus = normalizeRunnerWorkItemStatus(request.root_work_item_status) || "backlog";
3270
+ const transitions = buildRunnerRootWorkItemTransitionPath(currentStatus, targetStatus);
3271
+ let lastError = "";
3272
+ const actorBotID = firstNonEmptyString([
3273
+ normalizedRoute?.botID,
3274
+ normalizedRoute?.botId,
3275
+ normalizedRoute?.serverBotID,
3276
+ normalizedRoute?.server_bot_id,
3277
+ ]);
3278
+ const actorBotName = firstNonEmptyString([
3279
+ normalizedRoute?.botName,
3280
+ normalizedRoute?.bot_name,
3281
+ normalizedRoute?.serverBotName,
3282
+ normalizedRoute?.server_bot_name,
3283
+ ]);
3284
+ for (const nextStatus of transitions) {
3285
+ try {
3286
+ await transitionProjectWorkItem({
3287
+ siteBaseURL: runtime.baseURL,
3288
+ token: runtime.token,
3289
+ timeoutSeconds: runtime.timeoutSeconds,
3290
+ actorUserID: runtime.actor.user_id,
3291
+ actorBotID: String(actorBotID || "").trim(),
3292
+ actorBotName: String(actorBotName || "").trim(),
3293
+ workItemID: rootWorkItemID,
3294
+ status: nextStatus,
3295
+ });
3296
+ currentStatus = nextStatus;
3297
+ } catch (err) {
3298
+ lastError = String(err?.message || err).trim() || `failed to transition work item to ${nextStatus}`;
3299
+ break;
3300
+ }
3301
+ }
3302
+ const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, key, {
3303
+ root_work_item_status: currentStatus,
3304
+ root_work_item_last_error: lastError,
3305
+ });
3306
+ saveBotRunnerState({
3307
+ routes: currentState.routes,
3308
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3309
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3310
+ requests: nextRequests,
3311
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3312
+ });
3313
+ return {
3314
+ ok: lastError === "",
3315
+ request: nextRequest,
3316
+ transitioned: transitions.length > 0 && lastError === "",
3317
+ error: lastError,
3318
+ };
3319
+ }
3320
+
2594
3321
  function resolveRunnerContinuationRequestForBotReply({
2595
3322
  normalizedRoute,
2596
3323
  routeKey,
@@ -2918,6 +3645,30 @@ function runnerLedgerEntryMatchesProject(entryRaw, normalizedRoute, requestIndex
2918
3645
  );
2919
3646
  }
2920
3647
 
3648
+ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
3649
+ const localEntry = safeObject(localEntryRaw);
3650
+ const serverEntry = safeObject(serverEntryRaw);
3651
+ const merged = {
3652
+ ...localEntry,
3653
+ ...serverEntry,
3654
+ };
3655
+ const preserveLocalStringWhenServerBlank = (fieldName) => {
3656
+ const serverValue = String(serverEntry[fieldName] || "").trim();
3657
+ const localValue = String(localEntry[fieldName] || "").trim();
3658
+ if (!serverValue && localValue) {
3659
+ merged[fieldName] = localValue;
3660
+ }
3661
+ };
3662
+ preserveLocalStringWhenServerBlank("conversation_id");
3663
+ preserveLocalStringWhenServerBlank("root_work_item_id");
3664
+ preserveLocalStringWhenServerBlank("root_work_item_title");
3665
+ preserveLocalStringWhenServerBlank("root_work_item_status");
3666
+ preserveLocalStringWhenServerBlank("root_thread_id");
3667
+ preserveLocalStringWhenServerBlank("root_work_item_created_at");
3668
+ preserveLocalStringWhenServerBlank("root_work_item_last_error");
3669
+ return merged;
3670
+ }
3671
+
2921
3672
  function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRoute, serverRequests = [], serverCommentStates = []) {
2922
3673
  const state = safeObject(currentState);
2923
3674
  const normalizedRequests = normalizeBotRunnerRequests(state.requests);
@@ -2941,7 +3692,12 @@ function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRo
2941
3692
  const request = safeObject(requestRaw);
2942
3693
  const requestKey = String(request.request_key || request.requestKey || "").trim();
2943
3694
  if (!requestKey) continue;
2944
- nextRequests[requestKey] = normalizeBotRunnerRequests({ [requestKey]: request })[requestKey];
3695
+ const localRequest = safeObject(normalizedRequests[requestKey]);
3696
+ nextRequests[requestKey] = normalizeBotRunnerRequests({
3697
+ [requestKey]: {
3698
+ ...mergeRunnerRequestForServerHydration(localRequest, request),
3699
+ },
3700
+ })[requestKey];
2945
3701
  }
2946
3702
 
2947
3703
  const requestIndex = normalizeBotRunnerRequests(nextRequests);
@@ -4925,6 +5681,10 @@ async function createProjectWorkItem(params) {
4925
5681
  return createProjectWorkItemImpl(params, buildRunnerDataDeps());
4926
5682
  }
4927
5683
 
5684
+ async function transitionProjectWorkItem(params) {
5685
+ return transitionProjectWorkItemImpl(params, buildRunnerDataDeps());
5686
+ }
5687
+
4928
5688
  async function createProjectEvidence(params) {
4929
5689
  return createProjectEvidenceImpl(params, buildRunnerDataDeps());
4930
5690
  }
@@ -4933,6 +5693,10 @@ async function createWorkItemThread(params) {
4933
5693
  return createWorkItemThreadImpl(params, buildRunnerDataDeps());
4934
5694
  }
4935
5695
 
5696
+ async function listWorkItemThreads(params) {
5697
+ return listWorkItemThreadsImpl(params, buildRunnerDataDeps());
5698
+ }
5699
+
4936
5700
  async function linkWorkItemEvidence(params) {
4937
5701
  return linkWorkItemEvidenceImpl(params, buildRunnerDataDeps());
4938
5702
  }
@@ -5241,6 +6005,14 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
5241
6005
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
5242
6006
  .map((value) => normalizeTelegramMentionUsername(value))
5243
6007
  .filter(Boolean),
6008
+ root_work_item: String(entry.root_work_item_id || "").trim()
6009
+ ? {
6010
+ id: String(entry.root_work_item_id || "").trim(),
6011
+ title: String(entry.root_work_item_title || "").trim(),
6012
+ status: normalizeRunnerWorkItemStatus(entry.root_work_item_status),
6013
+ thread_id: String(entry.root_thread_id || "").trim(),
6014
+ }
6015
+ : null,
5244
6016
  };
5245
6017
  }
5246
6018
 
@@ -5299,7 +6071,7 @@ function pickPreferredStatusLookupRequest(entries = []) {
5299
6071
  return candidates[0];
5300
6072
  }
5301
6073
 
5302
- function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
6074
+ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runnerStateOverride = null }) {
5303
6075
  const parsed = safeObject(selectedRecord?.parsedArchive);
5304
6076
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5305
6077
  const currentChatID = String(parsed.chatID || parsed.chatId || "").trim();
@@ -5318,18 +6090,23 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5318
6090
  && activeSourceMessageID > 0
5319
6091
  && currentMessageID === activeSourceMessageID
5320
6092
  );
5321
- let runnerState = { requests: {} };
5322
- try {
5323
- runnerState = loadBotRunnerState();
5324
- } catch {}
6093
+ let runnerState = safeObject(runnerStateOverride);
6094
+ if (!Object.keys(runnerState).length) {
6095
+ runnerState = { requests: {} };
6096
+ try {
6097
+ runnerState = loadBotRunnerState();
6098
+ } catch {}
6099
+ }
5325
6100
  const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
5326
6101
  const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
6102
+ const activeRequestKey = String(safeObject(routeState).active_request_key || "").trim();
5327
6103
  const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
5328
6104
  entry,
5329
6105
  routeKey,
5330
6106
  selfBotUsername,
5331
6107
  currentMessageID,
5332
6108
  );
6109
+ const referencedRequestCandidate = safeObject(replyChainContext.referencedRequest);
5333
6110
  let relatedActiveRequest = null;
5334
6111
  let relatedRequest = null;
5335
6112
  const selectors = currentConversationID
@@ -5339,20 +6116,43 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5339
6116
  if (!scopedRequests.length && currentConversationID) {
5340
6117
  scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
5341
6118
  }
6119
+ if (!scopedRequests.length && activeRequestKey) {
6120
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
6121
+ }
5342
6122
  const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
5343
- relatedActiveRequest = eligibleScopedRequests
6123
+ const statusLookupCandidates = [...eligibleScopedRequests];
6124
+ if (
6125
+ Object.keys(referencedRequestCandidate).length > 0
6126
+ && requestMatchesCurrentRoute(referencedRequestCandidate)
6127
+ && !statusLookupCandidates.some(
6128
+ (entry) => String(safeObject(entry).request_key || "").trim() === String(referencedRequestCandidate.request_key || "").trim(),
6129
+ )
6130
+ ) {
6131
+ statusLookupCandidates.push(referencedRequestCandidate);
6132
+ }
6133
+ relatedActiveRequest = statusLookupCandidates
5344
6134
  .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
5345
- relatedRequest = pickPreferredStatusLookupRequest(
5346
- eligibleScopedRequests.length
5347
- ? eligibleScopedRequests
5348
- : (replyChainContext.referencedRequest ? [replyChainContext.referencedRequest] : []).filter(requestMatchesCurrentRoute),
5349
- );
6135
+ relatedRequest = pickPreferredStatusLookupRequest(statusLookupCandidates);
5350
6136
  const lastAction = String(safeObject(routeState).last_action || "").trim();
5351
6137
  const lastReason = String(safeObject(routeState).last_reason || "").trim();
5352
6138
  const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
5353
6139
  const routeConversationID = String(safeObject(routeState).last_conversation_id || "").trim();
6140
+ const activeRootWorkItemID = String(safeObject(routeState).active_root_work_item_id || "").trim();
6141
+ const activeRootWorkItemTitle = String(safeObject(routeState).active_root_work_item_title || "").trim();
6142
+ const activeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).active_root_work_item_status);
6143
+ const routeRootWorkItemID = String(safeObject(routeState).last_root_work_item_id || "").trim();
6144
+ const routeRootWorkItemTitle = String(safeObject(routeState).last_root_work_item_title || "").trim();
6145
+ const routeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).last_root_work_item_status);
5354
6146
  const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
5355
6147
  const routeWorkItemTitles = ensureArray(safeObject(routeState).last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
6148
+ const requestRootWorkItem = String(safeObject(relatedRequest).root_work_item_id || "").trim()
6149
+ ? {
6150
+ id: String(safeObject(relatedRequest).root_work_item_id || "").trim(),
6151
+ title: String(safeObject(relatedRequest).root_work_item_title || "").trim(),
6152
+ status: normalizeRunnerWorkItemStatus(safeObject(relatedRequest).root_work_item_status),
6153
+ thread_id: String(safeObject(relatedRequest).root_thread_id || "").trim(),
6154
+ }
6155
+ : null;
5356
6156
  return {
5357
6157
  kind: "runner_status",
5358
6158
  status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
@@ -5376,6 +6176,21 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5376
6176
  : null,
5377
6177
  related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
5378
6178
  related_request: relatedRequest ? summarizeRunnerRequestForStatusLookup(relatedRequest) : null,
6179
+ root_work_item: String(safeObject(requestRootWorkItem).id || "").trim()
6180
+ ? requestRootWorkItem
6181
+ : activeRootWorkItemID
6182
+ ? {
6183
+ id: activeRootWorkItemID,
6184
+ title: activeRootWorkItemTitle,
6185
+ status: activeRootWorkItemStatus,
6186
+ }
6187
+ : currentConversationID && routeConversationID === currentConversationID && routeRootWorkItemID
6188
+ ? {
6189
+ id: routeRootWorkItemID,
6190
+ title: routeRootWorkItemTitle,
6191
+ status: routeRootWorkItemStatus,
6192
+ }
6193
+ : null,
5379
6194
  route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
5380
6195
  ? {
5381
6196
  ids: routeWorkItemIDs,
@@ -5442,6 +6257,13 @@ async function resolveInformationalQueryReply({
5442
6257
  };
5443
6258
  }
5444
6259
  if (normalizedIntentType === "status_query") {
6260
+ let hydratedRunnerState = null;
6261
+ try {
6262
+ hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
6263
+ normalizedRoute: route,
6264
+ runtime,
6265
+ });
6266
+ } catch {}
5445
6267
  return {
5446
6268
  handled: true,
5447
6269
  source: "runner.status",
@@ -5451,6 +6273,7 @@ async function resolveInformationalQueryReply({
5451
6273
  route,
5452
6274
  routeState,
5453
6275
  selectedRecord,
6276
+ runnerStateOverride: hydratedRunnerState,
5454
6277
  }),
5455
6278
  };
5456
6279
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
@@ -5591,14 +6414,18 @@ function emptyRunnerActiveExecutionPatch() {
5591
6414
  active_request_key: "",
5592
6415
  active_started_at: "",
5593
6416
  active_heartbeat_at: "",
6417
+ active_root_work_item_id: "",
6418
+ active_root_work_item_title: "",
6419
+ active_root_work_item_status: "",
5594
6420
  active_runner_pid: undefined,
5595
6421
  active_execution_token: "",
5596
6422
  };
5597
6423
  }
5598
6424
 
5599
- function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
6425
+ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "", rootWorkItem = {}) {
5600
6426
  const nowISO = new Date().toISOString();
5601
6427
  const parsed = safeObject(selectedRecord?.parsedArchive);
6428
+ const root = safeObject(rootWorkItem);
5602
6429
  return {
5603
6430
  active_comment_id: String(selectedRecord?.id || "").trim(),
5604
6431
  active_comment_created_at: firstNonEmptyString([selectedRecord?.createdAt, selectedRecord?.updatedAt]),
@@ -5606,6 +6433,9 @@ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
5606
6433
  active_request_key: String(requestKey || "").trim(),
5607
6434
  active_started_at: nowISO,
5608
6435
  active_heartbeat_at: nowISO,
6436
+ active_root_work_item_id: String(root.id || root.root_work_item_id || "").trim(),
6437
+ active_root_work_item_title: String(root.title || root.root_work_item_title || "").trim(),
6438
+ active_root_work_item_status: normalizeRunnerWorkItemStatus(root.status || root.root_work_item_status),
5609
6439
  active_runner_pid: process.pid,
5610
6440
  active_execution_token: `${Date.now()}-${process.pid}-${String(selectedRecord?.id || "").trim()}`,
5611
6441
  };
@@ -6085,7 +6915,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6085
6915
  }
6086
6916
  return null;
6087
6917
  };
6088
- const prepareRunnerRequestClaim = (selectedRecord, selectedResponderSelectors = []) => {
6918
+ const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = []) => {
6089
6919
  const parsed = safeObject(selectedRecord?.parsedArchive);
6090
6920
  const kind = String(parsed.kind || "").trim().toLowerCase();
6091
6921
  if (kind === "bot_reply") {
@@ -6115,6 +6945,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6115
6945
  routeKey,
6116
6946
  selectedRecord,
6117
6947
  selectedBotUsernames: selectedResponderSelectors,
6948
+ runtime,
6118
6949
  });
6119
6950
  };
6120
6951
  if (deferExecution) {
@@ -6201,7 +7032,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6201
7032
  });
6202
7033
  continue;
6203
7034
  }
6204
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
7035
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
6205
7036
  if (!requestClaim.ok) {
6206
7037
  await syncRunnerRequestLedgerForProjectToServer({
6207
7038
  normalizedRoute,
@@ -6226,9 +7057,64 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6226
7057
  });
6227
7058
  continue;
6228
7059
  }
7060
+ const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
7061
+ normalizedRoute,
7062
+ selectedRecord,
7063
+ runtime,
7064
+ requestKey: requestClaim.requestKey,
7065
+ });
7066
+ const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
7067
+ normalizedRoute,
7068
+ routeKey,
7069
+ selectedRecord,
7070
+ runtime,
7071
+ requestKey: requestClaim.requestKey,
7072
+ });
7073
+ if (!rootWorkItemClaim.ok) {
7074
+ if (String(requestClaim.requestKey || "").trim()) {
7075
+ markRunnerRequestLifecycle({
7076
+ normalizedRoute,
7077
+ requestKey: requestClaim.requestKey,
7078
+ selectedRecord,
7079
+ routeKey,
7080
+ outcome: "closed",
7081
+ closedReason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim(),
7082
+ });
7083
+ await syncRunnerRequestLedgerForProjectToServer({
7084
+ normalizedRoute,
7085
+ runtime,
7086
+ });
7087
+ }
7088
+ saveRunnerRouteState(
7089
+ routeKey,
7090
+ buildRunnerRouteStateFromComment(selectedRecord, {
7091
+ last_action: "request_skipped",
7092
+ last_reason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim() || "root_work_item_create_failed",
7093
+ last_trigger: "work_item_root",
7094
+ last_request_key: String(requestClaim.requestKey || "").trim(),
7095
+ }),
7096
+ );
7097
+ skippedRecords.push({
7098
+ id: selectedRecord.id,
7099
+ reason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim() || "root_work_item_create_failed",
7100
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
7101
+ diagnosticType: "skip",
7102
+ action: "skip_missing_root_work_item",
7103
+ closedReason: String(rootWorkItemClaim.reason || "").trim(),
7104
+ });
7105
+ continue;
7106
+ }
7107
+ const claimedRequest = safeObject(rootWorkItemClaim.request || inheritedRootReference.request || requestClaim.request);
6229
7108
  saveRunnerRouteState(routeKey, {
6230
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey),
7109
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7110
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7111
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7112
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7113
+ }),
6231
7114
  last_request_key: String(requestClaim.requestKey || "").trim(),
7115
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
7116
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
7117
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
6232
7118
  });
6233
7119
  await syncRunnerRequestLedgerForProjectToServer({
6234
7120
  normalizedRoute,
@@ -6375,7 +7261,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6375
7261
  continue;
6376
7262
  }
6377
7263
  const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
6378
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
7264
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
6379
7265
  if (!requestClaim.ok) {
6380
7266
  await syncRunnerRequestLedgerForProjectToServer({
6381
7267
  normalizedRoute,
@@ -6400,11 +7286,67 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6400
7286
  });
6401
7287
  continue;
6402
7288
  }
7289
+ const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
7290
+ normalizedRoute,
7291
+ selectedRecord,
7292
+ runtime,
7293
+ requestKey: requestClaim.requestKey,
7294
+ });
7295
+ const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
7296
+ normalizedRoute,
7297
+ routeKey,
7298
+ selectedRecord,
7299
+ runtime,
7300
+ requestKey: requestClaim.requestKey,
7301
+ });
7302
+ if (!rootWorkItemClaim.ok) {
7303
+ if (String(requestClaim.requestKey || "").trim()) {
7304
+ markRunnerRequestLifecycle({
7305
+ normalizedRoute,
7306
+ requestKey: requestClaim.requestKey,
7307
+ selectedRecord,
7308
+ routeKey,
7309
+ outcome: "closed",
7310
+ closedReason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim(),
7311
+ });
7312
+ await syncRunnerRequestLedgerForProjectToServer({
7313
+ normalizedRoute,
7314
+ runtime,
7315
+ });
7316
+ }
7317
+ saveRunnerRouteState(
7318
+ routeKey,
7319
+ buildRunnerRouteStateFromComment(selectedRecord, {
7320
+ last_action: "request_skipped",
7321
+ last_reason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim() || "root_work_item_create_failed",
7322
+ last_trigger: "work_item_root",
7323
+ last_request_key: String(requestClaim.requestKey || "").trim(),
7324
+ }),
7325
+ );
7326
+ skippedRecords.push({
7327
+ id: selectedRecord.id,
7328
+ reason: String(rootWorkItemClaim.error || rootWorkItemClaim.reason || "root_work_item_create_failed").trim() || "root_work_item_create_failed",
7329
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
7330
+ diagnosticType: "skip",
7331
+ action: "skip_missing_root_work_item",
7332
+ closedReason: String(rootWorkItemClaim.reason || "").trim(),
7333
+ });
7334
+ continue;
7335
+ }
7336
+ const claimedRequest = safeObject(rootWorkItemClaim.request || inheritedRootReference.request || requestClaim.request);
6403
7337
  saveRunnerRouteState(routeKey, {
6404
- active_request_key: String(requestClaim.requestKey || "").trim(),
7338
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7339
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7340
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7341
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7342
+ }),
6405
7343
  last_request_key: String(requestClaim.requestKey || "").trim(),
7344
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
7345
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
7346
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
6406
7347
  });
6407
7348
  if (String(requestClaim.requestKey || "").trim()) {
7349
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6408
7350
  markRunnerRequestLifecycle({
6409
7351
  normalizedRoute,
6410
7352
  requestKey: requestClaim.requestKey,
@@ -6412,6 +7354,22 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6412
7354
  routeKey,
6413
7355
  outcome: "running",
6414
7356
  });
7357
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7358
+ normalizedRoute,
7359
+ runtime,
7360
+ requestKey: requestClaim.requestKey,
7361
+ });
7362
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7363
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7364
+ saveRunnerRouteState(routeKey, {
7365
+ active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7366
+ active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7367
+ active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7368
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7369
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7370
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7371
+ });
7372
+ }
6415
7373
  await syncRunnerRequestLedgerForProjectToServer({
6416
7374
  normalizedRoute,
6417
7375
  runtime,
@@ -6422,7 +7380,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6422
7380
  normalizedRoute,
6423
7381
  routeState: {
6424
7382
  ...currentRouteState,
6425
- active_request_key: String(requestClaim.requestKey || "").trim(),
7383
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7384
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7385
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7386
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7387
+ }),
6426
7388
  },
6427
7389
  selectedRecord,
6428
7390
  pendingOrdered: pending.ordered,
@@ -6457,6 +7419,19 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6457
7419
  outcome: "skipped",
6458
7420
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
6459
7421
  });
7422
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7423
+ normalizedRoute,
7424
+ runtime,
7425
+ requestKey: requestClaim.requestKey,
7426
+ });
7427
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7428
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7429
+ saveRunnerRouteState(routeKey, {
7430
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7431
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7432
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7433
+ });
7434
+ }
6460
7435
  await syncRunnerRequestLedgerForProjectToServer({
6461
7436
  normalizedRoute,
6462
7437
  runtime,
@@ -6466,6 +7441,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6466
7441
  continue;
6467
7442
  }
6468
7443
  if (String(requestClaim.requestKey || "").trim()) {
7444
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6469
7445
  markRunnerRequestLifecycle({
6470
7446
  normalizedRoute,
6471
7447
  requestKey: requestClaim.requestKey,
@@ -6479,8 +7455,28 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6479
7455
  nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
6480
7456
  currentBotSelector,
6481
7457
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
6482
- normalizedIntent: String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim(),
7458
+ normalizedIntent: resolvedIntentType,
7459
+ });
7460
+ await ensureRunnerRootWorkItemForRequest({
7461
+ normalizedRoute,
7462
+ routeKey,
7463
+ selectedRecord,
7464
+ runtime,
7465
+ requestKey: requestClaim.requestKey,
7466
+ });
7467
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7468
+ normalizedRoute,
7469
+ runtime,
7470
+ requestKey: requestClaim.requestKey,
6483
7471
  });
7472
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7473
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7474
+ saveRunnerRouteState(routeKey, {
7475
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7476
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7477
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7478
+ });
7479
+ }
6484
7480
  await syncRunnerRequestLedgerForProjectToServer({
6485
7481
  normalizedRoute,
6486
7482
  runtime,
@@ -8172,7 +9168,7 @@ async function runRunnerStop(flags) {
8172
9168
  }
8173
9169
  process.stdout.write("Detached runner stop: OK\n");
8174
9170
  process.stdout.write(`registry_file: ${payload.registry_file}\n`);
8175
- for (const entry of stopped) {
9171
+ for (const entry of ensureArray(payload.stopped)) {
8176
9172
  process.stdout.write(`stopped: ${entry.launch_id} pid=${entry.pid} routes=${entry.route_names.join(", ") || "-"}\n`);
8177
9173
  }
8178
9174
  }
@@ -8197,7 +9193,7 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
8197
9193
  process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}\n`);
8198
9194
  return payload;
8199
9195
  }
8200
- const launch = launchDetachedRunnerProcess(flags, routes, sourceCommand);
9196
+ const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
8201
9197
  const nextLaunches = {
8202
9198
  ...safeObject(registry).launches,
8203
9199
  [launch.launch_id]: launch,
@@ -8724,6 +9720,22 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8724
9720
  routeKey: deferredExecution.routeKey,
8725
9721
  outcome: "running",
8726
9722
  });
9723
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9724
+ normalizedRoute: deferredExecution.normalizedRoute,
9725
+ runtime: deferredExecution.runtime,
9726
+ requestKey: deferredExecution.requestKey,
9727
+ });
9728
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9729
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9730
+ saveRunnerRouteState(deferredExecution.routeKey, {
9731
+ active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9732
+ active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9733
+ active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9734
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9735
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9736
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9737
+ });
9738
+ }
8727
9739
  await syncRunnerRequestLedgerForProjectToServer({
8728
9740
  normalizedRoute: deferredExecution.normalizedRoute,
8729
9741
  runtime: deferredExecution.runtime,
@@ -8786,6 +9798,19 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8786
9798
  outcome: "skipped",
8787
9799
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
8788
9800
  });
9801
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9802
+ normalizedRoute: deferredExecution.normalizedRoute,
9803
+ runtime: deferredExecution.runtime,
9804
+ requestKey: deferredExecution.requestKey,
9805
+ });
9806
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9807
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9808
+ saveRunnerRouteState(deferredExecution.routeKey, {
9809
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9810
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9811
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9812
+ });
9813
+ }
8789
9814
  await syncRunnerRequestLedgerForProjectToServer({
8790
9815
  normalizedRoute: deferredExecution.normalizedRoute,
8791
9816
  runtime: deferredExecution.runtime,
@@ -8806,6 +9831,9 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8806
9831
  };
8807
9832
  }
8808
9833
  if (String(deferredExecution.requestKey || "").trim()) {
9834
+ const resolvedIntentType = String(
9835
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
9836
+ ).trim();
8809
9837
  markRunnerRequestLifecycle({
8810
9838
  normalizedRoute: deferredExecution.normalizedRoute,
8811
9839
  requestKey: deferredExecution.requestKey,
@@ -8821,8 +9849,28 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8821
9849
  deferredExecution.bot?.username || deferredExecution.bot?.name,
8822
9850
  ),
8823
9851
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8824
- normalizedIntent: String(safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "").trim(),
9852
+ normalizedIntent: resolvedIntentType,
8825
9853
  });
9854
+ await ensureRunnerRootWorkItemForRequest({
9855
+ normalizedRoute: deferredExecution.normalizedRoute,
9856
+ routeKey: deferredExecution.routeKey,
9857
+ selectedRecord: deferredExecution.selectedRecord,
9858
+ runtime: deferredExecution.runtime,
9859
+ requestKey: deferredExecution.requestKey,
9860
+ });
9861
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9862
+ normalizedRoute: deferredExecution.normalizedRoute,
9863
+ runtime: deferredExecution.runtime,
9864
+ requestKey: deferredExecution.requestKey,
9865
+ });
9866
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9867
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9868
+ saveRunnerRouteState(deferredExecution.routeKey, {
9869
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9870
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9871
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9872
+ });
9873
+ }
8826
9874
  await syncRunnerRequestLedgerForProjectToServer({
8827
9875
  normalizedRoute: deferredExecution.normalizedRoute,
8828
9876
  runtime: deferredExecution.runtime,
@@ -8851,6 +9899,19 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8851
9899
  outcome: "error",
8852
9900
  closedReason: errorText || "execution_error",
8853
9901
  });
9902
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9903
+ normalizedRoute: deferredExecution.normalizedRoute,
9904
+ runtime: deferredExecution.runtime,
9905
+ requestKey: deferredExecution.requestKey,
9906
+ });
9907
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9908
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9909
+ saveRunnerRouteState(deferredExecution.routeKey, {
9910
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9911
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9912
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9913
+ });
9914
+ }
8854
9915
  await syncRunnerRequestLedgerForProjectToServer({
8855
9916
  normalizedRoute: deferredExecution.normalizedRoute,
8856
9917
  runtime: deferredExecution.runtime,
@@ -13130,6 +14191,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
13130
14191
  runnerRouteLogicalSignature,
13131
14192
  loadBotRunnerState,
13132
14193
  saveBotRunnerState,
14194
+ mergeServerRunnerRequestLedgerIntoLocalState,
13133
14195
  buildRunnerStatusQueryLookup,
13134
14196
  tryJsonParse,
13135
14197
  safeObject,