@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
@@ -53,6 +53,8 @@ export interface WorkflowAgentOutcome {
53
53
  readonly tokens: number
54
54
  readonly toolCalls: number
55
55
  readonly error?: string
56
+ /** The id of the child session the host ran this agent in, recorded in the journal. */
57
+ readonly sessionId?: string
56
58
  }
57
59
 
58
60
  /**
@@ -113,6 +115,8 @@ export interface WorkflowAgentEvent {
113
115
  readonly resultPreview?: string
114
116
  readonly error?: string
115
117
  readonly skipped?: boolean
118
+ /** True when the agent did not run: a resumed run returned the result it saved earlier. */
119
+ readonly cached?: boolean
116
120
  }
117
121
 
118
122
  export interface WorkflowLogEvent {
@@ -133,6 +137,8 @@ export interface WorkflowUsage {
133
137
  readonly tokens: number
134
138
  readonly toolCalls: number
135
139
  readonly elapsedMs: number
140
+ /** Agents that returned a result saved by an earlier pass instead of running (a subset of `agentsDone`). */
141
+ readonly cachedAgents?: number
136
142
  }
137
143
 
138
144
  export interface WorkflowFinishedEvent {
@@ -177,6 +183,14 @@ export type WorkflowJournalRecord =
177
183
  readonly meta: WorkflowMeta
178
184
  readonly args: unknown
179
185
  readonly limits: WorkflowResolvedLimits
186
+ /** Present on every pass after the first; a resumed run keeps its run id and journal. */
187
+ readonly resume?: {
188
+ readonly pass: number
189
+ readonly previousFinish: WorkflowFinishStatus | 'unfinished'
190
+ readonly scriptSha256Changed: boolean
191
+ }
192
+ /** Where the script came from when the host read it from a file. */
193
+ readonly scriptPath?: string
180
194
  }
181
195
  | {
182
196
  readonly type: 'phase'
@@ -200,6 +214,12 @@ export type WorkflowJournalRecord =
200
214
  readonly effort?: string
201
215
  readonly schema: boolean
202
216
  }
217
+ /** Identifies the call across passes; see `workflowCallKey`. */
218
+ readonly key?: string
219
+ /** 1 for the first pass. A record from an older pass than the latest `run_started` is stale. */
220
+ readonly pass?: number
221
+ /** The agent did not run; its result came from an earlier pass. */
222
+ readonly cached?: true
203
223
  }
204
224
  | {
205
225
  readonly type: 'agent_outcome'
@@ -213,6 +233,18 @@ export type WorkflowJournalRecord =
213
233
  readonly finishedAt: number
214
234
  readonly resultPreview?: string
215
235
  readonly error?: string
236
+ readonly key?: string
237
+ readonly pass?: number
238
+ readonly cached?: true
239
+ /** The agent's full value (after schema validation); present only for a finished, non-null result. */
240
+ readonly result?: unknown
241
+ /** A value existed but could not be saved; a resume runs this agent again. */
242
+ readonly unsaved?: true
243
+ /** The run or the agent was stopped before the agent finished; a resume runs it again and keeps its saved results valid. */
244
+ readonly interrupted?: true
245
+ /** Where a cached result was first recorded. */
246
+ readonly resultFrom?: { readonly pass: number; readonly index: number }
247
+ readonly childSessionId?: string
216
248
  }
217
249
  | { readonly type: 'log'; readonly at: number; readonly message: string }
218
250
  | {
@@ -222,12 +254,14 @@ export type WorkflowJournalRecord =
222
254
  readonly result?: unknown
223
255
  readonly error?: string
224
256
  readonly usage: WorkflowUsage
257
+ /** Written by a later session for a run whose process ended without finishing it. */
258
+ readonly recovered?: true
225
259
  }
226
260
 
227
261
  /** Where a run's audit trail goes. Injected so the SDK stays host-agnostic. */
228
262
  export interface WorkflowJournal {
229
263
  /** Called once, before the first record, with the exact script text. */
230
- writeScript?(source: string, sha256: string): Promise<void> | void
264
+ writeScript?(source: string, sha256: string, pass?: number): Promise<void> | void
231
265
  append(record: WorkflowJournalRecord): Promise<void> | void
232
266
  }
