How to Debug Cordis Fiber and Effect Issues: A Complete Guide

To debug Cordis fiber and effect issues, inspect the Fiber.state property and Fiber._error field to identify lifecycle failures, enable detailed logging via enableLogs: true, and use fiber.getEffects() to verify proper disposal of resources created through ctx.fiber.effect.

Debugging Cordis fiber and effect issues requires understanding the framework's execution model, where fibers manage plugin lifecycles and effects handle disposable resources. In the cordiverse/cordis repository, the Fiber class in /packages/core/src/fiber.ts orchestrates state transitions from PENDING to DISPOSED, while the effect system tracks resource cleanup through disposable lists. This guide walks through the source code architecture, common failure modes, and practical debugging techniques to resolve stuck fibers, leaking effects, and unexpected state transitions.

Understanding the Cordis Fiber Architecture

The Fiber State Machine

Every plugin in Cordis runs inside a Fiber instance created by new Fiber(parent, config, inject, runtime, getOuterStack) in /packages/core/src/registry.ts【/packages/core/src/registry.ts#L93-L108】. The fiber implements a strict state machine via Fiber.state, which transitions through FiberState values: PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED【/packages/core/src/fiber.ts#L78-L85】.

Key lifecycle hooks control these transitions:

  • _refresh – Runs after fiber creation or when injected dependencies change. It recomputes the epoch string and may trigger reload or unload operations【/packages/core/src/fiber.ts#L85-L97】.
  • _setEpoch – Called when the epoch changes; decides whether to transition to LOADING for a reload or UNLOADING for disposal【/packages/core/src/fiber.ts#L99-L112】.
  • _reload – Executes the plugin's execute function and activates the fiber【/packages/core/src/fiber.ts#L15-L35】.
  • _unload – Disposes all registered effects and transitions to DISPOSED【/packages/core/src/fiber.ts#L36-L48】.

When disposal occurs, Fiber.dispose removes the fiber from runtime.fibers, clears its configuration, and waits for pending inertia promises to settle【/packages/core/src/fiber.ts#L70-L99】.

Effect Lifecycle and Tracking

Effects are created via ctx.fiber.effect(() => …, 'label'), implemented in Fiber.effect【/packages/core/src/fiber.ts#L75-L84】. The executor function may return:

  • A synchronous disposable (() => void)
  • An iterable of disposables
  • A promise of a disposable
  • An async iterable

The fiber stores each disposable in Fiber._disposables (a DisposableList) and builds a tree of EffectMeta objects for debugging purposes【/packages/core/src/fiber.ts#L22-L31】【/packages/core/src/fiber.ts#L92-L106】.

Common Cordis Fiber and Effect Failure Modes

Fiber stays in PENDING state – This occurs when Fiber._runner.epoch never leaves INACTIVE, typically due to validation errors during resolveConfig or exceptions thrown before _refresh runs. Check Fiber._error (exposed via await() which re-throws) or ctx.logger.error output inside Fiber.dispose【/packages/core/src/fiber.ts#L73-L78】.

Effect never runs or never disposes – The effect executor likely returned a non-function or non-iterable value, triggering TypeError('Invalid effect'). The error is wrapped by composeError in /packages/core/src/utils.ts, so inspect the stack trace printed by ctx.logger.error【/packages/core/src/utils.ts#L60-L78】.

ctx.fiber.dispose() is ignored – This happens when the fiber's uid is already null (previously disposed) or the fiber is mid-reload/unload, causing an early return. Verify fiber.uid and fiber.state via console logging before calling dispose.

Unexpected FiberState.FAILED – An exception bubbled out of _reload or _unload. The error is stored in Fiber._error and re-thrown by await()【/packages/core/src/fiber.ts#L60-L66】. Call await fiber in a test or REPL to surface the underlying error.

Step-by-Step Debugging Workflow

