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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +61 -267
  2. package/dist/application/dag/generate-task-dag.js +19 -75
  3. package/dist/application/task-lifecycle/advance.js +5 -24
  4. package/dist/application/task-lifecycle/observe.js +17 -171
  5. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  6. package/dist/application/task-lifecycle/recommendations.js +2 -13
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +0 -5
  10. package/dist/commands/client-recovery.js +0 -3
  11. package/dist/commands/init.js +1 -27
  12. package/dist/commands/task-advance.js +0 -19
  13. package/dist/executors/dag-pi-executor.js +231 -3784
  14. package/dist/executors/pi-executor.js +4 -15
  15. package/dist/executors/pi-extension-resolver.js +14 -2
  16. package/dist/executors/shell-executor.js +217 -945
  17. package/dist/executors/shell-write-guard.js +0 -7
  18. package/dist/shared/dag-failure-category.js +0 -12
  19. package/dist/shared/openspec-spec.js +4 -70
  20. package/dist/shared/operator/capabilities.js +0 -7
  21. package/dist/task/config-types.js +7 -34
  22. package/dist/task/contract/adopt.js +0 -4
  23. package/dist/task/contract/import-revision.js +0 -4
  24. package/dist/task/contract/project.js +0 -3
  25. package/dist/task/contract/schema.js +1 -2
  26. package/dist/task/frontend-project-capability.js +20 -203
  27. package/dist/task/runtime.js +2 -5
  28. package/dist/task/source-prepare/build-draft.js +3 -3
  29. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  30. package/dist/task/source-prepare/prepare.js +1 -89
  31. package/dist/task/source-prepare/semantic-intake.js +2 -6
  32. package/dist/task/source-references.js +1 -22
  33. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  34. package/dist/worker/console/chat/pi-runtime.js +235 -13
  35. package/dist/worker/console/chat/repo-browser.js +10 -3
  36. package/dist/worker/console/chat/routes.js +86 -44
  37. package/dist/worker/console/chat/semantic-activity.js +12 -11
  38. package/dist/worker/console/chat/session-stats.js +96 -0
  39. package/dist/worker/console/chat/session-store.js +133 -70
  40. package/dist/worker/console/chat/shortcuts.js +1 -1
  41. package/dist/worker/console/chat/turn-process.js +139 -60
  42. package/dist/worker/console/index.js +0 -3
  43. package/dist/worker/console/operation-store.js +6 -226
  44. package/dist/worker/console/operator-actions.js +7 -169
  45. package/dist/worker/console/pi-plugins.js +3 -1
  46. package/dist/worker/console/server.js +19 -4
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DQ5jlYVU.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BeTVCzH9.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-DassREBi.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-DyeNX8Ek.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-BXb_zxGx.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-B_lofstT.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-B9F9gxGC.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-Byd5zHEQ.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-BqoxMJk9.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-CT7d6YZw.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CdiNyN0N.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-BGw9bpVc.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-DiDVlVB1.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-BhHf-4yK.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-B79pEgwv.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-Cb70EcUV.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-ieUI0-eY.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-ieUI0-eY.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-DBHLnAf1.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-BT_JRsOH.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-Bss-msa3.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-B8Gr24pt.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-BrjU70Jw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cp_JpPGd.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-BWKICEP-.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D3z_UNR_.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-_Ywm390x.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-D9QYQQMB.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BNgXNKAr.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-QRLnkPCg.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-B9fOduNi.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-FSEdtPN2.js} +1 -1
  79. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
  80. package/dist/worker/console/static/assets/index-DtPrwu7M.js +451 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-D5Bjg9dm.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BlNRkWvf.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-BdJiw3SL.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-CsDNlYht.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-8g9dbEmX.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-ChyQH_ut.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-DwYoCTYt.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-T_b2rS17.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-1Cr7WMhH.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-Bq4qM59o.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-ytFgR1RL.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CnUdUVYX.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-_5rJqG6R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-D5imyGbt.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-DvgDn3wL.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BViu7duc.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-kNxnG4_w.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BcULKg2P.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DyTzlcWH.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-DmPveIjt.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-9r-iFy0R.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-D3fIXdvM.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-BCSS9SBg.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  107. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  108. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  109. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  110. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  113. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  114. package/dist/worker/console/workspace-context.js +1 -111
  115. package/dist/worker/observability/read-model.js +4 -0
  116. package/dist/worker/observe/routes.js +4 -0
  117. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  118. package/dist/worker/observe/static/views/dag-inspector.js +2 -7
  119. package/dist/worker/observe/static/views/session-timeline.js +46 -15
  120. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  121. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  122. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  123. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  125. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  126. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  127. package/dist/workflows/dag/frontend-repair.js +432 -1
  128. package/dist/workflows/dag/frontend-review-context.js +15 -261
  129. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  130. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  131. package/dist/workflows/dag/init-hybrid.js +494 -685
  132. package/dist/workflows/dag/interrupt-request.js +0 -7
  133. package/dist/workflows/dag/node-execution.js +4 -732
  134. package/dist/workflows/dag/prompt.js +1 -34
  135. package/dist/workflows/dag/report.js +1 -37
  136. package/dist/workflows/dag/rerun-feedback.js +1 -256
  137. package/dist/workflows/dag/rerun-plan.js +2 -10
  138. package/dist/workflows/dag/rerun-run.js +0 -23
  139. package/dist/workflows/dag/rerun-task.js +0 -29
  140. package/dist/workflows/dag/retry-policy.js +104 -214
  141. package/dist/workflows/dag/runner.js +122 -429
  142. package/dist/workflows/dag/scheduler.js +16 -114
  143. package/dist/workflows/dag/types.js +12 -239
  144. package/dist/workflows/dag/validate.js +12 -28
  145. package/docs/init-surface.manifest.json +12 -30
  146. package/docs/skills/vetted-skill-registry.md +2 -4
  147. package/docs/templates/README.md +0 -2
  148. package/docs/templates/agent-dag-report.schema.json +2 -8
  149. package/docs/templates/agent-dag.schema.json +1 -1
  150. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  151. package/harness.json +3 -3
  152. package/package.json +1 -1
  153. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  154. package/skills/frontend-design-review/SKILL.md +41 -22
  155. package/skills/frontend-implementation/SKILL.md +52 -0
  156. package/skills/frontend-implementation/references/code-standards.md +33 -0
  157. package/skills/frontend-implementation/references/design-spec.md +56 -0
  158. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  159. package/skills/frontend-review/SKILL.md +15 -20
  160. package/skills/frontend-review/references/review-findings.md +7 -6
  161. package/skills/frontend-verification/SKILL.md +1 -1
  162. package/skills/loop-agent/references/command-reference.md +0 -1
  163. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  164. package/dist/commands/dag-follow-up.js +0 -138
  165. package/dist/executors/pi-read-budget-policy.js +0 -239
  166. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  167. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  168. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  171. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  172. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  175. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  176. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  177. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  178. package/dist/workflows/dag/frontend-closeout.js +0 -221
  179. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  180. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  181. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  182. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  183. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  184. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  185. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  186. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  187. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  188. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  189. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  190. package/dist/workflows/dag/frontend-shape.js +0 -427
  191. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  192. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  193. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  194. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  195. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  196. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  197. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  198. package/dist/workflows/dag/recovery-lease.js +0 -80
  199. package/docs/templates/frontend-implementation-dag.json +0 -89
  200. package/docs/templates/spec-registry.schema.json +0 -45
  201. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  202. package/skills/frontend-contract/SKILL.md +0 -23
  203. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  204. package/skills/frontend-plan/SKILL.md +0 -26
  205. package/skills/frontend-plan/references/decision-contract.md +0 -37
  206. package/skills/frontend-plan/references/design-decisions.md +0 -17
  207. package/skills/frontend-scout/SKILL.md +0 -25
  208. package/skills/frontend-scout/references/design-evidence.md +0 -16
  209. package/skills/frontend-scout/references/scout-evidence.md +0 -23
