@tea-agent/loop-agent 0.39.0-beta.8 → 0.39.0-beta.9
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 +1 -1
- package/CHANGELOG.md +17 -5
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/commands/init.js +26 -0
- package/dist/executors/dag-pi-executor.js +326 -8
- package/dist/executors/pi-read-budget-policy.js +166 -0
- package/dist/executors/shell-executor.js +26 -30
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +123 -4
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +3 -0
- package/dist/worker/console/server.js +73 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
- package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
- package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
- package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/static/api.js +3 -3
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +5 -0
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/styles.css +331 -35
- package/dist/worker/observe/static/views/dag-inspector.js +509 -78
- package/dist/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/frontend-design-policy.js +19 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +109 -49
- package/dist/workflows/dag/frontend-plan-render.js +27 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
- package/dist/workflows/dag/frontend-review-context.js +102 -15
- package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
- package/dist/workflows/dag/frontend-writer-admission.js +12 -0
- package/dist/workflows/dag/init-hybrid.js +293 -65
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +111 -1
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +26 -3
- package/dist/workflows/dag/runner.js +38 -8
- package/dist/workflows/dag/types.js +51 -11
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +4 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +2 -3
- package/skills/frontend-implementation/references/design-spec.md +4 -3
- package/skills/frontend-implementation/references/node-contracts.md +4 -3
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
|
@@ -410,6 +410,13 @@ export function startInterruptWatcher(input) {
|
|
|
410
410
|
let watcher;
|
|
411
411
|
try {
|
|
412
412
|
watcher = watch(path.join(input.runDir, ".runtime"), () => tick());
|
|
413
|
+
// fs.watch can report EMFILE/ENOSPC asynchronously after watch() returns.
|
|
414
|
+
// Polling below is the reliable path, so watcher errors must never escape
|
|
415
|
+
// as an unhandled EventEmitter error and orphan the DAG runner.
|
|
416
|
+
watcher.on("error", () => {
|
|
417
|
+
watcher?.close();
|
|
418
|
+
watcher = undefined;
|
|
419
|
+
});
|
|
413
420
|
watcher.unref?.();
|
|
414
421
|
}
|
|
415
422
|
catch {
|
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
6
|
+
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
6
7
|
import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
|
|
7
8
|
import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
8
9
|
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
@@ -164,6 +165,45 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
164
165
|
];
|
|
165
166
|
}
|
|
166
167
|
const FRONTEND_PLAN_NODE_ID = "frontend-plan-pi";
|
|
168
|
+
const COMPACT_RETRY_SECTION_MAX_CHARS = 1_200;
|
|
169
|
+
const COMPACT_RETRY_TASK_MAX_CHARS = 4_800;
|
|
170
|
+
function compactRetryText(text, maxChars) {
|
|
171
|
+
if (text.length <= maxChars)
|
|
172
|
+
return text;
|
|
173
|
+
const headChars = Math.floor(maxChars * 0.78);
|
|
174
|
+
const tailChars = Math.max(0, maxChars - headChars - 52);
|
|
175
|
+
return `${text.slice(0, headChars)}\n… [omitted after context-overflow] …\n${text.slice(-tailChars)}`;
|
|
176
|
+
}
|
|
177
|
+
function compactRetrySection(prompt, name, maxChars) {
|
|
178
|
+
const match = prompt.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
|
|
179
|
+
return compactRetryText(match?.[1]?.trim() ?? "(not available)", maxChars);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A context-overflow retry must not resend the full resolved skills, governance
|
|
183
|
+
* payload, repeated source excerpts, and upstream previews that caused the
|
|
184
|
+
* previous session to overflow. Keep the immutable node contract plus bounded
|
|
185
|
+
* objective/criteria/task excerpts; canonical artifacts remain readable by
|
|
186
|
+
* pointer from the retained task instructions.
|
|
187
|
+
*/
|
|
188
|
+
export function buildContextOverflowRetryPrompt(task, basePrompt) {
|
|
189
|
+
return [
|
|
190
|
+
"<compact_retry_context>",
|
|
191
|
+
"This is a fresh, compact retry after provider context-overflow. The original full prompt remains audit evidence but is intentionally not resent.",
|
|
192
|
+
"Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by canonical artifacts. Start from the smallest retained artifact and expand only the exact file or diff fragment needed.",
|
|
193
|
+
"</compact_retry_context>",
|
|
194
|
+
`<dag_objective>\n${compactRetrySection(basePrompt, "dag_objective", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</dag_objective>`,
|
|
195
|
+
`<success_criteria>\n${compactRetrySection(basePrompt, "success_criteria", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</success_criteria>`,
|
|
196
|
+
`<global_constraints>\n${compactRetrySection(basePrompt, "global_constraints", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</global_constraints>`,
|
|
197
|
+
`<node_contract>\n${compactRetrySection(basePrompt, "node_contract", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</node_contract>`,
|
|
198
|
+
`<upstream_context>\n${compactRetrySection(basePrompt, "upstream_context", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</upstream_context>`,
|
|
199
|
+
[
|
|
200
|
+
"<task>",
|
|
201
|
+
`Original task id: ${task.id}`,
|
|
202
|
+
compactRetrySection(basePrompt, "task", COMPACT_RETRY_TASK_MAX_CHARS),
|
|
203
|
+
"</task>",
|
|
204
|
+
].join("\n"),
|
|
205
|
+
].join("\n\n");
|
|
206
|
+
}
|
|
167
207
|
function isFrontendPlanLadderTask(task) {
|
|
168
208
|
return task.id === FRONTEND_PLAN_NODE_ID;
|
|
169
209
|
}
|
|
@@ -184,9 +224,25 @@ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
|
|
|
184
224
|
const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
|
|
185
225
|
return { digest, hasTerminalFact };
|
|
186
226
|
}
|
|
187
|
-
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
|
|
227
|
+
export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
|
|
188
228
|
if (attemptNumber <= 1)
|
|
189
229
|
return basePrompt;
|
|
230
|
+
// Context overflow is a transport/session failure, not a plan-protocol
|
|
231
|
+
// failure. It must take precedence over the plan retry ladder so every
|
|
232
|
+
// retry, including a repeated plan overflow, starts from the compact
|
|
233
|
+
// envelope rather than re-sending the original prompt.
|
|
234
|
+
if (previousFailureCategory === "context-overflow") {
|
|
235
|
+
return [
|
|
236
|
+
buildContextOverflowRetryPrompt(task, basePrompt),
|
|
237
|
+
"",
|
|
238
|
+
"<retry_instruction>",
|
|
239
|
+
"The previous attempt exceeded the provider context window. This retry starts a fresh session: keep the evidence surface narrow.",
|
|
240
|
+
"Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by the canonical contract/review context. Read the smallest relevant artifact first, then only the specific source file or diff fragment needed to decide a finding.",
|
|
241
|
+
"Do not read artifacts/diff_patch.patch in full. For frontend review, read the bounded diff summary and then at most the specific per-file diff fragments it identifies, in part order.",
|
|
242
|
+
"Use concise tool calls and finish the required terminal/output protocol as soon as the evidence is sufficient.",
|
|
243
|
+
"</retry_instruction>",
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
190
246
|
if (frontendPlanRetryStep === "compact-terminal-first") {
|
|
191
247
|
return [
|
|
192
248
|
basePrompt,
|
|
@@ -239,6 +295,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
239
295
|
].join("\n");
|
|
240
296
|
}
|
|
241
297
|
if (previousFailureCategory === "read-burst") {
|
|
298
|
+
if (task.id !== FRONTEND_PLAN_NODE_ID) {
|
|
299
|
+
return [
|
|
300
|
+
basePrompt,
|
|
301
|
+
"",
|
|
302
|
+
"<retry_instruction>",
|
|
303
|
+
"The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
|
|
304
|
+
"Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
|
|
305
|
+
"</retry_instruction>",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
242
308
|
return [
|
|
243
309
|
basePrompt,
|
|
244
310
|
"",
|
|
@@ -430,6 +496,35 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
430
496
|
"</retry_instruction>",
|
|
431
497
|
].join("\n");
|
|
432
498
|
}
|
|
499
|
+
const PROMPT_RESTART_CANDIDATE_MAX_BYTES = 64 * 1024;
|
|
500
|
+
function promptRestartCandidateForAttempt(input) {
|
|
501
|
+
if (input.attemptPrompt === input.baseRuntimePrompt) {
|
|
502
|
+
return input.taskPrompt;
|
|
503
|
+
}
|
|
504
|
+
const appendedPrefix = `${input.baseRuntimePrompt}\n\n`;
|
|
505
|
+
if (input.attemptPrompt.startsWith(appendedPrefix)) {
|
|
506
|
+
return [
|
|
507
|
+
input.taskPrompt,
|
|
508
|
+
input.attemptPrompt.slice(appendedPrefix.length),
|
|
509
|
+
]
|
|
510
|
+
.filter(Boolean)
|
|
511
|
+
.join("\n\n");
|
|
512
|
+
}
|
|
513
|
+
return input.attemptPrompt;
|
|
514
|
+
}
|
|
515
|
+
async function persistFailedPromptRestartCandidate(input) {
|
|
516
|
+
const redacted = redactSecrets(input.prompt);
|
|
517
|
+
const preview = truncateUtf8Preview(redacted, PROMPT_RESTART_CANDIDATE_MAX_BYTES);
|
|
518
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, `prompt-candidate-attempt-${input.attempt}.json`, {
|
|
519
|
+
schemaVersion: 1,
|
|
520
|
+
attempt: input.attempt,
|
|
521
|
+
failureCategory: input.failureCategory ?? "unknown",
|
|
522
|
+
prompt: preview,
|
|
523
|
+
truncated: preview !== redacted,
|
|
524
|
+
maxBytes: PROMPT_RESTART_CANDIDATE_MAX_BYTES,
|
|
525
|
+
sha256: createHash("sha256").update(redacted).digest("hex"),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
433
528
|
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
|
|
434
529
|
const policy = resolveContextPolicy(spec);
|
|
435
530
|
const skillNames = policy.resolveSkills(spec, task);
|
|
@@ -904,6 +999,7 @@ export async function executeDagNode(input) {
|
|
|
904
999
|
// exactly this attempt's segment (B1: earlier attempts legitimately use
|
|
905
1000
|
// ls/find and must not be attributed to the repair attempt).
|
|
906
1001
|
const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
|
|
1002
|
+
let promptRestartCandidate = task.subtask_prompt;
|
|
907
1003
|
let result;
|
|
908
1004
|
try {
|
|
909
1005
|
validateRepairArtifactGateBeforeShell({
|
|
@@ -974,6 +1070,11 @@ export async function executeDagNode(input) {
|
|
|
974
1070
|
previousReason: previousProtocolReason,
|
|
975
1071
|
});
|
|
976
1072
|
}
|
|
1073
|
+
promptRestartCandidate = promptRestartCandidateForAttempt({
|
|
1074
|
+
taskPrompt: task.subtask_prompt,
|
|
1075
|
+
baseRuntimePrompt: prompt,
|
|
1076
|
+
attemptPrompt,
|
|
1077
|
+
});
|
|
977
1078
|
result = await executeNode({
|
|
978
1079
|
task,
|
|
979
1080
|
cwd,
|
|
@@ -1101,6 +1202,15 @@ export async function executeDagNode(input) {
|
|
|
1101
1202
|
const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
|
|
1102
1203
|
totalAttemptWallDurationMs += attemptWallDurationMs;
|
|
1103
1204
|
const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
|
|
1205
|
+
if (task.executor === "pi" && !result.ok) {
|
|
1206
|
+
await persistFailedPromptRestartCandidate({
|
|
1207
|
+
runDir,
|
|
1208
|
+
nodeId,
|
|
1209
|
+
attempt: attemptNumber,
|
|
1210
|
+
failureCategory: result.failureCategory,
|
|
1211
|
+
prompt: promptRestartCandidate,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1104
1214
|
const attemptRecord = {
|
|
1105
1215
|
attempt: attemptNumber,
|
|
1106
1216
|
startedAt: attemptStartedAt,
|
|
@@ -45,9 +45,42 @@ function formatResolvedSkillInstructions(instructions) {
|
|
|
45
45
|
if (!instructions || instructions.length === 0)
|
|
46
46
|
return "(none)";
|
|
47
47
|
return instructions
|
|
48
|
-
.map((instruction) => instruction
|
|
48
|
+
.map((instruction) => formatSkillChecklist(instruction))
|
|
49
49
|
.join("\n\n---\n\n");
|
|
50
50
|
}
|
|
51
|
+
/** Keep skill provenance, but turn bounded inline manuals into a small action list. */
|
|
52
|
+
function formatSkillChecklist(instruction) {
|
|
53
|
+
const metadata = [
|
|
54
|
+
`Skill: ${instruction.name}`,
|
|
55
|
+
`Resolution: ${instruction.resolution}`,
|
|
56
|
+
...(instruction.path ? [`Path: ${instruction.path}`] : []),
|
|
57
|
+
...(instruction.sha256 ? [`Sha256: ${instruction.sha256}`] : []),
|
|
58
|
+
"Injection mode: checklist",
|
|
59
|
+
];
|
|
60
|
+
if (instruction.resolution !== "resolved")
|
|
61
|
+
return [...metadata, instruction.promptText].join("\n");
|
|
62
|
+
const source = instruction.promptText.split("Instructions:\n")[1] ?? "";
|
|
63
|
+
const candidates = source
|
|
64
|
+
.split(/\r?\n/)
|
|
65
|
+
.map((line) => line.trim())
|
|
66
|
+
.filter((line) => /^(?:#{1,3}\s+|[-*]\s+|\d+[.)]\s+)/.test(line) || line.length > 0)
|
|
67
|
+
.map((line) => line.replace(/^#{1,3}\s+/, "").replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, ""))
|
|
68
|
+
.filter((line) => line.length > 0);
|
|
69
|
+
const checklist = [];
|
|
70
|
+
let chars = 0;
|
|
71
|
+
for (const item of candidates) {
|
|
72
|
+
const bounded = item.slice(0, 240);
|
|
73
|
+
if (checklist.length >= 10 || chars + bounded.length > 1_600)
|
|
74
|
+
break;
|
|
75
|
+
checklist.push(`- ${bounded}`);
|
|
76
|
+
chars += bounded.length;
|
|
77
|
+
}
|
|
78
|
+
return [
|
|
79
|
+
...metadata,
|
|
80
|
+
"Checklist (apply these actions; consult the immutable resolved file only when a listed action requires detail):",
|
|
81
|
+
...(checklist.length > 0 ? checklist : ["- Follow the resolved skill's declared workflow."]),
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
51
84
|
export function formatOutputLanguageBlock(language = DEFAULT_DAG_OUTPUT_LANGUAGE) {
|
|
52
85
|
if (language === "en") {
|
|
53
86
|
return [
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { DAG_RUNS_DIR } from "./lifecycle.js";
|
|
6
|
+
function recoveryLeasePath(cwd, parentRunId) {
|
|
7
|
+
// Never derive a filesystem name directly from an externally supplied legacy
|
|
8
|
+
// run id. The content remains auditable while the filename is traversal-safe.
|
|
9
|
+
const key = createHash("sha256").update(parentRunId).digest("hex");
|
|
10
|
+
return path.join(cwd, DAG_RUNS_DIR, ".recovery-leases", `${key}.json`);
|
|
11
|
+
}
|
|
12
|
+
function parseLease(raw, parentRunId) {
|
|
13
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
14
|
+
throw new Error("invalid dag recovery lease: expected object");
|
|
15
|
+
}
|
|
16
|
+
const record = raw;
|
|
17
|
+
if (record.schemaVersion !== 1 ||
|
|
18
|
+
record.parentRunId !== parentRunId ||
|
|
19
|
+
typeof record.requestId !== "string" ||
|
|
20
|
+
record.requestId.length === 0 ||
|
|
21
|
+
(record.kind !== "auto-frontend-recovery" &&
|
|
22
|
+
record.kind !== "rerun-from-node" &&
|
|
23
|
+
record.kind !== "standalone-task-rerun") ||
|
|
24
|
+
typeof record.leaseToken !== "string" ||
|
|
25
|
+
record.leaseToken.length === 0 ||
|
|
26
|
+
typeof record.acquiredAt !== "string" ||
|
|
27
|
+
typeof record.executionRef !== "undefined" &&
|
|
28
|
+
typeof record.executionRef !== "string") {
|
|
29
|
+
throw new Error("invalid dag recovery lease: schema mismatch");
|
|
30
|
+
}
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
33
|
+
export async function acquireDagRecoveryLease(input) {
|
|
34
|
+
const leasePath = recoveryLeasePath(input.cwd, input.parentRunId);
|
|
35
|
+
const lease = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
parentRunId: input.parentRunId,
|
|
38
|
+
requestId: input.requestId,
|
|
39
|
+
kind: input.kind,
|
|
40
|
+
leaseToken: randomBytes(16).toString("hex"),
|
|
41
|
+
acquiredAt: (input.now ?? (() => new Date()))().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
await mkdir(path.dirname(leasePath), { recursive: true });
|
|
44
|
+
try {
|
|
45
|
+
// `wx` is the cross-process compare-and-create primitive. The lease is
|
|
46
|
+
// deliberately immutable for a parent run: a later recovery must follow
|
|
47
|
+
// the committed child lineage rather than fork another DAG.
|
|
48
|
+
await writeFile(leasePath, `${JSON.stringify(lease, null, 2)}\n`, {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
flag: "wx",
|
|
51
|
+
});
|
|
52
|
+
return { kind: "acquired", leasePath, lease };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const existing = parseLease(JSON.parse(await readFile(leasePath, "utf8")), input.parentRunId);
|
|
59
|
+
return { kind: "held", leasePath, lease: existing };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Persist the operation/run identity before the caller is allowed to execute it. */
|
|
63
|
+
export async function bindDagRecoveryLeaseExecution(input) {
|
|
64
|
+
if (!input.executionRef.trim()) {
|
|
65
|
+
throw new Error("dag recovery lease executionRef is required");
|
|
66
|
+
}
|
|
67
|
+
const existing = parseLease(JSON.parse(await readFile(input.leasePath, "utf8")), input.lease.parentRunId);
|
|
68
|
+
if (existing.leaseToken !== input.lease.leaseToken) {
|
|
69
|
+
throw new Error("dag recovery lease ownership drift");
|
|
70
|
+
}
|
|
71
|
+
if (existing.executionRef && existing.executionRef !== input.executionRef) {
|
|
72
|
+
throw new Error("dag recovery lease execution identity drift");
|
|
73
|
+
}
|
|
74
|
+
const bound = {
|
|
75
|
+
...existing,
|
|
76
|
+
executionRef: input.executionRef,
|
|
77
|
+
};
|
|
78
|
+
await writeJsonAtomic(input.leasePath, bound);
|
|
79
|
+
return bound;
|
|
80
|
+
}
|
|
@@ -12,6 +12,7 @@ import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } fr
|
|
|
12
12
|
import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
13
13
|
import { topoSortToRanks } from "./topo.js";
|
|
14
14
|
import { captureWorkspaceCheckpoint, readWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
|
|
15
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
15
16
|
export const RERUN_PLAN_REL_PATH = ".runtime/rerun-plan.json";
|
|
16
17
|
export const IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
|
|
17
18
|
export const WORKER_ASSOCIATION_REL_PATH = ".runtime/worker-association.json";
|
|
@@ -572,6 +573,20 @@ export async function executeDagRerun(input) {
|
|
|
572
573
|
}
|
|
573
574
|
const parentSpec = await readDagRunSpec(located.runDir);
|
|
574
575
|
const parentState = await readDagRunState(located.runDir);
|
|
576
|
+
const recoveryLease = await acquireDagRecoveryLease({
|
|
577
|
+
cwd: input.cwd,
|
|
578
|
+
parentRunId: input.parentRunId,
|
|
579
|
+
requestId: input.requestId,
|
|
580
|
+
kind: "rerun-from-node",
|
|
581
|
+
});
|
|
582
|
+
if (recoveryLease.kind === "held") {
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
plan,
|
|
586
|
+
errorCode: "RECOVERY_LEASE_HELD",
|
|
587
|
+
message: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
575
590
|
const staged = await stageContinuationRun({
|
|
576
591
|
cwd: input.cwd,
|
|
577
592
|
parentRunDir: located.runDir,
|
|
@@ -583,6 +598,14 @@ export async function executeDagRerun(input) {
|
|
|
583
598
|
requestId: input.requestId,
|
|
584
599
|
promptOverrides: input.promptOverrides,
|
|
585
600
|
});
|
|
601
|
+
// Stage is non-executing; bind the child identity immediately before handing
|
|
602
|
+
// it to the runner so no recovery DAG can start without durable parent
|
|
603
|
+
// lineage ownership.
|
|
604
|
+
await bindDagRecoveryLeaseExecution({
|
|
605
|
+
leasePath: recoveryLease.leasePath,
|
|
606
|
+
lease: recoveryLease.lease,
|
|
607
|
+
executionRef: staged.state.runId,
|
|
608
|
+
});
|
|
586
609
|
const continuationOptions = {
|
|
587
610
|
cwd: input.cwd,
|
|
588
611
|
spec: staged.spec,
|
|
@@ -4,6 +4,7 @@ import { generateTaskDagUseCase } from "../../application/dag/generate-task-dag.
|
|
|
4
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { DAG_RUNS_DIR, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
6
6
|
import { deriveDagRerunFeedback } from "./rerun-feedback.js";
|
|
7
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
7
8
|
import { parseDagSpec, } from "./types.js";
|
|
8
9
|
export const RERUN_TASK_LINEAGE_REL_PATH = ".runtime/rerun-task-lineage.json";
|
|
9
10
|
export function parseDagRerunTaskArgs(args) {
|
|
@@ -369,6 +370,34 @@ export async function rerunStandaloneTask(input) {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
}
|
|
373
|
+
const recoveryLease = execute
|
|
374
|
+
? await acquireDagRecoveryLease({
|
|
375
|
+
cwd: input.cwd,
|
|
376
|
+
parentRunId: input.parentRunId,
|
|
377
|
+
requestId: input.requestId,
|
|
378
|
+
kind: "standalone-task-rerun",
|
|
379
|
+
})
|
|
380
|
+
: undefined;
|
|
381
|
+
if (recoveryLease?.kind === "held") {
|
|
382
|
+
return blockedResult({
|
|
383
|
+
parentRunId: input.parentRunId,
|
|
384
|
+
reason: input.reason,
|
|
385
|
+
requestId: input.requestId,
|
|
386
|
+
blockedReason: "recovery-lease-held",
|
|
387
|
+
note: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
|
|
388
|
+
taskId: taskResolution.taskId,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (recoveryLease?.kind === "acquired") {
|
|
392
|
+
// Persist a non-empty execution identity before generateTaskDagUseCase can
|
|
393
|
+
// execute its new DAG. The generated run id is not available yet, so the
|
|
394
|
+
// request id is the stable lineage handle for this entry point.
|
|
395
|
+
await bindDagRecoveryLeaseExecution({
|
|
396
|
+
leasePath: recoveryLease.leasePath,
|
|
397
|
+
lease: recoveryLease.lease,
|
|
398
|
+
executionRef: `standalone-task-rerun:${input.requestId}`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
372
401
|
const generateResult = await generateTaskDagUseCase({
|
|
373
402
|
repoRoot: input.cwd,
|
|
374
403
|
taskId: taskResolution.taskId,
|
|
@@ -132,13 +132,24 @@ export const dagRetryPolicySchema = z
|
|
|
132
132
|
/**
|
|
133
133
|
* The default retry policy applied to safe generated read-only Pi nodes.
|
|
134
134
|
* Total attempts: 3, exponential backoff with cap.
|
|
135
|
+
*
|
|
136
|
+
* Includes `context-overflow`: a read-only node that blew the context window
|
|
137
|
+
* (400 request too large, e.g. a review node accumulating too many reads) is a
|
|
138
|
+
* transient, safe-to-retry failure — the retry re-runs the node with a
|
|
139
|
+
* compact-context instruction instead of failing the whole run. Previously
|
|
140
|
+
* only writer transport policies had context-overflow; contract/scout/
|
|
141
|
+
* design-review/review fell straight to ERROR without a retry attempt.
|
|
135
142
|
*/
|
|
136
143
|
export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
|
|
137
144
|
maxAttempts: 3,
|
|
138
145
|
backoff: "exponential",
|
|
139
146
|
initialDelayMs: 2000,
|
|
140
147
|
maxDelayMs: 30000,
|
|
141
|
-
retryCategories: [
|
|
148
|
+
retryCategories: [
|
|
149
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
150
|
+
READ_BURST_RETRY_CATEGORY,
|
|
151
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
152
|
+
],
|
|
142
153
|
};
|
|
143
154
|
/**
|
|
144
155
|
* Structured planner nodes may retry with a compact-output instruction
|
|
@@ -148,7 +159,11 @@ export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
|
|
|
148
159
|
*/
|
|
149
160
|
export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
|
|
150
161
|
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
151
|
-
retryCategories: [
|
|
162
|
+
retryCategories: [
|
|
163
|
+
...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES,
|
|
164
|
+
READ_BURST_RETRY_CATEGORY,
|
|
165
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
166
|
+
],
|
|
152
167
|
};
|
|
153
168
|
/**
|
|
154
169
|
* Planner read-only nodes (standard DAG contract-pi / plan-pi) may retry with
|
|
@@ -159,7 +174,12 @@ export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
|
|
|
159
174
|
*/
|
|
160
175
|
export const PLANNER_OUTPUT_LIMIT_RETRY_POLICY = {
|
|
161
176
|
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
162
|
-
retryCategories: [
|
|
177
|
+
retryCategories: [
|
|
178
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
179
|
+
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
180
|
+
READ_BURST_RETRY_CATEGORY,
|
|
181
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
182
|
+
],
|
|
163
183
|
};
|
|
164
184
|
/**
|
|
165
185
|
* Default retry for reviewer / recovery nodes that declare outputProtocol.
|
|
@@ -180,6 +200,8 @@ export const FRONTEND_REVIEW_TERMINAL_RETRY_POLICY = {
|
|
|
180
200
|
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
181
201
|
retryCategories: [
|
|
182
202
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
203
|
+
READ_BURST_RETRY_CATEGORY,
|
|
204
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
183
205
|
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
184
206
|
],
|
|
185
207
|
};
|
|
@@ -553,5 +575,6 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
|
|
|
553
575
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
554
576
|
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
555
577
|
READ_BURST_RETRY_CATEGORY,
|
|
578
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
556
579
|
],
|
|
557
580
|
};
|
|
@@ -31,6 +31,7 @@ import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas,
|
|
|
31
31
|
import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
|
|
32
32
|
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
33
33
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
34
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
34
35
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
35
36
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
36
37
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
@@ -822,6 +823,15 @@ async function executeDagCheckpoint(input) {
|
|
|
822
823
|
"frontend recovery request-id reuse conflict";
|
|
823
824
|
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
824
825
|
}
|
|
826
|
+
else if (created.kind === "lease-held") {
|
|
827
|
+
// A different recovery entrance has already durably claimed this
|
|
828
|
+
// parent. Do not "help" by running its operation: that would reopen
|
|
829
|
+
// the exact two-child-DAG race the lease prevents.
|
|
830
|
+
state.status = "failed";
|
|
831
|
+
state.terminalReason =
|
|
832
|
+
"frontend recovery parent lease already held";
|
|
833
|
+
state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
|
|
834
|
+
}
|
|
825
835
|
else {
|
|
826
836
|
const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
|
|
827
837
|
const result = await runRecoveryOperationController({
|
|
@@ -1263,16 +1273,25 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1263
1273
|
/**
|
|
1264
1274
|
* Construct (or reuse) the recovery operation for a failed frontend run.
|
|
1265
1275
|
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
* `
|
|
1269
|
-
*
|
|
1270
|
-
* frozen via `computeFrontendRecoveryPlanForSource` and
|
|
1271
|
-
* run dir's bounded `.runtime/` namespace
|
|
1272
|
-
*
|
|
1276
|
+
* A parent-run atomic lease is acquired before `OperationStore.create`, then
|
|
1277
|
+
* its operation id is persisted into that lease before the controller may run.
|
|
1278
|
+
* `clientRequestId` remains a useful store-level replay key, but is not a
|
|
1279
|
+
* cross-process recovery admission boundary. The continuation DagSpec/reset
|
|
1280
|
+
* partition is frozen via `computeFrontendRecoveryPlanForSource` and
|
|
1281
|
+
* serialized under the run dir's bounded `.runtime/` namespace. `planHash` is
|
|
1282
|
+
* the second idempotency dimension of `actionParams`.
|
|
1273
1283
|
*/
|
|
1274
1284
|
async function createRecoveryOperation(input) {
|
|
1275
1285
|
const { cwd, runDir, spec, state, trigger } = input;
|
|
1286
|
+
const recoveryLease = await acquireDagRecoveryLease({
|
|
1287
|
+
cwd,
|
|
1288
|
+
parentRunId: state.runId,
|
|
1289
|
+
requestId: trigger.requestId,
|
|
1290
|
+
kind: "auto-frontend-recovery",
|
|
1291
|
+
});
|
|
1292
|
+
if (recoveryLease.kind === "held") {
|
|
1293
|
+
return { kind: "lease-held", lease: recoveryLease.lease };
|
|
1294
|
+
}
|
|
1276
1295
|
const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
|
|
1277
1296
|
const plan = computeFrontendRecoveryPlanForSource({
|
|
1278
1297
|
spec,
|
|
@@ -1292,7 +1311,7 @@ async function createRecoveryOperation(input) {
|
|
|
1292
1311
|
recoveryMode: trigger.recoveryMode,
|
|
1293
1312
|
plan,
|
|
1294
1313
|
});
|
|
1295
|
-
|
|
1314
|
+
const created = await input.deps.store.create({
|
|
1296
1315
|
clientRequestId: `dag-recovery-${state.runId}`,
|
|
1297
1316
|
action: "dagRunTask",
|
|
1298
1317
|
actionParams: {
|
|
@@ -1314,6 +1333,17 @@ async function createRecoveryOperation(input) {
|
|
|
1314
1333
|
"--json",
|
|
1315
1334
|
],
|
|
1316
1335
|
});
|
|
1336
|
+
if (created.kind !== "conflict") {
|
|
1337
|
+
// The durable lease must name the operation before controller execution.
|
|
1338
|
+
// A crash in this narrow window leaves a safely blocked reservation rather
|
|
1339
|
+
// than a second process that can launch another recovery DAG.
|
|
1340
|
+
await bindDagRecoveryLeaseExecution({
|
|
1341
|
+
leasePath: recoveryLease.leasePath,
|
|
1342
|
+
lease: recoveryLease.lease,
|
|
1343
|
+
executionRef: created.operation.operationId,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
return created;
|
|
1317
1347
|
}
|
|
1318
1348
|
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
1319
1349
|
if (rankWriterNodeIds.length <= 1)
|
|
@@ -946,6 +946,15 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
|
|
|
946
946
|
role: dagRoleSchema.optional(),
|
|
947
947
|
skills: z.array(z.string()).optional(),
|
|
948
948
|
toolProfile: dagToolProfileSchema.optional(),
|
|
949
|
+
/** Bounded read-only discovery budget for nodes that inspect a repository. */
|
|
950
|
+
readBudget: z
|
|
951
|
+
.object({
|
|
952
|
+
maxFiles: z.number().int().positive(),
|
|
953
|
+
maxBytes: z.number().int().positive(),
|
|
954
|
+
maxMs: z.number().int().positive().optional(),
|
|
955
|
+
})
|
|
956
|
+
.strict()
|
|
957
|
+
.optional(),
|
|
949
958
|
/**
|
|
950
959
|
* Explicit Pi extension allowlist (backend implementation DAGs only):
|
|
951
960
|
* frozen short ids resolved at run time against the Pi settings package
|
|
@@ -1071,20 +1080,51 @@ export const dagEvaluationBindingSchema = z
|
|
|
1071
1080
|
.strict();
|
|
1072
1081
|
/** Eval Lab Campaign Budget (W3.4); enforced by runner ledger when mode=hard. */
|
|
1073
1082
|
export const dagBudgetSchema = campaignBudgetSchema;
|
|
1074
|
-
export const
|
|
1083
|
+
export const dagSourceBindingSourceSchema = z.object({
|
|
1084
|
+
kind: z.enum(["requirement", "constraint", "reference"]),
|
|
1085
|
+
path: z.string().min(1),
|
|
1086
|
+
sha256: z
|
|
1087
|
+
.string()
|
|
1088
|
+
.regex(/^[a-f0-9]{64}$/, "sha256 must be lowercase hex"),
|
|
1089
|
+
});
|
|
1090
|
+
/**
|
|
1091
|
+
* DagSourceBinding v1:无 ledger 字段(schemaVersion=1,.strict() 拒绝 ledger
|
|
1092
|
+
* 字段混入,不得以 v1+optional 冒充 v2)。非 frontend task / 无可解析引用时使用。
|
|
1093
|
+
*/
|
|
1094
|
+
export const dagSourceBindingV1Schema = z
|
|
1095
|
+
.object({
|
|
1075
1096
|
schemaVersion: z.literal(1),
|
|
1076
1097
|
taskId: z.string().min(1),
|
|
1077
|
-
sources: z
|
|
1078
|
-
.array(z.object({
|
|
1079
|
-
kind: z.enum(["requirement", "constraint", "reference"]),
|
|
1080
|
-
path: z.string().min(1),
|
|
1081
|
-
sha256: z
|
|
1082
|
-
.string()
|
|
1083
|
-
.regex(/^[a-f0-9]{64}$/, "sha256 must be lowercase hex"),
|
|
1084
|
-
}))
|
|
1085
|
-
.min(1),
|
|
1098
|
+
sources: z.array(dagSourceBindingSourceSchema).min(1),
|
|
1086
1099
|
requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
|
|
1087
|
-
})
|
|
1100
|
+
})
|
|
1101
|
+
.strict();
|
|
1102
|
+
/**
|
|
1103
|
+
* DagSourceBinding v2(20260824-frontend-source-fidelity-ledger-v1):仅
|
|
1104
|
+
* frontend-implementation 且存在可解析 requirement/acceptance 引用时产出;
|
|
1105
|
+
* ledgerPath/ledgerSha256/inputDigest/requirementToFragments 全部必填且完整,
|
|
1106
|
+
* 伪造或缺失任一字段被 schema/prewrite 拒绝(AC-002 / AC-FIDELITY-REPAIR-002)。
|
|
1107
|
+
*/
|
|
1108
|
+
export const dagSourceBindingV2Schema = z
|
|
1109
|
+
.object({
|
|
1110
|
+
schemaVersion: z.literal(2),
|
|
1111
|
+
taskId: z.string().min(1),
|
|
1112
|
+
sources: z.array(dagSourceBindingSourceSchema).min(1),
|
|
1113
|
+
requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
|
|
1114
|
+
/** 受管 requirement-ledger.json 的 DAG 可解析路径(.harness/tasks/…/source/…)。 */
|
|
1115
|
+
ledgerPath: z.string().min(1),
|
|
1116
|
+
ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1117
|
+
inputDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1118
|
+
requirementToFragments: z.record(z.string(), z.array(z.string())),
|
|
1119
|
+
})
|
|
1120
|
+
.strict();
|
|
1121
|
+
export const dagSourceBindingSchema = z.discriminatedUnion("schemaVersion", [
|
|
1122
|
+
dagSourceBindingV1Schema,
|
|
1123
|
+
dagSourceBindingV2Schema,
|
|
1124
|
+
]);
|
|
1125
|
+
export function isDagSourceBindingV2(binding) {
|
|
1126
|
+
return binding.schemaVersion === 2;
|
|
1127
|
+
}
|
|
1088
1128
|
const dagRerunFeedbackEvidencePathSchema = z
|
|
1089
1129
|
.string()
|
|
1090
1130
|
.min(1)
|
package/docs/README.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| 选择快速、标准或完整验证命令 | `governance/verification-matrix.md` |
|
|
28
28
|
| 理解 CLI、runtime 和初始化能力边界 | `runtime/loop-agent-harness.md` |
|
|
29
29
|
| 运行 DAG、查看报告或处理失败 | `runtime/agent-dag-runner.md`、`runtime/agent-dag-recovery-playbook.md`;Operator Chat P0 closeout 见 `reports/feature/2026-08-14-operator-chat-dag-recovery-p0.md` |
|
|
30
|
+
| 理解前端实现 DAG 与需求保真账本 | `runtime/frontend-implementation-workflow.md`(含 source fidelity ledger v2 绑定、顺序 reconciliation、独立 reviewer、human resolution 与 prewrite fail-closed);init-evolution 审查见 `reports/init-evolution/2026-08-24-frontend-source-fidelity-ledger-init-evolution-review.md` |
|
|
30
31
|
| 按 TDD、验证和调试纪律推进修复 | `governance/harness-methodology-tdd.md`、`governance/harness-methodology-verification.md`、`governance/harness-methodology-debugging.md` |
|
|
31
32
|
| 执行真实 backend-test provider 验证 | `runtime/backend-test-live-campaign.md` |
|
|
32
33
|
| 判断单任务 loop-agent Production Readiness | `operations/production-readiness.md` |
|