8.5 KiB
Phase 103 — Onboarding chips suggest only session-opening questions, never follow-ups
Source: Owner request (chat, 2026-09-12) — "The suggested questions on the chat page should not include follow-up questions from a session. The problem is this: Users will ask 'What are the correct arguments for qwen 3.8 27b on llama.cpp?' and BOR will answer. Then, users will ask 'What about qwen 3.6 35b?'. That second question … shows up as a suggested question to start a conversation but that question makes no sense without the context behind it. The only questions that should show up as a suggested question are those at the very beginning of a session."
Story: n/a (owner request — tightens the onboarding-chips contract of 80_history_suggestion_chips; the story file suggestion-chips.md cited by phases 05/80 no longer exists in .agents/user_stories/).
Context: app/api/suggestions.py — last_questions(db, limit=3) walks each saved chat's messages in REVERSE (newest-first) and collects every user question (the contract this phase replaces); GET /api/suggestions (the require_user-gated endpoint, phase 79) with the seed fallback (get_settings().suggestions — BOR_SUGGESTIONS / built-in) when the walk yields nothing. app/models.py — SavedChat.messages is the raw bor.chat.v1 JSONB list in CONVERSATIONAL order (oldest→newest); SavedChat.title is the first user question truncated to 120 chars + whitespace-collapsed at save time (app/api/chats.py _auto_title, _AUTO_TITLE_MAX = 120) and user-editable on re-Save. tests/integration/test_suggestions_api.py — the phase-80 state matrix (REWRITTEN in task 01). tests/e2e/test_suggestion_chips.py — the phase-80 story suite (REWRITTEN in task 03, the phase-76/80 precedent). Docs carrying the "last 3 questions asked" wording: app/config.py ~L373-377 (the suggestions seed docstring), .env.example ~L60 (the BOR_SUGGESTIONS comment), README.md ~L74.
Objective
A suggested question must make sense on its own: the onboarding chips are the first user question of each saved chat (the session's opening question) — follow-up questions ("What about …?") can never appear, because they are unanswerable without the session behind them. Walk order, dedup, the cap of 3, and the seed fallback are unchanged.
Owner decisions (chat, 2026-09-12 — recorded per AGENTS.md rule 3)
- A1 — openers only: "The only questions that should show up as a suggested question are those at the very beginning of a session." Each saved chat contributes AT MOST ONE chip: its first user question. In the owner's example, "What are the correct arguments for qwen 3.8 27b on llama.cpp?" (the opener) may chip; "What about qwen 3.6 35b?" (the follow-up) may not.
- A2 — everything else unchanged: chats still walked newest-
updated_atfirst (created_attiebreak), EXACT (case-sensitive) de-dup, cap 3 applied AFTER dedup, seed fallback when the walk yields zero openers (BOR_SUGGESTIONSoverride or built-in default) — all phase-80 contracts survive. The deflection "Maybe try" chips (app/rag/suggestions.pyderive_suggestions, carried in the chat response) are a separate contract and untouched. The FRONTEND is untouched — the chip row renders whatever the endpoint returns (chip sizing/truncation is phase 104's job). - A3 — the defensive opener rule: a chat's opener is its first
who == "user"message whose trimmedtextis non-blank. A LEADING blank user entry (the UI cannot produce one —handleSendtrims and guards!text) does not disqualify the chat; a record with no non-blank user message (brain-only, or blank-user-only) contributes nothing. - A4 — read
messages, nottitle:SavedChat.titleis truncated to 120 chars + whitespace-collapsed at save time and is user-editable on re-Save — the chips must carry the EXACT full opener text from the rawbor.chat.v1record (the phase-80 precedent: no SQL JSON ops, the deserialized list).
Design (shared by all tasks — the executor reads this, not the chat)
app/api/suggestions.py— the ONLY file changed inapp/:last_questionsis RENAMEDopening_questions(the old name would lie about the semantics; the helper is module-internal — its only caller is the endpoint, the tests hit the endpoint). Signature unchanged:(db: Session, limit: int = 3) -> list[str].- The new walk: for each chat in
updated_at DESC, created_at DESCorder, walkchat.messages or []FORWARD (oldest→newest —bor.chat.v1conversational order), take the first entry withwho == "user"whose trimmedtextis non-blank (the opener, per A3); if found and not alreadyseen(exact, case-sensitive), append it; stop oncelimitUNIQUE openers are collected. Result in encounter order (newest chat first). No other endpoint change:qs = opening_questions(db)→SuggestionList(suggestions=qs if qs else get_settings().suggestions). - Docstrings (the house dense-docstring style): the module docstring's phase-80 paragraph becomes the opener contract — WHY follow-ups are excluded (a follow-up like "What about X?" is meaningless as a conversation starter — the owner's llama.cpp/qwen example); the function docstring documents the forward walk, the A3 rule, the A4 why-not-title note, and that dedup/cap/order are the phase-80 contracts; the endpoint docstring says "the opening questions of the 3 most recent saved chats — or, before any question has ever been saved, the seed list".
- Not touched: schemas, models, migrations,
app/rag/suggestions.py(deflection), all offrontend/(the chips render the endpoint's list — truncation of long chips is phase 104), the auth gate. - Docs (task 02):
app/config.pyseed docstring,.env.exampleBOR_SUGGESTIONScomment,README.mdchat-features line — "the last 3 questions asked" → "the opening questions of the 3 most recent saved chats (the session openers, newest first)".
Dependencies
80_history_suggestion_chips(complete) — the endpoint, the seed fallback, the dedup/cap/order contracts, the story E2E suite this phase rewrites.79_api_tokens(complete) — therequire_usergate; the tests sign in first (unchanged).102_extensionless_filenames(todo) — queue order only (numeric); no code dependency (different subsystem).
Tasks
01_opener_extraction.md— theopening_questionsrewrite (rename + forward walk + docstrings) + the integration matrix rewrite.02_openers_docs.md— the "last 3 questions" → "session openers" wording in config /.env.example/ README.03_e2e_suite_commit.md— the story-suite rewrite to the opener semantics + regression E2Es + full gate + atomic commit.
Testing & Quality
- Integration — REWRITTEN
tests/integration/test_suggestions_api.py: the full opener matrix (one chat's follow-ups never surface; the cap now binds ACROSS chats; opener dedup; case variants; the A3 leading-blank rule; brain-only → seed; auth 401 — full detail in task 01). - E2E — REWRITTEN
tests/e2e/test_suggestion_chips.py(the phase-76/80 precedent: a semantic change rewrites the story suite in place), run in isolation: the opener-only core state (a 3-turn chat yields EXACTLY its opener as the single chip), the three-openers state, partial, seed, refetch-on-New-chat, plus the carried-over phase-05 behavior (one-tap submit, keyboard walk, the mobile single horizontal-scroll row). - Coverage: >90% on
app/(uv run pytest --cov=app --cov-report=term-missing) — the changed file isapp/api/suggestions.py, every branch of the new walk covered by the matrix.
Completion Criteria
- One saved chat with 4 user questions → the chip row holds EXACTLY its first question; none of the 3 follow-ups appears (integration + E2E pins).
- Four saved chats (each multi-turn) → exactly the 3 newest chats' OPENERS; the oldest opener is dropped by the cap; no follow-up text anywhere.
- Seed fallback, dedup, case-variant, partial (2 chips), brain-only/blank (A3), and 401 pins all green.
uv run pytest tests/e2e/test_suggestion_chips.py -v --no-covgreen in isolation;test_responsive_polish.py+test_chat_persistence.pygreen in isolation (regressions).- The deflection "Maybe try" chips are UNCHANGED (
derive_suggestions+ its suites green). uv run pytestgreen; coverage >90%;uv run ruff check . && uv run pyrightclean.- One atomic
--no-gpg-signConventional-Commits commit (e.g.fix(chat): onboarding chips are the session-opening questions, never follow-ups); phase dir moved to.agents/phases/complete/.