@dudousxd/nestjs-agent-store-drizzle 0.23.0 → 0.25.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/README.md CHANGED
@@ -16,15 +16,33 @@ pnpm add @dudousxd/nestjs-agent-store-drizzle drizzle-orm
16
16
  ## Use
17
17
 
18
18
  The host app owns the connection and passes in an already-opened Drizzle handle — this module never
19
- opens one itself. Any SQLite-dialect driver works (better-sqlite3, libsql, D1, …).
19
+ opens one itself. **SQLite, Postgres and MySQL** all work — CI runs every store suite on
20
+ better-sqlite3, node-postgres (Postgres 16) and mysql2 (MySQL 8.4), and the other Drizzle drivers of
21
+ each dialect speak the same query builder. Build the handle with the schema object for your dialect;
22
+ every store reads the dialect off the handle.
20
23
 
21
24
  ```ts
25
+ // SQLite
22
26
  import Database from 'better-sqlite3';
23
27
  import { drizzle } from 'drizzle-orm/better-sqlite3';
24
- import { DrizzleAgentStoreModule, agentSchema, ensureAgentSchema } from '@dudousxd/nestjs-agent-store-drizzle';
25
-
28
+ import { agentSchema } from '@dudousxd/nestjs-agent-store-drizzle';
26
29
  const db = drizzle(new Database('app.db'), { schema: agentSchema });
27
- await ensureAgentSchema(db); // idempotent CREATE TABLE IF NOT EXISTS — or run your own migrations
30
+
31
+ // Postgres
32
+ import { drizzle } from 'drizzle-orm/node-postgres';
33
+ import { pgAgentSchema } from '@dudousxd/nestjs-agent-store-drizzle';
34
+ const db = drizzle(pool, { schema: pgAgentSchema });
35
+
36
+ // MySQL (8.0+)
37
+ import { drizzle } from 'drizzle-orm/mysql2';
38
+ import { mysqlAgentSchema } from '@dudousxd/nestjs-agent-store-drizzle';
39
+ const db = drizzle(pool, { schema: mysqlAgentSchema, mode: 'default' });
40
+ ```
41
+
42
+ ```ts
43
+ import { DrizzleAgentStoreModule, ensureAgentSchema } from '@dudousxd/nestjs-agent-store-drizzle';
44
+
45
+ await ensureAgentSchema(db); // idempotent: creates what is missing — or run your own migrations
28
46
 
29
47
  @Module({
30
48
  imports: [
@@ -35,10 +53,31 @@ await ensureAgentSchema(db); // idempotent CREATE TABLE IF NOT EXISTS — or run
35
53
  export class AppModule {}
36
54
  ```
37
55
 
38
- The package ships the `agentSchema` (Drizzle tables), `ensureAgentSchema` (a non-destructive
39
- DDL helper for a quick start), `DrizzleAgentStore`, `DrizzleGovernanceQueries`, and
40
- `DrizzleRagIngestionLog`. For production,
41
- prefer your normal drizzle-kit migrations over the `ensureAgentSchema` helper.
56
+ The package ships the tables for each dialect (`agentSchema` · `pgAgentSchema` · `mysqlAgentSchema`
57
+ — same table, column and property names), `ensureAgentSchema` (a non-destructive DDL helper),
58
+ `DrizzleAgentStore`, `DrizzleGovernanceQueries`, and `DrizzleRagIngestionLog`. For production, prefer
59
+ your normal drizzle-kit migrations over the `ensureAgentSchema` helper: point drizzle-kit at the
60
+ schema object for your dialect.
61
+
62
+ ### What each dialect stores
63
+
64
+ | | SQLite | Postgres | MySQL |
65
+ |---|---|---|---|
66
+ | JSON columns | `text` (JSON) | `jsonb` | `json` |
67
+ | timestamps | epoch-ms `integer` | `timestamptz(3)` | `datetime(3)`, UTC |
68
+ | long text (content, reasoning, errors) | `text` | `text` | `longtext` |
69
+ | keys and labels | `text` | `text` | `varchar(255)` |
70
+ | prices / costs | `real` | `double precision` | `double` |
71
+ | id / actor comparison | exact | exact | exact (`utf8mb4_bin`) |
72
+
73
+ `ensureAgentSchema` renders its DDL from these schema objects, creates missing tables, columns and
74
+ indexes, and never drops or alters anything. On Postgres and MySQL it holds a cross-replica lock
75
+ (`pg_advisory_xact_lock` / `get_lock`) for the duration, so replicas booting an empty database at
76
+ once do not race their `CREATE TABLE`s.
77
+
78
+ Messages carry a per-thread `seq`, assigned on append, which is what orders a transcript — two
79
+ messages of one turn routinely share a timestamp. A row written before the column existed has none
80
+ and sorts first, by `created_at`, as before.
42
81
 
