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.cjs CHANGED
@@ -4,9 +4,9 @@ var fs6 = require("node:fs");
4
4
  var path7 = require("node:path");
5
5
  var crypto$1 = require('crypto');
6
6
  var fs = require('fs/promises');
7
+ var async_hooks = require('async_hooks');
7
8
  var sqlite$1 = require("node:sqlite");
8
9
  var sqliteVec = require('sqlite-vec');
9
- var async_hooks = require('async_hooks');
10
10
  var events = require('events');
11
11
 
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -472,8 +472,8 @@ var AsyncMutex = class {
472
472
  */
473
473
  async runExclusive(fn) {
474
474
  let release;
475
- const next = new Promise((resolve) => {
476
- release = resolve;
475
+ const next = new Promise((resolve3) => {
476
+ release = resolve3;
477
477
  });
478
478
  const previous = this.chain;
479
479
  this.chain = previous.then(() => next);
@@ -2265,19 +2265,35 @@ var DEFAULT_CONCURRENCY = {
2265
2265
  maxRetries: 5,
2266
2266
  baseBackoffMs: 50,
2267
2267
  maxBackoffMs: 2e3,
2268
- lockTimeoutMs: 5e3
2268
+ lockTimeoutMs: 5e3,
2269
+ lockDeadlineMs: 3e4
2270
+ };
2271
+ var MULTI_PROCESS_CONCURRENCY = {
2272
+ maxRetries: 12,
2273
+ baseBackoffMs: 40,
2274
+ maxBackoffMs: 3e3,
2275
+ lockTimeoutMs: 15e3,
2276
+ lockDeadlineMs: 9e4
2269
2277
  };
2270
2278
  function resolveConcurrencyConfig(input) {
2279
+ const base = input?.multiProcess ? MULTI_PROCESS_CONCURRENCY : DEFAULT_CONCURRENCY;
2280
+ const maxRetries = input?.maxRetries ?? base.maxRetries;
2281
+ const baseBackoffMs = input?.baseBackoffMs ?? base.baseBackoffMs;
2282
+ const maxBackoffMs = input?.maxBackoffMs ?? base.maxBackoffMs;
2283
+ const lockTimeoutMs = input?.lockTimeoutMs ?? base.lockTimeoutMs;
2284
+ const lockDeadlineMs = input?.lockDeadlineMs ?? Math.max(base.lockDeadlineMs, lockTimeoutMs * (maxRetries + 1));
2271
2285
  const resolved = {
2272
- maxRetries: input?.maxRetries ?? DEFAULT_CONCURRENCY.maxRetries,
2273
- baseBackoffMs: input?.baseBackoffMs ?? DEFAULT_CONCURRENCY.baseBackoffMs,
2274
- maxBackoffMs: input?.maxBackoffMs ?? DEFAULT_CONCURRENCY.maxBackoffMs,
2275
- lockTimeoutMs: input?.lockTimeoutMs ?? DEFAULT_CONCURRENCY.lockTimeoutMs
2286
+ maxRetries,
2287
+ baseBackoffMs,
2288
+ maxBackoffMs,
2289
+ lockTimeoutMs,
2290
+ lockDeadlineMs
2276
2291
  };
2277
2292
  assertPositiveInt(resolved.maxRetries, "concurrency.maxRetries");
2278
2293
  assertPositiveNumber(resolved.baseBackoffMs, "concurrency.baseBackoffMs");
2279
2294
  assertPositiveNumber(resolved.maxBackoffMs, "concurrency.maxBackoffMs");
2280
2295
  assertPositiveInt(resolved.lockTimeoutMs, "concurrency.lockTimeoutMs");
2296
+ assertPositiveInt(resolved.lockDeadlineMs, "concurrency.lockDeadlineMs");
2281
2297
  if (resolved.maxBackoffMs < resolved.baseBackoffMs) {
2282
2298
  throw new ConfigurationError(
2283
2299
  "concurrency.maxBackoffMs must be >= concurrency.baseBackoffMs"
@@ -2303,19 +2319,25 @@ function isSqliteBusyError(error) {
2303
2319
  return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("busy");
2304
2320
  }
2305
2321
  function sleep(ms) {
2306
- return new Promise((resolve) => setTimeout(resolve, ms));
2322
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2307
2323
  }
2308
2324
  function backoffDelay(attempt, config) {
2309
2325
  const exp = Math.min(
2310
2326
  config.maxBackoffMs,
2311
2327
  config.baseBackoffMs * 2 ** attempt
2312
2328
  );
2313
- const jitter = Math.random() * config.baseBackoffMs;
2314
- return Math.min(config.maxBackoffMs, exp + jitter);
2329
+ return Math.random() * exp;
2330
+ }
2331
+ function deadlineExceeded(startedAt, config) {
2332
+ return performance.now() - startedAt >= config.lockDeadlineMs;
2315
2333
  }
2316
2334
  async function withImmediateTransaction(db, config, fn, onRetry) {
2317
2335
  let lastError;
2336
+ const startedAt = performance.now();
2318
2337
  for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
2338
+ if (attempt > 0 && deadlineExceeded(startedAt, config)) {
2339
+ break;
2340
+ }
2319
2341
  try {
2320
2342
  db.exec("BEGIN IMMEDIATE");
2321
2343
  try {
@@ -2335,7 +2357,7 @@ async function withImmediateTransaction(db, config, fn, onRetry) {
2335
2357
  if (!isBusy) {
2336
2358
  throw error;
2337
2359
  }
2338
- if (attempt >= config.maxRetries) {
2360
+ if (attempt >= config.maxRetries || deadlineExceeded(startedAt, config)) {
2339
2361
  break;
2340
2362
  }
2341
2363
  const delay = backoffDelay(attempt, config);
@@ -2348,7 +2370,7 @@ async function withImmediateTransaction(db, config, fn, onRetry) {
2348
2370
  {
2349
2371
  cause: lastError instanceof Error ? lastError : void 0,
2350
2372
  reason: "SQLITE_BUSY exhausted retries",
2351
- suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
2373
+ suggestion: "Increase concurrency.maxRetries / concurrency.lockDeadlineMs, set concurrency.multiProcess: true for shared-file writers, or use the Postgres backend."
2352
2374
  }
2353
2375
  );
2354
2376
  }
@@ -2406,6 +2428,20 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2406
2428
  transactionDepth = 0;
2407
2429
  /** Incrementing counter for deterministic savepoint names. */
2408
2430
  savepointCounter = 0;
2431
+ /**
2432
+ * Serializes top-level write transactions on the single SQLite connection.
2433
+ * SQLite is single-writer; serializing in-process avoids SQLITE_BUSY churn
2434
+ * AND prevents interleaved BEGIN/SAVEPOINT state corruption when many async
2435
+ * callers share one connection (see "no such savepoint" class of bugs).
2436
+ */
2437
+ writeMutex = new AsyncMutex();
2438
+ /**
2439
+ * Async-context flag: true while executing inside a top-level write
2440
+ * transaction held by this provider. Used so writes issued from within an
2441
+ * ambient transaction (e.g. compress()) join that ACID unit, WITHOUT
2442
+ * mistaking the coalescer's own flush window for an ambient transaction.
2443
+ */
2444
+ txContext = new async_hooks.AsyncLocalStorage();
2409
2445
  /** Hot in-process ANN for blob backend (sqlite-vec unavailable platforms). */
2410
2446
  memoryIndex = null;
2411
2447
  memoryIndexDirty = false;
@@ -2448,62 +2484,85 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2448
2484
  }
2449
2485
  /** Open the database, run migrations, and prepare statements. */
2450
2486
  async open() {
2451
- try {
2452
- const dbPath = this.resolvePath(this.connectionString);
2453
- this.resolvedPath = dbPath;
2454
- if (dbPath !== ":memory:") {
2455
- fs6__default.default.mkdirSync(path7__default.default.dirname(dbPath), { recursive: true });
2456
- }
2457
- const db = new sqlite$1.DatabaseSync(dbPath, { allowExtension: true });
2458
- this.db = db;
2459
- db.exec("PRAGMA journal_mode = WAL;");
2460
- db.exec(`
2461
- PRAGMA synchronous = NORMAL;
2462
- PRAGMA foreign_keys = ON;
2463
- PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
2464
- PRAGMA temp_store = MEMORY;
2465
- PRAGMA cache_size = -32768;
2466
- PRAGMA mmap_size = 134217728;
2467
- PRAGMA wal_autocheckpoint = 2000;
2468
- PRAGMA recursive_triggers = OFF;
2469
- `);
2470
- this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
2471
- this.runMigrations(db);
2472
- this.statements = this.prepareStatements(db);
2473
- const backend = this.readMetaString(META_KEYS.vectorBackend);
2474
- const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
2475
- if (backend) {
2476
- this.vectorBackend = backend;
2477
- } else if (this.sqliteVecLoaded) {
2478
- this.vectorBackend = "sqlite-vec";
2479
- } else {
2480
- if (!warnedSqliteVecFallback) {
2481
- warnedSqliteVecFallback = true;
2482
- warnLogger.warn(
2483
- "sqlite-vec unavailable on this platform; using blob ANN fallback."
2487
+ const startedAt = performance.now();
2488
+ let attempt = 0;
2489
+ for (; ; ) {
2490
+ try {
2491
+ this.openOnce();
2492
+ return;
2493
+ } catch (error) {
2494
+ try {
2495
+ this.db?.close();
2496
+ } catch {
2497
+ }
2498
+ this.db = null;
2499
+ this.statements = null;
2500
+ const busy = isSqliteBusyError(error);
2501
+ const exhausted = attempt >= this.concurrency.maxRetries || performance.now() - startedAt >= this.concurrency.lockDeadlineMs;
2502
+ if (!busy || exhausted) {
2503
+ throw new InitializationError(
2504
+ `Failed to open SQLite database: ${this.describe(error)}`,
2505
+ { cause: error instanceof Error ? error : void 0 }
2484
2506
  );
2485
2507
  }
2486
- this.vectorBackend = "blob";
2508
+ const base = Math.min(
2509
+ this.concurrency.maxBackoffMs,
2510
+ this.concurrency.baseBackoffMs * 2 ** attempt
2511
+ );
2512
+ const delay = Math.random() * base;
2513
+ this.retryLog?.(
2514
+ `SQLITE_BUSY during open attempt=${attempt + 1} backoffMs=${Math.round(delay)}`
2515
+ );
2516
+ attempt += 1;
2517
+ await new Promise((r) => setTimeout(r, delay));
2487
2518
  }
2488
- if (dims !== null) {
2489
- this.vectorDimensions = dims;
2490
- this.ensureVectorStorage(dims);
2491
- this.reprepareVectorStatements();
2492
- if (this.vectorBackend === "blob") {
2493
- this.memoryIndexDirty = true;
2494
- }
2519
+ }
2520
+ }
2521
+ /** Single open attempt — connect, apply pragmas, migrate, prepare. */
2522
+ openOnce() {
2523
+ const dbPath = this.resolvePath(this.connectionString);
2524
+ this.resolvedPath = dbPath;
2525
+ if (dbPath !== ":memory:") {
2526
+ fs6__default.default.mkdirSync(path7__default.default.dirname(dbPath), { recursive: true });
2527
+ }
2528
+ const db = new sqlite$1.DatabaseSync(dbPath, { allowExtension: true });
2529
+ this.db = db;
2530
+ db.exec(`PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};`);
2531
+ db.exec("PRAGMA journal_mode = WAL;");
2532
+ db.exec(`
2533
+ PRAGMA synchronous = NORMAL;
2534
+ PRAGMA foreign_keys = ON;
2535
+ PRAGMA temp_store = MEMORY;
2536
+ PRAGMA cache_size = -32768;
2537
+ PRAGMA mmap_size = 134217728;
2538
+ PRAGMA wal_autocheckpoint = 2000;
2539
+ PRAGMA recursive_triggers = OFF;
2540
+ `);
2541
+ this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
2542
+ this.runMigrations(db);
2543
+ this.statements = this.prepareStatements(db);
2544
+ const backend = this.readMetaString(META_KEYS.vectorBackend);
2545
+ const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
2546
+ if (backend) {
2547
+ this.vectorBackend = backend;
2548
+ } else if (this.sqliteVecLoaded) {
2549
+ this.vectorBackend = "sqlite-vec";
2550
+ } else {
2551
+ if (!warnedSqliteVecFallback) {
2552
+ warnedSqliteVecFallback = true;
2553
+ warnLogger.warn(
2554
+ "sqlite-vec unavailable on this platform; using blob ANN fallback."
2555
+ );
2495
2556
  }
2496
- } catch (error) {
2497
- try {
2498
- this.db?.close();
2499
- } catch {
2557
+ this.vectorBackend = "blob";
2558
+ }
2559
+ if (dims !== null) {
2560
+ this.vectorDimensions = dims;
2561
+ this.ensureVectorStorage(dims);
2562
+ this.reprepareVectorStatements();
2563
+ if (this.vectorBackend === "blob") {
2564
+ this.memoryIndexDirty = true;
2500
2565
  }
2501
- this.db = null;
2502
- this.statements = null;
2503
- throw new InitializationError(
2504
- `Failed to open SQLite database: ${this.describe(error)}`,
2505
- { cause: error instanceof Error ? error : void 0 }
2506
- );
2507
2566
  }
2508
2567
  }
2509
2568
  /** Drain pending inserts, optimize, and close the SQLite connection. */
@@ -2576,8 +2635,11 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2576
2635
  /** Insert a single memory row (coalesced under concurrent writers). */
2577
2636
  async insertMemory(input) {
2578
2637
  this.requireVectorReady();
2579
- return new Promise((resolve, reject) => {
2580
- this.insertQueue.push({ input, resolve, reject });
2638
+ if (this.txContext.getStore() === true) {
2639
+ return this.insertMemoryImmediate(input);
2640
+ }
2641
+ return new Promise((resolve3, reject) => {
2642
+ this.insertQueue.push({ input, resolve: resolve3, reject });
2581
2643
  if (this.insertQueue.length >= INSERT_COALESCE_THRESHOLD) {
2582
2644
  this.insertFlushScheduled = false;
2583
2645
  void this.flushInsertQueue();
@@ -2872,7 +2934,8 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2872
2934
  const stmts = this.requireStatements();
2873
2935
  const archived = [];
2874
2936
  return this.withTransaction(() => {
2875
- for (const id of ids) {
2937
+ const orderedIds = [...new Set(ids)].sort();
2938
+ for (const id of orderedIds) {
2876
2939
  const existing = stmts.getMemoryById.get(id, organization);
2877
2940
  const result = stmts.archiveMemory.run(
2878
2941
  compressedIntoId,
@@ -3013,7 +3076,7 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
3013
3076
  }
3014
3077
  async withTransaction(fn) {
3015
3078
  const db = this.requireDb();
3016
- if (this.transactionDepth > 0) {
3079
+ if (this.txContext.getStore() === true) {
3017
3080
  const savepointName = `wolbarg_sp_${this.savepointCounter++}`;
3018
3081
  db.exec(`SAVEPOINT ${savepointName}`);
3019
3082
  try {
@@ -3031,21 +3094,26 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
3031
3094
  this.transactionDepth -= 1;
3032
3095
  }
3033
3096
  }
3034
- this.transactionDepth = 1;
3035
- try {
3036
- return await withImmediateTransaction(
3037
- db,
3038
- this.concurrency,
3039
- fn,
3040
- (attempt, delayMs) => {
3041
- this.retryLog?.(
3042
- `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
3097
+ return this.writeMutex.runExclusive(
3098
+ () => this.txContext.run(true, async () => {
3099
+ this.transactionDepth = 1;
3100
+ this.savepointCounter = 0;
3101
+ try {
3102
+ return await withImmediateTransaction(
3103
+ db,
3104
+ this.concurrency,
3105
+ fn,
3106
+ (attempt, delayMs) => {
3107
+ this.retryLog?.(
3108
+ `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
3109
+ );
3110
+ }
3043
3111
  );
3112
+ } finally {
3113
+ this.transactionDepth = 0;
3044
3114
  }
3045
- );
3046
- } finally {
3047
- this.transactionDepth = 0;
3048
- }
3115
+ })
3116
+ );
3049
3117
  }
3050
3118
  // ─── internals ───────────────────────────────────────────────────────────
3051
3119
  tryLoadSqliteVec(db) {
@@ -4212,8 +4280,20 @@ function createPostgresListenerFromPool(pool, onError) {
4212
4280
  }
4213
4281
 
4214
4282
  // src/storage/providers/postgres.ts
4215
- var txStore = new async_hooks.AsyncLocalStorage();
4216
4283
  var warnLogger2 = new WolbargLogger("warn");
4284
+ function isRetriablePgTxError(error) {
4285
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
4286
+ if (code === "40P01" || code === "40001") {
4287
+ return true;
4288
+ }
4289
+ const msg = error instanceof Error ? error.message : String(error);
4290
+ return /deadlock detected|could not serialize access/i.test(msg);
4291
+ }
4292
+ function pgTxBackoffMs(attempt) {
4293
+ const base = 25 * 2 ** attempt;
4294
+ const jitter = Math.random() * 25;
4295
+ return Math.min(1e3, base + jitter);
4296
+ }
4217
4297
  var warnedPgvectorFallback = false;
4218
4298
  var warnedFtsKeywordSearch2 = false;
4219
4299
  var warnedHnswSoftFail = false;
@@ -4258,7 +4338,11 @@ function withPoolStartupOptions(connectionString, durableWrites) {
4258
4338
  "-c jit=off",
4259
4339
  // Bake HNSW search GUCs into the connection — avoids per-recall set_config.
4260
4340
  "-c hnsw.ef_search=40",
4261
- "-c hnsw.iterative_scan=relaxed_order"
4341
+ "-c hnsw.iterative_scan=relaxed_order",
4342
+ // Bound waits so lock storms cannot hang agents indefinitely.
4343
+ "-c statement_timeout=30000",
4344
+ "-c lock_timeout=10000",
4345
+ "-c idle_in_transaction_session_timeout=60000"
4262
4346
  ];
4263
4347
  if (!durableWrites) {
4264
4348
  flags.unshift("-c synchronous_commit=off");
@@ -4339,6 +4423,8 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4339
4423
  insertFlushScheduled = false;
4340
4424
  insertFlushTimer = null;
4341
4425
  insertFlushInFlight = 0;
4426
+ /** Per-provider TX context — never share across PostgresStorageProvider instances. */
4427
+ txStore = new async_hooks.AsyncLocalStorage();
4342
4428
  /**
4343
4429
  * @param options - Connection string, pool size, and durability flags.
4344
4430
  */
@@ -4638,8 +4724,11 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4638
4724
  /** Insert a single memory row (coalesced under concurrent writers). */
4639
4725
  async insertMemory(input) {
4640
4726
  this.requireVectorReady();
4641
- return new Promise((resolve, reject) => {
4642
- this.insertQueue.push({ input, resolve, reject });
4727
+ if (this.txStore.getStore()) {
4728
+ return this.insertMemoryImmediate(input);
4729
+ }
4730
+ return new Promise((resolve3, reject) => {
4731
+ this.insertQueue.push({ input, resolve: resolve3, reject });
4643
4732
  if (this.insertQueue.length >= COALESCE_FLUSH_THRESHOLD) {
4644
4733
  this.clearInsertFlushTimer();
4645
4734
  void this.flushInsertQueue();
@@ -4874,10 +4963,18 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
4874
4963
  /** Update memory content, metadata, embedding, and content hash. */
4875
4964
  async updateMemory(input) {
4876
4965
  return this.withTransaction(async () => {
4877
- const existing = await this.getMemoryById(input.id, input.organization);
4878
- if (!existing) {
4966
+ const locked = await this.query(
4967
+ `SELECT id, organization, agent, content_text, metadata_json,
4968
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
4969
+ FROM memories
4970
+ WHERE id = $1 AND organization = $2
4971
+ FOR UPDATE`,
4972
+ [input.id, input.organization]
4973
+ );
4974
+ if (locked.rows.length === 0) {
4879
4975
  return null;
4880
4976
  }
4977
+ const existing = this.mapRow(locked.rows[0]);
4881
4978
  const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
4882
4979
  await this.query(
4883
4980
  `UPDATE memories SET
@@ -5195,23 +5292,38 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5195
5292
  if (ids.length === 0) {
5196
5293
  return [];
5197
5294
  }
5295
+ const orderedIds = [...new Set(ids)].sort();
5296
+ await this.query(
5297
+ `SELECT id FROM memories
5298
+ WHERE organization = $1 AND archived = false AND id = ANY($2::text[])
5299
+ ORDER BY id
5300
+ FOR UPDATE`,
5301
+ [organization, orderedIds]
5302
+ );
5198
5303
  const result = await this.query(
5199
5304
  `UPDATE memories
5200
5305
  SET archived = true, compressed_into = $1, updated_at = $2
5201
5306
  WHERE organization = $3 AND archived = false AND id = ANY($4::text[])
5202
5307
  RETURNING id`,
5203
- [compressedIntoId, archivedAt, organization, ids]
5308
+ [compressedIntoId, archivedAt, organization, orderedIds]
5204
5309
  );
5205
5310
  const archived = result.rows.map((r) => String(r.id));
5206
5311
  if (archived.length === 0) {
5207
5312
  return [];
5208
5313
  }
5209
- await this.query(
5210
- `UPDATE memory_embeddings
5211
- SET archived = true
5212
- WHERE memory_id = ANY($1::text[])`,
5213
- [archived]
5214
- );
5314
+ if (this.hasPgvector) {
5315
+ await this.query(
5316
+ `UPDATE memory_embeddings
5317
+ SET archived = true
5318
+ WHERE memory_id = ANY($1::text[])`,
5319
+ [archived]
5320
+ );
5321
+ } else {
5322
+ await this.query(
5323
+ `DELETE FROM memory_embeddings_blob WHERE memory_id = ANY($1::text[])`,
5324
+ [archived]
5325
+ );
5326
+ }
5215
5327
  const histIds = [];
5216
5328
  const memIds = [];
5217
5329
  const types = [];
@@ -5314,27 +5426,40 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5314
5426
  return Number(result.rows[0]?.size ?? 0);
5315
5427
  }
5316
5428
  async withTransaction(fn) {
5317
- const existing = txStore.getStore();
5429
+ const existing = this.txStore.getStore();
5318
5430
  if (existing) {
5319
5431
  return fn();
5320
5432
  }
5321
- const client = await this.requirePool().connect();
5322
- try {
5323
- await client.query("BEGIN");
5324
- const result = await txStore.run(client, fn);
5325
- await client.query("COMMIT");
5326
- return result;
5327
- } catch (error) {
5328
- await client.query("ROLLBACK").catch(() => void 0);
5329
- if (error instanceof DatabaseError || error instanceof InitializationError) {
5330
- throw error;
5433
+ const maxAttempts = 5;
5434
+ let lastError;
5435
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
5436
+ const client = await this.requirePool().connect();
5437
+ try {
5438
+ await client.query("BEGIN");
5439
+ const result = await this.txStore.run(client, fn);
5440
+ await client.query("COMMIT");
5441
+ return result;
5442
+ } catch (error) {
5443
+ await client.query("ROLLBACK").catch(() => void 0);
5444
+ lastError = error;
5445
+ if (isRetriablePgTxError(error) && attempt < maxAttempts - 1) {
5446
+ await new Promise((r) => setTimeout(r, pgTxBackoffMs(attempt)));
5447
+ continue;
5448
+ }
5449
+ if (error instanceof DatabaseError || error instanceof InitializationError) {
5450
+ throw error;
5451
+ }
5452
+ throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
5453
+ cause: error instanceof Error ? error : void 0
5454
+ });
5455
+ } finally {
5456
+ client.release();
5331
5457
  }
5332
- throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
5333
- cause: error instanceof Error ? error : void 0
5334
- });
5335
- } finally {
5336
- client.release();
5337
5458
  }
5459
+ throw new DatabaseError(
5460
+ `Transaction failed after retries: ${this.describe(lastError)}`,
5461
+ { cause: lastError instanceof Error ? lastError : void 0 }
5462
+ );
5338
5463
  }
5339
5464
  async runMigrations() {
5340
5465
  await this.query(`
@@ -5562,13 +5687,13 @@ var PostgresStorageProvider = class _PostgresStorageProvider {
5562
5687
  await this.query(`DELETE FROM memory_embeddings_blob WHERE memory_id = $1`, [memoryId]).catch(() => void 0);
5563
5688
  }
5564
5689
  async query(text, params) {
5565
- const tx = txStore.getStore();
5690
+ const tx = this.txStore.getStore();
5566
5691
  const target = tx ?? this.requirePool();
5567
5692
  return target.query(text, params);
5568
5693
  }
5569
5694
  /** Named prepared statement — parse/plan cached per pool connection. */
5570
5695
  async queryNamed(name, text, params) {
5571
- const tx = txStore.getStore();
5696
+ const tx = this.txStore.getStore();
5572
5697
  const target = tx ?? this.requirePool();
5573
5698
  return target.query({ name, text, values: params });
5574
5699
  }
@@ -5654,7 +5779,7 @@ function createDatabaseProvider(config) {
5654
5779
  }
5655
5780
 
5656
5781
  // src/version.ts
5657
- var SDK_VERSION = "0.5.5";
5782
+ var SDK_VERSION = "0.5.6";
5658
5783
 
5659
5784
  // src/memory/transfer.ts
5660
5785
  var warnedMissingExportManifest = false;
@@ -9610,12 +9735,18 @@ var Wolbarg = class {
9610
9735
  createdAt: timestamp,
9611
9736
  updatedAt: timestamp
9612
9737
  });
9738
+ const sourceIds = [...records.map((r) => r.id)].sort();
9613
9739
  const archivedIds = await storage.archiveMemories(
9614
- records.map((r) => r.id),
9740
+ sourceIds,
9615
9741
  organization,
9616
9742
  summaryId,
9617
9743
  timestamp
9618
9744
  );
9745
+ if (archivedIds.length < 2) {
9746
+ throw new ValidationError(
9747
+ "compress lost a concurrency race: fewer than 2 source memories remained active"
9748
+ );
9749
+ }
9619
9750
  return {
9620
9751
  summary: toMemoryRecord(summaryRow),
9621
9752
  archivedIds
@@ -10627,8 +10758,11 @@ var meta = {
10627
10758
  };
10628
10759
 
10629
10760
  // src/factories/index.ts
10630
- function sqlite(connectionString) {
10631
- return new SqliteStorageProvider({ connectionString });
10761
+ function sqlite(connectionString, options) {
10762
+ return new SqliteStorageProvider({
10763
+ connectionString,
10764
+ concurrency: options?.concurrency
10765
+ });
10632
10766
  }
10633
10767
  function sqliteConfig(connectionString) {
10634
10768
  return {
@@ -11145,9 +11279,279 @@ function percentile(sorted, p) {
11145
11279
  return sorted[idx];
11146
11280
  }
11147
11281
 
11282
+ // src/config/providers.ts
11283
+ var EMBEDDING_PROVIDER_PRESETS = [
11284
+ {
11285
+ id: "openai",
11286
+ label: "OpenAI",
11287
+ defaultBaseUrl: "https://api.openai.com/v1",
11288
+ defaultModel: "text-embedding-3-small",
11289
+ apiKeyEnv: "OPENAI_API_KEY"
11290
+ },
11291
+ {
11292
+ id: "ollama",
11293
+ label: "Ollama (local)",
11294
+ defaultBaseUrl: "http://127.0.0.1:11434/v1",
11295
+ defaultModel: "nomic-embed-text",
11296
+ apiKeyEnv: "OLLAMA_API_KEY",
11297
+ apiKeyHint: "Any non-empty value works locally (e.g. ollama)"
11298
+ },
11299
+ {
11300
+ id: "openrouter",
11301
+ label: "OpenRouter",
11302
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
11303
+ defaultModel: "openai/text-embedding-3-small",
11304
+ apiKeyEnv: "OPENROUTER_API_KEY"
11305
+ },
11306
+ {
11307
+ id: "lmstudio",
11308
+ label: "LM Studio (local)",
11309
+ defaultBaseUrl: "http://127.0.0.1:1234/v1",
11310
+ defaultModel: "text-embedding-nomic-embed-text-v1.5",
11311
+ apiKeyEnv: "LM_STUDIO_API_KEY",
11312
+ apiKeyHint: "Often unused locally \u2014 any non-empty value is fine"
11313
+ },
11314
+ {
11315
+ id: "gemini",
11316
+ label: "Google Gemini",
11317
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
11318
+ defaultModel: "text-embedding-004",
11319
+ apiKeyEnv: "GEMINI_API_KEY"
11320
+ },
11321
+ {
11322
+ id: "together",
11323
+ label: "Together AI",
11324
+ defaultBaseUrl: "https://api.together.xyz/v1",
11325
+ defaultModel: "togethercomputer/m2-bert-80M-8k-retrieval",
11326
+ apiKeyEnv: "TOGETHER_API_KEY"
11327
+ },
11328
+ {
11329
+ id: "vllm",
11330
+ label: "vLLM (local/server)",
11331
+ defaultBaseUrl: "http://127.0.0.1:8000/v1",
11332
+ defaultModel: "text-embedding-ada-002",
11333
+ apiKeyEnv: "VLLM_API_KEY",
11334
+ apiKeyHint: "Only if your server requires auth"
11335
+ },
11336
+ {
11337
+ id: "custom",
11338
+ label: "Custom OpenAI-compatible",
11339
+ defaultBaseUrl: "https://api.openai.com/v1",
11340
+ defaultModel: "text-embedding-3-small",
11341
+ apiKeyEnv: "WOLBARG_EMBEDDING_API_KEY"
11342
+ }
11343
+ ];
11344
+ function getEmbeddingProviderPreset(id) {
11345
+ return EMBEDDING_PROVIDER_PRESETS.find((p) => p.id === id);
11346
+ }
11347
+ var DEFAULT_SQLITE_DB_PATH = ".wolbarg/shared-memory/memory.db";
11348
+ var DEFAULT_ORGANIZATION = "default";
11349
+ var DEFAULT_CONFIG_PATH = ".wolbarg/config.json";
11350
+ var DEFAULT_ENV_PATH = ".wolbarg/.env";
11351
+ function defaultProjectConfig() {
11352
+ return {
11353
+ version: 1,
11354
+ organization: DEFAULT_ORGANIZATION,
11355
+ database: {
11356
+ provider: "sqlite",
11357
+ url: DEFAULT_SQLITE_DB_PATH
11358
+ },
11359
+ paths: {
11360
+ config: DEFAULT_CONFIG_PATH
11361
+ }
11362
+ };
11363
+ }
11364
+ function resolveConfigPath(cwd = process.cwd(), configPath) {
11365
+ const rel = configPath ?? DEFAULT_CONFIG_PATH;
11366
+ return path7.isAbsolute(rel) ? rel : path7.resolve(cwd, rel);
11367
+ }
11368
+ function resolveEnvPath(cwd = process.cwd(), envPath) {
11369
+ const rel = envPath ?? DEFAULT_ENV_PATH;
11370
+ return path7.isAbsolute(rel) ? rel : path7.resolve(cwd, rel);
11371
+ }
11372
+ function loadProjectConfig(cwd = process.cwd(), configPath) {
11373
+ const full = resolveConfigPath(cwd, configPath);
11374
+ if (!fs6.existsSync(full)) return null;
11375
+ const raw = JSON.parse(fs6.readFileSync(full, "utf8"));
11376
+ if (!raw || raw.version !== 1 || !raw.database?.url) {
11377
+ throw new Error(`Invalid Wolbarg config at ${full}`);
11378
+ }
11379
+ return raw;
11380
+ }
11381
+ function saveProjectConfig(config, options = {}) {
11382
+ const cwd = options.cwd ?? process.cwd();
11383
+ const configPath = resolveConfigPath(cwd, options.configPath);
11384
+ const dir = path7.dirname(configPath);
11385
+ if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
11386
+ if (fs6.existsSync(configPath) && options.overwrite === false) {
11387
+ throw new Error(`Config already exists: ${configPath} (pass --force to overwrite)`);
11388
+ }
11389
+ const toWrite = {
11390
+ ...config,
11391
+ paths: {
11392
+ config: options.configPath ?? DEFAULT_CONFIG_PATH,
11393
+ ...options.envPath || options.apiKey ? { env: options.envPath ?? DEFAULT_ENV_PATH } : config.paths?.env ? { env: config.paths.env } : {}
11394
+ }
11395
+ };
11396
+ fs6.writeFileSync(configPath, `${JSON.stringify(toWrite, null, 2)}
11397
+ `, "utf8");
11398
+ let envPath;
11399
+ if (options.apiKey && config.embedding?.apiKeyEnv) {
11400
+ envPath = resolveEnvPath(cwd, options.envPath);
11401
+ ensureParent(envPath);
11402
+ upsertEnvVar(envPath, config.embedding.apiKeyEnv, options.apiKey);
11403
+ ensureGitignore(cwd, [DEFAULT_ENV_PATH, ".env"]);
11404
+ }
11405
+ if (config.database.provider === "sqlite" && !config.database.url.includes("://")) {
11406
+ const dbPath = path7.isAbsolute(config.database.url) ? config.database.url : path7.resolve(cwd, config.database.url);
11407
+ ensureParent(dbPath);
11408
+ }
11409
+ return { configPath, envPath };
11410
+ }
11411
+ function ensureParent(filePath) {
11412
+ const dir = path7.dirname(filePath);
11413
+ if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
11414
+ }
11415
+ function upsertEnvVar(envPath, key, value) {
11416
+ ensureParent(envPath);
11417
+ let text = fs6.existsSync(envPath) ? fs6.readFileSync(envPath, "utf8") : "";
11418
+ const line = `${key}=${shellQuoteEnv(value)}`;
11419
+ const re = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
11420
+ if (re.test(text)) {
11421
+ text = text.replace(re, line);
11422
+ } else {
11423
+ if (text && !text.endsWith("\n")) text += "\n";
11424
+ text += `${line}
11425
+ `;
11426
+ }
11427
+ fs6.writeFileSync(envPath, text, "utf8");
11428
+ }
11429
+ function shellQuoteEnv(value) {
11430
+ if (/[\s#"']/.test(value)) {
11431
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11432
+ }
11433
+ return value;
11434
+ }
11435
+ function escapeRegExp(s) {
11436
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11437
+ }
11438
+ function ensureGitignore(cwd, entries) {
11439
+ const gi = path7.join(cwd, ".gitignore");
11440
+ let text = fs6.existsSync(gi) ? fs6.readFileSync(gi, "utf8") : "";
11441
+ let changed = false;
11442
+ for (const entry of entries) {
11443
+ if (text.split(/\r?\n/).some((l) => l.trim() === entry)) continue;
11444
+ if (text && !text.endsWith("\n")) text += "\n";
11445
+ text += `${entry}
11446
+ `;
11447
+ changed = true;
11448
+ }
11449
+ if (changed) fs6.writeFileSync(gi, text, "utf8");
11450
+ }
11451
+ function resolveEmbeddingApiKey(config, env = process.env) {
11452
+ const name = config.embedding?.apiKeyEnv;
11453
+ if (!name) return "";
11454
+ return env[name] ?? "";
11455
+ }
11456
+ function assertEmbeddingPreset(provider) {
11457
+ const preset = getEmbeddingProviderPreset(provider);
11458
+ if (!preset) {
11459
+ throw new Error(
11460
+ `Unknown embedding provider "${provider}". Choose one of: ${[
11461
+ "openai",
11462
+ "ollama",
11463
+ "openrouter",
11464
+ "lmstudio",
11465
+ "gemini",
11466
+ "together",
11467
+ "vllm",
11468
+ "custom"
11469
+ ].join(", ")}`
11470
+ );
11471
+ }
11472
+ return preset.id;
11473
+ }
11474
+ function applyEnvFile(envPath, base = {}) {
11475
+ const out = { ...base };
11476
+ if (!fs6.existsSync(envPath)) return out;
11477
+ const text = fs6.readFileSync(envPath, "utf8");
11478
+ for (const rawLine of text.split(/\r?\n/)) {
11479
+ const line = rawLine.trim();
11480
+ if (!line || line.startsWith("#")) continue;
11481
+ const eq = line.indexOf("=");
11482
+ if (eq <= 0) continue;
11483
+ const key = line.slice(0, eq).trim();
11484
+ let value = line.slice(eq + 1).trim();
11485
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
11486
+ value = value.slice(1, -1);
11487
+ }
11488
+ if (out[key] === void 0) out[key] = value;
11489
+ }
11490
+ return out;
11491
+ }
11492
+ function projectConfigToWolbargOptions(config, env = process.env) {
11493
+ if (!config.embedding) {
11494
+ throw new Error(
11495
+ "Project config has no embedding section. Re-run `wolbarg init` and configure an embedding provider, or pass embedding explicitly to wolbarg()."
11496
+ );
11497
+ }
11498
+ const apiKey = resolveEmbeddingApiKey(config, env);
11499
+ if (!apiKey) {
11500
+ throw new Error(
11501
+ `Missing API key. Set ${config.embedding.apiKeyEnv} in the environment or in .wolbarg/.env (from wolbarg init).`
11502
+ );
11503
+ }
11504
+ const dbUrl = config.database.url;
11505
+ return {
11506
+ organization: config.organization,
11507
+ database: {
11508
+ provider: config.database.provider,
11509
+ url: dbUrl,
11510
+ connectionString: dbUrl
11511
+ },
11512
+ embedding: {
11513
+ baseUrl: config.embedding.baseUrl,
11514
+ apiKey,
11515
+ model: config.embedding.model
11516
+ }
11517
+ };
11518
+ }
11519
+ function createWolbargFromProjectConfig(options = {}) {
11520
+ const cwd = options.cwd ?? process.cwd();
11521
+ const config = loadProjectConfig(cwd, options.configPath);
11522
+ if (!config) {
11523
+ throw new Error(
11524
+ `No Wolbarg config found. Run \`wolbarg init\` in ${cwd} (expected .wolbarg/config.json).`
11525
+ );
11526
+ }
11527
+ let env = options.env ?? { ...process.env };
11528
+ if (options.loadEnvFile !== false) {
11529
+ const envFile = resolveEnvPath(
11530
+ cwd,
11531
+ config.paths?.env ?? DEFAULT_ENV_PATH
11532
+ );
11533
+ env = applyEnvFile(envFile, env);
11534
+ }
11535
+ if (options.organization) {
11536
+ config.organization = options.organization;
11537
+ }
11538
+ if (config.database.provider === "sqlite" && !config.database.url.includes("://") && !path7.isAbsolute(config.database.url)) {
11539
+ config.database = {
11540
+ ...config.database,
11541
+ url: path7.resolve(cwd, config.database.url)
11542
+ };
11543
+ }
11544
+ return wolbarg(projectConfigToWolbargOptions(config, env));
11545
+ }
11546
+
11148
11547
  exports.CompressionError = CompressionError;
11149
11548
  exports.ConfigurationError = ConfigurationError;
11549
+ exports.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH;
11550
+ exports.DEFAULT_ENV_PATH = DEFAULT_ENV_PATH;
11551
+ exports.DEFAULT_ORGANIZATION = DEFAULT_ORGANIZATION;
11552
+ exports.DEFAULT_SQLITE_DB_PATH = DEFAULT_SQLITE_DB_PATH;
11150
11553
  exports.DatabaseError = DatabaseError;
11554
+ exports.EMBEDDING_PROVIDER_PRESETS = EMBEDDING_PROVIDER_PRESETS;
11151
11555
  exports.EmbeddingError = EmbeddingError;
11152
11556
  exports.FixedChunkingStrategy = FixedChunkingStrategy;
11153
11557
  exports.GraphCheckpointNotSupportedError = GraphCheckpointNotSupportedError;
@@ -11175,6 +11579,8 @@ exports.ValidationError = ValidationError;
11175
11579
  exports.Wolbarg = Wolbarg;
11176
11580
  exports.WolbargError = WolbargError;
11177
11581
  exports.WolbargLogger = WolbargLogger;
11582
+ exports.applyEnvFile = applyEnvFile;
11583
+ exports.assertEmbeddingPreset = assertEmbeddingPreset;
11178
11584
  exports.bgeReranker = bgeReranker;
11179
11585
  exports.bm25 = bm25;
11180
11586
  exports.cohereReranker = cohereReranker;
@@ -11186,11 +11592,15 @@ exports.createLlmProvider = createLlmProvider;
11186
11592
  exports.createStorageProvider = createStorageProvider;
11187
11593
  exports.createTelemetryProvider = createTelemetryProvider;
11188
11594
  exports.createWolbarg = createWolbarg;
11595
+ exports.createWolbargFromProjectConfig = createWolbargFromProjectConfig;
11189
11596
  exports.crossEncoder = crossEncoder;
11597
+ exports.defaultProjectConfig = defaultProjectConfig;
11190
11598
  exports.geminiEmbedding = geminiEmbedding;
11191
11599
  exports.geminiVision = geminiVision;
11600
+ exports.getEmbeddingProviderPreset = getEmbeddingProviderPreset;
11192
11601
  exports.jinaReranker = jinaReranker;
11193
11602
  exports.lmStudioEmbedding = lmStudioEmbedding;
11603
+ exports.loadProjectConfig = loadProjectConfig;
11194
11604
  exports.meta = meta;
11195
11605
  exports.neo4jGraph = neo4jGraph;
11196
11606
  exports.ollamaEmbedding = ollamaEmbedding;
@@ -11205,7 +11615,12 @@ exports.openaiReranker = openaiReranker;
11205
11615
  exports.openaiVision = openaiVision;
11206
11616
  exports.postgres = postgres;
11207
11617
  exports.postgresConfig = postgresConfig;
11618
+ exports.projectConfigToWolbargOptions = projectConfigToWolbargOptions;
11619
+ exports.resolveConfigPath = resolveConfigPath;
11620
+ exports.resolveEmbeddingApiKey = resolveEmbeddingApiKey;
11621
+ exports.resolveEnvPath = resolveEnvPath;
11208
11622
  exports.runBenchmark = runBenchmark;
11623
+ exports.saveProjectConfig = saveProjectConfig;
11209
11624
  exports.sqlite = sqlite;
11210
11625
  exports.sqliteCheckpoint = sqliteCheckpoint;
11211
11626
  exports.sqliteConfig = sqliteConfig;
@@ -11214,6 +11629,7 @@ exports.sqliteTelemetry = sqliteTelemetry;
11214
11629
  exports.summarizeBenchmark = summarizeBenchmark;
11215
11630
  exports.tesseract = tesseract;
11216
11631
  exports.togetherEmbedding = togetherEmbedding;
11632
+ exports.upsertEnvVar = upsertEnvVar;
11217
11633
  exports.vllmEmbedding = vllmEmbedding;
11218
11634
  exports.wolbarg = wolbarg;
11219
11635
  exports.wrapOperationError = wrapOperationError;