Skip to content

fix(core): notify reloads immediately and debounce at bursty sources - #46843

Merged
kitlangton merged 1 commit into
v2from
nonblocking-reload
Sep 2, 2026
Merged

kitlangton merged 1 commit into
v2from
nonblocking-reload

Conversation

@kitlangton

@kitlangton kitlangton commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Why

State treated its two change paths differently: a registration or disposal notified immediately, while reload() waited out a 500 ms trailing debounce first. The debounce assumed every reload came from a bursty file watcher. Counting the callers on v2, that is true for three of them and wrong for the rest:

  • Explicit operations. Mcp.add and Mcp.remove reload so their reconcile creates or drops the server entry, and ACP prompts right after mcp.add, so they must wait for reconcile — but they were idling for the debounce first. Every mcp.add sat for 500 ms before the connection even started.
  • Multi-domain pairs. Seven sites reload Integration then Catalog after auth changes and Ollama/LM Studio/vLLM/Models.dev discovery; two serial debounces meant the catalog update reached the TUI about a second after the models were already readable.
  • Config-driven reloads. config.updated is already debounced 100 ms in Config, so those paid a second debounce for nothing.

The three genuinely bursty consumers (instruction scanner, skill scanner, MCP ToolsChanged) were relying on a side effect: the consumer being blocked in reload() for 500 ms let a PubSub.sliding(1) or runForEachArray coalesce the burst behind it. Measured with a 3- and 6-event editor save burst against the instruction scanner, that produced two notifications per burst, not one.

What Changes

  • reload() notifies immediately and awaits the notification, exactly like transform and dispose. state.ts loses reloadDebounce, requestedAt, pending, and the detached debounce loop (−32 lines); changed is one code path.
  • The three bursty consumers debounce at the source with Stream.debounce("100 millis"), matching Config, agent.ts, and command.ts. Measured: the same save bursts now produce one notification.
  • Debouncing a Stream forks its upstream pull into a child fiber, so the PubSub subscription happens one fiber hop later than a plain runForEach and an event published in that hop is lost. All three sites therefore subscribe first (PubSub.subscribe, scoped) and debounce the already-open subscription (Stream.fromSubscription). tool/mcp.ts bridges the bus into its own sliding PubSub for the same reason; the bus subscription itself stays eager as before.
  • State.batch is unchanged and remains the way to coalesce several changes into one notification; it no longer doubles as the escape hatch from a debounce.
  • Tests: the debounce-specific State tests are replaced by immediate-notification equivalents; twelve fork → TestClock.adjust(500) → join dances around reload() collapse to yield* reload(); test/lib/clock.ts no longer mentions a State debounce.

Verification

# packages/core
bun typecheck
bun run test                                    # 3,993 pass, 40 skip, 0 fail
# packages/cli
bun run test test/acp                           # 96 pass, 0 fail
# packages/server
bun run ../core/script/test.ts                  # 50 pass, 3 skip, 0 fail

Burst measurement (temporary harness, not committed): instruction scanner with 3 and 6 watcher events 2 ms apart — before: 2 notifications per burst; immediate reload without a source debounce: 3 and 6; with the source debounce: 1 and 1.

reload() waited out a 500 ms debounce while registrations notified at once; explicit operations and multi-domain reload pairs paid 0.5-1 s for nothing, and the bursty consumers coalesced only because reload blocked them. State now notifies reloads immediately; the instruction scanner, skill scanner, and MCP ToolsChanged consumer debounce at the source on an eagerly opened subscription so no event is lost while the debounce starts its pull.
@kitlangton kitlangton changed the title fix(core): batch explicit reloads to skip the debounce fix(core): notify reloads immediately and debounce at bursty sources Sep 2, 2026
@kitlangton
kitlangton merged commit b45882e into v2 Sep 2, 2026
10 checks passed
@kitlangton
kitlangton deleted the nonblocking-reload branch September 2, 2026 17:53
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