@intentic/sandbox-contract 1.240.0 → 1.242.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 (310) hide show
  1. package/README.md +52 -16
  2. package/dist/agent-catalog.d.ts +5 -28
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +21 -157
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -2
  7. package/dist/agent-run-model.d.ts.map +1 -1
  8. package/dist/agent-run-model.js +9 -6
  9. package/dist/agent-run-model.js.map +1 -1
  10. package/dist/agent-runtimes.d.ts +27 -0
  11. package/dist/agent-runtimes.d.ts.map +1 -0
  12. package/dist/agent-runtimes.js +113 -0
  13. package/dist/agent-runtimes.js.map +1 -0
  14. package/dist/approvals-execution.d.ts +11 -0
  15. package/dist/approvals-execution.d.ts.map +1 -0
  16. package/dist/approvals-execution.js +44 -0
  17. package/dist/approvals-execution.js.map +1 -0
  18. package/dist/arrival.d.ts +151 -0
  19. package/dist/arrival.d.ts.map +1 -0
  20. package/dist/arrival.js +56 -0
  21. package/dist/arrival.js.map +1 -0
  22. package/dist/card-status.d.ts +5 -0
  23. package/dist/card-status.d.ts.map +1 -0
  24. package/dist/card-status.js +74 -0
  25. package/dist/card-status.js.map +1 -0
  26. package/dist/chores/chores.d.ts.map +1 -1
  27. package/dist/chores/chores.js +1 -1
  28. package/dist/chores/chores.js.map +1 -1
  29. package/dist/command-classes.d.ts +0 -1
  30. package/dist/command-classes.d.ts.map +1 -1
  31. package/dist/command-classes.js +9 -2
  32. package/dist/command-classes.js.map +1 -1
  33. package/dist/command-run.d.ts +10 -0
  34. package/dist/command-run.d.ts.map +1 -0
  35. package/dist/command-run.js +46 -0
  36. package/dist/command-run.js.map +1 -0
  37. package/dist/contracts/agent.contract.d.ts +991 -771
  38. package/dist/contracts/agent.contract.d.ts.map +1 -1
  39. package/dist/contracts/agents.contract.d.ts +345 -337
  40. package/dist/contracts/agents.contract.d.ts.map +1 -1
  41. package/dist/contracts/agents.contract.js +20 -2
  42. package/dist/contracts/agents.contract.js.map +1 -1
  43. package/dist/contracts/approvals.contract.d.ts +99 -0
  44. package/dist/contracts/approvals.contract.d.ts.map +1 -0
  45. package/dist/contracts/approvals.contract.js +32 -0
  46. package/dist/contracts/approvals.contract.js.map +1 -0
  47. package/dist/contracts/ci.contract.d.ts +1 -4
  48. package/dist/contracts/ci.contract.d.ts.map +1 -1
  49. package/dist/contracts/ci.contract.js +1 -9
  50. package/dist/contracts/ci.contract.js.map +1 -1
  51. package/dist/contracts/claude.contract.d.ts +9 -0
  52. package/dist/contracts/claude.contract.d.ts.map +1 -1
  53. package/dist/contracts/cursor.contract.d.ts +6 -0
  54. package/dist/contracts/cursor.contract.d.ts.map +1 -1
  55. package/dist/contracts/git.contract.d.ts +63 -1
  56. package/dist/contracts/git.contract.d.ts.map +1 -1
  57. package/dist/contracts/git.contract.js +22 -4
  58. package/dist/contracts/git.contract.js.map +1 -1
  59. package/dist/contracts/grok.contract.d.ts +3 -0
  60. package/dist/contracts/grok.contract.d.ts.map +1 -1
  61. package/dist/contracts/keys.contract.d.ts +81 -0
  62. package/dist/contracts/keys.contract.d.ts.map +1 -0
  63. package/dist/contracts/keys.contract.js +51 -0
  64. package/dist/contracts/keys.contract.js.map +1 -0
  65. package/dist/contracts/panels.contract.d.ts +7 -0
  66. package/dist/contracts/panels.contract.d.ts.map +1 -1
  67. package/dist/contracts/prepush.contract.js +2 -2
  68. package/dist/contracts/providers.contract.d.ts +2 -0
  69. package/dist/contracts/providers.contract.d.ts.map +1 -1
  70. package/dist/contracts/runner.contract.d.ts +84 -86
  71. package/dist/contracts/runner.contract.d.ts.map +1 -1
  72. package/dist/contracts/safety.contract.d.ts +35 -0
  73. package/dist/contracts/safety.contract.d.ts.map +1 -0
  74. package/dist/contracts/safety.contract.js +32 -0
  75. package/dist/contracts/safety.contract.js.map +1 -0
  76. package/dist/contracts/sessions.contract.d.ts +73 -337
  77. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  78. package/dist/contracts/settings.contract.d.ts +30 -32
  79. package/dist/contracts/settings.contract.d.ts.map +1 -1
  80. package/dist/contracts/system.contract.d.ts +139 -337
  81. package/dist/contracts/system.contract.d.ts.map +1 -1
  82. package/dist/contracts/translator.contract.d.ts +31 -3
  83. package/dist/contracts/translator.contract.d.ts.map +1 -1
  84. package/dist/contracts/usage.contract.d.ts +29 -20
  85. package/dist/contracts/usage.contract.d.ts.map +1 -1
  86. package/dist/contracts/usage.contract.js +13 -0
  87. package/dist/contracts/usage.contract.js.map +1 -1
  88. package/dist/contracts/workspace.contract.d.ts +8 -0
  89. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  90. package/dist/conversation-ids.d.ts +2 -0
  91. package/dist/conversation-ids.d.ts.map +1 -1
  92. package/dist/conversation-ids.js +8 -0
  93. package/dist/conversation-ids.js.map +1 -1
  94. package/dist/credential-material.d.ts +1 -0
  95. package/dist/credential-material.d.ts.map +1 -1
  96. package/dist/credential-material.js +24 -2
  97. package/dist/credential-material.js.map +1 -1
  98. package/dist/definition.d.ts +51 -93
  99. package/dist/definition.d.ts.map +1 -1
  100. package/dist/definition.js +6 -28
  101. package/dist/definition.js.map +1 -1
  102. package/dist/events.d.ts +1831 -2285
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +155 -55
  105. package/dist/events.js.map +1 -1
  106. package/dist/history-state.d.ts.map +1 -1
  107. package/dist/history-state.js +1 -0
  108. package/dist/history-state.js.map +1 -1
  109. package/dist/index.d.ts +1959 -1903
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +20 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/ingress-protocol.d.ts +13 -1
  114. package/dist/ingress-protocol.d.ts.map +1 -1
  115. package/dist/ingress-protocol.js +19 -0
  116. package/dist/ingress-protocol.js.map +1 -1
  117. package/dist/mentions.d.ts +3 -0
  118. package/dist/mentions.d.ts.map +1 -0
  119. package/dist/mentions.js +13 -0
  120. package/dist/mentions.js.map +1 -0
  121. package/dist/needs-action.d.ts +7 -0
  122. package/dist/needs-action.d.ts.map +1 -0
  123. package/dist/needs-action.js +3 -0
  124. package/dist/needs-action.js.map +1 -0
  125. package/dist/overlay-lint.d.ts +7 -0
  126. package/dist/overlay-lint.d.ts.map +1 -0
  127. package/dist/overlay-lint.js +68 -0
  128. package/dist/overlay-lint.js.map +1 -0
  129. package/dist/owner-ticket.d.ts +18 -0
  130. package/dist/owner-ticket.d.ts.map +1 -0
  131. package/dist/owner-ticket.js +51 -0
  132. package/dist/owner-ticket.js.map +1 -0
  133. package/dist/plan-pools.d.ts +12 -0
  134. package/dist/plan-pools.d.ts.map +1 -0
  135. package/dist/plan-pools.js +30 -0
  136. package/dist/plan-pools.js.map +1 -0
  137. package/dist/provider-specs.d.ts +228 -0
  138. package/dist/provider-specs.d.ts.map +1 -0
  139. package/dist/provider-specs.js +124 -0
  140. package/dist/provider-specs.js.map +1 -0
  141. package/dist/quick-model.d.ts.map +1 -1
  142. package/dist/quick-model.js +2 -1
  143. package/dist/quick-model.js.map +1 -1
  144. package/dist/runner-protocol.js +2 -2
  145. package/dist/runner-protocol.js.map +1 -1
  146. package/dist/runtime-state.d.ts +2 -2
  147. package/dist/runtime-state.d.ts.map +1 -1
  148. package/dist/runtime-state.js +1 -1
  149. package/dist/runtime-state.js.map +1 -1
  150. package/dist/safety-policy.d.ts +49 -0
  151. package/dist/safety-policy.d.ts.map +1 -0
  152. package/dist/safety-policy.js +52 -0
  153. package/dist/safety-policy.js.map +1 -0
  154. package/dist/schemas/agent.d.ts +21 -3
  155. package/dist/schemas/agent.d.ts.map +1 -1
  156. package/dist/schemas/agent.js +18 -8
  157. package/dist/schemas/agent.js.map +1 -1
  158. package/dist/schemas/agents.d.ts +19 -0
  159. package/dist/schemas/agents.d.ts.map +1 -1
  160. package/dist/schemas/agents.js +15 -0
  161. package/dist/schemas/agents.js.map +1 -1
  162. package/dist/schemas/approvals.d.ts +241 -0
  163. package/dist/schemas/approvals.d.ts.map +1 -0
  164. package/dist/schemas/approvals.js +63 -0
  165. package/dist/schemas/approvals.js.map +1 -0
  166. package/dist/schemas/automations.d.ts +5 -0
  167. package/dist/schemas/automations.d.ts.map +1 -1
  168. package/dist/schemas/ci.d.ts +5 -9
  169. package/dist/schemas/ci.d.ts.map +1 -1
  170. package/dist/schemas/ci.js +3 -10
  171. package/dist/schemas/ci.js.map +1 -1
  172. package/dist/schemas/computers.d.ts +31 -5
  173. package/dist/schemas/computers.d.ts.map +1 -1
  174. package/dist/schemas/computers.js +7 -5
  175. package/dist/schemas/computers.js.map +1 -1
  176. package/dist/schemas/environment.d.ts +11 -131
  177. package/dist/schemas/environment.d.ts.map +1 -1
  178. package/dist/schemas/environment.js +5 -42
  179. package/dist/schemas/environment.js.map +1 -1
  180. package/dist/schemas/git-history.d.ts +4 -0
  181. package/dist/schemas/git-history.d.ts.map +1 -1
  182. package/dist/schemas/git.d.ts +179 -0
  183. package/dist/schemas/git.d.ts.map +1 -1
  184. package/dist/schemas/git.js +39 -1
  185. package/dist/schemas/git.js.map +1 -1
  186. package/dist/schemas/panels.d.ts +21 -0
  187. package/dist/schemas/panels.d.ts.map +1 -1
  188. package/dist/schemas/panels.js +5 -0
  189. package/dist/schemas/panels.js.map +1 -1
  190. package/dist/schemas/plan-limits.d.ts +50 -3
  191. package/dist/schemas/plan-limits.d.ts.map +1 -1
  192. package/dist/schemas/plan-limits.js +11 -6
  193. package/dist/schemas/plan-limits.js.map +1 -1
  194. package/dist/schemas/provider-oauth.d.ts +6 -0
  195. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  196. package/dist/schemas/provider-subscriptions.d.ts +2 -1
  197. package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
  198. package/dist/schemas/provider-subscriptions.js +2 -1
  199. package/dist/schemas/provider-subscriptions.js.map +1 -1
  200. package/dist/schemas/settings.d.ts +29 -16
  201. package/dist/schemas/settings.d.ts.map +1 -1
  202. package/dist/schemas/settings.js +13 -20
  203. package/dist/schemas/settings.js.map +1 -1
  204. package/dist/schemas/system.d.ts +3 -0
  205. package/dist/schemas/system.d.ts.map +1 -1
  206. package/dist/schemas/system.js +5 -1
  207. package/dist/schemas/system.js.map +1 -1
  208. package/dist/schemas/workspace-repos.d.ts +14 -0
  209. package/dist/schemas/workspace-repos.d.ts.map +1 -1
  210. package/dist/schemas/workspace-repos.js +5 -0
  211. package/dist/schemas/workspace-repos.js.map +1 -1
  212. package/dist/schemas/workspace-tree.d.ts +1 -0
  213. package/dist/schemas/workspace-tree.d.ts.map +1 -1
  214. package/dist/schemas/workspace-tree.js +3 -0
  215. package/dist/schemas/workspace-tree.js.map +1 -1
  216. package/dist/transcript-fold.d.ts +31 -0
  217. package/dist/transcript-fold.d.ts.map +1 -0
  218. package/dist/transcript-fold.js +457 -0
  219. package/dist/transcript-fold.js.map +1 -0
  220. package/dist/workspace-state.d.ts +20 -3
  221. package/dist/workspace-state.d.ts.map +1 -1
  222. package/dist/workspace-state.js +22 -2
  223. package/dist/workspace-state.js.map +1 -1
  224. package/package.json +27 -5
  225. package/src/agent-catalog.test.ts +1 -1
  226. package/src/agent-catalog.ts +76 -555
  227. package/src/agent-run-model.test.ts +31 -15
  228. package/src/agent-run-model.ts +26 -12
  229. package/src/agent-runtimes.ts +419 -0
  230. package/src/approvals-execution.ts +96 -0
  231. package/src/arrival.ts +160 -0
  232. package/src/capability-ledger.test.ts +6 -4
  233. package/src/card-status.ts +93 -0
  234. package/src/chores/chores.ts +6 -3
  235. package/src/command-classes.test.ts +25 -0
  236. package/src/command-classes.ts +47 -5
  237. package/src/command-run.ts +78 -0
  238. package/src/contract-lock.test.ts +1 -1
  239. package/src/contract-lock.ts +2 -2
  240. package/src/contracts/agents.contract.ts +35 -2
  241. package/src/contracts/approvals.contract.ts +37 -0
  242. package/src/contracts/ci.contract.ts +0 -13
  243. package/src/contracts/git.contract.ts +28 -3
  244. package/src/contracts/keys.contract.ts +79 -0
  245. package/src/contracts/prepush.contract.ts +3 -3
  246. package/src/contracts/safety.contract.ts +48 -0
  247. package/src/contracts/usage.contract.ts +22 -0
  248. package/src/conversation-ids.test.ts +31 -1
  249. package/src/conversation-ids.ts +37 -3
  250. package/src/credential-material.test.ts +69 -1
  251. package/src/credential-material.ts +86 -5
  252. package/src/definition.ts +25 -53
  253. package/src/events.ts +374 -173
  254. package/src/history-state.ts +6 -0
  255. package/src/index.ts +24 -5
  256. package/src/ingress-protocol.ts +52 -1
  257. package/src/mentions.test.ts +22 -0
  258. package/src/mentions.ts +25 -0
  259. package/src/needs-action.ts +14 -0
  260. package/src/overlay-lint.test.ts +89 -0
  261. package/src/overlay-lint.ts +116 -0
  262. package/src/owner-ticket.test.ts +54 -0
  263. package/src/owner-ticket.ts +102 -0
  264. package/src/plan-pools.test.ts +71 -0
  265. package/src/plan-pools.ts +82 -0
  266. package/src/provider-specs.test.ts +130 -0
  267. package/src/provider-specs.ts +352 -0
  268. package/src/quick-model.ts +9 -5
  269. package/src/runner-protocol.ts +2 -2
  270. package/src/runtime-state.ts +6 -6
  271. package/src/safety-policy.ts +165 -0
  272. package/src/schemas/agent.ts +62 -17
  273. package/src/schemas/agents.ts +71 -0
  274. package/src/schemas/approvals.ts +174 -0
  275. package/src/schemas/ci.ts +19 -26
  276. package/src/schemas/computers.ts +52 -11
  277. package/src/schemas/environment.ts +21 -100
  278. package/src/schemas/git.ts +121 -6
  279. package/src/schemas/issues.ts +1 -1
  280. package/src/schemas/panels.ts +20 -0
  281. package/src/schemas/plan-limits.ts +58 -11
  282. package/src/schemas/provider-subscriptions.ts +7 -2
  283. package/src/schemas/settings.ts +67 -93
  284. package/src/schemas/system.ts +12 -1
  285. package/src/schemas/workspace-repos.ts +9 -0
  286. package/src/schemas/workspace-tree.ts +11 -0
  287. package/src/transcript-fold.test.ts +493 -0
  288. package/src/transcript-fold.ts +655 -0
  289. package/src/workspace-state.test.ts +77 -29
  290. package/src/workspace-state.ts +99 -31
  291. package/dist/contracts/drafts.contract.d.ts +0 -57
  292. package/dist/contracts/drafts.contract.d.ts.map +0 -1
  293. package/dist/contracts/drafts.contract.js +0 -32
  294. package/dist/contracts/drafts.contract.js.map +0 -1
  295. package/dist/definition-action.d.ts +0 -7
  296. package/dist/definition-action.d.ts.map +0 -1
  297. package/dist/definition-action.js +0 -3
  298. package/dist/definition-action.js.map +0 -1
  299. package/dist/publish-drafts.d.ts +0 -7
  300. package/dist/publish-drafts.d.ts.map +0 -1
  301. package/dist/publish-drafts.js +0 -20
  302. package/dist/publish-drafts.js.map +0 -1
  303. package/dist/schemas/drafts.d.ts +0 -84
  304. package/dist/schemas/drafts.d.ts.map +0 -1
  305. package/dist/schemas/drafts.js +0 -44
  306. package/dist/schemas/drafts.js.map +0 -1
  307. package/src/contracts/drafts.contract.ts +0 -35
  308. package/src/definition-action.ts +0 -8
  309. package/src/publish-drafts.ts +0 -59
  310. package/src/schemas/drafts.ts +0 -108