233
267
 
@@ -283,6 +317,80 @@ export interface WorkflowRunOptions {
283
317
  readonly onControl?: (control: WorkflowControl) => void
284
318
  /** Default: a fresh `w` + 8 base36 id. */
285
319
  readonly runId?: string
320
+ /**
321
+ * Continue an earlier run: agents whose prompt and options are unchanged
322
+ * return the result saved in the journal instead of running. Build it with
323
+ * `loadWorkflowResume`, and pass the same `runId`.
324
+ */
325
+ readonly resume?: WorkflowResumeState
326
+ /**
327
+ * Make the script repeatable, which resuming needs: `Date.now()`,
328
+ * `new Date()` with no argument and `Math.random()` throw, and `eval` is
329
+ * removed. Default false.
330
+ */
331
+ readonly deterministic?: boolean
332
+ /** Say in a failure that the run can be resumed. Default false. */
333
+ readonly canResume?: boolean
334
+ /** Recorded in `run_started` when the host read the script from a file. */
335
+ readonly scriptPath?: string
336
+ }
337
+
338
+ /** Everything a resumed pass needs from the earlier passes of one run. */
339
+ export interface WorkflowResumeState {
340
+ /** The number of the pass about to run (2 for the first resume). */
341
+ readonly pass: number
342
+ readonly results: ReadonlyMap<
343
+ string,
344
+ {
345
+ readonly value: unknown
346
+ readonly pass: number
347
+ readonly index: number
348
+ readonly childSessionId?: string
349
+ }
350
+ >
351
+ /** Keys the earlier passes asked for, finished or not. */
352
+ readonly seen: ReadonlySet<string>
353
+ /** Keys whose last outcome was a failure or a skip. */
354
+ readonly failed: ReadonlySet<string>
355
+ /** Keys that were asked for again after a result was saved and never finished since. */
356
+ readonly demoted: ReadonlySet<string>
357
+ readonly scriptSha256?: string
358
+ readonly previousFinish: WorkflowFinishStatus | 'unfinished'
359
+ /** Plain-language findings about the journal (changed, cut short, saved before resuming existed). */
360
+ readonly problems: readonly string[]
361
+ readonly resumable: boolean
362
+ /** First line (1-based) the integrity check could not trust, when any. */
363
+ readonly brokenAt?: number
364
+ }
365
+
366
+ /** A journal that can be read back, with its integrity checked. */
367
+ export interface WorkflowJournalReader {
368
+ read(): Promise<CheckedWorkflowJournal>
369
+ }
370
+
371
+ /** A journal record as written by `FileWorkflowJournal`: numbered and chained to the line before it. */
372
+ export type ChainedWorkflowJournalRecord = WorkflowJournalRecord & {
373
+ readonly seq: number
374
+ readonly prev: string
375
+ }
376
+
377
+ /** What reading a journal found. Only `records` can be trusted. */
378
+ export interface CheckedWorkflowJournal {
379
+ /** Records that passed the integrity check, in order. */
380
+ readonly records: readonly ChainedWorkflowJournalRecord[]
381
+ /** The journal was written before records were chained; it can be shown but not resumed. */
382
+ readonly legacy: boolean
383
+ /** The last write was cut off mid-line and was ignored. */
384
+ readonly tornTail: boolean
385
+ /** First line (1-based) that failed the check, when lines were changed, removed or cut short. */
386
+ readonly brokenAt?: number
387
+ /** Sequence number and hash the next record must chain from. */
388
+ readonly lastSeq: number
389
+ readonly lastHash: string
390
+ /** The file ends in a newline (false after a torn write). */
391
+ readonly endsWithNewline: boolean
392
+ /** Records of a legacy journal, for display only. */
393
+ readonly untrusted: readonly WorkflowJournalRecord[]
286
394
  }
287
395
 
