md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""IdleStore — last_activity_ts rows backing the Idle trigger.
|
|
|
|
All writes pass through ``to_iso_format`` over a tz-aware datetime, so
|
|
``last_activity_ts`` is a fixed-format ISO 8601 string whose
|
|
lexicographic order matches temporal order — :meth:`scan_idle` relies
|
|
on this to keep the column un-wrapped in its predicate so SQLite can
|
|
use ``idx_idle_scan``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from everos.component.utils.datetime import from_iso_format, to_iso_format
|
|
from everos.infra.ome._stores.storage import OMEStorage
|
|
|
|
|
|
class IdleStore:
|
|
"""SQLite-backed last-activity tracker for the ``Idle`` trigger."""
|
|
|
|
def __init__(self, storage: OMEStorage) -> None:
|
|
self._storage = storage
|
|
|
|
async def touch(self, strategy_name: str, bucket_key: str, *, at: datetime) -> None:
|
|
"""UPSERT ``last_activity_ts = at`` for ``(strategy_name, bucket_key)``."""
|
|
async with self._storage.connect() as conn:
|
|
await conn.execute(
|
|
"INSERT INTO idle_store "
|
|
"(strategy_name, bucket_key, last_activity_ts) "
|
|
"VALUES (?, ?, ?) "
|
|
"ON CONFLICT(strategy_name, bucket_key) DO UPDATE SET "
|
|
"last_activity_ts = excluded.last_activity_ts",
|
|
(strategy_name, bucket_key, to_iso_format(at)),
|
|
)
|
|
await conn.commit()
|
|
|
|
async def scan_idle(
|
|
self, strategy_name: str, *, idle_seconds: int, now: datetime
|
|
) -> list[str]:
|
|
"""Return bucket_keys with ``last_activity_ts`` older than ``idle_seconds``."""
|
|
# Cutoff on the RHS so the indexed column stays un-wrapped.
|
|
cutoff = to_iso_format(now - timedelta(seconds=idle_seconds))
|
|
async with self._storage.connect() as conn:
|
|
cur = await conn.execute(
|
|
"SELECT bucket_key FROM idle_store "
|
|
"WHERE strategy_name = ? AND last_activity_ts <= ? "
|
|
"ORDER BY last_activity_ts ASC",
|
|
(strategy_name, cutoff),
|
|
)
|
|
rows = await cur.fetchall()
|
|
return [r[0] for r in rows]
|
|
|
|
async def get_last_activity(
|
|
self, strategy_name: str, bucket_key: str
|
|
) -> datetime | None:
|
|
"""Return the stored ``last_activity_ts`` (``None`` if never touched)."""
|
|
async with self._storage.connect() as conn:
|
|
cur = await conn.execute(
|
|
"SELECT last_activity_ts FROM idle_store "
|
|
"WHERE strategy_name = ? AND bucket_key = ?",
|
|
(strategy_name, bucket_key),
|
|
)
|
|
row = await cur.fetchone()
|
|
return from_iso_format(row[0]) if row else None
|