metheus-governance-mcp-cli 0.2.231 → 0.2.232

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  adjudicateRunnerStartupLoopWithAI,
16
16
  analyzeHumanConversationIntentWithAI,
17
17
  auditRoleExecutionPlanWithAI,
18
- auditDirectHumanReplyWithAI,
19
18
  explainExecutionFailureWithAI,
20
19
  normalizeExecutionArtifacts,
21
20
  planRoleExecutionWithAI,
@@ -1915,6 +1914,15 @@ function mergeRunnerStateRecords(preferred, fallback) {
1915
1914
  }
1916
1915
  return allowUndefined ? undefined : 0;
1917
1916
  };
1917
+ const pickArrayField = (key, normalizer = (value) => String(value || "").trim()) => {
1918
+ if (hasOwn(primary, key)) {
1919
+ const value = uniqueOrderedStrings(ensureArray(primary[key]), normalizer).filter(Boolean);
1920
+ if (value.length) {
1921
+ return value;
1922
+ }
1923
+ }
1924
+ return uniqueOrderedStrings(ensureArray(secondary[key]), normalizer).filter(Boolean);
1925
+ };
1918
1926
  return {
1919
1927
  last_processed_comment_id: pickStringField("last_processed_comment_id"),
1920
1928
  last_processed_created_at: pickStringField("last_processed_created_at"),
@@ -1949,6 +1957,31 @@ function mergeRunnerStateRecords(preferred, fallback) {
1949
1957
  last_root_work_item_id: pickStringField("last_root_work_item_id"),
1950
1958
  last_root_work_item_title: pickStringField("last_root_work_item_title"),
1951
1959
  last_root_work_item_status: pickStringField("last_root_work_item_status"),
1960
+ last_contract_validation_status: pickStringField("last_contract_validation_status"),
1961
+ last_contract_validation_reason: pickStringField("last_contract_validation_reason"),
1962
+ last_normalized_execution_contract_type: pickStringField("last_normalized_execution_contract_type"),
1963
+ last_assignment_validation_status: pickStringField("last_assignment_validation_status"),
1964
+ last_assignment_validation_reason: pickStringField("last_assignment_validation_reason"),
1965
+ last_followup_ai_reply_preview: pickStringField("last_followup_ai_reply_preview"),
1966
+ last_followup_execution_contract_type: pickStringField("last_followup_execution_contract_type"),
1967
+ last_followup_normalized_execution_contract_type: pickStringField("last_followup_normalized_execution_contract_type"),
1968
+ last_followup_response_contract_validation_status: pickStringField("last_followup_response_contract_validation_status"),
1969
+ last_followup_response_contract_validation_reason: pickStringField("last_followup_response_contract_validation_reason"),
1970
+ last_followup_assignment_validation_status: pickStringField("last_followup_assignment_validation_status"),
1971
+ last_followup_assignment_validation_reason: pickStringField("last_followup_assignment_validation_reason"),
1972
+ last_followup_delivery_status: pickStringField("last_followup_delivery_status"),
1973
+ last_followup_archive_status: pickStringField("last_followup_archive_status"),
1974
+ last_followup_transport_error: pickStringField("last_followup_transport_error"),
1975
+ last_followup_archive_error: pickStringField("last_followup_archive_error"),
1976
+ last_contract_validation_targets: pickArrayField("last_contract_validation_targets", normalizeTelegramMentionUsername),
1977
+ last_normalized_execution_contract_targets: pickArrayField("last_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
1978
+ last_normalized_execution_next_responders: pickArrayField("last_normalized_execution_next_responders", normalizeTelegramMentionUsername),
1979
+ last_followup_execution_contract_targets: pickArrayField("last_followup_execution_contract_targets", normalizeTelegramMentionUsername),
1980
+ last_followup_next_expected_responders: pickArrayField("last_followup_next_expected_responders", normalizeTelegramMentionUsername),
1981
+ last_followup_normalized_execution_contract_targets: pickArrayField("last_followup_normalized_execution_contract_targets", normalizeTelegramMentionUsername),
1982
+ last_followup_normalized_execution_next_responders: pickArrayField("last_followup_normalized_execution_next_responders", normalizeTelegramMentionUsername),
1983
+ last_followup_response_contract_validation_targets: pickArrayField("last_followup_response_contract_validation_targets", normalizeTelegramMentionUsername),
1984
+ last_followup_assignment_validation_modes: pickArrayField("last_followup_assignment_validation_modes", (value) => String(value || "").trim().toLowerCase()),
1952
1985
  conversation_sessions: {
1953
1986
  ...safeObject(secondary.conversation_sessions),
1954
1987
  ...safeObject(primary.conversation_sessions),
@@ -2443,6 +2476,7 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2443
2476
  ),
2444
2477
  ai_reply_generated_at: firstNonEmptyString([entry.ai_reply_generated_at, entry.aiReplyGeneratedAt]),
2445
2478
  ai_reply_preview: String(entry.ai_reply_preview || entry.aiReplyPreview || "").trim(),
2479
+ followup_ai_reply_preview: String(entry.followup_ai_reply_preview || entry.followupAiReplyPreview || "").trim(),
2446
2480
  root_execution_contract_type: String(entry.root_execution_contract_type || entry.rootExecutionContractType || "").trim().toLowerCase(),
2447
2481
  root_execution_contract_targets: ensureArray(entry.root_execution_contract_targets || entry.rootExecutionContractTargets)
2448
2482
  .map((value) => normalizeTelegramMentionUsername(value))
@@ -2461,15 +2495,43 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2461
2495
  response_contract_validation_targets: ensureArray(entry.response_contract_validation_targets || entry.responseContractValidationTargets)
2462
2496
  .map((value) => normalizeTelegramMentionUsername(value))
2463
2497
  .filter(Boolean),
2498
+ followup_execution_contract_type: String(entry.followup_execution_contract_type || entry.followupExecutionContractType || "").trim().toLowerCase(),
2499
+ followup_execution_contract_targets: ensureArray(entry.followup_execution_contract_targets || entry.followupExecutionContractTargets)
2500
+ .map((value) => normalizeTelegramMentionUsername(value))
2501
+ .filter(Boolean),
2502
+ followup_next_expected_responders: ensureArray(entry.followup_next_expected_responders || entry.followupNextExpectedResponders)
2503
+ .map((value) => normalizeTelegramMentionUsername(value))
2504
+ .filter(Boolean),
2505
+ followup_normalized_execution_contract_type: String(entry.followup_normalized_execution_contract_type || entry.followupNormalizedExecutionContractType || "").trim().toLowerCase(),
2506
+ followup_normalized_execution_contract_targets: ensureArray(entry.followup_normalized_execution_contract_targets || entry.followupNormalizedExecutionContractTargets)
2507
+ .map((value) => normalizeTelegramMentionUsername(value))
2508
+ .filter(Boolean),
2509
+ followup_normalized_execution_next_responders: ensureArray(entry.followup_normalized_execution_next_responders || entry.followupNormalizedExecutionNextResponders)
2510
+ .map((value) => normalizeTelegramMentionUsername(value))
2511
+ .filter(Boolean),
2512
+ followup_response_contract_validation_status: String(entry.followup_response_contract_validation_status || entry.followupResponseContractValidationStatus || "").trim().toLowerCase(),
2513
+ followup_response_contract_validation_reason: String(entry.followup_response_contract_validation_reason || entry.followupResponseContractValidationReason || "").trim(),
2514
+ followup_response_contract_validation_targets: ensureArray(entry.followup_response_contract_validation_targets || entry.followupResponseContractValidationTargets)
2515
+ .map((value) => normalizeTelegramMentionUsername(value))
2516
+ .filter(Boolean),
2464
2517
  assignment_validation_status: String(entry.assignment_validation_status || entry.assignmentValidationStatus || "").trim().toLowerCase(),
2465
2518
  assignment_validation_reason: String(entry.assignment_validation_reason || entry.assignmentValidationReason || "").trim(),
2466
2519
  assignment_validation_modes: ensureArray(entry.assignment_validation_modes || entry.assignmentValidationModes)
2467
2520
  .map((value) => String(value || "").trim().toLowerCase())
2468
2521
  .filter(Boolean),
2522
+ followup_assignment_validation_status: String(entry.followup_assignment_validation_status || entry.followupAssignmentValidationStatus || "").trim().toLowerCase(),
2523
+ followup_assignment_validation_reason: String(entry.followup_assignment_validation_reason || entry.followupAssignmentValidationReason || "").trim(),
2524
+ followup_assignment_validation_modes: ensureArray(entry.followup_assignment_validation_modes || entry.followupAssignmentValidationModes)
2525
+ .map((value) => String(value || "").trim().toLowerCase())
2526
+ .filter(Boolean),
2469
2527
  delivery_status: String(entry.delivery_status || entry.deliveryStatus || "").trim().toLowerCase(),
2470
2528
  archive_status: String(entry.archive_status || entry.archiveStatus || "").trim().toLowerCase(),
2471
2529
  transport_error: String(entry.transport_error || entry.transportError || "").trim(),
2472
2530
  archive_error: String(entry.archive_error || entry.archiveError || "").trim(),
2531
+ followup_delivery_status: String(entry.followup_delivery_status || entry.followupDeliveryStatus || "").trim().toLowerCase(),
2532
+ followup_archive_status: String(entry.followup_archive_status || entry.followupArchiveStatus || "").trim().toLowerCase(),
2533
+ followup_transport_error: String(entry.followup_transport_error || entry.followupTransportError || "").trim(),
2534
+ followup_archive_error: String(entry.followup_archive_error || entry.followupArchiveError || "").trim(),
2473
2535
  normalized_intent: String(entry.normalized_intent || entry.normalizedIntent || "").trim().toLowerCase(),
2474
2536
  status,
2475
2537
  claimed_by_route: String(entry.claimed_by_route || entry.claimedByRoute || "").trim(),
@@ -3051,11 +3113,341 @@ function runnerRequestHasConversationContractData(entryRaw) {
3051
3113
  || ensureArray(entry.conversation_allowed_responders).length
3052
3114
  || entry.conversation_allow_bot_to_bot === true
3053
3115
  || String(entry.conversation_reply_expectation || "").trim()
3054
- || String(entry.execution_contract_type || "").trim()
3055
- || entry.execution_contract_actionable === true
3056
- || ensureArray(entry.execution_contract_targets).length
3057
- || ensureArray(entry.next_expected_responders).length
3116
+ || runnerRequestPreferredExecutionContractType(entry)
3117
+ || runnerRequestPreferredExecutionContractActionable(entry) === true
3118
+ || runnerRequestPreferredExecutionContractTargets(entry).length
3119
+ || runnerRequestPreferredNextExpectedResponders(entry).length
3120
+ );
3121
+ }
3122
+
3123
+ function runnerRequestPreferredExecutionContractType(entryRaw) {
3124
+ const entry = safeObject(entryRaw);
3125
+ return String(
3126
+ entry.execution_contract_type
3127
+ || entry.followup_execution_contract_type
3128
+ || entry.root_execution_contract_type
3129
+ || "",
3130
+ ).trim().toLowerCase();
3131
+ }
3132
+
3133
+ function runnerRequestPreferredExecutionContractActionable(entryRaw) {
3134
+ const entry = safeObject(entryRaw);
3135
+ return entry.execution_contract_actionable === true;
3136
+ }
3137
+
3138
+ function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3139
+ const entry = safeObject(entryRaw);
3140
+ return uniqueOrderedStrings(
3141
+ ensureArray(entry.execution_contract_targets).length
3142
+ ? entry.execution_contract_targets
3143
+ : ensureArray(entry.followup_execution_contract_targets).length
3144
+ ? entry.followup_execution_contract_targets
3145
+ : ensureArray(entry.root_execution_contract_targets).length
3146
+ ? entry.root_execution_contract_targets
3147
+ : [],
3148
+ normalizeTelegramMentionUsername,
3149
+ );
3150
+ }
3151
+
3152
+ function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3153
+ const entry = safeObject(entryRaw);
3154
+ return uniqueOrderedStrings(
3155
+ ensureArray(entry.next_expected_responders).length
3156
+ ? entry.next_expected_responders
3157
+ : ensureArray(entry.followup_next_expected_responders).length
3158
+ ? entry.followup_next_expected_responders
3159
+ : ensureArray(entry.root_next_expected_responders).length
3160
+ ? entry.root_next_expected_responders
3161
+ : [],
3162
+ normalizeTelegramMentionUsername,
3163
+ );
3164
+ }
3165
+
3166
+ function runnerRequestPreferredAIReplyPreview(entryRaw) {
3167
+ const entry = safeObject(entryRaw);
3168
+ return String(
3169
+ entry.ai_reply_preview
3170
+ || entry.followup_ai_reply_preview
3171
+ || entry.root_ai_reply_preview
3172
+ || "",
3173
+ ).trim();
3174
+ }
3175
+
3176
+ function runnerRequestPreferredResponseContractValidationStatus(entryRaw) {
3177
+ const entry = safeObject(entryRaw);
3178
+ return String(
3179
+ entry.response_contract_validation_status
3180
+ || entry.followup_response_contract_validation_status
3181
+ || entry.root_response_contract_validation_status
3182
+ || "",
3183
+ ).trim().toLowerCase();
3184
+ }
3185
+
3186
+ function runnerRequestPreferredResponseContractValidationReason(entryRaw) {
3187
+ const entry = safeObject(entryRaw);
3188
+ return String(
3189
+ entry.response_contract_validation_reason
3190
+ || entry.followup_response_contract_validation_reason
3191
+ || entry.root_response_contract_validation_reason
3192
+ || "",
3193
+ ).trim();
3194
+ }
3195
+
3196
+ function runnerRequestPreferredResponseContractValidationTargets(entryRaw) {
3197
+ const entry = safeObject(entryRaw);
3198
+ return uniqueOrderedStrings(
3199
+ ensureArray(entry.response_contract_validation_targets).length
3200
+ ? entry.response_contract_validation_targets
3201
+ : ensureArray(entry.followup_response_contract_validation_targets).length
3202
+ ? entry.followup_response_contract_validation_targets
3203
+ : ensureArray(entry.root_response_contract_validation_targets).length
3204
+ ? entry.root_response_contract_validation_targets
3205
+ : [],
3206
+ normalizeTelegramMentionUsername,
3207
+ );
3208
+ }
3209
+
3210
+ function runnerRequestPreferredAssignmentValidationStatus(entryRaw) {
3211
+ const entry = safeObject(entryRaw);
3212
+ return String(
3213
+ entry.assignment_validation_status
3214
+ || entry.followup_assignment_validation_status
3215
+ || "",
3216
+ ).trim().toLowerCase();
3217
+ }
3218
+
3219
+ function runnerRequestPreferredAssignmentValidationReason(entryRaw) {
3220
+ const entry = safeObject(entryRaw);
3221
+ return String(
3222
+ entry.assignment_validation_reason
3223
+ || entry.followup_assignment_validation_reason
3224
+ || "",
3225
+ ).trim();
3226
+ }
3227
+
3228
+ function runnerRequestPreferredAssignmentValidationModes(entryRaw) {
3229
+ const entry = safeObject(entryRaw);
3230
+ return uniqueOrderedStrings(
3231
+ ensureArray(entry.assignment_validation_modes).length
3232
+ ? entry.assignment_validation_modes
3233
+ : ensureArray(entry.followup_assignment_validation_modes).length
3234
+ ? entry.followup_assignment_validation_modes
3235
+ : [],
3236
+ (value) => String(value || "").trim().toLowerCase(),
3237
+ );
3238
+ }
3239
+
3240
+ function runnerRequestPreferredDeliveryStatus(entryRaw) {
3241
+ const entry = safeObject(entryRaw);
3242
+ return String(
3243
+ entry.delivery_status
3244
+ || entry.followup_delivery_status
3245
+ || "",
3246
+ ).trim().toLowerCase();
3247
+ }
3248
+
3249
+ function runnerRequestPreferredArchiveStatus(entryRaw) {
3250
+ const entry = safeObject(entryRaw);
3251
+ return String(
3252
+ entry.archive_status
3253
+ || entry.followup_archive_status
3254
+ || "",
3255
+ ).trim().toLowerCase();
3256
+ }
3257
+
3258
+ function runnerRequestPreferredTransportError(entryRaw) {
3259
+ const entry = safeObject(entryRaw);
3260
+ return String(
3261
+ entry.transport_error
3262
+ || entry.followup_transport_error
3263
+ || "",
3264
+ ).trim();
3265
+ }
3266
+
3267
+ function runnerRequestPreferredArchiveError(entryRaw) {
3268
+ const entry = safeObject(entryRaw);
3269
+ return String(
3270
+ entry.archive_error
3271
+ || entry.followup_archive_error
3272
+ || "",
3273
+ ).trim();
3274
+ }
3275
+
3276
+ function buildRunnerValidationAndDeliverySummary({
3277
+ aiReplyPreview = "",
3278
+ executionContractType = "",
3279
+ executionContractTargets = [],
3280
+ nextExpectedResponders = [],
3281
+ responseContractValidationStatus = "",
3282
+ responseContractValidationReason = "",
3283
+ responseContractValidationTargets = [],
3284
+ assignmentValidationStatus = "",
3285
+ assignmentValidationReason = "",
3286
+ assignmentValidationModes = [],
3287
+ deliveryStatus = "",
3288
+ archiveStatus = "",
3289
+ transportError = "",
3290
+ archiveError = "",
3291
+ } = {}) {
3292
+ return {
3293
+ ai_reply_preview: String(aiReplyPreview || "").trim(),
3294
+ execution_contract_type: String(executionContractType || "").trim().toLowerCase(),
3295
+ execution_contract_targets: uniqueOrderedStrings(
3296
+ ensureArray(executionContractTargets),
3297
+ normalizeTelegramMentionUsername,
3298
+ ),
3299
+ next_expected_responders: uniqueOrderedStrings(
3300
+ ensureArray(nextExpectedResponders),
3301
+ normalizeTelegramMentionUsername,
3302
+ ),
3303
+ response_contract_validation_status: String(responseContractValidationStatus || "").trim().toLowerCase(),
3304
+ response_contract_validation_reason: String(responseContractValidationReason || "").trim(),
3305
+ response_contract_validation_targets: uniqueOrderedStrings(
3306
+ ensureArray(responseContractValidationTargets),
3307
+ normalizeTelegramMentionUsername,
3308
+ ),
3309
+ assignment_validation_status: String(assignmentValidationStatus || "").trim().toLowerCase(),
3310
+ assignment_validation_reason: String(assignmentValidationReason || "").trim(),
3311
+ assignment_validation_modes: uniqueOrderedStrings(
3312
+ ensureArray(assignmentValidationModes),
3313
+ (value) => String(value || "").trim().toLowerCase(),
3314
+ ),
3315
+ delivery_status: String(deliveryStatus || "").trim().toLowerCase(),
3316
+ archive_status: String(archiveStatus || "").trim().toLowerCase(),
3317
+ transport_error: String(transportError || "").trim(),
3318
+ archive_error: String(archiveError || "").trim(),
3319
+ };
3320
+ }
3321
+
3322
+ function buildRunnerRequestRootWorkItemSummary(entryRaw) {
3323
+ const entry = safeObject(entryRaw);
3324
+ const rootWorkItemID = String(entry.root_work_item_id || "").trim();
3325
+ if (!rootWorkItemID) {
3326
+ return null;
3327
+ }
3328
+ return {
3329
+ id: rootWorkItemID,
3330
+ title: String(entry.root_work_item_title || "").trim(),
3331
+ status: normalizeRunnerWorkItemStatus(entry.root_work_item_status),
3332
+ thread_id: String(entry.root_thread_id || "").trim(),
3333
+ };
3334
+ }
3335
+
3336
+ function buildRunnerRouteLastResultSummary(routeStateRaw) {
3337
+ const routeState = safeObject(routeStateRaw);
3338
+ const executionContractType = String(
3339
+ routeState.last_followup_execution_contract_type
3340
+ || routeState.last_normalized_execution_contract_type
3341
+ || "",
3342
+ ).trim().toLowerCase();
3343
+ const executionContractTargets = uniqueOrderedStrings(
3344
+ ensureArray(routeState.last_followup_execution_contract_targets).length
3345
+ ? routeState.last_followup_execution_contract_targets
3346
+ : ensureArray(routeState.last_normalized_execution_contract_targets),
3347
+ normalizeTelegramMentionUsername,
3348
+ );
3349
+ const nextExpectedResponders = uniqueOrderedStrings(
3350
+ ensureArray(routeState.last_followup_next_expected_responders).length
3351
+ ? routeState.last_followup_next_expected_responders
3352
+ : ensureArray(routeState.last_normalized_execution_next_responders),
3353
+ normalizeTelegramMentionUsername,
3354
+ );
3355
+ const responseContractValidationStatus = String(
3356
+ routeState.last_followup_response_contract_validation_status
3357
+ || routeState.last_contract_validation_status
3358
+ || "",
3359
+ ).trim().toLowerCase();
3360
+ const responseContractValidationReason = String(
3361
+ routeState.last_followup_response_contract_validation_reason
3362
+ || routeState.last_contract_validation_reason
3363
+ || "",
3364
+ ).trim();
3365
+ const responseContractValidationTargets = uniqueOrderedStrings(
3366
+ ensureArray(routeState.last_followup_response_contract_validation_targets).length
3367
+ ? routeState.last_followup_response_contract_validation_targets
3368
+ : ensureArray(routeState.last_contract_validation_targets),
3369
+ normalizeTelegramMentionUsername,
3370
+ );
3371
+ const assignmentValidationStatus = String(
3372
+ routeState.last_followup_assignment_validation_status
3373
+ || routeState.last_assignment_validation_status
3374
+ || "",
3375
+ ).trim().toLowerCase();
3376
+ const assignmentValidationReason = String(
3377
+ routeState.last_followup_assignment_validation_reason
3378
+ || routeState.last_assignment_validation_reason
3379
+ || "",
3380
+ ).trim();
3381
+ const assignmentValidationModes = uniqueOrderedStrings(
3382
+ ensureArray(routeState.last_followup_assignment_validation_modes),
3383
+ (value) => String(value || "").trim().toLowerCase(),
3058
3384
  );
3385
+ return {
3386
+ action: String(routeState.last_action || "").trim(),
3387
+ reason: String(routeState.last_reason || "").trim(),
3388
+ intent_type: String(routeState.last_intent_type || "").trim(),
3389
+ ...buildRunnerValidationAndDeliverySummary({
3390
+ aiReplyPreview: routeState.last_followup_ai_reply_preview,
3391
+ executionContractType,
3392
+ executionContractTargets,
3393
+ nextExpectedResponders,
3394
+ responseContractValidationStatus,
3395
+ responseContractValidationReason,
3396
+ responseContractValidationTargets,
3397
+ assignmentValidationStatus,
3398
+ assignmentValidationReason,
3399
+ assignmentValidationModes,
3400
+ deliveryStatus: routeState.last_followup_delivery_status,
3401
+ archiveStatus: routeState.last_followup_archive_status,
3402
+ transportError: routeState.last_followup_transport_error,
3403
+ archiveError: routeState.last_followup_archive_error,
3404
+ }),
3405
+ workspace_dir: String(routeState.last_workspace_dir || "").trim(),
3406
+ artifact_validation: String(routeState.last_artifact_validation || "").trim(),
3407
+ artifact_paths: ensureArray(routeState.last_artifact_paths).map((item) => String(item || "").trim()).filter(Boolean),
3408
+ artifact_errors: ensureArray(routeState.last_artifact_errors).map((item) => String(item || "").trim()).filter(Boolean),
3409
+ boundary_violations: ensureArray(routeState.last_boundary_violations).map((item) => safeObject(item)),
3410
+ };
3411
+ }
3412
+
3413
+ function buildRunnerStatusLookupWorkItemSummary({
3414
+ relatedRequest,
3415
+ routeState,
3416
+ currentConversationID,
3417
+ }) {
3418
+ const requestRootWorkItem = buildRunnerRequestRootWorkItemSummary(relatedRequest);
3419
+ const safeRouteState = safeObject(routeState);
3420
+ const routeConversationID = String(safeRouteState.last_conversation_id || "").trim();
3421
+ const activeRootWorkItemID = String(safeRouteState.active_root_work_item_id || "").trim();
3422
+ const activeRootWorkItemTitle = String(safeRouteState.active_root_work_item_title || "").trim();
3423
+ const activeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeRouteState.active_root_work_item_status);
3424
+ const routeRootWorkItemID = String(safeRouteState.last_root_work_item_id || "").trim();
3425
+ const routeRootWorkItemTitle = String(safeRouteState.last_root_work_item_title || "").trim();
3426
+ const routeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeRouteState.last_root_work_item_status);
3427
+ const routeWorkItemIDs = ensureArray(safeRouteState.last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
3428
+ const routeWorkItemTitles = ensureArray(safeRouteState.last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
3429
+ return {
3430
+ root_work_item: requestRootWorkItem
3431
+ || (activeRootWorkItemID
3432
+ ? {
3433
+ id: activeRootWorkItemID,
3434
+ title: activeRootWorkItemTitle,
3435
+ status: activeRootWorkItemStatus,
3436
+ }
3437
+ : currentConversationID && routeConversationID === currentConversationID && routeRootWorkItemID
3438
+ ? {
3439
+ id: routeRootWorkItemID,
3440
+ title: routeRootWorkItemTitle,
3441
+ status: routeRootWorkItemStatus,
3442
+ }
3443
+ : null),
3444
+ route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
3445
+ ? {
3446
+ ids: routeWorkItemIDs,
3447
+ titles: routeWorkItemTitles,
3448
+ }
3449
+ : null,
3450
+ };
3059
3451
  }
3060
3452
 
3061
3453
  function pickRunnerSharedConversationSourceRequest(entries = [], excludeRequestKey = "") {
@@ -3648,28 +4040,26 @@ async function claimRunnerRequestForHumanComment({
3648
4040
  || referencedRequest.conversation_reply_expectation
3649
4041
  || "",
3650
4042
  ).trim().toLowerCase(),
3651
- execution_contract_type: String(
3652
- existing.execution_contract_type || sharedConversationSource.execution_contract_type || referencedRequest.execution_contract_type || "",
3653
- ).trim().toLowerCase(),
3654
- execution_contract_actionable: existing.execution_contract_actionable === true
3655
- || sharedConversationSource.execution_contract_actionable === true
3656
- || referencedRequest.execution_contract_actionable === true,
4043
+ execution_contract_type: runnerRequestPreferredExecutionContractType(existing)
4044
+ || runnerRequestPreferredExecutionContractType(sharedConversationSource)
4045
+ || runnerRequestPreferredExecutionContractType(referencedRequest),
4046
+ execution_contract_actionable: runnerRequestPreferredExecutionContractActionable(existing)
4047
+ || runnerRequestPreferredExecutionContractActionable(sharedConversationSource)
4048
+ || runnerRequestPreferredExecutionContractActionable(referencedRequest),
3657
4049
  execution_contract_targets: uniqueOrderedStrings(
3658
- ensureArray(existing.execution_contract_targets).length
3659
- ? existing.execution_contract_targets
3660
- : ensureArray(sharedConversationSource.execution_contract_targets).length
3661
- ? sharedConversationSource.execution_contract_targets
3662
- : referencedRequest.execution_contract_targets,
4050
+ runnerRequestPreferredExecutionContractTargets(existing).length
4051
+ ? runnerRequestPreferredExecutionContractTargets(existing)
4052
+ : runnerRequestPreferredExecutionContractTargets(sharedConversationSource).length
4053
+ ? runnerRequestPreferredExecutionContractTargets(sharedConversationSource)
4054
+ : runnerRequestPreferredExecutionContractTargets(referencedRequest),
3663
4055
  normalizeTelegramMentionUsername,
3664
4056
  ),
3665
4057
  next_expected_responders: uniqueOrderedStrings(
3666
- ensureArray(existing.next_expected_responders).length
3667
- ? existing.next_expected_responders
3668
- : ensureArray(sharedConversationSource.next_expected_responders).length
3669
- ? sharedConversationSource.next_expected_responders
3670
- : ensureArray(referencedRequest.next_expected_responders).length
3671
- ? referencedRequest.next_expected_responders
3672
- : [],
4058
+ runnerRequestPreferredNextExpectedResponders(existing).length
4059
+ ? runnerRequestPreferredNextExpectedResponders(existing)
4060
+ : runnerRequestPreferredNextExpectedResponders(sharedConversationSource).length
4061
+ ? runnerRequestPreferredNextExpectedResponders(sharedConversationSource)
4062
+ : runnerRequestPreferredNextExpectedResponders(referencedRequest),
3673
4063
  normalizeTelegramMentionUsername,
3674
4064
  ),
3675
4065
  normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
@@ -3718,92 +4108,15 @@ async function claimRunnerRequestForHumanComment({
3718
4108
  };
3719
4109
  }
3720
4110
 
3721
- function isActionableRunnerRequestIntent(rawIntent) {
3722
- const normalizedIntent = String(rawIntent || "").trim().toLowerCase();
3723
- return Boolean(normalizedIntent) && !isInformationalRunnerRequestIntent(normalizedIntent);
3724
- }
3725
-
3726
- function runnerRequestHasConversationContract(requestRaw) {
3727
- const request = safeObject(requestRaw);
3728
- return Boolean(
3729
- String(request.conversation_id || "").trim()
3730
- || String(request.conversation_intent_mode || "").trim()
3731
- || String(request.conversation_reply_expectation || "").trim()
3732
- || ensureArray(request.conversation_participants).length
3733
- || ensureArray(request.conversation_initial_responders).length
3734
- || ensureArray(request.conversation_allowed_responders).length
3735
- );
3736
- }
3737
-
3738
- function runnerRequestHasCompleteConversationContract(requestRaw) {
3739
- const request = safeObject(requestRaw);
3740
- const intentMode = String(request.conversation_intent_mode || "").trim().toLowerCase();
3741
- const conversationID = String(request.conversation_id || "").trim();
3742
- const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3743
- const participants = ensureArray(request.conversation_participants);
3744
- const initialResponders = ensureArray(request.conversation_initial_responders);
3745
- const allowedResponders = ensureArray(request.conversation_allowed_responders);
3746
- if (!runnerRequestHasConversationContract(request)) {
3747
- return false;
3748
- }
3749
- if (!conversationID || !intentMode || !replyExpectation) {
3750
- return false;
3751
- }
3752
- if (intentMode === "single_bot") {
3753
- return allowedResponders.length > 0 || participants.length > 0;
3754
- }
3755
- return participants.length > 0 && initialResponders.length > 0 && allowedResponders.length > 0;
3756
- }
3757
-
3758
- function runnerRequestHasExecutionContract(requestRaw) {
3759
- const request = safeObject(requestRaw);
3760
- return Boolean(
3761
- String(request.execution_contract_type || "").trim()
3762
- || request.execution_contract_actionable === true
3763
- || ensureArray(request.execution_contract_targets).length
3764
- || ensureArray(request.next_expected_responders).length
3765
- );
3766
- }
3767
-
3768
- function runnerRequestHasCompleteExecutionContract(requestRaw) {
3769
- const request = safeObject(requestRaw);
3770
- const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3771
- const executionTargets = ensureArray(request.execution_contract_targets);
3772
- const nextExpectedResponders = ensureArray(request.next_expected_responders);
3773
- if (!runnerRequestHasExecutionContract(request)) {
3774
- return false;
3775
- }
3776
- if (!executionContractType) {
3777
- return false;
3778
- }
3779
- if (executionContractType === "delegation") {
3780
- return executionTargets.length > 0 || nextExpectedResponders.length > 0;
3781
- }
3782
- if (executionContractType === "summary_request") {
3783
- return nextExpectedResponders.length > 0 || executionTargets.length > 0;
3784
- }
3785
- return true;
3786
- }
3787
-
3788
- function runnerRequestHasContractSignals(requestRaw) {
3789
- const request = safeObject(requestRaw);
3790
- return runnerRequestHasConversationContract(request) || runnerRequestHasExecutionContract(request);
3791
- }
3792
-
3793
4111
  function runnerRequestRequiresActionableContract(requestRaw) {
3794
4112
  const request = safeObject(requestRaw);
3795
- const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3796
- const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3797
- const intentMode = String(request.conversation_intent_mode || "").trim().toLowerCase();
3798
- if (request.execution_contract_actionable === true) {
4113
+ const executionContractType = runnerRequestPreferredExecutionContractType(request);
4114
+ if (runnerRequestPreferredExecutionContractActionable(request) === true) {
3799
4115
  return true;
3800
4116
  }
3801
4117
  if (["delegation", "direct_result", "summary_request", "final_summary"].includes(executionContractType)) {
3802
4118
  return true;
3803
4119
  }
3804
- if (intentMode === "single_bot" && replyExpectation === "actionable") {
3805
- return true;
3806
- }
3807
4120
  return false;
3808
4121
  }
3809
4122
 
@@ -3836,13 +4149,9 @@ function truncateRunnerWorkItemTitleText(rawText, maxLength = 96) {
3836
4149
 
3837
4150
  function buildRunnerRootWorkItemTitle({ selectedRecord, request }) {
3838
4151
  const parsed = safeObject(selectedRecord?.parsedArchive);
3839
- const intent = String(safeObject(request).normalized_intent || "").trim().toLowerCase();
4152
+ void request;
3840
4153
  const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 84);
3841
- const prefix = intent === "ctxpack_mutation"
3842
- ? "Ctxpack request"
3843
- : intent === "workitem_mutation"
3844
- ? "Work item request"
3845
- : "Runner request";
4154
+ const prefix = "Runner request";
3846
4155
  if (requestBody) {
3847
4156
  return `${prefix}: ${requestBody}`;
3848
4157
  }
@@ -4290,6 +4599,52 @@ function buildRunnerRootWorkItemTransitionPath(currentStatusRaw, targetStatusRaw
4290
4599
  return [];
4291
4600
  }
4292
4601
 
4602
+ function buildRunnerRouteFollowupSnapshotPatch(selectedRecordRaw, resultRaw = {}) {
4603
+ const parsed = safeObject(safeObject(selectedRecordRaw).parsedArchive);
4604
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
4605
+ if (["telegram_message", "telegram_edited_message"].includes(commentKind)) {
4606
+ return {};
4607
+ }
4608
+ const result = safeObject(resultRaw);
4609
+ return cleanupRunnerStateRecord({
4610
+ last_followup_ai_reply_preview: String(result.ai_reply_preview || "").trim(),
4611
+ last_followup_execution_contract_type: String(result.execution_contract_type || "").trim().toLowerCase(),
4612
+ last_followup_execution_contract_targets: uniqueOrderedStrings(
4613
+ ensureArray(result.execution_contract_targets),
4614
+ normalizeTelegramMentionUsername,
4615
+ ),
4616
+ last_followup_next_expected_responders: uniqueOrderedStrings(
4617
+ ensureArray(result.next_expected_responders),
4618
+ normalizeTelegramMentionUsername,
4619
+ ),
4620
+ last_followup_normalized_execution_contract_type: String(result.normalized_execution_contract_type || "").trim().toLowerCase(),
4621
+ last_followup_normalized_execution_contract_targets: uniqueOrderedStrings(
4622
+ ensureArray(result.normalized_execution_contract_targets),
4623
+ normalizeTelegramMentionUsername,
4624
+ ),
4625
+ last_followup_normalized_execution_next_responders: uniqueOrderedStrings(
4626
+ ensureArray(result.normalized_execution_next_responders),
4627
+ normalizeTelegramMentionUsername,
4628
+ ),
4629
+ last_followup_response_contract_validation_status: String(result.response_contract_validation_status || "").trim().toLowerCase(),
4630
+ last_followup_response_contract_validation_reason: String(result.response_contract_validation_reason || "").trim(),
4631
+ last_followup_response_contract_validation_targets: uniqueOrderedStrings(
4632
+ ensureArray(result.response_contract_validation_targets),
4633
+ normalizeTelegramMentionUsername,
4634
+ ),
4635
+ last_followup_assignment_validation_status: String(result.assignment_validation_status || "").trim().toLowerCase(),
4636
+ last_followup_assignment_validation_reason: String(result.assignment_validation_reason || "").trim(),
4637
+ last_followup_assignment_validation_modes: uniqueOrderedStrings(
4638
+ ensureArray(result.assignment_validation_modes),
4639
+ (value) => String(value || "").trim().toLowerCase(),
4640
+ ),
4641
+ last_followup_delivery_status: String(result.delivery_status || "").trim().toLowerCase(),
4642
+ last_followup_archive_status: String(result.archive_status || "").trim().toLowerCase(),
4643
+ last_followup_transport_error: String(result.transport_error || "").trim(),
4644
+ last_followup_archive_error: String(result.archive_error || "").trim(),
4645
+ });
4646
+ }
4647
+
4293
4648
  function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestRaw, routeKeyHint = "") {
4294
4649
  const currentState = safeObject(currentStateRaw);
4295
4650
  const request = safeObject(requestRaw);
@@ -4334,6 +4689,44 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
4334
4689
  || routeState.last_root_work_item_status,
4335
4690
  );
4336
4691
  }
4692
+ const setFollowupStringPatch = (requestField, routeFields = []) => {
4693
+ if (String(request[requestField] || "").trim()) {
4694
+ return;
4695
+ }
4696
+ const recovered = firstNonEmptyString(routeFields.map((field) => routeState[field]));
4697
+ if (recovered) {
4698
+ patch[requestField] = recovered;
4699
+ }
4700
+ };
4701
+ const setFollowupArrayPatch = (requestField, routeFields = [], normalizer = normalizeTelegramMentionUsername) => {
4702
+ if (ensureArray(request[requestField]).length) {
4703
+ return;
4704
+ }
4705
+ for (const field of ensureArray(routeFields)) {
4706
+ const recovered = uniqueOrderedStrings(ensureArray(routeState[field]), normalizer).filter(Boolean);
4707
+ if (recovered.length) {
4708
+ patch[requestField] = recovered;
4709
+ return;
4710
+ }
4711
+ }
4712
+ };
4713
+ setFollowupStringPatch("followup_ai_reply_preview", ["last_followup_ai_reply_preview"]);
4714
+ setFollowupStringPatch("followup_execution_contract_type", ["last_followup_execution_contract_type"]);
4715
+ setFollowupArrayPatch("followup_execution_contract_targets", ["last_followup_execution_contract_targets"]);
4716
+ setFollowupArrayPatch("followup_next_expected_responders", ["last_followup_next_expected_responders"]);
4717
+ setFollowupStringPatch("followup_normalized_execution_contract_type", ["last_followup_normalized_execution_contract_type", "last_normalized_execution_contract_type"]);
4718
+ setFollowupArrayPatch("followup_normalized_execution_contract_targets", ["last_followup_normalized_execution_contract_targets", "last_normalized_execution_contract_targets"]);
4719
+ setFollowupArrayPatch("followup_normalized_execution_next_responders", ["last_followup_normalized_execution_next_responders", "last_normalized_execution_next_responders"]);
4720
+ setFollowupStringPatch("followup_response_contract_validation_status", ["last_followup_response_contract_validation_status", "last_contract_validation_status"]);
4721
+ setFollowupStringPatch("followup_response_contract_validation_reason", ["last_followup_response_contract_validation_reason", "last_contract_validation_reason"]);
4722
+ setFollowupArrayPatch("followup_response_contract_validation_targets", ["last_followup_response_contract_validation_targets", "last_contract_validation_targets"]);
4723
+ setFollowupStringPatch("followup_assignment_validation_status", ["last_followup_assignment_validation_status", "last_assignment_validation_status"]);
4724
+ setFollowupStringPatch("followup_assignment_validation_reason", ["last_followup_assignment_validation_reason", "last_assignment_validation_reason"]);
4725
+ setFollowupArrayPatch("followup_assignment_validation_modes", ["last_followup_assignment_validation_modes"], (value) => String(value || "").trim().toLowerCase());
4726
+ setFollowupStringPatch("followup_delivery_status", ["last_followup_delivery_status"]);
4727
+ setFollowupStringPatch("followup_archive_status", ["last_followup_archive_status"]);
4728
+ setFollowupStringPatch("followup_transport_error", ["last_followup_transport_error"]);
4729
+ setFollowupStringPatch("followup_archive_error", ["last_followup_archive_error"]);
4337
4730
  const requestStatus = normalizeRunnerRequestStatus(request.status);
4338
4731
  if (!isFinalRunnerRequestStatus(requestStatus)) {
4339
4732
  const routeAction = String(routeState.last_action || "").trim().toLowerCase();
@@ -4693,6 +5086,7 @@ function markRunnerRequestLifecycle({
4693
5086
  const nowISO = new Date().toISOString();
4694
5087
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
4695
5088
  const isRootHumanComment = ["telegram_message", "telegram_edited_message"].includes(commentKind);
5089
+ const isFollowupComment = !isRootHumanComment;
4696
5090
  const patch = {
4697
5091
  conversation_id: conversationID,
4698
5092
  conversation_participants: uniqueOrderedStrings(
@@ -4757,7 +5151,16 @@ function markRunnerRequestLifecycle({
4757
5151
  ai_reply_generated_at: aiReplyGenerated === true
4758
5152
  ? firstNonEmptyString([aiReplyGeneratedAt, existing.ai_reply_generated_at, nowISO])
4759
5153
  : String(existing.ai_reply_generated_at || "").trim(),
4760
- ai_reply_preview: String(aiReplyPreview || existing.ai_reply_preview || "").trim(),
5154
+ ai_reply_preview: String(
5155
+ isRootHumanComment
5156
+ ? aiReplyPreview || existing.ai_reply_preview || ""
5157
+ : existing.ai_reply_preview || "",
5158
+ ).trim(),
5159
+ followup_ai_reply_preview: String(
5160
+ isFollowupComment
5161
+ ? aiReplyPreview || existing.followup_ai_reply_preview || ""
5162
+ : existing.followup_ai_reply_preview || "",
5163
+ ).trim(),
4761
5164
  root_execution_contract_type: String(
4762
5165
  isRootHumanComment
4763
5166
  ? nextExecutionContractType
@@ -4803,36 +5206,146 @@ function markRunnerRequestLifecycle({
4803
5206
  normalizeTelegramMentionUsername,
4804
5207
  ),
4805
5208
  response_contract_validation_status: String(
4806
- responseContractValidationStatus || existing.response_contract_validation_status || "",
5209
+ isRootHumanComment
5210
+ ? responseContractValidationStatus || existing.response_contract_validation_status || ""
5211
+ : existing.response_contract_validation_status || "",
4807
5212
  ).trim().toLowerCase(),
4808
5213
  response_contract_validation_reason: String(
4809
- responseContractValidationReason || existing.response_contract_validation_reason || "",
5214
+ isRootHumanComment
5215
+ ? responseContractValidationReason || existing.response_contract_validation_reason || ""
5216
+ : existing.response_contract_validation_reason || "",
4810
5217
  ).trim(),
4811
5218
  response_contract_validation_targets: uniqueOrderedStrings(
4812
- ensureArray(responseContractValidationTargets).length
5219
+ isRootHumanComment && ensureArray(responseContractValidationTargets).length
4813
5220
  ? responseContractValidationTargets
4814
5221
  : existing.response_contract_validation_targets,
4815
5222
  normalizeTelegramMentionUsername,
4816
5223
  ),
4817
- assignment_validation_status: String(
4818
- assignmentValidationStatus || existing.assignment_validation_status || "",
5224
+ followup_execution_contract_type: String(
5225
+ isFollowupComment
5226
+ ? nextExecutionContractType || existing.followup_execution_contract_type || ""
5227
+ : existing.followup_execution_contract_type || "",
4819
5228
  ).trim().toLowerCase(),
4820
- assignment_validation_reason: String(
4821
- assignmentValidationReason || existing.assignment_validation_reason || "",
4822
- ).trim(),
4823
- assignment_validation_modes: uniqueOrderedStrings(
4824
- ensureArray(assignmentValidationModes).length
4825
- ? assignmentValidationModes
4826
- : existing.assignment_validation_modes,
4827
- (value) => String(value || "").trim().toLowerCase(),
5229
+ followup_execution_contract_targets: uniqueOrderedStrings(
5230
+ isFollowupComment && ensureArray(executionContractTargets).length
5231
+ ? executionContractTargets
5232
+ : existing.followup_execution_contract_targets,
5233
+ normalizeTelegramMentionUsername,
4828
5234
  ),
4829
- delivery_status: String(deliveryStatus || existing.delivery_status || "").trim().toLowerCase(),
4830
- archive_status: String(archiveStatus || existing.archive_status || "").trim().toLowerCase(),
4831
- transport_error: String(transportError || existing.transport_error || "").trim(),
4832
- archive_error: String(archiveError || existing.archive_error || "").trim(),
4833
- normalized_intent: nextNormalizedIntent,
4834
- status: nextStatus,
4835
- started_at: firstNonEmptyString([existing.started_at, nowISO]),
5235
+ followup_next_expected_responders: uniqueOrderedStrings(
5236
+ isFollowupComment && ensureArray(nextExpectedResponders).length
5237
+ ? nextExpectedResponders
5238
+ : existing.followup_next_expected_responders,
5239
+ normalizeTelegramMentionUsername,
5240
+ ),
5241
+ followup_normalized_execution_contract_type: String(
5242
+ isFollowupComment
5243
+ ? normalizedExecutionContractType || existing.followup_normalized_execution_contract_type || ""
5244
+ : existing.followup_normalized_execution_contract_type || "",
5245
+ ).trim().toLowerCase(),
5246
+ followup_normalized_execution_contract_targets: uniqueOrderedStrings(
5247
+ isFollowupComment && ensureArray(normalizedExecutionContractTargets).length
5248
+ ? normalizedExecutionContractTargets
5249
+ : existing.followup_normalized_execution_contract_targets,
5250
+ normalizeTelegramMentionUsername,
5251
+ ),
5252
+ followup_normalized_execution_next_responders: uniqueOrderedStrings(
5253
+ isFollowupComment && ensureArray(normalizedExecutionNextResponders).length
5254
+ ? normalizedExecutionNextResponders
5255
+ : existing.followup_normalized_execution_next_responders,
5256
+ normalizeTelegramMentionUsername,
5257
+ ),
5258
+ followup_response_contract_validation_status: String(
5259
+ isFollowupComment
5260
+ ? responseContractValidationStatus || existing.followup_response_contract_validation_status || ""
5261
+ : existing.followup_response_contract_validation_status || "",
5262
+ ).trim().toLowerCase(),
5263
+ followup_response_contract_validation_reason: String(
5264
+ isFollowupComment
5265
+ ? responseContractValidationReason || existing.followup_response_contract_validation_reason || ""
5266
+ : existing.followup_response_contract_validation_reason || "",
5267
+ ).trim(),
5268
+ followup_response_contract_validation_targets: uniqueOrderedStrings(
5269
+ isFollowupComment && ensureArray(responseContractValidationTargets).length
5270
+ ? responseContractValidationTargets
5271
+ : existing.followup_response_contract_validation_targets,
5272
+ normalizeTelegramMentionUsername,
5273
+ ),
5274
+ assignment_validation_status: String(
5275
+ isRootHumanComment
5276
+ ? assignmentValidationStatus || existing.assignment_validation_status || ""
5277
+ : existing.assignment_validation_status || "",
5278
+ ).trim().toLowerCase(),
5279
+ assignment_validation_reason: String(
5280
+ isRootHumanComment
5281
+ ? assignmentValidationReason || existing.assignment_validation_reason || ""
5282
+ : existing.assignment_validation_reason || "",
5283
+ ).trim(),
5284
+ assignment_validation_modes: uniqueOrderedStrings(
5285
+ isRootHumanComment && ensureArray(assignmentValidationModes).length
5286
+ ? assignmentValidationModes
5287
+ : existing.assignment_validation_modes,
5288
+ (value) => String(value || "").trim().toLowerCase(),
5289
+ ),
5290
+ followup_assignment_validation_status: String(
5291
+ isFollowupComment
5292
+ ? assignmentValidationStatus || existing.followup_assignment_validation_status || ""
5293
+ : existing.followup_assignment_validation_status || "",
5294
+ ).trim().toLowerCase(),
5295
+ followup_assignment_validation_reason: String(
5296
+ isFollowupComment
5297
+ ? assignmentValidationReason || existing.followup_assignment_validation_reason || ""
5298
+ : existing.followup_assignment_validation_reason || "",
5299
+ ).trim(),
5300
+ followup_assignment_validation_modes: uniqueOrderedStrings(
5301
+ isFollowupComment && ensureArray(assignmentValidationModes).length
5302
+ ? assignmentValidationModes
5303
+ : existing.followup_assignment_validation_modes,
5304
+ (value) => String(value || "").trim().toLowerCase(),
5305
+ ),
5306
+ delivery_status: String(
5307
+ isRootHumanComment
5308
+ ? deliveryStatus || existing.delivery_status || ""
5309
+ : existing.delivery_status || "",
5310
+ ).trim().toLowerCase(),
5311
+ archive_status: String(
5312
+ isRootHumanComment
5313
+ ? archiveStatus || existing.archive_status || ""
5314
+ : existing.archive_status || "",
5315
+ ).trim().toLowerCase(),
5316
+ transport_error: String(
5317
+ isRootHumanComment
5318
+ ? transportError || existing.transport_error || ""
5319
+ : existing.transport_error || "",
5320
+ ).trim(),
5321
+ archive_error: String(
5322
+ isRootHumanComment
5323
+ ? archiveError || existing.archive_error || ""
5324
+ : existing.archive_error || "",
5325
+ ).trim(),
5326
+ followup_delivery_status: String(
5327
+ isFollowupComment
5328
+ ? deliveryStatus || existing.followup_delivery_status || ""
5329
+ : existing.followup_delivery_status || "",
5330
+ ).trim().toLowerCase(),
5331
+ followup_archive_status: String(
5332
+ isFollowupComment
5333
+ ? archiveStatus || existing.followup_archive_status || ""
5334
+ : existing.followup_archive_status || "",
5335
+ ).trim().toLowerCase(),
5336
+ followup_transport_error: String(
5337
+ isFollowupComment
5338
+ ? transportError || existing.followup_transport_error || ""
5339
+ : existing.followup_transport_error || "",
5340
+ ).trim(),
5341
+ followup_archive_error: String(
5342
+ isFollowupComment
5343
+ ? archiveError || existing.followup_archive_error || ""
5344
+ : existing.followup_archive_error || "",
5345
+ ).trim(),
5346
+ normalized_intent: nextNormalizedIntent,
5347
+ status: nextStatus,
5348
+ started_at: firstNonEmptyString([existing.started_at, nowISO]),
4836
5349
  completed_at: nextStatus === "completed" ? nowISO : String(existing.completed_at || "").trim(),
4837
5350
  closed_at: (nextStatus === "closed" || nextStatus === "expired" || nextStatus === "loop_closed")
4838
5351
  ? nowISO
@@ -5046,23 +5559,55 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
5046
5559
  preserveLocalStringWhenServerBlank("conversation_summary_bot");
5047
5560
  preserveLocalStringWhenServerBlank("conversation_reply_expectation");
5048
5561
  preserveLocalStringWhenServerBlank("execution_contract_type");
5562
+ preserveLocalStringWhenServerBlank("normalized_execution_contract_type");
5563
+ preserveLocalStringWhenServerBlank("ai_reply_generated_at");
5564
+ preserveLocalStringWhenServerBlank("ai_reply_preview");
5049
5565
  preserveLocalStringWhenServerBlank("root_work_item_id");
5050
5566
  preserveLocalStringWhenServerBlank("root_work_item_title");
5051
5567
  preserveLocalStringWhenServerBlank("root_work_item_status");
5052
5568
  preserveLocalStringWhenServerBlank("root_thread_id");
5053
5569
  preserveLocalStringWhenServerBlank("root_work_item_created_at");
5054
5570
  preserveLocalStringWhenServerBlank("root_work_item_last_error");
5571
+ preserveLocalStringWhenServerBlank("root_execution_contract_type");
5572
+ preserveLocalStringWhenServerBlank("root_ai_reply_preview");
5573
+ preserveLocalStringWhenServerBlank("root_response_contract_validation_status");
5574
+ preserveLocalStringWhenServerBlank("root_response_contract_validation_reason");
5575
+ preserveLocalStringWhenServerBlank("followup_ai_reply_preview");
5576
+ preserveLocalStringWhenServerBlank("followup_execution_contract_type");
5577
+ preserveLocalStringWhenServerBlank("followup_normalized_execution_contract_type");
5578
+ preserveLocalStringWhenServerBlank("followup_response_contract_validation_status");
5579
+ preserveLocalStringWhenServerBlank("followup_response_contract_validation_reason");
5580
+ preserveLocalStringWhenServerBlank("followup_assignment_validation_status");
5581
+ preserveLocalStringWhenServerBlank("followup_assignment_validation_reason");
5582
+ preserveLocalStringWhenServerBlank("followup_delivery_status");
5583
+ preserveLocalStringWhenServerBlank("followup_archive_status");
5584
+ preserveLocalStringWhenServerBlank("followup_transport_error");
5585
+ preserveLocalStringWhenServerBlank("followup_archive_error");
5055
5586
  preserveLocalArrayWhenServerEmpty("conversation_participants");
5056
5587
  preserveLocalArrayWhenServerEmpty("conversation_initial_responders");
5057
5588
  preserveLocalArrayWhenServerEmpty("conversation_allowed_responders");
5058
5589
  preserveLocalArrayWhenServerEmpty("execution_contract_targets");
5059
5590
  preserveLocalArrayWhenServerEmpty("next_expected_responders");
5591
+ preserveLocalArrayWhenServerEmpty("normalized_execution_contract_targets");
5592
+ preserveLocalArrayWhenServerEmpty("normalized_execution_next_responders");
5593
+ preserveLocalArrayWhenServerEmpty("root_execution_contract_targets");
5594
+ preserveLocalArrayWhenServerEmpty("root_next_expected_responders");
5595
+ preserveLocalArrayWhenServerEmpty("root_response_contract_validation_targets");
5596
+ preserveLocalArrayWhenServerEmpty("followup_execution_contract_targets");
5597
+ preserveLocalArrayWhenServerEmpty("followup_next_expected_responders");
5598
+ preserveLocalArrayWhenServerEmpty("followup_normalized_execution_contract_targets");
5599
+ preserveLocalArrayWhenServerEmpty("followup_normalized_execution_next_responders");
5600
+ preserveLocalArrayWhenServerEmpty("followup_response_contract_validation_targets");
5601
+ preserveLocalArrayWhenServerEmpty("followup_assignment_validation_modes");
5060
5602
  if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
5061
5603
  merged.conversation_allow_bot_to_bot = true;
5062
5604
  }
5063
5605
  if (serverEntry.execution_contract_actionable !== true && localEntry.execution_contract_actionable === true) {
5064
5606
  merged.execution_contract_actionable = true;
5065
5607
  }
5608
+ if (serverEntry.ai_reply_generated !== true && localEntry.ai_reply_generated === true) {
5609
+ merged.ai_reply_generated = true;
5610
+ }
5066
5611
  const localStatus = normalizeRunnerRequestStatus(localEntry.status);
5067
5612
  const serverStatus = normalizeRunnerRequestStatus(serverEntry.status);
5068
5613
  if (isFinalRunnerRequestStatus(localStatus) && !isFinalRunnerRequestStatus(serverStatus)) {
@@ -7450,17 +7995,26 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
7450
7995
  updated_at: String(entry.updated_at || "").trim(),
7451
7996
  source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
7452
7997
  last_source_message_id: intFromRawAllowZero(entry.last_source_message_id, 0) || undefined,
7998
+ ...buildRunnerValidationAndDeliverySummary({
7999
+ aiReplyPreview: runnerRequestPreferredAIReplyPreview(entry),
8000
+ executionContractType: runnerRequestPreferredExecutionContractType(entry),
8001
+ executionContractTargets: runnerRequestPreferredExecutionContractTargets(entry),
8002
+ nextExpectedResponders: runnerRequestPreferredNextExpectedResponders(entry),
8003
+ responseContractValidationStatus: runnerRequestPreferredResponseContractValidationStatus(entry),
8004
+ responseContractValidationReason: runnerRequestPreferredResponseContractValidationReason(entry),
8005
+ responseContractValidationTargets: runnerRequestPreferredResponseContractValidationTargets(entry),
8006
+ assignmentValidationStatus: runnerRequestPreferredAssignmentValidationStatus(entry),
8007
+ assignmentValidationReason: runnerRequestPreferredAssignmentValidationReason(entry),
8008
+ assignmentValidationModes: runnerRequestPreferredAssignmentValidationModes(entry),
8009
+ deliveryStatus: runnerRequestPreferredDeliveryStatus(entry),
8010
+ archiveStatus: runnerRequestPreferredArchiveStatus(entry),
8011
+ transportError: runnerRequestPreferredTransportError(entry),
8012
+ archiveError: runnerRequestPreferredArchiveError(entry),
8013
+ }),
7453
8014
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
7454
8015
  .map((value) => normalizeTelegramMentionUsername(value))
7455
8016
  .filter(Boolean),
7456
- root_work_item: String(entry.root_work_item_id || "").trim()
7457
- ? {
7458
- id: String(entry.root_work_item_id || "").trim(),
7459
- title: String(entry.root_work_item_title || "").trim(),
7460
- status: normalizeRunnerWorkItemStatus(entry.root_work_item_status),
7461
- thread_id: String(entry.root_thread_id || "").trim(),
7462
- }
7463
- : null,
8017
+ root_work_item: buildRunnerRequestRootWorkItemSummary(entry),
7464
8018
  };
7465
8019
  }
7466
8020
 
@@ -7519,6 +8073,150 @@ function pickPreferredStatusLookupRequest(entries = []) {
7519
8073
  return candidates[0];
7520
8074
  }
7521
8075
 
8076
+ function resolveRunnerStatusLookupRequests({
8077
+ runnerState,
8078
+ route,
8079
+ routeKey,
8080
+ selfBotUsername,
8081
+ currentMessageID,
8082
+ currentConversationID,
8083
+ currentChatID,
8084
+ activeRequestKey,
8085
+ replyChainContext,
8086
+ }) {
8087
+ const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
8088
+ entry,
8089
+ routeKey,
8090
+ selfBotUsername,
8091
+ currentMessageID,
8092
+ );
8093
+ const referencedRequestCandidate = safeObject(replyChainContext?.referencedRequest);
8094
+ const selectors = currentConversationID
8095
+ ? { conversationID: currentConversationID, chatID: currentChatID }
8096
+ : { chatID: currentChatID };
8097
+ let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
8098
+ if (!scopedRequests.length && currentConversationID) {
8099
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
8100
+ }
8101
+ if (!scopedRequests.length && activeRequestKey) {
8102
+ scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
8103
+ }
8104
+ const statusLookupCandidates = scopedRequests.filter(requestMatchesCurrentRoute);
8105
+ if (
8106
+ Object.keys(referencedRequestCandidate).length > 0
8107
+ && requestMatchesCurrentRoute(referencedRequestCandidate)
8108
+ && !statusLookupCandidates.some(
8109
+ (entry) => String(safeObject(entry).request_key || "").trim() === String(referencedRequestCandidate.request_key || "").trim(),
8110
+ )
8111
+ ) {
8112
+ statusLookupCandidates.push(referencedRequestCandidate);
8113
+ }
8114
+ return {
8115
+ related_active_request: statusLookupCandidates
8116
+ .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null,
8117
+ related_request: pickPreferredStatusLookupRequest(statusLookupCandidates),
8118
+ };
8119
+ }
8120
+
8121
+ function buildRunnerShowLastRunPayload(lastRunSummaryRaw) {
8122
+ const lastRunSummary = safeObject(lastRunSummaryRaw);
8123
+ return {
8124
+ action: lastRunSummary.action || "-",
8125
+ reason: lastRunSummary.reason || "-",
8126
+ intent_type: lastRunSummary.intent_type || "-",
8127
+ ai_reply_preview: lastRunSummary.ai_reply_preview || "-",
8128
+ execution_contract_type: lastRunSummary.execution_contract_type || "-",
8129
+ execution_contract_targets: ensureArray(lastRunSummary.execution_contract_targets),
8130
+ next_expected_responders: ensureArray(lastRunSummary.next_expected_responders),
8131
+ response_contract_validation_status: lastRunSummary.response_contract_validation_status || "-",
8132
+ response_contract_validation_reason: lastRunSummary.response_contract_validation_reason || "-",
8133
+ response_contract_validation_targets: ensureArray(lastRunSummary.response_contract_validation_targets),
8134
+ assignment_validation_status: lastRunSummary.assignment_validation_status || "-",
8135
+ assignment_validation_reason: lastRunSummary.assignment_validation_reason || "-",
8136
+ assignment_validation_modes: ensureArray(lastRunSummary.assignment_validation_modes),
8137
+ delivery_status: lastRunSummary.delivery_status || "-",
8138
+ archive_status: lastRunSummary.archive_status || "-",
8139
+ transport_error: lastRunSummary.transport_error || "-",
8140
+ archive_error: lastRunSummary.archive_error || "-",
8141
+ workspace_dir: lastRunSummary.workspace_dir || "-",
8142
+ artifact_validation: lastRunSummary.artifact_validation || "-",
8143
+ artifact_paths: ensureArray(lastRunSummary.artifact_paths),
8144
+ artifact_errors: ensureArray(lastRunSummary.artifact_errors),
8145
+ boundary_violations: ensureArray(lastRunSummary.boundary_violations),
8146
+ };
8147
+ }
8148
+
8149
+ function buildRunnerShowActiveExecutionPayload(activeExecutionStateRaw) {
8150
+ const activeExecutionState = safeObject(activeExecutionStateRaw);
8151
+ return {
8152
+ active: activeExecutionState.active === true,
8153
+ stale: activeExecutionState.stale === true,
8154
+ stuck: activeExecutionState.stuck === true,
8155
+ comment_id: String(activeExecutionState.commentID || "").trim(),
8156
+ source_message_id: intFromRawAllowZero(activeExecutionState.sourceMessageID, 0),
8157
+ started_at: String(activeExecutionState.startedAt || "").trim(),
8158
+ age_seconds: intFromRawAllowZero(activeExecutionState.ageSeconds, 0),
8159
+ warning: String(activeExecutionState.warning || "").trim(),
8160
+ };
8161
+ }
8162
+
8163
+ function buildRunnerShowResolvedContext({
8164
+ normalizedRoute,
8165
+ diagnostics,
8166
+ envConfig,
8167
+ resolvedServerBotName,
8168
+ botNameSource,
8169
+ }) {
8170
+ return {
8171
+ resolved_server_identity: {
8172
+ server_bot_name: resolvedServerBotName,
8173
+ server_bot_name_source: botNameSource,
8174
+ server_bot_id: normalizedRoute.botID || "-",
8175
+ telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
8176
+ },
8177
+ resolved_destination: {
8178
+ destination_label: String(normalizedRoute.destinationLabel || "").trim() || "-",
8179
+ destination_id: String(normalizedRoute.destinationID || "").trim() || "-",
8180
+ destination_source: "route_config",
8181
+ },
8182
+ workspace_mapping: {
8183
+ workspace_dir: diagnostics.workspaceDir || "-",
8184
+ workspace_source: diagnostics.workspaceSource || "-",
8185
+ },
8186
+ execution_profile: {
8187
+ route_role: normalizedRoute.role || "-",
8188
+ role_profile_name: diagnostics.roleProfileName || "-",
8189
+ client: String(diagnostics.roleProfile?.client || "").trim() || "-",
8190
+ model: String(diagnostics.roleProfile?.model || "").trim() || "-",
8191
+ permission_mode: String(diagnostics.roleProfile?.permissionMode || "").trim() || "-",
8192
+ reasoning_effort: String(diagnostics.roleProfile?.reasoningEffort || "").trim() || "-",
8193
+ },
8194
+ };
8195
+ }
8196
+
8197
+ function buildRunnerStatusReplyChainResolution(replyChainContextRaw) {
8198
+ const replyChainContext = safeObject(replyChainContextRaw);
8199
+ return {
8200
+ reason: String(replyChainContext.reason || "").trim(),
8201
+ reply_to_message_id: intFromRawAllowZero(replyChainContext.replyToMessageID, 0) || undefined,
8202
+ anchor_message_id: intFromRawAllowZero(replyChainContext.anchorMessageID, 0) || undefined,
8203
+ };
8204
+ }
8205
+
8206
+ function buildRunnerStatusActiveExecutionSummary(activeExecutionRaw, selfBusyFiltered = false) {
8207
+ const activeExecution = safeObject(activeExecutionRaw);
8208
+ if (!(activeExecution.active && !selfBusyFiltered)) {
8209
+ return null;
8210
+ }
8211
+ return {
8212
+ started_at: String(activeExecution.startedAt || "").trim(),
8213
+ age_seconds: intFromRawAllowZero(activeExecution.ageSeconds, 0),
8214
+ stale: activeExecution.stale === true,
8215
+ stuck: activeExecution.stuck === true,
8216
+ warning: String(activeExecution.warning || "").trim(),
8217
+ };
8218
+ }
8219
+
7522
8220
  function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runnerStateOverride = null }) {
7523
8221
  const parsed = safeObject(selectedRecord?.parsedArchive);
7524
8222
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
@@ -7548,109 +8246,156 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runne
7548
8246
  const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
7549
8247
  const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
7550
8248
  const activeRequestKey = String(safeObject(routeState).active_request_key || "").trim();
7551
- const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
7552
- entry,
8249
+ const { related_active_request: relatedActiveRequest, related_request: relatedRequest } = resolveRunnerStatusLookupRequests({
8250
+ runnerState,
8251
+ route,
7553
8252
  routeKey,
7554
8253
  selfBotUsername,
7555
8254
  currentMessageID,
7556
- );
7557
- const referencedRequestCandidate = safeObject(replyChainContext.referencedRequest);
7558
- let relatedActiveRequest = null;
7559
- let relatedRequest = null;
7560
- const selectors = currentConversationID
7561
- ? { conversationID: currentConversationID, chatID: currentChatID }
7562
- : { chatID: currentChatID };
7563
- let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
7564
- if (!scopedRequests.length && currentConversationID) {
7565
- scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
7566
- }
7567
- if (!scopedRequests.length && activeRequestKey) {
7568
- scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
7569
- }
7570
- const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
7571
- const statusLookupCandidates = [...eligibleScopedRequests];
7572
- if (
7573
- Object.keys(referencedRequestCandidate).length > 0
7574
- && requestMatchesCurrentRoute(referencedRequestCandidate)
7575
- && !statusLookupCandidates.some(
7576
- (entry) => String(safeObject(entry).request_key || "").trim() === String(referencedRequestCandidate.request_key || "").trim(),
7577
- )
7578
- ) {
7579
- statusLookupCandidates.push(referencedRequestCandidate);
7580
- }
7581
- relatedActiveRequest = statusLookupCandidates
7582
- .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
7583
- relatedRequest = pickPreferredStatusLookupRequest(statusLookupCandidates);
7584
- const lastAction = String(safeObject(routeState).last_action || "").trim();
7585
- const lastReason = String(safeObject(routeState).last_reason || "").trim();
7586
- const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
7587
- const routeConversationID = String(safeObject(routeState).last_conversation_id || "").trim();
7588
- const activeRootWorkItemID = String(safeObject(routeState).active_root_work_item_id || "").trim();
7589
- const activeRootWorkItemTitle = String(safeObject(routeState).active_root_work_item_title || "").trim();
7590
- const activeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).active_root_work_item_status);
7591
- const routeRootWorkItemID = String(safeObject(routeState).last_root_work_item_id || "").trim();
7592
- const routeRootWorkItemTitle = String(safeObject(routeState).last_root_work_item_title || "").trim();
7593
- const routeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).last_root_work_item_status);
7594
- const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
7595
- const routeWorkItemTitles = ensureArray(safeObject(routeState).last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
7596
- const requestRootWorkItem = String(safeObject(relatedRequest).root_work_item_id || "").trim()
7597
- ? {
7598
- id: String(safeObject(relatedRequest).root_work_item_id || "").trim(),
7599
- title: String(safeObject(relatedRequest).root_work_item_title || "").trim(),
7600
- status: normalizeRunnerWorkItemStatus(safeObject(relatedRequest).root_work_item_status),
7601
- thread_id: String(safeObject(relatedRequest).root_thread_id || "").trim(),
7602
- }
7603
- : null;
8255
+ currentConversationID,
8256
+ currentChatID,
8257
+ activeRequestKey,
8258
+ replyChainContext,
8259
+ });
8260
+ const lastRouteResult = buildRunnerRouteLastResultSummary(routeState);
8261
+ const workItemSummary = buildRunnerStatusLookupWorkItemSummary({
8262
+ relatedRequest,
8263
+ routeState,
8264
+ currentConversationID,
8265
+ });
7604
8266
  return {
7605
8267
  kind: "runner_status",
7606
8268
  status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
7607
8269
  ? "running"
7608
8270
  : String(safeObject(relatedRequest).status || "").trim() || "idle",
7609
8271
  resolved_conversation_id: currentConversationID,
7610
- reply_chain_resolution: {
7611
- reason: String(replyChainContext.reason || "").trim(),
7612
- reply_to_message_id: intFromRawAllowZero(replyChainContext.replyToMessageID, 0) || undefined,
7613
- anchor_message_id: intFromRawAllowZero(replyChainContext.anchorMessageID, 0) || undefined,
7614
- },
8272
+ reply_chain_resolution: buildRunnerStatusReplyChainResolution(replyChainContext),
7615
8273
  self_busy_filtered: selfBusyFiltered,
7616
- active_execution: activeExecution.active && !selfBusyFiltered
7617
- ? {
7618
- started_at: String(activeExecution.startedAt || "").trim(),
7619
- age_seconds: intFromRawAllowZero(activeExecution.ageSeconds, 0),
7620
- stale: activeExecution.stale === true,
7621
- stuck: activeExecution.stuck === true,
7622
- warning: String(activeExecution.warning || "").trim(),
7623
- }
7624
- : null,
8274
+ active_execution: buildRunnerStatusActiveExecutionSummary(activeExecution, selfBusyFiltered),
7625
8275
  related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
7626
8276
  related_request: relatedRequest ? summarizeRunnerRequestForStatusLookup(relatedRequest) : null,
7627
- root_work_item: String(safeObject(requestRootWorkItem).id || "").trim()
7628
- ? requestRootWorkItem
7629
- : activeRootWorkItemID
7630
- ? {
7631
- id: activeRootWorkItemID,
7632
- title: activeRootWorkItemTitle,
7633
- status: activeRootWorkItemStatus,
7634
- }
7635
- : currentConversationID && routeConversationID === currentConversationID && routeRootWorkItemID
7636
- ? {
7637
- id: routeRootWorkItemID,
7638
- title: routeRootWorkItemTitle,
7639
- status: routeRootWorkItemStatus,
7640
- }
7641
- : null,
7642
- route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
7643
- ? {
7644
- ids: routeWorkItemIDs,
7645
- titles: routeWorkItemTitles,
7646
- }
7647
- : null,
7648
- last_route_result: {
7649
- action: lastAction,
7650
- reason: lastReason,
7651
- intent_type: lastIntentType,
7652
- },
8277
+ root_work_item: workItemSummary.root_work_item,
8278
+ route_work_items: workItemSummary.route_work_items,
8279
+ last_route_result: lastRouteResult,
8280
+ };
8281
+ }
8282
+
8283
+ function buildProjectWorkspaceLookupResponse({ route, executionPlan, executionOverride }) {
8284
+ const workspace = resolveProjectWorkspaceBindingSummary(
8285
+ route?.projectID,
8286
+ executionPlan?.workspaceDir || route?.workspaceDir || "",
8287
+ );
8288
+ return buildLookupOnlyInformationalReply("project.workspace", {
8289
+ ...workspace,
8290
+ }, executionOverride);
8291
+ }
8292
+
8293
+ function buildSmallTalkLookupResponse({ route, messageText, executionOverride }) {
8294
+ return buildLookupOnlyInformationalReply("small_talk", {
8295
+ intent_type: "small_talk",
8296
+ user_message: messageText,
8297
+ bot_name: firstNonEmptyString([route?.botName, route?.serverBotName, route?.server_bot_name, route?.name]),
8298
+ bot_role: String(route?.role || route?.roleProfile || "").trim(),
8299
+ }, executionOverride);
8300
+ }
8301
+
8302
+ function buildProjectBotRolesLookupResponse({ route, executionOverride }) {
8303
+ const payload = buildProjectBotRolesPayload({
8304
+ projectID: route?.projectID,
8305
+ provider: route?.provider,
8306
+ destinationID: route?.destinationID,
8307
+ destinationLabel: route?.destinationLabel,
8308
+ });
8309
+ return buildLookupOnlyInformationalReply("project.bot_roles", {
8310
+ ...payload,
8311
+ }, executionOverride);
8312
+ }
8313
+
8314
+ async function buildRunnerStatusLookupOnlyResponse({
8315
+ route,
8316
+ routeState,
8317
+ selectedRecord,
8318
+ runtime,
8319
+ executionOverride,
8320
+ }) {
8321
+ let hydratedRunnerState = null;
8322
+ try {
8323
+ hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
8324
+ normalizedRoute: route,
8325
+ runtime,
8326
+ });
8327
+ } catch {}
8328
+ const lookup = buildRunnerStatusQueryLookup({
8329
+ route,
8330
+ routeState,
8331
+ selectedRecord,
8332
+ runnerStateOverride: hydratedRunnerState,
8333
+ });
8334
+ return buildLookupOnlyInformationalReply("runner.status", lookup, executionOverride);
8335
+ }
8336
+
8337
+ async function buildArtifactLocationLookupResponse({
8338
+ route,
8339
+ runtime,
8340
+ messageText,
8341
+ executionOverride,
8342
+ }) {
8343
+ const payload = await locateProjectFilesForQuery({
8344
+ siteBaseURL: runtime.baseURL,
8345
+ projectID: route?.projectID,
8346
+ token: runtime.token,
8347
+ timeoutSeconds: runtime.timeoutSeconds,
8348
+ query: messageText,
8349
+ });
8350
+ return buildLookupOnlyInformationalReply("project.file.locate", {
8351
+ ...payload,
8352
+ }, executionOverride);
8353
+ }
8354
+
8355
+ async function resolveLookupOnlyInformationalReply({
8356
+ normalizedIntentType,
8357
+ route,
8358
+ routeState,
8359
+ selectedRecord,
8360
+ runtime,
8361
+ executionPlan,
8362
+ messageText,
8363
+ executionOverride,
8364
+ }) {
8365
+ const lookupHandlers = {
8366
+ small_talk: () => buildSmallTalkLookupResponse({ route, messageText, executionOverride }),
8367
+ workspace_query: () => buildProjectWorkspaceLookupResponse({ route, executionPlan, executionOverride }),
8368
+ bot_role_query: () => buildProjectBotRolesLookupResponse({ route, executionOverride }),
8369
+ status_query: () => buildRunnerStatusLookupOnlyResponse({
8370
+ route,
8371
+ routeState,
8372
+ selectedRecord,
8373
+ runtime,
8374
+ executionOverride,
8375
+ }),
8376
+ artifact_location_query: () => buildArtifactLocationLookupResponse({
8377
+ route,
8378
+ runtime,
8379
+ messageText,
8380
+ executionOverride,
8381
+ }),
8382
+ };
8383
+ const handler = lookupHandlers[normalizedIntentType];
8384
+ return typeof handler === "function" ? handler() : null;
8385
+ }
8386
+
8387
+ function buildLookupOnlyInformationalReply(source, lookup, executionOverride = null) {
8388
+ const response = {
8389
+ handled: true,
8390
+ source: String(source || "").trim(),
8391
+ response_mode: "lookup_only",
8392
+ reply: "",
8393
+ lookup: safeObject(lookup),
7653
8394
  };
8395
+ if (executionOverride && typeof executionOverride === "object") {
8396
+ response.execution_override = executionOverride;
8397
+ }
8398
+ return response;
7654
8399
  }
7655
8400
 
7656
8401
  async function resolveInformationalQueryReply({
@@ -7664,20 +8409,19 @@ async function resolveInformationalQueryReply({
7664
8409
  const normalizedIntentType = String(intentType || "").trim();
7665
8410
  const messageText = String(safeObject(selectedRecord?.parsedArchive).body || "").trim();
7666
8411
  const executionOverride = buildInformationalMiniExecutionOverride({ route, executionPlan });
8412
+ const lookupOnlyResponse = await resolveLookupOnlyInformationalReply({
8413
+ normalizedIntentType,
8414
+ route,
8415
+ routeState,
8416
+ selectedRecord,
8417
+ runtime,
8418
+ executionPlan,
8419
+ messageText,
8420
+ executionOverride,
8421
+ });
8422
+ return lookupOnlyResponse || null;
7667
8423
  if (normalizedIntentType === "small_talk") {
7668
- return {
7669
- handled: true,
7670
- response_mode: "lookup_only",
7671
- reply: "",
7672
- source: "small_talk",
7673
- lookup: {
7674
- intent_type: "small_talk",
7675
- user_message: messageText,
7676
- bot_name: firstNonEmptyString([route?.botName, route?.serverBotName, route?.server_bot_name, route?.name]),
7677
- bot_role: String(route?.role || route?.roleProfile || "").trim(),
7678
- },
7679
- execution_override: executionOverride,
7680
- };
8424
+ return buildSmallTalkLookupResponse({ route, messageText, executionOverride });
7681
8425
  }
7682
8426
  /* Dead legacy direct small_talk branch kept commented for reference.
7683
8427
  if (false && normalizedIntentType === "small_talk") {
@@ -7689,41 +8433,31 @@ async function resolveInformationalQueryReply({
7689
8433
  }
7690
8434
  */
7691
8435
  if (normalizedIntentType === "workspace_query") {
8436
+ return buildProjectWorkspaceLookupResponse({
8437
+ route,
8438
+ executionPlan,
8439
+ executionOverride,
8440
+ });
7692
8441
  const workspace = resolveProjectWorkspaceBindingSummary(route?.projectID, executionPlan?.workspaceDir || route?.workspaceDir || "");
7693
8442
  const workspaceDir = String(workspace.workspace_dir || "").trim();
7694
8443
  const reply = workspaceDir
7695
8444
  ? `현재 이 프로젝트에 바인딩된 로컬 작업 폴더는 "${workspaceDir}" 입니다.`
7696
8445
  : "현재 이 프로젝트는 project-workspaces.json에 로컬 작업 폴더가 바인딩되어 있지 않습니다.";
7697
- return {
7698
- handled: true,
7699
- response_mode: "lookup_only",
7700
- reply: "",
7701
- source: "project.workspace",
7702
- lookup: {
7703
- ...workspace,
7704
- },
7705
- execution_override: executionOverride,
7706
- };
8446
+ return buildLookupOnlyInformationalReply("project.workspace", {
8447
+ ...workspace,
8448
+ }, executionOverride);
7707
8449
  }
7708
8450
  if (normalizedIntentType === "bot_role_query") {
7709
- const payload = buildProjectBotRolesPayload({
7710
- projectID: route?.projectID,
7711
- provider: route?.provider,
7712
- destinationID: route?.destinationID,
7713
- destinationLabel: route?.destinationLabel,
7714
- });
7715
- return {
7716
- handled: true,
7717
- response_mode: "lookup_only",
7718
- reply: "",
7719
- source: "project.bot_roles",
7720
- lookup: {
7721
- ...payload,
7722
- },
7723
- execution_override: executionOverride,
7724
- };
8451
+ return buildProjectBotRolesLookupResponse({ route, executionOverride });
7725
8452
  }
7726
8453
  if (normalizedIntentType === "status_query") {
8454
+ return buildRunnerStatusLookupOnlyResponse({
8455
+ route,
8456
+ routeState,
8457
+ selectedRecord,
8458
+ runtime,
8459
+ executionOverride,
8460
+ });
7727
8461
  let hydratedRunnerState = null;
7728
8462
  try {
7729
8463
  hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
@@ -7731,18 +8465,13 @@ async function resolveInformationalQueryReply({
7731
8465
  runtime,
7732
8466
  });
7733
8467
  } catch {}
7734
- return {
7735
- handled: true,
7736
- source: "runner.status",
7737
- response_mode: "lookup_only",
7738
- reply: "",
7739
- lookup: buildRunnerStatusQueryLookup({
7740
- route,
7741
- routeState,
7742
- selectedRecord,
7743
- runnerStateOverride: hydratedRunnerState,
7744
- }),
7745
- };
8468
+ const lookup = buildRunnerStatusQueryLookup({
8469
+ route,
8470
+ routeState,
8471
+ selectedRecord,
8472
+ runnerStateOverride: hydratedRunnerState,
8473
+ });
8474
+ return buildLookupOnlyInformationalReply("runner.status", lookup, executionOverride);
7746
8475
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
7747
8476
  if (activeExecution.active) {
7748
8477
  const startedAt = String(activeExecution.startedAt || "").trim();
@@ -7774,23 +8503,12 @@ async function resolveInformationalQueryReply({
7774
8503
  };
7775
8504
  }
7776
8505
  if (normalizedIntentType === "artifact_location_query") {
7777
- const payload = await locateProjectFilesForQuery({
7778
- siteBaseURL: runtime.baseURL,
7779
- projectID: route?.projectID,
7780
- token: runtime.token,
7781
- timeoutSeconds: runtime.timeoutSeconds,
7782
- query: messageText,
8506
+ return buildArtifactLocationLookupResponse({
8507
+ route,
8508
+ runtime,
8509
+ messageText,
8510
+ executionOverride,
7783
8511
  });
7784
- return {
7785
- handled: true,
7786
- response_mode: "lookup_only",
7787
- reply: "",
7788
- source: "project.file.locate",
7789
- lookup: {
7790
- ...payload,
7791
- },
7792
- execution_override: executionOverride,
7793
- };
7794
8512
  }
7795
8513
  return null;
7796
8514
  }
@@ -7801,7 +8519,6 @@ function buildRunnerExecutionDeps() {
7801
8519
  adjudicateRunnerStartupLoopWithAI,
7802
8520
  analyzeHumanConversationIntentWithAI,
7803
8521
  auditRoleExecutionPlanWithAI,
7804
- auditDirectHumanReplyWithAI,
7805
8522
  planRoleExecutionWithAI,
7806
8523
  repairRoleExecutionPlanWithAI,
7807
8524
  normalizeRunnerRoleProfileName,
@@ -8401,6 +9118,74 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8401
9118
  }
8402
9119
  return null;
8403
9120
  };
9121
+ const buildSelectedRecordSkippedRecord = (selectedRecord, reason, extra = {}) => ({
9122
+ id: selectedRecord.id,
9123
+ reason: String(reason || "").trim(),
9124
+ messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
9125
+ ...safeObject(extra),
9126
+ });
9127
+ const saveSelectedRecordSkipState = (selectedRecord, {
9128
+ action,
9129
+ reason,
9130
+ trigger = "",
9131
+ statePatch = {},
9132
+ }) => {
9133
+ saveRunnerRouteState(
9134
+ routeKey,
9135
+ buildRunnerRouteStateFromComment(selectedRecord, {
9136
+ last_action: String(action || "").trim(),
9137
+ last_reason: String(reason || "").trim(),
9138
+ last_trigger: String(trigger || "").trim(),
9139
+ ...safeObject(statePatch),
9140
+ }),
9141
+ );
9142
+ };
9143
+ const pushSelectedRecordSkip = (skippedRecords, selectedRecord, {
9144
+ action,
9145
+ reason,
9146
+ trigger = "",
9147
+ statePatch = {},
9148
+ recordPatch = {},
9149
+ }) => {
9150
+ saveSelectedRecordSkipState(selectedRecord, {
9151
+ action,
9152
+ reason,
9153
+ trigger,
9154
+ statePatch,
9155
+ });
9156
+ skippedRecords.push(
9157
+ buildSelectedRecordSkippedRecord(selectedRecord, reason, recordPatch),
9158
+ );
9159
+ };
9160
+ const finalizeSkippedPendingRecords = async (skippedRecords) => {
9161
+ if (!ensureArray(skippedRecords).length) {
9162
+ return null;
9163
+ }
9164
+ await archiveRunnerDiagnosticComments({
9165
+ comments,
9166
+ skippedRecords,
9167
+ normalizedRoute,
9168
+ bot,
9169
+ archiveThread,
9170
+ runtime,
9171
+ });
9172
+ const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
9173
+ const lastSkipped = skippedRecords[skippedRecords.length - 1];
9174
+ return {
9175
+ route_key: routeKey,
9176
+ route_name: normalizedRoute.name,
9177
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
9178
+ outcome: "skipped",
9179
+ detail: distinctReasons.join("; ") || "all pending messages were skipped",
9180
+ archive_source: String(archiveThread.source || "").trim() || "-",
9181
+ archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
9182
+ thread_id: archiveThread.threadID,
9183
+ comment_id: lastSkipped.id,
9184
+ skipped_count: skippedRecords.length,
9185
+ execution_mode: executionPlan.mode,
9186
+ role_profile: executionPlan.roleProfileName,
9187
+ };
9188
+ };
8404
9189
  const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = [], sharedHumanIntent = null) => {
8405
9190
  const parsed = safeObject(selectedRecord?.parsedArchive);
8406
9191
  const kind = String(parsed.kind || "").trim().toLowerCase();
@@ -8437,316 +9222,122 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8437
9222
  archiveThreadID: archiveThread.threadID,
8438
9223
  });
8439
9224
  };
8440
- if (deferExecution) {
8441
- const skippedRecords = [];
8442
- for (const selectedRecord of pending.pending) {
8443
- const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
8444
- if (duplicateArchivedSkip) {
8445
- saveRunnerRouteState(
8446
- routeKey,
8447
- buildRunnerRouteStateFromComment(selectedRecord, {
8448
- last_action: "duplicate_skipped",
8449
- last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
8450
- last_trigger: "archive_dedupe",
8451
- }),
8452
- );
8453
- skippedRecords.push(duplicateArchivedSkip);
8454
- continue;
8455
- }
8456
- const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
8457
- if (triggerDecision.shouldRespond !== true) {
8458
- saveRunnerRouteState(
8459
- routeKey,
8460
- buildRunnerRouteStateFromComment(selectedRecord, {
8461
- last_action: "trigger_skipped",
8462
- last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8463
- last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
8464
- }),
8465
- );
8466
- skippedRecords.push({
8467
- id: selectedRecord.id,
8468
- reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8469
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8470
- suppressDiagnostic: true,
8471
- });
8472
- continue;
8473
- }
8474
- const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
8475
- routeKey,
8476
- normalizedRoute,
8477
- selectedRecord,
8478
- pendingOrdered: pending.ordered,
8479
- bot,
8480
- managedConversationBots,
8481
- representativeExecutionPlan,
8482
- importOutcome,
8483
- archiveThread,
8484
- runtime,
8485
- suppressDiagnostic: representativeBotSelector
8486
- ? currentBotSelector !== representativeBotSelector
8487
- : false,
8488
- });
8489
- if (startupLoopSkipped) {
8490
- skippedRecords.push(startupLoopSkipped.skippedRecord);
8491
- continue;
8492
- }
8493
- const routingExecutionDeps = {
8494
- ...buildRunnerExecutionDeps(),
8495
- managedConversationBots,
8496
- resolveConversationPeerBots: resolveRunnerConversationPeers,
8497
- };
8498
- const sharedHumanIntentContext = await resolveHumanIntentContext({
8499
- selectedRecord,
8500
- normalizedRoute,
8501
- bot,
8502
- executionPlan,
8503
- deps: routingExecutionDeps,
8504
- });
8505
- if (safeObject(sharedHumanIntentContext).contractNeedsResolution === true) {
8506
- const reason = "human intent contract is incomplete and requires regeneration";
8507
- saveRunnerRouteState(
8508
- routeKey,
8509
- buildRunnerRouteStateFromComment(selectedRecord, {
8510
- last_action: "needs_contract",
8511
- last_reason: reason,
8512
- last_trigger: "human_intent_contract",
8513
- }),
8514
- );
8515
- skippedRecords.push({
8516
- id: selectedRecord.id,
8517
- reason,
8518
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8519
- });
8520
- continue;
8521
- }
8522
- const precomputedAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
8523
- selectedRecord,
8524
- normalizedRoute,
8525
- bot,
8526
- deps: routingExecutionDeps,
8527
- precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8528
- });
8529
- const adjudication = precomputedAdjudication || await resolveRunnerResponderAdjudication({
8530
- selectedRecord,
8531
- pendingOrdered: pending.ordered,
8532
- normalizedRoute,
8533
- bot,
8534
- executionPlan,
8535
- deps: routingExecutionDeps,
8536
- precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8537
- });
8538
- const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
8539
- .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
8540
- .includes(currentBotSelector);
8541
- if (!currentBotSelected) {
8542
- saveRunnerRouteState(
9225
+ const buildContinuationResponderAdjudication = (selectedRecord, requestRaw) => {
9226
+ const request = safeObject(requestRaw);
9227
+ const referencedBotUsernames = ensureArray(safeObject(selectedRecord?.parsedArchive).mentionUsernames)
9228
+ .map((value) => normalizeTelegramMentionUsername(value))
9229
+ .filter(Boolean);
9230
+ const selectedBotUsernames = uniqueOrderedStrings(
9231
+ ensureArray(request.next_expected_responders).length
9232
+ ? request.next_expected_responders
9233
+ : ensureArray(request.conversation_allowed_responders).length
9234
+ ? request.conversation_allowed_responders
9235
+ : ensureArray(request.selected_bot_usernames).length
9236
+ ? request.selected_bot_usernames
9237
+ : request.conversation_initial_responders,
9238
+ normalizeTelegramMentionUsername,
9239
+ );
9240
+ return {
9241
+ decision: selectedBotUsernames.length > 1
9242
+ ? "multiple_responders"
9243
+ : selectedBotUsernames.length === 1
9244
+ ? "single_responder"
9245
+ : "no_responder",
9246
+ selected_bot_usernames: selectedBotUsernames,
9247
+ referenced_bot_usernames: referencedBotUsernames,
9248
+ confidence: "high",
9249
+ reason_code: selectedBotUsernames.length > 0
9250
+ ? "continuation_request_contract"
9251
+ : "continuation_request_contract_no_responder",
9252
+ clarification: "",
9253
+ };
9254
+ };
9255
+ const finalizePreparedRunnerRequest = async (selectedRecord, requestClaim) => {
9256
+ const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
9257
+ normalizedRoute,
9258
+ selectedRecord,
9259
+ runtime,
9260
+ requestKey: requestClaim.requestKey,
9261
+ });
9262
+ const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
9263
+ normalizedRoute,
9264
+ routeKey,
9265
+ selectedRecord,
9266
+ runtime,
9267
+ requestKey: requestClaim.requestKey,
9268
+ });
9269
+ const claimedRequest = safeObject(
9270
+ loadRunnerRequestByKey(requestClaim.requestKey)
9271
+ || rootWorkItemClaim.request
9272
+ || inheritedRootReference.request
9273
+ || requestClaim.request,
9274
+ );
9275
+ const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
9276
+ if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
9277
+ const rootWorkItemFailure = String(
9278
+ rootWorkItemClaim.error
9279
+ || rootWorkItemClaim.reason
9280
+ || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
9281
+ ).trim() || "root_work_item_create_failed";
9282
+ if (String(requestClaim.requestKey || "").trim()) {
9283
+ markRunnerRequestLifecycle({
9284
+ normalizedRoute,
9285
+ requestKey: requestClaim.requestKey,
9286
+ selectedRecord,
8543
9287
  routeKey,
8544
- buildRunnerRouteStateFromComment(selectedRecord, {
8545
- last_action: "adjudication_skipped",
8546
- last_reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8547
- last_trigger: "responder_adjudication",
8548
- }),
8549
- );
8550
- skippedRecords.push({
8551
- id: selectedRecord.id,
8552
- reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8553
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
9288
+ outcome: "closed",
9289
+ closedReason: rootWorkItemFailure,
8554
9290
  });
8555
- continue;
8556
- }
8557
- const requestClaim = await prepareRunnerRequestClaim(
8558
- selectedRecord,
8559
- adjudication.selected_bot_usernames,
8560
- safeObject(sharedHumanIntentContext).humanIntent || null,
8561
- );
8562
- if (!requestClaim.ok) {
8563
9291
  await syncRunnerRequestLedgerForProjectToServer({
8564
9292
  normalizedRoute,
8565
9293
  runtime,
8566
9294
  });
8567
- saveRunnerRouteState(
8568
- routeKey,
8569
- buildRunnerRouteStateFromComment(selectedRecord, {
8570
- last_action: "request_skipped",
8571
- last_reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8572
- last_trigger: "request_ledger",
8573
- }),
8574
- );
8575
- skippedRecords.push({
8576
- id: selectedRecord.id,
8577
- reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8578
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8579
- diagnosticType: "skip",
8580
- contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
8581
- action: "skip_invalid_request_state",
8582
- closedReason: String(requestClaim.reason || "").trim(),
8583
- });
8584
- continue;
8585
9295
  }
8586
- const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
8587
- normalizedRoute,
8588
- selectedRecord,
8589
- runtime,
8590
- requestKey: requestClaim.requestKey,
8591
- });
8592
- const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
8593
- normalizedRoute,
8594
- routeKey,
8595
- selectedRecord,
8596
- runtime,
8597
- requestKey: requestClaim.requestKey,
8598
- });
8599
- const claimedRequest = safeObject(
8600
- loadRunnerRequestByKey(requestClaim.requestKey)
8601
- || rootWorkItemClaim.request
8602
- || inheritedRootReference.request
8603
- || requestClaim.request,
8604
- );
8605
- const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
8606
- if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
8607
- const rootWorkItemFailure = String(
8608
- rootWorkItemClaim.error
8609
- || rootWorkItemClaim.reason
8610
- || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
8611
- ).trim() || "root_work_item_create_failed";
8612
- if (String(requestClaim.requestKey || "").trim()) {
8613
- markRunnerRequestLifecycle({
8614
- normalizedRoute,
8615
- requestKey: requestClaim.requestKey,
8616
- selectedRecord,
8617
- routeKey,
8618
- outcome: "closed",
8619
- closedReason: rootWorkItemFailure,
8620
- });
8621
- await syncRunnerRequestLedgerForProjectToServer({
8622
- normalizedRoute,
8623
- runtime,
8624
- });
8625
- }
8626
- saveRunnerRouteState(
8627
- routeKey,
8628
- buildRunnerRouteStateFromComment(selectedRecord, {
8629
- last_action: "request_skipped",
8630
- last_reason: rootWorkItemFailure,
8631
- last_trigger: "work_item_root",
8632
- last_request_key: String(requestClaim.requestKey || "").trim(),
8633
- }),
8634
- );
8635
- skippedRecords.push({
8636
- id: selectedRecord.id,
8637
- reason: rootWorkItemFailure,
8638
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8639
- diagnosticType: "skip",
8640
- action: "skip_missing_root_work_item",
8641
- closedReason: rootWorkItemFailure,
8642
- });
8643
- continue;
8644
- }
8645
- saveRunnerRouteState(routeKey, {
8646
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
8647
- id: String(claimedRequest.root_work_item_id || "").trim(),
8648
- title: String(claimedRequest.root_work_item_title || "").trim(),
8649
- status: String(claimedRequest.root_work_item_status || "").trim(),
8650
- }),
8651
- last_request_key: String(requestClaim.requestKey || "").trim(),
8652
- last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
8653
- last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
8654
- last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
8655
- });
8656
- await syncRunnerRequestLedgerForProjectToServer({
8657
- normalizedRoute,
8658
- runtime,
8659
- });
8660
9296
  return {
8661
- route_key: routeKey,
8662
- route_name: normalizedRoute.name,
8663
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
8664
- outcome: "accepted",
8665
- detail: `accepted comment ${selectedRecord.id} for background execution`,
8666
- archive_source: String(archiveThread.source || "").trim() || "-",
8667
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
8668
- thread_id: archiveThread.threadID,
8669
- comment_id: selectedRecord.id,
8670
- execution_mode: executionPlan.mode,
8671
- role_profile: executionPlan.roleProfileName,
8672
- deferred_execution: {
8673
- routeKey,
8674
- normalizedRoute,
8675
- routeState: safeObject(loadBotRunnerState().routes[routeKey]),
8676
- selectedRecord,
8677
- pendingOrdered: pending.ordered,
8678
- bot,
8679
- destination,
8680
- archiveThread,
8681
- executionPlan,
8682
- runtime,
8683
- managedConversationBots,
8684
- requestKey: String(requestClaim.requestKey || "").trim(),
8685
- triggerDecision,
8686
- responderAdjudication: adjudication,
8687
- humanIntentContext: sharedHumanIntentContext,
9297
+ ok: false,
9298
+ skip: {
9299
+ kind: "skip",
9300
+ skipAction: "request_skipped",
9301
+ skipReason: rootWorkItemFailure,
9302
+ skipTrigger: "work_item_root",
9303
+ skipStatePatch: {
9304
+ last_request_key: String(requestClaim.requestKey || "").trim(),
9305
+ },
9306
+ skipRecordPatch: {
9307
+ diagnosticType: "skip",
9308
+ action: "skip_missing_root_work_item",
9309
+ closedReason: rootWorkItemFailure,
9310
+ },
8688
9311
  },
8689
9312
  };
8690
9313
  }
8691
- if (skippedRecords.length > 0) {
8692
- await archiveRunnerDiagnosticComments({
8693
- comments,
8694
- skippedRecords,
8695
- normalizedRoute,
8696
- bot,
8697
- archiveThread,
8698
- runtime,
8699
- });
8700
- const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
8701
- const lastSkipped = skippedRecords[skippedRecords.length - 1];
8702
- return {
8703
- route_key: routeKey,
8704
- route_name: normalizedRoute.name,
8705
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
8706
- outcome: "skipped",
8707
- detail: distinctReasons.join("; ") || "all pending messages were skipped",
8708
- archive_source: String(archiveThread.source || "").trim() || "-",
8709
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
8710
- thread_id: archiveThread.threadID,
8711
- comment_id: lastSkipped.id,
8712
- skipped_count: skippedRecords.length,
8713
- execution_mode: executionPlan.mode,
8714
- role_profile: executionPlan.roleProfileName,
8715
- };
8716
- }
8717
- }
8718
- const skippedRecords = [];
8719
- for (const selectedRecord of pending.pending) {
9314
+ return {
9315
+ ok: true,
9316
+ claimedRequest,
9317
+ };
9318
+ };
9319
+ const prepareRunnerSelectedRecordForExecution = async (selectedRecord) => {
8720
9320
  const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
8721
9321
  if (duplicateArchivedSkip) {
8722
- saveRunnerRouteState(
8723
- routeKey,
8724
- buildRunnerRouteStateFromComment(selectedRecord, {
8725
- last_action: "duplicate_skipped",
8726
- last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
8727
- last_trigger: "archive_dedupe",
8728
- }),
8729
- );
8730
- skippedRecords.push(duplicateArchivedSkip);
8731
- continue;
9322
+ return {
9323
+ kind: "skip",
9324
+ skipAction: "duplicate_skipped",
9325
+ skipReason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
9326
+ skipTrigger: "archive_dedupe",
9327
+ skipRecordPatch: duplicateArchivedSkip,
9328
+ };
8732
9329
  }
8733
9330
  const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
8734
9331
  if (triggerDecision.shouldRespond !== true) {
8735
- saveRunnerRouteState(
8736
- routeKey,
8737
- buildRunnerRouteStateFromComment(selectedRecord, {
8738
- last_action: "trigger_skipped",
8739
- last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8740
- last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
8741
- }),
8742
- );
8743
- skippedRecords.push({
8744
- id: selectedRecord.id,
8745
- reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8746
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8747
- suppressDiagnostic: true,
8748
- });
8749
- continue;
9332
+ return {
9333
+ kind: "skip",
9334
+ skipAction: "trigger_skipped",
9335
+ skipReason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
9336
+ skipTrigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
9337
+ skipRecordPatch: {
9338
+ suppressDiagnostic: true,
9339
+ },
9340
+ };
8750
9341
  }
8751
9342
  const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
8752
9343
  routeKey,
@@ -8764,14 +9355,66 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8764
9355
  : false,
8765
9356
  });
