@namzu/sdk 58.1.1 → 60.0.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 (312) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/account/binding.d.ts +20 -0
  3. package/dist/account/binding.d.ts.map +1 -0
  4. package/dist/account/binding.js +27 -0
  5. package/dist/account/binding.js.map +1 -0
  6. package/dist/account/index.d.ts +9 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +5 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/lock.d.ts +33 -0
  11. package/dist/account/lock.d.ts.map +1 -0
  12. package/dist/account/lock.js +170 -0
  13. package/dist/account/lock.js.map +1 -0
  14. package/dist/account/registry.d.ts +21 -0
  15. package/dist/account/registry.d.ts.map +1 -0
  16. package/dist/account/registry.js +340 -0
  17. package/dist/account/registry.js.map +1 -0
  18. package/dist/account/types.d.ts +124 -0
  19. package/dist/account/types.d.ts.map +1 -0
  20. package/dist/account/types.js +35 -0
  21. package/dist/account/types.js.map +1 -0
  22. package/dist/compaction/context-window.d.ts +53 -0
  23. package/dist/compaction/context-window.d.ts.map +1 -1
  24. package/dist/compaction/context-window.js +141 -2
  25. package/dist/compaction/context-window.js.map +1 -1
  26. package/dist/execution/workflow/index.d.ts +3 -1
  27. package/dist/execution/workflow/index.d.ts.map +1 -1
  28. package/dist/execution/workflow/index.js +2 -1
  29. package/dist/execution/workflow/index.js.map +1 -1
  30. package/dist/execution/workflow/journal.d.ts +24 -4
  31. package/dist/execution/workflow/journal.d.ts.map +1 -1
  32. package/dist/execution/workflow/journal.js +131 -9
  33. package/dist/execution/workflow/journal.js.map +1 -1
  34. package/dist/execution/workflow/prelude.d.ts +2 -0
  35. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  36. package/dist/execution/workflow/prelude.js +49 -8
  37. package/dist/execution/workflow/prelude.js.map +1 -1
  38. package/dist/execution/workflow/resume.d.ts +59 -0
  39. package/dist/execution/workflow/resume.d.ts.map +1 -0
  40. package/dist/execution/workflow/resume.js +273 -0
  41. package/dist/execution/workflow/resume.js.map +1 -0
  42. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  43. package/dist/execution/workflow/runtime.js +112 -8
  44. package/dist/execution/workflow/runtime.js.map +1 -1
  45. package/dist/execution/workflow/types.d.ts +105 -1
  46. package/dist/execution/workflow/types.d.ts.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/pals/communication/ingress-schema.d.ts +38 -38
  52. package/dist/peers/record.d.ts +98 -0
  53. package/dist/peers/record.d.ts.map +1 -1
  54. package/dist/peers/record.js +17 -0
  55. package/dist/peers/record.js.map +1 -1
  56. package/dist/peers/registry.d.ts.map +1 -1
  57. package/dist/peers/registry.js +2 -2
  58. package/dist/peers/registry.js.map +1 -1
  59. package/dist/plugin/compat.d.ts +24 -0
  60. package/dist/plugin/compat.d.ts.map +1 -0
  61. package/dist/plugin/compat.js +149 -0
  62. package/dist/plugin/compat.js.map +1 -0
  63. package/dist/plugin/loader.d.ts +2 -1
  64. package/dist/plugin/loader.d.ts.map +1 -1
  65. package/dist/plugin/loader.js +6 -0
  66. package/dist/plugin/loader.js.map +1 -1
  67. package/dist/pricing/index.d.ts.map +1 -1
  68. package/dist/pricing/index.js +2 -1
  69. package/dist/pricing/index.js.map +1 -1
  70. package/dist/process/kill-tree.d.ts +44 -0
  71. package/dist/process/kill-tree.d.ts.map +1 -1
  72. package/dist/process/kill-tree.js +102 -4
  73. package/dist/process/kill-tree.js.map +1 -1
  74. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  75. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  76. package/dist/prompt/coding-agent-doctrine.js +9 -1
  77. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  78. package/dist/provider/errors.d.ts.map +1 -1
  79. package/dist/provider/errors.js +25 -2
  80. package/dist/provider/errors.js.map +1 -1
  81. package/dist/provider/model-window-suffix.d.ts +18 -0
  82. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  83. package/dist/provider/model-window-suffix.js +77 -0
  84. package/dist/provider/model-window-suffix.js.map +1 -0
  85. package/dist/provider/token-budget.d.ts.map +1 -1
  86. package/dist/provider/token-budget.js +9 -1
  87. package/dist/provider/token-budget.js.map +1 -1
  88. package/dist/public-runtime.d.ts +6 -3
  89. package/dist/public-runtime.d.ts.map +1 -1
  90. package/dist/public-runtime.js +6 -4
  91. package/dist/public-runtime.js.map +1 -1
  92. package/dist/public-types.d.ts +3 -0
  93. package/dist/public-types.d.ts.map +1 -1
  94. package/dist/registry/command/kernel-commands.d.ts +1 -4
  95. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  96. package/dist/registry/command/kernel-commands.js +0 -28
  97. package/dist/registry/command/kernel-commands.js.map +1 -1
  98. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  99. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  100. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  101. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  102. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  103. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  104. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  105. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  106. package/dist/runtime/jobs/liveness.d.ts +67 -0
  107. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  108. package/dist/runtime/jobs/liveness.js +94 -0
  109. package/dist/runtime/jobs/liveness.js.map +1 -0
  110. package/dist/runtime/jobs/registry.d.ts +21 -0
  111. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  112. package/dist/runtime/jobs/registry.js +104 -2
  113. package/dist/runtime/jobs/registry.js.map +1 -1
  114. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  115. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  116. package/dist/runtime/jobs/wake-budget.js +48 -0
  117. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  118. package/dist/runtime/query/detached-calls.d.ts +65 -0
  119. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  120. package/dist/runtime/query/detached-calls.js +116 -0
  121. package/dist/runtime/query/detached-calls.js.map +1 -0
  122. package/dist/runtime/query/executor.d.ts +11 -3
  123. package/dist/runtime/query/executor.d.ts.map +1 -1
  124. package/dist/runtime/query/executor.js +100 -8
  125. package/dist/runtime/query/executor.js.map +1 -1
  126. package/dist/runtime/query/index.d.ts +20 -0
  127. package/dist/runtime/query/index.d.ts.map +1 -1
  128. package/dist/runtime/query/index.js +31 -5
  129. package/dist/runtime/query/index.js.map +1 -1
  130. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  131. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  132. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  133. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  134. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  135. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  136. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  138. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  139. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  140. package/dist/runtime/query/session-storage.js +8 -3
  141. package/dist/runtime/query/session-storage.js.map +1 -1
  142. package/dist/runtime/query/steering.d.ts +29 -3
  143. package/dist/runtime/query/steering.d.ts.map +1 -1
  144. package/dist/runtime/query/steering.js +58 -3
  145. package/dist/runtime/query/steering.js.map +1 -1
  146. package/dist/runtime/query/tooling.d.ts +1 -0
  147. package/dist/runtime/query/tooling.d.ts.map +1 -1
  148. package/dist/runtime/query/tooling.js +1 -0
  149. package/dist/runtime/query/tooling.js.map +1 -1
  150. package/dist/runtime/system-events.d.ts +5 -0
  151. package/dist/runtime/system-events.d.ts.map +1 -1
  152. package/dist/runtime/system-events.js +7 -1
  153. package/dist/runtime/system-events.js.map +1 -1
  154. package/dist/sandbox/denial.d.ts +11 -0
  155. package/dist/sandbox/denial.d.ts.map +1 -0
  156. package/dist/sandbox/denial.js +61 -0
  157. package/dist/sandbox/denial.js.map +1 -0
  158. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  159. package/dist/sandbox/file-walk-program.js +31 -13
  160. package/dist/sandbox/file-walk-program.js.map +1 -1
  161. package/dist/sandbox/file-walk.d.ts +2 -0
  162. package/dist/sandbox/file-walk.d.ts.map +1 -1
  163. package/dist/sandbox/file-walk.js +64 -13
  164. package/dist/sandbox/file-walk.js.map +1 -1
  165. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  166. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  167. package/dist/sandbox/ignore-matcher.js +11 -0
  168. package/dist/sandbox/ignore-matcher.js.map +1 -0
  169. package/dist/sandbox/ignore-source.d.ts +26 -0
  170. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  171. package/dist/sandbox/ignore-source.js +221 -0
  172. package/dist/sandbox/ignore-source.js.map +1 -0
  173. package/dist/sandbox/provider/local.d.ts.map +1 -1
  174. package/dist/sandbox/provider/local.js +11 -1
  175. package/dist/sandbox/provider/local.js.map +1 -1
  176. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  177. package/dist/scheduler/completion-inbox.js +1 -0
  178. package/dist/scheduler/completion-inbox.js.map +1 -1
  179. package/dist/session/log-hash.d.ts.map +1 -1
  180. package/dist/session/log-hash.js +18 -1
  181. package/dist/session/log-hash.js.map +1 -1
  182. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  183. package/dist/store/session-index/sqlite.js +169 -16
  184. package/dist/store/session-index/sqlite.js.map +1 -1
  185. package/dist/tools/builtins/bash.d.ts.map +1 -1
  186. package/dist/tools/builtins/bash.js +10 -4
  187. package/dist/tools/builtins/bash.js.map +1 -1
  188. package/dist/tools/builtins/computer-use.d.ts +35 -0
  189. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  190. package/dist/tools/builtins/computer-use.js +512 -67
  191. package/dist/tools/builtins/computer-use.js.map +1 -1
  192. package/dist/tools/builtins/glob.d.ts +1 -0
  193. package/dist/tools/builtins/glob.d.ts.map +1 -1
  194. package/dist/tools/builtins/glob.js +65 -28
  195. package/dist/tools/builtins/glob.js.map +1 -1
  196. package/dist/tools/builtins/grep.d.ts +2 -0
  197. package/dist/tools/builtins/grep.d.ts.map +1 -1
  198. package/dist/tools/builtins/grep.js +94 -40
  199. package/dist/tools/builtins/grep.js.map +1 -1
  200. package/dist/tools/builtins/job.d.ts +1 -1
  201. package/dist/tools/builtins/job.d.ts.map +1 -1
  202. package/dist/tools/builtins/job.js +34 -7
  203. package/dist/tools/builtins/job.js.map +1 -1
  204. package/dist/tools/builtins/search-support.d.ts +44 -0
  205. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  206. package/dist/tools/builtins/search-support.js +89 -0
  207. package/dist/tools/builtins/search-support.js.map +1 -0
  208. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  209. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  210. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  211. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  212. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  213. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  214. package/dist/tools/builtins/wait-for-job.js +13 -2
  215. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  216. package/dist/tools/command-shell.d.ts.map +1 -1
  217. package/dist/tools/command-shell.js +14 -5
  218. package/dist/tools/command-shell.js.map +1 -1
  219. package/dist/types/computer-use/index.d.ts +129 -13
  220. package/dist/types/computer-use/index.d.ts.map +1 -1
  221. package/dist/types/computer-use/index.js.map +1 -1
  222. package/dist/types/errors/catalog.d.ts.map +1 -1
  223. package/dist/types/errors/catalog.js +6 -0
  224. package/dist/types/errors/catalog.js.map +1 -1
  225. package/dist/types/plugin/index.d.ts +24 -0
  226. package/dist/types/plugin/index.d.ts.map +1 -1
  227. package/dist/types/plugin/index.js +4 -0
  228. package/dist/types/plugin/index.js.map +1 -1
  229. package/dist/types/provider/errors.d.ts +7 -0
  230. package/dist/types/provider/errors.d.ts.map +1 -1
  231. package/dist/types/provider/errors.js +27 -3
  232. package/dist/types/provider/errors.js.map +1 -1
  233. package/dist/types/sandbox/index.d.ts +38 -1
  234. package/dist/types/sandbox/index.d.ts.map +1 -1
  235. package/dist/types/sandbox/index.js.map +1 -1
  236. package/dist/types/session/records.d.ts +479 -1
  237. package/dist/types/session/records.d.ts.map +1 -1
  238. package/dist/types/session/records.js +45 -1
  239. package/dist/types/session/records.js.map +1 -1
  240. package/dist/types/tool/index.d.ts +30 -0
  241. package/dist/types/tool/index.d.ts.map +1 -1
  242. package/dist/types/tool/index.js.map +1 -1
  243. package/dist/types/tool/presentation.d.ts +8 -0
  244. package/dist/types/tool/presentation.d.ts.map +1 -1
  245. package/package.json +1 -1
  246. package/src/account/binding.ts +43 -0
  247. package/src/account/index.ts +32 -0
  248. package/src/account/lock.ts +184 -0
  249. package/src/account/registry.ts +413 -0
  250. package/src/account/types.ts +137 -0
  251. package/src/compaction/context-window.ts +163 -2
  252. package/src/execution/workflow/index.ts +14 -1
  253. package/src/execution/workflow/journal.ts +161 -11
  254. package/src/execution/workflow/prelude.ts +51 -8
  255. package/src/execution/workflow/resume.ts +313 -0
  256. package/src/execution/workflow/runtime.ts +127 -9
  257. package/src/execution/workflow/types.ts +109 -1
  258. package/src/index.ts +17 -0
  259. package/src/peers/record.ts +18 -0
  260. package/src/peers/registry.ts +2 -2
  261. package/src/plugin/compat.ts +173 -0
  262. package/src/plugin/loader.ts +7 -1
  263. package/src/pricing/index.ts +2 -1
  264. package/src/process/kill-tree.ts +106 -4
  265. package/src/prompt/coding-agent-doctrine.ts +9 -1
  266. package/src/provider/errors.ts +27 -1
  267. package/src/provider/model-window-suffix.ts +84 -0
  268. package/src/provider/token-budget.ts +9 -3
  269. package/src/public-runtime.ts +38 -2
  270. package/src/public-types.ts +16 -0
  271. package/src/registry/command/kernel-commands.ts +1 -32
  272. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  273. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  274. package/src/runtime/jobs/liveness.ts +144 -0
  275. package/src/runtime/jobs/registry.ts +114 -2
  276. package/src/runtime/jobs/wake-budget.ts +55 -0
  277. package/src/runtime/query/detached-calls.ts +135 -0
  278. package/src/runtime/query/executor.ts +102 -12
  279. package/src/runtime/query/index.ts +50 -7
  280. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  281. package/src/runtime/query/iteration/phases/context.ts +7 -0
  282. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  283. package/src/runtime/query/session-storage.ts +8 -3
  284. package/src/runtime/query/steering.ts +77 -6
  285. package/src/runtime/query/tooling.ts +2 -0
  286. package/src/runtime/system-events.ts +12 -1
  287. package/src/sandbox/denial.ts +79 -0
  288. package/src/sandbox/file-walk-program.ts +32 -13
  289. package/src/sandbox/file-walk.ts +75 -12
  290. package/src/sandbox/ignore-matcher.ts +46 -0
  291. package/src/sandbox/ignore-source.ts +220 -0
  292. package/src/sandbox/provider/local.ts +11 -1
  293. package/src/scheduler/completion-inbox.ts +1 -0
  294. package/src/session/log-hash.ts +21 -0
  295. package/src/store/session-index/sqlite.ts +171 -16
  296. package/src/tools/builtins/bash.ts +11 -4
  297. package/src/tools/builtins/computer-use.ts +593 -68
  298. package/src/tools/builtins/glob.ts +79 -32
  299. package/src/tools/builtins/grep.ts +107 -43
  300. package/src/tools/builtins/job.ts +42 -12
  301. package/src/tools/builtins/search-support.ts +107 -0
  302. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  303. package/src/tools/builtins/wait-for-job.ts +14 -2
  304. package/src/tools/command-shell.ts +13 -5
  305. package/src/types/computer-use/index.ts +157 -15
  306. package/src/types/errors/catalog.ts +6 -0
  307. package/src/types/plugin/index.ts +8 -0
  308. package/src/types/provider/errors.ts +40 -3
  309. package/src/types/sandbox/index.ts +40 -1
  310. package/src/types/session/records.ts +52 -2
  311. package/src/types/tool/index.ts +30 -0
  312. package/src/types/tool/presentation.ts +8 -0
