Skip to content

feat(memory): pluggable storage backends#998

Open
erma07 wants to merge 2 commits intoRightNow-AI:mainfrom
erma07:feat/pluggable-memory-backends
Open

feat(memory): pluggable storage backends#998
erma07 wants to merge 2 commits intoRightNow-AI:mainfrom
erma07:feat/pluggable-memory-backends

Conversation

@erma07
Copy link
Copy Markdown

@erma07 erma07 commented Apr 6, 2026

Summary

Redesign the openfang-memory crate with pluggable storage backends. The main storage backend (SQLite or PostgreSQL) and the semantic/vector backend (SQLite, PostgreSQL, Qdrant, HTTP) are now independently configurable, allowing mix-and-match deployments like PostgreSQL for structured data with Qdrant for vector search.

Architecture

The orchestration layer (substrate.rs) is now 100% backend-agnostic — zero database-specific imports. All storage is abstracted through 9 backend traits with implementations per database:

Trait SQLite PostgreSQL Qdrant HTTP
Structured x x
Semantic x x x x
Knowledge x x
Session x x
Usage x x
PairedDevices x x
TaskQueue x x
Consolidation x x
Audit x x

Each backend lives in its own folder (sqlite/, postgres/, qdrant/, http/) with identical file structure (11 files each for SQLite and PostgreSQL). Shared serialization and parsing logic is extracted into helpers.rs to eliminate cross-backend duplication.

SessionBackend uses Rust default trait implementations for 5 methods (create_session, create_session_with_label, append_canonical, canonical_context, store_llm_summary) — new backends only need to implement the storage primitives.

Changes

  • New backend traits: PairedDevicesBackend, TaskQueueBackend, ConsolidationBackend, AuditBackend (added to existing StructuredBackend, SemanticBackend, KnowledgeBackend, SessionBackend, UsageBackend)
  • PostgreSQL: full parity with SQLite — all 9 backends implemented with pgvector for vectors, versioned migrations matching SQLite v1–v9
  • Qdrant: semantic-only backend via gRPC with auto-collection creation and cosine similarity search
  • HTTP: semantic-only backend routing to a remote memory-api gateway with automatic fallback to local storage
  • Folder restructure: SQLite code moved from top-level into sqlite/, all database-specific code isolated in its backend folder
  • External callers migrated: kernel, API routes, and runtime now use trait-based APIs (Arc<dyn UsageBackend>, AuditBackend) — no more leaked rusqlite::Connection types
  • New config field: semantic_backend allows independent vector backend selection
  • Docker Compose: added pgvector/pgvector:pg18 and qdrant/qdrant:latest services for integration testing

Testing

cargo clippy --workspace --all-targets -- -D warnings passes
cargo test --workspace passes (65 tests: 40 unit + 25 integration)
Integration tested against live PostgreSQL (pgvector/pg18) and Qdrant
All SQLite, PostgreSQL, and Qdrant backends verified with identical test suite

Security

No new unsafe code (existing sqlite-vec FFI registration unchanged)
No secrets or API keys in diff
User input validated at boundaries
usage_conn() removed — no more raw database connection leaks to external crates

Configuration

[memory]
backend = "sqlite"              # main storage: "sqlite" or "postgres"
semantic_backend = "qdrant"     # vector search: "sqlite", "postgres", "qdrant", or "http"

postgres_url = "postgresql://user:pass@localhost/openfang"
qdrant_url = "http://localhost:6334"


Copy link
Copy Markdown
Member

@jaberjaber23 jaberjaber23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Audit — APPROVED (with rebase needed)

Verdict: APPROVE — clean, well-architected refactor. Cannot merge due to conflicts.

Security findings:

  1. No data exfiltration — The HTTP semantic backend only routes to a user-configured memory_api_url from config, with automatic fallback to local storage on failure. No hardcoded external endpoints.

  2. No SQL injection — PostgreSQL backend uses parameterized queries ($1, $2 etc.) throughout. No format!() with SQL + user data found.

  3. Unsafe blocks are legitimate — All 5 unsafe blocks are for sqlite_vec::sqlite3_vec_init FFI registration (SQLite vector extension). This is an existing pattern that was moved, not introduced. The transmute is required for the SQLite auto-extension API.

  4. No secrets in diff — Docker Compose test credentials (POSTGRES_PASSWORD: openfang) are for local integration testing only, under the test/db profile. Acceptable.

  5. No new unsafe code paths — The trait-based architecture is actually more secure than before: usage_conn() which leaked raw rusqlite::Connection to external crates has been removed. All external callers now go through Arc<dyn UsageBackend> trait objects.

  6. Line count justified — 7171 additions across 22 Rust files + Cargo.lock changes. The breakdown: ~1300 lines Cargo.lock dependency updates, ~2000 lines PostgreSQL backend (11 files mirroring SQLite), ~700 lines Qdrant backend, ~300 lines HTTP backend rename/extension, ~800 lines backend traits + helpers, ~500 lines migration code, ~500 lines tests, ~1000 lines SQLite file reorganization (moved, not new).

  7. Qdrant backend — Uses official qdrant-client crate via gRPC. Auto-creates collections with cosine similarity. No authentication bypass.

