fix: keep client session projection stores across removal and revival

The per-session ProjectionValueStore was deleted on host/session-removed
even though the Session instance survives removal (open() is idempotent and
drop() only removes the map entry). A re-added or re-listed session resumed
on the same instance with an empty projection face, so projection-backed UI
(such as the OpenRouter cost readout) reverted to blank until a fresh host
baseline landed.

Retain the store on removal and lift the removed tombstone from the
surviving instance on revival (host/session-added and list-refresh
re-listing), so chat switching keeps cost readouts populated.
This commit is contained in:
2026-08-20 22:10:43 +07:00
parent ed152416d5
commit d2ad46b8aa
8 changed files with 266 additions and 5 deletions

View File

@@ -190,7 +190,7 @@ describe('list lifecycle', () => {
expect(manager.getListSnapshot().items.map(i => i.sessionId)).toEqual([S2])
})
it('retains title projections before list arrival, keeps last-wins by seq, and clears them on removal', async () => {
it('retains title projections before list arrival, keeps last-wins by seq, and survives removal', async () => {
const api = new FakeApiClient()
const manager = new SessionManager(api, fakeRemote())
const titleFrame = (rpcId: string, title: string, seq: number) => {
@@ -212,9 +212,13 @@ describe('list lifecycle', () => {
expect(titled.items[0]?.title).toBe('Newest')
expect(titled.items[1]?.title).toBeUndefined()
// A removed-then-re-added session is the same durable lifecycle under the
// resident-instance rule: its projection face (here the title) survives
// the round-trip, so the row resurfacing shows the retained value rather
// than re-seeding from an empty store.
manager.handleHostEnvelope({ rpcId: 'removed' as never, payload: { type: 'host/session-removed', sessionId: S1 } })
manager.handleHostEnvelope({ rpcId: 'readded' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
expect(manager.getListSnapshot().items.find(item => item.sessionId === S1)?.title).toBeUndefined()
expect(manager.getListSnapshot().items.find(item => item.sessionId === S1)?.title).toBe('Newest')
})
it('seeds cold titles from the list rows\' projections block under higher-seq-wins', async () => {