@shadowforge0/aquifer-memory 1.5.8 → 1.5.9

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.
Files changed (2) hide show
  1. package/README.md +80 -1
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -132,6 +132,8 @@ Need LLM summarization, the knowledge graph, OpenAI embeddings, or the reranker?
132
132
  | `AQUIFER_RERANK_PROVIDER` | No | Reranker provider: `tei`, `jina`, `openrouter` | `tei` |
133
133
  | `AQUIFER_RERANK_BASE_URL` | No | Reranker endpoint | `http://localhost:8080` |
134
134
  | `AQUIFER_AGENT_ID` | No | Default agent ID | `main` |
135
+ | `AQUIFER_MIGRATIONS_MODE` | No | Startup handshake mode: `apply` (default), `check`, `off` | `apply` |
136
+ | `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | No | Advisory-lock wait before `AQ_MIGRATION_LOCK_TIMEOUT` (default 30000) | `30000` |
135
137
 
136
138
  Full env-to-config mapping is in [consumers/shared/config.js](consumers/shared/config.js).
137
139
 
@@ -377,9 +379,36 @@ Returns an Aquifer instance. Config:
377
379
  }
378
380
  ```
379
381
 
382
+ #### `aquifer.init()`
383
+
384
+ Startup handshake — resolves pending migrations and returns a StartupEnvelope. Hosts should `await` this before accepting traffic. In `apply` mode a `ready=false` envelope is the signal to abort startup.
385
+
386
+ ```javascript
387
+ const envelope = await aquifer.init();
388
+ // {
389
+ // ready: true,
390
+ // memoryMode: 'rw', // 'rw' | 'ro' | 'off'
391
+ // migrationMode: 'apply', // 'apply' | 'check' | 'off'
392
+ // pendingMigrations: [], // migration ids still outstanding
393
+ // appliedMigrations: ['001-base', '003-trust-feedback', '004-completion', '006-insights'],
394
+ // error: null, // { code, message } on failure
395
+ // durationMs: 1035,
396
+ // }
397
+ ```
398
+
399
+ The MCP consumer (`consumers/mcp.js`) already wires `aquifer.init()` before `server.connect()` and exits non-zero if `ready=false` under `apply` mode.
400
+
401
+ #### `aquifer.listPendingMigrations()` / `aquifer.getMigrationStatus()`
402
+
403
+ Returns `{ required, applied, pending, lastRunAt }` via a `pg_tables` signature probe. No DDL runs. Use it from a health check or from a consumer that wants to surface drift before calling `init()`.
404
+
380
405
  #### `aquifer.migrate()`
381
406
 
382
- Runs SQL migrations (idempotent). Creates tables, indexes, triggers, and extensions.
407
+ Runs SQL migrations (idempotent). Creates tables, indexes, triggers, and extensions. Uses `pg_try_advisory_lock` with a 250 ms poll and a `lockTimeoutMs` deadline (30 s default); on exhaustion throws with `code: 'AQ_MIGRATION_LOCK_TIMEOUT'`. On success returns `{ ok: true, durationMs, notices, ddlExecuted }`; on failure throws an error whose `err.notices` / `err.failedAt` describe the stage that blew up. Most callers should go through `aquifer.init()` instead.
408
+
409
+ #### `aquifer.ensureMigrated()`
410
+
411
+ Lazy idempotent wrapper — fires `migrate()` once on first call, no-ops afterwards. Honors `migrations.mode`: `check` only probes, `off` marks the instance migrated without touching the DB.
383
412
 
384
413
  #### `aquifer.commit(sessionId, messages, opts)`
385
414
 
