@namzu/sdk 59.0.0 → 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 (260) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/compaction/context-window.d.ts +53 -0
  3. package/dist/compaction/context-window.d.ts.map +1 -1
  4. package/dist/compaction/context-window.js +141 -2
  5. package/dist/compaction/context-window.js.map +1 -1
  6. package/dist/execution/workflow/index.d.ts +3 -1
  7. package/dist/execution/workflow/index.d.ts.map +1 -1
  8. package/dist/execution/workflow/index.js +2 -1
  9. package/dist/execution/workflow/index.js.map +1 -1
  10. package/dist/execution/workflow/journal.d.ts +24 -4
  11. package/dist/execution/workflow/journal.d.ts.map +1 -1
  12. package/dist/execution/workflow/journal.js +131 -9
  13. package/dist/execution/workflow/journal.js.map +1 -1
  14. package/dist/execution/workflow/prelude.d.ts +2 -0
  15. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  16. package/dist/execution/workflow/prelude.js +49 -8
  17. package/dist/execution/workflow/prelude.js.map +1 -1
  18. package/dist/execution/workflow/resume.d.ts +59 -0
  19. package/dist/execution/workflow/resume.d.ts.map +1 -0
  20. package/dist/execution/workflow/resume.js +273 -0
  21. package/dist/execution/workflow/resume.js.map +1 -0
  22. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  23. package/dist/execution/workflow/runtime.js +112 -8
  24. package/dist/execution/workflow/runtime.js.map +1 -1
  25. package/dist/execution/workflow/types.d.ts +105 -1
  26. package/dist/execution/workflow/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugin/compat.d.ts +24 -0
  32. package/dist/plugin/compat.d.ts.map +1 -0
  33. package/dist/plugin/compat.js +149 -0
  34. package/dist/plugin/compat.js.map +1 -0
  35. package/dist/plugin/loader.d.ts +2 -1
  36. package/dist/plugin/loader.d.ts.map +1 -1
  37. package/dist/plugin/loader.js +6 -0
  38. package/dist/plugin/loader.js.map +1 -1
  39. package/dist/pricing/index.d.ts.map +1 -1
  40. package/dist/pricing/index.js +2 -1
  41. package/dist/pricing/index.js.map +1 -1
  42. package/dist/process/kill-tree.d.ts +44 -0
  43. package/dist/process/kill-tree.d.ts.map +1 -1
  44. package/dist/process/kill-tree.js +102 -4
  45. package/dist/process/kill-tree.js.map +1 -1
  46. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  47. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  48. package/dist/prompt/coding-agent-doctrine.js +9 -1
  49. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  50. package/dist/provider/model-window-suffix.d.ts +18 -0
  51. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  52. package/dist/provider/model-window-suffix.js +77 -0
  53. package/dist/provider/model-window-suffix.js.map +1 -0
  54. package/dist/public-runtime.d.ts +5 -3
  55. package/dist/public-runtime.d.ts.map +1 -1
  56. package/dist/public-runtime.js +5 -4
  57. package/dist/public-runtime.js.map +1 -1
  58. package/dist/public-types.d.ts +1 -0
  59. package/dist/public-types.d.ts.map +1 -1
  60. package/dist/registry/command/kernel-commands.d.ts +1 -4
  61. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  62. package/dist/registry/command/kernel-commands.js +0 -28
  63. package/dist/registry/command/kernel-commands.js.map +1 -1
  64. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  65. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  66. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  67. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  68. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  69. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  70. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  71. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  72. package/dist/runtime/jobs/liveness.d.ts +67 -0
  73. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  74. package/dist/runtime/jobs/liveness.js +94 -0
  75. package/dist/runtime/jobs/liveness.js.map +1 -0
  76. package/dist/runtime/jobs/registry.d.ts +21 -0
  77. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  78. package/dist/runtime/jobs/registry.js +104 -2
  79. package/dist/runtime/jobs/registry.js.map +1 -1
  80. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  81. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  82. package/dist/runtime/jobs/wake-budget.js +48 -0
  83. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  84. package/dist/runtime/query/detached-calls.d.ts +65 -0
  85. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  86. package/dist/runtime/query/detached-calls.js +116 -0
  87. package/dist/runtime/query/detached-calls.js.map +1 -0
  88. package/dist/runtime/query/executor.d.ts +4 -1
  89. package/dist/runtime/query/executor.d.ts.map +1 -1
  90. package/dist/runtime/query/executor.js +90 -6
  91. package/dist/runtime/query/executor.js.map +1 -1
  92. package/dist/runtime/query/index.d.ts +20 -0
  93. package/dist/runtime/query/index.d.ts.map +1 -1
  94. package/dist/runtime/query/index.js +31 -5
  95. package/dist/runtime/query/index.js.map +1 -1
  96. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  97. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  98. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  99. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  100. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  101. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  102. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  105. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  106. package/dist/runtime/query/session-storage.js +8 -3
  107. package/dist/runtime/query/session-storage.js.map +1 -1
  108. package/dist/runtime/query/steering.d.ts +29 -3
  109. package/dist/runtime/query/steering.d.ts.map +1 -1
  110. package/dist/runtime/query/steering.js +58 -3
  111. package/dist/runtime/query/steering.js.map +1 -1
  112. package/dist/runtime/query/tooling.d.ts +1 -0
  113. package/dist/runtime/query/tooling.d.ts.map +1 -1
  114. package/dist/runtime/query/tooling.js +1 -0
  115. package/dist/runtime/query/tooling.js.map +1 -1
  116. package/dist/runtime/system-events.d.ts +5 -0
  117. package/dist/runtime/system-events.d.ts.map +1 -1
  118. package/dist/runtime/system-events.js +7 -1
  119. package/dist/runtime/system-events.js.map +1 -1
  120. package/dist/sandbox/denial.d.ts +11 -0
  121. package/dist/sandbox/denial.d.ts.map +1 -0
  122. package/dist/sandbox/denial.js +61 -0
  123. package/dist/sandbox/denial.js.map +1 -0
  124. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  125. package/dist/sandbox/file-walk-program.js +31 -13
  126. package/dist/sandbox/file-walk-program.js.map +1 -1
  127. package/dist/sandbox/file-walk.d.ts +2 -0
  128. package/dist/sandbox/file-walk.d.ts.map +1 -1
  129. package/dist/sandbox/file-walk.js +64 -13
  130. package/dist/sandbox/file-walk.js.map +1 -1
  131. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  132. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  133. package/dist/sandbox/ignore-matcher.js +11 -0
  134. package/dist/sandbox/ignore-matcher.js.map +1 -0
  135. package/dist/sandbox/ignore-source.d.ts +26 -0
  136. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  137. package/dist/sandbox/ignore-source.js +221 -0
  138. package/dist/sandbox/ignore-source.js.map +1 -0
  139. package/dist/sandbox/provider/local.d.ts.map +1 -1
  140. package/dist/sandbox/provider/local.js +11 -1
  141. package/dist/sandbox/provider/local.js.map +1 -1
  142. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  143. package/dist/scheduler/completion-inbox.js +1 -0
  144. package/dist/scheduler/completion-inbox.js.map +1 -1
  145. package/dist/session/log-hash.d.ts.map +1 -1
  146. package/dist/session/log-hash.js +18 -1
  147. package/dist/session/log-hash.js.map +1 -1
  148. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  149. package/dist/store/session-index/sqlite.js +169 -16
  150. package/dist/store/session-index/sqlite.js.map +1 -1
  151. package/dist/tools/builtins/bash.d.ts.map +1 -1
  152. package/dist/tools/builtins/bash.js +10 -4
  153. package/dist/tools/builtins/bash.js.map +1 -1
  154. package/dist/tools/builtins/computer-use.d.ts +35 -0
  155. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  156. package/dist/tools/builtins/computer-use.js +512 -67
  157. package/dist/tools/builtins/computer-use.js.map +1 -1
  158. package/dist/tools/builtins/glob.d.ts +1 -0
  159. package/dist/tools/builtins/glob.d.ts.map +1 -1
  160. package/dist/tools/builtins/glob.js +65 -28
  161. package/dist/tools/builtins/glob.js.map +1 -1
  162. package/dist/tools/builtins/grep.d.ts +2 -0
  163. package/dist/tools/builtins/grep.d.ts.map +1 -1
  164. package/dist/tools/builtins/grep.js +94 -40
  165. package/dist/tools/builtins/grep.js.map +1 -1
  166. package/dist/tools/builtins/job.d.ts +1 -1
  167. package/dist/tools/builtins/job.d.ts.map +1 -1
  168. package/dist/tools/builtins/job.js +34 -7
  169. package/dist/tools/builtins/job.js.map +1 -1
  170. package/dist/tools/builtins/search-support.d.ts +44 -0
  171. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  172. package/dist/tools/builtins/search-support.js +89 -0
  173. package/dist/tools/builtins/search-support.js.map +1 -0
  174. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  175. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  176. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  177. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  178. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  179. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  180. package/dist/tools/builtins/wait-for-job.js +13 -2
  181. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  182. package/dist/tools/command-shell.d.ts.map +1 -1
  183. package/dist/tools/command-shell.js +14 -5
  184. package/dist/tools/command-shell.js.map +1 -1
  185. package/dist/types/computer-use/index.d.ts +129 -13
  186. package/dist/types/computer-use/index.d.ts.map +1 -1
  187. package/dist/types/computer-use/index.js.map +1 -1
  188. package/dist/types/plugin/index.d.ts +24 -0
  189. package/dist/types/plugin/index.d.ts.map +1 -1
  190. package/dist/types/plugin/index.js +4 -0
  191. package/dist/types/plugin/index.js.map +1 -1
  192. package/dist/types/sandbox/index.d.ts +38 -1
  193. package/dist/types/sandbox/index.d.ts.map +1 -1
  194. package/dist/types/sandbox/index.js.map +1 -1
  195. package/dist/types/session/records.d.ts +264 -0
  196. package/dist/types/session/records.d.ts.map +1 -1
  197. package/dist/types/session/records.js +32 -1
  198. package/dist/types/session/records.js.map +1 -1
  199. package/dist/types/tool/index.d.ts +30 -0
  200. package/dist/types/tool/index.d.ts.map +1 -1
  201. package/dist/types/tool/index.js.map +1 -1
  202. package/dist/types/tool/presentation.d.ts +8 -0
  203. package/dist/types/tool/presentation.d.ts.map +1 -1
  204. package/package.json +1 -1
  205. package/src/compaction/context-window.ts +163 -2
  206. package/src/execution/workflow/index.ts +14 -1
  207. package/src/execution/workflow/journal.ts +161 -11
  208. package/src/execution/workflow/prelude.ts +51 -8
  209. package/src/execution/workflow/resume.ts +313 -0
  210. package/src/execution/workflow/runtime.ts +127 -9
  211. package/src/execution/workflow/types.ts +109 -1
  212. package/src/index.ts +17 -0
  213. package/src/plugin/compat.ts +173 -0
  214. package/src/plugin/loader.ts +7 -1
  215. package/src/pricing/index.ts +2 -1
  216. package/src/process/kill-tree.ts +106 -4
  217. package/src/prompt/coding-agent-doctrine.ts +9 -1
  218. package/src/provider/model-window-suffix.ts +84 -0
  219. package/src/public-runtime.ts +21 -2
  220. package/src/public-types.ts +1 -0
  221. package/src/registry/command/kernel-commands.ts +1 -32
  222. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  223. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  224. package/src/runtime/jobs/liveness.ts +144 -0
  225. package/src/runtime/jobs/registry.ts +114 -2
  226. package/src/runtime/jobs/wake-budget.ts +55 -0
  227. package/src/runtime/query/detached-calls.ts +135 -0
  228. package/src/runtime/query/executor.ts +92 -10
  229. package/src/runtime/query/index.ts +50 -7
  230. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  231. package/src/runtime/query/iteration/phases/context.ts +7 -0
  232. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  233. package/src/runtime/query/session-storage.ts +8 -3
  234. package/src/runtime/query/steering.ts +77 -6
  235. package/src/runtime/query/tooling.ts +2 -0
  236. package/src/runtime/system-events.ts +12 -1
  237. package/src/sandbox/denial.ts +79 -0
  238. package/src/sandbox/file-walk-program.ts +32 -13
  239. package/src/sandbox/file-walk.ts +75 -12
  240. package/src/sandbox/ignore-matcher.ts +46 -0
  241. package/src/sandbox/ignore-source.ts +220 -0
  242. package/src/sandbox/provider/local.ts +11 -1
  243. package/src/scheduler/completion-inbox.ts +1 -0
  244. package/src/session/log-hash.ts +21 -0
  245. package/src/store/session-index/sqlite.ts +171 -16
  246. package/src/tools/builtins/bash.ts +11 -4
  247. package/src/tools/builtins/computer-use.ts +593 -68
  248. package/src/tools/builtins/glob.ts +79 -32
  249. package/src/tools/builtins/grep.ts +107 -43
  250. package/src/tools/builtins/job.ts +42 -12
  251. package/src/tools/builtins/search-support.ts +107 -0
  252. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  253. package/src/tools/builtins/wait-for-job.ts +14 -2
  254. package/src/tools/command-shell.ts +13 -5
  255. package/src/types/computer-use/index.ts +157 -15
  256. package/src/types/plugin/index.ts +8 -0
  257. package/src/types/sandbox/index.ts +40 -1
  258. package/src/types/session/records.ts +36 -2
  259. package/src/types/tool/index.ts +30 -0
  260. package/src/types/tool/presentation.ts +8 -0
