metheus-governance-mcp-cli 0.2.196 → 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,
@@ -2363,6 +2386,16 @@ function buildRunnerRequestKey({
2363
2386
  ].join("::");
2364
2387
  }
2365
2388
 
2389
+ function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID) {
2390
+ const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
2391
+ const normalizedChatID = String(chatID || "").trim() || "-";
2392
+ const normalizedAnchorMessageID = intFromRawAllowZero(anchorMessageID, 0);
2393
+ if (normalizedAnchorMessageID <= 0) {
2394
+ return "";
2395
+ }
2396
+ return `reply_chain:${provider}:${normalizedChatID}:${normalizedAnchorMessageID}`;
2397
+ }
2398
+
2366
2399
  function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2367
2400
  const requests = normalizeBotRunnerRequests(state?.requests);
2368
2401
  const projectID = String(normalizedRoute?.projectID || "").trim();
@@ -2386,6 +2419,190 @@ function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2386
2419
  });
2387
2420
  }
2388
2421
 
2422
+ function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
2423
+ const chatID = String(selectors.chatID || "").trim();
2424
+ const messageID = intFromRawAllowZero(selectors.messageID, 0);
2425
+ if (!chatID || messageID <= 0) {
2426
+ return [];
2427
+ }
2428
+ return findRunnerRequestsForScope(state, normalizedRoute, { chatID })
2429
+ .filter((entry) => (
2430
+ intFromRawAllowZero(entry.source_message_id, 0) === messageID
2431
+ || intFromRawAllowZero(entry.last_source_message_id, 0) === messageID
2432
+ ));
2433
+ }
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
+
2496
+ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, selectedRecord) {
2497
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2498
+ const explicitConversationID = String(parsed.conversationID || "").trim();
2499
+ if (explicitConversationID) {
2500
+ return {
2501
+ conversationID: explicitConversationID,
2502
+ replyToMessageID: intFromRawAllowZero(parsed.replyToMessageID, 0),
2503
+ anchorMessageID: 0,
2504
+ reason: "archive_conversation",
2505
+ referencedRequest: null,
2506
+ };
2507
+ }
2508
+ const chatID = String(parsed.chatID || parsed.chatId || "").trim();
2509
+ const replyToMessageID = intFromRawAllowZero(parsed.replyToMessageID, 0);
2510
+ if (!chatID || replyToMessageID <= 0) {
2511
+ return {
2512
+ conversationID: "",
2513
+ replyToMessageID,
2514
+ anchorMessageID: 0,
2515
+ reason: "",
2516
+ referencedRequest: null,
2517
+ };
2518
+ }
2519
+ const referencedRequest = safeObject(findRunnerRequestsForMessageID(state, normalizedRoute, {
2520
+ chatID,
2521
+ messageID: replyToMessageID,
2522
+ })[0]);
2523
+ const referencedConversationID = String(referencedRequest.conversation_id || "").trim();
2524
+ const anchorMessageID = intFromRawAllowZero(referencedRequest.source_message_id, 0) || replyToMessageID;
2525
+ return {
2526
+ conversationID: referencedConversationID || buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID),
2527
+ replyToMessageID,
2528
+ anchorMessageID,
2529
+ reason: referencedConversationID
2530
+ ? "reply_request_conversation"
2531
+ : Object.keys(referencedRequest).length > 0
2532
+ ? "reply_request_synthetic"
2533
+ : "reply_message_synthetic",
2534
+ referencedRequest: Object.keys(referencedRequest).length > 0 ? referencedRequest : null,
2535
+ };
2536
+ }
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
+
2389
2606
  function upsertRunnerRequest(state, requestKey, patch = {}) {
2390
2607
  const currentState = safeObject(state);
2391
2608
  const requests = normalizeBotRunnerRequests(currentState.requests);
@@ -2425,87 +2642,679 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
2425
2642
  };
2426
2643
  }
2427
2644
 