288
396
  export interface WorkflowRunResult {
package/src/index.ts CHANGED
@@ -41,10 +41,27 @@ export type {
41
41
  MonitorStreamOptions,
42
42
  MonitorStreamStats,
43
43
  } from './runtime/jobs/monitor-stream.js'
44
+ export { WakeBudget, wakeBudgetFromEnv } from './runtime/jobs/wake-budget.js'
44
45
  export {
45
46
  createBackgroundJobId,
46
47
  isBackgroundJobId,
47
48
  } from './runtime/jobs/job-id.js'
49
+ // Three facts about a running item (where it is, when it last progressed,
50
+ // whether it is alive) and the one verdict derived from them.
51
+ export {
52
+ LIVENESS_QUIET_MS,
53
+ LIVENESS_STALLED_MS,
54
+ formatLivenessAge,
55
+ formatLivenessLine,
56
+ judgeLiveness,
57
+ } from './runtime/jobs/liveness.js'
58
+ export type {
59
+ Liveness,
60
+ LivenessInput,
61
+ LivenessKind,
62
+ LivenessThresholds,
63
+ LivenessVerdict,
64
+ } from './runtime/jobs/liveness.js'
48
65
  export type {
49
66
  BackgroundJob,
50
67
  ForegroundCommand,
@@ -43,9 +43,27 @@ export const PeerRecordSchema = z
43
43
  token: z.string().min(1),
44
44
  protocol: z.literal(PEER_PROTOCOL_VERSION),
45
45
  cliVersion: z.string(),
46
+ /** The slot a spaces host gave this terminal at launch; how the host finds its own sessions. */
47
+ hostSlot: z.string().min(1).max(128).optional(),
48
+ /** The conversation this terminal is on, once it has one. */
49
+ conversationId: z.string().min(1).optional(),
50
+ /** The label of the non-default account the conversation is bound to; absent for the default. */
51
+ account: z.string().min(1).optional(),
52
+ /** True while the bound account is out of allowance and the terminal is waiting for a choice. */
53
+ limited: z.boolean().optional(),
54
+ /** When the allowance is expected back (epoch ms); absent when the provider did not say. */
55
+ limitedUntil: z.number().int().positive().optional(),
46
56
  })
47
57
  .strict()
48
58
 
59
+ /**
60
+ * The reader's view of a record: the same fields, but a key this build does
61
+ * not know is dropped instead of failing the whole record. A record written by
62
+ * a newer build (extra optional fields) stays visible to an older reader.
63
+ * Writers keep using the strict `PeerRecordSchema`.
64
+ */
65
+ export const PeerRecordReaderSchema = PeerRecordSchema.strip()
66
+
49
67
  export type PeerRecord = z.infer<typeof PeerRecordSchema>
50
68
 
51
69
  const PEER_REF_HEX_LENGTH = 6
@@ -20,7 +20,7 @@ import {
20
20
  import { basename, dirname, join } from 'node:path'
21
21
  import { parsePeerAddress } from './address.js'
22
22
  import { pingPeer } from './client.js'
23
- import { type PeerRecord, PeerRecordSchema } from './record.js'
23
+ import { type PeerRecord, PeerRecordReaderSchema, PeerRecordSchema } from './record.js'
24
24
 
25
25
  const RECORD_FILE_MODE = 0o600
26
26
 
@@ -75,7 +75,7 @@ function parseRecordText(text: string): PeerRecord | undefined {
75
75
  } catch {
76
76
  return undefined
77
77
  }
78
- const result = PeerRecordSchema.safeParse(parsed)
78
+ const result = PeerRecordReaderSchema.safeParse(parsed)
79
79
  return result.success ? result.data : undefined
80
80
  }
81
81
 
