← Back to Kontasks
Investigation: WebSocket Turn History Loss on Disconnect
Root cause analysis and solution design for lost turn history when browser disconnects during active turn
Output
🔍 Investigation: WebSocket Turn History Loss
Problem Statement
When a turn completes while the browser is disconnected (e.g., switched to another app), the turn history is lost and never arrives when reconnecting. The console also enters an unstable state requiring logout/login to recover.
Root Cause Analysis
Found 3 critical gaps in the current architecture:
| Gap 1 | Buffer deleted on turn end Line 437 in console-websocket.ts: activeTurnsByUser.delete(username)When the turn completes, ALL buffered events are immediately deleted - even if no client received them. |
| Gap 2 | In-memory only storage Events stored in Map<username, ActiveTurnState>No persistence to disk. Server restart = all buffers lost. Completed turn = buffer gone. |
| Gap 3 | No post-turn delivery mechanism Lines 457-460 log "Turn end not delivered" but take no action If client disconnects before turn_end, there's no way to recover the missed history. |
Current Flow (Broken Scenario)
1. User sends prompt → Buffer created, events start streaming 2. Browser disconnects → Events keep buffering server-side ✓ 3. Turn completes → Buffer DELETED (line 437) ✗ 4. Browser reconnects → No active turn, no buffer, nothing to replay 5. State machine confused → Expects events but none arrive
Proposed Solution: Persistent Turn Event Store
Implement a file-backed event journal that persists turn events for configurable retention (default: 48 hours).
Architecture
konui/data/turn-events/ ├── {username}/ │ ├── {requestId}.jsonl # Append-only event log │ └── {requestId}.meta.json # Turn metadata (status, timestamps) └── cleanup.lock # Prevents concurrent cleanup
Key Changes
| Component | Change |
|---|---|
| sendEvent() | Append to both in-memory buffer AND JSONL file |
| endTurn() | Update meta.json with status: completed, keep events on disk |
| handleUpgrade() | Check for completed-but-undelivered turns, replay from disk |
| New: cleanup job | Hourly job removes turns older than 48h |
| New: delivery tracking | Mark turn as "delivered" only after client confirms receipt |
Implementation Phases
Phase 1 Persistent Event Store (fixes core issue)
• JSONL event files with append-only writes
• Meta files tracking turn state
• Disk-based replay on reconnect
• JSONL event files with append-only writes
• Meta files tracking turn state
• Disk-based replay on reconnect
Phase 2 Delivery Confirmation (reliability)
• Client sends ack after receiving turn_end
• Server marks turn as delivered
• Undelivered turns stay until confirmed
• Client sends ack after receiving turn_end
• Server marks turn as delivered
• Undelivered turns stay until confirmed
Phase 3 State Machine Fix (UX improvement)
• Better handling of reconnect with completed turn
• Clear status indicators for "missed turn recovery"
• Graceful degradation when recovery fails
• Better handling of reconnect with completed turn
• Clear status indicators for "missed turn recovery"
• Graceful degradation when recovery fails
What's Next?
Choose how to proceed:
Quick Actions
Original Request
No layout configured
Details
Type
General
Status
Completed
Scope
vibetools
Tags
websocketconsolerecoveryarchitecture
Created
8 Jan 2026, 6:54 pm
Updated
8 Jan 2026, 6:54 pm
Timing
⏱️ 207.0s
0 tools
● TTFE 0%
● Thinking 100%
● Tools 0%
Created By
claude
Raw Data
{
"id": "7a7acd5b-faa4-4d5d-9d13-2e91eb635e10",
"type": "general",
"status": "completed",
"title": "Investigation: WebSocket Turn History Loss on Disconnect",
"description": "Root cause analysis and solution design for lost turn history when browser disconnects during active turn",
"context": {
"output": "<div style=\"background:#1e1e1e;color:#e0e0e0;padding:20px;border-radius:8px;font-family:'Consolas','Monaco',monospace;\"><h2 style=\"font-family:sans-serif;color:#fff;margin:0 0 16px 0;\">🔍 Investigation: WebSocket Turn History Loss</h2><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;margin-bottom:16px;\"><h3 style=\"color:#f59e0b;margin:0 0 12px 0;\">Problem Statement</h3><p style=\"margin:0;line-height:1.6;\">When a turn completes while the browser is disconnected (e.g., switched to another app), the turn history is lost and never arrives when reconnecting. The console also enters an unstable state requiring logout/login to recover.</p></div><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;margin-bottom:16px;\"><h3 style=\"color:#ef4444;margin:0 0 12px 0;\">Root Cause Analysis</h3><p style=\"margin:0 0 12px 0;line-height:1.6;\">Found <strong>3 critical gaps</strong> in the current architecture:</p><table style=\"width:100%;border-collapse:collapse;\"><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px 0;color:#98c379;font-weight:bold;\">Gap 1</td><td style=\"padding:8px;\"><strong>Buffer deleted on turn end</strong><br/><span style=\"color:#9ca3af;\">Line 437 in console-websocket.ts: <code style=\"background:#1e1e1e;padding:2px 6px;border-radius:3px;\">activeTurnsByUser.delete(username)</code></span><br/>When the turn completes, ALL buffered events are immediately deleted - even if no client received them.</td></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px 0;color:#98c379;font-weight:bold;\">Gap 2</td><td style=\"padding:8px;\"><strong>In-memory only storage</strong><br/><span style=\"color:#9ca3af;\">Events stored in <code style=\"background:#1e1e1e;padding:2px 6px;border-radius:3px;\">Map<username, ActiveTurnState></code></span><br/>No persistence to disk. Server restart = all buffers lost. Completed turn = buffer gone.</td></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px 0;color:#98c379;font-weight:bold;\">Gap 3</td><td style=\"padding:8px;\"><strong>No post-turn delivery mechanism</strong><br/><span style=\"color:#9ca3af;\">Lines 457-460 log \"Turn end not delivered\" but take no action</span><br/>If client disconnects before turn_end, there's no way to recover the missed history.</td></tr></table></div><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;margin-bottom:16px;\"><h3 style=\"color:#3b82f6;margin:0 0 12px 0;\">Current Flow (Broken Scenario)</h3><pre style=\"background:#1e1e1e;padding:12px;border-radius:6px;overflow-x:auto;line-height:1.6;\"><span style=\"color:#7f848e;\">1.</span> <span style=\"color:#98c379;\">User sends prompt</span> → Buffer created, events start streaming\n<span style=\"color:#7f848e;\">2.</span> <span style=\"color:#f59e0b;\">Browser disconnects</span> → Events keep buffering server-side ✓\n<span style=\"color:#7f848e;\">3.</span> <span style=\"color:#ef4444;\">Turn completes</span> → <strong>Buffer DELETED</strong> (line 437) ✗\n<span style=\"color:#7f848e;\">4.</span> <span style=\"color:#9ca3af;\">Browser reconnects</span> → No active turn, no buffer, nothing to replay\n<span style=\"color:#7f848e;\">5.</span> <span style=\"color:#ef4444;\">State machine confused</span> → Expects events but none arrive</pre></div><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;margin-bottom:16px;\"><h3 style=\"color:#22c55e;margin:0 0 12px 0;\">Proposed Solution: Persistent Turn Event Store</h3><p style=\"margin:0 0 16px 0;\">Implement a <strong>file-backed event journal</strong> that persists turn events for configurable retention (default: 48 hours).</p><h4 style=\"color:#e5c07b;margin:0 0 8px 0;\">Architecture</h4><pre style=\"background:#1e1e1e;padding:12px;border-radius:6px;overflow-x:auto;line-height:1.6;\"><span style=\"color:#c678dd;\">konui/data/turn-events/</span>\n├── <span style=\"color:#61afef;\">{username}/</span>\n│ ├── <span style=\"color:#e5c07b;\">{requestId}.jsonl</span> <span style=\"color:#7f848e;\"># Append-only event log</span>\n│ └── <span style=\"color:#e5c07b;\">{requestId}.meta.json</span> <span style=\"color:#7f848e;\"># Turn metadata (status, timestamps)</span>\n└── <span style=\"color:#98c379;\">cleanup.lock</span> <span style=\"color:#7f848e;\"># Prevents concurrent cleanup</span></pre><h4 style=\"color:#e5c07b;margin:16px 0 8px 0;\">Key Changes</h4><table style=\"width:100%;border-collapse:collapse;\"><tr style=\"border-bottom:1px solid #4b5563;\"><th style=\"text-align:left;padding:8px;color:#9ca3af;\">Component</th><th style=\"text-align:left;padding:8px;color:#9ca3af;\">Change</th></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px;\">sendEvent()</td><td style=\"padding:8px;\">Append to both in-memory buffer AND JSONL file</td></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px;\">endTurn()</td><td style=\"padding:8px;\">Update meta.json with <code style=\"background:#1e1e1e;padding:2px 4px;border-radius:3px;\">status: completed</code>, keep events on disk</td></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px;\">handleUpgrade()</td><td style=\"padding:8px;\">Check for completed-but-undelivered turns, replay from disk</td></tr><tr style=\"border-bottom:1px solid #4b5563;\"><td style=\"padding:8px;\">New: cleanup job</td><td style=\"padding:8px;\">Hourly job removes turns older than 48h</td></tr><tr><td style=\"padding:8px;\">New: delivery tracking</td><td style=\"padding:8px;\">Mark turn as \"delivered\" only after client confirms receipt</td></tr></table></div><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;margin-bottom:16px;\"><h3 style=\"color:#8b5cf6;margin:0 0 12px 0;\">Implementation Phases</h3><div style=\"margin-bottom:12px;\"><span style=\"background:#22c55e;color:#000;padding:2px 8px;border-radius:4px;font-size:0.8rem;font-weight:bold;\">Phase 1</span> <strong style=\"margin-left:8px;\">Persistent Event Store</strong> <span style=\"color:#7f848e;\">(fixes core issue)</span><br/><span style=\"color:#9ca3af;margin-left:60px;\">• JSONL event files with append-only writes<br/>• Meta files tracking turn state<br/>• Disk-based replay on reconnect</span></div><div style=\"margin-bottom:12px;\"><span style=\"background:#3b82f6;color:#fff;padding:2px 8px;border-radius:4px;font-size:0.8rem;font-weight:bold;\">Phase 2</span> <strong style=\"margin-left:8px;\">Delivery Confirmation</strong> <span style=\"color:#7f848e;\">(reliability)</span><br/><span style=\"color:#9ca3af;margin-left:60px;\">• Client sends ack after receiving turn_end<br/>• Server marks turn as delivered<br/>• Undelivered turns stay until confirmed</span></div><div><span style=\"background:#f59e0b;color:#000;padding:2px 8px;border-radius:4px;font-size:0.8rem;font-weight:bold;\">Phase 3</span> <strong style=\"margin-left:8px;\">State Machine Fix</strong> <span style=\"color:#7f848e;\">(UX improvement)</span><br/><span style=\"color:#9ca3af;margin-left:60px;\">• Better handling of reconnect with completed turn<br/>• Clear status indicators for \"missed turn recovery\"<br/>• Graceful degradation when recovery fails</span></div></div><div style=\"background:#2d2d2d;padding:16px;border-radius:6px;\"><h4 style=\"margin:0 0 8px 0;color:#fff;\">What's Next?</h4><p style=\"color:#9ca3af;margin:0;\">Choose how to proceed:</p></div></div>",
"requestedAt": "2026-01-08T00:05:00.000Z",
"requestId": "d94d1368-8f10-4701-bb50-d1bbf02f8102",
"choices": [
{
"label": "Implement Phase 1",
"value": "Implement Phase 1: Persistent Event Store for WebSocket turn history recovery. Create the JSONL event files and meta tracking, modify sendEvent() to write to disk, and update handleUpgrade() to replay from disk for completed turns.",
"primary": true
},
{
"label": "More detail on design",
"value": "Show more detailed design for the persistent event store including file format, cleanup strategy, and edge cases"
},
{
"label": "Quick fix first",
"value": "Implement a quick fix to address the state machine instability issue first, before tackling full persistence"
},
{
"label": "Add to backlog",
"value": "Add WebSocket turn history persistence to VIBE.md backlog for later implementation"
}
],
"turnTiming": {
"totalMs": 206955,
"ttfeMs": 52,
"thinkingMs": 206903,
"toolExecutionMs": 0,
"toolCallCount": 0,
"thinkingPct": 100,
"toolsPct": 0,
"ttfePct": 0
}
},
"createdBy": "claude",
"createdAt": "2026-01-08T08:54:28.397Z",
"updatedAt": "2026-01-08T08:54:45.801Z",
"requestId": "d94d1368-8f10-4701-bb50-d1bbf02f8102",
"scope": "vibetools",
"tags": [
"websocket",
"console",
"recovery",
"architecture"
],
"targetUser": "claude"
}