8766
9357
  if (startupLoopSkipped) {
8767
- skippedRecords.push(startupLoopSkipped.skippedRecord);
8768
- continue;
9358
+ return {
9359
+ kind: "startup_loop_skipped",
9360
+ skippedRecord: startupLoopSkipped.skippedRecord,
9361
+ };
8769
9362
  }
8770
9363
  const routingExecutionDeps = {
8771
9364
  ...buildRunnerExecutionDeps(),
8772
9365
  managedConversationBots,
8773
9366
  resolveConversationPeerBots: resolveRunnerConversationPeers,
8774
9367
  };
9368
+ const selectedRecordKind = String(safeObject(selectedRecord?.parsedArchive).kind || "").trim().toLowerCase();
9369
+ if (selectedRecordKind === "bot_reply") {
9370
+ const requestClaim = await prepareRunnerRequestClaim(selectedRecord);
9371
+ if (!requestClaim.ok) {
9372
+ await syncRunnerRequestLedgerForProjectToServer({
9373
+ normalizedRoute,
9374
+ runtime,
9375
+ });
9376
+ return {
9377
+ kind: "skip",
9378
+ skipAction: "request_skipped",
9379
+ skipReason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
9380
+ skipTrigger: "request_ledger",
9381
+ skipRecordPatch: {
9382
+ diagnosticType: "skip",
9383
+ contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
9384
+ action: "skip_invalid_request_state",
9385
+ closedReason: String(requestClaim.reason || "").trim(),
9386
+ },
9387
+ };
9388
+ }
9389
+ const continuationAdjudication = buildContinuationResponderAdjudication(
9390
+ selectedRecord,
9391
+ requestClaim.request,
9392
+ );
9393
+ const currentBotSelected = ensureArray(continuationAdjudication.selected_bot_usernames)
9394
+ .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
9395
+ .includes(currentBotSelector);
9396
+ if (!currentBotSelected) {
9397
+ return {
9398
+ kind: "skip",
9399
+ skipAction: "adjudication_skipped",
9400
+ skipReason: String(continuationAdjudication.reason_code || "").trim() || "not_selected_by_request_contract",
9401
+ skipTrigger: "request_contract",
9402
+ };
9403
+ }
9404
+ const finalizedRequest = await finalizePreparedRunnerRequest(selectedRecord, requestClaim);
9405
+ if (!finalizedRequest.ok) {
9406
+ return finalizedRequest.skip;
9407
+ }
9408
+ return {
9409
+ kind: "ready",
9410
+ triggerDecision,
9411
+ routingExecutionDeps,
9412
+ sharedHumanIntentContext: null,
9413
+ adjudication: continuationAdjudication,
9414
+ requestClaim,
9415
+ claimedRequest: finalizedRequest.claimedRequest,
9416
+ };
9417
+ }
8775
9418
  const sharedHumanIntentContext = await resolveHumanIntentContext({
8776
9419
  selectedRecord,
8777
9420
  normalizedRoute,
@@ -8780,30 +9423,31 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8780
9423
  deps: routingExecutionDeps,
8781
9424
  });
8782
9425
  if (safeObject(sharedHumanIntentContext).contractNeedsResolution === true) {
8783
- const reason = "human intent contract is incomplete and requires regeneration";
8784
- saveRunnerRouteState(
8785
- routeKey,
8786
- buildRunnerRouteStateFromComment(selectedRecord, {
8787
- last_action: "needs_contract",
8788
- last_reason: reason,
8789
- last_trigger: "human_intent_contract",
8790
- }),
8791
- );
8792
- skippedRecords.push({
8793
- id: selectedRecord.id,
8794
- reason,
8795
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8796
- });
8797
- continue;
9426
+ return {
9427
+ kind: "skip",
9428
+ skipAction: "needs_contract",
9429
+ skipReason: "human intent contract is incomplete and requires regeneration",
9430
+ skipTrigger: "human_intent_contract",
9431
+ };
8798
9432
  }
8799
- const precomputedInlineAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
9433
+ const precomputedAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
8800
9434
  selectedRecord,
8801
9435
  normalizedRoute,
8802
9436
  bot,
8803
9437
  deps: routingExecutionDeps,
8804
9438
  precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8805
9439
  });
8806
- const inlineAdjudication = precomputedInlineAdjudication || await resolveRunnerResponderAdjudication({
9440
+ const shouldRequireContractDrivenResponderSelection = selectedRecordKind !== "bot_reply"
9441
+ && safeObject(selectedRecord?.parsedArchive).senderIsBot !== true;
9442
+ if (!precomputedAdjudication && shouldRequireContractDrivenResponderSelection) {
9443
+ return {
9444
+ kind: "skip",
9445
+ skipAction: "needs_contract",
9446
+ skipReason: "human intent contract did not select any responder",
9447
+ skipTrigger: "human_intent_contract",
9448
+ };
9449
+ }
9450
+ const adjudication = precomputedAdjudication || await resolveRunnerResponderAdjudication({
8807
9451
  selectedRecord,
8808
9452
  pendingOrdered: pending.ordered,
8809
9453
  normalizedRoute,
@@ -8812,29 +9456,20 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8812
9456
  deps: routingExecutionDeps,
8813
9457
  precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8814
9458
  });
8815
- const inlineCurrentBotSelected = ensureArray(inlineAdjudication.selected_bot_usernames)
9459
+ const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
8816
9460
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
8817
9461
  .includes(currentBotSelector);
8818
- if (!inlineCurrentBotSelected) {
8819
- saveRunnerRouteState(
8820
- routeKey,
8821
- buildRunnerRouteStateFromComment(selectedRecord, {
8822
- last_action: "adjudication_skipped",
8823
- last_reason: String(inlineAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8824
- last_trigger: "responder_adjudication",
8825
- }),
8826
- );
8827
- skippedRecords.push({
8828
- id: selectedRecord.id,
8829
- reason: String(inlineAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8830
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8831
- });
8832
- continue;
9462
+ if (!currentBotSelected) {
9463
+ return {
9464
+ kind: "skip",
9465
+ skipAction: "adjudication_skipped",
9466
+ skipReason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
9467
+ skipTrigger: "responder_adjudication",
9468
+ };
8833
9469
  }
8834
- const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
8835
9470
  const requestClaim = await prepareRunnerRequestClaim(
8836
9471
  selectedRecord,
8837
- inlineAdjudication.selected_bot_usernames,
9472
+ adjudication.selected_bot_usernames,
8838
9473
  safeObject(sharedHumanIntentContext).humanIntent || null,
8839
9474
  );
8840
9475
  if (!requestClaim.ok) {
@@ -8842,84 +9477,136 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8842
9477
  normalizedRoute,
8843
9478
  runtime,
8844
9479
  });
8845
- saveRunnerRouteState(
8846
- routeKey,
8847
- buildRunnerRouteStateFromComment(selectedRecord, {
8848
- last_action: "request_skipped",
8849
- last_reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8850
- last_trigger: "request_ledger",
9480
+ return {
9481
+ kind: "skip",
9482
+ skipAction: "request_skipped",
9483
+ skipReason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
9484
+ skipTrigger: "request_ledger",
9485
+ skipRecordPatch: {
9486
+ diagnosticType: "skip",
9487
+ contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
9488
+ action: "skip_invalid_request_state",
9489
+ closedReason: String(requestClaim.reason || "").trim(),
9490
+ },
9491
+ };
9492
+ }
9493
+ const finalizedRequest = await finalizePreparedRunnerRequest(selectedRecord, requestClaim);
9494
+ if (!finalizedRequest.ok) {
9495
+ return finalizedRequest.skip;
9496
+ }
9497
+ return {
9498
+ kind: "ready",
9499
+ triggerDecision,
9500
+ routingExecutionDeps,
9501
+ sharedHumanIntentContext,
9502
+ adjudication,
9503
+ requestClaim,
9504
+ claimedRequest: finalizedRequest.claimedRequest,
9505
+ };
9506
+ };
9507
+ if (deferExecution) {
9508
+ const skippedRecords = [];
9509
+ for (const selectedRecord of pending.pending) {
9510
+ const preparation = await prepareRunnerSelectedRecordForExecution(selectedRecord);
9511
+ if (preparation.kind === "skip") {
9512
+ pushSelectedRecordSkip(skippedRecords, selectedRecord, {
9513
+ action: preparation.skipAction,
9514
+ reason: preparation.skipReason,
9515
+ trigger: preparation.skipTrigger,
9516
+ statePatch: preparation.skipStatePatch,
9517
+ recordPatch: preparation.skipRecordPatch,
9518
+ });
9519
+ continue;
9520
+ }
9521
+ if (preparation.kind === "startup_loop_skipped") {
9522
+ skippedRecords.push(preparation.skippedRecord);
9523
+ continue;
9524
+ }
9525
+ const {
9526
+ triggerDecision,
9527
+ sharedHumanIntentContext,
9528
+ adjudication,
9529
+ requestClaim,
9530
+ claimedRequest,
9531
+ } = preparation;
9532
+ saveRunnerRouteState(routeKey, {
9533
+ ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
9534
+ id: String(claimedRequest.root_work_item_id || "").trim(),
9535
+ title: String(claimedRequest.root_work_item_title || "").trim(),
9536
+ status: String(claimedRequest.root_work_item_status || "").trim(),
8851
9537
  }),
8852
- );
8853
- skippedRecords.push({
8854
- id: selectedRecord.id,
8855
- reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8856
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8857
- diagnosticType: "skip",
8858
- contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
8859
- action: "skip_invalid_request_state",
8860
- closedReason: String(requestClaim.reason || "").trim(),
9538
+ last_request_key: String(requestClaim.requestKey || "").trim(),
9539
+ last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
9540
+ last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
9541
+ last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
8861
9542
  });
8862
- continue;
8863
- }
8864
- const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
8865
- normalizedRoute,
8866
- selectedRecord,
8867
- runtime,
8868
- requestKey: requestClaim.requestKey,
8869
- });
8870
- const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
8871
- normalizedRoute,
8872
- routeKey,
8873
- selectedRecord,
8874
- runtime,
8875
- requestKey: requestClaim.requestKey,
8876
- });
8877
- const claimedRequest = safeObject(
8878
- loadRunnerRequestByKey(requestClaim.requestKey)
8879
- || rootWorkItemClaim.request
8880
- || inheritedRootReference.request
8881
- || requestClaim.request,
8882
- );
8883
- const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
8884
- if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
8885
- const rootWorkItemFailure = String(
8886
- rootWorkItemClaim.error
8887
- || rootWorkItemClaim.reason
8888
- || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
8889
- ).trim() || "root_work_item_create_failed";
8890
- if (String(requestClaim.requestKey || "").trim()) {
8891
- markRunnerRequestLifecycle({
8892
- normalizedRoute,
8893
- requestKey: requestClaim.requestKey,
8894
- selectedRecord,
9543
+ await syncRunnerRequestLedgerForProjectToServer({
9544
+ normalizedRoute,
9545
+ runtime,
9546
+ });
9547
+ return {
9548
+ route_key: routeKey,
9549
+ route_name: normalizedRoute.name,
9550
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
9551
+ outcome: "accepted",
9552
+ detail: `accepted comment ${selectedRecord.id} for background execution`,
9553
+ archive_source: String(archiveThread.source || "").trim() || "-",
9554
+ archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
9555
+ thread_id: archiveThread.threadID,
9556
+ comment_id: selectedRecord.id,
9557
+ execution_mode: executionPlan.mode,
9558
+ role_profile: executionPlan.roleProfileName,
9559
+ deferred_execution: {
8895
9560
  routeKey,
8896
- outcome: "closed",
8897
- closedReason: rootWorkItemFailure,
8898
- });
8899
- await syncRunnerRequestLedgerForProjectToServer({
8900
9561
  normalizedRoute,
9562
+ routeState: safeObject(loadBotRunnerState().routes[routeKey]),
9563
+ selectedRecord,
9564
+ pendingOrdered: pending.ordered,
9565
+ bot,
9566
+ destination,
9567
+ archiveThread,
9568
+ executionPlan,
8901
9569
  runtime,
8902
- });
9570
+ managedConversationBots,
9571
+ requestKey: String(requestClaim.requestKey || "").trim(),
9572
+ triggerDecision,
9573
+ responderAdjudication: adjudication,
9574
+ humanIntentContext: sharedHumanIntentContext,
9575
+ },
9576
+ };
9577
+ }
9578
+ {
9579
+ const skippedOutcome = await finalizeSkippedPendingRecords(skippedRecords);
9580
+ if (skippedOutcome) {
9581
+ return skippedOutcome;
8903
9582
  }
8904
- saveRunnerRouteState(
8905
- routeKey,
8906
- buildRunnerRouteStateFromComment(selectedRecord, {
8907
- last_action: "request_skipped",
8908
- last_reason: rootWorkItemFailure,
8909
- last_trigger: "work_item_root",
8910
- last_request_key: String(requestClaim.requestKey || "").trim(),
8911
- }),
8912
- );
8913
- skippedRecords.push({
8914
- id: selectedRecord.id,
8915
- reason: rootWorkItemFailure,
8916
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8917
- diagnosticType: "skip",
8918
- action: "skip_missing_root_work_item",
8919
- closedReason: rootWorkItemFailure,
9583
+ }
9584
+ }
9585
+ const skippedRecords = [];
9586
+ for (const selectedRecord of pending.pending) {
9587
+ const preparation = await prepareRunnerSelectedRecordForExecution(selectedRecord);
9588
+ if (preparation.kind === "skip") {
9589
+ pushSelectedRecordSkip(skippedRecords, selectedRecord, {
9590
+ action: preparation.skipAction,
9591
+ reason: preparation.skipReason,
9592
+ trigger: preparation.skipTrigger,
9593
+ statePatch: preparation.skipStatePatch,
9594
+ recordPatch: preparation.skipRecordPatch,
8920
9595
  });
8921
9596
  continue;
8922
9597
  }
9598
+ if (preparation.kind === "startup_loop_skipped") {
9599
+ skippedRecords.push(preparation.skippedRecord);
9600
+ continue;
9601
+ }
9602
+ const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
9603
+ const {
9604
+ triggerDecision,
9605
+ sharedHumanIntentContext,
9606
+ adjudication: inlineAdjudication,
9607
+ requestClaim,
9608
+ claimedRequest,
9609
+ } = preparation;
8923
9610
  saveRunnerRouteState(routeKey, {
8924
9611
  ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
8925
9612
  id: String(claimedRequest.root_work_item_id || "").trim(),
@@ -9106,31 +9793,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
9106
9793
  };
9107
9794
  }
9108
9795
 
9109
- if (skippedRecords.length > 0) {
9110
- await archiveRunnerDiagnosticComments({
9111
- comments,
9112
- skippedRecords,
9113
- normalizedRoute,
9114
- bot,
9115
- archiveThread,
9116
- runtime,
9117
- });
9118
- const lastSkipped = skippedRecords[skippedRecords.length - 1];
9119
- const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
9120
- return {
9121
- route_key: routeKey,
9122
- route_name: normalizedRoute.name,
9123
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
9124
- outcome: "skipped",
9125
- detail: distinctReasons.join("; ") || "all pending messages were skipped",
9126
- archive_source: String(archiveThread.source || "").trim() || "-",
9127
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
9128
- thread_id: archiveThread.threadID,
9129
- comment_id: lastSkipped.id,
9130
- skipped_count: skippedRecords.length,
9131
- execution_mode: executionPlan.mode,
9132
- role_profile: executionPlan.roleProfileName,
9133
- };
9796
+ {
9797
+ const skippedOutcome = await finalizeSkippedPendingRecords(skippedRecords);
9798
+ if (skippedOutcome) {
9799
+ return skippedOutcome;
9800
+ }
9134
9801
  }
9135
9802
 
9136
9803
  return {
@@ -10353,70 +11020,40 @@ function buildRunnerShowPayload(route, flags = {}) {
10353
11020
  const resolvedServerBotName = firstNonEmptyString([
10354
11021
  normalizedRoute.botName,
10355
11022
  matchedTelegramEntry?.serverBotName,
10356
- "-",
10357
- ]);
10358
- const envConfig = normalizedRoute.provider
10359
- ? loadProviderEnvConfig(normalizedRoute.provider, {
10360
- botID: normalizedRoute.botID,
10361
- botName: resolvedServerBotName !== "-" ? resolvedServerBotName : "",
10362
- route: normalizedRoute,
10363
- })
10364
- : null;
10365
- return {
10366
- ok: diagnostics.errors.length === 0,
10367
- route_name: normalizedRoute.name || runnerRouteKey(normalizedRoute),
10368
- route_key: runnerRouteKey(normalizedRoute),
10369
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
10370
- route_config_file: runnerConfig.filePath,
10371
- workspace_registry_file: String(runnerConfig.workspaceRegistryFilePath || botRunnerWorkspaceRegistryFilePath()).trim() || "-",
10372
- route_config: serializeRunnerRoute(normalizedRoute),
10373
- resolved_server_identity: {
10374
- server_bot_name: resolvedServerBotName,
10375
- server_bot_name_source: botNameSource,
10376
- server_bot_id: normalizedRoute.botID || "-",
10377
- telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
10378
- },
10379
- resolved_destination: {
10380
- destination_label: String(normalizedRoute.destinationLabel || "").trim() || "-",
10381
- destination_id: String(normalizedRoute.destinationID || "").trim() || "-",
10382
- destination_source: "route_config",
10383
- },
10384
- workspace_mapping: {
10385
- workspace_dir: diagnostics.workspaceDir || "-",
10386
- workspace_source: diagnostics.workspaceSource || "-",
10387
- },
10388
- execution_profile: {
10389
- route_role: normalizedRoute.role || "-",
10390
- role_profile_name: diagnostics.roleProfileName || "-",
10391
- client: String(diagnostics.roleProfile?.client || "").trim() || "-",
10392
- model: String(diagnostics.roleProfile?.model || "").trim() || "-",
10393
- permission_mode: String(diagnostics.roleProfile?.permissionMode || "").trim() || "-",
10394
- reasoning_effort: String(diagnostics.roleProfile?.reasoningEffort || "").trim() || "-",
10395
- },
10396
- last_run: {
10397
- action: String(routeState.last_action || "").trim() || "-",
10398
- reason: String(routeState.last_reason || "").trim() || "-",
10399
- intent_type: String(routeState.last_intent_type || "").trim() || "-",
10400
- workspace_dir: String(routeState.last_workspace_dir || "").trim() || "-",
10401
- artifact_validation: String(routeState.last_artifact_validation || "").trim() || "-",
10402
- artifact_paths: ensureArray(routeState.last_artifact_paths).map((item) => String(item || "").trim()).filter(Boolean),
10403
- artifact_errors: ensureArray(routeState.last_artifact_errors).map((item) => String(item || "").trim()).filter(Boolean),
10404
- boundary_violations: ensureArray(routeState.last_boundary_violations).map((item) => safeObject(item)),
10405
- },
10406
- active_execution: {
10407
- active: activeExecutionState.active === true,
10408
- stale: activeExecutionState.stale === true,
10409
- stuck: activeExecutionState.stuck === true,
10410
- comment_id: String(activeExecutionState.commentID || "").trim(),
10411
- source_message_id: intFromRawAllowZero(activeExecutionState.sourceMessageID, 0),
10412
- started_at: String(activeExecutionState.startedAt || "").trim(),
10413
- age_seconds: intFromRawAllowZero(activeExecutionState.ageSeconds, 0),
10414
- warning: String(activeExecutionState.warning || "").trim(),
10415
- },
11023
+ "-",
11024
+ ]);
11025
+ const envConfig = normalizedRoute.provider
11026
+ ? loadProviderEnvConfig(normalizedRoute.provider, {
11027
+ botID: normalizedRoute.botID,
11028
+ botName: resolvedServerBotName !== "-" ? resolvedServerBotName : "",
11029
+ route: normalizedRoute,
11030
+ })
11031
+ : null;
11032
+ const lastRunSummary = buildRunnerRouteLastResultSummary(routeState);
11033
+ const lastRunPayload = buildRunnerShowLastRunPayload(lastRunSummary);
11034
+ const activeExecutionPayload = buildRunnerShowActiveExecutionPayload(activeExecutionState);
11035
+ const resolvedContext = buildRunnerShowResolvedContext({
11036
+ normalizedRoute,
11037
+ diagnostics,
11038
+ envConfig,
11039
+ resolvedServerBotName,
11040
+ botNameSource,
11041
+ });
11042
+ return {
11043
+ ok: diagnostics.errors.length === 0,
11044
+ route_name: normalizedRoute.name || runnerRouteKey(normalizedRoute),
11045
+ route_key: runnerRouteKey(normalizedRoute),
11046
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11047
+ route_config_file: runnerConfig.filePath,
11048
+ workspace_registry_file: String(runnerConfig.workspaceRegistryFilePath || botRunnerWorkspaceRegistryFilePath()).trim() || "-",
11049
+ route_config: serializeRunnerRoute(normalizedRoute),
11050
+ ...resolvedContext,
11051
+ last_run: lastRunPayload,
11052
+ active_execution: activeExecutionPayload,
10416
11053
  route_selection_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.",
10417
11054
  warnings: [
10418
11055
  ...ensureArray(diagnostics.warnings),
10419
- ...(activeExecutionState.stuck ? [activeExecutionState.warning] : []),
11056
+ ...(activeExecutionPayload.stuck && activeExecutionPayload.warning ? [activeExecutionPayload.warning] : []),
10420
11057
  ],
10421
11058
  errors: diagnostics.errors,
10422
11059
  };
@@ -10468,6 +11105,20 @@ async function runRunnerShow(flags) {
10468
11105
  ` action: ${payload.last_run.action}`,
10469
11106
  ` reason: ${payload.last_run.reason}`,
10470
11107
  ` intent_type: ${payload.last_run.intent_type}`,
11108
+ ` ai_reply_preview: ${payload.last_run.ai_reply_preview}`,
11109
+ ` execution_contract_type: ${payload.last_run.execution_contract_type}`,
11110
+ ` execution_contract_targets: ${payload.last_run.execution_contract_targets.length ? payload.last_run.execution_contract_targets.join(", ") : "-"}`,
11111
+ ` next_expected_responders: ${payload.last_run.next_expected_responders.length ? payload.last_run.next_expected_responders.join(", ") : "-"}`,
11112
+ ` response_contract_validation_status: ${payload.last_run.response_contract_validation_status}`,
11113
+ ` response_contract_validation_reason: ${payload.last_run.response_contract_validation_reason}`,
11114
+ ` response_contract_validation_targets: ${payload.last_run.response_contract_validation_targets.length ? payload.last_run.response_contract_validation_targets.join(", ") : "-"}`,
11115
+ ` assignment_validation_status: ${payload.last_run.assignment_validation_status}`,
11116
+ ` assignment_validation_reason: ${payload.last_run.assignment_validation_reason}`,
11117
+ ` assignment_validation_modes: ${payload.last_run.assignment_validation_modes.length ? payload.last_run.assignment_validation_modes.join(", ") : "-"}`,
11118
+ ` delivery_status: ${payload.last_run.delivery_status}`,
11119
+ ` archive_status: ${payload.last_run.archive_status}`,
11120
+ ` transport_error: ${payload.last_run.transport_error}`,
11121
+ ` archive_error: ${payload.last_run.archive_error}`,
10471
11122
  ` workspace_dir: ${payload.last_run.workspace_dir}`,
10472
11123
  ` artifact_validation: ${payload.last_run.artifact_validation}`,
10473
11124
  ` artifact_paths: ${payload.last_run.artifact_paths.length ? payload.last_run.artifact_paths.join(", ") : "-"}`,
@@ -11013,6 +11664,161 @@ function summarizeRunnerTUIPhases(routes = []) {
11013
11664
  return { counts, warningCount };
11014
11665
  }
11015
11666
 
11667
+ function buildRunnerTUISummaryLine(phaseSummary) {
11668
+ const summary = safeObject(phaseSummary);
11669
+ const counts = safeObject(summary.counts);
11670
+ return `Summary: waiting=${intFromRawAllowZero(counts.waiting, 0)} polling=${intFromRawAllowZero(counts.polling, 0)} running=${intFromRawAllowZero(counts.running, 0)} busy=${intFromRawAllowZero(counts.busy, 0)} replied=${intFromRawAllowZero(counts.replied, 0)} error=${intFromRawAllowZero(counts.error, 0)} warnings=${intFromRawAllowZero(summary.warningCount, 0)}`;
11671
+ }
11672
+
11673
+ function buildRunnerTUIRouteTableHeader() {
11674
+ return [
11675
+ bootstrapPadRight("Route", 24),
11676
+ bootstrapPadRight("Phase", 18),
11677
+ bootstrapPadRight("Age", 7),
11678
+ bootstrapPadRight("AI", 20),
11679
+ bootstrapPadRight("Intent", 18),
11680
+ bootstrapPadRight("Msg", 8),
11681
+ bootstrapPadRight("Next", 8),
11682
+ bootstrapPadRight("Warn", 6),
11683
+ ].join(" ");
11684
+ }
11685
+
11686
+ function buildRunnerTUIRouteRow(route, now, useColor) {
11687
+ const currentRoute = safeObject(route);
11688
+ const phase = normalizeRunnerTUIPhase(currentRoute.phase);
11689
+ const aiLabel = truncateRunnerTUIText(
11690
+ currentRoute.client
11691
+ ? `${String(currentRoute.client || "").trim()}/${String(currentRoute.model || "").trim() || "-"}`
11692
+ : "-",
11693
+ 20,
11694
+ );
11695
+ const ageLabel = formatRunnerTUIAge(now, currentRoute.phase_started_at || currentRoute.updated_at || now);
11696
+ const nextSeconds = Number.isFinite(Number(currentRoute.next_run_at)) && Number(currentRoute.next_run_at) > now
11697
+ ? `${Math.max(0, Math.ceil((Number(currentRoute.next_run_at) - now) / 1000))}s`
11698
+ : phase === "ai_running" || phase === "delivering" || phase === "context_suggesting"
11699
+ ? "run"
11700
+ : "-";
11701
+ const warningLabel = String(currentRoute.warning || currentRoute.last_error || "").trim() ? "yes" : "-";
11702
+ return [
11703
+ bootstrapPadRight(truncateRunnerTUIText(currentRoute.route_name, 24), 24),
11704
+ colorizeRunnerTUIPhase(phase, 18, useColor),
11705
+ bootstrapPadRight(ageLabel, 7),
11706
+ bootstrapPadRight(aiLabel, 20),
11707
+ bootstrapPadRight(truncateRunnerTUIText(currentRoute.intent_type || "-", 18), 18),
11708
+ bootstrapPadRight(String(currentRoute.source_message_id || "-"), 8),
11709
+ bootstrapPadRight(nextSeconds, 8),
11710
+ bootstrapPadRight(warningLabel, 6),
11711
+ ].join(" ");
11712
+ }
11713
+
11714
+ function buildRunnerTUIRouteDetailLine(route) {
11715
+ const currentRoute = safeObject(route);
11716
+ const detailParts = [
11717
+ String(currentRoute.detail || "").trim(),
11718
+ String(currentRoute.planner_client || "").trim() || String(currentRoute.planner_model || "").trim()
11719
+ ? `planner=${String(currentRoute.planner_client || "").trim() || "-"}/${String(currentRoute.planner_model || "").trim() || "-"}`
11720
+ : "",
11721
+ String(currentRoute.context_suggestion_status || "").trim()
11722
+ ? `context=${String(currentRoute.context_suggestion_status || "").trim()}`
11723
+ : "",
11724
+ String(currentRoute.warning || "").trim()
11725
+ ? `warning=${String(currentRoute.warning || "").trim()}`
11726
+ : "",
11727
+ String(currentRoute.last_error || "").trim()
11728
+ ? `error=${String(currentRoute.last_error || "").trim()}`
11729
+ : "",
11730
+ ].filter(Boolean);
11731
+ return ` ${truncateRunnerTUIText(detailParts.join(" | ") || "-", 116)}`;
11732
+ }
11733
+
11734
+ function buildRunnerTUIRecentEventLine(event) {
11735
+ const currentEvent = safeObject(event);
11736
+ return ` [${String(currentEvent.time || "").trim() || "--:--:--"}] ${truncateRunnerTUIText(currentEvent.route_name, 24)} ${bootstrapPadRight(String(currentEvent.outcome || "-").trim(), 12)} ${truncateRunnerTUIText(currentEvent.detail || "-", 74)}`;
11737
+ }
11738
+
11739
+ function buildRunnerTUIRecentEvent(event, fallbackRouteName = "runner", fallbackOutcome = "-", fallbackDetail = "") {
11740
+ const currentEvent = safeObject(event);
11741
+ return {
11742
+ time: new Date().toLocaleTimeString("en-US", { hour12: false }),
11743
+ route_name: String(currentEvent.route_name || fallbackRouteName || "runner").trim(),
11744
+ outcome: String(currentEvent.outcome || fallbackOutcome || "-").trim(),
11745
+ detail: String(currentEvent.detail || fallbackDetail || "").trim(),
11746
+ };
11747
+ }
11748
+
11749
+ function buildRunnerTUIInitialRouteState(route) {
11750
+ const normalizedRoute = normalizeRunnerRoute(route);
11751
+ const routeKey = runnerRouteKey(normalizedRoute);
11752
+ const now = Date.now();
11753
+ return {
11754
+ route_key: routeKey,
11755
+ route_name: normalizedRoute.name,
11756
+ phase: "waiting",
11757
+ detail: "awaiting next poll",
11758
+ client: "",
11759
+ model: "",
11760
+ planner_client: "",
11761
+ planner_model: "",
11762
+ intent_type: "",
11763
+ source_message_id: "",
11764
+ next_run_at: 0,
11765
+ context_suggestion_status: "",
11766
+ warning: "",
11767
+ last_error: "",
11768
+ phase_started_at: now,
11769
+ updated_at: now,
11770
+ };
11771
+ }
11772
+
11773
+ function appendRunnerTUIRecentEvent(recentEvents, event, fallbackRouteName = "runner", fallbackOutcome = "-", fallbackDetail = "") {
11774
+ const nextEvents = ensureArray(recentEvents);
11775
+ nextEvents.unshift(buildRunnerTUIRecentEvent(event, fallbackRouteName, fallbackOutcome, fallbackDetail));
11776
+ if (nextEvents.length > 8) {
11777
+ nextEvents.length = 8;
11778
+ }
11779
+ }
11780
+
11781
+ function buildRunnerTUIRouteStatePatchFromResult(result, routeState = null, currentRouteState = null) {
11782
+ const current = safeObject(currentRouteState);
11783
+ const currentRouteSnapshot = safeObject(routeState);
11784
+ return {
11785
+ phase: mapRunnerTUIPhaseFromOutcome(result?.outcome),
11786
+ detail: String(result?.detail || "").trim(),
11787
+ intent_type: String(currentRouteSnapshot.last_intent_type || current.intent_type || "").trim(),
11788
+ source_message_id: intFromRawAllowZero(currentRouteSnapshot.last_source_message_id, intFromRawAllowZero(current.source_message_id, 0)) || current.source_message_id,
11789
+ context_suggestion_status: String(result?.context_suggestion_status || currentRouteSnapshot.last_context_suggestion_status || current.context_suggestion_status || "").trim(),
11790
+ warning: String(currentRouteSnapshot.active_execution_warning || result?.warning || "").trim(),
11791
+ last_error: String(currentRouteSnapshot.last_error || "").trim(),
11792
+ };
11793
+ }
11794
+
11795
+ function buildRunnerTUIRouteStatePatchFromExecutionStage({ executionPlan, selectedRecord, phase, detail, warning = "", contextSuggestionStatus = "", intentType = "" }) {
11796
+ const currentExecutionPlan = safeObject(executionPlan);
11797
+ const normalizedPhase = normalizeRunnerTUIPhase(phase);
11798
+ const clearLastError = ["ai_running", "delivering", "context_suggesting", "replied"].includes(normalizedPhase);
11799
+ const plannerFallbackClient = String(currentExecutionPlan.roleProfile?.client || "").trim();
11800
+ const plannerFallbackModel = String(currentExecutionPlan.roleProfile?.model || "").trim();
11801
+ const plannerClient = resolveRolePlannerClient({ env: process.env }) || plannerFallbackClient;
11802
+ const plannerModel = resolveRolePlannerModelDisplayName({
11803
+ env: process.env,
11804
+ fallbackClient: plannerFallbackClient,
11805
+ fallbackModel: plannerFallbackModel,
11806
+ });
11807
+ return {
11808
+ phase: normalizedPhase,
11809
+ detail: String(detail || "").trim(),
11810
+ client: plannerFallbackClient,
11811
+ model: plannerFallbackModel,
11812
+ planner_client: String(plannerClient || "").trim(),
11813
+ planner_model: String(plannerModel || "").trim(),
11814
+ source_message_id: intFromRawAllowZero(safeObject(selectedRecord).parsedArchive?.messageID, 0),
11815
+ intent_type: String(intentType || currentExecutionPlan.intentType || "").trim(),
11816
+ warning: String(warning || "").trim(),
11817
+ context_suggestion_status: String(contextSuggestionStatus || "").trim(),
11818
+ last_error: clearLastError ? "" : undefined,
11819
+ };
11820
+ }
11821
+
11016
11822
  function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1, now = Date.now(), stopping = false, useColor = false, sourceLabel = "runner start", logFilePath = "" }) {
11017
11823
  const lines = [];
11018
11824
  const activeCount = ensureArray(routes).filter((item) => ["polling", "ai_running", "delivering", "context_suggesting", "busy"].includes(normalizeRunnerTUIPhase(item.phase))).length;
@@ -11022,65 +11828,14 @@ function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1,
11022
11828
  if (String(logFilePath || "").trim()) {
11023
11829
  lines.push(`Log: ${truncateRunnerTUIText(String(logFilePath || "").trim(), 112)}`);
11024
11830
  }
11025
- lines.push(`Summary: waiting=${phaseSummary.counts.waiting} polling=${phaseSummary.counts.polling} running=${phaseSummary.counts.running} busy=${phaseSummary.counts.busy} replied=${phaseSummary.counts.replied} error=${phaseSummary.counts.error} warnings=${phaseSummary.warningCount}`);
11831
+ lines.push(buildRunnerTUISummaryLine(phaseSummary));
11026
11832
  lines.push("Ctrl+C to stop. Foreground runner state is shown below.");
11027
11833
  lines.push("");
11028
- lines.push(
11029
- [
11030
- bootstrapPadRight("Route", 24),
11031
- bootstrapPadRight("Phase", 18),
11032
- bootstrapPadRight("Age", 7),
11033
- bootstrapPadRight("AI", 20),
11034
- bootstrapPadRight("Intent", 18),
11035
- bootstrapPadRight("Msg", 8),
11036
- bootstrapPadRight("Next", 8),
11037
- bootstrapPadRight("Warn", 6),
11038
- ].join(" "),
11039
- );
11834
+ lines.push(buildRunnerTUIRouteTableHeader());
11040
11835
  lines.push("-".repeat(120));
11041
11836
  for (const route of ensureArray(routes)) {
11042
- const phase = normalizeRunnerTUIPhase(route.phase);
11043
- const aiLabel = truncateRunnerTUIText(
11044
- route.client
11045
- ? `${String(route.client || "").trim()}/${String(route.model || "").trim() || "-"}`
11046
- : "-",
11047
- 20,
11048
- );
11049
- const ageLabel = formatRunnerTUIAge(now, route.phase_started_at || route.updated_at || now);
11050
- const nextSeconds = Number.isFinite(Number(route.next_run_at)) && Number(route.next_run_at) > now
11051
- ? `${Math.max(0, Math.ceil((Number(route.next_run_at) - now) / 1000))}s`
11052
- : phase === "ai_running" || phase === "delivering" || phase === "context_suggesting"
11053
- ? "run"
11054
- : "-";
11055
- const warningLabel = String(route.warning || route.last_error || "").trim() ? "yes" : "-";
11056
- lines.push(
11057
- [
11058
- bootstrapPadRight(truncateRunnerTUIText(route.route_name, 24), 24),
11059
- colorizeRunnerTUIPhase(phase, 18, useColor),
11060
- bootstrapPadRight(ageLabel, 7),
11061
- bootstrapPadRight(aiLabel, 20),
11062
- bootstrapPadRight(truncateRunnerTUIText(route.intent_type || "-", 18), 18),
11063
- bootstrapPadRight(String(route.source_message_id || "-"), 8),
11064
- bootstrapPadRight(nextSeconds, 8),
11065
- bootstrapPadRight(warningLabel, 6),
11066
- ].join(" "),
11067
- );
11068
- const detailParts = [
11069
- String(route.detail || "").trim(),
11070
- String(route.planner_client || "").trim() || String(route.planner_model || "").trim()
11071
- ? `planner=${String(route.planner_client || "").trim() || "-"}/${String(route.planner_model || "").trim() || "-"}`
11072
- : "",
11073
- String(route.context_suggestion_status || "").trim()
11074
- ? `context=${String(route.context_suggestion_status || "").trim()}`
11075
- : "",
11076
- String(route.warning || "").trim()
11077
- ? `warning=${String(route.warning || "").trim()}`
11078
- : "",
11079
- String(route.last_error || "").trim()
11080
- ? `error=${String(route.last_error || "").trim()}`
11081
- : "",
11082
- ].filter(Boolean);
11083
- lines.push(` ${truncateRunnerTUIText(detailParts.join(" | ") || "-", 116)}`);
11837
+ lines.push(buildRunnerTUIRouteRow(route, now, useColor));
11838
+ lines.push(buildRunnerTUIRouteDetailLine(route));
11084
11839
  }
11085
11840
  lines.push("");
11086
11841
  lines.push(useColor ? bootstrapColorText("Recent Events", "35") : "Recent Events");
@@ -11088,9 +11843,7 @@ function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1,
11088
11843
  lines.push(" (none yet)");
11089
11844
  } else {
11090
11845
  for (const event of ensureArray(recentEvents).slice(0, 8)) {
11091
- lines.push(
11092
- ` [${String(event.time || "").trim() || "--:--:--"}] ${truncateRunnerTUIText(event.route_name, 24)} ${bootstrapPadRight(String(event.outcome || "-").trim(), 12)} ${truncateRunnerTUIText(event.detail || "-", 74)}`,
11093
- );
11846
+ lines.push(buildRunnerTUIRecentEventLine(event));
11094
11847
  }
11095
11848
  }
11096
11849
  return lines.join("\n");
@@ -11102,37 +11855,15 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11102
11855
  return null;
11103
11856
  }
11104
11857
  const useColor = bootstrapSupportsANSIColors();
11105
- const routeStates = new Map();
11858
+ const routeStates = new Map(
11859
+ ensureArray(routes).map((route) => {
11860
+ const routeState = buildRunnerTUIInitialRouteState(route);
11861
+ return [routeState.route_key, routeState];
11862
+ }),
11863
+ );
11106
11864
  const recentEvents = [];
11107
- for (const route of ensureArray(routes)) {
11108
- const normalizedRoute = normalizeRunnerRoute(route);
11109
- const routeKey = runnerRouteKey(normalizedRoute);
11110
- routeStates.set(routeKey, {
11111
- route_key: routeKey,
11112
- route_name: normalizedRoute.name,
11113
- phase: "waiting",
11114
- detail: "awaiting next poll",
11115
- client: "",
11116
- model: "",
11117
- planner_client: "",
11118
- planner_model: "",
11119
- intent_type: "",
11120
- source_message_id: "",
11121
- next_run_at: 0,
11122
- context_suggestion_status: "",
11123
- warning: "",
11124
- last_error: "",
11125
- phase_started_at: Date.now(),
11126
- updated_at: Date.now(),
11127
- });
11128
- }
11129
11865
  if (bootstrapEvent && typeof bootstrapEvent === "object") {
11130
- recentEvents.unshift({
11131
- time: new Date().toLocaleTimeString("en-US", { hour12: false }),
11132
- route_name: String(bootstrapEvent.route_name || sourceLabel || "runner").trim(),
11133
- outcome: String(bootstrapEvent.outcome || "prepared").trim(),
11134
- detail: String(bootstrapEvent.detail || "").trim(),
11135
- });
11866
+ recentEvents.unshift(buildRunnerTUIRecentEvent(bootstrapEvent, sourceLabel, "prepared"));
11136
11867
  }
11137
11868
  let intervalHandle = null;
11138
11869
  let disposed = false;
@@ -11171,57 +11902,27 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11171
11902
  const routeKey = String(result?.route_key || "").trim();
11172
11903
  if (routeKey && routeStates.has(routeKey)) {
11173
11904
  const current = safeObject(routeStates.get(routeKey));
11174
- setRouteState(routeKey, {
11175
- phase: mapRunnerTUIPhaseFromOutcome(result?.outcome),
11176
- detail: String(result?.detail || "").trim(),
11177
- intent_type: String(routeState?.last_intent_type || current.intent_type || "").trim(),
11178
- source_message_id: intFromRawAllowZero(routeState?.last_source_message_id, intFromRawAllowZero(current.source_message_id, 0)) || current.source_message_id,
11179
- context_suggestion_status: String(result?.context_suggestion_status || routeState?.last_context_suggestion_status || current.context_suggestion_status || "").trim(),
11180
- warning: String(safeObject(routeState).active_execution_warning || result?.warning || "").trim(),
11181
- last_error: String(routeState?.last_error || "").trim(),
11182
- });
11905
+ setRouteState(routeKey, buildRunnerTUIRouteStatePatchFromResult(result, routeState, current));
11183
11906
  }
11184
11907
  if (!result || ["idle", "busy"].includes(String(result.outcome || "").trim().toLowerCase())) {
11185
11908
  return;
11186
11909
  }
11187
- recentEvents.unshift({
11188
- time: new Date().toLocaleTimeString("en-US", { hour12: false }),
11189
- route_name: String(result.route_name || result.route_key || "").trim(),
11190
- outcome: String(result.outcome || "").trim(),
11191
- detail: String(result.detail || "").trim(),
11192
- });
11193
- if (recentEvents.length > 8) {
11194
- recentEvents.length = 8;
11195
- }
11910
+ appendRunnerTUIRecentEvent(recentEvents, result, String(result.route_name || result.route_key || "").trim());
11196
11911
  render(false);
11197
11912
  };
11198
11913
 
11199
11914
  const reportExecutionStage = ({ routeKey, executionPlan, selectedRecord, phase, detail, warning = "", contextSuggestionStatus = "", intentType = "" }) => {
11200
11915
  const normalizedRouteKey = String(routeKey || "").trim();
11201
11916
  if (!normalizedRouteKey || !routeStates.has(normalizedRouteKey)) return;
11202
- const normalizedPhase = normalizeRunnerTUIPhase(phase);
11203
- const clearLastError = ["ai_running", "delivering", "context_suggesting", "replied"].includes(normalizedPhase);
11204
- const plannerFallbackClient = String(safeObject(executionPlan).roleProfile?.client || "").trim();
11205
- const plannerFallbackModel = String(safeObject(executionPlan).roleProfile?.model || "").trim();
11206
- const plannerClient = resolveRolePlannerClient({ env: process.env }) || plannerFallbackClient;
11207
- const plannerModel = resolveRolePlannerModelDisplayName({
11208
- env: process.env,
11209
- fallbackClient: plannerFallbackClient,
11210
- fallbackModel: plannerFallbackModel,
11211
- });
11212
- setRouteState(normalizedRouteKey, {
11213
- phase: normalizedPhase,
11214
- detail: String(detail || "").trim(),
11215
- client: plannerFallbackClient,
11216
- model: plannerFallbackModel,
11217
- planner_client: String(plannerClient || "").trim(),
11218
- planner_model: String(plannerModel || "").trim(),
11219
- source_message_id: intFromRawAllowZero(safeObject(selectedRecord).parsedArchive?.messageID, 0),
11220
- intent_type: String(intentType || safeObject(executionPlan).intentType || "").trim(),
11221
- warning: String(warning || "").trim(),
11222
- context_suggestion_status: String(contextSuggestionStatus || "").trim(),
11223
- last_error: clearLastError ? "" : undefined,
11224
- });
11917
+ setRouteState(normalizedRouteKey, buildRunnerTUIRouteStatePatchFromExecutionStage({
11918
+ executionPlan,
11919
+ selectedRecord,
11920
+ phase,
11921
+ detail,
11922
+ warning,
11923
+ contextSuggestionStatus,
11924
+ intentType,
11925
+ }));
11225
11926
  };
11226
11927
 
11227
11928
  const updateNextRunAt = (routeKey, nextRunAt) => {
@@ -11248,6 +11949,550 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11248
11949
  };
11249
11950
  }
11250
11951
 
11952
+ function beginRunnerStartRoutePoll({ routeKey, normalizedRoute, tui, runnerLogger }) {
11953
+ tui?.setRouteState(routeKey, {
11954
+ phase: "polling",
11955
+ detail: "checking inbound messages and archive thread",
11956
+ });
11957
+ runnerLogger?.append("route_poll", {
11958
+ route_key: routeKey,
11959
+ route_name: normalizedRoute.name,
11960
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11961
+ });
11962
+ }
11963
+
11964
+ function buildRunnerStartRouteResultLogPayload(result, routeKey, routeName) {
11965
+ return {
11966
+ route_key: String(result?.route_key || routeKey).trim(),
11967
+ route_name: String(result?.route_name || routeName || "").trim(),
11968
+ outcome: String(result?.outcome || "").trim(),
11969
+ detail: String(result?.detail || "").trim(),
11970
+ comment_id: String(result?.comment_id || "").trim(),
11971
+ };
11972
+ }
11973
+
11974
+ function publishRunnerStartRouteResult({ result, routeState = null, routeKey, routeName, runnerLogger, tui, jsonMode }) {
11975
+ runnerLogger?.append("route_result", buildRunnerStartRouteResultLogPayload(result, routeKey, routeName));
11976
+ if (tui) {
11977
+ tui.recordResult(result, routeState);
11978
+ return;
11979
+ }
11980
+ printRunnerResult("start", result, jsonMode);
11981
+ }
11982
+
11983
+ function buildRunnerStartCycleErrorResult({ normalizedRoute, routeKey, errorText, fatalArchiveBootstrapError = false }) {
11984
+ return {
11985
+ route_key: routeKey,
11986
+ route_name: normalizedRoute.name,
11987
+ logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11988
+ outcome: fatalArchiveBootstrapError ? "blocked" : "error",
11989
+ detail: errorText,
11990
+ };
11991
+ }
11992
+
11993
+ function finalizeRunnerStartRouteCycle({ routeKey, normalizedRoute, cycleOutcome, schedules, tui }) {
11994
+ let routeState = safeObject(loadBotRunnerState().routes[routeKey]);
11995
+ let activeExecutionState = resolveRunnerActiveExecutionState(routeState);
11996
+ const successfulCycleOutcome = [
11997
+ "accepted",
11998
+ "busy",
11999
+ "idle",
12000
+ "primed",
12001
+ "skipped",
12002
+ "replied",
12003
+ "dry_run",
12004
+ "delivery_failed_after_generation",
12005
+ ].includes(cycleOutcome);
12006
+ const shouldClearLastError = successfulCycleOutcome
12007
+ && String(routeState.last_error || "").trim();
12008
+ const shouldClearStaleActiveExecution = !activeExecutionState.active
12009
+ && successfulCycleOutcome
12010
+ && String(routeState.active_comment_id || "").trim();
12011
+ if (shouldClearLastError || shouldClearStaleActiveExecution) {
12012
+ saveRunnerRouteState(routeKey, {
12013
+ ...(shouldClearStaleActiveExecution ? emptyRunnerActiveExecutionPatch() : {}),
12014
+ ...(shouldClearLastError ? { last_error: "" } : {}),
12015
+ });
12016
+ routeState = safeObject(loadBotRunnerState().routes[routeKey]);
12017
+ activeExecutionState = resolveRunnerActiveExecutionState(routeState);
12018
+ }
12019
+ tui?.setRouteState(routeKey, {
12020
+ intent_type: String(routeState.last_intent_type || "").trim(),
12021
+ source_message_id: intFromRawAllowZero(routeState.last_source_message_id, 0),
12022
+ warning: String(activeExecutionState.warning || "").trim(),
12023
+ last_error: String(routeState.last_error || "").trim(),
12024
+ });
12025
+ const lastErrorText = String(routeState.last_error || "").trim();
12026
+ const isFatalArchiveBootstrapError = lastErrorText.includes("Archive thread is missing")
12027
+ && lastErrorText.includes("write access is denied");
12028
+ const nextRunAt = Date.now() + (isFatalArchiveBootstrapError
12029
+ ? 60000
12030
+ : Math.max(1000, normalizedRoute.pollIntervalMs));
12031
+ schedules.set(routeKey, nextRunAt);
12032
+ tui?.updateNextRunAt(routeKey, nextRunAt);
12033
+ }
12034
+
12035
+ async function syncRunnerDeferredExecutionRunningState(deferredExecution) {
12036
+ if (!String(deferredExecution?.requestKey || "").trim()) {
12037
+ return;
12038
+ }
12039
+ markRunnerRequestLifecycle({
12040
+ normalizedRoute: deferredExecution.normalizedRoute,
12041
+ requestKey: deferredExecution.requestKey,
12042
+ selectedRecord: deferredExecution.selectedRecord,
12043
+ routeKey: deferredExecution.routeKey,
12044
+ outcome: "running",
12045
+ });
12046
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
12047
+ normalizedRoute: deferredExecution.normalizedRoute,
12048
+ runtime: deferredExecution.runtime,
12049
+ requestKey: deferredExecution.requestKey,
12050
+ });
12051
+ const syncedRequest = safeObject(rootWorkItemSync.request);
12052
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
12053
+ saveRunnerRouteState(deferredExecution.routeKey, {
12054
+ active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
12055
+ active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
12056
+ active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
12057
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
12058
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
12059
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
12060
+ });
12061
+ }
12062
+ await syncRunnerRequestLedgerForProjectToServer({
12063
+ normalizedRoute: deferredExecution.normalizedRoute,
12064
+ runtime: deferredExecution.runtime,
12065
+ });
12066
+ }
12067
+
12068
+ async function finalizeRunnerDeferredExecutionSkipped(deferredExecution, processed) {
12069
+ if (String(deferredExecution?.requestKey || "").trim()) {
12070
+ markRunnerRequestLifecycle({
12071
+ normalizedRoute: deferredExecution.normalizedRoute,
12072
+ requestKey: deferredExecution.requestKey,
12073
+ selectedRecord: deferredExecution.selectedRecord,
12074
+ routeKey: deferredExecution.routeKey,
12075
+ outcome: "skipped",
12076
+ closedReason: String(processed?.skippedRecord?.reason || "skipped").trim() || "skipped",
12077
+ });
12078
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
12079
+ normalizedRoute: deferredExecution.normalizedRoute,
12080
+ runtime: deferredExecution.runtime,
12081
+ requestKey: deferredExecution.requestKey,
12082
+ });
12083
+ const syncedRequest = safeObject(rootWorkItemSync.request);
12084
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
12085
+ saveRunnerRouteState(deferredExecution.routeKey, {
12086
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
12087
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
12088
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
12089
+ });
12090
+ }
12091
+ await syncRunnerRequestLedgerForProjectToServer({
12092
+ normalizedRoute: deferredExecution.normalizedRoute,
12093
+ runtime: deferredExecution.runtime,
12094
+ });
12095
+ }
12096
+ return {
12097
+ route_key: deferredExecution.routeKey,
12098
+ route_name: deferredExecution.normalizedRoute.name,
12099
+ logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
12100
+ outcome: "skipped",
12101
+ detail: String(processed?.skippedRecord?.reason || "trigger policy skipped message").trim(),
12102
+ archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
12103
+ archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
12104
+ thread_id: deferredExecution.archiveThread.threadID,
12105
+ comment_id: deferredExecution.selectedRecord.id,
12106
+ execution_mode: deferredExecution.executionPlan.mode,
12107
+ role_profile: deferredExecution.executionPlan.roleProfileName,
12108
+ };
12109
+ }
12110
+
12111
+ async function finalizeRunnerDeferredExecutionProcessed(deferredExecution, processed) {
12112
+ if (String(deferredExecution?.requestKey || "").trim()) {
12113
+ const resolvedIntentType = String(
12114
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
12115
+ ).trim();
12116
+ markRunnerRequestLifecycle({
12117
+ normalizedRoute: deferredExecution.normalizedRoute,
12118
+ requestKey: deferredExecution.requestKey,
12119
+ selectedRecord: deferredExecution.selectedRecord,
12120
+ routeKey: deferredExecution.routeKey,
12121
+ outcome: processed.kind === "delivery_failed"
12122
+ ? "delivery_failed_after_generation"
12123
+ : String(processed.result?.outcome || "replied").trim().toLowerCase(),
12124
+ conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
12125
+ conversationParticipants: ensureArray(processed.result?.conversation_participants),
12126
+ conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
12127
+ allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
12128
+ conversationLeadBot: String(processed.result?.conversation_lead_bot || "").trim(),
12129
+ conversationSummaryBot: String(processed.result?.conversation_summary_bot || "").trim(),
12130
+ conversationAllowBotToBot: processed.result?.conversation_allow_bot_to_bot === true,
12131
+ conversationReplyExpectation: String(processed.result?.conversation_reply_expectation || "").trim(),
12132
+ executionContractType: String(processed.result?.execution_contract_type || "").trim(),
12133
+ executionContractActionable: processed.result?.execution_contract_actionable === true,
12134
+ executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
12135
+ nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
12136
+ normalizedExecutionContractType: String(processed.result?.normalized_execution_contract_type || "").trim(),
12137
+ normalizedExecutionContractTargets: ensureArray(processed.result?.normalized_execution_contract_targets),
12138
+ normalizedExecutionNextResponders: ensureArray(processed.result?.normalized_execution_next_responders),
12139
+ currentBotSelector: normalizeTelegramMentionUsername(
12140
+ deferredExecution.bot?.username || deferredExecution.bot?.name,
12141
+ ),
12142
+ conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
12143
+ normalizedIntent: resolvedIntentType,
12144
+ aiReplyGenerated: processed.result?.ai_reply_generated === true,
12145
+ aiReplyGeneratedAt: String(processed.result?.ai_reply_generated_at || "").trim(),
12146
+ aiReplyPreview: String(processed.result?.ai_reply_preview || "").trim(),
12147
+ responseContractValidationStatus: String(processed.result?.response_contract_validation_status || "").trim(),
12148
+ responseContractValidationReason: String(processed.result?.response_contract_validation_reason || "").trim(),
12149
+ responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
12150
+ assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
12151
+ assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
12152
+ assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
12153
+ deliveryStatus: String(processed.result?.delivery_status || "").trim(),
12154
+ archiveStatus: String(processed.result?.archive_status || "").trim(),
12155
+ transportError: String(processed.result?.transport_error || "").trim(),
12156
+ archiveError: String(processed.result?.archive_error || "").trim(),
12157
+ });
12158
+ if (processed.kind !== "delivery_failed") {
12159
+ await ensureRunnerRootWorkItemForRequest({
12160
+ normalizedRoute: deferredExecution.normalizedRoute,
12161
+ routeKey: deferredExecution.routeKey,
12162
+ selectedRecord: deferredExecution.selectedRecord,
12163
+ runtime: deferredExecution.runtime,
12164
+ requestKey: deferredExecution.requestKey,
12165
+ });
12166
+ }
12167
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
12168
+ normalizedRoute: deferredExecution.normalizedRoute,
12169
+ runtime: deferredExecution.runtime,
12170
+ requestKey: deferredExecution.requestKey,
12171
+ });
12172
+ const syncedRequest = safeObject(rootWorkItemSync.request);
12173
+ const followupRoutePatch = buildRunnerRouteFollowupSnapshotPatch(
12174
+ deferredExecution.selectedRecord,
12175
+ processed.result,
12176
+ );
12177
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
12178
+ saveRunnerRouteState(deferredExecution.routeKey, {
12179
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
12180
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
12181
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
12182
+ ...followupRoutePatch,
12183
+ });
12184
+ } else if (Object.keys(followupRoutePatch).length) {
12185
+ saveRunnerRouteState(deferredExecution.routeKey, followupRoutePatch);
12186
+ }
12187
+ await syncRunnerRequestLedgerForProjectToServer({
12188
+ normalizedRoute: deferredExecution.normalizedRoute,
12189
+ runtime: deferredExecution.runtime,
12190
+ });
12191
+ }
12192
+ return {
12193
+ logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
12194
+ archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
12195
+ archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
12196
+ ...processed.result,
12197
+ };
12198
+ }
12199
+
12200
+ async function finalizeRunnerDeferredExecutionError(deferredExecution, errorText) {
12201
+ const currentRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
12202
+ saveRunnerRouteState(deferredExecution.routeKey, {
12203
+ ...currentRouteState,
12204
+ ...emptyRunnerActiveExecutionPatch(),
12205
+ last_error: errorText,
12206
+ });
12207
+ if (String(deferredExecution?.requestKey || "").trim()) {
12208
+ const currentRequest = safeObject(loadRunnerRequestByKey(deferredExecution.requestKey));
12209
+ const resolvedIntentType = String(
12210
+ safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type
12211
+ || currentRequest.normalized_intent
12212
+ || "",
12213
+ ).trim();
12214
+ markRunnerRequestLifecycle({
12215
+ normalizedRoute: deferredExecution.normalizedRoute,
12216
+ requestKey: deferredExecution.requestKey,
12217
+ selectedRecord: deferredExecution.selectedRecord,
12218
+ routeKey: deferredExecution.routeKey,
12219
+ outcome: "error",
12220
+ closedReason: errorText || "execution_error",
12221
+ normalizedIntent: resolvedIntentType,
12222
+ });
12223
+ await ensureRunnerRootWorkItemForRequest({
12224
+ normalizedRoute: deferredExecution.normalizedRoute,
12225
+ routeKey: deferredExecution.routeKey,
12226
+ selectedRecord: deferredExecution.selectedRecord,
12227
+ runtime: deferredExecution.runtime,
12228
+ requestKey: deferredExecution.requestKey,
12229
+ });
12230
+ const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
12231
+ normalizedRoute: deferredExecution.normalizedRoute,
12232
+ runtime: deferredExecution.runtime,
12233
+ requestKey: deferredExecution.requestKey,
12234
+ });
12235
+ const syncedRequest = safeObject(rootWorkItemSync.request);
12236
+ if (String(syncedRequest.root_work_item_id || "").trim()) {
12237
+ saveRunnerRouteState(deferredExecution.routeKey, {
12238
+ last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
12239
+ last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
12240
+ last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
12241
+ });
12242
+ }
12243
+ await syncRunnerRequestLedgerForProjectToServer({
12244
+ normalizedRoute: deferredExecution.normalizedRoute,
12245
+ runtime: deferredExecution.runtime,
12246
+ });
12247
+ }
12248
+ return {
12249
+ route_key: deferredExecution.routeKey,
12250
+ route_name: deferredExecution.normalizedRoute.name,
12251
+ logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
12252
+ outcome: "error",
12253
+ detail: errorText,
12254
+ archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
12255
+ archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
12256
+ thread_id: deferredExecution.archiveThread.threadID,
12257
+ comment_id: deferredExecution.selectedRecord.id,
12258
+ };
12259
+ }
12260
+
12261
+ function reportRunnerDeferredExecutionAccepted({ deferredExecution, runnerLogger, tui }) {
12262
+ tui?.reportExecutionStage({
12263
+ routeKey: deferredExecution.routeKey,
12264
+ executionPlan: deferredExecution.executionPlan,
12265
+ selectedRecord: deferredExecution.selectedRecord,
12266
+ phase: "ai_running",
12267
+ detail: `running local AI client for comment ${String(deferredExecution.selectedRecord?.id || "").trim() || "-"}`,
12268
+ });
12269
+ runnerLogger?.append("execution_accepted", {
12270
+ route_key: deferredExecution.routeKey,
12271
+ route_name: deferredExecution.normalizedRoute?.name,
12272
+ comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
12273
+ source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
12274
+ execution_mode: String(deferredExecution.executionPlan?.mode || "").trim(),
12275
+ role_profile: String(deferredExecution.executionPlan?.roleProfileName || "").trim(),
12276
+ });
12277
+ }
12278
+
12279
+ function createRunnerDeferredExecutionStageReporter({ deferredExecution, runnerLogger, tui }) {
12280
+ return (stage) => {
12281
+ runnerLogger?.append("execution_stage", {
12282
+ route_key: deferredExecution.routeKey,
12283
+ route_name: deferredExecution.normalizedRoute?.name,
12284
+ comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
12285
+ source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
12286
+ phase: String(safeObject(stage).phase || "").trim(),
12287
+ detail: String(safeObject(stage).detail || "").trim(),
12288
+ intent_type: String(safeObject(stage).intentType || "").trim(),
12289
+ context_suggestion_status: String(safeObject(stage).contextSuggestionStatus || "").trim(),
12290
+ });
12291
+ tui?.reportExecutionStage({
12292
+ routeKey: deferredExecution.routeKey,
12293
+ executionPlan: deferredExecution.executionPlan,
12294
+ selectedRecord: deferredExecution.selectedRecord,
12295
+ ...safeObject(stage),
12296
+ });
12297
+ };
12298
+ }
12299
+
12300
+ function publishRunnerDeferredExecutionFinalResult({
12301
+ deferredExecution,
12302
+ finalResult,
12303
+ runnerLogger,
12304
+ tui,
12305
+ jsonMode,
12306
+ }) {
12307
+ const latestRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
12308
+ runnerLogger?.append("execution_result", {
12309
+ route_key: String(finalResult?.route_key || deferredExecution.routeKey).trim(),
12310
+ route_name: String(finalResult?.route_name || deferredExecution.normalizedRoute?.name || "").trim(),
12311
+ outcome: String(finalResult?.outcome || "").trim(),
12312
+ detail: String(finalResult?.detail || "").trim(),
12313
+ comment_id: String(finalResult?.comment_id || deferredExecution.selectedRecord?.id || "").trim(),
12314
+ source_message_id: intFromRawAllowZero(
12315
+ latestRouteState?.last_source_message_id,
12316
+ intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
12317
+ ),
12318
+ intent_type: String(latestRouteState?.last_intent_type || "").trim(),
12319
+ context_suggestion_status: String(
12320
+ finalResult?.context_suggestion_status || latestRouteState?.last_context_suggestion_status || "",
12321
+ ).trim(),
12322
+ });
12323
+ if (tui) {
12324
+ tui.recordResult(finalResult, latestRouteState);
12325
+ return;
12326
+ }
12327
+ printRunnerResult("start", finalResult, jsonMode);
12328
+ }
12329
+
12330
+ function handleRunnerStartRouteCycleError({
12331
+ normalizedRoute,
12332
+ routeKey,
12333
+ errorText,
12334
+ runnerLogger,
12335
+ tui,
12336
+ jsonMode,
12337
+ }) {
12338
+ const fatalArchiveBootstrapError = errorText.includes("Archive thread is missing")
12339
+ && errorText.includes("write access is denied");
12340
+ saveRunnerRouteState(routeKey, {
12341
+ ...safeObject(loadBotRunnerState().routes[routeKey]),
12342
+ last_error: errorText,
12343
+ });
12344
+ const result = buildRunnerStartCycleErrorResult({
12345
+ normalizedRoute,
12346
+ routeKey,
12347
+ errorText,
12348
+ fatalArchiveBootstrapError,
12349
+ });
12350
+ publishRunnerStartRouteResult({
12351
+ result,
12352
+ routeKey,
12353
+ routeName: normalizedRoute.name,
12354
+ runnerLogger,
12355
+ tui,
12356
+ jsonMode,
12357
+ });
12358
+ return fatalArchiveBootstrapError ? "blocked" : "error";
12359
+ }
12360
+
12361
+ function publishRunnerStartImmediateResult({
12362
+ result,
12363
+ routeKey,
12364
+ normalizedRoute,
12365
+ runnerLogger,
12366
+ tui,
12367
+ jsonMode,
12368
+ }) {
12369
+ publishRunnerStartRouteResult({
12370
+ result,
12371
+ routeKey,
12372
+ routeName: normalizedRoute.name,
12373
+ runnerLogger,
12374
+ tui,
12375
+ jsonMode,
12376
+ });
12377
+ }
12378
+
12379
+ function resolveRunnerStartDueRoutes(routes, schedules, now = Date.now()) {
12380
+ const dueRoutes = [];
12381
+ let nextSleepMs = 5000;
12382
+ for (const route of routes) {
12383
+ const normalizedRoute = normalizeRunnerRoute(route);
12384
+ const routeKey = runnerRouteKey(normalizedRoute);
12385
+ const nextAt = Number(schedules.get(routeKey) || 0);
12386
+ if (nextAt > now) {
12387
+ nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - now));
12388
+ continue;
12389
+ }
12390
+ dueRoutes.push(normalizedRoute);
12391
+ }
12392
+ return {
12393
+ dueRoutes,
12394
+ nextSleepMs,
12395
+ };
12396
+ }
12397
+
12398
+ function refreshRunnerStartNextSleepMs(routes, schedules, currentNextSleepMs, now = Date.now()) {
12399
+ let nextSleepMs = currentNextSleepMs;
12400
+ for (const route of routes) {
12401
+ const normalizedRoute = normalizeRunnerRoute(route);
12402
+ const routeKey = runnerRouteKey(normalizedRoute);
12403
+ const nextAt = Number(schedules.get(routeKey) || 0);
12404
+ if (nextAt > now) {
12405
+ nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - now));
12406
+ }
12407
+ }
12408
+ return nextSleepMs;
12409
+ }
12410
+
12411
+ function finalizeRunnerDeferredExecutionLoopState({
12412
+ deferredExecution,
12413
+ executionHeartbeat,
12414
+ inFlightExecutions,
12415
+ schedules,
12416
+ tui,
12417
+ }) {
12418
+ executionHeartbeat.stop();
12419
+ inFlightExecutions.delete(deferredExecution.routeKey);
12420
+ const nextRunAt = Date.now() + 250;
12421
+ schedules.set(deferredExecution.routeKey, nextRunAt);
12422
+ tui?.updateNextRunAt(deferredExecution.routeKey, nextRunAt);
12423
+ }
12424
+
12425
+ function createRunnerDeferredExecutionPromise({
12426
+ deferredExecution,
12427
+ executionHeartbeat,
12428
+ inFlightExecutions,
12429
+ schedules,
12430
+ tui,
12431
+ runnerLogger,
12432
+ jsonMode,
12433
+ reportRunnerStage,
12434
+ }) {
12435
+ return (async () => {
12436
+ try {
12437
+ const processed = await processRunnerSelectedRecord({
12438
+ routeKey: deferredExecution.routeKey,
12439
+ normalizedRoute: deferredExecution.normalizedRoute,
12440
+ routeState: deferredExecution.routeState,
12441
+ selectedRecord: deferredExecution.selectedRecord,
12442
+ pendingOrdered: deferredExecution.pendingOrdered,
12443
+ bot: deferredExecution.bot,
12444
+ destination: deferredExecution.destination,
12445
+ archiveThread: deferredExecution.archiveThread,
12446
+ executionPlan: deferredExecution.executionPlan,
12447
+ runtime: deferredExecution.runtime,
12448
+ triggerDecision: deferredExecution.triggerDecision,
12449
+ responderAdjudication: deferredExecution.responderAdjudication,
12450
+ persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
12451
+ precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
12452
+ deps: {
12453
+ saveRunnerRouteState,
12454
+ startRunnerTypingHeartbeat,
12455
+ runRunnerAIExecution,
12456
+ explainExecutionFailureWithAI,
12457
+ performLocalBotDelivery,
12458
+ serializeRunnerTriggerPolicy,
12459
+ serializeRunnerArchivePolicy,
12460
+ buildRunnerExecutionDeps,
12461
+ buildRunnerDeliveryDeps,
12462
+ buildRunnerRuntimeDeps,
12463
+ managedConversationBots: deferredExecution.managedConversationBots,
12464
+ resolveConversationPeerBots: resolveRunnerConversationPeers,
12465
+ reportRunnerStage,
12466
+ },
12467
+ });
12468
+ if (processed.kind === "skipped") {
12469
+ return await finalizeRunnerDeferredExecutionSkipped(deferredExecution, processed);
12470
+ }
12471
+ return await finalizeRunnerDeferredExecutionProcessed(deferredExecution, processed);
12472
+ } catch (err) {
12473
+ const errorText = String(err?.message || err).trim();
12474
+ return await finalizeRunnerDeferredExecutionError(deferredExecution, errorText);
12475
+ } finally {
12476
+ finalizeRunnerDeferredExecutionLoopState({
12477
+ deferredExecution,
12478
+ executionHeartbeat,
12479
+ inFlightExecutions,
12480
+ schedules,
12481
+ tui,
12482
+ });
12483
+ }
12484
+ })().then((finalResult) => {
12485
+ publishRunnerDeferredExecutionFinalResult({
12486
+ deferredExecution,
12487
+ finalResult,
12488
+ runnerLogger,
12489
+ tui,
12490
+ jsonMode,
12491
+ });
12492
+ return finalResult;
12493
+ });
12494
+ }
12495
+
11251
12496
  async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11252
