@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.10

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 (226) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -15,18 +15,18 @@
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { access } from "node:fs/promises";
17
17
  import { parseAssistantContent } from "./assistant-content.js";
18
- import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
18
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, OPERATOR_CHAT_SQL_TOOL_CAPABILITIES, } from "./tools.js";
19
19
  import { InvalidChatSessionModeError, SESSION_MODE_ACTION_DENIED_CODE, SESSION_MODE_ACTION_DENIED_MESSAGE, SESSION_MODE_IMMUTABLE_CODE, UnknownChatSessionModeError, isWorkflowSessionMode, parseCreateSessionMode, readStoredSessionMode, shouldAcceptOperatorSnapshot, } from "./session-mode.js";
20
20
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
21
21
  import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
22
- import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
22
+ import { projectUserTurnOutline } from "./user-turn-outline.js";
23
+ import { hashChatTurnPayload, projectChatToolEventPayload, projectUnknownChatEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
23
24
  import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
24
25
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
25
26
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
26
27
  import { ComposerDraftStore } from "./composer-draft-store.js";
27
- import { walkRepoFiles } from "./repo-walk.js";
28
- import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
29
- import { listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
28
+ import { scrubSecrets } from "./explore-tools.js";
29
+ import { listChatFilePalette, listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
30
30
  import { projectRuntimeContext } from "./runtime-context.js";
31
31
  import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
32
32
  import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
@@ -36,6 +36,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
36
36
  import { makeHumanGateCard, } from "./human-gate-card.js";
37
37
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
38
38
  import { projectTaskContext } from "./context-panel.js";
39
+ import { deriveContextInsights, estimateTokensFromText, } from "./context-insights.js";
39
40
  import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
40
41
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
41
42
  import { sendJson } from "../routes.js";
@@ -49,6 +50,7 @@ import { TerminalSessionRegistry } from "./terminal-sessions.js";
49
50
  import { handleTerminalRequest } from "./terminal-routes.js";
50
51
  import { handleScmRequest } from "./scm-routes.js";
51
52
  import { handleBrowserRequest } from "./browser-routes.js";
53
+ import { DbConnectionError } from "./db-connections.js";
52
54
  function projectBrowserChatSession(record) {
53
55
  const { telemetrySessionId, ...projected } = record;
54
56
  void telemetrySessionId;
@@ -64,6 +66,14 @@ function reopenInitFromRecord(record) {
64
66
  sessionMode: record.sessionMode,
65
67
  };
66
68
  }
69
+ function workspaceProjectionFromRecord(record) {
70
+ return {
71
+ sessionMode: record.sessionMode,
72
+ ...(record.modelProvider ? { modelProvider: record.modelProvider } : {}),
73
+ ...(record.modelId ? { modelId: record.modelId } : {}),
74
+ ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel } : {}),
75
+ };
76
+ }
67
77
  async function readChatSessionMode(deps, sessionId) {
68
78
  let record;
69
79
  try {
@@ -196,6 +206,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
196
206
  ...(finish
197
207
  ? { finishedSeq: finish.seq, finishedAt: finish.at }
198
208
  : {}),
209
+ ...(start && typeof start.data.step === "number"
210
+ ? { step: start.data.step }
211
+ : {}),
212
+ ...(start &&
213
+ typeof start.data.parentToolCallId ===
214
+ "string"
215
+ ? {
216
+ parentToolCallId: start.data.parentToolCallId,
217
+ }
218
+ : {}),
199
219
  };
200
220
  });
201
221
  }
@@ -212,6 +232,43 @@ function turnSummary(turn) {
212
232
  abortRequestedAt: turn.abortRequestedAt ?? null,
213
233
  };
214
234
  }
235
+ function turnStateSummary(turn) {
236
+ return {
237
+ turnId: turn.turnId,
238
+ state: turn.state === "queued" ? "running" : turn.state,
239
+ };
240
+ }
241
+ function turnStatesForIds(events, sessionId, turnIds) {
242
+ const projected = new Map();
243
+ for (const turnId of turnIds) {
244
+ if (!turnId || projected.has(turnId))
245
+ continue;
246
+ const turn = events.getTurn(sessionId, turnId);
247
+ if (turn)
248
+ projected.set(turnId, turnStateSummary(turn));
249
+ }
250
+ return [...projected.values()];
251
+ }
252
+ const MAX_STATE_QUERY_TURN_IDS = 8;
253
+ const MAX_STATE_QUERY_TURN_ID_CHARS = 240;
254
+ function requestedStateTurnIds(req) {
255
+ const result = [];
256
+ const seen = new Set();
257
+ const url = new URL(req.url ?? "/", "http://localhost");
258
+ for (const raw of url.searchParams.getAll("turnId")) {
259
+ const turnId = raw.trim();
260
+ if (!turnId ||
261
+ turnId.length > MAX_STATE_QUERY_TURN_ID_CHARS ||
262
+ seen.has(turnId)) {
263
+ continue;
264
+ }
265
+ seen.add(turnId);
266
+ result.push(turnId);
267
+ if (result.length >= MAX_STATE_QUERY_TURN_IDS)
268
+ break;
269
+ }
270
+ return result;
271
+ }
215
272
  function isTerminalTurnState(state) {
216
273
  return state === "settled" || state === "aborted" || state === "failed";
217
274
  }
