How to Debug Cordis: Fiber-Based Debugging and Logging Strategies

Enable debug logging via ctx.intercept('logger', { level: 3 }), inspect fiber state at ctx.fiber, and wrap effects with labeled disposables to trace plugin lifecycles.

Cordis is a fiber-based plugin framework that isolates contexts and tracks effects through a sophisticated lifecycle system. Learning how to debug Cordis effectively requires understanding its core architecture—specifically how the Context, Fiber, and LoggerService interact to manage plugin states and errors. This guide provides practical strategies for tracing issues within the cordiverse/cordis codebase using actual source implementations from the core and loader packages.

Understanding the Cordis Debugging Architecture

Before setting breakpoints, you need to understand four key components that handle state isolation, lifecycle management, and message routing.

Context and Service Isolation

The Context object in packages/core/src/context.ts (lines 9‑78) serves as the central hub for services including events, logger, reflect, and registry. It creates a proxy via ReflectService.handler that intercepts property access, enabling shadow isolation between plugins. Two critical methods for debugging are:

  • isolate(name, label?) – Creates a new isolated context where a symbol is attached to symbols.isolate (lines 65‑69). This allows you to sandbox specific service instances.
  • intercept(name, config) – Clones the intercept map to override service configurations on a per-plugin basis (lines 71‑77). This is the primary mechanism for adjusting log levels without affecting the global application.

Fiber Lifecycle and State Tracking

Each plugin instance runs inside a Fiber, defined in packages/core/src/fiber.ts (lines 78‑166). The fiber manages the plugin lifecycle through state transitions and effect registration:

  • Instantiation – When a plugin loads, the fiber constructor (lines 22‑36) initializes a _runner object holding the current epoch and an execute function that invokes the plugin’s callback.
  • State Machine – The fiber transitions through PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED via _setEpoch (lines 99‑115). These changes are emitted via ctx.emit('internal/status', …) (line 60), allowing you to listen for lifecycle events.
  • Error Capture – Errors are stored in fiber._error and propagated to the logger (lines 174‑177). The composeError utility (line 30) wraps effect execution and attaches an outer stack trace for easier debugging.

LoggerService and Message Buffering

The LoggerService in packages/core/src/logger.ts (lines 12‑46) creates per-plugin Logger objects that buffer and forward messages to registered exporters. Key implementation details include:

  • Configuration Resolution – _resolveConfig() merges intercept configurations from the current context chain (lines 14‑24), allowing specific plugins to override global log levels.
  • Message Structure – When you call logger.debug(), the service builds a Message object containing a sequence number (sn), timestamp (ts), and call location (lines 38‑45).
  • Level Filtering – Exporters iterate through registered handlers, and each exporter’s level property determines if the message is emitted (lines 41‑44). The service supports standard levels: error, warn, info, and debug (level 3).

Dynamic Resolution with createResolve

For module-related debugging, packages/loader/src/resolve.ts (lines 31‑63) provides the createResolve helper. This function determines the nearest package.json directory and writes a hidden .cordis/resolve.mjs file that uses import.meta.resolve to handle specifiers relative to the plugin’s scope. This ensures consistent resolution during hot-module-replacement (HMR) cycles.

Practical Debugging Strategies

1. Enable Debug Logging for Specific Plugins

Instead of flooding your console with global debug output, use ctx.intercept to target specific plugins:

// Set debug level (3) for this plugin only
ctx.intercept('logger', { level: 3 })

The LoggerService._resolveConfig() method merges this intercept configuration with the global context chain, ensuring that logger.debug() calls appear in your output while other plugins remain at their default levels.

2. Inspect Fiber State at Runtime

Access the fiber instance through ctx.fiber to examine the internal state machine:

const fiber = ctx.fiber
console.log('Current state:', fiber.state)      // FiberState enum value
console.log('Captured error:', fiber._error)     // Error object or null
console.log('Active effects:', fiber.getEffects())

The state property reflects the current lifecycle phase (LOADING, ACTIVE, etc.) as updated by _updateState (lines 55‑63). Checking _error immediately after a plugin failure reveals the captured exception before it propagates.

3. Track Effect Lifecycles with Labels

Effects registered via Fiber.effect() (lines 75‑84 and 120‑127) return disposables that include EffectMeta metadata. Wrap critical code with labeled effects to trace hierarchical relationships:

ctx.effect(() => {
  // Your initialization code here
  return () => {
    // Cleanup logic
  }
}, 'database-connection')