2428
- function claimRunnerRequestForHumanComment({
2645
+ async function claimRunnerRequestForHumanComment({
2646
+ normalizedRoute,
2647
+ routeKey,
2648
+ selectedRecord,
2649
+ selectedBotUsernames = [],
2650
+ normalizedIntent = "",
2651
+ runtime = null,
2652
+ }) {
2653
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2654
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
2655
+ if (!isInboundArchiveKind(commentKind)) {
2656
+ return {
2657
+ ok: false,
2658
+ reason: "non_human_comment_cannot_create_request",
2659
+ };
2660
+ }
2661
+ const requestKey = buildRunnerRequestKey({
2662
+ normalizedRoute,
2663
+ selectedRecord,
2664
+ selectedBotUsernames,
2665
+ normalizedIntent,
2666
+ });
2667
+ const currentState = loadBotRunnerState();
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);
2676
+ const conversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
2677
+ let stateForClaim = safeObject(replyChainResolution.state);
2678
+ if (
2679
+ Object.keys(referencedRequest).length > 0
2680
+ && conversationID
2681
+ && !String(referencedRequest.conversation_id || "").trim()
2682
+ && String(referencedRequest.request_key || "").trim()
2683
+ ) {
2684
+ const backfilled = upsertRunnerRequest(stateForClaim, referencedRequest.request_key, {
2685
+ conversation_id: conversationID,
2686
+ });
2687
+ stateForClaim = {
2688
+ ...stateForClaim,
2689
+ requests: backfilled.requests,
2690
+ };
2691
+ }
2692
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
2693
+ const existing = safeObject(requests[requestKey]);
2694
+ if (isFinalRunnerRequestStatus(existing.status)) {
2695
+ return {
2696
+ ok: false,
2697
+ reason: "request_already_finalized",
2698
+ requestKey,
2699
+ };
2700
+ }
2701
+ if (
2702
+ isActiveRunnerRequestStatus(existing.status)
2703
+ && String(existing.claimed_by_route || "").trim()
2704
+ && String(existing.claimed_by_route || "").trim() !== String(routeKey || "").trim()
2705
+ ) {
2706
+ return {
2707
+ ok: false,
2708
+ reason: "request_already_claimed",
2709
+ requestKey,
2710
+ };
2711
+ }
2712
+ const nowISO = new Date().toISOString();
2713
+ const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
2714
+ project_id: String(normalizedRoute?.projectID || "").trim(),
2715
+ provider: String(normalizedRoute?.provider || "").trim(),
2716
+ chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
2717
+ source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2718
+ root_comment_id: String(selectedRecord?.id || "").trim(),
2719
+ root_comment_kind: commentKind,
2720
+ conversation_id: conversationID,
2721
+ selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
2722
+ normalized_intent: String(normalizedIntent || "").trim().toLowerCase(),
2723
+ status: "claimed",
2724
+ claimed_by_route: String(routeKey || "").trim(),
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(),
2737
+ last_comment_id: String(selectedRecord?.id || "").trim(),
2738
+ last_comment_kind: commentKind,
2739
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2740
+ });
2741
+ const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
2742
+ project_id: String(normalizedRoute?.projectID || "").trim(),
2743
+ provider: String(normalizedRoute?.provider || "").trim(),
2744
+ request_key: requestKey,
2745
+ route_key: String(routeKey || "").trim(),
2746
+ conversation_id: conversationID,
2747
+ source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2748
+ comment_kind: commentKind,
2749
+ request_status: "claimed",
2750
+ });
2751
+ saveBotRunnerState({
2752
+ routes: stateForClaim.routes,
2753
+ sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
2754
+ excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
2755
+ requests: nextRequests,
2756
+ consumedComments: nextConsumedComments,
2757
+ });
2758
+ return {
2759
+ ok: true,
2760
+ requestKey,
2761
+ request,
2762
+ };
2763
+ }
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({
2429
3238
  normalizedRoute,
2430
- routeKey,
2431
- selectedRecord,
2432
- selectedBotUsernames = [],
2433
- normalizedIntent = "",
3239
+ runtime,
3240
+ requestKey,
2434
3241
  }) {
2435
- const parsed = safeObject(selectedRecord?.parsedArchive);
2436
- const commentKind = String(parsed.kind || "").trim().toLowerCase();
2437
- if (!isInboundArchiveKind(commentKind)) {
3242
+ const key = String(requestKey || "").trim();
3243
+ if (!key) {
2438
3244
  return {
2439
3245
  ok: false,
2440
- reason: "non_human_comment_cannot_create_request",
3246
+ reason: "request_key_missing",
2441
3247
  };
2442
3248
  }
2443
- const requestKey = buildRunnerRequestKey({
2444
- normalizedRoute,
2445
- selectedRecord,
2446
- selectedBotUsernames,
2447
- normalizedIntent,
2448
- });
2449
3249
  const currentState = loadBotRunnerState();
2450
- const requests = normalizeBotRunnerRequests(currentState.requests);
2451
- const existing = safeObject(requests[requestKey]);
2452
- if (isFinalRunnerRequestStatus(existing.status)) {
3250
+ const request = safeObject(normalizeBotRunnerRequests(currentState.requests)[key]);
3251
+ const rootWorkItemID = String(request.root_work_item_id || "").trim();
3252
+ if (!rootWorkItemID) {
2453
3253
  return {
2454
- ok: false,
2455
- reason: "request_already_finalized",
2456
- requestKey,
3254
+ ok: true,
3255
+ skipped: true,
3256
+ reason: "root_work_item_missing",
3257
+ request,
2457
3258
  };
2458
3259
  }
2459
- if (
2460
- isActiveRunnerRequestStatus(existing.status)
2461
- && String(existing.claimed_by_route || "").trim()
2462
- && String(existing.claimed_by_route || "").trim() !== String(routeKey || "").trim()
2463
- ) {
3260
+ const targetStatus = deriveRunnerRootWorkItemTargetStatus(request.status);
3261
+ if (!targetStatus || !runtime?.baseURL || !runtime?.token || !runtime?.actor?.user_id) {
2464
3262
  return {
2465
- ok: false,
2466
- reason: "request_already_claimed",
2467
- requestKey,
3263
+ ok: true,
3264
+ skipped: true,
3265
+ reason: !targetStatus ? "no_target_status" : "governance_runtime_unavailable",
3266
+ request,
2468
3267
  };
2469
3268
  }
2470
- const nowISO = new Date().toISOString();
2471
- const { requests: nextRequests, request } = upsertRunnerRequest(currentState, requestKey, {
2472
- project_id: String(normalizedRoute?.projectID || "").trim(),
2473
- provider: String(normalizedRoute?.provider || "").trim(),
2474
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
2475
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2476
- root_comment_id: String(selectedRecord?.id || "").trim(),
2477
- root_comment_kind: commentKind,
2478
- conversation_id: String(parsed.conversationID || "").trim(),
2479
- selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
2480
- normalized_intent: String(normalizedIntent || "").trim().toLowerCase(),
2481
- status: "claimed",
2482
- claimed_by_route: String(routeKey || "").trim(),
2483
- claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
2484
- last_comment_id: String(selectedRecord?.id || "").trim(),
2485
- last_comment_kind: commentKind,
2486
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2487
- });
2488
- const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(currentState, selectedRecord?.id, {
2489
- project_id: String(normalizedRoute?.projectID || "").trim(),
2490
- provider: String(normalizedRoute?.provider || "").trim(),
2491
- request_key: requestKey,
2492
- route_key: String(routeKey || "").trim(),
2493
- conversation_id: String(parsed.conversationID || "").trim(),
2494
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
2495
- comment_kind: commentKind,
2496
- request_status: "claimed",
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,
2497
3305
  });
2498
3306
  saveBotRunnerState({
2499
3307
  routes: currentState.routes,
2500
3308
  sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
2501
3309
  excludedComments: currentState.excludedComments || currentState.excluded_comments,
2502
3310
  requests: nextRequests,
2503
- consumedComments: nextConsumedComments,
3311
+ consumedComments: currentState.consumedComments || currentState.consumed_comments,
2504
3312
  });
2505
3313
  return {
2506
- ok: true,
2507
- requestKey,
2508
- request,
3314
+ ok: lastError === "",
3315
+ request: nextRequest,
3316
+ transitioned: transitions.length > 0 && lastError === "",
3317
+ error: lastError,
2509
3318
  };
2510
3319
  }
2511
3320
 
@@ -2836,6 +3645,30 @@ function runnerLedgerEntryMatchesProject(entryRaw, normalizedRoute, requestIndex
2836
3645
  );
2837
3646
  }
2838
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
+
2839
3672
  function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRoute, serverRequests = [], serverCommentStates = []) {
2840
3673
  const state = safeObject(currentState);
2841
3674
  const normalizedRequests = normalizeBotRunnerRequests(state.requests);
@@ -2859,7 +3692,12 @@ function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRo
2859
3692
  const request = safeObject(requestRaw);
2860
3693
  const requestKey = String(request.request_key || request.requestKey || "").trim();
2861
3694
  if (!requestKey) continue;
2862
- 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];
2863
3701
  }
