diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed
index 3848a718..fd6afcbc 100644
--- a/.pytest_cache/v/cache/lastfailed
+++ b/.pytest_cache/v/cache/lastfailed
@@ -41,11 +41,6 @@
"tests/shared/test_importance_scorer.py::test_scorer_with_emotion_engine": true,
"tests/shared/test_importance_scorer.py::test_dynamic_weight_update": true,
"tests/shared/test_importance_scorer.py::test_tech_context_bonus": true,
- "tests/test_core/test_core.py::test_reflex_buffer": true,
- "tests/test_features/test_backup_path_safety.py::test_restore_rejects_traversal[manifest_files0-True]": true,
- "tests/test_features/test_backup_path_safety.py::test_restore_rejects_traversal[manifest_files1-True]": true,
- "tests/test_features/test_import_export_path_safety.py::test_import_rejects_traversal[../../etc/passwd]": true,
- "tests/test_features/test_import_export_path_safety.py::test_import_rejects_traversal[/etc/passwd]": true,
"tests/test_hooks/test_registry_core.py::test_manual_registration": true,
"tests/test_hypothesis.py::test_chaos_db_locked_graceful": true,
"tests/test_hypothesis.py::test_chaos_api_timeout_does_not_hang": true,
diff --git a/config.py b/config.py
index c3849e3a..fd51ba64 100644
--- a/config.py
+++ b/config.py
@@ -67,21 +67,23 @@ def is_hook_enabled(self, layer: str, hook: str) -> bool:
"wiki_agent",
],
}
- if hook in known_hooks.get(layer, []):
- return self.get("hooks", layer, hook, default=True)
- return self.get("hooks", layer, hook, default=False)
+ res = self.get("hooks", layer, hook, default=True) if hook in known_hooks.get(layer, []) else self.get("hooks", layer, hook, default=False)
+ return bool(res)
def is_feature_enabled(self, feature: str) -> bool:
- return self.get("features", feature, default=False)
+ return bool(self.get("features", feature, default=False))
- def get_wiki_types(self, layer: str) -> list:
- return self.get("wiki", layer, default=[])
+ def get_wiki_types(self, layer: str) -> list[str]:
+ res: Any = self.get("wiki", layer, default=[])
+ if not isinstance(res, list):
+ return []
+ return [str(x) for x in res]
def get_limit(self, key: str) -> int:
- return self.get("limits", key, default=0)
+ return int(self.get("limits", key, default=0))
def get_forgetting(self, key: str) -> float:
- return self.get("forgetting", key, default=0.0)
+ return float(self.get("forgetting", key, default=0.0))
config = Config()
diff --git a/core/__init__.py b/core/__init__.py
index 6918db53..11d38cea 100644
--- a/core/__init__.py
+++ b/core/__init__.py
@@ -5,7 +5,7 @@
Two-layer: user facts + agent identity
"""
-from typing import Optional
+from typing import Optional, Any
from config import config
from shared.connection import AsyncConnectionManager, connection_manager
@@ -19,11 +19,11 @@
class MemoryLayer:
"""Unified async memory layer for both user and agent."""
- def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectionManager | None = None, cache=None):
+ def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectionManager | None = None, cache: Any = None) -> None:
self.layer_type = layer_type
self.user_id = user_id
self._cm = cm or connection_manager
- self._cache = cache
+ self._cache: Any = cache
self.l1 = ReflexBuffer(max_size=config.get_limit("l1_buffer_size"))
self.l2 = SessionStore(cm=self._cm)
self.l3 = EpisodicMemory(cm=self._cm)
@@ -32,14 +32,15 @@ def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectio
async def remember(self, key: str, value: str, importance: float = 0.5) -> int:
return await self.l4.save(self.user_id, key, value, importance)
- async def recall(self, query: str, limit: int = 10) -> list[dict]:
+ async def recall(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
cache_key = f"recall:{self.user_id}:{query}:{limit}"
- cached = self._cache.get(cache_key) if self._cache else None
+ cached: Any = self._cache.get(cache_key) if self._cache else None
if cached is not None:
- return cached
+ return list(cached) if isinstance(cached, list) else []
- results = []
- results.extend(await self.l4.search(self.user_id, query, limit))
+ results: list[dict[str, Any]] = []
+ l4_hits = await self.l4.search(self.user_id, query, limit)
+ results.extend(l4_hits)
episodes = await self.l3.search(self.user_id, query, limit)
results.extend([{"summary": e.summary, "weight": e.emotional_weight} for e in episodes])
final = results[:limit]
@@ -53,24 +54,24 @@ async def forget(self, key: str) -> bool:
return await self.l4.delete(self.user_id, key)
async def get_context(self) -> str:
- parts = []
+ parts: list[str] = []
recent = self.l1.get_recent(5)
if recent:
- parts.append("RECENT: " + "; ".join([r.content[:50] for r in recent]))
+ parts.append("RECENT: " + "; ".join([str(r.content)[:50] for r in recent]))
facts = await self.l4.get_all(self.user_id, limit=10)
if facts:
- parts.append("FACTS: " + "; ".join([f"{f.key}={f.value[:30]}" for f in facts]))
+ parts.append("FACTS: " + "; ".join([f"{f.key}={str(f.value)[:30]}" for f in facts]))
return "\n".join(parts)
- async def cleanup(self) -> dict:
+ async def cleanup(self) -> dict[str, int]:
archived = await self.l3.archive_old(self.user_id)
return {"archived": archived}
class MemoryManager:
- def __init__(self, cm: AsyncConnectionManager | None = None, cache=None):
+ def __init__(self, cm: AsyncConnectionManager | None = None, cache: Any = None) -> None:
self._cm = cm or connection_manager
- self._cache = cache
+ self._cache: Any = cache
self.layers: dict[str, MemoryLayer] = {}
def get_layer(self, layer_type: str, user_id: str = "default") -> MemoryLayer:
@@ -85,8 +86,8 @@ def user_memory(self, user_id: str = "default") -> MemoryLayer:
def agent_memory(self, user_id: str = "default") -> MemoryLayer:
return self.get_layer("agent", user_id)
- async def cleanup_all(self) -> dict:
- results = {}
+ async def cleanup_all(self) -> dict[str, dict[str, int]]:
+ results: dict[str, dict[str, int]] = {}
for key, layer in self.layers.items():
results[key] = await layer.cleanup()
return results
diff --git a/core/episodic.py b/core/episodic.py
index b4bde6a6..06c83540 100644
--- a/core/episodic.py
+++ b/core/episodic.py
@@ -7,6 +7,7 @@
import json
import time
from dataclasses import dataclass
+from typing import Any
from shared.connection import AsyncConnectionManager, connection_manager
from shared.constants import DB_NAME
@@ -26,7 +27,7 @@ class EpisodicMemory:
def __init__(self, cm: AsyncConnectionManager | None = None):
self._cm = cm or connection_manager
- async def _init_db(self):
+ async def _init_db(self) -> None:
await self._cm.execute_script(
DB_NAME,
"""
@@ -47,10 +48,10 @@ async def save(self, user_id: str, summary: str, emotional_weight: float = 0.5,
conn = await self._cm.get(DB_NAME)
cursor = await conn.execute(
"INSERT INTO episodes (user_id, summary, emotional_weight, tags, created_at) VALUES (?, ?, ?, ?, ?)",
- (user_id, summary, emotional_weight, json.dumps(tags or []), time.time()),
+ (user_id, summary, emotional_weight, json.dumps(tags or [])),
)
await conn.commit()
- return cursor.lastrowid
+ return int(cursor.lastrowid or 0)
async def get_episodes(self, user_id: str, limit: int = 20, offset: int = 0) -> list[Episode]:
conn = await self._cm.get(DB_NAME)
@@ -70,7 +71,7 @@ async def search_by_tag(self, user_id: str, tag: str, limit: int = 10) -> list[E
rows = await cursor.fetchall()
return [self._row_to_episode(r) for r in rows]
- async def search(self, user_id: str, query: str, limit: int = 10) -> list:
+ async def search(self, user_id: str, query: str, limit: int = 10) -> list[Episode]:
conn = await self._cm.get(DB_NAME)
cursor = await conn.execute(
"SELECT * FROM episodes WHERE user_id=? AND summary LIKE ? ORDER BY created_at DESC LIMIT ?",
@@ -98,21 +99,21 @@ async def archive_old(self, user_id: str, days: int = 90) -> int:
for row in rows:
await am.archive(
user_id=user_id,
- content=row["summary"],
+ content=str(row["summary"]),
memory_type="episode",
- importance=row["emotional_weight"],
- original_id=row["episode_id"],
+ importance=float(row["emotional_weight"]),
+ original_id=int(row["episode_id"]),
reason=f"inactive_{days}d",
)
archived_count += 1
- ids = [row["episode_id"] for row in rows]
+ ids = [int(row["episode_id"]) for row in rows]
if not ids:
return archived_count
placeholders = ",".join(["?"] * len(ids))
sql = f"DELETE FROM episodes WHERE episode_id IN ({placeholders})"
- await conn.execute(sql, ids)
+ await conn.execute(sql, tuple(ids))
await conn.commit()
return archived_count
@@ -124,14 +125,14 @@ async def count(self, user_id: str) -> int:
(user_id,),
)
row = await cursor.fetchone()
- return row["cnt"] if row else 0
+ return int(row["cnt"]) if row and row[0] is not None else 0
- def _row_to_episode(self, row) -> Episode:
+ def _row_to_episode(self, row: dict[str, Any] | Any) -> Episode:
return Episode(
- episode_id=row["episode_id"],
- user_id=row["user_id"],
- summary=row["summary"],
- emotional_weight=row["emotional_weight"],
- tags=json.loads(row["tags"]) if row["tags"] else [],
- created_at=row["created_at"],
+ episode_id=int(row["episode_id"]),
+ user_id=str(row["user_id"]),
+ summary=str(row["summary"]),
+ emotional_weight=float(row["emotional_weight"]),
+ tags=list(json.loads(row["tags"])) if row["tags"] else [],
+ created_at=float(row["created_at"]),
)
diff --git a/core/memory.py b/core/memory.py
index 1f696f03..4e8507cd 100644
--- a/core/memory.py
+++ b/core/memory.py
@@ -32,7 +32,7 @@ class CoreMemory:
def __init__(self, cm: AsyncConnectionManager | None = None):
self._cm = cm or connection_manager
- async def _init_db(self):
+ async def _init_db(self) -> None:
await self._cm.execute_script(
DB_NAME,
"""
@@ -61,7 +61,7 @@ async def save(
memory_kind: str | None = None,
expires_at: float | None = None,
source: str = "manual",
- metadata: dict | None = None,
+ metadata: dict[str, Any] | None = None,
) -> int:
from shared.memory_types import (
MemoryKind,
@@ -105,9 +105,9 @@ async def save(
"""UPDATE core_memory SET value=?, importance=?, memory_kind=?,
expires_at=?, source=?, metadata=?, updated_at=?
WHERE entry_id=?""",
- (value, importance, memory_kind, expires_at, source, metadata_json, now, existing["entry_id"]),
+ (value, importance, memory_kind, expires_at, source, metadata_json, now, int(existing["entry_id"])),
)
- entry_id = existing["entry_id"]
+ entry_id = int(existing["entry_id"])
else:
cursor = await conn.execute(
"""INSERT INTO core_memory
@@ -116,7 +116,7 @@ async def save(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(user_id, key, value, importance, memory_kind, expires_at, source, metadata_json, now, now),
)
- entry_id = cursor.lastrowid
+ entry_id = int(cursor.lastrowid or 0)
await conn.commit()
return entry_id
@@ -144,14 +144,14 @@ async def delete(self, user_id: str, key: str) -> bool:
await conn.commit()
return cursor.rowcount > 0
- async def search(self, user_id: str, query: str, limit: int = 10) -> list[dict]:
+ async def search(self, user_id: str, query: str, limit: int = 10) -> list[dict[str, Any]]:
conn = await self._cm.get(DB_NAME)
cursor = await conn.execute(
"SELECT * FROM core_memory WHERE user_id=? AND (key LIKE ? OR value LIKE ?) ORDER BY importance DESC LIMIT ?",
(user_id, f"%{query}%", f"%{query}%", limit),
)
rows = await cursor.fetchall()
- return [{"key": r["key"], "value": r["value"], "importance": r["importance"]} for r in rows]
+ return [{"key": str(r["key"]), "value": str(r["value"]), "importance": float(r["importance"])} for r in rows]
async def count(self, user_id: str | None = None) -> int:
conn = await self._cm.get(DB_NAME)
@@ -160,18 +160,18 @@ async def count(self, user_id: str | None = None) -> int:
else:
cursor = await conn.execute("SELECT COUNT(*) FROM core_memory")
row = await cursor.fetchone()
- return row[0] if row else 0
+ return int(row[0]) if row and row[0] is not None else 0
- def _row_to_entry(self, row) -> CoreEntry:
+ def _row_to_entry(self, row: dict[str, Any] | Any) -> CoreEntry:
return CoreEntry(
- entry_id=row["entry_id"],
- user_id=row["user_id"],
- key=row["key"],
- value=row["value"],
- importance=row["importance"],
- memory_kind=row["memory_kind"] or "fact",
- created_at=row["created_at"],
- updated_at=row["updated_at"],
+ entry_id=int(row["entry_id"]),
+ user_id=str(row["user_id"]),
+ key=str(row["key"]),
+ value=str(row["value"]),
+ importance=float(row["importance"]),
+ memory_kind=str(row["memory_kind"] or "fact"),
+ created_at=float(row["created_at"]),
+ updated_at=float(row["updated_at"]),
)
async def list_by_kind(
diff --git a/core/session.py b/core/session.py
index e4e5719a..e7642df2 100644
--- a/core/session.py
+++ b/core/session.py
@@ -8,6 +8,7 @@
import time
import uuid
from dataclasses import dataclass, field
+from typing import Any
from shared.connection import AsyncConnectionManager, connection_manager
from shared.constants import DB_NAME
diff --git a/features/auth/api_key.py b/features/auth/api_key.py
index f3530aca..79a726bf 100644
--- a/features/auth/api_key.py
+++ b/features/auth/api_key.py
@@ -3,19 +3,20 @@
import secrets
import time
from pathlib import Path
+from typing import Any
from features.auth.models import APIKeyModel
from features.auth.store import EncryptedStore
class APIKeyAuth:
- def __init__(self, keys_file: Path | None = None):
+ def __init__(self, keys_file: Path | None = None) -> None:
if keys_file is None:
# Default location if not provided
keys_file = Path("data/auth/keys.enc")
self.store = EncryptedStore(keys_file, APIKeyModel)
- self._keys: dict[str, dict] = self.store.load()
+ self._keys: dict[str, dict[str, Any]] = self.store.load()
def create_key(self, user_id: str, label: str) -> str:
"""Generate ak_... key, save state."""
@@ -27,7 +28,7 @@ def create_key(self, user_id: str, label: str) -> str:
self.store.save(self._keys)
return key
- def verify(self, key: str) -> dict | None:
+ def verify(self, key: str) -> dict[str, Any] | None:
"""
Check key validity and enabled status.
Update last_used timestamp and save() on success.
@@ -68,7 +69,7 @@ def delete_key(self, key: str) -> bool:
return True
return False
- def list_keys(self) -> list[dict]:
+ def list_keys(self) -> list[dict[str, Any]]:
"""Return masked keys with metadata."""
result = []
for key, data in self._keys.items():
diff --git a/features/backup.py b/features/backup.py
index ad0640de..3e1e6d4b 100644
--- a/features/backup.py
+++ b/features/backup.py
@@ -47,17 +47,14 @@ async def restore(self, backup_name: str) -> dict[str, Any]:
return {"error": f"Backup not found: {backup_name}"}
manifest_path = src / "manifest.json"
- # noqa: SKY-D325
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {"files": [f.name for f in src.glob("*.db")]}
restored = []
for db_file in manifest.get("files", []):
# Whitelisted filenames are safe, plus safe_resolve guard.
- # noqa: SKY-D215
safe_resolve(self.base_dir, db_file) # raises ValueError if traversal
backup_file = src / db_file
if backup_file.exists():
- # noqa: SKY-D215
shutil.copy2(backup_file, self.base_dir / db_file)
restored.append(db_file)
diff --git a/features/backup_cron.py b/features/backup_cron.py
index fd0ba59f..f72a75b4 100644
--- a/features/backup_cron.py
+++ b/features/backup_cron.py
@@ -199,8 +199,8 @@ def restore(self, backup_name: str) -> dict[str, Any]:
return {"restored": restored, "backup": backup_name}
- def list_backups(self) -> list:
- backups = []
+ def list_backups(self) -> list[dict[str, Any]]:
+ backups: list[dict[str, Any]] = []
for d in sorted(self.backup_dir.iterdir(), reverse=True):
if d.is_dir():
info = {"name": d.name}
diff --git a/features/dashboard.py b/features/dashboard.py
index 5901d7c0..2350472b 100644
--- a/features/dashboard.py
+++ b/features/dashboard.py
@@ -196,23 +196,23 @@ async def get_stats(self, user_id: str = "default") -> dict[str, Any]:
"agent_wiki": await aw.count(user_id),
}
- async def get_user_facts(self, user_id: str = "default") -> list:
+ async def get_user_facts(self, user_id: str = "default") -> list[dict[str, Any]]:
facts = await self.mm.user_memory(user_id).l4.get_all(user_id, limit=50)
return [{"key": f.key, "value": f.value, "importance": f.importance} for f in facts]
- async def get_agent_facts(self, user_id: str = "default") -> list:
+ async def get_agent_facts(self, user_id: str = "default") -> list[dict[str, Any]]:
facts = await self.mm.agent_memory(user_id).l4.get_all(user_id, limit=50)
return [{"key": f.key, "value": f.value, "importance": f.importance} for f in facts]
- async def get_user_episodes(self, user_id: str = "default") -> list:
+ async def get_user_episodes(self, user_id: str = "default") -> list[dict[str, Any]]:
eps = await self.mm.user_memory(user_id).l3.get_episodes(user_id, limit=20)
return [{"summary": e.summary, "weight": e.emotional_weight, "tags": e.tags} for e in eps]
- async def get_agent_episodes(self, user_id: str = "default") -> list:
+ async def get_agent_episodes(self, user_id: str = "default") -> list[dict[str, Any]]:
eps = await self.mm.agent_memory(user_id).l3.get_episodes(user_id, limit=20)
return [{"summary": e.summary, "weight": e.emotional_weight, "tags": e.tags} for e in eps]
- async def get_audit(self, limit: int = 20) -> list:
+ async def get_audit(self, limit: int = 20) -> list[dict[str, Any]]:
from features.audit_trail import AuditTrail
at = AuditTrail()
diff --git a/features/import_export.py b/features/import_export.py
index 5e296d32..8dd6c47a 100644
--- a/features/import_export.py
+++ b/features/import_export.py
@@ -13,7 +13,7 @@
from shared.connection import AsyncConnectionManager, connection_manager
from shared.constants import DB_NAME
from shared.path_safety import safe_resolve
-from typing import Any, cast
+from typing import Any
class ImportExport:
@@ -24,7 +24,8 @@ def __init__(self, cm: AsyncConnectionManager | None = None):
@property
def base_dir(self) -> Path:
- return cast("Path", self._cm.base_dir)
+ res: Any = self._cm.base_dir
+ return Path(res) if res else Path.home() / ".mcp-ariel-memory"
async def export_user(self, user_id: str) -> str:
core_memory: list[dict[str, Any]] = []
diff --git a/features/secrets.py b/features/secrets.py
index 869f5233..0f7f7e10 100644
--- a/features/secrets.py
+++ b/features/secrets.py
@@ -14,7 +14,7 @@
import logging
import os
from pathlib import Path
-from typing import Any, cast
+from typing import Any
from shared.crypto import decrypt_json as _decrypt_json
from shared.crypto import encrypt_json as _encrypt_json
@@ -97,16 +97,14 @@ def _load_master_key() -> bytes:
# Try environment variable with argon2id KDF
env_seed = os.environ.get(_ENV_VAR)
if env_seed:
- return cast(
- "bytes",
- argon2id.kdf(
- size=_MASTER_KEY_LEN,
- password=env_seed.encode("utf-8"),
- salt=_KDF_SALT,
- opslimit=argon2id.OPSLIMIT_MODERATE,
- memlimit=argon2id.MEMLIMIT_MODERATE,
- ),
+ res_kdf = argon2id.kdf(
+ size=_MASTER_KEY_LEN,
+ password=env_seed.encode("utf-8"),
+ salt=_KDF_SALT,
+ opslimit=argon2id.OPSLIMIT_MODERATE,
+ memlimit=argon2id.MEMLIMIT_MODERATE,
)
+ return bytes(res_kdf)
# Auto-generate key for dev convenience
import secrets as _secrets
@@ -114,16 +112,14 @@ def _load_master_key() -> bytes:
auto_key = _secrets.token_hex(32)
logger.warning("No master key found. Auto-generating key and saving to .env. For production, use keyring or set MCP_MASTER_KEY explicitly.")
_save_dotenv(_ENV_VAR, auto_key)
- return cast(
- "bytes",
- argon2id.kdf(
- size=_MASTER_KEY_LEN,
- password=auto_key.encode("utf-8"),
- salt=_KDF_SALT,
- opslimit=argon2id.OPSLIMIT_MODERATE,
- memlimit=argon2id.MEMLIMIT_MODERATE,
- ),
+ res_auto = argon2id.kdf(
+ size=_MASTER_KEY_LEN,
+ password=auto_key.encode("utf-8"),
+ salt=_KDF_SALT,
+ opslimit=argon2id.OPSLIMIT_MODERATE,
+ memlimit=argon2id.MEMLIMIT_MODERATE,
)
+ return bytes(res_auto)
_master_cache: dict[str, bytes] = {}
@@ -140,7 +136,7 @@ def _get_master_key() -> bytes:
def encrypt_json(data: dict[str, Any] | list[Any]) -> bytes:
"""Encrypt JSON data. Returns nonce(24) || ciphertext."""
- return cast("bytes", _encrypt_json(data, _get_master_key()))
+ return _encrypt_json(data, _get_master_key())
def decrypt_json(blob: bytes) -> Any:
@@ -157,10 +153,9 @@ def is_encrypted_blob(path: Path) -> bool:
if not path.exists():
return False
# Path is verified to be within app data dir by caller, safe.
- # noqa: SKY-D325
with path.open("rb") as f:
head = f.read(1)
- return cast("bool", _is_encrypted_blob(head))
+ return bool(_is_encrypted_blob(head))
def install_master_key_to_keychain(hex_key: str) -> None:
diff --git a/graph/epistemic.py b/graph/epistemic.py
index ccfa210a..2f09a4e6 100644
--- a/graph/epistemic.py
+++ b/graph/epistemic.py
@@ -9,11 +9,14 @@
import logging
import time
from dataclasses import dataclass
-from typing import Any
+from typing import Any, TYPE_CHECKING
from shared.connection import connection_manager
from shared.constants import DB_NAME
+if TYPE_CHECKING:
+ from shared.connection import AsyncConnectionManager
+
logger = logging.getLogger(__name__)
@@ -52,7 +55,7 @@ class EpistemicGraph:
USER_TAGS = USER_TAGS
AGENT_TAGS = AGENT_TAGS
- def __init__(self, cm=None, layer: str = "user"):
+ def __init__(self, cm: AsyncConnectionManager | None = None, layer: str = "user") -> None:
self._cm = cm or connection_manager
self.layer = layer
@@ -66,7 +69,7 @@ def is_known_tag(tag: str) -> bool:
"""Check whether a tag belongs to USER_TAGS or AGENT_TAGS."""
return tag in USER_TAGS or tag in AGENT_TAGS
- async def init_db(self):
+ async def init_db(self) -> None:
await self._cm.execute_script(
DB_NAME,
"""
@@ -116,7 +119,7 @@ async def add_node(self, user_id: str, content: str, node_type: str, tags: list[
"INSERT INTO epi_nodes (layer, user_id, content, node_type, tags, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
(self.layer, user_id, content, node_type, json.dumps(tags or []), confidence, time.time()),
)
- node_id = cursor.lastrowid
+ node_id = int(cursor.lastrowid or 0)
if tags:
for tag in tags:
await conn.execute(
@@ -126,7 +129,7 @@ async def add_node(self, user_id: str, content: str, node_type: str, tags: list[
await conn.commit()
return node_id
- async def add_edge(self, source_id: int, target_id: int, relation: str, weight: float = 0.8):
+ async def add_edge(self, source_id: int, target_id: int, relation: str, weight: float = 0.8) -> None:
conn = await self._cm.get(DB_NAME)
await conn.execute(
"INSERT OR REPLACE INTO epi_edges (source_id, target_id, relation, weight, created_at) VALUES (?, ?, ?, ?, ?)",
@@ -144,7 +147,7 @@ async def query_by_tag(self, user_id: str, tag: str, limit: int = 20) -> list[Ep
(self.layer, user_id, tag, limit),
)
rows = await cur.fetchall()
- return [self._row_to_node(r) for r in rows]
+ return [self._row_to_node(dict(r)) for r in rows]
async def query_by_type(self, user_id: str, node_type: str, limit: int = 20) -> list[EpistemicNode]:
conn = await self._cm.get(DB_NAME)
@@ -153,7 +156,7 @@ async def query_by_type(self, user_id: str, node_type: str, limit: int = 20) ->
(self.layer, user_id, node_type, limit),
)
rows = await cur.fetchall()
- return [self._row_to_node(r) for r in rows]
+ return [self._row_to_node(dict(r)) for r in rows]
async def get_neighbors(self, node_id: int, depth: int = 1) -> list[dict[str, Any]]:
conn = await self._cm.get(DB_NAME)
@@ -217,14 +220,14 @@ async def count_nodes(self, user_id: str | None = None) -> int:
row = await cur.fetchone()
return row[0] if row else 0
- def _row_to_node(self, row) -> EpistemicNode:
+ def _row_to_node(self, row: dict[str, Any]) -> EpistemicNode:
return EpistemicNode(
- node_id=row["node_id"],
- user_id=row["user_id"],
- layer=row["layer"],
- content=row["content"],
- node_type=row["node_type"],
- tags=json.loads(row["tags"]) if row["tags"] else [],
- confidence=row["confidence"],
- created_at=row["created_at"],
+ node_id=int(row["node_id"]),
+ user_id=str(row["user_id"]),
+ layer=str(row["layer"]),
+ content=str(row["content"]),
+ node_type=str(row["node_type"]),
+ tags=list(json.loads(row["tags"])) if row["tags"] else [],
+ confidence=float(row["confidence"]),
+ created_at=float(row["created_at"]),
)
diff --git a/graph/temporal.py b/graph/temporal.py
index 8f57b712..b023eacc 100644
--- a/graph/temporal.py
+++ b/graph/temporal.py
@@ -6,11 +6,14 @@
import time
from dataclasses import dataclass
-from typing import Any
+from typing import Any, TYPE_CHECKING
from shared.connection import connection_manager
from shared.constants import DB_NAME
+if TYPE_CHECKING:
+ from shared.connection import AsyncConnectionManager
+
@dataclass
class TemporalEvent:
@@ -20,14 +23,14 @@ class TemporalEvent:
content: str
timestamp: float
importance: float
- metadata: dict
+ metadata: dict[str, Any]
class TemporalGraph:
- def __init__(self, cm=None):
+ def __init__(self, cm: AsyncConnectionManager | None = None) -> None:
self._cm = cm or connection_manager
- async def init_db(self):
+ async def init_db(self) -> None:
await self._cm.execute_script(
DB_NAME,
"""
@@ -53,7 +56,7 @@ async def init_db(self):
""",
)
- async def add_event(self, user_id: str, event_type: str, content: str, importance: float = 0.5, metadata: dict | None = None) -> int:
+ async def add_event(self, user_id: str, event_type: str, content: str, importance: float = 0.5, metadata: dict[str, Any] | None = None) -> int:
import json
conn = await self._cm.get(DB_NAME)
@@ -62,9 +65,9 @@ async def add_event(self, user_id: str, event_type: str, content: str, importanc
(user_id, event_type, content, time.time(), importance, json.dumps(metadata or {})),
)
await conn.commit()
- return cursor.lastrowid
+ return int(cursor.lastrowid or 0)
- async def link_events(self, from_event: int, to_event: int, link_type: str = "follows", strength: float = 0.5):
+ async def link_events(self, from_event: int, to_event: int, link_type: str = "follows", strength: float = 0.5) -> None:
conn = await self._cm.get(DB_NAME)
await conn.execute(
"INSERT OR REPLACE INTO temporal_links (from_event, to_event, link_type, strength) VALUES (?, ?, ?, ?)",
diff --git a/hooks/loader.py b/hooks/loader.py
index ff6faed4..431bc2a3 100644
--- a/hooks/loader.py
+++ b/hooks/loader.py
@@ -5,5 +5,3 @@
def load_all_hooks() -> None:
"""Import hook modules to trigger @hook_registry.mark decorators."""
- import hooks.user_hooks
- import hooks.agent_hooks # noqa: F401
diff --git a/hooks/models.py b/hooks/models.py
index c784bd83..de3a90d1 100644
--- a/hooks/models.py
+++ b/hooks/models.py
@@ -10,7 +10,7 @@
class HookHandler:
"""Metadata for a registered hook handler."""
- func: Callable
+ func: Callable[..., Any]
name: str
layer: str
is_async: bool
diff --git a/hooks/registry.py b/hooks/registry.py
index 9d4e3bf8..ce196e5f 100644
--- a/hooks/registry.py
+++ b/hooks/registry.py
@@ -19,7 +19,7 @@
class HookHandler(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
- func: Callable
+ func: Callable[..., Any]
name: str
layer: str
is_async: bool
diff --git a/lifecycle/compactor.py b/lifecycle/compactor.py
index 55c766d4..a6acab62 100644
--- a/lifecycle/compactor.py
+++ b/lifecycle/compactor.py
@@ -50,7 +50,8 @@ async def run_cleanup(self, user_id: str = "default") -> dict[str, int]:
await conn.execute("DELETE FROM core_memory WHERE entry_id=?", (row["id"],))
archived_count += 1
except (KeyError, RuntimeError):
- logger.exception("Failed to archive memory %s", row.get("id", "unknown"))
+ rid = row["id"] if isinstance(row, dict) else "unknown"
+ logger.exception("Failed to archive memory %s", rid)
await conn.commit()
logger.info("Memory compaction: archived %d memories for user %s", archived_count, user_id)
diff --git a/lifecycle/consolidation.py b/lifecycle/consolidation.py
index f2c11534..d6aab558 100644
--- a/lifecycle/consolidation.py
+++ b/lifecycle/consolidation.py
@@ -93,9 +93,14 @@ async def consolidate_episodes(
async def get_stats(self, user_id: str) -> dict[str, int]:
conn = await self._cm.get(DB_NAME)
total_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=?", (user_id,))
- total = (await total_cursor.fetchone())[0]
+ total_row = await total_cursor.fetchone()
+ total = int(total_row[0]) if total_row and total_row[0] is not None else 0
+
high_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=? AND importance > 0.7", (user_id,))
- high = (await high_cursor.fetchone())[0]
+ high_row = await high_cursor.fetchone()
+ high = int(high_row[0]) if high_row and high_row[0] is not None else 0
+
low_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=? AND importance < 0.3", (user_id,))
- low = (await low_cursor.fetchone())[0]
+ low_row = await low_cursor.fetchone()
+ low = int(low_row[0]) if low_row and low_row[0] is not None else 0
return {"total": total, "high_importance": high, "low_importance": low}
diff --git a/lifecycle/emotion/engine.py b/lifecycle/emotion/engine.py
index 2d905757..25b6f4cd 100644
--- a/lifecycle/emotion/engine.py
+++ b/lifecycle/emotion/engine.py
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
+from re import Pattern
from .models import EmotionMarkerConfig, EmotionResult
@@ -12,16 +13,16 @@ class EmotionEngine:
MARKER_SCORE_DEFAULT = 0.4
EMOJI_SCORE_DEFAULT = 0.3
- def __init__(self, config: EmotionMarkerConfig):
+ def __init__(self, config: EmotionMarkerConfig) -> None:
self.config = config
- self.phrase_regex: re.Pattern | None = None
- self.marker_regex: re.Pattern | None = None
- self.emoji_regex: re.Pattern | None = None
+ self.phrase_regex: Pattern[str] | None = None
+ self.marker_regex: Pattern[str] | None = None
+ self.emoji_regex: Pattern[str] | None = None
self._compile()
def _compile(self) -> None:
"""Compile regex patterns once."""
- phrase_patterns = []
+ phrase_patterns: list[str] = []
for i, p in enumerate(self.config.phrases):
pattern = p.pattern.replace(" ", r"\s+(?:\w+\s+)?")
phrase_patterns.append(f"(?P
{pattern})")
@@ -34,7 +35,7 @@ def _compile(self) -> None:
[f"(?P{'|'.join(re.escape(i) for i in icons)})" for cat, icons in self.config.emojis.items() if icons]
)
- def _build_regex(self, parts: list[str], flags: int = 0) -> re.Pattern | None:
+ def _build_regex(self, parts: list[str], flags: int = 0) -> Pattern[str] | None:
return re.compile("|".join(parts), flags) if parts else None
def detect(self, text: str) -> list[EmotionResult]:
diff --git a/lifecycle/forgetting.py b/lifecycle/forgetting.py
index f54f0f8f..326a638d 100644
--- a/lifecycle/forgetting.py
+++ b/lifecycle/forgetting.py
@@ -120,7 +120,7 @@ async def archive_old_entries(self) -> int:
ids = [r["entry_id"] for r in all_rows]
placeholders = ",".join(["?"] * len(ids))
# Parameterized via placeholders, safe.
- await conn.execute(f"DELETE FROM core_memory WHERE entry_id IN ({placeholders})", ids)
+ await conn.execute(f"DELETE FROM core_memory WHERE entry_id IN ({placeholders})", tuple(ids))
await conn.commit()
logger.info("Archived %d entries", archived_count)
return archived_count
@@ -142,7 +142,7 @@ async def compress_duplicates(self) -> int:
)
changes_cursor = await conn.execute("SELECT changes()")
changes_row = await changes_cursor.fetchone()
- removed += changes_row[0]
+ removed += int(changes_row[0]) if changes_row and changes_row[0] is not None else 0
await conn.commit()
return removed
except Exception:
diff --git a/mcp_server/app.py b/mcp_server/app.py
index c70d67b0..3b6e4fe9 100644
--- a/mcp_server/app.py
+++ b/mcp_server/app.py
@@ -1,5 +1,6 @@
import os
import time as _time
+from typing import Any
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
@@ -42,7 +43,8 @@ async def check_rate_limit(request: Request, api_rate_limiter: RateLimiter) -> b
return True
user = get_user_from_token(request)
result = await api_rate_limiter.check(user)
- return result.get("allowed", True)
+ res: Any = result.get("allowed", True)
+ return bool(res)
def create_app(mcp: FastMCP, ctx: AppContext) -> Starlette:
diff --git a/mcp_server/context.py b/mcp_server/context.py
index d181d172..2f7253bb 100644
--- a/mcp_server/context.py
+++ b/mcp_server/context.py
@@ -17,7 +17,7 @@
class AppContext:
- def __init__(self):
+ def __init__(self) -> None:
self.cache = MemoryCache()
self.mm = MemoryManager(cache=self.cache)
self.user_wiki = WikiManager(layer="user")
diff --git a/mcp_server/middlewares.py b/mcp_server/middlewares.py
index 8e35e92d..c4a6a88a 100644
--- a/mcp_server/middlewares.py
+++ b/mcp_server/middlewares.py
@@ -1,13 +1,16 @@
import os
+from collections.abc import Awaitable, Callable
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware
-from starlette.responses import JSONResponse
+from starlette.responses import JSONResponse, Response
+from starlette.requests import Request
+from starlette.applications import Starlette
from features.auth import bearer_auth
from config import config
class AuthMiddleware(BaseHTTPMiddleware):
- async def dispatch(self, request, call_next):
+ async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
if request.url.path in ("/mcp", "/health", "/ready", "/alive"):
return await call_next(request)
if os.environ.get("MCP_AUTH_DISABLED"):
@@ -18,9 +21,9 @@ async def dispatch(self, request, call_next):
return await call_next(request)
-def add_middlewares(app):
+def add_middlewares(app: Starlette) -> None:
app.add_middleware(AuthMiddleware)
- allowed_origins = config.get("cors", "allowed_origins", default=["http://localhost:*", "http://127.0.0.1:*"])
+ allowed_origins: list[str] = config.get("cors", "allowed_origins", default=["http://localhost:*", "http://127.0.0.1:*"])
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
diff --git a/mcp_server/registry.py b/mcp_server/registry.py
index 7150e3bd..a8625023 100644
--- a/mcp_server/registry.py
+++ b/mcp_server/registry.py
@@ -14,19 +14,19 @@
# if TYPE_CHECKING:
-_tools: dict[str, Callable] = {}
+_tools: dict[str, Callable[..., Any]] = {}
-def _get_ctx(ctx: Context | None) -> Any:
+def _get_ctx(ctx: Context[Any, Any] | None) -> Any:
"""Extract AppContext from FastMCP lifespan context."""
if ctx is None:
raise ValueError("Context is required but was None")
return ctx.request_context.lifespan_context
-def register_tool(name: str, func: Callable) -> None:
+def register_tool(name: str, func: Callable[..., Any]) -> None:
_tools[name] = func
-def get_all_tools() -> dict[str, Callable]:
+def get_all_tools() -> dict[str, Callable[..., Any]]:
return dict(_tools)
diff --git a/mcp_server/server.py b/mcp_server/server.py
index 3f3e90e0..2cf66204 100644
--- a/mcp_server/server.py
+++ b/mcp_server/server.py
@@ -5,7 +5,7 @@
import logging
from pathlib import Path
-from typing import Any
+from typing import Any, Literal
from mcp.server.fastmcp import FastMCP
# Ensure the root of the repo is in the path
@@ -20,10 +20,10 @@
)
-STDIO_TRANSPORT = "stdio"
+STDIO_TRANSPORT: Literal["stdio"] = "stdio"
-def _register_all_tools():
+def _register_all_tools() -> None:
from mcp_server.registry import get_all_tools
for name, func in get_all_tools().items():
@@ -80,7 +80,7 @@ def _run_with_dashboard(host: str, port: int) -> None:
uvicorn.run(app, host=host, port=port)
-def _setup_shutdown_signals(handler) -> None:
+def _setup_shutdown_signals(handler: Any) -> None:
import signal
signal.signal(signal.SIGTERM, handler)
diff --git a/mcp_server/tools/episodic.py b/mcp_server/tools/episodic.py
index fec07cba..2249e5b6 100644
--- a/mcp_server/tools/episodic.py
+++ b/mcp_server/tools/episodic.py
@@ -5,9 +5,8 @@
from shared.constants import DB_NAME
from shared.metrics import metrics
-import mcp_server.tools_layer as tl
-from .base import _validate_layer, _check_rate_limit, _get_memory, _invalidate_cache
-from typing import TYPE_CHECKING
+from .base import _validate_layer, _check_rate_limit, _get_memory, _invalidate_cache, _fire_hook
+from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import Context
@@ -19,8 +18,8 @@ async def memory_episode_save(
summary: str = "",
weight: float = 0.5,
tags: list[str] | None = None,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Save an episode to L3 episodic memory."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -29,15 +28,15 @@ async def memory_episode_save(
rate_limit = await _check_rate_limit(app, user_id)
if rate_limit:
- return rate_limit
+ return dict(rate_limit)
episode_id = await _get_memory(app, layer, user_id).l3.save(user_id, summary, weight, tags)
_invalidate_cache(layer, user_id)
# Fire post-save hooks
- await tl._fire_hook("emotion_trigger", layer, {"summary": summary, "emotional_weight": weight, "user_id": user_id})
- await tl._fire_hook("state_delta", layer, {"summary": summary, "tags": tags, "user_id": user_id})
- await tl._fire_hook("consolidation", layer, {"trigger": "episode_save", "user_id": user_id})
+ await _fire_hook("emotion_trigger", layer, {"summary": summary, "emotional_weight": weight, "user_id": user_id})
+ await _fire_hook("state_delta", layer, {"summary": summary, "tags": tags, "user_id": user_id})
+ await _fire_hook("consolidation", layer, {"trigger": "episode_save", "user_id": user_id})
return EpisodeResult(episode_id=episode_id).dict()
@@ -47,15 +46,15 @@ async def memory_episode_recall(
user_id: str = "default",
tag: str = "",
limit: int = 10,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Recall episodes, optionally filtered by tag."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
metrics.inc("tool_calls")
metrics.inc("tool_episode_recall")
- await tl._fire_hook("retrieval_router", layer, {"query": tag or "episodes", "user_id": user_id, "limit": limit})
+ await _fire_hook("retrieval_router", layer, {"query": tag or "episodes", "user_id": user_id, "limit": limit})
mem = _get_memory(app, layer, user_id)
if tag:
@@ -70,8 +69,8 @@ async def memory_episode_list(
user_id: str = "default",
limit: int = 10,
offset: int = 0,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""List episodes from L3 episodic memory."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -88,8 +87,8 @@ async def memory_episode_get(
layer: str = "user",
user_id: str = "default",
episode_id: int = 0,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Get a single episode by ID."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
diff --git a/mcp_server/tools/graph.py b/mcp_server/tools/graph.py
index 6b5e65e8..9806e211 100644
--- a/mcp_server/tools/graph.py
+++ b/mcp_server/tools/graph.py
@@ -5,9 +5,8 @@
from shared.constants import DB_NAME
from shared.metrics import metrics
-import mcp_server.tools_layer as tl
-from .base import _validate_layer, _check_rate_limit, _get_graph, _invalidate_cache
-from typing import TYPE_CHECKING
+from .base import _validate_layer, _check_rate_limit, _get_graph, _invalidate_cache, _fire_hook
+from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import Context
@@ -19,8 +18,8 @@ async def memory_graph_add(
content: str = "",
node_type: str = "fact",
tags: list[str] | None = None,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Add a node to the epistemic graph."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -29,7 +28,7 @@ async def memory_graph_add(
rate_limit = await _check_rate_limit(app, user_id)
if rate_limit:
- return rate_limit
+ return dict(rate_limit)
node_id = await _get_graph(app, layer).add_node(user_id, content, node_type, tags)
_invalidate_cache(layer, user_id)
@@ -43,7 +42,7 @@ async def memory_graph_add(
}
hook_name = hook_map.get(node_type)
if hook_name:
- await tl._fire_hook(hook_name, layer, {"node_type": node_type, "content": content, "user_id": user_id})
+ await _fire_hook(hook_name, layer, {"node_type": node_type, "content": content, "user_id": user_id})
return GraphNodeResult(node_id=node_id).dict()
@@ -54,15 +53,15 @@ async def memory_graph_query(
tag: str = "",
node_type: str = "",
limit: int = 20,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Query the epistemic graph by tag or node type."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
metrics.inc("tool_calls")
metrics.inc("tool_graph_query")
- await tl._fire_hook("retrieval_router", layer, {"query": tag or node_type, "user_id": user_id, "limit": limit})
+ await _fire_hook("retrieval_router", layer, {"query": tag or node_type, "user_id": user_id, "limit": limit})
graph = _get_graph(app, layer)
if tag:
@@ -79,8 +78,8 @@ async def memory_graph_nodes(
user_id: str = "default",
node_type: str = "",
limit: int = 20,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""List nodes from the epistemic graph."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -96,7 +95,7 @@ async def memory_graph_nodes(
(graph.layer, user_id, limit),
)
rows = await cur.fetchall()
- nodes = [graph._row_to_node(r) for r in rows]
+ nodes = [graph._row_to_node(dict(r)) for r in rows]
return {"nodes": [{"id": n.node_id, "content": n.content, "type": n.node_type, "tags": n.tags} for n in nodes], "count": len(nodes)}
@@ -105,8 +104,8 @@ async def memory_graph_edges(
user_id: str = "",
node_id: int = 0,
limit: int = 20,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""List edges from the epistemic graph."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
diff --git a/mcp_server/tools/memory.py b/mcp_server/tools/memory.py
index b834a9b5..80e26246 100644
--- a/mcp_server/tools/memory.py
+++ b/mcp_server/tools/memory.py
@@ -7,9 +7,6 @@
from mcp_server.utils.privacy import strip_secrets
from shared.metrics import metrics
-# Import tools_layer to use its _fire_hook which might be monkeypatched
-import mcp_server.tools_layer as tl
-
from .base import (
_validate_layer,
_check_rate_limit,
@@ -19,6 +16,7 @@
_invalidate_cache,
_get_recall_cache,
_set_recall_cache,
+ _fire_hook,
)
from typing import TYPE_CHECKING, Any
@@ -35,8 +33,8 @@ async def memory_remember(
value: str = "",
importance: float = 0.5,
session_id: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Save a fact to long-term memory (L4 CoreMemory)."""
value = strip_secrets(value)
if session_id and _dedup_cache.is_duplicate(session_id, key, value):
@@ -50,9 +48,9 @@ async def memory_remember(
rate_limit = await _check_rate_limit(app, user_id)
if rate_limit:
- return rate_limit
+ return dict(rate_limit)
- gate = await tl._fire_hook("importance_gate", layer, {"text": value, "key": key, "importance": importance})
+ gate = await _fire_hook("importance_gate", layer, {"text": value, "key": key, "importance": importance})
if gate.get("results") and any(r.get("bypass") for r in gate["results"] if isinstance(r, dict)):
logger.info("Importance gate bypassed: key=%s, importance=%.2f, user=%s", key, importance, user_id)
return RememberResult(status="skipped", reason="below_importance_threshold").dict()
@@ -75,22 +73,22 @@ async def memory_remember(
else:
entry_id = await mem.remember(key, value, importance)
node_id = await graph.add_node(user_id, value, "fact", [], importance)
- await tl._fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem)
- await tl._fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem)
+ await _fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem)
+ await _fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem)
_invalidate_cache(layer, user_id)
return RememberResult(status="ok", entry_id=entry_id, graph_node_id=node_id).dict()
async def _fire_post_remember_hooks(layer: str, user_id: str, key: str, value: str, mem: Any) -> None:
- await tl._fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem)
- await tl._fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem)
+ await _fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem)
+ await _fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem)
if "error" in key.lower():
- await tl._fire_hook("error_occurred", layer, {"key": key, "value": value, "user_id": user_id})
+ await _fire_hook("error_occurred", layer, {"key": key, "value": value, "user_id": user_id})
elif "decision" in key.lower():
- await tl._fire_hook("decision_made", layer, {"key": key, "value": value, "user_id": user_id})
+ await _fire_hook("decision_made", layer, {"key": key, "value": value, "user_id": user_id})
elif "correction" in key.lower():
- await tl._fire_hook("self_correction", layer, {"key": key, "value": value, "user_id": user_id})
+ await _fire_hook("self_correction", layer, {"key": key, "value": value, "user_id": user_id})
async def memory_recall(
@@ -98,15 +96,15 @@ async def memory_recall(
user_id: str = "default",
query: str = "",
limit: int = 10,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Search memory across L3 (episodes) and L4 (facts)."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
metrics.inc("tool_calls")
metrics.inc("tool_recall")
- await tl._fire_hook("retrieval_router", layer, {"query": query, "user_id": user_id, "limit": limit})
+ await _fire_hook("retrieval_router", layer, {"query": query, "user_id": user_id, "limit": limit})
cached = _get_recall_cache(query, user_id, layer, limit)
if cached is not None:
@@ -115,7 +113,7 @@ async def memory_recall(
results = await _get_memory(app, layer, user_id).recall(query, limit)
_set_recall_cache(query, user_id, layer, limit, results)
- await tl._fire_hook("auto_context", layer, {"query": query, "results_count": len(results), "user_id": user_id})
+ await _fire_hook("auto_context", layer, {"query": query, "results_count": len(results), "user_id": user_id})
return RecallResult(results=results, count=len(results)).dict()
@@ -124,8 +122,8 @@ async def memory_forget(
layer: str = "user",
user_id: str = "default",
key: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Delete a fact from L4 memory."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -134,7 +132,7 @@ async def memory_forget(
rate_limit = await _check_rate_limit(app, user_id)
if rate_limit:
- return rate_limit
+ return dict(rate_limit)
deleted = await _get_memory(app, layer, user_id).forget(key)
_invalidate_cache(layer, user_id)
diff --git a/mcp_server/tools/ops.py b/mcp_server/tools/ops.py
index aee1821e..39e3e172 100644
--- a/mcp_server/tools/ops.py
+++ b/mcp_server/tools/ops.py
@@ -17,7 +17,6 @@
from shared.metrics import metrics
from shared.constants import DB_NAME
-import mcp_server.tools_layer as tl
from .base import (
_validate_layer,
_get_memory,
@@ -28,9 +27,10 @@
_set_cached,
_estimate_tokens,
_truncate_to_budget,
+ _fire_hook,
DEFAULT_TOKEN_BUDGET,
)
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import Context
@@ -39,8 +39,8 @@
async def memory_stats(
layer: str = "user",
user_id: str = "default",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Get memory statistics for a layer."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -63,8 +63,8 @@ async def memory_stats(
async def memory_context(
layer: str = "user",
user_id: str = "default",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Return compressed context summary for prompt injection."""
metrics.inc("tool_calls")
metrics.inc("tool_context")
@@ -116,13 +116,13 @@ async def memory_context(
async def memory_context_inject(
layer: str = "user",
user_id: str = "default",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Return compressed summary for prompt injection (L4 top-10 + L3 top-3)."""
metrics.inc("tool_calls")
metrics.inc("tool_context_inject")
- await tl._fire_hook("auto_context", layer, {"query": "context_inject", "user_id": user_id})
+ await _fire_hook("auto_context", layer, {"query": "context_inject", "user_id": user_id})
cache_key = _get_cache_key(layer, user_id)
cached = _get_cached(cache_key)
@@ -133,7 +133,7 @@ async def memory_context_inject(
mem = _get_memory(app, layer, user_id)
wiki = _get_wiki(app, layer)
- await tl._fire_hook("wiki_agent", layer, {"user_id": user_id, "query": "context_inject"})
+ await _fire_hook("wiki_agent", layer, {"user_id": user_id, "query": "context_inject"})
l4_facts = await mem.l4.get_all(user_id, 10)
facts_text = "; ".join([f"{f.key}={f.value[:30]}" for f in l4_facts])
@@ -173,7 +173,7 @@ async def memory_context_inject(
"token_budget": DEFAULT_TOKEN_BUDGET,
}
_set_cached(cache_key, result)
- await tl._fire_hook("dream_buffer", layer, {"text": context_text, "user_id": user_id})
+ await _fire_hook("dream_buffer", layer, {"text": context_text, "user_id": user_id})
return result
@@ -183,8 +183,8 @@ async def memory_api_key(
user_id: str = "default",
label: str = "",
api_key: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Manage API keys."""
from features.auth import api_key_auth
@@ -203,8 +203,8 @@ async def memory_api_key(
async def memory_backup(
action: str = "status",
backup_name: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Manage backups."""
from features.backup_cron import backup_cron
@@ -226,8 +226,8 @@ async def memory_backup(
async def memory_saga(
action: str = "consolidate",
user_id: str = "default",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Run sagas with auto-rollback on failure."""
metrics.inc("tool_calls")
metrics.inc("tool_saga")
@@ -254,8 +254,8 @@ async def memory_data(
user_id: str = "default",
file_path: str = "",
target_user_id: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Import/export memory data."""
metrics.inc("tool_calls")
metrics.inc("tool_data")
@@ -271,8 +271,8 @@ async def memory_data(
async def memory_sync_replica(
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Sync read-only replica for dashboard/metrics."""
metrics.inc("tool_calls")
metrics.inc("tool_sync_replica")
@@ -285,8 +285,8 @@ async def memory_sync_replica(
async def memory_cleanup(
user_id: str = "default",
retention_days: int = 30,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Full memory cleanup: deduplicate, archive, clean staging."""
metrics.inc("tool_calls")
metrics.inc("tool_cleanup")
@@ -323,61 +323,61 @@ async def memory_cleanup(
async def memory_lucidity_purge(
user_id: str = "default",
hours: int = 24,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Emergency purge: delete all data from the last N hours."""
metrics.inc("tool_calls")
metrics.inc("tool_lucidity_purge")
app = _get_ctx(ctx)
cutoff = time.time() - (hours * 3600)
- async def _delete_core():
+ async def _delete_core() -> int:
conn = await app.mm.user_memory(user_id).l4._cm.get(DB_NAME)
try:
cursor = await conn.execute("DELETE FROM core_memory WHERE user_id=? AND created_at > ?", (user_id, cutoff))
- result = cursor.rowcount
+ result = int(cursor.rowcount)
await conn.commit()
return result
finally:
await conn.close()
- async def _delete_episodes():
+ async def _delete_episodes() -> int:
conn = await app.mm.user_memory(user_id).l3._cm.get(DB_NAME)
try:
cursor = await conn.execute("DELETE FROM episodes WHERE user_id=? AND created_at > ?", (user_id, cutoff))
- result = cursor.rowcount
+ result = int(cursor.rowcount)
await conn.commit()
return result
finally:
await conn.close()
- async def _delete_staging():
+ async def _delete_staging() -> int:
from shared.dream_buffer import DreamBuffer
db = DreamBuffer()
- return db.clear_staging(user_id)
+ return await db.clear_staging(user_id)
- async def _delete_audit():
+ async def _delete_audit() -> int:
from features.audit_trail import AuditTrail
at = AuditTrail()
conn = await at._cm.get(DB_NAME)
try:
cursor = await conn.execute("DELETE FROM audit_log WHERE user_id=? AND timestamp > ?", (user_id, cutoff))
- result = cursor.rowcount
+ result = int(cursor.rowcount)
await conn.commit()
return result
finally:
await conn.close()
- async def _delete_graph():
+ async def _delete_graph() -> int:
from graph.epistemic import EpistemicGraph
eg = EpistemicGraph(layer="user")
conn = await eg._cm.get(DB_NAME)
try:
cursor = await conn.execute("DELETE FROM epi_nodes WHERE user_id=? AND created_at > ?", (user_id, cutoff))
- result = cursor.rowcount
+ result = int(cursor.rowcount)
await conn.commit()
return result
finally:
@@ -402,8 +402,8 @@ async def memory_search(
limit: int = 10,
strategy: str = "hybrid",
sources: str = "all",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Hybrid search across RAG + Wiki with strategy selection."""
metrics.inc("tool_calls")
metrics.inc("tool_search")
diff --git a/mcp_server/tools/session.py b/mcp_server/tools/session.py
index 35f5dba6..193e9d9e 100644
--- a/mcp_server/tools/session.py
+++ b/mcp_server/tools/session.py
@@ -4,9 +4,8 @@
from mcp_server.registry import _get_ctx
from shared.metrics import metrics
-import mcp_server.tools_layer as tl
-from .base import _validate_layer, _check_rate_limit, _get_memory
-from typing import TYPE_CHECKING
+from .base import _validate_layer, _check_rate_limit, _get_memory, _fire_hook
+from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import Context
@@ -15,8 +14,8 @@
async def memory_session_start(
layer: str = "user",
user_id: str = "default",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Start a new memory session."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -28,7 +27,7 @@ async def memory_session_start(
return rate_limit
session_id = await _get_memory(app, layer, user_id).l2.create_session(user_id)
- await tl._fire_hook("message_received", layer, {"text": "session_started", "session_id": session_id, "user_id": user_id})
+ await _fire_hook("message_received", layer, {"text": "session_started", "session_id": session_id, "user_id": user_id})
return SessionResult(session_id=session_id).dict()
@@ -38,8 +37,8 @@ async def memory_session_end(
user_id: str = "default",
session_id: str = "",
summary: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""End a session and save summary."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -52,8 +51,8 @@ async def memory_session_end(
await _get_memory(app, layer, user_id).l2.close_session(session_id, summary)
- await tl._fire_hook("consolidation", layer, {"trigger": "session_end", "session_id": session_id, "user_id": user_id})
- await tl._fire_hook("state_delta", layer, {"trigger": "session_end", "session_id": session_id, "summary": summary, "user_id": user_id})
+ await _fire_hook("consolidation", layer, {"trigger": "session_end", "session_id": session_id, "user_id": user_id})
+ await _fire_hook("state_delta", layer, {"trigger": "session_end", "session_id": session_id, "summary": summary, "user_id": user_id})
return SessionResult(status="ok").dict()
@@ -62,8 +61,8 @@ async def memory_session_list(
layer: str = "user",
user_id: str = "default",
limit: int = 10,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""List recent memory sessions."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
diff --git a/mcp_server/tools/wiki.py b/mcp_server/tools/wiki.py
index d17ac9d4..db3da7e2 100644
--- a/mcp_server/tools/wiki.py
+++ b/mcp_server/tools/wiki.py
@@ -2,7 +2,7 @@
from mcp_server.registry import _get_ctx
from .base import _validate_layer, _get_wiki
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import Context
@@ -14,13 +14,13 @@ async def wiki_add(
content: str = "",
wiki_type: str = "concept",
tags: list[str] | None = None,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Add or update a wiki page."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
wiki = _get_wiki(app, layer)
- await wiki.save(title, content, wiki_type, tags)
+ await wiki.add(wiki_type, title, content, tags)
return {"status": "ok", "title": title}
@@ -28,22 +28,25 @@ async def wiki_search(
layer: str = "user",
query: str = "",
limit: int = 10,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Search wiki pages."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
wiki = _get_wiki(app, layer)
results = await wiki.search(query, limit)
- return {"results": [{"title": r.title, "type": r.wiki_type, "tags": r.tags} for r in results], "count": len(results)}
+ return {
+ "results": [{"title": str(r.get("title", "")), "type": str(r.get("wiki_type", "")), "tags": list(r.get("tags", []))} for r in results],
+ "count": len(results),
+ }
async def wiki_list(
layer: str = "user",
wiki_type: str = "",
limit: int = 20,
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""List wiki pages."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
@@ -52,14 +55,17 @@ async def wiki_list(
pages = await wiki.list_by_type(wiki_type, limit)
else:
pages = await wiki.list_all(limit)
- return {"pages": [{"title": p.title, "type": p.wiki_type, "tags": p.tags} for p in pages], "count": len(pages)}
+ return {
+ "pages": [{"title": str(p.title), "type": str(p.wiki_type), "tags": list(p.tags)} for p in pages],
+ "count": len(pages),
+ }
async def wiki_delete(
layer: str = "user",
title: str = "",
- ctx: Context | None = None,
-) -> dict:
+ ctx: Context[Any, Any] | None = None,
+) -> dict[str, Any]:
"""Delete a wiki page."""
app = _get_ctx(ctx)
layer = _validate_layer(layer)
diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py
index 859be95b..f16e8f5d 100644
--- a/mcp_server/tools_layer.py
+++ b/mcp_server/tools_layer.py
@@ -31,7 +31,7 @@
# For tests that monkeypatch tl._fire_hook
-def _fire_hook_wrapper(*args, **kwargs):
+def _fire_hook_wrapper(*args: Any, **kwargs: Any) -> Any:
from .tools import base
return base._fire_hook(*args, **kwargs)
diff --git a/mcp_server/utils/circuit_breaker.py b/mcp_server/utils/circuit_breaker.py
index ef11e6c2..1afe9c20 100644
--- a/mcp_server/utils/circuit_breaker.py
+++ b/mcp_server/utils/circuit_breaker.py
@@ -113,11 +113,6 @@ def allow_request(self) -> bool:
self._total_rejections += 1
return False
- def reset(self) -> None:
- self._failures = 0
- self._state = CircuitState.CLOSED
- self._opened_at = 0.0
-
def get_metrics(self) -> dict[str, Any]:
return {
"name": self.name,
@@ -177,7 +172,9 @@ def get_all_metrics(self) -> dict[str, dict[str, Any]]:
def reset_all(self) -> None:
for breaker in self._breakers.values():
- breaker.reset()
+ breaker._failures = 0
+ breaker._state = CircuitState.CLOSED
+ breaker._opened_at = 0.0
breaker_registry = CircuitBreakerRegistry()
diff --git a/pyproject.toml b/pyproject.toml
index b598d339..1fab8493 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -162,32 +162,31 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
-
-[tool.skylos]
-exclude = [".repowise", ".codegraph", "docs", "__pycache__"]
-
-# SQL injection false positives: all findings use parameterized queries (?)
-# or build SQL templates (table names, IN clauses) — not user data injection
-ignore = ["SKY-D211"]
-
-[tool.skylos.quality]
-max_complexity = 15
-max_lines = 100
-max_args = 6
+"tests/*" = ["S101", "ANN", "D", "SLF001", "PLR", "ARG", "FBT", "PT", "B011", "B023", "B904", "RET504", "TRY", "EM", "S", "PLW", "PLR"]
[tool.mypy]
python_version = "3.12"
-warn_return_any = false
+warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
check_untyped_defs = true
-ignore_missing_imports = true
+ignore_missing_imports = false
explicit_package_bases = true
-exclude = ["tests"]
+exclude = ["tests", ".venv"]
+strict = true
[[tool.mypy.overrides]]
-module = ["tests.*"]
-ignore_errors = true
+module = [
+ "yaml.*",
+ "pynacl.*",
+ "nacl.*",
+ "prometheus_client.*",
+ "aiosqlite.*",
+ "alembic.*",
+ "mcp.*",
+ "keyring.*",
+]
+ignore_missing_imports = true
[tool.coverage.run]
omit = ["demo.py", "__main__.py"]
diff --git a/rag/conflict.py b/rag/conflict.py
index 5bb2199d..01fd87d9 100644
--- a/rag/conflict.py
+++ b/rag/conflict.py
@@ -72,7 +72,7 @@ class ConflictResolver:
def __init__(self, cm: AsyncConnectionManager | None = None):
self._cm = cm or connection_manager
- async def _init_db(self):
+ async def _init_db(self) -> None:
await self._cm.execute_script(
DB_NAME,
"""
@@ -98,15 +98,17 @@ async def check(self, user_id: str, new_content: str, min_similarity: float = 0.
for row in rows:
existing_id, existing_content, is_conflict, group_id = row
- similarity = self._calculate_similarity(new_content, existing_content)
- if similarity > min_similarity and existing_content != new_content:
- return await self._handle_conflict(conn, existing_id, existing_content, is_conflict, group_id, new_content, similarity)
+ similarity = float(self._calculate_similarity(new_content, str(existing_content)))
+ if similarity > min_similarity and str(existing_content) != new_content:
+ return await self._handle_conflict(
+ conn, int(existing_id), str(existing_content), bool(is_conflict), group_id, new_content, similarity
+ )
await conn.execute("INSERT INTO memory_conflicts (user_id, content) VALUES (?, ?)", (user_id, new_content))
await conn.commit()
return {"content": new_content, "is_conflict": False}
- async def _find_potential_conflicts(self, conn, user_id: str, keywords: list[str]):
+ async def _find_potential_conflicts(self, conn: Any, user_id: str, keywords: list[str]) -> Any:
like_conditions = " OR ".join(["content LIKE ?" for _ in keywords])
like_params = [f"%{kw}%" for kw in keywords]
cur = await conn.execute(
@@ -115,8 +117,10 @@ async def _find_potential_conflicts(self, conn, user_id: str, keywords: list[str
)
return await cur.fetchall()
- async def _handle_conflict(self, conn, existing_id, existing_content, is_conflict, group_id, new_content, similarity):
- gid = group_id or str(uuid.uuid4())
+ async def _handle_conflict(
+ self, conn: Any, existing_id: int, existing_content: str, is_conflict: bool, group_id: str | None, new_content: str, similarity: float
+ ) -> dict[str, Any]:
+ gid = str(group_id) if group_id else str(uuid.uuid4())
if not is_conflict:
await conn.execute("UPDATE memory_conflicts SET is_conflict=1, conflict_group_id=? WHERE id=?", (gid, existing_id))
await conn.commit()
diff --git a/rag/engine.py b/rag/engine.py
index 944d932c..cc92893c 100644
--- a/rag/engine.py
+++ b/rag/engine.py
@@ -31,9 +31,9 @@ def __init__(
binary_dim: int = 384,
binary_threshold_mode: str = "naive",
binary_thresholds_path: str | None = None,
- thresholds=None,
+ thresholds: Any = None,
search_strategy: StrategyT = "fts",
- ):
+ ) -> None:
self._cm = cm or connection_manager
self.layer = layer
self.binary_dim = binary_dim
@@ -149,7 +149,7 @@ async def get_relations(self, page_id: int, depth: int = 1) -> list[dict[str, An
rows = await cur.fetchall()
return [{"id": r[0], "title": r[1], "relation": r[2], "weight": r[3]} for r in rows]
- async def add_relation(self, source_id: int, target_id: int, relation_type: str = "elaborates", weight: float = 0.8):
+ async def add_relation(self, source_id: int, target_id: int, relation_type: str = "elaborates", weight: float = 0.8) -> None:
conn = await self._cm.get(DB_NAME)
await conn.execute(
"INSERT OR REPLACE INTO rag_relations (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)",
@@ -168,4 +168,4 @@ async def count_pages(self, user_id: str | None = None) -> int:
async def count_chunks(self) -> int:
conn = await self._cm.get(DB_NAME)
row = await (await conn.execute("SELECT COUNT(*) FROM rag_chunks")).fetchone()
- return row[0] if row else 0
+ return int(row[0]) if row and row[0] is not None else 0
diff --git a/rag/ingestor.py b/rag/ingestor.py
index a00c6d39..8e8c4d26 100644
--- a/rag/ingestor.py
+++ b/rag/ingestor.py
@@ -29,7 +29,7 @@ async def ingest(self, title: str, content: str, user_id: str, wiki_type: str |
cursor = await conn.execute("SELECT id FROM rag_pages WHERE sha256_hash = ? AND user_id = ?", (content_hash, user_id))
row = await cursor.fetchone()
if row:
- return row[0]
+ return int(row[0]) if row[0] is not None else None
# Split text into chunks
chunks_text = chunk_text(content)
@@ -81,7 +81,7 @@ async def ingest(self, title: str, content: str, user_id: str, wiki_type: str |
cursor = await conn.execute("SELECT last_insert_rowid()")
row = await cursor.fetchone()
- page_id = row[0]
+ page_id = int(row[0]) if row and row[0] is not None else 0
# Batch Insertion
chunk_data = []
diff --git a/rag/quantize.py b/rag/quantize.py
index 86d17c5c..b439101f 100644
--- a/rag/quantize.py
+++ b/rag/quantize.py
@@ -7,7 +7,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence
@@ -23,7 +23,7 @@
DEFAULT_DIM = 384
-def _check_numpy():
+def _check_numpy() -> None:
if not _HAS_NUMPY:
raise ImportError("numpy is required for binary embeddings. Install with: pip install mcp-ariel-memory[binary]")
@@ -61,7 +61,7 @@ def supervised_threshold(
pos_pairs: Iterable[tuple[Sequence[float], Sequence[float]]],
dim: int = DEFAULT_DIM,
n_candidates: int = 50,
-):
+) -> np.ndarray:
"""Per-dimension threshold maximizing agreement on positive pairs.
Args:
@@ -100,7 +100,7 @@ def supervised_threshold(
def train_supervised_thresholds(
pos_pairs: list[tuple[Sequence[float], Sequence[float]]],
neg_pairs: list[tuple[Sequence[float], Sequence[float]]] | None = None,
- emb_fn: Callable | None = None,
+ emb_fn: Callable[[Any], Sequence[float]] | None = None,
n_candidates: int = 50,
dim: int = DEFAULT_DIM,
) -> np.ndarray:
@@ -162,17 +162,18 @@ def train_supervised_thresholds(
return thresholds
-def save_thresholds(thresholds: np.ndarray, path: str):
+def save_thresholds(thresholds: np.ndarray, path: str) -> None:
"""Save thresholds to .npy file."""
_check_numpy()
np.save(path, thresholds)
-def load_thresholds(path: str) -> np.ndarray | None:
+def load_thresholds(path: str) -> Any | None:
"""Load thresholds from .npy file. Returns None if file doesn't exist."""
_check_numpy()
try:
- return np.load(path)
+ res: Any = np.load(path)
+ return res
except (FileNotFoundError, Exception):
return None
diff --git a/rag/router.py b/rag/router.py
index 14e46482..5b761207 100644
--- a/rag/router.py
+++ b/rag/router.py
@@ -114,7 +114,13 @@ def __init__(self, strategy: Strategy, context: list[dict[str, Any]], confidence
class RetrievalRouter:
- def __init__(self, layer: str = "user", user_id: str = "default", keyword_overrides: dict | None = None, recent_max_chars: int = 60):
+ def __init__(
+ self,
+ layer: str = "user",
+ user_id: str = "default",
+ keyword_overrides: dict[str, Any] | None = None,
+ recent_max_chars: int = 60,
+ ) -> None:
self.layer = layer
self.user_id = user_id
self.recent_max_chars = recent_max_chars
@@ -143,7 +149,7 @@ def _flat_keywords(self, kind: str) -> list[str]:
out.extend(v)
return [kw.lower() for kw in out]
- async def route(self, query: str, recent_context: list[dict] | None = None) -> RouterResult:
+ async def route(self, query: str, recent_context: list[dict[str, Any]] | None = None) -> RouterResult:
q = query.lower()
for route in _ROUTE_TABLE:
@@ -164,31 +170,51 @@ async def _match_route(
keyword_kind: str | None,
strategy: Strategy,
confidence: float,
- recent_context: list[dict] | None,
+ recent_context: list[dict[str, Any]] | None,
) -> RouterResult | None:
"""Try to match a single route. Returns RouterResult or None."""
if keyword_kind == "recent":
- if self._is_recent_query(q_lower) and recent_context:
- return RouterResult(strategy, recent_context, confidence)
+ return self._match_recent(q_lower, strategy, confidence, recent_context)
+
+ if keyword_kind == "wiki":
+ return await self._match_wiki(q_lower, query, strategy, confidence)
+
+ if keyword_kind == "entity":
+ return await self._match_entity(query, strategy, confidence)
+
+ if keyword_kind == "graph":
+ return await self._match_graph(q_lower, strategy, confidence)
+
+ if keyword_kind is None:
+ return await self._match_semantic(query, strategy, confidence)
+
+ return None
- elif keyword_kind == "wiki":
- if self._is_wiki_query(q_lower):
- return await self._route_wiki(query, strategy, confidence)
+ def _match_recent(self, q_lower: str, strategy: Strategy, confidence: float, recent_context: list[dict[str, Any]] | None) -> RouterResult | None:
+ if self._is_recent_query(q_lower) and recent_context:
+ return RouterResult(strategy, recent_context, confidence)
+ return None
- elif keyword_kind == "entity":
- entities = self._extract_entities(query)
- if entities:
- return await self._route_entities(entities, strategy, confidence)
+ async def _match_wiki(self, q_lower: str, query: str, strategy: Strategy, confidence: float) -> RouterResult | None:
+ if self._is_wiki_query(q_lower):
+ return await self._route_wiki(query, strategy, confidence)
+ return None
- elif keyword_kind == "graph":
- if self._is_graph_query(q_lower):
- return await self._route_graph(q_lower, strategy, confidence)
+ async def _match_entity(self, query: str, strategy: Strategy, confidence: float) -> RouterResult | None:
+ entities = self._extract_entities(query)
+ if entities:
+ return await self._route_entities(entities, strategy, confidence)
+ return None
- elif keyword_kind is None:
- results = await self._rag.search(query, self.user_id, strategy="hybrid", limit=3)
- if results:
- return RouterResult(strategy, results, confidence)
+ async def _match_graph(self, q_lower: str, strategy: Strategy, confidence: float) -> RouterResult | None:
+ if self._is_graph_query(q_lower):
+ return await self._route_graph(q_lower, strategy, confidence)
+ return None
+ async def _match_semantic(self, query: str, strategy: Strategy, confidence: float) -> RouterResult | None:
+ results = await self._rag.search(query, self.user_id, strategy="hybrid", limit=3)
+ if results:
+ return RouterResult(strategy, results, confidence)
return None
async def _route_wiki(self, query: str, strategy: Strategy, confidence: float) -> RouterResult:
diff --git a/rag/schema.py b/rag/schema.py
index ca3cc0f6..c538fd64 100644
--- a/rag/schema.py
+++ b/rag/schema.py
@@ -7,7 +7,7 @@
logger = logging.getLogger(__name__)
-async def init_rag_db(cm: AsyncConnectionManager, fts_available: bool):
+async def init_rag_db(cm: AsyncConnectionManager, fts_available: bool) -> None:
"""
Initialize RAG database schema.
"""
diff --git a/rag/search.py b/rag/search.py
index 4d25900d..858b89ee 100644
--- a/rag/search.py
+++ b/rag/search.py
@@ -3,6 +3,7 @@
import logging
from contextlib import suppress
from typing import Any
+from collections.abc import Callable
from shared.connection import AsyncConnectionManager
from shared.constants import DB_NAME
@@ -67,7 +68,7 @@ async def search_binary(
query: str,
user_id: str,
limit: int,
- binary_for_fn,
+ binary_for_fn: Callable[[list[float]], bytes],
binary_dim: int,
) -> list[dict[str, Any]]:
"""Exhaustive linear scan over binary embeddings."""
@@ -123,7 +124,7 @@ async def search_rrf(
user_id: str,
limit: int,
k: int = 60,
- binary_for_fn=None,
+ binary_for_fn: Callable[[list[float]], bytes] | None = None,
binary_dim: int = 384,
fts_available: bool = True,
) -> list[dict[str, Any]]:
@@ -132,9 +133,10 @@ async def search_rrf(
fts_ranks = {doc["id"]: rank for rank, doc in enumerate(fts_results)}
bin_ranks = {}
- with suppress(Exception):
- bin_results = await search_binary(cm, query, user_id, limit * 3, binary_for_fn, binary_dim)
- bin_ranks = {r["id"]: rank for rank, r in enumerate(bin_results)}
+ if binary_for_fn:
+ with suppress(Exception):
+ bin_results = await search_binary(cm, query, user_id, limit * 3, binary_for_fn, binary_dim)
+ bin_ranks = {r["id"]: rank for rank, r in enumerate(bin_results)}
def rrf(rank: int) -> float:
return 1.0 / (k + rank + 1)
@@ -156,7 +158,7 @@ def rrf(rank: int) -> float:
placeholders = ",".join(["?"] * len(sorted_ids))
cur = await conn.execute(
f"SELECT id, title, content, wiki_type FROM rag_pages WHERE id IN ({placeholders})",
- sorted_ids,
+ tuple(sorted_ids),
)
rows = await cur.fetchall()
by_id = {r[0]: r for r in rows}
@@ -189,7 +191,7 @@ def auto_strategy(query: str) -> str:
return "hybrid"
-def materialize_candidates(results: list[dict[str, Any]]) -> list:
+def materialize_candidates(results: list[dict[str, Any]]) -> list[Any]:
"""Convert raw search dicts to ScoredCandidate objects for the Scorer."""
from rag.scoring import ScoredCandidate
@@ -216,16 +218,16 @@ def materialize_candidates(results: list[dict[str, Any]]) -> list:
return list(seen.values())
-def format_result(c) -> dict[str, Any]:
+def format_result(c: Any) -> dict[str, Any]:
"""Convert a ScoredCandidate back to a result dict."""
- content = c.content
+ content: str = str(c.content)
if len(content) > 500:
content = content[:500] + "..."
return {
- "id": c.id,
- "title": c.title,
+ "id": int(c.id),
+ "title": str(c.title),
"content": content,
- "wiki_type": c.wiki_type,
- "score": c.final_score or c.rrf_score,
- "source": c.source,
+ "wiki_type": str(c.wiki_type),
+ "score": float(c.final_score or c.rrf_score),
+ "source": str(c.source),
}
diff --git a/rag/searcher.py b/rag/searcher.py
index abec9756..4d442647 100644
--- a/rag/searcher.py
+++ b/rag/searcher.py
@@ -31,16 +31,17 @@ def __init__(
async def _check_fts(self) -> bool:
if self._fts_available is not None:
- return self._fts_available
+ return bool(self._fts_available)
conn = await self._cm.get(DB_NAME)
try:
cur = await conn.execute("PRAGMA compile_options")
- options = [r[0] for r in await cur.fetchall()]
+ rows = await cur.fetchall()
+ options = [str(r[0]) for r in rows]
self._fts_available = "ENABLE_FTS5" in options
except Exception:
self._fts_available = False
- return self._fts_available or False
+ return bool(self._fts_available)
async def _search_fts5(self, query: str, user_id: str, limit: int) -> list[SearchResult]:
fts_ready = await self._check_fts()
@@ -59,7 +60,7 @@ async def _search_fts5(self, query: str, user_id: str, limit: int) -> list[Searc
async def _search_mib(self, query: str, user_id: str, limit: int) -> list[SearchResult]:
from rag.quantize import embed_to_binary
- def default_bin_for(emb):
+ def default_bin_for(emb: list[float]) -> bytes:
return embed_to_binary(emb, threshold=0.0, dim=len(emb))
raw_results = await search_binary(self._cm, query, user_id, limit, default_bin_for, self.binary_dim)
diff --git a/shared/archived_memories.py b/shared/archived_memories.py
index 21c5be67..b47b7cdd 100644
--- a/shared/archived_memories.py
+++ b/shared/archived_memories.py
@@ -43,7 +43,8 @@ async def archive(
(user_id, original_id, content, memory_type, importance, reason),
)
await conn.commit()
- return int(cursor.lastrowid)
+ last_id: Any = cursor.lastrowid
+ return int(last_id) if last_id is not None else 0
async def get_archived(self, user_id: str = "default", limit: int = 50) -> list[dict[str, Any]]:
conn = await self._cm.get(DB_NAME)
diff --git a/shared/dream_buffer.py b/shared/dream_buffer.py
index 1c5c8b13..0f9f9aad 100644
--- a/shared/dream_buffer.py
+++ b/shared/dream_buffer.py
@@ -46,7 +46,8 @@ async def add(
(user_id, session_id, event_id, content, importance, json.dumps(metadata or {})),
)
await conn.commit()
- return int(cursor.lastrowid)
+ last_id: Any = cursor.lastrowid
+ return int(last_id) if last_id is not None else 0
async def get_staging(self, user_id: str = "default", session_id: str | None = None) -> list[dict[str, Any]]:
conn = await self._cm.get(DB_NAME)
diff --git a/shared/embeddings.py b/shared/embeddings.py
index 78821255..90e90e3f 100644
--- a/shared/embeddings.py
+++ b/shared/embeddings.py
@@ -22,7 +22,7 @@ def _get_model(model_name: str | None = None) -> Any:
target = model_name or DEFAULT_MODEL
if _model is None or _model_name != target:
try:
- from sentence_transformers import SentenceTransformer
+ from sentence_transformers import SentenceTransformer # type: ignore[import-not-found]
_model = SentenceTransformer(target)
_model_name = target
diff --git a/shared/importance/scorer.py b/shared/importance/scorer.py
index 86881672..5dd501a6 100644
--- a/shared/importance/scorer.py
+++ b/shared/importance/scorer.py
@@ -39,8 +39,8 @@ def __init__(
]
self._config_path = Path(config_path)
self._data_path = Path(data_path)
- self._tech_re = None
- self._noise_re = None
+ self._tech_re: re.Pattern[str] | None = None
+ self._noise_re: re.Pattern[str] | None = None
def _load_config(self) -> ImportanceConfig:
if self._config:
@@ -72,7 +72,7 @@ def score(self, text: str, context: dict[str, Any] | None = None, **kwargs: Any)
context["tech_re"] = self._tech_re
context["noise_re"] = self._noise_re
- results = {}
+ results: dict[str, float] = {}
for signal in self._signals:
name = signal.__class__.__name__.lower().replace("signal", "")
if name == "basetype":
@@ -85,7 +85,7 @@ def score(self, text: str, context: dict[str, Any] | None = None, **kwargs: Any)
name = "noise_penalty"
elif name == "emotion":
name = "emotional"
- results[name] = signal.calculate(text, context)
+ results[name] = float(signal.calculate(text, context))
signals = ImportanceSignals(**results)
diff --git a/shared/importance/signals/emotion_signal.py b/shared/importance/signals/emotion_signal.py
index f92df310..50eae704 100644
--- a/shared/importance/signals/emotion_signal.py
+++ b/shared/importance/signals/emotion_signal.py
@@ -12,11 +12,12 @@ def calculate(self, text: str, context: dict[str, Any]) -> float:
engine = context.get("_emotion_engine")
if not engine:
# Fallback to provided emotion_weight if any
- return max(0.0, min(1.0, context.get("emotion_weight", 0.0)))
+ val: Any = context.get("emotion_weight", 0.0)
+ return float(max(0.0, min(1.0, float(val))))
results = engine.detect(text)
if not results:
return 0.0
# Use max score from detected emotions
- return max(res.score for res in results)
+ return float(max(float(res.score) for res in results))
diff --git a/shared/memory_types.py b/shared/memory_types.py
index 538788a6..6f379ddf 100644
--- a/shared/memory_types.py
+++ b/shared/memory_types.py
@@ -256,4 +256,4 @@ async def backfill_null_kinds(cm: Any, dry_run: bool = True) -> int:
return int(row["c"])
cur = await conn.execute("UPDATE core_memory SET memory_kind = 'fact' WHERE memory_kind IS NULL")
await conn.commit()
- return cur.rowcount
+ return int(cur.rowcount)
diff --git a/shared/middleware.py b/shared/middleware.py
index 91437c8b..42e4275c 100644
--- a/shared/middleware.py
+++ b/shared/middleware.py
@@ -20,9 +20,9 @@ class MiddlewareContext:
tool_name: str = ""
user_id: str = "default"
- args: dict = field(default_factory=dict)
+ args: dict[str, Any] = field(default_factory=dict)
result: Any = None
- metadata: dict = field(default_factory=dict)
+ metadata: dict[str, Any] = field(default_factory=dict)
start_time: float = 0.0
blocked: bool = False
block_reason: str = ""
@@ -90,7 +90,7 @@ def __init__(
threshold: float = 0.3,
technical_weight: float = 0.3,
question_weight: float = 0.2,
- scorer=None,
+ scorer: Any = None,
memory_kind_hint: str | None = None,
):
self._min_length = min_length
@@ -145,7 +145,7 @@ async def process(self, ctx: MiddlewareContext, next: MiddlewareNext) -> Any:
def calculate_score(self, text: str) -> float:
"""Calculate importance score using ImportanceScorer."""
signals = self._scorer.score(text=text, kind=self.memory_kind_hint)
- return signals.total()
+ return float(signals.total())
class ValidationMiddleware(Middleware):
@@ -191,20 +191,20 @@ async def process(self, ctx: MiddlewareContext, next: MiddlewareNext) -> Any:
class MiddlewarePipeline:
"""Middleware chain."""
- def __init__(self):
+ def __init__(self) -> None:
self._middlewares: list[Middleware] = []
def add(self, middleware: Middleware) -> MiddlewarePipeline:
self._middlewares.append(middleware)
return self
- async def execute(self, ctx: MiddlewareContext, handler: Callable) -> Any:
+ async def execute(self, ctx: MiddlewareContext, handler: Callable[[MiddlewareContext], Any]) -> Any:
async def _run(index: int, ctx: MiddlewareContext) -> Any:
if index >= len(self._middlewares):
- result = handler(ctx)
- if hasattr(result, "__await__"):
- return await result
- return result
+ res = handler(ctx)
+ if hasattr(res, "__await__"):
+ return await res
+ return res
return await self._middlewares[index].process(ctx, lambda c: _run(index + 1, c))
return await _run(0, ctx)
@@ -214,7 +214,7 @@ def list_middlewares(self) -> list[str]:
# Default pipeline
-default_pipeline = MiddlewarePipeline()
+default_pipeline: MiddlewarePipeline = MiddlewarePipeline()
default_pipeline.add(ValidationMiddleware())
default_pipeline.add(RateLimitMiddleware())
default_pipeline.add(ImportanceGateMiddleware())
diff --git a/shared/saga/__init__.py b/shared/saga/__init__.py
index 15346f6d..1624a4be 100644
--- a/shared/saga/__init__.py
+++ b/shared/saga/__init__.py
@@ -1,7 +1,14 @@
from shared.saga.impl.base import Saga, SAGA_DIR, saga_watchdog, SagaWatchdog
from shared.saga.impl.backup import create_backup_saga
from shared.saga.impl.consolidation import create_consolidation_saga
-from shared.saga.impl.crypto import encrypt_json, decrypt_json, write_state_atomic, read_state, read_state_legacy_or_encrypted
+from shared.saga.impl.crypto import (
+ encrypt_json as encrypt_json,
+ decrypt_json as decrypt_json,
+ write_state_atomic as write_state_atomic,
+ read_state as read_state,
+ read_state_legacy_or_encrypted as read_state_legacy_or_encrypted,
+ is_encrypted_blob as is_encrypted_blob,
+)
from shared.saga.schema import SagaStatus, SagaStepState, SagaState
from shared.saga.engine import SagaEngine, SagaStep
from shared.saga.persistence import FileSagaStore, ISagaStore
@@ -21,6 +28,7 @@
"create_consolidation_saga",
"decrypt_json",
"encrypt_json",
+ "is_encrypted_blob",
"read_state",
"read_state_legacy_or_encrypted",
"saga_watchdog",
diff --git a/shared/saga/impl/base.py b/shared/saga/impl/base.py
index 3763f6cb..5eea3021 100644
--- a/shared/saga/impl/base.py
+++ b/shared/saga/impl/base.py
@@ -35,7 +35,6 @@ def is_encrypted_blob(path: Path) -> bool:
"""Check if file is encrypted (not plain JSON)."""
if not path.exists():
return False
- # noqa: SKY-D325
with path.open("rb") as f:
head = f.read(1)
return _is_crypto_encrypted_blob(head)
@@ -123,7 +122,7 @@ def add_step(
def _save_state(self) -> None:
"""Save state to disk (encrypted if available)."""
state_file = SAGA_DIR / (self._saga_id + ".json")
- state = {
+ state: dict[str, Any] = {
"name": self.name,
"saga_id": self._saga_id,
"status": self._status.value,
@@ -137,15 +136,13 @@ def _save_state(self) -> None:
SAGA_DIR.mkdir(parents=True, exist_ok=True)
if _HAS_ENCRYPTION:
blob = encrypt_json(state)
- # noqa: SKY-D324
state_file.write_bytes(blob)
else:
- # noqa: SKY-D324
state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")
except Exception:
logger.exception("Failed to save saga state")
- def _load_state(self, saga_id: str) -> dict | None:
+ def _load_state(self, saga_id: str) -> dict[str, Any] | None:
"""Load state from disk (supports encrypted and legacy plain JSON)."""
from shared.saga import read_state_legacy_or_encrypted
@@ -208,7 +205,8 @@ async def _get_cached_result(self, key: str) -> dict[str, Any] | None:
return None
result_blob = row["result_json"]
if isinstance(result_blob, (bytes, bytearray)):
- return decrypt_json(bytes(result_blob))
+ res: Any = decrypt_json(bytes(result_blob))
+ return dict(res) if isinstance(res, dict) else None
return dict(json.loads(result_blob)) if result_blob else None
except Exception:
return None
@@ -280,7 +278,7 @@ async def _run_step_action(self, step: SagaStep) -> dict[str, Any]:
if asyncio.iscoroutine(action_result):
result = await asyncio.wait_for(action_result, timeout=float(step_timeout))
else:
- result = action_result # type: ignore[assignment]
+ result = action_result
return result if isinstance(result, dict) else {"value": result}
async def _handle_retry_pause(self, step: SagaStep, attempt: int, exc: Exception) -> None:
@@ -449,10 +447,8 @@ def _check_stuck_sagas(self) -> None:
state["status"] = "stuck"
state["stuck_reason"] = f"timeout_after_{int(age)}s"
if _HAS_ENCRYPTION:
- # noqa: SKY-D324
state_file.write_bytes(encrypt_json(state))
else:
- # noqa: SKY-D324
state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")
logger.warning("Saga '%s' marked as STUCK (age=%ds)", saga_name, int(age))
@@ -500,10 +496,8 @@ def recover_saga(self, saga_id: str) -> dict[str, Any] | None:
state["status"] = "manual_review_required"
state["recovered_at"] = time.time()
if _HAS_ENCRYPTION:
- # noqa: SKY-D324
state_file.write_bytes(encrypt_json(state))
else:
- # noqa: SKY-D324
state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")
return {"status": "manual_review_required", "state": state}
diff --git a/shared/saga/impl/crypto.py b/shared/saga/impl/crypto.py
index 29e71539..79e297a1 100644
--- a/shared/saga/impl/crypto.py
+++ b/shared/saga/impl/crypto.py
@@ -15,15 +15,23 @@
from features.secrets import decrypt_json, encrypt_json
from shared.crypto import is_encrypted_blob as _is_crypto_encrypted_blob
+__all__ = [
+ "decrypt_json",
+ "encrypt_json",
+ "is_encrypted_blob",
+ "read_state",
+ "read_state_legacy_or_encrypted",
+ "write_state_atomic",
+]
+
def is_encrypted_blob(path: Path) -> bool:
"""Check if file is encrypted (not plain JSON)."""
if not path.exists():
return False
- # noqa: SKY-D325
with path.open("rb") as f:
head = f.read(1)
- return _is_crypto_encrypted_blob(head)
+ return bool(_is_crypto_encrypted_blob(head))
if TYPE_CHECKING:
@@ -40,11 +48,9 @@ def write_state_atomic(path: Path, state: dict[str, Any]) -> None:
blob = encrypt_json(state)
tmp = path.with_suffix(path.suffix + ".tmp")
with tmp.open("wb") as f:
- # noqa: SKY-D324
f.write(blob)
with contextlib.suppress(OSError, PermissionError):
os.chmod(tmp, 0o600)
- # noqa: SKY-D324
tmp.replace(path)
with contextlib.suppress(OSError, PermissionError):
os.chmod(path, 0o600)
@@ -54,22 +60,23 @@ def read_state(path: Path) -> dict[str, Any]:
"""Read encrypted state file."""
if not path.exists():
raise FileNotFoundError(path)
- # noqa: SKY-D325
with path.open("rb") as f:
blob = f.read()
- return decrypt_json(blob)
+ res: Any = decrypt_json(blob)
+ return dict(res) if isinstance(res, dict) else {}
def read_state_legacy_or_encrypted(path: Path) -> dict[str, Any]:
"""Backward-compat: reads legacy plain JSON or encrypted, rotates legacy to encrypted."""
if not path.exists():
raise FileNotFoundError(path)
- # noqa: SKY-D325
with path.open("rb") as f:
blob = f.read()
if is_encrypted_blob(path):
- return decrypt_json(blob)
+ res: Any = decrypt_json(blob)
+ return dict(res) if isinstance(res, dict) else {}
warnings.warn(f"{path} is plain JSON; rotating to encrypted", DeprecationWarning, stacklevel=2)
- legacy = json.loads(blob.decode("utf-8"))
- write_state_atomic(path, legacy)
- return legacy
+ legacy: Any = json.loads(blob.decode("utf-8"))
+ state: dict[str, Any] = dict(legacy) if isinstance(legacy, dict) else {}
+ write_state_atomic(path, state)
+ return state
diff --git a/wiki/index.py b/wiki/index.py
index 59629813..547204bd 100644
--- a/wiki/index.py
+++ b/wiki/index.py
@@ -8,9 +8,13 @@
import json
import logging
import time
+from typing import Any, TYPE_CHECKING
from shared.constants import DB_NAME
-from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from wiki.models import WikiEntry
+ from shared.connection import AsyncConnectionManager
if TYPE_CHECKING:
from wiki.models import WikiEntry
@@ -26,7 +30,7 @@ def __init__(self, connection_manager: AsyncConnectionManager, layer: str):
self._cm = connection_manager
self.layer = layer
- async def init_db(self):
+ async def init_db(self) -> None:
"""Initialize tables and indexes."""
await self._cm.execute_script(
DB_NAME,
@@ -57,7 +61,7 @@ async def init_db(self):
""",
)
- async def save(self, entry: WikiEntry, content_hash: str):
+ async def save(self, entry: WikiEntry, content_hash: str) -> None:
"""Atomic insert/update in both wiki_index and wiki_fts."""
now = time.time()
tags_json = json.dumps(entry.tags)
@@ -120,7 +124,7 @@ async def save(self, entry: WikiEntry, content_hash: str):
await conn.commit()
- async def get_by_path(self, file_path: str) -> dict | None:
+ async def get_by_path(self, file_path: str) -> dict[str, Any] | None:
"""Fetch metadata and hash by file path."""
conn = await self._cm.get(DB_NAME)
cur = await conn.execute(
@@ -130,7 +134,7 @@ async def get_by_path(self, file_path: str) -> dict | None:
row = await cur.fetchone()
return dict(row) if row else None
- async def search(self, query: str, limit: int = 10) -> list[dict]:
+ async def search(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
"""Optimized FTS5 search JOINing wiki_fts with wiki_index."""
conn = await self._cm.get(DB_NAME)
try:
@@ -148,7 +152,7 @@ async def search(self, query: str, limit: int = 10) -> list[dict]:
logger.exception("Search failed for query '%s'", query)
return []
- async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict]:
+ async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict[str, Any]]:
"""List entries of a specific type."""
conn = await self._cm.get(DB_NAME)
cur = await conn.execute(
@@ -158,7 +162,7 @@ async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict]:
rows = await cur.fetchall()
return [dict(r) for r in rows]
- async def list_all(self, limit: int = 50) -> list[dict]:
+ async def list_all(self, limit: int = 50) -> list[dict[str, Any]]:
"""List all entries in the current layer."""
conn = await self._cm.get(DB_NAME)
cur = await conn.execute(
@@ -168,7 +172,7 @@ async def list_all(self, limit: int = 50) -> list[dict]:
rows = await cur.fetchall()
return [dict(r) for r in rows]
- async def delete(self, file_path: str):
+ async def delete(self, file_path: str) -> None:
"""Remove from both tables."""
conn = await self._cm.get(DB_NAME)
cur = await conn.execute(
diff --git a/wiki/manager.py b/wiki/manager.py
index c2ee0cf2..8beceba4 100644
--- a/wiki/manager.py
+++ b/wiki/manager.py
@@ -47,7 +47,7 @@ def __init__(self, layer: str = "user", base_dir: str | None = None, cm: AsyncCo
self.index = WikiIndex(self._cm, layer)
self.parser = WikiParser()
- async def init_db(self):
+ async def init_db(self) -> None:
"""Delegate to index layer."""
await self.index.init_db()
@@ -95,7 +95,7 @@ async def update(
content: str | None = None,
tags: list[str] | None = None,
importance: float | None = None,
- ):
+ ) -> None:
"""Update .md file and re-index."""
p = safe_resolve(self.base_dir, file_path)
if not await asyncio.to_thread(p.exists):
@@ -153,10 +153,10 @@ async def list_all(self, limit: int = 50) -> list[WikiEntry]:
rows = await self.index.list_all(limit)
return await self._rows_to_entries(rows)
- async def _rows_to_entries(self, rows: list[dict]) -> list[WikiEntry]:
+ async def _rows_to_entries(self, rows: list[dict[str, Any]]) -> list[WikiEntry]:
entries = []
for r in rows:
- entry = await self.get(r["file_path"])
+ entry = await self.get(str(r["file_path"]))
if entry:
entries.append(entry)
return entries
@@ -187,8 +187,8 @@ async def reindex_all(self) -> dict[str, int]:
md_files = []
enabled_types = self._get_enabled_types()
- def _collect_files():
- files = []
+ def _collect_files() -> list[Path]:
+ files: list[Path] = []
for wiki_type in enabled_types:
type_dir = self.base_dir / wiki_type
if type_dir.exists() and type_dir.is_dir():
@@ -197,7 +197,7 @@ def _collect_files():
md_files = await asyncio.to_thread(_collect_files)
- async def _process_file(f: Path):
+ async def _process_file(f: Path) -> str:
try:
text = await asyncio.to_thread(f.read_text, encoding="utf-8")
entry = self.parser.parse(text, f)
@@ -235,12 +235,12 @@ async def sync_external(self, external_dirs: list[str] | None = None) -> dict[st
if not await asyncio.to_thread(p.exists):
continue
- def _find_md():
+ def _find_md() -> list[Path]:
return list(p.glob("**/*.md"))
md_files = await asyncio.to_thread(_find_md)
- async def _sync_file(f: Path):
+ async def _sync_file(f: Path) -> str:
try:
content = await asyncio.to_thread(f.read_text, encoding="utf-8")
parsed_entry = self.parser.parse(content, f)
diff --git a/wiki/shared.py b/wiki/shared.py
index 85354886..c8ead1e2 100644
--- a/wiki/shared.py
+++ b/wiki/shared.py
@@ -10,31 +10,36 @@
from typing import Any
-def load_config() -> dict:
+def load_config() -> dict[str, Any]:
"""Load config.yaml, return {} on failure."""
try:
import yaml
config_path = Path(__file__).parent.parent / "config.yaml"
with config_path.open() as f:
- return yaml.safe_load(f) or {}
- except (OSError, yaml.YAMLError):
+ res: Any = yaml.safe_load(f)
+ return dict(res) if isinstance(res, dict) else {}
+ except (OSError, Exception):
return {}
def get_enabled_types(layer: str, all_types: list[str]) -> list[str]:
"""Return wiki types enabled in config for the given layer."""
cfg = load_config()
- layer_cfg = cfg.get("wiki", {}).get(layer, {})
+ wiki_cfg: dict[str, Any] = cfg.get("wiki", {})
+ layer_cfg: dict[str, Any] = wiki_cfg.get(layer, {})
if not layer_cfg:
return all_types
- return [t for t in all_types if layer_cfg.get(t, True)]
+ return [t for t in all_types if bool(layer_cfg.get(t, True))]
def get_external_dirs(layer: str) -> list[str]:
"""Return external directory paths from config for the given layer."""
cfg = load_config()
- return cfg.get("wiki", {}).get(layer, {}).get("external_dirs", [])
+ wiki_cfg: dict[str, Any] = cfg.get("wiki", {})
+ layer_cfg: dict[str, Any] = wiki_cfg.get(layer, {})
+ res: Any = layer_cfg.get("external_dirs", [])
+ return [str(x) for x in res] if isinstance(res, list) else []
ALLOWED_TABLES = {"user_wiki", "agent_wiki", "wiki_index"}
@@ -43,18 +48,19 @@ def get_external_dirs(layer: str) -> list[str]:
def parse_tags(raw_tags: Any) -> list[str]:
"""Parse tags from JSON string or list."""
if isinstance(raw_tags, str):
- return json.loads(raw_tags) if raw_tags else []
- return raw_tags or []
+ res: Any = json.loads(raw_tags) if raw_tags else []
+ return [str(x) for x in res] if isinstance(res, list) else []
+ return [str(x) for x in raw_tags] if isinstance(raw_tags, list) else []
-def format_search_result(row: tuple, content_limit: int = 300) -> dict[str, Any]:
+def format_search_result(row: tuple[Any, ...], content_limit: int = 300) -> dict[str, Any]:
"""Format FTS search result row into dict."""
return {
"id": row[0],
"title": row[1],
- "content": row[2][:content_limit],
+ "content": str(row[2])[:content_limit],
"type": row[3],
"tags": parse_tags(row[4]),
"importance": row[5],
- "score": abs(row[6]) if row[6] else 0,
+ "score": abs(float(row[6])) if row[6] else 0.0,
}