Build a Reconnecting SSE Task Stream With Node.js A developer built a reconnecting Server-Sent Events (SSE) task stream in Node.js to study state flow for long-running AI tasks. The implementation uses event IDs and the Last-Event-ID header to support replay and recovery after disconnection, with a test suite verifying duplicate-free convergence. The project serves as a learning exercise for concepts like idempotent consumers and cursor-based resumption, applicable to WebSocket clients and message queues. A long-running AI task needs to report more than “loading.” It may be queued, editing, testing, waiting for input, retrying, or complete. For a learning project, Server-Sent Events SSE are a small way to study that state flow. The server sends UTF-8 events over one HTTP response, the client remembers an event ID, and a reconnect can continue after the last accepted event. Important scope note: MonkeyCode https://github.com/chaitin/MonkeyCode uses WebSocket , not SSE, for the task streams I reviewed. Its mobile task-stream client https://github.com/chaitin/MonkeyCode/blob/c58bcd4dd4b7031f469a1271f276d22550b8f523/mobile/src/api/stream.ts includes reconnection, queued replies, and deduplication behavior. I used those reliability concerns as inspiration for this smaller SSE exercise, not as a description of MonkeyCode's transport. You need Node.js 20 or newer. There are no packages to install. The complete companion project has two files: sse-task-stream.mjs server, parser, reconnecting consumer test-sse.mjs deliberate disconnect and assertions An SSE record ends with a blank line. id is the resume cursor, event names the event type, and data carries the payload. res.write id: ${event.id}\n + event: task\n + data: ${JSON.stringify event }\n\n ; Our states are deliberately deterministic: js const events = "queued", "running", "testing", "complete" ; On the first connection, the test server sends only events 1 and 2, then closes. This simulates an interrupted response without waiting for a real network failure. Last-Event-ID The client stores the greatest accepted ID. Its next request includes: Last-Event-ID: 2 The server filters its replayable event log: js const last = Number req.headers "last-event-id" ?? 0 ; const pending = events .map state, index = { id: index + 1, state } .filter event = event.id last ; This example keeps the log in memory. A production server needs durable retention, authorization, bounded history, and a snapshot rule for cursors that have expired. Even with cursors, clients should handle replay. The consumer stores accepted events in a Map keyed by ID: js const seen = new Map ; seen.set event.id, event ; lastEventId = Math.max lastEventId, event.id ; Replacing the same key makes duplicate delivery harmless for this state projection. Real effects—charging a card or merging a PR—need server-side idempotency too. node test-sse.mjs Expected output: PASS reconnected, resumed after event 2, and converged without duplicates The test asserts the exact final sequence: 1 queued 2 running 3 testing 4 complete It also verifies that four event IDs produce four records after reconnection. Using SSE for two-way interactive traffic. Browser EventSource is server-to-client. Commands need separate HTTP requests, or you may prefer WebSocket when both directions are frequent. Treating reconnect as recovery. A socket reopening is not enough. The client needs a cursor or a fresh authoritative snapshot. Using array position as permanent identity. This demo can because the event list is fixed. Production IDs must remain stable across processes and restarts. Never expiring history. Keep a retention window and define what happens when Last-Event-ID is too old—usually return a snapshot cursor, not a partial story. Assuming exactly-once delivery. Design for at-least-once events and idempotent projection. After completing it, you should be able to explain the difference between connection recovery and state recovery; why an event needs an identity; how Last-Event-ID supports replay; and why deduplication belongs in the consumer even when the server tries to resume precisely. Those concepts transfer to WebSocket clients, message queues, change streams, and long-running agent interfaces. The wire format changes; the convergence problem remains. Disclosure: I contribute to the MonkeyCode project. The MonkeyCode transport statement is based on the linked source at commit c58bcd4 ; this SSE project is a standalone learning implementation tested locally.