12497
  const jsonMode = boolFromRaw(flags.json, false);
11253
12498
  const sourceLabel = String(safeObject(options).sourceLabel || "runner start").trim() || "runner start";
@@ -11277,463 +12522,92 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11277
12522
  }
11278
12523
  tui?.start();
11279
12524
  while (!stopRequested) {
11280
- const now = Date.now();
11281
- const dueRoutes = [];
11282
- let nextSleepMs = 5000;
11283
- for (const route of routes) {
11284
- const normalizedRoute = normalizeRunnerRoute(route);
11285
- const routeKey = runnerRouteKey(normalizedRoute);
11286
- const nextAt = Number(schedules.get(routeKey) || 0);
11287
- if (nextAt > now) {
11288
- nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - now));
11289
- continue;
11290
- }
11291
- dueRoutes.push(normalizedRoute);
11292
- }
12525
+ const { dueRoutes, nextSleepMs: initialNextSleepMs } = resolveRunnerStartDueRoutes(routes, schedules, Date.now());
12526
+ let nextSleepMs = initialNextSleepMs;
11293
12527
  if (dueRoutes.length > 0) {
11294
12528
  const runtime = await resolveRunnerContext(flags);
11295
12529
  const dueRouteGroups = groupRunnerRoutesBySchedulingTarget(dueRoutes);
11296
12530
  await runTasksWithConcurrencyLimit(dueRouteGroups, concurrency, async (routeGroup) => {
11297
12531
  for (const normalizedRoute of ensureArray(routeGroup)) {
11298
12532
  const routeKey = runnerRouteKey(normalizedRoute);
11299
- tui?.setRouteState(routeKey, {
11300
- phase: "polling",
11301
- detail: "checking inbound messages and archive thread",
11302
- });
11303
- runnerLogger?.append("route_poll", {
11304
- route_key: routeKey,
11305
- route_name: normalizedRoute.name,
11306
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11307
- });
12533
+ beginRunnerStartRoutePoll({ routeKey, normalizedRoute, tui, runnerLogger });
11308
12534
  let cycleOutcome = "polling";
11309
12535
  try {
11310
12536
  const result = await processRunnerRouteOnce(normalizedRoute, runtime, "start", { deferExecution: true });
11311
12537
  cycleOutcome = String(result?.outcome || "").trim().toLowerCase() || "idle";
11312
12538
  const deferredExecution = safeObject(result.deferred_execution);
11313
12539
  if (deferredExecution.routeKey) {
11314
- tui?.reportExecutionStage({
11315
- routeKey: deferredExecution.routeKey,
11316
- executionPlan: deferredExecution.executionPlan,
11317
- selectedRecord: deferredExecution.selectedRecord,
11318
- phase: "ai_running",
11319
- detail: `running local AI client for comment ${String(deferredExecution.selectedRecord?.id || "").trim() || "-"}`,
11320
- });
11321
- runnerLogger?.append("execution_accepted", {
11322
- route_key: deferredExecution.routeKey,
11323
- route_name: deferredExecution.normalizedRoute?.name,
11324
- comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
11325
- source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
11326
- execution_mode: String(deferredExecution.executionPlan?.mode || "").trim(),
11327
- role_profile: String(deferredExecution.executionPlan?.roleProfileName || "").trim(),
12540
+ reportRunnerDeferredExecutionAccepted({
12541
+ deferredExecution,
12542
+ runnerLogger,
12543
+ tui,
11328
12544
  });
11329
12545
  const executionHeartbeat = startRunnerExecutionHeartbeat(
11330
12546
  deferredExecution.routeKey,
11331
12547
  deferredExecution.selectedRecord,
11332
12548
  );
11333
- if (String(deferredExecution.requestKey || "").trim()) {
11334
- markRunnerRequestLifecycle({
11335
- normalizedRoute: deferredExecution.normalizedRoute,
11336
- requestKey: deferredExecution.requestKey,
11337
- selectedRecord: deferredExecution.selectedRecord,
11338
- routeKey: deferredExecution.routeKey,
11339
- outcome: "running",
11340
- });
11341
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11342
- normalizedRoute: deferredExecution.normalizedRoute,
11343
- runtime: deferredExecution.runtime,
11344
- requestKey: deferredExecution.requestKey,
11345
- });
11346
- const syncedRequest = safeObject(rootWorkItemSync.request);
11347
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11348
- saveRunnerRouteState(deferredExecution.routeKey, {
11349
- active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11350
- active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11351
- active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11352
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11353
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11354
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11355
- });
11356
- }
11357
- await syncRunnerRequestLedgerForProjectToServer({
11358
- normalizedRoute: deferredExecution.normalizedRoute,
11359
- runtime: deferredExecution.runtime,
11360
- });
11361
- }
11362
- const executionPromise = (async () => {
11363
- try {
11364
- const processed = await processRunnerSelectedRecord({
11365
- routeKey: deferredExecution.routeKey,
11366
- normalizedRoute: deferredExecution.normalizedRoute,
11367
- routeState: deferredExecution.routeState,
11368
- selectedRecord: deferredExecution.selectedRecord,
11369
- pendingOrdered: deferredExecution.pendingOrdered,
11370
- bot: deferredExecution.bot,
11371
- destination: deferredExecution.destination,
11372
- archiveThread: deferredExecution.archiveThread,
11373
- executionPlan: deferredExecution.executionPlan,
11374
- runtime: deferredExecution.runtime,
11375
- triggerDecision: deferredExecution.triggerDecision,
11376
- responderAdjudication: deferredExecution.responderAdjudication,
11377
- persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
11378
- precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
11379
- deps: {
11380
- saveRunnerRouteState,
11381
- startRunnerTypingHeartbeat,
11382
- runRunnerAIExecution,
11383
- explainExecutionFailureWithAI,
11384
- performLocalBotDelivery,
11385
- serializeRunnerTriggerPolicy,
11386
- serializeRunnerArchivePolicy,
11387
- buildRunnerExecutionDeps,
11388
- buildRunnerDeliveryDeps,
11389
- buildRunnerRuntimeDeps,
11390
- managedConversationBots: deferredExecution.managedConversationBots,
11391
- resolveConversationPeerBots: resolveRunnerConversationPeers,
11392
- reportRunnerStage: (stage) => {
11393
- runnerLogger?.append("execution_stage", {
11394
- route_key: deferredExecution.routeKey,
11395
- route_name: deferredExecution.normalizedRoute?.name,
11396
- comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
11397
- source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
11398
- phase: String(safeObject(stage).phase || "").trim(),
11399
- detail: String(safeObject(stage).detail || "").trim(),
11400
- intent_type: String(safeObject(stage).intentType || "").trim(),
11401
- context_suggestion_status: String(safeObject(stage).contextSuggestionStatus || "").trim(),
11402
- });
11403
- tui?.reportExecutionStage({
11404
- routeKey: deferredExecution.routeKey,
11405
- executionPlan: deferredExecution.executionPlan,
11406
- selectedRecord: deferredExecution.selectedRecord,
11407
- ...safeObject(stage),
11408
- });
11409
- },
11410
- },
11411
- });
11412
- if (processed.kind === "skipped") {
11413
- if (String(deferredExecution.requestKey || "").trim()) {
11414
- markRunnerRequestLifecycle({
11415
- normalizedRoute: deferredExecution.normalizedRoute,
11416
- requestKey: deferredExecution.requestKey,
11417
- selectedRecord: deferredExecution.selectedRecord,
11418
- routeKey: deferredExecution.routeKey,
11419
- outcome: "skipped",
11420
- closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
11421
- });
11422
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11423
- normalizedRoute: deferredExecution.normalizedRoute,
11424
- runtime: deferredExecution.runtime,
11425
- requestKey: deferredExecution.requestKey,
11426
- });
11427
- const syncedRequest = safeObject(rootWorkItemSync.request);
11428
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11429
- saveRunnerRouteState(deferredExecution.routeKey, {
11430
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11431
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11432
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11433
- });
11434
- }
11435
- await syncRunnerRequestLedgerForProjectToServer({
11436
- normalizedRoute: deferredExecution.normalizedRoute,
11437
- runtime: deferredExecution.runtime,
11438
- });
11439
- }
11440
- return {
11441
- route_key: deferredExecution.routeKey,
11442
- route_name: deferredExecution.normalizedRoute.name,
11443
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11444
- outcome: "skipped",
11445
- detail: String(processed.skippedRecord?.reason || "trigger policy skipped message").trim(),
11446
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11447
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11448
- thread_id: deferredExecution.archiveThread.threadID,
11449
- comment_id: deferredExecution.selectedRecord.id,
11450
- execution_mode: deferredExecution.executionPlan.mode,
11451
- role_profile: deferredExecution.executionPlan.roleProfileName,
11452
- };
11453
- }
11454
- if (String(deferredExecution.requestKey || "").trim()) {
11455
- const resolvedIntentType = String(
11456
- safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
11457
- ).trim();
11458
- markRunnerRequestLifecycle({
11459
- normalizedRoute: deferredExecution.normalizedRoute,
11460
- requestKey: deferredExecution.requestKey,
11461
- selectedRecord: deferredExecution.selectedRecord,
11462
- routeKey: deferredExecution.routeKey,
11463
- outcome: processed.kind === "delivery_failed"
11464
- ? "delivery_failed_after_generation"
11465
- : String(processed.result?.outcome || "replied").trim().toLowerCase(),
11466
- conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
11467
- conversationParticipants: ensureArray(processed.result?.conversation_participants),
11468
- conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
11469
- allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
11470
- conversationLeadBot: String(processed.result?.conversation_lead_bot || "").trim(),
11471
- conversationSummaryBot: String(processed.result?.conversation_summary_bot || "").trim(),
11472
- conversationAllowBotToBot: processed.result?.conversation_allow_bot_to_bot === true,
11473
- conversationReplyExpectation: String(processed.result?.conversation_reply_expectation || "").trim(),
11474
- executionContractType: String(processed.result?.execution_contract_type || "").trim(),
11475
- executionContractActionable: processed.result?.execution_contract_actionable === true,
11476
- executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
11477
- nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
11478
- normalizedExecutionContractType: String(processed.result?.normalized_execution_contract_type || "").trim(),
11479
- normalizedExecutionContractTargets: ensureArray(processed.result?.normalized_execution_contract_targets),
11480
- normalizedExecutionNextResponders: ensureArray(processed.result?.normalized_execution_next_responders),
11481
- currentBotSelector: normalizeTelegramMentionUsername(
11482
- deferredExecution.bot?.username || deferredExecution.bot?.name,
11483
- ),
11484
- conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
11485
- normalizedIntent: resolvedIntentType,
11486
- aiReplyGenerated: processed.result?.ai_reply_generated === true,
11487
- aiReplyGeneratedAt: String(processed.result?.ai_reply_generated_at || "").trim(),
11488
- aiReplyPreview: String(processed.result?.ai_reply_preview || "").trim(),
11489
- responseContractValidationStatus: String(processed.result?.response_contract_validation_status || "").trim(),
11490
- responseContractValidationReason: String(processed.result?.response_contract_validation_reason || "").trim(),
11491
- responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
11492
- assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
11493
- assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
11494
- assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
11495
- deliveryStatus: String(processed.result?.delivery_status || "").trim(),
11496
- archiveStatus: String(processed.result?.archive_status || "").trim(),
11497
- transportError: String(processed.result?.transport_error || "").trim(),
11498
- archiveError: String(processed.result?.archive_error || "").trim(),
11499
- });
11500
- if (processed.kind !== "delivery_failed") {
11501
- await ensureRunnerRootWorkItemForRequest({
11502
- normalizedRoute: deferredExecution.normalizedRoute,
11503
- routeKey: deferredExecution.routeKey,
11504
- selectedRecord: deferredExecution.selectedRecord,
11505
- runtime: deferredExecution.runtime,
11506
- requestKey: deferredExecution.requestKey,
11507
- });
11508
- }
11509
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11510
- normalizedRoute: deferredExecution.normalizedRoute,
11511
- runtime: deferredExecution.runtime,
11512
- requestKey: deferredExecution.requestKey,
11513
- });
11514
- const syncedRequest = safeObject(rootWorkItemSync.request);
11515
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11516
- saveRunnerRouteState(deferredExecution.routeKey, {
11517
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11518
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11519
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11520
- });
11521
- }
11522
- await syncRunnerRequestLedgerForProjectToServer({
11523
- normalizedRoute: deferredExecution.normalizedRoute,
11524
- runtime: deferredExecution.runtime,
11525
- });
11526
- }
11527
- return {
11528
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11529
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11530
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11531
- ...processed.result,
11532
- };
11533
- } catch (err) {
11534
- const currentRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
11535
- const errorText = String(err?.message || err).trim();
11536
- saveRunnerRouteState(deferredExecution.routeKey, {
11537
- ...currentRouteState,
11538
- ...emptyRunnerActiveExecutionPatch(),
11539
- last_error: errorText,
11540
- });
11541
- if (String(deferredExecution.requestKey || "").trim()) {
11542
- const currentRequest = safeObject(loadRunnerRequestByKey(deferredExecution.requestKey));
11543
- const resolvedIntentType = String(
11544
- safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type
11545
- || currentRequest.normalized_intent
11546
- || "",
11547
- ).trim();
11548
- markRunnerRequestLifecycle({
11549
- normalizedRoute: deferredExecution.normalizedRoute,
11550
- requestKey: deferredExecution.requestKey,
11551
- selectedRecord: deferredExecution.selectedRecord,
11552
- routeKey: deferredExecution.routeKey,
11553
- outcome: "error",
11554
- closedReason: errorText || "execution_error",
11555
- normalizedIntent: resolvedIntentType,
11556
- });
11557
- await ensureRunnerRootWorkItemForRequest({
11558
- normalizedRoute: deferredExecution.normalizedRoute,
11559
- routeKey: deferredExecution.routeKey,
11560
- selectedRecord: deferredExecution.selectedRecord,
11561
- runtime: deferredExecution.runtime,
11562
- requestKey: deferredExecution.requestKey,
11563
- });
11564
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11565
- normalizedRoute: deferredExecution.normalizedRoute,
11566
- runtime: deferredExecution.runtime,
11567
- requestKey: deferredExecution.requestKey,
11568
- });
11569
- const syncedRequest = safeObject(rootWorkItemSync.request);
11570
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11571
- saveRunnerRouteState(deferredExecution.routeKey, {
11572
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11573
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11574
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11575
- });
11576
- }
11577
- await syncRunnerRequestLedgerForProjectToServer({
11578
- normalizedRoute: deferredExecution.normalizedRoute,
11579
- runtime: deferredExecution.runtime,
11580
- });
11581
- }
11582
- return {
11583
- route_key: deferredExecution.routeKey,
11584
- route_name: deferredExecution.normalizedRoute.name,
11585
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11586
- outcome: "error",
11587
- detail: errorText,
11588
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11589
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11590
- thread_id: deferredExecution.archiveThread.threadID,
11591
- comment_id: deferredExecution.selectedRecord.id,
11592
- };
11593
- } finally {
11594
- executionHeartbeat.stop();
11595
- inFlightExecutions.delete(deferredExecution.routeKey);
11596
- schedules.set(deferredExecution.routeKey, Date.now() + 250);
11597
- tui?.updateNextRunAt(deferredExecution.routeKey, Date.now() + 250);
11598
- }
11599
- })();
11600
- inFlightExecutions.set(routeKey, executionPromise);
11601
- executionPromise.then((finalResult) => {
11602
- const latestRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
11603
- runnerLogger?.append("execution_result", {
11604
- route_key: String(finalResult?.route_key || deferredExecution.routeKey).trim(),
11605
- route_name: String(finalResult?.route_name || deferredExecution.normalizedRoute?.name || "").trim(),
11606
- outcome: String(finalResult?.outcome || "").trim(),
11607
- detail: String(finalResult?.detail || "").trim(),
11608
- comment_id: String(finalResult?.comment_id || deferredExecution.selectedRecord?.id || "").trim(),
11609
- source_message_id: intFromRawAllowZero(latestRouteState?.last_source_message_id, intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0)),
11610
- intent_type: String(latestRouteState?.last_intent_type || "").trim(),
11611
- context_suggestion_status: String(finalResult?.context_suggestion_status || latestRouteState?.last_context_suggestion_status || "").trim(),
11612
- });
11613
- if (tui) {
11614
- tui.recordResult(finalResult, latestRouteState);
11615
- } else {
11616
- printRunnerResult("start", finalResult, jsonMode);
11617
- }
12549
+ await syncRunnerDeferredExecutionRunningState(deferredExecution);
12550
+ const reportRunnerStage = createRunnerDeferredExecutionStageReporter({
12551
+ deferredExecution,
12552
+ runnerLogger,
12553
+ tui,
11618
12554
  });
12555
+ const executionPromise = createRunnerDeferredExecutionPromise({
12556
+ deferredExecution,
12557
+ executionHeartbeat,
12558
+ inFlightExecutions,
12559
+ schedules,
12560
+ tui,
12561
+ runnerLogger,
12562
+ jsonMode,
12563
+ reportRunnerStage,
12564
+ });
12565
+ inFlightExecutions.set(routeKey, executionPromise);
11619
12566
  const acceptedResult = {
11620
12567
  ...result,
11621
12568
  deferred_execution: undefined,
11622
12569
  };
11623
12570
  cycleOutcome = String(acceptedResult?.outcome || "accepted").trim().toLowerCase() || "accepted";
11624
- runnerLogger?.append("route_result", {
11625
- route_key: String(acceptedResult?.route_key || routeKey).trim(),
11626
- route_name: String(acceptedResult?.route_name || normalizedRoute.name || "").trim(),
11627
- outcome: String(acceptedResult?.outcome || "").trim(),
11628
- detail: String(acceptedResult?.detail || "").trim(),
12571
+ publishRunnerStartImmediateResult({
12572
+ result: acceptedResult,
12573
+ routeKey,
12574
+ normalizedRoute,
12575
+ runnerLogger,
12576
+ tui,
12577
+ jsonMode,
11629
12578
  });
11630
- if (tui) {
11631
- tui.recordResult(acceptedResult);
11632
- } else {
11633
- printRunnerResult("start", acceptedResult, jsonMode);
11634
- }
11635
- } else if (result.outcome !== "busy") {
11636
- runnerLogger?.append("route_result", {
11637
- route_key: String(result?.route_key || routeKey).trim(),
11638
- route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
11639
- outcome: String(result?.outcome || "").trim(),
11640
- detail: String(result?.detail || "").trim(),
11641
- comment_id: String(result?.comment_id || "").trim(),
11642
- });
11643
- if (tui) {
11644
- tui.recordResult(result);
11645
- } else {
11646
- printRunnerResult("start", result, jsonMode);
11647
- }
11648
12579
  } else {
11649
- runnerLogger?.append("route_result", {
11650
- route_key: String(result?.route_key || routeKey).trim(),
11651
- route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
11652
- outcome: "busy",
11653
- detail: String(result?.detail || "").trim(),
11654
- comment_id: String(result?.comment_id || "").trim(),
12580
+ publishRunnerStartImmediateResult({
12581
+ result,
12582
+ routeKey,
12583
+ normalizedRoute,
12584
+ runnerLogger,
12585
+ tui,
12586
+ jsonMode,
11655
12587
  });
11656
- tui?.recordResult(result);
11657
12588
  }
11658
12589
  } catch (err) {
11659
- const errorText = String(err?.message || err);
11660
- const fatalArchiveBootstrapError = errorText.includes("Archive thread is missing")
11661
- && errorText.includes("write access is denied");
11662
- cycleOutcome = fatalArchiveBootstrapError ? "blocked" : "error";
11663
- saveRunnerRouteState(routeKey, {
11664
- ...safeObject(loadBotRunnerState().routes[routeKey]),
11665
- last_error: errorText,
11666
- });
11667
- const result = {
11668
- route_key: routeKey,
11669
- route_name: normalizedRoute.name,
11670
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11671
- outcome: fatalArchiveBootstrapError ? "blocked" : "error",
11672
- detail: errorText,
11673
- };
11674
- runnerLogger?.append("route_result", {
11675
- route_key: routeKey,
11676
- route_name: normalizedRoute.name,
11677
- outcome: String(result.outcome || "error").trim(),
11678
- detail: errorText,
12590
+ cycleOutcome = handleRunnerStartRouteCycleError({
12591
+ normalizedRoute,
12592
+ routeKey,
12593
+ errorText: String(err?.message || err),
12594
+ runnerLogger,
12595
+ tui,
12596
+ jsonMode,
11679
12597
  });
11680
- if (tui) {
11681
- tui.recordResult(result);
11682
- } else {
11683
- printRunnerResult("start", result, jsonMode);
11684
- }
11685
12598
  } finally {
11686
- let routeState = safeObject(loadBotRunnerState().routes[routeKey]);
11687
- let activeExecutionState = resolveRunnerActiveExecutionState(routeState);
11688
- const successfulCycleOutcome = [
11689
- "accepted",
11690
- "busy",
11691
- "idle",
11692
- "primed",
11693
- "skipped",
11694
- "replied",
11695
- "dry_run",
11696
- "delivery_failed_after_generation",
11697
- ].includes(cycleOutcome);
11698
- const shouldClearLastError = successfulCycleOutcome
11699
- && String(routeState.last_error || "").trim();
11700
- const shouldClearStaleActiveExecution = !activeExecutionState.active
11701
- && successfulCycleOutcome
11702
- && String(routeState.active_comment_id || "").trim();
11703
- if (shouldClearLastError || shouldClearStaleActiveExecution) {
11704
- saveRunnerRouteState(routeKey, {
11705
- ...(shouldClearStaleActiveExecution ? emptyRunnerActiveExecutionPatch() : {}),
11706
- ...(shouldClearLastError ? { last_error: "" } : {}),
11707
- });
11708
- routeState = safeObject(loadBotRunnerState().routes[routeKey]);
11709
- activeExecutionState = resolveRunnerActiveExecutionState(routeState);
11710
- }
11711
- tui?.setRouteState(routeKey, {
11712
- intent_type: String(routeState.last_intent_type || "").trim(),
11713
- source_message_id: intFromRawAllowZero(routeState.last_source_message_id, 0),
11714
- warning: String(activeExecutionState.warning || "").trim(),
11715
- last_error: String(routeState.last_error || "").trim(),
12599
+ finalizeRunnerStartRouteCycle({
12600
+ routeKey,
12601
+ normalizedRoute,
12602
+ cycleOutcome,
12603
+ schedules,
12604
+ tui,
11716
12605
  });
11717
- const lastErrorText = String(routeState.last_error || "").trim();
11718
- const isFatalArchiveBootstrapError = lastErrorText.includes("Archive thread is missing")
11719
- && lastErrorText.includes("write access is denied");
11720
- const nextRunAt = Date.now() + (isFatalArchiveBootstrapError
11721
- ? 60000
11722
- : Math.max(1000, normalizedRoute.pollIntervalMs));
11723
- schedules.set(routeKey, nextRunAt);
11724
- tui?.updateNextRunAt(routeKey, nextRunAt);
11725
12606
  }
11726
12607
  }
11727
12608
  });
11728
12609
  }
11729
- for (const route of routes) {
11730
- const normalizedRoute = normalizeRunnerRoute(route);
11731
- const routeKey = runnerRouteKey(normalizedRoute);
11732
- const nextAt = Number(schedules.get(routeKey) || 0);
11733
- if (nextAt > Date.now()) {
11734
- nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - Date.now()));
11735
- }
11736
- }
12610
+ nextSleepMs = refreshRunnerStartNextSleepMs(routes, schedules, nextSleepMs, Date.now());
11737
12611
  if (!stopRequested) {
11738
12612
  await sleep(Math.max(250, nextSleepMs));
11739
12613
  }