@@ -242,10 +299,99 @@ function writeChatSse(res, event) {
242
299
  /** Map a runtime ChatTurnEvent to the persisted event-store partial.
243
300
  *
244
301
  * Returns undefined for heartbeat (not persisted — keep-alive only). */
245
- function turnEventToStorePartial(event) {
302
+ export function turnEventToStorePartial(event) {
246
303
  switch (event.type) {
247
304
  case "agent_start":
248
305
  return { kind: "agent_start", data: {} };
306
+ case "step_start":
307
+ case "step_end":
308
+ return {
309
+ kind: event.type,
310
+ data: {
311
+ step: Number(event.step ?? 1),
312
+ ...(event.type === "step_end"
313
+ ? { outcome: String(event.outcome ?? "completed") }
314
+ : {}),
315
+ },
316
+ };
317
+ case "model_retry":
318
+ {
319
+ const retryState = event.state === "completed" || event.state === "failed"
320
+ ? event.state
321
+ : "scheduled";
322
+ return {
323
+ kind: "model_retry",
324
+ data: {
325
+ step: Number(event.step ?? 1),
326
+ attempt: Number(event.attempt ?? 1),
327
+ ...(typeof event.maxAttempts === "number"
328
+ ? { maxAttempts: event.maxAttempts }
329
+ : {}),
330
+ ...(typeof event.delayMs === "number" ? { delayMs: event.delayMs } : {}),
331
+ ...(typeof event.errorMessage === "string"
332
+ ? { errorMessage: event.errorMessage }
333
+ : {}),
334
+ state: retryState,
335
+ ...(typeof event.success === "boolean" ? { success: event.success } : {}),
336
+ },
337
+ };
338
+ }
339
+ case "turn_max_tokens":
340
+ return {
341
+ kind: "turn_max_tokens",
342
+ data: { step: Number(event.step ?? 1) },
343
+ };
344
+ case "turn_error":
345
+ return {
346
+ kind: "turn_error",
347
+ data: {
348
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
349
+ ...(typeof event.code === "string" ? { code: event.code } : {}),
350
+ message: String(event.message ?? "模型请求失败"),
351
+ },
352
+ };
353
+ case "request_context":
354
+ return {
355
+ kind: "request_context",
356
+ data: {
357
+ requestId: String(event.requestId ?? ""),
358
+ step: Number(event.step ?? 1),
359
+ ...(event.model && typeof event.model === "object"
360
+ ? { model: event.model }
361
+ : {}),
362
+ ...(typeof event.thinkingLevel === "string"
363
+ ? { thinkingLevel: event.thinkingLevel }
364
+ : {}),
365
+ systemPromptHash: String(event.systemPromptHash ?? ""),
366
+ systemPromptChars: Number(event.systemPromptChars ?? 0),
367
+ activeTools: Array.isArray(event.activeTools) ? event.activeTools : [],
368
+ inputChars: Number(event.inputChars ?? 0),
369
+ imageCount: Number(event.imageCount ?? 0),
370
+ sources: Array.isArray(event.sources) ? event.sources : [],
371
+ },
372
+ };
373
+ case "command":
374
+ return {
375
+ kind: "command",
376
+ data: {
377
+ commandId: String(event.commandId ?? ""),
378
+ name: String(event.name ?? "unknown"),
379
+ ...(typeof event.argsPreview === "string"
380
+ ? { argsPreview: event.argsPreview }
381
+ : {}),
382
+ ...(typeof event.action === "string" ? { action: event.action } : {}),
383
+ outcome: event.outcome === "failed" ? "failed" : "completed",
384
+ ...(typeof event.message === "string" ? { message: event.message } : {}),
385
+ },
386
+ };
387
+ case "unknown":
388
+ return {
389
+ kind: "unknown",
390
+ data: projectUnknownChatEventPayload({
391
+ eventType: String(event.eventType ?? "unknown"),
392
+ value: event.value,
393
+ }),
394
+ };
249
395
  case "message_update":
250
396
  case "message_end": {
251
397
  const content = parseAssistantContent(event.content);
@@ -290,6 +436,10 @@ function turnEventToStorePartial(event) {
290
436
  event.messageId
291
437
  ? { messageId: event.messageId }
292
438
  : {}),
439
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
440
+ ...(typeof event.stopReason === "string"
441
+ ? { stopReason: event.stopReason }
442
+ : {}),
293
443
  },
294
444
  };
295
445
  }
@@ -303,6 +453,24 @@ function turnEventToStorePartial(event) {
303
453
  toolCallId: String(event.toolCallId ?? ""),
304
454
  toolName: String(event.toolName ?? ""),
305
455
  value: event.args,
456
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
457
+ ...(typeof event.parentToolCallId === "string"
458
+ ? { parentToolCallId: event.parentToolCallId }
459
+ : {}),
460
+ }),
461
+ };
462
+ case "tool_update":
463
+ return {
464
+ kind: "tool_update",
465
+ data: projectChatToolEventPayload({
466
+ kind: "tool_update",
467
+ toolCallId: String(event.toolCallId ?? ""),
468
+ toolName: String(event.toolName ?? ""),
469
+ value: event.partialResult,
470
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
471
+ ...(typeof event.parentToolCallId === "string"
472
+ ? { parentToolCallId: event.parentToolCallId }
473
+ : {}),
306
474
  }),
307
475
  };
308
476
  case "tool_result": {
@@ -317,6 +485,10 @@ function turnEventToStorePartial(event) {
317
485
  toolName: String(event.toolName ?? ""),
318
486
  value: event.result,
319
487
  isError: Boolean(event.isError),
488
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
489
+ ...(typeof event.parentToolCallId === "string"
490
+ ? { parentToolCallId: event.parentToolCallId }
491
+ : {}),
320
492
  ...(resultOperationId ? { operationId: resultOperationId } : {}),
321
493
  }),
322
494
  };
