@intentic/sandbox-contract 1.312.0 → 1.313.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/README.md +26 -3
  2. package/dist/chores/index.d.ts +0 -1
  3. package/dist/chores/index.d.ts.map +1 -1
  4. package/dist/chores/index.js +0 -1
  5. package/dist/chores/index.js.map +1 -1
  6. package/dist/contracts/accounts.contract.d.ts +57 -0
  7. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.d.ts +436 -0
  9. package/dist/contracts/agent.contract.d.ts.map +1 -1
  10. package/dist/contracts/agent.contract.js +11 -1
  11. package/dist/contracts/agent.contract.js.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +123 -96
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/capabilities.contract.js +1 -1
  15. package/dist/contracts/capabilities.contract.js.map +1 -1
  16. package/dist/contracts/device.contract.d.ts +19 -4
  17. package/dist/contracts/device.contract.d.ts.map +1 -1
  18. package/dist/contracts/device.contract.js +1 -1
  19. package/dist/contracts/device.contract.js.map +1 -1
  20. package/dist/contracts/extensions.contract.d.ts +11 -0
  21. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  22. package/dist/contracts/git.contract.d.ts +2 -0
  23. package/dist/contracts/git.contract.d.ts.map +1 -1
  24. package/dist/contracts/personas.contract.d.ts +2 -0
  25. package/dist/contracts/personas.contract.d.ts.map +1 -1
  26. package/dist/contracts/runner.contract.d.ts +138 -0
  27. package/dist/contracts/runner.contract.d.ts.map +1 -1
  28. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  29. package/dist/contracts/secrets.contract.js +1 -0
  30. package/dist/contracts/secrets.contract.js.map +1 -1
  31. package/dist/contracts/sessions.contract.d.ts +59 -0
  32. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  33. package/dist/contracts/settings.contract.d.ts +12 -8
  34. package/dist/contracts/settings.contract.d.ts.map +1 -1
  35. package/dist/contracts/settings.contract.js +2 -2
  36. package/dist/contracts/settings.contract.js.map +1 -1
  37. package/dist/contracts/system.contract.d.ts +139 -19
  38. package/dist/contracts/system.contract.d.ts.map +1 -1
  39. package/dist/contracts/translator.contract.d.ts +72 -0
  40. package/dist/contracts/translator.contract.d.ts.map +1 -1
  41. package/dist/contracts/workspace.contract.d.ts +85 -14
  42. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  43. package/dist/documents/index.d.ts +1 -0
  44. package/dist/documents/index.d.ts.map +1 -1
  45. package/dist/documents/index.js +1 -0
  46. package/dist/documents/index.js.map +1 -1
  47. package/dist/documents/read.d.ts +24 -0
  48. package/dist/documents/read.d.ts.map +1 -0
  49. package/dist/documents/read.js +81 -0
  50. package/dist/documents/read.js.map +1 -0
  51. package/dist/events/agent-events.d.ts +235 -0
  52. package/dist/events/agent-events.d.ts.map +1 -1
  53. package/dist/events/agent-events.js +3 -0
  54. package/dist/events/agent-events.js.map +1 -1
  55. package/dist/events/child-run.d.ts +9 -0
  56. package/dist/events/child-run.d.ts.map +1 -0
  57. package/dist/events/child-run.js +41 -0
  58. package/dist/events/child-run.js.map +1 -0
  59. package/dist/events/errands.d.ts +5 -0
  60. package/dist/events/errands.d.ts.map +1 -0
  61. package/dist/events/errands.js +19 -0
  62. package/dist/events/errands.js.map +1 -0
  63. package/dist/events/requests.d.ts +152 -0
  64. package/dist/events/requests.d.ts.map +1 -1
  65. package/dist/events/requests.js +26 -1
  66. package/dist/events/requests.js.map +1 -1
  67. package/dist/events/system-events.d.ts +8 -12
  68. package/dist/events/system-events.d.ts.map +1 -1
  69. package/dist/events/transcript.d.ts +387 -0
  70. package/dist/events/transcript.d.ts.map +1 -1
  71. package/dist/events/transcript.js +3 -0
  72. package/dist/events/transcript.js.map +1 -1
  73. package/dist/front/browser-wire.d.ts +6 -0
  74. package/dist/front/browser-wire.d.ts.map +1 -0
  75. package/dist/front/browser-wire.js +4 -0
  76. package/dist/front/browser-wire.js.map +1 -0
  77. package/dist/front/front-wire.d.ts +6 -0
  78. package/dist/front/front-wire.d.ts.map +1 -0
  79. package/dist/front/front-wire.js +5 -0
  80. package/dist/front/front-wire.js.map +1 -0
  81. package/dist/front/generated/browser-wire.d.ts +19 -0
  82. package/dist/front/generated/browser-wire.d.ts.map +1 -0
  83. package/dist/front/generated/browser-wire.js +2 -0
  84. package/dist/front/generated/browser-wire.js.map +1 -0
  85. package/dist/front/generated/wire.d.ts +36 -30
  86. package/dist/front/generated/wire.d.ts.map +1 -1
  87. package/dist/ids/conversation-ids.d.ts +1 -0
  88. package/dist/ids/conversation-ids.d.ts.map +1 -1
  89. package/dist/ids/conversation-ids.js +2 -2
  90. package/dist/ids/conversation-ids.js.map +1 -1
  91. package/dist/ids/session-names.d.ts +3 -0
  92. package/dist/ids/session-names.d.ts.map +1 -1
  93. package/dist/ids/session-names.js +3 -0
  94. package/dist/ids/session-names.js.map +1 -1
  95. package/dist/index.d.ts +1036 -168
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +8 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/models/agent-catalog.d.ts.map +1 -1
  100. package/dist/models/agent-catalog.js +4 -6
  101. package/dist/models/agent-catalog.js.map +1 -1
  102. package/dist/models/agent-runtimes.d.ts +2 -1
  103. package/dist/models/agent-runtimes.d.ts.map +1 -1
  104. package/dist/models/agent-runtimes.js +7 -1
  105. package/dist/models/agent-runtimes.js.map +1 -1
  106. package/dist/models/plan-pools.d.ts +22 -1
  107. package/dist/models/plan-pools.d.ts.map +1 -1
  108. package/dist/models/plan-pools.js +86 -0
  109. package/dist/models/plan-pools.js.map +1 -1
  110. package/dist/policy/request-status.d.ts +1 -1
  111. package/dist/policy/request-status.d.ts.map +1 -1
  112. package/dist/policy/request-status.js +4 -11
  113. package/dist/policy/request-status.js.map +1 -1
  114. package/dist/policy/reserved-servers.d.ts.map +1 -1
  115. package/dist/policy/reserved-servers.js +2 -1
  116. package/dist/policy/reserved-servers.js.map +1 -1
  117. package/dist/policy/turned-away.d.ts.map +1 -1
  118. package/dist/policy/turned-away.js +1 -0
  119. package/dist/policy/turned-away.js.map +1 -1
  120. package/dist/protocol/edge-verdict.d.ts +2 -1
  121. package/dist/protocol/edge-verdict.d.ts.map +1 -1
  122. package/dist/protocol/edge-verdict.js +1 -1
  123. package/dist/protocol/edge-verdict.js.map +1 -1
  124. package/dist/protocol/ingress-contract.d.ts +1 -1
  125. package/dist/protocol/ingress-contract.d.ts.map +1 -1
  126. package/dist/protocol/ingress-contract.js +1 -1
  127. package/dist/protocol/ingress-contract.js.map +1 -1
  128. package/dist/protocol/raw-routes.d.ts +18 -40
  129. package/dist/protocol/raw-routes.d.ts.map +1 -1
  130. package/dist/protocol/raw-routes.js +5 -16
  131. package/dist/protocol/raw-routes.js.map +1 -1
  132. package/dist/protocol/route-meta.d.ts +20 -0
  133. package/dist/protocol/route-meta.d.ts.map +1 -1
  134. package/dist/protocol/route-meta.js +16 -0
  135. package/dist/protocol/route-meta.js.map +1 -1
  136. package/dist/protocol/tunnel-bulk.d.ts +2 -0
  137. package/dist/protocol/tunnel-bulk.d.ts.map +1 -0
  138. package/dist/protocol/tunnel-bulk.js +3 -0
  139. package/dist/protocol/tunnel-bulk.js.map +1 -0
  140. package/dist/protocol/x402.d.ts +17 -0
  141. package/dist/protocol/x402.d.ts.map +1 -0
  142. package/dist/protocol/x402.js +37 -0
  143. package/dist/protocol/x402.js.map +1 -0
  144. package/dist/schemas/agent.d.ts +25 -0
  145. package/dist/schemas/agent.d.ts.map +1 -1
  146. package/dist/schemas/agent.js +14 -2
  147. package/dist/schemas/agent.js.map +1 -1
  148. package/dist/schemas/agents.d.ts +17 -18
  149. package/dist/schemas/agents.d.ts.map +1 -1
  150. package/dist/schemas/agents.js +16 -3
  151. package/dist/schemas/agents.js.map +1 -1
  152. package/dist/schemas/automations.d.ts +4 -6
  153. package/dist/schemas/automations.d.ts.map +1 -1
  154. package/dist/schemas/automations.js.map +1 -1
  155. package/dist/schemas/devices.d.ts +132 -7
  156. package/dist/schemas/devices.d.ts.map +1 -1
  157. package/dist/schemas/devices.js +43 -18
  158. package/dist/schemas/devices.js.map +1 -1
  159. package/dist/schemas/extension-updates.d.ts +22 -0
  160. package/dist/schemas/extension-updates.d.ts.map +1 -1
  161. package/dist/schemas/extension-updates.js +4 -0
  162. package/dist/schemas/extension-updates.js.map +1 -1
  163. package/dist/schemas/git/git.d.ts +3 -0
  164. package/dist/schemas/git/git.d.ts.map +1 -1
  165. package/dist/schemas/hosts.d.ts +29 -5
  166. package/dist/schemas/hosts.d.ts.map +1 -1
  167. package/dist/schemas/hosts.js +20 -7
  168. package/dist/schemas/hosts.js.map +1 -1
  169. package/dist/schemas/keep-warm.d.ts +2 -12
  170. package/dist/schemas/keep-warm.d.ts.map +1 -1
  171. package/dist/schemas/keep-warm.js +8 -10
  172. package/dist/schemas/keep-warm.js.map +1 -1
  173. package/dist/schemas/metrics.d.ts +22 -0
  174. package/dist/schemas/metrics.d.ts.map +1 -1
  175. package/dist/schemas/metrics.js +18 -3
  176. package/dist/schemas/metrics.js.map +1 -1
  177. package/dist/schemas/personas.d.ts +3 -0
  178. package/dist/schemas/personas.d.ts.map +1 -1
  179. package/dist/schemas/personas.js +9 -0
  180. package/dist/schemas/personas.js.map +1 -1
  181. package/dist/schemas/providers/plan-limits.d.ts +140 -0
  182. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  183. package/dist/schemas/providers/plan-limits.js +47 -1
  184. package/dist/schemas/providers/plan-limits.js.map +1 -1
  185. package/dist/schemas/providers/provider-oauth.d.ts +57 -0
  186. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  187. package/dist/schemas/providers/provider-oauth.js +6 -1
  188. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  189. package/dist/schemas/settings-history.d.ts +28 -1
  190. package/dist/schemas/settings-history.d.ts.map +1 -1
  191. package/dist/schemas/settings-history.js +35 -1
  192. package/dist/schemas/settings-history.js.map +1 -1
  193. package/dist/schemas/settings.d.ts +184 -4
  194. package/dist/schemas/settings.d.ts.map +1 -1
  195. package/dist/schemas/settings.js +31 -23
  196. package/dist/schemas/settings.js.map +1 -1
  197. package/dist/schemas/speaker.d.ts +36 -0
  198. package/dist/schemas/speaker.d.ts.map +1 -0
  199. package/dist/schemas/speaker.js +62 -0
  200. package/dist/schemas/speaker.js.map +1 -0
  201. package/dist/schemas/state-plan.d.ts +46 -0
  202. package/dist/schemas/state-plan.d.ts.map +1 -0
  203. package/dist/schemas/state-plan.js +31 -0
  204. package/dist/schemas/state-plan.js.map +1 -0
  205. package/dist/schemas/system.d.ts +48 -8
  206. package/dist/schemas/system.d.ts.map +1 -1
  207. package/dist/schemas/system.js +10 -14
  208. package/dist/schemas/system.js.map +1 -1
  209. package/dist/schemas/workspace/mainline.d.ts +206 -64
  210. package/dist/schemas/workspace/mainline.d.ts.map +1 -1
  211. package/dist/schemas/workspace/mainline.js +73 -25
  212. package/dist/schemas/workspace/mainline.js.map +1 -1
  213. package/dist/state/contract-lock.d.ts +1 -0
  214. package/dist/state/contract-lock.d.ts.map +1 -1
  215. package/dist/state/contract-lock.js +47 -1
  216. package/dist/state/contract-lock.js.map +1 -1
  217. package/dist/state/definition.d.ts +24 -16
  218. package/dist/state/definition.d.ts.map +1 -1
  219. package/dist/state/fix-stance.d.ts +1 -1
  220. package/dist/state/fix-stance.d.ts.map +1 -1
  221. package/dist/state/fix-stance.js +2 -1
  222. package/dist/state/fix-stance.js.map +1 -1
  223. package/dist/state/history-state.d.ts.map +1 -1
  224. package/dist/state/history-state.js +2 -0
  225. package/dist/state/history-state.js.map +1 -1
  226. package/dist/state/landed-commit.d.ts +1 -0
  227. package/dist/state/landed-commit.d.ts.map +1 -1
  228. package/dist/state/landed-commit.js +2 -0
  229. package/dist/state/landed-commit.js.map +1 -1
  230. package/dist/state/red-streak.d.ts +7 -0
  231. package/dist/state/red-streak.d.ts.map +1 -0
  232. package/dist/state/red-streak.js +11 -0
  233. package/dist/state/red-streak.js.map +1 -0
  234. package/dist/state/workspace-state.d.ts +1 -0
  235. package/dist/state/workspace-state.d.ts.map +1 -1
  236. package/dist/state/workspace-state.js +6 -2
  237. package/dist/state/workspace-state.js.map +1 -1
  238. package/dist/text/shell-regions.d.ts +1 -0
  239. package/dist/text/shell-regions.d.ts.map +1 -1
  240. package/dist/text/shell-regions.js +1 -1
  241. package/dist/text/shell-regions.js.map +1 -1
  242. package/dist/text/transcript-fold.d.ts.map +1 -1
  243. package/dist/text/transcript-fold.js +14 -7
  244. package/dist/text/transcript-fold.js.map +1 -1
  245. package/package.json +28 -29
  246. package/src/chores/index.ts +0 -1
  247. package/src/contracts/agent.contract.ts +14 -0
  248. package/src/contracts/capabilities.contract.ts +2 -2
  249. package/src/contracts/device.contract.ts +5 -3
  250. package/src/contracts/secrets.contract.ts +2 -0
  251. package/src/contracts/settings.contract.ts +2 -1
  252. package/src/documents/conversions.ts +3 -3
  253. package/src/documents/index.ts +3 -1
  254. package/src/documents/read.ts +114 -0
  255. package/src/events/agent-events.ts +5 -0
  256. package/src/events/child-run.test.ts +81 -0
  257. package/src/events/child-run.ts +73 -0
  258. package/src/events/errands.test.ts +22 -0
  259. package/src/events/errands.ts +27 -0
  260. package/src/events/requests.ts +40 -1
  261. package/src/events/transcript.ts +8 -0
  262. package/src/front/browser-wire.ts +20 -0
  263. package/src/front/front-wire.ts +15 -0
  264. package/src/front/generated/browser-wire.json +138 -0
  265. package/src/front/generated/browser-wire.ts +16 -0
  266. package/src/front/generated/front-wire.json +793 -0
  267. package/src/front/generated/tunnel.json +38 -0
  268. package/src/front/generated/wire.ts +21 -24
  269. package/src/front/wire-manifests.test.ts +61 -0
  270. package/src/ids/conversation-ids.ts +6 -5
  271. package/src/ids/session-names.ts +7 -0
  272. package/src/index.ts +8 -1
  273. package/src/models/agent-catalog.test.ts +6 -4
  274. package/src/models/agent-catalog.ts +4 -6
  275. package/src/models/agent-runtimes.ts +22 -7
  276. package/src/models/capability-ledger.test.ts +2 -0
  277. package/src/models/plan-pools.test.ts +121 -2
  278. package/src/models/plan-pools.ts +152 -2
  279. package/src/policy/request-status.ts +4 -12
  280. package/src/policy/reserved-servers.test.ts +6 -0
  281. package/src/policy/reserved-servers.ts +13 -10
  282. package/src/policy/turned-away.ts +3 -0
  283. package/src/protocol/edge-verdict.ts +13 -11
  284. package/src/protocol/ingress-contract.fixture.json +13 -0
  285. package/src/protocol/ingress-contract.ts +9 -5
  286. package/src/protocol/raw-routes.ts +10 -21
  287. package/src/protocol/route-meta.ts +36 -1
  288. package/src/protocol/tunnel-bulk.test.ts +37 -0
  289. package/src/protocol/tunnel-bulk.ts +7 -0
  290. package/src/protocol/x402.test.ts +29 -0
  291. package/src/protocol/x402.ts +61 -0
  292. package/src/schemas/agent.ts +28 -3
  293. package/src/schemas/agents.ts +32 -6
  294. package/src/schemas/automations.ts +4 -2
  295. package/src/schemas/devices.ts +106 -39
  296. package/src/schemas/extension-updates.ts +6 -0
  297. package/src/schemas/hosts.ts +62 -19
  298. package/src/schemas/ic-shape-args.test.ts +64 -0
  299. package/src/schemas/keep-warm.test.ts +6 -13
  300. package/src/schemas/keep-warm.ts +15 -20
  301. package/src/schemas/metrics.ts +26 -3
  302. package/src/schemas/personas.ts +15 -0
  303. package/src/schemas/providers/plan-limits.ts +71 -1
  304. package/src/schemas/providers/provider-oauth.ts +10 -1
  305. package/src/schemas/settings-history.ts +50 -3
  306. package/src/schemas/settings.test.ts +54 -1
  307. package/src/schemas/settings.ts +49 -31
  308. package/src/schemas/speaker.ts +89 -0
  309. package/src/schemas/state-plan.test.ts +69 -0
  310. package/src/schemas/state-plan.ts +47 -0
  311. package/src/schemas/system.ts +20 -16
  312. package/src/schemas/workspace/mainline.test.ts +28 -0
  313. package/src/schemas/workspace/mainline.ts +111 -44
  314. package/src/state/contract-lock.test.ts +65 -4
  315. package/src/state/contract-lock.ts +70 -2
  316. package/src/state/fix-stance.test.ts +8 -0
  317. package/src/state/fix-stance.ts +3 -3
  318. package/src/state/history-state.ts +5 -1
  319. package/src/state/landed-commit.test.ts +12 -1
  320. package/src/state/landed-commit.ts +5 -0
  321. package/src/state/red-streak.test.ts +28 -0
  322. package/src/state/red-streak.ts +28 -0
  323. package/src/state/workspace-state.test.ts +7 -1
  324. package/src/state/workspace-state.ts +10 -2
  325. package/src/text/shell-regions.test.ts +38 -0
  326. package/src/text/shell-regions.ts +3 -3
  327. package/src/text/transcript-fold.test.ts +13 -0
  328. package/src/text/transcript-fold.ts +17 -8
  329. package/dist/chores/fix-deps.d.ts +0 -10
  330. package/dist/chores/fix-deps.d.ts.map +0 -1
  331. package/dist/chores/fix-deps.js +0 -19
  332. package/dist/chores/fix-deps.js.map +0 -1
  333. package/dist/front/terminal-frames.d.ts +0 -17
  334. package/dist/front/terminal-frames.d.ts.map +0 -1
  335. package/dist/front/terminal-frames.js +0 -79
  336. package/dist/front/terminal-frames.js.map +0 -1
  337. package/dist/protocol/tunnel-lanes.d.ts +0 -10
  338. package/dist/protocol/tunnel-lanes.d.ts.map +0 -1
  339. package/dist/protocol/tunnel-lanes.js +0 -16
  340. package/dist/protocol/tunnel-lanes.js.map +0 -1
  341. package/dist/protocol/upgrade-relay.d.ts +0 -4
  342. package/dist/protocol/upgrade-relay.d.ts.map +0 -1
  343. package/dist/protocol/upgrade-relay.js +0 -25
  344. package/dist/protocol/upgrade-relay.js.map +0 -1
  345. package/src/chores/fix-deps.ts +0 -23
  346. package/src/front/terminal-frames.fixture.json +0 -55
  347. package/src/front/terminal-frames.test.ts +0 -63
  348. package/src/front/terminal-frames.ts +0 -102
  349. package/src/protocol/tunnel-lanes.json +0 -136
  350. package/src/protocol/tunnel-lanes.test.ts +0 -30
  351. package/src/protocol/tunnel-lanes.ts +0 -32
  352. package/src/protocol/upgrade-relay.ts +0 -28
