feat: SQLite event storage with pagination and filtering

Implement SQLite-based event persistence as specified in sqlite-event-manager.md.

## Changes

### Backend
- **EventStorage** (`src/classes/event_storage.py`): New SQLite storage layer
  - Cursor-based pagination with compound cursor `{month_stamp}_{rowid}`
  - Avatar filtering (single and pair queries)
  - Major/minor event separation
  - Cleanup API with `keep_major` and `before_month_stamp` filters

- **EventManager** (`src/classes/event_manager.py`): Refactored to use SQLite
  - Delegates to EventStorage for persistence
  - Memory fallback mode for testing
  - New `get_events_paginated()` method

- **API** (`src/server/main.py`):
  - `GET /api/events` - Paginated event retrieval with filtering
  - `DELETE /api/events/cleanup` - User-triggered cleanup

### Frontend
- **EventPanel.vue**: Scroll-to-load pagination, dual-person filter UI
- **world.ts**: Event state management with pagination
- **game.ts**: New API client methods

### Testing
- 81 new tests for EventStorage, EventManager, and API
- Added `pytest-asyncio` and `httpx` to requirements.txt

## Known Issues: Save/Load is Currently Broken

After loading a saved game, the following issues occur:

1. **Wrong database used**: API returns events from the startup database instead
   of the loaded save's `_events.db` file
2. **Events from wrong time period**: Shows events from year 115 when loaded
   save is at year 114
3. **Pagination broken after load**: `has_more` returns `False` despite hundreds
   of events in the saved database
4. **Filter functionality broken**: Character selection filter stops working
   after loading a game

Root cause: `load_game.py` does not properly switch the EventManager's database
connection to the loaded save's events database.
This commit is contained in:
Zihao Xu
2026-01-07 00:40:34 -08:00
parent e4ff312f58
commit a1f08dd0ab
14 changed files with 2892 additions and 195 deletions

View File

@@ -66,6 +66,34 @@ export interface LLMConfigDTO {
mode: string;
}
// --- Events Pagination ---
export interface EventDTO {
id: string;
text: string;
content: string;
year: number;
month: number;
month_stamp: number;
related_avatar_ids: string[];
is_major: boolean;
is_story: boolean;
}
export interface EventsResponseDTO {
events: EventDTO[];
next_cursor: string | null;
has_more: boolean;
}
export interface FetchEventsParams {
avatar_id?: string;
avatar_id_1?: string;
avatar_id_2?: string;
cursor?: string;
limit?: number;
}
export const gameApi = {
// --- World State ---
@@ -165,5 +193,25 @@ export const gameApi = {
saveLLMConfig(config: LLMConfigDTO) {
return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config);
},
// --- Events Pagination ---
fetchEvents(params: FetchEventsParams = {}) {
const query = new URLSearchParams();
if (params.avatar_id) query.set('avatar_id', params.avatar_id);
if (params.avatar_id_1) query.set('avatar_id_1', params.avatar_id_1);
if (params.avatar_id_2) query.set('avatar_id_2', params.avatar_id_2);
if (params.cursor) query.set('cursor', params.cursor);
if (params.limit) query.set('limit', String(params.limit));
const qs = query.toString();
return httpClient.get<EventsResponseDTO>(`/api/events${qs ? '?' + qs : ''}`);
},
cleanupEvents(keepMajor = true, beforeMonthStamp?: number) {
const query = new URLSearchParams();
query.set('keep_major', String(keepMajor));
if (beforeMonthStamp !== undefined) query.set('before_month_stamp', String(beforeMonthStamp));
return httpClient.delete<{ deleted: number }>(`/api/events/cleanup?${query}`);
}
};