metheus-governance-mcp-cli 0.2.197 → 0.2.199

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,
@@ -2363,6 +2386,66 @@ function buildRunnerRequestKey({
2363
2386
  ].join("::");
2364
2387
  }
2365
2388
 
2389
+ function looksLikeRunnerClaimQuestion(rawText) {
2390
+ return /[??]/.test(String(rawText || ""));
2391
+ }
2392
+
2393
+ function inferRunnerRequestClaimIntent(selectedRecord) {
2394
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2395
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
2396
+ if (!isInboundArchiveKind(commentKind)) {
2397
+ return "";
2398
+ }
2399
+ const rawText = String(parsed.body || "").trim();
2400
+ const normalizedText = rawText.toLowerCase();
2401
+ const replyToMessageID = intFromRawAllowZero(parsed.replyToMessageID, 0);
2402
+ if (!rawText) {
2403
+ return "";
2404
+ }
2405
+ if (/^(hi|hello|hey|thanks|thank you|good morning|good afternoon|good evening)\b/.test(normalizedText)) {
2406
+ return "small_talk";
2407
+ }
2408
+ if (/\b(bot role|your role|what do you do|who are you|which bot|who should respond)\b/.test(normalizedText)) {
2409
+ return "bot_role_query";
2410
+ }
2411
+ if (/\b(workspace|working directory|workdir|project folder|local folder)\b/.test(normalizedText)) {
2412
+ return "workspace_query";
2413
+ }
2414
+ if (
2415
+ (/\b(where|path|locate|find)\b/.test(normalizedText) || /\.[a-z0-9]{1,8}\b/.test(normalizedText))
2416
+ && /\b(file|folder|path|doc|guide|readme|workspace)\b/.test(normalizedText)
2417
+ ) {
2418
+ return "artifact_location_query";
2419
+ }
2420
+ if (/\b(why|explain|what is|what does|how does|describe)\b/.test(normalizedText) && looksLikeRunnerClaimQuestion(rawText)) {
2421
+ return "explanation_query";
2422
+ }
2423
+ if (
2424
+ /\b(status|progress|done|finished|complete|completed|working on|current work|did you|handled|handle it|what are you working|check status)\b/
2425
+ .test(normalizedText)
2426
+ ) {
2427
+ return "status_query";
2428
+ }
2429
+ if (replyToMessageID > 0 && looksLikeRunnerClaimQuestion(rawText)) {
2430
+ return "status_query";
2431
+ }
2432
+ if (looksLikeRunnerClaimQuestion(rawText) && normalizedText.split(/\s+/).filter(Boolean).length <= 8) {
2433
+ return "status_query";
2434
+ }
2435
+ return "general_execution";
2436
+ }
2437
+
2438
+ function resolveRunnerRequestClaimIntent({
2439
+ normalizedIntent = "",
2440
+ selectedRecord,
2441
+ }) {
2442
+ const explicitIntent = String(normalizedIntent || "").trim().toLowerCase();
2443
+ if (explicitIntent) {
2444
+ return explicitIntent;
2445
+ }
2446
+ return inferRunnerRequestClaimIntent(selectedRecord);
2447
+ }
2448
+
2366
2449
  function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID) {
2367
2450
  const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
2368
2451
  const normalizedChatID = String(chatID || "").trim() || "-";
@@ -2409,6 +2492,67 @@ function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {})
2409
2492
  ));
2410
2493
  }
2411
2494
 
2495
+ function sortRunnerRequestEntriesNewestFirst(entries = []) {
2496
+ return ensureArray(entries).slice().sort((leftRaw, rightRaw) => {
2497
+ const left = safeObject(leftRaw);
2498
+ const right = safeObject(rightRaw);
2499
+ const leftTime = firstNonEmptyString([left.updated_at, left.completed_at, left.closed_at, left.claimed_at]);
2500
+ const rightTime = firstNonEmptyString([right.updated_at, right.completed_at, right.closed_at, right.claimed_at]);
2501
+ if (leftTime && rightTime && leftTime !== rightTime) {
2502
+ return leftTime < rightTime ? 1 : -1;
2503
+ }
2504
+ return String(left.request_key || "").localeCompare(String(right.request_key || ""));
2505
+ });
2506
+ }
2507
+
2508
+ async function findServerRunnerRequestForMessageID({
2509
+ normalizedRoute,
2510
+ runtime,
2511
+ chatID,
2512
+ messageID,
2513
+ }) {
2514
+ const projectID = String(normalizedRoute?.projectID || "").trim();
2515
+ const provider = String(normalizedRoute?.provider || "").trim();
2516
+ const normalizedChatID = String(chatID || "").trim();
2517
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2518
+ if (
2519
+ !projectID
2520
+ || !provider
2521
+ || !normalizedChatID
2522
+ || normalizedMessageID <= 0
2523
+ || !runtime?.baseURL
2524
+ || !runtime?.token
2525
+ ) {
2526
+ return null;
2527
+ }
2528
+ try {
2529
+ const serverRequests = await listProjectRunnerRequests({
2530
+ siteBaseURL: runtime.baseURL,
2531
+ projectID,
2532
+ token: runtime.token,
2533
+ timeoutSeconds: runtime.timeoutSeconds,
2534
+ actorUserID: runtime.actor?.user_id,
2535
+ limit: 500,
2536
+ offset: 0,
2537
+ });
2538
+ const matched = sortRunnerRequestEntriesNewestFirst(serverRequests.filter((entryRaw) => {
2539
+ const entry = safeObject(entryRaw);
2540
+ return (
2541
+ String(entry.project_id || "").trim() === projectID
2542
+ && String(entry.provider || "").trim() === provider
2543
+ && String(entry.chat_id || "").trim() === normalizedChatID
2544
+ && (
2545
+ intFromRawAllowZero(entry.source_message_id, 0) === normalizedMessageID
2546
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === normalizedMessageID
2547
+ )
2548
+ );
2549
+ }));
2550
+ return safeObject(matched[0]);
2551
+ } catch {
2552
+ return null;
2553
+ }
2554
+ }
2555
+
2412
2556
  function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
2413
2557
  const parsed = safeObject(selectedRecord?.parsedArchive);
2414
2558
  const explicitConversationID = String(parsed.conversationID || "").trim();
@@ -2451,6 +2595,74 @@ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, sele
2451
2595
  };
2452
2596
  }
2453
2597
 
2598
+ async function resolveRunnerReplyChainConversationContextWithServerFallback({
2599
+ state,
2600
+ normalizedRoute,
2601
+ selectedRecord,
2602
+ runtime,
2603
+ }) {
2604
+ const initialState = safeObject(state);
2605
+ const initialContext = resolveRunnerReplyChainConversationContext(initialState, normalizedRoute, selectedRecord);
2606
+ if (safeObject(initialContext.referencedRequest).request_key) {
2607
+ return {
2608
+ state: initialState,
2609
+ replyChainContext: initialContext,
2610
+ hydrated: false,
2611
+ };
2612
+ }
2613
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2614
+ const replyToMessageID = intFromRawAllowZero(
2615
+ parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
2616
+ 0,
2617
+ );
2618
+ if (replyToMessageID <= 0 || !runtime?.baseURL || !runtime?.token) {
2619
+ return {
2620
+ state: initialState,
2621
+ replyChainContext: initialContext,
2622
+ hydrated: false,
2623
+ };
2624
+ }
2625
+ 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;
2636
+ return {
2637
+ state: {
2638
+ ...initialState,
2639
+ requests: requestIndex,
2640
+ },
2641
+ replyChainContext: {
2642
+ conversationID: String(serverReferencedRequest.conversation_id || "").trim()
2643
+ || buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID),
2644
+ replyToMessageID,
2645
+ anchorMessageID,
2646
+ reason: String(serverReferencedRequest.conversation_id || "").trim()
2647
+ ? "reply_request_conversation_server"
2648
+ : "reply_request_synthetic_server",
2649
+ referencedRequest: serverReferencedRequest,
2650
+ },
2651
+ hydrated: false,
2652
+ };
2653
+ }
2654
+ const hydratedState = await hydrateRunnerRequestLedgerFromServer({
2655
+ normalizedRoute,
2656
+ runtime,
2657
+ });
2658
+ const hydratedContext = resolveRunnerReplyChainConversationContext(hydratedState, normalizedRoute, selectedRecord);
2659
+ return {
2660
+ state: hydratedState,
2661
+ replyChainContext: hydratedContext,
2662
+ hydrated: true,
2663
+ };
2664
+ }
2665
+
2454
2666
  function upsertRunnerRequest(state, requestKey, patch = {}) {
2455
2667
  const currentState = safeObject(state);
2456
2668
  const requests = normalizeBotRunnerRequests(currentState.requests);
@@ -2490,104 +2702,699 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
2490
2702
  };
