@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.
- package/AGENTS.md +2 -6
- package/CHANGELOG.md +72 -163
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/infrastructure/harness/atomic-write.js +1 -2
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/frontend-project-capability.js +12 -162
- package/dist/task/source-prepare/completeness.js +0 -17
- package/dist/task/source-prepare/parse-intent.js +1 -15
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -61
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -86
- package/dist/worker/console/chat/routes.js +8 -36
- package/dist/worker/console/chat/session-store.js +0 -3
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -97
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +13 -373
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +86 -238
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/node-execution.js +6 -105
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/report.js +0 -6
- package/dist/workflows/dag/retry-policy.js +0 -11
- package/dist/workflows/dag/runner.js +12 -314
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -132
- package/dist/workflows/dag/validate.js +2 -7
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/frontend-design-contract.md +14 -34
- package/docs/templates/frontend-task-constraints.md +13 -33
- package/docs/templates/frontend-task-requirement.md +20 -54
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -4
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -3
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -471
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- 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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
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)
|