@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.2

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 (209) hide show
  1. package/CHANGELOG.md +61 -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/pi-extension-resolver.js +14 -2
  16. package/dist/executors/shell-executor.js +217 -945
  17. package/dist/executors/shell-write-guard.js +0 -7
  18. package/dist/shared/dag-failure-category.js +0 -12
  19. package/dist/shared/openspec-spec.js +4 -70
  20. package/dist/shared/operator/capabilities.js +0 -7
  21. package/dist/task/config-types.js +7 -34
  22. package/dist/task/contract/adopt.js +0 -4
  23. package/dist/task/contract/import-revision.js +0 -4
  24. package/dist/task/contract/project.js +0 -3
  25. package/dist/task/contract/schema.js +1 -2
  26. package/dist/task/frontend-project-capability.js +20 -203
  27. package/dist/task/runtime.js +2 -5
  28. package/dist/task/source-prepare/build-draft.js +3 -3
  29. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  30. package/dist/task/source-prepare/prepare.js +1 -89
  31. package/dist/task/source-prepare/semantic-intake.js +2 -6
  32. package/dist/task/source-references.js +1 -22
  33. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  34. package/dist/worker/console/chat/pi-runtime.js +235 -13
  35. package/dist/worker/console/chat/repo-browser.js +10 -3
  36. package/dist/worker/console/chat/routes.js +86 -44
  37. package/dist/worker/console/chat/semantic-activity.js +12 -11
  38. package/dist/worker/console/chat/session-stats.js +96 -0
  39. package/dist/worker/console/chat/session-store.js +133 -70
  40. package/dist/worker/console/chat/shortcuts.js +1 -1
  41. package/dist/worker/console/chat/turn-process.js +139 -60
  42. package/dist/worker/console/index.js +0 -3
  43. package/dist/worker/console/operation-store.js +6 -226
  44. package/dist/worker/console/operator-actions.js +7 -169
  45. package/dist/worker/console/pi-plugins.js +3 -1
  46. package/dist/worker/console/server.js +19 -4
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DQ5jlYVU.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BeTVCzH9.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-DassREBi.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-DyeNX8Ek.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-BXb_zxGx.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-B_lofstT.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-B9F9gxGC.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-Byd5zHEQ.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-BqoxMJk9.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-CT7d6YZw.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CdiNyN0N.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-BGw9bpVc.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-DiDVlVB1.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-BhHf-4yK.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-B79pEgwv.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-Cb70EcUV.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-ieUI0-eY.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-ieUI0-eY.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-DBHLnAf1.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-BT_JRsOH.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-Bss-msa3.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-B8Gr24pt.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-BrjU70Jw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cp_JpPGd.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-BWKICEP-.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D3z_UNR_.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-_Ywm390x.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-D9QYQQMB.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BNgXNKAr.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-QRLnkPCg.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-B9fOduNi.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-FSEdtPN2.js} +1 -1
  79. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
  80. package/dist/worker/console/static/assets/index-DtPrwu7M.js +451 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-D5Bjg9dm.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BlNRkWvf.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-BdJiw3SL.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-CsDNlYht.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-8g9dbEmX.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-ChyQH_ut.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-DwYoCTYt.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-T_b2rS17.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-1Cr7WMhH.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-Bq4qM59o.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-ytFgR1RL.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CnUdUVYX.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-_5rJqG6R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-D5imyGbt.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-DvgDn3wL.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BViu7duc.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-kNxnG4_w.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BcULKg2P.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DyTzlcWH.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-DmPveIjt.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-9r-iFy0R.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-D3fIXdvM.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-BCSS9SBg.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  107. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  108. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  109. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  110. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  113. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  114. package/dist/worker/console/workspace-context.js +1 -111
  115. package/dist/worker/observability/read-model.js +4 -0
  116. package/dist/worker/observe/routes.js +4 -0
  117. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  118. package/dist/worker/observe/static/views/dag-inspector.js +2 -7
  119. package/dist/worker/observe/static/views/session-timeline.js +46 -15
  120. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  121. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  122. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  123. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  125. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  126. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  127. package/dist/workflows/dag/frontend-repair.js +432 -1
  128. package/dist/workflows/dag/frontend-review-context.js +15 -261
  129. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  130. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  131. package/dist/workflows/dag/init-hybrid.js +494 -685
  132. package/dist/workflows/dag/interrupt-request.js +0 -7
  133. package/dist/workflows/dag/node-execution.js +4 -732
  134. package/dist/workflows/dag/prompt.js +1 -34
  135. package/dist/workflows/dag/report.js +1 -37
  136. package/dist/workflows/dag/rerun-feedback.js +1 -256
  137. package/dist/workflows/dag/rerun-plan.js +2 -10
  138. package/dist/workflows/dag/rerun-run.js +0 -23
  139. package/dist/workflows/dag/rerun-task.js +0 -29
  140. package/dist/workflows/dag/retry-policy.js +104 -214
  141. package/dist/workflows/dag/runner.js +122 -429
  142. package/dist/workflows/dag/scheduler.js +16 -114
  143. package/dist/workflows/dag/types.js +12 -239
  144. package/dist/workflows/dag/validate.js +12 -28
  145. package/docs/init-surface.manifest.json +12 -30
  146. package/docs/skills/vetted-skill-registry.md +2 -4
  147. package/docs/templates/README.md +0 -2
  148. package/docs/templates/agent-dag-report.schema.json +2 -8
  149. package/docs/templates/agent-dag.schema.json +1 -1
  150. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  151. package/harness.json +3 -3
  152. package/package.json +1 -1
  153. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  154. package/skills/frontend-design-review/SKILL.md +41 -22
  155. package/skills/frontend-implementation/SKILL.md +52 -0
  156. package/skills/frontend-implementation/references/code-standards.md +33 -0
  157. package/skills/frontend-implementation/references/design-spec.md +56 -0
  158. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  159. package/skills/frontend-review/SKILL.md +15 -20
  160. package/skills/frontend-review/references/review-findings.md +7 -6
  161. package/skills/frontend-verification/SKILL.md +1 -1
  162. package/skills/loop-agent/references/command-reference.md +0 -1
  163. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  164. package/dist/commands/dag-follow-up.js +0 -138
  165. package/dist/executors/pi-read-budget-policy.js +0 -239
  166. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  167. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  168. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  171. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  172. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  175. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  176. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  177. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  178. package/dist/workflows/dag/frontend-closeout.js +0 -221
  179. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  180. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  181. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  182. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  183. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  184. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  185. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  186. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  187. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  188. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  189. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  190. package/dist/workflows/dag/frontend-shape.js +0 -427
  191. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  192. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  193. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  194. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  195. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  196. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  197. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  198. package/dist/workflows/dag/recovery-lease.js +0 -80
  199. package/docs/templates/frontend-implementation-dag.json +0 -89
  200. package/docs/templates/spec-registry.schema.json +0 -45
  201. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  202. package/skills/frontend-contract/SKILL.md +0 -23
  203. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  204. package/skills/frontend-plan/SKILL.md +0 -26
  205. package/skills/frontend-plan/references/decision-contract.md +0 -37
  206. package/skills/frontend-plan/references/design-decisions.md +0 -17
  207. package/skills/frontend-scout/SKILL.md +0 -25
  208. package/skills/frontend-scout/references/design-evidence.md +0 -16
  209. package/skills/frontend-scout/references/scout-evidence.md +0 -23
