The reconnect supervisor used connection lost for every transition into backoff, including an initial startup attempt that never established a connection and later retry attempts that also failed. That wording implied a previously healthy generation and obscured whether any tools had ever been registered.
Capture whether the generation had reached the established state before scheduling recovery. Established disconnects retain connection lost/reconnecting; startup and retry failures now report connection failed/retrying. The reconnect-disabled diagnostic uses the same distinction while preserving its concrete manual-recovery guidance.
Unit assertions cover established loss, initial failure, retry failure, and both reconnect-disabled branches. Focused package coverage remains 100%, and the bilingual Agent Note records the observable state vocabulary.
The supervisor selected strict startup registration with a shared isFirstSync flag. Because the MCP SDK may deliver tools/list_changed before connect() resolves, that notification could enter the sync queue first, consume the strict option inside its contained handler, and leave the actual activation sync non-fatal.
Pass startup intent explicitly to connectGeneration(). Only the plugin activation attempt receives the failOnStartupError registration policy; notification-driven syncs and later reconnect generations always use contained runtime semantics. Queue arrival order can no longer redefine startup behavior.
A regression test injects list_changed from inside connect(), keeps a foreign namespace squatter in place, and proves activation still rejects after the notification's contained sync. Focused package coverage remains 100%, and the bilingual reconnect note records the ownership rule.
The MCP SDK starts a fire-and-forget close when initialization fails. Its stdio transport clears its process field before that close finishes, so our second Client.close() could return immediately and the reconnect timer could launch a replacement while the original child was still alive.
Track the transport onclose signal for every client generation and gate failed-attempt backoff on both Client.close() settlement and that signal. Use the same barrier during plugin disposal. If the SDK's bounded stdio termination window expires without onclose, fail closed and report incomplete shutdown instead of risking overlapping server processes.
Regression coverage models the SDK's early-returning second close, delayed and missing close signals, pending-connect disposal, close rejection, and the terminal timeout path. The reconnect Agent Note and Chinese counterpart now record the quiescence contract.
Integrate master at 8b4ef532 before addressing review feedback so each bot issue remains isolated in its own descendant commit. Resolve the vendor package rescope by moving the PR's new MCP supervisor imports to @deepseek-ai/cordis and @deepseek-ai/schemastery, preserve the reconnect implementation and e2e coverage, and regenerate the merged config and module catalogs with fresh bilingual pairing records.
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
A per-instance connection supervisor restarts the original server config
with exponential backoff when the transport closes, re-runs tool discovery
on success, and atomically replaces the previous generation. Default policy
retries for ~2.5 minutes (10 attempts, 500ms→30s doubling) before giving up
and unregistering the server's tools.
New config block reconnect { enabled, initialDelayMs, maxDelayMs, maxAttempts }
on both transports; misconfiguration fails plugin load. A connection that
survives past the stability window (maxDelayMs) resets the attempt budget,
so occasional crashes recover indefinitely while a crash loop still exhausts
the cap.
Integrates with the upstream failOnStartupError: the initial sync uses
registrationFailure:'throw' when that flag is set so a squatted namespace
still rejects activation.
Fixes#1746
Delete design-session citations (decision/audit/plan ordinals, stack
positions), change narration, review choreography, and reviewer-addressed
justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and
generator templates; restate every affected fact as current-state contract
prose. Fix generated docs at their sources and regenerate the catalogs and
cordis-surface regions; re-paste type-equiv blocks; update every bilingual
counterpart and re-record the pairs. Record the citation rule in the
committed-artifact-citations Agent Note.
ds-review-bot round 1 on the repository-plugin runtime:
- a manifest-declared skill root absent or non-directory in the installed
package now fails the plugin load (skill-local treats a missing root as
legitimately empty, which silently mounted a skill-less plugin)
- includeDefaultRoots: false no longer inherits $DSH_BUNDLED_SKILL_DIR, so
isolated repository providers see only their explicit roots
- prepared wrapper baseUrl schema requires the file: scheme, failing hostile
URLs at the declared validation boundary
- preparedPath reuses format.ts's isOutside; SERVER_NAME_PATTERN is exported
and pinned equal to dsh-mcp-client's, with the restatement justified (the
prepare bin keeps a zod-only module graph); the unexplained `as never`
cast now carries its schemastery rationale
- the import-free wrapper assertion also rejects dynamic import(
- the headless fixture wrapper is regenerated by the real prepareDshPlugin
and a drift test pins fixture == generator output
- prepareDshPlugin JSDoc states the non-atomic publish repair contract
Review direction (tianyicui, PR #660): in a stacked PR, change all other
process-running places to use the new service.
- lsp-local: LspConnection spawns through ctx.subprocess (piped protocol
streams + a no-spill collected stderr tail); its private process-tree
helpers (POSIX group signalling, Windows taskkill, liveness polling) are
deleted in favor of the seam's handle verbs, and its buildChildEnv now
rides scrubbedParentEnv (LSP children also stop inheriting stale DSH_*).
The plugin injects 'subprocess'; compositions/tests mount
dsh-subprocess-local.
- subagent-acp: the ACP child spawns through the seam (piped ndjson streams,
inherited stderr); spawn failure surfaces through done-rejection into the
same startup race; disposal is handle.dispose with the plugin's configured
graces. dsh-subagent-subprocess is DELETED — its dispose ladder and scrub
are the seam's, and the isolated-config-dir helper had no consumer.
- mcp-client, pty-local, sdk-helper: adopt scrubbedParentEnv as the one
scrub definition (their spawns stay put by ownership: the MCP SDK and
node-pty own those calls; the SDK wizard runs outside any composition).
- Coverage: per-file 100% over every touched src file, with each v8 ignore
carrying a platform or contract reason; new suites cover stdio
dispositions, the dispose ladder tiers, injected-win32 tree semantics,
waitForExit, settled-kill/terminate no-ops, and spawn-failure disposal.
- Docs: consumer-migration Agent Note (en; zh follows in this PR), seam note
updated in place, subprocess.md rewritten for the reshaped vocabulary
(type-equiv re-registered), READMEs and SERVICE_ROLES updated, taskkill
added to knip ignoreBinaries.
Research across 8 multi-server agent clients (Claude Code, Codex, Gemini
CLI, VS Code, Cline, Roo Code, Goose, OpenCode) showed all of them keep
the server namespace in model-facing MCP tool names; the RFC's premise
for raw names ("servers already prefix their tools") is false for the
official GitHub/filesystem/Sentry servers.
- Config: drop toolPrefix; require serverName ([A-Za-z0-9_-]{1,32}),
duplicate serverName fails the later instance at load (per-root
reservation, released on dispose)
- Names: always mcp__<serverName>__<rawName>; normalize to the DeepSeek
64-char [A-Za-z0-9_-] contract with a deterministic 12-hex identity
hash on lossy normalization; raw name is the only thing sent on the
wire (tools/call)
- Sync: two-phase fetch/swap — fetch failure keeps the previous
generation; a swap conflict rolls back the whole generation (never a
partial set); duplicate raw names reject the tool list
- RFC: moved to implemented/ (status + skeleton rewritten per the
format contract), naming design + tier-level test coverage recorded
- Tests: naming algorithm unit suite; keyless Streamable HTTP e2e
against an in-process StreamableHTTPServerTransport (namespace
discovery, execution, per-request auth headers); dotted-name
normalization e2e via a new fixture tool
Connects to an external MCP server and registers its tools on
ctx.tools. Supports stdio (child process) and Streamable HTTP
transports. Credential-shaped env vars are scrubbed before forwarding
to child processes.
- Plugin lifecycle: connect, sync tools, re-sync on ToolListChanged,
dispose unregisters and closes
- Full JSDoc on all exports (@param/@returns on functions)
- 100% per-file coverage (apply lifecycle, args coercion, env scrubbing)
- Config catalog regenerated
- syncTools: on paginated listTools failure, unregister any tools already
registered in the current sync before rethrowing (prevents orphans)
- Effect disposer: call client.close() directly without awaiting startup
completion — aborts a hanging connect promptly on HMR/dispose
- syncTools: on paginated listTools failure, unregister any tools already
registered in the current sync before rethrowing (prevents orphans)
- Effect disposer: call client.close() directly without awaiting startup
completion — aborts a hanging connect promptly on HMR/dispose
- Add client.onclose handler to unregister tools when the MCP server
disconnects (crash or intentional close)
- Replace bare `let disposers` with a shared mutable state object so the
effect disposer and notification handler always reference the current
generation
- Serialize tools/list_changed resyncs with latest-wins coalescing
(syncing + pendingResync flags) to prevent concurrent races
Connects to an external MCP server and registers its tools on
ctx.tools. Supports stdio (child process) and Streamable HTTP
transports. Credential-shaped env vars are scrubbed before forwarding
to child processes.
- Plugin lifecycle: connect, sync tools, re-sync on ToolListChanged,
dispose unregisters and closes
- Full JSDoc on all exports (@param/@returns on functions)
- 100% per-file coverage (apply lifecycle, args coercion, env scrubbing)
- Config catalog regenerated