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:
libs/libs-api/auth-core/src/lib/guards/unified-auth.guard.ts— global deny-by-default guardlibs/libs-api/auth-core/src/lib/guards/ws-auth.guard.ts— WebSocket authAPIs/backend-ai-api/src/app/internal/internal-sessions.controller.ts— service-only session accessAPIs/backend-ai-api/src/app/sessions/chat.gateway.ts— now authenticates WS connectionsAPIs/backend-ai-api/src/app/sessions/sessions.controller.ts— ownership checks on all endpointslibs/libs-api/auth-core/src/lib/utils/tenant-sync.util.ts— tenant sync plugin
Design documents from S226:
docs/superpowers/specs/2026-03-16-sessions-security-unification-design.md— full security architecturedocs/superpowers/plans/2026-03-16-sessions-security-unification.md— implementation plan (22 tasks)docs/english/sessions/S226/S226-sessions-security-unification-progress.md— non-technical progress report
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:
- Detect a stale/dead EventSource
- Query the backend for task status
- Reconnect to the SSE stream for the same
taskId
Key file: libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.ts (lines 451-700)
- Line 452: Opens
new EventSource(url)— no reconnection logic - Line 461: Creates streaming message with ID
stream-${taskId} - Line 464:
onmessagehandler processes deltas but can't resume after disconnect
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:
- Partial/streaming messages exist ONLY in IndexedDB (browser storage)
- They never reach MongoDB (the persistent backend)
- On page refresh, IndexedDB shows the stale streaming message
- MongoDB has no record of the in-progress response
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:
- The final result is emitted to a dead SSE stream (no subscribers)
AgentSessionServicestores the final task result, but only as{ taskId, status, result }- There's no replay endpoint for missed SSE events
- The frontend can't query "what happened to taskId X after I disconnected?"
Key files:
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts(lines 172-246) — task execution continues after SSE disconnectAPIs/backend-vitto-api/src/app/mind/schemas/agent-session.schema.ts— stores final task state, not intermediate streaming
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:
- Check IndexedDB for any message with
isStreaming: true - If found, query
GET /engine/task/:taskId/status - If task is still running → reconnect EventSource with
?fromEvent=N(resume from last known event) - If task completed → fetch final response and finalize the message
- 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:
- Number each event (
id: N) - On reconnection with
Last-Event-ID, replay events from that point - This is built into the
EventSourceAPI — the browser sends the header automatically on reconnect
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:
- Chat Persistence Architecture (S129-166.3):
autoFlashStatehas UNRESOLVABLE race conditions. MongoDB is source of truth viaChatBackendSyncService. - Conversation Genome (S166.3):
ConversationGenomeServicecompiles compact session summaries. Streaming messages should be included before compilation. - Vitto Super-Agent (S98): 4-phase workflow (ANALYZE → PLAN → EXECUTE → VERIFY). CostGuard limits: 25 iterations, 100K tokens, 5min max.
From S226 Decisions (MCP):
dev_1773688175247_8u7bu47— Architecture decision for unified auth guard. Outcome: success.- Convention
conv_1773688184714_w68bn— Security convention (deny-by-default, @Public opt-in, @ServiceOnly for internal routes).
Existing Related Schemas:
ChatSession(ai-agents DB) — now hastenantId+tenantSyncPlugin(S226)AgentSessionDoc(vitto-mind DB) — now hastenantId(S226), storesactiveTasks: ActiveTaskRef[]CustomerSession(vitto-mind DB) — now hastenantId(S226)
Success Criteria
- User refreshes page mid-Vitto-response → response continues and appears when page reloads
- User navigates away and comes back → completed response is waiting
- Network hiccup drops SSE → frontend reconnects and gets missed events
- Backend crash during response → task recovery picks up where it left off
- Multiple browser tabs → each sees consistent state via MongoDB sync
- 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:
- Backend event persistence — persist SSE events to MongoDB as they're emitted
- Task status endpoint — query endpoint for post-disconnect recovery
- Frontend reconnection — detect stale streams, query status, resume
- SSE Last-Event-ID support — native browser reconnection with event replay
- Streaming sync — modify ChatBackendSyncService to sync in-progress messages