2491
2703
  }
2492
2704
 
2493
- function claimRunnerRequestForHumanComment({
2705
+ async function claimRunnerRequestForHumanComment({
2494
2706
  normalizedRoute,
2495
2707
  routeKey,
2496
2708
  selectedRecord,
2497
2709
  selectedBotUsernames = [],
2498
2710
  normalizedIntent = "",
2711
+ runtime = null,
2712
+ }) {
2713
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2714
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
2715
+ if (!isInboundArchiveKind(commentKind)) {
2716
+ return {
2717
+ ok: false,
2718
+ reason: "non_human_comment_cannot_create_request",
2719
+ };
2720
+ }
2721
+ const currentState = loadBotRunnerState();
2722
+ const replyChainResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
2723
+ state: currentState,
2724
+ normalizedRoute,
2725
+ selectedRecord,
2726
+ runtime,
2727
+ });
2728
+ const replyChainContext = safeObject(replyChainResolution.replyChainContext);
2729
+ const referencedRequest = safeObject(replyChainContext.referencedRequest);
2730
+ const conversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
2731
+ const resolvedNormalizedIntent = resolveRunnerRequestClaimIntent({
2732
+ normalizedIntent,
2733
+ selectedRecord,
2734
+ });
2735
+ const requestKey = buildRunnerRequestKey({
2736
+ normalizedRoute,
2737
+ selectedRecord,
2738
+ selectedBotUsernames,
2739
+ normalizedIntent: resolvedNormalizedIntent,
2740
+ });
2741
+ let stateForClaim = safeObject(replyChainResolution.state);
2742
+ if (
2743
+ Object.keys(referencedRequest).length > 0
2744
+ && conversationID
2745
+ && !String(referencedRequest.conversation_id || "").trim()
2746
+ && String(referencedRequest.request_key || "").trim()
2747
+ ) {
2748
+ const backfilled = upsertRunnerRequest(stateForClaim, referencedRequest.request_key, {
2749
+ conversation_id: conversationID,
2750
+ });
2751
+ stateForClaim = {
2752
+ ...stateForClaim,
2753
+ requests: backfilled.requests,
2754
+ };
2755
+ }
2756
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
2757
+ const existing = safeObject(requests[requestKey]);
2758
+ if (isFinalRunnerRequestStatus(existing.status)) {
2759
+ return {
2760
+ ok: false,
2761
+ reason: "request_already_finalized",
2762
+ requestKey,
2763
+ };
2764
+ }
2765
+ if (
2766
+ isActiveRunnerRequestStatus(existing.status)
2767
+ && String(existing.claimed_by_route || "").trim()
2768
+ && String(existing.claimed_by_route || "").trim() !== String(routeKey || "").trim()
2769
+ ) {
2770
+ return {
2771
+ ok: false,
2772
+ reason: "request_already_claimed",
2773
+ requestKey,
2774
+ };
2775
+ }
2776
+ const nowISO = new Date().toISOString();
2777
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
2778
+ project_id: String(normalizedRoute?.projectID || "").trim(),
2779
+ provider: String(normalizedRoute?.provider || "").trim(),
2780
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
2781
+ source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2782
+ root_comment_id: String(selectedRecord?.id || "").trim(),
2783
+ root_comment_kind: commentKind,
2784
+ conversation_id: conversationID,
2785
+ selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
2786
+ normalized_intent: resolvedNormalizedIntent,
2787
+ status: "claimed",
2788
+ claimed_by_route: String(routeKey || "").trim(),
2789
+ claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
2790
+ root_work_item_id: String(existing.root_work_item_id || referencedRequest.root_work_item_id || "").trim(),
2791
+ root_work_item_title: String(existing.root_work_item_title || referencedRequest.root_work_item_title || "").trim(),
2792
+ root_work_item_status: normalizeRunnerWorkItemStatus(
2793
+ existing.root_work_item_status || referencedRequest.root_work_item_status,
2794
+ ),
2795
+ root_thread_id: String(existing.root_thread_id || referencedRequest.root_thread_id || "").trim(),
2796
+ root_work_item_created_at: firstNonEmptyString([
2797
+ existing.root_work_item_created_at,
2798
+ referencedRequest.root_work_item_created_at,
2799
+ ]),
2800
+ root_work_item_last_error: String(existing.root_work_item_last_error || "").trim(),
2801
+ last_comment_id: String(selectedRecord?.id || "").trim(),
2802
+ last_comment_kind: commentKind,
2803
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2804
+ });
2805
+ const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
2806
+ project_id: String(normalizedRoute?.projectID || "").trim(),
2807
+ provider: String(normalizedRoute?.provider || "").trim(),
2808
+ request_key: requestKey,
2809
+ route_key: String(routeKey || "").trim(),
2810
+ conversation_id: conversationID,
2811
+ source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2812
+ comment_kind: commentKind,
2813
+ request_status: "claimed",
2814
+ });
2815
+ saveBotRunnerState({
2816
+ routes: stateForClaim.routes,
2817
+ sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
2818
+ excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
2819
+ requests: nextRequests,
2820
+ consumedComments: nextConsumedComments,
2821
+ });
2822
+ return {
2823
+ ok: true,
2824
+ requestKey,
2825
+ request,
2826
+ };
2827
+ }
2828
+
2829
+ function isActionableRunnerRequestIntent(rawIntent) {
2830
+ const normalizedIntent = String(rawIntent || "").trim().toLowerCase();
2831
+ return Boolean(normalizedIntent) && !isInformationalRunnerRequestIntent(normalizedIntent);
2832
+ }
2833
+
2834
+ function loadRunnerRequestByKey(requestKey) {
2835
+ const key = String(requestKey || "").trim();
2836
+ if (!key) {
2837
+ return {};
2838
+ }
2839
+ return safeObject(normalizeBotRunnerRequests(loadBotRunnerState().requests)[key]);
2840
+ }
2841
+
2842
+ function actionableRunnerRequestMissingRootWorkItem(requestRaw) {
2843
+ const request = safeObject(requestRaw);
2844
+ return (
2845
+ isActionableRunnerRequestIntent(request.normalized_intent)
2846
+ && !String(request.root_work_item_id || "").trim()
2847
+ );
2848
+ }
2849
+
2850
+ function truncateRunnerWorkItemTitleText(rawText, maxLength = 96) {
2851
+ const text = String(rawText || "").replace(/\s+/g, " ").trim();
2852
+ if (!text) {
2853
+ return "";
2854
+ }
2855
+ if (text.length <= maxLength) {
2856
+ return text;
2857
+ }
2858
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trim()}...`;
2859
+ }
2860
+
2861
+ function buildRunnerRootWorkItemTitle({ selectedRecord, request }) {
2862
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2863
+ const intent = String(safeObject(request).normalized_intent || "").trim().toLowerCase();
2864
+ const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 84);
2865
+ const prefix = intent === "ctxpack_mutation"
2866
+ ? "Ctxpack request"
2867
+ : intent === "workitem_mutation"
2868
+ ? "Work item request"
2869
+ : "Runner request";
2870
+ if (requestBody) {
2871
+ return `${prefix}: ${requestBody}`;
2872
+ }
2873
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
2874
+ return messageID > 0 ? `${prefix} #${messageID}` : prefix;
2875
+ }
2876
+
2877
+ function buildRunnerRootWorkItemDescription({
2878
+ normalizedRoute,
2879
+ routeKey,
2880
+ selectedRecord,
2881
+ request,
2882
+ }) {
2883
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2884
+ const lines = [
2885
+ `Source request: ${String(parsed.body || "").trim() || "(empty)"}`,
2886
+ `Intent: ${String(safeObject(request).normalized_intent || "").trim() || "unknown"}`,
2887
+ `Route: ${String(normalizedRoute?.name || routeKey || "").trim() || "-"}`,
2888
+ `Provider: ${String(normalizedRoute?.provider || "").trim() || "-"}`,
2889
+ `Chat ID: ${String(parsed.chatID || parsed.chatId || "").trim() || "-"}`,
2890
+ `Message ID: ${intFromRawAllowZero(parsed.messageID, 0) || "-"}`,
2891
+ `Request key: ${String(safeObject(request).request_key || "").trim() || "-"}`,
2892
+ ];
2893
+ const conversationID = String(safeObject(request).conversation_id || parsed.conversationID || "").trim();
2894
+ if (conversationID) {
2895
+ lines.push(`Conversation ID: ${conversationID}`);
2896
+ }
2897
+ const selectedBots = ensureArray(safeObject(request).selected_bot_usernames)
2898
+ .map((item) => normalizeTelegramMentionUsername(item))
2899
+ .filter(Boolean);
2900
+ if (selectedBots.length > 0) {
2901
+ lines.push(`Selected bots: ${selectedBots.join(", ")}`);
2902
+ }
2903
+ return lines.join("\n").trim();
2904
+ }
2905
+
2906
+ function buildRunnerRootWorkItemThreadTitle({ selectedRecord }) {
2907
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2908
+ const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 72);
2909
+ if (requestBody) {
2910
+ return `Request Context: ${requestBody}`;
2911
+ }
2912
+ const messageID = intFromRawAllowZero(parsed.messageID, 0);
2913
+ return messageID > 0 ? `Request Context #${messageID}` : "Request Context";
2914
+ }
2915
+
2916
+ function buildRunnerRootWorkItemThreadBody({
2917
+ normalizedRoute,
2918
+ routeKey,
2919
+ selectedRecord,
2920
+ request,
2921
+ }) {
2922
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2923
+ const lines = [
2924
+ "Runner root request context thread.",
2925
+ buildRunnerRootWorkItemDescription({
2926
+ normalizedRoute,
2927
+ routeKey,
2928
+ selectedRecord,
2929
+ request,
2930
+ }),
2931
+ ];
2932
+ if (String(selectedRecord?.id || "").trim()) {
2933
+ lines.push(`Archive comment ID: ${String(selectedRecord.id || "").trim()}`);
2934
+ }
2935
+ const occurredAt = String(parsed.occurredAt || parsed.occurred_at || "").trim();
2936
+ if (occurredAt) {
2937
+ lines.push(`Occurred at: ${occurredAt}`);
2938
+ }
2939
+ return lines.filter(Boolean).join("\n").trim();
2940
+ }
2941
+
2942
+ async function ensureRunnerRootThreadForRequest({
2943
+ normalizedRoute,
2944
+ routeKey,
2945
+ selectedRecord,
2946
+ runtime,
2947
+ requestKey,
2948
+ }) {
2949
+ const key = String(requestKey || "").trim();
2950
+ if (!key) {
2951
+ return {
2952
+ ok: false,
2953
+ reason: "request_key_missing",
2954
+ };
2955
+ }
2956
+ const currentState = loadBotRunnerState();
2957
+ const request = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
2958
+ const rootWorkItemID = String(request.root_work_item_id || "").trim();
2959
+ if (!Object.keys(request).length) {
2960
+ return {
2961
+ ok: false,
2962
+ reason: "request_not_found",
2963
+ requestKey: key,
2964
+ };
2965
+ }
2966
+ if (!rootWorkItemID) {
2967
+ return {
2968
+ ok: true,
2969
+ requestKey: key,
2970
+ request,
2971
+ skipped: true,
2972
+ reason: "root_work_item_missing",
2973
+ };
2974
+ }
2975
+ if (String(request.root_thread_id || "").trim()) {
2976
+ return {
2977
+ ok: true,
2978
+ requestKey: key,
2979
+ request,
2980
+ reused: true,
2981
+ };
2982
+ }
2983
+ if (!runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
2984
+ return {
2985
+ ok: false,
2986
+ reason: "governance_runtime_unavailable",
2987
+ requestKey: key,
2988
+ request,
2989
+ };
2990
+ }
2991
+ try {
2992
+ let rootThreadID = "";
2993
+ const existingThreads = ensureArray(await listWorkItemThreads({
2994
+ siteBaseURL: runtime.baseURL,
2995
+ token: runtime.token,
2996
+ timeoutSeconds: runtime.timeoutSeconds,
2997
+ workItemID: rootWorkItemID,
2998
+ status: "",
2999
+ }));
3000
+ rootThreadID = String(
3001
+ safeObject(existingThreads[0]).id
3002
+ || safeObject(existingThreads[0]).thread_id
3003
+ || safeObject(existingThreads[0]).threadID
3004
+ || "",
3005
+ ).trim();
3006
+ if (!rootThreadID) {
3007
+ const createdThread = safeObject(await createWorkItemThread({
3008
+ siteBaseURL: runtime.baseURL,
3009
+ token: runtime.token,
3010
+ timeoutSeconds: runtime.timeoutSeconds,
3011
+ actorUserID: runtime.actor.user_id,
3012
+ workItemID: rootWorkItemID,
3013
+ title: buildRunnerRootWorkItemThreadTitle({
3014
+ selectedRecord,
3015
+ }),
3016
+ body: buildRunnerRootWorkItemThreadBody({
3017
+ normalizedRoute,
3018
+ routeKey,
3019
+ selectedRecord,
3020
+ request,
3021
+ }),
3022
+ }));
3023
+ rootThreadID = String(createdThread.thread_id || createdThread.threadID || createdThread.id || "").trim();
3024
+ }
3025
+ if (!rootThreadID) {
3026
+ throw new Error("root thread creation returned no id");
3027
+ }
3028
+ const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, key, {
3029
+ root_thread_id: rootThreadID,
3030
+ });
3031
+ saveBotRunnerState({
3032
+ routes: currentState.routes,
3033
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3034
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3035
+ requests: nextRequests,
3036
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3037
+ });
3038
+ return {
3039
+ ok: true,
3040
+ requestKey: key,
3041
+ request: nextRequest,
3042
+ created: true,
3043
+ };
3044
+ } catch (err) {
3045
+ return {
3046
+ ok: false,
3047
+ reason: "root_thread_create_failed",
3048
+ requestKey: key,
3049
+ request,
3050
+ error: String(err?.message || err).trim() || "failed to create root thread",
3051
+ };
3052
+ }
3053
+ }
3054
+
3055
+ async function inheritRunnerReferenceRootWorkItemForRequest({
3056
+ normalizedRoute,
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 parsed = safeObject(selectedRecord?.parsedArchive);
3069
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
3070
+ const replyToMessageID = intFromRawAllowZero(parsed.replyToMessageID, 0);
3071
+ if (!chatID || replyToMessageID <= 0) {
3072
+ return {
3073
+ ok: true,
3074
+ requestKey: key,
3075
+ skipped: true,
3076
+ reason: "reply_reference_missing",
3077
+ };
3078
+ }
3079
+ const currentState = loadBotRunnerState();
3080
+ const currentRequest = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3081
+ if (!Object.keys(currentRequest).length) {
3082
+ return {
3083
+ ok: false,
3084
+ requestKey: key,
3085
+ reason: "request_not_found",
3086
+ };
3087
+ }
3088
+ if (String(currentRequest.root_work_item_id || "").trim()) {
3089
+ return {
3090
+ ok: true,
3091
+ requestKey: key,
3092
+ request: currentRequest,
3093
+ reused: true,
3094
+ };
3095
+ }
3096
+ const serverReferencedRequest = await findServerRunnerRequestForMessageID({
3097
+ normalizedRoute,
3098
+ runtime,
3099
+ chatID,
3100
+ messageID: replyToMessageID,
3101
+ });
3102
+ if (!String(serverReferencedRequest.root_work_item_id || "").trim()) {
3103
+ return {
3104
+ ok: true,
3105
+ requestKey: key,
3106
+ request: currentRequest,
3107
+ skipped: true,
3108
+ reason: "referenced_root_work_item_missing",
3109
+ };
3110
+ }
3111
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3112
+ root_work_item_id: String(serverReferencedRequest.root_work_item_id || "").trim(),
3113
+ root_work_item_title: String(serverReferencedRequest.root_work_item_title || "").trim(),
3114
+ root_work_item_status: normalizeRunnerWorkItemStatus(serverReferencedRequest.root_work_item_status),
3115
+ root_thread_id: String(serverReferencedRequest.root_thread_id || "").trim(),
3116
+ root_work_item_created_at: firstNonEmptyString([serverReferencedRequest.root_work_item_created_at]),
3117
+ root_work_item_last_error: String(serverReferencedRequest.root_work_item_last_error || "").trim(),
3118
+ });
3119
+ saveBotRunnerState({
3120
+ routes: currentState.routes,
3121
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3122
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3123
+ requests: nextRequests,
3124
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3125
+ });
3126
+ return {
3127
+ ok: true,
3128
+ requestKey: key,
3129
+ request,
3130
+ inherited: true,
3131
+ };
3132
+ }
3133
+
3134
+ async function ensureRunnerRootWorkItemForRequest({
3135
+ normalizedRoute,
3136
+ routeKey,
3137
+ selectedRecord,
3138
+ runtime,
3139
+ requestKey,
3140
+ }) {
3141
+ const key = String(requestKey || "").trim();
3142
+ if (!key) {
3143
+ return {
3144
+ ok: false,
3145
+ reason: "request_key_missing",
3146
+ };
3147
+ }
3148
+ const currentState = loadBotRunnerState();
3149
+ const existing = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3150
+ if (!Object.keys(existing).length) {
3151
+ return {
3152
+ ok: false,
3153
+ reason: "request_not_found",
3154
+ requestKey: key,
3155
+ };
3156
+ }
3157
+ if (!isActionableRunnerRequestIntent(existing.normalized_intent)) {
3158
+ return {
3159
+ ok: true,
3160
+ requestKey: key,
3161
+ request: existing,
3162
+ skipped: true,
3163
+ reason: "informational_request",
3164
+ };
3165
+ }
3166
+ if (String(existing.root_work_item_id || "").trim()) {
3167
+ const rootThreadClaim = await ensureRunnerRootThreadForRequest({
3168
+ normalizedRoute,
3169
+ routeKey,
3170
+ selectedRecord,
3171
+ runtime,
3172
+ requestKey: key,
3173
+ });
3174
+ return {
3175
+ ok: true,
3176
+ requestKey: key,
3177
+ request: safeObject(rootThreadClaim.request || existing),
3178
+ reused: true,
3179
+ root_thread_created: rootThreadClaim.created === true,
3180
+ root_thread_error: String(rootThreadClaim.error || "").trim(),
3181
+ };
3182
+ }
3183
+ if (!runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
3184
+ return {
3185
+ ok: false,
3186
+ reason: "governance_runtime_unavailable",
3187
+ requestKey: key,
3188
+ };
3189
+ }
3190
+ try {
3191
+ const actorBotID = firstNonEmptyString([
3192
+ normalizedRoute?.botID,
3193
+ normalizedRoute?.botId,
3194
+ normalizedRoute?.serverBotID,
3195
+ normalizedRoute?.server_bot_id,
3196
+ ]);
3197
+ const actorBotName = firstNonEmptyString([
3198
+ normalizedRoute?.botName,
3199
+ normalizedRoute?.bot_name,
3200
+ normalizedRoute?.serverBotName,
3201
+ normalizedRoute?.server_bot_name,
3202
+ ]);
3203
+ const title = buildRunnerRootWorkItemTitle({
3204
+ selectedRecord,
3205
+ request: existing,
3206
+ });
3207
+ const description = buildRunnerRootWorkItemDescription({
3208
+ normalizedRoute,
3209
+ routeKey,
3210
+ selectedRecord,
3211
+ request: existing,
3212
+ });
3213
+ const created = safeObject(await createProjectWorkItem({
3214
+ siteBaseURL: runtime.baseURL,
3215
+ token: runtime.token,
3216
+ timeoutSeconds: runtime.timeoutSeconds,
3217
+ actorUserID: runtime.actor.user_id,
3218
+ actorBotID: String(actorBotID || "").trim(),
3219
+ actorBotName: String(actorBotName || "").trim(),
3220
+ projectID: String(normalizedRoute?.projectID || "").trim(),
3221
+ title,
3222
+ description,
3223
+ }));
3224
+ const rootWorkItemID = String(created.id || created.work_item_id || created.workItemID || "").trim();
3225
+ if (!rootWorkItemID) {
3226
+ throw new Error("work item creation returned no id");
3227
+ }
3228
+ const rootWorkItemStatus = normalizeRunnerWorkItemStatus(created.status || "backlog") || "backlog";
3229
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3230
+ root_work_item_id: rootWorkItemID,
3231
+ root_work_item_title: String(created.title || title).trim() || title,
3232
+ root_work_item_status: rootWorkItemStatus,
3233
+ root_work_item_created_at: new Date().toISOString(),
3234
+ root_work_item_last_error: "",
3235
+ });
3236
+ saveBotRunnerState({
3237
+ routes: currentState.routes,
3238
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3239
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3240
+ requests: nextRequests,
3241
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3242
+ });
3243
+ const rootThreadClaim = await ensureRunnerRootThreadForRequest({
3244
+ normalizedRoute,
3245
+ routeKey,
3246
+ selectedRecord,
3247
+ runtime,
3248
+ requestKey: key,
3249
+ });
3250
+ return {
3251
+ ok: true,
3252
+ requestKey: key,
3253
+ request: safeObject(rootThreadClaim.request || request),
3254
+ created: true,
3255
+ root_thread_created: rootThreadClaim.created === true,
3256
+ root_thread_error: String(rootThreadClaim.error || "").trim(),
3257
+ };
3258
+ } catch (err) {
3259
+ const errorText = String(err?.message || err).trim() || "failed to create root work item";
3260
+ const { requests: nextRequests, request } = upsertRunnerRequest(currentState, key, {
3261
+ root_work_item_last_error: errorText,
3262
+ });
3263
+ saveBotRunnerState({
3264
+ routes: currentState.routes,
3265
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3266
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
3267
+ requests: nextRequests,
3268
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
3269
+ });
3270
+ return {
3271
+ ok: false,
3272
+ reason: "root_work_item_create_failed",
3273
+ requestKey: key,
3274
+ request,
3275
+ error: errorText,
3276
+ };
3277
+ }
3278
+ }
3279
+
3280
+ function deriveRunnerRootWorkItemTargetStatus(rawRequestStatus) {
3281
+ const status = normalizeRunnerRequestStatus(rawRequestStatus);
3282
+ if (status === "running") {
3283
+ return "doing";
3284
+ }
3285
+ if (status === "completed" || status === "loop_closed") {
3286
+ return "done";
3287
+ }
3288
+ if (status === "closed" || status === "expired") {
3289
+ return "canceled";
3290
+ }
3291
+ return "";
3292
+ }
3293
+
3294
+ function buildRunnerRootWorkItemTransitionPath(currentStatusRaw, targetStatusRaw) {
3295
+ const currentStatus = normalizeRunnerWorkItemStatus(currentStatusRaw) || "backlog";
3296
+ const targetStatus = normalizeRunnerWorkItemStatus(targetStatusRaw);
3297
+ if (!targetStatus || currentStatus === targetStatus) {
3298
+ return [];
3299
+ }
3300
+ if (targetStatus === "doing") {
3301
+ return currentStatus === "backlog" ? ["doing"] : [];
3302
+ }
3303
+ if (targetStatus === "done") {
3304
+ if (currentStatus === "backlog") return ["doing", "review", "done"];
3305
+ if (currentStatus === "doing") return ["review", "done"];
3306
+ if (currentStatus === "review") return ["done"];
3307
+ return [];
3308
+ }
3309
+ if (targetStatus === "canceled") {
3310
+ if (currentStatus === "backlog" || currentStatus === "doing" || currentStatus === "review") {
3311
+ return ["canceled"];
3312
+ }
3313
+ }
3314
+ return [];
3315
+ }
3316
+
3317
+ async function syncRunnerRequestRootWorkItemForOutcome({
3318
+ normalizedRoute,
3319
+ runtime,
3320
+ requestKey,
2499
3321
  }) {
2500
- const parsed = safeObject(selectedRecord?.parsedArchive);
2501
- const commentKind = String(parsed.kind || "").trim().toLowerCase();
2502
- if (!isInboundArchiveKind(commentKind)) {
3322
+ const key = String(requestKey || "").trim();
3323
+ if (!key) {
2503
3324
  return {
2504
3325
  ok: false,
2505
- reason: "non_human_comment_cannot_create_request",
3326
+ reason: "request_key_missing",
2506
3327
  };
2507
3328
  }
2508
- const requestKey = buildRunnerRequestKey({
2509
- normalizedRoute,
2510
- selectedRecord,
2511
- selectedBotUsernames,
2512
- normalizedIntent,
2513
- });
2514
3329
  const currentState = loadBotRunnerState();
2515
- const replyChainContext = resolveRunnerReplyChainConversationContext(currentState, normalizedRoute, selectedRecord);
2516
- const conversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
2517
- let stateForClaim = currentState;
2518
- if (
2519
- replyChainContext.referencedRequest
2520
- && conversationID
2521
- && !String(replyChainContext.referencedRequest.conversation_id || "").trim()
2522
- && String(replyChainContext.referencedRequest.request_key || "").trim()
2523
- ) {
2524
- const backfilled = upsertRunnerRequest(stateForClaim, replyChainContext.referencedRequest.request_key, {
2525
- conversation_id: conversationID,
2526
- });
2527
- stateForClaim = {
2528
- ...stateForClaim,
2529
- requests: backfilled.requests,
2530
- };
2531
- }
2532
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
2533
- const existing = safeObject(requests[requestKey]);
2534
- if (isFinalRunnerRequestStatus(existing.status)) {
3330
+ const request = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3331
+ const rootWorkItemID = String(request.root_work_item_id || "").trim();
3332
+ if (!rootWorkItemID) {
2535
3333
  return {
2536
- ok: false,
2537
- reason: "request_already_finalized",
2538
- requestKey,
3334
+ ok: true,
3335
+ skipped: true,
3336
+ reason: "root_work_item_missing",
3337
+ request,
2539
3338
  };
2540
3339
  }
2541
- if (
2542
- isActiveRunnerRequestStatus(existing.status)
2543
- && String(existing.claimed_by_route || "").trim()
2544
- && String(existing.claimed_by_route || "").trim() !== String(routeKey || "").trim()
2545
- ) {
3340
+ const targetStatus = deriveRunnerRootWorkItemTargetStatus(request.status);
3341
+ if (!targetStatus || !runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
2546
3342
  return {
2547
- ok: false,
2548
- reason: "request_already_claimed",
2549
- requestKey,
3343
+ ok: true,
3344
+ skipped: true,
3345
+ reason: !targetStatus ? "no_target_status" : "governance_runtime_unavailable",
3346
+ request,
2550
3347
  };
2551
3348
  }
2552
- const nowISO = new Date().toISOString();
2553
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
2554
- project_id: String(normalizedRoute?.projectID || "").trim(),
2555
- provider: String(normalizedRoute?.provider || "").trim(),
2556
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
2557
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2558
- root_comment_id: String(selectedRecord?.id || "").trim(),
2559
- root_comment_kind: commentKind,
2560
- conversation_id: conversationID,
2561
- selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
2562
- normalized_intent: String(normalizedIntent || "").trim().toLowerCase(),
2563
- status: "claimed",
2564
- claimed_by_route: String(routeKey || "").trim(),
2565
- claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
2566
- last_comment_id: String(selectedRecord?.id || "").trim(),
2567
- last_comment_kind: commentKind,
2568
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2569
- });
2570
- const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
2571
- project_id: String(normalizedRoute?.projectID || "").trim(),
2572
- provider: String(normalizedRoute?.provider || "").trim(),
2573
- request_key: requestKey,
2574
- route_key: String(routeKey || "").trim(),
2575
- conversation_id: conversationID,
2576
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2577
- comment_kind: commentKind,
2578
- request_status: "claimed",
3349
+ let currentStatus = normalizeRunnerWorkItemStatus(request.root_work_item_status) || "backlog";
3350
+ const transitions = buildRunnerRootWorkItemTransitionPath(currentStatus, targetStatus);
3351
+ let lastError = "";
3352
+ const actorBotID = firstNonEmptyString([
3353
+ normalizedRoute?.botID,
3354
+ normalizedRoute?.botId,
3355
+ normalizedRoute?.serverBotID,
3356
+ normalizedRoute?.server_bot_id,
3357
+ ]);
3358
+ const actorBotName = firstNonEmptyString([
3359
+ normalizedRoute?.botName,
3360
+ normalizedRoute?.bot_name,
3361
+ normalizedRoute?.serverBotName,
3362
+ normalizedRoute?.server_bot_name,
3363
+ ]);
3364
+ for (const nextStatus of transitions) {
3365
+ try {
3366
+ await transitionProjectWorkItem({
3367
+ siteBaseURL: runtime.baseURL,
3368
+ token: runtime.token,
3369
+ timeoutSeconds: runtime.timeoutSeconds,
3370
+ actorUserID: runtime.actor.user_id,
3371
+ actorBotID: String(actorBotID || "").trim(),
3372
+ actorBotName: String(actorBotName || "").trim(),
3373
+ workItemID: rootWorkItemID,
3374
+ status: nextStatus,
3375
+ });
3376
+ currentStatus = nextStatus;
3377
+ } catch (err) {
3378
+ lastError = String(err?.message || err).trim() || `failed to transition work item to ${nextStatus}`;
3379
+ break;
3380
+ }
3381
+ }
3382
+ const { requests: nextRequests, request: nextRequest } = upsertRunnerRequest(currentState, key, {
3383
+ root_work_item_status: currentStatus,
3384
+ root_work_item_last_error: lastError,
2579
3385
  });
2580
3386
  saveBotRunnerState({
2581
- routes: stateForClaim.routes,
2582
- sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
2583
- excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
3387
+ routes: currentState.routes,
3388
+ sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
3389
+ excludedComments: currentState.excludedComments || currentState.excluded_comments,
2584
3390
  requests: nextRequests,
2585
- consumedComments: nextConsumedComments,
3391
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
2586
3392
  });
2587
3393
  return {
2588
- ok: true,
2589
- requestKey,
2590
- request,
3394
+ ok: lastError === "",
3395
+ request: nextRequest,
3396
+ transitioned: transitions.length > 0 && lastError === "",
3397
+ error: lastError,
2591
3398
  };
2592
3399
  }
2593
3400
 
@@ -2918,6 +3725,30 @@ function runnerLedgerEntryMatchesProject(entryRaw, normalizedRoute, requestIndex
2918
3725
  );
2919
3726
  }