2864
3702
 
2865
3703
  const requestIndex = normalizeBotRunnerRequests(nextRequests);
@@ -4843,6 +5681,10 @@ async function createProjectWorkItem(params) {
4843
5681
  return createProjectWorkItemImpl(params, buildRunnerDataDeps());
4844
5682
  }
4845
5683
 
5684
+ async function transitionProjectWorkItem(params) {
5685
+ return transitionProjectWorkItemImpl(params, buildRunnerDataDeps());
5686
+ }
5687
+
4846
5688
  async function createProjectEvidence(params) {
4847
5689
  return createProjectEvidenceImpl(params, buildRunnerDataDeps());
4848
5690
  }
@@ -4851,6 +5693,10 @@ async function createWorkItemThread(params) {
4851
5693
  return createWorkItemThreadImpl(params, buildRunnerDataDeps());
4852
5694
  }
4853
5695
 
5696
+ async function listWorkItemThreads(params) {
5697
+ return listWorkItemThreadsImpl(params, buildRunnerDataDeps());
5698
+ }
5699
+
4854
5700
  async function linkWorkItemEvidence(params) {
4855
5701
  return linkWorkItemEvidenceImpl(params, buildRunnerDataDeps());
4856
5702
  }
@@ -5149,20 +5995,85 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
5149
5995
  return {
5150
5996
  status: String(entry.status || "").trim(),
5151
5997
  normalized_intent: String(entry.normalized_intent || "").trim(),
5998
+ conversation_id: String(entry.conversation_id || "").trim(),
5999
+ closed_reason: String(entry.closed_reason || "").trim(),
5152
6000
  claimed_at: firstNonEmptyString([entry.claimed_at, entry.started_at]),
5153
6001
  started_at: firstNonEmptyString([entry.started_at, entry.claimed_at]),
5154
6002
  updated_at: String(entry.updated_at || "").trim(),
5155
6003
  source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
6004
+ last_source_message_id: intFromRawAllowZero(entry.last_source_message_id, 0) || undefined,
5156
6005
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
5157
6006
  .map((value) => normalizeTelegramMentionUsername(value))
5158
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,
5159
6016
  };
