Skip to content

Chat backend: streaming answers, cited passages, renaming threads - #40

Merged
Badri-Rajendran merged 12 commits into
mainfrom
feature_chat_redesign_api
Sep 26, 2026
Merged

Badri-Rajendran merged 12 commits into
mainfrom
feature_chat_redesign_api

Conversation

@Badri-Rajendran

Copy link
Copy Markdown
Owner

Problem

The chat answers only after a long wait, with no sign of progress. Its citations are bare file names that can't be read, and threads can't be renamed. The approved chat redesign (spec: docs/superpowers/specs/2026-09-26-chat-redesign-design.md, mockups: docs/design/chat-redesign/) needs three backend capabilities. This PR is Part A of the plan (docs/superpowers/plans/2026-09-26-chat-redesign.md). The frontend follows in a second PR.

Implementation

  • Streaming answers: POST /api/chat/threads/<id>/messages/stream sends Server-Sent Events:
    • the events are user_message, stage, notice, delta, reset, done and error;
    • generation.answer is now a thin wrapper over an event generator (answer_events), so the CLI, the evals and the JSON route are unchanged;
    • text is streamed only once the answer is grounded, and a round that turns into a tool call sends reset;
    • done carries exactly what was saved;
    • every check that can fail runs first and returns JSON; the question is then committed before the model runs;
    • the stream uses its own database session, because the view's session is closed by teardown before the body runs;
    • if the client disconnects, the model stream closes and the spend is still recorded once.
  • Cited passages: GET /api/chat/sources/<id>:
    • a source is addressed only by the citation's own id, never by chunk id, and another user's citation returns 404;
    • the quote is at most 300 characters: the part of the passage most like the answer;
    • it names the source kind, title, document and section, and links only to safe https URLs;
    • Wikipedia text carries its CC BY-SA credit, read from the source registry;
    • each citation now stores a SHA-256 of the passage it used, so a rebuilt chunk shows as changed rather than silently different text. Older citations show as unverified.
  • Renaming threads: PATCH /api/chat/threads/<id> trims the title, requires 1–200 characters, and leaves updated_at alone, so the thread keeps its place in the list.
  • Shared send limit: both send routes share one 15-per-minute limit (scope="chat_send").
  • HTTP errors are JSON: 404, 405, 429 and the others now return {"error": …} instead of Werkzeug's HTML page. Their status codes and headers are kept.
  • Review fixes:
    • a streamed round that never reports usage (a disconnect, Stop, or a dropped connection) is now counted by a high estimate, so the daily budget can't be bypassed (OWASP LLM10);
    • spend is marked as recorded only after its commit succeeds.
  • ADR 0027 records all of this. It amends ADR 0007 (citation hashes) and ADR 0022 (each stream holds a gthread thread).

Validation

  • make check: 731 passed, 94.52% coverage (floor 85%). ruff clean, bandit no issues, pip-audit no known vulnerabilities, alembic check no new operations.
  • The migration round-trips (upgrade, downgrade, upgrade).
  • Each task was written test-first. The new test files are test_chat_stream.py, test_passages.py and test_api_errors.py, with additions to test_chat.py, test_generation.py and test_rate_limits.py.
  • An independent review found no critical issues. Its two important findings are fixed here, with tests.

Security, DB and API impact

  • DB: migration 9c1e4b7a2d10 adds message_sources.content_sha256 (nullable).
  • API:
    • three new endpoints, rate-limited at 15 per minute shared, 60 per minute and 30 per minute;
    • SourceResponse gains id;
    • error bodies are now JSON.
  • Security:
    • every new route requires a login and checks ownership (404, never 403);
    • SQL is parameterised only;
    • no question, answer or passage text, ZIP code, age or date of birth is logged;
    • no exception text reaches the client;
    • URLs are https only and pass unsafe_reason;
    • quotes stay brief, as ADR 0013 requires.

Follow-ups (not in this PR, by choice)

  • create_engine(hide_parameters=True), so SQL parameters (which can include answer text) never reach error logs.
  • Correct ADR 0027's timeout bound: the SDK's 30 s timeout applies between streamed chunks, not to a whole round. Either enforce a total deadline or reword the ADR.
  • Apply the rename title's 200-character limit after trimming, as the spec says. Today a padded title is refused.
  • Strengthen test_the_question_is_committed_before_the_model_is_called, which can't tell the explicit commit apart, and add a test that an error raised mid-iteration closes the model stream.

Copilot AI lite review requested due to automatic review settings September 26, 2026 23:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Badri-Rajendran
Badri-Rajendran merged commit 8dcf2fe into main Sep 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants