@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
@@ -1,3 +1,4 @@
1
+ import { existsSync, statSync } from 'node:fs'
1
2
  import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises'
2
3
  import { createRequire } from 'node:module'
3
4
  import { basename, dirname, join } from 'node:path'
@@ -305,32 +306,135 @@ async function sweepAbandonedRebuilds(path: string): Promise<void> {
305
306
  }
306
307
  }
307
308
 
309
+ /** `device:inode` of the file at `path`, or undefined when there is none. */
310
+ function fileIdentity(path: string): string | undefined {
311
+ try {
312
+ const { dev, ino } = statSync(path)
313
+ return `${dev}:${ino}`
314
+ } catch {
315
+ return undefined
316
+ }
317
+ }
318
+
308
319
  function userVersion(db: DatabaseSync): number {
309
320
  return Number(db.prepare('PRAGMA user_version').get()?.user_version)
310
321
  }
311
322
 
323
+ /** SQLite primary result codes for "the image is malformed" and "the file is not a database". */
324
+ const SQLITE_CORRUPT = 11
325
+ const SQLITE_NOTADB = 26
326
+
327
+ /** Whether `error` says the file's content is not a usable SQLite database. */
328
+ function isNotADatabase(error: unknown): boolean {
329
+ const code = (error as { errcode?: unknown } | null)?.errcode
330
+ return typeof code === 'number' && [SQLITE_CORRUPT, SQLITE_NOTADB].includes(code & 0xff)
331
+ }
332
+
333
+ const SQLITE_BUSY = 5
334
+ const SQLITE_READONLY = 8
335
+ /** `SQLITE_READONLY_DBMOVED`: the file this connection opened was renamed or unlinked. */
336
+ const SQLITE_READONLY_DBMOVED = 1032
337
+
338
+ function errcode(error: unknown): number | undefined {
339
+ const code = (error as { errcode?: unknown } | null)?.errcode
340
+ return typeof code === 'number' ? code : undefined
341
+ }
342
+
343
+ /** Whether `error` says another process holds the index locked past the busy timeout. */
344
+ function isBusy(error: unknown): boolean {
345
+ const code = errcode(error)
346
+ return code !== undefined && (code & 0xff) === SQLITE_BUSY
347
+ }
348
+
349
+ /** Whether `error` says the file under this connection was replaced by another process. */
350
+ function isMoved(error: unknown): boolean {
351
+ const code = errcode(error)
352
+ return code === SQLITE_READONLY_DBMOVED && (code & 0xff) === SQLITE_READONLY
353
+ }
354
+
355
+ /** A plain-language error for an index another process keeps locked. */
356
+ function busyError(path: string, cause: unknown): SessionIndexError {
357
+ return new SessionIndexError(
358
+ `The session index at ${path} is in use by another process. Try again in a moment.`,
359
+ { cause },
360
+ )
361
+ }
362
+
312
363
  /**
313
- * The version of the index file at `path`: -1 when there is none, 0 when it
314
- * cannot be read as a SQLite database, otherwise its `user_version`.
364
+ * The version of the index file at `path`: -1 when there is none, 0 when its
365
+ * content is not a SQLite database, otherwise its `user_version`.
366
+ *
367
+ * The file is opened for writing, never read-only. A writer that died inside
368
+ * a transaction leaves a hot journal beside the file, and only a connection
369
+ * that may write can roll it back; a read-only one fails on it, which used to
370
+ * read as "not a database" and sent the caller to replace a healthy index,
371
+ * leaving the dead writer's journal beside the new file for the next
372
+ * connection to play back into it. Only a SQLite "corrupt" or "not a
373
+ * database" answer means 0; being busy, read-only or unable to read is thrown,
374
+ * never a reason to rebuild.
315
375
  */
316
376
  function indexVersion(path: string): number {
377
+ if (!existsSync(path)) return -1
317
378
  const { DatabaseSync } = requireSqlite()
318
- let db: DatabaseSync
319
- try {
320
- db = new DatabaseSync(path, { readOnly: true })
321
- } catch {
322
- return -1
323
- }
379
+ const db = new DatabaseSync(path)
324
380
  try {
325
381
  db.exec('PRAGMA busy_timeout = 10000')
326
382
  return userVersion(db)
327
- } catch {
328
- return 0
383
+ } catch (error) {
384
+ if (isNotADatabase(error)) return 0
385
+ throw isBusy(error) ? busyError(path, error) : error
329
386
  } finally {
330
387
  db.close()
331
388
  }
332
389
  }
333
390
 
