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 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.
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`.
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.
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
CREATETABLEmemory_embeddings(
idBIGSERIALPRIMARYKEY,
user_idTEXTNOTNULL,
contentTEXTNOTNULL,
embeddingVECTOR(1536)NOTNULL,
typeTEXTNOTNULLDEFAULT'fact',
source_memory_idTEXT,
source_session_idTEXT,
source_message_idTEXT,
metadataJSONBNOTNULLDEFAULT'{}'::jsonb,
created_atTIMESTAMPTZNOTNULLDEFAULTNOW(),
updated_atTIMESTAMPTZNOTNULLDEFAULTNOW()
);
```
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.
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.
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`.
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: