@tea-agent/loop-agent 0.42.0-next.1 → 0.42.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 (238) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +319 -1
  3. package/dist/application/dag/generate-task-dag.js +75 -19
  4. package/dist/application/dag/run-dag.js +41 -0
  5. package/dist/application/task-lifecycle/advance.js +24 -5
  6. package/dist/application/task-lifecycle/observe.js +171 -17
  7. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  8. package/dist/application/task-lifecycle/recommendations.js +13 -2
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +7 -0
  11. package/dist/cli/program.js +6 -1
  12. package/dist/commands/client-recovery.js +3 -0
  13. package/dist/commands/dag-follow-up.js +138 -0
  14. package/dist/commands/dag-rerun-task.js +2 -0
  15. package/dist/commands/init.js +27 -1
  16. package/dist/commands/task-advance.js +19 -0
  17. package/dist/executors/dag-pi-executor.js +3976 -76
  18. package/dist/executors/pi-executor.js +15 -4
  19. package/dist/executors/pi-read-budget-policy.js +239 -0
  20. package/dist/executors/shell-executor.js +978 -188
  21. package/dist/executors/shell-write-guard.js +7 -0
  22. package/dist/infrastructure/console/operation-store.js +226 -6
  23. package/dist/shared/dag-failure-category.js +12 -0
  24. package/dist/shared/openspec-spec.js +70 -4
  25. package/dist/shared/operator/capabilities.js +7 -0
  26. package/dist/task/config-types.js +105 -5
  27. package/dist/task/contract/adopt.js +4 -0
  28. package/dist/task/contract/import-revision.js +4 -0
  29. package/dist/task/contract/project.js +3 -0
  30. package/dist/task/contract/schema.js +2 -1
  31. package/dist/task/frontend-project-capability.js +203 -20
  32. package/dist/task/runtime.js +5 -2
  33. package/dist/task/source-prepare/build-draft.js +3 -3
  34. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  35. package/dist/task/source-prepare/prepare.js +126 -21
  36. package/dist/task/source-prepare/semantic-intake.js +6 -2
  37. package/dist/task/source-references.js +22 -1
  38. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  39. package/dist/worker/console/chat/model-resolver.js +7 -4
  40. package/dist/worker/console/chat/pi-runtime.js +258 -11
  41. package/dist/worker/console/chat/repo-browser.js +10 -3
  42. package/dist/worker/console/chat/routes.js +165 -71
  43. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  44. package/dist/worker/console/chat/session-catalog.js +2 -0
  45. package/dist/worker/console/chat/session-store.js +159 -70
  46. package/dist/worker/console/chat/shortcuts.js +19 -3
  47. package/dist/worker/console/chat/turn-process.js +111 -56
  48. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  49. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  50. package/dist/worker/console/index.js +3 -0
  51. package/dist/worker/console/operator-actions.js +169 -7
  52. package/dist/worker/console/pi-readiness.js +3 -2
  53. package/dist/worker/console/server.js +23 -1
  54. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  55. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
  56. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  57. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  58. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  59. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  60. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  70. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  71. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  72. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  73. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  75. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  81. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  82. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  83. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  84. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  85. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  86. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  87. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  88. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  89. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  90. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  91. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  92. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
  93. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
  94. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  95. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  96. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  97. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  98. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  99. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  100. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  101. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  102. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  103. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  105. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  106. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  107. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  108. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  109. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  111. package/dist/worker/console/static/index.html +2 -2
  112. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  113. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  114. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  115. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  116. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  117. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  118. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  119. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  120. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  121. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  122. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  123. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  124. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  125. package/dist/worker/console/workspace-context.js +115 -1
  126. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  127. package/dist/worker/observe/routes.js +4 -0
  128. package/dist/worker/observe/static/dag-helpers.js +4 -2
  129. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  130. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  131. package/dist/worker/observe/static/kpi.js +2 -2
  132. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  133. package/dist/worker/observe/static/operator-chrome.js +37 -23
  134. package/dist/worker/observe/static/relations.js +2 -2
  135. package/dist/worker/observe/static/router.d.ts +12 -1
  136. package/dist/worker/observe/static/router.js +48 -6
  137. package/dist/worker/observe/static/run-processing.js +4 -2
  138. package/dist/worker/observe/static/shell-chrome.js +2 -2
  139. package/dist/worker/observe/static/state.js +5 -0
  140. package/dist/worker/observe/static/styles.css +95 -0
  141. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  142. package/dist/worker/observe/static/views/dag.js +10 -2
  143. package/dist/worker/observe/static/views/dags.js +3 -1
  144. package/dist/worker/observe/static/views/dashboard.js +14 -6
  145. package/dist/worker/observe/static/views/pool.js +7 -2
  146. package/dist/worker/observe/static/views/run.js +12 -3
  147. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  148. package/dist/worker/observe/static/views/task.js +7 -2
  149. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  150. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  151. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  152. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  153. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  154. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  155. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  156. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  157. package/dist/workflows/dag/frontend-closeout.js +221 -0
  158. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  159. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  161. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  163. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  164. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  165. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  166. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  167. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  168. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  169. package/dist/workflows/dag/frontend-repair.js +1 -432
  170. package/dist/workflows/dag/frontend-review-context.js +261 -15
  171. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  172. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  173. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  174. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  175. package/dist/workflows/dag/frontend-shape.js +435 -0
  176. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  177. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  178. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  179. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  180. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  181. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  182. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  183. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  184. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  185. package/dist/workflows/dag/init-hybrid.js +1028 -562
  186. package/dist/workflows/dag/interrupt-request.js +7 -0
  187. package/dist/workflows/dag/node-execution.js +828 -4
  188. package/dist/workflows/dag/prompt.js +70 -3
  189. package/dist/workflows/dag/recovery-lease.js +170 -0
  190. package/dist/workflows/dag/report.js +37 -1
  191. package/dist/workflows/dag/rerun-feedback.js +315 -1
  192. package/dist/workflows/dag/rerun-plan.js +17 -6
  193. package/dist/workflows/dag/rerun-run.js +44 -6
  194. package/dist/workflows/dag/rerun-task.js +251 -13
  195. package/dist/workflows/dag/retry-policy.js +219 -104
  196. package/dist/workflows/dag/runner.js +580 -124
  197. package/dist/workflows/dag/scheduler.js +133 -20
  198. package/dist/workflows/dag/types.js +244 -16
  199. package/dist/workflows/dag/validate.js +28 -12
  200. package/docs/examples/README.md +5 -0
  201. package/docs/init-surface.manifest.json +30 -12
  202. package/docs/skills/vetted-skill-registry.md +4 -2
  203. package/docs/templates/README.md +2 -0
  204. package/docs/templates/agent-dag-report.schema.json +8 -2
  205. package/docs/templates/agent-dag.schema.json +1 -1
  206. package/docs/templates/backend-test-dag.json +23 -20
  207. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  208. package/docs/templates/frontend-implementation-dag.json +89 -0
  209. package/docs/templates/spec-registry.schema.json +45 -0
  210. package/harness.json +1 -1
  211. package/package.json +4 -3
  212. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  213. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  214. package/skills/frontend-contract/SKILL.md +23 -0
  215. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  216. package/skills/frontend-design-review/SKILL.md +22 -41
  217. package/skills/frontend-plan/SKILL.md +26 -0
  218. package/skills/frontend-plan/references/decision-contract.md +37 -0
  219. package/skills/frontend-plan/references/design-decisions.md +17 -0
  220. package/skills/frontend-review/SKILL.md +20 -15
  221. package/skills/frontend-review/references/review-findings.md +6 -7
  222. package/skills/frontend-scout/SKILL.md +25 -0
  223. package/skills/frontend-scout/references/design-evidence.md +16 -0
  224. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  225. package/skills/frontend-verification/SKILL.md +1 -1
  226. package/skills/loop-agent/references/command-reference.md +1 -0
  227. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  228. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  229. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  230. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  231. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  232. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  233. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  234. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  235. package/skills/frontend-implementation/SKILL.md +0 -52
  236. package/skills/frontend-implementation/references/code-standards.md +0 -33
  237. package/skills/frontend-implementation/references/design-spec.md +0 -56
  238. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -26,7 +26,7 @@
