feat: scaled transact budgets + labeled timeout diagnostics + envelope docs

- The transact apply budget scales with the batch — max(30s, opCount x 2s) —
  or is exactly the caller's new TransactOptions.timeoutMs. A flat 30s cap
  silently limited honest bulk work to ~15 operations on network-attached
  storage (~2s/op measured in a production import) while looking generous for
  small batches. Internal batch paths (removeMany chunks) get the same
  scaling via the shared transactTimeoutBudget helper.
- TransactionTimeoutError now reports the operation it stopped at as i/N with
  the operation's name, elapsed vs budgeted time, and states the batch rolled
  back atomically and is retryable; context carries the fields
  programmatically.
- The transact operational envelope is documented (optimistic-concurrency
  guide): budget math, chunking with ifAbsent idempotency, when to reach for
  addMany vs transact, and the precompute pattern (embedBatch + per-op
  vector) that keeps inference out of the commit path.
- Embedding claims made honest per an end-to-end probe of the built dist:
  batch and single embedding paths produce bit-identical vectors and a
  vector-supplied add is fully searchable, but batch throughput on the
  default WASM engine measures comparable to sequential (~160ms/text) — the
  5-10x speedup claim in the addMany JSDoc is replaced with the measured
  reality and the actual win (inference outside the budgeted write path).
This commit is contained in:
David Snelling 2026-07-17 16:49:38 -07:00
parent e0f6e7722f
commit 6ef9fcb7a2
7 changed files with 175 additions and 15 deletions

View file

@ -71,6 +71,7 @@ import type {
} from './plugin.js'
import { ConnectionsCodec } from './hnsw/connectionsCodec.js'
import { TransactionManager } from './transaction/TransactionManager.js'
import { transactTimeoutBudget } from './transaction/Transaction.js'
import { RevisionConflictError } from './transaction/RevisionConflictError.js'
import { EntityNotFoundError, RelationNotFoundError } from './errors/notFound.js'
import {
@ -1752,7 +1753,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
captureAndCheck({ nouns, verbs } as CommitBeforeImages)
}
await this.generationStore.runWithoutGeneration(() =>
this.transactionManager.executeTransaction(run)
this.transactionManager.executeTransaction(run, {
timeout: transactTimeoutBudget(
(touched.nouns?.length ?? 0) + (touched.verbs?.length ?? 0)
)
})
)
const timestamp = Date.now()
// Bootstrap writes are not generation-stamped; emit without one.
@ -1764,7 +1769,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
receipt = await this.generationStore.commitSingleOp({
touched,
precommit: captureAndCheck,
execute: () => this.transactionManager.executeTransaction(run)
execute: () =>
this.transactionManager.executeTransaction(run, {
timeout: transactTimeoutBudget(
(touched.nouns?.length ?? 0) + (touched.verbs?.length ?? 0)
)
})
})
} catch (err) {
// A failed rollback that left the store inconsistent (a remove/update
@ -6655,10 +6665,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
/**
* Add multiple entities in a single batch operation
*
* Uses batch embedding (embedBatch) to pre-compute all vectors in a single
* WASM forward pass instead of N individual embed() calls, providing 5-10x
* speedup on bulk inserts. Automatically adapts batch size and parallelism
* to the storage adapter (e.g., smaller batches for cloud storage).
* Uses batch embedding (embedBatch) to pre-compute all vectors before any
* storage write, keeping model inference out of the per-item write path.
* (On the default WASM engine, batch throughput is comparable to sequential
* embed() calls measured ~160 ms/text either way; a native embedding
* provider may batch faster.) Automatically adapts batch size and
* parallelism to the storage adapter (e.g., smaller batches for cloud
* storage).
*
* @param params - Batch add parameters
* @param params.items - Array of AddParams (same shape as brain.add())
@ -7797,11 +7810,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
ifAtGeneration: options?.ifAtGeneration,
precommit: casPrecommit,
execute: async () => {
await this.transactionManager.executeTransaction(async (tx) => {
for (const operation of plan.operations) {
tx.addOperation(operation)
}
})
await this.transactionManager.executeTransaction(
async (tx) => {
for (const operation of plan.operations) {
tx.addOperation(operation)
}
},
// Budget scales with the batch (or the caller's explicit
// timeoutMs): a flat 30s cap silently limited honest bulk work
// to ~15 ops on network disks (~2s/op measured in the field).
{ timeout: transactTimeoutBudget(plan.operations.length, options?.timeoutMs) }
)
}
}))
} catch (err) {