fix(monitoring): persist session summaries to avoid history scans

- Maintain counts and display snapshots in source transactions with replay-safe deduplication.
- Read session lists exclusively from summaries and remove execution-only filters.
- Automatically initialize historical summaries in bounded, resumable batches during OSS startup.
- Add behavioral contracts covering accounting, retention, query cost and PostgreSQL concurrency.

Verified with the required session_summary suite and real container upgrade startup.
This commit is contained in:
BiFangKNT
2026-10-20 06:12:00 +08:00
parent 62b647761f
commit 810d053df9
33 changed files with 4227 additions and 466 deletions
@@ -224,7 +224,7 @@ def _query(context, sender_id):
@pytest.mark.parametrize('user_id', [123456789, -100123456789, 0, None, '', '00123', ' opaque用户 '])
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
@pytest.mark.parametrize('record_type', ['message', 'feedback'])
async def test_optional_user_ids_round_trip_through_asyncpg(service, user_id, record_type):
context = _context(WORKSPACE_A)
expected = str(user_id) if isinstance(user_id, int) else user_id
@@ -238,8 +238,6 @@ async def test_optional_user_ids_round_trip_through_asyncpg(service, user_id, re
)
details = await _read(service, 'get_message_details', context, record_id)
assert details['message']['user_id'] == expected
elif record_type == 'session':
await service.record_session_start(context, **RESOURCE, session_id='same-session', user_id=user_id)
rows, total = await _read(service, 'get_sessions', context)
assert total == 1
assert rows[0]['user_id'] == expected
@@ -302,7 +300,7 @@ async def test_query_error_persists_error_message_and_linked_log(service, user_i
@pytest.mark.parametrize('user_id', [True, 1.5, b'123', ['123']])
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
@pytest.mark.parametrize('record_type', ['message', 'feedback'])
async def test_unsupported_user_ids_fail_at_the_write_boundary(service, user_id, record_type):
context = _context(WORKSPACE_A)
with pytest.raises(TypeError, match='user_id must be a string, integer, or None'):
@@ -314,8 +312,6 @@ async def test_unsupported_user_ids_fail_at_the_write_boundary(service, user_id,
session_id='session',
user_id=user_id,
)
elif record_type == 'session':
await service.record_session_start(context, **RESOURCE, session_id='session', user_id=user_id)
else:
await service.record_feedback(context, feedback_id='feedback', feedback_type=1, user_id=user_id)
async with service.ap.persistence_mgr.tenant_scope(WORKSPACE_A):
@@ -326,7 +322,6 @@ async def test_unsupported_user_ids_fail_at_the_write_boundary(service, user_id,
async def test_session_analysis_aggregates_under_cloud_sql_guard(service):
context = _context(WORKSPACE_A)
await service.record_session_start(context, **RESOURCE, session_id='same-session')
await service.record_message(context, **RESOURCE, session_id='same-session', message_content='hello')
result = await _read(service, 'get_session_analysis', context, 'same-session')
assert result['found'] is True
@@ -107,7 +107,10 @@ async def test_legacy_sqlite_resources_are_backfilled_and_contracted(tmp_path):
)
assert (
await conn.scalar(
sa.text('SELECT COUNT(*) FROM workspace_metadata WHERE workspace_uuid = :workspace_uuid'),
sa.text(
'SELECT COUNT(*) FROM workspace_metadata WHERE workspace_uuid = :workspace_uuid '
"AND key IN ('wizard_status', 'wizard_progress', 'rag_plugin_migration_needed')"
),
{'workspace_uuid': workspace_uuid},
)
== 3
@@ -296,7 +299,15 @@ async def test_fresh_sqlite_schema_matches_resource_tenancy_contract(tmp_path):
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "fresh-resources.db"}')
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Match PersistenceManager: 0040 creates the receipt FK after older PK migrations.
await conn.run_sync(
lambda sync: Base.metadata.create_all(
sync,
tables=[
table for table in Base.metadata.sorted_tables if table.name != 'monitoring_session_receipts'
],
)
)
await run_alembic_stamp(engine, '0001_baseline')
await run_alembic_upgrade(engine, 'head')
@@ -312,3 +323,76 @@ async def test_fresh_sqlite_schema_matches_resource_tenancy_contract(tmp_path):
assert columns['legacy_vector_collection']['nullable'] is False
finally:
await engine.dispose()
@pytest.mark.session_summary
@pytest.mark.contract('summary_schema')
@pytest.mark.parametrize('has_history', [False, True])
async def test_legacy_sqlite_without_monitoring_tables_initializes_summaries(tmp_path, monkeypatch, has_history):
import datetime
import logging
from types import SimpleNamespace
from langbot.pkg.api.http.context import ExecutionContext
from langbot.pkg.api.http.service.monitoring import MonitoringService
from langbot.pkg.entity.persistence.event_log import EventLog
from langbot.pkg.persistence.mgr import PersistenceManager
from langbot.pkg.persistence.session_summary_backfill import SessionSummaryBackfill, require_initialized
from langbot.pkg.utils import constants
instance = str(uuid.uuid4())
monkeypatch.setattr(constants, 'instance_id', instance)
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "legacy-without-monitoring.db"}')
try:
await create_legacy_resource_schema(engine, instance_uuid=instance)
async with engine.begin() as conn:
for name in TENANT_TABLES:
if name.startswith('monitoring_'):
await conn.exec_driver_sql(f'DROP TABLE {name}')
if has_history:
await conn.run_sync(EventLog.__table__.create)
await conn.execute(
sa.insert(EventLog).values(
event_id='legacy-event',
event_type='message.received',
source='platform',
workspace_id=instance,
bot_id='bot-1',
conversation_id='group_legacy',
created_at=datetime.datetime(2026, 1, 1),
)
)
await run_alembic_stamp(engine, '0008_mcp_resource_prefs')
ap = SimpleNamespace(logger=logging.getLogger(__name__))
ap.persistence_mgr = PersistenceManager(ap)
ap.persistence_mgr.db = SimpleNamespace(get_engine=lambda: engine)
await ap.persistence_mgr._initialize_managed_schema()
await ap.persistence_mgr._enable_sqlite_foreign_keys_after_migration()
tables = await _inspect(engine, lambda conn: set(sa.inspect(conn).get_table_names()))
assert {'monitoring_sessions', 'monitoring_session_receipts'} <= tables
service = MonitoringService(ap)
if has_history:
with pytest.raises(RuntimeError, match='offline initialization'):
async with service._summary_session(instance) as session:
await require_initialized(session, instance)
worker = SessionSummaryBackfill(service._summary_session, 'sqlite')
while not await worker.run_batch(instance):
pass
ctx = ExecutionContext(instance_uuid=instance, workspace_uuid=instance, placement_generation=1)
await service.record_message(
ctx,
bot_id='bot-1',
bot_name='Bot',
pipeline_id='pipeline-1',
pipeline_name='Pipeline',
session_id='group_live',
message_content='hello',
)
await ap.persistence_mgr._initialize_managed_schema()
rows, total = await service.get_sessions(ctx)
assert total == 1 + int(has_history)
assert all(row['message_count'] == 1 for row in rows)
async with engine.connect() as conn:
assert (await conn.exec_driver_sql('PRAGMA foreign_key_check')).all() == []
finally:
await engine.dispose()