Skip to content

feat: moved chat persistance to Server Side#1341

Merged
MODSetter merged 2 commits intomainfrom
dev
May 4, 2026
Merged

feat: moved chat persistance to Server Side#1341
MODSetter merged 2 commits intomainfrom
dev

Conversation

@MODSetter
Copy link
Copy Markdown
Owner

@MODSetter MODSetter commented May 4, 2026

Description

Motivation and Context

FIX #

Screenshots

API Changes

  • This PR includes API changes

Change Type

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring
  • Documentation
  • Dependency/Build system
  • Breaking change
  • Other (specify):

Testing Performed

  • Tested locally
  • Manual/QA verification

Checklist

  • Follows project coding standards and conventions
  • Documentation updated as needed
  • Dependencies updated as needed
  • No lint/build errors or new warnings
  • All relevant tests are passing

High-level PR Summary

This PR moves chat message persistence from client-side round-trips to server-side writes within the streaming chat flow. The backend now persists both user and assistant messages immediately during the stream via new persist_user_turn, persist_assistant_shell, and finalize_assistant_turn helpers, emitting SSE events (data-user-message-id and data-assistant-message-id) so the frontend can rename optimistic placeholder IDs to canonical database IDs in real-time. Two database migrations add partial unique indexes on (thread_id, turn_id, role) and (message_id) to make concurrent writes idempotent and prevent duplicate rows when legacy frontend code races the new server-side persistence. A new AssistantContentBuilder mirrors the frontend's rich content projection server-side so the persisted JSONB matches the live-rendered UI. The legacy appendMessage route is converted to a recovery-only no-op that returns existing rows when the unique index fires, and comprehensive integration tests verify the cross-writer contract holds under race conditions.

⏱️ Estimated Review Time: 3+ hours

💡 Review Order Suggestion
Order File Path
1 surfsense_backend/alembic/versions/141_unique_chat_message_turn_role.py
2 surfsense_backend/alembic/versions/142_token_usage_message_id_unique.py
3 surfsense_backend/app/db.py
4 surfsense_backend/app/tasks/chat/persistence.py
5 surfsense_backend/app/tasks/chat/content_builder.py
6 surfsense_backend/app/tasks/chat/stream_new_chat.py
7 surfsense_backend/app/routes/new_chat_routes.py
8 surfsense_backend/app/schemas/new_chat.py
9 surfsense_web/lib/chat/stream-side-effects.ts
10 surfsense_web/lib/chat/streaming-state.ts
11 surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
12 surfsense_web/lib/chat/thread-persistence.ts
13 surfsense_backend/tests/integration/chat/test_persistence.py
14 surfsense_backend/tests/integration/chat/test_message_id_sse.py
15 surfsense_backend/tests/integration/chat/test_append_message_recovery.py
16 surfsense_backend/tests/unit/tasks/chat/test_content_builder.py
17 surfsense_backend/tests/unit/test_stream_new_chat_contract.py
18 surfsense_backend/tests/integration/chat/__init__.py
19 .vscode/launch.json

Need help? Join our Discord

Summary by CodeRabbit

  • New Features

    • Chat messages now persist server-side with SSE-based message ID confirmation for improved reliability and synchronization.
    • Enhanced handling of concurrent message writes with automatic recovery from duplicate attempts.
    • Richer metadata support for mentioned documents in chat conversations.
  • Deprecations

    • Legacy message append endpoint deprecated in favor of SSE-based message ID handshake.
  • Tests

    • Added comprehensive test coverage for message persistence and recovery flows.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
surf-sense-frontend Ready Ready Preview, Comment May 4, 2026 10:09am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10dc2dbc-7bd0-44ae-8ebf-570303bc4dbf

📥 Commits

Reviewing files that changed from the base of the PR and between 635866a and b5612f1.