2920
3727
 
3728
+ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
3729
+ const localEntry = safeObject(localEntryRaw);
3730
+ const serverEntry = safeObject(serverEntryRaw);
3731
+ const merged = {
3732
+ ...localEntry,
3733
+ ...serverEntry,
3734
+ };
3735
+ const preserveLocalStringWhenServerBlank = (fieldName) => {
3736
+ const serverValue = String(serverEntry[fieldName] || "").trim();
3737
+ const localValue = String(localEntry[fieldName] || "").trim();
3738
+ if (!serverValue && localValue) {
3739
+ merged[fieldName] = localValue;
3740
+ }
3741
+ };
3742
+ preserveLocalStringWhenServerBlank("conversation_id");
3743
+ preserveLocalStringWhenServerBlank("root_work_item_id");
3744
+ preserveLocalStringWhenServerBlank("root_work_item_title");
3745
+ preserveLocalStringWhenServerBlank("root_work_item_status");
3746
+ preserveLocalStringWhenServerBlank("root_thread_id");
3747
+ preserveLocalStringWhenServerBlank("root_work_item_created_at");
3748
+ preserveLocalStringWhenServerBlank("root_work_item_last_error");
3749
+ return merged;
3750
+ }
3751
+
2921
3752
  function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRoute, serverRequests = [], serverCommentStates = []) {
2922
3753
  const state = safeObject(currentState);
2923
3754
  const normalizedRequests = normalizeBotRunnerRequests(state.requests);
@@ -2941,7 +3772,12 @@ function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRo
2941
3772
  const request = safeObject(requestRaw);
2942
3773
  const requestKey = String(request.request_key || request.requestKey || "").trim();
2943
3774
  if (!requestKey) continue;
2944
- nextRequests[requestKey] = normalizeBotRunnerRequests({ [requestKey]: request })[requestKey];
3775
+ const localRequest = safeObject(normalizedRequests[requestKey]);
3776
+ nextRequests[requestKey] = normalizeBotRunnerRequests({
3777
+ [requestKey]: {
3778
+ ...mergeRunnerRequestForServerHydration(localRequest, request),
3779
+ },
3780
+ })[requestKey];
2945
3781
  }
