@coool/opencode-adaptive 0.0.0-stage → 1.0.0

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 (440) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -2
  3. package/dist/adapters/codex-capacity/index.d.ts +5 -0
  4. package/dist/adapters/codex-capacity/index.d.ts.map +1 -0
  5. package/dist/adapters/codex-capacity/index.js +5 -0
  6. package/dist/adapters/codex-capacity/index.js.map +1 -0
  7. package/dist/adapters/codex-capacity/parse.d.ts +28 -0
  8. package/dist/adapters/codex-capacity/parse.d.ts.map +1 -0
  9. package/dist/adapters/codex-capacity/parse.js +130 -0
  10. package/dist/adapters/codex-capacity/parse.js.map +1 -0
  11. package/dist/adapters/codex-capacity/reading.d.ts +10 -0
  12. package/dist/adapters/codex-capacity/reading.d.ts.map +1 -0
  13. package/dist/adapters/codex-capacity/reading.js +45 -0
  14. package/dist/adapters/codex-capacity/reading.js.map +1 -0
  15. package/dist/adapters/codex-capacity/sensor.d.ts +24 -0
  16. package/dist/adapters/codex-capacity/sensor.d.ts.map +1 -0
  17. package/dist/adapters/codex-capacity/sensor.js +30 -0
  18. package/dist/adapters/codex-capacity/sensor.js.map +1 -0
  19. package/dist/adapters/codex-capacity/types.d.ts +35 -0
  20. package/dist/adapters/codex-capacity/types.d.ts.map +1 -0
  21. package/dist/adapters/codex-capacity/types.js +4 -0
  22. package/dist/adapters/codex-capacity/types.js.map +1 -0
  23. package/dist/adapters/opencode/escalation-tool.d.ts +52 -0
  24. package/dist/adapters/opencode/escalation-tool.d.ts.map +1 -0
  25. package/dist/adapters/opencode/escalation-tool.js +59 -0
  26. package/dist/adapters/opencode/escalation-tool.js.map +1 -0
  27. package/dist/adapters/opencode/events.d.ts +24 -0
  28. package/dist/adapters/opencode/events.d.ts.map +1 -0
  29. package/dist/adapters/opencode/events.js +68 -0
  30. package/dist/adapters/opencode/events.js.map +1 -0
  31. package/dist/adapters/opencode/hooks.d.ts +35 -0
  32. package/dist/adapters/opencode/hooks.d.ts.map +1 -0
  33. package/dist/adapters/opencode/hooks.js +12 -0
  34. package/dist/adapters/opencode/hooks.js.map +1 -0
  35. package/dist/adapters/opencode/index.d.ts +17 -0
  36. package/dist/adapters/opencode/index.d.ts.map +1 -0
  37. package/dist/adapters/opencode/index.js +9 -0
  38. package/dist/adapters/opencode/index.js.map +1 -0
  39. package/dist/adapters/opencode/inventory.d.ts +31 -0
  40. package/dist/adapters/opencode/inventory.d.ts.map +1 -0
  41. package/dist/adapters/opencode/inventory.js +53 -0
  42. package/dist/adapters/opencode/inventory.js.map +1 -0
  43. package/dist/adapters/opencode/report.d.ts +92 -0
  44. package/dist/adapters/opencode/report.d.ts.map +1 -0
  45. package/dist/adapters/opencode/report.js +145 -0
  46. package/dist/adapters/opencode/report.js.map +1 -0
  47. package/dist/adapters/opencode/session-ops.d.ts +40 -0
  48. package/dist/adapters/opencode/session-ops.d.ts.map +1 -0
  49. package/dist/adapters/opencode/session-ops.js +32 -0
  50. package/dist/adapters/opencode/session-ops.js.map +1 -0
  51. package/dist/adapters/opencode/storage.d.ts +16 -0
  52. package/dist/adapters/opencode/storage.d.ts.map +1 -0
  53. package/dist/adapters/opencode/storage.js +8 -0
  54. package/dist/adapters/opencode/storage.js.map +1 -0
  55. package/dist/adapters/opencode/tier-tool.d.ts +38 -0
  56. package/dist/adapters/opencode/tier-tool.d.ts.map +1 -0
  57. package/dist/adapters/opencode/tier-tool.js +72 -0
  58. package/dist/adapters/opencode/tier-tool.js.map +1 -0
  59. package/dist/adapters/opencode/types.d.ts +102 -0
  60. package/dist/adapters/opencode/types.d.ts.map +1 -0
  61. package/dist/adapters/opencode/types.js +2 -0
  62. package/dist/adapters/opencode/types.js.map +1 -0
  63. package/dist/core/compatibility/gate.d.ts +10 -0
  64. package/dist/core/compatibility/gate.d.ts.map +1 -0
  65. package/dist/core/compatibility/gate.js +55 -0
  66. package/dist/core/compatibility/gate.js.map +1 -0
  67. package/dist/core/compatibility/index.d.ts +3 -0
  68. package/dist/core/compatibility/index.d.ts.map +1 -0
  69. package/dist/core/compatibility/index.js +3 -0
  70. package/dist/core/compatibility/index.js.map +1 -0
  71. package/dist/core/compatibility/types.d.ts +54 -0
  72. package/dist/core/compatibility/types.d.ts.map +1 -0
  73. package/dist/core/compatibility/types.js +27 -0
  74. package/dist/core/compatibility/types.js.map +1 -0
  75. package/dist/core/scheduler/admit.d.ts +7 -0
  76. package/dist/core/scheduler/admit.d.ts.map +1 -0
  77. package/dist/core/scheduler/admit.js +153 -0
  78. package/dist/core/scheduler/admit.js.map +1 -0
  79. package/dist/core/scheduler/escalate.d.ts +7 -0
  80. package/dist/core/scheduler/escalate.d.ts.map +1 -0
  81. package/dist/core/scheduler/escalate.js +56 -0
  82. package/dist/core/scheduler/escalate.js.map +1 -0
  83. package/dist/core/scheduler/index.d.ts +5 -0
  84. package/dist/core/scheduler/index.d.ts.map +1 -0
  85. package/dist/core/scheduler/index.js +5 -0
  86. package/dist/core/scheduler/index.js.map +1 -0
  87. package/dist/core/scheduler/mode.d.ts +9 -0
  88. package/dist/core/scheduler/mode.d.ts.map +1 -0
  89. package/dist/core/scheduler/mode.js +10 -0
  90. package/dist/core/scheduler/mode.js.map +1 -0
  91. package/dist/core/scheduler/types.d.ts +143 -0
  92. package/dist/core/scheduler/types.d.ts.map +1 -0
  93. package/dist/core/scheduler/types.js +74 -0
  94. package/dist/core/scheduler/types.js.map +1 -0
  95. package/dist/core/tier-policy/index.d.ts +4 -0
  96. package/dist/core/tier-policy/index.d.ts.map +1 -0
  97. package/dist/core/tier-policy/index.js +4 -0
  98. package/dist/core/tier-policy/index.js.map +1 -0
  99. package/dist/core/tier-policy/resolve.d.ts +19 -0
  100. package/dist/core/tier-policy/resolve.d.ts.map +1 -0
  101. package/dist/core/tier-policy/resolve.js +52 -0
  102. package/dist/core/tier-policy/resolve.js.map +1 -0
  103. package/dist/core/tier-policy/tier-list.d.ts +11 -0
  104. package/dist/core/tier-policy/tier-list.d.ts.map +1 -0
  105. package/dist/core/tier-policy/tier-list.js +23 -0
  106. package/dist/core/tier-policy/tier-list.js.map +1 -0
  107. package/dist/core/tier-policy/types.d.ts +85 -0
  108. package/dist/core/tier-policy/types.d.ts.map +1 -0
  109. package/dist/core/tier-policy/types.js +45 -0
  110. package/dist/core/tier-policy/types.js.map +1 -0
  111. package/dist/core/work-unit/index.d.ts +4 -0
  112. package/dist/core/work-unit/index.d.ts.map +1 -0
  113. package/dist/core/work-unit/index.js +4 -0
  114. package/dist/core/work-unit/index.js.map +1 -0
  115. package/dist/core/work-unit/interruption.d.ts +8 -0
  116. package/dist/core/work-unit/interruption.d.ts.map +1 -0
  117. package/dist/core/work-unit/interruption.js +12 -0
  118. package/dist/core/work-unit/interruption.js.map +1 -0
  119. package/dist/core/work-unit/tracker.d.ts +46 -0
  120. package/dist/core/work-unit/tracker.d.ts.map +1 -0
  121. package/dist/core/work-unit/tracker.js +136 -0
  122. package/dist/core/work-unit/tracker.js.map +1 -0
  123. package/dist/core/work-unit/types.d.ts +146 -0
  124. package/dist/core/work-unit/types.d.ts.map +1 -0
  125. package/dist/core/work-unit/types.js +23 -0
  126. package/dist/core/work-unit/types.js.map +1 -0
  127. package/dist/execution/continuation/execute.d.ts +19 -0
  128. package/dist/execution/continuation/execute.d.ts.map +1 -0
  129. package/dist/execution/continuation/execute.js +31 -0
  130. package/dist/execution/continuation/execute.js.map +1 -0
  131. package/dist/execution/continuation/index.d.ts +6 -0
  132. package/dist/execution/continuation/index.d.ts.map +1 -0
  133. package/dist/execution/continuation/index.js +5 -0
  134. package/dist/execution/continuation/index.js.map +1 -0
  135. package/dist/execution/continuation/ledger.d.ts +20 -0
  136. package/dist/execution/continuation/ledger.d.ts.map +1 -0
  137. package/dist/execution/continuation/ledger.js +31 -0
  138. package/dist/execution/continuation/ledger.js.map +1 -0
  139. package/dist/execution/continuation/plan.d.ts +13 -0
  140. package/dist/execution/continuation/plan.d.ts.map +1 -0
  141. package/dist/execution/continuation/plan.js +20 -0
  142. package/dist/execution/continuation/plan.js.map +1 -0
  143. package/dist/execution/continuation/prompts.d.ts +8 -0
  144. package/dist/execution/continuation/prompts.d.ts.map +1 -0
  145. package/dist/execution/continuation/prompts.js +8 -0
  146. package/dist/execution/continuation/prompts.js.map +1 -0
  147. package/dist/execution/continuation/types.d.ts +70 -0
  148. package/dist/execution/continuation/types.d.ts.map +1 -0
  149. package/dist/execution/continuation/types.js +15 -0
  150. package/dist/execution/continuation/types.js.map +1 -0
  151. package/dist/execution/escalation/index.d.ts +2 -0
  152. package/dist/execution/escalation/index.d.ts.map +1 -0
  153. package/dist/execution/escalation/index.js +2 -0
  154. package/dist/execution/escalation/index.js.map +1 -0
  155. package/dist/execution/escalation/types.d.ts +31 -0
  156. package/dist/execution/escalation/types.d.ts.map +1 -0
  157. package/dist/execution/escalation/types.js +49 -0
  158. package/dist/execution/escalation/types.js.map +1 -0
  159. package/dist/execution/index.d.ts +5 -0
  160. package/dist/execution/index.d.ts.map +1 -0
  161. package/dist/execution/index.js +5 -0
  162. package/dist/execution/index.js.map +1 -0
  163. package/dist/execution/parking/index.d.ts +3 -0
  164. package/dist/execution/parking/index.d.ts.map +1 -0
  165. package/dist/execution/parking/index.js +2 -0
  166. package/dist/execution/parking/index.js.map +1 -0
  167. package/dist/execution/parking/plan.d.ts +40 -0
  168. package/dist/execution/parking/plan.d.ts.map +1 -0
  169. package/dist/execution/parking/plan.js +34 -0
  170. package/dist/execution/parking/plan.js.map +1 -0
  171. package/dist/execution/recovery/gate.d.ts +20 -0
  172. package/dist/execution/recovery/gate.d.ts.map +1 -0
  173. package/dist/execution/recovery/gate.js +23 -0
  174. package/dist/execution/recovery/gate.js.map +1 -0
  175. package/dist/execution/recovery/index.d.ts +3 -0
  176. package/dist/execution/recovery/index.d.ts.map +1 -0
  177. package/dist/execution/recovery/index.js +2 -0
  178. package/dist/execution/recovery/index.js.map +1 -0
  179. package/dist/execution/retry/decide.d.ts +30 -0
  180. package/dist/execution/retry/decide.d.ts.map +1 -0
  181. package/dist/execution/retry/decide.js +76 -0
  182. package/dist/execution/retry/decide.js.map +1 -0
  183. package/dist/execution/retry/index.d.ts +3 -0
  184. package/dist/execution/retry/index.d.ts.map +1 -0
  185. package/dist/execution/retry/index.js +3 -0
  186. package/dist/execution/retry/index.js.map +1 -0
  187. package/dist/execution/retry/types.d.ts +66 -0
  188. package/dist/execution/retry/types.d.ts.map +1 -0
  189. package/dist/execution/retry/types.js +9 -0
  190. package/dist/execution/retry/types.js.map +1 -0
  191. package/dist/gui-experimental.cjs +1 -0
  192. package/dist/gui.d.ts +17 -0
  193. package/dist/gui.d.ts.map +1 -0
  194. package/dist/gui.js +70 -0
  195. package/dist/gui.js.map +1 -0
  196. package/dist/health/capacity/index.d.ts +4 -0
  197. package/dist/health/capacity/index.d.ts.map +1 -0
  198. package/dist/health/capacity/index.js +4 -0
  199. package/dist/health/capacity/index.js.map +1 -0
  200. package/dist/health/capacity/merge.d.ts +58 -0
  201. package/dist/health/capacity/merge.d.ts.map +1 -0
  202. package/dist/health/capacity/merge.js +154 -0
  203. package/dist/health/capacity/merge.js.map +1 -0
  204. package/dist/health/capacity/registry.d.ts +41 -0
  205. package/dist/health/capacity/registry.d.ts.map +1 -0
  206. package/dist/health/capacity/registry.js +106 -0
  207. package/dist/health/capacity/registry.js.map +1 -0
  208. package/dist/health/capacity/types.d.ts +74 -0
  209. package/dist/health/capacity/types.d.ts.map +1 -0
  210. package/dist/health/capacity/types.js +6 -0
  211. package/dist/health/capacity/types.js.map +1 -0
  212. package/dist/health/circuit-breaker/breaker.d.ts +84 -0
  213. package/dist/health/circuit-breaker/breaker.d.ts.map +1 -0
  214. package/dist/health/circuit-breaker/breaker.js +132 -0
  215. package/dist/health/circuit-breaker/breaker.js.map +1 -0
  216. package/dist/health/circuit-breaker/index.d.ts +3 -0
  217. package/dist/health/circuit-breaker/index.d.ts.map +1 -0
  218. package/dist/health/circuit-breaker/index.js +3 -0
  219. package/dist/health/circuit-breaker/index.js.map +1 -0
  220. package/dist/health/circuit-breaker/types.d.ts +53 -0
  221. package/dist/health/circuit-breaker/types.d.ts.map +1 -0
  222. package/dist/health/circuit-breaker/types.js +13 -0
  223. package/dist/health/circuit-breaker/types.js.map +1 -0
  224. package/dist/health/classifier/classify.d.ts +9 -0
  225. package/dist/health/classifier/classify.d.ts.map +1 -0
  226. package/dist/health/classifier/classify.js +245 -0
  227. package/dist/health/classifier/classify.js.map +1 -0
  228. package/dist/health/classifier/index.d.ts +3 -0
  229. package/dist/health/classifier/index.d.ts.map +1 -0
  230. package/dist/health/classifier/index.js +3 -0
  231. package/dist/health/classifier/index.js.map +1 -0
  232. package/dist/health/classifier/types.d.ts +72 -0
  233. package/dist/health/classifier/types.d.ts.map +1 -0
  234. package/dist/health/classifier/types.js +27 -0
  235. package/dist/health/classifier/types.js.map +1 -0
  236. package/dist/health/index.d.ts +5 -0
  237. package/dist/health/index.d.ts.map +1 -0
  238. package/dist/health/index.js +5 -0
  239. package/dist/health/index.js.map +1 -0
  240. package/dist/health/resource-health/index.d.ts +5 -0
  241. package/dist/health/resource-health/index.d.ts.map +1 -0
  242. package/dist/health/resource-health/index.js +5 -0
  243. package/dist/health/resource-health/index.js.map +1 -0
  244. package/dist/health/resource-health/key.d.ts +19 -0
  245. package/dist/health/resource-health/key.d.ts.map +1 -0
  246. package/dist/health/resource-health/key.js +54 -0
  247. package/dist/health/resource-health/key.js.map +1 -0
  248. package/dist/health/resource-health/registry.d.ts +41 -0
  249. package/dist/health/resource-health/registry.d.ts.map +1 -0
  250. package/dist/health/resource-health/registry.js +258 -0
  251. package/dist/health/resource-health/registry.js.map +1 -0
  252. package/dist/health/resource-health/transitions.d.ts +11 -0
  253. package/dist/health/resource-health/transitions.d.ts.map +1 -0
  254. package/dist/health/resource-health/transitions.js +165 -0
  255. package/dist/health/resource-health/transitions.js.map +1 -0
  256. package/dist/health/resource-health/types.d.ts +116 -0
  257. package/dist/health/resource-health/types.d.ts.map +1 -0
  258. package/dist/health/resource-health/types.js +43 -0
  259. package/dist/health/resource-health/types.js.map +1 -0
  260. package/dist/index.d.ts +17 -0
  261. package/dist/index.d.ts.map +1 -0
  262. package/dist/index.js +80 -0
  263. package/dist/index.js.map +1 -0
  264. package/dist/plugin-id.d.ts +3 -0
  265. package/dist/plugin-id.d.ts.map +1 -0
  266. package/dist/plugin-id.js +3 -0
  267. package/dist/plugin-id.js.map +1 -0
  268. package/dist/rpc/definition.d.ts +100 -0
  269. package/dist/rpc/definition.d.ts.map +1 -0
  270. package/dist/rpc/definition.js +104 -0
  271. package/dist/rpc/definition.js.map +1 -0
  272. package/dist/rpc/handlers.d.ts +65 -0
  273. package/dist/rpc/handlers.d.ts.map +1 -0
  274. package/dist/rpc/handlers.js +111 -0
  275. package/dist/rpc/handlers.js.map +1 -0
  276. package/dist/rpc/status.d.ts +43 -0
  277. package/dist/rpc/status.d.ts.map +1 -0
  278. package/dist/rpc/status.js +45 -0
  279. package/dist/rpc/status.js.map +1 -0
  280. package/dist/rpc.d.ts +11 -0
  281. package/dist/rpc.d.ts.map +1 -0
  282. package/dist/rpc.js +8 -0
  283. package/dist/rpc.js.map +1 -0
  284. package/dist/state/capacity/index.d.ts +4 -0
  285. package/dist/state/capacity/index.d.ts.map +1 -0
  286. package/dist/state/capacity/index.js +4 -0
  287. package/dist/state/capacity/index.js.map +1 -0
  288. package/dist/state/capacity/parse.d.ts +3 -0
  289. package/dist/state/capacity/parse.d.ts.map +1 -0
  290. package/dist/state/capacity/parse.js +110 -0
  291. package/dist/state/capacity/parse.js.map +1 -0
  292. package/dist/state/capacity/store.d.ts +19 -0
  293. package/dist/state/capacity/store.d.ts.map +1 -0
  294. package/dist/state/capacity/store.js +64 -0
  295. package/dist/state/capacity/store.js.map +1 -0
  296. package/dist/state/capacity/types.d.ts +38 -0
  297. package/dist/state/capacity/types.d.ts.map +1 -0
  298. package/dist/state/capacity/types.js +11 -0
  299. package/dist/state/capacity/types.js.map +1 -0
  300. package/dist/state/health/index.d.ts +5 -0
  301. package/dist/state/health/index.d.ts.map +1 -0
  302. package/dist/state/health/index.js +4 -0
  303. package/dist/state/health/index.js.map +1 -0
  304. package/dist/state/health/parse.d.ts +3 -0
  305. package/dist/state/health/parse.d.ts.map +1 -0
  306. package/dist/state/health/parse.js +121 -0
  307. package/dist/state/health/parse.js.map +1 -0
  308. package/dist/state/health/store.d.ts +18 -0
  309. package/dist/state/health/store.d.ts.map +1 -0
  310. package/dist/state/health/store.js +55 -0
  311. package/dist/state/health/store.js.map +1 -0
  312. package/dist/state/health/types.d.ts +24 -0
  313. package/dist/state/health/types.d.ts.map +1 -0
  314. package/dist/state/health/types.js +5 -0
  315. package/dist/state/health/types.js.map +1 -0
  316. package/dist/state/parked-work/index.d.ts +5 -0
  317. package/dist/state/parked-work/index.d.ts.map +1 -0
  318. package/dist/state/parked-work/index.js +4 -0
  319. package/dist/state/parked-work/index.js.map +1 -0
  320. package/dist/state/parked-work/parse.d.ts +7 -0
  321. package/dist/state/parked-work/parse.d.ts.map +1 -0
  322. package/dist/state/parked-work/parse.js +83 -0
  323. package/dist/state/parked-work/parse.js.map +1 -0
  324. package/dist/state/parked-work/store.d.ts +18 -0
  325. package/dist/state/parked-work/store.d.ts.map +1 -0
  326. package/dist/state/parked-work/store.js +81 -0
  327. package/dist/state/parked-work/store.js.map +1 -0
  328. package/dist/state/parked-work/types.d.ts +42 -0
  329. package/dist/state/parked-work/types.d.ts.map +1 -0
  330. package/dist/state/parked-work/types.js +7 -0
  331. package/dist/state/parked-work/types.js.map +1 -0
  332. package/dist/state/receipts/bound.d.ts +15 -0
  333. package/dist/state/receipts/bound.d.ts.map +1 -0
  334. package/dist/state/receipts/bound.js +77 -0
  335. package/dist/state/receipts/bound.js.map +1 -0
  336. package/dist/state/receipts/index.d.ts +7 -0
  337. package/dist/state/receipts/index.d.ts.map +1 -0
  338. package/dist/state/receipts/index.js +5 -0
  339. package/dist/state/receipts/index.js.map +1 -0
  340. package/dist/state/receipts/parse.d.ts +7 -0
  341. package/dist/state/receipts/parse.d.ts.map +1 -0
  342. package/dist/state/receipts/parse.js +226 -0
  343. package/dist/state/receipts/parse.js.map +1 -0
  344. package/dist/state/receipts/store.d.ts +35 -0
  345. package/dist/state/receipts/store.d.ts.map +1 -0
  346. package/dist/state/receipts/store.js +128 -0
  347. package/dist/state/receipts/store.js.map +1 -0
  348. package/dist/state/receipts/types.d.ts +62 -0
  349. package/dist/state/receipts/types.d.ts.map +1 -0
  350. package/dist/state/receipts/types.js +12 -0
  351. package/dist/state/receipts/types.js.map +1 -0
  352. package/dist/state/session-modes/index.d.ts +5 -0
  353. package/dist/state/session-modes/index.d.ts.map +1 -0
  354. package/dist/state/session-modes/index.js +4 -0
  355. package/dist/state/session-modes/index.js.map +1 -0
  356. package/dist/state/session-modes/parse.d.ts +9 -0
  357. package/dist/state/session-modes/parse.d.ts.map +1 -0
  358. package/dist/state/session-modes/parse.js +62 -0
  359. package/dist/state/session-modes/parse.js.map +1 -0
  360. package/dist/state/session-modes/store.d.ts +21 -0
  361. package/dist/state/session-modes/store.d.ts.map +1 -0
  362. package/dist/state/session-modes/store.js +73 -0
  363. package/dist/state/session-modes/store.js.map +1 -0
  364. package/dist/state/session-modes/types.d.ts +42 -0
  365. package/dist/state/session-modes/types.d.ts.map +1 -0
  366. package/dist/state/session-modes/types.js +7 -0
  367. package/dist/state/session-modes/types.js.map +1 -0
  368. package/dist/state/settings/index.d.ts +5 -0
  369. package/dist/state/settings/index.d.ts.map +1 -0
  370. package/dist/state/settings/index.js +4 -0
  371. package/dist/state/settings/index.js.map +1 -0
  372. package/dist/state/settings/parse.d.ts +34 -0
  373. package/dist/state/settings/parse.d.ts.map +1 -0
  374. package/dist/state/settings/parse.js +261 -0
  375. package/dist/state/settings/parse.js.map +1 -0
  376. package/dist/state/settings/store.d.ts +22 -0
  377. package/dist/state/settings/store.d.ts.map +1 -0
  378. package/dist/state/settings/store.js +57 -0
  379. package/dist/state/settings/store.js.map +1 -0
  380. package/dist/state/settings/types.d.ts +63 -0
  381. package/dist/state/settings/types.d.ts.map +1 -0
  382. package/dist/state/settings/types.js +12 -0
  383. package/dist/state/settings/types.js.map +1 -0
  384. package/dist/supervisor/index.d.ts +5 -0
  385. package/dist/supervisor/index.d.ts.map +1 -0
  386. package/dist/supervisor/index.js +4 -0
  387. package/dist/supervisor/index.js.map +1 -0
  388. package/dist/supervisor/log.d.ts +11 -0
  389. package/dist/supervisor/log.d.ts.map +1 -0
  390. package/dist/supervisor/log.js +14 -0
  391. package/dist/supervisor/log.js.map +1 -0
  392. package/dist/supervisor/supervisor.d.ts +69 -0
  393. package/dist/supervisor/supervisor.d.ts.map +1 -0
  394. package/dist/supervisor/supervisor.js +1343 -0
  395. package/dist/supervisor/supervisor.js.map +1 -0
  396. package/dist/supervisor/types.d.ts +50 -0
  397. package/dist/supervisor/types.d.ts.map +1 -0
  398. package/dist/supervisor/types.js +3 -0
  399. package/dist/supervisor/types.js.map +1 -0
  400. package/dist/supervisor/view.d.ts +4 -0
  401. package/dist/supervisor/view.d.ts.map +1 -0
  402. package/dist/supervisor/view.js +4 -0
  403. package/dist/supervisor/view.js.map +1 -0
  404. package/dist/tui.d.ts +16 -0
  405. package/dist/tui.d.ts.map +1 -0
  406. package/dist/tui.js +252 -0
  407. package/dist/tui.js.map +1 -0
  408. package/dist/ui/gui/api.d.ts +18 -0
  409. package/dist/ui/gui/api.d.ts.map +1 -0
  410. package/dist/ui/gui/api.js +21 -0
  411. package/dist/ui/gui/api.js.map +1 -0
  412. package/dist/ui/gui/view.d.ts +27 -0
  413. package/dist/ui/gui/view.d.ts.map +1 -0
  414. package/dist/ui/gui/view.js +49 -0
  415. package/dist/ui/gui/view.js.map +1 -0
  416. package/dist/ui/tui/controller.d.ts +98 -0
  417. package/dist/ui/tui/controller.d.ts.map +1 -0
  418. package/dist/ui/tui/controller.js +399 -0
  419. package/dist/ui/tui/controller.js.map +1 -0
  420. package/dist/ui/tui/menu.d.ts +56 -0
  421. package/dist/ui/tui/menu.d.ts.map +1 -0
  422. package/dist/ui/tui/menu.js +176 -0
  423. package/dist/ui/tui/menu.js.map +1 -0
  424. package/dist/ui/tui/options.d.ts +20 -0
  425. package/dist/ui/tui/options.d.ts.map +1 -0
  426. package/dist/ui/tui/options.js +17 -0
  427. package/dist/ui/tui/options.js.map +1 -0
  428. package/dist/ui/tui/receipts.d.ts +22 -0
  429. package/dist/ui/tui/receipts.d.ts.map +1 -0
  430. package/dist/ui/tui/receipts.js +109 -0
  431. package/dist/ui/tui/receipts.js.map +1 -0
  432. package/dist/ui/tui/status.d.ts +23 -0
  433. package/dist/ui/tui/status.d.ts.map +1 -0
  434. package/dist/ui/tui/status.js +90 -0
  435. package/dist/ui/tui/status.js.map +1 -0
  436. package/dist/ui/tui/tiers.d.ts +52 -0
  437. package/dist/ui/tui/tiers.d.ts.map +1 -0
  438. package/dist/ui/tui/tiers.js +108 -0
  439. package/dist/ui/tui/tiers.js.map +1 -0
  440. package/package.json +85 -4