@@ -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";
@@ -67,10 +67,11 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
67
67
  "You are General Operator Chat.",
68
68
  "Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
69
69
  "No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
70
- "Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait with 60s → 90s → 120s → 180s backoff; reset to 60s on state change; 30-60s re-checks when stall suspected. Every operationWait continuation MUST use afterSeq=previous nextSeq (recommendedNextCall); never reuse older cursors. Heartbeats: not meaningful progress. Run id 20260829-slug ≠ task id 2026-08-29-slug.",
71
- "On failure consume terminal runSummary first. If progress exists, wait; else dagReport then dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun (node rerun only: reruns the failed subgraph, keeps prior successes). If plan ineligible (unsafe-shell subgraph, binding/workspace drift): Worker Failed → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume. Run-failed task without open gate is terminal: do not re-advance a run-failed task. Before run start, two identical lifecycleState/blocker/revision/DAG-hash observations mean no progress: stop and report after one bounded probe; never loop identical bash/read/grep. Auto-approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
70
+ "Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
71
+ "Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
72
72
  "Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
73
73
  "Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
74
+ "Narrate long-DAG progress briefly at start, meaningful phase changes/findings, verification failure/recovery, prolonged quiet and terminal state: say what finished and what is next. Never narrate unchanged operationWait polls or heartbeats, or claim beyond structured facts.",
74
75
  ].join("\n");
