@iowarp/clio-coder 0.3.8 → 0.3.9

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 (333) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -3
  3. package/dist/{acp-U67UHUK2.js → acp-7LOELQFP.js} +6 -6
  4. package/dist/{agents-YU6SGALZ.js → agents-FIBG2SHA.js} +27 -25
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-5ZPJOIVG.js → auth-OI4LIH2I.js} +11 -12
  7. package/dist/{builtins-C6JMZVV6.js → builtins-AD25UL3C.js} +5 -5
  8. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  9. package/dist/chunk-3DPEIQKN.js +113 -0
  10. package/dist/{chunk-4SPRNWDE.js → chunk-3DUR4WUA.js} +15 -15
  11. package/dist/{chunk-VHN4MY6O.js → chunk-3MRC2YSQ.js} +2 -2
  12. package/dist/{chunk-5DHKRSMQ.js → chunk-3UUY7R3Z.js} +11 -7
  13. package/dist/{chunk-IGWKHNIQ.js → chunk-3V5AYSEQ.js} +8 -8
  14. package/dist/{chunk-FHJEP5SW.js → chunk-465CC7FK.js} +8 -5
  15. package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
  16. package/dist/{chunk-A3WNZD3P.js → chunk-4H6ULJ3H.js} +67 -21
  17. package/dist/{chunk-TB5666IT.js → chunk-4LJX2PUC.js} +3 -3
  18. package/dist/{chunk-XWSF374K.js → chunk-56KB5IJP.js} +2 -2
  19. package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
  20. package/dist/{chunk-2HEJ2F35.js → chunk-5HFBWUMU.js} +20 -8
  21. package/dist/{chunk-WNIJTQQK.js → chunk-5PVQ4SRS.js} +78 -6
  22. package/dist/{chunk-DYIM5TJT.js → chunk-5QKCQQ3E.js} +262 -6
  23. package/dist/{chunk-TYPGUK6W.js → chunk-5T7RBWN2.js} +111 -5
  24. package/dist/{chunk-IIZWH4XA.js → chunk-774ILSRL.js} +2 -2
  25. package/dist/chunk-7C6RYZGQ.js +391 -0
  26. package/dist/{chunk-TANS5ZJS.js → chunk-AD7Y7STJ.js} +3 -3
  27. package/dist/{chunk-RWSI4YD7.js → chunk-AEYBF3TB.js} +33 -12
  28. package/dist/{chunk-DGSYXYMX.js → chunk-AMKHQW3C.js} +2 -2
  29. package/dist/{chunk-VWZOAB7K.js → chunk-B5XRQOLB.js} +7 -7
  30. package/dist/{chunk-WXY7KU3G.js → chunk-BVDVID7E.js} +2 -2
  31. package/dist/{chunk-PMDBGQSJ.js → chunk-CA42X6KT.js} +2 -2
  32. package/dist/{chunk-HLE42MG7.js → chunk-D73KXYPF.js} +3 -3
  33. package/dist/{chunk-5Q2VVUKB.js → chunk-DG4M6ZUE.js} +3 -3
  34. package/dist/{chunk-ME6CCNFO.js → chunk-EBOC7MT3.js} +6 -6
  35. package/dist/{chunk-MXKJU4JB.js → chunk-ECUO3KDP.js} +48 -7
  36. package/dist/{chunk-26LEYJZH.js → chunk-FALJGAWU.js} +2 -2
  37. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  38. package/dist/{chunk-7RGZWPB6.js → chunk-GAYUJ7LE.js} +67 -13
  39. package/dist/{chunk-VCBR6CU7.js → chunk-HAY4ZE2P.js} +2 -2
  40. package/dist/{chunk-FBVTI2TJ.js → chunk-HCBCAYZU.js} +11 -130
  41. package/dist/{chunk-WSB3FPX7.js → chunk-HJB5IUKP.js} +32 -136
  42. package/dist/{chunk-J3YUBZWY.js → chunk-HKO36JWF.js} +33 -5
  43. package/dist/{chunk-E77JEWSD.js → chunk-HPCTNZM2.js} +6 -36
  44. package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
  45. package/dist/{chunk-NMPKI6XL.js → chunk-JEQQR47K.js} +37 -18
  46. package/dist/{chunk-3BINW3FP.js → chunk-KV2AOLDF.js} +24 -4
  47. package/dist/{chunk-YS5VLNH5.js → chunk-LXPJXFM5.js} +7 -7
  48. package/dist/{chunk-7RFXX52T.js → chunk-MIX5N5AC.js} +271 -41
  49. package/dist/{chunk-K4XHGFR5.js → chunk-MLOK6ZOS.js} +1297 -218
  50. package/dist/{chunk-ZNLWCMVZ.js → chunk-MV2VUEJC.js} +2 -2
  51. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  52. package/dist/{chunk-5H3GB5BO.js → chunk-N3PBVRTZ.js} +4 -382
  53. package/dist/{chunk-2HFZQUHL.js → chunk-N5XKWMDW.js} +17 -7
  54. package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
  55. package/dist/{chunk-GU2UIAFZ.js → chunk-NQ6UCCOD.js} +3 -3
  56. package/dist/{chunk-N22QMJKY.js → chunk-NZU6YDNV.js} +4 -4
  57. package/dist/{chunk-ZVJ5BLO2.js → chunk-O6I4CIEU.js} +151 -13
  58. package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
  59. package/dist/{chunk-VAWNZU7Z.js → chunk-P3JGPQFL.js} +2 -2
  60. package/dist/{chunk-IJ7RPIYJ.js → chunk-PNY46YEY.js} +20 -3
  61. package/dist/{chunk-U6MBIEMB.js → chunk-PZ4I4JE2.js} +56 -35
  62. package/dist/{chunk-GPIEI3LY.js → chunk-QQ7EKM72.js} +2 -2
  63. package/dist/{chunk-WLFILSD5.js → chunk-R7LNVMCS.js} +66 -28
  64. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  65. package/dist/chunk-RKKLTLYB.js +45 -0
  66. package/dist/{chunk-TT36MB5S.js → chunk-RKRLDWD3.js} +3 -1
  67. package/dist/{chunk-TTHACPOM.js → chunk-S4COXYBG.js} +456 -18
  68. package/dist/{chunk-WWCZ5F23.js → chunk-T3Z6VAAF.js} +69 -10
  69. package/dist/{chunk-GN57SG4G.js → chunk-TD7UE2L5.js} +9 -7
  70. package/dist/{chunk-TLQJPP24.js → chunk-TEO2TLVN.js} +523 -322
  71. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  72. package/dist/{chunk-KTYTFRMB.js → chunk-VKBMFOYV.js} +17 -15
  73. package/dist/{chunk-PT7HYKEM.js → chunk-VO2LKSTM.js} +2 -2
  74. package/dist/{chunk-P43ETTHK.js → chunk-VPTUJU4P.js} +2 -2
  75. package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
  76. package/dist/{chunk-EMYUUSFG.js → chunk-WXCJ7VME.js} +5 -5
  77. package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
  78. package/dist/{chunk-JOZYP4GM.js → chunk-YKOFT37S.js} +5 -5
  79. package/dist/chunk-YSEHGPCT.js +127 -0
  80. package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
  81. package/dist/cli/index.js +27 -27
  82. package/dist/{clio-QVTYJ57A.js → clio-LT5V7SSZ.js} +6 -6
  83. package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +4 -4
  84. package/dist/{config-LW5IJFQN.js → config-RXS5T3JT.js} +71 -43
  85. package/dist/{configure-7XIZCOU4.js → configure-2WYWSCSD.js} +14 -15
  86. package/dist/{context-Y6Y7QPR6.js → context-I3BTOTCS.js} +12 -12
  87. package/dist/{context-L3WL3X7K.js → context-MVOORGMF.js} +34 -33
  88. package/dist/{context-N52ZA626.js → context-PALKKQYL.js} +20 -20
  89. package/dist/{context-clear-MBQRLSDQ.js → context-clear-N2WOYZ2K.js} +34 -33
  90. package/dist/{context-index-HVMFQHK3.js → context-index-HNG3MOME.js} +2 -2
  91. package/dist/{context-working-set-GS6DSO7F.js → context-working-set-MIEVECVZ.js} +10 -11
  92. package/dist/{dispatch-runner-22ZCNOM3.js → dispatch-runner-VVA4SRRH.js} +34 -33
  93. package/dist/doctor-TWBWFK5V.js +165 -0
  94. package/dist/{eval-BEC2WHDA.js → eval-IJ5VEZDJ.js} +2016 -142
  95. package/dist/{evidence-REJUMSKM.js → evidence-L5APPXNV.js} +29 -28
  96. package/dist/{evolve-PY5ZBA5K.js → evolve-RGNKFJ52.js} +29 -28
  97. package/dist/{extensions-HVKU65YU.js → extensions-7WYWUX5A.js} +9 -3
  98. package/dist/{fleet-7WZEWRFA.js → fleet-6CNVBZZP.js} +87 -55
  99. package/dist/{fleet-commands-UVHWM76J.js → fleet-commands-L2SXSYEI.js} +6 -6
  100. package/dist/{fleet-graph-6ULH7PES.js → fleet-graph-2J3OOIPO.js} +14 -12
  101. package/dist/{fleet-preflight-J53T6CCE.js → fleet-preflight-CZRJ4JP5.js} +3 -4
  102. package/dist/{fleet-validate-72PC4SLA.js → fleet-validate-C5RI6DP7.js} +16 -15
  103. package/dist/{init-OG3TPGQG.js → init-VBN2ACVA.js} +50 -48
  104. package/dist/{library-CNTMPLRF.js → library-JHGUMLY2.js} +13 -11
  105. package/dist/{memory-6IS7F275.js → memory-K4OQIYWG.js} +31 -30
  106. package/dist/{models-ENRJDA5W.js → models-2NCZUWDD.js} +23 -22
  107. package/dist/{monitor-XLDVO7TN.js → monitor-MMVTJABD.js} +35 -34
  108. package/dist/{orchestrator-6KSPYRHA.js → orchestrator-ZKBPCHW6.js} +1627 -294
  109. package/dist/{reset-RZ4ER727.js → reset-DD5JGOY3.js} +3 -3
  110. package/dist/{run-Y2CNK5RU.js → run-QEGNX7FL.js} +56 -55
  111. package/dist/{share-A55GYP6Z.js → share-JKD3BQMW.js} +13 -11
  112. package/dist/{skills-ALC5J6AT.js → skills-LMQIKDOZ.js} +14 -12
  113. package/dist/{skills-eval-JPBEBYQU.js → skills-eval-I7X2774U.js} +34 -32
  114. package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
  115. package/dist/{support-MIETYA5E.js → support-I7LOJLIF.js} +4 -4
  116. package/dist/{targets-VGNXIR3S.js → targets-RUSR6B5Z.js} +58 -32
  117. package/dist/{terminal-lease-WOBR64YA.js → terminal-lease-QYVORFR4.js} +6 -4
  118. package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
  119. package/dist/{upgrade-FUSUAGHR.js → upgrade-XANW3FXB.js} +17 -16
  120. package/dist/{usage-N4MKVHKD.js → usage-4H7ZRXQT.js} +88 -44
  121. package/dist/{verifiers-YAWOJ3H2.js → verifiers-UZXNBZEB.js} +6 -6
  122. package/dist/{verify-LTDHYBGY.js → verify-BVKWTNDL.js} +5 -5
  123. package/dist/{wiki-generate-6M7GHTBJ.js → wiki-generate-MY7WV2QI.js} +49 -47
  124. package/dist/worker/entry.js +29 -33
  125. package/docs/alcf-provider.md +1 -1
  126. package/docs/architecture.md +1 -1
  127. package/docs/artifact-versions.md +6 -1
  128. package/docs/built-in-agents.md +1 -1
  129. package/docs/capacity-and-scheduling.md +23 -2
  130. package/docs/commands-and-modes.md +1 -1
  131. package/docs/configuration-and-targets.md +30 -3
  132. package/docs/context-engine.md +63 -4
  133. package/docs/documentation-coverage.md +3 -3
  134. package/docs/documentation-guide.md +1 -1
  135. package/docs/environment-variables.md +2 -0
  136. package/docs/eval-runner.md +262 -11
  137. package/docs/evals-internal.md +72 -2
  138. package/docs/evidence-and-memory.md +11 -10
  139. package/docs/evolution.md +1 -1
  140. package/docs/extensions-and-sharing.md +3 -1
  141. package/docs/fleet-dispatch.md +4 -4
  142. package/docs/installation-and-lifecycle.md +1 -1
  143. package/docs/middleware-and-components.md +1 -1
  144. package/docs/model-catalog.md +1 -1
  145. package/docs/observability.md +53 -2
  146. package/docs/proactive-memory.md +127 -14
  147. package/docs/prompt-envelope-and-tools.md +19 -1
  148. package/docs/provider-adapter-cookbook.md +1 -1
  149. package/docs/release-cut-checklist.md +19 -3
  150. package/docs/safety-model.md +1 -1
  151. package/docs/scientific-validation.md +1 -1
  152. package/docs/skills-marketplace.md +1 -1
  153. package/docs/tool-usage.md +1 -1
  154. package/docs/trace-store.md +1 -1
  155. package/docs/troubleshooting.md +87 -0
  156. package/docs/tui-design.md +1 -1
  157. package/docs/worker-dispatch-mechanics.md +1 -1
  158. package/package.json +2 -1
  159. package/src/cli/agents.ts +1 -1
  160. package/src/cli/config-inspect.ts +33 -6
  161. package/src/cli/config.ts +1 -1
  162. package/src/cli/doctor-state-size.ts +82 -0
  163. package/src/cli/doctor.ts +3 -1
  164. package/src/cli/eval.ts +80 -16
  165. package/src/cli/extensions.ts +5 -1
  166. package/src/cli/fleet.ts +32 -3
  167. package/src/cli/targets.ts +44 -13
  168. package/src/cli/trace.ts +63 -4
  169. package/src/cli/usage.ts +63 -14
  170. package/src/core/bus-events.ts +29 -1
  171. package/src/core/cache-telemetry.ts +42 -0
  172. package/src/core/config.ts +18 -0
  173. package/src/core/defaults.ts +36 -6
  174. package/src/core/endpoint-key.ts +27 -0
  175. package/src/core/residency-target-key.ts +25 -0
  176. package/src/core/response-schema.ts +36 -2
  177. package/src/domains/config/classify.ts +3 -0
  178. package/src/domains/context/codewiki/coordinator.ts +12 -4
  179. package/src/domains/dispatch/admission.ts +40 -3
  180. package/src/domains/dispatch/capacity-lease.ts +98 -9
  181. package/src/domains/dispatch/contract.ts +11 -0
  182. package/src/domains/dispatch/execution-plan.ts +44 -4
  183. package/src/domains/dispatch/extension.ts +166 -42
  184. package/src/domains/dispatch/fleet-run.ts +23 -3
  185. package/src/domains/dispatch/heartbeat.ts +32 -8
  186. package/src/domains/dispatch/index.ts +3 -0
  187. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  188. package/src/domains/dispatch/reservation-store.ts +116 -8
  189. package/src/domains/dispatch/state.ts +4 -0
  190. package/src/domains/dispatch/worker-spawn.ts +25 -11
  191. package/src/domains/dispatch/write-boundary-enforcer.ts +20 -3
  192. package/src/domains/dispatch/write-boundary.ts +62 -1
  193. package/src/domains/eval/artifacts/store.ts +62 -0
  194. package/src/domains/eval/compare/behavioral.ts +224 -0
  195. package/src/domains/eval/compare/compare.ts +355 -2
  196. package/src/domains/eval/compare/envelope.ts +128 -0
  197. package/src/domains/eval/compare/gates.ts +24 -6
  198. package/src/domains/eval/compare/thresholds.ts +30 -3
  199. package/src/domains/eval/execution-provenance.ts +240 -0
  200. package/src/domains/eval/metrics/aggregate.ts +136 -0
  201. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  202. package/src/domains/eval/metrics/tracked.ts +413 -0
  203. package/src/domains/eval/provenance.ts +117 -0
  204. package/src/domains/eval/reports/comparison.ts +128 -0
  205. package/src/domains/eval/reports/junit.ts +17 -3
  206. package/src/domains/eval/reports/markdown.ts +3 -3
  207. package/src/domains/eval/reports/text.ts +14 -0
  208. package/src/domains/eval/run-compare.ts +20 -0
  209. package/src/domains/eval/runners/clio-run.ts +127 -0
  210. package/src/domains/eval/runners/external-command.ts +28 -3
  211. package/src/domains/eval/schema/adapter.ts +111 -0
  212. package/src/domains/eval/schema/artifact.ts +20 -0
  213. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  214. package/src/domains/eval/schema/behavioral.ts +520 -0
  215. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  216. package/src/domains/eval/schema/serving.ts +74 -0
  217. package/src/domains/eval/schema/suite.ts +38 -8
  218. package/src/domains/eval/schema/validate.ts +58 -3
  219. package/src/domains/eval/schema/verdict.ts +237 -0
  220. package/src/domains/eval/suites/resolve.ts +2 -0
  221. package/src/domains/eval/suites/run.ts +264 -33
  222. package/src/domains/eval/verifiers/command.ts +2 -1
  223. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  224. package/src/domains/evidence/build.ts +2 -13
  225. package/src/domains/evidence/eval.ts +2 -12
  226. package/src/domains/evidence/findings-markdown.ts +33 -0
  227. package/src/domains/evidence/run-trust.ts +7 -113
  228. package/src/domains/evidence/trust-projection.ts +2 -2
  229. package/src/domains/extensions/compatibility.ts +285 -0
  230. package/src/domains/extensions/discovery.ts +38 -3
  231. package/src/domains/extensions/resources.ts +1 -1
  232. package/src/domains/extensions/state.ts +12 -3
  233. package/src/domains/extensions/types.ts +2 -0
  234. package/src/domains/lifecycle/doctor.ts +69 -1
  235. package/src/domains/memory/index.ts +14 -0
  236. package/src/domains/memory/task-bank-promotion.ts +64 -0
  237. package/src/domains/memory/task-memory-policy.ts +77 -8
  238. package/src/domains/memory/task-memory-spend.ts +131 -0
  239. package/src/domains/memory/task-memory-status.ts +7 -0
  240. package/src/domains/memory/task-memory-telemetry.ts +2 -0
  241. package/src/domains/middleware/index.ts +1 -0
  242. package/src/domains/middleware/memory-intervention.ts +69 -5
  243. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  244. package/src/domains/observability/background-memory-usage.ts +140 -0
  245. package/src/domains/observability/cost.ts +1 -1
  246. package/src/domains/observability/index.ts +7 -0
  247. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  248. package/src/domains/observability/trace-store.ts +192 -2
  249. package/src/domains/prompts/compiler.ts +100 -13
  250. package/src/domains/providers/endpoint-capacity.ts +96 -0
  251. package/src/domains/providers/index.ts +10 -0
  252. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  253. package/src/domains/providers/runtime-resolution.ts +8 -1
  254. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  255. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  256. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  257. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  258. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  259. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  260. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  261. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  262. package/src/domains/providers/types/capability-flags.ts +2 -0
  263. package/src/domains/providers/types/target-descriptor.ts +2 -0
  264. package/src/domains/resources/prompts/loader.ts +95 -33
  265. package/src/domains/safety/call-target.ts +52 -0
  266. package/src/domains/safety/run-effects.ts +35 -4
  267. package/src/domains/session/context-accounting.ts +52 -1
  268. package/src/domains/session/context-ledger.ts +37 -13
  269. package/src/domains/session/index.ts +6 -0
  270. package/src/domains/session/prompt-cache.ts +140 -0
  271. package/src/domains/session/prompt-manifest.ts +42 -0
  272. package/src/engine/acp/adapter.ts +18 -3
  273. package/src/engine/ai.ts +35 -0
  274. package/src/engine/apis/llamacpp-residency.ts +55 -3
  275. package/src/engine/apis/lmstudio.ts +25 -5
  276. package/src/engine/apis/ollama-native.ts +2 -1
  277. package/src/engine/apis/openai-completions.ts +80 -17
  278. package/src/engine/apis/residency-lock.ts +3 -1
  279. package/src/engine/apis/residency.ts +34 -1
  280. package/src/engine/provider-payload.ts +29 -1
  281. package/src/entry/orchestrator.ts +176 -30
  282. package/src/interactive/chat-loop-messages.ts +26 -7
  283. package/src/interactive/chat-loop.ts +318 -41
  284. package/src/interactive/chat-panel.ts +62 -8
  285. package/src/interactive/clio-editor.ts +45 -8
  286. package/src/interactive/context-activity.ts +5 -1
  287. package/src/interactive/context-meter.ts +1 -1
  288. package/src/interactive/context-overlay.ts +40 -10
  289. package/src/interactive/cost-overlay.ts +64 -6
  290. package/src/interactive/dispatch-board.ts +84 -12
  291. package/src/interactive/fleet-run-preview.ts +41 -15
  292. package/src/interactive/handoff-round.ts +41 -2
  293. package/src/interactive/interactive-application.ts +24 -1
  294. package/src/interactive/interactive-input-runtime.ts +8 -0
  295. package/src/interactive/interactive-presentation.ts +4 -0
  296. package/src/interactive/interactive-shell.ts +20 -17
  297. package/src/interactive/interactive-slash-runtime.ts +27 -4
  298. package/src/interactive/memory-overlay.ts +8 -0
  299. package/src/interactive/mutation-preview.ts +295 -0
  300. package/src/interactive/overlay-general-openers.ts +16 -0
  301. package/src/interactive/overlay-key-routing.ts +38 -0
  302. package/src/interactive/overlay-lifecycle.ts +38 -5
  303. package/src/interactive/overlay-permission-lifecycle.ts +22 -2
  304. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  305. package/src/interactive/overlays/ask-user.ts +91 -19
  306. package/src/interactive/overlays/help-reference.ts +4 -0
  307. package/src/interactive/overlays/prompts.ts +11 -1
  308. package/src/interactive/overlays/settings.ts +35 -1
  309. package/src/interactive/permission-hint.ts +34 -2
  310. package/src/interactive/permission-overlay.ts +159 -9
  311. package/src/interactive/prewarm.ts +197 -0
  312. package/src/interactive/render-trace.ts +162 -15
  313. package/src/interactive/renderers/tool-execution.ts +4 -0
  314. package/src/interactive/side-question.ts +58 -1
  315. package/src/interactive/status/controller.ts +11 -0
  316. package/src/interactive/status/state-machine.ts +54 -2
  317. package/src/interactive/status/types.ts +7 -0
  318. package/src/interactive/terminal-lease.ts +2 -0
  319. package/src/interactive/turn-context.ts +299 -31
  320. package/src/interactive/turn-persistence.ts +14 -4
  321. package/src/interactive/turn-prewarm.ts +364 -0
  322. package/src/interactive/turn-queues.ts +7 -4
  323. package/src/interactive/turn-runtime.ts +8 -1
  324. package/src/interactive/turn-state.ts +23 -0
  325. package/src/interactive/view/view-overlay.ts +28 -3
  326. package/src/tools/ask-user.ts +43 -2
  327. package/src/tools/dispatch-plan.ts +17 -9
  328. package/src/tools/dispatch-scout.ts +1 -1
  329. package/src/tools/registry.ts +16 -0
  330. package/dist/chunk-AOCYTWAV.js +0 -449
  331. package/dist/chunk-HWUFFB6L.js +0 -83
  332. package/dist/chunk-R346GLFC.js +0 -31
  333. package/dist/doctor-M7YEDGAE.js +0 -91
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The session pre-warm round: the request the next turn would send, minus the
3
+ * operator's text, issued as soon as the prefix is known.
4
+ *
5
+ * On a local server prefill is the cost. A fresh session's first turn prefills
6
+ * the whole compiled prompt plus the tool schemas before the model emits a
7
+ * token, and a resumed session's first turn prefills the entire replayed
8
+ * history. Both are paid after the operator presses Enter, and both are fully
9
+ * determined before they type anything. llama.cpp picks the slot with the
10
+ * longest common prefix and re-evaluates only the suffix, so sending the prefix
11
+ * early leaves the processed KV in the slot for the real turn to land on.
12
+ *
13
+ * The payload is not hand-assembled. It goes through `streamSimple`, the same
14
+ * dispatcher `createEngineAgent` hands pi-agent-core as its `streamFn`, with the
15
+ * same system prompt, the same tool schemas, the same replayed messages, and the
16
+ * same thinking level, so `withSamplingOverrides`, the thinking composition, and
17
+ * `cache_prompt` resolve exactly as they do for a turn. Any byte that differs
18
+ * ahead of the user turn would defeat the purpose.
19
+ */
20
+
21
+ import type { BackendCompletionTimings } from "../core/cache-telemetry.js";
22
+ import { streamSimple } from "../engine/ai.js";
23
+ import type { AgentMessage, AgentTool, EngineModel, Usage } from "../engine/types.js";
24
+
25
+ /**
26
+ * The one user message appended so the chat template renders the prefix up to
27
+ * the user turn. One character: it has to exist for the template to close the
28
+ * prefix, and everything after the last shared byte is re-evaluated by the real
29
+ * turn anyway.
30
+ */
31
+ export const PREWARM_USER_TEXT = ".";
32
+
33
+ /** Output budget for the round. The tokens the model produces are not the point. */
34
+ export const PREWARM_MAX_TOKENS = 1;
35
+
36
+ /** Which trigger asked for the pre-warm. Recorded on the ledger entry. */
37
+ export type PrewarmTrigger = "session-start" | "resume" | "compaction";
38
+
39
+ export interface PrewarmContext {
40
+ systemPrompt: string;
41
+ messages: ReadonlyArray<AgentMessage>;
42
+ tools: ReadonlyArray<AgentTool>;
43
+ }
44
+
45
+ /**
46
+ * The roles pi-agent-core's default `convertToLlm` keeps when it turns the
47
+ * agent's message list into provider messages. The pre-warm applies the same
48
+ * filter so its message array is the one a turn would send, not the agent's
49
+ * internal list.
50
+ */
51
+ const LLM_MESSAGE_ROLES: ReadonlySet<string> = new Set(["user", "assistant", "toolResult"]);
52
+
53
+ /**
54
+ * The prefix of the next turn's request: system prompt, tool schemas, and the
55
+ * replayed conversation, with no user turn yet.
56
+ */
57
+ function prewarmPrefix(state: {
58
+ systemPrompt: string;
59
+ messages: ReadonlyArray<AgentMessage>;
60
+ tools: ReadonlyArray<AgentTool>;
61
+ }): PrewarmContext {
62
+ return {
63
+ systemPrompt: state.systemPrompt,
64
+ messages: state.messages.filter((message) => LLM_MESSAGE_ROLES.has((message as { role?: string }).role ?? "")),
65
+ tools: [...state.tools],
66
+ };
67
+ }
68
+
69
+ export interface PrewarmRoundInput {
70
+ model: EngineModel;
71
+ /** Live agent state the next turn would send. Read-only; the round copies it. */
72
+ state: {
73
+ systemPrompt: string;
74
+ messages: ReadonlyArray<AgentMessage>;
75
+ tools: ReadonlyArray<AgentTool>;
76
+ thinkingLevel: string;
77
+ };
78
+ apiKey?: string;
79
+ signal?: AbortSignal;
80
+ /** Test seam. Production uses the engine dispatcher a turn runs on. */
81
+ streamFn?: typeof streamSimple;
82
+ }
83
+
84
+ export interface PrewarmRoundResult {
85
+ /** True when the operator submitted (or the session moved) before the round settled. */
86
+ aborted: boolean;
87
+ /** Provider usage for the round; null when the backend reported none. */
88
+ usage: Usage | null;
89
+ /** Backend prefill and prediction timings when the server reported them. */
90
+ backend: BackendCompletionTimings | null;
91
+ /** Wall clock the round spent, in the shape a turn's assistant entry uses. */
92
+ timing: { ttftMs: number | null; apiMs: number };
93
+ /** Provider error text when the round failed outright. */
94
+ errorMessage: string | null;
95
+ }
96
+
97
+ function usageOf(message: unknown): Usage | null {
98
+ if (message === null || typeof message !== "object") return null;
99
+ const usage = (message as { usage?: unknown }).usage;
100
+ return usage !== null && typeof usage === "object" ? (usage as Usage) : null;
101
+ }
102
+
103
+ function backendOf(message: unknown): BackendCompletionTimings | null {
104
+ if (message === null || typeof message !== "object") return null;
105
+ const backend = (message as { backendTimings?: unknown }).backendTimings;
106
+ return backend !== null && typeof backend === "object" ? (backend as BackendCompletionTimings) : null;
107
+ }
108
+
109
+ /**
110
+ * Prompt tokens the backend actually processed for one pre-warm. The server's
111
+ * own prompt count is authoritative when it reported one; otherwise the
112
+ * provider usage prompt side, cached tokens folded in, is the honest figure.
113
+ * Null when neither exists, which is different from a measured zero.
114
+ */
115
+ export function prewarmPromptTokens(result: Pick<PrewarmRoundResult, "usage" | "backend">): number | null {
116
+ if (result.backend) return result.backend.promptTokens;
117
+ const usage = result.usage;
118
+ if (!usage) return null;
119
+ const total = (usage.input || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
120
+ return total > 0 ? total : null;
121
+ }
122
+
123
+ /**
124
+ * Send the prefix and wait for the backend to acknowledge it. Resolves rather
125
+ * than rejects on a provider failure: a pre-warm is an optimization, and a
126
+ * backend that refuses it must not be able to break the session it was warming.
127
+ */
128
+ export async function runPrewarmRound(input: PrewarmRoundInput): Promise<PrewarmRoundResult> {
129
+ const prefix = prewarmPrefix(input.state);
130
+ const context = {
131
+ systemPrompt: prefix.systemPrompt,
132
+ messages: [
133
+ ...prefix.messages,
134
+ { role: "user", content: [{ type: "text", text: PREWARM_USER_TEXT }], timestamp: Date.now() },
135
+ ],
136
+ tools: prefix.tools,
137
+ };
138
+ // pi-agent-core maps `thinkingLevel: "off"` onto an absent `reasoning`; the
139
+ // pre-warm has to make the same mapping or the thinking composition resolves
140
+ // against a different level and the rendered template moves.
141
+ const options: Record<string, unknown> = { maxTokens: PREWARM_MAX_TOKENS };
142
+ if (input.state.thinkingLevel !== "off") options.reasoning = input.state.thinkingLevel;
143
+ if (input.apiKey !== undefined) options.apiKey = input.apiKey;
144
+ if (input.signal !== undefined) options.signal = input.signal;
145
+
146
+ const send = input.streamFn ?? streamSimple;
147
+ const startedAt = performance.now();
148
+ let firstDeltaAt: number | null = null;
149
+ const elapsed = (): number => Math.round(Math.max(0, performance.now() - startedAt));
150
+ const ttft = (): number | null => (firstDeltaAt === null ? null : Math.round(Math.max(0, firstDeltaAt - startedAt)));
151
+
152
+ try {
153
+ const events = send(
154
+ input.model,
155
+ context as unknown as Parameters<typeof streamSimple>[1],
156
+ options as unknown as Parameters<typeof streamSimple>[2],
157
+ );
158
+ for await (const event of events) {
159
+ if (firstDeltaAt === null && event.type !== "start") firstDeltaAt = performance.now();
160
+ if (event.type === "done") {
161
+ return {
162
+ aborted: false,
163
+ usage: usageOf(event.message),
164
+ backend: backendOf(event.message),
165
+ timing: { ttftMs: ttft(), apiMs: elapsed() },
166
+ errorMessage: null,
167
+ };
168
+ }
169
+ if (event.type === "error") {
170
+ const failed = event.error as { stopReason?: unknown; errorMessage?: unknown };
171
+ const aborted = event.reason === "aborted" || failed.stopReason === "aborted" || input.signal?.aborted === true;
172
+ return {
173
+ aborted,
174
+ usage: usageOf(event.error),
175
+ backend: backendOf(event.error),
176
+ timing: { ttftMs: ttft(), apiMs: elapsed() },
177
+ errorMessage: aborted ? null : typeof failed.errorMessage === "string" ? failed.errorMessage : "pre-warm failed",
178
+ };
179
+ }
180
+ }
181
+ } catch (error) {
182
+ return {
183
+ aborted: input.signal?.aborted === true,
184
+ usage: null,
185
+ backend: null,
186
+ timing: { ttftMs: ttft(), apiMs: elapsed() },
187
+ errorMessage: input.signal?.aborted === true ? null : error instanceof Error ? error.message : String(error),
188
+ };
189
+ }
190
+ return {
191
+ aborted: input.signal?.aborted === true,
192
+ usage: null,
193
+ backend: null,
194
+ timing: { ttftMs: ttft(), apiMs: elapsed() },
195
+ errorMessage: null,
196
+ };
197
+ }
@@ -1,6 +1,6 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { appendFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import type { TuiRenderObserver, TuiRenderPhase } from "../engine/tui.js";
5
5
 
6
6
  /** The trace format is append-only JSONL so partial sessions remain readable. */
@@ -106,6 +106,106 @@ export type RenderTraceRecord =
106
106
  }
107
107
  | { type: "trace_drop"; version: typeof RENDER_TRACE_VERSION; at: number; records: number };
108
108
 
109
+ /** One byte-level delivery from the stdin reader to the application listener. */
110
+ export type RenderTraceInputIngressRecord = Extract<RenderTraceRecord, { type: "input_ingress" }>;
111
+
112
+ /**
113
+ * How many `input_ingress` records and how many committed frame records the
114
+ * always-on ring keeps, each. 256 keystrokes and 256 committed frames cover the
115
+ * last stretch of interaction an operator would still be describing when they
116
+ * kill a wedged pane, and cost about 200 KB of process memory in the worst case
117
+ * (a frame record carries its terminal-write commits).
118
+ */
119
+ export const INPUT_WEDGE_RING_CAPACITY = 256;
120
+
121
+ /** Subdirectory of the state directory that holds SIGTERM wedge dumps. */
122
+ export const INPUT_WEDGE_DUMP_DIRNAME = "input-wedge";
123
+
124
+ /** Dumps kept in that directory; older ones are removed as new ones land. */
125
+ export const INPUT_WEDGE_DUMP_RETAINED = 5;
126
+
127
+ /**
128
+ * Which half of the input pipeline was still moving when the dump was taken.
129
+ *
130
+ * `input-not-committed` is the consumer's failure: bytes reached the
131
+ * application listener and no frame that carries them ever reached stdout.
132
+ * `no-input-recorded` is the reader's: nothing was delivered at all, so an
133
+ * operator who was typing was typing into a reader that had stopped.
134
+ * `input-committed` says neither half of this pipeline was stuck.
135
+ */
136
+ export type InputWedgeClassification = "no-input-recorded" | "input-not-committed" | "input-committed";
137
+
138
+ export interface InputWedgeSnapshot {
139
+ version: typeof RENDER_TRACE_VERSION;
140
+ capacity: number;
141
+ pid: number;
142
+ /** Wall clock at trace open, so a record's relative `at` maps onto real time. */
143
+ openedAtEpochMs: number;
144
+ atEpochMs: number;
145
+ /** Milliseconds since trace open, the same clock every record's `at` uses. */
146
+ at: number;
147
+ classification: InputWedgeClassification;
148
+ msSinceLastInputIngress: number | null;
149
+ msSinceLastCommittedFrame: number | null;
150
+ inputIngress: RenderTraceInputIngressRecord[];
151
+ frames: RenderTraceFrameRecord[];
152
+ }
153
+
154
+ /** Bounded FIFO that overwrites its oldest entry rather than growing. */
155
+ function createRing<T>(capacity: number): { push(value: T): void; toArray(): T[] } {
156
+ const slots: T[] = [];
157
+ let next = 0;
158
+ return {
159
+ push(value): void {
160
+ if (slots.length < capacity) slots.push(value);
161
+ else slots[next] = value;
162
+ next = (next + 1) % capacity;
163
+ },
164
+ toArray(): T[] {
165
+ if (slots.length < capacity) return [...slots];
166
+ return [...slots.slice(next), ...slots.slice(0, next)];
167
+ },
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Read the two rings the way the ticket asks the wedge to be read: the newest
173
+ * input the operator should have seen, against the newest frame that reached
174
+ * stdout carrying it.
175
+ */
176
+ function classifyInputWedge(
177
+ inputIngress: ReadonlyArray<RenderTraceInputIngressRecord>,
178
+ frames: ReadonlyArray<RenderTraceFrameRecord>,
179
+ ): InputWedgeClassification {
180
+ const newest = [...inputIngress].reverse().find((record) => record.visualExpected) ?? inputIngress.at(-1);
181
+ if (newest === undefined) return "no-input-recorded";
182
+ return frames.some((frame) => frame.inputHighWater >= newest.inputSeq) ? "input-committed" : "input-not-committed";
183
+ }
184
+
185
+ /**
186
+ * Write one snapshot under `<stateDir>/input-wedge/`, newest-first by name, and
187
+ * drop everything past the retention bound. Synchronous on purpose: the caller
188
+ * is a signal handler on a process that is about to end.
189
+ */
190
+ export function writeInputWedgeDump(stateDir: string, snapshot: InputWedgeSnapshot): string {
191
+ const directory = join(stateDir, INPUT_WEDGE_DUMP_DIRNAME);
192
+ mkdirSync(directory, { recursive: true });
193
+ const stamp = new Date(snapshot.atEpochMs).toISOString().replace(/[:.]/gu, "-");
194
+ const path = join(directory, `${stamp}-${snapshot.pid}.json`);
195
+ writeFileSync(path, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
196
+ try {
197
+ const existing = readdirSync(directory)
198
+ .filter((name) => name.endsWith(".json"))
199
+ .sort();
200
+ for (const name of existing.slice(0, Math.max(0, existing.length - INPUT_WEDGE_DUMP_RETAINED))) {
201
+ rmSync(join(directory, name), { force: true });
202
+ }
203
+ } catch {
204
+ // Retention is housekeeping; a dump that landed is worth more than a tidy directory.
205
+ }
206
+ return path;
207
+ }
208
+
109
209
  interface TraceWriter {
110
210
  enqueue(record: RenderTraceRecord): void;
111
211
  close(): Promise<void>;
@@ -236,6 +336,8 @@ export interface RenderTrace extends TuiRenderObserver {
236
336
  recordTerminalDrain(writeId: number): void;
237
337
  currentFrameId(): number | null;
238
338
  onFirstFrameCommit(listener: (frameId: number) => void): void;
339
+ /** The always-on ring as it stands, classified. Allocates; call it once, on the way out. */
340
+ snapshotInputWedge(): InputWedgeSnapshot;
239
341
  close(): Promise<void>;
240
342
  }
241
343
 
@@ -255,17 +357,32 @@ export function renderTracePath(env: NodeJS.ProcessEnv = process.env): string |
255
357
  return raw && raw.length > 0 ? raw : null;
256
358
  }
257
359
 
360
+ /**
361
+ * One tracer, two sinks. The JSONL file is the armed sink: it exists only when
362
+ * `CLIO_CODER_RENDER_TRACE` named a path, and it keeps every record. The
363
+ * bounded in-memory ring is the always-on sink: it keeps the last
364
+ * {@link INPUT_WEDGE_RING_CAPACITY} `input_ingress` records and the same number
365
+ * of committed frames, so the SIGTERM that recovers a wedged pane can still say
366
+ * which half of the input pipeline stopped (#224). A null `path` is the
367
+ * ring-only mode, and there the recorders that only ever fed the file keep
368
+ * their counters and skip building a record nothing would read.
369
+ */
258
370
  export function createRenderTrace(
259
- path: string,
371
+ path: string | null,
260
372
  now: () => number = () => performance.now(),
261
373
  writerOptions: AsyncTraceWriterOptions = {},
262
374
  ): RenderTrace {
263
375
  // Initialization happens before the terminal/TUI starts, never in a frame.
264
- mkdirSync(dirname(path), { recursive: true });
265
- writeFileSync(path, "", "utf8");
376
+ if (path !== null) {
377
+ mkdirSync(dirname(path), { recursive: true });
378
+ writeFileSync(path, "", "utf8");
379
+ }
266
380
  const openedAt = now();
381
+ const openedAtEpochMs = Date.now();
267
382
  const at = (): number => rounded(now() - openedAt);
268
- const writer = createAsyncTraceWriter(path, { ...writerOptions, recordTime: at });
383
+ const writer = path === null ? null : createAsyncTraceWriter(path, { ...writerOptions, recordTime: at });
384
+ const inputRing = createRing<RenderTraceInputIngressRecord>(INPUT_WEDGE_RING_CAPACITY);
385
+ const frameRing = createRing<RenderTraceFrameRecord>(INPUT_WEDGE_RING_CAPACITY);
269
386
  let frameSeq = 0;
270
387
  let writeSeq = 0;
271
388
  let eventSeq = 0;
@@ -282,7 +399,7 @@ export function createRenderTrace(
282
399
  const ranges = new Map<string, { codeUnits: number; graphemes: number }>();
283
400
  const pendingDrains = new Map<number, { frameId: number | null; at: number }>();
284
401
 
285
- writer.enqueue({ type: "trace_start", version: RENDER_TRACE_VERSION, at: 0 });
402
+ writer?.enqueue({ type: "trace_start", version: RENDER_TRACE_VERSION, at: 0 });
286
403
 
287
404
  const trace: RenderTrace = {
288
405
  recordPanelRender(metrics): void {
@@ -294,12 +411,15 @@ export function createRenderTrace(
294
411
  },
295
412
  recordVisibleEvent(fields): number {
296
413
  eventSeq += 1;
414
+ // The ring keeps no provider deltas, so ring-only mode does not pay for
415
+ // grapheme segmentation on every token of every stream.
416
+ if (writer === null) return eventSeq;
297
417
  const key = `${generation}:${fields.kind}:${fields.contentIndex}`;
298
418
  const previous = ranges.get(key) ?? { codeUnits: 0, graphemes: 0 };
299
419
  const graphemes = graphemeCount(fields.delta);
300
420
  const next = { codeUnits: previous.codeUnits + fields.delta.length, graphemes: previous.graphemes + graphemes };
301
421
  ranges.set(key, next);
302
- writer.enqueue({
422
+ writer?.enqueue({
303
423
  type: "event_ingress",
304
424
  version: RENDER_TRACE_VERSION,
305
425
  eventSeq,
@@ -317,7 +437,7 @@ export function createRenderTrace(
317
437
  },
318
438
  recordQueue(sequence, action): void {
319
439
  queueDepth = Math.max(0, queueDepth + (action === "admit" ? 1 : -1));
320
- writer.enqueue({
440
+ writer?.enqueue({
321
441
  type: "queue",
322
442
  version: RENDER_TRACE_VERSION,
323
443
  eventSeq: sequence,
@@ -328,7 +448,7 @@ export function createRenderTrace(
328
448
  },
329
449
  recordPanelApplied(sequence): void {
330
450
  panelHighWater = Math.max(panelHighWater, sequence);
331
- writer.enqueue({
451
+ writer?.enqueue({
332
452
  type: "panel",
333
453
  version: RENDER_TRACE_VERSION,
334
454
  eventSeq: sequence,
@@ -339,7 +459,7 @@ export function createRenderTrace(
339
459
  recordInputIngress(action, bytes, visualExpected = action !== "no-visual-change"): number {
340
460
  inputSeq += 1;
341
461
  if (visualExpected) inputHighWater = inputSeq;
342
- writer.enqueue({
462
+ const record: RenderTraceInputIngressRecord = {
343
463
  type: "input_ingress",
344
464
  version: RENDER_TRACE_VERSION,
345
465
  inputSeq,
@@ -347,7 +467,9 @@ export function createRenderTrace(
347
467
  at: at(),
348
468
  bytes,
349
469
  visualExpected,
350
- });
470
+ };
471
+ inputRing.push(record);
472
+ writer?.enqueue(record);
351
473
  return inputSeq;
352
474
  },
353
475
  beginFrame(fields): FrameState {
@@ -403,8 +525,11 @@ export function createRenderTrace(
403
525
  },
404
526
  commits: frame.commits,
405
527
  };
406
- writer.enqueue(record);
528
+ writer?.enqueue(record);
407
529
  if (frame.commits.length > 0) {
530
+ // Only a frame that reached stdout answers the question the ring exists
531
+ // for, which is whether the consumer half is still painting.
532
+ frameRing.push(record);
408
533
  lastCommitAt = frame.commits.at(-1)?.at ?? endAt;
409
534
  if (!firstCommitDelivered) {
410
535
  firstCommitDelivered = true;
@@ -439,6 +564,7 @@ export function createRenderTrace(
439
564
  backpressured: !fields.returned,
440
565
  };
441
566
  currentFrame?.commits.push(commit);
567
+ if (writer === null) return writeSeq;
442
568
  writer.enqueue({ type: "terminal_write", version: RENDER_TRACE_VERSION, frameId, ...commit });
443
569
  if (!fields.returned) pendingDrains.set(writeSeq, { frameId, at: commit.at });
444
570
  return writeSeq;
@@ -448,7 +574,7 @@ export function createRenderTrace(
448
574
  if (!pending) return;
449
575
  pendingDrains.delete(writeId);
450
576
  const drainAt = at();
451
- writer.enqueue({
577
+ writer?.enqueue({
452
578
  type: "terminal_drain",
453
579
  version: RENDER_TRACE_VERSION,
454
580
  writeId,
@@ -461,7 +587,28 @@ export function createRenderTrace(
461
587
  onFirstFrameCommit(listener): void {
462
588
  firstCommitListener = listener;
463
589
  },
464
- close: () => writer.close(),
590
+ snapshotInputWedge(): InputWedgeSnapshot {
591
+ const inputIngress = inputRing.toArray();
592
+ const frames = frameRing.toArray();
593
+ const nowAt = at();
594
+ const lastIngressAt = inputIngress.at(-1)?.at;
595
+ const lastFrame = frames.at(-1);
596
+ const lastFrameCommitAt = lastFrame?.commits.at(-1)?.at ?? lastFrame?.endAt;
597
+ return {
598
+ version: RENDER_TRACE_VERSION,
599
+ capacity: INPUT_WEDGE_RING_CAPACITY,
600
+ pid: process.pid,
601
+ openedAtEpochMs,
602
+ atEpochMs: Date.now(),
603
+ at: nowAt,
604
+ classification: classifyInputWedge(inputIngress, frames),
605
+ msSinceLastInputIngress: lastIngressAt === undefined ? null : rounded(nowAt - lastIngressAt),
606
+ msSinceLastCommittedFrame: lastFrameCommitAt === undefined ? null : rounded(nowAt - lastFrameCommitAt),
607
+ inputIngress,
608
+ frames,
609
+ };
610
+ },
611
+ close: () => writer?.close() ?? Promise.resolve(),
465
612
  };
466
613
  return trace;
467
614
  }
@@ -18,6 +18,7 @@ import { formatSize } from "../../engine/truncate.js";
18
18
  import { visibleWidth, wrapTextWithAnsi } from "../../engine/tui.js";
19
19
  import { classifyResourceRead, toolPresentationPolicy } from "../../tools/presentation.js";
20
20
  import { toolResultPresentationPolicy, toolResultPresentationText } from "../../tools/result-disposition.js";
21
+ import { mutationFactsLine } from "../mutation-preview.js";
21
22
  import type { ApprovalRequestView } from "../permission-overlay.js";
22
23
  import { clioTheme, formatCompactMs, GLYPH } from "../theme/index.js";
23
24
  import { renderDiffLines } from "./diff.js";
@@ -490,6 +491,9 @@ export function renderToolAwaitingApproval(
490
491
  ["action", view.actionClass],
491
492
  ["axis", axis],
492
493
  ...(view.target !== undefined && view.target.length > 0 ? [["target", view.target]] : []),
494
+ // Size and digest, never the mutation text: this row is the transcript,
495
+ // which is written, replayed, and shared (issue #254).
496
+ ...(view.mutation !== undefined ? [["mutation", mutationFactsLine(view.mutation)]] : []),
493
497
  ] as const;
494
498
  for (const [label, value] of facts) {
495
499
  lines.push(...indentAndWrap(`${dim(`${label} ·`)} ${value}`, width, false));
@@ -13,7 +13,13 @@
13
13
  * machine without adding an engine seam of its own. Tools are never sent.
14
14
  */
15
15
 
16
+ import {
17
+ isResponseSchemaRejection,
18
+ type ResponseSchemaDialect,
19
+ responseSchemaDialectFor,
20
+ } from "../core/response-schema.js";
16
21
  import { stream } from "../engine/ai.js";
22
+ import { patchResponseSchemaPayloadForDialect } from "../engine/provider-payload.js";
17
23
  import type { AgentMessage, EngineModel, Usage } from "../engine/types.js";
18
24
 
19
25
  /**
@@ -67,6 +73,18 @@ export interface OutOfTurnRoundInput extends Omit<SideQuestionInput, "question">
67
73
  systemPrompt: string;
68
74
  /** The one message appended to the read-only copy of the history. */
69
75
  userText: string;
76
+ /**
77
+ * Bind the answer to a JSON schema on the wire, where the runtime takes one.
78
+ *
79
+ * A round that asked for a schema in prose and bound nothing is what made
80
+ * `/handoff` refuse on local targets: the model answered with prose around
81
+ * the object, or with no object at all, and the parser had nothing to work
82
+ * with (issue #223). Absent, or on a runtime with no dialect, the request
83
+ * goes out exactly as before and the prompt-level instruction carries it.
84
+ */
85
+ responseSchema?: { name: string; schema: Record<string, unknown> };
86
+ /** Resolved runtime id, which is what decides the wire dialect. */
87
+ runtimeId?: string;
70
88
  }
71
89
 
72
90
  export interface SideQuestionResult {
@@ -80,7 +98,12 @@ function positive(value: unknown): number {
80
98
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
81
99
  }
82
100
 
83
- function sideQuestionUsage(raw: unknown): SideQuestionUsage | null {
101
+ /**
102
+ * Normalize one provider usage object into the shape `/cost` and the
103
+ * out-of-turn usage store take. Shared with the session pre-warm, which is
104
+ * another call billed beside the session rather than inside it.
105
+ */
106
+ export function sideQuestionUsage(raw: unknown): SideQuestionUsage | null {
84
107
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
85
108
  const usage = raw as Partial<Usage> & { reasoning?: number };
86
109
  const input = positive(usage.input);
@@ -128,11 +151,45 @@ export async function runSideQuestion(input: SideQuestionInput): Promise<SideQue
128
151
  return runOutOfTurnRound({ ...rest, systemPrompt: SIDE_QUESTION_SYSTEM_PROMPT, userText: question });
129
152
  }
130
153
 
154
+ interface SchemaBinding {
155
+ dialect: ResponseSchemaDialect;
156
+ name: string;
157
+ schema: Record<string, unknown>;
158
+ }
159
+
160
+ /** The schema binding for this round, or null when nothing can be bound on the wire. */
161
+ function schemaBindingFor(input: OutOfTurnRoundInput): SchemaBinding | null {
162
+ if (input.responseSchema === undefined || input.runtimeId === undefined) return null;
163
+ const dialect = responseSchemaDialectFor(input.runtimeId);
164
+ return dialect === null ? null : { dialect, ...input.responseSchema };
165
+ }
166
+
131
167
  /** The shared round. See {@link OutOfTurnRoundInput}. */
132
168
  export async function runOutOfTurnRound(input: OutOfTurnRoundInput): Promise<SideQuestionResult> {
169
+ const binding = schemaBindingFor(input);
170
+ if (binding === null) return runRound(input, null);
171
+ try {
172
+ return await runRound(input, binding);
173
+ } catch (error) {
174
+ // Native enforcement is an optimization here, never a precondition. A
175
+ // server that refuses the constrained request answers with the same 400
176
+ // the worker seam already recognizes, and the round is worth more
177
+ // unconstrained than not at all. Anything else is a real failure and
178
+ // rejects, so a broken target still costs exactly one round.
179
+ if (input.signal?.aborted === true) throw error;
180
+ if (!isResponseSchemaRejection(error instanceof Error ? error.message : String(error))) throw error;
181
+ return runRound(input, null);
182
+ }
183
+ }
184
+
185
+ async function runRound(input: OutOfTurnRoundInput, binding: SchemaBinding | null): Promise<SideQuestionResult> {
133
186
  const options: Record<string, unknown> = { maxTokens: input.maxTokens ?? SIDE_QUESTION_MAX_TOKENS };
134
187
  if (input.apiKey !== undefined) options.apiKey = input.apiKey;
135
188
  if (input.signal !== undefined) options.signal = input.signal;
189
+ if (binding !== null) {
190
+ options.onPayload = (payload: unknown): unknown | undefined =>
191
+ patchResponseSchemaPayloadForDialect(payload, binding.dialect, binding.schema, binding.name);
192
+ }
136
193
 
137
194
  const context = {
138
195
  systemPrompt: input.systemPrompt,
@@ -178,6 +178,17 @@ export function createStatusController(deps: StatusControllerDeps): StatusContro
178
178
  notify(prev, status, force);
179
179
  };
180
180
 
181
+ // The window between the editor and the stream. `preparing` and `idle`
182
+ // bracket it; `compacting` inside it is already carried by the compaction
183
+ // bus overlay, which now lands because `preparing` is an active phase.
184
+ if (deps.chat.onTurnPreparation) {
185
+ unsubscribes.push(
186
+ deps.chat.onTurnPreparation((phase) => {
187
+ apply({ type: phase === "idle" ? "submit_settled" : "submit_accepted" }, true);
188
+ }),
189
+ );
190
+ }
191
+
181
192
  unsubscribes.push(
182
193
  deps.chat.onEvent((event: ChatLoopEvent) => {
183
194
  if (event.type === "agent_status") return;
@@ -53,6 +53,21 @@ export interface DispatchProgressStatusEvent {
53
53
  type: "dispatch_progress";
54
54
  }
55
55
 
56
+ /**
57
+ * A prompt has left the editor and Clio is preparing the turn: capability
58
+ * probe, pre-submit compaction, prompt compile, overflow preflight. None of
59
+ * that produces an engine event, so without this the footer kept reporting the
60
+ * previous turn as done for the whole window (issue #251).
61
+ */
62
+ export interface SubmitAcceptedStatusEvent {
63
+ type: "submit_accepted";
64
+ }
65
+
66
+ /** The prepared turn was admitted, refused, or failed. Either way the window is over. */
67
+ export interface SubmitSettledStatusEvent {
68
+ type: "submit_settled";
69
+ }
70
+
56
71
  export type StatusInputEvent =
57
72
  | ChatLoopEvent
58
73
  | WatchdogTickEvent
@@ -60,7 +75,9 @@ export type StatusInputEvent =
60
75
  | OverlayPopEvent
61
76
  | RunAbortedStatusEvent
62
77
  | ForceCancelledStatusEvent
63
- | DispatchProgressStatusEvent;
78
+ | DispatchProgressStatusEvent
79
+ | SubmitAcceptedStatusEvent
80
+ | SubmitSettledStatusEvent;
64
81
 
65
82
  const OVERLAY_PHASES = new Set<StatusPhase>(["tool_blocked", "retrying", "compacting", "dispatching", "stuck"]);
66
83
  const CORE_ACTIVE_PHASES = new Set<StatusPhase>(["preparing", "waiting_model", "thinking", "writing", "tool_running"]);
@@ -292,8 +309,43 @@ function retryOverlay(status: {
292
309
 
293
310
  export function reduceStatus(prev: AgentStatus, event: StatusInputEvent, ctx: ReduceContext): AgentStatus {
294
311
  switch (event.type) {
295
- case "agent_start":
312
+ // A consumed prompt occupies `preparing` before any engine event exists,
313
+ // and the previous turn's summary rides along untouched: a refused
314
+ // admission puts the footer back on the last turn that actually ran,
315
+ // which is still the truth about what happened.
316
+ case "submit_accepted":
296
317
  if (isActive(prev.phase)) return prev;
318
+ return {
319
+ ...prev,
320
+ phase: "preparing",
321
+ since: ctx.now,
322
+ lastMeaningfulAt: ctx.now,
323
+ localRuntime: ctx.localRuntime,
324
+ preparingSubmission: true,
325
+ watchdogTier: 0,
326
+ resumePhase: undefined,
327
+ activePhases: undefined,
328
+ overlayStack: [],
329
+ tool: undefined,
330
+ toolStartedAt: undefined,
331
+ retry: undefined,
332
+ dispatch: undefined,
333
+ };
334
+ case "submit_settled":
335
+ if (prev.preparingSubmission !== true) return prev;
336
+ return {
337
+ ...prev,
338
+ phase: prev.summary ? "ended" : "idle",
339
+ since: ctx.now,
340
+ preparingSubmission: undefined,
341
+ resumePhase: undefined,
342
+ activePhases: undefined,
343
+ overlayStack: [],
344
+ };
345
+ case "agent_start":
346
+ // A window this opened is the same turn arriving, not a duplicate
347
+ // start, so the run resets through it rather than being ignored.
348
+ if (isActive(prev.phase) && prev.preparingSubmission !== true) return prev;
297
349
  return {
298
350
  ...INITIAL_STATUS,
299
351
  phase: "preparing",