wolbarg 0.4.0 → 0.5.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.
package/dist/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var path5 = require("node:path");
3
+ var fs6 = require("node:fs");
4
+ var path7 = require("node:path");
4
5
  var crypto$1 = require('crypto');
5
6
  var fs = require('fs/promises');
6
- var fs5 = require("node:fs");
7
7
  var sqlite$1 = require("node:sqlite");
8
8
  var sqliteVec = require('sqlite-vec');
9
9
  var async_hooks = require('async_hooks');
@@ -29,9 +29,9 @@ function _interopNamespace(e) {
29
29
  return Object.freeze(n);
30
30
  }
31
31
 
32
- var path5__default = /*#__PURE__*/_interopDefault(path5);
32
+ var fs6__default = /*#__PURE__*/_interopDefault(fs6);
33
+ var path7__default = /*#__PURE__*/_interopDefault(path7);
33
34
  var fs__default = /*#__PURE__*/_interopDefault(fs);
34
- var fs5__default = /*#__PURE__*/_interopDefault(fs5);
35
35
  var sqliteVec__namespace = /*#__PURE__*/_interopNamespace(sqliteVec);
36
36
 
37
37
  // src/core/wolbarg.ts
@@ -112,6 +112,20 @@ var ProviderNotConfiguredError = class extends ConfigurationError {
112
112
  this.provider = provider;
113
113
  }
114
114
  };
115
+ var GraphCheckpointNotSupportedError = class extends WolbargError {
116
+ constructor(backend, operation) {
117
+ super(
118
+ `graph checkpoint not supported for network-backed graph providers (${backend})`,
119
+ "GRAPH_CHECKPOINT_NOT_SUPPORTED",
120
+ {
121
+ operation,
122
+ reason: `${backend} is networked / not file-backed`,
123
+ suggestion: "Use sqliteGraph({ path }) for local snapshots, or checkpoint memory storage only without a network graph provider."
124
+ }
125
+ );
126
+ this.name = "GraphCheckpointNotSupportedError";
127
+ }
128
+ };
115
129
  function wrapOperationError(operation, error) {
116
130
  if (error instanceof WolbargError) {
117
131
  return error;
@@ -412,9 +426,9 @@ var AsyncMutex = class {
412
426
  }
413
427
  }
414
428
  };
