You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
🤖 fix: clear todos on stream end with smart reconnection handling (#498)
## Problem
**TODOs were not being cleared when streams ended**, causing stale todos
to persist in the UI until the next user message. Additionally, on page
reload, TODOs were incorrectly reconstructed from history even for
completed streams.
The challenge: We need **different behavior for two reload scenarios**:
- **Reconnection during active stream**: Should reconstruct TODOs (work
in progress)
- **Reload after completed stream**: Should NOT reconstruct TODOs (clean
slate)
## Solution
### 1. Clear TODOs on stream end
Modified `cleanupStreamState()` to clear `currentTodos` when streams
complete (end/abort/error). TODOs are now truly stream-scoped.
### 2. Smart reconstruction on reload
**Key insight**: Check buffered events for `stream-start` to detect
active streams.
- `loadHistoricalMessages()` now accepts `hasActiveStream` parameter
- `WorkspaceStore` checks buffered events before loading history
- **Active stream** (hasActiveStream=true) → Reconstruct TODOs ✅
- **Completed stream** (hasActiveStream=false) → Don't reconstruct TODOs
✅
- **agentStatus** always reconstructed (persists across sessions) ✅
### 3. Improved separation of concerns
Centralized tool persistence logic in `processToolResult`:
- Added `context` parameter: `"streaming" | "historical"`
- `loadHistoricalMessages` no longer knows about specific tool behaviors
- Each tool declares its own persistence policy in one place
## Implementation
**WorkspaceStore checks for active streams:**
```typescript
const pendingEvents = this.pendingStreamEvents.get(workspaceId) ?? [];
const hasActiveStream = pendingEvents.some(
(event) => "type" in event && event.type === "stream-start"
);
aggregator.loadHistoricalMessages(historicalMsgs, hasActiveStream);
```
**StreamingMessageAggregator handles context:**
```typescript
loadHistoricalMessages(messages: CmuxMessage[], hasActiveStream = false) {
const context = hasActiveStream ? "streaming" : "historical";
// Process tool results with context
this.processToolResult(toolName, input, output, context);
}
```
**processToolResult decides based on tool + context:**
```typescript
private processToolResult(toolName, input, output, context: "streaming" | "historical") {
// TODOs: stream-scoped (only during streaming)
if (toolName === "todo_write" && context === "streaming") {
this.currentTodos = args.todos;
}
// agentStatus: persistent (always reconstruct)
if (toolName === "status_set") {
this.agentStatus = { emoji, message, url };
}
}
```
## Testing
Comprehensive test coverage for the full todo lifecycle:
✅ Clear todos on stream end
✅ Clear todos on stream abort
✅ Reconstruct todos when `hasActiveStream=true` (reconnection)
✅ Don't reconstruct todos when `hasActiveStream=false` (completed)
✅ Always reconstruct agentStatus (persists across sessions)
✅ Clear todos on new user message
All 146 message-related tests pass.
## Behavior Matrix
| Scenario | TODOs | agentStatus |
|----------|-------|-------------|
| During streaming | ✅ Visible | ✅ Visible |
| Stream ends | ❌ Cleared | ✅ Persists |
| Reload (active stream) | ✅ Reconstructed | ✅ Reconstructed |
| Reload (completed) | ❌ Not reconstructed | ✅ Reconstructed |
| New user message | ❌ Cleared | ❌ Cleared |
## Key Insight
The **reconnection scenario** is the critical edge case: when a user
reloads during an active stream, historical messages contain completed
tool calls from the *current* stream, and buffered events contain
`stream-start`. In this case, we *should* reconstruct TODOs to show work
in progress.
This is fundamentally different from reloading after stream completion,
where TODOs should remain cleared.
---
_Generated with `cmux`_
0 commit comments