oclaw/runtime/operations/scripts/clear_all_chat_sessions.py
oliver d14e9d3596 feat(persistence): PostgreSQL assistant store, chat persist fixes, gateway scripts
- Add SQLAlchemy Core repos, pg adapter/compat, assistant_store factory, Alembic bootstrap and migration/cutover scripts.

- Harden chat_message writes (NUL scrub for PG), turn_uuid on attempt failure, WS turn_runner fallbacks and gateway executed_turn_uuid init.

- start_gateway: log paths, PS7 stderr handling via cmd, background stdout/stderr redirect; runtime assistant_runtime_log_dir export.

- Ops: clear_all_chat_sessions with PG-only --postgresql and env-gated wipe; clear_postgres_chat_sessions.ps1.

- Tests: SA repos, pg compat, persist fallback, smoke env isolation; CI and docs touch-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:33:50 +08:00

147 lines
4.7 KiB
Python

"""Delete every row in ``chat_session`` (and session-bound helper rows).
Uses the same assistant store as the gateway (SQLite or PostgreSQL per env).
Requires ``--yes`` **and** environment ``AIA_CONFIRM_CHAT_SESSION_WIPE=1`` to avoid accidental wipes.
PostgreSQL (force, avoids wiping SQLite by mistake)::
set AIA_CONFIRM_CHAT_SESSION_WIPE=1
python runtime/operations/scripts/clear_all_chat_sessions.py --yes --postgresql
Or use ``runtime/operations/scripts/clear_postgres_chat_sessions.ps1`` (loads ``_local/system.env`` then runs the above).
"""
from __future__ import annotations
import argparse
import os
import sys
from typing import Any
def _exec(store: Any, sql: str) -> None:
with store._connect() as conn:
conn.execute(sql)
def _try_exec(store: Any, sql: str) -> bool:
try:
_exec(store, sql)
return True
except Exception:
return False
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument(
"--yes",
action="store_true",
help="Confirm destructive delete of all chat sessions.",
)
p.add_argument(
"--dry-run",
action="store_true",
help="Only print how many sessions exist; do not delete.",
)
p.add_argument(
"--postgresql",
action="store_true",
help="After loading env, force AIA_ASSISTANT_DB_BACKEND=postgresql and abort unless the store is PG.",
)
args = p.parse_args()
if not args.yes and not args.dry_run:
print("Refusing to run without --yes (or use --dry-run to count only).", file=sys.stderr)
return 2
if args.yes and not args.dry_run and str(os.getenv("AIA_CONFIRM_CHAT_SESSION_WIPE") or "").strip().lower() not in {
"1",
"true",
"yes",
"on",
}:
print(
"Refusing destructive wipe: set environment AIA_CONFIRM_CHAT_SESSION_WIPE=1 together with --yes.",
file=sys.stderr,
)
return 2
try:
from interfaces.http.fastapi_app import load_system_env
load_system_env()
except Exception:
pass
if args.postgresql:
os.environ["AIA_ASSISTANT_DB_BACKEND"] = "postgresql"
from svc.persistence.assistant_store import get_assistant_store, reset_assistant_store_singleton
if args.postgresql:
reset_assistant_store_singleton()
store = get_assistant_store()
if args.postgresql and not bool(getattr(store, "_use_pg", False)):
print(
"error: --postgresql was set but assistant store is not PostgreSQL "
"(check AIA_ASSISTANT_DATABASE_URL / OPS_ASSISTANT_DATABASE_URL).",
file=sys.stderr,
)
return 2
n0 = int(store.count_sessions() or 0)
print(f"session_count_before={n0}")
if args.dry_run:
return 0
if n0 <= 0:
print("nothing_to_do")
return 0
# Rows that reference sessions but are not always ON DELETE CASCADE across backends.
for sql in (
"DELETE FROM trace_event",
"DELETE FROM agent_eval_log",
"DELETE FROM oclaw_attempt",
"DELETE FROM oclaw_run",
"DELETE FROM oclaw_task",
"DELETE FROM memory_vector WHERE memory_id IN (SELECT memory_id FROM memory_item WHERE session_id IN (SELECT id FROM chat_session))",
"DELETE FROM memory_item WHERE session_id IN (SELECT id FROM chat_session)",
"DELETE FROM memory_hit_log WHERE session_id IN (SELECT id FROM chat_session)",
):
if _try_exec(store, sql):
print(f"ok_stmt={sql[:72]}...")
else:
print(f"skip_stmt={sql[:72]}...")
deleted_bulk = 0
try:
with store._connect() as conn:
cur = conn.execute("DELETE FROM chat_session")
deleted_bulk = int(getattr(cur, "rowcount", 0) or 0)
except Exception as exc:
print(f"bulk_delete_chat_session_failed={exc!r}; falling back to per-session delete")
batch = 0
while True:
rows = store.list_sessions(limit=400, offset=0)
if not rows:
break
for s in rows:
store.delete_session(str(s.id))
batch += 1
if batch > 1_000_000:
print("abort_loop_guard", file=sys.stderr)
return 1
deleted_bulk = batch
n1 = int(store.count_sessions() or 0)
print(f"deleted_sessions_bulk={deleted_bulk}")
print(f"session_count_after={n1}")
try:
store._chat_messages_repo().delete_messages_where_session_missing()
store._tool_log_queries_repo().delete_tool_logs_where_session_missing()
except Exception as exc:
print(f"orphan_cleanup_note={exc!r}")
return 0 if n1 == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())