Skip to content

Commit 4e021a2

Browse files
committed
copilot suggestions
1 parent 8b9591e commit 4e021a2

3 files changed

Lines changed: 67 additions & 32 deletions

File tree

src/ai_agent/agent/tools/repo_info_tool.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async def tool_repo_summary(input: RepoSummaryInput) -> RepoSummaryOutput:
8484

8585
await _REPO_INFO_LOCK.acquire()
8686
try:
87-
raw = db.get(_REPO_INFO_NS, cache_key)
87+
raw = await asyncio.to_thread(db.get, _REPO_INFO_NS, cache_key)
8888
if raw is not None:
8989
cached = RepoSummaryOutput.model_validate_json(raw)
9090
log.info(f"Repo info cache hit for {effective_url}")
@@ -120,7 +120,8 @@ async def tool_repo_summary(input: RepoSummaryInput) -> RepoSummaryOutput:
120120
await _REPO_INFO_LOCK.acquire()
121121
try:
122122
if result.source != "error" and REPO_INFO_CACHE_TTL_SECONDS > 0:
123-
db.set(
123+
await asyncio.to_thread(
124+
db.set,
124125
_REPO_INFO_NS,
125126
cache_key,
126127
result.model_dump_json(),

src/ai_agent/utils/cache_db.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,15 @@ def __init__(self, db_path: str | Path | None = None) -> None:
7676
# Public API
7777
# ------------------------------------------------------------------
7878

79-
def get(self, namespace: str, key: str) -> Optional[str]:
80-
"""Return the cached value or *None* if missing / expired."""
79+
def get(
80+
self, namespace: str, key: str, *, track_lru: bool = True
81+
) -> Optional[str]:
82+
"""Return the cached value or *None* if missing / expired.
83+
84+
*track_lru* (default ``True``) updates *accessed_at* on a hit so that
85+
LRU eviction in :meth:`set` works correctly. Pass ``False`` when the
86+
namespace has no capacity limit and you want to avoid the extra write.
87+
"""
8188
now = time.time()
8289
with self._lock:
8390
row = self._conn.execute(
@@ -95,12 +102,13 @@ def get(self, namespace: str, key: str) -> Optional[str]:
95102
)
96103
self._conn.commit()
97104
return None
98-
# Touch for LRU tracking
99-
self._conn.execute(
100-
"UPDATE cache SET accessed_at = ? WHERE namespace = ? AND key = ?",
101-
(now, namespace, key),
102-
)
103-
self._conn.commit()
105+
if track_lru:
106+
# Touch accessed_at so LRU eviction in set() stays accurate.
107+
self._conn.execute(
108+
"UPDATE cache SET accessed_at = ? WHERE namespace = ? AND key = ?",
109+
(now, namespace, key),
110+
)
111+
self._conn.commit()
104112
return value
105113

106114
def set(
@@ -179,6 +187,36 @@ def clear(self, namespace: Optional[str] = None) -> None:
179187
self._conn.execute("DELETE FROM cache")
180188
self._conn.commit()
181189

190+
def sweep_expired(self) -> int:
191+
"""Delete all expired rows across every namespace.
192+
193+
Returns the number of rows removed. Designed to be called from a
194+
background thread or an atexit hook without touching private state.
195+
"""
196+
now = time.time()
197+
with self._lock:
198+
deleted = self._conn.execute(
199+
"DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at <= ?",
200+
(now,),
201+
).rowcount
202+
self._conn.commit()
203+
return deleted
204+
205+
def vacuum_and_close(self) -> None:
206+
"""Sweep expired rows, VACUUM the database, then close the connection.
207+
208+
VACUUM must run outside any open transaction; this method handles the
209+
``isolation_level`` toggling safely with a ``finally`` guard.
210+
"""
211+
self.sweep_expired()
212+
with self._lock:
213+
try:
214+
self._conn.isolation_level = None
215+
self._conn.execute("VACUUM")
216+
finally:
217+
self._conn.isolation_level = ""
218+
self.close()
219+
182220
def close(self) -> None:
183221
"""Close the underlying database connection."""
184222
try:
@@ -206,6 +244,16 @@ def get_cache_db() -> CacheDB:
206244
return _db
207245

208246

247+
def get_cache_db_or_none() -> Optional[CacheDB]:
248+
"""Return the singleton if it has already been initialised, else ``None``.
249+
250+
Unlike :func:`get_cache_db` this never creates the database — useful for
251+
background housekeeping that should be a no-op before the first real cache
252+
access.
253+
"""
254+
return _db
255+
256+
209257
def reset_cache_db(db: Optional[CacheDB] = None) -> None:
210258
"""Replace the singleton — mainly useful for tests.
211259

src/ai_agent/utils/shutdown.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,13 @@
3939

4040
def _sweep_cache_db() -> None:
4141
"""Delete expired rows from the cache DB (lightweight, runs periodically)."""
42-
from ai_agent.utils.cache_db import _db # noqa: PLC0415
42+
from ai_agent.utils.cache_db import get_cache_db_or_none # noqa: PLC0415
4343

44-
if _db is None:
44+
db = get_cache_db_or_none()
45+
if db is None:
4546
return
4647
try:
47-
now = time.time()
48-
with _db._lock:
49-
deleted = _db._conn.execute(
50-
"DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at <= ?",
51-
(now,),
52-
).rowcount
53-
_db._conn.commit()
48+
deleted = db.sweep_expired()
5449
if deleted:
5550
log.debug("Cache sweep: removed %d expired row(s).", deleted)
5651
except Exception:
@@ -59,23 +54,14 @@ def _sweep_cache_db() -> None:
5954

6055
def _vacuum_and_close_cache_db() -> None:
6156
"""Final shutdown: VACUUM and close the cache DB (runs via atexit)."""
62-
from ai_agent.utils.cache_db import _db, get_cache_db # noqa: PLC0415
57+
from ai_agent.utils.cache_db import get_cache_db_or_none # noqa: PLC0415
6358

64-
if _db is None:
59+
db = get_cache_db_or_none()
60+
if db is None:
6561
return
6662
try:
67-
db = get_cache_db()
68-
_sweep_cache_db()
69-
with db._lock:
70-
# VACUUM must run outside any transaction.
71-
previous_isolation_level = db._conn.isolation_level
72-
try:
73-
db._conn.isolation_level = None
74-
db._conn.execute("VACUUM")
75-
finally:
76-
db._conn.isolation_level = previous_isolation_level
63+
db.vacuum_and_close()
7764
log.info("Cache DB shutdown: VACUUM complete.")
78-
db.close()
7965
except Exception:
8066
log.exception("Cache DB shutdown cleanup failed.")
8167

0 commit comments

Comments
 (0)