📒 Files selected for processing (19)
  • .vscode/launch.json
  • surfsense_backend/alembic/versions/141_unique_chat_message_turn_role.py
  • surfsense_backend/alembic/versions/142_token_usage_message_id_unique.py
  • surfsense_backend/app/db.py
  • surfsense_backend/app/routes/new_chat_routes.py
  • surfsense_backend/app/schemas/new_chat.py
  • surfsense_backend/app/tasks/chat/content_builder.py
  • surfsense_backend/app/tasks/chat/persistence.py
  • surfsense_backend/app/tasks/chat/stream_new_chat.py
  • surfsense_backend/tests/integration/chat/__init__.py
  • surfsense_backend/tests/integration/chat/test_append_message_recovery.py
  • surfsense_backend/tests/integration/chat/test_message_id_sse.py
  • surfsense_backend/tests/integration/chat/test_persistence.py
  • surfsense_backend/tests/unit/tasks/chat/test_content_builder.py
  • surfsense_backend/tests/unit/test_stream_new_chat_contract.py
  • surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
  • surfsense_web/lib/chat/stream-side-effects.ts
  • surfsense_web/lib/chat/streaming-state.ts
  • surfsense_web/lib/chat/thread-persistence.ts

📝 Walkthrough

Walkthrough

This PR implements server-authoritative chat persistence with SSE-driven canonical message ID handshakes. It replaces frontend-side row persistence with idempotent database upserts keyed on partial unique indexes, introduces server-side content building to keep streamed state coherent with persistence, and adds SSE events to communicate canonical message IDs back to the frontend for optimistic-to-real ID renaming.

Changes

Server-Authoritative Persistence & SSE Message ID Handshake

Layer / File(s) Summary
Database Schema
surfsense_backend/alembic/versions/141_...py, surfsense_backend/alembic/versions/142_...py, surfsense_backend/app/db.py
Partial unique indexes enforce idempotency: NewChatMessage(thread_id, turn_id, role) WHERE turn_id IS NOT NULL prevents duplicate user/assistant rows; TokenUsage(message_id) WHERE message_id IS NOT NULL prevents duplicate token usage per message. Migration 141 creates the chat-message index; migration 142 deduplicates existing token-usage rows and creates its index.
Persistence Layer
surfsense_backend/app/tasks/chat/persistence.py
New module adds persist_user_turn(), persist_assistant_shell(), and finalize_assistant_turn() helpers using PostgreSQL INSERT ... ON CONFLICT DO NOTHING ... RETURNING pattern against partial unique indexes to achieve idempotent inserts; missing conflict returns use follow-up SELECT to recover existing row IDs. Finalization overwrites assistant content and persists token usage in a single atomic insert.
Content Builder
surfsense_backend/app/tasks/chat/content_builder.py
New AssistantContentBuilder class mirrors frontend ContentPart[] accumulation in memory via lifecycle handlers (text/reasoning/tool-input/tool-output deltas, thinking steps, step separators). Supports mid-turn interruption via mark_interrupted() and produces persistence-ready snapshots. Utility methods include is_empty(), stats() for telemetry.
Stream Orchestration
surfsense_backend/app/tasks/chat/stream_new_chat.py
Pre-persists user and assistant shell rows before streaming; emits user-message-id and assistant-message-id SSE events; drives AssistantContentBuilder lifecycle handlers at every streaming yield site (text/reasoning/tool deltas, thinking steps). Finalization on disconnect calls finalize_assistant_turn() with builder snapshot. Applied to both stream_new_chat and stream_resume_chat.
Request Schema & Route Integration
surfsense_backend/app/schemas/new_chat.py, surfsense_backend/app/routes/new_chat_routes.py
Added MentionedDocumentInfo schema and propagated mentioned_documents parameter through /new_chat, /regenerate, /resume. Route append_message refactored to use INSERT ... ON CONFLICT DO NOTHING for both user and assistant messages; replaced ORM-based token tracking with direct TokenUsage upsert keyed by message_id.
Frontend SSE Types & Parsing
surfsense_web/lib/chat/streaming-state.ts, surfsense_web/lib/chat/stream-side-effects.ts
Extended SSEEvent union with data-user-message-id and data-assistant-message-id event types carrying { message_id: number; turn_id: string }. New readStreamedMessageId() parser validates and returns { messageId, turnId } or null for malformed payloads.
Frontend SSE Integration
surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
Removed FE-side persistUserTurn / persistAssistantTurn persistence callbacks; changed userMsgId and assistantMsgId from const to mutable let. Added SSE handlers for data-user-message-id and data-assistant-message-id to rename optimistic IDs in-place and migrate dependent state (messageDocumentsMap, tokenUsageStore, pendingInterrupt references) to canonical ID keys. Applied to onNew, handleResume, and handleRegenerate flows.
Tests
surfsense_backend/tests/unit/tasks/chat/test_content_builder.py, surfsense_backend/tests/unit/test_stream_new_chat_contract.py, surfsense_backend/tests/integration/chat/test_persistence.py, surfsense_backend/tests/integration/chat/test_append_message_recovery.py, surfsense_backend/tests/integration/chat/test_message_id_sse.py
Unit tests cover content builder delta coalescing, reasoning/text ordering, tool-call assembly, thinking-step insertion, step-separator rules, interruption handling, snapshot isolation, and stats. Integration tests validate persist_user_turn / persist_assistant_shell idempotency and conflict recovery, finalize_assistant_turn content/token-usage persistence, cross-writer race scenarios (FE vs. server finalization order), SSE message-id byte shape, and canonical DB anchoring. Contract test updated to reflect server-authoritative persistence.

