@flowdesk/opencode-plugin 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-task-output.d.ts +17 -0
  3. package/dist/agent-task-output.d.ts.map +1 -0
  4. package/dist/agent-task-output.js +119 -0
  5. package/dist/agent-task-output.js.map +1 -0
  6. package/dist/agent-task-runner.d.ts +24 -0
  7. package/dist/agent-task-runner.d.ts.map +1 -1
  8. package/dist/agent-task-runner.js +536 -61
  9. package/dist/agent-task-runner.js.map +1 -1
  10. package/dist/auto-continue-preview-tool.d.ts +36 -0
  11. package/dist/auto-continue-preview-tool.d.ts.map +1 -0
  12. package/dist/auto-continue-preview-tool.js +119 -0
  13. package/dist/auto-continue-preview-tool.js.map +1 -0
  14. package/dist/completion-ui-cache.d.ts +6 -0
  15. package/dist/completion-ui-cache.d.ts.map +1 -0
  16. package/dist/completion-ui-cache.js +260 -0
  17. package/dist/completion-ui-cache.js.map +1 -0
  18. package/dist/controlled-write-tool.d.ts +49 -0
  19. package/dist/controlled-write-tool.d.ts.map +1 -0
  20. package/dist/controlled-write-tool.js +296 -0
  21. package/dist/controlled-write-tool.js.map +1 -0
  22. package/dist/event-hook-observer.d.ts +14 -0
  23. package/dist/event-hook-observer.d.ts.map +1 -0
  24. package/dist/event-hook-observer.js +193 -0
  25. package/dist/event-hook-observer.js.map +1 -0
  26. package/dist/managed-dispatch-adapter.d.ts +1 -0
  27. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  28. package/dist/managed-dispatch-adapter.js +100 -29
  29. package/dist/managed-dispatch-adapter.js.map +1 -1
  30. package/dist/model-selection-engine.d.ts +47 -0
  31. package/dist/model-selection-engine.d.ts.map +1 -0
  32. package/dist/model-selection-engine.js +175 -0
  33. package/dist/model-selection-engine.js.map +1 -0
  34. package/dist/provider-usage-live-tool.d.ts +27 -0
  35. package/dist/provider-usage-live-tool.d.ts.map +1 -1
  36. package/dist/provider-usage-live-tool.js +443 -4
  37. package/dist/provider-usage-live-tool.js.map +1 -1
  38. package/dist/quick-reviewer-run.d.ts +3 -0
  39. package/dist/quick-reviewer-run.d.ts.map +1 -1
  40. package/dist/quick-reviewer-run.js +20 -8
  41. package/dist/quick-reviewer-run.js.map +1 -1
  42. package/dist/runtime-reviewer-execution-bridge.d.ts +21 -0
  43. package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
  44. package/dist/runtime-reviewer-execution-bridge.js +238 -0
  45. package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
  46. package/dist/server.d.ts +60 -1
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +800 -41
  49. package/dist/server.js.map +1 -1
  50. package/dist/stall-recovery.d.ts +60 -0
  51. package/dist/stall-recovery.d.ts.map +1 -1
  52. package/dist/stall-recovery.js +763 -11
  53. package/dist/stall-recovery.js.map +1 -1
  54. package/dist/status-live-tool.d.ts +81 -0
  55. package/dist/status-live-tool.d.ts.map +1 -1
  56. package/dist/status-live-tool.js +620 -38
  57. package/dist/status-live-tool.js.map +1 -1
  58. package/dist/tui-subtask-activity.d.ts +69 -0
  59. package/dist/tui-subtask-activity.d.ts.map +1 -0
  60. package/dist/tui-subtask-activity.js +266 -0
  61. package/dist/tui-subtask-activity.js.map +1 -0
  62. package/dist/tui-usage-snapshot.d.ts +44 -0
  63. package/dist/tui-usage-snapshot.d.ts.map +1 -0
  64. package/dist/tui-usage-snapshot.js +397 -0
  65. package/dist/tui-usage-snapshot.js.map +1 -0
  66. package/dist/tui.d.ts +7 -0
  67. package/dist/tui.d.ts.map +1 -0
  68. package/dist/tui.js +134 -0
  69. package/dist/tui.js.map +1 -0
  70. package/dist/workflow-assign-tool.d.ts +23 -0
  71. package/dist/workflow-assign-tool.d.ts.map +1 -0
  72. package/dist/workflow-assign-tool.js +117 -0
  73. package/dist/workflow-assign-tool.js.map +1 -0
  74. package/dist/workflow-author-tool.d.ts +29 -0
  75. package/dist/workflow-author-tool.d.ts.map +1 -0
  76. package/dist/workflow-author-tool.js +227 -0
  77. package/dist/workflow-author-tool.js.map +1 -0
  78. package/dist/workflow-dispatch-plan-tool.d.ts +47 -0
  79. package/dist/workflow-dispatch-plan-tool.d.ts.map +1 -0
  80. package/dist/workflow-dispatch-plan-tool.js +251 -0
  81. package/dist/workflow-dispatch-plan-tool.js.map +1 -0
  82. package/dist/workflow-dispatch-tool.d.ts +56 -0
  83. package/dist/workflow-dispatch-tool.d.ts.map +1 -0
  84. package/dist/workflow-dispatch-tool.js +306 -0
  85. package/dist/workflow-dispatch-tool.js.map +1 -0
  86. package/dist/workflow-orchestrator.d.ts +31 -0
  87. package/dist/workflow-orchestrator.d.ts.map +1 -0
  88. package/dist/workflow-orchestrator.js +160 -0
  89. package/dist/workflow-orchestrator.js.map +1 -0
  90. package/dist/workflow-scheduler.d.ts +19 -0
  91. package/dist/workflow-scheduler.d.ts.map +1 -0
  92. package/dist/workflow-scheduler.js +45 -0
  93. package/dist/workflow-scheduler.js.map +1 -0
  94. package/dist/workflow-synthesis-tool.d.ts +31 -0
  95. package/dist/workflow-synthesis-tool.d.ts.map +1 -0
  96. package/dist/workflow-synthesis-tool.js +194 -0
  97. package/dist/workflow-synthesis-tool.js.map +1 -0
  98. package/package.json +10 -2
