memini-ai-dev v1.0.0 — Embedded pgembed Architecture Design¶
Author: boomerang-architect (read-only design review) Date: 2026-07-15 Status: ✅ Approved by user. Q1-Q7 decisions baked in. Ready for implementation. Target branch: feature/embedded-pgembed-v1 (to be created from main at 7eed224 = v0.8.2)
Decision Log (Q1–Q7)¶
| # | Question | Decision |
|---|---|---|
| Q1 | Data directory location | B: XDG ~/.local/share/memini-ai/pgembed/data. server.json at ~/.memini-ai/pgembed/server.json (memini namespace for state). |
| Q2 | Server liveness protocol | Cooperative heartbeat (1s client ping, 2s timeout, 5s drain grace, distributed sweep). See Heartbeat Design. |
| Q3 | Team server write-through | C: Async dual-write, embedded is canonical, team is fire-and-forget. See Team Write-Through. |
| Q4 | Backwards compat (MEMINI_DB_URL) | Breaking change. v0.8.2 users with MEMINI_DB_URL set get pgembed mode by default, must set MEMINI_VECTOR_BACKEND=postgres-external. One-time warning on startup. Optional memini-ai migrate CLI to copy data. |
| Q5 | Delete dead branch | LEAVE feature/postgres-in-process-research alone. (The handoff agent's claim that main was overwritten was false; user verified.) |
| Q6 | Fusion default mode | fusion_mode="none" for pip install memini-ai (default). fusion_mode="rrf" is set by user when installing with pip install memini-ai[team]. See Team Install Path. |
| Q7 | Vectorscale in embedded | A: Use vectorscale (DiskANN) in embedded mode. _detect_vectorscale() at database.py:239 already handles this. |
Table of Contents¶
- pgembed Smoke Test — VERIFIED
- Driver-Pattern Interface Spec
- EmbeddedPGDriver State Machine + Heartbeat Protocol
- Multi-Process Attach Algorithm
- Config Field Design
- RRF Fusion Across Embedded + Team
- Failure-Mode Table
- Test Strategy
- Implementation Plan (Task List)
- Risks
1. pgembed Smoke Test — VERIFIED¶
1.1 Environment¶
| Item | Value |
|---|---|
| Python | 3.13.13 (uv venv, .venv/bin/python → cpython-3.13.13-linux-x86_64-gnu) |
| OS | CachyOS Linux, x86_64 |
| pgembed version | 0.2.0 (wheel: 28.6MB, installed via uv pip install pgembed) |
| Dependencies | fasteners==0.20, psutil==7.2.2 (auto-installed) |
| Postgres version | 17.9 (PostgreSQL 17.9 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.2.1) |
| Connection method | Unix socket (not TCP) — postgresql://postgres:@/postgres?host=/tmp/pgembed-smoke |
1.2 Extensions Available (Exact Output)¶
Verified via asyncpg query against a live pgembed server:
Total extensions: 5
pg_search 0.21.14 (not installed)
pg_textsearch 0.6.1 (not installed)
plpgsql 1.0 1.0
vector 0.8.2 (not installed)
vectorscale 0.9.0 (not installed)
Critical finding: The handoff doc (section 8) claimed vectorscale was "listed in EXTENSION_NAMES but marked unavailable." This is wrong. vectorscale 0.9.0 IS available in pg_available_extensions and CREATE EXTENSION vectorscale succeeds. Both embedded and team users can get StreamingDiskANN indexes (Q7 decision).
1.3 Port Behavior¶
pgembed uses Unix domain sockets by default, not TCP ports. URI: postgresql://postgres:@/postgres?host=<data_dir>. No port conflicts, no firewall issues.
1.4 Multi-Process Behavior (Verified)¶
Test: Start server from Process A, then start a second get_server() on the same data dir from Process B. Same URI returned, only one Postgres process running.
1.5 pgembed API Surface¶
def get_server(
pgdata: Union[Path, str],
cleanup_mode: Optional[str] = 'stop' # 'stop' = stop on cleanup, None = keep running, 'delete' = stop + delete
) -> PostgresServer: ...
class PostgresServer:
def get_uri(self) -> str: ...
def cleanup(self) -> None: ... # segfaults in psql subprocess on Python 3.13, use asyncpg instead
Note: server.psql() segfaults on this machine (Python 3.13.13, SIGSEGV in bundled psql binary). We use asyncpg connections instead.
2. Driver-Pattern Interface Spec¶
2.1 Design Goal¶
The 1820-LOC PostgresDatabase class (at src/memini_ai/postgres/database.py) must work against either an embedded pgembed server or an external Docker Postgres server, with zero changes to its 19 ABC method implementations. The driver abstracts the connection acquisition + lifecycle management.
2.2 Proposed Interface¶
# src/memini_ai/postgres/driver.py (NEW FILE, ~470 LOC)
from __future__ import annotations
from typing import Protocol, runtime_checkable
@runtime_checkable
class DatabaseDriver(Protocol):
"""Minimal interface for backend-specific connection lifecycle."""
async def get_uri(self) -> str:
"""Return a PostgreSQL connection URI ready for asyncpg.
For pgembed: returns the Unix socket URI from get_server().
For external: returns the configured MEMINI_DB_URL.
Must be callable before initialize() — the URI is needed
to create the asyncpg pool at PostgresDatabase.initialize():167.
Idempotent.
"""
...
async def initialize(self) -> None:
"""Perform backend-specific initialization AFTER the pool exists.
For pgembed: starts the heartbeat + sweep background task.
For external: no-op.
Idempotent.
"""
...
async def shutdown(self) -> None:
"""Gracefully release backend resources.
For pgembed: stops the heartbeat task, removes this client from
the clients map, initiates shutdown if last client. Does NOT
stop the server unless this is the last client AND the sweep
would also initiate shutdown.
For external: no-op.
"""
...
def is_ready(self) -> bool:
"""Synchronous check: is the backend accepting connections?
For pgembed: checks postmaster PID is alive.
For external: always returns True.
"""
...
2.3 Why the Driver Does NOT Own the Pool¶
The driver hands back a URI. PostgresDatabase continues to own the asyncpg pool (created at database.py:167). This is the right split because: 1. Pool lifecycle is coupled to query patterns (min_size, max_size, SSL, pgvector codec) 2. The 19 ABC methods all call await self._get_pool() (line 278) 3. Minimal surface — 4 methods captures the entire backend difference
2.4 Interaction with Existing Lifecycle¶
create_database(config)
→ driver = EmbeddedPGDriver(config.pgembed_data_dir) # or ExternalPGDriver(config.db_url)
→ PostgresDatabase(driver=driver)
→ (lazy) first query calls initialize()
→ uri = await driver.get_uri()
→ asyncpg.create_pool(uri) # line 167 (unchanged)
→ await driver.initialize() # NEW: backend-specific init
→ _ensure_schema() # line 174 (unchanged)
Total change to PostgresDatabase: ~15 lines modified, 0 lines deleted from the 19 ABC methods.
2.5 ExternalPGDriver (Trivial)¶
class ExternalPGDriver:
"""Driver for external (Docker/team) Postgres — preserves v0.8.2 behavior."""
def __init__(self, db_url: str) -> None:
self._db_url = db_url
async def get_uri(self) -> str:
return self._db_url
async def initialize(self) -> None:
pass
async def shutdown(self) -> None:
pass
def is_ready(self) -> bool:
return True
3. EmbeddedPGDriver State Machine + Heartbeat Protocol¶
This section replaces the original refcount-based design. The user rejected "auto-stop after N idle minutes" in favor of a cooperative heartbeat protocol.
3.1 The server.json Schema¶
{
"version": "1.0.0",
"state": "running",
"pid": 1162268,
"uri": "postgresql://postgres:@/postgres?host=/home/jcharles/.local/share/memini-ai/pgembed/data",
"data_dir": "/home/jcharles/.local/share/memini-ai/pgembed/data",
"server_started_at": "2026-07-15T12:00:00.000Z",
"heartbeat_interval_s": 60,
"heartbeat_timeout_s": 120,
"grace_margin_s": 10,
"shutdown_initiated_by": null,
"shutdown_initiated_at": null,
"clients": {
"a1b2c3d4-e5f6-7890-abcd-ef1234567890": {
"pid": 1162268,
"last_heartbeat": "2026-07-15T12:05:30.000Z",
"role": "primary"
},
"f9e8d7c6-b5a4-3210-fedc-ba0987654321": {
"pid": 1163456,
"last_heartbeat": "2026-07-15T12:05:28.000Z",
"role": "passive"
}
}
}
3.2 Field Reference¶
| Field | Type | Written by | Purpose |
|---|---|---|---|
version | string | First client | Schema version |
state | enum | All clients | starting, running, draining, stopped, dead |
pid | int | First client | Postmaster PID (for os.kill(pid, 0)) |
uri | string | First client | Connection URI |
data_dir | string | First client | Path to pgembed data dir |
server_started_at | ISO 8601 | First client, never overwritten | Observability: "data dir running on and off for 23 days" |
heartbeat_interval_s | int | First client (config) | How often clients ping (default 60) |
heartbeat_timeout_s | int | First client (config) | Stale threshold (default 120) |
grace_margin_s | int | First client (config) | Buffer for clock skew (default 10) |
shutdown_initiated_by | string or null | Sweeping client | Election token |
shutdown_initiated_at | ISO 8601 or null | Sweeping client | When drain started |
clients | map[string→ClientInfo] | Each client writes its own entry | Per-client heartbeat map |
3.3 ClientInfo Sub-object¶
| Field | Type | Purpose |
|---|---|---|
pid | int | OS PID of this client process |
last_heartbeat | ISO 8601 | Last time this client wrote a heartbeat |
role | string | "primary" (has active pool) or "passive" (attached, no queries yet) |
3.4 State Diagram¶
┌──────────┐
create() │ │ get_server() succeeds
──────────────►│ STARTING │──────────────────────┐
│ │ │
└────┬─────┘ │
│ ▼
│ get_server() fails ┌─────────┐
└─────────────────────►│ │
│ RUNNING │◄──── attach() (existing server)
│ │
└────┬────┘
│
┌───────────────────────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ │ explicit shutdown │ │ crash/ │ │
│ STOPPED │◄───────────────────────────────│ DRAINING │ kill -9 │ DEAD │
│ │ (last client leaves) │ │─────────────►│ │
└─────────┘ └──────────┘ └─────────┘
│ │
│ │ new client heartbeats
│ │ (cancels drain)
▼ ▼
(server down, ┌─────────┐
data dir intact) │ RUNNING │
└─────────┘
State triggers:
| State | Entry | Exit |
|---|---|---|
| STOPPED | Initial; sweep sees all clients stale; explicit memini-ai stop | get_uri() reads state, finds stopped, calls get_server() → STARTING |
| STARTING | get_server() called, no existing RUNNING server found | Postgres accepts connections → RUNNING; failure → DEAD |
| RUNNING | Postgres ready; new client attaches via heartbeat | Last client leaves + sweep sees all stale → DRAINING; process crash → DEAD |
| DRAINING | Last client gone OR explicit shutdown | Grace expires → STOPPED; new client heartbeats during grace → back to RUNNING |
| DEAD | Process crash, kill -9, data dir corruption, init failure | Next get_uri() detects DEAD, cleans up, restarts from STOPPED → STARTING |
3.5 Client-Side Heartbeat Task¶
class EmbeddedPGDriver:
def __init__(self, data_dir: str | Path) -> None:
self._client_id = str(uuid4()) # stable per process
self._heartbeat_task: asyncio.Task | None = None
self._heartbeat_stop: asyncio.Event = asyncio.Event()
self._healthy: bool = True
self._role: str = "passive"
async def _heartbeat_loop(self) -> None:
interval = self._read_state().get("heartbeat_interval_s", 60)
consecutive_failures = 0
while not self._heartbeat_stop.is_set():
try:
await asyncio.sleep(interval)
except asyncio.CancelledError:
break
if self._heartbeat_stop.is_set():
break
try:
self._write_heartbeat()
consecutive_failures = 0
except OSError as e:
consecutive_failures += 1
logger.warning("heartbeat_write_failed",
client_id=self._client_id, attempt=consecutive_failures, error=str(e))
if consecutive_failures >= 3:
logger.error("heartbeat_write_failed_permanently", client_id=self._client_id)
self._healthy = False
continue
await self._sweep_stale_clients()
3.6 The Sweep Algorithm (Distributed)¶
Every client participates. No single point of failure. No external scheduler.
async def _sweep_stale_clients(self) -> None:
state = self._read_state()
if state is None or state.get("shutdown_initiated_by") is not None:
return
clients = state.get("clients", {})
if not clients:
return
now = datetime.now(timezone.utc)
timeout_s = state.get("heartbeat_timeout_s", 120)
grace_s = state.get("grace_margin_s", 10)
threshold = now - timedelta(seconds=timeout_s + grace_s)
for client_id, info in clients.items():
last_hb = datetime.fromisoformat(info["last_heartbeat"])
if last_hb > threshold:
return # At least one client alive
# ALL stale — initiate shutdown
if not self._try_claim_shutdown(state):
return # Another client beat us
await asyncio.sleep(5) # 5s grace for late heartbeats
# Re-check: did anyone heartbeat during grace?
state = self._read_state()
if state:
for info in state.get("clients", {}).values():
last_hb = datetime.fromisoformat(info["last_heartbeat"])
if last_hb > threshold:
self._clear_shutdown_token()
return
await self._stop_server()
3.7 Election: _try_claim_shutdown()¶
def _try_claim_shutdown(self, state: dict) -> bool:
with fasteners.InterProcessLock(str(self._state_lock)):
current = self._read_state()
if current is None or current.get("shutdown_initiated_by") is not None:
return False
current["shutdown_initiated_by"] = self._client_id
current["shutdown_initiated_at"] = datetime.now(timezone.utc).isoformat()
current["state"] = "draining"
self._state_file.write_text(json.dumps(current, indent=2))
return True
3.8 Detection Latency¶
Worst case = heartbeat_interval + heartbeat_timeout + grace_margin = 60 + 120 + 10 = 190s from last client crash to shutdown init, plus 5s grace = 195s total.
3.9 Race Conditions¶
| Race | Mitigation |
|---|---|
| Two clients both see all clients stale | _try_claim_shutdown() uses fasteners.InterProcessLock; only one writes shutdown_initiated_by |
| Late heartbeat during sweep grace period | 5s grace + 10s grace_margin absorb the race; re-check at end of grace cancels shutdown if needed |
| New client during DRAINING | New client writes its heartbeat, clears shutdown_initiated_by, sets state: "running". Existing client re-checks and cancels drain. |
| Clock skew | grace_margin_s = 10 absorbs up to 10s of NTP stepping. On a single machine (only deployment model), skew is effectively zero. |
3.10 Health Check API¶
def is_healthy(self) -> bool:
"""Check if this client's connection is healthy."""
if not self._healthy:
return False
state = self._read_state()
if state is None:
return False
pid = state.get("pid")
if pid is None:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False
def get_health_report(self) -> dict:
"""Detailed health report for memini-ai status / MCP tool."""
# ... see architect's Section 5.1
The get_status MCP tool is extended to include the health report when running in pgembed mode.
4. Multi-Process Attach Algorithm¶
4.1 Pseudocode (with heartbeat)¶
import asyncio
import json
import os
import uuid
from datetime import datetime, timezone, timedelta
from pathlib import Path
import fasteners
import pgembed
class EmbeddedPGDriver:
"""Driver for embedded pgembed Postgres with cooperative heartbeat liveness.
ONE embedded server, shared by ALL memini-ai processes on the same
machine. Uses pgembed's native get_server() for process coordination
and a server.json state file for observability + heartbeat map.
"""
def __init__(self, data_dir: str | Path) -> None:
self._data_dir = Path(data_dir).expanduser().resolve()
# server.json stays in ~/.memini-ai/pgembed/ for namespace consistency
self._state_dir = Path("~/.memini-ai/pgembed").expanduser().resolve()
self._state_file = self._state_dir / "server.json"
self._state_lock = self._state_dir / "server.json.lock"
self._server = None
self._uri = None
self._client_id = str(uuid4())
self._role = "passive"
self._healthy = True
self._heartbeat_task = None
self._heartbeat_stop = asyncio.Event()
self._explicit_shutdown = False
self._state_dir.mkdir(parents=True, exist_ok=True)
# ── Public API (DatabaseDriver Protocol) ──────────────────────
async def get_uri(self) -> str:
if self._uri is not None:
return self._uri
existing = self._read_state()
if existing and existing.get("state") in ("running", "draining"):
self._uri = await self._attach_to_existing(existing)
return self._uri
if existing and existing.get("state") == "dead":
self._cleanup_stale_state()
self._uri = await self._start_new_server()
return self._uri
async def initialize(self) -> None:
if self._server is None:
await self.get_uri()
self._role = "primary" # Now actively querying
async def shutdown(self) -> None:
# Stop heartbeat first
if self._heartbeat_task is not None:
self._heartbeat_stop.set()
self._heartbeat_task.cancel()
try:
await self._heartbeat_task
except asyncio.CancelledError:
pass
self._heartbeat_task = None
if self._server is None:
return
# Remove this client from the map
self._remove_client()
# Check if we're the last
state = self._read_state()
remaining = state.get("clients", {}) if state else {}
if not remaining or self._explicit_shutdown:
await self._stop_server()
def is_ready(self) -> bool:
state = self._read_state()
if state and state.get("state") == "running":
pid = state.get("pid")
if pid is not None:
try:
os.kill(pid, 0)
return True
except OSError:
return False
return False
def request_explicit_shutdown(self) -> None:
self._explicit_shutdown = True
# ── Server lifecycle ──────────────────────────────────────────
async def _attach_to_existing(self, state: dict) -> str:
self._server = pgembed.get_server(state["data_dir"], cleanup_mode=None)
uri = self._server.get_uri()
self._register_client()
self._start_heartbeat()
return uri
async def _start_new_server(self) -> str:
self._write_initial_state()
try:
self._server = pgembed.get_server(str(self._data_dir), cleanup_mode=None)
except Exception as e:
self._write_state("dead", refcount=0)
raise RuntimeError(f"Failed to start embedded Postgres: {e}") from e
uri = self._server.get_uri()
self._register_client()
self._start_heartbeat()
return uri
def _cleanup_stale_state(self) -> None:
if self._state_file.exists():
self._state_file.unlink()
# ── State file management ────────────────────────────────────
def _read_state(self) -> dict | None:
if not self._state_file.exists():
return None
try:
return json.loads(self._state_file.read_text())
except (json.JSONDecodeError, OSError):
return None
def _write_initial_state(self) -> None:
with fasteners.InterProcessLock(str(self._state_lock)):
existing = self._read_state() or {}
# Preserve server_started_at across restarts
started_at = existing.get("server_started_at") or datetime.now(timezone.utc).isoformat()
data = {
"version": "1.0.0",
"state": "starting",
"pid": os.getpid(), # will be replaced by postmaster pid
"uri": "",
"data_dir": str(self._data_dir),
"server_started_at": started_at,
"heartbeat_interval_s": 60,
"heartbeat_timeout_s": 120,
"grace_margin_s": 10,
"shutdown_initiated_by": None,
"shutdown_initiated_at": None,
"clients": {},
}
self._state_file.write_text(json.dumps(data, indent=2))
def _register_client(self) -> None:
with fasteners.InterProcessLock(str(self._state_lock)):
state = self._read_state() or {}
state.setdefault("clients", {})
state["clients"][self._client_id] = {
"pid": os.getpid(),
"last_heartbeat": datetime.now(timezone.utc).isoformat(),
"role": self._role,
}
# Update postmaster pid if we just started
if self._server and self._server._postmaster_info:
state["pid"] = self._server._postmaster_info.pid
state["state"] = "running"
state["uri"] = self._uri or state.get("uri", "")
self._state_file.write_text(json.dumps(state, indent=2))
def _remove_client(self) -> None:
with fasteners.InterProcessLock(str(self._state_lock)):
state = self._read_state()
if state and self._client_id in state.get("clients", {}):
del state["clients"][self._client_id]
self._state_file.write_text(json.dumps(state, indent=2))
def _write_heartbeat(self) -> None:
with fasteners.InterProcessLock(str(self._state_lock)):
state = self._read_state() or {}
state.setdefault("clients", {})
state["clients"][self._client_id] = {
"pid": os.getpid(),
"last_heartbeat": datetime.now(timezone.utc).isoformat(),
"role": self._role,
}
self._state_file.write_text(json.dumps(state, indent=2))
4.2 Scenario Walkthroughs¶
| Scenario | Sequence |
|---|---|
| Process A starts, no server | get_uri() → no state → _start_new_server() → pgembed.get_server() → Postgres starts → _register_client() → _start_heartbeat() |
| Process B starts, finds A's server | get_uri() → state={running, clients: {A}} → _attach_to_existing() → pgembed.get_server() returns cached → _register_client() (B added) → heartbeat starts |
| Process A exits gracefully | shutdown() → stop heartbeat → _remove_client() (A removed) → clients map still has B → server stays |
| Process A is kill -9'd | No shutdown() called → A's heartbeat stops being written → after ~190s, B's sweep sees A stale → B initiates shutdown (or A's heartbeat expires) |
| Last client exits gracefully | shutdown() → _remove_client() → clients map empty → _stop_server() → cleanup() → state="stopped" |
User runs memini-ai stop | request_explicit_shutdown() → shutdown() → skip clients-empty check → _stop_server() → state="stopped" |
| Process C starts after server stopped | get_uri() → state={stopped} → _start_new_server() → pgembed reuses data dir (memories preserved) → state="running" (preserves server_started_at) |
| Process D starts, finds DEAD state | get_uri() → state={dead} → _cleanup_stale_state() → _start_new_server() → pgembed detects data dir, reuses |
4.3 Lock Strategy¶
- pgembed's data-dir lock — prevents two processes from starting Postgres on same data dir. Ground truth.
server.json.lock— protectsclientsmap andshutdown_initiated_bytoken. Tiny critical sections.- No socket lock — pgembed uses Unix sockets, naturally single-writer.
5. Config Field Design¶
5.1 New Fields in MeminiConfig¶
# In src/memini_ai/config.py, inside class MeminiConfig(BaseSettings):
vector_backend: Literal["pgembed", "postgres-external"] = Field(
default="pgembed",
alias="MEMINI_VECTOR_BACKEND",
description=(
"Vector database backend. 'pgembed' (default) starts an embedded "
"PostgreSQL 17 server in-process. 'postgres-external' connects to "
"an external PostgreSQL server via MEMINI_DB_URL (Docker, team server, etc.)."
),
)
pgembed_data_dir: str = Field(
default="~/.local/share/memini-ai/pgembed/data",
alias="MEMINI_PGEMBED_DATA_DIR",
description=(
"Data directory for the embedded pgembed PostgreSQL server. "
"Persistent across restarts. Default: ~/.local/share/memini-ai/pgembed/data"
),
)
team_db_url: str = Field(
default="",
alias="MEMINI_TEAM_DB_URL",
description=(
"Optional team/shared PostgreSQL server URL for RRF fusion with "
"embedded results."
),
)
fusion_mode: Literal["none", "rrf"] = Field(
default="none",
alias="MEMINI_FUSION_MODE",
description=(
"Fusion mode for multi-backend queries. 'none' (default) queries "
"only the primary backend. 'rrf' fuses results from embedded + team."
),
)
5.2 Backward Compatibility (Q4 Decision: BREAK v0.8.2 — REFUSE TO START)¶
Q4 decision: vector_backend always wins. v0.8.2 users with MEMINI_DB_URL set but no explicit MEMINI_VECTOR_BACKEND will get a hard RuntimeError on startup. No silent compat, no silent data loss, no surprise empty database. The user MUST explicitly choose.
| User Scenario | v0.8.2 Config | v1.0.0 Behavior |
|---|---|---|
v0.8.2 user with MEMINI_DB_URL set | MEMINI_DB_URL=postgresql://... | REFUSE TO START. Raises RuntimeError with clear remediation. See Refuse-to-Start Error. |
| New user, no config | Nothing set | vector_backend="pgembed" (default), data dir at ~/.local/share/memini-ai/pgembed/data, embedded server auto-starts on first query. |
| User wants embedded only | MEMINI_VECTOR_BACKEND=pgembed | Embedded server only. |
| User wants team only | MEMINI_VECTOR_BACKEND=postgres-external + MEMINI_DB_URL=... | Team server only. (Same as v0.8.2 with explicit backend selection.) |
| User wants both fused | MEMINI_VECTOR_BACKEND=pgembed + MEMINI_TEAM_DB_URL=... + MEMINI_FUSION_MODE=rrf | Embedded + team with RRF fusion. |
5.3 create_database() Factory¶
def create_database(config: MeminiConfig | None = None) -> VectorDatabase:
if config is None:
config = get_config()
# Q4: vector_backend always wins. No auto-detect, no silent compat.
# v0.8.2 users with MEMINI_DB_URL set MUST set MEMINI_VECTOR_BACKEND=postgres-explicit
# or run `memini-ai migrate` to copy data to embedded mode.
from memini_ai.postgres.driver import EmbeddedPGDriver, ExternalPGDriver
if config.vector_backend == "pgembed":
data_dir = Path(config.pgembed_data_dir).expanduser()
driver = EmbeddedPGDriver(data_dir)
elif config.vector_backend == "postgres-external":
db_url = config.db_url or os.environ.get("MEMINI_DB_URL", "")
if not db_url:
raise ValueError(
"MEMINI_VECTOR_BACKEND=postgres-external but MEMINI_DB_URL is not set. "
"Set MEMINI_DB_URL or switch to MEMINI_VECTOR_BACKEND=pgembed for embedded mode."
)
driver = ExternalPGDriver(db_url)
else:
raise ValueError(f"Unknown vector_backend: {config.vector_backend}")
from memini_ai.postgres import PostgresDatabase
db = PostgresDatabase(
driver=driver,
project_id=config.project_id,
sslmode=config.db_sslmode,
sslrootcert=config.db_sslrootcert,
)
if config.fusion_mode == "rrf" and config.team_db_url:
from memini_ai.memory.rrf_database import RRFDatabase
team_driver = ExternalPGDriver(config.team_db_url)
team_db = PostgresDatabase(driver=team_driver, project_id=config.project_id)
db = RRFDatabase(primary=db, secondary=team_db, k=config.rrf_k)
return db
5.4 Refuse-to-Start Error¶
When a user has MEMINI_DB_URL set (indicating v0.8.2 external-Postgres setup) but MEMINI_VECTOR_BACKEND is not explicitly set, create_database() raises a clear error BEFORE starting any database:
# In create_database(), called at server startup:
if (
(config.db_url or os.environ.get("MEMINI_DB_URL"))
and "MEMINI_VECTOR_BACKEND" not in os.environ
):
raise RuntimeError(
"memini-ai v1.0.0: MEMINI_DB_URL is set but MEMINI_VECTOR_BACKEND is not.\n"
"\n"
"v1.0.0 defaults to 'pgembed' (embedded PostgreSQL) which IGNORES MEMINI_DB_URL.\n"
"Your v0.8.2 setup connected to an external Postgres server via MEMINI_DB_URL.\n"
"\n"
"To keep using your external server:\n"
" export MEMINI_VECTOR_BACKEND=postgres-external\n"
"\n"
"To switch to embedded mode (and copy your data):\n"
" unset MEMINI_DB_URL\n"
" memini-ai migrate --from='<your old MEMINI_DB_URL>'\n"
"\n"
"Set MEMINI_VECTOR_BACKEND explicitly to suppress this error."
)
Rationale (user decision, 2026-07-15): - User explicitly said "I don't give a rats ass about backwards compat. We don't build around tech debt." - Silent compat (auto-detect from MEMINI_DB_URL) would silently route around vector_backend — the exact kind of tech debt the user wants to avoid - Soft warning + start would silently drop the user's external DB connection on first query — silent data loss - Hard error forces explicit choice, no surprises, no "what happened to my memories" support tickets - The error is loud, specific, and gives two clear paths forward
5.5 Config Validation¶
@field_validator("vector_backend")
@classmethod
def _validate_vector_backend(cls, v: str) -> str:
allowed = {"pgembed", "postgres-external"}
if v not in allowed:
raise ValueError(f"vector_backend must be one of {allowed}, got '{v}'")
return v
@field_validator("fusion_mode")
@classmethod
def _validate_fusion_mode(cls, v: str) -> str:
allowed = {"none", "rrf"}
if v not in allowed:
raise ValueError(f"fusion_mode must be one of {allowed}, got '{v}'")
return v
5.5 Config Validation¶
@field_validator("vector_backend")
@classmethod
def _validate_vector_backend(cls, v: str) -> str:
allowed = {"pgembed", "postgres-external"}
if v not in allowed:
raise ValueError(f"vector_backend must be one of {allowed}, got '{v}'")
return v
@field_validator("fusion_mode")
@classmethod
def _validate_fusion_mode(cls, v: str) -> str:
allowed = {"none", "rrf"}
if v not in allowed:
raise ValueError(f"fusion_mode must be one of {allowed}, got '{v}'")
return v
6. RRF Fusion Across Embedded + Team¶
6.1 What Already Exists (Reused As-Is)¶
src/memini_ai/memory/rrf.py (250 LOC): - reciprocal_rank_fusion(ranked_lists, k=60) — pure function, accepts variable-length inputs - rrf_with_limit(ranked_lists, k, limit) — top-N convenience wrapper - rrf_search(conn, query_text, embedder, ...) — intra-server fan-out (384+1024 columns)
The new fusion is one level up: fan out across TWO Postgres servers, each of which may internally do its own 384+1024 RRF.
6.2 RRFDatabase Wrapper¶
# src/memini_ai/memory/rrf_database.py (NEW FILE, ~210 LOC)
class RRFDatabase(VectorDatabase):
"""Wraps two VectorDatabase instances and fuses query results via RRF.
Writes go to primary only. Queries fan out to both backends in
parallel and fuse ranked lists using RRF.
Tie-breaking: same memory_id in both → primary (embedded) wins.
Failure mode: secondary unreachable → degrade to primary-only.
"""
def __init__(self, primary: VectorDatabase, secondary: VectorDatabase, k: int = 60) -> None:
self._primary = primary
self._secondary = secondary
self._k = k
self._initialized = False
self._team_write_failures: int = 0
async def initialize(self) -> None:
if self._initialized:
return
await self._primary.initialize()
try:
await self._secondary.initialize()
except Exception:
logger.warning("team_server_init_failed", message="Team server unavailable at startup")
self._initialized = True
async def add_memory(self, entry: MemoryEntry) -> str:
await self.initialize()
memory_id = await self._primary.add_memory(entry)
if self._secondary is not None:
asyncio.create_task(self._write_to_team(entry, memory_id)) # Q3: fire-and-forget
return memory_id
async def _write_to_team(self, entry: MemoryEntry, memory_id: str) -> None:
try:
await self._secondary.add_memory(entry)
except Exception as e:
self._team_write_failures += 1
logger.warning("team_write_failed", memory_id=memory_id, error=str(e)[:200])
async def get_memory(self, memory_id: str, include_archived: bool = False) -> MemoryEntry | None:
await self.initialize()
result = await self._primary.get_memory(memory_id, include_archived)
if result is not None:
return result
try:
return await self._secondary.get_memory(memory_id, include_archived)
except Exception:
return None
async def query_memories(self, vector, options, collection_name=None) -> list[MemoryEntry]:
await self.initialize()
primary_task = asyncio.create_task(self._primary.query_memories(vector, options, collection_name))
secondary_task = asyncio.create_task(self._secondary.query_memories(vector, options, collection_name))
results_primary = await primary_task
try:
results_secondary = await secondary_task
except Exception as e:
logger.warning("team_server_query_failed", error=str(e)[:200], fallback="embedded_only")
return results_primary
ranked_ids = [[e.id for e in results_primary], [e.id for e in results_secondary]]
fused_ids = rrf_with_limit(ranked_ids, k=self._k, limit=options.top_k)
entries_by_id = {e.id: e for e in results_primary}
for e in results_secondary:
entries_by_id.setdefault(e.id, e)
return [entries_by_id[mid] for mid in fused_ids if mid in entries_by_id]
# ... all other read methods delegate to primary, same pattern
6.3 Tie-Breaking Policy¶
| Situation | Policy |
|---|---|
| Memory in both, same content | RRF boosts naturally (sum of both contributions) |
| Memory in both, different content | Primary (embedded) wins via setdefault() |
| Memory only in embedded | Single-contribution RRF score |
| Memory only in team | Single-contribution RRF score, re-hydrated from team |
| Team unreachable | Degrade to embedded-only |
| Primary (embedded) fails | Propagates to caller (embedded is authoritative) |
6.4 MCP Tool Surface¶
Existing query_memories MCP tool — no change. Fusion is transparent. get_status extended with embedded_server health report when in pgembed mode.
6.5 Team Install Path (Q6 Decision)¶
# pyproject.toml
[project.optional-dependencies]
team = [] # No extra deps needed; asyncpg is already core
pip install memini-ai[team] is identical to pip install memini-ai in terms of installed packages. The distinction is documented, not enforced: the README tells users to set:
export MEMINI_VECTOR_BACKEND=pgembed
export MEMINI_TEAM_DB_URL=postgresql://team-server/memini
export MEMINI_FUSION_MODE=rrf
The [team] extra exists as a discoverable marker for "I want team-aware setup." Future versions could add additional deps (e.g., a team-server-specific client) without breaking the marker.
7. Failure-Mode Table¶
| # | Scenario | Embedded State | Team State | Expected Behavior | Severity |
|---|---|---|---|---|---|
| 1 | User has only embedded configured | RUNNING | N/A | Return embedded results. No MEMINI_TEAM_DB_URL. create_database() returns plain PostgresDatabase. | — |
| 2 | User has only team configured | N/A | RUNNING | Return team results. MEMINI_DB_URL set. Identical to v0.8.2. | — |
| 3 | User has both, team reachable | RUNNING | RUNNING | RRF-fuse both. Parallel queries. Primary (embedded) wins on content conflicts. | — |
| 4 | User has both, team unreachable | RUNNING | DOWN | Degrade to embedded-only. secondary_task raises → caught → log warning → return primary. | LOW |
| 5 | User has both, team unreachable at startup | RUNNING | DOWN | RRFDatabase.initialize() catches secondary init failure → logs warning → continues. | LOW |
| 6 | User has both, embedded crashed mid-query | CRASHED | RUNNING | primary_task raises → propagates to caller. On retry, EmbeddedPGDriver detects DEAD and auto-restarts. | HIGH |
| 7 | User has both, embedded crashed, auto-restart | DEAD → STARTING → RUNNING | RUNNING | Next get_uri() detects DEAD → cleanup → restart. Memoris preserved. | MEDIUM |
| 8 | User has neither | NOT CONFIGURED | NOT CONFIGURED | create_database() raises clear ValueError. | HIGH |
| 9 | Team has vectorscale, embedded does not | HNSW | DiskANN | Same query, different index. RRF fuses ranks, not raw scores. Not a problem. | LOW |
| 10 | Team has different schema version than embedded | RUNNING (v1.0.0) | RUNNING (v0.8.2) | RRFDatabase.initialize() checks both. If incompatible, logs warning, disables team fusion. User must migrate team. | MEDIUM |
| 11 | Embedded data dir corrupted | DEAD | RUNNING | Driver detects corruption → DEAD. If team available, degrades to team-only. User must run memini-ai init --reset. | HIGH |
| 12 | Disk full on embedded data dir | RUNNING (writes fail) | RUNNING | add_memory fails. Heartbeat writes also fail → after 3 failures, self._healthy = False. Reads may still work. | HIGH |
| 13 | Embedded server port/socket conflict | N/A | N/A | Cannot happen. pgembed uses Unix domain sockets. | — |
| 14 | Two users on same machine, different data dirs | RUNNING (user A) | RUNNING (user B) | Each has own MEMINI_PGEMBED_DATA_DIR → independent servers. | — |
| 15 | NEW (Q2): heartbeat write fails (disk full) | RUNNING | any | Client logs warning, increments failure counter. At 3 failures, marks self._healthy = False. Server stays up — client's own queries still work. | LOW |
| 16 | NEW (Q2): all clients stale (last crashed) | DRAINING → STOPPED | any | Surviving client's sweep detects, initiates shutdown via election token. 5s grace period. Re-checks at end of grace; cancels if new heartbeat seen. Worst case: 195s from last crash. | MEDIUM |
| 17 | NEW (Q2): two clients both see all stale | DRAINING | any | _try_claim_shutdown() uses lock; only one writes shutdown_initiated_by. Second backs off. | LOW |
| 18 | NEW (Q2): new client during DRAINING | RUNNING | any | New client writes heartbeat, clears shutdown_initiated_by, sets state="running". Existing client re-checks, cancels drain. | LOW |
| 19 | NEW (Q3): team write fails (network blip) | RUNNING | RUNNING | team_write_failed warning logged, _team_write_failures counter incremented. No retry, no surface to user. | LOW |
| 20 | NEW (Q4): v0.8.2 user with MEMINI_DB_URL set, no MEMINI_VECTOR_BACKEND | NOT STARTED | N/A | RuntimeError raised at startup with clear remediation. User must set MEMINI_VECTOR_BACKEND=postgres-external or run memini-ai migrate. No silent data loss. | MEDIUM |
8. Test Strategy¶
8.1 pgembed Driver Tests (~40 tests, no Docker)¶
Isolation: per-test temp data dir, cleanup_mode='stop'.
| Category | What It Tests | Est. |
|---|---|---|
| Driver lifecycle | get_uri() → initialize() → shutdown() → is_ready() | 4 |
| Server start/stop | Verify postmaster.pid, asyncpg connection, cleanup | 3 |
| Schema init | _ensure_schema(), 13 tables, extensions installed | 3 |
| Vector operations | Insert + query 384-dim vector, cosine distance | 5 |
| Multi-process attach | Subprocess + parent share server, both query | 4 |
| Heartbeat writes | Client registers, writes heartbeat, others see it | 3 |
| Heartbeat stale sweep | All clients stale → sweep initiates shutdown, election works | 4 |
| Grace period cancel | New client heartbeats during drain → shutdown cancelled | 2 |
| Crash recovery | kill -9 a client, verify heartbeat expires, sweep cleans up | 3 |
| State file correctness | server.json reflects state through transitions | 4 |
| Health check | is_healthy(), get_health_report() | 3 |
| Subtotal | 40 |
8.2 RRF Fusion Tests (~15 tests)¶
Isolation: fake VectorDatabase implementations with canned ranked lists.
| Category | What It Tests | Est. |
|---|---|---|
| Fusion correctness | Known ranked lists → expected RRF output | 5 |
| Team unreachable | Mock raises → degrade to primary-only | 2 |
| Tie-breaking | Same memory_id in both → primary wins | 2 |
| Empty results | One or both backends empty → no crash | 2 |
| Parallel execution | Both queries fire concurrently | 2 |
| Write delegation | add_memory hits primary, async fires to team | 2 |
| Subtotal | 15 |
8.3 Existing Test Suite¶
Parametrize pg_db fixture for both backends — 809 existing tests × 2 backends = 1618 test runs in CI. Add MEMINI_VECTOR_BACKEND env var to control which backend is used in CI.
8.4 Config Validation Tests (~10 tests)¶
| Test | What It Tests | Est. |
|---|---|---|
| Valid backends | "pgembed", "postgres-external" accepted | 2 |
| Invalid backend | "chdb" raises ValidationError | 1 |
| Valid fusion modes | "none", "rrf" accepted | 2 |
| Invalid fusion mode | "condorcet" raises | 1 |
| v0.8.2 compat warning | MEMINI_DB_URL set + default vector_backend → warning printed | 1 |
| Explicit override | MEMINI_VECTOR_BACKEND=postgres-external + MEMINI_DB_URL → no warning | 1 |
| Subtotal | 8 |
8.5 CLI Tests (~7 tests)¶
| Test | What It Tests | Est. |
|---|---|---|
memini-ai init | Creates data dir, starts server, reports URI | 2 |
memini-ai status | Reports server state, memory count, health report | 2 |
memini-ai stop | Stops server, verifies server.json shows "stopped" | 2 |
memini-ai stop --force | Stops even with active handles | 1 |
| Subtotal | 7 |
8.6 Total New Tests¶
| Area | New Tests |
|---|---|
| EmbeddedPGDriver | 40 |
| RRFDatabase fusion | 15 |
| Config validation | 8 |
| CLI commands | 7 |
| Total | ~70 |
9. Implementation Plan (Task List)¶
9.1 Task Table¶
| # | Task | Files Touched | LOC Est | Type | Depends On |
|---|---|---|---|---|---|
| T1 | Create feature/embedded-pgembed-v1 from main (7eed224) | — | 0 | git | — |
| T2 | Add DatabaseDriver Protocol + ExternalPGDriver | src/memini_ai/postgres/driver.py (NEW) | ~80 | coder-friendly | T1 |
| T3 | Add EmbeddedPGDriver with state machine + heartbeat + sweep | src/memini_ai/postgres/driver.py (append) | ~390 | needs architect review | T2 |
| T4 | Add config fields: vector_backend, pgembed_data_dir, team_db_url, fusion_mode | src/memini_ai/config.py | ~40 | coder-friendly | T1 |
| T5 | Modify PostgresDatabase.__init__ to accept driver instead of db_url | src/memini_ai/postgres/database.py | ~15 | coder-friendly | T2, T3 |
| T6 | Update create_database() factory with v0.8.2 compat warning | src/memini_ai/memory/database.py | ~50 | coder-friendly | T4, T5 |
| T7 | Add RRFDatabase wrapper for embedded+team fusion with async dual-write | src/memini_ai/memory/rrf_database.py (NEW) | ~210 | needs architect review | T6 |
| T8 | Add memini-ai init/status/stop/migrate CLI commands | src/memini_ai/cli.py (NEW) | ~200 | coder-friendly | T3, T6 |
| T9 | Write EmbeddedPGDriver tests (40 tests) | tests/test_pgembed_driver.py (NEW) | ~400 | coder-friendly | T3 |
| T10 | Write RRFDatabase fusion tests (15 tests) | tests/test_rrf_database.py (NEW) | ~200 | coder-friendly | T7 |
| T11 | Write config validation + v0.8.2 compat warning tests (8 tests) | tests/test_config.py (append) | ~80 | coder-friendly | T4 |
| T12 | Parametrize existing test suite for pgembed backend | tests/conftest.py, tests/test_postgres_database.py | ~50 | coder-friendly | T6, T9 |
| T13 | Update pyproject.toml: add pgembed>=0.2.0 dep, optional [team] extra | pyproject.toml | ~10 | coder-friendly | T1 |
| T14 | Update README, CHANGELOG, write memini-ai migrate script | README.md, CHANGELOG.md, scripts/migrate_external_to_embedded.py (NEW) | ~400 | coder-friendly | T13 |
| T15 | Release: bumpversion --minor --apply, tag v1.0.0, push, publish to PyPI | — | 0 | release | T1-T14 |
9.2 Dependency Graph¶
T1 (branch)
├─ T2 (Protocol + ExternalPGDriver) ─────────────┐
├─ T3 (EmbeddedPGDriver + heartbeat) ────────────┤
├─ T4 (config fields) ───────────────────────────┤
└─ T13 (pyproject.toml) ─────────────────────────┤
│
T2 + T3 ──► T5 (modify PostgresDatabase.__init__) ┤
T4 + T5 ──► T6 (update create_database factory) │
T6 ──────► T7 (RRFDatabase wrapper) │
T3 + T6 ──► T8 (CLI commands) │
T3 ──────► T9 (driver tests) │
T7 ──────► T10 (fusion tests) │
T4 ──────► T11 (config tests) │
T6 + T9 ──► T12 (parametrize existing tests) │
T13 ─────► T14 (docs + migration script) │
T1-T14 ──► T15 (release) │
9.3 Parallelizable Groups¶
| Group | Tasks | Parallel? | Reason |
|---|---|---|---|
| A | T2, T3, T4, T13 | ✅ | Independent files |
| B | T5, T8, T9, T11 | ✅ | T5: T2+T3. T8: T3+T6. T9: T3. T11: T4. |
| C | T7, T12 | ✅ | T7: T6. T12: T6+T9. |
| D | T10 | After T7 | Needs RRFDatabase |
| E | T14 | After T13 | Needs pyproject.toml |
| F | T15 | After all | Release |
9.4 LOC Summary¶
| Category | New Files | Modified Files | New LOC | Modified LOC |
|---|---|---|---|---|
| Driver | 1 | 0 | ~470 | 0 |
| RRFDatabase | 1 | 0 | ~210 | 0 |
| Config | 0 | 1 | ~40 | 0 |
| Database | 0 | 2 | ~50 | ~15 |
| CLI | 1 | 0 | ~200 | 0 |
| Tests | 2 | 2 | ~680 | ~50 |
| Docs + Migration | 2 | 2 | ~400 | 0 |
| Total | 7 | 7 | ~2,050 | ~65 |
10. Risks¶
| # | Risk | Severity | Likelihood | Mitigation |
|---|---|---|---|---|
| 1 | pgembed 0.2.0 is unmaintained (last release Mar 2026) | MEDIUM | LOW | Apache 2.0, wraps standard Postgres 17 binaries. Can fork if needed. Wheel is self-contained (64MB). |
| 2 | Unix socket path length limits (108 bytes on Linux) | LOW | LOW | ~/.local/share/memini-ai/pgembed/data is 40 chars. Well within limit. |
| 3 | Data dir grows unbounded (Postgres WAL, dead tuples) | MEDIUM | MEDIUM | Postgres autovacuum enabled by default. Add memini-ai vacuum CLI in v1.1.0. |
| 4 | Heartbeat detection latency (195s worst case) | LOW | — | Acceptable: server consumes ~50MB RAM idle. 3 minutes of idle RAM after last client crashes is a reasonable trade-off for no external scheduler. |
| 5 | server.psql() segfaults on Python 3.13 (pgembed's bundled psql) | LOW | — | We use asyncpg, not server.psql(). |
| 6 | pgembed wheel is 64MB — install size | LOW | — | Acceptable for zero-dependency Postgres. Alternative (Docker) is 200MB+. The [postgres-external] extra lets users opt out (but the dep is still pulled in v1.0.0; will be optional in v1.1). |
| 7 | Q4 breaking change annoys v0.8.2 users | MEDIUM | MEDIUM | One-time startup warning with clear remediation. Optional memini-ai migrate script. User explicitly approved. |
| 8 | Q3 team write failures are silent (no retry) | LOW | MEDIUM | Counter exposed in get_status. User can monitor. Fire-and-forget was an explicit user choice. |
| 9 | First-client writes heartbeat config — subsequent clients must accept | LOW | LOW | Config is written once with defaults (60s/120s/10s). Subsequent clients read and adopt. If first client has weird config, others follow. (Could add a "config mismatch" warning in v1.1.) |
| 10 | vectorscale 0.9.0 in pgembed may differ from team server's version | LOW | MEDIUM | _detect_vectorscale() gracefully falls back to HNSW. |
| 11 | Two memini-ai processes on different machines (network scenario) | — | — | Out of scope for v1.0.0. pgembed is local-only. Network access would need a real Postgres server (postgres-external mode). |
| 12 | Migration script complexity (external → embedded) | MEDIUM | MEDIUM | memini-ai migrate uses pg_dump on the source + pg_restore on the target. Standard Postgres tooling, well-tested. ~200 LOC. |
Appendix A: Key File Reference¶
| File | LOC | Role in v1.0.0 |
|---|---|---|
src/memini_ai/postgres/database.py | 1820 | Sacred — ~15 lines changed (driver instead of db_url). 19 ABC methods unchanged. |
src/memini_ai/memory/database.py | 319 | Factory create_database() — ~50 LOC change for backend selection + v0.8.2 compat warning |
src/memini_ai/memory/rrf.py | 250 | Reused as-is — reciprocal_rank_fusion() already accepts variable-length inputs. No changes. |
src/memini_ai/memory/system.py | 1039 | Unchanged. |
src/memini_ai/config.py | 572 | +4 fields (vector_backend, pgembed_data_dir, team_db_url, fusion_mode), +2 validators |
src/memini_ai/postgres/schema.py | 631 | Unchanged — same 13 tables, same extensions, same indexes on both backends |
src/memini_ai/postgres/queries.py | 730 | Unchanged — same 63 query constants, same SQL on both backends |
src/memini_ai/postgres/driver.py | NEW | ~470 LOC — DatabaseDriver Protocol + EmbeddedPGDriver (heartbeat, sweep, state machine) + ExternalPGDriver (trivial) |
src/memini_ai/memory/rrf_database.py | NEW | ~210 LOC — RRFDatabase wrapper: writes go to primary, async dual-write to team, queries RRF-fuse both |
src/memini_ai/cli.py | NEW | ~200 LOC — init, status, stop, migrate |
scripts/migrate_external_to_embedded.py | NEW | ~200 LOC — pg_dump from external + pg_restore to embedded |
tests/test_pgembed_driver.py | NEW | ~400 LOC — 40 tests for EmbeddedPGDriver lifecycle, heartbeat, sweep, multi-process, crash recovery, state file |
tests/test_rrf_database.py | NEW | ~200 LOC — 15 tests for RRFDatabase fusion, degradation, tie-breaking, parallel execution, async dual-write |
pyproject.toml | EDIT | +pgembed>=0.2.0 in core deps, optional [team] extra (no extra deps, just a marker) |
Appendix B: Handoff Doc Corrections¶
The handoff doc (git show 8ce151a:HANDOFF-v1.0.0-pgembed.md) contained inaccuracies corrected by the smoke test:
| Claim in Handoff | Reality |
|---|---|
| "vectorscale — listed but marked unavailable" | 0.9.0 IS available. CREATE EXTENSION vectorscale succeeds. |
| "pgembed uses TCP ports" | Uses Unix domain sockets. URI: postgresql://postgres:@/postgres?host=<data_dir>. |
| "server.psql() works" | Segfaults on Python 3.13.13. Use asyncpg. |
| "chDB code exists on main and needs deletion" | Only on abandoned feature/chdb-migration branch. Not on main. Q5: leave the branch. |
"cli.py needs rewriting for chdb" | Doesn't exist on main. We're creating it fresh for init/status/stop/migrate. |
| "1-second startup, no Docker" | Correct — verified. |
| "real Postgres 17 with pgvector bundled" | Correct — PostgreSQL 17.9, pgvector 0.8.2, vectorscale 0.9.0. |
Appendix C: What We Are NOT Building¶
- NOT migrating the 941 live memories from
memini-postgresto pgembed. Docker container stays. pgembed is for NEW deployments. - NOT changing the MCP tool surface. All 35+ tools work identically.
- NOT touching the 19 ABC methods in
PostgresDatabase. Stable core. - NOT touching the
feature/postgres-in-process-researchbranch. Q5: leave it alone. - NOT adding runtime backend switching. Backend is chosen at startup via config.
- NOT supporting Windows. pgembed is Linux/macOS only.
- NOT building a "list backends" or "switch backend at runtime" feature.
- NOT auto-installing pgembed as opt-in. v1.0.0 ships it as a hard dep. v1.1+ may make it optional via
[embedded]extra.