@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3
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 +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1645 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1150 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +42 -16
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +347 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isTerminalOperationState, } from "../../worker/console/operation-store.js";
|
|
2
|
+
import { runOperation, } from "../../worker/console/operation-runner.js";
|
|
3
|
+
import { buildFailureCapsule, classifyFailureOwner, projectProtocolFailureReason, } from "./frontend-recovery-capsule.js";
|
|
4
|
+
import { computeNormalizedFailureFingerprint, evaluateNonConvergence, foldOperationBudgets, } from "./frontend-recovery-lineage.js";
|
|
5
|
+
import { projectedInterruptStatus, } from "./interrupt-request.js";
|
|
6
|
+
import { evaluateNodeLiveness, } from "./liveness-policy.js";
|
|
7
|
+
/**
|
|
8
|
+
* Deterministic legacy Console operation-state → canonical frontend
|
|
9
|
+
* operation-state/terminal projection. This is the single adapter-layer
|
|
10
|
+
* projection at the controller boundary; the generic Console operation schema
|
|
11
|
+
* is not changed (AC-001).
|
|
12
|
+
*/
|
|
13
|
+
export function projectLegacyOperationState(state) {
|
|
14
|
+
switch (state) {
|
|
15
|
+
case "queued":
|
|
16
|
+
case "starting":
|
|
17
|
+
case "running":
|
|
18
|
+
return { operationState: "active" };
|
|
19
|
+
case "succeeded":
|
|
20
|
+
return { operationState: "success", operationTerminal: "success" };
|
|
21
|
+
case "failed":
|
|
22
|
+
case "timed-out":
|
|
23
|
+
case "needs-reconcile":
|
|
24
|
+
return {
|
|
25
|
+
operationState: "failed-internal",
|
|
26
|
+
operationTerminal: "failed-internal",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Conservative default recovery mode by owner. Non-writer owners restart as a
|
|
32
|
+
* new DAG; environment issues wait for the external dependency; unknown never
|
|
33
|
+
* auto-selects writer recovery (AC-003).
|
|
34
|
+
*/
|
|
35
|
+
export function projectDefaultRecoveryMode(owner) {
|
|
36
|
+
switch (owner) {
|
|
37
|
+
case "contract":
|
|
38
|
+
case "scout":
|
|
39
|
+
case "plan":
|
|
40
|
+
case "implementation":
|
|
41
|
+
case "verification-config":
|
|
42
|
+
case "scope":
|
|
43
|
+
return "new-dag";
|
|
44
|
+
case "environment":
|
|
45
|
+
return "wait-external";
|
|
46
|
+
case "unknown":
|
|
47
|
+
return "none";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function buildCapsuleOnTerminal(input) {
|
|
51
|
+
const protocolFailureReason = projectProtocolFailureReason(input.protocolFailureReason);
|
|
52
|
+
if (input.terminalReason === "non-converging") {
|
|
53
|
+
return buildFailureCapsule({
|
|
54
|
+
parentRunId: input.parentRunId,
|
|
55
|
+
operationState: "non-converging",
|
|
56
|
+
recoveryMode: "none",
|
|
57
|
+
protocolFailureReason,
|
|
58
|
+
failureOwner: input.owner,
|
|
59
|
+
evidenceRefs: [],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return buildFailureCapsule({
|
|
63
|
+
parentRunId: input.parentRunId,
|
|
64
|
+
operationState: "recoverable",
|
|
65
|
+
recoveryMode: input.deps.recoveryMode ?? projectDefaultRecoveryMode(input.owner),
|
|
66
|
+
protocolFailureReason,
|
|
67
|
+
failureOwner: input.owner,
|
|
68
|
+
evidenceRefs: [],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function runRecoveryOperationController(input) {
|
|
72
|
+
const { operationId, parentRunId, deps } = input;
|
|
73
|
+
const maxAttempts = Math.max(1, deps.maxAttempts ?? 5);
|
|
74
|
+
const now = deps.now ?? (() => new Date());
|
|
75
|
+
const readInterrupt = deps.readInterrupt ?? (async () => undefined);
|
|
76
|
+
const readLedgers = deps.readLedgers ?? (async () => []);
|
|
77
|
+
const readChangedFiles = deps.readChangedFiles ?? (async () => []);
|
|
78
|
+
const fingerprints = [...(deps.previousFingerprints ?? [])];
|
|
79
|
+
let budgetAggregate;
|
|
80
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
81
|
+
const operation = await deps.store.get(operationId);
|
|
82
|
+
if (!operation) {
|
|
83
|
+
return {
|
|
84
|
+
terminalReason: "operation-not-found",
|
|
85
|
+
attempts: attempt,
|
|
86
|
+
fingerprints,
|
|
87
|
+
detail: `operation not found: ${operationId}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Total budget fold + hard stop (resource protection, fail-closed).
|
|
91
|
+
const ledgers = await readLedgers();
|
|
92
|
+
if (deps.budget) {
|
|
93
|
+
budgetAggregate = foldOperationBudgets({
|
|
94
|
+
ledgers,
|
|
95
|
+
budget: deps.budget,
|
|
96
|
+
now,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (budgetAggregate?.status === "breached") {
|
|
100
|
+
return {
|
|
101
|
+
terminalReason: "budget-exhausted",
|
|
102
|
+
attempts: attempt,
|
|
103
|
+
budgetAggregate,
|
|
104
|
+
fingerprints,
|
|
105
|
+
detail: "operation total budget breached",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const owner = deps.failureOwner ??
|
|
109
|
+
classifyFailureOwner({
|
|
110
|
+
rawFailureCategory: operation.errorCode,
|
|
111
|
+
nodeId: operationId,
|
|
112
|
+
});
|
|
113
|
+
const protocolFailureReason = deps.protocolFailureReason ??
|
|
114
|
+
operation.errorMessage ??
|
|
115
|
+
operation.errorCode ??
|
|
116
|
+
"unknown failure";
|
|
117
|
+
if (isTerminalOperationState(operation.state)) {
|
|
118
|
+
if (operation.state === "succeeded") {
|
|
119
|
+
return {
|
|
120
|
+
terminalReason: "succeeded",
|
|
121
|
+
attempts: attempt,
|
|
122
|
+
budgetAggregate,
|
|
123
|
+
fingerprints,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Failed/timed-out: fingerprint the failure and stop if it repeats.
|
|
127
|
+
fingerprints.push(computeNormalizedFailureFingerprint({
|
|
128
|
+
failureOwner: owner,
|
|
129
|
+
protocolFailureReason,
|
|
130
|
+
invalidatedFactIds: [],
|
|
131
|
+
}));
|
|
132
|
+
const nonConvergence = evaluateNonConvergence({
|
|
133
|
+
fingerprints,
|
|
134
|
+
changedFiles: await readChangedFiles(),
|
|
135
|
+
// Budget breach already short-circuited above; fail-closed here.
|
|
136
|
+
budgetBreached: false,
|
|
137
|
+
});
|
|
138
|
+
if (nonConvergence.terminal) {
|
|
139
|
+
return {
|
|
140
|
+
terminalReason: nonConvergence.terminalReason,
|
|
141
|
+
attempts: attempt,
|
|
142
|
+
budgetAggregate,
|
|
143
|
+
fingerprints,
|
|
144
|
+
capsule: buildCapsuleOnTerminal({
|
|
145
|
+
parentRunId,
|
|
146
|
+
deps,
|
|
147
|
+
owner,
|
|
148
|
+
protocolFailureReason,
|
|
149
|
+
terminalReason: "non-converging",
|
|
150
|
+
}),
|
|
151
|
+
detail: nonConvergence.reason,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
terminalReason: "failed",
|
|
156
|
+
attempts: attempt,
|
|
157
|
+
budgetAggregate,
|
|
158
|
+
fingerprints,
|
|
159
|
+
capsule: buildCapsuleOnTerminal({
|
|
160
|
+
parentRunId,
|
|
161
|
+
deps,
|
|
162
|
+
owner,
|
|
163
|
+
protocolFailureReason,
|
|
164
|
+
terminalReason: "failed",
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Non-terminal: gate on liveness + interrupt before advancing.
|
|
169
|
+
const liveness = evaluateNodeLiveness({ node: deps.node ?? {}, now: now() });
|
|
170
|
+
if (liveness.status === "needs-attention") {
|
|
171
|
+
return {
|
|
172
|
+
terminalReason: "needs-attention",
|
|
173
|
+
attempts: attempt,
|
|
174
|
+
budgetAggregate,
|
|
175
|
+
fingerprints,
|
|
176
|
+
detail: "node liveness requires attention; abort recovery loop",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const interrupt = await readInterrupt();
|
|
180
|
+
const projected = projectedInterruptStatus(interrupt);
|
|
181
|
+
if (projected === "requested" ||
|
|
182
|
+
projected === "acknowledged" ||
|
|
183
|
+
projected === "needs-reconcile") {
|
|
184
|
+
return {
|
|
185
|
+
terminalReason: "interrupted",
|
|
186
|
+
attempts: attempt,
|
|
187
|
+
budgetAggregate,
|
|
188
|
+
fingerprints,
|
|
189
|
+
detail: `interrupt projection: ${projected}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
await runOperation(operationId, deps.operationRunnerDeps);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
terminalReason: "max-attempts",
|
|
196
|
+
attempts: maxAttempts,
|
|
197
|
+
budgetAggregate,
|
|
198
|
+
fingerprints,
|
|
199
|
+
detail: `recovery loop exceeded maxAttempts=${maxAttempts}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assembleCumulativeDiffContext, } from "./frontend-review-context.js";
|
|
3
|
+
import { deterministicSha256 } from "./frontend-implementation-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* M6 candidate lineage + operation budget primitives (pure functions).
|
|
6
|
+
*
|
|
7
|
+
* Self-contained types only: this module never extends `DagRunState`/`DagSpec`
|
|
8
|
+
* and never mutates the legacy operation/budget modules — it only imports them.
|
|
9
|
+
* Candidate lineage distinguishes a committed parent candidate (retained into
|
|
10
|
+
* lineage) from an uncommitted partial write (CAS-rolled back, never retained).
|
|
11
|
+
*/
|
|
12
|
+
const hash64 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
13
|
+
/* ------------------------------------------------------------------ *
|
|
14
|
+
* Commit state (AC-002). *
|
|
15
|
+
* ------------------------------------------------------------------ */
|
|
16
|
+
export const commitStateSchema = z.enum([
|
|
17
|
+
"uncommitted-partial",
|
|
18
|
+
"committed-candidate",
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Deterministic commit-state classifier. A journal that reached `completed`
|
|
22
|
+
* (or a restore result that did) means the parent candidate was committed and
|
|
23
|
+
* must be retained into lineage. Anything else — `blocked`, an uncommitted
|
|
24
|
+
* half-write, or a rollback that could not be proven — is fail-closed to
|
|
25
|
+
* `uncommitted-partial` and is never retained.
|
|
26
|
+
*/
|
|
27
|
+
export function classifyCommitState(input) {
|
|
28
|
+
if (input.journalState === "completed") {
|
|
29
|
+
return "committed-candidate";
|
|
30
|
+
}
|
|
31
|
+
if (input.restoreResult?.state === "completed") {
|
|
32
|
+
return "committed-candidate";
|
|
33
|
+
}
|
|
34
|
+
return "uncommitted-partial";
|
|
35
|
+
}
|
|
36
|
+
/* ------------------------------------------------------------------ *
|
|
37
|
+
* Candidate lineage (AC-002). *
|
|
38
|
+
* ------------------------------------------------------------------ */
|
|
39
|
+
export const candidateLineageV1Schema = z
|
|
40
|
+
.object({
|
|
41
|
+
schemaVersion: z.literal(1),
|
|
42
|
+
taskBaselineHash: hash64,
|
|
43
|
+
parentRunId: z.string().min(1),
|
|
44
|
+
parentCandidateHash: hash64,
|
|
45
|
+
runBaselineHash: hash64,
|
|
46
|
+
runDeltaHash: hash64,
|
|
47
|
+
cumulativeDiffHash: hash64,
|
|
48
|
+
commitState: commitStateSchema,
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
/**
|
|
52
|
+
* taskBaseline → candidate cumulative diff digest (AC-004). Reuses the
|
|
53
|
+
* deterministic `assembleCumulativeDiffContext` and returns only its
|
|
54
|
+
* `cumulativeDigest`, so the lineage hash is stable regardless of input order.
|
|
55
|
+
*/
|
|
56
|
+
export function computeCumulativeDiffHash(input) {
|
|
57
|
+
return assembleCumulativeDiffContext(input).cumulativeDigest;
|
|
58
|
+
}
|
|
59
|
+
function pushAggregateBreach(aggregate, dimension, consumed, limit, now) {
|
|
60
|
+
if (limit === undefined || consumed <= limit)
|
|
61
|
+
return;
|
|
62
|
+
const duplicate = aggregate.breaches.some((existing) => existing.dimension === dimension && existing.limit === limit);
|
|
63
|
+
if (!duplicate) {
|
|
64
|
+
aggregate.breaches.push({
|
|
65
|
+
dimension,
|
|
66
|
+
limit,
|
|
67
|
+
consumed,
|
|
68
|
+
at: now().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (aggregate.mode === "hard") {
|
|
72
|
+
aggregate.status = "breached";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fold parent + all continuation/recovery run ledgers into a single operation
|
|
77
|
+
* budget aggregate and evaluate it against the operation total `CampaignBudget`
|
|
78
|
+
* limits. Missing token samples never count as zero (aligned with the ledger
|
|
79
|
+
* contract): if any ledger lacks a token total, the aggregate token total is
|
|
80
|
+
* `null` and no token breach is derived.
|
|
81
|
+
*/
|
|
82
|
+
export function foldOperationBudgets(input) {
|
|
83
|
+
const now = input.now ?? (() => new Date());
|
|
84
|
+
const aggregate = {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
mode: input.budget.mode,
|
|
87
|
+
limits: { ...input.budget.limits },
|
|
88
|
+
consumed: {
|
|
89
|
+
executorCalls: 0,
|
|
90
|
+
wallTimeMs: 0,
|
|
91
|
+
tokens: null,
|
|
92
|
+
repairPasses: 0,
|
|
93
|
+
peakContextChars: 0,
|
|
94
|
+
missingTokenNodeIds: [],
|
|
95
|
+
},
|
|
96
|
+
breaches: [],
|
|
97
|
+
status: input.budget.mode === "record-only" ? "record-only" : "ok",
|
|
98
|
+
};
|
|
99
|
+
let anyUnknownTokens = false;
|
|
100
|
+
let knownTokenSum = 0;
|
|
101
|
+
const missing = new Set();
|
|
102
|
+
for (const ledger of input.ledgers) {
|
|
103
|
+
aggregate.consumed.executorCalls += ledger.consumed.executorCalls;
|
|
104
|
+
aggregate.consumed.wallTimeMs += ledger.consumed.wallTimeMs;
|
|
105
|
+
aggregate.consumed.repairPasses += ledger.consumed.repairPasses;
|
|
106
|
+
aggregate.consumed.peakContextChars = Math.max(aggregate.consumed.peakContextChars, ledger.consumed.peakContextChars);
|
|
107
|
+
if (ledger.consumed.tokens === null) {
|
|
108
|
+
anyUnknownTokens = true;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
knownTokenSum += ledger.consumed.tokens;
|
|
112
|
+
}
|
|
113
|
+
for (const nodeId of ledger.consumed.missingTokenNodeIds) {
|
|
114
|
+
missing.add(nodeId);
|
|
115
|
+
}
|
|
116
|
+
aggregate.breaches.push(...ledger.breaches);
|
|
117
|
+
}
|
|
118
|
+
aggregate.consumed.tokens = anyUnknownTokens ? null : knownTokenSum;
|
|
119
|
+
aggregate.consumed.missingTokenNodeIds = [...missing].sort((a, b) => a.localeCompare(b));
|
|
120
|
+
if (aggregate.mode === "hard") {
|
|
121
|
+
const limits = aggregate.limits;
|
|
122
|
+
pushAggregateBreach(aggregate, "executorCalls", aggregate.consumed.executorCalls, limits.maxExecutorCalls, now);
|
|
123
|
+
pushAggregateBreach(aggregate, "wallTimeMs", aggregate.consumed.wallTimeMs, limits.maxWallTimeMs, now);
|
|
124
|
+
pushAggregateBreach(aggregate, "repairPasses", aggregate.consumed.repairPasses, limits.maxRepairPasses, now);
|
|
125
|
+
pushAggregateBreach(aggregate, "contextChars", aggregate.consumed.peakContextChars, limits.maxContextChars, now);
|
|
126
|
+
if (aggregate.consumed.tokens !== null) {
|
|
127
|
+
pushAggregateBreach(aggregate, "tokens", aggregate.consumed.tokens, limits.maxTokens, now);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return aggregate;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stable, pure failure fingerprint. No wall clock, no random bytes: identical
|
|
134
|
+
* inputs always produce the same digest, so repeated failures across recovery
|
|
135
|
+
* runs can be compared for non-convergence detection.
|
|
136
|
+
*/
|
|
137
|
+
export function computeNormalizedFailureFingerprint(input) {
|
|
138
|
+
const payload = {
|
|
139
|
+
failureOwner: input.failureOwner ?? "unknown",
|
|
140
|
+
protocolFailureReason: input.protocolFailureReason ?? "",
|
|
141
|
+
restartPhase: input.restartPhase ?? "unknown",
|
|
142
|
+
invalidatedFactIds: [...(input.invalidatedFactIds ?? [])].sort(),
|
|
143
|
+
commitState: input.commitState ?? "uncommitted-partial",
|
|
144
|
+
};
|
|
145
|
+
return deterministicSha256(payload);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Bounded-loop stop decision. A repeated failure fingerprint with an empty
|
|
149
|
+
* change manifest proves no workspace progress → `non-converging`. An exceeded
|
|
150
|
+
* operation total budget → `budget-exhausted`. Otherwise continue (but never
|
|
151
|
+
* unbounded: the controller still caps attempts).
|
|
152
|
+
*/
|
|
153
|
+
export function evaluateNonConvergence(input) {
|
|
154
|
+
if (input.budgetBreached) {
|
|
155
|
+
return {
|
|
156
|
+
terminal: true,
|
|
157
|
+
terminalReason: "budget-exhausted",
|
|
158
|
+
reason: "operation total budget breached; stop before exhausting resources",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const counts = new Map();
|
|
162
|
+
for (const fingerprint of input.fingerprints) {
|
|
163
|
+
counts.set(fingerprint, (counts.get(fingerprint) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
const repeated = [...counts.values()].some((count) => count >= 2);
|
|
166
|
+
if (repeated && input.changedFiles.length === 0) {
|
|
167
|
+
return {
|
|
168
|
+
terminal: true,
|
|
169
|
+
terminalReason: "non-converging",
|
|
170
|
+
reason: "repeated failure fingerprint with an empty change manifest; recovery would loop forever",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
terminal: false,
|
|
175
|
+
terminalReason: "continue",
|
|
176
|
+
reason: "progress observable or fingerprint changed; continue bounded recovery",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -14,19 +14,19 @@ export const FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH = ".runtime/import-manif
|
|
|
14
14
|
/** Staging directory prefix under `active/` for in-flight child materialization. */
|
|
15
15
|
export const FRONTEND_RECOVERY_STAGING_PREFIX = ".frontend-recovery-staging-";
|
|
16
16
|
export function deriveFrontendRecoveryFailureSource(result) {
|
|
17
|
-
if (result.
|
|
18
|
-
|
|
17
|
+
if (result.failureSource === "frontend-plan-pi" ||
|
|
18
|
+
result.failureSource === "frontend-design-policy-shell" ||
|
|
19
|
+
result.failureSource === "frontend-implement-pi" ||
|
|
20
|
+
result.failureSource === "frontend-verify-shell") {
|
|
21
|
+
return result.failureSource;
|
|
19
22
|
}
|
|
20
|
-
|
|
21
|
-
return "frontend-plan-pi";
|
|
22
|
-
}
|
|
23
|
-
return "frontend-prewrite-gate-shell";
|
|
23
|
+
return "frontend-writer-admission-shell";
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Compute the frontend candidate-continuation reset partition:
|
|
27
|
-
* - `frontend-plan-pi` failure → reset plan + design-review +
|
|
28
|
-
* - `frontend-
|
|
29
|
-
* -
|
|
27
|
+
* - `frontend-plan-pi` failure → reset plan + design-review + policy/admission (+ descendants);
|
|
28
|
+
* - `frontend-design-policy-shell` failure → reset policy/admission (+ descendants);
|
|
29
|
+
* - admission/design-review failure → reset writer admission (+ descendants);
|
|
30
30
|
*
|
|
31
31
|
* Everything upstream of the failure source is imported as verified parent facts.
|
|
32
32
|
*/
|
|
@@ -55,7 +55,14 @@ export function computeFrontendWriterRecoveryPlan(input) {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
function computeFrontendRecoveryPlanForSource(input) {
|
|
58
|
-
|
|
58
|
+
// Verification failure means the implementation did not pass the frozen
|
|
59
|
+
// verification commands / trace. Resetting only verify + its descendants
|
|
60
|
+
// would re-run the same failing checks against the same implementation, so
|
|
61
|
+
// reset from implement: the recovery re-implements and re-verifies.
|
|
62
|
+
const resetRoot = input.failureSource === "frontend-verify-shell"
|
|
63
|
+
? "frontend-implement-pi"
|
|
64
|
+
: input.failureSource;
|
|
65
|
+
const resetNodeIds = computeResetClosure(input.spec, resetRoot);
|
|
59
66
|
const resetSet = new Set(resetNodeIds);
|
|
60
67
|
const importedNodeIds = input.spec.tasks
|
|
61
68
|
.map((task) => task.id)
|
|
@@ -6,7 +6,7 @@ import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_RE
|
|
|
6
6
|
import { dagRunDirExists, getDagRunDir, readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
|
|
7
7
|
import { buildDagRunId } from "./runner.js";
|
|
8
8
|
import { prepareRunDir, writeNodeRecord } from "./run-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import { FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, readFrontendWriterAdmissionResult, } from "./scheduler.js";
|
|
10
10
|
import { topoSortToRanks } from "./topo.js";
|
|
11
11
|
/**
|
|
12
12
|
* Candidate-continuation child creation kernel (phase 3b).
|
|
@@ -19,8 +19,8 @@ import { topoSortToRanks } from "./topo.js";
|
|
|
19
19
|
* phase=child-running. Scheduling/execution of the child is phase 3c and out
|
|
20
20
|
* of scope here.
|
|
21
21
|
*/
|
|
22
|
-
/** Canonical contract artifact default location; overridden by the
|
|
23
|
-
*
|
|
22
|
+
/** Canonical contract artifact default location; overridden by the design
|
|
23
|
+
* policy shell's configured outputDir/artifactName when present. */
|
|
24
24
|
const FRONTEND_CANONICAL_CONTRACT_DEFAULT_REL_PATH = "contracts/frontend-implementation-contract.json";
|
|
25
25
|
function resolveDeps(deps) {
|
|
26
26
|
return {
|
|
@@ -62,6 +62,9 @@ function isValidFrontendRecoveryMarker(raw) {
|
|
|
62
62
|
* Read + validate the activation marker. Missing or invalid markers return
|
|
63
63
|
* `undefined` so the phase-3c gate and tests can treat an unmarked active
|
|
64
64
|
* child as not executable (AC-2 / AC-5d).
|
|
65
|
+
*
|
|
66
|
+
* @deprecated Read-only/scheduler compatibility only; the runner no longer
|
|
67
|
+
* uses activation markers as the authoritative recovery entry point.
|
|
65
68
|
*/
|
|
66
69
|
export async function readFrontendRecoveryMarker(parentRunDir, requestId) {
|
|
67
70
|
let raw;
|
|
@@ -79,8 +82,8 @@ export async function readFrontendRecoveryMarker(parentRunDir, requestId) {
|
|
|
79
82
|
return raw;
|
|
80
83
|
}
|
|
81
84
|
function classifyFrontendFactKind(nodeId) {
|
|
82
|
-
if (/
|
|
83
|
-
return "
|
|
85
|
+
if (/admission|design-policy|verify|policy/i.test(nodeId))
|
|
86
|
+
return "gate";
|
|
84
87
|
if (/scout/i.test(nodeId))
|
|
85
88
|
return "scout";
|
|
86
89
|
if (/contract/i.test(nodeId))
|
|
@@ -91,9 +94,9 @@ function classifyFrontendFactKind(nodeId) {
|
|
|
91
94
|
}
|
|
92
95
|
function resolveCanonicalContractRelPath(spec) {
|
|
93
96
|
for (const task of spec.tasks) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return path.posix.join(
|
|
97
|
+
const policy = task.shell?.frontendDesignPolicy;
|
|
98
|
+
if (policy) {
|
|
99
|
+
return path.posix.join(policy.outputDir, policy.artifactName);
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
return FRONTEND_CANONICAL_CONTRACT_DEFAULT_REL_PATH;
|
|
@@ -286,15 +289,15 @@ async function materializeChildStaging(input) {
|
|
|
286
289
|
});
|
|
287
290
|
}
|
|
288
291
|
else {
|
|
289
|
-
const
|
|
290
|
-
if (!
|
|
291
|
-
throw new Error(
|
|
292
|
+
const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
|
|
293
|
+
if (!admissionRead.ok) {
|
|
294
|
+
throw new Error(admissionRead.reason);
|
|
292
295
|
}
|
|
293
296
|
plan = computeFrontendRecoveryPlan({
|
|
294
297
|
spec: parentSpec,
|
|
295
298
|
parentRunId,
|
|
296
299
|
requestId,
|
|
297
|
-
result:
|
|
300
|
+
result: admissionRead.result,
|
|
298
301
|
});
|
|
299
302
|
}
|
|
300
303
|
const { ranks } = topoSortToRanks(parentSpec);
|
|
@@ -330,17 +333,17 @@ async function materializeChildStaging(input) {
|
|
|
330
333
|
throw new Error(`node ${task.id} missing from frontend recovery plan partition`);
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
|
-
// Canonical contract +
|
|
336
|
+
// Canonical contract + writer-admission identity facts are copied alongside
|
|
334
337
|
// the per-node facts so reset nodes can re-read them (AC-4).
|
|
335
338
|
const canonicalContract = await copyArtifactVerified({
|
|
336
339
|
parentRunDir,
|
|
337
340
|
newRunDir: stagingDir,
|
|
338
341
|
relativePath: resolveCanonicalContractRelPath(parentSpec),
|
|
339
342
|
});
|
|
340
|
-
const
|
|
343
|
+
const writerAdmissionResult = await copyArtifactVerified({
|
|
341
344
|
parentRunDir,
|
|
342
345
|
newRunDir: stagingDir,
|
|
343
|
-
relativePath:
|
|
346
|
+
relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
|
|
344
347
|
});
|
|
345
348
|
const childState = {
|
|
346
349
|
version: 1,
|
|
@@ -377,9 +380,9 @@ async function materializeChildStaging(input) {
|
|
|
377
380
|
relativePath: canonicalContract.destinationRelativePath,
|
|
378
381
|
sha256: canonicalContract.sha256,
|
|
379
382
|
},
|
|
380
|
-
|
|
381
|
-
relativePath:
|
|
382
|
-
sha256:
|
|
383
|
+
writerAdmissionResult: {
|
|
384
|
+
relativePath: writerAdmissionResult.destinationRelativePath,
|
|
385
|
+
sha256: writerAdmissionResult.sha256,
|
|
383
386
|
},
|
|
384
387
|
importedFacts,
|
|
385
388
|
};
|
|
@@ -417,6 +420,9 @@ async function verifyThreeWayConsistency(input) {
|
|
|
417
420
|
* intent. Single-writer: the recovery coordinator is the only caller.
|
|
418
421
|
* Fail-closed on revision drift, missing intent, unreadable prewrite result,
|
|
419
422
|
* hash mismatch, marker-write failure, or consistency failure.
|
|
423
|
+
*
|
|
424
|
+
* @deprecated The M6 operation controller is the authoritative recovery loop;
|
|
425
|
+
* this kernel is retained only for read-only/scheduler compatibility.
|
|
420
426
|
*/
|
|
421
427
|
export async function stageRecoveryChild(options) {
|
|
422
428
|
const { cwd, parentRunId } = options;
|
|
@@ -452,8 +458,15 @@ export async function stageRecoveryChild(options) {
|
|
|
452
458
|
}
|
|
453
459
|
}
|
|
454
460
|
// Derive the childRunId exactly once; replay always reuses a reserved id.
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
// buildDagRunId is deterministic for the same task title on the same day. A
|
|
462
|
+
// recovery child must never reuse its still-active parent directory, or the
|
|
463
|
+
// staging path aliases the parent and the required import manifest is absent.
|
|
464
|
+
const childRunId = recovery.childRunId ?? await (async () => {
|
|
465
|
+
const generatedChildRunId = await (options.buildChildRunId ?? buildDagRunId)(parentSpec, cwd);
|
|
466
|
+
return generatedChildRunId === parentRunId
|
|
467
|
+
? `${generatedChildRunId}-recovery-${requestId.slice(0, 8)}`
|
|
468
|
+
: generatedChildRunId;
|
|
469
|
+
})();
|
|
457
470
|
// Orphan staging cleanup (D4.3): remove any in-flight staging dir for this id.
|
|
458
471
|
const activeRootDir = getDagRunDir(cwd, "active", "");
|
|
459
472
|
const stagingDir = path.join(activeRootDir, `${FRONTEND_RECOVERY_STAGING_PREFIX}${childRunId}`);
|