@dudousxd/nestjs-agent-store-drizzle 0.9.0 → 0.10.1

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
@@ -8,11 +8,11 @@ var __export = (target, all) => {
8
8
  for (var name in all)
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
- var __copyProps = (to, from, except, desc3) => {
11
+ var __copyProps = (to, from, except, desc4) => {
12
12
  if (from && typeof from === "object" || typeof from === "function") {
13
13
  for (let key of __getOwnPropNames(from))
14
14
  if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc3 = __getOwnPropDesc(from, key)) || desc3.enumerable });
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc4 = __getOwnPropDesc(from, key)) || desc4.enumerable });
16
16
  }
17
17
  return to;
18
18
  };
@@ -24,7 +24,11 @@ __export(index_exports, {
24
24
  DrizzleAgentStore: () => DrizzleAgentStore,
25
25
  DrizzleAgentStoreModule: () => DrizzleAgentStoreModule,
26
26
  DrizzleGovernanceQueries: () => DrizzleGovernanceQueries,
27
+ DrizzleMemoryProvider: () => DrizzleMemoryProvider,
27
28
  DrizzlePricingStore: () => DrizzlePricingStore,
29
+ DrizzleRagIngestionLog: () => DrizzleRagIngestionLog,
30
+ RAG_INGESTION_LOG_PAGE_ORDER: () => RAG_INGESTION_LOG_PAGE_ORDER,
31
+ agentMemory: () => agentMemory,
28
32
  agentMessage: () => agentMessage,
29
33
  agentModelPricing: () => agentModelPricing,
30
34
  agentRun: () => agentRun,
@@ -32,7 +36,8 @@ __export(index_exports, {
32
36
  agentThread: () => agentThread,
33
37
  agentTokenUsage: () => agentTokenUsage,
34
38
  agentToolCall: () => agentToolCall,
35
- ensureAgentSchema: () => ensureAgentSchema
39
+ ensureAgentSchema: () => ensureAgentSchema,
40
+ ragIngestionLog: () => ragIngestionLog
36
41
  });
37
42
  module.exports = __toCommonJS(index_exports);
38
43
 
@@ -189,13 +194,67 @@ var agentRun = (0, import_sqlite_core.sqliteTable)("agent_run", {
189
194
  }, (table) => [
190
195
  (0, import_sqlite_core.index)("agent_run_started_idx").on(table.startedAt)
191
196
  ]);
197
+ var agentMemory = (0, import_sqlite_core.sqliteTable)("agent_memory", {
198
+ id: (0, import_sqlite_core.text)("id").primaryKey(),
199
+ /** The opaque scope token. Unique with `key`: one fact per key per scope. */
200
+ scope: (0, import_sqlite_core.text)("scope").notNull(),
201
+ /** What the fact is ABOUT. Two scopes sharing a key are one question answered twice. */
202
+ key: (0, import_sqlite_core.text)("key").notNull(),
203
+ /** The fact itself, one line, as the model reads it. */
204
+ text: (0, import_sqlite_core.text)("text").notNull(),
205
+ originAuthor: (0, import_sqlite_core.text)("origin_author").$type().notNull(),
206
+ /** A pointer that is allowed to dangle — see the table note. */
207
+ originThreadId: (0, import_sqlite_core.text)("origin_thread_id"),
208
+ originRunId: (0, import_sqlite_core.text)("origin_run_id"),
209
+ originActorRef: (0, import_sqlite_core.text)("origin_actor_ref"),
210
+ /** Always-on: carried in every prompt. Set by a host's console, never by a write. */
211
+ pinned: (0, import_sqlite_core.integer)("pinned", {
212
+ mode: "boolean"
213
+ }).notNull().default(false),
214
+ createdAt: (0, import_sqlite_core.integer)("created_at", {
215
+ mode: "timestamp_ms"
216
+ }).notNull(),
217
+ updatedAt: (0, import_sqlite_core.integer)("updated_at", {
218
+ mode: "timestamp_ms"
219
+ }).notNull()
220
+ }, (table) => [
221
+ (0, import_sqlite_core.uniqueIndex)("agent_memory_scope_key_uq").on(table.scope, table.key)
222
+ ]);
223
+ var ragIngestionLog = (0, import_sqlite_core.sqliteTable)("rag_ingestion_log", {
224
+ /** The vector-store document id (chunk ids are `${documentId}#<n>`). */
225
+ documentId: (0, import_sqlite_core.text)("document_id").primaryKey(),
226
+ status: (0, import_sqlite_core.text)("status").$type().notNull(),
227
+ collection: (0, import_sqlite_core.text)("collection"),
228
+ ownerType: (0, import_sqlite_core.text)("owner_type"),
229
+ ownerId: (0, import_sqlite_core.text)("owner_id"),
230
+ /** Citation-facing origin — the media record's path. */
231
+ source: (0, import_sqlite_core.text)("source"),
232
+ mimeType: (0, import_sqlite_core.text)("mime_type"),
233
+ size: (0, import_sqlite_core.integer)("size"),
234
+ /** Chunk count on `ingested`; null otherwise. */
235
+ chunks: (0, import_sqlite_core.integer)("chunks"),
236
+ /** Skip reason on `skipped` (`unsupported-type` | `too-large` | `empty-text`); null otherwise. */
237
+ reason: (0, import_sqlite_core.text)("reason"),
238
+ /** Error message on `failed`; null otherwise. */
239
+ error: (0, import_sqlite_core.text)("error"),
240
+ createdAt: (0, import_sqlite_core.integer)("created_at", {
241
+ mode: "timestamp_ms"
242
+ }).notNull(),
243
+ updatedAt: (0, import_sqlite_core.integer)("updated_at", {
244
+ mode: "timestamp_ms"
245
+ }).notNull()
246
+ }, (table) => [
247
+ (0, import_sqlite_core.index)("rag_ingestion_log_collection_idx").on(table.collection, table.updatedAt)
248
+ ]);
192
249
  var agentSchema = {
193
250
  agentThread,
194
251
  agentMessage,
195
252
  agentToolCall,
196
253
  agentTokenUsage,
197
254
  agentModelPricing,
198
- agentRun
255
+ agentRun,
256
+ agentMemory,
257
+ ragIngestionLog
199
258
  };
200
259
 
201
260
  // src/ensure-schema.ts
@@ -290,7 +349,41 @@ var statements = [
290
349
  parent_run_id TEXT
291
350
  )`,
292
351
  `CREATE INDEX IF NOT EXISTS agent_run_started_idx
293
- ON agent_run (started_at)`
352
+ ON agent_run (started_at)`,
353
+ // `key` and `text` are quoted: both are keywords in at least one engine this DDL is read by, and
354
+ // they are the SPI's own field names, which is worth more here than dodging the quoting.
355
+ `CREATE TABLE IF NOT EXISTS agent_memory (
356
+ id TEXT PRIMARY KEY NOT NULL,
357
+ scope TEXT NOT NULL,
358
+ "key" TEXT NOT NULL,
359
+ "text" TEXT NOT NULL,
360
+ origin_author TEXT NOT NULL,
361
+ origin_thread_id TEXT,
362
+ origin_run_id TEXT,
363
+ origin_actor_ref TEXT,
364
+ pinned INTEGER NOT NULL DEFAULT 0,
365
+ created_at INTEGER NOT NULL,
366
+ updated_at INTEGER NOT NULL
367
+ )`,
368
+ `CREATE UNIQUE INDEX IF NOT EXISTS agent_memory_scope_key_uq
369
+ ON agent_memory (scope, "key")`,
370
+ `CREATE TABLE IF NOT EXISTS rag_ingestion_log (
371
+ document_id TEXT PRIMARY KEY NOT NULL,
372
+ status TEXT NOT NULL,
373
+ collection TEXT,
374
+ owner_type TEXT,
375
+ owner_id TEXT,
376
+ source TEXT,
377
+ mime_type TEXT,
378
+ size INTEGER,
379
+ chunks INTEGER,
380
+ reason TEXT,
381
+ error TEXT,
382
+ created_at INTEGER NOT NULL,
383
+ updated_at INTEGER NOT NULL
384
+ )`,
385
+ `CREATE INDEX IF NOT EXISTS rag_ingestion_log_collection_idx
386
+ ON rag_ingestion_log (collection, updated_at)`
294
387
  ];
295
388
  var additiveColumns = [
296
389
  {
@@ -1434,8 +1527,150 @@ var DrizzleGovernanceQueries = class {
1434
1527
  }
1435
1528
  };
1436
1529
 
1437
- // src/drizzle-pricing-store.ts
1530
+ // src/drizzle-memory-provider.ts
1531
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
1438
1532
  var import_drizzle_orm4 = require("drizzle-orm");
1533
+ function toRecord(row) {
1534
+ return {
1535
+ id: row.id,
1536
+ key: row.key,
1537
+ text: row.text,
1538
+ scope: row.scope,
1539
+ origin: {
1540
+ author: row.originAuthor,
1541
+ ...row.originThreadId !== null ? {
1542
+ threadId: row.originThreadId
1543
+ } : {},
1544
+ ...row.originRunId !== null ? {
1545
+ runId: row.originRunId
1546
+ } : {},
1547
+ ...row.originActorRef !== null ? {
1548
+ actorRef: row.originActorRef
1549
+ } : {}
1550
+ },
1551
+ updatedAt: row.updatedAt.toISOString(),
1552
+ pinned: row.pinned
1553
+ };
1554
+ }
1555
+ __name(toRecord, "toRecord");
1556
+ var DrizzleMemoryProvider = class {
1557
+ static {
1558
+ __name(this, "DrizzleMemoryProvider");
1559
+ }
1560
+ db;
1561
+ constructor(db) {
1562
+ this.db = db;
1563
+ }
1564
+ /**
1565
+ * Every memory visible at `scopes` — asked on EVERY turn, so it is one indexed `scope in (…)`
1566
+ * against the leading column of the (`scope`, `key`) unique index.
1567
+ *
1568
+ * The scope filter is in the QUERY. The library drops out-of-scope records it is handed, but that
1569
+ * is a backstop: a memory held for another actor or another tenant is never selected here, so it
1570
+ * is unreachable rather than outranked.
1571
+ */
1572
+ async list({ scopes }) {
1573
+ if (scopes.length === 0) {
1574
+ return [];
1575
+ }
1576
+ const rows = await this.db.select().from(agentMemory).where((0, import_drizzle_orm4.inArray)(agentMemory.scope, [
1577
+ ...scopes
1578
+ ]));
1579
+ return rows.map(toRecord);
1580
+ }
1581
+ /**
1582
+ * Delete one, and only from the actor's OWN scope.
1583
+ *
1584
+ * `memoryForgetVerdict` already holds the library's endpoint to that rule; the `where` repeats it
1585
+ * because `forget` takes an id and nothing else, so a caller reaching this provider by any other
1586
+ * path would otherwise delete a tenant's or the deployment's memory by knowing one id.
1587
+ *
1588
+ * `false` where nothing matched — which covers "no such id" and "not yours" identically, so the
1589
+ * answer cannot be used to find out which memories exist about other people.
1590
+ */
1591
+ async forget({ id, ctx }) {
1592
+ const deleted = await this.db.delete(agentMemory).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(agentMemory.id, id), (0, import_drizzle_orm4.eq)(agentMemory.scope, (0, import_nestjs_agent_core2.actorScope)(ctx.actor)))).returning({
1593
+ id: agentMemory.id
1594
+ });
1595
+ return deleted.length > 0;
1596
+ }
1597
+ /**
1598
+ * Upsert on (`scope`, `key`) — one statement, against the unique index, rather than a read
1599
+ * followed by a write: two turns concluding the same key at once would otherwise race to a
1600
+ * duplicate-key insert and one of them would fail the `remember` tool.
1601
+ *
1602
+ * The conflict `set` names exactly what a rewrite may change, so `pinned`, `createdAt` and `id`
1603
+ * are left alone by construction rather than by an exclusion anyone could forget to extend.
1604
+ * Dropping `pinned` would silently unpin a row the next time the agent restated the fact;
1605
+ * rewriting `createdAt` would lose when the belief was first formed; reassigning `id` would
1606
+ * invalidate the delete handle a person was already shown.
1607
+ */
1608
+ async write({ key, text: text2, scope, origin, ctx }) {
1609
+ assertAuthorMayWriteScope({
1610
+ scope,
1611
+ origin,
1612
+ ctx
1613
+ });
1614
+ const now = /* @__PURE__ */ new Date();
1615
+ const columns = {
1616
+ originAuthor: origin.author,
1617
+ originThreadId: origin.threadId ?? null,
1618
+ originRunId: origin.runId ?? null,
1619
+ originActorRef: origin.actorRef ?? null
1620
+ };
1621
+ const [row] = await this.db.insert(agentMemory).values({
1622
+ id: crypto.randomUUID(),
1623
+ scope,
1624
+ key,
1625
+ text: text2,
1626
+ ...columns,
1627
+ pinned: false,
1628
+ createdAt: now,
1629
+ updatedAt: now
1630
+ }).onConflictDoUpdate({
1631
+ target: [
1632
+ agentMemory.scope,
1633
+ agentMemory.key
1634
+ ],
1635
+ set: {
1636
+ text: text2,
1637
+ ...columns,
1638
+ updatedAt: now
1639
+ }
1640
+ }).returning();
1641
+ if (row === void 0) {
1642
+ throw new Error(`DrizzleMemoryProvider: the upsert of "${key}" at "${scope}" returned no row; the driver does not support RETURNING on conflict.`);
1643
+ }
1644
+ return toRecord(row);
1645
+ }
1646
+ /**
1647
+ * Make one memory always-on, or stop it being one. The operator act the SPI has no method for, on
1648
+ * purpose: a pin grants a fact a permanent place in every future prompt, so nothing an agent can
1649
+ * reach may set it. Authorize it in your own console — this takes no `ScopeContext` because it is
1650
+ * not an actor's act on their own memory.
1651
+ *
1652
+ * `false` where there is no such id.
1653
+ */
1654
+ async pin({ id, pinned }) {
1655
+ const updated = await this.db.update(agentMemory).set({
1656
+ pinned,
1657
+ updatedAt: /* @__PURE__ */ new Date()
1658
+ }).where((0, import_drizzle_orm4.eq)(agentMemory.id, id)).returning({
1659
+ id: agentMemory.id
1660
+ });
1661
+ return updated.length > 0;
1662
+ }
1663
+ };
1664
+ function assertAuthorMayWriteScope({ scope, origin, ctx }) {
1665
+ const own = (0, import_nestjs_agent_core2.actorScope)(ctx.actor);
1666
+ if (origin.author !== "human" && scope !== own) {
1667
+ throw new Error(`DrizzleMemoryProvider: refusing to write an agent-authored memory at "${scope}"; an agent may write only at "${own}"`);
1668
+ }
1669
+ }
1670
+ __name(assertAuthorMayWriteScope, "assertAuthorMayWriteScope");
1671
+
1672
+ // src/drizzle-pricing-store.ts
1673
+ var import_drizzle_orm5 = require("drizzle-orm");
1439
1674
  var DrizzlePricingStore = class {
1440
1675
  static {
1441
1676
  __name(this, "DrizzlePricingStore");
@@ -1447,7 +1682,7 @@ var DrizzlePricingStore = class {
1447
1682
  async upsertModelPrice(input) {
1448
1683
  await this.db.update(agentModelPricing).set({
1449
1684
  isCurrent: false
1450
- }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(agentModelPricing.modelId, input.modelId), (0, import_drizzle_orm4.eq)(agentModelPricing.isCurrent, true)));
1685
+ }).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.eq)(agentModelPricing.modelId, input.modelId), (0, import_drizzle_orm5.eq)(agentModelPricing.isCurrent, true)));
1451
1686
  await this.db.insert(agentModelPricing).values({
1452
1687
  id: crypto.randomUUID(),
1453
1688
  modelId: input.modelId,
@@ -1460,7 +1695,7 @@ var DrizzlePricingStore = class {
1460
1695
  });
1461
1696
  }