2946
3782
 
2947
3783
  const requestIndex = normalizeBotRunnerRequests(nextRequests);
@@ -3094,6 +3930,12 @@ async function syncRunnerRequestLedgerForProjectToServer({ normalizedRoute, runt
3094
3930
  const commentStates = buildProjectRunnerRequestCommentStatesForSync(state, normalizedRoute);
3095
3931
 
3096
3932
  for (const request of requests) {
3933
+ if (
3934
+ isActionableRunnerRequestIntent(request.normalized_intent)
3935
+ && !String(request.root_work_item_id || "").trim()
3936
+ ) {
3937
+ continue;
3938
+ }
3097
3939
  await upsertProjectRunnerRequest({
3098
3940
  siteBaseURL: runtime.baseURL,
3099
3941
  projectID,
@@ -4925,6 +5767,10 @@ async function createProjectWorkItem(params) {
4925
5767
  return createProjectWorkItemImpl(params, buildRunnerDataDeps());
4926
5768
  }
4927
5769
 
5770
+ async function transitionProjectWorkItem(params) {
5771
+ return transitionProjectWorkItemImpl(params, buildRunnerDataDeps());
5772
+ }
5773
+
4928
5774
  async function createProjectEvidence(params) {
4929
5775
  return createProjectEvidenceImpl(params, buildRunnerDataDeps());
4930
5776
  }
@@ -4933,6 +5779,10 @@ async function createWorkItemThread(params) {
4933
5779
  return createWorkItemThreadImpl(params, buildRunnerDataDeps());
4934
5780
  }
4935
5781
 
5782
+ async function listWorkItemThreads(params) {
5783
+ return listWorkItemThreadsImpl(params, buildRunnerDataDeps());
5784
+ }
5785
+
4936
5786
  async function linkWorkItemEvidence(params) {
4937
5787
  return linkWorkItemEvidenceImpl(params, buildRunnerDataDeps());
4938
5788
  }
@@ -5241,6 +6091,14 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
5241
6091
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
5242
6092
  .map((value) => normalizeTelegramMentionUsername(value))
5243
6093
  .filter(Boolean),
6094
+ root_work_item: String(entry.root_work_item_id || "").trim()
6095
+ ? {
6096
+ id: String(entry.root_work_item_id || "").trim(),
6097
+ title: String(entry.root_work_item_title || "").trim(),
6098
+ status: normalizeRunnerWorkItemStatus(entry.root_work_item_status),
6099
+ thread_id: String(entry.root_thread_id || "").trim(),
6100
+ }
6101
+ : null,
5244
6102
  };
5245
6103
  }
5246
6104
 
@@ -5299,7 +6157,7 @@ function pickPreferredStatusLookupRequest(entries = []) {
5299
6157
  return candidates[0];
5300
6158
  }
5301
6159
 
5302
- function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
6160
+ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runnerStateOverride = null }) {
5303
6161
  const parsed = safeObject(selectedRecord?.parsedArchive);
5304
6162
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5305
6163
  const currentChatID = String(parsed.chatID || parsed.chatId || "").trim();
@@ -5318,18 +6176,23 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5318
6176
  && activeSourceMessageID > 0
5319
6177
  && currentMessageID === activeSourceMessageID
5320
6178
  );
5321
- let runnerState = { requests: {} };
5322
- try {
5323
- runnerState = loadBotRunnerState();
5324
- } catch {}
6179
+ let runnerState = safeObject(runnerStateOverride);
6180
+ if (!Object.keys(runnerState).length) {
6181
+ runnerState = { requests: {} };
6182
+ try {
6183
+ runnerState = loadBotRunnerState();
6184
+ } catch {}
6185
+ }
5325
6186
  const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
5326
6187
  const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
6188
+ const activeRequestKey = String(safeObject(routeState).active_request_key || "").trim();
5327
6189
  const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
5328
6190
  entry,
5329
6191
  routeKey,
5330
6192
  selfBotUsername,
5331
6193
  currentMessageID,
5332
6194
  );
