DSH plugin lifecycle: Fiber states, nesting and dispose

Concepts & ArchitecturePublished 2026-10-17Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginFiberdisposelifecyclecordis
How a DSH plugin lives: six Fiber states, inject-gated apply, nested ctx.plugin fibers, and dispose cleanup order including the async trap.

A DSH plugin does not simply stay loaded once it starts; a six-state Fiber machine governs it — it waits in PENDING until dependencies are ready, enters FAILED when apply throws, and passes through UNLOADING before DISPOSED. That state machine explains the most common confusion in plugin development: "the plugin is installed but never runs" usually means inject dependencies are not satisfied and the Fiber never reached apply. The other half comes from teardown: registrations made through ctx are cleaned automatically, but asynchronous cleanup has no ordering guarantee, so ordered teardown has to be composed yourself.

DSH plugin Fiber states: six states and three transitions

Bottom line: the six states answer one question — can this plugin run, where did it fail, and how far did unload get.

StateMeaningTypical cause
PENDINGDeclared, dependencies not readyinject services not provided yet
LOADINGDependencies ready, running applyNormal load
ACTIVEPlugin runningLoad succeeded
FAILEDapply threwSynchronous errors, initialization failure
UNLOADINGReleasing resourcesdispose, dependency loss, hot reload
DISPOSEDFully unloadedTeardown finished
  1. Normal path: PENDING → LOADING → ACTIVE.
  2. Failure path: LOADING → FAILED, where apply threw.
  3. Unload path: ACTIVE → UNLOADING → DISPOSED, only after cleanup finishes.

Diagnostic rule: when "the plugin does nothing," check which state its Fiber is in — PENDING is a dependency problem, FAILED is a code problem, and the fixes differ completely.

DSH plugin inject-gated loading: when apply actually runs

Bottom line: a DSH plugin declaring inject waits for every required service, so "not running" often means queued on a dependency rather than not installed.

ts
export const inject = ['tools', 'llm']

export function apply(ctx: Context) {
  // ctx.tools and ctx.llm are guaranteed ready here
}
  1. Declare real dependencies only. Every unrelated service in inject is another dependency to wait for.
  2. Confirm the provider is loaded. If the plugin providing the service is absent, you stay in PENDING forever.
  3. Know what dependency loss does. If a providing service disappears (for example during a provider swap), the DSH plugin is unloaded automatically (ACTIVE → DISPOSED) and reloads when the service returns — this is by design.
  4. Verify: temporarily remove inject and run once. If the plugin runs, the problem is the dependency, not the code.

DSH plugin nested contexts: ctx.plugin and child Fibers

Bottom line: ctx.plugin() creates a child plugin with its own Fiber that shares the parent context and unloads with it — the lightest way to split internal responsibilities.

  1. Register a child:
ts
export function apply(ctx: Context) {
  ctx.plugin(childPlugin)   // the child owns its own Fiber
}
  1. Understand inheritance: the child inherits the parent context but has an independent lifecycle; unloading the parent recursively unloads the child, and the child can also be disposed early.
  2. Use it to split internals: when one plugin holds several capabilities, separate their state machines with child plugins instead of piling logic into one apply.
  3. Know the boundary: a child plugin is not another package and shares the process and context; real isolation needs profiles or separate plugin packages.

DSH plugin dispose: cleanup order and the concurrency trap

Bottom line: dispose clears registrations, recursively unloads children and waits for async cleanup; but async disposers run concurrently, so ordered teardown belongs in one effect.

  1. What is cleaned automatically: ctx.on listeners, ctx.tools.register tools, ctx.llm.registerAdapter adapters and any ctx.effect resource.
  2. Ordering rule: disposers start in reverse registration order.
  3. Concurrency trap: multiple asynchronous disposers run concurrently and are not guaranteed to finish one by one. Order-dependent cleanup must live inside one ctx.effect() disposer, which awaits its steps itself.
  4. Terminate an instance manually:
ts
const fiber = ctx.plugin(myPlugin)
await fiber.dispose()   // wait for cleanup to finish
  1. Verify removal: after unloading, fire the old event and call the old tool once — nothing should respond. If something does, a registration bypassed ctx.

DSH plugin lifecycle debugging: FAILED, PENDING and hot reload