Debug Warnings Configuration

Layer / File(s) Summary
Launch Configuration
.vscode/launch.json
Added PYTHONWARNINGS environment variable setting ignore::UserWarning:pydantic.main to all FastAPI and Celery debug launch configurations to suppress noisy pydantic validation warnings during development.

Sequence Diagram

sequenceDiagram
    participant FE as Frontend
    participant Route as Backend Route
    participant Persist as Persistence Layer
    participant DB as Database
    participant Stream as Stream Orchestrator
    participant SSE as SSE Service

    FE->>Route: POST /new_chat (optimistic userMsgId, assistantMsgId)
    Route->>Persist: persist_user_turn()
    Persist->>DB: INSERT user row (ON CONFLICT DO NOTHING)
    DB-->>Persist: user message_id
    Route->>Persist: persist_assistant_shell()
    Persist->>DB: INSERT assistant shell (ON CONFLICT DO NOTHING)
    DB-->>Persist: assistant message_id
    Route->>Stream: stream_new_chat(...)
    Stream->>SSE: format_data("data-user-message-id", id)
    SSE-->>FE: SSE event
    FE->>FE: Rename optimistic userMsgId to canonical ID
    Stream->>SSE: format_data("data-assistant-message-id", id)
    SSE-->>FE: SSE event
    FE->>FE: Rename optimistic assistantMsgId to canonical ID
    
    par Streaming Content
        Stream->>Stream: on_text_start/delta/end
        Stream->>SSE: format_data("text-delta", ...)
        SSE-->>FE: Stream text content
    and Content Building
        Stream->>Stream: content_builder.on_text_start/delta/end
        Stream->>Stream: Accumulate ContentPart[]
    end
    
    Note over Stream,DB: On disconnect or completion
    Stream->>Stream: mark_interrupted() if needed
    Stream->>Persist: finalize_assistant_turn(snapshot)
    Persist->>DB: UPDATE assistant row + INSERT token_usage
    DB-->>Persist: Done
    Stream-->>FE: SSE close
    FE->>FE: Render canonical persisted content
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~105 minutes

Possibly related PRs

Poem

🐰 A rabbit hops where messages now bloom,
Server-side rooms, no FE gloom!
Idempotent hashes, content built true,
SSE whispers IDs fresh and new—
Content snapshots, race-safe and sound,
Persistence magic without a bound! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@MODSetter MODSetter merged commit 743eff4 into main May 4, 2026
4 of 11 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.

1 participant