@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.
- package/AGENTS.md +2 -6
- package/CHANGELOG.md +71 -144
- 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/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/contract/project.js +0 -4
- package/dist/task/source-prepare/completeness.js +0 -53
- package/dist/task/source-prepare/parse-intent.js +12 -82
- package/dist/task/source-prepare/prepare.js +5 -102
- 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 -152
- 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 -111
- package/dist/worker/console/chat/routes.js +10 -43
- package/dist/worker/console/chat/session-store.js +1 -9
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -204
- 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 +14 -391
- 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 -114
- 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 +13 -67
- 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 +3 -10
- 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/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- 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 +82 -165
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +15 -333
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -130
- 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/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -9
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -4
- 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 -477
- 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/abnfDiagram-N423BO3Z-DSvF6RXQ.js +0 -1
- package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +0 -1
- package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +0 -36
- package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +0 -132
- package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +0 -10
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +0 -1
- package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +0 -1
- package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +0 -231
- package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +0 -15
- package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +0 -88
- package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +0 -206
- package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +0 -1
- package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +0 -1
- package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +0 -1
- package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +0 -1
- package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +0 -166
- package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +0 -62
- package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +0 -321
- package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +0 -4
- package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +0 -30
- package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +0 -3
- package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +0 -24
- package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +0 -24
- package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +0 -41
- package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +0 -1
- package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +0 -85
- package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +0 -156
- package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +0 -292
- package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +0 -106
- package/dist/worker/console/static/assets/graph-DOmOIIwC.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +0 -2
- package/dist/worker/console/static/assets/init-Gi6I4Gst.js +0 -1
- package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +0 -70
- package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +0 -139
- package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +0 -89
- package/dist/worker/console/static/assets/layout-D-LzfAck.js +0 -1
- package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +0 -1
- package/dist/worker/console/static/assets/map-DxJ2ADlA.js +0 -1
- package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +0 -308
- package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +0 -96
- package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +0 -1
- package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +0 -39
- package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +0 -7
- package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +0 -1
- package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +0 -84
- package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +0 -40
- package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +0 -162
- package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +0 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
- package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +0 -120
- package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +0 -34
- package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +0 -78
- package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +0 -7
- package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
- 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
|
@@ -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,
|
|
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";
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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:
|
|
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
|
|
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)
|