@intentic/sandbox-contract 1.331.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 (332) 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 +11 -0
  5. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.d.ts +37 -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 +103 -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 +72 -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 +17 -0
  41. package/dist/contracts/system.contract.d.ts.map +1 -1
  42. package/dist/contracts/translator.contract.d.ts +4 -0
  43. package/dist/contracts/translator.contract.d.ts.map +1 -1
  44. package/dist/events/agent-events.d.ts +46 -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 +2 -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 +313 -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 +25 -18
  83. package/dist/models/model-roles.d.ts.map +1 -1
  84. package/dist/models/model-roles.js +23 -27
  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 +3 -1
  91. package/dist/models/plan-pools.d.ts.map +1 -1
  92. package/dist/models/plan-pools.js +18 -6
  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 +32 -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/git/git-scope.d.ts +20 -0
  156. package/dist/schemas/git/git-scope.d.ts.map +1 -0
  157. package/dist/schemas/git/git-scope.js +37 -0
  158. package/dist/schemas/git/git-scope.js.map +1 -0
  159. package/dist/schemas/git/git.d.ts +1 -0
  160. package/dist/schemas/git/git.d.ts.map +1 -1
  161. package/dist/schemas/git/git.js +9 -1
  162. package/dist/schemas/git/git.js.map +1 -1
  163. package/dist/schemas/hosts.d.ts +42 -0
  164. package/dist/schemas/hosts.d.ts.map +1 -1
  165. package/dist/schemas/hosts.js +3 -0
  166. package/dist/schemas/hosts.js.map +1 -1
  167. package/dist/schemas/keep-warm.d.ts +1 -0
  168. package/dist/schemas/keep-warm.d.ts.map +1 -1
  169. package/dist/schemas/keep-warm.js +1 -1
  170. package/dist/schemas/keep-warm.js.map +1 -1
  171. package/dist/schemas/logs.d.ts +21 -0
  172. package/dist/schemas/logs.d.ts.map +1 -1
  173. package/dist/schemas/logs.js +19 -0
  174. package/dist/schemas/logs.js.map +1 -1
  175. package/dist/schemas/metrics.d.ts +12 -0
  176. package/dist/schemas/metrics.d.ts.map +1 -1
  177. package/dist/schemas/metrics.js +26 -8
  178. package/dist/schemas/metrics.js.map +1 -1
  179. package/dist/schemas/needs.d.ts +19 -0
  180. package/dist/schemas/needs.d.ts.map +1 -1
  181. package/dist/schemas/needs.js +17 -3
  182. package/dist/schemas/needs.js.map +1 -1
  183. package/dist/schemas/offload.d.ts +154 -0
  184. package/dist/schemas/offload.d.ts.map +1 -1
  185. package/dist/schemas/offload.js +34 -0
  186. package/dist/schemas/offload.js.map +1 -1
  187. package/dist/schemas/privacy.d.ts +19 -0
  188. package/dist/schemas/privacy.d.ts.map +1 -1
  189. package/dist/schemas/privacy.js +17 -2
  190. package/dist/schemas/privacy.js.map +1 -1
  191. package/dist/schemas/project-bring-back.d.ts +172 -0
  192. package/dist/schemas/project-bring-back.d.ts.map +1 -0
  193. package/dist/schemas/project-bring-back.js +39 -0
  194. package/dist/schemas/project-bring-back.js.map +1 -0
  195. package/dist/schemas/providers/handoff.d.ts +77 -0
  196. package/dist/schemas/providers/handoff.d.ts.map +1 -0
  197. package/dist/schemas/providers/handoff.js +85 -0
  198. package/dist/schemas/providers/handoff.js.map +1 -0
  199. package/dist/schemas/providers/plan-limits.d.ts +27 -0
  200. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  201. package/dist/schemas/providers/plan-limits.js +8 -1
  202. package/dist/schemas/providers/plan-limits.js.map +1 -1
  203. package/dist/schemas/providers/provider-oauth.d.ts +4 -0
  204. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  205. package/dist/schemas/providers/usage.d.ts +3 -1
  206. package/dist/schemas/providers/usage.d.ts.map +1 -1
  207. package/dist/schemas/providers/usage.js +7 -2
  208. package/dist/schemas/providers/usage.js.map +1 -1
  209. package/dist/schemas/run-targets.d.ts.map +1 -1
  210. package/dist/schemas/run-targets.js +5 -2
  211. package/dist/schemas/run-targets.js.map +1 -1
  212. package/dist/schemas/settings-history.d.ts +1 -1
  213. package/dist/schemas/settings-history.d.ts.map +1 -1
  214. package/dist/schemas/settings-history.js +21 -0
  215. package/dist/schemas/settings-history.js.map +1 -1
  216. package/dist/schemas/settings.d.ts +67 -100
  217. package/dist/schemas/settings.d.ts.map +1 -1
  218. package/dist/schemas/settings.js +41 -24
  219. package/dist/schemas/settings.js.map +1 -1
  220. package/dist/schemas/short-codes.d.ts +31 -0
  221. package/dist/schemas/short-codes.d.ts.map +1 -0
  222. package/dist/schemas/short-codes.js +28 -0
  223. package/dist/schemas/short-codes.js.map +1 -0
  224. package/dist/schemas/system.d.ts +130 -0
  225. package/dist/schemas/system.d.ts.map +1 -1
  226. package/dist/schemas/system.js +37 -1
  227. package/dist/schemas/system.js.map +1 -1
  228. package/dist/state/definition.d.ts +64 -40
  229. package/dist/state/definition.d.ts.map +1 -1
  230. package/dist/state/history-state.d.ts.map +1 -1
  231. package/dist/state/history-state.js +1 -0
  232. package/dist/state/history-state.js.map +1 -1
  233. package/dist/state/runtime-state.d.ts +3 -0
  234. package/dist/state/runtime-state.d.ts.map +1 -1
  235. package/dist/state/runtime-state.js +1 -0
  236. package/dist/state/runtime-state.js.map +1 -1
  237. package/dist/state/versions.d.ts +6 -0
  238. package/dist/state/versions.d.ts.map +1 -1
  239. package/dist/state/versions.js +47 -0
  240. package/dist/state/versions.js.map +1 -1
  241. package/dist/text/embed.d.ts +2 -1
  242. package/dist/text/embed.d.ts.map +1 -1
  243. package/dist/text/embed.js +10 -5
  244. package/dist/text/embed.js.map +1 -1
  245. package/dist/text/title.d.ts.map +1 -1
  246. package/dist/text/title.js +4 -7
  247. package/dist/text/title.js.map +1 -1
  248. package/dist/text/transcript-fold.d.ts +2 -1
  249. package/dist/text/transcript-fold.d.ts.map +1 -1
  250. package/dist/text/transcript-fold.js +21 -6
  251. package/dist/text/transcript-fold.js.map +1 -1
  252. package/package.json +28 -6
  253. package/src/chores/probes.ts +3 -1
  254. package/src/contracts/agent.contract.ts +24 -3
  255. package/src/contracts/offload.contract.ts +21 -1
  256. package/src/contracts/privacy.contract.ts +13 -0
  257. package/src/contracts/settings.contract.ts +23 -0
  258. package/src/events/agent-events.ts +13 -0
  259. package/src/events/agent-words.ts +2 -3
  260. package/src/events/resume.test.ts +14 -2
  261. package/src/events/resume.ts +32 -3
  262. package/src/events/sandbox-notice.ts +31 -1
  263. package/src/events/transcript.ts +27 -2
  264. package/src/index.ts +5 -0
  265. package/src/models/agent-catalog.test.ts +4 -3
  266. package/src/models/agent-catalog.ts +10 -7
  267. package/src/models/agent-runtimes.ts +30 -1
  268. package/src/models/capability-ledger.test.ts +4 -0
  269. package/src/models/model-order.ts +13 -0
  270. package/src/models/model-pins.ts +5 -4
  271. package/src/models/model-roles.test.ts +9 -31
  272. package/src/models/model-roles.ts +35 -50
  273. package/src/models/model-setups.test.ts +100 -0
  274. package/src/models/model-setups.ts +151 -0
  275. package/src/models/plan-pools.test.ts +58 -0
  276. package/src/models/plan-pools.ts +38 -7
  277. package/src/models/provider-specs.ts +20 -2
  278. package/src/policy/command-classes.ts +34 -3
  279. package/src/policy/credential-material.ts +58 -16
  280. package/src/policy/search-globs.ts +135 -0
  281. package/src/policy/secret-hosts.ts +7 -0
  282. package/src/policy/tests/command-classes.test.ts +35 -0
  283. package/src/policy/tests/credential-material.test.ts +20 -0
  284. package/src/policy/tests/search-globs.test.ts +46 -1
  285. package/src/policy/tests/secret-hosts.test.ts +17 -1
  286. package/src/policy/turned-away.ts +2 -0
  287. package/src/protocol/doors/runner-protocol.ts +14 -1
  288. package/src/protocol/pairing-redeem.test.ts +86 -0
  289. package/src/protocol/pairing-redeem.ts +79 -0
  290. package/src/protocol/peer-mcp-server.ts +1 -2
  291. package/src/protocol/raw/raw-json-routes.ts +3 -0
  292. package/src/protocol/raw/raw-routes.ts +5 -0
  293. package/src/protocol/sse.test.ts +50 -1
  294. package/src/protocol/sse.ts +172 -26
  295. package/src/schemas/agent.ts +23 -2
  296. package/src/schemas/automations.ts +6 -2
  297. package/src/schemas/daemon-health.test.ts +28 -0
  298. package/src/schemas/devices.ts +11 -2
  299. package/src/schemas/engines.ts +1 -1
  300. package/src/schemas/git/git-scope.test.ts +91 -0
  301. package/src/schemas/git/git-scope.ts +71 -0
  302. package/src/schemas/git/git.ts +15 -1
  303. package/src/schemas/hosts.ts +7 -0
  304. package/src/schemas/keep-warm.ts +2 -1
  305. package/src/schemas/logs.ts +25 -0
  306. package/src/schemas/metrics.test.ts +80 -0
  307. package/src/schemas/metrics.ts +31 -9
  308. package/src/schemas/needs.ts +29 -3
  309. package/src/schemas/offload.ts +76 -0
  310. package/src/schemas/privacy.ts +27 -3
  311. package/src/schemas/project-bring-back.test.ts +15 -0
  312. package/src/schemas/project-bring-back.ts +78 -0
  313. package/src/schemas/providers/handoff.test.ts +42 -0
  314. package/src/schemas/providers/handoff.ts +128 -0
  315. package/src/schemas/providers/plan-limits.ts +14 -1
  316. package/src/schemas/providers/usage.ts +13 -7
  317. package/src/schemas/run-targets.ts +5 -2
  318. package/src/schemas/settings-history.ts +28 -0
  319. package/src/schemas/settings.test.ts +74 -21
  320. package/src/schemas/settings.ts +74 -44
  321. package/src/schemas/short-codes.ts +48 -0
  322. package/src/schemas/system.ts +63 -1
  323. package/src/state/history-state.ts +4 -0
  324. package/src/state/runtime-state.test.ts +4 -0
  325. package/src/state/runtime-state.ts +4 -0
  326. package/src/state/versions.test.ts +30 -1
  327. package/src/state/versions.ts +68 -0
  328. package/src/text/embed.test.ts +14 -0
  329. package/src/text/embed.ts +19 -10
  330. package/src/text/title.ts +5 -9
  331. package/src/text/transcript-fold.test.ts +61 -0
  332. package/src/text/transcript-fold.ts +31 -6
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { RunnerCommandFrameSchema, RunnerCommandSchema } from "../protocol/doors/runner-protocol.js";
3
+ import { AgentHarnessSchema, AgentProviderSchema } from "./agent.js";
3
4
 
