@intentic/sandbox-contract 1.314.0 → 1.316.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 (165) hide show
  1. package/README.md +5 -5
  2. package/dist/contracts/accounts.contract.d.ts +6 -0
  3. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.d.ts +2 -0
  5. package/dist/contracts/agent.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.js +1 -1
  7. package/dist/contracts/agent.contract.js.map +1 -1
  8. package/dist/contracts/agents.contract.d.ts +293 -0
  9. package/dist/contracts/agents.contract.d.ts.map +1 -1
  10. package/dist/contracts/agents.contract.js +14 -4
  11. package/dist/contracts/agents.contract.js.map +1 -1
  12. package/dist/contracts/ci.contract.d.ts +22 -0
  13. package/dist/contracts/ci.contract.d.ts.map +1 -1
  14. package/dist/contracts/settings.contract.d.ts +0 -3
  15. package/dist/contracts/settings.contract.d.ts.map +1 -1
  16. package/dist/contracts/system.contract.d.ts +6 -0
  17. package/dist/contracts/system.contract.d.ts.map +1 -1
  18. package/dist/contracts/system.contract.js +1 -1
  19. package/dist/contracts/system.contract.js.map +1 -1
  20. package/dist/contracts/translator.contract.d.ts +12 -0
  21. package/dist/contracts/translator.contract.d.ts.map +1 -1
  22. package/dist/contracts/workspace.contract.d.ts +15 -137
  23. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  24. package/dist/contracts/workspace.contract.js +25 -15
  25. package/dist/contracts/workspace.contract.js.map +1 -1
  26. package/dist/events/agent-events.d.ts.map +1 -1
  27. package/dist/events/agent-events.js.map +1 -1
  28. package/dist/events/agent-words.d.ts.map +1 -1
  29. package/dist/events/agent-words.js +5 -4
  30. package/dist/events/agent-words.js.map +1 -1
  31. package/dist/events/land-breakage.d.ts +0 -2
  32. package/dist/events/land-breakage.d.ts.map +1 -1
  33. package/dist/events/land-breakage.js +0 -2
  34. package/dist/events/land-breakage.js.map +1 -1
  35. package/dist/events/requests.js +1 -1
  36. package/dist/events/requests.js.map +1 -1
  37. package/dist/events/system-events.d.ts +4 -0
  38. package/dist/events/system-events.d.ts.map +1 -1
  39. package/dist/events/transcript.d.ts +1 -0
  40. package/dist/events/transcript.d.ts.map +1 -1
  41. package/dist/events/transcript.js +5 -1
  42. package/dist/events/transcript.js.map +1 -1
  43. package/dist/index.d.ts +358 -142
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/models/agent-runtimes.d.ts.map +1 -1
  48. package/dist/models/agent-runtimes.js +1 -1
  49. package/dist/models/agent-runtimes.js.map +1 -1
  50. package/dist/models/plan-pools.d.ts +3 -1
  51. package/dist/models/plan-pools.d.ts.map +1 -1
  52. package/dist/models/plan-pools.js +11 -4
  53. package/dist/models/plan-pools.js.map +1 -1
  54. package/dist/protocol/raw-routes.d.ts +9 -0
  55. package/dist/protocol/raw-routes.d.ts.map +1 -1
  56. package/dist/protocol/raw-routes.js +2 -0
  57. package/dist/protocol/raw-routes.js.map +1 -1
  58. package/dist/protocol/request-id.d.ts +0 -1
  59. package/dist/protocol/request-id.d.ts.map +1 -1
  60. package/dist/protocol/request-id.js +0 -1
  61. package/dist/protocol/request-id.js.map +1 -1
  62. package/dist/schemas/agent.js +1 -1
  63. package/dist/schemas/agent.js.map +1 -1
  64. package/dist/schemas/agents.d.ts +7 -1
  65. package/dist/schemas/agents.d.ts.map +1 -1
  66. package/dist/schemas/agents.js +14 -3
  67. package/dist/schemas/agents.js.map +1 -1
  68. package/dist/schemas/automations.d.ts +1 -0
  69. package/dist/schemas/automations.d.ts.map +1 -1
  70. package/dist/schemas/ci.d.ts +45 -0
  71. package/dist/schemas/ci.d.ts.map +1 -1
  72. package/dist/schemas/ci.js +18 -0
  73. package/dist/schemas/ci.js.map +1 -1
  74. package/dist/schemas/devices.d.ts +0 -1
  75. package/dist/schemas/devices.d.ts.map +1 -1
  76. package/dist/schemas/devices.js +3 -4
  77. package/dist/schemas/devices.js.map +1 -1
  78. package/dist/schemas/personas.js +3 -3
  79. package/dist/schemas/personas.js.map +1 -1
  80. package/dist/schemas/providers/plan-limits.d.ts +21 -0
  81. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  82. package/dist/schemas/providers/plan-limits.js +13 -3
  83. package/dist/schemas/providers/plan-limits.js.map +1 -1
  84. package/dist/schemas/providers/provider-oauth.d.ts +6 -0
  85. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  86. package/dist/schemas/settings-history.d.ts +1 -1
  87. package/dist/schemas/settings-history.d.ts.map +1 -1
  88. package/dist/schemas/settings-history.js +2 -1
  89. package/dist/schemas/settings-history.js.map +1 -1
  90. package/dist/schemas/settings.d.ts +0 -4
  91. package/dist/schemas/settings.d.ts.map +1 -1
  92. package/dist/schemas/settings.js +6 -13
  93. package/dist/schemas/settings.js.map +1 -1
  94. package/dist/schemas/terminal.d.ts.map +1 -1
  95. package/dist/schemas/terminal.js +3 -3
  96. package/dist/schemas/terminal.js.map +1 -1
  97. package/dist/schemas/workspace/push-checks.d.ts +222 -0
  98. package/dist/schemas/workspace/push-checks.d.ts.map +1 -0
  99. package/dist/schemas/workspace/push-checks.js +97 -0
  100. package/dist/schemas/workspace/push-checks.js.map +1 -0
  101. package/dist/schemas/workspace/workspace-tree.d.ts +9 -0
  102. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  103. package/dist/schemas/workspace/workspace-tree.js +12 -0
  104. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  105. package/dist/state/definition.d.ts +0 -4
  106. package/dist/state/definition.d.ts.map +1 -1
  107. package/dist/state/runtime-state.d.ts +2 -2
  108. package/dist/state/runtime-state.d.ts.map +1 -1
  109. package/dist/state/runtime-state.js +1 -1
  110. package/dist/state/runtime-state.js.map +1 -1
  111. package/dist/state/workspace-state.d.ts +1 -11
  112. package/dist/state/workspace-state.d.ts.map +1 -1
  113. package/dist/state/workspace-state.js +1 -13
  114. package/dist/state/workspace-state.js.map +1 -1
  115. package/dist/text/transcript-fold.d.ts +8 -0
  116. package/dist/text/transcript-fold.d.ts.map +1 -1
  117. package/dist/text/transcript-fold.js +67 -14
  118. package/dist/text/transcript-fold.js.map +1 -1
  119. package/package.json +5 -5
  120. package/src/contracts/agent.contract.ts +1 -1
  121. package/src/contracts/agents.contract.ts +15 -3
  122. package/src/contracts/system.contract.ts +1 -1
  123. package/src/contracts/workspace.contract.ts +35 -21
  124. package/src/events/agent-events.ts +4 -2
  125. package/src/events/agent-words.test.ts +8 -1
  126. package/src/events/agent-words.ts +8 -5
  127. package/src/events/errands.test.ts +4 -4
  128. package/src/events/land-breakage.ts +5 -9
  129. package/src/events/requests.ts +1 -1
  130. package/src/events/transcript.ts +9 -3
  131. package/src/events/verify-nudge.ts +3 -3
  132. package/src/index.ts +2 -2
  133. package/src/models/agent-runtimes.ts +5 -4
  134. package/src/models/plan-pools.test.ts +22 -0
  135. package/src/models/plan-pools.ts +26 -5
  136. package/src/protocol/ingress-contract.fixture.json +2 -0
  137. package/src/protocol/raw-routes.ts +4 -1
  138. package/src/protocol/request-id.ts +0 -3
  139. package/src/schemas/agent.ts +1 -1
  140. package/src/schemas/agents.ts +21 -8
  141. package/src/schemas/automations.ts +2 -2
  142. package/src/schemas/ci.ts +25 -0
  143. package/src/schemas/devices.ts +8 -12
  144. package/src/schemas/personas.ts +3 -3
  145. package/src/schemas/providers/plan-limits.ts +21 -3
  146. package/src/schemas/settings-history.ts +3 -1
  147. package/src/schemas/settings.test.ts +4 -3
  148. package/src/schemas/settings.ts +17 -31
  149. package/src/schemas/speaker.ts +4 -4
  150. package/src/schemas/terminal.ts +12 -10
  151. package/src/schemas/workspace/{mainline.test.ts → push-checks.test.ts} +5 -5
  152. package/src/schemas/workspace/push-checks.ts +155 -0
  153. package/src/schemas/workspace/workspace-tree.ts +15 -0
  154. package/src/state/fix-stance.test.ts +2 -1
  155. package/src/state/landed-commit.ts +1 -1
  156. package/src/state/red-streak.ts +4 -4
  157. package/src/state/runtime-state.ts +2 -3
  158. package/src/state/workspace-state.ts +1 -13
  159. package/src/text/transcript-fold.test.ts +107 -0
  160. package/src/text/transcript-fold.ts +95 -16
  161. package/dist/schemas/workspace/mainline.d.ts +0 -513
  162. package/dist/schemas/workspace/mainline.d.ts.map +0 -1
  163. package/dist/schemas/workspace/mainline.js +0 -182
  164. package/dist/schemas/workspace/mainline.js.map +0 -1
  165. package/src/schemas/workspace/mainline.ts +0 -268