@@ -1,9 +1,12 @@
1
1
  import { applyFlowDeskSessionEvidenceWriteIntentsV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, projectFlowDeskLaneStallV1, } from "@flowdesk/core";
2
- import { createHmac, createHash, timingSafeEqual } from "node:crypto";
2
+ import { createHmac, createHash, timingSafeEqual, randomBytes } from "node:crypto";
3
3
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1, } from "./managed-dispatch-adapter.js";
6
6
  import { FLOWDESK_TIMEOUT_DEFAULTS, FlowDeskTimeoutError, withTimeout, } from "./shared/with-timeout.js";
7
+ import { executeFlowDeskAgentTaskV1, AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION, sanitizeFlowDeskTaskResultTextV1 } from "./agent-task-runner.js";
8
+ import { observeFlowDeskAgentTaskOutputV1 } from "./agent-task-output.js";
9
+ import { refreshFlowDeskCompletionUiCachesV1 } from "./completion-ui-cache.js";
7
10
  export async function checkSdkSessionApiHealthV1(client, sessionId, timeouts = {}) {
8
11
  if (typeof client.session.messages !== "function") {
9
12
  return { status: "unknown", reason: "sdk_messages_not_available" };
@@ -423,6 +426,15 @@ export function evaluateGuardedAutoAbortHookV1(input) {
423
426
  function isReviewerLaneContext(value) {
424
427
  return isRecord(value) && value.schema_version === "flowdesk.reviewer_lane_context.v1";
425
428
  }
429
+ function isAgentTaskContext(value) {
430
+ return isRecord(value) && value.schema_version === "flowdesk.agent_task_context.v1";
431
+ }
432
+ function isTaskFailed(value) {
433
+ return isRecord(value) && value.schema_version === "flowdesk.task_failed.v1";
434
+ }
435
+ function isTaskResult(value) {
436
+ return isRecord(value) && value.schema_version === "flowdesk.task_result.v1";
437
+ }
426
438
  function isPendingRetryPlan(value) {
427
439
  return isRecord(value) && value.schema_version === "flowdesk.pending_retry_plan.v1";
428
440
  }
@@ -432,6 +444,152 @@ function isRetryExecuted(value) {
432
444
  function isRetryFailed(value) {
433
445
  return isRecord(value) && value.schema_version === "flowdesk.retry_failed.v1";
434
446
  }
447
+ function latestLifecycleByLane(records) {
448
+ const byLane = new Map();
449
+ for (const record of records) {
450
+ const existing = byLane.get(record.lane_id);
451
+ if (existing === undefined ||
452
+ Date.parse(record.updated_at) > Date.parse(existing.updated_at) ||
453
+ (Date.parse(record.updated_at) === Date.parse(existing.updated_at) && record.state > existing.state)) {
454
+ byLane.set(record.lane_id, record);
455
+ }
456
+ }
457
+ return byLane;
458
+ }
459
+ function latestTaskFailedByLane(records) {
460
+ const byLane = new Map();
461
+ for (const record of records) {
462
+ const existing = byLane.get(record.lane_id);
463
+ if (existing === undefined || Date.parse(record.created_at) >= Date.parse(existing.created_at)) {
464
+ byLane.set(record.lane_id, record);
465
+ }
466
+ }
467
+ return byLane;
468
+ }
469
+ function latestTaskResultByLane(records) {
470
+ const byLane = new Map();
471
+ for (const record of records) {
472
+ const existing = byLane.get(record.lane_id);
473
+ if (existing === undefined || Date.parse(record.created_at) >= Date.parse(existing.created_at)) {
474
+ byLane.set(record.lane_id, record);
475
+ }
476
+ }
477
+ return byLane;
478
+ }
479
+ function agentTaskContextByLane(records) {
480
+ const byLane = new Map();
481
+ for (const record of records)
482
+ byLane.set(record.lane_id, record);
483
+ return byLane;
484
+ }
485
+ /**
486
+ * Safe local cleanup/backfill for legacy `flowdesk_agent_task_run` lanes.
487
+ *
488
+ * Older agent-task results/failures could persist `task_result.v1` or
489
+ * `task_failed.v1` while the latest `lane_lifecycle` remained
490
+ * `created`/`running`, which made status cards keep reporting stale active
491
+ * lanes. This helper only writes terminal lifecycle evidence when existing
492
+ * durable evidence already proves the agent-task ended.
493
+ * It never launches, aborts, retries, enables dispatch authority, or rewrites
494
+ * existing evidence.
495
+ */
496
+ export function backfillTerminalAgentTaskFailedLanesV1(input) {
497
+ const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
498
+ if (!reloaded.ok)
499
+ return { status: "backfill_skipped", workflowId: input.workflowId, reason: "session_evidence_reload_failed" };
500
+ const lifecycles = [];
501
+ const taskResults = [];
502
+ const taskFailures = [];
503
+ const contexts = [];
504
+ for (const entry of reloaded.entries) {
505
+ if (isLaneLifecycleRecord(entry.record))
506
+ lifecycles.push(entry.record);
507
+ else if (isTaskResult(entry.record))
508
+ taskResults.push(entry.record);
509
+ else if (isTaskFailed(entry.record))
510
+ taskFailures.push(entry.record);
511
+ else if (isAgentTaskContext(entry.record))
512
+ contexts.push(entry.record);
513
+ }
514
+ const latestLifecycle = latestLifecycleByLane(lifecycles);
515
+ const latestResult = latestTaskResultByLane(taskResults);
516
+ const latestFailure = latestTaskFailedByLane(taskFailures);
517
+ const contextByLane = agentTaskContextByLane(contexts);
518
+ const observedAt = (input.now ?? new Date()).toISOString();
519
+ const token = timestampToken(input.now ?? new Date());
520
+ const terminalEvidenceIds = [];
521
+ let lanesScanned = 0;
522
+ for (const [laneId, result] of latestResult) {
523
+ const latest = latestLifecycle.get(laneId);
524
+ if (latest === undefined)
525
+ continue;
526
+ lanesScanned++;
527
+ if (!ABORT_ELIGIBLE_LANE_STATES.has(latest.state))
528
+ continue;
529
+ const context = contextByLane.get(laneId);
530
+ const evidenceId = `lifecycle-agent-task-terminal-backfill-${safeToken(laneId)}-${token}`;
531
+ const terminalRecord = {
532
+ ...latest,
533
+ workflow_id: result.workflow_id,
534
+ lane_id: laneId,
535
+ agent_ref: context?.agent_ref ?? result.agent_ref ?? latest.agent_ref,
536
+ provider_qualified_model_id: context?.provider_qualified_model_id ?? result.provider_qualified_model_id ?? latest.provider_qualified_model_id,
537
+ parent_session_ref: context?.parent_session_ref ?? latest.parent_session_ref,
538
+ state: "incomplete",
539
+ verdict_ref: undefined,
540
+ output_ref: `output-${safeToken(result.task_id)}`,
541
+ updated_at: observedAt,
542
+ spawned_by: latest.spawned_by ?? "flowdesk",
543
+ dispatch_authority_enabled: false,
544
+ providerCall: false,
545
+ actualLaneLaunch: false,
546
+ runtimeExecution: false,
547
+ };
548
+ if (writeEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, evidenceId, record: terminalRecord })) {
549
+ terminalEvidenceIds.push(evidenceId);
550
+ }
551
+ }
552
+ for (const [laneId, failed] of latestFailure) {
553
+ if (latestResult.has(laneId))
554
+ continue;
555
+ const latest = latestLifecycle.get(laneId);
556
+ if (latest === undefined)
557
+ continue;
558
+ lanesScanned++;
559
+ if (!ABORT_ELIGIBLE_LANE_STATES.has(latest.state))
560
+ continue;
561
+ const context = contextByLane.get(laneId);
562
+ const state = failed.failure_category === "no_response" ? "no_output" : "invocation_failed";
563
+ const evidenceId = `lifecycle-agent-task-terminal-backfill-${safeToken(laneId)}-${token}`;
564
+ const terminalRecord = {
565
+ ...latest,
566
+ workflow_id: failed.workflow_id,
567
+ lane_id: laneId,
568
+ agent_ref: context?.agent_ref ?? failed.agent_ref ?? latest.agent_ref,
569
+ provider_qualified_model_id: context?.provider_qualified_model_id ?? failed.provider_qualified_model_id ?? latest.provider_qualified_model_id,
570
+ parent_session_ref: context?.parent_session_ref ?? latest.parent_session_ref,
571
+ state,
572
+ verdict_ref: undefined,
573
+ output_ref: undefined,
574
+ updated_at: observedAt,
575
+ spawned_by: latest.spawned_by ?? "flowdesk",
576
+ dispatch_authority_enabled: false,
577
+ providerCall: false,
578
+ actualLaneLaunch: false,
579
+ runtimeExecution: false,
580
+ };
581
+ if (writeEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, evidenceId, record: terminalRecord })) {
582
+ terminalEvidenceIds.push(evidenceId);
583
+ }
584
+ }
585
+ return {
586
+ status: "backfill_completed",
587
+ workflowId: input.workflowId,
588
+ lanesScanned,
589
+ lanesTerminalized: terminalEvidenceIds.length,
590
+ terminalLifecycleEvidenceIds: terminalEvidenceIds,
591
+ };
592
+ }
435
593
  /**
436
594
  * On startup/reload: any `pending_retry_plan` in `launched` state with no matching
437
595
  * `retry_executed` or `retry_failed` within 10 minutes of `created_at` is reconciled
@@ -522,6 +680,19 @@ function buildRetryLaunchPlanFromContextV1(context, newLaneId, parentSessionId)
522
680
  runtimeExecution: false,
523
681
  };
524
682
  }
683
+ function parentSessionIdFromRef(parentSessionRef) {
684
+ return parentSessionRef.startsWith("ses-") && parentSessionRef.length > "ses-".length
685
+ ? parentSessionRef.slice("ses-".length)
686
+ : undefined;
687
+ }
688
+ function writeRetryFailedV1(input) {
689
+ writeEvidence({
690
+ rootDir: input.rootDir,
691
+ workflowId: input.workflowId,
692
+ evidenceId: input.evidenceId,
693
+ record: input.record,
694
+ });
695
+ }
525
696
  /**
526
697
  * Evaluate guarded auto-retry hook following the execution order from the design doc exactly.
527
698
  * Called after `evaluateGuardedAutoAbortHookV1` returns `auto_abort_executed`.
@@ -572,7 +743,8 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
572
743
  redactedReason: "sdk_client_missing_or_session_create_unavailable",
573
744
  };
574
745
  }
575
- // Step 4: Load reviewer_lane_context.v1 for laneId
746
+ // Step 4: Load retry context for laneId. Reviewer context remains the
747
+ // preferred P7 path; generic agent tasks fall back to agent_task_context.v1.
576
748
  const reloaded = reloadFlowDeskSessionEvidenceV1({
577
749
  rootDir: input.rootDir,
578
750
  workflowId: input.workflowId,
@@ -580,18 +752,31 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
580
752
  if (!reloaded.ok) {
581
753
  return { status: "auto_retry_disabled", reason: "context_missing" };
582
754
  }
583
- const contextEntry = reloaded.entries.find((e) => isReviewerLaneContext(e.record) && e.record.lane_id === input.laneId);
755
+ const reviewerContextEntry = reloaded.entries.find((e) => isReviewerLaneContext(e.record) && e.record.lane_id === input.laneId);
756
+ const agentTaskContextEntry = reviewerContextEntry === undefined
757
+ ? reloaded.entries.find((e) => isAgentTaskContext(e.record) && e.record.lane_id === input.laneId)
758
+ : undefined;
759
+ const contextEntry = reviewerContextEntry ?? agentTaskContextEntry;
584
760
  if (contextEntry === undefined) {
585
761
  return { status: "auto_retry_disabled", reason: "context_missing" };
586
762
  }
587
- const context = contextEntry.record;
763
+ const isAgentTaskRetry = reviewerContextEntry === undefined;
764
+ const reviewerContext = isAgentTaskRetry ? undefined : contextEntry.record;
765
+ const agentTaskContext = isAgentTaskRetry ? contextEntry.record : undefined;
766
+ const context = reviewerContext ?? agentTaskContext;
588
767
  // Step 5: Verify context.redaction_version present
589
768
  if (!context.redaction_version || typeof context.redaction_version !== "string" || context.redaction_version.trim().length === 0) {
590
769
  return { status: "auto_retry_disabled", reason: "context_redaction_invalid" };
591
770
  }
592
- // Step 6: Verify context.workflow_id === workflowId && context.perspective valid
771
+ // Step 6: Verify context.workflow_id === workflowId and context-specific invariants.
593
772
  const VALID_PERSPECTIVES = new Set(["policy_security", "architecture", "verification_implementation"]);
594
- if (context.workflow_id !== input.workflowId || !VALID_PERSPECTIVES.has(context.perspective)) {
773
+ if (context.workflow_id !== input.workflowId) {
774
+ return { status: "auto_retry_disabled", reason: "invariant_violated" };
775
+ }
776
+ if (reviewerContext !== undefined && !VALID_PERSPECTIVES.has(reviewerContext.perspective)) {
777
+ return { status: "auto_retry_disabled", reason: "invariant_violated" };
778
+ }
779
+ if (agentTaskContext !== undefined && agentTaskContext.prompt_text_truncated === true) {
595
780
  return { status: "auto_retry_disabled", reason: "invariant_violated" };
596
781
  }
597
782
  // Step 7: Count cap — retry_executed + retry_failed + pending_retry_plan(pending|launched) for laneId
@@ -674,8 +859,116 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
674
859
  redactedReason: "pending_retry_plan_write_failed",
675
860
  };
676
861
  }
862
+ if (agentTaskContext !== undefined) {
863
+ const retryParentSessionId = parentSessionIdFromRef(agentTaskContext.parent_session_ref);
864
+ if (retryParentSessionId === undefined) {
865
+ const failedId = `retry-failed-agent-task-parent-${safeToken(newLaneId)}-${retryToken}`;
866
+ writeRetryFailedV1({
867
+ rootDir: input.rootDir,
868
+ workflowId: input.workflowId,
869
+ evidenceId: failedId,
870
+ record: {
871
+ schema_version: "flowdesk.retry_failed.v1",
872
+ workflow_id: input.workflowId,
873
+ original_lane_id: input.laneId,
874
+ new_lane_id: newLaneId,
875
+ retry_attempt: retryAttempt,
876
+ failure_category: "invariant_violated",
877
+ redacted_reason: "agent_task_context_parent_session_ref_invalid",
878
+ created_at: now.toISOString(),
879
+ dispatch_authority_enabled: false,
880
+ },
881
+ });
882
+ writeEvidence({
883
+ rootDir: input.rootDir,
884
+ workflowId: input.workflowId,
885
+ evidenceId: `${pendingRetryEvidenceId}-failed`,
886
+ record: { ...pendingRecord, status: "failed" },
887
+ });
888
+ return {
889
+ status: "retry_failed",
890
+ failureCategory: "invariant_violated",
891
+ redactedReason: "agent_task_context_parent_session_ref_invalid",
892
+ };
893
+ }
894
+ const taskResult = await executeFlowDeskAgentTaskV1({
895
+ workflowId: input.workflowId,
896
+ taskId: agentTaskContext.task_id,
897
+ laneId: newLaneId,
898
+ agentRef: agentTaskContext.agent_ref,
899
+ providerQualifiedModelId: agentTaskContext.provider_qualified_model_id,
900
+ promptText: agentTaskContext.prompt_text,
901
+ parentSessionId: retryParentSessionId,
902
+ rootDir: input.rootDir,
903
+ client: input.client,
904
+ timeoutMs: timeoutMs,
905
+ _nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
906
+ _messagesTimeoutMs: input._messagesTimeoutMs,
907
+ });
908
+ if (taskResult.status === "task_failed") {
909
+ const failedId = `retry-failed-agent-task-${safeToken(newLaneId)}-${retryToken}`;
910
+ writeRetryFailedV1({
911
+ rootDir: input.rootDir,
912
+ workflowId: input.workflowId,
913
+ evidenceId: failedId,
914
+ record: {
915
+ schema_version: "flowdesk.retry_failed.v1",
916
+ workflow_id: input.workflowId,
917
+ original_lane_id: input.laneId,
918
+ new_lane_id: newLaneId,
919
+ retry_attempt: retryAttempt,
920
+ failure_category: taskResult.failureCategory === "no_response" ? "indeterminate_launch" : "sdk_create_failed",
921
+ redacted_reason: `agent_task_retry_${taskResult.failureCategory}`,
922
+ created_at: now.toISOString(),
923
+ dispatch_authority_enabled: false,
924
+ },
925
+ });
926
+ writeEvidence({
927
+ rootDir: input.rootDir,
928
+ workflowId: input.workflowId,
929
+ evidenceId: `${pendingRetryEvidenceId}-failed`,
930
+ record: { ...pendingRecord, status: "failed" },
931
+ });
932
+ return {
933
+ status: "retry_failed",
934
+ failureCategory: taskResult.failureCategory,
935
+ redactedReason: taskResult.redactedReason,
936
+ };
937
+ }
938
+ const executedId = `retry-executed-${safeToken(newLaneId)}-${retryToken}`;
939
+ const executedRecord = {
940
+ schema_version: "flowdesk.retry_executed.v1",
941
+ workflow_id: input.workflowId,
942
+ original_lane_id: input.laneId,
943
+ new_lane_id: newLaneId,
944
+ retry_attempt: retryAttempt,
945
+ retry_kind: "agent_task",
946
+ task_id: agentTaskContext.task_id,
947
+ provider_qualified_model_id: agentTaskContext.provider_qualified_model_id,
948
+ new_parent_session_ref: agentTaskContext.parent_session_ref,
949
+ created_at: now.toISOString(),
950
+ dispatch_authority_enabled: false,
951
+ };
952
+ writeEvidence({
953
+ rootDir: input.rootDir,
954
+ workflowId: input.workflowId,
955
+ evidenceId: executedId,
956
+ record: executedRecord,
957
+ });
958
+ writeEvidence({
959
+ rootDir: input.rootDir,
960
+ workflowId: input.workflowId,
961
+ evidenceId: `${pendingRetryEvidenceId}-launched`,
962
+ record: { ...pendingRecord, status: "launched" },
963
+ });
964
+ return {
965
+ status: "retry_launched",
966
+ newLaneId,
967
+ pendingRetryEvidenceId,
968
+ };
969
+ }
677
970
  // Step 12: Call launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1
678
- const launchPlan = buildRetryLaunchPlanFromContextV1(context, newLaneId, input.parentSessionId);
971
+ const launchPlan = buildRetryLaunchPlanFromContextV1(reviewerContext, newLaneId, input.parentSessionId);
679
972
  let launchResult;
680
973
  try {
681
974
  const launchPromise = launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1({
@@ -684,7 +977,7 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
684
977
  request: {
685
978
  allowActualLaneLaunch: true,
686
979
  parentSessionId: input.parentSessionId,
687
- promptText: context.prompt_text,
980
+ promptText: reviewerContext.prompt_text,
688
981
  dispatchMethod: "prompt",
689
982
  },
690
983
  });
@@ -771,10 +1064,11 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
771
1064
  original_lane_id: input.laneId,
772
1065
  new_lane_id: newLaneId,
773
1066
  retry_attempt: retryAttempt,
774
- perspective: context.perspective,
775
- provider_qualified_model_id: context.provider_qualified_model_id,
1067
+ retry_kind: "reviewer_lane",
1068
+ perspective: reviewerContext.perspective,
1069
+ provider_qualified_model_id: reviewerContext.provider_qualified_model_id,
776
1070
  new_parent_session_ref: newParentSessionRef,
777
- original_attempt_id: context.original_attempt_id,
1071
+ original_attempt_id: reviewerContext.original_attempt_id,
778
1072
  created_at: now.toISOString(),
779
1073
  dispatch_authority_enabled: false,
780
1074
  };
@@ -879,6 +1173,23 @@ export async function runFlowDeskWatchdogCycleV1(input) {
879
1173
  const workflowIds = listWatchdogWorkflowIds(input.rootDir);
880
1174
  const now = input.now ?? new Date();
881
1175
  for (const workflowId of workflowIds) {
1176
+ // Monitor async-mode child sessions (nudge + abort + result collection)
1177
+ if (input.client !== undefined) {
1178
+ try {
1179
+ await monitorChildSessionsV1({
1180
+ rootDir: input.rootDir,
1181
+ workflowId,
1182
+ client: input.client,
1183
+ now,
1184
+ });
1185
+ }
1186
+ catch { /* best-effort, must not crash watchdog */ }
1187
+ }
1188
+ backfillTerminalAgentTaskFailedLanesV1({
1189
+ rootDir: input.rootDir,
1190
+ workflowId,
1191
+ now,
1192
+ });
882
1193
  // Reload evidence for this workflow
