@desplega.ai/agent-swarm 1.157.0 → 1.158.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 (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { scrubSecrets } from "../utils/secret-scrubber";
3
4
 
4
5
  /**
5
6
  * Async database seam.
@@ -153,10 +154,10 @@ export function isInTransaction(): boolean {
153
154
  *
154
155
  * Two properties keep a retrying operation from starving the rest of the
155
156
  * process, both measured (see PR #1229's read-heavy probe):
156
- * - each retryable driver attempt runs under a SHORT `busy_timeout`
157
- * (`attemptSpinMs`), so the driver's synchronous busy wait blocks the
158
- * event loop for at most that long per attempt; the real waiting happens
159
- * in the async backoff sleeps,
157
+ * - every driver attempt runs under a SHORT `busy_timeout`
158
+ * (`attemptSpinMs`, or `finalAttemptSpinMs` for the last one), so the
159
+ * driver's synchronous busy wait blocks the event loop for at most that
160
+ * long per attempt; the real waiting happens in the async backoff sleeps,
160
161
  * - the FIFO lock is RELEASED during those sleeps (except after COMMIT
161
162
  * failures, where the open transaction pins the connection), so queued
162
163
  * reads flow while a writer waits out an external lock holder.
@@ -168,17 +169,66 @@ export type BusyRetryOptions = {
168
169
  backoffMs: number[];
169
170
  /** `busy_timeout` applied for the duration of each retryable driver attempt. */
170
171
  attemptSpinMs: number;
172
+ /**
173
+ * `busy_timeout` for the final attempt, after the backoff budget is spent.
174
+ * Defaults to `DEFAULT_FINAL_ATTEMPT_SPIN_MS`.
175
+ */
176
+ finalAttemptSpinMs?: number;
171
177
  };
172
178
 
173
- // The backoff sum (~6.9s) plus per-attempt spins must outlast a realistic
179
+ /**
180
+ * The final attempt used to camp on the connection's ambient busy_timeout
181
+ * (5000ms), which froze every in-flight request for up to 5s whenever an
182
+ * external holder outlived the backoff budget. A slightly longer spin than
183
+ * `attemptSpinMs` still widens the last window without that freeze.
184
+ */
185
+ const DEFAULT_FINAL_ATTEMPT_SPIN_MS = 250;
186
+
187
+ // The backoff sum (~10.9s) plus per-attempt spins must outlast a realistic
174
188
  // external hold (litestream checkpoint stalls measured in seconds) so writes
175
189
  // land in the holder's release gaps instead of failing at the driver cliff.
190
+ // It covers the patience the old 5s synchronous final camp provided, spent
191
+ // in async sleeps instead. The 500ms tail keeps attempts at most one spin
192
+ // apart from any 500ms release gap, so a lone writer cannot phase-lock with a
193
+ // periodic holder (measured: 6 of 6 writes under a 6s-hold/500ms-gap locker;
194
+ // 1s spacing lost 3 of 6).
176
195
  const DEFAULT_BUSY_RETRY: BusyRetryOptions = {
177
- maxWaitMs: 10_000,
178
- backoffMs: [25, 50, 100, 250, 500, 1000, 1000, 1000, 1000, 1000, 1000],
196
+ maxWaitMs: 11_000,
197
+ backoffMs: [25, 50, 100, 250, ...Array<number>(21).fill(500)],
179
198
  attemptSpinMs: 100,
199
+ finalAttemptSpinMs: DEFAULT_FINAL_ATTEMPT_SPIN_MS,
180
200
  };
181
201
 
202
+ /**
203
+ * A single synchronous driver call longer than this froze the whole event
204
+ * loop (every route, not only its own request) and is logged.
205
+ */
206
+ const SLOW_STATEMENT_WARN_MS = 100;
207
+ const SLOW_STATEMENT_SQL_MAX_CHARS = 200;
208
+
209
+ /** Params are never logged: they can carry secrets and user content. */
210
+ function warnIfSlowStatement(sql: string, durationMs: number): void {
211
+ if (durationMs < SLOW_STATEMENT_WARN_MS) return;
212
+ const flat = sql.replace(/\s+/g, " ").trim();
213
+ const shown =
214
+ flat.length > SLOW_STATEMENT_SQL_MAX_CHARS
215
+ ? `${flat.slice(0, SLOW_STATEMENT_SQL_MAX_CHARS)}…`
216
+ : flat;
217
+ console.warn(
218
+ `[db-client] slow sync statement blocked the event loop for ${Math.round(durationMs)}ms: ${scrubSecrets(shown)}`,
219
+ );
220
+ }
221
+
222
+ /** Run one synchronous driver call and log it when it blocked the loop too long. */
223
+ function timedStatement<T>(sql: string, run: () => T): T {
224
+ const startedAt = performance.now();
225
+ try {
226
+ return run();
227
+ } finally {
228
+ warnIfSlowStatement(sql, performance.now() - startedAt);
229
+ }
230
+ }
231
+
182
232
  function sleep(ms: number): Promise<void> {
183
233
  return new Promise((resolve) => setTimeout(resolve, ms));
184
234
  }
