metheus-governance-mcp-cli 0.2.230 → 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),
@@ -2430,26 +2463,75 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2430
2463
  next_expected_responders: ensureArray(entry.next_expected_responders || entry.nextExpectedResponders)
2431
2464
  .map((value) => normalizeTelegramMentionUsername(value))
2432
2465
  .filter(Boolean),
2466
+ normalized_execution_contract_type: String(entry.normalized_execution_contract_type || entry.normalizedExecutionContractType || "").trim().toLowerCase(),
2467
+ normalized_execution_contract_targets: ensureArray(entry.normalized_execution_contract_targets || entry.normalizedExecutionContractTargets)
2468
+ .map((value) => normalizeTelegramMentionUsername(value))
2469
+ .filter(Boolean),
2470
+ normalized_execution_next_responders: ensureArray(entry.normalized_execution_next_responders || entry.normalizedExecutionNextResponders)
2471
+ .map((value) => normalizeTelegramMentionUsername(value))
2472
+ .filter(Boolean),
2433
2473
  ai_reply_generated: boolFromRaw(
2434
2474
  entry.ai_reply_generated ?? entry.aiReplyGenerated,
2435
2475
  false,
2436
2476
  ),
2437
2477
  ai_reply_generated_at: firstNonEmptyString([entry.ai_reply_generated_at, entry.aiReplyGeneratedAt]),
2438
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(),
2480
+ root_execution_contract_type: String(entry.root_execution_contract_type || entry.rootExecutionContractType || "").trim().toLowerCase(),
2481
+ root_execution_contract_targets: ensureArray(entry.root_execution_contract_targets || entry.rootExecutionContractTargets)
2482
+ .map((value) => normalizeTelegramMentionUsername(value))
2483
+ .filter(Boolean),
2484
+ root_next_expected_responders: ensureArray(entry.root_next_expected_responders || entry.rootNextExpectedResponders)
2485
+ .map((value) => normalizeTelegramMentionUsername(value))
2486
+ .filter(Boolean),
2487
+ root_ai_reply_preview: String(entry.root_ai_reply_preview || entry.rootAiReplyPreview || "").trim(),
2488
+ root_response_contract_validation_status: String(entry.root_response_contract_validation_status || entry.rootResponseContractValidationStatus || "").trim().toLowerCase(),
2489
+ root_response_contract_validation_reason: String(entry.root_response_contract_validation_reason || entry.rootResponseContractValidationReason || "").trim(),
2490
+ root_response_contract_validation_targets: ensureArray(entry.root_response_contract_validation_targets || entry.rootResponseContractValidationTargets)
2491
+ .map((value) => normalizeTelegramMentionUsername(value))
2492
+ .filter(Boolean),
2439
2493
  response_contract_validation_status: String(entry.response_contract_validation_status || entry.responseContractValidationStatus || "").trim().toLowerCase(),
2440
2494
  response_contract_validation_reason: String(entry.response_contract_validation_reason || entry.responseContractValidationReason || "").trim(),
2441
2495
  response_contract_validation_targets: ensureArray(entry.response_contract_validation_targets || entry.responseContractValidationTargets)
2442
2496
  .map((value) => normalizeTelegramMentionUsername(value))
2443
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),
2444
2517
  assignment_validation_status: String(entry.assignment_validation_status || entry.assignmentValidationStatus || "").trim().toLowerCase(),
2445
2518
  assignment_validation_reason: String(entry.assignment_validation_reason || entry.assignmentValidationReason || "").trim(),
2446
2519
  assignment_validation_modes: ensureArray(entry.assignment_validation_modes || entry.assignmentValidationModes)
2447
2520
  .map((value) => String(value || "").trim().toLowerCase())
2448
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),
2449
2527
  delivery_status: String(entry.delivery_status || entry.deliveryStatus || "").trim().toLowerCase(),
2450
2528
  archive_status: String(entry.archive_status || entry.archiveStatus || "").trim().toLowerCase(),
2451
2529
  transport_error: String(entry.transport_error || entry.transportError || "").trim(),
2452
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(),
2453
2535
  normalized_intent: String(entry.normalized_intent || entry.normalizedIntent || "").trim().toLowerCase(),
2454
2536
  status,
2455
2537
  claimed_by_route: String(entry.claimed_by_route || entry.claimedByRoute || "").trim(),
@@ -3031,11 +3113,341 @@ function runnerRequestHasConversationContractData(entryRaw) {
3031
3113
  || ensureArray(entry.conversation_allowed_responders).length
3032
3114
  || entry.conversation_allow_bot_to_bot === true
3033
3115
  || String(entry.conversation_reply_expectation || "").trim()
3034
- || String(entry.execution_contract_type || "").trim()
3035
- || entry.execution_contract_actionable === true
3036
- || ensureArray(entry.execution_contract_targets).length
3037
- || 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(),
3038
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
+ };
3039
3451
  }
3040
3452
 