5160
6017
  }
5161
6018
 
5162
- function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
6019
+ function isInformationalRunnerRequestIntent(intentType) {
6020
+ return new Set([
6021
+ "status_query",
6022
+ "bot_role_query",
6023
+ "workspace_query",
6024
+ "explanation_query",
6025
+ "small_talk",
6026
+ ]).has(String(intentType || "").trim().toLowerCase());
6027
+ }
6028
+
6029
+ function requestEligibleForStatusLookup(entryRaw, routeKey, selfBotUsername, currentMessageID) {
6030
+ const entry = safeObject(entryRaw);
6031
+ const sourceMessageID = intFromRawAllowZero(entry.source_message_id, 0);
6032
+ const lastSourceMessageID = intFromRawAllowZero(entry.last_source_message_id, 0);
6033
+ if (
6034
+ (currentMessageID > 0 && sourceMessageID === currentMessageID)
6035
+ || (currentMessageID > 0 && lastSourceMessageID === currentMessageID)
6036
+ ) {
6037
+ return false;
6038
+ }
6039
+ if (String(entry.claimed_by_route || "").trim() === routeKey) {
6040
+ return true;
6041
+ }
6042
+ if (!selfBotUsername) {
6043
+ return true;
6044
+ }
6045
+ return ensureArray(entry.selected_bot_usernames)
6046
+ .map((value) => normalizeTelegramMentionUsername(value))
6047
+ .filter(Boolean)
6048
+ .includes(selfBotUsername);
6049
+ }
6050
+
6051
+ function pickPreferredStatusLookupRequest(entries = []) {
6052
+ const candidates = ensureArray(entries).map((entry) => safeObject(entry)).filter((entry) => Object.keys(entry).length > 0);
6053
+ if (!candidates.length) {
6054
+ return null;
6055
+ }
6056
+ const activeNonInformational = candidates.filter((entry) => (
6057
+ isActiveRunnerRequestStatus(entry.status)
6058
+ && !isInformationalRunnerRequestIntent(entry.normalized_intent)
6059
+ ));
6060
+ if (activeNonInformational.length) {
6061
+ return activeNonInformational[0];
6062
+ }
6063
+ const nonInformational = candidates.filter((entry) => !isInformationalRunnerRequestIntent(entry.normalized_intent));
6064
+ if (nonInformational.length) {
6065
+ return nonInformational[0];
6066
+ }
6067
+ const activeAny = candidates.filter((entry) => isActiveRunnerRequestStatus(entry.status));
6068
+ if (activeAny.length) {
6069
+ return activeAny[0];
6070
+ }
6071
+ return candidates[0];
6072
+ }
6073
+
6074
+ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runnerStateOverride = null }) {
5163
6075
  const parsed = safeObject(selectedRecord?.parsedArchive);
5164
6076
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5165
- const currentConversationID = String(parsed.conversationID || "").trim();
5166
6077
  const currentChatID = String(parsed.chatID || parsed.chatId || "").trim();
5167
6078
  const routeKey = runnerRouteKey(route);
5168
6079
  const selfBotUsername = normalizeTelegramMentionUsername(firstNonEmptyString([
@@ -5179,34 +6090,80 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5179
6090
  && activeSourceMessageID > 0
5180
6091
  && currentMessageID === activeSourceMessageID
5181
6092
  );
6093
+ let runnerState = safeObject(runnerStateOverride);
6094
+ if (!Object.keys(runnerState).length) {
6095
+ runnerState = { requests: {} };
6096
+ try {
6097
+ runnerState = loadBotRunnerState();
6098
+ } catch {}
6099
+ }
6100
+ const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
6101
+ const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
6102
+ const activeRequestKey = String(safeObject(routeState).active_request_key || "").trim();
6103
+ const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
6104
+ entry,
6105
+ routeKey,
6106
+ selfBotUsername,
6107
+ currentMessageID,
6108
+ );
6109
+ const referencedRequestCandidate = safeObject(replyChainContext.referencedRequest);
5182
6110
  let relatedActiveRequest = null;
5183
- try {
5184
- const runnerState = loadBotRunnerState();
5185
- const selectors = currentConversationID
5186
- ? { conversationID: currentConversationID, chatID: currentChatID }
5187
- : { chatID: currentChatID };
5188
- let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
5189
- if (!scopedRequests.length && currentConversationID) {
5190
- scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
5191
- }
5192
- relatedActiveRequest = scopedRequests
5193
- .filter((entry) => isActiveRunnerRequestStatus(entry.status))
5194
- .filter((entry) => intFromRawAllowZero(entry.source_message_id, 0) !== currentMessageID)
5195
- .filter((entry) => {
5196
- if (String(entry.claimed_by_route || "").trim() === routeKey) return true;
5197
- if (!selfBotUsername) return true;
5198
- return ensureArray(entry.selected_bot_usernames)
5199
- .map((value) => normalizeTelegramMentionUsername(value))
5200
- .filter(Boolean)
5201
- .includes(selfBotUsername);
5202
- })[0] || null;
5203
- } catch {}
6111
+ let relatedRequest = null;
6112
+ const selectors = currentConversationID
6113
+ ? { conversationID: currentConversationID, chatID: currentChatID }
6114
+ : { chatID: currentChatID };
6115
+ let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
6116
+ if (!scopedRequests.length && currentConversationID) {
6117
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
6118
+ }
6119
+ if (!scopedRequests.length && activeRequestKey) {
6120
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
6121
+ }
6122
+ const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
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
6134
+ .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
6135
+ relatedRequest = pickPreferredStatusLookupRequest(statusLookupCandidates);
5204
6136
  const lastAction = String(safeObject(routeState).last_action || "").trim();
5205
6137
  const lastReason = String(safeObject(routeState).last_reason || "").trim();
5206
6138
  const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
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);
6146
+ const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
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;
5207
6156
  return {
5208
6157
  kind: "runner_status",
5209
- status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest ? "running" : "idle",
6158
+ status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
6159
+ ? "running"
6160
+ : String(safeObject(relatedRequest).status || "").trim() || "idle",
6161
+ resolved_conversation_id: currentConversationID,
6162
+ reply_chain_resolution: {
6163
+ reason: String(replyChainContext.reason || "").trim(),
6164
+ reply_to_message_id: intFromRawAllowZero(replyChainContext.replyToMessageID, 0) || undefined,
6165
+ anchor_message_id: intFromRawAllowZero(replyChainContext.anchorMessageID, 0) || undefined,
6166
+ },
5210
6167
  self_busy_filtered: selfBusyFiltered,
5211
6168
  active_execution: activeExecution.active && !selfBusyFiltered
5212
6169
  ? {
@@ -5218,6 +6175,28 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord }) {
5218
6175
  }
5219
6176
  : null,
5220
6177
  related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
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,
6194
+ route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
6195
+ ? {
6196
+ ids: routeWorkItemIDs,
6197
+ titles: routeWorkItemTitles,
6198
+ }
6199
+ : null,
5221
6200
  last_route_result: {
5222
6201
  action: lastAction,
5223
6202
  reason: lastReason,
@@ -5278,6 +6257,13 @@ async function resolveInformationalQueryReply({
5278
6257
  };
5279
6258
  }
5280
6259
  if (normalizedIntentType === "status_query") {
6260
+ let hydratedRunnerState = null;
6261
+ try {
6262
+ hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
6263
+ normalizedRoute: route,
6264
+ runtime,
6265
+ });
6266
+ } catch {}
5281
6267
  return {
5282
6268
  handled: true,
5283
6269
  source: "runner.status",
@@ -5287,6 +6273,7 @@ async function resolveInformationalQueryReply({
5287
6273
  route,
5288
6274
  routeState,
5289
6275
  selectedRecord,
6276
+ runnerStateOverride: hydratedRunnerState,
5290
6277
  }),
5291
6278
  };
5292
6279
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
@@ -5427,14 +6414,18 @@ function emptyRunnerActiveExecutionPatch() {
5427
6414
  active_request_key: "",
5428
6415
  active_started_at: "",
5429
6416
  active_heartbeat_at: "",
6417
+ active_root_work_item_id: "",
6418
+ active_root_work_item_title: "",
6419
+ active_root_work_item_status: "",
5430
6420
  active_runner_pid: undefined,
5431
6421
  active_execution_token: "",
5432
6422
  };
5433
6423
  }