@@ -0,0 +1,38 @@
1
+ {
2
+ "close": {
3
+ "away": 1001,
4
+ "displaced": 4001,
5
+ "refused": 4003
6
+ },
7
+ "headers": {
8
+ "bulk": "x-intentic-bulk",
9
+ "grant": "x-intentic-grant",
10
+ "lane": "x-intentic-lane",
11
+ "transports": "x-intentic-transports"
12
+ },
13
+ "lanes": [
14
+ "interactive",
15
+ "bulk"
16
+ ],
17
+ "liveness": {
18
+ "deadAfterMs": 45000,
19
+ "pingEveryMs": 15000
20
+ },
21
+ "mux": "yamux",
22
+ "path": "/tunnel/v2",
23
+ "quic": {
24
+ "alpn": "intentic-tunnel/1",
25
+ "hello": {
26
+ "gone": 2,
27
+ "held": 0,
28
+ "refused": 1
29
+ },
30
+ "yieldAfterBytes": 1048576
31
+ },
32
+ "transports": [
33
+ "quic",
34
+ "h3",
35
+ "webtransport"
36
+ ],
37
+ "upgrade": "websocket"
38
+ }
@@ -16,7 +16,9 @@ export type Endpoint = { host: string, port: number, };
16
16
  /**
17
17
  * Everything Node sends the front.
18
18
  */