3041
3453
  function pickRunnerSharedConversationSourceRequest(entries = [], excludeRequestKey = "") {
@@ -3628,28 +4040,26 @@ async function claimRunnerRequestForHumanComment({
3628
4040
  || referencedRequest.conversation_reply_expectation
3629
4041
  || "",
3630
4042
  ).trim().toLowerCase(),
3631
- execution_contract_type: String(
3632
- existing.execution_contract_type || sharedConversationSource.execution_contract_type || referencedRequest.execution_contract_type || "",
3633
- ).trim().toLowerCase(),
3634
- execution_contract_actionable: existing.execution_contract_actionable === true
3635
- || sharedConversationSource.execution_contract_actionable === true
3636
- || 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),
3637
4049
  execution_contract_targets: uniqueOrderedStrings(
3638
- ensureArray(existing.execution_contract_targets).length
3639
- ? existing.execution_contract_targets
3640
- : ensureArray(sharedConversationSource.execution_contract_targets).length
3641
- ? sharedConversationSource.execution_contract_targets
3642
- : referencedRequest.execution_contract_targets,
4050
+ runnerRequestPreferredExecutionContractTargets(existing).length
4051
+ ? runnerRequestPreferredExecutionContractTargets(existing)
4052
+ : runnerRequestPreferredExecutionContractTargets(sharedConversationSource).length
4053
+ ? runnerRequestPreferredExecutionContractTargets(sharedConversationSource)
4054
+ : runnerRequestPreferredExecutionContractTargets(referencedRequest),
3643
4055
  normalizeTelegramMentionUsername,
3644
4056
  ),
3645
4057
  next_expected_responders: uniqueOrderedStrings(
3646
- ensureArray(existing.next_expected_responders).length
3647
- ? existing.next_expected_responders
3648
- : ensureArray(sharedConversationSource.next_expected_responders).length
3649
- ? sharedConversationSource.next_expected_responders
3650
- : ensureArray(referencedRequest.next_expected_responders).length
3651
- ? referencedRequest.next_expected_responders
3652
- : [],
4058
+ runnerRequestPreferredNextExpectedResponders(existing).length
4059
+ ? runnerRequestPreferredNextExpectedResponders(existing)
4060
+ : runnerRequestPreferredNextExpectedResponders(sharedConversationSource).length
4061
+ ? runnerRequestPreferredNextExpectedResponders(sharedConversationSource)
4062
+ : runnerRequestPreferredNextExpectedResponders(referencedRequest),
3653
4063
  normalizeTelegramMentionUsername,
3654
4064
  ),
3655
4065
  normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
@@ -3698,92 +4108,15 @@ async function claimRunnerRequestForHumanComment({
3698
4108
  };
3699
4109
  }
3700
4110
 
3701
- function isActionableRunnerRequestIntent(rawIntent) {
3702
- const normalizedIntent = String(rawIntent || "").trim().toLowerCase();
3703
- return Boolean(normalizedIntent) && !isInformationalRunnerRequestIntent(normalizedIntent);
3704
- }
3705
-
3706
- function runnerRequestHasConversationContract(requestRaw) {
3707
- const request = safeObject(requestRaw);
3708
- return Boolean(
3709
- String(request.conversation_id || "").trim()
3710
- || String(request.conversation_intent_mode || "").trim()
3711
- || String(request.conversation_reply_expectation || "").trim()
3712
- || ensureArray(request.conversation_participants).length
3713
- || ensureArray(request.conversation_initial_responders).length
3714
- || ensureArray(request.conversation_allowed_responders).length
3715
- );
3716
- }
3717
-
3718
- function runnerRequestHasCompleteConversationContract(requestRaw) {
3719
- const request = safeObject(requestRaw);
3720
- const intentMode = String(request.conversation_intent_mode || "").trim().toLowerCase();
3721
- const conversationID = String(request.conversation_id || "").trim();
3722
- const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3723
- const participants = ensureArray(request.conversation_participants);
3724
- const initialResponders = ensureArray(request.conversation_initial_responders);
3725
- const allowedResponders = ensureArray(request.conversation_allowed_responders);
3726
- if (!runnerRequestHasConversationContract(request)) {
3727
- return false;
3728
- }
3729
- if (!conversationID || !intentMode || !replyExpectation) {
3730
- return false;
3731
- }
3732
- if (intentMode === "single_bot") {
3733
- return allowedResponders.length > 0 || participants.length > 0;
3734
- }
3735
- return participants.length > 0 && initialResponders.length > 0 && allowedResponders.length > 0;
3736
- }
3737
-
3738
- function runnerRequestHasExecutionContract(requestRaw) {
3739
- const request = safeObject(requestRaw);
3740
- return Boolean(
3741
- String(request.execution_contract_type || "").trim()
3742
- || request.execution_contract_actionable === true
3743
- || ensureArray(request.execution_contract_targets).length
3744
- || ensureArray(request.next_expected_responders).length
3745
- );
3746
- }
3747
-
3748
- function runnerRequestHasCompleteExecutionContract(requestRaw) {
3749
- const request = safeObject(requestRaw);
3750
- const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3751
- const executionTargets = ensureArray(request.execution_contract_targets);
3752
- const nextExpectedResponders = ensureArray(request.next_expected_responders);
3753
- if (!runnerRequestHasExecutionContract(request)) {
3754
- return false;
3755
- }
3756
- if (!executionContractType) {
3757
- return false;
3758
- }
3759
- if (executionContractType === "delegation") {
3760
- return executionTargets.length > 0 || nextExpectedResponders.length > 0;
3761
- }
3762
- if (executionContractType === "summary_request") {
3763
- return nextExpectedResponders.length > 0 || executionTargets.length > 0;
3764
- }
3765
- return true;
3766
- }
3767
-
3768
- function runnerRequestHasContractSignals(requestRaw) {
3769
- const request = safeObject(requestRaw);
3770
- return runnerRequestHasConversationContract(request) || runnerRequestHasExecutionContract(request);
3771
- }
3772
-
3773
4111
  function runnerRequestRequiresActionableContract(requestRaw) {
3774
4112
  const request = safeObject(requestRaw);
3775
- const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3776
- const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3777
- const intentMode = String(request.conversation_intent_mode || "").trim().toLowerCase();
3778
- if (request.execution_contract_actionable === true) {
4113
+ const executionContractType = runnerRequestPreferredExecutionContractType(request);
4114
+ if (runnerRequestPreferredExecutionContractActionable(request) === true) {
3779
4115
  return true;
3780
4116
  }
3781
4117
  if (["delegation", "direct_result", "summary_request", "final_summary"].includes(executionContractType)) {
3782
4118
  return true;
3783
4119
  }
3784
- if (intentMode === "single_bot" && replyExpectation === "actionable") {
3785
- return true;
3786
- }
3787
4120
  return false;
3788
4121
  }
3789
4122
 
@@ -3816,13 +4149,9 @@ function truncateRunnerWorkItemTitleText(rawText, maxLength = 96) {
3816
4149
 
3817
4150
  function buildRunnerRootWorkItemTitle({ selectedRecord, request }) {
3818
4151
  const parsed = safeObject(selectedRecord?.parsedArchive);
3819
- const intent = String(safeObject(request).normalized_intent || "").trim().toLowerCase();
4152
+ void request;
3820
4153
  const requestBody = truncateRunnerWorkItemTitleText(parsed.body || "", 84);
3821
- const prefix = intent === "ctxpack_mutation"
3822
- ? "Ctxpack request"
3823
- : intent === "workitem_mutation"
3824
- ? "Work item request"
3825
- : "Runner request";
4154
+ const prefix = "Runner request";
3826
4155
  if (requestBody) {
3827
4156
  return `${prefix}: ${requestBody}`;
3828
4157
  }
@@ -4270,6 +4599,52 @@ function buildRunnerRootWorkItemTransitionPath(currentStatusRaw, targetStatusRaw
4270
4599
  return [];
4271
4600
  }
4272
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
+
4273
4648
  function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestRaw, routeKeyHint = "") {
4274
4649
  const currentState = safeObject(currentStateRaw);
4275
4650
  const request = safeObject(requestRaw);
@@ -4314,6 +4689,44 @@ function buildRunnerRequestRecoveryPatchFromRouteState(currentStateRaw, requestR
4314
4689
  || routeState.last_root_work_item_status,
4315
4690
  );
4316
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"]);
4317
4730
  const requestStatus = normalizeRunnerRequestStatus(request.status);
4318
4731
  if (!isFinalRunnerRequestStatus(requestStatus)) {
4319
4732
  const routeAction = String(routeState.last_action || "").trim().toLowerCase();
@@ -4597,6 +5010,9 @@ function markRunnerRequestLifecycle({
4597
5010
  aiReplyGenerated = false,
4598
5011
  aiReplyGeneratedAt = "",
4599
5012
  aiReplyPreview = "",
5013
+ normalizedExecutionContractType = "",
5014
+ normalizedExecutionContractTargets = [],
5015
+ normalizedExecutionNextResponders = [],
4600
5016
  responseContractValidationStatus = "",
4601
5017
  responseContractValidationReason = "",
4602
5018
  responseContractValidationTargets = [],
@@ -4668,6 +5084,9 @@ function markRunnerRequestLifecycle({
4668
5084
  return normalizeRunnerRequestStatus(existing.status);
4669
5085
  })();
4670
5086
  const nowISO = new Date().toISOString();
5087
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
5088
+ const isRootHumanComment = ["telegram_message", "telegram_edited_message"].includes(commentKind);
5089
+ const isFollowupComment = !isRootHumanComment;
4671
5090
  const patch = {
4672
5091
  conversation_id: conversationID,
4673
5092
  conversation_participants: uniqueOrderedStrings(
@@ -4713,39 +5132,217 @@ function markRunnerRequestLifecycle({
4713
5132
  ensureArray(nextExpectedResponders).length ? nextExpectedResponders : existing.next_expected_responders,
4714
5133
  normalizeTelegramMentionUsername,
4715
5134
  ),
5135
+ normalized_execution_contract_type: String(
5136
+ normalizedExecutionContractType || existing.normalized_execution_contract_type || "",
5137
+ ).trim().toLowerCase(),
5138
+ normalized_execution_contract_targets: uniqueOrderedStrings(
5139
+ ensureArray(normalizedExecutionContractTargets).length
5140
+ ? normalizedExecutionContractTargets
5141
+ : existing.normalized_execution_contract_targets,
5142
+ normalizeTelegramMentionUsername,
5143
+ ),
5144
+ normalized_execution_next_responders: uniqueOrderedStrings(
5145
+ ensureArray(normalizedExecutionNextResponders).length
5146
+ ? normalizedExecutionNextResponders
5147
+ : existing.normalized_execution_next_responders,
5148
+ normalizeTelegramMentionUsername,
5149
+ ),
4716
5150
  ai_reply_generated: aiReplyGenerated === true || existing.ai_reply_generated === true,
4717
5151
  ai_reply_generated_at: aiReplyGenerated === true
4718
5152
  ? firstNonEmptyString([aiReplyGeneratedAt, existing.ai_reply_generated_at, nowISO])
4719
5153
  : String(existing.ai_reply_generated_at || "").trim(),
4720
- ai_reply_preview: String(aiReplyPreview || existing.ai_reply_preview || "").trim(),
4721
- response_contract_validation_status: String(
4722
- responseContractValidationStatus || existing.response_contract_validation_status || "",
4723
- ).trim().toLowerCase(),
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(),
5164
+ root_execution_contract_type: String(
5165
+ isRootHumanComment
5166
+ ? nextExecutionContractType
5167
+ : existing.root_execution_contract_type || existing.execution_contract_type || "",
5168
+ ).trim().toLowerCase(),
5169
+ root_execution_contract_targets: uniqueOrderedStrings(
5170
+ isRootHumanComment && ensureArray(executionContractTargets).length
5171
+ ? executionContractTargets
5172
+ : ensureArray(existing.root_execution_contract_targets).length
5173
+ ? existing.root_execution_contract_targets
5174
+ : existing.execution_contract_targets,
5175
+ normalizeTelegramMentionUsername,
5176
+ ),
5177
+ root_next_expected_responders: uniqueOrderedStrings(
5178
+ isRootHumanComment && ensureArray(nextExpectedResponders).length
5179
+ ? nextExpectedResponders
5180
+ : ensureArray(existing.root_next_expected_responders).length
5181
+ ? existing.root_next_expected_responders
5182
+ : existing.next_expected_responders,
5183
+ normalizeTelegramMentionUsername,
5184
+ ),
5185
+ root_ai_reply_preview: String(
5186
+ isRootHumanComment
5187
+ ? aiReplyPreview || existing.root_ai_reply_preview || existing.ai_reply_preview || ""
5188
+ : existing.root_ai_reply_preview || existing.ai_reply_preview || "",
5189
+ ).trim(),
5190
+ root_response_contract_validation_status: String(
5191
+ isRootHumanComment
5192
+ ? responseContractValidationStatus || existing.root_response_contract_validation_status || existing.response_contract_validation_status || ""
5193
+ : existing.root_response_contract_validation_status || existing.response_contract_validation_status || "",
5194
+ ).trim().toLowerCase(),
5195
+ root_response_contract_validation_reason: String(
5196
+ isRootHumanComment
5197
+ ? responseContractValidationReason || existing.root_response_contract_validation_reason || existing.response_contract_validation_reason || ""
5198
+ : existing.root_response_contract_validation_reason || existing.response_contract_validation_reason || "",
5199
+ ).trim(),
5200
+ root_response_contract_validation_targets: uniqueOrderedStrings(
5201
+ isRootHumanComment && ensureArray(responseContractValidationTargets).length
5202
+ ? responseContractValidationTargets
5203
+ : ensureArray(existing.root_response_contract_validation_targets).length
5204
+ ? existing.root_response_contract_validation_targets
5205
+ : existing.response_contract_validation_targets,
5206
+ normalizeTelegramMentionUsername,
5207
+ ),
5208
+ response_contract_validation_status: String(
5209
+ isRootHumanComment
5210
+ ? responseContractValidationStatus || existing.response_contract_validation_status || ""
5211
+ : existing.response_contract_validation_status || "",
5212
+ ).trim().toLowerCase(),
4724
5213
  response_contract_validation_reason: String(
4725
- responseContractValidationReason || existing.response_contract_validation_reason || "",
5214
+ isRootHumanComment
5215
+ ? responseContractValidationReason || existing.response_contract_validation_reason || ""
5216
+ : existing.response_contract_validation_reason || "",
4726
5217
  ).trim(),
4727
5218
  response_contract_validation_targets: uniqueOrderedStrings(
4728
- ensureArray(responseContractValidationTargets).length
5219
+ isRootHumanComment && ensureArray(responseContractValidationTargets).length
4729
5220
  ? responseContractValidationTargets
4730
5221
  : existing.response_contract_validation_targets,
4731
5222
  normalizeTelegramMentionUsername,
4732
5223
  ),
5224
+ followup_execution_contract_type: String(
5225
+ isFollowupComment
5226
+ ? nextExecutionContractType || existing.followup_execution_contract_type || ""
5227
+ : existing.followup_execution_contract_type || "",
5228
+ ).trim().toLowerCase(),
5229
+ followup_execution_contract_targets: uniqueOrderedStrings(
5230
+ isFollowupComment && ensureArray(executionContractTargets).length
5231
+ ? executionContractTargets
5232
+ : existing.followup_execution_contract_targets,
5233
+ normalizeTelegramMentionUsername,
5234
+ ),
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
+ ),
4733
5274
  assignment_validation_status: String(
4734
- assignmentValidationStatus || existing.assignment_validation_status || "",
5275
+ isRootHumanComment
5276
+ ? assignmentValidationStatus || existing.assignment_validation_status || ""
5277
+ : existing.assignment_validation_status || "",
4735
5278
  ).trim().toLowerCase(),
4736
5279
  assignment_validation_reason: String(
4737
- assignmentValidationReason || existing.assignment_validation_reason || "",
5280
+ isRootHumanComment
5281
+ ? assignmentValidationReason || existing.assignment_validation_reason || ""
5282
+ : existing.assignment_validation_reason || "",
4738
5283
  ).trim(),
4739
5284
  assignment_validation_modes: uniqueOrderedStrings(
4740
- ensureArray(assignmentValidationModes).length
5285
+ isRootHumanComment && ensureArray(assignmentValidationModes).length
4741
5286
  ? assignmentValidationModes
4742
5287
  : existing.assignment_validation_modes,
4743
5288
  (value) => String(value || "").trim().toLowerCase(),
4744
5289
  ),
4745
- delivery_status: String(deliveryStatus || existing.delivery_status || "").trim().toLowerCase(),
4746
- archive_status: String(archiveStatus || existing.archive_status || "").trim().toLowerCase(),
4747
- transport_error: String(transportError || existing.transport_error || "").trim(),
4748
- archive_error: String(archiveError || existing.archive_error || "").trim(),
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(),
4749
5346
  normalized_intent: nextNormalizedIntent,
4750
5347
  status: nextStatus,
4751
5348
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -4962,23 +5559,55 @@ function mergeRunnerRequestForServerHydration(localEntryRaw, serverEntryRaw) {
4962
5559
  preserveLocalStringWhenServerBlank("conversation_summary_bot");
4963
5560
  preserveLocalStringWhenServerBlank("conversation_reply_expectation");
4964
5561
  preserveLocalStringWhenServerBlank("execution_contract_type");
5562
+ preserveLocalStringWhenServerBlank("normalized_execution_contract_type");
5563
+ preserveLocalStringWhenServerBlank("ai_reply_generated_at");
5564
+ preserveLocalStringWhenServerBlank("ai_reply_preview");
4965
5565
  preserveLocalStringWhenServerBlank("root_work_item_id");
4966
5566
  preserveLocalStringWhenServerBlank("root_work_item_title");
4967
5567
  preserveLocalStringWhenServerBlank("root_work_item_status");
4968
5568
  preserveLocalStringWhenServerBlank("root_thread_id");
4969
5569
  preserveLocalStringWhenServerBlank("root_work_item_created_at");
4970
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");
4971
5586
  preserveLocalArrayWhenServerEmpty("conversation_participants");
4972
5587
  preserveLocalArrayWhenServerEmpty("conversation_initial_responders");
4973
5588
  preserveLocalArrayWhenServerEmpty("conversation_allowed_responders");
4974
5589
  preserveLocalArrayWhenServerEmpty("execution_contract_targets");
4975
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");
4976
5602
  if (serverEntry.conversation_allow_bot_to_bot !== true && localEntry.conversation_allow_bot_to_bot === true) {
4977
5603
  merged.conversation_allow_bot_to_bot = true;
4978
5604
  }
4979
5605
  if (serverEntry.execution_contract_actionable !== true && localEntry.execution_contract_actionable === true) {
4980
5606
  merged.execution_contract_actionable = true;
4981
5607
  }
5608
+ if (serverEntry.ai_reply_generated !== true && localEntry.ai_reply_generated === true) {
5609
+ merged.ai_reply_generated = true;
5610
+ }
4982
5611
  const localStatus = normalizeRunnerRequestStatus(localEntry.status);
4983
5612
  const serverStatus = normalizeRunnerRequestStatus(serverEntry.status);
4984
5613
  if (isFinalRunnerRequestStatus(localStatus) && !isFinalRunnerRequestStatus(serverStatus)) {
@@ -7366,17 +7995,26 @@ function summarizeRunnerRequestForStatusLookup(entryRaw) {
7366
7995
  updated_at: String(entry.updated_at || "").trim(),
7367
7996
  source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
7368
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
+ }),
7369
8014
  selected_bot_usernames: ensureArray(entry.selected_bot_usernames)
7370
8015
  .map((value) => normalizeTelegramMentionUsername(value))
7371
8016
  .filter(Boolean),
7372
- root_work_item: String(entry.root_work_item_id || "").trim()
7373
- ? {
7374
- id: String(entry.root_work_item_id || "").trim(),
7375
- title: String(entry.root_work_item_title || "").trim(),
7376
- status: normalizeRunnerWorkItemStatus(entry.root_work_item_status),
7377
- thread_id: String(entry.root_thread_id || "").trim(),
7378
- }
7379
- : null,
8017
+ root_work_item: buildRunnerRequestRootWorkItemSummary(entry),
7380
8018
  };
7381
8019
  }
7382
8020
 
@@ -7435,6 +8073,150 @@ function pickPreferredStatusLookupRequest(entries = []) {
7435
8073
  return candidates[0];
7436
8074
  }
7437
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
+
7438
8220
  function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runnerStateOverride = null }) {
7439
8221
  const parsed = safeObject(selectedRecord?.parsedArchive);
7440
8222
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
@@ -7464,109 +8246,156 @@ function buildRunnerStatusQueryLookup({ route, routeState, selectedRecord, runne
7464
8246
  const replyChainContext = resolveRunnerReplyChainConversationContext(runnerState, route, selectedRecord);
7465
8247
  const currentConversationID = String(parsed.conversationID || replyChainContext.conversationID || "").trim();
7466
8248
  const activeRequestKey = String(safeObject(routeState).active_request_key || "").trim();
7467
- const requestMatchesCurrentRoute = (entry) => requestEligibleForStatusLookup(
7468
- entry,
8249
+ const { related_active_request: relatedActiveRequest, related_request: relatedRequest } = resolveRunnerStatusLookupRequests({
8250
+ runnerState,
8251
+ route,
7469
8252
  routeKey,
7470
8253
  selfBotUsername,
7471
8254
  currentMessageID,
7472
- );
7473
- const referencedRequestCandidate = safeObject(replyChainContext.referencedRequest);
7474
- let relatedActiveRequest = null;
7475
- let relatedRequest = null;
7476
- const selectors = currentConversationID
7477
- ? { conversationID: currentConversationID, chatID: currentChatID }
7478
- : { chatID: currentChatID };
7479
- let scopedRequests = findRunnerRequestsForScope(runnerState, route, selectors);
7480
- if (!scopedRequests.length && currentConversationID) {
7481
- scopedRequests = findRunnerRequestsForScope(runnerState, route, { chatID: currentChatID });
7482
- }
7483
- if (!scopedRequests.length && activeRequestKey) {
7484
- scopedRequests = findRunnerRequestsForScope(runnerState, route, { requestKey: activeRequestKey });
7485
- }
7486
- const eligibleScopedRequests = scopedRequests.filter(requestMatchesCurrentRoute);
7487
- const statusLookupCandidates = [...eligibleScopedRequests];
7488
- if (
7489
- Object.keys(referencedRequestCandidate).length > 0
7490
- && requestMatchesCurrentRoute(referencedRequestCandidate)
7491
- && !statusLookupCandidates.some(
7492
- (entry) => String(safeObject(entry).request_key || "").trim() === String(referencedRequestCandidate.request_key || "").trim(),
7493
- )
7494
- ) {
7495
- statusLookupCandidates.push(referencedRequestCandidate);
7496
- }
7497
- relatedActiveRequest = statusLookupCandidates
7498
- .filter((entry) => isActiveRunnerRequestStatus(entry.status))[0] || null;
7499
- relatedRequest = pickPreferredStatusLookupRequest(statusLookupCandidates);
7500
- const lastAction = String(safeObject(routeState).last_action || "").trim();
7501
- const lastReason = String(safeObject(routeState).last_reason || "").trim();
7502
- const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
7503
- const routeConversationID = String(safeObject(routeState).last_conversation_id || "").trim();
7504
- const activeRootWorkItemID = String(safeObject(routeState).active_root_work_item_id || "").trim();
7505
- const activeRootWorkItemTitle = String(safeObject(routeState).active_root_work_item_title || "").trim();
7506
- const activeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).active_root_work_item_status);
7507
- const routeRootWorkItemID = String(safeObject(routeState).last_root_work_item_id || "").trim();
7508
- const routeRootWorkItemTitle = String(safeObject(routeState).last_root_work_item_title || "").trim();
7509
- const routeRootWorkItemStatus = normalizeRunnerWorkItemStatus(safeObject(routeState).last_root_work_item_status);
7510
- const routeWorkItemIDs = ensureArray(safeObject(routeState).last_work_item_ids).map((item) => String(item || "").trim()).filter(Boolean);
7511
- const routeWorkItemTitles = ensureArray(safeObject(routeState).last_work_item_titles).map((item) => String(item || "").trim()).filter(Boolean);
7512
- const requestRootWorkItem = String(safeObject(relatedRequest).root_work_item_id || "").trim()
7513
- ? {
7514
- id: String(safeObject(relatedRequest).root_work_item_id || "").trim(),
7515
- title: String(safeObject(relatedRequest).root_work_item_title || "").trim(),
7516
- status: normalizeRunnerWorkItemStatus(safeObject(relatedRequest).root_work_item_status),
7517
- thread_id: String(safeObject(relatedRequest).root_thread_id || "").trim(),
7518
- }
7519
- : 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
+ });
7520
8266
  return {
7521
8267
  kind: "runner_status",
7522
8268
  status: (!selfBusyFiltered && activeExecution.active) || relatedActiveRequest
7523
8269
  ? "running"
7524
8270
  : String(safeObject(relatedRequest).status || "").trim() || "idle",
7525
8271
  resolved_conversation_id: currentConversationID,
7526
- reply_chain_resolution: {
7527
- reason: String(replyChainContext.reason || "").trim(),
7528
- reply_to_message_id: intFromRawAllowZero(replyChainContext.replyToMessageID, 0) || undefined,
7529
- anchor_message_id: intFromRawAllowZero(replyChainContext.anchorMessageID, 0) || undefined,
7530
- },
8272
+ reply_chain_resolution: buildRunnerStatusReplyChainResolution(replyChainContext),
7531
8273
  self_busy_filtered: selfBusyFiltered,
7532
- active_execution: activeExecution.active && !selfBusyFiltered
7533
- ? {
7534
- started_at: String(activeExecution.startedAt || "").trim(),
7535
- age_seconds: intFromRawAllowZero(activeExecution.ageSeconds, 0),
7536
- stale: activeExecution.stale === true,
7537
- stuck: activeExecution.stuck === true,
7538
- warning: String(activeExecution.warning || "").trim(),
7539
- }
7540
- : null,
8274
+ active_execution: buildRunnerStatusActiveExecutionSummary(activeExecution, selfBusyFiltered),
7541
8275
  related_active_request: relatedActiveRequest ? summarizeRunnerRequestForStatusLookup(relatedActiveRequest) : null,
7542
8276
  related_request: relatedRequest ? summarizeRunnerRequestForStatusLookup(relatedRequest) : null,
7543
- root_work_item: String(safeObject(requestRootWorkItem).id || "").trim()
7544
- ? requestRootWorkItem
7545
- : activeRootWorkItemID
7546
- ? {
7547
- id: activeRootWorkItemID,
7548
- title: activeRootWorkItemTitle,
7549
- status: activeRootWorkItemStatus,
7550
- }
7551
- : currentConversationID && routeConversationID === currentConversationID && routeRootWorkItemID
7552
- ? {
7553
- id: routeRootWorkItemID,
7554
- title: routeRootWorkItemTitle,
7555
- status: routeRootWorkItemStatus,
7556
- }
7557
- : null,
7558
- route_work_items: currentConversationID && routeConversationID === currentConversationID && (routeWorkItemIDs.length > 0 || routeWorkItemTitles.length > 0)
7559
- ? {
7560
- ids: routeWorkItemIDs,
7561
- titles: routeWorkItemTitles,
7562
- }
7563
- : null,
7564
- last_route_result: {
7565
- action: lastAction,
7566
- reason: lastReason,
7567
- intent_type: lastIntentType,
7568
- },
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),
7569
8394
  };
8395
+ if (executionOverride && typeof executionOverride === "object") {
8396
+ response.execution_override = executionOverride;
8397
+ }
8398
+ return response;
7570
8399
  }
7571
8400
 
7572
8401
  async function resolveInformationalQueryReply({
@@ -7580,20 +8409,19 @@ async function resolveInformationalQueryReply({
7580
8409
  const normalizedIntentType = String(intentType || "").trim();
7581
8410
  const messageText = String(safeObject(selectedRecord?.parsedArchive).body || "").trim();
7582
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;
7583
8423
  if (normalizedIntentType === "small_talk") {
7584
- return {
7585
- handled: true,
7586
- response_mode: "lookup_only",
7587
- reply: "",
7588
- source: "small_talk",
7589
- lookup: {
7590
- intent_type: "small_talk",
7591
- user_message: messageText,
7592
- bot_name: firstNonEmptyString([route?.botName, route?.serverBotName, route?.server_bot_name, route?.name]),
7593
- bot_role: String(route?.role || route?.roleProfile || "").trim(),
7594
- },
7595
- execution_override: executionOverride,
7596
- };
8424
+ return buildSmallTalkLookupResponse({ route, messageText, executionOverride });
7597
8425
  }
7598
8426
  /* Dead legacy direct small_talk branch kept commented for reference.
7599
8427
  if (false && normalizedIntentType === "small_talk") {
@@ -7605,41 +8433,31 @@ async function resolveInformationalQueryReply({
7605
8433
  }
7606
8434
  */
7607
8435
  if (normalizedIntentType === "workspace_query") {
8436
+ return buildProjectWorkspaceLookupResponse({
8437
+ route,
8438
+ executionPlan,
8439
+ executionOverride,
8440
+ });
7608
8441
  const workspace = resolveProjectWorkspaceBindingSummary(route?.projectID, executionPlan?.workspaceDir || route?.workspaceDir || "");
7609
8442
  const workspaceDir = String(workspace.workspace_dir || "").trim();
7610
8443
  const reply = workspaceDir
7611
8444
  ? `현재 이 프로젝트에 바인딩된 로컬 작업 폴더는 "${workspaceDir}" 입니다.`
7612
8445
  : "현재 이 프로젝트는 project-workspaces.json에 로컬 작업 폴더가 바인딩되어 있지 않습니다.";
7613
- return {
7614
- handled: true,
7615
- response_mode: "lookup_only",
7616
- reply: "",
7617
- source: "project.workspace",
7618
- lookup: {
7619
- ...workspace,
7620
- },
7621
- execution_override: executionOverride,
7622
- };
8446
+ return buildLookupOnlyInformationalReply("project.workspace", {
8447
+ ...workspace,
8448
+ }, executionOverride);
7623
8449
  }
7624
8450
  if (normalizedIntentType === "bot_role_query") {
7625
- const payload = buildProjectBotRolesPayload({
7626
- projectID: route?.projectID,
7627
- provider: route?.provider,
7628
- destinationID: route?.destinationID,
7629
- destinationLabel: route?.destinationLabel,
7630
- });
7631
- return {
7632
- handled: true,
7633
- response_mode: "lookup_only",
7634
- reply: "",
7635
- source: "project.bot_roles",
7636
- lookup: {
7637
- ...payload,
7638
- },
7639
- execution_override: executionOverride,
7640
- };
8451
+ return buildProjectBotRolesLookupResponse({ route, executionOverride });
7641
8452
  }
7642
8453
  if (normalizedIntentType === "status_query") {
8454
+ return buildRunnerStatusLookupOnlyResponse({
8455
+ route,
8456
+ routeState,
8457
+ selectedRecord,
8458
+ runtime,
8459
+ executionOverride,
8460
+ });
7643
8461
  let hydratedRunnerState = null;
7644
8462
  try {
7645
8463
  hydratedRunnerState = await hydrateRunnerRequestLedgerFromServer({
@@ -7647,18 +8465,13 @@ async function resolveInformationalQueryReply({
7647
8465
  runtime,
7648
8466
  });
7649
8467
  } catch {}
7650
- return {
7651
- handled: true,
7652
- source: "runner.status",
7653
- response_mode: "lookup_only",
7654
- reply: "",
7655
- lookup: buildRunnerStatusQueryLookup({
7656
- route,
7657
- routeState,
7658
- selectedRecord,
7659
- runnerStateOverride: hydratedRunnerState,
7660
- }),
7661
- };
8468
+ const lookup = buildRunnerStatusQueryLookup({
8469
+ route,
8470
+ routeState,
8471
+ selectedRecord,
8472
+ runnerStateOverride: hydratedRunnerState,
8473
+ });
8474
+ return buildLookupOnlyInformationalReply("runner.status", lookup, executionOverride);
7662
8475
  const activeExecution = resolveRunnerActiveExecutionState(routeState);
7663
8476
  if (activeExecution.active) {
7664
8477
  const startedAt = String(activeExecution.startedAt || "").trim();
@@ -7690,23 +8503,12 @@ async function resolveInformationalQueryReply({
7690
8503
  };
7691
8504
  }
7692
8505
  if (normalizedIntentType === "artifact_location_query") {
7693
- const payload = await locateProjectFilesForQuery({
7694
- siteBaseURL: runtime.baseURL,
7695
- projectID: route?.projectID,
7696
- token: runtime.token,
7697
- timeoutSeconds: runtime.timeoutSeconds,
7698
- query: messageText,
8506
+ return buildArtifactLocationLookupResponse({
8507
+ route,
8508
+ runtime,
8509
+ messageText,
8510
+ executionOverride,
7699
8511
  });
7700
- return {
7701
- handled: true,
7702
- response_mode: "lookup_only",
7703
- reply: "",
7704
- source: "project.file.locate",
7705
- lookup: {
7706
- ...payload,
7707
- },
7708
- execution_override: executionOverride,
7709
- };
7710
8512
  }
7711
8513
  return null;
7712
8514
  }
@@ -7717,7 +8519,6 @@ function buildRunnerExecutionDeps() {
7717
8519
  adjudicateRunnerStartupLoopWithAI,
7718
8520
  analyzeHumanConversationIntentWithAI,
7719
8521
  auditRoleExecutionPlanWithAI,
7720
- auditDirectHumanReplyWithAI,
7721
8522
  planRoleExecutionWithAI,
7722
8523
  repairRoleExecutionPlanWithAI,
7723
8524
  normalizeRunnerRoleProfileName,
@@ -8317,6 +9118,74 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8317
9118
  }
8318
9119
  return null;
8319
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
+ };
8320
9189
  const prepareRunnerRequestClaim = async (selectedRecord, selectedResponderSelectors = [], sharedHumanIntent = null) => {
8321
9190
  const parsed = safeObject(selectedRecord?.parsedArchive);
8322
9191
  const kind = String(parsed.kind || "").trim().toLowerCase();
@@ -8353,316 +9222,122 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8353
9222
  archiveThreadID: archiveThread.threadID,
8354
9223
  });
8355
9224
  };
8356
- if (deferExecution) {
8357
- const skippedRecords = [];
8358
- for (const selectedRecord of pending.pending) {
8359
- const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
8360
- if (duplicateArchivedSkip) {
8361
- saveRunnerRouteState(
8362
- routeKey,
8363
- buildRunnerRouteStateFromComment(selectedRecord, {
8364
- last_action: "duplicate_skipped",
8365
- last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
8366
- last_trigger: "archive_dedupe",
8367
- }),
8368
- );
8369
- skippedRecords.push(duplicateArchivedSkip);
8370
- continue;
8371
- }
8372
- const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
8373
- if (triggerDecision.shouldRespond !== true) {
8374
- saveRunnerRouteState(
8375
- routeKey,
8376
- buildRunnerRouteStateFromComment(selectedRecord, {
8377
- last_action: "trigger_skipped",
8378
- last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8379
- last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
8380
- }),
8381
- );
8382
- skippedRecords.push({
8383
- id: selectedRecord.id,
8384
- reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8385
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8386
- suppressDiagnostic: true,
8387
- });
8388
- continue;
8389
- }
8390
- const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
8391
- routeKey,
8392
- normalizedRoute,
8393
- selectedRecord,
8394
- pendingOrdered: pending.ordered,
8395
- bot,
8396
- managedConversationBots,
8397
- representativeExecutionPlan,
8398
- importOutcome,
8399
- archiveThread,
8400
- runtime,
8401
- suppressDiagnostic: representativeBotSelector
8402
- ? currentBotSelector !== representativeBotSelector
8403
- : false,
8404
- });
8405
- if (startupLoopSkipped) {
8406
- skippedRecords.push(startupLoopSkipped.skippedRecord);
8407
- continue;
8408
- }
8409
- const routingExecutionDeps = {
8410
- ...buildRunnerExecutionDeps(),
8411
- managedConversationBots,
8412
- resolveConversationPeerBots: resolveRunnerConversationPeers,
8413
- };
8414
- const sharedHumanIntentContext = await resolveHumanIntentContext({
8415
- selectedRecord,
8416
- normalizedRoute,
8417
- bot,
8418
- executionPlan,
8419
- deps: routingExecutionDeps,
8420
- });
8421
- if (safeObject(sharedHumanIntentContext).contractNeedsResolution === true) {
8422
- const reason = "human intent contract is incomplete and requires regeneration";
8423
- saveRunnerRouteState(
8424
- routeKey,
8425
- buildRunnerRouteStateFromComment(selectedRecord, {
8426
- last_action: "needs_contract",
8427
- last_reason: reason,
8428
- last_trigger: "human_intent_contract",
8429
- }),
8430
- );
8431
- skippedRecords.push({
8432
- id: selectedRecord.id,
8433
- reason,
8434
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8435
- });
8436
- continue;
8437
- }
8438
- const precomputedAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
8439
- selectedRecord,
8440
- normalizedRoute,
8441
- bot,
8442
- deps: routingExecutionDeps,
8443
- precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8444
- });
8445
- const adjudication = precomputedAdjudication || await resolveRunnerResponderAdjudication({
8446
- selectedRecord,
8447
- pendingOrdered: pending.ordered,
8448
- normalizedRoute,
8449
- bot,
8450
- executionPlan,
8451
- deps: routingExecutionDeps,
8452
- precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8453
- });
8454
- const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
8455
- .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
8456
- .includes(currentBotSelector);
8457
- if (!currentBotSelected) {
8458
- 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,
8459
9287
  routeKey,
8460
- buildRunnerRouteStateFromComment(selectedRecord, {
8461
- last_action: "adjudication_skipped",
8462
- last_reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8463
- last_trigger: "responder_adjudication",
8464
- }),
8465
- );
8466
- skippedRecords.push({
8467
- id: selectedRecord.id,
8468
- reason: String(adjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8469
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
9288
+ outcome: "closed",
9289
+ closedReason: rootWorkItemFailure,
8470
9290
  });
8471
- continue;
8472
- }
8473
- const requestClaim = await prepareRunnerRequestClaim(
8474
- selectedRecord,
8475
- adjudication.selected_bot_usernames,
8476
- safeObject(sharedHumanIntentContext).humanIntent || null,
8477
- );
8478
- if (!requestClaim.ok) {
8479
9291
  await syncRunnerRequestLedgerForProjectToServer({
8480
9292
  normalizedRoute,
8481
9293
  runtime,
8482
9294
  });
8483
- saveRunnerRouteState(
8484
- routeKey,
8485
- buildRunnerRouteStateFromComment(selectedRecord, {
8486
- last_action: "request_skipped",
8487
- last_reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8488
- last_trigger: "request_ledger",
8489
- }),
8490
- );
8491
- skippedRecords.push({
8492
- id: selectedRecord.id,
8493
- reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8494
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8495
- diagnosticType: "skip",
8496
- contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
8497
- action: "skip_invalid_request_state",
8498
- closedReason: String(requestClaim.reason || "").trim(),
8499
- });
8500
- continue;
8501
- }
8502
- const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
8503
- normalizedRoute,
8504
- selectedRecord,
8505
- runtime,
8506
- requestKey: requestClaim.requestKey,
8507
- });
8508
- const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
8509
- normalizedRoute,
8510
- routeKey,
8511
- selectedRecord,
8512
- runtime,
8513
- requestKey: requestClaim.requestKey,
8514
- });
8515
- const claimedRequest = safeObject(
8516
- loadRunnerRequestByKey(requestClaim.requestKey)
8517
- || rootWorkItemClaim.request
8518
- || inheritedRootReference.request
8519
- || requestClaim.request,
8520
- );
8521
- const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
8522
- if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
8523
- const rootWorkItemFailure = String(
8524
- rootWorkItemClaim.error
8525
- || rootWorkItemClaim.reason
8526
- || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
8527
- ).trim() || "root_work_item_create_failed";
8528
- if (String(requestClaim.requestKey || "").trim()) {
8529
- markRunnerRequestLifecycle({
8530
- normalizedRoute,
8531
- requestKey: requestClaim.requestKey,
8532
- selectedRecord,
8533
- routeKey,
8534
- outcome: "closed",
8535
- closedReason: rootWorkItemFailure,
8536
- });
8537
- await syncRunnerRequestLedgerForProjectToServer({
8538
- normalizedRoute,
8539
- runtime,
8540
- });
8541
- }
8542
- saveRunnerRouteState(
8543
- routeKey,
8544
- buildRunnerRouteStateFromComment(selectedRecord, {
8545
- last_action: "request_skipped",
8546
- last_reason: rootWorkItemFailure,
8547
- last_trigger: "work_item_root",
8548
- last_request_key: String(requestClaim.requestKey || "").trim(),
8549
- }),
8550
- );
8551
- skippedRecords.push({
8552
- id: selectedRecord.id,
8553
- reason: rootWorkItemFailure,
8554
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8555
- diagnosticType: "skip",
8556
- action: "skip_missing_root_work_item",
8557
- closedReason: rootWorkItemFailure,
8558
- });
8559
- continue;
8560
9295
  }
8561
- saveRunnerRouteState(routeKey, {
8562
- ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
8563
- id: String(claimedRequest.root_work_item_id || "").trim(),
8564
- title: String(claimedRequest.root_work_item_title || "").trim(),
8565
- status: String(claimedRequest.root_work_item_status || "").trim(),
8566
- }),
8567
- last_request_key: String(requestClaim.requestKey || "").trim(),
8568
- last_root_work_item_id: String(claimedRequest.root_work_item_id || "").trim(),
8569
- last_root_work_item_title: String(claimedRequest.root_work_item_title || "").trim(),
8570
- last_root_work_item_status: String(claimedRequest.root_work_item_status || "").trim(),
8571
- });
8572
- await syncRunnerRequestLedgerForProjectToServer({
8573
- normalizedRoute,
8574
- runtime,
8575
- });
8576
9296
  return {
8577
- route_key: routeKey,
8578
- route_name: normalizedRoute.name,
8579
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
8580
- outcome: "accepted",
8581
- detail: `accepted comment ${selectedRecord.id} for background execution`,
8582
- archive_source: String(archiveThread.source || "").trim() || "-",
8583
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
8584
- thread_id: archiveThread.threadID,
8585
- comment_id: selectedRecord.id,
8586
- execution_mode: executionPlan.mode,
8587
- role_profile: executionPlan.roleProfileName,
8588
- deferred_execution: {
8589
- routeKey,
8590
- normalizedRoute,
8591
- routeState: safeObject(loadBotRunnerState().routes[routeKey]),
8592
- selectedRecord,
8593
- pendingOrdered: pending.ordered,
8594
- bot,
8595
- destination,
8596
- archiveThread,
8597
- executionPlan,
8598
- runtime,
8599
- managedConversationBots,
8600
- requestKey: String(requestClaim.requestKey || "").trim(),
8601
- triggerDecision,
8602
- responderAdjudication: adjudication,
8603
- 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
+ },
8604
9311
  },
8605
9312
  };
8606
9313
  }
