metheus-governance-mcp-cli 0.2.296 → 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 (31) hide show
  1. package/cli.mjs +2601 -736
  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 +180 -10
  14. package/lib/runner-orchestration-decision-bundle.mjs +217 -22
  15. package/lib/runner-orchestration-entrypoints.mjs +354 -52
  16. package/lib/runner-orchestration-intent-contracts.mjs +145 -20
  17. package/lib/runner-orchestration-selected-record-context.mjs +452 -21
  18. package/lib/runner-orchestration-selected-record-delivery-handoff.mjs +2 -0
  19. package/lib/runner-orchestration-selected-record-outcome.mjs +1 -1
  20. package/lib/runner-orchestration-selected-record-preparation.mjs +91 -12
  21. package/lib/runner-orchestration-selected-record-reply-outcome.mjs +169 -4
  22. package/lib/runner-orchestration-selected-record-terminal-outcome.mjs +17 -1
  23. package/lib/runner-orchestration-step-inputs.mjs +87 -3
  24. package/lib/runner-orchestration-step-requirements.mjs +274 -0
  25. package/lib/runner-orchestration-visibility.mjs +9 -3
  26. package/lib/runner-orchestration.mjs +399 -50
  27. package/lib/runner-recovery.mjs +30 -23
  28. package/lib/runner-runtime.mjs +299 -9
  29. package/lib/selftest-runner-scenarios.mjs +3982 -357
  30. package/lib/selftest-telegram-e2e.mjs +173 -24
  31. 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,21 +4457,42 @@ 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);
4473
+ const authoritativeExecutionContractType = String(
4474
+ decisionBundle.execution_contract_type || "",
4475
+ ).trim().toLowerCase();
3877
4476
  return String(
3878
- decisionBundle.execution_contract_type
4477
+ authoritativeExecutionContractType
3879
4478
  || entry.execution_contract_type
3880
4479
  || entry.root_execution_contract_type
3881
4480
  || "",
3882
4481
  ).trim().toLowerCase();
3883
4482
  }
3884
-
4483
+
3885
4484
  function runnerRequestPreferredExecutionContractActionable(entryRaw) {
3886
4485
  const entry = safeObject(entryRaw);
3887
4486
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
3888
- return decisionBundle.execution_contract_actionable === true || entry.execution_contract_actionable === true;
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) {
4493
+ return decisionBundle.execution_contract_actionable === true;
4494
+ }
4495
+ return entry.execution_contract_actionable === true;
3889
4496
  }
3890
4497
 
3891
4498
  function runnerRequestPreferredExecutionContractTargets(entryRaw) {
@@ -3895,14 +4502,14 @@ function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3895
4502
  ensureArray(decisionBundle.execution_contract_targets).length
3896
4503
  ? decisionBundle.execution_contract_targets
3897
4504
  : ensureArray(entry.execution_contract_targets).length
3898
- ? entry.execution_contract_targets
3899
- : ensureArray(entry.root_execution_contract_targets).length
3900
- ? entry.root_execution_contract_targets
3901
- : [],
4505
+ ? entry.execution_contract_targets
4506
+ : ensureArray(entry.root_execution_contract_targets).length
4507
+ ? entry.root_execution_contract_targets
4508
+ : [],
3902
4509
  normalizeTelegramMentionUsername,
3903
4510
  );
3904
4511
  }
3905
-
4512
+
3906
4513
  function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3907
4514
  const entry = safeObject(entryRaw);
3908
4515
  const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
@@ -3910,32 +4517,38 @@ function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3910
4517
  ensureArray(decisionBundle.next_expected_responders).length
3911
4518
  ? decisionBundle.next_expected_responders
3912
4519
  : ensureArray(entry.next_expected_responders).length
3913
- ? entry.next_expected_responders
3914
- : ensureArray(entry.root_next_expected_responders).length
3915
- ? entry.root_next_expected_responders
3916
- : [],
4520
+ ? entry.next_expected_responders
4521
+ : ensureArray(entry.root_next_expected_responders).length
4522
+ ? entry.root_next_expected_responders
4523
+ : [],
3917
4524
  normalizeTelegramMentionUsername,
3918
4525
  );
3919
4526
  }
3920
-
4527
+
3921
4528
  function runnerRequestPreferredAuthoritySelectedBotUsernames(entryRaw) {
3922
4529
  const entry = safeObject(entryRaw);
4530
+ const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
4531
+ const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
3923
4532
  const normalizedSummaryBot = normalizeTelegramMentionUsername(entry.conversation_summary_bot);
3924
4533
  return uniqueOrderedStrings(
3925
- runnerRequestPreferredNextExpectedResponders(entry).length
3926
- ? runnerRequestPreferredNextExpectedResponders(entry)
3927
- : runnerRequestPreferredExecutionContractTargets(entry).length
3928
- ? runnerRequestPreferredExecutionContractTargets(entry)
3929
- : ensureArray(entry.selected_bot_usernames).length
3930
- ? entry.selected_bot_usernames
3931
- : ensureArray(entry.conversation_initial_responders).length
3932
- ? entry.conversation_initial_responders
3933
- : normalizedSummaryBot
3934
- ? [normalizedSummaryBot]
3935
- : [],
3936
- normalizeTelegramMentionUsername,
3937
- );
3938
- }
4534
+ runnerRequestPreferredNextExpectedResponders(entry).length
4535
+ ? runnerRequestPreferredNextExpectedResponders(entry)
4536
+ : runnerRequestPreferredExecutionContractTargets(entry).length
4537
+ ? runnerRequestPreferredExecutionContractTargets(entry)
4538
+ : hasAuthoritativeDecisionBundle && ensureArray(decisionBundle.selected_bot_usernames).length
4539
+ ? decisionBundle.selected_bot_usernames
4540
+ : hasAuthoritativeDecisionBundle && ensureArray(decisionBundle.initial_responders).length
4541
+ ? decisionBundle.initial_responders
4542
+ : ensureArray(entry.selected_bot_usernames).length
4543
+ ? entry.selected_bot_usernames
4544
+ : ensureArray(entry.conversation_initial_responders).length
4545
+ ? entry.conversation_initial_responders
4546
+ : normalizedSummaryBot
4547
+ ? [normalizedSummaryBot]
4548
+ : [],
4549
+ normalizeTelegramMentionUsername,
4550
+ );
4551
+ }
3939
4552
 
