Plugin version: dsh-opencode-session-header@0.1.0
Harness: DeepSeek Harness (@deepseek-ai/dsh) 0.1.5-rc.2
Runtime: Node.js v24.15.0, Windows 11 (10.0.26200)
Severity: blocks boot — the whole plugin tree fails to load
Summary
With dsh-vision-router present in the same profile, mounting this plugin makes the
harness fail to boot with RangeError: Maximum call stack size exceeded. The repeating
stack frame is this plugin's own fetch middleware:
Exception in PromiseRejectCallback:
file:///.../node_modules/dsh-opencode-session-header/lib/index.js:171
return next(input, { ...init, headers })
RangeError: Maximum call stack size exceeded
Removing either plugin makes the profile boot cleanly.
Reproduction (bisected, minimal)
A profile whose dsh.profile.bundles contains only:
["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-opencode-session-header"]
boots fine. Adding dsh-vision-router@2.2.0 to that minimal list makes it crash
immediately at boot (no LLM request is involved — both plugins issue network calls during
apply(), and the recursion starts there).
Bisect log (profile had 41 non-core plugins; each round was a restart):
| Round |
plugins enabled |
result |
| 1 |
0–20 (21 plugins) |
crash |
| 2 |
0–10 |
crash |
| 3 |
0–5 |
crash |
| 4 |
0–2 |
crash |
| 5 |
dsh-vision-router alone |
crash |
| control |
same set, vision-router removed |
clean boot |
Root cause (strong indication)
The two plugins make mutually incompatible assumptions about globalThis.fetch:
A. This plugin treats any external reassignment of globalThis.fetch as its new
underlying fetch. lib/index.js, installFetchPipeline():
const prevDesc = Object.getOwnPropertyDescriptor(globalThis, 'fetch')
state.patchedFetch = compose(state)
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
enumerable: prevDesc?.enumerable ?? true,
get() { return state.patchedFetch },
set(newFetch) {
if (newFetch === state.patchedFetch) return
prevDesc?.set?.call(globalThis, newFetch)
state.setUnderlyingFetch(newFetch) // ← captures the foreign wrapper
state.patchedFetch = compose(state)
},
})
(and ensureFetchPipeline() captures let underlyingFetch = globalThis.fetch at first use).
B. dsh-vision-router replaces globalThis.fetch with a wrapper whose original is
whatever globalThis.fetch was at wrap time. lib/pi-ai-bridge-wire-compat.js:
const original = globalThis.fetch // line 286
...
if (!active) return Reflect.apply(original, globalThis, [input, init]) // early path taken at boot
...
globalThis.fetch = wrapped // line 337
return () => { if (globalThis.fetch === wrapped) globalThis.fetch = original } // line 340
Interleaved at boot, this closes a cycle:
- this plugin installs its accessor;
underlying = native fetch
- vision-router wraps and reads
original → this plugin's patched fetch
globalThis.fetch = wrapped fires this plugin's setter → underlying = wrapped
- patched fetch → middleware →
next → underlying (= wrapped)
wrapped (inactive path) applies original → this plugin's patched fetch → step 4
Note that each side is defensible alone: vision-router does capture its original and does
restore on dispose, and this plugin's own tests pass (13/13) — the failure only exists when
the two are combined. Vision-router also has a second, dynamic-injection pattern
(index.js:820 and lib/ollama-cold-start.js:115: fetchImpl = (...args) => globalThis.fetch(...args))
which is the same class of hazard.
Why this plugin's own tests don't catch it
tests/smoke.mjs exercises createSessionHeaderMiddleware() against a stub next and never
installs the pipeline on a live globalThis.fetch. I wrote an independent probe that installs
the middleware through your exported registerFetchMiddleware() and then performs two real
fetch() calls (one matching opencode.ai, one not) — it completed without recursion, i.e.
the install path is sound in isolation. The cycle needs a second global-fetch writer.
Suggested directions
- Don't adopt external assignments as the underlying fetch: keep the reference captured at
install time and treat a foreign assignment as "someone else now owns globalThis.fetch"
(e.g. re-read prevDesc/skip re-capture, or stop wrapping and log a warning).
- Or make
next() re-entrancy-safe (depth counter or an in-flight marker) so a wrapper that
re-enters the patched fetch degrades instead of overflowing.
- Longer term, the harness could offer an official seam (an injected
ctx.http / a documented
fetch-pipeline API) so plugins stop racing over globalThis.fetch.
Happy to test a patch — the bisect setup above reproduces in under a minute.
Plugin version:
dsh-opencode-session-header@0.1.0Harness: DeepSeek Harness (
@deepseek-ai/dsh)0.1.5-rc.2Runtime: Node.js v24.15.0, Windows 11 (10.0.26200)
Severity: blocks boot — the whole plugin tree fails to load
Summary
With
dsh-vision-routerpresent in the same profile, mounting this plugin makes theharness fail to boot with
RangeError: Maximum call stack size exceeded. The repeatingstack frame is this plugin's own fetch middleware:
Removing either plugin makes the profile boot cleanly.
Reproduction (bisected, minimal)
A profile whose
dsh.profile.bundlescontains only:boots fine. Adding
dsh-vision-router@2.2.0to that minimal list makes it crashimmediately at boot (no LLM request is involved — both plugins issue network calls during
apply(), and the recursion starts there).Bisect log (profile had 41 non-core plugins; each round was a restart):
Root cause (strong indication)
The two plugins make mutually incompatible assumptions about
globalThis.fetch:A. This plugin treats any external reassignment of
globalThis.fetchas its newunderlying fetch.
lib/index.js,installFetchPipeline():(and
ensureFetchPipeline()captureslet underlyingFetch = globalThis.fetchat first use).B. dsh-vision-router replaces
globalThis.fetchwith a wrapper whoseoriginaliswhatever
globalThis.fetchwas at wrap time.lib/pi-ai-bridge-wire-compat.js:Interleaved at boot, this closes a cycle:
underlying = native fetchoriginal→ this plugin's patched fetchglobalThis.fetch = wrappedfires this plugin's setter →underlying = wrappednext→underlying(=wrapped)wrapped(inactive path) appliesoriginal→ this plugin's patched fetch → step 4Note that each side is defensible alone: vision-router does capture its original and does
restore on dispose, and this plugin's own tests pass (13/13) — the failure only exists when
the two are combined. Vision-router also has a second, dynamic-injection pattern
(
index.js:820andlib/ollama-cold-start.js:115:fetchImpl = (...args) => globalThis.fetch(...args))which is the same class of hazard.
Why this plugin's own tests don't catch it
tests/smoke.mjsexercisescreateSessionHeaderMiddleware()against a stubnextand neverinstalls the pipeline on a live
globalThis.fetch. I wrote an independent probe that installsthe middleware through your exported
registerFetchMiddleware()and then performs two realfetch()calls (one matchingopencode.ai, one not) — it completed without recursion, i.e.the install path is sound in isolation. The cycle needs a second global-fetch writer.
Suggested directions
install time and treat a foreign assignment as "someone else now owns
globalThis.fetch"(e.g. re-read
prevDesc/skip re-capture, or stop wrapping and log a warning).next()re-entrancy-safe (depth counter or an in-flight marker) so a wrapper thatre-enters the patched fetch degrades instead of overflowing.
ctx.http/ a documentedfetch-pipeline API) so plugins stop racing over
globalThis.fetch.Happy to test a patch — the bisect setup above reproduces in under a minute.