fix: avoid reader use after seeding side-effect in inline completions fetch (fixes #323531) - #323549
Open
vs-code-engineering[bot] wants to merge 1 commit into
Open
Conversation
… fetch The _fetchInlineCompletionsPromise derived read reader-tracked observables (_didUndoInlineEdits, _inlineEditsEnabled) after calling seedInlineCompletionsWithSuggestWidget(), whose nested transaction re-enters and recomputes the same derived, invalidating the reader and throwing "The reader object cannot be used outside its compute function!". Capture those reads into locals before the seed so no read(reader) follows the state-mutating side-effect. Fixes #323531 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Summary
InlineCompletionsModel's_fetchInlineCompletionsPromisederived performs a state-mutating side-effect —seedInlineCompletionsWithSuggestWidget()— in the middle of its compute, then continues to read reader-tracked observables. The seed opens atransactionwhose synchronous commit re-enters the derived through therecomputeInitiallyAndOnChangekeep-alive and invalidates the active reader, so the nextthis._didUndoInlineEdits.read(reader)throws"The reader object cannot be used outside its compute function!". The crash is intermittent and most common wheninlineSuggest.experimental.showOnSuggestConflictis active, which forces the seed gate to fire while the suggest widget is open.Fixes #323531
Recommended reviewer:
@hedietCulprit Commit
e61cfda@hedietmain(tracks an internal backlog item)_shouldShowOnSuggestConflictoverride (if (this._shouldShowOnSuggestConflict.read(undefined)) { suggestItem = undefined; }) immediately before the seed gateif (suggestWidgetInlineCompletions && !suggestItem). ForcingsuggestItem = undefinedmakes the gate fire even while a suggestion is selected, drivingseedInlineCompletionsWithSuggestWidget()— and its reader-invalidating re-entrant transaction — far more often. The underlying seed-in-compute pattern predates this commit, so this is best characterized as the recent-regression driver rather than the sole origin; precise build bisection was not possible because the workflow checkout is shallow (fetch-depth=1), which prevents localgit blame/ancestry verification.Code Flow
sequenceDiagram participant Trigger as Suggest widget / config participant Fetch as fetchInlineCompletionsPromise derived participant Seed as seedInlineCompletionsWithSuggestWidget participant KeepAlive as recomputeInitiallyAndOnChange participant Derived as derivedImpl recompute Trigger->>Fetch: recompute, reader is valid Note over Fetch: reads _selectedSuggestItem and _textModelVersionId via reader Fetch->>Seed: seedInlineCompletionsWithSuggestWidget at L388 Note over Seed: Root cause: opens transaction and mutates _state Seed->>KeepAlive: transaction commit re-enters the keep-alive observer KeepAlive->>Derived: recomputes the same derived re-entrantly Note over Derived: finally block sets _isReaderValid = false Derived-->>Fetch: returns, reader now invalid Fetch->>Fetch: _didUndoInlineEdits.read(reader) at L395 Note over Fetch: Error thrown: reader used outside its compute functionAffected Files
src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.tsthis._didUndoInlineEdits.read(reader)executes after the seed; L388 (root cause):this._source.seedInlineCompletionsWithSuggestWidget()is called inside the derived compute, before two later reader reads (L395, L428)src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.tsseedInlineCompletionsWithSuggestWidget()openstransaction(tx => { ... this._state.set(...) ... }), whose synchronous commit re-enters the keep-alive observer that recomputes the fetch derivedsrc/vs/base/common/observableInternal/observables/derivedImpl.ts_ensureReaderValid()throws the error;_recompute()'sfinallysets_isReaderValid = false, which the re-entrant recompute leaves behindRepro Steps
This is a timing-dependent re-entrancy bug; it requires the seed gate to fire during a fetch recompute. To maximize the likelihood:
inlineSuggest.experimental.showOnSuggestConflict(this forcessuggestItem = undefined, widening the seed gate)._source.suggestWidgetInlineCompletionsis populated while no inline suggest item is selected.suggestWidgetInlineCompletions && !suggestItemis true,seedInlineCompletionsWithSuggestWidget()runs, its transaction re-enters and invalidates the reader, and the followingthis._didUndoInlineEdits.read(reader)throws. Without the experimental flag the same crash occurs whenever the suggest widget has completions but no item is selected during a fetch recompute.How the Fix Works
Chosen approach (
inlineCompletionsModel.ts): The two reader-tracked reads that previously ran after the seed —this._didUndoInlineEdits.read(reader)(was L395) andthis._inlineEditsEnabled.read(reader)(was L428) — are captured into locals immediately before the seed gate, and the later sites use those locals. The seeding side-effect itself is unchanged, but it is now the last reader-affecting operation in the compute, so noread(reader)can run after the reader has been invalidated by the seed's re-entrant transaction. This fixes the producer of the invalid sequence — the side-effect ordering inside the derived — rather than guarding the crash site. It mirrors the discipline already present in this same compute, where thecleartransaction is immediately followed byreturnprecisely so that no reader read follows it. The governing principle is to order all reader reads before any state-mutating side-effect within a derived compute.Behavioral equivalence is preserved:
_inlineEditsEnabledisinlineSuggest.map(v => !!v.edits.enabled)(config-derived) and_didUndoInlineEditstracks only_textModelVersionId; neither depends on_state, so reading them a few lines earlier returns identical values. Hoisting_didUndoInlineEditsabove thedontRefetchearly-return adds no new effective subscription, because its sole upstream_textModelVersionIdis already read unconditionally earlier in the same compute.Alternatives considered:
try/catchat the crash-site read — rejected: that hides the symptom at the consumer instead of fixing the producer-side ordering, and would silence the error telemetry without removing the re-entrancy.dontRefetchanddidUndoearly-returns, so relocating it past them would skip seeding in those branches, a behavior change.Recommended Owner
@hediet— Henning Dieterichs authored both the inline-completions model / observablereaderframework and theinlineSuggest.experimental.showOnSuggestConflictfeature (commite61cfda) whose override widens the seed trigger. He is a core VS Code editor-team maintainer with write access and is actively committing to this exact file (merges through 2026-02), satisfying both the team-membership and liveness gates. Cascade step 1 (culprit-commit author) selects him directly.