The PR has merge conflicts (DIRTY state). Author needs to rebase before merge.

@jaberjaber23
Copy link
Copy Markdown
Member

This PR has merge conflicts. Please rebase onto the latest main branch and resolve conflicts so we can merge.

@erma07 erma07 force-pushed the feat/pluggable-memory-backends branch from f2ec259 to d42e5f7 Compare April 11, 2026 05:29
@erma07
Copy link
Copy Markdown
Author

erma07 commented Apr 12, 2026

resolved

Copy link
Copy Markdown
Member

@jaberjaber23 jaberjaber23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the ambitious pluggable-memory work @erma07 — the direction (trait split, SQLite vs Postgres vs Qdrant, HTTP store) is something we want. But at +7243/-2112 across 48 files, this has to land clean. Requesting changes.

Blockers

  1. HttpSemanticStore::remember returns a random MemoryId::new() instead of the server's ID (crates/openfang-memory/src/http/semantic.rs — around the remember impl). It logs resp.id but discards it. forget and update_embedding then go to fallback with wrong IDs — silently broken for the entire HTTP mode.
  2. HttpSemanticStore::recall fabricates a fresh MemoryId::new() per row — IDs are unstable across calls, so dedup/forget/update semantics don't work at all for HTTP recall.
  3. MemorySubstrate::open_postgres uses tokio::runtime::Handle::current().block_on(...). Panics if MemorySubstrate::open is ever called outside a Tokio runtime. Confirm the kernel's boot path and either guarantee a runtime is active or switch to async-open.
  4. Docs vs code disagreement on semantic_backend. substrate.rs header comments list "qdrant" and "postgres+qdrant" as valid values, but select_semantic has no "postgres" arm — Ok(default) fallback silently accepts unknown values. Either implement Postgres-backed semantic or remove it from the docs.
  5. Cargo.lock shows workspace version 0.5.9 → 0.5.5 and drops rustls from openfang-kernel. Almost certainly a wrong-base-branch artifact. Please rebase on current main and regenerate the lockfile.
  6. Breaking API changes without a migration note. usage_conn() is removed and module paths (consolidation, knowledge, etc.) moved under sqlite::…. External consumers using use openfang_memory::knowledge::… will break. Either add re-exports for one release or add a clear MIGRATION note.
  7. Silent audit-log restore failure. AuditLog::with_backend uses if let Ok(rows) = backend.load_entries(...) and proceeds as if empty on Err. For a security audit trail, a failed load should at least warn loudly, ideally fail-closed.

Concerns

  • unsafe FFI registration for sqlite-vec via sqlite3_auto_extension + transmute in the production SqliteBackend::open path — the PR claim "no new unsafe" is inaccurate. The usage is standard for the crate, but please be explicit in the docs/changelog.
  • QdrantSemanticStore::recall returns Ok(vec![]) when query_embedding is None. Should return Err or an explicit "embedding required" error.
  • JSONL mirror writes sessions_dir.join(format!("{}.jsonl", session.id.0)) — document that SessionId is UUID-only so reviewers can rule out path traversal.
  • docker-compose.yml adds POSTGRES_PASSWORD: openfang — fine for the dev profile, please mark it so nobody copies it to prod.
  • Whether openfang-kernel / the binary crates enable the postgres / qdrant features on openfang-memory isn't visible in the snippets I reviewed — please confirm end-to-end wiring.

New dependencies (all direct adds trustworthy, some supply-chain work needed)

sqlite-vec, hex, sha2, tokio-postgres, deadpool-postgres, pgvector, qdrant-client — names match well-known crates, not typosquats. sqlite-vec is specialized; please confirm it's maintained and has wheels/builds for all platforms in the release matrix (Windows in particular). The transitive set (tonic, prost-types, hyper-timeout, deadpool, stringprep, etc.) is typical for gRPC + Postgres and acceptable.

Recommendation

Land as-is: no. Land after fixes: yes, this is valuable. Specifically:

  1. Fix the HTTP ID handling (blockers 1 & 2).
  2. Either implement or remove semantic_backend = "postgres".
  3. Rebase and regenerate Cargo.lock cleanly against current main.
  4. Resolve the Tokio-handle concern for open_postgres.
  5. Restore behavior on audit-log load failure.
  6. Integration-test the hybrid matrix (backend = "sqlite" + semantic_backend = "qdrant", etc.) — the backend_integration.rs test file is there, just make sure CI actually runs it with the postgres,qdrant features enabled.

Happy to re-review once those are in.

erma07 added 2 commits April 18, 2026 12:45
…t support

Redesign the openfang-memory crate for pluggable storage backends.
The main backend (sqlite or postgres) and semantic backend (sqlite,
postgres, qdrant, http) are independently configurable.

