@tea-agent/loop-agent 0.42.0-next.2 → 0.42.0-next.3

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 (193) hide show
  1. package/CHANGELOG.md +242 -1
  2. package/dist/application/dag/generate-task-dag.js +75 -19
  3. package/dist/application/task-lifecycle/advance.js +24 -5
  4. package/dist/application/task-lifecycle/observe.js +171 -17
  5. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +7 -0
  9. package/dist/cli/program.js +5 -0
  10. package/dist/commands/client-recovery.js +3 -0
  11. package/dist/commands/dag-follow-up.js +138 -0
  12. package/dist/commands/init.js +27 -1
  13. package/dist/commands/task-advance.js +19 -0
  14. package/dist/executors/dag-pi-executor.js +3784 -231
  15. package/dist/executors/pi-executor.js +15 -4
  16. package/dist/executors/pi-read-budget-policy.js +239 -0
  17. package/dist/executors/shell-executor.js +942 -187
  18. package/dist/executors/shell-write-guard.js +7 -0
  19. package/dist/infrastructure/console/operation-store.js +226 -6
  20. package/dist/shared/dag-failure-category.js +12 -0
  21. package/dist/shared/openspec-spec.js +70 -4
  22. package/dist/shared/operator/capabilities.js +7 -0
  23. package/dist/task/config-types.js +34 -5
  24. package/dist/task/contract/adopt.js +4 -0
  25. package/dist/task/contract/import-revision.js +4 -0
  26. package/dist/task/contract/project.js +3 -0
  27. package/dist/task/contract/schema.js +2 -1
  28. package/dist/task/frontend-project-capability.js +203 -20
  29. package/dist/task/runtime.js +5 -2
  30. package/dist/task/source-prepare/build-draft.js +3 -3
  31. package/dist/task/source-prepare/fragment-inventory.js +15 -9
  32. package/dist/task/source-prepare/prepare.js +89 -1
  33. package/dist/task/source-prepare/semantic-intake.js +6 -2
  34. package/dist/task/source-references.js +22 -1
  35. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  36. package/dist/worker/console/chat/pi-runtime.js +173 -7
  37. package/dist/worker/console/chat/repo-browser.js +10 -3
  38. package/dist/worker/console/chat/routes.js +30 -19
  39. package/dist/worker/console/chat/session-store.js +117 -70
  40. package/dist/worker/console/chat/shortcuts.js +1 -1
  41. package/dist/worker/console/chat/turn-process.js +111 -56
  42. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  43. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  44. package/dist/worker/console/index.js +3 -0
  45. package/dist/worker/console/operator-actions.js +169 -7
  46. package/dist/worker/console/server.js +3 -0
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
  79. package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
  80. package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  107. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  108. package/dist/worker/console/workspace-context.js +111 -1
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  111. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  112. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  113. package/dist/workflows/dag/frontend-closeout.js +221 -0
  114. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  115. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  116. package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
  117. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
  119. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  120. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  121. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  122. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  123. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  124. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  125. package/dist/workflows/dag/frontend-repair.js +1 -432
  126. package/dist/workflows/dag/frontend-review-context.js +261 -15
  127. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  128. package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
  129. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  130. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  131. package/dist/workflows/dag/frontend-shape.js +435 -0
  132. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  133. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  134. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  135. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  136. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  137. package/dist/workflows/dag/frontend-verification-trace.js +249 -24
  138. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  139. package/dist/workflows/dag/frontend-writer-admission.js +285 -0
  140. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  141. package/dist/workflows/dag/init-hybrid.js +677 -492
  142. package/dist/workflows/dag/interrupt-request.js +7 -0
  143. package/dist/workflows/dag/node-execution.js +732 -4
  144. package/dist/workflows/dag/prompt.js +34 -1
  145. package/dist/workflows/dag/recovery-lease.js +80 -0
  146. package/dist/workflows/dag/report.js +37 -1
  147. package/dist/workflows/dag/rerun-feedback.js +256 -1
  148. package/dist/workflows/dag/rerun-plan.js +17 -6
  149. package/dist/workflows/dag/rerun-run.js +23 -0
  150. package/dist/workflows/dag/rerun-task.js +29 -0
  151. package/dist/workflows/dag/retry-policy.js +214 -104
  152. package/dist/workflows/dag/runner.js +433 -126
  153. package/dist/workflows/dag/scheduler.js +114 -16
  154. package/dist/workflows/dag/types.js +239 -12
  155. package/dist/workflows/dag/validate.js +28 -12
  156. package/docs/examples/README.md +5 -0
  157. package/docs/init-surface.manifest.json +30 -12
  158. package/docs/skills/vetted-skill-registry.md +4 -2
  159. package/docs/templates/README.md +2 -0
  160. package/docs/templates/agent-dag-report.schema.json +8 -2
  161. package/docs/templates/agent-dag.schema.json +1 -1
  162. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  163. package/docs/templates/frontend-implementation-dag.json +89 -0
  164. package/docs/templates/spec-registry.schema.json +45 -0
  165. package/harness.json +1 -1
  166. package/package.json +1 -1
  167. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  168. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  169. package/skills/frontend-contract/SKILL.md +23 -0
  170. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  171. package/skills/frontend-design-review/SKILL.md +22 -41
  172. package/skills/frontend-plan/SKILL.md +26 -0
  173. package/skills/frontend-plan/references/decision-contract.md +37 -0
  174. package/skills/frontend-plan/references/design-decisions.md +17 -0
  175. package/skills/frontend-review/SKILL.md +20 -15
  176. package/skills/frontend-review/references/review-findings.md +6 -7
  177. package/skills/frontend-scout/SKILL.md +25 -0
  178. package/skills/frontend-scout/references/design-evidence.md +16 -0
  179. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  180. package/skills/frontend-verification/SKILL.md +1 -1
  181. package/skills/loop-agent/references/command-reference.md +1 -0
  182. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  183. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  185. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  186. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  187. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  188. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  189. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  190. package/skills/frontend-implementation/SKILL.md +0 -52
  191. package/skills/frontend-implementation/references/code-standards.md +0 -33
  192. package/skills/frontend-implementation/references/design-spec.md +0 -56
  193. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -384,6 +384,44 @@ export class ChatSessionStore {
384
384
  return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
385
385
  });