@@ -1,5 +1,5 @@
1
1
  import { errandOfPrompt, errandOfRow } from "./errands.js";
2
- import { landBreakagePrompt, landFixPrompt } from "./land-breakage.js";
2
+ import { LAND_BREAKAGE_OPENING, LAND_FIX_OPENING } from "./land-breakage.js";
3
3
  import { LAND_CONFLICT_OPENING } from "./land-conflict.js";
4
4
  import { verifyNudgePrompt } from "./verify-nudge.js";
5
5
 
@@ -11,12 +11,12 @@ describe(`which errand a message is`, () => {
11
11
  });
12
12
 
13
13
  test(`is read by its opening on a row written before rows said, and on nothing but a message`, () => {
14
- expect(errandOfPrompt(landBreakagePrompt([`- a failure`]))).toBe(`land-breakage`);
15
- expect(errandOfPrompt(landFixPrompt([`- a failure`]))).toBe(`land-fix`);
14
+ expect(errandOfPrompt(`${LAND_BREAKAGE_OPENING}\n\n- a failure`)).toBe(`land-breakage`);
15
+ expect(errandOfPrompt(`${LAND_FIX_OPENING}\n\n- a failure`)).toBe(`land-fix`);
16
16
  expect(errandOfPrompt(verifyNudgePrompt([`x`]))).toBe(`verify-nudge`);
17
17
  expect(errandOfPrompt(`${LAND_CONFLICT_OPENING}\n\nthe files`)).toBe(`land-conflict`);
18
18
  expect(errandOfPrompt(`Please fix the parser`)).toBeUndefined();
19
- expect(errandOfRow({ role: `assistant`, text: landFixPrompt([]) })).toBeUndefined();
19
+ expect(errandOfRow({ role: `assistant`, text: LAND_FIX_OPENING })).toBeUndefined();
20
20
  expect(errandOfRow({ role: `user`, text: `hello` })).toBeUndefined();
21
21
  });
22
22
  });
@@ -1,23 +1,19 @@
1
- // The follow-up the daemon sends a conversation whose land turned the main tree's own check red, recognised by its opening.
1
+ // The follow-ups the daemon USED to send when the check it ran after every land went red: back to the conversation
2
+ // that landed the work, or to a fresh conversation started on it. Nothing sends either any more: nothing checks work
3
+ // after it lands, CI checks what the owner pushes (schemas/workspace/push-checks.ts). They stay on the wire because
4
+ // records already hold them, and the chat has to go on recognising them: a prompt nobody typed must not reach a reader
5
+ // as their own words.
2
6
 
3
7
  // Anchored on by the chat, so it must stay unique and stable across releases.
4
8
  export const LAND_BREAKAGE_OPENING =
5
9
  "What this conversation landed turned the main tree's own check red after it arrived. Each failure below appeared with that land and was not failing before it. Fix them on this branch and check the fix as you would any change; it lands like any other turn.";
6
10
 
