fix(api): add pagination to GET /api/sessions/{id}/messages

The session messages endpoint returned ALL messages in a single
response with no limit/offset. Sessions with 500+ messages produced
1.2-1.6 MB JSON payloads, causing GIL starvation and WebSocket
timeouts on the Desktop client (#60155).

Add optional limit/offset query params to both the API endpoint and
SessionDB.get_messages(). Limit clamped to 500 max per page. Response
now includes a pagination object with limit/offset/returned count.

Backward compatible: callers that omit limit get the old behavior
(all messages).

Closes #60155
This commit is contained in:
mahdiwafy 2026-07-07 23:01:58 +07:00
parent 009b42d008
commit d58396b154
2 changed files with 36 additions and 9 deletions

View file

@ -8121,15 +8121,30 @@ async def get_session_latest_descendant(session_id: str):
}
@app.get("/api/sessions/{session_id}/messages")
async def get_session_messages(session_id: str, profile: Optional[str] = None):
async def get_session_messages(
session_id: str,
profile: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
):
db = _open_session_db_for_profile(profile)
try:
sid = db.resolve_session_id(session_id)
if not sid:
raise HTTPException(status_code=404, detail="Session not found")
sid = db.resolve_resume_session_id(sid)
messages = db.get_messages(sid)
return {"session_id": sid, "messages": messages}
# Clamp limit to prevent abuse (max 500 per page)
_limit = min(limit, 500) if limit is not None else None
messages = db.get_messages(sid, limit=_limit, offset=offset)
return {
"session_id": sid,
"messages": messages,
"pagination": {
"limit": _limit,
"offset": offset,
"returned": len(messages),
},
}
finally:
db.close()

View file

@ -3710,7 +3710,11 @@ class SessionDB:
def get_messages(
self, session_id: str, include_inactive: bool = False
self,
session_id: str,
include_inactive: bool = False,
limit: Optional[int] = None,
offset: int = 0,
) -> List[Dict[str, Any]]:
"""Load messages for a session in insertion order.
@ -3721,14 +3725,22 @@ class SessionDB:
Ordered by AUTOINCREMENT id (true insertion order) rather than
timestamp see c03acca50 for the WSL2 clock-regression rationale.
When ``limit`` is provided, returns at most ``limit`` messages
starting from ``offset`` (0-based, in insertion order). Enables
pagination for the API endpoint to avoid loading entire transcripts.
"""
active_clause = "" if include_inactive else " AND active = 1"
sql = (
"SELECT * FROM messages WHERE session_id = ?"
f"{active_clause} ORDER BY id"
)
params: list = [session_id]
if limit is not None:
sql += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
with self._lock:
cursor = self._conn.execute(
"SELECT * FROM messages WHERE session_id = ?"
f"{active_clause} ORDER BY id",
(session_id,),
)
cursor = self._conn.execute(sql, params)
rows = cursor.fetchall()
result = []
for row in rows: