Chat session emptied by checkpoint restore becomes permanently unreachable and flickers in the sessions list
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
A local chat session whose requests are all removed (e.g. restoring to the **first** checkpoint) keeps its full transcript on disk but becomes permanently unreachable in the Chat Sessions list — and flickers in and out of the list while its model happens to be loaded.
I hit this on a real session with 33 requests and a ~21 MB operation log. The transcript is still physically present in the file; it is only logically erased.
### Root cause
Two predicates decide session-list membership, and they disagree about empty sessions.
**History path** — `chatServiceImpl.ts:485-489` — excludes empty entries:
```ts
async getHistorySessionItems(): Promise {
const index = await this._chatSessionStore.getIndex();
return Object.values(index)
.filter(entry => !entry.isExternal)
.filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId))
&& entry.initialLocation === ChatAgentLocation.Chat
&& !entry.isEmpty) // <-- excluded here
```
**Live path** — `chatServiceImpl.ts:518` — has no emptiness check at all:
```ts
private shouldBeInHistory(entry: ChatModel): boolean {
return !entry.isImported && !entry.isDeleted
&& !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource)
&& entry.initialLocation === ChatAgentLocation.Chat;
}
```
Because the history path deliberately skips sessions that are currently loaded (`!this._sessionModels.has(...)`), the same session is:
* **loaded** → rendered by the live path → **visible**
* **evicted** → rendered from the index → `isEmpty: true` → **hidden**
That is the flicker. Once the model is evicted for good, the session is gone from the UI permanently, with no way to reopen it.
### How the session becomes empty
`chatWidget.ts:3227-3234` removes requests from a live model when restoring a checkpoint:
```ts
const requests = this.viewModel.model.getRequests();
for (let i = requests.length - 1; i >= 0; i -= 1) {
const request = requests[i];
if (request.shouldBeBlocked.get() || request === this.viewModel.model.checkpoint) {
this.chatService.removeRequest(this.viewModel.sessionResource, request.id);
}
}
```
If the checkpoint is the first request, this empties the model. The operation-log differ then records a truncation (`objectMutationLog.ts:615-616`):
```ts
} else if (currArr.length < prevArr.length) {
// Items removed from end
entries.push({ kind: EntryKind.Push, k: path.slice(), i: currArr.length });
}
```
which lands in the session file as the record below, where `i: 0` means "remove everything from index 0" per the type doc at `objectMutationLog.ts:214`:
```json
{"kind":2,"k":["requests"],"i":0}
```
`writeSession` then updates the index from that same now-empty model (`chatSessionStore.ts:397-399`, `getSessionMetadataSync` at `:887`), so the index records `isEmpty: true` and a `timing` with only `created`:
```json
{
"sessionId": "",
"title": "",
"lastMessageDate": 1787866212101,
"timing": { "created": 1787866212101 },
"isEmpty": true,
"isExternal": false
}
```
Compare a healthy entry, which carries `lastRequestStarted` / `lastRequestEnded` and `isEmpty: false`.
### Truncation itself is normal — only truncating to 0 is fatal
Scanning my `emptyWindowChatSessions` folder, partial truncations are routine and those sessions display fine:
| requests written | truncated to | visible in list |
|---|---|---|
| 37 | 29 | yes |
| 2 | 1 | yes |
| 33 | **0** | **no** |
| 2 | **0** | **no** |
So the problem is specific to a session being reduced to zero requests: `isEmpty` flips to `true` and the entry is filtered out forever, even though the file still holds the entire conversation.
### Steps to reproduce
1. Start a chat and send several requests.
2. Restore the checkpoint on the **first** request, so every request is removed.
3. Observe the session while its model is still loaded — it is listed.
4. Reload the window (or let the model be evicted).
5. The session is no longer in the Chat Sessions list. Its `.jsonl` under `globalStorage/emptyWindowChatSessions/` (or the workspace's `chatSessions/`) still contains the full transcript, and its `chat.ChatSessionStore.index` entry still has the title.
### Expected
Either:
* the two predicates agree, so a session does not appear and disappear based on whether its model is loaded; and
* a session that has a persisted title and transcript is not silently unreachable — reducing it to zero requests should not be indistinguishable from a never-used "New Chat".
### Notes
`isEmpty` is computed correctly from the model — this is not a stale-index bug. The index faithfully records a session that was logically emptied. The defect is that (a) the two list paths disagree, and (b) "has no requests right now" is conflated with "was never used", which makes a real conversation unrecoverable through the UI.
Happy to send a PR once there is a steer on the intended behaviour — the mechanical part (one shared predicate) is easy; the product question is whether an emptied-but-titled session should remain listed.
VS Code 1.134.0 (also present on `main`).
Contributor guide
Assessment
This issue has not been assessed yet.