Follow this systematic approach when debugging Cordis fiber and effect issues:

  1. Enable detailed logging – Set ctx.fiber.entry?.parent.tree.enableLogs = true or pass enableLogs: true in the loader configuration. The LoggerService prints timestamps and stack traces for every ctx.logger.error call.

  2. Inspect the fiber's metadata – Access the internal state to diagnose lifecycle issues:

    const fiber = ctx.fiber // root fiber
    console.log('state →', fiber.state)
    console.log('epoch →', (fiber as any)._runner?.epoch)
    console.log('effects →', fiber.getEffects().map(m => m.label))
  3. Force a reload – If you suspect stale configuration, invoke fiber.restart() or fiber.update(newConfig) to reset the epoch and trigger _refresh/_reload【/packages/core/src/fiber.ts#L68-L74】.

  4. Break on errors – Insert a temporary debugger statement inside Fiber._execute or composeError to capture the exact call stack when an effect throws.

  5. Unit-test the problematic plugin – Use the test harness in packages/core/tests/*.spec.ts to isolate the plugin. The spec files create a fresh context and expose the fiber via ctx.plugin(Loader); you can then call await fiber and assert on fiber.state or fiber._error.

Practical Debugging Examples

Example 1: Checking a Fiber's State in a Plugin

Use this pattern to verify your plugin reaches the ACTIVE state and to inspect effect registration:

import { Context, Inject } from 'cordis'

export default class Demo {
  @Inject('logger')
  apply(ctx: Context) {
    const fiber = ctx.fiber
    // Log the current state
    ctx.logger.info('Fiber state:', fiber.state)
    
    // Create an effect that cleans up a timer
    return ctx.fiber.effect(() => {
      const timer = setInterval(() => ctx.logger.debug('tick'), 1000)
      return () => clearInterval(timer) // disposable
    }, 'demo-timer')
  }
}

When the plugin loads successfully, you will see "Fiber state: ACTIVE" in the console. If the plugin throws during apply, the state becomes FAILED and the error prints automatically.

Example 2: Triggering a Reload After Config Change

Test how your plugin handles configuration updates by forcing a reload cycle:

// Assume `ctx` is a running Cordis context
const fiber = await ctx.plugin(Demo, { interval: 500 })
await fiber // Wait for initial load

// Update config to force reload
fiber.update({ interval: 200 })
await fiber // Wait for reload to finish
ctx.logger.info('Reload complete, state:', fiber.state)

If the new config fails validation and resolveConfig throws a ValidationError, fiber.state becomes FAILED and the error is logged via the fiber's error handling mechanism.

Example 3: Debugging a Hanging Effect

Identify async iterables that never complete using this test pattern:

import { expect } from 'chai'
import { Context } from 'cordis'

describe('hanging effect', () => {
  it('should not leave dangling disposables', async () => {
    const ctx = new Context()
    const fiber = await ctx.plugin({ 
      name: 'leaky', 
      apply: (c) => {
        // Returns an async iterable that never finishes
        return c.fiber.effect(async function* () {
          while (true) yield () => {}
        }, 'leaky')
      }
    })
    
    // Wait a tick then dispose
    await new Promise(r => setTimeout(r, 10))
    await fiber.dispose()
    
    // All disposables must be cleared
    expect(fiber.getEffects()).to.be.empty
  })
})

If the async iterable never yields a final done value, the test surfaces the problem by checking fiber.getEffects() after disposal, which should be empty in a properly cleaned-up fiber.

Key Source Files for Debugging Cordis

Understanding these core files accelerates root cause analysis:

  • /packages/core/src/fiber.ts – Implements the Fiber class, state machine transitions, effect handling, and reload/unload logic. Contains Fiber.effect, Fiber.dispose, and the _refresh/_setEpoch hooks.

  • /packages/core/src/context.ts – Sets up the root Context, creates the initial fiber, and exposes the ctx.fiber property for accessing the current execution context.

  • /packages/core/src/registry.ts – Handles plugin registration via RegistryService.plugin, creates a Fiber for each plugin, and provides the ctx.plugin and ctx.inject APIs.

  • /packages/core/src/utils.ts – Supplies utility helpers including DisposableList, composeError, and buildOuterStack used throughout the fiber and effect system.

Summary

  • Cordis fibers manage plugin lifecycles through a strict state machine (PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED) implemented in /packages/core/src/fiber.ts.
  • Effects must return valid disposables (functions, iterables, or promises); invalid returns trigger TypeError wrapped by composeError in /packages/core/src/utils.ts.
  • Debug stuck fibers by checking fiber.state, fiber._error, and (fiber as any)._runner?.epoch to identify validation or dependency resolution failures.
  • Force reloads using fiber.update() or fiber.restart() to test configuration changes and verify _refresh behavior.
  • Verify cleanup by asserting fiber.getEffects() is empty after disposal to catch hanging async iterables or undisposable resources.

Frequently Asked Questions

Why does my Cordis fiber stay stuck in the PENDING state?

A fiber remains PENDING when Fiber._runner.epoch never transitions from INACTIVE, usually due to validation errors during resolveConfig or an exception thrown before the _refresh hook executes. Inspect Fiber._error (re-thrown by await fiber) or check the logger output from Fiber.dispose to identify the blocking error【/packages/core/src/fiber.ts#L73-L78】.

How do I detect if a Cordis effect failed to dispose properly?

Call fiber.getEffects() after disposal; if the array is not empty, resources remain active. This commonly occurs when an effect executor returns an async iterable that never completes or a non-disposable value. The error is captured by composeError and logged via ctx.logger.error with a stack trace built by buildOuterStack【/packages/core/src/utils.ts#L60-L78】.

What causes a Cordis fiber to enter the FAILED state unexpectedly?

The FAILED state occurs when an exception bubbles out of _reload or _unload during the plugin lifecycle. The error is stored in Fiber._error and will be re-thrown when you await the fiber. Check the stack trace to distinguish between errors in the plugin's apply function and errors in the disposal logic【/packages/core/src/fiber.ts#L60-L66】.

How can I force a Cordis plugin to reload for debugging purposes?

Invoke fiber.update(newConfig) to change the configuration and trigger a reload, or call fiber.restart() to reset the epoch and re-execute the plugin. Both methods trigger _setEpoch and _refresh, allowing you to test how the plugin handles state transitions and configuration validation【/packages/core/src/fiber.ts#L68-L74】.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →