mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
Add dry-run sqlite retention prune for tool/trace bloat.
Keeps recent days of chat tool messages, tool_log, traces, and sent outbound; admin API and CLI support apply + optional VACUUM. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cc9a516cb4
commit
48e5872045
4 changed files with 619 additions and 0 deletions
141
tests/test_sqlite_retention.py
Normal file
141
tests/test_sqlite_retention.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from svc.persistence.sqlite_retention import prune_sqlite_retention
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
|
||||
|
||||
class SqliteRetentionTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
self.db = Path(self._tmp.name) / "ret.sqlite"
|
||||
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
|
||||
os.environ["AIA_ASSISTANT_DB_BACKEND"] = "sqlite"
|
||||
self.store = SqliteStore(str(self.db))
|
||||
self.sess = self.store.create_session("retention-test")
|
||||
self.session_id = str(self.sess.id)
|
||||
self.now = datetime(2026, 8, 10, 12, 0, 0, tzinfo=timezone.utc)
|
||||
self.old = (self.now - timedelta(days=40)).isoformat()
|
||||
self.fresh = (self.now - timedelta(days=2)).isoformat()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _seed(self) -> None:
|
||||
with self.store._connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO chat_message(session_id, role, content, timestamp) VALUES (?,?,?,?)",
|
||||
(self.session_id, "tool", "OLD_TOOL_PAYLOAD" * 100, self.old),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO chat_message(session_id, role, content, timestamp) VALUES (?,?,?,?)",
|
||||
(self.session_id, "tool", "FRESH_TOOL", self.fresh),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO chat_message(session_id, role, content, timestamp) VALUES (?,?,?,?)",
|
||||
(self.session_id, "assistant", "keep me", self.old),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO tool_log(session_id, tool_name, specialist, args, result, timestamp) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(self.session_id, "t1", "", "{}", '"OLD_RESULT"', self.old),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO tool_log(session_id, tool_name, specialist, args, result, timestamp) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(self.session_id, "t2", "", "{}", '"FRESH"', self.fresh),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO trace_event(session_id, trace_id, span_id, parent_span_id, event_type, payload, timestamp) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(self.session_id, "tr1", "sp1", "", "x", "{}", self.old),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO channel_outbound_message"
|
||||
"(id, tenant_id, channel, account_id, chat_id, text, status, source, created_at, error) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
("ob-old", "", "whatsapp", "wa", "c", "hi", "sent", "", self.old, ""),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO channel_outbound_message"
|
||||
"(id, tenant_id, channel, account_id, chat_id, text, status, source, created_at, error) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
("ob-pending", "", "whatsapp", "wa", "c", "hi", "pending", "", self.old, ""),
|
||||
)
|
||||
|
||||
def test_dry_run_then_apply(self) -> None:
|
||||
self._seed()
|
||||
plan = prune_sqlite_retention(self.store, keep_days=30, dry_run=True, now=self.now)
|
||||
self.assertTrue(plan.get("ok"))
|
||||
self.assertTrue(plan.get("dry_run"))
|
||||
self.assertGreaterEqual(int(plan.get("total_rows") or 0), 3)
|
||||
self.assertGreater(int((plan.get("targets") or {}).get("chat_message_tool", {}).get("rows") or 0), 0)
|
||||
|
||||
# Nothing deleted yet.
|
||||
with self.store._connect() as conn:
|
||||
n_tool = conn.execute(
|
||||
"SELECT COUNT(*) FROM chat_message WHERE role='tool'"
|
||||
).fetchone()[0]
|
||||
self.assertEqual(int(n_tool), 2)
|
||||
|
||||
out = prune_sqlite_retention(
|
||||
self.store,
|
||||
keep_days=30,
|
||||
dry_run=False,
|
||||
vacuum=False,
|
||||
now=self.now,
|
||||
)
|
||||
self.assertTrue(out.get("ok"))
|
||||
self.assertFalse(out.get("dry_run"))
|
||||
self.assertGreaterEqual(int(out.get("deleted_rows") or 0), 3)
|
||||
|
||||
with self.store._connect() as conn:
|
||||
roles = [
|
||||
r[0]
|
||||
for r in conn.execute("SELECT role FROM chat_message ORDER BY id").fetchall()
|
||||
]
|
||||
self.assertIn("assistant", roles)
|
||||
self.assertIn("tool", roles) # fresh tool kept
|
||||
self.assertEqual(roles.count("tool"), 1)
|
||||
tool_logs = conn.execute("SELECT COUNT(*) FROM tool_log").fetchone()[0]
|
||||
self.assertEqual(int(tool_logs), 1)
|
||||
traces = conn.execute("SELECT COUNT(*) FROM trace_event").fetchone()[0]
|
||||
self.assertEqual(int(traces), 0)
|
||||
outbound = {
|
||||
r[0]: r[1]
|
||||
for r in conn.execute(
|
||||
"SELECT id, status FROM channel_outbound_message"
|
||||
).fetchall()
|
||||
}
|
||||
self.assertNotIn("ob-old", outbound)
|
||||
self.assertIn("ob-pending", outbound)
|
||||
|
||||
def test_vacuum_shrinks_after_large_delete(self) -> None:
|
||||
self._seed()
|
||||
# Inflate then delete + vacuum.
|
||||
with self.store._connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO chat_message(session_id, role, content, timestamp) VALUES (?,?,?,?)",
|
||||
(self.session_id, "tool", "X" * 200_000, self.old),
|
||||
)
|
||||
before = self.db.stat().st_size
|
||||
out = prune_sqlite_retention(
|
||||
self.store,
|
||||
keep_days=30,
|
||||
dry_run=False,
|
||||
vacuum=True,
|
||||
now=self.now,
|
||||
)
|
||||
self.assertTrue(out.get("ok"))
|
||||
self.assertTrue((out.get("vacuum") or {}).get("ok"))
|
||||
after = self.db.stat().st_size
|
||||
self.assertLess(after, before)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue