@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.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/CHANGELOG.md +47 -267
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +0 -5
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +231 -3784
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/task/config-types.js +7 -34
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +176 -9
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +42 -22
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +139 -60
- package/dist/worker/console/index.js +0 -3
- package/dist/worker/console/operation-store.js +6 -226
- package/dist/worker/console/operator-actions.js +7 -169
- package/dist/worker/console/pi-plugins.js +3 -1
- package/dist/worker/console/server.js +0 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-D7oU983t.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BrKSRdFS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-CiXV7L56.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-D_Hs_gIG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-DbM-Lqqh.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1xajGpe.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-D1M8uqJw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-4D3RFHT6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CdwpY_n9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-BLeLZ9PG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CeRNEMvc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-CjkHP8Vy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-B54ZUQY-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-4bKaMVmx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-Cyjfznu_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CDN-zGFV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-HxJmz411.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-DtSt5K2w.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-VtkSpRmG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-Bj4DT4du.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-Bpuk6BmP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-BHYGv_yO.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-DEx3WwnT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-Cj0dS5e8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-B97h8l21.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-DMSFAhBd.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-DhVOCbt6.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-GlakITWL.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-VxPpu9eQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js} +1 -1
- package/dist/worker/console/static/assets/index-BLzcAykx.js +451 -0
- package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-BXYuU4Uw.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-C34erCbU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-DZCAiOU3.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-C9gaTdUm.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CvI0Z17U.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-DMMSIx7j.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-BKKP0EO6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-C3LiFbl5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-DNthMjzc.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-miFsr4Tv.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-yXUncmn6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-DLeWKLak.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-CI3he1R3.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-B7jfDkS5.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BmrZod7J.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-CDOlHVSY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-Cz-XX1gy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-caV4cI8C.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-dImPlnIO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CtRYVJcn.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/workspace-context.js +1 -111
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/views/dag-inspector.js +2 -7
- package/dist/worker/observe/static/views/session-timeline.js +46 -15
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/init-hybrid.js +494 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +4 -732
- package/dist/workflows/dag/prompt.js +1 -34
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +2 -10
- package/dist/workflows/dag/rerun-run.js +0 -23
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +122 -429
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/types.js +12 -239
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +0 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Turn grouping for Operator Chat inline process UX (pi-web ProcessDetails mind).
|
|
3
3
|
*/
|
|
4
4
|
import { getDisplayableAssistantBlocks, joinAssistantText, parseAssistantContent, splitFinalAssistantBlocks, } from "./assistant-content.js";
|
|
5
|
-
import { semanticActivityForTool, } from "./semantic-activity.js";
|
|
5
|
+
import { dagProgressActivityForTool, semanticActivityForTool, } from "./semantic-activity.js";
|
|
6
6
|
function emptyGroup(key) {
|
|
7
7
|
return {
|
|
8
8
|
key,
|
|
@@ -72,14 +72,22 @@ export function hydrateSessionThread(raw) {
|
|
|
72
72
|
turnId = item.turnId;
|
|
73
73
|
if (!turnId)
|
|
74
74
|
turnId = `restored-tool-${item.id}`;
|
|
75
|
+
// Prefer event-ring timestamps (tool_call at → tool_result at) so
|
|
76
|
+
// refreshed sessions keep real elapsed durations instead of 0ms.
|
|
77
|
+
const startedAtMs = item.startedAt ? Date.parse(item.startedAt) : NaN;
|
|
78
|
+
const finishedAtMs = item.finishedAt ? Date.parse(item.finishedAt) : NaN;
|
|
75
79
|
const entry = {
|
|
76
80
|
id: item.toolCallId || item.id,
|
|
77
81
|
toolName: item.toolName || "tool",
|
|
78
82
|
status: item.isError === true ? "error" : "ok",
|
|
79
83
|
resultText: item.text,
|
|
80
84
|
result: item.text,
|
|
81
|
-
createdAt
|
|
82
|
-
|
|
85
|
+
createdAt: Number.isFinite(startedAtMs)
|
|
86
|
+
? startedAtMs
|
|
87
|
+
: createdAt,
|
|
88
|
+
finishedAt: Number.isFinite(finishedAtMs)
|
|
89
|
+
? finishedAtMs
|
|
90
|
+
: createdAt,
|
|
83
91
|
turnId: item.turnId ?? turnId,
|
|
84
92
|
...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
|
|
85
93
|
...(item.finishedSeq === undefined
|
|
@@ -88,7 +96,10 @@ export function hydrateSessionThread(raw) {
|
|
|
88
96
|
...(item.operationId ? { operationId: item.operationId } : {}),
|
|
89
97
|
};
|
|
90
98
|
toolCalls.push(entry);
|
|
91
|
-
|
|
99
|
+
// Durable rows with an event sequence are already self-ordering. Only
|
|
100
|
+
// legacy rows need the historical "next assistant" attribution fallback.
|
|
101
|
+
if (item.eventSeq === undefined)
|
|
102
|
+
pendingTools.push(entry);
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
return { messages, toolCalls };
|
|
@@ -118,6 +129,11 @@ function flushToolBuffer(processItems, buffer) {
|
|
|
118
129
|
return;
|
|
119
130
|
const sorted = sortToolsStable(buffer);
|
|
120
131
|
for (const tool of sorted) {
|
|
132
|
+
const dagProgress = dagProgressActivityForTool(tool);
|
|
133
|
+
if (dagProgress) {
|
|
134
|
+
processItems.push({ kind: "dag-progress", progress: dagProgress });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
121
137
|
const activity = semanticActivityForTool(tool);
|
|
122
138
|
if (activity) {
|
|
123
139
|
processItems.push({ kind: "activity", activity });
|
|
@@ -127,48 +143,92 @@ function flushToolBuffer(processItems, buffer) {
|
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
function collapseDagProgressItems(items) {
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
return items.filter((item) => {
|
|
149
|
+
if (item.kind !== "dag-progress")
|
|
150
|
+
return true;
|
|
151
|
+
if (seen.has(item.progress.activityRef))
|
|
152
|
+
return false;
|
|
153
|
+
seen.add(item.progress.activityRef);
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function fillProcessDetails(group) {
|
|
158
|
+
const usedToolIds = new Set();
|
|
159
|
+
const nodes = [];
|
|
160
|
+
let insertionOrder = 0;
|
|
161
|
+
const addNode = (input) => {
|
|
162
|
+
if (input.items.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
nodes.push({
|
|
165
|
+
...(input.eventSeq === undefined ? {} : { eventSeq: input.eventSeq }),
|
|
166
|
+
createdAt: input.createdAt,
|
|
167
|
+
insertionOrder: insertionOrder++,
|
|
168
|
+
items: input.items,
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
const addAssistantNode = (assistant, blocks, includeProviderError = false) => {
|
|
172
|
+
let items = [];
|
|
173
|
+
let anchorSeq = assistant.eventSeq;
|
|
174
|
+
let anchorAt = assistant.createdAt;
|
|
175
|
+
const flushItems = () => {
|
|
176
|
+
addNode({
|
|
177
|
+
items,
|
|
178
|
+
...(anchorSeq === undefined ? {} : { eventSeq: anchorSeq }),
|
|
179
|
+
createdAt: anchorAt,
|
|
180
|
+
});
|
|
181
|
+
items = [];
|
|
182
|
+
};
|
|
183
|
+
for (const block of blocks) {
|
|
184
|
+
if (block.type === "thinking") {
|
|
185
|
+
items.push({ kind: "thinking", thinking: block.thinking });
|
|
186
|
+
continue;
|
|
144
187
|
}
|
|
145
|
-
|
|
188
|
+
if (block.type === "text") {
|
|
189
|
+
if (block.text.trim())
|
|
190
|
+
items.push({ kind: "text", text: block.text });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const tool = group.tools.find((entry) => entry.id === block.toolCallId);
|
|
194
|
+
if (!tool || usedToolIds.has(tool.id))
|
|
195
|
+
continue;
|
|
196
|
+
flushItems();
|
|
197
|
+
usedToolIds.add(tool.id);
|
|
198
|
+
const toolItems = [];
|
|
199
|
+
flushToolBuffer(toolItems, [tool]);
|
|
200
|
+
addNode({
|
|
201
|
+
items: toolItems,
|
|
202
|
+
...(tool.eventSeq === undefined
|
|
203
|
+
? anchorSeq === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { eventSeq: anchorSeq }
|
|
206
|
+
: { eventSeq: tool.eventSeq }),
|
|
207
|
+
createdAt: tool.createdAt,
|
|
208
|
+
});
|
|
209
|
+
anchorSeq = tool.eventSeq ?? anchorSeq;
|
|
210
|
+
anchorAt = tool.createdAt;
|
|
146
211
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
212
|
+
if (includeProviderError && assistant.providerError) {
|
|
213
|
+
items.push({ kind: "provider-error", error: assistant.providerError });
|
|
214
|
+
}
|
|
215
|
+
flushItems();
|
|
216
|
+
// Preserve the old-data fallback after this stage. Sequenced unmatched
|
|
217
|
+
// tools stay independent and are merged by their own eventSeq below.
|
|
218
|
+
const legacyUnmatched = group.tools.filter((tool) => !usedToolIds.has(tool.id) &&
|
|
219
|
+
tool.eventSeq === undefined &&
|
|
220
|
+
tool.assistantMessageId === assistant.id);
|
|
221
|
+
for (const tool of sortToolsStable(legacyUnmatched)) {
|
|
150
222
|
usedToolIds.add(tool.id);
|
|
223
|
+
const toolItems = [];
|
|
224
|
+
flushToolBuffer(toolItems, [tool]);
|
|
225
|
+
addNode({
|
|
226
|
+
items: toolItems,
|
|
227
|
+
...(anchorSeq === undefined ? {} : { eventSeq: anchorSeq }),
|
|
228
|
+
createdAt: Math.max(anchorAt, tool.createdAt),
|
|
229
|
+
});
|
|
151
230
|
}
|
|
152
|
-
}
|
|
153
|
-
flushToolBuffer(processItems, buffer);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Insert tools attributed to this assistant stage but without a matching
|
|
157
|
-
* content toolCall block (live SSE tool_call frames often arrive before the
|
|
158
|
-
* assistant content does). They follow the stage's thinking/text instead of
|
|
159
|
-
* sinking to the end of the whole turn.
|
|
160
|
-
*/
|
|
161
|
-
function appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds) {
|
|
162
|
-
const unmatched = group.tools.filter((tool) => !usedToolIds.has(tool.id) && tool.assistantMessageId === assistant.id);
|
|
163
|
-
if (unmatched.length === 0)
|
|
164
|
-
return;
|
|
165
|
-
for (const tool of unmatched)
|
|
166
|
-
usedToolIds.add(tool.id);
|
|
167
|
-
flushToolBuffer(processItems, unmatched);
|
|
168
|
-
}
|
|
169
|
-
function fillProcessDetails(group) {
|
|
170
|
-
const usedToolIds = new Set();
|
|
171
|
-
const processItems = [];
|
|
231
|
+
};
|
|
172
232
|
const lastAssistant = group.assistants.at(-1);
|
|
173
233
|
const finalAssistant = lastAssistant?.kind === "provider-error" ? undefined : lastAssistant;
|
|
174
234
|
const finalIndex = finalAssistant
|
|
@@ -178,30 +238,47 @@ function fillProcessDetails(group) {
|
|
|
178
238
|
if (index === finalIndex)
|
|
179
239
|
continue;
|
|
180
240
|
if (assistant.kind === "provider-error" && assistant.providerError) {
|
|
181
|
-
|
|
182
|
-
appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
|
|
183
|
-
processItems.push({
|
|
184
|
-
kind: "provider-error",
|
|
185
|
-
error: assistant.providerError,
|
|
186
|
-
});
|
|
241
|
+
addAssistantNode(assistant, getDisplayableAssistantBlocks(assistant.content ?? []), true);
|
|
187
242
|
continue;
|
|
188
243
|
}
|
|
189
|
-
|
|
190
|
-
appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
|
|
244
|
+
addAssistantNode(assistant, getDisplayableAssistantBlocks(assistant.content ?? []));
|
|
191
245
|
}
|
|
192
246
|
const blocks = finalAssistant?.content ?? [];
|
|
193
247
|
const split = splitFinalAssistantBlocks(blocks, { isStreaming: group.live });
|
|
194
|
-
emitProcessBlocks(split.processBlocks, group, processItems, usedToolIds);
|
|
195
248
|
if (finalAssistant) {
|
|
196
|
-
|
|
249
|
+
addAssistantNode(finalAssistant, split.processBlocks);
|
|
250
|
+
}
|
|
251
|
+
// DSH-style peer nodes: a sequenced tool keeps its own anchor instead of
|
|
252
|
+
// being appended to whichever assistant id happens to own it at render time.
|
|
253
|
+
for (const tool of group.tools) {
|
|
254
|
+
if (usedToolIds.has(tool.id) || tool.eventSeq === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
usedToolIds.add(tool.id);
|
|
257
|
+
const items = [];
|
|
258
|
+
flushToolBuffer(items, [tool]);
|
|
259
|
+
nodes.push({
|
|
260
|
+
eventSeq: tool.eventSeq,
|
|
261
|
+
createdAt: tool.createdAt,
|
|
262
|
+
insertionOrder: insertionOrder++,
|
|
263
|
+
items,
|
|
264
|
+
});
|
|
197
265
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
266
|
+
const fullySequenced = nodes.every((node) => node.eventSeq !== undefined);
|
|
267
|
+
nodes.sort((a, b) => {
|
|
268
|
+
if (fullySequenced && a.eventSeq !== b.eventSeq) {
|
|
269
|
+
return a.eventSeq - b.eventSeq;
|
|
270
|
+
}
|
|
271
|
+
if (a.createdAt !== b.createdAt)
|
|
272
|
+
return a.createdAt - b.createdAt;
|
|
273
|
+
return a.insertionOrder - b.insertionOrder;
|
|
274
|
+
});
|
|
275
|
+
const processItems = nodes.flatMap((node) => node.items);
|
|
276
|
+
// Bounded fallback for legacy restored rows with neither event sequence nor
|
|
277
|
+
// assistant attribution: keep them at the turn end in stable local order.
|
|
278
|
+
const legacyOrphans = group.tools.filter((tool) => !usedToolIds.has(tool.id));
|
|
279
|
+
if (legacyOrphans.length > 0)
|
|
280
|
+
flushToolBuffer(processItems, legacyOrphans);
|
|
281
|
+
group.processItems = collapseDagProgressItems(processItems);
|
|
205
282
|
if (!finalAssistant)
|
|
206
283
|
return;
|
|
207
284
|
if (blocks.length === 0) {
|
|
@@ -282,7 +359,9 @@ export function buildTurnProcessGroups(input) {
|
|
|
282
359
|
if (assigned.has(tool.id))
|
|
283
360
|
continue;
|
|
284
361
|
if (tool.turnId) {
|
|
285
|
-
const byTurn = groups.find((group) => group.
|
|
362
|
+
const byTurn = groups.find((group) => group.user?.turnId === tool.turnId ||
|
|
363
|
+
group.assistants.some((assistant) => assistant.turnId === tool.turnId) ||
|
|
364
|
+
group.tools.some((t) => t.turnId === tool.turnId) ||
|
|
286
365
|
group.key === `turn-${tool.turnId}` ||
|
|
287
366
|
(group.user && `restored-${group.user.id}` === tool.turnId));
|
|
288
367
|
if (byTurn) {
|
|
@@ -11,9 +11,6 @@ export { createClosedResourceLoader, createInterviewResourceLoader, tryActivateT
|
|
|
11
11
|
export { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
12
12
|
export { openConsoleAppData, defaultConsoleAppDataRoot } from "./app-data.js";
|
|
13
13
|
export { OperationStore } from "./operation-store.js";
|
|
14
|
-
export { createFrontendSplitOperationAdapter } from "./frontend-split-operation-adapter.js";
|
|
15
|
-
export { createFrontendHumanDecisionAdapter } from "./frontend-human-decision-adapter.js";
|
|
16
|
-
export { FrontendSplitTaskMaterializer } from "../materialize/frontend-split-task-materializer.js";
|
|
17
14
|
export { createOperationEventStore } from "./operation-sse.js";
|
|
18
15
|
export { DagConfirmationStore } from "./dag-confirmation.js";
|
|
19
16
|
export { DraftStore } from "./draft-store.js";
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
|
|
4
|
-
import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
|
|
5
|
-
import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
|
|
6
|
-
import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
|
|
7
3
|
import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
|
|
8
4
|
export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
|
|
9
5
|
function operationFile(appData, operationId) {
|
|
@@ -19,21 +15,16 @@ export function computeActionPayloadHash(action, actionParams) {
|
|
|
19
15
|
export function newOperationId() {
|
|
20
16
|
return `op_${randomBytes(12).toString("hex")}`;
|
|
21
17
|
}
|
|
22
|
-
const sharedWriteChains = new Map();
|
|
23
18
|
export class OperationStore {
|
|
24
19
|
appData;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Per-key write lock. Concurrent update/save/create on the same operation
|
|
22
|
+
* or clientRequestId must not race rename() into the same JSON path
|
|
23
|
+
* (Windows EPERM) or last-write-wins lost patches.
|
|
24
|
+
*/
|
|
25
|
+
writeChains = new Map();
|
|
28
26
|
constructor(appData) {
|
|
29
27
|
this.appData = appData;
|
|
30
|
-
const root = path.resolve(appData.operations);
|
|
31
|
-
let chains = sharedWriteChains.get(root);
|
|
32
|
-
if (!chains) {
|
|
33
|
-
chains = new Map();
|
|
34
|
-
sharedWriteChains.set(root, chains);
|
|
35
|
-
}
|
|
36
|
-
this.writeChains = chains;
|
|
37
28
|
}
|
|
38
29
|
serialize(key, op) {
|
|
39
30
|
const prev = this.writeChains.get(key) ?? Promise.resolve();
|
|
@@ -75,7 +66,6 @@ export class OperationStore {
|
|
|
75
66
|
payloadHash,
|
|
76
67
|
state: "queued",
|
|
77
68
|
createdAt: now,
|
|
78
|
-
revision: 1,
|
|
79
69
|
taskId: input.taskId,
|
|
80
70
|
cliArgs: input.cliArgs,
|
|
81
71
|
...(input.externalCommand
|
|
@@ -87,61 +77,11 @@ export class OperationStore {
|
|
|
87
77
|
return { kind: "created", operation };
|
|
88
78
|
});
|
|
89
79
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Idempotently materialize a split child/integration operation with a
|
|
92
|
-
* caller-specified deterministic operationId (from the split manifest).
|
|
93
|
-
* Unlike create() (random op_<hex> id) and save/update (no create), this is
|
|
94
|
-
* the durable cross-restart idempotency boundary for the production
|
|
95
|
-
* FrontendSplitTaskMaterializer backend: an existing operation file with the
|
|
96
|
-
* same taskId + payloadHash is returned as {created:false}; any drift fails
|
|
97
|
-
* closed instead of overwriting or silently diverging.
|
|
98
|
-
*/
|
|
99
|
-
async ensureSplitMaterializedOperation(input) {
|
|
100
|
-
return this.serialize(input.operationId, async () => {
|
|
101
|
-
const existing = await this.get(input.operationId);
|
|
102
|
-
if (existing) {
|
|
103
|
-
if (existing.taskId !== input.taskId ||
|
|
104
|
-
existing.payloadHash !== input.payloadHash) {
|
|
105
|
-
throw new Error("frontend split operation materialization payload drift");
|
|
106
|
-
}
|
|
107
|
-
return { operation: existing, created: false };
|
|
108
|
-
}
|
|
109
|
-
const now = new Date().toISOString();
|
|
110
|
-
const operation = {
|
|
111
|
-
schemaVersion: 1,
|
|
112
|
-
operationId: input.operationId,
|
|
113
|
-
clientRequestId: `split:${input.operationId}`,
|
|
114
|
-
action: input.action,
|
|
115
|
-
actionParams: input.actionParams,
|
|
116
|
-
payloadHash: input.payloadHash,
|
|
117
|
-
state: "queued",
|
|
118
|
-
createdAt: now,
|
|
119
|
-
revision: 1,
|
|
120
|
-
taskId: input.taskId,
|
|
121
|
-
...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
|
|
122
|
-
};
|
|
123
|
-
await writeSecureJson(operationFile(this.appData, input.operationId), operation);
|
|
124
|
-
return { operation, created: true };
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
80
|
async get(operationId) {
|
|
128
81
|
return readJsonIfExists(operationFile(this.appData, operationId));
|
|
129
82
|
}
|
|
130
83
|
async save(operation) {
|
|
131
84
|
return this.serialize(operation.operationId, async () => {
|
|
132
|
-
const current = await this.get(operation.operationId);
|
|
133
|
-
if (current?.revision !== undefined && operation.revision !== current.revision)
|
|
134
|
-
throw new Error("operation revision may not be changed by save");
|
|
135
|
-
if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
|
|
136
|
-
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
137
|
-
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
138
|
-
return current;
|
|
139
|
-
}
|
|
140
|
-
if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
|
|
141
|
-
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
142
|
-
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
143
|
-
return current;
|
|
144
|
-
}
|
|
145
85
|
await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
|
|
146
86
|
return operation;
|
|
147
87
|
});
|
|
@@ -152,10 +92,6 @@ export class OperationStore {
|
|
|
152
92
|
if (!current) {
|
|
153
93
|
throw new Error(`operation not found: ${operationId}`);
|
|
154
94
|
}
|
|
155
|
-
if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
156
|
-
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
157
|
-
if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
158
|
-
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
159
95
|
const next = {
|
|
160
96
|
...current,
|
|
161
97
|
...patch,
|
|
@@ -170,162 +106,6 @@ export class OperationStore {
|
|
|
170
106
|
return next;
|
|
171
107
|
});
|
|
172
108
|
}
|
|
173
|
-
async materializeFrontendHumanDecision(input) {
|
|
174
|
-
return this.serialize(input.operationId, async () => {
|
|
175
|
-
const current = await this.get(input.operationId);
|
|
176
|
-
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
|
|
177
|
-
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
178
|
-
return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
|
|
179
|
-
}
|
|
180
|
-
const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
|
|
181
|
-
const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
|
|
182
|
-
if (result.outcome !== "applied")
|
|
183
|
-
return result;
|
|
184
|
-
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
|
|
185
|
-
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
186
|
-
return result;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
async consumeFrontendHumanDecision(input) {
|
|
190
|
-
return this.serialize(input.operationId, async () => {
|
|
191
|
-
const current = await this.get(input.operationId);
|
|
192
|
-
if (!current || current.revision === undefined || !current.frontendHumanDecision) {
|
|
193
|
-
const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
194
|
-
return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
|
|
195
|
-
}
|
|
196
|
-
const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
197
|
-
if (current.revision !== input.expectedRecordRevision)
|
|
198
|
-
return { outcome: "rejected", reason: "record revision conflict", projection };
|
|
199
|
-
const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
200
|
-
if (result.outcome !== "applied")
|
|
201
|
-
return result;
|
|
202
|
-
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
|
|
203
|
-
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
204
|
-
return result;
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
async initializeFrontendHumanApprovedSplit(input) {
|
|
208
|
-
return this.serialize(input.operationId, async () => {
|
|
209
|
-
const current = await this.get(input.operationId);
|
|
210
|
-
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
211
|
-
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
|
|
212
|
-
return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
|
|
213
|
-
const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
214
|
-
if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
|
|
215
|
-
return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
|
|
216
|
-
const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
|
|
217
|
-
if (receipt.kind !== "split-materialization-approval")
|
|
218
|
-
return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
|
|
219
|
-
if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
|
|
220
|
-
return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
|
|
221
|
-
const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
222
|
-
if (consumed.outcome !== "applied")
|
|
223
|
-
return consumed;
|
|
224
|
-
const initialized = initializeFrontendSplitParent({
|
|
225
|
-
execution: input.execution,
|
|
226
|
-
manifest: input.manifest,
|
|
227
|
-
expectedOperationRevision: input.execution.operationRevision,
|
|
228
|
-
successorOperationRevision: consumed.projection.revision,
|
|
229
|
-
envelopeTargetDigest: humanProjection.envelope.targetDigest,
|
|
230
|
-
activeWriterOperationId: input.activeWriterOperationId,
|
|
231
|
-
activeProviderOperationId: input.activeProviderOperationId,
|
|
232
|
-
authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
|
|
233
|
-
});
|
|
234
|
-
if (initialized.outcome !== "applied")
|
|
235
|
-
return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
|
|
236
|
-
const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
|
|
237
|
-
const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
|
|
238
|
-
const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
|
|
239
|
-
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
240
|
-
return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
async compareAndSwapFrontendSplit(input) {
|
|
244
|
-
return this.serialize(input.operationId, async () => {
|
|
245
|
-
const current = await this.get(input.operationId);
|
|
246
|
-
if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
|
|
247
|
-
return { outcome: "conflict" };
|
|
248
|
-
const currentProjection = current.frontendSplit;
|
|
249
|
-
if (currentProjection) {
|
|
250
|
-
if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
|
|
251
|
-
return { outcome: "conflict" };
|
|
252
|
-
}
|
|
253
|
-
else if (input.expectedExecutionDigest || input.expectedParentDigest)
|
|
254
|
-
return { outcome: "conflict" };
|
|
255
|
-
const next = currentProjection
|
|
256
|
-
? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
|
|
257
|
-
: frontendSplitProjectionSchema.parse(input.next);
|
|
258
|
-
if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
|
|
259
|
-
throw new Error("split CAS next revision or operation binding mismatch");
|
|
260
|
-
const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
|
|
261
|
-
const record = { ...current, revision: next.revision, frontendSplit: next, state };
|
|
262
|
-
await writeSecureJson(operationFile(this.appData, input.operationId), record);
|
|
263
|
-
return { outcome: "applied", projection: next };
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
async loadChildCloseoutFacts(input) {
|
|
267
|
-
const factsDir = input.closeoutRef.ref;
|
|
268
|
-
const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
|
|
269
|
-
if (facts === undefined)
|
|
270
|
-
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
|
|
271
|
-
const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
|
|
272
|
-
if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
|
|
273
|
-
throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
|
|
274
|
-
const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
|
|
275
|
-
const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
|
|
276
|
-
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
|
|
277
|
-
}
|
|
278
|
-
async settleFrontendSplitAggregate(input) {
|
|
279
|
-
return this.serialize(input.operationId, async () => {
|
|
280
|
-
const record = await this.get(input.operationId);
|
|
281
|
-
if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
|
|
282
|
-
return { outcome: "conflict" };
|
|
283
|
-
const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
|
|
284
|
-
if (authoritative.parent.operationId !== input.operationId
|
|
285
|
-
|| authoritative.revision !== input.expectedRevision
|
|
286
|
-
|| authoritative.execution.digest !== input.expectedExecutionDigest
|
|
287
|
-
|| authoritative.parent.digest !== input.expectedParentDigest
|
|
288
|
-
|| authoritative.manifest.digest !== input.expectedManifestDigest
|
|
289
|
-
|| authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
|
|
290
|
-
|| authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
|
|
291
|
-
return { outcome: "conflict" };
|
|
292
|
-
if (!authoritative.candidateBinding)
|
|
293
|
-
return { outcome: "conflict" };
|
|
294
|
-
const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
|
|
295
|
-
if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
|
|
296
|
-
return { outcome: "conflict" };
|
|
297
|
-
const transaction = await input.prepareAggregate({
|
|
298
|
-
operationId: authoritative.parent.operationId,
|
|
299
|
-
aggregate: expectedAggregate,
|
|
300
|
-
});
|
|
301
|
-
try {
|
|
302
|
-
const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
|
|
303
|
-
const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
|
|
304
|
-
const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
|
|
305
|
-
const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
|
|
306
|
-
await transaction.commit({
|
|
307
|
-
parentRecordPath: operationFile(this.appData, record.operationId),
|
|
308
|
-
parentRecord: nextRecord,
|
|
309
|
-
});
|
|
310
|
-
return { outcome: "applied", projection: next };
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
await transaction.rollback();
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
async initializeFrontendSplit(expectedRevision, projectionValue) {
|
|
319
|
-
const projection = frontendSplitProjectionSchema.parse(projectionValue);
|
|
320
|
-
return this.serialize(projection.parent.operationId, async () => {
|
|
321
|
-
const current = await this.get(projection.parent.operationId);
|
|
322
|
-
if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
|
|
323
|
-
return { outcome: "conflict" };
|
|
324
|
-
const record = { ...current, revision: projection.revision, frontendSplit: projection };
|
|
325
|
-
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
326
|
-
return { outcome: "applied", projection };
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
109
|
async list() {
|
|
330
110
|
const files = await listJsonFiles(this.appData.operations);
|
|
331
111
|
const ops = [];
|