perf(state): add messages(session_id, id) index for window/ordering queries

Every ORDER BY id query on the messages table sorted or scanned the
whole session: get_messages_around's window seek, latest_message_row_id
(LIMIT 1), and get_messages' full-load ordering all paid O(session
history) per call — hot mid-turn via session_search and reactions.
messages.id is an original column (INTEGER PRIMARY KEY AUTOINCREMENT),
so the index lives in SCHEMA_SQL next to idx_messages_session — no
legacy-column migration hazard (the kanban lesson from #28776 does not
apply).

Measured (real schema, one 20k-message session, median of 30):
get_messages_around 7.08 -> 0.22 ms (32x), latest_message_row_id
3.37 -> 0.011 ms (307x), get_messages full load 111.6 -> 98.6 ms
(1.13x — remaining cost is row deserialization, not the sort).
Window results byte-identical at probe points across the session.

Tests: VM-step pin (get_messages_around bounded work, calibrated
~12 vs ~855 handler calls, threshold 300 — fails without the index)
and window parity with/without the index. No EXPLAIN/plan text
(behavior contracts, AGENTS.md).
This commit is contained in:
spfcraze
2026-08-01 10:46:21 -04:00
committed by kshitij
parent 6e786e927f
commit 8f91e249e4
4 changed files with 45 additions and 0 deletions

View File

@@ -283,6 +283,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_source_id ON sessions(source, id);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
CREATE INDEX IF NOT EXISTS idx_compression_locks_expires ON compression_locks(expires_at);
CREATE INDEX IF NOT EXISTS idx_session_model_usage_session ON session_model_usage(session_id);
CREATE INDEX IF NOT EXISTS idx_session_model_usage_model ON session_model_usage(model);

View File

@@ -2698,6 +2698,48 @@ class TestGetMessagesPagination:
messages = db.get_messages("s1")
assert [m["content"] for m in messages] == [f"msg-{i}" for i in range(10)]
def test_window_query_bounded_work(self, db):
"""Perf contract: get_messages_around must seek by index, not scan
the session's whole message history. Measured behaviorally via
SQLite progress-handler steps (behavior contracts over snapshots,
AGENTS.md — no EXPLAIN text). Calibrated on this seed (3000
messages): indexed = ~12 handler calls, unindexed full-session
scan = ~855. Threshold 300: >25x headroom above the indexed path,
~3x below the scan. Same pattern as the loader call-count pins in
tests/tools/test_approval_config_readonly.py."""
self._seed(db, n=3000)
mid = db.get_messages("s1", limit=1, offset=1500)[0]["id"]
steps = [0]
def progress():
steps[0] += 1
return 0
db._conn.set_progress_handler(progress, 100)
try:
db.get_messages_around("s1", mid, window=20)
finally:
db._conn.set_progress_handler(None, 0)
assert steps[0] < 300, (
f"get_messages_around executed {steps[0]}x100 VM steps — the "
"session-history scan is back (idx_messages_session_id missing "
"or unused)")
def test_window_results_identical_with_and_without_index(self, db):
"""The index must not change results: identical windows at probe
points across the session, with and without it."""
self._seed(db, n=500)
ids = [m["id"] for m in db.get_messages("s1")]
probes = (ids[0], ids[len(ids) // 2], ids[-1])
with_index = [db.get_messages_around("s1", mid, window=5)
for mid in probes]
db._conn.execute("DROP INDEX idx_messages_session_id")
without_index = [db.get_messages_around("s1", mid, window=5)
for mid in probes]
assert with_index == without_index
def test_limit_pages_in_insertion_order(self, db):
self._seed(db)
page1 = db.get_messages("s1", limit=4, offset=0)

View File

@@ -110,6 +110,7 @@ CREATE TABLE IF NOT EXISTS messages (
);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
```
Notes:

View File

@@ -90,6 +90,7 @@ CREATE TABLE IF NOT EXISTS messages (
);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
```
说明: