wolbarg 0.5.5 → 0.5.7

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.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import fs6 from "node:fs";
2
- import path7 from "node:path";
1
+ import fs6, { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import path7, { isAbsolute, resolve, dirname, join } from "node:path";
3
3
  import { createHash } from 'crypto';
4
4
  import fs from 'fs/promises';
5
+ import { AsyncLocalStorage } from 'async_hooks';
5
6
  import { DatabaseSync } from "node:sqlite";
6
7
  import * as sqliteVec from 'sqlite-vec';
7
- import { AsyncLocalStorage } from 'async_hooks';
8
8
  import { EventEmitter } from 'events';
9
9
 
10
10
  // src/core/wolbarg.ts
@@ -445,8 +445,8 @@ var AsyncMutex = class {
445
445
  */
446
446
  async runExclusive(fn) {
447
447
  let release;
448
- const next = new Promise((resolve) => {
449
- release = resolve;
448
+ const next = new Promise((resolve3) => {
449
+ release = resolve3;
450
450
  });
451
451
  const previous = this.chain;
452
452
  this.chain = previous.then(() => next);
@@ -2238,19 +2238,35 @@ var DEFAULT_CONCURRENCY = {
2238
2238
  maxRetries: 5,
2239
2239
  baseBackoffMs: 50,
2240
2240
  maxBackoffMs: 2e3,
2241
- lockTimeoutMs: 5e3
2241
+ lockTimeoutMs: 5e3,
2242
+ lockDeadlineMs: 3e4
2243
+ };
2244
+ var MULTI_PROCESS_CONCURRENCY = {
2245
+ maxRetries: 12,
2246
+ baseBackoffMs: 40,
2247
+ maxBackoffMs: 3e3,
2248
+ lockTimeoutMs: 15e3,
2249
+ lockDeadlineMs: 9e4
2242
2250
  };
2243
2251
  function resolveConcurrencyConfig(input) {
2252
+ const base = input?.multiProcess ? MULTI_PROCESS_CONCURRENCY : DEFAULT_CONCURRENCY;
2253
+ const maxRetries = input?.maxRetries ?? base.maxRetries;
2254
+ const baseBackoffMs = input?.baseBackoffMs ?? base.baseBackoffMs;
2255
+ const maxBackoffMs = input?.maxBackoffMs ?? base.maxBackoffMs;
2256
+ const lockTimeoutMs = input?.lockTimeoutMs ?? base.lockTimeoutMs;
2257
+ const lockDeadlineMs = input?.lockDeadlineMs ?? Math.max(base.lockDeadlineMs, lockTimeoutMs * (maxRetries + 1));
2244
2258
  const resolved = {
2245
- maxRetries: input?.maxRetries ?? DEFAULT_CONCURRENCY.maxRetries,
2246
- baseBackoffMs: input?.baseBackoffMs ?? DEFAULT_CONCURRENCY.baseBackoffMs,
2247
- maxBackoffMs: input?.maxBackoffMs ?? DEFAULT_CONCURRENCY.maxBackoffMs,
2248
- lockTimeoutMs: input?.lockTimeoutMs ?? DEFAULT_CONCURRENCY.lockTimeoutMs
2259
+ maxRetries,
2260
+ baseBackoffMs,
2261
+ maxBackoffMs,
2262
+ lockTimeoutMs,
2263
+ lockDeadlineMs
2249
2264
  };
2250
2265
  assertPositiveInt(resolved.maxRetries, "concurrency.maxRetries");
2251
2266
  assertPositiveNumber(resolved.baseBackoffMs, "concurrency.baseBackoffMs");
2252
2267
  assertPositiveNumber(resolved.maxBackoffMs, "concurrency.maxBackoffMs");
2253
2268
  assertPositiveInt(resolved.lockTimeoutMs, "concurrency.lockTimeoutMs");
2269
+ assertPositiveInt(resolved.lockDeadlineMs, "concurrency.lockDeadlineMs");
2254
2270
  if (resolved.maxBackoffMs < resolved.baseBackoffMs) {
2255
2271
  throw new ConfigurationError(
2256
2272
  "concurrency.maxBackoffMs must be >= concurrency.baseBackoffMs"
@@ -2276,19 +2292,25 @@ function isSqliteBusyError(error) {
2276
2292
  return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("busy");
2277
2293
  }
2278
2294
  function sleep(ms) {
2279
- return new Promise((resolve) => setTimeout(resolve, ms));
2295
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2280
2296
  }
2281
2297
  function backoffDelay(attempt, config) {
2282
2298
  const exp = Math.min(
2283
2299
  config.maxBackoffMs,
2284
2300
  config.baseBackoffMs * 2 ** attempt
2285
2301
  );
2286
- const jitter = Math.random() * config.baseBackoffMs;
2287
- return Math.min(config.maxBackoffMs, exp + jitter);
2302
+ return Math.random() * exp;
2303
+ }
2304
+ function deadlineExceeded(startedAt, config) {
2305
+ return performance.now() - startedAt >= config.lockDeadlineMs;
2288
2306
  }
2289
2307
  async function withImmediateTransaction(db, config, fn, onRetry) {
2290
2308
  let lastError;
2309
+ const startedAt = performance.now();
2291
2310
  for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
2311
+ if (attempt > 0 && deadlineExceeded(startedAt, config)) {
2312
+ break;
2313
+ }
2292
2314
  try {
2293
2315
  db.exec("BEGIN IMMEDIATE");
2294
2316
  try {
@@ -2308,7 +2330,7 @@ async function withImmediateTransaction(db, config, fn, onRetry) {
2308
2330
  if (!isBusy) {
2309
2331
  throw error;
2310
2332
  }
2311
- if (attempt >= config.maxRetries) {
2333
+ if (attempt >= config.maxRetries || deadlineExceeded(startedAt, config)) {
2312
2334
  break;
2313
2335
  }
2314
2336
  const delay = backoffDelay(attempt, config);
@@ -2321,7 +2343,7 @@ async function withImmediateTransaction(db, config, fn, onRetry) {
2321
2343
  {
2322
2344
  cause: lastError instanceof Error ? lastError : void 0,
2323
2345
  reason: "SQLITE_BUSY exhausted retries",
2324
- suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
2346
+ suggestion: "Increase concurrency.maxRetries / concurrency.lockDeadlineMs, set concurrency.multiProcess: true for shared-file writers, or use the Postgres backend."
2325
2347
  }
2326
2348
  );
2327
2349
  }
@@ -2379,6 +2401,20 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2379
2401
  transactionDepth = 0;
2380
2402
  /** Incrementing counter for deterministic savepoint names. */
2381
2403
  savepointCounter = 0;
2404
+ /**
2405
+ * Serializes top-level write transactions on the single SQLite connection.
2406
+ * SQLite is single-writer; serializing in-process avoids SQLITE_BUSY churn
2407
+ * AND prevents interleaved BEGIN/SAVEPOINT state corruption when many async
2408
+ * callers share one connection (see "no such savepoint" class of bugs).
2409
+ */
2410
+ writeMutex = new AsyncMutex();
2411
+ /**
2412
+ * Async-context flag: true while executing inside a top-level write
2413
+ * transaction held by this provider. Used so writes issued from within an
2414
+ * ambient transaction (e.g. compress()) join that ACID unit, WITHOUT
2415
+ * mistaking the coalescer's own flush window for an ambient transaction.
2416
+ */
2417
+ txContext = new AsyncLocalStorage();
2382
2418
  /** Hot in-process ANN for blob backend (sqlite-vec unavailable platforms). */
2383
2419
  memoryIndex = null;
2384
2420
  memoryIndexDirty = false;
@@ -2421,62 +2457,85 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2421
2457
  }
2422
2458
  /** Open the database, run migrations, and prepare statements. */
2423
2459
  async open() {
2424
- try {
2425
- const dbPath = this.resolvePath(this.connectionString);
2426
- this.resolvedPath = dbPath;
2427
- if (dbPath !== ":memory:") {
2428
- fs6.mkdirSync(path7.dirname(dbPath), { recursive: true });
2429
- }
2430
- const db = new DatabaseSync(dbPath, { allowExtension: true });
2431
- this.db = db;
2432
- db.exec("PRAGMA journal_mode = WAL;");
2433
- db.exec(`
2434
- PRAGMA synchronous = NORMAL;
2435
- PRAGMA foreign_keys = ON;
2436
- PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
2437
- PRAGMA temp_store = MEMORY;
2438
- PRAGMA cache_size = -32768;
2439
- PRAGMA mmap_size = 134217728;
2440
- PRAGMA wal_autocheckpoint = 2000;
2441
- PRAGMA recursive_triggers = OFF;
2442
- `);
2443
- this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
2444
- this.runMigrations(db);
2445
- this.statements = this.prepareStatements(db);
2446
- const backend = this.readMetaString(META_KEYS.vectorBackend);
2447
- const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
2448
- if (backend) {
2449
- this.vectorBackend = backend;
2450
- } else if (this.sqliteVecLoaded) {
2451
- this.vectorBackend = "sqlite-vec";
2452
- } else {
2453
- if (!warnedSqliteVecFallback) {
2454
- warnedSqliteVecFallback = true;
2455
- warnLogger.warn(
2456
- "sqlite-vec unavailable on this platform; using blob ANN fallback."
2460
+ const startedAt = performance.now();
2461
+ let attempt = 0;
2462
+ for (; ; ) {
2463
+ try {
2464
+ this.openOnce();
2465
+ return;
2466
+ } catch (error) {
2467
+ try {
2468
+ this.db?.close();
2469
+ } catch {
2470
+ }
2471
+ this.db = null;
2472
+ this.statements = null;
2473
+ const busy = isSqliteBusyError(error);
2474
+ const exhausted = attempt >= this.concurrency.maxRetries || performance.now() - startedAt >= this.concurrency.lockDeadlineMs;
2475
+ if (!busy || exhausted) {
2476
+ throw new InitializationError(
2477
+ `Failed to open SQLite database: ${this.describe(error)}`,
2478
+ { cause: error instanceof Error ? error : void 0 }
2457
2479
  );
2458
2480
  }
2459
- this.vectorBackend = "blob";
2481
+ const base = Math.min(
2482
+ this.concurrency.maxBackoffMs,
2483
+ this.concurrency.baseBackoffMs * 2 ** attempt
2484
+ );
2485
+ const delay = Math.random() * base;
2486
+ this.retryLog?.(
2487
+ `SQLITE_BUSY during open attempt=${attempt + 1} backoffMs=${Math.round(delay)}`
2488
+ );
2489
+ attempt += 1;
2490
+ await new Promise((r) => setTimeout(r, delay));
2460
2491
  }
2461
- if (dims !== null) {
2462
- this.vectorDimensions = dims;
2463
- this.ensureVectorStorage(dims);
2464
- this.reprepareVectorStatements();
2465
- if (this.vectorBackend === "blob") {
2466
- this.memoryIndexDirty = true;
2467
- }
2492
+ }
2493
+ }
2494
+ /** Single open attempt — connect, apply pragmas, migrate, prepare. */
2495
+ openOnce() {
2496
+ const dbPath = this.resolvePath(this.connectionString);
2497
+ this.resolvedPath = dbPath;
2498
+ if (dbPath !== ":memory:") {
2499
+ fs6.mkdirSync(path7.dirname(dbPath), { recursive: true });
2500
+ }
2501
+ const db = new DatabaseSync(dbPath, { allowExtension: true });
2502
+ this.db = db;
2503
+ db.exec(`PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};`);
2504
+ db.exec("PRAGMA journal_mode = WAL;");
2505
+ db.exec(`
2506
+ PRAGMA synchronous = NORMAL;
2507
+ PRAGMA foreign_keys = ON;
2508
+ PRAGMA temp_store = MEMORY;
2509
+ PRAGMA cache_size = -32768;
2510
+ PRAGMA mmap_size = 134217728;
2511
+ PRAGMA wal_autocheckpoint = 2000;
2512
+ PRAGMA recursive_triggers = OFF;
2513
+ `);
2514
+ this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
2515
+ this.runMigrations(db);
2516
+ this.statements = this.prepareStatements(db);
2517
+ const backend = this.readMetaString(META_KEYS.vectorBackend);
2518
+ const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
2519
+ if (backend) {
2520
+ this.vectorBackend = backend;
2521
+ } else if (this.sqliteVecLoaded) {
2522
+ this.vectorBackend = "sqlite-vec";
2523
+ } else {
2524
+ if (!warnedSqliteVecFallback) {
2525
+ warnedSqliteVecFallback = true;
2526
+ warnLogger.warn(
2527
+ "sqlite-vec unavailable on this platform; using blob ANN fallback."
2528
+ );
2468
2529
  }
2469
- } catch (error) {
2470
- try {
2471
- this.db?.close();
2472
- } catch {
2530
+ this.vectorBackend = "blob";
2531
+ }
2532
+ if (dims !== null) {
2533
+ this.vectorDimensions = dims;
2534
+ this.ensureVectorStorage(dims);
2535
+ this.reprepareVectorStatements();
2536
+ if (this.vectorBackend === "blob") {
2537
+ this.memoryIndexDirty = true;
2473
2538
  }
2474
- this.db = null;
2475
- this.statements = null;
2476
- throw new InitializationError(
2477
- `Failed to open SQLite database: ${this.describe(error)}`,
2478
- { cause: error instanceof Error ? error : void 0 }
2479
- );
2480
2539
  }
2481
2540
  }
2482
2541
  /** Drain pending inserts, optimize, and close the SQLite connection. */
@@ -2549,8 +2608,11 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2549
2608
  /** Insert a single memory row (coalesced under concurrent writers). */
2550
2609
  async insertMemory(input) {
2551
2610
  this.requireVectorReady();
2552
- return new Promise((resolve, reject) => {
2553
- this.insertQueue.push({ input, resolve, reject });
2611
+ if (this.txContext.getStore() === true) {
2612
+ return this.insertMemoryImmediate(input);
2613
+ }
2614
+ return new Promise((resolve3, reject) => {
2615
+ this.insertQueue.push({ input, resolve: resolve3, reject });
2554
2616
  if (this.insertQueue.length >= INSERT_COALESCE_THRESHOLD) {
2555
2617
  this.insertFlushScheduled = false;
2556
2618
  void this.flushInsertQueue();
@@ -2845,7 +2907,8 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2845
2907
  const stmts = this.requireStatements();
2846
2908
  const archived = [];
2847
2909
  return this.withTransaction(() => {
2848
- for (const id of ids) {
2910
+ const orderedIds = [...new Set(ids)].sort();
2911
+ for (const id of orderedIds) {
2849
2912
  const existing = stmts.getMemoryById.get(id, organization);
2850
2913
  const result = stmts.archiveMemory.run(
2851
2914
  compressedIntoId,
@@ -2986,7 +3049,7 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2986
3049
  }
2987
3050
  async withTransaction(fn) {
2988
3051
  const db = this.requireDb();
2989
- if (this.transactionDepth > 0) {
3052
+ if (this.txContext.getStore() === true) {
2990
3053
  const savepointName = `wolbarg_sp_${this.savepointCounter++}`;
2991
3054
  db.exec(`SAVEPOINT ${savepointName}`);
2992
3055
  try {
@@ -3004,21 +3067,26 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
3004
3067
  this.transactionDepth -= 1;
3005
3068
  }
3006
3069
  }
3007
- this.transactionDepth = 1;
3008
- try {
3009
- return await withImmediateTransaction(
3010
- db,
3011
- this.concurrency,
3012
- fn,
3013
- (attempt, delayMs) => {
3014
- this.retryLog?.(
3015
- `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
3070
+ return this.writeMutex.runExclusive(
3071
+ () => this.txContext.run(true, async () => {
3072
+ this.transactionDepth = 1;
3073
+ this.savepointCounter = 0;
3074
+ try {
3075
+ return await withImmediateTransaction(
3076
+ db,
3077
+ this.concurrency,
3078
+ fn,
3079
+ (attempt, delayMs) => {
3080
+ this.retryLog?.(
3081
+ `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
3082
+ );
3083
+ }
3016
3084
  );
3085
+ } finally {
3086
+ this.transactionDepth = 0;
3017
3087
  }
3018
- );
3019
- } finally {
3020
- this.transactionDepth = 0;
3021
- }
3088
+ })
3089
+ );
3022
3090
  }
3023
3091
  // ─── internals ───────────────────────────────────────────────────────────
3024
3092
  tryLoadSqliteVec(db) {
@@ -4185,8 +4253,20 @@ function createPostgresListenerFromPool(pool, onError) {
4185
4253
  }
4186
4254
 
4187
4255
  // src/storage/providers/postgres.ts
4188
- var txStore = new AsyncLocalStorage();
4189
4256
  var warnLogger2 = new WolbargLogger("warn");
4257
+ function isRetriablePgTxError(error) {
4258
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
4259
+ if (code === "40P01" || code === "40001") {
4260
+ return true;
4261
+ }
4262
+ const msg = error instanceof Error ? error.message : String(error);
4263
+ return /deadlock detected|could not serialize access/i.test(msg);
4264
+ }
4265
+ function pgTxBackoffMs(attempt) {
4266
+ const base = 25 * 2 ** attempt;
4267
+ const jitter = Math.random() * 25;
4268
+ return Math.min(1e3, base + jitter);
4269
+ }
4190
4270
  var warnedPgvectorFallback = false;
4191
4271
  var warnedFtsKeywordSearch2 = false;
4192
4272
  var warnedHnswSoftFail = false;
@@ -4231,7 +4311,11 @@ function withPoolStartupOptions(connectionString, durableWrites) {
4231
4311
  "-c jit=off",
4232
4312
  // Bake HNSW search GUCs into the connection — avoids per-recall set_config.
4233
4313
  "-c hnsw.ef_search=40",
4234
- "-c hnsw.iterative_scan=relaxed_order"
4314
+ "-c hnsw.iterative_scan=relaxed_order",
4315
+ // Bound waits so lock storms cannot hang agents indefinitely.
4316
+ "-c statement_timeout=30000",
4317
+ "-c lock_timeout=10000",
4318
+ "-c idle_in_transaction_session_timeout=60000"
4235
4319
  ];
4236
4320
  if (!durableWrites) {
4237
4321
  flags.unshift("-c synchronous_commit=off");
@@ -4312,6 +4396,8 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4312
4396
  insertFlushScheduled = false;
4313
4397
  insertFlushTimer = null;
4314
4398
  insertFlushInFlight = 0;
4399
+ /** Per-provider TX context — never share across PostgresStorageProvider instances. */
4400
+ txStore = new AsyncLocalStorage();
4315
4401
  /**
4316
4402
  * @param options - Connection string, pool size, and durability flags.
4317
4403
  */
@@ -4611,8 +4697,11 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4611
4697
  /** Insert a single memory row (coalesced under concurrent writers). */
4612
4698
  async insertMemory(input) {
4613
4699
  this.requireVectorReady();
4614
- return new Promise((resolve, reject) => {
4615
- this.insertQueue.push({ input, resolve, reject });
4700
+ if (this.txStore.getStore()) {
4701
+ return this.insertMemoryImmediate(input);
4702
+ }
4703
+ return new Promise((resolve3, reject) => {
4704
+ this.insertQueue.push({ input, resolve: resolve3, reject });
4616
4705
  if (this.insertQueue.length >= COALESCE_FLUSH_THRESHOLD) {
4617
4706
  this.clearInsertFlushTimer();
4618
4707
  void this.flushInsertQueue();
@@ -4847,10 +4936,18 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4847
4936
  /** Update memory content, metadata, embedding, and content hash. */
4848
4937
  async updateMemory(input) {
4849
4938
  return this.withTransaction(async () => {
4850
- const existing = await this.getMemoryById(input.id, input.organization);
4851
- if (!existing) {
4939
+ const locked = await this.query(
4940
+ `SELECT id, organization, agent, content_text, metadata_json,
4941
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
4942
+ FROM memories
4943
+ WHERE id = $1 AND organization = $2
4944
+ FOR UPDATE`,
4945
+ [input.id, input.organization]
4946
+ );
4947
+ if (locked.rows.length === 0) {
4852
4948
  return null;
4853
4949
  }
4950
+ const existing = this.mapRow(locked.rows[0]);
4854
4951
  const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
4855
4952
  await this.query(
4856
4953
  `UPDATE memories SET
@@ -5168,23 +5265,38 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5168
5265
  if (ids.length === 0) {
5169
5266
  return [];
5170
5267
  }
5268
+ const orderedIds = [...new Set(ids)].sort();
5269
+ await this.query(
5270
+ `SELECT id FROM memories
5271
+ WHERE organization = $1 AND archived = false AND id = ANY($2::text[])
5272
+ ORDER BY id
5273
+ FOR UPDATE`,
5274
+ [organization, orderedIds]
5275
+ );
5171
5276
  const result = await this.query(
5172
5277
  `UPDATE memories
5173
5278
  SET archived = true, compressed_into = $1, updated_at = $2
5174
5279
  WHERE organization = $3 AND archived = false AND id = ANY($4::text[])
5175
5280
  RETURNING id`,
5176
- [compressedIntoId, archivedAt, organization, ids]
5281
+ [compressedIntoId, archivedAt, organization, orderedIds]
5177
5282
  );
5178
5283
  const archived = result.rows.map((r) => String(r.id));
5179
5284
  if (archived.length === 0) {
5180
5285
  return [];
5181
5286
  }
5182
- await this.query(
5183
- `UPDATE memory_embeddings
5184
- SET archived = true
5185
- WHERE memory_id = ANY($1::text[])`,
5186
- [archived]
5187
- );
5287
+ if (this.hasPgvector) {
5288
+ await this.query(
5289
+ `UPDATE memory_embeddings
5290
+ SET archived = true
5291
+ WHERE memory_id = ANY($1::text[])`,
5292
+ [archived]
5293
+ );
5294
+ } else {
5295
+ await this.query(
5296
+ `DELETE FROM memory_embeddings_blob WHERE memory_id = ANY($1::text[])`,
5297
+ [archived]
5298
+ );
5299
+ }
5188
5300
  const histIds = [];
5189
5301
  const memIds = [];
5190
5302
  const types = [];
@@ -5287,27 +5399,40 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5287
5399
  return Number(result.rows[0]?.size ?? 0);
5288
5400
  }
5289
5401
  async withTransaction(fn) {
5290
- const existing = txStore.getStore();
5402
+ const existing = this.txStore.getStore();
5291
5403
  if (existing) {
5292
5404
  return fn();
5293
5405
  }
5294
- const client = await this.requirePool().connect();
5295
- try {
5296
- await client.query("BEGIN");
5297
- const result = await txStore.run(client, fn);
5298
- await client.query("COMMIT");
5299
- return result;
5300
- } catch (error) {
5301
- await client.query("ROLLBACK").catch(() => void 0);
5302
- if (error instanceof DatabaseError || error instanceof InitializationError) {
5303
- throw error;
5406
+ const maxAttempts = 5;
5407
+ let lastError;
5408
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
5409
+ const client = await this.requirePool().connect();
5410
+ try {
5411
+ await client.query("BEGIN");
5412
+ const result = await this.txStore.run(client, fn);
5413
+ await client.query("COMMIT");
5414
+ return result;
5415
+ } catch (error) {
5416
+ await client.query("ROLLBACK").catch(() => void 0);
5417
+ lastError = error;
5418
+ if (isRetriablePgTxError(error) && attempt < maxAttempts - 1) {
5419
+ await new Promise((r) => setTimeout(r, pgTxBackoffMs(attempt)));
5420
+ continue;
5421
+ }
5422
+ if (error instanceof DatabaseError || error instanceof InitializationError) {
5423
+ throw error;
5424
+ }
5425
+ throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
5426
+ cause: error instanceof Error ? error : void 0
5427
+ });
5428
+ } finally {
5429
+ client.release();
5304
5430
  }
5305
- throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
5306
- cause: error instanceof Error ? error : void 0
5307
- });
5308
- } finally {
5309
- client.release();
5310
5431
  }
5432
+ throw new DatabaseError(
5433
+ `Transaction failed after retries: ${this.describe(lastError)}`,
5434
+ { cause: lastError instanceof Error ? lastError : void 0 }
5435
+ );
5311
5436
  }
5312
5437
  async runMigrations() {
5313
5438
  await this.query(`
@@ -5535,13 +5660,13 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5535
5660
  await this.query(`DELETE FROM memory_embeddings_blob WHERE memory_id = $1`, [memoryId]).catch(() => void 0);
5536
5661
  }
5537
5662
  async query(text, params) {
5538
- const tx = txStore.getStore();
5663
+ const tx = this.txStore.getStore();
5539
5664
  const target = tx ?? this.requirePool();
5540
5665
  return target.query(text, params);
5541
5666
  }
5542
5667
  /** Named prepared statement — parse/plan cached per pool connection. */
5543
5668
  async queryNamed(name, text, params) {
5544
- const tx = txStore.getStore();
5669
+ const tx = this.txStore.getStore();
5545
5670
  const target = tx ?? this.requirePool();
5546
5671
  return target.query({ name, text, values: params });
5547
5672
  }
@@ -5627,7 +5752,7 @@ function createDatabaseProvider(config) {
5627
5752
  }
5628
5753
 
5629
5754
  // src/version.ts
5630
- var SDK_VERSION = "0.5.5";
5755
+ var SDK_VERSION = "0.5.6";
5631
5756
 
5632
5757
  // src/memory/transfer.ts
5633
5758
  var warnedMissingExportManifest = false;
@@ -9583,12 +9708,18 @@ var Wolbarg = class {
9583
9708
  createdAt: timestamp,
9584
9709
  updatedAt: timestamp
9585
9710
  });
9711
+ const sourceIds = [...records.map((r) => r.id)].sort();
9586
9712
  const archivedIds = await storage.archiveMemories(
9587
- records.map((r) => r.id),
9713
+ sourceIds,
9588
9714
  organization,
9589
9715
  summaryId,
9590
9716
  timestamp
9591
9717
  );
9718
+ if (archivedIds.length < 2) {
9719
+ throw new ValidationError(
9720
+ "compress lost a concurrency race: fewer than 2 source memories remained active"
9721
+ );
9722
+ }
9592
9723
  return {
9593
9724
  summary: toMemoryRecord(summaryRow),
9594
9725
  archivedIds
@@ -10600,8 +10731,11 @@ var meta = {
10600
10731
  };
10601
10732
 
10602
10733
  // src/factories/index.ts
10603
- function sqlite(connectionString) {
10604
- return new SqliteStorageProvider({ connectionString });
10734
+ function sqlite(connectionString, options) {
10735
+ return new SqliteStorageProvider({
10736
+ connectionString,
10737
+ concurrency: options?.concurrency
10738
+ });
10605
10739
  }
10606
10740
  function sqliteConfig(connectionString) {
10607
10741
  return {
@@ -11118,6 +11252,271 @@ function percentile(sorted, p) {
11118
11252
  return sorted[idx];
11119
11253
  }
11120
11254
 
11121
- export { CompressionError, ConfigurationError, DatabaseError, EmbeddingError, FixedChunkingStrategy, GraphCheckpointNotSupportedError, HeadingChunkingStrategy, InitializationError, LlmCompressionProvider, MarkdownChunkingStrategy, MemoryNotFoundError, Neo4jGraphProvider, NoopTelemetryProvider, ParagraphChunkingStrategy, PostgresStorageProvider, ProviderNotConfiguredError, SDK_VERSION, SentenceChunkingStrategy, SqliteCheckpointProvider, SqliteDatabaseProvider, SqliteEventDatabase, SqliteGraphProvider, SqliteStorageProvider, SqliteTelemetryProvider, StorageLockedError, TelemetryEmitter, ValidationError, Wolbarg, WolbargError, WolbargLogger, bgeReranker, bm25, cohereReranker, createChunkingStrategy, createCompressionProvider, createDatabaseProvider, createEmbeddingProvider, createLlmProvider, createStorageProvider, createTelemetryProvider, createWolbarg, crossEncoder, geminiEmbedding, geminiVision, jinaReranker, lmStudioEmbedding, meta, neo4jGraph, ollamaEmbedding, ollamaLlm, openRouterEmbedding, openRouterLlm, openaiCompatibleEmbedding, openaiCompatibleLlm, openaiEmbedding, openaiLlm, openaiReranker, openaiVision, postgres, postgresConfig, runBenchmark, sqlite, sqliteCheckpoint, sqliteConfig, sqliteGraph, sqliteTelemetry, summarizeBenchmark, tesseract, togetherEmbedding, vllmEmbedding, wolbarg, wrapOperationError };
11255
+ // src/config/providers.ts
11256
+ var EMBEDDING_PROVIDER_PRESETS = [
11257
+ {
11258
+ id: "openai",
11259
+ label: "OpenAI",
11260
+ defaultBaseUrl: "https://api.openai.com/v1",
11261
+ defaultModel: "text-embedding-3-small",
11262
+ apiKeyEnv: "OPENAI_API_KEY"
11263
+ },
11264
+ {
11265
+ id: "ollama",
11266
+ label: "Ollama (local)",
11267
+ defaultBaseUrl: "http://127.0.0.1:11434/v1",
11268
+ defaultModel: "nomic-embed-text",
11269
+ apiKeyEnv: "OLLAMA_API_KEY",
11270
+ apiKeyHint: "Any non-empty value works locally (e.g. ollama)"
11271
+ },
11272
+ {
11273
+ id: "openrouter",
11274
+ label: "OpenRouter",
11275
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
11276
+ defaultModel: "openai/text-embedding-3-small",
11277
+ apiKeyEnv: "OPENROUTER_API_KEY"
11278
+ },
11279
+ {
11280
+ id: "lmstudio",
11281
+ label: "LM Studio (local)",
11282
+ defaultBaseUrl: "http://127.0.0.1:1234/v1",
11283
+ defaultModel: "text-embedding-nomic-embed-text-v1.5",
11284
+ apiKeyEnv: "LM_STUDIO_API_KEY",
11285
+ apiKeyHint: "Often unused locally \u2014 any non-empty value is fine"
11286
+ },
11287
+ {
11288
+ id: "gemini",
11289
+ label: "Google Gemini",
11290
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
11291
+ defaultModel: "text-embedding-004",
11292
+ apiKeyEnv: "GEMINI_API_KEY"
11293
+ },
11294
+ {
11295
+ id: "together",
11296
+ label: "Together AI",
11297
+ defaultBaseUrl: "https://api.together.xyz/v1",
11298
+ defaultModel: "togethercomputer/m2-bert-80M-8k-retrieval",
11299
+ apiKeyEnv: "TOGETHER_API_KEY"
11300
+ },
11301
+ {
11302
+ id: "vllm",
11303
+ label: "vLLM (local/server)",
11304
+ defaultBaseUrl: "http://127.0.0.1:8000/v1",
11305
+ defaultModel: "text-embedding-ada-002",
11306
+ apiKeyEnv: "VLLM_API_KEY",
11307
+ apiKeyHint: "Only if your server requires auth"
11308
+ },
11309
+ {
11310
+ id: "custom",
11311
+ label: "Custom OpenAI-compatible",
11312
+ defaultBaseUrl: "https://api.openai.com/v1",
11313
+ defaultModel: "text-embedding-3-small",
11314
+ apiKeyEnv: "WOLBARG_EMBEDDING_API_KEY"
11315
+ }
11316
+ ];
11317
+ function getEmbeddingProviderPreset(id) {
11318
+ return EMBEDDING_PROVIDER_PRESETS.find((p) => p.id === id);
11319
+ }
11320
+ var DEFAULT_SQLITE_DB_PATH = ".wolbarg/shared-memory/memory.db";
11321
+ var DEFAULT_ORGANIZATION = "default";
11322
+ var DEFAULT_CONFIG_PATH = ".wolbarg/config.json";
11323
+ var DEFAULT_ENV_PATH = ".wolbarg/.env";
11324
+ function defaultProjectConfig() {
11325
+ return {
11326
+ version: 1,
11327
+ organization: DEFAULT_ORGANIZATION,
11328
+ database: {
11329
+ provider: "sqlite",
11330
+ url: DEFAULT_SQLITE_DB_PATH
11331
+ },
11332
+ paths: {
11333
+ config: DEFAULT_CONFIG_PATH
11334
+ }
11335
+ };
11336
+ }
11337
+ function resolveConfigPath(cwd = process.cwd(), configPath) {
11338
+ const rel = configPath ?? DEFAULT_CONFIG_PATH;
11339
+ return isAbsolute(rel) ? rel : resolve(cwd, rel);
11340
+ }
11341
+ function resolveEnvPath(cwd = process.cwd(), envPath) {
11342
+ const rel = envPath ?? DEFAULT_ENV_PATH;
11343
+ return isAbsolute(rel) ? rel : resolve(cwd, rel);
11344
+ }
11345
+ function loadProjectConfig(cwd = process.cwd(), configPath) {
11346
+ const full = resolveConfigPath(cwd, configPath);
11347
+ if (!existsSync(full)) return null;
11348
+ const raw = JSON.parse(readFileSync(full, "utf8"));
11349
+ if (!raw || raw.version !== 1 || !raw.database?.url) {
11350
+ throw new Error(`Invalid Wolbarg config at ${full}`);
11351
+ }
11352
+ return raw;
11353
+ }
11354
+ function saveProjectConfig(config, options = {}) {
11355
+ const cwd = options.cwd ?? process.cwd();
11356
+ const configPath = resolveConfigPath(cwd, options.configPath);
11357
+ const dir = dirname(configPath);
11358
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
11359
+ if (existsSync(configPath) && options.overwrite === false) {
11360
+ throw new Error(`Config already exists: ${configPath} (pass --force to overwrite)`);
11361
+ }
11362
+ const toWrite = {
11363
+ ...config,
11364
+ paths: {
11365
+ config: options.configPath ?? DEFAULT_CONFIG_PATH,
11366
+ ...options.envPath || options.apiKey ? { env: options.envPath ?? DEFAULT_ENV_PATH } : config.paths?.env ? { env: config.paths.env } : {}
11367
+ }
11368
+ };
11369
+ writeFileSync(configPath, `${JSON.stringify(toWrite, null, 2)}
11370
+ `, "utf8");
11371
+ let envPath;
11372
+ if (options.apiKey && config.embedding?.apiKeyEnv) {
11373
+ envPath = resolveEnvPath(cwd, options.envPath);
11374
+ ensureParent(envPath);
11375
+ upsertEnvVar(envPath, config.embedding.apiKeyEnv, options.apiKey);
11376
+ ensureGitignore(cwd, [DEFAULT_ENV_PATH, ".env"]);
11377
+ }
11378
+ if (config.database.provider === "sqlite" && !config.database.url.includes("://")) {
11379
+ const dbPath = isAbsolute(config.database.url) ? config.database.url : resolve(cwd, config.database.url);
11380
+ ensureParent(dbPath);
11381
+ }
11382
+ return { configPath, envPath };
11383
+ }
11384
+ function ensureParent(filePath) {
11385
+ const dir = dirname(filePath);
11386
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
11387
+ }
11388
+ function upsertEnvVar(envPath, key, value) {
11389
+ ensureParent(envPath);
11390
+ let text = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
11391
+ const line = `${key}=${shellQuoteEnv(value)}`;
11392
+ const re = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
11393
+ if (re.test(text)) {
11394
+ text = text.replace(re, line);
11395
+ } else {
11396
+ if (text && !text.endsWith("\n")) text += "\n";
11397
+ text += `${line}
11398
+ `;
11399
+ }
11400
+ writeFileSync(envPath, text, "utf8");
11401
+ }
11402
+ function shellQuoteEnv(value) {
11403
+ if (/[\s#"']/.test(value)) {
11404
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11405
+ }
11406
+ return value;
11407
+ }
11408
+ function escapeRegExp(s) {
11409
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11410
+ }
11411
+ function ensureGitignore(cwd, entries) {
11412
+ const gi = join(cwd, ".gitignore");
11413
+ let text = existsSync(gi) ? readFileSync(gi, "utf8") : "";
11414
+ let changed = false;
11415
+ for (const entry of entries) {
11416
+ if (text.split(/\r?\n/).some((l) => l.trim() === entry)) continue;
11417
+ if (text && !text.endsWith("\n")) text += "\n";
11418
+ text += `${entry}
11419
+ `;
11420
+ changed = true;
11421
+ }
11422
+ if (changed) writeFileSync(gi, text, "utf8");
11423
+ }
11424
+ function resolveEmbeddingApiKey(config, env = process.env) {
11425
+ const name = config.embedding?.apiKeyEnv;
11426
+ if (!name) return "";
11427
+ return env[name] ?? "";
11428
+ }
11429
+ function assertEmbeddingPreset(provider) {
11430
+ const preset = getEmbeddingProviderPreset(provider);
11431
+ if (!preset) {
11432
+ throw new Error(
11433
+ `Unknown embedding provider "${provider}". Choose one of: ${[
11434
+ "openai",
11435
+ "ollama",
11436
+ "openrouter",
11437
+ "lmstudio",
11438
+ "gemini",
11439
+ "together",
11440
+ "vllm",
11441
+ "custom"
11442
+ ].join(", ")}`
11443
+ );
11444
+ }
11445
+ return preset.id;
11446
+ }
11447
+ function applyEnvFile(envPath, base = {}) {
11448
+ const out = { ...base };
11449
+ if (!existsSync(envPath)) return out;
11450
+ const text = readFileSync(envPath, "utf8");
11451
+ for (const rawLine of text.split(/\r?\n/)) {
11452
+ const line = rawLine.trim();
11453
+ if (!line || line.startsWith("#")) continue;
11454
+ const eq = line.indexOf("=");
11455
+ if (eq <= 0) continue;
11456
+ const key = line.slice(0, eq).trim();
11457
+ let value = line.slice(eq + 1).trim();
11458
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
11459
+ value = value.slice(1, -1);
11460
+ }
11461
+ if (out[key] === void 0) out[key] = value;
11462
+ }
11463
+ return out;
11464
+ }
11465
+ function projectConfigToWolbargOptions(config, env = process.env) {
11466
+ if (!config.embedding) {
11467
+ throw new Error(
11468
+ "Project config has no embedding section. Re-run `wolbarg init` and configure an embedding provider, or pass embedding explicitly to wolbarg()."
11469
+ );
11470
+ }
11471
+ const apiKey = resolveEmbeddingApiKey(config, env);
11472
+ if (!apiKey) {
11473
+ throw new Error(
11474
+ `Missing API key. Set ${config.embedding.apiKeyEnv} in the environment or in .wolbarg/.env (from wolbarg init).`
11475
+ );
11476
+ }
11477
+ const dbUrl = config.database.url;
11478
+ return {
11479
+ organization: config.organization,
11480
+ database: {
11481
+ provider: config.database.provider,
11482
+ url: dbUrl,
11483
+ connectionString: dbUrl
11484
+ },
11485
+ embedding: {
11486
+ baseUrl: config.embedding.baseUrl,
11487
+ apiKey,
11488
+ model: config.embedding.model
11489
+ }
11490
+ };
11491
+ }
11492
+ function createWolbargFromProjectConfig(options = {}) {
11493
+ const cwd = options.cwd ?? process.cwd();
11494
+ const config = loadProjectConfig(cwd, options.configPath);
11495
+ if (!config) {
11496
+ throw new Error(
11497
+ `No Wolbarg config found. Run \`wolbarg init\` in ${cwd} (expected .wolbarg/config.json).`
11498
+ );
11499
+ }
11500
+ let env = options.env ?? { ...process.env };
11501
+ if (options.loadEnvFile !== false) {
11502
+ const envFile = resolveEnvPath(
11503
+ cwd,
11504
+ config.paths?.env ?? DEFAULT_ENV_PATH
11505
+ );
11506
+ env = applyEnvFile(envFile, env);
11507
+ }
11508
+ if (options.organization) {
11509
+ config.organization = options.organization;
11510
+ }
11511
+ if (config.database.provider === "sqlite" && !config.database.url.includes("://") && !isAbsolute(config.database.url)) {
11512
+ config.database = {
11513
+ ...config.database,
11514
+ url: resolve(cwd, config.database.url)
11515
+ };
11516
+ }
11517
+ return wolbarg(projectConfigToWolbargOptions(config, env));
11518
+ }
11519
+
11520
+ export { CompressionError, ConfigurationError, DEFAULT_CONFIG_PATH, DEFAULT_ENV_PATH, DEFAULT_ORGANIZATION, DEFAULT_SQLITE_DB_PATH, DatabaseError, EMBEDDING_PROVIDER_PRESETS, EmbeddingError, FixedChunkingStrategy, GraphCheckpointNotSupportedError, HeadingChunkingStrategy, InitializationError, LlmCompressionProvider, MarkdownChunkingStrategy, MemoryNotFoundError, Neo4jGraphProvider, NoopTelemetryProvider, ParagraphChunkingStrategy, PostgresStorageProvider, ProviderNotConfiguredError, SDK_VERSION, SentenceChunkingStrategy, SqliteCheckpointProvider, SqliteDatabaseProvider, SqliteEventDatabase, SqliteGraphProvider, SqliteStorageProvider, SqliteTelemetryProvider, StorageLockedError, TelemetryEmitter, ValidationError, Wolbarg, WolbargError, WolbargLogger, applyEnvFile, assertEmbeddingPreset, bgeReranker, bm25, cohereReranker, createChunkingStrategy, createCompressionProvider, createDatabaseProvider, createEmbeddingProvider, createLlmProvider, createStorageProvider, createTelemetryProvider, createWolbarg, createWolbargFromProjectConfig, crossEncoder, defaultProjectConfig, geminiEmbedding, geminiVision, getEmbeddingProviderPreset, jinaReranker, lmStudioEmbedding, loadProjectConfig, meta, neo4jGraph, ollamaEmbedding, ollamaLlm, openRouterEmbedding, openRouterLlm, openaiCompatibleEmbedding, openaiCompatibleLlm, openaiEmbedding, openaiLlm, openaiReranker, openaiVision, postgres, postgresConfig, projectConfigToWolbargOptions, resolveConfigPath, resolveEmbeddingApiKey, resolveEnvPath, runBenchmark, saveProjectConfig, sqlite, sqliteCheckpoint, sqliteConfig, sqliteGraph, sqliteTelemetry, summarizeBenchmark, tesseract, togetherEmbedding, upsertEnvVar, vllmEmbedding, wolbarg, wrapOperationError };
11122
11521
  //# sourceMappingURL=index.js.map
11123
11522
  //# sourceMappingURL=index.js.map