@@ -13,6 +13,7 @@
13
13
  * Read routes (GET capabilities / sessions) are open to loopback.
14
14
  */
15
15
  import { randomUUID } from "node:crypto";
16
+ import { access } from "node:fs/promises";
16
17
  import { parseAssistantContent } from "./assistant-content.js";
17
18
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
18
19
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
@@ -34,7 +35,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
34
35
  import { makeHumanGateCard, } from "./human-gate-card.js";
35
36
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
36
37
  import { projectTaskContext } from "./context-panel.js";
37
- import { deriveChatSessionTelemetry } from "./session-stats.js";
38
+ import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
38
39
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
39
40
  import { sendJson } from "../routes.js";
40
41
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
@@ -99,8 +100,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
99
100
  }
100
101
  return {
101
102
  ...message,
102
- ...(start ? { eventSeq: start.seq, turnId: start.turnId } : {}),
103
- ...(finish ? { finishedSeq: finish.seq } : {}),
103
+ ...(start
104
+ ? {
105
+ eventSeq: start.seq,
106
+ turnId: start.turnId,
107
+ startedAt: start.at,
108
+ }
109
+ : {}),
110
+ ...(finish
111
+ ? { finishedSeq: finish.seq, finishedAt: finish.at }
112
+ : {}),
104
113
  };
