@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,12 @@
1
+ export const RECEIPTS_KEY = "coool.adaptive/receipts";
2
+ export const RECEIPTS_VERSION = 1;
3
+ /** Product bound — not a user knob. The ring keeps the newest receipts. */
4
+ export const MAX_RECEIPTS = 200;
5
+ /**
6
+ * Every free-text string stored in a receipt is truncated to this bound
7
+ * before persisting (and re-truncated on read). Boundedness is the
8
+ * no-secrets invariant: prompts, error bodies, and credentials have no
9
+ * field to live in and no room to hide in.
10
+ */
11
+ export const MAX_RECEIPT_TEXT = 128;
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/state/receipts/types.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAA;AACrD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAA;AACjC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAC/B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAA","sourcesContent":["/**\n * Decision receipts (ADP-030): small bounded local records explaining why\n * Adaptive made each decision. Receipts exist for explainability, debugging,\n * tests, and a future outcome-learning policy — never injected into prompts,\n * never leaving the machine. Retention is bounded (oldest evicted).\n */\nimport type { Decision } from \"../../core/scheduler/types.js\"\nimport type { TierName } from \"../../core/tier-policy/types.js\"\nimport type { InterruptReason } from \"../../core/work-unit/interruption.js\"\nimport type { FailureCategory } from \"../../health/classifier/types.js\"\n\nexport const RECEIPTS_KEY = \"coool.adaptive/receipts\"\nexport const RECEIPTS_VERSION = 1\n/** Product bound — not a user knob. The ring keeps the newest receipts. */\nexport const MAX_RECEIPTS = 200\n/**\n * Every free-text string stored in a receipt is truncated to this bound\n * before persisting (and re-truncated on read). Boundedness is the\n * no-secrets invariant: prompts, error bodies, and credentials have no\n * field to live in and no room to hide in.\n */\nexport const MAX_RECEIPT_TEXT = 128\n\nexport interface ReceiptMeta {\n /** Decision timestamp (adapter clock, ms epoch). */\n readonly at: number\n readonly sessionID: string\n readonly unitID?: string | undefined\n /** Required tier when routing named one (pins carry no tier). */\n readonly tier?: TierName | undefined\n}\n\nexport interface ReceiptInput extends ReceiptMeta {\n /** The decision exactly as the scheduler/mode wrapper produced it. */\n readonly decision: Decision\n}\n\n/**\n * Unit outcome attached after termination. Structured vocabulary only —\n * never the raw error text (ADP-030: no secrets, bounded records).\n */\nexport type ReceiptOutcome =\n | { readonly status: \"succeeded\" }\n | { readonly status: \"failed\"; readonly category: FailureCategory }\n | { readonly status: \"interrupted\"; readonly reason: InterruptReason }\n\nexport interface Receipt extends ReceiptMeta {\n /** Monotonic id assigned by the store; continues across restarts. */\n readonly id: number\n readonly decision: Decision\n readonly outcome?: ReceiptOutcome\n}\n\nexport interface ReceiptsPayload {\n readonly version: typeof RECEIPTS_VERSION\n readonly receipts: readonly Receipt[]\n}\n\n/** Storage seam — the adapter binds `RECEIPTS_KEY` to `ctx.storage`. */\nexport interface ReceiptsStorage {\n read(): Promise<unknown>\n write(payload: ReceiptsPayload): Promise<void>\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { parseSessionModesPayload, isValidSessionID } from "./parse.js";
2
+ export { SessionModeStore } from "./store.js";
3
+ export { SESSION_MODES_KEY, SESSION_MODES_VERSION, MAX_SESSION_MODE_RECORDS, MAX_SESSION_ID_LENGTH, } from "./types.js";
4
+ export type { SessionModeRecord, SessionModesPayload, SessionModeSetResult, SessionModesStorage, } from "./types.js";
@@ -0,0 +1,4 @@
1
+ export { parseSessionModesPayload, isValidSessionID } from "./parse.js";
2
+ export { SessionModeStore } from "./store.js";
3
+ export { SESSION_MODES_KEY, SESSION_MODES_VERSION, MAX_SESSION_MODE_RECORDS, MAX_SESSION_ID_LENGTH, } from "./types.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/state/session-modes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAC7C,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,YAAY,CAAA","sourcesContent":["export { parseSessionModesPayload, isValidSessionID } from \"./parse.js\"\nexport { SessionModeStore } from \"./store.js\"\nexport {\n SESSION_MODES_KEY,\n SESSION_MODES_VERSION,\n MAX_SESSION_MODE_RECORDS,\n MAX_SESSION_ID_LENGTH,\n} from \"./types.js\"\nexport type {\n SessionModeRecord,\n SessionModesPayload,\n SessionModeSetResult,\n SessionModesStorage,\n} from \"./types.js\"\n"]}
@@ -0,0 +1,8 @@
1
+ import type { SessionModeRecord } from "./types.js";
2
+ /**
3
+ * A non-empty bounded session identifier. Unlike the health parser's provider
4
+ * identifiers (truncated on read), an over-long sessionID is dropped: the id
5
+ * is a host identity, not a label, and no real session id exceeds the bound.
6
+ */
7
+ export declare function isValidSessionID(raw: unknown): raw is string;
8
+ export declare function parseSessionModesPayload(raw: unknown): SessionModeRecord[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Read-side parsing for persisted session-mode overrides. Total like every
3
+ * `state/` parser: it accepts `unknown` and returns typed records, dropping
4
+ * anything invalid — a corrupted file can never take the plugin down
5
+ * (ADP-031). A partially corrupted payload keeps its good entries.
6
+ */
7
+ import { OPERATING_MODES } from "../../core/scheduler/types.js";
8
+ import { MAX_SESSION_ID_LENGTH, SESSION_MODES_VERSION } from "./types.js";
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function isFiniteNumber(raw) {
13
+ return typeof raw === "number" && Number.isFinite(raw);
14
+ }
15
+ /**
16
+ * A non-empty bounded session identifier. Unlike the health parser's provider
17
+ * identifiers (truncated on read), an over-long sessionID is dropped: the id
18
+ * is a host identity, not a label, and no real session id exceeds the bound.
19
+ */
20
+ export function isValidSessionID(raw) {
21
+ return typeof raw === "string" && raw.length > 0 && raw.length <= MAX_SESSION_ID_LENGTH;
22
+ }
23
+ function parseSessionID(raw) {
24
+ return isValidSessionID(raw) ? raw : undefined;
25
+ }
26
+ function parseMode(raw) {
27
+ if (typeof raw !== "string")
28
+ return undefined;
29
+ return OPERATING_MODES.includes(raw) ? raw : undefined;
30
+ }
31
+ function parseRecord(raw) {
32
+ if (!isRecord(raw))
33
+ return undefined;
34
+ const sessionID = parseSessionID(raw["sessionID"]);
35
+ const mode = parseMode(raw["mode"]);
36
+ const updatedAt = raw["updatedAt"];
37
+ if (sessionID === undefined || mode === undefined || !isFiniteNumber(updatedAt))
38
+ return undefined;
39
+ return { sessionID, mode, updatedAt };
40
+ }
41
+ export function parseSessionModesPayload(raw) {
42
+ if (!isRecord(raw))
43
+ return [];
44
+ if (raw["version"] !== SESSION_MODES_VERSION)
45
+ return [];
46
+ const list = raw["records"];
47
+ if (!Array.isArray(list))
48
+ return [];
49
+ // A hostile or duplicated payload can carry the same session twice; the
50
+ // most recently updated record wins so `get` is not order-dependent.
51
+ const bySession = new Map();
52
+ for (const entry of list.slice(0, 512)) {
53
+ const record = parseRecord(entry);
54
+ if (record === undefined)
55
+ continue;
56
+ const kept = bySession.get(record.sessionID);
57
+ if (kept === undefined || record.updatedAt > kept.updatedAt)
58
+ bySession.set(record.sessionID, record);
59
+ }
60
+ return [...bySession.values()];
61
+ }
62
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/state/session-modes/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAE/D,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAGzE,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;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,qBAAqB,CAAA;AACzF,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAChD,CAAC;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAQ,eAAqC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,GAAqB,CAAC,CAAC,CAAC,SAAS,CAAA;AAClG,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAA;IAClC,IAAI,SAAS,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IACjG,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;AACvC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,GAAY;IACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC7B,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,qBAAqB;QAAE,OAAO,EAAE,CAAA;IACvD,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,SAAS,GAAG,IAAI,GAAG,EAA6B,CAAA;IACtD,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,SAAQ;QAClC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAC5C,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS;YACzD,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAA;AAChC,CAAC","sourcesContent":["/**\n * Read-side parsing for persisted session-mode overrides. Total like every\n * `state/` parser: it accepts `unknown` and returns typed records, dropping\n * anything invalid — a corrupted file can never take the plugin down\n * (ADP-031). A partially corrupted payload keeps its good entries.\n */\nimport { OPERATING_MODES } from \"../../core/scheduler/types.js\"\nimport type { OperatingMode } from \"../../core/scheduler/types.js\"\nimport { MAX_SESSION_ID_LENGTH, SESSION_MODES_VERSION } from \"./types.js\"\nimport type { SessionModeRecord } 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\n/**\n * A non-empty bounded session identifier. Unlike the health parser's provider\n * identifiers (truncated on read), an over-long sessionID is dropped: the id\n * is a host identity, not a label, and no real session id exceeds the bound.\n */\nexport function isValidSessionID(raw: unknown): raw is string {\n return typeof raw === \"string\" && raw.length > 0 && raw.length <= MAX_SESSION_ID_LENGTH\n}\n\nfunction parseSessionID(raw: unknown): string | undefined {\n return isValidSessionID(raw) ? raw : undefined\n}\n\nfunction parseMode(raw: unknown): OperatingMode | undefined {\n if (typeof raw !== \"string\") return undefined\n return (OPERATING_MODES as readonly string[]).includes(raw) ? (raw as OperatingMode) : undefined\n}\n\nfunction parseRecord(raw: unknown): SessionModeRecord | undefined {\n if (!isRecord(raw)) return undefined\n const sessionID = parseSessionID(raw[\"sessionID\"])\n const mode = parseMode(raw[\"mode\"])\n const updatedAt = raw[\"updatedAt\"]\n if (sessionID === undefined || mode === undefined || !isFiniteNumber(updatedAt)) return undefined\n return { sessionID, mode, updatedAt }\n}\n\nexport function parseSessionModesPayload(raw: unknown): SessionModeRecord[] {\n if (!isRecord(raw)) return []\n if (raw[\"version\"] !== SESSION_MODES_VERSION) return []\n const list = raw[\"records\"]\n if (!Array.isArray(list)) return []\n // A hostile or duplicated payload can carry the same session twice; the\n // most recently updated record wins so `get` is not order-dependent.\n const bySession = new Map<string, SessionModeRecord>()\n for (const entry of list.slice(0, 512)) {\n const record = parseRecord(entry)\n if (record === undefined) continue\n const kept = bySession.get(record.sessionID)\n if (kept === undefined || record.updatedAt > kept.updatedAt)\n bySession.set(record.sessionID, record)\n }\n return [...bySession.values()]\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import type { OperatingMode } from "../../core/scheduler/types.js";
2
+ import type { SessionModeRecord, SessionModesStorage } from "./types.js";
3
+ export declare class SessionModeStore {
4
+ #private;
5
+ private readonly storage;
6
+ private readonly clock;
7
+ constructor(storage: SessionModesStorage, clock: () => number);
8
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
9
+ load(): Promise<void>;
10
+ /** All retained records. */
11
+ snapshot(): readonly SessionModeRecord[];
12
+ /** The session's override, or `null` when it follows the global mode. */
13
+ get(sessionID: string): OperatingMode | null;
14
+ /**
15
+ * Set (or with `null`, remove) one session's override. The bound keeps the
16
+ * most recently updated records — overrides for long-gone sessions matter
17
+ * less than the ones being actively steered.
18
+ */
19
+ set(sessionID: string, mode: OperatingMode | null): Promise<void>;
20
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Bounded per-session operating-mode override store. Keeps the most recently
3
+ * updated records (bounded, least-recently-updated evicted) and persists on
4
+ * every change. As everywhere else in `state/`, memory is only advanced after
5
+ * a successful write, so memory and disk never diverge; write failures
6
+ * propagate to the caller — the wiring layer's fail-open wrapper decides how
7
+ * to degrade (ADP-031).
8
+ */
9
+ import { parseSessionModesPayload, isValidSessionID } from "./parse.js";
10
+ import { MAX_SESSION_ID_LENGTH, MAX_SESSION_MODE_RECORDS, SESSION_MODES_VERSION } from "./types.js";
11
+ import { OPERATING_MODES } from "../../core/scheduler/types.js";
12
+ export class SessionModeStore {
13
+ storage;
14
+ clock;
15
+ #records = [];
16
+ #queue = Promise.resolve();
17
+ constructor(storage, clock) {
18
+ this.storage = storage;
19
+ this.clock = clock;
20
+ }
21
+ /** Rebuild from storage (restart behavior). A read failure yields an empty store. */
22
+ load() {
23
+ return this.#serialize(() => this.#load());
24
+ }
25
+ async #load() {
26
+ let raw;
27
+ try {
28
+ raw = await this.storage.read();
29
+ }
30
+ catch {
31
+ this.#records = [];
32
+ return;
33
+ }
34
+ this.#records = parseSessionModesPayload(raw);
35
+ }
36
+ /** All retained records. */
37
+ snapshot() {
38
+ return this.#records;
39
+ }
40
+ /** The session's override, or `null` when it follows the global mode. */
41
+ get(sessionID) {
42
+ return this.#records.find((record) => record.sessionID === sessionID)?.mode ?? null;
43
+ }
44
+ /**
45
+ * Set (or with `null`, remove) one session's override. The bound keeps the
46
+ * most recently updated records — overrides for long-gone sessions matter
47
+ * less than the ones being actively steered.
48
+ */
49
+ async set(sessionID, mode) {
50
+ // Defensive re-validation at the persistence boundary: the RPC handler
51
+ // validates first and reports named errors; these guards keep malformed
52
+ // in-process callers from persisting junk.
53
+ if (!isValidSessionID(sessionID)) {
54
+ throw new Error(`sessionID must be a non-empty string of at most ${String(MAX_SESSION_ID_LENGTH)} characters`);
55
+ }
56
+ if (mode !== null && !OPERATING_MODES.includes(mode)) {
57
+ throw new Error(`mode must be one of ${OPERATING_MODES.join(", ")} or null`);
58
+ }
59
+ return this.#serialize(() => this.#set(sessionID, mode));
60
+ }
61
+ async #set(sessionID, mode) {
62
+ const updatedAt = this.clock();
63
+ const kept = this.#records.filter((record) => record.sessionID !== sessionID);
64
+ const next = mode === null
65
+ ? kept
66
+ : bound([...kept, { sessionID, mode, updatedAt }], MAX_SESSION_MODE_RECORDS);
67
+ // Persist BEFORE committing memory: a write failure must leave memory
68
+ // and disk consistent (the caller decides how to fail open).
69
+ const payload = { version: SESSION_MODES_VERSION, records: next };
70
+ await this.storage.write(payload);
71
+ this.#records = next;
72
+ }
73
+ // Reloads share the mutation queue so stale reads cannot overwrite commits.
74
+ // Recover only the queue tail: the failed operation still rejects its caller.
75
+ #serialize(operation) {
76
+ const run = this.#queue.then(operation);
77
+ this.#queue = run.catch(() => undefined);
78
+ return run;
79
+ }
80
+ }
81
+ /** Keep the newest records when over the cap. */
82
+ function bound(records, max) {
83
+ if (records.length <= max)
84
+ return records;
85
+ return [...records].sort((left, right) => right.updatedAt - left.updatedAt).slice(0, max);
86
+ }
87
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/state/session-modes/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bounded per-session operating-mode override store. Keeps the most recently\n * updated records (bounded, least-recently-updated evicted) and persists on\n * every change. As everywhere else in `state/`, memory is only advanced after\n * a successful write, so memory and disk never diverge; write failures\n * propagate to the caller — the wiring layer's fail-open wrapper decides how\n * to degrade (ADP-031).\n */\nimport { parseSessionModesPayload, isValidSessionID } from \"./parse.js\"\nimport { MAX_SESSION_ID_LENGTH, MAX_SESSION_MODE_RECORDS, SESSION_MODES_VERSION } from \"./types.js\"\nimport type { OperatingMode } from \"../../core/scheduler/types.js\"\nimport type { SessionModeRecord, SessionModesPayload, SessionModesStorage } from \"./types.js\"\nimport { OPERATING_MODES } from \"../../core/scheduler/types.js\"\n\nexport class SessionModeStore {\n #records: SessionModeRecord[] = []\n #queue: Promise<void> = Promise.resolve()\n\n constructor(\n private readonly storage: SessionModesStorage,\n private readonly clock: () => number,\n ) {}\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 = parseSessionModesPayload(raw)\n }\n\n /** All retained records. */\n snapshot(): readonly SessionModeRecord[] {\n return this.#records\n }\n\n /** The session's override, or `null` when it follows the global mode. */\n get(sessionID: string): OperatingMode | null {\n return this.#records.find((record) => record.sessionID === sessionID)?.mode ?? null\n }\n\n /**\n * Set (or with `null`, remove) one session's override. The bound keeps the\n * most recently updated records — overrides for long-gone sessions matter\n * less than the ones being actively steered.\n */\n async set(sessionID: string, mode: OperatingMode | null): Promise<void> {\n // Defensive re-validation at the persistence boundary: the RPC handler\n // validates first and reports named errors; these guards keep malformed\n // in-process callers from persisting junk.\n if (!isValidSessionID(sessionID)) {\n throw new Error(\n `sessionID must be a non-empty string of at most ${String(MAX_SESSION_ID_LENGTH)} characters`,\n )\n }\n if (mode !== null && !(OPERATING_MODES as readonly string[]).includes(mode)) {\n throw new Error(`mode must be one of ${OPERATING_MODES.join(\", \")} or null`)\n }\n return this.#serialize(() => this.#set(sessionID, mode))\n }\n\n async #set(sessionID: string, mode: OperatingMode | null): Promise<void> {\n const updatedAt = this.clock()\n const kept = this.#records.filter((record) => record.sessionID !== sessionID)\n const next =\n mode === null\n ? kept\n : bound([...kept, { sessionID, mode, updatedAt }], MAX_SESSION_MODE_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: SessionModesPayload = { version: SESSION_MODES_VERSION, records: next }\n await this.storage.write(payload)\n this.#records = next\n }\n\n // Reloads share the mutation queue so stale reads cannot overwrite commits.\n // Recover only the queue tail: the failed operation still rejects its caller.\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\n/** Keep the newest records when over the cap. */\nfunction bound(records: SessionModeRecord[], max: number): SessionModeRecord[] {\n if (records.length <= max) return records\n return [...records].sort((left, right) => right.updatedAt - left.updatedAt).slice(0, max)\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Persisted per-session operating-mode overrides (ADP-054). The operating mode
3
+ * is otherwise a global setting, but supervision decisions are made per
4
+ * session — the override lets one session run under a different mode without
5
+ * touching the global default. Records persist through the storage seam like
6
+ * every other state: the host's TUI-mode plugin reconciliation can serve RPC
7
+ * from an instance whose event-pump hooks live on another instance, and a
8
+ * restart must not silently drop explicit user intent.
9
+ */
10
+ import type { OperatingMode } from "../../core/scheduler/types.js";
11
+ export declare const SESSION_MODES_KEY = "coool.adaptive/session-modes";
12
+ export declare const SESSION_MODES_VERSION = 1;
13
+ /** Product bound — not a user knob. Keeps the most recently updated records. */
14
+ export declare const MAX_SESSION_MODE_RECORDS = 512;
15
+ /** A session id beyond this bound is not a real host identifier. */
16
+ export declare const MAX_SESSION_ID_LENGTH = 128;
17
+ export interface SessionModeRecord {
18
+ readonly sessionID: string;
19
+ readonly mode: OperatingMode;
20
+ readonly updatedAt: number;
21
+ }
22
+ export interface SessionModesPayload {
23
+ readonly version: typeof SESSION_MODES_VERSION;
24
+ readonly records: readonly SessionModeRecord[];
25
+ }
26
+ /**
27
+ * A successful override write. Mirrors the settings patch-result convention:
28
+ * validation failures are per-field errors, internal failures throw (the
29
+ * RPC handler degrades them fail-open).
30
+ */
31
+ export type SessionModeSetResult = {
32
+ readonly ok: true;
33
+ } | {
34
+ readonly ok: false;
35
+ readonly errors: readonly string[];
36
+ };
37
+ /** Storage seam — the adapter binds `SESSION_MODES_KEY` to `ctx.storage`. */
38
+ export interface SessionModesStorage {
39
+ read(): Promise<unknown>;
40
+ write(payload: SessionModesPayload): Promise<void>;
41
+ }
@@ -0,0 +1,7 @@
1
+ export const SESSION_MODES_KEY = "coool.adaptive/session-modes";
2
+ export const SESSION_MODES_VERSION = 1;
3
+ /** Product bound — not a user knob. Keeps the most recently updated records. */
4
+ export const MAX_SESSION_MODE_RECORDS = 512;
5
+ /** A session id beyond this bound is not a real host identifier. */
6
+ export const MAX_SESSION_ID_LENGTH = 128;
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/state/session-modes/types.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,iBAAiB,GAAG,8BAA8B,CAAA;AAC/D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAA;AACtC,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAC3C,oEAAoE;AACpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAA","sourcesContent":["/**\n * Persisted per-session operating-mode overrides (ADP-054). The operating mode\n * is otherwise a global setting, but supervision decisions are made per\n * session — the override lets one session run under a different mode without\n * touching the global default. Records persist through the storage seam like\n * every other state: the host's TUI-mode plugin reconciliation can serve RPC\n * from an instance whose event-pump hooks live on another instance, and a\n * restart must not silently drop explicit user intent.\n */\nimport type { OperatingMode } from \"../../core/scheduler/types.js\"\n\nexport const SESSION_MODES_KEY = \"coool.adaptive/session-modes\"\nexport const SESSION_MODES_VERSION = 1\n/** Product bound — not a user knob. Keeps the most recently updated records. */\nexport const MAX_SESSION_MODE_RECORDS = 512\n/** A session id beyond this bound is not a real host identifier. */\nexport const MAX_SESSION_ID_LENGTH = 128\n\nexport interface SessionModeRecord {\n readonly sessionID: string\n readonly mode: OperatingMode\n readonly updatedAt: number\n}\n\nexport interface SessionModesPayload {\n readonly version: typeof SESSION_MODES_VERSION\n readonly records: readonly SessionModeRecord[]\n}\n\n/**\n * A successful override write. Mirrors the settings patch-result convention:\n * validation failures are per-field errors, internal failures throw (the\n * RPC handler degrades them fail-open).\n */\nexport type SessionModeSetResult =\n { readonly ok: true } | { readonly ok: false; readonly errors: readonly string[] }\n\n/** Storage seam — the adapter binds `SESSION_MODES_KEY` to `ctx.storage`. */\nexport interface SessionModesStorage {\n read(): Promise<unknown>\n write(payload: SessionModesPayload): Promise<void>\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { defaultSettings, applyPatch, migrateSettings, parseAgentTiers, parseMode, parsePin, parseTierLists, } from "./parse.js";
2
+ export { SettingsStore } from "./store.js";
3
+ export { MAX_AGENT_TIERS, MAX_TIER_RESOURCES, SETTINGS_KEY, SETTINGS_VERSION } from "./types.js";
4
+ export type { PatchResult, SettingsPatch, SettingsStorage, SettingsV1 } from "./types.js";
@@ -0,0 +1,4 @@
1
+ export { defaultSettings, applyPatch, migrateSettings, parseAgentTiers, parseMode, parsePin, parseTierLists, } from "./parse.js";
2
+ export { SettingsStore } from "./store.js";
3
+ export { MAX_AGENT_TIERS, MAX_TIER_RESOURCES, SETTINGS_KEY, SETTINGS_VERSION } from "./types.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/state/settings/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,SAAS,EACT,QAAQ,EACR,cAAc,GACf,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA","sourcesContent":["export {\n defaultSettings,\n applyPatch,\n migrateSettings,\n parseAgentTiers,\n parseMode,\n parsePin,\n parseTierLists,\n} from \"./parse.js\"\nexport { SettingsStore } from \"./store.js\"\nexport { MAX_AGENT_TIERS, MAX_TIER_RESOURCES, SETTINGS_KEY, SETTINGS_VERSION } from \"./types.js\"\nexport type { PatchResult, SettingsPatch, SettingsStorage, SettingsV1 } from \"./types.js\"\n"]}
@@ -0,0 +1,33 @@
1
+ import type { OperatingMode } from "../../core/scheduler/types.js";
2
+ import type { TierName } from "../../core/tier-policy/types.js";
3
+ import type { ModelRef } from "../../core/work-unit/types.js";
4
+ import type { SettingsV1 } from "./types.js";
5
+ export declare function parseMode(raw: unknown): OperatingMode | undefined;
6
+ export declare function parseTierLists(raw: unknown): Readonly<Record<TierName, readonly ModelRef[]>> | undefined;
7
+ export declare function parseModelRef(raw: unknown): ModelRef | undefined;
8
+ export declare function parsePin(raw: unknown): ModelRef | null | undefined;
9
+ export declare function parseAgentTiers(raw: unknown): Readonly<Record<string, TierName>> | undefined;
10
+ /** Settings with nothing configured: OBSERVE, empty tiers, no pin (ADP-029/032). */
11
+ export declare function defaultSettings(): SettingsV1;
12
+ /**
13
+ * Migration-on-read. `undefined`/absent record → defaults (first install);
14
+ * versionless (v0 draft) and v1 records are validated field-wise with
15
+ * defaults for unusable fields; a record written by a NEWER version is
16
+ * returned as defaults rather than guessed at (and is only re-persisted once
17
+ * the user changes something, which stamps the current version).
18
+ */
19
+ export declare function migrateSettings(raw: unknown): SettingsV1;
20
+ /**
21
+ * Validate a patch against current settings. The patch is WIRE input (unknown
22
+ * by contract — surfaces send untrusted JSON over RPC; the SettingsPatch type
23
+ * documents the expected shape, the field parsers enforce it). Invalid fields
24
+ * collect error strings (at most one per field, five fields); a valid patch
25
+ * produces a NEW settings object (inputs are never mutated).
26
+ */
27
+ export declare function applyPatch(current: SettingsV1, patch: unknown): {
28
+ ok: true;
29
+ settings: SettingsV1;
30
+ } | {
31
+ ok: false;
32
+ errors: string[];
33
+ };
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Bounded shape validation and migration-on-read (ADP-036). Every parser is
3
+ * total: it accepts `unknown` and either returns a typed value or `undefined`
4
+ * — the read path always falls back to a default rather than throwing, so a
5
+ * corrupted record can never take the plugin (or the host) down (ADP-031).
6
+ *
7
+ * Plain objects are never indexed with untrusted strings beyond own-property
8
+ * checks (`Object.hasOwn`) — prototype-chain keys like "toString" must not be
9
+ * mistaken for data (project-wide audit rule).
10
+ */
11
+ import { OPERATING_MODES } from "../../core/scheduler/types.js";
12
+ import { normalizeTierList } from "../../core/tier-policy/tier-list.js";
13
+ import { TIERS } from "../../core/tier-policy/types.js";
14
+ import { MAX_AGENT_TIERS, MAX_TIER_RESOURCES, SETTINGS_VERSION } from "./types.js";
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ /**
19
+ * Quoted, bounded rendering of an untrusted patch value inside an error
20
+ * message (errors travel to surfaces over RPC; JSON.stringify is only ever
21
+ * called on strings here, so a cyclic object cannot make applyPatch throw).
22
+ */
23
+ function describePatchValue(value) {
24
+ if (typeof value === "string") {
25
+ const quoted = JSON.stringify(value);
26
+ return quoted.length > 64 ? `${quoted.slice(0, 64)}…` : quoted;
27
+ }
28
+ if (value === null)
29
+ return "null";
30
+ if (typeof value === "object")
31
+ return "object";
32
+ if (typeof value === "number" || typeof value === "boolean")
33
+ return String(value);
34
+ return typeof value;
35
+ }
36
+ export function parseMode(raw) {
37
+ if (typeof raw !== "string")
38
+ return undefined;
39
+ return OPERATING_MODES.find((mode) => mode === raw);
40
+ }
41
+ /**
42
+ * Legacy persisted value: v1 settings written before the `control → active`
43
+ * rename store `mode: "control"`. Migration-on-read maps it; patches stay
44
+ * strict (wire input must use the current vocabulary).
45
+ */
46
+ function parseLegacyMode(raw) {
47
+ return raw === "control" ? "active" : parseMode(raw);
48
+ }
49
+ export function parseTierLists(raw) {
50
+ if (!isRecord(raw))
51
+ return undefined;
52
+ const tiers = {};
53
+ for (const tier of TIERS) {
54
+ tiers[tier] = normalizeTierListValue(raw[tier]);
55
+ }
56
+ return tiers;
57
+ }
58
+ /** Shared per-tier normalization (drop invalid, dedupe, cap) for both paths. */
59
+ function normalizeTierListValue(list) {
60
+ return Array.isArray(list)
61
+ ? normalizeTierList(list.map(parseModelRef).filter((ref) => ref !== undefined)).slice(0, MAX_TIER_RESOURCES)
62
+ : [];
63
+ }
64
+ export function parseModelRef(raw) {
65
+ if (!isRecord(raw))
66
+ return undefined;
67
+ const providerID = raw["providerID"];
68
+ const id = raw["id"];
69
+ const variant = raw["variant"];
70
+ if (typeof providerID !== "string" || providerID === "")
71
+ return undefined;
72
+ if (typeof id !== "string" || id === "")
73
+ return undefined;
74
+ return {
75
+ providerID,
76
+ id,
77
+ ...(typeof variant === "string" && variant !== "" ? { variant } : {}),
78
+ };
79
+ }
80
+ export function parsePin(raw) {
81
+ if (raw === null)
82
+ return null;
83
+ return parseModelRef(raw);
84
+ }
85
+ export function parseAgentTiers(raw) {
86
+ if (!isRecord(raw))
87
+ return undefined;
88
+ const entries = [];
89
+ for (const [agent, tier] of Object.entries(raw)) {
90
+ if (agent === "")
91
+ continue;
92
+ const parsed = parseModelessTier(tier);
93
+ if (parsed === undefined)
94
+ continue;
95
+ entries.push([agent, parsed]);
96
+ if (entries.length === MAX_AGENT_TIERS)
97
+ break;
98
+ }
99
+ // `Object.fromEntries` creates own data properties, so even a "__proto__"
100
+ // key from JSON input cannot pollute the prototype here.
101
+ return Object.fromEntries(entries);
102
+ }
103
+ function parseModelessTier(raw) {
104
+ if (typeof raw !== "string")
105
+ return undefined;
106
+ return TIERS.find((tier) => tier === raw);
107
+ }
108
+ /**
109
+ * Patch-path tier-list validation. Unlike the read path (which fills missing
110
+ * or unusable tiers with `[]`), a patch that names `tiers` REPLACES the whole
111
+ * map — silently filling an omitted key with `[]` would wipe that tier's
112
+ * configured resources, so every key must be present and array-typed. List
113
+ * contents still go through the shared normalization (drop invalid, dedupe,
114
+ * cap), which is repair of user-typed entries rather than data loss.
115
+ */
116
+ function parseTierListsPatch(raw) {
117
+ if (!isRecord(raw))
118
+ return { ok: false, error: "invalid tiers" };
119
+ const missing = TIERS.filter((tier) => raw[tier] === undefined);
120
+ if (missing.length > 0) {
121
+ const named = missing.map((tier) => JSON.stringify(tier)).join(", ");
122
+ return { ok: false, error: `invalid tiers: missing ${named}` };
123
+ }
124
+ const tiers = {};
125
+ for (const tier of TIERS) {
126
+ const list = raw[tier];
127
+ if (!Array.isArray(list)) {
128
+ return { ok: false, error: `invalid tiers: ${JSON.stringify(tier)} must be an array` };
129
+ }
130
+ tiers[tier] = normalizeTierListValue(list);
131
+ }
132
+ return { ok: true, value: tiers };
133
+ }
134
+ /**
135
+ * Patch-path agent-tier validation. Unlike the read path (which drops invalid
136
+ * entries as migration-on-read repair), a patch REPLACES the whole map —
137
+ * silently dropping entries would let a single typo wipe the persisted hints,
138
+ * so any invalid entry (and an over-cap patch) is rejected with an error that
139
+ * names the offending field and value.
140
+ */
141
+ function parseAgentTiersPatch(raw) {
142
+ if (!isRecord(raw))
143
+ return { ok: false, error: "invalid agentTiers" };
144
+ const entries = [];
145
+ for (const [agent, tier] of Object.entries(raw)) {
146
+ if (entries.length === MAX_AGENT_TIERS) {
147
+ return {
148
+ ok: false,
149
+ error: `invalid agentTiers: more than ${String(MAX_AGENT_TIERS)} entries`,
150
+ };
151
+ }
152
+ if (agent === "")
153
+ return { ok: false, error: "invalid agentTiers: empty agent name" };
154
+ const parsed = parseModelessTier(tier);
155
+ if (parsed === undefined) {
156
+ return {
157
+ ok: false,
158
+ error: `invalid agentTiers entry ${describePatchValue(agent)}: ${describePatchValue(tier)}`,
159
+ };
160
+ }
161
+ entries.push([agent, parsed]);
162
+ }
163
+ // `Object.fromEntries` creates own data properties, so even a "__proto__"
164
+ // key from JSON input cannot pollute the prototype here.
165
+ return { ok: true, value: Object.fromEntries(entries) };
166
+ }
167
+ /** Settings with nothing configured: OBSERVE, empty tiers, no pin (ADP-029/032). */
168
+ export function defaultSettings() {
169
+ return {
170
+ version: SETTINGS_VERSION,
171
+ mode: "observe",
172
+ tiers: { quick: [], standard: [], advanced: [] },
173
+ pin: null,
174
+ agentTiers: {},
175
+ inviteDismissed: false,
176
+ };
177
+ }
178
+ /**
179
+ * Migration-on-read. `undefined`/absent record → defaults (first install);
180
+ * versionless (v0 draft) and v1 records are validated field-wise with
181
+ * defaults for unusable fields; a record written by a NEWER version is
182
+ * returned as defaults rather than guessed at (and is only re-persisted once
183
+ * the user changes something, which stamps the current version).
184
+ */
185
+ export function migrateSettings(raw) {
186
+ if (!isRecord(raw))
187
+ return defaultSettings();
188
+ const version = raw["version"];
189
+ if (version !== SETTINGS_VERSION && version !== 0 && version !== undefined) {
190
+ return defaultSettings();
191
+ }
192
+ return fieldsFrom(raw);
193
+ }
194
+ function fieldsFrom(raw) {
195
+ const mode = parseLegacyMode(raw["mode"]) ?? "observe";
196
+ const tiers = parseTierLists(raw["tiers"]) ?? defaultSettings().tiers;
197
+ const pin = parsePin(raw["pin"]) ?? null;
198
+ const agentTiers = parseAgentTiers(raw["agentTiers"]) ?? {};
199
+ const inviteDismissed = raw["inviteDismissed"] === true;
200
+ return { version: SETTINGS_VERSION, mode, tiers, pin, agentTiers, inviteDismissed };
201
+ }
202
+ /**
203
+ * Validate a patch against current settings. The patch is WIRE input (unknown
204
+ * by contract — surfaces send untrusted JSON over RPC; the SettingsPatch type
205
+ * documents the expected shape, the field parsers enforce it). Invalid fields
206
+ * collect error strings (at most one per field, five fields); a valid patch
207
+ * produces a NEW settings object (inputs are never mutated).
208
+ */
209
+ export function applyPatch(current, patch) {
210
+ if (!isRecord(patch))
211
+ return { ok: false, errors: ["invalid patch"] };
212
+ const request = patch;
213
+ const errors = [];
214
+ let mode = current.mode;
215
+ let tiers = current.tiers;
216
+ let pin = current.pin;
217
+ let agentTiers = current.agentTiers;
218
+ let inviteDismissed = current.inviteDismissed;
219
+ if (request.mode !== undefined) {
220
+ const parsed = parseMode(request.mode);
221
+ if (parsed === undefined)
222
+ errors.push("invalid mode");
223
+ else
224
+ mode = parsed;
225
+ }
226
+ if (request.tiers !== undefined) {
227
+ const parsed = parseTierListsPatch(request.tiers);
228
+ if (parsed.ok)
229
+ tiers = parsed.value;
230
+ else
231
+ errors.push(parsed.error);
232
+ }
233
+ // `pin === null` is a meaningful clear and must not read as absence.
234
+ if (request.pin !== undefined) {
235
+ const parsed = parsePin(request.pin);
236
+ if (parsed === undefined)
237
+ errors.push("invalid pin");
238
+ else
239
+ pin = parsed;
240
+ }
241
+ if (request.agentTiers !== undefined) {
242
+ const parsed = parseAgentTiersPatch(request.agentTiers);
243
+ if (parsed.ok)
244
+ agentTiers = parsed.value;
245
+ else
246
+ errors.push(parsed.error);
247
+ }
248
+ if (request.inviteDismissed !== undefined) {
249
+ if (typeof request.inviteDismissed !== "boolean")
250
+ errors.push("invalid inviteDismissed");
251
+ else
252
+ inviteDismissed = request.inviteDismissed;
253
+ }
254
+ if (errors.length > 0)
255
+ return { ok: false, errors };
256
+ return {
257
+ ok: true,
258
+ settings: { version: SETTINGS_VERSION, mode, tiers, pin, agentTiers, inviteDismissed },
259
+ };
260
+ }
261
+ //# sourceMappingURL=parse.js.map