2026 03 16 Vitto Chat Persistence Recovery
Documentation for Plans
Vitto Chat Persistence & Response Recovery — Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Ensure Vitto's responses survive any kind of frontend disconnection — page refresh, browser close, device switch, or network hiccup.
Architecture: 6 targeted changes across backend-vitto-api (3 changes) and frontend vitto-chat (3 changes). The backend bridge writes completed responses to ai-agents DB unconditionally. ReplaySubject buffers events for reconnecting clients. A new status endpoint enables frontend recovery of stale streams. No new databases, no new collections — leverages existing bridgeMessageToAiApi() and agent-sessions.
Tech Stack: NestJS (backend-vitto-api), Angular 20+ with signals (frontend), RxJS ReplaySubject, Mongoose/MongoDB, Socket.IO
Spec: docs/superpowers/specs/2026-03-16-vitto-chat-persistence-recovery-design.md
Git constraint (S228): Do NOT commit during this session — another session is running concurrently on the same branch.
Chunk 1: Backend Changes (Tasks 1-4)
Task 1: Bridge Completed Responses to ai-agents DB
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts:1014-1018
This is the highest-impact change. After the conversational loop completes, call bridgeMessageToAiApi() to push the response to ai-agents DB. This ensures the response is visible to loadBackendSessions() even if the frontend never reconnects.
- Step 1: Verify BackendAiClientService is injectable
Check that VittoEngineService already has BackendAiClientService injected. The actual property name is aiClient (verified at line 73: private readonly aiClient: BackendAiClientService).
Run: grep -n 'aiClient\|BackendAiClientService' APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts
If NOT injected, add it to the constructor. The service is exported by ConnectivityModule which is imported in AppModule.
- Step 2: Add bridge call after agentSessions.addMessage()
In vitto-engine.service.ts, after line 1017 (the assistant addMessage call), add:
// S228: Bridge response to ai-agents DB for multi-device recovery.
// Even if the frontend never reconnects, loadBackendSessions() will find this.
this.aiClient.bridgeMessageToAiApi({
userId,
spaceId,
channel: 'web',
messages: [{ role: 'assistant', content: result.output }],
title: `Vitto conversation`,
}).catch(err => this.logger.debug(`Bridge to ai-api failed (non-critical): ${(err as Error).message}`));
This goes between line 1017 and line 1019 (the comment // GAP 6: Emit task.completed).
- Step 3: Verify the change compiles
Run: npx nx build backend-vitto-api --skip-nx-cache 2>&1 | tail -20
Expected: BUILD SUCCESS (or only pre-existing warnings).
Task 2: Subject to ReplaySubject + Extended Teardown Window
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts:12,60,872,1048-1052
Replace the plain Subject with ReplaySubject so late-connecting clients get buffered event replay.
- Step 1: Add ReplaySubject to imports
In vitto-engine.service.ts, line 12, change:
import { Subject, Subscription } from 'rxjs';
to:
import { Subject, ReplaySubject, Subscription } from 'rxjs';
- Step 2: Update the Map type annotation
Line 60, change:
private readonly conversationalStreams = new Map<string, Subject<SSEEvent>>();
to:
private readonly conversationalStreams = new Map<string, ReplaySubject<SSEEvent>>();
- Step 3: Create ReplaySubject in startConversationalMessage
Line 872, change:
const streamSubject = new Subject<SSEEvent>();
to:
// S228: ReplaySubject buffers events for late subscribers (reconnecting clients).
// 100 events max, 5-minute window (matches CostGuard max task duration).
const streamSubject = new ReplaySubject<SSEEvent>(100, 5 * 60 * 1000);
- Step 4: Extend teardown window from 30s to 5 minutes
Lines 1048-1052, change:
// Keep the stream alive for 30s so late SSE subscribers can still read the final event
setTimeout(() => {
streamSubject.complete();
this.conversationalStreams.delete(taskId);
}, 30000);
to:
// S228: Keep stream alive for 5min so reconnecting clients get full event replay.
// Matches CostGuard max task duration. ReplaySubject buffers up to 100 events.
const STREAM_TTL_MS = 5 * 60 * 1000;
setTimeout(() => {
streamSubject.complete();
this.conversationalStreams.delete(taskId);
this.completedTaskResults.delete(taskId);
}, STREAM_TTL_MS);
Note: this.completedTaskResults is added in Task 3.
- Step 5: Verify the change compiles
Run: npx nx build backend-vitto-api --skip-nx-cache 2>&1 | tail -20
Expected: May fail until Task 3 adds completedTaskResults. If so, temporarily comment out that line and verify the ReplaySubject changes compile. Uncomment after Task 3.
Task 3: Add completedTaskResults Map + Task Status Endpoint
Files:
Modify:
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts:52-65,1017-1042Modify:
APIs/backend-vitto-api/src/app/engine/vitto.controller.ts:3515-3555Modify:
APIs/backend-vitto-api/src/app/engine/agent-session.service.ts:18-25,103-127Modify:
APIs/backend-vitto-api/src/app/mind/schemas/agent-session.schema.ts:11-32Step 1: Add completedTaskResults Map declaration
In vitto-engine.service.ts, after line 65 (after taskContexts declaration), add:
/** S228: Track completed conversational task results for status queries.
* Populated on task success/failure, cleaned up alongside conversationalStreams. */
private readonly completedTaskResults = new Map<string, {
status: 'completed' | 'failed';
response?: string;
error?: string;
completedAt: number;
userId: string;
}>();
- Step 2: Populate completedTaskResults on task success
In vitto-engine.service.ts, after line 1017 (after assistant addMessage) and BEFORE the bridge call added in Task 1, add:
// S228: Record terminal state for status endpoint
this.completedTaskResults.set(taskId, {
status: 'completed',
response: result.output,
completedAt: Date.now(),
userId,
});
- Step 3: Populate completedTaskResults on task failure
In the catch block (after line 1042, after the task.failed event emission), add:
// S228: Record terminal state for status endpoint
this.completedTaskResults.set(taskId, {
status: 'failed',
error: errMessage,
completedAt: Date.now(),
userId,
});
- Step 4: Add getConversationalTaskStatus method to VittoEngineService
Add this public method after the existing getConversationalStream method (after line 1137):
/** S228: Query the status of a conversational task for frontend recovery. */
async getConversationalTaskStatus(taskId: string, userId: string): Promise<{
status: 'running' | 'completed' | 'failed' | 'unknown';
response?: string;
error?: string;
completedAt?: number;
userId?: string;
}> {
// 1. Check completed results map (most common recovery path)
const completed = this.completedTaskResults.get(taskId);
if (completed) {
return {
status: completed.status,
response: completed.response,
error: completed.error,
completedAt: completed.completedAt,
userId: completed.userId,
};
}
// 2. Check if stream still exists (task is running)
if (this.conversationalStreams.has(taskId)) {
return { status: 'running' };
}
// 3. Fallback: query agent-sessions for this taskId in message metadata.
// We can't use getSession(userId) because we don't know spaceId.
// Instead, query the model directly for any active session belonging
// to this user that has a message with the matching taskId.
try {
const session = await this.agentSessions.findSessionByTaskId(userId, taskId);
if (session) {
const match = [...session.messages].reverse().find(
m => m.role === 'assistant' && (m.metadata as Record<string, unknown>)?.taskId === taskId,
);
if (match) {
return { status: 'completed', response: match.content, completedAt: match.timestamp?.getTime?.(), userId };
}
}
} catch { /* non-critical */ }
return { status: 'unknown' };
}
Note: Check if AgentSessionService has a getSession(userId) method. If it uses a different method name (e.g., getOrCreateSession), use that. The key is finding the session by userId and scanning its messages for matching taskId in metadata. See Task 4 if the method is missing.
- Step 5: Add metadata field to SessionMessage interface
In agent-session.service.ts, line 18-25, change:
export interface SessionMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
toolName?: string;
toolCallId?: string;
timestamp: Date;
channel?: string;
}
to:
export interface SessionMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
toolName?: string;
toolCallId?: string;
timestamp: Date;
channel?: string;
/** S228: Optional metadata for task tracking (taskId, etc.) */
metadata?: Record<string, unknown>;
}
- Step 6: Add metadata field to AgentSessionMessage schema
In agent-session.schema.ts, after line 29 (after channel prop), add:
@Prop({ type: Object })
metadata?: Record<string, unknown>;
- Step 7: Add taskId to the assistant addMessage call
In vitto-engine.service.ts, line 1017, change:
this.agentSessions.addMessage(userId, { role: 'assistant', content: result.output, timestamp: new Date(), channel: 'web' }, spaceId).catch(() => { /* non-critical */ });
to:
this.agentSessions.addMessage(userId, { role: 'assistant', content: result.output, timestamp: new Date(), channel: 'web', metadata: { taskId } }, spaceId).catch(() => { /* non-critical */ });
- Step 8: Add the controller endpoint
In vitto.controller.ts, BEFORE the @Sse('message/:taskId/stream') endpoint (before line 3555), add:
/** S228: Task status endpoint for frontend recovery after disconnection. */
@Public()
@Get('message/:taskId/status')
async getConversationalTaskStatus(
@Param('taskId') taskId: string,
@CurrentUser() user: AuthenticatedUser,
) {
const userId = user?.userId || 'anonymous';
const result = await this.engine.getConversationalTaskStatus(taskId, userId);
// Auth check: for completed/failed, verify the requesting user owns the task.
// The taskId format is conv-{timestamp}-{random} — cryptographically random but
// we still enforce ownership for defense-in-depth.
if ((result.status === 'completed' || result.status === 'failed') && result.userId && result.userId !== userId) {
return { status: 'unknown' as const }; // Hide other users' task results
}
// Strip internal userId from response
const { userId: _uid, ...publicResult } = result as any;
return publicResult;
}
Important route ordering: This @Get('message/:taskId/status') must be declared BEFORE @Sse('message/:taskId/stream') in the controller, because NestJS matches routes top-to-bottom and @Sse could swallow the /status path. Verify the route order after adding.
- Step 9: Verify the full backend compiles
Run: npx nx build backend-vitto-api --skip-nx-cache 2>&1 | tail -20
Expected: BUILD SUCCESS.
Task 4: Add findSessionByTaskId method to AgentSessionService
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/agent-session.service.ts
The status endpoint needs to find a session by taskId (stored in message metadata). Since sessions are keyed by userId:spaceId and we don't know the spaceId at query time, we need a query that searches across all of a user's sessions.
- Step 1: Add findSessionByTaskId method
/** S228: Find a user's session containing a message with the given taskId in metadata. */
async findSessionByTaskId(userId: string, taskId: string): Promise<AgentSessionDoc | null> {
return this.sessionModel.findOne({
sessionKey: { $regex: `^${userId}` },
status: 'active',
'messages.metadata.taskId': taskId,
}).exec();
}
Note: This uses a regex prefix match on sessionKey to match both userId (no space) and userId:spaceId (with space) formats. The messages.metadata.taskId query works because Mongoose stores the nested object directly.
- Step 2: Verify compilation
Run: npx nx build backend-vitto-api --skip-nx-cache 2>&1 | tail -20
Chunk 2: Frontend Changes (Tasks 5-7)
Task 5: Fix Reconciliation Corruption
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-persistence.service.ts:845Step 1: Filter isStreaming messages from reconciliation payload
In vitto-chat-persistence.service.ts, line 845, change:
const localMessageIds = localSession.messages.map(m => m.id);
to:
// S228: Exclude stale isStreaming messages from reconciliation.
// Without this filter, broken partial messages get pushed to MongoDB.
const localMessageIds = localSession.messages
.filter(m => !m.isStreaming)
.map(m => m.id);
- Step 2: Also filter the missingOnBackend sync
In the same method, lines 871-879, the missingMessages are sent to the backend. Add an isStreaming filter there too for defense-in-depth. Change:
const missingMessages = localSession.messages.filter(
m => result.missingOnBackend.includes(m.id)
);
to:
const missingMessages = localSession.messages.filter(
m => result.missingOnBackend.includes(m.id) && !m.isStreaming // S228: Never sync stale streaming messages
);
- Step 3: Verify frontend compiles
Run: npx nx build vitto-chat --skip-nx-cache 2>&1 | tail -20
Expected: BUILD SUCCESS.
Task 6: Add Skip-Sync for Bridged Messages
Files:
Modify:
libs/libs-ai/chat-sync/src/lib/services/chat-backend-sync.service.ts:156Step 1: Add bridged message filter to ChatBackendSyncService
In chat-backend-sync.service.ts, line 156, change:
const finalized = messages.filter(m => !m.isStreaming);
to:
// S228: Skip streaming messages AND messages already bridged by the backend
const finalized = messages.filter(m => !m.isStreaming && !(m as any).metadata?.bridged);
- Step 2: Verify the SyncMessage type
Check if SyncMessage has a metadata field. If not, the (m as any) cast handles it. If you want type safety, add metadata?: Record<string, unknown> to the SyncMessage interface in chat-sync.models.ts.
Run: grep -n 'interface SyncMessage' libs/libs-ai/chat-sync/src/lib/models/chat-sync.models.ts
- Step 3: Verify frontend compiles
Run: npx nx build chat-sync --skip-nx-cache 2>&1 | tail -20
Task 7: Frontend Stale Stream Recovery + Emotion Suppression
Files:
- Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.ts:156-206 - Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-persistence.service.ts - Modify:
libs/libs-ai/vitto-chat/src/lib/components/chat-shell/chat-shell.component.ts:308
This is the largest task. It adds recovery logic that runs on page load.
- Step 1: Add helper to persistence service to find stale streaming messages
In vitto-chat-persistence.service.ts, add a public method (near the other message accessor methods):
/** S228: Find all messages with isStreaming: true in the active session. */
getStaleStreamingMessages(): PersistedChatMessage[] {
const sessionId = this._activeSessionId();
if (!sessionId) return [];
const session = this.state().sessions[sessionId];
if (!session) return [];
return session.messages.filter(m => m.isStreaming === true);
}
/** S228: Signal that stale stream recovery is complete. Unblocks reconciliation. */
private _recoveryComplete$ = new Subject<void>();
setRecoveryComplete(): void { this._recoveryComplete$.next(); this._recoveryComplete$.complete(); }
private _recoveryDone = false;
Note: Import Subject from rxjs if not already imported in this file.
- Step 2: Add reconciliation gate for recovery ordering (RxJS-based)
In reconcileActiveSession(), after line 840 (if (!localSession || localSession.messageCount === 0) return;), add:
// S228: Wait for stale stream recovery before reconciling (RxJS, no setInterval).
if (!this._recoveryDone) {
this._recoveryComplete$.pipe(
take(1),
timeout(10000), // Safety: unblock after 10s regardless
catchError(() => of(undefined)),
).subscribe(() => {
this._recoveryDone = true;
this.reconcileActiveSession();
});
return;
}
Note: Add take, timeout, catchError, of to RxJS imports if not already present.
- Step 2b: Extend PersistedChatMessage metadata type
In vitto-chat-persistence.service.ts, find the PersistedChatMessage interface's metadata field (around line 70-81). Add recovery-specific fields:
metadata?: {
// ... existing fields (model, provider, tokens, cost, etc.) ...
/** S228: Recovery metadata */
recovered?: boolean;
bridged?: boolean;
error?: boolean;
};
This prevents TypeScript compilation errors when recovery code writes metadata: { recovered: true }.
- Step 3: Add recoverStaleStreams method to VittoChatEngineService
In vitto-chat-engine.service.ts, add near checkAndReconnectActiveGeneration (around line 1305):
/**
* S228: Detect and recover stale isStreaming messages after page load.
* Called after hydration + auth, BEFORE reconciliation completes.
*/
private async recoverStaleStreams(): Promise<void> {
if (!this.vittoApiUrl) {
this.persistence.setRecoveryComplete();
return;
}
const staleMessages = this.persistence.getStaleStreamingMessages();
if (staleMessages.length === 0) {
this.persistence.setRecoveryComplete();
return;
}
this.logger.info(`[VittoChatEngine] Recovering ${staleMessages.length} stale streaming message(s)`);
const recoveries = staleMessages.map(async (msg) => {
const taskId = msg.id?.replace('stream-', '');
if (!taskId) {
this.persistence.updateMessage(msg.id, {
isStreaming: false,
content: (msg.content || '') + '\n\n*[Response interrupted]*',
metadata: { ...msg.metadata, recovered: true, error: true },
});
return;
}
try {
const response = await fetch(`${this.vittoApiUrl}/engine/message/${taskId}/status`);
if (!response.ok) throw new Error(`Status ${response.status}`);
const status = await response.json() as {
status: 'running' | 'completed' | 'failed' | 'unknown';
response?: string;
error?: string;
};
switch (status.status) {
case 'completed':
this.logger.info(`[VittoChatEngine] Recovered completed task ${taskId}`);
this.persistence.updateMessage(msg.id, {
id: `vitto-${taskId}`,
content: status.response || msg.content || '',
isStreaming: false,
metadata: { ...msg.metadata, recovered: true, bridged: true },
});
break;
case 'running':
this.logger.info(`[VittoChatEngine] Reconnecting to running task ${taskId}`);
this.reconnectToStream(taskId, msg.id);
break;
case 'failed':
this.persistence.updateMessage(msg.id, {
isStreaming: false,
content: (msg.content || '') + '\n\n*[Response failed]*',
metadata: { ...msg.metadata, recovered: true, error: true },
});
break;
default:
this.persistence.updateMessage(msg.id, {
isStreaming: false,
content: (msg.content || '') + '\n\n*[Response interrupted]*',
metadata: { ...msg.metadata, recovered: true, error: true },
});
break;
}
} catch (err) {
this.logger.warn(`[VittoChatEngine] Recovery query failed for ${taskId}: ${(err as Error).message}`);
// Network error — leave as-is, will retry on next page load
}
});
await Promise.allSettled(recoveries);
this.persistence.setRecoveryComplete();
}
- Step 4: Add reconnectToStream helper
Add below recoverStaleStreams:
/** S228: Reopen EventSource to a running task (ReplaySubject replays buffered events). */
private reconnectToStream(taskId: string, streamingMsgId: string): void {
const streamUrl = `${this.vittoApiUrl}/engine/message/${taskId}/stream`;
const eventSource = new EventSource(streamUrl);
eventSource.onmessage = (event: MessageEvent) => {
try {
const sseEvent = JSON.parse(event.data as string) as { type: string; data?: Record<string, unknown> };
if (sseEvent.type === 'agent.text' || sseEvent.type === 'content_delta' || sseEvent.type === 'text_delta') {
const delta = (sseEvent.data?.['content'] as string) || (sseEvent.data?.['text'] as string) || '';
if (delta) {
// Accumulate content (delta is partial text, not replacement)
const currentMsgs = this.persistence.messages();
const existing = currentMsgs.find(m => m.id === streamingMsgId);
const updatedContent = (existing?.content || '') + delta;
this.persistence.updateMessage(streamingMsgId, { content: updatedContent });
}
}
if (sseEvent.type === 'task.completed') {
const response = sseEvent.data?.['response'] as string || '';
this.persistence.updateMessage(streamingMsgId, {
id: `vitto-${taskId}`,
content: response,
isStreaming: false,
metadata: { recovered: true, bridged: true },
});
this.stopLoadingWithTimeout();
eventSource.close();
}
if (sseEvent.type === 'task.failed') {
const msgs = this.persistence.messages();
const current = msgs.find(m => m.id === streamingMsgId);
this.persistence.updateMessage(streamingMsgId, {
isStreaming: false,
content: (current?.content || '') + '\n\n*[Response failed]*',
metadata: { recovered: true, error: true },
});
this.stopLoadingWithTimeout();
eventSource.close();
}
} catch { /* skip parse errors */ }
};
eventSource.onerror = () => {
eventSource.close();
};
this.startLoadingWithTimeout();
}
- Step 5: Wire recoverStaleStreams into the init effect
In the constructor effect, add the recovery call INSIDE the if (!this.backendLoaded) block, between line 214 (this.checkAndReconnectActiveGeneration(spaceId || '');) and the closing brace of the block at line 215. It must be BEFORE the return; at line 216:
// S228: Recover stale streaming messages from previous sessions
this.recoverStaleStreams().catch(err =>
this.logger.warn('[VittoChatEngine] Stale stream recovery failed', err),
);
- Step 6: Add emotion suppression guard
In chat-shell.component.ts, line 308, change:
if (!msg || msg.isStreaming) {
to:
// S228: Skip emotion inference for recovered messages (avoids unexpected avatar animation)
if (!msg || msg.isStreaming || (msg.metadata as any)?.recovered) {
- Step 7: Verify all frontend libraries compile
Run:
npx nx build vitto-chat --skip-nx-cache 2>&1 | tail -20
npx nx build chat-sync --skip-nx-cache 2>&1 | tail -20
Expected: All BUILD SUCCESS.
Chunk 3: Cleanup + Integration Verification (Tasks 8-9)
Task 8: One-Time MongoDB Cleanup
Run after deployment to clean existing corrupted data (stale isStreaming: true messages in ai-agents DB):
- Step 1: Run cleanup script against ai-agents DB
// Run in mongosh connected to ai-agents DB
const oneHourAgo = Date.now() - (60 * 60 * 1000);
db.chat_sessions.updateMany(
{ 'messages': { $elemMatch: { isStreaming: true, timestamp: { $lt: oneHourAgo } } } },
{ $pull: { messages: { isStreaming: true, timestamp: { $lt: oneHourAgo } } } }
)
The 1-hour filter ensures we don't remove actively-streaming messages during a live deployment.
- Step 2: Verify cleanup results
db.chat_sessions.countDocuments({ 'messages.isStreaming': true })
Expected: 0 (or only messages created within the last hour).
Task 9: Manual Integration Verification
Files: None (testing only)
- Step 1: Start all backends
Start backend-api (3000), backend-ai-api (3001), and backend-vitto-api (3003). Wait for all to report listening.
- Step 2: Start the frontend
Start the space app on port 6200. Wait for it to be available.
- Step 3: Test normal flow (no disconnect)
- Open browser to localhost:6200, navigate to Vitto chat
- Send a message, verify response appears normally
- Check DevTools Network tab: confirm both SSE stream AND bridge POST occurred
- Step 4: Test page refresh mid-stream
- Send a message to Vitto
- While processing, refresh the page (F5)
- On reload, check console for
[VittoChatEngine] Recovering X stale streaming message(s) - Verify one of: response appears (completed), loading resumes (running), or error shown (unknown)
- Step 5: Test browser close + reopen
- Send a message, close the browser tab entirely
- Wait 10 seconds, open a new tab to localhost:6200
- Navigate to Vitto chat, verify the response is present (loaded from ai-agents DB)
- Step 6: Test the status endpoint directly
curl http://localhost:3003/vitto/engine/message/conv-XXXXX/status
Expected: { "status": "completed", "response": "...", "completedAt": ... } or { "status": "unknown" }.
- Step 7: Verify no stale messages in MongoDB
Check ai-agents DB for isStreaming: true corruption. Expected: 0 new stale messages.
- Step 8: Test multi-device (second tab)
- Open two browser tabs to Vitto chat
- Send a message in Tab A
- Verify response appears in Tab B (via Socket.IO from bridge write)
Summary
| Task | Change | Files Modified | Effort |
|---|---|---|---|
| 1 | Bridge to ai-agents DB | vitto-engine.service.ts |
~30 min |
| 2 | ReplaySubject + extended window | vitto-engine.service.ts |
~15 min |
| 3 | completedTaskResults Map + status endpoint | vitto-engine.service.ts, vitto.controller.ts, agent-session.service.ts, agent-session.schema.ts |
~2 hr |
| 4 | findSessionByTaskId helper | agent-session.service.ts |
~15 min |
| 5 | Reconciliation corruption fix | vitto-chat-persistence.service.ts |
~30 min |
| 6 | Skip-sync for bridged messages | chat-backend-sync.service.ts |
~30 min |
| 7 | Frontend recovery + emotion suppression | vitto-chat-engine.service.ts, vitto-chat-persistence.service.ts, chat-shell.component.ts |
~3 hr |
| 8 | MongoDB cleanup script | None (manual) | ~15 min |
| 9 | Manual integration testing | None | ~1 hr |
Total estimated: ~8.5 hours
Dependencies:
- Tasks 1-4 (backend) can be done in parallel with Tasks 5-6 (frontend infra)
- Task 3 Steps 5-6 (schema changes) MUST complete before Step 7 (metadata in addMessage) or Mongoose silently drops the field
- Task 7 (frontend recovery) depends on Task 3 (status endpoint exists)
- Task 8 (cleanup) can run anytime after deployment
- Task 9 (testing) depends on all other tasks
Deviation from spec: The reconnectToStream method lives in vitto-chat-engine.service.ts (not vitto-chat-streaming.service.ts as listed in the spec's Files Modified table). This is intentional — the engine service already manages EventSource lifecycle and the streaming service would require additional cross-service wiring.