@@ -606,6 +778,7 @@ export async function handleChatCapabilities(_req, res, deps) {
606
778
  deniedTools: OPERATOR_CHAT_DENIED_TOOLS,
607
779
  deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
608
780
  tools: toolSchemas,
781
+ builtinCustomTools: OPERATOR_CHAT_SQL_TOOL_CAPABILITIES,
609
782
  builtinSubagents: [
610
783
  {
611
784
  toolId: "Agent",
@@ -803,25 +976,47 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
803
976
  });
804
977
  return;
805
978
  }
979
+ let forkedHandle;
980
+ let forkPersisted = false;
806
981
  try {
982
+ const body = await readJsonBody(req);
983
+ const throughMessageId = typeof body.throughMessageId === "string" && body.throughMessageId.trim()
984
+ ? body.throughMessageId.trim()
985
+ : undefined;
986
+ const throughUserMessageIndex = throughMessageId
987
+ ? source.messages
988
+ .filter((message) => message.role === "user")
989
+ .findIndex((message) => message.id === throughMessageId)
990
+ : undefined;
991
+ if (throughMessageId && throughUserMessageIndex === -1) {
992
+ throw new Error("FORK_MESSAGE_NOT_FOUND");
993
+ }
807
994
  const handle = await deps.runtime.forkSession({
808
995
  sourceSessionFile: source.sessionFile ?? "",
809
996
  sessionMode: source.sessionMode,
810
997
  ...(source.modelProvider && source.modelId
811
998
  ? { model: { provider: source.modelProvider, modelId: source.modelId } }
812
999
  : {}),
1000
+ ...(throughUserMessageIndex !== undefined
1001
+ ? { throughUserMessageIndex }
1002
+ : {}),
813
1003
  });
1004
+ forkedHandle = handle;
814
1005
  await deps.store.forkFrom({
815
1006
  sourceSessionId,
816
1007
  targetSessionId: handle.sessionId,
817
1008
  targetSessionFile: handle.sessionFile,
1009
+ ...(throughMessageId ? { throughMessageId } : {}),
818
1010
  });
1011
+ forkPersisted = true;
819
1012
  await deps.attachSession?.(handle.sessionId).catch(() => undefined);
820
- const copied = deps.events.forkContextRefs({
821
- sourceSessionId,
822
- targetSessionId: handle.sessionId,
823
- targetTurnId: `${handle.sessionId}:fork`,
824
- });
1013
+ const copied = throughMessageId
1014
+ ? { copiedCount: 0 }
1015
+ : deps.events.forkContextRefs({
1016
+ sourceSessionId,
1017
+ targetSessionId: handle.sessionId,
1018
+ targetTurnId: `${handle.sessionId}:fork`,
1019
+ });
825
1020
  await deps.operationLinker.recoverSession(handle.sessionId);
826
1021
  sendJson(res, 201, {
827
1022
  ok: true,
@@ -835,6 +1030,12 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
835
1030
  });
836
1031
  }
837
1032
  catch (error) {
1033
+ if (forkedHandle && !forkPersisted) {
1034
+ deps.runtime.dispose(forkedHandle.sessionId);
1035
+ await deps.store
1036
+ .discardUnownedSession(forkedHandle.sessionId, forkedHandle.sessionFile)
1037
+ .catch(() => undefined);
1038
+ }
838
1039
  sendJson(res, 409, {
839
1040
  ok: false,
840
1041
  error: {
@@ -981,12 +1182,21 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
981
1182
  const lastEvent = snapshot.at(-1);
982
1183
  const activeTurn = deps.events.getActiveTurn(sessionId);
983
1184
  const pendingQuestion = await deps.questionRegistry?.get(sessionId);
1185
+ const sddSessionIdentity = deps.sddDataAlignment && (await deps.sddDataAlignment.isEligible())
1186
+ ? { telemetrySessionId: record.telemetrySessionId ?? null }
1187
+ : undefined;
1188
+ const projectedMessages = projectStoredMessagesWithEventOrder(messagePage.messages, snapshot);
1189
+ const turnStates = turnStatesForIds(deps.events, sessionId, [
1190
+ ...projectedMessages.map((message) => message.turnId),
1191
+ ...snapshot.map((event) => event.turnId),
1192
+ ]);
984
1193
  sendJson(res, 200, {
985
1194
  ok: true,
986
1195
  eventEpoch: deps.events.epoch(sessionId),
987
1196
  session: {
988
1197
  ...projectBrowserChatSession(record),
989
- messages: projectStoredMessagesWithEventOrder(messagePage.messages, snapshot),
1198
+ messages: projectedMessages,
1199
+ turnStates,
990
1200
  composerDraft,
991
1201
  activeTurnId: activeTurn?.turnId,
992
1202
  turnPhase: activeTurn
@@ -1006,6 +1216,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
1006
1216
  sessionStats: telemetry.stats,
1007
1217
  usage: telemetry.usage,
1008
1218
  turnTelemetry: deriveChatTurnTelemetry(snapshot),
1219
+ userTurnOutline: projectUserTurnOutline(record.messages ?? []),
1220
+ ...(sddSessionIdentity
1221
+ ? { sddDataAlignment: sddSessionIdentity }
1222
+ : {}),
1009
1223
  contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
1010
1224
  ? deps.runtime.getContextPressureTelemetry(sessionId)
1011
1225
  : undefined,
@@ -1021,6 +1235,16 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
1021
1235
  turnId: event.turnId,
1022
1236
  data: event.data,
1023
1237
  })),
1238
+ conversationEvents: snapshot.filter((event) => [
1239
+ "step_start",
1240
+ "step_end",
1241
+ "model_retry",
1242
+ "turn_error",
1243
+ "turn_max_tokens",
1244
+ "request_context",
1245
+ "command",
1246
+ "unknown",
1247
+ ].includes(event.kind)),
1024
1248
  lastEventId: lastEvent?.eventId ?? null,
1025
1249
  lastEventSeq: lastEvent?.seq ?? 0,
1026
1250
  });
@@ -1271,7 +1495,14 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
1271
1495
  data: {
1272
1496
  ...(clientRequestId ? { clientRequestId } : {}),
1273
1497
  ...(steering ? { steering: true } : {}),
1274
- message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
1498
+ message: {
1499
+ id: userMessage.id,
1500
+ text: userMessage.text,
1501
+ at: userMessage.at,
1502
+ ...(userMessage.attachments?.length
1503
+ ? { attachments: userMessage.attachments }
1504
+ : {}),
1505
+ },
1275
1506
  },
1276
1507
  });
