Skip to content

fix: preserve chat session index during workspace transition#305109

Open
w1nsid wants to merge 1 commit intomicrosoft:mainfrom
w1nsid:fix/preserve-chat-index-on-workspace-transition
Open

fix: preserve chat session index during workspace transition#305109
w1nsid wants to merge 1 commit intomicrosoft:mainfrom
w1nsid:fix/preserve-chat-index-on-workspace-transition

Conversation

@w1nsid
Copy link
Copy Markdown

@w1nsid w1nsid commented Mar 26, 2026

Bug

When the workspace identity changes (e.g. adding a folder to a single-folder Remote SSH session and saving as a .code-workspace file), all Copilot Chat history disappears from Show Chats. The .jsonl session files are correctly copied to the new workspace storage folder, but the index that maps session IDs to titles/timestamps is lost — so after the window reloads (which always happens on Remote SSH), Show Chats returns nothing.

Root Cause

In ChatSessionStore.migrateSessionsToNewWorkspace():

  1. Session .jsonl files are copied from the old storage root to the new one
  2. this.indexCache = undefined clears the in-memory index
  3. flushIndex() is called, which internally calls internalGetIndex()
  4. But storageService.switch() has already run (in enterWorkspace() before fireDidEnterWorkspace), which closes the old workspace storage DB and creates a new empty one
  5. StorageScope.WORKSPACE now points to the new empty DB — internalGetIndex() reads nothing, creates an empty index, and flushes it
  6. All session metadata is lost — titles, timestamps, session IDs are gone

This affects both workspace→workspace transitions (the primary repro) and empty window→workspace transitions.

Fix

Register an onWillSaveState listener that eagerly populates indexCache via internalGetIndex(). The storage service fires onWillSaveState before closing the old workspace storage in switch(), so the cache captures the old index while the old DB is still open. When migrateSessionsToNewWorkspace runs afterward, it uses the cached index as the source of truth, then merges old entries into the new scope's index (preserving any entries that may already exist in the target).

This approach works for all transition types because the cache is populated before the storage swap — regardless of whether the old scope was APPLICATION (empty window) or WORKSPACE.

Test

Added session index is preserved after workspace transition regression test that:

  1. Stores sessions in an empty window
  2. Fires onWillSaveState to simulate storageService.switch() populating the cache
  3. Switches TestContextService to a non-empty workspace (simulating the scope change)
  4. Fires onDidEnterWorkspace to trigger migration
  5. Creates a fresh ChatSessionStore to verify the index was persisted to the correct WORKSPACE scope (not just cached in memory)

Repro steps (before fix)

  1. Connect to a remote host via Remote SSH
  2. Open a folder — have several Copilot Chat conversations
  3. Add another folder to the workspace (making it multi-root)
  4. Save Workspace As... when prompted
  5. Window reloads — all chat history is gone from Show Chats
  6. The .jsonl session files exist in the new workspaceStorage/ folder but the index is empty

Fixes #301793
Related: #285242

Copilot AI review requested due to automatic review settings March 26, 2026 10:17
@vs-code-engineering vs-code-engineering Bot added this to the 1.114.0 milestone Mar 26, 2026
@w1nsid
Copy link
Copy Markdown
Author

w1nsid commented Mar 26, 2026

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Copilot Chat workspace-transition regression where chat session metadata (index: titles/timestamps/session IDs) can be lost when the workspace identity changes (e.g., Save Workspace As...), causing Show Chats to appear empty after reload even though session files exist on disk.

Changes:

  • Preserve the in-memory chat session index during workspace migration so flushIndex() writes the previous entries into the new workspace scope.
  • Add a regression test intended to validate session index preservation across a workspace transition.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Adjusts workspace migration to retain and flush the prior session index instead of regenerating an empty index in the new workspace scope.
src/vs/workbench/contrib/chat/test/common/model/chatSessionStore.test.ts Adds a regression test for index preservation across a simulated workspace transition.

Comment thread src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Outdated
Comment thread src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Outdated
@w1nsid w1nsid force-pushed the fix/preserve-chat-index-on-workspace-transition branch 2 times, most recently from 51d9360 to d01815d Compare March 26, 2026 13:41
@w1nsid w1nsid requested a review from Copilot March 26, 2026 13:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Outdated
Comment thread src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts Outdated
@meganrogge meganrogge removed this from the 1.114.0 milestone Mar 26, 2026
@meganrogge meganrogge assigned bpasero and unassigned meganrogge Mar 26, 2026
@bpasero bpasero added bug Issue identified by VS Code Team member as probable bug confirmation-pending chat 2026-themes Issues related to the 2026 light and dark themes and removed 2026-themes Issues related to the 2026 light and dark themes labels Mar 26, 2026
@bpasero
Copy link
Copy Markdown
Member

bpasero commented Mar 26, 2026

@roblourens wanna take a look?

Copy link
Copy Markdown
Member

@roblourens roblourens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this is reasonable. Could you

  • Scope it down for just the case when the window is empty, reading from app storage, because the workspace case seems to work and is what we fixed earlier
  • Open an issue to track this
  • Check Copilot's comments

@bpasero bpasero removed their assignment Mar 29, 2026
@w1nsid w1nsid force-pushed the fix/preserve-chat-index-on-workspace-transition branch from d76ed81 to 73b2d13 Compare April 2, 2026 10:37
@w1nsid
Copy link
Copy Markdown
Author

w1nsid commented Apr 2, 2026

