Pull Request Overview
- Opened on September 18, 2026
- Status Merged
- Commit count 4 with first commit September 18, 2026
Total Delta
Open Days
Test Delta
How long has this pull request spent in each phase of its lifecycle?
Data pending calculation for pull request
Use resolver-owned synthetic documents in multi-diff
Closes #336548.
Root cause
Multi-diff created synthetic empty-side models directly through IModelService. Opening one through the text-model resolver established an independent owner: releasing the last temporary document reference could destroy a model still owned by the multi-diff editor.
Fix
- Add
ITextModelService.createSyntheticDocument(value, languageSelection), returning an owning resolved-model reference. - Create synthetic models inside the workbench resolver's existing reference collection. The creator and subsequent URI opens share one lifetime authority; the initial reference is established before
onModelAddedcan reenter the resolver. - Await synthetic sides before constructing multi-diff items. Keep source and model references alive through initialization and the shared diff-view-model lifetime, with cleanup for removal, disposal, and partial initialization failure.
- Provide the equivalent in-memory resolver for standalone editors and reuse it in editor tests and component fixtures. Externally created standalone models remain caller-owned.
This replaces the earlier model-service shared-count implementation. There is no additional shared-model map or reference count in IModelService, and ordinary workbench resolver ownership remains unchanged.
Validation
- 69 focused unit tests passing, covering resolver ownership, notification reentrancy, both release orders, pending reacquisition, failure cleanup, multi-diff lifecycle, standalone models, editor input, and Sessions integration.
- All 33 multi-diff component fixtures render and dispose without errors.
- Targeted ESLint and commit hygiene pass.
- Local full type checking / layer type checking report only the pre-existing missing
@modelcontextprotocol/sdk/types.jsdependency in unrelated agent-host Claude files. CI will validate the clean dependency installation.
Summary
The multi-diff editor can leave large blank regions because synthetic empty-side text models are disposed while their diff view models are still alive.
Comment-range prefetch resolves these models through the text-model resolver. When the resulting temporary document reference expires or is evicted, the resolver destroys the underlying model despite the multi-diff retaining its own independent ownership. Rendering then throws Model is disposed!; the virtualized item latches the failure while continuing to reserve its default 500-pixel height.
Observed behavior
Investigated in the Agents window on Insiders build b065ad9cd83dae266607baf77f10c1fbf0330fba.
- In a session changes view containing 55 files, 51 original-side models were disposed; none of the modified-side models were disposed.
- The disposed originals were synthetic
inmemory://model/...empty sides for added files. - Their
DocumentDiffItemViewModelowners and owning disposable stores were still alive. - Re-rendering an affected item threw
Model is disposed!from text-model attachment. - Failed virtualized entries remained unbound but retained a 500-pixel size, producing the whitespace.
Reproduction
- Open session changes containing added files in the Agents multi-diff editor, with a comment-range provider enabled.
- Allow comment-range prefetch to open the synthetic empty-side documents.
- Let the temporary document references expire (three minutes), or load enough documents to trigger reference-cache eviction.
- Scroll so affected entries need to acquire/rebind an editor template.
- Observe missing diff contents and large blank regions; rendering fails with
Model is disposed!.
The ownership conflict was also verified deterministically with an isolated diff-view-model probe: after its empty side was automatically opened through comment prefetch, releasing only that probe's temporary document-reference entry produced:
text model disposed: true
multi-diff owner alive: true
multi-diff reference count: 1
multi-diff owning store disposed: false
The live reference-cache settings were maxAge = 180000 ms and maxSize = 50; count cleanup starts at 60 entries and evicts 10.
Traced code path
- DocumentDiffItemViewModel creates a missing side directly with
IModelService.createModel('', null)and puts the raw model in the diff view model's disposable store. It does not hold a resolver reference for that side. - CommentService prefetches comments on
onModelAdded; it excludesvscodeSourceControl, but notinmemory. - ExtHostComments.$provideCommentingRanges calls
ensureDocumentData, which can call$tryOpenDocument. - MainThreadDocuments acquires a resolver reference and adds it to
BoundModelReferenceCollection. - BoundModelReferenceCollection releases references on timeout/count/size eviction.
- ResourceModelCollection.destroyReferencedObject disposes the resource-model wrapper when its own reference count reaches zero. The multi-diff's independent count is not included.
- TextResourceEditorModel.dispose unconditionally calls
modelService.destroyModel(...), including for an existing model it did not create. - ManagedVirtualizedItem.render latches
_didRenderFail, so subsequent renders are skipped while layout still reserves the item's height.
Captured runtime stacks confirmed both sides of this chain:
Acquisition:
ExtHostComments.$provideCommentingRanges -> ExtHostDocuments.ensureDocumentData
-> RPC -> MainThreadDocuments.$tryOpenDocument -> _handleAsResourceInput
-> TextModelResolverService.createModelReference
Disposal:
ResourceModelCollection.destroyReferencedObject
-> TextResourceEditorModel.dispose -> ModelService.destroyModel
-> TextModel.dispose
Expected behavior / fix direction
Synthetic diff sides should participate in the same shared lifetime as resolver clients. Releasing a comment/document reference must not destroy a model still needed by the multi-diff; conversely, closing the diff must not destroy a model while another shared reference remains.
A focused fix should give synthetic sides long-lived resolver references tied to the shared diff view model (not recycled editor templates), and avoid independently disposing their raw text models. Initialization/cancellation and standalone-editor behavior need to be accounted for. Missing-side resource identity should remain distinct from the synthetic backing model so additions/deletions retain the correct semantics.
Filtering comment prefetch would avoid this trigger but would not fix the ownership mismatch. Removing destruction from TextResourceEditorModel globally also needs care: existing resolver tests explicitly expect in-memory models to be disposed on last-reference release.
Regression coverage
- Comment-reference timeout and count eviction while a diff remains alive.
- Scroll away/back and editor-template recycling after reference release.
- Both lifetime orders: comment reference released first; diff released first while another resolver reference remains.
- Disposal exactly once after the last shared owner releases.
- Cancellation/partial initialization without leaked references.
PR was closed without comments.