6195
+ const referencedRequestCandidate = safeObject(replyChainContext.referencedRequest);
5333
6196
  let relatedActiveRequest = null;
5334
6197
  let relatedRequest = null;
5335
6198
  const selectors = currentConversationID
@@ -5339,20 +6202,43 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5339
6202
  if (!scopedRequests.length && currentConversationID) {
5340
6203
  scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
5341
6204
  }
6205
+ if (!scopedRequests.length && activeRequestKey) {
6206
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
6207
+ }
5342
6208
  const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
5343
- relatedActiveRequest = eligibleScopedRequests
6209
+ const statusLookupCandidates = [...eligibleScopedRequests];
6210
+ if (
6211
+ Object.keys(referencedRequestCandidate).length > 0
6212
+ && requestMatchesCurrentRoute(referencedRequestCandidate)
6213
+ && !statusLookupCandidates.some(
6214
+ (entry) => String(safeObject(entry).request_key || "").trim() === String(referencedRequestCandidate.request_key || "").trim(),
6215
+ )
6216
+ ) {
6217
+ statusLookupCandidates.push(referencedRequestCandidate);
6218
+ }
6219
+ relatedActiveRequest = statusLookupCandidates
5344
6220
  .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
5345
- relatedRequest = pickPreferredStatusLookupRequest(
5346
- eligibleScopedRequests.length
5347
- ? eligibleScopedRequests
5348
- : (replyChainContext.referencedRequest ? [replyChainContext.referencedRequest] : []).filter(requestMatchesCurrentRoute),
5349
- );
6221
+ relatedRequest = pickPreferredStatusLookupRequest(statusLookupCandidates);
5350
6222
  const lastAction = String(safeObject(routeState).last_action || "").trim();
5351
6223
  const lastReason = String(safeObject(routeState).last_reason || "").trim();
5352
6224
  const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
5353
6225
  const routeConversationID = String(safeObject(routeState).last_conversation_id || "").trim();
6226
+ const activeRootWorkItemID = String(safeObject(routeState).active_root_work_item_id || "").trim();
6227
+ const activeRootWorkItemTitle = String(safeObject(routeState).active_root_work_item_title || "").trim();
6228
+ const activeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).active_root_work_item_status);
6229
+ const routeRootWorkItemID = String(safeObject(routeState).last_root_work_item_id || "").trim();
6230
+ const routeRootWorkItemTitle = String(safeObject(routeState).last_root_work_item_title || "").trim();
6231
+ const routeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).last_root_work_item_status);
5354
6232
  const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
5355
6233
  const routeWorkItemTitles = ensureArray(safeObject(routeState).last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
6234
+ const requestRootWorkItem = String(safeObject(relatedRequest).root_work_item_id || "").trim()
6235
+ ? {
6236
+ id: String(safeObject(relatedRequest).root_work_item_id || "").trim(),
6237
+ title: String(safeObject(relatedRequest).root_work_item_title || "").trim(),
6238
+ status: normalizeRunnerWorkItemStatus(safeObject(relatedRequest).root_work_item_status),
6239
+ thread_id: String(safeObject(relatedRequest).root_thread_id || "").trim(),
6240
+ }
6241
+ : null;
5356
6242
  return {
5357
6243
  kind: "runner_status",
5358
6244
  status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
@@ -5376,6 +6262,21 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5376
6262
  : null,
5377
6263
  related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
5378
6264
  related_request: relatedRequest ? summarizeRunnerRequestForStatusLookup(relatedRequest) : null,
6265
+ root_work_item: String(safeObject(requestRootWorkItem).id || "").trim()
6266
+ ? requestRootWorkItem
6267
+ : activeRootWorkItemID
6268
+ ? {
6269
+ id: activeRootWorkItemID,
6270
+ title: activeRootWorkItemTitle,
6271
+ status: activeRootWorkItemStatus,
6272
+ }
6273
+ : currentConversationID && routeConversationID === currentConversationID && routeRootWorkItemID
6274
+ ? {
6275
+ id: routeRootWorkItemID,
6276
+ title: routeRootWorkItemTitle,
6277
+ status: routeRootWorkItemStatus,
6278
+ }
6279
+ : null,
5379
6280
  route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
5380
6281
  ? {
5381
6282
  ids: routeWorkItemIDs,
@@ -5442,6 +6343,13 @@ async function resolveInformationalQueryReply({
5442
6343
  };
5443
6344
  }
5444
6345
  if (normalizedIntentType === "status_query") {
6346
+ let hydratedRunnerState = null;
6347
+ try {
6348
+ hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
6349
+ normalizedRoute: route,
6350
+ runtime,
6351
+ });
6352
+ } catch {}
5445
6353
  return {
5446
6354
  handled: true,
5447
6355
  source: "runner.status",
@@ -5451,6 +6359,7 @@ async function resolveInformationalQueryReply({
5451
6359
  route,
5452
6360
  routeState,
5453
6361
  selectedRecord,
6362
+ runnerStateOverride: hydratedRunnerState,
5454
6363
  }),
5455
6364
  };
5456
6365
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
@@ -5591,14 +6500,18 @@ function emptyRunnerActiveExecutionPatch() {
5591
6500
  active_request_key: "",
5592
6501
  active_started_at: "",
5593
6502
  active_heartbeat_at: "",
6503
+ active_root_work_item_id: "",
6504
+ active_root_work_item_title: "",
6505
+ active_root_work_item_status: "",
5594
6506
  active_runner_pid: undefined,
5595
6507
  active_execution_token: "",
5596
6508
  };
5597
6509
  }
5598
6510
 
5599
- function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
6511
+ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "", rootWorkItem = {}) {
5600
6512
  const nowISO = new Date().toISOString();
5601
6513
  const parsed = safeObject(selectedRecord?.parsedArchive);
6514
+ const root = safeObject(rootWorkItem);
5602
6515
  return {
5603
6516
  active_comment_id: String(selectedRecord?.id || "").trim(),
5604
6517
  active_comment_created_at: firstNonEmptyString([selectedRecord?.createdAt, selectedRecord?.updatedAt]),
@@ -5606,6 +6519,9 @@ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
5606
6519
  active_request_key: String(requestKey || "").trim(),
5607
6520
  active_started_at: nowISO,
5608
6521
  active_heartbeat_at: nowISO,
6522
+ active_root_work_item_id: String(root.id || root.root_work_item_id || "").trim(),
6523
+ active_root_work_item_title: String(root.title || root.root_work_item_title || "").trim(),
6524
+ active_root_work_item_status: normalizeRunnerWorkItemStatus(root.status || root.root_work_item_status),
5609
6525
  active_runner_pid: process.pid,
5610
6526
  active_execution_token: `${Date.now()}-${process.pid}-${String(selectedRecord?.id || "").trim()}`,
5611
6527
  };
@@ -6085,7 +7001,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6085
7001
  }