@@ -0,0 +1,173 @@
1
+ import type { PluginManifest } from '../types/plugin/index.js'
2
+
3
+ /**
4
+ * Whether a plugin can run on this machine and this runtime, decided from what
5
+ * its manifest declares. A manifest that declares nothing is compatible with
6
+ * everything, so every plugin written before these fields existed loads as it did.
7
+ */
8
+
9
+ type Triple = readonly [number, number, number]
10
+
11
+ interface Parsed {
12
+ readonly triple: Triple
13
+ /** The prerelease tag (`rc.1`), or undefined for a release. */
14
+ readonly pre: string | undefined
15
+ /** How many numeric parts were written: `1` is 1, `1.4` is 2, `1.4.2` is 3. */
16
+ readonly written: number
17
+ }
18
+
19
+ const VERSION_PATTERN =
20
+ /^v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/i
21
+
22
+ function parseVersion(text: string): Parsed | undefined {
23
+ const match = VERSION_PATTERN.exec(text.trim())
24
+ if (!match) return undefined
25
+ const part = (value: string | undefined): number =>
26
+ value === undefined || value === 'x' || value === '*' ? 0 : Number(value)
27
+ const written = [match[1], match[2], match[3]].filter(
28
+ (p) => p !== undefined && p !== 'x' && p !== '*',
29
+ ).length
30
+ return {
31
+ triple: [Number(match[1]), part(match[2]), part(match[3])],
32
+ pre: match[4],
33
+ written,
34
+ }
35
+ }
36
+
37
+ function compareTriples(a: Triple, b: Triple): number {
38
+ for (let i = 0; i < 3; i++) {
39
+ const diff = (a[i] as number) - (b[i] as number)
40
+ if (diff !== 0) return diff
41
+ }
42
+ return 0
43
+ }
44
+
45
+ /** Full precedence: a release is above any prerelease of the same numbers. */
46
+ function compare(a: Parsed, b: Parsed): number {
47
+ const order = compareTriples(a.triple, b.triple)
48
+ if (order !== 0) return order
49
+ if (a.pre === b.pre) return 0
50
+ if (a.pre === undefined) return 1
51
+ if (b.pre === undefined) return -1
52
+ return a.pre < b.pre ? -1 : 1
53
+ }
54
+
55
+ /**
56
+ * The first version a `^` or `~` range excludes, from how many parts were
57
+ * written. `^1` and `~1` stop below 2.0.0; `~1.4` and `~1.4.2` below 1.5.0;
58
+ * `^0` below 1.0.0, `^0.2` below 0.3.0, `^0.0.3` below 0.0.4.
59
+ */
60
+ function upperBound(operator: '^' | '~', target: Parsed): Triple {
61
+ const [major, minor, patch] = target.triple
62
+ if (operator === '~') return target.written <= 1 ? [major + 1, 0, 0] : [major, minor + 1, 0]
63
+ if (major > 0 || target.written <= 1) return [major + 1, 0, 0]
64
+ if (minor > 0 || target.written === 2) return [0, minor + 1, 0]
65
+ return [0, 0, patch + 1]
66
+ }
67
+
68
+ /** One comparator such as `>=1.2.0`, `^2`, `~1.4` or `3.1.0`; undefined when unreadable. */
69
+ function satisfiesComparator(version: Parsed, token: string): boolean | undefined {
70
+ const op = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(token)
71
+ if (!op) return undefined
72
+ const operator = op[1] ?? '='
73
+ const target = parseVersion(op[2] as string)
74
+ if (!target) return undefined
75
+ const order = compare(version, target)
76
+ switch (operator) {
77
+ case '>=':
78
+ return order >= 0
79
+ case '<=':
80
+ return order <= 0
81
+ case '>':
82
+ return order > 0
83
+ case '<':
84
+ return order < 0
85
+ case '^':
86
+ case '~':
87
+ // The upper bound is compared on the numbers alone, so a prerelease of the
88
+ // next major (`2.0.0-rc.1`) is not inside `^1`.
89
+ return order >= 0 && compareTriples(version.triple, upperBound(operator, target)) < 0
90
+ default: {
91
+ // A bare `1` or `1.4` names a whole major or minor.
92
+ if (version.pre !== target.pre) return false
93
+ const written = (op[2] as string).replace(/^v/i, '').split(/[-+]/)[0] as string
94
+ const parts = written.split('.').filter((p) => p !== 'x' && p !== '*')
95
+ return parts.every((p, i) => Number(p) === version.triple[i])
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Whether `version` satisfies `range`. Supports space-separated comparators
102
+ * that must all hold, `||` between alternatives, and the operators `>=`, `>`,
103
+ * `<=`, `<`, `=`, `^` and `~`. Returns undefined when the range cannot be read.
104
+ */
105
+ export function versionSatisfies(versionText: string, range: string): boolean | undefined {
106
+ const version = parseVersion(versionText)
107
+ if (!version) return undefined
108
+ let readable = true
109
+ let any = false
110
+ for (const alternative of range.split('||')) {
111
+ // Join an operator written apart from its number (">= 1.2") first.
112
+ const tokens = alternative
113
+ .trim()
114
+ .replace(/(>=|<=|>|<|=|\^|~)\s+/g, '$1')
115
+ .split(/\s+/)
116
+ .filter(Boolean)
117
+ if (tokens.length === 0) {
118
+ readable = false
119
+ continue
120
+ }
121
+ let all = true
122
+ for (const token of tokens) {
123
+ const ok = satisfiesComparator(version, token)
124
+ if (ok === undefined) {
125
+ readable = false
126
+ all = false
127
+ break
128
+ }
129
+ if (!ok) all = false
130
+ }
131
+ if (all) any = true
132
+ }
133
+ if (any) return true
134
+ return readable ? false : undefined
135
+ }
136
+
137
+ export interface PluginHostFacts {
138
+ /** The operating system, as `process.platform` names it. Default: this process. */
139
+ readonly platform?: string
140
+ /** The Namzu runtime version. Default: this package's own version. */
141
+ readonly namzuVersion?: string
142
+ }
143
+
144
+ /**
145
+ * Refuse a plugin that declares it does not support this platform or this
146
+ * Namzu version. The reason names the plugin, what it declares and what this
147
+ * machine is, so a person can act on it without reading the manifest.
148
+ *
149
+ * A runtime whose own version is unknown (`0.0.0`, the placeholder a bundle
150
+ * reports when it cannot read its package file) is not held against the range:
151
+ * refusing every plugin on a guess would be worse than admitting one.
152
+ */
153
+ export function assertCompatible(manifest: PluginManifest, host: Required<PluginHostFacts>): void {
154
+ const platforms = manifest.platforms
155
+ if (platforms && platforms.length > 0 && !platforms.includes(host.platform)) {
156
+ throw new Error(
157
+ `Plugin "${manifest.name}" supports only ${platforms.join(', ')}, and this machine runs ${host.platform}, so it was not loaded.`,
158
+ )
159
+ }
160
+ const range = manifest.engines?.namzu
161
+ if (range === undefined || host.namzuVersion === '0.0.0') return
162
+ const ok = versionSatisfies(host.namzuVersion, range)
163
+ if (ok === undefined) {
164
+ throw new Error(
165
+ `Plugin "${manifest.name}" declares the Namzu version range "${range}", which cannot be read, so it was not loaded. Write it like ">=60.0.0" or "^60.1.0".`,
166
+ )
167
+ }
168
+ if (!ok) {
169
+ throw new Error(
170
+ `Plugin "${manifest.name}" needs Namzu ${range}, and this is Namzu ${host.namzuVersion}, so it was not loaded. Update Namzu or use a plugin version made for this one.`,
171
+ )
172
+ }
173
+ }
@@ -11,6 +11,8 @@ import { type PluginManifest, PluginManifestSchema } from '../types/plugin/index
11
11
  import { toErrorMessage } from '../utils/error.js'
12
12
  import { SCOPE_ATTRIBUTE } from '../utils/log/types.js'
13
13
  import { type Logger, resolveLogger } from '../utils/logger.js'
14
+ import { VERSION } from '../version.js'
15
+ import { type PluginHostFacts, assertCompatible } from './compat.js'
14
16
 
15
17
  /**
16
18
  * Scans a directory for subdirectories containing a plugin manifest.
@@ -161,7 +163,7 @@ const REGISTRY_BACKED_CONTRIBUTIONS = ['skills'] as const
161
163
  * through host configuration; what does not exist is the manifest path
162
164
  * into them. Naming the types is what makes that actionable.
163
165
  */
164
- export interface PluginEnablementCapabilities {
166
+ export interface PluginEnablementCapabilities extends PluginHostFacts {
165
167
  /**
166
168
  * Whether the host wired a `SkillRegistry`.
167
169
  *
@@ -178,6 +180,10 @@ export function assertEnableable(
178
180
  manifest: PluginManifest,
179
181
  capabilities: PluginEnablementCapabilities = {},
180
182
  ): void {
183
+ assertCompatible(manifest, {
184
+ platform: capabilities.platform ?? platform(),
185
+ namzuVersion: capabilities.namzuVersion ?? VERSION,
186
+ })
181
187
  const record = manifest as unknown as Record<string, unknown[] | undefined>
182
188
  // One ordered pass over all three rather than two group filters
183
189
  // concatenated, so the message keeps naming them in the order the
@@ -14,6 +14,7 @@
14
14
  * than refreshed, and `rates.source.json` for the rates themselves.
15
15
  */
16
16
 
17
+ import { stripModelWindowSuffix } from '../compaction/context-window.js'
17
18
  import type { ModelPricing } from '../utils/cost.js'
18
19
  import { VENDOR_RATES } from './catalogue.generated.js'
19
20
 
@@ -35,7 +36,7 @@ const SNAPSHOT_SUFFIX = /[-@]\d{8}$/
35
36
 
36
37
  /** Lowercase, snapshot suffix removed. Exported because tests assert it. */
37
38
  export function normaliseModelId(model: string): string {
38
- return model.toLowerCase().replace(SNAPSHOT_SUFFIX, '')
39
+ return stripModelWindowSuffix(model).toLowerCase().replace(SNAPSHOT_SUFFIX, '')
39
40
  }
40
41
 
41
42
  /**
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process'
2
+ import { readFileSync, readdirSync } from 'node:fs'
2
3
 
3
4
  /**
4
5
  * Git Bash children that the Windows process tree cannot reach.
@@ -11,13 +12,82 @@ import { execFileSync } from 'node:child_process'
11
12
  * in its environment, which every descendant inherits however it was started;
12
13
  * the tag is how the tree is found again.
13
14
  */
14
- const MSYS_SHELLS = new WeakMap<object, { readonly bash: string; readonly tag: string }>()
15
+ const MSYS_SHELLS = new WeakMap<object, { readonly bash?: string; readonly tag: string }>()
16
+
17
+ /**
18
+ * Whether a pid may be signalled, or addressed as a group with `-pid`.
19
+ *
20
+ * `kill(-1, sig)` signals every process the user may signal and `kill(0, sig)`
21
+ * signals the caller's own process group, so a recorded pid of 1, 0, a negative
22
+ * or a fractional number (a test double, a corrupt record, `NaN`) would take
23
+ * down the host's whole session. Only an integer above 1 that is not this
24
+ * process is a real, separate process. A refused pid is treated as already gone.
25
+ */
26
+ export function isSignalablePid(pid: unknown): pid is number {
27
+ return typeof pid === 'number' && Number.isInteger(pid) && pid > 1 && pid !== process.pid
28
+ }
15
29
 
16
30
  /** Remember how to find every process a Git Bash child started. Called by the spawn. */
17
31
  export function registerMsysShell(child: object, shell: { bash: string; tag: string }): void {
18
32
  MSYS_SHELLS.set(child, shell)
19
33
  }
20
34
 
35
+ /**
36
+ * Remember the tag a POSIX child's environment carries, so a kill can also find
37
+ * a descendant that left the child's process group (`setsid`, a double fork
38
+ * that starts its own session). Such a process has no group to signal, but it
39
+ * inherited the environment, and the tag is how it is found again.
40
+ *
41
+ * The limit is the same as the group's own: a process that clears its
42
+ * environment before it forks escapes this too.
43
+ */
44
+ export function registerProcessTag(child: object, tag: string): void {
45
+ MSYS_SHELLS.set(child, { tag })
46
+ }
47
+
48
+ /**
49
+ * Signal every process of this user whose environment carries one of the tags,
50
+ * by reading `/proc/<pid>/environ`. Linux only: other POSIX systems have no
51
+ * `/proc`, and there the process group is all a kill can reach. A process whose
52
+ * environment cannot be read (another user's, or one that has just ended) is
53
+ * skipped without further work.
54
+ *
55
+ * Several tags are looked for in one pass, so stopping many jobs at once reads
56
+ * each environment once, not once per job. The scan is synchronous and its cost
57
+ * grows with the number of processes on the machine. There is also a window
58
+ * between reading an environment and signalling its pid in which the pid could
59
+ * be reused by an unrelated process of the same user; the window is the length
60
+ * of one read, and the signal goes only to a pid whose environment carried the
61
+ * tag a moment before.
62
+ */
63
+ export function signalTagged(tags: string | readonly string[], signal: NodeJS.Signals): number {
64
+ const needles = (typeof tags === 'string' ? [tags] : tags).map(
65
+ (tag) => `${MSYS_TAG_VARIABLE}=${tag}\0`,
66
+ )
67
+ if (needles.length === 0) return 0
68
+ let entries: string[]
69
+ try {
70
+ entries = readdirSync('/proc')
71
+ } catch {
72
+ return 0
73
+ }
74
+ let signalled = 0
75
+ for (const entry of entries) {
76
+ if (!/^\d+$/.test(entry)) continue
77
+ const pid = Number(entry)
78
+ if (!isSignalablePid(pid)) continue
79
+ try {
80
+ const environ = readFileSync(`/proc/${entry}/environ`, 'latin1')
81
+ if (!needles.some((needle) => environ.includes(needle))) continue
82
+ process.kill(pid, signal)
83
+ signalled++
84
+ } catch {
85
+ // Not ours to read, or gone since the listing.
86
+ }
87
+ }
88
+ return signalled
89
+ }
90
+
21
91
  /** The environment variable that carries the tag. */
22
92
  export const MSYS_TAG_VARIABLE = 'NAMZU_SHELL_TAG'
23
93
 
@@ -86,17 +156,49 @@ export function killTree(
86
156
  child: { readonly pid?: number | undefined },
87
157
  signal: NodeJS.Signals,
88
158
  ): void {
89
- if (!child.pid) return
159
+ killTrees([child], signal)
160
+ }
161
+
162
+ /**
163
+ * `killTree` for several children at once: each child's group is signalled, then
164
+ * the environment scan runs once for all their tags. Used where the number of
165
+ * kills is unbounded and cannot wait, such as the host's exit.
166
+ */
167
+ export function killTrees(
168
+ children: readonly { readonly pid?: number | undefined }[],
169
+ signal: NodeJS.Signals,
170
+ ): void {
171
+ const tags: string[] = []
172
+ for (const child of children) {
173
+ if (!isSignalablePid(child.pid)) continue
174
+ killOne(child, signal, tags)
175
+ }
176
+ if (tags.length > 0 && process.platform === 'linux') signalTagged(tags, signal)
177
+ }
178
+
179
+ function killOne(
180
+ child: { readonly pid?: number | undefined },
181
+ signal: NodeJS.Signals,
182
+ tags: string[],
183
+ ): void {
184
+ if (!isSignalablePid(child.pid)) return
90
185
  try {
91
186
  if (process.platform === 'win32') {
92
187
  const msys = MSYS_SHELLS.get(child)
93
- if (msys !== undefined) killMsysTagged(msys)
188
+ if (msys?.bash !== undefined) killMsysTagged({ bash: msys.bash, tag: msys.tag })
94
189
  execFileSync(taskkillPath(), ['/pid', String(child.pid), '/t', '/f'], {
95
190
  stdio: 'ignore',
96
191
  windowsHide: true,
97
192
  })
98
193
  } else {
99
- process.kill(-child.pid, signal)
194
+ try {
195
+ process.kill(-child.pid, signal)
196
+ } finally {
197
+ // After the group, which is cheap and exact: what is left is only
198
+ // what started its own session. Also when the group is already gone.
199
+ const tagged = MSYS_SHELLS.get(child)
200
+ if (tagged !== undefined) tags.push(tagged.tag)
201
+ }
100
202
  }
101
203
  } catch {
102
204
  // Group (or tree) already gone
@@ -82,7 +82,15 @@ export const CODING_AGENT_DELEGATION_DOCTRINE = `### Planning and delegating
82
82
  - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. A fresh sub-agent starts with none of your context and returns what you ask for.
83
83
  - When the \`Agent\` tool lists a \`fork\` type, use \`subagent_type: "fork"\` when a side task would fill your context with output you will not need again, such as an open-ended survey. A fork starts from this conversation under your own instructions, so its prompt is only the directive: what to do and where its scope ends, without the background. Never write or guess what a fork will find before its report arrives. A fork cannot start forks of its own.
84
84
  - When delegated agents finish, tell the user what each one found in your next message. Their results reach you and not the user, so a result you do not pass on stays unseen.
85
- - Delegate lookups — where something is defined, which files reference it, how a module works — with \`subagent_type: "explore"\`: it has reading and searching tools only and never interrupts the user for permission. Reserve the default sub-agent for work that changes files or runs commands.`
85
+ - Delegate lookups — where something is defined, which files reference it, how a module works — with \`subagent_type: "explore"\`: it has reading and searching tools only and never interrupts the user for permission. Reserve the default sub-agent for work that changes files or runs commands.
86
+
87
+ ### Answering how work is going
88
+ - When asked how things stand while background work runs, read the note about running work on your request first if the host gives you one, and add one status read from the tool that started an item only when the note does not say enough. The answer needs no waiting or polling and leaves the work running, because stopping or restarting an item to answer would lose what it has done.
89
+ - Give each item the state you were given for it (progressing, waiting, stalled or dead) and how long ago it last moved, so the person can judge it for themselves. Stalled means only quiet for a while, not stuck.
90
+ - Group the answer as finished (with its result), running (and where it is), queued, and needing a decision. Lead with the outcome, keep it short, and write in the language the person used, states included.
91
+ - Name each item by what it is doing, not by its id: an id means nothing to the person, so give one only when they ask for it or need it for a command.
92
+ - A result exists only once its completion message has arrived, so for an unfinished item say it is still running; a predicted result reads as a fact and may be wrong.
93
+ - For an item reported dead, say it ended without a result and offer to run it again; starting the same work again unasked could repeat what it already did.`
86
94
 
87
95
  /**
88
96
  * For a session whose permission layer refuses every mutation. Says what that
@@ -33,6 +33,7 @@ import { LOG_SECRET_PATTERNS } from '../constants/secret-patterns.js'
33
33
  import type { ProviderErrorKind, ProviderRequestErrorInit } from '../types/provider/error.js'
34
34
  import {
35
35
  type TransportCause,
36
+ isUsageLimitSignal,
36
37
  readCertificateFailure,
37
38
  readTransportCause,
38
39
  } from '../types/provider/errors.js'
@@ -130,6 +131,31 @@ function providerCode(value: unknown): string | undefined {
130
131
  return safeProviderCode(record.code ?? nested)
131
132
  }
132
133
 
134
+ /**
135
+ * A vendor can name a spent allowance in `type` instead of `code`
136
+ * (`{"error":{"type":"usage_limit_reached"}}`, `{"error":{"type":"billing_error"}}`).
137
+ * Only those identifiers are lifted to a provider code; any other `type` is a
138
+ * category and stays where it was.
139
+ */
140
+ function usageLimitTypeOf(error: unknown): string | undefined {
141
+ if (typeof error !== 'object' || error === null) return undefined
142
+ const outer = error as { type?: unknown; error?: unknown }
143
+ const inner =
144
+ typeof outer.error === 'object' && outer.error !== null
145
+ ? (outer.error as { type?: unknown; error?: unknown })
146
+ : undefined
147
+ const innermost =
148
+ typeof inner?.error === 'object' && inner.error !== null
149
+ ? (inner.error as { type?: unknown })
150
+ : undefined
151
+ for (const candidate of [outer.type, inner?.type, innermost?.type]) {
152
+ if (typeof candidate === 'string' && isUsageLimitSignal(candidate, undefined)) {
153
+ return safeProviderCode(candidate)
154
+ }
155
+ }
156
+ return undefined
157
+ }
158
+
133
159
  /**
134
160
  * Credential shapes to scrub before a provider's words are kept.
135
161
  *
@@ -524,7 +550,7 @@ export function providerVendorError(input: {
524
550
  }): ProviderRequestError {
525
551
  const { error } = input
526
552
  const status = vendorErrorStatus(error)
527
- const exactProviderCode = providerCode(error)
553
+ const exactProviderCode = providerCode(error) ?? usageLimitTypeOf(error)
528
554
  const name = error instanceof Error ? error.name : ''
529
555
  const message = error instanceof Error ? error.message : ''
530
556