@iowarp/clio-coder 0.3.7 → 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 (400) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +13 -4
  3. package/dist/{acp-SK4MD6MM.js → acp-7LOELQFP.js} +13 -13
  4. package/dist/{agents-2FN2K6ME.js → agents-FIBG2SHA.js} +41 -37
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-QIYZWM5I.js → auth-OI4LIH2I.js} +31 -24
  7. package/dist/builtins-AD25UL3C.js +17 -0
  8. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  9. package/dist/chunk-3DPEIQKN.js +113 -0
  10. package/dist/{chunk-EOOQZZDE.js → chunk-3DUR4WUA.js} +19 -19
  11. package/dist/{chunk-WHJYKASB.js → chunk-3MRC2YSQ.js} +2 -2
  12. package/dist/{chunk-EBEFWSGL.js → chunk-3UUY7R3Z.js} +14 -10
  13. package/dist/{chunk-LADCF22A.js → chunk-3V5AYSEQ.js} +113 -54
  14. package/dist/{chunk-BMWK7ZIZ.js → chunk-465CC7FK.js} +16 -13
  15. package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
  16. package/dist/{chunk-CEYBNUGC.js → chunk-4H6ULJ3H.js} +378 -36
  17. package/dist/{chunk-YTYFXUI3.js → chunk-4LJX2PUC.js} +9 -9
  18. package/dist/{chunk-DOOEX22V.js → chunk-56KB5IJP.js} +5 -5
  19. package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
  20. package/dist/{chunk-TSHXZTOQ.js → chunk-5HFBWUMU.js} +23 -11
  21. package/dist/{chunk-5UJ6ECTS.js → chunk-5PVQ4SRS.js} +80 -8
  22. package/dist/{chunk-ZWLZP4ZT.js → chunk-5QKCQQ3E.js} +359 -17
  23. package/dist/{chunk-6M7VS3J3.js → chunk-5T7RBWN2.js} +111 -5
  24. package/dist/chunk-774ILSRL.js +172 -0
  25. package/dist/chunk-7C6RYZGQ.js +391 -0
  26. package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
  27. package/dist/{chunk-C4JBQ5SR.js → chunk-AD7Y7STJ.js} +6 -6
  28. package/dist/{chunk-GEYXPTRF.js → chunk-AEYBF3TB.js} +33 -12
  29. package/dist/{chunk-2SFS6XQE.js → chunk-AMKHQW3C.js} +3 -2
  30. package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
  31. package/dist/{chunk-MXI6J5JF.js → chunk-B5XRQOLB.js} +10 -10
  32. package/dist/{chunk-X2KV5FXT.js → chunk-BVDVID7E.js} +2 -2
  33. package/dist/{chunk-JNXPYBB4.js → chunk-CA42X6KT.js} +3 -3
  34. package/dist/{chunk-VREKEFLL.js → chunk-D73KXYPF.js} +3 -3
  35. package/dist/{chunk-JTSEDYVQ.js → chunk-DG4M6ZUE.js} +7 -7
  36. package/dist/{chunk-DQA7QLMD.js → chunk-EBOC7MT3.js} +10 -25
  37. package/dist/{chunk-KZ2H5X4G.js → chunk-ECUO3KDP.js} +129 -14
  38. package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
  39. package/dist/{chunk-YD734TPH.js → chunk-FALJGAWU.js} +2 -2
  40. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  41. package/dist/{chunk-XEGB6BCN.js → chunk-GAYUJ7LE.js} +68 -14
  42. package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
  43. package/dist/{chunk-IR4CFBFN.js → chunk-HAY4ZE2P.js} +12 -12
  44. package/dist/{chunk-UVDSQ6LW.js → chunk-HCBCAYZU.js} +74 -147
  45. package/dist/{chunk-4DWFMQDR.js → chunk-HJB5IUKP.js} +89 -145
  46. package/dist/{chunk-M4AKACEO.js → chunk-HKO36JWF.js} +33 -5
  47. package/dist/{chunk-KCMKRQX4.js → chunk-HPCTNZM2.js} +45 -82
  48. package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
  49. package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
  50. package/dist/chunk-JEQQR47K.js +3025 -0
  51. package/dist/{chunk-FO5ZOVUY.js → chunk-KV2AOLDF.js} +27 -7
  52. package/dist/chunk-LU7P4LHA.js +33 -0
  53. package/dist/{chunk-6TUKSZVF.js → chunk-LXPJXFM5.js} +11 -11
  54. package/dist/{chunk-VQNODYQ4.js → chunk-MIX5N5AC.js} +488 -3668
  55. package/dist/chunk-MLOK6ZOS.js +2888 -0
  56. package/dist/{chunk-ZZMN5OM4.js → chunk-MV2VUEJC.js} +2 -2
  57. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  58. package/dist/{chunk-OBMAI2DP.js → chunk-N3PBVRTZ.js} +12 -388
  59. package/dist/{chunk-WJHBC77E.js → chunk-N5XKWMDW.js} +17 -7
  60. package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
  61. package/dist/{chunk-UFQ3F4FW.js → chunk-NQ6UCCOD.js} +4 -4
  62. package/dist/chunk-NUGM5KR6.js +165 -0
  63. package/dist/{chunk-DMD2AGVS.js → chunk-NZU6YDNV.js} +20 -18
  64. package/dist/{chunk-WHGPSPT5.js → chunk-O6I4CIEU.js} +151 -13
  65. package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
  66. package/dist/{chunk-PD3MESLB.js → chunk-P3JGPQFL.js} +4 -4
  67. package/dist/{chunk-UHXRNZ2J.js → chunk-PNY46YEY.js} +23 -6
  68. package/dist/{chunk-THKY7CD7.js → chunk-PZ4I4JE2.js} +134 -29
  69. package/dist/{chunk-SROCI7ZU.js → chunk-QQ7EKM72.js} +5 -5
  70. package/dist/{chunk-QCTRSGHQ.js → chunk-R7LNVMCS.js} +91 -53
  71. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  72. package/dist/chunk-RKKLTLYB.js +45 -0
  73. package/dist/{chunk-OB5HIGJY.js → chunk-RKRLDWD3.js} +4 -1
  74. package/dist/{chunk-DJNLUABN.js → chunk-S4COXYBG.js} +588 -32
  75. package/dist/{chunk-3HAPLH5M.js → chunk-T3Z6VAAF.js} +172 -11
  76. package/dist/{chunk-FOT2FX5J.js → chunk-TD7UE2L5.js} +12 -10
  77. package/dist/{chunk-UUANF5CR.js → chunk-TEO2TLVN.js} +856 -967
  78. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  79. package/dist/{chunk-EELBMBT6.js → chunk-VKBMFOYV.js} +74 -15
  80. package/dist/chunk-VO2LKSTM.js +165 -0
  81. package/dist/{chunk-5C77SEEY.js → chunk-VPTUJU4P.js} +3 -3
  82. package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
  83. package/dist/{chunk-J7PIKKWC.js → chunk-WXCJ7VME.js} +8 -8
  84. package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
  85. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  86. package/dist/{chunk-AB4XIIVB.js → chunk-YKOFT37S.js} +6 -6
  87. package/dist/chunk-YSEHGPCT.js +127 -0
  88. package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
  89. package/dist/cli/index.js +32 -32
  90. package/dist/{clio-WBVQEBKO.js → clio-LT5V7SSZ.js} +9 -9
  91. package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +5 -5
  92. package/dist/codewiki/build-worker.js +4 -4
  93. package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
  94. package/dist/{config-TRBL3RCF.js → config-RXS5T3JT.js} +98 -65
  95. package/dist/{configure-OLCVPHNM.js → configure-2WYWSCSD.js} +26 -22
  96. package/dist/{context-MJIJ6GOX.js → context-I3BTOTCS.js} +12 -12
  97. package/dist/{context-XEWE3MOJ.js → context-MVOORGMF.js} +54 -47
  98. package/dist/{context-WFPKQSM6.js → context-PALKKQYL.js} +28 -28
  99. package/dist/{context-clear-KNOS2JPB.js → context-clear-N2WOYZ2K.js} +53 -46
  100. package/dist/{context-index-SSR5ECNE.js → context-index-HNG3MOME.js} +6 -6
  101. package/dist/{context-working-set-EUXAZI6N.js → context-working-set-MIEVECVZ.js} +17 -18
  102. package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-VVA4SRRH.js} +90 -61
  103. package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
  104. package/dist/doctor-TWBWFK5V.js +165 -0
  105. package/dist/eval-IJ5VEZDJ.js +4483 -0
  106. package/dist/{evidence-JZNBUOQZ.js → evidence-L5APPXNV.js} +68 -61
  107. package/dist/{evolve-FJVC4KKI.js → evolve-RGNKFJ52.js} +47 -40
  108. package/dist/{extensions-IQL36S7K.js → extensions-7WYWUX5A.js} +13 -7
  109. package/dist/{fleet-BDKYJFCP.js → fleet-6CNVBZZP.js} +113 -76
  110. package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-L2SXSYEI.js} +10 -10
  111. package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-2J3OOIPO.js} +17 -15
  112. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-CZRJ4JP5.js} +5 -6
  113. package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-C5RI6DP7.js} +20 -19
  114. package/dist/{init-LQUB5COQ.js → init-VBN2ACVA.js} +70 -63
  115. package/dist/{library-NJAHIGG4.js → library-JHGUMLY2.js} +22 -20
  116. package/dist/{memory-OG6HOYKM.js → memory-K4OQIYWG.js} +49 -42
  117. package/dist/{models-5ZG5XY7J.js → models-2NCZUWDD.js} +35 -29
  118. package/dist/{monitor-TJ7AMTGB.js → monitor-MMVTJABD.js} +64 -45
  119. package/dist/{orchestrator-WZYB54DM.js → orchestrator-ZKBPCHW6.js} +1971 -520
  120. package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
  121. package/dist/registry-LG64LTF4.js +11 -0
  122. package/dist/{reset-PXQT45IY.js → reset-DD5JGOY3.js} +11 -11
  123. package/dist/{run-FQ74YF62.js → run-QEGNX7FL.js} +89 -83
  124. package/dist/{share-FW7SVCL3.js → share-JKD3BQMW.js} +20 -18
  125. package/dist/{skills-7E7IRB3R.js → skills-LMQIKDOZ.js} +23 -21
  126. package/dist/{skills-eval-LI75W6OK.js → skills-eval-I7X2774U.js} +59 -52
  127. package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
  128. package/dist/support-I7LOJLIF.js +38 -0
  129. package/dist/{targets-4CIFKCTW.js → targets-RUSR6B5Z.js} +77 -42
  130. package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-QYVORFR4.js} +6 -4
  131. package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
  132. package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
  133. package/dist/{upgrade-K2HVIVMQ.js → upgrade-XANW3FXB.js} +29 -26
  134. package/dist/{usage-GTZELZQX.js → usage-4H7ZRXQT.js} +110 -61
  135. package/dist/{verifiers-RLAHT27O.js → verifiers-UZXNBZEB.js} +13 -13
  136. package/dist/{verify-BX3BRKH5.js → verify-BVKWTNDL.js} +9 -9
  137. package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-MY7WV2QI.js} +76 -69
  138. package/dist/worker/entry.js +69 -66
  139. package/docs/alcf-provider.md +1 -1
  140. package/docs/architecture.md +1 -1
  141. package/docs/artifact-versions.md +11 -5
  142. package/docs/built-in-agents.md +1 -1
  143. package/docs/capacity-and-scheduling.md +23 -2
  144. package/docs/commands-and-modes.md +2 -2
  145. package/docs/configuration-and-targets.md +37 -5
  146. package/docs/context-engine.md +63 -4
  147. package/docs/documentation-coverage.md +3 -3
  148. package/docs/documentation-guide.md +1 -1
  149. package/docs/environment-variables.md +2 -0
  150. package/docs/eval-runner.md +262 -11
  151. package/docs/evals-internal.md +72 -2
  152. package/docs/evidence-and-memory.md +77 -12
  153. package/docs/evolution.md +1 -1
  154. package/docs/extensions-and-sharing.md +3 -1
  155. package/docs/fleet-dispatch.md +34 -9
  156. package/docs/glossary.md +21 -1
  157. package/docs/installation-and-lifecycle.md +1 -1
  158. package/docs/middleware-and-components.md +1 -1
  159. package/docs/model-catalog.md +1 -1
  160. package/docs/observability.md +54 -3
  161. package/docs/proactive-memory.md +127 -14
  162. package/docs/prompt-envelope-and-tools.md +22 -2
  163. package/docs/provider-adapter-cookbook.md +1 -1
  164. package/docs/release-cut-checklist.md +60 -41
  165. package/docs/safety-model.md +1 -1
  166. package/docs/scientific-validation.md +1 -1
  167. package/docs/skills-marketplace.md +1 -1
  168. package/docs/tool-usage.md +1 -1
  169. package/docs/trace-store.md +1 -1
  170. package/docs/troubleshooting.md +87 -0
  171. package/docs/tui-design.md +1 -1
  172. package/docs/worker-dispatch-mechanics.md +1 -1
  173. package/package.json +2 -2
  174. package/src/cli/agents.ts +1 -1
  175. package/src/cli/argv.ts +5 -0
  176. package/src/cli/config-inspect.ts +33 -6
  177. package/src/cli/config.ts +1 -1
  178. package/src/cli/configure.ts +107 -23
  179. package/src/cli/doctor-state-size.ts +82 -0
  180. package/src/cli/doctor.ts +7 -1
  181. package/src/cli/eval.ts +80 -16
  182. package/src/cli/evidence.ts +30 -25
  183. package/src/cli/extensions.ts +5 -1
  184. package/src/cli/fleet-preflight.ts +2 -12
  185. package/src/cli/fleet.ts +32 -3
  186. package/src/cli/shared.ts +1 -0
  187. package/src/cli/targets.ts +45 -11
  188. package/src/cli/trace.ts +63 -4
  189. package/src/cli/usage.ts +63 -14
  190. package/src/cli/validate-model.ts +60 -5
  191. package/src/core/bus-events.ts +54 -1
  192. package/src/core/cache-telemetry.ts +42 -0
  193. package/src/core/commit-attribution.ts +4 -4
  194. package/src/core/config.ts +18 -0
  195. package/src/core/defaults.ts +36 -6
  196. package/src/core/endpoint-key.ts +27 -0
  197. package/src/core/path-boundary.ts +100 -0
  198. package/src/core/residency-target-key.ts +25 -0
  199. package/src/core/response-schema.ts +36 -2
  200. package/src/domains/agents/extension.ts +2 -11
  201. package/src/domains/agents/fleet-contract.ts +30 -12
  202. package/src/domains/agents/recipe.ts +7 -1
  203. package/src/domains/agents/registry.ts +73 -5
  204. package/src/domains/agents/result-contract.ts +128 -17
  205. package/src/domains/agents/write-boundary.ts +15 -50
  206. package/src/domains/config/classify.ts +3 -0
  207. package/src/domains/context/codewiki/coordinator.ts +12 -4
  208. package/src/domains/context/project-rules.ts +51 -1
  209. package/src/domains/dispatch/admission.ts +40 -3
  210. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  211. package/src/domains/dispatch/assignment-store.ts +151 -14
  212. package/src/domains/dispatch/capacity-lease.ts +98 -9
  213. package/src/domains/dispatch/contract.ts +26 -1
  214. package/src/domains/dispatch/delegation-plan.ts +2 -5
  215. package/src/domains/dispatch/execution-plan.ts +44 -4
  216. package/src/domains/dispatch/execution-role.ts +9 -1
  217. package/src/domains/dispatch/extension.ts +309 -96
  218. package/src/domains/dispatch/fleet-run.ts +78 -4
  219. package/src/domains/dispatch/gate-role-prompts.ts +38 -0
  220. package/src/domains/dispatch/heartbeat.ts +32 -8
  221. package/src/domains/dispatch/index.ts +6 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +84 -8
  224. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  225. package/src/domains/dispatch/path-scope.ts +370 -0
  226. package/src/domains/dispatch/receipt-integrity.ts +2 -1
  227. package/src/domains/dispatch/reservation-store.ts +116 -8
  228. package/src/domains/dispatch/state.ts +4 -0
  229. package/src/domains/dispatch/types.ts +14 -7
  230. package/src/domains/dispatch/validation.ts +6 -3
  231. package/src/domains/dispatch/worker-spawn.ts +25 -11
  232. package/src/domains/dispatch/write-boundary-enforcer.ts +62 -0
  233. package/src/domains/dispatch/write-boundary.ts +262 -22
  234. package/src/domains/eval/artifacts/store.ts +62 -0
  235. package/src/domains/eval/compare/behavioral.ts +224 -0
  236. package/src/domains/eval/compare/compare.ts +355 -2
  237. package/src/domains/eval/compare/envelope.ts +128 -0
  238. package/src/domains/eval/compare/gates.ts +24 -6
  239. package/src/domains/eval/compare/thresholds.ts +30 -3
  240. package/src/domains/eval/execution-provenance.ts +240 -0
  241. package/src/domains/eval/metrics/aggregate.ts +136 -0
  242. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  243. package/src/domains/eval/metrics/evidence.ts +79 -2
  244. package/src/domains/eval/metrics/tracked.ts +413 -0
  245. package/src/domains/eval/provenance.ts +117 -0
  246. package/src/domains/eval/reports/comparison.ts +128 -0
  247. package/src/domains/eval/reports/junit.ts +17 -3
  248. package/src/domains/eval/reports/markdown.ts +3 -3
  249. package/src/domains/eval/reports/text.ts +14 -0
  250. package/src/domains/eval/run-compare.ts +20 -0
  251. package/src/domains/eval/runners/clio-run.ts +139 -2
  252. package/src/domains/eval/runners/external-command.ts +28 -3
  253. package/src/domains/eval/schema/adapter.ts +111 -0
  254. package/src/domains/eval/schema/artifact.ts +20 -0
  255. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  256. package/src/domains/eval/schema/behavioral.ts +520 -0
  257. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  258. package/src/domains/eval/schema/serving.ts +74 -0
  259. package/src/domains/eval/schema/suite.ts +38 -8
  260. package/src/domains/eval/schema/validate.ts +58 -3
  261. package/src/domains/eval/schema/verdict.ts +237 -0
  262. package/src/domains/eval/suites/resolve.ts +2 -0
  263. package/src/domains/eval/suites/run.ts +264 -33
  264. package/src/domains/eval/verifiers/command.ts +2 -1
  265. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  266. package/src/domains/evidence/build.ts +68 -21
  267. package/src/domains/evidence/eval.ts +2 -12
  268. package/src/domains/evidence/findings-markdown.ts +33 -0
  269. package/src/domains/evidence/index.ts +21 -0
  270. package/src/domains/evidence/provenance.ts +46 -11
  271. package/src/domains/evidence/run-trust.ts +7 -113
  272. package/src/domains/evidence/trust-projection.ts +274 -0
  273. package/src/domains/evidence/trust-status.ts +145 -17
  274. package/src/domains/evidence/types.ts +4 -0
  275. package/src/domains/extensions/compatibility.ts +285 -0
  276. package/src/domains/extensions/discovery.ts +126 -4
  277. package/src/domains/extensions/resources.ts +21 -9
  278. package/src/domains/extensions/state.ts +18 -5
  279. package/src/domains/extensions/types.ts +6 -1
  280. package/src/domains/lifecycle/doctor.ts +209 -2
  281. package/src/domains/memory/index.ts +14 -0
  282. package/src/domains/memory/task-bank-promotion.ts +64 -0
  283. package/src/domains/memory/task-memory-policy.ts +77 -8
  284. package/src/domains/memory/task-memory-spend.ts +131 -0
  285. package/src/domains/memory/task-memory-status.ts +7 -0
  286. package/src/domains/memory/task-memory-telemetry.ts +2 -0
  287. package/src/domains/middleware/index.ts +1 -0
  288. package/src/domains/middleware/memory-intervention.ts +69 -5
  289. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  290. package/src/domains/observability/background-memory-usage.ts +140 -0
  291. package/src/domains/observability/cost.ts +1 -1
  292. package/src/domains/observability/index.ts +7 -0
  293. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  294. package/src/domains/observability/trace-store.ts +192 -2
  295. package/src/domains/prompts/compiler.ts +100 -13
  296. package/src/domains/prompts/contract.ts +3 -5
  297. package/src/domains/providers/endpoint-capacity.ts +96 -0
  298. package/src/domains/providers/extension.ts +30 -2
  299. package/src/domains/providers/index.ts +10 -0
  300. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  301. package/src/domains/providers/runtime-resolution.ts +8 -1
  302. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  303. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  304. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  305. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  306. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  307. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  308. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  309. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  310. package/src/domains/providers/types/capability-flags.ts +2 -0
  311. package/src/domains/providers/types/target-descriptor.ts +2 -0
  312. package/src/domains/resources/common-loader.ts +3 -0
  313. package/src/domains/resources/prompts/loader.ts +184 -17
  314. package/src/domains/safety/call-target.ts +52 -0
  315. package/src/domains/safety/policy-engine.ts +5 -5
  316. package/src/domains/safety/run-effects.ts +96 -2
  317. package/src/domains/safety/scope.ts +7 -12
  318. package/src/domains/session/context-accounting.ts +52 -1
  319. package/src/domains/session/context-ledger.ts +37 -13
  320. package/src/domains/session/index.ts +6 -0
  321. package/src/domains/session/prompt-cache.ts +140 -0
  322. package/src/domains/session/prompt-manifest.ts +42 -0
  323. package/src/engine/acp/adapter.ts +18 -3
  324. package/src/engine/acp/server.ts +4 -1
  325. package/src/engine/ai.ts +35 -0
  326. package/src/engine/apis/llamacpp-residency.ts +55 -3
  327. package/src/engine/apis/lmstudio.ts +25 -5
  328. package/src/engine/apis/ollama-native.ts +2 -1
  329. package/src/engine/apis/openai-completions.ts +80 -17
  330. package/src/engine/apis/residency-lock.ts +3 -1
  331. package/src/engine/apis/residency.ts +34 -1
  332. package/src/engine/prompt-templates.ts +18 -1
  333. package/src/engine/provider-payload.ts +29 -1
  334. package/src/engine/worker-runtime.ts +6 -3
  335. package/src/entry/orchestrator.ts +176 -30
  336. package/src/interactive/chat-loop-messages.ts +26 -7
  337. package/src/interactive/chat-loop.ts +318 -41
  338. package/src/interactive/chat-panel.ts +62 -8
  339. package/src/interactive/clio-editor.ts +45 -8
  340. package/src/interactive/context-activity.ts +5 -1
  341. package/src/interactive/context-meter.ts +1 -1
  342. package/src/interactive/context-overlay.ts +40 -10
  343. package/src/interactive/cost-overlay.ts +64 -6
  344. package/src/interactive/dispatch-board.ts +127 -5
  345. package/src/interactive/fleet-run-preview.ts +41 -15
  346. package/src/interactive/handoff-round.ts +41 -2
  347. package/src/interactive/interactive-application.ts +24 -1
  348. package/src/interactive/interactive-event-projection.ts +14 -0
  349. package/src/interactive/interactive-input-runtime.ts +8 -0
  350. package/src/interactive/interactive-presentation.ts +4 -0
  351. package/src/interactive/interactive-shell.ts +20 -17
  352. package/src/interactive/interactive-slash-runtime.ts +27 -4
  353. package/src/interactive/memory-overlay.ts +8 -0
  354. package/src/interactive/mutation-preview.ts +295 -0
  355. package/src/interactive/overlay-general-openers.ts +16 -0
  356. package/src/interactive/overlay-key-routing.ts +38 -0
  357. package/src/interactive/overlay-lifecycle.ts +38 -5
  358. package/src/interactive/overlay-permission-lifecycle.ts +22 -2
  359. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  360. package/src/interactive/overlays/ask-user.ts +91 -19
  361. package/src/interactive/overlays/help-reference.ts +4 -0
  362. package/src/interactive/overlays/prompts.ts +11 -1
  363. package/src/interactive/overlays/settings.ts +176 -48
  364. package/src/interactive/permission-hint.ts +34 -2
  365. package/src/interactive/permission-overlay.ts +159 -9
  366. package/src/interactive/prewarm.ts +197 -0
  367. package/src/interactive/render-trace.ts +162 -15
  368. package/src/interactive/renderers/tool-execution.ts +4 -0
  369. package/src/interactive/side-question.ts +58 -1
  370. package/src/interactive/slash-commands.ts +7 -2
  371. package/src/interactive/status/controller.ts +11 -0
  372. package/src/interactive/status/state-machine.ts +54 -2
  373. package/src/interactive/status/types.ts +7 -0
  374. package/src/interactive/terminal-lease.ts +2 -0
  375. package/src/interactive/turn-context.ts +299 -31
  376. package/src/interactive/turn-persistence.ts +14 -4
  377. package/src/interactive/turn-prewarm.ts +364 -0
  378. package/src/interactive/turn-queues.ts +7 -4
  379. package/src/interactive/turn-runtime.ts +8 -1
  380. package/src/interactive/turn-state.ts +23 -0
  381. package/src/interactive/view/artifacts.ts +42 -9
  382. package/src/interactive/view/view-overlay.ts +43 -6
  383. package/src/interactive/worker-receipts.ts +14 -2
  384. package/src/interactive/worker-stream.ts +8 -0
  385. package/src/tools/ask-user.ts +43 -2
  386. package/src/tools/dispatch-admission.ts +12 -13
  387. package/src/tools/dispatch-arguments.ts +27 -0
  388. package/src/tools/dispatch-plan.ts +46 -9
  389. package/src/tools/dispatch-runner.ts +48 -13
  390. package/src/tools/dispatch-scout.ts +1 -1
  391. package/src/tools/monitor.ts +13 -0
  392. package/src/tools/registry.ts +16 -0
  393. package/src/tools/worker-evidence.ts +19 -13
  394. package/src/worker/spec-contract.ts +2 -1
  395. package/dist/chunk-AOCYTWAV.js +0 -449
  396. package/dist/chunk-HWUFFB6L.js +0 -83
  397. package/dist/chunk-R346GLFC.js +0 -31
  398. package/dist/chunk-ZGH7FGS5.js +0 -1079
  399. package/dist/doctor-RN4YKO2X.js +0 -87
  400. package/dist/eval-RUBJVSNQ.js +0 -2557
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { endpointLabel } from "../providers/endpoint-capacity.js";
2
3
  import type { ActionClass } from "../safety/action-classifier.js";
