Skip to content

fix(core): subscribe before debouncing config plugin updates - #46874

Merged
kitlangton merged 1 commit into
v2from
eager-debounce-subscribe
Sep 2, 2026
Merged

kitlangton merged 1 commit into
v2from
eager-debounce-subscribe

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

Why

Stream.debounce forks its upstream pull onto a child fiber via transformPull, so a pipeline that debounces a bus or PubSub stream opens the underlying subscription one fiber hop after Effect.forkScoped({ startImmediately: true }) returns. PubSub does not buffer for future subscribers, so anything published in that hop is dropped. #46843 fixed this in instruction.ts, skill.ts, and tool/mcp.ts by subscribing eagerly and debouncing the already-open subscription; config/plugin/agent.ts and config/plugin/command.ts still debounced a lazy subscription.

Those two sites also had a second hop hiding underneath: they fan in config.changes() and ctx.event.subscribe() with Stream.merge, and Channel.merge forks each side with a scheduled (non-immediate) forkIn. Applying the reference pattern on top of the merged stream therefore still left both subscriptions one hop late (verified: the new test kept failing with that shape). The fix runs each source on its own startImmediately fiber instead of through merge.

What Changes

  • packages/core/src/config/plugin/agent.ts: replace Stream.merge(...).pipe(Stream.debounce, runForEach(reload)) with an eager PubSub.sliding<void>(1) feed. config.changes() (filtered to agent sources) and ctx.event.subscribe() (filtered to config.updated) each run Stream.runForEach(notify) forked with startImmediately: true, so both subscriptions open synchronously. The sliding subscription is then Stream.fromSubscription(...).pipe(Stream.debounce("100 millis"), Stream.runForEach(() => reload)). Same 100 ms window, same single serialized reload feed.
  • packages/core/src/config/plugin/command.ts: identical restructuring for command sources.
  • packages/core/test/config/agent.test.ts, packages/core/test/config/command.test.ts: new case rebuilds on a config update published immediately after startup. It starts the plugin with no directory entries (so startup has no filesystem hop that could mask a late subscription), publishes config.updated through the real Bus in the same fiber step, and requires one reload.

Verification

From packages/core:

  • bun typecheck: clean.
  • New tests fail deterministically on the unfixed source (condition never became true after 100 advances in advance), both for the original merge + debounce shape and for the intermediate "eager runForEach on the merged stream" shape.
  • bun run test test/config/agent.test.ts test/config/command.test.ts --rerun-each 10: 260 pass, 0 fail.
  • bun run test (full core suite): 3996 pass, 39 skip, 0 fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant