DeepSeek Harness: Dependency-Driven Loading and Hot Reload

Last updated: 2026-08-31

From "manually controlling load order" to "declare dependencies, framework handles the rest" — dependency-driven loading means developers only declare relationships. Combined with Hot Module Replacement (HMR), code changes take effect without restarting. The development experience improves dramatically.

💡 Tip: Dependency-driven + HMR = "change and it takes effect." You only declare dependency relationships; the framework handles load order. You only save code; the framework handles hot reload. The core productivity gain is eliminating "waiting for restart" time.

📋 Prerequisites: Completed 14-inject.md, understand inject dependency declarations

1. What You'll Learn


Unload Reload

2. Dependency Graph and Topological Sorting

(1) Building the Dependency Graph

At startup, the framework traverses all plugins' inject declarations and builds a Directed Acyclic Graph (DAG):

TYPESCRIPT
function buildDependencyGraph(plugins: Plugin[]) {
  const graph = new DAG()
  for (const plugin of plugins) {
    graph.addNode(plugin.name)
    for (const dep of plugin.inject) {
      graph.addEdge(dep, plugin.name)
    }
  }
  return graph
}

(2) Topological Sorting

Topological sorting determines load order:

TEXT 📖 Display only
Plugin declarations:
  core:    inject = []
  tools:   inject = ['core']
  llm:     inject = ['core']
  my-tool: inject = ['tools', 'llm']

Dependency graph:
  core → tools → my-tool
  core → llm   → my-tool

Topological sort result: [core, tools, llm, my-tool]

(3) Parallel Loading

Plugins with no dependency relationships load in parallel:

100%
graph TB
    subgraph Phase1[Phase 1]
        CORE[core]
    end
    subgraph Phase2[Phase 2 - Parallel]
        TOOLS[tools]
        LLM[llm]
    end
    subgraph Phase3[Phase 3]
        MYTOOL[my-tool]
    end
    CORE --> TOOLS
    CORE --> LLM
    TOOLS --> MYTOOL
    LLM --> MYTOOL

(4) ▶ Example 4

100%
graph TB
    CORE[core] --> TOOLS[tools]
    CORE --> SESSIONS[sessions]
    CORE --> LOGGER[logger]
    TOOLS --> MY_TOOL[my-tool]
    SESSIONS --> MY_TOOL
    LOGGER --> TRAJECTORY[trajectory]
    SESSIONS --> TRAJECTORY

3. Auto-Loading When Dependencies Are Ready

(1) Dynamic Dependency Satisfaction

Plugins don't need all dependencies satisfied at startup. When a dependency service is registered later, pending plugins automatically activate:

TYPESCRIPT
// Plugin A: inject = ['tools'] — tools not yet registered
// → Fiber state: pending

// Later, tools plugin loads and registers service
// → Plugin A's Fiber auto-transitions to active, calls apply

(2) ▶ Example 2

TYPESCRIPT
export function apply(ctx: Context) {
  if (someCondition) {
    ctx.provide('optional-service', impl)
    // Pending plugins depending on optional-service now auto-activate
  }
}

(3) ▶ Example 3

100%
sequenceDiagram
    participant F as Framework
    participant A as Plugin A (inject: tools)
    participant T as Tools Plugin
    
    F->>A: Register → pending (tools not ready)
    F->>T: Register → active
    T->>F: Register tools service
    F->>A: Dependency ready → active
    A->>F: apply() executes

(4) Permanently Unsatisfiable Dependencies

If a declared required dependency can never be satisfied:

TEXT 📖 Display only
[warn] plugin my-plugin has unsatisfied dependency: nonexistent-service
[warn] my-plugin will remain in pending state

The plugin doesn't error — it just stays in pending forever. Optional dependencies (with ?) produce no warning when unsatisfied.


4. Hot Module Replacement (HMR) Mechanism

(1) HMR Concept

Hot Module Replacement allows replacing plugin code at runtime without restarting the entire DSH:

100%
graph LR
    CHANGE[Code change] --> DETECT[File change detection]
    DETECT --> DISPOSE[Old Fiber disposing]
    DISPOSE --> LOAD[New code loaded]
    LOAD --> ACTIVE[New Fiber active]

(2) Enabling HMR

BASH
pnpm dsh web --patch --watch

The --watch flag enables file watching; when plugin source code changes, it automatically triggers a reload.

(3) Complete HMR Flow

  1. File system watcher detects src/index.ts change
  2. Old plugin's Fiber enters disposing state
  3. All cleanup functions execute (ctx.effect, auto-cleanup)
  4. New code compiles and loads
  5. New Fiber created, enters pending/active
  6. Dependent plugins reload as needed

(4) HMR Limitations

Scenario HMR Support Notes
Modify execute function Tool logic hot-updates
Modify Config Configuration re-validated
Modify inject ⚠️ May trigger cascading reload
Modify name Requires manual restart
Modify dependency versions Requires manual restart

(5) Cascading Reload

When a depended-upon plugin reloads, plugins depending on it also reload:

TEXT 📖 Display only
tools plugin HMR reload → my-tool (depends on tools) also reloads

This ensures dependency consistency, but can cause "reload storms":

TEXT 📖 Display only
core reload → tools reload → my-tool reload → ... (entire dependency chain reloads)

5. Nested Context

(1) Context Hierarchy

Cordis supports nested contexts — child contexts inherit parent context services but can override them:

