@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
@@ -14,6 +14,21 @@ import type { SessionMeta } from "./contract.js";
14
14
  * stay diffable without recompiling anything.
15
15
  */
16
16
 
17
+ /**
18
+ * Layout version of the compiled prompt these records describe.
19
+ *
20
+ * 1: the order shipped through 0.3.8.
21
+ * 2: stable-prefix-first ordering with one `# Memory` header, and
22
+ * `contextWindowSource` recorded alongside the window the prompt states
23
+ * (issue #249).
24
+ *
25
+ * A record without a `version` field predates the field and is read as 1, so a
26
+ * 0.3.8 manifest still parses. Bump this whenever the compiled text moves for a
27
+ * reason other than its inputs: a resumed session then has the version in hand
28
+ * to explain the one `promptRecompiled` entry its first compile writes.
29
+ */
30
+ export const PROMPT_MANIFEST_VERSION = 2;
31
+
17
32
  export interface PromptManifestSection {
18
33
  id: string;
19
34
  tokenEstimate: number;
@@ -27,6 +42,8 @@ export interface PromptManifestFragment {
27
42
  }
28
43
 
29
44
  export interface SessionPromptCompileRecord {
45
+ /** Prompt layout version; see `PROMPT_MANIFEST_VERSION`. Absent on pre-0.3.9 records. */
46
+ version?: number;
30
47
  /** ISO timestamp of the compile. */
31
48
  at: string;
32
49
  /** Hash of the previously served prompt, null on the first compile. */
@@ -35,6 +52,14 @@ export interface SessionPromptCompileRecord {
35
52
  tokenEstimate: number;
36
53
  /** Thinking dial applied to the live agent when this prompt was compiled. */
37
54
  thinkingLevel: string | null;
55
+ /**
56
+ * The window the prompt's `Context window: N` states, and the layer that
57
+ * answered it. `loaded` is what the backend has this model open at; anything
58
+ * else means the figure can still move once a loaded window is observed, and
59
+ * a later recompile is then explained rather than mysterious.
60
+ */
61
+ contextWindow?: number | null;
62
+ contextWindowSource?: string | null;
38
63
  projectPreload: ProjectPreloadClass | null;
39
64
  sections: PromptManifestSection[];
40
65
  fragments: PromptManifestFragment[];
@@ -102,9 +127,21 @@ function isProjectPreloadClass(value: unknown): value is ProjectPreloadClass {
102
127
  );
103
128
  }
104
129
 
130
+ /** Optional fields added after 0.3.8: absent is valid, present must be well-typed. */
131
+ function isOptional<T>(value: unknown, guard: (candidate: unknown) => candidate is T): boolean {
132
+ return value === undefined || guard(value);
133
+ }
134
+
105
135
  function isSessionPromptCompileRecord(value: unknown): value is SessionPromptCompileRecord {
106
136
  if (!isRecord(value)) return false;
107
137
  return (
138
+ isOptional(value.version, isNonNegativeInteger) &&
139
+ isOptional(value.contextWindow, (candidate): candidate is number | null =>
140
+ candidate === null ? true : isNonNegativeInteger(candidate),
141
+ ) &&
142
+ isOptional(value.contextWindowSource, (candidate): candidate is string | null =>
143
+ candidate === null ? true : typeof candidate === "string",
144
+ ) &&
108
145
  isCanonicalUtcTimestamp(value.at) &&
109
146
  (value.previousHash === null || isSha256(value.previousHash)) &&
110
147
  isSha256(value.systemPromptHash) &&
@@ -118,6 +155,11 @@ function isSessionPromptCompileRecord(value: unknown): value is SessionPromptCom
118
155
  );
119
156
  }
120
157
 
158
+ /** A record written before the field existed describes the 0.3.8 layout. */
159
+ export function promptManifestVersion(record: SessionPromptCompileRecord): number {
160
+ return record.version ?? 1;
161
+ }
162
+
121
163
  export function getPromptManifestFilePath(meta: SessionMeta, stateDir?: string): string {
122
164
  const safeMeta = {
123
165
  ...meta,
@@ -1,8 +1,10 @@
1
+ import { performance } from "node:perf_hooks";
1
2
  import {
2
3
  DEFAULT_DELEGATION_CONNECT_TIMEOUT_MS,
3
4
  DEFAULT_DELEGATION_TURN_TIMEOUT_MS,
4
5
  type DelegationAgentConfig,
5
6
  } from "../../core/defaults.js";
7
+ import type { HeartbeatStamp } from "../../domains/dispatch/heartbeat.js";
6
8
  import type { AutonomyLevel } from "../../domains/safety/autonomy.js";
7
9
  import type { SafetyContract } from "../../domains/safety/contract.js";
8
10
  import type { AgentEvent } from "../types.js";
@@ -40,6 +42,10 @@ export interface AcpDelegationRunInput {
40
42
  terminationWaitMs?: number;
41
43
  /** Grace for session/cancel before abort escalates into hard termination. */
42
44
  cancelGraceMs?: number;
45
+ /** Wall clock for the persisted heartbeat anchor. */
46
+ now?: () => number;
47
+ /** Monotonic clock for event-inactivity spans. */
48
+ monotonicNow?: () => number;
43
49
  }
44
50
 
45
51
  export interface AcpDelegationRunHandle {
@@ -54,7 +60,7 @@ export interface AcpDelegationRunHandle {
54
60
  * final observation bound expires.
55
61
  */
56
62
  kill(): void;
57
- heartbeatAt: { current: number };
63
+ heartbeatAt: HeartbeatStamp;
58
64
  toolCallLog(): ReturnType<AcpToolMediator["snapshot"]>["toolCallLog"];
59
65
  }
60
66
 
@@ -176,7 +182,14 @@ export function startAcpDelegationRun(input: AcpDelegationRunInput): AcpDelegati
176
182
  // peer into an unbounded request.
177
183
  const connectTimeoutMs = input.agent.connectTimeoutMs ?? DEFAULT_DELEGATION_CONNECT_TIMEOUT_MS;
178
184
  const turnTimeoutMs = input.agent.turnTimeoutMs ?? DEFAULT_DELEGATION_TURN_TIMEOUT_MS;
179
- const heartbeatAt = { current: Date.now() };
185
+ const now = input.now ?? Date.now;
186
+ const monotonicNow = input.monotonicNow ?? (() => performance.now());
187
+ const heartbeatWallAnchor = now();
188
+ const heartbeatMonotonicAnchor = monotonicNow();
189
+ const heartbeatAt: HeartbeatStamp = {
190
+ current: heartbeatWallAnchor,
191
+ monotonic: heartbeatMonotonicAnchor,
192
+ };
180
193
  const queue = new AsyncEventQueue<AcpRunEvent>();
181
194
  const usage = emptyUsage();
182
195
  const mapper = new AcpEventMapper();
@@ -202,7 +215,9 @@ export function startAcpDelegationRun(input: AcpDelegationRunInput): AcpDelegati
202
215
  cancelTimer = null;
203
216
  };
204
217
  const emit = (event: AcpRunEvent): void => {
205
- heartbeatAt.current = Date.now();
218
+ const stamp = monotonicNow();
219
+ heartbeatAt.monotonic = stamp;
220
+ heartbeatAt.current = heartbeatWallAnchor + (stamp - heartbeatMonotonicAnchor);
206
221
  queue.push(event);
207
222
  };
208
223
  const mediator = new AcpToolMediator({
package/src/engine/ai.ts CHANGED
@@ -27,9 +27,11 @@ import {
27
27
  type ToolCall,
28
28
  type Usage,
29
29
  } from "@earendil-works/pi-ai";
30
+ import type { BackendCompletionTimings } from "../core/cache-telemetry.js";
30
31
  import {
31
32
  completeEngineSimple,
32
33
  engineStream,
34
+ engineStreamSimple,
33
35
  registerEngineBuiltins,
34
36
  registerEngineFauxProvider,
35
37
  } from "./api-registry.js";
@@ -40,6 +42,14 @@ export { StringEnum };
40
42
 
41
43
  export const stream = engineStream;
42
44
 
45
+ /**
46
+ * The dispatcher a turn runs on. `createEngineAgent` hands this to pi-agent-core
47
+ * as its `streamFn`, so a caller that needs a request byte-identical to the one
48
+ * the next turn would send (the session pre-warm) sends it through here rather
49
+ * than through `stream`, whose bare-call path has to guess a thinking level.
50
+ */
51
+ export const streamSimple = engineStreamSimple;
52
+
43
53
  export interface EngineTextCompletionInput {
44
54
  model: EngineModel;
45
55
  systemPrompt: string;
@@ -55,6 +65,21 @@ export interface EngineTextCompletionResult {
55
65
  text: string;
56
66
  inputTokens: number;
57
67
  outputTokens: number;
68
+ /**
69
+ * The provider's full usage record for the call, so a caller that has to
70
+ * account for the spend does not have to re-derive it from two token counts.
71
+ */
72
+ usage: {
73
+ input: number;
74
+ output: number;
75
+ cacheRead: number;
76
+ cacheWrite: number;
77
+ reasoning: number;
78
+ totalTokens: number;
79
+ costUsd: number;
80
+ };
81
+ /** Backend prefill and prediction facts when the serving runtime reported them. */
82
+ backend: BackendCompletionTimings | null;
58
83
  }
59
84
 
60
85
  /** Run one tool-free completion while keeping pi message types at the engine boundary. */
@@ -83,6 +108,16 @@ export async function completeEngineText(input: EngineTextCompletionInput): Prom
83
108
  .join(""),
84
109
  inputTokens: response.usage.input,
85
110
  outputTokens: response.usage.output,
111
+ usage: {
112
+ input: response.usage.input,
113
+ output: response.usage.output,
114
+ cacheRead: response.usage.cacheRead,
115
+ cacheWrite: response.usage.cacheWrite,
116
+ reasoning: response.usage.reasoning ?? 0,
117
+ totalTokens: response.usage.totalTokens,
118
+ costUsd: response.usage.cost.total,
119
+ },
120
+ backend: response.backendTimings ?? null,
86
121
  };
87
122
  }
88
123
 
@@ -1,4 +1,5 @@
1
1
  import { performance } from "node:perf_hooks";
2
+ import { residencyTargetKey } from "../../core/residency-target-key.js";
2
3
  import { sleep } from "../../core/timers.js";
3
4
  import {
4
5
  type ResidencyAdapter,
@@ -169,6 +170,26 @@ async function fetchRouterModels(
169
170
  return parseRouterModels(await response.json());
170
171
  }
171
172
 
173
+ class RouterModelPostError extends Error {
174
+ constructor(
175
+ readonly url: string,
176
+ readonly status: number,
177
+ readonly responseBody: string,
178
+ ) {
179
+ super(`llama.cpp router rejected ${url}: HTTP ${status}; response: ${responseBody}`);
180
+ this.name = "RouterModelPostError";
181
+ }
182
+ }
183
+
184
+ async function routerErrorBody(response: Response): Promise<string> {
185
+ try {
186
+ const body = (await response.text()).trim();
187
+ return body.length > 0 ? body : "<empty body>";
188
+ } catch (error) {
189
+ return `<unreadable body: ${error instanceof Error ? error.message : String(error)}>`;
190
+ }
191
+ }
192
+
172
193
  async function postRouterModel(fetchImpl: typeof fetch, url: string, modelId: string): Promise<void> {
173
194
  const response = await fetchImpl(url, {
174
195
  method: "POST",
@@ -177,7 +198,38 @@ async function postRouterModel(fetchImpl: typeof fetch, url: string, modelId: st
177
198
  signal: AbortSignal.timeout(LOAD_TIMEOUT_MS),
178
199
  });
179
200
  if (response.ok) return;
180
- throw new Error(`llama.cpp router rejected ${url}: HTTP ${response.status}`);
201
+ throw new RouterModelPostError(url, response.status, await routerErrorBody(response));
202
+ }
203
+
204
+ function alreadyRunningResponse(body: string): boolean {
205
+ return body.toLowerCase().includes("already running");
206
+ }
207
+
208
+ /**
209
+ * A router wake and Clio's explicit load can cross between the listing and the
210
+ * POST. In that race the router rejects the duplicate request even though the
211
+ * requested model now owns a resident slot. Re-read the authoritative listing
212
+ * before treating a non-2xx as a load failure; only a definitively absent,
213
+ * unloaded, or failed model preserves the rejection.
214
+ */
215
+ async function postRouterLoad(input: LlamaCppResidencyInput, fetchImpl: typeof fetch, modelId: string): Promise<void> {
216
+ try {
217
+ await postRouterModel(fetchImpl, loadUrl(input.baseUrl), modelId);
218
+ } catch (error) {
219
+ if (!(error instanceof RouterModelPostError)) throw error;
220
+ let model: LlamaCppRouterModel | undefined;
221
+ try {
222
+ model = (await fetchRouterModels(input, fetchImpl)).find((entry) => entry.id === modelId);
223
+ } catch {
224
+ // The router's explicit "already running" answer is sufficient even if
225
+ // the confirming snapshot itself races or is temporarily unavailable.
226
+ if (alreadyRunningResponse(error.responseBody)) return;
227
+ throw error;
228
+ }
229
+ if (alreadyRunningResponse(error.responseBody)) return;
230
+ if (model !== undefined && model.state !== "unloaded" && model.state !== "failed") return;
231
+ throw error;
232
+ }
181
233
  }
182
234
 
183
235
  async function waitForLoaded(input: LlamaCppResidencyInput, fetchImpl: typeof fetch, modelId: string): Promise<void> {
@@ -206,7 +258,7 @@ async function ensureModelLoaded(
206
258
  // router answers `400 model is already running`, which fails the turn before
207
259
  // it reaches the model.
208
260
  if (knownState === "loaded" || knownState === "sleeping") return;
209
- if (knownState !== "loading") await postRouterModel(fetchImpl, loadUrl(input.baseUrl), modelId);
261
+ if (knownState !== "loading") await postRouterLoad(input, fetchImpl, modelId);
210
262
  await waitForLoaded(input, fetchImpl, modelId);
211
263
  }
212
264
 
@@ -246,7 +298,7 @@ export async function ensureLlamaCppResidency(input: LlamaCppResidencyInput): Pr
246
298
  const fetchImpl = input.fetchImpl ?? fetch;
247
299
  let snapshot: LlamaCppRouterModel[] = [];
248
300
  const adapter: ResidencyAdapter = {
249
- targetKey: `llamacpp|${input.baseUrl}`,
301
+ targetKey: residencyTargetKey("llamacpp", input.baseUrl),
250
302
  targetId: input.targetId,
251
303
  runtimeId: input.runtimeId,
252
304
  keepModelId: input.keepModelId,
@@ -1,5 +1,6 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
 
3
+ import { residencyTargetKey } from "../../core/residency-target-key.js";
3
4
  import {
4
5
  type LmStudioCatalog,
5
6
  type LmStudioLoadedInstance,
@@ -11,7 +12,7 @@ import {
11
12
  } from "../../domains/providers/runtimes/common/lmstudio-http.js";
12
13
  import type { TargetDescriptor } from "../../domains/providers/types/target-descriptor.js";
13
14
  import { coResidentContextCeiling, fitLoadContextLength } from "./lmstudio-residency.js";
14
- import { emitResidencyNotice, reconcileResidency, residencyManagedFor } from "./residency.js";
15
+ import { emitResidencyMutation, emitResidencyNotice, reconcileResidency, residencyManagedFor } from "./residency.js";
15
16
  import { withResidencyLock } from "./residency-lock.js";
16
17
 
17
18
  interface LmStudioModelMetadata {
@@ -205,7 +206,7 @@ export async function ensureLmStudioResidency(
205
206
  entry.loadedInstances.map((instance) => ({ modelKey: entry.key, identifier: instance.id, instance })),
206
207
  );
207
208
  const managed = residencyManagedFor(info.lifecycle);
208
- const targetKey = `lmstudio|${lmStudioRootUrl(target.url ?? model.baseUrl)}`;
209
+ const targetKey = residencyTargetKey("lmstudio", target.url ?? model.baseUrl);
209
210
  const contextLength = target.lmstudio?.load?.contextLength;
210
211
  const plan = await reconcileResidency({
211
212
  targetKey,
@@ -248,17 +249,36 @@ export async function ensureLmStudioResidency(
248
249
  });
249
250
  }
250
251
  }
252
+ const loadAndReport = async (): Promise<string> => {
253
+ const instanceId = await loadOwnedInstance(target, targetKey, body, options.apiKey, options.signal);
254
+ emitResidencyMutation({
255
+ targetKey,
256
+ targetId: info.targetId,
257
+ runtimeId: "lmstudio",
258
+ model: modelKey,
259
+ operation: "load",
260
+ });
261
+ return instanceId ?? model.id;
262
+ };
251
263
  try {
252
- return (await loadOwnedInstance(target, targetKey, body, options.apiKey, options.signal)) ?? model.id;
264
+ return await loadAndReport();
253
265
  } catch (error) {
254
266
  if (plan.decision === "observe" || plan.fallbackEvict.length === 0) throw error;
255
267
  return withResidencyLock(targetKey, async () => {
256
268
  for (const candidate of plan.fallbackEvict) {
257
269
  for (const entry of instances.filter((resident) => resident.modelKey === candidate.modelId)) {
258
- await unloadOwnedInstance(target, targetKey, entry.identifier, options.apiKey, options.signal);
270
+ if (await unloadOwnedInstance(target, targetKey, entry.identifier, options.apiKey, options.signal)) {
271
+ emitResidencyMutation({
272
+ targetKey,
273
+ targetId: info.targetId,
274
+ runtimeId: "lmstudio",
275
+ model: candidate.modelId,
276
+ operation: "evict",
277
+ });
278
+ }
259
279
  }
260
280
  }
261
- return (await loadOwnedInstance(target, targetKey, body, options.apiKey, options.signal)) ?? model.id;
281
+ return loadAndReport();
262
282
  });
263
283
  }
264
284
  }
@@ -24,6 +24,7 @@ import {
24
24
  type Tool as OllamaTool,
25
25
  type ToolCall as OllamaToolCall,
26
26
  } from "ollama";
27
+ import { residencyTargetKey } from "../../core/residency-target-key.js";
27
28
  import {
28
29
  type AppliedThinking,
29
30
  resolveModelRuntimeCapabilitiesForModel,
@@ -75,7 +76,7 @@ async function reconcileOllamaResidency(model: Model<"ollama-native">, headers:
75
76
  if (!baseUrl) return;
76
77
  const metadata = (model as Model<"ollama-native"> & ClioRuntimeMetadata).clio;
77
78
  const adapter: ResidencyAdapter = {
78
- targetKey: `ollama-native|${baseUrl}`,
79
+ targetKey: residencyTargetKey("ollama-native", baseUrl),
79
80
  targetId: ollamaTargetId(model),
80
81
  runtimeId: "ollama-native",
81
82
  keepModelId: model.id,
@@ -16,6 +16,7 @@ import {
16
16
  type Usage,
17
17
  } from "@earendil-works/pi-ai";
18
18
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
19
+ import type { BackendCompletionTimings, BackendTimingsSource } from "../../core/cache-telemetry.js";
19
20
  import type { ResponseModelIdObservation } from "../../core/response-model-id.js";
20
21
  import {
21
22
  type AppliedThinking,
@@ -41,6 +42,8 @@ declare module "@earendil-works/pi-ai" {
41
42
  interface AssistantMessage {
42
43
  /** Direct observation of model-id presence in an OpenAI-compatible response. */
43
44
  responseModelIdObservation?: ResponseModelIdObservation;
45
+ /** Backend-reported prefill and prediction timings when available. */
46
+ backendTimings?: BackendCompletionTimings;
44
47
  }
45
48
  }
46
49
 
@@ -95,13 +98,57 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
95
98
  interface ResponseModelIdCapture {
96
99
  reportedModelId: string | null;
97
100
  observed: boolean;
98
- done: boolean;
101
+ modelIdDone: boolean;
102
+ backendTimings: BackendCompletionTimings | null;
103
+ backendTimingsSource: BackendTimingsSource | null;
99
104
  buffer: string;
100
105
  decoder: TextDecoder | null;
101
106
  }
102
107
 
103
- function observeResponseModelIdLine(line: string, capture: ResponseModelIdCapture): void {
104
- if (capture.done) return;
108
+ function nonnegativeFiniteNumber(value: unknown): number | null {
109
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
110
+ }
111
+
112
+ /**
113
+ * The operator's llama.cpp router reported build b226-2115b73d8. Its nonstream
114
+ * response placed a `{ prompt_n, prompt_ms, predicted_n, predicted_ms, cache_n }`
115
+ * timing object at the top level. Its stream placed the timing object on the
116
+ * final SSE JSON event without `timings_per_token`. Setting
117
+ * `timings_per_token: true` repeated cumulative timing objects for individual
118
+ * tokens. LM Studio's OpenAI-compatible stream and nonstream responses omitted
119
+ * timings both with and without that flag. Its native runtime reported
120
+ * llama.cpp-win-x86_64-nvidia-cuda12-avx2 2.29.0.
121
+ */
122
+ function backendCompletionTimings(value: unknown, source: BackendTimingsSource): BackendCompletionTimings | null {
123
+ if (!isPlainRecord(value)) return null;
124
+ const promptN = nonnegativeFiniteNumber(value.prompt_n);
125
+ const predictedN = nonnegativeFiniteNumber(value.predicted_n);
126
+ const promptMs = nonnegativeFiniteNumber(value.prompt_ms);
127
+ const predictedMs = nonnegativeFiniteNumber(value.predicted_ms);
128
+ if (promptN === null || predictedN === null || promptMs === null || predictedMs === null) return null;
129
+ const cacheN = nonnegativeFiniteNumber(value.cache_n);
130
+ return {
131
+ promptTokens: promptN + (cacheN ?? 0),
132
+ cachedTokens: cacheN,
133
+ predictedTokens: predictedN,
134
+ promptMs,
135
+ predictedMs,
136
+ source,
137
+ };
138
+ }
139
+
140
+ function backendTimingsSourceForModel(model: Model<Api>): BackendTimingsSource | null {
141
+ const metadata = (model as Model<Api> & ClioRuntimeMetadata).clio;
142
+ if (model.provider === "llamacpp" && metadata?.runtimeId === "llamacpp") return "llamacpp-timings";
143
+ if (model.provider === "lmstudio" && metadata?.runtimeId === "lmstudio") return "lmstudio-timings";
144
+ return null;
145
+ }
146
+
147
+ function captureCanStopEarly(capture: ResponseModelIdCapture): boolean {
148
+ return capture.modelIdDone && capture.backendTimingsSource === null;
149
+ }
150
+
151
+ function observeResponseMetadataLine(line: string, capture: ResponseModelIdCapture): void {
105
152
  const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
106
153
  if (!normalized.startsWith("data:")) return;
107
154
  const data = normalized.slice("data:".length).trimStart();
@@ -109,10 +156,16 @@ function observeResponseModelIdLine(line: string, capture: ResponseModelIdCaptur
109
156
  try {
110
157
  const payload = JSON.parse(data) as unknown;
111
158
  if (!isPlainRecord(payload)) return;
112
- const model = payload.model;
113
- if (typeof model === "string" && model.trim().length > 0) {
114
- capture.reportedModelId = model.trim();
115
- capture.done = true;
159
+ if (!capture.modelIdDone) {
160
+ const model = payload.model;
161
+ if (typeof model === "string" && model.trim().length > 0) {
162
+ capture.reportedModelId = model.trim();
163
+ capture.modelIdDone = true;
164
+ }
165
+ }
166
+ if (capture.backendTimingsSource !== null) {
167
+ const timings = backendCompletionTimings(payload.timings, capture.backendTimingsSource);
168
+ if (timings !== null) capture.backendTimings = timings;
116
169
  }
117
170
  } catch {
118
171
  // A partial or vendor-specific event is pi-ai's parsing concern. This
@@ -125,14 +178,14 @@ function observeResponseModelIdBytes(
125
178
  capture: ResponseModelIdCapture,
126
179
  flush = false,
127
180
  ): void {
128
- if (capture.done || !capture.decoder) return;
181
+ if (!capture.decoder) return;
129
182
  capture.buffer += chunk ? capture.decoder.decode(chunk, { stream: !flush }) : capture.decoder.decode();
130
183
  let newline = capture.buffer.indexOf("\n");
131
184
  while (newline >= 0) {
132
185
  const line = capture.buffer.slice(0, newline);
133
186
  capture.buffer = capture.buffer.slice(newline + 1);
134
- observeResponseModelIdLine(line, capture);
135
- if (capture.done) {
187
+ observeResponseMetadataLine(line, capture);
188
+ if (captureCanStopEarly(capture)) {
136
189
  capture.buffer = "";
137
190
  capture.decoder = null;
138
191
  return;
@@ -140,10 +193,10 @@ function observeResponseModelIdBytes(
140
193
  newline = capture.buffer.indexOf("\n");
141
194
  }
142
195
  if (flush && capture.buffer.length > 0) {
143
- observeResponseModelIdLine(capture.buffer, capture);
196
+ observeResponseMetadataLine(capture.buffer, capture);
144
197
  capture.buffer = "";
145
198
  }
146
- if (capture.done || flush) capture.decoder = null;
199
+ if (captureCanStopEarly(capture) || flush) capture.decoder = null;
147
200
  }
148
201
 
149
202
  function captureResponseModelId(response: Response, capture: ResponseModelIdCapture): Response {
@@ -154,12 +207,12 @@ function captureResponseModelId(response: Response, capture: ResponseModelIdCapt
154
207
  new TransformStream<Uint8Array, Uint8Array>({
155
208
  transform(chunk, controller) {
156
209
  capture.observed = true;
157
- if (!capture.done) observeResponseModelIdBytes(chunk, capture);
210
+ observeResponseModelIdBytes(chunk, capture);
158
211
  controller.enqueue(chunk);
159
212
  },
160
213
  flush() {
161
214
  capture.observed = true;
162
- if (!capture.done) observeResponseModelIdBytes(undefined, capture, true);
215
+ observeResponseModelIdBytes(undefined, capture, true);
163
216
  },
164
217
  }),
165
218
  );
@@ -171,13 +224,16 @@ function captureResponseModelId(response: Response, capture: ResponseModelIdCapt
171
224
  }
172
225
 
173
226
  function withResponseModelIdCapture<TOptions extends StreamOptions>(
227
+ model: Model<Api>,
174
228
  options: TOptions,
175
229
  sourceFactory: (capturedOptions: TOptions) => AssistantMessageEventStream,
176
230
  ): AssistantMessageEventStream {
177
231
  const capture: ResponseModelIdCapture = {
178
232
  reportedModelId: null,
179
233
  observed: false,
180
- done: false,
234
+ modelIdDone: false,
235
+ backendTimings: null,
236
+ backendTimingsSource: backendTimingsSourceForModel(model),
181
237
  buffer: "",
182
238
  decoder: new TextDecoder(),
183
239
  };
@@ -197,8 +253,13 @@ function withResponseModelIdCapture<TOptions extends StreamOptions>(
197
253
  ? { state: "not-reported" }
198
254
  : { state: "reported", reportedModelId: capture.reportedModelId }
199
255
  : { state: "not-observed" };
200
- if (event.type === "done") event.message.responseModelIdObservation = observation;
201
- else if (event.type === "error") event.error.responseModelIdObservation = observation;
256
+ if (event.type === "done") {
257
+ event.message.responseModelIdObservation = observation;
258
+ if (capture.backendTimings !== null) event.message.backendTimings = capture.backendTimings;
259
+ } else if (event.type === "error") {
260
+ event.error.responseModelIdObservation = observation;
261
+ if (capture.backendTimings !== null) event.error.backendTimings = capture.backendTimings;
262
+ }
202
263
  annotated.push(event as AssistantMessageEvent);
203
264
  }
204
265
  annotated.end();
@@ -863,6 +924,7 @@ export const openAICompletionsApiProvider: EngineApiProvider<"openai-completions
863
924
  stripNeverReasoningFromStream(
864
925
  filterGemmaChannelStream(
865
926
  withResponseModelIdCapture(
927
+ model,
866
928
  withRemainingContextBudget(model, effectiveContext, withSamplers),
867
929
  (capturedOptions) =>
868
930
  withLocalResidency(
@@ -895,6 +957,7 @@ export const openAICompletionsApiProvider: EngineApiProvider<"openai-completions
895
957
  stripNeverReasoningFromStream(
896
958
  filterGemmaChannelStream(
897
959
  withResponseModelIdCapture(
960
+ model,
898
961
  withRemainingContextBudget(model, effectiveContext, withSamplers),
899
962
  (capturedOptions) =>
900
963
  withLocalResidency(
@@ -12,12 +12,14 @@
12
12
  import { join } from "node:path";
13
13
  import { withStateFileLock } from "../../core/state-file-lock.js";
14
14
  import { clioStatePath } from "../../core/xdg.js";
15
+ import { canonicalEndpointKey } from "../../domains/providers/endpoint-capacity.js";
15
16
 
16
17
  /** Total time one process waits for another's mutation before proceeding unlocked. */
17
18
  const LOCK_WAIT_MS = 120_000;
18
19
 
19
20
  function lockTargetFor(targetKey: string): string {
20
- return join(clioStatePath(), "residency-locks", targetKey.replace(/[^a-zA-Z0-9._-]+/g, "_"));
21
+ const key = canonicalEndpointKey(targetKey) ?? targetKey;
22
+ return join(clioStatePath(), "residency-locks", key.replace(/[^a-zA-Z0-9._-]+/g, "_"));
21
23
  }
22
24
 
23
25
  /** Run `fn` while holding the per-target residency lock. */
@@ -35,7 +35,12 @@
35
35
  * unload/load against the same server.
36
36
  */
37
37
 
38
- import { BusChannels, type RuntimeNoticeKind, type RuntimeNoticePayload } from "../../core/bus-events.js";
38
+ import {
39
+ BusChannels,
40
+ type ResidencyMutationPayload,
41
+ type RuntimeNoticeKind,
42
+ type RuntimeNoticePayload,
43
+ } from "../../core/bus-events.js";
39
44
  import type { ProtectedModelRef, ResidencyRole } from "../../core/residency-protection.js";
40
45
  import { getSharedBus } from "../../core/shared-bus.js";
41
46
  import { withResidencyLock } from "./residency-lock.js";
@@ -73,6 +78,11 @@ export function emitResidencyNotice(notice: ResidencyNotice): void {
73
78
  }
74
79
  }
75
80
 
81
+ /** Publish one successful residency mutation to in-process cache telemetry. */
82
+ export function emitResidencyMutation(mutation: Omit<ResidencyMutationPayload, "at">): void {
83
+ getSharedBus().emit(BusChannels.ResidencyMutation, { ...mutation, at: Date.now() });
84
+ }
85
+
76
86
  // --- configured-model protection ---------------------------------------------
77
87
 
78
88
  // Wire model ids the operator's own configuration references, with the role
@@ -543,6 +553,13 @@ async function restoreEvictedModels(
543
553
  try {
544
554
  await adapter.reloadEvicted(entry.modelId);
545
555
  restored.push(entry.modelId);
556
+ emitResidencyMutation({
557
+ targetKey: adapter.targetKey,
558
+ targetId: adapter.targetId,
559
+ runtimeId: adapter.runtimeId,
560
+ model: entry.modelId,
561
+ operation: "load",
562
+ });
546
563
  } catch {
547
564
  failed.push(entry.modelId);
548
565
  }
@@ -705,6 +722,13 @@ export async function reconcileResidency(adapter: ResidencyAdapter): Promise<Rec
705
722
  await adapter.unload(entry.modelId);
706
723
  forgetClioLoaded(adapter.targetKey, entry.modelId);
707
724
  evicted.push(entry);
725
+ emitResidencyMutation({
726
+ targetKey: adapter.targetKey,
727
+ targetId: adapter.targetId,
728
+ runtimeId: adapter.runtimeId,
729
+ model: entry.modelId,
730
+ operation: "evict",
731
+ });
708
732
  } catch {
709
733
  // Best-effort: a failed unload self-heals on the next reconcile.
710
734
  }
@@ -712,6 +736,15 @@ export async function reconcileResidency(adapter: ResidencyAdapter): Promise<Rec
712
736
  if (!adapter.load) return;
713
737
  try {
714
738
  await adapter.load(adapter.keepModelId);
739
+ if (!plan.keepResident) {
740
+ emitResidencyMutation({
741
+ targetKey: adapter.targetKey,
742
+ targetId: adapter.targetId,
743
+ runtimeId: adapter.runtimeId,
744
+ model: adapter.keepModelId,
745
+ operation: "load",
746
+ });
747
+ }
715
748
  } catch (error) {
716
749
  // The eviction bought nothing: the slot it freed is still empty and
717
750
  // the target is now serving less than before the reconcile. Put back