@intentic/sandbox-contract 1.176.3 → 1.209.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 (334) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-catalog.d.ts +15 -2
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +50 -1
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -0
  7. package/dist/agent-run-model.d.ts.map +1 -0
  8. package/dist/agent-run-model.js +10 -0
  9. package/dist/agent-run-model.js.map +1 -0
  10. package/dist/capability-env.d.ts +2 -0
  11. package/dist/capability-env.d.ts.map +1 -0
  12. package/dist/capability-env.js +2 -0
  13. package/dist/capability-env.js.map +1 -0
  14. package/dist/chores/chores.d.ts +9 -0
  15. package/dist/chores/chores.d.ts.map +1 -1
  16. package/dist/chores/chores.js +257 -18
  17. package/dist/chores/chores.js.map +1 -1
  18. package/dist/chores/digest.js +0 -0
  19. package/dist/chores/digest.js.map +1 -1
  20. package/dist/chores/extension-update.d.ts +9 -0
  21. package/dist/chores/extension-update.d.ts.map +1 -0
  22. package/dist/chores/extension-update.js +15 -0
  23. package/dist/chores/extension-update.js.map +1 -0
  24. package/dist/chores/fix-deps.d.ts +10 -0
  25. package/dist/chores/fix-deps.d.ts.map +1 -0
  26. package/dist/chores/fix-deps.js +18 -0
  27. package/dist/chores/fix-deps.js.map +1 -0
  28. package/dist/chores/index.d.ts +7 -2
  29. package/dist/chores/index.d.ts.map +1 -1
  30. package/dist/chores/index.js +4 -1
  31. package/dist/chores/index.js.map +1 -1
  32. package/dist/chores/probes.d.ts.map +1 -1
  33. package/dist/chores/probes.js +127 -3
  34. package/dist/chores/probes.js.map +1 -1
  35. package/dist/chores/stack.d.ts +27 -0
  36. package/dist/chores/stack.d.ts.map +1 -0
  37. package/dist/chores/stack.js +121 -0
  38. package/dist/chores/stack.js.map +1 -0
  39. package/dist/chores/verdict.d.ts +2 -1
  40. package/dist/chores/verdict.d.ts.map +1 -1
  41. package/dist/chores/verdict.js +53 -6
  42. package/dist/chores/verdict.js.map +1 -1
  43. package/dist/contract-lock.d.ts +3 -0
  44. package/dist/contract-lock.d.ts.map +1 -0
  45. package/dist/contract-lock.js +35 -0
  46. package/dist/contract-lock.js.map +1 -0
  47. package/dist/contracts/activity.contract.d.ts +10 -0
  48. package/dist/contracts/activity.contract.d.ts.map +1 -1
  49. package/dist/contracts/agent.contract.d.ts +230 -20
  50. package/dist/contracts/agent.contract.d.ts.map +1 -1
  51. package/dist/contracts/agent.contract.js +2 -1
  52. package/dist/contracts/agent.contract.js.map +1 -1
  53. package/dist/contracts/agents.contract.d.ts +847 -1
  54. package/dist/contracts/agents.contract.d.ts.map +1 -1
  55. package/dist/contracts/agents.contract.js +3 -1
  56. package/dist/contracts/agents.contract.js.map +1 -1
  57. package/dist/contracts/automations.contract.d.ts +89 -0
  58. package/dist/contracts/automations.contract.d.ts.map +1 -1
  59. package/dist/contracts/automations.contract.js +3 -1
  60. package/dist/contracts/automations.contract.js.map +1 -1
  61. package/dist/contracts/capabilities.contract.d.ts +108 -32
  62. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  63. package/dist/contracts/capabilities.contract.js +5 -1
  64. package/dist/contracts/capabilities.contract.js.map +1 -1
  65. package/dist/contracts/chores.contract.d.ts +43 -0
  66. package/dist/contracts/chores.contract.d.ts.map +1 -1
  67. package/dist/contracts/ci.contract.d.ts +5 -0
  68. package/dist/contracts/ci.contract.d.ts.map +1 -1
  69. package/dist/contracts/ci.contract.js +2 -2
  70. package/dist/contracts/ci.contract.js.map +1 -1
  71. package/dist/contracts/claude.contract.d.ts +3 -14
  72. package/dist/contracts/claude.contract.d.ts.map +1 -1
  73. package/dist/contracts/claude.contract.js +2 -3
  74. package/dist/contracts/claude.contract.js.map +1 -1
  75. package/dist/contracts/drafts.contract.d.ts +4 -0
  76. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  77. package/dist/contracts/endpoints.contract.d.ts +28 -12
  78. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  79. package/dist/contracts/endpoints.contract.js +9 -0
  80. package/dist/contracts/endpoints.contract.js.map +1 -1
  81. package/dist/contracts/extensions.contract.d.ts +399 -69
  82. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  83. package/dist/contracts/extensions.contract.js +15 -1
  84. package/dist/contracts/extensions.contract.js.map +1 -1
  85. package/dist/contracts/git.contract.d.ts +209 -11
  86. package/dist/contracts/git.contract.d.ts.map +1 -1
  87. package/dist/contracts/git.contract.js +12 -2
  88. package/dist/contracts/git.contract.js.map +1 -1
  89. package/dist/contracts/grok.contract.d.ts +0 -13
  90. package/dist/contracts/grok.contract.d.ts.map +1 -1
  91. package/dist/contracts/grok.contract.js +1 -2
  92. package/dist/contracts/grok.contract.js.map +1 -1
  93. package/dist/contracts/host.contract.d.ts +39 -0
  94. package/dist/contracts/host.contract.d.ts.map +1 -1
  95. package/dist/contracts/host.contract.js +3 -2
  96. package/dist/contracts/host.contract.js.map +1 -1
  97. package/dist/contracts/loops.contract.d.ts +343 -0
  98. package/dist/contracts/loops.contract.d.ts.map +1 -0
  99. package/dist/contracts/loops.contract.js +11 -0
  100. package/dist/contracts/loops.contract.js.map +1 -0
  101. package/dist/contracts/panels.contract.d.ts +6 -0
  102. package/dist/contracts/panels.contract.d.ts.map +1 -1
  103. package/dist/contracts/personas.contract.d.ts +112 -0
  104. package/dist/contracts/personas.contract.d.ts.map +1 -0
  105. package/dist/contracts/personas.contract.js +13 -0
  106. package/dist/contracts/personas.contract.js.map +1 -0
  107. package/dist/contracts/ports.contract.d.ts +1 -0
  108. package/dist/contracts/ports.contract.d.ts.map +1 -1
  109. package/dist/contracts/prepush.contract.d.ts +1 -0
  110. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  111. package/dist/contracts/prepush.contract.js.map +1 -1
  112. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  113. package/dist/contracts/providers.contract.d.ts.map +1 -0
  114. package/dist/contracts/providers.contract.js +6 -0
  115. package/dist/contracts/providers.contract.js.map +1 -0
  116. package/dist/contracts/public.contract.d.ts +24 -0
  117. package/dist/contracts/public.contract.d.ts.map +1 -0
  118. package/dist/contracts/public.contract.js +8 -0
  119. package/dist/contracts/public.contract.js.map +1 -0
  120. package/dist/contracts/push.contract.d.ts +3 -3
  121. package/dist/contracts/secrets.contract.d.ts +9 -0
  122. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  123. package/dist/contracts/sessions.contract.d.ts +16 -1
  124. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  125. package/dist/contracts/sessions.contract.js +1 -1
  126. package/dist/contracts/sessions.contract.js.map +1 -1
  127. package/dist/contracts/settings.contract.d.ts +352 -58
  128. package/dist/contracts/settings.contract.d.ts.map +1 -1
  129. package/dist/contracts/settings.contract.js +2 -1
  130. package/dist/contracts/settings.contract.js.map +1 -1
  131. package/dist/contracts/share.contract.d.ts +55 -0
  132. package/dist/contracts/share.contract.d.ts.map +1 -0
  133. package/dist/contracts/share.contract.js +9 -0
  134. package/dist/contracts/share.contract.js.map +1 -0
  135. package/dist/contracts/skills.contract.d.ts +41 -0
  136. package/dist/contracts/skills.contract.d.ts.map +1 -0
  137. package/dist/contracts/skills.contract.js +9 -0
  138. package/dist/contracts/skills.contract.js.map +1 -0
  139. package/dist/contracts/system.contract.d.ts +204 -8
  140. package/dist/contracts/system.contract.d.ts.map +1 -1
  141. package/dist/contracts/system.contract.js +10 -2
  142. package/dist/contracts/system.contract.js.map +1 -1
  143. package/dist/contracts/workflows.contract.d.ts +575 -0
  144. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  145. package/dist/contracts/workflows.contract.js +13 -0
  146. package/dist/contracts/workflows.contract.js.map +1 -0
  147. package/dist/contracts/workspace.contract.d.ts +18 -4
  148. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  149. package/dist/contracts/workspace.contract.js +6 -3
  150. package/dist/contracts/workspace.contract.js.map +1 -1
  151. package/dist/conversation-ids.d.ts +2 -0
  152. package/dist/conversation-ids.d.ts.map +1 -0
  153. package/dist/conversation-ids.js +113 -0
  154. package/dist/conversation-ids.js.map +1 -0
  155. package/dist/events.d.ts +523 -17
  156. package/dist/events.d.ts.map +1 -1
  157. package/dist/events.js +106 -13
  158. package/dist/events.js.map +1 -1
  159. package/dist/history-state.d.ts.map +1 -1
  160. package/dist/history-state.js +10 -0
  161. package/dist/history-state.js.map +1 -1
  162. package/dist/hostnames.d.ts +4 -0
  163. package/dist/hostnames.d.ts.map +1 -1
  164. package/dist/hostnames.js +7 -2
  165. package/dist/hostnames.js.map +1 -1
  166. package/dist/index.d.ts +3791 -478
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +39 -17
  169. package/dist/index.js.map +1 -1
  170. package/dist/listener-protocol.d.ts +77 -0
  171. package/dist/listener-protocol.d.ts.map +1 -0
  172. package/dist/listener-protocol.js +33 -0
  173. package/dist/listener-protocol.js.map +1 -0
  174. package/dist/model-order.d.ts +1 -0
  175. package/dist/model-order.d.ts.map +1 -1
  176. package/dist/model-order.js +16 -0
  177. package/dist/model-order.js.map +1 -1
  178. package/dist/output-fields.d.ts +30 -0
  179. package/dist/output-fields.d.ts.map +1 -0
  180. package/dist/output-fields.js +57 -0
  181. package/dist/output-fields.js.map +1 -0
  182. package/dist/path-refs.d.ts.map +1 -1
  183. package/dist/path-refs.js.map +1 -1
  184. package/dist/publish-drafts.d.ts +7 -0
  185. package/dist/publish-drafts.d.ts.map +1 -0
  186. package/dist/publish-drafts.js +20 -0
  187. package/dist/publish-drafts.js.map +1 -0
  188. package/dist/quick-model.d.ts +2 -1
  189. package/dist/quick-model.d.ts.map +1 -1
  190. package/dist/quick-model.js +18 -14
  191. package/dist/quick-model.js.map +1 -1
  192. package/dist/routes.d.ts +3 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +63 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/runtime-state.d.ts +33 -0
  197. package/dist/runtime-state.d.ts.map +1 -0
  198. package/dist/runtime-state.js +14 -0
  199. package/dist/runtime-state.js.map +1 -0
  200. package/dist/schemas.d.ts +4886 -851
  201. package/dist/schemas.d.ts.map +1 -1
  202. package/dist/schemas.js +993 -192
  203. package/dist/schemas.js.map +1 -1
  204. package/dist/search-globs.d.ts +6 -0
  205. package/dist/search-globs.d.ts.map +1 -0
  206. package/dist/search-globs.js +39 -0
  207. package/dist/search-globs.js.map +1 -0
  208. package/dist/share-paths.d.ts +8 -0
  209. package/dist/share-paths.d.ts.map +1 -0
  210. package/dist/share-paths.js +21 -0
  211. package/dist/share-paths.js.map +1 -0
  212. package/dist/title.d.ts.map +1 -1
  213. package/dist/title.js +1 -1
  214. package/dist/title.js.map +1 -1
  215. package/dist/tunnel-ids.d.ts +1 -0
  216. package/dist/tunnel-ids.d.ts.map +1 -1
  217. package/dist/tunnel-ids.js +1 -0
  218. package/dist/tunnel-ids.js.map +1 -1
  219. package/dist/versions.d.ts +4 -0
  220. package/dist/versions.d.ts.map +1 -0
  221. package/dist/versions.js +15 -0
  222. package/dist/versions.js.map +1 -0
  223. package/dist/workflow-faults.d.ts +4 -0
  224. package/dist/workflow-faults.d.ts.map +1 -0
  225. package/dist/workflow-faults.js +115 -0
  226. package/dist/workflow-faults.js.map +1 -0
  227. package/dist/workspace-state.d.ts +280 -1
  228. package/dist/workspace-state.d.ts.map +1 -1
  229. package/dist/workspace-state.js +163 -21
  230. package/dist/workspace-state.js.map +1 -1
  231. package/package.json +30 -6
  232. package/src/agent-catalog.test.ts +147 -7
  233. package/src/agent-catalog.ts +233 -21
  234. package/src/agent-run-model.test.ts +60 -0
  235. package/src/agent-run-model.ts +51 -0
  236. package/src/capability-env.ts +10 -0
  237. package/src/capability-ledger.test.ts +110 -0
  238. package/src/chores/chores.test.ts +32 -0
  239. package/src/chores/chores.ts +456 -32
  240. package/src/chores/digest.ts +0 -0
  241. package/src/chores/extension-update.ts +37 -0
  242. package/src/chores/fix-deps.ts +40 -0
  243. package/src/chores/index.ts +7 -2
  244. package/src/chores/probes.test.ts +155 -3
  245. package/src/chores/probes.ts +200 -4
  246. package/src/chores/stack.test.ts +126 -0
  247. package/src/chores/stack.ts +244 -0
  248. package/src/chores/verdict.test.ts +357 -19
  249. package/src/chores/verdict.ts +95 -21
  250. package/src/contract-lock.test.ts +17 -0
  251. package/src/contract-lock.ts +66 -0
  252. package/src/contracts/agent.contract.ts +6 -0
  253. package/src/contracts/agents.contract.ts +12 -0
  254. package/src/contracts/automations.contract.ts +17 -1
  255. package/src/contracts/capabilities.contract.ts +22 -0
  256. package/src/contracts/ci.contract.ts +10 -2
  257. package/src/contracts/claude.contract.ts +8 -4
  258. package/src/contracts/endpoints.contract.ts +21 -0
  259. package/src/contracts/extensions.contract.ts +42 -0
  260. package/src/contracts/git.contract.ts +33 -6
  261. package/src/contracts/grok.contract.ts +4 -3
  262. package/src/contracts/host.contract.ts +13 -2
  263. package/src/contracts/loops.contract.ts +62 -0
  264. package/src/contracts/personas.contract.ts +56 -0
  265. package/src/contracts/prepush.contract.ts +4 -3
  266. package/src/contracts/providers.contract.ts +23 -0
  267. package/src/contracts/public.contract.ts +14 -0
  268. package/src/contracts/sessions.contract.ts +5 -1
  269. package/src/contracts/settings.contract.ts +5 -0
  270. package/src/contracts/share.contract.ts +20 -0
  271. package/src/contracts/skills.contract.ts +24 -0
  272. package/src/contracts/system.contract.ts +27 -3
  273. package/src/contracts/workflows.contract.ts +78 -0
  274. package/src/contracts/workspace.contract.ts +10 -2
  275. package/src/conversation-ids.test.ts +25 -0
  276. package/src/conversation-ids.ts +160 -0
  277. package/src/events.test.ts +20 -1
  278. package/src/events.ts +404 -43
  279. package/src/history-state.ts +30 -1
  280. package/src/hostnames.ts +20 -6
  281. package/src/index.ts +58 -17
  282. package/src/listener-protocol.ts +96 -0
  283. package/src/model-order.test.ts +32 -1
  284. package/src/model-order.ts +56 -3
  285. package/src/output-fields.ts +111 -0
  286. package/src/path-refs.test.ts +4 -4
  287. package/src/path-refs.ts +5 -5
  288. package/src/publish-drafts.ts +59 -0
  289. package/src/quick-model.test.ts +70 -27
  290. package/src/quick-model.ts +76 -40
  291. package/src/routes.test.ts +91 -3
  292. package/src/routes.ts +135 -0
  293. package/src/runtime-state.test.ts +45 -0
  294. package/src/runtime-state.ts +124 -0
  295. package/src/schemas.test.ts +16 -29
  296. package/src/schemas.ts +3475 -552
  297. package/src/search-globs.test.ts +46 -0
  298. package/src/search-globs.ts +76 -0
  299. package/src/share-paths.test.ts +48 -0
  300. package/src/share-paths.ts +68 -0
  301. package/src/state-portability.ts +8 -8
  302. package/src/title.test.ts +20 -1
  303. package/src/title.ts +12 -3
  304. package/src/tunnel-ids.ts +15 -1
  305. package/src/versions.test.ts +39 -0
  306. package/src/versions.ts +48 -0
  307. package/src/workflow-faults.test.ts +127 -0
  308. package/src/workflow-faults.ts +188 -0
  309. package/src/workspace-state.test.ts +169 -15
  310. package/src/workspace-state.ts +427 -35
  311. package/dist/contracts/codex.contract.d.ts.map +0 -1
  312. package/dist/contracts/codex.contract.js +0 -6
  313. package/dist/contracts/codex.contract.js.map +0 -1
  314. package/dist/contracts/gemini.contract.d.ts +0 -16
  315. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  316. package/dist/contracts/gemini.contract.js +0 -6
  317. package/dist/contracts/gemini.contract.js.map +0 -1
  318. package/dist/contracts/kimi.contract.d.ts +0 -16
  319. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  320. package/dist/contracts/kimi.contract.js +0 -6
  321. package/dist/contracts/kimi.contract.js.map +0 -1
  322. package/dist/contracts/komodo.contract.d.ts +0 -125
  323. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  324. package/dist/contracts/komodo.contract.js +0 -14
  325. package/dist/contracts/komodo.contract.js.map +0 -1
  326. package/dist/contracts/memory.contract.d.ts +0 -34
  327. package/dist/contracts/memory.contract.d.ts.map +0 -1
  328. package/dist/contracts/memory.contract.js +0 -9
  329. package/dist/contracts/memory.contract.js.map +0 -1
  330. package/src/contracts/codex.contract.ts +0 -10
  331. package/src/contracts/gemini.contract.ts +0 -11
  332. package/src/contracts/kimi.contract.ts +0 -9
  333. package/src/contracts/komodo.contract.ts +0 -36
  334. package/src/contracts/memory.contract.ts +0 -13