4
5
  // OFFLOADED WORK, as the in-sandbox `offload-run` command and the editor see it (settings `offload`): a heavy line this
5
6
  // sandbox hands a runner on one of the owner's machines instead of running it itself.
@@ -42,3 +43,78 @@ export type OffloadRecord = z.infer<typeof OffloadRecordSchema>;
42
43
  // `offload.commands` keys it) and the pattern that sorts a line into it.
43
44
  export const OffloadKindSchema = z.object({ id: z.string().min(1), pattern: z.string() });
44
45
  export type OffloadKind = z.infer<typeof OffloadKindSchema>;
46
+
47
+ // NEW SESSIONS ACROSS THE OWNER'S MACHINES (settings `offload.sessions`, GET /offload/placement). One measure for every
48
+ // machine, this sandbox and each runner alike, and one judgement over it (runners/fleet-advice.ts), so the composer's
49
+ // suggestion, the placement menu and an automatic placement can never disagree about which machine has room.
50
+
51
+ // What happens to a new session while this machine is busy and another has room: `suggest` says so above the
52
+ // composer, `auto` starts it there unless the person keeps it here, `off` says nothing.
53
+ export const SessionsPlacementSchema = z.enum(["suggest", "auto", "off"]);
54
+ export type SessionsPlacement = z.infer<typeof SessionsPlacementSchema>;
55
+
56
+ // `calm` takes another agent without anyone noticing; `busy` takes one at a cost to the agents already running; `full`
57
+ // would hold or warn a person's next turn (the turn door's own figure).
58
+ export const MachineStandingSchema = z.enum(["calm", "busy", "full"]);
59
+ export type MachineStanding = z.infer<typeof MachineStandingSchema>;
60
+
61
+ // What made a machine busy or full: memory short of what an agent needs, memory stalling, CPUs saturated, or as many
62
+ // agents running as it has slots for.
63
+ export const MachineStrainSchema = z.enum(["memory", "memory-stall", "cpu", "agents"]);
64
+ export type MachineStrain = z.infer<typeof MachineStrainSchema>;
65
+
66
+ export const MachineLoadSchema = z.object({
67
+ standing: MachineStandingSchema,
68
+ // Why it is not calm, strongest first; empty when it is.
69
+ strain: z.array(MachineStrainSchema),
70
+ cpus: z.number().int().positive(),
71
+ // The one-minute load average over the cores: 1 is every core busy.
72
+ load: z.number().nonnegative(),
73
+ // What its resource budget has free, and the limit that is measured against; absent where nothing measures it.
74
+ memoryFreeMb: z.number().int().nonnegative().optional(),
75
+ memoryLimitMb: z.number().int().positive().optional(),
76
+ // Conversations running a turn there now, and how many it holds at once (the fan-out scheduler's own slots).
77
+ agents: z.number().int().nonnegative(),
78
+ slots: z.number().int().positive(),
79
+ // The share of the machine still free by its tightest measure (slots, memory, CPU), so two machines of different
80
+ // sizes compare: 0 is none, 1 is idle.
81
+ headroom: z.number().min(0).max(1),
82
+ });
83
+ export type MachineLoad = z.infer<typeof MachineLoadSchema>;
84
+
85
+ // Why a new session could not start on a runner right now: the machine is asleep, it has never said how big it is, the
86
+ // draft's model signs in with a credential that does not travel, the privacy shield keeps it here, or the person is
87
+ // limited to areas a runner cannot fence.
88
+ export const FleetUnfitSchema = z.enum(["offline", "unmeasured", "credentials", "privacy", "areas"]);
89
+ export type FleetUnfit = z.infer<typeof FleetUnfitSchema>;
90
+
91
+ export const FleetMachineSchema = z.object({
92
+ runner: z.string().min(1),
93
+ // The machine the runner sits on, the word the owner knows it by.
94
+ name: z.string().min(1),
95
+ online: z.boolean(),
96
+ // Absent while it is offline or has never been measured.
97
+ load: MachineLoadSchema.optional(),
98
+ // Absent when a new session could start there now.
99
+ unfit: FleetUnfitSchema.optional(),
100
+ });
101
+ export type FleetMachine = z.infer<typeof FleetMachineSchema>;
102
+
103
+ export const FleetAdviceInputSchema = z.object({
104
+ // The draft's provider and harness, which decide whether its credential can reach a runner; absent reads as the
105
+ // sandbox's defaults.
106
+ agent: AgentProviderSchema.optional(),
107
+ harness: AgentHarnessSchema.optional(),
108
+ // The draft's conversation, which the privacy shield may trust on its own.
109
+ conversationId: z.string().min(1).optional(),
110
+ });
111
+ export type FleetAdviceInput = z.infer<typeof FleetAdviceInputSchema>;
112
+
113
+ export const FleetAdviceSchema = z.object({
114
+ mode: SessionsPlacementSchema,
115
+ here: MachineLoadSchema,
116
+ machines: z.array(FleetMachineSchema),
117
+ // Where a new session should start instead of here: absent while here is calm, or when no machine is better off.
118
+ suggestion: z.object({ runner: z.string().min(1), name: z.string().min(1) }).optional(),
119
+ });
120
+ export type FleetAdvice = z.infer<typeof FleetAdviceSchema>;
@@ -87,7 +87,8 @@ export const trustedInConversation = (
87
87
  policy: Pick<PrivacyShieldPolicy, "conversations">,
88
88
  provider: string,
89
89
  conversationId: string | undefined,
90
- ): boolean => conversationId !== undefined && policy.conversations.some((entry) => entry.conversationId === conversationId && entry.provider === provider);
90
+ ): boolean =>
91
+ conversationId !== undefined && policy.conversations.some((entry) => entry.conversationId === conversationId && entry.provider === provider);
91
92
 