TYPESCRIPT
export function apply(ctx: Context) {
  const childCtx = ctx.extend({
    // Override or add services
  })
  
  childCtx.plugin({
    name: 'child-plugin',
    apply(innerCtx) {
      // innerCtx inherits ctx's services
    }
  })
}

(2) Context Inheritance Rules

TEXT 📖 Display only
Parent ctx: { tools, llm, sessions }
Child ctx: { tools(overridden), cache(added) }

Child ctx sees: { tools(overridden version), llm, sessions, cache }

(3) Nested Context Use Cases

Scenario Description
Session isolation Each session has an independent ctx
Request scope Each request creates a temporary ctx
Testing Create isolated test contexts
Multi-Agent Each Agent has an independent tool set

(4) Nesting Depth

Theoretically unlimited, but overly deep nesting impacts performance:

TYPESCRIPT
// ❌ Too deep
ctx.extend().extend().extend().extend()

// ✅ Moderate nesting
const sessionCtx = ctx.extend({ session })

6. Config-Change-Triggered Reload

(1) Automatic Reload

When users modify plugin configuration in the Web UI, the framework automatically triggers a reload:

100%
graph LR
    UI[Web UI modifies config] --> VALID[Schema validation]
    VALID --> OLD[Old Fiber disposing]
    OLD --> NEW[New Config + New Fiber]
    NEW --> ACTIVE[Fiber active]

(2) Partial Config Hot Update

Some config changes don't require a full reload:

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.on('config/updated', (newConfig) => {
    if (newConfig.debug !== ctx.config.debug) {
      ctx.logger.level = newConfig.debug ? 'debug' : 'info'
    }
  })
}

(3) Configs Requiring Full Reload

These config changes require a complete reload:

(4) Reload and Persistence

Config changes are persisted to cordis.yml after reload:

YAML
plugins:
  my-plugin:
    config:
      debug: true  # User modified via Web UI, auto-persisted

7. HMR Best Practices for Development

(1) Keep apply Idempotent

The apply function should be idempotent — multiple calls produce consistent results:

TYPESCRIPT
// ✅ Idempotent: each apply registers the same tool
export function apply(ctx: Context) {
  ctx.tools.register(fileCountTool)
}

// ❌ Non-idempotent: apply accumulates side effects
let counter = 0
export function apply(ctx: Context) {
  counter++  // Counter increments after reload
}

(2) Avoid Global State

TYPESCRIPT
// ❌ Global state: old state persists after HMR reload
const globalCache = new Map()

// ✅ Closure state: each apply creates new state
export function apply(ctx: Context) {
  const cache = new Map()
  ctx.effect(() => () => cache.clear())
}

(3) Complete Cleanup Functions

During HMR reload, old Fiber cleanup functions must completely clean up all resources:

TYPESCRIPT
export function apply(ctx: Context) {
  const ws = new WebSocket('ws://localhost:8080')
  
  // ✅ Register cleanup
  ctx.effect(() => () => ws.close())
  
  // ❌ Forgot cleanup → old connection leaks after reload
}

(4) Development Workflow

Alice's recommended HMR development loop:

BASH
# 1. Start development mode with HMR
pnpm dsh web --patch --watch

# 2. Write code normally, auto-reloads on save
# Terminal output:
# [hmr] file changed: src/index.ts
# [hmr] disposing my-plugin (old)
# [hmr] loading my-plugin (new)
# [my-plugin] plugin reloaded

# 3. Check reload logs, confirm no cleanup errors

(5) HMR Debugging Tips

TYPESCRIPT
// Add debug log at the start of apply
export function apply(ctx: Context) {
  ctx.logger.info('apply called at', new Date().toISOString())
  // ...
}
// If you see apply called unexpectedly multiple times, it means HMR cascading reload

❓ FAQ

Q What's the difference between HMR and manual restart?
A HMR only reloads changed plugins and their dependency chains; other plugins are unaffected. Manual restart reloads all plugins, taking longer.
Q Are sessions lost during HMR reload?
A No. Session data is managed by the sessions service, which isn't affected by HMR. Only the state of reloaded plugins is lost.
Q How to tell if a reload succeeded?
A Check terminal logs. Successful reloads output [hmr] loading xxx (new) and [xxx] plugin reloaded. Failures output error messages.
Q What if cascading reloads are too frequent?
A Check if you're unnecessarily depending on low-level plugins (like core). If a tool plugin only depends on tools, it won't cascade when core reloads.
Q HMR behavior in nested contexts?
A When a plugin in a child context reloads, only plugins within that child context are affected; the parent context is unaffected.
Q Should I use HMR in production?
A No. HMR is a development tool; production should use stable loading. The --watch flag is only for development.

📖 Summary


📝 Exercises

1. ⭐ Basic: Start pnpm dsh web --patch --watch, modify a loaded plugin's apply function (add a log line), save and observe the HMR reload messages in the terminal.

2. ⭐⭐ Intermediate: Create two plugins with a dependency relationship (A inject B). Start HMR, modify B's code, and observe whether A cascadingly reloads. Then modify only A's code and confirm B is unaffected.

3. ⭐⭐⭐ Challenge: Write a plugin using global variables (non-idempotent). After HMR reload, observe variable value changes. Then refactor to closure state (idempotent) and verify consistent behavior after reload. Record the terminal output comparison before and after refactoring.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