8607
- if (skippedRecords.length > 0) {
8608
- await archiveRunnerDiagnosticComments({
8609
- comments,
8610
- skippedRecords,
8611
- normalizedRoute,
8612
- bot,
8613
- archiveThread,
8614
- runtime,
8615
- });
8616
- const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
8617
- const lastSkipped = skippedRecords[skippedRecords.length - 1];
8618
- return {
8619
- route_key: routeKey,
8620
- route_name: normalizedRoute.name,
8621
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
8622
- outcome: "skipped",
8623
- detail: distinctReasons.join("; ") || "all pending messages were skipped",
8624
- archive_source: String(archiveThread.source || "").trim() || "-",
8625
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
8626
- thread_id: archiveThread.threadID,
8627
- comment_id: lastSkipped.id,
8628
- skipped_count: skippedRecords.length,
8629
- execution_mode: executionPlan.mode,
8630
- role_profile: executionPlan.roleProfileName,
8631
- };
8632
- }
8633
- }
8634
- const skippedRecords = [];
8635
- for (const selectedRecord of pending.pending) {
9314
+ return {
9315
+ ok: true,
9316
+ claimedRequest,
9317
+ };
9318
+ };
9319
+ const prepareRunnerSelectedRecordForExecution = async (selectedRecord) => {
8636
9320
  const duplicateArchivedSkip = maybeBuildDuplicateArchivedSkip(selectedRecord, refreshedState);
8637
9321
  if (duplicateArchivedSkip) {
8638
- saveRunnerRouteState(
8639
- routeKey,
8640
- buildRunnerRouteStateFromComment(selectedRecord, {
8641
- last_action: "duplicate_skipped",
8642
- last_reason: String(duplicateArchivedSkip.reason || "duplicate_archived_source_message").trim(),
8643
- last_trigger: "archive_dedupe",
8644
- }),
8645
- );
8646
- skippedRecords.push(duplicateArchivedSkip);
8647
- 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
+ };
8648
9329
  }
