metheus-governance-mcp-cli 0.2.298 → 0.2.304

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.
Files changed (29) hide show
  1. package/cli.mjs +2512 -678
  2. package/lib/local-ai-adapters.mjs +96 -9
  3. package/lib/provider-local-transport.mjs +56 -0
  4. package/lib/runner-data.mjs +155 -3
  5. package/lib/runner-delivery-archive-handoff.mjs +20 -0
  6. package/lib/runner-delivery-context.mjs +49 -8
  7. package/lib/runner-delivery.mjs +2 -0
  8. package/lib/runner-execution.mjs +46 -6
  9. package/lib/runner-helpers.mjs +210 -2
  10. package/lib/runner-local-inbound-receipts.mjs +23 -0
  11. package/lib/runner-media-transcription.mjs +232 -0
  12. package/lib/runner-media-vision.mjs +268 -0
  13. package/lib/runner-orchestration-adjudication.mjs +145 -5
  14. package/lib/runner-orchestration-decision-bundle.mjs +111 -22
  15. package/lib/runner-orchestration-entrypoints.mjs +229 -53
  16. package/lib/runner-orchestration-selected-record-context.mjs +438 -8
  17. package/lib/runner-orchestration-selected-record-delivery-handoff.mjs +2 -0
  18. package/lib/runner-orchestration-selected-record-outcome.mjs +1 -1
  19. package/lib/runner-orchestration-selected-record-preparation.mjs +63 -9
  20. package/lib/runner-orchestration-selected-record-reply-outcome.mjs +169 -4
  21. package/lib/runner-orchestration-selected-record-terminal-outcome.mjs +17 -1
  22. package/lib/runner-orchestration-step-inputs.mjs +87 -3
  23. package/lib/runner-orchestration-step-requirements.mjs +274 -0
  24. package/lib/runner-orchestration-visibility.mjs +9 -3
  25. package/lib/runner-orchestration.mjs +399 -50
  26. package/lib/runner-runtime.mjs +299 -9
  27. package/lib/selftest-runner-scenarios.mjs +3565 -280
  28. package/lib/selftest-telegram-e2e.mjs +173 -24
  29. package/package.json +2 -2
package/cli.mjs CHANGED
@@ -137,6 +137,7 @@ import {
137
137
  import {
138
138
  applyPendingAgeSelection,
139
139
  buildCanonicalHumanInboundKey as buildRunnerCanonicalHumanInboundKey,
140
+ buildTelegramMediaPlaceholder,
140
141
  buildTelegramBotReplyEnvelope,
141
142
  buildTelegramMessageEnvelopeFromParsedArchive as buildRunnerTelegramMessageEnvelopeFromParsedArchive,
142
143
  buildRunnerRouteDuplicateStateFromComment,
@@ -147,11 +148,13 @@ import {
147
148
  isTelegramLocalInboundEnvelopeForRoute,
148
149
  isInboundArchiveKind,
149
150
  normalizeTelegramMessageEnvelope as normalizeRunnerTelegramMessageEnvelope,
151
+ normalizeTelegramMessageAttachments,
150
152
  normalizeArchiveCommentRecord,
151
153
  selectPendingArchiveComments,
152
154
  printRunnerResult,
153
155
  } from "./lib/runner-helpers.mjs";
154
156
  import {
157
+ normalizeRunnerRecentLocalInboundReceiptEntry,
155
158
  normalizeRunnerRecentLocalInboundReceiptMap,
156
159
  } from "./lib/runner-local-inbound-receipts.mjs";
157
160
  import {
@@ -159,30 +162,40 @@ import {
159
162
  validateRunnerConversationDecisionBundle,
160
163
  } from "./lib/runner-orchestration-decision-bundle.mjs";
161
164
  import {
165
+ transcribeRunnerTelegramAudioAttachments,
166
+ } from "./lib/runner-media-transcription.mjs";
167
+ import {
168
+ analyzeRunnerTelegramImageAttachments,
169
+ } from "./lib/runner-media-vision.mjs";
170
+ import {
171
+ createProjectPrivateChat as createProjectPrivateChatImpl,
162
172
  createProjectContextItem as createProjectContextItemImpl,
163
- createProjectEvidence as createProjectEvidenceImpl,
164
- createProjectWorkItem as createProjectWorkItemImpl,
165
- createThreadComment as createThreadCommentImpl,
166
- createWorkItemThread as createWorkItemThreadImpl,
167
- deleteProjectContextItem as deleteProjectContextItemImpl,
168
- discoverArchiveThreadForDestination as discoverArchiveThreadForDestinationImpl,
169
- linkWorkItemEvidence as linkWorkItemEvidenceImpl,
170
- listProjectChatDestinations as listProjectChatDestinationsImpl,
171
- listProjectContextItems as listProjectContextItemsImpl,
172
- listProjectCtxpackFiles as listProjectCtxpackFilesImpl,
173
- listProjectRunnerRequestCommentStates as listProjectRunnerRequestCommentStatesImpl,
174
- listProjectRunnerRequests as listProjectRunnerRequestsImpl,
175
- listWorkItemThreads as listWorkItemThreadsImpl,
176
- listThreadComments as listThreadCommentsImpl,
177
- listThreadCommentsTail as listThreadCommentsTailImpl,
173
+ createProjectEvidence as createProjectEvidenceImpl,
174
+ createProjectWorkItem as createProjectWorkItemImpl,
175
+ createThreadComment as createThreadCommentImpl,
176
+ getProjectRunnerSettings as getProjectRunnerSettingsImpl,
177
+ createWorkItemThread as createWorkItemThreadImpl,
178
+ deleteProjectContextItem as deleteProjectContextItemImpl,
179
+ discoverArchiveThreadForDestination as discoverArchiveThreadForDestinationImpl,
180
+ linkWorkItemEvidence as linkWorkItemEvidenceImpl,
181
+ listProjectChatDestinations as listProjectChatDestinationsImpl,
182
+ listProjectContextItems as listProjectContextItemsImpl,
183
+ listProjectCtxpackFiles as listProjectCtxpackFilesImpl,
184
+ listProjectPrivateChats as listProjectPrivateChatsImpl,
185
+ listProjectRunnerRequestCommentStates as listProjectRunnerRequestCommentStatesImpl,
186
+ listProjectRunnerRequests as listProjectRunnerRequestsImpl,
187
+ listWorkItemThreads as listWorkItemThreadsImpl,
188
+ listThreadComments as listThreadCommentsImpl,
189
+ listThreadCommentsTail as listThreadCommentsTailImpl,
178
190
  listUserBotsForRunner as listUserBotsForRunnerImpl,
179
191
  selectProjectChatDestination as selectProjectChatDestinationImpl,
180
192
  selectRunnerBot as selectRunnerBotImpl,
181
- transitionProjectWorkItem as transitionProjectWorkItemImpl,
182
- upsertProjectRunnerRequest as upsertProjectRunnerRequestImpl,
183
- upsertProjectRunnerRequestCommentState as upsertProjectRunnerRequestCommentStateImpl,
184
- updateProjectContextItem as updateProjectContextItemImpl,
185
- } from "./lib/runner-data.mjs";
193
+ transitionProjectWorkItem as transitionProjectWorkItemImpl,
194
+ upsertProjectRunnerRequest as upsertProjectRunnerRequestImpl,
195
+ upsertProjectRunnerRequestCommentState as upsertProjectRunnerRequestCommentStateImpl,
196
+ updateProjectPrivateChat as updateProjectPrivateChatImpl,
197
+ updateProjectContextItem as updateProjectContextItemImpl,
198
+ } from "./lib/runner-data.mjs";
186
199
  import {
187
200
  evaluateTelegramRunnerTrigger,
188
201
  resolveRunnerPrecomputedTriggerDecision,
@@ -239,13 +252,14 @@ import {
239
252
  formatBotReplyArchiveComment,
240
253
  performLocalBotDelivery,
241
254
  } from "./lib/runner-delivery.mjs";
242
- import {
243
- resolveRunnerExecutionPlan,
244
- resolveRunnerExecutionPlanForRole,
245
- resolveRunnerRoleProfile,
246
- resolveRunnerWorkspaceSelection,
247
- runRunnerAIExecution,
248
- } from "./lib/runner-execution.mjs";
255
+ import {
256
+ resolveRunnerExecutionPlan,
257
+ resolveRunnerExecutionPlanForRole,
258
+ resolveRunnerLocalAIExecutionProfile,
259
+ resolveRunnerRoleProfile,
260
+ resolveRunnerWorkspaceSelection,
261
+ runRunnerAIExecution,
262
+ } from "./lib/runner-execution.mjs";
249
263
  import {
250
264
  buildRunnerResponderAdjudicationFromConversationContext,
251
265
  buildRunnerResponderAdjudicationFromHumanIntent,
@@ -1430,13 +1444,14 @@ function botRunnerConfigTemplate() {
1430
1444
  role_profile: "monitor",
1431
1445
  },
1432
1446
  },
1433
- routes: [
1434
- {
1435
- name: "telegram-monitor",
1436
- enabled: false,
1437
- project_id: "<project_uuid>",
1438
- provider: "telegram",
1439
- role: "monitor",
1447
+ routes: [
1448
+ {
1449
+ name: "telegram-monitor",
1450
+ enabled: false,
1451
+ route_kind: "room",
1452
+ project_id: "<project_uuid>",
1453
+ provider: "telegram",
1454
+ role: "monitor",
1440
1455
  role_profile: "monitor",
1441
1456
  server_bot_name: "",
1442
1457
  server_bot_id: "",
@@ -1479,13 +1494,14 @@ function serializeBotRunnerWorkspaceRegistry(projectMappings) {
1479
1494
  };
1480
1495
  }
1481
1496
 
1482
- function serializeRunnerRoute(route) {
1483
- const normalized = normalizeRunnerRoute(route);
1484
- return {
1485
- name: normalized.name,
1486
- enabled: normalized.enabled,
1487
- project_id: normalized.projectID,
1488
- provider: normalized.provider,
1497
+ function serializeRunnerRoute(route) {
1498
+ const normalized = normalizeRunnerRoute(route);
1499
+ return {
1500
+ name: normalized.name,
1501
+ enabled: normalized.enabled,
1502
+ route_kind: normalized.routeKind,
1503
+ project_id: normalized.projectID,
1504
+ provider: normalized.provider,
1489
1505
  role: normalized.role,
1490
1506
  ...(normalized.roleProfile ? { role_profile: normalized.roleProfile } : {}),
1491
1507
  server_bot_name: normalized.botName,
@@ -1872,19 +1888,32 @@ function rememberProjectWorkspaceMapping({ projectID, workspaceDir, source }) {
1872
1888
  };
1873
1889
  }
1874
1890
 
1875
- function parseRunnerStateRouteKey(routeKey) {
1876
- const text = String(routeKey || "").trim();
1877
- if (!text) return null;
1878
- const parts = text.split("::");
1879
- if (parts.length < 6) return null;
1880
- return {
1881
- raw: text,
1882
- name: String(parts[0] || "").trim(),
1883
- projectID: String(parts[1] || "").trim(),
1884
- provider: normalizeBotProvider(parts[2]),
1885
- role: normalizeBotRole(parts[3]),
1886
- botSelector: String(parts[4] || "").trim(),
1887
- destinationSelector: parts.slice(5).join("::").trim(),
1891
+ function parseRunnerStateRouteKey(routeKey) {
1892
+ const text = String(routeKey || "").trim();
1893
+ if (!text) return null;
1894
+ const parts = text.split("::");
1895
+ if (parts.length < 6) return null;
1896
+ if (parts.length >= 7) {
1897
+ return {
1898
+ raw: text,
1899
+ name: String(parts[0] || "").trim(),
1900
+ projectID: String(parts[1] || "").trim(),
1901
+ provider: normalizeBotProvider(parts[2]),
1902
+ routeKind: normalizeRunnerRouteKind(parts[3], "room"),
1903
+ role: normalizeBotRole(parts[4]),
1904
+ botSelector: String(parts[5] || "").trim(),
1905
+ destinationSelector: parts.slice(6).join("::").trim(),
1906
+ };
1907
+ }
1908
+ return {
1909
+ raw: text,
1910
+ name: String(parts[0] || "").trim(),
1911
+ projectID: String(parts[1] || "").trim(),
1912
+ provider: normalizeBotProvider(parts[2]),
1913
+ routeKind: "room",
1914
+ role: normalizeBotRole(parts[3]),
1915
+ botSelector: String(parts[4] || "").trim(),
1916
+ destinationSelector: parts.slice(5).join("::").trim(),
1888
1917
  };
1889
1918
  }
1890
1919
 
@@ -1920,19 +1949,22 @@ function findConfiguredRoutesForAnonymousStateKey(parsedKey, runnerConfig) {
1920
1949
  const enabledRoutes = ensureArray(runnerConfig?.routes)
1921
1950
  .map((route) => normalizeRunnerRoute(route))
1922
1951
  .filter((route) => route.enabled);
1923
- return enabledRoutes.filter((route) => {
1924
- if (route.projectID !== routeKey.projectID) return false;
1925
- if (route.provider !== routeKey.provider) return false;
1926
- if (route.role !== routeKey.role) return false;
1927
- if (!runnerStateSelectorMatchesRoute(routeKey.botSelector, route.botID, route.botName)) {
1928
- return false;
1929
- }
1930
- if (!runnerStateSelectorMatchesRoute(routeKey.destinationSelector, route.destinationID, route.destinationLabel)) {
1931
- return false;
1932
- }
1933
- return true;
1934
- });
1935
- }
1952
+ return enabledRoutes.filter((route) => {
1953
+ if (route.projectID !== routeKey.projectID) return false;
1954
+ if (route.provider !== routeKey.provider) return false;
1955
+ if ((route.routeKind || "room") !== (routeKey.routeKind || "room")) return false;
1956
+ if (route.role !== routeKey.role) return false;
1957
+ if (!runnerStateSelectorMatchesRoute(routeKey.botSelector, route.botID, route.botName)) {
1958
+ return false;
1959
+ }
1960
+ const routeTargetID = route.routeKind === "dm" ? "direct-messages" : route.destinationID;
1961
+ const routeTargetLabel = route.routeKind === "dm" ? describeRunnerRouteTargetLabel(route) : route.destinationLabel;
1962
+ if (!runnerStateSelectorMatchesRoute(routeKey.destinationSelector, routeTargetID, routeTargetLabel)) {
1963
+ return false;
1964
+ }
1965
+ return true;
1966
+ });
1967
+ }
1936
1968
 
1937
1969
  function configuredRunnerRouteKeys(runnerConfig) {
1938
1970
  return new Set(
@@ -2979,10 +3011,10 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2979
3011
  source_bot_username: entry.source_message_bot_username || entry.sourceMessageBotUsername,
2980
3012
  }
2981
3013
  : {};
2982
- const normalizedSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
2983
- ...safeObject(
2984
- normalizeRunnerTelegramMessageEnvelope(
2985
- entry.source_message_envelope
3014
+ const normalizedSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
3015
+ ...safeObject(
3016
+ normalizeRunnerTelegramMessageEnvelope(
3017
+ entry.source_message_envelope
2986
3018
  || entry.sourceMessageEnvelope
2987
3019
  || fallbackSourceMessageEnvelope,
2988
3020
  ),
@@ -3028,10 +3060,155 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
3028
3060
  source_bot_username: firstNonEmptyString([
3029
3061
  safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).source_bot_username,
3030
3062
  safeObject(entry.source_message_envelope || entry.sourceMessageEnvelope).sourceBotUsername,
3031
- entry.source_message_bot_username,
3032
- entry.sourceMessageBotUsername,
3033
- ]),
3034
- });
3063
+ entry.source_message_bot_username,
3064
+ entry.sourceMessageBotUsername,
3065
+ ]),
3066
+ });
3067
+ const normalizedRootSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
3068
+ ...safeObject(
3069
+ normalizeRunnerTelegramMessageEnvelope(
3070
+ entry.root_source_message_envelope
3071
+ || entry.rootSourceMessageEnvelope
3072
+ || normalizedSourceMessageEnvelope,
3073
+ ),
3074
+ ),
3075
+ chat_id: firstNonEmptyString([
3076
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).chat_id,
3077
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).chatID,
3078
+ normalizedSourceMessageEnvelope.chat_id,
3079
+ entry.chat_id,
3080
+ entry.chatID,
3081
+ ]),
3082
+ message_id: intFromRawAllowZero(
3083
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).message_id
3084
+ || safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).messageID
3085
+ || entry.root_source_message_id
3086
+ || entry.rootSourceMessageID
3087
+ || entry.source_message_id
3088
+ || entry.sourceMessageID,
3089
+ 0,
3090
+ ) || undefined,
3091
+ message_thread_id: intFromRawAllowZero(
3092
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).message_thread_id
3093
+ || safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).messageThreadID
3094
+ || entry.root_source_message_thread_id
3095
+ || entry.rootSourceMessageThreadID
3096
+ || entry.source_message_thread_id
3097
+ || entry.sourceMessageThreadID,
3098
+ 0,
3099
+ ) || undefined,
3100
+ body: firstNonEmptyString([
3101
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).body,
3102
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).text,
3103
+ entry.root_source_message_body,
3104
+ entry.rootSourceMessageBody,
3105
+ entry.source_message_body,
3106
+ entry.sourceMessageBody,
3107
+ normalizedSourceMessageEnvelope.body,
3108
+ ]),
3109
+ source_origin: firstNonEmptyString([
3110
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).source_origin,
3111
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).sourceOrigin,
3112
+ entry.root_source_message_origin,
3113
+ entry.rootSourceMessageOrigin,
3114
+ entry.source_message_origin,
3115
+ entry.sourceMessageOrigin,
3116
+ normalizedSourceMessageEnvelope.source_origin,
3117
+ ]),
3118
+ source_route_key: firstNonEmptyString([
3119
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).source_route_key,
3120
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).sourceRouteKey,
3121
+ entry.root_source_message_route_key,
3122
+ entry.rootSourceMessageRouteKey,
3123
+ entry.source_message_route_key,
3124
+ entry.sourceMessageRouteKey,
3125
+ normalizedSourceMessageEnvelope.source_route_key,
3126
+ ]),
3127
+ source_bot_username: firstNonEmptyString([
3128
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).source_bot_username,
3129
+ safeObject(entry.root_source_message_envelope || entry.rootSourceMessageEnvelope).sourceBotUsername,
3130
+ entry.root_source_message_bot_username,
3131
+ entry.rootSourceMessageBotUsername,
3132
+ entry.source_message_bot_username,
3133
+ entry.sourceMessageBotUsername,
3134
+ normalizedSourceMessageEnvelope.source_bot_username,
3135
+ ]),
3136
+ });
3137
+ const normalizedCurrentSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope({
3138
+ ...safeObject(
3139
+ normalizeRunnerTelegramMessageEnvelope(
3140
+ entry.current_source_message_envelope
3141
+ || entry.currentSourceMessageEnvelope
3142
+ || normalizedSourceMessageEnvelope,
3143
+ ),
3144
+ ),
3145
+ chat_id: firstNonEmptyString([
3146
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).chat_id,
3147
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).chatID,
3148
+ normalizedSourceMessageEnvelope.chat_id,
3149
+ entry.chat_id,
3150
+ entry.chatID,
3151
+ ]),
3152
+ message_id: intFromRawAllowZero(
3153
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).message_id
3154
+ || safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).messageID
3155
+ || entry.current_source_message_id
3156
+ || entry.currentSourceMessageID
3157
+ || entry.last_source_message_id
3158
+ || entry.lastSourceMessageID
3159
+ || normalizedSourceMessageEnvelope.message_id,
3160
+ 0,
3161
+ ) || undefined,
3162
+ message_thread_id: intFromRawAllowZero(
3163
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).message_thread_id
3164
+ || safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).messageThreadID
3165
+ || entry.current_source_message_thread_id
3166
+ || entry.currentSourceMessageThreadID
3167
+ || entry.last_source_message_thread_id
3168
+ || entry.lastSourceMessageThreadID
3169
+ || normalizedSourceMessageEnvelope.message_thread_id,
3170
+ 0,
3171
+ ) || undefined,
3172
+ body: firstNonEmptyString([
3173
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).body,
3174
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).text,
3175
+ entry.current_source_message_body,
3176
+ entry.currentSourceMessageBody,
3177
+ entry.source_message_body,
3178
+ entry.sourceMessageBody,
3179
+ normalizedSourceMessageEnvelope.body,
3180
+ ]),
3181
+ source_origin: firstNonEmptyString([
3182
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).source_origin,
3183
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).sourceOrigin,
3184
+ entry.current_source_message_origin,
3185
+ entry.currentSourceMessageOrigin,
3186
+ entry.source_message_origin,
3187
+ entry.sourceMessageOrigin,
3188
+ normalizedSourceMessageEnvelope.source_origin,
3189
+ ]),
3190
+ source_route_key: firstNonEmptyString([
3191
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).source_route_key,
3192
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).sourceRouteKey,
3193
+ entry.current_source_message_route_key,
3194
+ entry.currentSourceMessageRouteKey,
3195
+ entry.source_message_route_key,
3196
+ entry.sourceMessageRouteKey,
3197
+ normalizedSourceMessageEnvelope.source_route_key,
3198
+ ]),
3199
+ source_bot_username: firstNonEmptyString([
3200
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).source_bot_username,
3201
+ safeObject(entry.current_source_message_envelope || entry.currentSourceMessageEnvelope).sourceBotUsername,
3202
+ entry.current_source_message_bot_username,
3203
+ entry.currentSourceMessageBotUsername,
3204
+ entry.source_message_bot_username,
3205
+ entry.sourceMessageBotUsername,
3206
+ normalizedSourceMessageEnvelope.source_bot_username,
3207
+ ]),
3208
+ });
3209
+ const normalizedReplyChainContext = normalizeRunnerReplyChainContext(
3210
+ entry.reply_chain_context || entry.replyChainContext,
3211
+ );
3035
3212
  normalized[requestKey] = {
3036
3213
  request_key: requestKey,
3037
3214
  project_id: String(entry.project_id || entry.projectID || "").trim(),
@@ -3075,16 +3252,108 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
3075
3252
  || normalizedSourceMessageEnvelope.source_route_key
3076
3253
  || "",
3077
3254
  ).trim(),
3078
- source_message_bot_username: normalizeTelegramMentionUsername(
3079
- entry.source_message_bot_username
3080
- || entry.sourceMessageBotUsername
3081
- || normalizedSourceMessageEnvelope.source_bot_username,
3082
- ),
3083
- source_message_envelope: normalizedSourceMessageEnvelope,
3084
- root_comment_id: String(entry.root_comment_id || entry.rootCommentID || "").trim(),
3085
- root_comment_kind: String(entry.root_comment_kind || entry.rootCommentKind || "").trim().toLowerCase(),
3086
- conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
3087
- reply_chain_context: normalizeRunnerReplyChainContext(entry.reply_chain_context || entry.replyChainContext),
3255
+ source_message_bot_username: normalizeTelegramMentionUsername(
3256
+ entry.source_message_bot_username
3257
+ || entry.sourceMessageBotUsername
3258
+ || normalizedSourceMessageEnvelope.source_bot_username,
3259
+ ),
3260
+ source_message_envelope: normalizedSourceMessageEnvelope,
3261
+ root_source_message_id: intFromRawAllowZero(
3262
+ entry.root_source_message_id
3263
+ || entry.rootSourceMessageID
3264
+ || entry.source_message_id
3265
+ || entry.sourceMessageID
3266
+ || normalizedRootSourceMessageEnvelope.message_id,
3267
+ 0,
3268
+ ) || undefined,
3269
+ root_source_message_thread_id: intFromRawAllowZero(
3270
+ entry.root_source_message_thread_id
3271
+ || entry.rootSourceMessageThreadID
3272
+ || entry.source_message_thread_id
3273
+ || entry.sourceMessageThreadID
3274
+ || normalizedRootSourceMessageEnvelope.message_thread_id,
3275
+ 0,
3276
+ ) || undefined,
3277
+ root_source_message_body: firstNonEmptyString([
3278
+ entry.root_source_message_body,
3279
+ entry.rootSourceMessageBody,
3280
+ entry.source_message_body,
3281
+ entry.sourceMessageBody,
3282
+ normalizedRootSourceMessageEnvelope.body,
3283
+ ]),
3284
+ root_source_message_origin: String(
3285
+ entry.root_source_message_origin
3286
+ || entry.rootSourceMessageOrigin
3287
+ || entry.source_message_origin
3288
+ || entry.sourceMessageOrigin
3289
+ || normalizedRootSourceMessageEnvelope.source_origin
3290
+ || "",
3291
+ ).trim().toLowerCase(),
3292
+ root_source_message_route_key: String(
3293
+ entry.root_source_message_route_key
3294
+ || entry.rootSourceMessageRouteKey
3295
+ || entry.source_message_route_key
3296
+ || entry.sourceMessageRouteKey
3297
+ || normalizedRootSourceMessageEnvelope.source_route_key
3298
+ || "",
3299
+ ).trim(),
3300
+ root_source_message_bot_username: normalizeTelegramMentionUsername(
3301
+ entry.root_source_message_bot_username
3302
+ || entry.rootSourceMessageBotUsername
3303
+ || entry.source_message_bot_username
3304
+ || entry.sourceMessageBotUsername
3305
+ || normalizedRootSourceMessageEnvelope.source_bot_username,
3306
+ ),
3307
+ root_source_message_envelope: normalizedRootSourceMessageEnvelope,
3308
+ current_source_message_id: intFromRawAllowZero(
3309
+ entry.current_source_message_id
3310
+ || entry.currentSourceMessageID
3311
+ || entry.last_source_message_id
3312
+ || entry.lastSourceMessageID
3313
+ || normalizedCurrentSourceMessageEnvelope.message_id,
3314
+ 0,
3315
+ ) || undefined,
3316
+ current_source_message_thread_id: intFromRawAllowZero(
3317
+ entry.current_source_message_thread_id
3318
+ || entry.currentSourceMessageThreadID
3319
+ || entry.last_source_message_thread_id
3320
+ || entry.lastSourceMessageThreadID
3321
+ || normalizedCurrentSourceMessageEnvelope.message_thread_id,
3322
+ 0,
3323
+ ) || undefined,
3324
+ current_source_message_body: firstNonEmptyString([
3325
+ entry.current_source_message_body,
3326
+ entry.currentSourceMessageBody,
3327
+ normalizedCurrentSourceMessageEnvelope.body,
3328
+ ]),
3329
+ current_source_message_origin: String(
3330
+ entry.current_source_message_origin
3331
+ || entry.currentSourceMessageOrigin
3332
+ || normalizedCurrentSourceMessageEnvelope.source_origin
3333
+ || "",
3334
+ ).trim().toLowerCase(),
3335
+ current_source_message_route_key: String(
3336
+ entry.current_source_message_route_key
3337
+ || entry.currentSourceMessageRouteKey
3338
+ || normalizedCurrentSourceMessageEnvelope.source_route_key
3339
+ || "",
3340
+ ).trim(),
3341
+ current_source_message_bot_username: normalizeTelegramMentionUsername(
3342
+ entry.current_source_message_bot_username
3343
+ || entry.currentSourceMessageBotUsername
3344
+ || normalizedCurrentSourceMessageEnvelope.source_bot_username,
3345
+ ),
3346
+ current_source_message_envelope: normalizedCurrentSourceMessageEnvelope,
3347
+ current_turn_relation: String(
3348
+ entry.current_turn_relation
3349
+ || entry.currentTurnRelation
3350
+ || normalizedReplyChainContext.current_turn_relation
3351
+ || "",
3352
+ ).trim(),
3353
+ root_comment_id: String(entry.root_comment_id || entry.rootCommentID || "").trim(),
3354
+ root_comment_kind: String(entry.root_comment_kind || entry.rootCommentKind || "").trim().toLowerCase(),
3355
+ conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
3356
+ reply_chain_context: normalizedReplyChainContext,
3088
3357
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames || entry.selectedBotUsernames)
3089
3358
  .map((value) => normalizeTelegramMentionUsername(value))