105
114
  });
106
115
  }
@@ -460,7 +469,7 @@ function sendRuntimeUnavailable(res, error) {
460
469
  * handled here — only reopen/init readiness failures.
461
470
  */
462
471
  function mapReopenSessionFailure(reopen) {
463
- if (reopen.code === "NO_SESSION_FILE") {
472
+ if (reopen.code === "NO_SESSION_FILE" || reopen.code === "SESSION_FILE_MISSING") {
464
473
  return {
465
474
  status: 404,
466
475
  code: "PI_SESSION_NOT_FOUND",
@@ -480,6 +489,19 @@ function mapReopenSessionFailure(reopen) {
480
489
  message: reopen.message,
481
490
  };
482
491
  }
492
+ /** Drop an orphan catalog record whose promised Pi JSONL is gone. */
493
+ async function healMissingSessionFile(deps, sessionId, reopen) {
494
+ if (reopen && reopen.code !== "NO_SESSION_FILE" && reopen.code !== "SESSION_FILE_MISSING") {
495
+ return;
496
+ }
497
+ if (typeof deps.store.remove !== "function")
498
+ return;
499
+ await deps.store.remove(sessionId).catch(() => undefined);
500
+ }
501
+ async function mapFailedReopen(deps, sessionId, reopen) {
502
+ await healMissingSessionFile(deps, sessionId, reopen);
503
+ return mapReopenSessionFailure(reopen);
504
+ }
483
505
  export async function handleChatCapabilities(_req, res, deps) {
484
506
  const toolSchemas = buildModelCallableToolSchemas();
485
507
  sendJson(res, 200, {
@@ -723,10 +745,7 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
723
745
  sessionFile: record.sessionFile,
724
746
  });
725
747
  if (!reopened.ok) {
726
- const mapped = mapReopenSessionFailure({
727
- code: reopened.code,
728
- message: reopened.message,
729
- });
748
+ const mapped = await mapFailedReopen(deps, sessionId, reopened);
730
749
  sendJson(res, mapped.status, {
731
750
  ok: false,
732
751
  error: { code: mapped.code, message: mapped.message },
@@ -801,6 +820,29 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
801
820
  });
802
821
  return;
803
822
  }
823
+ if (record.sessionFile) {
824
+ try {
825
+ await access(record.sessionFile);
826
+ }
827
+ catch (error) {
828
+ const code = error && typeof error === "object" && "code" in error
829
+ ? String(error.code ?? "")
830
+ : "";
831
+ if (code === "ENOENT") {
832
+ await healMissingSessionFile(deps, sessionId, {
833
+ code: "NO_SESSION_FILE",
834
+ });
835
+ sendJson(res, 404, {
836
+ ok: false,
837
+ error: {
838
+ code: "PI_SESSION_NOT_FOUND",
839
+ message: `Pi session file missing or not openable: ${record.sessionFile}`,
840
+ },
841
+ });
842
+ return;
843
+ }
844
+ }
845
+ }
804
846
  await deps.operationLinker.recoverSession(sessionId);
805
847
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
806
848
  const snapshot = deps.events.snapshot(sessionId);
@@ -828,6 +870,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
828
870
  hasBash: true,
829
871
  sessionStats: telemetry.stats,
830
872
  usage: telemetry.usage,
873
+ turnTelemetry: deriveChatTurnTelemetry(snapshot),
831
874
  contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
832
875
  ? deps.runtime.getContextPressureTelemetry(sessionId)
833
876
  : undefined,
@@ -1056,20 +1099,10 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1056
1099
  }
1057
1100
  const item = claim.deferredTurn;
1058
1101
  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);
1102
+ await deps.runtime.steer(sessionId, item.clientRequestId, item.text, item.images);
1103
+ // session.steer() only acknowledges queueing. Keep the durable item in
1104
+ // dispatching until Pi emits the corresponding user message_start.
1105
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, item, item.revision);
1073
1106
  sendJson(res, 200, {
1074
1107
  ok: true,
1075
1108
  accepted: true,
@@ -1093,6 +1126,21 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1093
1126
  });
1094
1127
  }
1095
1128
  }
