You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The single-pane docked-tabs reconcile pipeline runs asynchronously on a Sequencer. When SinglePaneDockedTabsCoordinator is disposed (session switch / Existing-session strategy teardown / window close) while a reconcile is still queued or in-flight, that reconcile keeps running and eventually calls IEditorGroup.replaceEditors, which instantiates an editor pane through the coordinator's IInstantiationService. By then the DI tree has been disposed, so createInstance throws InstantiationService has been disposed. Impact: an unhandled error on the Agent Sessions window during teardown/session switching (Mac + Windows, 1.136 insiders).
sessions: stabilize single pane lifecycle behavior
Why
This commit introduced the Sequencer-based asynchronous reconcile pipeline (queueReconcile to _reconcile to _reconcileCore) with generation checkpoints, but dispose() was never overridden to cancel work already queued on the sequencer. A reconcile queued before disposal therefore survives teardown and reaches replaceEditors, which instantiates an editor pane through the disposed instantiation service.
The reconcile pipeline has since been extended (e.g. #332365, "Show Changes view for new sessions", which added the _reconcileForeignChangesEditors to replaceEditors path seen in the stack), but the missing disposal cancellation is the underlying defect.
Code Flow
sequenceDiagram
participant Trigger as Ambient trigger
participant Queue as queueReconcile / Sequencer
participant Reconcile as _reconcileCore
participant Group as replaceEditors
participant DI as InstantiationService
Trigger->>Queue: queueReconcile(target, trigger)
Note over Queue: reconcile queued on Sequencer
Note over DI: Root cause: coordinator disposed,<br/>DI tree torn down,<br/>queued reconcile not cancelled
Queue->>Reconcile: _reconcile(generation) runs after dispose
Reconcile->>Group: _reconcileForeignChangesEditors then replaceEditors
Group->>DI: createInstance(editor pane)
Note over DI: Error thrown:<br/>InstantiationService has been disposed
L69 (from stack): _throwIfDisposed throws InstantiationService has been disposed
Repro Steps
This is a teardown/dispose race, so it is timing-dependent:
Open the Agent Sessions window with a workspace-backed session that shows managed docked tabs (Changes + Files).
Trigger a reconcile (switch sessions, reveal the side pane, or open/close an editor) so a reconcile is queued on the sequencer.
Immediately dispose the coordinator before the queued reconcile resolves — e.g. switch away to a different session (Existing to New/QuickChat) or close the window while the async replaceEditors/openEditor step is pending.
The surviving reconcile reaches replaceEditors to createInstance on the disposed instantiation service and throws.
To increase likelihood: rapidly switch sessions or close the window right after a session switch, which maximizes the window between "reconcile queued" and "reconcile reaches an editor-open call."
How the Fix Works
Lifecycle pattern: use-after-dispose. Producer site: SinglePaneDockedTabsCoordinator.dispose() (previously absent) — the disposable owner did not cancel callbacks already queued on this._sequencer. Fix location: singlePaneDockedTabsCoordinator.ts — new dispose() override plus entry guards in _reconcile / _reconcileCore.
Chosen approach (producer-side; fix where the lifecycle is owned rather than guarding the crash site):
Added an override dispose() that bumps this._generation and clears this._pending before super.dispose(). Bumping the generation makes any reconcile still queued on the sequencer bail at its generation checkpoints, so it never proceeds to open/replace editors after teardown.
Added this._store.isDisposed to the early-return guard in _reconcile, and a this._store.isDisposed check immediately before the first async editor operation in _reconcileCore (_reconcileForeignChangesEditors), which is where the reported stack crashes. This covers a reconcile that was already past its entry check when disposal happened.
This follows the data-producer / lifecycle-owner principle: the coordinator owns the sequencer and the DI-backed editor operations, so cancelling its own in-flight work on dispose is the correct place for the fix — not a try/catch at the createInstance crash site, which would swallow the error and hide any other genuine disposal bug from telemetry.
Alternatives considered:
Wrap createInstance/replaceEditors in try/catch and swallow the disposed error — rejected: it hides the symptom at the crash site instead of stopping the stale async work at its lifecycle owner, and would silence unrelated disposal errors from telemetry.
Check isDisposed only inside queueReconcile — rejected: it does not stop reconciles already queued/in-flight at the moment of disposal, which is exactly the race in the stack trace.
Recommended Owner
@sandy081 — author of the recent single-pane reconcile/lifecycle commits (#330573 introducing the sequencer pipeline, #332365) and owner of the src/vs/sessions/contrib/layout area.
Original error: ERR_API: [2026-08-31T15:02:30.996Z] create pull request in microsoft/vscode failed (attempt 1)
Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.
To create the pull request manually:
gh pr create --title "fix: cancel pending single-pane docked-tab reconciles on dispose (fixes #333537)" --base main --head vscodebot-pr:errors-fix/333537-docked-tabs-dispose-00db8b91ba5544e7 --repo microsoft/vscode
Show patch preview (50 of 50 lines)
From 26cac5c99d8cc6c8cc1fc1135bfb5af586617402 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 54f9f8e33bdb926d87925863eac27aacc52d50e5
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 31 Aug 2026 14:53:37 +0000
Subject: [PATCH] fix: cancel pending single-pane docked-tab reconciles on
dispose (fixes #333537)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../singlePane/singlePaneDockedTabsCoordinator.ts | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts
index 9e9f433c5ec..74fe274a4c0 100644
--- a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts+++ b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts@@ -310,8 +310,18 @@ export class SinglePaneDockedTabsCoordinator extends Disposable {
// --- Reconcile --------------------------------------------------------
+ override dispose(): void {+ // Cancel any pending/in-flight reconciles queued on the sequencer: bumping the+ // generation makes queued reconciles bail at their entry (and in-flight ones at their+ // next generation checkpoint) so none open editors — which would instantiate an editor+ // pane through the now-disposed instantiation service after teardown.+ this._generation++;+ this._pending = undefined;+ super.dispose();+ }+
private async _reconcile(generation: number): Promise<void> {
- if (generation !== this._generation || !this._pending) {+ if (this._store.isDisposed || generation !== this._generation || !this._pending) {
return;
}
@@ -343,6 +353,9 @@ export class SinglePaneDockedTabsCoordinator extends Disposable {
// group is never mistaken for the user closing all tabs (which would close the side pane).
const suppression = this._la
... (truncated)
Summary
The single-pane docked-tabs reconcile pipeline runs asynchronously on a
Sequencer. WhenSinglePaneDockedTabsCoordinatoris disposed (session switch / Existing-session strategy teardown / window close) while a reconcile is still queued or in-flight, that reconcile keeps running and eventually callsIEditorGroup.replaceEditors, which instantiates an editor pane through the coordinator'sIInstantiationService. By then the DI tree has been disposed, socreateInstancethrowsInstantiationService has been disposed. Impact: an unhandled error on the Agent Sessions window during teardown/session switching (Mac + Windows, 1.136 insiders).Fixes #333537
Recommended reviewer:
@sandy081Culprit Commit
f45fb5350b9@sandy081Sequencer-based asynchronous reconcile pipeline (queueReconcileto_reconcileto_reconcileCore) with generation checkpoints, butdispose()was never overridden to cancel work already queued on the sequencer. A reconcile queued before disposal therefore survives teardown and reachesreplaceEditors, which instantiates an editor pane through the disposed instantiation service.Code Flow
sequenceDiagram participant Trigger as Ambient trigger participant Queue as queueReconcile / Sequencer participant Reconcile as _reconcileCore participant Group as replaceEditors participant DI as InstantiationService Trigger->>Queue: queueReconcile(target, trigger) Note over Queue: reconcile queued on Sequencer Note over DI: Root cause: coordinator disposed,<br/>DI tree torn down,<br/>queued reconcile not cancelled Queue->>Reconcile: _reconcile(generation) runs after dispose Reconcile->>Group: _reconcileForeignChangesEditors then replaceEditors Group->>DI: createInstance(editor pane) Note over DI: Error thrown:<br/>InstantiationService has been disposedAffected Files
src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.tsqueueReconcilequeues_reconcile(generation)onthis._sequencer; class had nodispose()override to cancel queued worksrc/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts_reconcileForeignChangesEditorscallsgroup.replaceEditors([{ replacement: this._instantiationService.createInstance(...) }])src/vs/workbench/browser/editor.tsinstantiatetocreateInstancesrc/vs/platform/instantiation/common/instantiationService.ts_throwIfDisposedthrowsInstantiationService has been disposedRepro Steps
This is a teardown/dispose race, so it is timing-dependent:
replaceEditors/openEditorstep is pending.replaceEditorstocreateInstanceon the disposed instantiation service and throws.To increase likelihood: rapidly switch sessions or close the window right after a session switch, which maximizes the window between "reconcile queued" and "reconcile reaches an editor-open call."
How the Fix Works
Lifecycle pattern: use-after-dispose.
Producer site:
SinglePaneDockedTabsCoordinator.dispose()(previously absent) — the disposable owner did not cancel callbacks already queued onthis._sequencer.Fix location:
singlePaneDockedTabsCoordinator.ts— newdispose()override plus entry guards in_reconcile/_reconcileCore.Chosen approach (producer-side; fix where the lifecycle is owned rather than guarding the crash site):
override dispose()that bumpsthis._generationand clearsthis._pendingbeforesuper.dispose(). Bumping the generation makes any reconcile still queued on the sequencer bail at its generation checkpoints, so it never proceeds to open/replace editors after teardown.this._store.isDisposedto the early-return guard in_reconcile, and athis._store.isDisposedcheck immediately before the first async editor operation in_reconcileCore(_reconcileForeignChangesEditors), which is where the reported stack crashes. This covers a reconcile that was already past its entry check when disposal happened.This follows the data-producer / lifecycle-owner principle: the coordinator owns the sequencer and the DI-backed editor operations, so cancelling its own in-flight work on dispose is the correct place for the fix — not a
try/catchat thecreateInstancecrash site, which would swallow the error and hide any other genuine disposal bug from telemetry.Alternatives considered:
createInstance/replaceEditorsintry/catchand swallow the disposed error — rejected: it hides the symptom at the crash site instead of stopping the stale async work at its lifecycle owner, and would silence unrelated disposal errors from telemetry.isDisposedonly insidequeueReconcile— rejected: it does not stop reconciles already queued/in-flight at the moment of disposal, which is exactly the race in the stack trace.Recommended Owner
@sandy081— author of the recent single-pane reconcile/lifecycle commits (#330573 introducing the sequencer pipeline, #332365) and owner of thesrc/vs/sessions/contrib/layoutarea.Note
This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch
errors-fix/333537-docked-tabs-dispose-00db8b91ba5544e7.Original error: ERR_API: [2026-08-31T15:02:30.996Z] create pull request in microsoft/vscode failed (attempt 1)
Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.
To create the pull request manually:
gh pr create --title "fix: cancel pending single-pane docked-tab reconciles on dispose (fixes #333537)" --base main --head vscodebot-pr:errors-fix/333537-docked-tabs-dispose-00db8b91ba5544e7 --repo microsoft/vscodeShow patch preview (50 of 50 lines)