Extract shared runtime factory for API and MCP - #64
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude review |
|
Claude finished @Atharva-Kanherkar's task in 1m 50s —— View job PR Review: Extract shared runtime factory for API and MCP
Overall: This is a clean, well-motivated refactor. The core goal — eliminating Issues1. Overbroad exception suppression — def _safe_contradiction_lookup(...):
try:
return active_service.contradiction_detector.find_potential_contradictions(record)
except (ValueError, Exception): # Exception already subsumes ValueError
return []
2. Potential assert runtime.semantic_store.get_by_id(runtime.semantic_store.get_all_records()[0].id) is not NoneIf records = runtime.semantic_store.get_all_records()
assert len(records) == 1
assert runtime.semantic_store.get_by_id(records[0].id) is not None3. Fragile private-attribute introspection — def _embedder_dimensions(embedder: TextEmbedder) -> int | None:
for attr in ("_dimensions", "dimensions"):
value = getattr(embedder, attr, None)
if isinstance(value, int):
return value
return NoneProbing Design Notes4. runtime = build_runtime(...)
self.media_store = runtime.media_store
self.bus = runtime.bus
# ... 7 more linesStoring the 5. CLI bypasses The CLI still constructs stores manually via 6. Non-atomic lazy service initialisation — def service() -> MemoryAPIService:
if app.state.service is None:
app.state.service = MemoryAPIService(**app.state.service_config)
return app.state.serviceUnder concurrent requests two threads can both observe What's Good
|
There was a problem hiding this comment.
Pull request overview
This PR extracts a shared runtime.py factory to centralize wiring of the event bus, stores, retriever, contradiction detector, forgetting service, and event recorder, so both FastAPI and the upcoming MCP server can share the same composition path while avoiding unsafe global config mutation.
Changes:
- Added
runtime.build_runtime()/MemoryRuntimecontainer and migratedapi/app.pyto consume it. - Made Chroma DB paths injectable per store/runtime and made embedding dimensions injectable (and enforced in retriever/vector normalization).
- Added focused runtime tests for Chroma path isolation and embedding-dimension validation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
runtime.py |
New shared runtime/container factory and event recording support. |
api/app.py |
Replaces in-module wiring/config mutation with build_runtime() and adds embedding_dimensions injection. |
stores/semantic_store.py |
Allows injecting chroma_path into Chroma PersistentClient construction. |
stores/episodic_store.py |
Allows injecting chroma_path into Chroma PersistentClient construction. |
stores/procedural_store.py |
Allows injecting chroma_path into Chroma PersistentClient construction. |
retrieval/retriever.py |
Adds injectable embedding_dimensions for query-vector validation. |
utils/embeddings.py |
Adds embedder dimensions surface and makes Gemini embedding dimensionality runtime-configurable. |
tests/test_runtime.py |
New tests verifying runtime isolation (paths/config) and dimension validation behavior. |
tests/helpers.py |
Updates test embedder to expose dimensions. |
demo/cli.py |
Small refactor of local wiring helpers/import order (keeps CLI wiring separate from shared runtime). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
|
|
||
| def _embedder_dimensions(embedder: TextEmbedder) -> int | None: | ||
| return embedder.dimensions |
There was a problem hiding this comment.
_embedder_dimensions() assumes every passed embedder has a .dimensions attribute. If a caller provides a duck-typed embedder that only implements embed_text/embed_query (common in existing tests/stubs), build_runtime() will raise AttributeError. To keep the runtime factory more resilient, consider using getattr(embedder, "dimensions", None) (or otherwise gracefully treating missing dimensions as unknown).
| return embedder.dimensions | |
| return getattr(embedder, "dimensions", None) |
| ): | ||
| super().__init__(event_bus=event_bus) | ||
| client = chromadb.PersistentClient(path=config.CHROMA_DB_PATH) | ||
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) |
There was a problem hiding this comment.
chroma_path or config.CHROMA_DB_PATH treats an empty string the same as None, silently falling back to the global default path. That can accidentally defeat per-runtime path isolation if an empty/blank value is passed through. Prefer an explicit None check (and consider rejecting blank strings) so invalid paths fail fast instead of writing to the shared default DB.
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) | |
| if chroma_path is None: | |
| effective_chroma_path = config.CHROMA_DB_PATH | |
| else: | |
| if isinstance(chroma_path, str) and chroma_path.strip() == "": | |
| raise ValueError("chroma_path must not be empty or only whitespace") | |
| effective_chroma_path = chroma_path | |
| client = chromadb.PersistentClient(path=effective_chroma_path) |
| ): | ||
| super().__init__(event_bus=event_bus) | ||
| client = chromadb.PersistentClient(path=config.CHROMA_DB_PATH) | ||
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) |
There was a problem hiding this comment.
chroma_path or config.CHROMA_DB_PATH treats an empty string the same as None, silently falling back to the global default path. That can accidentally defeat per-runtime path isolation if an empty/blank value is passed through. Prefer an explicit None check (and consider rejecting blank strings) so invalid paths fail fast instead of writing to the shared default DB.
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) | |
| if chroma_path is None: | |
| resolved_chroma_path = config.CHROMA_DB_PATH | |
| else: | |
| # Reject empty or whitespace-only paths so they don't silently | |
| # fall back to the global default Chroma DB path. | |
| if isinstance(chroma_path, str) and not chroma_path.strip(): | |
| raise ValueError("chroma_path must be a non-empty string or None.") | |
| resolved_chroma_path = chroma_path | |
| client = chromadb.PersistentClient(path=resolved_chroma_path) |
| ): | ||
| super().__init__(event_bus=event_bus) | ||
| client = chromadb.PersistentClient(path=config.CHROMA_DB_PATH) | ||
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) |
There was a problem hiding this comment.
chroma_path or config.CHROMA_DB_PATH treats an empty string the same as None, silently falling back to the global default path. That can accidentally defeat per-runtime path isolation if an empty/blank value is passed through. Prefer an explicit None check (and consider rejecting blank strings) so invalid paths fail fast instead of writing to the shared default DB.
| client = chromadb.PersistentClient(path=chroma_path or config.CHROMA_DB_PATH) | |
| if chroma_path is None: | |
| db_path = config.CHROMA_DB_PATH | |
| else: | |
| # Reject empty or whitespace-only paths to avoid silently falling back | |
| if isinstance(chroma_path, str) and not chroma_path.strip(): | |
| raise ValueError("chroma_path must be a non-empty path when provided") | |
| db_path = chroma_path | |
| client = chromadb.PersistentClient(path=db_path) |
| def _safe_contradiction_lookup( | ||
| active_service: MemoryAPIService, | ||
| record: SemanticMemory, | ||
| ) -> list[ContradictionCandidate]: | ||
| try: | ||
| return active_service.contradiction_detector.find_potential_contradictions(record) | ||
| except (ValueError, Exception): | ||
| except ValueError: | ||
| return [] |
There was a problem hiding this comment.
_safe_contradiction_lookup() now only catches ValueError. If Chroma (or any other unexpected error) occurs during contradiction lookup, the API will return a 500 after the semantic record has already been stored, which can lead to client retries and duplicate writes. Consider catching broader exceptions here (at least the likely runtime/storage exceptions) and returning an empty candidate list (or a structured "lookup_status") so the write endpoint remains idempotent/safe on transient failures.
Summary
Why
Issue #58 requires the runtime wiring to move out of api/app.py so FastAPI and the upcoming MCP server can share the same composition path. The old initialization flow mutated module-level config while constructing stores, which was unsafe if multiple runtimes were built concurrently with different Chroma paths.
Validation
Issue Links