19
- export type FromNode = { "kind": "hello", build: string, pid: number, } | { "kind": "listen", config: ListenConfig, } | { "kind": "certificate", certificate?: Certificate, } | { "kind": "tunnel", tunnel?: TunnelConfig, } | { "kind": "answer", id: number, answer: Answer, } | { "kind": "refused", id: number, message: string, } | { "kind": "revoke", member?: string, } | { "kind": "watch", checkout: WatchedCheckout, } | { "kind": "unwatch", dir: string, } | { "kind": "sync", id: number, dirs: Array<string>, };
19
+ export type FromNode = { "kind": "hello", build: string, pid: number, } | { "kind": "listen", config: ListenConfig, } | { "kind": "certificate", certificate?: Certificate, } | { "kind": "tunnel", tunnel?: TunnelConfig, } | { "kind": "ask", id: number, question: FrontQuestion, } | { "kind": "answer", id: number, answer: Answer, } | { "kind": "refused", id: number, message: string, } | { "kind": "revoke", member?: string, } | { "kind": "watch", checkout: WatchedCheckout, } | { "kind": "unwatch", dir: string, };
20
+
21
+ export type FrontAnswer = { "answer": "sync", generations: Array<number | null>, };
20
22
 
21
23
  /**
22
24
  * Request headers only the front sets on what it forwards to Node; it strips them from everything arriving outside.
@@ -24,6 +26,11 @@ export type FromNode = { "kind": "hello", build: string, pid: number, } | { "kin
24
26
  */