@@ -60,6 +60,8 @@ export type JobWaitOutcome =
60
60
  /** Which clock ran out. */
61
61
  readonly cause: 'idle' | 'wall'
62
62
  readonly elapsedMs: number
63
+ /** How long the job had been silent when the wait gave up. */
64
+ readonly quietForMs: number
63
65
  } & JobWaitProgress)
64
66
 
65
67
  /**
@@ -167,6 +169,7 @@ export async function waitForJobWithBounds(
167
169
  kind: 'timeout',
168
170
  cause: 'wall',
169
171
  elapsedMs: elapsed,
172
+ quietForMs: now() - lastProgressAt,
170
173
  output,
171
174
  nextOffset: cursor,
172
175
  droppedBytes,
@@ -182,6 +185,7 @@ export async function waitForJobWithBounds(
182
185
  kind: 'timeout',
183
186
  cause: 'idle',
184
187
  elapsedMs: elapsed,
188
+ quietForMs: quietFor,
185
189
  output,
186
190
  nextOffset: cursor,
187
191
  droppedBytes,
@@ -214,7 +218,7 @@ export function describeJobWaitTimeout(
214
218
  const seconds = Math.round(outcome.elapsedMs / 1000)
215
219
  const resume = `call wait_for_job or job read with from_offset ${outcome.nextOffset} to see only what comes next`
216
220
  if (outcome.cause === 'idle') {
217
- return `Job ${id} went quiet: no new output for a while, after ${seconds}s. It has not been stopped and may still be working — ${resume}.`
221
+ return `Job ${id} went quiet: no new output for ${Math.round(outcome.quietForMs / 1000)}s (waited ${seconds}s). It has not been stopped and may still be working — ${resume}.`
218
222
  }
219
223
  return `Job ${id} has been running for ${seconds}s without finishing. It has not been stopped — ${resume}.`
220
224
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import { invalidBackgroundJobIdInput } from '../../runtime/jobs/job-id.js'
4
+ import { LIVENESS_STALLED_MS } from '../../runtime/jobs/liveness.js'
4
5
  import { readPositiveIntEnv } from '../../utils/env.js'
5
6
  import { defineTool } from '../defineTool.js'
6
7
  import { describeJobWaitTimeout, waitForJobWithBounds } from './wait-for-job-bounds.js'
@@ -20,7 +21,7 @@ import { describeJobWaitTimeout, waitForJobWithBounds } from './wait-for-job-bou
20
21
  */