3090
3359
  .filter(Boolean),
@@ -3095,12 +3364,25 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
3095
3364
  entry.authoritative_decision_bundle || entry.authoritativeDecisionBundle,
3096
3365
  )
3097
3366
  : {},
3367
+ last_reply_decision_bundle: Object.keys(
3368
+ safeObject(entry.last_reply_decision_bundle || entry.lastReplyDecisionBundle),
3369
+ ).length
3370
+ ? normalizeRunnerConversationDecisionBundle(
3371
+ entry.last_reply_decision_bundle || entry.lastReplyDecisionBundle,
3372
+ )
3373
+ : {},
3098
3374
  decision_bundle_validation_status: String(
3099
3375
  entry.decision_bundle_validation_status || entry.decisionBundleValidationStatus || "",
3100
3376
  ).trim().toLowerCase(),
3101
3377
  decision_bundle_validation_reason: String(
3102
3378
  entry.decision_bundle_validation_reason || entry.decisionBundleValidationReason || "",
3103
3379
  ).trim(),
3380
+ last_reply_decision_bundle_validation_status: String(
3381
+ entry.last_reply_decision_bundle_validation_status || entry.lastReplyDecisionBundleValidationStatus || "",
3382
+ ).trim().toLowerCase(),
3383
+ last_reply_decision_bundle_validation_reason: String(
3384
+ entry.last_reply_decision_bundle_validation_reason || entry.lastReplyDecisionBundleValidationReason || "",
3385
+ ).trim(),
3104
3386
  conversation_allowed_responders: ensureArray(entry.conversation_allowed_responders || entry.conversationAllowedResponders)
3105
3387
  .map((value) => normalizeTelegramMentionUsername(value))
3106
3388
  .filter(Boolean),
@@ -3602,7 +3884,7 @@ function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
3602
3884
  });
3603
3885
  }
3604
3886
 
