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>
This commit is contained in:
oliver 2026-05-14 09:33:50 +08:00
parent 2b32d11f43
commit d14e9d3596
103 changed files with 7574 additions and 1641 deletions

View file

@ -0,0 +1,121 @@
"""SA migration: tool_log MCP queries + trace_event writes/lists."""
from __future__ import annotations
import pytest
from svc.persistence.db.engine import clear_assistant_engine_cache
from svc.persistence.sqlite_store import SqliteStore
@pytest.fixture
def fresh_sqlite_store(monkeypatch: pytest.MonkeyPatch, tmp_path):
monkeypatch.delenv("AIA_ASSISTANT_DB_BACKEND", raising=False)
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
dbfile = tmp_path / "sa_tl_tr.sqlite"
monkeypatch.setenv("AIA_ASSISTANT_DB_PATH", str(dbfile))
clear_assistant_engine_cache()
s = SqliteStore(str(dbfile))
try:
yield s
finally:
clear_assistant_engine_cache()
def test_sa_mcp_tool_summaries_and_call_logs(fresh_sqlite_store: SqliteStore) -> None:
s = fresh_sqlite_store
sess = s.create_session("MCP2")
s.add_tool_log(sess.id, "mcp__srv99__x", {}, {"ok": True}, specialist="sp1")
s.add_tool_log(sess.id, "mcp__srv99__x", {}, {"ok": True}, specialist="sp1")
agg = s.list_mcp_tool_aggregate_usage()
assert "mcp__srv99__x" in agg
assert agg["mcp__srv99__x"]["count"] == 2
summ = s.list_mcp_tool_usage_summary(limit=50)
hit = next(x for x in summ if x["tool_name"] == "mcp__srv99__x")
assert hit["count"] == 2
assert hit["server_id"] == "srv99"
logs = s.list_mcp_tool_call_logs(server_id="srv99", limit=10)
assert len(logs) == 2
assert logs[0]["session_id"] == sess.id
def test_sa_trace_roundtrip_batch_and_window(fresh_sqlite_store: SqliteStore) -> None:
s = fresh_sqlite_store
sess = s.create_session("TR")
s.add_trace_event(
session_id=sess.id,
trace_id="t1",
span_id="s0",
parent_span_id=None,
event_type="noise",
payload={"a": 1},
)
s.add_trace_events_batch(
[
{
"session_id": sess.id,
"trace_id": "t1",
"span_id": "s1",
"parent_span_id": None,
"event_type": "turn_started",
"payload": {},
},
{
"session_id": sess.id,
"trace_id": "t1",
"span_id": "s2",
"parent_span_id": None,
"event_type": "turn_finished",
"payload": {},
},
]
)
desc = s.list_trace_events(session_id=sess.id, limit=10)
assert len(desc) == 3
asc_rows = s.list_trace_events_for_trace(session_id=sess.id, trace_id="t1", limit=50)
assert len(asc_rows) == 3
assert asc_rows[0]["event_type"] == "noise"
st, en = s.get_turn_time_window(session_id=sess.id, trace_id="t1")
assert st is not None and en is not None
def test_sa_add_tool_log_get_tool_logs(fresh_sqlite_store: SqliteStore) -> None:
s = fresh_sqlite_store
sess = s.create_session("TL")
s.add_tool_log(sess.id, "my_tool", {"a": 1}, {"ok": True}, specialist="spec", duration_ms=42)
logs = s.get_tool_logs(sess.id, limit=10)
assert len(logs) == 1
assert logs[0]["tool_name"] == "my_tool"
assert logs[0]["specialist"] == "spec"
assert logs[0]["args"] == {"a": 1}
assert logs[0]["result"] == {"ok": True}
assert logs[0]["duration_ms"] == 42
def test_sa_list_messages_in_time_window(fresh_sqlite_store: SqliteStore) -> None:
s = fresh_sqlite_store
sess = s.create_session("Win")
t0 = "2024-01-01T10:00:00+00:00"
t1 = "2024-01-01T10:05:00+00:00"
t2 = "2024-01-01T10:10:00+00:00"
s.add_message(sess.id, "user", "a", timestamp=t0)
s.add_message(sess.id, "user", "b", timestamp=t1)
s.add_message(sess.id, "user", "c", timestamp=t2)
rows = s.list_messages_in_time_window(session_id=sess.id, start_ts=t0, end_ts=t1, limit=50)
assert len(rows) == 2
assert [x["content"] for x in rows] == ["a", "b"]
def test_sa_move_tool_logs_to_session(fresh_sqlite_store: SqliteStore) -> None:
s = fresh_sqlite_store
src = s.create_session("SrcS")
dst = s.create_session("DstS")
s.add_tool_log(src.id, "t_move", {}, {"ok": 1})
n = s.move_tool_logs_to_session(from_session_id=src.id, to_session_id=dst.id)
assert n == 1
assert s.get_tool_logs(src.id, limit=10) == []
logs = s.get_tool_logs(dst.id, limit=10)
assert len(logs) == 1
assert logs[0]["tool_name"] == "t_move"
assert s.move_tool_logs_to_session(from_session_id=src.id, to_session_id=dst.id) == 0
assert s.move_tool_logs_to_session(from_session_id="", to_session_id=dst.id) == 0