cordis-plugin-stream-ceiling
Verified@argszero/cordis-plugin-stream-ceiling Β· v0.1.1 Β· MIT
Stream ceiling for dsh: an absolute bound on one model call, mounted on the llm/stream waterfall. The harness's streamIdleTimeoutMs is re-armed by every SSE keep-alive comment (comments never enter the payload stream), so it bounds keep-alive silence rath
Install
dsh plugin add @argszero/cordis-plugin-stream-ceiling Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Readme
@argszero/cordis-plugin-stream-ceiling
An absolute bound on one model call that transport keep-alives cannot extend, for
deepseek-harness (dsh),
mounted on the public llm/stream waterfall.
The gap
dsh's streaming adapters bound silence between transport events
(streamIdleTimeoutMs). SSE keep-alive comments count as transport events, so a
provider that keeps commenting without ever sending a terminal event re-arms that
watchdog forever:
util/timeoutdocumentspulse()as "re-arm from now, it does not accumulate";- the deepseek adapter passes
() => { watchdog.pulse() }asonActivity; sse.tsdocuments that comments "never enter the yielded payload stream".
The result is a call with no upper bound. In
discussion #6959,
a payload that was already complete sat for 10.0 / 17.3 / 17.7 minutes against a
configured streamIdleTimeoutMs: 300000, and the failure text named only the
threshold β so a 17-minute wait was reported as after 300000ms.
What this plugin does
A llm/stream consumer only ever sees payload chunks, so every bound here is
defined on the payload stream. There is no in-band traffic that can extend it β
the re-arming bug is unreproducible here by construction.
| option | measures | default |
|---|---|---|
maxStalledMs |
silence between payload chunks (and time-to-first-chunk) | 600000 |
maxCallMs |
the whole call, chunk deliveries included | 0 (off) |
payloadGraceMs |
silence after the payload has closed (every block ended) while the provider still owes a terminal event | 0 (off) |
code |
failure code carried by the terminal error finish |
TIMEOUT |
skipPurposes |
auxiliary call purposes exempted from every bound | [] |
When one elapses the plugin emits a single terminal finish whose message names
the duration actually measured beside the bound it breached:
stream-ceiling: no model output for 1062000ms (maxStalledMs 300000ms; 14 chunk(s) received, last "reasoning-delta") β transport keep-alives cannot extend this bound
stream-ceiling: no terminal event for 61500ms after the payload closed (payloadGraceMs 60000ms; 3 chunk(s) received, last "block-end")
stream-ceiling: call exceeded maxCallMs 1000000ms (measured 1002400ms of 1002400ms call time; 812 chunk(s) received, last "text-delta")
The provider's own watchdog is left alone: the plugin decides when the caller stops waiting, it never reconfigures an adapter.
Install
dsh plugin add @argszero/cordis-plugin-stream-ceiling
or mount the bundle patch directly:
- insert:
- id: stream-ceiling
name: '@argszero/cordis-plugin-stream-ceiling'
Tune it from a profile layer:
- set:
- id: stream-ceiling
config:
maxStalledMs: 600000
maxCallMs: 1800000 # optional absolute ceiling (0 = off)
payloadGraceMs: 60000 # bound the "#6959 shape": payload complete, no terminal event
code: TIMEOUT
skipPurposes: [compaction]
Choosing code
TIMEOUT is in RetryPolicySchema's default retryableCodes, so the call is
retried like any other transient failure (at most maxRetries, default 5) β a
false trip costs one attempt, not the turn. Set code: STREAM_CEILING (or any
code outside that list) to fail the call outright instead; that gives up the
attempt's output but stops a provider that hangs on every attempt.
Choosing payloadGraceMs
This is the shape reported in #6959 and #6594: the answer is complete, and the
adapter waits for an event that never arrives. It is off by default because how
long a provider legitimately pauses between blocks is provider behavior, not an
error β a grace well under maxStalledMs (a minute, say) turns a seventeen-minute
hang into a retry.
What it deliberately does not do
A plugin cannot abort a transport it does not own. GenerateOptions.signal is a
read-only AbortSignal (no abort()), and an async generator queues return()
behind an outstanding read, so abandoning the upstream iterator cannot interrupt
the provider's parked read either (see test/ceiling.spec.mjs, "abandoning the
upstream iterator cannot interrupt its pending read"). The plugin ends the
call β which is what the loop, the retry policy and the user observe; the
provider releases its socket when the adapter's own read settles or the request
context is torn down. Bounding the adapter's socket is the adapter's own job.
Requirements
@deepseek-ai/dsh-llm:>=0.1.2-rc.1 <0.2.0 || >=0.1.3-alpha.2 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0Those four comparators admit exactly eight released versions β
0.1.2-rc.1,0.1.3-alpha.2,0.1.5-alpha.1,0.1.5-alpha.2,0.1.5-rc.1,0.1.5-rc.2,0.1.6-alpha.1,0.1.6-alpha.2β and each of them has had this suite built and run against it with the peer pinned to that single line. Thellm/streamwaterfall and itsStreamChunkgrammar are identical across all of them.Write it with one comparator per line, and quote the whole thing. A semver comparator admits a prerelease only when some comparator in the same group shares that prerelease's
major.minor.patchtuple, so the upper bound does not do what it looks like it does. This all-covering-looking one-liner admits one version:// admits only 0.1.2-rc.1 β "<0.2.0" is inert for prereleases, and no other // comparator in the group names the 0.1.3 / 0.1.5 / 0.1.6 tuples ">=0.1.2-rc.1 <0.2.0"test/peer-range.spec.mjsrecomputes the admitted set withsemverand fails if it and this section disagree β the quoted range above is asserted against the manifest, so the two cannot drift apart.@deepseek-ai/cordis^4.0.2
Tests
npm install && npm run build && npm test
18 tests: the arithmetic, the wrapper against a driven clock (including that 500
simulated keep-alive pulses cannot re-arm a bound), and an end-to-end run through
a real cordis Context and llm/stream waterfall. test/peer-range.spec.mjs
adds 5 more that guard the peer range and the quoted requirement above.
License
MIT