21
22
 
22
23
  const DEFAULT_TIMEOUT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_TIMEOUT_MS', 5 * 60 * 1000)
23
- const DEFAULT_IDLE_TIMEOUT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_IDLE_MS', 2 * 60 * 1000)
24
+ const DEFAULT_IDLE_TIMEOUT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_IDLE_MS', LIVENESS_STALLED_MS)
24
25
 
25
26
  /**
26
27
  * The longest either bound will accept from the model.
@@ -146,7 +147,11 @@ export const WaitForJobTool = defineTool({
146
147
  }
147
148
 
148
149
  if (input.output_stream !== undefined && input.output_contains === undefined) {
149
- return { success: false, output: '', error: 'output_stream requires output_contains.' }
150
+ return {
151
+ success: false,
152
+ output: '',
153
+ error: 'output_stream requires output_contains.',
154
+ }
150
155
  }
151
156
  if (input.output_contains !== undefined) {
152
157
  if (!jobs.waitForOutput) {
@@ -261,6 +266,13 @@ export const WaitForJobTool = defineTool({
261
266
  }
262
267
  }
263
268
 
269
+ // The exit and its output are in this result, so the notice that would
270
+ // announce the same exit is withdrawn rather than repeated. Only when
271
+ // the model was given all of it: output the retention cap dropped, or
272
+ // that this bounded result omitted, leaves the notice in place.
273
+ if (outcome.kind === 'exited' && outcome.droppedBytes === 0 && outcome.omittedOutputBytes === 0)
274
+ jobs.markCollected?.(input.id)
275
+
264
276
  const notices = [
265
277
  ...(outcome.droppedBytes > 0
266
278
  ? [`[${outcome.droppedBytes} bytes were dropped by the job's output retention cap]`]
@@ -55,7 +55,7 @@ import { randomUUID } from 'node:crypto'
55
55
  import { constants, accessSync, existsSync } from 'node:fs'
56
56
  import { delimiter, isAbsolute, join, win32 } from 'node:path'
57
57
 
58
- import { MSYS_TAG_VARIABLE, registerMsysShell } from '../process/kill-tree.js'
58
+ import { MSYS_TAG_VARIABLE, registerMsysShell, registerProcessTag } from '../process/kill-tree.js'
59
59
  import type { ShellDialect } from '../types/tool/index.js'
60
60
 
61
61
  /** The shell a host-side command runs in. */
@@ -452,8 +452,14 @@ export function spawnHostShell(
452
452
  const spawnFn = options.spawnImpl ?? nodeSpawn
453
453
  const shell = options.shell ?? hostCommandShell()
454
454
  const spawned = hostShellSpawn(command, options.env, shell)
455
- // Git Bash: tag the shell so a kill can find what a forked command left.
456
- const tag = shell.windows === true && shell.path !== undefined ? randomUUID() : undefined
455
+ // Tag the shell so a kill can find what a forked command left: under Git
456
+ // Bash, the processes its runtime forked out of the Windows tree's reach; on
457
+ // Linux, the ones that started their own session and so left the process
458
+ // group. Other platforms have neither a bash runtime nor `/proc` to look in.
459
+ const platform = options.platform ?? process.platform
460
+ const windowsBash = shell.windows === true && shell.path !== undefined
461
+ const tag =
462
+ windowsBash || (shell.windows !== true && platform === 'linux') ? randomUUID() : undefined
457
463
  const resolved =
458
464
  tag === undefined ? spawned : { ...spawned, env: { ...spawned.env, [MSYS_TAG_VARIABLE]: tag } }
459
465
  const common: SpawnOptions = {
@@ -471,8 +477,10 @@ export function spawnHostShell(
471
477
  }
472
478
  if (resolved.file !== undefined) {
473
479
  const child = spawnFn(resolved.file, [...resolved.args], common)
474
- if (tag !== undefined && child !== null && typeof child === 'object')
475
- registerMsysShell(child, { bash: resolved.file, tag })
480
+ if (tag !== undefined && child !== null && typeof child === 'object') {
481
+ if (windowsBash) registerMsysShell(child, { bash: resolved.file, tag })
482
+ else registerProcessTag(child, tag)
483
+ }
476
484
  return child
477
485
  }
478
486
  const windows = windowsCmdSpawn(command, resolved.env)
@@ -207,6 +207,15 @@ export interface FocusWindowResult {
207
207
  readonly ok: boolean
208
208
  /** The window in front after the attempt, or null when none could be read. */
209
209
  readonly focusedId: string | null
210
+ /**
211
+ * Set with `detail` when the host did not try at all (the screen was in
212
+ * use, a prompt had it, the user said no). `not_delivered` / `safe` means
213
+ * nothing was done and asking again later is fine.
214
+ */
215
+ readonly delivery?: ActionDelivery
216
+ readonly retrySafety?: ActionRetrySafety
217
+ /** Why, in words the model can act on. */
218
+ readonly detail?: string
210
219
  }
211
220
 
212
221
  /**
@@ -231,9 +240,13 @@ export type UiElementAction =
231
240
  */
232
241
  export interface UiElement {
233
242
  /**
234
- * Opaque reference for {@link ComputerUseHost.uiAct}, valid until the
235
- * host's next snapshot. Empty for an element the host cannot act on (a
236
- * label, a group), which is in the tree for what it says.
243
+ * Opaque reference for {@link ComputerUseHost.uiAct}. It names the control,
244
+ * not one reading of it: a host that can find the control again keeps the
245
+ * ref usable across snapshots, and fails with a {@link UiActFailureCode}
246
+ * when it cannot. Empty for an element the host cannot act on (a label, a
247
+ * group), which is in the tree for what it says. A host may also give a
248
+ * ref to a region it left unexpanded (see {@link UiElement.hiddenChildren});
249
+ * that ref is the `root` of a drill-down snapshot, not a target for `uiAct`.
237
250
  */
238
251
  readonly ref: string
239
252
  /** Platform role, e.g. `Button`, `Edit`, `ListItem` (UIA ControlType) or `AXButton`. */
@@ -248,6 +261,12 @@ export interface UiElement {
248
261
  readonly states?: readonly string[]
249
262
  readonly actions?: readonly UiElementAction[]
250
263
  readonly children?: readonly UiElement[]
264
+ /**
265
+ * How many children of this element the host left out of the snapshot to
266
+ * keep it short. Read them with a snapshot whose {@link UiSnapshotOptions.root}
267
+ * is this element's ref.
268
+ */
269
+ readonly hiddenChildren?: number
251
270
  }
252
271
 
253
272
  /**
@@ -264,8 +283,41 @@ export interface UiSnapshot {
264
283
  readonly root: UiElement
265
284
  /** True when the host stopped walking the tree before it ended (a size or time bound). */
266
285
  readonly truncated?: boolean
286
+ /**
287
+ * True when the tree is an outline of a larger one: regions carry
288
+ * {@link UiElement.hiddenChildren} and can be opened with
289
+ * {@link UiSnapshotOptions.root}.
290
+ */
291
+ readonly outline?: boolean
292
+ /** How many controls the whole window holds, when the host knows. */
293
+ readonly total?: number
294
+ /** True when this snapshot is one region of the window (it was asked for with `root`). */
295
+ readonly region?: boolean
296
+ /** For a region listed in pages: the `offset` that reads the page after this one. */
297
+ readonly next?: number
298
+ }
299
+
300
+ /**
301
+ * What a snapshot is asked to read.
302
+ *
303
+ * @experimental See {@link ComputerUseCapabilities.uiTree}.
304
+ */
305
+ export interface UiSnapshotOptions {
306
+ /** A ref from an earlier snapshot: read only that element's subtree. Refs outside it stay valid. */
307
+ readonly root?: string
308
+ /** With `root`: skip this many of the region's children (the page a snapshot reported as `next`). */
309
+ readonly offset?: number
310
+ /** Only controls whose text contains this (case-insensitive), with the elements above them. */
311
+ readonly query?: string
267
312
  }
268
313
 
314
+ /** Why a ref could not be acted on, as a typed failure. */
315
+ export type UiActFailureCode =
316
+ /** The control the ref named is gone, or no longer something that can be acted on. */
317
+ | 'STALE_REF'
318
+ /** Several controls fit what the ref named; the host did not guess. */
319
+ | 'AMBIGUOUS_TARGET'
320
+
269
321
  /**
270
322
  * What {@link ComputerUseHost.uiAct} did.
271
323
  *
@@ -275,6 +327,51 @@ export interface UiActResult {
275
327
  readonly ok: boolean
276
328
  /** Why it did not, in words the model can act on (a stale ref, a disabled element). */
277
329
  readonly detail?: string
330
+ /** Set when the ref could not be resolved to one control; nothing was sent. */
331
+ readonly code?: UiActFailureCode
332
+ /** What is known about the effect. Absent from a host that does not classify. */
333
+ readonly delivery?: ActionDelivery
334
+ /** Whether repeating the action blindly is safe. Set together with `delivery`. */
335
+ readonly retrySafety?: ActionRetrySafety
336
+ /** The state read back after a stateful action, when the host could read it. */
337
+ readonly readBack?: {
338
+ readonly expected?: string
339
+ readonly observed?: string
340
+ }
341
+ }
342
+
343
+ /**
344
+ * How far an action is known to have taken effect.
345
+ *
346
+ * - `delivered_verified`: the host read the state back and it matches.
347
+ * - `delivered_unverified`: the input was sent (or may have been) and no
348
+ * read-back confirmed it. A failed or impossible read-back lands here, never
349
+ * in `not_delivered`.
350
+ * - `not_delivered`: the action was refused before any input, or a read-back
351
+ * proved the state did not change.
352
+ */
353
+ export type ActionDelivery = 'delivered_verified' | 'delivered_unverified' | 'not_delivered'
354
+
355
+ /**
356
+ * `safe`: nothing reached the target, so the same action may be repeated.
357
+ * `unsafe`: the action may have taken effect; inspect before repeating it.
358
+ */
359
+ export type ActionRetrySafety = 'safe' | 'unsafe'
360
+
361
+ /**
362
+ * What a window-scoped input action learned about its own delivery.
363
+ *
364
+ * @experimental See {@link ComputerUseHost.executeWindow}.
365
+ */
366
+ export interface WindowInputResult {
367
+ readonly delivery: ActionDelivery
368
+ readonly retrySafety: ActionRetrySafety
369
+ readonly detail?: string
370
+ /** What the window read afterwards, when the host verified the input by reading it. */
371
+ readonly readBack?: {
372
+ readonly expected?: string
373
+ readonly observed?: string
374
+ }
278
375
  }
279
376
 
280
377
  export interface Point {
@@ -294,7 +391,11 @@ export type ComputerUseAction =
294
391
  | { readonly type: 'screenshot' }
295
392
  | { readonly type: 'cursor_position' }
296
393
  | { readonly type: 'mouse_move'; readonly to: Point }
297
- | { readonly type: 'mouse_click'; readonly at: Point; readonly button: MouseButton }
394
+ | {
395
+ readonly type: 'mouse_click'
396
+ readonly at: Point
397
+ readonly button: MouseButton
398
+ }
298
399
  | {
299
400
  readonly type: 'mouse_drag'
300
401
  readonly from: Point
@@ -335,7 +436,13 @@ export function assertComputerUseActionType(type: ComputerUseAction['type']): vo
335
436
  export type ComputerUseResult =
336
437
  | { readonly type: 'screenshot'; readonly result: ScreenshotResult }
337
438
  | { readonly type: 'cursor_position'; readonly point: Point }
338
- | { readonly type: 'ok' }
439
+ | {
440
+ readonly type: 'ok'
441
+ /** Set by a host that can tell more than "the call returned". */
442
+ readonly delivery?: ActionDelivery
443
+ readonly retrySafety?: ActionRetrySafety
444
+ readonly detail?: string
445
+ }
339
446
 
340
447
  /**
341
448
  * A state-changing desktop action started, but its subprocess did not report
@@ -370,49 +477,84 @@ export interface ComputerUseHost {
370
477
  * Points in `action` are physical pixels relative to the display of the
371
478
  * most recent capture; a `screenshot` result carries that display in
372
479
  * {@link ScreenshotResult.display}.
480
+ *
481
+ * Every method that reaches the desktop takes a trailing `signal`: the
482
+ * tool call's cancellation (the user interrupted the turn). On abort a host
483
+ * stops waiting for the desktop and answers at once. An action that may
484
+ * already have been sent answers `delivered_unverified` with `retrySafety:
485
+ * 'unsafe'`; one that provably was not answers `not_delivered` with
486
+ * `'safe'`. A read throws.
373
487
  */
374
- execute(action: ComputerUseAction): Promise<ComputerUseResult>
488
+ execute(action: ComputerUseAction, signal?: AbortSignal): Promise<ComputerUseResult>
375
489
 
376
490
  /**
377
491
  * The visible, titled top-level windows, front to back where the platform
378
492
  * knows the order. Offered to the model only with
379
493
  * {@link ComputerUseCapabilities.windows}.
380
494
  */
381
- listWindows?(): Promise<readonly WindowInfo[]>
495
+ listWindows?(signal?: AbortSignal): Promise<readonly WindowInfo[]>
382
496
  /**
383
497
  * Bring a window to the front, restoring it when minimized. Offered only
384
498
  * with {@link ComputerUseCapabilities.windows}.
385
499
  */
386
- focusWindow?(id: string): Promise<FocusWindowResult>
500
+ focusWindow?(id: string, signal?: AbortSignal): Promise<FocusWindowResult>
501
+ /**
502
+ * The model's turn `turnId` is making a tool call. A host that keeps state
503
+ * per turn (an Escape stop, refused permissions) starts a new one when the
504
+ * id changes.
505
+ */
506
+ beginTurn?(turnId: string): void
507
+ /**
508
+ * The model's turn ended or was interrupted. A host releases anything it
509
+ * holds for the turn: a lease on the screen, an input monitor, a window it
510
+ * moved forward.
511
+ */
512
+ endTurn?(): void | Promise<void>
387
513
  /** Capture a named window. Never silently replace a failed capture with the desktop. */
388
- captureWindow?(id: string): Promise<WindowScreenshotResult>
514
+ captureWindow?(id: string, signal?: AbortSignal): Promise<WindowScreenshotResult>
389
515
  /**
390
516
  * Act in the PNG pixel space of `captureId`. The host must verify the
391
517
  * capture is current and still belongs to the same process/window before
392
518
  * dispatching input. No implicit fallback to display input is permitted.
393
519
  */
394
- executeWindow?(captureId: string, action: WindowInputAction): Promise<void>
520
+ // `void` stays in the union: a host that returns nothing (`Promise<void>`) must keep compiling.
521
+ executeWindow?(
522
+ captureId: string,
523
+ action: WindowInputAction,
524
+ signal?: AbortSignal,
525
+ // biome-ignore lint/suspicious/noConfusingVoidType: widening an existing `Promise<void>` return
526
+ ): Promise<void | WindowInputResult>
395
527
  /**
396
528
  * Capture one region of the current display at full physical resolution.
397
529
  * `rect` is display-relative physical pixels; the result's `width` and
398
530
  * `height` are the region's. Used by `zoom` when
399
531
  * {@link ComputerUseCapabilities.regionCapture} is set.
400
532
  */
401
- captureRegion?(rect: Rect): Promise<ScreenshotResult>
533
+ captureRegion?(rect: Rect, signal?: AbortSignal): Promise<ScreenshotResult>
402
534
  /**
403
535
  * The accessibility tree of one window, or of the focused window when
404
- * `windowId` is omitted. Offered only with {@link ComputerUseCapabilities.uiTree}.
536
+ * `windowId` is omitted. With `options.root` only that element's subtree is
537
+ * read. Offered only with {@link ComputerUseCapabilities.uiTree}.
405
538
  *
406
539
  * @experimental
407
540
  */
408
- uiSnapshot?(windowId?: string): Promise<UiSnapshot>
541
+ uiSnapshot?(
542
+ windowId?: string,
543
+ options?: UiSnapshotOptions,
544
+ signal?: AbortSignal,
545
+ ): Promise<UiSnapshot>
409
546
  /**
410
- * Act on an element from the latest {@link uiSnapshot}. `value` is the text
547
+ * Act on an element from an earlier {@link uiSnapshot}. `value` is the text
411
548
  * for `set_value`. Offered only with {@link ComputerUseCapabilities.uiTree}.
412
549
  *
413
550
  * @experimental
414
551
  */
415
- uiAct?(ref: string, action: UiElementAction, value?: string): Promise<UiActResult>
552
+ uiAct?(
553
+ ref: string,
554
+ action: UiElementAction,
555
+ value?: string,
556
+ signal?: AbortSignal,
557
+ ): Promise<UiActResult>
416
558
 
417
559
  initialize?(): Promise<void>
418
560
  dispose?(): Promise<void>
@@ -164,6 +164,12 @@ export const DEFAULT_ERROR_RULES: readonly ErrorCatalogRule[] = [
164
164
  message: 'The provider rejected the credentials for this turn.',
165
165
  hint: 'Check that the API key is set, is not expired, and belongs to the account the model is billed to. Retrying will not help until it changes.',
166
166
  },
167
+ {
168
+ id: 'provider.usage_limit',
169
+ when: (facts) => facts.code === 'usage_limit',
170
+ message: 'The account has used up its allowance until the limit resets.',
171
+ hint: 'Waiting inside this turn cannot help. Continue on another account or provider, or resume after the reset time the provider reported.',
172
+ },
167
173
  {
168
174
  id: 'provider.rate_limit',
169
175
  when: (facts) => facts.code === 'rate_limit' || facts.status === 429,
@@ -549,6 +549,10 @@ export interface PluginManifest {
549
549
  * owns delegation, not by the runtime.
550
550
  */
551
551
  readonly agents?: readonly string[]
552
+ /** Operating systems the plugin supports, as `process.platform` names them. Absent means all. */
553
+ readonly platforms?: readonly string[]
554
+ /** Runtime versions the plugin supports. Absent means any. */
555
+ readonly engines?: { readonly namzu?: string }
552
556
  }
553
557
 
554
558
  export const PluginMCPServerConfigSchema = z
@@ -589,6 +593,10 @@ export const PluginManifestSchema = z.object({
589
593
  personas: z.array(z.string()).max(MAX_PERSONAS_PER_PLUGIN).optional(),
590
594
  commands: z.array(PluginRelativeDirSchema).max(MAX_COMMAND_DIRS_PER_PLUGIN).optional(),
591
595
  agents: z.array(PluginRelativeDirSchema).max(MAX_AGENT_DIRS_PER_PLUGIN).optional(),
596
+ /** Operating systems the plugin supports, as `process.platform` names them. Absent means all. */
597
+ platforms: z.array(z.string().min(1)).max(16).optional(),
598
+ /** Runtime versions the plugin supports. Absent means any. */
599
+ engines: z.object({ namzu: z.string().min(1).max(200).optional() }).optional(),
592
600
  })
593
601
 
594
602
  // ---------------------------------------------------------------------------
@@ -15,6 +15,12 @@
15
15
  export type ProviderErrorCode =
16
16
  /** 429. Back off; honour `retryAfterMs` when the server sent one. */
17
17
  | 'rate_limit'
18
+ /**
19
+ * The account's usage allowance is spent until a later reset, as opposed to
20
+ * a momentary burst limit. Waiting inside this turn cannot help, so it is
21
+ * not retried; a host may offer another account instead.
22
+ */
23
+ | 'usage_limit'
18
24
  /** 529 / 503 — the model is up but saturated. Back off. */
19
25
  | 'overloaded'
20
26
  /** Any other 5xx. Back off. */
@@ -487,7 +493,10 @@ const STRUCTURAL_CODES: Readonly<Record<string, ProviderErrorCode>> = {
487
493
  string_above_max_length: 'context_length_exceeded',
488
494
  rate_limit_exceeded: 'rate_limit',
489
495
  rate_limit_error: 'rate_limit',
490
- insufficient_quota: 'rate_limit',
496
+ insufficient_quota: 'usage_limit',
497
+ usage_limit_reached: 'usage_limit',
498
+ usage_limit_exceeded: 'usage_limit',
499
+ billing_error: 'usage_limit',
491
500
  overloaded_error: 'overloaded',
492
501
  content_filter: 'content_filter',
493
502
  content_policy_violation: 'content_filter',
@@ -497,6 +506,24 @@ const STRUCTURAL_CODES: Readonly<Record<string, ProviderErrorCode>> = {
497
506
  model_not_found: 'not_found',
498
507
  }
499
508
 
509
+ /**
510
+ * Is this exact provider code, or the provider's own complaint, a spent
511
+ * allowance? The codes are the vendors' identifiers (`insufficient_quota`,
512
+ * `usage_limit_reached`, `usage_limit_exceeded`, `billing_error`); the
513
+ * sentences are the two vendors that word it only in prose: an exhausted
514
+ * prepaid balance and an exhausted plan allowance.
515
+ */
516
+ const USAGE_LIMIT_DETAIL: readonly RegExp[] = [
517
+ /credit balance is too low/i,
518
+ /exceeded your current quota/i,
519
+ /usage limit (?:has been )?(?:reached|exceeded)/i,
520
+ ]
521
+
522
+ export function isUsageLimitSignal(code: string | undefined, detail: string | undefined): boolean {
523
+ if (code !== undefined && STRUCTURAL_CODES[code.toLowerCase()] === 'usage_limit') return true
524
+ return detail !== undefined && USAGE_LIMIT_DETAIL.some((re) => re.test(detail))
525
+ }
526
+
500
527
  function codeFromStructure(err: unknown): ProviderErrorCode | undefined {
501
528
  for (const link of causeChain(err)) {
502
529
  for (const field of [link.code, link.type, (link.error as { type?: unknown })?.type]) {
@@ -556,6 +583,8 @@ function classifyFromProviderRequestError(
556
583
  const candidate = err as Error & {
557
584
  kind?: unknown
558
585
  providerId?: unknown
586
+ providerCode?: unknown
587
+ detail?: unknown
559
588
  status?: unknown
560
589
  retryAfterMs?: unknown
561
590
  shouldRetry?: unknown
@@ -563,15 +592,23 @@ function classifyFromProviderRequestError(
563
592
  if (typeof candidate.kind !== 'string') return undefined
564
593
  const mapped = KIND_TO_CODE[candidate.kind]
565
594
  if (!mapped) return undefined
595
+ // A throttle or an invalid request that says the allowance is spent is not
596
+ // a burst limit: resending the same request cannot succeed.
597
+ const spent =
598
+ (candidate.kind === 'throttle' || candidate.kind === 'bad_request') &&
599
+ isUsageLimitSignal(
600
+ typeof candidate.providerCode === 'string' ? candidate.providerCode : undefined,
601
+ typeof candidate.detail === 'string' ? candidate.detail : undefined,
602
+ )
566
603
 
567
604
  const retryAfterMs =
568
605
  typeof candidate.retryAfterMs === 'number' ? candidate.retryAfterMs : readRetryAfterMs(err, now)
569
606
 
570
607
  return new ProviderError({
571
- code: mapped.code,
608
+ code: spent ? 'usage_limit' : mapped.code,
572
609
  message: err.message,
573
610
  // The provider saying "do not resend" beats the kind's own verdict.
574
- retryable: candidate.shouldRetry === false ? false : mapped.retryable,
611
+ retryable: spent || candidate.shouldRetry === false ? false : mapped.retryable,
575
612
  cause: err,
576
613
  ...(typeof candidate.providerId === 'string'
577
614
  ? { providerId: candidate.providerId }
@@ -89,6 +89,18 @@ export function assertSandboxEnvironment(env: SandboxEnvironment): void {
89
89
  // Exec result
90
90
  // ---------------------------------------------------------------------------
91
91
 
92
+ /**
93
+ * A backend's reading of why a non-zero result is not simply "the command
94
+ * failed". Inference from the error text, never proof.
95
+ *
96
+ * - `denial`: the sandbox appears to have refused a file access.
97
+ * - `runner-failure`: the launcher failed before the command ran.
98
+ */
99
+ export interface SandboxExecAttribution {
100
+ readonly kind: 'denial' | 'runner-failure'
101
+ readonly message: string
102
+ }
103
+
92
104
  export interface SandboxExecResult {
93
105
  readonly exitCode: number
94
106
  readonly stdout: string
@@ -107,6 +119,8 @@ export interface SandboxExecResult {
107
119
  */
108
120
  readonly stdoutTruncated?: boolean
109
121
  readonly stderrTruncated?: boolean
122
+ /** Set when a non-zero exit looks like a sandbox denial or a launcher failure. */
123
+ readonly attribution?: SandboxExecAttribution
110
124
  }
111
125
 
112
126
  // ---------------------------------------------------------------------------
@@ -189,10 +203,35 @@ export interface SandboxWalkFilesOptions {
189
203
  readonly maxDepth?: number
190
204
  /** Positive safe integer bounding emitted matching files. */
191
205
  readonly maxEntries: number
192
- /** Positive safe integer bounding examined directory entries; defaults to 20,000. */
206
+ /**
207
+ * Positive safe integer bounding examined directory entries; defaults to
208
+ * 500,000. Reaching it ends the walk with the entries found so far (see
209
+ * {@link SandboxWalkSummary.stopped}) unless `failOnLimit` is set.
210
+ */
193
211
  readonly maxVisitedEntries?: number
194
212
  /** Match hidden names through wildcards; explicit dotfile patterns always work. Defaults to false. */
195
213
  readonly includeHidden?: boolean
214
+ /**
215
+ * Skip files and directories matched by `.gitignore`, `.ignore` and
216
+ * `.git/info/exclude` (the first only inside a git repository), and never
217
+ * descend into version-control directories. The root itself is never
218
+ * skipped. Defaults to true; pass false to walk ignored trees too.
219
+ */
220
+ readonly respectIgnore?: boolean
221
+ /** Throw an error with code `ERR_FILE_WALK_LIMIT` at `maxVisitedEntries` instead of ending. Defaults to false. */
222
+ readonly failOnLimit?: boolean
223
+ /** Called once when the walk ends, with how it ended. A walker that cannot report omits the call. */
224
+ readonly onSummary?: (summary: SandboxWalkSummary) => void
225
+ }
226
+
227
+ /** How a bounded walk ended. */
228
+ export interface SandboxWalkSummary {
229
+ /** Directory entries examined. */
230
+ readonly visited: number
231
+ /** Directories skipped because an ignore rule matched them. */
232
+ readonly ignoredDirectories: number
233
+ /** Present when the walk ended at its entry bound, so the entries are not the whole tree. */
234
+ readonly stopped?: 'entry-limit'
196
235
  }
197
236
 
198
237
  // ---------------------------------------------------------------------------
@@ -278,7 +278,9 @@ export const TurnSettlementSchema = z
278
278
  const completedSettlement = TurnSettlementSchema.extend({
279
279
  status: z.enum(['completed', 'cancelled']),
280
280
  })
281
- const failedSettlement = TurnSettlementSchema.extend({ status: z.literal('failed') })
281
+ const failedSettlement = TurnSettlementSchema.extend({
282
+ status: z.literal('failed'),
283
+ })
282
284
 
283
285
  /** A body too large for one record, written (and fsynced) to `tool-results/` before the record. */
284
286
  const spill = z
@@ -576,6 +578,19 @@ export const BudgetBoundRecordSchema = inTurn('budget_bound', {
576
578
  accountId: text.min(1),
577
579
  })
578
580
 
581
+ /**
582
+ * The account a session's provider calls run under. Written when a session is
583
+ * bound to an account other than the provider's `default`, and again on each
584
+ * switch; never inside an open turn's work, only between turns. Replay reads
585
+ * the last one. A build that predates this record refuses a log holding it, so
586
+ * a session that never leaves the default account never writes one.
587
+ */
588
+ export const AccountBoundRecordSchema = recordSchema('account_bound', {
589
+ accountId: text.min(1),
590
+ provider: text.min(1),
591
+ reason: z.enum(['start', 'user_switch', 'limit_switch']),
592
+ })
593
+
579
594
  export const LogRepairedRecordSchema = recordSchema('log_repaired', {
580
595
  truncatedBytes: positive,
581
596
  lastGoodSeq: positive,
@@ -612,6 +627,7 @@ const RECORD_ONLY_SCHEMAS = {
612
627
  child_session_ended: ChildSessionEndedRecordSchema,
613
628
  audit: AuditRecordSchema,
614
629
  budget_bound: BudgetBoundRecordSchema,
630
+ account_bound: AccountBoundRecordSchema,
615
631
  log_repaired: LogRepairedRecordSchema,
616
632
  } as const
617
633
 
@@ -784,6 +800,36 @@ const RECORD_OPTIONS = [
784
800
  ...z.ZodDiscriminatedUnionOption<'type'>[],
785
801
  ]
786
802
 
803
+ /**
804
+ * A record of a type this reader does not know, written by a newer writer
805
+ * that flagged it `ignorable: true`: a promise that no reader needs it to
806
+ * rebuild the conversation. It is kept in the hash chain and verified like any
807
+ * other record, but never moves the fold. A known type never qualifies (its
808
+ * own schema decides), and an unknown type without the flag is still refused,
809
+ * so a record that does move the fold fails loudly on an older reader.
810
+ */
811
+ export const IgnorableRecordSchema = z
812
+ .object({
813
+ ...envelopeShape,
814
+ type: z
815
+ .string()
816
+ .min(1)
817
+ .refine((type) => !(SESSION_RECORD_TYPES as readonly string[]).includes(type), {
818
+ message: 'a type this reader knows is validated by its own schema',
819
+ }),
820
+ ignorable: z.literal(true),
821
+ })
822
+ .passthrough()
823
+ .superRefine((record, context) => {
824
+ if (record.seq === 1 || record.prev === null) {
825
+ context.addIssue({
826
+ code: z.ZodIssueCode.custom,
827
+ path: ['type'],
828
+ message: 'session_started is the first record of a session log, and only the first',
829
+ })
830
+ }
831
+ })
832
+
787
833
  /** Validates one session record: envelope, discriminant, and every specified payload. */
788
834
  export const SessionRecordSchema = z
789
835
  .discriminatedUnion('type', RECORD_OPTIONS)
@@ -959,7 +1005,9 @@ export type SessionEventRecord<T extends PersistedSessionEventType = PersistedSe
959
1005
  ? Omit<EventOf<T>, 'sessionId' | 'turnId' | 'lineage' | 'v' | 'seq' | 'generation'> &
960
1006
  RecordEnvelope &
961
1007
  (T extends 'tool_completed'
962
- ? { readonly structuredResultSpill?: z.infer<typeof structuredResultSpill> }
1008
+ ? {
1009
+ readonly structuredResultSpill?: z.infer<typeof structuredResultSpill>
1010
+ }
963
1011
  : T extends 'turn_completed'
964
1012
  ? {
965
1013
  readonly resultSpill?: z.infer<typeof spill>
@@ -982,6 +1030,7 @@ export type CompactionRecord = z.infer<typeof CompactionRecordSchema>
982
1030
  export type ChildSessionEndedRecord = z.infer<typeof ChildSessionEndedRecordSchema>
983
1031
  export type AuditRecord = z.infer<typeof AuditRecordSchema>
984
1032
  export type BudgetBoundRecord = z.infer<typeof BudgetBoundRecordSchema>
1033
+ export type AccountBoundRecord = z.infer<typeof AccountBoundRecordSchema>
985
1034
  export type LogRepairedRecord = z.infer<typeof LogRepairedRecordSchema>
986
1035
 
987
1036
  export type SessionRecordOnly =
@@ -998,6 +1047,7 @@ export type SessionRecordOnly =
998
1047
  | ChildSessionEndedRecord
999
1048
  | AuditRecord
1000
1049
  | BudgetBoundRecord
1050
+ | AccountBoundRecord
1001
1051
  | LogRepairedRecord
1002
1052
 
1003
1053
  /** One line of a session log. */