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
{{ message }}
Repository navigation
dispatcher's second streaming site answers a write-less transport res.json(result.result) — the SSE descriptor serializes with its AsyncIterable dropped, silent total event loss #9961
Found while running the #9936 reader/reachability sweep (recording only, not starting). Measured on origin/main at 5989b0de90.
Blocked-by: #9936 — the correct shape for this body follows the same fork ruling that card carries; #9936 remains open and is not addressed here.
The site
packages/runtime/src/dispatcher-plugin.ts, in the dispatch-result writer (locate by text const isStream = r && typeof r === 'object', around line 351 on the measured ref; numbers drift). This is a DIFFERENT site from the mountRouteOnServer fallback #9936 names — that one is the enumerated unenveloped: 1 in check-route-envelope.mjs; this body is a relayed res.json(result.result) and is invisible to the gate's counters by design.
When a dispatch result carries the SSE/streaming descriptor and the transport's res HAS write/end, the events stream correctly. When res lacks them, the guard fails and control falls through to:
result.result is the stream descriptor itself: { type: 'stream', contentType, events, vercelDataStream, headers } where events is an AsyncIterable. JSON.stringify serializes an async generator object to {} — the caller receives the descriptor's metadata with the payload gone, HTTP 200, and the iterable is never drained. Unlike #9936's site (which at least collects and returns the events), this one loses every event silently.
Reachability
Same population as #9936's site: both in-repo http.server providers (plugin-hono-server/src/adapter.ts, qa/http-conformance/src/adapter.ts) construct write/end unconditionally, so no shipped composition reaches this branch. It is reachable only through an externally supplied Runtime({ server }) transport that omits the optional write/end — a composition packages/spec/src/contracts/http-server.ts explicitly permits, and for which its #3607 / ADR-0076 OQ#10 contract text prescribes falling back to buffered send(). No in-repo test covers either streaming branch of dispatcher-plugin.
Why filed separately
#9936's scope is explicitly its one enumerated { events } literal body. This site needs the same ruling applied (buffered send() per the contract text, an envelope, or retirement), so it should be routed after that fork is decided rather than fixed to a shape the ruling might contradict.
Claim: PM loop round 25 — family member of chain-head #9936
Session: session_01WeN7F6jQFpcqW2BN56RdPa
Branch: claude/issue-9936-sse-fallback-envelope (⚠️ the chain-head's branch — this card does not get its own)
Worktree: objectstack-issue-9936
Domain: domain:cli
File surface: packages/runtime/src/dispatcher-plugin.ts (the dispatch-result writer's write-less fallback), plus a pin and .changeset/**
Clause-②: yes — same fork, same contract as #9936
Serial constraints: 🔒 same file as #9936, which is precisely why this is a family dispatch and not a queued sibling. Same-file work is hard serial in this lane (maintainer-reaffirmed 2026-08-19), so shipping these as two PRs would serialise them for no benefit and risk two shapes for one contract.
Option B, ruled on #9936 (comment 5340848774): implement the contract's own prescription. packages/spec/src/contracts/http-server.ts, the JSDoc on IHttpResponse.write (:86-96 on 5989b0de90), states it verbatim:
CONTRACT (#3607, ADR-0076 OQ#10): consumers that emit streaming results … feature-detect this member and fall back to buffered send() when absent.
I verified that text myself. Today's body contradicts a recorded contract rather than awaiting a new one, which is why this was not escalated to the maintainer.
⚠️ This site is the WORSE of the two — do not copy-paste #9936's fix onto it
#9936's site at least collects the events into an array before answering. This one loses them entirely: res.json(result.result) serializes the stream descriptor with its eventsAsyncIterable dropped to {} by JSON.stringify, the caller gets HTTP 200 with the payload gone, and the iterable is never drained.
⇒ The fix must actually drain the iterable and emit the frames, not re-serialize the descriptor into a better-shaped envelope.
What rides the family PR
One branch, per-member commits, one PR carrying Fixes #9936 and Fixes #9961. Both sites take the same shape — that is the point of ruling them together.
⛔ packages/spec/** untouched: you are implementing the contract, not amending it. ⛔ Never weaken a gate. ⚠️ Note this body is a relayedres.json(...) and is invisible to check-route-envelope's counters by design — so unlike #9936's site, no gate number will move for this half. That makes the pin the only evidence it was fixed; state plainly what it would catch.
Report on the chain-head card #9936 with the os-dev-report marker, covering both members.
Activity
huangyiirene commentedon Aug 19, 2026
Claim: PM loop round 25 — family member of chain-head #9936⚠️ the chain-head's branch — this card does not get its own)
Session:
session_01WeN7F6jQFpcqW2BN56RdPaBranch:
claude/issue-9936-sse-fallback-envelope(Worktree:
objectstack-issue-9936Domain:
domain:cliFile surface:
packages/runtime/src/dispatcher-plugin.ts(the dispatch-result writer's write-less fallback), plus a pin and.changeset/**Clause-②: yes — same fork, same contract as #9936
Serial constraints: 🔒 same file as #9936, which is precisely why this is a family dispatch and not a queued sibling. Same-file work is hard serial in this lane (maintainer-reaffirmed 2026-08-19), so shipping these as two PRs would serialise them for no benefit and risk two shapes for one contract.
Ruling — inherited from #9936, ⛔ not re-litigable
Option B, ruled on #9936 (comment
5340848774): implement the contract's own prescription.packages/spec/src/contracts/http-server.ts, the JSDoc onIHttpResponse.write(:86-96on5989b0de90), states it verbatim:I verified that text myself. Today's body contradicts a recorded contract rather than awaiting a new one, which is why this was not escalated to the maintainer.
#9936's site at least collects the events into an array before answering. This one loses them entirely:
res.json(result.result)serializes the stream descriptor with itseventsAsyncIterabledropped to{}byJSON.stringify, the caller gets HTTP 200 with the payload gone, and the iterable is never drained.⇒ The fix must actually drain the iterable and emit the frames, not re-serialize the descriptor into a better-shaped envelope.
What rides the family PR
One branch, per-member commits, one PR carrying
Fixes #9936andFixes #9961. Both sites take the same shape — that is the point of ruling them together.⛔⚠️ Note this body is a relayed
packages/spec/**untouched: you are implementing the contract, not amending it. ⛔ Never weaken a gate.res.json(...)and is invisible tocheck-route-envelope's counters by design — so unlike #9936's site, no gate number will move for this half. That makes the pin the only evidence it was fixed; state plainly what it would catch.Report on the chain-head card #9936 with the
os-dev-reportmarker, covering both members.Generated by Claude Code