Repository navigation
fix(core): notify reloads immediately and debounce at bursty sources - #46843
Merged
Merged
Conversation
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
force-pushed
the
nonblocking-reload
branch
from
September 2, 2026 17:41
b1f36d7 to
2f4b560
Compare
jinhuang712
pushed a commit
to jinhuang712/opencode
that referenced
this pull request
Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Statetreated its two change paths differently: a registration or disposal notified immediately, whilereload()waited out a 500 ms trailing debounce first. The debounce assumed every reload came from a bursty file watcher. Counting the callers onv2, that is true for three of them and wrong for the rest:Mcp.addandMcp.removereload so their reconcile creates or drops the server entry, and ACP prompts right aftermcp.add, so they must wait for reconcile — but they were idling for the debounce first. Everymcp.addsat for 500 ms before the connection even started.config.updatedis already debounced 100 ms inConfig, 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 inreload()for 500 ms let aPubSub.sliding(1)orrunForEachArraycoalesce 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 liketransformanddispose.state.tslosesreloadDebounce,requestedAt,pending, and the detached debounce loop (−32 lines);changedis one code path.Stream.debounce("100 millis"), matchingConfig,agent.ts, andcommand.ts. Measured: the same save bursts now produce one notification.Streamforks its upstream pull into a child fiber, so the PubSub subscription happens one fiber hop later than a plainrunForEachand 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.tsbridges the bus into its own sliding PubSub for the same reason; the bus subscription itself stays eager as before.State.batchis unchanged and remains the way to coalesce several changes into one notification; it no longer doubles as the escape hatch from a debounce.fork → TestClock.adjust(500) → joindances aroundreload()collapse toyield* reload();test/lib/clock.tsno longer mentions a State debounce.Verification
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.