3940
4553
  function extractRunnerExplicitSelectedBotUsernamesFromParsed(parsedArchiveRaw) {
3941
4554
  const parsedArchive = safeObject(parsedArchiveRaw);
@@ -3956,6 +4569,8 @@ function runnerRequestAllowsSharedHumanClaimTakeover({
3956
4569
  canonicalHumanMessageKey = "",
3957
4570
  }) {
3958
4571
  const entry = safeObject(entryRaw);
4572
+ const decisionBundle = runnerRequestAuthoritativeDecisionBundle(entry);
4573
+ const hasAuthoritativeDecisionBundle = Object.keys(decisionBundle).length > 0;
3959
4574
  const normalizedCurrentBotUsername = normalizeTelegramMentionUsername(currentBotUsername);
3960
4575
  const normalizedCanonicalHumanMessageKey = String(canonicalHumanMessageKey || "").trim();
3961
4576
  if (!normalizedCurrentBotUsername || !normalizedCanonicalHumanMessageKey) {
@@ -3969,11 +4584,17 @@ function runnerRequestAllowsSharedHumanClaimTakeover({
3969
4584
  return pendingResponders.includes(normalizedCurrentBotUsername);
3970
4585
  }
3971
4586
  const directHumanResponders = uniqueOrderedStrings(
3972
- [
3973
- ...ensureArray(entry.selected_bot_usernames),
3974
- ...ensureArray(entry.conversation_initial_responders),
3975
- ...ensureArray(entry.conversation_allowed_responders),
3976
- ],
4587
+ hasAuthoritativeDecisionBundle
4588
+ ? [
4589
+ ...ensureArray(decisionBundle.selected_bot_usernames),
4590
+ ...ensureArray(decisionBundle.initial_responders),
4591
+ ...ensureArray(decisionBundle.allowed_responders),
4592
+ ]
4593
+ : [
4594
+ ...ensureArray(entry.selected_bot_usernames),
4595
+ ...ensureArray(entry.conversation_initial_responders),
4596
+ ...ensureArray(entry.conversation_allowed_responders),
4597
+ ],
3977
4598
  normalizeTelegramMentionUsername,
3978
4599
  );
3979
4600
  return directHumanResponders.includes(normalizedCurrentBotUsername);
@@ -4715,10 +5336,10 @@ function resolveRunnerReplyChainConversationContext(state, normalizedRoute, sele
4715
5336
  };
4716
5337
  }
4717
5338
 
4718
- async function resolveRunnerReplyChainConversationContextWithServerFallback({
4719
- state,
4720
- normalizedRoute,
4721
- selectedRecord,
5339
+ async function resolveRunnerReplyChainConversationContextWithServerFallback({
5340
+ state,
5341
+ normalizedRoute,
5342
+ selectedRecord,
4722
5343
  runtime,
4723
5344
  archiveThreadID = "",
4724
5345
  hydrationAttempted = false,
@@ -4733,14 +5354,70 @@ async function resolveRunnerReplyChainConversationContextWithServerFallback({
4733
5354
  };
4734
5355
  }
4735
5356
  const parsed = safeObject(selectedRecord?.parsedArchive);
4736
- const initialReplyToMessageID = intFromRawAllowZero(
4737
- parsed.replyToMessageID || safeObject(initialContext).replyToMessageID,
4738
- 0,
4739
- );
4740
- if (initialReplyToMessageID <= 0) {
4741
- return {
4742
- state: initialState,
4743
- 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,
4744
5421
  hydrated: false,
4745
5422
  };
4746
5423
  }
@@ -5070,7 +5747,27 @@ async function claimRunnerRequestForHumanComment({
5070
5747
  requests: backfilled.requests,
5071
5748
  };
5072
5749
  }
5073
- 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);
5074
5771
  let sharedConversationSource = currentMessageID > 0
5075
5772
  ? pickRunnerSharedConversationSourceRequest(
5076
5773
  findRunnerRequestsForMessageID(stateForClaim, normalizedRoute, {
@@ -5081,10 +5778,10 @@ async function claimRunnerRequestForHumanComment({
5081
5778
  provisionalRequestKey,
5082
5779
  )
5083
5780
  : {};
5084
- if (
5085
- !Object.keys(sharedConversationSource).length
5086
- && currentMessageID > 0
5087
- && runtime?.baseURL
5781
+ if (
5782
+ !Object.keys(sharedConversationSource).length
5783
+ && currentMessageID > 0
5784
+ && runtime?.baseURL
5088
5785
  && runtime?.token
5089
5786
  ) {
5090
5787
  sharedConversationSource = safeObject(await findServerRunnerConversationSourceRequestForMessageID({
@@ -5096,68 +5793,332 @@ async function claimRunnerRequestForHumanComment({
5096
5793
  excludeRequestKey: provisionalRequestKey,
5097
5794
  }));
5098
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
+ }
5099
5806
  const authorityContext = resolveRunnerHumanCommentAuthorityContext({
5100
5807
  normalizedRoute,
5101
5808
  selectedRecord,
5102
- replyChainContext,
5103
- referencedRequest,
5809
+ replyChainContext,
5810
+ referencedRequest,
5104
5811
  sharedConversationSource,
5105
5812
  selectedBotUsernames,
5106
- normalizedSharedHumanIntent,
5107
- resolvedNormalizedIntent,
5108
- });
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
+ }
5109
5890
  const authoritySource = safeObject(authorityContext.authoritySource);
5110
- const decisionBundleValidation = validateRunnerConversationDecisionBundle(decisionBundle);
5111
- if (
5112
- Object.keys(safeObject(decisionBundle)).length > 0
5113
- && decisionBundleValidation.ok !== true
5114
- ) {
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) {
5115
5964
  return {
5116
5965
  ok: false,
5117
5966
  reason: "invalid_decision_bundle",
5118
5967
  detail: String(decisionBundleValidation.reason || "").trim() || "invalid_decision_bundle",
5119
5968
  };
5120
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
+ }
5121
5980
  const authoritativeDecisionBundle = decisionBundleValidation.ok === true
5122
5981
  ? safeObject(decisionBundleValidation.bundle)
5123
5982
  : {};
5124
- const normalizedAuthoritativeSourceMessageEnvelope = normalizeRunnerTelegramMessageEnvelope(
5125
- authoritativeSourceMessageEnvelope,
5126
- );
5127
- const currentClaimBotUsername = normalizeTelegramMentionUsername(
5128
- normalizedAuthoritativeSourceMessageEnvelope.source_bot_username
5129
- || normalizedAuthoritativeSourceMessageEnvelope.sender_username
5130
- || currentRouteState.last_source_bot_username
5131
- || currentRouteState.source_message_bot_username
5132
- || 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,
5133
6002
  );
5134
- const authorityReplyChainContext = await buildRunnerAuthorityReplyChainContext({
5135
- selectedRecord,
5136
- replyChainContext,
5137
- authoritySource,
5138
- runtime,
5139
- archiveThreadID,
5140
- });
5141
- const resolvedConversationID = String(authorityContext.conversationID || "").trim();
5142
- const preferredNormalizedIntent = String(authorityContext.normalizedIntent || "").trim().toLowerCase();
5143
- const requestSelectedBotUsernames = ensureArray(authorityContext.selectedBotUsernames);
5144
6003
  const effectiveSelectedBotUsernames = uniqueOrderedStrings(
5145
6004
  ensureArray(authoritativeDecisionBundle.selected_bot_usernames).length
5146
6005
  ? authoritativeDecisionBundle.selected_bot_usernames
5147
6006
  : requestSelectedBotUsernames,
5148
6007
  normalizeTelegramMentionUsername,
5149
6008
  );
5150
- const requestKey = buildRunnerRequestKey({
5151
- normalizedRoute,
5152
- selectedRecord,
5153
- selectedBotUsernames: effectiveSelectedBotUsernames,
5154
- normalizedIntent: preferredNormalizedIntent,
5155
- conversationID: resolvedConversationID,
5156
- });
5157
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
5158
- const existing = safeObject(requests[requestKey]);
5159
- if (isFinalRunnerRequestStatus(existing.status)) {
5160
- 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 {
5161
6122
  ok: false,
5162
6123
  reason: "request_already_finalized",
5163
6124
  requestKey,
@@ -5181,6 +6142,15 @@ async function claimRunnerRequestForHumanComment({
5181
6142
  }
5182
6143
  const nowISO = new Date().toISOString();
5183
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
+ );
5184
6154
  const preserveExistingCanonicalSourceEnvelope = Boolean(
5185
6155
  canonicalHumanMessageKey
5186
6156
  && String(existing.canonical_human_message_key || "").trim() === canonicalHumanMessageKey
@@ -5192,6 +6162,23 @@ async function claimRunnerRequestForHumanComment({
5192
6162
  : Object.keys(normalizedAuthoritativeSourceMessageEnvelope).length
5193
6163
  ? normalizedAuthoritativeSourceMessageEnvelope
5194
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;
5195
6182
  const decisionConversationParticipants = uniqueOrderedStrings(
5196
6183
  ensureArray(authoritativeDecisionBundle.participants),
5197
6184
  normalizeTelegramMentionUsername,
@@ -5205,33 +6192,114 @@ async function claimRunnerRequestForHumanComment({
5205
6192
  normalizeTelegramMentionUsername,
5206
6193
  );
5207
6194
  const hasAuthoritativeConversationDecision = Object.keys(authoritativeDecisionBundle).length > 0;
5208
- const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
5209
- project_id: String(normalizedRoute?.projectID || "").trim(),
5210
- provider: String(normalizedRoute?.provider || "").trim(),
5211
- chat_id: String(parsed.chatID || parsed.chatId || "").trim(),
5212
- source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5213
- source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5214
- source_message_body: String(parsed.body || "").trim(),
5215
- canonical_human_message_key: canonicalHumanMessageKey,
5216
- source_message_origin: String(sourceMessageEnvelope.source_origin || "").trim().toLowerCase(),
5217
- source_message_route_key: String(sourceMessageEnvelope.source_route_key || "").trim(),
5218
- source_message_bot_username: normalizeTelegramMentionUsername(sourceMessageEnvelope.source_bot_username),
5219
- source_message_envelope: sourceMessageEnvelope,
5220
- root_comment_id: String(selectedRecord?.id || "").trim(),
5221
- root_comment_kind: commentKind,
5222
- 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,
5223
6285
  reply_chain_context: authorityReplyChainContext,
5224
6286
  selected_bot_usernames: effectiveSelectedBotUsernames,
5225
- authoritative_decision_bundle: authoritativeDecisionBundle,
5226
- decision_bundle_validation_status: String(decisionBundleValidation.status || "").trim(),
5227
- 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(),
5228
6296
  conversation_intent_mode: String(
5229
6297
  (hasAuthoritativeConversationDecision
5230
6298
  ? authoritativeDecisionBundle.conversation_intent_mode
5231
6299
  : "")
5232
6300
  || normalizedSharedHumanIntent.intentMode
5233
6301
  || existing.conversation_intent_mode
5234
- || authoritySource.conversation_intent_mode
6302
+ || effectiveAuthoritySource.conversation_intent_mode
5235
6303
  || "",
5236
6304
  ).trim().toLowerCase(),
5237
6305
  conversation_lead_bot: normalizeTelegramMentionUsername(
@@ -5240,7 +6308,7 @@ async function claimRunnerRequestForHumanComment({
5240
6308
  : "")
5241
6309
  || normalizedSharedHumanIntent.leadBotSelector
5242
6310
  || existing.conversation_lead_bot
5243
- || authoritySource.conversation_lead_bot,
6311
+ || effectiveAuthoritySource.conversation_lead_bot,
5244
6312
  ),
5245
6313
  conversation_summary_bot: normalizeTelegramMentionUsername(
5246
6314
  (hasAuthoritativeConversationDecision
@@ -5248,7 +6316,7 @@ async function claimRunnerRequestForHumanComment({
5248
6316
  : "")
5249
6317
  || normalizedSharedHumanIntent.summaryBotSelector
5250
6318
  || existing.conversation_summary_bot
5251
- || authoritySource.conversation_summary_bot,
6319
+ || effectiveAuthoritySource.conversation_summary_bot,
5252
6320
  ),
5253
6321
  conversation_participants: uniqueOrderedStrings(
5254
6322
  hasAuthoritativeConversationDecision && decisionConversationParticipants.length
@@ -5257,10 +6325,10 @@ async function claimRunnerRequestForHumanComment({
5257
6325
  ? normalizedSharedHumanIntent.participantSelectors
5258
6326
  : ensureArray(existing.conversation_participants).length
5259
6327
  ? existing.conversation_participants
5260
- : ensureArray(authoritySource.conversation_participants).length
5261
- ? authoritySource.conversation_participants
5262
- : [],
5263
- normalizeTelegramMentionUsername,
6328
+ : ensureArray(effectiveAuthoritySource.conversation_participants).length
6329
+ ? effectiveAuthoritySource.conversation_participants
6330
+ : [],
6331
+ normalizeTelegramMentionUsername,
5264
6332
  ),
5265
6333
  conversation_initial_responders: uniqueOrderedStrings(
5266
6334
  hasAuthoritativeConversationDecision && decisionInitialResponders.length
@@ -5269,10 +6337,10 @@ async function claimRunnerRequestForHumanComment({
5269
6337
  ? normalizedSharedHumanIntent.initialResponderSelectors
5270
6338
  : ensureArray(existing.conversation_initial_responders).length
5271
6339
  ? existing.conversation_initial_responders
5272
- : ensureArray(authoritySource.conversation_initial_responders).length
5273
- ? authoritySource.conversation_initial_responders
5274
- : [],
5275
- normalizeTelegramMentionUsername,
6340
+ : ensureArray(effectiveAuthoritySource.conversation_initial_responders).length
6341
+ ? effectiveAuthoritySource.conversation_initial_responders
6342
+ : [],
6343
+ normalizeTelegramMentionUsername,
5276
6344
  ),
5277
6345
  conversation_allowed_responders: uniqueOrderedStrings(
5278
6346
  hasAuthoritativeConversationDecision && decisionAllowedResponders.length
@@ -5281,9 +6349,9 @@ async function claimRunnerRequestForHumanComment({
5281
6349
  ? normalizedSharedHumanIntent.allowedResponderSelectors
5282
6350
  : ensureArray(existing.conversation_allowed_responders).length
5283
6351
  ? existing.conversation_allowed_responders
5284
- : ensureArray(authoritySource.conversation_allowed_responders).length
5285
- ? authoritySource.conversation_allowed_responders
5286
- : [],
6352
+ : ensureArray(effectiveAuthoritySource.conversation_allowed_responders).length
6353
+ ? effectiveAuthoritySource.conversation_allowed_responders
6354
+ : [],
5287
6355
  normalizeTelegramMentionUsername,
5288
6356
  ),
5289
6357
  conversation_allow_bot_to_bot: (hasAuthoritativeConversationDecision
@@ -5291,14 +6359,14 @@ async function claimRunnerRequestForHumanComment({
5291
6359
  : false)
5292
6360
  || normalizedSharedHumanIntent.allowBotToBot === true
5293
6361
  || existing.conversation_allow_bot_to_bot === true
5294
- || authoritySource.conversation_allow_bot_to_bot === true,
6362
+ || effectiveAuthoritySource.conversation_allow_bot_to_bot === true,
5295
6363
  conversation_reply_expectation: String(
5296
6364
  (hasAuthoritativeConversationDecision
5297
6365
  ? authoritativeDecisionBundle.conversation_reply_expectation
5298
6366
  : "")
5299
6367
  || normalizedSharedHumanIntent.replyExpectation
5300
6368
  || existing.conversation_reply_expectation
5301
- || authoritySource.conversation_reply_expectation
6369
+ || effectiveAuthoritySource.conversation_reply_expectation
5302
6370
  || "",
5303
6371
  ).trim().toLowerCase(),
5304
6372
  execution_contract_type: String(
@@ -5306,20 +6374,20 @@ async function claimRunnerRequestForHumanComment({
5306
6374
  ? authoritativeDecisionBundle.execution_contract_type
5307
6375
  : "")
5308
6376
  || runnerRequestPreferredExecutionContractType(existing)
5309
- || runnerRequestPreferredExecutionContractType(authoritySource)
6377
+ || runnerRequestPreferredExecutionContractType(effectiveAuthoritySource)
5310
6378
  || "",
5311
6379
  ).trim().toLowerCase(),
5312
6380
  execution_contract_actionable: (hasAuthoritativeConversationDecision
5313
6381
  ? authoritativeDecisionBundle.execution_contract_actionable === true
5314
6382
  : false)
5315
6383
  || runnerRequestPreferredExecutionContractActionable(existing)
5316
- || runnerRequestPreferredExecutionContractActionable(authoritySource),
6384
+ || runnerRequestPreferredExecutionContractActionable(effectiveAuthoritySource),
5317
6385
  execution_contract_targets: uniqueOrderedStrings(
5318
6386
  hasAuthoritativeConversationDecision && ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
5319
6387
  ? authoritativeDecisionBundle.execution_contract_targets
5320
6388
  : runnerRequestPreferredExecutionContractTargets(existing).length
5321
6389
  ? runnerRequestPreferredExecutionContractTargets(existing)
5322
- : runnerRequestPreferredExecutionContractTargets(authoritySource),
6390
+ : runnerRequestPreferredExecutionContractTargets(effectiveAuthoritySource),
5323
6391
  normalizeTelegramMentionUsername,
5324
6392
  ),
5325
6393
  next_expected_responders: uniqueOrderedStrings(
@@ -5327,31 +6395,77 @@ async function claimRunnerRequestForHumanComment({
5327
6395
  ? authoritativeDecisionBundle.next_expected_responders
5328
6396
  : runnerRequestPreferredNextExpectedResponders(existing).length
5329
6397
  ? runnerRequestPreferredNextExpectedResponders(existing)
5330
- : runnerRequestPreferredNextExpectedResponders(authoritySource),
6398
+ : runnerRequestPreferredNextExpectedResponders(effectiveAuthoritySource),
5331
6399
  normalizeTelegramMentionUsername,
5332
- ),
5333
- normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
6400
+ ),
6401
+ normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
5334
6402
  status: "claimed",
5335
- claimed_by_route: String(routeKey || "").trim(),
5336
- claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
5337
- root_work_item_id: String(existing.root_work_item_id || authoritySource.root_work_item_id || "").trim(),
5338
- root_work_item_title: String(existing.root_work_item_title || authoritySource.root_work_item_title || "").trim(),
5339
- root_work_item_status: normalizeRunnerWorkItemStatus(
5340
- existing.root_work_item_status || authoritySource.root_work_item_status,
5341
- ),
5342
- root_thread_id: String(existing.root_thread_id || authoritySource.root_thread_id || "").trim(),
5343
- root_work_item_created_at: firstNonEmptyString([
5344
- existing.root_work_item_created_at,
5345
- authoritySource.root_work_item_created_at,
5346
- ]),
5347
- root_work_item_last_error: String(
5348
- existing.root_work_item_last_error || authoritySource.root_work_item_last_error || "",
5349
- ).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(),
5350
6418
  last_comment_id: String(selectedRecord?.id || "").trim(),
5351
6419
  last_comment_kind: commentKind,
5352
- last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
5353
- last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
5354
- });
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
+ }
5355
6469
  const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
5356
6470
  project_id: String(normalizedRoute?.projectID || "").trim(),
5357
6471
  provider: String(normalizedRoute?.provider || "").trim(),
@@ -5365,18 +6479,55 @@ async function claimRunnerRequestForHumanComment({
5365
6479
  comment_kind: commentKind,
5366
6480
  request_status: "claimed",
5367
6481
  });
5368
- saveBotRunnerState({
5369
- routes: stateForClaim.routes,
5370
- sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
5371
- excludedComments: stateForClaim.excludedComments || stateForClaim.excluded_comments,
5372
- requests: nextRequests,
5373
- consumedComments: nextConsumedComments,
5374
- });
5375
- return {
5376
- ok: true,
5377
- requestKey,
5378
- request,
5379
- };
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
+ };
5380
6531
  }
5381
6532
 
5382
6533
  function runnerRequestRequiresActionableContract(requestRaw) {
@@ -6391,6 +7542,9 @@ function markRunnerRequestLifecycle({
6391
7542
  const sourceMessageEnvelope = Object.keys(normalizedAuthoritativeSourceMessageEnvelope).length
6392
7543
  ? normalizedAuthoritativeSourceMessageEnvelope
6393
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;
6394
7548
  const normalizedDecisionBundle = normalizeRunnerConversationDecisionBundle(decisionBundle);
6395
7549
  const resolvedDecisionBundleValidation = Object.keys(safeObject(decisionBundle)).length > 0
6396
7550
  ? validateRunnerConversationDecisionBundle(normalizedDecisionBundle)
@@ -6400,9 +7554,60 @@ function markRunnerRequestLifecycle({
6400
7554
  reason: String(decisionBundleValidationReason || "").trim(),
6401
7555
  bundle: {},
6402
7556
  };
6403
- const authoritativeDecisionBundle = resolvedDecisionBundleValidation.ok === true
7557
+ const existingAuthoritativeDecisionBundle = runnerRequestAuthoritativeDecisionBundle(existing);
7558
+ const incomingDecisionBundle = resolvedDecisionBundleValidation.ok === true
6404
7559
  ? safeObject(resolvedDecisionBundleValidation.bundle)
6405
- : 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();
6406
7611
  const effectiveReplyToMessageID = intFromRawAllowZero(replyToMessageID, 0);
6407
7612
  const lastReplyMessageEnvelope = buildTelegramBotReplyEnvelope({
6408
7613
  sourceEnvelope: sourceMessageEnvelope,
@@ -6416,7 +7621,7 @@ function markRunnerRequestLifecycle({
6416
7621
  const normalizedDeliveryStatus = String(deliveryStatus || "").trim().toLowerCase();
6417
7622
  const persistSuccessfulReplyEnvelope = ["delivered", "dry_run"].includes(normalizedDeliveryStatus)
6418
7623
  && intFromRawAllowZero(lastReplyMessageEnvelope.message_id, 0) > 0;
6419
- const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
7624
+ const attemptedDeliveryEnvelope = buildTelegramBotReplyEnvelope({
6420
7625
  sourceEnvelope: sourceMessageEnvelope,
6421
7626
  chatID: existing.chat_id,
6422
7627
  messageThreadID: lastReplyMessageThreadID,
@@ -6432,9 +7637,13 @@ function markRunnerRequestLifecycle({
6432
7637
  || intFromRawAllowZero(replyToMessageID, 0) > 0
6433
7638
  || intFromRawAllowZero(lastReplyMessageThreadID, 0) > 0
6434
7639
  );
7640
+ const continuationDecisionBundle = Object.keys(incomingDecisionBundle).length > 0
7641
+ ? incomingDecisionBundle
7642
+ : safeObject(existing.last_reply_decision_bundle);
6435
7643
  const rootEffectiveExecutionContractTargets = uniqueOrderedStrings(
6436
7644
  [
6437
7645
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets),
7646
+ ...ensureArray(continuationDecisionBundle.execution_contract_targets),
6438
7647
  ...ensureArray(executionContractTargets),
6439
7648
  ...ensureArray(normalizedExecutionContractTargets),
6440
7649
  ...ensureArray(responseContractValidationTargets),
@@ -6444,6 +7653,7 @@ function markRunnerRequestLifecycle({
6444
7653
  const rootEffectiveNextExpectedResponders = uniqueOrderedStrings(
6445
7654
  [
6446
7655
  ...ensureArray(authoritativeDecisionBundle.next_expected_responders),
7656
+ ...ensureArray(continuationDecisionBundle.next_expected_responders),
6447
7657
  ...ensureArray(nextExpectedResponders),
6448
7658
  ...ensureArray(normalizedExecutionNextResponders),
6449
7659
  ...ensureArray(responseContractValidationTargets),
@@ -6454,11 +7664,15 @@ function markRunnerRequestLifecycle({
6454
7664
  [
6455
7665
  ...ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
6456
7666
  ? ensureArray(authoritativeDecisionBundle.execution_contract_targets)
7667
+ : ensureArray(continuationDecisionBundle.execution_contract_targets).length
7668
+ ? ensureArray(continuationDecisionBundle.execution_contract_targets)
6457
7669
  : ensureArray(executionContractTargets).length
6458
7670
  ? ensureArray(executionContractTargets)
6459
7671
  : ensureArray(existing.execution_contract_targets),
6460
7672
  ...ensureArray(authoritativeDecisionBundle.next_expected_responders).length
6461
7673
  ? ensureArray(authoritativeDecisionBundle.next_expected_responders)
7674
+ : ensureArray(continuationDecisionBundle.next_expected_responders).length
7675
+ ? ensureArray(continuationDecisionBundle.next_expected_responders)
6462
7676
  : ensureArray(nextExpectedResponders).length
6463
7677
  ? ensureArray(nextExpectedResponders)
6464
7678
  : ensureArray(existing.next_expected_responders),
@@ -6467,13 +7681,20 @@ function markRunnerRequestLifecycle({
6467
7681
  ).filter((selector) => selector && selector !== normalizedCurrentBotSelector);
6468
7682
  const nextExecutionContractType = String(
6469
7683
  authoritativeDecisionBundle.execution_contract_type
7684
+ || continuationDecisionBundle.execution_contract_type
6470
7685
  || executionContractType
6471
7686
  || existing.execution_contract_type
6472
7687
  || "",
6473
7688
  ).trim().toLowerCase();
7689
+ const shouldPromoteRootContinuationState = isRootHumanComment || existingHasImmutableRootAuthority;
6474
7690
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6475
7691
  const normalizedFailureReplyClassification = String(failureReplyClassification || "").trim().toLowerCase();
6476
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
+ );
6477
7698
  const shouldPersistReplyAnchor = (
6478
7699
  aiReplyGenerated === true
6479
7700
  || intFromRawAllowZero(lastReplyMessageID, 0) > 0
@@ -6488,6 +7709,7 @@ function markRunnerRequestLifecycle({
6488
7709
  : continuationSelectors.length > 0;
6489
7710
  const shouldRemainRunningAfterSkip = normalizedOutcome === "skipped"
6490
7711
  && parsedKind === "bot_reply"
7712
+ && !isTerminalStaleReplyAnchorSkip
6491
7713
  && authoritativeDecisionBundle.should_close_after_reply !== true
6492
7714
  && (
6493
7715
  nextExecutionContractType === "delegation"
@@ -6548,25 +7770,17 @@ function markRunnerRequestLifecycle({
6548
7770
  return normalizeRunnerRequestStatus(existing.status);
6549
7771
  })();
6550
7772
  const nowISO = new Date().toISOString();
6551
- const commentKind = String(parsed.kind || "").trim().toLowerCase();
6552
- const isRootHumanComment = ["telegram_message", "telegram_edited_message"].includes(commentKind);
6553
- const isFollowupComment = !isRootHumanComment;
6554
7773
  const patch = {
6555
7774
  authoritative_decision_bundle: Object.keys(authoritativeDecisionBundle).length > 0
6556
7775
  ? authoritativeDecisionBundle
6557
7776
  : safeObject(existing.authoritative_decision_bundle),
6558
- decision_bundle_validation_status: String(
6559
- resolvedDecisionBundleValidation.status
6560
- || decisionBundleValidationStatus
6561
- || existing.decision_bundle_validation_status
6562
- || "",
6563
- ).trim(),
6564
- decision_bundle_validation_reason: String(
6565
- resolvedDecisionBundleValidation.reason
6566
- || decisionBundleValidationReason
6567
- || existing.decision_bundle_validation_reason
6568
- || "",
6569
- ).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,
6570
7784
  conversation_id: conversationID,
6571
7785
  conversation_participants: uniqueOrderedStrings(
6572
7786
  ensureArray(authoritativeDecisionBundle.participants).length
@@ -6621,7 +7835,7 @@ function markRunnerRequestLifecycle({
6621
7835
  execution_contract_targets: uniqueOrderedStrings(
6622
7836
  ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
6623
7837
  ? authoritativeDecisionBundle.execution_contract_targets
6624
- : isRootHumanComment && rootEffectiveExecutionContractTargets.length
7838
+ : shouldPromoteRootContinuationState && rootEffectiveExecutionContractTargets.length
6625
7839
  ? rootEffectiveExecutionContractTargets
6626
7840
  : ensureArray(executionContractTargets).length
6627
7841
  ? executionContractTargets
@@ -6631,7 +7845,7 @@ function markRunnerRequestLifecycle({
6631
7845
  next_expected_responders: uniqueOrderedStrings(
6632
7846
  ensureArray(authoritativeDecisionBundle.next_expected_responders).length
6633
7847
  ? authoritativeDecisionBundle.next_expected_responders
6634
- : isRootHumanComment && rootEffectiveNextExpectedResponders.length
7848
+ : shouldPromoteRootContinuationState && rootEffectiveNextExpectedResponders.length
6635
7849
  ? rootEffectiveNextExpectedResponders
6636
7850
  : ensureArray(nextExpectedResponders).length
6637
7851
  ? nextExpectedResponders
@@ -6667,24 +7881,24 @@ function markRunnerRequestLifecycle({
6667
7881
  ? aiReplyPreview || existing.followup_ai_reply_preview || ""
6668
7882
  : existing.followup_ai_reply_preview || "",
6669
7883
  ).trim(),
6670
- root_execution_contract_type: String(
6671
- isRootHumanComment
6672
- ? nextExecutionContractType
6673
- : existing.root_execution_contract_type || existing.execution_contract_type || "",
6674
- ).trim().toLowerCase(),
6675
- root_execution_contract_targets: uniqueOrderedStrings(
6676
- isRootHumanComment && rootEffectiveExecutionContractTargets.length
6677
- ? rootEffectiveExecutionContractTargets
6678
- : ensureArray(existing.root_execution_contract_targets).length
6679
- ? 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
6680
7894
  : existing.execution_contract_targets,
6681
7895
  normalizeTelegramMentionUsername,
6682
7896
  ),
6683
- root_next_expected_responders: uniqueOrderedStrings(
6684
- isRootHumanComment && rootEffectiveNextExpectedResponders.length
6685
- ? rootEffectiveNextExpectedResponders
6686
- : ensureArray(existing.root_next_expected_responders).length
6687
- ? 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
6688
7902
  : existing.next_expected_responders,
6689
7903
  normalizeTelegramMentionUsername,
6690
7904
  ),
@@ -6978,18 +8192,36 @@ function cleanupBotRunnerRequestState({
6978
8192
  }
6979
8193
  const expiresAtMs = Date.parse(String(session.expires_at || "").trim());
6980
8194
  const expired = Number.isFinite(expiresAtMs) && expiresAtMs <= nowMs;
6981
- const activeRequests = Object.values(nextRequests).filter((entry) => (
6982
- String(entry.project_id || "").trim() === String(candidateRoute.projectID || "").trim()
6983
- && String(entry.provider || "").trim() === String(candidateRoute.provider || "").trim()
6984
- && String(entry.conversation_id || "").trim() === String(conversationID || "").trim()
6985
- && isActiveRunnerRequestStatus(entry.status)
6986
- ));
6987
- const pendingContinuationResponders = ensureArray(session.next_expected_responders)
6988
- .map((value) => normalizeTelegramMentionUsername(value))
6989
- .filter(Boolean);
6990
- if (!expired && (activeRequests.length > 0 || pendingContinuationResponders.length > 0)) {
6991
- continue;
6992
- }
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
+ }
6993
8225
  const closedReason = expired ? "expired_session" : "orphaned_open_session";
6994
8226
  conversationSessions[conversationID] = {
6995
8227
  ...session,
@@ -7096,18 +8328,30 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7096
8328
  merged[fieldName] = localValue;
7097
8329
  }
7098
8330
  };
7099
- const preserveLocalArrayWhenServerEmpty = (fieldName) => {
7100
- const serverValues = ensureArray(serverEntry[fieldName]).filter(Boolean);
7101
- const localValues = ensureArray(localEntry[fieldName]).filter(Boolean);
7102
- if (serverValues.length === 0 && localValues.length > 0) {
7103
- merged[fieldName] = localValues;
7104
- }
7105
- };
7106
- const preserveLocalObjectWhenServerBlank = (fieldName) => {
7107
- const serverValue = safeObject(serverEntry[fieldName]);
7108
- const localValue = safeObject(localEntry[fieldName]);
7109
- if (!Object.keys(serverValue).length && Object.keys(localValue).length) {
7110
- merged[fieldName] = localValue;
8331
+ const preserveLocalArrayWhenServerEmpty = (fieldName) => {
8332
+ const serverValues = ensureArray(serverEntry[fieldName]).filter(Boolean);
8333
+ const localValues = ensureArray(localEntry[fieldName]).filter(Boolean);
8334
+ if (serverValues.length === 0 && localValues.length > 0) {
8335
+ merged[fieldName] = localValues;
8336
+ }
8337
+ };
8338
+ const clearMergedStringWhenServerBlank = (fieldName) => {
8339
+ const serverValue = String(serverEntry[fieldName] || "").trim();
8340
+ if (!serverValue) {
8341
+ delete merged[fieldName];
8342
+ }
8343
+ };
8344
+ const clearMergedArrayWhenServerEmpty = (fieldName) => {
8345
+ const serverValues = ensureArray(serverEntry[fieldName]).filter(Boolean);
8346
+ if (serverValues.length === 0) {
8347
+ delete merged[fieldName];
8348
+ }
8349
+ };
8350
+ const preserveLocalObjectWhenServerBlank = (fieldName) => {
8351
+ const serverValue = safeObject(serverEntry[fieldName]);
8352
+ const localValue = safeObject(localEntry[fieldName]);
8353
+ if (!Object.keys(serverValue).length && Object.keys(localValue).length) {
8354
+ merged[fieldName] = localValue;
7111
8355
  }
7112
8356
  };
7113
8357
  preserveLocalNumberWhenServerMissing("source_message_id");
@@ -7126,6 +8370,8 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7126
8370
  preserveLocalStringWhenServerBlank("conversation_reply_expectation");
7127
8371
  preserveLocalStringWhenServerBlank("decision_bundle_validation_status");
7128
8372
  preserveLocalStringWhenServerBlank("decision_bundle_validation_reason");
8373
+ preserveLocalStringWhenServerBlank("last_reply_decision_bundle_validation_status");
8374
+ preserveLocalStringWhenServerBlank("last_reply_decision_bundle_validation_reason");
7129
8375
  preserveLocalStringWhenServerBlank("execution_contract_type");
7130
8376
  preserveLocalStringWhenServerBlank("normalized_execution_contract_type");
7131
8377
  preserveLocalStringWhenServerBlank("ai_reply_generated_at");
@@ -7136,16 +8382,13 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7136
8382
  preserveLocalStringWhenServerBlank("root_thread_id");
7137
8383
  preserveLocalStringWhenServerBlank("root_work_item_created_at");
7138
8384
  preserveLocalStringWhenServerBlank("root_work_item_last_error");
7139
- preserveLocalStringWhenServerBlank("root_execution_contract_type");
7140
- preserveLocalStringWhenServerBlank("root_ai_reply_preview");
7141
- preserveLocalStringWhenServerBlank("root_response_contract_validation_status");
7142
- preserveLocalStringWhenServerBlank("root_response_contract_validation_reason");
7143
- preserveLocalStringWhenServerBlank("followup_ai_reply_preview");
7144
- preserveLocalStringWhenServerBlank("followup_execution_contract_type");
7145
- preserveLocalStringWhenServerBlank("followup_normalized_execution_contract_type");
7146
- preserveLocalStringWhenServerBlank("followup_response_contract_validation_status");
7147
- preserveLocalStringWhenServerBlank("followup_response_contract_validation_reason");
7148
- preserveLocalStringWhenServerBlank("followup_assignment_validation_status");
8385
+ preserveLocalStringWhenServerBlank("root_ai_reply_preview");
8386
+ preserveLocalStringWhenServerBlank("root_response_contract_validation_status");
8387
+ preserveLocalStringWhenServerBlank("root_response_contract_validation_reason");
8388
+ preserveLocalStringWhenServerBlank("followup_ai_reply_preview");
8389
+ preserveLocalStringWhenServerBlank("followup_response_contract_validation_status");
8390
+ preserveLocalStringWhenServerBlank("followup_response_contract_validation_reason");
8391
+ preserveLocalStringWhenServerBlank("followup_assignment_validation_status");
7149
8392
  preserveLocalStringWhenServerBlank("followup_assignment_validation_reason");
7150
8393
  preserveLocalStringWhenServerBlank("followup_delivery_status");
7151
8394
  preserveLocalStringWhenServerBlank("followup_archive_status");
@@ -7154,24 +8397,24 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
7154
8397
  preserveLocalArrayWhenServerEmpty("conversation_participants");
7155
8398
  preserveLocalArrayWhenServerEmpty("conversation_initial_responders");
7156
8399
  preserveLocalArrayWhenServerEmpty("conversation_allowed_responders");
7157
- preserveLocalArrayWhenServerEmpty("execution_contract_targets");
7158
- preserveLocalArrayWhenServerEmpty("next_expected_responders");
7159
- preserveLocalArrayWhenServerEmpty("normalized_execution_contract_targets");
7160
- preserveLocalArrayWhenServerEmpty("normalized_execution_next_responders");
7161
- preserveLocalArrayWhenServerEmpty("root_execution_contract_targets");
7162
- preserveLocalArrayWhenServerEmpty("root_next_expected_responders");
7163
- preserveLocalArrayWhenServerEmpty("root_response_contract_validation_targets");
7164
- preserveLocalArrayWhenServerEmpty("followup_execution_contract_targets");
7165
- preserveLocalArrayWhenServerEmpty("followup_next_expected_responders");
7166
- preserveLocalArrayWhenServerEmpty("followup_normalized_execution_contract_targets");
7167
- preserveLocalArrayWhenServerEmpty("followup_normalized_execution_next_responders");
8400
+ preserveLocalArrayWhenServerEmpty("root_response_contract_validation_targets");
7168
8401
  preserveLocalArrayWhenServerEmpty("followup_response_contract_validation_targets");
7169
8402
  preserveLocalArrayWhenServerEmpty("followup_assignment_validation_modes");
7170
8403
  preserveLocalObjectWhenServerBlank("authoritative_decision_bundle");
8404
+ preserveLocalObjectWhenServerBlank("last_reply_decision_bundle");
7171
8405
  preserveLocalObjectWhenServerBlank("reply_chain_context");
7172
- if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
7173
- merged.conversation_allow_bot_to_bot = true;
7174
- }
8406
+ clearMergedStringWhenServerBlank("root_execution_contract_type");
8407
+ clearMergedArrayWhenServerEmpty("root_execution_contract_targets");
8408
+ clearMergedArrayWhenServerEmpty("root_next_expected_responders");
8409
+ clearMergedStringWhenServerBlank("followup_execution_contract_type");
8410
+ clearMergedStringWhenServerBlank("followup_normalized_execution_contract_type");
8411
+ clearMergedArrayWhenServerEmpty("followup_execution_contract_targets");
8412
+ clearMergedArrayWhenServerEmpty("followup_next_expected_responders");
8413
+ clearMergedArrayWhenServerEmpty("followup_normalized_execution_contract_targets");
8414
+ clearMergedArrayWhenServerEmpty("followup_normalized_execution_next_responders");
8415
+ if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
8416
+ merged.conversation_allow_bot_to_bot = true;
8417
+ }
7175
8418
  if (serverEntry.execution_contract_actionable !== true && localEntry.execution_contract_actionable === true) {
7176
8419
  merged.execution_contract_actionable = true;
7177
8420
  }
@@ -7729,31 +8972,60 @@ function scanExternalProjectArtifacts({
7729
8972
  };
7730
8973
  }
7731
8974
 
7732
- function normalizeRunnerRouteIdentityText(rawValue) {
7733
- return String(rawValue || "").trim().toLowerCase();
7734
- }
7735
-
7736
- function runnerRouteLogicalSignature(route) {
7737
- const normalized = normalizeRunnerRoute(route);
7738
- return [
7739
- normalized.projectID || "-",
7740
- normalized.provider || "-",
7741
- normalized.role || "-",
7742
- normalized.botID || normalizeRunnerRouteIdentityText(normalized.botName) || "-",
7743
- normalized.destinationID || normalizeRunnerRouteIdentityText(normalized.destinationLabel) || "-",
7744
- ].join("::");
7745
- }
7746
-
7747
- function describeRunnerRouteLogicalTarget(route) {
7748
- const normalized = normalizeRunnerRoute(route);
7749
- return [
7750
- `project_id=${normalized.projectID || "-"}`,
7751
- `provider=${normalized.provider || "-"}`,
7752
- `role=${normalized.role || "-"}`,
7753
- `server_bot=${normalized.botName || normalized.botID || "-"}`,
7754
- `destination=${normalized.destinationLabel || normalized.destinationID || "-"}`,
7755
- ].join(", ");
7756
- }
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
+ }
7757
9029
 
7758
9030
  function findRunnerRouteLogicalConflicts(route, config) {
7759
9031
  const normalizedRoute = normalizeRunnerRoute(route);
@@ -7873,14 +9145,23 @@ async function runTasksWithConcurrencyLimit(items, limit, worker) {
7873
9145
  return output;
7874
9146
  }
7875
9147
 
7876
- function runnerRouteSchedulingGroupKey(route) {
7877
- const normalized = normalizeRunnerRoute(route);
7878
- return [
7879
- String(normalized.projectID || "").trim(),
7880
- String(normalized.provider || "").trim(),
7881
- String(normalized.destinationID || "").trim() || String(normalized.destinationLabel || "").trim() || String(normalized.name || "").trim(),
7882
- ].join("::");
7883
- }
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
+ }
7884
9165
 
7885
9166
  function groupRunnerRoutesBySchedulingTarget(routes) {
7886
9167
  const groups = [];
@@ -7914,13 +9195,14 @@ function normalizeBotRole(rawValue) {
7914
9195
  return "";
7915
9196
  }
7916
9197
 
7917
- function normalizeRunnerRoute(rawRoute, overrides = {}) {
7918
- const route = {
7919
- ...safeObject(rawRoute),
7920
- ...safeObject(overrides),
7921
- };
7922
- const projectID = String(route.project_id || route.projectID || "").trim();
7923
- 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);
7924
9206
  const role = normalizeBotRole(route.role || route.bot_role || route.botRole);
7925
9207
  const roleProfile = normalizeRunnerRoleProfileName(route.role_profile || route.roleProfile || route.execution_profile || route.executionProfile);
7926
9208
  const name = String(route.name || "").trim();
@@ -7938,11 +9220,12 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
7938
9220
  route.dry_run_delivery ?? route.dryRunDelivery ?? process.env.METHEUS_BOT_DELIVERY_DRY_RUN,
7939
9221
  false,
7940
9222
  );
7941
- return {
7942
- name,
7943
- enabled: boolFromRaw(route.enabled, true),
7944
- projectID,
7945
- provider,
9223
+ return {
9224
+ name,
9225
+ enabled: boolFromRaw(route.enabled, true),
9226
+ routeKind,
9227
+ projectID,
9228
+ provider,
7946
9229
  role,
7947
9230
  roleProfile,
7948
9231
  botName,
@@ -7962,17 +9245,18 @@ function normalizeRunnerRoute(rawRoute, overrides = {}) {
7962
9245
  };
7963
9246
  }
7964
9247
 
7965
- function runnerRouteKey(route) {
7966
- const normalized = normalizeRunnerRoute(route);
7967
- return [
7968
- normalized.name || "-",
7969
- normalized.projectID || "-",
7970
- normalized.provider || "-",
7971
- normalized.role || "-",
7972
- normalized.botID || normalized.botName || "-",
7973
- normalized.destinationID || normalized.destinationLabel || "-",
7974
- ].join("::");
7975
- }
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
+ }
7976
9260
 
7977
9261
  function validateRunnerRoute(route, { requireCommand = true, config = null } = {}) {
7978
9262
  const errors = [];
@@ -7984,12 +9268,15 @@ function validateRunnerRoute(route, { requireCommand = true, config = null } = {
7984
9268
  if (!route.provider) {
7985
9269
  errors.push("provider is required");
7986
9270
  }
7987
- if (!route.role) {
7988
- errors.push("role must be one of: monitor, review, worker, approval");
7989
- }
7990
- if (requireCommand && !route.command) {
7991
- errors.push("command is required");
7992
- }
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
+ }
7993
9280
  const configuredRoleProfiles = safeObject(normalizedConfig.roleProfiles);
7994
9281
  const resolvedRoleProfileName = normalizeRunnerRoleProfileName(route.roleProfile || route.role);
7995
9282
  if (resolvedRoleProfileName && !configuredRoleProfiles[resolvedRoleProfileName] && !legacyCommandAvailable) {
@@ -8050,10 +9337,10 @@ function resolveConfiguredRunnerRouteServerBotName(route, telegramEntries = [])
8050
9337
  return String(matchedTelegramEntry?.serverBotName || "").trim();
8051
9338
  }
8052
9339
 
8053
- function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags, options = {}) {
8054
- const candidate = normalizeRunnerRoute(route);
8055
- const candidateBotName = resolveConfiguredRunnerRouteServerBotName(candidate, options.telegramEntries);
8056
- 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"])) {
8057
9344
  return false;
8058
9345
  }
8059
9346
  if (inlineRoute.projectID && candidate.projectID !== inlineRoute.projectID) {
@@ -8062,12 +9349,15 @@ function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags,
8062
9349
  if (inlineRoute.provider && candidate.provider !== inlineRoute.provider) {
8063
9350
  return false;
8064
9351
  }
8065
- if (inlineRoute.role && candidate.role !== inlineRoute.role) {
8066
- return false;
8067
- }
8068
- if (hasRunnerFlag(flags, "role-profile") && candidate.roleProfile !== inlineRoute.roleProfile) {
8069
- return false;
8070
- }
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
+ }
8071
9361
  if (inlineRoute.botID && candidate.botID !== inlineRoute.botID) {
8072
9362
  return false;
8073
9363
  }
@@ -8089,8 +9379,8 @@ function configuredRunnerRouteMatchesInlineSelection(route, inlineRoute, flags,
8089
9379
  return true;
8090
9380
  }
8091
9381
 
8092
- function applyRunnerRouteFlagOverrides(route, flags) {
8093
- const merged = normalizeRunnerRoute(route);
9382
+ function applyRunnerRouteFlagOverrides(route, flags) {
9383
+ const merged = normalizeRunnerRoute(route);
8094
9384
  if (hasRunnerFlag(flags, "route-name")) {
8095
9385
  merged.name = String(flags["route-name"] || "").trim();
8096
9386
  }
@@ -8103,12 +9393,15 @@ function applyRunnerRouteFlagOverrides(route, flags) {
8103
9393
  if (hasRunnerFlag(flags, "role")) {
8104
9394
  merged.role = normalizeBotRole(flags.role);
8105
9395
  }
8106
- if (hasRunnerFlag(flags, "role-profile")) {
8107
- merged.roleProfile = normalizeRunnerRoleProfileName(flags["role-profile"]);
8108
- }
8109
- if (hasRunnerFlag(flags, "bot-name")) {
8110
- merged.botName = String(flags["bot-name"] || "").trim();
8111
- }
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
+ }
8112
9405
  if (hasRunnerFlag(flags, "bot-id")) {
8113
9406
  merged.botID = String(flags["bot-id"] || "").trim();
8114
9407
  }
@@ -8194,12 +9487,13 @@ function applyRunnerRouteFlagOverrides(route, flags) {
8194
9487
  return merged;
8195
9488
  }
8196
9489
 
8197
- function resolveRunnerRoutes(flags, mode) {
8198
- const inlineRoute = normalizeRunnerRoute({
8199
- name: flags["route-name"],
8200
- enabled: true,
8201
- project_id: flags["project-id"],
8202
- 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,
8203
9497
  role: flags.role,
8204
9498
  role_profile: flags["role-profile"],
8205
9499
  bot_name: flags["bot-name"],
@@ -8309,13 +9603,29 @@ function resolveRunnerRoutes(flags, mode) {
8309
9603
  );
8310
9604
  }
8311
9605
 
8312
- async function listProjectChatDestinations(params) {
8313
- return listProjectChatDestinationsImpl(params, buildRunnerDataDeps());
8314
- }
8315
-
8316
- async function listProjectContextItems(params) {
8317
- return listProjectContextItemsImpl(params, buildRunnerDataDeps());
8318
- }
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
+ }
8319
9629
 
8320
9630
  async function listProjectRunnerRequests(params) {
8321
9631
  return listProjectRunnerRequestsImpl(params, buildRunnerDataDeps());
@@ -8394,7 +9704,7 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8394
9704
  occurredAt: normalized?.occurredAt,
8395
9705
  messageThreadID: normalized?.messageThreadID,
8396
9706
  replyToMessageID: normalized?.replyToMessageID,
8397
- body: normalized?.text,
9707
+ body: normalized?.body || normalized?.text,
8398
9708
  }),
8399
9709
  sourceOrigin: String(normalized?.archiveSourceOrigin || normalized?.sourceOrigin || "").trim().toLowerCase(),
8400
9710
  sourceRouteKey: String(normalized?.archiveSourceRouteKey || normalized?.sourceRouteKey || "").trim(),
@@ -8403,7 +9713,11 @@ function buildTelegramArchiveStructuredPayload(normalized) {
8403
9713
  replyToSender: String(normalized?.replyToFromName || "").trim(),
8404
9714
  replyToUsername: String(normalized?.replyToFromUsername || "").trim(),
8405
9715
  replyToSenderIsBot: normalized?.replyToFromIsBot === true,
8406
- 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(),
8407
9721
  };
8408
9722
  return payload;
8409
9723
  }
@@ -8547,6 +9861,12 @@ function parseArchivedChatComment(rawBody) {
8547
9861
  replyToSenderIsBot: payloadHas("replyToSenderIsBot")
8548
9862
  ? boolFromRaw(structuredPayload.replyToSenderIsBot, false)
8549
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(),
8550
9870
  botID: firstNonEmptyString([safeObject(structuredPayload).botID, metadata.bot_id]),
8551
9871
  botName: firstNonEmptyString([safeObject(structuredPayload).botName, metadata.bot_name]),
8552
9872
  botUsername: normalizeTelegramMentionUsername(
@@ -8793,9 +10113,107 @@ function toISOStringFromUnix(rawValue) {
8793
10113
  return new Date(numeric * 1000).toISOString();
8794
10114
  }
8795
10115
 
8796
- function collectTelegramUpdateText(message) {
8797
- return firstNonEmptyString([message?.text, message?.caption]);
8798
- }
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
+ }
8799
10217
 
8800
10218
  function normalizeTelegramMentionUsername(rawValue) {
8801
10219
  return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
@@ -8830,23 +10248,29 @@ function extractTelegramMentionUsernames(text, entities) {
8830
10248
  return Array.from(set);
8831
10249
  }
8832
10250
 
8833
- function normalizeLocalTelegramUpdate(rawUpdate) {
10251
+ function normalizeLocalTelegramUpdate(rawUpdate) {
8834
10252
  const update = safeObject(rawUpdate);
8835
10253
  const message = safeObject(update.message || update.edited_message);
8836
10254
  if (!message || Object.keys(message).length === 0) {
8837
10255
  return null;
8838
10256
  }
8839
- const chat = safeObject(message.chat);
8840
- const from = safeObject(message.from);
8841
- const replyTo = safeObject(message.reply_to_message);
8842
- const replyToFrom = safeObject(replyTo.from);
8843
- const text = collectTelegramUpdateText(message);
8844
- if (!text) {
8845
- return null;
8846
- }
8847
- const mentionUsernames = extractTelegramMentionUsernames(
8848
- text,
8849
- 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,
8850
10274
  );
8851
10275
  return {
8852
10276
  eventName: update.edited_message ? "telegram.message.updated" : "telegram.message.created",
@@ -8856,12 +10280,17 @@ function normalizeLocalTelegramUpdate(rawUpdate) {
8856
10280
  chatType: String(chat.type || "").trim(),
8857
10281
  chatTitle: firstNonEmptyString([chat.title, chat.username, joinTextParts([chat.first_name, chat.last_name]), chat.id]),
8858
10282
  fromID: String(from.id || "").trim(),
8859
- fromName: firstNonEmptyString([joinTextParts([from.first_name, from.last_name]), from.username, from.id]),
8860
- fromUsername: String(from.username || "").trim(),
8861
- fromIsBot: Boolean(from.is_bot),
8862
- mentionUsernames,
8863
- text,
8864
- 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),
8865
10294
  messageThreadID: String(message.message_thread_id || "").trim(),
8866
10295
  replyToMessageID: intFromRawAllowZero(replyTo.message_id, 0),
8867
10296
  replyToFromName: firstNonEmptyString([joinTextParts([replyToFrom.first_name, replyToFrom.last_name]), replyToFrom.username, replyToFrom.id]),
@@ -8907,6 +10336,7 @@ function formatTelegramInboundArchiveComment(normalized) {
8907
10336
  const archiveSourceRouteKey = String(normalized.archiveSourceRouteKey || normalized.sourceRouteKey || "").trim();
8908
10337
  const archiveSourceBotUsername = String(normalized.archiveSourceBotUsername || normalized.sourceBotUsername || "").trim();
8909
10338
  const archivePayload = buildTelegramArchiveStructuredPayload(normalized);
10339
+ const normalizedAttachments = normalizeTelegramMessageAttachments(normalized.attachments);
8910
10340
  const headerLines = [
8911
10341
  `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
8912
10342
  `chat_id: ${normalized.chatID || "<missing>"}`,
@@ -8929,10 +10359,19 @@ function formatTelegramInboundArchiveComment(normalized) {
8929
10359
  if (normalized.fromUsername) {
8930
10360
  headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
8931
10361
  }
8932
- if (normalized.mentionUsernames.length > 0) {
8933
- headerLines.push(`mention_usernames: ${normalized.mentionUsernames.map((item) => `@${item}`).join(", ")}`);
8934
- }
8935
- 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) {
8936
10375
  headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
8937
10376
  headerLines.push(`reply_to_sender_is_bot: ${normalized.replyToFromIsBot ? "true" : "false"}`);
8938
10377
  if (normalized.replyToFromName) {
@@ -8941,12 +10380,12 @@ function formatTelegramInboundArchiveComment(normalized) {
8941
10380
  if (normalized.replyToFromUsername) {
8942
10381
  headerLines.push(`reply_to_telegram_username: @${normalized.replyToFromUsername.replace(/^@+/, "")}`);
8943
10382
  }
8944
- }
10383
+ }
8945
10384
  if (intFromRawAllowZero(normalized.messageThreadID, 0) > 0) {
8946
10385
  headerLines.push(`message_thread_id: ${intFromRawAllowZero(normalized.messageThreadID, 0)}`);
8947
10386
  }
8948
10387
  headerLines.push(`archive_payload_json: ${JSON.stringify(archivePayload)}`);
8949
- return `${headerLines.join("\n")}\n\n${String(normalized.text || "").trim()}`;
10388
+ return `${headerLines.join("\n")}\n\n${String(normalized.body || normalized.text || "").trim()}`;
8950
10389
  }
8951
10390
 
8952
10391
  async function postJSONWithAuthHeaders(urlText, timeoutSeconds, token, payload, extraHeaders = {}) {
@@ -9905,25 +11344,33 @@ function buildRunnerShowActiveExecutionPayload(activeExecutionStateRaw) {
9905
11344
  };
9906
11345
  }
9907
11346
 
9908
- function buildRunnerShowResolvedContext({
9909
- normalizedRoute,
9910
- diagnostics,
9911
- envConfig,
9912
- resolvedServerBotName,
11347
+ function buildRunnerShowResolvedContext({
11348
+ normalizedRoute,
11349
+ diagnostics,
11350
+ envConfig,
11351
+ resolvedServerBotName,
9913
11352
  botNameSource,
9914
11353
  }) {
9915
- return {
9916
- resolved_server_identity: {
9917
- server_bot_name: resolvedServerBotName,
9918
- server_bot_name_source: botNameSource,
9919
- server_bot_id: normalizedRoute.botID || "-",
9920
- telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
9921
- },
9922
- resolved_destination: {
9923
- destination_label: String(normalizedRoute.destinationLabel || "").trim() || "-",
9924
- destination_id: String(normalizedRoute.destinationID || "").trim() || "-",
9925
- destination_source: "route_config",
9926
- },
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
+ },
9927
11374
  workspace_mapping: {
9928
11375
  workspace_dir: diagnostics.workspaceDir || "-",
9929
11376
  workspace_source: diagnostics.workspaceSource || "-",
@@ -10167,8 +11614,8 @@ async function resolveInformationalQueryReply({
10167
11614
  return lookupOnlyResponse || null;
10168
11615
  }
10169
11616
 
10170
- function buildRunnerExecutionDeps() {
10171
- return {
11617
+ function buildRunnerExecutionDeps() {
11618
+ return {
10172
11619
  adjudicateRunnerStartupLoopWithAI,
10173
11620
  analyzeHumanConversationIntentWithAI,
10174
11621
  auditRoleExecutionPlanWithAI,
@@ -10197,6 +11644,8 @@ function buildRunnerExecutionDeps() {
10197
11644
  createProjectContextItem,
10198
11645
  replaceProjectCtxpackFiles,
10199
11646
  resolveInformationalQueryReply,
11647
+ transcribeRunnerTelegramAudioAttachments,
11648
+ analyzeRunnerTelegramImageAttachments,
10200
11649
  prepareRunnerSelectedRecordRecoveryContext: (input) => prepareRunnerSelectedRecordRecoveryContextImpl({
10201
11650
  ...safeObject(input),
10202
11651
  deps: buildRunnerRecoveryDeps(),
@@ -10204,15 +11653,19 @@ function buildRunnerExecutionDeps() {
10204
11653
  };
10205
11654
  }
10206
11655
 
10207
- function buildRunnerRuntimeDeps() {
10208
- return {
10209
- loadProviderEnvConfig,
10210
- loadBotRunnerState,
10211
- saveBotRunnerState,
10212
- getTelegramBotMe,
10213
- getTelegramWebhookInfo,
10214
- deleteTelegramWebhook,
10215
- 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,
10216
11669
  getTelegramUpdates,
10217
11670
  normalizeLocalTelegramUpdate,
10218
11671
  listThreadComments,
@@ -10555,23 +12008,34 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10555
12008
  runnerConfig,
10556
12009
  buildRunnerExecutionDeps(),
10557
12010
  );
10558
- const destinations = await listProjectChatDestinations({
10559
- siteBaseURL: runtime.baseURL,
10560
- projectID: normalizedRoute.projectID,
10561
- token: runtime.token,
10562
- timeoutSeconds: runtime.timeoutSeconds,
10563
- });
10564
- const destination = selectProjectChatDestination(
10565
- destinations,
10566
- {
10567
- destinationID: normalizedRoute.destinationID,
10568
- destinationLabel: normalizedRoute.destinationLabel,
10569
- },
10570
- normalizedRoute.provider,
10571
- );
10572
- const archiveThread = await discoverArchiveThreadForDestination({
10573
- siteBaseURL: runtime.baseURL,
10574
- 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,
10575
12039
  provider: normalizedRoute.provider,
10576
12040
  destination,
10577
12041
  token: runtime.token,
@@ -10668,9 +12132,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10668
12132
  });
10669
12133
  const followupRequestsForPending = Object.values(requests).filter((entryRaw) => {
10670
12134
  const entry = safeObject(entryRaw);
10671
- const nextExpectedResponders = ensureArray(entry.next_expected_responders)
10672
- .map((value) => normalizeTelegramMentionUsername(value))
10673
- .filter(Boolean);
12135
+ const nextExpectedResponders = runnerRequestPreferredNextExpectedResponders(entry);
10674
12136
  if (!nextExpectedResponders.includes(currentBotSelector)) {
10675
12137
  return false;
10676
12138
  }
@@ -11260,8 +12722,8 @@ function buildRunnerRouteListRows() {
11260
12722
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
11261
12723
  const telegramState = readTelegramEnvState();
11262
12724
  const telegramEntries = ensureArray(telegramState.entries);
11263
- return ensureArray(config.routes).map((rawRoute, index) => {
11264
- const route = normalizeRunnerRoute(rawRoute);
12725
+ return ensureArray(config.routes).map((rawRoute, index) => {
12726
+ const route = normalizeRunnerRoute(rawRoute);
11265
12727
  const matchedTelegramEntry = route.provider === "telegram"
11266
12728
  ? telegramEntries.find((entry) => {
11267
12729
  const current = safeObject(entry);
@@ -11281,24 +12743,25 @@ function buildRunnerRouteListRows() {
11281
12743
  matchedTelegramEntry?.serverBotName,
11282
12744
  "-",
11283
12745
  ]);
11284
- return {
11285
- index: index + 1,
11286
- name: route.name || runnerRouteKey(route),
11287
- logicalSignature: runnerRouteLogicalSignature(route),
11288
- enabled: route.enabled !== false,
11289
- provider: route.provider || "-",
11290
- projectID: route.projectID || "-",
11291
- botName: resolvedBotName,
11292
- botNameSource,
11293
- botID: route.botID || "-",
11294
- role: route.role || "-",
11295
- roleProfile: route.roleProfile || "-",
11296
- destinationLabel: route.destinationLabel || "-",
11297
- pollIntervalMs: route.pollIntervalMs || 0,
11298
- configFilePath: config.filePath,
11299
- };
11300
- });
11301
- }
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
+ }
11302
12765
 
11303
12766
  function slugifyRunnerRouteSegment(rawValue) {
11304
12767
  return String(rawValue || "")
@@ -11308,12 +12771,13 @@ function slugifyRunnerRouteSegment(rawValue) {
11308
12771
  .replace(/^-+|-+$/g, "");
11309
12772
  }
11310
12773
 
11311
- function buildRunnerRouteNameSuggestion({ provider = "", role = "", botName = "" }, existingNames = []) {
11312
- const segments = [
11313
- slugifyRunnerRouteSegment(provider),
11314
- slugifyRunnerRouteSegment(role),
11315
- slugifyRunnerRouteSegment(botName),
11316
- ].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);
11317
12781
  const base = segments.join("-") || "runner-route";
11318
12782
  const existing = new Set(ensureArray(existingNames).map((name) => String(name || "").trim().toLowerCase()).filter(Boolean));
11319
12783
  if (!existing.has(base)) {
@@ -11364,15 +12828,15 @@ function removeRunnerRouteFromConfig(config, routeName) {
11364
12828
  };
11365
12829
  }
11366
12830
 
11367
- function formatRunnerRouteChoiceLabel(route, telegramEntries = []) {
11368
- const normalizedRoute = normalizeRunnerRoute(route);
11369
- const resolvedName = resolveConfiguredRunnerRouteServerBotName(normalizedRoute, telegramEntries);
11370
- const routeName = normalizedRoute.name || runnerRouteKey(normalizedRoute);
11371
- const role = normalizedRoute.role || "-";
11372
- const botName = resolvedName || normalizedRoute.botName || normalizedRoute.botID || "-";
11373
- const destination = normalizedRoute.destinationLabel || normalizedRoute.destinationID || "-";
11374
- return `${routeName}${normalizedRoute.enabled ? "" : " [disabled]"} - ${normalizedRoute.provider || "-"} | ${role} | ${botName} | ${destination}`;
11375
- }
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
+ }
11376
12840
 
11377
12841
  async function selectRunnerManagementRoute(ui, flags, { title = "Select runner route" } = {}) {
11378
12842
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
@@ -11516,15 +12980,16 @@ async function selectRunnerServerBotProfile(ui, { provider, role, flags, current
11516
12980
  token,
11517
12981
  timeoutSeconds,
11518
12982
  });
11519
- const candidates = ensureArray(bots)
11520
- .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);
11521
12985
  if (!candidates.length) {
11522
12986
  throw new Error(`No active ${providerEnvConfig(provider).label} server bot exists for role "${role}".`);
11523
12987
  }
11524
- const resolveChoice = (bot) => ({
11525
- name: String(bot.name || "").trim(),
11526
- id: String(bot.id || "").trim(),
11527
- });
12988
+ const resolveChoice = (bot) => ({
12989
+ name: String(bot.name || "").trim(),
12990
+ id: String(bot.id || "").trim(),
12991
+ username: normalizeTelegramBotUsername(bot.username),
12992
+ });
11528
12993
  if (flaggedBotID) {
11529
12994
  const matched = candidates.find((bot) => bot.id === flaggedBotID);
11530
12995
  if (!matched) {
@@ -11566,18 +13031,28 @@ async function selectRunnerServerBotProfile(ui, { provider, role, flags, current
11566
13031
  return resolveChoice(matched);
11567
13032
  }
11568
13033
 
11569
- async function selectRunnerDestination(ui, { projectID, provider, flags, currentDestinationID = "", currentDestinationLabel = "" }) {
11570
- const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
11571
- const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
11572
- const destinations = await listProjectChatDestinations({
11573
- siteBaseURL,
11574
- projectID,
11575
- token,
11576
- timeoutSeconds,
11577
- });
11578
- const candidates = ensureArray(destinations)
11579
- .map((item) => normalizeChatDestination(item))
11580
- .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);
11581
13056
  const flaggedID = String(flags["destination-id"] || "").trim();
11582
13057
  const flaggedLabel = String(flags["destination-label"] || "").trim();
11583
13058
  if (flaggedID) {
@@ -11621,15 +13096,98 @@ async function selectRunnerDestination(ui, { projectID, provider, flags, current
11621
13096
  if (!matched) {
11622
13097
  throw new Error("selected destination was not found");
11623
13098
  }
11624
- return { id: matched.id, label: matched.label || matched.id };
11625
- }
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
+ }
11626
13183
 
11627
- function buildRunnerRoutePayload({
11628
- currentRoute = null,
11629
- name = "",
11630
- enabled = true,
11631
- projectID = "",
11632
- provider = "",
13184
+ function buildRunnerRoutePayload({
13185
+ currentRoute = null,
13186
+ name = "",
13187
+ enabled = true,
13188
+ routeKind = "",
13189
+ projectID = "",
13190
+ provider = "",
11633
13191
  role = "",
11634
13192
  roleProfile = "",
11635
13193
  serverBotName = "",
@@ -11644,12 +13202,13 @@ function buildRunnerRoutePayload({
11644
13202
  const archivePolicy = currentRoute
11645
13203
  ? safeObject(currentRoute.archivePolicy)
11646
13204
  : defaultRunnerArchivePolicyForRole(roleProfile || role);
11647
- return normalizeRunnerRoute({
11648
- ...(currentRoute ? serializeRunnerRoute(currentRoute) : {}),
11649
- name,
11650
- enabled,
11651
- project_id: projectID,
11652
- 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,
11653
13212
  role,
11654
13213
  role_profile: roleProfile,
11655
13214
  server_bot_name: serverBotName,
@@ -11662,45 +13221,48 @@ function buildRunnerRoutePayload({
11662
13221
  });
11663
13222
  }
11664
13223
 
11665
- async function runRunnerRouteAdd(flags) {
11666
- const ui = createPrompter();
11667
- try {
11668
- if (shouldRenderPromptChrome(flags)) {
11669
- ui.setFlow("RUNNER ROUTE ADD", "Create one executable runner route from server bot and destination");
11670
- }
11671
- const config = loadBotRunnerConfig({ persistIfNeeded: true });
11672
- const provider = await selectRunnerRouteProvider(ui, flags, "");
11673
- const projectID = await resolveRunnerRouteProjectID(ui, flags, config);
11674
- const role = await selectRunnerRole(ui, flags, "");
11675
- const botSelection = await selectRunnerServerBotProfile(ui, { provider, role, flags });
11676
- const destination = await selectRunnerDestination(ui, {
11677
- projectID,
11678
- provider,
11679
- flags,
11680
- });
11681
- const suggestedName = buildRunnerRouteNameSuggestion({
11682
- provider,
11683
- role,
11684
- botName: botSelection.name,
11685
- }, ensureArray(config.routes).map((route) => normalizeRunnerRoute(route).name));
11686
- const routeName = String(flags.name || "").trim() || suggestedName;
11687
- 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;
11688
13249
  const enabled = Object.prototype.hasOwnProperty.call(flags, "enabled")
11689
13250
  ? boolFromRaw(flags.enabled, true)
11690
13251
  : true;
11691
- const nextRoute = buildRunnerRoutePayload({
11692
- name: routeName,
11693
- enabled,
11694
- projectID,
11695
- provider,
11696
- role,
11697
- roleProfile: role,
11698
- serverBotName: botSelection.name,
11699
- serverBotID: botSelection.id,
11700
- destinationID: destination.id,
11701
- destinationLabel: destination.label,
11702
- pollIntervalMs,
11703
- });
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
+ });
11704
13266
  const saved = upsertRunnerRouteConfig(config, nextRoute);
11705
13267
  const filePath = saveBotRunnerConfig(saved, config.filePath);
11706
13268
  if (!String(flags.name || "").trim()) {
@@ -11786,24 +13348,28 @@ async function runRunnerRouteEdit(flags) {
11786
13348
  })
11787
13349
  : { name: currentRoute.botName, id: currentRoute.botID };
11788
13350
 
11789
- const destinationAction = await promptChoice(
11790
- ui,
11791
- "Project chat destination",
11792
- [
11793
- { value: "keep", label: "Keep current destination", description: currentRoute.destinationLabel || currentRoute.destinationID || "-" },
11794
- { value: "change", label: "Change destination", description: "select another active project destination" },
11795
- ],
11796
- { defaultIndex: projectID !== currentRoute.projectID ? 1 : 0 },
11797
- );
11798
- const destination = destinationAction?.value === "change" || projectID !== currentRoute.projectID
11799
- ? await selectRunnerDestination(ui, {
11800
- projectID,
11801
- provider: currentRoute.provider,
11802
- flags: {},
11803
- currentDestinationID: currentRoute.destinationID,
11804
- currentDestinationLabel: currentRoute.destinationLabel,
11805
- })
11806
- : { 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
+ })();
11807
13373
 
11808
13374
  const pollAction = await promptChoice(
11809
13375
  ui,
@@ -11842,12 +13408,13 @@ async function runRunnerRouteEdit(flags) {
11842
13408
  process.stdout.write("Cancelled.\n");
11843
13409
  return;
11844
13410
  }
11845
- const nextRoute = buildRunnerRoutePayload({
11846
- currentRoute,
11847
- name: routeName,
11848
- enabled,
11849
- projectID,
11850
- provider: currentRoute.provider,
13411
+ const nextRoute = buildRunnerRoutePayload({
13412
+ currentRoute,
13413
+ name: routeName,
13414
+ enabled,
13415
+ routeKind: currentRoute.routeKind,
13416
+ projectID,
13417
+ provider: currentRoute.provider,
11851
13418
  role,
11852
13419
  roleProfile: role,
11853
13420
  serverBotName: botSelection.name,
@@ -11911,30 +13478,37 @@ function parseRunnerProjectUpRoles(flags) {
11911
13478
  return ordered;
11912
13479
  }
11913
13480
 
11914
- function runnerProjectUpScopeKey({ projectID, provider, destinationID = "", destinationLabel = "", botIdentity = "" }) {
11915
- return [
11916
- String(projectID || "").trim() || "-",
11917
- String(provider || "").trim() || "-",
11918
- String(destinationID || "").trim() || normalizeRunnerRouteIdentityText(destinationLabel) || "-",
11919
- normalizeRunnerRouteIdentityText(botIdentity) || "-",
11920
- ].join("::");
11921
- }
11922
-
11923
- function routeMatchesRunnerProjectUpScope(routeRaw, scopeRaw) {
11924
- const route = normalizeRunnerRoute(routeRaw);
11925
- const scope = safeObject(scopeRaw);
11926
- if (String(route.projectID || "").trim() !== String(scope.projectID || "").trim()) return false;
11927
- if (String(route.provider || "").trim() !== String(scope.provider || "").trim()) return false;
11928
- const routeDestinationID = String(route.destinationID || "").trim();
11929
- const scopeDestinationID = String(scope.destinationID || "").trim();
11930
- const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
11931
- const scopeDestinationLabel = normalizeRunnerRouteIdentityText(scope.destinationLabel);
11932
- const destinationMatches = scopeDestinationID
11933
- ? (routeDestinationID === scopeDestinationID || (!routeDestinationID && routeDestinationLabel === scopeDestinationLabel))
11934
- : routeDestinationLabel === scopeDestinationLabel;
11935
- if (!destinationMatches) return false;
11936
- const routeBotName = normalizeRunnerRouteIdentityText(route.botName);
11937
- 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);
11938
13512
  const routeBotID = String(route.botID || "").trim();
11939
13513
  const scopeRoleIDs = new Set(
11940
13514
  Object.values(safeObject(scope.serverRoleIDs))
@@ -11947,17 +13521,24 @@ function routeMatchesRunnerProjectUpScope(routeRaw, scopeRaw) {
11947
13521
  );
11948
13522
  }
11949
13523
 
11950
- function resolveRunnerConversationPeers(routeRaw) {
11951
- const normalizedRoute = normalizeRunnerRoute(routeRaw);
11952
- 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 });
11953
13533
  const seen = new Set();
11954
13534
  const peers = [];
11955
13535
  ensureArray(config.routes)
11956
13536
  .map((rawRoute) => normalizeRunnerRoute(rawRoute))
11957
- .filter((route) => route.enabled)
11958
- .filter((route) => String(route.projectID || "").trim() === String(normalizedRoute.projectID || "").trim())
11959
- .filter((route) => String(route.provider || "").trim() === String(normalizedRoute.provider || "").trim())
11960
- .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) => {
11961
13542
  const routeDestinationID = String(route.destinationID || "").trim();
11962
13543
  const targetDestinationID = String(normalizedRoute.destinationID || "").trim();
11963
13544
  const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
@@ -11978,17 +13559,33 @@ function resolveRunnerConversationPeers(routeRaw) {
11978
13559
  return peers;
11979
13560
  }
11980
13561
 
11981
- function resolveRunnerConversationManagedBots(routeRaw, availableBots = []) {
11982
- const normalizedRoute = normalizeRunnerRoute(routeRaw);
11983
- 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 });
11984
13580
  const seen = new Set();
11985
13581
  const managed = [];
11986
13582
  ensureArray(config.routes)
11987
13583
  .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) => {
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) => {
11992
13589
  const routeDestinationID = String(route.destinationID || "").trim();
11993
13590
  const targetDestinationID = String(normalizedRoute.destinationID || "").trim();
11994
13591
  const routeDestinationLabel = normalizeRunnerRouteIdentityText(route.destinationLabel);
@@ -12039,20 +13636,22 @@ function applyRunnerProjectUpRouteSuggestions(routeSuggestions) {
12039
13636
  desiredRoutes.forEach((suggestionRaw) => {
12040
13637
  const suggestion = safeObject(suggestionRaw);
12041
13638
  const routePayload = normalizeRunnerRoute(suggestion.routePayload);
12042
- const scopeKey = runnerProjectUpScopeKey({
12043
- projectID: routePayload.projectID,
12044
- provider: routePayload.provider,
12045
- destinationID: routePayload.destinationID,
12046
- destinationLabel: routePayload.destinationLabel,
12047
- 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,
12048
13646
  });
12049
13647
  if (!scopeGroups.has(scopeKey)) {
12050
13648
  scopeGroups.set(scopeKey, {
12051
- projectID: routePayload.projectID,
12052
- provider: routePayload.provider,
12053
- destinationID: routePayload.destinationID,
12054
- destinationLabel: routePayload.destinationLabel,
12055
- 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,
12056
13655
  serverRoleIDs: { ...safeObject(suggestion.serverRoleIDs) },
12057
13656
  desiredSignatures: new Set(),
12058
13657
  });
@@ -12105,11 +13704,12 @@ function applyRunnerProjectUpRouteSuggestions(routeSuggestions) {
12105
13704
  };
12106
13705
  }
12107
13706
 
12108
- function resolveRunnerProjectUpRoutes({
12109
- projectID,
12110
- provider,
12111
- destinationID,
12112
- destinationLabel,
13707
+ function resolveRunnerProjectUpRoutes({
13708
+ projectID,
13709
+ provider,
13710
+ routeKind = "",
13711
+ destinationID,
13712
+ destinationLabel,
12113
13713
  botName = "",
12114
13714
  botID = "",
12115
13715
  roleFilter = [],
@@ -12117,20 +13717,22 @@ function resolveRunnerProjectUpRoutes({
12117
13717
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
12118
13718
  const telegramEntries = ensureArray(readTelegramEnvState().entries);
12119
13719
  const normalizedRoles = ensureArray(roleFilter).map((value) => normalizeBotRole(value)).filter(Boolean);
12120
- const selectionFlags = {
12121
- "project-id": projectID,
12122
- provider,
12123
- "destination-id": destinationID,
12124
- "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,
12125
13726
  ...(botName ? { "bot-name": botName } : {}),
12126
13727
  ...(botID ? { "bot-id": botID } : {}),
12127
13728
  };
12128
- const selectionRoute = normalizeRunnerRoute({
12129
- project_id: projectID,
12130
- provider,
12131
- destination_id: destinationID,
12132
- destination_label: destinationLabel,
12133
- 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,
12134
13736
  server_bot_id: botID,
12135
13737
  });
12136
13738
  const matched = ensureArray(config.routes)
@@ -12142,10 +13744,12 @@ function resolveRunnerProjectUpRoutes({
12142
13744
  return matched;
12143
13745
  }
12144
13746
  const seenBotDestinations = new Map();
12145
- return matched.filter((route) => {
12146
- const botIdentity = String(route.serverBotID || route.botID || route.botName || route.serverBotName || "").trim().toLowerCase();
12147
- const destIdentity = String(route.destinationID || route.destinationLabel || "").trim().toLowerCase();
12148
- 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}`;
12149
13753
  if (!dedupeKey || dedupeKey === "::") return true;
12150
13754
  if (seenBotDestinations.has(dedupeKey)) return false;
12151
13755
  seenBotDestinations.set(dedupeKey, true);
@@ -12227,6 +13831,95 @@ async function runRunnerProjectUp(flags) {
12227
13831
  });
12228
13832
  }
12229
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
+
12230
13923
  function resolveRunnerProjectTUIRuntimePolicy(flags = {}) {
12231
13924
  const explicitStartRequested = Object.prototype.hasOwnProperty.call(flags, "start");
12232
13925
  return {
@@ -12757,6 +14450,7 @@ function buildRunnerProjectUpNextSteps({
12757
14450
  shouldStartRunner,
12758
14451
  projectID,
12759
14452
  provider,
14453
+ routeKind = "room",
12760
14454
  destinationID,
12761
14455
  matchingRoutes,
12762
14456
  startFlags,
@@ -12764,14 +14458,18 @@ function buildRunnerProjectUpNextSteps({
12764
14458
  const nextSteps = [];
12765
14459
  const routeNames = ensureArray(matchingRoutes).map((route) => normalizeRunnerRoute(route).name).filter(Boolean);
12766
14460
  if (!applyRequested) {
12767
- 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`);
12768
14464
  }
12769
14465
  if (!shouldStartRunner) {
12770
14466
  if (routeNames.length > 0) {
12771
14467
  nextSteps.push(`${CLI_NAME} runner show --route-name ${routeNames[0]}`);
12772
14468
  nextSteps.push(buildRunnerStartDetachedCommand(startFlags));
12773
14469
  } else if (applyRequested) {
12774
- 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`);
12775
14473
  }
12776
14474
  return nextSteps;
12777
14475
  }
@@ -12798,16 +14496,151 @@ async function buildRunnerProjectUpResult(flags = {}) {
12798
14496
  shouldStartRunner,
12799
14497
  foregroundStartRequested,
12800
14498
  } = resolveRunnerProjectUpExecutionPolicy(flags);
12801
- const roleFilter = parseRunnerProjectUpRoles(flags);
12802
- const botNameFilter = String(flags["bot-name"] || "").trim();
12803
- const botIDFilter = String(flags["bot-id"] || "").trim();
12804
- const auditPayload = await buildBotRoomAuditPayload(
12805
- ui,
12806
- {
12807
- ...flags,
12808
- provider,
12809
- apply: false,
12810
- 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,
12811
14644
  },
12812
14645
  buildBotCommandDeps(),
12813
14646
  );
@@ -12839,12 +14672,13 @@ async function buildRunnerProjectUpResult(flags = {}) {
12839
14672
  })
12840
14673
  : null;
12841
14674
  const applyResult = safeObject(applyResultRaw);
12842
- const matchingRoutes = resolveRunnerProjectUpRoutes({
12843
- projectID: String(auditPayload.projectID || "").trim(),
12844
- provider,
12845
- destinationID: String(destination.destinationID || "").trim(),
12846
- destinationLabel: String(destination.destinationLabel || "").trim(),
12847
- 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,
12848
14682
  botID: botIDFilter,
12849
14683
  roleFilter,
12850
14684
  });
@@ -12892,6 +14726,7 @@ async function buildRunnerProjectUpResult(flags = {}) {
12892
14726
  shouldStartRunner,
12893
14727
  projectID: summaryPayload.project_id,
12894
14728
  provider,
14729
+ routeKind: "room",
12895
14730
  destinationID: summaryPayload.destination_id,
12896
14731
  matchingRoutes,
12897
14732
  startFlags,
@@ -12927,9 +14762,9 @@ async function runRunnerList(flags) {
12927
14762
  process.stdout.write(`${JSON.stringify({ ok: true, routes: rows }, null, 2)}\n`);
12928
14763
  return;
12929
14764
  }
12930
- process.stdout.write("Runner routes\n");
12931
- 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");
12932
- 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");
12933
14768
  if (!rows.length) {
12934
14769
  process.stdout.write(" none configured\n");
12935
14770
  return;
@@ -12940,9 +14775,10 @@ async function runRunnerList(flags) {
12940
14775
  [
12941
14776
  ` - ${row.name}${row.enabled ? "" : " [disabled]"}`,
12942
14777
  ` logical_signature: ${row.logicalSignature}`,
12943
- ` provider: ${row.provider}`,
12944
- ` project_id: ${row.projectID}`,
12945
- ` 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}`,
12946
14782
  ` server_bot_name_source: ${row.botNameSource}`,
12947
14783
  ` server_bot_id: ${row.botID}`,
12948
14784
  ` role: ${row.role}`,
@@ -13166,6 +15002,25 @@ function runnerDetachedRouteSetSignature(routes) {
13166
15002
 
13167
15003
  function runnerSchedulingTargetKeyFromRouteKey(routeKey) {
13168
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
+ }
13169
15024
  if (parts.length < 6) {
13170
15025
  return "";
13171
15026
  }
@@ -13261,8 +15116,8 @@ function classifyDetachedRunnerLaunchReuse(registry, routes) {
13261
15116
  ),
13262
15117
  );
13263
15118
  const detail = supersets.length > 1
13264
- ? `multiple detached runners already cover the requested route set for the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")})`
13265
- : `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`;
13266
15121
  return {
13267
15122
  kind: "conflict",
13268
15123
  detail,
@@ -18896,17 +20751,24 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18896
20751
  buildToolAliasMaps,
18897
20752
  rewriteAliasedToolCallToCanonical,
18898
20753
  normalizeBotRunnerConfigContents,
18899
- defaultLocalBotBridgeCommand,
18900
- resolveRunnerExecutionPlan,
18901
- resolveRunnerExecutionPlanForRole,
18902
- normalizeRunnerRoute,
20754
+ defaultLocalBotBridgeCommand,
20755
+ cliName: CLI_NAME,
20756
+ resolveRunnerExecutionPlan,
20757
+ resolveRunnerExecutionPlanForRole,
20758
+ resolveRunnerLocalAIExecutionProfile,
20759
+ normalizeRunnerRoute,
18903
20760
  buildRunnerRouteNameSuggestion,
18904
- buildRunnerRoutePayload,
18905
- upsertRunnerRouteConfig,
18906
- removeRunnerRouteFromConfig,
18907
- acquireRunnerRouteLease,
18908
- buildRunnerExecutionDeps,
18909
- defaultBotRunnerRoleProfiles,
20761
+ buildRunnerRoutePayload,
20762
+ upsertRunnerRouteConfig,
20763
+ removeRunnerRouteFromConfig,
20764
+ acquireRunnerRouteLease,
20765
+ validateRunnerRoute,
20766
+ runnerRouteSchedulingGroupKey,
20767
+ buildRunnerProjectUpDirectMessageRouteSuggestions,
20768
+ resolveRunnerProjectUpRoutes,
20769
+ buildRunnerProjectUpNextSteps,
20770
+ buildRunnerExecutionDeps,
20771
+ defaultBotRunnerRoleProfiles,
18910
20772
  resolveRunnerRoutes,
18911
20773
  runnerRouteKey,
18912
20774
  runnerRouteLogicalSignature,
@@ -18962,6 +20824,9 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
18962
20824
  formatTelegramInboundArchiveComment,
18963
20825
  findArchivedBotReplyRecord,
18964
20826
  parseArchivedChatComment,
20827
+ normalizeLocalTelegramUpdate,
20828
+ normalizeRunnerTelegramMessageEnvelope,
20829
+ normalizeRunnerRecentLocalInboundReceiptEntry,
18965
20830
  intFromRawAllowZero,
18966
20831
  validateWorkspaceArtifacts,
18967
20832
  });
@@ -20013,7 +21878,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
20013
21878
  "detached_runner_blocks_disjoint_same_target_parallel_launches",
20014
21879
  decision.kind === "conflict"
20015
21880
  && ensureArray(decision.overlapping_route_names).length === 0
20016
- && String(decision.detail || "").includes("same project/provider/destination target"),
21881
+ && String(decision.detail || "").includes("same project/provider/route target"),
20017
21882
  `kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
20018
21883
  );
20019
21884
  } catch (err) {