92
93
  // The policy with that grant made or taken back. A full list drops its oldest grant to make room, since the newest is
93
94
  // the one somebody is waiting on.
@@ -104,7 +105,11 @@ export const DEFAULT_PRIVACY_SHIELD: PrivacyShieldPolicy = PrivacyShieldPolicySc
104
105
  export const PrivacyProviderSchema = z.object({
105
106
  id: z.string().describe("Provider id, as the trusted list names it."),
106
107
  label: z.string(),
107
- shieldable: z.boolean().describe("The shield can read what its runtime sends by content (through the gateway or the runtime's hooks); one it cannot is refused while the shield is on, unless trusted."),
108
+ shieldable: z
109
+ .boolean()
110
+ .describe(
111
+ "The shield can read what its runtime sends by content (through the gateway or the runtime's hooks); one it cannot is refused while the shield is on, unless trusted.",
112
+ ),
108
113
  local: z.boolean().describe("It runs on this machine, so it is trusted whatever the list says."),
109
114
  });
110
115
  export type PrivacyProvider = z.infer<typeof PrivacyProviderSchema>;
@@ -140,6 +145,10 @@ export const PrivacyReplacementSchema = z.object({
140
145
  .string()
141
146
  .describe("The masked text around the token as it left this machine (watching: as it would have). Tokens only, never a value."),
142
147
  image: z.boolean().optional().describe("Found in an image's text, so the token was painted over the picture rather than written."),
148
+ imageId: z
149
+ .string()
150
+ .optional()
151
+ .describe("The image it was painted over, as privacy.image reads it back: the picture before and after masking, while it is still kept."),
143
152
  });