26
26
  * `createDefaultPiSdkBindings()`.
27
27
  */
28
28
  import { createHash, randomBytes } from "node:crypto";
29
- import { access } from "node:fs/promises";
29
+ import { access, mkdir, rm, stat, writeFile } from "node:fs/promises";
30
30
  import path from "node:path";
31
31
  import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
32
32
  import { parseProviderError, } from "./provider-error.js";
@@ -43,6 +43,7 @@ import { filterActiveInterviewTools } from "../interview/tools.js";
43
43
  import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
44
44
  import { buildTodoContinuationContext, } from "./todos.js";
45
45
  import { buildGoalContinuationContext, } from "./goals.js";
46
+ import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
46
47
  /**
47
48
  * Base system prompt fragment every General Operator Chat session receives.
48
49
  *
@@ -298,6 +299,24 @@ export function createGoalCompactionContextExtension(options) {
298
299
  },
299
300
  };
300
301
  }
302
+ export function createWizardLocalHeadersExtension(options) {
303
+ return {
304
+ name: "operator-chat-wizard-local-headers",
305
+ hidden: true,
306
+ factory: (api) => {
307
+ api.on("before_provider_headers", async (rawEvent, context) => {
308
+ if (!context || !rawEvent.headers || typeof rawEvent.headers !== "object")
309
+ return;
310
+ await injectWizardLocalHeaders({
311
+ headers: rawEvent.headers,
312
+ context,
313
+ sessionId: options.sessionId,
314
+ service: options.service,
315
+ });
316
+ });
317
+ },
318
+ };
319
+ }
301
320
  /**
302
321
  * Build Pi custom ToolDefinition objects for every whitelisted operator action.
303
322
  * Each tool's execute() routes into the operator action dispatcher (Gate 3).
@@ -931,6 +950,44 @@ export class PiSessionInitFailedError extends Error {
931
950
  this.name = "PiSessionInitFailedError";
932
951
  }
933
952
  }
953
+ async function readModelConfigFileVersion(filePath) {
954
+ try {
955
+ const value = await stat(filePath, { bigint: true });
956
+ return {
957
+ exists: true,
958
+ dev: value.dev,
959
+ ino: value.ino,
960
+ size: value.size,
961
+ mtimeNs: value.mtimeNs,
962
+ ctimeNs: value.ctimeNs,
963
+ };
964
+ }
965
+ catch (error) {
966
+ if (error !== null &&
967
+ typeof error === "object" &&
968
+ "code" in error &&
969
+ error.code === "ENOENT") {
970
+ return { exists: false };
971
+ }
972
+ throw new Error("MODEL_CONFIG_CHECK_FAILED");
973
+ }
974
+ }
975
+ function sameModelConfigFileVersion(left, right) {
976
+ if (!left.exists || !right.exists)
977
+ return left.exists === right.exists;
978
+ return (left.dev === right.dev &&
979
+ left.ino === right.ino &&
980
+ left.size === right.size &&
981
+ left.mtimeNs === right.mtimeNs &&
982
+ left.ctimeNs === right.ctimeNs);
983
+ }
984
+ async function captureModelConfigFile(agentDir) {
985
+ const filePath = path.join(agentDir, "models.json");
986
+ return {
987
+ path: filePath,
988
+ version: await readModelConfigFileVersion(filePath),
989
+ };
990
+ }
934
991
  /**
935
992
  * Console Pi runtime — holds Chat sessions and drives prompt() with three-gate
936
993
  * tool enforcement. Session-per-sessionId; cross-request reuse via open().
@@ -940,6 +997,8 @@ export class ConsolePiRuntime {
940
997
  loader;
941
998
  sessions = new Map();
942
999
  modelRuntimes = new Map();
1000
+ /** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
1001
+ modelConfigFiles = new Map();
943
1002
  sessionManagers = new Map();
944
1003
  /** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
945
1004
  serviceScopes = new Map();
@@ -949,6 +1008,8 @@ export class ConsolePiRuntime {
949
1008
  reloadInFlight = new Set();
950
1009
  /** Synchronous prompt ownership per session (admission before first await). */
