DeepSeek Harness Automatic Cleanup and ctx.effect()
Previous ChapterDeepSeek Harness Loading Local PluginsPrinting a line of log is all it does, but real plugins register listeners, tools, and timers.
This section solves one problem: when the plugin is unloaded, who cleans up these resources?
Registration is delegated to ctx, and cleanup is also delegated to ctx.
Automatic cleanup mechanism
Anything registered through ctx—event listeners, tools, timers—is automatically cleaned up when the plugin is unloaded.
You don't need to manually call removeListener or clearInterval.
The framework can automatically clean up because all registrations through ctx are recorded in the plugin's Fiber scope.
On unload, the framework revokes them in reverse order of registration.
Which operations are automatically tracked
The following operations are automatically tracked and cleaned up.
| Register Operation | Behavior during uninstallation |
|---|---|
| ctx.on(event, handler) | Event listeners are automatically removed |
| ctx.tools.register(tool) | Automatic revocation of tool registration |
| ctx.llm.registerAdapter(names, adapter) | Automatic revocation of LLM adapter registration |
| ctx.effect(() => cleanup) | Execute the returned disposer cleanup function. |
ctx.effect(): hand manual resources over to the framework.
Some resources are not in the above list, such as a network connection.
Use ctx.effect() to tell the framework how to clean it up.
ctx.effect accepts a callback. In the callback, create resources and return a cleanup function (disposer).
This disposer will be executed when the plugin is unloaded.
disposer(The cleanup function) is the return value of the ctx.effect callback.
It describes "how to destroy the resources created this time".
When the plugin is unloaded, the framework will call it.
Here is an example of a heartbeat timer:
Example
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
// Create a timer: print heartbeat every 5 seconds
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// The returned cleanup function is executed when the plugin is unloaded
// Equivalent to: you don't need to manually clearInterval in the unload logic
return () => clearInterval(timer)
})
}
If nothing needs to be done on unload, the disposer can be omitted.
But once resources such as timers and connections are created, be sure to return the corresponding cleanup function.

Details of execution order
When the plugin is unloaded, disposers are invoked in reverse order of registration.
Multiple async disposers run concurrently; there is no guarantee that they will complete one by one.
Cleanup steps that have order dependencies must be placed in the same disposer returned by a single ctx.effect(), and that disposer is responsible for serialized waiting.
Note: This rule means register first, clean up later—later-registered resources are cleaned up first.
Summary and self-test
Resources registered through ctx are automatically cleaned up on unload; for manual resources, use ctx.effect() to provide a disposer.
other extensions
1. Do timers registered through ctx need manual clearInterval?
2. When is the disposer of ctx.effect(() => disposer) executed?
3. Why should order-dependent cleanup be placed in the same effect?