@@ -206,15 +256,15 @@ class BunSqliteClient implements DbClient {
206
256
  ) {}
207
257
 
208
258
  query<T>(sql: string, params: DbParam[] = []): Promise<T[]> {
209
- return this.execute((db) => db.query<T, DbParam[]>(sql).all(...params));
259
+ return this.execute(sql, (db) => db.query<T, DbParam[]>(sql).all(...params));
210
260
  }
211
261
 
212
262
  get<T>(sql: string, params: DbParam[] = []): Promise<T | null> {
213
- return this.execute((db) => db.query<T, DbParam[]>(sql).get(...params));
263
+ return this.execute(sql, (db) => db.query<T, DbParam[]>(sql).get(...params));
214
264
  }
215
265
 
216
266
  run(sql: string, params: DbParam[] = []): Promise<{ changes: number }> {
217
- return this.execute((db) => {
267
+ return this.execute(sql, (db) => {
218
268
  const result = db.query(sql).run(...params);
219
269
  return { changes: result.changes };
220
270
  });
@@ -226,6 +276,7 @@ class BunSqliteClient implements DbClient {
226
276
  ): Promise<{ changes: number; executionMs: number }> {
227
277
  let executionMs = 0;
228
278
  const result = await this.execute(
279
+ sql,
229
280
  (db) => {
230
281
  const run = db.query(sql).run(...params);
231
282
  return { changes: run.changes };
@@ -248,12 +299,10 @@ class BunSqliteClient implements DbClient {
248
299
  for (let attemptIndex = 0; ; attemptIndex++) {
249
300
  const backoff = this.busyRetry.backoffMs[attemptIndex];
250
301
  const canRetryAfter = backoff !== undefined && sleptMs + backoff <= this.busyRetry.maxWaitMs;
251
- // The FINAL attempt camps on the ambient busy_timeout instead of the
252
- // short spin: a lone writer sampling 100ms windows can stay phase-locked
253
- // with a periodic external holder and miss every release gap (measured:
254
- // 3 of 6 writes lost at 1 writer under a 6s/500ms locker). One long
255
- // synchronous spin per request, only after every polite attempt failed,
256
- // acquires the lock the instant the holder releases.
302
+ // The FINAL attempt spins `finalAttemptSpinMs`, never the ambient
303
+ // busy_timeout: a 5s synchronous camp froze every in-flight request.
304
+ // The dense 500ms backoff tail catches a periodic holder's release gap
305
+ // instead (see DEFAULT_BUSY_RETRY).
257
306
  const attempt = await this.runTransactionAttempt(fn, readOnly, !canRetryAfter);
258
307
  if (attempt.ok) return attempt.value;
259
308
  if (!canRetryAfter || backoff === undefined) throw attempt.busyBegin;
@@ -274,7 +323,7 @@ class BunSqliteClient implements DbClient {
274
323
  private async runTransactionAttempt<T>(
275
324
  fn: (tx: DbExecutor) => Promise<T>,
276
325
  readOnly: boolean,
277
- campOnAmbientTimeout = false,
326
+ finalAttempt = false,
278
327
  ): Promise<{ ok: true; value: T } | { ok: false; busyBegin: unknown }> {
279
328
  const release = await this.lock.acquire();
280
329
  const ctx: TxContext = { closed: false, afterCommit: [] };
@@ -293,13 +342,7 @@ class BunSqliteClient implements DbClient {
293
342
  // instead of on an arbitrary statement mid-callback where it is
294
343
  // not. The short spin keeps the driver's synchronous busy wait off
295
344
  // the event loop; the real waiting is the caller's async backoff.
296
- // The final attempt camps on the ambient busy_timeout instead (see
297
- // transaction()).
298
- if (campOnAmbientTimeout) {
299
- db.run("BEGIN IMMEDIATE");
300
- } else {
301
- this.shortSpin(db, () => db.run("BEGIN IMMEDIATE"));
302
- }
345
+ this.shortSpin(db, () => db.run("BEGIN IMMEDIATE"), finalAttempt);
303
346
  } catch (err) {
304
347
  if (isSqliteBusy(err)) return { ok: false, busyBegin: err };
305
348
  throw err;
@@ -393,19 +436,27 @@ class BunSqliteClient implements DbClient {
393
436
  * a post-commit continuation (e.g. queueMicrotask) is treated as absent.
394
437
  */
395
438
  private async execute<T>(
439
+ sql: string,
396
440
  op: (db: Database) => T,
397
441
  recordExecutionMs?: (ms: number) => void,
398
442
  ): Promise<T> {
399
443
  // Wraps ONLY the synchronous driver call, so lock acquisition above and
400
444
  // backoff sleeps below are never charged to the statement. Called once per
401
445
  // attempt; a retrying statement reports the sum. See DbClient.runTimed.
402
- const timed = (attempt: () => T): T => {
403
- if (!recordExecutionMs) return attempt();
446
+ // A BUSY attempt that will be retried is not logged as slow: its block is
447
+ // capped at `attemptSpinMs` by construction.
448
+ const timed = (attempt: () => T, retryable = false): T => {
404
449
  const startedAt = performance.now();
450
+ let willRetry = false;
405
451
  try {
406
452
  return attempt();
453
+ } catch (err) {
454
+ willRetry = retryable && isSqliteBusy(err);
455
+ throw err;
407
456
  } finally {
408
- recordExecutionMs(performance.now() - startedAt);
457
+ const ms = performance.now() - startedAt;
458
+ recordExecutionMs?.(ms);
459
+ if (!willRetry) warnIfSlowStatement(sql, ms);
409
460
  }
410
461
  };
411
462
  const ctx = txContext.getStore();
@@ -420,13 +471,11 @@ class BunSqliteClient implements DbClient {
420
471
  let backoff = 0;
421
472
  try {
422
473
  // A top-level single statement is atomic, so a repeat on BUSY is
423
- // exact. The final attempt camps on the ambient busy_timeout so a
424
- // lone writer cannot stay phase-locked with a periodic holder (see
474
+ // exact. The final attempt spins `finalAttemptSpinMs` (see
425
475
  // transaction()).
426
- return timed(() =>
427
- canRetryAfter
428
- ? this.shortSpin(this.getDatabase(), () => op(this.getDatabase()))
429
- : op(this.getDatabase()),
476
+ return timed(
477
+ () => this.shortSpin(this.getDatabase(), () => op(this.getDatabase()), !canRetryAfter),
478
+ canRetryAfter,
430
479
  );
431
480
  } catch (err) {
432
481
  if (!isSqliteBusy(err) || !canRetryAfter || next === undefined) throw err;
@@ -442,17 +491,21 @@ class BunSqliteClient implements DbClient {
442
491
  }
443
492
 
444
493
  /**
445
- * Run a retryable driver attempt under a short `busy_timeout` so the
446
- * synchronous in-driver busy wait blocks the event loop for at most
447
- * `attemptSpinMs`. Only called while the FIFO lock is held, so the
448
- * set/attempt/restore sequence cannot interleave with other client work.
494
+ * Run a driver attempt under a short `busy_timeout` so the synchronous
495
+ * in-driver busy wait blocks the event loop for at most `attemptSpinMs`
496
+ * (`finalAttemptSpinMs` for the final attempt). Only called while the FIFO
497
+ * lock is held, so the set/attempt/restore sequence cannot interleave with
498
+ * other client work.
449
499
  */
450
- private shortSpin<T>(db: Database, attempt: () => T): T {
500
+ private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false): T {
451
501
  if (this.restoreBusyTimeoutMs === null) {
452
502
  const row = db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
453
503
  this.restoreBusyTimeoutMs = row?.timeout ?? 5000;
454
504
  }
455
- db.exec(`PRAGMA busy_timeout = ${this.busyRetry.attemptSpinMs}`);
505
+ const spinMs = finalAttempt
506
+ ? (this.busyRetry.finalAttemptSpinMs ?? DEFAULT_FINAL_ATTEMPT_SPIN_MS)
507
+ : this.busyRetry.attemptSpinMs;
508
+ db.exec(`PRAGMA busy_timeout = ${spinMs}`);
456
509
  try {
457
510
  return attempt();
458
511
  } finally {
@@ -488,17 +541,23 @@ class BunSqliteClient implements DbClient {
488
541
  };
489
542
  return {
490
543
  query: async <T>(sql: string, params: DbParam[] = []): Promise<T[]> =>
491
- guard()
492
- .query<T, DbParam[]>(sql)
493
- .all(...params),
544
+ timedStatement(sql, () =>
545
+ guard()
546
+ .query<T, DbParam[]>(sql)
547
+ .all(...params),
548
+ ),
494
549
  get: async <T>(sql: string, params: DbParam[] = []): Promise<T | null> =>
495
- guard()
496
- .query<T, DbParam[]>(sql)
497
- .get(...params),
550
+ timedStatement(sql, () =>
551
+ guard()
552
+ .query<T, DbParam[]>(sql)
553
+ .get(...params),
554
+ ),
498
555
  run: async (sql: string, params: DbParam[] = []): Promise<{ changes: number }> => {
499
- const result = guard()
500
- .query(sql)
501
- .run(...params);
556
+ const result = timedStatement(sql, () =>
557
+ guard()
558
+ .query(sql)
559
+ .run(...params),
560
+ );
502
561
  return { changes: result.changes };
503
562
  },
504
563
  };