7
- // The prompt a breakage actually sends: its opening, then the failures, each already worded for the model.
8
- export const landBreakagePrompt = (parts: readonly string[]): string => [LAND_BREAKAGE_OPENING, ...parts].join("\n\n");
9
-
10
11
  // Whether a stored prompt is one.
11
12
  export const isLandBreakage = (prompt: string): boolean => prompt.startsWith(LAND_BREAKAGE_OPENING);
12
13
 
13
- // The opening of a FRESH conversation the daemon starts on a red main-line check that the conversation that landed the
14
- // work could not take: nobody could be named, several could, or the one named had gone cold or run out of room.
15
14
  // Anchored on by the chat like the follow-up above, so it must stay unique and stable across releases.
16
15
  export const LAND_FIX_OPENING =
17
16
  "The main tree's own check went red after work landed, and no conversation still holding that work could take it, so this one was started to fix it. The failures, the lands they arrived with and where to read those conversations are below. Fix them in this isolated worktree and check the fix as you would any change; your work lands like any other turn.";
18
17
 
19
- // The prompt a fresh fix-up is started with: its opening, then the evidence, each part already worded for the model.
20
- export const landFixPrompt = (parts: readonly string[]): string => [LAND_FIX_OPENING, ...parts].join("\n\n");
21
-
22
18
  // Whether a stored prompt is one.
23
19
  export const isLandFix = (prompt: string): boolean => prompt.startsWith(LAND_FIX_OPENING);
@@ -99,7 +99,7 @@ export const PermissionAskSchema = z.object({
99
99
  "The program this request is holding, when the request is about one. Present on a command gate's request and absent on every other permission ask.",
100
100
  ),
101
101
  child: ChildAgentAskSchema.optional().describe(
102
- "The child agent this request would start or reach, and what it runs on. Present on the child-agent gate's request and absent on every other permission ask.",
102
+ "The subagent this request would start or reach, and what it runs on. Present on the request to start or reach a subagent and absent on every other permission ask.",
103
103
  ),
104
104
  // The judge's own sentence, present only when the title can't say it; never written by the agent being gated.
105
105
  explain: z
@@ -97,9 +97,15 @@ export const TranscriptToolSchema: z.ZodType<TranscriptTool> = z.lazy(() =>
97
97
  ),
98
98
  }),
99
99
  );
100
- // The child a call started, keyed by that call's own id, so `subagent`/`subagent_update` frames need no separate
101
- // correlation. Identifying fields arrive once; status/spend/activity replace on each update.
100
+ // The subagent a call started, on that call's card whichever mechanism started it (transcript-fold.ts places it).
101
+ // Identifying fields arrive once; status/spend/activity replace on each update.
102
102
  export const TranscriptSubagentSchema = z.object({
103
+ id: z
104
+ .string()
105
+ .optional()
106
+ .describe(
107
+ "Its own id, where that is not the card's: a spawned subagent is named by its own conversation, which is what the roster, `wait` and its page call it. Absent, the card's id is its id, as it is for one the runtime started in-process.",
108
+ ),
103
109
  kind: SubagentKindSchema,
104
110
  agentType: z.string().optional(),
105
111
  description: z.string().optional(),
@@ -151,7 +157,7 @@ export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
151
157
 
152
158
  // Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender.
153
159
  export const TranscriptAgentWordsSchema = z.object({
154
- kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a child agent this one started."),
160
+ kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a subagent this one started."),
155
161
  from: z.string().describe("The sending conversation's id."),
156
162
  title: z.string().optional().describe("The sender's title, when it had one."),
157
163
  failed: z.boolean().optional().describe("A child's report on a turn that failed rather than finished."),
@@ -1,7 +1,7 @@
1
1
  // The follow-up the daemon USED to send when a turn ended with work it could not confirm. Nothing sends it any more:
2
- // checks run after work lands, never inside a conversation (schemas/workspace/mainline.ts). It stays on the wire because
3
- // records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not reach a reader as
4
- // their own words.
2
+ // nothing checks inside a conversation, CI checks what the owner pushes (schemas/workspace/push-checks.ts). It stays on
3
+ // the wire because records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not
4
+ // reach a reader as their own words.
5
5
 
6
6
  // Anchored on by the reader, so it must stay unique and stable across releases — a reworded opening un-recognises every
7
7
  // nudge already in a record, and they read as the user's own typing again.
package/src/index.ts CHANGED
@@ -83,7 +83,7 @@ export { intenticContract } from "./contracts/intentic.contract.js";
83
83
  export { inventoryContract } from "./contracts/inventory.contract.js";
84
84
  export { issuesContract } from "./contracts/issues.contract.js";
85
85
  export { logsContract } from "./contracts/logs.contract.js";
86
- export { REQUEST_ID_EVIDENCE_ROUTE, REQUEST_ID_HEADER } from "./protocol/request-id.js";
86
+ export { REQUEST_ID_HEADER } from "./protocol/request-id.js";
87
87
  export * from "./protocol/edge-verdict.js";
88
88
  export { loopsContract } from "./contracts/loops.contract.js";
89
89
  export { panelsContract } from "./contracts/panels.contract.js";
@@ -234,7 +234,7 @@ export * from "./schemas/webext.js";
234
234
  export * from "./schemas/workflows.js";
235
235
  export * from "./schemas/workspace/workspace-repos.js";
236
236
  export * from "./schemas/workspace/workspace-search.js";
237
- export * from "./schemas/workspace/mainline.js";
237
+ export * from "./schemas/workspace/push-checks.js";
238
238
  export * from "./schemas/workspace/workspace-setup.js";
239
239
  export * from "./schemas/workspace/workspace-tree.js";
240
240
  export * from "./state/arrival.js";
@@ -181,7 +181,7 @@ export const PI: AgentCapabilities = {
181
181
  secrets: "none",
182
182
  };
183
183
 
184
- // Cursor's own runtime, driven through `@cursor/sdk` in this daemon's process; an embedding surface, so most seams
184
+ // Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
185
185
  // other runtimes lack are function arguments here. The harness axis doesn't apply: the SDK is the only door.
186
186
  export const CURSOR: AgentCapabilities = {
187
187
  runtime: "cursor",
@@ -199,11 +199,12 @@ export const CURSOR: AgentCapabilities = {
199
199
  // Cursor publishes effort as model parameters, not one scale; true here just means it's forwardable at all.
200
200
  effort: true,
201
201
  fastMode: false,
202
- // cwd: Cursor's loop runs inside the daemon's own process, whose /work must stay the shared checkout.
203
- isolation: "cwd",
202
+ // The SDK agent of an anchored turn runs in a runtime process born in the turn's mount namespace (the daemon keeps
203
+ // the loop, the tools and the hook gate), so its shell and its edits see the worktree at /work.
204
+ isolation: "namespace",
204
205
  // Cursor's commands are files on disk the SDK loads but doesn't publish back, so there's no list to offer.
205
206
  commands: false,
206
- // The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
207
+ // The SDK runs its shell itself, as a plain child; there is no tmux session for the terminal panel to attach to.
207
208
  terminals: false,
208
209
  // The SDK throws typed errors instead of dissolving a refusal into prose, so the adapter files coded frames.
209
210
  recovery: true,
@@ -167,6 +167,13 @@ test("a revoked sign-in outranks a lost seat, which outranks a bench, whatever t
167
167
  reason: "cooling down",
168
168
  until: 2_000,
169
169
  });
170
+ // One Google wants its owner to verify: neither waiting nor reconnecting lifts it, and the row carries the page.
171
+ expect(serviceState({ ...facts, cooling: { reason: "Verify your account to continue.", verify: "https://accounts.google.com/v" } })).toEqual({
172
+ kind: "blocked",
173
+ fix: "verify",
174
+ reason: "Verify your account to continue.",
175
+ url: "https://accounts.google.com/v",
176
+ });
170
177
  // A bench whose instant has passed is over.
171
178
  expect(serviceState({ ...facts, cooling: { until: 999 } }, undefined, undefined, 1_000_000)).toEqual({ kind: "ready", room: 100 });
172
179
  });
@@ -202,6 +209,21 @@ test("a refusal stands until a later reading contradicts it, and never past its
202
209
  expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [])).toBe("standing");
203
210
  });