5434
6424
 
5435
- function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
6425
+ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "", rootWorkItem = {}) {
5436
6426
  const nowISO = new Date().toISOString();
5437
6427
  const parsed = safeObject(selectedRecord?.parsedArchive);
6428
+ const root = safeObject(rootWorkItem);
5438
6429
  return {
5439
6430
  active_comment_id: String(selectedRecord?.id || "").trim(),
5440
6431
  active_comment_created_at: firstNonEmptyString([selectedRecord?.createdAt, selectedRecord?.updatedAt]),
@@ -5442,6 +6433,9 @@ function buildRunnerActiveExecutionPatch(selectedRecord, requestKey = "") {
5442
6433
  active_request_key: String(requestKey || "").trim(),
5443
6434
  active_started_at: nowISO,
5444
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),
5445
6439
  active_runner_pid: process.pid,
5446
6440
  active_execution_token: `${Date.now()}-${process.pid}-${String(selectedRecord?.id || "").trim()}`,
5447
6441
  };
@@ -5921,7 +6915,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
5921
6915
  }
5922
6916
  return null;
5923
6917
  };
5924
- const prepareRunnerRequestClaim = (selectedRecord, selectedResponderSelectors = []) => {
6918
+ const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = []) => {
5925
6919
  const parsed = safeObject(selectedRecord?.parsedArchive);
5926
6920
  const kind = String(parsed.kind || "").trim().toLowerCase();
5927
6921
  if (kind === "bot_reply") {
@@ -5951,6 +6945,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
5951
6945
  routeKey,
5952
6946
  selectedRecord,
5953
6947
  selectedBotUsernames: selectedResponderSelectors,
6948
+ runtime,
5954
6949
  });
