@tea-agent/loop-agent 0.36.4-beta.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 (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  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/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  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 -61
  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 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  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 +13 -373
  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 -89
  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 +11 -30
  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 +1 -6
  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/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -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";
@@ -451,51 +448,6 @@ async function executeDagCheckpoint(input) {
451
448
  void persistState().catch(() => { });
452
449
  }, runnerLivenessPolicy.heartbeatIntervalMs);
453
450
  heartbeatTimer.unref();
454
- await persistState();
455
- const interruptController = new AbortController();
456
- const expectedRunnerIdentity = runnerIdentityFromState(state);
457
- const interruptWatcher = expectedRunnerIdentity
458
- ? startInterruptWatcher({
459
- runDir,
460
- runId: state.runId,
461
- expectedRunnerIdentity,
462
- controller: interruptController,
463
- })
464
- : undefined;
465
- const abortSignal = mergeAbortSignals(input.abortSignal, interruptController.signal);
466
- // Graceful terminal persistence: an outer SIGTERM/SIGINT (operator hard
467
- // timeout, supervision layer, or shell wall-clock) must not leave the run
468
- // orphaned in RUNNING with a dead heartbeat. Persist a terminal failed
469
- // state with an explicit terminalReason before exiting so downstream
470
- // liveness/doctor tooling sees a diagnosable terminal instead of an orphan.
471
- let terminalSignal;
472
- const shutdownOnSignal = (signal) => {
473
- if (terminalSignal)
474
- return;
475
- terminalSignal = signal;
476
- clearInterval(heartbeatTimer);
477
- if (state.runner)
478
- state.runner.heartbeatAt = new Date().toISOString();
479
- state.status = "failed";
480
- state.finishedAt = new Date().toISOString();
481
- state.terminalReason = `runner terminated by ${signal} before run completion`;
482
- console.warn(`[run-dag] received ${signal}; persisting terminal state and exiting`);
483
- const exit = () => process.exit(signal === "SIGINT" ? 130 : 143);
484
- // Best-effort atomic persist with a hard exit deadline so a stuck write
485
- // queue cannot keep the process alive past the supervisor's kill window.
486
- const hardExit = setTimeout(exit, 5000);
487
- hardExit.unref();
488
- void persistState()
489
- .catch(() => { })
490
- .finally(() => {
491
- clearTimeout(hardExit);
492
- exit();
493
- });
494
- };
495
- const onSigTerm = () => shutdownOnSignal("SIGTERM");
496
- const onSigInt = () => shutdownOnSignal("SIGINT");
497
- process.once("SIGTERM", onSigTerm);
498
- process.once("SIGINT", onSigInt);
499
451
  try {
500
452
  const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
501
453
  const baseExecuteNode = input.executeNode ??
@@ -552,7 +504,6 @@ async function executeDagCheckpoint(input) {
552
504
  tasksById,
553
505
  maxConcurrent,
554
506
  persistState,
555
- runDir,
556
507
  abortSignal: input.abortSignal,
557
508
  createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
558
509
  baseExecuteNode,
@@ -562,7 +513,7 @@ async function executeDagCheckpoint(input) {
562
513
  meta: { runDir, runId: state.runId, spec },
563
514
  }),
564
515
  executeScheduledNode: async (nodeId, executeNode, onPause) => {
565
- if (abortSignal?.aborted)
516
+ if (input.abortSignal?.aborted)
566
517
  return;
567
518
  if (isHardBudgetBreached(state.budgetLedger))
568
519
  return;
@@ -629,7 +580,6 @@ async function executeDagCheckpoint(input) {
629
580
  // still producing a truthful failure handoff for max-passes/non-retry.
630
581
  pausedByNodeId = await executeRanks(terminalCloseoutRanks);
631
582
  }
632
- interruptWatcher?.stop();
633
583
  // Stop heartbeats before terminal archive. A late heartbeat writing
634
584
  // state.json under completed/ trips the completed-facts write guard and
635
585
  // makes run-dag exit non-zero after every node already finished.
@@ -640,71 +590,22 @@ async function executeDagCheckpoint(input) {
640
590
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
641
591
  if (pausedByNodeId) {
642
592
  state.status = "paused";
643
- await finalizeRunOwnedInterrupt({
644
- runDir,
645
- state,
646
- paused: true,
647
- interruptAborted: interruptController.signal.aborted,
648
- });
649
593
  await persistState();
650
594
  await notifyRunObserver(input.observer, "onRunFinish", state);
651
595
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
652
596
  }
653
597
  else {
654
- const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
655
- await finalizeRunOwnedInterrupt({
656
- runDir,
657
- state,
658
- paused: false,
659
- interruptAborted: interruptController.signal.aborted,
660
- });
598
+ finalizeTerminalRunStatus(state, spec.tasks.length);
661
599
  await persistState();
662
- if (recoveryPending) {
663
- // Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
664
- // child and run it synchronously. Dynamic import avoids a static
665
- // runner ↔ frontend-recovery-run module cycle. stageRecoveryChild
666
- // locates the parent via the canonical active/ layout, so recovery
667
- // only fires when this run is still the canonical active run dir
668
- // (direct runDagContinuation callers using an arbitrary runDir keep
669
- // the previous scheduler-layer skip semantics).
670
- const canonicalActiveDir = getDagRunDir(cwd, "active", state.runId);
671
- if (path.resolve(runDir) === path.resolve(canonicalActiveDir)) {
672
- const { stageRecoveryChild } = await import("./frontend-recovery-run.js");
673
- const staged = await stageRecoveryChild({
674
- cwd,
675
- parentRunId: state.runId,
676
- });
677
- // stageRecoveryChild CAS-advanced the parent's on-disk lineage
678
- // (child-staging → child-running + childRunId). Refresh the in-memory
679
- // copy so the terminal persistState below does not regress the parent
680
- // back to stale child-staging.
681
- const parentOnDisk = await readDagRunState(runDir);
682
- state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
683
- const childState = await readDagRunState(staged.childRunDir);
684
- const childSpec = await readDagRunSpec(staged.childRunDir);
685
- await runDagContinuation({
686
- cwd,
687
- spec: childSpec,
688
- state: childState,
689
- runDir: staged.childRunDir,
690
- maxConcurrent,
691
- executeNode: input.executeNode,
692
- observer: input.observer,
693
- abortSignal,
694
- });
695
- }
696
- }
697
600
  await notifyRunObserver(input.observer, "onRunFinish", state);
698
- if (!recoveryPending) {
699
- try {
700
- const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
701
- await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
702
- }
703
- catch (error) {
704
- console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
705
- }
706
- runDir = await moveToCompletedRunDir(runDir, completedRunDir);
601
+ try {
602
+ const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
603
+ await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
707
604
  }
605
+ catch (error) {
606
+ console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
607
+ }
608
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
708
609
  }
709
610
  await relocateRunArtifactPaths({
710
611
  runDir,
@@ -727,50 +628,7 @@ async function executeDagCheckpoint(input) {
727
628
  }
728
629
  finally {
729
630
  clearInterval(heartbeatTimer);
730
- interruptWatcher?.stop();
731
- process.removeListener("SIGTERM", onSigTerm);
732
- process.removeListener("SIGINT", onSigInt);
733
- }
734
- }
735
- async function finalizeRunOwnedInterrupt(input) {
736
- const request = await readInterruptRequest(input.runDir);
737
- if (!request)
738
- return;
739
- if (request.status === "settled" || request.status === "needs-reconcile") {
740
- return;
741
- }
742
- const hasUnconfirmed = Object.values(input.state.nodes).some((node) => node.failureCategory === "termination-unconfirmed");
743
- if (hasUnconfirmed || input.paused) {
744
- await markInterruptNeedsReconcile({
745
- runDir: input.runDir,
746
- outcome: hasUnconfirmed
747
- ? "termination-unconfirmed"
748
- : "paused-before-settle",
749
- });
750
- return;
751
- }
752
- if (input.interruptAborted &&
753
- request.status === "acknowledged" &&
754
- (input.state.status === "failed" ||
755
- input.state.status === "partial_failed" ||
756
- input.state.failureCategory === "controller-interrupted")) {
757
- await settleDagInterrupt({
758
- runDir: input.runDir,
759
- outcome: "controller-interrupted",
760
- });
761
- return;
762
- }
763
- if (request.status === "requested") {
764
- await markInterruptNeedsReconcile({
765
- runDir: input.runDir,
766
- outcome: "runner-did-not-acknowledge",
767
- });
768
- return;
769
631
  }
770
- await markInterruptNeedsReconcile({
771
- runDir: input.runDir,
772
- outcome: "run-exited-before-interrupt-settled",
773
- });
774
632
  }
775
633
  async function notifyRunObserver(observer, event, state) {
776
634
  try {
@@ -837,154 +695,7 @@ function isSuccessfulConvergenceTerminal(state) {
837
695
  return true;
838
696
  return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
839
697
  }
840
- function buildFrontendRecoveryIntent(state, failureSource) {
841
- const existing = state.frontendRecoveryState;
842
- const requestId = existing?.requestId ?? randomUUID();
843
- return {
844
- schemaVersion: 1,
845
- phase: "child-staging",
846
- requestId,
847
- recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
848
- parentRunId: state.runId,
849
- ...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
850
- attemptId: existing?.attemptId ?? `recovery-${requestId}`,
851
- attemptIndex: existing?.attemptIndex ?? 0,
852
- continuationCount: existing?.continuationCount ?? 0,
853
- ...(failureSource ? { failureSource } : {}),
854
- revision: (existing?.revision ?? 0) + 1,
855
- };
856
- }
857
- function buildCandidateContractInvalidResult(state, result, artifactHash) {
858
- const recovery = state.frontendRecoveryState;
859
- return {
860
- schemaVersion: 1,
861
- outcome: "candidate-contract-invalid",
862
- origin: {
863
- kind: "frontend-prewrite-gate",
864
- parentRunId: recovery?.parentRunId ?? state.runId,
865
- ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
866
- requestId: recovery?.requestId ?? "",
867
- failedNodeId: result.selectedPlanNodeId,
868
- },
869
- failureClass: {
870
- code: "candidate-contract-invalid",
871
- classification: result.classification,
872
- reason: result.failureReason ?? "candidate contract invalid",
873
- },
874
- evidenceRefs: [
875
- {
876
- runId: state.runId,
877
- relativePath: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
878
- sha256: artifactHash,
879
- },
880
- ],
881
- };
882
- }
883
- function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
884
- const recovery = state.frontendRecoveryState;
885
- return {
886
- schemaVersion: 1,
887
- outcome: "auto-recovery-blocked",
888
- origin: {
889
- kind: "frontend-writer",
890
- parentRunId: recovery?.parentRunId ?? state.runId,
891
- ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
892
- requestId: recovery?.requestId ?? "",
893
- failedNodeId,
894
- },
895
- failureClass: {
896
- code: "writer-transient-partial-write",
897
- classification: "transient",
898
- reason,
899
- },
900
- evidenceRefs: [],
901
- };
902
- }
903
- function settleFrontendRecovery(state, outcome) {
904
- const recovery = state.frontendRecoveryState;
905
- if (!recovery)
906
- return;
907
- recovery.phase = "settled";
908
- if (!state.frontendRecoveryResult) {
909
- state.frontendRecoveryResult = {
910
- schemaVersion: 1,
911
- outcome,
912
- origin: {
913
- kind: "frontend-prewrite-gate",
914
- parentRunId: recovery.parentRunId,
915
- ...(recovery.childRunId ? { childRunId: recovery.childRunId } : {}),
916
- requestId: recovery.requestId,
917
- failedNodeId: "",
918
- },
919
- evidenceRefs: [],
920
- };
921
- }
922
- }
923
- export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
924
- const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
925
- // Terminal override: a denied frontend writer must never surface as a
926
- // mechanical partial_failed just because prewrite FINISHED while the writer
927
- // was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
928
- let recoveryPending = false;
929
- const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
930
- const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
931
- if (hasFrontendWriter && !skipFrontendRecovery) {
932
- const admission = await readFrontendPrewriteResult(runDir);
933
- if (admission.ok) {
934
- if (admission.result.classification === "blocked") {
935
- state.status = "failed";
936
- return { recoveryPending: false };
937
- }
938
- if (admission.result.classification === "retryable-invalid") {
939
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
940
- if (continuationCount >= 1) {
941
- // Continuation quota exhausted: a second retryable-invalid candidate
942
- // is authoritative candidate-contract-invalid → forced failed.
943
- state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
944
- state.status = "failed";
945
- return { recoveryPending: false };
946
- }
947
- // Root continuation still available: record the recovery intent and let
948
- // mechanical aggregation settle partial_failed/failed. The parent keeps
949
- // phase != settled (recovery in progress) and no final root manifest.
950
- state.frontendRecoveryState = buildFrontendRecoveryIntent(state);
951
- recoveryPending = true;
952
- }
953
- }
954
- }
955
- // Phase 5: writer transient partial write → rollback + recovery intent. Only
956
- // for frontend-implementation writers with a remaining root continuation, and
957
- // only when the rollback journal (captured before the provider call) restores
958
- // cleanly; otherwise the run stays failed with auto-recovery-blocked.
959
- if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
960
- const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
961
- const node = state.nodes[id];
962
- return (node &&
963
- node.status === "ERROR" &&
964
- isFrontendWriterTransientPartialWrite({
965
- failureCategory: node.failureCategory,
966
- }));
967
- });
968
- if (writerNodeId) {
969
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
970
- if (continuationCount < 1) {
971
- const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
972
- const rollback = await rollbackFrontendWriter({
973
- cwd: repoRoot,
974
- parentRunId: state.runId,
975
- });
976
- if (rollback.ok) {
977
- state.frontendRecoveryState = buildFrontendRecoveryIntent(state, "frontend-implement-pi");
978
- recoveryPending = true;
979
- }
980
- else {
981
- state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
982
- state.status = "failed";
983
- return { recoveryPending: false };
984
- }
985
- }
986
- }
987
- }
698
+ function finalizeTerminalRunStatus(state, taskCount) {
988
699
  const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
989
700
  const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
990
701
  if (state.convergence && supersededFailures.length > 0) {
@@ -1008,19 +719,6 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1008
719
  else {
1009
720
  state.status = "failed";
1010
721
  }
1011
- // Terminal recovery bookkeeping for a child attempt: a child that finished
1012
- // recovered settles its own lineage; any other child terminal settles as
1013
- // auto-recovery-blocked (phase 3 performs no further recovery).
1014
- const recovery = state.frontendRecoveryState;
1015
- if (recovery && recovery.attemptIndex >= 1) {
1016
- if (state.status === "finished") {
1017
- settleFrontendRecovery(state, "recovered");
1018
- }
1019
- else if (!state.frontendRecoveryResult) {
1020
- settleFrontendRecovery(state, "auto-recovery-blocked");
1021
- }
1022
- }
1023
- return { recoveryPending };
1024
722
  }
1025
723
  function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
1026
724
  if (rankWriterNodeIds.length <= 1)