75
76
  /** Compose the inspectable system prompt actually injected into Operator Chat. */
76
77
  export function composeOperatorChatSystemPrompt(input) {
@@ -930,6 +931,44 @@ export class PiSessionInitFailedError extends Error {
930
931
  this.name = "PiSessionInitFailedError";
931
932
  }
932
933
  }
934
+ async function readModelConfigFileVersion(filePath) {
935
+ try {
936
+ const value = await stat(filePath, { bigint: true });
937
+ return {
938
+ exists: true,
939
+ dev: value.dev,
940
+ ino: value.ino,
941
+ size: value.size,
942
+ mtimeNs: value.mtimeNs,
943
+ ctimeNs: value.ctimeNs,
944
+ };
945
+ }
946
+ catch (error) {
947
+ if (error !== null &&
948
+ typeof error === "object" &&
949
+ "code" in error &&
950
+ error.code === "ENOENT") {
951
+ return { exists: false };
952
+ }
953
+ throw new Error("MODEL_CONFIG_CHECK_FAILED");
954
+ }
955
+ }
956
+ function sameModelConfigFileVersion(left, right) {
957
+ if (!left.exists || !right.exists)
958
+ return left.exists === right.exists;
959
+ return (left.dev === right.dev &&
960
+ left.ino === right.ino &&
961
+ left.size === right.size &&
962
+ left.mtimeNs === right.mtimeNs &&
963
+ left.ctimeNs === right.ctimeNs);
964
+ }
965
+ async function captureModelConfigFile(agentDir) {
966
+ const filePath = path.join(agentDir, "models.json");
967
+ return {
968
+ path: filePath,
969
+ version: await readModelConfigFileVersion(filePath),
970
+ };
971
+ }
933
972
  /**
934
973
  * Console Pi runtime — holds Chat sessions and drives prompt() with three-gate
935
974
  * tool enforcement. Session-per-sessionId; cross-request reuse via open().
@@ -939,6 +978,8 @@ export class ConsolePiRuntime {
939
978
  loader;
940
979
  sessions = new Map();
941
980
  modelRuntimes = new Map();
981
+ /** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
982
+ modelConfigFiles = new Map();
942
983
  sessionManagers = new Map();
943
984
  /** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
944
985
  serviceScopes = new Map();
@@ -948,6 +989,8 @@ export class ConsolePiRuntime {
948
989
  reloadInFlight = new Set();
949
990
  /** Synchronous prompt ownership per session (admission before first await). */
