@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
@@ -12,6 +12,7 @@
12
12
  import { createHash, randomBytes } from "node:crypto";
13
13
  import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
14
14
  import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
15
+ import { performance } from "node:perf_hooks";
15
16
  import { BusChannels, type DispatchCompletedPayload, type DispatchRunIdentity } from "../../core/bus-events.js";
16
17
  import { DEFAULT_SETTINGS, type DelegationToolGovernance } from "../../core/defaults.js";
17
18
  import type { DomainBundle, DomainContext, DomainExtension } from "../../core/domain-loader.js";
@@ -76,6 +77,9 @@ import { type EffectivePricing, resolveEffectivePricing } from "../providers/cat
76
77
  import {
77
78
  type CapabilityFlags,
78
79
  canonicalizeWireModelId,
80
+ type EndpointCapacity,
81
+ endpointCapacitiesForStatuses,
82
+ endpointCapacityForStatus,
79
83
  firstRuntimeResolutionError,
80
84
  isDispatchEligibleRuntime,
81
85
  type ProvidersContract,
@@ -166,7 +170,14 @@ import {
166
170
  } from "./failure-classification.js";
167
171
  import { routeFactVerdict } from "./fleet-preflight.js";
168
172
  import { competeStanceLiner, isBoundedGateRolePrompt } from "./gate-role-prompts.js";
169
- import { classifyHeartbeat, DEFAULT_HEARTBEAT_SPEC, type HeartbeatSpec, type HeartbeatStatus } from "./heartbeat.js";
173
+ import {
174
+ classifyHeartbeat,
175
+ DEFAULT_HEARTBEAT_SPEC,
176
+ type HeartbeatSpec,
177
+ type HeartbeatStamp,
178
+ type HeartbeatStatus,
179
+ heartbeatMonotonicAt,
180
+ } from "./heartbeat.js";
170
181
  import { hostVerificationRejection, runHostVerification } from "./host-verification.js";
171
182
  import { renderDispatchIntentRequirements } from "./intent-requirements.js";
172
183
  import { adaptJointRouteInput } from "./joint-route-adapter.js";
@@ -205,6 +216,7 @@ import {
205
216
  planQueueSlot,
206
217
  type ReservationCapacitySnapshot,
207
218
  rebindDispatchReservationMember,
219
+ releaseDispatchReservation,
208
220
  releaseDispatchReservationMember,
209
221
  reservedBudgetUsd,
210
222
  reservedPlanPeakSlots,
@@ -286,6 +298,7 @@ import {
286
298
  type SpawnOptions as WorkerSpawnOptions,
287
299
  type WorkerSpec,
288
300
  } from "./worker-spawn.js";
301
+ import type { WriteBoundaryAttribution, WriteBoundaryAttributionDowngrade } from "./write-boundary.js";
289
302
 
290
303
  interface RunTokenMeter {
291
304
  inputTokens: number;
@@ -337,7 +350,7 @@ interface ActiveRun {
337
350
  /** ACP event-inactivity window; null for native runs (heartbeat spec governs those). */
338
351
  stallTimeoutMs: number | null;
339
352
  lineage: RunLineage;
340
- heartbeatAt: { current: number } | null;
353
+ heartbeatAt: HeartbeatStamp | null;
341
354
  heartbeatStatus: HeartbeatStatus;
342
355
  meter: RunTokenMeter;
343
356
  pricing: EffectivePricing["rates"];
@@ -373,6 +386,8 @@ export interface DispatchBundleOptions {
373
386
  heartbeatIntervalMs?: number;
374
387
  resilienceCooldownMs?: number;
375
388
  now?: () => number;
389
+ /** Monotonic clock for heartbeat age and ACP event-inactivity spans. */
390
+ monotonicNow?: () => number;
376
391
  /** Immutable per-attempt session settings view; falls back to the shared snapshot. */
377
392
  getSettings?: () => EffectiveSettings;
378
393
  /** Live hard-block state cloned into each mediated worker spec. */
@@ -2397,6 +2412,7 @@ export function createDispatchBundle(
2397
2412
  return DEFAULT_RESILIENCE_COOLDOWN_MS;
2398
2413
  };
2399
2414
  const now = options?.now ?? (() => Date.now());
2415
+ const monotonicNow = options?.monotonicNow ?? (() => performance.now());
2400
2416
  // Durable evidence owner used by shadow observation and active readiness.
2401
2417
  const routeObserver: RouteObserver = options?.routeObserver ?? createRouteObserver({});
2402
2418
 
@@ -2406,9 +2422,9 @@ export function createDispatchBundle(
2406
2422
  const targetCooldowns = new Map<string, { until: number; reason: string }>();
2407
2423
  const ownedReservations = new Set<string>();
2408
2424
  /**
2409
- * Paths each finalized run's own tool calls aimed a successful mutation at,
2410
- * newest last. `null` records a run whose write record is unusable, which is
2411
- * a different answer from an empty list and has to survive as one.
2425
+ * Attribution from each finalized run's successful tool calls, newest last.
2426
+ * An incomplete record retains its paths as a lower bound beside the reasons
2427
+ * it cannot be read as a closed list.
2412
2428
  *
2413
2429
  * Held in memory rather than sealed into the receipt: the only reader is the
2414
2430
  * write boundary closing a window seconds later in this process, and a
@@ -2416,13 +2432,13 @@ export function createDispatchBundle(
2416
2432
  * Bounded because a long session finalizes many runs and none of this is
2417
2433
  * needed once its window has closed.
2418
2434
  */
2419
- const runWriteTargets = new Map<string, ReadonlyArray<string> | null>();
2420
- const recordRunWriteTargets = (runId: string, paths: ReadonlyArray<string> | null): void => {
2421
- runWriteTargets.set(runId, paths);
2422
- while (runWriteTargets.size > RUN_WRITE_TARGET_HISTORY) {
2423
- const oldest = runWriteTargets.keys().next();
2435
+ const runWriteAttributions = new Map<string, WriteBoundaryAttribution>();
2436
+ const recordRunWriteAttribution = (runId: string, attribution: WriteBoundaryAttribution): void => {
2437
+ runWriteAttributions.set(runId, attribution);
2438
+ while (runWriteAttributions.size > RUN_WRITE_TARGET_HISTORY) {
2439
+ const oldest = runWriteAttributions.keys().next();
2424
2440
  if (oldest.done === true) break;
2425
- runWriteTargets.delete(oldest.value);
2441
+ runWriteAttributions.delete(oldest.value);
2426
2442
  }
2427
2443
  };
2428
2444
  const capacityAdmission = createCapacityAdmissionController({
@@ -2430,7 +2446,7 @@ export function createDispatchBundle(
2430
2446
  const settings = getEffectiveSettings();
2431
2447
  const nodes: Record<string, number> = { local: configuredGlobalCapacity(settings) };
2432
2448
  for (const node of settings?.fleet?.nodes ?? []) nodes[node.id] = node.maxWorkers;
2433
- return { global: configuredGlobalCapacity(settings), nodes };
2449
+ return { global: configuredGlobalCapacity(settings), nodes, endpoints: configuredEndpointLimits() };
2434
2450
  },
2435
2451
  now,
2436
2452
  reservedPlanPeak: (planId) =>
@@ -2443,6 +2459,21 @@ export function createDispatchBundle(
2443
2459
  }),
2444
2460
  );
2445
2461
 
2462
+ function configuredEndpointLimits(): Readonly<Record<string, number>> {
2463
+ return Object.fromEntries(
2464
+ Object.entries(endpointCapacitiesForStatuses(providers.list())).map(([key, value]) => [key, value.limit]),
2465
+ );
2466
+ }
2467
+
2468
+ function endpointCapacityForTarget(targetId: string): EndpointCapacity | null {
2469
+ const status = providers.list().find((entry) => entry.target.id === targetId);
2470
+ if (status === undefined) return null;
2471
+ const endpoint = endpointCapacityForStatus(status);
2472
+ if (endpoint === null) return null;
2473
+ const configured = endpointCapacitiesForStatuses(providers.list())[endpoint.key];
2474
+ return configured ?? endpoint;
2475
+ }
2476
+
2446
2477
  function reservationCapacitySnapshot(settings: EffectiveSettings): ReservationCapacitySnapshot {
2447
2478
  const preflight = scheduling.preflight();
2448
2479
  // Reservation planning compares against a fresh read, never the display cache.
@@ -2452,9 +2483,21 @@ export function createDispatchBundle(
2452
2483
  };
2453
2484
  for (const node of settings?.fleet?.nodes ?? [])
2454
2485
  nodes[node.id] = { active: usage.nodes[node.id] ?? 0, limit: node.maxWorkers };
2486
+ const endpoints = Object.fromEntries(
2487
+ Object.entries(configuredEndpointLimits()).map(([key, limit]) => [
2488
+ key,
2489
+ {
2490
+ active: usage.endpoints[key] ?? 0,
2491
+ limit,
2492
+ leases: usage.endpointHolders[key]?.leases ?? 0,
2493
+ foregroundStreams: usage.endpointHolders[key]?.foregroundStreams ?? 0,
2494
+ },
2495
+ ]),
2496
+ );
2455
2497
  return {
2456
2498
  global: { active: usage.global, limit: configuredGlobalCapacity(settings) },
2457
2499
  nodes,
2500
+ endpoints,
2458
2501
  budget: { currentUsd: preflight.currentUsd, ceilingUsd: preflight.ceilingUsd },
2459
2502
  };
2460
2503
  }
@@ -2466,6 +2509,7 @@ export function createDispatchBundle(
2466
2509
  memberId: task.memberId,
2467
2510
  wave: task.wave,
2468
2511
  nodeId: task.resolution.node.id,
2512
+ ...(task.resolution.endpoint !== undefined ? { endpointKey: task.resolution.endpoint.key } : {}),
2469
2513
  costUpperBoundUsd: task.resolution.routeApproval?.totalCostUpperBoundUsd ?? task.resolution.costUpperBoundUsd,
2470
2514
  })),
2471
2515
  capacity: reservationCapacitySnapshot(getEffectiveSettings()),
@@ -2520,7 +2564,12 @@ export function createDispatchBundle(
2520
2564
  return scheduling.maxWorkers?.() ?? (configured === "auto" || configured === undefined ? 4 : Math.max(1, configured));
2521
2565
  }
2522
2566
 
2523
- async function admitAssignmentCapacity(req: DispatchRequest, nodeId: string, timing: RunPhaseMarks) {
2567
+ async function admitAssignmentCapacity(
2568
+ req: DispatchRequest,
2569
+ nodeId: string,
2570
+ timing: RunPhaseMarks,
2571
+ endpoint?: EndpointCapacity | null,
2572
+ ) {
2524
2573
  const queuedAt = now();
2525
2574
  timing.queuedAt = new Date(queuedAt).toISOString();
2526
2575
  const assignmentId = req.lineage?.rootRunId ?? `pending-${queuedAt.toString(36)}-${randomBytes(6).toString("hex")}`;
@@ -2538,9 +2587,16 @@ export function createDispatchBundle(
2538
2587
  const admitted = await capacityAdmission.admit({
2539
2588
  assignmentId,
2540
2589
  nodeId,
2590
+ ...(endpoint !== null && endpoint !== undefined ? { endpointKey: endpoint.key } : {}),
2541
2591
  deadlineAt,
2542
2592
  ...plan,
2543
- ...(req.reservation !== undefined && req.lineage === undefined ? { reservation: req.reservation } : {}),
2593
+ // A fleet's first attempt carries lineage and still owns the held
2594
+ // reservation member that admission must transfer into its lease.
2595
+ // Retries carry the same reservation identity after that member was
2596
+ // consumed, so they reacquire/rebind the assignment lease normally.
2597
+ ...(req.reservation !== undefined && (req.lineage === undefined || req.lineage.attempt === 0)
2598
+ ? { reservation: req.reservation }
2599
+ : {}),
2544
2600
  });
2545
2601
  timing.admittedAt = new Date(admitted.admittedAt).toISOString();
2546
2602
  return admitted.lease;
@@ -2557,6 +2613,7 @@ export function createDispatchBundle(
2557
2613
  function rebindReservationSlot(
2558
2614
  req: DispatchRequest,
2559
2615
  nodeId: string,
2616
+ endpoint: EndpointCapacity | null,
2560
2617
  costUsd: number,
2561
2618
  settings: EffectiveSettings,
2562
2619
  ): void {
@@ -2565,6 +2622,7 @@ export function createDispatchBundle(
2565
2622
  const rebind = {
2566
2623
  ...req.reservation,
2567
2624
  nodeId,
2625
+ ...(endpoint !== null ? { endpointKey: endpoint.key } : {}),
2568
2626
  costUpperBoundUsd: req.routeApproval?.totalCostUpperBoundUsd ?? costUsd,
2569
2627
  capacity,
2570
2628
  nowMs: now(),
@@ -2672,6 +2730,21 @@ export function createDispatchBundle(
2672
2730
 
2673
2731
  type RetryScheduleResult = { scheduled: true } | { scheduled: false; settlementDetail?: string };
2674
2732
 
2733
+ /** A reserved plan member owns its retry chain independently of the shared fleet root assignment. */
2734
+ function retryChainIsLive(run: ActiveRun): boolean {
2735
+ if (run.aborted) return false;
2736
+ const reservation = run.req.reservation;
2737
+ if (reservation === undefined) return assignments.get(run.lineage.rootRunId)?.status === "running";
2738
+ try {
2739
+ const record = getDispatchReservation(reservation.ownerId);
2740
+ const member = record?.members.find((entry) => entry.memberId === reservation.memberId);
2741
+ return record?.status === "active" && member !== undefined && member.status !== "released";
2742
+ } catch (error) {
2743
+ reportDispatchDiagnostic(`read retry liveness for reservation member ${reservation.memberId}`, error);
2744
+ return false;
2745
+ }
2746
+ }
2747
+
2675
2748
  function maybeScheduleRetry(
2676
2749
  run: ActiveRun,
2677
2750
  outcome: RunOutcome,
@@ -2681,7 +2754,7 @@ export function createDispatchBundle(
2681
2754
  ): RetryScheduleResult {
2682
2755
  if (draining) return { scheduled: false };
2683
2756
  const rootRunId = run.lineage.rootRunId;
2684
- if (assignments.get(rootRunId)?.status !== "running") return { scheduled: false };
2757
+ if (!retryChainIsLive(run)) return { scheduled: false };
2685
2758
  if (!RETRYABLE_OUTCOMES.has(outcome)) {
2686
2759
  retryBackoff.delete(rootRunId);
2687
2760
  return { scheduled: false };
@@ -2828,7 +2901,7 @@ export function createDispatchBundle(
2828
2901
  reason = "retry",
2829
2902
  decision: RetryDecision = decideRetry("internal", run.lineage.attempt, assignmentPolicyFor(run.req).maxRetries),
2830
2903
  ): Promise<void> {
2831
- if (draining || assignments.get(run.lineage.rootRunId)?.status !== "running") return;
2904
+ if (draining || !retryChainIsLive(run)) return;
2832
2905
  const excludesNode = decision.excludedRouteParts.includes("node");
2833
2906
  // Record the failed node so placement can choose an eligible survivor.
2834
2907
  const rerouteHops =
@@ -3010,8 +3083,8 @@ export function createDispatchBundle(
3010
3083
  }
3011
3084
  }
3012
3085
 
3013
- function heartbeatIso(heartbeatMs: number): string {
3014
- return new Date(heartbeatMs).toISOString();
3086
+ function heartbeatIso(heartbeat: HeartbeatStamp): string {
3087
+ return new Date(heartbeat.current).toISOString();
3015
3088
  }
3016
3089
 
3017
3090
  function heartbeatRunStatus(status: HeartbeatStatus): RunStatus {
@@ -3032,7 +3105,7 @@ export function createDispatchBundle(
3032
3105
  event: {
3033
3106
  type: "heartbeat_status",
3034
3107
  status,
3035
- heartbeatAt: run.heartbeatAt ? heartbeatIso(run.heartbeatAt.current) : null,
3108
+ heartbeatAt: run.heartbeatAt ? heartbeatIso(run.heartbeatAt) : null,
3036
3109
  },
3037
3110
  });
3038
3111
  }
@@ -3050,16 +3123,16 @@ export function createDispatchBundle(
3050
3123
  */
3051
3124
  function checkActiveHeartbeats(): void {
3052
3125
  if (!ledger) return;
3053
- const tickNow = now();
3126
+ const tickMonotonic = monotonicNow();
3054
3127
  for (const run of active.values()) {
3055
3128
  if (run.aborted || run.stallKilled || !run.heartbeatAt) continue;
3056
- const heartbeatMs = run.heartbeatAt.current;
3057
- if (!Number.isFinite(heartbeatMs)) continue;
3129
+ const heartbeatMonotonic = heartbeatMonotonicAt(run.heartbeatAt);
3130
+ if (!Number.isFinite(heartbeatMonotonic) || !Number.isFinite(run.heartbeatAt.current)) continue;
3058
3131
  if (run.runtimeKind === "acp-delegation") {
3059
- ledger.update(run.runId, { heartbeatAt: heartbeatIso(heartbeatMs) });
3132
+ ledger.update(run.runId, { heartbeatAt: heartbeatIso(run.heartbeatAt) });
3060
3133
  const stallMs = run.stallTimeoutMs;
3061
3134
  if (stallMs === null || stallMs <= 0) continue;
3062
- if (tickNow - heartbeatMs <= stallMs) continue;
3135
+ if (tickMonotonic - heartbeatMonotonic <= stallMs) continue;
3063
3136
  run.stallKilled = true;
3064
3137
  run.heartbeatStatus = "dead";
3065
3138
  emitHeartbeatStatus(run, "dead");
@@ -3070,10 +3143,10 @@ export function createDispatchBundle(
3070
3143
  }
3071
3144
  continue;
3072
3145
  }
3073
- const status = classifyHeartbeat(heartbeatMs, tickNow, heartbeatSpec);
3146
+ const status = classifyHeartbeat(heartbeatMonotonic, tickMonotonic, heartbeatSpec);
3074
3147
  const patch: Partial<RunEnvelope> = {
3075
3148
  status: heartbeatRunStatus(status),
3076
- heartbeatAt: heartbeatIso(heartbeatMs),
3149
+ heartbeatAt: heartbeatIso(run.heartbeatAt),
3077
3150
  };
3078
3151
  ledger.update(run.runId, patch);
3079
3152
  // Node staleness display feeds off the freshest worker heartbeat.
@@ -3493,7 +3566,7 @@ export function createDispatchBundle(
3493
3566
 
3494
3567
  assertBudgetAdmitsRoute(req, { rates: null, provenance: "unknown" }, settings);
3495
3568
 
3496
- const capacityLease = await admitAssignmentCapacity(req, "local", timing);
3569
+ const capacityLease = await admitAssignmentCapacity(req, "local", timing, null);
3497
3570
  const leaseSlot = createLeaseSlotGuard(capacityAdmission, capacityLease.leaseId, req.lineage !== undefined);
3498
3571
 
3499
3572
  // Resolve the ledger before starting the ACP process: an external agent
@@ -3519,6 +3592,8 @@ export function createDispatchBundle(
3519
3592
  safety,
3520
3593
  autonomy: lifecycle.autonomy,
3521
3594
  clientVersion: readClioVersion(),
3595
+ now,
3596
+ monotonicNow,
3522
3597
  });
3523
3598
  } catch (error) {
3524
3599
  leaseSlot.release();
@@ -3707,7 +3782,7 @@ export function createDispatchBundle(
3707
3782
  ledgerRef.update(envelope.id, {
3708
3783
  status: "running",
3709
3784
  pid: acp.pid,
3710
- heartbeatAt: heartbeatIso(acp.heartbeatAt.current),
3785
+ heartbeatAt: heartbeatIso(acp.heartbeatAt),
3711
3786
  lineage,
3712
3787
  identity,
3713
3788
  node: LOCAL_RUN_NODE,
@@ -4104,7 +4179,7 @@ export function createDispatchBundle(
4104
4179
  cacheReadTokenCount: receiptDraft.cacheReadTokenCount ?? 0,
4105
4180
  cacheWriteTokenCount: receiptDraft.cacheWriteTokenCount ?? 0,
4106
4181
  reasoningTokenCount: receiptDraft.reasoningTokenCount ?? 0,
4107
- heartbeatAt: heartbeatIso(acp.heartbeatAt.current),
4182
+ heartbeatAt: heartbeatIso(acp.heartbeatAt),
4108
4183
  };
4109
4184
  ledgerRef.update(envelope.id, ledgerPatch);
4110
4185
  const receipt = ledgerRef.recordReceipt(envelope.id, sealRouteDecision(receiptDraft, routeDecision));
@@ -4262,7 +4337,7 @@ export function createDispatchBundle(
4262
4337
  "dispatch: writeRoots cannot be enforced on an ACP delegation target; the external agent runs its own tool surface. Dispatch to a native or claude-sdk worker.",
4263
4338
  );
4264
4339
  }
4265
- rebindReservationSlot(req, "local", UNKNOWN_PRICING_ADMISSION_ESTIMATE_USD, settings);
4340
+ rebindReservationSlot(req, "local", null, UNKNOWN_PRICING_ADMISSION_ESTIMATE_USD, settings);
4266
4341
  routeObservation = observeShadowRoute(req, undefined, settings);
4267
4342
  return attachRouteObservation({
4268
4343
  ...(await dispatchAcpDelegation(req, settings, timing, routeObservation.decision, observer)),
@@ -4296,10 +4371,12 @@ export function createDispatchBundle(
4296
4371
  model: lifecycle.target.wireModelId,
4297
4372
  node: placement?.node?.id ?? "local",
4298
4373
  };
4374
+ const endpoint = endpointCapacityForTarget(lifecycle.target.target.id);
4299
4375
  assertRouteWithinApprovedEnvelope(req, effectiveRoute);
4300
4376
  rebindReservationSlot(
4301
4377
  req,
4302
4378
  effectiveRoute.node,
4379
+ endpoint,
4303
4380
  conservativeRouteAdmissionEstimateUsd(lifecycle.target.effectivePricing, admissionMaxOutputTokens(settings)),
4304
4381
  settings,
4305
4382
  );
@@ -4318,7 +4395,7 @@ export function createDispatchBundle(
4318
4395
  shadow: () => observeShadowRoute(req, placed, settings),
4319
4396
  });
4320
4397
 
4321
- const capacityLease = await admitAssignmentCapacity(req, placement?.node.id ?? "local", timing);
4398
+ const capacityLease = await admitAssignmentCapacity(req, placement?.node.id ?? "local", timing, endpoint);
4322
4399
  const leaseSlot = createLeaseSlotGuard(capacityAdmission, capacityLease.leaseId, req.lineage !== undefined);
4323
4400
 
4324
4401
  const ledgerRef = (() => {
@@ -4387,6 +4464,8 @@ export function createDispatchBundle(
4387
4464
  try {
4388
4465
  worker = (placement?.spawn ?? spawnWorker)(spec, {
4389
4466
  cwd: lifecycle.cwd,
4467
+ now,
4468
+ monotonicNow,
4390
4469
  ...(agentLedgerId !== null ? { onLedgerPost } : {}),
4391
4470
  });
4392
4471
  } catch (error) {
@@ -4443,10 +4522,21 @@ export function createDispatchBundle(
4443
4522
  const upstreamResponses: RunReceiptUpstreamResponse[] = [];
4444
4523
  const skillActivations: SkillActivation[] = [];
4445
4524
  const finishContractEntries: unknown[] = [];
4525
+ let runIdForPermissionAudit: string | null = null;
4446
4526
  // The worker's own tool calls, folded into what this run changed and what
4447
4527
  // it validated. The sealed mutation-report contract is measured against
4448
4528
  // this, so a reported path the run never touched cannot seal as fact.
4449
- const runEffects = createRunEffectsRecorder(lifecycle.cwd);
4529
+ const runEffects = createRunEffectsRecorder(lifecycle.cwd, {
4530
+ onWriteRecordDowngraded(downgrade) {
4531
+ const runId = runIdForPermissionAudit;
4532
+ if (runId === null) return;
4533
+ context.bus.emit(BusChannels.DispatchProgress, {
4534
+ runId,
4535
+ agentId: req.agentId,
4536
+ event: { type: "clio_write_record_downgraded", payload: downgrade },
4537
+ });
4538
+ },
4539
+ });
4450
4540
  let finishContractAssistantText = "";
4451
4541
  let finishContractAssistantTurnId: string | null = null;
4452
4542
  let failureMessage: string | undefined;
@@ -4456,7 +4546,6 @@ export function createDispatchBundle(
4456
4546
  const acceptsOutcomeCodeEvents =
4457
4547
  lifecycle.runtimeKind === "http" ||
4458
4548
  (lifecycle.runtimeKind === "sdk" && lifecycle.target.runtime.id === "claude-sdk");
4459
- let runIdForPermissionAudit: string | null = null;
4460
4549
  let workerPolicyPermissionCounter = 0;
4461
4550
  const outputCapture = createWorkerOutputCapture();
4462
4551
  const markObservedPhase = (field: "firstModelTokenAt" | "firstToolAt"): void => {
@@ -4739,7 +4828,7 @@ export function createDispatchBundle(
4739
4828
  ...(req.council !== undefined ? { council: req.council } : {}),
4740
4829
  ...(req.plan !== undefined ? { plan: req.plan } : {}),
4741
4830
  ...(lifecycle.personaOverride ? { personaOverride: lifecycle.personaOverride } : {}),
4742
- ...(heartbeatAt ? { heartbeatAt: heartbeatIso(heartbeatAt.current) } : {}),
4831
+ ...(heartbeatAt ? { heartbeatAt: heartbeatIso(heartbeatAt) } : {}),
4743
4832
  });
4744
4833
  observer?.onAdmitted({
4745
4834
  runId: envelope.id,
@@ -4755,6 +4844,7 @@ export function createDispatchBundle(
4755
4844
  // per-worker meter. All optional so consumers degrade to local/plain.
4756
4845
  const fleetIdentity = {
4757
4846
  budget: lifecycle.budgetEnvelope,
4847
+ ...(endpoint !== null ? { endpoint: { key: endpoint.key, label: endpoint.label, limit: endpoint.limit } } : {}),
4758
4848
  ...(placement !== undefined && placement !== null ? { node: placement.node.id } : {}),
4759
4849
  ...(req.gate !== undefined ? { gate: { role: req.gate.role, cycle: req.gate.cycle } } : {}),
4760
4850
  ...(req.council !== undefined ? { council: req.council } : {}),
@@ -5331,7 +5421,7 @@ export function createDispatchBundle(
5331
5421
  ...(receiptDraft.cacheWriteTokenCount !== undefined
5332
5422
  ? { cacheWriteTokenCount: receiptDraft.cacheWriteTokenCount }
5333
5423
  : {}),
5334
- ...(activeRun.heartbeatAt ? { heartbeatAt: heartbeatIso(activeRun.heartbeatAt.current) } : {}),
5424
+ ...(activeRun.heartbeatAt ? { heartbeatAt: heartbeatIso(activeRun.heartbeatAt) } : {}),
5335
5425
  };
5336
5426
  if (receiptDraft.reasoningTokenCount !== undefined) {
5337
5427
  ledgerPatch.reasoningTokenCount = receiptDraft.reasoningTokenCount;
@@ -5354,9 +5444,28 @@ export function createDispatchBundle(
5354
5444
  // the set. That run's window keeps today's blame-the-whole-diff
5355
5445
  // behaviour; a run that never touched such a tool keeps the
5356
5446
  // protection.
5357
- const writeRecordUsable =
5358
- receipt.safety?.toolTelemetry?.coverage === "complete" && observedRunEffects.writeRecordComplete;
5359
- recordRunWriteTargets(envelope.id, writeRecordUsable ? [...observedRunEffects.mutatedPaths] : null);
5447
+ const toolTelemetryComplete = receipt.safety?.toolTelemetry?.coverage === "complete";
5448
+ const downgrades: WriteBoundaryAttributionDowngrade[] = observedRunEffects.writeRecordDowngrades.map((entry) => ({
5449
+ reason: entry.reason,
5450
+ tool: entry.tool,
5451
+ toolCallId: entry.toolCallId,
5452
+ runId: envelope.id,
5453
+ stepId: null,
5454
+ }));
5455
+ if (!toolTelemetryComplete) {
5456
+ downgrades.push({
5457
+ reason: "incomplete_tool_telemetry",
5458
+ tool: null,
5459
+ toolCallId: null,
5460
+ runId: envelope.id,
5461
+ stepId: null,
5462
+ });
5463
+ }
5464
+ recordRunWriteAttribution(envelope.id, {
5465
+ recorded: [...observedRunEffects.mutatedPaths],
5466
+ complete: toolTelemetryComplete && observedRunEffects.writeRecordComplete,
5467
+ downgrades,
5468
+ });
5360
5469
  await ledgerRef.persist();
5361
5470
  if (worktreeReceipt?.applied === true && req.taskWorktree !== undefined) {
5362
5471
  try {
@@ -5628,12 +5737,14 @@ export function createDispatchBundle(
5628
5737
  allowedTools: effectiveTools,
5629
5738
  settings,
5630
5739
  });
5740
+ const endpoint = endpointCapacityForTarget(target.target.id);
5631
5741
  return {
5632
5742
  agentId: req.agentId,
5633
5743
  specFingerprint: agentSpecFingerprint(agentSpec),
5634
5744
  targetId: target.target.id,
5635
5745
  wireModelId: target.wireModelId,
5636
5746
  runtimeId: target.runtime.id,
5747
+ ...(endpoint !== null ? { endpoint: { key: endpoint.key, label: endpoint.label, limit: endpoint.limit } } : {}),
5637
5748
  node: previewNode(req).node,
5638
5749
  thinkingLevel: target.thinkingLevel,
5639
5750
  toolSignature: toolSignature(effectiveTools),
@@ -6133,19 +6244,21 @@ export function createDispatchBundle(
6133
6244
  */
6134
6245
  function snapshot(): DispatchSnapshot {
6135
6246
  const tickNow = now();
6247
+ const tickMonotonic = monotonicNow();
6136
6248
  const running: DispatchSnapshot["running"] = [];
6137
6249
  const totals = { ...finalizedTotals };
6138
6250
  const costAmounts = [...finalizedCosts];
6139
6251
  for (const run of active.values()) {
6140
6252
  let heartbeat: "alive" | "stale" | "dead" | "n/a" = "n/a";
6141
- if (run.heartbeatAt && Number.isFinite(run.heartbeatAt.current)) {
6253
+ if (run.heartbeatAt && Number.isFinite(heartbeatMonotonicAt(run.heartbeatAt))) {
6254
+ const heartbeatMonotonic = heartbeatMonotonicAt(run.heartbeatAt);
6142
6255
  if (run.runtimeKind === "acp-delegation") {
6143
6256
  const stallMs = run.stallTimeoutMs;
6144
6257
  if (stallMs !== null && stallMs > 0) {
6145
- heartbeat = tickNow - run.heartbeatAt.current > stallMs ? "dead" : "alive";
6258
+ heartbeat = tickMonotonic - heartbeatMonotonic > stallMs ? "dead" : "alive";
6146
6259
  }
6147
6260
  } else {
6148
- heartbeat = classifyHeartbeat(run.heartbeatAt.current, tickNow, heartbeatSpec);
6261
+ heartbeat = classifyHeartbeat(heartbeatMonotonic, tickMonotonic, heartbeatSpec);
6149
6262
  }
6150
6263
  }
6151
6264
  const meter = run.meter;
@@ -6271,6 +6384,7 @@ export function createDispatchBundle(
6271
6384
  routeCandidates,
6272
6385
  reservations: {
6273
6386
  prepare: prepareReservation,
6387
+ release: (ownerId) => releaseDispatchReservation(ownerId, now()),
6274
6388
  rollback: (ownerId) => rollbackDispatchReservation(ownerId, now()),
6275
6389
  rollbackUnconsumed: (ownerId) => rollbackUnconsumedDispatchReservation(ownerId, now()),
6276
6390
  get: getDispatchReservation,
@@ -6291,7 +6405,17 @@ export function createDispatchBundle(
6291
6405
  observedRunWrites(runId) {
6292
6406
  // A run this process never finalized, or finalized long enough ago to
6293
6407
  // have been evicted, is unknown rather than silent, so both answer null.
6294
- return runWriteTargets.get(runId) ?? null;
6408
+ const attribution = runWriteAttributions.get(runId);
6409
+ return attribution?.complete === true ? [...attribution.recorded] : null;
6410
+ },
6411
+ observedRunWriteAttribution(runId) {
6412
+ const attribution = runWriteAttributions.get(runId);
6413
+ if (attribution === undefined) return null;
6414
+ return {
6415
+ recorded: [...attribution.recorded],
6416
+ complete: attribution.complete,
6417
+ downgrades: attribution.downgrades?.map((entry) => ({ ...entry })) ?? [],
6418
+ };
6295
6419
  },
6296
6420
  assignments: {
6297
6421
  get: (id) => assignments.get(id),
@@ -46,6 +46,7 @@ import {
46
46
  import { verifyReceiptIntegrity } from "./receipt-integrity.js";
47
47
  import { type FleetRunRecord, writeFleetRun } from "./state.js";
48
48
  import type { RunReceipt } from "./types.js";
49
+ import type { WriteBoundaryAttribution } from "./write-boundary.js";
49
50
  import { createWriteBoundaryEnforcer } from "./write-boundary-enforcer.js";
50
51
 
51
52
  /** Owner id for a plan with no model runs; it holds nothing to release. */
@@ -278,10 +279,29 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
278
279
  // A code step runs a registered command in a subprocess. Nothing
279
280
  // enumerates what that command wrote, so the step contributes no record
280
281
  // and its window falls back to blaming the whole diff.
281
- if (step === undefined || step.kind !== "agent") return null;
282
+ if (step === undefined) return null;
283
+ if (step.kind !== "agent") {
284
+ return {
285
+ recorded: [],
286
+ complete: false,
287
+ downgrades: [
288
+ {
289
+ reason: "code_step_untracked",
290
+ tool: null,
291
+ toolCallId: null,
292
+ runId: null,
293
+ stepId,
294
+ },
295
+ ],
296
+ } satisfies WriteBoundaryAttribution;
297
+ }
282
298
  const receipt = receiptsByStep.get(stepId);
283
299
  if (receipt === undefined) return null;
284
- return dispatch.observedRunWrites?.(receipt.runId) ?? null;
300
+ if (dispatch.observedRunWriteAttribution !== undefined) {
301
+ return dispatch.observedRunWriteAttribution(receipt.runId);
302
+ }
303
+ const recorded = dispatch.observedRunWrites?.(receipt.runId) ?? null;
304
+ return recorded === null ? null : { recorded, complete: true, downgrades: [] };
285
305
  },
286
306
  onVerdict(verdict, path) {
287
307
  // Concurrent changes are reported too. The window did not fail for them
@@ -704,7 +724,7 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
704
724
  cancel: (assignmentId) => dispatch.abort(assignmentId),
705
725
  release: (ownerId) => {
706
726
  if (ownerId === NO_WORKER_RESERVATION) return;
707
- dispatch.reservations?.rollbackUnconsumed(ownerId);
727
+ dispatch.reservations?.release(ownerId);
708
728
  },
709
729
  releaseUnconsumed: (ownerId) => {
710
730
  if (ownerId === NO_WORKER_RESERVATION) return;
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Pure heartbeat watchdog classifier (Phase 6 slice 3).
3
3
  *
4
- * Maps a last-seen heartbeat timestamp to one of three states. The dispatch
5
- * domain drives this on each scheduler tick; the orchestrator uses `stale` to
6
- * surface a warning in the UI and `dead` to reap a subprocess whose event
7
- * stream has gone quiet for too long.
4
+ * Maps a last-seen monotonic stamp to one of three states. The dispatch domain
5
+ * drives this on each scheduler tick; the orchestrator uses `stale` to surface
6
+ * a warning in the UI and `dead` to reap a subprocess whose event stream has
7
+ * gone quiet for too long.
8
8
  *
9
- * No I/O, no clocks of its own. Callers pass `now` to keep the function pure
10
- * and easy to test.
9
+ * No I/O, no clocks of its own. Callers pass both monotonic values to keep the
10
+ * function pure and easy to test.
11
11
  */
12
12
 
13
13
  export interface HeartbeatSpec {
@@ -17,10 +17,34 @@ export interface HeartbeatSpec {
17
17
 
18
18
  export type HeartbeatStatus = "alive" | "stale" | "dead";
19
19
 
20
+ /**
21
+ * Mutable heartbeat facts shared by a transport and the dispatch watchdog.
22
+ * `current` is an absolute wall-clock instant for display and persistence.
23
+ * `monotonic` is from this process's monotonic clock and is the only value
24
+ * liveness code may compare with a later clock read.
25
+ *
26
+ * The monotonic field is optional only for injected legacy test handles. Real
27
+ * transports always provide it. Treating an old handle's `current` as its
28
+ * monotonic stamp preserves the structural test seam without reintroducing a
29
+ * subtraction between two wall-clock reads in the product path.
30
+ */
31
+ export interface HeartbeatStamp {
32
+ current: number;
33
+ monotonic?: number;
34
+ }
35
+
20
36
  export const DEFAULT_HEARTBEAT_SPEC: HeartbeatSpec = { windowMs: 5000, graceMs: 10000 };
21
37
 
22
- export function classifyHeartbeat(heartbeatAt: number, now: number, spec: HeartbeatSpec): HeartbeatStatus {
23
- const age = now - heartbeatAt;
38
+ export function heartbeatMonotonicAt(heartbeat: HeartbeatStamp): number {
39
+ return heartbeat.monotonic ?? heartbeat.current;
40
+ }
41
+
42
+ export function classifyHeartbeat(
43
+ heartbeatMonotonicAt: number,
44
+ monotonicNow: number,
45
+ spec: HeartbeatSpec,
46
+ ): HeartbeatStatus {
47
+ const age = monotonicNow - heartbeatMonotonicAt;
24
48
  if (age <= spec.windowMs) return "alive";
25
49
  if (age <= spec.windowMs + spec.graceMs) return "stale";
26
50
  return "dead";
@@ -161,12 +161,15 @@ export {
161
161
  export type {
162
162
  WorkspaceSnapshot,
163
163
  WriteBoundaryAttribution,
164
+ WriteBoundaryAttributionDowngrade,
165
+ WriteBoundaryAttributionDowngradeReason,
164
166
  WriteBoundaryStatus,
165
167
  WriteBoundaryVerdict,
166
168
  } from "./write-boundary.js";
167
169
  export {
168
170
  assertWriteBoundaryVisibleToGit,
169
171
  captureWorkspaceSnapshot,
172
+ describeWriteBoundaryAttributionDowngrade,
170
173
  diffWorkspace,
171
174
  enforceWriteBoundary,
172
175
  WRITE_BOUNDARY_VIOLATION_REASON,
@@ -172,6 +172,11 @@ function sealedEnvelopeFor(runId: string): RunEnvelope | null {
172
172
  * whole orchestrator downtime, which is not a measurement of anything. The
173
173
  * last heartbeat is the honest bound; a row that never heartbeat has only its
174
174
  * own start, which reads as a zero-length run rather than an invented one.
175
+ *
176
+ * A restart has no shared monotonic origin with the process that owned the
177
+ * worker, so recovery never estimates heartbeat age. It first adjudicates the
178
+ * host-scoped pid and uses the persisted wall-clock heartbeat only as this
179
+ * display/evidence bound after that process is known to be gone.
175
180
  */
176
181
  function closeAbandonedRows(ledger: Ledger): { closed: number; sealed: number } {
177
182
  let closed = 0;