m0serve Realtime from a synchronous Python app

Streamed WSGI bodies — shipped 2026-08-27

A design note from the engineering record, moved out of ROADMAP.md on 2026-09-03 and kept as written. The roadmap itself now holds only the project's state; the reasoning lives here.

The last buffered shape. A generator the application did not size — Django's StreamingHttpResponse, the thing every Django SSE tutorial returns — was joined whole by the shim, so a never-ending one never answered and pinned its --blocking-threads thread until the bounded join abandoned it (REAL_APP_VALIDATION.md, textshelf). It streams now, and nothing new was invented to do it: a pool thread is a second producer on the chunk channel the executor already streams throughP begin frame, s chunks, e end, the loop's own framing and end-of-stream — and the loop's per-slot "is this a channel stream?" question generalised from "the lane has an ack pair" to "the slot has an ack fd" (slot_channel_stream). What buffers is decided in the shim in order: an app Content-Length (every framework page — the rule that keeps the wire byte-identical and every bench number unchanged), list bodies, Django's HttpResponse, HEAD, bodiless statuses, M0-Hold. Only pool threads stream; the loop's handler keeps joining.

An adversarial review of the first design found four defects before a line was written, each now a rule in CLAUDE.md: the isinstance rule would have chunk-streamed every Django page (HttpResponse is an iterable); slot_is_executor's unmounted shortcut would have turned every M0-Hold on the default topology into a chunk-framed, heartbeat-less stream the moment the channel existed (the chunk pair and the executor's ack pair are separate switches now); "queue a chunk only if subscribed" does not cover two writers on a recycled slot (every frame carries a generation, executors included); and the loop cannot learn "aborted" from one bool (the abort is a tagged datagram on the completion channel, checked against the head's generation). Building it found two more: Int(Int32(UInt32(0xFFFFFFFF))) is 4294967295 on Mojo 1.0, not -1, so the disconnect ack never decoded until sign-extended by hand; and a stream of small events never exhausts a 16 KB window, so credit alone never made a thread look at the fd the disconnect arrives on — it polls before every piece now. Both are pinned.

smoke-wsgi-stream is the guard: time-to-first-piece against a 600 ms body, the hand-decoded chunked probe plus a second request on the same connection, HTTP/1.0 close-delimited, write() inside the generator in order, honest truncation on a raise, 16 concurrent 1 MB bodies byte-exact, five abandoned never-ending streams on a four-thread pool, SIGTERM with a live stream in under a second and with a sleeping generator named as the straggler, wsgiref.validate on a pool, and Django's StreamingHttpResponse reused connection and all.

Recorded follow-ups, not built: an iterator that carries its own Content-Length (FileResponse) still buffers rather than streaming with its declared length — identity framing for both producers; a framed stream ignores the request's Connection: close at its end (pre-existing, both producers); and the loop handler's frame dispatch is covered end to end by the smoke rather than by a unit test, because it needs a WSGIApp to construct.