Architecture:
- substrate.rs is 100% backend-agnostic (zero rusqlite imports)
- 9 backend traits: Structured, Semantic, Knowledge, Session, Usage,
  PairedDevices, TaskQueue, Consolidation, Audit
- SessionBackend has 5 default trait impls (create_session,
  canonical_context, append_canonical, store_llm_summary, etc.)
- Shared helpers.rs for serialization/parsing across backends
- JSONL session mirror extracted to standalone filesystem utility

Backends:
- sqlite/  — 11 files, full implementation with sqlite-vec vectors
- postgres/ — 11 files, full implementation with pgvector
- qdrant/  — semantic-only, gRPC vector similarity search
- http/    — semantic-only, remote memory-api gateway with fallback

External callers migrated:
- kernel uses memory.usage_arc() and memory.audit() (was usage_conn())
- api routes use memory.usage() trait method
- runtime AuditLog uses AuditBackend trait (was raw rusqlite Connection)
- MeteringEngine accepts Arc<dyn UsageBackend> (was Arc<UsageStore>)

Config:
  [memory]
  backend = "sqlite"           # or "postgres"
  semantic_backend = "qdrant"  # independently: sqlite/postgres/qdrant/http

Docker: pgvector/pg18 + qdrant services for integration testing.
65 tests (40 unit + 25 integration) across all backends.
…ructure

Resolves the seven blockers and five concerns from the pluggable-memory
PR review, plus a restructure pass that tightens correctness + ergonomics.

Blockers
- HTTP semantic store parses server-emitted UUIDs via a new parse_memory_id
  helper instead of fabricating MemoryId::new(); forget/update_embedding
  return explicit Err instead of silently delegating to a disconnected
  fallback store (B1, B2).
- New async MemorySubstrate::open_async does all Postgres init via real
  .await; no more nested Handle::current().block_on. Kernel boot uses it;
  sync open() errors cleanly for async-only backends (B3).
- PostgresSemanticStore implemented over deadpool-postgres + pgvector.
  Config is now typed: MemoryBackendKind / SemanticBackendKind with
  rename_all=snake_case + deny_unknown_fields. Dispatch is an exhaustive
  enum match; unknown values are rejected at parse time (B4).
- Rebased onto v0.5.10, Cargo.lock regenerated cleanly (B5).
- MIGRATION.md documents every break with before/after snippets (B6).
- AuditLog::with_backend now returns OpenFangResult<Self>; load errors
  and integrity-check failures both tracing::error! and propagate via ?.
  Boot fails closed on a broken audit trail (B7).

Concerns
- SAFETY comments on both sqlite-vec FFI registration blocks (C1).
- Qdrant recall returns Err on None embedding; Qdrant/HTTP init now
  fail-fast with the backend name + URL in the error message, no silent
  SQLite fallback (C2).
- jsonl.rs documents that SessionId wraps uuid::Uuid, so the filename
  component is path-traversal safe (C3).
- docker-compose.yml is fully env-overridable with dev-only comments (C4).
- Feature forwarding wired end-to-end: openfang-memory -> openfang-kernel
  -> openfang-cli (C5). Test feature_gated_backend_errors_cleanly_when_
  feature_off locks the behavior in.

Restructure pass
- Dropped DEFAULT from the Postgres task_queue v1 migration so it matches
  SQLite's strict NOT NULL semantics.
- Error classification fix: "no Postgres pool available" is now Config
  (caller misuse), consistent with sibling feature-gate mismatches.
- Qdrant recall skips + warns on malformed payloads instead of fabricating
  MemoryId/AgentId; mirrors the HTTP-side fix.
- QdrantCollectionGuard RAII teardown deletes test collections on Drop.
- MemoryConfig connection fields grouped into typed structs
  (PostgresConnConfig, QdrantConnConfig, HttpMemoryConnConfig) via
  serde(flatten) -- TOML shape is byte-identical; Rust consumers that
  construct MemoryConfig by hand must update to nested fields.
- Unit tests for parse_memory_id; error-path test for Postgres pool
  misuse; four hybrid-matrix integration tests (sqlite+qdrant,
  sqlite+postgres, postgres+qdrant, qdrant_unreachable_fails_fast).
- Removed dead PgSemanticStore alias and RawGraphRow::r_id field.

CI: new integration-backends job brings up pgvector/pgvector:pg16 and
qdrant/qdrant:latest as services and runs
cargo test -p openfang-memory --features postgres,qdrant --test
backend_integration, exercising the hybrid matrix end-to-end.

Verification: cargo build across default / --no-default-features /
--features postgres,qdrant all green; 2302 workspace lib tests pass;
clippy clean on the same matrix with -D warnings.
@erma07 erma07 force-pushed the feat/pluggable-memory-backends branch from d42e5f7 to 32196b7 Compare April 18, 2026 12:46
@erma07
Copy link
Copy Markdown
Author

erma07 commented Apr 18, 2026

@jaberjaber23 , please review the changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants