From d236b4befd79ae36feee94434c4548bce3d7c557 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Thu, 30 Apr 2026 14:09:46 -0700 Subject: [PATCH] fix(memory): make numpy import optional for proxy boot path The proxy startup chain eagerly loads `headroom.memory.adapters`, which imports `ports.py` and `sqlite.py`. Both files declared `import numpy as np` at module top even though numpy is not in base dependencies (it is only under `[all]`, `[dev]`, `[evals]`, and `[relevance]` extras). As a result, `pipx install 'headroom-ai[mcp]' && headroom proxy` fails with `ModuleNotFoundError: No module named 'numpy'` even when memory features are disabled (the default). Both files already use `from __future__ import annotations`, so the `np.ndarray` type hints are strings at runtime - numpy is only needed for static type checking. Move the module-level `import numpy as np` under a `TYPE_CHECKING` guard. For the only runtime users (`SQLiteMemoryStore._serialize_embedding` and `_deserialize_embedding`), add a local `import numpy as np` so they raise a clear `ImportError` only when embeddings are actually persisted. Net effect: the proxy boots without numpy installed; memory features that actually use numpy still work when its optional deps are present. Fixes #332 --- headroom/memory/adapters/sqlite.py | 11 ++++++++--- headroom/memory/ports.py | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/headroom/memory/adapters/sqlite.py b/headroom/memory/adapters/sqlite.py index 1a2d7c2a3..66ffb56f9 100644 --- a/headroom/memory/adapters/sqlite.py +++ b/headroom/memory/adapters/sqlite.py @@ -14,13 +14,14 @@ import re import sqlite3 from datetime import datetime, timezone from pathlib import Path -from typing import Any - -import numpy as np +from typing import TYPE_CHECKING, Any from ..models import Memory, ScopeLevel from ..ports import MemoryFilter +if TYPE_CHECKING: + import numpy as np + # Regex pattern for safe metadata keys: alphanumeric, underscores, hyphens only # This prevents JSON path injection attacks via malicious key names _SAFE_METADATA_KEY_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") @@ -165,6 +166,8 @@ class SQLiteMemoryStore: """Serialize numpy array to bytes for BLOB storage.""" if embedding is None: return None + import numpy as np + return bytes(embedding.astype(np.float32).tobytes()) def _deserialize_embedding( @@ -173,6 +176,8 @@ class SQLiteMemoryStore: """Deserialize bytes back to numpy array.""" if data is None: return None + import numpy as np + arr = np.frombuffer(data, dtype=np.float32) return arr diff --git a/headroom/memory/ports.py b/headroom/memory/ports.py index 53d05924a..fd2790cf2 100644 --- a/headroom/memory/ports.py +++ b/headroom/memory/ports.py @@ -5,12 +5,13 @@ from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Protocol, runtime_checkable - -import numpy as np +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from headroom.memory.models import Memory, ScopeLevel +if TYPE_CHECKING: + import numpy as np + # ============================================================================= # Filter Dataclasses # =============================================================================