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 OperationBehavior 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

// File path: scratch-plugin/src/heartbeat.ts
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.

  • 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?

other extensions