950
991
  promptOwners = new Set();
992
+ /** Steering requests accepted by the SDK but not yet consumed into Pi context. */
993
+ pendingSteering = new Map();
951
994
  /** At most one detached automatic-title task per durable Console session. */
952
995
  titleInFlight = new Set();
953
996
  /** Composer thinking selection ("auto" = no explicit override). */
@@ -1262,6 +1305,15 @@ export class ConsolePiRuntime {
1262
1305
  if (!sessionFile) {
1263
1306
  throw new Error(`chat session shell missing sessionFile for ${sessionId}`);
1264
1307
  }
1308
+ // Pi's SessionManager.create deliberately postpones writing a JSONL until
1309
+ // an assistant response. Materialize a valid header now, otherwise Console
1310
+ // could persist a path that disappears on a restart before first reply.
1311
+ await this.bindings.materializeSessionShell?.({
1312
+ sessionManager,
1313
+ sessionId,
1314
+ sessionFile,
1315
+ cwd: this.options.cwd,
1316
+ });
1265
1317
  this.sessionManagers.set(sessionId, sessionManager);
1266
1318
  this.thinkingSelections.set(sessionId, "auto");
1267
1319
  // Detached single-flight materialization; dependent routes await the same promise.
@@ -1340,6 +1392,9 @@ export class ConsolePiRuntime {
1340
1392
  if (this.disposedSessions.has(sessionId))
1341
1393
  return;
1342
1394
  const agentDir = await safeGetAgentDir(this.bindings);
1395
+ // Capture before services parse models.json. A concurrent edit can therefore
1396
+ // cause at most one harmless extra refresh, never a missed next-turn refresh.
1397
+ const modelConfigFile = await captureModelConfigFile(agentDir);
1343
1398
  const appendSystemPrompt = [
1344
1399
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1345
1400
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -1399,6 +1454,7 @@ export class ConsolePiRuntime {
1399
1454
  this.sessions.set(sessionId, session);
1400
1455
  this.sessionManagers.set(sessionId, init.sessionManager);
1401
1456
  this.modelRuntimes.set(sessionId, modelRuntime);
1457
+ this.modelConfigFiles.set(sessionId, modelConfigFile);
1402
1458
  this.sessionModels.set(sessionId, model);
1403
1459
  this.serviceScopes.set(sessionId, {
1404
1460
  resourceLoader: services.resourceLoader,
@@ -1419,6 +1475,7 @@ export class ConsolePiRuntime {
1419
1475
  this.sessions.delete(sessionId);
1420
1476
  this.sessionManagers.delete(sessionId);
1421
1477
  this.modelRuntimes.delete(sessionId);
1478
+ this.modelConfigFiles.delete(sessionId);
1422
1479
  this.sessionModels.delete(sessionId);
1423
1480
  this.serviceScopes.delete(sessionId);
1424
1481
  this.revisions.delete(sessionId);
@@ -1497,6 +1554,7 @@ export class ConsolePiRuntime {
1497
1554
  this.sessions.set(sessionId, built.session);
1498
1555
  this.sessionManagers.set(sessionId, sessionManager);
1499
1556
  this.modelRuntimes.set(sessionId, built.modelRuntime);
1557
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
1500
1558
  this.sessionModels.set(sessionId, init.model);
1501
1559
  this.serviceScopes.set(sessionId, built.services);
1502
1560
  this.revisions.set(sessionId, 1);
@@ -1568,11 +1626,69 @@ export class ConsolePiRuntime {
1568
1626
  if (!resolved)
1569
1627
  throw new Error("MODEL_NOT_AVAILABLE");
1570
1628
  await session.setModel(resolved);
1571
- this.bumpRevision(sessionId);
1572
- return {
1629
+ const applied = {
1573
1630
  provider: session.model?.provider ?? model.provider,
1574
1631
  modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
1575
1632
  };
1633
+ this.sessionModels.set(sessionId, applied);
1634
+ this.bumpRevision(sessionId);
1635
+ return applied;
1636
+ }
1637
+ async refreshModelConfiguration(sessionId, mode) {
1638
+ const session = this.sessions.get(sessionId);
1639
+ const modelRuntime = this.modelRuntimes.get(sessionId);
1640
+ const configFile = this.modelConfigFiles.get(sessionId);
1641
+ if (!session || !modelRuntime || !configFile) {
1642
+ throw new Error(`chat session not active: ${sessionId}`);
1643
+ }
1644
+ const nextVersion = await readModelConfigFileVersion(configFile.path);
1645
+ if (mode === "if-changed" &&
1646
+ sameModelConfigFileVersion(configFile.version, nextVersion)) {
1647
+ return false;
1648
+ }
1649
+ if (typeof modelRuntime.refresh !== "function") {
1650
+ throw new Error("MODEL_CONFIG_REFRESH_UNSUPPORTED");
1651
+ }
1652
+ const stored = this.sessionModels.get(sessionId);
1653
+ const provider = session.model?.provider ?? stored?.provider;
1654
+ const modelId = session.model?.id ?? session.model?.modelId ?? stored?.modelId;
1655
+ try {
1656
+ await modelRuntime.refresh({ allowNetwork: false });
1657
+ if (typeof modelRuntime.getError === "function" && modelRuntime.getError()) {
1658
+ throw new Error("MODEL_CONFIG_REFRESH_FAILED");
1659
+ }
1660
+ if (provider && modelId) {
1661
+ if (typeof session.setModel !== "function") {
1662
+ throw new Error("MODEL_CONFIG_REBIND_UNSUPPORTED");
1663
+ }
1664
+ const resolved = this.bindings.resolveModel({
1665
+ modelRuntime,
1666
+ provider,
1667
+ modelId,
1668
+ });
1669
+ if (!resolved) {
1670
+ throw new Error("MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH");
1671
+ }
1672
+ await session.setModel(resolved);
1673
+ this.sessionModels.set(sessionId, { provider, modelId });
1674
+ }
1675
+ }
1676
+ catch (error) {
1677
+ if (error instanceof Error &&
1678
+ (error.message === "MODEL_CONFIG_REBIND_UNSUPPORTED" ||
1679
+ error.message === "MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH")) {
1680
+ throw error;
1681
+ }
1682
+ // Do not surface SDK/config error text: it may contain credential material.
1683
+ throw new Error("MODEL_CONFIG_REFRESH_FAILED");
1684
+ }
1685
+ if (this.disposedSessions.has(sessionId) ||
1686
+ this.sessions.get(sessionId) !== session) {
1687
+ throw new Error(`chat session not active: ${sessionId}`);
1688
+ }
1689
+ configFile.version = nextVersion;
1690
+ this.bumpRevision(sessionId);
1691
+ return true;
1576
1692
  }
1577
1693
  /**
1578
1694
  * Apply a thinking-level selection. `auto` records the selection WITHOUT
@@ -1695,6 +1811,8 @@ export class ConsolePiRuntime {
1695
1811
  };
1696
1812
  }
1697
1813
  const busyReasons = [];
1814
+ if (this.promptOwners.has(sessionId))
1815
+ busyReasons.push("prompt in flight");
1698
1816
  if (session.isStreaming === true || session.isIdle === false)
1699
1817
  busyReasons.push("streaming");
1700
1818
  if (session.isCompacting === true)
@@ -1726,6 +1844,7 @@ export class ConsolePiRuntime {
1726
1844
  const services = this.serviceScopes.get(sessionId);
1727
1845
  if (services)
1728
1846
  this.applySessionCompactionOverride(sessionId, services, session);
1847
+ await this.refreshModelConfiguration(sessionId, "force");
1729
1848
  }
1730
1849
  catch (error) {
1731
1850
  return {
@@ -1737,7 +1856,6 @@ export class ConsolePiRuntime {
1737
1856
  finally {
1738
1857
  this.reloadInFlight.delete(sessionId);
1739
1858
  }
1740
- this.bumpRevision(sessionId);
1741
1859
  // Reload replaces the effective resource/session context. Persisted Goal
1742
1860
  // remains visible, but continuation authorization must be renewed by user.
1743
1861
  this.options.goalRegistry?.disarm(sessionId);
@@ -2203,6 +2321,16 @@ export class ConsolePiRuntime {
2203
2321
  });
2204
2322
  }
2205
2323
  catch (error) {
2324
+ const code = error && typeof error === "object" && "code" in error
2325
+ ? String(error.code ?? "")
2326
+ : "";
2327
+ if (code === "SESSION_FILE_MISSING" || code === "ENOENT") {
2328
+ return {
2329
+ ok: false,
2330
+ code: "NO_SESSION_FILE",
2331
+ message: `Pi session file missing or not openable: ${init.sessionFile}`,
2332
+ };
2333
+ }
2206
2334
  return {
2207
2335
  ok: false,
2208
2336
  code: "REOPEN_FAILED",
@@ -2256,6 +2384,7 @@ export class ConsolePiRuntime {
2256
2384
  this.sessions.set(sessionId, session);
2257
2385
  this.sessionManagers.set(sessionId, sessionManager);
2258
2386
  this.modelRuntimes.set(sessionId, built.modelRuntime);
2387
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
2259
2388
  this.sessionModels.set(sessionId, init.model);
2260
2389
  this.serviceScopes.set(sessionId, built.services);
2261
2390
  this.revisions.set(sessionId, 1);
@@ -2309,6 +2438,7 @@ export class ConsolePiRuntime {
2309
2438
  */
2310
2439
  async buildSessionWithServices(init) {
2311
2440
  const agentDir = await safeGetAgentDir(this.bindings);
2441
+ const modelConfigFile = await captureModelConfigFile(agentDir);
2312
2442
  const appendSystemPrompt = [
2313
2443
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2314
2444
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -2342,6 +2472,7 @@ export class ConsolePiRuntime {
2342
2472
  return {
2343
2473
  session,
2344
2474
  modelRuntime,
2475
+ modelConfigFile,
2345
2476
  services: {
2346
2477
  resourceLoader: services.resourceLoader,
2347
2478
  settingsManager: services.settingsManager,
@@ -2353,14 +2484,31 @@ export class ConsolePiRuntime {
2353
2484
  return this.sessions.get(sessionId);
2354
2485
  }
2355
2486
  /** Inject one queued message into the currently running Pi turn. */
2356
- async steer(sessionId, text, images) {
2487
+ async steer(sessionId, clientRequestId, text, images) {
2357
2488
  const session = this.sessions.get(sessionId);
2358
2489
  if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
2359
2490
  throw new Error("CHAT_TURN_NOT_ACTIVE");
2360
2491
  }
2361
2492
  if (typeof session.steer !== "function")
2362
2493
  throw new Error("CHAT_STEER_UNAVAILABLE");
2363
- await session.steer(text, images);
2494
+ const pending = this.pendingSteering.get(sessionId) ?? [];
2495
+ const identity = { clientRequestId, text };
2496
+ pending.push(identity);
2497
+ this.pendingSteering.set(sessionId, pending);
2498
+ try {
2499
+ await session.steer(text, images);
2500
+ }
2501
+ catch (error) {
2502
+ const current = this.pendingSteering.get(sessionId);
2503
+ if (current) {
2504
+ const index = current.indexOf(identity);
2505
+ if (index >= 0)
2506
+ current.splice(index, 1);
2507
+ if (current.length === 0)
2508
+ this.pendingSteering.delete(sessionId);
2509
+ }
2510
+ throw error;
2511
+ }
2364
2512
  }
2365
2513
  hasSession(sessionId) {
2366
2514
  return this.sessions.has(sessionId);
@@ -2396,12 +2544,13 @@ export class ConsolePiRuntime {
2396
2544
  */
2397
2545
  async prompt(sessionId, text, onEvent, options) {
2398
2546
  // Synchronous admission fence — before any await.
2399
- if (this.promptOwners.has(sessionId)) {
2547
+ if (this.promptOwners.has(sessionId) || this.reloadInFlight.has(sessionId)) {
2400
2548
  throw new Error("CHAT_TURN_ACTIVE");
2401
2549
  }
2402
2550
  this.promptOwners.add(sessionId);
2403
2551
  let unsub;
2404
2552
  let abortPromise;
2553
+ let initialUserMessageStarted = false;
2405
2554
  const onAbort = () => {
2406
2555
  // M1 T08 / AC-R3-003: Stop must abort only the current turn, NEVER
2407
2556
  // dispose the session. Await session.abort() so ownership settles
@@ -2458,6 +2607,11 @@ export class ConsolePiRuntime {
2458
2607
  if (options?.signal?.aborted) {
2459
2608
  return;
2460
2609
  }
2610
+ await this.refreshModelConfiguration(sessionId, "if-changed");
2611
+ // Stop may arrive while stat/refresh/rebind is in flight.
2612
+ if (options?.signal?.aborted) {
2613
+ return;
2614
+ }
2461
2615
  const emitContextUsage = () => {
2462
2616
  try {
2463
2617
  const contextUsage = session.getSessionStats?.()?.contextUsage;
@@ -2499,6 +2653,27 @@ export class ConsolePiRuntime {
2499
2653
  };
2500
2654
  unsub = session.subscribe((event) => {
2501
2655
  const observedAt = Date.now();
2656
+ if (event.type === "message_start" && isUserSdkMessage(event.message)) {
2657
+ if (!initialUserMessageStarted) {
2658
+ // The first user message_start belongs to session.prompt(text), not
2659
+ // to a steering request that may have raced with prompt startup.
2660
+ initialUserMessageStarted = true;
2661
+ }
2662
+ else {
2663
+ const pending = this.pendingSteering.get(sessionId);
2664
+ const accepted = pending?.shift();
2665
+ if (pending?.length === 0)
2666
+ this.pendingSteering.delete(sessionId);
2667
+ if (accepted) {
2668
+ onEvent({
2669
+ type: "steering_accepted",
2670
+ sessionId,
2671
+ clientRequestId: accepted.clientRequestId,
2672
+ text: accepted.text,
2673
+ });
2674
+ }
2675
+ }
2676
+ }
2502
2677
  if (event.type === "compaction_start") {
2503
2678
  activeCompaction = {
2504
2679
  compactionId: newCompactionId(sessionId),
@@ -2637,6 +2812,10 @@ export class ConsolePiRuntime {
2637
2812
  finally {
2638
2813
  if (this.options.questionRegistry)
2639
2814
  this.options.questionRegistry.clearTurn(sessionId);
2815
+ // Unconsumed durable DeferredTurns remain dispatching and are recovered by
2816
+ // the route layer after this active Turn closes. Never carry their volatile
2817
+ // identities into a later prompt.
2818
+ this.pendingSteering.delete(sessionId);
2640
2819
  this.promptOwners.delete(sessionId);
2641
2820
  }
2642
2821
  }
@@ -2758,11 +2937,13 @@ export class ConsolePiRuntime {
2758
2937
  this.sessionManagers.delete(sessionId);
2759
2938
  this.mainlineLeaves.delete(sessionId);
2760
2939
  this.modelRuntimes.delete(sessionId);
2940
+ this.modelConfigFiles.delete(sessionId);
2761
2941
  this.sessionModels.delete(sessionId);
2762
2942
  this.serviceScopes.delete(sessionId);
2763
2943
  this.revisions.delete(sessionId);
2764
2944
  this.thinkingSelections.delete(sessionId);
2765
2945
  this.reloadInFlight.delete(sessionId);
2946
+ this.pendingSteering.delete(sessionId);
2766
2947
  }
2767
2948
  disposeAll() {
2768
2949
  for (const id of [...this.sessions.keys()])
@@ -2795,6 +2976,11 @@ function mapAssistantMessageEvent(type, sessionId, message) {
2795
2976
  ...(content.length > 0 ? { content } : {}),
2796
2977
  };
2797
2978
  }
2979
+ function isUserSdkMessage(message) {
2980
+ return Boolean(message &&
2981
+ typeof message === "object" &&
2982
+ message.role === "user");
2983
+ }
2798
2984
  function mapSdkEvent(sessionId, event, compactionId) {
2799
2985
  switch (event.type) {
2800
2986
  case "agent_start":
@@ -2980,6 +3166,36 @@ export function createDefaultPiSdkBindings() {
2980
3166
  id: opts.sessionId,
2981
3167
  });
2982
3168
  },
3169
+ materializeSessionShell: async (opts) => {
3170
+ const sdk = (await loadSdk());
3171
+ const manager = opts.sessionManager;
3172
+ if (typeof manager.setSessionFile !== "function") {
3173
+ throw new Error("Pi SessionManager cannot materialize a session file");
3174
+ }
3175
+ let created = false;
3176
+ try {
3177
+ await mkdir(path.dirname(opts.sessionFile), { recursive: true });
3178
+ // Match Pi's documented session header. Rebinding through the public
3179
+ // SessionManager API marks it flushed, so Pi appends the first user
3180
+ // entry instead of trying to create the already-present file later.
3181
+ await writeFile(opts.sessionFile, `${JSON.stringify({
3182
+ type: "session",
3183
+ version: typeof sdk.CURRENT_SESSION_VERSION === "number"
3184
+ ? sdk.CURRENT_SESSION_VERSION
3185
+ : 3,
3186
+ id: opts.sessionId,
3187
+ timestamp: new Date().toISOString(),
3188
+ cwd: opts.cwd,
3189
+ })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3190
+ created = true;
3191
+ manager.setSessionFile(opts.sessionFile);
3192
+ }
3193
+ catch (error) {
3194
+ if (created)
3195
+ await rm(opts.sessionFile, { force: true }).catch(() => undefined);
3196
+ throw error;
3197
+ }
3198
+ },
2983
3199
  createInMemorySessionManager: async (opts) => {
2984
3200
  const sdk = (await loadSdk());
2985
3201
  return sdk.SessionManager.inMemory(opts.cwd);
@@ -2996,13 +3212,19 @@ export function createDefaultPiSdkBindings() {
2996
3212
  // wrong conversation under the durable Console sessionId. Fail closed
2997
3213
  // before open when the persisted file is absent.
2998
3214
  await access(opts.sessionFile);
2999
- return sdk.SessionManager.open(opts.sessionFile);
3000
3215
  }
3001
- catch {
3002
- // Missing/corrupt session file — caller fails closed rather than
3003
- // silently creating a fresh session and losing prior context.
3216
+ catch (error) {
3217
+ const code = error && typeof error === "object" && "code" in error
3218
+ ? String(error.code ?? "")
3219
+ : "";
3220
+ if (code === "ENOENT") {
3221
+ throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
3222
+ }
3223
+ // File exists but is unreadable (permissions / I/O) — caller maps
3224
+ // undefined to 503 rather than silently creating a fresh session.
3004
3225
  return undefined;
3005
3226
  }
3227
+ return sdk.SessionManager.open(opts.sessionFile);
3006
3228
  },
3007
3229
  resolveModel: (opts) => {
3008
3230
  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
  }