1277
1508
  }
@@ -1279,7 +1510,7 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
1279
1510
  * get_state equivalent): exposes the active turn + the latest turn's durable
1280
1511
  * outcome WITHOUT the full message payload, so the UI can poll it on an
1281
1512
  * interval / visibility / online without paying for session detail. */
1282
- export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1513
+ export async function handleGetChatSessionState(req, res, deps, sessionId) {
1283
1514
  const record = await deps.store.get(sessionId);
1284
1515
  if (!record) {
1285
1516
  sendJson(res, 404, {
@@ -1310,9 +1541,15 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1310
1541
  : runtimeBusy
1311
1542
  ? "running"
1312
1543
  : "idle";
1544
+ const turnStates = turnStatesForIds(deps.events, sessionId, [
1545
+ active?.turnId,
1546
+ latest?.turnId,
1547
+ ...requestedStateTurnIds(req),
1548
+ ]);
1313
1549
  sendJson(res, 200, {
1314
1550
  ok: true,
1315
1551
  state: {
1552
+ turnStates,
1316
1553
  // Legacy flat fields retained for older clients.
1317
1554
  activeTurnId: active?.turnId ?? null,
1318
1555
  activeTurnState: active?.state ?? null,
@@ -2669,6 +2906,7 @@ export async function handleChatEventsStream(req, res, deps, sessionId) {
2669
2906
  kind: "stream_ready",
2670
2907
  sessionId,
2671
2908
  epoch: deps.events.epoch(sessionId),
2909
+ lastSeq: deps.events.latestSeq(sessionId),
2672
2910
  latestEventId: retainedTail?.eventId ?? null,
2673
2911
  });
2674
2912
  let closed = false;
@@ -2890,28 +3128,8 @@ async function handlePiConfig(req, res, deps, resource) {
2890
3128
  async function handleChatFiles(res, deps, sessionId, query) {
2891
3129
  if (!(await requireChatSession(res, deps, sessionId)))
2892
3130
  return;
2893
- const q = query.trim().toLowerCase();
2894
- const walked = await walkRepoFiles(deps.appData.repoRoot, {
2895
- skipSensitive: true,
2896
- maxFiles: 5000,
2897
- includeDirs: true,
2898
- });
2899
- const entries = walked
2900
- .filter((entry) => !isSensitivePath(entry.repoRelative) &&
2901
- (!q || entry.repoRelative.toLowerCase().includes(q)))
2902
- .sort((a, b) => {
2903
- const aDir = a.repoRelative.endsWith("/");
2904
- const bDir = b.repoRelative.endsWith("/");
2905
- if (aDir !== bDir)
2906
- return aDir ? -1 : 1;
2907
- return a.repoRelative.localeCompare(b.repoRelative);
2908
- })
2909
- .slice(0, 50)
2910
- .map((entry) => ({
2911
- path: entry.repoRelative,
2912
- isDir: entry.repoRelative.endsWith("/"),
2913
- }));
2914
- sendJson(res, 200, { ok: true, files: entries });
3131
+ const files = await listChatFilePalette(deps.appData.repoRoot, query);
3132
+ sendJson(res, 200, { ok: true, files });
2915
3133
  }
2916
3134
  export async function handleChatCommands(res, deps, sessionId) {
2917
3135
  try {
@@ -2973,7 +3191,32 @@ async function handleSddBingo(req, res, deps, sessionId) {
2973
3191
  throw new SddDataAlignmentError("SDD_INVALID_SKILL_NAME", "请求体只允许 skillName 字段", 400);
2974
3192
  }
2975
3193
  const result = await deps.sddDataAlignment.report(sessionId, body.skillName);
2976
- sendJson(res, 200, { ok: true, skillName: result.skillName });
3194
+ const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:operator-action`;
3195
+ deps.events.append(sessionId, turnId, {
3196
+ kind: "operator-action",
3197
+ data: {
3198
+ schemaVersion: 1,
3199
+ action: "sdd-bingo",
3200
+ skillName: result.skillName,
3201
+ telemetrySessionId: result.telemetrySessionId,
3202
+ },
3203
+ });
3204
+ deps.events.append(sessionId, `${sessionId}:command:${result.telemetrySessionId}`, {
3205
+ kind: "command",
3206
+ data: {
3207
+ commandId: `sdd-bingo:${result.telemetrySessionId}`,
3208
+ name: "sdd-bingo",
3209
+ ...(result.skillName ? { argsPreview: result.skillName } : {}),
3210
+ action: "sdd-bingo",
3211
+ outcome: "completed",
3212
+ message: `Session: ${result.telemetrySessionId}`,
3213
+ },
3214
+ });
3215
+ sendJson(res, 200, {
3216
+ ok: true,
3217
+ skillName: result.skillName,
3218
+ telemetrySessionId: result.telemetrySessionId,
3219
+ });
2977
3220
  }
2978
3221
  catch (error) {
2979
3222
  if (error instanceof SddDataAlignmentError) {
@@ -2990,20 +3233,25 @@ async function handleSddBingo(req, res, deps, sessionId) {
2990
3233
  }
2991
3234
  }
2992
3235
  export async function handleRuntimeContext(res, deps, sessionId) {
2993
- const ensured = await ensureActiveSession(deps, sessionId);
2994
- if (!ensured.ok) {
2995
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3236
+ const record = await deps.store.get(sessionId);
3237
+ if (!record) {
3238
+ sendJson(res, 404, {
3239
+ ok: false,
3240
+ error: {
3241
+ code: "PI_SESSION_NOT_FOUND",
3242
+ message: `chat session not found: ${sessionId}`,
3243
+ },
3244
+ });
2996
3245
  return;
2997
3246
  }
2998
- // AC-03: system prompt comes from the REAL Pi session (agent.state.systemPrompt
2999
- // fallback chain); resources come from that session's ResourceLoader. The raw
3000
- // prompt stays server-side; the browser only ever receives the scrubbed +
3001
- // length-limited projection. RF-02: a critical projection failure is 503
3002
- // PI_RUNTIME_UNAVAILABLE — never a 500 leak or a fake-empty context.
3247
+ // Live Agent is preferred. Cold inspect uses workspace ResourceLoader /
3248
+ // ModelRuntime without SessionManager.open(jsonl); prompt still reopens.
3249
+ // RF-02: a critical projection failure is 503 PI_RUNTIME_UNAVAILABLE.
3003
3250
  let snapshot;
3004
3251
  try {
3005
3252
  snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3006
3253
  includeRaw: true,
3254
+ workspaceProjection: workspaceProjectionFromRecord(record),
3007
3255
  });
3008
3256
  }
3009
3257
  catch (error) {
@@ -3053,19 +3301,94 @@ export async function handleRuntimeContext(res, deps, sessionId) {
3053
3301
  }),
3054
3302
  });
3055
3303
  }
3304
+ /**
3305
+ * GET /sessions/:id/context-insights: dsh-context parity dashboard projection.
3306
+ * Folds the persisted event ring (works for idle/restored sessions) and, when
3307
+ * the Pi session is live, anchors the estimate with the runtime snapshot's
3308
+ * system-prompt size + active tool count. Read-only; never fabricates payload
3309
+ * text beyond the bounded previews already stored on the ring.
3310
+ */
3311
+ export async function handleContextInsights(res, deps, sessionId) {
3312
+ const record = await deps.store.get(sessionId);
3313
+ if (!record) {
3314
+ sendJson(res, 404, {
3315
+ ok: false,
3316
+ error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
3317
+ });
3318
+ return;
3319
+ }
3320
+ await deps.operationLinker.recoverSession(sessionId);
3321
+ const snapshot = deps.events.snapshot(sessionId);
3322
+ let systemPromptChars;
3323
+ let systemPromptPreview;
3324
+ let activeTools;
3325
+ let skillInjections;
3326
+ try {
3327
+ const runtimeSnapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3328
+ includeRaw: true,
3329
+ workspaceProjection: workspaceProjectionFromRecord(record),
3330
+ });
3331
+ if (runtimeSnapshot) {
3332
+ systemPromptChars = runtimeSnapshot.systemPrompt.characters;
3333
+ const raw = runtimeSnapshot.systemPrompt.raw ??
3334
+ runtimeSnapshot.systemPrompt.text;
3335
+ if (typeof raw === "string" && raw) {
3336
+ systemPromptPreview = raw.slice(0, 120_000);
3337
+ }
3338
+ activeTools = runtimeSnapshot.activeTools;
3339
+ skillInjections = runtimeSnapshot.skills
3340
+ .slice(0, 50)
3341
+ .map((skill) => ({
3342
+ name: skill.name,
3343
+ tokens: estimateTokensFromText(skill.description ?? skill.name),
3344
+ }));
3345
+ }
3346
+ }
3347
+ catch {
3348
+ // Insights are derived from the durable ring; a live-snapshot miss only
3349
+ // drops the anchor figures / browser static categories, never the dashboard.
3350
+ }
3351
+ // Tool schemas: capabilities registry projection for whitelisted operator
3352
+ // actions, plus Pi builtins active on this session.
3353
+ const schemaTools = buildModelCallableToolSchemas().map((tool) => ({
3354
+ id: tool.toolId,
3355
+ description: tool.description,
3356
+ }));
3357
+ const activeSet = new Set((activeTools ?? []).map((t) => t.toLowerCase()));
3358
+ const builtinTools = (activeTools ?? [])
3359
+ .filter((name) => !schemaTools.some((t) => t.id === name))
3360
+ .map((name) => ({ id: name, description: "" }));
3361
+ const insights = deriveContextInsights(snapshot, {
3362
+ sessionId,
3363
+ ...(systemPromptChars !== undefined ? { systemPromptChars } : {}),
3364
+ ...(systemPromptPreview !== undefined
3365
+ ? { systemPromptPreview }
3366
+ : {}),
3367
+ tools: [...schemaTools, ...builtinTools],
3368
+ ...(skillInjections ? { skillInjections } : {}),
3369
+ });
3370
+ sendJson(res, 200, { ok: true, insights });
3371
+ }
3056
3372
  // Exported for the direct handler-boundary tests (trace binding for
3057
3373
  // vt-route-model-errors); router dispatch behavior is unchanged.
3058
3374
  export async function handleModels(req, res, deps, sessionId) {
3059
- // AC-01/AC-06: models (and model/thinking mutation) requests reuse the same
3060
- // activation path. A missing/reopen-failed runtime is a STRUCTURED error —
3061
- // never a fabricated empty model list.
3062
- const ensured = await ensureActiveSession(deps, sessionId);
3063
- if (!ensured.ok) {
3064
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3065
- return;
3066
- }
3067
3375
  try {
3068
3376
  if ((req.method ?? "GET").toUpperCase() === "GET") {
3377
+ // Catalog is workspace-scoped (models.json / ModelRuntime). Do not wait
3378
+ // for Pi conversation reopen and do not start it here — prompt / reload
3379
+ // / model mutation still reopen, and concurrent reopen would stall this
3380
+ // cheap list on the single Node thread.
3381
+ const record = await deps.store.get(sessionId);
3382
+ if (!record) {
3383
+ sendJson(res, 404, {
3384
+ ok: false,
3385
+ error: {
3386
+ code: "PI_SESSION_NOT_FOUND",
3387
+ message: `chat session not found: ${sessionId}`,
3388
+ },
3389
+ });
3390
+ return;
3391
+ }
3069
3392
  let models;
3070
3393
  try {
3071
3394
  models = await deps.runtime.listModels(sessionId);
@@ -3094,6 +3417,11 @@ export async function handleModels(req, res, deps, sessionId) {
3094
3417
  });
3095
3418
  return;
3096
3419
  }
3420
+ const ensured = await ensureActiveSession(deps, sessionId);
3421
+ if (!ensured.ok) {
3422
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3423
+ return;
3424
+ }
3097
3425
  const gate = gateMutation(req, deps);
3098
3426
  if (!gate.ok) {
3099
3427
  sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
@@ -3149,18 +3477,24 @@ async function requireChatSession(res, deps, sessionId) {
3149
3477
  });
3150
3478
  return false;
3151
3479
  }
3152
- /**
3153
- * Unified session activation (design §2.1 / AC-01): models, runtime snapshot,
3154
- * reload and model/thinking mutation requests all flow through this path.
3155
- *
3156
- * - runtime already holds the session → ok;
3157
- * - only a persisted record exists → reopen from record.sessionFile;
3158
- * - no record / missing session file → 404 PI_SESSION_NOT_FOUND;
3159
- * - SDK reopen failure → 503 PI_RUNTIME_UNAVAILABLE.
3160
- *
3161
- * Never fabricates a fresh session or an empty model list.
3162
- */
3480
+ const ensureActiveSessionInFlight = new WeakMap();
3163
3481
  export async function ensureActiveSession(deps, sessionId) {
3482
+ let bySession = ensureActiveSessionInFlight.get(deps);
3483
+ if (!bySession) {
3484
+ bySession = new Map();
3485
+ ensureActiveSessionInFlight.set(deps, bySession);
3486
+ }
3487
+ const existing = bySession.get(sessionId);
3488
+ if (existing)
3489
+ return existing;
3490
+ const pending = runEnsureActiveSession(deps, sessionId).finally(() => {
3491
+ if (bySession.get(sessionId) === pending)
3492
+ bySession.delete(sessionId);
3493
+ });
3494
+ bySession.set(sessionId, pending);
3495
+ return pending;
3496
+ }
3497
+ async function runEnsureActiveSession(deps, sessionId) {
3164
3498
  // Wait for create-path materialization before treating the session as missing.
3165
3499
  if (typeof deps.runtime.ensureSessionReady === "function") {
3166
3500
  try {
@@ -3406,19 +3740,28 @@ export async function handleReloadSession(req, res, deps, sessionId) {
3406
3740
  });
3407
3741
  }
3408
3742
  /**
3409
- * GET /sessions/:id/runtime-snapshot (AC-01): the unified live snapshot.
3743
+ * GET /sessions/:id/runtime-snapshot (AC-01): live Agent when present,
3744
+ * otherwise workspace ResourceLoader projection (no JSONL reopen).
3410
3745
  */
3411
3746
  export async function handleRuntimeSnapshot(_res, res, deps, sessionId) {
3412
- const ensured = await ensureActiveSession(deps, sessionId);
3413
- if (!ensured.ok) {
3414
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3747
+ const record = await deps.store.get(sessionId);
3748
+ if (!record) {
3749
+ sendJson(res, 404, {
3750
+ ok: false,
3751
+ error: {
3752
+ code: "PI_SESSION_NOT_FOUND",
3753
+ message: `chat session not found: ${sessionId}`,
3754
+ },
3755
+ });
3415
3756
  return;
3416
3757
  }
3417
3758
  // RF-02: a critical snapshot projection failure is 503 PI_RUNTIME_UNAVAILABLE
3418
3759
  // — never a 200 fake-empty snapshot.
3419
3760
  let snapshot;
3420
3761
  try {
3421
- snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
3762
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3763
+ workspaceProjection: workspaceProjectionFromRecord(record),
3764
+ });
3422
3765
  }
3423
3766
  catch (error) {
3424
3767
  if (sendRuntimeUnavailable(res, error))
@@ -3901,7 +4244,7 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
3901
4244
  : step === "confirm"
3902
4245
  ? "confirmDagConfirmation"
3903
4246
  : "runDag";
3904
- const result = await dispatchOperatorAction(ctx, {
4247
+ const result = await dispatchOperatorAction({ ...ctx, chatSessionRef: sessionId }, {
3905
4248
  action,
3906
4249
  actionParams: body,
3907
4250
  clientRequestId: typeof body.clientRequestId === "string"
@@ -4241,7 +4584,14 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4241
4584
  limit: Number.isFinite(limit) ? limit : 30,
4242
4585
  beforeId: url.searchParams.get("beforeId") ?? undefined,
4243
4586
  });
4244
- sendJson(res, 200, { ok: true, ...page });
4587
+ const snapshot = deps.events.snapshot(sessionId);
4588
+ const messages = projectStoredMessagesWithEventOrder(page.messages, snapshot);
4589
+ sendJson(res, 200, {
4590
+ ok: true,
4591
+ ...page,
4592
+ messages,
4593
+ turnStates: turnStatesForIds(deps.events, sessionId, messages.map((message) => message.turnId)),
4594
+ });
4245
4595
  }
4246
4596
  catch (error) {
4247
4597
  sendJson(res, 404, {
@@ -4253,6 +4603,23 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4253
4603
  });
4254
4604
  }
4255
4605
  }
4606
+ export async function handleChatAttachment(res, deps, sessionId, attachmentId) {
4607
+ const attachment = await deps.store.readAttachment(sessionId, attachmentId);
4608
+ if (!attachment) {
4609
+ sendJson(res, 404, {
4610
+ ok: false,
4611
+ error: { code: "NOT_FOUND", message: "chat attachment not found" },
4612
+ });
4613
+ return;
4614
+ }
4615
+ res.writeHead(200, {
4616
+ "content-type": attachment.metadata.mimeType,
4617
+ "content-length": String(attachment.bytes.length),
4618
+ "cache-control": "private, max-age=31536000, immutable",
4619
+ "x-content-type-options": "nosniff",
4620
+ });
4621
+ res.end(attachment.bytes);
4622
+ }
4256
4623
  /**
4257
4624
  * Dispatch a chat HTTP request. Returns true if handled.
4258
4625
  * Mounted under /api/operator/v1/chat/*.
@@ -4279,6 +4646,24 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
4279
4646
  throw error;
4280
4647
  }
4281
4648
  }
4649
+ function databaseConnectionInput(body, engine) {
4650
+ return {
4651
+ name: typeof body.name === "string" ? body.name : "",
4652
+ engine,
4653
+ ...(typeof body.path === "string" ? { path: body.path } : {}),
4654
+ ...(body.mode === "parameters" || body.mode === "url-env" ? { mode: body.mode } : {}),
4655
+ ...(typeof body.urlEnv === "string" ? { urlEnv: body.urlEnv } : {}),
4656
+ ...(typeof body.host === "string" ? { host: body.host } : {}),
4657
+ ...(typeof body.port === "number" ? { port: body.port } : {}),
4658
+ ...(typeof body.database === "string" ? { database: body.database } : {}),
4659
+ ...(typeof body.username === "string" ? { username: body.username } : {}),
4660
+ ...(body.passwordMode === "runtime" || body.passwordMode === "environment" || body.passwordMode === "encrypted" ? { passwordMode: body.passwordMode } : {}),
4661
+ ...(typeof body.passwordRef === "string" ? { passwordRef: body.passwordRef } : {}),
4662
+ ...(typeof body.passwordEnv === "string" ? { passwordEnv: body.passwordEnv } : {}),
4663
+ ...(typeof body.password === "string" ? { password: body.password } : {}),
4664
+ ...(typeof body.ssl === "boolean" ? { ssl: body.ssl } : {}),
4665
+ };
4666
+ }
4282
4667
  async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4283
4668
  // Terminal management API (model + Console UI shared PTY registry).
4284
4669
  if (await handleTerminalRequest(req, res, {
@@ -4358,6 +4743,117 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4358
4743
  await handleChatCapabilities(req, res, deps);
4359
4744
  return true;
4360
4745
  }
4746
+ if (method === "GET" && pathname === "/api/operator/v1/chat/database-connections/export") {
4747
+ sendJson(res, 200, { ok: true, document: deps.runtime.exportDatabaseConnections() });
4748
+ return true;
4749
+ }
4750
+ if (method === "POST" && pathname === "/api/operator/v1/chat/database-connections/import") {
4751
+ const gate = gateMutation(req, deps);
4752
+ if (!gate.ok) {
4753
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
4754
+ return true;
4755
+ }
4756
+ try {
4757
+ const body = await readJsonBody(req);
4758
+ const conflict = body.conflict === "overwrite" ? "overwrite" : "skip";
4759
+ const result = await deps.runtime.importDatabaseConnections(body.document, conflict);
4760
+ sendJson(res, 200, { ok: true, result });
4761
+ }
4762
+ catch (error) {
4763
+ const code = error instanceof DbConnectionError ? error.code : "DB_PERSISTENCE_ERROR";
4764
+ const status = code === "DB_PERSISTENCE_ERROR" ? 500 : 400;
4765
+ sendJson(res, status, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
4766
+ }
4767
+ return true;
4768
+ }
4769
+ if (pathname === "/api/operator/v1/chat/database-connections" &&
4770
+ (method === "GET" || method === "POST")) {
4771
+ if (method === "GET") {
4772
+ sendJson(res, 200, { ok: true, connections: deps.runtime.listDatabaseConnections() });
4773
+ return true;
4774
+ }
4775
+ const gate = gateMutation(req, deps);
4776
+ if (!gate.ok) {
4777
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
4778
+ return true;
4779
+ }
4780
+ try {
4781
+ const body = await readJsonBody(req);
4782
+ const engine = body.engine;
4783
+ if (engine !== "sqlite" && engine !== "postgres" && engine !== "mysql") {
4784
+ throw new DbConnectionError("DB_INVALID_CONFIG", "engine must be sqlite, postgres, or mysql");
4785
+ }
4786
+ const connection = await deps.runtime.addDatabaseConnection(databaseConnectionInput(body, engine));
4787
+ sendJson(res, 201, { ok: true, connection });
4788
+ }
4789
+ catch (error) {
4790
+ const code = error instanceof DbConnectionError ? error.code : "DB_PERSISTENCE_ERROR";
4791
+ const status = code === "DB_PERSISTENCE_ERROR" ? 500 : 400;
4792
+ sendJson(res, status, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
4793
+ }
4794
+ return true;
4795
+ }
4796
+ const databaseBrowseRoute = pathname.match(/^\/api\/operator\/v1\/chat\/database-connections\/([^/]+)\/browse$/);
4797
+ if (databaseBrowseRoute && method === "GET") {
4798
+ const name = decodeURIComponent(databaseBrowseRoute[1]);
4799
+ try {
4800
+ const table = url.searchParams.get("table")?.trim();
4801
+ const database = url.searchParams.get("database")?.trim() || undefined;
4802
+ if (!table) {
4803
+ const databases = await deps.runtime.listDatabaseDatabases(name);
4804
+ const connection = deps.runtime.listDatabaseConnections().find((item) => item.name === name);
4805
+ const shouldLoadTables = connection?.engine === "sqlite" || Boolean(database || connection?.database);
4806
+ sendJson(res, 200, { ok: true, connection: name, database, databases, tables: shouldLoadTables ? await deps.runtime.listDatabaseTables(name, database) : [] });
4807
+ }
4808
+ else {
4809
+ const page = Number(url.searchParams.get("page") ?? "1");
4810
+ const pageSize = Number(url.searchParams.get("pageSize") ?? "50");
4811
+ sendJson(res, 200, { ok: true, result: await deps.runtime.browseDatabaseTable(name, table, page, pageSize, database) });
4812
+ }
4813
+ }
4814
+ catch (error) {
4815
+ const code = error instanceof DbConnectionError ? error.code : "DB_ENGINE_ERROR";
4816
+ const status = code === "DB_CONNECTION_NOT_FOUND" ? 404 : 400;
4817
+ sendJson(res, status, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
4818
+ }
4819
+ return true;
4820
+ }
4821
+ const databaseConnectionRoute = pathname.match(/^\/api\/operator\/v1\/chat\/database-connections\/([^/]+)(?:\/(test))?$/);
4822
+ if (databaseConnectionRoute && (method === "DELETE" || method === "POST" || method === "PATCH")) {
4823
+ const gate = gateMutation(req, deps);
4824
+ if (!gate.ok) {
4825
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
4826
+ return true;
4827
+ }
4828
+ const name = decodeURIComponent(databaseConnectionRoute[1]);
4829
+ try {
4830
+ if (method === "POST" && databaseConnectionRoute[2] === "test") {
4831
+ sendJson(res, 200, { ok: true, result: await deps.runtime.testDatabaseConnection(name) });
4832
+ }
4833
+ else if (method === "PATCH" && !databaseConnectionRoute[2]) {
4834
+ const body = await readJsonBody(req);
4835
+ const engine = body.engine;
4836
+ if (engine !== "sqlite" && engine !== "postgres" && engine !== "mysql") {
4837
+ throw new DbConnectionError("DB_INVALID_CONFIG", "engine must be sqlite, postgres, or mysql");
4838
+ }
4839
+ const connection = await deps.runtime.updateDatabaseConnection(name, databaseConnectionInput(body, engine));
4840
+ sendJson(res, 200, { ok: true, connection });
4841
+ }
4842
+ else if (method === "DELETE" && !databaseConnectionRoute[2]) {
4843
+ await deps.runtime.removeDatabaseConnection(name);
4844
+ sendJson(res, 200, { ok: true });
4845
+ }
4846
+ else {
4847
+ sendJson(res, 405, { ok: false, error: { code: "METHOD_NOT_ALLOWED", message: "method not allowed" } });
4848
+ }
4849
+ }
4850
+ catch (error) {
4851
+ const code = error instanceof DbConnectionError ? error.code : "DB_ENGINE_ERROR";
4852
+ const status = code === "DB_CONNECTION_NOT_FOUND" ? 404 : code === "DB_PERSISTENCE_ERROR" ? 500 : 400;
4853
+ sendJson(res, status, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
4854
+ }
4855
+ return true;
4856
+ }
4361
4857
  const htmlPreviewRoute = matchRepoHtmlPreviewRoute(pathname);
4362
4858
  if (method === "GET" && htmlPreviewRoute) {
4363
4859
  if (!htmlPreviewRoute.rawPath)
@@ -4443,7 +4939,7 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4443
4939
  await handleSddBingo(req, res, deps, decodeURIComponent(sddBingo[1]));
4444
4940
  return true;
4445
4941
  }
4446
- const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|models|model|reload|commands)$/);
4942
+ const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|context-insights|models|model|reload|commands)$/);
4447
4943
  if (m4Route) {
4448
4944
  const id = decodeURIComponent(m4Route[1]);
4449
4945
  const route = m4Route[2];
@@ -4463,6 +4959,10 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4463
4959
  await handleRuntimeSnapshot(req, res, deps, id);
4464
4960
  return true;
4465
4961
  }
4962
+ if (route === "context-insights" && method === "GET") {
4963
+ await handleContextInsights(res, deps, id);
4964
+ return true;
4965
+ }
4466
4966
  if (route === "models" && method === "GET") {
4467
4967
  await handleModels(req, res, deps, id);
4468
4968
  return true;
@@ -4557,6 +5057,11 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4557
5057
  await handleChatMessages(res, deps, decodeURIComponent(sessionMessages[1]), url);
4558
5058
  return true;
4559
5059
  }
5060
+ const sessionAttachment = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/attachments\/([^/]+)$/);
5061
+ if (method === "GET" && sessionAttachment) {
5062
+ await handleChatAttachment(res, deps, decodeURIComponent(sessionAttachment[1]), decodeURIComponent(sessionAttachment[2]));
5063
+ return true;
5064
+ }
4560
5065
  const sessionPrompt = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/prompt$/);
4561
5066
  if (method === "POST" && sessionPrompt) {
4562
5067
  await handleChatPrompt(req, res, deps, decodeURIComponent(sessionPrompt[1]));