391
+ /**
392
+ * Put the finished `temporary` file in place of the index at `path`.
393
+ *
394
+ * Takes the old index's exclusive lock first. That waits out a peer that is
395
+ * mid-write and rolls back a hot journal left by one that died, so no journal
396
+ * for the old file survives to be played into the new one. A leftover journal
397
+ * of a file that is not a database is removed too: it can describe nothing
398
+ * but that file.
399
+ */
400
+ async function replaceIndexFile(temporary: string, path: string, force: boolean): Promise<boolean> {
401
+ let old: DatabaseSync | undefined
402
+ if (existsSync(path)) {
403
+ const { DatabaseSync } = requireSqlite()
404
+ old = new DatabaseSync(path)
405
+ try {
406
+ old.exec('PRAGMA busy_timeout = 10000')
407
+ old.exec('BEGIN EXCLUSIVE')
408
+ // A peer may have finished its own rebuild while this one ran. With the
409
+ // lock held, nobody can change the file between this check and the
410
+ // rename, so a current index is left alone rather than replaced under
411
+ // the connections that already adopted it.
412
+ if (!force && userVersion(old) === SESSION_INDEX_VERSION) {
413
+ old.close()
414
+ old = undefined
415
+ return false
416
+ }
417
+ } catch (error) {
418
+ old?.close()
419
+ old = undefined
420
+ if (!isNotADatabase(error)) throw isBusy(error) ? busyError(path, error) : error
421
+ }
422
+ }
423
+ try {
424
+ // Windows will not rename over a file that is open, so the lock is let go
425
+ // first there; elsewhere it is held until the new file is in place.
426
+ if (process.platform === 'win32') {
427
+ old?.close()
428
+ old = undefined
429
+ }
430
+ await rm(`${path}-journal`, { force: true })
431
+ await rename(temporary, path)
432
+ return true
433
+ } finally {
434
+ old?.close()
435
+ }
436
+ }
437
+
334
438
  export interface SqliteSessionIndexOptions {
335
439
  /** The index file, normally `$NAMZU_HOME/index.sqlite`. */
336
440
  readonly path: string
@@ -371,15 +475,21 @@ export function rebuildSqliteSessionIndex(
371
475
  */
372
476
  export class SqliteSessionIndex extends SessionIndexBase {
373
477
  readonly backend = 'sqlite' as const
374
- readonly #db: DatabaseSync
478
+ #db: DatabaseSync
479
+ /** Where `#db` was opened, or undefined for a temporary build file that is never watched. */
480
+ readonly #path: string | undefined
481
+ /** Identity of the file `#db` has open, to notice another process replacing it. */
482
+ #identity: string | undefined
375
483
  readonly #sink: IndexSink
376
484
  readonly #statements = new Map<string, StatementSync>()
377
485
  #open = true
378
486
 
379
487
  /** Wraps an open database whose schema is current. Use {@link SqliteSessionIndex.open}. */
380
- private constructor(db: DatabaseSync) {
488
+ private constructor(db: DatabaseSync, path?: string) {
381
489
  super()
382
490
  this.#db = db
491
+ this.#path = path
492
+ this.#identity = path === undefined ? undefined : fileIdentity(path)
383
493
  const run = (sql: string, ...values: SQLInputValue[]) => {
384
494
  this.#statement(sql).run(...values)
385
495
  }
@@ -583,8 +693,7 @@ export class SqliteSessionIndex extends SessionIndexBase {
583
693
  if (!options.force && (await pathExists(options.path))) {
584
694
  if (indexVersion(options.path) === SESSION_INDEX_VERSION) return false
585
695
  }
586
- await rename(temporary, options.path)
587
- return true
696
+ return await replaceIndexFile(temporary, options.path, options.force === true)
588
697
  } finally {
589
698
  await rm(temporary, { force: true })
590
699
  await rm(`${temporary}-journal`, { force: true })
@@ -615,13 +724,55 @@ export class SqliteSessionIndex extends SessionIndexBase {
615
724
  db.close()
616
725
  throw error
617
726
  }
618
- const index = new SqliteSessionIndex(db)
727
+ const index = new SqliteSessionIndex(db, options.path)
619
728
  await index.sync(options.home)
620
729
  return index
621
730
  }
622
731
 
732
+ /**
733
+ * Another process rebuilt the index and renamed a new file over it: this
734
+ * connection still has the old, unlinked file, whose reads go stale and
735
+ * whose writes fail with SQLITE_READONLY_DBMOVED. Reopen the file now at
736
+ * the path. A no-op for a temporary build file, and when the path holds
737
+ * nothing to adopt.
738
+ */
739
+ #reopenIfReplaced(force = false): void {
740
+ const path = this.#path
741
+ if (path === undefined || !this.#open) return
742
+ const identity = fileIdentity(path)
743
+ if (identity === undefined || (!force && identity === this.#identity)) return
744
+ const { DatabaseSync } = requireSqlite()
745
+ const db = new DatabaseSync(path)
746
+ try {
747
+ configure(db)
748
+ if (userVersion(db) !== SESSION_INDEX_VERSION) {
749
+ throw new SessionIndexError(`The index at ${path} changed version while in use.`)
750
+ }
751
+ } catch (error) {
752
+ db.close()
753
+ throw error
754
+ }
755
+ const previous = this.#db
756
+ this.#statements.clear()
757
+ this.#db = db
758
+ this.#identity = fileIdentity(path)
759
+ try {
760
+ previous.close()
761
+ } catch {
762
+ // The old file is already unlinked; nothing is lost if it will not close.
763
+ }
764
+ }
765
+
623
766
  protected transaction<T>(work: (sink: IndexSink) => T): T {
624
- this.#db.exec('BEGIN IMMEDIATE')
767
+ this.#reopenIfReplaced()
768
+ try {
769
+ this.#db.exec('BEGIN IMMEDIATE')
770
+ } catch (error) {
771
+ // Nothing has run yet, so the work can start again on the new file.
772
+ if (!isMoved(error)) throw error
773
+ this.#reopenIfReplaced(true)
774
+ this.#db.exec('BEGIN IMMEDIATE')
775
+ }
625
776
  try {
626
777
  const result = work(this.#sink)
627
778
  this.#db.exec('COMMIT')
@@ -643,10 +794,12 @@ export class SqliteSessionIndex extends SessionIndexBase {
643
794
  }
644
795
 
645
796
  #all(sql: string, ...values: SQLInputValue[]): Row[] {
797
+ this.#reopenIfReplaced()
646
798
  return this.#statement(sql).all(...values) as Row[]
647
799
  }
648
800
 
649
801
  protected readSession(sessionId: SessionId): SessionRow | undefined {
802
+ this.#reopenIfReplaced()
650
803
  const row = this.#statement('SELECT * FROM sessions WHERE id = ?').get(sessionId) as
651
804
  | Row
652
805
  | undefined
@@ -707,6 +860,7 @@ export class SqliteSessionIndex extends SessionIndexBase {
707
860
  kind: ExternalRefKind,
708
861
  externalId: string,
709
862
  ): Promise<ExternalRefTarget | undefined> {
863
+ this.#reopenIfReplaced()
710
864
  const row = this.#db
711
865
  .prepare(
712
866
  `SELECT * FROM external_refs WHERE protocol = ? AND kind = ? AND external_id = ?
@@ -749,6 +903,7 @@ export class SqliteSessionIndex extends SessionIndexBase {
749
903
  values.push(options.sessionId)
750
904
  }
751
905
  const clause = where.length === 0 ? '' : `WHERE ${where.join(' AND ')}`
906
+ this.#reopenIfReplaced()
752
907
  const statement = this.#db.prepare(
753
908
  `SELECT text, session_id, turn_id, seq, part, source, tool_name, is_error FROM evidence_fts
754
909
  ${clause} ORDER BY session_id, seq, part`,
@@ -751,12 +751,14 @@ const BashToolBase = defineTool({
751
751
  clipped.length > 0
752
752
  ? `[${clipped.join(' and ')} was truncated by the sandbox output cap. The omitted output is unavailable in this result. Use a saved artifact or a read-only observation; do not repeat a state-changing action to recover its output.]`
753
753
  : '',
754
+ result.attribution ? `[${result.attribution.message}]` : '',
754
755
  ]
755
756
  .filter(Boolean)
756
757
  .join('\n\n')
757
758
 
758
759
  const judged = result.timedOut ? undefined : judgeExit(input.command, result.exitCode)
759
- const answered = judged !== undefined && !judged.isError
760
+ const answered =
761
+ judged !== undefined && !judged.isError && result.attribution?.kind !== 'runner-failure'
760
762
  return {
761
763
  success: answered,
762
764
  output: output || '(no output)',
@@ -767,12 +769,17 @@ const BashToolBase = defineTool({
767
769
  timedOut: result.timedOut,
768
770
  stdoutTruncated: result.stdoutTruncated ?? false,
769
771
  stderrTruncated: result.stderrTruncated ?? false,
772
+ ...(result.attribution ? { sandboxAttribution: result.attribution.kind } : {}),
770
773
  },
771
774
  error: result.timedOut
772
775
  ? `Command timed out after ${input.timeout}ms. Any captured output before the deadline is above.`
773
- : !answered
774
- ? `Command exited with code ${result.exitCode}`
775
- : undefined,
776
+ : result.attribution?.kind === 'runner-failure'
777
+ ? 'The sandbox launcher failed before the command ran (runner failure, not a command failure).'
778
+ : !answered
779
+ ? result.attribution?.kind === 'denial'
780
+ ? `Command exited with code ${result.exitCode}; looks like a sandbox denial`
781
+ : `Command exited with code ${result.exitCode}`
782
+ : undefined,
776
783
  facts: commandFacts({
777
784
  command: input.command,
778
785
  exitCode: result.exitCode,