Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .pytest_cache/v/cache/lastfailed
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 10 additions & 8 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
33 changes: 17 additions & 16 deletions core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Comment on lines +35 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat invalid cache values as cache misses.

At Line 39, a non-list cache value returns [] and skips both L4 and episodic searches. A malformed or legacy cache entry can therefore hide valid memories. Fall through to the storage search and overwrite the invalid cache value.

Proposed fix
         if cached is not None:
-            return list(cached) if isinstance(cached, list) else []
+            if isinstance(cached, list):
+                return list(cached)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
async def recall(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
cache_key = f"recall:{self.user_id}:{query}:{limit}"
cached: Any = self._cache.get(cache_key) if self._cache else None
if cached is not None:
if isinstance(cached, list):
return list(cached)
results: list[dict[str, Any]] = []
l4_hits = await self.l4.search(self.user_id, query, limit)
results.extend(l4_hits)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/__init__.py` around lines 35 - 43, Update recall so cached values are
returned only when cached is a list; treat any non-list value as a cache miss,
continue through the existing l4.search and episodic search flow, and overwrite
the invalid cache entry with the resulting memories.

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]
Expand All @@ -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:
Expand All @@ -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
Expand Down
35 changes: 18 additions & 17 deletions core/episodic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
"""
Expand All @@ -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 [])),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Episode INSERT binding mismatch

When any episode is saved, this four-value tuple is bound to an INSERT with five placeholders, causing SQLite to raise a binding-count error before the episode is persisted.

Suggested change
(user_id, summary, emotional_weight, json.dumps(tags or [])),
(user_id, summary, emotional_weight, json.dumps(tags or []), time.time()),
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/episodic.py
Line: 51

Comment:
**Episode INSERT binding mismatch**

When any episode is saved, this four-value tuple is bound to an `INSERT` with five placeholders, causing SQLite to raise a binding-count error before the episode is persisted.

```suggestion
            (user_id, summary, emotional_weight, json.dumps(tags or []), time.time()),
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

)
await conn.commit()
return cursor.lastrowid
return int(cursor.lastrowid or 0)
Comment on lines +51 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the created_at bind value.

The SQL statement has five placeholders, but the tuple has four values. Every call to save fails with a binding-count error. Add the timestamp as the fifth value.

Proposed fix
-            (user_id, summary, emotional_weight, json.dumps(tags or [])),
+            (user_id, summary, emotional_weight, json.dumps(tags or []), time.time()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(user_id, summary, emotional_weight, json.dumps(tags or [])),
)
await conn.commit()
return cursor.lastrowid
return int(cursor.lastrowid or 0)
(user_id, summary, emotional_weight, json.dumps(tags or []), time.time()),
)
await conn.commit()
return int(cursor.lastrowid or 0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/episodic.py` around lines 51 - 54, Add the missing created_at timestamp
bind value to the parameter tuple in save, matching the five placeholders in its
SQL statement; preserve the existing user_id, summary, emotional_weight, and
serialized tags bindings and commit/return behavior.


async def get_episodes(self, user_id: str, limit: int = 20, offset: int = 0) -> list[Episode]:
conn = await self._cm.get(DB_NAME)
Expand All @@ -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 ?",
Expand Down Expand Up @@ -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

Expand All @@ -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"]),
)
34 changes: 17 additions & 17 deletions core/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions features/auth/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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.
Expand Down Expand Up @@ -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():
Expand Down
3 changes: 0 additions & 3 deletions features/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions features/backup_cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading
Loading