@coool/opencode-adaptive 1.0.0 → 1.2.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 (381) hide show
  1. package/README.md +8 -7
  2. package/dist/adapters/codex-capacity/index.d.ts +0 -1
  3. package/dist/adapters/codex-capacity/index.js.map +1 -1
  4. package/dist/adapters/codex-capacity/parse.d.ts +2 -2
  5. package/dist/adapters/codex-capacity/parse.js +19 -3
  6. package/dist/adapters/codex-capacity/parse.js.map +1 -1
  7. package/dist/adapters/codex-capacity/reading.d.ts +0 -1
  8. package/dist/adapters/codex-capacity/reading.js.map +1 -1
  9. package/dist/adapters/codex-capacity/sensor.d.ts +0 -1
  10. package/dist/adapters/codex-capacity/sensor.js.map +1 -1
  11. package/dist/adapters/codex-capacity/types.d.ts +0 -4
  12. package/dist/adapters/codex-capacity/types.js +14 -0
  13. package/dist/adapters/codex-capacity/types.js.map +1 -1
  14. package/dist/adapters/opencode/escalation-tool.d.ts +0 -1
  15. package/dist/adapters/opencode/escalation-tool.js +5 -3
  16. package/dist/adapters/opencode/escalation-tool.js.map +1 -1
  17. package/dist/adapters/opencode/events.d.ts +0 -1
  18. package/dist/adapters/opencode/events.js.map +1 -1
  19. package/dist/adapters/opencode/hooks.d.ts +0 -1
  20. package/dist/adapters/opencode/hooks.js +24 -7
  21. package/dist/adapters/opencode/hooks.js.map +1 -1
  22. package/dist/adapters/opencode/index.d.ts +0 -1
  23. package/dist/adapters/opencode/index.js.map +1 -1
  24. package/dist/adapters/opencode/inventory.d.ts +0 -1
  25. package/dist/adapters/opencode/inventory.js.map +1 -1
  26. package/dist/adapters/opencode/report.d.ts +0 -1
  27. package/dist/adapters/opencode/report.js.map +1 -1
  28. package/dist/adapters/opencode/session-ops.d.ts +0 -1
  29. package/dist/adapters/opencode/session-ops.js.map +1 -1
  30. package/dist/adapters/opencode/storage.d.ts +0 -1
  31. package/dist/adapters/opencode/storage.js.map +1 -1
  32. package/dist/adapters/opencode/tier-tool.d.ts +0 -1
  33. package/dist/adapters/opencode/tier-tool.js.map +1 -1
  34. package/dist/adapters/opencode/types.d.ts +0 -1
  35. package/dist/adapters/opencode/types.js.map +1 -1
  36. package/dist/core/compatibility/gate.d.ts +0 -3
  37. package/dist/core/compatibility/gate.js +0 -4
  38. package/dist/core/compatibility/gate.js.map +1 -1
  39. package/dist/core/compatibility/index.d.ts +0 -1
  40. package/dist/core/compatibility/index.js.map +1 -1
  41. package/dist/core/compatibility/types.d.ts +0 -1
  42. package/dist/core/compatibility/types.js.map +1 -1
  43. package/dist/core/scheduler/admit.d.ts +0 -1
  44. package/dist/core/scheduler/admit.js +22 -21
  45. package/dist/core/scheduler/admit.js.map +1 -1
  46. package/dist/core/scheduler/escalate.d.ts +0 -1
  47. package/dist/core/scheduler/escalate.js +6 -17
  48. package/dist/core/scheduler/escalate.js.map +1 -1
  49. package/dist/core/scheduler/index.d.ts +0 -1
  50. package/dist/core/scheduler/index.js.map +1 -1
  51. package/dist/core/scheduler/mode.d.ts +0 -1
  52. package/dist/core/scheduler/mode.js.map +1 -1
  53. package/dist/core/scheduler/types.d.ts +2 -9
  54. package/dist/core/scheduler/types.js +2 -1
  55. package/dist/core/scheduler/types.js.map +1 -1
  56. package/dist/core/tier-policy/index.d.ts +0 -1
  57. package/dist/core/tier-policy/index.js.map +1 -1
  58. package/dist/core/tier-policy/resolve.d.ts +0 -1
  59. package/dist/core/tier-policy/resolve.js.map +1 -1
  60. package/dist/core/tier-policy/tier-list.d.ts +0 -1
  61. package/dist/core/tier-policy/tier-list.js.map +1 -1
  62. package/dist/core/tier-policy/types.d.ts +2 -1
  63. package/dist/core/tier-policy/types.js +4 -0
  64. package/dist/core/tier-policy/types.js.map +1 -1
  65. package/dist/core/work-unit/index.d.ts +0 -1
  66. package/dist/core/work-unit/index.js.map +1 -1
  67. package/dist/core/work-unit/interruption.d.ts +0 -1
  68. package/dist/core/work-unit/interruption.js.map +1 -1
  69. package/dist/core/work-unit/tracker.d.ts +11 -4
  70. package/dist/core/work-unit/tracker.js +26 -6
  71. package/dist/core/work-unit/tracker.js.map +1 -1
  72. package/dist/core/work-unit/types.d.ts +6 -1
  73. package/dist/core/work-unit/types.js.map +1 -1
  74. package/dist/execution/continuation/execute.d.ts +0 -1
  75. package/dist/execution/continuation/execute.js.map +1 -1
  76. package/dist/execution/continuation/index.d.ts +0 -2
  77. package/dist/execution/continuation/index.js +0 -1
  78. package/dist/execution/continuation/index.js.map +1 -1
  79. package/dist/execution/continuation/plan.d.ts +0 -1
  80. package/dist/execution/continuation/plan.js +1 -3
  81. package/dist/execution/continuation/plan.js.map +1 -1
  82. package/dist/execution/continuation/prompts.d.ts +0 -1
  83. package/dist/execution/continuation/prompts.js.map +1 -1
  84. package/dist/execution/continuation/types.d.ts +0 -2
  85. package/dist/execution/continuation/types.js.map +1 -1
  86. package/dist/execution/escalation/index.d.ts +0 -1
  87. package/dist/execution/escalation/index.js.map +1 -1
  88. package/dist/execution/escalation/types.d.ts +0 -1
  89. package/dist/execution/escalation/types.js.map +1 -1
  90. package/dist/execution/index.d.ts +0 -1
  91. package/dist/execution/index.js.map +1 -1
  92. package/dist/execution/parking/index.d.ts +0 -1
  93. package/dist/execution/parking/index.js.map +1 -1
  94. package/dist/execution/parking/plan.d.ts +0 -1
  95. package/dist/execution/parking/plan.js.map +1 -1
  96. package/dist/execution/retry/decide.d.ts +0 -1
  97. package/dist/execution/retry/decide.js +1 -2
  98. package/dist/execution/retry/decide.js.map +1 -1
  99. package/dist/execution/retry/index.d.ts +0 -1
  100. package/dist/execution/retry/index.js.map +1 -1
  101. package/dist/execution/retry/types.d.ts +0 -2
  102. package/dist/execution/retry/types.js.map +1 -1
  103. package/dist/gui-experimental.cjs +1 -1
  104. package/dist/gui.d.ts +0 -1
  105. package/dist/gui.js +6 -3
  106. package/dist/gui.js.map +1 -1
  107. package/dist/health/capacity/index.d.ts +0 -1
  108. package/dist/health/capacity/index.js.map +1 -1
  109. package/dist/health/capacity/merge.d.ts +0 -1
  110. package/dist/health/capacity/merge.js.map +1 -1
  111. package/dist/health/capacity/registry.d.ts +0 -1
  112. package/dist/health/capacity/registry.js.map +1 -1
  113. package/dist/health/capacity/types.d.ts +0 -1
  114. package/dist/health/capacity/types.js.map +1 -1
  115. package/dist/health/circuit-breaker/breaker.d.ts +8 -1
  116. package/dist/health/circuit-breaker/breaker.js +18 -0
  117. package/dist/health/circuit-breaker/breaker.js.map +1 -1
  118. package/dist/health/circuit-breaker/index.d.ts +0 -1
  119. package/dist/health/circuit-breaker/index.js.map +1 -1
  120. package/dist/health/circuit-breaker/types.d.ts +0 -1
  121. package/dist/health/circuit-breaker/types.js.map +1 -1
  122. package/dist/health/classifier/classify.d.ts +0 -1
  123. package/dist/health/classifier/classify.js +13 -1
  124. package/dist/health/classifier/classify.js.map +1 -1
  125. package/dist/health/classifier/index.d.ts +0 -1
  126. package/dist/health/classifier/index.js.map +1 -1
  127. package/dist/health/classifier/types.d.ts +0 -1
  128. package/dist/health/classifier/types.js.map +1 -1
  129. package/dist/health/index.d.ts +0 -1
  130. package/dist/health/index.js.map +1 -1
  131. package/dist/health/resource-health/index.d.ts +0 -1
  132. package/dist/health/resource-health/index.js.map +1 -1
  133. package/dist/health/resource-health/key.d.ts +0 -1
  134. package/dist/health/resource-health/key.js.map +1 -1
  135. package/dist/health/resource-health/registry.d.ts +6 -1
  136. package/dist/health/resource-health/registry.js +19 -1
  137. package/dist/health/resource-health/registry.js.map +1 -1
  138. package/dist/health/resource-health/transitions.d.ts +2 -4
  139. package/dist/health/resource-health/transitions.js +10 -18
  140. package/dist/health/resource-health/transitions.js.map +1 -1
  141. package/dist/health/resource-health/types.d.ts +7 -1
  142. package/dist/health/resource-health/types.js +1 -0
  143. package/dist/health/resource-health/types.js.map +1 -1
  144. package/dist/index.d.ts +0 -6
  145. package/dist/index.js +44 -19
  146. package/dist/index.js.map +1 -1
  147. package/dist/plugin-id.d.ts +0 -1
  148. package/dist/plugin-id.js.map +1 -1
  149. package/dist/rpc/definition.d.ts +24 -1
  150. package/dist/rpc/definition.js +28 -2
  151. package/dist/rpc/definition.js.map +1 -1
  152. package/dist/rpc/handlers.d.ts +12 -24
  153. package/dist/rpc/handlers.js +22 -8
  154. package/dist/rpc/handlers.js.map +1 -1
  155. package/dist/rpc/status.d.ts +0 -1
  156. package/dist/rpc/status.js.map +1 -1
  157. package/dist/rpc.d.ts +0 -1
  158. package/dist/rpc.js.map +1 -1
  159. package/dist/state/capacity/index.d.ts +0 -1
  160. package/dist/state/capacity/index.js.map +1 -1
  161. package/dist/state/capacity/parse.d.ts +0 -1
  162. package/dist/state/capacity/parse.js +14 -2
  163. package/dist/state/capacity/parse.js.map +1 -1
  164. package/dist/state/capacity/store.d.ts +0 -1
  165. package/dist/state/capacity/store.js +16 -2
  166. package/dist/state/capacity/store.js.map +1 -1
  167. package/dist/state/capacity/types.d.ts +0 -1
  168. package/dist/state/capacity/types.js.map +1 -1
  169. package/dist/state/health/index.d.ts +0 -1
  170. package/dist/state/health/index.js.map +1 -1
  171. package/dist/state/health/parse.d.ts +0 -1
  172. package/dist/state/health/parse.js +20 -4
  173. package/dist/state/health/parse.js.map +1 -1
  174. package/dist/state/health/store.d.ts +0 -1
  175. package/dist/state/health/store.js +16 -2
  176. package/dist/state/health/store.js.map +1 -1
  177. package/dist/state/health/types.d.ts +0 -1
  178. package/dist/state/health/types.js.map +1 -1
  179. package/dist/state/parked-work/index.d.ts +0 -1
  180. package/dist/state/parked-work/index.js.map +1 -1
  181. package/dist/state/parked-work/parse.d.ts +0 -1
  182. package/dist/state/parked-work/parse.js.map +1 -1
  183. package/dist/state/parked-work/store.d.ts +0 -1
  184. package/dist/state/parked-work/store.js +20 -3
  185. package/dist/state/parked-work/store.js.map +1 -1
  186. package/dist/state/parked-work/types.d.ts +0 -1
  187. package/dist/state/parked-work/types.js.map +1 -1
  188. package/dist/state/receipts/bound.d.ts +0 -1
  189. package/dist/state/receipts/bound.js.map +1 -1
  190. package/dist/state/receipts/index.d.ts +0 -1
  191. package/dist/state/receipts/index.js.map +1 -1
  192. package/dist/state/receipts/parse.d.ts +0 -1
  193. package/dist/state/receipts/parse.js.map +1 -1
  194. package/dist/state/receipts/store.d.ts +0 -1
  195. package/dist/state/receipts/store.js +24 -4
  196. package/dist/state/receipts/store.js.map +1 -1
  197. package/dist/state/receipts/types.d.ts +0 -1
  198. package/dist/state/receipts/types.js.map +1 -1
  199. package/dist/state/session-modes/index.d.ts +0 -1
  200. package/dist/state/session-modes/index.js.map +1 -1
  201. package/dist/state/session-modes/parse.d.ts +0 -1
  202. package/dist/state/session-modes/parse.js.map +1 -1
  203. package/dist/state/session-modes/store.d.ts +0 -1
  204. package/dist/state/session-modes/store.js +15 -1
  205. package/dist/state/session-modes/store.js.map +1 -1
  206. package/dist/state/session-modes/types.d.ts +0 -1
  207. package/dist/state/session-modes/types.js.map +1 -1
  208. package/dist/state/settings/index.d.ts +0 -1
  209. package/dist/state/settings/index.js.map +1 -1
  210. package/dist/state/settings/parse.d.ts +0 -1
  211. package/dist/state/settings/parse.js.map +1 -1
  212. package/dist/state/settings/store.d.ts +0 -1
  213. package/dist/state/settings/store.js +9 -1
  214. package/dist/state/settings/store.js.map +1 -1
  215. package/dist/state/settings/types.d.ts +0 -1
  216. package/dist/state/settings/types.js.map +1 -1
  217. package/dist/state/tier-requests/index.d.ts +4 -0
  218. package/dist/state/tier-requests/index.js +4 -0
  219. package/dist/state/tier-requests/index.js.map +1 -0
  220. package/dist/state/tier-requests/parse.d.ts +3 -0
  221. package/dist/state/tier-requests/parse.js +53 -0
  222. package/dist/state/tier-requests/parse.js.map +1 -0
  223. package/dist/state/tier-requests/store.d.ts +27 -0
  224. package/dist/state/tier-requests/store.js +96 -0
  225. package/dist/state/tier-requests/store.js.map +1 -0
  226. package/dist/state/tier-requests/types.d.ts +30 -0
  227. package/dist/state/tier-requests/types.js +7 -0
  228. package/dist/state/tier-requests/types.js.map +1 -0
  229. package/dist/supervisor/index.d.ts +0 -1
  230. package/dist/supervisor/index.js.map +1 -1
  231. package/dist/supervisor/log.d.ts +0 -1
  232. package/dist/supervisor/log.js.map +1 -1
  233. package/dist/supervisor/supervisor.d.ts +23 -6
  234. package/dist/supervisor/supervisor.js +414 -111
  235. package/dist/supervisor/supervisor.js.map +1 -1
  236. package/dist/supervisor/types.d.ts +0 -1
  237. package/dist/supervisor/types.js.map +1 -1
  238. package/dist/supervisor/view.d.ts +0 -1
  239. package/dist/supervisor/view.js.map +1 -1
  240. package/dist/tui.d.ts +0 -1
  241. package/dist/tui.js +14 -10
  242. package/dist/tui.js.map +1 -1
  243. package/dist/ui/gui/api.d.ts +0 -1
  244. package/dist/ui/gui/api.js.map +1 -1
  245. package/dist/ui/gui/view.d.ts +0 -1
  246. package/dist/ui/gui/view.js.map +1 -1
  247. package/dist/ui/tui/controller.d.ts +0 -1
  248. package/dist/ui/tui/controller.js +70 -24
  249. package/dist/ui/tui/controller.js.map +1 -1
  250. package/dist/ui/tui/menu.d.ts +1 -2
  251. package/dist/ui/tui/menu.js +2 -2
  252. package/dist/ui/tui/menu.js.map +1 -1
  253. package/dist/ui/tui/options.d.ts +0 -1
  254. package/dist/ui/tui/options.js.map +1 -1
  255. package/dist/ui/tui/receipts.d.ts +0 -1
  256. package/dist/ui/tui/receipts.js +4 -1
  257. package/dist/ui/tui/receipts.js.map +1 -1
  258. package/dist/ui/tui/status.d.ts +1 -2
  259. package/dist/ui/tui/status.js +4 -2
  260. package/dist/ui/tui/status.js.map +1 -1
  261. package/dist/ui/tui/tiers.d.ts +0 -1
  262. package/dist/ui/tui/tiers.js.map +1 -1
  263. package/package.json +2 -2
  264. package/dist/adapters/codex-capacity/index.d.ts.map +0 -1
  265. package/dist/adapters/codex-capacity/parse.d.ts.map +0 -1
  266. package/dist/adapters/codex-capacity/reading.d.ts.map +0 -1
  267. package/dist/adapters/codex-capacity/sensor.d.ts.map +0 -1
  268. package/dist/adapters/codex-capacity/types.d.ts.map +0 -1
  269. package/dist/adapters/opencode/escalation-tool.d.ts.map +0 -1
  270. package/dist/adapters/opencode/events.d.ts.map +0 -1
  271. package/dist/adapters/opencode/hooks.d.ts.map +0 -1
  272. package/dist/adapters/opencode/index.d.ts.map +0 -1
  273. package/dist/adapters/opencode/inventory.d.ts.map +0 -1
  274. package/dist/adapters/opencode/report.d.ts.map +0 -1
  275. package/dist/adapters/opencode/session-ops.d.ts.map +0 -1
  276. package/dist/adapters/opencode/storage.d.ts.map +0 -1
  277. package/dist/adapters/opencode/tier-tool.d.ts.map +0 -1
  278. package/dist/adapters/opencode/types.d.ts.map +0 -1
  279. package/dist/core/compatibility/gate.d.ts.map +0 -1
  280. package/dist/core/compatibility/index.d.ts.map +0 -1
  281. package/dist/core/compatibility/types.d.ts.map +0 -1
  282. package/dist/core/scheduler/admit.d.ts.map +0 -1
  283. package/dist/core/scheduler/escalate.d.ts.map +0 -1
  284. package/dist/core/scheduler/index.d.ts.map +0 -1
  285. package/dist/core/scheduler/mode.d.ts.map +0 -1
  286. package/dist/core/scheduler/types.d.ts.map +0 -1
  287. package/dist/core/tier-policy/index.d.ts.map +0 -1
  288. package/dist/core/tier-policy/resolve.d.ts.map +0 -1
  289. package/dist/core/tier-policy/tier-list.d.ts.map +0 -1
  290. package/dist/core/tier-policy/types.d.ts.map +0 -1
  291. package/dist/core/work-unit/index.d.ts.map +0 -1
  292. package/dist/core/work-unit/interruption.d.ts.map +0 -1
  293. package/dist/core/work-unit/tracker.d.ts.map +0 -1
  294. package/dist/core/work-unit/types.d.ts.map +0 -1
  295. package/dist/execution/continuation/execute.d.ts.map +0 -1
  296. package/dist/execution/continuation/index.d.ts.map +0 -1
  297. package/dist/execution/continuation/ledger.d.ts +0 -20
  298. package/dist/execution/continuation/ledger.d.ts.map +0 -1
  299. package/dist/execution/continuation/ledger.js +0 -31
  300. package/dist/execution/continuation/ledger.js.map +0 -1
  301. package/dist/execution/continuation/plan.d.ts.map +0 -1
  302. package/dist/execution/continuation/prompts.d.ts.map +0 -1
  303. package/dist/execution/continuation/types.d.ts.map +0 -1
  304. package/dist/execution/escalation/index.d.ts.map +0 -1
  305. package/dist/execution/escalation/types.d.ts.map +0 -1
  306. package/dist/execution/index.d.ts.map +0 -1
  307. package/dist/execution/parking/index.d.ts.map +0 -1
  308. package/dist/execution/parking/plan.d.ts.map +0 -1
  309. package/dist/execution/recovery/gate.d.ts +0 -20
  310. package/dist/execution/recovery/gate.d.ts.map +0 -1
  311. package/dist/execution/recovery/gate.js +0 -23
  312. package/dist/execution/recovery/gate.js.map +0 -1
  313. package/dist/execution/recovery/index.d.ts +0 -3
  314. package/dist/execution/recovery/index.d.ts.map +0 -1
  315. package/dist/execution/recovery/index.js +0 -2
  316. package/dist/execution/recovery/index.js.map +0 -1
  317. package/dist/execution/retry/decide.d.ts.map +0 -1
  318. package/dist/execution/retry/index.d.ts.map +0 -1
  319. package/dist/execution/retry/types.d.ts.map +0 -1
  320. package/dist/gui.d.ts.map +0 -1
  321. package/dist/health/capacity/index.d.ts.map +0 -1
  322. package/dist/health/capacity/merge.d.ts.map +0 -1
  323. package/dist/health/capacity/registry.d.ts.map +0 -1
  324. package/dist/health/capacity/types.d.ts.map +0 -1
  325. package/dist/health/circuit-breaker/breaker.d.ts.map +0 -1
  326. package/dist/health/circuit-breaker/index.d.ts.map +0 -1
  327. package/dist/health/circuit-breaker/types.d.ts.map +0 -1
  328. package/dist/health/classifier/classify.d.ts.map +0 -1
  329. package/dist/health/classifier/index.d.ts.map +0 -1
  330. package/dist/health/classifier/types.d.ts.map +0 -1
  331. package/dist/health/index.d.ts.map +0 -1
  332. package/dist/health/resource-health/index.d.ts.map +0 -1
  333. package/dist/health/resource-health/key.d.ts.map +0 -1
  334. package/dist/health/resource-health/registry.d.ts.map +0 -1
  335. package/dist/health/resource-health/transitions.d.ts.map +0 -1
  336. package/dist/health/resource-health/types.d.ts.map +0 -1
  337. package/dist/index.d.ts.map +0 -1
  338. package/dist/plugin-id.d.ts.map +0 -1
  339. package/dist/rpc/definition.d.ts.map +0 -1
  340. package/dist/rpc/handlers.d.ts.map +0 -1
  341. package/dist/rpc/status.d.ts.map +0 -1
  342. package/dist/rpc.d.ts.map +0 -1
  343. package/dist/state/capacity/index.d.ts.map +0 -1
  344. package/dist/state/capacity/parse.d.ts.map +0 -1
  345. package/dist/state/capacity/store.d.ts.map +0 -1
  346. package/dist/state/capacity/types.d.ts.map +0 -1
  347. package/dist/state/health/index.d.ts.map +0 -1
  348. package/dist/state/health/parse.d.ts.map +0 -1
  349. package/dist/state/health/store.d.ts.map +0 -1
  350. package/dist/state/health/types.d.ts.map +0 -1
  351. package/dist/state/parked-work/index.d.ts.map +0 -1
  352. package/dist/state/parked-work/parse.d.ts.map +0 -1
  353. package/dist/state/parked-work/store.d.ts.map +0 -1
  354. package/dist/state/parked-work/types.d.ts.map +0 -1
  355. package/dist/state/receipts/bound.d.ts.map +0 -1
  356. package/dist/state/receipts/index.d.ts.map +0 -1
  357. package/dist/state/receipts/parse.d.ts.map +0 -1
  358. package/dist/state/receipts/store.d.ts.map +0 -1
  359. package/dist/state/receipts/types.d.ts.map +0 -1
  360. package/dist/state/session-modes/index.d.ts.map +0 -1
  361. package/dist/state/session-modes/parse.d.ts.map +0 -1
  362. package/dist/state/session-modes/store.d.ts.map +0 -1
  363. package/dist/state/session-modes/types.d.ts.map +0 -1
  364. package/dist/state/settings/index.d.ts.map +0 -1
  365. package/dist/state/settings/parse.d.ts.map +0 -1
  366. package/dist/state/settings/store.d.ts.map +0 -1
  367. package/dist/state/settings/types.d.ts.map +0 -1
  368. package/dist/supervisor/index.d.ts.map +0 -1
  369. package/dist/supervisor/log.d.ts.map +0 -1
  370. package/dist/supervisor/supervisor.d.ts.map +0 -1
  371. package/dist/supervisor/types.d.ts.map +0 -1
  372. package/dist/supervisor/view.d.ts.map +0 -1
  373. package/dist/tui.d.ts.map +0 -1
  374. package/dist/ui/gui/api.d.ts.map +0 -1
  375. package/dist/ui/gui/view.d.ts.map +0 -1
  376. package/dist/ui/tui/controller.d.ts.map +0 -1
  377. package/dist/ui/tui/menu.d.ts.map +0 -1
  378. package/dist/ui/tui/options.d.ts.map +0 -1
  379. package/dist/ui/tui/receipts.d.ts.map +0 -1
  380. package/dist/ui/tui/status.d.ts.map +0 -1
  381. package/dist/ui/tui/tiers.d.ts.map +0 -1