5955
6950
  };
5956
6951
  if (deferExecution) {
@@ -6037,7 +7032,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6037
7032
  });
6038
7033
  continue;
6039
7034
  }
6040
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
7035
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, adjudication.selected_bot_usernames);
6041
7036
  if (!requestClaim.ok) {
6042
7037
  await syncRunnerRequestLedgerForProjectToServer({
6043
7038
  normalizedRoute,
@@ -6062,9 +7057,64 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6062
7057
  });
6063
7058
  continue;
6064
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);
6065
7108
  saveRunnerRouteState(routeKey, {
6066
- ...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
+ }),
6067
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(),
6068
7118
  });
6069
7119
  await syncRunnerRequestLedgerForProjectToServer({
6070
7120
  normalizedRoute,
@@ -6211,7 +7261,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6211
7261
  continue;
6212
7262
  }
6213
7263
  const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
6214
- const requestClaim = prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
7264
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord, inlineAdjudication.selected_bot_usernames);
6215
7265
  if (!requestClaim.ok) {
6216
7266
  await syncRunnerRequestLedgerForProjectToServer({
6217
7267
  normalizedRoute,
@@ -6236,11 +7286,67 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6236
7286
  });
6237
7287
  continue;
6238
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);
6239
7337
  saveRunnerRouteState(routeKey, {
6240
- 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
+ }),
6241
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(),
6242
7347
  });