@@ -0,0 +1,1343 @@
1
+ var _a;
2
+ /**
3
+ * The supervisor (ADP-025..ADP-032): owns all state stores and glues the pure
4
+ * core to the host reports. It contains no policy of its own — every decision
5
+ * comes from the pure scheduler/retry/tier modules — and it applies the two
6
+ * wiring contracts: fail-open (ADP-031, internal failures log and defer) and
7
+ * Observe ≡ Control (the same admission path, only the mode wrapper differs).
8
+ *
9
+ * Wiring decisions recorded here (see docs/ARCHITECTURE.md):
10
+ * - admission runs only on `execution-started` (a safe boundary); auxiliary
11
+ * per-request routing is not applied in v1 because switching the model for
12
+ * an auxiliary request would also move the primary model.
13
+ * - park records arise only from owned-recovery failures and resume
14
+ * boundaries; admission cannot hold an already-started episode, so an
15
+ * admission `park` decision is recorded as a receipt without a parked
16
+ * record (the episode proceeds on OpenCode's own model).
17
+ * - retry-hook veto decisions are not receipts (they are not scheduler
18
+ * decisions); admission receipts plus outcome attachment cover ADP-030.
19
+ * - interruption reasons outside the known vocabulary leave the unit open
20
+ * (a later episode supersedes it); no outcome is ever fabricated.
21
+ * - escalation (ADP-015..017) triggers only on capability-classified unit
22
+ * failures (context overflow, content policy); infrastructure failures
23
+ * never escalate, and the per-unit budget is finite.
24
+ */
25
+ import { WorkUnitTracker } from "../core/work-unit/tracker.js";
26
+ import { sameModelRef, } from "../core/work-unit/types.js";
27
+ import { INTERRUPT_REASONS } from "../core/work-unit/interruption.js";
28
+ import { resolveRouting } from "../core/tier-policy/resolve.js";
29
+ import { TIERS, nextTier } from "../core/tier-policy/types.js";
30
+ import { admit, forMode as schedulerForMode, planEscalation, schedulerHealthKey, } from "../core/scheduler/index.js";
31
+ import { MAX_ESCALATION_EVIDENCE } from "../core/scheduler/types.js";
32
+ import { checkCompatibility } from "../core/compatibility/gate.js";
33
+ import { classifyFailure, CapacityRegistry, ResourceHealthRegistry } from "../health/index.js";
34
+ import { sweepRecord } from "../health/resource-health/transitions.js";
35
+ import { HealthRecordStore, HEALTH_KEY } from "../state/health/index.js";
36
+ import { CapacityReadingStore, CAPACITY_KEY } from "../state/capacity/index.js";
37
+ import { mergeReadings } from "../health/capacity/merge.js";
38
+ import { resourceChain, resourceKeyID } from "../health/resource-health/key.js";
39
+ import { SettingsStore, SETTINGS_KEY } from "../state/settings/index.js";
40
+ import { SessionModeStore, SESSION_MODES_KEY } from "../state/session-modes/index.js";
41
+ import { ReceiptStore, RECEIPTS_KEY } from "../state/receipts/index.js";
42
+ import { buildStatus } from "../rpc/status.js";
43
+ import { RECEIPTS_VIEW_LIMIT } from "./view.js";
44
+ import { ParkedWorkStore, PARKED_KEY } from "../state/parked-work/index.js";
45
+ import { decideRetry, executeContinuation, forMode as retryForMode, planContinuation, selectResumable, } from "../execution/index.js";
46
+ import { escalationDetail } from "../execution/escalation/index.js";
47
+ import { listCandidates, sessionOps, storageSeam } from "../adapters/opencode/index.js";
48
+ import { CodexCapacitySensor } from "../adapters/codex-capacity/index.js";
49
+ import { logFailOpen } from "./log.js";
50
+ import { CODEX_PROVIDER_ID } from "./types.js";
51
+ /** Which tiers currently have configured resources (ADP-005). */
52
+ /** Vocabulary guard without a cast: strict member equality. */
53
+ function isTierName(tier) {
54
+ return TIERS.some((name) => name === tier);
55
+ }
56
+ function configuredTiers(settings) {
57
+ return TIERS.filter((tier) => settings.tiers[tier].length > 0);
58
+ }
59
+ /** Narrow a wire interrupt reason to the known vocabulary. */
60
+ function interruptReason(reason) {
61
+ return INTERRUPT_REASONS.includes(reason)
62
+ ? reason
63
+ : undefined;
64
+ }
65
+ /**
66
+ * Failure categories that count as structured capability evidence (ADP-015).
67
+ * The remaining evidence kinds (agent request, reviewer rejection) have no
68
+ * structured host source in v2 and are never inferred from free text.
69
+ */
70
+ const CAPABILITY_EVIDENCE_CATEGORIES = [
71
+ "context_overflow",
72
+ "content_policy",
73
+ ];
74
+ /** Finite per-unit escalation budget (ADP-017). */
75
+ const MAX_ESCALATIONS_PER_UNIT = 2;
76
+ export class Supervisor {
77
+ #options;
78
+ #settings;
79
+ #receipts;
80
+ #parked;
81
+ #tracker = new WorkUnitTracker();
82
+ #health;
83
+ #healthRecords;
84
+ #capacityStore;
85
+ #sessionModes;
86
+ #capacity;
87
+ #codex;
88
+ #vetoes = new Map();
89
+ #escalations = new Map();
90
+ #admissionTiers = new Map();
91
+ /** Recorded escalation-tool requests per unit (ADP-015 agent self-reports). */
92
+ #escalationEvidence = new Map();
93
+ /** Last agent observed per session (role-hint routing evidence, ADP-002). */
94
+ // Bounded like every other per-session map: long-lived hosts meet many
95
+ // short-lived subagent sessions, and agent hints for long-gone sessions
96
+ // are worthless. Insertion order gives the eviction (oldest hint first).
97
+ static MAX_AGENT_HINTS = 1024;
98
+ #agents = new Map();
99
+ /**
100
+ * Per-session tier requests (agent-declared via the adaptive_tier tool):
101
+ * the request is STICKY — it steers the session's next and following
102
+ * admissions until cleared or the session dies — because an agent works
103
+ * across many units in one task. Insertion order gives the eviction.
104
+ */
105
+ static MAX_TIER_REQUESTS = 1024;
106
+ #tierRequests = new Map();
107
+ /**
108
+ * Sessions with an owned continuation prompt in flight: the prompt hook
109
+ * will fire for it, but that prompt belongs to the active unit — it must
110
+ * not re-admit (prevents an admission ping-pong).
111
+ */
112
+ #continuationPending = new Set();
113
+ /**
114
+ * Depth of continuation prompts whose prompt hook has not run yet (or whose
115
+ * host call is still in flight). While a session is inside this window, a
116
+ * unit close must not clear the ownership mark: the close event of the
117
+ * failed unit is pumped concurrently with the continuation's own
118
+ * beforePrompt → prompt-hook sequence, and clearing the mark there would
119
+ * make the continuation re-admit (admission ping-pong).
120
+ */
121
+ #continuationsInFlight = new Map();
122
+ /**
123
+ * Half-open probe tickets by session: a probe admitted while computing the
124
+ * recovery for a veto belongs to the continuation episode that follows, so
125
+ * the ticket is parked here until that unit opens and takes it.
126
+ */
127
+ #sessionProbeTickets = new Map();
128
+ /**
129
+ * Half-open probe tickets by unit: the unit admitted as (or carrying) the
130
+ * breaker's single probe. Its termination reports the probe outcome — a
131
+ * success closes the breaker, a failure reopens it, an interruption re-arms
132
+ * it — otherwise a lost probe would wedge the resource in half_open.
133
+ */
134
+ #probeTickets = new Map();
135
+ /**
136
+ * Session → tier admitted by the prompt hook for the unit that is about to
137
+ * open. Consumed by execution-started, which then skips re-routing (the
138
+ * model is already applied to the very turn that triggered admission).
139
+ */
140
+ #promptAdmission = new Map();
141
+ /**
142
+ * Tier intent for an owned continuation (retry recovery, escalation,
143
+ * parked resume) set before its prompt is delivered. The continuation's
144
+ * prompt hook re-arms the prompt-admission marker from this so the
145
+ * continuation's execution-started skips re-routing — a fresh admission
146
+ * would flip an escalated episode back to the preferred model mid-unit
147
+ * (a switch only lands on the next turn). Cleared when the continuation
148
+ * unit opens, its prompt is rejected, or the session dies.
149
+ */
150
+ #continuationTiers = new Map();
151
+ #ready;
152
+ #ops;
153
+ constructor(options) {
154
+ this.#options = options;
155
+ this.#health = new ResourceHealthRegistry(options.clock);
156
+ this.#capacity = new CapacityRegistry(options.clock);
157
+ this.#codex = new CodexCapacitySensor({ providerID: CODEX_PROVIDER_ID });
158
+ this.#capacity.register(this.#codex);
159
+ this.#settings = new SettingsStore(storageSeam(SETTINGS_KEY, options.storage));
160
+ this.#receipts = new ReceiptStore(storageSeam(RECEIPTS_KEY, options.storage));
161
+ this.#parked = new ParkedWorkStore(storageSeam(PARKED_KEY, options.storage));
162
+ this.#healthRecords = new HealthRecordStore(storageSeam(HEALTH_KEY, options.storage));
163
+ this.#capacityStore = new CapacityReadingStore(storageSeam(CAPACITY_KEY, options.storage));
164
+ this.#sessionModes = new SessionModeStore(storageSeam(SESSION_MODES_KEY, options.storage), options.clock);
165
+ this.#ops = sessionOps(options.session, {
166
+ beforeSwitch: (sessionID, model) => {
167
+ this.#tracker.noteOwnSwitch(sessionID, model);
168
+ },
169
+ beforePrompt: (sessionID) => {
170
+ this.#continuationPending.add(sessionID);
171
+ this.#continuationsInFlight.set(sessionID, (this.#continuationsInFlight.get(sessionID) ?? 0) + 1);
172
+ },
173
+ afterPrompt: (sessionID, delivered) => {
174
+ // sessionOps.prompt calls this only after its own beforePrompt, so
175
+ // the entry is always present; the assertion avoids a default branch
176
+ // no caller can reach.
177
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
178
+ const depth = this.#continuationsInFlight.get(sessionID);
179
+ if (depth <= 1) {
180
+ this.#continuationsInFlight.delete(sessionID);
181
+ if (!delivered) {
182
+ // The last in-flight owned prompt never reached the host's hook:
183
+ // its ownership mark would swallow the next user prompt's
184
+ // admission, so it is dropped here (the hook can no longer fire).
185
+ // The continuation's tier intent dies with it — no episode will
186
+ // open to carry it.
187
+ this.#continuationPending.delete(sessionID);
188
+ this.#continuationTiers.delete(sessionID);
189
+ }
190
+ }
191
+ else {
192
+ this.#continuationsInFlight.set(sessionID, depth - 1);
193
+ }
194
+ },
195
+ });
196
+ // Durable state loads before the first report is handled: a later load
197
+ // must never clobber decisions recorded in the meantime.
198
+ const loadState = async () => {
199
+ await this.#receipts.load();
200
+ await this.#parked.load();
201
+ // Unit ids are durable keys (`${sessionID}#${episode}`) persisted in
202
+ // receipts and parked work. Seed the tracker's per-session episode
203
+ // counters above every persisted id: after a restart the counters
204
+ // must never re-mint a colliding id, which would replace a parked
205
+ // record (the store parks one record per id) and misattribute
206
+ // receipt outcomes.
207
+ const floors = new Map();
208
+ const bump = (unitID) => {
209
+ if (unitID === undefined)
210
+ return;
211
+ const cut = unitID.lastIndexOf("#");
212
+ if (cut <= 0)
213
+ return;
214
+ const sessionID = unitID.slice(0, cut);
215
+ const episode = Number(unitID.slice(cut + 1));
216
+ if (!Number.isInteger(episode) || episode <= 0)
217
+ return;
218
+ if (episode > (floors.get(sessionID) ?? 0))
219
+ floors.set(sessionID, episode);
220
+ };
221
+ for (const receipt of this.#receipts.snapshot())
222
+ bump(receipt.unitID);
223
+ for (const record of this.#parked.list())
224
+ bump(record.id);
225
+ this.#tracker.seedEpisodes(floors);
226
+ // Health records hydrate the registry: the host's TUI-mode plugin
227
+ // reconciliation can serve RPC from an instance whose hooks (event
228
+ // pump) live on another instance — persisted failures are the only
229
+ // health that instance can see, and a fresh instance must also assess
230
+ // persisted failures before routing instead of starting from empty.
231
+ await this.#healthRecords.load();
232
+ this.#health.hydrate(this.#healthRecords.snapshot());
233
+ // Session-mode overrides hydrate the same way: a restart (or the
234
+ // host's double-instance reconciliation) must keep steering sessions
235
+ // under the modes the user set for them.
236
+ await this.#sessionModes.load();
237
+ await this.#capacityStore.load();
238
+ return this.#settings.load();
239
+ };
240
+ this.#ready = loadState();
241
+ }
242
+ /** Settings as of the latest load/patch (awaits the constructor load). */
243
+ async #ensureSettings() {
244
+ await this.#ready;
245
+ return this.#settings.get();
246
+ }
247
+ /**
248
+ * The session's effective operating mode (ADP-054): its per-session
249
+ * override when one is set, else the global mode. Every session-scoped
250
+ * supervision decision consults this instead of `settings.mode` directly.
251
+ */
252
+ #modeFor(sessionID, settings) {
253
+ return this.#sessionModes.get(sessionID) ?? settings.mode;
254
+ }
255
+ // ---------- public entry points (never throw — fail-open, ADP-031) ----------
256
+ /** Handle one host report (event pump or session hook). */
257
+ async handleReport(report) {
258
+ try {
259
+ const settings = await this.#ensureSettings();
260
+ // An OFF session is fully inert — no supervision, no receipts, no
261
+ // health evidence. Idle is the exception: it is the bookkeeping
262
+ // boundary (parked-work sweep, dead-session override pruning) and runs
263
+ // for every report so other sessions' overrides are maintained; the
264
+ // sweep itself gates per parked session below.
265
+ if (report.kind !== "idle" && this.#modeFor(report.sessionID, settings) === "off")
266
+ return;
267
+ await this.#dispatch(report, settings);
268
+ }
269
+ catch (error) {
270
+ logFailOpen("report", error);
271
+ }
272
+ }
273
+ /**
274
+ * Handle one `retry` hook invocation. On a veto this mutates
275
+ * `input.decision` to `{ retry: false }`; otherwise the decision is left
276
+ * untouched and OpenCode's native retry policy stands.
277
+ */
278
+ async handleRetry(input) {
279
+ try {
280
+ const settings = await this.#ensureSettings();
281
+ if (this.#modeFor(input.sessionID, settings) === "off")
282
+ return;
283
+ await this.#handleRetry(input, settings);
284
+ }
285
+ catch (error) {
286
+ logFailOpen("retry", error);
287
+ }
288
+ }
289
+ /**
290
+ * Record an escalation-tool request (ADP-015 agent self-report) against the
291
+ * session's current work unit. Returns false when no unit is active — the
292
+ * request is then discarded (escalation is a per-unit signal).
293
+ */
294
+ noteEscalationRequest(sessionID, reason, evidence) {
295
+ const unit = this.#tracker.active(sessionID);
296
+ if (unit === undefined)
297
+ return false;
298
+ const recorded = this.#escalationEvidence.get(unit.id) ?? [];
299
+ recorded.push({
300
+ kind: "agent-request",
301
+ detail: escalationDetail({ reason, evidence }),
302
+ });
303
+ this.#escalationEvidence.set(unit.id, recorded.slice(-MAX_ESCALATION_EVIDENCE));
304
+ return true;
305
+ }
306
+ /**
307
+ * Record an agent-declared tier request for the session (the adaptive_tier
308
+ * tool). STICKY: it steers the session's next and following admissions
309
+ * until cleared — an agent works across many units in one task. Returns
310
+ * false on an unknown tier (the tool re-validates before calling).
311
+ */
312
+ noteTierRequest(sessionID, tier) {
313
+ if (!isTierName(tier))
314
+ return false;
315
+ if (!this.#tierRequests.has(sessionID) &&
316
+ this.#tierRequests.size >= _a.MAX_TIER_REQUESTS) {
317
+ // The map is non-empty here (size >= MAX_TIER_REQUESTS > 0): Map
318
+ // preserves insertion order, so the first key is the oldest request.
319
+ for (const oldest of this.#tierRequests.keys()) {
320
+ this.#tierRequests.delete(oldest);
321
+ break;
322
+ }
323
+ }
324
+ this.#tierRequests.set(sessionID, { tier, via: "manual-command" });
325
+ return true;
326
+ }
327
+ /** The session's tier request, or `null` when it follows the routing ladder. */
328
+ async getTierRequest(sessionID) {
329
+ await this.#ready;
330
+ return this.#tierRequests.get(sessionID)?.tier ?? null;
331
+ }
332
+ /** Clear one session's tier request (`null` is the only valid input). */
333
+ async setTierRequest(sessionID, tier) {
334
+ await this.#ready;
335
+ if (tier !== null)
336
+ throw new Error("only null clears a tier request");
337
+ this.#tierRequests.delete(sessionID);
338
+ }
339
+ // ---------- state API for the control surfaces (ADP-033/034/036) ----------
340
+ /** Current settings (awaits the constructor load). */
341
+ async settings() {
342
+ return this.#ensureSettings();
343
+ }
344
+ /** Validate and persist a settings patch; returns per-field errors. */
345
+ async updateSettings(patch) {
346
+ await this.#ready;
347
+ return this.#settings.update(patch);
348
+ }
349
+ /** The session's mode override, or `null` when it follows the global mode. */
350
+ async getSessionMode(sessionID) {
351
+ await this.#ready;
352
+ return this.#sessionModes.get(sessionID);
353
+ }
354
+ /**
355
+ * Persist one session's mode override (`null` clears it). Throws on write
356
+ * failure — the RPC handler degrades that to an error patch result
357
+ * (fail-open, ADP-031), while the routing path never calls this.
358
+ */
359
+ async setSessionMode(sessionID, mode) {
360
+ await this.#ready;
361
+ await this.#sessionModes.set(sessionID, mode);
362
+ }
363
+ /**
364
+ * Bounded status snapshot for the status view (ADP-033). Health is read
365
+ * from the persisted record set (not the in-memory registry): the serving
366
+ * instance's registry can be empty when the host's plugin reconciliation
367
+ * placed the event-pump hooks on another instance, while the persisted
368
+ * mirror is shared by every instance.
369
+ */
370
+ async status() {
371
+ const settings = await this.#ensureSettings();
372
+ await this.#healthRecords.load();
373
+ // The persisted mirror serves the status surface: a serving instance
374
+ // without the event-pump hooks must still report sensed capacity.
375
+ await this.#capacityStore.load();
376
+ const now = this.#options.clock();
377
+ const records = this.#healthRecords.snapshot().map((record) => sweepRecord(record, now));
378
+ // Capacity readings are gathered per configured ref (mirroring the health
379
+ // input shape) so buildStatus stays a pure function of its inputs.
380
+ const capacity = {};
381
+ for (const tier of TIERS) {
382
+ for (const ref of settings.tiers[tier]) {
383
+ const chainIDs = new Set(resourceChain(refKey(ref)).map(resourceKeyID));
384
+ const candidates = this.#capacityStore
385
+ .snapshot()
386
+ .filter((entry) => chainIDs.has(resourceKeyID(entry.key)))
387
+ .map((entry) => entry.reading);
388
+ capacity[schedulerHealthKey(ref)] = mergeReadings(candidates, now);
389
+ }
390
+ }
391
+ return buildStatus({
392
+ settings,
393
+ health: records,
394
+ capacity,
395
+ parked: this.#parked.count(),
396
+ receipts: this.#receipts.snapshot().length,
397
+ });
398
+ }
399
+ /** Newest-first bounded receipt view (ADP-030). */
400
+ async receipts() {
401
+ await this.#ready;
402
+ return this.#receipts.snapshot().slice(-RECEIPTS_VIEW_LIMIT).reverse();
403
+ }
404
+ // ---------- dispatch ----------
405
+ async #dispatch(report, settings) {
406
+ switch (report.kind) {
407
+ case "execution-started":
408
+ await this.#onExecutionStarted(report, settings);
409
+ return;
410
+ case "execution-succeeded":
411
+ await this.#onExecutionSucceeded(report);
412
+ return;
413
+ case "execution-failed":
414
+ await this.#onExecutionFailed(report, settings);
415
+ return;
416
+ case "execution-interrupted":
417
+ await this.#onExecutionInterrupted(report);
418
+ return;
419
+ case "model-selected":
420
+ this.#tracker.apply({
421
+ kind: "model-selected",
422
+ sessionID: report.sessionID,
423
+ at: report.at,
424
+ model: report.model,
425
+ });
426
+ return;
427
+ case "model-request":
428
+ if (report.agent !== undefined) {
429
+ if (!this.#agents.has(report.sessionID) &&
430
+ this.#agents.size >= _a.MAX_AGENT_HINTS) {
431
+ // The guard above ensures the map is at its cap (>= 1 entries),
432
+ // so the first key always exists; the assertion avoids a branch
433
+ // the coverage gate would reject as unreachable.
434
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
435
+ const oldest = this.#agents.keys().next().value;
436
+ this.#agents.delete(oldest);
437
+ }
438
+ this.#agents.set(report.sessionID, report.agent);
439
+ }
440
+ this.#tracker.apply({
441
+ kind: "model-request",
442
+ sessionID: report.sessionID,
443
+ at: this.#options.clock(),
444
+ model: report.model,
445
+ });
446
+ return;
447
+ case "http-response":
448
+ await this.#onHttpResponse(report);
449
+ return;
450
+ case "idle":
451
+ await this.#onIdle(settings);
452
+ return;
453
+ case "prompt":
454
+ await this.#onPrompt(report.sessionID, settings);
455
+ return;
456
+ }
457
+ }
458
+ // ---------- admission ----------
459
+ /**
460
+ * CONTROL-mode admission in the awaited prompt hook. The hook runs before
461
+ * the turn's model resolution, so the applied model governs the very turn
462
+ * that triggered admission (the event-pump execution-started report is
463
+ * asynchronous and would only affect the next turn — verified on host
464
+ * 2.2.5). OBSERVE never switches, so it needs no prompt-time decision and
465
+ * stays on the pump entirely.
466
+ */
467
+ async #onPrompt(sessionID, settings) {
468
+ const mode = this.#modeFor(sessionID, settings);
469
+ if (mode !== "active")
470
+ return;
471
+ if (this.#continuationPending.delete(sessionID)) {
472
+ // An owned continuation (retry recovery, escalation, parked resume)
473
+ // already owns this unit's routing: re-arm the admission marker from
474
+ // the continuation's tier intent so execution-started skips a fresh
475
+ // (continuation-unaware) admission.
476
+ const tier = this.#continuationTiers.get(sessionID);
477
+ this.#promptAdmission.set(sessionID, tier !== undefined ? { tier } : {});
478
+ return;
479
+ }
480
+ // A fresh user prompt cancels any stale continuation intent and marker:
481
+ // this admission re-routes from scratch.
482
+ this.#continuationTiers.delete(sessionID);
483
+ this.#promptAdmission.delete(sessionID);
484
+ // A pending probe ticket from an earlier prompt admission (or a failed
485
+ // recovery) can only be carried by the unit the ORIGINAL prompt opens;
486
+ // a fresh admission means that episode never ran, so the ticket re-arms
487
+ // here instead of being adopted by whichever unit opens next.
488
+ const stale = this.#sessionProbeTickets.get(sessionID);
489
+ if (stale !== undefined) {
490
+ this.#sessionProbeTickets.delete(sessionID);
491
+ this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
492
+ }
493
+ let probeTicket;
494
+ try {
495
+ const admission = await this.#admit(settings, sessionID);
496
+ probeTicket = admission.probeTicket;
497
+ const wrapped = schedulerForMode(admission.decision, mode);
498
+ if (wrapped.kind === "apply") {
499
+ await this.#ops.switchModel(sessionID, wrapped.model);
500
+ }
501
+ // A pin is not a tier — receipts only carry a tier for tiered routing.
502
+ const tier = routingTier(admission.routing);
503
+ await this.#recordReceipt({
504
+ sessionID,
505
+ ...(tier !== undefined ? { tier } : {}),
506
+ decision: wrapped,
507
+ });
508
+ this.#promptAdmission.set(sessionID, { ...(tier !== undefined ? { tier } : {}) });
509
+ // The prompt-admitted probe episode is the turn that follows.
510
+ if (wrapped.kind === "apply" && probeTicket !== undefined) {
511
+ this.#sessionProbeTickets.set(sessionID, probeTicket);
512
+ }
513
+ }
514
+ catch (error) {
515
+ // A probe admitted for this turn is lost when the admission fails (the
516
+ // switch threw): no unit will ever carry the ticket, so re-arm it.
517
+ if (probeTicket !== undefined) {
518
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
519
+ }
520
+ // Fail-open: no marker → execution-started re-routes the unit itself.
521
+ logFailOpen("admission", error);
522
+ await this.#recordReceipt({
523
+ sessionID,
524
+ decision: { kind: "defer", reason: "routing-defer", detail: "internal error" },
525
+ });
526
+ }
527
+ }
528
+ async #onExecutionStarted(report, settings) {
529
+ const opened = this.#tracker.apply({
530
+ kind: "execution-started",
531
+ sessionID: report.sessionID,
532
+ at: report.at,
533
+ });
534
+ const unit = opened.unit;
535
+ // The prompt-hook marker belongs to the unit this report OPENS, so read
536
+ // it before the close cleanup: a superseding start would otherwise
537
+ // discard it and re-route an already-admitted turn.
538
+ const admitted = this.#promptAdmission.get(report.sessionID);
539
+ // A superseding start closes the previous episode — release its
540
+ // per-unit bookkeeping (veto budget) too.
541
+ this.#afterUnitClose(opened);
542
+ // The prompt hook already admitted and applied for this unit — carry the
543
+ // tier into escalation context and skip re-routing.
544
+ if (admitted !== undefined) {
545
+ this.#promptAdmission.delete(report.sessionID);
546
+ this.#continuationTiers.delete(report.sessionID);
547
+ if (admitted.tier !== undefined)
548
+ this.#admissionTiers.set(unit.id, admitted.tier);
549
+ // The prompt-time admission receipt was recorded before this unit
550
+ // existed; adopt it so outcomes attach to it like any unit receipt.
551
+ try {
552
+ await this.#receipts.adoptUnit(report.sessionID, unit.id);
553
+ }
554
+ catch (error) {
555
+ logFailOpen("receipts", error);
556
+ }
557
+ const sessionTicket = this.#sessionProbeTickets.get(report.sessionID);
558
+ if (sessionTicket !== undefined) {
559
+ this.#sessionProbeTickets.delete(report.sessionID);
560
+ this.#probeTickets.set(unit.id, sessionTicket);
561
+ }
562
+ return;
563
+ }
564
+ // A probe admitted while computing a veto's recovery belongs to the
565
+ // continuation episode that follows — the first start on the session
566
+ // after the veto.
567
+ const carriedTicket = this.#sessionProbeTickets.get(report.sessionID);
568
+ if (carriedTicket !== undefined) {
569
+ this.#sessionProbeTickets.delete(report.sessionID);
570
+ this.#probeTickets.set(unit.id, carriedTicket);
571
+ }
572
+ try {
573
+ await this.#routeAdmission(unit, settings, this.#modeFor(unit.sessionID, settings));
574
+ }
575
+ catch (error) {
576
+ logFailOpen("admission", error);
577
+ await this.#recordReceipt({
578
+ sessionID: unit.sessionID,
579
+ unitID: unit.id,
580
+ decision: { kind: "defer", reason: "routing-defer", detail: "internal error" },
581
+ });
582
+ }
583
+ }
584
+ /** Fresh admission: routing ladder → scheduler, raw (unwrapped). */
585
+ async #admit(settings, sessionID) {
586
+ const routing = resolveRouting({
587
+ ...(settings.pin !== null ? { pin: settings.pin } : {}),
588
+ // The agent-declared tier request (adaptive_tier tool) outranks the
589
+ // role hints but stays below the pin (ADP-002 ladder).
590
+ ...(this.#tierRequests.has(sessionID)
591
+ ? { tierRequest: this.#tierRequests.get(sessionID) }
592
+ : {}),
593
+ // The agent-role hint is the session's last observed agent — the host
594
+ // reports it on every model request (ADP-002 level 3).
595
+ ...(this.#agents.has(sessionID) ? { agent: this.#agents.get(sessionID) } : {}),
596
+ agentTiers: settings.agentTiers,
597
+ configuredTiers: configuredTiers(settings),
598
+ });
599
+ const tierCandidates = routing.kind === "tier" ? settings.tiers[routing.tier] : [];
600
+ const inventory = await listCandidates(this.#options.model);
601
+ // Capacity sensors are pull-based: refresh before assessing so the
602
+ // admission decision sees current readings.
603
+ await this.#capacity.refresh();
604
+ const health = {};
605
+ const capacity = {};
606
+ // Tickets issued by this assessment sweep (see the re-arm below).
607
+ const issued = [];
608
+ for (const candidate of inventory) {
609
+ const ref = candidateRef(candidate);
610
+ const mapKey = schedulerHealthKey(ref);
611
+ const assessment = this.#health.assess(refKey(ref));
612
+ health[mapKey] = assessment;
613
+ if (assessment.kind === "probe")
614
+ issued.push(assessment.ticket);
615
+ const capacityAssessment = this.#capacity.assessment(refKey(ref));
616
+ if (capacityAssessment.kind === "estimate")
617
+ capacity[mapKey] = capacityAssessment.signals;
618
+ }
619
+ const input = {
620
+ routing,
621
+ // The host exposes no per-episode demand source in the v2 wiring
622
+ // (no tool/modality/context requirement reaches the plugin), so the
623
+ // compatibility gate runs on availability metadata only. Documented
624
+ // limitation — see TEST_STRATEGY "Compatibility gate demand".
625
+ demand: {},
626
+ inventory,
627
+ tierCandidates,
628
+ health,
629
+ capacity,
630
+ };
631
+ const decision = admit(input);
632
+ // Assess admits a half-open probe for every due breaker it touches —
633
+ // including candidates this decision passed over (a ready sibling wins)
634
+ // or dispatched nothing for (defer/park). An issued-but-undispatched
635
+ // probe would wedge its resource in half_open: no unit ever carries the
636
+ // ticket, so nothing reports it and the resource never recovers. Re-arm
637
+ // each unselected ticket with the breaker's same backoff window — a
638
+ // probe that never ran is no health evidence.
639
+ const selectedId = decision.kind === "apply" ? decision.evidence.probeId : undefined;
640
+ let probeTicket;
641
+ for (const ticket of issued) {
642
+ if (selectedId !== undefined && ticket.id === selectedId) {
643
+ probeTicket = ticket;
644
+ }
645
+ else {
646
+ this.#health.reportProbe(ticket, { success: false, category: "user_cancelled" });
647
+ }
648
+ }
649
+ return { routing, decision, ...(probeTicket !== undefined ? { probeTicket } : {}) };
650
+ }
651
+ async #routeAdmission(unit, settings, mode) {
652
+ const { routing, decision, probeTicket } = await this.#admit(settings, unit.sessionID);
653
+ // The tier the unit was admitted under is escalation context (ADP-015);
654
+ // recorded for every tiered routing outcome (including a defer, which
655
+ // still names the tier it could not honor).
656
+ if (routing.kind !== "pin") {
657
+ this.#admissionTiers.set(unit.id, routing.tier);
658
+ }
659
+ const wrapped = schedulerForMode(decision, mode);
660
+ if (wrapped.kind === "apply") {
661
+ try {
662
+ await this.#applyModel(unit, wrapped);
663
+ }
664
+ catch (error) {
665
+ // The episode cannot carry the probe when the switch itself failed —
666
+ // re-arm the ticket instead of wedging the resource half-open.
667
+ if (probeTicket !== undefined) {
668
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
669
+ }
670
+ throw error;
671
+ }
672
+ if (probeTicket !== undefined)
673
+ this.#probeTickets.set(unit.id, probeTicket);
674
+ return;
675
+ }
676
+ if (probeTicket !== undefined) {
677
+ // OBSERVE wraps an apply — including a probe admission — without
678
+ // running it: no episode will ever report the ticket, so re-arm it.
679
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
680
+ }
681
+ // Observe-only records the identical decision; defer and park leave the
682
+ // episode on OpenCode's own model (admission cannot hold an episode).
683
+ const tier = routingTier(routing);
684
+ await this.#recordReceipt({
685
+ sessionID: unit.sessionID,
686
+ unitID: unit.id,
687
+ ...(tier !== undefined ? { tier } : {}),
688
+ decision: wrapped,
689
+ });
690
+ }
691
+ /** CONTROL-mode application at the admission boundary; OBSERVE never reaches here. */
692
+ async #applyModel(unit, decision) {
693
+ await this.#ops.switchModel(unit.sessionID, decision.model);
694
+ await this.#recordReceipt({
695
+ sessionID: unit.sessionID,
696
+ unitID: unit.id,
697
+ ...(decision.tier !== undefined ? { tier: decision.tier } : {}),
698
+ decision,
699
+ });
700
+ }
701
+ // ---------- unit termination ----------
702
+ async #onExecutionSucceeded(report) {
703
+ const unit = this.#tracker.active(report.sessionID);
704
+ const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
705
+ const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
706
+ const closed = this.#tracker.apply({
707
+ kind: "execution-succeeded",
708
+ sessionID: report.sessionID,
709
+ at: report.at,
710
+ });
711
+ this.#afterUnitClose(closed);
712
+ if (model !== undefined) {
713
+ this.#observeHealth({ kind: "success", key: refKey(model) });
714
+ }
715
+ if (unit !== undefined) {
716
+ if (probeTicket !== undefined) {
717
+ this.#probeTickets.delete(unit.id);
718
+ this.#reportProbeOutcome(probeTicket, { success: true });
719
+ }
720
+ await this.#attachOutcome(unit, { status: "succeeded" });
721
+ }
722
+ }
723
+ async #onExecutionFailed(report, settings) {
724
+ const unit = this.#tracker.active(report.sessionID);
725
+ const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
726
+ const unitID = unit?.id;
727
+ // Budget and tier are read before the close clears the per-unit maps.
728
+ const admissionTier = unitID !== undefined ? this.#admissionTiers.get(unitID) : undefined;
729
+ const escalationUsed = unitID !== undefined ? (this.#escalations.get(unitID) ?? 0) : 0;
730
+ // Tool evidence is captured pre-close like the other per-unit maps.
731
+ const toolEvidence = unitID !== undefined ? (this.#escalationEvidence.get(unitID) ?? []) : [];
732
+ const probeTicket = unitID !== undefined ? this.#probeTickets.get(unitID) : undefined;
733
+ const closed = this.#tracker.apply({
734
+ kind: "execution-failed",
735
+ sessionID: report.sessionID,
736
+ at: report.at,
737
+ error: report.error,
738
+ });
739
+ this.#afterUnitClose(closed);
740
+ const classification = classifyFailure({
741
+ structured: {
742
+ type: report.error.type,
743
+ ...(report.error.retryAfterMs !== undefined
744
+ ? { retryAfterMs: report.error.retryAfterMs }
745
+ : {}),
746
+ },
747
+ status: report.error.status,
748
+ // Terminal classification may use the bounded textual last resort
749
+ // (content-policy rejections arrive as free text, not error types).
750
+ // The retry hook deliberately omits text: it must stay conservative.
751
+ text: report.error.message,
752
+ });
753
+ if (model !== undefined) {
754
+ this.#observeHealth(failureEvent(refKey(model), classification));
755
+ }
756
+ if (unit !== undefined) {
757
+ await this.#attachOutcome(unit, { status: "failed", category: classification.category });
758
+ if (probeTicket !== undefined) {
759
+ this.#probeTickets.delete(unit.id);
760
+ this.#reportProbeOutcome(probeTicket, { success: false, category: classification.category });
761
+ }
762
+ }
763
+ await this.#escalateOnFailure(report, unitID, model, classification, admissionTier, escalationUsed, toolEvidence, settings);
764
+ }
765
+ /**
766
+ * Escalation targets must be usable: the scheduler contract hands
767
+ * planEscalation "already gated" resources, so the wiring filters the next
768
+ * tier's configured refs to candidates that exist in the inventory, pass
769
+ * the compatibility gate, are not health-blocked, and differ from the
770
+ * model that just failed. Evaluated WITHOUT probe admission (evaluate,
771
+ * not assess) — gating an escalation must never flip a breaker or strand
772
+ * an unowned ticket.
773
+ */
774
+ async #eligibleEscalationTargets(refs, failed) {
775
+ let inventory;
776
+ try {
777
+ inventory = await listCandidates(this.#options.model);
778
+ }
779
+ catch (error) {
780
+ // No inventory means no verifiable target: escalating would switch
781
+ // blind, so the budget is kept instead of burned.
782
+ logFailOpen("escalation", error);
783
+ return [];
784
+ }
785
+ const eligible = [];
786
+ for (const ref of refs) {
787
+ if (sameModelRef(ref, failed))
788
+ continue;
789
+ const candidate = inventory.find((entry) => sameModelRef(candidateRef(entry), ref));
790
+ if (candidate === undefined)
791
+ continue;
792
+ if (!checkCompatibility(candidate, {}).compatible)
793
+ continue;
794
+ if (this.#health.evaluate(refKey(ref)).kind !== "available")
795
+ continue;
796
+ eligible.push(ref);
797
+ }
798
+ return eligible;
799
+ }
800
+ /**
801
+ * Escalation on a terminally failed unit (ADP-015..017): structured
802
+ * capability evidence only, same-model variant first then next tier,
803
+ * finite budget. Control continues the interrupted work on the escalated
804
+ * resource; Observe records the identical decision; defer receipts stand.
805
+ */
806
+ async #escalateOnFailure(report, unitID, model, classification, admissionTier, escalationUsed, toolEvidence, settings) {
807
+ if (unitID === undefined ||
808
+ model === undefined ||
809
+ admissionTier === undefined ||
810
+ !CAPABILITY_EVIDENCE_CATEGORIES.includes(classification.category)) {
811
+ return;
812
+ }
813
+ const next = nextTier(admissionTier);
814
+ const evidence = [
815
+ ...toolEvidence,
816
+ { kind: "repeated-failure", detail: classification.category },
817
+ ];
818
+ const nextResources = next !== undefined ? settings.tiers[next] : [];
819
+ const eligible = await this.#eligibleEscalationTargets(nextResources, model);
820
+ const plan = planEscalation({
821
+ currentModel: model,
822
+ currentTier: admissionTier,
823
+ nextTierResources: eligible,
824
+ evidence,
825
+ budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_UNIT },
826
+ });
827
+ const wrapped = schedulerForMode(plan, this.#modeFor(report.sessionID, settings));
828
+ if (wrapped.kind === "escalate") {
829
+ this.#escalations.set(unitID, escalationUsed + 1);
830
+ // Recorded unit-less on purpose: the receipt describes the CONTINUATION
831
+ // decision, so the continuation unit adopts it and its outcome attaches
832
+ // here (the failed unit's own outcome attaches to its apply receipt).
833
+ await this.#recordReceipt({
834
+ sessionID: report.sessionID,
835
+ tier: wrapped.tier,
836
+ decision: wrapped,
837
+ });
838
+ // Continue the interrupted work on the escalated resource; the unit is
839
+ // terminally failed, so an ops failure is only logged (never parked).
840
+ try {
841
+ await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier);
842
+ }
843
+ catch (error) {
844
+ logFailOpen("escalation-ops", error);
845
+ }
846
+ return;
847
+ }
848
+ await this.#recordReceipt({
849
+ sessionID: report.sessionID,
850
+ tier: plan.kind === "escalate" ? plan.tier : admissionTier,
851
+ decision: wrapped,
852
+ });
853
+ }
854
+ async #onExecutionInterrupted(report) {
855
+ const reason = interruptReason(report.reason);
856
+ if (reason === undefined) {
857
+ logFailOpen("interrupt", new Error(`unknown interruption reason: ${report.reason.slice(0, 32)}`));
858
+ return;
859
+ }
860
+ const unit = this.#tracker.active(report.sessionID);
861
+ const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
862
+ const closed = this.#tracker.apply({
863
+ kind: "execution-interrupted",
864
+ sessionID: report.sessionID,
865
+ at: report.at,
866
+ reason,
867
+ });
868
+ this.#afterUnitClose(closed);
869
+ // User and host interruptions never produce health evidence (the
870
+ // classifier maps them outside the infrastructure vocabulary).
871
+ if (unit !== undefined) {
872
+ // An interrupted probe is no evidence about resource health: re-arm the
873
+ // breaker's same window rather than doubling it.
874
+ if (probeTicket !== undefined) {
875
+ this.#probeTickets.delete(unit.id);
876
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
877
+ }
878
+ await this.#attachOutcome(unit, { status: "interrupted", reason });
879
+ }
880
+ }
881
+ /**
882
+ * One probe ticket's resolution at its episode's termination. A probe that
883
+ * ends without a reported outcome (superseded, lost) would wedge the
884
+ * resource in half_open, so the close cleanup reports those as
885
+ * interruptions — re-arming the same backoff window, never doubling it.
886
+ */
887
+ #reportProbeOutcome(ticket, outcome) {
888
+ // reportProbe is total and synchronous; validation failures (stale or
889
+ // unknown ticket) are reported by its boolean return, not by throwing.
890
+ this.#health.reportProbe(ticket, outcome);
891
+ }
892
+ /** Bounded per-unit bookkeeping once the tracker closed the unit. */
893
+ #afterUnitClose(closed) {
894
+ const unitID = closedUnitID(closed);
895
+ if (unitID !== undefined) {
896
+ this.#vetoes.delete(unitID);
897
+ this.#escalations.delete(unitID);
898
+ this.#admissionTiers.delete(unitID);
899
+ this.#escalationEvidence.delete(unitID);
900
+ const leftover = this.#probeTickets.get(unitID);
901
+ if (leftover !== undefined) {
902
+ // The explicit outcome reporters run before this cleanup; reaching
903
+ // here means the unit ended without one (superseded or lost).
904
+ this.#reportProbeOutcome(leftover, { success: false, category: "user_cancelled" });
905
+ this.#probeTickets.delete(unitID);
906
+ }
907
+ }
908
+ if (closed.kind === "opened" || closed.kind === "closed") {
909
+ // Per-session flags for a unit that will never open again must not
910
+ // leak: a stale continuation mark would swallow a real user prompt's
911
+ // admission; a stale admission marker would skip routing. The guard
912
+ // keeps the wipe out of an in-flight continuation's mark window (its
913
+ // own close events are pumped concurrently with the mark).
914
+ const sid = closed.unit.sessionID;
915
+ // A continuation tier intent pins the marker: the failed unit's close
916
+ // event can arrive after the continuation's prompt hook set it, and
917
+ // wiping then would re-route the continuation unit.
918
+ if ((this.#continuationsInFlight.get(sid) ?? 0) === 0 && !this.#continuationTiers.has(sid)) {
919
+ this.#continuationPending.delete(sid);
920
+ this.#promptAdmission.delete(sid);
921
+ }
922
+ }
923
+ }
924
+ // ---------- http evidence ----------
925
+ async #onHttpResponse(report) {
926
+ if (report.capturedHeaders !== undefined) {
927
+ this.#codex.observe(report.capturedHeaders, this.#options.clock());
928
+ await this.#syncCapacityMirror();
929
+ }
930
+ if (report.status < 400)
931
+ return;
932
+ const unit = this.#tracker.active(report.sessionID);
933
+ const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
934
+ if (model === undefined)
935
+ return;
936
+ const classification = classifyFailure({
937
+ status: report.status,
938
+ ...(report.retryAfterHeader !== undefined
939
+ ? { headers: { "retry-after": report.retryAfterHeader } }
940
+ : {}),
941
+ });
942
+ this.#observeHealth(failureEvent(refKey(model), classification));
943
+ }
944
+ // ---------- retry hook ----------
945
+ async #handleRetry(input, settings) {
946
+ const classification = classifyFailure({
947
+ structured: {
948
+ type: input.error.type,
949
+ ...(input.error.retryAfterMs !== undefined
950
+ ? { retryAfterMs: input.error.retryAfterMs }
951
+ : {}),
952
+ },
953
+ status: input.error.status,
954
+ });
955
+ this.#observeHealth(failureEvent(refKey(input.model), classification));
956
+ // Recovery = fresh admission; the failed resource is already health-blocked.
957
+ let recovery = { kind: "none" };
958
+ let routingTier;
959
+ let probeTicket;
960
+ let admission;
961
+ try {
962
+ const { routing, decision, probeTicket: ticket, } = await this.#admit(settings, input.sessionID);
963
+ routingTier = routing.kind === "pin" ? undefined : routing.tier;
964
+ if (decision.kind === "apply") {
965
+ admission = decision;
966
+ recovery = {
967
+ kind: "switch",
968
+ model: decision.model,
969
+ ...(decision.evidence.probeId !== undefined
970
+ ? { probeId: decision.evidence.probeId }
971
+ : {}),
972
+ };
973
+ probeTicket = ticket;
974
+ }
975
+ }
976
+ catch (error) {
977
+ logFailOpen("recovery", error);
978
+ }
979
+ const unitID = this.#tracker.active(input.sessionID)?.id;
980
+ const vetoesUsed = unitID !== undefined ? (this.#vetoes.get(unitID) ?? 0) : 0;
981
+ const core = decideRetry({
982
+ classification,
983
+ failingModel: input.model,
984
+ native: input.decision,
985
+ recovery,
986
+ vetoesUsed,
987
+ });
988
+ const wrapped = retryForMode(core, this.#modeFor(input.sessionID, settings));
989
+ if (wrapped.kind !== "veto") {
990
+ // OBSERVE (and a declined veto) never runs the recovery episode, so an
991
+ // admitted probe has no continuation to report it — re-arm it here or
992
+ // the resource wedges half-open.
993
+ if (probeTicket !== undefined) {
994
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
995
+ }
996
+ return;
997
+ }
998
+ input.decision = { retry: false };
999
+ if (unitID !== undefined) {
1000
+ this.#vetoes.set(unitID, vetoesUsed + 1);
1001
+ }
1002
+ // A probe admitted by this recovery belongs to the continuation episode
1003
+ // that ownRecovery is about to start; its termination reports the probe.
1004
+ if (probeTicket !== undefined) {
1005
+ // The session holds one pending probe slot. A still-pending older
1006
+ // ticket belongs to a recovery whose continuation has not started (its
1007
+ // episode can no longer claim this slot) — re-arm it before replacing.
1008
+ const stale = this.#sessionProbeTickets.get(input.sessionID);
1009
+ if (stale !== undefined && stale !== probeTicket) {
1010
+ this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
1011
+ }
1012
+ this.#sessionProbeTickets.set(input.sessionID, probeTicket);
1013
+ }
1014
+ // The continuation episode is not re-admitted — its prompt hook is
1015
+ // consumed as an owned continuation — so the recovery admission is
1016
+ // recorded here, unit-less; the unit that opens next adopts this receipt
1017
+ // and carries the outcome.
1018
+ // A veto implies the admission was an apply: the recovery option exists
1019
+ // only when the fresh admission selected a resource, so the assertion
1020
+ // cannot be undefined on this path.
1021
+ await this.#recordReceipt({
1022
+ sessionID: input.sessionID,
1023
+ tier: routingTier,
1024
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a veto implies the admission was an apply (the recovery exists only then)
1025
+ decision: admission,
1026
+ });
1027
+ void this.#ownRecovery(input.sessionID, wrapped.model, routingTier)
1028
+ .then(() => undefined)
1029
+ .catch(async (error) => {
1030
+ logFailOpen("recovery-ops", error);
1031
+ // The recovery rejected before (or while) delivering its continuation
1032
+ // prompt, so no unit will ever carry the probe it admitted — re-arm
1033
+ // it instead of wedging the resource half-open.
1034
+ if (probeTicket !== undefined &&
1035
+ this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
1036
+ this.#sessionProbeTickets.delete(input.sessionID);
1037
+ this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
1038
+ }
1039
+ if (unitID === undefined || routingTier === undefined)
1040
+ return;
1041
+ try {
1042
+ await this.#parked.park({
1043
+ id: unitID,
1044
+ sessionID: input.sessionID,
1045
+ tier: routingTier,
1046
+ reason: "all-blocked",
1047
+ detail: "continuation failed",
1048
+ parkedAt: this.#options.clock(),
1049
+ });
1050
+ }
1051
+ catch (parkError) {
1052
+ logFailOpen("parked", parkError);
1053
+ }
1054
+ });
1055
+ }
1056
+ /**
1057
+ * Owned horizontal recovery: settle → switch → continue from session
1058
+ * state. `tier` is the continuation unit's routing tier — recorded so the
1059
+ * continuation's prompt hook re-arms the admission marker (the unit must
1060
+ * not be re-routed: that would discard the recovery/escalation decision).
1061
+ */
1062
+ async #ownRecovery(sessionID, model, tier) {
1063
+ if (tier !== undefined)
1064
+ this.#continuationTiers.set(sessionID, tier);
1065
+ const alive = await this.#isSessionAlive(sessionID);
1066
+ const recovery = { kind: "switch", model };
1067
+ const action = planContinuation({
1068
+ recovery,
1069
+ continuationPossible: alive,
1070
+ });
1071
+ try {
1072
+ await executeContinuation({ sessionID, action, ops: this.#ops });
1073
+ }
1074
+ catch (error) {
1075
+ // The continuation prompt never reached the host, so no unit will open
1076
+ // to consume the tier intent — drop it or it would pin the marker
1077
+ // wipes and mislabel a later user prompt's admission.
1078
+ if (tier !== undefined)
1079
+ this.#continuationTiers.delete(sessionID);
1080
+ throw error;
1081
+ }
1082
+ }
1083
+ // ---------- parked-work resume (safe boundary: idle) ----------
1084
+ async #onIdle(settings) {
1085
+ const records = this.#parked.list();
1086
+ // Override holders join the alive sweep: a dead session's override is
1087
+ // pruned here (its per-session steering is worthless once the session is
1088
+ // gone), in the same sweep that clears the other per-session maps.
1089
+ const overrideIDs = this.#sessionModes.snapshot().map((record) => record.sessionID);
1090
+ if (records.length === 0 && overrideIDs.length === 0 && this.#tierRequests.size === 0)
1091
+ return;
1092
+ // OFF sessions are not supervised: their parked records are neither
1093
+ // dropped nor resumed, and no alive probe runs for them.
1094
+ const supervised = new Set(records
1095
+ .map((record) => record.sessionID)
1096
+ .filter((sessionID) => this.#modeFor(sessionID, settings) !== "off"));
1097
+ const alive = new Map();
1098
+ for (const sessionID of new Set([...supervised, ...overrideIDs])) {
1099
+ alive.set(sessionID, await this.#isSessionAlive(sessionID));
1100
+ }
1101
+ // Tier-request holders join the alive sweep too: a dead session's sticky
1102
+ // tier request dies with its session, like the other per-session maps.
1103
+ for (const sessionID of this.#tierRequests.keys()) {
1104
+ if (!alive.has(sessionID))
1105
+ alive.set(sessionID, await this.#isSessionAlive(sessionID));
1106
+ }
1107
+ const plan = selectResumable(records, {
1108
+ tierConfigured: (tier) => configuredTiers(settings).includes(tier),
1109
+ sessionAlive: (sessionID) => alive.get(sessionID) === true,
1110
+ });
1111
+ for (const dropped of plan.drop) {
1112
+ if (this.#modeFor(dropped.sessionID, settings) === "off")
1113
+ continue;
1114
+ await this.#dropParked(dropped);
1115
+ }
1116
+ // Tier requests die with their session, like the other per-session maps.
1117
+ for (const sessionID of this.#tierRequests.keys()) {
1118
+ if (alive.get(sessionID) === false)
1119
+ this.#tierRequests.delete(sessionID);
1120
+ }
1121
+ // At most ONE continuation per session per sweep: prompting a session
1122
+ // with an active unit would supersede the user's in-flight episode (not
1123
+ // a safe boundary), and several back-to-back continuation prompts could
1124
+ // redo work an earlier one already completed (ADP-023).
1125
+ const attempted = new Set();
1126
+ for (const record of plan.attempt) {
1127
+ if (this.#modeFor(record.sessionID, settings) === "off")
1128
+ continue;
1129
+ if (this.#tracker.active(record.sessionID) !== undefined)
1130
+ continue;
1131
+ if (attempted.has(record.sessionID))
1132
+ continue;
1133
+ attempted.add(record.sessionID);
1134
+ await this.#resumeAttempt(record, settings);
1135
+ }
1136
+ for (const [sessionID, isAlive] of alive) {
1137
+ if (!isAlive) {
1138
+ this.#continuationTiers.delete(sessionID);
1139
+ this.#promptAdmission.delete(sessionID);
1140
+ this.#continuationPending.delete(sessionID);
1141
+ if (this.#sessionModes.get(sessionID) !== null) {
1142
+ try {
1143
+ await this.#sessionModes.set(sessionID, null);
1144
+ }
1145
+ catch (error) {
1146
+ logFailOpen("session-modes", error);
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ async #dropParked(record) {
1153
+ try {
1154
+ await this.#parked.resume(record.id);
1155
+ }
1156
+ catch (error) {
1157
+ logFailOpen("parked", error);
1158
+ }
1159
+ }
1160
+ /** One parked record's fresh routing attempt at the idle boundary. */
1161
+ async #resumeAttempt(record, settings) {
1162
+ // The parked session resumes under ITS effective mode, not the idle
1163
+ // report's session's mode.
1164
+ const mode = this.#modeFor(record.sessionID, settings);
1165
+ let decision;
1166
+ try {
1167
+ decision = (await this.#admit(settings, record.sessionID)).decision;
1168
+ }
1169
+ catch (error) {
1170
+ logFailOpen("resume", error);
1171
+ return;
1172
+ }
1173
+ if (mode === "observe") {
1174
+ await this.#recordReceipt({
1175
+ sessionID: record.sessionID,
1176
+ unitID: record.id,
1177
+ tier: record.tier,
1178
+ decision: { kind: "observe-only", would: decision },
1179
+ });
1180
+ return;
1181
+ }
1182
+ if (decision.kind !== "apply") {
1183
+ // Still no usable resource (or the routing deferred) — stay parked.
1184
+ await this.#recordReceipt({
1185
+ sessionID: record.sessionID,
1186
+ unitID: record.id,
1187
+ tier: record.tier,
1188
+ decision,
1189
+ });
1190
+ return;
1191
+ }
1192
+ const popped = await this.#popParked(record);
1193
+ if (popped === undefined)
1194
+ return;
1195
+ // The resume continuation is not re-admitted (owned continuation), so
1196
+ // the freshly routed admission is recorded here, unit-less; the unit
1197
+ // that opens next adopts it and carries the outcome.
1198
+ await this.#recordReceipt({
1199
+ sessionID: record.sessionID,
1200
+ tier: record.tier,
1201
+ decision,
1202
+ });
1203
+ // The resume continuation is an owned recovery: settle first, apply the
1204
+ // freshly routed model, then prompt — the episode's first LLM call must
1205
+ // not hit the model the work was parked away from.
1206
+ try {
1207
+ await this.#ownRecovery(record.sessionID, decision.model, record.tier);
1208
+ }
1209
+ catch (error) {
1210
+ logFailOpen("resume", error);
1211
+ await this.#rePark(record);
1212
+ }
1213
+ }
1214
+ async #popParked(record) {
1215
+ try {
1216
+ return await this.#parked.resume(record.id);
1217
+ }
1218
+ catch (error) {
1219
+ logFailOpen("parked", error);
1220
+ return undefined;
1221
+ }
1222
+ }
1223
+ async #rePark(record) {
1224
+ try {
1225
+ await this.#parked.park({
1226
+ ...record,
1227
+ attempts: (record.attempts ?? 0) + 1,
1228
+ parkedAt: this.#options.clock(),
1229
+ });
1230
+ }
1231
+ catch (error) {
1232
+ logFailOpen("parked", error);
1233
+ }
1234
+ }
1235
+ // ---------- shared helpers ----------
1236
+ async #isSessionAlive(sessionID) {
1237
+ try {
1238
+ await this.#options.session.get({ sessionID });
1239
+ return true;
1240
+ }
1241
+ catch {
1242
+ return false;
1243
+ }
1244
+ }
1245
+ async #recordReceipt(input) {
1246
+ try {
1247
+ await this.#receipts.record({ at: this.#options.clock(), ...input });
1248
+ }
1249
+ catch (error) {
1250
+ logFailOpen("receipts", error);
1251
+ }
1252
+ }
1253
+ async #attachOutcome(unit, outcome) {
1254
+ try {
1255
+ await this.#receipts.attachOutcome({ sessionID: unit.sessionID, unitID: unit.id }, outcome);
1256
+ }
1257
+ catch (error) {
1258
+ logFailOpen("receipts", error);
1259
+ }
1260
+ }
1261
+ #observeHealth(event) {
1262
+ this.#health.observe(event);
1263
+ // Persist the record mirror fire-and-forget: the status surface reads
1264
+ // persisted records so a serving instance without the event-pump hooks
1265
+ // still reports true health. Failures are fail-open (ADP-031).
1266
+ const records = this.#health.snapshot();
1267
+ void this.#healthRecords.replace(records).catch((error) => {
1268
+ logFailOpen("health", error);
1269
+ });
1270
+ }
1271
+ /**
1272
+ * Persist the capacity mirror after a passive observation (same instance
1273
+ * split rationale as the health mirror): the status surface reads persisted
1274
+ * readings so a serving instance without the event-pump hooks still reports
1275
+ * sensed capacity. Failures are fail-open (ADP-031).
1276
+ */
1277
+ async #syncCapacityMirror() {
1278
+ try {
1279
+ await this.#capacity.refresh();
1280
+ const entries = [];
1281
+ for (const sensor of this.#capacity.snapshot()) {
1282
+ if (sensor.latest !== null) {
1283
+ entries.push({ key: sensor.resource, reading: sensor.latest });
1284
+ }
1285
+ }
1286
+ await this.#capacityStore.replace(entries);
1287
+ }
1288
+ catch (error) {
1289
+ logFailOpen("capacity", error);
1290
+ }
1291
+ }
1292
+ }
1293
+ _a = Supervisor;
1294
+ /** A pin is not a tier — receipts only carry a tier for tiered routing. */
1295
+ function routingTier(routing) {
1296
+ return routing.kind === "pin" ? undefined : routing.tier;
1297
+ }
1298
+ /** The unit a tracker result closed or superseded, if any. */
1299
+ function closedUnitID(result) {
1300
+ switch (result.kind) {
1301
+ case "closed":
1302
+ return result.unit.id;
1303
+ case "opened":
1304
+ return result.superseded?.id;
1305
+ default:
1306
+ return undefined;
1307
+ }
1308
+ }
1309
+ /** ModelRef for a normalized candidate. */
1310
+ function candidateRef(candidate) {
1311
+ return {
1312
+ providerID: candidate.providerID,
1313
+ id: candidate.modelID,
1314
+ ...(candidate.variant !== undefined ? { variant: candidate.variant } : {}),
1315
+ };
1316
+ }
1317
+ /** ResourceKey for a model ref (no account level is observable in v1). */
1318
+ function refKey(ref) {
1319
+ // The host reports base-model calls with variant "default" (verified on
1320
+ // 2.2.5), while the inventory enumerates the bare model plus its named
1321
+ // variants. Taken literally, a "default" key would orphan every
1322
+ // unvarianted failure record on a key no admission ever assesses — health
1323
+ // and breaker state would be invisible to routing.
1324
+ const variant = ref.variant === "default" ? undefined : ref.variant;
1325
+ return {
1326
+ providerID: ref.providerID,
1327
+ modelID: ref.id,
1328
+ ...(variant !== undefined ? { variantID: variant } : {}),
1329
+ };
1330
+ }
1331
+ /** Health failure event from a classification (bounded structured evidence). */
1332
+ function failureEvent(key, classification) {
1333
+ return {
1334
+ kind: "failure",
1335
+ key,
1336
+ category: classification.category,
1337
+ definitive: classification.definitive,
1338
+ ...(classification.retry.retryAfterMs !== undefined
1339
+ ? { retryAfterMs: classification.retry.retryAfterMs }
1340
+ : {}),
1341
+ };
1342
+ }
1343
+ //# sourceMappingURL=supervisor.js.map