@coool/opencode-adaptive 0.0.0-stage → 1.0.1

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 (334) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/dist/adapters/codex-capacity/index.d.ts +4 -0
  4. package/dist/adapters/codex-capacity/index.js +5 -0
  5. package/dist/adapters/codex-capacity/index.js.map +1 -0
  6. package/dist/adapters/codex-capacity/parse.d.ts +28 -0
  7. package/dist/adapters/codex-capacity/parse.js +146 -0
  8. package/dist/adapters/codex-capacity/parse.js.map +1 -0
  9. package/dist/adapters/codex-capacity/reading.d.ts +9 -0
  10. package/dist/adapters/codex-capacity/reading.js +45 -0
  11. package/dist/adapters/codex-capacity/reading.js.map +1 -0
  12. package/dist/adapters/codex-capacity/sensor.d.ts +23 -0
  13. package/dist/adapters/codex-capacity/sensor.js +30 -0
  14. package/dist/adapters/codex-capacity/sensor.js.map +1 -0
  15. package/dist/adapters/codex-capacity/types.d.ts +31 -0
  16. package/dist/adapters/codex-capacity/types.js +18 -0
  17. package/dist/adapters/codex-capacity/types.js.map +1 -0
  18. package/dist/adapters/opencode/escalation-tool.d.ts +51 -0
  19. package/dist/adapters/opencode/escalation-tool.js +61 -0
  20. package/dist/adapters/opencode/escalation-tool.js.map +1 -0
  21. package/dist/adapters/opencode/events.d.ts +23 -0
  22. package/dist/adapters/opencode/events.js +68 -0
  23. package/dist/adapters/opencode/events.js.map +1 -0
  24. package/dist/adapters/opencode/hooks.d.ts +34 -0
  25. package/dist/adapters/opencode/hooks.js +29 -0
  26. package/dist/adapters/opencode/hooks.js.map +1 -0
  27. package/dist/adapters/opencode/index.d.ts +16 -0
  28. package/dist/adapters/opencode/index.js +9 -0
  29. package/dist/adapters/opencode/index.js.map +1 -0
  30. package/dist/adapters/opencode/inventory.d.ts +30 -0
  31. package/dist/adapters/opencode/inventory.js +53 -0
  32. package/dist/adapters/opencode/inventory.js.map +1 -0
  33. package/dist/adapters/opencode/report.d.ts +91 -0
  34. package/dist/adapters/opencode/report.js +145 -0
  35. package/dist/adapters/opencode/report.js.map +1 -0
  36. package/dist/adapters/opencode/session-ops.d.ts +39 -0
  37. package/dist/adapters/opencode/session-ops.js +32 -0
  38. package/dist/adapters/opencode/session-ops.js.map +1 -0
  39. package/dist/adapters/opencode/storage.d.ts +15 -0
  40. package/dist/adapters/opencode/storage.js +8 -0
  41. package/dist/adapters/opencode/storage.js.map +1 -0
  42. package/dist/adapters/opencode/tier-tool.d.ts +37 -0
  43. package/dist/adapters/opencode/tier-tool.js +72 -0
  44. package/dist/adapters/opencode/tier-tool.js.map +1 -0
  45. package/dist/adapters/opencode/types.d.ts +101 -0
  46. package/dist/adapters/opencode/types.js +2 -0
  47. package/dist/adapters/opencode/types.js.map +1 -0
  48. package/dist/core/compatibility/gate.d.ts +7 -0
  49. package/dist/core/compatibility/gate.js +51 -0
  50. package/dist/core/compatibility/gate.js.map +1 -0
  51. package/dist/core/compatibility/index.d.ts +2 -0
  52. package/dist/core/compatibility/index.js +3 -0
  53. package/dist/core/compatibility/index.js.map +1 -0
  54. package/dist/core/compatibility/types.d.ts +53 -0
  55. package/dist/core/compatibility/types.js +27 -0
  56. package/dist/core/compatibility/types.js.map +1 -0
  57. package/dist/core/scheduler/admit.d.ts +6 -0
  58. package/dist/core/scheduler/admit.js +154 -0
  59. package/dist/core/scheduler/admit.js.map +1 -0
  60. package/dist/core/scheduler/escalate.d.ts +6 -0
  61. package/dist/core/scheduler/escalate.js +45 -0
  62. package/dist/core/scheduler/escalate.js.map +1 -0
  63. package/dist/core/scheduler/index.d.ts +4 -0
  64. package/dist/core/scheduler/index.js +5 -0
  65. package/dist/core/scheduler/index.js.map +1 -0
  66. package/dist/core/scheduler/mode.d.ts +8 -0
  67. package/dist/core/scheduler/mode.js +10 -0
  68. package/dist/core/scheduler/mode.js.map +1 -0
  69. package/dist/core/scheduler/types.d.ts +136 -0
  70. package/dist/core/scheduler/types.js +75 -0
  71. package/dist/core/scheduler/types.js.map +1 -0
  72. package/dist/core/tier-policy/index.d.ts +3 -0
  73. package/dist/core/tier-policy/index.js +4 -0
  74. package/dist/core/tier-policy/index.js.map +1 -0
  75. package/dist/core/tier-policy/resolve.d.ts +18 -0
  76. package/dist/core/tier-policy/resolve.js +52 -0
  77. package/dist/core/tier-policy/resolve.js.map +1 -0
  78. package/dist/core/tier-policy/tier-list.d.ts +10 -0
  79. package/dist/core/tier-policy/tier-list.js +23 -0
  80. package/dist/core/tier-policy/tier-list.js.map +1 -0
  81. package/dist/core/tier-policy/types.d.ts +86 -0
  82. package/dist/core/tier-policy/types.js +49 -0
  83. package/dist/core/tier-policy/types.js.map +1 -0
  84. package/dist/core/work-unit/index.d.ts +3 -0
  85. package/dist/core/work-unit/index.js +4 -0
  86. package/dist/core/work-unit/index.js.map +1 -0
  87. package/dist/core/work-unit/interruption.d.ts +7 -0
  88. package/dist/core/work-unit/interruption.js +12 -0
  89. package/dist/core/work-unit/interruption.js.map +1 -0
  90. package/dist/core/work-unit/tracker.d.ts +53 -0
  91. package/dist/core/work-unit/tracker.js +156 -0
  92. package/dist/core/work-unit/tracker.js.map +1 -0
  93. package/dist/core/work-unit/types.d.ts +151 -0
  94. package/dist/core/work-unit/types.js +23 -0
  95. package/dist/core/work-unit/types.js.map +1 -0
  96. package/dist/execution/continuation/execute.d.ts +18 -0
  97. package/dist/execution/continuation/execute.js +31 -0
  98. package/dist/execution/continuation/execute.js.map +1 -0
  99. package/dist/execution/continuation/index.d.ts +4 -0
  100. package/dist/execution/continuation/index.js +4 -0
  101. package/dist/execution/continuation/index.js.map +1 -0
  102. package/dist/execution/continuation/plan.d.ts +12 -0
  103. package/dist/execution/continuation/plan.js +18 -0
  104. package/dist/execution/continuation/plan.js.map +1 -0
  105. package/dist/execution/continuation/prompts.d.ts +7 -0
  106. package/dist/execution/continuation/prompts.js +8 -0
  107. package/dist/execution/continuation/prompts.js.map +1 -0
  108. package/dist/execution/continuation/types.d.ts +68 -0
  109. package/dist/execution/continuation/types.js +15 -0
  110. package/dist/execution/continuation/types.js.map +1 -0
  111. package/dist/execution/escalation/index.d.ts +1 -0
  112. package/dist/execution/escalation/index.js +2 -0
  113. package/dist/execution/escalation/index.js.map +1 -0
  114. package/dist/execution/escalation/types.d.ts +30 -0
  115. package/dist/execution/escalation/types.js +49 -0
  116. package/dist/execution/escalation/types.js.map +1 -0
  117. package/dist/execution/index.d.ts +4 -0
  118. package/dist/execution/index.js +5 -0
  119. package/dist/execution/index.js.map +1 -0
  120. package/dist/execution/parking/index.d.ts +2 -0
  121. package/dist/execution/parking/index.js +2 -0
  122. package/dist/execution/parking/index.js.map +1 -0
  123. package/dist/execution/parking/plan.d.ts +39 -0
  124. package/dist/execution/parking/plan.js +34 -0
  125. package/dist/execution/parking/plan.js.map +1 -0
  126. package/dist/execution/retry/decide.d.ts +29 -0
  127. package/dist/execution/retry/decide.js +75 -0
  128. package/dist/execution/retry/decide.js.map +1 -0
  129. package/dist/execution/retry/index.d.ts +2 -0
  130. package/dist/execution/retry/index.js +3 -0
  131. package/dist/execution/retry/index.js.map +1 -0
  132. package/dist/execution/retry/types.d.ts +64 -0
  133. package/dist/execution/retry/types.js +9 -0
  134. package/dist/execution/retry/types.js.map +1 -0
  135. package/dist/gui-experimental.cjs +1 -0
  136. package/dist/gui.d.ts +16 -0
  137. package/dist/gui.js +73 -0
  138. package/dist/gui.js.map +1 -0
  139. package/dist/health/capacity/index.d.ts +3 -0
  140. package/dist/health/capacity/index.js +4 -0
  141. package/dist/health/capacity/index.js.map +1 -0
  142. package/dist/health/capacity/merge.d.ts +57 -0
  143. package/dist/health/capacity/merge.js +154 -0
  144. package/dist/health/capacity/merge.js.map +1 -0
  145. package/dist/health/capacity/registry.d.ts +40 -0
  146. package/dist/health/capacity/registry.js +106 -0
  147. package/dist/health/capacity/registry.js.map +1 -0
  148. package/dist/health/capacity/types.d.ts +73 -0
  149. package/dist/health/capacity/types.js +6 -0
  150. package/dist/health/capacity/types.js.map +1 -0
  151. package/dist/health/circuit-breaker/breaker.d.ts +91 -0
  152. package/dist/health/circuit-breaker/breaker.js +150 -0
  153. package/dist/health/circuit-breaker/breaker.js.map +1 -0
  154. package/dist/health/circuit-breaker/index.d.ts +2 -0
  155. package/dist/health/circuit-breaker/index.js +3 -0
  156. package/dist/health/circuit-breaker/index.js.map +1 -0
  157. package/dist/health/circuit-breaker/types.d.ts +52 -0
  158. package/dist/health/circuit-breaker/types.js +13 -0
  159. package/dist/health/circuit-breaker/types.js.map +1 -0
  160. package/dist/health/classifier/classify.d.ts +8 -0
  161. package/dist/health/classifier/classify.js +245 -0
  162. package/dist/health/classifier/classify.js.map +1 -0
  163. package/dist/health/classifier/index.d.ts +2 -0
  164. package/dist/health/classifier/index.js +3 -0
  165. package/dist/health/classifier/index.js.map +1 -0
  166. package/dist/health/classifier/types.d.ts +71 -0
  167. package/dist/health/classifier/types.js +27 -0
  168. package/dist/health/classifier/types.js.map +1 -0
  169. package/dist/health/index.d.ts +4 -0
  170. package/dist/health/index.js +5 -0
  171. package/dist/health/index.js.map +1 -0
  172. package/dist/health/resource-health/index.d.ts +4 -0
  173. package/dist/health/resource-health/index.js +5 -0
  174. package/dist/health/resource-health/index.js.map +1 -0
  175. package/dist/health/resource-health/key.d.ts +18 -0
  176. package/dist/health/resource-health/key.js +54 -0
  177. package/dist/health/resource-health/key.js.map +1 -0
  178. package/dist/health/resource-health/registry.d.ts +46 -0
  179. package/dist/health/resource-health/registry.js +276 -0
  180. package/dist/health/resource-health/registry.js.map +1 -0
  181. package/dist/health/resource-health/transitions.d.ts +9 -0
  182. package/dist/health/resource-health/transitions.js +157 -0
  183. package/dist/health/resource-health/transitions.js.map +1 -0
  184. package/dist/health/resource-health/types.d.ts +122 -0
  185. package/dist/health/resource-health/types.js +44 -0
  186. package/dist/health/resource-health/types.js.map +1 -0
  187. package/dist/index.d.ts +11 -0
  188. package/dist/index.js +105 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/plugin-id.d.ts +2 -0
  191. package/dist/plugin-id.js +3 -0
  192. package/dist/plugin-id.js.map +1 -0
  193. package/dist/rpc/definition.d.ts +123 -0
  194. package/dist/rpc/definition.js +130 -0
  195. package/dist/rpc/definition.js.map +1 -0
  196. package/dist/rpc/handlers.d.ts +53 -0
  197. package/dist/rpc/handlers.js +125 -0
  198. package/dist/rpc/handlers.js.map +1 -0
  199. package/dist/rpc/status.d.ts +42 -0
  200. package/dist/rpc/status.js +45 -0
  201. package/dist/rpc/status.js.map +1 -0
  202. package/dist/rpc.d.ts +10 -0
  203. package/dist/rpc.js +8 -0
  204. package/dist/rpc.js.map +1 -0
  205. package/dist/state/capacity/index.d.ts +3 -0
  206. package/dist/state/capacity/index.js +4 -0
  207. package/dist/state/capacity/index.js.map +1 -0
  208. package/dist/state/capacity/parse.d.ts +2 -0
  209. package/dist/state/capacity/parse.js +122 -0
  210. package/dist/state/capacity/parse.js.map +1 -0
  211. package/dist/state/capacity/store.d.ts +18 -0
  212. package/dist/state/capacity/store.js +78 -0
  213. package/dist/state/capacity/store.js.map +1 -0
  214. package/dist/state/capacity/types.d.ts +37 -0
  215. package/dist/state/capacity/types.js +11 -0
  216. package/dist/state/capacity/types.js.map +1 -0
  217. package/dist/state/health/index.d.ts +4 -0
  218. package/dist/state/health/index.js +4 -0
  219. package/dist/state/health/index.js.map +1 -0
  220. package/dist/state/health/parse.d.ts +2 -0
  221. package/dist/state/health/parse.js +137 -0
  222. package/dist/state/health/parse.js.map +1 -0
  223. package/dist/state/health/store.d.ts +17 -0
  224. package/dist/state/health/store.js +69 -0
  225. package/dist/state/health/store.js.map +1 -0
  226. package/dist/state/health/types.d.ts +23 -0
  227. package/dist/state/health/types.js +5 -0
  228. package/dist/state/health/types.js.map +1 -0
  229. package/dist/state/parked-work/index.d.ts +4 -0
  230. package/dist/state/parked-work/index.js +4 -0
  231. package/dist/state/parked-work/index.js.map +1 -0
  232. package/dist/state/parked-work/parse.d.ts +6 -0
  233. package/dist/state/parked-work/parse.js +83 -0
  234. package/dist/state/parked-work/parse.js.map +1 -0
  235. package/dist/state/parked-work/store.d.ts +17 -0
  236. package/dist/state/parked-work/store.js +98 -0
  237. package/dist/state/parked-work/store.js.map +1 -0
  238. package/dist/state/parked-work/types.d.ts +41 -0
  239. package/dist/state/parked-work/types.js +7 -0
  240. package/dist/state/parked-work/types.js.map +1 -0
  241. package/dist/state/receipts/bound.d.ts +14 -0
  242. package/dist/state/receipts/bound.js +77 -0
  243. package/dist/state/receipts/bound.js.map +1 -0
  244. package/dist/state/receipts/index.d.ts +6 -0
  245. package/dist/state/receipts/index.js +5 -0
  246. package/dist/state/receipts/index.js.map +1 -0
  247. package/dist/state/receipts/parse.d.ts +6 -0
  248. package/dist/state/receipts/parse.js +226 -0
  249. package/dist/state/receipts/parse.js.map +1 -0
  250. package/dist/state/receipts/store.d.ts +34 -0
  251. package/dist/state/receipts/store.js +148 -0
  252. package/dist/state/receipts/store.js.map +1 -0
  253. package/dist/state/receipts/types.d.ts +61 -0
  254. package/dist/state/receipts/types.js +12 -0
  255. package/dist/state/receipts/types.js.map +1 -0
  256. package/dist/state/session-modes/index.d.ts +4 -0
  257. package/dist/state/session-modes/index.js +4 -0
  258. package/dist/state/session-modes/index.js.map +1 -0
  259. package/dist/state/session-modes/parse.d.ts +8 -0
  260. package/dist/state/session-modes/parse.js +62 -0
  261. package/dist/state/session-modes/parse.js.map +1 -0
  262. package/dist/state/session-modes/store.d.ts +20 -0
  263. package/dist/state/session-modes/store.js +87 -0
  264. package/dist/state/session-modes/store.js.map +1 -0
  265. package/dist/state/session-modes/types.d.ts +41 -0
  266. package/dist/state/session-modes/types.js +7 -0
  267. package/dist/state/session-modes/types.js.map +1 -0
  268. package/dist/state/settings/index.d.ts +4 -0
  269. package/dist/state/settings/index.js +4 -0
  270. package/dist/state/settings/index.js.map +1 -0
  271. package/dist/state/settings/parse.d.ts +33 -0
  272. package/dist/state/settings/parse.js +261 -0
  273. package/dist/state/settings/parse.js.map +1 -0
  274. package/dist/state/settings/store.d.ts +21 -0
  275. package/dist/state/settings/store.js +65 -0
  276. package/dist/state/settings/store.js.map +1 -0
  277. package/dist/state/settings/types.d.ts +62 -0
  278. package/dist/state/settings/types.js +12 -0
  279. package/dist/state/settings/types.js.map +1 -0
  280. package/dist/state/tier-requests/index.d.ts +4 -0
  281. package/dist/state/tier-requests/index.js +4 -0
  282. package/dist/state/tier-requests/index.js.map +1 -0
  283. package/dist/state/tier-requests/parse.d.ts +3 -0
  284. package/dist/state/tier-requests/parse.js +53 -0
  285. package/dist/state/tier-requests/parse.js.map +1 -0
  286. package/dist/state/tier-requests/store.d.ts +27 -0
  287. package/dist/state/tier-requests/store.js +96 -0
  288. package/dist/state/tier-requests/store.js.map +1 -0
  289. package/dist/state/tier-requests/types.d.ts +30 -0
  290. package/dist/state/tier-requests/types.js +7 -0
  291. package/dist/state/tier-requests/types.js.map +1 -0
  292. package/dist/supervisor/index.d.ts +4 -0
  293. package/dist/supervisor/index.js +4 -0
  294. package/dist/supervisor/index.js.map +1 -0
  295. package/dist/supervisor/log.d.ts +10 -0
  296. package/dist/supervisor/log.js +14 -0
  297. package/dist/supervisor/log.js.map +1 -0
  298. package/dist/supervisor/supervisor.d.ts +86 -0
  299. package/dist/supervisor/supervisor.js +1629 -0
  300. package/dist/supervisor/supervisor.js.map +1 -0
  301. package/dist/supervisor/types.d.ts +49 -0
  302. package/dist/supervisor/types.js +3 -0
  303. package/dist/supervisor/types.js.map +1 -0
  304. package/dist/supervisor/view.d.ts +3 -0
  305. package/dist/supervisor/view.js +4 -0
  306. package/dist/supervisor/view.js.map +1 -0
  307. package/dist/tui.d.ts +15 -0
  308. package/dist/tui.js +256 -0
  309. package/dist/tui.js.map +1 -0
  310. package/dist/ui/gui/api.d.ts +17 -0
  311. package/dist/ui/gui/api.js +21 -0
  312. package/dist/ui/gui/api.js.map +1 -0
  313. package/dist/ui/gui/view.d.ts +26 -0
  314. package/dist/ui/gui/view.js +49 -0
  315. package/dist/ui/gui/view.js.map +1 -0
  316. package/dist/ui/tui/controller.d.ts +97 -0
  317. package/dist/ui/tui/controller.js +415 -0
  318. package/dist/ui/tui/controller.js.map +1 -0
  319. package/dist/ui/tui/menu.d.ts +55 -0
  320. package/dist/ui/tui/menu.js +176 -0
  321. package/dist/ui/tui/menu.js.map +1 -0
  322. package/dist/ui/tui/options.d.ts +19 -0
  323. package/dist/ui/tui/options.js +17 -0
  324. package/dist/ui/tui/options.js.map +1 -0
  325. package/dist/ui/tui/receipts.d.ts +21 -0
  326. package/dist/ui/tui/receipts.js +112 -0
  327. package/dist/ui/tui/receipts.js.map +1 -0
  328. package/dist/ui/tui/status.d.ts +22 -0
  329. package/dist/ui/tui/status.js +92 -0
  330. package/dist/ui/tui/status.js.map +1 -0
  331. package/dist/ui/tui/tiers.d.ts +51 -0
  332. package/dist/ui/tui/tiers.js +108 -0
  333. package/dist/ui/tui/tiers.js.map +1 -0
  334. package/package.json +85 -4
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Text fed to the textual last-resort layer is truncated to this bound before
3
+ * matching, so adversarial or enormous error bodies cannot dominate
4
+ * classification cost or smuggle patterns in late bytes (ADP-008).
5
+ */
6
+ const TEXT_BOUND = 512;
7
+ /**
8
+ * Retry/failover policy per category (ADP-010, ADP-011).
9
+ *
10
+ * - Infrastructure failures (rate limiting, quota, outage, overload, timeout)
11
+ * are failover-eligible: horizontal recovery within the tier is exactly
12
+ * their remedy.
13
+ * - Permanent configuration failures (auth, invalid model, content policy)
14
+ * and context overflow never retry and never failover: retrying the same
15
+ * request cannot help and switching resources would reproduce the failure.
16
+ * - User cancellation is final: no retry, no failover (ADP-010).
17
+ * - Unknown failures defer to OpenCode's native handling.
18
+ */
19
+ const CATEGORY_POLICY = {
20
+ rate_limited: { retryable: true, failoverEligible: true },
21
+ quota_exhausted: { retryable: false, failoverEligible: true },
22
+ outage: { retryable: true, failoverEligible: true },
23
+ overload: { retryable: true, failoverEligible: true },
24
+ timeout: { retryable: true, failoverEligible: true },
25
+ auth_failure: { retryable: false, failoverEligible: false },
26
+ invalid_model: { retryable: false, failoverEligible: false },
27
+ context_overflow: { retryable: false, failoverEligible: false },
28
+ content_policy: { retryable: false, failoverEligible: false },
29
+ user_cancelled: { retryable: false, failoverEligible: false },
30
+ unknown: { retryable: false, failoverEligible: false },
31
+ };
32
+ /**
33
+ * Error tags from the documented @opencode/protocol vocabulary that map
34
+ * directly to a failure category. Tags absent here (`ProviderRequestError`,
35
+ * `UnknownError`, anything unrecognized) are structured but not
36
+ * category-specific: they never guess, and classification falls through to
37
+ * the refining layers.
38
+ */
39
+ const STRUCTURED_TYPES = new Map([
40
+ ["UnauthorizedError", "auth_failure"],
41
+ ["ForbiddenError", "auth_failure"],
42
+ ["ModelNotFoundError", "invalid_model"],
43
+ ["UnsupportedModelError", "invalid_model"],
44
+ ["ServiceUnavailableError", "outage"],
45
+ ]);
46
+ /**
47
+ * Bounded textual patterns, last resort only (ADP-008). Ordered most
48
+ * specific first; the first match wins. These encode provider error-message
49
+ * conventions documented by the research phase, not a general-purpose
50
+ * keyword classifier.
51
+ */
52
+ const TEXTUAL_PATTERNS = [
53
+ {
54
+ category: "context_overflow",
55
+ pattern: /context (length|window)|maximum context|input length exceeds|prompt is too long|too many (input )?tokens|exceeds the maximum/i,
56
+ },
57
+ { category: "quota_exhausted", pattern: /\bquota\b|credit balance/i },
58
+ { category: "rate_limited", pattern: /rate ?limit/i },
59
+ { category: "timeout", pattern: /\btimeout\b|timed? ?out|deadline exceeded|etimedout/i },
60
+ {
61
+ category: "auth_failure",
62
+ pattern: /unauthorized|invalid (api|access) key|api key (expired|invalid)|authentication|permission denied/i,
63
+ },
64
+ {
65
+ category: "invalid_model",
66
+ pattern: /model.{0,30}(does not exist|not found)|invalid model|unknown model|no such model/i,
67
+ },
68
+ {
69
+ category: "content_policy",
70
+ pattern: /content (management )?(policy|filter)|safety system|violates?.{0,32}policy|flagged as/i,
71
+ },
72
+ { category: "overload", pattern: /overloaded|over capacity|high demand/i },
73
+ { category: "user_cancelled", pattern: /cancel+(l)?ed|aborted by (the )?user|user abort/i },
74
+ ];
75
+ function isHttpStatus(status) {
76
+ return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599;
77
+ }
78
+ /**
79
+ * Upper bound on a provider-claimed retry delay. `Retry-After` and structured
80
+ * retry metadata are untrusted provider data: a hostile or misconfigured
81
+ * value ("999999999" seconds) must not convert one response into a
82
+ * near-permanent resource blacklist. The host's own retry scheduling waits
83
+ * out the raw header verbatim — this bound only concerns the health window
84
+ * the supervisor derives from the classification (ADP-011 boundedness).
85
+ */
86
+ const MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1000;
87
+ function boundedRetryAfterMs(retryAfterMs) {
88
+ if (retryAfterMs === undefined) {
89
+ return undefined;
90
+ }
91
+ return Math.min(retryAfterMs, MAX_RETRY_AFTER_MS);
92
+ }
93
+ function isNonNegativeInteger(value) {
94
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
95
+ }
96
+ function lowercasedHeaders(headers) {
97
+ const lowered = new Map();
98
+ if (headers === undefined) {
99
+ return lowered;
100
+ }
101
+ for (const [name, value] of Object.entries(headers)) {
102
+ lowered.set(name.toLowerCase(), value);
103
+ }
104
+ return lowered;
105
+ }
106
+ /**
107
+ * Parses a `Retry-After` header into milliseconds. Only the integer-seconds
108
+ * form is accepted: the HTTP-date form requires a clock, which the classifier
109
+ * deliberately does not have (reset-time handling belongs to the capacity
110
+ * boundary, T-05/T-15).
111
+ */
112
+ function retryAfterHeaderMs(headers) {
113
+ const raw = headers.get("retry-after");
114
+ if (raw === undefined || !/^\d+$/.test(raw)) {
115
+ return undefined;
116
+ }
117
+ return Number(raw) * 1000;
118
+ }
119
+ /**
120
+ * Standard rate-limit headers (case-insensitive `x-ratelimit-remaining*`).
121
+ * All-zero remaining capacity indicates a fixed-window quota exhaustion
122
+ * rather than a short burst limit.
123
+ */
124
+ function fixedWindowExhausted(headers) {
125
+ let sawRemaining = false;
126
+ for (const [name, value] of headers) {
127
+ if (name.startsWith("x-ratelimit-remaining")) {
128
+ sawRemaining = true;
129
+ if (value !== "0") {
130
+ return false;
131
+ }
132
+ }
133
+ }
134
+ return sawRemaining;
135
+ }
136
+ function retryAdvice(category, retryAfterMs) {
137
+ const policy = CATEGORY_POLICY[category];
138
+ const bounded = boundedRetryAfterMs(retryAfterMs);
139
+ if (bounded === undefined) {
140
+ return { retryable: policy.retryable };
141
+ }
142
+ return { retryable: policy.retryable, retryAfterMs: bounded };
143
+ }
144
+ function classification(category, layer, definitive, retryAfterMs) {
145
+ const policy = CATEGORY_POLICY[category];
146
+ return {
147
+ category,
148
+ layer,
149
+ definitive,
150
+ retry: retryAdvice(category, retryAfterMs),
151
+ failoverEligible: policy.failoverEligible,
152
+ };
153
+ }
154
+ /**
155
+ * Classifies an HTTP status. 429 consults the standard rate-limit headers in
156
+ * the same step: status plus documented standard headers are one HTTP
157
+ * semantics layer, and the quota refinement is more specific evidence than a
158
+ * bare 429, so it cannot weaken the ADP-008 precedence.
159
+ */
160
+ function classifyStatus(status, headers) {
161
+ if (status === 429) {
162
+ return fixedWindowExhausted(headers) ? "quota_exhausted" : "rate_limited";
163
+ }
164
+ if (status === 401 || status === 403) {
165
+ return "auth_failure";
166
+ }
167
+ if (status === 408) {
168
+ return "timeout";
169
+ }
170
+ if (status === 413) {
171
+ return "context_overflow";
172
+ }
173
+ if (status === 502 || status === 503 || status === 504) {
174
+ return "outage";
175
+ }
176
+ return undefined;
177
+ }
178
+ function classifyText(text) {
179
+ const bounded = text.length > TEXT_BOUND ? text.slice(0, TEXT_BOUND) : text;
180
+ for (const { category, pattern } of TEXTUAL_PATTERNS) {
181
+ if (pattern.test(bounded)) {
182
+ return category;
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+ /**
188
+ * Total classification of layered failure evidence. Never throws and always
189
+ * returns a category: any evidence shape outside the documented vocabularies
190
+ * classifies as `unknown` via the `none` layer, deferring to OpenCode's
191
+ * native failure handling (ADP-009, ADP-012).
192
+ */
193
+ export function classifyFailure(evidence) {
194
+ // Layer 0: interruption evidence. Interruptions are host decisions, not
195
+ // provider failures; a user cancellation is final (ADP-010).
196
+ if (evidence.interrupted === "user") {
197
+ return classification("user_cancelled", "interruption", true, undefined);
198
+ }
199
+ if (evidence.interrupted !== undefined) {
200
+ return classification("unknown", "interruption", true, undefined);
201
+ }
202
+ // Layer 1: structured OpenCode metadata.
203
+ const structured = evidence.structured;
204
+ const structuredRetryAfterMs = isNonNegativeInteger(structured?.retryAfterMs)
205
+ ? structured.retryAfterMs
206
+ : undefined;
207
+ if (structured !== undefined) {
208
+ const mapped = STRUCTURED_TYPES.get(structured.type);
209
+ if (mapped !== undefined) {
210
+ return classification(mapped, "structured", true, structuredRetryAfterMs);
211
+ }
212
+ // Unmapped tag: fall through so HTTP semantics can refine the category.
213
+ }
214
+ // Layer 2: HTTP status (+ standard headers for the 429 refinement).
215
+ const headers = lowercasedHeaders(evidence.headers);
216
+ if (isHttpStatus(evidence.status)) {
217
+ const category = classifyStatus(evidence.status, headers);
218
+ if (category !== undefined) {
219
+ return classification(category, "status", true, structuredRetryAfterMs ?? retryAfterHeaderMs(headers));
220
+ }
221
+ }
222
+ // Layer 3: standard headers alone (Retry-After without a decisive status).
223
+ const headerRetryAfterMs = retryAfterHeaderMs(headers);
224
+ if (headerRetryAfterMs !== undefined) {
225
+ return classification("rate_limited", "headers", false, headerRetryAfterMs);
226
+ }
227
+ // Layer 4: documented provider-specific metadata, normalized by our own
228
+ // adapters after validation (T-15).
229
+ if (evidence.providerMetadata?.quotaExhausted === true) {
230
+ return classification("quota_exhausted", "provider-metadata", true, undefined);
231
+ }
232
+ if (evidence.providerMetadata?.overloaded === true) {
233
+ return classification("overload", "provider-metadata", true, structuredRetryAfterMs);
234
+ }
235
+ // Layer 5: bounded textual classification, always non-definitive.
236
+ const text = evidence.text;
237
+ if (text !== undefined) {
238
+ const category = classifyText(text);
239
+ if (category !== undefined) {
240
+ return classification(category, "textual", false, structuredRetryAfterMs);
241
+ }
242
+ }
243
+ return classification("unknown", "none", false, undefined);
244
+ }
245
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/health/classifier/classify.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ErrorEvidence, FailureCategory, FailureClassification, RetryAdvice } from \"./types.js\"\n\n/**\n * Text fed to the textual last-resort layer is truncated to this bound before\n * matching, so adversarial or enormous error bodies cannot dominate\n * classification cost or smuggle patterns in late bytes (ADP-008).\n */\nconst TEXT_BOUND = 512\n\n/**\n * Retry/failover policy per category (ADP-010, ADP-011).\n *\n * - Infrastructure failures (rate limiting, quota, outage, overload, timeout)\n * are failover-eligible: horizontal recovery within the tier is exactly\n * their remedy.\n * - Permanent configuration failures (auth, invalid model, content policy)\n * and context overflow never retry and never failover: retrying the same\n * request cannot help and switching resources would reproduce the failure.\n * - User cancellation is final: no retry, no failover (ADP-010).\n * - Unknown failures defer to OpenCode's native handling.\n */\nconst CATEGORY_POLICY: Readonly<\n Record<FailureCategory, { retryable: boolean; failoverEligible: boolean }>\n> = {\n rate_limited: { retryable: true, failoverEligible: true },\n quota_exhausted: { retryable: false, failoverEligible: true },\n outage: { retryable: true, failoverEligible: true },\n overload: { retryable: true, failoverEligible: true },\n timeout: { retryable: true, failoverEligible: true },\n auth_failure: { retryable: false, failoverEligible: false },\n invalid_model: { retryable: false, failoverEligible: false },\n context_overflow: { retryable: false, failoverEligible: false },\n content_policy: { retryable: false, failoverEligible: false },\n user_cancelled: { retryable: false, failoverEligible: false },\n unknown: { retryable: false, failoverEligible: false },\n}\n\n/**\n * Error tags from the documented @opencode/protocol vocabulary that map\n * directly to a failure category. Tags absent here (`ProviderRequestError`,\n * `UnknownError`, anything unrecognized) are structured but not\n * category-specific: they never guess, and classification falls through to\n * the refining layers.\n */\nconst STRUCTURED_TYPES = new Map<string, FailureCategory>([\n [\"UnauthorizedError\", \"auth_failure\"],\n [\"ForbiddenError\", \"auth_failure\"],\n [\"ModelNotFoundError\", \"invalid_model\"],\n [\"UnsupportedModelError\", \"invalid_model\"],\n [\"ServiceUnavailableError\", \"outage\"],\n])\n\n/**\n * Bounded textual patterns, last resort only (ADP-008). Ordered most\n * specific first; the first match wins. These encode provider error-message\n * conventions documented by the research phase, not a general-purpose\n * keyword classifier.\n */\nconst TEXTUAL_PATTERNS: readonly {\n readonly category: FailureCategory\n readonly pattern: RegExp\n}[] = [\n {\n category: \"context_overflow\",\n pattern:\n /context (length|window)|maximum context|input length exceeds|prompt is too long|too many (input )?tokens|exceeds the maximum/i,\n },\n { category: \"quota_exhausted\", pattern: /\\bquota\\b|credit balance/i },\n { category: \"rate_limited\", pattern: /rate ?limit/i },\n { category: \"timeout\", pattern: /\\btimeout\\b|timed? ?out|deadline exceeded|etimedout/i },\n {\n category: \"auth_failure\",\n pattern:\n /unauthorized|invalid (api|access) key|api key (expired|invalid)|authentication|permission denied/i,\n },\n {\n category: \"invalid_model\",\n pattern: /model.{0,30}(does not exist|not found)|invalid model|unknown model|no such model/i,\n },\n {\n category: \"content_policy\",\n pattern:\n /content (management )?(policy|filter)|safety system|violates?.{0,32}policy|flagged as/i,\n },\n { category: \"overload\", pattern: /overloaded|over capacity|high demand/i },\n { category: \"user_cancelled\", pattern: /cancel+(l)?ed|aborted by (the )?user|user abort/i },\n]\n\nfunction isHttpStatus(status: number | undefined): status is number {\n return typeof status === \"number\" && Number.isInteger(status) && status >= 100 && status <= 599\n}\n\n/**\n * Upper bound on a provider-claimed retry delay. `Retry-After` and structured\n * retry metadata are untrusted provider data: a hostile or misconfigured\n * value (\"999999999\" seconds) must not convert one response into a\n * near-permanent resource blacklist. The host's own retry scheduling waits\n * out the raw header verbatim — this bound only concerns the health window\n * the supervisor derives from the classification (ADP-011 boundedness).\n */\nconst MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1000\n\nfunction boundedRetryAfterMs(retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs === undefined) {\n return undefined\n }\n return Math.min(retryAfterMs, MAX_RETRY_AFTER_MS)\n}\n\nfunction isNonNegativeInteger(value: number | undefined): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n}\n\nfunction lowercasedHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n): ReadonlyMap<string, string> {\n const lowered = new Map<string, string>()\n if (headers === undefined) {\n return lowered\n }\n for (const [name, value] of Object.entries(headers)) {\n lowered.set(name.toLowerCase(), value)\n }\n return lowered\n}\n\n/**\n * Parses a `Retry-After` header into milliseconds. Only the integer-seconds\n * form is accepted: the HTTP-date form requires a clock, which the classifier\n * deliberately does not have (reset-time handling belongs to the capacity\n * boundary, T-05/T-15).\n */\nfunction retryAfterHeaderMs(headers: ReadonlyMap<string, string>): number | undefined {\n const raw = headers.get(\"retry-after\")\n if (raw === undefined || !/^\\d+$/.test(raw)) {\n return undefined\n }\n return Number(raw) * 1000\n}\n\n/**\n * Standard rate-limit headers (case-insensitive `x-ratelimit-remaining*`).\n * All-zero remaining capacity indicates a fixed-window quota exhaustion\n * rather than a short burst limit.\n */\nfunction fixedWindowExhausted(headers: ReadonlyMap<string, string>): boolean {\n let sawRemaining = false\n for (const [name, value] of headers) {\n if (name.startsWith(\"x-ratelimit-remaining\")) {\n sawRemaining = true\n if (value !== \"0\") {\n return false\n }\n }\n }\n return sawRemaining\n}\n\nfunction retryAdvice(category: FailureCategory, retryAfterMs: number | undefined): RetryAdvice {\n const policy = CATEGORY_POLICY[category]\n const bounded = boundedRetryAfterMs(retryAfterMs)\n if (bounded === undefined) {\n return { retryable: policy.retryable }\n }\n return { retryable: policy.retryable, retryAfterMs: bounded }\n}\n\nfunction classification(\n category: FailureCategory,\n layer: FailureClassification[\"layer\"],\n definitive: boolean,\n retryAfterMs: number | undefined,\n): FailureClassification {\n const policy = CATEGORY_POLICY[category]\n return {\n category,\n layer,\n definitive,\n retry: retryAdvice(category, retryAfterMs),\n failoverEligible: policy.failoverEligible,\n }\n}\n\n/**\n * Classifies an HTTP status. 429 consults the standard rate-limit headers in\n * the same step: status plus documented standard headers are one HTTP\n * semantics layer, and the quota refinement is more specific evidence than a\n * bare 429, so it cannot weaken the ADP-008 precedence.\n */\nfunction classifyStatus(\n status: number,\n headers: ReadonlyMap<string, string>,\n): FailureCategory | undefined {\n if (status === 429) {\n return fixedWindowExhausted(headers) ? \"quota_exhausted\" : \"rate_limited\"\n }\n if (status === 401 || status === 403) {\n return \"auth_failure\"\n }\n if (status === 408) {\n return \"timeout\"\n }\n if (status === 413) {\n return \"context_overflow\"\n }\n if (status === 502 || status === 503 || status === 504) {\n return \"outage\"\n }\n return undefined\n}\n\nfunction classifyText(text: string): FailureCategory | undefined {\n const bounded = text.length > TEXT_BOUND ? text.slice(0, TEXT_BOUND) : text\n for (const { category, pattern } of TEXTUAL_PATTERNS) {\n if (pattern.test(bounded)) {\n return category\n }\n }\n return undefined\n}\n\n/**\n * Total classification of layered failure evidence. Never throws and always\n * returns a category: any evidence shape outside the documented vocabularies\n * classifies as `unknown` via the `none` layer, deferring to OpenCode's\n * native failure handling (ADP-009, ADP-012).\n */\nexport function classifyFailure(evidence: ErrorEvidence): FailureClassification {\n // Layer 0: interruption evidence. Interruptions are host decisions, not\n // provider failures; a user cancellation is final (ADP-010).\n if (evidence.interrupted === \"user\") {\n return classification(\"user_cancelled\", \"interruption\", true, undefined)\n }\n if (evidence.interrupted !== undefined) {\n return classification(\"unknown\", \"interruption\", true, undefined)\n }\n\n // Layer 1: structured OpenCode metadata.\n const structured = evidence.structured\n const structuredRetryAfterMs = isNonNegativeInteger(structured?.retryAfterMs)\n ? structured.retryAfterMs\n : undefined\n if (structured !== undefined) {\n const mapped = STRUCTURED_TYPES.get(structured.type)\n if (mapped !== undefined) {\n return classification(mapped, \"structured\", true, structuredRetryAfterMs)\n }\n // Unmapped tag: fall through so HTTP semantics can refine the category.\n }\n\n // Layer 2: HTTP status (+ standard headers for the 429 refinement).\n const headers = lowercasedHeaders(evidence.headers)\n if (isHttpStatus(evidence.status)) {\n const category = classifyStatus(evidence.status, headers)\n if (category !== undefined) {\n return classification(\n category,\n \"status\",\n true,\n structuredRetryAfterMs ?? retryAfterHeaderMs(headers),\n )\n }\n }\n\n // Layer 3: standard headers alone (Retry-After without a decisive status).\n const headerRetryAfterMs = retryAfterHeaderMs(headers)\n if (headerRetryAfterMs !== undefined) {\n return classification(\"rate_limited\", \"headers\", false, headerRetryAfterMs)\n }\n\n // Layer 4: documented provider-specific metadata, normalized by our own\n // adapters after validation (T-15).\n if (evidence.providerMetadata?.quotaExhausted === true) {\n return classification(\"quota_exhausted\", \"provider-metadata\", true, undefined)\n }\n if (evidence.providerMetadata?.overloaded === true) {\n return classification(\"overload\", \"provider-metadata\", true, structuredRetryAfterMs)\n }\n\n // Layer 5: bounded textual classification, always non-definitive.\n const text = evidence.text\n if (text !== undefined) {\n const category = classifyText(text)\n if (category !== undefined) {\n return classification(category, \"textual\", false, structuredRetryAfterMs)\n }\n }\n\n return classification(\"unknown\", \"none\", false, undefined)\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./classify.js";
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./classify.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/health/classifier/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA","sourcesContent":["export * from \"./types.js\"\nexport * from \"./classify.js\"\n"]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Failure classification types (ADP-008, ADP-009).
3
+ *
4
+ * The classifier consumes a layered evidence record assembled by the OpenCode
5
+ * adapter. Evidence layers exist so classification can honor the ADP-008
6
+ * precedence chain: structured metadata beats HTTP status beats standard
7
+ * headers beats documented provider-specific metadata beats bounded text.
8
+ */
9
+ import type { InterruptReason } from "../../core/work-unit/interruption.js";
10
+ export declare const FAILURE_CATEGORIES: readonly ["rate_limited", "quota_exhausted", "outage", "overload", "timeout", "auth_failure", "invalid_model", "context_overflow", "content_policy", "user_cancelled", "unknown"];
11
+ export type FailureCategory = (typeof FAILURE_CATEGORIES)[number];
12
+ /**
13
+ * Where the winning classification came from. Exposed so receipts can show
14
+ * why a decision was made and so tests can pin the precedence chain.
15
+ */
16
+ export declare const EVIDENCE_LAYERS: readonly ["interruption", "structured", "status", "headers", "provider-metadata", "textual", "none"];
17
+ export type EvidenceLayer = (typeof EVIDENCE_LAYERS)[number];
18
+ /**
19
+ * Structured OpenCode evidence (`Session.StructuredError`): a server-assigned
20
+ * error tag plus, when the provider sent it, a retry delay OpenCode already
21
+ * parsed from `Retry-After`. `type` is a free-form string on the wire, so the
22
+ * classifier only interprets types from the documented @opencode/protocol
23
+ * vocabulary and falls through to lower layers for anything unmapped instead
24
+ * of guessing.
25
+ */
26
+ export interface StructuredSignal {
27
+ readonly type: string;
28
+ readonly retryAfterMs?: number | undefined;
29
+ }
30
+ /**
31
+ * Documented provider-specific metadata, already normalized by our own
32
+ * adapters (e.g. the Codex capacity sensor validates `x-codex-*` headers
33
+ * before producing these). The classifier never reads raw provider headers
34
+ * itself.
35
+ */
36
+ export interface ProviderMetadata {
37
+ readonly quotaExhausted?: boolean | undefined;
38
+ readonly overloaded?: boolean | undefined;
39
+ }
40
+ /**
41
+ * Layered failure evidence. `status` is the merged HTTP status (the adapter
42
+ * prefers the wire status seen by `http.response` over any status echoed in
43
+ * structured metadata). `text` is the bounded concatenation of error message
44
+ * and response body, bounded again defensively inside the classifier.
45
+ */
46
+ export interface ErrorEvidence {
47
+ readonly interrupted?: InterruptReason | undefined;
48
+ readonly structured?: StructuredSignal | undefined;
49
+ readonly status?: number | undefined;
50
+ readonly headers?: Readonly<Record<string, string>> | undefined;
51
+ readonly providerMetadata?: ProviderMetadata | undefined;
52
+ readonly text?: string | undefined;
53
+ }
54
+ /** Advisory retry semantics; the retry policy (T-12) makes final decisions. */
55
+ export interface RetryAdvice {
56
+ readonly retryable: boolean;
57
+ readonly retryAfterMs?: number;
58
+ }
59
+ export interface FailureClassification {
60
+ readonly category: FailureCategory;
61
+ readonly layer: EvidenceLayer;
62
+ /**
63
+ * True when the evidence conclusively identifies the failure. Textual
64
+ * classifications are always non-definitive; scheduling treats
65
+ * non-definitive evidence more conservatively (ADP-012).
66
+ */
67
+ readonly definitive: boolean;
68
+ readonly retry: RetryAdvice;
69
+ /** Horizontal recovery (switch resource within the tier) is allowed. */
70
+ readonly failoverEligible: boolean;
71
+ }
@@ -0,0 +1,27 @@
1
+ export const FAILURE_CATEGORIES = [
2
+ "rate_limited",
3
+ "quota_exhausted",
4
+ "outage",
5
+ "overload",
6
+ "timeout",
7
+ "auth_failure",
8
+ "invalid_model",
9
+ "context_overflow",
10
+ "content_policy",
11
+ "user_cancelled",
12
+ "unknown",
13
+ ];
14
+ /**
15
+ * Where the winning classification came from. Exposed so receipts can show
16
+ * why a decision was made and so tests can pin the precedence chain.
17
+ */
18
+ export const EVIDENCE_LAYERS = [
19
+ "interruption",
20
+ "structured",
21
+ "status",
22
+ "headers",
23
+ "provider-metadata",
24
+ "textual",
25
+ "none",
26
+ ];
27
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/health/classifier/types.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,cAAc;IACd,iBAAiB;IACjB,QAAQ;IACR,UAAU;IACV,SAAS;IACT,cAAc;IACd,eAAe;IACf,kBAAkB;IAClB,gBAAgB;IAChB,gBAAgB;IAChB,SAAS;CAC2B,CAAA;AAItC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,cAAc;IACd,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,mBAAmB;IACnB,SAAS;IACT,MAAM;CAC8B,CAAA","sourcesContent":["/**\n * Failure classification types (ADP-008, ADP-009).\n *\n * The classifier consumes a layered evidence record assembled by the OpenCode\n * adapter. Evidence layers exist so classification can honor the ADP-008\n * precedence chain: structured metadata beats HTTP status beats standard\n * headers beats documented provider-specific metadata beats bounded text.\n */\nimport type { InterruptReason } from \"../../core/work-unit/interruption.js\"\n\nexport const FAILURE_CATEGORIES = [\n \"rate_limited\",\n \"quota_exhausted\",\n \"outage\",\n \"overload\",\n \"timeout\",\n \"auth_failure\",\n \"invalid_model\",\n \"context_overflow\",\n \"content_policy\",\n \"user_cancelled\",\n \"unknown\",\n] as const satisfies readonly string[]\n\nexport type FailureCategory = (typeof FAILURE_CATEGORIES)[number]\n\n/**\n * Where the winning classification came from. Exposed so receipts can show\n * why a decision was made and so tests can pin the precedence chain.\n */\nexport const EVIDENCE_LAYERS = [\n \"interruption\",\n \"structured\",\n \"status\",\n \"headers\",\n \"provider-metadata\",\n \"textual\",\n \"none\",\n] as const satisfies readonly string[]\n\nexport type EvidenceLayer = (typeof EVIDENCE_LAYERS)[number]\n\n/**\n * Structured OpenCode evidence (`Session.StructuredError`): a server-assigned\n * error tag plus, when the provider sent it, a retry delay OpenCode already\n * parsed from `Retry-After`. `type` is a free-form string on the wire, so the\n * classifier only interprets types from the documented @opencode/protocol\n * vocabulary and falls through to lower layers for anything unmapped instead\n * of guessing.\n */\nexport interface StructuredSignal {\n readonly type: string\n readonly retryAfterMs?: number | undefined\n}\n\n/**\n * Documented provider-specific metadata, already normalized by our own\n * adapters (e.g. the Codex capacity sensor validates `x-codex-*` headers\n * before producing these). The classifier never reads raw provider headers\n * itself.\n */\nexport interface ProviderMetadata {\n readonly quotaExhausted?: boolean | undefined\n readonly overloaded?: boolean | undefined\n}\n\n/**\n * Layered failure evidence. `status` is the merged HTTP status (the adapter\n * prefers the wire status seen by `http.response` over any status echoed in\n * structured metadata). `text` is the bounded concatenation of error message\n * and response body, bounded again defensively inside the classifier.\n */\nexport interface ErrorEvidence {\n readonly interrupted?: InterruptReason | undefined\n readonly structured?: StructuredSignal | undefined\n readonly status?: number | undefined\n readonly headers?: Readonly<Record<string, string>> | undefined\n readonly providerMetadata?: ProviderMetadata | undefined\n readonly text?: string | undefined\n}\n\n/** Advisory retry semantics; the retry policy (T-12) makes final decisions. */\nexport interface RetryAdvice {\n readonly retryable: boolean\n readonly retryAfterMs?: number\n}\n\nexport interface FailureClassification {\n readonly category: FailureCategory\n readonly layer: EvidenceLayer\n /**\n * True when the evidence conclusively identifies the failure. Textual\n * classifications are always non-definitive; scheduling treats\n * non-definitive evidence more conservatively (ADP-012).\n */\n readonly definitive: boolean\n readonly retry: RetryAdvice\n /** Horizontal recovery (switch resource within the tier) is allowed. */\n readonly failoverEligible: boolean\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export * from "./circuit-breaker/index.js";
2
+ export * from "./capacity/index.js";
3
+ export * from "./classifier/index.js";
4
+ export * from "./resource-health/index.js";
@@ -0,0 +1,5 @@
1
+ export * from "./circuit-breaker/index.js";
2
+ export * from "./capacity/index.js";
3
+ export * from "./classifier/index.js";
4
+ export * from "./resource-health/index.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA","sourcesContent":["export * from \"./circuit-breaker/index.js\"\nexport * from \"./capacity/index.js\"\nexport * from \"./classifier/index.js\"\nexport * from \"./resource-health/index.js\"\n"]}
@@ -0,0 +1,4 @@
1
+ export * from "./key.js";
2
+ export * from "./registry.js";
3
+ export * from "./transitions.js";
4
+ export * from "./types.js";
@@ -0,0 +1,5 @@
1
+ export * from "./key.js";
2
+ export * from "./registry.js";
3
+ export * from "./transitions.js";
4
+ export * from "./types.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/health/resource-health/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA","sourcesContent":["export * from \"./key.js\"\nexport * from \"./registry.js\"\nexport * from \"./transitions.js\"\nexport * from \"./types.js\"\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resource key helpers: scope derivation and canonical identity.
3
+ */
4
+ import type { ResourceKey, ResourceScope } from "./types.js";
5
+ /** The scope a key is attributed at: the most specific present field. */
6
+ export declare function resourceScope(key: ResourceKey): ResourceScope;
7
+ /** Canonical string identity for map keys and receipts. */
8
+ export declare function resourceKeyID(key: ResourceKey): string;
9
+ /**
10
+ * The chain of scopes implied by `key`, ordered widest → narrowest. Each level
11
+ * keeps the narrower fields that are present, so a key without an account
12
+ * (OpenCode model references carry only provider and model) still yields a
13
+ * model-level entry. Health assessment walks this chain because a blocked
14
+ * ancestor blocks every descendant (e.g. a provider-wide rate limit skips all
15
+ * of its models). A variant field without a model field is degenerate and
16
+ * contributes no level of its own.
17
+ */
18
+ export declare function resourceChain(key: ResourceKey): ResourceKey[];
@@ -0,0 +1,54 @@
1
+ /** The scope a key is attributed at: the most specific present field. */
2
+ export function resourceScope(key) {
3
+ if (key.variantID !== undefined) {
4
+ return "variant";
5
+ }
6
+ if (key.modelID !== undefined) {
7
+ return "model";
8
+ }
9
+ if (key.accountID !== undefined) {
10
+ return "account";
11
+ }
12
+ return "provider";
13
+ }
14
+ /** Canonical string identity for map keys and receipts. */
15
+ export function resourceKeyID(key) {
16
+ return JSON.stringify([
17
+ key.providerID,
18
+ key.accountID ?? null,
19
+ key.modelID ?? null,
20
+ key.variantID ?? null,
21
+ ]);
22
+ }
23
+ /**
24
+ * The chain of scopes implied by `key`, ordered widest → narrowest. Each level
25
+ * keeps the narrower fields that are present, so a key without an account
26
+ * (OpenCode model references carry only provider and model) still yields a
27
+ * model-level entry. Health assessment walks this chain because a blocked
28
+ * ancestor blocks every descendant (e.g. a provider-wide rate limit skips all
29
+ * of its models). A variant field without a model field is degenerate and
30
+ * contributes no level of its own.
31
+ */
32
+ export function resourceChain(key) {
33
+ const chain = [{ providerID: key.providerID }];
34
+ if (key.accountID !== undefined) {
35
+ chain.push({ providerID: key.providerID, accountID: key.accountID });
36
+ }
37
+ if (key.modelID !== undefined) {
38
+ chain.push(key.accountID === undefined
39
+ ? { providerID: key.providerID, modelID: key.modelID }
40
+ : { providerID: key.providerID, accountID: key.accountID, modelID: key.modelID });
41
+ }
42
+ if (key.modelID !== undefined && key.variantID !== undefined) {
43
+ chain.push(key.accountID === undefined
44
+ ? { providerID: key.providerID, modelID: key.modelID, variantID: key.variantID }
45
+ : {
46
+ providerID: key.providerID,
47
+ accountID: key.accountID,
48
+ modelID: key.modelID,
49
+ variantID: key.variantID,
50
+ });
51
+ }
52
+ return chain;
53
+ }
54
+ //# sourceMappingURL=key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../../../src/health/resource-health/key.ts"],"names":[],"mappings":"AAKA,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,GAAgB;IAC5C,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,GAAgB;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,GAAG,CAAC,UAAU;QACd,GAAG,CAAC,SAAS,IAAI,IAAI;QACrB,GAAG,CAAC,OAAO,IAAI,IAAI;QACnB,GAAG,CAAC,SAAS,IAAI,IAAI;KACtB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,GAAgB;IAC5C,MAAM,KAAK,GAAkB,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;IAC7D,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAA;IACtE,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CACR,GAAG,CAAC,SAAS,KAAK,SAAS;YACzB,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE;YACtD,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CACnF,CAAA;IACH,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAC7D,KAAK,CAAC,IAAI,CACR,GAAG,CAAC,SAAS,KAAK,SAAS;YACzB,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE;YAChF,CAAC,CAAC;gBACE,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,SAAS,EAAE,GAAG,CAAC,SAAS;aACzB,CACN,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/**\n * Resource key helpers: scope derivation and canonical identity.\n */\nimport type { ResourceKey, ResourceScope } from \"./types.js\"\n\n/** The scope a key is attributed at: the most specific present field. */\nexport function resourceScope(key: ResourceKey): ResourceScope {\n if (key.variantID !== undefined) {\n return \"variant\"\n }\n if (key.modelID !== undefined) {\n return \"model\"\n }\n if (key.accountID !== undefined) {\n return \"account\"\n }\n return \"provider\"\n}\n\n/** Canonical string identity for map keys and receipts. */\nexport function resourceKeyID(key: ResourceKey): string {\n return JSON.stringify([\n key.providerID,\n key.accountID ?? null,\n key.modelID ?? null,\n key.variantID ?? null,\n ])\n}\n\n/**\n * The chain of scopes implied by `key`, ordered widest → narrowest. Each level\n * keeps the narrower fields that are present, so a key without an account\n * (OpenCode model references carry only provider and model) still yields a\n * model-level entry. Health assessment walks this chain because a blocked\n * ancestor blocks every descendant (e.g. a provider-wide rate limit skips all\n * of its models). A variant field without a model field is degenerate and\n * contributes no level of its own.\n */\nexport function resourceChain(key: ResourceKey): ResourceKey[] {\n const chain: ResourceKey[] = [{ providerID: key.providerID }]\n if (key.accountID !== undefined) {\n chain.push({ providerID: key.providerID, accountID: key.accountID })\n }\n if (key.modelID !== undefined) {\n chain.push(\n key.accountID === undefined\n ? { providerID: key.providerID, modelID: key.modelID }\n : { providerID: key.providerID, accountID: key.accountID, modelID: key.modelID },\n )\n }\n if (key.modelID !== undefined && key.variantID !== undefined) {\n chain.push(\n key.accountID === undefined\n ? { providerID: key.providerID, modelID: key.modelID, variantID: key.variantID }\n : {\n providerID: key.providerID,\n accountID: key.accountID,\n modelID: key.modelID,\n variantID: key.variantID,\n },\n )\n }\n return chain\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { type Assessment, type Clock, type HealthEvent, type ProbeOutcome, type ProbeTicket, type ResourceHealthConfig, type ResourceKey, type ResourceRecord } from "./types.js";
2
+ export declare class ResourceHealthRegistry {
3
+ #private;
4
+ constructor(clock: Clock, config?: ResourceHealthConfig);
5
+ /**
6
+ * Seeds the registry from persisted records (restart / cross-instance
7
+ * reload). Records are swept against the current clock and inserted
8
+ * without persisting — hydration must not write back what it just read.
9
+ * A hydrated half_open record cannot keep its probe (the ticket lived in
10
+ * the previous instance's memory), so it re-opens due immediately — its
11
+ * backoff window had already elapsed when it went half-open, and the next
12
+ * assessment admits exactly one probe as usual.
13
+ */
14
+ hydrate(records: readonly ResourceRecord[]): void;
15
+ observe(event: HealthEvent): ResourceRecord;
16
+ /**
17
+ * Assesses whether the resource may take new work right now, composing the
18
+ * full ancestor chain. Precedence: the most severe hard resource state
19
+ * wins (narrowest scope on ties), then the breaker scan (widest scope
20
+ * first — a due wide breaker is probed before a narrow one), then draining,
21
+ * then available.
22
+ */
23
+ assess(key: ResourceKey): Assessment;
24
+ /**
25
+ * Non-mutating assessment for decisions that must not admit a probe
26
+ * (escalation-target gating): identical hard-state and draining walk, but
27
+ * an open or half-open breaker blocks without being flipped or ticketed.
28
+ */
29
+ evaluate(key: ResourceKey): Assessment;
30
+ /**
31
+ * Reports the outcome of an admitted probe. Only the ticket issued for the
32
+ * current half-open record is honored; stale tickets (probe already
33
+ * resolved, record reset, or unknown key) are rejected.
34
+ */
35
+ reportProbe(ticket: ProbeTicket, outcome: ProbeOutcome): boolean;
36
+ /**
37
+ * Returns an admitted-but-never-dispatched probe ticket: the breaker
38
+ * reopens still due, so the next admission can issue a fresh probe. Same
39
+ * stale-ticket rule as `reportProbe`.
40
+ */
41
+ releaseProbe(ticket: ProbeTicket): boolean;
42
+ /** Current (swept) record for one key, if the registry tracks it. */
43
+ recordFor(key: ResourceKey): ResourceRecord | undefined;
44
+ /** All tracked records, swept against the clock. */
45
+ snapshot(): ResourceRecord[];
46
+ }