DeepSeek Harness: Plugin Lifecycle: Fiber State Machine
Last updated: 2026-08-31
Every plugin in Cordis isn't a static code block, but a living entity with a lifecycle — a Fiber. Understanding the Fiber state machine means understanding the full journey from "waiting to load" to "running" to "graceful exit," which is the key to writing robust plugins.
📋 Prerequisites: Completed 13-effect.md, understand auto-cleanup and ctx.effect()
1. What You'll Learn
- Fiber concept: a plugin's state container
- Lifecycle: pending → active → disposing → disposed
- Fiber creation and destruction
- Parent-child Fiber relationships
- Error handling and state rollback
- Fiber context isolation
2. Fiber Concept
(1) What is a Fiber
Fiber is Cordis's abstraction of a plugin's runtime state — each plugin instance corresponds to a Fiber object that records which lifecycle stage the plugin is currently in.
interface Fiber {
id: string
name: string
state: FiberState
context: Context
parent: Fiber | null
children: Fiber[]
}
(2) Why Fiber is Needed
Without Fiber, plugins only have two states: "loaded" and "unloaded." But in practice:
- When dependencies aren't ready, a plugin should "wait" rather than "fail"
- During unloading, a plugin needs to be "cleaning up" rather than just disappearing
- On error, a plugin might need to "retry" rather than "die"
Fiber provides a clear state machine model for these intermediate states.
(3) ▶ Example 3
Fiber = plugin's "soul" (state information)
Context = plugin's "body" (resources and environment)
Each Fiber holds a Context; the Context's lifecycle is bound to the Fiber.
3. Lifecycle States
(1) ▶ Example 1
stateDiagram-v2
[*] --> pending: Plugin registered
pending --> active: Dependencies ready + apply succeeds
pending --> errored: apply fails
active --> disposing: Unload requested
errored --> active: Retry succeeds
errored --> disposing: Retry abandoned
disposing --> disposed: Cleanup complete
(2) State Meanings
| State | Meaning | ctx Available | Reversible |
|---|---|---|---|
| pending | Waiting for dependencies | Limited | ✅ |
| active | Running normally | Fully | ✅ |
| disposing | Cleaning up | Read-only | ❌ |
| disposed | Destroyed | Unavailable | ❌ |
| errored | Startup failed | Unavailable | ✅ |
(3) pending State
When a plugin declares inject but dependencies aren't registered yet, Fiber enters pending:
export const inject = ['tools']
// tools service not yet registered → Fiber is pending
// tools registered → Fiber transitions to active, calls apply
During pending:
- The plugin's apply hasn't been called yet
- Dependency services on ctx are unavailable
- Automatically transitions to active once dependencies are ready
(4) active State
After apply executes successfully, Fiber enters active:
export function apply(ctx: Context) {
// Fiber is now active
ctx.logger.info('I am alive!')
ctx.on('session/created', (s) => {
// Can use all services normally
})
}
During active:
- All declared services are available
- Can register new resources and listeners
- Can respond to events and commands
(5) disposing State
After receiving an unload request, Fiber enters disposing and begins cleaning up resources:
disposing process:
1. Stop accepting new requests
2. Execute ctx.effect() cleanup functions in reverse order
3. Auto-remove event listeners
4. Clear timers
5. Unregister commands and services
(6) disposed State
After all resources are cleaned up, Fiber enters disposed:
- ctx is no longer available
- Any call to ctx will throw an error
- Fiber object is retained for auditing and logging
4. Fiber Creation and Destruction
(1) Creation Timing
Fiber is automatically created when a plugin is registered:
// Framework internal logic (pseudocode)
function registerPlugin(plugin: PluginDefinition) {
const fiber = new Fiber({
id: generateId(),
name: plugin.name,
state: hasDeps(plugin) ? 'pending' : 'active'
})
if (fiber.state === 'active') {
fiber.context = createContext(fiber)
plugin.apply(fiber.context)
}
}
(2) Destruction Triggers
Fiber destruction is triggered by:
| Trigger | Description |
|---|---|
ctx.dispose() |
Active unload |
| Parent Fiber destroyed | Cascading unload |
| Config change reload | Old Fiber destroyed, new Fiber created |
(3) ▶ Example 3
graph TD
TRIGGER[Unload trigger] --> STOP[Stop new requests]
STOP --> CHILD[Destroy child Fibers]
CHILD --> CLEAN[Execute cleanup functions]
CLEAN --> DISPOSED[Mark disposed]
5. Parent-Child Fiber Relationships
(1) Hierarchical Structure
Fibers support parent-child relationships, forming tree structures:
graph TD
ROOT[Root Fiber<br/>dsh-core] --> A[Plugin A Fiber]
ROOT --> B[Plugin B Fiber]
A --> A1[Sub-plugin A1]
A --> A2[Sub-plugin A2]
(2) Establishing Parent-Child Relationships
Create child Fibers via ctx.plugin():
export function apply(ctx: Context) {
ctx.plugin({
name: 'sub-plugin',
apply(subCtx: Context) {
subCtx.logger.info('I am a child fiber')
}
})
}
(3) Cascading Destruction
When a parent Fiber is destroyed, all child Fibers are automatically destroyed:
Unload Plugin A:
→ First destroy Sub-plugin A1
→ Then destroy Sub-plugin A2
→ Finally destroy Plugin A
This "children before parent" destruction order ensures dependencies aren't broken.
(4) Scope Inheritance
Child Fibers inherit the parent Fiber's ctx:
// Services registered by parent plugin are accessible to child plugins
export function apply(ctx: Context) {
ctx.provide('parent-service', { ... })
ctx.plugin({
name: 'child',
inject: ['parent-service'],
apply(childCtx) {
childCtx['parent-service'] // ✅ Can access parent service
}
})
}
6. Error Handling and State Rollback
(1) apply Failure
When apply throws an exception, Fiber enters the errored state:
export function apply(ctx: Context) {
throw new Error('initialization failed')
// Fiber → errored
}
(2) Automatic Retry
Cordis automatically retries errored Fibers:
1st attempt: apply() → throw Error → errored
2nd attempt: (wait 1s) apply() → throw Error → errored
3rd attempt: (wait 2s) apply() → throw Error → errored
4th attempt: (wait 4s) apply() → success → active
Retry interval uses exponential backoff: 1s → 2s → 4s → 8s → ... → max 60s.
(3) Retry Strategy Configuration
export const Config = Schema.object({
maxRetries: Schema.number().default(5).description('Max retry attempts'),
retryInterval: Schema.number().default(1000).description('Initial retry interval (ms)')
})
(4) Manual Retry
ctx.on('fiber/errored', (fiber) => {
ctx.logger.warn(`plugin ${fiber.name} errored, retrying...`)
fiber.retry()
})
(5) Non-Retryable Errors
Some errors shouldn't be retried:
export function apply(ctx: Context) {
if (!process.env.REQUIRED_VAR) {
// Configuration error, retrying won't help
throw new NonRetryableError('REQUIRED_VAR is not set')
}
}
(6) Cleanup Phase Errors
Errors during the disposing phase don't prevent Fiber from transitioning to disposed, but are logged:
[warn] cleanup error in plugin my-plugin: Connection already closed
[info] plugin my-plugin disposed (with 1 cleanup warnings)
7. Fiber Context Isolation
(1) Each Fiber Has an Independent ctx
const fiberA = new Fiber({ name: 'plugin-a' })
const fiberB = new Fiber({ name: 'plugin-b' })
fiberA.context !== fiberB.context // true
(2) Isolation Boundaries
| Resource | Isolated | Description |
|---|---|---|
| Event listeners | ✅ | Each Fiber registers independently |
| Timers | ✅ | Each Fiber cleans up independently |
| Commands | ⚠️ | Globally shared, but with scopes |
| Services | ❌ | Globally shared |
| Configuration | ✅ | Each plugin is independent |
(3) Balancing Service Sharing and Isolation
Services are globally shared — this is a Cordis design decision. Service A registered by plugin A can be used by plugin B via inject. If isolation is needed, use the scope mechanism (see 20-scope.md).
(4) State Queries
// Query Fiber state
ctx.fiber.state // 'active'
ctx.fiber.id // 'fiber-abc-123'
ctx.fiber.parent // parent Fiber or null
ctx.fiber.children // child Fiber[]
❓ FAQ
ctx.plugin(), the framework automatically creates Fibers.typescript ctx.on('fiber/created', (fiber) => { ... }) ctx.on('fiber/active', (fiber) => { ... }) ctx.on('fiber/disposing', (fiber) => { ... }) ctx.on('fiber/disposed', (fiber) => { ... }) ctx.on('fiber/errored', (fiber) => { ... }) 📖 Summary
- Fiber is Cordis's abstraction of plugin runtime state; each plugin instance corresponds to a Fiber
- Lifecycle: pending (waiting for deps) → active (running) → disposing (cleaning up) → disposed (destroyed)
- Parent-child Fibers support cascading destruction in "children before parent" order
- On apply failure, Fiber enters errored with automatic exponential backoff retry
- Each Fiber has its own independent Context; event listeners and timers are isolated, services are globally shared
- Monitor plugin state through Fiber lifecycle events
📝 Exercises
1. ⭐ Basic: Write a plugin that outputs the current Fiber's state and id in apply. Start it and check the logs to confirm the Fiber is in the active state.
2. ⭐⭐ Intermediate: Write a plugin that intentionally throws an error in apply (simulating initialization failure). Observe the Fiber's errored state and retry behavior. Then fix the error and verify the Fiber recovers to active.
3. ⭐⭐⭐ Challenge: Create a parent-child Fiber structure: the parent plugin registers a service, the child plugin uses it via inject. Unload the parent plugin and verify the child is also cascadingly destroyed. Output the destruction order in cleanup functions to confirm "children before parent."