6086
7002
  return null;
6087
7003
  };
6088
- const prepareRunnerRequestClaim = (selectedRecord, selectedResponderSelectors = []) => {
7004
+ const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = []) => {
6089
7005
  const parsed = safeObject(selectedRecord?.parsedArchive);
6090
7006
  const kind = String(parsed.kind || "").trim().toLowerCase();
6091
7007
  if (kind === "bot_reply") {
@@ -6115,6 +7031,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6115
7031
  routeKey,
6116
7032
  selectedRecord,
6117
7033
  selectedBotUsernames: selectedResponderSelectors,
7034
+ runtime,
6118
7035
  });
6119
7036
  };
6120
7037
  if (deferExecution) {
@@ -6201,7 +7118,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6201
7118
  });
6202
7119
  continue;
6203
7120
  }
6204
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
7121
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
6205
7122
  if (!requestClaim.ok) {
6206
7123
  await syncRunnerRequestLedgerForProjectToServer({
6207
7124
  normalizedRoute,
@@ -6226,9 +7143,75 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6226
7143
  });
6227
7144
  continue;
6228
7145
  }
7146
+ const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
7147
+ normalizedRoute,
7148
+ selectedRecord,
7149
+ runtime,
7150
+ requestKey: requestClaim.requestKey,
7151
+ });
7152
+ const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
7153
+ normalizedRoute,
7154
+ routeKey,
7155
+ selectedRecord,
7156
+ runtime,
7157
+ requestKey: requestClaim.requestKey,
7158
+ });
7159
+ const claimedRequest = safeObject(
7160
+ loadRunnerRequestByKey(requestClaim.requestKey)
7161
+ || rootWorkItemClaim.request
7162
+ || inheritedRootReference.request
7163
+ || requestClaim.request,
7164
+ );
7165
+ const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
7166
+ if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
7167
+ const rootWorkItemFailure = String(
7168
+ rootWorkItemClaim.error
7169
+ || rootWorkItemClaim.reason
7170
+ || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
7171
+ ).trim() || "root_work_item_create_failed";
7172
+ if (String(requestClaim.requestKey || "").trim()) {
7173
+ markRunnerRequestLifecycle({
7174
+ normalizedRoute,
7175
+ requestKey: requestClaim.requestKey,
7176
+ selectedRecord,
7177
+ routeKey,
7178
+ outcome: "closed",
7179
+ closedReason: rootWorkItemFailure,
7180
+ });
7181
+ await syncRunnerRequestLedgerForProjectToServer({
7182
+ normalizedRoute,
7183
+ runtime,
7184
+ });
7185
+ }
7186
+ saveRunnerRouteState(
7187
+ routeKey,
7188
+ buildRunnerRouteStateFromComment(selectedRecord, {
7189
+ last_action: "request_skipped",
7190
+ last_reason: rootWorkItemFailure,
7191
+ last_trigger: "work_item_root",
7192
+ last_request_key: String(requestClaim.requestKey || "").trim(),
7193
+ }),
7194
+ );
7195
+ skippedRecords.push({
7196
+ id: selectedRecord.id,
7197
+ reason: rootWorkItemFailure,
7198
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
7199
+ diagnosticType: "skip",
7200
+ action: "skip_missing_root_work_item",
7201
+ closedReason: rootWorkItemFailure,
7202
+ });
7203
+ continue;
7204
+ }
6229
7205
  saveRunnerRouteState(routeKey, {
6230
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey),
7206
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7207
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7208
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7209
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7210
+ }),
6231
7211
  last_request_key: String(requestClaim.requestKey || "").trim(),
7212
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
7213
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
7214
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
6232
7215
  });
6233
7216
  await syncRunnerRequestLedgerForProjectToServer({
6234
7217
  normalizedRoute,
@@ -6375,7 +7358,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6375
7358
  continue;
6376
7359
  }
6377
7360
  const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
6378
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
7361
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
6379
7362
  if (!requestClaim.ok) {
6380
7363
  await syncRunnerRequestLedgerForProjectToServer({
6381
7364
  normalizedRoute,
@@ -6400,11 +7383,78 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6400
7383
  });
6401
7384
  continue;
6402
7385
  }
7386
+ const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
7387
+ normalizedRoute,
7388
+ selectedRecord,
7389
+ runtime,
7390
+ requestKey: requestClaim.requestKey,
7391
+ });
7392
+ const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
7393
+ normalizedRoute,
7394
+ routeKey,
7395
+ selectedRecord,
7396
+ runtime,
7397
+ requestKey: requestClaim.requestKey,
7398
+ });
7399
+ const claimedRequest = safeObject(
7400
+ loadRunnerRequestByKey(requestClaim.requestKey)
7401
+ || rootWorkItemClaim.request
7402
+ || inheritedRootReference.request
7403
+ || requestClaim.request,
7404
+ );
7405
+ const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
7406
+ if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
7407
+ const rootWorkItemFailure = String(
7408
+ rootWorkItemClaim.error
7409
+ || rootWorkItemClaim.reason
7410
+ || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
7411
+ ).trim() || "root_work_item_create_failed";
7412
+ if (String(requestClaim.requestKey || "").trim()) {
7413
+ markRunnerRequestLifecycle({
7414
+ normalizedRoute,
7415
+ requestKey: requestClaim.requestKey,
7416
+ selectedRecord,
7417
+ routeKey,
7418
+ outcome: "closed",
7419
+ closedReason: rootWorkItemFailure,
7420
+ });
7421
+ await syncRunnerRequestLedgerForProjectToServer({
7422
+ normalizedRoute,
7423
+ runtime,
7424
+ });
7425
+ }
7426
+ saveRunnerRouteState(
7427
+ routeKey,
7428
+ buildRunnerRouteStateFromComment(selectedRecord, {
7429
+ last_action: "request_skipped",
7430
+ last_reason: rootWorkItemFailure,
7431
+ last_trigger: "work_item_root",
7432
+ last_request_key: String(requestClaim.requestKey || "").trim(),
7433
+ }),
7434
+ );
7435
+ skippedRecords.push({
7436
+ id: selectedRecord.id,
7437
+ reason: rootWorkItemFailure,
7438
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
7439
+ diagnosticType: "skip",
7440
+ action: "skip_missing_root_work_item",
7441
+ closedReason: rootWorkItemFailure,
7442
+ });
7443
+ continue;
7444
+ }
6403
7445
  saveRunnerRouteState(routeKey, {
6404
- active_request_key: String(requestClaim.requestKey || "").trim(),
7446
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7447
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7448
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7449
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7450
+ }),
6405
7451
  last_request_key: String(requestClaim.requestKey || "").trim(),
7452
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
7453
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
7454
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
6406
7455
  });
6407
7456
  if (String(requestClaim.requestKey || "").trim()) {
7457
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6408
7458
  markRunnerRequestLifecycle({
6409
7459
  normalizedRoute,
6410
7460
  requestKey: requestClaim.requestKey,
@@ -6412,6 +7462,22 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6412
7462
  routeKey,
6413
7463
  outcome: "running",
6414
7464
  });
7465
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7466
+ normalizedRoute,
7467
+ runtime,
7468
+ requestKey: requestClaim.requestKey,
7469
+ });
7470
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7471
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7472
+ saveRunnerRouteState(routeKey, {
7473
+ active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7474
+ active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7475
+ active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7476
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7477
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7478
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7479
+ });
7480
+ }
6415
7481
  await syncRunnerRequestLedgerForProjectToServer({
6416
7482
  normalizedRoute,
6417
7483
  runtime,
@@ -6422,7 +7488,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6422
7488
  normalizedRoute,
6423
7489
  routeState: {
6424
7490
  ...currentRouteState,
6425
- active_request_key: String(requestClaim.requestKey || "").trim(),
7491
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
7492
+ id: String(claimedRequest.root_work_item_id || "").trim(),
7493
+ title: String(claimedRequest.root_work_item_title || "").trim(),
7494
+ status: String(claimedRequest.root_work_item_status || "").trim(),
7495
+ }),
6426
7496
  },
