@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { TYPED_EVENT_FACT_KINDS, stageTypedEventRecord, } from "./frontend-typed-event-store.js";
|
|
3
|
+
import { createDagEventObserver } from "./event-observer.js";
|
|
4
|
+
/**
|
|
5
|
+
* M1 shadow-only provider/model capability matrix (AC-003).
|
|
6
|
+
*
|
|
7
|
+
* Records seven provider/model capability facts as read-only observations:
|
|
8
|
+
* - `terminal-commit`
|
|
9
|
+
* - `disconnect`
|
|
10
|
+
* - `duplicate-tool-call`
|
|
11
|
+
* - `length-before-terminal`
|
|
12
|
+
* - `reasoning-timeout`
|
|
13
|
+
* - `provider-ended-without-terminal`
|
|
14
|
+
* - `read-only-fact-channel`
|
|
15
|
+
*
|
|
16
|
+
* This matrix is intentionally shadow-only: `recordProviderCapabilityFact`
|
|
17
|
+
* returns a record for audit, never a branch-control signal, and this module
|
|
18
|
+
* must never be imported into `node-execution.ts`'s model-selection / branch
|
|
19
|
+
* control path. It is deliberately distinct from `frontend-project-capability`
|
|
20
|
+
* (framework / mock discovery).
|
|
21
|
+
*
|
|
22
|
+
* The read-only fact channel composes the existing `createDagEventObserver`
|
|
23
|
+
* (which already swallows its own write errors) so that a shadow-channel
|
|
24
|
+
* failure can never affect canonical DAG execution.
|
|
25
|
+
*/
|
|
26
|
+
export const PROVIDER_CAPABILITY_FACT_KINDS = TYPED_EVENT_FACT_KINDS;
|
|
27
|
+
/** Terminal finish-reason shapes, modeled after
|
|
28
|
+
* `src/executors/pi-event-serializer.ts`'s private `TERMINAL_FINISH_REASON_TYPES`
|
|
29
|
+
* / `readExplicitFinishReason` (those symbols are not exported, so the matrix
|
|
30
|
+
* keeps its own local projection). */
|
|
31
|
+
const TERMINAL_FINISH_REASON_TYPES = new Set([
|
|
32
|
+
"message_end",
|
|
33
|
+
"turn_end",
|
|
34
|
+
"assistant_message",
|
|
35
|
+
]);
|
|
36
|
+
function readExplicitFinishReason(source) {
|
|
37
|
+
const message = isRecord(source.message) ? source.message : undefined;
|
|
38
|
+
const candidates = [
|
|
39
|
+
source.finish_reason,
|
|
40
|
+
source.finishReason,
|
|
41
|
+
source.stop_reason,
|
|
42
|
+
source.stopReason,
|
|
43
|
+
message?.finish_reason,
|
|
44
|
+
message?.finishReason,
|
|
45
|
+
message?.stop_reason,
|
|
46
|
+
message?.stopReason,
|
|
47
|
+
];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function isRecord(value) {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
/** True when an observed provider event is terminal-shaped. Shadow-only: the
|
|
59
|
+
* result is a fact, not a control decision. */
|
|
60
|
+
export function isTerminalProviderEventShape(event) {
|
|
61
|
+
const type = typeof event.type === "string" ? event.type : undefined;
|
|
62
|
+
return type !== undefined && TERMINAL_FINISH_REASON_TYPES.has(type);
|
|
63
|
+
}
|
|
64
|
+
/** Build a `terminal-commit` fact from a provider event, if the event carries a
|
|
65
|
+
* terminal shape or an explicit finish reason. Returns undefined when the event
|
|
66
|
+
* has no terminal evidence (the caller records nothing in that case). */
|
|
67
|
+
export function buildTerminalCommitFact(event) {
|
|
68
|
+
if (!isTerminalProviderEventShape(event)) {
|
|
69
|
+
const reason = readExplicitFinishReason(event);
|
|
70
|
+
if (reason === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
return { kind: "terminal-commit", finishReason: reason };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
kind: "terminal-commit",
|
|
76
|
+
finishReason: readExplicitFinishReason(event),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A+B (AC-005): route a requested fact kind through the frozen provider
|
|
81
|
+
* capability matrix. Kinds outside the seven-value matrix fail closed to
|
|
82
|
+
* `unsupported-provider-capability` so a plan retry can select a bounded
|
|
83
|
+
* tool-only / backup-model step instead of repeating an unsupported tool.
|
|
84
|
+
*/
|
|
85
|
+
export function routeFrontendProviderCapability(input) {
|
|
86
|
+
if (TYPED_EVENT_FACT_KINDS.includes(input.factKind)) {
|
|
87
|
+
return { ok: true, kind: input.factKind };
|
|
88
|
+
}
|
|
89
|
+
return { ok: false, reason: "unsupported-provider-capability" };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Append a capability fact as a staged read-only observation. Returns the
|
|
93
|
+
* record (for audit) and never a branch-control signal. Unknown fact kinds are
|
|
94
|
+
* rejected fail-closed so the matrix cannot silently widen its own catalog.
|
|
95
|
+
*/
|
|
96
|
+
export function recordProviderCapabilityFact(options) {
|
|
97
|
+
const kind = options.fact.kind;
|
|
98
|
+
if (!TYPED_EVENT_FACT_KINDS.includes(kind)) {
|
|
99
|
+
throw new Error(`unknown provider capability fact kind: ${String(kind)}`);
|
|
100
|
+
}
|
|
101
|
+
const eventId = options.eventId ?? randomUUID();
|
|
102
|
+
const record = stageTypedEventRecord(options.store, {
|
|
103
|
+
eventId,
|
|
104
|
+
requestId: options.requestId,
|
|
105
|
+
attemptId: options.attemptId,
|
|
106
|
+
fact: options.fact,
|
|
107
|
+
recordedAt: options.recordedAt,
|
|
108
|
+
});
|
|
109
|
+
return { eventId, record };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Attach a shadow observer that records `read-only-fact-channel` facts while
|
|
113
|
+
* delegating the canonical event JSONL to the existing `createDagEventObserver`.
|
|
114
|
+
* The shadow channel never throws into the caller: any recording failure is
|
|
115
|
+
* swallowed so canonical execution is unaffected.
|
|
116
|
+
*/
|
|
117
|
+
export function createProviderCapabilityShadowObserver(options) {
|
|
118
|
+
const base = createDagEventObserver({
|
|
119
|
+
eventsJsonlPath: options.eventsJsonlPath,
|
|
120
|
+
dagRunId: options.dagRunId,
|
|
121
|
+
spec: options.spec,
|
|
122
|
+
});
|
|
123
|
+
const recordReadOnlyFact = (eventType) => {
|
|
124
|
+
try {
|
|
125
|
+
recordProviderCapabilityFact({
|
|
126
|
+
store: options.store,
|
|
127
|
+
attemptId: options.attemptId,
|
|
128
|
+
requestId: options.requestId,
|
|
129
|
+
fact: { kind: "read-only-fact-channel", eventType },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Shadow channel must never affect canonical execution.
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const observer = {
|
|
137
|
+
onRunStart: async (state) => {
|
|
138
|
+
recordReadOnlyFact("dag.run.started");
|
|
139
|
+
await base.observer.onRunStart?.(state);
|
|
140
|
+
},
|
|
141
|
+
onNodeStart: async (nodeId, state) => {
|
|
142
|
+
recordReadOnlyFact("dag.node.started");
|
|
143
|
+
await base.observer.onNodeStart?.(nodeId, state);
|
|
144
|
+
},
|
|
145
|
+
onNodeOutput: async (nodeId, chunk, state) => {
|
|
146
|
+
recordReadOnlyFact("dag.node.output");
|
|
147
|
+
await base.observer.onNodeOutput?.(nodeId, chunk, state);
|
|
148
|
+
},
|
|
149
|
+
onNodeFinish: async (nodeId, state) => {
|
|
150
|
+
recordReadOnlyFact("dag.node.finished");
|
|
151
|
+
await base.observer.onNodeFinish?.(nodeId, state);
|
|
152
|
+
},
|
|
153
|
+
onRunFinish: async (state) => {
|
|
154
|
+
recordReadOnlyFact("dag.run.finished");
|
|
155
|
+
await base.observer.onRunFinish?.(state);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return { observer, flush: base.flush };
|
|
159
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
3
|
+
import { routeDagFailure, } from "./failure-routing.js";
|
|
4
|
+
import { computeNormalizedFailureFingerprint, } from "./frontend-recovery-lineage.js";
|
|
5
|
+
/**
|
|
6
|
+
* M6 failure capsule + deterministic ownership classifier + fact invalidation
|
|
7
|
+
* matrix + frozen command reuse validation (pure functions).
|
|
8
|
+
*
|
|
9
|
+
* AC-001 invariant: `restartPhase` is always the runtime classifier output.
|
|
10
|
+
* `modelSuggestedRestartPhase` is recorded separately and NEVER overrides it.
|
|
11
|
+
*
|
|
12
|
+
* AC-001/AC-002: canonical frontend recovery taxonomy. Operation state and the
|
|
13
|
+
* optional terminal are separate fields; legacy Console states
|
|
14
|
+
* (`queued|running|succeeded|failed|timed-out|needs-reconcile`), boolean
|
|
15
|
+
* terminal markers, and legacy owner/mode strings never appear in the capsule.
|
|
16
|
+
*
|
|
17
|
+
* All schemas are self-contained and `.strict()`: this module never extends
|
|
18
|
+
* `DagRunState`/`DagSpec` and only imports the existing recovery modules.
|
|
19
|
+
*/
|
|
20
|
+
const hash64 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
21
|
+
/* ------------------------------------------------------------------ *
|
|
22
|
+
* Restart phase + ownership. *
|
|
23
|
+
* ------------------------------------------------------------------ */
|
|
24
|
+
export const restartPhaseSchema = z.enum([
|
|
25
|
+
"contract",
|
|
26
|
+
"scout",
|
|
27
|
+
"plan",
|
|
28
|
+
"implement",
|
|
29
|
+
]);
|
|
30
|
+
export const RESTART_PHASE_ORDER = [
|
|
31
|
+
"contract",
|
|
32
|
+
"scout",
|
|
33
|
+
"plan",
|
|
34
|
+
"implement",
|
|
35
|
+
];
|
|
36
|
+
export const failureOwnerSchema = z.enum([
|
|
37
|
+
"contract",
|
|
38
|
+
"scout",
|
|
39
|
+
"plan",
|
|
40
|
+
"implementation",
|
|
41
|
+
"verification-config",
|
|
42
|
+
"environment",
|
|
43
|
+
"scope",
|
|
44
|
+
"unknown",
|
|
45
|
+
]);
|
|
46
|
+
export const recoveryModeSchema = z.enum([
|
|
47
|
+
"node-retry",
|
|
48
|
+
"continuation",
|
|
49
|
+
"new-dag",
|
|
50
|
+
"wait-external",
|
|
51
|
+
"human-decision",
|
|
52
|
+
"none",
|
|
53
|
+
]);
|
|
54
|
+
/* ------------------------------------------------------------------ *
|
|
55
|
+
* Canonical operation state / terminal / protocol failure. *
|
|
56
|
+
* ------------------------------------------------------------------ */
|
|
57
|
+
export const frontendOperationTerminalSchema = z.enum([
|
|
58
|
+
"success",
|
|
59
|
+
"non-converging",
|
|
60
|
+
"cancelled",
|
|
61
|
+
"superseded",
|
|
62
|
+
"failed-internal",
|
|
63
|
+
]);
|
|
64
|
+
export const frontendOperationStateSchema = z.enum([
|
|
65
|
+
"active",
|
|
66
|
+
"orchestrating-split",
|
|
67
|
+
"recoverable",
|
|
68
|
+
"blocked-human",
|
|
69
|
+
"blocked-external",
|
|
70
|
+
"success",
|
|
71
|
+
"non-converging",
|
|
72
|
+
"cancelled",
|
|
73
|
+
"superseded",
|
|
74
|
+
"failed-internal",
|
|
75
|
+
]);
|
|
76
|
+
const FRONTEND_OPERATION_TERMINAL_VALUES = frontendOperationTerminalSchema.options;
|
|
77
|
+
/** True when the canonical state is one of the five terminal values. */
|
|
78
|
+
export function isFrontendOperationTerminalState(state) {
|
|
79
|
+
return FRONTEND_OPERATION_TERMINAL_VALUES.includes(state);
|
|
80
|
+
}
|
|
81
|
+
export const frontendNodeProtocolFailureReasonSchema = z.enum([
|
|
82
|
+
"length-before-terminal",
|
|
83
|
+
"terminal-event-missing",
|
|
84
|
+
"reasoning-timeout",
|
|
85
|
+
"provider-ended-without-terminal",
|
|
86
|
+
"terminal-conflict",
|
|
87
|
+
"unsupported-tool-capability",
|
|
88
|
+
"typed-fact-invalid",
|
|
89
|
+
]);
|
|
90
|
+
const PROTOCOL_REASON_ALIASES = {
|
|
91
|
+
"frontend candidate contract invalid": "typed-fact-invalid",
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Project a free-form protocol failure string to the canonical 7-value enum.
|
|
95
|
+
* Known aliases map to their canonical value; the 7 enum values pass through;
|
|
96
|
+
* anything else (including `frontend writer transient partial write` and
|
|
97
|
+
* `unknown failure`) fails closed to `undefined`.
|
|
98
|
+
*/
|
|
99
|
+
export function projectProtocolFailureReason(reason) {
|
|
100
|
+
const alias = PROTOCOL_REASON_ALIASES[reason];
|
|
101
|
+
if (alias)
|
|
102
|
+
return alias;
|
|
103
|
+
const parsed = frontendNodeProtocolFailureReasonSchema.safeParse(reason);
|
|
104
|
+
return parsed.success ? parsed.data : undefined;
|
|
105
|
+
}
|
|
106
|
+
function ownerFromNodeId(nodeId) {
|
|
107
|
+
const id = nodeId.toLowerCase();
|
|
108
|
+
if (id.includes("contract"))
|
|
109
|
+
return "contract";
|
|
110
|
+
if (id.includes("scout"))
|
|
111
|
+
return "scout";
|
|
112
|
+
if (id.includes("plan"))
|
|
113
|
+
return "plan";
|
|
114
|
+
if (id.includes("implement"))
|
|
115
|
+
return "implementation";
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function ownerFromProductLine(productLine) {
|
|
119
|
+
if (!productLine)
|
|
120
|
+
return "unknown";
|
|
121
|
+
switch (productLine) {
|
|
122
|
+
case "SpecUnclear":
|
|
123
|
+
return "contract";
|
|
124
|
+
case "ContractMismatch":
|
|
125
|
+
return "plan";
|
|
126
|
+
case "ProductBug":
|
|
127
|
+
case "TestBug":
|
|
128
|
+
case "RiskyChange":
|
|
129
|
+
return "implementation";
|
|
130
|
+
case "EnvFailure":
|
|
131
|
+
case "FlakyTest":
|
|
132
|
+
case "DependencyFailure":
|
|
133
|
+
return "environment";
|
|
134
|
+
case "NeedsHuman":
|
|
135
|
+
case "Unknown":
|
|
136
|
+
return "unknown";
|
|
137
|
+
default: {
|
|
138
|
+
const exhaustive = productLine;
|
|
139
|
+
return exhaustive;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Deterministic ownership classifier. Priority order:
|
|
145
|
+
* 1. explicit failure source/nodeId phase substring;
|
|
146
|
+
* 2. verification-config signals (frozen command drift, stale verification
|
|
147
|
+
* plan, unresolved entrypoint);
|
|
148
|
+
* 3. environment signals (env/flaky/dependency/network/rate-limit/provider
|
|
149
|
+
* unavailable);
|
|
150
|
+
* 4. scope signals (path/write/forbidden/write-guard/writeSet violations);
|
|
151
|
+
* 5. product-line routing fallback;
|
|
152
|
+
* 6. fail-closed `unknown`.
|
|
153
|
+
*
|
|
154
|
+
* `verification-config`, `environment`, `scope`, and `unknown` never resolve to
|
|
155
|
+
* `implementation` (AC-003): writer recovery is never auto-selected for them.
|
|
156
|
+
*/
|
|
157
|
+
export function classifyFailureOwner(input) {
|
|
158
|
+
const source = (input.failureSource ?? input.nodeId ?? "").toLowerCase();
|
|
159
|
+
const fromSource = ownerFromNodeId(source);
|
|
160
|
+
if (fromSource)
|
|
161
|
+
return fromSource;
|
|
162
|
+
const raw = (input.rawFailureCategory ?? "").toLowerCase();
|
|
163
|
+
const nodeId = (input.nodeId ?? "").toLowerCase();
|
|
164
|
+
const normalized = input.normalizedFailureCategory ??
|
|
165
|
+
normalizeDagFailureCategory(input.rawFailureCategory);
|
|
166
|
+
// 2. verification-config signals.
|
|
167
|
+
if (raw.includes("verification-plan-stale") ||
|
|
168
|
+
raw.includes("verification-plan") ||
|
|
169
|
+
raw.includes("frozen") ||
|
|
170
|
+
raw.includes("drift") ||
|
|
171
|
+
raw.includes("entrypoint") ||
|
|
172
|
+
nodeId.includes("verification-config")) {
|
|
173
|
+
return "verification-config";
|
|
174
|
+
}
|
|
175
|
+
// 3. environment signals.
|
|
176
|
+
if (raw.includes("env-failure") ||
|
|
177
|
+
raw.includes("env failure") ||
|
|
178
|
+
raw.includes("flaky") ||
|
|
179
|
+
raw.includes("dependency") ||
|
|
180
|
+
raw.includes("network") ||
|
|
181
|
+
raw.includes("rate-limit") ||
|
|
182
|
+
raw.includes("rate_limit") ||
|
|
183
|
+
raw.includes("quota") ||
|
|
184
|
+
raw.includes("unavailable") ||
|
|
185
|
+
normalized === "executor") {
|
|
186
|
+
return "environment";
|
|
187
|
+
}
|
|
188
|
+
// 4. scope signals.
|
|
189
|
+
if (raw.includes("path") ||
|
|
190
|
+
raw.includes("forbidden") ||
|
|
191
|
+
raw.includes("write-guard") ||
|
|
192
|
+
raw.includes("write_guard") ||
|
|
193
|
+
raw.includes("writeset") ||
|
|
194
|
+
normalized === "write-guard") {
|
|
195
|
+
return "scope";
|
|
196
|
+
}
|
|
197
|
+
if (normalized === "success")
|
|
198
|
+
return "unknown";
|
|
199
|
+
const routing = routeDagFailure({
|
|
200
|
+
rawFailureCategory: input.rawFailureCategory,
|
|
201
|
+
normalizedFailureCategory: normalized,
|
|
202
|
+
nodeId: input.nodeId,
|
|
203
|
+
});
|
|
204
|
+
return ownerFromProductLine(routing.productLineFailureCategory);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Deterministic owner → restart phase mapping. `environment` and `unknown`
|
|
208
|
+
* have no restart phase (fail closed to `undefined`); `verification-config`
|
|
209
|
+
* and `scope` restart from contract (the only rebuild path — new-dag from
|
|
210
|
+
* contract, §6.1.2). The parameter accepts a legacy/unknown string so existing
|
|
211
|
+
* shell-executor call sites that pass legacy owner names still typecheck and
|
|
212
|
+
* fail closed to `undefined`.
|
|
213
|
+
*/
|
|
214
|
+
export function classifyRestartPhase(owner) {
|
|
215
|
+
switch (owner) {
|
|
216
|
+
case "contract":
|
|
217
|
+
return "contract";
|
|
218
|
+
case "scout":
|
|
219
|
+
return "scout";
|
|
220
|
+
case "plan":
|
|
221
|
+
return "plan";
|
|
222
|
+
case "implementation":
|
|
223
|
+
return "implement";
|
|
224
|
+
case "verification-config":
|
|
225
|
+
case "scope":
|
|
226
|
+
return "contract";
|
|
227
|
+
case "environment":
|
|
228
|
+
case "unknown":
|
|
229
|
+
return undefined;
|
|
230
|
+
default:
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A+B (AC-009): deterministic issueCategory → restart phase mapping (§4.10).
|
|
236
|
+
* `approved-design-defect` restarts at `plan` (design decisions live in the
|
|
237
|
+
* plan phase); `unknown` fails closed to `undefined` so the runtime classifier
|
|
238
|
+
* never trusts an untyped category.
|
|
239
|
+
*/
|
|
240
|
+
export function classifyIssueCategoryRestartPhase(issueCategory) {
|
|
241
|
+
switch (issueCategory) {
|
|
242
|
+
case "implementation-mismatch":
|
|
243
|
+
return "implement";
|
|
244
|
+
case "approved-design-defect":
|
|
245
|
+
return "plan";
|
|
246
|
+
case "target-surface-defect":
|
|
247
|
+
return "scout";
|
|
248
|
+
case "contract-requirement-gap":
|
|
249
|
+
return "contract";
|
|
250
|
+
case "unknown":
|
|
251
|
+
default:
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function factPhaseOf(factId) {
|
|
256
|
+
const id = factId.toLowerCase();
|
|
257
|
+
if (id.includes("contract"))
|
|
258
|
+
return "contract";
|
|
259
|
+
if (id.includes("scout"))
|
|
260
|
+
return "scout";
|
|
261
|
+
if (id.includes("plan"))
|
|
262
|
+
return "plan";
|
|
263
|
+
if (id.includes("implement"))
|
|
264
|
+
return "implement";
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Deterministic fact invalidation matrix. Restarting at a phase invalidates
|
|
269
|
+
* that phase and everything downstream, while upstream facts stay reusable.
|
|
270
|
+
* Unknown fact ids fail closed to `invalidated`. At the exact restart phase,
|
|
271
|
+
* a committed candidate survives (reusable); an uncommitted partial write does
|
|
272
|
+
* not.
|
|
273
|
+
*/
|
|
274
|
+
export function evaluateFactInvalidation(input) {
|
|
275
|
+
const factPhase = factPhaseOf(input.factId);
|
|
276
|
+
if (factPhase === undefined)
|
|
277
|
+
return "invalidated";
|
|
278
|
+
const factIndex = RESTART_PHASE_ORDER.indexOf(factPhase);
|
|
279
|
+
const restartIndex = RESTART_PHASE_ORDER.indexOf(input.restartPhase);
|
|
280
|
+
if (factIndex < restartIndex)
|
|
281
|
+
return "reusable";
|
|
282
|
+
if (factIndex > restartIndex)
|
|
283
|
+
return "invalidated";
|
|
284
|
+
return input.commitState === "committed-candidate" ? "reusable" : "invalidated";
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Hash/fingerprint-driven frozen command reuse check. Unknown/malformed
|
|
288
|
+
* command hash → refuse (fail-closed). Any invalidated input fact (by matrix
|
|
289
|
+
* or explicit set) → refuse. All facts reusable + known hash → reuse.
|
|
290
|
+
*/
|
|
291
|
+
export function validateFrozenCommandReuse(input) {
|
|
292
|
+
if (!/^[a-f0-9]{64}$/.test(input.commandHash)) {
|
|
293
|
+
return {
|
|
294
|
+
decision: "refuse",
|
|
295
|
+
reason: "unknown or malformed command hash; fail-closed",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const explicitlyInvalidated = new Set(input.invalidatedFactIds);
|
|
299
|
+
for (const factId of input.inputFactIds) {
|
|
300
|
+
const disposition = evaluateFactInvalidation({
|
|
301
|
+
factId,
|
|
302
|
+
restartPhase: input.restartPhase,
|
|
303
|
+
commitState: input.commitState,
|
|
304
|
+
});
|
|
305
|
+
if (disposition === "invalidated" || explicitlyInvalidated.has(factId)) {
|
|
306
|
+
return {
|
|
307
|
+
decision: "refuse",
|
|
308
|
+
reason: `input fact ${factId} is invalidated; frozen command cannot be reused`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
decision: "reuse",
|
|
314
|
+
reason: "all input facts reusable and command hash is known",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/* ------------------------------------------------------------------ *
|
|
318
|
+
* Failure capsule (AC-001). *
|
|
319
|
+
* ------------------------------------------------------------------ */
|
|
320
|
+
export const failureCapsuleV1Schema = z
|
|
321
|
+
.object({
|
|
322
|
+
schemaVersion: z.literal(1),
|
|
323
|
+
parentRunId: z.string().min(1),
|
|
324
|
+
operationState: frontendOperationStateSchema,
|
|
325
|
+
operationTerminal: frontendOperationTerminalSchema.optional(),
|
|
326
|
+
recoveryMode: recoveryModeSchema,
|
|
327
|
+
protocolFailureReason: frontendNodeProtocolFailureReasonSchema.optional(),
|
|
328
|
+
failureOwner: failureOwnerSchema,
|
|
329
|
+
restartPhase: restartPhaseSchema.optional(),
|
|
330
|
+
modelSuggestedRestartPhase: restartPhaseSchema.optional(),
|
|
331
|
+
invalidatedFactIds: z.array(z.string().min(1)),
|
|
332
|
+
reusableFactIds: z.array(z.string().min(1)),
|
|
333
|
+
evidenceRefs: z.array(z.string().min(1)),
|
|
334
|
+
normalizedFingerprint: hash64,
|
|
335
|
+
})
|
|
336
|
+
.strict()
|
|
337
|
+
.superRefine((value, ctx) => {
|
|
338
|
+
// Cross-field invariant 1: terminal state ⇔ matching terminal present;
|
|
339
|
+
// non-terminal state ⇔ terminal absent.
|
|
340
|
+
if (isFrontendOperationTerminalState(value.operationState)) {
|
|
341
|
+
if (value.operationTerminal === undefined) {
|
|
342
|
+
ctx.addIssue({
|
|
343
|
+
code: z.ZodIssueCode.custom,
|
|
344
|
+
message: `terminal operation state ${value.operationState} requires operationTerminal`,
|
|
345
|
+
path: ["operationTerminal"],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else if (value.operationTerminal !== value.operationState) {
|
|
349
|
+
ctx.addIssue({
|
|
350
|
+
code: z.ZodIssueCode.custom,
|
|
351
|
+
message: `operationTerminal must equal operationState for terminal state (${value.operationState})`,
|
|
352
|
+
path: ["operationTerminal"],
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (value.operationTerminal !== undefined) {
|
|
357
|
+
ctx.addIssue({
|
|
358
|
+
code: z.ZodIssueCode.custom,
|
|
359
|
+
message: `non-terminal operation state ${value.operationState} must not carry operationTerminal`,
|
|
360
|
+
path: ["operationTerminal"],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// Cross-field invariant 2: continuation/new-dag require a restart phase;
|
|
364
|
+
// all other modes forbid one.
|
|
365
|
+
const modeRequiresRestart = value.recoveryMode === "continuation" || value.recoveryMode === "new-dag";
|
|
366
|
+
if (modeRequiresRestart && value.restartPhase === undefined) {
|
|
367
|
+
ctx.addIssue({
|
|
368
|
+
code: z.ZodIssueCode.custom,
|
|
369
|
+
message: `recoveryMode ${value.recoveryMode} requires restartPhase`,
|
|
370
|
+
path: ["restartPhase"],
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (!modeRequiresRestart && value.restartPhase !== undefined) {
|
|
374
|
+
ctx.addIssue({
|
|
375
|
+
code: z.ZodIssueCode.custom,
|
|
376
|
+
message: `recoveryMode ${value.recoveryMode} must not carry restartPhase`,
|
|
377
|
+
path: ["restartPhase"],
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Cross-field invariant 3: when present, restartPhase must equal the
|
|
381
|
+
// runtime classifier output (a model suggestion never overrides it).
|
|
382
|
+
if (value.restartPhase !== undefined) {
|
|
383
|
+
const expectedRestartPhase = classifyRestartPhase(value.failureOwner);
|
|
384
|
+
if (value.restartPhase !== expectedRestartPhase) {
|
|
385
|
+
ctx.addIssue({
|
|
386
|
+
code: z.ZodIssueCode.custom,
|
|
387
|
+
message: `restartPhase must equal runtime classifier output (${expectedRestartPhase ?? "undefined"})`,
|
|
388
|
+
path: ["restartPhase"],
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const invalidated = new Set(value.invalidatedFactIds);
|
|
393
|
+
const overlap = value.reusableFactIds.filter((id) => invalidated.has(id));
|
|
394
|
+
if (overlap.length > 0) {
|
|
395
|
+
ctx.addIssue({
|
|
396
|
+
code: z.ZodIssueCode.custom,
|
|
397
|
+
message: `fact ids cannot be both invalidated and reusable: ${overlap.join(",")}`,
|
|
398
|
+
path: ["reusableFactIds"],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const expectedFingerprint = computeNormalizedFailureFingerprint({
|
|
402
|
+
failureOwner: value.failureOwner,
|
|
403
|
+
protocolFailureReason: value.protocolFailureReason ?? "",
|
|
404
|
+
restartPhase: value.restartPhase,
|
|
405
|
+
invalidatedFactIds: value.invalidatedFactIds,
|
|
406
|
+
});
|
|
407
|
+
if (value.normalizedFingerprint !== expectedFingerprint) {
|
|
408
|
+
ctx.addIssue({
|
|
409
|
+
code: z.ZodIssueCode.custom,
|
|
410
|
+
message: "normalizedFingerprint must match the deterministic fingerprint",
|
|
411
|
+
path: ["normalizedFingerprint"],
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
/**
|
|
416
|
+
* Assemble a validated failure capsule. `restartPhase` is derived from the
|
|
417
|
+
* runtime classifier only when the mode requires one; a model suggestion is
|
|
418
|
+
* only recorded, never applied. The schema then enforces the cross-field
|
|
419
|
+
* invariants (terminal/state, mode/phase, classifier authority).
|
|
420
|
+
*/
|
|
421
|
+
export function buildFailureCapsule(input) {
|
|
422
|
+
const modeRequiresRestart = input.recoveryMode === "continuation" || input.recoveryMode === "new-dag";
|
|
423
|
+
const restartPhase = modeRequiresRestart
|
|
424
|
+
? classifyRestartPhase(input.failureOwner)
|
|
425
|
+
: undefined;
|
|
426
|
+
const operationTerminal = isFrontendOperationTerminalState(input.operationState)
|
|
427
|
+
? input.operationState
|
|
428
|
+
: undefined;
|
|
429
|
+
const invalidatedFactIds = [...new Set(input.invalidatedFactIds ?? [])].sort();
|
|
430
|
+
const reusableFactIds = [...new Set(input.reusableFactIds ?? [])].sort();
|
|
431
|
+
return failureCapsuleV1Schema.parse({
|
|
432
|
+
schemaVersion: 1,
|
|
433
|
+
parentRunId: input.parentRunId,
|
|
434
|
+
operationState: input.operationState,
|
|
435
|
+
...(operationTerminal !== undefined ? { operationTerminal } : {}),
|
|
436
|
+
recoveryMode: input.recoveryMode,
|
|
437
|
+
...(input.protocolFailureReason !== undefined
|
|
438
|
+
? { protocolFailureReason: input.protocolFailureReason }
|
|
439
|
+
: {}),
|
|
440
|
+
failureOwner: input.failureOwner,
|
|
441
|
+
...(restartPhase !== undefined ? { restartPhase } : {}),
|
|
442
|
+
...(input.modelSuggestedRestartPhase !== undefined
|
|
443
|
+
? { modelSuggestedRestartPhase: input.modelSuggestedRestartPhase }
|
|
444
|
+
: {}),
|
|
445
|
+
invalidatedFactIds,
|
|
446
|
+
reusableFactIds,
|
|
447
|
+
evidenceRefs: [...new Set(input.evidenceRefs ?? [])].sort(),
|
|
448
|
+
normalizedFingerprint: computeNormalizedFailureFingerprint({
|
|
449
|
+
failureOwner: input.failureOwner,
|
|
450
|
+
protocolFailureReason: input.protocolFailureReason ?? "",
|
|
451
|
+
restartPhase,
|
|
452
|
+
invalidatedFactIds,
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
}
|