144
153
  export type PrivacyReplacement = z.infer<typeof PrivacyReplacementSchema>;
145
154
 
@@ -162,6 +171,17 @@ export const PrivacyLedgerEntrySchema = z.object({
162
171
  });
163
172
  export type PrivacyLedgerEntry = z.infer<typeof PrivacyLedgerEntrySchema>;
164
173
 
174
+ // One image the shield painted over, as it was and as it left: the owner peeking at what the provider saw. The picture
175
+ // before holds the personal data, so it is kept beside the vault, off the workspace, and only the owner may read it.
176
+ export const PRIVACY_IMAGE_ID = /^[A-Za-z0-9_-]{16,64}$/u;
177
+ export const PrivacyImageSchema = z.object({ mediaType: z.string(), data: z.string().describe("Base64.") });
178
+ export const PrivacyImagePairSchema = z.object({
179
+ at: z.string().describe("When it was first painted, as an ISO timestamp."),
180
+ before: PrivacyImageSchema,
181
+ after: PrivacyImageSchema,
182
+ });
183
+ export type PrivacyImagePair = z.infer<typeof PrivacyImagePairSchema>;
184
+
165
185
  // Tokens read back to their values, for the owner checking what the shield masked; a token this vault never gave out is
166
186
  // left out of the answer.