43
82
  ## The read a turn makes
44
83
 
@@ -56,7 +95,7 @@ a client rendering a transcript.
56
95
  ## Memory
57
96
 
58
97
  `DrizzleMemoryProvider` is a `MemoryProvider` over `agent_memory`, a table `ensureAgentSchema`
59
- creates alongside the other six.
98
+ creates alongside the other agent tables.
60
99
 
61
100
  ```ts
62
101
  import { DrizzleAgentStoreModule, DrizzleMemoryProvider } from '@dudousxd/nestjs-agent-store-drizzle';
@@ -198,3 +237,100 @@ for the full model.
198
237
  ## License
199
238
 
200
239
  MIT © Davide Carvalho
240
+
241
+
242
+ Action preflight confirmations persist in the nullable `agent_tool_call.confirmation` JSON column.
243
+ The schema helper adds this column to existing databases. Hosts managing their own migrations must
244
+ add it before upgrading (`JSONB` on PostgreSQL, `JSON` on MySQL, JSON text on SQLite). The stored
245
+ confirmation is returned in `StoredMessage.approvals` so a reload uses the same wording.
246
+
247
+ ### Durable action proposals
248
+
249
+ `DrizzleAgentStore` also implements the optional core `ActionProposalStore` capability.
250
+ Its `createActionProposal`, scoped get/list, immutable decision, lease claim/renewal,
251
+ and settlement methods persist to the independent `agent_action_proposal` table;
252
+ no persisted thread, message, or run is required. The standalone
253
+ `DrizzleActionProposalStore` exposes the same methods when only this capability is needed.
254
+ Both accept an optional second constructor argument `{ clock: () => number }` for a
255
+ trusted server clock (default `Date.now`); never pass a request timestamp as that clock.
256
+
257
+ Creation is replay-safe against the original canonical JSON snapshot. A proposal ID
258
+ is globally unique, and every read or mutation checks tenant, actor, and thread scope.
259
+ Approval and queued execution commit together in one version-fenced SQL update.
260
+ Claims/recovery increment a generation and issue a new token; renewal and settlement
261
+ require both and an unexpired lease. Expiry is inclusive (`now >= expiresAt`).
262
+ A contended caller transaction can return `conflict` after bounded CAS retries;
263
+ retry from a fresh transaction when its repeatable-read snapshot is stale.
264
+ The stable `idempotencyKey` survives recovery: delivery is at least once, so the tool
265
+ must enforce idempotency for its external effect. This storage capability does not
266
+ start a worker or change the agent loop.
267
+
268
+ For existing databases, run `ensureAgentSchema(db)` to add the new table and its
269
+ scope/creation-order index without altering existing data. Lists filter and limit in
270
+ SQL (default 100, maximum 1000), with UTF16 lexical ID ordering for equal creation
271
+ times. Logical proposal IDs are limited to 255 UTF16 code units by the core contract.
272
+ If you own migrations,
273
+ include `agent_action_proposal` from `agentSchema`, `pgAgentSchema`, or
274
+ `mysqlAgentSchema` (or the corresponding `agentSchemaDdl(dialect)` statements).
275
+ The physical primary key and scope key are SHA-256 digests; original identifiers
276
+ remain in the canonical JSON text snapshot and are matched exactly, including
277
+ trailing spaces,
278
+ independently of MySQL collation padding. Text storage preserves valid JSON strings
279
+ containing NUL or lone surrogates on every dialect. Decision audit and execution
280
+ work share
281
+ the snapshot column, fenced by the numeric `version` column. Existing table schemas
282
+ and fixture callers remain compatible.
283
+
284
+ Worker discovery is available on `DrizzleAgentStore` and `DrizzleActionProposalStore`
285
+ through the privileged `claimNextActionProposal({ workerId, leaseMs })` and
286
+ `expireActionProposals({ limit })` methods. Do not expose these unscoped methods on
287
+ public routes. Discovery selects at most 32 queued or expired-lease candidates;
288
+ expiry selects at most `limit` (1–1000) pending proposals due at the trusted store
289
+ clock. Existing scoped, version-fenced mutations decide which worker wins.
290
+
291
+ Upgrading an existing proposal table requires the additive nullable
292
+ `execution_status`, `lease_expires_at`, and `proposal_expires_at` columns, the
293
+ `discovery_index_version` column with default 0, and the execution/expiry indexes
294
+ from the exported schema. `ensureAgentSchema` adds these definitions; hosts using
295
+ Drizzle migrations must include them in their forward migration. Stop old writers,
296
+ apply the DDL, then repeat the bounded backfill before starting new workers:
297
+
298
+ ```ts
299
+ while (await store.backfillActionProposalDiscoveryIndex({ limit: 1000 })) {
300
+ // Each batch derives metadata from the canonical payload under its row version.
301
+ }
302
+ ```
303
+
304
+ New writes maintain index version 1 and all discovery metadata atomically with the
305
+ proposal snapshot. Backfill increments the row version without changing the
306
+ proposal payload, timestamps, or audit. It does not invoke transitions or execute
307
+ tools. This upgrade does not support mixed old and new writers: old writers cannot
308
+ maintain the new indexes. Discovery deliberately excludes rows awaiting backfill;
309
+ it never performs an implicit, unbounded migration.
310
+
311
+ ### Independent approval runtime
312
+
313
+ `actionApprovalMode: 'independent'` requires proposal discovery, atomic replacement,
314
+ terminal outcome admission and the thread queue on the same database. This adapter
315
+ advertises admission support for better-sqlite3, node-postgres, mysql2 and libSQL;
316
+ other Drizzle drivers retain blocking behavior and fail independent configuration.
317
+ Synchronous SQLite transaction callbacks complete synchronously, including both
318
+ assistant insertion and the delivery compare-and-set. PostgreSQL/MySQL admission
319
+ locks the proposal and conversation before checking the delivery lease again.
320
+
321
+ The updated schemas add nullable `delivery_status`, `delivery_lease_expires_at`,
322
+ `replacement_group_key`, `outcome_id_key` and their indexes to proposals,
323
+ `action_proposal_outcome` to messages, `proposal_id` to tool calls and
324
+ `ui_capabilities` to queued messages. `ensureAgentSchema` adds missing columns and
325
+ indexes; applications owning drizzle-kit migrations must include the same additions.
326
+ Outcomes use escaped TEXT/LONGTEXT metadata to preserve JSON strings exactly.
327
+
328
+ Delivery writes one assistant fact and its UI together with its admitted marker,
329
+ waits while a user turn owns the conversation and deduplicates retries. Replacement
330
+ only closes pending proposals in the same exact scope, tool and replacement group.
331
+ An existing transaction passed as the database handle remains the transaction
332
+ owner: a caller rollback also rolls back replacement or admission. External side
333
+ effects remain at least once; use the stable tool-context idempotency key.
334
+
335
+ See [the shared setup and rollout guide](../../docs/independent-approvals.md) for
336
+ policies, text/channel decisions, remembered approvals and component capabilities.