204
211
 
212
+ // Codex's usage endpoint kept reading room while every turn was refused "try again at 3:38 PM": the provider's own
213
+ // instant is what ends the refusal, where it is known which account it refused.
214
+ test("a spent allowance with the provider's own reset stands until that instant, whatever a later reading says", () => {
215
+ const room = at(2_000, window({ kind: "five_hour", utilization: 0 }));
216
+ const until = { kind: "limit", resetsAt: 10 } as const;
217
+ expect(refusalVerdict(refusal({ ...until, account: "a" }), [{ account: "a", usage: room }], 9_999)).toBe("standing");
218
+ expect(refusalVerdict(refusal({ ...until, account: "a" }), [{ account: "a", usage: room }], 10_000)).toBe("answered");
219
+ // A routed refusal names no account, but with one account there is no doubt whose it was.
220
+ expect(refusalVerdict(refusal(until), [{ account: "a", usage: room }], 9_999)).toBe("standing");
221
+ // Over several, the translator benches the one it refused and routes on, so another's room still answers it.
222
+ expect(refusalVerdict(refusal(until), [{ account: "a", usage: room }, { account: "b", usage: room }], 9_999)).toBe("answered");
223
+ // Spent until the instant the provider named, not the pool's own reset.
224
+ expect(serviceStates([{ account: "a", usage: room }], refusal({ ...until, account: "a" }), undefined, 9_999).get("a")).toEqual({ kind: "spent", reopensAt: 10 });
225
+ });
226
+
205
227
  test("a provider's accounts are judged together: a standing refusal lands only on the account it names", () => {
206
228
  const room = at(500, window({ kind: "five_hour", utilization: 40 }));
207
229
  const states = serviceStates([{ account: "a", usage: room }, { account: "b", usage: room }], refusal({ kind: "entitlement", account: "a" }));
@@ -145,8 +145,11 @@ export interface ServiceFacts {
145
145
  // Why the sign-in needs renewing, where the provider said.
146
146
  readonly detail?: string | undefined;
147
147
  readonly seatRefusal?: string | undefined;
148
- readonly cooling?: { readonly until?: number | undefined; readonly reason?: string | undefined } | undefined;
148
+ readonly cooling?: { readonly until?: number | undefined; readonly reason?: string | undefined; readonly verify?: string | undefined } | undefined;
149
149
  readonly usage?: AccountUsage | undefined;
150
+ // How the account is named to a person (its label, a routed credential's address); a surface that quotes a figure
151
+ // says whose it is.
152
+ readonly label?: string | undefined;
150
153
  }
151
154
 
152
155
  // A spent allowance is over once a reading with room lands after it; a refused credential once any reading does (the
@@ -168,11 +171,22 @@ const answers = (refusal: ProviderRefusal, facts: ServiceFacts): boolean => {
168
171
  * the account it named is disconnected, `answered` once that account (or, for a refusal naming none, any account) has a
169
172
  * later reading that contradicts it, else `standing`. An empty list is one not loaded yet, so the refusal stands.
170
173
  */
171
- export const refusalVerdict = (refusal: ProviderRefusal, accounts: readonly ServiceFacts[]): "standing" | "answered" | "gone" => {
174
+ export const refusalVerdict = (
175
+ refusal: ProviderRefusal,
176
+ accounts: readonly ServiceFacts[],
177
+ now: number = Date.now(),
178
+ ): "standing" | "answered" | "gone" => {
172
179
  const named = accounts.filter((facts) => facts.account === refusal.account);
173
180
  if (refusal.account !== undefined && named.length === 0) {
174
181
  return accounts.length === 0 ? "standing" : "gone";
175
182
  }
183
+ // Where the provider named when to try again, and it is known which account it refused (it named one, or there is
184
+ // only one), its own words outrank a usage endpoint that kept showing room while it refused: over at that instant,
185
+ // and not before. A nameless refusal over several accounts is still answered by any of them reading room, since the
186
+ // translator benches the one it refused and routes on.
187
+ if (refusal.kind === "limit" && refusal.resetsAt !== undefined && (refusal.account !== undefined || accounts.length <= 1)) {
188
+ return refusal.resetsAt * 1000 <= now ? "answered" : "standing";
189
+ }
176
190
  return (refusal.account === undefined ? accounts : named).some((facts) => answers(refusal, facts)) ? "answered" : "standing";
177
191
  };
178
192
 
@@ -215,10 +229,12 @@ const refusedPool = (usage: AccountUsage | undefined, refusal: ProviderRefusal,
215
229
  if (usage === undefined || binding === undefined) {
216
230
  // No pool to pin: the refusal itself is the evidence, unless it was about a different model than this question.
217
231
  const sameModel = refusal.model === undefined || model === undefined || refusal.model === model.id;
218
- return sameModel ? { kind: "spent" } : headroomState(usage, model);
232
+ return sameModel ? spent(refusal.resetsAt) : headroomState(usage, model);
219
233
  }
220
234
  const pinned = usage.windows.map((window) => (window === binding ? { ...window, utilization: Math.max(window.utilization, SPENT_UTILIZATION) } : window));
221
- return headroomState({ ...usage, windows: pinned }, model);
235
+ const state = headroomState({ ...usage, windows: pinned }, model);
236
+ // The provider's own "try again at" is when it reopens, over whatever the polled pool last said.
237
+ return state.kind === "spent" && refusal.resetsAt !== undefined ? spent(refusal.resetsAt) : state;
222
238
  };
223
239
 
224
240
  /**
@@ -240,6 +256,11 @@ export const serviceState = (facts: ServiceFacts, refusal?: ProviderRefusal, mod
240
256
  return { kind: "blocked", fix: "admin", reason: refusal.message };
241
257
  }
242
258
  const cooling = facts.cooling;
259
+ // A bench the provider wants a person for: the account's owner confirms it on the provider's page, and no wait or
260
+ // reconnect here lifts it. Outranks the retry instant, which is only when the proxy will ask again.
261
+ if (cooling?.verify !== undefined) {
262
+ return { kind: "blocked", fix: "verify", reason: cooling.reason ?? "the provider wants this account verified", url: cooling.verify };
263
+ }
243
264
  // A bench with no instant is one no wait lifts (a Google account with no project): somebody has to connect again.
244
265
  if (cooling !== undefined && cooling.until === undefined) {
245
266
  return { kind: "blocked", fix: "reconnect", reason: cooling.reason ?? "benched by the translator" };
@@ -257,7 +278,7 @@ export const serviceStates = (
257
278
  model?: ModelRef,
258
279
  now: number = Date.now(),
259
280
  ): ReadonlyMap<string, AccountState> => {
260
- const standing = refusal !== undefined && refusalVerdict(refusal, accounts) === "standing" ? refusal : undefined;
281
+ const standing = refusal !== undefined && refusalVerdict(refusal, accounts, now) === "standing" ? refusal : undefined;
261
282
  return new Map(
262
283
  accounts.map((facts) => [
263
284
  facts.account,
@@ -48,6 +48,7 @@
48
48
  "GET /diff/raw",
49
49
  "GET /workspace/raw",
50
50
  "GET /workspace/media",
51
+ "GET /workspace/download",
51
52
  "POST /workspace/upload",
52
53
  "POST /workspace/upload-archive",
53
54
  "GET /system/sync/ssh",
@@ -59,6 +60,7 @@
59
60
  ],
60
61
  "lanes": [
61
62
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/media?path=a.mp4", "lane": "bulk" },
63
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/download?ticket=t", "lane": "bulk" },
62
64
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload?path=a", "lane": "bulk" },
63
65
  { "host": "sandbox-abcdef012345.sbx.example.test:443", "method": "GET", "path": "/system/runners/git/app/info/refs?service=git-upload-pack", "lane": "bulk" },
64
66
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/system/runners/git/app/git-receive-pack", "lane": "bulk" },
@@ -18,6 +18,8 @@ export const RAW_ROUTES = {
18
18
  "GET /workspace/thumb": { guest: true },
19
19
  // Fetched by a <video>/<audio> tag, which sends no header: its scoped ticket is checked in the handler.
20
20
  "GET /workspace/media": { auth: "door", guest: true, lane: "bulk" },
21
+ // Navigated to for a ZIP of a selection, so no header either: the ticket it carries names the selection.
22
+ "GET /workspace/download": { auth: "door", guest: true, lane: "bulk" },
21
23
  // An attachment is part of the message it rides with; any other target edits the shared tree.
22
24
  "POST /workspace/upload": { floor: "writer", attachmentFloor: "collaborator", lane: "bulk" },
23
25
  "POST /workspace/upload-diff": {},
@@ -91,12 +93,13 @@ export const RAW_ROUTES = {
91
93
  "GET /wallet/status": { agent: true, control: "never" },
92
94
  "POST /wallet/fetch": { agent: true, control: "never" },
93
95
  "GET /wallet/history": { agent: true, control: "never" },
94
- // The `agents` CLI's children: start, follow up, answer (never a consent card), list.
96
+ // The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, list.
95
97
  "POST /children/spawn": { agent: true, control: "never" },
96
98
  "GET /children/providers": { agent: true, control: "never" },
97
99
  "POST /children/wait": { agent: true, control: "never" },
98
100
  "POST /children/send": { agent: true, control: "never" },
99
101
  "POST /children/answer": { agent: true, control: "never" },
102
+ "POST /children/cancel": { agent: true, control: "never" },
100
103
  "GET /children": { agent: true, control: "never" },
101
104
  // The fleet reads, and the one write: words in front of another conversation, which a person can do by typing.
102
105
  "GET /fleet": { agent: true },
@@ -1,5 +1,2 @@
1
1
  // Correlates a browser call to the daemon request that served it; a token only, never a security boundary.
2
2
  export const REQUEST_ID_HEADER = "x-intentic-request-id";
3
-
4
- // Confirms daemon CORS accepts this header before sending; no evidence here means don't send, unlike supportsRoute.
5
- export const REQUEST_ID_EVIDENCE_ROUTE = "logs.report";
@@ -131,7 +131,7 @@ export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
131
131
  const AgentTurnFieldsSchema = z.object({
132
132
  prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
133
133
  errand: TurnErrandSchema.optional().describe(
134
- "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a red main line to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
134
+ "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a red CI run to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
135
135
  ),
136
136
  // Minted by the sender so a send whose answer was lost can be sent again without being delivered twice.
137
137
  messageId: z
@@ -88,11 +88,11 @@ export const UnfinishedWorkSchema = z.object({
88
88
  check: z
89
89
  .string()
90
90
  .optional()
91
- .describe("The end-of-turn check that was still failing when the turn ended, by name. No longer written: checks run after work lands."),
91
+ .describe("The end-of-turn check that was still failing when the turn ended, by name. No longer written: nothing checks inside a turn."),
92
92
  });
93
93
  export type UnfinishedWork = z.infer<typeof UnfinishedWorkSchema>;
94
94
  // What the last turn showed of its own work, read off its tool calls and never asked of the model: the record a card
95
- // badges instead of sending the turn back to prove anything. Checks run after the work lands (mainline.ts).
95
+ // badges instead of sending the turn back to prove anything. CI checks what the owner pushes (push-checks.ts).
96
96
  export const TurnProofSchema = z.object({
97
97
  at: z.number().describe("When the turn that left this ended, in milliseconds."),
98
98
  verification: z
@@ -137,7 +137,7 @@ export const LandedMessageSchema = z.object({
137
137
  .optional()
138
138
  .describe("Why a test this change weakened is meant to be weaker, in the conversation's own words. Nearly always absent."),
139
139
  // Present only when the conversation declared an exception a check should grant this change (`Allow: <check> — <reason>`);
140
- // the check after the land and the push read it off the commit.
140
+ // the push reads it off the commit.
141
141
  allows: z
142
142
  .array(z.string())
143
143
  .optional()
@@ -418,6 +418,13 @@ export const AgentSummarySchema = z.object({
418
418
  .describe(
419
419
  "When somebody last opened it, in milliseconds. Newer activity than this is what makes it unread. Kept by the sandbox rather than by a browser, so clearing site data or picking up a phone does not resurrect every badge.",
420
420
  ),
421
+ // Browser-side words the daemon cannot read, reported so the unattended archive sweep never files them away.
422
+ unsentAt: z
423
+ .number()
424
+ .optional()
425
+ .describe(
426
+ "Since when somebody's composer has held a message for it that they have not sent yet, in milliseconds. While set, the sandbox never archives it on its own for being idle.",
427
+ ),
421
428
  attention: AgentAttentionSchema.describe("Which kinds of waiting-for-you it is doing."),
422
429
  // Only the causes that still hold, re-read live (conversations/land/standing.ts), never the stored report's own list: a
423
430
  // blocker the user has since cleared is not something to offer them an action about.
@@ -448,7 +455,7 @@ export const AgentSummarySchema = z.object({
448
455
  })
449
456
  .optional()
450
457
  .describe(
451
- "Subagents and child agents this one delegated to. Absent means it never has, which is most conversations. Their spend is their own and is not folded into this conversation's cost.",
458
+ "Subagents this one started, in-process and spawned alike. Absent means it never has, which is most conversations. Their spend is their own and is not folded into this conversation's cost.",
452
459
  ),
453
460
  // Cumulative output across every repo (base → branch tip), refreshed on each land; independent of what has actually
454
461
  // landed.
@@ -567,10 +574,8 @@ export const AgentSummarySchema = z.object({
567
574
  ),
568
575
  });
569
576
  export type AgentSummary = z.infer<typeof AgentSummarySchema>;
570
- // Whether a conversation runs again by itself: the daemon's own reading, or, from a daemon older than it, whether a watch
571
- // is armed, which was all that one could say.
572
- export const awaitsWake = (agent: { readonly awaitingWake?: boolean | undefined; readonly watches?: readonly unknown[] | undefined }): boolean =>
573
- agent.awaitingWake ?? (agent.watches?.length ?? 0) > 0;
577
+ // Whether a conversation runs again by itself: the daemon's own reading, never guessed from its armed watches.
578
+ export const awaitsWake = (agent: { readonly awaitingWake?: boolean | undefined }): boolean => agent.awaitingWake === true;
574
579
  // One armed watch as a card carries it; derived from AgentSummarySchema.watches so the two shapes can't drift apart.
575
580
  export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
576
581
  // One background job as a card carries it.
@@ -740,6 +745,14 @@ export const AgentAutoLandSchema = z.object({
740
745
  "Whether its work merges automatically when a turn finishes. Null clears the override and goes back to following the sandbox-wide setting, so a conversation does not sit holding a frozen copy of a default it has quietly stopped following.",
741
746
  ),
742
747
  });
748
+ // A composer's unsent words, reported by the editor that holds them: the words themselves stay in the browser.
749
+ export const AgentUnsentSchema = z.object({
750
+ id: z.string().min(1).describe("Which conversation."),
751
+ at: z
752
+ .number()
753
+ .nullable()
754
+ .describe("When the composer started holding the unsent message, in milliseconds. Null says it no longer holds one: it was sent or cleared."),
755
+ });
743
756
  // Same `null`-clears-the-override shape as autoLand, for this conversation's own answer to one ending's question.
744
757
  // One route rather than one per ending: they are the same decision asked about different walls, and three near-identical
745
758
  // verbs is how the surfaces drifted apart in the first place.
@@ -28,8 +28,8 @@ import { ZoneSchema } from "../time/zone.js";
28
28
  // reads these.
29
29
  // turn.settled: fires once per isolated turn regardless of outcome, while the diff is still on screen
30
30
  // agent.landed: fires only once work reaches the main tree, including a later manual Land
31
- // deps.broken/deps.fixed: edges from the dependency verifier around a landed change, not standing states, so a chore
32
- // wakes on the transition, never on a tree that stays red
31
+ // deps.broken/deps.fixed: retired with the check that ran after every land (2026-09-27). Nothing emits them any more;
32
+ // they stay so an automation written for them still reads, and it never fires. Main's red CI is the `ci` listener's.
33
33
  export const WorkspaceEventKindSchema = z.enum(["turn.settled", "agent.landed", "deps.broken", "deps.fixed"]);
34
34
  export type WorkspaceEventKind = z.infer<typeof WorkspaceEventKindSchema>;
35
35
  // Payload of a workspace-triggered wake, delivered as JSON in $AUTOMATION_PAYLOAD and appended to the prompt.
package/src/schemas/ci.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // ci: pipeline runs on the workspace repos' github/gitlab remotes
2
2
  import { z } from "zod";
3
3
  import { AgentRunPickSchema } from "./agent.js";
4
+ import { RedDecisionSchema } from "./workspace/push-checks.js";
4
5
  // The daemon maps each workspace repo to its CI project via a connected github/gitlab capability, registers a webhook
5
6
  // to fire `ci` listener automations instantly, and serves the Pipelines rail from a webhook-freshened, poll-backfilled
6
7
  // cache. `host` names which provider API serves a repo; the listener provider is always `ci`, since a trigger narrows
@@ -39,6 +40,9 @@ export const PipelineRunSchema = z.object({
39
40
  .describe(
40
41
  "What set it off, in the forge's own word rather than flattened into a shared vocabulary, because the forge's word is the precise one.",
41
42
  ),
43
+ // github's workflow name: several workflows run on one push there, and one passing says nothing of another. Absent on
44
+ // gitlab, where a commit's pipeline is the project's one.
45
+ workflow: z.string().optional().describe("Which workflow it is a run of, where a push starts several. Absent where a commit has one pipeline."),
42
46
  branch: z.string().describe("Which branch."),
43
47
  sha: z.string().describe("Which commit."),
44
48
  status: PipelineStatusSchema.describe(
@@ -108,10 +112,31 @@ export type CiRepo = z.infer<typeof CiRepoSchema>;
108
112
  // Fallback poll interval when a repo's webhook couldn't register; lives here since both the poller and the automation
109
113
  // editor need it to tell the owner what a `hookWarning` actually costs (minutes' delay, not the feature).
110
114
  export const CI_POLL_INTERVAL_MS = 2 * 60_000;
115
+ // Main's CI while it is red, as the daemon's one fix agent on it sees it (ci/main-fixer.ts): from the first job that
116
+ // failed until a later run of every workflow that failed on it passes, whatever else failed along the way.
117
+ export const CiMainRedSchema = z.object({
118
+ repo: z.string().describe("Which workspace repository."),
119
+ branch: z.string().describe("Which main-line branch."),
120
+ since: z.number().describe("When its first job failed, in milliseconds."),
121
+ runId: z.number().describe("The newest run that failed on it."),
122
+ jobs: z.array(z.string()).describe("The jobs failing on it now, by name: the newest red run's failures, and any that failed since."),
123
+ fixer: z
124
+ .string()
125
+ .optional()
126
+ .describe("The one conversation working on it, which every failure goes to until the branch is green. Absent while nobody is on it."),
127
+ decision: RedDecisionSchema.optional().describe(
128
+ "The latest thing decided about it: `fix-up` while the fix agent has it, `spent` once it waits for you (its turns are used up, or it stopped without a fix), `reported` when repairs are off.",
129
+ ),
130
+ });
131
+ export type CiMainRed = z.infer<typeof CiMainRedSchema>;
111
132
  export const CiRunsResponseSchema = z.object({
112
133
  repos: z.array(CiRepoSchema).describe("Which workspace repositories are wired to a forge, and how each one's notifications are set up."),
113
134
  // Newest first, across all mapped repos.
114
135
  runs: z.array(PipelineRunSchema).describe("Runs across all of them, newest first."),
136
+ reds: z
137
+ .array(CiMainRedSchema)
138
+ .optional()
139
+ .describe("Every main-line branch red right now, with the fix agent on it. Absent from a daemon that keeps none."),
115
140
  });
116
141
  export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
117
142
  // Re-resolves repo → project + token on every call, so a stale card can't act on a mapping the workspace no longer has.
@@ -505,7 +505,7 @@ export const DeviceSchema = z.object({
505
505
  // The host connection's key, when this machine is also a connected device. Absent otherwise.
506
506
  hostId: z.string().optional(),
507
507
  // The card that connection is of, beside the key rather than parsed out of it: the machine's name, a label. Absent
508
- // with `hostId`, and from a daemon older than the field (`deviceCard` reads the key then).
508
+ // with `hostId`.
509
509
  card: z.string().optional(),
510
510
  // The computer this row is an environment of, from whichever door has said (MachineIdSchema): what `machinesOf`
511
511
  // groups by. Absent when no door has, which leaves the row a computer of its own.
@@ -563,20 +563,15 @@ export const deviceDistro = (device: Device): string | undefined => {
563
563
  const byEnvironment = (a: Device, b: Device): number =>
564
564
  Number(isWslDevice(a)) - Number(isWslDevice(b)) || (deviceDistro(a) ?? a.label).localeCompare(deviceDistro(b) ?? b.label);
565
565
 
566
- // The card a device's door hangs off: one card is one computer, so two doors naming the same card are one machine
567
- // whatever either has said about itself — which is the whole of what an environment that has never connected says.
568
- // The row's own label, else its key read by the one parser there is (a row from a daemon older than `card`).
569
- export const deviceCard = (device: Pick<Device, "card" | "hostId">): string | undefined =>
570
- device.card ?? (device.hostId === undefined ? undefined : parseHostConnection(device.hostId).card);
571
-
572
566
  // The computer a device is on, by whichever door said: its row, its connect-time facts, its report, its sync enrollment.
573
567
  export const deviceMachineId = (device: Device): string | undefined =>
574
568
  device.machineId ?? device.facts?.machineId ?? device.report?.machineId ?? device.sync?.machineId;
575
569
 
576
- // What makes two devices one computer: a shared card, or a shared machine id. Nothing else: a hostname is shared by a
577
- // PC and its WSL distros and by two unrelated machines with the same name, so it joined things by accident.
570
+ // What makes two devices one computer: a shared card (the row's own `card`, never parsed back out of its key: one card
571
+ // is one computer, whatever either door has said about itself), or a shared machine id. Nothing else: a hostname is
572
+ // shared by a PC and its WSL distros and by two unrelated machines with the same name, so it joined things by accident.
578
573
  const tokensOf = (device: Device): string[] => {
579
- const card = deviceCard(device);
574
+ const { card } = device;
580
575
  const machine = deviceMachineId(device);
581
576
  return [...(card === undefined ? [] : [`card:${card.toLowerCase()}`]), ...(machine === undefined ? [] : [`machine:${machine}`])];
582
577
  };
@@ -621,8 +616,9 @@ const machineOf = (environments: readonly Device[]): Machine => {
621
616
  if (first === undefined) {
622
617
  return { key: "", label: "", environments };
623
618
  }
624
- const named = environments.map(deviceCard).find((key) => key !== undefined) ?? (rest.length === 0 ? undefined : deviceMachineId(first));
625
- const label = first.label === first.hostId ? (deviceCard(first) ?? first.label) : first.label;
619
+ const named =
620
+ environments.map((device) => device.card).find((key) => key !== undefined) ?? (rest.length === 0 ? undefined : deviceMachineId(first));
621
+ const label = first.label === first.hostId ? (first.card ?? first.label) : first.label;
626
622
  return { key: named ?? first.key, label, environments };
627
623
  };
628
624
 
@@ -134,15 +134,15 @@ export const TURN_BRIEFING_NOTES: readonly TurnBriefingNote[] = [
134
134
  },
135
135
  {
136
136
  id: "delegation",
137
- label: "Spawning child agents",
137
+ label: "Spawning subagents",
138
138
  when: "First message, on a shell-only runtime, for a card that may delegate.",
139
139
  cost: "The agent does the work itself rather than handing parts of it to other conversations.",
140
140
  },
141
141
  {
142
142
  id: "checks",
143
143
  label: "Checks after landing",
144
- when: "First message and after a compaction, and again whenever the main tree's own check turns red or back to green: what runs after its work lands, and which failures the main tree already has.",
145
- cost: "The agent does not know which failures are the main tree's own and not its doing, so it may spend a turn chasing one somebody else is already fixing.",
144
+ when: "First message and after a compaction: that nothing checks its work when it finishes or after it lands, which checks are its to run, and that CI checks what the owner pushes.",
145
+ cost: "The agent does not know what checks its work, so it may run the whole repository's suite on a shared machine, or chase a failure main already has.",
146
146
  },
147
147
  {
148
148
  id: "dependencies",
@@ -47,7 +47,7 @@ export const AccountUsageSchema = z.object({
47
47
  export type AccountUsage = z.infer<typeof AccountUsageSchema>;
48
48
  // Who has to act for a blocked account, which is the only thing a surface chooses its instruction by: the reason is the
49
49
  // words for a person, never a key.
50
- export const AccountFixSchema = z.enum(["reconnect", "admin", "wait"]);
50
+ export const AccountFixSchema = z.enum(["reconnect", "admin", "verify", "wait"]);
51
51
  export type AccountFix = z.infer<typeof AccountFixSchema>;
52
52
  // Whether an account can serve a turn, decided once by the daemon (models/plan-pools.ts `serviceState`) from every fact
53
53
  // it holds about it: a revoked sign-in, a lost seat, a translator bench, a refusal still standing, and its plan limits.
@@ -69,10 +69,11 @@ export const AccountStateSchema = z.discriminatedUnion("kind", [
69
69
  z.object({
70
70
  kind: z.literal("blocked"),
71
71
  fix: AccountFixSchema.describe(
72
- "Who can make it serve again: `reconnect` (sign in again on this sandbox), `admin` (an organisation admin hands the seat back), or `wait` (it lifts by itself, at `until` where known).",
72
+ "Who can make it serve again: `reconnect` (sign in again on this sandbox), `admin` (an organisation admin hands the seat back), `verify` (the account's owner confirms it on the provider's page, at `url`), or `wait` (it lifts by itself, at `until` where known).",
73
73
  ),
74
74
  reason: z.string().describe("Why, in words a person can act on: the provider's own sentence where it gave one."),
75
75
  until: z.number().optional().describe("When waiting lifts it, in epoch seconds, for `wait` only."),
76
+ url: z.string().optional().describe("The provider's page where the account's owner lifts it, for `verify` only."),
76
77
  }),
77
78
  z.object({ kind: z.literal("unknown").describe("Nothing blocks it and nothing has been measured: usable, never read as room.") }),
78
79
  ]);
@@ -122,6 +123,14 @@ export const ProviderRefusalSchema = z.object({
122
123
  // Lets a `limit` refusal be read against the pool that model spends (`UsageWindow.gates`), not the account's
123
124
  // fullest pool.
124
125
  model: z.string().optional().describe("Which model the refused turn was on, where that is known."),
126
+ // The provider's own "try again at …", which a polled reading cannot contradict: a usage endpoint can keep showing
127
+ // room while the plan refuses every turn.
128
+ resetsAt: z
129
+ .number()
130
+ .optional()
131
+ .describe(
132
+ "When the provider said to try again, in epoch seconds, for a spent allowance where it named one. Until then the refusal stands whatever a reading says; after it, it is over.",
133
+ ),
125
134
  });
126
135
  export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
127
136
  export const ProviderRefusalsSchema = z.object({
@@ -151,6 +160,9 @@ export const TranslatorAccountSchema = z.object({
151
160
  // Why, in the words of what is missing: the proxy's own sentence where it gave one short enough to print,
152
161
  // else this sandbox's. Never a pasted upstream body.
153
162
  reason: z.string().optional(),
163
+ // The provider's page where the account's owner confirms it (Google's VALIDATION_REQUIRED). Present means
164
+ // a person has to act there: the proxy's retry fails until they do.
165
+ verify: z.string().optional(),
154
166
  })
155
167
  .optional(),
156
168
  state: AccountStateSchema.optional().describe(
@@ -205,7 +217,7 @@ export const AgentReplySchema = z.discriminatedUnion("kind", [
205
217
  feedback: z.string().optional().describe("Why not, which goes back to the model as the reason."),
206
218
  // Whole, not a patch: effort or an account named for one model means nothing on another.
207
219
  child: ChildRunSchema.optional().describe(
208
- "For a request to start a child agent: what to start it on instead of what the agent asked for. It replaces the whole run (model, account, effort and the rest), not only the fields it names. Ignored with a no, and on any other request.",
220
+ "For a request to start a subagent: what to start it on instead of what the agent asked for. It replaces the whole run (model, account, effort and the rest), not only the fields it names. Ignored with a no, and on any other request.",
209
221
  ),
210
222
  }),
211
223
  z.object({
@@ -400,6 +412,12 @@ export const SwitchAccountSchema = z.object({
400
412
  .describe(
401
413
  "Keep the provider session across the move (the model keeps everything, and re-reads all of it once on the other account) rather than opening a fresh one seeded from the record.",
402
414
  ),
415
+ run: z
416
+ .boolean()
417
+ .optional()
418
+ .describe(
419
+ "Also run a turn that a spent allowance, a stop or a refusal is holding, at once on the new account. Leave it out to only move the conversation: a held turn stays held until something asks for it.",
420
+ ),
403
421
  });
404
422
  export type SwitchAccount = z.infer<typeof SwitchAccountSchema>;
405
423
  export const AccountSwitchedSchema = z.object({
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { dropAll, fold, isJsonObject, mapValue, retype } from "../documents/conversions.js";
2
+ import { at, drop, dropAll, fold, isJsonObject, mapValue, retype } from "../documents/conversions.js";
3
3
  import type { KeepWarmSettings, RuleSchema } from "./settings.js";
4
4
 
5
5
  // The conversions the settings shape has had, oldest first: what brings any earlier release's settings to
@@ -105,4 +105,6 @@ export const SETTINGS_HISTORY = [
105
105
  "drops rules at the retired turn.ending moment (and the verify-ui-edits built-in), which run nothing",
106
106
  ),
107
107
  keepWarmObject,
108
+ // Where the check after landing ran (2026-09-27): nothing checks work after it lands any more, CI does.
109
+ at("offload", drop("landCheck")),
108
110
  ] as const;