951
1010
  promptOwners = new Set();
1011
+ /** Steering requests accepted by the SDK but not yet consumed into Pi context. */
1012
+ pendingSteering = new Map();
952
1013
  /** At most one detached automatic-title task per durable Console session. */
953
1014
  titleInFlight = new Set();
954
1015
  /** Composer thinking selection ("auto" = no explicit override). */
@@ -1019,10 +1080,17 @@ export class ConsolePiRuntime {
1019
1080
  registry: this.options.goalRegistry,
1020
1081
  })
1021
1082
  : undefined;
1083
+ const wizardLocalHeadersExtension = sessionId && this.options.sddDataAlignment
1084
+ ? createWizardLocalHeadersExtension({
1085
+ sessionId,
1086
+ service: this.options.sddDataAlignment,
1087
+ })
1088
+ : undefined;
1022
1089
  const extraFactories = [
1023
1090
  todoContextExtension,
1024
1091
  goalContextExtension,
1025
1092
  pressureExtension,
1093
+ wizardLocalHeadersExtension,
1026
1094
  ].filter((item) => Boolean(item));
1027
1095
  return {
1028
1096
  ...base,
@@ -1263,6 +1331,15 @@ export class ConsolePiRuntime {
1263
1331
  if (!sessionFile) {
1264
1332
  throw new Error(`chat session shell missing sessionFile for ${sessionId}`);
1265
1333
  }
1334
+ // Pi's SessionManager.create deliberately postpones writing a JSONL until
1335
+ // an assistant response. Materialize a valid header now, otherwise Console
1336
+ // could persist a path that disappears on a restart before first reply.
1337
+ await this.bindings.materializeSessionShell({
1338
+ sessionManager,
1339
+ sessionId,
1340
+ sessionFile,
1341
+ cwd: this.options.cwd,
1342
+ });
1266
1343
  this.sessionManagers.set(sessionId, sessionManager);
1267
1344
  this.thinkingSelections.set(sessionId, "auto");
1268
1345
  // Detached single-flight materialization; dependent routes await the same promise.
@@ -1341,6 +1418,9 @@ export class ConsolePiRuntime {
1341
1418
  if (this.disposedSessions.has(sessionId))
1342
1419
  return;
1343
1420
  const agentDir = await safeGetAgentDir(this.bindings);
1421
+ // Capture before services parse models.json. A concurrent edit can therefore
1422
+ // cause at most one harmless extra refresh, never a missed next-turn refresh.
1423
+ const modelConfigFile = await captureModelConfigFile(agentDir);
1344
1424
  const appendSystemPrompt = [
1345
1425
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1346
1426
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -1400,6 +1480,7 @@ export class ConsolePiRuntime {
1400
1480
  this.sessions.set(sessionId, session);
1401
1481
  this.sessionManagers.set(sessionId, init.sessionManager);
1402
1482
  this.modelRuntimes.set(sessionId, modelRuntime);
1483
+ this.modelConfigFiles.set(sessionId, modelConfigFile);
1403
1484
  this.sessionModels.set(sessionId, model);
1404
1485
  this.serviceScopes.set(sessionId, {
1405
1486
  resourceLoader: services.resourceLoader,
@@ -1420,6 +1501,7 @@ export class ConsolePiRuntime {
1420
1501
  this.sessions.delete(sessionId);
1421
1502
  this.sessionManagers.delete(sessionId);
1422
1503
  this.modelRuntimes.delete(sessionId);
1504
+ this.modelConfigFiles.delete(sessionId);
1423
1505
  this.sessionModels.delete(sessionId);
1424
1506
  this.serviceScopes.delete(sessionId);
1425
1507
  this.revisions.delete(sessionId);
@@ -1498,6 +1580,7 @@ export class ConsolePiRuntime {
1498
1580
  this.sessions.set(sessionId, built.session);
1499
1581
  this.sessionManagers.set(sessionId, sessionManager);
1500
1582
  this.modelRuntimes.set(sessionId, built.modelRuntime);
1583
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
1501
1584
  this.sessionModels.set(sessionId, init.model);
1502
1585
  this.serviceScopes.set(sessionId, built.services);
1503
1586
  this.revisions.set(sessionId, 1);
@@ -1569,11 +1652,69 @@ export class ConsolePiRuntime {
1569
1652
  if (!resolved)
1570
1653
  throw new Error("MODEL_NOT_AVAILABLE");
1571
1654
  await session.setModel(resolved);
1572
- this.bumpRevision(sessionId);
1573
- return {
1655
+ const applied = {
1574
1656
  provider: session.model?.provider ?? model.provider,
1575
1657
  modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
1576
1658
  };
1659
+ this.sessionModels.set(sessionId, applied);
1660
+ this.bumpRevision(sessionId);
1661
+ return applied;
1662
+ }
1663
+ async refreshModelConfiguration(sessionId, mode) {
1664
+ const session = this.sessions.get(sessionId);
1665
+ const modelRuntime = this.modelRuntimes.get(sessionId);
1666
+ const configFile = this.modelConfigFiles.get(sessionId);
1667
+ if (!session || !modelRuntime || !configFile) {
1668
+ throw new Error(`chat session not active: ${sessionId}`);
1669
+ }
1670
+ const nextVersion = await readModelConfigFileVersion(configFile.path);
1671
+ if (mode === "if-changed" &&
1672
+ sameModelConfigFileVersion(configFile.version, nextVersion)) {
1673
+ return false;
1674
+ }
1675
+ if (typeof modelRuntime.refresh !== "function") {
1676
+ throw new Error("MODEL_CONFIG_REFRESH_UNSUPPORTED");
1677
+ }
1678
+ const stored = this.sessionModels.get(sessionId);
1679
+ const provider = session.model?.provider ?? stored?.provider;
1680
+ const modelId = session.model?.id ?? session.model?.modelId ?? stored?.modelId;
1681
+ try {
1682
+ await modelRuntime.refresh({ allowNetwork: false });
1683
+ if (typeof modelRuntime.getError === "function" && modelRuntime.getError()) {
1684
+ throw new Error("MODEL_CONFIG_REFRESH_FAILED");
1685
+ }
1686
+ if (provider && modelId) {
1687
+ if (typeof session.setModel !== "function") {
1688
+ throw new Error("MODEL_CONFIG_REBIND_UNSUPPORTED");
1689
+ }
1690
+ const resolved = this.bindings.resolveModel({
1691
+ modelRuntime,
1692
+ provider,
1693
+ modelId,
1694
+ });
1695
+ if (!resolved) {
1696
+ throw new Error("MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH");
1697
+ }
1698
+ await session.setModel(resolved);
1699
+ this.sessionModels.set(sessionId, { provider, modelId });
1700
+ }
1701
+ }
1702
+ catch (error) {
1703
+ if (error instanceof Error &&
1704
+ (error.message === "MODEL_CONFIG_REBIND_UNSUPPORTED" ||
1705
+ error.message === "MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH")) {
1706
+ throw error;
1707
+ }
1708
+ // Do not surface SDK/config error text: it may contain credential material.
1709
+ throw new Error("MODEL_CONFIG_REFRESH_FAILED");
1710
+ }
1711
+ if (this.disposedSessions.has(sessionId) ||
1712
+ this.sessions.get(sessionId) !== session) {
1713
+ throw new Error(`chat session not active: ${sessionId}`);
1714
+ }
1715
+ configFile.version = nextVersion;
1716
+ this.bumpRevision(sessionId);
1717
+ return true;
1577
1718
  }
1578
1719
  /**
1579
1720
  * Apply a thinking-level selection. `auto` records the selection WITHOUT
@@ -1696,6 +1837,8 @@ export class ConsolePiRuntime {
1696
1837
  };
1697
1838
  }
1698
1839
  const busyReasons = [];
1840
+ if (this.promptOwners.has(sessionId))
1841
+ busyReasons.push("prompt in flight");
1699
1842
  if (session.isStreaming === true || session.isIdle === false)
1700
1843
  busyReasons.push("streaming");
1701
1844
  if (session.isCompacting === true)
@@ -1727,6 +1870,7 @@ export class ConsolePiRuntime {
1727
1870
  const services = this.serviceScopes.get(sessionId);
1728
1871
  if (services)
1729
1872
  this.applySessionCompactionOverride(sessionId, services, session);
1873
+ await this.refreshModelConfiguration(sessionId, "force");
1730
1874
  }
1731
1875
  catch (error) {
1732
1876
  return {
@@ -1738,7 +1882,6 @@ export class ConsolePiRuntime {
1738
1882
  finally {
1739
1883
  this.reloadInFlight.delete(sessionId);
1740
1884
  }
1741
- this.bumpRevision(sessionId);
1742
1885
  // Reload replaces the effective resource/session context. Persisted Goal
1743
1886
  // remains visible, but continuation authorization must be renewed by user.
1744
1887
  this.options.goalRegistry?.disarm(sessionId);
@@ -2204,6 +2347,16 @@ export class ConsolePiRuntime {
2204
2347
  });
2205
2348
  }
2206
2349
  catch (error) {
2350
+ const code = error && typeof error === "object" && "code" in error
2351
+ ? String(error.code ?? "")
2352
+ : "";
2353
+ if (code === "SESSION_FILE_MISSING" || code === "ENOENT") {
2354
+ return {
2355
+ ok: false,
2356
+ code: "NO_SESSION_FILE",
2357
+ message: `Pi session file missing or not openable: ${init.sessionFile}`,
2358
+ };
2359
+ }
2207
2360
  return {
2208
2361
  ok: false,
2209
2362
  code: "REOPEN_FAILED",
@@ -2257,6 +2410,7 @@ export class ConsolePiRuntime {
2257
2410
  this.sessions.set(sessionId, session);
2258
2411
  this.sessionManagers.set(sessionId, sessionManager);
2259
2412
  this.modelRuntimes.set(sessionId, built.modelRuntime);
2413
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
2260
2414
  this.sessionModels.set(sessionId, init.model);
2261
2415
  this.serviceScopes.set(sessionId, built.services);
2262
2416
  this.revisions.set(sessionId, 1);
@@ -2310,6 +2464,7 @@ export class ConsolePiRuntime {
2310
2464
  */
2311
2465
  async buildSessionWithServices(init) {
2312
2466
  const agentDir = await safeGetAgentDir(this.bindings);
2467
+ const modelConfigFile = await captureModelConfigFile(agentDir);
2313
2468
  const appendSystemPrompt = [
2314
2469
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2315
2470
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -2343,6 +2498,7 @@ export class ConsolePiRuntime {
2343
2498
  return {
2344
2499
  session,
2345
2500
  modelRuntime,
2501
+ modelConfigFile,
2346
2502
  services: {
2347
2503
  resourceLoader: services.resourceLoader,
2348
2504
  settingsManager: services.settingsManager,
@@ -2354,14 +2510,31 @@ export class ConsolePiRuntime {
2354
2510
  return this.sessions.get(sessionId);
2355
2511
  }
2356
2512
  /** Inject one queued message into the currently running Pi turn. */
2357
- async steer(sessionId, text, images) {
2513
+ async steer(sessionId, clientRequestId, text, images) {
2358
2514
  const session = this.sessions.get(sessionId);
2359
2515
  if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
2360
2516
  throw new Error("CHAT_TURN_NOT_ACTIVE");
2361
2517
  }
2362
2518
  if (typeof session.steer !== "function")
2363
2519
  throw new Error("CHAT_STEER_UNAVAILABLE");
2364
- await session.steer(text, images);
2520
+ const pending = this.pendingSteering.get(sessionId) ?? [];
2521
+ const identity = { clientRequestId, text };
2522
+ pending.push(identity);
2523
+ this.pendingSteering.set(sessionId, pending);
2524
+ try {
2525
+ await session.steer(text, images);
2526
+ }
2527
+ catch (error) {
2528
+ const current = this.pendingSteering.get(sessionId);
2529
+ if (current) {
2530
+ const index = current.indexOf(identity);
2531
+ if (index >= 0)
2532
+ current.splice(index, 1);
2533
+ if (current.length === 0)
2534
+ this.pendingSteering.delete(sessionId);
2535
+ }
2536
+ throw error;
2537
+ }
2365
2538
  }
2366
2539
  hasSession(sessionId) {
2367
2540
  return this.sessions.has(sessionId);
@@ -2397,12 +2570,13 @@ export class ConsolePiRuntime {
2397
2570
  */
2398
2571
  async prompt(sessionId, text, onEvent, options) {
2399
2572
  // Synchronous admission fence — before any await.
2400
- if (this.promptOwners.has(sessionId)) {
2573
+ if (this.promptOwners.has(sessionId) || this.reloadInFlight.has(sessionId)) {
2401
2574
  throw new Error("CHAT_TURN_ACTIVE");
2402
2575
  }
2403
2576
  this.promptOwners.add(sessionId);
2404
2577
  let unsub;
2405
2578
  let abortPromise;
2579
+ let initialUserMessageStarted = false;
2406
2580
  const onAbort = () => {
2407
2581
  // M1 T08 / AC-R3-003: Stop must abort only the current turn, NEVER
2408
2582
  // dispose the session. Await session.abort() so ownership settles
@@ -2459,6 +2633,11 @@ export class ConsolePiRuntime {
2459
2633
  if (options?.signal?.aborted) {
2460
2634
  return;
2461
2635
  }
2636
+ await this.refreshModelConfiguration(sessionId, "if-changed");
2637
+ // Stop may arrive while stat/refresh/rebind is in flight.
2638
+ if (options?.signal?.aborted) {
2639
+ return;
2640
+ }
2462
2641
  const emitContextUsage = () => {
2463
2642
  try {
2464
2643
  const contextUsage = session.getSessionStats?.()?.contextUsage;
@@ -2500,6 +2679,27 @@ export class ConsolePiRuntime {
2500
2679
  };
2501
2680
  unsub = session.subscribe((event) => {
2502
2681
  const observedAt = Date.now();
2682
+ if (event.type === "message_start" && isUserSdkMessage(event.message)) {
2683
+ if (!initialUserMessageStarted) {
2684
+ // The first user message_start belongs to session.prompt(text), not
2685
+ // to a steering request that may have raced with prompt startup.
2686
+ initialUserMessageStarted = true;
2687
+ }
2688
+ else {
2689
+ const pending = this.pendingSteering.get(sessionId);
2690
+ const accepted = pending?.shift();
2691
+ if (pending?.length === 0)
2692
+ this.pendingSteering.delete(sessionId);
2693
+ if (accepted) {
2694
+ onEvent({
2695
+ type: "steering_accepted",
2696
+ sessionId,
2697
+ clientRequestId: accepted.clientRequestId,
2698
+ text: accepted.text,
2699
+ });
2700
+ }
2701
+ }
2702
+ }
2503
2703
  if (event.type === "compaction_start") {
2504
2704
  activeCompaction = {
2505
2705
  compactionId: newCompactionId(sessionId),
@@ -2638,6 +2838,10 @@ export class ConsolePiRuntime {
2638
2838
  finally {
2639
2839
  if (this.options.questionRegistry)
2640
2840
  this.options.questionRegistry.clearTurn(sessionId);
2841
+ // Unconsumed durable DeferredTurns remain dispatching and are recovered by
2842
+ // the route layer after this active Turn closes. Never carry their volatile
2843
+ // identities into a later prompt.
2844
+ this.pendingSteering.delete(sessionId);
2641
2845
  this.promptOwners.delete(sessionId);
2642
2846
  }
2643
2847
  }
@@ -2759,11 +2963,13 @@ export class ConsolePiRuntime {
2759
2963
  this.sessionManagers.delete(sessionId);
2760
2964
  this.mainlineLeaves.delete(sessionId);
2761
2965
  this.modelRuntimes.delete(sessionId);
2966
+ this.modelConfigFiles.delete(sessionId);
2762
2967
  this.sessionModels.delete(sessionId);
2763
2968
  this.serviceScopes.delete(sessionId);
2764
2969
  this.revisions.delete(sessionId);
2765
2970
  this.thinkingSelections.delete(sessionId);
2766
2971
  this.reloadInFlight.delete(sessionId);
2972
+ this.pendingSteering.delete(sessionId);
2767
2973
  }
2768
2974
  disposeAll() {
2769
2975
  for (const id of [...this.sessions.keys()])
@@ -2796,6 +3002,11 @@ function mapAssistantMessageEvent(type, sessionId, message) {
2796
3002
  ...(content.length > 0 ? { content } : {}),
2797
3003
  };
2798
3004
  }
3005
+ function isUserSdkMessage(message) {
3006
+ return Boolean(message &&
3007
+ typeof message === "object" &&
3008
+ message.role === "user");
3009
+ }
2799
3010
  function mapSdkEvent(sessionId, event, compactionId) {
2800
3011
  switch (event.type) {
2801
3012
  case "agent_start":
@@ -2981,6 +3192,36 @@ export function createDefaultPiSdkBindings() {
2981
3192
  id: opts.sessionId,
2982
3193
  });
2983
3194
  },
3195
+ materializeSessionShell: async (opts) => {
3196
+ const sdk = (await loadSdk());
3197
+ const manager = opts.sessionManager;
3198
+ if (typeof manager.setSessionFile !== "function") {
3199
+ throw new Error("Pi SessionManager cannot materialize a session file");
3200
+ }
3201
+ let created = false;
3202
+ try {
3203
+ await mkdir(path.dirname(opts.sessionFile), { recursive: true });
3204
+ // Match Pi's documented session header. Rebinding through the public
3205
+ // SessionManager API marks it flushed, so Pi appends the first user
3206
+ // entry instead of trying to create the already-present file later.
3207
+ await writeFile(opts.sessionFile, `${JSON.stringify({
3208
+ type: "session",
3209
+ version: typeof sdk.CURRENT_SESSION_VERSION === "number"
3210
+ ? sdk.CURRENT_SESSION_VERSION
3211
+ : 3,
3212
+ id: opts.sessionId,
3213
+ timestamp: new Date().toISOString(),
3214
+ cwd: opts.cwd,
3215
+ })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3216
+ created = true;
3217
+ manager.setSessionFile(opts.sessionFile);
3218
+ }
3219
+ catch (error) {
3220
+ if (created)
3221
+ await rm(opts.sessionFile, { force: true }).catch(() => undefined);
3222
+ throw error;
3223
+ }
3224
+ },
2984
3225
  createInMemorySessionManager: async (opts) => {
2985
3226
  const sdk = (await loadSdk());
2986
3227
  return sdk.SessionManager.inMemory(opts.cwd);
@@ -2997,13 +3238,19 @@ export function createDefaultPiSdkBindings() {
2997
3238
  // wrong conversation under the durable Console sessionId. Fail closed
2998
3239
  // before open when the persisted file is absent.
2999
3240
  await access(opts.sessionFile);
3000
- return sdk.SessionManager.open(opts.sessionFile);
3001
3241
  }
3002
- catch {
3003
- // Missing/corrupt session file — caller fails closed rather than
3004
- // silently creating a fresh session and losing prior context.
3242
+ catch (error) {
3243
+ const code = error && typeof error === "object" && "code" in error
3244
+ ? String(error.code ?? "")
3245
+ : "";
3246
+ if (code === "ENOENT") {
3247
+ throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
3248
+ }
3249
+ // File exists but is unreadable (permissions / I/O) — caller maps
3250
+ // undefined to 503 rather than silently creating a fresh session.
3005
3251
  return undefined;
3006
3252
  }
3253
+ return sdk.SessionManager.open(opts.sessionFile);
3007
3254
  },
3008
3255
  resolveModel: (opts) => {
3009
3256
  const rt = opts.modelRuntime;
@@ -9,6 +9,9 @@ const TREE_LIMIT = 500;
9
9
  export const TEXT_PREVIEW_LIMIT = 512 * 1024;
10
10
  /** Full-file ceiling for the sandboxed HTML preview route and its assets. */
11
11
  export const HTML_PREVIEW_ASSET_LIMIT = 20 * 1024 * 1024;
12
+ /** Self-contained HTML exports commonly embed data and can be much larger than
13
+ * an individual image/script asset. Keep a separate, still-bounded ceiling. */
14
+ export const HTML_PREVIEW_DOCUMENT_LIMIT = 64 * 1024 * 1024;
12
15
  export const REPO_HTML_PREVIEW_ROUTE_PREFIX = "/api/operator/v1/chat/repo/html/";
13
16
  const SKIP_DIRS = new Set([
14
17
  ".git",
@@ -190,8 +193,12 @@ export async function readRepoHtmlAsset(repoRoot, rawPath) {
190
193
  const info = await handle.stat();
191
194
  if (!info.isFile())
192
195
  throw new RepoBrowserError(400, "NOT_A_FILE", "path is not a regular file");
193
- if (info.size > HTML_PREVIEW_ASSET_LIMIT)
194
- throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${HTML_PREVIEW_ASSET_LIMIT} byte HTML preview limit`);
196
+ const contentType = repoHtmlAssetContentType(guard.repoRelative);
197
+ const limit = contentType.startsWith("text/html")
198
+ ? HTML_PREVIEW_DOCUMENT_LIMIT
199
+ : HTML_PREVIEW_ASSET_LIMIT;
200
+ if (info.size > limit)
201
+ throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${limit} byte HTML preview limit`);
195
202
  const rechecked = await guardReadPath(repoRoot, rawPath);
196
203
  if (!rechecked.ok)
197
204
  throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
@@ -202,7 +209,7 @@ export async function readRepoHtmlAsset(repoRoot, rawPath) {
202
209
  throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
203
210
  return {
204
211
  body: await handle.readFile(),
205
- contentType: repoHtmlAssetContentType(guard.repoRelative),
212
+ contentType,
206
213
  bytes: info.size,
207
214
  };
208
215
  }