987 lines
33 KiB
Markdown
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
|