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.

💡 Tip: Fiber's core value is "observable lifecycle" — every state transition has clear trigger conditions and observable side effects. You always know what state a plugin is in, and why.

📋 Prerequisites: Completed 13-effect.md, understand auto-cleanup and ctx.effect()

1. What You'll Learn


Fiber State

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.

TYPESCRIPT
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:

Fiber provides a clear state machine model for these intermediate states.

(3) ▶ Example 3

TEXT 📖 Display only
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

100%
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:

TYPESCRIPT
export const inject = ['tools']

// tools service not yet registered → Fiber is pending
// tools registered → Fiber transitions to active, calls apply

During pending:

(4) active State

After apply executes successfully, Fiber enters active:

TYPESCRIPT
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:

(5) disposing State

After receiving an unload request, Fiber enters disposing and begins cleaning up resources:

TEXT 📖 Display only
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:


4. Fiber Creation and Destruction

(1) Creation Timing

Fiber is automatically created when a plugin is registered:

TYPESCRIPT
// 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

100%
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:

100%
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():

TYPESCRIPT
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:

TEXT 📖 Display only
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:

TYPESCRIPT
// 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:

TYPESCRIPT
export function apply(ctx: Context) {
  throw new Error('initialization failed')
  // Fiber → errored
}

(2) Automatic Retry

Cordis automatically retries errored Fibers:

TEXT 📖 Display only
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

TYPESCRIPT
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

TYPESCRIPT
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:

TYPESCRIPT
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:

TEXT 📖 Display only
[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

TYPESCRIPT
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

TYPESCRIPT
// 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

Q What's the relationship between Fiber and Thread?
A None. Fiber is not an OS thread, but Cordis's logical state unit. All Fibers run in the same Node.js event loop.
Q Can I manually create a Fiber?
A Not recommended. When registering sub-plugins via ctx.plugin(), the framework automatically creates Fibers.
Q Do errored plugins consume resources?
A No. An errored Fiber doesn't hold ctx resources; only the Fiber object itself (minimal memory) is retained for state tracking.
Q Can a child Fiber survive after its parent is destroyed?
A No. Parent-child relationships are strong dependencies — parent destruction always destroys children. If you need independent lifecycles, don't establish parent-child relationships.
Q How to monitor all Fiber states?
A Listen to Fiber lifecycle events: 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) => { ... })
Q When config changes cause Fiber rebuild, do the old cleanup functions execute?
A Yes. On config change, the old Fiber goes through the full disposing → disposed flow; all cleanup functions execute. Then a new Fiber is created and enters pending/active.

📖 Summary


📝 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."

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%

🙏 帮我们做得更好

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

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