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.
📋 Prerequisites: Completed 14-inject.md, understand inject dependency declarations
1. What You'll Learn
- Dependency graph and topological sorting
- Auto-loading when dependencies are ready
- Hot Module Replacement (HMR) mechanism
- Nested Context
- Config-change-triggered reload
- HMR best practices for development
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):
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:
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:
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
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:
// 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
export function apply(ctx: Context) {
if (someCondition) {
ctx.provide('optional-service', impl)
// Pending plugins depending on optional-service now auto-activate
}
}
(3) ▶ Example 3
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:
[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:
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
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
- File system watcher detects
src/index.tschange - Old plugin's Fiber enters disposing state
- All cleanup functions execute (ctx.effect, auto-cleanup)
- New code compiles and loads
- New Fiber created, enters pending/active
- 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:
tools plugin HMR reload → my-tool (depends on tools) also reloads
This ensures dependency consistency, but can cause "reload storms":
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:
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
Parent ctx: { tools, llm, sessions }
Child ctx: { tools(overridden), cache(added) }
Child ctx sees: { tools(overridden version), llm, sessions, cache }
- Service lookup: check child ctx first, then parent ctx (prototype chain pattern)
- Event propagation: child ctx events bubble up to parent ctx
- Resource cleanup: child ctx destruction doesn't affect parent ctx
(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:
// ❌ 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:
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:
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:
- inject list changes
- Port number changes
- Service registration parameter changes
(4) Reload and Persistence
Config changes are persisted to cordis.yml after reload:
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:
// ✅ 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
// ❌ 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:
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:
# 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
// 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
[hmr] loading xxx (new) and [xxx] plugin reloaded. Failures output error messages.--watch flag is only for development.📖 Summary
- Dependency graph is built as a DAG from inject declarations; topological sorting determines load order
- Dynamic dependency satisfaction: pending plugins auto-activate when dependencies are ready
- HMR enabled via
--watch; code changes auto-reload plugins - Nested contexts inherit parent services, supporting overrides and isolation
- Config changes trigger automatic reload; some changes can hot-update
- HMR best practices: idempotent apply, avoid global state, complete cleanup functions
📝 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.