@@ -5,6 +5,7 @@ import type { TaskHandle } from '../../../types/agent/scheduler.js'
5
5
  import { createRuntimeContextMessage } from '../../../types/message/index.js'
6
6
  import type { SessionEvent } from '../../../types/session/index.js'
7
7
  import { readPositiveIntEnv } from '../../../utils/env.js'
8
+ import { formatLateResults } from '../detached-calls.js'
8
9
  import { formatJobNote, formatMonitorNote } from '../steering.js'
9
10
  import type { IterationContext } from './phases/index.js'
10
11
 
@@ -165,7 +166,8 @@ export async function* holdForOutstandingWork(
165
166
  ): AsyncGenerator<SessionEvent, boolean> {
166
167
  const inbox = ctx.completionInbox?.holdsTurn ? ctx.completionInbox : undefined
167
168
  const jobs = ctx.awaitedJobs?.hasPendingWork ? ctx.awaitedJobs : undefined
168
- if (!inbox && !jobs) return false
169
+ const detached = ctx.detachedCalls?.hasWork ? ctx.detachedCalls : undefined
170
+ if (!inbox && !jobs && !detached) return false
169
171
 
170
172
  // Read HERE rather than from `forceFinalize`, which was sampled at the
171
173
  // top of the iteration: one that has since crossed the finalize point
@@ -179,7 +181,7 @@ export async function* holdForOutstandingWork(
179
181
  // to be running. A job therefore never shortens a wait, and it never
180
182
  // lengthens one either: where a task is outstanding too, that is how
181
183
  // long this turn was waiting anyway.
182
- let graceMs = inbox ? settleGraceMs(remainingMs) : awaitedJobGraceMs(remainingMs)
184
+ let graceMs = inbox || detached ? settleGraceMs(remainingMs) : awaitedJobGraceMs(remainingMs)
183
185
  // A monitor the turn owns is held for until the monitor itself ends, which
184
186
  // its own deadline bounds; the two-minute job ceiling would walk away from
185
187
  // a watch with most of its time left. As with a task, this only ever
@@ -207,6 +209,7 @@ export async function* holdForOutstandingWork(
207
209
  await Promise.race([
208
210
  ...(inbox ? [inbox.waitForArrival(graceMs, waiting.signal)] : []),
209
211
  ...(jobs ? [jobs.waitForArrival(graceMs, waiting.signal)] : []),
212
+ ...(detached ? [detached.waitForArrival(graceMs, waiting.signal)] : []),
210
213
  ...(ctx.waitForInbound ? [ctx.waitForInbound(waiting.signal)] : []),
211
214
  ...(ctx.durableInbound?.wait ? [ctx.durableInbound.wait(waiting.signal)] : []),
212
215
  ])
@@ -228,8 +231,14 @@ export async function* holdForOutstandingWork(
228
231
  }
229
232
  const exited = deliverAwaitedJobExits(ctx)
230
233
  const monitored = deliverMonitorNotices(ctx)
234
+ const late = ctx.detachedCalls?.take() ?? []
235
+ if (late.length > 0) {
236
+ ctx.recorder.pushMessage(createRuntimeContextMessage(formatLateResults(late), 'job-exit'))
237
+ }
231
238
  const inbound = await deliverInbound()
232
- if (arrived.length === 0 && !exited && !monitored && inbound === 0) return false
239
+ if (arrived.length === 0 && !exited && !monitored && late.length === 0 && inbound === 0) {
240
+ return false
241
+ }
233
242
  await ctx.emitEvent({
234
243
  type: 'iteration_completed',
235
244
  turnId: ctx.recorder.turnId,
@@ -40,6 +40,7 @@ import type { TaskStore } from '../../../../types/task/index.js'
40
40
  import type { Logger } from '../../../../utils/logger.js'
41
41
  import type { AwaitedJobs } from '../../../jobs/awaited-jobs.js'
42
42
  import type { CheckpointManager } from '../../checkpoint.js'
43
+ import type { DetachedCalls } from '../../detached-calls.js'
43
44
  import type { EmitEvent } from '../../events.js'
44
45
  import type { ToolExecutor } from '../../executor.js'
45
46
  import type { GuardCoordinator } from '../../guard.js'
@@ -171,6 +172,12 @@ export interface IterationContext {
171
172
  * and a turn whose model stopped calling tools settles without waiting.
172
173
  */
173
174
  readonly awaitedJobs?: AwaitedJobs
175
+ /**
176
+ * Calls answered with a placeholder because the operator wrote while they
177
+ * ran. Absent means no call is ever handed back and the operator's message
178
+ * waits for the batch, as it always has.
179
+ */
180
+ readonly detachedCalls?: DetachedCalls
174
181
  readonly checkpointMgr: CheckpointManager
175
182
  readonly planManager: PlanManager
176
183
 
@@ -7,10 +7,12 @@ import type {
7
7
  ToolCallDeclined,
8
8
  ToolCallSummary,
9
9
  } from '../../../../types/hitl/index.js'
10
+ import { createRuntimeContextMessage } from '../../../../types/message/index.js'
10
11
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
11
12
  import type { SessionEvent } from '../../../../types/session/index.js'
12
13
  import type { ShellDialect } from '../../../../types/tool/index.js'
13
14
  import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
15
+ import { type BatchDetach, DETACH_AFTER_MS, formatLateResults } from '../../detached-calls.js'
14
16
  import type {
15
17
  PreparedToolBatch,
16
18
  ReviewedExecution,
@@ -65,6 +67,60 @@ function hookAskedReason(reason: string | undefined): string {
65
67
  : 'a hook asked that a person approve this call'
66
68
  }
67
69
 
70
+ /**
71
+ * Watch for the operator writing while this batch runs.
72
+ *
73
+ * The batch is handed back once a message has arrived and the batch has run for
74
+ * {@link DETACH_AFTER_MS}; a short call finishes first and nothing changes. The
75
+ * arrival signal is the host's `waitForOperator`, which fires for operator text
76
+ * only, so a host that supplies none never detaches a call and a notice from a
77
+ * child or a peer never does.
78
+ */
79
+ function watchForOperator(
80
+ ctx: IterationContext,
81
+ startedAt: number,
82
+ ): { readonly detach: BatchDetach; stop(): void } | undefined {
83
+ const calls = ctx.detachedCalls
84
+ const wakes = calls?.waitForOperator ? [calls.waitForOperator] : []
85
+ if (!calls || wakes.length === 0) return undefined
86
+ const watching = new AbortController()
87
+ let timer: ReturnType<typeof setTimeout> | undefined
88
+ const requested = new Promise<void>((resolve) => {
89
+ const settled = Promise.race(
90
+ wakes.map((wake) => {
91
+ try {
92
+ return wake(watching.signal)
93
+ } catch {
94
+ return new Promise<void>(() => {})
95
+ }
96
+ }),
97
+ )
98
+ settled.then(
99
+ () => {
100
+ if (watching.signal.aborted) return
101
+ const wait = Math.max(0, startedAt + DETACH_AFTER_MS - Date.now())
102
+ timer = setTimeout(resolve, wait)
103
+ },
104
+ // An aborted wait is the batch ending, which is not a message.
105
+ () => {},
106
+ )
107
+ })
108
+ return {
109
+ detach: { requested, calls },
110
+ stop() {
111
+ watching.abort()
112
+ if (timer !== undefined) clearTimeout(timer)
113
+ },
114
+ }
115
+ }
116
+
117
+ /** The outcomes of calls that were handed back as still running and have since finished. */
118
+ function takeLateResults(ctx: IterationContext) {
119
+ const outcomes = ctx.detachedCalls?.take()
120
+ if (!outcomes || outcomes.length === 0) return undefined
121
+ return createRuntimeContextMessage(formatLateResults(outcomes), 'job-exit')
122
+ }
123
+
68
124
  export type ToolReviewDecision = 'executed' | 'rejected' | 'stop'
69
125
 
70
126
  /**
@@ -290,14 +346,21 @@ export async function* runToolReview(
290
346
  ): Promise<void> => {
291
347
  await announceRefusals(denials)
292
348
  const startedAt = Date.now()
293
- const batch = await ctx.toolExecutor.executeBatch(
294
- response,
295
- withRepeatRefusals(denials),
296
- undefined,
297
- preparedBatch,
298
- declines,
299
- review,
300
- )
349
+ const watch = watchForOperator(ctx, startedAt)
350
+ let batch: Awaited<ReturnType<typeof ctx.toolExecutor.executeBatch>>
351
+ try {
352
+ batch = await ctx.toolExecutor.executeBatch(
353
+ response,
354
+ withRepeatRefusals(denials),
355
+ undefined,
356
+ preparedBatch,
357
+ declines,
358
+ review,
359
+ watch?.detach,
360
+ )
361
+ } finally {
362
+ watch?.stop()
363
+ }
301
364
  toolMs += Date.now() - startedAt
302
365
  executed = batch.results
303
366
  await announceBatch(batch.results)
@@ -369,6 +432,8 @@ export async function* runToolReview(
369
432
  },
370
433
  )
371
434
  if (monitorNotice) ctx.recorder.pushMessage(monitorNotice)
435
+ const lateResults = takeLateResults(ctx)
436
+ if (lateResults) ctx.recorder.pushMessage(lateResults)
372
437
  const credentialNotice = takeWithheldEnvNotice(ctx.recorder.messages, batch.observations)
373
438
  if (credentialNotice) ctx.recorder.pushMessage(credentialNotice)
374
439
  if (jobNoticeDelivered) ctx.onJobNoticeDelivered?.()
@@ -273,9 +273,14 @@ export function checkpointLogView(log: SessionLog): CheckpointLogView {
273
273
  return {
274
274
  async verifyThrough(scope, throughSeq, throughSha256) {
275
275
  own(scope)
276
- const read = await log.readAll({ mode: 'tolerant', throughSeq })
277
- const entry = read.entries.at(-1)
278
- return entry?.pointer.seq === throughSeq && entry.pointer.sha256 === throughSha256
276
+ // Stream the walk and keep only the last pointer: the chain is
277
+ // verified as it is read, and a long session must not be held in
278
+ // memory just to look at its final entry.
279
+ let last: { seq: number; sha256: string } | undefined
280
+ for await (const entry of log.read({ mode: 'tolerant', throughSeq })) {
281
+ last = entry.pointer
282
+ }
283
+ return last?.seq === throughSeq && last.sha256 === throughSha256
279
284
  },
280
285
  async writtenDocSha256(scope, checkpointId) {
281
286
  own(scope)
@@ -64,17 +64,31 @@ export interface SteeringChannel {
64
64
  }
65
65
 
66
66
  export class SteeringBinding implements SteeringChannel {
67
- private queued: string[] = []
67
+ private queued: { readonly text: string; readonly key?: string }[] = []
68
68
 
69
- steer(text: string): void {
69
+ /**
70
+ * `key` names what the text is about (a job id), so the text can be
71
+ * taken back with {@link withdraw} while it is still undelivered.
72
+ */
73
+ steer(text: string, key?: string): void {
70
74
  const trimmed = text.trim()
71
75
  if (!trimmed) return
72
- this.queued.push(trimmed)
76
+ this.queued.push(key === undefined ? { text: trimmed } : { text: trimmed, key })
77
+ }
78
+
79
+ /**
80
+ * Take back every undelivered text queued under `key`. Returns whether any
81
+ * was queued. Text already drained has been delivered and is not touched.
82
+ */
83
+ withdraw(key: string): boolean {
84
+ const before = this.queued.length
85
+ this.queued = this.queued.filter((entry) => entry.key !== key)
86
+ return this.queued.length !== before
73
87
  }
74
88
 
75
89
  drain(): string | undefined {
76
90
  if (this.queued.length === 0) return undefined
77
- const joined = this.queued.join('\n')
91
+ const joined = this.queued.map((entry) => entry.text).join('\n')
78
92
  this.queued = []
79
93
  return joined
80
94
  }
@@ -84,6 +98,60 @@ export class SteeringBinding implements SteeringChannel {
84
98
  }
85
99
  }
86
100
 
101
+ /**
102
+ * The most output a job-exit notice carries inline. A job whose whole output
103
+ * fits is told with its result; a larger one is read with the job tool, so a
104
+ * noisy build cannot turn every exit into a large message.
105
+ */
106
+ export const JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES = 2048
107
+
108
+ /**
109
+ * One job's exit as the text the model reads. The output is inlined only when
110
+ * the whole of it fits {@link JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES} and none was
111
+ * dropped by the retention cap; it is untrusted process output, so it rides in
112
+ * the system-event envelope rather than in the notice's own words.
113
+ */
114
+ export function formatJobExitNotice(
115
+ job: Pick<BackgroundJob, 'id' | 'command' | 'status' | 'exitCode' | 'signal'> &
116
+ Partial<Pick<BackgroundJob, 'exitedAt'>>,
117
+ output?: {
118
+ readonly chunk: string
119
+ readonly nextOffset: number
120
+ readonly droppedBytes: number
121
+ },
122
+ ): string {
123
+ const outcome =
124
+ job.status === 'killed'
125
+ ? 'was stopped'
126
+ : `exited with code ${job.exitCode ?? 'unknown'}${job.signal ? ` (${job.signal})` : ''}`
127
+ const endedAt = job.exitedAt === undefined ? '' : ` at ${new Date(job.exitedAt).toISOString()}`
128
+ const head = `Background job ${job.id} (${job.command}) ${outcome}${endedAt}.`
129
+ if (
130
+ output === undefined ||
131
+ output.chunk.length === 0 ||
132
+ output.droppedBytes > 0 ||
133
+ output.nextOffset > JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES
134
+ ) {
135
+ return `${head} Read its output with the job tool if you need it.`
136
+ }
137
+ return `${head}\n${formatSystemEvent({
138
+ kind: 'job',
139
+ id: job.id,
140
+ status: job.status === 'killed' ? 'cancelled' : job.exitCode === 0 ? 'completed' : 'failed',
141
+ summary: `Output of ${job.id}`,
142
+ source: `job ${job.id}: ${oneMetadataLine(job.command)}`,
143
+ body: {
144
+ envelope: {
145
+ kind: 'job-output',
146
+ attributes: { job: job.id },
147
+ provenance:
148
+ 'Everything the job printed. The command, or whatever it reads, wrote it; it is not the operator.',
149
+ },
150
+ content: output.chunk,
151
+ },
152
+ })}`
153
+ }
154
+
87
155
  /** What the model reads when a background job it started has ended. */
88
156
  export function formatJobNote(text: string): string {
89
157
  return frameNotice('job', '[Background job update]', text)
@@ -116,7 +184,7 @@ const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/s
116
184
  */
117
185
  export function formatMonitorEvent(
118
186
  event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'> &
119
- Partial<Pick<MonitorEvent, 'transport'>>,
187
+ Partial<Pick<MonitorEvent, 'transport' | 'at'>>,
120
188
  command: string,
121
189
  ): string {
122
190
  return formatSystemEvent({
@@ -126,6 +194,7 @@ export function formatMonitorEvent(
126
194
  summary: `Monitor event: "${oneMetadataLine(event.description)}"`,
127
195
  source: `monitor ${event.jobId}: ${oneMetadataLine(command)}`,
128
196
  more: `job {"action":"kill","id":"${event.jobId}"} stops it`,
197
+ ...(event.at !== undefined ? { at: event.at } : {}),
129
198
  body: {
130
199
  envelope: {
131
200
  kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
@@ -170,7 +239,8 @@ export function formatMonitorEnd(
170
239
  | 'transport'
171
240
  | 'closeCode'
172
241
  | 'closeReason'
173
- >,
242
+ > &
243
+ Partial<Pick<BackgroundJob, 'exitedAt'>>,
174
244
  stats: { readonly stdoutChars: number },
175
245
  ): string {
176
246
  const end = describeMonitorEnd(job, stats)
@@ -181,6 +251,7 @@ export function formatMonitorEnd(
181
251
  summary: end.summary,
182
252
  source: `monitor ${job.id}: ${oneMetadataLine(job.command)}`,
183
253
  more: `job {"action":"read","id":"${job.id}"} reads its output`,
254
+ ...(job.exitedAt !== undefined ? { at: job.exitedAt } : {}),
184
255
  })
185
256
  }
186
257
 
@@ -46,6 +46,7 @@ export interface ToolingBootstrapConfig {
46
46
  backgroundJobOwner?: string
47
47
  /** Where `wait_for_job` records wait-intent; see the executor's own field. */
48
48
  onJobAwaited?: (id: string) => void
49
+ onJobCollected?: (id: string) => void
49
50
  /** See `ToolExecutorConfig.monitorTimeoutCapMs`. */
50
51
  monitorTimeoutCapMs?: number
51
52
  /** See `ToolExecutorConfig.onMonitorStarted`. */
@@ -110,6 +111,7 @@ export class ToolingBootstrap {
110
111
  ...(config.backgroundJobs ? { backgroundJobs: config.backgroundJobs } : {}),
111
112
  ...(config.backgroundJobOwner ? { backgroundJobOwner: config.backgroundJobOwner } : {}),
112
113
  ...(config.onJobAwaited ? { onJobAwaited: config.onJobAwaited } : {}),
114
+ ...(config.onJobCollected ? { onJobCollected: config.onJobCollected } : {}),
113
115
  ...(config.monitorTimeoutCapMs !== undefined
114
116
  ? { monitorTimeoutCapMs: config.monitorTimeoutCapMs }
115
117
  : {}),
@@ -102,6 +102,11 @@ export interface SystemEvent {
102
102
  readonly more?: string
103
103
  /** Omit for an event with nothing to show; renders as `(no output)`. */
104
104
  readonly body?: SystemEventBody
105
+ /**
106
+ * When it happened (epoch ms). Rendered as an `at` attribute in ISO form so
107
+ * the model can tell a fresh event from one that waited in a queue.
108
+ */
109
+ readonly at?: number
105
110
  }
106
111
 
107
112
  export interface FormatSystemEventOptions {
@@ -209,6 +214,12 @@ function renderBody(body: SystemEventBody | undefined, options: FormatSystemEven
209
214
  )
210
215
  }
211
216
 
217
+ function atAttribute(at: number | undefined): string {
218
+ if (at === undefined || !Number.isFinite(at)) return ''
219
+ const date = new Date(at)
220
+ return Number.isNaN(date.getTime()) ? '' : ` at="${date.toISOString()}"`
221
+ }
222
+
212
223
  /**
213
224
  * Render one asynchronous event as provider-required user-role context.
214
225
  *
@@ -248,7 +259,7 @@ export function formatSystemEvent(
248
259
  ])
249
260
  const closingTag = `</system-event-${nonce}>`
250
261
  const lines = [
251
- `<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}">`,
262
+ `<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}"${atAttribute(event.at)}>`,
252
263
  SYSTEM_EVENT_HEADER,
253
264
  nonceClosureStatement(closingTag),
254
265
  `summary: ${summary}`,
@@ -0,0 +1,79 @@
1
+ import type { SandboxEnvironment, SandboxExecAttribution } from '../types/sandbox/index.js'
2
+
3
+ /**
4
+ * Reading a sandboxed command's failure: did the command fail, did the
5
+ * sandbox refuse it, or did the launcher never get as far as running it?
6
+ *
7
+ * The model cannot tell these apart from an exit code. A denied write looks
8
+ * like any other failure, so it retries the same command and burns turns
9
+ * on something no retry can fix; a launcher that could not set up its
10
+ * namespaces looks like the command crashing. Both call for a different
11
+ * answer (ask for the escape, or report a runner fault).
12
+ *
13
+ * This is inference from stderr text, not a verdict from the kernel, and
14
+ * the wording says so. It only ever adds a note; the result is unchanged.
15
+ */
16
+
17
+ /** Launcher setup failures print this prefix before any user command runs. */
18
+ const RUNNER_PREFIX: Partial<Record<SandboxEnvironment, RegExp>> = {
19
+ 'linux-bwrap': /^bwrap: (?!execvp )/m,
20
+ 'linux-namespace': /^unshare: /m,
21
+ 'macos-seatbelt': /^sandbox-exec: /m,
22
+ }
23
+
24
+ const EROFS = /Read-only file system|\bEROFS\b/i
25
+ const PERMISSION = /Permission denied|Operation not permitted|\bEACCES\b|\bEPERM\b/i
26
+ const ABSOLUTE_PATH = /(?:^|[\s'"`:(])(\/[^\s'"`:)]+)/g
27
+
28
+ /** Only tiers that confine file writes can produce a sandbox denial. */
29
+ const CONFINES_FILES: ReadonlySet<SandboxEnvironment> = new Set(['linux-bwrap', 'macos-seatbelt'])
30
+
31
+ function within(path: string, root: string): boolean {
32
+ return path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`)
33
+ }
34
+
35
+ function pathsIn(line: string): string[] {
36
+ return [...line.matchAll(ABSOLUTE_PATH)].map((m) => m[1] as string)
37
+ }
38
+
39
+ export interface AttributionInput {
40
+ readonly environment: SandboxEnvironment
41
+ readonly exitCode: number
42
+ readonly stderr: string
43
+ readonly timedOut: boolean
44
+ /** Paths the sandbox lets the command write: root, added directories, scratch. */
45
+ readonly allowedRoots: readonly string[]
46
+ }
47
+
48
+ export function attributeSandboxFailure(
49
+ input: AttributionInput,
50
+ ): SandboxExecAttribution | undefined {
51
+ if (input.exitCode === 0 || input.timedOut || input.stderr === '') return undefined
52
+
53
+ const runner = RUNNER_PREFIX[input.environment]
54
+ if (runner?.test(input.stderr)) {
55
+ const line = input.stderr.split('\n').find((l) => runner.test(l)) ?? ''
56
+ return {
57
+ kind: 'runner-failure',
58
+ message: `The sandbox launcher failed before the command could run (${line.trim()}). This is a runner fault, not a failure of the command; retrying the same command will not help.`,
59
+ }
60
+ }
61
+
62
+ if (!CONFINES_FILES.has(input.environment)) return undefined
63
+
64
+ for (const line of input.stderr.split('\n')) {
65
+ if (EROFS.test(line)) return denial()
66
+ if (!PERMISSION.test(line)) continue
67
+ const paths = pathsIn(line)
68
+ if (paths.some((p) => !input.allowedRoots.some((root) => within(p, root)))) return denial()
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ function denial(): SandboxExecAttribution {
74
+ return {
75
+ kind: 'denial',
76
+ message:
77
+ 'This looks like a sandbox denial: the command touched a path the sandbox does not allow it to write. This is inferred from the error text, not proven. Retrying the same command will fail the same way; if the action is needed, ask to run it outside the sandbox instead.',
78
+ }
79
+ }
@@ -1,5 +1,7 @@
1
+ import { IGNORE_SOURCE } from './ignore-source.js'
2
+
1
3
  /** Stable guest code: host bundlers must never rewrite a function we serialize. */
2
- export const FILE_WALK_PROGRAM = String.raw`
4
+ export const FILE_WALK_PROGRAM = `${IGNORE_SOURCE}${String.raw`
3
5
  const fs = require('node:fs/promises');
4
6
  const path = require('node:path');
5
7
  const plan = JSON.parse(process.argv[1]);
@@ -9,10 +11,17 @@ const write = async value => {
9
11
  await new Promise(resolve => process.stdout.once('drain', resolve));
10
12
  }
11
13
  };
14
+ let stopped;
15
+ let visitedTotal = 0;
16
+ let ignoredDirs = 0;
12
17
  const visit = count => {
13
- if (count > plan.maxVisitedEntries) {
18
+ visitedTotal = count;
19
+ if (count <= plan.maxVisitedEntries) return true;
20
+ if (plan.failOnLimit) {
14
21
  throw Object.assign(new Error('File search stopped after examining ' + plan.maxVisitedEntries + ' entries; narrow its root or pattern.'), {code:'ERR_FILE_WALK_LIMIT'});
15
22
  }
23
+ stopped = 'entry-limit';
24
+ return false;
16
25
  };
17
26
  const patterns = plan.patterns.map(set => set.map(part =>
18
27
  typeof part === 'object' && part !== null ? new RegExp(part.source, part.flags + 's') : part));
@@ -41,9 +50,13 @@ async function run() {
41
50
  let emitted = 0;
42
51
  const matchers = plan.expressions.map(({source, flags}) => new RegExp(source, flags + 's'));
43
52
  const maxDepth = plan.maxDepth === null ? Infinity : plan.maxDepth;
44
- const open = async (absolute, relative, depth) => {
45
- try { stack.push({dir: await fs.opendir(absolute), relative, depth}); }
46
- catch (error) { if (!missing(error)) throw error; }
53
+ const ignore = plan.respectIgnore ? ignCreate({fs, path, extraFiles: []}) : null;
54
+ const open = async (absolute, relative, depth, layered) => {
55
+ try {
56
+ const dir = await fs.opendir(absolute);
57
+ if (layered && ignore) await ignore.enter(absolute);
58
+ stack.push({dir, relative, depth, layered});
59
+ } catch (error) { if (!missing(error)) throw error; }
47
60
  };
48
61
  try {
49
62
  if (plan.patterns.length === 0) return;
@@ -53,36 +66,42 @@ async function run() {
53
66
  if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
54
67
  const rootInfo = await fs.lstat(plan.root);
55
68
  if (rootInfo.isFile()) {
56
- visit(++visited);
69
+ if (!visit(++visited)) return;
57
70
  if (matchers.some(matcher => matcher.test(path.basename(plan.root)))) {
58
71
  await write({type:'entry', path:plan.root, size:rootInfo.size});
59
72
  }
60
73
  return;
61
74
  }
75
+ if (ignore) { await ignore.init(plan.root); await ignore.enter(plan.root); }
62
76
  let start = plan.root;
63
77
  for (const part of plan.prefix) {
64
78
  start = path.join(start, part);
65
79
  let info;
66
80
  try { info = await fs.lstat(start); }
67
81
  catch (error) { if (missing(error)) return; throw error; }
68
- visit(++visited);
82
+ if (!visit(++visited)) return;
69
83
  if (!info.isDirectory()) return;
84
+ if (ignore) await ignore.enter(start);
70
85
  }
71
86
  if (plan.prefix.length >= maxDepth) return;
72
- await open(start, plan.prefix.join('/'), plan.prefix.length);
87
+ await open(start, plan.prefix.join('/'), plan.prefix.length, false);
73
88
  while (stack.length) {
74
89
  const frame = stack[stack.length - 1];
75
90
  const entry = await frame.dir.read();
76
- if (!entry) { stack.pop(); await frame.dir.close(); continue; }
77
- visit(++visited);
91
+ if (!entry) { stack.pop(); if (frame.layered && ignore) ignore.leave(); await frame.dir.close(); continue; }
92
+ if (!visit(++visited)) return;
93
+ if (IGN_VCS.includes(entry.name)) continue;
78
94
  const relative = frame.relative ? frame.relative + '/' + entry.name : entry.name;
79
95
  const absolute = path.join(plan.root, relative);
80
96
  const depth = frame.depth + 1;
81
97
  if (entry.isDirectory()) {
82
- if (depth < maxDepth && canDescend(relative)) await open(absolute, relative, depth);
98
+ if (depth >= maxDepth || !canDescend(relative)) continue;
99
+ if (ignore && ignore.isIgnored(absolute, true)) { ignoredDirs++; continue; }
100
+ await open(absolute, relative, depth, true);
83
101
  continue;
84
102
  }
85
103
  if (!entry.isFile() || !matchers.some(matcher => matcher.test(relative))) continue;
104
+ if (ignore && ignore.isIgnored(absolute, false)) continue;
86
105
  let info;
87
106
  try { info = await fs.lstat(absolute); }
88
107
  catch (error) { if (missing(error)) continue; throw error; }
@@ -95,7 +114,7 @@ async function run() {
95
114
  }
96
115
  }
97
116
  run().then(
98
- () => write({type:'done'}),
117
+ () => write({type:'done', visited:visitedTotal, ignoredDirs, stopped}),
99
118
  async error => { await write({type:'error', message:error.message, code:error.code}); process.exitCode = 1; },
100
119
  );
101
- `
120
+ `}`