@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
@@ -34,7 +34,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
34
34
  import { makeHumanGateCard, } from "./human-gate-card.js";
35
35
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
36
36
  import { projectTaskContext } from "./context-panel.js";
37
- import { deriveChatSessionTelemetry } from "./session-stats.js";
37
+ import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
38
38
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
39
39
  import { sendJson } from "../routes.js";
40
40
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
@@ -99,8 +99,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
99
99
  }
100
100
  return {
101
101
  ...message,
102
- ...(start ? { eventSeq: start.seq, turnId: start.turnId } : {}),
103
- ...(finish ? { finishedSeq: finish.seq } : {}),
102
+ ...(start
103
+ ? {
104
+ eventSeq: start.seq,
105
+ turnId: start.turnId,
106
+ startedAt: start.at,
107
+ }
108
+ : {}),
109
+ ...(finish
110
+ ? { finishedSeq: finish.seq, finishedAt: finish.at }
111
+ : {}),
104
112
  };
105
113
  });
106
114
  }
@@ -828,6 +836,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
828
836
  hasBash: true,
829
837
  sessionStats: telemetry.stats,
830
838
  usage: telemetry.usage,
839
+ turnTelemetry: deriveChatTurnTelemetry(snapshot),
831
840
  contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
832
841
  ? deps.runtime.getContextPressureTelemetry(sessionId)
833
842
  : undefined,
@@ -1056,20 +1065,10 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1056
1065
  }
1057
1066
  const item = claim.deferredTurn;
1058
1067
  try {
1059
- await deps.runtime.steer(sessionId, item.text, item.images);
1060
- const userMessage = await deps.store.appendMessage(sessionId, {
1061
- role: "user",
1062
- text: item.text,
1063
- });
1064
- deps.events.append(sessionId, active.turnId, {
1065
- kind: "user_message",
1066
- data: {
1067
- clientRequestId: item.clientRequestId,
1068
- message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
1069
- },
1070
- });
1071
- await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
1072
- await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
1068
+ await deps.runtime.steer(sessionId, item.clientRequestId, item.text, item.images);
1069
+ // session.steer() only acknowledges queueing. Keep the durable item in
1070
+ // dispatching until Pi emits the corresponding user message_start.
1071
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, item, item.revision);
1073
1072
  sendJson(res, 200, {
1074
1073
  ok: true,
1075
1074
  accepted: true,
@@ -1093,6 +1092,21 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1093
1092
  });
1094
1093
  }
1095
1094
  }
1095
+ async function persistAcceptedSteering(deps, sessionId, turnId, userMessage, clientRequestId) {
1096
+ deps.events.append(sessionId, turnId, {
1097
+ kind: "user_message",
1098
+ data: {
1099
+ clientRequestId,
1100
+ steering: true,
1101
+ message: {
1102
+ id: userMessage.id,
1103
+ text: userMessage.text,
1104
+ at: userMessage.at,
1105
+ },
1106
+ },
1107
+ });
1108
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
1109
+ }
1096
1110
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
1097
1111
  * get_state equivalent): exposes the active turn + the latest turn's durable
1098
1112
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -1652,6 +1666,9 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1652
1666
  if (!res.writableEnded)
1653
1667
  writeChatSse(res, persisted);
1654
1668
  },
1669
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
1670
+ await persistAcceptedSteering(deps, sessionId, turnId, userMessage, accepted.clientRequestId);
1671
+ },
1655
1672
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1656
1673
  sessionId,
1657
1674
  turnId,
@@ -1962,6 +1979,9 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1962
1979
  if (placeholder)
1963
1980
  emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1964
1981
  },
1982
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
1983
+ await persistAcceptedSteering(deps, sessionId, turn.turnId, userMessage, accepted.clientRequestId);
1984
+ },
1965
1985
  persistUserMessage: trustedPayload?.persistUserMessage,
1966
1986
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1967
1987
  sessionId,