If an unhandled rejection occurs, the fiber logs the error with the effect label attached, making it easier to identify which disposable failed during the UNLOADING phase.

4. Verify Module Resolution Paths

When debugging import failures or HMR issues, use the resolver directly to confirm paths resolve correctly within the plugin’s scope:

import { createResolve } from '@cordis/loader'

const resolve = await createResolve(import.meta.url)
const resolvedPath = resolve('./config/schema.ts')
console.log('Resolved to:', resolvedPath)

This helper respects the plugin’s package.json boundary and ensures that relative specifiers resolve consistently across HMR cycles.

5. Capture Full Stack Traces from Effect Errors

Errors thrown inside effects are processed by composeError, which calls buildOuterStack from the utilities to append an outer trace. When catching errors, inspect the full error.stack property:

try {
  await someEffect()
} catch (err) {
  console.error(err.stack)  // Contains both inner and outer stack traces
}

This enriched trace shows exactly which effect in the fiber hierarchy triggered the failure.

Debug Code Examples

Enable Verbose Logging for a Single Plugin

import { Context } from 'cordis'

export function apply(ctx: Context) {
  // Isolate logger config to debug level for this scope only
  ctx.intercept('logger', { name: 'data-processor', level: 3 })
  
  const logger = ctx.logger('data-processor')
  logger.debug('Processing batch started')
  logger.debug('Configuration loaded:', ctx.config)
}

Inspect Fiber During Plugin Initialization

import { Context } from 'cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    const fiber = ctx.fiber
    console.log('--- Fiber Debug Snapshot ---')
    console.log('State:', fiber.state)           // e.g., FiberState.LOADING
    console.log('UID:', fiber.uid)
    console.log('Epoch:', fiber._runner?.epoch)
    
    // Return cleanup function
    return () => console.log('Effect disposed')
  }, 'debug-snapshot')
}

Resolve Modules Within Plugin Scope

import { Context } from 'cordis'
import { createResolve } from '@cordis/loader'

export async function apply(ctx: Context) {
  const resolve = await createResolve(import.meta.url)
  
  if (resolve) {
    const utilsPath = resolve('./utils/helpers')
    ctx.logger.debug('Resolved utils path:', utilsPath)
    
    // Dynamic import using resolved path
    const helpers = await import(utilsPath)
  }
}

Summary

Debugging Cordis effectively relies on understanding its fiber-based architecture and leveraging its built-in instrumentation:

  • Use ctx.intercept('logger', { level: 3 }) to enable debug output for specific plugins without affecting global log levels.
  • Access ctx.fiber to inspect the current lifecycle state (LOADING, ACTIVE, etc.) and check _error for captured exceptions.
  • Label your effects with ctx.effect(callback, 'label') to trace which disposables fail during cleanup.
  • Utilize createResolve from @cordis/loader to verify that dynamic imports resolve correctly within the plugin’s package scope.
  • Inspect error.stack on caught exceptions to view the full trace including outer stacks added by composeError.

Frequently Asked Questions

How do I enable debug logging for only one plugin in Cordis?

Use the ctx.intercept() method to override the logger configuration for that specific context scope. Pass { level: 3 } to set the level to DEBUG (where 0=error, 1=warn, 2=info, 3=debug). The LoggerService._resolveConfig() method in packages/core/src/logger.ts merges this intercept with the global configuration, ensuring only that plugin emits debug messages.

What information does ctx.fiber.state provide?

The state property returns a FiberState enum value indicating the plugin’s current lifecycle phase: PENDING, LOADING, ACTIVE, UNLOADING, or DISPOSED. This state is updated internally by _setEpoch in packages/core/src/fiber.ts and emitted via the internal/status event, allowing you to track exactly when a plugin finishes initialization or begins teardown.

How can I identify which effect is causing a disposal error?

Wrap your effect code with a descriptive label as the second argument to ctx.effect(). If the effect throws during execution or cleanup, Cordis logs the error with this label attached to the EffectMeta metadata. You can also call fiber.getEffects() to retrieve the list of active effect disposables and their associated metadata.

Why are my dynamic imports failing in Cordis plugins?

Dynamic imports may fail if they resolve outside the plugin’s package scope or if the specifier is incorrect relative to the source file. Use createResolve(import.meta.url) from @cordis/loader to obtain a resolver function that respects the nearest package.json boundary. This ensures consistent module resolution during both normal execution and HMR reloads.

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 →