@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,73 @@
1
+ /**
2
+ * Capacity sensor boundary (ADP-020).
3
+ *
4
+ * Capacity is observed, never inferred: a sensor reports what its telemetry
5
+ * actually shows (used/remaining ratios, retry-after, reset time) and grades
6
+ * its own confidence. A `null` reading or assessment means "no trustworthy
7
+ * estimate" — callers must treat that as unknown, never as healthy. Provider
8
+ * specific sensing (passive Codex headers, optional quota endpoints) lives in
9
+ * adapters; this module owns the boundary type, the deterministic merge of
10
+ * concurrent readings for one resource, and the derivation of the
11
+ * draining/exhausted signals consumed by the health layer (ADP-021).
12
+ */
13
+ import type { ResourceKey } from "../resource-health/types.js";
14
+ export declare const CONFIDENCE_LEVELS: readonly ["low", "medium", "high"];
15
+ /** How much a sensor trusts its own numbers. `high` is required for draining. */
16
+ export type Confidence = (typeof CONFIDENCE_LEVELS)[number];
17
+ /**
18
+ * One observation of a resource's capacity. Ratios are fractions in [0, 1].
19
+ * A reading may carry any subset of the measured fields (e.g. a header that
20
+ * only reports a Retry-After); a reading with none of them carries no
21
+ * information and is discarded. `observedAt` is when the underlying telemetry
22
+ * was seen, not when the sensor was asked.
23
+ */
24
+ export interface CapacityReading {
25
+ readonly usedRatio?: number | undefined;
26
+ readonly remainingRatio?: number | undefined;
27
+ readonly retryAfterMs?: number | undefined;
28
+ /** Epoch milliseconds at which capacity is expected to reset. */
29
+ readonly resetAt?: number | undefined;
30
+ readonly confidence: Confidence;
31
+ readonly observedAt: number;
32
+ /** Bounded, secret-free label of the data source (e.g. a header name). */
33
+ readonly evidence?: string | undefined;
34
+ }
35
+ /** A source of capacity observations for exactly one resource. */
36
+ export interface CapacitySensor {
37
+ readonly id: string;
38
+ readonly resource: ResourceKey;
39
+ /**
40
+ * Returns the latest reading, or `null` when the sensor has no trustworthy
41
+ * estimate. Expected "no data" must be returned as `null`, never thrown;
42
+ * the registry still guards against throws (fail-open: a failing sensor is
43
+ * no estimate, not an error).
44
+ */
45
+ read(): Promise<CapacityReading | null>;
46
+ }
47
+ export interface CapacityConfig {
48
+ /** Readings older than this are treated as absent (no estimate). */
49
+ readonly maxAgeMs: number;
50
+ /**
51
+ * Near-exhaustion bound at or below which high-confidence evidence marks
52
+ * the resource `draining` (ADP-021). A fixed product constant, deliberately
53
+ * not user-configurable — no threshold sliders.
54
+ */
55
+ readonly drainingRemainingThreshold: number;
56
+ }
57
+ export declare const DEFAULT_CAPACITY_CONFIG: CapacityConfig;
58
+ /** Drain/exhaust derivation for a merged reading. */
59
+ export interface CapacitySignals {
60
+ readonly exhausted: boolean;
61
+ readonly draining: boolean;
62
+ }
63
+ /**
64
+ * Scheduler-facing view for one resource. `none` is the explicit "no
65
+ * estimate" answer — unknown capacity, not zero capacity.
66
+ */
67
+ export type CapacityAssessment = {
68
+ readonly kind: "none";
69
+ } | {
70
+ readonly kind: "estimate";
71
+ readonly reading: CapacityReading;
72
+ readonly signals: CapacitySignals;
73
+ };
@@ -0,0 +1,6 @@
1
+ export const CONFIDENCE_LEVELS = ["low", "medium", "high"];
2
+ export const DEFAULT_CAPACITY_CONFIG = {
3
+ maxAgeMs: 60_000,
4
+ drainingRemainingThreshold: 0.1,
5
+ };
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/health/capacity/types.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAsC,CAAA;AAgD/F,MAAM,CAAC,MAAM,uBAAuB,GAAmB;IACrD,QAAQ,EAAE,MAAM;IAChB,0BAA0B,EAAE,GAAG;CAChC,CAAA","sourcesContent":["/**\n * Capacity sensor boundary (ADP-020).\n *\n * Capacity is observed, never inferred: a sensor reports what its telemetry\n * actually shows (used/remaining ratios, retry-after, reset time) and grades\n * its own confidence. A `null` reading or assessment means \"no trustworthy\n * estimate\" — callers must treat that as unknown, never as healthy. Provider\n * specific sensing (passive Codex headers, optional quota endpoints) lives in\n * adapters; this module owns the boundary type, the deterministic merge of\n * concurrent readings for one resource, and the derivation of the\n * draining/exhausted signals consumed by the health layer (ADP-021).\n */\nimport type { ResourceKey } from \"../resource-health/types.js\"\n\nexport const CONFIDENCE_LEVELS = [\"low\", \"medium\", \"high\"] as const satisfies readonly string[]\n\n/** How much a sensor trusts its own numbers. `high` is required for draining. */\nexport type Confidence = (typeof CONFIDENCE_LEVELS)[number]\n\n/**\n * One observation of a resource's capacity. Ratios are fractions in [0, 1].\n * A reading may carry any subset of the measured fields (e.g. a header that\n * only reports a Retry-After); a reading with none of them carries no\n * information and is discarded. `observedAt` is when the underlying telemetry\n * was seen, not when the sensor was asked.\n */\nexport interface CapacityReading {\n readonly usedRatio?: number | undefined\n readonly remainingRatio?: number | undefined\n readonly retryAfterMs?: number | undefined\n /** Epoch milliseconds at which capacity is expected to reset. */\n readonly resetAt?: number | undefined\n readonly confidence: Confidence\n readonly observedAt: number\n /** Bounded, secret-free label of the data source (e.g. a header name). */\n readonly evidence?: string | undefined\n}\n\n/** A source of capacity observations for exactly one resource. */\nexport interface CapacitySensor {\n readonly id: string\n readonly resource: ResourceKey\n /**\n * Returns the latest reading, or `null` when the sensor has no trustworthy\n * estimate. Expected \"no data\" must be returned as `null`, never thrown;\n * the registry still guards against throws (fail-open: a failing sensor is\n * no estimate, not an error).\n */\n read(): Promise<CapacityReading | null>\n}\n\nexport interface CapacityConfig {\n /** Readings older than this are treated as absent (no estimate). */\n readonly maxAgeMs: number\n /**\n * Near-exhaustion bound at or below which high-confidence evidence marks\n * the resource `draining` (ADP-021). A fixed product constant, deliberately\n * not user-configurable — no threshold sliders.\n */\n readonly drainingRemainingThreshold: number\n}\n\nexport const DEFAULT_CAPACITY_CONFIG: CapacityConfig = {\n maxAgeMs: 60_000,\n drainingRemainingThreshold: 0.1,\n}\n\n/** Drain/exhaust derivation for a merged reading. */\nexport interface CapacitySignals {\n readonly exhausted: boolean\n readonly draining: boolean\n}\n\n/**\n * Scheduler-facing view for one resource. `none` is the explicit \"no\n * estimate\" answer — unknown capacity, not zero capacity.\n */\nexport type CapacityAssessment =\n | { readonly kind: \"none\" }\n | {\n readonly kind: \"estimate\"\n readonly reading: CapacityReading\n readonly signals: CapacitySignals\n }\n"]}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pure circuit breaker state machine (ADP-019).
3
+ *
4
+ * All functions are total, synchronous, and clock-free: `now` is always an
5
+ * injected parameter. The single-probe guarantee comes from the admission
6
+ * protocol: `admitProbe` synchronously transitions an open (and due) breaker
7
+ * to half-open, so the second concurrent caller observes `half_open` and is
8
+ * blocked — there is never an await between the state check and the
9
+ * transition. Callers must persist the returned record immediately.
10
+ */
11
+ import type { FailureCategory } from "../classifier/types.js";
12
+ import { type BreakerConfig, type BreakerRecord } from "./types.js";
13
+ export declare function initialBreaker(): BreakerRecord;
14
+ export interface BreakerFailureResult {
15
+ readonly record: BreakerRecord;
16
+ /** True exactly when this failure tripped the breaker from closed to open. */
17
+ readonly opened: boolean;
18
+ }
19
+ /**
20
+ * Records a countable failure. Callers route only infrastructure failures
21
+ * here (the resource-health transitions layer sends outage, overload, and
22
+ * timeout); the breaker additionally exempts rate limiting and quota
23
+ * exhaustion, which own their windowed resource states. Failures observed
24
+ * while the breaker is not closed cannot happen through the admission
25
+ * protocol (non-probe callers are blocked) and are ignored defensively.
26
+ */
27
+ export declare function recordBreakerFailure(record: BreakerRecord, category: FailureCategory, now: number, config: BreakerConfig): BreakerFailureResult;
28
+ /**
29
+ * Observed success closes the breaker and resets the failure/backoff state.
30
+ * Evidence beats inference: a success is proof the resource works, from any
31
+ * prior breaker state.
32
+ */
33
+ export declare function recordBreakerSuccess(): BreakerRecord;
34
+ export type ProbeAdmission = {
35
+ kind: "closed";
36
+ record: BreakerRecord;
37
+ } | {
38
+ kind: "waiting";
39
+ record: BreakerRecord;
40
+ retryAtMs: number;
41
+ } | {
42
+ kind: "in-flight";
43
+ record: BreakerRecord;
44
+ } | {
45
+ kind: "admitted";
46
+ record: BreakerRecord;
47
+ };
48
+ /**
49
+ * Attempts to admit the single half-open probe. Callers must persist the
50
+ * returned record synchronously; the returned record carries no probeId — the
51
+ * registry stamps its own ticket id on the persisted copy.
52
+ */
53
+ /**
54
+ * Non-mutating wait computation for an open breaker, admission view: the
55
+ * remaining backoff in ms, or `undefined` when the window has elapsed (or the
56
+ * record is not open). Used to decide whether a probe may be admitted.
57
+ */
58
+ export declare function breakerWait(record: BreakerRecord, now: number): number | undefined;
59
+ /**
60
+ * Non-mutating block computation for an open breaker, observer view: an open
61
+ * breaker blocks regardless of elapsed backoff — recovery counts only once a
62
+ * probe proved it — so an elapsed (or, unreachable through the state machine,
63
+ * missing) stamp simply means zero remaining wait.
64
+ */
65
+ export declare function breakerBlockWait(record: BreakerRecord, now: number): number | undefined;
66
+ export declare function admitProbe(record: BreakerRecord, now: number): ProbeAdmission;
67
+ /** Closes the breaker when a probe report confirms recovery. */
68
+ export declare function recordProbeSuccess(record: BreakerRecord): BreakerRecord;
69
+ /**
70
+ * Expires a probe whose episode never reported an outcome. A lost probe is
71
+ * no health evidence: the breaker re-arms the SAME backoff window (no
72
+ * doubling) so a fresh probe can be admitted after it. Evaluated lazily by
73
+ * the registry's assessment sweep — there are no timers.
74
+ */
75
+ export declare function expireProbe(record: BreakerRecord, now: number, config: BreakerConfig): BreakerRecord;
76
+ /**
77
+ * Returns an admitted probe that was never dispatched (the admission picked
78
+ * another resource, only observed, or failed to switch). No request ran, so
79
+ * this is no evidence at all: the breaker goes back to open with the window
80
+ * it was admitted under — still due — instead of restarting the backoff,
81
+ * which would push a passed-over resource back on every admission.
82
+ */
83
+ export declare function releaseProbe(record: BreakerRecord): BreakerRecord;
84
+ /**
85
+ * Records a failed probe. A non-exempt failure reopens the breaker with a
86
+ * doubled backoff. An exempt failure (the probe was rate limited or quota
87
+ * blocked) re-arms the same backoff window without escalation: the resource
88
+ * was rejected for demand reasons, not degraded health, so doubling would
89
+ * overstate the failure.
90
+ */
91
+ export declare function recordProbeFailure(record: BreakerRecord, category: FailureCategory, now: number, config: BreakerConfig): BreakerRecord;
@@ -0,0 +1,150 @@
1
+ import { BREAKER_EXEMPT_CATEGORIES } from "./types.js";
2
+ export function initialBreaker() {
3
+ return { state: "closed", failures: 0, openCount: 0 };
4
+ }
5
+ /**
6
+ * Backoff window for the `openCount`-th opening: doubling, capped at
7
+ * `maxBackoffMs` so a persistently failing resource never waits longer than
8
+ * the configured ceiling.
9
+ */
10
+ function backoffMs(openCount, config) {
11
+ return Math.min(config.baseOpenMs * 2 ** (openCount - 1), config.maxBackoffMs);
12
+ }
13
+ function openBreaker(record, openCount, now, config) {
14
+ return {
15
+ state: "open",
16
+ failures: record.failures,
17
+ openCount,
18
+ reopenAt: now + backoffMs(openCount, config),
19
+ };
20
+ }
21
+ /**
22
+ * Records a countable failure. Callers route only infrastructure failures
23
+ * here (the resource-health transitions layer sends outage, overload, and
24
+ * timeout); the breaker additionally exempts rate limiting and quota
25
+ * exhaustion, which own their windowed resource states. Failures observed
26
+ * while the breaker is not closed cannot happen through the admission
27
+ * protocol (non-probe callers are blocked) and are ignored defensively.
28
+ */
29
+ export function recordBreakerFailure(record, category, now, config) {
30
+ if (BREAKER_EXEMPT_CATEGORIES.includes(category) || record.state !== "closed") {
31
+ return { record, opened: false };
32
+ }
33
+ const failures = record.failures + 1;
34
+ if (failures < config.failureThreshold) {
35
+ return { record: { ...record, failures }, opened: false };
36
+ }
37
+ return {
38
+ record: openBreaker({ ...record, failures }, record.openCount + 1, now, config),
39
+ opened: true,
40
+ };
41
+ }
42
+ /**
43
+ * Observed success closes the breaker and resets the failure/backoff state.
44
+ * Evidence beats inference: a success is proof the resource works, from any
45
+ * prior breaker state.
46
+ */
47
+ export function recordBreakerSuccess() {
48
+ return { state: "closed", failures: 0, openCount: 0 };
49
+ }
50
+ /**
51
+ * Attempts to admit the single half-open probe. Callers must persist the
52
+ * returned record synchronously; the returned record carries no probeId — the
53
+ * registry stamps its own ticket id on the persisted copy.
54
+ */
55
+ /**
56
+ * Non-mutating wait computation for an open breaker, admission view: the
57
+ * remaining backoff in ms, or `undefined` when the window has elapsed (or the
58
+ * record is not open). Used to decide whether a probe may be admitted.
59
+ */
60
+ export function breakerWait(record, now) {
61
+ if (record.state !== "open")
62
+ return undefined;
63
+ const reopenAt = record.reopenAt;
64
+ if (reopenAt !== undefined && now < reopenAt)
65
+ return reopenAt - now;
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Non-mutating block computation for an open breaker, observer view: an open
70
+ * breaker blocks regardless of elapsed backoff — recovery counts only once a
71
+ * probe proved it — so an elapsed (or, unreachable through the state machine,
72
+ * missing) stamp simply means zero remaining wait.
73
+ */
74
+ export function breakerBlockWait(record, now) {
75
+ if (record.state !== "open")
76
+ return undefined;
77
+ return Math.max(0, (record.reopenAt ?? 0) - now);
78
+ }
79
+ export function admitProbe(record, now) {
80
+ if (record.state === "closed") {
81
+ return { kind: "closed", record };
82
+ }
83
+ if (record.state === "half_open") {
84
+ return { kind: "in-flight", record };
85
+ }
86
+ const wait = breakerWait(record, now);
87
+ if (wait !== undefined) {
88
+ return { kind: "waiting", record, retryAtMs: wait };
89
+ }
90
+ return {
91
+ kind: "admitted",
92
+ record: { state: "half_open", failures: record.failures, openCount: record.openCount },
93
+ };
94
+ }
95
+ /** Closes the breaker when a probe report confirms recovery. */
96
+ export function recordProbeSuccess(record) {
97
+ if (record.state !== "half_open") {
98
+ return record;
99
+ }
100
+ return { state: "closed", failures: 0, openCount: 0 };
101
+ }
102
+ /**
103
+ * Expires a probe whose episode never reported an outcome. A lost probe is
104
+ * no health evidence: the breaker re-arms the SAME backoff window (no
105
+ * doubling) so a fresh probe can be admitted after it. Evaluated lazily by
106
+ * the registry's assessment sweep — there are no timers.
107
+ */
108
+ export function expireProbe(record, now, config) {
109
+ if (record.state !== "half_open" ||
110
+ record.probeAt === undefined ||
111
+ now - record.probeAt < config.probeTimeoutMs) {
112
+ return record;
113
+ }
114
+ return openBreaker(record, record.openCount, now, config);
115
+ }
116
+ /**
117
+ * Returns an admitted probe that was never dispatched (the admission picked
118
+ * another resource, only observed, or failed to switch). No request ran, so
119
+ * this is no evidence at all: the breaker goes back to open with the window
120
+ * it was admitted under — still due — instead of restarting the backoff,
121
+ * which would push a passed-over resource back on every admission.
122
+ */
123
+ export function releaseProbe(record) {
124
+ if (record.state !== "half_open") {
125
+ return record;
126
+ }
127
+ return {
128
+ state: "open",
129
+ failures: record.failures,
130
+ openCount: record.openCount,
131
+ ...(record.probeAt !== undefined ? { reopenAt: record.probeAt } : {}),
132
+ };
133
+ }
134
+ /**
135
+ * Records a failed probe. A non-exempt failure reopens the breaker with a
136
+ * doubled backoff. An exempt failure (the probe was rate limited or quota
137
+ * blocked) re-arms the same backoff window without escalation: the resource
138
+ * was rejected for demand reasons, not degraded health, so doubling would
139
+ * overstate the failure.
140
+ */
141
+ export function recordProbeFailure(record, category, now, config) {
142
+ if (record.state !== "half_open") {
143
+ return record;
144
+ }
145
+ if (BREAKER_EXEMPT_CATEGORIES.includes(category)) {
146
+ return openBreaker(record, record.openCount, now, config);
147
+ }
148
+ return openBreaker(record, record.openCount + 1, now, config);
149
+ }
150
+ //# sourceMappingURL=breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaker.js","sourceRoot":"","sources":["../../../src/health/circuit-breaker/breaker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure circuit breaker state machine (ADP-019).\n *\n * All functions are total, synchronous, and clock-free: `now` is always an\n * injected parameter. The single-probe guarantee comes from the admission\n * protocol: `admitProbe` synchronously transitions an open (and due) breaker\n * to half-open, so the second concurrent caller observes `half_open` and is\n * blocked — there is never an await between the state check and the\n * transition. Callers must persist the returned record immediately.\n */\nimport type { FailureCategory } from \"../classifier/types.js\"\nimport { BREAKER_EXEMPT_CATEGORIES, type BreakerConfig, type BreakerRecord } from \"./types.js\"\n\nexport function initialBreaker(): BreakerRecord {\n return { state: \"closed\", failures: 0, openCount: 0 }\n}\n\n/**\n * Backoff window for the `openCount`-th opening: doubling, capped at\n * `maxBackoffMs` so a persistently failing resource never waits longer than\n * the configured ceiling.\n */\nfunction backoffMs(openCount: number, config: BreakerConfig): number {\n return Math.min(config.baseOpenMs * 2 ** (openCount - 1), config.maxBackoffMs)\n}\n\nfunction openBreaker(\n record: BreakerRecord,\n openCount: number,\n now: number,\n config: BreakerConfig,\n): BreakerRecord {\n return {\n state: \"open\",\n failures: record.failures,\n openCount,\n reopenAt: now + backoffMs(openCount, config),\n }\n}\n\nexport interface BreakerFailureResult {\n readonly record: BreakerRecord\n /** True exactly when this failure tripped the breaker from closed to open. */\n readonly opened: boolean\n}\n\n/**\n * Records a countable failure. Callers route only infrastructure failures\n * here (the resource-health transitions layer sends outage, overload, and\n * timeout); the breaker additionally exempts rate limiting and quota\n * exhaustion, which own their windowed resource states. Failures observed\n * while the breaker is not closed cannot happen through the admission\n * protocol (non-probe callers are blocked) and are ignored defensively.\n */\nexport function recordBreakerFailure(\n record: BreakerRecord,\n category: FailureCategory,\n now: number,\n config: BreakerConfig,\n): BreakerFailureResult {\n if (BREAKER_EXEMPT_CATEGORIES.includes(category) || record.state !== \"closed\") {\n return { record, opened: false }\n }\n const failures = record.failures + 1\n if (failures < config.failureThreshold) {\n return { record: { ...record, failures }, opened: false }\n }\n return {\n record: openBreaker({ ...record, failures }, record.openCount + 1, now, config),\n opened: true,\n }\n}\n\n/**\n * Observed success closes the breaker and resets the failure/backoff state.\n * Evidence beats inference: a success is proof the resource works, from any\n * prior breaker state.\n */\nexport function recordBreakerSuccess(): BreakerRecord {\n return { state: \"closed\", failures: 0, openCount: 0 }\n}\n\nexport type ProbeAdmission =\n | { kind: \"closed\"; record: BreakerRecord }\n | { kind: \"waiting\"; record: BreakerRecord; retryAtMs: number }\n | { kind: \"in-flight\"; record: BreakerRecord }\n | { kind: \"admitted\"; record: BreakerRecord }\n\n/**\n * Attempts to admit the single half-open probe. Callers must persist the\n * returned record synchronously; the returned record carries no probeId — the\n * registry stamps its own ticket id on the persisted copy.\n */\n/**\n * Non-mutating wait computation for an open breaker, admission view: the\n * remaining backoff in ms, or `undefined` when the window has elapsed (or the\n * record is not open). Used to decide whether a probe may be admitted.\n */\nexport function breakerWait(record: BreakerRecord, now: number): number | undefined {\n if (record.state !== \"open\") return undefined\n const reopenAt = record.reopenAt\n if (reopenAt !== undefined && now < reopenAt) return reopenAt - now\n return undefined\n}\n\n/**\n * Non-mutating block computation for an open breaker, observer view: an open\n * breaker blocks regardless of elapsed backoff — recovery counts only once a\n * probe proved it — so an elapsed (or, unreachable through the state machine,\n * missing) stamp simply means zero remaining wait.\n */\nexport function breakerBlockWait(record: BreakerRecord, now: number): number | undefined {\n if (record.state !== \"open\") return undefined\n return Math.max(0, (record.reopenAt ?? 0) - now)\n}\n\nexport function admitProbe(record: BreakerRecord, now: number): ProbeAdmission {\n if (record.state === \"closed\") {\n return { kind: \"closed\", record }\n }\n if (record.state === \"half_open\") {\n return { kind: \"in-flight\", record }\n }\n const wait = breakerWait(record, now)\n if (wait !== undefined) {\n return { kind: \"waiting\", record, retryAtMs: wait }\n }\n return {\n kind: \"admitted\",\n record: { state: \"half_open\", failures: record.failures, openCount: record.openCount },\n }\n}\n\n/** Closes the breaker when a probe report confirms recovery. */\nexport function recordProbeSuccess(record: BreakerRecord): BreakerRecord {\n if (record.state !== \"half_open\") {\n return record\n }\n return { state: \"closed\", failures: 0, openCount: 0 }\n}\n\n/**\n * Expires a probe whose episode never reported an outcome. A lost probe is\n * no health evidence: the breaker re-arms the SAME backoff window (no\n * doubling) so a fresh probe can be admitted after it. Evaluated lazily by\n * the registry's assessment sweep — there are no timers.\n */\nexport function expireProbe(\n record: BreakerRecord,\n now: number,\n config: BreakerConfig,\n): BreakerRecord {\n if (\n record.state !== \"half_open\" ||\n record.probeAt === undefined ||\n now - record.probeAt < config.probeTimeoutMs\n ) {\n return record\n }\n return openBreaker(record, record.openCount, now, config)\n}\n\n/**\n * Returns an admitted probe that was never dispatched (the admission picked\n * another resource, only observed, or failed to switch). No request ran, so\n * this is no evidence at all: the breaker goes back to open with the window\n * it was admitted under — still due — instead of restarting the backoff,\n * which would push a passed-over resource back on every admission.\n */\nexport function releaseProbe(record: BreakerRecord): BreakerRecord {\n if (record.state !== \"half_open\") {\n return record\n }\n return {\n state: \"open\",\n failures: record.failures,\n openCount: record.openCount,\n ...(record.probeAt !== undefined ? { reopenAt: record.probeAt } : {}),\n }\n}\n\n/**\n * Records a failed probe. A non-exempt failure reopens the breaker with a\n * doubled backoff. An exempt failure (the probe was rate limited or quota\n * blocked) re-arms the same backoff window without escalation: the resource\n * was rejected for demand reasons, not degraded health, so doubling would\n * overstate the failure.\n */\nexport function recordProbeFailure(\n record: BreakerRecord,\n category: FailureCategory,\n now: number,\n config: BreakerConfig,\n): BreakerRecord {\n if (record.state !== \"half_open\") {\n return record\n }\n if (BREAKER_EXEMPT_CATEGORIES.includes(category)) {\n return openBreaker(record, record.openCount, now, config)\n }\n return openBreaker(record, record.openCount + 1, now, config)\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export * from "./breaker.js";
2
+ export * from "./types.js";
@@ -0,0 +1,3 @@
1
+ export * from "./breaker.js";
2
+ export * from "./types.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/health/circuit-breaker/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA","sourcesContent":["export * from \"./breaker.js\"\nexport * from \"./types.js\"\n"]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Circuit breaker types (ADP-019).
3
+ *
4
+ * The breaker is the quarantine mechanism for transient infrastructure
5
+ * failures (outage, overload, timeout): repeated failures open the resource
6
+ * for a backoff window, after which exactly one probe request is admitted
7
+ * (half-open state). Rate limiting and quota exhaustion are deliberately
8
+ * exempt from breaker counting because they already have their own windowed
9
+ * resource states; counting them here would double-punish a resource that is
10
+ * already known to need a cooldown.
11
+ */
12
+ import type { FailureCategory } from "../classifier/types.js";
13
+ export declare const BREAKER_STATES: readonly ["closed", "open", "half_open"];
14
+ export type BreakerState = (typeof BREAKER_STATES)[number];
15
+ export interface BreakerConfig {
16
+ /** Consecutive countable failures in the closed state that trip the breaker. */
17
+ readonly failureThreshold: number;
18
+ /** Backoff for the first opening; doubles on each reopening, capped below. */
19
+ readonly baseOpenMs: number;
20
+ /** Upper bound for the backoff window. */
21
+ readonly maxBackoffMs: number;
22
+ /**
23
+ * How long a half-open probe may stay unreported before assessment treats
24
+ * it as lost. Generous by design: a probe episode is a real work unit and
25
+ * may run for minutes; the deadline only exists so a probe whose terminal
26
+ * event never arrives cannot wedge the resource in half_open forever.
27
+ */
28
+ readonly probeTimeoutMs: number;
29
+ }
30
+ export declare const DEFAULT_BREAKER_CONFIG: BreakerConfig;
31
+ export declare const BREAKER_EXEMPT_CATEGORIES: readonly FailureCategory[];
32
+ /**
33
+ * Persistent breaker record for one resource. `openCount` drives the doubling
34
+ * backoff and resets only when the breaker closes (a probe or an observed
35
+ * success). Rate limiting, quota exhaustion and user cancellation are
36
+ * deliberately exempt from breaker counting: the first two own their own
37
+ * windowed resource states (counting them would double-punish a resource
38
+ * already known to need a cooldown), and a user-interrupted probe says
39
+ * nothing about resource health, so an interrupted probe re-arms the same
40
+ * window rather than doubling it. `probeId` identifies the single admitted
41
+ * probe while half-open so stale or forged probe reports can be rejected;
42
+ * `probeAt` records when it was admitted so a probe whose outcome report is
43
+ * lost can expire instead of wedging the resource half-open forever.
44
+ */
45
+ export interface BreakerRecord {
46
+ readonly state: BreakerState;
47
+ readonly failures: number;
48
+ readonly openCount: number;
49
+ readonly reopenAt?: number | undefined;
50
+ readonly probeId?: string | undefined;
51
+ readonly probeAt?: number | undefined;
52
+ }
@@ -0,0 +1,13 @@
1
+ export const BREAKER_STATES = ["closed", "open", "half_open"];
2
+ export const DEFAULT_BREAKER_CONFIG = {
3
+ failureThreshold: 5,
4
+ baseOpenMs: 30_000,
5
+ maxBackoffMs: 1_800_000,
6
+ probeTimeoutMs: 600_000,
7
+ };
8
+ export const BREAKER_EXEMPT_CATEGORIES = [
9
+ "rate_limited",
10
+ "quota_exhausted",
11
+ "user_cancelled",
12
+ ];
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/health/circuit-breaker/types.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAsC,CAAA;AAoBlG,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,gBAAgB,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM;IAClB,YAAY,EAAE,SAAS;IACvB,cAAc,EAAE,OAAO;CACxB,CAAA;AAED,MAAM,CAAC,MAAM,yBAAyB,GAA+B;IACnE,cAAc;IACd,iBAAiB;IACjB,gBAAgB;CACjB,CAAA","sourcesContent":["/**\n * Circuit breaker types (ADP-019).\n *\n * The breaker is the quarantine mechanism for transient infrastructure\n * failures (outage, overload, timeout): repeated failures open the resource\n * for a backoff window, after which exactly one probe request is admitted\n * (half-open state). Rate limiting and quota exhaustion are deliberately\n * exempt from breaker counting because they already have their own windowed\n * resource states; counting them here would double-punish a resource that is\n * already known to need a cooldown.\n */\nimport type { FailureCategory } from \"../classifier/types.js\"\n\nexport const BREAKER_STATES = [\"closed\", \"open\", \"half_open\"] as const satisfies readonly string[]\n\nexport type BreakerState = (typeof BREAKER_STATES)[number]\n\nexport interface BreakerConfig {\n /** Consecutive countable failures in the closed state that trip the breaker. */\n readonly failureThreshold: number\n /** Backoff for the first opening; doubles on each reopening, capped below. */\n readonly baseOpenMs: number\n /** Upper bound for the backoff window. */\n readonly maxBackoffMs: number\n /**\n * How long a half-open probe may stay unreported before assessment treats\n * it as lost. Generous by design: a probe episode is a real work unit and\n * may run for minutes; the deadline only exists so a probe whose terminal\n * event never arrives cannot wedge the resource in half_open forever.\n */\n readonly probeTimeoutMs: number\n}\n\nexport const DEFAULT_BREAKER_CONFIG: BreakerConfig = {\n failureThreshold: 5,\n baseOpenMs: 30_000,\n maxBackoffMs: 1_800_000,\n probeTimeoutMs: 600_000,\n}\n\nexport const BREAKER_EXEMPT_CATEGORIES: readonly FailureCategory[] = [\n \"rate_limited\",\n \"quota_exhausted\",\n \"user_cancelled\",\n]\n\n/**\n * Persistent breaker record for one resource. `openCount` drives the doubling\n * backoff and resets only when the breaker closes (a probe or an observed\n * success). Rate limiting, quota exhaustion and user cancellation are\n * deliberately exempt from breaker counting: the first two own their own\n * windowed resource states (counting them would double-punish a resource\n * already known to need a cooldown), and a user-interrupted probe says\n * nothing about resource health, so an interrupted probe re-arms the same\n * window rather than doubling it. `probeId` identifies the single admitted\n * probe while half-open so stale or forged probe reports can be rejected;\n * `probeAt` records when it was admitted so a probe whose outcome report is\n * lost can expire instead of wedging the resource half-open forever.\n */\nexport interface BreakerRecord {\n readonly state: BreakerState\n readonly failures: number\n readonly openCount: number\n readonly reopenAt?: number | undefined\n readonly probeId?: string | undefined\n readonly probeAt?: number | undefined\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import type { ErrorEvidence, FailureClassification } from "./types.js";
2
+ /**
3
+ * Total classification of layered failure evidence. Never throws and always
4
+ * returns a category: any evidence shape outside the documented vocabularies
5
+ * classifies as `unknown` via the `none` layer, deferring to OpenCode's
6
+ * native failure handling (ADP-009, ADP-012).
7
+ */
8
+ export declare function classifyFailure(evidence: ErrorEvidence): FailureClassification;