883
1194
  const reloaded = reloadFlowDeskSessionEvidenceV1({
884
1195
  rootDir: input.rootDir,
@@ -923,6 +1234,8 @@ export async function runFlowDeskWatchdogCycleV1(input) {
923
1234
  client: input.client,
924
1235
  parentSessionId: input.parentSessionId,
925
1236
  now,
1237
+ _nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
1238
+ _messagesTimeoutMs: input._messagesTimeoutMs,
926
1239
  });
927
1240
  if (retryResult.status === "retry_launched") {
928
1241
  lanesRetried++;
@@ -959,4 +1272,443 @@ export async function runFlowDeskWatchdogCycleV1(input) {
959
1272
  lanesFailed,
960
1273
  };
961
1274
  }
1275
+ // ─────────────────────────────────────────────────────────────────────────────
1276
+ // Child session monitor (async-mode lanes)
1277
+ // ─────────────────────────────────────────────────────────────────────────────
1278
+ const AGENT_TASK_NUDGE_TEXT_WATCHDOG = "Please provide your final answer now. If you have completed your analysis, output your complete response.";
1279
+ /** Poll result from one session.messages call */
1280
+ function monitorRecord(value) {
1281
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1282
+ ? value
1283
+ : undefined;
1284
+ }
1285
+ function monitorResponseData(value) {
1286
+ const record = monitorRecord(value);
1287
+ return record !== undefined && "data" in record ? record.data : value;
1288
+ }
1289
+ function monitorSdkErrorResponse(value) {
1290
+ const record = monitorRecord(value);
1291
+ const data = monitorRecord(monitorResponseData(value));
1292
+ return record?.error !== undefined || data?.error !== undefined;
1293
+ }
1294
+ async function pollChildSessionOutput(client, childSessionId, messagesTimeoutMs = 3_000) {
1295
+ const messages = client.session.messages;
1296
+ if (typeof messages !== "function")
1297
+ return null;
1298
+ try {
1299
+ const readMessages = async () => {
1300
+ const current = await messages.call(client.session, {
1301
+ sessionID: childSessionId,
1302
+ });
1303
+ if (!monitorSdkErrorResponse(current))
1304
+ return current;
1305
+ return messages.call(client.session, {
1306
+ path: { id: childSessionId },
1307
+ });
1308
+ };
1309
+ const raw = await Promise.race([
1310
+ readMessages(),
1311
+ new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
1312
+ ]);
1313
+ if (raw === null)
1314
+ return null;
1315
+ const observed = observeFlowDeskAgentTaskOutputV1(raw);
1316
+ if (observed.terminalObserved && observed.latestText !== undefined && observed.latestText.trim().length > 0)
1317
+ return { text: observed.latestText, completionStatus: "final", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis };
1318
+ return null;
1319
+ }
1320
+ catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ async function pollChildSessionCandidate(client, childSessionId, messagesTimeoutMs = 3_000) {
1325
+ const messages = client.session.messages;
1326
+ if (typeof messages !== "function")
1327
+ return null;
1328
+ try {
1329
+ const readMessages = async () => {
1330
+ const current = await messages.call(client.session, { sessionID: childSessionId });
1331
+ if (!monitorSdkErrorResponse(current))
1332
+ return current;
1333
+ return messages.call(client.session, { path: { id: childSessionId } });
1334
+ };
1335
+ const raw = await Promise.race([
1336
+ readMessages(),
1337
+ new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
1338
+ ]);
1339
+ if (raw === null)
1340
+ return null;
1341
+ const observed = observeFlowDeskAgentTaskOutputV1(raw);
1342
+ if (observed.latestText !== undefined && observed.latestText.trim().length > 0)
1343
+ return { text: observed.latestText, completionStatus: observed.terminalObserved ? "final" : "partial", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis };
1344
+ return null;
1345
+ }
1346
+ catch {
1347
+ return null;
1348
+ }
1349
+ }
1350
+ /** Send a noReply nudge to a child session — best effort with hard timeout */
1351
+ async function sendWatchdogNudge(client, childSessionId, timeoutMs = 5_000) {
1352
+ const promptFn = client.session.promptAsync ?? client.session.prompt;
1353
+ if (promptFn === undefined)
1354
+ return "skipped";
1355
+ try {
1356
+ await Promise.race([
1357
+ promptFn.call(client.session, {
1358
+ sessionID: childSessionId,
1359
+ noReply: true,
1360
+ parts: [{ type: "text", text: AGENT_TASK_NUDGE_TEXT_WATCHDOG }],
1361
+ }),
1362
+ new Promise((_, reject) => setTimeout(() => reject(new Error("nudge timeout")), timeoutMs)),
1363
+ ]);
1364
+ return "sent";
1365
+ }
1366
+ catch {
1367
+ return "timeout";
1368
+ }
1369
+ }
1370
+ /** Abort a child session via the injected SDK client */
1371
+ async function abortChildSession(client, childSessionId) {
1372
+ const abort = client.session.abort;
1373
+ if (typeof abort !== "function")
1374
+ return;
1375
+ try {
1376
+ await abort.call(client.session, {
1377
+ path: { id: childSessionId },
1378
+ });
1379
+ }
1380
+ catch { /* best-effort */ }
1381
+ }
1382
+ function writeChildSessionEvidence(rootDir, workflowId, evidenceId, record) {
1383
+ const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({ workflowId, evidenceId, record });
1384
+ if (!prepared.ok || prepared.writeIntent === undefined)
1385
+ return false;
1386
+ const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(rootDir, [prepared.writeIntent]);
1387
+ return applied.ok && applied.writtenPaths.length > 0;
1388
+ }
1389
+ function laneAlreadyHasTerminalTaskEvidence(input) {
1390
+ const reloaded = reloadFlowDeskSessionEvidenceV1({
1391
+ rootDir: input.rootDir,
1392
+ workflowId: input.workflowId,
1393
+ });
1394
+ if (!reloaded.ok)
1395
+ return false;
1396
+ return reloaded.entries.some((entry) => {
1397
+ if (entry.evidenceClass !== "task_result" && entry.evidenceClass !== "task_failed")
1398
+ return false;
1399
+ const record = entry.record;
1400
+ return record.lane_id === input.laneId;
1401
+ });
1402
+ }
1403
+ function childProgressLabel(value) {
1404
+ const compact = value.replace(/\s+/g, " ").trim();
1405
+ return compact.length > 120 ? `${compact.slice(0, 119)}…` : compact;
1406
+ }
1407
+ function writeAgentTaskProgressEvidence(input) {
1408
+ const record = {
1409
+ schema_version: "flowdesk.agent_task_progress.v1",
1410
+ workflow_id: input.workflowId,
1411
+ lane_id: input.laneId,
1412
+ task_id: input.taskId,
1413
+ agent_ref: input.agentRef,
1414
+ provider_qualified_model_id: input.providerQualifiedModelId,
1415
+ progress_seq: input.progressSeq,
1416
+ observed_at: input.observedAt,
1417
+ phase: input.phase,
1418
+ progress_label: childProgressLabel(input.progressLabel),
1419
+ progress_ref: `progress-${input.laneId}-${input.progressSeq}`,
1420
+ redaction_version: "v1",
1421
+ dispatch_authority_enabled: false,
1422
+ };
1423
+ writeChildSessionEvidence(input.rootDir, input.workflowId, `agent-task-progress-${input.laneId}-${input.progressSeq}`, record);
1424
+ }
1425
+ /**
1426
+ * Monitor all async-mode agent task lanes in the given workflow.
1427
+ * Called from the watchdog cycle once per interval:
1428
+ * - Poll session.messages for each running child session
1429
+ * - On result: write task_result evidence + terminal lifecycle
1430
+ * - At 20s silence: nudge with noReply: true
1431
+ * - At 40s: second nudge
1432
+ * - At 60s+: session.abort + task_failed + terminal lifecycle
1433
+ */
1434
+ export async function monitorChildSessionsV1(input) {
1435
+ const nowMs = (input.now ?? new Date()).getTime();
1436
+ const nudgeQuietPeriodMs = input.nudgeQuietPeriodMs ?? 20_000;
1437
+ const maxNudges = input.maxNudges ?? 2;
1438
+ const abortThresholdMs = input.abortThresholdMs ?? 60_000;
1439
+ const result = { lanesPolled: 0, lanesCompleted: 0, lanesNudged: 0, lanesAborted: 0 };
1440
+ const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
1441
+ if (!reloaded.ok)
1442
+ return result;
1443
+ // Find all child session records for running lanes
1444
+ const childRecords = reloaded.entries
1445
+ .filter(e => e.evidenceClass === "agent_task_child_session")
1446
+ .map(e => e.record)
1447
+ .filter(r => r.schema_version === AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION);
1448
+ // Find lanes that are NOT yet terminal (by lifecycle state or by task_result/task_failed evidence)
1449
+ const terminalLaneIds = new Set();
1450
+ const terminalTaskResultLaneIds = new Set();
1451
+ const awaitingPermissionLaneIds = new Set();
1452
+ const latestProgressByLane = new Map();
1453
+ for (const entry of reloaded.entries) {
1454
+ const rec = entry.record;
1455
+ const laneIdVal = typeof rec.lane_id === "string" ? rec.lane_id : undefined;
1456
+ if (!laneIdVal)
1457
+ continue;
1458
+ if (entry.evidenceClass === "agent_task_progress") {
1459
+ const observedAtMs = typeof rec.observed_at === "string" ? Date.parse(rec.observed_at) : NaN;
1460
+ const phase = typeof rec.phase === "string" ? rec.phase : undefined;
1461
+ const current = latestProgressByLane.get(laneIdVal);
1462
+ if (phase !== undefined && Number.isFinite(observedAtMs) && (current === undefined || current.observedAtMs <= observedAtMs)) {
1463
+ latestProgressByLane.set(laneIdVal, { observedAtMs, phase });
1464
+ }
1465
+ }
1466
+ if (entry.evidenceClass === "lane_lifecycle") {
1467
+ const s = rec.state;
1468
+ if (s === "complete" || s === "invocation_failed" || s === "no_output" || s === "incomplete")
1469
+ terminalLaneIds.add(laneIdVal);
1470
+ }
1471
+ else if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
1472
+ terminalLaneIds.add(laneIdVal);
1473
+ if (entry.evidenceClass === "task_result")
1474
+ terminalTaskResultLaneIds.add(laneIdVal);
1475
+ }
1476
+ }
1477
+ for (const [laneId, progress] of latestProgressByLane) {
1478
+ if (progress.phase === "awaiting_permission")
1479
+ awaitingPermissionLaneIds.add(laneId);
1480
+ }
1481
+ // Defensive fallback for older/event-written progress records: if any explicit
1482
+ // awaiting_permission marker is present and no later permission response has
1483
+ // been observed, suspend watchdog nudge/abort for the lane.
1484
+ for (const entry of reloaded.entries) {
1485
+ if (entry.evidenceClass !== "agent_task_progress")
1486
+ continue;
1487
+ const rec = entry.record;
1488
+ if (rec.phase === "awaiting_permission" && typeof rec.lane_id === "string")
1489
+ awaitingPermissionLaneIds.add(rec.lane_id);
1490
+ if (rec.phase === "waiting" && typeof rec.progress_label === "string" && rec.progress_label.includes("permission response") && typeof rec.lane_id === "string")
1491
+ awaitingPermissionLaneIds.delete(rec.lane_id);
1492
+ }
1493
+ for (const record of childRecords) {
1494
+ const laneId = typeof record.lane_id === "string" ? record.lane_id : "";
1495
+ const childSessionId = typeof record.child_session_id === "string" ? record.child_session_id : "";
1496
+ if (!laneId || !childSessionId)
1497
+ continue;
1498
+ if (terminalLaneIds.has(laneId)) {
1499
+ if (terminalTaskResultLaneIds.has(laneId)) {
1500
+ refreshFlowDeskCompletionUiCachesV1({
1501
+ rootDir: input.rootDir,
1502
+ workflowId: input.workflowId,
1503
+ observedAt: new Date(nowMs).toISOString(),
1504
+ });
1505
+ }
1506
+ continue; // already done
1507
+ }
1508
+ if (awaitingPermissionLaneIds.has(laneId))
1509
+ continue; // user permission is outstanding; do not nudge or abort
1510
+ result.lanesPolled++;
1511
+ const createdAtMs = typeof record.created_at === "string" ? Date.parse(record.created_at) : nowMs;
1512
+ const nudgeCount = typeof record.nudge_count === "number" ? record.nudge_count : 0;
1513
+ const lastNudgeAtMs = typeof record.last_nudge_at === "string" ? Date.parse(record.last_nudge_at) : createdAtMs;
1514
+ const silenceMs = nowMs - lastNudgeAtMs;
1515
+ const totalAgeMs = nowMs - createdAtMs;
1516
+ // 1. Try to collect terminal result text. Candidate text without terminal is kept for abort-time partial capture.
1517
+ const resultObservation = await pollChildSessionOutput(input.client, childSessionId);
1518
+ if (resultObservation !== null && resultObservation.text.trim().length > 0) {
1519
+ const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
1520
+ const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
1521
+ const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
1522
+ if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
1523
+ continue;
1524
+ }
1525
+ const token = randomBytes(4).toString("hex");
1526
+ const completedAt = new Date(nowMs).toISOString();
1527
+ const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(resultObservation.text);
1528
+ const finalText = sanitizedResult.text;
1529
+ const taskResultEvidenceId = `task-result-${taskId}-watchdog-${token}`;
1530
+ const taskResultWritten = input._forceTaskResultWriteFailureForTest === true
1531
+ ? false
1532
+ : writeChildSessionEvidence(input.rootDir, input.workflowId, taskResultEvidenceId, {
1533
+ schema_version: "flowdesk.task_result.v1",
1534
+ workflow_id: input.workflowId,
1535
+ lane_id: laneId,
1536
+ task_id: taskId,
1537
+ agent_ref: agentRef,
1538
+ provider_qualified_model_id: modelId,
1539
+ task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
1540
+ result_text: finalText,
1541
+ result_text_truncated: sanitizedResult.truncated,
1542
+ result_text_sha256: createHash("sha256").update(resultObservation.text).digest("hex"),
1543
+ completion_status: resultObservation.completionStatus,
1544
+ output_kind: resultObservation.outputKind,
1545
+ usable_for_synthesis: resultObservation.usableForSynthesis,
1546
+ missing_contract: false,
1547
+ created_at: completedAt,
1548
+ dispatch_authority_enabled: false,
1549
+ });
1550
+ if (!taskResultWritten) {
1551
+ writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-result-write-${token}`, {
1552
+ schema_version: "flowdesk.task_failed.v1",
1553
+ workflow_id: input.workflowId,
1554
+ lane_id: laneId,
1555
+ task_id: taskId,
1556
+ agent_ref: agentRef,
1557
+ provider_qualified_model_id: modelId,
1558
+ failure_category: "unknown",
1559
+ redacted_reason: "watchdog could not persist task_result evidence",
1560
+ created_at: completedAt,
1561
+ dispatch_authority_enabled: false,
1562
+ });
1563
+ writeAgentTaskProgressEvidence({
1564
+ rootDir: input.rootDir,
1565
+ workflowId: input.workflowId,
1566
+ laneId,
1567
+ taskId,
1568
+ agentRef,
1569
+ providerQualifiedModelId: modelId,
1570
+ phase: "failed",
1571
+ progressSeq: 20 + nudgeCount,
1572
+ progressLabel: "async agent task result persistence failed",
1573
+ observedAt: completedAt,
1574
+ });
1575
+ continue;
1576
+ }
1577
+ writeAgentTaskProgressEvidence({
1578
+ rootDir: input.rootDir,
1579
+ workflowId: input.workflowId,
1580
+ laneId,
1581
+ taskId,
1582
+ agentRef,
1583
+ providerQualifiedModelId: modelId,
1584
+ phase: "finalizing",
1585
+ progressSeq: 10 + nudgeCount,
1586
+ progressLabel: "async agent task result captured by watchdog",
1587
+ observedAt: completedAt,
1588
+ });
1589
+ refreshFlowDeskCompletionUiCachesV1({
1590
+ rootDir: input.rootDir,
1591
+ workflowId: input.workflowId,
1592
+ observedAt: completedAt,
1593
+ });
1594
+ result.lanesCompleted++;
1595
+ continue;
1596
+ }
1597
+ // 2. Abort threshold exceeded
1598
+ if (totalAgeMs >= abortThresholdMs && nudgeCount >= maxNudges) {
1599
+ await abortChildSession(input.client, childSessionId);
1600
+ const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
1601
+ const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
1602
+ const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
1603
+ if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
1604
+ continue;
1605
+ }
1606
+ const token = randomBytes(4).toString("hex");
1607
+ const abortedAt = new Date(nowMs).toISOString();
1608
+ const partialObservation = await pollChildSessionCandidate(input.client, childSessionId);
1609
+ if (partialObservation !== null && partialObservation.text.trim().length > 0) {
1610
+ const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(partialObservation.text);
1611
+ const taskResultWritten = writeChildSessionEvidence(input.rootDir, input.workflowId, `task-result-${taskId}-watchdog-partial-${token}`, {
1612
+ schema_version: "flowdesk.task_result.v1",
1613
+ workflow_id: input.workflowId,
1614
+ lane_id: laneId,
1615
+ task_id: taskId,
1616
+ agent_ref: agentRef,
1617
+ provider_qualified_model_id: modelId,
1618
+ task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
1619
+ result_text: sanitizedResult.text,
1620
+ result_text_truncated: sanitizedResult.truncated,
1621
+ result_text_sha256: createHash("sha256").update(partialObservation.text).digest("hex"),
1622
+ completion_status: "partial",
1623
+ output_kind: partialObservation.outputKind,
1624
+ usable_for_synthesis: partialObservation.usableForSynthesis,
1625
+ missing_contract: true,
1626
+ created_at: abortedAt,
1627
+ dispatch_authority_enabled: false,
1628
+ });
1629
+ if (taskResultWritten) {
1630
+ writeAgentTaskProgressEvidence({
1631
+ rootDir: input.rootDir,
1632
+ workflowId: input.workflowId,
1633
+ laneId,
1634
+ taskId,
1635
+ agentRef,
1636
+ providerQualifiedModelId: modelId,
1637
+ phase: "finalizing",
1638
+ progressSeq: 20 + nudgeCount,
1639
+ progressLabel: "async agent task partial result captured before abort",
1640
+ observedAt: abortedAt,
1641
+ });
1642
+ refreshFlowDeskCompletionUiCachesV1({
1643
+ rootDir: input.rootDir,
1644
+ workflowId: input.workflowId,
1645
+ observedAt: abortedAt,
1646
+ });
1647
+ result.lanesCompleted++;
1648
+ continue;
1649
+ }
1650
+ }
1651
+ const failureCategory = totalAgeMs > abortThresholdMs * 2
1652
+ ? "network_interrupted"
1653
+ : "sdk_prompt_timeout";
1654
+ writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-abort-${token}`, {
1655
+ schema_version: "flowdesk.task_failed.v1",
1656
+ workflow_id: input.workflowId,
1657
+ lane_id: laneId,
1658
+ task_id: taskId,
1659
+ agent_ref: agentRef,
1660
+ provider_qualified_model_id: modelId,
1661
+ failure_category: failureCategory,
1662
+ redacted_reason: `watchdog aborted child session after ${Math.round(totalAgeMs / 1000)}s with no response`,
1663
+ created_at: abortedAt,
1664
+ dispatch_authority_enabled: false,
1665
+ });
1666
+ writeAgentTaskProgressEvidence({
1667
+ rootDir: input.rootDir,
1668
+ workflowId: input.workflowId,
1669
+ laneId,
1670
+ taskId,
1671
+ agentRef,
1672
+ providerQualifiedModelId: modelId,
1673
+ phase: "failed",
1674
+ progressSeq: 20 + nudgeCount,
1675
+ progressLabel: "async agent task aborted after no response",
1676
+ observedAt: abortedAt,
1677
+ });
1678
+ result.lanesAborted++;
1679
+ continue;
1680
+ }
1681
+ // 3. Nudge if silence threshold exceeded
1682
+ if (silenceMs >= nudgeQuietPeriodMs && nudgeCount < maxNudges) {
1683
+ await sendWatchdogNudge(input.client, childSessionId);
1684
+ result.lanesNudged++;
1685
+ const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
1686
+ const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
1687
+ const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
1688
+ const nudgedAt = new Date(nowMs).toISOString();
1689
+ writeAgentTaskProgressEvidence({
1690
+ rootDir: input.rootDir,
1691
+ workflowId: input.workflowId,
1692
+ laneId,
1693
+ taskId,
1694
+ agentRef,
1695
+ providerQualifiedModelId: modelId,
1696
+ phase: "nudged",
1697
+ progressSeq: 2 + nudgeCount,
1698
+ progressLabel: "async agent task nudged after quiet period",
1699
+ observedAt: nudgedAt,
1700
+ });
1701
+ // Update nudge_count in evidence (overwrite record)
1702
+ const evidenceId = reloaded.entries
1703
+ .find(e => e.evidenceClass === "agent_task_child_session" && e.record.lane_id === laneId)
1704
+ ?.evidenceId ?? `agent-task-child-session-${laneId}-watchdog`;
1705
+ writeChildSessionEvidence(input.rootDir, input.workflowId, evidenceId, {
1706
+ ...record,
1707
+ nudge_count: nudgeCount + 1,
1708
+ last_nudge_at: new Date(nowMs).toISOString(),
1709
+ });
1710
+ }
1711
+ }
1712
+ return result;
1713
+ }
962
1714
  //# sourceMappingURL=stall-recovery.js.map