3
4
  import type { ScopeSpec } from "../safety/scope.js";
4
5
  import { createAdmissionQueue } from "./admission-queue.js";
@@ -7,6 +8,8 @@ import {
7
8
  type CapacityLease,
8
9
  type CapacityLimits,
9
10
  capacityLeaseUsage,
11
+ describeEndpointCapacityHolders,
12
+ type EndpointCapacityHolders,
10
13
  heartbeatCapacityLease,
11
14
  releaseCapacityLease,
12
15
  renameCapacityLeaseAssignment,
@@ -41,6 +44,7 @@ export interface CapacityAdmissionController {
41
44
  admit(input: {
42
45
  assignmentId: string;
43
46
  nodeId: string;
47
+ endpointKey?: string;
44
48
  deadlineAt: number;
45
49
  priority?: number;
46
50
  planId?: string;
@@ -105,13 +109,31 @@ const PUMP_MAX_MS = 500;
105
109
  function describeAdmissionFailure(input: {
106
110
  state: "canceled" | "timed_out";
107
111
  nodeId: string;
112
+ endpointKey?: string;
108
113
  waitedMs: number;
109
114
  overdueAtQueueMs: number;
110
115
  queueDepth: number;
111
116
  limits: CapacityLimits;
112
- usage: { global: number; nodes: Readonly<Record<string, number>> };
117
+ usage: {
118
+ global: number;
119
+ nodes: Readonly<Record<string, number>>;
120
+ endpoints: Readonly<Record<string, number>>;
121
+ endpointHolders?: Readonly<Record<string, EndpointCapacityHolders>>;
122
+ };
113
123
  }): string {
114
124
  if (input.state === "canceled") return "dispatch: admission canceled before a capacity slot opened";
125
+ if (input.endpointKey !== undefined) {
126
+ const endpointLimit = input.limits.endpoints[input.endpointKey];
127
+ const endpointActive = input.usage.endpoints[input.endpointKey] ?? 0;
128
+ if (endpointLimit !== undefined && endpointActive >= endpointLimit) {
129
+ const holders = input.usage.endpointHolders?.[input.endpointKey];
130
+ const occupancy =
131
+ holders === undefined
132
+ ? "the supplied capacity snapshot does not attribute the occupied slots"
133
+ : describeEndpointCapacityHolders(holders);
134
+ return `dispatch: admission denied: endpoint '${endpointLabel(input.endpointKey)}' capacity reached (${endpointActive}/${endpointLimit} slots): ${occupancy}; collect in-flight runs or point workers at a second server`;
135
+ }
136
+ }
115
137
  const nodeLimit = input.limits.nodes[input.nodeId];
116
138
  const nodeActive = input.usage.nodes[input.nodeId] ?? 0;
117
139
  const where =
@@ -142,7 +164,12 @@ function describeAdmissionFailure(input: {
142
164
  export function createCapacityAdmissionController(options: {
143
165
  limits: () => CapacityLimits;
144
166
  /** Live lease counts for the timeout message; defaults to the durable state file. */
145
- usage?: () => { global: number; nodes: Readonly<Record<string, number>> };
167
+ usage?: () => {
168
+ global: number;
169
+ nodes: Readonly<Record<string, number>>;
170
+ endpoints: Readonly<Record<string, number>>;
171
+ endpointHolders?: Readonly<Record<string, EndpointCapacityHolders>>;
172
+ };
146
173
  now?: () => number;
147
174
  maxQueueSize?: number;
148
175
  queueCeilingMs?: number;
@@ -175,7 +202,14 @@ export function createCapacityAdmissionController(options: {
175
202
  admittedLease = request.value();
176
203
  return true;
177
204
  } catch (error) {
178
- // Capacity is a wait, not a failure. Anything else fails the request.
205
+ // Endpoint saturation is a named refusal because waiting would keep a
206
+ // model request queued behind the same inference scheduler. Existing
207
+ // node and global admission retain their bounded wait behavior.
208
+ if (error instanceof Error && /endpoint '.*' capacity reached/u.test(error.message)) {
209
+ queue.fail(request.requestId, error);
210
+ return false;
211
+ }
212
+ // Node and global capacity can wait. Anything else fails the request.
179
213
  if (error instanceof Error && /capacity reached/.test(error.message)) return false;
180
214
  queue.fail(request.requestId, error instanceof Error ? error : new Error(String(error)));
181
215
  return false;
@@ -229,12 +263,14 @@ export function createCapacityAdmissionController(options: {
229
263
  memberId: input.reservation.memberId,
230
264
  assignmentId: input.assignmentId,
231
265
  nodeId: input.nodeId,
266
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
232
267
  limits: options.limits(),
233
268
  nowMs: now(),
234
269
  })
235
270
  : acquireCapacityLease({
236
271
  assignmentId: input.assignmentId,
237
272
  nodeId: input.nodeId,
273
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
238
274
  limits: options.limits(),
239
275
  nowMs: now(),
240
276
  }),
@@ -246,6 +282,7 @@ export function createCapacityAdmissionController(options: {
246
282
  describeAdmissionFailure({
247
283
  state: outcome.state,
248
284
  nodeId: input.nodeId,
285
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
249
286
  waitedMs: now() - queuedAt,
250
287
  overdueAtQueueMs: queuedAt - input.deadlineAt,
251
288
  queueDepth: queue.size(),
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Startup reconciliation for durable assignments. The in-memory retry queue and
3
- * assignment registry do not survive a restart (Symphony §14.3), so every
4
- * durable record still marked `running` after a crash is orphaned: no active
5
- * attempt, no queued retry, and no attached terminal promise can ever settle it.
3
+ * assignment registry do not survive a restart (Symphony §14.3), so a durable
4
+ * record whose process owner is gone is orphaned: no active attempt, no queued
5
+ * retry, and no attached terminal promise can ever settle it. Running records
6
+ * owned by a live sibling process are deliberately left alone.
6
7
  *
7
8
  * This pass resolves each orphan deterministically against durable ledger
8
9
  * evidence so a later `wait`/`collect` can never observe a stuck `running`
@@ -11,7 +12,7 @@
11
12
  */
12
13
 
13
14
  import type { AssignmentStatus } from "./assignment.js";
14
- import type { DurableAssignmentRecord } from "./assignment-store.js";
15
+ import type { AssignmentVerdictOwner, DurableAssignmentRecord } from "./assignment-store.js";
15
16
 
16
17
  /** Minimal terminal view of one attempt's durable ledger row. */
17
18
  export interface ReconcileAttemptView {
@@ -25,13 +26,16 @@ export interface ReconcileAttemptView {
25
26
  export interface AssignmentReconcileDeps {
26
27
  /** Durable records currently marked `running`. */
27
28
  listRunning: () => ReadonlyArray<DurableAssignmentRecord>;
29
+ /** Decide liveness from the record's durable pid and process birth token. */
30
+ ownerAlive: (record: DurableAssignmentRecord) => boolean;
28
31
  /** Resolve one attempt's terminal view from the ledger, or null if pruned. */
29
32
  lookupAttempt: (runId: string) => ReconcileAttemptView | null;
30
- /** Persist the reconciled terminal status. */
33
+ /** Persist the reconciled terminal status, as the record's verdict owner when it has one. */
31
34
  settle: (
32
35
  assignmentId: string,
33
36
  terminalRunId: string,
34
37
  status: Exclude<AssignmentStatus, "running">,
38
+ owner?: AssignmentVerdictOwner,
35
39
  ) => Promise<unknown>;
36
40
  }
37
41
 
@@ -44,6 +48,10 @@ export interface AssignmentReconcileSummary {
44
48
 
45
49
  /**
46
50
  * Deterministic rule, in priority order:
51
+ * -1. a record owned by a live process is not an orphan and remains running;
52
+ * 0. a record whose verdict belongs to its opener is failed (abandoned): the
53
+ * opener died before reaching a verdict, and a green attempt under it is
54
+ * one step of a run, never the run's answer;
47
55
  * 1. a recovered succeeded terminal attempt wins (`succeeded`);
48
56
  * 2. otherwise the last terminal attempt marks the assignment `failed`;
49
57
  * 3. otherwise no attempt is recoverable and the assignment is failed
@@ -53,6 +61,15 @@ export async function reconcileOrphanAssignments(deps: AssignmentReconcileDeps):
53
61
  const summary: AssignmentReconcileSummary = { recovered: 0, abandoned: 0 };
54
62
  for (const record of deps.listRunning()) {
55
63
  if (record.status !== "running") continue;
64
+ if (deps.ownerAlive(record)) continue;
65
+ if (record.verdictOwner !== undefined) {
66
+ // Attempts under a claimed row are fleet steps, not the fleet verdict.
67
+ // Filing a green step as the terminal id of an abandoned fleet would
68
+ // contradict that step's immutable ledger result.
69
+ await deps.settle(record.assignmentId, record.assignmentId, "failed", record.verdictOwner);
70
+ summary.abandoned += 1;
71
+ continue;
72
+ }
56
73
  const views = record.attempts
57
74
  .map((runId) => deps.lookupAttempt(runId))
58
75
  .filter((view): view is ReconcileAttemptView => view !== null);
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
8
9
  import { withStateFileLock } from "../../core/state-file-lock.js";
9
10
  import { clioStateDir } from "../../core/xdg.js";
10
11
  import { atomicWrite } from "../../engine/session.js";
@@ -12,11 +13,54 @@ import type { AssignmentStatus } from "./assignment.js";
12
13
 
13
14
  const MAX_ASSIGNMENT_RECORDS = 1_000;
14
15
 
16
+ /**
17
+ * Who decides a record's status.
18
+ *
19
+ * Absent is the ordinary case: the record is one logical dispatch, so the
20
+ * attempts filed under it are the only thing that can settle it. A fleet claims
21
+ * the verdict instead, because a fleet root gathers every step of a run under
22
+ * one id and no single step is the run's answer. Without the claim the last
23
+ * step to settle writes the row, which reports a green step of an aborted run
24
+ * as the run's outcome.
25
+ */
26
+ export type AssignmentVerdictOwner = "fleet";
27
+
28
+ /**
29
+ * Durable identity of the process executing a running assignment. A pid alone
30
+ * is not enough: the OS may recycle it after a crash, so the process birth
31
+ * token must still match before another extension treats the owner as live.
32
+ */
33
+ export interface AssignmentProcessOwner {
34
+ pid: number;
35
+ processBirthToken: string;
36
+ acquiredAt: string;
37
+ }
38
+
39
+ export interface AssignmentProcessOwnerProbe {
40
+ birthToken(pid: number): string | null;
41
+ /** True when a missing birth token proves that the process is gone. */
42
+ tokenProvesDeath?: boolean;
43
+ /** Liveness fallback for platforms whose birth token is synthetic. */
44
+ alive?(pid: number): boolean;
45
+ }
46
+
47
+ /**
48
+ * One durable logical-dispatch row.
49
+ *
50
+ * `attempts` holds terminal run ids: an agent attempt's receipt id, and for a
51
+ * fleet also each deterministic step's `code-*` run id, whose evidence lives
52
+ * under `code-steps/<assignmentId>/`. `status` answers for the whole record,
53
+ * so for a fleet it answers for the whole run rather than for its last step.
54
+ */
15
55
  export interface DurableAssignmentRecord {
16
56
  assignmentId: string;
17
57
  attempts: string[];
18
58
  terminalRunId: string | null;
19
59
  status: AssignmentStatus;
60
+ /** Set when something other than the attempts decides `status`. */
61
+ verdictOwner?: AssignmentVerdictOwner;
62
+ /** Process lease held only while this record is running. */
63
+ processOwner?: AssignmentProcessOwner;
20
64
  }
21
65
 
22
66
  interface AssignmentStoreFile {
@@ -28,6 +72,18 @@ function storePath(): string {
28
72
  return join(clioStateDir(), "assignments.json");
29
73
  }
30
74
 
75
+ function validProcessOwner(value: unknown): value is AssignmentProcessOwner {
76
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
77
+ const owner = value as Partial<AssignmentProcessOwner>;
78
+ return (
79
+ Number.isSafeInteger(owner.pid) &&
80
+ (owner.pid ?? 0) > 0 &&
81
+ typeof owner.processBirthToken === "string" &&
82
+ typeof owner.acquiredAt === "string" &&
83
+ Number.isFinite(Date.parse(owner.acquiredAt))
84
+ );
85
+ }
86
+
31
87
  function validRecord(value: unknown): value is DurableAssignmentRecord {
32
88
  if (typeof value !== "object" || value === null) return false;
33
89
  const record = value as Partial<DurableAssignmentRecord>;
@@ -36,6 +92,8 @@ function validRecord(value: unknown): value is DurableAssignmentRecord {
36
92
  Array.isArray(record.attempts) &&
37
93
  record.attempts.every((runId) => typeof runId === "string") &&
38
94
  (record.terminalRunId === null || typeof record.terminalRunId === "string") &&
95
+ (record.verdictOwner === undefined || record.verdictOwner === "fleet") &&
96
+ (record.processOwner === undefined || validProcessOwner(record.processOwner)) &&
39
97
  (record.status === "running" ||
40
98
  record.status === "succeeded" ||
41
99
  record.status === "failed" ||
@@ -57,7 +115,32 @@ function readStore(): DurableAssignmentRecord[] {
57
115
  }
58
116
 
59
117
  function copyRecord(record: DurableAssignmentRecord): DurableAssignmentRecord {
60
- return { ...record, attempts: [...record.attempts] };
118
+ return {
119
+ ...record,
120
+ attempts: [...record.attempts],
121
+ ...(record.processOwner === undefined ? {} : { processOwner: { ...record.processOwner } }),
122
+ };
123
+ }
124
+
125
+ const defaultProcessOwnerProbe: AssignmentProcessOwnerProbe = {
126
+ birthToken: processBirthToken,
127
+ tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
128
+ alive: processAlive,
129
+ };
130
+
131
+ /** True only when the durable identity still names the same live process. */
132
+ export function assignmentProcessOwnerAlive(
133
+ record: Pick<DurableAssignmentRecord, "processOwner">,
134
+ probe: AssignmentProcessOwnerProbe = defaultProcessOwnerProbe,
135
+ ): boolean {
136
+ const owner = record.processOwner;
137
+ if (owner === undefined) return false;
138
+ const current = probe.birthToken(owner.pid);
139
+ if (current === null || current !== owner.processBirthToken) return false;
140
+ // A synthetic `pid-N` token cannot distinguish pid reuse, so require an
141
+ // independent liveness signal on platforms without a real birth-token source.
142
+ if (probe.tokenProvesDeath === false) return probe.alive?.(owner.pid) ?? false;
143
+ return true;
61
144
  }
62
145
 
63
146
  function writeStore(assignments: ReadonlyArray<DurableAssignmentRecord>): void {
@@ -114,49 +197,103 @@ export async function renameStoredAssignment(
114
197
  return copyRecord(result);
115
198
  }
116
199
 
117
- export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
118
- return updateRecord(
200
+ function currentProcessOwner(): AssignmentProcessOwner {
201
+ const token = processBirthToken();
202
+ if (token === null) throw new Error("dispatch: cannot establish assignment owner process birth token");
203
+ return {
204
+ pid: process.pid,
205
+ processBirthToken: token,
206
+ acquiredAt: new Date().toISOString(),
207
+ };
208
+ }
209
+
210
+ function openRecord(assignmentId: string): DurableAssignmentRecord {
211
+ return {
119
212
  assignmentId,
120
- (current) => current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" },
121
- );
213
+ attempts: [],
214
+ terminalRunId: null,
215
+ status: "running",
216
+ processOwner: currentProcessOwner(),
217
+ };
218
+ }
219
+
220
+ function ensureRunningProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
221
+ if (record.status !== "running" || record.processOwner !== undefined) return record;
222
+ return { ...record, processOwner: currentProcessOwner() };
223
+ }
224
+
225
+ function clearProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
226
+ const result = { ...record };
227
+ delete result.processOwner;
228
+ return result;
229
+ }
230
+
231
+ export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
232
+ return updateRecord(assignmentId, (current) => ensureRunningProcessOwner(current ?? openRecord(assignmentId)));
233
+ }
234
+
235
+ /**
236
+ * Reserve this record's status for `owner`, opening the record if it is new.
237
+ * Attempts filed by anyone are still recorded; only the verdict is reserved,
238
+ * and until the owner settles it the record stays `running`.
239
+ */
240
+ export function claimAssignmentVerdict(
241
+ assignmentId: string,
242
+ owner: AssignmentVerdictOwner,
243
+ ): Promise<DurableAssignmentRecord> {
244
+ return updateRecord(assignmentId, (current) => ({
245
+ ...ensureRunningProcessOwner(current ?? openRecord(assignmentId)),
246
+ verdictOwner: owner,
247
+ }));
122
248
  }
123
249
 
124
250
  export function recordAssignmentAttempt(assignmentId: string, runId: string): Promise<DurableAssignmentRecord> {
125
251
  return updateRecord(assignmentId, (current) => {
126
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
252
+ const base = ensureRunningProcessOwner(current ?? openRecord(assignmentId));
127
253
  return base.attempts.includes(runId) ? base : { ...base, attempts: [...base.attempts, runId] };
128
254
  });
129
255
  }
130
256
 
131
257
  export function failQueuedAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
132
258
  return updateRecord(assignmentId, (current) => {
133
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
134
- return base.status === "running" ? { ...base, status: "failed" } : base;
259
+ const base = current ?? openRecord(assignmentId);
260
+ if (base.verdictOwner !== undefined) return base;
261
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "failed" }) : base;
135
262
  });
136
263
  }
137
264
 
138
265
  export function timeoutStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
139
266
  return updateRecord(assignmentId, (current) => {
140
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
141
- return base.status === "running" ? { ...base, status: "timed_out" } : base;
267
+ const base = current ?? openRecord(assignmentId);
268
+ if (base.verdictOwner !== undefined) return base;
269
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "timed_out" }) : base;
142
270
  });
143
271
  }
144
272
 
145
273
  export function cancelStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
146
274
  return updateRecord(assignmentId, (current) => {
147
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
148
- return base.status === "running" ? { ...base, status: "canceled" } : base;
275
+ const base = current ?? openRecord(assignmentId);
276
+ if (base.verdictOwner !== undefined) return base;
277
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "canceled" }) : base;
149
278
  });
150
279
  }
151
280
 
281
+ /**
282
+ * File `terminalRunId` as an attempt and, when the caller owns the verdict, set
283
+ * the record's status. An attempt settling under a claimed record settles
284
+ * itself and not the record: one fleet step reaching a green receipt says
285
+ * nothing about the six steps behind it that never ran.
286
+ */
152
287
  export function settleStoredAssignment(
153
288
  assignmentId: string,
154
289
  terminalRunId: string,
155
290
  status: Exclude<AssignmentStatus, "running">,
291
+ owner?: AssignmentVerdictOwner,
156
292
  ): Promise<DurableAssignmentRecord> {
157
293
  return updateRecord(assignmentId, (current) => {
158
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
294
+ const base = current ?? openRecord(assignmentId);
159
295
  const attempts = base.attempts.includes(terminalRunId) ? base.attempts : [...base.attempts, terminalRunId];
160
- return { ...base, attempts, terminalRunId, status };
296
+ if (base.verdictOwner !== owner) return { ...base, attempts };
297
+ return clearProcessOwner({ ...base, attempts, terminalRunId, status });
161
298
  });
162
299
  }
@@ -6,6 +6,7 @@ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from ".
6
6
  import { FILE_LOCK_ACQUIRE_TIMEOUT_MS, withStateFileLockSync } from "../../core/state-file-lock.js";
7
7
  import { clioStateDir } from "../../core/xdg.js";
8
8
  import { atomicWrite } from "../../engine/session.js";
9
+ import { endpointLabel, foregroundStreamUsage } from "../providers/endpoint-capacity.js";
9
10
 
10
11
  export { processBirthToken } from "../../core/process-identity.js";
11
12
 
@@ -30,6 +31,8 @@ export interface CapacityLease {
30
31
  leaseId: string;
31
32
  assignmentId: string;
32
33
  nodeId: string;
34
+ /** Canonical inference endpoint. Absent only on leases written before endpoint capacity existed. */
35
+ endpointKey?: string;
33
36
  /** Owner host. Absent only on records written before the field existed; those are read as this host. */
34
37
  host?: string;
35
38
  ownerPid: number;
@@ -43,6 +46,7 @@ export interface CapacityLease {
43
46
  export interface CapacityLimits {
44
47
  global: number;
45
48
  nodes: Readonly<Record<string, number>>;
49
+ endpoints: Readonly<Record<string, number>>;
46
50
  }
47
51
  export interface CapacityDrain {
48
52
  requestedByPid: number;
@@ -81,6 +85,7 @@ function valid(value: unknown): value is CapacityLease {
81
85
  typeof lease.leaseId === "string" &&
82
86
  typeof lease.assignmentId === "string" &&
83
87
  typeof lease.nodeId === "string" &&
88
+ (lease.endpointKey === undefined || typeof lease.endpointKey === "string") &&
84
89
  (lease.host === undefined || typeof lease.host === "string") &&
85
90
  Number.isInteger(lease.ownerPid) &&
86
91
  typeof lease.processBirthToken === "string" &&
@@ -182,9 +187,14 @@ const MEMBER_STATUSES = new Set(["held", "consumed", "released"]);
182
187
  * rejected here as well: silently skipping an unreadable record would under-count
183
188
  * capacity and admit past the cap.
184
189
  */
185
- function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number; nodes: Record<string, number> } {
190
+ function heldReservationUsage(values: ReadonlyArray<unknown>): {
191
+ global: number;
192
+ nodes: Record<string, number>;
193
+ endpoints: Record<string, number>;
194
+ } {
186
195
  let global = 0;
187
196
  const nodes: Record<string, number> = {};
197
+ const endpoints: Record<string, number> = {};
188
198
  for (const value of values) {
189
199
  if (typeof value !== "object" || value === null)
190
200
  throw new Error("dispatch capacity store has invalid reservation records");
@@ -194,10 +204,11 @@ function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number;
194
204
  if (record.status !== "active") continue;
195
205
  const byWave = new Map<number, number>();
196
206
  const byNodeWave = new Map<string, Map<number, number>>();
207
+ const byEndpointWave = new Map<string, Map<number, number>>();
197
208
  for (const raw of record.members) {
198
209
  if (typeof raw !== "object" || raw === null)
199
210
  throw new Error("dispatch capacity store has invalid reservation records");
200
- const member = raw as { status?: unknown; wave?: unknown; nodeId?: unknown };
211
+ const member = raw as { status?: unknown; wave?: unknown; nodeId?: unknown; endpointKey?: unknown };
201
212
  if (
202
213
  typeof member.status !== "string" ||
203
214
  !MEMBER_STATUSES.has(member.status) ||
@@ -211,13 +222,53 @@ function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number;
211
222
  const nodeWaves = byNodeWave.get(member.nodeId) ?? new Map<number, number>();
212
223
  nodeWaves.set(wave, (nodeWaves.get(wave) ?? 0) + 1);
213
224
  byNodeWave.set(member.nodeId, nodeWaves);
225
+ if (member.endpointKey !== undefined && typeof member.endpointKey !== "string")
226
+ throw new Error("dispatch capacity store has invalid reservation records");
227
+ if (typeof member.endpointKey === "string") {
228
+ const endpointWaves = byEndpointWave.get(member.endpointKey) ?? new Map<number, number>();
229
+ endpointWaves.set(wave, (endpointWaves.get(wave) ?? 0) + 1);
230
+ byEndpointWave.set(member.endpointKey, endpointWaves);
231
+ }
214
232
  }
215
233
  global += Math.max(0, ...byWave.values());
216
234
  for (const [id, waves] of byNodeWave) nodes[id] = (nodes[id] ?? 0) + Math.max(0, ...waves.values());
235
+ for (const [key, waves] of byEndpointWave) endpoints[key] = (endpoints[key] ?? 0) + Math.max(0, ...waves.values());
217
236
  }
218
- return { global, nodes };
237
+ return { global, nodes, endpoints };
219
238
  }
220
- function assertCapacity(file: CapacityStateFile, nodeId: string, limits: CapacityLimits): void {
239
+
240
+ export interface EndpointCapacityHolders {
241
+ leases: number;
242
+ reservations: number;
243
+ foregroundStreams: number;
244
+ }
245
+
246
+ /** Name the endpoint-capacity addends that are actually nonzero. */
247
+ export function describeEndpointCapacityHolders(holders: EndpointCapacityHolders): string {
248
+ const parts: string[] = [];
249
+ const add = (count: number, singular: string, plural: string): void => {
250
+ if (count > 0) parts.push(`${count} ${count === 1 ? singular : plural}`);
251
+ };
252
+ add(holders.leases, "active lease", "active leases");
253
+ add(holders.reservations, "held reservation", "held reservations");
254
+ add(holders.foregroundStreams, "foreground stream", "foreground streams");
255
+ if (parts.length === 0) return "no active lease, held reservation, or foreground stream currently holds a slot";
256
+ const named =
257
+ parts.length === 1
258
+ ? parts[0]
259
+ : parts.length === 2
260
+ ? `${parts[0]} and ${parts[1]}`
261
+ : `${parts.slice(0, -1).join(", ")}, and ${parts.at(-1)}`;
262
+ const total = holders.leases + holders.reservations + holders.foregroundStreams;
263
+ return `${named} ${total === 1 ? "holds" : "hold"} ${total === 1 ? "the slot" : "the slots"}`;
264
+ }
265
+
266
+ function assertCapacity(
267
+ file: CapacityStateFile,
268
+ nodeId: string,
269
+ endpointKey: string | undefined,
270
+ limits: CapacityLimits,
271
+ ): void {
221
272
  const held = heldReservationUsage(file.reservations);
222
273
  const globalUsed = file.leases.length + held.global;
223
274
  // The denial names the compliant next move, not just the gate. A model told
@@ -235,10 +286,28 @@ function assertCapacity(file: CapacityStateFile, nodeId: string, limits: Capacit
235
286
  if (used >= nodeLimit)
236
287
  throw new Error(`dispatch: admission denied: node '${nodeId}' capacity reached (${used}/${nodeLimit})`);
237
288
  }
289
+ if (endpointKey !== undefined) {
290
+ const endpointLimit = limits.endpoints[endpointKey];
291
+ if (endpointLimit !== undefined) {
292
+ if (endpointLimit < 1)
293
+ throw new Error(`dispatch: admission denied: endpoint '${endpointLabel(endpointKey)}' capacity unavailable`);
294
+ const holders: EndpointCapacityHolders = {
295
+ leases: file.leases.filter((lease) => lease.endpointKey === endpointKey).length,
296
+ reservations: held.endpoints[endpointKey] ?? 0,
297
+ foregroundStreams: foregroundStreamUsage()[endpointKey] ?? 0,
298
+ };
299
+ const used = holders.leases + holders.reservations + holders.foregroundStreams;
300
+ if (used >= endpointLimit)
301
+ throw new Error(
302
+ `dispatch: admission denied: endpoint '${endpointLabel(endpointKey)}' capacity reached (${used}/${endpointLimit} slots): ${describeEndpointCapacityHolders(holders)}; collect in-flight runs or point workers at a second server`,
303
+ );
304
+ }
305
+ }
238
306
  }
239
307
  export function acquireCapacityLease(input: {
240
308
  assignmentId: string;
241
309
  nodeId: string;
310
+ endpointKey?: string;
242
311
  limits: CapacityLimits;
243
312
  nowMs?: number;
244
313
  ttlMs?: number;
@@ -259,10 +328,12 @@ export function acquireCapacityLease(input: {
259
328
  );
260
329
  const existing = file.leases.find((lease) => lease.assignmentId === input.assignmentId);
261
330
  if (existing) {
262
- if (existing.nodeId !== input.nodeId) {
331
+ if (existing.nodeId !== input.nodeId || existing.endpointKey !== input.endpointKey) {
263
332
  const without = { ...file, leases: file.leases.filter((lease) => lease !== existing) };
264
- assertCapacity(without, input.nodeId, input.limits);
333
+ assertCapacity(without, input.nodeId, input.endpointKey, input.limits);
265
334
  existing.nodeId = input.nodeId;
335
+ if (input.endpointKey === undefined) delete existing.endpointKey;
336
+ else existing.endpointKey = input.endpointKey;
266
337
  }
267
338
  existing.heartbeatAt = new Date(nowMs).toISOString();
268
339
  existing.expiresAt = new Date(nowMs + (input.ttlMs ?? DEFAULT_CAPACITY_LEASE_TTL_MS)).toISOString();
@@ -270,7 +341,7 @@ export function acquireCapacityLease(input: {
270
341
  return clone(existing);
271
342
  }
272
343
  input.onAcquiredUnderLock?.(file);
273
- assertCapacity(file, input.nodeId, input.limits);
344
+ assertCapacity(file, input.nodeId, input.endpointKey, input.limits);
274
345
  const token = input.processBirthToken ?? processBirthToken(input.ownerPid ?? process.pid);
275
346
  if (!token) throw new Error("dispatch: cannot establish process birth token");
276
347
  const at = new Date(nowMs).toISOString();
@@ -278,6 +349,7 @@ export function acquireCapacityLease(input: {
278
349
  leaseId: `lease-${nowMs.toString(36)}-${randomBytes(6).toString("hex")}`,
279
350
  assignmentId: input.assignmentId,
280
351
  nodeId: input.nodeId,
352
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
281
353
  host: hostname(),
282
354
  ownerPid: input.ownerPid ?? process.pid,
283
355
  processBirthToken: token,
@@ -365,11 +437,28 @@ export function capacityDrain(nowMs = Date.now()): CapacityDrain | null {
365
437
  export function capacityLeaseUsage(options?: { nowMs?: number; probe?: LeaseOwnerProbe }): {
366
438
  global: number;
367
439
  nodes: Readonly<Record<string, number>>;
440
+ endpoints: Readonly<Record<string, number>>;
441
+ endpointHolders: Readonly<Record<string, EndpointCapacityHolders>>;
368
442
  } {
369
443
  const leases = listCapacityLeases(options);
370
444
  const nodes: Record<string, number> = {};
371
- for (const lease of leases) nodes[lease.nodeId] = (nodes[lease.nodeId] ?? 0) + 1;
372
- return { global: leases.length, nodes };
445
+ const foreground = foregroundStreamUsage();
446
+ const endpoints: Record<string, number> = { ...foreground };
447
+ const endpointHolders: Record<string, EndpointCapacityHolders> = Object.fromEntries(
448
+ Object.entries(foreground).map(([endpointKey, foregroundStreams]) => [
449
+ endpointKey,
450
+ { leases: 0, reservations: 0, foregroundStreams },
451
+ ]),
452
+ );
453
+ for (const lease of leases) {
454
+ nodes[lease.nodeId] = (nodes[lease.nodeId] ?? 0) + 1;
455
+ if (lease.endpointKey !== undefined) {
456
+ endpoints[lease.endpointKey] = (endpoints[lease.endpointKey] ?? 0) + 1;
457
+ const holders = endpointHolders[lease.endpointKey] ?? { leases: 0, reservations: 0, foregroundStreams: 0 };
458
+ endpointHolders[lease.endpointKey] = { ...holders, leases: holders.leases + 1 };
459
+ }
460
+ }
461
+ return { global: leases.length, nodes, endpoints, endpointHolders };
373
462
  }
374
463
 
375
464
  /**