@intentic/sandbox-contract 1.330.0 → 1.332.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 (337) hide show
  1. package/README.md +15 -0
  2. package/dist/chores/probes.d.ts.map +1 -1
  3. package/dist/chores/probes.js.map +1 -1
  4. package/dist/contracts/accounts.contract.d.ts +15 -0
  5. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.d.ts +38 -0
  7. package/dist/contracts/agent.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.js +14 -3
  9. package/dist/contracts/agent.contract.js.map +1 -1
  10. package/dist/contracts/agents.contract.d.ts +29 -0
  11. package/dist/contracts/agents.contract.d.ts.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +2 -0
  13. package/dist/contracts/automations.contract.d.ts.map +1 -1
  14. package/dist/contracts/device.contract.d.ts +7 -0
  15. package/dist/contracts/device.contract.d.ts.map +1 -1
  16. package/dist/contracts/git.contract.d.ts +2 -0
  17. package/dist/contracts/git.contract.d.ts.map +1 -1
  18. package/dist/contracts/logs.contract.d.ts +10 -0
  19. package/dist/contracts/logs.contract.d.ts.map +1 -1
  20. package/dist/contracts/needs.contract.d.ts +7 -0
  21. package/dist/contracts/needs.contract.d.ts.map +1 -1
  22. package/dist/contracts/offload.contract.d.ts +70 -0
  23. package/dist/contracts/offload.contract.d.ts.map +1 -1
  24. package/dist/contracts/offload.contract.js +10 -1
  25. package/dist/contracts/offload.contract.js.map +1 -1
  26. package/dist/contracts/privacy.contract.d.ts +14 -0
  27. package/dist/contracts/privacy.contract.d.ts.map +1 -1
  28. package/dist/contracts/privacy.contract.js +11 -1
  29. package/dist/contracts/privacy.contract.js.map +1 -1
  30. package/dist/contracts/providers.contract.d.ts +3 -0
  31. package/dist/contracts/providers.contract.d.ts.map +1 -1
  32. package/dist/contracts/runner.contract.d.ts +106 -2
  33. package/dist/contracts/runner.contract.d.ts.map +1 -1
  34. package/dist/contracts/sessions.contract.d.ts +1 -0
  35. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  36. package/dist/contracts/settings.contract.d.ts +74 -94
  37. package/dist/contracts/settings.contract.d.ts.map +1 -1
  38. package/dist/contracts/settings.contract.js +19 -1
  39. package/dist/contracts/settings.contract.js.map +1 -1
  40. package/dist/contracts/system.contract.d.ts +20 -0
  41. package/dist/contracts/system.contract.d.ts.map +1 -1
  42. package/dist/contracts/translator.contract.d.ts +8 -0
  43. package/dist/contracts/translator.contract.d.ts.map +1 -1
  44. package/dist/events/agent-events.d.ts +47 -0
  45. package/dist/events/agent-events.d.ts.map +1 -1
  46. package/dist/events/agent-events.js +6 -0
  47. package/dist/events/agent-events.js.map +1 -1
  48. package/dist/events/agent-words.d.ts.map +1 -1
  49. package/dist/events/agent-words.js +1 -1
  50. package/dist/events/agent-words.js.map +1 -1
  51. package/dist/events/resume.d.ts +3 -0
  52. package/dist/events/resume.d.ts.map +1 -1
  53. package/dist/events/resume.js +17 -2
  54. package/dist/events/resume.js.map +1 -1
  55. package/dist/events/sandbox-notice.d.ts +18 -0
  56. package/dist/events/sandbox-notice.d.ts.map +1 -1
  57. package/dist/events/sandbox-notice.js +27 -1
  58. package/dist/events/sandbox-notice.js.map +1 -1
  59. package/dist/events/system-events.d.ts +4 -0
  60. package/dist/events/system-events.d.ts.map +1 -1
  61. package/dist/events/transcript.d.ts +64 -0
  62. package/dist/events/transcript.d.ts.map +1 -1
  63. package/dist/events/transcript.js +17 -1
  64. package/dist/events/transcript.js.map +1 -1
  65. package/dist/index.d.ts +327 -124
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +5 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/models/agent-catalog.d.ts.map +1 -1
  70. package/dist/models/agent-catalog.js +8 -7
  71. package/dist/models/agent-catalog.js.map +1 -1
  72. package/dist/models/agent-runtimes.d.ts +3 -1
  73. package/dist/models/agent-runtimes.d.ts.map +1 -1
  74. package/dist/models/agent-runtimes.js +10 -0
  75. package/dist/models/agent-runtimes.js.map +1 -1
  76. package/dist/models/model-order.d.ts +1 -0
  77. package/dist/models/model-order.d.ts.map +1 -1
  78. package/dist/models/model-order.js +9 -0
  79. package/dist/models/model-order.js.map +1 -1
  80. package/dist/models/model-pins.d.ts.map +1 -1
  81. package/dist/models/model-pins.js.map +1 -1
  82. package/dist/models/model-roles.d.ts +32 -17
  83. package/dist/models/model-roles.d.ts.map +1 -1
  84. package/dist/models/model-roles.js +30 -26
  85. package/dist/models/model-roles.js.map +1 -1
  86. package/dist/models/model-setups.d.ts +46 -0
  87. package/dist/models/model-setups.d.ts.map +1 -0
  88. package/dist/models/model-setups.js +86 -0
  89. package/dist/models/model-setups.js.map +1 -0
  90. package/dist/models/plan-pools.d.ts +5 -1
  91. package/dist/models/plan-pools.d.ts.map +1 -1
  92. package/dist/models/plan-pools.js +41 -13
  93. package/dist/models/plan-pools.js.map +1 -1
  94. package/dist/models/provider-specs.d.ts +20 -0
  95. package/dist/models/provider-specs.d.ts.map +1 -1
  96. package/dist/models/provider-specs.js +13 -1
  97. package/dist/models/provider-specs.js.map +1 -1
  98. package/dist/policy/command-classes.d.ts.map +1 -1
  99. package/dist/policy/command-classes.js +26 -3
  100. package/dist/policy/command-classes.js.map +1 -1
  101. package/dist/policy/credential-material.d.ts +6 -0
  102. package/dist/policy/credential-material.d.ts.map +1 -1
  103. package/dist/policy/credential-material.js +37 -15
  104. package/dist/policy/credential-material.js.map +1 -1
  105. package/dist/policy/search-globs.d.ts +3 -0
  106. package/dist/policy/search-globs.d.ts.map +1 -1
  107. package/dist/policy/search-globs.js +118 -0
  108. package/dist/policy/search-globs.js.map +1 -1
  109. package/dist/policy/secret-hosts.d.ts +1 -0
  110. package/dist/policy/secret-hosts.d.ts.map +1 -1
  111. package/dist/policy/secret-hosts.js +1 -0
  112. package/dist/policy/secret-hosts.js.map +1 -1
  113. package/dist/policy/turned-away.d.ts.map +1 -1
  114. package/dist/policy/turned-away.js +1 -0
  115. package/dist/policy/turned-away.js.map +1 -1
  116. package/dist/protocol/doors/runner-protocol.d.ts +9 -0
  117. package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
  118. package/dist/protocol/doors/runner-protocol.js +5 -0
  119. package/dist/protocol/doors/runner-protocol.js.map +1 -1
  120. package/dist/protocol/pairing-redeem.d.ts +23 -0
  121. package/dist/protocol/pairing-redeem.d.ts.map +1 -0
  122. package/dist/protocol/pairing-redeem.js +44 -0
  123. package/dist/protocol/pairing-redeem.js.map +1 -0
  124. package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
  125. package/dist/protocol/peer-mcp-server.js +1 -1
  126. package/dist/protocol/peer-mcp-server.js.map +1 -1
  127. package/dist/protocol/raw/raw-json-routes.d.ts +16 -0
  128. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  129. package/dist/protocol/raw/raw-json-routes.js +2 -0
  130. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  131. package/dist/protocol/raw/raw-routes.d.ts +8 -0
  132. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  133. package/dist/protocol/raw/raw-routes.js +2 -0
  134. package/dist/protocol/raw/raw-routes.js.map +1 -1
  135. package/dist/protocol/sse.d.ts +20 -1
  136. package/dist/protocol/sse.d.ts.map +1 -1
  137. package/dist/protocol/sse.js +112 -17
  138. package/dist/protocol/sse.js.map +1 -1
  139. package/dist/schemas/agent.d.ts +35 -0
  140. package/dist/schemas/agent.d.ts.map +1 -1
  141. package/dist/schemas/agent.js +12 -1
  142. package/dist/schemas/agent.js.map +1 -1
  143. package/dist/schemas/automations.d.ts +6 -0
  144. package/dist/schemas/automations.d.ts.map +1 -1
  145. package/dist/schemas/automations.js +3 -1
  146. package/dist/schemas/automations.js.map +1 -1
  147. package/dist/schemas/devices.d.ts +17 -0
  148. package/dist/schemas/devices.d.ts.map +1 -1
  149. package/dist/schemas/devices.js +2 -1
  150. package/dist/schemas/devices.js.map +1 -1
  151. package/dist/schemas/engines.d.ts +8 -1
  152. package/dist/schemas/engines.d.ts.map +1 -1
  153. package/dist/schemas/engines.js +1 -1
  154. package/dist/schemas/engines.js.map +1 -1
  155. package/dist/schemas/environment.d.ts +30 -0
  156. package/dist/schemas/environment.d.ts.map +1 -1
  157. package/dist/schemas/environment.js +13 -0
  158. package/dist/schemas/environment.js.map +1 -1
  159. package/dist/schemas/git/git-scope.d.ts +20 -0
  160. package/dist/schemas/git/git-scope.d.ts.map +1 -0
  161. package/dist/schemas/git/git-scope.js +37 -0
  162. package/dist/schemas/git/git-scope.js.map +1 -0
  163. package/dist/schemas/git/git.d.ts +1 -0
  164. package/dist/schemas/git/git.d.ts.map +1 -1
  165. package/dist/schemas/git/git.js +9 -1
  166. package/dist/schemas/git/git.js.map +1 -1
  167. package/dist/schemas/hosts.d.ts +42 -0
  168. package/dist/schemas/hosts.d.ts.map +1 -1
  169. package/dist/schemas/hosts.js +3 -0
  170. package/dist/schemas/hosts.js.map +1 -1
  171. package/dist/schemas/keep-warm.d.ts +1 -0
  172. package/dist/schemas/keep-warm.d.ts.map +1 -1
  173. package/dist/schemas/keep-warm.js +1 -1
  174. package/dist/schemas/keep-warm.js.map +1 -1
  175. package/dist/schemas/logs.d.ts +21 -0
  176. package/dist/schemas/logs.d.ts.map +1 -1
  177. package/dist/schemas/logs.js +19 -0
  178. package/dist/schemas/logs.js.map +1 -1
  179. package/dist/schemas/metrics.d.ts +12 -0
  180. package/dist/schemas/metrics.d.ts.map +1 -1
  181. package/dist/schemas/metrics.js +26 -8
  182. package/dist/schemas/metrics.js.map +1 -1
  183. package/dist/schemas/needs.d.ts +19 -0
  184. package/dist/schemas/needs.d.ts.map +1 -1
  185. package/dist/schemas/needs.js +17 -3
  186. package/dist/schemas/needs.js.map +1 -1
  187. package/dist/schemas/offload.d.ts +154 -0
  188. package/dist/schemas/offload.d.ts.map +1 -1
  189. package/dist/schemas/offload.js +34 -0
  190. package/dist/schemas/offload.js.map +1 -1
  191. package/dist/schemas/privacy.d.ts +19 -0
  192. package/dist/schemas/privacy.d.ts.map +1 -1
  193. package/dist/schemas/privacy.js +17 -2
  194. package/dist/schemas/privacy.js.map +1 -1
  195. package/dist/schemas/project-bring-back.d.ts +172 -0
  196. package/dist/schemas/project-bring-back.d.ts.map +1 -0
  197. package/dist/schemas/project-bring-back.js +39 -0
  198. package/dist/schemas/project-bring-back.js.map +1 -0
  199. package/dist/schemas/providers/handoff.d.ts +77 -0
  200. package/dist/schemas/providers/handoff.d.ts.map +1 -0
  201. package/dist/schemas/providers/handoff.js +85 -0
  202. package/dist/schemas/providers/handoff.js.map +1 -0
  203. package/dist/schemas/providers/plan-limits.d.ts +34 -0
  204. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  205. package/dist/schemas/providers/plan-limits.js +13 -2
  206. package/dist/schemas/providers/plan-limits.js.map +1 -1
  207. package/dist/schemas/providers/provider-oauth.d.ts +8 -0
  208. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  209. package/dist/schemas/providers/usage.d.ts +3 -1
  210. package/dist/schemas/providers/usage.d.ts.map +1 -1
  211. package/dist/schemas/providers/usage.js +7 -2
  212. package/dist/schemas/providers/usage.js.map +1 -1
  213. package/dist/schemas/run-targets.d.ts.map +1 -1
  214. package/dist/schemas/run-targets.js +5 -2
  215. package/dist/schemas/run-targets.js.map +1 -1
  216. package/dist/schemas/settings-history.d.ts +6 -1
  217. package/dist/schemas/settings-history.d.ts.map +1 -1
  218. package/dist/schemas/settings-history.js +25 -1
  219. package/dist/schemas/settings-history.js.map +1 -1
  220. package/dist/schemas/settings.d.ts +69 -100
  221. package/dist/schemas/settings.d.ts.map +1 -1
  222. package/dist/schemas/settings.js +41 -24
  223. package/dist/schemas/settings.js.map +1 -1
  224. package/dist/schemas/short-codes.d.ts +31 -0
  225. package/dist/schemas/short-codes.d.ts.map +1 -0
  226. package/dist/schemas/short-codes.js +28 -0
  227. package/dist/schemas/short-codes.js.map +1 -0
  228. package/dist/schemas/system.d.ts +130 -0
  229. package/dist/schemas/system.d.ts.map +1 -1
  230. package/dist/schemas/system.js +37 -1
  231. package/dist/schemas/system.js.map +1 -1
  232. package/dist/state/definition.d.ts +64 -40
  233. package/dist/state/definition.d.ts.map +1 -1
  234. package/dist/state/history-state.d.ts.map +1 -1
  235. package/dist/state/history-state.js +1 -0
  236. package/dist/state/history-state.js.map +1 -1
  237. package/dist/state/runtime-state.d.ts +3 -0
  238. package/dist/state/runtime-state.d.ts.map +1 -1
  239. package/dist/state/runtime-state.js +1 -0
  240. package/dist/state/runtime-state.js.map +1 -1
  241. package/dist/state/versions.d.ts +6 -0
  242. package/dist/state/versions.d.ts.map +1 -1
  243. package/dist/state/versions.js +47 -0
  244. package/dist/state/versions.js.map +1 -1
  245. package/dist/text/embed.d.ts +2 -1
  246. package/dist/text/embed.d.ts.map +1 -1
  247. package/dist/text/embed.js +10 -5
  248. package/dist/text/embed.js.map +1 -1
  249. package/dist/text/title.d.ts.map +1 -1
  250. package/dist/text/title.js +4 -7
  251. package/dist/text/title.js.map +1 -1
  252. package/dist/text/transcript-fold.d.ts +2 -1
  253. package/dist/text/transcript-fold.d.ts.map +1 -1
  254. package/dist/text/transcript-fold.js +21 -6
  255. package/dist/text/transcript-fold.js.map +1 -1
  256. package/package.json +28 -6
  257. package/src/chores/probes.ts +3 -1
  258. package/src/contracts/agent.contract.ts +24 -3
  259. package/src/contracts/offload.contract.ts +21 -1
  260. package/src/contracts/privacy.contract.ts +13 -0
  261. package/src/contracts/settings.contract.ts +23 -0
  262. package/src/events/agent-events.ts +13 -0
  263. package/src/events/agent-words.ts +2 -3
  264. package/src/events/resume.test.ts +14 -2
  265. package/src/events/resume.ts +32 -3
  266. package/src/events/sandbox-notice.ts +31 -1
  267. package/src/events/transcript.ts +27 -2
  268. package/src/index.ts +5 -0
  269. package/src/models/agent-catalog.test.ts +4 -3
  270. package/src/models/agent-catalog.ts +10 -7
  271. package/src/models/agent-runtimes.ts +30 -1
  272. package/src/models/capability-ledger.test.ts +4 -0
  273. package/src/models/model-order.ts +13 -0
  274. package/src/models/model-pins.ts +5 -4
  275. package/src/models/model-roles.test.ts +9 -31
  276. package/src/models/model-roles.ts +42 -49
  277. package/src/models/model-setups.test.ts +100 -0
  278. package/src/models/model-setups.ts +151 -0
  279. package/src/models/plan-pools.test.ts +85 -0
  280. package/src/models/plan-pools.ts +76 -17
  281. package/src/models/provider-specs.ts +20 -2
  282. package/src/policy/command-classes.ts +34 -3
  283. package/src/policy/credential-material.ts +58 -16
  284. package/src/policy/search-globs.ts +135 -0
  285. package/src/policy/secret-hosts.ts +7 -0
  286. package/src/policy/tests/command-classes.test.ts +35 -0
  287. package/src/policy/tests/credential-material.test.ts +20 -0
  288. package/src/policy/tests/search-globs.test.ts +46 -1
  289. package/src/policy/tests/secret-hosts.test.ts +17 -1
  290. package/src/policy/turned-away.ts +2 -0
  291. package/src/protocol/doors/runner-protocol.ts +14 -1
  292. package/src/protocol/pairing-redeem.test.ts +86 -0
  293. package/src/protocol/pairing-redeem.ts +79 -0
  294. package/src/protocol/peer-mcp-server.ts +1 -2
  295. package/src/protocol/raw/raw-json-routes.ts +3 -0
  296. package/src/protocol/raw/raw-routes.ts +5 -0
  297. package/src/protocol/sse.test.ts +50 -1
  298. package/src/protocol/sse.ts +172 -26
  299. package/src/schemas/agent.ts +23 -2
  300. package/src/schemas/automations.ts +6 -2
  301. package/src/schemas/daemon-health.test.ts +28 -0
  302. package/src/schemas/devices.ts +11 -2
  303. package/src/schemas/engines.ts +1 -1
  304. package/src/schemas/environment.ts +25 -0
  305. package/src/schemas/git/git-scope.test.ts +91 -0
  306. package/src/schemas/git/git-scope.ts +71 -0
  307. package/src/schemas/git/git.ts +15 -1
  308. package/src/schemas/hosts.ts +7 -0
  309. package/src/schemas/keep-warm.ts +2 -1
  310. package/src/schemas/logs.ts +25 -0
  311. package/src/schemas/metrics.test.ts +80 -0
  312. package/src/schemas/metrics.ts +31 -9
  313. package/src/schemas/needs.ts +29 -3
  314. package/src/schemas/offload.ts +76 -0
  315. package/src/schemas/privacy.ts +27 -3
  316. package/src/schemas/project-bring-back.test.ts +15 -0
  317. package/src/schemas/project-bring-back.ts +78 -0
  318. package/src/schemas/providers/handoff.test.ts +42 -0
  319. package/src/schemas/providers/handoff.ts +128 -0
  320. package/src/schemas/providers/plan-limits.ts +23 -2
  321. package/src/schemas/providers/usage.ts +13 -7
  322. package/src/schemas/run-targets.ts +5 -2
  323. package/src/schemas/settings-history.ts +43 -1
  324. package/src/schemas/settings.test.ts +84 -6
  325. package/src/schemas/settings.ts +74 -44
  326. package/src/schemas/short-codes.ts +48 -0
  327. package/src/schemas/system.ts +63 -1
  328. package/src/state/history-state.ts +4 -0
  329. package/src/state/runtime-state.test.ts +4 -0
  330. package/src/state/runtime-state.ts +4 -0
  331. package/src/state/versions.test.ts +30 -1
  332. package/src/state/versions.ts +68 -0
  333. package/src/text/embed.test.ts +14 -0
  334. package/src/text/embed.ts +19 -10
  335. package/src/text/title.ts +5 -9
  336. package/src/text/transcript-fold.test.ts +61 -0
  337. package/src/text/transcript-fold.ts +31 -6
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+
3
+ // BRINGING AN AGENT'S WORK BACK FROM A FOLDER'S OWN SANDBOX: what the machine agent's `sync changes`, `sync bring-back`,
4
+ // `sync restore` and `sync direction --json` print, and so what the editor reads off the `intentic:project` event. The
5
+ // desktop app (project.rs) runs the verb and relays its JSON whole as `result`, beside a `kind` of its own, so it
6
+ // keeps no field of these: a new one reaches the editor without a desktop release. The machine agent wrote these as
7
+ // TypeScript interfaces and the editor as its own zod, which disagreed in name (the agent's internal `held` reason
8
+ // prints as `conflict: true`).
9
+
10
+ export const ProjectDirectionSchema = z.enum(["to-sandbox", "both"]);
11
+ export type ProjectDirection = z.infer<typeof ProjectDirectionSchema>;
12
+
13
+ export const ProjectChangeKindSchema = z.enum(["added", "modified", "deleted"]);
14
+ export type ProjectChangeKind = z.infer<typeof ProjectChangeKindSchema>;
15
+
16
+ // One entry the sandbox changed that the folder has not. `size` is the sandbox copy's, what bringing it back writes;
17
+ // `conflict` marks a `modified` path changed in the folder too, which a bring-back lists but keeps the folder's copy of.
18
+ export const ProjectChangeSchema = z.object({
19
+ path: z.string().min(1),
20
+ kind: ProjectChangeKindSchema,
21
+ size: z.number().optional(),
22
+ // The agent only ever prints `true`; a reader takes any boolean.
23
+ conflict: z.boolean().optional(),
24
+ });
25
+ export type ProjectChange = z.infer<typeof ProjectChangeSchema>;
26
+
27
+ // An entry a bring-back or restore left alone, and why (the folder's copy changed too, say).
28
+ export const ProjectSkippedSchema = z.object({ path: z.string(), reason: z.string() });
29
+ export type ProjectSkipped = z.infer<typeof ProjectSkippedSchema>;
30
+
31
+ export const ProjectAppliedSchema = z.object({ path: z.string(), kind: ProjectChangeKindSchema });
32
+ export type ProjectApplied = z.infer<typeof ProjectAppliedSchema>;
33
+
34
+ // A verb that ran and failed, in the agent's own sentence.
35
+ export const ProjectFailedSchema = z.object({ ok: z.literal(false), error: z.string() });
36
+
37
+ export const ProjectChangesResultSchema = z.object({
38
+ ok: z.literal(true),
39
+ // The pairing's key, which names the folder and its sandbox.
40
+ pairing: z.string().optional(),
41
+ direction: ProjectDirectionSchema,
42
+ changes: z.array(ProjectChangeSchema),
43
+ // More changes than the agent lists (its CHANGES_MAX); the rest are not named.
44
+ truncated: z.boolean().optional(),
45
+ });
46
+ export type ProjectChangesResult = z.infer<typeof ProjectChangesResultSchema>;
47
+
48
+ export const BringBackResultSchema = z.object({
49
+ ok: z.literal(true),
50
+ // The restore point that undoes it.
51
+ point: z.string().min(1),
52
+ applied: z.array(ProjectAppliedSchema),
53
+ skipped: z.array(ProjectSkippedSchema).optional(),
54
+ });
55
+ export type BringBackResult = z.infer<typeof BringBackResultSchema>;
56
+
57
+ export const RestoreResultSchema = z.object({
58
+ ok: z.literal(true),
59
+ restored: z.number(),
60
+ // The editor only counts what a restore did, so a skipped list of a shape it does not know reads as no list rather
61
+ // than costing the whole answer, and with it the step that waits for it.
62
+ skipped: z.array(ProjectSkippedSchema).optional().catch(undefined),
63
+ });
64
+ export type RestoreResult = z.infer<typeof RestoreResultSchema>;
65
+
66
+ export const DirectionResultSchema = z.object({ ok: z.literal(true), direction: ProjectDirectionSchema });
67
+ export type DirectionResult = z.infer<typeof DirectionResultSchema>;
68
+
69
+ // The `intentic:project` event's detail: the verb's kind with its result, or a verb that never reached its sandbox
70
+ // (no machine agent runs on this computer, say).
71
+ export const ProjectAnswerSchema = z.discriminatedUnion("kind", [
72
+ z.object({ kind: z.literal("changes"), result: z.union([ProjectChangesResultSchema, ProjectFailedSchema]) }),
73
+ z.object({ kind: z.literal("brought-back"), result: z.union([BringBackResultSchema, ProjectFailedSchema]) }),
74
+ z.object({ kind: z.literal("restored"), result: z.union([RestoreResultSchema, ProjectFailedSchema]) }),
75
+ z.object({ kind: z.literal("direction"), result: z.union([DirectionResultSchema, ProjectFailedSchema]) }),
76
+ z.object({ kind: z.literal("error"), verb: z.string(), error: z.string() }),
77
+ ]);
78
+ export type ProjectAnswer = z.infer<typeof ProjectAnswerSchema>;
@@ -0,0 +1,42 @@
1
+ import { type HandoffMode, handedOffLine, handedOffNotice, suggestHandoff } from "./handoff.js";
2
+
3
+ const THRESHOLDS = { carryUnder: 150_000, summaryOver: 700_000 };
4
+ const all = (): boolean => true;
5
+
6
+ describe("suggestHandoff", () => {
7
+ it("carries a small session, trims the middle band, summarises a very large one", () => {
8
+ expect(suggestHandoff(90_000, all, THRESHOLDS)).toBe("carry");
9
+ expect(suggestHandoff(150_000, all, THRESHOLDS)).toBe("trim");
10
+ expect(suggestHandoff(699_999, all, THRESHOLDS)).toBe("trim");
11
+ expect(suggestHandoff(700_000, all, THRESHOLDS)).toBe("summary");
12
+ });
13
+
14
+ it("carries when nothing measured the session, since there is no size to save on", () => {
15
+ expect(suggestHandoff(undefined, all, THRESHOLDS)).toBe("carry");
16
+ });
17
+
18
+ it("falls to the next way where the preferred one is not available", () => {
19
+ const noTrim = (mode: HandoffMode): boolean => mode !== "trim";
20
+ expect(suggestHandoff(400_000, noTrim, THRESHOLDS)).toBe("summary");
21
+ const carryOnly = (mode: HandoffMode): boolean => mode === "carry";
22
+ expect(suggestHandoff(900_000, carryOnly, THRESHOLDS)).toBe("carry");
23
+ expect(suggestHandoff(900_000, () => false, THRESHOLDS)).toBeUndefined();
24
+ });
25
+ });
26
+
27
+ describe("handedOffLine", () => {
28
+ it("names what the continuing session started from beside what carrying would have re-read", () => {
29
+ expect(handedOffLine({ mode: "trim", tokens: 112_400, from: 431_000, cleared: 180 })).toBe(
30
+ "Continued in a trimmed copy of the session (about 112k tokens instead of 431k): 180 older tool outputs cleared, every message and tool call kept.",
31
+ );
32
+ expect(handedOffLine({ mode: "summary", model: "claude-haiku-5-5", from: 431_000 })).toBe(
33
+ "Continued in a fresh session opened with a summary of the conversation written by claude-haiku-5-5.",
34
+ );
35
+ expect(handedOffLine({ mode: "carry", tokens: 431_000 })).toBe("Continued in the same session, re-reading all of it (about 431k tokens).");
36
+ });
37
+
38
+ it("says plainly when the way picked could not be built", () => {
39
+ expect(handedOffLine({ mode: "trim", fellBack: true })).toBe("Could not trim the session for the hand-off, so it was carried whole.");
40
+ expect(handedOffNotice({ mode: "summary", fellBack: true })).toStrictEqual({ code: "handedOff", params: { mode: "summary", fellBack: true } });
41
+ });
42
+ });
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import { tokensLabel } from "../keep-warm.js";
3
+ import type { SandboxNotice } from "../../events/sandbox-notice.js";
4
+
5
+ // What a conversation hands the session that continues it after a spent usage allowance, when the prompt cache is cold
6
+ // and continuing means reading the conversation again from scratch. Measured on real sessions before it was built
7
+ // (sandbox/bench/handoff-bench.ts, 2026-10-09): context is ~94% tool output, and every call of the next turn re-reads
8
+ // whatever the session starts from, so the starting size sets the cost of the whole turn, not just of its first call.
9
+ //
10
+ // carry resume the session as it is: the model keeps everything, and re-reads all of it
11
+ // trim resume a copy of the session with older tool output cleared; every message and tool call stays
12
+ // summary a fresh session opened with a smaller model's summary and the last exchanges word for word
13
+ //
14
+ // The sandbox suggests one by the conversation's size; the person picks, on the card or as a standing setting.
15
+
16
+ export const HANDOFF_MODES = ["carry", "trim", "summary"] as const;
17
+ export const HandoffModeSchema = z.enum(HANDOFF_MODES);
18
+ export type HandoffMode = z.infer<typeof HandoffModeSchema>;
19
+
20
+ // The standing answer: `suggested` follows the size-based suggestion, anything else names one mode, used wherever it
21
+ // is available.
22
+ export const LimitHandoffSchema = z.enum(["suggested", ...HANDOFF_MODES]);
23
+ export type LimitHandoff = z.infer<typeof LimitHandoffSchema>;
24
+
25
+ // How many of the newest tool results a trimmed session keeps whole.
26
+ export const TRIM_KEEPS_RESULTS = 10;
27
+
28
+ export const HandoffOfferSchema = z.object({
29
+ suggested: HandoffModeSchema.describe("The hand-off the sandbox would use unless told otherwise."),
30
+ basis: z
31
+ .enum(["size", "setting"])
32
+ .describe("Why that one: the conversation's size (the sandbox's suggestion), or the owner's standing setting naming it."),
33
+ carry: z
34
+ .object({
35
+ tokens: z.number().optional().describe("The context resuming the session re-reads. Absent where nothing measured it."),
36
+ })
37
+ .optional()
38
+ .describe("Present where the session can be resumed as it is."),
39
+ trim: z
40
+ .object({
41
+ tokens: z.number().describe("The estimated context a trimmed copy of the session starts from."),
42
+ cleared: z.number().describe("How many older tool outputs trimming clears."),
43
+ })
44
+ .optional()
45
+ .describe("Present where this runtime's session can be trimmed: a Claude Code session the sandbox holds."),
46
+ summary: z
47
+ .object({
48
+ tokens: z.number().describe("The estimated context a fresh session opened with the summary starts from."),
49
+ reads: z.number().optional().describe("The estimated tokens the summarising model reads once to write it."),
50
+ })
51
+ .optional()
52
+ .describe("Present where a model is set for hand-off summaries (Sandbox ▸ Agent ▸ Models)."),
53
+ chosen: HandoffModeSchema.optional().describe("The hand-off a person picked for this held turn, which every later send of it uses."),
54
+ });
55
+ export type HandoffOffer = z.infer<typeof HandoffOfferSchema>;
56
+
57
+ export const ChooseHandoffSchema = z.object({
58
+ conversationId: z.string().min(1).describe("Which conversation's held turn."),
59
+ handoff: HandoffModeSchema.describe("How the next send of the held turn continues the conversation."),
60
+ });
61
+ export type ChooseHandoff = z.infer<typeof ChooseHandoffSchema>;
62
+
63
+ export const handoffAvailable = (offer: HandoffOffer, mode: HandoffMode): boolean => offer[mode] !== undefined;
64
+
65
+ // The sandbox's suggestion for a session of `tokens` context: carry a small one (trimming saves little), trim the
66
+ // middle band (keeps almost everything at a fraction of the read), summarise a very large one (a trimmed copy still
67
+ // holds every message and tool call, which past ~700k is itself large). Falls to whatever is available.
68
+ export const suggestHandoff = (
69
+ tokens: number | undefined,
70
+ available: (mode: HandoffMode) => boolean,
71
+ thresholds: { readonly carryUnder: number; readonly summaryOver: number },
72
+ ): HandoffMode | undefined => {
73
+ const order: readonly HandoffMode[] =
74
+ tokens === undefined || tokens < thresholds.carryUnder
75
+ ? ["carry", "trim", "summary"]
76
+ : tokens >= thresholds.summaryOver
77
+ ? ["summary", "trim", "carry"]
78
+ : ["trim", "summary", "carry"];
79
+ return order.find((mode) => available(mode));
80
+ };
81
+
82
+ // How a held turn actually continued, told once as its re-run starts, which the transcript keeps as a row: what it
83
+ // started from beside what carrying the session whole would have re-read. `fellBack` names a hand-off that was asked for
84
+ // and could not be built (no session file to trim, no model answered for the summary), which went on with the short
85
+ // record hand-off instead.
86
+ export const HandedOffSchema = z.object({
87
+ mode: HandoffModeSchema,
88
+ tokens: z.number().optional().describe("The context the continuing session starts from, estimated."),
89
+ from: z.number().optional().describe("What carrying the session whole would have re-read."),
90
+ cleared: z.number().optional().describe("Older tool outputs a trimmed copy cleared."),
91
+ model: z.string().optional().describe("The model that wrote the summary."),
92
+ fellBack: z.boolean().optional(),
93
+ });
94
+ export type HandedOff = z.infer<typeof HandedOffSchema>;
95
+
96
+ export const handedOffNotice = (event: HandedOff): Extract<SandboxNotice, { code: "handedOff" }> => ({
97
+ code: "handedOff",
98
+ params: {
99
+ mode: event.mode,
100
+ ...(event.tokens === undefined ? {} : { tokens: tokensLabel(event.tokens) }),
101
+ ...(event.from === undefined ? {} : { from: tokensLabel(event.from) }),
102
+ ...(event.cleared === undefined ? {} : { cleared: event.cleared }),
103
+ ...(event.model === undefined ? {} : { model: event.model }),
104
+ ...(event.fellBack === true ? { fellBack: true } : {}),
105
+ },
106
+ });
107
+
108
+ /** The transcript's line for how a held turn continued. */
109
+ export const handedOffLine = (event: HandedOff): string => {
110
+ if (event.fellBack === true) {
111
+ return event.mode === "trim"
112
+ ? "Could not trim the session for the hand-off, so it was carried whole."
113
+ : "Could not write a summary for the hand-off, so the fresh session opened with the recent conversation and where the work stands instead.";
114
+ }
115
+ const tokens = event.tokens === undefined ? undefined : tokensLabel(event.tokens);
116
+ switch (event.mode) {
117
+ case "carry":
118
+ return `Continued in the same session, re-reading all of it${tokens === undefined ? "" : ` (about ${tokens} tokens)`}.`;
119
+ case "trim": {
120
+ const cleared = event.cleared ?? 0;
121
+ const size =
122
+ tokens === undefined ? "" : event.from === undefined ? ` (about ${tokens} tokens)` : ` (about ${tokens} tokens instead of ${tokensLabel(event.from)})`;
123
+ return `Continued in a trimmed copy of the session${size}: ${cleared} older tool output${cleared === 1 ? "" : "s"} cleared, every message and tool call kept.`;
124
+ }
125
+ case "summary":
126
+ return `Continued in a fresh session opened with a summary of the conversation${event.model === undefined ? "" : ` written by ${event.model}`}.`;
127
+ }
128
+ };
@@ -3,6 +3,7 @@ import { ChildRunSchema, PAGE_VALUE_MAX } from "../../events/requests.js";
3
3
  import { TRANSLATOR_PROVIDERS, type TranslatorProvider } from "../../models/provider-specs.js";
