Skip to content

fix(core): prevent stale Fiber generations from committing - #54

Open
morluto wants to merge 1 commit into
cordiverse:mainfrom
morluto:agent/fix-fiber-lifecycle
Open

fix(core): prevent stale Fiber generations from committing#54
morluto wants to merge 1 commit into
cordiverse:mainfrom
morluto:agent/fix-fiber-lifecycle

Conversation

@morluto

@morluto morluto commented Aug 13, 2026

Copy link
Copy Markdown

Fixes #34.

Problem

A Fiber currently uses its dependency epoch both as the desired-state snapshot and as the identity of in-flight work. If an async plugin load observes A → B → A, the original load sees the same epoch when it resumes and is allowed to commit. The Fiber can therefore report ACTIVE with a new config or provider even though that generation was never applied.

A related failure occurs after plugin execution rejects: restart() successfully runs the plugin again, but the previous execution error remains attached to the Fiber, leaving it in FAILED.

stateDiagram-v2
    [*] --> LoadingA: generation 1 / config A
    LoadingA --> DesiredB: update while apply awaits
    DesiredB --> DesiredA2: desired epoch returns to A
    LoadingA --> Unloading: generation 1 settles stale
    Unloading --> LoadingA2: start latest generation
    LoadingA2 --> Active: only generation 3 commits
Loading

On current main, the issue #34 sequence produces:

Observation Current main This change
Applied configs [1] [1, 2]
Published config { value: 2 } { value: 2 }
Final state ACTIVE ACTIVE

The current state is internally inconsistent: config 2 is published without ever running the plugin with config 2.

Change

Track a monotonic generation alongside the dependency epoch. Every real desired-state transition receives a fresh generation, even if its epoch string repeats. Reload execution, async-generator collection, error publication, and completion now require both the epoch and generation to remain current.

Starting a loadable generation also clears the previous execution error. A successful explicit restart can therefore recover a failed Fiber, while a failure from superseded work cannot poison the latest generation.

This keeps the existing serialized reload/unload pump and coalesces notifications whose epoch does not change.

Regression coverage

The tests exercise:

  • the exact config A → B → A race from Bug: epoch ABA race #34;
  • provider replacement while the previous implementation is still loading;
  • a stale execution that rejects after a newer config is requested;
  • recovery from FAILED through a successful restart.

The ABA and restart tests were also run against 8cc9e33; they respectively produced [1] with published config 2, and remained FAILED with the original error.

Performance sanity check

A synthetic loop creating, awaiting, and disposing 5,000 synchronous plugins took 932.8 ms on 8cc9e33 and 941.6 ms with this change (single run, Node 22.22.1). This is a smoke check rather than a performance claim; the added counter does not introduce per-effect records or a new scheduler.

Validation

  • corepack yarn vitest packages/core/tests/fiber.spec.ts --run — 11 tests passed
  • corepack yarn test — 19 files and 166 tests passed
  • git diff --check origin/main...HEAD — passed

@morluto
morluto marked this pull request as ready for review August 13, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: epoch ABA race

1 participant