415
- function joinUrl(baseUrl, path7) {
429
+ function joinUrl(baseUrl, path8) {
416
430
  const base = baseUrl.replace(/\/+$/, "");
417
- const suffix = path7.startsWith("/") ? path7 : `/${path7}`;
431
+ const suffix = path8.startsWith("/") ? path8 : `/${path8}`;
418
432
  return `${base}${suffix}`;
419
433
  }
420
434
 
@@ -1109,7 +1123,7 @@ function extOf(filename) {
1109
1123
  if (!filename) {
1110
1124
  return "";
1111
1125
  }
1112
- return path5__default.default.extname(filename).toLowerCase();
1126
+ return path7__default.default.extname(filename).toLowerCase();
1113
1127
  }
1114
1128
  var TextParser = class {
1115
1129
  name = "text";
@@ -1254,7 +1268,7 @@ async function loadIngestSource(source) {
1254
1268
  const buffer = await fs__default.default.readFile(source.path);
1255
1269
  return {
1256
1270
  buffer,
1257
- filename: path5__default.default.basename(source.path),
1271
+ filename: path7__default.default.basename(source.path),
1258
1272
  mimeType: source.mimeType
1259
1273
  };
1260
1274
  }
@@ -1274,6 +1288,9 @@ function isStorageProvider(value) {
1274
1288
  function isTelemetryProvider(value) {
1275
1289
  return typeof value.emit === "function";
1276
1290
  }
1291
+ function isGraphProvider(value) {
1292
+ return typeof value.open === "function" && typeof value.linkMemories === "function" && typeof value.getRelated === "function";
1293
+ }
1277
1294
  function resolveDatabaseUrl(config) {
1278
1295
  const url = "url" in config && config.url || "connectionString" in config && config.connectionString || "";
1279
1296
  return typeof url === "string" ? url : "";
@@ -1344,11 +1361,11 @@ function jsonPath(field) {
1344
1361
  return `$.${field}`;
1345
1362
  }
1346
1363
  function compileComparison(field, op) {
1347
- const path7 = jsonPath(field);
1348
- if (!path7) {
1364
+ const path8 = jsonPath(field);
1365
+ if (!path8) {
1349
1366
  return null;
1350
1367
  }
1351
- const extract = `json_extract(metadata_json, '${path7}')`;
1368
+ const extract = `json_extract(metadata_json, '${path8}')`;
1352
1369
  if ("eq" in op) {
1353
1370
  const value = op.eq;
1354
1371
  if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
@@ -1493,6 +1510,40 @@ CREATE TABLE IF NOT EXISTS embedding_cache (
1493
1510
  last_used_at TEXT NOT NULL
1494
1511
  );
1495
1512
  `;
1513
+ var CREATE_GRAPH_NODES_TABLE = `
1514
+ CREATE TABLE IF NOT EXISTS graph_nodes (
1515
+ id TEXT PRIMARY KEY NOT NULL,
1516
+ type TEXT NOT NULL CHECK (type IN ('memory', 'entity')),
1517
+ ref_id TEXT NOT NULL,
1518
+ name TEXT NULL,
1519
+ metadata TEXT NOT NULL DEFAULT '{}',
1520
+ created_at TEXT NOT NULL
1521
+ );
1522
+ `;
1523
+ var CREATE_GRAPH_EDGES_TABLE = `
1524
+ CREATE TABLE IF NOT EXISTS graph_edges (
1525
+ id TEXT PRIMARY KEY NOT NULL,
1526
+ from_node_id TEXT NOT NULL,
1527
+ to_node_id TEXT NOT NULL,
1528
+ relation TEXT NOT NULL,
1529
+ metadata TEXT NOT NULL DEFAULT '{}',
1530
+ created_at TEXT NOT NULL,
1531
+ FOREIGN KEY (from_node_id) REFERENCES graph_nodes(id) ON DELETE CASCADE,
1532
+ FOREIGN KEY (to_node_id) REFERENCES graph_nodes(id) ON DELETE CASCADE
1533
+ );
1534
+ `;
1535
+ var CREATE_GRAPH_INDEXES = [
1536
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_nodes_type_ref
1537
+ ON graph_nodes(type, ref_id);`,
1538
+ `CREATE INDEX IF NOT EXISTS idx_graph_edges_from
1539
+ ON graph_edges(from_node_id);`,
1540
+ `CREATE INDEX IF NOT EXISTS idx_graph_edges_to
1541
+ ON graph_edges(to_node_id);`,
1542
+ `CREATE INDEX IF NOT EXISTS idx_graph_edges_from_rel
1543
+ ON graph_edges(from_node_id, relation);`,
1544
+ `CREATE INDEX IF NOT EXISTS idx_graph_edges_to_rel
1545
+ ON graph_edges(to_node_id, relation);`
1546
+ ];
1496
1547
  var CREATE_INDEXES = [
1497
1548
  `CREATE INDEX IF NOT EXISTS idx_memories_org_agent ON memories(organization, agent);`,
1498
1549
  `CREATE INDEX IF NOT EXISTS idx_memories_org_archived ON memories(organization, archived);`,
@@ -2093,7 +2144,7 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2093
2144
  const dbPath = this.resolvePath(this.connectionString);
2094
2145
  this.resolvedPath = dbPath;
2095
2146
  if (dbPath !== ":memory:") {
2096
- fs5__default.default.mkdirSync(path5__default.default.dirname(dbPath), { recursive: true });
2147
+ fs6__default.default.mkdirSync(path7__default.default.dirname(dbPath), { recursive: true });
2097
2148
  }
2098
2149
  const db = new sqlite$1.DatabaseSync(dbPath, { allowExtension: true });
2099
2150
  this.db = db;
@@ -2581,11 +2632,11 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
2581
2632
  }
2582
2633
  const dbPath = this.resolvedPath ?? this.resolvePath(this.connectionString);
2583
2634
  try {
2584
- let total = fs5__default.default.statSync(dbPath).size;
2635
+ let total = fs6__default.default.statSync(dbPath).size;
2585
2636
  for (const suffix of ["-wal", "-shm"]) {
2586
2637
  const side = `${dbPath}${suffix}`;
2587
- if (fs5__default.default.existsSync(side)) {
2588
- total += fs5__default.default.statSync(side).size;
2638
+ if (fs6__default.default.existsSync(side)) {
2639
+ total += fs6__default.default.statSync(side).size;
2589
2640
  }
2590
2641
  }
2591
2642
  return total;
@@ -3361,7 +3412,7 @@ var SqliteStorageProvider = class _SqliteStorageProvider {
3361
3412
  if (connectionString === ":memory:") {
3362
3413
  return ":memory:";
3363
3414
  }
3364
- return path5__default.default.isAbsolute(connectionString) ? connectionString : path5__default.default.resolve(process.cwd(), connectionString);
3415
+ return path7__default.default.isAbsolute(connectionString) ? connectionString : path7__default.default.resolve(process.cwd(), connectionString);
3365
3416
  }
3366
3417
  requireDb() {
3367
3418
  if (!this.db) {
@@ -3412,8 +3463,8 @@ function jsonbTextExtract(field) {
3412
3463
  if (parts.length === 1) {
3413
3464
  return `metadata_json->>'${parts[0]}'`;
3414
3465
  }
3415
- const path7 = parts.join(",");
3416
- return `metadata_json #>> '{${path7}}'`;
3466
+ const path8 = parts.join(",");
3467
+ return `metadata_json #>> '{${path8}}'`;
3417
3468
  }
3418
3469
  function jsonbContainment(field, value, paramIndex) {
3419
3470
  if (!FIELD_RE2.test(field)) {
@@ -5112,7 +5163,11 @@ function createStorageProvider(config, options) {
5112
5163
  function createDatabaseProvider(config) {
5113
5164
  return createStorageProvider(config);
5114
5165
  }
5115
- var SDK_VERSION = "0.3.0";
5166
+
5167
+ // src/version.ts
5168
+ var SDK_VERSION = "0.5.0";
5169
+
5170
+ // src/memory/transfer.ts
5116
5171
  var SqliteMemoryTransferProvider = class {
5117
5172
  async exportTo(exportPath, sourcePath, organization) {
5118
5173
  const resolvedSource = resolvePath(sourcePath);
@@ -5121,7 +5176,7 @@ var SqliteMemoryTransferProvider = class {
5121
5176
  "Cannot export an in-memory database. Use a file-backed SQLite database."
5122
5177
  );
5123
5178
  }
5124
- if (!fs5__default.default.existsSync(resolvedSource)) {
5179
+ if (!fs6__default.default.existsSync(resolvedSource)) {
5125
5180
  throw new DatabaseError(
5126
5181
  `Failed to execute export()
5127
5182
  Reason:
@@ -5131,7 +5186,7 @@ Initialize Wolbarg and verify the database path.`
5131
5186
  );
5132
5187
  }
5133
5188
  const resolvedExport = resolvePath(exportPath);
5134
- fs5__default.default.mkdirSync(path5__default.default.dirname(resolvedExport), { recursive: true });
5189
+ fs6__default.default.mkdirSync(path7__default.default.dirname(resolvedExport), { recursive: true });
5135
5190
  const dbExportPath = resolvedExport.endsWith(".db") ? resolvedExport : `${resolvedExport}.db`;
5136
5191
  const manifestPath = `${dbExportPath}.manifest.json`;
5137
5192
  await copySqlite(resolvedSource, dbExportPath);
@@ -5143,17 +5198,17 @@ Initialize Wolbarg and verify the database path.`
5143
5198
  sourcePath: resolvedSource,
5144
5199
  organization
5145
5200
  };
5146
- fs5__default.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
5201
+ fs6__default.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
5147
5202
  return {
5148
5203
  path: dbExportPath,
5149
5204
  manifest,
5150
- sizeBytes: fs5__default.default.statSync(dbExportPath).size
5205
+ sizeBytes: fs6__default.default.statSync(dbExportPath).size
5151
5206
  };
5152
5207
  }
5153
5208
  async importFrom(exportPath, targetPath) {
5154
5209
  const resolvedExport = resolvePath(exportPath);
5155
- const dbExportPath = resolvedExport.endsWith(".db") ? resolvedExport : fs5__default.default.existsSync(`${resolvedExport}.db`) ? `${resolvedExport}.db` : resolvedExport;
5156
- if (!fs5__default.default.existsSync(dbExportPath)) {
5210
+ const dbExportPath = resolvedExport.endsWith(".db") ? resolvedExport : fs6__default.default.existsSync(`${resolvedExport}.db`) ? `${resolvedExport}.db` : resolvedExport;
5211
+ if (!fs6__default.default.existsSync(dbExportPath)) {
5157
5212
  throw new ValidationError(
5158
5213
  `Failed to execute import()
5159
5214
  Reason:
@@ -5164,8 +5219,8 @@ Pass the path returned by export().`
5164
5219
  }
5165
5220
  const manifestPath = `${dbExportPath}.manifest.json`;
5166
5221
  let manifest;
5167
- if (fs5__default.default.existsSync(manifestPath)) {
5168
- manifest = JSON.parse(fs5__default.default.readFileSync(manifestPath, "utf8"));
5222
+ if (fs6__default.default.existsSync(manifestPath)) {
5223
+ manifest = JSON.parse(fs6__default.default.readFileSync(manifestPath, "utf8"));
5169
5224
  if (manifest.format !== "wolbarg-export-v1") {
5170
5225
  throw new ValidationError(
5171
5226
  `Unsupported export format: ${String(manifest.format)}`
@@ -5186,8 +5241,8 @@ Pass the path returned by export().`
5186
5241
  }
5187
5242
  for (const suffix of ["", "-wal", "-shm"]) {
5188
5243
  const side = `${resolvedTarget}${suffix}`;
5189
- if (fs5__default.default.existsSync(side)) {
5190
- fs5__default.default.rmSync(side, { force: true });
5244
+ if (fs6__default.default.existsSync(side)) {
5245
+ fs6__default.default.rmSync(side, { force: true });
5191
5246
  }
5192
5247
  }
5193
5248
  await copySqlite(dbExportPath, resolvedTarget);
@@ -5195,7 +5250,7 @@ Pass the path returned by export().`
5195
5250
  }
5196
5251
  };
5197
5252
  async function copySqlite(sourcePath, destPath) {
5198
- fs5__default.default.mkdirSync(path5__default.default.dirname(destPath), { recursive: true });
5253
+ fs6__default.default.mkdirSync(path7__default.default.dirname(destPath), { recursive: true });
5199
5254
  let source = null;
5200
5255
  let dest = null;
5201
5256
  try {
@@ -5213,7 +5268,7 @@ async function copySqlite(sourcePath, destPath) {
5213
5268
  source = null;
5214
5269
  dest.close();
5215
5270
  dest = null;
5216
- fs5__default.default.copyFileSync(sourcePath, destPath);
5271
+ fs6__default.default.copyFileSync(sourcePath, destPath);
5217
5272
  }
5218
5273
  } catch (error) {
5219
5274
  throw new DatabaseError(
@@ -5233,7 +5288,7 @@ async function copySqlite(sourcePath, destPath) {
5233
5288
  }
5234
5289
  function resolvePath(p) {
5235
5290
  if (p === ":memory:") return ":memory:";
5236
- return path5__default.default.isAbsolute(p) ? p : path5__default.default.resolve(process.cwd(), p);
5291
+ return path7__default.default.isAbsolute(p) ? p : path7__default.default.resolve(process.cwd(), p);
5237
5292
  }
5238
5293
 
5239
5294
  // src/memory/index.ts
@@ -5360,6 +5415,723 @@ function adaptiveFetchK(topK, overFetchFactor, hasFilters) {
5360
5415
  return Math.min(Math.max(Math.ceil(topK * factor), topK), 1e3);
5361
5416
  }
5362
5417
 
5418
+ // src/graph/memory-node.ts
5419
+ function stubMemoryRecord(id) {
5420
+ return {
5421
+ id,
5422
+ organization: "",
5423
+ agent: "",
5424
+ content: { text: "" },
5425
+ metadata: {},
5426
+ archived: false,
5427
+ compressedInto: null,
5428
+ createdAt: /* @__PURE__ */ new Date(0),
5429
+ updatedAt: /* @__PURE__ */ new Date(0)
5430
+ };
5431
+ }
5432
+ function serializeMetadata2(meta2) {
5433
+ return JSON.stringify(meta2 ?? {});
5434
+ }
5435
+ function deserializeMetadata2(raw) {
5436
+ if (raw == null || raw === "") return {};
5437
+ if (typeof raw === "object" && !Array.isArray(raw)) {
5438
+ return raw;
5439
+ }
5440
+ if (typeof raw !== "string") return {};
5441
+ try {
5442
+ const parsed = JSON.parse(raw);
5443
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5444
+ return parsed;
5445
+ }
5446
+ } catch {
5447
+ }
5448
+ return {};
5449
+ }
5450
+ function rowToMemoryRecord(row) {
5451
+ const id = String(row.id ?? row.n_id ?? "");
5452
+ const createdAt = parseDate(row.created_at ?? row.createdAt);
5453
+ const updatedAt = parseDate(row.updated_at ?? row.updatedAt);
5454
+ return {
5455
+ id,
5456
+ organization: String(row.organization ?? ""),
5457
+ agent: String(row.agent ?? ""),
5458
+ content: { text: String(row.content_text ?? row.contentText ?? "") },
5459
+ metadata: deserializeMetadata2(row.metadata_json ?? row.metadata),
5460
+ archived: Boolean(row.archived),
5461
+ compressedInto: row.compressed_into == null || row.compressed_into === "" ? null : String(row.compressed_into),
5462
+ createdAt,
5463
+ updatedAt
5464
+ };
5465
+ }
5466
+ function parseDate(value) {
5467
+ if (value instanceof Date) return value;
5468
+ if (typeof value === "string" || typeof value === "number") {
5469
+ const d = new Date(value);
5470
+ if (!Number.isNaN(d.getTime())) return d;
5471
+ }
5472
+ return /* @__PURE__ */ new Date(0);
5473
+ }
5474
+ function entityIdFrom(name, type) {
5475
+ const key = `${type.trim().toLowerCase()}::${name.trim().toLowerCase()}`;
5476
+ let h = 2166136261;
5477
+ for (let i = 0; i < key.length; i += 1) {
5478
+ h ^= key.charCodeAt(i);
5479
+ h = Math.imul(h, 16777619);
5480
+ }
5481
+ const a = (h >>> 0).toString(16).padStart(8, "0");
5482
+ let h2 = 5381;
5483
+ for (let i = 0; i < key.length; i += 1) {
5484
+ h2 = h2 * 33 ^ key.charCodeAt(i);
5485
+ }
5486
+ const b = (h2 >>> 0).toString(16).padStart(8, "0");
5487
+ return `ent_${a}${b}`;
5488
+ }
5489
+
5490
+ // src/graph/sync/cascade.ts
5491
+ var ENTITY_MENTIONS_RELATION = "MENTIONS";
5492
+ function cascadeDeleteMemoryNode(db, memoryId) {
5493
+ const node = db.prepare(
5494
+ `SELECT id FROM graph_nodes WHERE type = 'memory' AND ref_id = ?`
5495
+ ).get(memoryId);
5496
+ if (!node) return;
5497
+ db.prepare(
5498
+ `DELETE FROM graph_edges WHERE from_node_id = ? OR to_node_id = ?`
5499
+ ).run(node.id, node.id);
5500
+ db.prepare(`DELETE FROM graph_nodes WHERE id = ?`).run(node.id);
5501
+ }
5502
+
5503
+ // src/graph/providers/sqlite-graph.ts
5504
+ var DEFAULT_GET_RELATED_DEPTH = 1;
5505
+ var MAX_GET_RELATED_DEPTH = 16;
5506
+ var SqliteGraphProvider = class {
5507
+ name = "sqlite";
5508
+ dbPath;
5509
+ concurrency;
5510
+ db = null;
5511
+ opened = false;
5512
+ constructor(options) {
5513
+ if (!options?.path || typeof options.path !== "string" || !options.path.trim()) {
5514
+ throw new ConfigurationError("sqlite graph requires a non-empty path");
5515
+ }
5516
+ this.dbPath = path7__default.default.resolve(options.path.trim());
5517
+ this.concurrency = resolveConcurrencyConfig(options.concurrency);
5518
+ }
5519
+ supportsFileSnapshot() {
5520
+ return this.dbPath !== ":memory:";
5521
+ }
5522
+ getDataPath() {
5523
+ return this.dbPath === ":memory:" ? null : this.dbPath;
5524
+ }
5525
+ async open() {
5526
+ if (this.opened) return;
5527
+ try {
5528
+ if (this.dbPath !== ":memory:") {
5529
+ fs6__default.default.mkdirSync(path7__default.default.dirname(this.dbPath), { recursive: true });
5530
+ }
5531
+ const db = new sqlite$1.DatabaseSync(this.dbPath);
5532
+ db.exec("PRAGMA journal_mode = WAL;");
5533
+ db.exec(`
5534
+ PRAGMA synchronous = NORMAL;
5535
+ PRAGMA foreign_keys = ON;
5536
+ PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
5537
+ PRAGMA temp_store = MEMORY;
5538
+ PRAGMA cache_size = -8192;
5539
+ PRAGMA wal_autocheckpoint = 1000;
5540
+ `);
5541
+ db.exec(CREATE_GRAPH_NODES_TABLE);
5542
+ db.exec(CREATE_GRAPH_EDGES_TABLE);
5543
+ for (const indexSql of CREATE_GRAPH_INDEXES) {
5544
+ db.exec(indexSql);
5545
+ }
5546
+ this.db = db;
5547
+ this.opened = true;
5548
+ } catch (error) {
5549
+ try {
5550
+ this.db?.close();
5551
+ } catch {
5552
+ }
5553
+ this.db = null;
5554
+ this.opened = false;
5555
+ throw new DatabaseError(
5556
+ `Failed to open SQLite graph database: ${describe(error)}`,
5557
+ { cause: error instanceof Error ? error : void 0 }
5558
+ );
5559
+ }
5560
+ }
5561
+ async close() {
5562
+ if (!this.db) {
5563
+ this.opened = false;
5564
+ return;
5565
+ }
5566
+ try {
5567
+ try {
5568
+ this.db.exec("PRAGMA optimize;");
5569
+ } catch {
5570
+ }
5571
+ this.db.close();
5572
+ } catch (error) {
5573
+ throw new DatabaseError(
5574
+ `Failed to close SQLite graph database: ${describe(error)}`,
5575
+ { cause: error instanceof Error ? error : void 0 }
5576
+ );
5577
+ } finally {
5578
+ this.db = null;
5579
+ this.opened = false;
5580
+ }
5581
+ }
5582
+ async linkMemories(fromId, toId, relation, metadata) {
5583
+ const db = this.requireDb();
5584
+ await this.withTx(() => {
5585
+ const fromNode = this.ensureMemoryNodeSync(db, fromId);
5586
+ const toNode = this.ensureMemoryNodeSync(db, toId);
5587
+ db.prepare(
5588
+ `DELETE FROM graph_edges
5589
+ WHERE from_node_id = ? AND to_node_id = ? AND relation = ?`
5590
+ ).run(fromNode, toNode, relation);
5591
+ db.prepare(
5592
+ `INSERT INTO graph_edges (id, from_node_id, to_node_id, relation, metadata, created_at)
5593
+ VALUES (?, ?, ?, ?, ?, ?)`
5594
+ ).run(
5595
+ crypto.randomUUID(),
5596
+ fromNode,
5597
+ toNode,
5598
+ relation,
5599
+ serializeMetadata2(metadata),
5600
+ nowIso2()
5601
+ );
5602
+ });
5603
+ }
5604
+ async unlinkMemories(fromId, toId, relation) {
5605
+ const db = this.requireDb();
5606
+ await this.withTx(() => {
5607
+ const fromNode = this.findMemoryNodeId(db, fromId);
5608
+ const toNode = this.findMemoryNodeId(db, toId);
5609
+ if (!fromNode || !toNode) return;
5610
+ if (relation !== void 0) {
5611
+ db.prepare(
5612
+ `DELETE FROM graph_edges
5613
+ WHERE from_node_id = ? AND to_node_id = ? AND relation = ?`
5614
+ ).run(fromNode, toNode, relation);
5615
+ return;
5616
+ }
5617
+ db.prepare(
5618
+ `DELETE FROM graph_edges WHERE from_node_id = ? AND to_node_id = ?`
5619
+ ).run(fromNode, toNode);
5620
+ });
5621
+ }
5622
+ /**
5623
+ * Bounded recursive CTE over `graph_edges`.
5624
+ *
5625
+ * Default depth: {@link DEFAULT_GET_RELATED_DEPTH} (1). Cap: 16.
5626
+ * Only memory↔memory edges are walked (entity MENTIONS edges are excluded).
5627
+ * Cycle-safe via path membership (`instr`) so A→B→C→A terminates.
5628
+ *
5629
+ * Returns stub {@link MemoryRecord}s (id + empty fields). The facade
5630
+ * re-hydrates full rows via `toMemoryRecord` from storage when possible.
5631
+ */
5632
+ async getRelated(memoryId, options) {
5633
+ const db = this.requireDb();
5634
+ const depth = Math.max(
5635
+ 1,
5636
+ Math.min(options?.depth ?? DEFAULT_GET_RELATED_DEPTH, MAX_GET_RELATED_DEPTH)
5637
+ );
5638
+ const direction = options?.direction ?? "both";
5639
+ const relation = options?.relation ?? null;
5640
+ const start = this.findMemoryNodeId(db, memoryId);
5641
+ if (!start) return [];
5642
+ const sql = buildGetRelatedSql(direction);
5643
+ const rows = db.prepare(sql).all(start, start, depth, relation, relation);
5644
+ const seen = /* @__PURE__ */ new Set();
5645
+ const out = [];
5646
+ for (const row of rows) {
5647
+ if (row.ref_id === memoryId || seen.has(row.ref_id)) continue;
5648
+ seen.add(row.ref_id);
5649
+ out.push(stubMemoryRecord(row.ref_id));
5650
+ }
5651
+ return out;
5652
+ }
5653
+ async upsertEntity(entity) {
5654
+ const id = entityIdFrom(entity.name, entity.type);
5655
+ const db = this.requireDb();
5656
+ const meta2 = serializeMetadata2({
5657
+ ...entity.metadata ?? {},
5658
+ entityType: entity.type
5659
+ });
5660
+ await this.withTx(() => {
5661
+ const existing = db.prepare(
5662
+ `SELECT id FROM graph_nodes WHERE type = 'entity' AND ref_id = ?`
5663
+ ).get(id);
5664
+ if (existing) {
5665
+ db.prepare(
5666
+ `UPDATE graph_nodes SET name = ?, metadata = ? WHERE id = ?`
5667
+ ).run(entity.name, meta2, existing.id);
5668
+ } else {
5669
+ db.prepare(
5670
+ `INSERT INTO graph_nodes (id, type, ref_id, name, metadata, created_at)
5671
+ VALUES (?, 'entity', ?, ?, ?, ?)`
5672
+ ).run(id, id, entity.name, meta2, nowIso2());
5673
+ }
5674
+ });
5675
+ return id;
5676
+ }
5677
+ async linkEntityToMemory(entityId, memoryId, role) {
5678
+ const db = this.requireDb();
5679
+ await this.withTx(() => {
5680
+ const entity = db.prepare(
5681
+ `SELECT id FROM graph_nodes WHERE type = 'entity' AND ref_id = ?`
5682
+ ).get(entityId);
5683
+ if (!entity) {
5684
+ throw new DatabaseError(`Entity not found: ${entityId}`, {
5685
+ operation: "linkEntityToMemory"
5686
+ });
5687
+ }
5688
+ const memoryNode = this.ensureMemoryNodeSync(db, memoryId);
5689
+ db.prepare(
5690
+ `DELETE FROM graph_edges
5691
+ WHERE from_node_id = ? AND to_node_id = ? AND relation = ?`
5692
+ ).run(entity.id, memoryNode, ENTITY_MENTIONS_RELATION);
5693
+ db.prepare(
5694
+ `INSERT INTO graph_edges (id, from_node_id, to_node_id, relation, metadata, created_at)
5695
+ VALUES (?, ?, ?, ?, ?, ?)`
5696
+ ).run(
5697
+ crypto.randomUUID(),
5698
+ entity.id,
5699
+ memoryNode,
5700
+ ENTITY_MENTIONS_RELATION,
5701
+ serializeMetadata2({ role: role ?? "" }),
5702
+ nowIso2()
5703
+ );
5704
+ });
5705
+ }
5706
+ async deleteMemory(memoryId) {
5707
+ const db = this.requireDb();
5708
+ await this.withTx(() => {
5709
+ cascadeDeleteMemoryNode(db, memoryId);
5710
+ });
5711
+ }
5712
+ /**
5713
+ * Raw Cypher escape hatch — **not supported** on the SQLite graph provider.
5714
+ * Use the typed methods, or open the underlying SQLite file yourself for raw SQL.
5715
+ */
5716
+ async query(_cypher, _params) {
5717
+ throw new DatabaseError(
5718
+ "raw Cypher queries are not supported by the SQLite graph provider \u2014 use the typed methods, or query the underlying SQLite tables directly via your own connection if you need raw access",
5719
+ {
5720
+ operation: "query",
5721
+ suggestion: "Use linkMemories / getRelated / upsertEntity / linkEntityToMemory, or open the graph .db file with node:sqlite for ad-hoc SQL"
5722
+ }
5723
+ );
5724
+ }
5725
+ async health() {
5726
+ try {
5727
+ if (!this.opened || !this.db) {
5728
+ return { ok: false, backend: "sqlite", details: { reason: "not open" } };
5729
+ }
5730
+ const nodeCount = Number(
5731
+ this.db.prepare(`SELECT COUNT(*) AS c FROM graph_nodes`).get().c
5732
+ );
5733
+ const edgeCount = Number(
5734
+ this.db.prepare(`SELECT COUNT(*) AS c FROM graph_edges`).get().c
5735
+ );
5736
+ return {
5737
+ ok: true,
5738
+ backend: "sqlite",
5739
+ details: {
5740
+ path: this.dbPath,
5741
+ embedded: true,
5742
+ nodeCount,
5743
+ edgeCount,
5744
+ exists: this.dbPath === ":memory:" || fs6__default.default.existsSync(this.dbPath)
5745
+ }
5746
+ };
5747
+ } catch (error) {
5748
+ return {
5749
+ ok: false,
5750
+ backend: "sqlite",
5751
+ details: {
5752
+ path: this.dbPath,
5753
+ error: error instanceof Error ? error.message : String(error)
5754
+ }
5755
+ };
5756
+ }
5757
+ }
5758
+ async withTx(fn) {
5759
+ const db = this.requireDb();
5760
+ return withImmediateTransaction(db, this.concurrency, fn);
5761
+ }
5762
+ requireDb() {
5763
+ if (!this.db || !this.opened) {
5764
+ throw new DatabaseError("SQLite graph is not open", {
5765
+ operation: "graph",
5766
+ suggestion: "Call ready() / open() before graph operations"
5767
+ });
5768
+ }
5769
+ return this.db;
5770
+ }
5771
+ findMemoryNodeId(db, memoryId) {
5772
+ const row = db.prepare(
5773
+ `SELECT id FROM graph_nodes WHERE type = 'memory' AND ref_id = ?`
5774
+ ).get(memoryId);
5775
+ return row?.id ?? null;
5776
+ }
5777
+ ensureMemoryNodeSync(db, memoryId) {
5778
+ const existing = this.findMemoryNodeId(db, memoryId);
5779
+ if (existing) return existing;
5780
+ db.prepare(
5781
+ `INSERT INTO graph_nodes (id, type, ref_id, name, metadata, created_at)
5782
+ VALUES (?, 'memory', ?, NULL, '{}', ?)`
5783
+ ).run(memoryId, memoryId, nowIso2());
5784
+ return memoryId;
5785
+ }
5786
+ };
5787
+ function buildGetRelatedSql(direction) {
5788
+ const mentions = ENTITY_MENTIONS_RELATION;
5789
+ if (direction === "out") {
5790
+ return `
5791
+ WITH RECURSIVE walk(node_id, depth, path) AS (
5792
+ SELECT ? AS node_id, 0, '/' || ? || '/'
5793
+ UNION ALL
5794
+ SELECT e.to_node_id, w.depth + 1, w.path || e.to_node_id || '/'
5795
+ FROM walk w
5796
+ JOIN graph_edges e ON e.from_node_id = w.node_id
5797
+ JOIN graph_nodes dest ON dest.id = e.to_node_id AND dest.type = 'memory'
5798
+ WHERE w.depth < ?
5799
+ AND instr(w.path, '/' || e.to_node_id || '/') = 0
5800
+ AND (? IS NULL OR e.relation = ?)
5801
+ AND e.relation != '${mentions}'
5802
+ )
5803
+ SELECT DISTINCT n.ref_id AS ref_id
5804
+ FROM walk w
5805
+ JOIN graph_nodes n ON n.id = w.node_id
5806
+ WHERE w.depth > 0 AND n.type = 'memory'
5807
+ `;
5808
+ }
5809
+ if (direction === "in") {
5810
+ return `
5811
+ WITH RECURSIVE walk(node_id, depth, path) AS (
5812
+ SELECT ? AS node_id, 0, '/' || ? || '/'
5813
+ UNION ALL
5814
+ SELECT e.from_node_id, w.depth + 1, w.path || e.from_node_id || '/'
5815
+ FROM walk w
5816
+ JOIN graph_edges e ON e.to_node_id = w.node_id
5817
+ JOIN graph_nodes src ON src.id = e.from_node_id AND src.type = 'memory'
5818
+ WHERE w.depth < ?
5819
+ AND instr(w.path, '/' || e.from_node_id || '/') = 0
5820
+ AND (? IS NULL OR e.relation = ?)
5821
+ AND e.relation != '${mentions}'
5822
+ )
5823
+ SELECT DISTINCT n.ref_id AS ref_id
5824
+ FROM walk w
5825
+ JOIN graph_nodes n ON n.id = w.node_id
5826
+ WHERE w.depth > 0 AND n.type = 'memory'
5827
+ `;
5828
+ }
5829
+ return `
5830
+ WITH RECURSIVE walk(node_id, depth, path) AS (
5831
+ SELECT ? AS node_id, 0, '/' || ? || '/'
5832
+ UNION ALL
5833
+ SELECT neighbor.id, w.depth + 1, w.path || neighbor.id || '/'
5834
+ FROM walk w
5835
+ JOIN graph_edges e
5836
+ ON e.from_node_id = w.node_id OR e.to_node_id = w.node_id
5837
+ JOIN graph_nodes neighbor
5838
+ ON neighbor.id = CASE
5839
+ WHEN e.from_node_id = w.node_id THEN e.to_node_id
5840
+ ELSE e.from_node_id
5841
+ END
5842
+ AND neighbor.type = 'memory'
5843
+ WHERE w.depth < ?
5844
+ AND instr(w.path, '/' || neighbor.id || '/') = 0
5845
+ AND (? IS NULL OR e.relation = ?)
5846
+ AND e.relation != '${mentions}'
5847
+ )
5848
+ SELECT DISTINCT n.ref_id AS ref_id
5849
+ FROM walk w
5850
+ JOIN graph_nodes n ON n.id = w.node_id
5851
+ WHERE w.depth > 0 AND n.type = 'memory'
5852
+ `;
5853
+ }
5854
+ function nowIso2() {
5855
+ return (/* @__PURE__ */ new Date()).toISOString();
5856
+ }
5857
+ function describe(error) {
5858
+ return error instanceof Error ? error.message : String(error);
5859
+ }
5860
+
5861
+ // src/graph/providers/neo4j.ts
5862
+ var Neo4jGraphProvider = class {
5863
+ name = "neo4j";
5864
+ url;
5865
+ username;
5866
+ password;
5867
+ database;
5868
+ driver = null;
5869
+ opened = false;
5870
+ constructor(options) {
5871
+ if (!options?.url?.trim()) {
5872
+ throw new ConfigurationError("neo4j graph requires a non-empty url");
5873
+ }
5874
+ if (!options?.username?.trim()) {
5875
+ throw new ConfigurationError("neo4j graph requires a non-empty username");
5876
+ }
5877
+ if (typeof options.password !== "string") {
5878
+ throw new ConfigurationError("neo4j graph requires a password string");
5879
+ }
5880
+ this.url = options.url.trim();
5881
+ this.username = options.username.trim();
5882
+ this.password = options.password;
5883
+ this.database = options.database?.trim() || void 0;
5884
+ }
5885
+ supportsFileSnapshot() {
5886
+ return false;
5887
+ }
5888
+ getDataPath() {
5889
+ return null;
5890
+ }
5891
+ async open() {
5892
+ if (this.opened) return;
5893
+ let mod;
5894
+ try {
5895
+ mod = await import('neo4j-driver');
5896
+ } catch {
5897
+ throw new ConfigurationError(
5898
+ 'Neo4j graph requires the optional "neo4j-driver" package. Install it with: npm install neo4j-driver'
5899
+ );
5900
+ }
5901
+ this.driver = mod.driver(
5902
+ this.url,
5903
+ mod.auth.basic(this.username, this.password)
5904
+ );
5905
+ await this.driver.verifyConnectivity();
5906
+ this.opened = true;
5907
+ await this.withSession(async (session) => {
5908
+ try {
5909
+ await session.run(
5910
+ `CREATE CONSTRAINT memory_id IF NOT EXISTS FOR (m:Memory) REQUIRE m.id IS UNIQUE`
5911
+ );
5912
+ } catch {
5913
+ }
5914
+ try {
5915
+ await session.run(
5916
+ `CREATE CONSTRAINT entity_id IF NOT EXISTS FOR (e:Entity) REQUIRE e.id IS UNIQUE`
5917
+ );
5918
+ } catch {
5919
+ }
5920
+ });
5921
+ }
5922
+ async close() {
5923
+ if (this.driver) {
5924
+ await this.driver.close();
5925
+ }
5926
+ this.driver = null;
5927
+ this.opened = false;
5928
+ }
5929
+ requireDriver() {
5930
+ if (!this.driver || !this.opened) {
5931
+ throw new DatabaseError("Neo4j graph is not open", {
5932
+ operation: "graph",
5933
+ suggestion: "Call ready() / open() before graph operations"
5934
+ });
5935
+ }
5936
+ return this.driver;
5937
+ }
5938
+ async withSession(fn) {
5939
+ const driver = this.requireDriver();
5940
+ const session = this.database ? driver.session({ database: this.database }) : driver.session();
5941
+ try {
5942
+ return await fn(session);
5943
+ } finally {
5944
+ await session.close();
5945
+ }
5946
+ }
5947
+ async run(cypher, params = {}) {
5948
+ return this.withSession(async (session) => {
5949
+ const result = await session.run(cypher, params);
5950
+ return result.records.map((rec) => {
5951
+ try {
5952
+ return rec.toObject();
5953
+ } catch {
5954
+ const obj = {};
5955
+ for (const key of rec.keys) {
5956
+ obj[key] = unwrapNeo4jValue(rec.get(key));
5957
+ }
5958
+ return obj;
5959
+ }
5960
+ });
5961
+ });
5962
+ }
5963
+ async ensureMemoryNode(id) {
5964
+ await this.run(
5965
+ `MERGE (m:Memory { id: $id })
5966
+ ON CREATE SET
5967
+ m.organization = '',
5968
+ m.agent = '',
5969
+ m.content_text = '',
5970
+ m.metadata_json = '{}',
5971
+ m.archived = false,
5972
+ m.compressed_into = '',
5973
+ m.created_at = '',
5974
+ m.updated_at = ''`,
5975
+ { id }
5976
+ );
5977
+ }
5978
+ async linkMemories(fromId, toId, relation, metadata) {
5979
+ await this.ensureMemoryNode(fromId);
5980
+ await this.ensureMemoryNode(toId);
5981
+ await this.run(
5982
+ `MATCH (a:Memory { id: $fromId }), (b:Memory { id: $toId })
5983
+ MERGE (a)-[r:RELATED { relation: $relation }]->(b)
5984
+ SET r.metadata_json = $metadata`,
5985
+ {
5986
+ fromId,
5987
+ toId,
5988
+ relation,
5989
+ metadata: serializeMetadata2(metadata)
5990
+ }
5991
+ );
5992
+ }
5993
+ async unlinkMemories(fromId, toId, relation) {
5994
+ if (relation !== void 0) {
5995
+ await this.run(
5996
+ `MATCH (a:Memory { id: $fromId })-[r:RELATED { relation: $relation }]->(b:Memory { id: $toId })
5997
+ DELETE r`,
5998
+ { fromId, toId, relation }
5999
+ );
6000
+ return;
6001
+ }
6002
+ await this.run(
6003
+ `MATCH (a:Memory { id: $fromId })-[r:RELATED]->(b:Memory { id: $toId })
6004
+ DELETE r`,
6005
+ { fromId, toId }
6006
+ );
6007
+ }
6008
+ /**
6009
+ * Native Neo4j variable-length path traversal.
6010
+ * Relationship filter uses RELATED.relation property for parity with Kuzu.
6011
+ */
6012
+ async getRelated(memoryId, options) {
6013
+ const depth = Math.max(1, Math.min(options?.depth ?? 1, 16));
6014
+ const direction = options?.direction ?? "both";
6015
+ const relation = options?.relation;
6016
+ const pathPattern = direction === "out" ? `(start)-[:RELATED*1..${depth}]->(n:Memory)` : direction === "in" ? `(start)<-[:RELATED*1..${depth}]-(n:Memory)` : `(start)-[:RELATED*1..${depth}]-(n:Memory)`;
6017
+ const cypher = relation ? `MATCH (start:Memory { id: $id }), p = ${pathPattern}
6018
+ WHERE ALL(r IN relationships(p) WHERE r.relation = $relation)
6019
+ AND n.id <> $id
6020
+ RETURN DISTINCT
6021
+ n.id AS id, n.organization AS organization, n.agent AS agent,
6022
+ n.content_text AS content_text, n.metadata_json AS metadata_json,
6023
+ n.archived AS archived, n.compressed_into AS compressed_into,
6024
+ n.created_at AS created_at, n.updated_at AS updated_at` : `MATCH (start:Memory { id: $id }), p = ${pathPattern}
6025
+ WHERE n.id <> $id
6026
+ RETURN DISTINCT
6027
+ n.id AS id, n.organization AS organization, n.agent AS agent,
6028
+ n.content_text AS content_text, n.metadata_json AS metadata_json,
6029
+ n.archived AS archived, n.compressed_into AS compressed_into,
6030
+ n.created_at AS created_at, n.updated_at AS updated_at`;
6031
+ const params = { id: memoryId };
6032
+ if (relation !== void 0) params.relation = relation;
6033
+ const rows = await this.run(cypher, params);
6034
+ return rows.map(
6035
+ (row) => rowToMemoryRecord(unwrapRow(row))
6036
+ );
6037
+ }
6038
+ async upsertEntity(entity) {
6039
+ const id = entityIdFrom(entity.name, entity.type);
6040
+ await this.run(
6041
+ `MERGE (e:Entity { id: $id })
6042
+ SET e.name = $name, e.type = $type, e.metadata_json = $metadata`,
6043
+ {
6044
+ id,
6045
+ name: entity.name,
6046
+ type: entity.type,
6047
+ metadata: serializeMetadata2(entity.metadata)
6048
+ }
6049
+ );
6050
+ return id;
6051
+ }
6052
+ async linkEntityToMemory(entityId, memoryId, role) {
6053
+ await this.ensureMemoryNode(memoryId);
6054
+ const found = await this.run(
6055
+ `MATCH (e:Entity { id: $id }) RETURN e.id AS id`,
6056
+ { id: entityId }
6057
+ );
6058
+ if (found.length === 0) {
6059
+ throw new DatabaseError(`Entity not found: ${entityId}`, {
6060
+ operation: "linkEntityToMemory"
6061
+ });
6062
+ }
6063
+ await this.run(
6064
+ `MATCH (e:Entity { id: $entityId }), (m:Memory { id: $memoryId })
6065
+ MERGE (e)-[r:MENTIONS]->(m)
6066
+ SET r.role = $role`,
6067
+ { entityId, memoryId, role: role ?? "" }
6068
+ );
6069
+ }
6070
+ async deleteMemory(memoryId) {
6071
+ await this.run(
6072
+ `MATCH (m:Memory { id: $id })
6073
+ DETACH DELETE m`,
6074
+ { id: memoryId }
6075
+ );
6076
+ }
6077
+ async query(cypher, params) {
6078
+ return this.run(cypher, params ?? {});
6079
+ }
6080
+ async health() {
6081
+ try {
6082
+ if (!this.opened || !this.driver) {
6083
+ return { ok: false, backend: "neo4j", details: { reason: "not open" } };
6084
+ }
6085
+ await this.driver.verifyConnectivity();
6086
+ let serverInfo = void 0;
6087
+ if (typeof this.driver.getServerInfo === "function") {
6088
+ serverInfo = await this.driver.getServerInfo();
6089
+ }
6090
+ return {
6091
+ ok: true,
6092
+ backend: "neo4j",
6093
+ details: {
6094
+ url: this.url,
6095
+ database: this.database ?? "default",
6096
+ networked: true,
6097
+ serverInfo
6098
+ }
6099
+ };
6100
+ } catch (error) {
6101
+ return {
6102
+ ok: false,
6103
+ backend: "neo4j",
6104
+ details: {
6105
+ url: this.url,
6106
+ error: error instanceof Error ? error.message : String(error)
6107
+ }
6108
+ };
6109
+ }
6110
+ }
6111
+ };
6112
+ function unwrapNeo4jValue(value) {
6113
+ if (value == null) return value;
6114
+ if (typeof value === "object" && value !== null && "toNumber" in value) {
6115
+ try {
6116
+ return value.toNumber();
6117
+ } catch {
6118
+ }
6119
+ }
6120
+ if (typeof value === "object" && value !== null && "properties" in value) {
6121
+ return unwrapRow(
6122
+ value.properties
6123
+ );
6124
+ }
6125
+ return value;
6126
+ }
6127
+ function unwrapRow(row) {
6128
+ const out = {};
6129
+ for (const [k, v] of Object.entries(row)) {
6130
+ out[k] = unwrapNeo4jValue(v);
6131
+ }
6132
+ return out;
6133
+ }
6134
+
5363
6135
  // src/core/validate.ts
5364
6136
  function assertNonEmpty(value, fieldName) {
5365
6137
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -5440,9 +6212,21 @@ function validateTelemetryConfig(config) {
5440
6212
  if (!config.database || typeof config.database !== "object") {
5441
6213
  throw new ConfigurationError("telemetry.database is required when telemetry is enabled");
5442
6214
  }
6215
+ const providerName = String(
6216
+ config.database.provider ?? ""
6217
+ );
6218
+ if (providerName === "kuzu" || providerName === "neo4j") {
6219
+ throw new ConfigurationError(
6220
+ `telemetry supports sqlite or postgres only (got "${providerName}")`,
6221
+ {
6222
+ reason: `provider=${providerName}`,
6223
+ suggestion: 'Use telemetry: { database: { provider: "sqlite", url: "./telemetry.db" } }. Graph backends (kuzu/neo4j) are not valid telemetry stores.'
6224
+ }
6225
+ );
6226
+ }
5443
6227
  if (config.database.provider !== "sqlite") {
5444
6228
  throw new ConfigurationError(
5445
- `Unsupported telemetry provider "${config.database.provider}". Only "sqlite" is implemented in v0.3.0; PostgreSQL will be added later without changing application code.`,
6229
+ `Unsupported telemetry provider "${config.database.provider}". Only "sqlite" is implemented; PostgreSQL typed but not implemented. Telemetry supports sqlite or postgres only \u2014 not kuzu/neo4j.`,
5446
6230
  {
5447
6231
  reason: `provider=${config.database.provider}`,
5448
6232
  suggestion: 'Use telemetry: { database: { provider: "sqlite", url: "./telemetry.db" } }'
@@ -5527,14 +6311,74 @@ function validateWolbargOptions(options) {
5527
6311
  if (telemetry && !isTelemetryProvider(telemetry)) {
5528
6312
  telemetry = validateTelemetryConfig(telemetry);
5529
6313
  }
6314
+ let graph = options.graph;
6315
+ if (graph !== void 0) {
6316
+ graph = resolveGraphInput(graph);
6317
+ }
5530
6318
  return {
5531
6319
  ...options,
5532
6320
  organization: options.organization.trim(),
5533
6321
  storage,
5534
6322
  database: void 0,
5535
- ...telemetry ? { telemetry } : {}
6323
+ ...telemetry ? { telemetry } : {},
6324
+ ...graph !== void 0 ? { graph } : {}
5536
6325
  };
5537
6326
  }
6327
+ function resolveGraphInput(input) {
6328
+ if (input == null || typeof input !== "object") {
6329
+ throw new ConfigurationError(
6330
+ "graph must be a GraphProvider instance or a config object",
6331
+ {
6332
+ suggestion: "Use graph: sqliteGraph({ path }) or graph: neo4jGraph({ url, username, password })"
6333
+ }
6334
+ );
6335
+ }
6336
+ if (Array.isArray(input)) {
6337
+ throw new ConfigurationError(
6338
+ "graph must be a GraphProvider instance or a config object",
6339
+ {
6340
+ suggestion: "Use graph: sqliteGraph({ path }) or graph: neo4jGraph({ url, username, password })"
6341
+ }
6342
+ );
6343
+ }
6344
+ if (isGraphProvider(input)) {
6345
+ return input;
6346
+ }
6347
+ const config = input;
6348
+ if (config.provider === "sqlite") {
6349
+ if (typeof config.path !== "string" || !config.path.trim()) {
6350
+ throw new ConfigurationError(
6351
+ 'graph sqlite config requires a non-empty "path"'
6352
+ );
6353
+ }
6354
+ return new SqliteGraphProvider({ path: config.path });
6355
+ }
6356
+ if (config.provider === "neo4j") {
6357
+ if (typeof config.url !== "string" || !config.url.trim()) {
6358
+ throw new ConfigurationError('graph neo4j config requires a non-empty "url"');
6359
+ }
6360
+ if (typeof config.username !== "string" || !config.username.trim()) {
6361
+ throw new ConfigurationError(
6362
+ 'graph neo4j config requires a non-empty "username"'
6363
+ );
6364
+ }
6365
+ if (typeof config.password !== "string") {
6366
+ throw new ConfigurationError('graph neo4j config requires a "password" string');
6367
+ }
6368
+ return new Neo4jGraphProvider({
6369
+ url: config.url,
6370
+ username: config.username,
6371
+ password: config.password,
6372
+ database: config.database
6373
+ });
6374
+ }
6375
+ throw new ConfigurationError(
6376
+ `Unsupported graph provider "${String(config.provider)}". Supported: "sqlite", "neo4j", or a GraphProvider instance.`,
6377
+ {
6378
+ suggestion: 'Use graph: sqliteGraph({ path: "./local-graph.db" }) or graph: neo4jGraph({ url, username, password })'
6379
+ }
6380
+ );
6381
+ }
5538
6382
 
5539
6383
  // src/telemetry/logger.ts
5540
6384
  var LEVEL_ORDER = {
@@ -5814,7 +6658,7 @@ var SqliteEventDatabase = class {
5814
6658
  try {
5815
6659
  const dbPath = this.resolvePath(this.url);
5816
6660
  if (dbPath !== ":memory:" && !this.readonly) {
5817
- fs5__default.default.mkdirSync(path5__default.default.dirname(dbPath), { recursive: true });
6661
+ fs6__default.default.mkdirSync(path7__default.default.dirname(dbPath), { recursive: true });
5818
6662
  }
5819
6663
  const db = new sqlite$1.DatabaseSync(dbPath, {
5820
6664
  allowExtension: false,
@@ -5859,7 +6703,7 @@ var SqliteEventDatabase = class {
5859
6703
  this.db = null;
5860
6704
  this.columns.clear();
5861
6705
  throw new InitializationError(
5862
- `Failed to open telemetry EventDatabase: ${describe(error)}`,
6706
+ `Failed to open telemetry EventDatabase: ${describe2(error)}`,
5863
6707
  { cause: error instanceof Error ? error : void 0 }
5864
6708
  );
5865
6709
  }
@@ -5914,7 +6758,7 @@ var SqliteEventDatabase = class {
5914
6758
  return event;
5915
6759
  } catch (error) {
5916
6760
  throw new DatabaseError(
5917
- `Failed to write telemetry event: ${describe(error)}`,
6761
+ `Failed to write telemetry event: ${describe2(error)}`,
5918
6762
  { cause: error instanceof Error ? error : void 0 }
5919
6763
  );
5920
6764
  }
@@ -6070,7 +6914,7 @@ var SqliteEventDatabase = class {
6070
6914
  }
6071
6915
  resolvePath(url) {
6072
6916
  if (url === ":memory:") return ":memory:";
6073
- return path5__default.default.isAbsolute(url) ? url : path5__default.default.resolve(process.cwd(), url);
6917
+ return path7__default.default.isAbsolute(url) ? url : path7__default.default.resolve(process.cwd(), url);
6074
6918
  }
6075
6919
  };
6076
6920
  function normalizeEvent(input) {
@@ -6155,7 +6999,7 @@ function parseJson(value) {
6155
6999
  return null;
6156
7000
  }
6157
7001
  }
6158
- function describe(error) {
7002
+ function describe2(error) {
6159
7003
  return error instanceof Error ? error.message : String(error);
6160
7004
  }
6161
7005
 
@@ -6233,16 +7077,15 @@ var SqliteTelemetryProvider = class {
6233
7077
  }
6234
7078
  }
6235
7079
  };
6236
- var SDK_VERSION2 = "0.3.0";
6237
7080
  var SqliteCheckpointProvider = class {
6238
7081
  name = "sqlite";
6239
7082
  directory;
6240
7083
  ready = false;
6241
7084
  constructor(options) {
6242
- this.directory = options?.directory ?? path5__default.default.resolve(process.cwd(), ".wolbarg", "checkpoints");
7085
+ this.directory = options?.directory ?? path7__default.default.resolve(process.cwd(), ".wolbarg", "checkpoints");
6243
7086
  }
6244
7087
  async open() {
6245
- fs5__default.default.mkdirSync(this.directory, { recursive: true });
7088
+ fs6__default.default.mkdirSync(this.directory, { recursive: true });
6246
7089
  this.ready = true;
6247
7090
  }
6248
7091
  async close() {
@@ -6252,7 +7095,7 @@ var SqliteCheckpointProvider = class {
6252
7095
  this.requireReady();
6253
7096
  assertCheckpointName(name);
6254
7097
  const metaPath = this.metaPath(name);
6255
- if (fs5__default.default.existsSync(metaPath)) {
7098
+ if (fs6__default.default.existsSync(metaPath)) {
6256
7099
  throw new ValidationError(
6257
7100
  `Checkpoint "${name}" already exists. Choose a different name \u2014 checkpoints are never overwritten.`
6258
7101
  );
@@ -6263,7 +7106,7 @@ var SqliteCheckpointProvider = class {
6263
7106
  "Cannot checkpoint an in-memory database. Use a file-backed SQLite database."
6264
7107
  );
6265
7108
  }
6266
- if (!fs5__default.default.existsSync(resolvedSource)) {
7109
+ if (!fs6__default.default.existsSync(resolvedSource)) {
6267
7110
  throw new DatabaseError(
6268
7111
  `Failed to execute checkpoint()
6269
7112
  Reason:
@@ -6274,18 +7117,18 @@ Ensure Wolbarg has been initialized and the database path is correct.`
6274
7117
  }
6275
7118
  const snapshotPath = this.snapshotPath(name);
6276
7119
  await safeSqliteBackup(resolvedSource, snapshotPath);
6277
- const stats = fs5__default.default.statSync(snapshotPath);
7120
+ const stats = fs6__default.default.statSync(snapshotPath);
6278
7121
  const meta2 = {
6279
7122
  name,
6280
7123
  description: options?.description ?? null,
6281
7124
  createdAt: nowIso(),
6282
- sdkVersion: SDK_VERSION2,
7125
+ sdkVersion: SDK_VERSION,
6283
7126
  provider: this.name,
6284
7127
  snapshotPath,
6285
7128
  sourcePath: resolvedSource,
6286
7129
  sizeBytes: stats.size
6287
7130
  };
6288
- fs5__default.default.writeFileSync(metaPath, JSON.stringify(meta2, null, 2), "utf8");
7131
+ fs6__default.default.writeFileSync(metaPath, JSON.stringify(meta2, null, 2), "utf8");
6289
7132
  return meta2;
6290
7133
  }
6291
7134
  async rollback(name, targetPath) {
@@ -6300,11 +7143,11 @@ Ensure Wolbarg has been initialized and the database path is correct.`
6300
7143
  "Cannot rollback into an in-memory database."
6301
7144
  );
6302
7145
  }
6303
- fs5__default.default.mkdirSync(path5__default.default.dirname(resolvedTarget), { recursive: true });
7146
+ fs6__default.default.mkdirSync(path7__default.default.dirname(resolvedTarget), { recursive: true });
6304
7147
  for (const suffix of ["", "-wal", "-shm"]) {
6305
7148
  const side = `${resolvedTarget}${suffix}`;
6306
- if (fs5__default.default.existsSync(side)) {
6307
- fs5__default.default.rmSync(side, { force: true });
7149
+ if (fs6__default.default.existsSync(side)) {
7150
+ fs6__default.default.rmSync(side, { force: true });
6308
7151
  }
6309
7152
  }
6310
7153
  await safeSqliteBackup(meta2.snapshotPath, resolvedTarget);
@@ -6315,26 +7158,26 @@ Ensure Wolbarg has been initialized and the database path is correct.`
6315
7158
  const metaPath = this.metaPath(name);
6316
7159
  const snapshotPath = this.snapshotPath(name);
6317
7160
  let removed = false;
6318
- if (fs5__default.default.existsSync(metaPath)) {
6319
- fs5__default.default.rmSync(metaPath, { force: true });
7161
+ if (fs6__default.default.existsSync(metaPath)) {
7162
+ fs6__default.default.rmSync(metaPath, { force: true });
6320
7163
  removed = true;
6321
7164
  }
6322
- if (fs5__default.default.existsSync(snapshotPath)) {
6323
- fs5__default.default.rmSync(snapshotPath, { force: true });
7165
+ if (fs6__default.default.existsSync(snapshotPath)) {
7166
+ fs6__default.default.rmSync(snapshotPath, { force: true });
6324
7167
  removed = true;
6325
7168
  }
6326
7169
  return removed;
6327
7170
  }
6328
7171
  async listCheckpoints() {
6329
7172
  this.requireReady();
6330
- if (!fs5__default.default.existsSync(this.directory)) {
7173
+ if (!fs6__default.default.existsSync(this.directory)) {
6331
7174
  return [];
6332
7175
  }
6333
- const files = fs5__default.default.readdirSync(this.directory).filter((f) => f.endsWith(".json"));
7176
+ const files = fs6__default.default.readdirSync(this.directory).filter((f) => f.endsWith(".json"));
6334
7177
  const out = [];
6335
7178
  for (const file of files) {
6336
7179
  try {
6337
- const raw = fs5__default.default.readFileSync(path5__default.default.join(this.directory, file), "utf8");
7180
+ const raw = fs6__default.default.readFileSync(path7__default.default.join(this.directory, file), "utf8");
6338
7181
  out.push(JSON.parse(raw));
6339
7182
  } catch {
6340
7183
  }
@@ -6344,20 +7187,81 @@ Ensure Wolbarg has been initialized and the database path is correct.`
6344
7187
  async getCheckpoint(name) {
6345
7188
  this.requireReady();
6346
7189
  const metaPath = this.metaPath(name);
6347
- if (!fs5__default.default.existsSync(metaPath)) {
7190
+ if (!fs6__default.default.existsSync(metaPath)) {
6348
7191
  return null;
6349
7192
  }
6350
7193
  try {
6351
- return JSON.parse(fs5__default.default.readFileSync(metaPath, "utf8"));
7194
+ return JSON.parse(fs6__default.default.readFileSync(metaPath, "utf8"));
6352
7195
  } catch {
6353
7196
  return null;
6354
7197
  }
6355
7198
  }
7199
+ /**
7200
+ * Consistent SQLite file backup (WAL checkpoint + backup API / copy).
7201
+ * Used for memory snapshots and for secondary files such as the graph DB.
7202
+ */
7203
+ async backupSqliteFile(sourcePath, destPath) {
7204
+ this.requireReady();
7205
+ const resolved = resolveDbPath(sourcePath);
7206
+ if (resolved === ":memory:") {
7207
+ throw new ConfigurationError(
7208
+ "Cannot backup an in-memory database. Use a file-backed SQLite database."
7209
+ );
7210
+ }
7211
+ if (!fs6__default.default.existsSync(resolved)) {
7212
+ throw new DatabaseError(
7213
+ `SQLite file not found at ${resolved}`,
7214
+ {
7215
+ suggestion: "Ensure the source database exists before backing up."
7216
+ }
7217
+ );
7218
+ }
7219
+ await safeSqliteBackup(resolved, destPath);
7220
+ }
7221
+ /**
7222
+ * Snapshot a secondary SQLite file (typically the graph DB) next to a named
7223
+ * memory checkpoint as `{name}.graph.db`.
7224
+ */
7225
+ async checkpointGraph(name, graphSourcePath) {
7226
+ this.requireReady();
7227
+ assertCheckpointName(name);
7228
+ const dest = this.graphSnapshotPath(name);
7229
+ await this.backupSqliteFile(graphSourcePath, dest);
7230
+ return dest;
7231
+ }
7232
+ /**
7233
+ * Restore a previously snapshotted graph SQLite file into `targetPath`.
7234
+ */
7235
+ async rollbackGraph(name, targetPath) {
7236
+ this.requireReady();
7237
+ const snapshot = this.graphSnapshotPath(name);
7238
+ if (!fs6__default.default.existsSync(snapshot)) {
7239
+ return;
7240
+ }
7241
+ const resolvedTarget = resolveDbPath(targetPath);
7242
+ if (resolvedTarget === ":memory:") {
7243
+ throw new ConfigurationError(
7244
+ "Cannot rollback graph into an in-memory database."
7245
+ );
7246
+ }
7247
+ fs6__default.default.mkdirSync(path7__default.default.dirname(resolvedTarget), { recursive: true });
7248
+ for (const suffix of ["", "-wal", "-shm"]) {
7249
+ const side = `${resolvedTarget}${suffix}`;
7250
+ if (fs6__default.default.existsSync(side)) {
7251
+ fs6__default.default.rmSync(side, { force: true });
7252
+ }
7253
+ }
7254
+ await safeSqliteBackup(snapshot, resolvedTarget);
7255
+ }
7256
+ /** Absolute path of the graph snapshot for a named checkpoint, if present. */
7257
+ graphSnapshotPath(name) {
7258
+ return path7__default.default.join(this.directory, `${sanitize(name)}.graph.db`);
7259
+ }
6356
7260
  metaPath(name) {
6357
- return path5__default.default.join(this.directory, `${sanitize(name)}.json`);
7261
+ return path7__default.default.join(this.directory, `${sanitize(name)}.json`);
6358
7262
  }
6359
7263
  snapshotPath(name) {
6360
- return path5__default.default.join(this.directory, `${sanitize(name)}.db`);
7264
+ return path7__default.default.join(this.directory, `${sanitize(name)}.db`);
6361
7265
  }
6362
7266
  requireReady() {
6363
7267
  if (!this.ready) {
@@ -6380,12 +7284,12 @@ function assertCheckpointName(name) {
6380
7284
  }
6381
7285
  function resolveDbPath(connectionString) {
6382
7286
  if (connectionString === ":memory:") return ":memory:";
6383
- return path5__default.default.isAbsolute(connectionString) ? connectionString : path5__default.default.resolve(process.cwd(), connectionString);
7287
+ return path7__default.default.isAbsolute(connectionString) ? connectionString : path7__default.default.resolve(process.cwd(), connectionString);
6384
7288
  }
6385
7289
  async function safeSqliteBackup(sourcePath, destPath) {
6386
- fs5__default.default.mkdirSync(path5__default.default.dirname(destPath), { recursive: true });
6387
- if (fs5__default.default.existsSync(destPath)) {
6388
- fs5__default.default.rmSync(destPath, { force: true });
7290
+ fs6__default.default.mkdirSync(path7__default.default.dirname(destPath), { recursive: true });
7291
+ if (fs6__default.default.existsSync(destPath)) {
7292
+ fs6__default.default.rmSync(destPath, { force: true });
6389
7293
  }
6390
7294
  let source = null;
6391
7295
  let dest = null;
@@ -6404,7 +7308,7 @@ async function safeSqliteBackup(sourcePath, destPath) {
6404
7308
  source = null;
6405
7309
  dest.close();
6406
7310
  dest = null;
6407
- fs5__default.default.copyFileSync(sourcePath, destPath);
7311
+ fs6__default.default.copyFileSync(sourcePath, destPath);
6408
7312
  }
6409
7313
  } catch (error) {
6410
7314
  throw new DatabaseError(
@@ -6500,6 +7404,7 @@ var Wolbarg = class {
6500
7404
  keywordSearch = null;
6501
7405
  ocr = null;
6502
7406
  vision = null;
7407
+ graph = null;
6503
7408
  chunking = null;
6504
7409
  retrievalConfig = {};
6505
7410
  embeddingDimensions = null;
@@ -6546,6 +7451,9 @@ var Wolbarg = class {
6546
7451
  this.keywordSearch = validated.keywordSearch ?? null;
6547
7452
  this.ocr = validated.ocr ?? null;
6548
7453
  this.vision = validated.vision ?? null;
7454
+ if (validated.graph) {
7455
+ this.graph = validated.graph;
7456
+ }
6549
7457
  this.chunking = validated.chunking ?? null;
6550
7458
  this.retrievalConfig = validated.retrieval ?? {};
6551
7459
  this.subscribeBackend = new SqliteSubscribeEmitter();
@@ -6624,6 +7532,7 @@ var Wolbarg = class {
6624
7532
  await this.storage.open();
6625
7533
  await this.telemetry.open();
6626
7534
  await this.checkpointProvider?.open();
7535
+ await this.graph?.open();
6627
7536
  if (this.rawEmbedding && this.embeddingCacheConfig.enabled) {
6628
7537
  if (this.storage instanceof SqliteStorageProvider) {
6629
7538
  const sqlite2 = this.storage;
@@ -6675,6 +7584,7 @@ var Wolbarg = class {
6675
7584
  this.initialized = true;
6676
7585
  this.telemetry.emitStartup(this.storage.name);
6677
7586
  } catch (error) {
7587
+ await this.graph?.close().catch(() => void 0);
6678
7588
  await this.storage.close().catch(() => void 0);
6679
7589
  await this.telemetry.close().catch(() => void 0);
6680
7590
  this.initialized = false;
@@ -6701,7 +7611,7 @@ var Wolbarg = class {
6701
7611
  metadata: result.metadata,
6702
7612
  agentId: result.agent,
6703
7613
  tags: telemetryTags(result.metadata),
6704
- extra: { upsertAction: result.action }
7614
+ extra: { upsertAction: result.action, action: result.action }
6705
7615
  });
6706
7616
  return result;
6707
7617
  } catch (error) {
@@ -7405,6 +8315,13 @@ var Wolbarg = class {
7405
8315
  tags: commonTags(serialized.map((result) => result.metadata))
7406
8316
  };
7407
8317
  if (!explain) {
8318
+ if (options.includeGraph === true && this.graph) {
8319
+ const tGraph = performance.now();
8320
+ for (const hit of serialized) {
8321
+ hit.related = await this.hydrateRelated(hit.id);
8322
+ }
8323
+ trace.mark("databaseReadMs", performance.now() - tGraph);
8324
+ }
7408
8325
  trace.success(telemetryFields);
7409
8326
  return serialized;
7410
8327
  }
@@ -7757,21 +8674,82 @@ var Wolbarg = class {
7757
8674
  throw wrapOperationError("ingest", error);
7758
8675
  }
7759
8676
  }
8677
+ /**
8678
+ * Link two memories in the optional graph layer.
8679
+ * Throws {@link ProviderNotConfiguredError} when no graph provider is set.
8680
+ */
8681
+ async linkMemories(fromId, toId, relation, metadata) {
8682
+ const trace = this.telemetry.start("linkMemories");
8683
+ try {
8684
+ await this.requireReady();
8685
+ const graph = this.requireGraph("linkMemories");
8686
+ assertNonEmptyString(fromId, "fromId");
8687
+ assertNonEmptyString(toId, "toId");
8688
+ assertNonEmptyString(relation, "relation");
8689
+ const tGraph = performance.now();
8690
+ await graph.linkMemories(
8691
+ fromId.trim(),
8692
+ toId.trim(),
8693
+ relation.trim(),
8694
+ metadata
8695
+ );
8696
+ trace.mark("databaseWriteMs", performance.now() - tGraph);
8697
+ trace.success({
8698
+ provider: graph.name,
8699
+ memoryIds: [fromId.trim(), toId.trim()],
8700
+ extra: { relation: relation.trim() }
8701
+ });
8702
+ } catch (error) {
8703
+ trace.failure(error);
8704
+ throw wrapOperationError("linkMemories", error);
8705
+ }
8706
+ }
8707
+ /**
8708
+ * Traverse related memories via the optional graph layer.
8709
+ * Throws {@link ProviderNotConfiguredError} when no graph provider is set.
8710
+ * Results are re-hydrated from SQL storage when available.
8711
+ */
8712
+ async getRelated(memoryId, options) {
8713
+ const trace = this.telemetry.start("getRelated");
8714
+ try {
8715
+ await this.requireReady();
8716
+ const graph = this.requireGraph("getRelated");
8717
+ assertNonEmptyString(memoryId, "memoryId");
8718
+ const tGraph = performance.now();
8719
+ const related = await this.hydrateRelated(memoryId.trim(), options);
8720
+ trace.mark("databaseReadMs", performance.now() - tGraph);
8721
+ trace.success({
8722
+ provider: graph.name,
8723
+ memoryIds: [memoryId.trim(), ...related.map((r) => r.id)],
8724
+ returnedCount: related.length
8725
+ });
8726
+ return related;
8727
+ } catch (error) {
8728
+ trace.failure(error);
8729
+ throw wrapOperationError("getRelated", error);
8730
+ }
8731
+ }
7760
8732
  async forget(options) {
7761
8733
  const trace = this.telemetry.start("forget");
7762
8734
  try {
7763
8735
  const { storage, organization } = await this.requireReady();
8736
+ const deletedIds = [];
7764
8737
  const deleted = await this.withWriteLock(async () => {
7765
8738
  if ("id" in options && options.id !== void 0) {
7766
8739
  assertNonEmptyString(options.id, "id");
7767
- const ok = await storage.deleteMemoryById(
7768
- options.id.trim(),
7769
- organization
7770
- );
8740
+ const id = options.id.trim();
8741
+ const ok = await storage.deleteMemoryById(id, organization);
8742
+ if (ok) deletedIds.push(id);
7771
8743
  return ok ? 1 : 0;
7772
8744
  }
7773
8745
  if ("filter" in options && options.filter?.agent) {
7774
8746
  assertNonEmptyString(options.filter.agent, "filter.agent");
8747
+ const rows = await storage.listMemories({
8748
+ organization,
8749
+ agent: options.filter.agent.trim(),
8750
+ includeArchived: true
8751
+ });
8752
+ for (const row of rows) deletedIds.push(row.id);
7775
8753
  return storage.deleteMemoriesByFilter({
7776
8754
  organization,
7777
8755
  agent: options.filter.agent.trim(),
@@ -7782,12 +8760,20 @@ var Wolbarg = class {
7782
8760
  "forget requires either { id } or { filter: { agent } }"
7783
8761
  );
7784
8762
  });
8763
+ if (deleted > 0 && this.graph && deletedIds.length > 0) {
8764
+ const tGraph = performance.now();
8765
+ for (const id of deletedIds) {
8766
+ await this.graph.deleteMemory(id);
8767
+ }
8768
+ trace.mark("databaseWriteMs", performance.now() - tGraph);
8769
+ }
7785
8770
  trace.success({
7786
8771
  provider: storage.name,
7787
8772
  returnedCount: deleted,
7788
- memoryIds: "id" in options && options.id ? [options.id.trim()] : null,
8773
+ memoryIds: deletedIds.length > 0 ? deletedIds : null,
7789
8774
  filters: "filter" in options ? options.filter : null,
7790
- agentId: "filter" in options ? options.filter?.agent?.trim() ?? null : null
8775
+ agentId: "filter" in options ? options.filter?.agent?.trim() ?? null : null,
8776
+ extra: this.graph ? { graphCascade: deletedIds.length } : void 0
7791
8777
  });
7792
8778
  if (deleted > 0) {
7793
8779
  this.emitChange({
@@ -7871,6 +8857,15 @@ var Wolbarg = class {
7871
8857
  );
7872
8858
  }
7873
8859
  const deleted = await this.withWriteLock(async () => {
8860
+ if (this.graph) {
8861
+ const rows = await storage.listMemories({
8862
+ organization,
8863
+ includeArchived: true
8864
+ });
8865
+ for (const row of rows) {
8866
+ await this.graph.deleteMemory(row.id);
8867
+ }
8868
+ }
7874
8869
  return storage.clearOrganization(organization);
7875
8870
  });
7876
8871
  trace.success({ provider: storage.name, returnedCount: deleted });
@@ -7887,8 +8882,10 @@ var Wolbarg = class {
7887
8882
  await this.requireReady();
7888
8883
  const provider = this.requireCheckpointProvider();
7889
8884
  const source = this.requireMemoryDbPath();
8885
+ this.assertGraphCheckpointSupported("checkpoint");
7890
8886
  const tCheckpoint = performance.now();
7891
8887
  const meta2 = await provider.checkpoint(name, source, options);
8888
+ await this.snapshotGraphAlongside(meta2.snapshotPath, "checkpoint");
7892
8889
  trace.mark("databaseWriteMs", performance.now() - tCheckpoint);
7893
8890
  trace.success({
7894
8891
  provider: provider.name,
@@ -7917,8 +8914,13 @@ var Wolbarg = class {
7917
8914
  await this.storage.close();
7918
8915
  storageClosed = true;
7919
8916
  }
8917
+ const graphClosed = await this.closeGraphForSnapshot();
7920
8918
  const tRollback = performance.now();
7921
8919
  const meta2 = await provider.rollback(name, target);
8920
+ await this.restoreGraphAlongside(meta2.snapshotPath, "rollback");
8921
+ if (graphClosed) {
8922
+ await this.graph?.open();
8923
+ }
7922
8924
  trace.mark("databaseWriteMs", performance.now() - tRollback);
7923
8925
  await this.storage?.open();
7924
8926
  storageClosed = false;
@@ -8004,6 +9006,7 @@ var Wolbarg = class {
8004
9006
  try {
8005
9007
  await this.requireReady();
8006
9008
  const source = this.requireMemoryDbPath();
9009
+ this.assertGraphCheckpointSupported("export");
8007
9010
  if (this.storage?.name === "sqlite") {
8008
9011
  }
8009
9012
  const result = await this.transfer.exportTo(
@@ -8011,6 +9014,7 @@ var Wolbarg = class {
8011
9014
  source,
8012
9015
  this.organization ?? void 0
8013
9016
  );
9017
+ await this.snapshotGraphAlongside(result.path, "export");
8014
9018
  trace.success({
8015
9019
  provider: "sqlite",
8016
9020
  extra: { path: result.path, sizeBytes: result.sizeBytes }
@@ -8036,10 +9040,16 @@ var Wolbarg = class {
8036
9040
  await this.storage.close();
8037
9041
  storageClosed = true;
8038
9042
  }
9043
+ const graphClosed = await this.closeGraphForSnapshot();
9044
+ this.assertGraphCheckpointSupported("import");
8039
9045
  const result = await this.transfer.importFrom(
8040
9046
  exportPath,
8041
9047
  target
8042
9048
  );
9049
+ await this.restoreGraphAlongside(result.path, "import");
9050
+ if (graphClosed) {
9051
+ await this.graph?.open();
9052
+ }
8043
9053
  await this.storage?.open();
8044
9054
  storageClosed = false;
8045
9055
  if (this.embeddingDimensions !== null) {
@@ -8093,6 +9103,8 @@ var Wolbarg = class {
8093
9103
  this.pgListenBackend = null;
8094
9104
  await this.telemetry.close().catch(() => void 0);
8095
9105
  await this.checkpointProvider?.close().catch(() => void 0);
9106
+ await this.graph?.close().catch(() => void 0);
9107
+ this.graph = null;
8096
9108
  if (this.storage) {
8097
9109
  await this.storage.close();
8098
9110
  }
@@ -8138,6 +9150,106 @@ var Wolbarg = class {
8138
9150
  }
8139
9151
  return this.checkpointProvider;
8140
9152
  }
9153
+ requireGraph(method) {
9154
+ if (!this.graph) {
9155
+ throw new ProviderNotConfiguredError(
9156
+ "graph",
9157
+ method,
9158
+ "pass graph: sqliteGraph({ path }) or graph: neo4jGraph({ url, username, password })"
9159
+ );
9160
+ }
9161
+ return this.graph;
9162
+ }
9163
+ assertGraphCheckpointSupported(operation) {
9164
+ if (!this.graph) return;
9165
+ if (!this.graph.supportsFileSnapshot()) {
9166
+ throw new GraphCheckpointNotSupportedError(this.graph.name, operation);
9167
+ }
9168
+ }
9169
+ graphSnapshotDir(alongsidePath) {
9170
+ return `${alongsidePath}.graph`;
9171
+ }
9172
+ async closeGraphForSnapshot() {
9173
+ if (!this.graph || !this.graph.supportsFileSnapshot()) return false;
9174
+ await this.graph.close();
9175
+ return true;
9176
+ }
9177
+ async snapshotGraphAlongside(alongsidePath, operation) {
9178
+ if (!this.graph) return;
9179
+ if (!this.graph.supportsFileSnapshot()) {
9180
+ throw new GraphCheckpointNotSupportedError(this.graph.name, operation);
9181
+ }
9182
+ const dataPath = this.graph.getDataPath();
9183
+ if (!dataPath) return;
9184
+ const dest = this.graphSnapshotDir(alongsidePath);
9185
+ await this.graph.close();
9186
+ try {
9187
+ if (fs6__default.default.existsSync(dest)) {
9188
+ fs6__default.default.rmSync(dest, { recursive: true, force: true });
9189
+ }
9190
+ if (fs6__default.default.existsSync(dataPath)) {
9191
+ const stat = fs6__default.default.statSync(dataPath);
9192
+ if (stat.isDirectory()) {
9193
+ fs6__default.default.cpSync(dataPath, dest, { recursive: true });
9194
+ } else {
9195
+ fs6__default.default.mkdirSync(dest, { recursive: true });
9196
+ fs6__default.default.cpSync(dataPath, path7__default.default.join(dest, path7__default.default.basename(dataPath)));
9197
+ const parent = path7__default.default.dirname(dataPath);
9198
+ const base = path7__default.default.basename(dataPath);
9199
+ for (const entry of fs6__default.default.readdirSync(parent)) {
9200
+ if (entry === base) continue;
9201
+ if (entry.startsWith(base)) {
9202
+ fs6__default.default.cpSync(
9203
+ path7__default.default.join(parent, entry),
9204
+ path7__default.default.join(dest, entry),
9205
+ { recursive: true }
9206
+ );
9207
+ }
9208
+ }
9209
+ }
9210
+ }
9211
+ } finally {
9212
+ await this.graph.open();
9213
+ }
9214
+ }
9215
+ async restoreGraphAlongside(alongsidePath, operation) {
9216
+ if (!this.graph) return;
9217
+ if (!this.graph.supportsFileSnapshot()) {
9218
+ throw new GraphCheckpointNotSupportedError(this.graph.name, operation);
9219
+ }
9220
+ const dataPath = this.graph.getDataPath();
9221
+ if (!dataPath) return;
9222
+ const src = this.graphSnapshotDir(alongsidePath);
9223
+ if (!fs6__default.default.existsSync(src)) {
9224
+ return;
9225
+ }
9226
+ if (fs6__default.default.existsSync(dataPath)) {
9227
+ fs6__default.default.rmSync(dataPath, { recursive: true, force: true });
9228
+ }
9229
+ const entries = fs6__default.default.readdirSync(src);
9230
+ if (entries.length === 1 && entries[0] === path7__default.default.basename(dataPath)) {
9231
+ fs6__default.default.mkdirSync(path7__default.default.dirname(dataPath), { recursive: true });
9232
+ fs6__default.default.cpSync(path7__default.default.join(src, entries[0]), dataPath, { recursive: true });
9233
+ } else {
9234
+ fs6__default.default.mkdirSync(path7__default.default.dirname(dataPath), { recursive: true });
9235
+ fs6__default.default.cpSync(src, dataPath, { recursive: true });
9236
+ }
9237
+ }
9238
+ async hydrateRelated(memoryId, options) {
9239
+ const graph = this.requireGraph("getRelated");
9240
+ const related = await graph.getRelated(memoryId, options);
9241
+ const { storage, organization } = await this.requireReady();
9242
+ const out = [];
9243
+ for (const stub of related) {
9244
+ const row = await storage.getMemoryById(stub.id, organization);
9245
+ if (row) {
9246
+ out.push(toMemoryRecord(row));
9247
+ } else {
9248
+ out.push(stub);
9249
+ }
9250
+ }
9251
+ return out;
9252
+ }
8141
9253
  requireMemoryDbPath() {
8142
9254
  if (!this.memoryDbPath || this.memoryDbPath === ":memory:") {
8143
9255
  throw new ConfigurationError(
@@ -8147,7 +9259,7 @@ var Wolbarg = class {
8147
9259
  }
8148
9260
  );
8149
9261
  }
8150
- return path5__default.default.isAbsolute(this.memoryDbPath) ? this.memoryDbPath : path5__default.default.resolve(process.cwd(), this.memoryDbPath);
9262
+ return path7__default.default.isAbsolute(this.memoryDbPath) ? this.memoryDbPath : path7__default.default.resolve(process.cwd(), this.memoryDbPath);
8151
9263
  }
8152
9264
  };
8153
9265
  function wolbarg(options) {
@@ -8243,10 +9355,16 @@ function sqliteTelemetry(url) {
8243
9355
  function sqliteCheckpoint(directory) {
8244
9356
  return new SqliteCheckpointProvider({ directory });
8245
9357
  }
9358
+ function sqliteGraph(options) {
9359
+ return new SqliteGraphProvider(options);
9360
+ }
9361
+ function neo4jGraph(options) {
9362
+ return new Neo4jGraphProvider(options);
9363
+ }
8246
9364
  function createTelemetryProvider(config) {
8247
9365
  if (config.database.provider !== "sqlite") {
8248
9366
  throw new ConfigurationError(
8249
- `Unsupported telemetry provider "${config.database.provider}". Only "sqlite" is implemented in v0.3.0.`
9367
+ `Unsupported telemetry provider "${config.database.provider}". Only "sqlite" is implemented. Telemetry supports sqlite or postgres only \u2014 not kuzu/neo4j.`
8250
9368
  );
8251
9369
  }
8252
9370
  const url = config.database.url ?? config.database.connectionString ?? "";
@@ -8708,19 +9826,23 @@ exports.ConfigurationError = ConfigurationError;
8708
9826
  exports.DatabaseError = DatabaseError;
8709
9827
  exports.EmbeddingError = EmbeddingError;
8710
9828
  exports.FixedChunkingStrategy = FixedChunkingStrategy;
9829
+ exports.GraphCheckpointNotSupportedError = GraphCheckpointNotSupportedError;
8711
9830
  exports.HeadingChunkingStrategy = HeadingChunkingStrategy;
8712
9831
  exports.InitializationError = InitializationError;
8713
9832
  exports.LlmCompressionProvider = LlmCompressionProvider;
8714
9833
  exports.MarkdownChunkingStrategy = MarkdownChunkingStrategy;
8715
9834
  exports.MemoryNotFoundError = MemoryNotFoundError;
9835
+ exports.Neo4jGraphProvider = Neo4jGraphProvider;
8716
9836
  exports.NoopTelemetryProvider = NoopTelemetryProvider;
8717
9837
  exports.ParagraphChunkingStrategy = ParagraphChunkingStrategy;
8718
9838
  exports.PostgresStorageProvider = PostgresStorageProvider;
8719
9839
  exports.ProviderNotConfiguredError = ProviderNotConfiguredError;
9840
+ exports.SDK_VERSION = SDK_VERSION;
8720
9841
  exports.SentenceChunkingStrategy = SentenceChunkingStrategy;
8721
9842
  exports.SqliteCheckpointProvider = SqliteCheckpointProvider;
8722
9843
  exports.SqliteDatabaseProvider = SqliteDatabaseProvider;
8723
9844
  exports.SqliteEventDatabase = SqliteEventDatabase;
9845
+ exports.SqliteGraphProvider = SqliteGraphProvider;
8724
9846
  exports.SqliteStorageProvider = SqliteStorageProvider;
8725
9847
  exports.SqliteTelemetryProvider = SqliteTelemetryProvider;
8726
9848
  exports.StorageLockedError = StorageLockedError;
@@ -8746,6 +9868,7 @@ exports.geminiVision = geminiVision;
8746
9868
  exports.jinaReranker = jinaReranker;
8747
9869
  exports.lmStudioEmbedding = lmStudioEmbedding;
8748
9870
  exports.meta = meta;
9871
+ exports.neo4jGraph = neo4jGraph;
8749
9872
  exports.ollamaEmbedding = ollamaEmbedding;
8750
9873
  exports.ollamaLlm = ollamaLlm;
8751
9874
  exports.openRouterEmbedding = openRouterEmbedding;
@@ -8762,6 +9885,7 @@ exports.runBenchmark = runBenchmark;
8762
9885
  exports.sqlite = sqlite;
8763
9886
  exports.sqliteCheckpoint = sqliteCheckpoint;
8764
9887
  exports.sqliteConfig = sqliteConfig;
9888
+ exports.sqliteGraph = sqliteGraph;
8765
9889
  exports.sqliteTelemetry = sqliteTelemetry;
8766
9890
  exports.summarizeBenchmark = summarizeBenchmark;
8767
9891
  exports.tesseract = tesseract;