Bottom line: three symptoms map to three states — not running is PENDING, errors are FAILED, unchanged behavior after edits means HMR never unloaded the old instance.

  1. Not running: check inject dependencies and their providers, not the code.
  2. Errors: README the direct cause of FAILED and look first at synchronous errors before the first await in apply, which come with little stack context.
  3. Edits not taking effect: with @deepseek-ai/dsh-hmr loaded, editing a source file runs unload → reload → new apply; without it, restart the process.
  4. Infer whether cleanup happened: automatic registration cleanup is the precondition for hot replacement; leftover registrations show up as "two copies of the logic running."
  5. A minimal check:
ts
export function apply(ctx: Context) {
  console.log('plugin loading')
  ctx.effect(() => {
    console.log('effect registered')
    return () => console.log('effect cleaned up')
  })
}

Loading prints the first two lines, unloading prints the third — this minimal chain proves the state machine and cleanup both work.

DSH plugin lifecycle troubleshooting

Bottom line: locate the state first, then discuss code — the state machine already wrote the answer down.

  1. PENDING is not a failure. It means dependencies are not ready; check service names and providers.
  2. FAILED does not retry. Reload or restart after fixing.
  3. Automatic unload after dependency loss is by design — do not treat it as a crash.
  4. Async cleanup has no ordering guarantee. Compose dependent teardown into one effect.
  5. Hot reload is conditional: only with the HMR plugin loaded, and only because registrations can be undone automatically.
  6. Repair status, stated honestly: the states, cleanup semantics and three dispose guarantees come from the official Plugins and lifecycle page; confirm exact ctx signatures against the official Cordis API page and the types of your version.

Sources: Plugins and lifecycle, Cordis core API · Context, Your first plugin.

FAQ

Why does a DSH plugin stay in PENDING — is a dependency missing or is the plugin broken?

A DSH plugin in PENDING means its declared inject dependencies are not all ready, not that its code is wrong. Check that the service names in inject are spelled correctly and that the plugins providing them are loaded; once dependencies are satisfied, the Fiber enters LOADING and runs apply.

What happens to a DSH plugin Fiber when apply throws — does it retry automatically?

A DSH plugin whose apply throws enters FAILED and is not retried automatically. Fixing the code and triggering hot reload or restarting the process is what runs the load sequence again; check synchronous errors before the first await in apply first.

Do multiple ctx.effect disposers run concurrently in a DSH plugin, and how do I order cleanup?

DSH plugin disposers start in reverse registration order, but multiple asynchronous disposers run concurrently and are not guaranteed to finish one by one. Put order-dependent cleanup inside a single ctx.effect disposer and have that disposer await its own steps.

Does disposing a DSH plugin also remove its child plugins and event listeners?

Yes, disposing a DSH plugin removes every registration it owns and recursively unloads its child plugins. The returned promise settles after all asynchronous cleanup finishes, so you can await it before continuing.

After editing DSH plugin source, why does nothing change until the HMR cycle finishes?

DSH plugin hot reload unloads the old plugin, reloads the new code, then runs the new apply. Because registrations are cleaned up automatically, hot replacement never keeps the old instance's registrations and you never call removeListener by hand.

Related Terms

Fiber
Fiber is the runtime scope Cordis creates for each loaded DSH plugin, holding its state machine and all of its registrations. It decides when apply runs, when the plugin unloads, and which side effects are cleaned up.— https://deepseek-harness.github.io/deepseek-harness/develop/framework/
PENDING
PENDING is the initial Fiber state meaning a plugin is declared but its inject dependencies are not all ready. It is the state most often mistaken for a failure, when the framework is simply waiting on dependencies.— https://deepseek-harness.github.io/deepseek-harness/develop/framework/
dispose
dispose terminates one plugin instance early: it removes all of its registrations, recursively unloads child plugins, and returns a promise that settles after asynchronous cleanup completes.— https://deepseek-harness.github.io/deepseek-harness/reference/cordis-api/context
ctx.effect
ctx.effect registers a custom cleanup for resources the framework cannot track itself, such as network connections and timers: you pass a function that returns a disposer, and the framework calls it when the plugin unloads.— https://deepseek-harness.github.io/deepseek-harness/develop/basic/

Sources