v1.20.14 — Scroll-up history pagination + voice stale-routing fix
Highlights
🔊 Fix — Voice mode reads the Voice Comment LLM again on tool-using turns
The chat_voice_streamer introduced in v1.20.2 was being started prematurely on a stale router_decision SSE replayed from the previous turn's LangGraph checkpoint. If that prior turn was intention="conversation", the first router_decision of the new turn surfaced the stale "conversation" label at ~300 ms, kicking off direct-TTS of the displayed text — and an existing guard then blocked the Voice Comment LLM from taking over once the new router actually concluded intention="action". Reproduced from prod traces.
StreamingService now captures a stable signature of routing_history on the first values chunk and suppresses every router_decision SSE emission until the signature changes (i.e. until the current-turn router has appended a fresh RouterOutput). Symmetric to the existing _checkpoint_agent_results_ids guard for agent_results.
📜 Add — Scroll-up pagination on conversation history
GET /conversations/me/messages previously returned the 50 newest messages, full stop — no pagination, no scroll-back, no cursor. For conversations at the multi-million-token scale, the start of the discussion was unreachable even though every row was preserved in conversation_messages (v2 compaction is checkpoint-only — never deletes messages).
The endpoint now accepts ?before=<ISO datetime> and replies with has_more and next_cursor. Internally the service asks the repository for limit + 1 rows so has_more is derived without a COUNT(*). The existing composite index (conversation_id, created_at DESC) makes it an index-only seek per page regardless of conversation length — no migration needed.
The chat UI binds an IntersectionObserver on a 1-px sentinel above the first message; older pages prepend with id-based dedup, and a shared wasPrependRef flag makes the auto-scroll-to-bottom useEffect skip itself for that cycle. The viewport stays anchored exactly where the user was reading — zero visible jump.
⚙️ Configurable bounds
Pagination defaults (50 / 200 hard cap) moved to env vars:
| Env var | Default |
|---|---|
CONVERSATION_HISTORY_DEFAULT_LIMIT |
50 |
CONVERSATION_HISTORY_MAX_LIMIT |
200 |
Files & quality
- Backend —
apps/api/src/{core/{constants,config/advanced},domains/conversations/{repository,service,router,schemas},domains/agents/services/streaming/service}.py - Frontend —
apps/web/src/{hooks/useConversation,components/chat/ChatMessageList,app/[lng]/dashboard/chat/page,components/faq/FAQContent}.{ts,tsx}(FAQ changelogVersionKeys patched to enroll the four most recent v1_20_11..v1_20_14 translations that had been added but never displayed) - Tests — 4 backend router unit tests + 3 repo integration tests; voice-fix regression test (
test_process_values_chunk_suppresses_stale_routing_from_checkpoint); frontend hook test (page order, before-forwarding, mutex, first page omits before) - i18n (6 languages) —
chat.loading_older_messagesnew key, FAQ chat q12 nuanced + new q13 dedicated to scroll-up + count 12→13. FAQ changelogv1_20_14entry.how.{en,fr,de,es,it,zh}.mdsection 23.8 retitled and extended with the scroll-up bullet (showcase tone, integrated naturally — not a changelog) - Docs —
docs/technical/CONVERSATION_HISTORY_PAGINATION.md(new),docs/INDEX.mdcross-reference,docs/knowledge/02_chat.md("last 50" limitation nuanced + new section), README.md (Scroll-up History Pagination bullet next to Intelligent Context Compaction)
Ruff / Black / MyPy clean (858 source files); ESLint / tsc --noEmit clean; 82 vitest green; 8306 fast-unit pytest tests green; i18n parity verified across the 6 locales (4526 keys).
No DB migration, no new dependency, strictly additive on the API contract (older clients ignore has_more / next_cursor).