8649
9330
  const triggerDecision = evaluateTelegramRunnerTrigger(selectedRecord, normalizedRoute, bot);
8650
9331
  if (triggerDecision.shouldRespond !== true) {
8651
- saveRunnerRouteState(
8652
- routeKey,
8653
- buildRunnerRouteStateFromComment(selectedRecord, {
8654
- last_action: "trigger_skipped",
8655
- last_reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8656
- last_trigger: String(triggerDecision.trigger || "").trim() || "trigger_policy",
8657
- }),
8658
- );
8659
- skippedRecords.push({
8660
- id: selectedRecord.id,
8661
- reason: String(triggerDecision.reason || "trigger policy skipped message").trim() || "trigger policy skipped message",
8662
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8663
- suppressDiagnostic: true,
8664
- });
8665
- 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
+ };
8666
9341
  }
8667
9342
  const startupLoopSkipped = await maybeHandleRunnerStartupLoopCandidate({
8668
9343
  routeKey,
@@ -8680,14 +9355,66 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8680
9355
  : false,
8681
9356
  });
8682
9357
  if (startupLoopSkipped) {
8683
- skippedRecords.push(startupLoopSkipped.skippedRecord);
8684
- continue;
9358
+ return {
9359
+ kind: "startup_loop_skipped",
9360
+ skippedRecord: startupLoopSkipped.skippedRecord,
9361
+ };
8685
9362
  }
