Files
memind/docs/memory-v2/README.md
john 93d7bc0cd5
Memind CI / Test, build, and release guards (pull_request) Successful in 4m37s
feat: add episodic history recall
2026-07-22 12:36:20 +08:00

987 lines
33 KiB
Markdown

# 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.
### Runtime control flags
The `memind_adm` Memory V2 page exposes runtime controls separately from
`MEMORY_ENABLED`. They are disabled by default. Agent resolve currently
supports `shadow` observation and explicit `active` hidden-context injection;
promotion, Compact V2, and Reflection remain guarded for later stages:
| Admin field | Environment override | Default |
| --- | --- | --- |
| `runtimeControl.agentResolveEnabled` | `MEMORY_AGENT_RESOLVE_ENABLED` | `0` |
| `runtimeControl.agentInjectionMode` | `MEMORY_AGENT_INJECTION_MODE` | `off` |
| `runtimeControl.agentCanaryUserIds` | `MEMORY_AGENT_CANARY_USER_IDS` | empty |
| `runtimeControl.agentResolveLimit` | `MEMORY_AGENT_RESOLVE_LIMIT` | `3` |
| `runtimeControl.agentResolveTimeoutMs` | `MEMORY_AGENT_RESOLVE_TIMEOUT_MS` | `1200` |
| `runtimeControl.promotionEnabled` | `MEMORY_PROMOTION_ENABLED` | `0` |
| `runtimeControl.compactionV2Enabled` | `MEMORY_COMPACTION_V2_ENABLED` | `0` |
| `runtimeControl.reflectionEnabled` | `MEMORY_REFLECTION_ENABLED` | `0` |
| `runtimeControl.lifecycleWorkerEnabled` | `MEMORY_LIFECYCLE_WORKER_ENABLED` | `0` |
| `runtimeControl.lifecycleRolloutMode` | `MEMORY_LIFECYCLE_ROLLOUT_MODE` | `off` |
| `runtimeControl.lifecycleRolloutUserIds` | `MEMORY_LIFECYCLE_ROLLOUT_USER_IDS` | empty |
The controls are reported in `memoryV2.getStatus().runtimeControl`. Turning
them off must leave the existing legacy conversation-memory path unchanged.
The additive user-scoped management API is:
- `GET /user-memory/v1/items` to list active (or requested-status) items.
- `DELETE /user-memory/v1/items/:memoryId` to forget one item when lifecycle
forgetting is enabled.
Lifecycle workers are disabled by default. When explicitly enabled they run
expiration, conservative compaction observation, candidate promotion, and
reflection observation according to the rollout mode; none of these operations
blocks the chat path. `off` creates no worker scope and performs no lifecycle
mutation, `canary` is always user-scoped to the configured rollout IDs, and only
`active` permits an unscoped global worker run.
When candidate persistence is enabled, the Portal runtime idempotently creates
`h5_memory_v2_candidates` before enabling the MySQL candidate store. DDL failure
is fail-open for Portal chat and falls back to bounded in-memory candidates;
`memind_adm` uses the same schema helper during bootstrap and fails startup rather
than serving a permanently broken candidate API. The table is additive and must
not be dropped as part of an application rollback.
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_RETRIEVER_EPISODIC_ENABLED`
Enables the historical-session recall capability, but does not by itself authorize retrieval.
`MEMORY_RETRIEVER_ENABLED` must also be enabled and `MEMORY_RETRIEVER_EPISODIC_MODE` must be
`canary` or `active`. Missing or invalid mode is treated as `off`.
`MEMORY_RETRIEVER_EPISODIC_MODE`
Historical recall rollout mode: `off`, `canary`, or `active`. In canary mode only users listed in
`MEMORY_RETRIEVER_EPISODIC_CANARY_USER_IDS` may be indexed or queried.
`MEMORY_RETRIEVER_EPISODIC_CANARY_USER_IDS`
Comma- or whitespace-separated user IDs for historical recall canary testing.
`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`.
`MEMORY_PGVECTOR_SYNC_USER_LIMIT`
Maximum number of recent active memories synchronously upserted for the current
user after a successful `write(...)`, `compact(...)`, or candidate promotion.
Defaults to `50`. When a session ID is available the sync is additionally scoped
to that session. This immediate, idempotent sync keeps newly saved memories
recallable without waiting for the separate global checkpoint backfill.
## 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
```
Explicit historical recall is a separate bounded read path layered beside personal memory:
```text
direct chat / Agent memory context
-> episodicMemory.resolve
-> h5_episodic_memory_items (same user, excluding current session)
-> bounded h5_session_snapshots fallback for pre-index history
```
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