@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,2 @@
1
+ import type { ResourceRecord } from "../../health/resource-health/types.js";
2
+ export declare function parseHealthPayload(raw: unknown): ResourceRecord[];
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Read-side parsing for persisted health records. Every parser is total: it
3
+ * accepts `unknown` and returns typed records, dropping anything invalid — a
4
+ * corrupted health file can never take the plugin down (ADP-031). State and
5
+ * breaker vocabularies are membership-checked; timestamps must be finite
6
+ * numbers. An older plugin reading newer records may drop unknown-vocabulary
7
+ * entries — health records are bounded mirrors of an in-memory registry, and
8
+ * the payload version guards the schema itself.
9
+ */
10
+ import { BREAKER_STATES } from "../../health/circuit-breaker/types.js";
11
+ import { HEALTH_STATES } from "../../health/resource-health/types.js";
12
+ import { HEALTH_VERSION } from "./types.js";
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function isFiniteNumber(raw) {
17
+ return typeof raw === "number" && Number.isFinite(raw);
18
+ }
19
+ function isNonNegativeInt(raw) {
20
+ return isFiniteNumber(raw) && Number.isInteger(raw) && raw >= 0;
21
+ }
22
+ function parseVocab(raw, vocabulary) {
23
+ if (typeof raw !== "string")
24
+ return undefined;
25
+ return vocabulary.find((entry) => entry === raw);
26
+ }
27
+ /**
28
+ * A non-empty bounded identifier string (provider/model/variant/account ids).
29
+ * Over-long values are rejected, not truncated: the writer stores keys as-is,
30
+ * so a truncated key would name a different resource.
31
+ */
32
+ function parseIdentifier(raw) {
33
+ if (typeof raw !== "string" || raw === "" || raw.length > 128)
34
+ return undefined;
35
+ return raw;
36
+ }
37
+ function parseKey(raw) {
38
+ if (!isRecord(raw))
39
+ return undefined;
40
+ const providerID = parseIdentifier(raw["providerID"]);
41
+ if (providerID === undefined)
42
+ return undefined;
43
+ const accountID = parseIdentifier(raw["accountID"]);
44
+ const modelID = parseIdentifier(raw["modelID"]);
45
+ const variantID = parseIdentifier(raw["variantID"]);
46
+ // A present-but-unusable scope field would silently widen the key.
47
+ if (raw["accountID"] !== undefined && accountID === undefined)
48
+ return undefined;
49
+ if (raw["modelID"] !== undefined && modelID === undefined)
50
+ return undefined;
51
+ if (raw["variantID"] !== undefined && variantID === undefined)
52
+ return undefined;
53
+ // Scope fields must form a valid prefix: a variant without a model, or a
54
+ // model without its variant's provider context, is not a real resource.
55
+ if (variantID !== undefined && modelID === undefined)
56
+ return undefined;
57
+ return {
58
+ providerID,
59
+ ...(accountID !== undefined ? { accountID } : {}),
60
+ ...(modelID !== undefined ? { modelID } : {}),
61
+ ...(variantID !== undefined ? { variantID } : {}),
62
+ };
63
+ }
64
+ function parseBreaker(raw) {
65
+ if (!isRecord(raw))
66
+ return undefined;
67
+ const state = parseVocab(raw["state"], BREAKER_STATES);
68
+ const failures = raw["failures"];
69
+ const openCount = raw["openCount"];
70
+ if (state === undefined)
71
+ return undefined;
72
+ if (!(isNonNegativeInt(failures) && isNonNegativeInt(openCount)))
73
+ return undefined;
74
+ const reopenAt = raw["reopenAt"];
75
+ const probeId = parseIdentifier(raw["probeId"]);
76
+ const probeAt = raw["probeAt"];
77
+ return {
78
+ state,
79
+ failures,
80
+ openCount,
81
+ ...(isFiniteNumber(reopenAt) ? { reopenAt } : {}),
82
+ ...(probeId !== undefined ? { probeId } : {}),
83
+ ...(isFiniteNumber(probeAt) ? { probeAt } : {}),
84
+ };
85
+ }
86
+ function parseWindow(raw) {
87
+ if (!isRecord(raw))
88
+ return undefined;
89
+ const kind = parseVocab(raw["kind"], ["rate-limit", "quota"]);
90
+ const until = raw["until"];
91
+ if (kind === undefined || !isFiniteNumber(until))
92
+ return undefined;
93
+ return { kind, until };
94
+ }
95
+ function parseRecord(raw) {
96
+ if (!isRecord(raw))
97
+ return undefined;
98
+ const key = parseKey(raw["key"]);
99
+ const health = parseVocab(raw["health"], HEALTH_STATES);
100
+ const breaker = parseBreaker(raw["breaker"]);
101
+ const updatedAt = raw["updatedAt"];
102
+ if (key === undefined || health === undefined || breaker === undefined)
103
+ return undefined;
104
+ if (!isFiniteNumber(updatedAt))
105
+ return undefined;
106
+ const window = parseWindow(raw["window"]);
107
+ // Windowed states lift only when their window elapses: without a valid
108
+ // window the record would block the resource permanently, so it is dropped
109
+ // (the resource reads as healthy until fresh evidence arrives).
110
+ if ((health === "rate_limited" || health === "quota_exhausted") && window === undefined) {
111
+ return undefined;
112
+ }
113
+ return {
114
+ key,
115
+ health,
116
+ breaker,
117
+ ...(window !== undefined ? { window } : {}),
118
+ updatedAt,
119
+ };
120
+ }
121
+ export function parseHealthPayload(raw) {
122
+ if (!isRecord(raw))
123
+ return [];
124
+ if (raw["version"] !== HEALTH_VERSION)
125
+ return [];
126
+ const list = raw["records"];
127
+ if (!Array.isArray(list))
128
+ return [];
129
+ const parsed = [];
130
+ for (const entry of list.slice(0, 512)) {
131
+ const record = parseRecord(entry);
132
+ if (record !== undefined)
133
+ parsed.push(record);
134
+ }
135
+ return parsed;
136
+ }
137
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/state/health/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAA;AAMrE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AACxD,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAY;IACpC,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;AACjE,CAAC;AAED,SAAS,UAAU,CACjB,GAAY,EACZ,UAAa;IAEb,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,GAAG,CAAC,CAAA;AAClD,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAY;IACnC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,SAAS,CAAA;IAC/E,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAA;IACrD,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC9C,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;IACnD,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;IAC/C,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;IACnD,mEAAmE;IACnE,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC/E,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3E,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC/E,yEAAyE;IACzE,wEAAwE;IACxE,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACtE,OAAO;QACL,UAAU;QACV,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,GAAY;IAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAA;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAA;IAChC,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAA;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACzC,IAAI,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IAClF,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAA;IAChC,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAC9B,OAAO;QACL,KAAK;QACL,QAAQ;QACR,SAAS;QACT,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,CAAU,CAAC,CAAA;IACtE,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAA;IAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAClE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;IAChC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAA;IACvD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;IAC5C,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAA;IAClC,IAAI,GAAG,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxF,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACzC,uEAAuE;IACvE,2EAA2E;IAC3E,gEAAgE;IAChE,IAAI,CAAC,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,iBAAiB,CAAC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACxF,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO;QACL,GAAG;QACH,MAAM;QACN,OAAO;QACP,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,SAAS;KACV,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC7B,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,cAAc;QAAE,OAAO,EAAE,CAAA;IAChD,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,MAAM,GAAqB,EAAE,CAAA;IACnC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;QACjC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/**\n * Read-side parsing for persisted health records. Every parser is total: it\n * accepts `unknown` and returns typed records, dropping anything invalid — a\n * corrupted health file can never take the plugin down (ADP-031). State and\n * breaker vocabularies are membership-checked; timestamps must be finite\n * numbers. An older plugin reading newer records may drop unknown-vocabulary\n * entries — health records are bounded mirrors of an in-memory registry, and\n * the payload version guards the schema itself.\n */\nimport { BREAKER_STATES } from \"../../health/circuit-breaker/types.js\"\nimport type { BreakerRecord } from \"../../health/circuit-breaker/types.js\"\nimport { HEALTH_STATES } from \"../../health/resource-health/types.js\"\nimport type {\n ResourceKey,\n ResourceRecord,\n ResourceWindow,\n} from \"../../health/resource-health/types.js\"\nimport { HEALTH_VERSION } from \"./types.js\"\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isFiniteNumber(raw: unknown): raw is number {\n return typeof raw === \"number\" && Number.isFinite(raw)\n}\n\nfunction isNonNegativeInt(raw: unknown): raw is number {\n return isFiniteNumber(raw) && Number.isInteger(raw) && raw >= 0\n}\n\nfunction parseVocab<V extends readonly string[]>(\n raw: unknown,\n vocabulary: V,\n): V[number] | undefined {\n if (typeof raw !== \"string\") return undefined\n return vocabulary.find((entry) => entry === raw)\n}\n\n/**\n * A non-empty bounded identifier string (provider/model/variant/account ids).\n * Over-long values are rejected, not truncated: the writer stores keys as-is,\n * so a truncated key would name a different resource.\n */\nfunction parseIdentifier(raw: unknown): string | undefined {\n if (typeof raw !== \"string\" || raw === \"\" || raw.length > 128) return undefined\n return raw\n}\n\nfunction parseKey(raw: unknown): ResourceKey | undefined {\n if (!isRecord(raw)) return undefined\n const providerID = parseIdentifier(raw[\"providerID\"])\n if (providerID === undefined) return undefined\n const accountID = parseIdentifier(raw[\"accountID\"])\n const modelID = parseIdentifier(raw[\"modelID\"])\n const variantID = parseIdentifier(raw[\"variantID\"])\n // A present-but-unusable scope field would silently widen the key.\n if (raw[\"accountID\"] !== undefined && accountID === undefined) return undefined\n if (raw[\"modelID\"] !== undefined && modelID === undefined) return undefined\n if (raw[\"variantID\"] !== undefined && variantID === undefined) return undefined\n // Scope fields must form a valid prefix: a variant without a model, or a\n // model without its variant's provider context, is not a real resource.\n if (variantID !== undefined && modelID === undefined) return undefined\n return {\n providerID,\n ...(accountID !== undefined ? { accountID } : {}),\n ...(modelID !== undefined ? { modelID } : {}),\n ...(variantID !== undefined ? { variantID } : {}),\n }\n}\n\nfunction parseBreaker(raw: unknown): BreakerRecord | undefined {\n if (!isRecord(raw)) return undefined\n const state = parseVocab(raw[\"state\"], BREAKER_STATES)\n const failures = raw[\"failures\"]\n const openCount = raw[\"openCount\"]\n if (state === undefined) return undefined\n if (!(isNonNegativeInt(failures) && isNonNegativeInt(openCount))) return undefined\n const reopenAt = raw[\"reopenAt\"]\n const probeId = parseIdentifier(raw[\"probeId\"])\n const probeAt = raw[\"probeAt\"]\n return {\n state,\n failures,\n openCount,\n ...(isFiniteNumber(reopenAt) ? { reopenAt } : {}),\n ...(probeId !== undefined ? { probeId } : {}),\n ...(isFiniteNumber(probeAt) ? { probeAt } : {}),\n }\n}\n\nfunction parseWindow(raw: unknown): ResourceWindow | undefined {\n if (!isRecord(raw)) return undefined\n const kind = parseVocab(raw[\"kind\"], [\"rate-limit\", \"quota\"] as const)\n const until = raw[\"until\"]\n if (kind === undefined || !isFiniteNumber(until)) return undefined\n return { kind, until }\n}\n\nfunction parseRecord(raw: unknown): ResourceRecord | undefined {\n if (!isRecord(raw)) return undefined\n const key = parseKey(raw[\"key\"])\n const health = parseVocab(raw[\"health\"], HEALTH_STATES)\n const breaker = parseBreaker(raw[\"breaker\"])\n const updatedAt = raw[\"updatedAt\"]\n if (key === undefined || health === undefined || breaker === undefined) return undefined\n if (!isFiniteNumber(updatedAt)) return undefined\n const window = parseWindow(raw[\"window\"])\n // Windowed states lift only when their window elapses: without a valid\n // window the record would block the resource permanently, so it is dropped\n // (the resource reads as healthy until fresh evidence arrives).\n if ((health === \"rate_limited\" || health === \"quota_exhausted\") && window === undefined) {\n return undefined\n }\n return {\n key,\n health,\n breaker,\n ...(window !== undefined ? { window } : {}),\n updatedAt,\n }\n}\n\nexport function parseHealthPayload(raw: unknown): ResourceRecord[] {\n if (!isRecord(raw)) return []\n if (raw[\"version\"] !== HEALTH_VERSION) return []\n const list = raw[\"records\"]\n if (!Array.isArray(list)) return []\n const parsed: ResourceRecord[] = []\n for (const entry of list.slice(0, 512)) {\n const record = parseRecord(entry)\n if (record !== undefined) parsed.push(record)\n }\n return parsed\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { HealthStorage } from "./types.js";
2
+ import type { ResourceRecord } from "../../health/resource-health/types.js";
3
+ export declare class HealthRecordStore {
4
+ #private;
5
+ private readonly storage;
6
+ constructor(storage: HealthStorage);
7
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
8
+ load(): Promise<void>;
9
+ /** All retained records. */
10
+ snapshot(): readonly ResourceRecord[];
11
+ /**
12
+ * Replace the whole record set with a fresh registry snapshot. The bound
13
+ * keeps the most recently updated records — stale scopes matter less than
14
+ * the resources that are actually failing right now.
15
+ */
16
+ replace(records: readonly ResourceRecord[]): Promise<void>;
17
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Bounded health-record store: a persistence mirror of the in-memory
3
+ * resource-health registry. The store keeps the newest-updated records
4
+ * (bounded, least-recently-updated evicted) and persists on every replace.
5
+ * As everywhere else in `state/`, memory is only advanced after a successful
6
+ * write, so memory and disk never diverge; write failures propagate to the
7
+ * caller — the wiring layer's fail-open wrapper decides how to degrade
8
+ * (ADP-031).
9
+ */
10
+ import { parseHealthPayload } from "./parse.js";
11
+ import { MAX_HEALTH_RECORDS, HEALTH_VERSION } from "./types.js";
12
+ export class HealthRecordStore {
13
+ storage;
14
+ #records = [];
15
+ #queue = Promise.resolve();
16
+ constructor(storage) {
17
+ this.storage = storage;
18
+ }
19
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
20
+ load() {
21
+ return this.#serialize(() => this.#load());
22
+ }
23
+ async #load() {
24
+ let raw;
25
+ try {
26
+ raw = await this.storage.read();
27
+ }
28
+ catch {
29
+ this.#records = [];
30
+ return;
31
+ }
32
+ this.#records = parseHealthPayload(raw);
33
+ }
34
+ /** All retained records. */
35
+ snapshot() {
36
+ return this.#records;
37
+ }
38
+ /**
39
+ * Replace the whole record set with a fresh registry snapshot. The bound
40
+ * keeps the most recently updated records — stale scopes matter less than
41
+ * the resources that are actually failing right now.
42
+ */
43
+ replace(records) {
44
+ return this.#serialize(() => this.#replace(records));
45
+ }
46
+ async #replace(records) {
47
+ const next = records.length > MAX_HEALTH_RECORDS
48
+ ? [...records]
49
+ .sort((left, right) => right.updatedAt - left.updatedAt)
50
+ .slice(0, MAX_HEALTH_RECORDS)
51
+ : [...records];
52
+ // Persist BEFORE committing memory: a write failure must leave memory
53
+ // and disk consistent (the caller decides how to fail open).
54
+ const payload = {
55
+ version: HEALTH_VERSION,
56
+ records: next,
57
+ };
58
+ await this.storage.write(payload);
59
+ this.#records = next;
60
+ }
61
+ // Reloads and writes share one queue; otherwise a slow persistence call
62
+ // can restore older state after a newer call has already committed.
63
+ #serialize(operation) {
64
+ const run = this.#queue.then(operation);
65
+ this.#queue = run.catch(() => undefined);
66
+ return run;
67
+ }
68
+ }
69
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/state/health/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAI/D,MAAM,OAAO,iBAAiB;IAIC;IAH7B,QAAQ,GAAqB,EAAE,CAAA;IAC/B,MAAM,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAA;IAE5C,YAA6B,OAAsB;QAAtB,YAAO,GAAP,OAAO,CAAe;IAAG,CAAC;IAEvD,qFAAqF;IACrF,IAAI;QACF,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,GAAY,CAAA;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA;YAClB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;IACzC,CAAC;IAED,4BAA4B;IAC5B,QAAQ;QACN,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAAkC;QACxC,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;IACtD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAkC;QAC/C,MAAM,IAAI,GACR,OAAO,CAAC,MAAM,GAAG,kBAAkB;YACjC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;iBACT,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;iBACvD,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC;YACjC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAA;QAClB,sEAAsE;QACtE,6DAA6D;QAC7D,MAAM,OAAO,GAAkB;YAC7B,OAAO,EAAE,cAAc;YACvB,OAAO,EAAE,IAAI;SACd,CAAA;QACD,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACjC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;IACtB,CAAC;IAED,wEAAwE;IACxE,oEAAoE;IACpE,UAAU,CAAC,SAA8B;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACvC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACxC,OAAO,GAAG,CAAA;IACZ,CAAC;CACF","sourcesContent":["/**\n * Bounded health-record store: a persistence mirror of the in-memory\n * resource-health registry. The store keeps the newest-updated records\n * (bounded, least-recently-updated evicted) and persists on every replace.\n * As everywhere else in `state/`, memory is only advanced after a successful\n * write, so memory and disk never diverge; write failures propagate to the\n * caller — the wiring layer's fail-open wrapper decides how to degrade\n * (ADP-031).\n */\nimport { parseHealthPayload } from \"./parse.js\"\nimport { MAX_HEALTH_RECORDS, HEALTH_VERSION } from \"./types.js\"\nimport type { HealthPayload, HealthStorage } from \"./types.js\"\nimport type { ResourceRecord } from \"../../health/resource-health/types.js\"\n\nexport class HealthRecordStore {\n #records: ResourceRecord[] = []\n #queue: Promise<unknown> = Promise.resolve()\n\n constructor(private readonly storage: HealthStorage) {}\n\n /** Rebuild from storage (restart behavior). A read failure yields an empty store. */\n load(): Promise<void> {\n return this.#serialize(() => this.#load())\n }\n\n async #load(): Promise<void> {\n let raw: unknown\n try {\n raw = await this.storage.read()\n } catch {\n this.#records = []\n return\n }\n this.#records = parseHealthPayload(raw)\n }\n\n /** All retained records. */\n snapshot(): readonly ResourceRecord[] {\n return this.#records\n }\n\n /**\n * Replace the whole record set with a fresh registry snapshot. The bound\n * keeps the most recently updated records — stale scopes matter less than\n * the resources that are actually failing right now.\n */\n replace(records: readonly ResourceRecord[]): Promise<void> {\n return this.#serialize(() => this.#replace(records))\n }\n\n async #replace(records: readonly ResourceRecord[]): Promise<void> {\n const next =\n records.length > MAX_HEALTH_RECORDS\n ? [...records]\n .sort((left, right) => right.updatedAt - left.updatedAt)\n .slice(0, MAX_HEALTH_RECORDS)\n : [...records]\n // Persist BEFORE committing memory: a write failure must leave memory\n // and disk consistent (the caller decides how to fail open).\n const payload: HealthPayload = {\n version: HEALTH_VERSION,\n records: next,\n }\n await this.storage.write(payload)\n this.#records = next\n }\n\n // Reloads and writes share one queue; otherwise a slow persistence call\n // can restore older state after a newer call has already committed.\n #serialize(operation: () => Promise<void>): Promise<void> {\n const run = this.#queue.then(operation)\n this.#queue = run.catch(() => undefined)\n return run\n }\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Persisted resource health records. Health is otherwise memory-only state,
3
+ * but the host's TUI-mode plugin reconciliation can load the plugin several
4
+ * times into one process — hooks (the event pump) and RPC registration can
5
+ * end up on different instances. Without persistence the status surface is
6
+ * served by an instance whose registry never observed anything; persisted
7
+ * records make health readable by every instance, exactly like receipts,
8
+ * settings and parked work.
9
+ */
10
+ import type { ResourceRecord } from "../../health/resource-health/types.js";
11
+ export declare const HEALTH_KEY = "coool.adaptive/health";
12
+ export declare const HEALTH_VERSION = 1;
13
+ /** Product bound — not a user knob. Keeps the most recently updated records. */
14
+ export declare const MAX_HEALTH_RECORDS = 512;
15
+ export interface HealthPayload {
16
+ readonly version: typeof HEALTH_VERSION;
17
+ readonly records: readonly ResourceRecord[];
18
+ }
19
+ /** Storage seam — the adapter binds `HEALTH_KEY` to `ctx.storage`. */
20
+ export interface HealthStorage {
21
+ read(): Promise<unknown>;
22
+ write(payload: HealthPayload): Promise<void>;
23
+ }
@@ -0,0 +1,5 @@
1
+ export const HEALTH_KEY = "coool.adaptive/health";
2
+ export const HEALTH_VERSION = 1;
3
+ /** Product bound — not a user knob. Keeps the most recently updated records. */
4
+ export const MAX_HEALTH_RECORDS = 512;
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/state/health/types.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAA;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAC/B,gFAAgF;AAChF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA","sourcesContent":["/**\n * Persisted resource health records. Health is otherwise memory-only state,\n * but the host's TUI-mode plugin reconciliation can load the plugin several\n * times into one process — hooks (the event pump) and RPC registration can\n * end up on different instances. Without persistence the status surface is\n * served by an instance whose registry never observed anything; persisted\n * records make health readable by every instance, exactly like receipts,\n * settings and parked work.\n */\nimport type { ResourceRecord } from \"../../health/resource-health/types.js\"\n\nexport const HEALTH_KEY = \"coool.adaptive/health\"\nexport const HEALTH_VERSION = 1\n/** Product bound — not a user knob. Keeps the most recently updated records. */\nexport const MAX_HEALTH_RECORDS = 512\n\nexport interface HealthPayload {\n readonly version: typeof HEALTH_VERSION\n readonly records: readonly ResourceRecord[]\n}\n\n/** Storage seam — the adapter binds `HEALTH_KEY` to `ctx.storage`. */\nexport interface HealthStorage {\n read(): Promise<unknown>\n write(payload: HealthPayload): Promise<void>\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { parseParkedPayload } from "./parse.js";
2
+ export { ParkedWorkStore } from "./store.js";
3
+ export { MAX_PARKED, MAX_PARKED_TEXT, PARKED_KEY, PARKED_VERSION } from "./types.js";
4
+ export type { ParkedPayload, ParkedRecord, ParkedStorage } from "./types.js";
@@ -0,0 +1,4 @@
1
+ export { parseParkedPayload } from "./parse.js";
2
+ export { ParkedWorkStore } from "./store.js";
3
+ export { MAX_PARKED, MAX_PARKED_TEXT, PARKED_KEY, PARKED_VERSION } from "./types.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/state/parked-work/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA","sourcesContent":["export { parseParkedPayload } from \"./parse.js\"\nexport { ParkedWorkStore } from \"./store.js\"\nexport { MAX_PARKED, MAX_PARKED_TEXT, PARKED_KEY, PARKED_VERSION } from \"./types.js\"\nexport type { ParkedPayload, ParkedRecord, ParkedStorage } from \"./types.js\"\n"]}
@@ -0,0 +1,6 @@
1
+ import type { ParkedRecord } from "./types.js";
2
+ /**
3
+ * Parse a persisted parked payload. Accepts the versioned shape and a
4
+ * versionless draft array; anything else yields no records.
5
+ */
6
+ export declare function parseParkedPayload(raw: unknown): ParkedRecord[];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Read-side parsing for persisted parked records (ADP-024). Total: accepts
3
+ * `unknown`, drops invalid entries, never throws (ADP-031). Vocabulary is
4
+ * membership-checked (the write path is typed); free-text is re-bounded.
5
+ */
6
+ import { PARK_REASONS } from "../../core/scheduler/types.js";
7
+ import { TIERS } from "../../core/tier-policy/types.js";
8
+ import { MAX_PARKED, MAX_PARKED_TEXT, PARKED_VERSION } from "./types.js";
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function boundString(raw) {
13
+ if (typeof raw !== "string")
14
+ return undefined;
15
+ return raw.length > MAX_PARKED_TEXT ? raw.slice(0, MAX_PARKED_TEXT) : raw;
16
+ }
17
+ function parseOptionalBounded(raw) {
18
+ const value = boundString(raw);
19
+ return value === "" ? undefined : value;
20
+ }
21
+ /** Free-text detail keeps empty strings (round-trips through the store). */
22
+ function parseDetailBounded(raw) {
23
+ return boundString(raw) ?? undefined;
24
+ }
25
+ function parseParked(raw) {
26
+ if (!isRecord(raw))
27
+ return undefined;
28
+ const id = parseOptionalBounded(raw["id"]);
29
+ const sessionID = parseOptionalBounded(raw["sessionID"]);
30
+ const tierRaw = raw["tier"];
31
+ const reasonRaw = raw["reason"];
32
+ const parkedAt = raw["parkedAt"];
33
+ if (id === undefined ||
34
+ sessionID === undefined ||
35
+ typeof tierRaw !== "string" ||
36
+ typeof reasonRaw !== "string" ||
37
+ typeof parkedAt !== "number" ||
38
+ !Number.isFinite(parkedAt) ||
39
+ parkedAt < 0) {
40
+ return undefined;
41
+ }
42
+ const tier = TIERS.find((entry) => entry === tierRaw);
43
+ const reason = PARK_REASONS.find((entry) => entry === reasonRaw);
44
+ if (tier === undefined || reason === undefined)
45
+ return undefined;
46
+ const detail = parseDetailBounded(raw["detail"]);
47
+ const attemptsRaw = raw["attempts"];
48
+ const attempts = typeof attemptsRaw === "number" && Number.isInteger(attemptsRaw) && attemptsRaw >= 0
49
+ ? attemptsRaw
50
+ : undefined;
51
+ return {
52
+ id,
53
+ sessionID,
54
+ tier,
55
+ reason,
56
+ ...(detail !== undefined ? { detail } : {}),
57
+ parkedAt,
58
+ ...(attempts !== undefined ? { attempts } : {}),
59
+ };
60
+ }
61
+ /**
62
+ * Parse a persisted parked payload. Accepts the versioned shape and a
63
+ * versionless draft array; anything else yields no records.
64
+ */
65
+ export function parseParkedPayload(raw) {
66
+ let list;
67
+ if (isRecord(raw)) {
68
+ const version = raw["version"];
69
+ if (version !== PARKED_VERSION && version !== undefined)
70
+ return [];
71
+ list = raw["parked"];
72
+ }
73
+ else if (Array.isArray(raw)) {
74
+ list = raw;
75
+ }
76
+ if (!Array.isArray(list))
77
+ return [];
78
+ const parsed = list
79
+ .map(parseParked)
80
+ .filter((record) => record !== undefined);
81
+ return parsed.length > MAX_PARKED ? parsed.slice(-MAX_PARKED) : parsed;
82
+ }
83
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/state/parked-work/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read-side parsing for persisted parked records (ADP-024). Total: accepts\n * `unknown`, drops invalid entries, never throws (ADP-031). Vocabulary is\n * membership-checked (the write path is typed); free-text is re-bounded.\n */\nimport { PARK_REASONS } from \"../../core/scheduler/types.js\"\nimport { TIERS } from \"../../core/tier-policy/types.js\"\nimport { MAX_PARKED, MAX_PARKED_TEXT, PARKED_VERSION } from \"./types.js\"\nimport type { ParkedRecord } from \"./types.js\"\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction boundString(raw: unknown): string | undefined {\n if (typeof raw !== \"string\") return undefined\n return raw.length > MAX_PARKED_TEXT ? raw.slice(0, MAX_PARKED_TEXT) : raw\n}\n\nfunction parseOptionalBounded(raw: unknown): string | undefined {\n const value = boundString(raw)\n return value === \"\" ? undefined : value\n}\n\n/** Free-text detail keeps empty strings (round-trips through the store). */\nfunction parseDetailBounded(raw: unknown): string | undefined {\n return boundString(raw) ?? undefined\n}\n\nfunction parseParked(raw: unknown): ParkedRecord | undefined {\n if (!isRecord(raw)) return undefined\n const id = parseOptionalBounded(raw[\"id\"])\n const sessionID = parseOptionalBounded(raw[\"sessionID\"])\n const tierRaw = raw[\"tier\"]\n const reasonRaw = raw[\"reason\"]\n const parkedAt = raw[\"parkedAt\"]\n if (\n id === undefined ||\n sessionID === undefined ||\n typeof tierRaw !== \"string\" ||\n typeof reasonRaw !== \"string\" ||\n typeof parkedAt !== \"number\" ||\n !Number.isFinite(parkedAt) ||\n parkedAt < 0\n ) {\n return undefined\n }\n const tier = TIERS.find((entry) => entry === tierRaw)\n const reason = PARK_REASONS.find((entry) => entry === reasonRaw)\n if (tier === undefined || reason === undefined) return undefined\n const detail = parseDetailBounded(raw[\"detail\"])\n const attemptsRaw = raw[\"attempts\"]\n const attempts =\n typeof attemptsRaw === \"number\" && Number.isInteger(attemptsRaw) && attemptsRaw >= 0\n ? attemptsRaw\n : undefined\n return {\n id,\n sessionID,\n tier,\n reason,\n ...(detail !== undefined ? { detail } : {}),\n parkedAt,\n ...(attempts !== undefined ? { attempts } : {}),\n }\n}\n\n/**\n * Parse a persisted parked payload. Accepts the versioned shape and a\n * versionless draft array; anything else yields no records.\n */\nexport function parseParkedPayload(raw: unknown): ParkedRecord[] {\n let list: unknown\n if (isRecord(raw)) {\n const version = raw[\"version\"]\n if (version !== PARKED_VERSION && version !== undefined) return []\n list = raw[\"parked\"]\n } else if (Array.isArray(raw)) {\n list = raw\n }\n if (!Array.isArray(list)) return []\n const parsed = list\n .map(parseParked)\n .filter((record): record is ParkedRecord => record !== undefined)\n return parsed.length > MAX_PARKED ? parsed.slice(-MAX_PARKED) : parsed\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { ParkedRecord, ParkedStorage } from "./types.js";
2
+ export declare class ParkedWorkStore {
3
+ #private;
4
+ private readonly storage;
5
+ constructor(storage: ParkedStorage);
6
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
7
+ load(): Promise<void>;
8
+ /** All parked records, insertion order. */
9
+ list(): readonly ParkedRecord[];
10
+ count(): number;
11
+ park(record: ParkedRecord): Promise<void>;
12
+ /**
13
+ * Remove a parked record and return it so the execution layer knows what
14
+ * to continue. `undefined` (no record, no write) when the id is unknown.
15
+ */
16
+ resume(id: string): Promise<ParkedRecord | undefined>;
17
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Parked-work store (ADP-024). Records are keyed by unit id — parking the
3
+ * same unit again replaces the record (latest park wins). The cap
4
+ * `MAX_PARKED` evicts the oldest `parkedAt` first: parked work is waiting
5
+ * work, and unbounded growth would mean recovery never happens. Write
6
+ * failures propagate to the caller (fail-open is the wiring layer's call), and
7
+ * the in-memory list is only advanced after a successful write — memory and
8
+ * disk never diverge.
9
+ */
10
+ import { parseParkedPayload } from "./parse.js";
11
+ import { MAX_PARKED, MAX_PARKED_TEXT, PARKED_VERSION } from "./types.js";
12
+ function boundDetail(raw) {
13
+ return raw.length > MAX_PARKED_TEXT ? raw.slice(0, MAX_PARKED_TEXT) : raw;
14
+ }
15
+ export class ParkedWorkStore {
16
+ storage;
17
+ #records = [];
18
+ #queue = Promise.resolve();
19
+ constructor(storage) {
20
+ this.storage = storage;
21
+ }
22
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
23
+ load() {
24
+ return this.#serialize(() => this.#load());
25
+ }
26
+ async #load() {
27
+ let raw;
28
+ try {
29
+ raw = await this.storage.read();
30
+ }
31
+ catch {
32
+ this.#records = [];
33
+ return;
34
+ }
35
+ this.#records = parseParkedPayload(raw);
36
+ }
37
+ /** All parked records, insertion order. */
38
+ list() {
39
+ return this.#records;
40
+ }
41
+ count() {
42
+ return this.#records.length;
43
+ }
44
+ park(record) {
45
+ return this.#serialize(() => this.#park(record));
46
+ }
47
+ async #park(record) {
48
+ const bounded = {
49
+ ...record,
50
+ ...(record.detail !== undefined ? { detail: boundDetail(record.detail) } : {}),
51
+ };
52
+ const withoutId = this.#records.filter((entry) => entry.id !== bounded.id);
53
+ const next = this.#evictOldest([...withoutId, bounded]);
54
+ // Persist BEFORE committing memory: a failed resume must not silently
55
+ // drop the parked unit (memory and disk stay consistent on failure).
56
+ await this.#persist(next);
57
+ this.#records = next;
58
+ }
59
+ /**
60
+ * Remove a parked record and return it so the execution layer knows what
61
+ * to continue. `undefined` (no record, no write) when the id is unknown.
62
+ */
63
+ resume(id) {
64
+ return this.#serialize(() => this.#resume(id));
65
+ }
66
+ async #resume(id) {
67
+ const record = this.#records.find((entry) => entry.id === id);
68
+ if (record === undefined)
69
+ return undefined;
70
+ const next = this.#records.filter((entry) => entry.id !== id);
71
+ await this.#persist(next);
72
+ this.#records = next;
73
+ return record;
74
+ }
75
+ #evictOldest(records) {
76
+ if (records.length <= MAX_PARKED)
77
+ return records;
78
+ const byOldest = [...records].sort((a, b) => a.parkedAt - b.parkedAt);
79
+ const dropped = new Set(byOldest.slice(0, records.length - MAX_PARKED).map((r) => r.id));
80
+ return records.filter((record) => !dropped.has(record.id));
81
+ }
82
+ // Reads share the queue so reloads cannot overwrite newer commits. Recover
83
+ // the queue after rejection while preserving the failing caller's promise.
84
+ #serialize(operation) {
85
+ const run = this.#queue.then(operation);
86
+ this.#queue = run.catch(() => undefined);
87
+ return run;
88
+ }
89
+ /** Writes the given next state; the caller commits memory only on success. */
90
+ async #persist(parked) {
91
+ const payload = {
92
+ version: PARKED_VERSION,
93
+ parked,
94
+ };
95
+ await this.storage.write(payload);
96
+ }
97
+ }
98
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/state/parked-work/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parked-work store (ADP-024). Records are keyed by unit id — parking the\n * same unit again replaces the record (latest park wins). The cap\n * `MAX_PARKED` evicts the oldest `parkedAt` first: parked work is waiting\n * work, and unbounded growth would mean recovery never happens. Write\n * failures propagate to the caller (fail-open is the wiring layer's call), and\n * the in-memory list is only advanced after a successful write — memory and\n * disk never diverge.\n */\nimport { parseParkedPayload } from \"./parse.js\"\nimport { MAX_PARKED, MAX_PARKED_TEXT, PARKED_VERSION } from \"./types.js\"\nimport type { ParkedPayload, ParkedRecord, ParkedStorage } from \"./types.js\"\n\nfunction boundDetail(raw: string): string {\n return raw.length > MAX_PARKED_TEXT ? raw.slice(0, MAX_PARKED_TEXT) : raw\n}\n\nexport class ParkedWorkStore {\n #records: ParkedRecord[] = []\n #queue: Promise<unknown> = Promise.resolve()\n\n constructor(private readonly storage: ParkedStorage) {}\n\n /** Rebuild from storage (restart behavior). A read failure yields an empty store. */\n load(): Promise<void> {\n return this.#serialize(() => this.#load())\n }\n\n async #load(): Promise<void> {\n let raw: unknown\n try {\n raw = await this.storage.read()\n } catch {\n this.#records = []\n return\n }\n this.#records = parseParkedPayload(raw)\n }\n\n /** All parked records, insertion order. */\n list(): readonly ParkedRecord[] {\n return this.#records\n }\n\n count(): number {\n return this.#records.length\n }\n\n park(record: ParkedRecord): Promise<void> {\n return this.#serialize(() => this.#park(record))\n }\n\n async #park(record: ParkedRecord): Promise<void> {\n const bounded: ParkedRecord = {\n ...record,\n ...(record.detail !== undefined ? { detail: boundDetail(record.detail) } : {}),\n }\n const withoutId = this.#records.filter((entry) => entry.id !== bounded.id)\n const next = this.#evictOldest([...withoutId, bounded])\n // Persist BEFORE committing memory: a failed resume must not silently\n // drop the parked unit (memory and disk stay consistent on failure).\n await this.#persist(next)\n this.#records = next\n }\n\n /**\n * Remove a parked record and return it so the execution layer knows what\n * to continue. `undefined` (no record, no write) when the id is unknown.\n */\n resume(id: string): Promise<ParkedRecord | undefined> {\n return this.#serialize(() => this.#resume(id))\n }\n\n async #resume(id: string): Promise<ParkedRecord | undefined> {\n const record = this.#records.find((entry) => entry.id === id)\n if (record === undefined) return undefined\n const next = this.#records.filter((entry) => entry.id !== id)\n await this.#persist(next)\n this.#records = next\n return record\n }\n\n #evictOldest(records: ParkedRecord[]): ParkedRecord[] {\n if (records.length <= MAX_PARKED) return records\n const byOldest = [...records].sort((a, b) => a.parkedAt - b.parkedAt)\n const dropped = new Set(byOldest.slice(0, records.length - MAX_PARKED).map((r) => r.id))\n return records.filter((record) => !dropped.has(record.id))\n }\n\n // Reads share the queue so reloads cannot overwrite newer commits. Recover\n // the queue after rejection while preserving the failing caller's promise.\n #serialize<T>(operation: () => Promise<T>): Promise<T> {\n const run = this.#queue.then(operation)\n this.#queue = run.catch(() => undefined)\n return run\n }\n\n /** Writes the given next state; the caller commits memory only on success. */\n async #persist(parked: readonly ParkedRecord[]): Promise<void> {\n const payload: ParkedPayload = {\n version: PARKED_VERSION,\n parked,\n }\n await this.storage.write(payload)\n }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Durable parked work (ADP-024): admission-time records for work with no
3
+ * compatible healthy resource, persisted so they survive restart. A parked
4
+ * record carries only identity and routing context — resumption (T-13) is a
5
+ * clean continuation that re-runs routing fresh; it is never the resurrection
6
+ * of a failed network call.
7
+ */
8
+ import type { ParkReason } from "../../core/scheduler/types.js";
9
+ import type { TierName } from "../../core/tier-policy/types.js";
10
+ export declare const PARKED_KEY = "coool.adaptive/parked";
11
+ export declare const PARKED_VERSION = 1;
12
+ /** Product bound — not a user knob. Eviction drops the oldest parkedAt. */
13
+ export declare const MAX_PARKED = 64;
14
+ /** Free-text detail is bounded like every other persisted string. */
15
+ export declare const MAX_PARKED_TEXT = 128;
16
+ export interface ParkedRecord {
17
+ /** Unit id (`${sessionID}#${episode}`) — one parked record per unit. */
18
+ readonly id: string;
19
+ readonly sessionID: string;
20
+ /** Required tier at park time; resumption re-runs routing fresh. */
21
+ readonly tier: TierName;
22
+ readonly reason: ParkReason;
23
+ readonly detail?: string | undefined;
24
+ /** Park timestamp (adapter clock, ms epoch). */
25
+ readonly parkedAt: number;
26
+ /**
27
+ * Failed resume attempts (absent = never retried). The resume planner
28
+ * drops records whose attempts reach the planner's budget so a
29
+ * persistently failing continuation cannot retry forever (ADP-011).
30
+ */
31
+ readonly attempts?: number | undefined;
32
+ }
33
+ export interface ParkedPayload {
34
+ readonly version: typeof PARKED_VERSION;
35
+ readonly parked: readonly ParkedRecord[];
36
+ }
37
+ /** Storage seam — the adapter binds `PARKED_KEY` to `ctx.storage`. */
38
+ export interface ParkedStorage {
39
+ read(): Promise<unknown>;
40
+ write(payload: ParkedPayload): Promise<void>;
41
+ }
@@ -0,0 +1,7 @@
1
+ export const PARKED_KEY = "coool.adaptive/parked";
2
+ export const PARKED_VERSION = 1;
3
+ /** Product bound — not a user knob. Eviction drops the oldest parkedAt. */
4
+ export const MAX_PARKED = 64;
5
+ /** Free-text detail is bounded like every other persisted string. */
6
+ export const MAX_PARKED_TEXT = 128;
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/state/parked-work/types.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAA;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAC/B,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAA;AAC5B,qEAAqE;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA","sourcesContent":["/**\n * Durable parked work (ADP-024): admission-time records for work with no\n * compatible healthy resource, persisted so they survive restart. A parked\n * record carries only identity and routing context — resumption (T-13) is a\n * clean continuation that re-runs routing fresh; it is never the resurrection\n * of a failed network call.\n */\nimport type { ParkReason } from \"../../core/scheduler/types.js\"\nimport type { TierName } from \"../../core/tier-policy/types.js\"\n\nexport const PARKED_KEY = \"coool.adaptive/parked\"\nexport const PARKED_VERSION = 1\n/** Product bound — not a user knob. Eviction drops the oldest parkedAt. */\nexport const MAX_PARKED = 64\n/** Free-text detail is bounded like every other persisted string. */\nexport const MAX_PARKED_TEXT = 128\n\nexport interface ParkedRecord {\n /** Unit id (`${sessionID}#${episode}`) — one parked record per unit. */\n readonly id: string\n readonly sessionID: string\n /** Required tier at park time; resumption re-runs routing fresh. */\n readonly tier: TierName\n readonly reason: ParkReason\n readonly detail?: string | undefined\n /** Park timestamp (adapter clock, ms epoch). */\n readonly parkedAt: number\n /**\n * Failed resume attempts (absent = never retried). The resume planner\n * drops records whose attempts reach the planner's budget so a\n * persistently failing continuation cannot retry forever (ADP-011).\n */\n readonly attempts?: number | undefined\n}\n\nexport interface ParkedPayload {\n readonly version: typeof PARKED_VERSION\n readonly parked: readonly ParkedRecord[]\n}\n\n/** Storage seam — the adapter binds `PARKED_KEY` to `ctx.storage`. */\nexport interface ParkedStorage {\n read(): Promise<unknown>\n write(payload: ParkedPayload): Promise<void>\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bounding for receipts (ADP-030). Receipts are built from typed scheduler
3
+ * decisions, but every string they can carry is truncated to
4
+ * `MAX_RECEIPT_TEXT` before persisting — including identifiers. Boundedness
5
+ * is what makes the no-secrets invariant checkable: no field accepts
6
+ * unbounded text, so nothing unbounded can be persisted.
7
+ */
8
+ import type { Decision } from "../../core/scheduler/types.js";
9
+ export declare function boundText(raw: string): string;
10
+ /**
11
+ * Structural copy of a decision with every string bounded. `depth` guards
12
+ * the `observe-only` recursion (max one nesting level is ever produced).
13
+ */
14
+ export declare function boundDecision(decision: Decision, depth?: number): Decision;