167
187
  export const PRIVACY_REVEAL_MAX = 100;
@@ -205,7 +225,11 @@ export const PrivacyNameWordSchema = z.object({
205
225
  firstName: z.boolean().describe("A listed first name, in this form or as an inflection of one."),
206
226
  surname: z.boolean().describe("A listed surname, in this form or as an inflection of one."),
207
227
  surnameForm: z.boolean().describe("Shaped like a Polish surname (-ski, -cki, -wicz…), listed or not."),
208
- ambiguous: z.boolean().describe("Also an ordinary word in some language, so found only beside other evidence (a title, a name field) and no surname to pair a first name with."),
228
+ ambiguous: z
229
+ .boolean()
230
+ .describe(
231
+ "Also an ordinary word in some language, so found only beside other evidence (a title, a name field) and no surname to pair a first name with.",
232
+ ),
209
233
  never: z.boolean().describe("Never taken as part of a name (a title, an institution, a function word)."),
210
234
  });
211
235
  export type PrivacyNameWord = z.infer<typeof PrivacyNameWordSchema>;
@@ -0,0 +1,15 @@
1
+ import { type ProjectAnswer, ProjectAnswerSchema } from "./project-bring-back.js";
2
+
3
+ test("reads the agent's change listing as the desktop app relays it", () => {
4
+ const detail: ProjectAnswer = {
5
+ kind: "changes",
6
+ result: { ok: true, pairing: "p1", direction: "to-sandbox", changes: [{ path: "a.ts", kind: "modified", size: 3, conflict: true }], truncated: true },
7
+ };
8
+ expect(ProjectAnswerSchema.parse(detail)).toEqual(detail);
9
+ });
10
+
11
+ test("a restore's skipped entries are held to their shape, and a failure carries the agent's sentence", () => {
12
+ expect(ProjectAnswerSchema.parse({ kind: "restored", result: { ok: true, restored: 2, skipped: [{ path: "x", reason: "changed" }] } })).toMatchObject({ result: { restored: 2 } });
13
+ expect(ProjectAnswerSchema.parse({ kind: "brought-back", result: { ok: false, error: "conflict" } })).toEqual({ kind: "brought-back", result: { ok: false, error: "conflict" } });
14
+ expect(ProjectAnswerSchema.safeParse({ kind: "changes", result: { note: "x" } }).success).toBe(false);
15
+ });
@@ -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.
@@ -29,6 +30,12 @@ export const UsageWindowSchema = z.object({
29
30
  .describe(
30
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.",
31
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
+ ),
32
39
  });