8686
9363
  const routingExecutionDeps = {
8687
9364
  ...buildRunnerExecutionDeps(),
8688
9365
  managedConversationBots,
8689
9366
  resolveConversationPeerBots: resolveRunnerConversationPeers,
8690
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
+ }
8691
9418
  const sharedHumanIntentContext = await resolveHumanIntentContext({
8692
9419
  selectedRecord,
8693
9420
  normalizedRoute,
@@ -8696,30 +9423,31 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8696
9423
  deps: routingExecutionDeps,
8697
9424
  });
8698
9425
  if (safeObject(sharedHumanIntentContext).contractNeedsResolution === true) {
8699
- const reason = "human intent contract is incomplete and requires regeneration";
8700
- saveRunnerRouteState(
8701
- routeKey,
8702
- buildRunnerRouteStateFromComment(selectedRecord, {
8703
- last_action: "needs_contract",
8704
- last_reason: reason,
8705
- last_trigger: "human_intent_contract",
8706
- }),
8707
- );
8708
- skippedRecords.push({
8709
- id: selectedRecord.id,
8710
- reason,
8711
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8712
- });
8713
- 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
+ };
8714
9432
  }
8715
- const precomputedInlineAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
9433
+ const precomputedAdjudication = buildRunnerResponderAdjudicationFromHumanIntent({
8716
9434
  selectedRecord,
8717
9435
  normalizedRoute,
8718
9436
  bot,
8719
9437
  deps: routingExecutionDeps,
8720
9438
  precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8721
9439
  });
8722
- 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({
8723
9451
  selectedRecord,
8724
9452
  pendingOrdered: pending.ordered,
8725
9453
  normalizedRoute,
@@ -8728,29 +9456,20 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8728
9456
  deps: routingExecutionDeps,
8729
9457
  precomputedHumanIntent: safeObject(sharedHumanIntentContext).humanIntent || null,
8730
9458
  });
8731
- const inlineCurrentBotSelected = ensureArray(inlineAdjudication.selected_bot_usernames)
9459
+ const currentBotSelected = ensureArray(adjudication.selected_bot_usernames)
8732
9460
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
8733
9461
  .includes(currentBotSelector);
8734
- if (!inlineCurrentBotSelected) {
8735
- saveRunnerRouteState(
8736
- routeKey,
8737
- buildRunnerRouteStateFromComment(selectedRecord, {
8738
- last_action: "adjudication_skipped",
8739
- last_reason: String(inlineAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8740
- last_trigger: "responder_adjudication",
8741
- }),
8742
- );
8743
- skippedRecords.push({
8744
- id: selectedRecord.id,
8745
- reason: String(inlineAdjudication.reason_code || "").trim() || "not_selected_by_adjudicator",
8746
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8747
- });
8748
- 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
+ };
8749
9469
  }
8750
- const currentRouteState = safeObject(loadBotRunnerState().routes[routeKey]);
8751
9470
  const requestClaim = await prepareRunnerRequestClaim(
8752
9471
  selectedRecord,
8753
- inlineAdjudication.selected_bot_usernames,
9472
+ adjudication.selected_bot_usernames,
8754
9473
  safeObject(sharedHumanIntentContext).humanIntent || null,
8755
9474
  );
8756
9475
  if (!requestClaim.ok) {
@@ -8758,84 +9477,136 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8758
9477
  normalizedRoute,
8759
9478
  runtime,
8760
9479
  });
8761
- saveRunnerRouteState(
8762
- routeKey,
8763
- buildRunnerRouteStateFromComment(selectedRecord, {
8764
- last_action: "request_skipped",
8765
- last_reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8766
- 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(),
8767
9537
  }),
8768
- );
8769
- skippedRecords.push({
8770
- id: selectedRecord.id,
8771
- reason: String(requestClaim.reason || "request_unavailable").trim() || "request_unavailable",
8772
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8773
- diagnosticType: "skip",
8774
- contextExcluded: String(requestClaim.reason || "").trim() === "bot_reply_without_active_request",
8775
- action: "skip_invalid_request_state",
8776
- 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(),
8777
9542
  });
