@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.
Files changed (206) hide show
  1. package/CHANGELOG.md +47 -267
  2. package/dist/application/dag/generate-task-dag.js +19 -75
  3. package/dist/application/task-lifecycle/advance.js +5 -24
  4. package/dist/application/task-lifecycle/observe.js +17 -171
  5. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  6. package/dist/application/task-lifecycle/recommendations.js +2 -13
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +0 -5
  10. package/dist/commands/client-recovery.js +0 -3
  11. package/dist/commands/init.js +1 -27
  12. package/dist/commands/task-advance.js +0 -19
  13. package/dist/executors/dag-pi-executor.js +231 -3784
  14. package/dist/executors/pi-executor.js +4 -15
  15. package/dist/executors/shell-executor.js +217 -945
  16. package/dist/executors/shell-write-guard.js +0 -7
  17. package/dist/shared/dag-failure-category.js +0 -12
  18. package/dist/shared/openspec-spec.js +4 -70
  19. package/dist/shared/operator/capabilities.js +0 -7
  20. package/dist/task/config-types.js +7 -34
  21. package/dist/task/contract/adopt.js +0 -4
  22. package/dist/task/contract/import-revision.js +0 -4
  23. package/dist/task/contract/project.js +0 -3
  24. package/dist/task/contract/schema.js +1 -2
  25. package/dist/task/frontend-project-capability.js +20 -203
  26. package/dist/task/runtime.js +2 -5
  27. package/dist/task/source-prepare/build-draft.js +3 -3
  28. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  29. package/dist/task/source-prepare/prepare.js +1 -89
  30. package/dist/task/source-prepare/semantic-intake.js +2 -6
  31. package/dist/task/source-references.js +1 -22
  32. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  33. package/dist/worker/console/chat/pi-runtime.js +176 -9
  34. package/dist/worker/console/chat/repo-browser.js +10 -3
  35. package/dist/worker/console/chat/routes.js +42 -22
  36. package/dist/worker/console/chat/semantic-activity.js +12 -11
  37. package/dist/worker/console/chat/session-stats.js +96 -0
  38. package/dist/worker/console/chat/session-store.js +117 -70
  39. package/dist/worker/console/chat/shortcuts.js +1 -1
  40. package/dist/worker/console/chat/turn-process.js +139 -60
  41. package/dist/worker/console/index.js +0 -3
  42. package/dist/worker/console/operation-store.js +6 -226
  43. package/dist/worker/console/operator-actions.js +7 -169
  44. package/dist/worker/console/pi-plugins.js +3 -1
  45. package/dist/worker/console/server.js +0 -3
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-D7oU983t.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BrKSRdFS.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-CiXV7L56.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-D_Hs_gIG.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-DbM-Lqqh.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-D1xajGpe.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-D1M8uqJw.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-4D3RFHT6.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CdwpY_n9.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-BLeLZ9PG.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CeRNEMvc.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-CjkHP8Vy.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-B54ZUQY-.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-4bKaMVmx.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-Cyjfznu_.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CDN-zGFV.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-HxJmz411.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-DtSt5K2w.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-VtkSpRmG.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-Bj4DT4du.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-Bpuk6BmP.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-BHYGv_yO.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-DEx3WwnT.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-Cj0dS5e8.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-B97h8l21.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-DMSFAhBd.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-DhVOCbt6.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-GlakITWL.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-VxPpu9eQ.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js} +1 -1
  78. package/dist/worker/console/static/assets/index-BLzcAykx.js +451 -0
  79. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-BXYuU4Uw.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-C34erCbU.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-DZCAiOU3.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-C9gaTdUm.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CvI0Z17U.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-DMMSIx7j.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-BKKP0EO6.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-C3LiFbl5.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-DNthMjzc.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-miFsr4Tv.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-yXUncmn6.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-DLeWKLak.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-CI3he1R3.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-B7jfDkS5.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BmrZod7J.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-CDOlHVSY.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-Cz-XX1gy.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-caV4cI8C.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-dImPlnIO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CtRYVJcn.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  105. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  106. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  107. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  108. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  109. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  110. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  111. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  112. package/dist/worker/console/workspace-context.js +1 -111
  113. package/dist/worker/observability/read-model.js +4 -0
  114. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  115. package/dist/worker/observe/static/views/dag-inspector.js +2 -7
  116. package/dist/worker/observe/static/views/session-timeline.js +46 -15
  117. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  118. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  119. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  120. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  121. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  122. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  123. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  124. package/dist/workflows/dag/frontend-repair.js +432 -1
  125. package/dist/workflows/dag/frontend-review-context.js +15 -261
  126. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  127. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  128. package/dist/workflows/dag/init-hybrid.js +494 -685
  129. package/dist/workflows/dag/interrupt-request.js +0 -7
  130. package/dist/workflows/dag/node-execution.js +4 -732
  131. package/dist/workflows/dag/prompt.js +1 -34
  132. package/dist/workflows/dag/report.js +1 -37
  133. package/dist/workflows/dag/rerun-feedback.js +1 -256
  134. package/dist/workflows/dag/rerun-plan.js +2 -10
  135. package/dist/workflows/dag/rerun-run.js +0 -23
  136. package/dist/workflows/dag/rerun-task.js +0 -29
  137. package/dist/workflows/dag/retry-policy.js +104 -214
  138. package/dist/workflows/dag/runner.js +122 -429
  139. package/dist/workflows/dag/scheduler.js +16 -114
  140. package/dist/workflows/dag/types.js +12 -239
  141. package/dist/workflows/dag/validate.js +12 -28
  142. package/docs/init-surface.manifest.json +12 -30
  143. package/docs/skills/vetted-skill-registry.md +2 -4
  144. package/docs/templates/README.md +0 -2
  145. package/docs/templates/agent-dag-report.schema.json +2 -8
  146. package/docs/templates/agent-dag.schema.json +1 -1
  147. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  148. package/harness.json +3 -3
  149. package/package.json +1 -1
  150. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  151. package/skills/frontend-design-review/SKILL.md +41 -22
  152. package/skills/frontend-implementation/SKILL.md +52 -0
  153. package/skills/frontend-implementation/references/code-standards.md +33 -0
  154. package/skills/frontend-implementation/references/design-spec.md +56 -0
  155. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  156. package/skills/frontend-review/SKILL.md +15 -20
  157. package/skills/frontend-review/references/review-findings.md +7 -6
  158. package/skills/frontend-verification/SKILL.md +1 -1
  159. package/skills/loop-agent/references/command-reference.md +0 -1
  160. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  161. package/dist/commands/dag-follow-up.js +0 -138
  162. package/dist/executors/pi-read-budget-policy.js +0 -239
  163. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  164. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  165. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  166. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  167. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  168. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  169. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  170. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  171. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  172. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  173. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  174. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  175. package/dist/workflows/dag/frontend-closeout.js +0 -221
  176. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  177. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  178. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  179. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  180. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  181. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  182. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  183. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  184. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  185. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  186. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  187. package/dist/workflows/dag/frontend-shape.js +0 -427
  188. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  189. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  190. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  191. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  192. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  193. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  194. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  195. package/dist/workflows/dag/recovery-lease.js +0 -80
  196. package/docs/templates/frontend-implementation-dag.json +0 -89
  197. package/docs/templates/spec-registry.schema.json +0 -45
  198. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  199. package/skills/frontend-contract/SKILL.md +0 -23
  200. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  201. package/skills/frontend-plan/SKILL.md +0 -26
  202. package/skills/frontend-plan/references/decision-contract.md +0 -37
  203. package/skills/frontend-plan/references/design-decisions.md +0 -17
  204. package/skills/frontend-scout/SKILL.md +0 -25
  205. package/skills/frontend-scout/references/design-evidence.md +0 -16
  206. 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