Good catch, this is reasonable. Could you

  • Scope it down for just the case when the window is empty, reading from app storage, because the workspace case seems to work and is what we fixed earlier
  • Open an issue to track this
  • Check Copilot's comments

Thanks for taking a look @roblourens!

Scope it down for just the case when the window is empty, reading from app storage

So my actual issue relates to a workspace→workspace transition : I was on Remote SSH with a single folder open, added another folder, then saved the workspace. That goes through enterWorkspace()storageService.switch() which closes the old workspace DB before onDidEnterWorkspace fires. After the switch, StorageScope.WORKSPACE points to the new empty DB, so there's no way to read the old index from it.

The empty window case (APPLICATION scope) would survive the switch since it's global, but the workspace→workspace path is the one I actually hit and it wouldn't be covered by just reading from app storage.

I switched the approach to hooking into onWillSaveState, switch() fires that before closing the old storage (here), so we just populate indexCache while the old DB is still open. Ends up being less code and covers both cases.

Open an issue to track this

There's an existing one: #301793. Also linked #285242 which describes the same symptom.

Check Copilot's comments

The two new ones from the second review were about readIndexFromScope not working for workspace→workspace (correct, same issue as above) and a misleading comment in that method. Both adressed.


Disclosure: I used GitHub Copilot (chat + code review) throughout this PR — for tracing the root cause through the storage/workspace layers, drafting the fix, writing the test, and iterating on review feedback.

When saving an untitled workspace (Save Workspace As...), the workspace
identity changes and ChatSessionStore.migrateSessionsToNewWorkspace is
called via onDidEnterWorkspace. The method copies .jsonl session files
to the new storage location, but then clears indexCache and calls
flushIndex(). At this point, storageService.switch() has already moved
StorageScope.WORKSPACE to the new (empty) database. internalGetIndex()
reads from the new scope, finds nothing, creates an empty index, and
flushes that — losing all session metadata.

After window reload (which always happens for remote connections), the
session files exist on disk but the index is empty, so Show Chats
returns nothing. The user sees all their chat history wiped out.

Fix: capture the old index before clearing the cache and set it as the
new indexCache so flushIndex() writes the preserved entries to the new
workspace storage scope.

Adds a regression test verifying the session index survives a workspace
transition.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +80 to +86
// Eagerly populate the index cache before the storage service
// switches workspace storage. storageService.switch() fires
// onWillSaveState before closing the old storage, so this
// ensures indexCache holds the old index when migration runs.
this._register(this.storageService.onWillSaveState(() => {
this.internalGetIndex();
}));
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onWillSaveState currently calls internalGetIndex(), but internalGetIndex() reads from getIndexStorageScope() when indexCache is undefined. In the real enterWorkspace() flow, the workspace context is updated during configurationService.initialize(...) (WorkspaceService.initialize updates this.workspace) before storageService.switch() emits onWillSaveState, so getIndexStorageScope() may already be WORKSPACE even when the old index lived in APPLICATION (empty-window → workspace). If indexCache wasn’t already populated, this will cache an empty WORKSPACE index and migration can still drop the old APPLICATION-scoped entries. Consider capturing the old index from an explicit scope (e.g. read+parse from StorageScope.APPLICATION when transitioning from an empty window, and from pre-switch workspace storage for workspace→workspace), instead of relying on getIndexStorageScope() inside the will-save handler.

Copilot uses AI. Check for mistakes.
Comment on lines +476 to +484
// Simulate the real storageService.switch() sequence:
// 1. onWillSaveState fires (populates indexCache in the store)
// 2. Storage scope changes from APPLICATION to WORKSPACE
// 3. onDidEnterWorkspace fires (triggers migration)
storageService.testEmitWillSaveState(WillSaveStateReason.NONE);

const contextService = instantiationService.get(IWorkspaceContextService) as TestContextService;
contextService.setWorkspace(TestWorkspace);

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test simulates onWillSaveState firing before the workspace context switches from empty-window → workspace, but in the actual enterWorkspace() sequence the context is updated during configurationService.initialize(...) before storageService.switch() emits onWillSaveState. With the real ordering, getIndexStorageScope() can already be WORKSPACE when the old index is in APPLICATION, so this test may miss regressions unless it reorders setWorkspace(...) before testEmitWillSaveState(...) and also covers the case where indexCache is undefined (e.g. clear it after storeSessions() to ensure the will-save path actually loads the index).

Suggested change
// Simulate the real storageService.switch() sequence:
// 1. onWillSaveState fires (populates indexCache in the store)
// 2. Storage scope changes from APPLICATION to WORKSPACE
// 3. onDidEnterWorkspace fires (triggers migration)
storageService.testEmitWillSaveState(WillSaveStateReason.NONE);
const contextService = instantiationService.get(IWorkspaceContextService) as TestContextService;
contextService.setWorkspace(TestWorkspace);
// Simulate the real enterWorkspace()/storageService.switch() sequence:
// 1. Workspace context switches from empty-window to workspace (configurationService.initialize)
// 2. storageService.switch() emits onWillSaveState while the index is still in APPLICATION scope
// 3. onDidEnterWorkspace fires (triggers migration)
(store as any).indexCache = undefined;
const contextService = instantiationService.get(IWorkspaceContextService) as TestContextService;
contextService.setWorkspace(TestWorkspace);
storageService.testEmitWillSaveState(WillSaveStateReason.NONE);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Issue identified by VS Code Team member as probable bug chat confirmation-pending

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copilot Chat history is completely lost after saving an "Untitled Workspace" as a .code-workspace file

5 participants