@@ -20,13 +20,13 @@ var _a;
20
20
  * (a later episode supersedes it); no outcome is ever fabricated.
21
21
  * - escalation (ADP-015..017) triggers only on capability-classified unit
22
22
  * failures (context overflow, content policy); infrastructure failures
23
- * never escalate, and the per-unit budget is finite.
23
+ * never escalate, and the per-task budget is finite.
24
24
  */
25
25
  import { WorkUnitTracker } from "../core/work-unit/tracker.js";
26
26
  import { sameModelRef, } from "../core/work-unit/types.js";
27
27
  import { INTERRUPT_REASONS } from "../core/work-unit/interruption.js";
28
28
  import { resolveRouting } from "../core/tier-policy/resolve.js";
29
- import { TIERS, nextTier } from "../core/tier-policy/types.js";
29
+ import { isTierName, nextTier } from "../core/tier-policy/types.js";
30
30
  import { admit, forMode as schedulerForMode, planEscalation, schedulerHealthKey, } from "../core/scheduler/index.js";
31
31
  import { MAX_ESCALATION_EVIDENCE } from "../core/scheduler/types.js";
32
32
  import { checkCompatibility } from "../core/compatibility/gate.js";
@@ -38,6 +38,7 @@ import { mergeReadings } from "../health/capacity/merge.js";
38
38
  import { resourceChain, resourceKeyID } from "../health/resource-health/key.js";