@@ -2637,13 +2657,13 @@ async function handleRepoHtmlPreview(res, deps, rawPath) {
2637
2657
  "Cache-Control": "no-store",
2638
2658
  "X-Content-Type-Options": "nosniff",
2639
2659
  "Referrer-Policy": "no-referrer",
2640
- // The iframe also has an empty sandbox attribute. This response-level
2641
- // sandbox keeps direct/popup loads isolated, while explicit local-origin
2642
- // style/image/font/media sources let relative project assets render.
2660
+ // Project scripts may drive tabs and other local interactions, but the
2661
+ // opaque sandbox still withholds same-origin access, forms, popups and
2662
+ // top-level navigation. Network requests remain disabled below.
2643
2663
  "Content-Security-Policy": [
2644
- "sandbox",
2664
+ "sandbox allow-scripts",
2645
2665
  "default-src 'none'",
2646
- "script-src 'none'",
2666
+ `script-src 'unsafe-inline' ${origin}`,
2647
2667
  "object-src 'none'",
2648
2668
  "frame-src 'none'",
2649
2669
  "form-action 'none'",
@@ -296,13 +296,6 @@ function operationIdFromArgs(args) {
296
296
  const operationId = record?.operationId;
297
297
  return typeof operationId === "string" && operationId ? operationId : undefined;
298
298
  }
299
- function maxWaitSeconds(args) {
300
- const record = parseJsonRecord(args);
301
- const raw = record?.maxWaitMs;
302
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
303
- return undefined;
304
- return Math.max(1, Math.round(raw / 1000));
305
- }
306
299
  function activityRefForTool(tool) {
307
300
  // Plan A (2026-08-16): server backfills operationId on the persisted tool
308
301
  // entry (and live tool_result frames carry it in the result payload), so
@@ -313,6 +306,17 @@ function activityRefForTool(tool) {
313
306
  operationIdFromArgs(tool.resultText);
314
307
  return operationId ? `operation:${operationId}` : `tool:${tool.id}`;
315
308
  }
309
+ export function dagProgressActivityForTool(tool) {
310
+ if (tool.toolName !== "operationWait")
311
+ return undefined;
312
+ const activityRef = activityRefForTool(tool);
313
+ if (!activityRef.startsWith("operation:"))
314
+ return undefined;
315
+ return {
316
+ id: `dag-progress:${activityRef}`,
317
+ activityRef,
318
+ };
319
+ }
316
320
  function semanticKindForStatus(status) {
317
321
  if (status === "running")
318
322
  return "progress";
@@ -377,14 +381,11 @@ function nodeActivityFromMessages(messages, id, activityRef) {
377
381
  function operationWaitActivity(tool) {
378
382
  const activityRef = activityRefForTool(tool);
379
383
  const id = `activity:${tool.id}`;
380
- const seconds = maxWaitSeconds(tool.args);
381
384
  if (tool.status === "running") {
382
385
  return {
383
386
  id,
384
387
  kind: "progress",
385
- title: seconds
386
- ? `正在等待新的执行进展,最长等待时间 ${seconds} 秒`
387
- : "正在等待新的执行进展",
388
+ title: "正在等待新的执行进展",
388
389
  activityRef,
389
390
  };
390
391
  }
@@ -156,6 +156,102 @@ export function deriveChatSessionTelemetry(events) {
156
156
  export function billedInputTokens(usage) {
157
157
  return usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
158
158
  }
159
+ export function deriveChatTurnTelemetry(events) {
160
+ const byTurn = new Map();
161
+ const turnOf = (turnId) => {
162
+ let entry = byTurn.get(turnId);
163
+ if (!entry) {
164
+ entry = {
165
+ turnId,
166
+ steps: 0,
167
+ llmMs: 0,
168
+ toolMs: 0,
169
+ toolCalls: 0,
170
+ samples: [],
171
+ };
172
+ byTurn.set(turnId, entry);
173
+ }
174
+ return entry;
175
+ };
176
+ const toolStarts = new Map();
177
+ for (const event of events) {
178
+ const at = eventTime(event);
179
+ const turn = turnOf(event.turnId);
180
+ if (event.kind === "usage") {
181
+ const sample = usageSample(event.data);
182
+ if (sample)
183
+ turn.samples.push(sample);
184
+ continue;
185
+ }
186
+ if (event.kind === "user_message") {
187
+ if (turn.startedAt === undefined && at !== undefined)
188
+ turn.startedAt = at;
189
+ continue;
190
+ }
191
+ if (event.kind === "agent_start") {
192
+ if (at !== undefined)
193
+ turn.stepStart = at;
194
+ continue;
195
+ }
196
+ if (event.kind === "tool_call") {
197
+ turn.toolCalls += 1;
198
+ const toolCallId = event.data.toolCallId;
199
+ if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
200
+ toolStarts.set(`${event.turnId}\u0000${toolCallId}`, at);
201
+ }
202
+ continue;
203
+ }
204
+ if (event.kind === "tool_result") {
205
+ const toolCallId = event.data.toolCallId;
206
+ if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
207
+ const key = `${event.turnId}\u0000${toolCallId}`;
208
+ const startedAt = toolStarts.get(key);
209
+ if (startedAt !== undefined && at >= startedAt) {
210
+ turn.toolMs += at - startedAt;
211
+ }
212
+ toolStarts.delete(key);
213
+ // Next assistant step within the same turn starts after this tool.
214
+ turn.stepStart = at;
215
+ }
216
+ continue;
217
+ }
218
+ if (event.kind !== "message_end")
219
+ continue;
220
+ turn.steps += 1;
221
+ const recorded = stepTiming(event.data);
222
+ if (recorded?.llmMs !== undefined)
223
+ turn.llmMs += recorded.llmMs;
224
+ else if (at !== undefined &&
225
+ turn.stepStart !== undefined &&
226
+ at >= turn.stepStart) {
227
+ turn.llmMs += at - turn.stepStart;
228
+ }
229
+ if (recorded?.ttftMs !== undefined) {
230
+ turn.ttftMs = (turn.ttftMs ?? 0) + recorded.ttftMs;
231
+ }
232
+ if (recorded?.decodeMs !== undefined &&
233
+ recorded.decodeMs > 0 &&
234
+ recorded.outputTokens !== undefined) {
235
+ turn.decodeMs = (turn.decodeMs ?? 0) + recorded.decodeMs;
236
+ turn.decodeTokens = (turn.decodeTokens ?? 0) + recorded.outputTokens;
237
+ }
238
+ turn.stepStart = undefined;
239
+ }
240
+ return [...byTurn.values()].map((entry) => ({
241
+ turnId: entry.turnId,
242
+ steps: entry.steps,
243
+ llmMs: entry.llmMs,
244
+ toolMs: entry.toolMs,
245
+ toolCalls: entry.toolCalls,
246
+ ...(entry.ttftMs !== undefined ? { ttftMs: entry.ttftMs } : {}),
247
+ ...(entry.decodeMs !== undefined ? { decodeMs: entry.decodeMs } : {}),
248
+ ...(entry.decodeTokens !== undefined
249
+ ? { decodeTokens: entry.decodeTokens }
250
+ : {}),
251
+ usage: aggregateUsage(entry.samples),
252
+ ...(entry.startedAt !== undefined ? { startedAt: entry.startedAt } : {}),
253
+ }));
254
+ }
159
255
  export function cacheHitPercent(usage) {
160
256
  const billed = billedInputTokens(usage);
161
257
  return billed > 0 ? Math.round((usage.cacheReadTokens / billed) * 100) : null;
@@ -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
  },