Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,910 @@
|
||||
# Memind Memory V2
|
||||
|
||||
Memory V2 is a facade and policy abstraction layer over existing Memind memory systems.
|
||||
|
||||
It is not a new memory system. It does not introduce new storage, new execution paths, or a replacement runtime.
|
||||
|
||||
## Scope
|
||||
|
||||
Memory V2 wraps the existing:
|
||||
|
||||
- `conversation-memory.mjs`
|
||||
- MySQL memory tables such as `h5_conversation_messages` and `h5_user_memory_items`
|
||||
- capability-driven memory hooks such as `memory_store`, `context_memory`, and `chat_recall`
|
||||
|
||||
It provides:
|
||||
|
||||
- unified memory API: `resolve`, `write`, `compact`
|
||||
- feature-flag control plane
|
||||
- backend adapter layer, with the legacy conversation memory backend first
|
||||
- future optional plugin points for vector memory, extraction, lifecycle, behavior modeling, and policy routing
|
||||
|
||||
It guarantees:
|
||||
|
||||
- zero change to goosed runtime
|
||||
- zero change to PG session behavior
|
||||
- zero change to SSE protocol
|
||||
- zero change to core Portal execution flow
|
||||
- zero blocking on memory backend failure
|
||||
- full backward compatibility with the existing `conversation-memory` system
|
||||
|
||||
## Current Implementation
|
||||
|
||||
The first implementation is intentionally narrow:
|
||||
|
||||
- `memory-v2.mjs` defines the facade.
|
||||
- `memory-v2-runtime.mjs` wires server runtime backends from explicit environment variables.
|
||||
- `memory-v2-plugin-backends.mjs` defines disabled-by-default plugin slots for future backends.
|
||||
- `memory-v2-pgvector.mjs` defines a disabled-by-default semantic memory adapter skeleton.
|
||||
- `memory-v2-pgvector-schema.mjs` defines explicit, manual pgvector schema setup helpers.
|
||||
- `createLegacyMemoryBackend(...)` adapts the existing conversation memory service.
|
||||
- `resolveMemoryV2Policy(...)` maps feature flags into a stable policy object.
|
||||
- `server.mjs` creates Memory V2 from the existing conversation memory service.
|
||||
- `tkmind-proxy.mjs` reads user memories through `memoryV2.resolve(...)` before passing them into the existing session reconcile path.
|
||||
- `/user-memory/v1/remember-recent` writes through `memoryV2.write(...)`.
|
||||
- `/user-memory/v1/sync` compacts through `memoryV2.compact(...)`.
|
||||
- Runtime startup does not create schema.
|
||||
- No goosed or SSE behavior is changed.
|
||||
|
||||
The pgvector adapter does not create tables or generate embeddings. It only defines the adapter contract for a future semantic memory backend and requires explicit `enabled: true`, an injected PostgreSQL pool, and either an input embedding or an injected `embedQuery(...)` function.
|
||||
|
||||
The server runtime uses `createMemoryV2Runtime(...)`. It keeps pgvector dormant unless all of these are true:
|
||||
|
||||
- `MEMORY_VECTOR_ENABLED=1`
|
||||
- `MEMORY_PGVECTOR_DATABASE_URL` is set
|
||||
- `MEMORY_PGVECTOR_EMBEDDING_MODULE` points to a module exporting `embedQuery(query, input)` or a default function
|
||||
|
||||
If any requirement is missing, pgvector reports `available=false` and Memory V2 falls back to the legacy conversation-memory backend. The runtime never reads the app's MySQL `DATABASE_URL` for pgvector.
|
||||
|
||||
When pgvector is available and `MEMORY_BACKEND=pgvector`, only `resolve(...)` uses pgvector. `write(...)` and `compact(...)` continue to use the legacy backend unless a future backend explicitly supports those operations.
|
||||
|
||||
Local development may call `ensurePgvectorMemorySchema(...)` manually to create an empty table. Production must treat the same schema as a controlled migration with backup, approval, and a separate backfill plan.
|
||||
|
||||
## API Contract
|
||||
|
||||
### `resolve(input)`
|
||||
|
||||
Reads memory for the current request.
|
||||
|
||||
Expected input:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "h5 user id",
|
||||
"sessionId": "goosed session id",
|
||||
"query": "current user prompt",
|
||||
"limit": 40
|
||||
}
|
||||
```
|
||||
|
||||
Returns a normalized payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"enabled": true,
|
||||
"skipped": false,
|
||||
"source": "legacy-conversation-memory",
|
||||
"profile": null,
|
||||
"semanticMemories": [],
|
||||
"behaviorSummary": null,
|
||||
"activeGoals": [],
|
||||
"memories": []
|
||||
}
|
||||
```
|
||||
|
||||
### `write(input)`
|
||||
|
||||
Writes or queues memory-related evidence. In the legacy adapter this maps to `saveAndAnalyze(...)`.
|
||||
|
||||
Expected input:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "h5 user id",
|
||||
"sessionId": "goosed session id",
|
||||
"messages": []
|
||||
}
|
||||
```
|
||||
|
||||
### `compact(input)`
|
||||
|
||||
Compacts or analyzes pending user memory. In the legacy adapter this maps to `analyzeUser(...)`.
|
||||
|
||||
Expected input:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "h5 user id",
|
||||
"sessionId": "goosed session id"
|
||||
}
|
||||
```
|
||||
|
||||
### `getStatus()`
|
||||
|
||||
Returns the facade policy and backend contract status for read-only observability.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"backend": "legacy",
|
||||
"selectedBackend": "legacy-conversation-memory",
|
||||
"profileEnabled": true,
|
||||
"eventLogEnabled": true,
|
||||
"vectorEnabled": false,
|
||||
"failOpen": true,
|
||||
"backends": [
|
||||
{
|
||||
"name": "legacy-conversation-memory",
|
||||
"available": true,
|
||||
"supports": {
|
||||
"resolve": true,
|
||||
"write": true,
|
||||
"compact": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Adapter Contract
|
||||
|
||||
Every backend adapter must be optional and fail-open. A backend may implement any subset of the API, but it must not create a new execution path.
|
||||
|
||||
Required field:
|
||||
|
||||
- `name`: stable backend name used by `MEMORY_BACKEND`
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `isAvailable()`: returns `false` when the backend is configured but not usable
|
||||
- `resolve(input)`: returns profile, semantic memories, behavior summary, active goals, or legacy memories
|
||||
- `write(input)`: records or queues memory evidence
|
||||
- `compact(input)`: performs pending extraction, summarization, or lifecycle maintenance
|
||||
|
||||
Backend implementations must not:
|
||||
|
||||
- call goosed directly
|
||||
- mutate PG session state
|
||||
- change SSE payloads
|
||||
- block chat when unavailable
|
||||
- assume they are the only memory backend
|
||||
|
||||
## Plugin Registry
|
||||
|
||||
Memory V2 exposes future plugin slots through read-only backend status. This is a contract registry, not a service integration.
|
||||
|
||||
The default facade includes unavailable placeholders for:
|
||||
|
||||
| Backend | Category | Role |
|
||||
| --- | --- | --- |
|
||||
| `pgvector` | semantic | primary vector store |
|
||||
| `qdrant` | semantic | scale-out vector store |
|
||||
| `weaviate` | semantic | knowledge graph fusion |
|
||||
| `mem0` | extraction | automatic memory generation |
|
||||
| `letta` | lifecycle | long/short-term memory OS |
|
||||
| `neo4j` | behavior | behavior graph |
|
||||
| `redis-streams` | behavior | event tracking |
|
||||
| `langgraph` | policy | routing/reasoning policy |
|
||||
|
||||
These placeholders:
|
||||
|
||||
- report `available=false`
|
||||
- expose declared capability support in `/api/runtime/status`
|
||||
- never open network connections
|
||||
- never create tables or queues
|
||||
- never become selected while unavailable
|
||||
- allow `MEMORY_BACKEND=<future-backend>` to safely fall back to legacy
|
||||
|
||||
A real backend adapter replaces its placeholder only when the required client, credentials, schema, and tests are explicitly provided.
|
||||
|
||||
Backend selection is operation-aware:
|
||||
|
||||
- `resolve` may use a semantic backend such as pgvector.
|
||||
- `write` must fall back to the first available backend that supports writes, usually legacy conversation memory.
|
||||
- `compact` must fall back to the first available backend that supports compaction, usually legacy conversation memory.
|
||||
|
||||
This allows semantic retrieval to be canaried without replacing existing memory capture or compaction.
|
||||
|
||||
## Backend Contract Gate
|
||||
|
||||
`memory-v2-backend-contract.mjs` defines the adapter contract gate for current and future backends.
|
||||
|
||||
Every backend must:
|
||||
|
||||
- have a stable lowercase kebab-case `name`
|
||||
- implement at least one of `resolve`, `write`, or `compact`
|
||||
- expose `isAvailable()` or default to available
|
||||
- expose an unavailable reason when `isAvailable() === false`
|
||||
- use a known plugin category when `category` is present
|
||||
- keep feature flags in `SCREAMING_SNAKE_CASE` when `flag` is present
|
||||
|
||||
The default gate checks legacy plus all disabled plugin slots:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts
|
||||
```
|
||||
|
||||
End-to-end local validation can now be run from one entrypoint:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-stack -- \
|
||||
--base-url http://127.0.0.1:18081 \
|
||||
--expect-backend pgvector \
|
||||
--expect-selected-backend pgvector
|
||||
```
|
||||
|
||||
This aggregate check runs:
|
||||
|
||||
- backend contract validation
|
||||
- synthetic Memory V2 health validation
|
||||
- app-level `/api/runtime/status` canary
|
||||
- real cookie-backed `register/login -> agent/start -> agent/runs -> SSE -> remember-recent -> sync` session flow
|
||||
- backend smoke probes for every configured non-legacy backend
|
||||
|
||||
Unconfigured external backends are reported explicitly as `not_configured` instead of failing the whole check.
|
||||
|
||||
To see exactly which env vars are still missing for any backend, run:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-config -- --backend qdrant
|
||||
npm run check:memory-v2-config -- --backend letta --format shell
|
||||
```
|
||||
|
||||
`json` mode reports missing keys and readiness. `shell` mode prints a copy-paste export template for the selected backend.
|
||||
|
||||
When replacing the pgvector placeholder with the real disabled adapter:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts -- --include-pgvector-adapter
|
||||
```
|
||||
|
||||
Future adapters for Qdrant, Weaviate, Mem0, Letta, Neo4j, Redis Streams, or LangGraph must pass this contract before being wired into `createMemoryV2Runtime(...)`.
|
||||
|
||||
## Current Validation Baseline
|
||||
|
||||
As of the current Memory V2 rollout branch, local validation has already proven:
|
||||
|
||||
- `memoryV2.resolve(...)` can select `pgvector`
|
||||
- `memoryV2.write(...)` and `memoryV2.compact(...)` remain legacy-first
|
||||
- `/user-memory/v1/remember-recent` and `/user-memory/v1/sync` still reconcile back into the active session
|
||||
- the live Portal path `agent/start -> agent/runs -> SSE Finish -> session detail` continues to work under Memory V2
|
||||
|
||||
That baseline is now captured by `npm run check:memory-v2-session` and included in `npm run check:memory-v2-stack`.
|
||||
|
||||
## Backend Scaffold
|
||||
|
||||
`memory-v2-adapter-scaffold.mjs` renders disabled-by-default backend adapter templates that satisfy the contract gate.
|
||||
|
||||
Dry-run a Qdrant adapter:
|
||||
|
||||
```bash
|
||||
npm run scaffold:memory-v2-backend -- \
|
||||
--name qdrant \
|
||||
--category semantic \
|
||||
--role scale-out-vector-store \
|
||||
--capability resolve
|
||||
```
|
||||
|
||||
Write a Mem0 adapter scaffold:
|
||||
|
||||
```bash
|
||||
npm run scaffold:memory-v2-backend -- \
|
||||
--name mem0 \
|
||||
--category extraction \
|
||||
--role automatic-memory-generation \
|
||||
--capability write \
|
||||
--capability compact \
|
||||
--write
|
||||
```
|
||||
|
||||
The scaffold command refuses to overwrite existing files. After generating a real adapter, run:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts
|
||||
```
|
||||
|
||||
Then add focused adapter tests before wiring the adapter into `createMemoryV2Runtime(...)`.
|
||||
|
||||
## Feature Flags
|
||||
|
||||
`MEMORY_ENABLED`
|
||||
|
||||
Global Memory V2 switch. If unset, it follows the existing `USER_CONVERSATION_MEMORY_ENABLED` behavior for backward compatibility.
|
||||
|
||||
`MEMORY_PROFILE_ENABLED`
|
||||
|
||||
Controls whether resolved profile payloads may be returned.
|
||||
|
||||
`MEMORY_EVENT_LOG_ENABLED`
|
||||
|
||||
Controls memory write/event capture. When disabled, `write(...)` returns a skipped result without touching the backend.
|
||||
|
||||
`MEMORY_VECTOR_ENABLED`
|
||||
|
||||
Reserved for future vector backends. It is disabled by default.
|
||||
|
||||
`MEMORY_BACKEND`
|
||||
|
||||
Preferred backend name. The current default is `legacy`.
|
||||
|
||||
`MEMORY_FAIL_OPEN`
|
||||
|
||||
Defaults to enabled. Backend failures return degraded empty payloads rather than blocking chat.
|
||||
|
||||
`MEMORY_PGVECTOR_DATABASE_URL`
|
||||
|
||||
Dedicated PostgreSQL connection string for Memory V2 semantic memory. It must not point at the MySQL business database and is ignored unless `MEMORY_VECTOR_ENABLED=1`.
|
||||
|
||||
`MEMORY_PGVECTOR_EMBEDDING_MODULE`
|
||||
|
||||
Optional module path for pgvector runtime retrieval. The module must export `embedQuery(query, input)` or a default async function. Without this module, runtime pgvector retrieval stays unavailable even when a PostgreSQL URL is present.
|
||||
|
||||
`MEMORY_PGVECTOR_TABLE`
|
||||
|
||||
Optional table name for pgvector retrieval. Defaults to `memory_embeddings`.
|
||||
|
||||
`MEMORY_PGVECTOR_POOL_MAX`
|
||||
|
||||
Optional PostgreSQL pool size for Memory V2 pgvector retrieval. Defaults to `5`.
|
||||
|
||||
## Current Integration Points
|
||||
|
||||
Memory V2 is only allowed to sit above existing memory code.
|
||||
|
||||
Read path:
|
||||
|
||||
```text
|
||||
tkmind-proxy
|
||||
-> memoryV2.resolve
|
||||
-> legacy conversation-memory backend
|
||||
-> existing reconcileAgentSession
|
||||
-> existing buildSessionMemoryEntries
|
||||
-> existing harness remember/bootstrap
|
||||
```
|
||||
|
||||
Write path:
|
||||
|
||||
```text
|
||||
/user-memory/v1/remember-recent
|
||||
-> memoryV2.write
|
||||
-> legacy conversation-memory saveAndAnalyze
|
||||
```
|
||||
|
||||
Compact path:
|
||||
|
||||
```text
|
||||
/user-memory/v1/sync
|
||||
-> memoryV2.compact
|
||||
-> legacy conversation-memory analyzeUser
|
||||
```
|
||||
|
||||
If `MEMORY_ENABLED=0`, Memory V2 returns skipped/empty results and does not touch the legacy backend. The session reconcile path still runs for non-memory context such as sandbox guidance and time anchors.
|
||||
|
||||
Runtime observability:
|
||||
|
||||
```text
|
||||
/api/runtime/status
|
||||
-> memory: memoryV2.getStatus()
|
||||
```
|
||||
|
||||
This is read-only and exists only to make rollout/debugging visible.
|
||||
|
||||
Local health gate:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts
|
||||
npm run check:memory-v2 -- --require-enabled --expect-backend legacy
|
||||
```
|
||||
|
||||
Local app canary against a running Portal service:
|
||||
|
||||
```bash
|
||||
npm run canary:memory-v2-app -- \
|
||||
--base-url http://127.0.0.1:8081 \
|
||||
--require-enabled \
|
||||
--expect-backend legacy
|
||||
```
|
||||
|
||||
For pgvector canary, run the app with explicit Memory V2 env and assert both configured and selected backends:
|
||||
|
||||
```bash
|
||||
npm run canary:memory-v2-app -- \
|
||||
--base-url http://127.0.0.1:18081 \
|
||||
--require-enabled \
|
||||
--require-target-healthy \
|
||||
--expect-backend pgvector \
|
||||
--expect-selected-backend pgvector
|
||||
```
|
||||
|
||||
Production rollout details live in [production-rollout-runbook.md](production-rollout-runbook.md).
|
||||
|
||||
## Backend Plugin Slots
|
||||
|
||||
These are optional future backends. None is required for Memory V2 phase one.
|
||||
|
||||
Semantic memory:
|
||||
|
||||
- pgvector as the primary vector option
|
||||
- Qdrant as a scale-out option
|
||||
- Weaviate for knowledge graph fusion
|
||||
|
||||
Memory extraction:
|
||||
|
||||
- Mem0 for automatic memory generation
|
||||
|
||||
Memory lifecycle management:
|
||||
|
||||
- Letta for long-term and short-term memory operating semantics
|
||||
|
||||
Behavior and user model:
|
||||
|
||||
- Neo4j for behavior graph modeling
|
||||
- Redis Streams for event tracking
|
||||
|
||||
Memory policy engine:
|
||||
|
||||
- LangGraph for routing and reasoning policy
|
||||
|
||||
## Integration Rule
|
||||
|
||||
Future integration must use existing Memind injection points. The preferred path is:
|
||||
|
||||
```text
|
||||
tkmind-proxy / agent run
|
||||
-> Memory V2 resolve
|
||||
-> existing session reconcile
|
||||
-> existing buildSessionMemoryEntries
|
||||
-> existing harness remember/bootstrap
|
||||
```
|
||||
|
||||
Do not add direct goosed changes, PG session changes, SSE protocol changes, or new blocking calls in the reply path.
|
||||
|
||||
## Rollout Checklist
|
||||
|
||||
Phase 1 rollout:
|
||||
|
||||
1. Keep `MEMORY_BACKEND=legacy`.
|
||||
2. Enable `MEMORY_ENABLED=1`.
|
||||
3. Keep `MEMORY_VECTOR_ENABLED=0`.
|
||||
4. Verify `/api/runtime/status` reports `memory.enabled=true` and `selectedBackend=legacy-conversation-memory`.
|
||||
5. Run targeted tests before release.
|
||||
|
||||
Rollback:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=0
|
||||
```
|
||||
|
||||
Rollback must stop Memory V2 reads/writes while preserving non-memory session reconcile behavior.
|
||||
|
||||
Future backend rollout:
|
||||
|
||||
1. Add the backend adapter behind `MEMORY_BACKEND=<name>`.
|
||||
2. Replace the matching unavailable placeholder with the real adapter only inside Memory V2 wiring.
|
||||
3. Verify `getStatus().backends[]` reports capability support.
|
||||
4. Keep backend disabled or unavailable by default.
|
||||
5. Run local retrieval/write tests without changing goosed/SSE.
|
||||
6. Enable for a narrow canary only after legacy fallback remains green.
|
||||
|
||||
## pgvector Adapter Skeleton
|
||||
|
||||
`memory-v2-pgvector.mjs` is the first semantic-memory backend skeleton.
|
||||
|
||||
Current constraints:
|
||||
|
||||
- Disabled by default.
|
||||
- Requires an injected PostgreSQL pool with a `query(sql, params)` method.
|
||||
- Requires an explicit embedding via `resolve({ embedding })` or an injected `embedQuery(query, input)` function.
|
||||
- Server runtime only creates the PostgreSQL pool when vector retrieval, a PostgreSQL URL, and an embedding module are all configured.
|
||||
- Performs read-only vector lookup.
|
||||
- Does not create schema automatically.
|
||||
- Does not migrate or copy MySQL memory rows.
|
||||
- Does not call goosed, SSE, or Portal execution APIs.
|
||||
- Falls back through Memory V2 when unavailable.
|
||||
|
||||
Manual local schema helper:
|
||||
|
||||
```js
|
||||
import { ensurePgvectorMemorySchema } from './memory-v2-pgvector-schema.mjs';
|
||||
|
||||
await ensurePgvectorMemorySchema(pool, {
|
||||
tableName: 'memory_embeddings',
|
||||
dimensions: 1536,
|
||||
createExtension: true,
|
||||
createVectorIndex: false,
|
||||
});
|
||||
```
|
||||
|
||||
Dry-run CLI:
|
||||
|
||||
```bash
|
||||
node scripts/setup-memory-v2-pgvector-schema.mjs \
|
||||
--table memory_embeddings \
|
||||
--dimensions 1536 \
|
||||
--create-extension
|
||||
```
|
||||
|
||||
Apply CLI:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
node scripts/setup-memory-v2-pgvector-schema.mjs \
|
||||
--apply \
|
||||
--table memory_embeddings \
|
||||
--dimensions 1536 \
|
||||
--create-extension
|
||||
```
|
||||
|
||||
The CLI defaults to dry-run and never reads the existing MySQL `DATABASE_URL`. `--apply` requires an explicit PostgreSQL URL environment variable.
|
||||
|
||||
Expected table shape:
|
||||
|
||||
```sql
|
||||
CREATE TABLE memory_embeddings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
embedding VECTOR(1536) NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'fact',
|
||||
source_memory_id TEXT,
|
||||
source_session_id TEXT,
|
||||
source_message_id TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
Rollout rule:
|
||||
|
||||
Keep `MEMORY_BACKEND=legacy` until pgvector schema, embedding generation, backfill, and canary verification are designed separately.
|
||||
|
||||
Production migration considerations:
|
||||
|
||||
1. Confirm pgvector is installed on the target PostgreSQL server.
|
||||
2. Run `CREATE EXTENSION IF NOT EXISTS vector` only through the approved DB migration path.
|
||||
3. Create the empty `memory_embeddings` table first; do not backfill in the same release.
|
||||
4. Keep `MEMORY_BACKEND=legacy` after table creation.
|
||||
5. Build a separate backfill job from `h5_user_memory_items` into pgvector with idempotent checkpoints.
|
||||
6. Verify row counts, embedding dimensions, and retrieval quality before canarying `MEMORY_BACKEND=pgvector`.
|
||||
7. Rollback for the app remains `MEMORY_BACKEND=legacy` or `MEMORY_ENABLED=0`; database rollback should not delete populated memory rows without an explicit retention decision.
|
||||
|
||||
## pgvector Backfill Skeleton
|
||||
|
||||
`memory-v2-pgvector-backfill.mjs` defines the future MySQL-to-pgvector backfill contract.
|
||||
|
||||
Source:
|
||||
|
||||
```text
|
||||
h5_user_memory_items
|
||||
WHERE status = 'active'
|
||||
ORDER BY updated_at ASC, id ASC
|
||||
```
|
||||
|
||||
Destination:
|
||||
|
||||
```text
|
||||
memory_embeddings.source_memory_id = h5_user_memory_items.id
|
||||
```
|
||||
|
||||
The `source_memory_id` unique index makes backfill idempotent. Re-running the same batch updates content, embedding, type, source pointers, metadata, and `updated_at`.
|
||||
|
||||
Current constraints:
|
||||
|
||||
- Defaults to dry-run.
|
||||
- Dry-run reads MySQL only and does not call embedding or PostgreSQL.
|
||||
- Apply requires `pgPool.query(...)`.
|
||||
- Apply requires an explicit `embedMemory(memory) => number[]`.
|
||||
- Backfill is paginated with `{ updatedAt, id }` checkpoints.
|
||||
- Backfill does not change `MEMORY_BACKEND`.
|
||||
- Backfill does not create schema; run schema setup separately first.
|
||||
|
||||
Dry-run CLI:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKFILL_MYSQL_URL='mysql://...' \
|
||||
node scripts/backfill-memory-v2-pgvector.mjs \
|
||||
--limit 100 \
|
||||
--cursor-updated-at 0 \
|
||||
--cursor-id ''
|
||||
```
|
||||
|
||||
Apply CLI:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKFILL_MYSQL_URL='mysql://...' \
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
node scripts/backfill-memory-v2-pgvector.mjs \
|
||||
--apply \
|
||||
--limit 100 \
|
||||
--embedding-module ./scripts/embed-memory-v2-pgvector.mjs
|
||||
```
|
||||
|
||||
The apply command requires the embedding module to export `embedMemory(memory)` or a default function. The CLI intentionally uses `MEMORY_BACKFILL_MYSQL_URL` instead of the app's normal MySQL env so production backfills are explicit and auditable.
|
||||
|
||||
Production backfill sequence:
|
||||
|
||||
1. Run schema migration and keep `MEMORY_BACKEND=legacy`.
|
||||
2. Run dry-run batches and inspect counts/checkpoints.
|
||||
3. Run apply batches with a fixed embedding model and recorded dimensions.
|
||||
4. Store the last checkpoint externally after each successful batch.
|
||||
5. Compare MySQL active memory count against pgvector distinct `source_memory_id` count.
|
||||
6. Validate retrieval quality on a canary user.
|
||||
7. Only then consider `MEMORY_BACKEND=pgvector` canary.
|
||||
|
||||
## pgvector Smoke Test
|
||||
|
||||
`memory-v2-pgvector-smoke.mjs` and `scripts/smoke-memory-v2-pgvector.mjs` provide a local pgvector read/write smoke test.
|
||||
|
||||
The smoke test:
|
||||
|
||||
- uses synthetic `memory-v2-smoke-*` rows only
|
||||
- inserts deterministic vectors
|
||||
- verifies the nearest vector is returned first through the Memory V2 pgvector adapter
|
||||
- cleans synthetic rows by default
|
||||
- never runs from app startup
|
||||
- never reads the MySQL `DATABASE_URL`
|
||||
|
||||
Existing-schema smoke:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
npm run smoke:memory-v2-pgvector
|
||||
```
|
||||
|
||||
Local setup smoke that also creates an empty schema:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
npm run smoke:memory-v2-pgvector -- \
|
||||
--create-schema \
|
||||
--create-extension \
|
||||
--table memory_embeddings \
|
||||
--dimensions 3
|
||||
```
|
||||
|
||||
Production usage is limited to post-migration verification. Do not use the smoke script as a migration mechanism unless the migration plan explicitly approves `--create-schema`.
|
||||
|
||||
## Local Canary Env
|
||||
|
||||
`docs/memory-v2/local-canary.env.example` contains copyable environment examples for:
|
||||
|
||||
- legacy no-op facade canary
|
||||
- pgvector local semantic canary
|
||||
- Qdrant and Weaviate read-only canaries
|
||||
- Mem0, Letta, Neo4j, Redis Streams, and LangGraph external canaries
|
||||
|
||||
For local semantic smoke only, `scripts/embed-memory-v2-local-hash.mjs` exports a deterministic hash embedding function:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_EMBEDDING_MODULE=./scripts/embed-memory-v2-local-hash.mjs
|
||||
MEMORY_V2_LOCAL_EMBEDDING_DIMENSIONS=3
|
||||
```
|
||||
|
||||
This module is intentionally simple and deterministic. It is useful for local plumbing checks, but not for production retrieval quality.
|
||||
|
||||
## Qdrant Read-Only Adapter
|
||||
|
||||
`memory-v2-qdrant.mjs` is the first external plugin adapter beyond pgvector.
|
||||
|
||||
Current constraints:
|
||||
|
||||
- No Qdrant SDK is imported.
|
||||
- The adapter uses a minimal read-only HTTP client wrapper.
|
||||
- It only calls Qdrant search when `MEMORY_QDRANT_ENABLED=1`, `MEMORY_QDRANT_URL`, and `MEMORY_QDRANT_EMBEDDING_MODULE` are configured.
|
||||
- It never creates collections.
|
||||
- It never writes or upserts points.
|
||||
- `write(...)` and `compact(...)` continue to use legacy memory.
|
||||
|
||||
Runtime read-only configuration:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=qdrant
|
||||
MEMORY_QDRANT_ENABLED=1
|
||||
MEMORY_QDRANT_URL='http://127.0.0.1:6333'
|
||||
MEMORY_QDRANT_COLLECTION='memind_memory'
|
||||
MEMORY_QDRANT_EMBEDDING_MODULE='./scripts/embed-memory-v2-qdrant.mjs'
|
||||
```
|
||||
|
||||
Read-only smoke:
|
||||
|
||||
```bash
|
||||
MEMORY_QDRANT_URL='http://127.0.0.1:6333' \
|
||||
npm run smoke:memory-v2-qdrant -- \
|
||||
--collection memind_memory \
|
||||
--vector 1,0,0 \
|
||||
--limit 1
|
||||
```
|
||||
|
||||
Expected without embedding module:
|
||||
|
||||
```text
|
||||
selectedBackend=legacy-conversation-memory
|
||||
qdrant.available=false
|
||||
qdrant.reason=embedding_module_not_configured
|
||||
```
|
||||
|
||||
Expected with embedding module and reachable Qdrant:
|
||||
|
||||
```text
|
||||
selectedBackend=qdrant
|
||||
write_uses_legacy=true
|
||||
compact_uses_legacy=true
|
||||
```
|
||||
|
||||
## Mem0 Extraction Adapter
|
||||
|
||||
`memory-v2-mem0.mjs` defines the Memory Extraction backend boundary.
|
||||
|
||||
Current constraints:
|
||||
|
||||
- No Mem0 SDK is imported.
|
||||
- Runtime uses a lightweight HTTP client only when `MEMORY_MEM0_ENABLED=1` and `MEMORY_MEM0_API_KEY` are configured.
|
||||
- HTTP paths are configurable with `MEMORY_MEM0_WRITE_PATH` and `MEMORY_MEM0_COMPACT_PATH`.
|
||||
- Without a full configuration, status reports `mem0.available=false` with a concrete reason and Memory V2 falls back to legacy.
|
||||
- `MEMORY_BACKEND=mem0` does not affect `resolve(...)`.
|
||||
- `write(...)` and `compact(...)` can use Mem0 when explicitly selected; legacy remains the default and rollback path.
|
||||
|
||||
Runtime configuration:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=mem0
|
||||
MEMORY_MEM0_ENABLED=1
|
||||
MEMORY_MEM0_API_KEY='...'
|
||||
MEMORY_MEM0_PROJECT_ID='memind_project'
|
||||
MEMORY_MEM0_BASE_URL='https://api.mem0.ai'
|
||||
```
|
||||
|
||||
Smoke:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend mem0 --operation write
|
||||
```
|
||||
|
||||
## Letta Lifecycle Adapter
|
||||
|
||||
`memory-v2-letta.mjs` defines the Memory Lifecycle Management backend boundary.
|
||||
|
||||
Current constraints:
|
||||
|
||||
- No Letta SDK is imported.
|
||||
- Runtime uses a lightweight HTTP client only when `MEMORY_LETTA_ENABLED=1`, `MEMORY_LETTA_API_KEY`, and `MEMORY_LETTA_AGENT_ID` are configured.
|
||||
- HTTP paths are configurable with `MEMORY_LETTA_RESOLVE_PATH`, `MEMORY_LETTA_WRITE_PATH`, and `MEMORY_LETTA_COMPACT_PATH`.
|
||||
- Without a full configuration, status reports `letta.available=false` with a concrete reason and Memory V2 falls back to legacy.
|
||||
- `resolve(...)`, `write(...)`, and `compact(...)` can use Letta when explicitly selected.
|
||||
|
||||
Runtime configuration:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=letta
|
||||
MEMORY_LETTA_ENABLED=1
|
||||
MEMORY_LETTA_API_KEY='...'
|
||||
MEMORY_LETTA_PROJECT_ID='memind_project'
|
||||
MEMORY_LETTA_AGENT_ID='agent_1'
|
||||
MEMORY_LETTA_BASE_URL='https://api.letta.com'
|
||||
```
|
||||
|
||||
Smoke:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend letta --operation resolve
|
||||
```
|
||||
|
||||
Do not enable Letta as the selected backend in production until sampling, retention, conflict resolution, and rollback rules are defined for long/short-term lifecycle ownership.
|
||||
|
||||
## Remaining External Adapters
|
||||
|
||||
The remaining optional backends now have runtime-wired clients:
|
||||
|
||||
| Backend | File | Category | Runtime role |
|
||||
|---|---|---|---|
|
||||
| Weaviate | `memory-v2-weaviate.mjs` | semantic | GraphQL vector search |
|
||||
| Neo4j | `memory-v2-neo4j.mjs` | behavior | HTTP transaction behavior graph |
|
||||
| Redis Streams | `memory-v2-redis-streams.mjs` | behavior | Redis `XADD` event tracking |
|
||||
| LangGraph | `memory-v2-langgraph.mjs` | policy | HTTP policy resolve |
|
||||
|
||||
Current constraints:
|
||||
|
||||
- Weaviate and LangGraph use lightweight HTTP clients.
|
||||
- Neo4j uses the HTTP transactional endpoint; `neo4j-driver` is not introduced.
|
||||
- Redis Streams uses the existing `redis` dependency.
|
||||
- No default runtime path opens network connections; each adapter is dormant unless its feature flag and required config are present.
|
||||
- If configured incompletely, each adapter reports `available=false` with a concrete reason and Memory V2 falls back to legacy.
|
||||
- `redis-streams` is write-only and cannot affect `resolve(...)`.
|
||||
|
||||
Runtime flags:
|
||||
|
||||
```bash
|
||||
MEMORY_WEAVIATE_ENABLED=1
|
||||
MEMORY_WEAVIATE_URL='https://weaviate.example'
|
||||
MEMORY_WEAVIATE_COLLECTION='MemindMemory'
|
||||
MEMORY_WEAVIATE_EMBEDDING_MODULE='./scripts/embed-memory-v2-weaviate.mjs'
|
||||
|
||||
MEMORY_NEO4J_ENABLED=1
|
||||
MEMORY_NEO4J_HTTP_URL='http://localhost:7474'
|
||||
MEMORY_NEO4J_USER='neo4j'
|
||||
MEMORY_NEO4J_PASSWORD='...'
|
||||
MEMORY_NEO4J_DATABASE='neo4j'
|
||||
|
||||
MEMORY_REDIS_STREAMS_ENABLED=1
|
||||
MEMORY_REDIS_STREAMS_URL='redis://localhost:6379'
|
||||
MEMORY_REDIS_STREAMS_STREAM='memind:memory-events'
|
||||
|
||||
MEMORY_LANGGRAPH_ENABLED=1
|
||||
MEMORY_LANGGRAPH_URL='https://langgraph.example'
|
||||
MEMORY_LANGGRAPH_POLICY_ID='memory_policy'
|
||||
```
|
||||
|
||||
Smoke examples:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend weaviate --operation resolve
|
||||
npm run smoke:memory-v2-external -- --backend neo4j --operation write
|
||||
npm run smoke:memory-v2-external -- --backend redis-streams --operation write
|
||||
npm run smoke:memory-v2-external -- --backend langgraph --operation resolve
|
||||
```
|
||||
|
||||
Future production canaries must define per-backend ownership boundaries before any of these adapters can take over `resolve(...)` or `write(...)`.
|
||||
|
||||
## Verification
|
||||
|
||||
Targeted regression command:
|
||||
|
||||
```bash
|
||||
node --test memory-v2.test.mjs conversation-memory.test.mjs user-memory-profile.test.mjs tkmind-proxy.test.mjs
|
||||
node --test memory-v2-adapter-scaffold.test.mjs
|
||||
node --test memory-v2-backend-contract.test.mjs
|
||||
node --test memory-v2-health.test.mjs
|
||||
node --test memory-v2-runtime.test.mjs
|
||||
node --test memory-v2-plugin-backends.test.mjs
|
||||
node --test memory-v2-pgvector.test.mjs
|
||||
node --test memory-v2-pgvector-schema.test.mjs
|
||||
node --test memory-v2-pgvector-backfill.test.mjs
|
||||
node --test memory-v2-pgvector-smoke.test.mjs
|
||||
node --test memory-v2-qdrant.test.mjs
|
||||
node --test memory-v2-mem0.test.mjs
|
||||
node --test memory-v2-letta.test.mjs
|
||||
node --test memory-v2-external-adapters.test.mjs
|
||||
node --test scripts/check-memory-v2-contracts.test.mjs
|
||||
node --test scripts/check-memory-v2-health.test.mjs
|
||||
node --test scripts/setup-memory-v2-pgvector-schema.test.mjs
|
||||
node --test scripts/backfill-memory-v2-pgvector.test.mjs
|
||||
node --test scripts/scaffold-memory-v2-backend.test.mjs
|
||||
node --test scripts/smoke-memory-v2-pgvector.test.mjs
|
||||
node --test scripts/smoke-memory-v2-qdrant.test.mjs
|
||||
node --test scripts/smoke-memory-v2-external.test.mjs
|
||||
node --check server.mjs
|
||||
```
|
||||
|
||||
The targeted tests cover:
|
||||
|
||||
- legacy backend adaptation
|
||||
- backend adapter scaffold generation
|
||||
- backend adapter contract gate
|
||||
- local Memory V2 health gate checks
|
||||
- runtime pgvector wiring only when explicitly configured
|
||||
- configured backend selection and fallback
|
||||
- future plugin slot visibility without runtime selection
|
||||
- profile suppression via policy
|
||||
- fail-open resolve behavior
|
||||
- event-log write gating
|
||||
- session start memory injection through Memory V2
|
||||
- disabled Memory V2 not touching legacy memory or injecting stored memories
|
||||
- runtime status exposing Memory V2 policy and backend contract
|
||||
- reply path passing the current user prompt into `resolve(...)` while keeping the existing `/reply` execution path
|
||||
- pgvector adapter disabled-by-default behavior and parameterized semantic lookup
|
||||
- Qdrant read-only HTTP client, smoke CLI, and fallback behavior
|
||||
- Mem0 extraction HTTP client and runtime fallback behavior
|
||||
- Letta lifecycle HTTP client and runtime fallback behavior
|
||||
- Weaviate, Neo4j, Redis Streams, and LangGraph runtime client behavior
|
||||
- manual pgvector schema SQL generation and execution ordering
|
||||
- pgvector schema CLI dry-run and explicit apply guard
|
||||
- pgvector backfill dry-run, checkpoint pagination, and idempotent upsert contract
|
||||
- pgvector backfill CLI dry-run and explicit apply guard
|
||||
- pgvector local smoke fixture and CLI guards
|
||||
@@ -0,0 +1,69 @@
|
||||
# Memory V2 local canary example.
|
||||
# Copy the variables you need into your local shell or .env.
|
||||
# Do not use the local hash embedding module for production retrieval quality.
|
||||
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_FAIL_OPEN=1
|
||||
|
||||
# Legacy-safe default canary.
|
||||
MEMORY_BACKEND=legacy
|
||||
|
||||
# pgvector local canary.
|
||||
# MEMORY_BACKEND=pgvector
|
||||
# MEMORY_VECTOR_ENABLED=1
|
||||
# Homebrew PostgreSQL local default on John's machine:
|
||||
# MEMORY_PGVECTOR_DATABASE_URL=postgresql://john@127.0.0.1:5432/memind_memory
|
||||
# Password-based PostgreSQL example:
|
||||
# MEMORY_PGVECTOR_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/memind_memory
|
||||
# MEMORY_PGVECTOR_TABLE=memory_embeddings
|
||||
# MEMORY_PGVECTOR_EMBEDDING_MODULE=./scripts/embed-memory-v2-local-hash.mjs
|
||||
# MEMORY_V2_LOCAL_EMBEDDING_DIMENSIONS=3
|
||||
|
||||
# Qdrant read-only canary.
|
||||
# MEMORY_BACKEND=qdrant
|
||||
# MEMORY_QDRANT_ENABLED=1
|
||||
# MEMORY_QDRANT_URL=http://127.0.0.1:6333
|
||||
# MEMORY_QDRANT_COLLECTION=memind_memory
|
||||
# MEMORY_QDRANT_EMBEDDING_MODULE=./scripts/embed-memory-v2-local-hash.mjs
|
||||
|
||||
# Weaviate read-only canary.
|
||||
# MEMORY_BACKEND=weaviate
|
||||
# MEMORY_WEAVIATE_ENABLED=1
|
||||
# MEMORY_WEAVIATE_URL=http://127.0.0.1:8080
|
||||
# MEMORY_WEAVIATE_COLLECTION=MemindMemory
|
||||
# MEMORY_WEAVIATE_EMBEDDING_MODULE=./scripts/embed-memory-v2-local-hash.mjs
|
||||
|
||||
# Mem0 write/compact canary.
|
||||
# MEMORY_BACKEND=mem0
|
||||
# MEMORY_MEM0_ENABLED=1
|
||||
# MEMORY_MEM0_API_KEY=replace-me
|
||||
# MEMORY_MEM0_PROJECT_ID=memind_project
|
||||
# MEMORY_MEM0_BASE_URL=https://api.mem0.ai
|
||||
|
||||
# Letta lifecycle canary.
|
||||
# MEMORY_BACKEND=letta
|
||||
# MEMORY_LETTA_ENABLED=1
|
||||
# MEMORY_LETTA_API_KEY=replace-me
|
||||
# MEMORY_LETTA_AGENT_ID=agent_1
|
||||
# MEMORY_LETTA_PROJECT_ID=memind_project
|
||||
# MEMORY_LETTA_BASE_URL=https://api.letta.com
|
||||
|
||||
# Neo4j behavior graph canary.
|
||||
# MEMORY_BACKEND=neo4j
|
||||
# MEMORY_NEO4J_ENABLED=1
|
||||
# MEMORY_NEO4J_HTTP_URL=http://127.0.0.1:7474
|
||||
# MEMORY_NEO4J_USER=neo4j
|
||||
# MEMORY_NEO4J_PASSWORD=replace-me
|
||||
# MEMORY_NEO4J_DATABASE=neo4j
|
||||
|
||||
# Redis Streams event canary.
|
||||
# MEMORY_BACKEND=redis-streams
|
||||
# MEMORY_REDIS_STREAMS_ENABLED=1
|
||||
# MEMORY_REDIS_STREAMS_URL=redis://127.0.0.1:6379
|
||||
# MEMORY_REDIS_STREAMS_STREAM=memind:memory-events
|
||||
|
||||
# LangGraph policy canary.
|
||||
# MEMORY_BACKEND=langgraph
|
||||
# MEMORY_LANGGRAPH_ENABLED=1
|
||||
# MEMORY_LANGGRAPH_URL=http://127.0.0.1:2024
|
||||
# MEMORY_LANGGRAPH_POLICY_ID=memory_policy
|
||||
@@ -0,0 +1,335 @@
|
||||
# Memory V2 Production Rollout Runbook
|
||||
|
||||
Memory V2 is a facade and policy layer. Production rollout must not introduce new goosed, PG session, SSE, or Portal execution paths.
|
||||
|
||||
## Release Gates
|
||||
|
||||
Run the normal repository release checks first:
|
||||
|
||||
```bash
|
||||
bash scripts/check-release-ready.sh
|
||||
```
|
||||
|
||||
Run the Memory V2 backend contract gate:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts
|
||||
```
|
||||
|
||||
Run the Memory V2 health gate:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2 -- --require-enabled --expect-backend legacy
|
||||
```
|
||||
|
||||
Expected phase-one result:
|
||||
|
||||
- `ok=true`
|
||||
- contract gate reports unique backend names and legacy backend present
|
||||
- `summary.enabled=true`
|
||||
- `summary.backend=legacy`
|
||||
- `summary.selectedBackend=legacy-conversation-memory`
|
||||
- `fail_open` passes
|
||||
- `legacy_write_compact_supported` passes
|
||||
- `write_uses_legacy` passes
|
||||
- `compact_uses_legacy` passes
|
||||
|
||||
When validating the real pgvector adapter before canary:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts -- --include-pgvector-adapter
|
||||
```
|
||||
|
||||
Future backend adapter rule:
|
||||
|
||||
```bash
|
||||
npm run scaffold:memory-v2-backend -- \
|
||||
--name qdrant \
|
||||
--category semantic \
|
||||
--capability resolve
|
||||
```
|
||||
|
||||
Use the scaffold as the starting point, add focused adapter tests, then pass `npm run check:memory-v2-contracts` before wiring any external SDK into runtime. Do not wire external services directly into `server.mjs`, `tkmind-proxy.mjs`, goosed, SSE, or Portal execution code.
|
||||
|
||||
## Qdrant Read-Only Phase
|
||||
|
||||
The Qdrant adapter may be visible in runtime status before user traffic is canaried.
|
||||
|
||||
Fallback-only environment:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=qdrant
|
||||
MEMORY_QDRANT_ENABLED=1
|
||||
MEMORY_QDRANT_URL='http://qdrant.example:6333'
|
||||
MEMORY_QDRANT_COLLECTION='memind_memory'
|
||||
```
|
||||
|
||||
Expected without embedding module:
|
||||
|
||||
- `memory.backend=qdrant`
|
||||
- `memory.selectedBackend=legacy-conversation-memory`
|
||||
- `qdrant.available=false`
|
||||
- `qdrant.reason=embedding_module_not_configured`
|
||||
- `write_uses_legacy` still passes
|
||||
- `compact_uses_legacy` still passes
|
||||
|
||||
Read-only smoke:
|
||||
|
||||
```bash
|
||||
MEMORY_QDRANT_URL='http://qdrant.example:6333' \
|
||||
npm run smoke:memory-v2-qdrant -- \
|
||||
--collection memind_memory \
|
||||
--vector 1,0,0 \
|
||||
--limit 1
|
||||
```
|
||||
|
||||
The smoke command sends a Qdrant search request only. It must not create collections, write points, or run from app startup.
|
||||
|
||||
Runtime read-only canary requires:
|
||||
|
||||
```bash
|
||||
MEMORY_QDRANT_EMBEDDING_MODULE='./scripts/embed-memory-v2-qdrant.mjs'
|
||||
```
|
||||
|
||||
Even during read-only canary, `write_uses_legacy` and `compact_uses_legacy` must still pass.
|
||||
|
||||
## Mem0 Adapter Phase
|
||||
|
||||
Mem0 is the Memory Extraction backend boundary. The adapter uses HTTP and is dormant unless explicitly configured.
|
||||
|
||||
Environment:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=mem0
|
||||
MEMORY_MEM0_ENABLED=1
|
||||
MEMORY_MEM0_API_KEY='...'
|
||||
MEMORY_MEM0_PROJECT_ID='memind_project'
|
||||
MEMORY_MEM0_BASE_URL='https://api.mem0.ai'
|
||||
```
|
||||
|
||||
Smoke:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend mem0 --operation write
|
||||
```
|
||||
|
||||
Do not canary Mem0 writes until sampling, dedupe, rollback, and retention rules are accepted.
|
||||
|
||||
## Letta Lifecycle Adapter Phase
|
||||
|
||||
Letta is the Memory Lifecycle Management backend boundary. The adapter uses HTTP and is dormant unless explicitly configured.
|
||||
|
||||
Environment:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=letta
|
||||
MEMORY_LETTA_ENABLED=1
|
||||
MEMORY_LETTA_API_KEY='...'
|
||||
MEMORY_LETTA_PROJECT_ID='memind_project'
|
||||
MEMORY_LETTA_AGENT_ID='agent_1'
|
||||
MEMORY_LETTA_BASE_URL='https://api.letta.com'
|
||||
```
|
||||
|
||||
Smoke:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend letta --operation resolve
|
||||
```
|
||||
|
||||
Do not canary Letta until ownership boundaries, long/short-term promotion rules, conflict resolution with conversation-memory, rollback, and retention are accepted.
|
||||
|
||||
## Remaining External Adapter Phase
|
||||
|
||||
Weaviate, Neo4j, Redis Streams, and LangGraph are wired with runtime clients and remain dormant unless explicitly configured.
|
||||
|
||||
Environment examples:
|
||||
|
||||
```bash
|
||||
MEMORY_WEAVIATE_ENABLED=1
|
||||
MEMORY_WEAVIATE_URL='https://weaviate.example'
|
||||
MEMORY_WEAVIATE_COLLECTION='MemindMemory'
|
||||
MEMORY_WEAVIATE_EMBEDDING_MODULE='./scripts/embed-memory-v2-weaviate.mjs'
|
||||
|
||||
MEMORY_NEO4J_ENABLED=1
|
||||
MEMORY_NEO4J_HTTP_URL='http://localhost:7474'
|
||||
MEMORY_NEO4J_USER='neo4j'
|
||||
MEMORY_NEO4J_PASSWORD='...'
|
||||
|
||||
MEMORY_REDIS_STREAMS_ENABLED=1
|
||||
MEMORY_REDIS_STREAMS_URL='redis://localhost:6379'
|
||||
|
||||
MEMORY_LANGGRAPH_ENABLED=1
|
||||
MEMORY_LANGGRAPH_URL='https://langgraph.example'
|
||||
MEMORY_LANGGRAPH_POLICY_ID='memory_policy'
|
||||
```
|
||||
|
||||
Smoke:
|
||||
|
||||
```bash
|
||||
npm run smoke:memory-v2-external -- --backend weaviate --operation resolve
|
||||
npm run smoke:memory-v2-external -- --backend neo4j --operation write
|
||||
npm run smoke:memory-v2-external -- --backend redis-streams --operation write
|
||||
npm run smoke:memory-v2-external -- --backend langgraph --operation resolve
|
||||
```
|
||||
|
||||
Future canaries must be backend-specific and must define data ownership, dedupe, observability, rollback, and retention before any adapter can own a Memory V2 operation.
|
||||
|
||||
## Phase 1: Facade Over Legacy
|
||||
|
||||
Environment:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=legacy
|
||||
MEMORY_FAIL_OPEN=1
|
||||
MEMORY_VECTOR_ENABLED=0
|
||||
```
|
||||
|
||||
Runtime verification:
|
||||
|
||||
```bash
|
||||
curl -sk https://<host>/api/runtime/status
|
||||
```
|
||||
|
||||
Confirm:
|
||||
|
||||
- `memory.enabled=true`
|
||||
- `memory.backend=legacy`
|
||||
- `memory.selectedBackend=legacy-conversation-memory`
|
||||
- future plugin slots report `available=false`
|
||||
|
||||
Rollback:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=0
|
||||
```
|
||||
|
||||
## Phase 2: Empty pgvector Schema
|
||||
|
||||
This phase creates schema only. It must not change runtime backend selection.
|
||||
|
||||
Preconditions:
|
||||
|
||||
- PostgreSQL server is approved for Memory V2 semantic memory.
|
||||
- `pgvector` extension is available.
|
||||
- Backup/rollback decision is recorded by ops.
|
||||
|
||||
Dry-run:
|
||||
|
||||
```bash
|
||||
node scripts/setup-memory-v2-pgvector-schema.mjs \
|
||||
--table memory_embeddings \
|
||||
--dimensions 1536 \
|
||||
--create-extension
|
||||
```
|
||||
|
||||
Apply only through the approved production migration path:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
node scripts/setup-memory-v2-pgvector-schema.mjs \
|
||||
--apply \
|
||||
--table memory_embeddings \
|
||||
--dimensions 1536 \
|
||||
--create-extension
|
||||
```
|
||||
|
||||
Keep runtime on legacy:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKEND=legacy
|
||||
MEMORY_VECTOR_ENABLED=0
|
||||
```
|
||||
|
||||
Post-migration smoke verification:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
npm run smoke:memory-v2-pgvector -- \
|
||||
--table memory_embeddings \
|
||||
--dimensions 1536
|
||||
```
|
||||
|
||||
The smoke command inserts synthetic `memory-v2-smoke-*` rows, verifies nearest-vector retrieval, and deletes those rows by default. In production it should run only after the schema migration. Do not pass `--create-schema` in production unless the approved migration procedure explicitly requires it.
|
||||
|
||||
For local-only plumbing checks, use the deterministic hash embedding module:
|
||||
|
||||
```bash
|
||||
MEMORY_PGVECTOR_EMBEDDING_MODULE=./scripts/embed-memory-v2-local-hash.mjs
|
||||
MEMORY_V2_LOCAL_EMBEDDING_DIMENSIONS=3
|
||||
```
|
||||
|
||||
Do not use this local hash embedding module for production retrieval quality.
|
||||
|
||||
## Phase 3: Backfill
|
||||
|
||||
Backfill must be separate from schema creation and application rollout.
|
||||
|
||||
Dry-run:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKFILL_MYSQL_URL='mysql://...' \
|
||||
node scripts/backfill-memory-v2-pgvector.mjs --limit 100
|
||||
```
|
||||
|
||||
Apply:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKFILL_MYSQL_URL='mysql://...' \
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
|
||||
node scripts/backfill-memory-v2-pgvector.mjs \
|
||||
--apply \
|
||||
--limit 100 \
|
||||
--embedding-module ./scripts/embed-memory-v2-pgvector.mjs
|
||||
```
|
||||
|
||||
Operational rules:
|
||||
|
||||
- Store `{ updatedAt, id }` checkpoint externally after each successful batch.
|
||||
- Compare active MySQL memory count with pgvector distinct `source_memory_id` count.
|
||||
- Do not change `MEMORY_BACKEND` during backfill.
|
||||
- Do not delete pgvector rows during app rollback without an explicit retention decision.
|
||||
|
||||
## Phase 4: pgvector Resolve Canary
|
||||
|
||||
Only start canary after schema, backfill, and retrieval quality checks pass.
|
||||
|
||||
Environment:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=1
|
||||
MEMORY_BACKEND=pgvector
|
||||
MEMORY_VECTOR_ENABLED=1
|
||||
MEMORY_FAIL_OPEN=1
|
||||
MEMORY_PGVECTOR_DATABASE_URL='postgresql://...'
|
||||
MEMORY_PGVECTOR_EMBEDDING_MODULE='./scripts/embed-memory-v2-pgvector.mjs'
|
||||
```
|
||||
|
||||
Health gate:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2 -- --require-enabled --expect-backend pgvector
|
||||
```
|
||||
|
||||
Required invariant:
|
||||
|
||||
- `selectedBackend=pgvector` is allowed for `resolve`.
|
||||
- `write_uses_legacy` must still pass.
|
||||
- `compact_uses_legacy` must still pass.
|
||||
- Any pgvector error must fail open to legacy or degraded empty resolve.
|
||||
|
||||
Rollback:
|
||||
|
||||
```bash
|
||||
MEMORY_BACKEND=legacy
|
||||
MEMORY_VECTOR_ENABLED=0
|
||||
```
|
||||
|
||||
Emergency rollback:
|
||||
|
||||
```bash
|
||||
MEMORY_ENABLED=0
|
||||
```
|
||||
Reference in New Issue
Block a user