4
4
  import { AgentHarnessSchema, AgentProviderSchema, ConversationIdSchema, EditorContextSchema } from "../agent.js";
5
5
  import { MENTION_LIMIT } from "../../text/mentions.js";
6
+ import { HandoffModeSchema } from "./handoff.js";
6
7
  // Headroom is one shape shared by every provider, not a Claude idea others imitate: a native account and a routed
7
8
  // subscription differ in who holds the credential, never in what a reading is. Every surface that draws a percentage
8
9
  // reads this one type.
@@ -23,6 +24,18 @@ export const UsageWindowSchema = z.object({
23
24
  utilization: z.number(), // 0-100
24
25
  resetsAt: z.number().optional(),
25
26
  gates: WindowGatesSchema,
27
+ rounded: z
28
+ .boolean()
29
+ .optional()
30
+ .describe(
31
+ "The provider rounds this figure to a whole percent (Anthropic does), so a reading of 100 may be 99.5 and the pool can still serve. Such a pool reads as at the line, not spent: only the provider's refusal proves it empty.",
32
+ ),
33
+ lapsedAt: z
34
+ .number()
35
+ .optional()
36
+ .describe(
37
+ "When the window this pool was last read in closed, in epoch seconds, while nothing has read the pool since. Its `utilization` is then 0, the floor every new window starts from, not a measurement, and `resetsAt` is absent because the next reset is unknown.",
38
+ ),
26
39
  });