3605
- function findScopedConversationSessionState(state, normalizedRoute, conversationIDRaw = "") {
3887
+ function findScopedConversationSessionState(state, normalizedRoute, conversationIDRaw = "") {
3606
3888
  const conversationID = String(conversationIDRaw || "").trim();
3607
3889
  if (!conversationID) {
3608
3890
  return {};
@@ -3661,11 +3943,315 @@ function findScopedConversationSessionState(state, normalizedRoute, conversation
3661
3943
  }
3662
3944
  return String(left.routeKey || "").localeCompare(String(right.routeKey || ""));
3663
3945
  });
3664
- return safeObject(ranked[0]);
3665
- }
3666
-
3667
- function sessionAllowsConversationResponder(sessionRaw, responderSelectorRaw = "") {
3668
- const session = safeObject(sessionRaw);
3946
+ return safeObject(ranked[0]);
3947
+ }
3948
+
3949
+ const DIRECT_MESSAGE_CONTINUATION_LOOKBACK_MS = 45 * 60 * 1000;
3950
+
3951
+ function isPrivateTelegramChatID(chatIDRaw = "") {
3952
+ const chatID = String(chatIDRaw || "").trim();
3953
+ if (!chatID) return false;
3954
+ const numeric = Number.parseInt(chatID, 10);
3955
+ return Number.isFinite(numeric) && numeric > 0;
3956
+ }
3957
+
3958
+ function isRunnerOpenDirectMessageSession(sessionRaw, {
3959
+ chatID = "",
3960
+ botUsername = "",
3961
+ nowMs = Date.now(),
3962
+ } = {}) {
3963
+ const session = safeObject(sessionRaw);
3964
+ if (String(session.mode || "").trim().toLowerCase() !== "direct_single_bot") {
3965
+ return false;
3966
+ }
3967
+ if (String(session.status || "").trim().toLowerCase() !== "open") {
3968
+ return false;
3969
+ }
3970
+ const normalizedChatID = String(chatID || "").trim();
3971
+ if (normalizedChatID && String(session.chat_id || "").trim() !== normalizedChatID) {
3972
+ return false;
3973
+ }
3974
+ const normalizedBotUsername = normalizeTelegramMentionUsername(botUsername);
3975
+ if (
3976
+ normalizedBotUsername
3977
+ && normalizeTelegramMentionUsername(session.bot_username) !== normalizedBotUsername
3978
+ ) {
3979
+ return false;
3980
+ }
3981
+ const expiresAtMs = Date.parse(String(session.expires_at || "").trim());
3982
+ if (Number.isFinite(expiresAtMs) && expiresAtMs > 0 && expiresAtMs <= nowMs) {
3983
+ return false;
3984
+ }
3985
+ return true;
3986
+ }
3987
+
3988
+ function findRunnerDirectMessageSessionCandidate({
3989
+ state,
3990
+ normalizedRoute,
3991
+ routeKey = "",
3992
+ chatID = "",
3993
+ botUsername = "",
3994
+ nowMs = Date.now(),
3995
+ }) {
3996
+ const resolvedRouteKey = String(routeKey || runnerRouteKey(normalizedRoute)).trim();
3997
+ if (!resolvedRouteKey || !isPrivateTelegramChatID(chatID)) {
3998
+ return {};
3999
+ }
4000
+ const routeState = safeObject(safeObject(state?.routes)[resolvedRouteKey]);
4001
+ const ranked = Object.entries(safeObject(routeState.conversation_sessions))
4002
+ .map(([conversationID, sessionRaw]) => ({
4003
+ conversationID: String(conversationID || "").trim(),
4004
+ session: safeObject(sessionRaw),
4005
+ }))
4006
+ .filter((candidate) => candidate.conversationID)
4007
+ .filter((candidate) => isRunnerOpenDirectMessageSession(candidate.session, {
4008
+ chatID,
4009
+ botUsername,
4010
+ nowMs,
4011
+ }))
4012
+ .sort((left, right) => {
4013
+ const leftActivity = firstNonEmptyString([
4014
+ left.session.last_activity_at,
4015
+ left.session.updated_at,
4016
+ left.session.started_at,
4017
+ ]);
4018
+ const rightActivity = firstNonEmptyString([
4019
+ right.session.last_activity_at,
4020
+ right.session.updated_at,
4021
+ right.session.started_at,
4022
+ ]);
4023
+ if (leftActivity && rightActivity && leftActivity !== rightActivity) {
4024
+ return leftActivity < rightActivity ? 1 : -1;
4025
+ }
4026
+ return String(left.conversationID || "").localeCompare(String(right.conversationID || ""));
4027
+ });
4028
+ return safeObject(ranked[0]);
4029
+ }
4030
+
4031
+ function isRunnerDirectMessageContinuationRequestCandidate(entryRaw, {
4032
+ routeKey = "",
4033
+ chatID = "",
4034
+ botUsername = "",
4035
+ nowMs = Date.now(),
4036
+ } = {}) {
4037
+ const entry = safeObject(entryRaw);
4038
+ const normalizedChatID = String(chatID || "").trim();
4039
+ if (!normalizedChatID || String(entry.chat_id || "").trim() !== normalizedChatID) {
4040
+ return false;
4041
+ }
4042
+ const normalizedRouteKey = String(routeKey || "").trim();
4043
+ if (normalizedRouteKey) {
4044
+ const entryRouteKey = firstNonEmptyString([
4045
+ entry.claimed_by_route,
4046
+ entry.source_message_route_key,
4047
+ safeObject(entry.source_message_envelope).source_route_key,
4048
+ ]);
4049
+ if (entryRouteKey && entryRouteKey !== normalizedRouteKey) {
4050
+ return false;
4051
+ }
4052
+ }
4053
+ const normalizedBotUsername = normalizeTelegramMentionUsername(botUsername);
4054
+ if (normalizedBotUsername) {
4055
+ const entryBotUsername = normalizeTelegramMentionUsername(
4056
+ entry.source_message_bot_username
4057
+ || safeObject(entry.source_message_envelope).source_bot_username,
4058
+ );
4059
+ if (entryBotUsername && entryBotUsername !== normalizedBotUsername) {
4060
+ return false;
4061
+ }
4062
+ }
4063
+ const updatedAt = firstNonEmptyString([
4064
+ entry.updated_at,
4065
+ entry.completed_at,
4066
+ entry.closed_at,
4067
+ entry.claimed_at,
4068
+ ]);
4069
+ const updatedAtMs = Date.parse(updatedAt);
4070
+ return !Number.isFinite(updatedAtMs) || nowMs - updatedAtMs <= DIRECT_MESSAGE_CONTINUATION_LOOKBACK_MS;
4071
+ }
4072
+
4073
+ function sortRunnerDirectMessageContinuationCandidates(entries = []) {
4074
+ return ensureArray(entries).slice().sort((leftRaw, rightRaw) => {
4075
+ const left = safeObject(leftRaw);
4076
+ const right = safeObject(rightRaw);
4077
+ const leftStatus = normalizeRunnerRequestStatus(left.status);
4078
+ const rightStatus = normalizeRunnerRequestStatus(right.status);
4079
+ const leftActive = isActiveRunnerRequestStatus(leftStatus);
4080
+ const rightActive = isActiveRunnerRequestStatus(rightStatus);
4081
+ if (leftActive !== rightActive) {
4082
+ return leftActive ? -1 : 1;
4083
+ }
4084
+ const leftTime = firstNonEmptyString([left.updated_at, left.completed_at, left.closed_at, left.claimed_at]);
4085
+ const rightTime = firstNonEmptyString([right.updated_at, right.completed_at, right.closed_at, right.claimed_at]);
4086
+ if (leftTime && rightTime && leftTime !== rightTime) {
4087
+ return leftTime < rightTime ? 1 : -1;
4088
+ }
4089
+ return String(left.request_key || "").localeCompare(String(right.request_key || ""));
4090
+ });
4091
+ }
4092
+
4093
+ function findRunnerDirectMessageContinuationSourceRequest({
4094
+ state,
4095
+ normalizedRoute,
4096
+ routeKey = "",
4097
+ routeState = {},
4098
+ chatID = "",
4099
+ botUsername = "",
4100
+ nowMs = Date.now(),
4101
+ }) {
4102
+ const normalizedChatID = String(chatID || "").trim();
4103
+ if (!isPrivateTelegramChatID(normalizedChatID)) {
4104
+ return null;
4105
+ }
4106
+ const requestsByKey = normalizeBotRunnerRequests(safeObject(state).requests, nowMs);
4107
+ const resolvedRouteKey = String(routeKey || runnerRouteKey(normalizedRoute)).trim();
4108
+ const currentRouteState = safeObject(routeState);
4109
+ const resolveCandidateByKey = (requestKeyRaw = "") => {
4110
+ const requestKey = String(requestKeyRaw || "").trim();
4111
+ if (!requestKey) return null;
4112
+ const candidate = safeObject(requestsByKey[requestKey]);
4113
+ return candidate.request_key && isRunnerDirectMessageContinuationRequestCandidate(candidate, {
4114
+ routeKey: resolvedRouteKey,
4115
+ chatID: normalizedChatID,
4116
+ botUsername,
4117
+ nowMs,
4118
+ })
4119
+ ? candidate
4120
+ : null;
4121
+ };
4122
+ const activeRequestCandidate = resolveCandidateByKey(currentRouteState.active_request_key);
4123
+ if (activeRequestCandidate?.request_key) {
4124
+ return activeRequestCandidate;
4125
+ }
4126
+ const sessionCandidate = findRunnerDirectMessageSessionCandidate({
4127
+ state,
4128
+ normalizedRoute,
4129
+ routeKey: resolvedRouteKey,
4130
+ chatID: normalizedChatID,
4131
+ botUsername,
4132
+ nowMs,
4133
+ });
4134
+ const sessionRequestCandidate = resolveCandidateByKey(safeObject(sessionCandidate.session).request_key);
4135
+ if (sessionRequestCandidate?.request_key) {
4136
+ return sessionRequestCandidate;
4137
+ }
4138
+ const lastRequestCandidate = resolveCandidateByKey(currentRouteState.last_request_key);
4139
+ if (lastRequestCandidate?.request_key) {
4140
+ return lastRequestCandidate;
4141
+ }
4142
+ const scopedCandidates = sortRunnerDirectMessageContinuationCandidates(
4143
+ findRunnerRequestsForScope(state, normalizedRoute, { chatID: normalizedChatID })
4144
+ .filter((entry) => isRunnerDirectMessageContinuationRequestCandidate(entry, {
4145
+ routeKey: resolvedRouteKey,
4146
+ chatID: normalizedChatID,
4147
+ botUsername,
4148
+ nowMs,
4149
+ })),
4150
+ );
4151
+ return safeObject(scopedCandidates[0]).request_key ? safeObject(scopedCandidates[0]) : null;
4152
+ }
4153
+
4154
+ function isRunnerDirectMessageRestartMessage(selectedRecordRaw) {
4155
+ const parsed = safeObject(safeObject(selectedRecordRaw).parsedArchive);
4156
+ if (!isPrivateTelegramChatID(String(parsed.chatID || parsed.chatId || "").trim())) {
4157
+ return false;
4158
+ }
4159
+ const body = String(parsed.body || "").trim();
4160
+ if (!body) {
4161
+ return false;
4162
+ }
4163
+ return /^\/start(?:@[A-Za-z0-9_]+)?(?:\s|$)/i.test(body);
4164
+ }
4165
+
4166
+ function closeRunnerDirectMessageScopedActiveRequests({
4167
+ state,
4168
+ normalizedRoute,
4169
+ routeKey = "",
4170
+ chatID = "",
4171
+ botUsername = "",
4172
+ excludeRequestKey = "",
4173
+ closedReason = "direct_message_restart",
4174
+ nowISO = new Date().toISOString(),
4175
+ }) {
4176
+ const normalizedChatID = String(chatID || "").trim();
4177
+ if (!isPrivateTelegramChatID(normalizedChatID)) {
4178
+ return {
4179
+ requests: normalizeBotRunnerRequests(safeObject(state).requests),
4180
+ closedRequestKeys: [],
4181
+ };
4182
+ }
4183
+ const requests = normalizeBotRunnerRequests(safeObject(state).requests);
4184
+ const projectID = String(normalizedRoute?.projectID || "").trim();
4185
+ const provider = String(normalizedRoute?.provider || "").trim();
4186
+ const normalizedExcludeRequestKey = String(excludeRequestKey || "").trim();
4187
+ const normalizedRouteKey = String(routeKey || "").trim();
4188
+ const normalizedBotUsername = normalizeTelegramMentionUsername(botUsername);
4189
+ const scopedActiveRequests = sortRunnerDirectMessageContinuationCandidates(
4190
+ Object.values(requests).filter((entryRaw) => {
4191
+ const entry = safeObject(entryRaw);
4192
+ const requestKey = String(entry.request_key || "").trim();
4193
+ if (!requestKey || requestKey === normalizedExcludeRequestKey) {
4194
+ return false;
4195
+ }
4196
+ if (!isActiveRunnerRequestStatus(entry.status)) {
4197
+ return false;
4198
+ }
4199
+ if (projectID && String(entry.project_id || "").trim() !== projectID) {
4200
+ return false;
4201
+ }
4202
+ if (provider && String(entry.provider || "").trim() !== provider) {
4203
+ return false;
4204
+ }
4205
+ if (String(entry.chat_id || "").trim() !== normalizedChatID) {
4206
+ return false;
4207
+ }
4208
+ if (normalizedRouteKey) {
4209
+ const entryRouteKey = firstNonEmptyString([
4210
+ entry.claimed_by_route,
4211
+ entry.source_message_route_key,
4212
+ safeObject(entry.source_message_envelope).source_route_key,
4213
+ ]);
4214
+ if (entryRouteKey && entryRouteKey !== normalizedRouteKey) {
4215
+ return false;
4216
+ }
4217
+ }
4218
+ if (normalizedBotUsername) {
4219
+ const entryBotCandidates = uniqueOrderedStrings([
4220
+ entry.source_message_bot_username,
4221
+ safeObject(entry.source_message_envelope).source_bot_username,
4222
+ ...ensureArray(entry.selected_bot_usernames),
4223
+ ], normalizeTelegramMentionUsername);
4224
+ if (entryBotCandidates.length > 0 && !entryBotCandidates.includes(normalizedBotUsername)) {
4225
+ return false;
4226
+ }
4227
+ }
4228
+ return true;
4229
+ }),
4230
+ );
4231
+ const closedRequestKeys = [];
4232
+ for (const requestRaw of scopedActiveRequests) {
4233
+ const request = safeObject(requestRaw);
4234
+ const requestKey = String(request.request_key || "").trim();
4235
+ if (!requestKey) {
4236
+ continue;
4237
+ }
4238
+ requests[requestKey] = {
4239
+ ...request,
4240
+ status: "closed",
4241
+ closed_reason: closedReason,
4242
+ closed_at: nowISO,
4243
+ updated_at: nowISO,
4244
+ };
4245
+ closedRequestKeys.push(requestKey);
4246
+ }
4247
+ return {
4248
+ requests,
4249
+ closedRequestKeys,
4250
+ };
4251
+ }
4252
+
4253
+ function sessionAllowsConversationResponder(sessionRaw, responderSelectorRaw = "") {
4254
+ const session = safeObject(sessionRaw);
3669
4255
  const responderSelector = normalizeTelegramMentionUsername(responderSelectorRaw);
3670
4256
  if (!responderSelector) {
3671
4257
  return false;
@@ -3871,14 +4457,24 @@ function runnerRequestAuthoritativeDecisionBundle(entryRaw) {
3871
4457
  return validation.ok ? safeObject(validation.bundle) : {};
3872
4458
  }
3873
4459
 
4460
+ function runnerDecisionBundleDecisionType(bundleRaw) {
4461
+ return String(
4462
+ safeObject(bundleRaw).decision_type || safeObject(bundleRaw).decisionType || "",
4463
+ ).trim().toLowerCase();
4464
+ }
4465
+
4466
+ function runnerDecisionBundleIsRootHumanOpening(bundleRaw) {
4467
+ return runnerDecisionBundleDecisionType(bundleRaw) === "human_opening";
4468
+ }
4469
+
3874
4470
  function runnerRequestPreferredExecutionContractType(entryRaw) {
3875
4471
  const entry = safeObject(entryRaw);
3876
4472
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
3877
- const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
4473
+ const authoritativeExecutionContractType = String(
4474
+ decisionBundle.execution_contract_type || "",
4475
+ ).trim().toLowerCase();
3878
4476
  return String(
3879
- (hasAuthoritativeDecisionBundle
3880
- ? decisionBundle.execution_contract_type
3881
- : "")
4477
+ authoritativeExecutionContractType
3882
4478
  || entry.execution_contract_type
3883
4479
  || entry.root_execution_contract_type
3884
4480
  || "",
@@ -3888,8 +4484,12 @@ function runnerRequestPreferredExecutionContractType(entryRaw) {
3888
4484
  function runnerRequestPreferredExecutionContractActionable(entryRaw) {
3889
4485
  const entry = safeObject(entryRaw);
3890
4486
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
3891
- const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
3892
- if (hasAuthoritativeDecisionBundle) {
4487
+ const hasAuthoritativeExecutionContract = Boolean(
4488
+ String(decisionBundle.execution_contract_type || "").trim()
4489
+ || ensureArray(decisionBundle.execution_contract_targets).length
4490
+ || ensureArray(decisionBundle.next_expected_responders).length,
4491
+ );
4492
+ if (hasAuthoritativeExecutionContract) {
3893
4493
  return decisionBundle.execution_contract_actionable === true;
3894
4494
  }
3895
4495
  return entry.execution_contract_actionable === true;
@@ -3898,9 +4498,8 @@ function runnerRequestPreferredExecutionContractActionable(entryRaw) {
3898
4498
  function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3899
4499
  const entry = safeObject(entryRaw);
3900
4500
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
3901
- const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
3902
4501
  return uniqueOrderedStrings(
3903
- hasAuthoritativeDecisionBundle
4502
+ ensureArray(decisionBundle.execution_contract_targets).length
3904
4503
  ? decisionBundle.execution_contract_targets
3905
4504
  : ensureArray(entry.execution_contract_targets).length
3906
4505
  ? entry.execution_contract_targets
@@ -3914,9 +4513,8 @@ function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3914
4513
  function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3915
4514
  const entry = safeObject(entryRaw);
3916
4515
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
3917
- const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
3918
4516
  return uniqueOrderedStrings(
3919
- hasAuthoritativeDecisionBundle
4517
+ ensureArray(decisionBundle.next_expected_responders).length
3920
4518
  ? decisionBundle.next_expected_responders
3921
4519
  : ensureArray(entry.next_expected_responders).length
3922
4520
  ? entry.next_expected_responders
@@ -4738,10 +5336,10 @@ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, sele
4738
5336
  };
4739
5337
  }
4740
5338
 
4741
- async function resolveRunnerReplyChainConversationContextWithServerFallback({
4742
- state,
4743
- normalizedRoute,
4744
- selectedRecord,
5339
+ async function resolveRunnerReplyChainConversationContextWithServerFallback({
5340
+ state,
5341
+ normalizedRoute,
5342
+ selectedRecord,
4745
5343
  runtime,
4746
5344
  archiveThreadID = "",
4747
5345
  hydrationAttempted = false,
@@ -4756,14 +5354,70 @@ async function resolveRunnerReplyChainConversationContextWithServerFallback({
4756
5354
  };
4757
5355
  }
4758
5356
  const parsed = safeObject(selectedRecord?.parsedArchive);
4759
- const initialReplyToMessageID = intFromRawAllowZero(
4760
- parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
4761
- 0,
4762
- );
4763
- if (initialReplyToMessageID <= 0) {
4764
- return {
4765
- state: initialState,
4766
- replyChainContext: initialContext,
5357
+ const initialReplyToMessageID = intFromRawAllowZero(
5358
+ parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
5359
+ 0,
5360
+ );
5361
+ const routeKey = runnerRouteKey(normalizedRoute);
5362
+ const routeStateForLookup = safeObject(safeObject(initialState.routes)[routeKey]);
5363
+ const currentBotUsername = normalizeTelegramMentionUsername(
5364
+ normalizedRoute?.botName
5365
+ || normalizedRoute?.bot_name
5366
+ || normalizedRoute?.serverBotName
5367
+ || normalizedRoute?.server_bot_name,
5368
+ );
5369
+ if (initialReplyToMessageID <= 0) {
5370
+ const directMessageContinuationRequest = findRunnerDirectMessageContinuationSourceRequest({
5371
+ state: initialState,
5372
+ normalizedRoute,
5373
+ routeKey,
5374
+ routeState: routeStateForLookup,
5375
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5376
+ botUsername: currentBotUsername,
5377
+ });
5378
+ if (safeObject(directMessageContinuationRequest).request_key) {
5379
+ const referencedRequest = safeObject(directMessageContinuationRequest);
5380
+ const anchorMessageID = intFromRawAllowZero(referencedRequest.source_message_id, 0)
5381
+ || intFromRawAllowZero(parsed.messageID, 0);
5382
+ return {
5383
+ state: initialState,
5384
+ replyChainContext: {
5385
+ conversationID: String(referencedRequest.conversation_id || "").trim()
5386
+ || buildSyntheticReplyChainConversationID(
5387
+ normalizedRoute,
5388
+ String(parsed.chatID || parsed.chatId || "").trim(),
5389
+ anchorMessageID,
5390
+ ),
5391
+ replyToMessageID: 0,
5392
+ anchorMessageID,
5393
+ reason: "direct_message_continuation_request",
5394
+ referencedRequest,
5395
+ },
5396
+ hydrated: false,
5397
+ };
5398
+ }
5399
+ if (!hydrationAttempted && runtime?.baseURL && runtime?.token) {
5400
+ const hydratedState = await hydrateRunnerRequestLedgerFromServer({
5401
+ normalizedRoute,
5402
+ runtime,
5403
+ });
5404
+ const hydratedResolution = await resolveRunnerReplyChainConversationContextWithServerFallback({
5405
+ state: hydratedState,
5406
+ normalizedRoute,
5407
+ selectedRecord,
5408
+ runtime,
5409
+ archiveThreadID,
5410
+ hydrationAttempted: true,
5411
+ });
5412
+ return {
5413
+ state: hydratedResolution.state,
5414
+ replyChainContext: hydratedResolution.replyChainContext,
5415
+ hydrated: true,
5416
+ };
5417
+ }
5418
+ return {
5419
+ state: initialState,
5420
+ replyChainContext: initialContext,
4767
5421
  hydrated: false,
4768
5422
  };
4769
5423
  }
@@ -5093,7 +5747,27 @@ async function claimRunnerRequestForHumanComment({
5093
5747
  requests: backfilled.requests,
5094
5748
  };
5095
5749
  }
5096
- const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5750
+ const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
5751
+ const normalizedAuthoritativeSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(
5752
+ authoritativeSourceMessageEnvelope,
5753
+ );
5754
+ const currentClaimBotUsername = normalizeTelegramMentionUsername(
5755
+ normalizedAuthoritativeSourceMessageEnvelope.source_bot_username
5756
+ || normalizedAuthoritativeSourceMessageEnvelope.sender_username
5757
+ || currentRouteState.last_source_bot_username
5758
+ || currentRouteState.source_message_bot_username
5759
+ || currentRouteState.last_speaker_bot_username,
5760
+ );
5761
+ const claimBotUsernameCandidates = uniqueOrderedStrings([
5762
+ currentClaimBotUsername,
5763
+ normalizedAuthoritativeSourceMessageEnvelope.source_bot_username,
5764
+ normalizedAuthoritativeSourceMessageEnvelope.sender_username,
5765
+ ...ensureArray(selectedBotUsernames),
5766
+ ...ensureArray(normalizedSharedHumanIntent.participantSelectors),
5767
+ currentRouteState.last_source_bot_username,
5768
+ currentRouteState.source_message_bot_username,
5769
+ currentRouteState.last_speaker_bot_username,
5770
+ ], normalizeTelegramMentionUsername);
5097
5771
  let sharedConversationSource = currentMessageID > 0
5098
5772
  ? pickRunnerSharedConversationSourceRequest(
5099
5773
  findRunnerRequestsForMessageID(stateForClaim, normalizedRoute, {
@@ -5104,10 +5778,10 @@ async function claimRunnerRequestForHumanComment({
5104
5778
  provisionalRequestKey,
5105
5779
  )
5106
5780
  : {};
5107
- if (
5108
- !Object.keys(sharedConversationSource).length
5109
- && currentMessageID > 0
5110
- && runtime?.baseURL
5781
+ if (
5782
+ !Object.keys(sharedConversationSource).length
5783
+ && currentMessageID > 0
5784
+ && runtime?.baseURL
5111
5785
  && runtime?.token
5112
5786
  ) {
5113
5787
  sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
@@ -5119,68 +5793,332 @@ async function claimRunnerRequestForHumanComment({
5119
5793
  excludeRequestKey: provisionalRequestKey,
5120
5794
  }));
5121
5795
  }
5796
+ if (!Object.keys(sharedConversationSource).length) {
5797
+ sharedConversationSource = safeObject(findRunnerDirectMessageContinuationSourceRequest({
5798
+ state: stateForClaim,
5799
+ normalizedRoute,
5800
+ routeKey,
5801
+ routeState: currentRouteState,
5802
+ chatID: String(parsed.chatID || parsed.chatId || "").trim(),
5803
+ botUsername: currentClaimBotUsername,
5804
+ }));
5805
+ }
5122
5806
  const authorityContext = resolveRunnerHumanCommentAuthorityContext({
5123
5807
  normalizedRoute,
5124
5808
  selectedRecord,
5125
- replyChainContext,
5126
- referencedRequest,
5809
+ replyChainContext,
5810
+ referencedRequest,
5127
5811
  sharedConversationSource,
5128
5812
  selectedBotUsernames,
5129
- normalizedSharedHumanIntent,
5130
- resolvedNormalizedIntent,
5131
- });
5813
+ normalizedSharedHumanIntent,
5814
+ resolvedNormalizedIntent,
5815
+ });
5816
+ const normalizedChatID = String(parsed.chatID || parsed.chatId || "").trim();
5817
+ const isDirectMessageChat = isPrivateTelegramChatID(normalizedChatID);
5818
+ const isDirectMessageRestart = isDirectMessageChat && isRunnerDirectMessageRestartMessage(selectedRecord);
5819
+ const routeStateForClaim = safeObject(safeObject(stateForClaim.routes)[String(routeKey || "").trim()]);
5820
+ const currentRouteStateForClaim = safeObject(
5821
+ safeObject(currentState.routes)[String(routeKey || "").trim()],
5822
+ );
5823
+ const requestsForDirectMessageClaimLookup = normalizeBotRunnerRequests(stateForClaim.requests);
5824
+ const currentRequestsForDirectMessageClaimLookup = normalizeBotRunnerRequests(currentState.requests);
5825
+ const directMessageRouteActiveRequest = safeObject(
5826
+ requestsForDirectMessageClaimLookup[String(routeStateForClaim.active_request_key || "").trim()],
5827
+ );
5828
+ const directMessageRouteLastRequest = safeObject(
5829
+ requestsForDirectMessageClaimLookup[String(routeStateForClaim.last_request_key || "").trim()],
5830
+ );
5831
+ const currentDirectMessageRouteActiveRequest = safeObject(
5832
+ currentRequestsForDirectMessageClaimLookup[String(currentRouteStateForClaim.active_request_key || "").trim()],
5833
+ );
5834
+ const currentDirectMessageRouteLastRequest = safeObject(
5835
+ currentRequestsForDirectMessageClaimLookup[String(currentRouteStateForClaim.last_request_key || "").trim()],
5836
+ );
5837
+ const directMessageRestartRequestKeysToClose = new Set();
5838
+ if (isDirectMessageRestart) {
5839
+ const closeBotUsername = firstNonEmptyString(claimBotUsernameCandidates);
5840
+ const closedActiveDirectMessageRequests = closeRunnerDirectMessageScopedActiveRequests({
5841
+ state: stateForClaim,
5842
+ normalizedRoute,
5843
+ routeKey,
5844
+ chatID: normalizedChatID,
5845
+ botUsername: closeBotUsername,
5846
+ });
5847
+ stateForClaim = {
5848
+ ...stateForClaim,
5849
+ requests: closedActiveDirectMessageRequests.requests,
5850
+ };
5851
+ for (const requestKeyRaw of ensureArray(closedActiveDirectMessageRequests.closedRequestKeys)) {
5852
+ const requestKey = String(requestKeyRaw || "").trim();
5853
+ if (requestKey) {
5854
+ directMessageRestartRequestKeysToClose.add(requestKey);
5855
+ }
5856
+ }
5857
+ const restartRequests = normalizeBotRunnerRequests(stateForClaim.requests);
5858
+ for (const directMessageRequest of [
5859
+ directMessageRouteActiveRequest,
5860
+ directMessageRouteLastRequest,
5861
+ currentDirectMessageRouteActiveRequest,
5862
+ currentDirectMessageRouteLastRequest,
5863
+ ]) {
5864
+ const requestKey = String(safeObject(directMessageRequest).request_key || "").trim();
5865
+ if (!requestKey) {
5866
+ continue;
5867
+ }
5868
+ directMessageRestartRequestKeysToClose.add(requestKey);
5869
+ const existingDirectMessageRequest = safeObject(restartRequests[requestKey]);
5870
+ if (
5871
+ !String(existingDirectMessageRequest.request_key || "").trim()
5872
+ || !isActiveRunnerRequestStatus(existingDirectMessageRequest.status)
5873
+ || String(existingDirectMessageRequest.chat_id || "").trim() !== normalizedChatID
5874
+ ) {
5875
+ continue;
5876
+ }
5877
+ restartRequests[requestKey] = {
5878
+ ...existingDirectMessageRequest,
5879
+ status: "closed",
5880
+ closed_reason: "direct_message_restart",
5881
+ closed_at: new Date().toISOString(),
5882
+ updated_at: new Date().toISOString(),
5883
+ };
5884
+ }
5885
+ stateForClaim = {
5886
+ ...stateForClaim,
5887
+ requests: restartRequests,
5888
+ };
5889
+ }
5132
5890
  const authoritySource = safeObject(authorityContext.authoritySource);
5133
- const decisionBundleValidation = validateRunnerConversationDecisionBundle(decisionBundle);
5134
- if (
5135
- Object.keys(safeObject(decisionBundle)).length > 0
5136
- && decisionBundleValidation.ok !== true
5137
- ) {
5891
+ const effectiveAuthoritySource = isDirectMessageRestart ? {} : authoritySource;
5892
+ const authorityReplyChainContext = await buildRunnerAuthorityReplyChainContext({
5893
+ selectedRecord,
5894
+ replyChainContext,
5895
+ authoritySource: effectiveAuthoritySource,
5896
+ runtime,
5897
+ archiveThreadID,
5898
+ });
5899
+ const explicitDecisionBundle = safeObject(decisionBundle);
5900
+ const authorityRootMessageID = intFromRawAllowZero(
5901
+ authorityReplyChainContext.root_message_id || authorityReplyChainContext.rootMessageID,
5902
+ 0,
5903
+ );
5904
+ const authoritativeSourceMessageID = intFromRawAllowZero(
5905
+ normalizedAuthoritativeSourceMessageEnvelope.message_id
5906
+ || normalizedAuthoritativeSourceMessageEnvelope.messageID
5907
+ || currentMessageID,
5908
+ 0,
5909
+ );
5910
+ const explicitDecisionType = String(
5911
+ explicitDecisionBundle.decision_type || explicitDecisionBundle.decisionType || "",
5912
+ ).trim().toLowerCase();
5913
+ const preferAuthoritativeSourceReplyAnchor = explicitDecisionType === "human_opening";
5914
+ const inferredReplyAnchorMessageID = preferAuthoritativeSourceReplyAnchor
5915
+ ? authoritativeSourceMessageID > 0
5916
+ ? authoritativeSourceMessageID
5917
+ : authorityRootMessageID > 0
5918
+ ? authorityRootMessageID
5919
+ : 0
5920
+ : authorityRootMessageID > 0
5921
+ ? authorityRootMessageID
5922
+ : authoritativeSourceMessageID > 0
5923
+ ? authoritativeSourceMessageID
5924
+ : 0;
5925
+ const inferredReplyAnchorSource = preferAuthoritativeSourceReplyAnchor
5926
+ ? authoritativeSourceMessageID > 0
5927
+ ? "authoritative_source_message_envelope"
5928
+ : authorityRootMessageID > 0
5929
+ ? "reply_chain_root_message"
5930
+ : ""
5931
+ : authorityRootMessageID > 0
5932
+ ? "reply_chain_root_message"
5933
+ : authoritativeSourceMessageID > 0
5934
+ ? "authoritative_source_message_envelope"
5935
+ : "";
5936
+ const inferredReplyAnchorKind = inferredReplyAnchorMessageID > 0 ? "human_root" : "";
5937
+ const shouldOverrideHumanOpeningReplyAnchor = explicitDecisionType === "human_opening"
5938
+ && inferredReplyAnchorMessageID > 0;
5939
+ const decisionBundleWithReplyAnchor = Object.keys(explicitDecisionBundle).length > 0
5940
+ ? {
5941
+ ...explicitDecisionBundle,
5942
+ ...((shouldOverrideHumanOpeningReplyAnchor || !(
5943
+ intFromRawAllowZero(
5944
+ explicitDecisionBundle.reply_to_message_id || explicitDecisionBundle.replyToMessageID,
5945
+ 0,
5946
+ ) > 0
5947
+ )) && inferredReplyAnchorMessageID > 0
5948
+ ? { reply_to_message_id: inferredReplyAnchorMessageID }
5949
+ : {}),
5950
+ ...((shouldOverrideHumanOpeningReplyAnchor || !String(
5951
+ explicitDecisionBundle.reply_anchor_source || explicitDecisionBundle.replyAnchorSource || "",
5952
+ ).trim()) && inferredReplyAnchorSource
5953
+ ? { reply_anchor_source: inferredReplyAnchorSource }
5954
+ : {}),
5955
+ ...((shouldOverrideHumanOpeningReplyAnchor || !String(
5956
+ explicitDecisionBundle.reply_anchor_kind || explicitDecisionBundle.replyAnchorKind || "",
5957
+ ).trim()) && inferredReplyAnchorKind
5958
+ ? { reply_anchor_kind: inferredReplyAnchorKind }
5959
+ : {}),
5960
+ }
5961
+ : {};
5962
+ const decisionBundleValidation = validateRunnerConversationDecisionBundle(decisionBundleWithReplyAnchor);
5963
+ if (Object.keys(decisionBundleWithReplyAnchor).length > 0 && decisionBundleValidation.ok !== true) {
5138
5964
  return {
5139
5965
  ok: false,
5140
5966
  reason: "invalid_decision_bundle",
5141
5967
  detail: String(decisionBundleValidation.reason || "").trim() || "invalid_decision_bundle",
5142
5968
  };
5143
5969
  }
5970
+ if (
5971
+ Object.keys(decisionBundleWithReplyAnchor).length > 0
5972
+ && runnerDecisionBundleDecisionType(decisionBundleValidation.bundle) !== "human_opening"
5973
+ ) {
5974
+ return {
5975
+ ok: false,
5976
+ reason: "invalid_root_request_decision_bundle",
5977
+ detail: "root human requests require a human_opening authoritative decision bundle",
5978
+ };
5979
+ }
5144
5980
  const authoritativeDecisionBundle = decisionBundleValidation.ok === true
5145
5981
  ? safeObject(decisionBundleValidation.bundle)
5146
5982
  : {};
5147
- const normalizedAuthoritativeSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(
5148
- authoritativeSourceMessageEnvelope,
5149
- );
5150
- const currentClaimBotUsername = normalizeTelegramMentionUsername(
5151
- normalizedAuthoritativeSourceMessageEnvelope.source_bot_username
5152
- || normalizedAuthoritativeSourceMessageEnvelope.sender_username
5153
- || currentRouteState.last_source_bot_username
5154
- || currentRouteState.source_message_bot_username
5155
- || currentRouteState.last_speaker_bot_username,
5983
+ const resolvedConversationID = isDirectMessageRestart
5984
+ ? buildSyntheticHumanOpeningConversationID(
5985
+ normalizedRoute,
5986
+ normalizedChatID,
5987
+ currentMessageID,
5988
+ canonicalHumanMessageKey,
5989
+ )
5990
+ : String(authorityContext.conversationID || "").trim();
5991
+ const preferredNormalizedIntent = String(
5992
+ isDirectMessageRestart
5993
+ ? normalizedSharedHumanIntent.intentType || resolvedNormalizedIntent || ""
5994
+ : authorityContext.normalizedIntent,
5995
+ ).trim().toLowerCase();
5996
+ const requestSelectedBotUsernames = ensureArray(
5997
+ isDirectMessageRestart
5998
+ ? ensureArray(selectedBotUsernames).length
5999
+ ? selectedBotUsernames
6000
+ : authorityContext.selectedBotUsernames
6001
+ : authorityContext.selectedBotUsernames,
5156
6002
  );
5157
- const authorityReplyChainContext = await buildRunnerAuthorityReplyChainContext({
5158
- selectedRecord,
5159
- replyChainContext,
5160
- authoritySource,
5161
- runtime,
5162
- archiveThreadID,
5163
- });
5164
- const resolvedConversationID = String(authorityContext.conversationID || "").trim();
5165
- const preferredNormalizedIntent = String(authorityContext.normalizedIntent || "").trim().toLowerCase();
5166
- const requestSelectedBotUsernames = ensureArray(authorityContext.selectedBotUsernames);
5167
6003
  const effectiveSelectedBotUsernames = uniqueOrderedStrings(
5168
6004
  ensureArray(authoritativeDecisionBundle.selected_bot_usernames).length
5169
6005
  ? authoritativeDecisionBundle.selected_bot_usernames
5170
6006
  : requestSelectedBotUsernames,
5171
6007
  normalizeTelegramMentionUsername,
5172
6008
  );
5173
- const requestKey = buildRunnerRequestKey({
5174
- normalizedRoute,
5175
- selectedRecord,
5176
- selectedBotUsernames: effectiveSelectedBotUsernames,
5177
- normalizedIntent: preferredNormalizedIntent,
5178
- conversationID: resolvedConversationID,
5179
- });
5180
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
5181
- const existing = safeObject(requests[requestKey]);
5182
- if (isFinalRunnerRequestStatus(existing.status)) {
5183
- return {
6009
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
6010
+ const resolveDirectMessageContinuationRequestByKey = (requestKeyRaw = "") => {
6011
+ const requestKey = String(requestKeyRaw || "").trim();
6012
+ if (!requestKey) {
6013
+ return {};
6014
+ }
6015
+ const candidate = safeObject(requests[requestKey]);
6016
+ if (!String(candidate.request_key || "").trim()) {
6017
+ return {};
6018
+ }
6019
+ const botCandidates = claimBotUsernameCandidates.length > 0 ? claimBotUsernameCandidates : [""];
6020
+ for (const botUsernameCandidate of botCandidates) {
6021
+ if (isRunnerDirectMessageContinuationRequestCandidate(candidate, {
6022
+ routeKey: String(routeKey || "").trim(),
6023
+ chatID: normalizedChatID,
6024
+ botUsername: botUsernameCandidate,
6025
+ })) {
6026
+ return candidate;
6027
+ }
6028
+ }
6029
+ if (isRunnerDirectMessageContinuationRequestCandidate(candidate, {
6030
+ routeKey: String(routeKey || "").trim(),
6031
+ chatID: normalizedChatID,
6032
+ botUsername: "",
6033
+ })) {
6034
+ return candidate;
6035
+ }
6036
+ return {};
6037
+ };
6038
+ let directMessageContinuationRequest = {};
6039
+ if (isDirectMessageChat && !isDirectMessageRestart) {
6040
+ const prefersDirectMessageRouteActiveRequest = (requestRaw = {}) => {
6041
+ const request = safeObject(requestRaw);
6042
+ return (
6043
+ String(request.request_key || "").trim()
6044
+ && String(request.project_id || "").trim() === String(normalizedRoute?.projectID || "").trim()
6045
+ && String(request.provider || "").trim() === String(normalizedRoute?.provider || "").trim()
6046
+ && String(request.chat_id || "").trim() === normalizedChatID
6047
+ && isActiveRunnerRequestStatus(request.status)
6048
+ )
6049
+ ? request
6050
+ : {};
6051
+ };
6052
+ directMessageContinuationRequest = prefersDirectMessageRouteActiveRequest(directMessageRouteActiveRequest);
6053
+ if (!String(directMessageContinuationRequest.request_key || "").trim()) {
6054
+ directMessageContinuationRequest = prefersDirectMessageRouteActiveRequest(directMessageRouteLastRequest);
6055
+ }
6056
+ const directMessageContinuationRequestKeys = uniqueOrderedStrings([
6057
+ safeObject(effectiveAuthoritySource).request_key,
6058
+ safeObject(authoritySource).request_key,
6059
+ safeObject(referencedRequest).request_key,
6060
+ safeObject(sharedConversationSource).request_key,
6061
+ routeStateForClaim.active_request_key,
6062
+ routeStateForClaim.last_request_key,
6063
+ ]);
6064
+ if (!String(directMessageContinuationRequest.request_key || "").trim()) {
6065
+ for (const requestKeyCandidate of directMessageContinuationRequestKeys) {
6066
+ const matchedRequest = resolveDirectMessageContinuationRequestByKey(requestKeyCandidate);
6067
+ if (String(matchedRequest.request_key || "").trim()) {
6068
+ directMessageContinuationRequest = matchedRequest;
6069
+ break;
6070
+ }
6071
+ }
6072
+ }
6073
+ if (!String(directMessageContinuationRequest.request_key || "").trim()) {
6074
+ const lookupBotCandidates = claimBotUsernameCandidates.length > 0 ? claimBotUsernameCandidates : [""];
6075
+ for (const botUsernameCandidate of lookupBotCandidates) {
6076
+ const matchedRequest = safeObject(findRunnerDirectMessageContinuationSourceRequest({
6077
+ state: stateForClaim,
6078
+ normalizedRoute,
6079
+ routeKey,
6080
+ routeState: routeStateForClaim,
6081
+ chatID: normalizedChatID,
6082
+ botUsername: botUsernameCandidate,
6083
+ }));
6084
+ if (String(matchedRequest.request_key || "").trim()) {
6085
+ directMessageContinuationRequest = matchedRequest;
6086
+ break;
6087
+ }
6088
+ }
6089
+ }
6090
+ }
6091
+ const shouldReuseDirectMessageRequest = (
6092
+ isDirectMessageChat
6093
+ && !isDirectMessageRestart
6094
+ && String(directMessageContinuationRequest.request_key || "").trim()
6095
+ && isActiveRunnerRequestStatus(directMessageContinuationRequest.status)
6096
+ );
6097
+ const directMessageContinuationConversationID = String(
6098
+ directMessageContinuationRequest.conversation_id
6099
+ || safeObject(routeStateForClaim.conversation_sessions)[String(routeStateForClaim.last_conversation_id || "").trim()]?.conversation_id
6100
+ || "",
6101
+ ).trim();
6102
+ const effectiveConversationID = shouldReuseDirectMessageRequest
6103
+ ? String(
6104
+ resolvedConversationID
6105
+ || directMessageContinuationConversationID
6106
+ || safeObject(directMessageContinuationRequest.reply_chain_context).conversation_id
6107
+ || "",
6108
+ ).trim()
6109
+ : resolvedConversationID;
6110
+ const requestKey = shouldReuseDirectMessageRequest
6111
+ ? String(directMessageContinuationRequest.request_key || "").trim()
6112
+ : buildRunnerRequestKey({
6113
+ normalizedRoute,
6114
+ selectedRecord,
6115
+ selectedBotUsernames: effectiveSelectedBotUsernames,
6116
+ normalizedIntent: preferredNormalizedIntent,
6117
+ conversationID: effectiveConversationID,
6118
+ });
6119
+ const existing = safeObject(requests[requestKey]);
6120
+ if (isFinalRunnerRequestStatus(existing.status)) {
6121
+ return {
5184
6122
  ok: false,
5185
6123
  reason: "request_already_finalized",
5186
6124
  requestKey,
@@ -5204,6 +6142,15 @@ async function claimRunnerRequestForHumanComment({
5204
6142
  }
5205
6143
  const nowISO = new Date().toISOString();
5206
6144
  const existingSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(existing.source_message_envelope);
6145
+ const existingRootSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(
6146
+ existing.root_source_message_envelope
6147
+ || existing.rootSourceMessageEnvelope
6148
+ || existing.source_message_envelope,
6149
+ );
6150
+ const existingCurrentSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(
6151
+ existing.current_source_message_envelope
6152
+ || existing.currentSourceMessageEnvelope,
6153
+ );
5207
6154
  const preserveExistingCanonicalSourceEnvelope = Boolean(
5208
6155
  canonicalHumanMessageKey
5209
6156
  && String(existing.canonical_human_message_key || "").trim() === canonicalHumanMessageKey
@@ -5215,6 +6162,23 @@ async function claimRunnerRequestForHumanComment({
5215
6162
  : Object.keys(normalizedAuthoritativeSourceMessageEnvelope).length
5216
6163
  ? normalizedAuthoritativeSourceMessageEnvelope
5217
6164
  : existingSourceMessageEnvelope;
6165
+ const effectiveSourceMessageEnvelope = shouldReuseDirectMessageRequest
6166
+ ? (Object.keys(existingSourceMessageEnvelope).length
6167
+ ? existingSourceMessageEnvelope
6168
+ : sourceMessageEnvelope)
6169
+ : sourceMessageEnvelope;
6170
+ const rootSourceMessageEnvelope = shouldReuseDirectMessageRequest
6171
+ ? (Object.keys(existingRootSourceMessageEnvelope).length
6172
+ ? existingRootSourceMessageEnvelope
6173
+ : effectiveSourceMessageEnvelope)
6174
+ : sourceMessageEnvelope;
6175
+ const currentSourceMessageEnvelope = preserveExistingCanonicalSourceEnvelope
6176
+ ? (Object.keys(existingCurrentSourceMessageEnvelope).length
6177
+ ? existingCurrentSourceMessageEnvelope
6178
+ : sourceMessageEnvelope)
6179
+ : Object.keys(normalizedAuthoritativeSourceMessageEnvelope).length
6180
+ ? normalizedAuthoritativeSourceMessageEnvelope
6181
+ : sourceMessageEnvelope;
5218
6182
  const decisionConversationParticipants = uniqueOrderedStrings(
5219
6183
  ensureArray(authoritativeDecisionBundle.participants),
5220
6184
  normalizeTelegramMentionUsername,
@@ -5228,33 +6192,114 @@ async function claimRunnerRequestForHumanComment({
5228
6192
  normalizeTelegramMentionUsername,
5229
6193
  );
5230
6194
  const hasAuthoritativeConversationDecision = Object.keys(authoritativeDecisionBundle).length > 0;
5231
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
5232
- project_id: String(normalizedRoute?.projectID || "").trim(),
5233
- provider: String(normalizedRoute?.provider || "").trim(),
5234
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
5235
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5236
- source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5237
- source_message_body: String(parsed.body || "").trim(),
5238
- canonical_human_message_key: canonicalHumanMessageKey,
5239
- source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
5240
- source_message_route_key: String(sourceMessageEnvelope.source_route_key || "").trim(),
5241
- source_message_bot_username: normalizeTelegramMentionUsername(sourceMessageEnvelope.source_bot_username),
5242
- source_message_envelope: sourceMessageEnvelope,
5243
- root_comment_id: String(selectedRecord?.id || "").trim(),
5244
- root_comment_kind: commentKind,
5245
- conversation_id: resolvedConversationID,
6195
+ const rootSourceMessageID = shouldReuseDirectMessageRequest
6196
+ ? intFromRawAllowZero(existing.source_message_id, 0) || intFromRawAllowZero(parsed.messageID, 0)
6197
+ : intFromRawAllowZero(parsed.messageID, 0);
6198
+ const rootSourceMessageThreadID = shouldReuseDirectMessageRequest
6199
+ ? intFromRawAllowZero(existing.source_message_thread_id, 0)
6200
+ : intFromRawAllowZero(parsed.messageThreadID, 0);
6201
+ const rootSourceMessageBody = shouldReuseDirectMessageRequest
6202
+ ? String(existing.source_message_body || parsed.body || "").trim()
6203
+ : String(parsed.body || "").trim();
6204
+ const rootCanonicalHumanMessageKey = shouldReuseDirectMessageRequest
6205
+ ? firstNonEmptyString([
6206
+ existing.canonical_human_message_key,
6207
+ canonicalHumanMessageKey,
6208
+ ])
6209
+ : canonicalHumanMessageKey;
6210
+ const currentSourceMessageID = intFromRawAllowZero(
6211
+ currentSourceMessageEnvelope.message_id || currentMessageID,
6212
+ 0,
6213
+ );
6214
+ const currentSourceMessageThreadID = intFromRawAllowZero(
6215
+ currentSourceMessageEnvelope.message_thread_id || parsed.messageThreadID,
6216
+ 0,
6217
+ );
6218
+ const currentSourceMessageBody = firstNonEmptyString([
6219
+ currentSourceMessageEnvelope.body,
6220
+ parsed.body,
6221
+ ]);
6222
+ const currentTurnRelation = String(
6223
+ authorityReplyChainContext.current_turn_relation
6224
+ || authorityReplyChainContext.currentTurnRelation
6225
+ || existing.current_turn_relation
6226
+ || existing.currentTurnRelation
6227
+ || "",
6228
+ ).trim();
6229
+ let { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
6230
+ project_id: String(normalizedRoute?.projectID || "").trim(),
6231
+ provider: String(normalizedRoute?.provider || "").trim(),
6232
+ chat_id: normalizedChatID,
6233
+ source_message_id: rootSourceMessageID || undefined,
6234
+ source_message_thread_id: rootSourceMessageThreadID || undefined,
6235
+ source_message_body: rootSourceMessageBody,
6236
+ canonical_human_message_key: rootCanonicalHumanMessageKey,
6237
+ source_message_origin: shouldReuseDirectMessageRequest
6238
+ ? String(existing.source_message_origin || effectiveSourceMessageEnvelope.source_origin || "").trim().toLowerCase()
6239
+ : String(effectiveSourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
6240
+ source_message_route_key: shouldReuseDirectMessageRequest
6241
+ ? String(existing.source_message_route_key || effectiveSourceMessageEnvelope.source_route_key || "").trim()
6242
+ : String(effectiveSourceMessageEnvelope.source_route_key || "").trim(),
6243
+ source_message_bot_username: shouldReuseDirectMessageRequest
6244
+ ? normalizeTelegramMentionUsername(existing.source_message_bot_username || effectiveSourceMessageEnvelope.source_bot_username)
6245
+ : normalizeTelegramMentionUsername(effectiveSourceMessageEnvelope.source_bot_username),
6246
+ source_message_envelope: effectiveSourceMessageEnvelope,
6247
+ root_source_message_id: rootSourceMessageID || undefined,
6248
+ root_source_message_thread_id: rootSourceMessageThreadID || undefined,
6249
+ root_source_message_body: rootSourceMessageBody,
6250
+ root_source_message_origin: String(
6251
+ (shouldReuseDirectMessageRequest
6252
+ ? existing.root_source_message_origin || existing.source_message_origin
6253
+ : rootSourceMessageEnvelope.source_origin)
6254
+ || "",
6255
+ ).trim().toLowerCase(),
6256
+ root_source_message_route_key: String(
6257
+ (shouldReuseDirectMessageRequest
6258
+ ? existing.root_source_message_route_key || existing.source_message_route_key
6259
+ : rootSourceMessageEnvelope.source_route_key)
6260
+ || "",
6261
+ ).trim(),
6262
+ root_source_message_bot_username: normalizeTelegramMentionUsername(
6263
+ (shouldReuseDirectMessageRequest
6264
+ ? existing.root_source_message_bot_username || existing.source_message_bot_username
6265
+ : rootSourceMessageEnvelope.source_bot_username),
6266
+ ),
6267
+ root_source_message_envelope: rootSourceMessageEnvelope,
6268
+ current_source_message_id: currentSourceMessageID || undefined,
6269
+ current_source_message_thread_id: currentSourceMessageThreadID || undefined,
6270
+ current_source_message_body: currentSourceMessageBody,
6271
+ current_source_message_origin: String(currentSourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
6272
+ current_source_message_route_key: String(currentSourceMessageEnvelope.source_route_key || "").trim(),
6273
+ current_source_message_bot_username: normalizeTelegramMentionUsername(
6274
+ currentSourceMessageEnvelope.source_bot_username,
6275
+ ),
6276
+ current_source_message_envelope: currentSourceMessageEnvelope,
6277
+ current_turn_relation: currentTurnRelation,
6278
+ root_comment_id: shouldReuseDirectMessageRequest
6279
+ ? String(existing.root_comment_id || selectedRecord?.id || "").trim()
6280
+ : String(selectedRecord?.id || "").trim(),
6281
+ root_comment_kind: shouldReuseDirectMessageRequest
6282
+ ? String(existing.root_comment_kind || commentKind).trim().toLowerCase()
6283
+ : commentKind,
6284
+ conversation_id: effectiveConversationID,
5246
6285
  reply_chain_context: authorityReplyChainContext,
5247
6286
  selected_bot_usernames: effectiveSelectedBotUsernames,
5248
- authoritative_decision_bundle: authoritativeDecisionBundle,
5249
- decision_bundle_validation_status: String(decisionBundleValidation.status || "").trim(),
5250
- decision_bundle_validation_reason: String(decisionBundleValidation.reason || "").trim(),
6287
+ authoritative_decision_bundle: shouldReuseDirectMessageRequest
6288
+ ? safeObject(existing.authoritative_decision_bundle)
6289
+ : authoritativeDecisionBundle,
6290
+ decision_bundle_validation_status: shouldReuseDirectMessageRequest
6291
+ ? String(existing.decision_bundle_validation_status || "").trim()
6292
+ : String(decisionBundleValidation.status || "").trim(),
6293
+ decision_bundle_validation_reason: shouldReuseDirectMessageRequest
6294
+ ? String(existing.decision_bundle_validation_reason || "").trim()
6295
+ : String(decisionBundleValidation.reason || "").trim(),
5251
6296
  conversation_intent_mode: String(
5252
6297
  (hasAuthoritativeConversationDecision
5253
6298
  ? authoritativeDecisionBundle.conversation_intent_mode
5254
6299
  : "")
5255
6300
  || normalizedSharedHumanIntent.intentMode
5256
6301
  || existing.conversation_intent_mode
5257
- || authoritySource.conversation_intent_mode
6302
+ || effectiveAuthoritySource.conversation_intent_mode
5258
6303
  || "",
5259
6304
  ).trim().toLowerCase(),
5260
6305
  conversation_lead_bot: normalizeTelegramMentionUsername(
@@ -5263,7 +6308,7 @@ async function claimRunnerRequestForHumanComment({
5263
6308
  : "")
5264
6309
  || normalizedSharedHumanIntent.leadBotSelector
5265
6310
  || existing.conversation_lead_bot
5266
- || authoritySource.conversation_lead_bot,
6311
+ || effectiveAuthoritySource.conversation_lead_bot,
5267
6312
  ),
5268
6313
  conversation_summary_bot: normalizeTelegramMentionUsername(
5269
6314
  (hasAuthoritativeConversationDecision
@@ -5271,7 +6316,7 @@ async function claimRunnerRequestForHumanComment({
5271
6316
  : "")
5272
6317
  || normalizedSharedHumanIntent.summaryBotSelector
5273
6318
  || existing.conversation_summary_bot
5274
- || authoritySource.conversation_summary_bot,
6319
+ || effectiveAuthoritySource.conversation_summary_bot,
5275
6320
  ),
5276
6321
  conversation_participants: uniqueOrderedStrings(
5277
6322
  hasAuthoritativeConversationDecision && decisionConversationParticipants.length
@@ -5280,10 +6325,10 @@ async function claimRunnerRequestForHumanComment({
5280
6325
  ? normalizedSharedHumanIntent.participantSelectors
5281
6326
  : ensureArray(existing.conversation_participants).length
5282
6327
  ? existing.conversation_participants
5283
- : ensureArray(authoritySource.conversation_participants).length
5284
- ? authoritySource.conversation_participants
5285
- : [],
5286
- normalizeTelegramMentionUsername,
6328
+ : ensureArray(effectiveAuthoritySource.conversation_participants).length
6329
+ ? effectiveAuthoritySource.conversation_participants
6330
+ : [],
6331
+ normalizeTelegramMentionUsername,
5287
6332
  ),
5288
6333
  conversation_initial_responders: uniqueOrderedStrings(
5289
6334
  hasAuthoritativeConversationDecision && decisionInitialResponders.length
@@ -5292,10 +6337,10 @@ async function claimRunnerRequestForHumanComment({
5292
6337
  ? normalizedSharedHumanIntent.initialResponderSelectors
5293
6338
  : ensureArray(existing.conversation_initial_responders).length
5294
6339
  ? existing.conversation_initial_responders
5295
- : ensureArray(authoritySource.conversation_initial_responders).length
5296
- ? authoritySource.conversation_initial_responders
5297
- : [],
5298
- normalizeTelegramMentionUsername,
6340
+ : ensureArray(effectiveAuthoritySource.conversation_initial_responders).length
6341
+ ? effectiveAuthoritySource.conversation_initial_responders
6342
+ : [],
6343
+ normalizeTelegramMentionUsername,
5299
6344
  ),
5300
6345
  conversation_allowed_responders: uniqueOrderedStrings(
5301
6346
  hasAuthoritativeConversationDecision && decisionAllowedResponders.length
@@ -5304,9 +6349,9 @@ async function claimRunnerRequestForHumanComment({
5304
6349
  ? normalizedSharedHumanIntent.allowedResponderSelectors
5305
6350
  : ensureArray(existing.conversation_allowed_responders).length
5306
6351
  ? existing.conversation_allowed_responders
5307
- : ensureArray(authoritySource.conversation_allowed_responders).length
5308
- ? authoritySource.conversation_allowed_responders
5309
- : [],
6352
+ : ensureArray(effectiveAuthoritySource.conversation_allowed_responders).length
6353
+ ? effectiveAuthoritySource.conversation_allowed_responders
6354
+ : [],
5310
6355
  normalizeTelegramMentionUsername,
5311
6356
  ),
5312
6357
  conversation_allow_bot_to_bot: (hasAuthoritativeConversationDecision
@@ -5314,14 +6359,14 @@ async function claimRunnerRequestForHumanComment({
5314
6359
  : false)
5315
6360
  || normalizedSharedHumanIntent.allowBotToBot === true
5316
6361
  || existing.conversation_allow_bot_to_bot === true
5317
- || authoritySource.conversation_allow_bot_to_bot === true,
6362
+ || effectiveAuthoritySource.conversation_allow_bot_to_bot === true,
5318
6363
  conversation_reply_expectation: String(
5319
6364
  (hasAuthoritativeConversationDecision
5320
6365
  ? authoritativeDecisionBundle.conversation_reply_expectation
5321
6366
  : "")
5322
6367
  || normalizedSharedHumanIntent.replyExpectation
5323
6368
  || existing.conversation_reply_expectation
5324
- || authoritySource.conversation_reply_expectation
6369
+ || effectiveAuthoritySource.conversation_reply_expectation
5325
6370
  || "",
5326
6371
  ).trim().toLowerCase(),
5327
6372
  execution_contract_type: String(
@@ -5329,20 +6374,20 @@ async function claimRunnerRequestForHumanComment({
5329
6374
  ? authoritativeDecisionBundle.execution_contract_type
5330
6375
  : "")
5331
6376
  || runnerRequestPreferredExecutionContractType(existing)
5332
- || runnerRequestPreferredExecutionContractType(authoritySource)
6377
+ || runnerRequestPreferredExecutionContractType(effectiveAuthoritySource)
5333
6378
  || "",
5334
6379
  ).trim().toLowerCase(),
5335
6380
  execution_contract_actionable: (hasAuthoritativeConversationDecision
5336
6381
  ? authoritativeDecisionBundle.execution_contract_actionable === true
5337
6382
  : false)
5338
6383
  || runnerRequestPreferredExecutionContractActionable(existing)
5339
- || runnerRequestPreferredExecutionContractActionable(authoritySource),
6384
+ || runnerRequestPreferredExecutionContractActionable(effectiveAuthoritySource),
5340
6385
  execution_contract_targets: uniqueOrderedStrings(
5341
6386
  hasAuthoritativeConversationDecision && ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
5342
6387
  ? authoritativeDecisionBundle.execution_contract_targets
5343
6388
  : runnerRequestPreferredExecutionContractTargets(existing).length
5344
6389
  ? runnerRequestPreferredExecutionContractTargets(existing)
5345
- : runnerRequestPreferredExecutionContractTargets(authoritySource),
6390
+ : runnerRequestPreferredExecutionContractTargets(effectiveAuthoritySource),
5346
6391
  normalizeTelegramMentionUsername,
5347
6392
  ),
5348
6393
  next_expected_responders: uniqueOrderedStrings(
@@ -5350,31 +6395,77 @@ async function claimRunnerRequestForHumanComment({
5350
6395
  ? authoritativeDecisionBundle.next_expected_responders
5351
6396
  : runnerRequestPreferredNextExpectedResponders(existing).length
5352
6397
  ? runnerRequestPreferredNextExpectedResponders(existing)
5353
- : runnerRequestPreferredNextExpectedResponders(authoritySource),
6398
+ : runnerRequestPreferredNextExpectedResponders(effectiveAuthoritySource),
5354
6399
  normalizeTelegramMentionUsername,
5355
- ),
5356
- normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
6400
+ ),
6401
+ normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
5357
6402
  status: "claimed",
5358
- claimed_by_route: String(routeKey || "").trim(),
5359
- claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
5360
- root_work_item_id: String(existing.root_work_item_id || authoritySource.root_work_item_id || "").trim(),
5361
- root_work_item_title: String(existing.root_work_item_title || authoritySource.root_work_item_title || "").trim(),
5362
- root_work_item_status: normalizeRunnerWorkItemStatus(
5363
- existing.root_work_item_status || authoritySource.root_work_item_status,
5364
- ),
5365
- root_thread_id: String(existing.root_thread_id || authoritySource.root_thread_id || "").trim(),
5366
- root_work_item_created_at: firstNonEmptyString([
5367
- existing.root_work_item_created_at,
5368
- authoritySource.root_work_item_created_at,
5369
- ]),
5370
- root_work_item_last_error: String(
5371
- existing.root_work_item_last_error || authoritySource.root_work_item_last_error || "",
5372
- ).trim(),
6403
+ claimed_by_route: String(routeKey || "").trim(),
6404
+ claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
6405
+ root_work_item_id: String(existing.root_work_item_id || effectiveAuthoritySource.root_work_item_id || "").trim(),
6406
+ root_work_item_title: String(existing.root_work_item_title || effectiveAuthoritySource.root_work_item_title || "").trim(),
6407
+ root_work_item_status: normalizeRunnerWorkItemStatus(
6408
+ existing.root_work_item_status || effectiveAuthoritySource.root_work_item_status,
6409
+ ),
6410
+ root_thread_id: String(existing.root_thread_id || effectiveAuthoritySource.root_thread_id || "").trim(),
6411
+ root_work_item_created_at: firstNonEmptyString([
6412
+ existing.root_work_item_created_at,
6413
+ effectiveAuthoritySource.root_work_item_created_at,
6414
+ ]),
6415
+ root_work_item_last_error: String(
6416
+ existing.root_work_item_last_error || effectiveAuthoritySource.root_work_item_last_error || "",
6417
+ ).trim(),
5373
6418
  last_comment_id: String(selectedRecord?.id || "").trim(),
5374
6419
  last_comment_kind: commentKind,
5375
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5376
- last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5377
- });
6420
+ last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
6421
+ last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
6422
+ });
6423
+ if (isDirectMessageRestart) {
6424
+ const restartBotUsername = firstNonEmptyString(claimBotUsernameCandidates);
6425
+ const restartClosedRequests = closeRunnerDirectMessageScopedActiveRequests({
6426
+ state: {
6427
+ ...stateForClaim,
6428
+ requests: nextRequests,
6429
+ },
6430
+ normalizedRoute,
6431
+ routeKey,
6432
+ chatID: normalizedChatID,
6433
+ botUsername: restartBotUsername,
6434
+ excludeRequestKey: requestKey,
6435
+ });
6436
+ nextRequests = normalizeBotRunnerRequests(restartClosedRequests.requests);
6437
+ request = safeObject(nextRequests[requestKey]);
6438
+ }
6439
+ if (isDirectMessageRestart && directMessageRestartRequestKeysToClose.size > 0) {
6440
+ const restartFinalNowISO = new Date().toISOString();
6441
+ const claimedRequestKey = String(requestKey || "").trim();
6442
+ for (const restartRequestKeyRaw of directMessageRestartRequestKeysToClose) {
6443
+ const restartRequestKey = String(restartRequestKeyRaw || "").trim();
6444
+ if (!restartRequestKey || restartRequestKey === claimedRequestKey) {
6445
+ continue;
6446
+ }
6447
+ const existingRestartRequest = safeObject(
6448
+ nextRequests[restartRequestKey]
6449
+ || safeObject(stateForClaim.requests)[restartRequestKey]
6450
+ || safeObject(currentState.requests)[restartRequestKey]
6451
+ || safeObject(requests)[restartRequestKey],
6452
+ );
6453
+ if (
6454
+ !String(existingRestartRequest.request_key || "").trim()
6455
+ || String(existingRestartRequest.chat_id || "").trim() !== normalizedChatID
6456
+ ) {
6457
+ continue;
6458
+ }
6459
+ nextRequests[restartRequestKey] = {
6460
+ ...existingRestartRequest,
6461
+ status: "closed",
6462
+ closed_reason: "direct_message_restart",
6463
+ closed_at: restartFinalNowISO,
6464
+ updated_at: restartFinalNowISO,
6465
+ };
6466
+ }
6467
+ request = safeObject(nextRequests[requestKey]);
6468
+ }
5378
6469
  const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
5379
6470
  project_id: String(normalizedRoute?.projectID || "").trim(),
5380
6471
  provider: String(normalizedRoute?.provider || "").trim(),
@@ -5388,18 +6479,55 @@ async function claimRunnerRequestForHumanComment({
5388
6479
  comment_kind: commentKind,
5389
6480
  request_status: "claimed",
5390
6481
  });
5391
- saveBotRunnerState({
5392
- routes: stateForClaim.routes,
5393
- sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
5394
- excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
5395
- requests: nextRequests,
5396
- consumedComments: nextConsumedComments,
5397
- });
5398
- return {
5399
- ok: true,
5400
- requestKey,
5401
- request,
5402
- };
6482
+ saveBotRunnerState({
6483
+ routes: stateForClaim.routes,
6484
+ sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
6485
+ excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
6486
+ requests: nextRequests,
6487
+ consumedComments: nextConsumedComments,
6488
+ });
6489
+ if (isDirectMessageRestart && directMessageRestartRequestKeysToClose.size > 0) {
6490
+ const persistedState = loadBotRunnerState();
6491
+ const persistedRequests = normalizeBotRunnerRequests(persistedState.requests);
6492
+ let shouldResavePersistedRequests = false;
6493
+ const persistedRestartNowISO = new Date().toISOString();
6494
+ const claimedRequestKey = String(requestKey || "").trim();
6495
+ for (const restartRequestKey of directMessageRestartRequestKeysToClose) {
6496
+ if (!restartRequestKey || restartRequestKey === claimedRequestKey) {
6497
+ continue;
6498
+ }
6499
+ const existingPersistedRequest = safeObject(persistedRequests[restartRequestKey]);
6500
+ if (
6501
+ !String(existingPersistedRequest.request_key || "").trim()
6502
+ || !isActiveRunnerRequestStatus(existingPersistedRequest.status)
6503
+ || String(existingPersistedRequest.chat_id || "").trim() !== normalizedChatID
6504
+ ) {
6505
+ continue;
6506
+ }
6507
+ persistedRequests[restartRequestKey] = {
6508
+ ...existingPersistedRequest,
6509
+ status: "closed",
6510
+ closed_reason: "direct_message_restart",
6511
+ closed_at: persistedRestartNowISO,
6512
+ updated_at: persistedRestartNowISO,
6513
+ };
6514
+ shouldResavePersistedRequests = true;
6515
+ }
6516
+ if (shouldResavePersistedRequests) {
6517
+ saveBotRunnerState({
6518
+ routes: persistedState.routes,
6519
+ sharedInboxes: persistedState.sharedInboxes || persistedState.shared_inboxes,
6520
+ excludedComments: persistedState.excludedComments || persistedState.excluded_comments,
6521
+ requests: persistedRequests,
6522
+ consumedComments: persistedState.consumedComments || persistedState.consumed_comments,
6523
+ });
6524
+ }
6525
+ }
6526
+ return {
6527
+ ok: true,
6528
+ requestKey,
6529
+ request,
6530
+ };
5403
6531
  }
5404
6532
 
5405
6533
  function runnerRequestRequiresActionableContract(requestRaw) {
@@ -6414,6 +7542,9 @@ function markRunnerRequestLifecycle({
6414
7542
  const sourceMessageEnvelope = Object.keys(normalizedAuthoritativeSourceMessageEnvelope).length
6415
7543
  ? normalizedAuthoritativeSourceMessageEnvelope
6416
7544
  : normalizeRunnerTelegramMessageEnvelope(existing.source_message_envelope);
7545
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
7546
+ const isRootHumanComment = ["telegram_message", "telegram_edited_message"].includes(commentKind);
7547
+ const isFollowupComment = !isRootHumanComment;
6417
7548
  const normalizedDecisionBundle = normalizeRunnerConversationDecisionBundle(decisionBundle);
6418
7549
  const resolvedDecisionBundleValidation = Object.keys(safeObject(decisionBundle)).length > 0
6419
7550
  ? validateRunnerConversationDecisionBundle(normalizedDecisionBundle)
@@ -6423,9 +7554,60 @@ function markRunnerRequestLifecycle({
6423
7554
  reason: String(decisionBundleValidationReason || "").trim(),
6424
7555
  bundle: {},
6425
7556
  };
6426
- const authoritativeDecisionBundle = resolvedDecisionBundleValidation.ok === true
7557
+ const existingAuthoritativeDecisionBundle = runnerRequestAuthoritativeDecisionBundle(existing);
7558
+ const incomingDecisionBundle = resolvedDecisionBundleValidation.ok === true
6427
7559
  ? safeObject(resolvedDecisionBundleValidation.bundle)
6428
- : runnerRequestAuthoritativeDecisionBundle(existing);
7560
+ : {};
7561
+ const existingHasImmutableRootAuthority = runnerDecisionBundleIsRootHumanOpening(
7562
+ existingAuthoritativeDecisionBundle,
7563
+ );
7564
+ const incomingDecisionType = runnerDecisionBundleDecisionType(
7565
+ Object.keys(incomingDecisionBundle).length > 0 ? incomingDecisionBundle : normalizedDecisionBundle,
7566
+ );
7567
+ const shouldWriteIncomingAuthoritativeDecisionBundle = incomingDecisionType === "human_opening"
7568
+ && Object.keys(incomingDecisionBundle).length > 0;
7569
+ const preserveExistingAuthoritativeDecisionBundle = Object.keys(existingAuthoritativeDecisionBundle).length > 0
7570
+ && (
7571
+ isFollowupComment
7572
+ || existingHasImmutableRootAuthority
7573
+ || !shouldWriteIncomingAuthoritativeDecisionBundle
7574
+ );
7575
+ const authoritativeDecisionBundle = preserveExistingAuthoritativeDecisionBundle
7576
+ ? existingAuthoritativeDecisionBundle
7577
+ : Object.keys(incomingDecisionBundle).length > 0
7578
+ ? incomingDecisionBundle
7579
+ : existingAuthoritativeDecisionBundle;
7580
+ const effectiveDecisionBundleValidationStatus = preserveExistingAuthoritativeDecisionBundle
7581
+ ? String(existing.decision_bundle_validation_status || "").trim()
7582
+ : String(
7583
+ resolvedDecisionBundleValidation.status
7584
+ || decisionBundleValidationStatus
7585
+ || existing.decision_bundle_validation_status
7586
+ || "",
7587
+ ).trim();
7588
+ const effectiveDecisionBundleValidationReason = preserveExistingAuthoritativeDecisionBundle
7589
+ ? String(existing.decision_bundle_validation_reason || "").trim()
7590
+ : String(
7591
+ resolvedDecisionBundleValidation.reason
7592
+ || decisionBundleValidationReason
7593
+ || existing.decision_bundle_validation_reason
7594
+ || "",
7595
+ ).trim();
7596
+ const lastReplyDecisionBundle = Object.keys(incomingDecisionBundle).length > 0
7597
+ ? incomingDecisionBundle
7598
+ : safeObject(existing.last_reply_decision_bundle);
7599
+ const lastReplyDecisionBundleValidationStatus = String(
7600
+ resolvedDecisionBundleValidation.status
7601
+ || decisionBundleValidationStatus
7602
+ || existing.last_reply_decision_bundle_validation_status
7603
+ || "",
7604
+ ).trim();
7605
+ const lastReplyDecisionBundleValidationReason = String(
7606
+ resolvedDecisionBundleValidation.reason
7607
+ || decisionBundleValidationReason
7608
+ || existing.last_reply_decision_bundle_validation_reason
7609
+ || "",
7610
+ ).trim();
6429
7611
  const effectiveReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
6430
7612
  const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
6431
7613
  sourceEnvelope: sourceMessageEnvelope,
@@ -6439,7 +7621,7 @@ function markRunnerRequestLifecycle({
6439
7621
  const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
6440
7622
  const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
6441
7623
  && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
6442
- const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
7624
+ const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
6443
7625
  sourceEnvelope: sourceMessageEnvelope,
6444
7626
  chatID: existing.chat_id,
6445
7627
  messageThreadID: lastReplyMessageThreadID,
@@ -6455,9 +7637,13 @@ function markRunnerRequestLifecycle({
6455
7637
  || intFromRawAllowZero(replyToMessageID, 0) > 0
6456
7638
  || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6457
7639
  );
7640
+ const continuationDecisionBundle = Object.keys(incomingDecisionBundle).length > 0
7641
+ ? incomingDecisionBundle
7642
+ : safeObject(existing.last_reply_decision_bundle);
6458
7643
  const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
6459
7644
  [
6460
7645
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets),
7646
+ ...ensureArray(continuationDecisionBundle.execution_contract_targets),
6461
7647
  ...ensureArray(executionContractTargets),
6462
7648
  ...ensureArray(normalizedExecutionContractTargets),
6463
7649
  ...ensureArray(responseContractValidationTargets),
@@ -6467,6 +7653,7 @@ function markRunnerRequestLifecycle({
6467
7653
  const rootEffectiveNextExpectedResponders = uniqueOrderedStrings(
6468
7654
  [
6469
7655
  ...ensureArray(authoritativeDecisionBundle.next_expected_responders),
7656
+ ...ensureArray(continuationDecisionBundle.next_expected_responders),
6470
7657
  ...ensureArray(nextExpectedResponders),
6471
7658
  ...ensureArray(normalizedExecutionNextResponders),
6472
7659
  ...ensureArray(responseContractValidationTargets),
@@ -6477,11 +7664,15 @@ function markRunnerRequestLifecycle({
6477
7664
  [
6478
7665
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
6479
7666
  ? ensureArray(authoritativeDecisionBundle.execution_contract_targets)
7667
+ : ensureArray(continuationDecisionBundle.execution_contract_targets).length
7668
+ ? ensureArray(continuationDecisionBundle.execution_contract_targets)
6480
7669
  : ensureArray(executionContractTargets).length
6481
7670
  ? ensureArray(executionContractTargets)
6482
7671
  : ensureArray(existing.execution_contract_targets),
6483
7672
  ...ensureArray(authoritativeDecisionBundle.next_expected_responders).length
6484
7673
  ? ensureArray(authoritativeDecisionBundle.next_expected_responders)
7674
+ : ensureArray(continuationDecisionBundle.next_expected_responders).length
7675
+ ? ensureArray(continuationDecisionBundle.next_expected_responders)
6485
7676
  : ensureArray(nextExpectedResponders).length
6486
7677
  ? ensureArray(nextExpectedResponders)
6487
7678
  : ensureArray(existing.next_expected_responders),
@@ -6490,13 +7681,20 @@ function markRunnerRequestLifecycle({
6490
7681
  ).filter((selector) => selector && selector !== normalizedCurrentBotSelector);
6491
7682
  const nextExecutionContractType = String(
6492
7683
  authoritativeDecisionBundle.execution_contract_type
7684
+ || continuationDecisionBundle.execution_contract_type
6493
7685
  || executionContractType
6494
7686
  || existing.execution_contract_type
6495
7687
  || "",
6496
7688
  ).trim().toLowerCase();
7689
+ const shouldPromoteRootContinuationState = isRootHumanComment || existingHasImmutableRootAuthority;
6497
7690
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6498
7691
  const normalizedFailureReplyClassification = String(failureReplyClassification || "").trim().toLowerCase();
6499
7692
  const normalizedFailureFacts = safeObject(failureFacts);
7693
+ const isTerminalStaleReplyAnchorSkip = normalizedOutcome === "skipped"
7694
+ && (
7695
+ normalizedDeliveryStatus === "skipped_stale_reply_anchor"
7696
+ || String(closedReason || "").trim().toLowerCase() === "stale_reply_anchor"
7697
+ );
6500
7698
  const shouldPersistReplyAnchor = (
6501
7699
  aiReplyGenerated === true
6502
7700
  || intFromRawAllowZero(lastReplyMessageID, 0) > 0
@@ -6511,6 +7709,7 @@ function markRunnerRequestLifecycle({
6511
7709
  : continuationSelectors.length > 0;
6512
7710
  const shouldRemainRunningAfterSkip = normalizedOutcome === "skipped"
6513
7711
  && parsedKind === "bot_reply"
7712
+ && !isTerminalStaleReplyAnchorSkip
6514
7713
  && authoritativeDecisionBundle.should_close_after_reply !== true
6515
7714
  && (
6516
7715
  nextExecutionContractType === "delegation"
@@ -6571,25 +7770,17 @@ function markRunnerRequestLifecycle({
6571
7770
  return normalizeRunnerRequestStatus(existing.status);
6572
7771
  })();
6573
7772
  const nowISO = new Date().toISOString();
6574
- const commentKind = String(parsed.kind || "").trim().toLowerCase();
6575
- const isRootHumanComment = ["telegram_message", "telegram_edited_message"].includes(commentKind);
6576
- const isFollowupComment = !isRootHumanComment;
6577
7773
  const patch = {
6578
7774
  authoritative_decision_bundle: Object.keys(authoritativeDecisionBundle).length > 0
6579
7775
  ? authoritativeDecisionBundle
6580
7776
  : safeObject(existing.authoritative_decision_bundle),
6581
- decision_bundle_validation_status: String(
6582
- resolvedDecisionBundleValidation.status
6583
- || decisionBundleValidationStatus
6584
- || existing.decision_bundle_validation_status
6585
- || "",
6586
- ).trim(),
6587
- decision_bundle_validation_reason: String(
6588
- resolvedDecisionBundleValidation.reason
6589
- || decisionBundleValidationReason
6590
- || existing.decision_bundle_validation_reason
6591
- || "",
6592
- ).trim(),
7777
+ decision_bundle_validation_status: effectiveDecisionBundleValidationStatus,
7778
+ decision_bundle_validation_reason: effectiveDecisionBundleValidationReason,
7779
+ last_reply_decision_bundle: Object.keys(lastReplyDecisionBundle).length > 0
7780
+ ? lastReplyDecisionBundle
7781
+ : safeObject(existing.last_reply_decision_bundle),
7782
+ last_reply_decision_bundle_validation_status: lastReplyDecisionBundleValidationStatus,
7783
+ last_reply_decision_bundle_validation_reason: lastReplyDecisionBundleValidationReason,
6593
7784
  conversation_id: conversationID,
6594
7785
  conversation_participants: uniqueOrderedStrings(
6595
7786
  ensureArray(authoritativeDecisionBundle.participants).length
@@ -6644,7 +7835,7 @@ function markRunnerRequestLifecycle({
6644
7835
  execution_contract_targets: uniqueOrderedStrings(
6645
7836
  ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
6646
7837
  ? authoritativeDecisionBundle.execution_contract_targets
6647
- : isRootHumanComment && rootEffectiveExecutionContractTargets.length
7838
+ : shouldPromoteRootContinuationState && rootEffectiveExecutionContractTargets.length
6648
7839
  ? rootEffectiveExecutionContractTargets
6649
7840
  : ensureArray(executionContractTargets).length
6650
7841
  ? executionContractTargets
@@ -6654,7 +7845,7 @@ function markRunnerRequestLifecycle({
6654
7845
  next_expected_responders: uniqueOrderedStrings(
6655
7846
  ensureArray(authoritativeDecisionBundle.next_expected_responders).length
6656
7847
  ? authoritativeDecisionBundle.next_expected_responders
6657
- : isRootHumanComment && rootEffectiveNextExpectedResponders.length
7848
+ : shouldPromoteRootContinuationState && rootEffectiveNextExpectedResponders.length
6658
7849
  ? rootEffectiveNextExpectedResponders
6659
7850
  : ensureArray(nextExpectedResponders).length
6660
7851
  ? nextExpectedResponders
@@ -6690,24 +7881,24 @@ function markRunnerRequestLifecycle({
6690
7881
  ? aiReplyPreview || existing.followup_ai_reply_preview || ""
6691
7882
  : existing.followup_ai_reply_preview || "",
6692
7883
  ).trim(),
6693
- root_execution_contract_type: String(
6694
- isRootHumanComment
6695
- ? nextExecutionContractType
6696
- : existing.root_execution_contract_type || existing.execution_contract_type || "",
6697
- ).trim().toLowerCase(),
6698
- root_execution_contract_targets: uniqueOrderedStrings(
6699
- isRootHumanComment && rootEffectiveExecutionContractTargets.length
6700
- ? rootEffectiveExecutionContractTargets
6701
- : ensureArray(existing.root_execution_contract_targets).length
6702
- ? existing.root_execution_contract_targets
7884
+ root_execution_contract_type: String(
7885
+ shouldPromoteRootContinuationState
7886
+ ? nextExecutionContractType
7887
+ : existing.root_execution_contract_type || existing.execution_contract_type || "",
7888
+ ).trim().toLowerCase(),
7889
+ root_execution_contract_targets: uniqueOrderedStrings(
7890
+ shouldPromoteRootContinuationState && rootEffectiveExecutionContractTargets.length
7891
+ ? rootEffectiveExecutionContractTargets
7892
+ : ensureArray(existing.root_execution_contract_targets).length
7893
+ ? existing.root_execution_contract_targets
6703
7894
  : existing.execution_contract_targets,
6704
7895
  normalizeTelegramMentionUsername,
6705
7896
  ),
6706
- root_next_expected_responders: uniqueOrderedStrings(
6707
- isRootHumanComment && rootEffectiveNextExpectedResponders.length
6708
- ? rootEffectiveNextExpectedResponders
6709
- : ensureArray(existing.root_next_expected_responders).length
6710
- ? existing.root_next_expected_responders
7897
+ root_next_expected_responders: uniqueOrderedStrings(
7898
+ shouldPromoteRootContinuationState && rootEffectiveNextExpectedResponders.length
7899
+ ? rootEffectiveNextExpectedResponders
7900
+ : ensureArray(existing.root_next_expected_responders).length
7901
+ ? existing.root_next_expected_responders
6711
7902
  : existing.next_expected_responders,
6712
7903
  normalizeTelegramMentionUsername,
6713
7904
  ),
@@ -7001,18 +8192,36 @@ function cleanupBotRunnerRequestState({
7001
8192
  }
7002
8193
  const expiresAtMs = Date.parse(String(session.expires_at || "").trim());
7003
8194
  const expired = Number.isFinite(expiresAtMs) && expiresAtMs <= nowMs;
7004
- const activeRequests = Object.values(nextRequests).filter((entry) => (
7005
- String(entry.project_id || "").trim() === String(candidateRoute.projectID || "").trim()
7006
- && String(entry.provider || "").trim() === String(candidateRoute.provider || "").trim()
7007
- && String(entry.conversation_id || "").trim() === String(conversationID || "").trim()
7008
- && isActiveRunnerRequestStatus(entry.status)
7009
- ));
7010
- const pendingContinuationResponders = ensureArray(session.next_expected_responders)
7011
- .map((value) => normalizeTelegramMentionUsername(value))
7012
- .filter(Boolean);
7013
- if (!expired && (activeRequests.length > 0 || pendingContinuationResponders.length > 0)) {
7014
- continue;
7015
- }
8195
+ let activeRequests = Object.values(nextRequests).filter((entry) => (
8196
+ String(entry.project_id || "").trim() === String(candidateRoute.projectID || "").trim()
8197
+ && String(entry.provider || "").trim() === String(candidateRoute.provider || "").trim()
8198
+ && String(entry.conversation_id || "").trim() === String(conversationID || "").trim()
8199
+ && isActiveRunnerRequestStatus(entry.status)
8200
+ ));
8201
+ if (String(session.mode || "").trim() === "direct_single_bot") {
8202
+ const directMessageRequestKey = String(session.request_key || "").trim();
8203
+ if (directMessageRequestKey) {
8204
+ const directMessageRequest = safeObject(nextRequests[directMessageRequestKey]);
8205
+ if (
8206
+ Object.keys(directMessageRequest).length > 0
8207
+ && isActiveRunnerRequestStatus(directMessageRequest.status)
8208
+ && String(directMessageRequest.project_id || "").trim() === String(candidateRoute.projectID || "").trim()
8209
+ && String(directMessageRequest.provider || "").trim() === String(candidateRoute.provider || "").trim()
8210
+ && !activeRequests.some((entry) => String(entry.request_key || "").trim() === directMessageRequestKey)
8211
+ ) {
8212
+ activeRequests = [...activeRequests, directMessageRequest];
8213
+ }
8214
+ }
8215
+ }
8216
+ const pendingContinuationResponders = ensureArray(session.next_expected_responders)
8217
+ .map((value) => normalizeTelegramMentionUsername(value))
8218
+ .filter(Boolean);
8219
+ if (String(session.mode || "").trim() === "direct_single_bot" && !expired) {
8220
+ continue;
8221
+ }
8222
+ if (!expired && (activeRequests.length > 0 || pendingContinuationResponders.length > 0)) {
8223
+ continue;
8224
+ }
7016
8225
  const closedReason = expired ? "expired_session" : "orphaned_open_session";
7017
8226
  conversationSessions[conversationID] = {
7018
8227
  ...session,
@@ -7161,6 +8370,8 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7161
8370
  preserveLocalStringWhenServerBlank("conversation_reply_expectation");
7162
8371
  preserveLocalStringWhenServerBlank("decision_bundle_validation_status");
7163
8372
  preserveLocalStringWhenServerBlank("decision_bundle_validation_reason");
8373
+ preserveLocalStringWhenServerBlank("last_reply_decision_bundle_validation_status");
8374
+ preserveLocalStringWhenServerBlank("last_reply_decision_bundle_validation_reason");
7164
8375
  preserveLocalStringWhenServerBlank("execution_contract_type");
7165
8376
  preserveLocalStringWhenServerBlank("normalized_execution_contract_type");
7166
8377
  preserveLocalStringWhenServerBlank("ai_reply_generated_at");
@@ -7190,6 +8401,7 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7190
8401
  preserveLocalArrayWhenServerEmpty("followup_response_contract_validation_targets");
7191
8402
  preserveLocalArrayWhenServerEmpty("followup_assignment_validation_modes");
7192
8403
  preserveLocalObjectWhenServerBlank("authoritative_decision_bundle");
8404
+ preserveLocalObjectWhenServerBlank("last_reply_decision_bundle");
7193
8405
  preserveLocalObjectWhenServerBlank("reply_chain_context");
7194
8406
  clearMergedStringWhenServerBlank("root_execution_contract_type");
7195
8407
  clearMergedArrayWhenServerEmpty("root_execution_contract_targets");
@@ -7760,31 +8972,60 @@ function scanExternalProjectArtifacts({
7760
8972
  };
7761
8973
  }
7762
8974
 
7763
- function normalizeRunnerRouteIdentityText(rawValue) {
7764
- return String(rawValue || "").trim().toLowerCase();
7765
- }
7766
-
7767
- function runnerRouteLogicalSignature(route) {
7768
- const normalized = normalizeRunnerRoute(route);
7769
- return [
7770
- normalized.projectID || "-",
7771
- normalized.provider || "-",
7772
- normalized.role || "-",
7773
- normalized.botID || normalizeRunnerRouteIdentityText(normalized.botName) || "-",
7774
- normalized.destinationID || normalizeRunnerRouteIdentityText(normalized.destinationLabel) || "-",
7775
- ].join("::");
7776
- }
7777
-
7778
- function describeRunnerRouteLogicalTarget(route) {
7779
- const normalized = normalizeRunnerRoute(route);
7780
- return [
7781
- `project_id=${normalized.projectID || "-"}`,
7782
- `provider=${normalized.provider || "-"}`,
7783
- `role=${normalized.role || "-"}`,
7784
- `server_bot=${normalized.botName || normalized.botID || "-"}`,
7785
- `destination=${normalized.destinationLabel || normalized.destinationID || "-"}`,
7786
- ].join(", ");
7787
- }
8975
+ function normalizeRunnerRouteIdentityText(rawValue) {
8976
+ return String(rawValue || "").trim().toLowerCase();
8977
+ }
8978
+
8979
+ function normalizeRunnerRouteKind(rawValue, fallback = "room") {
8980
+ const value = String(rawValue || "").trim().toLowerCase();
8981
+ if (["dm", "direct-messages", "direct_messages", "private", "private-chat", "private_chat"].includes(value)) {
8982
+ return "dm";
8983
+ }
8984
+ if (["room", "chat-room", "chat_room"].includes(value)) {
8985
+ return "room";
8986
+ }
8987
+ return fallback;
8988
+ }
8989
+
8990
+ function buildRunnerRouteTargetIdentity(route) {
8991
+ const normalized = normalizeRunnerRoute(route);
8992
+ if (normalized.routeKind === "dm") {
8993
+ return "direct-messages";
8994
+ }
8995
+ return normalized.destinationID || normalizeRunnerRouteIdentityText(normalized.destinationLabel) || "-";
8996
+ }
8997
+
8998
+ function describeRunnerRouteTargetLabel(route) {
8999
+ const normalized = normalizeRunnerRoute(route);
9000
+ if (normalized.routeKind === "dm") {
9001
+ return "Direct Messages";
9002
+ }
9003
+ return normalized.destinationLabel || normalized.destinationID || "-";
9004
+ }
9005
+
9006
+ function runnerRouteLogicalSignature(route) {
9007
+ const normalized = normalizeRunnerRoute(route);
9008
+ return [
9009
+ normalized.projectID || "-",
9010
+ normalized.provider || "-",
9011
+ normalized.routeKind || "room",
9012
+ normalized.role || "-",
9013
+ normalized.botID || normalizeRunnerRouteIdentityText(normalized.botName) || "-",
9014
+ buildRunnerRouteTargetIdentity(normalized),
9015
+ ].join("::");
9016
+ }
9017
+
9018
+ function describeRunnerRouteLogicalTarget(route) {
9019
+ const normalized = normalizeRunnerRoute(route);
9020
+ return [
9021
+ `project_id=${normalized.projectID || "-"}`,
9022
+ `provider=${normalized.provider || "-"}`,
9023
+ `route_kind=${normalized.routeKind || "room"}`,
9024
+ `role=${normalized.role || "-"}`,
9025
+ `server_bot=${normalized.botName || normalized.botID || "-"}`,
9026
+ `target=${describeRunnerRouteTargetLabel(normalized)}`,
9027
+ ].join(", ");
9028
+ }
7788
9029
 
7789
9030
  function findRunnerRouteLogicalConflicts(route, config) {
7790
9031
  const normalizedRoute = normalizeRunnerRoute(route);
@@ -7904,14 +9145,23 @@ async function runTasksWithConcurrencyLimit(items, limit, worker) {
7904
9145
  return output;
7905
9146
  }
7906
9147
 
7907
- function runnerRouteSchedulingGroupKey(route) {
7908
- const normalized = normalizeRunnerRoute(route);
7909
- return [
7910
- String(normalized.projectID || "").trim(),
7911
- String(normalized.provider || "").trim(),
7912
- String(normalized.destinationID || "").trim() || String(normalized.destinationLabel || "").trim() || String(normalized.name || "").trim(),
7913
- ].join("::");
7914
- }
9148
+ function runnerRouteSchedulingGroupKey(route) {
9149
+ const normalized = normalizeRunnerRoute(route);
9150
+ if (normalized.routeKind === "dm") {
9151
+ return [
9152
+ String(normalized.projectID || "").trim(),
9153
+ String(normalized.provider || "").trim(),
9154
+ String(normalized.routeKind || "room").trim(),
9155
+ String(normalized.botID || "").trim() || normalizeRunnerRouteIdentityText(normalized.botName) || String(normalized.name || "").trim(),
9156
+ ].join("::");
9157
+ }
9158
+ return [
9159
+ String(normalized.projectID || "").trim(),
9160
+ String(normalized.provider || "").trim(),
9161
+ String(normalized.routeKind || "room").trim(),
9162
+ String(normalized.destinationID || "").trim() || String(normalized.destinationLabel || "").trim() || String(normalized.name || "").trim(),
9163
+ ].join("::");
9164
+ }
7915
9165
 
7916
9166
  function groupRunnerRoutesBySchedulingTarget(routes) {
7917
9167
  const groups = [];
@@ -7945,13 +9195,14 @@ function normalizeBotRole(rawValue) {
7945
9195
  return "";
7946
9196
  }
7947
9197
 
7948
- function normalizeRunnerRoute(rawRoute, overrides = {}) {
7949
- const route = {
7950
- ...safeObject(rawRoute),
7951
- ...safeObject(overrides),
7952
- };
7953
- const projectID = String(route.project_id || route.projectID || "").trim();
7954
- const provider = normalizeBotProvider(route.provider);
9198
+ function normalizeRunnerRoute(rawRoute, overrides = {}) {
9199
+ const route = {
9200
+ ...safeObject(rawRoute),
9201
+ ...safeObject(overrides),
9202
+ };
9203
+ const routeKind = normalizeRunnerRouteKind(route.route_kind ?? route.routeKind);
9204
+ const projectID = String(route.project_id || route.projectID || "").trim();
9205
+ const provider = normalizeBotProvider(route.provider);
7955
9206
  const role = normalizeBotRole(route.role || route.bot_role || route.botRole);
7956
9207
  const roleProfile = normalizeRunnerRoleProfileName(route.role_profile || route.roleProfile || route.execution_profile || route.executionProfile);
7957
9208
  const name = String(route.name || "").trim();
@@ -7969,11 +9220,12 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
7969
9220
  route.dry_run_delivery ?? route.dryRunDelivery ?? process.env.METHEUS_BOT_DELIVERY_DRY_RUN,
7970
9221
  false,
7971
9222
  );
7972
- return {
7973
- name,
7974
- enabled: boolFromRaw(route.enabled, true),
7975
- projectID,
7976
- provider,
9223
+ return {
9224
+ name,
9225
+ enabled: boolFromRaw(route.enabled, true),
9226
+ routeKind,
9227
+ projectID,
9228
+ provider,
7977
9229
  role,
7978
9230
  roleProfile,
7979
9231
  botName,
@@ -7993,17 +9245,18 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
7993
9245
  };
7994
9246
  }
7995
9247
 
7996
- function runnerRouteKey(route) {
7997
- const normalized = normalizeRunnerRoute(route);
7998
- return [
7999
- normalized.name || "-",
8000
- normalized.projectID || "-",
8001
- normalized.provider || "-",
8002
- normalized.role || "-",
8003
- normalized.botID || normalized.botName || "-",
8004
- normalized.destinationID || normalized.destinationLabel || "-",
8005
- ].join("::");
8006
- }
9248
+ function runnerRouteKey(route) {
9249
+ const normalized = normalizeRunnerRoute(route);
9250
+ return [
9251
+ normalized.name || "-",
9252
+ normalized.projectID || "-",
9253
+ normalized.provider || "-",
9254
+ normalized.routeKind || "room",
9255
+ normalized.role || "-",
9256
+ normalized.botID || normalized.botName || "-",
9257
+ buildRunnerRouteTargetIdentity(normalized),
9258
+ ].join("::");
9259
+ }
8007
9260
 
8008
9261
  function validateRunnerRoute(route, { requireCommand = true, config = null } = {}) {
8009
9262
  const errors = [];
@@ -8015,12 +9268,15 @@ function validateRunnerRoute(route, { requireCommand = true, config = null } = {
8015
9268
  if (!route.provider) {
8016
9269
  errors.push("provider is required");
8017
9270
  }
8018
- if (!route.role) {
8019
- errors.push("role must be one of: monitor, review, worker, approval");
8020
- }
8021
- if (requireCommand && !route.command) {
8022
- errors.push("command is required");
8023
- }
9271
+ if (!route.role) {
9272
+ errors.push("role must be one of: monitor, review, worker, approval");
9273
+ }
9274
+ if (route.routeKind === "room" && !route.destinationID && !route.destinationLabel) {
9275
+ errors.push("destination_id or destination_label is required for room routes");
9276
+ }
9277
+ if (requireCommand && !route.command) {
9278
+ errors.push("command is required");
9279
+ }
8024
9280
  const configuredRoleProfiles = safeObject(normalizedConfig.roleProfiles);
8025
9281
  const resolvedRoleProfileName = normalizeRunnerRoleProfileName(route.roleProfile || route.role);
8026
9282
  if (resolvedRoleProfileName && !configuredRoleProfiles[resolvedRoleProfileName] && !legacyCommandAvailable) {
@@ -8081,10 +9337,10 @@ function resolveConfiguredRunnerRouteServerBotName(route, telegramEntries = [])
8081
9337
  return String(matchedTelegramEntry?.serverBotName || "").trim();
8082
9338
  }
8083
9339
 
8084
- function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags, options = {}) {
8085
- const candidate = normalizeRunnerRoute(route);
8086
- const candidateBotName = resolveConfiguredRunnerRouteServerBotName(candidate, options.telegramEntries);
8087
- if (hasRunnerFlag(flags, "route-name") && !matchesRunnerRouteText(candidate.name, flags["route-name"])) {
9340
+ function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags, options = {}) {
9341
+ const candidate = normalizeRunnerRoute(route);
9342
+ const candidateBotName = resolveConfiguredRunnerRouteServerBotName(candidate, options.telegramEntries);
9343
+ if (hasRunnerFlag(flags, "route-name") && !matchesRunnerRouteText(candidate.name, flags["route-name"])) {
8088
9344
  return false;
8089
9345
  }
8090
9346
  if (inlineRoute.projectID && candidate.projectID !== inlineRoute.projectID) {
@@ -8093,12 +9349,15 @@ function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags,
8093
9349
  if (inlineRoute.provider && candidate.provider !== inlineRoute.provider) {
8094
9350
  return false;
8095
9351
  }
8096
- if (inlineRoute.role && candidate.role !== inlineRoute.role) {
8097
- return false;
8098
- }
8099
- if (hasRunnerFlag(flags, "role-profile") && candidate.roleProfile !== inlineRoute.roleProfile) {
8100
- return false;
8101
- }
9352
+ if (inlineRoute.role && candidate.role !== inlineRoute.role) {
9353
+ return false;
9354
+ }
9355
+ if (hasRunnerFlag(flags, "route-kind") && candidate.routeKind !== inlineRoute.routeKind) {
9356
+ return false;
9357
+ }
9358
+ if (hasRunnerFlag(flags, "role-profile") && candidate.roleProfile !== inlineRoute.roleProfile) {
9359
+ return false;
9360
+ }
8102
9361
  if (inlineRoute.botID && candidate.botID !== inlineRoute.botID) {
8103
9362
  return false;
8104
9363
  }
@@ -8120,8 +9379,8 @@ function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags,
8120
9379
  return true;
8121
9380
  }
8122
9381
 
8123
- function applyRunnerRouteFlagOverrides(route, flags) {
8124
- const merged = normalizeRunnerRoute(route);
9382
+ function applyRunnerRouteFlagOverrides(route, flags) {
9383
+ const merged = normalizeRunnerRoute(route);
8125
9384
  if (hasRunnerFlag(flags, "route-name")) {
8126
9385
  merged.name = String(flags["route-name"] || "").trim();
8127
9386
  }
@@ -8134,12 +9393,15 @@ function applyRunnerRouteFlagOverrides(route, flags) {
8134
9393
  if (hasRunnerFlag(flags, "role")) {
8135
9394
  merged.role = normalizeBotRole(flags.role);
8136
9395
  }
8137
- if (hasRunnerFlag(flags, "role-profile")) {
8138
- merged.roleProfile = normalizeRunnerRoleProfileName(flags["role-profile"]);
8139
- }
8140
- if (hasRunnerFlag(flags, "bot-name")) {
8141
- merged.botName = String(flags["bot-name"] || "").trim();
8142
- }
9396
+ if (hasRunnerFlag(flags, "role-profile")) {
9397
+ merged.roleProfile = normalizeRunnerRoleProfileName(flags["role-profile"]);
9398
+ }
9399
+ if (hasRunnerFlag(flags, "route-kind")) {
9400
+ merged.routeKind = normalizeRunnerRouteKind(flags["route-kind"], merged.routeKind || "room");
9401
+ }
9402
+ if (hasRunnerFlag(flags, "bot-name")) {
9403
+ merged.botName = String(flags["bot-name"] || "").trim();
9404
+ }
8143
9405
  if (hasRunnerFlag(flags, "bot-id")) {
8144
9406
  merged.botID = String(flags["bot-id"] || "").trim();
8145
9407
  }
@@ -8225,12 +9487,13 @@ function applyRunnerRouteFlagOverrides(route, flags) {
8225
9487
  return merged;
8226
9488
  }
8227
9489
 
8228
- function resolveRunnerRoutes(flags, mode) {
8229
- const inlineRoute = normalizeRunnerRoute({
8230
- name: flags["route-name"],
8231
- enabled: true,
8232
- project_id: flags["project-id"],
8233
- provider: flags.provider,
9490
+ function resolveRunnerRoutes(flags, mode) {
9491
+ const inlineRoute = normalizeRunnerRoute({
9492
+ name: flags["route-name"],
9493
+ enabled: true,
9494
+ ...(hasRunnerFlag(flags, "route-kind") ? { route_kind: flags["route-kind"] } : {}),
9495
+ project_id: flags["project-id"],
9496
+ provider: flags.provider,
8234
9497
  role: flags.role,
8235
9498
  role_profile: flags["role-profile"],
8236
9499
  bot_name: flags["bot-name"],
@@ -8340,13 +9603,29 @@ function resolveRunnerRoutes(flags, mode) {
8340
9603
  );
8341
9604
  }
8342
9605
 
8343
- async function listProjectChatDestinations(params) {
8344
- return listProjectChatDestinationsImpl(params, buildRunnerDataDeps());
8345
- }
8346
-
8347
- async function listProjectContextItems(params) {
8348
- return listProjectContextItemsImpl(params, buildRunnerDataDeps());
8349
- }
9606
+ async function listProjectChatDestinations(params) {
9607
+ return listProjectChatDestinationsImpl(params, buildRunnerDataDeps());
9608
+ }
9609
+
9610
+ async function getProjectRunnerSettings(params) {
9611
+ return getProjectRunnerSettingsImpl(params, buildRunnerDataDeps());
9612
+ }
9613
+
9614
+ async function listProjectPrivateChats(params) {
9615
+ return listProjectPrivateChatsImpl(params, buildRunnerDataDeps());
9616
+ }
9617
+
9618
+ async function createProjectPrivateChat(params) {
9619
+ return createProjectPrivateChatImpl(params, buildRunnerDataDeps());
9620
+ }
9621
+
9622
+ async function updateProjectPrivateChat(params) {
9623
+ return updateProjectPrivateChatImpl(params, buildRunnerDataDeps());
9624
+ }
9625
+
9626
+ async function listProjectContextItems(params) {
9627
+ return listProjectContextItemsImpl(params, buildRunnerDataDeps());
9628
+ }
8350
9629
 
8351
9630
  async function listProjectRunnerRequests(params) {
8352
9631
  return listProjectRunnerRequestsImpl(params, buildRunnerDataDeps());
@@ -8425,7 +9704,7 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8425
9704
  occurredAt: normalized?.occurredAt,
8426
9705
  messageThreadID: normalized?.messageThreadID,
8427
9706
  replyToMessageID: normalized?.replyToMessageID,
8428
- body: normalized?.text,
9707
+ body: normalized?.body || normalized?.text,
8429
9708
  }),
8430
9709
  sourceOrigin: String(normalized?.archiveSourceOrigin || normalized?.sourceOrigin || "").trim().toLowerCase(),
8431
9710
  sourceRouteKey: String(normalized?.archiveSourceRouteKey || normalized?.sourceRouteKey || "").trim(),
@@ -8434,7 +9713,11 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8434
9713
  replyToSender: String(normalized?.replyToFromName || "").trim(),
8435
9714
  replyToUsername: String(normalized?.replyToFromUsername || "").trim(),
8436
9715
  replyToSenderIsBot: normalized?.replyToFromIsBot === true,
8437
- body: String(normalized?.text || "").trim(),
9716
+ body: String(normalized?.body || normalized?.text || "").trim(),
9717
+ caption: String(normalized?.caption || "").trim(),
9718
+ attachments: normalizeTelegramMessageAttachments(normalized?.attachments),
9719
+ derivedText: String(normalized?.derivedText || normalized?.derived_text || "").trim(),
9720
+ derivedTextSource: String(normalized?.derivedTextSource || normalized?.derived_text_source || "").trim().toLowerCase(),
8438
9721
  };
8439
9722
  return payload;
8440
9723
  }
@@ -8578,6 +9861,12 @@ function parseArchivedChatComment(rawBody) {
8578
9861
  replyToSenderIsBot: payloadHas("replyToSenderIsBot")
8579
9862
  ? boolFromRaw(structuredPayload.replyToSenderIsBot, false)
8580
9863
  : boolFromRaw(metadata.reply_to_sender_is_bot, false),
9864
+ caption: String(safeObject(structuredPayload).caption || metadata.caption || "").trim(),
9865
+ attachments: normalizeTelegramMessageAttachments(
9866
+ safeObject(structuredPayload).attachments,
9867
+ ),
9868
+ derivedText: String(safeObject(structuredPayload).derivedText || metadata.derived_text || "").trim(),
9869
+ derivedTextSource: String(safeObject(structuredPayload).derivedTextSource || metadata.derived_text_source || "").trim().toLowerCase(),
8581
9870
  botID: firstNonEmptyString([safeObject(structuredPayload).botID, metadata.bot_id]),
8582
9871
  botName: firstNonEmptyString([safeObject(structuredPayload).botName, metadata.bot_name]),
8583
9872
  botUsername: normalizeTelegramMentionUsername(
@@ -8824,9 +10113,107 @@ function toISOStringFromUnix(rawValue) {
8824
10113
  return new Date(numeric * 1000).toISOString();
8825
10114
  }
8826
10115
 
8827
- function collectTelegramUpdateText(message) {
8828
- return firstNonEmptyString([message?.text, message?.caption]);
8829
- }
10116
+ function collectTelegramUpdateText(message) {
10117
+ return firstNonEmptyString([message?.text, message?.caption]);
10118
+ }
10119
+
10120
+ function normalizeTelegramAttachmentKind(rawValue) {
10121
+ const value = String(rawValue || "").trim().toLowerCase();
10122
+ if (!value) return "";
10123
+ if (["image", "picture"].includes(value)) return "photo";
10124
+ if (["voice_note", "voice-message", "voice_message"].includes(value)) return "voice";
10125
+ if (["file", "attachment"].includes(value)) return "document";
10126
+ return value;
10127
+ }
10128
+
10129
+ function normalizeTelegramAttachmentMetadata(kind, rawAttachment, extra = {}) {
10130
+ const attachment = safeObject(rawAttachment);
10131
+ const normalizedKind = normalizeTelegramAttachmentKind(kind);
10132
+ const fileID = String(attachment.file_id || "").trim();
10133
+ if (!normalizedKind || !fileID) {
10134
+ return null;
10135
+ }
10136
+ const normalized = {
10137
+ kind: normalizedKind,
10138
+ file_id: fileID,
10139
+ };
10140
+ const fileUniqueID = String(attachment.file_unique_id || "").trim();
10141
+ if (fileUniqueID) {
10142
+ normalized.file_unique_id = fileUniqueID;
10143
+ }
10144
+ const mimeType = String(attachment.mime_type || "").trim().toLowerCase();
10145
+ if (mimeType) {
10146
+ normalized.mime_type = mimeType;
10147
+ }
10148
+ const caption = String(extra.caption || "").trim();
10149
+ if (caption) {
10150
+ normalized.caption = caption;
10151
+ }
10152
+ const mediaGroupID = String(extra.media_group_id || attachment.media_group_id || "").trim();
10153
+ if (mediaGroupID) {
10154
+ normalized.media_group_id = mediaGroupID;
10155
+ }
10156
+ const fileName = String(attachment.file_name || "").trim();
10157
+ if (fileName) {
10158
+ normalized.file_name = fileName;
10159
+ }
10160
+ for (const [sourceKey, targetKey] of [
10161
+ ["duration", "duration"],
10162
+ ["file_size", "file_size"],
10163
+ ["width", "width"],
10164
+ ["height", "height"],
10165
+ ]) {
10166
+ const parsed = intFromRawAllowZero(attachment[sourceKey], 0);
10167
+ if (parsed > 0) {
10168
+ normalized[targetKey] = parsed;
10169
+ }
10170
+ }
10171
+ return normalized;
10172
+ }
10173
+
10174
+ function collectTelegramUpdateAttachments(message) {
10175
+ const safeMessage = safeObject(message);
10176
+ const normalizedAttachments = [];
10177
+ const caption = String(safeMessage.caption || "").trim();
10178
+ const mediaGroupID = String(safeMessage.media_group_id || "").trim();
10179
+ const photoCandidates = ensureArray(safeMessage.photo)
10180
+ .map((item) => safeObject(item))
10181
+ .filter((item) => String(item.file_id || "").trim());
10182
+ if (photoCandidates.length > 0) {
10183
+ const selectedPhoto = photoCandidates
10184
+ .slice()
10185
+ .sort((left, right) => {
10186
+ const leftScore = intFromRawAllowZero(left.file_size, 0)
10187
+ || (intFromRawAllowZero(left.width, 0) * intFromRawAllowZero(left.height, 0));
10188
+ const rightScore = intFromRawAllowZero(right.file_size, 0)
10189
+ || (intFromRawAllowZero(right.width, 0) * intFromRawAllowZero(right.height, 0));
10190
+ return leftScore - rightScore;
10191
+ })
10192
+ .pop();
10193
+ const normalizedPhoto = normalizeTelegramAttachmentMetadata("photo", selectedPhoto, {
10194
+ caption,
10195
+ media_group_id: mediaGroupID,
10196
+ });
10197
+ if (normalizedPhoto) {
10198
+ normalizedAttachments.push(normalizedPhoto);
10199
+ }
10200
+ }
10201
+ for (const [kind, rawValue] of [
10202
+ ["voice", safeMessage.voice],
10203
+ ["audio", safeMessage.audio],
10204
+ ["document", safeMessage.document],
10205
+ ["video", safeMessage.video],
10206
+ ]) {
10207
+ const normalized = normalizeTelegramAttachmentMetadata(kind, rawValue, {
10208
+ caption,
10209
+ media_group_id: mediaGroupID,
10210
+ });
10211
+ if (normalized) {
10212
+ normalizedAttachments.push(normalized);
10213
+ }
10214
+ }
10215
+ return normalizeTelegramMessageAttachments(normalizedAttachments);
10216
+ }
8830
10217
 
8831
10218
  function normalizeTelegramMentionUsername(rawValue) {
8832
10219
  return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
@@ -8861,23 +10248,29 @@ function extractTelegramMentionUsernames(text, entities) {
8861
10248
  return Array.from(set);
8862
10249
  }
8863
10250
 
8864
- function normalizeLocalTelegramUpdate(rawUpdate) {
10251
+ function normalizeLocalTelegramUpdate(rawUpdate) {
8865
10252
  const update = safeObject(rawUpdate);
8866
10253
  const message = safeObject(update.message || update.edited_message);
8867
10254
  if (!message || Object.keys(message).length === 0) {
8868
10255
  return null;
8869
10256
  }
8870
- const chat = safeObject(message.chat);
8871
- const from = safeObject(message.from);
8872
- const replyTo = safeObject(message.reply_to_message);
8873
- const replyToFrom = safeObject(replyTo.from);
8874
- const text = collectTelegramUpdateText(message);
8875
- if (!text) {
8876
- return null;
8877
- }
8878
- const mentionUsernames = extractTelegramMentionUsernames(
8879
- text,
8880
- message.entities || message.caption_entities,
10257
+ const chat = safeObject(message.chat);
10258
+ const from = safeObject(message.from);
10259
+ const replyTo = safeObject(message.reply_to_message);
10260
+ const replyToFrom = safeObject(replyTo.from);
10261
+ const caption = String(message.caption || "").trim();
10262
+ const attachments = collectTelegramUpdateAttachments(message);
10263
+ const text = collectTelegramUpdateText(message);
10264
+ const normalizedBody = firstNonEmptyString([
10265
+ text,
10266
+ buildTelegramMediaPlaceholder(attachments),
10267
+ ]);
10268
+ if (!normalizedBody) {
10269
+ return null;
10270
+ }
10271
+ const mentionUsernames = extractTelegramMentionUsernames(
10272
+ text,
10273
+ message.entities || message.caption_entities,
8881
10274
  );
8882
10275
  return {
8883
10276
  eventName: update.edited_message ? "telegram.message.updated" : "telegram.message.created",
@@ -8887,12 +10280,17 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8887
10280
  chatType: String(chat.type || "").trim(),
8888
10281
  chatTitle: firstNonEmptyString([chat.title, chat.username, joinTextParts([chat.first_name, chat.last_name]), chat.id]),
8889
10282
  fromID: String(from.id || "").trim(),
8890
- fromName: firstNonEmptyString([joinTextParts([from.first_name, from.last_name]), from.username, from.id]),
8891
- fromUsername: String(from.username || "").trim(),
8892
- fromIsBot: Boolean(from.is_bot),
8893
- mentionUsernames,
8894
- text,
8895
- occurredAt: toISOStringFromUnix(message.edit_date || message.date),
10283
+ fromName: firstNonEmptyString([joinTextParts([from.first_name, from.last_name]), from.username, from.id]),
10284
+ fromUsername: String(from.username || "").trim(),
10285
+ fromIsBot: Boolean(from.is_bot),
10286
+ mentionUsernames,
10287
+ text: normalizedBody,
10288
+ body: normalizedBody,
10289
+ caption,
10290
+ attachments,
10291
+ derivedText: "",
10292
+ derivedTextSource: "",
10293
+ occurredAt: toISOStringFromUnix(message.edit_date || message.date),
8896
10294
  messageThreadID: String(message.message_thread_id || "").trim(),
8897
10295
  replyToMessageID: intFromRawAllowZero(replyTo.message_id, 0),
8898
10296
  replyToFromName: firstNonEmptyString([joinTextParts([replyToFrom.first_name, replyToFrom.last_name]), replyToFrom.username, replyToFrom.id]),
@@ -8938,6 +10336,7 @@ function formatTelegramInboundArchiveComment(normalized) {
8938
10336
  const archiveSourceRouteKey = String(normalized.archiveSourceRouteKey || normalized.sourceRouteKey || "").trim();
8939
10337
  const archiveSourceBotUsername = String(normalized.archiveSourceBotUsername || normalized.sourceBotUsername || "").trim();
8940
10338
  const archivePayload = buildTelegramArchiveStructuredPayload(normalized);
10339
+ const normalizedAttachments = normalizeTelegramMessageAttachments(normalized.attachments);
8941
10340
  const headerLines = [
8942
10341
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
8943
10342
  `chat_id: ${normalized.chatID || "<missing>"}`,
@@ -8960,10 +10359,19 @@ function formatTelegramInboundArchiveComment(normalized) {
8960
10359
  if (normalized.fromUsername) {
8961
10360
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
8962
10361
  }
8963
- if (normalized.mentionUsernames.length > 0) {
8964
- headerLines.push(`mention_usernames: ${normalized.mentionUsernames.map((item) => `@${item}`).join(", ")}`);
8965
- }
8966
- if (normalized.replyToMessageID > 0) {
10362
+ if (normalized.mentionUsernames.length > 0) {
10363
+ headerLines.push(`mention_usernames: ${normalized.mentionUsernames.map((item) => `@${item}`).join(", ")}`);
10364
+ }
10365
+ if (normalizedAttachments.length > 0) {
10366
+ headerLines.push(`attachment_kinds: ${normalizedAttachments.map((item) => item.kind).join(", ")}`);
10367
+ }
10368
+ if (normalized.caption) {
10369
+ headerLines.push(`caption: ${String(normalized.caption).trim()}`);
10370
+ }
10371
+ if (normalized.derivedTextSource) {
10372
+ headerLines.push(`derived_text_source: ${String(normalized.derivedTextSource).trim().toLowerCase()}`);
10373
+ }
10374
+ if (normalized.replyToMessageID > 0) {
8967
10375
  headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
8968
10376
  headerLines.push(`reply_to_sender_is_bot: ${normalized.replyToFromIsBot ? "true" : "false"}`);
8969
10377
  if (normalized.replyToFromName) {
@@ -8972,12 +10380,12 @@ function formatTelegramInboundArchiveComment(normalized) {
8972
10380
  if (normalized.replyToFromUsername) {
8973
10381
  headerLines.push(`reply_to_telegram_username: @${normalized.replyToFromUsername.replace(/^@+/, "")}`);
8974
10382
  }
8975
- }
10383
+ }
8976
10384
  if (intFromRawAllowZero(normalized.messageThreadID, 0) > 0) {
8977
10385
  headerLines.push(`message_thread_id: ${intFromRawAllowZero(normalized.messageThreadID, 0)}`);
8978
10386
  }
8979
10387
  headerLines.push(`archive_payload_json: ${JSON.stringify(archivePayload)}`);
8980
- return `${headerLines.join("\n")}\n\n${String(normalized.text || "").trim()}`;
10388
+ return `${headerLines.join("\n")}\n\n${String(normalized.body || normalized.text || "").trim()}`;
8981
10389
  }
8982
10390
 
8983
10391
  async function postJSONWithAuthHeaders(urlText, timeoutSeconds, token, payload, extraHeaders = {}) {
@@ -9936,25 +11344,33 @@ function buildRunnerShowActiveExecutionPayload(activeExecutionStateRaw) {
9936
11344
  };
9937
11345
  }
9938
11346
 
9939
- function buildRunnerShowResolvedContext({
9940
- normalizedRoute,
9941
- diagnostics,
9942
- envConfig,
9943
- resolvedServerBotName,
11347
+ function buildRunnerShowResolvedContext({
11348
+ normalizedRoute,
11349
+ diagnostics,
11350
+ envConfig,
11351
+ resolvedServerBotName,
9944
11352
  botNameSource,
9945
11353
  }) {
9946
- return {
9947
- resolved_server_identity: {
9948
- server_bot_name: resolvedServerBotName,
9949
- server_bot_name_source: botNameSource,
9950
- server_bot_id: normalizedRoute.botID || "-",
9951
- telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
9952
- },
9953
- resolved_destination: {
9954
- destination_label: String(normalizedRoute.destinationLabel || "").trim() || "-",
9955
- destination_id: String(normalizedRoute.destinationID || "").trim() || "-",
9956
- destination_source: "route_config",
9957
- },
11354
+ return {
11355
+ resolved_server_identity: {
11356
+ server_bot_name: resolvedServerBotName,
11357
+ server_bot_name_source: botNameSource,
11358
+ server_bot_id: normalizedRoute.botID || "-",
11359
+ telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
11360
+ },
11361
+ route_target: {
11362
+ route_kind: normalizedRoute.routeKind || "room",
11363
+ target_label: describeRunnerRouteTargetLabel(normalizedRoute),
11364
+ },
11365
+ resolved_destination: {
11366
+ destination_label: normalizedRoute.routeKind === "dm"
11367
+ ? "Direct Messages"
11368
+ : (String(normalizedRoute.destinationLabel || "").trim() || "-"),
11369
+ destination_id: normalizedRoute.routeKind === "dm"
11370
+ ? "-"
11371
+ : (String(normalizedRoute.destinationID || "").trim() || "-"),
11372
+ destination_source: normalizedRoute.routeKind === "dm" ? "route_kind" : "route_config",
11373
+ },
9958
11374
  workspace_mapping: {
9959
11375
  workspace_dir: diagnostics.workspaceDir || "-",
9960
11376
  workspace_source: diagnostics.workspaceSource || "-",
@@ -10198,8 +11614,8 @@ async function resolveInformationalQueryReply({
10198
11614
  return lookupOnlyResponse || null;
10199
11615
  }
10200
11616
 
10201
- function buildRunnerExecutionDeps() {
10202
- return {
11617
+ function buildRunnerExecutionDeps() {
11618
+ return {
10203
11619
  adjudicateRunnerStartupLoopWithAI,
10204
11620
  analyzeHumanConversationIntentWithAI,
10205
11621
  auditRoleExecutionPlanWithAI,
@@ -10228,6 +11644,8 @@ function buildRunnerExecutionDeps() {
10228
11644
  createProjectContextItem,
10229
11645
  replaceProjectCtxpackFiles,
10230
11646
  resolveInformationalQueryReply,
11647
+ transcribeRunnerTelegramAudioAttachments,
11648
+ analyzeRunnerTelegramImageAttachments,
10231
11649
  prepareRunnerSelectedRecordRecoveryContext: (input) => prepareRunnerSelectedRecordRecoveryContextImpl({
10232
11650
  ...safeObject(input),
10233
11651
  deps: buildRunnerRecoveryDeps(),
@@ -10235,15 +11653,19 @@ function buildRunnerExecutionDeps() {
10235
11653
  };
10236
11654
  }
10237
11655
 
10238
- function buildRunnerRuntimeDeps() {
10239
- return {
10240
- loadProviderEnvConfig,
10241
- loadBotRunnerState,
10242
- saveBotRunnerState,
10243
- getTelegramBotMe,
10244
- getTelegramWebhookInfo,
10245
- deleteTelegramWebhook,
10246
- saveRunnerRouteState,
11656
+ function buildRunnerRuntimeDeps() {
11657
+ return {
11658
+ loadProviderEnvConfig,
11659
+ loadBotRunnerState,
11660
+ saveBotRunnerState,
11661
+ getProjectRunnerSettings,
11662
+ listProjectPrivateChats,
11663
+ createProjectPrivateChat,
11664
+ updateProjectPrivateChat,
11665
+ getTelegramBotMe,
11666
+ getTelegramWebhookInfo,
11667
+ deleteTelegramWebhook,
11668
+ saveRunnerRouteState,
10247
11669
  getTelegramUpdates,
10248
11670
  normalizeLocalTelegramUpdate,
10249
11671
  listThreadComments,
@@ -10586,23 +12008,34 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10586
12008
  runnerConfig,
10587
12009
  buildRunnerExecutionDeps(),
10588
12010
  );
10589
- const destinations = await listProjectChatDestinations({
10590
- siteBaseURL: runtime.baseURL,
10591
- projectID: normalizedRoute.projectID,
10592
- token: runtime.token,
10593
- timeoutSeconds: runtime.timeoutSeconds,
10594
- });
10595
- const destination = selectProjectChatDestination(
10596
- destinations,
10597
- {
10598
- destinationID: normalizedRoute.destinationID,
10599
- destinationLabel: normalizedRoute.destinationLabel,
10600
- },
10601
- normalizedRoute.provider,
10602
- );
10603
- const archiveThread = await discoverArchiveThreadForDestination({
10604
- siteBaseURL: runtime.baseURL,
10605
- projectID: normalizedRoute.projectID,
12011
+ const destination = normalizedRoute.routeKind === "dm"
12012
+ ? {
12013
+ id: "",
12014
+ label: `Direct Messages - ${String(bot?.name || bot?.username || normalizedRoute.botName || normalizedRoute.name || "Bot").trim()}`,
12015
+ chatID: `dm:${normalizeTelegramBotUsername(bot?.username || bot?.name || normalizedRoute.botName) || normalizedRoute.botID || normalizedRoute.name}`,
12016
+ provider: normalizedRoute.provider,
12017
+ routeKind: "dm",
12018
+ isDirectMessage: true,
12019
+ }
12020
+ : await (async () => {
12021
+ const destinations = await listProjectChatDestinations({
12022
+ siteBaseURL: runtime.baseURL,
12023
+ projectID: normalizedRoute.projectID,
12024
+ token: runtime.token,
12025
+ timeoutSeconds: runtime.timeoutSeconds,
12026
+ });
12027
+ return selectProjectChatDestination(
12028
+ destinations,
12029
+ {
12030
+ destinationID: normalizedRoute.destinationID,
12031
+ destinationLabel: normalizedRoute.destinationLabel,
12032
+ },
12033
+ normalizedRoute.provider,
12034
+ );
12035
+ })();
12036
+ const archiveThread = await discoverArchiveThreadForDestination({
12037
+ siteBaseURL: runtime.baseURL,
12038
+ projectID: normalizedRoute.projectID,
10606
12039
  provider: normalizedRoute.provider,
10607
12040
  destination,
10608
12041
  token: runtime.token,
@@ -10699,9 +12132,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10699
12132
  });
10700
12133
  const followupRequestsForPending = Object.values(requests).filter((entryRaw) => {
10701
12134
  const entry = safeObject(entryRaw);
10702
- const nextExpectedResponders = ensureArray(entry.next_expected_responders)
10703
- .map((value) => normalizeTelegramMentionUsername(value))
10704
- .filter(Boolean);
12135
+ const nextExpectedResponders = runnerRequestPreferredNextExpectedResponders(entry);
10705
12136
  if (!nextExpectedResponders.includes(currentBotSelector)) {
10706
12137
  return false;
10707
12138
  }
@@ -11291,8 +12722,8 @@ function buildRunnerRouteListRows() {
11291
12722
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
11292
12723
  const telegramState = readTelegramEnvState();
11293
12724
  const telegramEntries = ensureArray(telegramState.entries);
11294
- return ensureArray(config.routes).map((rawRoute, index) => {
11295
- const route = normalizeRunnerRoute(rawRoute);
12725
+ return ensureArray(config.routes).map((rawRoute, index) => {
12726
+ const route = normalizeRunnerRoute(rawRoute);
11296
12727
  const matchedTelegramEntry = route.provider === "telegram"
11297
12728
  ? telegramEntries.find((entry) => {
11298
12729
  const current = safeObject(entry);
@@ -11312,24 +12743,25 @@ function buildRunnerRouteListRows() {
11312
12743
  matchedTelegramEntry?.serverBotName,
11313
12744
  "-",
11314
12745
  ]);
11315
- return {
11316
- index: index + 1,
11317
- name: route.name || runnerRouteKey(route),
11318
- logicalSignature: runnerRouteLogicalSignature(route),
11319
- enabled: route.enabled !== false,
11320
- provider: route.provider || "-",
11321
- projectID: route.projectID || "-",
11322
- botName: resolvedBotName,
11323
- botNameSource,
11324
- botID: route.botID || "-",
11325
- role: route.role || "-",
11326
- roleProfile: route.roleProfile || "-",
11327
- destinationLabel: route.destinationLabel || "-",
11328
- pollIntervalMs: route.pollIntervalMs || 0,
11329
- configFilePath: config.filePath,
11330
- };
11331
- });
11332
- }
12746
+ return {
12747
+ index: index + 1,
12748
+ name: route.name || runnerRouteKey(route),
12749
+ logicalSignature: runnerRouteLogicalSignature(route),
12750
+ enabled: route.enabled !== false,
12751
+ provider: route.provider || "-",
12752
+ routeKind: route.routeKind || "room",
12753
+ projectID: route.projectID || "-",
12754
+ botName: resolvedBotName,
12755
+ botNameSource,
12756
+ botID: route.botID || "-",
12757
+ role: route.role || "-",
12758
+ roleProfile: route.roleProfile || "-",
12759
+ destinationLabel: describeRunnerRouteTargetLabel(route),
12760
+ pollIntervalMs: route.pollIntervalMs || 0,
12761
+ configFilePath: config.filePath,
12762
+ };
12763
+ });
12764
+ }
11333
12765
 
11334
12766
  function slugifyRunnerRouteSegment(rawValue) {
11335
12767
  return String(rawValue || "")
@@ -11339,12 +12771,13 @@ function slugifyRunnerRouteSegment(rawValue) {
11339
12771
  .replace(/^-+|-+$/g, "");
11340
12772
  }
11341
12773
 
11342
- function buildRunnerRouteNameSuggestion({ provider = "", role = "", botName = "" }, existingNames = []) {
11343
- const segments = [
11344
- slugifyRunnerRouteSegment(provider),
11345
- slugifyRunnerRouteSegment(role),
11346
- slugifyRunnerRouteSegment(botName),
11347
- ].filter(Boolean);
12774
+ function buildRunnerRouteNameSuggestion({ provider = "", role = "", botName = "", routeKind = "room" }, existingNames = []) {
12775
+ const segments = [
12776
+ slugifyRunnerRouteSegment(provider),
12777
+ slugifyRunnerRouteSegment(role),
12778
+ slugifyRunnerRouteSegment(botName),
12779
+ routeKind === "dm" ? "dm" : "",
12780
+ ].filter(Boolean);
11348
12781
  const base = segments.join("-") || "runner-route";
11349
12782
  const existing = new Set(ensureArray(existingNames).map((name) => String(name || "").trim().toLowerCase()).filter(Boolean));
11350
12783
  if (!existing.has(base)) {
@@ -11395,15 +12828,15 @@ function removeRunnerRouteFromConfig(config, routeName) {
11395
12828
  };
11396
12829
  }
11397
12830
 
11398
- function formatRunnerRouteChoiceLabel(route, telegramEntries = []) {
11399
- const normalizedRoute = normalizeRunnerRoute(route);
11400
- const resolvedName = resolveConfiguredRunnerRouteServerBotName(normalizedRoute, telegramEntries);
11401
- const routeName = normalizedRoute.name || runnerRouteKey(normalizedRoute);
11402
- const role = normalizedRoute.role || "-";
11403
- const botName = resolvedName || normalizedRoute.botName || normalizedRoute.botID || "-";
11404
- const destination = normalizedRoute.destinationLabel || normalizedRoute.destinationID || "-";
11405
- return `${routeName}${normalizedRoute.enabled ? "" : " [disabled]"} - ${normalizedRoute.provider || "-"} | ${role} | ${botName} | ${destination}`;
11406
- }
12831
+ function formatRunnerRouteChoiceLabel(route, telegramEntries = []) {
12832
+ const normalizedRoute = normalizeRunnerRoute(route);
12833
+ const resolvedName = resolveConfiguredRunnerRouteServerBotName(normalizedRoute, telegramEntries);
12834
+ const routeName = normalizedRoute.name || runnerRouteKey(normalizedRoute);
12835
+ const role = normalizedRoute.role || "-";
12836
+ const botName = resolvedName || normalizedRoute.botName || normalizedRoute.botID || "-";
12837
+ const target = describeRunnerRouteTargetLabel(normalizedRoute);
12838
+ return `${routeName}${normalizedRoute.enabled ? "" : " [disabled]"} - ${normalizedRoute.provider || "-"} | ${normalizedRoute.routeKind || "room"} | ${role} | ${botName} | ${target}`;
12839
+ }
11407
12840
 
11408
12841
  async function selectRunnerManagementRoute(ui, flags, { title = "Select runner route" } = {}) {
11409
12842
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
@@ -11547,15 +12980,16 @@ async function selectRunnerServerBotProfile(ui, { provider, role, flags, current
11547
12980
  token,
11548
12981
  timeoutSeconds,
11549
12982
  });
11550
- const candidates = ensureArray(bots)
11551
- .filter((bot) => bot.isActive && bot.provider === provider && bot.role === role);
12983
+ const candidates = ensureArray(bots)
12984
+ .filter((bot) => bot.isActive && bot.provider === provider && bot.role === role);
11552
12985
  if (!candidates.length) {
11553
12986
  throw new Error(`No active ${providerEnvConfig(provider).label} server bot exists for role "${role}".`);
11554
12987
  }
11555
- const resolveChoice = (bot) => ({
11556
- name: String(bot.name || "").trim(),
11557
- id: String(bot.id || "").trim(),
11558
- });
12988
+ const resolveChoice = (bot) => ({
12989
+ name: String(bot.name || "").trim(),
12990
+ id: String(bot.id || "").trim(),
12991
+ username: normalizeTelegramBotUsername(bot.username),
12992
+ });
11559
12993
  if (flaggedBotID) {
11560
12994
  const matched = candidates.find((bot) => bot.id === flaggedBotID);
11561
12995
  if (!matched) {
@@ -11597,18 +13031,28 @@ async function selectRunnerServerBotProfile(ui, { provider, role, flags, current
11597
13031
  return resolveChoice(matched);
11598
13032
  }
11599
13033
 
11600
- async function selectRunnerDestination(ui, { projectID, provider, flags, currentDestinationID = "", currentDestinationLabel = "" }) {
11601
- const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
11602
- const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
11603
- const destinations = await listProjectChatDestinations({
11604
- siteBaseURL,
11605
- projectID,
11606
- token,
11607
- timeoutSeconds,
11608
- });
11609
- const candidates = ensureArray(destinations)
11610
- .map((item) => normalizeChatDestination(item))
11611
- .filter((item) => item.isActive && item.provider === provider);
13034
+ async function selectRunnerDestination(ui, {
13035
+ projectID,
13036
+ provider,
13037
+ flags,
13038
+ currentDestinationID = "",
13039
+ currentDestinationLabel = "",
13040
+ preloadedDestinations = null,
13041
+ }) {
13042
+ let destinations = ensureArray(preloadedDestinations);
13043
+ if (!destinations.length) {
13044
+ const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
13045
+ const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
13046
+ destinations = await listProjectChatDestinations({
13047
+ siteBaseURL,
13048
+ projectID,
13049
+ token,
13050
+ timeoutSeconds,
13051
+ });
13052
+ }
13053
+ const candidates = ensureArray(destinations)
13054
+ .map((item) => normalizeChatDestination(item))
13055
+ .filter((item) => item.isActive && item.provider === provider);
11612
13056
  const flaggedID = String(flags["destination-id"] || "").trim();
11613
13057
  const flaggedLabel = String(flags["destination-label"] || "").trim();
11614
13058
  if (flaggedID) {
@@ -11652,15 +13096,98 @@ async function selectRunnerDestination(ui, { projectID, provider, flags, current
11652
13096
  if (!matched) {
11653
13097
  throw new Error("selected destination was not found");
11654
13098
  }
11655
- return { id: matched.id, label: matched.label || matched.id };
11656
- }
13099
+ return { id: matched.id, label: matched.label || matched.id };
13100
+ }
13101
+
13102
+ function isRunnerDirectMessageBotAllowed(projectSettingsRaw, botUsername) {
13103
+ const projectSettings = safeObject(projectSettingsRaw);
13104
+ const normalizedBotUsername = normalizeTelegramBotUsername(botUsername);
13105
+ if (!(projectSettings.directMessagesEnabled === true) || !normalizedBotUsername) {
13106
+ return false;
13107
+ }
13108
+ return ensureArray(projectSettings.directMessageBotUsernames)
13109
+ .map((value) => normalizeTelegramBotUsername(value))
13110
+ .filter(Boolean)
13111
+ .includes(normalizedBotUsername);
13112
+ }
13113
+
13114
+ async function resolveRunnerRouteTargetSelection(ui, {
13115
+ projectID,
13116
+ provider,
13117
+ flags,
13118
+ botSelection,
13119
+ }) {
13120
+ const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
13121
+ const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
13122
+ const [projectSettings, destinations] = await Promise.all([
13123
+ getProjectRunnerSettings({
13124
+ siteBaseURL,
13125
+ projectID,
13126
+ token,
13127
+ timeoutSeconds,
13128
+ }),
13129
+ listProjectChatDestinations({
13130
+ siteBaseURL,
13131
+ projectID,
13132
+ token,
13133
+ timeoutSeconds,
13134
+ }),
13135
+ ]);
13136
+ const activeDestinations = ensureArray(destinations)
13137
+ .map((item) => normalizeChatDestination(item))
13138
+ .filter((item) => item.isActive && item.provider === provider);
13139
+ const requestedRouteKind = hasRunnerFlag(flags, "route-kind")
13140
+ ? normalizeRunnerRouteKind(flags["route-kind"])
13141
+ : "";
13142
+ const dmAllowed = isRunnerDirectMessageBotAllowed(projectSettings, botSelection?.username);
13143
+ const routeKind = requestedRouteKind || (!activeDestinations.length && dmAllowed ? "dm" : "room");
13144
+ if (routeKind === "dm") {
13145
+ if (!dmAllowed) {
13146
+ const allowedBots = ensureArray(projectSettings.directMessageBotUsernames)
13147
+ .map((value) => `@${normalizeTelegramBotUsername(value)}`)
13148
+ .filter((value) => value !== "@")
13149
+ .join(", ");
13150
+ throw new Error(
13151
+ projectSettings.directMessagesEnabled === true
13152
+ ? `Bot ${String(botSelection?.username || botSelection?.name || botSelection?.id || "").trim() || "-"} is not enabled for project direct messages${allowedBots ? ` (allowed: ${allowedBots})` : ""}.`
13153
+ : `Project ${projectID} does not have 1:1 direct messages enabled.`,
13154
+ );
13155
+ }
13156
+ const envConfig = loadProviderEnvConfig(provider, {
13157
+ serverBotID: botSelection?.id,
13158
+ botName: botSelection?.name,
13159
+ botUsername: botSelection?.username,
13160
+ });
13161
+ if (!envConfig.ok) {
13162
+ throw new Error(`Direct message route requires a local ${providerEnvConfig(provider).label} bot binding for ${String(botSelection?.name || botSelection?.username || botSelection?.id || "").trim() || "-"}. ${String(envConfig.error || "").trim()}`);
13163
+ }
13164
+ return {
13165
+ routeKind,
13166
+ projectSettings,
13167
+ activeDestinations,
13168
+ destination: { id: "", label: "" },
13169
+ };
13170
+ }
13171
+ return {
13172
+ routeKind: "room",
13173
+ projectSettings,
13174
+ activeDestinations,
13175
+ destination: await selectRunnerDestination(ui, {
13176
+ projectID,
13177
+ provider,
13178
+ flags,
13179
+ preloadedDestinations: activeDestinations,
13180
+ }),
13181
+ };
13182
+ }
11657
13183
 
11658
- function buildRunnerRoutePayload({
11659
- currentRoute = null,
11660
- name = "",
11661
- enabled = true,
11662
- projectID = "",
11663
- provider = "",
13184
+ function buildRunnerRoutePayload({
13185
+ currentRoute = null,
13186
+ name = "",
13187
+ enabled = true,
13188
+ routeKind = "",
13189
+ projectID = "",
13190
+ provider = "",
11664
13191
  role = "",
11665
13192
  roleProfile = "",
11666
13193
  serverBotName = "",
@@ -11675,12 +13202,13 @@ function buildRunnerRoutePayload({
11675
13202
  const archivePolicy = currentRoute
11676
13203
  ? safeObject(currentRoute.archivePolicy)
11677
13204
  : defaultRunnerArchivePolicyForRole(roleProfile || role);
11678
- return normalizeRunnerRoute({
11679
- ...(currentRoute ? serializeRunnerRoute(currentRoute) : {}),
11680
- name,
11681
- enabled,
11682
- project_id: projectID,
11683
- provider,
13205
+ return normalizeRunnerRoute({
13206
+ ...(currentRoute ? serializeRunnerRoute(currentRoute) : {}),
13207
+ name,
13208
+ enabled,
13209
+ route_kind: normalizeRunnerRouteKind(routeKind || currentRoute?.routeKind || currentRoute?.route_kind),
13210
+ project_id: projectID,
13211
+ provider,
11684
13212
  role,
11685
13213
  role_profile: roleProfile,
11686
13214
  server_bot_name: serverBotName,
@@ -11693,45 +13221,48 @@ function buildRunnerRoutePayload({
11693
13221
  });
11694
13222
  }
11695
13223
 
11696
- async function runRunnerRouteAdd(flags) {
11697
- const ui = createPrompter();
11698
- try {
11699
- if (shouldRenderPromptChrome(flags)) {
11700
- ui.setFlow("RUNNER ROUTE ADD", "Create one executable runner route from server bot and destination");
11701
- }
11702
- const config = loadBotRunnerConfig({ persistIfNeeded: true });
11703
- const provider = await selectRunnerRouteProvider(ui, flags, "");
11704
- const projectID = await resolveRunnerRouteProjectID(ui, flags, config);
11705
- const role = await selectRunnerRole(ui, flags, "");
11706
- const botSelection = await selectRunnerServerBotProfile(ui, { provider, role, flags });
11707
- const destination = await selectRunnerDestination(ui, {
11708
- projectID,
11709
- provider,
11710
- flags,
11711
- });
11712
- const suggestedName = buildRunnerRouteNameSuggestion({
11713
- provider,
11714
- role,
11715
- botName: botSelection.name,
11716
- }, ensureArray(config.routes).map((route) => normalizeRunnerRoute(route).name));
11717
- const routeName = String(flags.name || "").trim() || suggestedName;
11718
- const pollIntervalMs = intFromRaw(flags["poll-interval-ms"], 5000) || 5000;
13224
+ async function runRunnerRouteAdd(flags) {
13225
+ const ui = createPrompter();
13226
+ try {
13227
+ if (shouldRenderPromptChrome(flags)) {
13228
+ ui.setFlow("RUNNER ROUTE ADD", "Create one executable runner route from server bot and target");
13229
+ }
13230
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
13231
+ const provider = await selectRunnerRouteProvider(ui, flags, "");
13232
+ const projectID = await resolveRunnerRouteProjectID(ui, flags, config);
13233
+ const role = await selectRunnerRole(ui, flags, "");
13234
+ const botSelection = await selectRunnerServerBotProfile(ui, { provider, role, flags });
13235
+ const targetSelection = await resolveRunnerRouteTargetSelection(ui, {
13236
+ projectID,
13237
+ provider,
13238
+ flags,
13239
+ botSelection,
13240
+ });
13241
+ const suggestedName = buildRunnerRouteNameSuggestion({
13242
+ provider,
13243
+ role,
13244
+ botName: botSelection.name,
13245
+ routeKind: targetSelection.routeKind,
13246
+ }, ensureArray(config.routes).map((route) => normalizeRunnerRoute(route).name));
13247
+ const routeName = String(flags.name || "").trim() || suggestedName;
13248
+ const pollIntervalMs = intFromRaw(flags["poll-interval-ms"], 5000) || 5000;
11719
13249
  const enabled = Object.prototype.hasOwnProperty.call(flags, "enabled")
11720
13250
  ? boolFromRaw(flags.enabled, true)
11721
13251
  : true;
11722
- const nextRoute = buildRunnerRoutePayload({
11723
- name: routeName,
11724
- enabled,
11725
- projectID,
11726
- provider,
11727
- role,
11728
- roleProfile: role,
11729
- serverBotName: botSelection.name,
11730
- serverBotID: botSelection.id,
11731
- destinationID: destination.id,
11732
- destinationLabel: destination.label,
11733
- pollIntervalMs,
11734
- });
13252
+ const nextRoute = buildRunnerRoutePayload({
13253
+ name: routeName,
13254
+ enabled,
13255
+ routeKind: targetSelection.routeKind,
13256
+ projectID,
13257
+ provider,
13258
+ role,
13259
+ roleProfile: role,
13260
+ serverBotName: botSelection.name,
13261
+ serverBotID: botSelection.id,
13262
+ destinationID: targetSelection.destination.id,
13263
+ destinationLabel: targetSelection.destination.label,
13264
+ pollIntervalMs,
13265
+ });
11735
13266
  const saved = upsertRunnerRouteConfig(config, nextRoute);
11736
13267
  const filePath = saveBotRunnerConfig(saved, config.filePath);
11737
13268
  if (!String(flags.name || "").trim()) {
@@ -11817,24 +13348,28 @@ async function runRunnerRouteEdit(flags) {
11817
13348
  })
11818
13349
  : { name: currentRoute.botName, id: currentRoute.botID };
11819
13350
 
11820
- const destinationAction = await promptChoice(
11821
- ui,
11822
- "Project chat destination",
11823
- [
11824
- { value: "keep", label: "Keep current destination", description: currentRoute.destinationLabel || currentRoute.destinationID || "-" },
11825
- { value: "change", label: "Change destination", description: "select another active project destination" },
11826
- ],
11827
- { defaultIndex: projectID !== currentRoute.projectID ? 1 : 0 },
11828
- );
11829
- const destination = destinationAction?.value === "change" || projectID !== currentRoute.projectID
11830
- ? await selectRunnerDestination(ui, {
11831
- projectID,
11832
- provider: currentRoute.provider,
11833
- flags: {},
11834
- currentDestinationID: currentRoute.destinationID,
11835
- currentDestinationLabel: currentRoute.destinationLabel,
11836
- })
11837
- : { id: currentRoute.destinationID, label: currentRoute.destinationLabel };
13351
+ const destination = currentRoute.routeKind === "dm"
13352
+ ? { id: "", label: "" }
13353
+ : await (async () => {
13354
+ const destinationAction = await promptChoice(
13355
+ ui,
13356
+ "Project chat destination",
13357
+ [
13358
+ { value: "keep", label: "Keep current destination", description: currentRoute.destinationLabel || currentRoute.destinationID || "-" },
13359
+ { value: "change", label: "Change destination", description: "select another active project destination" },
13360
+ ],
13361
+ { defaultIndex: projectID !== currentRoute.projectID ? 1 : 0 },
13362
+ );
13363
+ return destinationAction?.value === "change" || projectID !== currentRoute.projectID
13364
+ ? selectRunnerDestination(ui, {
13365
+ projectID,
13366
+ provider: currentRoute.provider,
13367
+ flags: {},
13368
+ currentDestinationID: currentRoute.destinationID,
13369
+ currentDestinationLabel: currentRoute.destinationLabel,
13370
+ })
13371
+ : { id: currentRoute.destinationID, label: currentRoute.destinationLabel };
13372
+ })();
11838
13373
 
11839
13374
  const pollAction = await promptChoice(
11840
13375
  ui,
@@ -11873,12 +13408,13 @@ async function runRunnerRouteEdit(flags) {
11873
13408
  process.stdout.write("Cancelled.\n");
11874
13409
  return;
11875
13410
  }
11876
- const nextRoute = buildRunnerRoutePayload({
11877
- currentRoute,
11878
- name: routeName,
11879
- enabled,
11880
- projectID,
11881
- provider: currentRoute.provider,
13411
+ const nextRoute = buildRunnerRoutePayload({
13412
+ currentRoute,
13413
+ name: routeName,
13414
+ enabled,
13415
+ routeKind: currentRoute.routeKind,
13416
+ projectID,
13417
+ provider: currentRoute.provider,
11882
13418
  role,
11883
13419
  roleProfile: role,
11884
13420
  serverBotName: botSelection.name,
@@ -11942,30 +13478,37 @@ function parseRunnerProjectUpRoles(flags) {
11942
13478
  return ordered;
11943
13479
  }
11944
13480
 
11945
- function runnerProjectUpScopeKey({ projectID, provider, destinationID = "", destinationLabel = "", botIdentity = "" }) {
11946
- return [
11947
- String(projectID || "").trim() || "-",
11948
- String(provider || "").trim() || "-",
11949
- String(destinationID || "").trim() || normalizeRunnerRouteIdentityText(destinationLabel) || "-",
11950
- normalizeRunnerRouteIdentityText(botIdentity) || "-",
11951
- ].join("::");
11952
- }
11953
-
11954
- function routeMatchesRunnerProjectUpScope(routeRaw, scopeRaw) {
11955
- const route = normalizeRunnerRoute(routeRaw);
11956
- const scope = safeObject(scopeRaw);
11957
- if (String(route.projectID || "").trim() !== String(scope.projectID || "").trim()) return false;
11958
- if (String(route.provider || "").trim() !== String(scope.provider || "").trim()) return false;
11959
- const routeDestinationID = String(route.destinationID || "").trim();
11960
- const scopeDestinationID = String(scope.destinationID || "").trim();
11961
- const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
11962
- const scopeDestinationLabel = normalizeRunnerRouteIdentityText(scope.destinationLabel);
11963
- const destinationMatches = scopeDestinationID
11964
- ? (routeDestinationID === scopeDestinationID || (!routeDestinationID && routeDestinationLabel === scopeDestinationLabel))
11965
- : routeDestinationLabel === scopeDestinationLabel;
11966
- if (!destinationMatches) return false;
11967
- const routeBotName = normalizeRunnerRouteIdentityText(route.botName);
11968
- const scopeBotIdentity = normalizeRunnerRouteIdentityText(scope.botIdentity);
13481
+ function runnerProjectUpScopeKey({ projectID, provider, routeKind = "room", destinationID = "", destinationLabel = "", botIdentity = "" }) {
13482
+ return [
13483
+ String(projectID || "").trim() || "-",
13484
+ String(provider || "").trim() || "-",
13485
+ normalizeRunnerRouteKind(routeKind),
13486
+ normalizeRunnerRouteKind(routeKind) === "dm"
13487
+ ? "direct-messages"
13488
+ : (String(destinationID || "").trim() || normalizeRunnerRouteIdentityText(destinationLabel) || "-"),
13489
+ normalizeRunnerRouteIdentityText(botIdentity) || "-",
13490
+ ].join("::");
13491
+ }
13492
+
13493
+ function routeMatchesRunnerProjectUpScope(routeRaw, scopeRaw) {
13494
+ const route = normalizeRunnerRoute(routeRaw);
13495
+ const scope = safeObject(scopeRaw);
13496
+ if (String(route.projectID || "").trim() !== String(scope.projectID || "").trim()) return false;
13497
+ if (String(route.provider || "").trim() !== String(scope.provider || "").trim()) return false;
13498
+ const scopeRouteKind = normalizeRunnerRouteKind(scope.routeKind, "room");
13499
+ if ((route.routeKind || "room") !== scopeRouteKind) return false;
13500
+ if (scopeRouteKind === "room") {
13501
+ const routeDestinationID = String(route.destinationID || "").trim();
13502
+ const scopeDestinationID = String(scope.destinationID || "").trim();
13503
+ const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
13504
+ const scopeDestinationLabel = normalizeRunnerRouteIdentityText(scope.destinationLabel);
13505
+ const destinationMatches = scopeDestinationID
13506
+ ? (routeDestinationID === scopeDestinationID || (!routeDestinationID && routeDestinationLabel === scopeDestinationLabel))
13507
+ : routeDestinationLabel === scopeDestinationLabel;
13508
+ if (!destinationMatches) return false;
13509
+ }
13510
+ const routeBotName = normalizeRunnerRouteIdentityText(route.botName);
13511
+ const scopeBotIdentity = normalizeRunnerRouteIdentityText(scope.botIdentity);
11969
13512
  const routeBotID = String(route.botID || "").trim();
11970
13513
  const scopeRoleIDs = new Set(
11971
13514
  Object.values(safeObject(scope.serverRoleIDs))
@@ -11978,17 +13521,24 @@ function routeMatchesRunnerProjectUpScope(routeRaw, scopeRaw) {
11978
13521
  );
11979
13522
  }
11980
13523
 
11981
- function resolveRunnerConversationPeers(routeRaw) {
11982
- const normalizedRoute = normalizeRunnerRoute(routeRaw);
11983
- const config = loadBotRunnerConfig({ persistIfNeeded: true });
13524
+ function resolveRunnerConversationPeers(routeRaw) {
13525
+ const normalizedRoute = normalizeRunnerRoute(routeRaw);
13526
+ if (normalizedRoute.routeKind === "dm") {
13527
+ return [{
13528
+ id: String(normalizedRoute.botID || "").trim(),
13529
+ name: String(normalizedRoute.botName || "").trim(),
13530
+ }].filter((item) => item.id || item.name);
13531
+ }
13532
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
11984
13533
  const seen = new Set();
11985
13534
  const peers = [];
11986
13535
  ensureArray(config.routes)
11987
13536
  .map((rawRoute) => normalizeRunnerRoute(rawRoute))
11988
- .filter((route) => route.enabled)
11989
- .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
11990
- .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
11991
- .filter((route) => {
13537
+ .filter((route) => route.enabled)
13538
+ .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
13539
+ .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
13540
+ .filter((route) => (route.routeKind || "room") === (normalizedRoute.routeKind || "room"))
13541
+ .filter((route) => {
11992
13542
  const routeDestinationID = String(route.destinationID || "").trim();
11993
13543
  const targetDestinationID = String(normalizedRoute.destinationID || "").trim();
11994
13544
  const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
@@ -12009,17 +13559,33 @@ function resolveRunnerConversationPeers(routeRaw) {
12009
13559
  return peers;
12010
13560
  }
12011
13561
 
12012
- function resolveRunnerConversationManagedBots(routeRaw, availableBots = []) {
12013
- const normalizedRoute = normalizeRunnerRoute(routeRaw);
12014
- const config = loadBotRunnerConfig({ persistIfNeeded: true });
13562
+ function resolveRunnerConversationManagedBots(routeRaw, availableBots = []) {
13563
+ const normalizedRoute = normalizeRunnerRoute(routeRaw);
13564
+ if (normalizedRoute.routeKind === "dm") {
13565
+ try {
13566
+ const bot = selectRunnerBot(availableBots, normalizedRoute);
13567
+ const username = String(bot?.username || bot?.name || normalizedRoute.botName || "").trim().replace(/^@+/, "").toLowerCase();
13568
+ return username ? [{
13569
+ username,
13570
+ display_name: String(bot?.name || bot?.username || normalizedRoute.botName || username).trim(),
13571
+ route_name: String(normalizedRoute.name || "").trim(),
13572
+ route: normalizedRoute,
13573
+ bot,
13574
+ }] : [];
13575
+ } catch {
13576
+ return [];
13577
+ }
13578
+ }
13579
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
12015
13580
  const seen = new Set();
12016
13581
  const managed = [];
12017
13582
  ensureArray(config.routes)
12018
13583
  .map((rawRoute) => normalizeRunnerRoute(rawRoute))
12019
- .filter((route) => route.enabled)
12020
- .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
12021
- .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
12022
- .filter((route) => {
13584
+ .filter((route) => route.enabled)
13585
+ .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
13586
+ .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
13587
+ .filter((route) => (route.routeKind || "room") === (normalizedRoute.routeKind || "room"))
13588
+ .filter((route) => {
12023
13589
  const routeDestinationID = String(route.destinationID || "").trim();
12024
13590
  const targetDestinationID = String(normalizedRoute.destinationID || "").trim();
12025
13591
  const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
@@ -12070,20 +13636,22 @@ function applyRunnerProjectUpRouteSuggestions(routeSuggestions) {
12070
13636
  desiredRoutes.forEach((suggestionRaw) => {
12071
13637
  const suggestion = safeObject(suggestionRaw);
12072
13638
  const routePayload = normalizeRunnerRoute(suggestion.routePayload);
12073
- const scopeKey = runnerProjectUpScopeKey({
12074
- projectID: routePayload.projectID,
12075
- provider: routePayload.provider,
12076
- destinationID: routePayload.destinationID,
12077
- destinationLabel: routePayload.destinationLabel,
12078
- botIdentity: suggestion.botIdentity || suggestion.serverBotName || routePayload.botName,
13639
+ const scopeKey = runnerProjectUpScopeKey({
13640
+ projectID: routePayload.projectID,
13641
+ provider: routePayload.provider,
13642
+ routeKind: routePayload.routeKind,
13643
+ destinationID: routePayload.destinationID,
13644
+ destinationLabel: routePayload.destinationLabel,
13645
+ botIdentity: suggestion.botIdentity || suggestion.serverBotName || routePayload.botName,
12079
13646
  });
12080
13647
  if (!scopeGroups.has(scopeKey)) {
12081
13648
  scopeGroups.set(scopeKey, {
12082
- projectID: routePayload.projectID,
12083
- provider: routePayload.provider,
12084
- destinationID: routePayload.destinationID,
12085
- destinationLabel: routePayload.destinationLabel,
12086
- botIdentity: suggestion.botIdentity || suggestion.serverBotName || routePayload.botName,
13649
+ projectID: routePayload.projectID,
13650
+ provider: routePayload.provider,
13651
+ routeKind: routePayload.routeKind,
13652
+ destinationID: routePayload.destinationID,
13653
+ destinationLabel: routePayload.destinationLabel,
13654
+ botIdentity: suggestion.botIdentity || suggestion.serverBotName || routePayload.botName,
12087
13655
  serverRoleIDs: { ...safeObject(suggestion.serverRoleIDs) },
12088
13656
  desiredSignatures: new Set(),
12089
13657
  });
@@ -12136,11 +13704,12 @@ function applyRunnerProjectUpRouteSuggestions(routeSuggestions) {
12136
13704
  };
12137
13705
  }
12138
13706
 
12139
- function resolveRunnerProjectUpRoutes({
12140
- projectID,
12141
- provider,
12142
- destinationID,
12143
- destinationLabel,
13707
+ function resolveRunnerProjectUpRoutes({
13708
+ projectID,
13709
+ provider,
13710
+ routeKind = "",
13711
+ destinationID,
13712
+ destinationLabel,
12144
13713
  botName = "",
12145
13714
  botID = "",
12146
13715
  roleFilter = [],
@@ -12148,20 +13717,22 @@ function resolveRunnerProjectUpRoutes({
12148
13717
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
12149
13718
  const telegramEntries = ensureArray(readTelegramEnvState().entries);
12150
13719
  const normalizedRoles = ensureArray(roleFilter).map((value) => normalizeBotRole(value)).filter(Boolean);
12151
- const selectionFlags = {
12152
- "project-id": projectID,
12153
- provider,
12154
- "destination-id": destinationID,
12155
- "destination-label": destinationLabel,
13720
+ const selectionFlags = {
13721
+ "project-id": projectID,
13722
+ provider,
13723
+ ...(routeKind ? { "route-kind": routeKind } : {}),
13724
+ "destination-id": destinationID,
13725
+ "destination-label": destinationLabel,
12156
13726
  ...(botName ? { "bot-name": botName } : {}),
12157
13727
  ...(botID ? { "bot-id": botID } : {}),
12158
13728
  };
12159
- const selectionRoute = normalizeRunnerRoute({
12160
- project_id: projectID,
12161
- provider,
12162
- destination_id: destinationID,
12163
- destination_label: destinationLabel,
12164
- server_bot_name: botName,
13729
+ const selectionRoute = normalizeRunnerRoute({
13730
+ project_id: projectID,
13731
+ provider,
13732
+ ...(routeKind ? { route_kind: routeKind } : {}),
13733
+ destination_id: destinationID,
13734
+ destination_label: destinationLabel,
13735
+ server_bot_name: botName,
12165
13736
  server_bot_id: botID,
12166
13737
  });
12167
13738
  const matched = ensureArray(config.routes)
@@ -12173,10 +13744,12 @@ function resolveRunnerProjectUpRoutes({
12173
13744
  return matched;
12174
13745
  }
12175
13746
  const seenBotDestinations = new Map();
12176
- return matched.filter((route) => {
12177
- const botIdentity = String(route.serverBotID || route.botID || route.botName || route.serverBotName || "").trim().toLowerCase();
12178
- const destIdentity = String(route.destinationID || route.destinationLabel || "").trim().toLowerCase();
12179
- const dedupeKey = `${botIdentity}::${destIdentity}`;
13747
+ return matched.filter((route) => {
13748
+ const botIdentity = String(route.serverBotID || route.botID || route.botName || route.serverBotName || "").trim().toLowerCase();
13749
+ const targetIdentity = route.routeKind === "dm"
13750
+ ? `${route.routeKind || "dm"}`
13751
+ : String(route.destinationID || route.destinationLabel || "").trim().toLowerCase();
13752
+ const dedupeKey = `${botIdentity}::${targetIdentity}`;
12180
13753
  if (!dedupeKey || dedupeKey === "::") return true;
12181
13754
  if (seenBotDestinations.has(dedupeKey)) return false;
12182
13755
  seenBotDestinations.set(dedupeKey, true);
@@ -12258,6 +13831,95 @@ async function runRunnerProjectUp(flags) {
12258
13831
  });
12259
13832
  }
12260
13833
 
13834
+ function resolveRunnerProjectUpDirectMessageBotUsername(botRaw, telegramEntries = []) {
13835
+ const bot = safeObject(botRaw);
13836
+ const directUsername = normalizeTelegramBotUsername(bot.username);
13837
+ if (directUsername) return directUsername;
13838
+ const matchedEntry = ensureArray(telegramEntries).find((entryRaw) => {
13839
+ const entry = safeObject(entryRaw);
13840
+ return (
13841
+ (bot.id && entry.serverBotID && entry.serverBotID === bot.id)
13842
+ || (bot.name && normalizeRunnerRouteIdentityText(entry.serverBotName) === normalizeRunnerRouteIdentityText(bot.name))
13843
+ );
13844
+ });
13845
+ return normalizeTelegramBotUsername(matchedEntry?.username);
13846
+ }
13847
+
13848
+ function buildRunnerProjectUpDirectMessageRouteSuggestions({
13849
+ projectID,
13850
+ provider,
13851
+ projectSettings,
13852
+ availableBots,
13853
+ telegramEntries = [],
13854
+ botNameFilter = "",
13855
+ botIDFilter = "",
13856
+ roleFilter = [],
13857
+ }) {
13858
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
13859
+ const allowedBotUsernames = new Set(
13860
+ ensureArray(safeObject(projectSettings).directMessageBotUsernames)
13861
+ .map((value) => normalizeTelegramBotUsername(value))
13862
+ .filter(Boolean),
13863
+ );
13864
+ const desiredRoles = ensureArray(roleFilter).length
13865
+ ? ensureArray(roleFilter).map((value) => normalizeBotRole(value)).filter(Boolean)
13866
+ : ["monitor"];
13867
+ const existingRoutes = ensureArray(config.routes).map((route) => normalizeRunnerRoute(route));
13868
+ return ensureArray(availableBots)
13869
+ .filter((bot) => bot && bot.isActive && bot.provider === provider)
13870
+ .map((bot) => ({
13871
+ ...bot,
13872
+ username: resolveRunnerProjectUpDirectMessageBotUsername(bot, telegramEntries),
13873
+ hasLocalBinding: Boolean(
13874
+ ensureArray(telegramEntries).find((entryRaw) => {
13875
+ const entry = safeObject(entryRaw);
13876
+ return (
13877
+ (bot.id && entry.serverBotID && entry.serverBotID === bot.id)
13878
+ || (bot.name && normalizeRunnerRouteIdentityText(entry.serverBotName) === normalizeRunnerRouteIdentityText(bot.name))
13879
+ || (resolveRunnerProjectUpDirectMessageBotUsername(bot, telegramEntries) && normalizeTelegramBotUsername(entry.username) === resolveRunnerProjectUpDirectMessageBotUsername(bot, telegramEntries))
13880
+ );
13881
+ }),
13882
+ ),
13883
+ }))
13884
+ .filter((bot) => bot.username && allowedBotUsernames.has(bot.username))
13885
+ .filter((bot) => bot.hasLocalBinding)
13886
+ .filter((bot) => !botIDFilter || String(bot.id || "").trim() === botIDFilter)
13887
+ .filter((bot) => !botNameFilter || matchesRunnerRouteText(bot.name, botNameFilter) || matchesRunnerRouteText(bot.username, botNameFilter))
13888
+ .filter((bot) => !desiredRoles.length || desiredRoles.includes(normalizeBotRole(bot.role)))
13889
+ .map((bot) => {
13890
+ const routePayload = buildRunnerRoutePayload({
13891
+ name: buildRunnerRouteNameSuggestion({
13892
+ provider,
13893
+ role: normalizeBotRole(bot.role) || "monitor",
13894
+ botName: bot.name,
13895
+ routeKind: "dm",
13896
+ }, existingRoutes.map((route) => route.name)),
13897
+ enabled: true,
13898
+ routeKind: "dm",
13899
+ projectID,
13900
+ provider,
13901
+ role: normalizeBotRole(bot.role) || "monitor",
13902
+ roleProfile: normalizeBotRole(bot.role) || "monitor",
13903
+ serverBotName: String(bot.name || "").trim(),
13904
+ serverBotID: String(bot.id || "").trim(),
13905
+ destinationID: "",
13906
+ destinationLabel: "",
13907
+ });
13908
+ const logicalSignature = runnerRouteLogicalSignature(routePayload);
13909
+ const existing = existingRoutes.find((route) => route.enabled !== false && runnerRouteLogicalSignature(route) === logicalSignature);
13910
+ return {
13911
+ status: existing ? "existing" : "create",
13912
+ routeName: String((existing || routePayload).name || "").trim(),
13913
+ logicalSignature,
13914
+ serverBotName: String(bot.name || "").trim(),
13915
+ role: normalizeBotRole(bot.role) || "monitor",
13916
+ botIdentity: bot.username,
13917
+ serverRoleIDs: bot.id ? { [normalizeBotRole(bot.role) || "monitor"]: String(bot.id || "").trim() } : {},
13918
+ routePayload,
13919
+ };
13920
+ });
13921
+ }
13922
+
12261
13923
  function resolveRunnerProjectTUIRuntimePolicy(flags = {}) {
12262
13924
  const explicitStartRequested = Object.prototype.hasOwnProperty.call(flags, "start");
12263
13925
  return {
@@ -12788,6 +14450,7 @@ function buildRunnerProjectUpNextSteps({
12788
14450
  shouldStartRunner,
12789
14451
  projectID,
12790
14452
  provider,
14453
+ routeKind = "room",
12791
14454
  destinationID,
12792
14455
  matchingRoutes,
12793
14456
  startFlags,
@@ -12795,14 +14458,18 @@ function buildRunnerProjectUpNextSteps({
12795
14458
  const nextSteps = [];
12796
14459
  const routeNames = ensureArray(matchingRoutes).map((route) => normalizeRunnerRoute(route).name).filter(Boolean);
12797
14460
  if (!applyRequested) {
12798
- nextSteps.push(`${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --destination-id ${destinationID} --apply true --start false`);
14461
+ nextSteps.push(routeKind === "dm"
14462
+ ? `${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --route-kind dm --apply true --start false`
14463
+ : `${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --destination-id ${destinationID} --apply true --start false`);
12799
14464
  }
12800
14465
  if (!shouldStartRunner) {
12801
14466
  if (routeNames.length > 0) {
12802
14467
  nextSteps.push(`${CLI_NAME} runner show --route-name ${routeNames[0]}`);
12803
14468
  nextSteps.push(buildRunnerStartDetachedCommand(startFlags));
12804
14469
  } else if (applyRequested) {
12805
- nextSteps.push(`${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --destination-id ${destinationID} --apply true --start false`);
14470
+ nextSteps.push(routeKind === "dm"
14471
+ ? `${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --route-kind dm --apply true --start false`
14472
+ : `${CLI_NAME} runner project up --project-id ${projectID} --provider ${provider} --destination-id ${destinationID} --apply true --start false`);
12806
14473
  }
12807
14474
  return nextSteps;
12808
14475
  }
@@ -12829,16 +14496,151 @@ async function buildRunnerProjectUpResult(flags = {}) {
12829
14496
  shouldStartRunner,
12830
14497
  foregroundStartRequested,
12831
14498
  } = resolveRunnerProjectUpExecutionPolicy(flags);
12832
- const roleFilter = parseRunnerProjectUpRoles(flags);
12833
- const botNameFilter = String(flags["bot-name"] || "").trim();
12834
- const botIDFilter = String(flags["bot-id"] || "").trim();
12835
- const auditPayload = await buildBotRoomAuditPayload(
12836
- ui,
12837
- {
12838
- ...flags,
12839
- provider,
12840
- apply: false,
12841
- json: false,
14499
+ const roleFilter = parseRunnerProjectUpRoles(flags);
14500
+ const botNameFilter = String(flags["bot-name"] || "").trim();
14501
+ const botIDFilter = String(flags["bot-id"] || "").trim();
14502
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
14503
+ const projectID = await resolveRunnerRouteProjectID(ui, flags, config);
14504
+ const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
14505
+ const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
14506
+ const [projectSettings, destinations] = await Promise.all([
14507
+ getProjectRunnerSettings({
14508
+ siteBaseURL,
14509
+ projectID,
14510
+ token,
14511
+ timeoutSeconds,
14512
+ }),
14513
+ listProjectChatDestinations({
14514
+ siteBaseURL,
14515
+ projectID,
14516
+ token,
14517
+ timeoutSeconds,
14518
+ }),
14519
+ ]);
14520
+ const activeDestinations = ensureArray(destinations)
14521
+ .map((item) => normalizeChatDestination(item))
14522
+ .filter((item) => item.isActive && item.provider === provider);
14523
+ const requestedRouteKind = hasRunnerFlag(flags, "route-kind")
14524
+ ? normalizeRunnerRouteKind(flags["route-kind"])
14525
+ : "";
14526
+ const useDirectMessageProjectUp = requestedRouteKind === "dm"
14527
+ || (!activeDestinations.length && safeObject(projectSettings).directMessagesEnabled === true);
14528
+ if (requestedRouteKind === "dm" && safeObject(projectSettings).directMessagesEnabled !== true) {
14529
+ throw new Error(`project ${projectID} does not have 1:1 direct messages enabled`);
14530
+ }
14531
+ if (useDirectMessageProjectUp) {
14532
+ const serverBots = await listUserBotsForRunner({
14533
+ siteBaseURL,
14534
+ token,
14535
+ timeoutSeconds,
14536
+ });
14537
+ const telegramEntries = ensureArray(readTelegramEnvState().entries);
14538
+ const filteredRouteSuggestions = buildRunnerProjectUpDirectMessageRouteSuggestions({
14539
+ projectID,
14540
+ provider,
14541
+ projectSettings,
14542
+ availableBots: serverBots,
14543
+ telegramEntries,
14544
+ botNameFilter,
14545
+ botIDFilter,
14546
+ roleFilter,
14547
+ });
14548
+ const applyResultRaw = applyRequested
14549
+ ? applyRunnerProjectUpRouteSuggestions(filteredRouteSuggestions)
14550
+ : null;
14551
+ const applyResult = safeObject(applyResultRaw);
14552
+ const matchingRoutes = resolveRunnerProjectUpRoutes({
14553
+ projectID,
14554
+ provider,
14555
+ routeKind: "dm",
14556
+ destinationID: "",
14557
+ destinationLabel: "",
14558
+ botName: botNameFilter,
14559
+ botID: botIDFilter,
14560
+ roleFilter,
14561
+ });
14562
+ const startFlags = {
14563
+ ...flags,
14564
+ provider,
14565
+ "project-id": projectID,
14566
+ "route-kind": "dm",
14567
+ ...(botNameFilter ? { "bot-name": botNameFilter } : {}),
14568
+ ...(botIDFilter ? { "bot-id": botIDFilter } : {}),
14569
+ };
14570
+ const applyFailureWarningOnly = canStartRunnerDespiteProjectUpApplyFailure({
14571
+ applyRequested,
14572
+ applyResult,
14573
+ matchingRoutes,
14574
+ });
14575
+ const summaryPayload = {
14576
+ ok: !applyRequested || applyResult.ok !== false || applyFailureWarningOnly,
14577
+ provider,
14578
+ route_kind: "dm",
14579
+ project_id: projectID,
14580
+ destination_label: "Direct Messages",
14581
+ destination_id: "-",
14582
+ room_probe_ok: true,
14583
+ room_visible_bot_admins: 0,
14584
+ my_server_bots: ensureArray(serverBots).filter((bot) => bot.provider === provider && bot.isActive).length,
14585
+ my_local_bots: telegramEntries.length,
14586
+ bot_name_filter: botNameFilter || "-",
14587
+ bot_id_filter: botIDFilter || "-",
14588
+ role_filter: roleFilter.join(", ") || "-",
14589
+ route_suggestions_create_total: filteredRouteSuggestions.filter((item) => safeObject(item).status === "create").length,
14590
+ route_suggestions_existing_total: filteredRouteSuggestions.filter((item) => safeObject(item).status === "existing").length,
14591
+ route_suggestions_blocked_total: filteredRouteSuggestions.filter((item) => safeObject(item).status === "blocked").length,
14592
+ route_apply_requested: applyRequested,
14593
+ route_apply_changed: Boolean(applyResult.changed),
14594
+ route_config_file: String(applyResult.filePath || config.filePath || "-").trim() || "-",
14595
+ enabled_routes_for_selection: matchingRoutes.map((route) => normalizeRunnerRoute(route).name).filter(Boolean),
14596
+ start_requested: startRequested,
14597
+ start_detached_requested: startDetachedRequested,
14598
+ next_steps: [],
14599
+ applied_routes: ensureArray(applyResult.appliedRoutes).map((item) => safeObject(item)),
14600
+ disabled_routes: ensureArray(applyResult.disabledRoutes).map((item) => safeObject(item)),
14601
+ };
14602
+ summaryPayload.next_steps.push(...buildRunnerProjectUpNextSteps({
14603
+ applyRequested,
14604
+ shouldStartRunner,
14605
+ projectID: summaryPayload.project_id,
14606
+ provider,
14607
+ routeKind: "dm",
14608
+ destinationID: "",
14609
+ matchingRoutes,
14610
+ startFlags,
14611
+ }));
14612
+ if (!filteredRouteSuggestions.length) {
14613
+ summaryPayload.warning = "No eligible 1:1 DM bot matched the current filters. Enable at least one project DM bot and ensure the local Telegram bot binding exists.";
14614
+ }
14615
+ if (applyRequested && applyResult.ok === false && applyResult.error) {
14616
+ if (applyFailureWarningOnly) {
14617
+ summaryPayload.warning = String(applyResult.error || "").trim();
14618
+ } else {
14619
+ summaryPayload.error = String(applyResult.error || "").trim();
14620
+ }
14621
+ }
14622
+ if (foregroundStartRequested && !summaryPayload.warning && !summaryPayload.error) {
14623
+ summaryPayload.warning = "foreground runner start was explicitly requested; the runner stops when this terminal session ends. Use runner start-detached for persistent polling.";
14624
+ }
14625
+ return {
14626
+ summaryPayload,
14627
+ applyRequested,
14628
+ applyResult,
14629
+ shouldStartRunner,
14630
+ startDetachedRequested,
14631
+ matchingRoutes,
14632
+ startFlags,
14633
+ applyFailureBlocksStart: applyRequested && applyResult.ok === false && !applyFailureWarningOnly,
14634
+ };
14635
+ }
14636
+ const auditPayload = await buildBotRoomAuditPayload(
14637
+ ui,
14638
+ {
14639
+ ...flags,
14640
+ "project-id": projectID,
14641
+ provider,
14642
+ apply: false,
14643
+ json: false,
12842
14644
  },
12843
14645
  buildBotCommandDeps(),
12844
14646
  );
@@ -12870,12 +14672,13 @@ async function buildRunnerProjectUpResult(flags = {}) {
12870
14672
  })
12871
14673
  : null;
12872
14674
  const applyResult = safeObject(applyResultRaw);
12873
- const matchingRoutes = resolveRunnerProjectUpRoutes({
12874
- projectID: String(auditPayload.projectID || "").trim(),
12875
- provider,
12876
- destinationID: String(destination.destinationID || "").trim(),
12877
- destinationLabel: String(destination.destinationLabel || "").trim(),
12878
- botName: botNameFilter,
14675
+ const matchingRoutes = resolveRunnerProjectUpRoutes({
14676
+ projectID: String(auditPayload.projectID || "").trim(),
14677
+ provider,
14678
+ routeKind: "room",
14679
+ destinationID: String(destination.destinationID || "").trim(),
14680
+ destinationLabel: String(destination.destinationLabel || "").trim(),
14681
+ botName: botNameFilter,
12879
14682
  botID: botIDFilter,
12880
14683
  roleFilter,
12881
14684
  });
@@ -12923,6 +14726,7 @@ async function buildRunnerProjectUpResult(flags = {}) {
12923
14726
  shouldStartRunner,
12924
14727
  projectID: summaryPayload.project_id,
12925
14728
  provider,
14729
+ routeKind: "room",
12926
14730
  destinationID: summaryPayload.destination_id,
12927
14731
  matchingRoutes,
12928
14732
  startFlags,
@@ -12958,9 +14762,9 @@ async function runRunnerList(flags) {
12958
14762
  process.stdout.write(`${JSON.stringify({ ok: true, routes: rows }, null, 2)}\n`);
12959
14763
  return;
12960
14764
  }
12961
- process.stdout.write("Runner routes\n");
12962
- process.stdout.write(" note: routes are the executable unit. Use --route-name in production. --bot-name and --bot-id are convenience selectors that resolve one enabled route only when the match is unique.\n");
12963
- process.stdout.write(" note: do not create two enabled routes for the same project/provider/role/server-bot/destination target.\n");
14765
+ process.stdout.write("Runner routes\n");
14766
+ process.stdout.write(" note: routes are the executable unit. Use --route-name in production. --bot-name and --bot-id are convenience selectors that resolve one enabled route only when the match is unique.\n");
14767
+ process.stdout.write(" note: do not create two enabled routes for the same project/provider/route-kind/server-bot/target.\n");
12964
14768
  if (!rows.length) {
12965
14769
  process.stdout.write(" none configured\n");
12966
14770
  return;
@@ -12971,9 +14775,10 @@ async function runRunnerList(flags) {
12971
14775
  [
12972
14776
  ` - ${row.name}${row.enabled ? "" : " [disabled]"}`,
12973
14777
  ` logical_signature: ${row.logicalSignature}`,
12974
- ` provider: ${row.provider}`,
12975
- ` project_id: ${row.projectID}`,
12976
- ` server_bot_name: ${row.botName}`,
14778
+ ` provider: ${row.provider}`,
14779
+ ` route_kind: ${row.routeKind}`,
14780
+ ` project_id: ${row.projectID}`,
14781
+ ` server_bot_name: ${row.botName}`,
12977
14782
  ` server_bot_name_source: ${row.botNameSource}`,
12978
14783
  ` server_bot_id: ${row.botID}`,
12979
14784
  ` role: ${row.role}`,
@@ -13197,6 +15002,25 @@ function runnerDetachedRouteSetSignature(routes) {
13197
15002
 
13198
15003
  function runnerSchedulingTargetKeyFromRouteKey(routeKey) {
13199
15004
  const parts = String(routeKey || "").trim().split("::").filter(Boolean);
15005
+ if (parts.length >= 7) {
15006
+ const projectID = String(parts[parts.length - 6] || "").trim();
15007
+ const provider = String(parts[parts.length - 5] || "").trim();
15008
+ const routeKind = normalizeRunnerRouteKind(parts[parts.length - 4], "room");
15009
+ if (routeKind === "dm") {
15010
+ return [
15011
+ projectID,
15012
+ provider,
15013
+ routeKind,
15014
+ String(parts[parts.length - 2] || "").trim(),
15015
+ ].join("::");
15016
+ }
15017
+ return [
15018
+ projectID,
15019
+ provider,
15020
+ routeKind,
15021
+ String(parts[parts.length - 1] || "").trim(),
15022
+ ].join("::");
15023
+ }
13200
15024
  if (parts.length < 6) {
13201
15025
  return "";
13202
15026
  }
@@ -13292,8 +15116,8 @@ function classifyDetachedRunnerLaunchReuse(registry, routes) {
13292
15116
  ),
13293
15117
  );
13294
15118
  const detail = supersets.length > 1
13295
- ? `multiple detached runners already cover the requested route set for the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")})`
13296
- : `another detached runner already owns the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")}). Stop it first or use runner project tui so one detached runner owns the full route set for that destination`;
15119
+ ? `multiple detached runners already cover the requested route set for the same project/provider/route target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")})`
15120
+ : `another detached runner already owns the same project/provider/route target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")}). Stop it first or use runner project tui so one detached runner owns the full route set for that target`;
13297
15121
  return {
13298
15122
  kind: "conflict",
13299
15123
  detail,
@@ -18927,17 +20751,24 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18927
20751
  buildToolAliasMaps,
18928
20752
  rewriteAliasedToolCallToCanonical,
18929
20753
  normalizeBotRunnerConfigContents,
18930
- defaultLocalBotBridgeCommand,
18931
- resolveRunnerExecutionPlan,
18932
- resolveRunnerExecutionPlanForRole,
18933
- normalizeRunnerRoute,
20754
+ defaultLocalBotBridgeCommand,
20755
+ cliName: CLI_NAME,
20756
+ resolveRunnerExecutionPlan,
20757
+ resolveRunnerExecutionPlanForRole,
20758
+ resolveRunnerLocalAIExecutionProfile,
20759
+ normalizeRunnerRoute,
18934
20760
  buildRunnerRouteNameSuggestion,
18935
- buildRunnerRoutePayload,
18936
- upsertRunnerRouteConfig,
18937
- removeRunnerRouteFromConfig,
18938
- acquireRunnerRouteLease,
18939
- buildRunnerExecutionDeps,
18940
- defaultBotRunnerRoleProfiles,
20761
+ buildRunnerRoutePayload,
20762
+ upsertRunnerRouteConfig,
20763
+ removeRunnerRouteFromConfig,
20764
+ acquireRunnerRouteLease,
20765
+ validateRunnerRoute,
20766
+ runnerRouteSchedulingGroupKey,
20767
+ buildRunnerProjectUpDirectMessageRouteSuggestions,
20768
+ resolveRunnerProjectUpRoutes,
20769
+ buildRunnerProjectUpNextSteps,
20770
+ buildRunnerExecutionDeps,
20771
+ defaultBotRunnerRoleProfiles,
18941
20772
  resolveRunnerRoutes,
18942
20773
  runnerRouteKey,
18943
20774
  runnerRouteLogicalSignature,
@@ -18993,6 +20824,9 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18993
20824
  formatTelegramInboundArchiveComment,
18994
20825
  findArchivedBotReplyRecord,
18995
20826
  parseArchivedChatComment,
20827
+ normalizeLocalTelegramUpdate,
20828
+ normalizeRunnerTelegramMessageEnvelope,
20829
+ normalizeRunnerRecentLocalInboundReceiptEntry,
18996
20830
  intFromRawAllowZero,
18997
20831
  validateWorkspaceArtifacts,
18998
20832
  });
@@ -20044,7 +21878,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
20044
21878
  "detached_runner_blocks_disjoint_same_target_parallel_launches",
20045
21879
  decision.kind === "conflict"
20046
21880
  && ensureArray(decision.overlapping_route_names).length === 0
20047
- && String(decision.detail || "").includes("same project/provider/destination target"),
21881
+ && String(decision.detail || "").includes("same project/provider/route target"),
20048
21882
  `kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
20049
21883
  );
20050
21884
  } catch (err) {