@tea-agent/loop-agent 0.37.0 → 0.37.1-beta.0

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 (192) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +71 -144
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/shared/operator/capabilities.js +13 -200
  18. package/dist/shared/package-metadata.js +0 -42
  19. package/dist/task/contract/project.js +0 -4
  20. package/dist/task/source-prepare/completeness.js +0 -53
  21. package/dist/task/source-prepare/parse-intent.js +12 -82
  22. package/dist/task/source-prepare/prepare.js +5 -102
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -152
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -111
  29. package/dist/worker/console/chat/routes.js +10 -43
  30. package/dist/worker/console/chat/session-store.js +1 -9
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -204
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +14 -391
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -114
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -67
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +3 -10
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  70. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  71. package/dist/workflows/dag/frontend-repair.js +18 -219
  72. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  73. package/dist/workflows/dag/init-hybrid.js +82 -165
  74. package/dist/workflows/dag/node-execution.js +0 -89
  75. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  76. package/dist/workflows/dag/runner.js +15 -333
  77. package/dist/workflows/dag/scheduler.js +3 -257
  78. package/dist/workflows/dag/types.js +9 -130
  79. package/docs/README.md +3 -3
  80. package/docs/architecture/evolution.md +67 -102
  81. package/docs/templates/backend-test-dag.json +50 -32
  82. package/docs/templates/init-managed-agents.md +2 -5
  83. package/harness.json +2 -2
  84. package/package.json +3 -9
  85. package/skills/loop-agent/SKILL.md +0 -1
  86. package/skills/loop-agent/references/command-reference.md +0 -4
  87. package/dist/build-stamp.json +0 -6
  88. package/dist/commands/dag-request-interrupt.js +0 -20
  89. package/dist/worker/console/chat/assistant-content.js +0 -121
  90. package/dist/worker/console/chat/semantic-activity.js +0 -477
  91. package/dist/worker/console/operation-run-facts.js +0 -190
  92. package/dist/worker/console/operation-wait.js +0 -355
  93. package/dist/worker/console/recovery-error-copy.js +0 -198
  94. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +0 -1
  95. package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +0 -1
  96. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +0 -36
  97. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +0 -132
  98. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +0 -10
  99. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  100. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +0 -1
  101. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +0 -1
  102. package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +0 -231
  103. package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +0 -15
  104. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +0 -88
  105. package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +0 -206
  106. package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +0 -1
  107. package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +0 -1
  108. package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +0 -1
  109. package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +0 -1
  110. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  112. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +0 -1
  113. package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +0 -166
  114. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +0 -62
  115. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +0 -321
  116. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +0 -4
  117. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +0 -1
  118. package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +0 -30
  119. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +0 -3
  120. package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +0 -24
  121. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +0 -24
  122. package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +0 -41
  123. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +0 -1
  124. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +0 -85
  125. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +0 -156
  126. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +0 -292
  127. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +0 -106
  128. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +0 -1
  129. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  130. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  131. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +0 -2
  132. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +0 -1
  133. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +0 -70
  134. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +0 -139
  135. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +0 -89
  136. package/dist/worker/console/static/assets/layout-D-LzfAck.js +0 -1
  137. package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +0 -1
  138. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +0 -1
  139. package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +0 -308
  140. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +0 -96
  141. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +0 -1
  142. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +0 -1
  143. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +0 -39
  144. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +0 -7
  145. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +0 -1
  146. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +0 -84
  147. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +0 -40
  148. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +0 -162
  149. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +0 -1
  150. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +0 -1
  151. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  152. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +0 -2
  153. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
  154. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +0 -120
  155. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +0 -34
  156. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +0 -78
  157. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +0 -7
  158. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  159. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  160. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  161. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  162. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  163. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  164. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  165. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  166. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  167. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  168. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  169. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  170. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  171. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  172. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  173. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  174. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  175. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  176. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  177. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  178. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  179. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  180. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  181. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  182. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  183. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  184. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  185. package/dist/workflows/dag/contract-output-registry.js +0 -14
  186. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  187. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  188. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  189. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  190. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  191. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  192. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -4,7 +4,6 @@ import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
6
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
7
- import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
8
7
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
9
8
  import { resolveContextPolicy } from "./context-policy.js";
10
9
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
@@ -13,8 +12,6 @@ import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry,
13
12
  import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
