@tea-agent/loop-agent 0.42.0-next.14 → 0.42.0-next.16
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/CHANGELOG.md +19 -3
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1437 -154
- package/dist/executors/shell-executor.js +107 -35
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +8 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BGMbAdAG.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CJu1LND8.js → arc-DN59MZqN.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BmJh9gMQ.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BWjaX3ZF.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CLEOAxLB.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
- package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CydnyQUX.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-tjJ1IoZ1.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DR358Phd.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-B-LuWFZD.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-FG53fA6v.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bkt-w8SZ.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-WxHo0z2I.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CqHrBJhp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-D4MVae_f.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CamBh0Ll.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C3qheCuW.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DQlWfkS7.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BAxa9rFb.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-YM09nLJw.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-I4W9EJj2.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DH8eJ8Ff.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D0D0ntDW.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BIISKq1s.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3--xyAWj0w.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B3sYXLJ4.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-zRSYWhJP.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BUYlie03.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DP4kzzrM.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CAPoUxTF.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
- package/dist/worker/console/static/assets/{index-CWhTyxvc.js → index-BmMi-Bve.js} +71 -71
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXPfjekV.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-aOI3pV6t.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DAPZuvi4.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-atwDzL3C.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D5E09qji.js → linear-PTmQ9LkV.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CE4idY-s.js → mermaid.core-B6Lxil0V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CbZxtPca.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-De-FYEte.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bs0USobz.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bq3ZrGCJ.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bk8ojyIj.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CGhoyws1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DmVSQs3b.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C80h1u0Q.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CcbksQVZ.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CFTTTQKj.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Cw7Jr7Tg.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CVcdTVa5.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BpLbmWp9.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-6PXfbwj-.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Bjkcz4ih.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +100 -15
- package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
- package/dist/workflows/dag/frontend-shape.js +16 -6
- package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/frontend-verification-trace.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +16 -13
- package/dist/workflows/dag/node-execution.js +178 -88
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +90 -4
- package/dist/workflows/dag/rerun-run.js +7 -0
- package/dist/workflows/dag/retry-policy.js +16 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +67 -7
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/package.json +2 -3
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +84 -10
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DOsaUk1k.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C4YCesmi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-lN2U_DLl.js +0 -8
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
|
|
5
|
+
import { workspaceDriftedPaths, } from "./workspace-checkpoint.js";
|
|
5
6
|
import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
|
|
6
7
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
7
8
|
import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
|
|
@@ -1110,6 +1111,46 @@ function deriveSuggestedAction(input) {
|
|
|
1110
1111
|
}
|
|
1111
1112
|
return "manual";
|
|
1112
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Gitignore-style-lite matcher for readSet/writeSet path patterns in scoped
|
|
1116
|
+
* workspace-drift evaluation: literal paths match exactly; `**` spans path
|
|
1117
|
+
* segments (a trailing `/**` also matches when nothing follows), `*` stays
|
|
1118
|
+
* within one segment, `?` is one non-separator char.
|
|
1119
|
+
*/
|
|
1120
|
+
function pathMatchesWorkspacePattern(pathValue, pattern) {
|
|
1121
|
+
const normalizedPath = pathValue.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1122
|
+
const trimmed = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1123
|
+
if (!trimmed || trimmed === "/")
|
|
1124
|
+
return false;
|
|
1125
|
+
if (trimmed === normalizedPath)
|
|
1126
|
+
return true;
|
|
1127
|
+
let source = "";
|
|
1128
|
+
for (let index = 0; index < trimmed.length; index += 1) {
|
|
1129
|
+
const char = trimmed[index];
|
|
1130
|
+
if (char === "*") {
|
|
1131
|
+
if (trimmed[index + 1] === "*") {
|
|
1132
|
+
if (trimmed[index + 2] === "/") {
|
|
1133
|
+
source += "(?:.*/)?";
|
|
1134
|
+
index += 2;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
source += ".*";
|
|
1138
|
+
index += 1;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
source += "[^/]*";
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
else if (char === "?") {
|
|
1146
|
+
source += "[^/]";
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return new RegExp(`^${source}$`).test(normalizedPath);
|
|
1153
|
+
}
|
|
1113
1154
|
function deriveRisk(reasonCodes, resetCount) {
|
|
1114
1155
|
if (reasonCodes.includes("restart-subgraph-contains-writer") ||
|
|
1115
1156
|
reasonCodes.includes("restart-subgraph-contains-unsafe-shell") ||
|
|
@@ -1213,7 +1254,14 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
1213
1254
|
blockedReasons.push("parent-lifecycle-ineligible");
|
|
1214
1255
|
}
|
|
1215
1256
|
else if (input.parentState.status !== "failed" &&
|
|
1216
|
-
input.parentState.status !== "partial_failed"
|
|
1257
|
+
input.parentState.status !== "partial_failed" &&
|
|
1258
|
+
// A superseded parent is a terminal operator judgment about the run as
|
|
1259
|
+
// a whole, not about its deterministic read-only compute: its FINISHED
|
|
1260
|
+
// upstream nodes are still valid fact donors for the imported set, and
|
|
1261
|
+
// every unsafe case (writers in the reset subgraph, decision gates,
|
|
1262
|
+
// stale checkpoints, unresolved ERROR nodes outside the closure) keeps
|
|
1263
|
+
// its own dedicated block below.
|
|
1264
|
+
input.parentState.status !== "superseded") {
|
|
1217
1265
|
reasonCodes.push("parent-lifecycle-ineligible");
|
|
1218
1266
|
blockedReasons.push("parent-lifecycle-ineligible");
|
|
1219
1267
|
}
|
|
@@ -1241,8 +1289,36 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
1241
1289
|
else if (input.currentWorkspace?.fingerprint &&
|
|
1242
1290
|
input.parentTerminalWorkspace.fingerprint !==
|
|
1243
1291
|
input.currentWorkspace.fingerprint) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1292
|
+
// Whole-fingerprint drift is refined to content-level paths: the rerun
|
|
1293
|
+
// re-derives (readSet) and rewrites (writeSet) the reset subgraph's own
|
|
1294
|
+
// paths, so drift confined to those paths is recomputed by the rerun
|
|
1295
|
+
// itself and must not force a full standalone rerun. Drift anywhere
|
|
1296
|
+
// else — especially inputs of imported fact-reuse nodes — keeps the
|
|
1297
|
+
// strict block. Indeterminate checkpoints fail closed.
|
|
1298
|
+
const driftedPaths = workspaceDriftedPaths(input.parentTerminalWorkspace, input.currentWorkspace);
|
|
1299
|
+
const importedReadPaths = importedNodeIds.flatMap((nodeId) => {
|
|
1300
|
+
const task = tasksByIdForPlan.get(nodeId);
|
|
1301
|
+
return task?.readSet ?? [];
|
|
1302
|
+
});
|
|
1303
|
+
const scopedDrift = driftedPaths !== undefined &&
|
|
1304
|
+
driftedPaths.length > 0 &&
|
|
1305
|
+
driftedPaths.every((driftPath) => !importedReadPaths.some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern)) &&
|
|
1306
|
+
resetNodeIds.some((nodeId) => {
|
|
1307
|
+
const task = tasksByIdForPlan.get(nodeId);
|
|
1308
|
+
if (!task)
|
|
1309
|
+
return false;
|
|
1310
|
+
return [
|
|
1311
|
+
...(task.writeSet ?? []),
|
|
1312
|
+
...(task.readSet ?? []),
|
|
1313
|
+
].some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern));
|
|
1314
|
+
}));
|
|
1315
|
+
if (scopedDrift) {
|
|
1316
|
+
reasonCodes.push("workspace-drift-scoped");
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
reasonCodes.push("workspace-drift");
|
|
1320
|
+
blockedReasons.push("workspace-drift");
|
|
1321
|
+
}
|
|
1246
1322
|
}
|
|
1247
1323
|
else if (!input.currentWorkspace?.fingerprint) {
|
|
1248
1324
|
reasonCodes.push("workspace-checkpoint-missing");
|
|
@@ -1293,8 +1369,18 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
1293
1369
|
}
|
|
1294
1370
|
for (const nodeId of importedNodeIds) {
|
|
1295
1371
|
const record = input.parentState.nodes[nodeId];
|
|
1296
|
-
if (!record)
|
|
1372
|
+
if (!record) {
|
|
1373
|
+
reasonCodes.push("parent-facts-invalid");
|
|
1374
|
+
blockedReasons.push("parent-facts-invalid");
|
|
1375
|
+
blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
|
|
1297
1376
|
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (record.status === "PENDING" || record.status === "RUNNING") {
|
|
1379
|
+
reasonCodes.push("parent-facts-invalid");
|
|
1380
|
+
blockedReasons.push("parent-facts-invalid");
|
|
1381
|
+
blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1298
1384
|
if (record.status === "ERROR") {
|
|
1299
1385
|
reasonCodes.push("parent-facts-invalid");
|
|
1300
1386
|
blockedReasons.push("parent-facts-invalid");
|
|
@@ -784,6 +784,13 @@ async function stageContinuationRun(input) {
|
|
|
784
784
|
if (!parentRecord) {
|
|
785
785
|
throw new Error(`parent node record missing for imported node: ${nodeId}`);
|
|
786
786
|
}
|
|
787
|
+
if (parentRecord.status !== "FINISHED" &&
|
|
788
|
+
!(parentRecord.status === "SKIPPED" &&
|
|
789
|
+
(parentRecord.skippedReason?.includes("condition") ||
|
|
790
|
+
parentRecord.skippedReason?.includes("runIf") ||
|
|
791
|
+
parentRecord.skippedReason?.includes("run-if")))) {
|
|
792
|
+
throw new Error(`parent node is not a settled fact donor for imported node: ${nodeId}`);
|
|
793
|
+
}
|
|
787
794
|
const { manifestNode, importedRecord } = await importNodeFacts({
|
|
788
795
|
parentRunDir: input.parentRunDir,
|
|
789
796
|
newRunDir: runDir,
|
|
@@ -7,8 +7,8 @@ import { TYPED_EVENT_FACT_KINDS } from "./frontend-typed-event-store.js";
|
|
|
7
7
|
* is re-exported here unchanged so retry-policy stays the single import
|
|
8
8
|
* surface for retry policies and helpers.
|
|
9
9
|
*/
|
|
10
|
-
export { ALL_DAG_RETRY_CATEGORIES, CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, PROTOCOL_INVALID_RETRY_CATEGORY, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, dagRetryBackoffSchema, dagRetryCategorySchema, dagRetryPolicySchema, } from "./dag-retry-schema.js";
|
|
11
|
-
import { CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./dag-retry-schema.js";
|
|
10
|
+
export { ALL_DAG_RETRY_CATEGORIES, CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, PROTOCOL_INVALID_RETRY_CATEGORY, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, dagRetryBackoffSchema, dagRetryCategorySchema, dagRetryPolicySchema, } from "./dag-retry-schema.js";
|
|
11
|
+
import { CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./dag-retry-schema.js";
|
|
12
12
|
const RETRY_SAFE_PI_ROLES = new Set([
|
|
13
13
|
"planner",
|
|
14
14
|
"scout",
|
|
@@ -19,7 +19,7 @@ const RETRY_SAFE_PI_ROLES = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
/**
|
|
21
21
|
* The default retry policy applied to safe generated read-only Pi nodes.
|
|
22
|
-
* Total attempts:
|
|
22
|
+
* Total attempts: 5, exponential backoff with cap.
|
|
23
23
|
*
|
|
24
24
|
* Includes `context-overflow`: a read-only node that blew the context window
|
|
25
25
|
* (400 request too large, e.g. a review node accumulating too many reads) is a
|
|
@@ -29,7 +29,7 @@ const RETRY_SAFE_PI_ROLES = new Set([
|
|
|
29
29
|
* design-review/review fell straight to ERROR without a retry attempt.
|
|
30
30
|
*/
|
|
31
31
|
export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
|
|
32
|
-
maxAttempts:
|
|
32
|
+
maxAttempts: 5,
|
|
33
33
|
backoff: "exponential",
|
|
34
34
|
initialDelayMs: 2000,
|
|
35
35
|
maxDelayMs: 30000,
|
|
@@ -125,7 +125,10 @@ export const WRITER_EMPTY_DIFF_RETRY_POLICY = {
|
|
|
125
125
|
backoff: "exponential",
|
|
126
126
|
initialDelayMs: 2000,
|
|
127
127
|
maxDelayMs: 30000,
|
|
128
|
-
retryCategories: [
|
|
128
|
+
retryCategories: [
|
|
129
|
+
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
130
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
131
|
+
],
|
|
129
132
|
};
|
|
130
133
|
/**
|
|
131
134
|
* Bounded transport retry for standard exclusive implementers when a provider
|
|
@@ -140,6 +143,7 @@ export const WRITER_TRANSPORT_RETRY_POLICY = {
|
|
|
140
143
|
retryCategories: [
|
|
141
144
|
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
142
145
|
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
146
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
143
147
|
],
|
|
144
148
|
};
|
|
145
149
|
/**
|
|
@@ -175,6 +179,7 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
|
|
|
175
179
|
retryCategories: [
|
|
176
180
|
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
177
181
|
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
182
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
178
183
|
],
|
|
179
184
|
};
|
|
180
185
|
/**
|
|
@@ -182,13 +187,14 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
|
|
|
182
187
|
* recovery attempts driven by Completeness Gate (missing/broken target files).
|
|
183
188
|
*/
|
|
184
189
|
export const BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY = {
|
|
185
|
-
maxAttempts:
|
|
190
|
+
maxAttempts: 5,
|
|
186
191
|
backoff: "exponential",
|
|
187
192
|
initialDelayMs: 2000,
|
|
188
193
|
maxDelayMs: 30000,
|
|
189
194
|
retryCategories: [
|
|
190
195
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
191
196
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
197
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
192
198
|
],
|
|
193
199
|
};
|
|
194
200
|
/** Markdown shard writers use one full attempt plus at most one bounded binding repair. */
|
|
@@ -472,9 +478,9 @@ function reasonMandatedFrontendPlanRetryStep(reason) {
|
|
|
472
478
|
}
|
|
473
479
|
}
|
|
474
480
|
/**
|
|
475
|
-
* Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=
|
|
476
|
-
*
|
|
477
|
-
*
|
|
481
|
+
* Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=5
|
|
482
|
+
* lets the producer validate and repair each generated increment while still
|
|
483
|
+
* bounding the four-rung ladder (normal → bounded-tool-only →
|
|
478
484
|
* compact-terminal-first → backup-model). `invalid-output` (typed-fact schema
|
|
479
485
|
* violations, e.g. verification targets referencing undeclared UI states)
|
|
480
486
|
* retries at the `normal` rung with the contractCheck reason injected via
|
|
@@ -485,7 +491,7 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
|
|
|
485
491
|
// incremental record_* calls; dense call bursts hit provider rate limits
|
|
486
492
|
// whose windows exceed the old 30s cap. Longer backoff gives the limit
|
|
487
493
|
// window time to expire before the next attempt.
|
|
488
|
-
maxAttempts:
|
|
494
|
+
maxAttempts: 5,
|
|
489
495
|
backoff: "exponential",
|
|
490
496
|
initialDelayMs: 5000,
|
|
491
497
|
maxDelayMs: 60000,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Runner exit-time diagnostics (silent-death forensics).
|
|
6
|
+
*
|
|
7
|
+
* A DAG runner can disappear without a crash report and without stderr (five
|
|
8
|
+
* reproductions during frontend recovery/long-plan runs, 2026-09-06): when a
|
|
9
|
+
* supervisor hard-kills the process or the event loop is wedged, no JS runs,
|
|
10
|
+
* so nothing is persisted and the run is only discoverable later as an
|
|
11
|
+
* orphaned RUNNING state with a frozen heartbeat. That leaves no evidence of
|
|
12
|
+
* WHERE the runner was when it died or whether the exit was even observable
|
|
13
|
+
* by Node.
|
|
14
|
+
*
|
|
15
|
+
* This module gives every abnormal death a durable trace next to state.json:
|
|
16
|
+
* - an `armed` line is written when the checkpoint starts executing;
|
|
17
|
+
* - a `process-exit` line is appended from the process `exit` hook with the
|
|
18
|
+
* exit code and a live state snapshot (status, terminalReason, RUNNING
|
|
19
|
+
* nodes, heartbeat staleness);
|
|
20
|
+
* - `uncaught-exception` / `unhandled-rejection` lines record the error
|
|
21
|
+
* before the process is allowed to crash exactly as it would have
|
|
22
|
+
* via uncaughtExceptionMonitor without changing the host's crash policy.
|
|
23
|
+
*
|
|
24
|
+
* On a clean terminal return the runner calls stop(), which removes every
|
|
25
|
+
* listener and deletes the journal, so completed runs carry no noise. A file
|
|
26
|
+
* left behind therefore means: diagnostics were armed, then the process went
|
|
27
|
+
* away through a path Node could not observe (`armed` line only → external
|
|
28
|
+
* SIGKILL/OOM/event-loop wedge) or through an observed abnormal exit (`armed`
|
|
29
|
+
* + one or more event lines → the reason is recorded in the file).
|
|
30
|
+
*
|
|
31
|
+
* Journal writes are strictly synchronous and best-effort: an append failure
|
|
32
|
+
* must never alter the runner's own crash/exit behavior.
|
|
33
|
+
*/
|
|
34
|
+
export const RUNNER_EXIT_DIAGNOSTICS_FILE = "runner-exit-diagnostics.jsonl";
|
|
35
|
+
function describeError(error) {
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
return { name: error.name, message: error.message };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return { message: String(error) };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { message: "<unstringifiable error>" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function appendJournalLineSync(journalPath, record) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
49
|
+
appendFileSync(journalPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Best-effort: journaling must never crash or alter exit behavior.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function removeJournalSync(journalPath) {
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(journalPath);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Tolerate a missing journal (e.g. nothing was ever written).
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function installRunnerExitDiagnostics(options) {
|
|
64
|
+
const journalPath = path.join(options.runDir, RUNNER_EXIT_DIAGNOSTICS_FILE);
|
|
65
|
+
const target = options.target ?? process;
|
|
66
|
+
const armedAt = Date.now();
|
|
67
|
+
let stopped = false;
|
|
68
|
+
const base = {
|
|
69
|
+
runId: options.runId,
|
|
70
|
+
title: options.title,
|
|
71
|
+
pid: process.pid,
|
|
72
|
+
hostname: hostname(),
|
|
73
|
+
};
|
|
74
|
+
const safeSnapshot = () => {
|
|
75
|
+
try {
|
|
76
|
+
return options.snapshot();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { status: "<snapshot-error>", running: [] };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const record = (kind, extra) => ({
|
|
83
|
+
ts: new Date().toISOString(),
|
|
84
|
+
kind,
|
|
85
|
+
...base,
|
|
86
|
+
...(extra ?? {}),
|
|
87
|
+
state: safeSnapshot(),
|
|
88
|
+
});
|
|
89
|
+
appendJournalLineSync(journalPath, {
|
|
90
|
+
...record("armed"),
|
|
91
|
+
armedAt: new Date(armedAt).toISOString(),
|
|
92
|
+
});
|
|
93
|
+
const onProcessExit = (code) => {
|
|
94
|
+
if (stopped)
|
|
95
|
+
return;
|
|
96
|
+
appendJournalLineSync(journalPath, record("process-exit", {
|
|
97
|
+
code: typeof code === "number" ? code : process.exitCode ?? 0,
|
|
98
|
+
elapsedMs: Date.now() - armedAt,
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
const onUncaughtException = (error, origin) => {
|
|
102
|
+
if (stopped)
|
|
103
|
+
return;
|
|
104
|
+
appendJournalLineSync(journalPath, record(origin === "unhandledRejection" ? "unhandled-rejection" : "uncaught-exception", {
|
|
105
|
+
error: describeError(error),
|
|
106
|
+
elapsedMs: Date.now() - armedAt,
|
|
107
|
+
}));
|
|
108
|
+
// Monitor listeners neither suppress fatal errors nor force a crash
|
|
109
|
+
// when the host has its own handlers or a nonfatal rejection policy.
|
|
110
|
+
};
|
|
111
|
+
target.on("exit", onProcessExit);
|
|
112
|
+
target.on("uncaughtExceptionMonitor", onUncaughtException);
|
|
113
|
+
return {
|
|
114
|
+
journalPath,
|
|
115
|
+
stop: () => {
|
|
116
|
+
if (stopped)
|
|
117
|
+
return;
|
|
118
|
+
stopped = true;
|
|
119
|
+
target.removeListener("exit", onProcessExit);
|
|
120
|
+
target.removeListener("uncaughtExceptionMonitor", onUncaughtException);
|
|
121
|
+
// Clean completion: leave completed runs free of diagnostics noise.
|
|
122
|
+
removeJournalSync(journalPath);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -32,11 +32,13 @@ import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFro
|
|
|
32
32
|
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
33
33
|
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
34
34
|
import { computeActionPayloadHash, } from "../../infrastructure/console/operation-store.js";
|
|
35
|
+
import { classifyFailureOwner, failureOwnerSchema } from "./frontend-recovery-capsule.js";
|
|
35
36
|
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, claimDagRecoveryLease, } from "./recovery-lease.js";
|
|
36
37
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
37
38
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
38
39
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
39
40
|
import { executeDynamicReduction } from "./dynamic-runtime/reduction.js";
|
|
41
|
+
import { installRunnerExitDiagnostics } from "./runner-exit-diagnostics.js";
|
|
40
42
|
export { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions };
|
|
41
43
|
export async function loadDagSpecFromFile(dagPath) {
|
|
42
44
|
const raw = JSON.parse(await readFile(dagPath, "utf-8"));
|
|
@@ -602,6 +604,36 @@ async function executeDagCheckpoint(input) {
|
|
|
602
604
|
const onSigInt = () => shutdownOnSignal("SIGINT");
|
|
603
605
|
process.once("SIGTERM", onSigTerm);
|
|
604
606
|
process.once("SIGINT", onSigInt);
|
|
607
|
+
// Exit-time diagnostics (silent-death forensics): write an `armed` line at
|
|
608
|
+
// checkpoint start and append process-exit / uncaught-exception /
|
|
609
|
+
// unhandled-rejection lines with a live state snapshot so an abnormal death
|
|
610
|
+
// (supervisor hard kill, wedged event loop, crash with lost stderr) leaves
|
|
611
|
+
// a durable trace next to state.json. Removed on clean terminal return.
|
|
612
|
+
const exitDiagnostics = installRunnerExitDiagnostics({
|
|
613
|
+
runDir,
|
|
614
|
+
runId: state.runId,
|
|
615
|
+
title: state.title,
|
|
616
|
+
snapshot: () => {
|
|
617
|
+
const heartbeatAt = state.runner?.heartbeatAt;
|
|
618
|
+
return {
|
|
619
|
+
status: state.status,
|
|
620
|
+
terminalReason: state.terminalReason,
|
|
621
|
+
failureCategory: state.failureCategory,
|
|
622
|
+
heartbeatAt,
|
|
623
|
+
heartbeatAgeMs: heartbeatAt === undefined
|
|
624
|
+
? undefined
|
|
625
|
+
: Math.max(0, Date.now() - Date.parse(heartbeatAt)),
|
|
626
|
+
running: Object.values(state.nodes)
|
|
627
|
+
.filter((node) => node.status === "RUNNING")
|
|
628
|
+
.map((node) => ({
|
|
629
|
+
id: node.id,
|
|
630
|
+
attempt: node.currentAttempt,
|
|
631
|
+
startedAt: node.startedAt,
|
|
632
|
+
lastMeaningfulProgressAt: node.lastMeaningfulProgressAt,
|
|
633
|
+
})),
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
});
|
|
605
637
|
try {
|
|
606
638
|
const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
|
|
607
639
|
const baseExecuteNode = input.executeNode ??
|
|
@@ -938,6 +970,7 @@ async function executeDagCheckpoint(input) {
|
|
|
938
970
|
finally {
|
|
939
971
|
clearInterval(heartbeatTimer);
|
|
940
972
|
interruptWatcher?.stop();
|
|
973
|
+
exitDiagnostics.stop();
|
|
941
974
|
process.removeListener("SIGTERM", onSigTerm);
|
|
942
975
|
process.removeListener("SIGINT", onSigInt);
|
|
943
976
|
}
|
|
@@ -1296,13 +1329,40 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
|
|
|
1296
1329
|
if (verifyFailed) {
|
|
1297
1330
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1298
1331
|
if (continuationCount < frontendRecoveryQuota) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1332
|
+
let recoveryOwner = classifyFailureOwner({
|
|
1333
|
+
nodeId: "frontend-verify-shell",
|
|
1334
|
+
rawFailureCategory: state.nodes["frontend-verify-shell"]?.failureCategory,
|
|
1335
|
+
});
|
|
1336
|
+
try {
|
|
1337
|
+
const verifyFailure = JSON.parse(await readFile(path.join(runDir, "contracts/frontend-verify-failure.json"), "utf8"));
|
|
1338
|
+
const owner = failureOwnerSchema.safeParse(verifyFailure.failureOwner);
|
|
1339
|
+
if (verifyFailure.schemaVersion === 1 &&
|
|
1340
|
+
verifyFailure.schemaId === "frontend-verify-failure-v1" &&
|
|
1341
|
+
verifyFailure.nodeId === "frontend-verify-shell" &&
|
|
1342
|
+
verifyFailure.failureCategory === state.nodes["frontend-verify-shell"]?.failureCategory &&
|
|
1343
|
+
owner.success) {
|
|
1344
|
+
recoveryOwner = owner.data;
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
recoveryOwner = "unknown";
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
// The node's classified category remains the fallback; never turn
|
|
1352
|
+
// unknown/environment/configuration failures into writer work.
|
|
1353
|
+
}
|
|
1354
|
+
if (recoveryOwner === "implementation")
|
|
1355
|
+
recoveryTrigger = {
|
|
1356
|
+
requestId: state.runId,
|
|
1357
|
+
failureSource: "frontend-verify-shell",
|
|
1358
|
+
failureOwner: recoveryOwner,
|
|
1359
|
+
protocolFailureReason: "frontend verification failure",
|
|
1360
|
+
recoveryMode: "continuation",
|
|
1361
|
+
};
|
|
1362
|
+
else
|
|
1363
|
+
state.terminalReason = recoveryOwner === "environment"
|
|
1364
|
+
? "frontend-verification-wait-external"
|
|
1365
|
+
: `frontend-verification-${recoveryOwner}-requires-contract-review`;
|
|
1306
1366
|
}
|
|
1307
1367
|
}
|
|
1308
1368
|
}
|
|
@@ -112,7 +112,10 @@ export function isStructuredRepairableFailureCategory(category) {
|
|
|
112
112
|
return (category === "invalid-output" ||
|
|
113
113
|
category === "structured-output-truncated" ||
|
|
114
114
|
category === "output-too-large" ||
|
|
115
|
-
category === "empty-output"
|
|
115
|
+
category === "empty-output" ||
|
|
116
|
+
// A length-truncated structured turn with a non-empty candidate artifact
|
|
117
|
+
// still repairs best through the frozen artifact repair prompt.
|
|
118
|
+
category === "output-limit");
|
|
116
119
|
}
|
|
117
120
|
export function formatStructuredArtifactPointer(input) {
|
|
118
121
|
return [
|
|
@@ -3,7 +3,7 @@ import { resolveShellCommands } from "../../executors/shell-executor.js";
|
|
|
3
3
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
4
4
|
import { resolveRepairTaskForGate } from "./repair-artifact.js";
|
|
5
5
|
import { topoSortToRanks } from "./topo.js";
|
|
6
|
-
import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, CONTEXT_OVERFLOW_RETRY_CATEGORY, } from "./retry-policy.js";
|
|
6
|
+
import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, CONTEXT_OVERFLOW_RETRY_CATEGORY, } from "./retry-policy.js";
|
|
7
7
|
import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
|
|
8
8
|
import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
|
|
9
9
|
const GOVERNANCE_WARNING_TYPES = new Set([
|
|
@@ -630,27 +630,28 @@ function validateRetryPolicyTaskConfig(task, issues) {
|
|
|
630
630
|
const allowed = new Set([
|
|
631
631
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
632
632
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
633
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
633
634
|
]);
|
|
634
635
|
const unsupported = task.retryPolicy.retryCategories.filter((category) => !allowed.has(category));
|
|
635
636
|
if (task.retryPolicy.maxAttempts < 2 ||
|
|
636
|
-
task.retryPolicy.maxAttempts >
|
|
637
|
+
task.retryPolicy.maxAttempts > 5 ||
|
|
637
638
|
unsupported.length > 0 ||
|
|
638
639
|
!categories.has(WRITER_EMPTY_DIFF_RETRY_CATEGORY)) {
|
|
639
640
|
issues.push({
|
|
640
641
|
type: "invalid-retry-policy",
|
|
641
|
-
message: `task ${task.id} backend-test writer retryPolicy must use 2..
|
|
642
|
+
message: `task ${task.id} backend-test writer retryPolicy must use 2..5 total attempts and only ${WRITER_EMPTY_DIFF_RETRY_CATEGORY}, ${INCOMPLETE_WRITE_SET_RETRY_CATEGORY}, and/or ${OUTPUT_LIMIT_RETRY_CATEGORY}`,
|
|
642
643
|
});
|
|
643
644
|
}
|
|
644
645
|
return;
|
|
645
646
|
}
|
|
646
647
|
if (isWriterEmptyDiffCandidate &&
|
|
647
648
|
(task.retryPolicy.maxAttempts !== 2 ||
|
|
648
|
-
task.retryPolicy.retryCategories.
|
|
649
|
-
task.retryPolicy.retryCategories
|
|
650
|
-
|
|
649
|
+
!task.retryPolicy.retryCategories.includes(WRITER_EMPTY_DIFF_RETRY_CATEGORY) ||
|
|
650
|
+
task.retryPolicy.retryCategories.some((category) => category !== WRITER_EMPTY_DIFF_RETRY_CATEGORY &&
|
|
651
|
+
category !== OUTPUT_LIMIT_RETRY_CATEGORY))) {
|
|
651
652
|
issues.push({
|
|
652
653
|
type: "invalid-retry-policy",
|
|
653
|
-
message: `task ${task.id} writer retryPolicy must use exactly two total attempts and
|
|
654
|
+
message: `task ${task.id} writer retryPolicy must use exactly two total attempts and ${WRITER_EMPTY_DIFF_RETRY_CATEGORY}, with optional ${OUTPUT_LIMIT_RETRY_CATEGORY}`,
|
|
654
655
|
});
|
|
655
656
|
return;
|
|
656
657
|
}
|
|
@@ -659,6 +660,7 @@ function validateRetryPolicyTaskConfig(task, issues) {
|
|
|
659
660
|
const allowedWriterTransportCategories = new Set([
|
|
660
661
|
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
661
662
|
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
663
|
+
OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
662
664
|
]);
|
|
663
665
|
const allAllowed = [...categories].every((category) => allowedWriterTransportCategories.has(category));
|
|
664
666
|
if (task.retryPolicy.maxAttempts !== 2 ||
|
|
@@ -667,7 +669,7 @@ function validateRetryPolicyTaskConfig(task, issues) {
|
|
|
667
669
|
!categories.has(WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY)) {
|
|
668
670
|
issues.push({
|
|
669
671
|
type: "invalid-retry-policy",
|
|
670
|
-
message: `task ${task.id} writer transport retryPolicy must use exactly two total attempts and only writer-clean-timeout (plus optional context-overflow)`,
|
|
672
|
+
message: `task ${task.id} writer transport retryPolicy must use exactly two total attempts and only writer-clean-timeout (plus optional context-overflow/output-limit)`,
|
|
671
673
|
});
|
|
672
674
|
}
|
|
673
675
|
}
|
|
@@ -184,3 +184,69 @@ export function workspaceFingerprintsMatch(left, right) {
|
|
|
184
184
|
return false;
|
|
185
185
|
return left.fingerprint === right.fingerprint;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Path-level drift between two checkpoints: paths whose content hash,
|
|
189
|
+
* porcelain state, or presence differs (changed paths plus stable Task
|
|
190
|
+
* Contract governance inputs). The whole-tree fingerprint also moves on HEAD
|
|
191
|
+
* commits or porcelain churn that leaves every path's content untouched;
|
|
192
|
+
* only content-level path diffs are actionable for scoped rerun eligibility.
|
|
193
|
+
* Returns undefined when either checkpoint lacks a path inventory so callers
|
|
194
|
+
* fail closed to the strict whole-fingerprint comparison.
|
|
195
|
+
*/
|
|
196
|
+
export function workspaceDriftedPaths(parent, current) {
|
|
197
|
+
// changedPaths is only the porcelain inventory; it cannot explain files
|
|
198
|
+
// already committed between two different HEADs. Never localize that drift.
|
|
199
|
+
if (!parent.gitHeadSha ||
|
|
200
|
+
!current.gitHeadSha ||
|
|
201
|
+
parent.gitHeadSha !== current.gitHeadSha) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
if (!Array.isArray(parent.changedPaths) || !Array.isArray(current.changedPaths)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
// Porcelain v1 records only the rename destination in this checkpoint
|
|
208
|
+
// schema, so the removed source path cannot be proven safe for imported
|
|
209
|
+
// readers. Keep scoped reuse fail-closed until both sides expose complete
|
|
210
|
+
// rename lineage.
|
|
211
|
+
if (parent.changedPaths.some((entry) => entry.state === "renamed") ||
|
|
212
|
+
current.changedPaths.some((entry) => entry.state === "renamed")) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if ((parent.stableGovernanceInputs === undefined) !==
|
|
216
|
+
(current.stableGovernanceInputs === undefined)) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const drifted = new Set();
|
|
220
|
+
const parentByPath = new Map(parent.changedPaths.map((entry) => [entry.path, entry]));
|
|
221
|
+
const currentByPath = new Map(current.changedPaths.map((entry) => [entry.path, entry]));
|
|
222
|
+
for (const [pathKey, parentEntry] of parentByPath) {
|
|
223
|
+
const currentEntry = currentByPath.get(pathKey);
|
|
224
|
+
if (!currentEntry) {
|
|
225
|
+
drifted.add(pathKey);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (parentEntry.contentSha256 !== currentEntry.contentSha256 ||
|
|
229
|
+
parentEntry.state !== currentEntry.state ||
|
|
230
|
+
parentEntry.mode !== currentEntry.mode) {
|
|
231
|
+
drifted.add(pathKey);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const pathKey of currentByPath.keys()) {
|
|
235
|
+
if (!parentByPath.has(pathKey))
|
|
236
|
+
drifted.add(pathKey);
|
|
237
|
+
}
|
|
238
|
+
if (parent.stableGovernanceInputs && current.stableGovernanceInputs) {
|
|
239
|
+
const parentGov = new Map(parent.stableGovernanceInputs.map((entry) => [entry.path, entry.contentSha256]));
|
|
240
|
+
const currentGov = new Map(current.stableGovernanceInputs.map((entry) => [entry.path, entry.contentSha256]));
|
|
241
|
+
for (const [pathKey, hash] of parentGov) {
|
|
242
|
+
const other = currentGov.get(pathKey);
|
|
243
|
+
if (other === undefined || other !== hash)
|
|
244
|
+
drifted.add(pathKey);
|
|
245
|
+
}
|
|
246
|
+
for (const [pathKey, hash] of currentGov) {
|
|
247
|
+
if (parentGov.get(pathKey) !== hash)
|
|
248
|
+
drifted.add(pathKey);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return [...drifted];
|
|
252
|
+
}
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
- [`production-readiness.md`](production-readiness.md):交付前的生产就绪判断。
|
|
12
12
|
- [`agent-worker-production-readiness.md`](agent-worker-production-readiness.md):agent-worker Feature/Task Pool 外层编排的 Production Readiness v1 操作合同。
|
|
13
13
|
- [`backend-test-jacoco-coverage.md`](backend-test-jacoco-coverage.md):Java 被测服务挂载 JaCoCo tcpserver agent 的 Maven、`java -jar` 与 Docker 操作手册。
|
|
14
|
+
- [`console-chat-dogfood-playbook.md`](console-chat-dogfood-playbook.md):Operator Chat(含 Explore 子 Agent)真实模型 dogfood 的 CLI 驱动步骤、mutation 双 token / SSE 超时 / question 应答等已知陷阱与验证方式。
|
|
14
15
|
|
|
15
16
|
验证命令仍以 [`../governance/verification-matrix.md`](../governance/verification-matrix.md) 为准。
|
|
@@ -466,9 +466,9 @@
|
|
|
466
466
|
"retryCategories": {
|
|
467
467
|
"type": "array",
|
|
468
468
|
"items": {
|
|
469
|
-
"enum": ["timeout", "network", "rate-limit", "unavailable", "empty-output", "output-too-large", "protocol-invalid", "invalid-output", "writer-empty-diff", "incomplete-write-set", "writer-clean-timeout", "read-burst", "context-overflow"]
|
|
469
|
+
"enum": ["timeout", "network", "rate-limit", "unavailable", "empty-output", "output-limit", "output-too-large", "protocol-invalid", "invalid-output", "writer-empty-diff", "incomplete-write-set", "writer-clean-timeout", "read-burst", "context-overflow"]
|
|
470
470
|
},
|
|
471
|
-
"default": ["timeout", "network", "rate-limit", "unavailable", "empty-output"],
|
|
471
|
+
"default": ["timeout", "network", "rate-limit", "unavailable", "empty-output", "output-limit"],
|
|
472
472
|
"description": "Failure categories eligible for retry. quota/auth/write-guard are never eligible. invalid-output is allowed only on explicit structured/protocol policies; writer-empty-diff and incomplete-write-set are reserved for requireChangedFiles generation/completeness writers; writer-clean-timeout is reserved for standard bounded writers after explicit zero-write-tool-call and empty-diff evidence."
|
|
473
473
|
}
|
|
474
474
|
}
|