1129
+ async function persistAcceptedSteering(deps, sessionId, turnId, userMessage, clientRequestId) {
1130
+ deps.events.append(sessionId, turnId, {
1131
+ kind: "user_message",
1132
+ data: {
1133
+ clientRequestId,
1134
+ steering: true,
1135
+ message: {
1136
+ id: userMessage.id,
1137
+ text: userMessage.text,
1138
+ at: userMessage.at,
1139
+ },
1140
+ },
1141
+ });
1142
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
1143
+ }
1096
1144
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
1097
1145
  * get_state equivalent): exposes the active turn + the latest turn's durable
1098
1146
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -1472,10 +1520,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
1472
1520
  : {}),
1473
1521
  });
1474
1522
  if (!result.ok) {
1475
- const mapped = mapReopenSessionFailure({
1476
- code: result.code,
1477
- message: result.message,
1478
- });
1523
+ const mapped = await mapFailedReopen(deps, sessionId, result);
1479
1524
  sendJson(res, mapped.status, {
1480
1525
  ok: false,
1481
1526
  error: { code: mapped.code, message: mapped.message },
@@ -1561,10 +1606,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1561
1606
  : {}),
1562
1607
  });
1563
1608
  if (!reopen.ok) {
1564
- const mapped = mapReopenSessionFailure({
1565
- code: reopen.code,
1566
- message: reopen.message,
1567
- });
1609
+ const mapped = await mapFailedReopen(deps, sessionId, reopen);
1568
1610
  sendJson(res, mapped.status, {
1569
1611
  ok: false,
1570
1612
  error: { code: mapped.code, message: mapped.message },
@@ -1652,6 +1694,9 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1652
1694
  if (!res.writableEnded)
1653
1695
  writeChatSse(res, persisted);
1654
1696
  },
1697
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
1698
+ await persistAcceptedSteering(deps, sessionId, turnId, userMessage, accepted.clientRequestId);
1699
+ },
1655
1700
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1656
1701
  sessionId,
1657
1702
  turnId,
@@ -1830,10 +1875,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1830
1875
  : {}),
1831
1876
  });
1832
1877
  if (!reopen.ok) {
1833
- const mapped = mapReopenSessionFailure({
1834
- code: reopen.code,
1835
- message: reopen.message,
1836
- });
1878
+ const mapped = await mapFailedReopen(deps, sessionId, reopen);
1837
1879
  sendJson(res, mapped.status, {
1838
1880
  ok: false,
1839
1881
  accepted: false,
@@ -1962,6 +2004,9 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1962
2004
  if (placeholder)
1963
2005
  emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1964
2006
  },
2007
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
2008
+ await persistAcceptedSteering(deps, sessionId, turn.turnId, userMessage, accepted.clientRequestId);
2009
+ },
1965
2010
  persistUserMessage: trustedPayload?.persistUserMessage,
1966
2011
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1967
2012
  sessionId,
@@ -2637,13 +2682,13 @@ async function handleRepoHtmlPreview(res, deps, rawPath) {
2637
2682
  "Cache-Control": "no-store",
2638
2683
  "X-Content-Type-Options": "nosniff",
2639
2684
  "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.
2685
+ // Project scripts may drive tabs and other local interactions, but the
2686
+ // opaque sandbox still withholds same-origin access, forms, popups and
2687
+ // top-level navigation. Network requests remain disabled below.
2643
2688
  "Content-Security-Policy": [
2644
- "sandbox",
2689
+ "sandbox allow-scripts",
2645
2690
  "default-src 'none'",
2646
- "script-src 'none'",
2691
+ `script-src 'unsafe-inline' ${origin}`,
2647
2692
  "object-src 'none'",
2648
2693
  "frame-src 'none'",
2649
2694
  "form-action 'none'",
@@ -2979,7 +3024,7 @@ export async function ensureActiveSession(deps, sessionId) {
2979
3024
  : {}),
2980
3025
  });
2981
3026
  if (!reopened.ok) {
2982
- return { ok: false, ...mapReopenSessionFailure(reopened) };
3027
+ return { ok: false, ...(await mapFailedReopen(deps, sessionId, reopened)) };
2983
3028
  }
2984
3029
  return { ok: true };
2985
3030
  }
@@ -3926,10 +3971,7 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3926
3971
  : {}),
3927
3972
  });
3928
3973
  if (!reopened.ok) {
3929
- const mapped = mapReopenSessionFailure({
3930
- code: reopened.code,
3931
- message: reopened.message,
3932
- });
3974
+ const mapped = await mapFailedReopen(deps, sessionId, reopened);
3933
3975
  sendJson(res, mapped.status, {
3934
3976
  ok: false,
3935
3977
  error: { code: mapped.code, message: mapped.message },
@@ -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;
@@ -38,6 +38,16 @@ export function cleanChatTitle(value) {
38
38
  function recordPath(appData, sessionId) {
39
39
  return path.join(appData.chats, `${sessionId}.json`);
40
40
  }
41
+ /** True when `sessionFile` lives inside this store's dedicated Pi JSONL dir. */
42
+ export function isConsoleOwnedSessionFile(appData, sessionFile) {
43
+ const value = sessionFile?.trim();
44
+ if (!value)
45
+ return false;
46
+ const root = path.resolve(appData.chatSessions);
47
+ const target = path.resolve(value);
48
+ const relative = path.relative(root, target);
49
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
50
+ }
41
51
  export class ChatSessionStore {
42
52
  appData;
43
53
  options;
@@ -237,8 +247,14 @@ export class ChatSessionStore {
237
247
  const record = await this.get(sessionId);
238
248
  if (!record)
239
249
  return false;
250
+ const sessionFile = record.sessionFile;
240
251
  await rm(recordPath(this.appData, sessionId), { force: true });
241
252
  this.options.onRecordChange?.(sessionId, undefined);
253
+ // Record first, then best-effort JSONL unlink. Final consistency is
254
+ // "both gone" — a leftover JSONL is harmless, a leftover record is not.
255
+ if (isConsoleOwnedSessionFile(this.appData, sessionFile) && sessionFile) {
256
+ await rm(sessionFile, { force: true }).catch(() => undefined);
257
+ }
242
258
  return true;
243
259
  });
244
260
  }
@@ -384,6 +400,44 @@ export class ChatSessionStore {
384
400
  return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
385
401
  });
386
402
  }
403
+ /**
404
+ * Commit Pi's steering acceptance as one durable record transition. Removing
405
+ * the dispatching queue item and appending its user message together prevents
406
+ * a crash between two writes from replaying or losing the accepted input.
407
+ */
408
+ async acceptSteeringMessage(sessionId, clientRequestId, text) {
409
+ return this.serialize(sessionId, async () => {
410
+ const record = await this.get(sessionId);
411
+ if (!record)
412
+ throw new Error(`chat session not found: ${sessionId}`);
413
+ const queue = [...(record.deferredTurns ?? [])];
414
+ const index = queue.findIndex((item) => item.clientRequestId === clientRequestId);
415
+ const deferredTurn = index >= 0 ? queue[index] : undefined;
416
+ if (!deferredTurn || deferredTurn.state !== "dispatching")
417
+ return undefined;
418
+ const message = {
419
+ id: `m_${record.messages.length + 1}_${Math.random().toString(36).slice(2, 8)}`,
420
+ role: "user",
421
+ text,
422
+ at: new Date().toISOString(),
423
+ };
424
+ queue.splice(index, 1);
425
+ record.messages.push(message);
426
+ record.deferredTurns = queue;
427
+ record.deferredTurn = queue[0];
428
+ record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
429
+ if (!record.displayTitle)
430
+ record.displayTitle = cleanChatTitle(message.text);
431
+ record.updatedAt = message.at;
432
+ await this.persist(record);
433
+ return {
434
+ record,
435
+ message,
436
+ deferredTurn,
437
+ clearedRevision: deferredTurn.revision + 1,
438
+ };
439
+ });
440
+ }
387
441
  async appendMessage(sessionId, message) {
388
442
  return this.serialize(sessionId, async () => {
389
443
  const record = await this.get(sessionId);
@@ -520,84 +574,91 @@ export async function runChatTurn(options) {
520
574
  // observable to tests and to a reconnecting client reading the record).
521
575
  const pending = [];
522
576
  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);
577
+ let eventChain = Promise.resolve();
578
+ const processEvent = async (event) => {
579
+ if (event.type === "steering_accepted") {
580
+ const committed = await store.acceptSteeringMessage(sessionId, event.clientRequestId, event.text);
581
+ if (committed) {
582
+ await options.onSteeringMessagePersisted?.(committed.record, committed.message, {
583
+ clientRequestId: event.clientRequestId,
584
+ });
533
585
  }
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 };
586
+ return;
587
+ }
588
+ if (event.type === "agent_settled") {
589
+ // The durable operation-ref linker may perform an async canonical
590
+ // operation read. Hold settled until those writes finish so replay
591
+ // never observes agent_settled before its accepted operation ref.
592
+ settledEvent = event;
593
+ }
594
+ else {
595
+ options.onEvent?.(event);
596
+ }
597
+ switch (event.type) {
598
+ case "message_update":
599
+ case "message_end":
600
+ if (event.type === "message_end") {
590
601
  pending.push(store.appendMessage(sessionId, {
591
602
  role: "assistant",
592
- kind: "provider-error",
593
- text: event.providerError.message,
594
- providerError: event.providerError,
603
+ text: event.text,
595
604
  ...(event.content && event.content.length > 0
596
605
  ? { content: event.content }
597
606
  : {}),
598
607
  }));
599
- break;
608
+ }
609
+ break;
610
+ case "tool_call":
611
+ // Forwarded to the caller (SSE) for UI feedback. The actual tool
612
+ // execution happens in the SDK's customTools[].execute(), which
613
+ // routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
614
+ // that would double-execute every tool call (P0.2).
615
+ break;
616
+ case "tool_result": {
617
+ const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
618
+ if (acceptedOperationId && options.onOperationAccepted) {
619
+ pending.push(Promise.resolve(options.onOperationAccepted({
620
+ sessionId,
621
+ toolCallId: event.toolCallId,
622
+ toolName: event.toolName,
623
+ operationId: acceptedOperationId,
624
+ })));
625
+ }
626
+ const resultText = safeStringifyResult(event.result);
627
+ pending.push(store.appendMessage(sessionId, {
628
+ role: "tool",
629
+ text: resultText,
630
+ toolCallId: event.toolCallId,
631
+ toolName: event.toolName,
632
+ isError: event.isError,
633
+ ...(acceptedOperationId ? { operationId: acceptedOperationId } : {}),
634
+ }));
635
+ pending.push(store.recordToolInvocation(sessionId, {
636
+ toolCallId: event.toolCallId,
637
+ toolName: event.toolName,
638
+ ok: !event.isError,
639
+ }));
640
+ break;
600
641
  }
642
+ case "error":
643
+ turnError = { code: "CHAT_TURN_ERROR", message: event.message };
644
+ pending.push(store.appendMessage(sessionId, {
645
+ role: "assistant",
646
+ kind: "provider-error",
647
+ text: event.providerError.message,
648
+ providerError: event.providerError,
649
+ ...(event.content && event.content.length > 0
650
+ ? { content: event.content }
651
+ : {}),
652
+ }));
653
+ break;
654
+ }
655
+ };
656
+ try {
657
+ await runtime.prompt(sessionId, text, (event) => {
658
+ // Pi's event callback is synchronous. Serialize projection work so the
659
+ // accepted steering user_message is durable and sequenced before any
660
+ // assistant/tool event that follows it.
661
+ eventChain = eventChain.then(() => processEvent(event)).catch(() => undefined);
601
662
  }, {
602
663
  signal: options.signal,
603
664
  images: options.images,
@@ -605,10 +666,12 @@ export async function runChatTurn(options) {
605
666
  });
606
667
  }
607
668
  catch (error) {
669
+ await eventChain;
608
670
  await Promise.allSettled(pending);
609
671
  const message = error instanceof Error ? error.message : String(error);
610
672
  return { ok: false, error: { code: "CHAT_TURN_FAILED", message } };
611
673
  }
674
+ await eventChain;
612
675
  await Promise.allSettled(pending);
613
676
  if (settledEvent)
614
677
  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
  },