6427
7497
  selectedRecord,
6428
7498
  pendingOrdered: pending.ordered,
@@ -6457,6 +7527,19 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6457
7527
  outcome: "skipped",
6458
7528
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
6459
7529
  });
7530
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7531
+ normalizedRoute,
7532
+ runtime,
7533
+ requestKey: requestClaim.requestKey,
7534
+ });
7535
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7536
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7537
+ saveRunnerRouteState(routeKey, {
7538
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7539
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7540
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7541
+ });
7542
+ }
6460
7543
  await syncRunnerRequestLedgerForProjectToServer({
6461
7544
  normalizedRoute,
6462
7545
  runtime,
@@ -6466,6 +7549,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6466
7549
  continue;
6467
7550
  }
6468
7551
  if (String(requestClaim.requestKey || "").trim()) {
7552
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6469
7553
  markRunnerRequestLifecycle({
6470
7554
  normalizedRoute,
6471
7555
  requestKey: requestClaim.requestKey,
@@ -6479,8 +7563,28 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6479
7563
  nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
6480
7564
  currentBotSelector,
6481
7565
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
6482
- normalizedIntent: String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim(),
7566
+ normalizedIntent: resolvedIntentType,
6483
7567
  });
7568
+ await ensureRunnerRootWorkItemForRequest({
7569
+ normalizedRoute,
7570
+ routeKey,
7571
+ selectedRecord,
7572
+ runtime,
7573
+ requestKey: requestClaim.requestKey,
7574
+ });
7575
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
7576
+ normalizedRoute,
7577
+ runtime,
7578
+ requestKey: requestClaim.requestKey,
7579
+ });
7580
+ const syncedRequest = safeObject(rootWorkItemSync.request);
7581
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
7582
+ saveRunnerRouteState(routeKey, {
7583
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
7584
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
7585
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
7586
+ });
7587
+ }
6484
7588
  await syncRunnerRequestLedgerForProjectToServer({
6485
7589
  normalizedRoute,
6486
7590
  runtime,
@@ -8172,7 +9276,7 @@ async function runRunnerStop(flags) {
8172
9276
  }
8173
9277
  process.stdout.write("Detached runner stop: OK\n");
8174
9278
  process.stdout.write(`registry_file: ${payload.registry_file}\n`);
8175
- for (const entry of stopped) {
9279
+ for (const entry of ensureArray(payload.stopped)) {
8176
9280
  process.stdout.write(`stopped: ${entry.launch_id} pid=${entry.pid} routes=${entry.route_names.join(", ") || "-"}\n`);
8177
9281
  }
8178
9282
  }
@@ -8197,7 +9301,7 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
8197
9301
  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
9302
  return payload;
8199
9303
  }
8200
- const launch = launchDetachedRunnerProcess(flags, routes, sourceCommand);
9304
+ const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
8201
9305
  const nextLaunches = {
8202
9306
  ...safeObject(registry).launches,
8203
9307
  [launch.launch_id]: launch,
@@ -8724,6 +9828,22 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8724
9828
  routeKey: deferredExecution.routeKey,
8725
9829
  outcome: "running",
8726
9830
  });
9831
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9832
+ normalizedRoute: deferredExecution.normalizedRoute,
9833
+ runtime: deferredExecution.runtime,
9834
+ requestKey: deferredExecution.requestKey,
9835
+ });
9836
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9837
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9838
+ saveRunnerRouteState(deferredExecution.routeKey, {
9839
+ active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9840
+ active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9841
+ active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9842
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9843
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9844
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9845
+ });
9846
+ }
8727
9847
  await syncRunnerRequestLedgerForProjectToServer({
8728
9848
  normalizedRoute: deferredExecution.normalizedRoute,
8729
9849
  runtime: deferredExecution.runtime,
@@ -8786,6 +9906,19 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8786
9906
  outcome: "skipped",
8787
9907
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
8788
9908
  });
9909
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9910
+ normalizedRoute: deferredExecution.normalizedRoute,
9911
+ runtime: deferredExecution.runtime,
9912
+ requestKey: deferredExecution.requestKey,
9913
+ });
9914
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9915
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9916
+ saveRunnerRouteState(deferredExecution.routeKey, {
9917
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9918
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9919
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9920
+ });
9921
+ }
8789
9922
  await syncRunnerRequestLedgerForProjectToServer({
8790
9923
  normalizedRoute: deferredExecution.normalizedRoute,
8791
9924
  runtime: deferredExecution.runtime,
@@ -8806,6 +9939,9 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8806
9939
  };
8807
9940
  }
8808
9941
  if (String(deferredExecution.requestKey || "").trim()) {
9942
+ const resolvedIntentType = String(
9943
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
9944
+ ).trim();
8809
9945
  markRunnerRequestLifecycle({
8810
9946
  normalizedRoute: deferredExecution.normalizedRoute,
8811
9947
  requestKey: deferredExecution.requestKey,
@@ -8821,8 +9957,28 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8821
9957
  deferredExecution.bot?.username || deferredExecution.bot?.name,
8822
9958
  ),
8823
9959
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8824
- normalizedIntent: String(safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "").trim(),
9960
+ normalizedIntent: resolvedIntentType,
9961
+ });
9962
+ await ensureRunnerRootWorkItemForRequest({
9963
+ normalizedRoute: deferredExecution.normalizedRoute,
9964
+ routeKey: deferredExecution.routeKey,
9965
+ selectedRecord: deferredExecution.selectedRecord,
9966
+ runtime: deferredExecution.runtime,
9967
+ requestKey: deferredExecution.requestKey,
9968
+ });
9969
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
9970
+ normalizedRoute: deferredExecution.normalizedRoute,
9971
+ runtime: deferredExecution.runtime,
9972
+ requestKey: deferredExecution.requestKey,
8825
9973
  });
9974
+ const syncedRequest = safeObject(rootWorkItemSync.request);
9975
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
9976
+ saveRunnerRouteState(deferredExecution.routeKey, {
9977
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
9978
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
9979
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
9980
+ });
9981
+ }
8826
9982
  await syncRunnerRequestLedgerForProjectToServer({
8827
9983
  normalizedRoute: deferredExecution.normalizedRoute,
8828
9984
  runtime: deferredExecution.runtime,
@@ -8843,6 +9999,12 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8843
9999
  last_error: errorText,
8844
10000
  });
8845
10001
  if (String(deferredExecution.requestKey || "").trim()) {
10002
+ const currentRequest = safeObject(loadRunnerRequestByKey(deferredExecution.requestKey));
10003
+ const resolvedIntentType = String(
10004
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type
10005
+ || currentRequest.normalized_intent
10006
+ || "",
10007
+ ).trim();
8846
10008
  markRunnerRequestLifecycle({
8847
10009
  normalizedRoute: deferredExecution.normalizedRoute,
8848
10010
  requestKey: deferredExecution.requestKey,
@@ -8850,7 +10012,28 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8850
10012
  routeKey: deferredExecution.routeKey,
8851
10013
  outcome: "error",
8852
10014
  closedReason: errorText || "execution_error",
10015
+ normalizedIntent: resolvedIntentType,
10016
+ });
10017
+ await ensureRunnerRootWorkItemForRequest({
10018
+ normalizedRoute: deferredExecution.normalizedRoute,
10019
+ routeKey: deferredExecution.routeKey,
10020
+ selectedRecord: deferredExecution.selectedRecord,
10021
+ runtime: deferredExecution.runtime,
10022
+ requestKey: deferredExecution.requestKey,
10023
+ });
10024
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
10025
+ normalizedRoute: deferredExecution.normalizedRoute,
10026
+ runtime: deferredExecution.runtime,
10027
+ requestKey: deferredExecution.requestKey,
8853
10028
  });
10029
+ const syncedRequest = safeObject(rootWorkItemSync.request);
10030
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
10031
+ saveRunnerRouteState(deferredExecution.routeKey, {
10032
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
10033
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
10034
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
10035
+ });
10036
+ }
8854
10037
  await syncRunnerRequestLedgerForProjectToServer({
8855
10038
  normalizedRoute: deferredExecution.normalizedRoute,
8856
10039
  runtime: deferredExecution.runtime,
@@ -13130,6 +14313,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
13130
14313
  runnerRouteLogicalSignature,
13131
14314
  loadBotRunnerState,
13132
14315
  saveBotRunnerState,
14316
+ mergeServerRunnerRequestLedgerIntoLocalState,
13133
14317
  buildRunnerStatusQueryLookup,
13134
14318
  tryJsonParse,
13135
14319
  safeObject,