- finishedAt: createdAt,
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
- pendingTools.push(entry);
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 emitProcessBlocks(blocks, group, processItems, usedToolIds) {
131
- let buffer = [];
132
- for (const block of blocks) {
133
- if (block.type === "thinking") {
134
- flushToolBuffer(processItems, buffer);
135
- buffer = [];
136
- processItems.push({ kind: "thinking", thinking: block.thinking });
137
- continue;
138
- }
139
- if (block.type === "text") {
140
- flushToolBuffer(processItems, buffer);
141
- buffer = [];
142
- if (block.text.trim()) {
143
- processItems.push({ kind: "text", text: block.text });
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
- continue;
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
- const tool = group.tools.find((entry) => entry.id === block.toolCallId);
148
- if (tool && !usedToolIds.has(tool.id)) {
149
- buffer.push(tool);
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
- emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
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
- emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
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
- appendAssistantUnmatchedTools(group, finalAssistant, processItems, usedToolIds);
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
- // Bounded fallback for legacy restored rows with no assistant attribution:
199
- // keep them at this turn's end in stable server order instead of randomly
200
- // interleaving them into earlier stages.
201
- const orphans = group.tools.filter((tool) => !usedToolIds.has(tool.id));
202
- if (orphans.length > 0)
203
- flushToolBuffer(processItems, orphans);
204
- group.processItems = processItems;
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.tools.some((t) => t.turnId === tool.turnId) ||
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
- /** Shared per-store-root, per-key write lock. Multiple OperationStore instances
26
- * in one worker process must observe the same CAS critical section. */
27
- writeChains;
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 = [];