@ixo/oracle-runtime-workers 0.13.0 → 0.15.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 (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +13 -8
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +267 -0
  18. package/src/channels/contract.ts +137 -0
  19. package/src/channels/turns.ts +270 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +170 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +1883 -264
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +504 -108
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +529 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. package/src/tasks/topic-deliverables.ts +70 -0
@@ -27,6 +27,7 @@ import {
27
27
  DoVfs,
28
28
  DoVfsError,
29
29
  isDoVfs,
30
+ type Exclusive,
30
31
  VACUUM_FILE_SUFFIX,
31
32
  VFS_PAGE_SIZE,
32
33
  type DoVfsOptions,
@@ -91,16 +92,35 @@ export function vfsNameForObject(ctx: DoSqliteContext): string {
91
92
  return `do-${ctx.id.toString()}`;
92
93
  }
93
94
 
95
+ /**
96
+ * The async chain holding the connection mutex: a top-level transaction, a
97
+ * `withoutTransactions()` callback, or one statement issued outside both.
98
+ * `inTx` is set from BEGIN until COMMIT/ROLLBACK has run. `held` turns
99
+ * false on release, so a promise chain that outlives its holder (started
100
+ * inside, still running afterwards) queues like any other caller instead
101
+ * of borrowing a lock that is no longer its own.
102
+ */
103
+ interface MutexFrame {
104
+ held: boolean;
105
+ inTx: boolean;
106
+ }
107
+
94
108
  export class DoSqliteDatabase {
95
109
  private db: number | null;
110
+ /** Open transaction levels on the connection (1 = BEGIN, +1 per savepoint). */
96
111
  private txDepth = 0;
97
112
  private savepointSeq = 0;
98
- /** Serialises top-level transactions (see `transaction()`). */
113
+ /**
114
+ * The connection mutex (see `withMutex()`): transactions, statements
115
+ * issued outside a transaction, and `withoutTransactions()` callbacks
116
+ * queue on it, so a statement never runs inside another chain's open
117
+ * transaction.
118
+ */
99
119
  private txQueue: Promise<void> = Promise.resolve();
100
120
  /** Serialises individual statements on this connection (see `serialized()`). */
101
121
  private opQueue: Promise<void> = Promise.resolve();
102
- /** Set while inside a transaction's async chain — nested calls become savepoints. */
103
- private readonly txContext = new AsyncLocalStorage<{ depth: number }>();
122
+ /** The frame of the chain that holds the mutex, visible to everything it awaits. */
123
+ private readonly holder = new AsyncLocalStorage<MutexFrame>();
104
124
 
105
125
  private constructor(
106
126
  private readonly runtime: SqliteRuntime,
@@ -252,18 +272,59 @@ export class DoSqliteDatabase {
252
272
  sql: string,
253
273
  params?: SqlParams,
254
274
  ): Promise<T[]> {
255
- return this.serialized(() =>
256
- this.retryingColdMisses(() => this.execUnlocked<T>(sql, params)),
275
+ return this.withMutex(() =>
276
+ this.serialized(() =>
277
+ this.retryingColdMisses(() => this.execUnlocked<T>(sql, params)),
278
+ ),
257
279
  );
258
280
  }
259
281
 
260
282
  /** Run a statement that produces no rows of interest; returns the change count. */
261
283
  async run(sql: string, params?: SqlParams): Promise<RunResult> {
262
- return this.serialized(() =>
263
- this.retryingColdMisses(() => this.runUnlocked(sql, params)),
284
+ return this.withMutex(() =>
285
+ this.serialized(() =>
286
+ this.retryingColdMisses(() => this.runUnlocked(sql, params)),
287
+ ),
264
288
  );
265
289
  }
266
290
 
291
+ /** The caller's mutex frame, when its async chain holds the mutex right now. */
292
+ private ownFrame(): MutexFrame | undefined {
293
+ const frame = this.holder.getStore();
294
+ return frame?.held === true ? frame : undefined;
295
+ }
296
+
297
+ /**
298
+ * Run `fn` holding the connection mutex. Re-entrant: a chain that already
299
+ * holds it (a transaction body, a `withoutTransactions()` callback, and
300
+ * everything they await) runs `fn` at once; any other chain queues until
301
+ * the holder is done — a statement issued while another chain's
302
+ * transaction is open runs after its COMMIT or ROLLBACK, never inside it.
303
+ *
304
+ * A holder must not await work that another chain started and that
305
+ * itself needs the mutex: that work waits for the holder, the holder for
306
+ * the work.
307
+ */
308
+ private async withMutex<T>(
309
+ fn: (frame: MutexFrame) => Promise<T>,
310
+ ): Promise<T> {
311
+ const own = this.ownFrame();
312
+ if (own !== undefined) return fn(own);
313
+ const previous = this.txQueue;
314
+ let release: () => void = () => undefined;
315
+ this.txQueue = new Promise<void>((resolve) => {
316
+ release = resolve;
317
+ });
318
+ await previous;
319
+ const frame: MutexFrame = { held: true, inTx: false };
320
+ try {
321
+ return await this.holder.run(frame, () => fn(frame));
322
+ } finally {
323
+ frame.held = false;
324
+ release();
325
+ }
326
+ }
327
+
267
328
  /**
268
329
  * Run one statement, re-running it after a cold-miss resolve. Only
269
330
  * outside a transaction: inside one, SQLite may have put the pager into
@@ -271,8 +332,7 @@ export class DoSqliteDatabase {
271
332
  * retried by `transaction()` instead.
272
333
  */
273
334
  private async retryingColdMisses<T>(op: () => Promise<T>): Promise<T> {
274
- if (this.txContext.getStore() !== undefined || this.txDepth > 0)
275
- return op();
335
+ if (this.ownFrame()?.inTx === true) return op();
276
336
  let pinned = false;
277
337
  try {
278
338
  for (let attempt = 0; ; attempt++) {
@@ -382,44 +442,21 @@ export class DoSqliteDatabase {
382
442
  }
383
443
 
384
444
  /**
385
- * Run `fn` inside a transaction. Top-level transactions are **serialised**
386
- * through a mutex — a Durable Object is single-threaded but async, and
387
- * LangGraph issues `put`/`putWrites` concurrently, so two callers awaiting
388
- * `BEGIN` at the same time would otherwise collide ("cannot start a
389
- * transaction within a transaction"). True nesting (a `transaction()` call
390
- * made *inside* `fn`'s async chain) is detected with AsyncLocalStorage and
391
- * becomes a SAVEPOINT. Commits when `fn` resolves, rolls back and rethrows
392
- * when it rejects. The VFS flushes committed pages to storage atomically
393
- * when SQLite releases the write lock at COMMIT.
445
+ * Run `fn` inside a transaction. Top-level transactions hold the
446
+ * connection mutex (see `withMutex()`) from BEGIN to COMMIT/ROLLBACK — a
447
+ * Durable Object is single-threaded but async, and LangGraph issues
448
+ * `put`/`putWrites` concurrently, so two callers awaiting `BEGIN` at the
449
+ * same time would otherwise collide ("cannot start a transaction within a
450
+ * transaction"), and a statement from another chain would join the open
451
+ * transaction. True nesting (a `transaction()` call made *inside* `fn`'s
452
+ * async chain) is detected with AsyncLocalStorage and becomes a
453
+ * SAVEPOINT. Commits when `fn` resolves, rolls back and rethrows when it
454
+ * rejects. The VFS flushes committed pages to storage atomically when
455
+ * SQLite releases the write lock at COMMIT.
394
456
  */
395
457
  async transaction<T>(fn: () => Promise<T>): Promise<T> {
396
- if (this.txContext.getStore() !== undefined) {
397
- const savepoint = `sp_${++this.savepointSeq}`;
398
- await this.run(`SAVEPOINT ${savepoint}`);
399
- this.txDepth++;
400
- try {
401
- const result = await fn();
402
- this.txDepth--;
403
- await this.run(`RELEASE ${savepoint}`);
404
- return result;
405
- } catch (error) {
406
- this.txDepth--;
407
- try {
408
- await this.run(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
409
- } catch {
410
- // The original error is more useful than a rollback failure.
411
- }
412
- throw error;
413
- }
414
- }
415
-
416
- const previous = this.txQueue;
417
- let release: () => void = () => undefined;
418
- this.txQueue = new Promise<void>((resolve) => {
419
- release = resolve;
420
- });
421
- await previous;
422
- try {
458
+ if (this.ownFrame()?.inTx === true) return this.savepoint(fn);
459
+ return this.withMutex(async (frame) => {
423
460
  // A cold page inside `fn` fails a statement with SQLITE_IOERR; the
424
461
  // transaction is rolled back, the segment(s) fetched and pinned, and
425
462
  // `fn` re-run from the top (it must be re-runnable: the checkpointer's
@@ -429,27 +466,7 @@ export class DoSqliteDatabase {
429
466
  try {
430
467
  for (let attempt = 0; ; attempt++) {
431
468
  try {
432
- return await this.txContext.run({ depth: 1 }, async () => {
433
- await this.run('BEGIN IMMEDIATE');
434
- this.txDepth++;
435
- try {
436
- const result = await fn();
437
- this.txDepth--;
438
- await this.run('COMMIT');
439
- // Misses a statement swallowed (see `retryingColdMisses`)
440
- // must not trigger a retry of some later, unrelated error.
441
- this.vfs.clearMisses();
442
- return result;
443
- } catch (error) {
444
- this.txDepth--;
445
- try {
446
- await this.run('ROLLBACK');
447
- } catch {
448
- // The original error is more useful than a rollback failure.
449
- }
450
- throw error;
451
- }
452
- });
469
+ return await this.transactionAttempt(frame, fn);
453
470
  } catch (error) {
454
471
  if (
455
472
  !this.vfs.hasMisses() ||
@@ -464,34 +481,76 @@ export class DoSqliteDatabase {
464
481
  } finally {
465
482
  if (pinned) this.vfs.releasePins();
466
483
  }
484
+ });
485
+ }
486
+
487
+ /** One BEGIN … COMMIT of `fn`, rolled back when anything in it fails. */
488
+ private async transactionAttempt<T>(
489
+ frame: MutexFrame,
490
+ fn: () => Promise<T>,
491
+ ): Promise<T> {
492
+ await this.run('BEGIN IMMEDIATE');
493
+ frame.inTx = true;
494
+ this.txDepth = 1;
495
+ try {
496
+ const result = await fn();
497
+ await this.run('COMMIT');
498
+ // Misses a statement swallowed (see `retryingColdMisses`) must not
499
+ // trigger a retry of some later, unrelated error.
500
+ this.vfs.clearMisses();
501
+ return result;
502
+ } catch (error) {
503
+ // Also after a failed COMMIT: SQLite keeps the transaction open when
504
+ // a deferred constraint fails it. When SQLite already rolled back on
505
+ // its own, this ROLLBACK fails harmlessly.
506
+ try {
507
+ await this.run('ROLLBACK');
508
+ } catch {
509
+ // The original error is more useful than a rollback failure.
510
+ }
511
+ throw error;
467
512
  } finally {
468
- release();
513
+ // Only now: until ROLLBACK/COMMIT has run, the transaction is open.
514
+ frame.inTx = false;
515
+ this.txDepth = 0;
516
+ }
517
+ }
518
+
519
+ /** A nested `transaction()`: a SAVEPOINT inside the caller's transaction. */
520
+ private async savepoint<T>(fn: () => Promise<T>): Promise<T> {
521
+ const savepoint = `sp_${++this.savepointSeq}`;
522
+ await this.run(`SAVEPOINT ${savepoint}`);
523
+ this.txDepth++;
524
+ try {
525
+ const result = await fn();
526
+ await this.run(`RELEASE ${savepoint}`);
527
+ return result;
528
+ } catch (error) {
529
+ try {
530
+ await this.run(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
531
+ } catch {
532
+ // The original error is more useful than a rollback failure.
533
+ }
534
+ throw error;
535
+ } finally {
536
+ this.txDepth--;
469
537
  }
470
538
  }
471
539
 
472
540
  /**
473
- * Run `fn` while HOLDING the transaction mutex without opening a
474
- * transaction — for statements SQLite refuses to run inside one (`VACUUM`).
475
- * Transactions started elsewhere queue behind it; statements inside `fn`
476
- * still serialise through the per-statement queue as usual.
541
+ * Run `fn` while HOLDING the connection mutex without opening a
542
+ * transaction — for statements SQLite refuses to run inside one
543
+ * (`VACUUM`) and for work no transaction may interleave with. Transactions
544
+ * and statements of other chains queue behind it; statements, nested
545
+ * `withoutTransactions()` and transactions inside `fn` run at once.
477
546
  */
478
547
  async withoutTransactions<T>(fn: () => Promise<T>): Promise<T> {
479
- if (this.txContext.getStore() !== undefined) {
548
+ if (this.ownFrame()?.inTx === true) {
480
549
  throw new DoVfsError(
481
550
  'withoutTransactions() cannot run inside a transaction',
482
551
  );
483
552
  }
484
- const previous = this.txQueue;
485
- let release: () => void = () => undefined;
486
- this.txQueue = new Promise<void>((resolve) => {
487
- release = resolve;
488
- });
489
- await previous;
490
- try {
491
- return await fn();
492
- } finally {
493
- release();
494
- }
553
+ return this.withMutex(() => fn());
495
554
  }
496
555
 
497
556
  /**
@@ -542,17 +601,24 @@ export class DoSqliteDatabase {
542
601
  return this.vfs.tierEnabled;
543
602
  }
544
603
 
545
- /** One eviction pass; holds the transaction mutex so no write interleaves. */
604
+ /**
605
+ * One eviction pass. It holds the connection mutex only for its short
606
+ * synchronous steps (plan, read a segment's rows, commit a segment) and
607
+ * never across R2 I/O, so a turn that starts mid-pass is not held up
608
+ * for the rest of it (see `DoVfs.tierFlush`).
609
+ */
546
610
  async tierFlush(
547
611
  opts: { force?: boolean; maxSegments?: number } = {},
548
612
  ): Promise<TierFlushResult> {
549
- if (this.txDepth > 0)
613
+ if (this.ownFrame()?.inTx === true)
550
614
  throw new DoVfsError('tierFlush() is not allowed inside a transaction');
551
- return this.withoutTransactions(() =>
552
- this.vfs.tierFlush(this.fileName, opts),
553
- );
615
+ return this.vfs.tierFlush(this.fileName, opts, this.exclusive);
554
616
  }
555
617
 
618
+ /** Runs a synchronous VFS step holding the connection mutex. */
619
+ private readonly exclusive: Exclusive = <T>(step: () => T): Promise<T> =>
620
+ this.withoutTransactions(async () => step());
621
+
556
622
  /** Persist the chunks touched since the last call (call at the end of a turn). */
557
623
  recordAccess(): void {
558
624
  this.vfs.recordAccess(this.fileName);
@@ -571,7 +637,7 @@ export class DoSqliteDatabase {
571
637
 
572
638
  /** Pull every cold chunk back into storage (the file is then fully hot). */
573
639
  async materializeTier(): Promise<{ chunks: number }> {
574
- if (this.txDepth > 0)
640
+ if (this.ownFrame()?.inTx === true)
575
641
  throw new DoVfsError(
576
642
  'materializeTier() is not allowed inside a transaction',
577
643
  );
@@ -695,7 +761,7 @@ export class DoSqliteDatabase {
695
761
  * the file is open. Transiently needs 2× the file in DO storage.
696
762
  */
697
763
  async compact(): Promise<{ beforeBytes: number; afterBytes: number }> {
698
- if (this.txDepth > 0)
764
+ if (this.ownFrame()?.inTx === true)
699
765
  throw new DoVfsError('compact() is not allowed inside a transaction');
700
766
  const target = `${this.fileName}${VACUUM_FILE_SUFFIX}`;
701
767
  return this.withoutTransactions(async () => {