39
39
  import { SettingsStore, SETTINGS_KEY } from "../state/settings/index.js";
40
40
  import { SessionModeStore, SESSION_MODES_KEY } from "../state/session-modes/index.js";
41
+ import { TierRequestStore, TIER_REQUESTS_KEY } from "../state/tier-requests/index.js";
41
42
  import { ReceiptStore, RECEIPTS_KEY } from "../state/receipts/index.js";
42
43
  import { buildStatus } from "../rpc/status.js";
43
44
  import { RECEIPTS_VIEW_LIMIT } from "./view.js";
@@ -48,11 +49,8 @@ import { listCandidates, sessionOps, storageSeam } from "../adapters/opencode/in
48
49
  import { CodexCapacitySensor } from "../adapters/codex-capacity/index.js";
49
50
  import { logFailOpen } from "./log.js";
50
51
  import { CODEX_PROVIDER_ID } from "./types.js";
52
+ import { TIERS } from "../core/tier-policy/types.js";
51
53
  /** 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
54
  function configuredTiers(settings) {
57
55
  return TIERS.filter((tier) => settings.tiers[tier].length > 0);
58
56
  }
@@ -71,8 +69,20 @@ const CAPABILITY_EVIDENCE_CATEGORIES = [
71
69
  "context_overflow",
72
70
  "content_policy",
73
71
  ];
74
- /** Finite per-unit escalation budget (ADP-017). */
75
- const MAX_ESCALATIONS_PER_UNIT = 2;
72
+ /** Finite per-task escalation budget (ADP-017). */
73
+ const MAX_ESCALATIONS_PER_TASK = 2;
74
+ /**
75
+ * The recovery's post-settle guard aborted it (the session went OFF or the
76
+ * user interrupted while the host settle was in flight). Callers use it to
77
+ * skip the ordinary ops-failure degradation (parking) — an aborted recovery
78
+ * is a steering change, not an infrastructure failure.
79
+ */
80
+ export class RecoveryAbortedError extends Error {
81
+ constructor(sessionID) {
82
+ super(`recovery aborted for session ${sessionID}`);
83
+ this.name = "RecoveryAbortedError";
84
+ }
85
+ }
76
86
  export class Supervisor {
77
87
  #options;
78
88
  #settings;
@@ -86,6 +96,12 @@ export class Supervisor {
86
96
  #capacity;
87
97
  #codex;
88
98
  #vetoes = new Map();
99
+ /**
100
+ * Escalations spent per session on the current task (one user prompt and
101
+ * the owned continuations it spawns). Keyed by session, not unit: every
102
+ * escalation continues on a NEW unit, so a per-unit count would restart
103
+ * the budget at each step. A fresh user prompt starts a new task.
104
+ */
89
105
  #escalations = new Map();
90
106
  #admissionTiers = new Map();
91
107
  /** Recorded escalation-tool requests per unit (ADP-015 agent self-reports). */
@@ -100,10 +116,20 @@ export class Supervisor {
100
116
  * Per-session tier requests (agent-declared via the adaptive_tier tool):
101
117
  * the request is STICKY — it steers the session's next and following
102
118
  * admissions until cleared or the session dies — because an agent works
103
- * across many units in one task. Insertion order gives the eviction.
119
+ * across many units in one task. Persisted like the session-mode
120
+ * overrides: the host's double plugin instance (hooks vs RPC) must not
121
+ * strand a request on the instance that never routes.
104
122
  */
105
- static MAX_TIER_REQUESTS = 1024;
106
- #tierRequests = new Map();
123
+ #tierRequests;
124
+ /**
125
+ * Sessions whose last model change was the user's own pick
126
+ * (`selected-external`): prompt admission does not re-route them —
127
+ * switching would undo the user's `/model` choice (ADP-028). Cleared by an
128
+ * adaptive switch, a tier request, or a mode change; a standing settings
129
+ * pin overrides the suppression (the pin is manual intent too).
130
+ */
131
+ static MAX_MANUAL_SELECTIONS = 1024;
132
+ #manualSelections = new Map();
107
133
  /**
108
134
  * Sessions with an owned continuation prompt in flight: the prompt hook
109
135
  * will fire for it, but that prompt belongs to the active unit — it must
@@ -119,6 +145,15 @@ export class Supervisor {
119
145
  * make the continuation re-admit (admission ping-pong).
120
146
  */
121
147
  #continuationsInFlight = new Map();
148
+ /**
149
+ * Sessions with an owned recovery in flight (between veto/resume and the
150
+ * delivered continuation prompt). The host settle inside it can take
151
+ * unbounded time; an interruption arriving during that window must abort
152
+ * the stale decision instead of acting after it.
153
+ */
154
+ #recoveryPending = new Set();
155
+ /** Interruptions observed while a recovery was pending on the session. */
156
+ #recoveryInterrupted = new Set();
122
157
  /**
123
158
  * Half-open probe tickets by session: a probe admitted while computing the
124
159
  * recovery for a veto belongs to the continuation episode that follows, so
@@ -132,6 +167,21 @@ export class Supervisor {
132
167
  * it — otherwise a lost probe would wedge the resource in half_open.
133
168
  */
134
169
  #probeTickets = new Map();
170
+ /**
171
+ * Per-unit dedup for breaker failure evidence (ADP-019). One failed attempt
172
+ * surfaces through up to three host signals — the pumped `http.response`
173
+ * event, the `retry` hook (the same request), and the terminal
174
+ * `execution-failed` (the last attempt) — and counting each would trip the
175
+ * breaker after ~2 real failures instead of `failureThreshold`. The exact
176
+ * arrival order of a pair is not guaranteed (the retry hook runs in the
177
+ * request lifecycle, the event pump lags), so the dedup is order-symmetric:
178
+ * the first signal of a pair counts and arms, the matching counterpart
179
+ * consumes it, a completed pair closes the attempt, and the terminal
180
+ * repeats only when the last attempt left no live evidence. A same-category
181
+ * observation with nothing armed is a NEW attempt (consecutive http-only
182
+ * failures each count). Cleared at unit close.
183
+ */
184
+ #attemptFailures = new Map();
135
185
  /**
136
186
  * Session → tier admitted by the prompt hook for the unit that is about to
137
187
  * open. Consumed by execution-started, which then skips re-routing (the
@@ -162,6 +212,7 @@ export class Supervisor {
162
212
  this.#healthRecords = new HealthRecordStore(storageSeam(HEALTH_KEY, options.storage));
163
213
  this.#capacityStore = new CapacityReadingStore(storageSeam(CAPACITY_KEY, options.storage));
164
214
  this.#sessionModes = new SessionModeStore(storageSeam(SESSION_MODES_KEY, options.storage), options.clock);
215
+ this.#tierRequests = new TierRequestStore(storageSeam(TIER_REQUESTS_KEY, options.storage), options.clock);
165
216
  this.#ops = sessionOps(options.session, {
166
217
  beforeSwitch: (sessionID, model) => {
167
218
  this.#tracker.noteOwnSwitch(sessionID, model);
@@ -234,6 +285,10 @@ export class Supervisor {
234
285
  // host's double-instance reconciliation) must keep steering sessions
235
286
  // under the modes the user set for them.
236
287
  await this.#sessionModes.load();
288
+ // Tier requests hydrate the same way: a tier request recorded through
289
+ // the host's other plugin instance (or before a restart) must still
290
+ // steer this instance's admissions.
291
+ await this.#tierRequests.load();
237
292
  await this.#capacityStore.load();
238
293
  return this.#settings.load();
239
294
  };
@@ -270,6 +325,42 @@ export class Supervisor {
270
325
  logFailOpen("report", error);
271
326
  }
272
327
  }
328
+ /** Marks the session's manual-selection state from an external selection event. */
329
+ #noteManualSelection(sessionID) {
330
+ if (!this.#manualSelections.has(sessionID) &&
331
+ this.#manualSelections.size >= _a.MAX_MANUAL_SELECTIONS) {
332
+ // The guard above ensures the map is at its cap (>= 1 entries), so the
333
+ // first key always exists; the assertion avoids a branch the coverage
334
+ // gate would reject as unreachable.
335
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
336
+ const oldest = this.#manualSelections.keys().next().value;
337
+ this.#manualSelections.delete(oldest);
338
+ }
339
+ this.#manualSelections.set(sessionID, true);
340
+ }
341
+ /**
342
+ * True while the session's last model change was the user's own pick.
343
+ * A standing settings pin is explicit manual intent as well (ADP-002):
344
+ * honoring it does not fight the pick, so suppression is skipped there.
345
+ */
346
+ #manuallySelected(sessionID, settings) {
347
+ return settings.pin === null && this.#manualSelections.has(sessionID);
348
+ }
349
+ /**
350
+ * The manual-pick suppression actually applied at an admission. It yields
351
+ * when the picked model is health-blocked: keeping the session on a
352
+ * quarantined resource does not honor the pick — the configured fallback
353
+ * takes over instead (the pick is re-honored only if the user picks again).
354
+ * Uses the non-mutating evaluation, never a probe admission.
355
+ */
356
+ #manualSuppressionHolds(sessionID, settings) {
357
+ if (!this.#manuallySelected(sessionID, settings))
358
+ return false;
359
+ const current = this.#tracker.lastModel(sessionID)?.model;
360
+ if (current === undefined)
361
+ return true;
362
+ return this.#health.evaluate(refKey(current)).kind !== "blocked";
363
+ }
273
364
  /**
274
365
  * Handle one `retry` hook invocation. On a veto this mutates
275
366
  * `input.decision` to `{ retry: false }`; otherwise the decision is left
@@ -308,33 +399,38 @@ export class Supervisor {
308
399
  * tool). STICKY: it steers the session's next and following admissions
309
400
  * until cleared — an agent works across many units in one task. Returns
310
401
  * false on an unknown tier (the tool re-validates before calling).
402
+ *
403
+ * Fire-and-forget persistence: the recorder signature is sync on both the
404
+ * tool and the RPC source interfaces, so the write is enqueued without
405
+ * awaiting it. Readers (routing, getTierRequest, the idle sweep) flush the
406
+ * store's queue before reading, which makes the record visible
407
+ * deterministically; a write failure drops the record fail-open (the tool
408
+ * told the agent "recorded", the persisted mirror simply never sees it —
409
+ * the same degradation the in-memory map had on restart).
311
410
  */
312
411
  noteTierRequest(sessionID, tier) {
313
412
  if (!isTierName(tier))
314
413
  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" });
414
+ void this.#tierRequests.set(sessionID, tier).catch(() => undefined);
415
+ // The user/agent explicitly asked Adaptive to route again (ADP-028).
416
+ this.#manualSelections.delete(sessionID);
325
417
  return true;
326
418
  }
327
419
  /** The session's tier request, or `null` when it follows the routing ladder. */
328
420
  async getTierRequest(sessionID) {
329
421
  await this.#ready;
330
- return this.#tierRequests.get(sessionID)?.tier ?? null;
422
+ // A fire-and-forget note may still be in the queue: flush before reading.
423
+ await this.#tierRequests.flush();
424
+ return this.#tierRequests.get(sessionID);
331
425
  }
332
426
  /** Clear one session's tier request (`null` is the only valid input). */
333
427
  async setTierRequest(sessionID, tier) {
334
428
  await this.#ready;
335
429
  if (tier !== null)
336
430
  throw new Error("only null clears a tier request");
337
- this.#tierRequests.delete(sessionID);
431
+ await this.#tierRequests.set(sessionID, null);
432
+ // Clearing through the control surface is the user asking Adaptive back.
433
+ this.#manualSelections.delete(sessionID);
338
434
  }
339
435
  // ---------- state API for the control surfaces (ADP-033/034/036) ----------
340
436
  /** Current settings (awaits the constructor load). */
@@ -344,7 +440,13 @@ export class Supervisor {
344
440
  /** Validate and persist a settings patch; returns per-field errors. */
345
441
  async updateSettings(patch) {
346
442
  await this.#ready;
347
- return this.#settings.update(patch);
443
+ const result = await this.#settings.update(patch);
444
+ if (result.ok && patch.mode !== undefined) {
445
+ // A global mode change is the user steering Adaptive explicitly
446
+ // (ADP-028): manual-selection suppression lifts for every session.
447
+ this.#manualSelections.clear();
448
+ }
449
+ return result;
348
450
  }
349
451
  /** The session's mode override, or `null` when it follows the global mode. */
350
452
  async getSessionMode(sessionID) {
@@ -359,6 +461,8 @@ export class Supervisor {
359
461
  async setSessionMode(sessionID, mode) {
360
462
  await this.#ready;
361
463
  await this.#sessionModes.set(sessionID, mode);
464
+ // A mode change is the user steering Adaptive explicitly (ADP-028).
465
+ this.#manualSelections.delete(sessionID);
362
466
  }
363
467
  /**
364
468
  * Bounded status snapshot for the status view (ADP-033). Health is read
@@ -416,14 +520,24 @@ export class Supervisor {
416
520
  case "execution-interrupted":
417
521
  await this.#onExecutionInterrupted(report);
418
522
  return;
419
- case "model-selected":
420
- this.#tracker.apply({
523
+ case "model-selected": {
524
+ const result = this.#tracker.apply({
421
525
  kind: "model-selected",
422
526
  sessionID: report.sessionID,
423
527
  at: report.at,
424
528
  model: report.model,
425
529
  });
530
+ // Whether the change was the user's pick or Adaptive's own switch
531
+ // decides who controls the session's model (ADP-028); the result
532
+ // carries the attribution the tracker resolved. Only the two
533
+ // selection sources reach this event kind.
534
+ const { source } = result.transition;
535
+ if (source === "selected-adaptive")
536
+ this.#manualSelections.delete(report.sessionID);
537
+ else
538
+ this.#noteManualSelection(report.sessionID);
426
539
  return;
540
+ }
427
541
  case "model-request":
428
542
  if (report.agent !== undefined) {
429
543
  if (!this.#agents.has(report.sessionID) &&
@@ -437,12 +551,20 @@ export class Supervisor {
437
551
  }
438
552
  this.#agents.set(report.sessionID, report.agent);
439
553
  }
440
- this.#tracker.apply({
441
- kind: "model-request",
442
- sessionID: report.sessionID,
443
- at: this.#options.clock(),
444
- model: report.model,
445
- });
554
+ // Only the agent loop's own request reports the model actually
555
+ // executing the unit's work. An auxiliary request (title, compaction,
556
+ // generate — ADP-027) carries the host's own cheap pick for that side
557
+ // request; adopting it as the session's model would misattribute the
558
+ // primary unit's health evidence and leave the next admission
559
+ // believing the unit runs on the wrong resource.
560
+ if (report.requestKind === "primary") {
561
+ this.#tracker.apply({
562
+ kind: "model-request",
563
+ sessionID: report.sessionID,
564
+ at: this.#options.clock(),
565
+ model: report.model,
566
+ });
567
+ }
446
568
  return;
447
569
  case "http-response":
448
570
  await this.#onHttpResponse(report);
@@ -481,14 +603,26 @@ export class Supervisor {
481
603
  // this admission re-routes from scratch.
482
604
  this.#continuationTiers.delete(sessionID);
483
605
  this.#promptAdmission.delete(sessionID);
606
+ this.#escalations.delete(sessionID);
484
607
  // A pending probe ticket from an earlier prompt admission (or a failed
485
608
  // 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.
609
+ // a fresh admission means that episode never ran, so the ticket is
610
+ // released here instead of being adopted by whichever unit opens next.
488
611
  const stale = this.#sessionProbeTickets.get(sessionID);
489
612
  if (stale !== undefined) {
490
613
  this.#sessionProbeTickets.delete(sessionID);
491
- this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
614
+ this.#health.releaseProbe(stale);
615
+ }
616
+ if (this.#manualSuppressionHolds(sessionID, settings)) {
617
+ // ADP-028: the user's own model pick stands (yielded to health if the
618
+ // picked resource is quarantined). Recording the defer (no switch, no
619
+ // marker) leaves OpenCode's choice — the picked model — in place;
620
+ // execution-started will skip routing the same way.
621
+ await this.#recordReceipt({
622
+ sessionID,
623
+ decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
624
+ });
625
+ return;
492
626
  }
493
627
  let probeTicket;
494
628
  try {
@@ -513,9 +647,9 @@ export class Supervisor {
513
647
  }
514
648
  catch (error) {
515
649
  // 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.
650
+ // switch threw): no unit will ever carry the ticket, so release it.
517
651
  if (probeTicket !== undefined) {
518
- this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
652
+ this.#health.releaseProbe(probeTicket);
519
653
  }
520
654
  // Fail-open: no marker → execution-started re-routes the unit itself.
521
655
  logFailOpen("admission", error);
@@ -582,14 +716,22 @@ export class Supervisor {
582
716
  }
583
717
  }
584
718
  /** Fresh admission: routing ladder → scheduler, raw (unwrapped). */
585
- async #admit(settings, sessionID) {
719
+ async #admit(settings, sessionID, requiredTier) {
720
+ // The agent-declared tier request (adaptive_tier tool) outranks the
721
+ // role hints but stays below the pin (ADP-002 ladder). The note is
722
+ // recorded fire-and-forget, so flush the store's queue before reading.
723
+ await this.#tierRequests.flush();
724
+ const tierRequest = this.#tierRequests.get(sessionID);
725
+ let routedTier;
726
+ if (requiredTier !== undefined) {
727
+ routedTier = { tier: requiredTier, via: "manual-command" };
728
+ }
729
+ else if (tierRequest !== null) {
730
+ routedTier = { tier: tierRequest, via: "manual-command" };
731
+ }
586
732
  const routing = resolveRouting({
587
733
  ...(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
- : {}),
734
+ ...(routedTier !== undefined ? { tierRequest: routedTier } : {}),
593
735
  // The agent-role hint is the session's last observed agent — the host
594
736
  // reports it on every model request (ADP-002 level 3).
595
737
  ...(this.#agents.has(sessionID) ? { agent: this.#agents.get(sessionID) } : {}),
@@ -603,10 +745,13 @@ export class Supervisor {
603
745
  await this.#capacity.refresh();
604
746
  const health = {};
605
747
  const capacity = {};
606
- // Tickets issued by this assessment sweep (see the re-arm below).
748
+ // Tickets issued by this assessment sweep (see the release below).
607
749
  const issued = [];
608
- for (const candidate of inventory) {
609
- const ref = candidateRef(candidate);
750
+ // Only the routed candidates are assessed: assess flips a due breaker to
751
+ // half_open, and admitting a quick unit has no business touching an
752
+ // advanced-tier resource's breaker.
753
+ const routed = routing.kind === "pin" ? [routing.model] : tierCandidates;
754
+ for (const ref of routed) {
610
755
  const mapKey = schedulerHealthKey(ref);
611
756
  const assessment = this.#health.assess(refKey(ref));
612
757
  health[mapKey] = assessment;
@@ -630,12 +775,12 @@ export class Supervisor {
630
775
  };
631
776
  const decision = admit(input);
632
777
  // 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.
778
+ // including candidates this decision passed over (an earlier ready
779
+ // candidate wins) or dispatched nothing for (defer/park). An
780
+ // issued-but-undispatched probe would wedge its resource in half_open:
781
+ // no unit ever carries the ticket. Release each unselected ticket — it
782
+ // stays due, because a probe that never ran is no health evidence and
783
+ // restarting its backoff would push it back on every admission.
639
784
  const selectedId = decision.kind === "apply" ? decision.evidence.probeId : undefined;
640
785
  let probeTicket;
641
786
  for (const ticket of issued) {
@@ -643,12 +788,22 @@ export class Supervisor {
643
788
  probeTicket = ticket;
644
789
  }
645
790
  else {
646
- this.#health.reportProbe(ticket, { success: false, category: "user_cancelled" });
791
+ this.#health.releaseProbe(ticket);
647
792
  }
648
793
  }
649
794
  return { routing, decision, ...(probeTicket !== undefined ? { probeTicket } : {}) };
650
795
  }
651
796
  async #routeAdmission(unit, settings, mode) {
797
+ if (this.#manualSuppressionHolds(unit.sessionID, settings)) {
798
+ // ADP-028: the session runs on the user's own pick (yielded to health
799
+ // if the picked resource is quarantined) — no re-route.
800
+ await this.#recordReceipt({
801
+ sessionID: unit.sessionID,
802
+ unitID: unit.id,
803
+ decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
804
+ });
805
+ return;
806
+ }
652
807
  const { routing, decision, probeTicket } = await this.#admit(settings, unit.sessionID);
653
808
  // The tier the unit was admitted under is escalation context (ADP-015);
654
809
  // recorded for every tiered routing outcome (including a defer, which
@@ -663,9 +818,9 @@ export class Supervisor {
663
818
  }
664
819
  catch (error) {
665
820
  // The episode cannot carry the probe when the switch itself failed —
666
- // re-arm the ticket instead of wedging the resource half-open.
821
+ // release the ticket instead of wedging the resource half-open.
667
822
  if (probeTicket !== undefined) {
668
- this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
823
+ this.#health.releaseProbe(probeTicket);
669
824
  }
670
825
  throw error;
671
826
  }
@@ -675,8 +830,8 @@ export class Supervisor {
675
830
  }
676
831
  if (probeTicket !== undefined) {
677
832
  // 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" });
833
+ // running it: no episode will ever report the ticket, so release it.
834
+ this.#health.releaseProbe(probeTicket);
680
835
  }
681
836
  // Observe-only records the identical decision; defer and park leave the
682
837
  // episode on OpenCode's own model (admission cannot hold an episode).
@@ -702,7 +857,7 @@ export class Supervisor {
702
857
  async #onExecutionSucceeded(report) {
703
858
  const unit = this.#tracker.active(report.sessionID);
704
859
  const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
705
- const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
860
+ const probeTicket = this.#takeProbeTicket(unit?.id);
706
861
  const closed = this.#tracker.apply({
707
862
  kind: "execution-succeeded",
708
863
  sessionID: report.sessionID,
@@ -714,7 +869,6 @@ export class Supervisor {
714
869
  }
715
870
  if (unit !== undefined) {
716
871
  if (probeTicket !== undefined) {
717
- this.#probeTickets.delete(unit.id);
718
872
  this.#reportProbeOutcome(probeTicket, { success: true });
719
873
  }
720
874
  await this.#attachOutcome(unit, { status: "succeeded" });
@@ -724,19 +878,16 @@ export class Supervisor {
724
878
  const unit = this.#tracker.active(report.sessionID);
725
879
  const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
726
880
  const unitID = unit?.id;
727
- // Budget and tier are read before the close clears the per-unit maps.
881
+ // The tier is read before the close clears the per-unit maps.
728
882
  const admissionTier = unitID !== undefined ? this.#admissionTiers.get(unitID) : undefined;
729
- const escalationUsed = unitID !== undefined ? (this.#escalations.get(unitID) ?? 0) : 0;
883
+ const escalationUsed = this.#escalations.get(report.sessionID) ?? 0;
730
884
  // Tool evidence is captured pre-close like the other per-unit maps.
731
885
  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);
886
+ const probeTicket = this.#takeProbeTicket(unitID);
887
+ // The terminal classifies and dedups BEFORE the close: the close clears
888
+ // the unit's attempt-failure entry, and the terminal must consult it —
889
+ // it repeats the episode's last attempt, which an attempt signal
890
+ // (http-response or retry hook) already counted.
740
891
  const classification = classifyFailure({
741
892
  structured: {
742
893
  type: report.error.type,
@@ -750,13 +901,20 @@ export class Supervisor {
750
901
  // The retry hook deliberately omits text: it must stay conservative.
751
902
  text: report.error.message,
752
903
  });
753
- if (model !== undefined) {
904
+ const countTerminal = this.#countTerminalFailure(unitID, classification.category);
905
+ const closed = this.#tracker.apply({
906
+ kind: "execution-failed",
907
+ sessionID: report.sessionID,
908
+ at: report.at,
909
+ error: report.error,
910
+ });
911
+ this.#afterUnitClose(closed);
912
+ if (model !== undefined && countTerminal) {
754
913
  this.#observeHealth(failureEvent(refKey(model), classification));
755
914
  }
756
915
  if (unit !== undefined) {
757
916
  await this.#attachOutcome(unit, { status: "failed", category: classification.category });
758
917
  if (probeTicket !== undefined) {
759
- this.#probeTickets.delete(unit.id);
760
918
  this.#reportProbeOutcome(probeTicket, { success: false, category: classification.category });
761
919
  }
762
920
  }
@@ -783,8 +941,11 @@ export class Supervisor {
783
941
  return [];
784
942
  }
785
943
  const eligible = [];
944
+ // Compared by resource key: the host reports base-model calls as variant
945
+ // "default", which is the same resource as the bare tier entry.
946
+ const failedID = resourceKeyID(refKey(failed));
786
947
  for (const ref of refs) {
787
- if (sameModelRef(ref, failed))
948
+ if (resourceKeyID(refKey(ref)) === failedID)
788
949
  continue;
789
950
  const candidate = inventory.find((entry) => sameModelRef(candidateRef(entry), ref));
790
951
  if (candidate === undefined)
@@ -822,11 +983,11 @@ export class Supervisor {
822
983
  currentTier: admissionTier,
823
984
  nextTierResources: eligible,
824
985
  evidence,
825
- budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_UNIT },
986
+ budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_TASK },
826
987
  });
827
988
  const wrapped = schedulerForMode(plan, this.#modeFor(report.sessionID, settings));
828
989
  if (wrapped.kind === "escalate") {
829
- this.#escalations.set(unitID, escalationUsed + 1);
990
+ this.#escalations.set(report.sessionID, escalationUsed + 1);
830
991
  // Recorded unit-less on purpose: the receipt describes the CONTINUATION
831
992
  // decision, so the continuation unit adopts it and its outcome attaches
832
993
  // here (the failed unit's own outcome attaches to its apply receipt).
@@ -838,10 +999,11 @@ export class Supervisor {
838
999
  // Continue the interrupted work on the escalated resource; the unit is
839
1000
  // terminally failed, so an ops failure is only logged (never parked).
840
1001
  try {
841
- await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier);
1002
+ await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier, () => this.#modeFor(report.sessionID, settings) !== "off");
842
1003
  }
843
1004
  catch (error) {
844
- logFailOpen("escalation-ops", error);
1005
+ if (!(error instanceof RecoveryAbortedError))
1006
+ logFailOpen("escalation-ops", error);
845
1007
  }
846
1008
  return;
847
1009
  }
@@ -858,7 +1020,13 @@ export class Supervisor {
858
1020
  return;
859
1021
  }
860
1022
  const unit = this.#tracker.active(report.sessionID);
861
- const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
1023
+ const probeTicket = this.#takeProbeTicket(unit?.id);
1024
+ // An interruption while a recovery is pending on this session aborts it
1025
+ // at its post-settle guard (the pending recovery's decision predates the
1026
+ // interruption and must not act after it).
1027
+ if (this.#recoveryPending.has(report.sessionID)) {
1028
+ this.#recoveryInterrupted.add(report.sessionID);
1029
+ }
862
1030
  const closed = this.#tracker.apply({
863
1031
  kind: "execution-interrupted",
864
1032
  sessionID: report.sessionID,
@@ -872,7 +1040,6 @@ export class Supervisor {
872
1040
  // An interrupted probe is no evidence about resource health: re-arm the
873
1041
  // breaker's same window rather than doubling it.
874
1042
  if (probeTicket !== undefined) {
875
- this.#probeTickets.delete(unit.id);
876
1043
  this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
877
1044
  }
878
1045
  await this.#attachOutcome(unit, { status: "interrupted", reason });
@@ -889,23 +1056,81 @@ export class Supervisor {
889
1056
  // unknown ticket) are reported by its boolean return, not by throwing.
890
1057
  this.#health.reportProbe(ticket, outcome);
891
1058
  }
1059
+ /**
1060
+ * One attempt's failure evidence (http-response event or retry hook):
1061
+ * returns whether THIS observation is the attempt's first count.
1062
+ */
1063
+ #countAttemptFailure(unitID, source, category) {
1064
+ if (unitID === undefined)
1065
+ return true;
1066
+ const ev = this.#attemptFailures.get(unitID) ?? {};
1067
+ if (source === "http" ? ev.retry === category : ev.http === category) {
1068
+ // The other host signal already counted this attempt: consume it.
1069
+ const closed = { ...ev, http: undefined, retry: undefined, closed: category };
1070
+ this.#attemptFailures.set(unitID, closed);
1071
+ return false;
1072
+ }
1073
+ this.#attemptFailures.set(unitID, source === "http" ? { ...ev, http: category } : { ...ev, retry: category });
1074
+ return true;
1075
+ }
1076
+ /**
1077
+ * The terminal `execution-failed` repeats the episode's last attempt: it
1078
+ * counts only when the last attempt left no live evidence (e.g. a
1079
+ * capability failure that never produced an HTTP response or retry hook).
1080
+ */
1081
+ #countTerminalFailure(unitID, category) {
1082
+ if (unitID === undefined)
1083
+ return true;
1084
+ const ev = this.#attemptFailures.get(unitID) ?? {};
1085
+ if (ev.http === category) {
1086
+ this.#attemptFailures.set(unitID, { ...ev, http: undefined });
1087
+ return false;
1088
+ }
1089
+ if (ev.retry === category) {
1090
+ this.#attemptFailures.set(unitID, { ...ev, retry: undefined });
1091
+ return false;
1092
+ }
1093
+ if (ev.closed === category) {
1094
+ this.#attemptFailures.set(unitID, { ...ev, closed: undefined });
1095
+ return false;
1096
+ }
1097
+ this.#attemptFailures.set(unitID, { ...ev, closed: category });
1098
+ return true;
1099
+ }
1100
+ /**
1101
+ * Hands a closing unit's probe ticket to its explicit outcome reporter.
1102
+ * Taken BEFORE the close: #afterUnitClose treats any ticket still held as
1103
+ * lost and re-opens the breaker, which would turn a probe's real outcome
1104
+ * into a stale report.
1105
+ */
1106
+ #takeProbeTicket(unitID) {
1107
+ if (unitID === undefined)
1108
+ return undefined;
1109
+ const ticket = this.#probeTickets.get(unitID);
1110
+ this.#probeTickets.delete(unitID);
1111
+ return ticket;
1112
+ }
892
1113
  /** Bounded per-unit bookkeeping once the tracker closed the unit. */
893
1114
  #afterUnitClose(closed) {
894
1115
  const unitID = closedUnitID(closed);
895
1116
  if (unitID !== undefined) {
896
1117
  this.#vetoes.delete(unitID);
897
- this.#escalations.delete(unitID);
898
1118
  this.#admissionTiers.delete(unitID);
899
1119
  this.#escalationEvidence.delete(unitID);
900
1120
  const leftover = this.#probeTickets.get(unitID);
901
1121
  if (leftover !== undefined) {
902
- // The explicit outcome reporters run before this cleanup; reaching
903
- // here means the unit ended without one (superseded or lost).
1122
+ // The explicit outcome reporters take their ticket before this
1123
+ // cleanup; reaching here means the unit ended without one
1124
+ // (superseded or lost).
904
1125
  this.#reportProbeOutcome(leftover, { success: false, category: "user_cancelled" });
905
1126
  this.#probeTickets.delete(unitID);
906
1127
  }
907
1128
  }
908
1129
  if (closed.kind === "opened" || closed.kind === "closed") {
1130
+ const unitID = closedUnitID(closed);
1131
+ if (unitID !== undefined) {
1132
+ this.#attemptFailures.delete(unitID);
1133
+ }
909
1134
  // Per-session flags for a unit that will never open again must not
910
1135
  // leak: a stale continuation mark would swallow a real user prompt's
911
1136
  // admission; a stale admission marker would skip routing. The guard
@@ -939,7 +1164,11 @@ export class Supervisor {
939
1164
  ? { headers: { "retry-after": report.retryAfterHeader } }
940
1165
  : {}),
941
1166
  });
942
- this.#observeHealth(failureEvent(refKey(model), classification));
1167
+ // The retry hook classifies the same request for the host's retry
1168
+ // decision; counting both would double every attempt (ADP-019).
1169
+ if (this.#countAttemptFailure(unit?.id, "http", classification.category)) {
1170
+ this.#observeHealth(failureEvent(refKey(model), classification));
1171
+ }
943
1172
  }
944
1173
  // ---------- retry hook ----------
945
1174
  async #handleRetry(input, settings) {
@@ -952,7 +1181,12 @@ export class Supervisor {
952
1181
  },
953
1182
  status: input.error.status,
954
1183
  });
955
- this.#observeHealth(failureEvent(refKey(input.model), classification));
1184
+ const unitID = this.#tracker.active(input.sessionID)?.id;
1185
+ // The pumped http-response event describes the same failed request;
1186
+ // counting both would double every attempt (ADP-019).
1187
+ if (this.#countAttemptFailure(unitID, "retry", classification.category)) {
1188
+ this.#observeHealth(failureEvent(refKey(input.model), classification));
1189
+ }
956
1190
  // Recovery = fresh admission; the failed resource is already health-blocked.
957
1191
  let recovery = { kind: "none" };
958
1192
  let routingTier;
@@ -976,7 +1210,6 @@ export class Supervisor {
976
1210
  catch (error) {
977
1211
  logFailOpen("recovery", error);
978
1212
  }
979
- const unitID = this.#tracker.active(input.sessionID)?.id;
980
1213
  const vetoesUsed = unitID !== undefined ? (this.#vetoes.get(unitID) ?? 0) : 0;
981
1214
  const core = decideRetry({
982
1215
  classification,
@@ -988,10 +1221,10 @@ export class Supervisor {
988
1221
  const wrapped = retryForMode(core, this.#modeFor(input.sessionID, settings));
989
1222
  if (wrapped.kind !== "veto") {
990
1223
  // 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
1224
+ // admitted probe has no continuation to report it — release it here or
992
1225
  // the resource wedges half-open.
993
1226
  if (probeTicket !== undefined) {
994
- this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
1227
+ this.#health.releaseProbe(probeTicket);
995
1228
  }
996
1229
  return;
997
1230
  }
@@ -1004,10 +1237,10 @@ export class Supervisor {
1004
1237
  if (probeTicket !== undefined) {
1005
1238
  // The session holds one pending probe slot. A still-pending older
1006
1239
  // ticket belongs to a recovery whose continuation has not started (its
1007
- // episode can no longer claim this slot) — re-arm it before replacing.
1240
+ // episode can no longer claim this slot) — release it before replacing.
1008
1241
  const stale = this.#sessionProbeTickets.get(input.sessionID);
1009
1242
  if (stale !== undefined && stale !== probeTicket) {
1010
- this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
1243
+ this.#health.releaseProbe(stale);
1011
1244
  }
1012
1245
  this.#sessionProbeTickets.set(input.sessionID, probeTicket);
1013
1246
  }
@@ -1024,17 +1257,28 @@ export class Supervisor {
1024
1257
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a veto implies the admission was an apply (the recovery exists only then)
1025
1258
  decision: admission,
1026
1259
  });
1027
- void this.#ownRecovery(input.sessionID, wrapped.model, routingTier)
1260
+ void this.#ownRecovery(input.sessionID, wrapped.model, routingTier, () => this.#modeFor(input.sessionID, settings) !== "off")
1028
1261
  .then(() => undefined)
1029
1262
  .catch(async (error) => {
1263
+ if (error instanceof RecoveryAbortedError) {
1264
+ // The user steered away (interruption / OFF) during the settle: the
1265
+ // veto stands, but no continuation episode will ever carry the
1266
+ // probe it admitted — release it instead of wedging half-open.
1267
+ if (probeTicket !== undefined &&
1268
+ this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
1269
+ this.#sessionProbeTickets.delete(input.sessionID);
1270
+ this.#health.releaseProbe(probeTicket);
1271
+ }
1272
+ return;
1273
+ }
1030
1274
  logFailOpen("recovery-ops", error);
1031
1275
  // The recovery rejected before (or while) delivering its continuation
1032
- // prompt, so no unit will ever carry the probe it admitted — re-arm
1276
+ // prompt, so no unit will ever carry the probe it admitted — release
1033
1277
  // it instead of wedging the resource half-open.
1034
1278
  if (probeTicket !== undefined &&
1035
1279
  this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
1036
1280
  this.#sessionProbeTickets.delete(input.sessionID);
1037
- this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
1281
+ this.#health.releaseProbe(probeTicket);
1038
1282
  }
1039
1283
  if (unitID === undefined || routingTier === undefined)
1040
1284
  return;
@@ -1059,17 +1303,33 @@ export class Supervisor {
1059
1303
  * continuation's prompt hook re-arms the admission marker (the unit must
1060
1304
  * not be re-routed: that would discard the recovery/escalation decision).
1061
1305
  */
1062
- async #ownRecovery(sessionID, model, tier) {
1306
+ async #ownRecovery(sessionID, model, tier, stillValid) {
1063
1307
  if (tier !== undefined)
1064
1308
  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
- });
1309
+ // The guard window opens before the first await so an interrupt pumped
1310
+ // during the admission race is still seen.
1311
+ this.#recoveryPending.add(sessionID);
1071
1312
  try {
1072
- await executeContinuation({ sessionID, action, ops: this.#ops });
1313
+ const alive = await this.#isSessionAlive(sessionID);
1314
+ const recovery = { kind: "switch", model };
1315
+ const action = planContinuation({
1316
+ recovery,
1317
+ continuationPossible: alive,
1318
+ });
1319
+ // executeContinuation settles first; the supervisor settles here so the
1320
+ // post-settle guard (mode OFF, user interruption) can abort before any
1321
+ // switch or prompt — the settle is the only step that can take
1322
+ // unbounded time, and a decision made before it may be stale after it.
1323
+ await this.#ops.settle(sessionID);
1324
+ if (!stillValid() || this.#recoveryInterrupted.delete(sessionID)) {
1325
+ throw new RecoveryAbortedError(sessionID);
1326
+ }
1327
+ // Already settled above; the action's own settle is a no-op.
1328
+ await executeContinuation({
1329
+ sessionID,
1330
+ action,
1331
+ ops: { ...this.#ops, settle: () => Promise.resolve() },
1332
+ });
1073
1333
  }
1074
1334
  catch (error) {
1075
1335
  // The continuation prompt never reached the host, so no unit will open
@@ -1079,6 +1339,9 @@ export class Supervisor {
1079
1339
  this.#continuationTiers.delete(sessionID);
1080
1340
  throw error;
1081
1341
  }
1342
+ finally {
1343
+ this.#recoveryPending.delete(sessionID);
1344
+ }
1082
1345
  }
1083
1346
  // ---------- parked-work resume (safe boundary: idle) ----------
1084
1347
  async #onIdle(settings) {
@@ -1087,7 +1350,8 @@ export class Supervisor {
1087
1350
  // pruned here (its per-session steering is worthless once the session is
1088
1351
  // gone), in the same sweep that clears the other per-session maps.
1089
1352
  const overrideIDs = this.#sessionModes.snapshot().map((record) => record.sessionID);
1090
- if (records.length === 0 && overrideIDs.length === 0 && this.#tierRequests.size === 0)
1353
+ const tierRequestIDs = this.#tierRequests.snapshot().map((record) => record.sessionID);
1354
+ if (records.length === 0 && overrideIDs.length === 0 && tierRequestIDs.length === 0)
1091
1355
  return;
1092
1356
  // OFF sessions are not supervised: their parked records are neither
1093
1357
  // dropped nor resumed, and no alive probe runs for them.
@@ -1100,7 +1364,7 @@ export class Supervisor {
1100
1364
  }
1101
1365
  // Tier-request holders join the alive sweep too: a dead session's sticky
1102
1366
  // tier request dies with its session, like the other per-session maps.
1103
- for (const sessionID of this.#tierRequests.keys()) {
1367
+ for (const sessionID of tierRequestIDs) {
1104
1368
  if (!alive.has(sessionID))
1105
1369
  alive.set(sessionID, await this.#isSessionAlive(sessionID));
1106
1370
  }
@@ -1114,9 +1378,18 @@ export class Supervisor {
1114
1378
  await this.#dropParked(dropped);
1115
1379
  }
1116
1380
  // 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);
1381
+ // The fire-and-forget note means the queue may still hold writes; the
1382
+ // serialized set applies after them. A write failure just leaves the
1383
+ // record (fail-open — it will die with the session on the next sweep).
1384
+ for (const sessionID of tierRequestIDs) {
1385
+ if (alive.get(sessionID) === false) {
1386
+ try {
1387
+ await this.#tierRequests.set(sessionID, null);
1388
+ }
1389
+ catch (error) {
1390
+ logFailOpen("tier-requests", error);
1391
+ }
1392
+ }
1120
1393
  }
1121
1394
  // At most ONE continuation per session per sweep: prompting a session
1122
1395
  // with an active unit would supersede the user's in-flight episode (not
@@ -1138,6 +1411,7 @@ export class Supervisor {
1138
1411
  this.#continuationTiers.delete(sessionID);
1139
1412
  this.#promptAdmission.delete(sessionID);
1140
1413
  this.#continuationPending.delete(sessionID);
1414
+ this.#escalations.delete(sessionID);
1141
1415
  if (this.#sessionModes.get(sessionID) !== null) {
1142
1416
  try {
1143
1417
  await this.#sessionModes.set(sessionID, null);
@@ -1163,14 +1437,19 @@ export class Supervisor {
1163
1437
  // report's session's mode.
1164
1438
  const mode = this.#modeFor(record.sessionID, settings);
1165
1439
  let decision;
1440
+ let probeTicket;
1166
1441
  try {
1167
- decision = (await this.#admit(settings, record.sessionID)).decision;
1442
+ const admission = await this.#admit(settings, record.sessionID, record.tier);
1443
+ decision = admission.decision;
1444
+ probeTicket = admission.probeTicket;
1168
1445
  }
1169
1446
  catch (error) {
1170
1447
  logFailOpen("resume", error);
1171
1448
  return;
1172
1449
  }
1173
1450
  if (mode === "observe") {
1451
+ if (probeTicket !== undefined)
1452
+ this.#health.releaseProbe(probeTicket);
1174
1453
  await this.#recordReceipt({
1175
1454
  sessionID: record.sessionID,
1176
1455
  unitID: record.id,
@@ -1180,7 +1459,9 @@ export class Supervisor {
1180
1459
  return;
1181
1460
  }
1182
1461
  if (decision.kind !== "apply") {
1183
- // Still no usable resource (or the routing deferred) — stay parked.
1462
+ // Still no usable resource (or the routing deferred) — stay parked. The
1463
+ // admission held no probe ticket: #admit keeps a ticket only for apply
1464
+ // decisions and releases every other one itself.
1184
1465
  await this.#recordReceipt({
1185
1466
  sessionID: record.sessionID,
1186
1467
  unitID: record.id,
@@ -1190,8 +1471,11 @@ export class Supervisor {
1190
1471
  return;
1191
1472
  }
1192
1473
  const popped = await this.#popParked(record);
1193
- if (popped === undefined)
1474
+ if (popped === undefined) {
1475
+ if (probeTicket !== undefined)
1476
+ this.#health.releaseProbe(probeTicket);
1194
1477
  return;
1478
+ }
1195
1479
  // The resume continuation is not re-admitted (owned continuation), so
1196
1480
  // the freshly routed admission is recorded here, unit-less; the unit
1197
1481
  // that opens next adopts it and carries the outcome.
@@ -1204,10 +1488,23 @@ export class Supervisor {
1204
1488
  // freshly routed model, then prompt — the episode's first LLM call must
1205
1489
  // not hit the model the work was parked away from.
1206
1490
  try {
1207
- await this.#ownRecovery(record.sessionID, decision.model, record.tier);
1491
+ if (probeTicket !== undefined)
1492
+ this.#sessionProbeTickets.set(record.sessionID, probeTicket);
1493
+ await this.#ownRecovery(record.sessionID, decision.model, record.tier,
1494
+ // The parked session's own effective mode and its still-open unit
1495
+ // window are re-checked after the settle; a session that gained an
1496
+ // active unit must not be prompted (not a safe boundary).
1497
+ () => this.#modeFor(record.sessionID, settings) !== "off" &&
1498
+ this.#tracker.active(record.sessionID) === undefined);
1208
1499
  }
1209
1500
  catch (error) {
1210
- logFailOpen("resume", error);
1501
+ if (probeTicket !== undefined &&
1502
+ this.#sessionProbeTickets.get(record.sessionID) === probeTicket) {
1503
+ this.#sessionProbeTickets.delete(record.sessionID);
1504
+ this.#health.releaseProbe(probeTicket);
1505
+ }
1506
+ if (!(error instanceof RecoveryAbortedError))
1507
+ logFailOpen("resume", error);
1211
1508
  await this.#rePark(record);
1212
1509
  }
1213
1510
  }
@@ -1238,8 +1535,14 @@ export class Supervisor {
1238
1535
  await this.#options.session.get({ sessionID });
1239
1536
  return true;
1240
1537
  }
1241
- catch {
1242
- return false;
1538
+ catch (error) {
1539
+ // The host rejects `session.get` for a missing session with a plain
1540
+ // error and exposes no typed status; only a definitive not-found
1541
+ // counts as dead. Anything else is treated as alive (fail open toward
1542
+ // keeping durable user state, ADP-031): a transient storage or host
1543
+ // error must never prune parked work, mode overrides, or tier
1544
+ // requests — the next sweep re-probes instead.
1545
+ return !/not found/i.test(String(error));
1243
1546
  }
1244
1547
  }
1245
1548
  async #recordReceipt(input) {