@@ -463,6 +492,26 @@ const result = await aquifer.bootstrap({
463
492
 
464
493
  Cross-session dedup on open loops and decisions, sentinel filtering (removes 無/none/n/a), and maxChars truncation.
465
494
 
495
+ #### `aquifer.insights.commitInsight(opts)` / `recallInsights(query, opts)` / `markStale(id)` / `supersede(oldId, newId)`
496
+
497
+ Higher-order reflections distilled from session windows (preferences, patterns, frustrations, workflows). Split into two identities: a **canonical key** that describes what the insight is *about* (stable across rewordings), and an **idempotency key** that describes which revision of that claim was written.
498
+
499
+ ```javascript
500
+ await aquifer.insights.commitInsight({
501
+ agentId: 'main',
502
+ type: 'preference',
503
+ canonicalClaim: 'mk prefers checking context before coding', // required — short declarative claim
504
+ title: 'Context-first discipline', // best-effort display
505
+ body: '…',
506
+ entities: ['mk', 'claude code'],
507
+ sourceSessionIds: ['sess-a', 'sess-b'],
508
+ evidenceWindow: { from: isoString, to: isoString },
509
+ importance: 0.9,
510
+ });
511
+ ```
512
+
513
+ Write rules: **duplicate** (same idempotency key → return existing), **revision** (same canonical key + newer evidence → INSERT + inline supersede of prior active), **back-fill revision** (same canonical key + older evidence → INSERT without supersede), **stale replay** (same canonical + same body → return existing). Old pre-1.5.6 rows are not retrofitted; their `canonical_key_v2` stays `NULL` and they age out naturally.
514
+
466
515
  #### `aquifer.close()`
467
516
 
468
517
  Closes the PostgreSQL connection pool (only if Aquifer created it).
@@ -498,9 +547,19 @@ createAquifer({
498
547
  access: 0.10, // access frequency weight
499
548
  entityBoost: 0.18, // entity match boost
500
549
  },
550
+ migrations: {
551
+ mode: 'apply', // 'apply' | 'check' | 'off'
552
+ lockTimeoutMs: 30000, // abort init() if advisory lock held this long
553
+ startupTimeoutMs: 60000, // overall init() deadline (plan probe + DDL combined)
554
+ onEvent: null, // (e) => void — lifecycle hook, see below
555
+ },
501
556
  });
502
557
  ```
503
558
 
559
+ ### Startup observability
560
+
561
+ Set `migrations.onEvent` to observe the lifecycle without parsing logs. Event names: `init_started`, `check_completed`, `apply_started`, `apply_succeeded`, `apply_failed`. Each payload carries `schema`, `mode`, the plan, `ddlExecuted`, `durationMs`, and on failure the `error` / `failedAt` / `notices`. No listener → zero cost.
562
+
504
563
  ### Entity Scope
505
564
 
506
565
  `entities.scope` defines the namespace for entity identity. The unique constraint is `(tenant_id, normalized_name, entity_scope)` — the same entity name in different scopes creates separate entities. This decouples entity identity from `agentId`, allowing multiple agents to share an entity namespace.
@@ -542,6 +601,22 @@ Key indexes: trigram on entity names, GiST on embeddings, unique on `(tenant_id,
542
601
 
543
602
  Also adds `trust_score` column to `session_summaries` (default 0.5, range 0–1).
544
603
 
604
+ ### 005-entity-state-history.sql *(entities enabled)*
605
+
606
+ | Table | Purpose |
607
+ |-------|---------|
608
+ | `entity_state_history` | Temporal state-change log with partial `UNIQUE (tenant, agent, entity, attribute) WHERE valid_to IS NULL` to enforce at-most-one-current. Out-of-order backfill is supported via predecessor/successor overlap checks |
609
+
610
+ Opt-in pipeline (`createAquifer({stateChanges: {enabled, whitelist, confidenceThreshold, timeoutMs, ...}})`) extracts temporal state transitions from session text during `enrich()`; off by default to control LLM cost.
611
+
612
+ ### 006-insights.sql
613
+
614
+ | Table | Purpose |
615
+ |-------|---------|
616
+ | `insights` | Higher-order reflections with TSTZRANGE evidence window, importance, GIN on source_session_ids, HNSW on 1024-dim embedding, and a non-unique partial index on `canonical_key_v2` for the canonical/revision dedup contract |
617
+
618
+ Key indexes: `idx_insights_canonical_v2_active` (partial on active rows with canonical key set), `idx_insights_idempotency_key` (unique on revision key).
619
+
545
620
  ---
546
621
 
547
622
  ## Troubleshooting
@@ -556,6 +631,10 @@ Also adds `trust_score` column to `session_summaries` (default 0.5, range 0–1)
556
631
 
557
632
  **Embedding provider connection refused** — Verify your `AQUIFER_EMBED_BASE_URL` is reachable. For local Ollama, make sure the server is running and the model is pulled (`ollama pull bge-m3`).
558
633
 
634
+ **`AQ_MIGRATION_LOCK_TIMEOUT` on startup** — another process holds the migration advisory lock for `aquifer:<schema>`. Either it is a concurrent `aquifer.init()` racing yours (expected; one will win, the other re-runs and finds `pending=[]`) or a crashed worker left the lock held. Raise `migrations.lockTimeoutMs`, or drop the stale backend via `SELECT pg_terminate_backend(pid) FROM pg_locks WHERE locktype='advisory'` after you have confirmed which pid is dead.
635
+
636
+ **MCP process exits non-zero at startup** — expected when `migrations.mode=apply` and `aquifer.init()` returns `ready=false`. Read the `[aquifer-mcp] startup aborted` line on stderr for the `error.code` / `failedAt`. If you need the old lazy-migrate-on-first-tool-call behaviour instead, set `AQUIFER_MIGRATIONS_MODE=check` (and run `migrate()` out of band) or `=off`.
637
+
559
638
  ---
560
639
 
561
640
  ## Dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadowforge0/aquifer-memory",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. MCP server, CLI, and library API.",
5
5
  "main": "index.js",
6
6
  "files": [