6243
7348
  if (String(requestClaim.requestKey || "").trim()) {
7349
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6244
7350
  markRunnerRequestLifecycle({
6245
7351
  normalizedRoute,
6246
7352
  requestKey: requestClaim.requestKey,
@@ -6248,6 +7354,22 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6248
7354
  routeKey,
6249
7355
  outcome: "running",
6250
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
+ }
6251
7373
  await syncRunnerRequestLedgerForProjectToServer({
6252
7374
  normalizedRoute,
6253
7375
  runtime,
@@ -6258,7 +7380,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6258
7380
  normalizedRoute,
6259
7381
  routeState: {
6260
7382
  ...currentRouteState,
6261
- 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
+ }),
6262
7388
  },
6263
7389
  selectedRecord,
6264
7390
  pendingOrdered: pending.ordered,
@@ -6293,6 +7419,19 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6293
7419
  outcome: "skipped",
6294
7420
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
6295
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
+ }
6296
7435
  await syncRunnerRequestLedgerForProjectToServer({
6297
7436
  normalizedRoute,
6298
7437
  runtime,
@@ -6302,6 +7441,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6302
7441
  continue;
6303
7442
  }
6304
7443
  if (String(requestClaim.requestKey || "").trim()) {
7444
+ const resolvedIntentType = String(safeObject(loadBotRunnerState().routes[routeKey]).last_intent_type || "").trim();
6305
7445
  markRunnerRequestLifecycle({
6306
7446
  normalizedRoute,
6307
7447
  requestKey: requestClaim.requestKey,
@@ -6315,8 +7455,28 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
6315
7455
  nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
6316
7456
  currentBotSelector,
6317
7457
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
6318
- 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,
6319
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
+ }
6320
7480
  await syncRunnerRequestLedgerForProjectToServer({
6321
7481
  normalizedRoute,
6322
7482
  runtime,
@@ -8008,7 +9168,7 @@ async function runRunnerStop(flags) {
8008
9168
  }
8009
9169
  process.stdout.write("Detached runner stop: OK\n");
8010
9170
  process.stdout.write(`registry_file: ${payload.registry_file}\n`);
8011
- for (const entry of stopped) {
9171
+ for (const entry of ensureArray(payload.stopped)) {
8012
9172
  process.stdout.write(`stopped: ${entry.launch_id} pid=${entry.pid} routes=${entry.route_names.join(", ") || "-"}\n`);
8013
9173
  }
8014
9174
  }
@@ -8033,7 +9193,7 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
8033
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`);
8034
9194
  return payload;
8035
9195
  }
8036
- const launch = launchDetachedRunnerProcess(flags, routes, sourceCommand);
9196
+ const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
8037
9197
  const nextLaunches = {
8038
9198
  ...safeObject(registry).launches,
8039
9199
  [launch.launch_id]: launch,
@@ -8560,6 +9720,22 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8560
9720
  routeKey: deferredExecution.routeKey,
8561
9721
  outcome: "running",
8562
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
+ }
8563
9739
  await syncRunnerRequestLedgerForProjectToServer({
8564
9740
  normalizedRoute: deferredExecution.normalizedRoute,
8565
9741
  runtime: deferredExecution.runtime,
@@ -8622,6 +9798,19 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8622
9798
  outcome: "skipped",
8623
9799
  closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
8624
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
+ }
8625
9814
  await syncRunnerRequestLedgerForProjectToServer({
8626
9815
  normalizedRoute: deferredExecution.normalizedRoute,
8627
9816
  runtime: deferredExecution.runtime,
@@ -8642,6 +9831,9 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8642
9831
  };
8643
9832
  }
8644
9833
  if (String(deferredExecution.requestKey || "").trim()) {
9834
+ const resolvedIntentType = String(
9835
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
9836
+ ).trim();
8645
9837
  markRunnerRequestLifecycle({
8646
9838
  normalizedRoute: deferredExecution.normalizedRoute,
8647
9839
  requestKey: deferredExecution.requestKey,
@@ -8657,8 +9849,28 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8657
9849
  deferredExecution.bot?.username || deferredExecution.bot?.name,
8658
9850
  ),
8659
9851
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8660
- normalizedIntent: String(safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "").trim(),
9852
+ normalizedIntent: resolvedIntentType,
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,
8661
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
+ }
8662
9874
  await syncRunnerRequestLedgerForProjectToServer({
8663
9875
  normalizedRoute: deferredExecution.normalizedRoute,
8664
9876
  runtime: deferredExecution.runtime,
@@ -8687,6 +9899,19 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
8687
9899
  outcome: "error",
8688
9900
  closedReason: errorText || "execution_error",
8689
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
+ }
8690
9915
  await syncRunnerRequestLedgerForProjectToServer({
8691
9916
  normalizedRoute: deferredExecution.normalizedRoute,
8692
9917
  runtime: deferredExecution.runtime,
@@ -12966,6 +14191,8 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
12966
14191
  runnerRouteLogicalSignature,
12967
14192
  loadBotRunnerState,
12968
14193
  saveBotRunnerState,
14194
+ mergeServerRunnerRequestLedgerIntoLocalState,
14195
+ buildRunnerStatusQueryLookup,
12969
14196
  tryJsonParse,
12970
14197
  safeObject,
12971
14198
  normalizeRunnerTriggerPolicy,