1462
1697
  async listCurrentPrices() {
1463
- const rows = await this.db.select().from(agentModelPricing).where((0, import_drizzle_orm4.eq)(agentModelPricing.isCurrent, true));
1698
+ const rows = await this.db.select().from(agentModelPricing).where((0, import_drizzle_orm5.eq)(agentModelPricing.isCurrent, true));
1464
1699
  return rows.map((row) => ({
1465
1700
  modelId: row.modelId,
1466
1701
  inputPricePer1m: row.inputPricePer1m,
@@ -1476,33 +1711,367 @@ var DrizzlePricingStore = class {
1476
1711
  }
1477
1712
  };
1478
1713
 
1479
- // src/drizzle-agent-store.module.ts
1480
- var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
1714
+ // src/drizzle-rag-ingestion-log.ts
1715
+ var import_node_diagnostics_channel = require("diagnostics_channel");
1481
1716
  var import_common = require("@nestjs/common");
1482
- function _ts_decorate(decorators, target, key, desc3) {
1483
- var c = arguments.length, r = c < 3 ? target : desc3 === null ? desc3 = Object.getOwnPropertyDescriptor(target, key) : desc3, d;
1484
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc3);
1717
+ var import_drizzle_orm6 = require("drizzle-orm");
1718
+ function _ts_decorate(decorators, target, key, desc4) {
1719
+ var c = arguments.length, r = c < 3 ? target : desc4 === null ? desc4 = Object.getOwnPropertyDescriptor(target, key) : desc4, d;
1720
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc4);
1485
1721
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1486
1722
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1487
1723
  }
1488
1724
  __name(_ts_decorate, "_ts_decorate");
1725
+ function _ts_metadata(k, v) {
1726
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1727
+ }
1728
+ __name(_ts_metadata, "_ts_metadata");
1729
+ var CHANNELS = [
1730
+ {
1731
+ name: "aviary:rag:media.ingested",
1732
+ status: "ingested"
1733
+ },
1734
+ {
1735
+ name: "aviary:rag:media.skipped",
1736
+ status: "skipped"
1737
+ },
1738
+ {
1739
+ name: "aviary:rag:media.failed",
1740
+ status: "failed"
1741
+ },
1742
+ {
1743
+ name: "aviary:rag:media.removed",
1744
+ status: "removed"
1745
+ }
1746
+ ];
1747
+ function str(value) {
1748
+ return typeof value === "string" && value.length > 0 ? value : null;
1749
+ }
1750
+ __name(str, "str");
1751
+ function num(value) {
1752
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1753
+ }
1754
+ __name(num, "num");
1755
+ var RAG_INGESTION_LOG_PAGE_ORDER = [
1756
+ (0, import_drizzle_orm6.desc)(ragIngestionLog.updatedAt),
1757
+ (0, import_drizzle_orm6.asc)(ragIngestionLog.documentId)
1758
+ ];
1759
+ var DEFAULT_PAGE_SIZE = 200;
1760
+ var DEFAULT_BATCH_SIZE = 200;
1761
+ function batches(batchSize) {
1762
+ const requested = Math.trunc(batchSize ?? DEFAULT_BATCH_SIZE);
1763
+ return Number.isFinite(requested) && requested >= 1 ? requested : DEFAULT_BATCH_SIZE;
1764
+ }
1765
+ __name(batches, "batches");
1766
+ var CURSOR_COLUMNS = {
1767
+ documentId: ragIngestionLog.documentId,
1768
+ updatedAt: ragIngestionLog.updatedAt
1769
+ };
1770
+ var DrizzleRagIngestionLog = class _DrizzleRagIngestionLog {
1771
+ static {
1772
+ __name(this, "DrizzleRagIngestionLog");
1773
+ }
1774
+ db;
1775
+ logger = new import_common.Logger(_DrizzleRagIngestionLog.name);
1776
+ teardowns = [];
1777
+ inFlight = /* @__PURE__ */ new Set();
1778
+ constructor(db) {
1779
+ this.db = db;
1780
+ }
1781
+ onModuleInit() {
1782
+ for (const channel of CHANNELS) {
1783
+ const listener = /* @__PURE__ */ __name((message) => {
1784
+ const payload = message?.payload;
1785
+ if (payload !== void 0) {
1786
+ this.track(this.record(channel.status, payload));
1787
+ }
1788
+ }, "listener");
1789
+ (0, import_node_diagnostics_channel.subscribe)(channel.name, listener);
1790
+ this.teardowns.push(() => (0, import_node_diagnostics_channel.unsubscribe)(channel.name, listener));
1791
+ }
1792
+ }
1793
+ async onModuleDestroy() {
1794
+ for (const teardown of this.teardowns) {
1795
+ teardown();
1796
+ }
1797
+ this.teardowns = [];
1798
+ await this.settle();
1799
+ }
1800
+ /** Await every in-flight write — for graceful shutdown and deterministic tests. */
1801
+ async settle() {
1802
+ await Promise.allSettled([
1803
+ ...this.inFlight
1804
+ ]);
1805
+ }
1806
+ /** The latest recorded outcome per document, newest first. */
1807
+ async list(query = {}) {
1808
+ const rows = this.db.select().from(ragIngestionLog).where(this.where(query)).orderBy(...RAG_INGESTION_LOG_PAGE_ORDER).limit(query.limit ?? DEFAULT_PAGE_SIZE);
1809
+ return query.offset === void 0 ? rows : rows.offset(query.offset);
1810
+ }
1811
+ /**
1812
+ * The same page plus the unpaginated total, so a caller can tell "these are all of them" from
1813
+ * "these are the first N". A list that silently truncates reads as complete when it isn't.
1814
+ *
1815
+ * Offset paging is only sound over a table nobody is deleting from underneath you: a row removed
1816
+ * behind the cursor shifts everything after it back by one, so the next page starts one row late
1817
+ * and that row is never seen. Use {@link iterate} for any sweep that deletes.
1818
+ */
1819
+ async listPage(query = {}) {
1820
+ const [totalRow] = await this.db.select({
1821
+ value: (0, import_drizzle_orm6.count)()
1822
+ }).from(ragIngestionLog).where(this.where(query));
1823
+ return {
1824
+ rows: await this.list(query),
1825
+ total: totalRow?.value ?? 0
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Every matching row, newest first, streamed in batches — the sweep-safe way to walk this table.
1830
+ *
1831
+ * Paging is *keyset*, not offset: each batch asks for the rows that sort strictly after the last
1832
+ * row yielded, using `(updatedAt, documentId)` — one point in
1833
+ * {@link RAG_INGESTION_LOG_PAGE_ORDER}, which is total because it ends in the primary key. A
1834
+ * cursor made of column values does not move when rows leave the table, which is what makes this
1835
+ * delete-safe *by construction* rather than by the caller carefully advancing an offset by only
1836
+ * the rows it kept.
1837
+ *
1838
+ * **The guarantee, stated once:** a row is yielded exactly when — at the moment the batch that
1839
+ * would contain it is fetched — the row exists, matches `where`, and sorts strictly after the
1840
+ * last row already yielded. Everything below is a consequence of that one sentence.
1841
+ *
1842
+ * What you can rely on:
1843
+ * - **Deleting rows while iterating is safe.** Delete any row you like — the one just yielded, the
1844
+ * whole batch, rows you have not reached yet — and every row that is still there when the sweep
1845
+ * reaches its position is visited exactly once. Nothing is skipped and nothing is repeated,
1846
+ * because deleting a row moves no *other* row's `(updatedAt, documentId)`. (Rows you delete
1847
+ * *ahead* of the cursor are simply never visited, which is the point of deleting them.)
1848
+ * - **Ties are safe.** A bulk upload stamps a whole batch with the same `updatedAt`; the
1849
+ * `documentId` tiebreaker keeps consecutive batches disjoint even when every row ties.
1850
+ * - **Each row is yielded at most once**, for as long as `updatedAt` only ever moves forward —
1851
+ * which is what `record()` does, since it always stamps `now`.
1852
+ *
1853
+ * What this is **not**: a snapshot, or a repeatable read. The sweep observes the table as it is
1854
+ * when each batch is fetched, so:
1855
+ * - A row **inserted** mid-sweep by `record()` is stamped `now`. The order is newest-first, so it
1856
+ * sorts ahead of a cursor the sweep has already moved past, and is not visited. A sweep
1857
+ * therefore never sees documents ingested after it started — the behaviour an orphan sweep
1858
+ * wants, since a document that arrived while you were sweeping is not an orphan. (A *direct*
1859
+ * insert with a backdated `updatedAt` lands ahead of the cursor and **is** visited; nothing in
1860
+ * this class writes one.)
1861
+ * - A row **re-ingested** mid-sweep has its `updatedAt` bumped to `now`, moving it ahead of the
1862
+ * cursor for the same reason. If the sweep had already passed it, it is not visited twice; if it
1863
+ * had not, it is **missed for this pass**. That is the honest limit: against a concurrent
1864
+ * writer `iterate` guarantees "at most once", not "at least once". Sweeps that must not miss a
1865
+ * concurrently-rewritten row should be idempotent and run again.
1866
+ * - Only a row whose `updatedAt` moves *backwards* — which needs a direct write — can be yielded
1867
+ * twice.
1868
+ * - `collection` / `status` are re-evaluated per batch, so a row that stops matching mid-sweep
1869
+ * stops being visited.
1870
+ *
1871
+ * There is no `limit`/`offset` here on purpose: `where` says *which* rows, `options` says how the
1872
+ * sweep is run. A sweep that stops early stops by `break`ing out of the `for await`.
1873
+ */
1874
+ iterate(where = {}, options = {}) {
1875
+ return this.sweep(where, batches(options.batchSize), options.after, (filter, limit) => this.db.select().from(ragIngestionLog).where(filter).orderBy(...RAG_INGESTION_LOG_PAGE_ORDER).limit(limit));
1876
+ }
1877
+ /**
1878
+ * Just the document ids of the matching rows, in {@link RAG_INGESTION_LOG_PAGE_ORDER}.
1879
+ *
1880
+ * Returns **all** of them — no 200-row cap, unlike `list` — by sweeping on {@link iterate}'s
1881
+ * keyset internally, so it is correct against concurrent deletes for the same reason `iterate`
1882
+ * is. What the cap protects against is hydrating an unbounded number of wide rows (`error` is a
1883
+ * TEXT column); this selects two columns — the id, plus the `updatedAt` the cursor needs — and
1884
+ * keeps only the ids, so the peak footprint is one batch of two-column rows plus a list of
1885
+ * strings whose size *is* the answer the caller asked for.
1886
+ *
1887
+ * For callers that need the id set of a collection and nothing else: an orphan sweep unioning the
1888
+ * log against the vector store's own document list.
1889
+ */
1890
+ async listDocumentIds(where = {}, options = {}) {
1891
+ const ids = [];
1892
+ const rows = this.sweep(where, batches(options.batchSize), void 0, (filter, limit) => this.db.select(CURSOR_COLUMNS).from(ragIngestionLog).where(filter).orderBy(...RAG_INGESTION_LOG_PAGE_ORDER).limit(limit));
1893
+ for await (const row of rows) {
1894
+ ids.push(row.documentId);
1895
+ }
1896
+ return ids;
1897
+ }
1898
+ /** The latest recorded outcome for one document, or null if it was never attempted. */
1899
+ async get(documentId) {
1900
+ const [row] = await this.db.select().from(ragIngestionLog).where((0, import_drizzle_orm6.eq)(ragIngestionLog.documentId, documentId));
1901
+ return row ?? null;
1902
+ }
1903
+ /** Forget one document's record. Returns whether a row was actually removed. */
1904
+ async remove(documentId) {
1905
+ const removed = await this.db.delete(ragIngestionLog).where((0, import_drizzle_orm6.eq)(ragIngestionLog.documentId, documentId)).returning({
1906
+ documentId: ragIngestionLog.documentId
1907
+ });
1908
+ return removed.length > 0;
1909
+ }
1910
+ /** Forget every record for a collection — for when the collection itself is deleted. */
1911
+ async removeByCollection(collection) {
1912
+ const removed = await this.db.delete(ragIngestionLog).where((0, import_drizzle_orm6.eq)(ragIngestionLog.collection, collection)).returning({
1913
+ documentId: ragIngestionLog.documentId
1914
+ });
1915
+ return removed.length;
1916
+ }
1917
+ /**
1918
+ * The keyset sweep both {@link iterate} and {@link listDocumentIds} are made of. `page` decides
1919
+ * how wide a row is — whole rows for `iterate`, two columns for `listDocumentIds` — and the
1920
+ * `R extends RagIngestionLogCursor` bound says the only thing the sweep needs back: a row it can
1921
+ * read the next cursor off.
1922
+ *
1923
+ * The cursor advances **per yielded row**, not per batch, so a consumer that `break`s mid-batch
1924
+ * leaves it on the last row it actually saw. It is two column values read off that row, so a row
1925
+ * leaving the table cannot move it — the whole reason this is delete-safe by construction.
1926
+ */
1927
+ async *sweep(where, batchSize, after, page) {
1928
+ let cursor = after;
1929
+ for (; ; ) {
1930
+ const rows = await page(this.keyset(where, cursor), batchSize);
1931
+ if (rows.length === 0) {
1932
+ return;
1933
+ }
1934
+ for (const row of rows) {
1935
+ yield row;
1936
+ cursor = {
1937
+ updatedAt: row.updatedAt,
1938
+ documentId: row.documentId
1939
+ };
1940
+ }
1941
+ }
1942
+ }
1943
+ where(query) {
1944
+ return (0, import_drizzle_orm6.and)(query.collection !== void 0 ? (0, import_drizzle_orm6.eq)(ragIngestionLog.collection, query.collection) : void 0, query.status !== void 0 ? (0, import_drizzle_orm6.eq)(ragIngestionLog.status, query.status) : void 0);
1945
+ }
1946
+ /**
1947
+ * The coordinates filter AND'd with "strictly after `cursor` in
1948
+ * {@link RAG_INGESTION_LOG_PAGE_ORDER}".
1949
+ *
1950
+ * The order is `updatedAt desc, documentId asc`, so "after" means an older timestamp, or the same
1951
+ * timestamp and a larger id. Both halves are needed: the `<` alone would re-yield every row tied
1952
+ * with the cursor, and dropping the tie branch would skip them instead.
1953
+ */
1954
+ keyset(where, cursor) {
1955
+ const base = this.where(where);
1956
+ if (cursor === void 0) {
1957
+ return base;
1958
+ }
1959
+ return (0, import_drizzle_orm6.and)(base, (0, import_drizzle_orm6.or)((0, import_drizzle_orm6.lt)(ragIngestionLog.updatedAt, cursor.updatedAt), (0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(ragIngestionLog.updatedAt, cursor.updatedAt), (0, import_drizzle_orm6.gt)(ragIngestionLog.documentId, cursor.documentId))));
1960
+ }
1961
+ /**
1962
+ * Upsert one outcome. Keyed by document id so the row always reflects the *current* state — a
1963
+ * retry that succeeds overwrites the failure it replaces, rather than leaving a stale error next
1964
+ * to a working document.
1965
+ */
1966
+ async record(status, payload) {
1967
+ const documentId = str(payload.mediaId);
1968
+ if (documentId === null) {
1969
+ return;
1970
+ }
1971
+ try {
1972
+ const now = /* @__PURE__ */ new Date();
1973
+ const collection = str(payload.collection);
1974
+ const ownerType = str(payload.ownerType);
1975
+ const ownerId = str(payload.ownerId);
1976
+ const source = str(payload.source);
1977
+ const mimeType = str(payload.mimeType);
1978
+ const size = num(payload.size);
1979
+ const outcome = {
1980
+ status,
1981
+ // The three outcome-specific columns are exclusive: null out the ones this status doesn't
1982
+ // own on every write, so a successful retry clears the previous attempt's error rather than
1983
+ // leaving it next to a working document. Always present → always in the conflict `set`.
1984
+ chunks: status === "ingested" ? num(payload.chunks) : null,
1985
+ reason: status === "skipped" ? str(payload.reason) : null,
1986
+ error: status === "failed" ? str(payload.error) : null,
1987
+ updatedAt: now,
1988
+ ...collection !== null ? {
1989
+ collection
1990
+ } : {},
1991
+ ...ownerType !== null ? {
1992
+ ownerType
1993
+ } : {},
1994
+ ...ownerId !== null ? {
1995
+ ownerId
1996
+ } : {},
1997
+ ...source !== null ? {
1998
+ source
1999
+ } : {},
2000
+ ...mimeType !== null ? {
2001
+ mimeType
2002
+ } : {},
2003
+ ...size !== null ? {
2004
+ size
2005
+ } : {}
2006
+ };
2007
+ await this.db.insert(ragIngestionLog).values({
2008
+ documentId,
2009
+ createdAt: now,
2010
+ ...outcome
2011
+ }).onConflictDoUpdate({
2012
+ target: ragIngestionLog.documentId,
2013
+ set: outcome
2014
+ });
2015
+ } catch (error) {
2016
+ this.logger.warn(`Could not record RAG ingestion outcome for ${documentId}: ${error instanceof Error ? error.message : String(error)}`);
2017
+ }
2018
+ }
2019
+ track(promise) {
2020
+ const tracked = promise.finally(() => {
2021
+ this.inFlight.delete(tracked);
2022
+ });
2023
+ this.inFlight.add(tracked);
2024
+ }
2025
+ };
2026
+ DrizzleRagIngestionLog = _ts_decorate([
2027
+ (0, import_common.Injectable)(),
2028
+ _ts_metadata("design:type", Function),
2029
+ _ts_metadata("design:paramtypes", [
2030
+ typeof AgentDrizzleDb === "undefined" ? Object : AgentDrizzleDb
2031
+ ])
2032
+ ], DrizzleRagIngestionLog);
2033
+
2034
+ // src/drizzle-agent-store.module.ts
2035
+ var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
2036
+ var import_common2 = require("@nestjs/common");
2037
+ function _ts_decorate2(decorators, target, key, desc4) {
2038
+ var c = arguments.length, r = c < 3 ? target : desc4 === null ? desc4 = Object.getOwnPropertyDescriptor(target, key) : desc4, d;
2039
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc4);
2040
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2041
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2042
+ }
2043
+ __name(_ts_decorate2, "_ts_decorate");
1489
2044
  var DrizzleAgentStoreModule = class _DrizzleAgentStoreModule {
1490
2045
  static {
1491
2046
  __name(this, "DrizzleAgentStoreModule");
1492
2047
  }
1493
2048
  static forRoot(options) {
2049
+ const ragProviders = options.ragIngestionLog === false ? [] : [
2050
+ {
2051
+ provide: DrizzleRagIngestionLog,
2052
+ useFactory: /* @__PURE__ */ __name(() => new DrizzleRagIngestionLog(options.db), "useFactory")
2053
+ }
2054
+ ];
1494
2055
  return {
1495
2056
  module: _DrizzleAgentStoreModule,
1496
2057
  global: true,
1497
2058
  providers: [
2059
+ ...ragProviders,
1498
2060
  {
1499
2061
  provide: DrizzleAgentStore,
1500
2062
  useFactory: /* @__PURE__ */ __name(() => new DrizzleAgentStore(options.db), "useFactory")
1501
2063
  },
1502
2064
  {
1503
- provide: import_nestjs_agent_core2.AGENT_STORE,
2065
+ provide: import_nestjs_agent_core3.AGENT_STORE,
1504
2066
  useExisting: DrizzleAgentStore
1505
2067
  },
2068
+ // Exported, never bound to AGENT_MEMORY: that token's PRESENCE is what turns memory on, so
2069
+ // binding it here would switch the feature on for every host that installs the store. A
2070
+ // host opts in by naming this provider in `AgentModule.forRoot({ memory: { provider } })`.
2071
+ {
2072
+ provide: DrizzleMemoryProvider,
2073
+ useFactory: /* @__PURE__ */ __name(() => new DrizzleMemoryProvider(options.db), "useFactory")
2074
+ },
1506
2075
  {
1507
2076
  provide: DrizzlePricingStore,
1508
2077
  useFactory: /* @__PURE__ */ __name(() => new DrizzlePricingStore(options.db), "useFactory")
@@ -1510,44 +2079,52 @@ var DrizzleAgentStoreModule = class _DrizzleAgentStoreModule {
1510
2079
  // AGENT_PRICING_STORE: the host's own store when supplied, else the default Drizzle one.
1511
2080
  // DrizzleGovernanceQueries prices usage against whatever is bound here.
1512
2081
  options.pricingStore ? {
1513
- provide: import_nestjs_agent_core2.AGENT_PRICING_STORE,
2082
+ provide: import_nestjs_agent_core3.AGENT_PRICING_STORE,
1514
2083
  useClass: options.pricingStore
1515
2084
  } : {
1516
- provide: import_nestjs_agent_core2.AGENT_PRICING_STORE,
2085
+ provide: import_nestjs_agent_core3.AGENT_PRICING_STORE,
1517
2086
  useExisting: DrizzlePricingStore
1518
2087
  },
1519
2088
  {
1520
2089
  provide: DrizzleGovernanceQueries,
1521
2090
  useFactory: /* @__PURE__ */ __name((pricingStore) => new DrizzleGovernanceQueries(options.db, pricingStore), "useFactory"),
1522
2091
  inject: [
1523
- import_nestjs_agent_core2.AGENT_PRICING_STORE
2092
+ import_nestjs_agent_core3.AGENT_PRICING_STORE
1524
2093
  ]
1525
2094
  },
1526
2095
  {
1527
- provide: import_nestjs_agent_core2.AGENT_GOVERNANCE_QUERIES,
2096
+ provide: import_nestjs_agent_core3.AGENT_GOVERNANCE_QUERIES,
1528
2097
  useExisting: DrizzleGovernanceQueries
1529
2098
  }
1530
2099
  ],
1531
2100
  exports: [
2101
+ ...options.ragIngestionLog === false ? [] : [
2102
+ DrizzleRagIngestionLog
2103
+ ],
1532
2104
  DrizzleAgentStore,
1533
- import_nestjs_agent_core2.AGENT_STORE,
2105
+ import_nestjs_agent_core3.AGENT_STORE,
2106
+ DrizzleMemoryProvider,
1534
2107
  DrizzleGovernanceQueries,
1535
- import_nestjs_agent_core2.AGENT_GOVERNANCE_QUERIES,
2108
+ import_nestjs_agent_core3.AGENT_GOVERNANCE_QUERIES,
1536
2109
  DrizzlePricingStore,
1537
- import_nestjs_agent_core2.AGENT_PRICING_STORE
2110
+ import_nestjs_agent_core3.AGENT_PRICING_STORE
1538
2111
  ]
1539
2112
  };
1540
2113
  }
1541
2114
  };
1542
- DrizzleAgentStoreModule = _ts_decorate([
1543
- (0, import_common.Module)({})
2115
+ DrizzleAgentStoreModule = _ts_decorate2([
2116
+ (0, import_common2.Module)({})
1544
2117
  ], DrizzleAgentStoreModule);
1545
2118
  // Annotate the CommonJS export names for ESM import in node:
1546
2119
  0 && (module.exports = {
1547
2120
  DrizzleAgentStore,
1548
2121
  DrizzleAgentStoreModule,
1549
2122
  DrizzleGovernanceQueries,
2123
+ DrizzleMemoryProvider,
1550
2124
  DrizzlePricingStore,
2125
+ DrizzleRagIngestionLog,
2126
+ RAG_INGESTION_LOG_PAGE_ORDER,
2127
+ agentMemory,
1551
2128
  agentMessage,
1552
2129
  agentModelPricing,
1553
2130
  agentRun,
@@ -1555,6 +2132,7 @@ DrizzleAgentStoreModule = _ts_decorate([
1555
2132
  agentThread,
1556
2133
  agentTokenUsage,
1557
2134
  agentToolCall,
1558
- ensureAgentSchema
2135
+ ensureAgentSchema,
2136
+ ragIngestionLog
1559
2137
  });
1560
2138
  //# sourceMappingURL=index.cjs.map