8778
- continue;
8779
- }
8780
- const inheritedRootReference = await inheritRunnerReferenceRootWorkItemForRequest({
8781
- normalizedRoute,
8782
- selectedRecord,
8783
- runtime,
8784
- requestKey: requestClaim.requestKey,
8785
- });
8786
- const rootWorkItemClaim = await ensureRunnerRootWorkItemForRequest({
8787
- normalizedRoute,
8788
- routeKey,
8789
- selectedRecord,
8790
- runtime,
8791
- requestKey: requestClaim.requestKey,
8792
- });
8793
- const claimedRequest = safeObject(
8794
- loadRunnerRequestByKey(requestClaim.requestKey)
8795
- || rootWorkItemClaim.request
8796
- || inheritedRootReference.request
8797
- || requestClaim.request,
8798
- );
8799
- const missingRequiredRootWorkItem = actionableRunnerRequestMissingRootWorkItem(claimedRequest);
8800
- if (!rootWorkItemClaim.ok || missingRequiredRootWorkItem) {
8801
- const rootWorkItemFailure = String(
8802
- rootWorkItemClaim.error
8803
- || rootWorkItemClaim.reason
8804
- || (missingRequiredRootWorkItem ? "root_work_item_missing" : "root_work_item_create_failed"),
8805
- ).trim() || "root_work_item_create_failed";
8806
- if (String(requestClaim.requestKey || "").trim()) {
8807
- markRunnerRequestLifecycle({
8808
- normalizedRoute,
8809
- requestKey: requestClaim.requestKey,
8810
- 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: {
8811
9560
  routeKey,
8812
- outcome: "closed",
8813
- closedReason: rootWorkItemFailure,
8814
- });
8815
- await syncRunnerRequestLedgerForProjectToServer({
8816
9561
  normalizedRoute,
9562
+ routeState: safeObject(loadBotRunnerState().routes[routeKey]),
9563
+ selectedRecord,
9564
+ pendingOrdered: pending.ordered,
9565
+ bot,
9566
+ destination,
9567
+ archiveThread,
9568
+ executionPlan,
8817
9569
  runtime,
8818
- });
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;
8819
9582
  }
8820
- saveRunnerRouteState(
8821
- routeKey,
8822
- buildRunnerRouteStateFromComment(selectedRecord, {
8823
- last_action: "request_skipped",
8824
- last_reason: rootWorkItemFailure,
8825
- last_trigger: "work_item_root",
8826
- last_request_key: String(requestClaim.requestKey || "").trim(),
8827
- }),
8828
- );
8829
- skippedRecords.push({
8830
- id: selectedRecord.id,
8831
- reason: rootWorkItemFailure,
8832
- messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
8833
- diagnosticType: "skip",
8834
- action: "skip_missing_root_work_item",
8835
- 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,
8836
9595
  });
8837
9596
  continue;
8838
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;
8839
9610
  saveRunnerRouteState(routeKey, {
8840
9611
  ...buildRunnerActiveExecutionPatch(selectedRecord, requestClaim.requestKey, {
8841
9612
  id: String(claimedRequest.root_work_item_id || "").trim(),
@@ -8967,6 +9738,9 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8967
9738
  executionContractActionable: processed.result?.execution_contract_actionable === true,
8968
9739
  executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
8969
9740
  nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
9741
+ normalizedExecutionContractType: String(processed.result?.normalized_execution_contract_type || "").trim(),
9742
+ normalizedExecutionContractTargets: ensureArray(processed.result?.normalized_execution_contract_targets),
9743
+ normalizedExecutionNextResponders: ensureArray(processed.result?.normalized_execution_next_responders),
8970
9744
  currentBotSelector,
8971
9745
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8972
9746
  normalizedIntent: resolvedIntentType,
@@ -9019,31 +9793,11 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
9019
9793
  };
9020
9794
  }
9021
9795
 
9022
- if (skippedRecords.length > 0) {
9023
- await archiveRunnerDiagnosticComments({
9024
- comments,
9025
- skippedRecords,
9026
- normalizedRoute,
9027
- bot,
9028
- archiveThread,
9029
- runtime,
9030
- });
9031
- const lastSkipped = skippedRecords[skippedRecords.length - 1];
9032
- const distinctReasons = Array.from(new Set(skippedRecords.map((item) => item.reason).filter(Boolean)));
9033
- return {
9034
- route_key: routeKey,
9035
- route_name: normalizedRoute.name,
9036
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
9037
- outcome: "skipped",
9038
- detail: distinctReasons.join("; ") || "all pending messages were skipped",
9039
- archive_source: String(archiveThread.source || "").trim() || "-",
9040
- archive_work_item_id: String(archiveThread.workItemID || "").trim() || "",
9041
- thread_id: archiveThread.threadID,
9042
- comment_id: lastSkipped.id,
9043
- skipped_count: skippedRecords.length,
9044
- execution_mode: executionPlan.mode,
9045
- role_profile: executionPlan.roleProfileName,
9046
- };
9796
+ {
9797
+ const skippedOutcome = await finalizeSkippedPendingRecords(skippedRecords);
9798
+ if (skippedOutcome) {
9799
+ return skippedOutcome;
9800
+ }
9047
9801
  }
9048
9802
 
9049
9803
  return {
@@ -10266,70 +11020,40 @@ function buildRunnerShowPayload(route, flags = {}) {
10266
11020
  const resolvedServerBotName = firstNonEmptyString([
10267
11021
  normalizedRoute.botName,
10268
11022
  matchedTelegramEntry?.serverBotName,
10269
- "-",
10270
- ]);
10271
- const envConfig = normalizedRoute.provider
10272
- ? loadProviderEnvConfig(normalizedRoute.provider, {
10273
- botID: normalizedRoute.botID,
10274
- botName: resolvedServerBotName !== "-" ? resolvedServerBotName : "",
10275
- route: normalizedRoute,
10276
- })
10277
- : null;
10278
- return {
10279
- ok: diagnostics.errors.length === 0,
10280
- route_name: normalizedRoute.name || runnerRouteKey(normalizedRoute),
10281
- route_key: runnerRouteKey(normalizedRoute),
10282
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
10283
- route_config_file: runnerConfig.filePath,
10284
- workspace_registry_file: String(runnerConfig.workspaceRegistryFilePath || botRunnerWorkspaceRegistryFilePath()).trim() || "-",
10285
- route_config: serializeRunnerRoute(normalizedRoute),
10286
- resolved_server_identity: {
10287
- server_bot_name: resolvedServerBotName,
10288
- server_bot_name_source: botNameSource,
10289
- server_bot_id: normalizedRoute.botID || "-",
10290
- telegram_entry_file: String(envConfig?.entryFilePath || "").trim() || "-",
10291
- },
10292
- resolved_destination: {
10293
- destination_label: String(normalizedRoute.destinationLabel || "").trim() || "-",
10294
- destination_id: String(normalizedRoute.destinationID || "").trim() || "-",
10295
- destination_source: "route_config",
10296
- },
10297
- workspace_mapping: {
10298
- workspace_dir: diagnostics.workspaceDir || "-",
10299
- workspace_source: diagnostics.workspaceSource || "-",
10300
- },
10301
- execution_profile: {
10302
- route_role: normalizedRoute.role || "-",
10303
- role_profile_name: diagnostics.roleProfileName || "-",
10304
- client: String(diagnostics.roleProfile?.client || "").trim() || "-",
10305
- model: String(diagnostics.roleProfile?.model || "").trim() || "-",
10306
- permission_mode: String(diagnostics.roleProfile?.permissionMode || "").trim() || "-",
10307
- reasoning_effort: String(diagnostics.roleProfile?.reasoningEffort || "").trim() || "-",
10308
- },
10309
- last_run: {
10310
- action: String(routeState.last_action || "").trim() || "-",
10311
- reason: String(routeState.last_reason || "").trim() || "-",
10312
- intent_type: String(routeState.last_intent_type || "").trim() || "-",
10313
- workspace_dir: String(routeState.last_workspace_dir || "").trim() || "-",
10314
- artifact_validation: String(routeState.last_artifact_validation || "").trim() || "-",
10315
- artifact_paths: ensureArray(routeState.last_artifact_paths).map((item) => String(item || "").trim()).filter(Boolean),
10316
- artifact_errors: ensureArray(routeState.last_artifact_errors).map((item) => String(item || "").trim()).filter(Boolean),
10317
- boundary_violations: ensureArray(routeState.last_boundary_violations).map((item) => safeObject(item)),
10318
- },
10319
- active_execution: {
10320
- active: activeExecutionState.active === true,
10321
- stale: activeExecutionState.stale === true,
10322
- stuck: activeExecutionState.stuck === true,
10323
- comment_id: String(activeExecutionState.commentID || "").trim(),
10324
- source_message_id: intFromRawAllowZero(activeExecutionState.sourceMessageID, 0),
10325
- started_at: String(activeExecutionState.startedAt || "").trim(),
10326
- age_seconds: intFromRawAllowZero(activeExecutionState.ageSeconds, 0),
10327
- warning: String(activeExecutionState.warning || "").trim(),
10328
- },
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,
10329
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.",
10330
11054
  warnings: [
10331
11055
  ...ensureArray(diagnostics.warnings),
10332
- ...(activeExecutionState.stuck ? [activeExecutionState.warning] : []),
11056
+ ...(activeExecutionPayload.stuck && activeExecutionPayload.warning ? [activeExecutionPayload.warning] : []),
10333
11057
  ],
10334
11058
  errors: diagnostics.errors,
10335
11059
  };
@@ -10381,6 +11105,20 @@ async function runRunnerShow(flags) {
10381
11105
  ` action: ${payload.last_run.action}`,
10382
11106
  ` reason: ${payload.last_run.reason}`,
10383
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}`,
10384
11122
  ` workspace_dir: ${payload.last_run.workspace_dir}`,
10385
11123
  ` artifact_validation: ${payload.last_run.artifact_validation}`,
10386
11124
  ` artifact_paths: ${payload.last_run.artifact_paths.length ? payload.last_run.artifact_paths.join(", ") : "-"}`,
@@ -10926,6 +11664,161 @@ function summarizeRunnerTUIPhases(routes = []) {
10926
11664
  return { counts, warningCount };
10927
11665
  }
10928
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
+
10929
11822
  function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1, now = Date.now(), stopping = false, useColor = false, sourceLabel = "runner start", logFilePath = "" }) {
10930
11823
  const lines = [];
10931
11824
  const activeCount = ensureArray(routes).filter((item) => ["polling", "ai_running", "delivering", "context_suggesting", "busy"].includes(normalizeRunnerTUIPhase(item.phase))).length;
@@ -10935,65 +11828,14 @@ function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1,
10935
11828
  if (String(logFilePath || "").trim()) {
10936
11829
  lines.push(`Log: ${truncateRunnerTUIText(String(logFilePath || "").trim(), 112)}`);
10937
11830
  }
10938
- 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));
10939
11832
  lines.push("Ctrl+C to stop. Foreground runner state is shown below.");
10940
11833
  lines.push("");
10941
- lines.push(
10942
- [
10943
- bootstrapPadRight("Route", 24),
10944
- bootstrapPadRight("Phase", 18),
10945
- bootstrapPadRight("Age", 7),
10946
- bootstrapPadRight("AI", 20),
10947
- bootstrapPadRight("Intent", 18),
10948
- bootstrapPadRight("Msg", 8),
10949
- bootstrapPadRight("Next", 8),
10950
- bootstrapPadRight("Warn", 6),
10951
- ].join(" "),
10952
- );
11834
+ lines.push(buildRunnerTUIRouteTableHeader());
10953
11835
  lines.push("-".repeat(120));
10954
11836
  for (const route of ensureArray(routes)) {
10955
- const phase = normalizeRunnerTUIPhase(route.phase);
10956
- const aiLabel = truncateRunnerTUIText(
10957
- route.client
10958
- ? `${String(route.client || "").trim()}/${String(route.model || "").trim() || "-"}`
10959
- : "-",
10960
- 20,
10961
- );
10962
- const ageLabel = formatRunnerTUIAge(now, route.phase_started_at || route.updated_at || now);
10963
- const nextSeconds = Number.isFinite(Number(route.next_run_at)) && Number(route.next_run_at) > now
10964
- ? `${Math.max(0, Math.ceil((Number(route.next_run_at) - now) / 1000))}s`
10965
- : phase === "ai_running" || phase === "delivering" || phase === "context_suggesting"
10966
- ? "run"
10967
- : "-";
10968
- const warningLabel = String(route.warning || route.last_error || "").trim() ? "yes" : "-";
10969
- lines.push(
10970
- [
10971
- bootstrapPadRight(truncateRunnerTUIText(route.route_name, 24), 24),
10972
- colorizeRunnerTUIPhase(phase, 18, useColor),
10973
- bootstrapPadRight(ageLabel, 7),
10974
- bootstrapPadRight(aiLabel, 20),
10975
- bootstrapPadRight(truncateRunnerTUIText(route.intent_type || "-", 18), 18),
10976
- bootstrapPadRight(String(route.source_message_id || "-"), 8),
10977
- bootstrapPadRight(nextSeconds, 8),
10978
- bootstrapPadRight(warningLabel, 6),
10979
- ].join(" "),
10980
- );
10981
- const detailParts = [
10982
- String(route.detail || "").trim(),
10983
- String(route.planner_client || "").trim() || String(route.planner_model || "").trim()
10984
- ? `planner=${String(route.planner_client || "").trim() || "-"}/${String(route.planner_model || "").trim() || "-"}`
10985
- : "",
10986
- String(route.context_suggestion_status || "").trim()
10987
- ? `context=${String(route.context_suggestion_status || "").trim()}`
10988
- : "",
10989
- String(route.warning || "").trim()
10990
- ? `warning=${String(route.warning || "").trim()}`
10991
- : "",
10992
- String(route.last_error || "").trim()
10993
- ? `error=${String(route.last_error || "").trim()}`
10994
- : "",
10995
- ].filter(Boolean);
10996
- lines.push(` ${truncateRunnerTUIText(detailParts.join(" | ") || "-", 116)}`);
11837
+ lines.push(buildRunnerTUIRouteRow(route, now, useColor));
11838
+ lines.push(buildRunnerTUIRouteDetailLine(route));
10997
11839
  }
10998
11840
  lines.push("");
10999
11841
  lines.push(useColor ? bootstrapColorText("Recent Events", "35") : "Recent Events");
@@ -11001,9 +11843,7 @@ function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1,
11001
11843
  lines.push(" (none yet)");
11002
11844
  } else {
11003
11845
  for (const event of ensureArray(recentEvents).slice(0, 8)) {
11004
- lines.push(
11005
- ` [${String(event.time || "").trim() || "--:--:--"}] ${truncateRunnerTUIText(event.route_name, 24)} ${bootstrapPadRight(String(event.outcome || "-").trim(), 12)} ${truncateRunnerTUIText(event.detail || "-", 74)}`,
11006
- );
11846
+ lines.push(buildRunnerTUIRecentEventLine(event));
11007
11847
  }
11008
11848
  }
11009
11849
  return lines.join("\n");
@@ -11015,37 +11855,15 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11015
11855
  return null;
11016
11856
  }
11017
11857
  const useColor = bootstrapSupportsANSIColors();
11018
- 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
+ );
11019
11864
  const recentEvents = [];
11020
- for (const route of ensureArray(routes)) {
11021
- const normalizedRoute = normalizeRunnerRoute(route);
11022
- const routeKey = runnerRouteKey(normalizedRoute);
11023
- routeStates.set(routeKey, {
11024
- route_key: routeKey,
11025
- route_name: normalizedRoute.name,
11026
- phase: "waiting",
11027
- detail: "awaiting next poll",
11028
- client: "",
11029
- model: "",
11030
- planner_client: "",
11031
- planner_model: "",
11032
- intent_type: "",
11033
- source_message_id: "",
11034
- next_run_at: 0,
11035
- context_suggestion_status: "",
11036
- warning: "",
11037
- last_error: "",
11038
- phase_started_at: Date.now(),
11039
- updated_at: Date.now(),
11040
- });
11041
- }
11042
11865
  if (bootstrapEvent && typeof bootstrapEvent === "object") {
11043
- recentEvents.unshift({
11044
- time: new Date().toLocaleTimeString("en-US", { hour12: false }),
11045
- route_name: String(bootstrapEvent.route_name || sourceLabel || "runner").trim(),
11046
- outcome: String(bootstrapEvent.outcome || "prepared").trim(),
11047
- detail: String(bootstrapEvent.detail || "").trim(),
11048
- });
11866
+ recentEvents.unshift(buildRunnerTUIRecentEvent(bootstrapEvent, sourceLabel, "prepared"));
11049
11867
  }
11050
11868
  let intervalHandle = null;
11051
11869
  let disposed = false;
@@ -11084,57 +11902,27 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11084
11902
  const routeKey = String(result?.route_key || "").trim();
11085
11903
  if (routeKey && routeStates.has(routeKey)) {
11086
11904
  const current = safeObject(routeStates.get(routeKey));
11087
- setRouteState(routeKey, {
11088
- phase: mapRunnerTUIPhaseFromOutcome(result?.outcome),
11089
- detail: String(result?.detail || "").trim(),
11090
- intent_type: String(routeState?.last_intent_type || current.intent_type || "").trim(),
11091
- source_message_id: intFromRawAllowZero(routeState?.last_source_message_id, intFromRawAllowZero(current.source_message_id, 0)) || current.source_message_id,
11092
- context_suggestion_status: String(result?.context_suggestion_status || routeState?.last_context_suggestion_status || current.context_suggestion_status || "").trim(),
11093
- warning: String(safeObject(routeState).active_execution_warning || result?.warning || "").trim(),
11094
- last_error: String(routeState?.last_error || "").trim(),
11095
- });
11905
+ setRouteState(routeKey, buildRunnerTUIRouteStatePatchFromResult(result, routeState, current));
11096
11906
  }
11097
11907
  if (!result || ["idle", "busy"].includes(String(result.outcome || "").trim().toLowerCase())) {
11098
11908
  return;
11099
11909
  }
11100
- recentEvents.unshift({
11101
- time: new Date().toLocaleTimeString("en-US", { hour12: false }),
11102
- route_name: String(result.route_name || result.route_key || "").trim(),
11103
- outcome: String(result.outcome || "").trim(),
11104
- detail: String(result.detail || "").trim(),
11105
- });
11106
- if (recentEvents.length > 8) {
11107
- recentEvents.length = 8;
11108
- }
11910
+ appendRunnerTUIRecentEvent(recentEvents, result, String(result.route_name || result.route_key || "").trim());
11109
11911
  render(false);
11110
11912
  };
11111
11913
 
11112
11914
  const reportExecutionStage = ({ routeKey, executionPlan, selectedRecord, phase, detail, warning = "", contextSuggestionStatus = "", intentType = "" }) => {
11113
11915
  const normalizedRouteKey = String(routeKey || "").trim();
11114
11916
  if (!normalizedRouteKey || !routeStates.has(normalizedRouteKey)) return;
11115
- const normalizedPhase = normalizeRunnerTUIPhase(phase);
11116
- const clearLastError = ["ai_running", "delivering", "context_suggesting", "replied"].includes(normalizedPhase);
11117
- const plannerFallbackClient = String(safeObject(executionPlan).roleProfile?.client || "").trim();
11118
- const plannerFallbackModel = String(safeObject(executionPlan).roleProfile?.model || "").trim();
11119
- const plannerClient = resolveRolePlannerClient({ env: process.env }) || plannerFallbackClient;
11120
- const plannerModel = resolveRolePlannerModelDisplayName({
11121
- env: process.env,
11122
- fallbackClient: plannerFallbackClient,
11123
- fallbackModel: plannerFallbackModel,
11124
- });
11125
- setRouteState(normalizedRouteKey, {
11126
- phase: normalizedPhase,
11127
- detail: String(detail || "").trim(),
11128
- client: plannerFallbackClient,
11129
- model: plannerFallbackModel,
11130
- planner_client: String(plannerClient || "").trim(),
11131
- planner_model: String(plannerModel || "").trim(),
11132
- source_message_id: intFromRawAllowZero(safeObject(selectedRecord).parsedArchive?.messageID, 0),
11133
- intent_type: String(intentType || safeObject(executionPlan).intentType || "").trim(),
11134
- warning: String(warning || "").trim(),
11135
- context_suggestion_status: String(contextSuggestionStatus || "").trim(),
11136
- last_error: clearLastError ? "" : undefined,
11137
- });
11917
+ setRouteState(normalizedRouteKey, buildRunnerTUIRouteStatePatchFromExecutionStage({
11918
+ executionPlan,
11919
+ selectedRecord,
11920
+ phase,
11921
+ detail,
11922
+ warning,
11923
+ contextSuggestionStatus,
11924
+ intentType,
11925
+ }));
11138
11926
  };
11139
11927
 
11140
11928
  const updateNextRunAt = (routeKey, nextRunAt) => {
@@ -11161,6 +11949,550 @@ function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabe
11161
11949
  };
11162
11950
  }
11163
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
+
11164
12496
  async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11165
12497
  const jsonMode = boolFromRaw(flags.json, false);
11166
12498
  const sourceLabel = String(safeObject(options).sourceLabel || "runner start").trim() || "runner start";
@@ -11190,460 +12522,92 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11190
12522
  }
11191
12523
  tui?.start();
11192
12524
  while (!stopRequested) {
11193
- const now = Date.now();
11194
- const dueRoutes = [];
11195
- let nextSleepMs = 5000;
11196
- for (const route of routes) {
11197
- const normalizedRoute = normalizeRunnerRoute(route);
11198
- const routeKey = runnerRouteKey(normalizedRoute);
11199
- const nextAt = Number(schedules.get(routeKey) || 0);
11200
- if (nextAt > now) {
11201
- nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - now));
11202
- continue;
11203
- }
11204
- dueRoutes.push(normalizedRoute);
11205
- }
12525
+ const { dueRoutes, nextSleepMs: initialNextSleepMs } = resolveRunnerStartDueRoutes(routes, schedules, Date.now());
12526
+ let nextSleepMs = initialNextSleepMs;
11206
12527
  if (dueRoutes.length > 0) {
11207
12528
  const runtime = await resolveRunnerContext(flags);
11208
12529
  const dueRouteGroups = groupRunnerRoutesBySchedulingTarget(dueRoutes);
11209
12530
  await runTasksWithConcurrencyLimit(dueRouteGroups, concurrency, async (routeGroup) => {
11210
12531
  for (const normalizedRoute of ensureArray(routeGroup)) {
11211
12532
  const routeKey = runnerRouteKey(normalizedRoute);
11212
- tui?.setRouteState(routeKey, {
11213
- phase: "polling",
11214
- detail: "checking inbound messages and archive thread",
11215
- });
11216
- runnerLogger?.append("route_poll", {
11217
- route_key: routeKey,
11218
- route_name: normalizedRoute.name,
11219
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11220
- });
12533
+ beginRunnerStartRoutePoll({ routeKey, normalizedRoute, tui, runnerLogger });
11221
12534
  let cycleOutcome = "polling";
11222
12535
  try {
11223
12536
  const result = await processRunnerRouteOnce(normalizedRoute, runtime, "start", { deferExecution: true });
11224
12537
  cycleOutcome = String(result?.outcome || "").trim().toLowerCase() || "idle";
11225
12538
  const deferredExecution = safeObject(result.deferred_execution);
11226
12539
  if (deferredExecution.routeKey) {
11227
- tui?.reportExecutionStage({
11228
- routeKey: deferredExecution.routeKey,
11229
- executionPlan: deferredExecution.executionPlan,
11230
- selectedRecord: deferredExecution.selectedRecord,
11231
- phase: "ai_running",
11232
- detail: `running local AI client for comment ${String(deferredExecution.selectedRecord?.id || "").trim() || "-"}`,
11233
- });
11234
- runnerLogger?.append("execution_accepted", {
11235
- route_key: deferredExecution.routeKey,
11236
- route_name: deferredExecution.normalizedRoute?.name,
11237
- comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
11238
- source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
11239
- execution_mode: String(deferredExecution.executionPlan?.mode || "").trim(),
11240
- role_profile: String(deferredExecution.executionPlan?.roleProfileName || "").trim(),
12540
+ reportRunnerDeferredExecutionAccepted({
12541
+ deferredExecution,
12542
+ runnerLogger,
12543
+ tui,
11241
12544
  });
11242
12545
  const executionHeartbeat = startRunnerExecutionHeartbeat(
11243
12546
  deferredExecution.routeKey,
11244
12547
  deferredExecution.selectedRecord,
11245
12548
  );
11246
- if (String(deferredExecution.requestKey || "").trim()) {
11247
- markRunnerRequestLifecycle({
11248
- normalizedRoute: deferredExecution.normalizedRoute,
11249
- requestKey: deferredExecution.requestKey,
11250
- selectedRecord: deferredExecution.selectedRecord,
11251
- routeKey: deferredExecution.routeKey,
11252
- outcome: "running",
11253
- });
11254
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11255
- normalizedRoute: deferredExecution.normalizedRoute,
11256
- runtime: deferredExecution.runtime,
11257
- requestKey: deferredExecution.requestKey,
11258
- });
11259
- const syncedRequest = safeObject(rootWorkItemSync.request);
11260
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11261
- saveRunnerRouteState(deferredExecution.routeKey, {
11262
- active_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11263
- active_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11264
- active_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11265
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11266
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11267
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11268
- });
11269
- }
11270
- await syncRunnerRequestLedgerForProjectToServer({
11271
- normalizedRoute: deferredExecution.normalizedRoute,
11272
- runtime: deferredExecution.runtime,
11273
- });
11274
- }
11275
- const executionPromise = (async () => {
11276
- try {
11277
- const processed = await processRunnerSelectedRecord({
11278
- routeKey: deferredExecution.routeKey,
11279
- normalizedRoute: deferredExecution.normalizedRoute,
11280
- routeState: deferredExecution.routeState,
11281
- selectedRecord: deferredExecution.selectedRecord,
11282
- pendingOrdered: deferredExecution.pendingOrdered,
11283
- bot: deferredExecution.bot,
11284
- destination: deferredExecution.destination,
11285
- archiveThread: deferredExecution.archiveThread,
11286
- executionPlan: deferredExecution.executionPlan,
11287
- runtime: deferredExecution.runtime,
11288
- triggerDecision: deferredExecution.triggerDecision,
11289
- responderAdjudication: deferredExecution.responderAdjudication,
11290
- persistedHumanIntentRequest: loadRunnerRequestByKey(deferredExecution.requestKey),
11291
- precomputedHumanIntentContext: safeObject(deferredExecution.humanIntentContext),
11292
- deps: {
11293
- saveRunnerRouteState,
11294
- startRunnerTypingHeartbeat,
11295
- runRunnerAIExecution,
11296
- explainExecutionFailureWithAI,
11297
- performLocalBotDelivery,
11298
- serializeRunnerTriggerPolicy,
11299
- serializeRunnerArchivePolicy,
11300
- buildRunnerExecutionDeps,
11301
- buildRunnerDeliveryDeps,
11302
- buildRunnerRuntimeDeps,
11303
- managedConversationBots: deferredExecution.managedConversationBots,
11304
- resolveConversationPeerBots: resolveRunnerConversationPeers,
11305
- reportRunnerStage: (stage) => {
11306
- runnerLogger?.append("execution_stage", {
11307
- route_key: deferredExecution.routeKey,
11308
- route_name: deferredExecution.normalizedRoute?.name,
11309
- comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
11310
- source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
11311
- phase: String(safeObject(stage).phase || "").trim(),
11312
- detail: String(safeObject(stage).detail || "").trim(),
11313
- intent_type: String(safeObject(stage).intentType || "").trim(),
11314
- context_suggestion_status: String(safeObject(stage).contextSuggestionStatus || "").trim(),
11315
- });
11316
- tui?.reportExecutionStage({
11317
- routeKey: deferredExecution.routeKey,
11318
- executionPlan: deferredExecution.executionPlan,
11319
- selectedRecord: deferredExecution.selectedRecord,
11320
- ...safeObject(stage),
11321
- });
11322
- },
11323
- },
11324
- });
11325
- if (processed.kind === "skipped") {
11326
- if (String(deferredExecution.requestKey || "").trim()) {
11327
- markRunnerRequestLifecycle({
11328
- normalizedRoute: deferredExecution.normalizedRoute,
11329
- requestKey: deferredExecution.requestKey,
11330
- selectedRecord: deferredExecution.selectedRecord,
11331
- routeKey: deferredExecution.routeKey,
11332
- outcome: "skipped",
11333
- closedReason: String(processed.skippedRecord?.reason || "skipped").trim() || "skipped",
11334
- });
11335
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11336
- normalizedRoute: deferredExecution.normalizedRoute,
11337
- runtime: deferredExecution.runtime,
11338
- requestKey: deferredExecution.requestKey,
11339
- });
11340
- const syncedRequest = safeObject(rootWorkItemSync.request);
11341
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11342
- saveRunnerRouteState(deferredExecution.routeKey, {
11343
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11344
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11345
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11346
- });
11347
- }
11348
- await syncRunnerRequestLedgerForProjectToServer({
11349
- normalizedRoute: deferredExecution.normalizedRoute,
11350
- runtime: deferredExecution.runtime,
11351
- });
11352
- }
11353
- return {
11354
- route_key: deferredExecution.routeKey,
11355
- route_name: deferredExecution.normalizedRoute.name,
11356
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11357
- outcome: "skipped",
11358
- detail: String(processed.skippedRecord?.reason || "trigger policy skipped message").trim(),
11359
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11360
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11361
- thread_id: deferredExecution.archiveThread.threadID,
11362
- comment_id: deferredExecution.selectedRecord.id,
11363
- execution_mode: deferredExecution.executionPlan.mode,
11364
- role_profile: deferredExecution.executionPlan.roleProfileName,
11365
- };
11366
- }
11367
- if (String(deferredExecution.requestKey || "").trim()) {
11368
- const resolvedIntentType = String(
11369
- safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type || "",
11370
- ).trim();
11371
- markRunnerRequestLifecycle({
11372
- normalizedRoute: deferredExecution.normalizedRoute,
11373
- requestKey: deferredExecution.requestKey,
11374
- selectedRecord: deferredExecution.selectedRecord,
11375
- routeKey: deferredExecution.routeKey,
11376
- outcome: processed.kind === "delivery_failed"
11377
- ? "delivery_failed_after_generation"
11378
- : String(processed.result?.outcome || "replied").trim().toLowerCase(),
11379
- conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
11380
- conversationParticipants: ensureArray(processed.result?.conversation_participants),
11381
- conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
11382
- allowedResponders: ensureArray(processed.result?.conversation_allowed_responders),
11383
- conversationLeadBot: String(processed.result?.conversation_lead_bot || "").trim(),
11384
- conversationSummaryBot: String(processed.result?.conversation_summary_bot || "").trim(),
11385
- conversationAllowBotToBot: processed.result?.conversation_allow_bot_to_bot === true,
11386
- conversationReplyExpectation: String(processed.result?.conversation_reply_expectation || "").trim(),
11387
- executionContractType: String(processed.result?.execution_contract_type || "").trim(),
11388
- executionContractActionable: processed.result?.execution_contract_actionable === true,
11389
- executionContractTargets: ensureArray(processed.result?.execution_contract_targets),
11390
- nextExpectedResponders: ensureArray(processed.result?.next_expected_responders),
11391
- currentBotSelector: normalizeTelegramMentionUsername(
11392
- deferredExecution.bot?.username || deferredExecution.bot?.name,
11393
- ),
11394
- conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
11395
- normalizedIntent: resolvedIntentType,
11396
- aiReplyGenerated: processed.result?.ai_reply_generated === true,
11397
- aiReplyGeneratedAt: String(processed.result?.ai_reply_generated_at || "").trim(),
11398
- aiReplyPreview: String(processed.result?.ai_reply_preview || "").trim(),
11399
- responseContractValidationStatus: String(processed.result?.response_contract_validation_status || "").trim(),
11400
- responseContractValidationReason: String(processed.result?.response_contract_validation_reason || "").trim(),
11401
- responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
11402
- assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
11403
- assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
11404
- assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
11405
- deliveryStatus: String(processed.result?.delivery_status || "").trim(),
11406
- archiveStatus: String(processed.result?.archive_status || "").trim(),
11407
- transportError: String(processed.result?.transport_error || "").trim(),
11408
- archiveError: String(processed.result?.archive_error || "").trim(),
11409
- });
11410
- if (processed.kind !== "delivery_failed") {
11411
- await ensureRunnerRootWorkItemForRequest({
11412
- normalizedRoute: deferredExecution.normalizedRoute,
11413
- routeKey: deferredExecution.routeKey,
11414
- selectedRecord: deferredExecution.selectedRecord,
11415
- runtime: deferredExecution.runtime,
11416
- requestKey: deferredExecution.requestKey,
11417
- });
11418
- }
11419
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11420
- normalizedRoute: deferredExecution.normalizedRoute,
11421
- runtime: deferredExecution.runtime,
11422
- requestKey: deferredExecution.requestKey,
11423
- });
11424
- const syncedRequest = safeObject(rootWorkItemSync.request);
11425
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11426
- saveRunnerRouteState(deferredExecution.routeKey, {
11427
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11428
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11429
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11430
- });
11431
- }
11432
- await syncRunnerRequestLedgerForProjectToServer({
11433
- normalizedRoute: deferredExecution.normalizedRoute,
11434
- runtime: deferredExecution.runtime,
11435
- });
11436
- }
11437
- return {
11438
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11439
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11440
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11441
- ...processed.result,
11442
- };
11443
- } catch (err) {
11444
- const currentRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
11445
- const errorText = String(err?.message || err).trim();
11446
- saveRunnerRouteState(deferredExecution.routeKey, {
11447
- ...currentRouteState,
11448
- ...emptyRunnerActiveExecutionPatch(),
11449
- last_error: errorText,
11450
- });
11451
- if (String(deferredExecution.requestKey || "").trim()) {
11452
- const currentRequest = safeObject(loadRunnerRequestByKey(deferredExecution.requestKey));
11453
- const resolvedIntentType = String(
11454
- safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]).last_intent_type
11455
- || currentRequest.normalized_intent
11456
- || "",
11457
- ).trim();
11458
- markRunnerRequestLifecycle({
11459
- normalizedRoute: deferredExecution.normalizedRoute,
11460
- requestKey: deferredExecution.requestKey,
11461
- selectedRecord: deferredExecution.selectedRecord,
11462
- routeKey: deferredExecution.routeKey,
11463
- outcome: "error",
11464
- closedReason: errorText || "execution_error",
11465
- normalizedIntent: resolvedIntentType,
11466
- });
11467
- await ensureRunnerRootWorkItemForRequest({
11468
- normalizedRoute: deferredExecution.normalizedRoute,
11469
- routeKey: deferredExecution.routeKey,
11470
- selectedRecord: deferredExecution.selectedRecord,
11471
- runtime: deferredExecution.runtime,
11472
- requestKey: deferredExecution.requestKey,
11473
- });
11474
- const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11475
- normalizedRoute: deferredExecution.normalizedRoute,
11476
- runtime: deferredExecution.runtime,
11477
- requestKey: deferredExecution.requestKey,
11478
- });
11479
- const syncedRequest = safeObject(rootWorkItemSync.request);
11480
- if (String(syncedRequest.root_work_item_id || "").trim()) {
11481
- saveRunnerRouteState(deferredExecution.routeKey, {
11482
- last_root_work_item_id: String(syncedRequest.root_work_item_id || "").trim(),
11483
- last_root_work_item_title: String(syncedRequest.root_work_item_title || "").trim(),
11484
- last_root_work_item_status: String(syncedRequest.root_work_item_status || "").trim(),
11485
- });
11486
- }
11487
- await syncRunnerRequestLedgerForProjectToServer({
11488
- normalizedRoute: deferredExecution.normalizedRoute,
11489
- runtime: deferredExecution.runtime,
11490
- });
11491
- }
11492
- return {
11493
- route_key: deferredExecution.routeKey,
11494
- route_name: deferredExecution.normalizedRoute.name,
11495
- logical_signature: runnerRouteLogicalSignature(deferredExecution.normalizedRoute),
11496
- outcome: "error",
11497
- detail: errorText,
11498
- archive_source: String(deferredExecution.archiveThread.source || "").trim() || "-",
11499
- archive_work_item_id: String(deferredExecution.archiveThread.workItemID || "").trim() || "",
11500
- thread_id: deferredExecution.archiveThread.threadID,
11501
- comment_id: deferredExecution.selectedRecord.id,
11502
- };
11503
- } finally {
11504
- executionHeartbeat.stop();
11505
- inFlightExecutions.delete(deferredExecution.routeKey);
11506
- schedules.set(deferredExecution.routeKey, Date.now() + 250);
11507
- tui?.updateNextRunAt(deferredExecution.routeKey, Date.now() + 250);
11508
- }
11509
- })();
11510
- inFlightExecutions.set(routeKey, executionPromise);
11511
- executionPromise.then((finalResult) => {
11512
- const latestRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
11513
- runnerLogger?.append("execution_result", {
11514
- route_key: String(finalResult?.route_key || deferredExecution.routeKey).trim(),
11515
- route_name: String(finalResult?.route_name || deferredExecution.normalizedRoute?.name || "").trim(),
11516
- outcome: String(finalResult?.outcome || "").trim(),
11517
- detail: String(finalResult?.detail || "").trim(),
11518
- comment_id: String(finalResult?.comment_id || deferredExecution.selectedRecord?.id || "").trim(),
11519
- source_message_id: intFromRawAllowZero(latestRouteState?.last_source_message_id, intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0)),
11520
- intent_type: String(latestRouteState?.last_intent_type || "").trim(),
11521
- context_suggestion_status: String(finalResult?.context_suggestion_status || latestRouteState?.last_context_suggestion_status || "").trim(),
11522
- });
11523
- if (tui) {
11524
- tui.recordResult(finalResult, latestRouteState);
11525
- } else {
11526
- printRunnerResult("start", finalResult, jsonMode);
11527
- }
12549
+ await syncRunnerDeferredExecutionRunningState(deferredExecution);
12550
+ const reportRunnerStage = createRunnerDeferredExecutionStageReporter({
12551
+ deferredExecution,
12552
+ runnerLogger,
12553
+ tui,
11528
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);
11529
12566
  const acceptedResult = {
11530
12567
  ...result,
11531
12568
  deferred_execution: undefined,
11532
12569
  };
11533
12570
  cycleOutcome = String(acceptedResult?.outcome || "accepted").trim().toLowerCase() || "accepted";
11534
- runnerLogger?.append("route_result", {
11535
- route_key: String(acceptedResult?.route_key || routeKey).trim(),
11536
- route_name: String(acceptedResult?.route_name || normalizedRoute.name || "").trim(),
11537
- outcome: String(acceptedResult?.outcome || "").trim(),
11538
- detail: String(acceptedResult?.detail || "").trim(),
12571
+ publishRunnerStartImmediateResult({
12572
+ result: acceptedResult,
12573
+ routeKey,
12574
+ normalizedRoute,
12575
+ runnerLogger,
12576
+ tui,
12577
+ jsonMode,
11539
12578
  });
11540
- if (tui) {
11541
- tui.recordResult(acceptedResult);
11542
- } else {
11543
- printRunnerResult("start", acceptedResult, jsonMode);
11544
- }
11545
- } else if (result.outcome !== "busy") {
11546
- runnerLogger?.append("route_result", {
11547
- route_key: String(result?.route_key || routeKey).trim(),
11548
- route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
11549
- outcome: String(result?.outcome || "").trim(),
11550
- detail: String(result?.detail || "").trim(),
11551
- comment_id: String(result?.comment_id || "").trim(),
11552
- });
11553
- if (tui) {
11554
- tui.recordResult(result);
11555
- } else {
11556
- printRunnerResult("start", result, jsonMode);
11557
- }
11558
12579
  } else {
11559
- runnerLogger?.append("route_result", {
11560
- route_key: String(result?.route_key || routeKey).trim(),
11561
- route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
11562
- outcome: "busy",
11563
- detail: String(result?.detail || "").trim(),
11564
- comment_id: String(result?.comment_id || "").trim(),
12580
+ publishRunnerStartImmediateResult({
12581
+ result,
12582
+ routeKey,
12583
+ normalizedRoute,
12584
+ runnerLogger,
12585
+ tui,
12586
+ jsonMode,
11565
12587
  });
11566
- tui?.recordResult(result);
11567
12588
  }
11568
12589
  } catch (err) {
11569
- const errorText = String(err?.message || err);
11570
- const fatalArchiveBootstrapError = errorText.includes("Archive thread is missing")
11571
- && errorText.includes("write access is denied");
11572
- cycleOutcome = fatalArchiveBootstrapError ? "blocked" : "error";
11573
- saveRunnerRouteState(routeKey, {
11574
- ...safeObject(loadBotRunnerState().routes[routeKey]),
11575
- last_error: errorText,
11576
- });
11577
- const result = {
11578
- route_key: routeKey,
11579
- route_name: normalizedRoute.name,
11580
- logical_signature: runnerRouteLogicalSignature(normalizedRoute),
11581
- outcome: fatalArchiveBootstrapError ? "blocked" : "error",
11582
- detail: errorText,
11583
- };
11584
- runnerLogger?.append("route_result", {
11585
- route_key: routeKey,
11586
- route_name: normalizedRoute.name,
11587
- outcome: String(result.outcome || "error").trim(),
11588
- detail: errorText,
12590
+ cycleOutcome = handleRunnerStartRouteCycleError({
12591
+ normalizedRoute,
12592
+ routeKey,
12593
+ errorText: String(err?.message || err),
12594
+ runnerLogger,
12595
+ tui,
12596
+ jsonMode,
11589
12597
  });
11590
- if (tui) {
11591
- tui.recordResult(result);
11592
- } else {
11593
- printRunnerResult("start", result, jsonMode);
11594
- }
11595
12598
  } finally {
11596
- let routeState = safeObject(loadBotRunnerState().routes[routeKey]);
11597
- let activeExecutionState = resolveRunnerActiveExecutionState(routeState);
11598
- const successfulCycleOutcome = [
11599
- "accepted",
11600
- "busy",
11601
- "idle",
11602
- "primed",
11603
- "skipped",
11604
- "replied",
11605
- "dry_run",
11606
- "delivery_failed_after_generation",
11607
- ].includes(cycleOutcome);
11608
- const shouldClearLastError = successfulCycleOutcome
11609
- && String(routeState.last_error || "").trim();
11610
- const shouldClearStaleActiveExecution = !activeExecutionState.active
11611
- && successfulCycleOutcome
11612
- && String(routeState.active_comment_id || "").trim();
11613
- if (shouldClearLastError || shouldClearStaleActiveExecution) {
11614
- saveRunnerRouteState(routeKey, {
11615
- ...(shouldClearStaleActiveExecution ? emptyRunnerActiveExecutionPatch() : {}),
11616
- ...(shouldClearLastError ? { last_error: "" } : {}),
11617
- });
11618
- routeState = safeObject(loadBotRunnerState().routes[routeKey]);
11619
- activeExecutionState = resolveRunnerActiveExecutionState(routeState);
11620
- }
11621
- tui?.setRouteState(routeKey, {
11622
- intent_type: String(routeState.last_intent_type || "").trim(),
11623
- source_message_id: intFromRawAllowZero(routeState.last_source_message_id, 0),
11624
- warning: String(activeExecutionState.warning || "").trim(),
11625
- last_error: String(routeState.last_error || "").trim(),
12599
+ finalizeRunnerStartRouteCycle({
12600
+ routeKey,
12601
+ normalizedRoute,
12602
+ cycleOutcome,
12603
+ schedules,
12604
+ tui,
11626
12605
  });
11627
- const lastErrorText = String(routeState.last_error || "").trim();
11628
- const isFatalArchiveBootstrapError = lastErrorText.includes("Archive thread is missing")
11629
- && lastErrorText.includes("write access is denied");
11630
- const nextRunAt = Date.now() + (isFatalArchiveBootstrapError
11631
- ? 60000
11632
- : Math.max(1000, normalizedRoute.pollIntervalMs));
11633
- schedules.set(routeKey, nextRunAt);
11634
- tui?.updateNextRunAt(routeKey, nextRunAt);
11635
12606
  }
11636
12607
  }
11637
12608
  });
11638
12609
  }
11639
- for (const route of routes) {
11640
- const normalizedRoute = normalizeRunnerRoute(route);
11641
- const routeKey = runnerRouteKey(normalizedRoute);
11642
- const nextAt = Number(schedules.get(routeKey) || 0);
11643
- if (nextAt > Date.now()) {
11644
- nextSleepMs = Math.min(nextSleepMs, Math.max(250, nextAt - Date.now()));
11645
- }
11646
- }
12610
+ nextSleepMs = refreshRunnerStartNextSleepMs(routes, schedules, nextSleepMs, Date.now());
11647
12611
  if (!stopRequested) {
11648
12612
  await sleep(Math.max(250, nextSleepMs));
11649
12613
  }