14
13
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
15
14
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
16
- import { getStructuredContractValidator } from "./contract-output-registry.js";
17
- import "./contract-validator-registrations.js";
18
15
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
19
16
  import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
20
17
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
@@ -141,20 +138,6 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
141
138
  buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
142
139
  ].join("\n");
143
140
  }
144
- if (previousFailureCategory === "invalid-output" &&
145
- task.structuredContractOutput &&
146
- previousProtocolReason) {
147
- return [
148
- basePrompt,
149
- "",
150
- "<retry_instruction>",
151
- "Previous attempt produced an invalid frontend implementation contract:",
152
- previousProtocolReason,
153
- "Return exactly one fenced json block conforming to the frontend-implementation-contract-v1 schema.",
154
- "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
155
- "</retry_instruction>",
156
- ].join("\n");
157
- }
158
141
  if (previousFailureCategory === "writer-empty-diff") {
159
142
  const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
160
143
  // When a completeness progress exists for this writer, fold the concrete
@@ -412,21 +395,6 @@ export async function executeDagNode(input) {
412
395
  await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
413
396
  };
414
397
  const failSkillSnapshot = (error) => failBeforePrompt(error, "skill-snapshot-integrity");
415
- const skipFrontendWriter = async (admission) => {
416
- const skippedAt = new Date().toISOString();
417
- node.startedAt ??= skippedAt;
418
- node.frontendWriterAdmission = admission;
419
- node.status = "SKIPPED";
420
- node.skippedReason = "frontend-prewrite-not-authorized";
421
- node.finishedAt = skippedAt;
422
- node.lastActivityAt = skippedAt;
423
- node.durationMs = durationBetween(node.startedAt, node.finishedAt);
424
- node.timing = { retryBackoffMs: 0, settlementCleanupMs: 0 };
425
- state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
426
- await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
427
- await input.persistState();
428
- await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
429
- };
430
398
  if (task.finalWriteSetApproval) {
431
399
  const authorization = parseAndValidateFinalWriteSetApproval({ task, spec, state });
432
400
  if (!authorization.ok) {
@@ -455,30 +423,6 @@ export async function executeDagNode(input) {
455
423
  effectiveWriteSet: [...authorization.effectiveWriteSet],
456
424
  };
457
425
  }
458
- if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
459
- const admission = await readFrontendPrewriteResult(runDir);
460
- if (!admission.ok) {
461
- await skipFrontendWriter(undefined);
462
- return;
463
- }
464
- const decision = isFrontendWriterAuthorized(admission.result);
465
- const record = {
466
- schemaVersion: 1,
467
- writerNodeId: nodeId,
468
- decision,
469
- sourceArtifact: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
470
- artifactHash: admission.artifactHash,
471
- checkedAt: new Date().toISOString(),
472
- reason: decision === "denied"
473
- ? `classification: ${admission.result.classification}`
474
- : null,
475
- };
476
- if (decision === "denied") {
477
- await skipFrontendWriter(record);
478
- return;
479
- }
480
- node.frontendWriterAdmission = record;
481
- }
482
426
  let projectGovernanceContext;
483
427
  if (task.governanceStandardReview) {
484
428
  try {
@@ -745,39 +689,6 @@ export async function executeDagNode(input) {
745
689
  previousProtocolReason = undefined;
746
690
  }
747
691
  }
748
- // R1: structured contract nodes self-validate their output so schema,
749
- // typo, and null violations surface as retryable invalid-output at the
750
- // producing node instead of failing the whole run at the prewrite gate.
751
- if (result.ok && task.structuredContractOutput) {
752
- const validator = getStructuredContractValidator(task.structuredContractOutput.schemaId);
753
- if (validator) {
754
- const contractText = canonicalNodeOutput(result);
755
- const contractCheck = await validator({
756
- runDir,
757
- text: contractText,
758
- sourceBinding: spec.sourceBinding,
759
- });
760
- if (!contractCheck.ok) {
761
- result = {
762
- ...result,
763
- ok: false,
764
- failureCategory: "invalid-output",
765
- stderr: [result.stderr, contractCheck.reason]
766
- .filter(Boolean)
767
- .join("\n"),
768
- };
769
- previousProtocolReason = contractCheck.reason;
770
- }
771
- else {
772
- previousProtocolReason = undefined;
773
- }
774
- }
775
- else {
776
- // DAG spec validation rejects unknown schemaIds; this is a
777
- // defensive fallback for a registry that has not been populated.
778
- console.warn(`[run-dag] warning: no structured contract validator registered for schemaId ${task.structuredContractOutput.schemaId}; skipping node self-check`);
779
- }
780
- }
781
692
  const attemptFinishedAt = new Date().toISOString();
782
693
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
783
694
  totalAttemptWallDurationMs += attemptWallDurationMs;
@@ -269,61 +269,3 @@ export function planDagRecovery(input) {
269
269
  }
270
270
  return planByNormalizedCategory(input);
271
271
  }
272
- /**
273
- * Phase 6: route a frontend recovery outcome to a recovery CTA (read-only; never
274
- * mutates a run's status). The six outcomes map to the existing recovery actions
275
- * so the Console recovery page reuses the current CTA surface.
276
- */
277
- export function recommendFrontendRecoveryOutcome(outcome) {
278
- switch (outcome) {
279
- case "none":
280
- return {
281
- action: "none",
282
- summary: "未发生前端恢复;按普通终态展示。",
283
- reason: "该 frontend-implementation 根没有触发自动恢复。",
284
- humanRequired: false,
285
- autoRetryEligible: false,
286
- };
287
- case "recovered":
288
- return {
289
- action: "none",
290
- summary: "自动恢复成功;child 已重新实现并通过验证。",
291
- reason: "parent 的 transient 失败已由 child 恢复,全链按 root 计一次成功。",
292
- humanRequired: false,
293
- autoRetryEligible: false,
294
- };
295
- case "candidate-contract-invalid":
296
- return {
297
- action: "manual-review",
298
- summary: "方案输出无效:candidate JSON 无法解析/推导,重试用尽。",
299
- reason: "建议人工检查需求或重新生成方案后再跑。",
300
- humanRequired: true,
301
- autoRetryEligible: false,
302
- };
303
- case "prewrite-blocked":
304
- return {
305
- action: "manual-review",
306
- summary: "治理门禁阻断:source stale / writeSet 越界 / Mock policy / contract 违规。",
307
- reason: "需人工修正任务边界/事实后重跑,不自动重试。",
308
- humanRequired: true,
309
- autoRetryEligible: false,
310
- };
311
- case "repair-exhausted":
312
- return {
313
- action: "rerun-after-fix",
314
- summary: "自动修复未通过:verify 失败,1 次 repair 后 reverify 仍失败。",
315
- reason: "建议 `dag rerun` 或人工排查。",
316
- humanRequired: true,
317
- autoRetryEligible: false,
318
- commandHint: "loop-agent dag rerun --run-id <runId>",
319
- };
320
- case "auto-recovery-blocked":
321
- return {
322
- action: "manual-review",
323
- summary: "自动恢复被阻止:rollback 无法证明完整或存在外部/越界修改。",
324
- reason: "必须人工介入,禁止自动覆盖用户文件。",
325
- humanRequired: true,
326
- autoRetryEligible: false,
327
- };
328
- }
329
- }
@@ -1,5 +1,4 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
- import { randomUUID } from "node:crypto";
3
2
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
4
3
  import { hostname } from "node:os";
5
4
  import path from "node:path";
@@ -7,8 +6,7 @@ import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../appl
7
6
  import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
8
7
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
9
8
  import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
10
- import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
11
- import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
9
+ import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
12
10
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
13
11
  import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
14
12
  import { createDagNodeExecutor } from "./executor-registry.js";
@@ -23,9 +21,8 @@ import { createSkillSnapshot, prepareSkillSnapshotForContinuation, writeSkillSna
23
21
  import { captureWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
24
22
  import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
25
23
  import { runConvergencePassController, } from "./convergence/controller.js";
26
- import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
24
+ import { executeDagRanksOnce, isConditionSkippedReason } from "./scheduler.js";
27
25
  import { topoSortToRanks } from "./topo.js";
28
- import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
29
26
  import { parseDagSpec, resolveModelForTask, } from "./types.js";
30
27
  import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
31
28
  import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
@@ -115,22 +112,6 @@ export async function buildDagRunId(spec, options) {
115
112
  const existing = await listExistingDagRunIds(options.cwd);
116
113
  return collisionSafeDagRunId(baseId, existing, now);
117
114
  }
118
- /**
119
- * Append a human-readable round label to a run title when the runId carries
120
- * a `-rN` collision suffix (2026-08-16): the sidebar / Inspect list showed
121
- * identical titles for reruns and only the id hinted at the round.
122
- */
123
- export function titleWithRoundLabel(title, runId) {
124
- const match = /-r(\d{1,2})$/.exec(runId);
125
- if (!match)
126
- return title;
127
- const round = Number.parseInt(match[1], 10);
128
- if (!Number.isFinite(round) || round < 2)
129
- return title;
130
- // Never stack labels on reruns of reruns.
131
- const base = title.replace(/\s*(第\s*\d+\s*轮)$/u, "");
132
- return `${base}(第 ${round} 轮)`;
133
- }
134
115
  export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "dag") {
135
116
  const nodes = {};
136
117
  for (const task of spec.tasks) {
@@ -152,7 +133,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
152
133
  : "execute";
153
134
  const state = {
154
135
  version: 1,
155
- title: titleWithRoundLabel(spec.title, runId),
136
+ title: spec.title,
156
137
  runId,
157
138
  cwd: opts.cwd,
158
139
  startedAt: new Date().toISOString(),
@@ -279,7 +260,7 @@ export async function runDag(spec, opts) {
279
260
  }
280
261
  if (opts.dryRun || opts.initOnly) {
281
262
  return {
282
- title: state.title,
263
+ title: spec.title,
283
264
  runId: state.runId,
284
265
  status: state.status,
285
266
  ranks,
@@ -467,51 +448,6 @@ async function executeDagCheckpoint(input) {
467
448
  void persistState().catch(() => { });
468
449
  }, runnerLivenessPolicy.heartbeatIntervalMs);
469
450
  heartbeatTimer.unref();
470
- await persistState();
471
- const interruptController = new AbortController();
472
- const expectedRunnerIdentity = runnerIdentityFromState(state);
473
- const interruptWatcher = expectedRunnerIdentity
474
- ? startInterruptWatcher({
475
- runDir,
476
- runId: state.runId,
477
- expectedRunnerIdentity,
478
- controller: interruptController,
479
- })
480
- : undefined;
481
- const abortSignal = mergeAbortSignals(input.abortSignal, interruptController.signal);
482
- // Graceful terminal persistence: an outer SIGTERM/SIGINT (operator hard
483
- // timeout, supervision layer, or shell wall-clock) must not leave the run
484
- // orphaned in RUNNING with a dead heartbeat. Persist a terminal failed
485
- // state with an explicit terminalReason before exiting so downstream
486
- // liveness/doctor tooling sees a diagnosable terminal instead of an orphan.
487
- let terminalSignal;
488
- const shutdownOnSignal = (signal) => {
489
- if (terminalSignal)
490
- return;
491
- terminalSignal = signal;
492
- clearInterval(heartbeatTimer);
493
- if (state.runner)
494
- state.runner.heartbeatAt = new Date().toISOString();
495
- state.status = "failed";
496
- state.finishedAt = new Date().toISOString();
497
- state.terminalReason = `runner terminated by ${signal} before run completion`;
498
- console.warn(`[run-dag] received ${signal}; persisting terminal state and exiting`);
499
- const exit = () => process.exit(signal === "SIGINT" ? 130 : 143);
500
- // Best-effort atomic persist with a hard exit deadline so a stuck write
501
- // queue cannot keep the process alive past the supervisor's kill window.
502
- const hardExit = setTimeout(exit, 5000);
503
- hardExit.unref();
504
- void persistState()
505
- .catch(() => { })
506
- .finally(() => {
507
- clearTimeout(hardExit);
508
- exit();
509
- });
510
- };
511
- const onSigTerm = () => shutdownOnSignal("SIGTERM");
512
- const onSigInt = () => shutdownOnSignal("SIGINT");
513
- process.once("SIGTERM", onSigTerm);
514
- process.once("SIGINT", onSigInt);
515
451
  try {
516
452
  const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
517
453
  const baseExecuteNode = input.executeNode ??
@@ -568,7 +504,6 @@ async function executeDagCheckpoint(input) {
568
504
  tasksById,
569
505
  maxConcurrent,
570
506
  persistState,
571
- runDir,
572
507
  abortSignal: input.abortSignal,
573
508
  createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
574
509
  baseExecuteNode,
@@ -578,7 +513,7 @@ async function executeDagCheckpoint(input) {
578
513
  meta: { runDir, runId: state.runId, spec },
579
514
  }),
580
515
  executeScheduledNode: async (nodeId, executeNode, onPause) => {
581
- if (abortSignal?.aborted)
516
+ if (input.abortSignal?.aborted)
582
517
  return;
583
518
  if (isHardBudgetBreached(state.budgetLedger))
584
519
  return;
@@ -645,7 +580,6 @@ async function executeDagCheckpoint(input) {
645
580
  // still producing a truthful failure handoff for max-passes/non-retry.
646
581
  pausedByNodeId = await executeRanks(terminalCloseoutRanks);
647
582
  }
648
- interruptWatcher?.stop();
649
583
  // Stop heartbeats before terminal archive. A late heartbeat writing
650
584
  // state.json under completed/ trips the completed-facts write guard and
651
585
  // makes run-dag exit non-zero after every node already finished.
@@ -656,71 +590,22 @@ async function executeDagCheckpoint(input) {
656
590
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
657
591
  if (pausedByNodeId) {
658
592
  state.status = "paused";
659
- await finalizeRunOwnedInterrupt({
660
- runDir,
661
- state,
662
- paused: true,
663
- interruptAborted: interruptController.signal.aborted,
664
- });
665
593
  await persistState();
666
594
  await notifyRunObserver(input.observer, "onRunFinish", state);
667
595
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
668
596
  }
669
597
  else {
670
- const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
671
- await finalizeRunOwnedInterrupt({
672
- runDir,
673
- state,
674
- paused: false,
675
- interruptAborted: interruptController.signal.aborted,
676
- });
598
+ finalizeTerminalRunStatus(state, spec.tasks.length);
677
599
  await persistState();
678
- if (recoveryPending) {
679
- // Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
680
- // child and run it synchronously. Dynamic import avoids a static
681
- // runner ↔ frontend-recovery-run module cycle. stageRecoveryChild
682
- // locates the parent via the canonical active/ layout, so recovery
683
- // only fires when this run is still the canonical active run dir
684
- // (direct runDagContinuation callers using an arbitrary runDir keep
685
- // the previous scheduler-layer skip semantics).
686
- const canonicalActiveDir = getDagRunDir(cwd, "active", state.runId);
687
- if (path.resolve(runDir) === path.resolve(canonicalActiveDir)) {
688
- const { stageRecoveryChild } = await import("./frontend-recovery-run.js");
689
- const staged = await stageRecoveryChild({
690
- cwd,
691
- parentRunId: state.runId,
692
- });
693
- // stageRecoveryChild CAS-advanced the parent's on-disk lineage
694
- // (child-staging → child-running + childRunId). Refresh the in-memory
695
- // copy so the terminal persistState below does not regress the parent
696
- // back to stale child-staging.
697
- const parentOnDisk = await readDagRunState(runDir);
698
- state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
699
- const childState = await readDagRunState(staged.childRunDir);
700
- const childSpec = await readDagRunSpec(staged.childRunDir);
701
- await runDagContinuation({
702
- cwd,
703
- spec: childSpec,
704
- state: childState,
705
- runDir: staged.childRunDir,
706
- maxConcurrent,
707
- executeNode: input.executeNode,
708
- observer: input.observer,
709
- abortSignal,
710
- });
711
- }
712
- }
713
600
  await notifyRunObserver(input.observer, "onRunFinish", state);
714
- if (!recoveryPending) {
715
- try {
716
- const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
717
- await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
718
- }
719
- catch (error) {
720
- console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
721
- }
722
- runDir = await moveToCompletedRunDir(runDir, completedRunDir);
601
+ try {
602
+ const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
603
+ await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
604
+ }
605
+ catch (error) {
606
+ console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
723
607
  }
608
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
724
609
  }
725
610
  await relocateRunArtifactPaths({
726
611
  runDir,
@@ -733,7 +618,7 @@ async function executeDagCheckpoint(input) {
733
618
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
734
619
  await persistState({ allowCompletedFactsWrite: true });
735
620
  return {
736
- title: state.title,
621
+ title: spec.title,
737
622
  runId: state.runId,
738
623
  status: state.status,
739
624
  ranks,
@@ -743,51 +628,8 @@ async function executeDagCheckpoint(input) {
743
628
  }
744
629
  finally {
745
630
  clearInterval(heartbeatTimer);
746
- interruptWatcher?.stop();
747
- process.removeListener("SIGTERM", onSigTerm);
748
- process.removeListener("SIGINT", onSigInt);
749
631
  }
750
632
  }
751
- async function finalizeRunOwnedInterrupt(input) {
752
- const request = await readInterruptRequest(input.runDir);
753
- if (!request)
754
- return;
755
- if (request.status === "settled" || request.status === "needs-reconcile") {
756
- return;
757
- }
758
- const hasUnconfirmed = Object.values(input.state.nodes).some((node) => node.failureCategory === "termination-unconfirmed");
759
- if (hasUnconfirmed || input.paused) {
760
- await markInterruptNeedsReconcile({
761
- runDir: input.runDir,
762
- outcome: hasUnconfirmed
763
- ? "termination-unconfirmed"
764
- : "paused-before-settle",
765
- });
766
- return;
767
- }
768
- if (input.interruptAborted &&
769
- request.status === "acknowledged" &&
770
- (input.state.status === "failed" ||
771
- input.state.status === "partial_failed" ||
772
- input.state.failureCategory === "controller-interrupted")) {
773
- await settleDagInterrupt({
774
- runDir: input.runDir,
775
- outcome: "controller-interrupted",
776
- });
777
- return;
778
- }
779
- if (request.status === "requested") {
780
- await markInterruptNeedsReconcile({
781
- runDir: input.runDir,
782
- outcome: "runner-did-not-acknowledge",
783
- });
784
- return;
785
- }
786
- await markInterruptNeedsReconcile({
787
- runDir: input.runDir,
788
- outcome: "run-exited-before-interrupt-settled",
789
- });
790
- }
791
633
  async function notifyRunObserver(observer, event, state) {
792
634
  try {
793
635
  await observer?.[event]?.(state);
@@ -853,154 +695,7 @@ function isSuccessfulConvergenceTerminal(state) {
853
695
  return true;
854
696
  return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
855
697
  }
856
- function buildFrontendRecoveryIntent(state, failureSource) {
857
- const existing = state.frontendRecoveryState;
858
- const requestId = existing?.requestId ?? randomUUID();
859
- return {
860
- schemaVersion: 1,
861
- phase: "child-staging",
862
- requestId,
863
- recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
864
- parentRunId: state.runId,
865
- ...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
866
- attemptId: existing?.attemptId ?? `recovery-${requestId}`,
867
- attemptIndex: existing?.attemptIndex ?? 0,
868
- continuationCount: existing?.continuationCount ?? 0,
869
- ...(failureSource ? { failureSource } : {}),
870
- revision: (existing?.revision ?? 0) + 1,
871
- };
872
- }
873
- function buildCandidateContractInvalidResult(state, result, artifactHash) {
874
- const recovery = state.frontendRecoveryState;
875
- return {
876
- schemaVersion: 1,
877
- outcome: "candidate-contract-invalid",
878
- origin: {
879
- kind: "frontend-prewrite-gate",
880
- parentRunId: recovery?.parentRunId ?? state.runId,
881
- ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
882
- requestId: recovery?.requestId ?? "",
883
- failedNodeId: result.selectedPlanNodeId,
884
- },
885
- failureClass: {
886
- code: "candidate-contract-invalid",
887
- classification: result.classification,
888
- reason: result.failureReason ?? "candidate contract invalid",
889
- },
890
- evidenceRefs: [
891
- {
892
- runId: state.runId,
893
- relativePath: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
894
- sha256: artifactHash,
895
- },
896
- ],
897
- };
898
- }
899
- function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
900
- const recovery = state.frontendRecoveryState;
901
- return {
902
- schemaVersion: 1,
903
- outcome: "auto-recovery-blocked",
904
- origin: {
905
- kind: "frontend-writer",
906
- parentRunId: recovery?.parentRunId ?? state.runId,
907
- ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
908
- requestId: recovery?.requestId ?? "",
909
- failedNodeId,
910
- },
911
- failureClass: {
912
- code: "writer-transient-partial-write",
913
- classification: "transient",
914
- reason,
915
- },
916
- evidenceRefs: [],
917
- };
918
- }
919
- function settleFrontendRecovery(state, outcome) {
920
- const recovery = state.frontendRecoveryState;
921
- if (!recovery)
922
- return;
923
- recovery.phase = "settled";
924
- if (!state.frontendRecoveryResult) {
925
- state.frontendRecoveryResult = {
926
- schemaVersion: 1,
927
- outcome,
928
- origin: {
929
- kind: "frontend-prewrite-gate",
930
- parentRunId: recovery.parentRunId,
931
- ...(recovery.childRunId ? { childRunId: recovery.childRunId } : {}),
932
- requestId: recovery.requestId,
933
- failedNodeId: "",
934
- },
935
- evidenceRefs: [],
936
- };
937
- }
938
- }
939
- export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
940
- const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
941
- // Terminal override: a denied frontend writer must never surface as a
942
- // mechanical partial_failed just because prewrite FINISHED while the writer
943
- // was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
944
- let recoveryPending = false;
945
- const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
946
- const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
947
- if (hasFrontendWriter && !skipFrontendRecovery) {
948
- const admission = await readFrontendPrewriteResult(runDir);
949
- if (admission.ok) {
950
- if (admission.result.classification === "blocked") {
951
- state.status = "failed";
952
- return { recoveryPending: false };
953
- }
954
- if (admission.result.classification === "retryable-invalid") {
955
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
956
- if (continuationCount >= 1) {
957
- // Continuation quota exhausted: a second retryable-invalid candidate
958
- // is authoritative candidate-contract-invalid → forced failed.
959
- state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
960
- state.status = "failed";
961
- return { recoveryPending: false };
962
- }
963
- // Root continuation still available: record the recovery intent and let
964
- // mechanical aggregation settle partial_failed/failed. The parent keeps
965
- // phase != settled (recovery in progress) and no final root manifest.
966
- state.frontendRecoveryState = buildFrontendRecoveryIntent(state);
967
- recoveryPending = true;
968
- }
969
- }
970
- }
971
- // Phase 5: writer transient partial write → rollback + recovery intent. Only
972
- // for frontend-implementation writers with a remaining root continuation, and
973
- // only when the rollback journal (captured before the provider call) restores
974
- // cleanly; otherwise the run stays failed with auto-recovery-blocked.
975
- if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
976
- const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
977
- const node = state.nodes[id];
978
- return (node &&
979
- node.status === "ERROR" &&
980
- isFrontendWriterTransientPartialWrite({
981
- failureCategory: node.failureCategory,
982
- }));
983
- });
984
- if (writerNodeId) {
985
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
986
- if (continuationCount < 1) {
987
- const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
988
- const rollback = await rollbackFrontendWriter({
989
- cwd: repoRoot,
990
- parentRunId: state.runId,
991
- });
992
- if (rollback.ok) {
993
- state.frontendRecoveryState = buildFrontendRecoveryIntent(state, "frontend-implement-pi");
994
- recoveryPending = true;
995
- }
996
- else {
997
- state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
998
- state.status = "failed";
999
- return { recoveryPending: false };
1000
- }
1001
- }
1002
- }
1003
- }
698
+ function finalizeTerminalRunStatus(state, taskCount) {
1004
699
  const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
1005
700
  const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
1006
701
  if (state.convergence && supersededFailures.length > 0) {
@@ -1024,19 +719,6 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1024
719
  else {
1025
720
  state.status = "failed";
1026
721
  }
1027
- // Terminal recovery bookkeeping for a child attempt: a child that finished
1028
- // recovered settles its own lineage; any other child terminal settles as
1029
- // auto-recovery-blocked (phase 3 performs no further recovery).
1030
- const recovery = state.frontendRecoveryState;
1031
- if (recovery && recovery.attemptIndex >= 1) {
1032
- if (state.status === "finished") {
1033
- settleFrontendRecovery(state, "recovered");
1034
- }
1035
- else if (!state.frontendRecoveryResult) {
1036
- settleFrontendRecovery(state, "auto-recovery-blocked");
1037
- }
1038
- }
1039
- return { recoveryPending };
1040
722
  }
1041
723
  function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
1042
724
  if (rankWriterNodeIds.length <= 1)