package/src/events.ts CHANGED
@@ -3,9 +3,12 @@ import {
3
3
  AgentProviderSchema,
4
4
  AgentReplySchema,
5
5
  AgentSummarySchema,
6
+ FastModeStateSchema,
6
7
  LandConflictSchema,
8
+ MemberRoleSchema,
7
9
  PermissionModeSchema,
8
10
  RateLimitInfoSchema,
11
+ ShareDetailSchema,
9
12
  SubagentKindSchema,
10
13
  SubagentStatusSchema,
11
14
  UsageWindowSchema,
@@ -51,6 +54,66 @@ export const PermissionAskSchema = z.object({
51
54
  });
52
55
  export type PermissionAsk = z.infer<typeof PermissionAskSchema>;
53
56
 
57
+ /* ONE PRICED SERVICE RUN, OFFERED — the card the daemon raises when the agent asks to run a premium service
58
+ * (platform/service-offer.ts). Everything with a number on it is the PLATFORM's answer, relayed verbatim from
59
+ * the catalog it serves the daemon: the model that asked contributes `request` (the JSON it wants sent) and
60
+ * `why` (its one line of rationale), and nothing else — which is what makes the price on the card impossible
61
+ * to misquote, and the click on it the only way the run can happen. */
62
+ export const ServiceOfferSchema = z.object({
63
+ // The service, as the platform lists it: `<slug>` is what the run names, the rest is the catalog row.
64
+ slug: z.string(),
65
+ name: z.string(),
66
+ publisher: z.string(),
67
+ description: z.string(),
68
+ creditsPerRun: z.number(),
69
+ // The owner's meter as the platform stated it with the catalog — what "N left today" renders from. Absent
70
+ // when the platform sent none (it answers a meter only to a member, and membership was already checked
71
+ // before this card went up, so in practice it is present; the field stays honest about the wire).
72
+ credits: z.object({ allowance: z.number(), remaining: z.number(), resetsAt: z.string() }).optional(),
73
+ // The request body the agent wants forwarded, verbatim — shown so the owner can see what leaves.
74
+ request: z.string(),
75
+ // The agent's one-line case for spending — the only prose on the card that is the model's.
76
+ why: z.string().optional(),
77
+ });
78
+ export type ServiceOffer = z.infer<typeof ServiceOfferSchema>;
79
+
80
+ /* WHAT A SERVICE STREAMS — the provider's event vocabulary, stated once here and imported by everyone who
81
+ * touches it: the platform validates each line of a provider's NDJSON against this before relaying it, the
82
+ * daemon turns `status` events into transcript frames, and the editor renders them under the offer card.
83
+ * A run is `status` lines (each replaces the last — a spinner label, not a log) ending in exactly one
84
+ * `result`, whose `data` is the answer the agent acts on. The union is where future event kinds land when
85
+ * services start streaming richer transcript elements; today's two are the smallest honest set. */
86
+ export const ServiceStreamEventSchema = z.discriminatedUnion(`event`, [
87
+ z.object({ event: z.literal(`status`), text: z.string() }),
88
+ z.object({ event: z.literal(`result`), data: z.unknown() }),
89
+ ]);
90
+ export type ServiceStreamEvent = z.infer<typeof ServiceStreamEventSchema>;
91
+
92
+ /* ONE MISSING CAPABILITY, ASKED FOR — the card the daemon raises when the agent hits something this sandbox
93
+ * is not connected to (capabilities/capability-offer.ts). `card` names the catalog card and `name` is that
94
+ * card's own title, both resolved by the daemon from the catalog it validates the ask against — the model
95
+ * that asked contributes `why` (its one line of rationale) and nothing else, which is what makes the card
96
+ * impossible to misrepresent, and the click on it the only way anything gets connected. */
97
+ export const CapabilityOfferSchema = z.object({
98
+ // The catalog card being asked for, and how the catalog itself titles it ("Notion", "GitHub", "Docker").
99
+ card: z.string(),
100
+ name: z.string(),
101
+ // The agent's one-line case for connecting it — the only prose on the card that is the model's.
102
+ why: z.string().optional(),
103
+ });
104
+ export type CapabilityOffer = z.infer<typeof CapabilityOfferSchema>;
105
+
106
+ /* The trailer the PLATFORM appends to every relayed run stream — never provider-authored: it is the ledger
107
+ * speaking after the stream settled. `ok` means the run served and was charged (`remaining` is the meter
108
+ * after); `refunded` means the provider's stream died before its `result` and the charge was reversed. */
109
+ export const ServiceRunReceiptSchema = z.object({
110
+ event: z.literal(`receipt`),
111
+ outcome: z.enum([`ok`, `refunded`]),
112
+ credits: z.number(),
113
+ remaining: z.number().optional(),
114
+ });
115
+ export type ServiceRunReceipt = z.infer<typeof ServiceRunReceiptSchema>;
116
+
54
117
  // One provider-advertised slash command — an ACP agent's available_commands entry, or a Claude Code session's
55
118
  // supportedCommands() (its built-ins plus the workspace's own .claude/commands and any plugin/skill commands).
56
119
  // `hint` is the argument placeholder the popover shows after the name.
@@ -101,12 +164,12 @@ export type ToolCallLocation = z.infer<typeof ToolCallLocationSchema>;
101
164
  // tools (old_string/new_string) and whole-file for Write; an absent oldText means a new file / unknown
102
165
  // previous content. Sides are capped daemon-side; `truncated` marks a clipped side.
103
166
  //
104
- // `image` is a PICTURE THE TOOL PRODUCED, carried as a workspace path rather than as bytes: a browser
105
- // screenshot is already on disk under .intentic/browser/output (the artifact hook put it there), so the client
106
- // fetches it from /workspace/raw like any other file. Base64 on the wire would push a third of a megabyte
107
- // through the event stream and into the stored transcript for every screenshot, to show something the
108
- // workspace can already serve — and the path is what keeps the picture openable afterwards, which the inlined
109
- // bytes would not be. Root-relative, forward-slash: the same route space as ToolCallLocation.
167
+ // `image` is a PICTURE THE TOOL PRODUCED, carried as a workspace path rather than as bytes. Browser screenshots
168
+ // already live under .intentic/artifacts/browser, and provider-generated images are copied into
169
+ // .intentic/artifacts/imagegen, so the client fetches either from /workspace/raw like any other file. Base64 on
170
+ // the wire would bloat the event stream and every stored transcript to show bytes the workspace already serves;
171
+ // the path also keeps the picture openable afterwards. Root-relative, forward-slash: the same route space as
172
+ // ToolCallLocation.
110
173
  export const ToolCallContentSchema = z.discriminatedUnion("type", [
111
174
  z.object({ type: z.literal("text"), text: z.string() }),
112
175
  z.object({
@@ -121,9 +184,10 @@ export const ToolCallContentSchema = z.discriminatedUnion("type", [
121
184
  export type ToolCallContent = z.infer<typeof ToolCallContentSchema>;
122
185
 
123
186
  // ---- restored transcripts ----
124
- // What /sessions/{id} replays into a reopened tab. Deliberately NOT SessionTranscriptMessage (the {role,text}
125
- // seed a resumed turn carries as prompt context): this one has to REDRAW the transcript the user was looking
126
- // at, so it keeps the assistant's thinking and the tool cards its turn ran. Reconstructed from the stored
187
+ // What /sessions/{id} replays into a reopened tab, and what the daemon's own conversation record stores. It has
188
+ // to REDRAW the transcript the user was looking at rather than merely paraphrase it, so it keeps the assistant's
189
+ // thinking and the tool cards its turn ran which is also what lets a runtime handoff carry more than bare
190
+ // prose across to the replacement session (see runtime-history.ts). Reconstructed from the stored
127
191
  // tool_use/tool_result blocks, so a restored card carries everything the live `tool_call` frame did except
128
192
  // the streaming-only correlation fields.
129
193
  //
@@ -158,23 +222,98 @@ export interface RestoredToolCall {
158
222
  thinking?: string | undefined;
159
223
  }
160
224
 
225
+ /* ONE NOTE THE DAEMON PUT IN FRONT OF A USER'S MESSAGE, as both audiences see it: the model reads `text`, and
226
+ * the chat draws `title` on a collapsed row that opens to that same `text`. Shared by the live frame and the
227
+ * restored transcript so a note reads identically whether the tab watched it arrive or reopened an hour later. */
228
+ export const TurnNoteSchema = z.object({ title: z.string(), text: z.string() });
229
+ export type TurnNote = z.infer<typeof TurnNoteSchema>;
230
+
161
231
  // One restored bubble. Each stored assistant message becomes its own, which is what reproduces the live
162
232
  // interleaving — prose, the tool cards that prose introduced, then the next block of prose — rather than
163
233
  // collapsing a turn's whole narration into a single bubble with its tools hanging off the end.
164
234
  export const RestoredMessageSchema = z.object({
165
- role: z.enum(["user", "assistant"]),
235
+ /* `notice` is neither side of the conversation: it is something that HAPPENED to the turn, recorded so a
236
+ * reopened session can say it. The one that matters is a refused turn — a provider that answers "your
237
+ * organization has disabled Claude subscription access" produced no assistant text, so a transcript of the
238
+ * two speakers alone ends on the user's message and the session reads as broken. It is the same muted line
239
+ * the live client draws for the codes it does not turn red (ChatRole's `notice`). */
240
+ role: z.enum(["user", "assistant", "notice"]),
166
241
  text: z.string(),
242
+ /* WHEN THIS TURN WAS SENT, in epoch milliseconds (user rows only) — what the chat shows on the bubble it
243
+ * belongs to. The turn's START, not the moment the record was written: a turn that ran for twenty minutes
244
+ * was still sent when the user pressed send, and a stamp taken at settlement would say the conversation
245
+ * happened at the times its answers finished.
246
+ *
247
+ * Only the user's row carries one, because it is the only row whose moment the daemon actually knows. A
248
+ * turn's frames arrive with no clock of their own, so an assistant bubble could only ever be stamped with
249
+ * the whole turn's start or end — a number that says nothing about when that particular block was written.
250
+ * Rows recorded before this existed simply have none, and the chat draws nothing for them. */
251
+ sentAt: z.number().optional(),
167
252
  // Files the user attached to this turn (user bubbles only) as workspace-relative paths, recovered from
168
253
  // the stored prompt's attachment note — so a reopened tab redraws chips, not the injected protocol text.
169
254
  attachments: z.array(z.string()).optional(),
255
+ /* The checkpoint this message can be rewound to (user bubbles only), filled in when the transcript is read
256
+ * back. Not stored in the record itself — it is looked up per read from the daemon's rewind points, which
257
+ * a rewind rewrites — so a reopened tab offers exactly the turns that are still there to go back to. */
258
+ checkpointId: z.string().optional(),
170
259
  thinking: z.string().optional(),
171
260
  tools: z.array(RestoredToolCallSchema).optional(),
261
+ /* What the daemon added to this turn's message (user rows only) — the same notes the live `preamble` frame
262
+ * carries, recovered from the stored prompt when the transcript is read back. The reader that strips them out
263
+ * of the user's words is the one that hands them over here instead of dropping them on the floor.
264
+ *
265
+ * On the message rather than as a row of its own, and that is load-bearing twice: they ARE part of what was
266
+ * sent, and a record row per turn preamble would break the one-row-per-bubble correspondence a branch counts
267
+ * with (see the client's recordedRows — notices are drawn locally and never recorded). */
268
+ notes: z.array(TurnNoteSchema).optional(),
269
+ /* THE USER WROTE THIS ROW WEARING THE AGENT'S VOICE (assistant rows only) — the composer's "as agent" mode
270
+ * appending straight into the record, with no turn behind it (agents.place).
271
+ *
272
+ * The flag exists for exactly one audience: the HUMAN re-reading the transcript, whose bubble carries a
273
+ * quiet mark so that months later their own words don't pass as the agent's. The one reader that must
274
+ * never see it is the agent itself — a placed line reaches the model only through the handoff that seeds a
275
+ * fresh runtime session (agent/runtime-history.ts), which renders role and text alone, so there the line is
276
+ * indistinguishable from anything the agent genuinely said. Keep it that way: rendering this flag into any
277
+ * agent-facing text would break the feature's whole contract. */
278
+ placed: z.boolean().optional(),
172
279
  });
173
280
  export type RestoredMessage = z.infer<typeof RestoredMessageSchema>;
174
281
 
175
282
  export const SessionTranscriptSchema = z.object({ messages: z.array(RestoredMessageSchema) });
176
283
  export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId: z.string().optional() });
177
284
 
285
+ /* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED — the whole of it, baked into the page as one JSON block.
286
+ *
287
+ * A share has to keep working with nothing behind it: no daemon, no session, no sandbox that has to still be
288
+ * running when the recipient finally opens the link. So the page carries its conversation rather than fetching
289
+ * it, which also settles the security question by construction — a page with nothing to ask has no way to ask
290
+ * for something it was not given.
291
+ *
292
+ * The messages are the SAME RestoredMessage rows the app replays a reopened tab from, already filtered to the
293
+ * chosen detail level and with every picture path rewritten to the copy published beside the page. That
294
+ * sameness is the point: the shared page renders them with the app's own components, so what a recipient sees
295
+ * is what the owner saw. */
296
+ export const SharePayloadSchema = z.object({
297
+ title: z.string(),
298
+ // When the snapshot was taken, not when the conversation happened — see SharedConversation.sharedAt.
299
+ sharedAt: z.number(),
300
+ detail: ShareDetailSchema,
301
+ messages: z.array(RestoredMessageSchema),
302
+ });
303
+ export type SharePayload = z.infer<typeof SharePayloadSchema>;
304
+
305
+ /* THE THREE RESTORABLE CARDS, named so the turn journal can hold them verbatim: a parked turn's raised cards
306
+ * are written down beside its prompt (sandbox turn-journal.ts), and a daemon death under the park restores the
307
+ * very same frames instead of ending the turn `interrupted` — the card the user was about to answer survives
308
+ * the restart that killed the process holding it. The two handover cards are deliberately not among them:
309
+ * `browser_help`'s Chromium and `terminal_help`'s waiting command both die with the container, so those parks
310
+ * cannot be restored, only reported. */
311
+ const PlanCardSchema = z.object({ kind: z.literal("plan"), requestId: z.string(), text: z.string() });
312
+ const QuestionCardSchema = z.object({ kind: z.literal("question"), requestId: z.string(), questions: z.array(AskQuestionSchema) });
313
+ const PermissionCardSchema = PermissionAskSchema.extend({ kind: z.literal("permission"), requestId: z.string() });
314
+ export const ParkedCardSchema = z.discriminatedUnion("kind", [PlanCardSchema, QuestionCardSchema, PermissionCardSchema]);
315
+ export type ParkedCard = z.infer<typeof ParkedCardSchema>;
316
+
178
317
  // One frame from an agent turn, relayed to the UI. `kind`-discriminated. The daemon normalizes the SDK's
179
318
  // ~40 SDKMessage types down to this union: high-value block types get a dedicated frame
180
319
  // (delta/thinking/tool_call/tool_call_update/todos/usage/rate_limit_info/account_usage/context_usage/init/compact); any SDK message
@@ -185,21 +324,26 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId:
185
324
  // subagent itself, keyed by the same tool_use id those tagged frames carry.
186
325
  export const AgentEventSchema = z.discriminatedUnion("kind", [
187
326
  z.object({ kind: z.literal("session"), sessionId: z.string() }),
188
- /* First frame of an isolated turn: the conversation's worktree identity — its branch (agent/<id>) and the
189
- * ROOT repo's short base sha (the checkout moment). Emitted before any provider frames.
327
+ /* WHERE AN ISOLATED TURN IS STANDING: the conversation's worktree identity — its branch (agent/<id>) and
328
+ * the ROOT repo's short base sha. First frame of the turn, before any provider frames, and again each time
329
+ * the branch MOVES underneath it, which is why `base` names where the branch sits now rather than the
330
+ * moment it was checked out.
190
331
  *
191
332
  * `unenforced` marks the degraded container: no CAP_SYS_ADMIN, so the turn's worktree could not be
192
333
  * bind-mounted over the workspace root and the harness is rewriting tool paths into it instead. That
193
334
  * fallback covers what arrives as tool input and not what a subprocess computes for itself, so the
194
335
  * operator needs to know — this state used to be one line in the daemon log at boot, and the way it got
195
- * noticed was files appearing in the main tree from agents that were supposed to be on branches.
336
+ * noticed was files appearing in the main tree from agents that were supposed to be on branches. Repeated
337
+ * on every emission, because it describes the turn and a client rebuilds its standing from the last frame.
196
338
  *
197
- * `sync` reports the pre-turn rebase (agents/sync.ts), and rides here because this frame is already the
198
- * turn's "where you are standing" announcement. Present only when the branch was BEHIND the main line —
199
- * `commits` is how many main-line commits it gained, `blocked` names the repos whose rebase would not apply
200
- * and was rolled back. Both can be non-empty at once in a multi-repo composition. It is a notice and never
201
- * a question: the user is answering their agent, and the alternative to rebasing is not "stay safe" but
202
- * "conflict at land time", which interrupts them harder. */
339
+ * `sync` reports a rebase (agents/sync.ts) and rides here because this frame is already the turn's "where
340
+ * you are standing" announcement. Present only when the branch was BEHIND the main line — `commits` is how
341
+ * many main-line commits it gained, `blocked` names the repos whose rebase would not apply and was rolled
342
+ * back. Both can be non-empty at once in a multi-repo composition. Two moments produce it: before the turn
343
+ * starts, and after a card the turn parked on is answered a question or a plan approval waits minutes
344
+ * for a person, and the main line does not stop moving meanwhile. It is a notice and never a question: the
345
+ * user is answering their agent, and the alternative to rebasing is not "stay safe" but "conflict at land
346
+ * time", which interrupts them harder. */
203
347
  z.object({
204
348
  kind: z.literal("worktree"),
205
349
  branch: z.string(),
@@ -213,18 +357,47 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
213
357
  // can say whether the user's own copy is at risk or the main line simply moved on underneath the agent.
214
358
  // held ⇒ auto-land is off for this agent: nothing was applied and nothing failed — the delta is waiting
215
359
  // on the branch for a deliberate Land (landed is false, conflicts absent).
360
+ // `deps` rides along when the landed delta left the main tree declaring dependencies it does not have —
361
+ // the residue of an agent adding one without installing it, which every LATER turn would inherit through
362
+ // the overlay it mounts over the main checkout. The daemon reconciles it rather than asking anyone to
363
+ // (workspace/reconcile-deps.ts); this is the receipt, and `deferred` is the honest answer while other turns
364
+ // are still running, since an install cannot touch a tree they are mounted on.
216
365
  z.object({
217
366
  kind: z.literal("landed"),
218
367
  landed: z.boolean(),
219
368
  conflicts: z.array(LandConflictSchema).optional(),
220
369
  held: z.boolean().optional(),
370
+ deps: z.object({ missing: z.number(), started: z.array(z.string()), deferred: z.boolean() }).optional(),
221
371
  }),
372
+ /* WHAT THE DAEMON ADDED TO THE USER'S MESSAGE before the model read it — the exact words, not a summary of
373
+ * them.
374
+ *
375
+ * A turn's prompt is not only what was typed: the daemon prepends notes the model needs and the user did not
376
+ * write (agent/turn-preamble.ts owns the list — a rebase that moved the branch, dependencies that are behind,
377
+ * workspace context retrieved for this very message, where an unenforced runtime's files really live). Those
378
+ * notes change what the agent does, and for a long time the chat's only trace of any of them was one muted
379
+ * line paraphrasing the rebase — so a user watching an agent act on instructions they could not see had no
380
+ * way to find out what those instructions said. This frame is the fix: the note text verbatim, one entry per
381
+ * note, rendered collapsed so it costs a click rather than a scroll.
382
+ *
383
+ * `title` is the note's own opening header, which is what the stripper already anchors on — so the two
384
+ * cannot drift, and a note nobody thought to title cannot reach the wire unlabelled.
385
+ *
386
+ * ONE MOMENT, always: the notes went in front of the user's own message before the turn started, so they hang
387
+ * off that message and are stored on it, which is how a reopened tab still has them. Nothing is injected into
388
+ * a RUNNING turn — the rebase taken while a card sat waiting was the only thing that ever was, and it no
389
+ * longer says anything to the model at all (agent/turn-preamble.ts). */
390
+ z.object({ kind: z.literal("preamble"), notes: z.array(TurnNoteSchema) }),
222
391
  // The SDK's init handshake; carries the model it actually resolved for the turn.
223
392
  z.object({ kind: z.literal("init"), model: z.string() }),
224
393
  // The pre-turn workspace snapshot's id (the attribution-fence "user" capture), emitted once before the
225
394
  // provider stream so the client can offer "restore to before this message" on the turn's user bubble.
226
395
  // Absent on isolated turns (they snapshot nothing) and when the tree was already clean at turn start.
227
- z.object({ kind: z.literal("checkpoint"), id: z.string() }),
396
+ /* The workspace checkpoint capturing the state as this turn FOUND it — what "go back to before this
397
+ * message" restores. `index` is the message's position in the conversation's transcript, which the rewind
398
+ * route addresses it by; absent on a turn with no conversation behind it (the bench, a one-shot), where
399
+ * the id still powers a plain restore but there is no message to rewind to. */
400
+ z.object({ kind: z.literal("checkpoint"), id: z.string(), index: z.number().int().nonnegative().optional() }),
228
401
  z.object({ kind: z.literal("delta"), text: z.string(), parentToolUseId: z.string().optional() }),
229
402
  // The prose block the `delta` frames were writing is finished. A turn emits several: the model says what
230
403
  // it is about to do, runs tools, reports what it found, runs more, then summarizes — each a separate text
@@ -318,6 +491,29 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
318
491
  // The live gate: the provider's answer to "may this turn run", pushed mid-turn. Drives the rate-limited
319
492
  // notice, not the headroom readouts — see RateLimitInfoSchema.
320
493
  RateLimitInfoSchema.extend({ kind: z.literal("rate_limit_info"), account: z.string().optional() }),
494
+ /* WHAT SPEED THIS TURN ACTUALLY RAN AT, and when it isn't the one asked for, why. Emitted only when the
495
+ * answer CHANGES within a turn, so the ordinary case is one frame at init and nothing after it; a turn that
496
+ * enters cooldown mid-flight (fast mode has its own rate-limit pool, separate from the model's) emits a
497
+ * second.
498
+ *
499
+ * This frame exists because fast mode fails SILENTLY and for a lot of different reasons — the plan is free,
500
+ * extra usage is off, the model doesn't offer it, the turn is routed through the translator and so isn't
501
+ * first-party, an env var disables it, the pool is in cooldown. Asking for it and getting standard speed is
502
+ * indistinguishable, from the outside, from asking for it and getting it: same frames, same text, a bill
503
+ * that differs by 2x. A toggle whose effect can't be observed is worse than no toggle, so the daemon
504
+ * reports the harness's own answer rather than the client's assumption.
505
+ *
506
+ * `reason` is forwarded VERBATIM as the string the harness reported (SDK: FastModeDisabledReason) rather
507
+ * than re-typed as an enum here: the set is the vendor's and grows on their schedule, and a reason this
508
+ * build hasn't heard of should reach the user as an unfamiliar word, not fail schema validation and take
509
+ * the whole frame with it. The client maps the ones it knows to sentences and shows the rest as-is. */
510
+ z.object({
511
+ kind: z.literal("fast_mode"),
512
+ state: FastModeStateSchema,
513
+ // Absent when nothing is blocking fast mode — including on `state: "on"`, and on an `off` that simply
514
+ // wasn't asked for.
515
+ reason: z.string().optional(),
516
+ }),
321
517
  /* The turn is alive but WAITING on the provider: a request failed transiently (5xx, 529, a dropped socket)
322
518
  * and the harness is retrying it inside this same turn. A status, not a failure — nothing has been lost and
323
519
  * the turn may still finish normally, so the client renders it where "thinking" goes rather than in the
@@ -329,16 +525,19 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
329
525
  * actually loses the work — so the wait has to be visible, with its own next-attempt clock.
330
526
  *
331
527
  * `attempt`/`maxAttempts` are the harness's own counters; `nextAttemptAt` (epoch ms) is when it will try
332
- * again, so the readout counts down instead of freezing on a number nobody can interpret. Optional because
333
- * only Claude's harness reports the delay: Codex says which attempt it is on and nothing else
334
- * (codex-agent.ts), and inventing a countdown for it would be a clock the retry never keeps. */
528
+ * again, so the readout counts down instead of freezing on a number nobody can interpret. BOTH are optional
529
+ * for the same reason, which is that each runtime publishes a different half of the wait and none of them
530
+ * publishes all of it: Claude's harness reports the delay and the bound, Codex says which attempt it is on
531
+ * and nothing else (codex-agent.ts), OpenCode names the next instant but no bound (grok-agent.ts). Inventing
532
+ * the missing half would be a countdown, or a limit, the retry never agreed to. */
335
533
  z.object({
336
534
  kind: z.literal("provider_retry"),
337
535
  attempt: z.number(),
338
- maxAttempts: z.number(),
536
+ maxAttempts: z.number().optional(),
339
537
  nextAttemptAt: z.number().optional(),
340
- // The HTTP status behind it when there was one (529 reads as capacity, 500 as a fault the client says
341
- // which). Absent for a transport failure that never got a response.
538
+ // The HTTP status behind it when there was one (529 reads as capacity, 429 as a rate limit, 500 as a
539
+ // fault — the client says which). Absent for a transport failure that never got a response, and for a
540
+ // runtime that reports the refusal as prose rather than a code (grok-agent.ts reads it back off that).
342
541
  status: z.number().optional(),
343
542
  }),
344
543
  // Every plan-limit pool for the account that served the turn, read from the CLI's usage endpoint once the
@@ -348,10 +547,73 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
348
547
  z.object({ kind: z.literal("account_usage"), account: z.string().optional(), windows: z.array(UsageWindowSchema) }),
349
548
  ContextUsageSchema.extend({ kind: z.literal("context_usage") }),
350
549
  z.object({ kind: z.literal("compact"), trigger: z.string(), preTokens: z.number().optional(), postTokens: z.number().optional() }),
351
- // The three interactive cards. Each parks the turn until `POST /agent/reply` resolves its `requestId`.
352
- z.object({ kind: z.literal("plan"), requestId: z.string(), text: z.string() }),
353
- z.object({ kind: z.literal("question"), requestId: z.string(), questions: z.array(AskQuestionSchema) }),
354
- PermissionAskSchema.extend({ kind: z.literal("permission"), requestId: z.string() }),
550
+ // The four interactive cards. Each parks the turn until `POST /agent/reply` resolves its `requestId`.
551
+ PlanCardSchema,
552
+ QuestionCardSchema,
553
+ PermissionCardSchema,
554
+ // The agent's browser needs a person: it parked mid-sign-in on something it cannot clear itself (a captcha,
555
+ // a password it does not hold, a phone check). `session` names the browser session on /browsers — the card's
556
+ // one action is going THERE, where the live stage and Take control already are; the Browsers banner and this
557
+ // card resolve the same requestId. `account` is the capability the sign-in is for, so the card can say whose
558
+ // login is stuck even after the browser has navigated somewhere unrecognizable.
559
+ z.object({
560
+ kind: z.literal("browser_help"),
561
+ requestId: z.string(),
562
+ session: z.string(),
563
+ account: z.string(),
564
+ message: z.string(),
565
+ }),
566
+ // The agent's TERMINAL needs a person: a command it started is sitting at a prompt it cannot answer (a
567
+ // one-time password, a security-key touch, a confirm). `session` names the tmux session on the terminal
568
+ // panel — the card's one action is going THERE, where the live pane and its prompt already are, which is
569
+ // the same division of labour the browser card has with /browsers. Not journalled for restore, and for the
570
+ // browser card's reason one door along: the pane holding the prompt belongs to a process the restart kills.
571
+ z.object({
572
+ kind: z.literal("terminal_help"),
573
+ requestId: z.string(),
574
+ session: z.string(),
575
+ message: z.string(),
576
+ }),
577
+ /* A premium service run awaiting the owner's click. Raised OUTSIDE the turn generator — the daemon's
578
+ * services route parks the agent's own `services run` call and pushes this frame into the live run
579
+ * (platform/service-offer.ts) — so unlike the four cards above it is not journalled for restore: its
580
+ * waiter is the CLI's held connection, which dies with the daemon, and a restored card would offer
581
+ * buttons nothing is waiting behind. Settles through the same `POST /agent/reply` as every other card. */
582
+ z.object({ kind: z.literal("service_offer"), requestId: z.string(), offer: ServiceOfferSchema }),
583
+ /* One event off an approved run's stream, pushed as the provider emits it so the settled card shows the
584
+ * run living rather than a spinner of unknowable length. Today that is `status` lines; `result` stays off
585
+ * the transcript on purpose (it is the agent's answer to act on, not the card's to duplicate) — the frame
586
+ * carries the whole union so richer event kinds land here without a contract break. */
587
+ z.object({ kind: z.literal("service_event"), requestId: z.string(), event: ServiceStreamEventSchema }),
588
+ /* How an approved run ended, pushed after the platform answered so the card can settle as a receipt
589
+ * rather than a promise: `ok` served and charged, `refunded` failed to answer and charged nothing,
590
+ * `refused` the platform said no after the click (a raced-out allowance). `remaining` is the meter after,
591
+ * when the platform stated one. Skip needs no receipt — nothing happened, and `resolved` already says so. */
592
+ z.object({
593
+ kind: z.literal("service_receipt"),
594
+ requestId: z.string(),
595
+ outcome: z.enum(["ok", "refunded", "refused"]),
596
+ credits: z.number(),
597
+ remaining: z.number().optional(),
598
+ }),
599
+ /* A missing capability asking for the owner's setup — the agent hit something this sandbox is not
600
+ * connected to and raised the card instead of describing manual steps. Raised OUTSIDE the turn generator
601
+ * exactly like the service offer above (the daemon's ask route parks the agent's `capabilities request`
602
+ * call and pushes this frame into the live run; capabilities/capability-offer.ts), so it is not
603
+ * journalled for restore either: its waiter is the CLI's held connection, which dies with the daemon.
604
+ * Settles through the same `POST /agent/reply` as every other card. */
605
+ z.object({ kind: z.literal("capability_offer"), requestId: z.string(), offer: CapabilityOfferSchema }),
606
+ /* How an accepted ask ended, pushed once the daemon stops watching for the connection: `connected` — the
607
+ * capability came live while the agent waited (`id` is the connected instance, the agent's handle for it)
608
+ * — or `unfinished`, the setup did not complete while anyone was waiting (the deadline passed, or the
609
+ * asking command died). A skip needs no outcome frame — nothing was set up, and `resolved` already says
610
+ * so. It is what settles the card's "waiting for you to finish setup" state on every surface. */
611
+ z.object({
612
+ kind: z.literal("capability_outcome"),
613
+ requestId: z.string(),
614
+ outcome: z.enum(["connected", "unfinished"]),
615
+ id: z.string().optional(),
616
+ }),
355
617
  // The card above named by `requestId` is released — the user answered (or dismissed it, or the turn was
356
618
  // stopped out from under it), so the turn is executing again. Emitted by whoever parked, the moment its
357
619
  // waiter settles, because the park's END is otherwise invisible on this stream: nothing else here says
@@ -395,6 +657,14 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
395
657
  // nothing is coming (the turn was already a resume, or it ran on a credential with nothing to
396
658
  // re-mint from) — that is the case where reconnecting really is the fix.
397
659
  "claude-token-refused",
660
+ /* THE ACCOUNT IS FINE AND STILL NOT ALLOWED TO RUN — an Anthropic organization that has turned
661
+ * Claude Code off for this seat. The token authenticates, the plan's own usage endpoint answers
662
+ * with real pools, and every turn is refused anyway, which is why it is its own code rather than
663
+ * a member of either neighbour: a spent allowance comes back on a clock and a refused credential
664
+ * comes back on a re-mint, and NEITHER of those is true here. Only an admin re-enabling access
665
+ * is, so nothing is re-run and nothing asks the user to reconnect — the one recovery that looks
666
+ * plausible and is guaranteed to waste their time. */
667
+ "claude-not-entitled",
398
668
  // The model provider itself failed transiently — 500/502/503, a 529 at capacity, a dropped
399
669
  // socket — and the harness's own in-turn retries did not outlast it. Nothing about the workspace
400
670
  // or the request is wrong, so the daemon remembers the turn and re-runs it on an escalating
@@ -427,9 +697,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
427
697
  * that is down only spends tokens on refusals, so each attempt waits longer than the last
428
698
  * (provider-health.ts owns the schedule).
429
699
  *
430
- * `attempt`/`maxAttempts` are on the wire so the notice can say the automation is BOUNDED. An
431
- * on-by-default retry that gives no account of how long it will keep going is the kind users switch off
432
- * defensively; one that says "attempt 2 of 6" is one they leave on. */
700
+ * `attempt`/`maxAttempts` are on the wire so the notice can say the automation is BOUNDED. A retry that
701
+ * gives no account of how long it will keep going is the kind users switch back off the week they turn
702
+ * it on; one that says "attempt 2 of 6" is one they leave on. */
433
703
  outage: z.object({ retryAt: z.number(), attempt: z.number(), maxAttempts: z.number() }).optional(),
434
704
  }),
435
705
  z.object({ kind: z.literal("done") }),
@@ -456,17 +726,27 @@ export type AttachFrame = z.infer<typeof AttachFrameSchema>;
456
726
  * carries the run's prompt verbatim, and a window joining a resumed run would otherwise render the note as a
457
727
  * message the USER wrote — the same words the user already said one run up, with a machine's preamble on them.
458
728
  * Recognising the prefix is what lets that window reuse the bubble that is already there instead. */
729
+ // The instruction the three whole-turn re-runs share: what follows the note is the original request, repeated.
730
+ // `answered` deliberately does not carry it — what follows THAT note is not a repetition but the user's answer,
731
+ // and telling the model to "continue from that point instead of starting over" about words it has never seen
732
+ // is how a resume reads as the user contradicting themselves.
733
+ const REPEATED =
734
+ "The interrupted request is repeated below — where part of it was already completed in this session, continue from that point instead of starting over.";
459
735
  export const RESUME_NOTES = {
460
- auth: "The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically.",
461
- outage: "The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically.",
462
- restart: "The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back.",
736
+ auth: `The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically. ${REPEATED}`,
737
+ outage: `The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically. ${REPEATED}`,
738
+ restart: `The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back. ${REPEATED}`,
739
+ // A turn that was PARKED on the user when the daemon died: nothing re-runs at boot — the card is restored
740
+ // instead, and this is the turn their answer starts (turn-resume.ts). What rides below the note is the
741
+ // answer itself, so the model picks the session back up at exactly the decision it had handed over.
742
+ answered:
743
+ "The sandbox restarted while this conversation was waiting for the user to respond; it is back, and their response follows below — continue from where the session left off.",
463
744
  } as const;
464
745
 
465
- // The prompt a resume actually sends: the note, then why the words below are being repeated, then them.
746
+ // The prompt a resume actually sends: the note (each carries its own account of what the words below are),
747
+ // then them.
466
748
  export const withResumeNote = (prompt: string, note: string): string =>
467
- Object.values(RESUME_NOTES).some((known) => prompt.startsWith(known))
468
- ? prompt
469
- : `${note} The interrupted request is repeated below — where part of it was already completed in this session, continue from that point instead of starting over.\n\n${prompt}`;
749
+ Object.values(RESUME_NOTES).some((known) => prompt.startsWith(known)) ? prompt : `${note}\n\n${prompt}`;
470
750
 
471
751
  // The user's own words inside a resumed prompt — the note and its explanation stripped back off. Returns the
472
752
  // prompt unchanged when it is not a resume, so a caller can hand every attach head through it.
@@ -475,6 +755,42 @@ export const withoutResumeNote = (prompt: string): string => {
475
755
  return note === undefined ? prompt : prompt.slice(prompt.indexOf("\n\n") + 2);
476
756
  };
477
757
 
758
+ export type ResumeReason = keyof typeof RESUME_NOTES;
759
+
760
+ /* HOW A RESUMED TURN READS TO THE PERSON — the same interruption the note above tells the model, said in the
761
+ * transcript's own voice instead.
762
+ *
763
+ * Stripping the note out of the user's words is only half the job, and for years it was the only half anyone
764
+ * did: what a reopened conversation showed was a paragraph of machine prose stapled to the front of a message
765
+ * the user had already sent once, directly under their own copy of it. Both halves of that are wrong — it was
766
+ * never their sentence, and the words under it are a REPEAT rather than something new they said.
767
+ *
768
+ * So the two shapes below, which is the whole of what a reader has to be told:
769
+ *
770
+ * `notice` — the three whole-turn re-runs. The words under the note are already in the transcript one turn up,
771
+ * so the repeat is dropped entirely and the interruption takes its place as a muted line, sitting with the
772
+ * failure line it resolves ("Failed to authenticate…") and reading like every other thing that HAPPENED to a
773
+ * turn rather than like something anybody typed.
774
+ *
775
+ * `note` — the answered case, where what rides under the note is the user's actual answer to a card and belongs
776
+ * in the transcript as their words. Nothing is dropped; the explanation rides that message as a collapsed row,
777
+ * the same disclosure every other daemon-written note gets (TurnNote). */
778
+ export type ResumeDisclosure = { readonly kind: "notice"; readonly text: string } | { readonly kind: "note"; readonly note: TurnNote };
779
+
780
+ const RESUME_DISCLOSURES: Record<ResumeReason, ResumeDisclosure> = {
781
+ auth: { kind: "notice", text: "Claude sign-in renewed — this turn picked up where it left off." },
782
+ outage: { kind: "notice", text: "The model provider came back — this turn picked up where it left off." },
783
+ restart: { kind: "notice", text: "The sandbox came back — this turn picked up where it left off." },
784
+ answered: { kind: "note", note: { title: "Picked back up after a sandbox restart", text: RESUME_NOTES.answered } },
785
+ };
786
+
787
+ // What a stored prompt's resume note should be SHOWN as; undefined when the prompt is not a resume at all, so
788
+ // every reader of a stored prompt can ask without first testing whether it is one.
789
+ export const resumeDisclosure = (prompt: string): ResumeDisclosure | undefined => {
790
+ const reason = (Object.keys(RESUME_NOTES) as ResumeReason[]).find((key) => prompt.startsWith(RESUME_NOTES[key]));
791
+ return reason === undefined ? undefined : RESUME_DISCLOSURES[reason];
792
+ };
793
+
478
794
  // One parsed line from `intentic … --output ndjson` (engine events, provider `log`, the terminal `result`).
479
795
  // Open-ended by design — the sandbox consumes the wire shape, not @intentic/engine's types — so a string
480
796
  // `kind` plus arbitrary extra fields pass through. The apply-events tail (intentic.contract `applyEvents`) rides
@@ -537,12 +853,22 @@ export type Boot = z.infer<typeof BootSchema>;
537
853
  // the last `pnpm build:sandbox`), and that stays fully supported — the browser just compares the two sets so a
538
854
  // route the daemon predates surfaces as a named, explained gap instead of a bare 404 nobody can attribute.
539
855
  //
856
+ // `shapes` answers the half `routes` structurally cannot: a route BOTH builds have, whose payload changed
857
+ // between them. Names match, so nothing 404s — the call goes out and a field the browser expects is simply
858
+ // missing from the answer. It is a map of route name → a fingerprint of that route's input and output schema
859
+ // (see routes.ts), so a difference is a named route rather than "something, somewhere, moved". Beside `routes`
860
+ // rather than folded into it: existence covers every route, shape covers only the ones that can be expressed.
861
+ //
540
862
  // Every added field is optional: a daemon built before one simply says nothing, and the browser's fallback is
541
- // the pre-existing behaviour — routes all assumed present, the daemon assumed ready, the cache left alone.
863
+ // the pre-existing behaviour — routes all assumed present, shapes all assumed to agree, the daemon assumed
864
+ // ready, the cache left alone. That is also why `routes` keeps its bare-string-array shape: an image already in
865
+ // the wild sends exactly that, and a breaking change here would fail the hello frame's own parse and take the
866
+ // whole event stream down for precisely the skew this frame exists to describe.
542
867
  export const HelloSchema = z.object({
543
868
  kind: z.literal("hello"),
544
869
  workspaceId: z.string(),
545
870
  routes: z.array(z.string()).optional(),
871
+ shapes: z.record(z.string(), z.string()).optional(),
546
872
  build: z.string().optional(),
547
873
  boot: BootProgressSchema.optional(),
548
874
  });
@@ -562,6 +888,36 @@ export type ReposChanged = z.infer<typeof ReposChangedSchema>;
562
888
  export const WorkspaceChangedSchema = z.object({ kind: z.literal("workspaceChanged"), paths: z.array(z.string()) });
563
889
  export type WorkspaceChanged = z.infer<typeof WorkspaceChangedSchema>;
564
890
 
891
+ /* THE REPOS WHOSE REFS JUST MOVED — a commit, a checkout, a branch or tag, a rebase started or aborted.
892
+ *
893
+ * A third push for the same reason as the two above, and the reason is structural: a repo's git dir does not
894
+ * live under /work at all (it is relocated onto /history so an isolated turn's worktree can stand in for the
895
+ * workspace root — see git/repo-git-dirs.ts), and the file watcher descent-ignores `.git` besides. So no
896
+ * `workspaceChanged` path can ever say "a ref moved", and a surface built on the commit graph would otherwise
897
+ * be exactly as fresh as the last thing the user clicked.
898
+ *
899
+ * It matters most for the work the user did NOT do: the agent commits, rebases and lands out-of-band, with no
900
+ * HTTP mutation in any browser to hang an invalidation on. Ids are root-relative, "root" being the /work repo.
901
+ * Diff-not-snapshot, unlike reposChanged: this names what moved, and a repo absent from a frame is a repo that
902
+ * did not move rather than one that stopped existing. */
903
+ export const RefsChangedSchema = z.object({ kind: z.literal("refsChanged"), repos: z.array(z.string()) });
904
+ export type RefsChanged = z.infer<typeof RefsChangedSchema>;
905
+
906
+ /* WHICH RUNNING THINGS JUST MOVED — a session opened or exited, a dev server bound its port, a browser closed,
907
+ * a subagent reported in.
908
+ *
909
+ * The fourth push, and the one that covers what the other three structurally cannot: none of this state is on
910
+ * disk, so no `workspaceChanged` path can name it, and none of it is a ref or a repo. Before it, every view of a
911
+ * running thing polled on its own timer — which is to say each browser asked, forever, a question only the
912
+ * daemon could answer and almost always answered "no change".
913
+ *
914
+ * Diff-not-snapshot, and deliberately thin: the frame carries the DOMAIN that moved, never the roster itself.
915
+ * Invalidation only reaches a query something is observing, so a tab showing none of these pays a frame and no
916
+ * request — whereas a roster on the wire would bill every connected browser the full list whether or not
917
+ * anything on screen renders it. Which query keys a domain stands for is runtime-state.ts's table. */
918
+ export const RuntimeChangedSchema = z.object({ kind: z.literal("runtimeChanged"), domains: z.array(z.string()) });
919
+ export type RuntimeChanged = z.infer<typeof RuntimeChangedSchema>;
920
+
565
921
  // One connected browser tab of a sandbox member. Identity fields come from the caller's verified Google ID
566
922
  // token; activity fields from the tab's own /system/presence reports. No timestamps on the wire — an entry's
567
923
  // lifetime IS its /events connection's lifetime, so there is nothing to age out or compare clocks over.
@@ -571,6 +927,9 @@ export const PresenceUserSchema = z.object({
571
927
  email: z.string(),
572
928
  name: z.string().optional(),
573
929
  picture: z.string().optional(),
930
+ // The caller's trust tier, resolved by the authorizer at connection time. On the roster so every member
931
+ // can see who may do what — and so a tab knows its OWN role without an owner-only lookup.
932
+ role: MemberRoleSchema,
574
933
  idle: z.boolean(),
575
934
  // Route/view name the tab is on ("workspace", "automations", "ext:<id>/<key>", …).
576
935
  view: z.string().optional(),
@@ -596,14 +955,16 @@ export const AgentsSchema = z.object({ kind: z.literal("agents"), agents: z.arra
596
955
  export type Agents = z.infer<typeof AgentsSchema>;
597
956
 
598
957
  // The /events stream union: the hello identity frame, then liveness heartbeats interleaved with boot progress,
599
- // workspace-change batches, repo-set snapshots, and presence + fleet roster snapshots. oRPC validates every
600
- // yielded frame against this, so all kinds must live here.
958
+ // workspace-change batches, repo-set snapshots, ref-move batches, runtime-domain nudges, and presence + fleet
959
+ // roster snapshots. oRPC validates every yielded frame against this, so all kinds must live here.
601
960
  export const SystemEventSchema = z.discriminatedUnion("kind", [
602
961
  HelloSchema,
603
962
  HeartbeatSchema,
604
963
  BootSchema,
605
964
  WorkspaceChangedSchema,
606
965
  ReposChangedSchema,
966
+ RefsChangedSchema,
967
+ RuntimeChangedSchema,
607
968
  PresenceSchema,
608
969
  AgentsSchema,
609
970
  ]);