Source launch loads workspace packages twice, splitting the Typert @Remote registry - the gateway then skips the service silently #2091
allenliang2022
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Under the source launcher (
pnpm dsh, i.e.node --import tsx/esm apps/cli/src/bin.ts), a workspace package is loaded twice — once fromsrcfor harness code, once fromlibfor anything resolved outside the tsconfig scope, which includes every plugin installed under$DSH_HOME. Any registry kept in module-private state is therefore split.@deepseek-ai/dsh-typert-protocolkeeps exactly such a registry, so a plugin's@Remotemarks land in one copy while the gateway reads the other. The gateway then silently skips the service and answers with a message that blames the plugin author.This is reproducible with a controlled probe; the exact skip is one line.
Reproduction
Identity comparison rather than a behavioural probe — a behavioural one can fail for unrelated reasons (decorator emit, wrong receiver) and its empty result then looks like a positive. Each run carries a control: the same specifier imported twice must be identical.
libnode --import tsx/esm(harness)src$DSH_HOME/profiles/web, plainnode(plugin)libSame probe, same control, only the resolution context changes. Harness gets
src, plugins getlib— two instances.The plugin side resolves through
$DSH_HOME/profiles/node_modules/@deepseek-ai/dsh-typert-protocol, a junction into the checkout whoseexports["."].defaultis./lib/index.js. The harness side is remapped bytsconfig.base.json:Running the same file under plain
nodeinstead of tsx removes the remapping, and the split with it.Where it fails
packages/typert/protocol/src/index.ts:126Module-private, so one table per module instance.
packages/api/gateway/src/index.ts:244-253:The first half succeeds under the split, which is what makes this hard to diagnose.
bindTypertRemote()returns a plain frozen object carried on the instance, not module-private state — so the service is found and the namespace matches. Only line 248 touches the WeakMap, reads the wrong copy, and line 249 drops the candidate without a word.The resulting message, "no active Remote method exports this endpoint", reads as "the plugin never declared this method". The truth is "the declaration is in another copy of this module". A plugin author following that message will look in the wrong place indefinitely.
Why I think this is a defect rather than an unsupported combination
Two rules from the repository's own
AGENTS.md:Line 249 is a silent skip of a referent that was found. And the mixing is not user error:
pnpm dshis the documented source entry point, plugins under$DSH_HOMEare a supported feature, and separate-source-launch-from-build states that source launches still requirepnpm run buildto produce Typert host artifacts. The two planes are therefore expected to coexist; nothing detects when they diverge.Suggested fixes, least invasive first
1. Make the skip speak. No design change, no effect on any succeeding path. At line 248-249 the gateway already holds everything needed to distinguish the two cases: the binding matched the namespace, yet
remoteMethods()returned nothing. "Namespace claimed, zero marks" is the signature of a duplicate protocol instance and cannot be produced by a plugin that simply forgot a decorator on that method — such a plugin still exposes its other marks. Naming the likely cause here would have turned this from a multi-hour trace into a log line.2. Detect the duplication directly. A module-scoped identity token compared once at gateway boot, or a version/identity field on the binding, reports "two instances of dsh-typert-protocol are loaded" before any request arrives.
3. Make duplication harmless. Move
markersto a realm-scoped registry (globalThis+Symbol.for('…')), which is the usual remedy for a cross-package registry that cannot guarantee single instantiation. Module-private state is the right default when one-instance-per-realm holds; here the product's own launcher breaks that invariant.Scope of what I verified
Verified on this machine: the two resolution contexts and their identity results above; the tsconfig mapping; the junction and its
exports; the WeakMap at protocolindex.ts:126; the gateway path atindex.ts:244-253including the error text.Not verified: that a plugin is hitting this in the current release.
dshmarketis immune — it declares no Typert dependency and mounts raw HTTP routes under/dsh-market/*. I did not audit other published plugins. So this is a demonstrated mechanism, not an observed live outage, and the impact depends on how many plugins use Typert@Remote.Also unverified: whether the team considers source launch plus third-party plugins a supported combination. If it is not, fix 1 still applies — the diagnostic would say so instead of misattributing the failure.
Disclosure
I misdiagnosed this same subsystem in an earlier report, claiming the gateway route table was a build-time artifact that third-party plugins could not extend. That was wrong:
resolveSrcDescriptorscans live services at runtime, and it is the very function quoted above. I am flagging it so the claims here get the scrutiny that history earns — everything above is either a quoted line or a probe result, and the probe carries a control precisely because my previous error came from trusting an unvalidated measurement.All reactions