25
27
  export type FrontHeader = "x-intentic-preview" | "x-intentic-preview-unreachable";
26
28
 
29
+ /**
30
+ * A question Node asks the front.
31
+ */
32
+ export type FrontQuestion = { "question": "sync", dirs: Array<string>, };
33
+
27
34
  /**
28
35
  * Every port the front binds, as Node's config names them; an absent one is not bound.
29
36
  */
@@ -42,47 +49,37 @@ frameAncestors: Array<string>,
42
49
  previewProbePath: string, };
43
50
 
44
51
  /**
45
- * Where a preview host's requests go, as Node resolved it.
46
- */
47
- export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "node" };
48
-
49
- /**
50
- * A question the front asks Node; the answer carries the same id.
52
+ * An answer Node renders whole when asked, for the front to write as it stands: a refusal page, or the probe's report.
51
53
  */
52
- export type Question = { "question": "preview", host: string, } | { "question": "terminal", query: string, };
53
-
54
- export type Scheme = "http" | "https";
54
+ export type Page = { status: number, headers: { [key in string]: string }, body: string, };
55
55
 
56
56
  /**
57
- * What the browser sends on a terminal socket, each a JSON text frame.
57
+ * What becomes of a preview host's request, decided by Node once, when asked.
58
58
  */
59
- export type TerminalClientMessage = { "type": "input", data: string, } | { "type": "resize", cols: number, rows: number, } | { "type": "ping" };
59
+ export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "page", page: Page, } | { "to": "outbox" };
60
60
 
61
61
  /**
62
- * What a terminal socket opens onto, as Node decided from its ticket, session name and working directory.
62
+ * A question the front asks Node.
63
63
  */
64
- export type TerminalPlan = { "plan": "tmux", session: string, argv: Array<string>, } | { "plan": "tail", path: string, } | { "plan": "exit", code: number, reason: string, } | { "plan": "refused", code: number, reason: string, };
64
+ export type Question = { "question": "preview", host: string, probe: boolean, } | { "question": "terminal", query: string, };
65
65
 
66
- /**
67
- * What a terminal socket sends as JSON text; the pane's own bytes travel as binary frames.
68
- */
69
- export type TerminalServerMessage = { "type": "exit", code: number, reason?: string, } | { "type": "pong" };
66
+ export type Scheme = "http" | "https";
70
67
 
71
68
  /**
72
- * The Upgrade a terminal opens with where no WebSocket rides the stream, a WebTransport one: its messages then travel
73
- * as frames (`FrameKind`). Exported as a literal type, as `FrontHeader` is.
69
+ * What a terminal socket opens onto, as Node decided from its ticket, session name and working directory.
74
70
  */
75
- export type TerminalUpgrade = "intentic-terminal";
71
+ export type TerminalPlan = { "plan": "session", name: string, createIn?: string, } | { "plan": "tail", path: string, } | { "plan": "exit", code: number, reason: string, } | { "plan": "refused", code: number, reason: string, };
76
72
 
77
73
  /**
78
74
  * Everything the front sends Node.
79
75
  */
80
- export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "tunnel", connected: boolean, } | { "kind": "synced", id: number, generations: Array<number | null>, };
76
+ export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "answer", id: number, answer: FrontAnswer, } | { "kind": "refused", id: number, message: string, } | { "kind": "tunnel", connected: boolean, };
81
77
 
82
78
  /**
83
- * The ingress tunnel's door and the reachability grant it presents there.
79
+ * The ingress tunnel's door, the reachability grant it presents there, and the daemon's transfer routes it announces
80
+ * (`METHOD /path` each, as the daemon names them), which the edge sends down the bulk socket.
84
81
  */
85
- export type TunnelConfig = { url: string, grant: string, };
82
+ export type TunnelConfig = { url: string, grant: string, bulk: Array<string>, };
86
83
 
