Files
memind/docs/memory-v2
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
..
2026-07-22 12:36:20 +08:00

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:

{
  "userId": "h5 user id",
  "sessionId": "goosed session id",
  "query": "current user prompt",
  "limit": 40
}

Returns a normalized payload:

{
  "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:

{
  "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:

{
  "userId": "h5 user id",
  "sessionId": "goosed session id"
}

getStatus()

Returns the facade policy and backend contract status for read-only observability.

Example:

{
  "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:

npm run check:memory-v2-contracts

End-to-end local validation can now be run from one entrypoint:

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:

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:

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:

npm run scaffold:memory-v2-backend -- \
  --name qdrant \
  --category semantic \
  --role scale-out-vector-store \
  --capability resolve

Write a Mem0 adapter scaffold:

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:

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:

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:

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:

/user-memory/v1/remember-recent
  -> memoryV2.write
  -> legacy conversation-memory saveAndAnalyze

Compact path:

/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:

/api/runtime/status
  -> memory: memoryV2.getStatus()

This is read-only and exists only to make rollout/debugging visible.

Local health gate:

npm run check:memory-v2-contracts
npm run check:memory-v2 -- --require-enabled --expect-backend legacy

Local app canary against a running Portal service:

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:

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.

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:

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:

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:

import { ensurePgvectorMemorySchema } from './memory-v2-pgvector-schema.mjs';

await ensurePgvectorMemorySchema(pool, {
  tableName: 'memory_embeddings',
  dimensions: 1536,
  createExtension: true,
  createVectorIndex: false,
});

Dry-run CLI:

node scripts/setup-memory-v2-pgvector-schema.mjs \
  --table memory_embeddings \
  --dimensions 1536 \
  --create-extension

Apply CLI:

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:

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:

h5_user_memory_items
  WHERE status = 'active'
  ORDER BY updated_at ASC, id ASC

Destination:

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:

MEMORY_BACKFILL_MYSQL_URL='mysql://...' \
node scripts/backfill-memory-v2-pgvector.mjs \
  --limit 100 \
  --cursor-updated-at 0 \
  --cursor-id ''

Apply CLI:

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:

MEMORY_PGVECTOR_DATABASE_URL='postgresql://...' \
npm run smoke:memory-v2-pgvector

Local setup smoke that also creates an empty schema:

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:

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:

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:

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:

selectedBackend=legacy-conversation-memory
qdrant.available=false
qdrant.reason=embedding_module_not_configured

Expected with embedding module and reachable Qdrant:

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:

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:

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:

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:

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:

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:

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:

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