386
386
  }
387
+ /**
388
+ * Commit Pi's steering acceptance as one durable record transition. Removing
389
+ * the dispatching queue item and appending its user message together prevents
390
+ * a crash between two writes from replaying or losing the accepted input.
391
+ */
392
+ async acceptSteeringMessage(sessionId, clientRequestId, text) {
393
+ return this.serialize(sessionId, async () => {
394
+ const record = await this.get(sessionId);
395
+ if (!record)
396
+ throw new Error(`chat session not found: ${sessionId}`);
397
+ const queue = [...(record.deferredTurns ?? [])];
398
+ const index = queue.findIndex((item) => item.clientRequestId === clientRequestId);
399
+ const deferredTurn = index >= 0 ? queue[index] : undefined;
400
+ if (!deferredTurn || deferredTurn.state !== "dispatching")
401
+ return undefined;
402
+ const message = {
403
+ id: `m_${record.messages.length + 1}_${Math.random().toString(36).slice(2, 8)}`,
404
+ role: "user",
405
+ text,
406
+ at: new Date().toISOString(),
407
+ };
408
+ queue.splice(index, 1);
409
+ record.messages.push(message);
410
+ record.deferredTurns = queue;
411
+ record.deferredTurn = queue[0];
412
+ record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
413
+ if (!record.displayTitle)
414
+ record.displayTitle = cleanChatTitle(message.text);
415
+ record.updatedAt = message.at;
416
+ await this.persist(record);
417
+ return {
418
+ record,
419
+ message,
420
+ deferredTurn,
421
+ clearedRevision: deferredTurn.revision + 1,
422
+ };
423
+ });
424
+ }
387
425
  async appendMessage(sessionId, message) {
388
426
  return this.serialize(sessionId, async () => {
389
427
  const record = await this.get(sessionId);
@@ -520,84 +558,91 @@ export async function runChatTurn(options) {
520
558
  // observable to tests and to a reconnecting client reading the record).
521
559
  const pending = [];
522
560
  let settledEvent;
523
- try {
524
- await runtime.prompt(sessionId, text, (event) => {
525
- if (event.type === "agent_settled") {
526
- // The durable operation-ref linker may perform an async canonical
527
- // operation read. Hold settled until those writes finish so replay
528
- // never observes agent_settled before its accepted operation ref.
529
- settledEvent = event;
530
- }
531
- else {
532
- options.onEvent?.(event);
561
+ let eventChain = Promise.resolve();
562
+ const processEvent = async (event) => {
563
+ if (event.type === "steering_accepted") {
564
+ const committed = await store.acceptSteeringMessage(sessionId, event.clientRequestId, event.text);
565
+ if (committed) {
566
+ await options.onSteeringMessagePersisted?.(committed.record, committed.message, {
567
+ clientRequestId: event.clientRequestId,
568
+ });
533
569
  }
534
- switch (event.type) {
535
- case "message_update":
536
- case "message_end":
537
- if (event.type === "message_end") {
538
- pending.push(store.appendMessage(sessionId, {
539
- role: "assistant",
540
- text: event.text,
541
- ...(event.content && event.content.length > 0
542
- ? { content: event.content }
543
- : {}),
544
- }));
545
- }
546
- break;
547
- case "tool_call":
548
- // Forwarded to the caller (SSE) for UI feedback. The actual tool
549
- // execution happens in the SDK's customTools[].execute(), which
550
- // routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
551
- // that would double-execute every tool call (P0.2).
552
- break;
553
- case "tool_result": {
554
- const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
555
- if (acceptedOperationId && options.onOperationAccepted) {
556
- pending.push(Promise.resolve(options.onOperationAccepted({
557
- sessionId,
558
- toolCallId: event.toolCallId,
559
- toolName: event.toolName,
560
- operationId: acceptedOperationId,
561
- })));
562
- }
563
- // tool_execution_end carries the real result of the tool call.
564
- // Persist exactly once (the tool message + audit trail). This is
565
- // the single point of record for tool outcomes (P0.2).
566
- const resultText = safeStringifyResult(event.result);
567
- // ChatSessionStore serializes writes per session, so these two calls
568
- // are safe to fire concurrently — neither clobbers the other.
569
- pending.push(store.appendMessage(sessionId, {
570
- role: "tool",
571
- text: resultText,
572
- toolCallId: event.toolCallId,
573
- toolName: event.toolName,
574
- isError: event.isError,
575
- // Durable tool→operation link (plan A): the restored timeline can
576
- // resolve operation:<id> refs without the live operation-ref event.
577
- ...(acceptedOperationId
578
- ? { operationId: acceptedOperationId }
579
- : {}),
580
- }));
581
- pending.push(store.recordToolInvocation(sessionId, {
582
- toolCallId: event.toolCallId,
583
- toolName: event.toolName,
584
- ok: !event.isError,
585
- }));
586
- break;
587
- }
588
- case "error":
589
- turnError = { code: "CHAT_TURN_ERROR", message: event.message };
570
+ return;
571
+ }
572
+ if (event.type === "agent_settled") {
573
+ // The durable operation-ref linker may perform an async canonical
574
+ // operation read. Hold settled until those writes finish so replay
575
+ // never observes agent_settled before its accepted operation ref.
576
+ settledEvent = event;
577
+ }
578
+ else {
579
+ options.onEvent?.(event);
580
+ }
581
+ switch (event.type) {
582
+ case "message_update":
583
+ case "message_end":
584
+ if (event.type === "message_end") {
590
585
  pending.push(store.appendMessage(sessionId, {
591
586
  role: "assistant",
592
- kind: "provider-error",
593
- text: event.providerError.message,
594
- providerError: event.providerError,
587
+ text: event.text,
595
588
  ...(event.content && event.content.length > 0
596
589
  ? { content: event.content }
597
590
  : {}),
598
591
  }));
599
- break;
592
+ }
593
+ break;
594
+ case "tool_call":
595
+ // Forwarded to the caller (SSE) for UI feedback. The actual tool
596
+ // execution happens in the SDK's customTools[].execute(), which
597
+ // routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
598
+ // that would double-execute every tool call (P0.2).
599
+ break;
600
+ case "tool_result": {
601
+ const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
602
+ if (acceptedOperationId && options.onOperationAccepted) {
603
+ pending.push(Promise.resolve(options.onOperationAccepted({
604
+ sessionId,
605
+ toolCallId: event.toolCallId,
606
+ toolName: event.toolName,
607
+ operationId: acceptedOperationId,
608
+ })));
609
+ }
610
+ const resultText = safeStringifyResult(event.result);
611
+ pending.push(store.appendMessage(sessionId, {
612
+ role: "tool",
613
+ text: resultText,
614
+ toolCallId: event.toolCallId,
615
+ toolName: event.toolName,
616
+ isError: event.isError,
617
+ ...(acceptedOperationId ? { operationId: acceptedOperationId } : {}),
618
+ }));
619
+ pending.push(store.recordToolInvocation(sessionId, {
620
+ toolCallId: event.toolCallId,
621
+ toolName: event.toolName,
622
+ ok: !event.isError,
623
+ }));
624
+ break;
600
625
  }
626
+ case "error":
627
+ turnError = { code: "CHAT_TURN_ERROR", message: event.message };
628
+ pending.push(store.appendMessage(sessionId, {
629
+ role: "assistant",
630
+ kind: "provider-error",
631
+ text: event.providerError.message,
632
+ providerError: event.providerError,
633
+ ...(event.content && event.content.length > 0
634
+ ? { content: event.content }
635
+ : {}),
636
+ }));
637
+ break;
638
+ }
639
+ };
640
+ try {
641
+ await runtime.prompt(sessionId, text, (event) => {
642
+ // Pi's event callback is synchronous. Serialize projection work so the
643
+ // accepted steering user_message is durable and sequenced before any
644
+ // assistant/tool event that follows it.
645
+ eventChain = eventChain.then(() => processEvent(event)).catch(() => undefined);
601
646
  }, {
602
647
  signal: options.signal,
603
648
  images: options.images,
@@ -605,10 +650,12 @@ export async function runChatTurn(options) {
605
650
  });
606
651
  }
607
652
  catch (error) {
653
+ await eventChain;
608
654
  await Promise.allSettled(pending);
609
655
  const message = error instanceof Error ? error.message : String(error);
610
656
  return { ok: false, error: { code: "CHAT_TURN_FAILED", message } };
611
657
  }
658
+ await eventChain;
612
659
  await Promise.allSettled(pending);
613
660
  if (settledEvent)
614
661
  options.onEvent?.(settledEvent);
@@ -37,7 +37,7 @@ export const SHORTCUTS = [
37
37
  {
38
38
  command: "/reload",
39
39
  label: "Reload session",
40
- description: "重新加载当前 Pi 会话的设置、扩展、技能、提示词、主题与上下文文件(不产生消息)",
40
+ description: "重新加载当前 Pi 会话的设置、扩展、技能、提示词、主题、上下文文件及模型与认证配置(不产生消息)",
41
41
  action: "reload",
42
42
  source: "operator",
43
43
  },
@@ -96,7 +96,10 @@ export function hydrateSessionThread(raw) {
96
96
  ...(item.operationId ? { operationId: item.operationId } : {}),
97
97
  };
98
98
  toolCalls.push(entry);
99
- 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);
100
103
  }
101
104
  }
102
105
  return { messages, toolCalls };
@@ -151,48 +154,81 @@ function collapseDagProgressItems(items) {
151
154
  return true;
152
155
  });
153
156
  }
154
- function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
155
- let buffer = [];
156
- for (const block of blocks) {
157
- if (block.type === "thinking") {
158
- flushToolBuffer(processItems, buffer);
159
- buffer = [];
160
- processItems.push({ kind: "thinking", thinking: block.thinking });
161
- continue;
162
- }
163
- if (block.type === "text") {
164
- flushToolBuffer(processItems, buffer);
165
- buffer = [];
166
- if (block.text.trim()) {
167
- processItems.push({ kind: "text", text: block.text });
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;
168
187
  }
169
- 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;
170
211
  }
171
- const tool = group.tools.find((entry) => entry.id === block.toolCallId);
172
- if (tool && !usedToolIds.has(tool.id)) {
173
- 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)) {
174
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
+ });
175
230
  }
176
- }
177
- flushToolBuffer(processItems, buffer);
178
- }
179
- /**
180
- * Insert tools attributed to this assistant stage but without a matching
181
- * content toolCall block (live SSE tool_call frames often arrive before the
182
- * assistant content does). They follow the stage's thinking/text instead of
183
- * sinking to the end of the whole turn.
184
- */
185
- function appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds) {
186
- const unmatched = group.tools.filter((tool) => !usedToolIds.has(tool.id) && tool.assistantMessageId === assistant.id);
187
- if (unmatched.length === 0)
188
- return;
189
- for (const tool of unmatched)
190
- usedToolIds.add(tool.id);
191
- flushToolBuffer(processItems, unmatched);
192
- }
193
- function fillProcessDetails(group) {
194
- const usedToolIds = new Set();
195
- const processItems = [];
231
+ };
196
232
  const lastAssistant = group.assistants.at(-1);
197
233
  const finalAssistant = lastAssistant?.kind === "provider-error" ? undefined : lastAssistant;
198
234
  const finalIndex = finalAssistant
@@ -202,29 +238,46 @@ function fillProcessDetails(group) {
202
238
  if (index === finalIndex)
203
239
  continue;
204
240
  if (assistant.kind === "provider-error" && assistant.providerError) {
205
- emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
206
- appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
207
- processItems.push({
208
- kind: "provider-error",
209
- error: assistant.providerError,
210
- });
241
+ addAssistantNode(assistant, getDisplayableAssistantBlocks(assistant.content ?? []), true);
211
242
  continue;
212
243
  }
213
- emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
214
- appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
244
+ addAssistantNode(assistant, getDisplayableAssistantBlocks(assistant.content ?? []));
215
245
  }
216
246
  const blocks = finalAssistant?.content ?? [];
217
247
  const split = splitFinalAssistantBlocks(blocks, { isStreaming: group.live });
218
- emitProcessBlocks(split.processBlocks, group, processItems, usedToolIds);
219
248
  if (finalAssistant) {
220
- appendAssistantUnmatchedTools(group, finalAssistant, processItems, usedToolIds);
249
+ addAssistantNode(finalAssistant, split.processBlocks);
221
250
  }
222
- // Bounded fallback for legacy restored rows with no assistant attribution:
223
- // keep them at this turn's end in stable server order instead of randomly
224
- // interleaving them into earlier stages.
225
- const orphans = group.tools.filter((tool) => !usedToolIds.has(tool.id));
226
- if (orphans.length > 0)
227
- flushToolBuffer(processItems, orphans);
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
+ });
265
+ }
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);
228
281
  group.processItems = collapseDagProgressItems(processItems);
229
282
  if (!finalAssistant)
230
283
  return;
@@ -306,7 +359,9 @@ export function buildTurnProcessGroups(input) {
306
359
  if (assigned.has(tool.id))
307
360
  continue;
308
361
  if (tool.turnId) {
309
- 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) ||
310
365
  group.key === `turn-${tool.turnId}` ||
311
366
  (group.user && `restored-${group.user.id}` === tool.turnId));
312
367
  if (byTurn) {
@@ -0,0 +1,19 @@
1
+ import { parseFrontendHumanDecisionProjection } from "../../workflows/dag/frontend-human-decision.js";
2
+ /** Worker/Console persistence seam. Canonical validation and freshness remain in the workflow service. */
3
+ export function createFrontendHumanDecisionAdapter(store) {
4
+ return {
5
+ async load(operationId) {
6
+ const record = await store.get(operationId);
7
+ if (!record || record.revision === undefined || !record.frontendHumanDecision)
8
+ return undefined;
9
+ return { revision: record.revision, projection: parseFrontendHumanDecisionProjection(record.frontendHumanDecision) };
10
+ },
11
+ materialize(input) {
12
+ return store.materializeFrontendHumanDecision(input);
13
+ },
14
+ consume(input) {
15
+ return store.consumeFrontendHumanDecision(input);
16
+ },
17
+ initializeSplit: store.initializeFrontendHumanApprovedSplit.bind(store),
18
+ };
19
+ }
@@ -0,0 +1,20 @@
1
+ import { FrontendSplitOrchestrator, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
2
+ /** Worker/Console boundary adapter. All orchestration policy remains canonical. */
3
+ export function createFrontendSplitOperationAdapter(dependencies) {
4
+ const ports = {
5
+ load: async (operationId) => {
6
+ const record = await dependencies.store.get(operationId);
7
+ return record?.frontendSplit === undefined ? undefined : frontendSplitProjectionSchema.parse(record.frontendSplit);
8
+ },
9
+ compareAndSwap: (input) => dependencies.store.compareAndSwapFrontendSplit(input),
10
+ materialize: async (request) => {
11
+ const result = await dependencies.materializer.materialize(request);
12
+ return { taskId: result.taskId, operationId: result.operationId };
13
+ },
14
+ schedule: dependencies.schedule,
15
+ auditStale: dependencies.auditStale,
16
+ loadChildCloseoutFacts: (input) => dependencies.store.loadChildCloseoutFacts(input),
17
+ settleAggregate: (input) => dependencies.store.settleFrontendSplitAggregate({ ...input, prepareAggregate: dependencies.prepareAggregate }),
18
+ };
19
+ return new FrontendSplitOrchestrator(ports);
20
+ }
@@ -11,6 +11,9 @@ export { createClosedResourceLoader, createInterviewResourceLoader, tryActivateT
11
11
  export { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
12
12
  export { openConsoleAppData, defaultConsoleAppDataRoot } from "../../infrastructure/console/app-data.js";
13
13
  export { OperationStore } from "../../infrastructure/console/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";
14
17
  export { createOperationEventStore } from "./operation-sse.js";
15
18
  export { DagConfirmationStore } from "./dag-confirmation.js";
16
19
  export { DraftStore } from "./draft-store.js";