S226 Handoff Vitto Chat Persistence

Documentation for Sessions

Handoff: Vitto Chat Persistence & Response Recovery

From: S226 (Sessions Security Unification) To: Next available session Priority: High — this is the original issue that triggered the S226 security work Date: 2026-03-16


The Original Problem

When a user sends a message to Vitto and something interrupts the frontend (page refresh, navigation, network hiccup), Vitto's response stalls permanently. The user sees a frozen "thinking" or partial response that never completes. Reloading the page doesn't fix it — the conversation is stuck.

This should never happen. Vitto's processing (thinking, emotions, tool calls, content generation) runs on the backend and should complete regardless of what the frontend does. The response should be waiting for the user when they come back.


What S226 Accomplished (Context for This Work)

S226 was a comprehensive security unification that locked down all 4 backends. While it didn't fix the persistence bug directly, it built the infrastructure that enables the fix:

S226 Deliverable How It Enables This Fix
UnifiedAuthGuard (APP_GUARD) Authenticated reconnections are now guaranteed
WsAuthGuard on ChatGateway Reconnected Socket.IO clients are properly authenticated
/internal/* routes with @ServiceOnly() Vitto can write to sessions without a frontend connection
Session ownership checks Resumed responses go to the correct user only
tenantId on all session schemas Multi-tenant scoping for session recovery queries
tenantSyncPlugin Ensures tenantId consistency on all writes

Key files created/modified in S226:

Design documents from S226:


Root Cause Analysis

The Vitto response pipeline assumes streaming is ephemeral — the SSE connection stays open, all events are delivered in real-time, and only the final message is persisted. This fails in 3 specific ways:

Failure 1: No SSE Reconnection

When the page refreshes, the EventSource connection drops. The frontend has no mechanism to:

Key file: libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.ts (lines 451-700)

Failure 2: Streaming Messages Excluded from Backend Sync

ChatBackendSyncService explicitly filters out streaming messages:

// chat-backend-sync.service.ts, line 156
const finalized = messages.filter(m => !m.isStreaming);

This means:

Key file: libs/libs-ai/chat-sync/src/lib/services/chat-backend-sync.service.ts (line 156)

Failure 3: No Task Resume Capability

The backend continues processing after the frontend disconnects — the task runs to completion. But:

Key files:


The Response Pipeline (Current Architecture)

User sends message
  └─ POST /engine/message → returns { taskId } immediately
       │
       ├─ Backend: vitto-engine.service.ts queues task
       │   └─ Task runs in background (LLM calls, tool execution, etc.)
       │       └─ Emits SSE events: content_delta, tool.starting, task.completed
       │
       └─ Frontend: opens EventSource to /engine/message/:taskId/stream
           └─ onmessage handler:
               ├─ Creates streaming message (isStreaming: true)
               ├─ Appends content deltas to message in-place
               ├─ Persists to IndexedDB (local only — NOT synced to MongoDB)
               └─ On "task.completed": finalizes message (isStreaming: false)
                   └─ NOW syncs to MongoDB via ChatBackendSyncService

The gap: Between "task queued" and "task.completed", ALL intermediate state lives only in the SSE connection. If that connection drops, the state is lost.


Proposed Solution Direction

The fix requires making the response pipeline backend-driven instead of frontend-dependent:

1. Backend-Side Event Persistence

Vitto should persist every SSE event to a durable store (MongoDB or Redis) as it emits them:

Task emits event → Write to event_log collection → THEN emit via SSE

This creates a replay-able event log per task. Even if no frontend is connected, the events are captured.

Candidate location: APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts — the emitTaskEvent() or equivalent method.

2. Task Status Query Endpoint

Add an endpoint for the frontend to check task status after reconnection:

GET /engine/task/:taskId/status → { status, lastEventIndex, events[] }

This lets the frontend ask "what happened while I was gone?" and get the missed events.

3. Frontend Reconnection Logic

On page load, VittoChatEngineService should:

  1. Check IndexedDB for any message with isStreaming: true
  2. If found, query GET /engine/task/:taskId/status
  3. If task is still running → reconnect EventSource with ?fromEvent=N (resume from last known event)
  4. If task completed → fetch final response and finalize the message
  5. If task failed → show error and clean up

4. SSE Resume Support (Last-Event-ID)

The SSE spec supports Last-Event-ID header for reconnection. The backend SSE endpoint should:

5. Sync Streaming Messages to Backend

Remove the isStreaming filter in ChatBackendSyncService or create a separate sync path for streaming state. The backend should know about in-progress conversations.


Key Files for Implementation

File Purpose What to Change
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts Task execution + SSE emission Add event persistence, replay support
APIs/backend-vitto-api/src/app/engine/vitto.controller.ts:3556 SSE endpoint Add Last-Event-ID support, task status endpoint
APIs/backend-vitto-api/src/app/mind/schemas/agent-session.schema.ts Task persistence Add streaming event log field
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.ts:451 Frontend EventSource Add reconnection logic, task status check on load
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-streaming.service.ts Streaming state Add recovery state management
libs/libs-ai/chat-sync/src/lib/services/chat-backend-sync.service.ts:156 Backend sync Remove/modify isStreaming filter
APIs/backend-ai-api/src/app/sessions/chat.gateway.ts Socket.IO Add missed-message replay on reconnection

Related Memory & Decisions

From MEMORY.md:

From S226 Decisions (MCP):

Existing Related Schemas:


Success Criteria

  1. User refreshes page mid-Vitto-response → response continues and appears when page reloads
  2. User navigates away and comes back → completed response is waiting
  3. Network hiccup drops SSE → frontend reconnects and gets missed events
  4. Backend crash during response → task recovery picks up where it left off
  5. Multiple browser tabs → each sees consistent state via MongoDB sync
  6. No data loss between SSE emission and MongoDB persistence

Estimated Scope

This is a medium-to-large feature touching both backend (vitto-api event persistence + replay) and frontend (reconnection logic + streaming state recovery). The S226 security infrastructure is a prerequisite and is complete.

Suggested phases:

  1. Backend event persistence — persist SSE events to MongoDB as they're emitted
  2. Task status endpoint — query endpoint for post-disconnect recovery
  3. Frontend reconnection — detect stale streams, query status, resume
  4. SSE Last-Event-ID support — native browser reconnection with event replay
  5. Streaming sync — modify ChatBackendSyncService to sync in-progress messages