33
40
  export type UsageWindow = z.infer<typeof UsageWindowSchema>;
34
41
  // A failed re-read, whatever the cause: a 429 park, a 403 asking for verification, a 5xx, a timeout. Recorded as the fact
@@ -399,8 +406,11 @@ export const ResumeRoutingSchema = z.object({
399
406
  .boolean()
400
407
  .optional()
401
408
  .describe(
402
- "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.",
403
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
+ ),
404
414
  });
405
415
  export type ResumeRouting = z.infer<typeof ResumeRoutingSchema>;
406
416
  // Returns an ordinary `StartedTurn`; the caller attaches to it like any turn someone else started. A resume note on the
@@ -477,6 +487,9 @@ export const SwitchAccountSchema = z.object({
477
487
  .describe(
478
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.",
479
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
+ ),
480
493
  run: z
481
494
  .boolean()
482
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
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
@@ -76,6 +79,28 @@ const refactorsFromChores = transform(
76
79
  (roles): JsonObject => ({ ...roles, "refactor-run": roles["maintenance-chore"] }),
77
80
  );
78
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
+
79
104
  export const SETTINGS_HISTORY = [
80
105
  // Every setting a release since 2026-08-10 had and this one does not (read off the contract lock's history):
81
106
  // retired, so passthrough stops carrying them, a definition naming one still applies, and no later setting may
@@ -125,5 +150,8 @@ export const SETTINGS_HISTORY = [
125
150
  at("modelRoles", drop("pre-push-fix")),
126
151
  // The short guidance form (2026-10-06): it lost its A/B to the full one, which every turn now gets.
127
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"]),
128
155
  at("modelRoles", refactorsFromChores),
156
+ roleListsIntoSetups,
129
157
  ] as const;