package/src/events.ts CHANGED
@@ -3,7 +3,7 @@ import { AgentHarnessSchema, AgentProviderSchema, PermissionModeSchema } from ".
3
3
  import { AgentSummarySchema, LandConflictSchema } from "./schemas/agents.js";
4
4
  import { RateLimitInfoSchema } from "./schemas/claude-gate.js";
5
5
  import { FastModeStateSchema } from "./schemas/fast-mode.js";
6
- import { AgentReplySchema, UsageWindowSchema } from "./schemas/plan-limits.js";
6
+ import { AccountUsageSchema, AgentReplySchema, ProviderRefusalSchema, UsageWindowSchema } from "./schemas/plan-limits.js";
7
7
  import { ShareDetailSchema } from "./schemas/share.js";
8
8
  import { MemberRoleSchema } from "./schemas/shared.js";
9
9
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "./schemas/terminal.js";
@@ -45,7 +45,11 @@ export type AskQuestion = z.infer<typeof AskQuestionSchema>;
45
45
  export const ProgramAskSchema = z.object({
46
46
  text: z.string().describe("What would run."),
47
47
  language: z.enum(["bash", "javascript"]).describe("Which of the two backends it is written for, named as the grammar that colours it."),
48
- truncated: z.boolean().describe("Whether this is the head of a longer program, so the card can say so instead of ending mid-word."),
48
+ truncated: z
49
+ .boolean()
50
+ .describe(
51
+ "Whether this is an excerpt of a longer program, so the card can say so instead of ending mid-word. An excerpt always carries the flagged fragment: the beginning, then a window around the fragment, with any skipped middle written into the text as a bracketed count.",
52
+ ),
49
53
  spans: z
50
54
  .array(z.object({ start: z.number().int().nonnegative(), end: z.number().int().nonnegative() }))
51
55
  .describe("Which fragments of the text put it in the class that held it. Offsets into text, in order, never overlapping."),
@@ -76,11 +80,16 @@ export const PermissionAskSchema = z.object({
76
80
  program: ProgramAskSchema.optional().describe(
77
81
  "The program this card is holding, when the card is about one. Present on a command gate's card and absent on every other permission ask.",
78
82
  ),
83
+ /* THE JUDGE'S OWN SENTENCE, and on a command card it is the reason the card exists rather than a note added
84
+ * to it: the judge read the owner's policy and the program and decided this needed asking, and this is what
85
+ * it decided. Written by the quick model from the program text and the policy, never by the agent being
86
+ * gated — a card whose persuasive half was authored by the thing it is stopping argues for its own approval,
87
+ * and the turns that raise cards are exactly the ones whose account of themselves may be a stranger's. */
79
88
  explain: z
80
89
  .string()
81
90
  .optional()
82
91
  .describe(
83
- "One plain sentence saying what the program does and why the agent wants it. Written by the quick model, never by the agent being gated, and only when the owner switched the setting on.",
92
+ "One plain sentence saying what the program does and why it is being asked about. Written by the judge that read your safety policy, never by the agent being gated.",
84
93
  ),
85
94
  });
86
95
  export type PermissionAsk = z.infer<typeof PermissionAskSchema>;
@@ -312,7 +321,7 @@ export type CardDocument = z.infer<typeof CardDocumentSchema>;
312
321
 
313
322
  /* ONE CARD'S OWN FIELDS, spelled once. Three readers carry the same card and must agree on what it is: the
314
323
  * frame that raises it (AgentEventSchema below), the journal entry that keeps a parked one across a restart
315
- * (ParkedCardSchema), and the record row that keeps it for good (RestoredMessageSchema's card fields). A shape
324
+ * (ParkedCardSchema), and the record row that keeps it for good (TranscriptRowSchema's card fields). A shape
316
325
  * declared inline in each was three shapes with one name. */
317
326
  const REQUEST_ID = z.string().describe("What to send back when you answer.");
318
327
  const planCard = {
@@ -395,72 +404,83 @@ const PermissionCardSchema = PermissionAskSchema.extend({
395
404
  export const ParkedCardSchema = z.discriminatedUnion("kind", [PlanCardSchema, QuestionCardSchema, PermissionCardSchema]);
396
405
  export type ParkedCard = z.infer<typeof ParkedCardSchema>;
397
406
 
398
- // ---- restored cards ----
399
- /* THE CARDS A TURN PARKED ON, as the record keeps them: the card exactly as it was raised, and the reply that
400
- * released it exactly as the client sent it (the `resolved` frame's own payload), plus whatever landed on the
401
- * card afterwards (a permission's late explanation, an offer's stream and receipt).
402
- *
403
- * They exist because the record used to keep NONE of this. A question the user answered was on screen for as
404
- * long as the turn's frame log lived (minutes) and in the browser's local mirror for as long as that survived
405
- * (until the next web build), and then the record repainted the conversation without it: the card, the
406
- * questions, and the user's own picks, gone from the only durable copy. The rule the record follows is that a
407
- * reopened chat redraws what was on screen, and a decision the user made is the part of a conversation they
408
- * come back to re-read.
407
+ // ---- transcript cards ----
408
+ /* THE CARDS A TURN PARKED ON, as a transcript row carries them: the card exactly as it was raised, how it was
409
+ * settled, and whatever landed on it afterwards (a permission's late explanation, an offer's stream and
410
+ * receipt). One shape for the live row and the recorded one, because they are the same row: the daemon folds
411
+ * the turn's frames into these rows as they stream (transcript-fold.ts) and writes the same rows down when the
412
+ * turn settles, so a chat reopened tomorrow is the chat that was on screen.
409
413
  *
410
- * The REPLY rides verbatim rather than as a derived status, for the same reason the `resolved` frame carries
411
- * it that way: the client already turns a reply into the card's frozen state for the live stream, and a
412
- * restored card goes through that one function too, so the two can never disagree about what "answered"
413
- * looks like. Absent, nobody answered (the turn was stopped, or died under the card), which is not a decision
414
- * and must not replay as one. */
415
- const settled = {
416
- reply: AgentReplySchema.optional().describe(
417
- "How it was answered, exactly as the client sent it. Absent when nobody did: the turn was stopped or died under the card, which is not a decision and does not read back as one.",
418
- ),
419
- };
420
- export const RestoredPlanSchema = z.object({ ...planCard, ...settled });
421
- export type RestoredPlan = z.infer<typeof RestoredPlanSchema>;
422
- export const RestoredQuestionSchema = z.object({ ...questionCard, ...settled });
423
- export type RestoredQuestion = z.infer<typeof RestoredQuestionSchema>;
424
- // `explain`, the quick model's late sentence (the `permission_note` frame), lands here through PermissionAskSchema.
425
- export const RestoredPermissionSchema = PermissionAskSchema.extend({ ...permissionCard, ...settled });
426
- export type RestoredPermission = z.infer<typeof RestoredPermissionSchema>;
427
- export const RestoredBrowserHelpSchema = z.object({ ...browserHelpCard, ...settled });
428
- export type RestoredBrowserHelp = z.infer<typeof RestoredBrowserHelpSchema>;
429
- export const RestoredTerminalHelpSchema = z.object({ ...terminalHelpCard, ...settled });
430
- export type RestoredTerminalHelp = z.infer<typeof RestoredTerminalHelpSchema>;
431
- export const RestoredServiceOfferSchema = z.object({
414
+ * The STATUS is settled by the fold, from the reply that released the card (card-status.ts), and rides the
415
+ * row rather than the reply it came from: every reader wants the verdict, and the one derivation lives beside
416
+ * the fold that applies it. `pending` is a card the turn is still parked on; `cancelled` is nobody answering,
417
+ * the turn stopped or died under the card, which is not a decision and does not read back as one. */
418
+ export const PlanStatusSchema = z.enum(["pending", "approved", "rejected", "cancelled"]);
419
+ export type PlanStatus = z.infer<typeof PlanStatusSchema>;
420
+ export const QuestionStatusSchema = z.enum(["pending", "answered", "cancelled"]);
421
+ export type QuestionStatus = z.infer<typeof QuestionStatusSchema>;
422
+ export const PermissionStatusSchema = z.enum(["pending", "allowed", "always", "denied", "cancelled"]);
423
+ export type PermissionStatus = z.infer<typeof PermissionStatusSchema>;
424
+ export const HelpStatusSchema = z.enum(["pending", "helped", "declined", "cancelled"]);
425
+ export type HelpStatus = z.infer<typeof HelpStatusSchema>;
426
+ export const OfferStatusSchema = z.enum(["pending", "approved", "skipped", "cancelled"]);
427
+ export type OfferStatus = z.infer<typeof OfferStatusSchema>;
428
+ // A yes settles the DECISION, not the ask: the owner is now setting the capability up, so the card moves to
429
+ // `connecting` and stays there until the capability_outcome frame says how the setup ended.
430
+ export const CapabilityOfferStatusSchema = z.enum(["pending", "connecting", "skipped", "cancelled"]);
431
+ export type CapabilityOfferStatus = z.infer<typeof CapabilityOfferStatusSchema>;
432
+
433
+ export const TranscriptPlanSchema = z.object({ ...planCard, status: PlanStatusSchema.describe("Where the decision stands.") });
434
+ export type TranscriptPlan = z.infer<typeof TranscriptPlanSchema>;
435
+ export const TranscriptQuestionSchema = z.object({
436
+ ...questionCard,
437
+ status: QuestionStatusSchema.describe("Where the answer stands."),
438
+ answers: z
439
+ .record(z.string(), z.array(z.string()))
440
+ .optional()
441
+ .describe("What was chosen, keyed by the question, with the chosen labels or the user's own words."),
442
+ });
443
+ export type TranscriptQuestion = z.infer<typeof TranscriptQuestionSchema>;
444
+ // `explain`, the judge's sentence, lands here through PermissionAskSchema; it is on the card from the moment
445
+ // it is raised, so nothing patches it in afterwards.
446
+ export const TranscriptPermissionSchema = PermissionAskSchema.extend({ ...permissionCard, status: PermissionStatusSchema.describe("Where the decision stands.") });
447
+ export type TranscriptPermission = z.infer<typeof TranscriptPermissionSchema>;
448
+ export const TranscriptBrowserHelpSchema = z.object({ ...browserHelpCard, status: HelpStatusSchema.describe("How the hand-over ended.") });
449
+ export type TranscriptBrowserHelp = z.infer<typeof TranscriptBrowserHelpSchema>;
450
+ export const TranscriptTerminalHelpSchema = z.object({ ...terminalHelpCard, status: HelpStatusSchema.describe("How the hand-over ended.") });
451
+ export type TranscriptTerminalHelp = z.infer<typeof TranscriptTerminalHelpSchema>;
452
+ export const TranscriptServiceOfferSchema = z.object({
432
453
  ...serviceOfferCard,
433
- ...settled,
454
+ status: OfferStatusSchema.describe("Where the decision stands."),
434
455
  events: z.array(ServiceStreamEventSchema).optional().describe("The approved run's stream, in order (the service_event frames)."),
435
456
  receipt: ServiceReceiptSchema.optional().describe("How the approved run ended (the service_receipt frame)."),
436
457
  });
437
- export type RestoredServiceOffer = z.infer<typeof RestoredServiceOfferSchema>;
438
- export const RestoredCapabilityOfferSchema = z.object({
458
+ export type TranscriptServiceOffer = z.infer<typeof TranscriptServiceOfferSchema>;
459
+ export const TranscriptCapabilityOfferSchema = z.object({
439
460
  ...capabilityOfferCard,
440
- ...settled,
461
+ status: CapabilityOfferStatusSchema.describe("Where the decision stands."),
441
462
  outcome: CapabilityOutcomeSchema.optional().describe("How an accepted ask's setup ended (the capability_outcome frame)."),
442
463
  });
443
- export type RestoredCapabilityOffer = z.infer<typeof RestoredCapabilityOfferSchema>;
444
- export const RestoredPaymentOfferSchema = z.object({
464
+ export type TranscriptCapabilityOffer = z.infer<typeof TranscriptCapabilityOfferSchema>;
465
+ export const TranscriptPaymentOfferSchema = z.object({
445
466
  ...paymentOfferCard,
446
- ...settled,
467
+ status: OfferStatusSchema.describe("Where the decision stands."),
447
468
  receipt: PaymentReceiptSchema.optional().describe("How the approved payment ended (the payment_receipt frame)."),
448
469
  });
449
- export type RestoredPaymentOffer = z.infer<typeof RestoredPaymentOfferSchema>;
470
+ export type TranscriptPaymentOffer = z.infer<typeof TranscriptPaymentOfferSchema>;
450
471
 
451
- // ---- restored transcripts ----
452
- // What /sessions/{id} replays into a reopened tab, and what the daemon's own conversation record stores. It has
453
- // to REDRAW the transcript the user was looking at rather than merely paraphrase it, so it keeps the assistant's
454
- // thinking and the tool cards its turn ran, which is also what lets a runtime handoff carry more than bare
455
- // prose across to the replacement session (see runtime-history.ts). Reconstructed from the stored
456
- // tool_use/tool_result blocks, so a restored card carries everything the live `tool_call` frame did except
457
- // the streaming-only correlation fields.
472
+ // ---- transcript rows ----
473
+ // What a conversation is made of, on every surface: the rows the daemon folds a turn's frames into as they
474
+ // stream (the live chat renders these, patched as they grow), the rows the record keeps once the turn settles,
475
+ // and the rows /agents/{id}/transcript replays into a reopened tab. One shape because it is one thing: a
476
+ // reopened chat REDRAWS the transcript the user was looking at rather than paraphrasing it, so a row keeps the
477
+ // assistant's thinking and the tool cards its block ran, which is also what lets a runtime handoff carry more
478
+ // than bare prose across to a replacement session (see runtime-history.ts).
458
479
  //
459
- // One restored tool card. A subagent's own calls and its thinking nest under the Agent card that spawned them,
460
- // the same two fields (and the same recursion) the live ChatTool carries, so a reopened chat redraws the
461
- // delegation it was showing instead of a leaf card with the whole child collapsed into its result text.
462
- // z.lazy because the shape refers to itself: a subagent that delegates nests one level deeper.
463
- export const RestoredToolCallSchema: z.ZodType<RestoredToolCall> = z.lazy(() =>
480
+ // One tool card. A subagent's own calls and its thinking nest under the Agent card that spawned them, so a
481
+ // delegation reads as one unit instead of a flat run of siblings. z.lazy because the shape refers to itself: a
482
+ // subagent that delegates nests one level deeper.
483
+ export const TranscriptToolSchema: z.ZodType<TranscriptTool> = z.lazy(() =>
464
484
  z.object({
465
485
  id: z.string().describe("The call's id."),
466
486
  name: z.string().describe("Which tool."),
@@ -472,17 +492,40 @@ export const RestoredToolCallSchema: z.ZodType<RestoredToolCall> = z.lazy(() =>
472
492
  locations: z.array(ToolCallLocationSchema).optional().describe("The files it touched."),
473
493
  content: z.array(ToolCallContentSchema).optional().describe("What it produced: text, a change to a file, or a picture."),
474
494
  children: z
475
- .array(RestoredToolCallSchema)
495
+ .array(TranscriptToolSchema)
476
496
  .optional()
477
497
  .describe(
478
498
  "Calls a delegated helper made, nested under the call that started it, so a reopened conversation redraws the delegation rather than collapsing it into one result.",
479
499
  ),
480
500
  thinking: z.string().optional().describe("What the agent was reasoning about around this call."),
501
+ subagent: TranscriptSubagentSchema.optional().describe(
502
+ "The helper this call started, as the daemon's registry sees it: what it is, how it is going, what it has spent. What a card can say about a backgrounded child whose result is minutes away.",
503
+ ),
481
504
  }),
482
505
  );
483
- // Mutable, unlike most of this file: both builders settle a card IN PLACE when its result arrives turns later
484
- // (restoredTurn's `cards` map, readWorkspaceSession's `awaiting`), which is what saves them a second pass.
485
- export interface RestoredToolCall {
506
+ /* THE CHILD A CALL STARTED, on the card whose id the `subagent`/`subagent_update` frames name (that call's own),
507
+ * so no correlation is needed: an Agent card wears its subagent's live state, and a Bash card that turned out
508
+ * to be a `codex exec` wears its delegate's. The identifying fields arrive once (the `subagent` frame), the
509
+ * moving ones (status, spend, what it is doing) replace as each update lands. */
510
+ export const TranscriptSubagentSchema = z.object({
511
+ kind: SubagentKindSchema,
512
+ agentType: z.string().optional(),
513
+ description: z.string().optional(),
514
+ model: z.string().optional(),
515
+ provider: z.string().optional(),
516
+ background: z.boolean().optional(),
517
+ status: SubagentStatusSchema,
518
+ tokens: z.number().optional(),
519
+ toolUses: z.number().optional(),
520
+ lastTool: z.string().optional(),
521
+ summary: z.string().optional(),
522
+ error: z.string().optional(),
523
+ verification: SubagentVerificationSchema.optional(),
524
+ });
525
+ export type TranscriptSubagent = z.infer<typeof TranscriptSubagentSchema>;
526
+ // Mutable, unlike most of this file: the fold settles a card IN PLACE when its result arrives turns later
527
+ // (transcript-fold.ts's `cards` map, readWorkspaceSession's `awaiting`), which is what saves it a second pass.
528
+ export interface TranscriptTool {
486
529
  id: string;
487
530
  name: string;
488
531
  category: ToolKind;
@@ -490,8 +533,9 @@ export interface RestoredToolCall {
490
533
  target?: string | undefined;
491
534
  locations?: ToolCallLocation[] | undefined;
492
535
  content?: ToolCallContent[] | undefined;
493
- children?: RestoredToolCall[] | undefined;
536
+ children?: TranscriptTool[] | undefined;
494
537
  thinking?: string | undefined;
538
+ subagent?: TranscriptSubagent | undefined;
495
539
  }
496
540
 
497
541
  /* ONE NOTE THE DAEMON PUT IN FRONT OF A USER'S MESSAGE, as both audiences see it: the model reads `text`, and
@@ -503,15 +547,26 @@ export const TurnNoteSchema = z.object({
503
547
  });
504
548
  export type TurnNote = z.infer<typeof TurnNoteSchema>;
505
549
 
506
- // One restored bubble. Each stored assistant message becomes its own, which is what reproduces the live
507
- // interleaving, prose, the tool cards that prose introduced, then the next block of prose, rather than
508
- // collapsing a turn's whole narration into a single bubble with its tools hanging off the end.
509
- export const RestoredMessageSchema = z.object({
510
- /* `notice` is neither side of the conversation: it is something that HAPPENED to the turn, recorded so a
511
- * reopened session can say it. The one that matters is a refused turn, a provider that answers "your
512
- * organization has disabled Claude subscription access" produced no assistant text, so a transcript of the
513
- * two speakers alone ends on the user's message and the session reads as broken. It is the same muted line
514
- * the live client draws for the codes it does not turn red (ChatRole's `notice`). */
550
+ // End-of-turn accounting (assistant rows only, the last bubble of a turn): what the turn cost, attached where
551
+ // the answer ended so a reader can see what each exchange spent.
552
+ export const TranscriptUsageSchema = z.object({
553
+ costUsd: z.number().optional(),
554
+ inputTokens: z.number().optional(),
555
+ outputTokens: z.number().optional(),
556
+ durationMs: z.number().optional(),
557
+ numTurns: z.number().optional(),
558
+ });
559
+ export type TranscriptUsage = z.infer<typeof TranscriptUsageSchema>;
560
+
561
+ // One row. Each block of the agent's prose is its own, with the tool cards that block introduced, which is what
562
+ // reproduces the way a turn actually unfolded rather than collapsing its whole narration into one bubble with
563
+ // every tool hanging off the end.
564
+ export const TranscriptRowSchema = z.object({
565
+ /* `notice` is neither side of the conversation: it is something that HAPPENED to the turn, a refusal, a
566
+ * landed delta, a compaction, a stop, written down so a reopened conversation says it too. The one that
567
+ * matters most is a refused turn: a provider that answers "your organization has disabled Claude
568
+ * subscription access" produced no assistant text, so a transcript of the two speakers alone ends on the
569
+ * user's message and reads as broken. */
515
570
  role: z
516
571
  .enum(["user", "assistant", "notice"])
517
572
  .describe(
@@ -533,28 +588,36 @@ export const RestoredMessageSchema = z.object({
533
588
  .describe(
534
589
  "When it was sent, in milliseconds. On the user's rows only, because that is the only moment actually known: a turn's own frames arrive with no clock, so stamping the agent's rows could only ever mean the whole turn's start or end.",
535
590
  ),
536
- // Files the user attached to this turn (user bubbles only) as workspace-relative paths, recovered from
537
- // the stored prompt's attachment note, so a reopened tab redraws chips, not the injected protocol text.
591
+ // Files the user attached to this turn (user rows only) as workspace-relative paths, the uploads alone:
592
+ // a path @-mentioned inline in the text is already visible there and is not drawn as a chip.
538
593
  attachments: z.array(z.string()).optional().describe("Files attached to this message, as workspace paths."),
539
- /* The checkpoint this message can be rewound to (user bubbles only), filled in when the transcript is read
540
- * back. Not stored in the record itself, it is looked up per read from the daemon's rewind points, which
541
- * a rewind rewrites, so a reopened tab offers exactly the turns that are still there to go back to. */
594
+ /* The checkpoint this message can be rewound to (user rows only), and where this message sits in the
595
+ * conversation's record, which is what the rewind route addresses it by. Never stored: both are stamped
596
+ * onto the live row by the turn's own `checkpoint` frame and onto a replayed row by the read that serves
597
+ * it, looked up from the daemon's rewind points, which a rewind rewrites, so a reopened tab offers exactly
598
+ * the turns that are still there to go back to. */
542
599
  checkpointId: z
543
600
  .string()
544
601
  .optional()
545
602
  .describe(
546
603
  "The saved point this message can be rewound to. Looked up on each read rather than stored, so what is offered is exactly what is still there to go back to.",
547
604
  ),
605
+ rewindIndex: z
606
+ .number()
607
+ .int()
608
+ .nonnegative()
609
+ .optional()
610
+ .describe("This message's position in the conversation's record, which is how a rewind names it. Present only beside a checkpoint."),
548
611
  thinking: z.string().optional().describe("What the agent was reasoning about."),
549
- tools: z.array(RestoredToolCallSchema).optional().describe("The tool calls this part of the turn made."),
612
+ tools: z.array(TranscriptToolSchema).optional().describe("The tool calls this part of the turn made."),
613
+ todos: z.array(TodoItemSchema).optional().describe("The agent's task checklist, as of this bubble."),
614
+ usage: TranscriptUsageSchema.optional().describe("What the turn cost, on the bubble its answer ended in."),
550
615
  /* What the daemon added to this turn's message (user rows only), the same notes the live `preamble` frame
551
- * carries. A daemon-recorded turn takes them straight off that frame in its own log, typed end to end
552
- * (sessions/turn-transcript.ts); only a conversation adopted from a provider's session store recovers them
553
- * by parsing the composed prompt kept there, the one store the daemon never wrote typed.
616
+ * carries, read off that frame by the fold.
554
617
  *
555
618
  * On the message rather than as a row of its own, and that matters twice: they ARE part of what was
556
619
  * sent, and a record row per turn preamble would break the one-row-per-bubble correspondence a branch counts
557
- * with (see the client's recordedRows, notices are drawn locally and never recorded). */
620
+ * with. */
558
621
  notes: z
559
622
  .array(TurnNoteSchema)
560
623
  .optional()
@@ -576,62 +639,118 @@ export const RestoredMessageSchema = z.object({
576
639
  .describe(
577
640
  "A person wrote this in the agent's voice, with no turn behind it. Marked for the human re-reading the conversation months later, so their own words do not pass as the agent's. The agent itself never sees the mark.",
578
641
  ),
579
- /* THE ONE-PRESS OFFER A RECORDED NOTICE CARRIES (notice rows only), named rather than inferred from its
580
- * words. Only `tierHold` today: the line saying this turn ran on a cheaper model, whose offer is "keep this
581
- * chat on my pick".
582
- *
583
- * It is on the wire because the offer has to survive a reopen, and a reopened tab has only the record. The
584
- * chat's other one-press notices are drawn live and never recorded, so they never needed this; a routed turn
585
- * is different precisely because the whole point of recording it is that somebody reads it LATER. A KIND, not
586
- * a callback, exactly as the live ones are: the reader decides what the press does and whether the offer
587
- * still stands (a chat already holding its pick shows a settled sentence, not a stale button). */
642
+ /* THE ONE-PRESS OFFER A NOTICE CARRIES (notice rows only), named rather than inferred from its words: the
643
+ * landed notice's "keep future work on the branch", the outage notice's "stop resuming these by itself",
644
+ * the terminal a dependency install the daemon just started is running in, and the routed turn's "keep
645
+ * this chat on my pick". A KIND, not a callback: the chat decides what the press does and whether the
646
+ * offer still stands (a chat already holding its pick shows a settled sentence, not a stale button). */
588
647
  noticeAction: z
589
- .enum(["tierHold"])
648
+ .enum(["landHold", "outageOptOut", "depsInstall", "tierHold"])
590
649
  .optional()
591
- .describe("A one-press follow-up this recorded notice offers, by name. The chat decides what it does and whether it still applies."),
650
+ .describe("A one-press follow-up this notice offers, by name. The chat decides what it does and whether it still applies."),
651
+ /* A WAIT THIS NOTICE DESCRIBES that had not finished when it was written (notice rows only): the chat draws
652
+ * a spinner over it while the wait is on, and the plain line once it is over. A KIND rather than a boolean
653
+ * because whether the wait is STILL running is a fact about the conversation now, not about a row in a
654
+ * record: the reader pairs the kind with the live state that answers it. */
655
+ noticeWait: z.enum(["credentialRenewal"]).optional().describe("The wait this notice describes, by name, so a reader can say whether it is still on."),
592
656
  /* THE CARD THIS BUBBLE PARKED ON (assistant rows only), at most one: a card closes the bubble it lands in,
593
- * live (turnReducer nulls the turn's bubble) and in the fold alike (sessions/turn-transcript.ts), so the
594
- * next thing the agent says opens a fresh row beneath it. One field per kind rather than one union field,
595
- * because that is the shape the live ChatMessage has and a restored row is meant to be indistinguishable
596
- * from the one it replaces. See the restored-cards section above for why these exist at all. */
597
- plan: RestoredPlanSchema.optional().describe("The plan this row asked approval for, and the answer."),
598
- question: RestoredQuestionSchema.optional().describe("The questions this row asked, and the picks that answered them."),
599
- permission: RestoredPermissionSchema.optional().describe("The tool this row asked permission for, and the decision."),
600
- browserHelp: RestoredBrowserHelpSchema.optional().describe("The browser hand-over this row asked for, and how it ended."),
601
- terminalHelp: RestoredTerminalHelpSchema.optional().describe("The terminal hand-over this row asked for, and how it ended."),
602
- serviceOffer: RestoredServiceOfferSchema.optional().describe("The priced service run this row offered, the decision, and the receipt."),
603
- capabilityOffer: RestoredCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
604
- paymentOffer: RestoredPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
657
+ * so the next thing the agent says opens a fresh row beneath it. One field per kind rather than one union
658
+ * field, so a reader reaches the card it draws by name. */
659
+ plan: TranscriptPlanSchema.optional().describe("The plan this row asked approval for, and the answer."),
660
+ question: TranscriptQuestionSchema.optional().describe("The questions this row asked, and the picks that answered them."),
661
+ permission: TranscriptPermissionSchema.optional().describe("The tool this row asked permission for, and the decision."),
662
+ browserHelp: TranscriptBrowserHelpSchema.optional().describe("The browser hand-over this row asked for, and how it ended."),
663
+ terminalHelp: TranscriptTerminalHelpSchema.optional().describe("The terminal hand-over this row asked for, and how it ended."),
664
+ serviceOffer: TranscriptServiceOfferSchema.optional().describe("The priced service run this row offered, the decision, and the receipt."),
665
+ capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
666
+ paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
605
667
  });
606
- export type RestoredMessage = z.infer<typeof RestoredMessageSchema>;
668
+ export type TranscriptRow = z.infer<typeof TranscriptRowSchema>;
607
669
 
608
670
  /* THE CARD FIELDS A ROW CAN CARRY, as one list, for every reader that has to ask "does this row hold a card":
609
- * the fold that counts a card-only bubble as a row (sessions/turn-transcript.ts), the client's own row count
610
- * (recordedRows), which must agree with it to the row or a branch is cut in the wrong place, and the client's
611
- * restore, which turns each into its live card. The live ChatMessage names its cards exactly this way, so the
612
- * list is the same list on both sides rather than two that have to be kept in step. */
613
- export const RESTORED_CARD_FIELDS = [
614
- "plan",
615
- "question",
616
- "permission",
617
- "browserHelp",
618
- "terminalHelp",
619
- "serviceOffer",
620
- "capabilityOffer",
621
- "paymentOffer",
622
- ] as const;
623
- export type RestoredCardField = (typeof RESTORED_CARD_FIELDS)[number];
624
- export type RestoredCards = Pick<RestoredMessage, RestoredCardField>;
625
- // Whether a row holds a card at all, the question the row counts on both sides ask.
626
- export const holdsCard = (message: RestoredCards): boolean => RESTORED_CARD_FIELDS.some((field) => message[field] !== undefined);
671
+ * the fold that counts a card-only bubble as a row, the chat's row count (a branch is cut by it), and the
672
+ * surfaces that draw whichever card a bubble is waiting on. */
673
+ export const CARD_FIELDS = ["plan", "question", "permission", "browserHelp", "terminalHelp", "serviceOffer", "capabilityOffer", "paymentOffer"] as const;
674
+ export type CardField = (typeof CARD_FIELDS)[number];
675
+ export type TranscriptCards = Pick<TranscriptRow, CardField>;
676
+ // Whether a row holds a card at all, answered or not.
677
+ export const holdsCard = (row: TranscriptCards): boolean => CARD_FIELDS.some((field) => row[field] !== undefined);
678
+ // Whether a row is holding the turn open on a card nobody has answered.
679
+ export const isAwaitingDecision = (row: TranscriptCards): boolean => CARD_FIELDS.some((field) => row[field]?.status === "pending");
680
+
681
+ /* ONE CHANGE TO A RUN'S ROWS, what the attach stream carries while a turn runs. The daemon folds each frame
682
+ * into its rows (transcript-fold.ts) and says what moved, so a client keeps rows, never frames: it applies
683
+ * these to the list it holds and draws it. `index` counts from the run's first row, which the attach head
684
+ * places in the conversation.
685
+ *
686
+ * Prose and thinking arrive as APPENDS to a row rather than as the row again, so the chat can type them out at
687
+ * the pace they are written; a tool card arrives whole (`tool`, by id, replacing an earlier copy of the same
688
+ * id wherever it nests), because its updates are snapshots already; everything else replaces its row. `drop`
689
+ * is the one removal: an assistant row opened for a block that then wrote nothing. */
690
+ export const TranscriptPatchSchema = z.discriminatedUnion("op", [
691
+ z.object({ op: z.literal("append").describe("A new row at the end."), row: TranscriptRowSchema }),
692
+ z.object({ op: z.literal("replace").describe("This row, whole, in place of the one at that index."), index: z.number().int().nonnegative(), row: TranscriptRowSchema }),
693
+ z.object({ op: z.literal("drop").describe("The row at that index is gone: it was opened and never written into."), index: z.number().int().nonnegative() }),
694
+ z.object({ op: z.literal("text").describe("More of the agent's prose, onto that row's text."), index: z.number().int().nonnegative(), text: z.string() }),
695
+ z.object({ op: z.literal("thinking").describe("More of the agent's reasoning, onto that row's thinking."), index: z.number().int().nonnegative(), text: z.string() }),
696
+ z.object({
697
+ op: z.literal("tool").describe("A tool card, whole: new, or the latest state of one already there, matched by id wherever it nests."),
698
+ index: z.number().int().nonnegative(),
699
+ tool: TranscriptToolSchema,
700
+ parent: z.string().optional().describe("The card this one nests under, when it is a delegated helper's own call."),
701
+ }),
702
+ ]);
703
+ export type TranscriptPatch = z.infer<typeof TranscriptPatchSchema>;
627
704
 
628
705
  export const SessionTranscriptSchema = z.object({
629
706
  messages: z
630
- .array(RestoredMessageSchema)
707
+ .array(TranscriptRowSchema)
631
708
  .describe(
632
709
  "The conversation, in order. Each block of the agent's prose is its own message with the tools that block introduced, which is what reproduces the way it actually unfolded.",
633
710
  ),
634
711
  });
712
+ /* HOW A TURN THAT LEFT WORK BEHIND ENDED, as the daemon has it, for whoever asks and however long after.
713
+ *
714
+ * One shape for every ending that leaves finished work behind a live session, because they are one situation
715
+ * with one answer (a press) and they differ only in what can honestly be said about WHEN and what the press
716
+ * DOES. The client folds this straight into its pick-up state (chat/pickUp.ts), which is why the field names
717
+ * line up: a second vocabulary between the two halves is how they came to disagree in the first place.
718
+ *
719
+ * IT USED TO BE A BOOLEAN, and the boolean is what left the longest wait uncovered. One flag could only say
720
+ * "a Stop, or a daemon killed under it", so a spent allowance, the one ending that reliably OUTLIVES the
721
+ * window that hit it, reached a reopened tab as nothing at all: no strip, no countdown, no press, and the user
722
+ * typing the word by hand hours later. It could not say more without these three facts, and each of them
723
+ * changes what the surface may promise:
724
+ *
725
+ * · `resetsAt` is the only honest "not before this" any ending knows, and it is the whole of what a chat
726
+ * reopened the next morning wants to be told;
727
+ * · `held` is what makes the press cheap. The daemon keeps the refused turn whole, so continuing RE-RUNS it
728
+ * and adds nothing to the conversation; without this the same press appends a message reading "Continue",
729
+ * which is exactly the transcript pollution the press exists to prevent, and `ran` separates a turn that
730
+ * got somewhere from one the allowance refused at the door (two different sentences);
731
+ * · `scheduled` says somebody else is already bringing this turn back, so the surface REPORTS a wait instead
732
+ * of offering one, and no local automation races the daemon's own pass for it. */
733
+ export const TurnEndingSchema = z.object({
734
+ reason: z
735
+ .enum(["stopped", "limit", "outage"])
736
+ .describe(
737
+ "Which ending left the work here: a Stop or a daemon killed under the turn, a spent usage allowance, or a provider that refused it.",
738
+ ),
739
+ resetsAt: z
740
+ .number()
741
+ .optional()
742
+ .describe("When the spent allowance reopens, in epoch seconds. Absent for every ending that names no instant, and for a provider that publishes none."),
743
+ held: z
744
+ .object({ ran: z.boolean().describe("Whether the held turn got anywhere before it was refused, which is a different sentence from one refused at the door.") })
745
+ .optional()
746
+ .describe("Present when the daemon still holds the refused turn whole, so a press re-runs it rather than appending a message after it."),
747
+ scheduled: z
748
+ .boolean()
749
+ .optional()
750
+ .describe("Whether something other than the user is already booked to send this turn again, so the surface reports the wait instead of offering a press."),
751
+ });
752
+ export type TurnEnding = z.infer<typeof TurnEndingSchema>;
753
+
635
754
  /* THE RECORD A REOPENED TAB IS REBUILT FROM: the messages, plus what the session behind them is BOUND to.
636
755
  *
637
756
  * A provider session is minted on one runtime under one credential, and it resumes only there, so a client
@@ -641,7 +760,7 @@ export const SessionTranscriptSchema = z.object({
641
760
  * is what made switching BACK to the account that minted it announce a fresh session and then retire a
642
761
  * perfectly resumable one, spending the whole transcript again on a cold prompt cache.
643
762
  *
644
- * All optional, and absent together on a conversation that has no session to resume. */
763
+ * The session fields are all optional, and absent together on a conversation that has no session to resume. */
645
764
  export const AgentTranscriptSchema = SessionTranscriptSchema.extend({
646
765
  sessionId: z.string().optional().describe("The provider session behind the last turn, when there is one."),
647
766
  provider: AgentProviderSchema.optional().describe("Which provider minted that session."),
@@ -650,6 +769,19 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({
650
769
  .string()
651
770
  .optional()
652
771
  .describe("Which stored account it belongs to, as the daemon resolved it. Absent when no stored account paid for the turn."),
772
+ /* AND HOW THE LAST TURN ENDED, for the endings that leave the client something to OFFER rather than
773
+ * something to draw: work half done behind a session that is perfectly alive, where the only thing missing
774
+ * is somebody saying carry on.
775
+ *
776
+ * It rides the transcript because the offer used to ride the WINDOW. A chat armed the continue press from
777
+ * the stream it was watching when the turn stopped, so the press existed only where somebody had been
778
+ * looking: stop an agent from the board with its chat closed, or reopen the tab on another device, or after
779
+ * a reload that dropped the tab, and the same stopped session came back with no way on but typing the word
780
+ * by hand, which is precisely what the press exists to spare. The daemon is the one party that knows this
781
+ * about a conversation whoever asks and however long after, so it is the one that says it. */
782
+ ending: TurnEndingSchema.optional().describe(
783
+ "How the last turn ended, when it left work behind that one press finishes. Absent for a conversation whose last turn ended on its own, and for the failures that name something to repair first.",
784
+ ),
653
785
  });
654
786
 
655
787
  /* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED, the whole of it, baked into the page as one JSON block.
@@ -659,7 +791,7 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({
659
791
  * it, which also settles the security question by construction, a page with nothing to ask has no way to ask
660
792
  * for something it was not given.
661
793
  *
662
- * The messages are the SAME RestoredMessage rows the app replays a reopened tab from, already filtered to the
794
+ * The messages are the SAME TranscriptRow rows the app replays a reopened tab from, already filtered to the
663
795
  * chosen detail level and with every picture path rewritten to the copy published beside the page. That
664
796
  * sameness is the point: the shared page renders them with the app's own components, so what a recipient sees
665
797
  * is what the owner saw. */
@@ -668,7 +800,7 @@ export const SharePayloadSchema = z.object({
668
800
  // When the snapshot was taken, not when the conversation happened, see SharedConversation.sharedAt.
669
801
  sharedAt: z.number(),
670
802
  detail: ShareDetailSchema,
671
- messages: z.array(RestoredMessageSchema),
803
+ messages: z.array(TranscriptRowSchema),
672
804
  });
673
805
  export type SharePayload = z.infer<typeof SharePayloadSchema>;
674
806
 
@@ -676,9 +808,8 @@ export type SharePayload = z.infer<typeof SharePayloadSchema>;
676
808
  // ~40 SDKMessage types down to this union: high-value block types get a dedicated frame
677
809
  // (delta/thinking/tool_call/tool_call_update/todos/usage/rate_limit_info/account_usage/context_usage/init/compact); any SDK message
678
810
  // without a UI mapping is dropped. `plan`/`question`/`permission` pause the turn until the user answers on the
679
- // `POST /agent/reply` side channel, and `resolved` releases the one it names; `permission_note` adds a sentence
680
- // to a permission card already on screen without settling it; `mode` reports the live permission posture as the
681
- // agent changes it.
811
+ // `POST /agent/reply` side channel, and `resolved` releases the one it names; `mode` reports the live
812
+ // permission posture as the agent changes it.
682
813
  // `parentToolUseId` tags frames produced inside a subagent (Task tool); `subagent`/`subagent_update` report the
683
814
  // subagent itself, keyed by the same tool_use id those tagged frames carry.
684
815
  export const AgentEventSchema = z.discriminatedUnion("kind", [
@@ -1048,19 +1179,12 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
1048
1179
  // already moved on. It rides verbatim, exactly as the client POSTed it; absent, nobody answered (the turn
1049
1180
  // was stopped, or died under the card), which is not a decision and must not replay as one.
1050
1181
  z.object({ kind: z.literal("resolved"), requestId: z.string(), reply: AgentReplySchema.optional() }),
1051
- /* A LATE SENTENCE FOR A CARD ALREADY ON SCREEN: the quick model's plain-language reading of a held command,
1052
- * landing on the permission card named by `requestId` (see PermissionAsk.explain).
1053
- *
1054
- * A frame of its own rather than a field on the card, because the card MUST NOT WAIT FOR IT. This is a
1055
- * safety prompt and the turn is parked on it; a quick-model rung can take tens of seconds before the chain
1056
- * steps down (agent/quick-model.ts measured one at 58), and holding the card back for that reads exactly
1057
- * like the agent freezing, which is the failure the explanation was meant to prevent rather than cause. So
1058
- * the card goes out complete and unexplained, and this arrives if and when it does. Same shape and the same
1059
- * reason as `service_event`, which streams a running service's status onto an offer already rendered.
1060
- *
1061
- * Never arrives at all when the setting is off, when nothing is connected to answer, or when the user
1062
- * settles the card first, and none of those change what the card says. */
1063
- z.object({ kind: z.literal("permission_note"), requestId: z.string(), explain: z.string() }),
1182
+ /* There was a `permission_note` frame here: a late sentence raced onto a command card that had already gone
1183
+ * out, because the explanation was optional and the card must not wait for a quick-model rung that might
1184
+ * take tens of seconds. It is gone with the setting that made it optional. The judge now decides the
1185
+ * verdict, so the sentence is not a decoration arriving afterwards it is the REASON THE CARD EXISTS, and
1186
+ * a card cannot be raised before it is known. Nothing races, and `PermissionAsk.explain` is populated at
1187
+ * raise time (guard/command-gate.ts). */
1064
1188
  // The turn's permission mode, whenever it changes, the user's pick at turn start, then every move the
1065
1189
  // AGENT makes on its own (EnterPlanMode on a request that needs thinking through, ExitPlanMode once the
1066
1190
  // user approves). The composer's mode selector follows this, so the UI never lies about the live posture.
@@ -1186,23 +1310,34 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
1186
1310
  // rate_limit_event or the account's persisted usage windows). Absent when the reset instant is unknown
1187
1311
  // (nothing to schedule against).
1188
1312
  resetsAt: z.number().optional(),
1189
- // Where the daemon's resume of THIS turn stands, for the two codes that have one (provider-outage,
1190
- // claude-token-refused). "scheduled" = the resume is armed and this turn comes back by itself;
1191
- // "available" = the daemon remembered the failed turn and arming THIS conversation
1192
- // (AgentSummarySchema.resumeAfterOutage) picks up that same resume, which is what the chat's offer
1193
- // banner hangs off, outage only, since a renewal is never gated on a posture at all. The two words
1194
- // are read against the effective posture (the conversation's override, else the sandbox default), so a
1195
- // chat armed on its own says "scheduled" while the unarmed board around it says "available".
1196
- // Absent means there is nothing automatic to resume: a spent usage limit never has one,
1197
- // and a refused credential has none once re-minting it has already been tried and failed.
1313
+ /* Where the daemon's resume of THIS turn stands, for the three codes that have one (provider-outage,
1314
+ * claude-token-refused, rate_limit). "scheduled" = the resume is armed and this turn comes back by
1315
+ * itself; "available" = the daemon remembered the failed turn and arming THIS conversation
1316
+ * (AgentSummarySchema's resumeAfterOutage / resumeAfterLimit) picks up that same resume, which is what
1317
+ * the offer banner hangs off, gated codes only, since a credential renewal is never gated on a posture
1318
+ * at all. The two words are read against the effective posture (the conversation's override, else the
1319
+ * sandbox default), so a chat armed on its own says "scheduled" while the unarmed board around it says
1320
+ * "available". Absent means there is nothing automatic to resume: a limit whose reset instant nobody
1321
+ * published has nothing to schedule against, and a refused credential has none once re-minting it has
1322
+ * already been tried and failed.
1323
+ *
1324
+ * A SPENT ALLOWANCE USED TO BE ABSENT HERE BY RULE, and the rule was right about the default and wrong
1325
+ * about the ceiling. The budget is the user's, so nothing fires unless they said so, which is what the
1326
+ * posture is; what the old absence also cost was the case a press cannot reach, a 2am wall on a board
1327
+ * nobody is watching. Both words are now honest for it: unarmed says "available", which is an offer,
1328
+ * and armed says "scheduled", which the card counts down to. */
1198
1329
  autoResume: z.enum(["scheduled", "available"]).optional(),
1199
1330
  /* THE DAEMON IS STILL HOLDING THIS EXACT TURN, so the way on is to RE-RUN it rather than to send
1200
1331
  * something after it. rate_limit only, and the counterpart to `autoResume` rather than a member of it:
1201
- * that field answers "is a machine bringing this back", and a spent allowance is the one failure where
1202
- * the answer is deliberately no (the allowance is the user's own budget to spend, turn-resume.ts). This
1332
+ * that field answers "is a machine bringing this back", which for a spent allowance is a posture the
1333
+ * user sets and defaults to no (the allowance is their own budget to spend, turn-resume.ts). This
1203
1334
  * answers the question that was never asked, "and if the user says go, what happens", which had exactly
1204
1335
  * one possible answer for as long as it went unasked: a new user message reading "Continue".
1205
1336
  *
1337
+ * BOTH ANSWERS RUN THROUGH THIS FIELD, which is why it is not folded into the one above: an armed
1338
+ * conversation's scheduled fire and an unarmed one's press are the same held turn re-run the same way,
1339
+ * and the only difference is who says go.
1340
+ *
1206
1341
  * That answer was wrong in a way the chat could not show. The press is not a new instruction, it is the
1207
1342
  * same one again, and appending it said otherwise to the only reader that matters: the provider session
1208
1343
  * grew one "Continue" per press, each with a synthetic "No response requested." above it, so a chat that
@@ -1229,34 +1364,73 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
1229
1364
  ]);
1230
1365
  export type AgentEvent = z.infer<typeof AgentEventSchema>;
1231
1366
 
1232
- // The /agent/attach stream: a head frame identifying the run, then its AgentEvents stamped with their 1-based
1233
- // seq (the client's resume cursor), then `end` when the run is over, every frame delivered, nothing more
1234
- // coming. A stream that closes WITHOUT `end` was dropped mid-run; the client re-attaches with `after` = the
1235
- // last seq it holds. The head's `prompt`/`startedAt` let a window that didn't initiate the turn (a reload, a
1236
- // second window, another device) synthesize the user bubble and the elapsed readout; its `seq` is the log
1237
- // length at attach time, the replay/live boundary.
1367
+ /* THE FRAMES THAT ARE FACTS ABOUT THE TURN rather than words in it: which session it runs, where it stands,
1368
+ * what it costs, how it failed. Everything else an AgentEvent can say is transcript, and reaches a client as
1369
+ * rows and patches (TranscriptPatchSchema) after the daemon has folded it; these reach it as themselves,
1370
+ * because there is nothing to fold, a client keeps them as state beside the transcript. A frame can be both,
1371
+ * a `worktree` that rebased writes a notice AND says where the branch is, so the two lists overlap, and the
1372
+ * fold and this list each take the half that is theirs. */
1373
+ export const TURN_FACT_KINDS = [
1374
+ "session",
1375
+ "worktree",
1376
+ "init",
1377
+ "terminal",
1378
+ "browser",
1379
+ "commands",
1380
+ "usage",
1381
+ "rate_limit_info",
1382
+ "fast_mode",
1383
+ "tier",
1384
+ "provider_retry",
1385
+ "account_usage",
1386
+ "context_usage",
1387
+ "mode",
1388
+ "error",
1389
+ ] as const;
1390
+ export type TurnFact = Extract<AgentEvent, { kind: (typeof TURN_FACT_KINDS)[number] }>;
1391
+ export const isTurnFact = (event: AgentEvent): event is TurnFact => (TURN_FACT_KINDS as readonly string[]).includes(event.kind);
1392
+ // The same members AgentEventSchema declares, picked out rather than declared twice: a fact's shape is the
1393
+ // frame's shape, and a second spelling of it would be the drift the list above exists to prevent.
1394
+ type AgentEventMember = (typeof AgentEventSchema.options)[number];
1395
+ const factMembers = AgentEventSchema.options.filter((member) => (TURN_FACT_KINDS as readonly string[]).includes(member.shape.kind.value)) as unknown as [
1396
+ AgentEventMember,
1397
+ ...AgentEventMember[],
1398
+ ];
1399
+ export const TurnFactSchema = z.discriminatedUnion("kind", factMembers) as unknown as z.ZodType<TurnFact>;
1400
+
1401
+ /* The /agent/attach stream: a head carrying the run's rows so far, then every change to them and every fact
1402
+ * about the turn as each lands, then `end` when the run is over, nothing more coming. A stream that closes
1403
+ * WITHOUT `end` was dropped mid-run; the client re-attaches and takes the head's rows again, whole, which is
1404
+ * what makes attaching idempotent: a window never re-folds what it has already drawn, it replaces it.
1405
+ *
1406
+ * Facts REPLAY on every attach (their seq is at or below the head's), because a window joining late still has
1407
+ * to learn which session the turn runs and where its branch stands; patches are only ever live (their seq is
1408
+ * above the head's), because the head already holds their result. */
1238
1409
  export const AttachFrameSchema = z.discriminatedUnion("kind", [
1239
1410
  z.object({
1240
- kind: z.literal("attached").describe("The first frame, identifying the run you have joined."),
1411
+ kind: z.literal("attached").describe("The first frame, identifying the run you have joined and handing you its transcript so far."),
1241
1412
  run: z.string().describe("The run's id."),
1242
- prompt: z.string().describe("What was said to start it, so a window that did not start the turn can still draw the message."),
1243
1413
  startedAt: z.number().describe("When it started, in milliseconds, so a window joining late can show how long it has been going."),
1244
- seq: z.number().describe("How many frames already exist. Everything at or below this number is replay; everything above it is live."),
1414
+ seq: z.number().describe("How many frames the run has produced so far. A fact at or below this number is being replayed; a patch is never."),
1415
+ rows: z
1416
+ .array(TranscriptRowSchema)
1417
+ .describe("The turn's rows as they stand: what was asked, and everything the agent has said and done since. Draw these, then apply the patches that follow."),
1245
1418
  }),
1246
1419
  z.object({
1247
- kind: z.literal("frame").describe("One thing that happened."),
1248
- seq: z
1249
- .number()
1250
- .describe("Its position in the run, counting from one. Keep the last one you saw and hand it back to resume rather than replay."),
1251
- event: AgentEventSchema.describe(
1252
- "What happened, as one of about forty shapes: the agent's words arriving piece by piece, a tool being called and answering, a plan or a question it is waiting on, a mode change, the turn's cost. Each carries its own `kind`.",
1253
- ),
1420
+ kind: z.literal("patch").describe("One change to the run's rows."),
1421
+ seq: z.number().describe("Its position in the run, counting from one."),
1422
+ patch: TranscriptPatchSchema,
1423
+ }),
1424
+ z.object({
1425
+ kind: z.literal("fact").describe("One thing about the turn that is not a row: its session, its branch, its cost, a failure."),
1426
+ seq: z.number().describe("Its position in the run, counting from one. At or below the head's number, it is being replayed."),
1427
+ fact: TurnFactSchema,
1254
1428
  }),
1255
1429
  z.object({
1256
1430
  kind: z
1257
1431
  .literal("end")
1258
1432
  .describe(
1259
- "The run is over and every frame has been delivered. A stream that closes without this was dropped mid-run, so re-attach with the last position you hold rather than assuming the turn finished.",
1433
+ "The run is over and every frame has been delivered. A stream that closes without this was dropped mid-run, so re-attach rather than assuming the turn finished.",
1260
1434
  ),
1261
1435
  }),
1262
1436
  ]);
@@ -1536,9 +1710,34 @@ export type Presence = z.infer<typeof PresenceSchema>;
1536
1710
  export const AgentsSchema = z.object({ kind: z.literal("agents"), agents: z.array(AgentSummarySchema), rev: z.number() });
1537
1711
  export type Agents = z.infer<typeof AgentsSchema>;
1538
1712
 
1713
+ /* AN ACCOUNT'S HEADROOM JUST MOVED, the reading itself, keyed the way the daemon's store keys it (a Claude
1714
+ * account id, or `${provider}:${authFile}` for a routed subscription).
1715
+ *
1716
+ * The fifth push, and the one that lets every ring, rail and picker row stop refetching on mount. A reading
1717
+ * lands on the daemon for one of four reasons, a turn settled, a plan refused, a screen asked, a provider
1718
+ * pushed, and until this frame existed only the window that caused it ever heard: every other window drew the
1719
+ * number it had loaded that morning until something in it happened to remount. Snapshot-not-diff per account,
1720
+ * last frame wins, and a browser that missed one simply holds the older reading, which is what `measuredAt`
1721
+ * is for. `usage` absent ⇒ the account's snapshot was cleared (it was disconnected). */
1722
+ export const AccountUsageChangedSchema = z.object({
1723
+ kind: z.literal("accountUsage"),
1724
+ // The provider whose row this account is, because the key alone does not say (a native id is bare).
1725
+ provider: z.string(),
1726
+ account: z.string(),
1727
+ usage: AccountUsageSchema.optional(),
1728
+ });
1729
+ export type AccountUsageChanged = z.infer<typeof AccountUsageChangedSchema>;
1730
+
1731
+ // A provider's last refusal was recorded or settled. The observed half of "can I run on this" (see
1732
+ // ProviderRefusalSchema), pushed for the same reason the reading above is: a refusal at 4am used to reach a
1733
+ // window only when it next reloaded its account rows. `refusal` absent ⇒ settled, nothing stands.
1734
+ export const ProviderRefusalChangedSchema = z.object({ kind: z.literal("providerRefusal"), provider: z.string(), refusal: ProviderRefusalSchema.optional() });
1735
+ export type ProviderRefusalChanged = z.infer<typeof ProviderRefusalChangedSchema>;
1736
+
1539
1737
  // The /events stream union: the hello identity frame, then liveness heartbeats interleaved with boot progress,
1540
- // workspace-change batches, repo-set snapshots, ref-move batches, runtime-domain nudges, and presence + fleet
1541
- // roster snapshots. oRPC validates every yielded frame against this, so all kinds must live here.
1738
+ // workspace-change batches, repo-set snapshots, ref-move batches, runtime-domain nudges, presence + fleet
1739
+ // roster snapshots, and account headroom / refusal changes. oRPC validates every yielded frame against this,
1740
+ // so all kinds must live here.
1542
1741
  export const SystemEventSchema = z.discriminatedUnion("kind", [
1543
1742
  HelloSchema,
1544
1743
  HeartbeatSchema,
@@ -1549,5 +1748,7 @@ export const SystemEventSchema = z.discriminatedUnion("kind", [
1549
1748
  RuntimeChangedSchema,
1550
1749
  PresenceSchema,
1551
1750
  AgentsSchema,
1751
+ AccountUsageChangedSchema,
1752
+ ProviderRefusalChangedSchema,
1552
1753
  ]);
1553
1754
  export type SystemEvent = z.infer<typeof SystemEventSchema>;