27
40
  export type UsageWindow = z.infer<typeof UsageWindowSchema>;
28
41
  // A failed re-read, whatever the cause: a 429 park, a 403 asking for verification, a 5xx, a timeout. Recorded as the fact
@@ -59,7 +72,9 @@ export const AccountStateSchema = z.discriminatedUnion("kind", [
59
72
  kind: z.literal("ready"),
60
73
  room: z
61
74
  .number()
62
- .describe("How much of the fullest pool that gates the turn is left, in percent (above 0, up to 100). Pickers take the most room."),
75
+ .describe(
76
+ "How much of the fullest pool that gates the turn is left, in percent (0 up to 100). Pickers take the most room. 0 is a rounded pool read at 100: at the line, tried after an unmeasured account, never a limit move's destination.",
77
+ ),
63
78
  }),
64
79
  z.object({
65
80
  kind: z.literal("spent"),
@@ -391,8 +406,11 @@ export const ResumeRoutingSchema = z.object({
391
406
  .boolean()
392
407
  .optional()
393
408
  .describe(
394
- "When the account changes, keep the provider session (the model keeps everything, and re-reads all of it once on the other account) rather than opening a fresh one seeded from the record. Ignored when the provider changes, or when nothing changes.",
409
+ "When the account changes, keep the provider session (the model keeps everything, and re-reads all of it once on the other account) rather than opening a fresh one seeded from the record. Ignored when the provider changes, or when nothing changes. `handoff` says the same and more; it wins where both are given.",
395
410
  ),
411
+ handoff: HandoffModeSchema.optional().describe(
412
+ "How a turn a spent allowance held continues the conversation, on this account or another: `carry` resumes the session as it is, `trim` resumes a copy with older tool output cleared, `summary` opens a fresh session with a summary. Leave it out for the one a person picked on the held turn, else the sandbox's setting (`limitHandoff`). Only a spent allowance's held turn reads it.",
413
+ ),
396
414
  });
397
415
  export type ResumeRouting = z.infer<typeof ResumeRoutingSchema>;
398
416
  // Returns an ordinary `StartedTurn`; the caller attaches to it like any turn someone else started. A resume note on the
@@ -469,6 +487,9 @@ export const SwitchAccountSchema = z.object({
469
487
  .describe(
470
488
  "Keep the provider session across the move (the model keeps everything, and re-reads all of it once on the other account) rather than opening a fresh one seeded from the record.",
471
489
  ),
490
+ handoff: HandoffModeSchema.optional().describe(
491
+ "How the held turn continues the conversation on the new account: `carry`, `trim` or `summary` (see `limitHandoff`). Wins over `carry`. Leave it out for the held turn's own pick, else the sandbox's setting.",
492
+ ),
472
493
  run: z
473
494
  .boolean()
474
495
  .optional()
@@ -23,10 +23,14 @@ export const UsageTurnSchema = z.object({
23
23
  .optional()
24
24
  .describe("The model that was asked for, when one was named. Differs from `model` when something resolved it."),
25
25
  harness: z.string().describe("Which agentic loop it ran on."),
26
- // cancelled: the user pressed Stop, not a failure.
26
+ // cancelled: stopped before it finished, not a failure: the user pressed Stop, or a daemon restart cut it (then
27
+ // `errorCode` is `interrupted` and `errorMessage` says what became of it).
27
28
  // error: the provider or request killed the turn.
28
29
  // Absent means the row predates recording, not that the turn succeeded.
29
- outcome: z.enum(["ok", "error", "cancelled"]).optional().describe("How it ended: finished, failed, or was stopped by the user."),
30
+ outcome: z
31
+ .enum(["ok", "error", "cancelled"])
32
+ .optional()
33
+ .describe("How it ended: finished, failed, or stopped before it finished (by the user, or by a restart, whose errorCode is `interrupted`)."),
30
34
  // The failing frame's code (e.g. rate_limit, provider-outage); absent even on error means unclassified.
31
35
  errorCode: z.string().optional().describe("The failure's code, when it had one."),
32
36
  // The failing frame's own sentence, capped at ERROR_MESSAGE_CHARS.
@@ -38,6 +42,11 @@ export const UsageTurnSchema = z.object({
38
42
  .describe(
39
43
  "Which conversation it belonged to, so spending can be traced to a card. Absent only for an internal one-off with no conversation at all.",
40
44
  ),
45
+ // The ids a turn is followed by elsewhere: its turn id (activity log, failure lines), the run its transcript rows are
46
+ // stamped with, and the provider session it ran in. Absent on rows written before they were recorded.
47
+ turnId: z.string().optional().describe("The turn's id in the activity log and the daemon's log lines."),
48
+ run: z.string().optional().describe("The run whose id the turn's transcript rows carry."),
49
+ sessionId: z.string().optional().describe("The provider session the turn ran in."),
41
50
  // The provider's own count for the request, since one exchange can be several; 1 when unreported.
42
51
  turns: z
43
52
  .number()
@@ -71,8 +80,8 @@ export const UsageTurnSchema = z.object({
71
80
  // Arm of the project-map experiment, stable per conversation; mapChars is the note's length when sent.
72
81
  mapArm: z.boolean().optional(),
73
82
  mapChars: z.number().optional(),
74
- // Which comparison `mapArm` belongs to: absent on the map-or-none rows, "compact-vs-full" since 2026-10-06, when
75
- // the arm became the compact map and the holdout the full one.
83
+ // Which comparison `mapArm` belongs to: absent on the first map-or-none rows; "compact-vs-full" from 2026-10-06 to
84
+ // 10-12, when the arm was the compact map and the holdout the full one; "map-vs-none" since, the map against none.
76
85
  mapCohort: z.string().optional(),
77
86
  // Arm of the field-notes experiment, stable per conversation; notesChars is what the budget let through.
78
87
  notesArm: z.boolean().optional(),
@@ -80,9 +89,6 @@ export const UsageTurnSchema = z.object({
80
89
  // Hash of the brief that was sent, recorded on control turns too. Load-bearing rather than decorative: the file is
81
90
  // rewritten monthly, so a 30-day window holds two different treatments and pooling them would measure neither.
82
91
  notesCohort: z.string().optional(),
83
- // Arm of the tool-result clearing experiment, stable per conversation; true is a conversation whose old tool results
84
- // the gateway drops. Native Claude turns only: no other runtime's requests pass a gateway that clears.
85
- clearingArm: z.boolean().optional(),
86
92
  // What became of pre-turn retrieval on this turn, and how long it took. Assignment and DELIVERY are different
87
93
  // facts: the first version of this mechanism was assigned to every eligible turn and reached four in five of them,
88
94
  // which is the difference between a null result and a mechanism that never ran. Absent means the flag was off, so
@@ -1,3 +1,4 @@
1
+ import { STATE_DIR } from "@intentic/constants";
1
2
  import { z } from "zod";
2
3
  import { ArtifactNameSchema } from "./device-artifacts.js";
3
4
 
@@ -21,14 +22,16 @@ import { ArtifactNameSchema } from "./device-artifacts.js";
21
22
  // }]
22
23
  // }
23
24
 
24
- export const RUN_TARGETS_FILE = ".intentic/run.json";
25
+ export const RUN_TARGETS_FILE = `${STATE_DIR}/run.json`;
25
26
 
26
27
  const RepoPathSchema = z
27
28
  .string()
28
29
  .min(1)
29
30
  .max(500)
30
31
  .refine((path) => !path.startsWith("/") && !path.split(/[\\/]/).includes(".."), "a path inside the repository, relative to its root");
31
- const BundleNameSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199}(\/[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199})*$/, "a relative name, folders by /");
32
+ const BundleNameSchema = z
33
+ .string()
34
+ .regex(/^[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199}(\/[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199})*$/, "a relative name, folders by /");
32
35
 
33
36
  export const RunTargetSchema = z
34
37
  .object({
@@ -1,5 +1,8 @@
1
1
  import { z } from "zod";
2
- import { at, drop, dropAll, fold, isJsonObject, mapValue, retype } from "../documents/conversions.js";
2
+ import { at, drop, dropAll, fold, isJsonObject, type JsonObject, mapValue, retype, transform } from "../documents/conversions.js";
3
+ import { type ModelRole, ModelRoleSchema } from "../models/model-roles.js";
4
+ import { setupsFromRoleLists } from "../models/model-setups.js";
5
+ import { ModelPinSchema, type ModelPin } from "./agent.js";
3
6
  import type { KeepWarmSettings, RuleSchema } from "./settings.js";
4
7
 
5
8
  // The conversions the settings shape has had, oldest first: what brings any earlier release's settings to
@@ -63,6 +66,41 @@ const keepWarmObject = fold("folds keepWarm, keepWarmHours, keepWarmMinTokens an
63
66
  },
64
67
  });
65
68
 
69
+ // The Refactors job (2026-10-09): a refactor started from a repository's Health tab used to name no job and opened on
70
+ // whatever model the composer last held. A file from before it inherits the Maintenance chores list, the job nearest in
71
+ // kind, so the owner's choice for work they start carries over instead of the new job opening empty. Only while the key
72
+ // is absent: an owner who later empties it stores [], which this leaves alone.
73
+ type RolesWithChores = JsonObject & { readonly "maintenance-chore": readonly unknown[] };
74
+ const holdsChoresWithoutRefactors = (roles: JsonObject): roles is RolesWithChores =>
75
+ !Object.hasOwn(roles, "refactor-run") && Array.isArray(roles["maintenance-chore"]) && roles["maintenance-chore"].length > 0;
76
+ const refactorsFromChores = transform(
77
+ "gives the new Refactors job the Maintenance chores list",
78
+ holdsChoresWithoutRefactors,
79
+ (roles): JsonObject => ({ ...roles, "refactor-run": roles["maintenance-chore"] }),
80
+ );
81
+
82
+ // Setups (2026-10-12): a job no longer holds a model list of its own (`modelRoles`) beside one default list
83
+ // (`modelDefault`); it runs on a named setup it shares with other jobs. Jobs that held the same list share one setup, a
84
+ // list most of whose jobs are suggested a preset becomes that preset, and a job that held none, or an empty one, is on
85
+ // none, which runs it exactly as before: not at all. A list that does not read (a role this version does not know, a malformed pin) is left out rather than refusing
86
+ // the whole file, the same reading the schema gave it.
87
+ const PinList = z.array(ModelPinSchema);
88
+ const holdsRoleLists = (settings: JsonObject): settings is JsonObject => Object.hasOwn(settings, "modelRoles") || Object.hasOwn(settings, "modelDefault");
89
+ const roleListsIntoSetups = transform("folds modelRoles and modelDefault into setups (modelSetups, roleSetups)", holdsRoleLists, (settings): JsonObject => {
90
+ const { modelRoles, modelDefault, ...rest } = settings;
91
+ const fallback = PinList.safeParse(modelDefault);
92
+ const lists: Partial<Record<ModelRole, ModelPin[]>> = {};
93
+ for (const role of ModelRoleSchema.options) {
94
+ const own = isJsonObject(modelRoles) ? PinList.safeParse(modelRoles[role]) : undefined;
95
+ const list = own?.success === true ? own.data : fallback.success && fallback.data.length > 0 ? fallback.data : undefined;
96
+ if (list !== undefined) {
97
+ lists[role] = list;
98
+ }
99
+ }
100
+ const { modelSetups, roleSetups } = setupsFromRoleLists(lists);
101
+ return { ...rest, modelSetups, roleSetups };
102
+ });
103
+
66
104
  export const SETTINGS_HISTORY = [
67
105
  // Every setting a release since 2026-08-10 had and this one does not (read off the contract lock's history):
68
106
  // retired, so passthrough stops carrying them, a definition naming one still applies, and no later setting may
@@ -112,4 +150,8 @@ export const SETTINGS_HISTORY = [
112
150
  at("modelRoles", drop("pre-push-fix")),
113
151
  // The short guidance form (2026-10-06): it lost its A/B to the full one, which every turn now gets.
114
152
  ...dropAll(["leanGuidance", "leanGuidanceHoldout"]),
153
+ // Tool-result clearing (2026-10-12): it lost its A/B, rewriting more of the cache than it saved and failing more calls.
154
+ ...dropAll(["toolResultClearing", "toolResultClearingHoldout"]),
155
+ at("modelRoles", refactorsFromChores),
156
+ roleListsIntoSetups,
115
157
  ] as const;
@@ -1,4 +1,6 @@
1
1
  import { convertDocument } from "../documents/conversions.js";
2
+ import type { ModelRole } from "../models/model-roles.js";
3
+ import { rolePins } from "../models/model-setups.js";
2
4
  import { SETTINGS_HISTORY } from "./settings-history.js";
3
5
  import { RepoChecksFileSchema, RuleSchema, SandboxSettingsSchema, SandboxSettingsWriteSchema } from "./settings.js";
4
6
 
@@ -112,6 +114,20 @@ describe(`the folded keep-warm settings`, () => {
112
114
  });
113
115
  });
114
116
 
117
+ // Tool-result clearing lost its A/B and was removed (2026-10-12); a file that still switches it on reads, without it.
118
+ test(`a stored tool-result clearing switch is dropped, and the rest of the file kept`, () => {
119
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, { iqSearch: true, toolResultClearing: true, toolResultClearingHoldout: 0.5 }, true);
120
+ expect(converted.value).toEqual({ iqSearch: true });
121
+ expect(SandboxSettingsSchema.parse(converted.value).iqSearch).toBe(true);
122
+ });
123
+
124
+ // What a job runs on, read the way every reader reads it, over a sandbox with nothing connected: the setups a stored
125
+ // file converts into are judged by what they make each job run on, not by their shape.
126
+ const runsOn = (stored: unknown, role: ModelRole): readonly unknown[] => {
127
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, stored, true);
128
+ return rolePins(SandboxSettingsSchema.parse(converted.value), role, []);
129
+ };
130
+
115
131
  // The pre-push fix's model list went with the fix a refused push used to offer (2026-09-28). A settings file that still
116
132
  // names it must read, since an unknown role refuses the whole file, and every other job's list stays as its owner set it.
117
133
  describe(`the retired pre-push fix role`, () => {
@@ -119,13 +135,75 @@ describe(`the retired pre-push fix role`, () => {
119
135
 
120
136
  test(`is dropped from a stored file, leaving every other job's list, and the file reads`, () => {
121
137
  const stored = { modelRoles: { "pipeline-fix": [pin], "pre-push-fix": [pin] } };
122
- expect(SandboxSettingsSchema.safeParse(stored).success).toBe(false);
123
- const converted = convertDocument(SETTINGS_HISTORY, `object`, stored, true);
124
- expect(converted.value).toEqual({ modelRoles: { "pipeline-fix": [pin] } });
125
- expect(SandboxSettingsSchema.parse(converted.value).modelRoles).toEqual({ "pipeline-fix": [pin] });
138
+ expect(SandboxSettingsSchema.safeParse({ roleSetups: { "pre-push-fix": `high` } }).success).toBe(false);
139
+ expect(runsOn(stored, `pipeline-fix`)).toEqual([pin]);
140
+ });
141
+ });
142
+
143
+ // The Refactors job arrived on 2026-10-09; a file from before it carries the owner's Maintenance chores list over, so a
144
+ // refactor started from Health opens on a model they chose rather than on an empty job.
145
+ describe(`the new Refactors job`, () => {
146
+ const opus = { provider: `claude`, model: `claude-opus-5-5` };
147
+ const sol = { provider: `codex`, model: `gpt-sol-6.1` };
148
+
149
+ test(`takes the Maintenance chores list in a file from before it, and every other list stays`, () => {
150
+ const stored = { modelRoles: { "maintenance-chore": [opus, sol], "pipeline-fix": [sol] } };
151
+ expect(runsOn(stored, `refactor-run`)).toEqual([opus, sol]);
152
+ expect(runsOn(stored, `pipeline-fix`)).toEqual([sol]);
153
+ });
154
+
155
+ test(`leaves a list the owner set, even an emptied one`, () => {
156
+ expect(runsOn({ modelRoles: { "maintenance-chore": [opus], "refactor-run": [sol] } }, `refactor-run`)).toEqual([sol]);
157
+ expect(runsOn({ modelRoles: { "maintenance-chore": [opus], "refactor-run": [] } }, `refactor-run`)).toEqual([]);
158
+ });
159
+ });
160
+
161
+ // Setups (2026-10-12): per-job lists and the default list become named setups that jobs share.
162
+ describe(`per-job model lists`, () => {
163
+ const opus = { provider: `claude`, model: `claude-opus-5-5`, effort: `high` };
164
+ const haiku = { provider: `claude`, model: `claude-haiku-4-5` };
165
+ const sol = { provider: `codex`, model: `gpt-sol-6.1` };
166
+
167
+ test(`become setups that every job runs exactly as before on`, () => {
168
+ const cases = [
169
+ [`commit-message`, [haiku]],
170
+ [`session-title`, [haiku]],
171
+ [`pipeline-fix`, [opus, sol]],
172
+ [`refactor-run`, [opus, sol]],
173
+ [`handoff-summary`, [sol, haiku]],
174
+ [`safety-judge`, []],
175
+ ] as const satisfies readonly (readonly [ModelRole, readonly unknown[]])[];
176
+ for (const [role, pins] of cases) {
177
+ expect(runsOn({ modelRoles: Object.fromEntries(cases) }, role)).toEqual(pins);
178
+ }
179
+ });
180
+
181
+ test(`share a setup where they held one list, which becomes the preset most of them suit`, () => {
182
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: { "commit-message": [haiku], "session-title": [haiku], "pipeline-fix": [opus], "handoff-summary": [sol] } }, true);
183
+ expect(converted.value).toEqual({
184
+ modelSetups: [
185
+ { id: `low`, pins: [haiku] },
186
+ { id: `setup-1`, name: `Setup 1`, pins: [sol] },
187
+ { id: `high`, pins: [opus] },
188
+ ],
189
+ roleSetups: { "commit-message": `low`, "session-title": `low`, "handoff-summary": `setup-1`, "pipeline-fix": `high` },
190
+ });
191
+ });
192
+
193
+ test(`carry the default list to every job that had none of its own`, () => {
194
+ const stored = { modelDefault: [sol], modelRoles: { "pipeline-fix": [opus] } };
195
+ expect(runsOn(stored, `commit-message`)).toEqual([sol]);
196
+ expect(runsOn(stored, `loop-iteration`)).toEqual([sol]);
197
+ expect(runsOn(stored, `pipeline-fix`)).toEqual([opus]);
198
+ });
199
+
200
+ test(`leave a job that never had a list on none, running exactly as before`, () => {
201
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: {} }, true);
202
+ expect(converted.value).toEqual({ modelSetups: [], roleSetups: {} });
203
+ expect(runsOn({ modelRoles: {} }, `safety-judge`)).toEqual([]);
126
204
  });
127
205
 
128
- test(`leaves a file without it alone`, () => {
129
- expect(convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: { "pipeline-fix": [pin] } }, true).changes).toEqual([]);
206
+ test(`read once, and a file already in setups is left alone`, () => {
207
+ expect(convertDocument(SETTINGS_HISTORY, `object`, { modelSetups: [{ id: `low`, pins: [haiku] }], roleSetups: {} }, true).changes).toEqual([]);
130
208
  });
131
209
  });