87
84
  /**
88
85
  * A local upstream a preview host relays to.
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { EDGE_VERDICT_HEADER, edgeVerdictOf } from "../protocol/edge-verdict.js";
3
+ import { INGRESS_GRANT_HEADER, INGRESS_TUNNEL_PATH } from "../protocol/ingress-contract.js";
4
+ import { EDGE_TRANSPORTS, TERMINAL_PATH, WEBTRANSPORT_PATH } from "./browser-wire.js";
5
+ import { ASK_PATIENCE_MS, FRAME_LENGTH_BYTES, FRONT_SOCKET_ENV, NODE_SOCKET_ENV } from "./front-wire.js";
6
+
7
+ // The manifests `cargo test` writes from the Rust crates that define this wire: every value TypeScript restates is read
8
+ // back against them, so the two languages cannot drift and contract.lock.json pins what both agree on.
9
+ const manifest = (name: string): Record<string, unknown> =>
10
+ JSON.parse(readFileSync(new URL(`./generated/${name}.json`, import.meta.url), "utf8")) as Record<string, unknown>;
11
+
12
+ const tunnel = manifest("tunnel") as {
13
+ path: string;
14
+ headers: { grant: string; transports: string };
15
+ transports: string[];
16
+ };
17
+ // SAFETY: front-wire's own test writes this manifest with exactly these keys, and the lock pins them.
18
+ const socket = manifest("front-wire") as {
19
+ env: { frontSocket: string; nodeSocket: string };
20
+ askPatienceMs: number;
21
+ frame: { lengthBytes: number };
22
+ };
23
+ const browser = manifest("browser-wire") as {
24
+ edge: { verdictHeader: string; verdicts: { oneOf: { const: string }[] } };
25
+ terminal: { path: string; upgrade: string };
26
+ webTransport: { path: string };
27
+ };
28
+
29
+ describe("the wire outside oRPC, as the Rust crates define it", () => {
30
+ it("names the tunnel door and its headers as the tunnel crate does", () => {
31
+ expect({ path: INGRESS_TUNNEL_PATH, grant: INGRESS_GRANT_HEADER }).toEqual({
32
+ path: tunnel.path,
33
+ grant: tunnel.headers.grant,
34
+ });
35
+ });
36
+
37
+ it("names the control socket's env vars, its patience and its framing as front-wire does", () => {
38
+ expect({ front: FRONT_SOCKET_ENV, node: NODE_SOCKET_ENV, patience: ASK_PATIENCE_MS, length: FRAME_LENGTH_BYTES }).toEqual({
39
+ front: socket.env.frontSocket,
40
+ node: socket.env.nodeSocket,
41
+ patience: socket.askPatienceMs,
42
+ length: socket.frame.lengthBytes,
43
+ });
44
+ });
45
+
46
+ it("declares exactly the transports an edge may", () => {
47
+ expect<readonly string[]>(EDGE_TRANSPORTS).toEqual(tunnel.transports);
48
+ });
49
+
50
+ it("reads the edge's verdicts and the browser's paths as browser-wire writes them", () => {
51
+ const verdicts = browser.edge.verdicts.oneOf.map((verdict) => verdict.const);
52
+ expect(verdicts).toEqual(["no-tunnel", "unknown-sandbox", "dropped"]);
53
+ expect<readonly (string | undefined)[]>(verdicts.map((verdict) => edgeVerdictOf(verdict))).toEqual(verdicts);
54
+ expect({ header: EDGE_VERDICT_HEADER, terminal: TERMINAL_PATH, session: WEBTRANSPORT_PATH, upgrade: "websocket" }).toEqual({
55
+ header: browser.edge.verdictHeader,
56
+ terminal: browser.terminal.path,
57
+ session: browser.webTransport.path,
58
+ upgrade: browser.terminal.upgrade,
59
+ });
60
+ });
61
+ });
@@ -160,15 +160,16 @@ const repoSlug = (repo: string): string =>
160
160
  .replace(/^-+|-+$/g, "") || "repo";
161
161
  export const ciFixConversationId = (repo: string, runId: number): string => `${CI_FIX_PREFIX}${repoSlug(repo)}-${runId}`;
162
162
 
163
- // Keyed by which gates failed (fixSignature), not a run id (a push has none); the same gates red again is the same
164
- // failure. Hashed rather than spelled into the branch name; the fix agent's first message names the gates.
163
+ // Keyed by when the project's push red began (pushFixBase, `red:<since>`), not a run id (a push has none): a refused push
164
+ // is owed by that red too (push-checks-store.ts). Hashed rather than spelled into the branch name; the fix agent's first
165
+ // message names what it is for.
165
166
  export const PUSH_FIX_PREFIX = "push-fix-";
166
167
 
167
168
  // FNV-1a, not a cryptographic hash: nothing here is secret, the only requirement is the same failure yields the same
168
- // seven characters in every browser and in node.
169
+ // seven characters in every browser and in node. The one spelling: a push finding's id (push-checks-store.ts) is this too.
169
170
  const FNV_OFFSET = 0x811c_9dc5;
170
171
  const FNV_PRIME = 0x0100_0193;
171
- const digest = (text: string): string => {
172
+ export const fnvDigest = (text: string): string => {
172
173
  let hash = FNV_OFFSET;
173
174
  for (let index = 0; index < text.length; index += 1) {
174
175
  hash = Math.imul(hash ^ text.charCodeAt(index), FNV_PRIME);
@@ -176,7 +177,7 @@ const digest = (text: string): string => {
176
177
  return (hash >>> 0).toString(36).padStart(7, "0");
177
178
  };
178
179
 
179
- export const pushFixConversationId = (scope: string, signature: string): string => `${PUSH_FIX_PREFIX}${repoSlug(scope)}-${digest(signature)}`;
180
+ export const pushFixConversationId = (scope: string, signature: string): string => `${PUSH_FIX_PREFIX}${repoSlug(scope)}-${fnvDigest(signature)}`;
180
181
 
181
182
  // A red main-line check that no conversation still holding the work could take: keyed by the project and the moment its
182
183
  // red streak began, so every attempt at one streak shares a base and a later streak in the same project starts over.
@@ -6,6 +6,8 @@ export const WEB_SESSION_PREFIX = "web-";
6
6
  export const AGENT_SESSION_PREFIX = "agent-";
7
7
  export const JOB_SESSION_PREFIX = "job-";
8
8
  export const BROWSER_SESSION_PREFIX = "browser-";
9
+ // A panel's session, one per managed process; the browser string-builds these names too, so never rename it.
10
+ export const PANEL_SESSION_PREFIX = "panel-";
9
11
 
10
12
  // Eight characters of the SDK session UUID, sanitized to the name-guard charset; groups a turn's work, including its
11
13
  // subagents', under one name. Undefined when it sanitizes to empty.
@@ -21,6 +23,11 @@ export const agentSessionName = (sessionId: string): string | undefined => {
21
23
  return suffix === undefined ? undefined : `${AGENT_SESSION_PREFIX}${suffix}`;
22
24
  };
23
25
 
26
+ // The session a panel keyed `key` runs in, and back; undefined for a name that isn't a panel session at all.
27
+ export const panelSession = (key: string): string => `${PANEL_SESSION_PREFIX}${key}`;
28
+ export const panelKeyOf = (session: string | undefined): string | undefined =>
29
+ session?.startsWith(PANEL_SESSION_PREFIX) === true ? session.slice(PANEL_SESSION_PREFIX.length) : undefined;
30
+
24
31
  // The browser session one SDK session drives. Shares the suffix with agentSessionName on purpose: a conversation's
25
32
  // shell and its browser read as the pair they are.
26
33
  export const browserSessionName = (sessionId: string): string | undefined => {
package/src/index.ts CHANGED
@@ -114,14 +114,17 @@ export * from "./events/transcript.js";
114
114
  export * from "./events/land-breakage.js";
115
115
  export * from "./events/land-conflict.js";
116
116
  export * from "./events/verify-nudge.js";
117
+ export * from "./events/errands.js";
117
118
  export * from "./events/watch-wake.js";
118
119
  export * from "./policy/request-status.js";
120
+ export * from "./events/child-run.js";
119
121
  export * from "./policy/turned-away.js";
120
122
  export * from "./text/mentions.js";
121
123
  export * from "./protocol/sse.js";
122
124
  export * from "./protocol/routes.js";
123
- export type { ControlReach, RouteMeta } from "./protocol/route-meta.js";
125
+ export { type ControlReach, defaultFloor, type RouteAccess, type RouteMeta, routeAccess } from "./protocol/route-meta.js";
124
126
  export { RAW_ROUTE_LIST, RAW_ROUTES, type RawRouteKey, rawRoutePath } from "./protocol/raw-routes.js";
127
+ export { tunnelBulkRoutes } from "./protocol/tunnel-bulk.js";
125
128
  export type { SandboxCallInput, SandboxGroup, SandboxHandlerInput, SandboxHandlerOutput, SandboxProcedure } from "./protocol/procedure-types.js";
126
129
  export * from "./policy/control-scopes.js";
127
130
  // The container's directory layout, re-exported since extensions can't import @intentic/constants directly.
@@ -133,6 +136,7 @@ export * from "./state/history-state.js";
133
136
  export * from "./state/fix-stance.js";
134
137
  export * from "./state/landed-commit.js";
135
138
  export * from "./state/fix-attempt-plan.js";
139
+ export * from "./state/red-streak.js";
136
140
  // Provider vocabulary bottom-up: agent-runtimes, provider-specs, agent-catalog; three modules since the dependency
137
141
  // points one way.
138
142
  export * from "./models/agent-runtimes.js";
@@ -173,6 +177,7 @@ export * from "./schemas/areas.js";
173
177
  export * from "./policy/fence-paths.js";
174
178
  export * from "./policy/persona-home.js";
175
179
  export * from "./schemas/agents.js";
180
+ export * from "./schemas/speaker.js";
176
181
  export * from "./schemas/approvals.js";
177
182
  export * from "./schemas/automations.js";
178
183
  export * from "./schemas/capabilities.js";
@@ -218,6 +223,7 @@ export * from "./schemas/share.js";
218
223
  export * from "./schemas/shared.js";
219
224
  export * from "./schemas/system-prompt.js";
220
225
  export * from "./schemas/system.js";
226
+ export * from "./schemas/state-plan.js";
221
227
  export * from "./schemas/terminal.js";
222
228
  export * from "./schemas/turn-break.js";
223
229
  export * from "./schemas/keep-warm.js";
@@ -238,6 +244,7 @@ export * from "./state/starter.js";
238
244
  export * from "./text/title.js";
239
245
  export * from "./state/versions.js";
240
246
  export * from "./text/model-answer.js";
247
+ export * from "./text/shell-regions.js";
241
248
  export * from "./text/whisper.js";
242
249
  export * from "./text/workflow-faults.js";
243
250
  export * from "./text/emoji.js";
@@ -31,7 +31,7 @@ describe("every provider/harness pair declares what it can do", () => {
31
31
 
32
32
  expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "pi"]).toContain(capabilities.runtime);
33
33
  expect(["modes", "plan"]).toContain(capabilities.permissions);
34
- expect(["full", "tools", "browser", "http", "none"]).toContain(capabilities.mcp);
34
+ expect(["full", "tools", "http", "none"]).toContain(capabilities.mcp);
35
35
  expect(capabilities.execution).toContain("shell");
36
36
  for (const backend of capabilities.execution) {
37
37
  expect(["shell", "js"]).toContain(backend);
@@ -135,8 +135,8 @@ test("the ceiling has nothing to disclose; a floor names what it lacks", () => {
135
135
  expect(acpCaps.terminals).toBe(true);
136
136
 
137
137
  const codexCaps = capabilitiesOf("codex", "native");
138
- expect(codexCaps.mcp).toBe("browser");
139
- expect(limitationsOf(codexCaps).some((line) => line.includes("browser"))).toBe(true);
138
+ expect(codexCaps.mcp).toBe("http");
139
+ expect(limitationsOf(codexCaps)).toContain("MCP servers only, none of the daemon's own tools, no plugins");
140
140
  expect(limitationsOf(codexCaps).length).toBeLessThan(limitationsOf({ ...grokCaps, mcp: "none" }).length + 5);
141
141
  });
142
142
 
@@ -154,13 +154,15 @@ test("every axis a record can lack has words for it", () => {
154
154
  commands: false,
155
155
  terminals: false,
156
156
  recovery: false,
157
+ warm: false,
157
158
  instructions: "none",
158
159
  skillDiscovery: "prompt",
159
160
  rulebook: "none",
160
161
  secrets: "none",
161
162
  };
162
163
 
163
- // Thirteen disclosable axes, thirteen sentences; `fastMode` is the exception, disclosed via fastAllowed.
164
+ // Thirteen disclosable axes, thirteen sentences; `fastMode` is disclosed via fastAllowed, and `warm` (keep-warm, off by
165
+ // default) is no limit on the turn itself.
164
166
  expect(limitationsOf(nothing)).toHaveLength(13);
165
167
  expect(limitationsOf(nothing).join(" ")).not.toContain("fast");
166
168
  });
@@ -116,12 +116,10 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
116
116
  ...(capabilities.mcp === "none"
117
117
  ? ["no MCP tools or plugins"]
118
118
  : capabilities.mcp === "http"
119
- ? ["MCP tools only, no plugins or browser"]
120
- : capabilities.mcp === "browser"
121
- ? ["browser tools only, no other MCP"]
122
- : capabilities.mcp === "tools"
123
- ? ["no plugins"]
124
- : []),
119
+ ? ["MCP servers only, none of the daemon's own tools, no plugins"]
120
+ : capabilities.mcp === "tools"
121
+ ? ["no plugins"]
122
+ : []),
125
123
  ...(capabilities.execution.includes("js") ? [] : ["no code runs, shell only"]),
126
124
  ...(capabilities.effort ? [] : ["no effort control"]),
127
125
  ...(capabilities.commands ? [] : ["no slash commands"]),
@@ -15,8 +15,13 @@ export interface AgentCapabilities {
15
15
  readonly permissions: "modes" | "plan";
16
16
  // Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
17
17
  readonly questions: boolean;
18
- // Which tools reach the agent, full down to none; never rounded up or down from what a runtime has.
19
- readonly mcp: "full" | "tools" | "browser" | "http" | "none";
18
+ // Which tools reach the agent, full down to none; never rounded up or down from what a runtime has. Every runtime
19
+ // but `none` takes the turn's whole remote list (TurnTools.remote: the daemon's MCP door's mounts, browsers
20
+ // included, and the mcp-kind cards), so no granted server reaches one runtime and misses another:
21
+ // full - that list, the daemon's own in-process servers (ui, secrets, watch, …), and plugin checkouts.
22
+ // tools - that list, and the daemon's own tools through the runtime's host callbacks; no plugins.
23
+ // http - that list alone, as http MCP servers.
24
+ readonly mcp: "full" | "tools" | "http" | "none";
20
25
  // Which ways of executing the daemon stands behind here; distinct from `mcp`, which names tools, not execution.
21
26
  readonly execution: readonly ExecutionBackend[];
22
27
  // Reasoning-effort selection is forwarded to the model.
@@ -31,6 +36,9 @@ export interface AgentCapabilities {
31
36
  readonly terminals: boolean;
32
37
  // Fails with the coded frames auto-resume keys off, so a turn the provider killed is re-run once it's back.
33
38
  readonly recovery: boolean;
39
+ // Can hold an idle conversation's prompt cache warm: its provider replays the last turn's prefix as one forked,
40
+ // unsaved request with every tool refused (keep-warm). Only where the provider module also implements `warm`.
41
+ readonly warm: boolean;
34
42
  // How much of the owner's instructions this runtime takes: replace the base, append to it, or no seam at all.
35
43
  readonly instructions: "replace" | "append" | "none";
36
44
  // How the runtime discovers loaded skills: scans its own filesystem, or the daemon names them in the opener.
@@ -61,6 +69,7 @@ export const CLAUDE_CODE: AgentCapabilities = {
61
69
  commands: true,
62
70
  terminals: true,
63
71
  recovery: true,
72
+ warm: true,
64
73
  instructions: "replace",
65
74
  skillDiscovery: "native",
66
75
  // The only runtime with its own pre-execution hook, so the only one where a hold can park instead of refusing.
@@ -68,14 +77,15 @@ export const CLAUDE_CODE: AgentCapabilities = {
68
77
  secrets: "masked",
69
78
  };
70
79
 
71
- // Codex app-server: item-level events, process-backed MCP, and four seams (steer, a question request, the skills list,
72
- // the shared mount namespace). Daemon-side servers, plugins, server approvals stay unwired.
80
+ // Codex app-server: item-level events, the turn's remote MCP servers over http, and four seams (steer, a question
81
+ // request, the skills list, the shared mount namespace). The daemon's in-process servers, plugins and server approvals
82
+ // stay unwired.
73
83
  export const CODEX: AgentCapabilities = {
74
84
  runtime: "codex",
75
85
  steering: true,
76
86
  permissions: "plan",
77
87
  questions: true,
78
- mcp: "browser",
88
+ mcp: "http",
79
89
  execution: ["shell"],
80
90
  effort: true,
81
91
  fastMode: false,
@@ -83,6 +93,7 @@ export const CODEX: AgentCapabilities = {
83
93
  commands: true,
84
94
  terminals: false,
85
95
  recovery: false,
96
+ warm: false,
86
97
  // Both ride the per-thread config: one replaces Codex's base prompt, the other adds a developer message.
87
98
  instructions: "replace",
88
99
  skillDiscovery: "native",
@@ -106,6 +117,7 @@ export const OPENCODE: AgentCapabilities = {
106
117
  commands: false,
107
118
  terminals: false,
108
119
  recovery: false,
120
+ warm: false,
109
121
  // `system` on the prompt body, per message; adds to OpenCode's own prompt, with no seam to replace it.
110
122
  instructions: "append",
111
123
  skillDiscovery: "prompt",
@@ -122,7 +134,7 @@ export const OPENCODE_GEMINI: AgentCapabilities = {
122
134
  };
123
135
 
124
136
  // Any agent speaking the Agent Client Protocol: a documented floor, not the native ceiling. Publishes commands and
125
- // terminals, takes http MCP tools when offered, but owns its own model, effort and permission posture.
137
+ // terminals, takes the turn's remote MCP servers when it advertises http MCP, but owns its own model, effort and permission posture.
126
138
  export const ACP: AgentCapabilities = {
127
139
  runtime: "acp",
128
140
  steering: false,
@@ -136,6 +148,7 @@ export const ACP: AgentCapabilities = {
136
148
  commands: true,
137
149
  terminals: true,
138
150
  recovery: false,
151
+ warm: false,
139
152
  // ACP's calls carry no system field: the agent owns its instructions, the persona note rides the message.
140
153
  instructions: "none",
141
154
  skillDiscovery: "prompt",
@@ -159,6 +172,7 @@ export const PI: AgentCapabilities = {
159
172
  commands: true,
160
173
  terminals: false,
161
174
  recovery: false,
175
+ warm: false,
162
176
  // Pi's RPC sets no standing instructions; like ACP, it hears the persona note through the user message.
163
177
  instructions: "none",
164
178
  skillDiscovery: "prompt",
@@ -177,7 +191,7 @@ export const CURSOR: AgentCapabilities = {
177
191
  permissions: "plan",
178
192
  // True since the daemon supplies its own ask tool; Cursor's own can fabricate an answer, so it's disallowed.
179
193
  questions: true,
180
- // stdio + http/sse MCP servers plus host callbacks; everything but a Claude Code plugin checkout.
194
+ // The turn's remote MCP servers plus host callbacks; everything but a Claude Code plugin checkout.
181
195
  mcp: "tools",
182
196
  // The JS backend rides Cursor's custom-tool seam rather than an MCP server, and consults the rulebook in its
183
197
  // handler, since the hook file only covers the shell.
@@ -193,6 +207,7 @@ export const CURSOR: AgentCapabilities = {
193
207
  terminals: false,
194
208
  // The SDK throws typed errors instead of dissolving a refusal into prose, so the adapter files coded frames.
195
209
  recovery: true,
210
+ warm: false,
196
211
  // append, reached differently: `beforeSubmitPrompt`'s reply folds the daemon's text onto Cursor's base. The SDK can
197
212
  // replace that base outright, but only for an entitled account, so the seam every turn can reach is this one.
198
213
  instructions: "append",
@@ -32,6 +32,8 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
32
32
  // - refuse-only: a hold refuses instead of parking
33
33
  // - other values: park on a card
34
34
  rulebook: "enforced",
35
+ // agent/run/turn/cache-keepwarm.ts files nothing to keep warm for a turn whose runtime lacks it.
36
+ warm: "enforced",
35
37
 
36
38
  // Descriptive: true of the runtime but nothing consults these; wiring one up moves it to enforced.
37
39
  questions: "descriptive",
@@ -1,5 +1,18 @@
1
- import { bindingWindow, gatesModel, gatingWindows, scopedWindow, windowLive, windowPeriod } from "./plan-pools.js";
2
- import type { AccountUsage, UsageWindow } from "../schemas/providers/plan-limits.js";
1
+ import {
2
+ bindingWindow,
3
+ gatesModel,
4
+ gatingWindows,
5
+ headroomState,
6
+ preferredAccount,
7
+ refusalVerdict,
8
+ roomiestAccount,
9
+ scopedWindow,
10
+ serviceState,
11
+ serviceStates,
12
+ windowLive,
13
+ windowPeriod,
14
+ } from "./plan-pools.js";
15
+ import type { AccountState, AccountUsage, ProviderRefusal, UsageWindow } from "../schemas/providers/plan-limits.js";
3
16
 
4
17
  // One rule for which pool blocks a given model, shared by the daemon and the browser: a plan can meter models
5
18
  // separately, so the account's fullest pool need not be the model's own.
@@ -102,3 +115,109 @@ test("a reading is live until its reset, or, with none published, for one window
102
115
  // And a pool whose length nothing names has only its reset instant; without one it stands.
103
116
  expect(windowLive(window({ kind: "claude:tangelo" }), NOW - 400 * HOUR, NOW)).toBe(true);
104
117
  });
118
+
119
+ // Serviceability: the one verdict on whether an account can serve a turn.
120
+
121
+ const at = (measuredAt: number, ...windows: UsageWindow[]): AccountUsage => ({ windows, measuredAt });
122
+ const refusal = (over: Partial<ProviderRefusal> & Pick<ProviderRefusal, "kind">): ProviderRefusal => ({ at: 1_000, message: "no.", ...over });
123
+
124
+ test("spent is the contract's one line: 99.5% still has room, 100% is spent until its last full pool reopens", () => {
125
+ expect(headroomState(usage(window({ kind: "five_hour", utilization: 99.5 })))).toEqual({ kind: "ready", room: 0.5 });
126
+ expect(
127
+ headroomState(usage(window({ kind: "five_hour", utilization: 100, resetsAt: 50 }), window({ kind: "seven_day", utilization: 100, resetsAt: 900 }))),
128
+ ).toEqual({ kind: "spent", reopensAt: 900 });
129
+ expect(headroomState(usage(window({ kind: "five_hour", utilization: 100 })))).toEqual({ kind: "spent" });
130
+ expect(headroomState(undefined)).toEqual({ kind: "unknown" });
131
+ expect(headroomState(CLAUDE, { id: "claude-haiku-4-5" })).toEqual({ kind: "ready", room: 70 });
132
+ expect(headroomState(CLAUDE, { id: "claude-opus-4-6" })).toEqual({ kind: "spent" });
133
+ });
134
+
135
+ test("with no model named, an account is spent only when no model can run on it", () => {
136
+ // A full Opus slice leaves every other model the room of the fullest pool still open.
137
+ expect(headroomState(CLAUDE)).toEqual({ kind: "ready", room: 70 });
138
+ expect(headroomState(GOOGLE)).toEqual({ kind: "ready", room: 73 });
139
+ // A pool gating every model, full: nothing runs, and it reopens with that pool.
140
+ expect(headroomState(usage(window({ kind: "seven_day", utilization: 100, resetsAt: 900 }), window({ kind: "model:Opus", utilization: 10, gates: { models: ["Opus"] } })))).toEqual({
141
+ kind: "spent",
142
+ reopensAt: 900,
143
+ });
144
+ // Every slice full: the first to reopen lets some model run again.
145
+ expect(
146
+ headroomState(
147
+ usage(
148
+ window({ kind: "g", utilization: 100, resetsAt: 500, gates: { models: ["gemini"] } }),
149
+ window({ kind: "c", utilization: 100, resetsAt: 800, gates: { models: ["claude"] } }),
150
+ ),
151
+ ),
152
+ ).toEqual({ kind: "spent", reopensAt: 500 });
153
+ });
154
+
155
+ test("a revoked sign-in outranks a lost seat, which outranks a bench, whatever the meters say", () => {
156
+ const facts = { account: "a", usage: usage(window({ kind: "five_hour", utilization: 0 })) };
157
+ expect(serviceState({ ...facts, needsReauth: true, seatRefusal: "no seat" })).toEqual({ kind: "blocked", fix: "reconnect", reason: "sign-in expired" });
158
+ expect(serviceState({ ...facts, seatRefusal: "Your org disabled Claude Code." })).toEqual({
159
+ kind: "blocked",
160
+ fix: "admin",
161
+ reason: "Your org disabled Claude Code.",
162
+ });
163
+ expect(serviceState({ ...facts, cooling: { reason: "no project" } })).toEqual({ kind: "blocked", fix: "reconnect", reason: "no project" });
164
+ expect(serviceState({ ...facts, cooling: { until: 2_000 } }, undefined, undefined, 1_000_000)).toEqual({
165
+ kind: "blocked",
166
+ fix: "wait",
167
+ reason: "cooling down",
168
+ until: 2_000,
169
+ });
170
+ // A bench whose instant has passed is over.
171
+ expect(serviceState({ ...facts, cooling: { until: 999 } }, undefined, undefined, 1_000_000)).toEqual({ kind: "ready", room: 100 });
172
+ });
173
+
174
+ test("a standing refusal blocks by its kind, and a limit refusal pins only the pool its model spends", () => {
175
+ const facts = { account: "a", usage: CLAUDE };
176
+ expect(serviceState(facts, refusal({ kind: "auth" }))).toEqual({ kind: "blocked", fix: "reconnect", reason: "no." });
177
+ expect(serviceState(facts, refusal({ kind: "entitlement" }))).toEqual({ kind: "blocked", fix: "admin", reason: "no." });
178
+ // Refused on Haiku: the 5-hour/weekly binding pool (seven_day, 30%) reads full, so Haiku is spent too.
179
+ expect(serviceState(facts, refusal({ kind: "limit", model: "claude-haiku-4-5" }), { id: "claude-haiku-4-5" })).toEqual({ kind: "spent" });
180
+ // Refused on Opus: its own slice was already full; Haiku keeps the room its pools show.
181
+ expect(serviceState(facts, refusal({ kind: "limit", model: "claude-opus-4-6" }), { id: "claude-haiku-4-5" })).toEqual({ kind: "ready", room: 70 });
182
+ // No reading at all: the refusal is the evidence, unless it was about another model.
183
+ expect(serviceState({ account: "a" }, refusal({ kind: "limit" }))).toEqual({ kind: "spent" });
184
+ expect(serviceState({ account: "a" }, refusal({ kind: "limit", model: "opus" }), { id: "haiku" })).toEqual({ kind: "unknown" });
185
+ });
186
+
187
+ test("a refusal stands until a later reading contradicts it, and never past its account's disconnection", () => {
188
+ const room = at(2_000, window({ kind: "five_hour", utilization: 40 }));
189
+ const full = at(2_000, window({ kind: "five_hour", utilization: 100 }));
190
+ const before = at(500, window({ kind: "five_hour", utilization: 40 }));
191
+ expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [{ account: "a", usage: room }])).toBe("answered");
192
+ expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [{ account: "a", usage: full }])).toBe("standing");
193
+ expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [{ account: "a", usage: before }])).toBe("standing");
194
+ // Another account's room proves nothing about the one refused.
195
+ expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [{ account: "a", usage: before }, { account: "b", usage: room }])).toBe("standing");
196
+ // A refusal naming no account is answered by any of them.
197
+ expect(refusalVerdict(refusal({ kind: "limit" }), [{ account: "a", usage: before }, { account: "b", usage: room }])).toBe("answered");
198
+ expect(refusalVerdict(refusal({ kind: "auth", account: "a" }), [{ account: "a", usage: full }])).toBe("answered");
199
+ expect(refusalVerdict(refusal({ kind: "auth", account: "a" }), [{ account: "a", usage: full, needsReauth: true }])).toBe("standing");
200
+ expect(refusalVerdict(refusal({ kind: "entitlement", account: "a" }), [{ account: "a", usage: room }])).toBe("standing");
201
+ expect(refusalVerdict(refusal({ kind: "limit", account: "gone" }), [{ account: "a", usage: room }])).toBe("gone");
202
+ expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [])).toBe("standing");
203
+ });
204
+
205
+ test("a provider's accounts are judged together: a standing refusal lands only on the account it names", () => {
206
+ const room = at(500, window({ kind: "five_hour", utilization: 40 }));
207
+ const states = serviceStates([{ account: "a", usage: room }, { account: "b", usage: room }], refusal({ kind: "entitlement", account: "a" }));
208
+ expect(Object.fromEntries(states)).toEqual({ a: { kind: "blocked", fix: "admin", reason: "no." }, b: { kind: "ready", room: 60 } });
209
+ const answered = serviceStates([{ account: "a", usage: at(2_000, window({ kind: "five_hour", utilization: 40 })) }], refusal({ kind: "limit", account: "a" }));
210
+ expect(answered.get("a")).toEqual({ kind: "ready", room: 60 });
211
+ });
212
+
213
+ test("an unnamed turn takes the most room, then an unmeasured account, then a spent one, and a blocked one only when that is all", () => {
214
+ const entry = (id: string, state: AccountState) => ({ id, state });
215
+ const blocked: AccountState = { kind: "blocked", fix: "admin", reason: "no seat" };
216
+ expect(preferredAccount([entry("seatless", blocked), entry("spent", { kind: "spent" }), entry("low", { kind: "ready", room: 5 }), entry("high", { kind: "ready", room: 60 })])?.id).toBe("high");
217
+ expect(preferredAccount([entry("seatless", blocked), entry("spent", { kind: "spent" }), entry("unread", { kind: "unknown" })])?.id).toBe("unread");
218
+ expect(preferredAccount([entry("seatless", blocked), entry("spent", { kind: "spent" })])?.id).toBe("spent");
219
+ expect(preferredAccount([entry("seatless", blocked), entry("other", blocked)])?.id).toBe("seatless");
220
+ expect(preferredAccount([entry("first", { kind: "ready", room: 60 }), entry("second", { kind: "ready", room: 60 })])?.id).toBe("first");
221
+ expect(roomiestAccount([entry("seatless", blocked), entry("unread", { kind: "unknown" })])).toBeUndefined();
222
+ expect(roomiestAccount([entry("low", { kind: "ready", room: 5 }), entry("high", { kind: "ready", room: 60 })])?.id).toBe("high");
223
+ });