@tea-agent/loop-agent 0.35.1-beta.2 → 0.35.1
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 +0 -2
- package/CHANGELOG.md +25 -24
- package/bin/loop-agent.js +1 -37
- package/dist/application/dag/generate-task-dag.js +4 -1
- package/dist/application/task-lifecycle/advance.js +14 -0
- package/dist/cli/program.js +2 -2
- package/dist/commands/task-advance.js +1 -0
- package/dist/executors/dag-pi-executor.js +0 -44
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/config-types.js +2 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/source-prepare/build-draft.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +37 -10
- package/dist/task/task-demand-routing.js +10 -0
- package/dist/worker/console/operator-actions.js +72 -6
- package/dist/worker/console/prd-intake-bridge.js +10 -3
- package/dist/worker/console/prd-reference-discovery.js +124 -0
- package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
- package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
- package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +26 -41
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +11 -245
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +2 -130
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
|
@@ -654,15 +654,8 @@ export const dagConvergenceSpecSchema = z
|
|
|
654
654
|
chainNodeIds: z.array(z.string()).optional(),
|
|
655
655
|
})
|
|
656
656
|
.optional();
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
* self-validation. Validators are registered in the contract output registry
|
|
660
|
-
* (src/workflows/dag/contract-output-registry.ts).
|
|
661
|
-
*/
|
|
662
|
-
export const structuredContractOutputSchemaIds = [
|
|
663
|
-
"frontend-implementation-contract-v1",
|
|
664
|
-
];
|
|
665
|
-
export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/, "task id must be kebab-case"),
|
|
657
|
+
export const dagTaskSchema = z.object({
|
|
658
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/, "task id must be kebab-case"),
|
|
666
659
|
depends_on: z.array(z.string()).default([]),
|
|
667
660
|
/**
|
|
668
661
|
* How SKIPPED upstreams affect readiness.
|
|
@@ -711,20 +704,6 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
|
|
|
711
704
|
* on safe read-only Pi nodes before the node becomes ERROR.
|
|
712
705
|
*/
|
|
713
706
|
outputProtocol: dagOutputProtocolSchema.optional(),
|
|
714
|
-
/**
|
|
715
|
-
* Node-level structured contract self-check: after the Pi output is
|
|
716
|
-
* produced, validate it against the named contract schema at the node so
|
|
717
|
-
* schema/typo/null violations become invalid-output (retryable on the node)
|
|
718
|
-
* instead of failing later at a downstream deterministic gate. Validators
|
|
719
|
-
* are resolved via the contract output registry keyed by schemaId.
|
|
720
|
-
*/
|
|
721
|
-
structuredContractOutput: z
|
|
722
|
-
.object({
|
|
723
|
-
schemaId: z.enum(structuredContractOutputSchemaIds),
|
|
724
|
-
retryOnInvalid: z.boolean().default(true),
|
|
725
|
-
})
|
|
726
|
-
.strict()
|
|
727
|
-
.optional(),
|
|
728
707
|
/**
|
|
729
708
|
* Fail-closed outcome/diff consistency contract for bounded Pi writers.
|
|
730
709
|
* The writer must begin with IMPLEMENTATION_OUTCOME: changed,
|
|
@@ -895,113 +874,6 @@ export const dagSpecSchema = z
|
|
|
895
874
|
});
|
|
896
875
|
}
|
|
897
876
|
});
|
|
898
|
-
export const FRONTEND_RECOVERY_STATE_SCHEMA_VERSION = 1;
|
|
899
|
-
export const FRONTEND_RECOVERY_RESULT_SCHEMA_VERSION = 1;
|
|
900
|
-
/**
|
|
901
|
-
* Frontend candidate-continuation recovery phase (decision B, phase-3 subset).
|
|
902
|
-
* Phase 3a parents converge first, so `rollback-pending` is not produced and
|
|
903
|
-
* was removed by AC-1; the phase starts at `child-staging` and ends at
|
|
904
|
-
* `settled`.
|
|
905
|
-
*/
|
|
906
|
-
export const frontendRecoveryPhaseSchema = z.enum([
|
|
907
|
-
"child-staging",
|
|
908
|
-
"child-activating",
|
|
909
|
-
"child-running",
|
|
910
|
-
"settled",
|
|
911
|
-
]);
|
|
912
|
-
/**
|
|
913
|
-
* Single-writer recovery intent/lineage stored on `DagRunState`. The parent run
|
|
914
|
-
* owns the authoritative copy and advances it via revision-guarded CAS writes;
|
|
915
|
-
* the child carries a frozen lineage snapshot so the activation gate can prove
|
|
916
|
-
* it is the reserved child of a child-running parent.
|
|
917
|
-
*
|
|
918
|
-
* Invariants: `attemptIndex ∈ {0,1}`; `continuationCount ∈ {0,1}`; a child's
|
|
919
|
-
* `recoveryRootRunId` always equals the root runId; `revision` is the CAS
|
|
920
|
-
* pre-comparison counter. `revision` monotonicity is enforced at runtime by the
|
|
921
|
-
* runner's `(existing?.revision ?? 0) + 1` CAS write (the schema only bounds it
|
|
922
|
-
* non-negative). `childRunId` may be absent through `child-activating` and must
|
|
923
|
-
* be non-empty from `child-running` onward.
|
|
924
|
-
*/
|
|
925
|
-
export const frontendRecoveryStateSchema = z
|
|
926
|
-
.object({
|
|
927
|
-
schemaVersion: z.literal(FRONTEND_RECOVERY_STATE_SCHEMA_VERSION),
|
|
928
|
-
phase: frontendRecoveryPhaseSchema,
|
|
929
|
-
requestId: z.string().min(1),
|
|
930
|
-
recoveryRootRunId: z.string().min(1),
|
|
931
|
-
parentRunId: z.string().min(1),
|
|
932
|
-
childRunId: z.string().min(1).optional(),
|
|
933
|
-
attemptId: z.string().min(1),
|
|
934
|
-
attemptIndex: z.number().int().min(0).max(1),
|
|
935
|
-
continuationCount: z.number().int().min(0).max(1),
|
|
936
|
-
/** Node id of the reset-closure root: frontend-plan-pi /
|
|
937
|
-
* frontend-plan-revision-pi / frontend-prewrite-gate-shell /
|
|
938
|
-
* frontend-implement-pi (writer partial write). */
|
|
939
|
-
failureSource: z.string().min(1).optional(),
|
|
940
|
-
revision: z.number().int().nonnegative(),
|
|
941
|
-
})
|
|
942
|
-
.strict()
|
|
943
|
-
.superRefine((value, ctx) => {
|
|
944
|
-
const requiresChild = value.phase === "child-running" || value.phase === "settled";
|
|
945
|
-
if (requiresChild && !value.childRunId) {
|
|
946
|
-
ctx.addIssue({
|
|
947
|
-
code: z.ZodIssueCode.custom,
|
|
948
|
-
message: `phase ${value.phase} requires a non-empty childRunId`,
|
|
949
|
-
path: ["childRunId"],
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
});
|
|
953
|
-
export const frontendRecoveryOutcomeSchema = z.enum([
|
|
954
|
-
"none",
|
|
955
|
-
"recovered",
|
|
956
|
-
"candidate-contract-invalid",
|
|
957
|
-
"prewrite-blocked",
|
|
958
|
-
"repair-exhausted",
|
|
959
|
-
"auto-recovery-blocked",
|
|
960
|
-
]);
|
|
961
|
-
export const frontendRecoveryOriginSchema = z
|
|
962
|
-
.object({
|
|
963
|
-
kind: z.enum(["frontend-prewrite-gate", "frontend-writer"]),
|
|
964
|
-
parentRunId: z.string().min(1),
|
|
965
|
-
childRunId: z.string().min(1).optional(),
|
|
966
|
-
requestId: z.string().min(1),
|
|
967
|
-
failedNodeId: z.string(),
|
|
968
|
-
})
|
|
969
|
-
.strict();
|
|
970
|
-
export const frontendRecoveryFailureClassSchema = z
|
|
971
|
-
.object({
|
|
972
|
-
code: z.enum([
|
|
973
|
-
"candidate-contract-invalid",
|
|
974
|
-
"prewrite-blocked",
|
|
975
|
-
"staging-failed",
|
|
976
|
-
"writer-transient-partial-write",
|
|
977
|
-
]),
|
|
978
|
-
classification: z.string(),
|
|
979
|
-
reason: z.string(),
|
|
980
|
-
})
|
|
981
|
-
.strict();
|
|
982
|
-
export const frontendRecoveryEvidenceRefSchema = z
|
|
983
|
-
.object({
|
|
984
|
-
runId: z.string().min(1),
|
|
985
|
-
relativePath: z.string().min(1),
|
|
986
|
-
sha256: z.string().min(1),
|
|
987
|
-
})
|
|
988
|
-
.strict();
|
|
989
|
-
/**
|
|
990
|
-
* Terminal frontend recovery result. `failureClass` is absent for `none`,
|
|
991
|
-
* `recovered` and `auto-recovery-blocked` outcomes. The schema intentionally
|
|
992
|
-
* stays permissive (failureClass optional, evidenceRefs may be empty) so it
|
|
993
|
-
* accepts the runner's actual phase-3a products, which do not always carry a
|
|
994
|
-
* failure class or evidence reference.
|
|
995
|
-
*/
|
|
996
|
-
export const frontendRecoveryResultSchema = z
|
|
997
|
-
.object({
|
|
998
|
-
schemaVersion: z.literal(FRONTEND_RECOVERY_RESULT_SCHEMA_VERSION),
|
|
999
|
-
outcome: frontendRecoveryOutcomeSchema,
|
|
1000
|
-
origin: frontendRecoveryOriginSchema,
|
|
1001
|
-
failureClass: frontendRecoveryFailureClassSchema.optional(),
|
|
1002
|
-
evidenceRefs: z.array(frontendRecoveryEvidenceRefSchema),
|
|
1003
|
-
})
|
|
1004
|
-
.strict();
|
|
1005
877
|
export const DEFAULT_DAG_EXECUTOR_MODELS = {
|
|
1006
878
|
pi: {
|
|
1007
879
|
LOW: "gpt-5.3-codex-spark",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tea-agent/loop-agent",
|
|
3
|
-
"version": "0.35.1
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"loop-agent": "bin/loop-agent.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"pi-prompt": "node --import tsx/esm src/cli.ts pi-prompt",
|
|
46
46
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
47
47
|
"brand:sync": "node scripts/sync-brand-assets.mjs",
|
|
48
|
-
"build": "npm run brand:sync && npm run clean && tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');const p='dist/worker/observe/static';fs.mkdirSync(p,{recursive:true});fs.cpSync('src/worker/observe/static',p,{recursive:true});\" && npm run console:build
|
|
48
|
+
"build": "npm run brand:sync && npm run clean && tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');const p='dist/worker/observe/static';fs.mkdirSync(p,{recursive:true});fs.cpSync('src/worker/observe/static',p,{recursive:true});\" && npm run console:build",
|
|
49
49
|
"console:typecheck": "tsc -p src/worker/console/tsconfig.json",
|
|
50
50
|
"console:build": "npm run console:typecheck && vite build --config src/worker/console/vite.config.ts",
|
|
51
51
|
"prepack": "npm run build",
|
package/dist/build-stamp.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { structuredContractOutputSchemaIds, } from "./types.js";
|
|
2
|
-
const validators = new Map();
|
|
3
|
-
export function registerStructuredContractValidator(schemaId, validator) {
|
|
4
|
-
if (validators.has(schemaId)) {
|
|
5
|
-
throw new Error(`structured contract validator already registered for ${schemaId}`);
|
|
6
|
-
}
|
|
7
|
-
validators.set(schemaId, validator);
|
|
8
|
-
}
|
|
9
|
-
export function getStructuredContractValidator(schemaId) {
|
|
10
|
-
if (structuredContractOutputSchemaIds.includes(schemaId)) {
|
|
11
|
-
return validators.get(schemaId);
|
|
12
|
-
}
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Side-effect module: registers every structured contract validator against
|
|
3
|
-
* its schemaId in the contract output registry. Imported once by the node
|
|
4
|
-
* executor; keeps per-contract imports out of the executor.
|
|
5
|
-
*/
|
|
6
|
-
import { validateFrontendContractNodeOutput } from "./frontend-implementation-contract.js";
|
|
7
|
-
import { registerStructuredContractValidator } from "./contract-output-registry.js";
|
|
8
|
-
registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { computeResetClosure } from "./rerun-plan.js";
|
|
2
|
-
/**
|
|
3
|
-
* Frontend-only candidate-continuation recovery planning (AC-5).
|
|
4
|
-
*
|
|
5
|
-
* Distinct from the generic `dag rerun` reset closure: the failure source is
|
|
6
|
-
* derived from the prewrite-result's `selectedPlanNodeId` rather than an
|
|
7
|
-
* operator-selected node, and the closure starts at the specific frontend
|
|
8
|
-
* candidate producer that failed.
|
|
9
|
-
*/
|
|
10
|
-
/** Idempotency namespace for requestId-keyed recovery intents (D2). */
|
|
11
|
-
export const FRONTEND_RECOVERY_INTENT_REL_DIR = ".runtime/frontend-recovery";
|
|
12
|
-
/** Import manifest path inside the child run dir; hashed into the activation marker. */
|
|
13
|
-
export const FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
|
|
14
|
-
/** Staging directory prefix under `active/` for in-flight child materialization. */
|
|
15
|
-
export const FRONTEND_RECOVERY_STAGING_PREFIX = ".frontend-recovery-staging-";
|
|
16
|
-
export function deriveFrontendRecoveryFailureSource(result) {
|
|
17
|
-
if (result.selectedPlanNodeId === "frontend-plan-revision-pi") {
|
|
18
|
-
return "frontend-plan-revision-pi";
|
|
19
|
-
}
|
|
20
|
-
if (result.selectedPlanNodeId === "frontend-plan-pi") {
|
|
21
|
-
return "frontend-plan-pi";
|
|
22
|
-
}
|
|
23
|
-
return "frontend-prewrite-gate-shell";
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Compute the frontend candidate-continuation reset partition:
|
|
27
|
-
* - `frontend-plan-pi` failure → reset plan + design-review + prewrite (+ descendants);
|
|
28
|
-
* - `frontend-plan-revision-pi` failure → reset revision + final-review + prewrite (+ descendants);
|
|
29
|
-
* - prewrite-only materialization → reset prewrite (+ descendants), reuse verified plan/review.
|
|
30
|
-
*
|
|
31
|
-
* Everything upstream of the failure source is imported as verified parent facts.
|
|
32
|
-
*/
|
|
33
|
-
export function computeFrontendRecoveryPlan(input) {
|
|
34
|
-
const failureSource = deriveFrontendRecoveryFailureSource(input.result);
|
|
35
|
-
return computeFrontendRecoveryPlanForSource({
|
|
36
|
-
spec: input.spec,
|
|
37
|
-
parentRunId: input.parentRunId,
|
|
38
|
-
requestId: input.requestId,
|
|
39
|
-
failureSource,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Phase 5: writer transient partial-write recovery plan. The writer itself
|
|
44
|
-
* failed after some bounded writes, so the child re-runs `frontend-implement-pi`
|
|
45
|
-
* (and its descendants) while importing the verified contract/scout/plan/prewrite
|
|
46
|
-
* facts. Everything upstream of the writer is imported; the writer + descendants
|
|
47
|
-
* are reset.
|
|
48
|
-
*/
|
|
49
|
-
export function computeFrontendWriterRecoveryPlan(input) {
|
|
50
|
-
return computeFrontendRecoveryPlanForSource({
|
|
51
|
-
spec: input.spec,
|
|
52
|
-
parentRunId: input.parentRunId,
|
|
53
|
-
requestId: input.requestId,
|
|
54
|
-
failureSource: "frontend-implement-pi",
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
function computeFrontendRecoveryPlanForSource(input) {
|
|
58
|
-
const resetNodeIds = computeResetClosure(input.spec, input.failureSource);
|
|
59
|
-
const resetSet = new Set(resetNodeIds);
|
|
60
|
-
const importedNodeIds = input.spec.tasks
|
|
61
|
-
.map((task) => task.id)
|
|
62
|
-
.filter((nodeId) => !resetSet.has(nodeId))
|
|
63
|
-
.sort();
|
|
64
|
-
return {
|
|
65
|
-
schemaVersion: 1,
|
|
66
|
-
parentRunId: input.parentRunId,
|
|
67
|
-
requestId: input.requestId,
|
|
68
|
-
failureSource: input.failureSource,
|
|
69
|
-
resetNodeIds,
|
|
70
|
-
importedNodeIds,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
export { computeFrontendRecoveryPlanForSource };
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
-
/**
|
|
6
|
-
* Phase 6: frontend recovery root manifest — the single final read model for a
|
|
7
|
-
* frontend recovery chain. A "root" is the first physical run of a
|
|
8
|
-
* frontend-implementation attempt; its parent/child lineage is counted once
|
|
9
|
-
* under `recoveryRootRunId`. report/Console read the manifest for dedup and
|
|
10
|
-
* final-status display instead of scanning parent/child fields (P1-4).
|
|
11
|
-
*/
|
|
12
|
-
export const FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID = "frontend-recovery-root-manifest-v1";
|
|
13
|
-
export const frontendRecoveryRootManifestSchema = z
|
|
14
|
-
.object({
|
|
15
|
-
schemaVersion: z.literal(1),
|
|
16
|
-
schemaId: z.literal(FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID),
|
|
17
|
-
recoveryRootRunId: z.string().min(1),
|
|
18
|
-
/** Terminal logical status for the whole chain (finished/partial_failed/failed). */
|
|
19
|
-
status: z.enum(["finished", "partial_failed", "failed"]),
|
|
20
|
-
outcome: z.enum([
|
|
21
|
-
"none",
|
|
22
|
-
"recovered",
|
|
23
|
-
"candidate-contract-invalid",
|
|
24
|
-
"prewrite-blocked",
|
|
25
|
-
"repair-exhausted",
|
|
26
|
-
"auto-recovery-blocked",
|
|
27
|
-
]),
|
|
28
|
-
attempts: z.array(z
|
|
29
|
-
.object({
|
|
30
|
-
runId: z.string().min(1),
|
|
31
|
-
role: z.enum(["root", "child"]),
|
|
32
|
-
attemptIndex: z.number().int().min(0),
|
|
33
|
-
outcome: z.enum([
|
|
34
|
-
"none",
|
|
35
|
-
"recovered",
|
|
36
|
-
"candidate-contract-invalid",
|
|
37
|
-
"prewrite-blocked",
|
|
38
|
-
"repair-exhausted",
|
|
39
|
-
"auto-recovery-blocked",
|
|
40
|
-
]),
|
|
41
|
-
status: z.enum(["finished", "partial_failed", "failed"]),
|
|
42
|
-
})
|
|
43
|
-
.strict()),
|
|
44
|
-
evidenceRefs: z.array(z
|
|
45
|
-
.object({
|
|
46
|
-
runId: z.string().min(1),
|
|
47
|
-
relativePath: z.string().min(1),
|
|
48
|
-
sha256: z.string().min(1),
|
|
49
|
-
})
|
|
50
|
-
.strict()),
|
|
51
|
-
createdAt: z.string().min(1),
|
|
52
|
-
})
|
|
53
|
-
.strict();
|
|
54
|
-
/** Relative path of the root manifest inside a run dir. */
|
|
55
|
-
export const FRONTEND_RECOVERY_ROOT_MANIFEST_REL = "contracts/frontend-recovery-root-manifest.json";
|
|
56
|
-
export function frontendRecoveryRootManifestAbsPath(runDir) {
|
|
57
|
-
return path.join(runDir, FRONTEND_RECOVERY_ROOT_MANIFEST_REL);
|
|
58
|
-
}
|
|
59
|
-
export async function writeFrontendRecoveryRootManifest(runDir, manifest) {
|
|
60
|
-
const abs = frontendRecoveryRootManifestAbsPath(runDir);
|
|
61
|
-
await mkdir(path.dirname(abs), { recursive: true });
|
|
62
|
-
await writeJsonAtomic(abs, manifest);
|
|
63
|
-
return abs;
|
|
64
|
-
}
|
|
65
|
-
export async function readFrontendRecoveryRootManifest(runDir) {
|
|
66
|
-
try {
|
|
67
|
-
const raw = JSON.parse(await readFile(frontendRecoveryRootManifestAbsPath(runDir), "utf8"));
|
|
68
|
-
const parsed = frontendRecoveryRootManifestSchema.safeParse(raw);
|
|
69
|
-
return parsed.success ? parsed.data : undefined;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Build a root manifest from a converged root run's lineage. The manifest is the
|
|
77
|
-
* single authority for final status/outcome: parent keeps its raw failure status,
|
|
78
|
-
* the chain's logical status is only `finished` when a child finished and the
|
|
79
|
-
* outcome is `recovered`.
|
|
80
|
-
*/
|
|
81
|
-
export function buildFrontendRecoveryRootManifest(input) {
|
|
82
|
-
const status = input.outcome === "recovered" ? "finished" : "failed";
|
|
83
|
-
return {
|
|
84
|
-
schemaVersion: 1,
|
|
85
|
-
schemaId: FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID,
|
|
86
|
-
recoveryRootRunId: input.recoveryRootRunId,
|
|
87
|
-
status,
|
|
88
|
-
outcome: input.outcome,
|
|
89
|
-
attempts: input.attempts,
|
|
90
|
-
evidenceRefs: input.evidenceRefs ?? [],
|
|
91
|
-
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Dedup a set of DAG runs by recovery root (P1-4): each root contributes exactly
|
|
96
|
-
* one summary; parent/child physical runs are folded into the root's attempts.
|
|
97
|
-
* A run with no `frontendRecoveryState` is its own single-attempt root.
|
|
98
|
-
*/
|
|
99
|
-
export function aggregateFrontendRecoveryRoots(runs) {
|
|
100
|
-
const byRoot = new Map();
|
|
101
|
-
for (const run of runs) {
|
|
102
|
-
const rootId = run.frontendRecoveryState?.recoveryRootRunId ?? run.runId;
|
|
103
|
-
const list = byRoot.get(rootId) ?? [];
|
|
104
|
-
list.push(run);
|
|
105
|
-
byRoot.set(rootId, list);
|
|
106
|
-
}
|
|
107
|
-
const result = [];
|
|
108
|
-
for (const [rootId, list] of byRoot) {
|
|
109
|
-
const sorted = [...list].sort((a, b) => (a.frontendRecoveryState?.attemptIndex ?? 0) -
|
|
110
|
-
(b.frontendRecoveryState?.attemptIndex ?? 0));
|
|
111
|
-
// The root's displayed status is the last attempt's status, unless the
|
|
112
|
-
// root run itself finished (a child recovered only surfaces `finished`
|
|
113
|
-
// via the manifest, not via the raw root run status).
|
|
114
|
-
const last = sorted[sorted.length - 1];
|
|
115
|
-
result.push({
|
|
116
|
-
recoveryRootRunId: rootId,
|
|
117
|
-
status: last.status,
|
|
118
|
-
attemptCount: sorted.length,
|
|
119
|
-
runIds: sorted.map((run) => run.runId),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
123
|
-
}
|