fix(client): update recency from user messages
This commit is contained in:
@@ -71,6 +71,7 @@ type SessionListMutation =
|
|||||||
| { kind: 'upsert'; summary: SessionSummary }
|
| { kind: 'upsert'; summary: SessionSummary }
|
||||||
| { kind: 'remove'; sessionId: SessionId }
|
| { kind: 'remove'; sessionId: SessionId }
|
||||||
| { kind: 'status'; sessionId: SessionId; running: boolean }
|
| { kind: 'status'; sessionId: SessionId; running: boolean }
|
||||||
|
| { kind: 'activity'; sessionId: SessionId; updatedAt: number }
|
||||||
/** Local first-send flip: the sender clears blank without waiting for a host frame. */
|
/** Local first-send flip: the sender clears blank without waiting for a host frame. */
|
||||||
| { kind: 'engaged'; sessionId: SessionId }
|
| { kind: 'engaged'; sessionId: SessionId }
|
||||||
|
|
||||||
@@ -685,6 +686,16 @@ export class SessionManager {
|
|||||||
handleMuxEnvelope(envelope: RpcRequest<MuxFrame>): void {
|
handleMuxEnvelope(envelope: RpcRequest<MuxFrame>): void {
|
||||||
const frame = envelope.payload
|
const frame = envelope.payload
|
||||||
if (frame.type === 'stream/error') return // Controller already treats this as stream failure
|
if (frame.type === 'stream/error') return // Controller already treats this as stream failure
|
||||||
|
if (
|
||||||
|
frame.type === 'session/event'
|
||||||
|
&& frame.event.type === 'user/message'
|
||||||
|
&& frame.event.data.source.kind === 'user'
|
||||||
|
) {
|
||||||
|
// session.list supplies the cold baseline, while a direct prompt or an
|
||||||
|
// admitted steer advances it between pulls. Max keeps replayed or
|
||||||
|
// repaired older user messages from moving the row backwards.
|
||||||
|
this.recordMutation({ kind: 'activity', sessionId: frame.sessionId, updatedAt: frame.event.time })
|
||||||
|
}
|
||||||
if (frame.type === 'session/projection') {
|
if (frame.type === 'session/projection') {
|
||||||
// Finished host-computed value: land it in the resident store whether or
|
// Finished host-computed value: land it in the resident store whether or
|
||||||
// not the Session is instantiated (list rows read the 'title' key). The
|
// not the Session is instantiated (list rows read the 'title' key). The
|
||||||
@@ -1115,6 +1126,11 @@ function applyMutation(summaries: readonly SessionSummary[], mutation: SessionLi
|
|||||||
&& (summary.running !== mutation.running || (mutation.running && summary.blank))
|
&& (summary.running !== mutation.running || (mutation.running && summary.blank))
|
||||||
? { ...summary, running: mutation.running, blank: summary.blank && !mutation.running }
|
? { ...summary, running: mutation.running, blank: summary.blank && !mutation.running }
|
||||||
: summary)
|
: summary)
|
||||||
|
case 'activity':
|
||||||
|
return summaries.map(summary => summary.sessionId === mutation.sessionId
|
||||||
|
&& mutation.updatedAt > summary.updatedAt
|
||||||
|
? { ...summary, updatedAt: mutation.updatedAt }
|
||||||
|
: summary)
|
||||||
case 'engaged':
|
case 'engaged':
|
||||||
return summaries.map(summary => summary.sessionId === mutation.sessionId && summary.blank
|
return summaries.map(summary => summary.sessionId === mutation.sessionId && summary.blank
|
||||||
? { ...summary, blank: false }
|
? { ...summary, blank: false }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|||||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||||
import { entries, plainTurn } from './event-script.ts'
|
import { entries, ev, plainTurn } from './event-script.ts'
|
||||||
|
|
||||||
const S1 = 'fk-m1' as SessionId
|
const S1 = 'fk-m1' as SessionId
|
||||||
const S2 = 'fk-m2' as SessionId
|
const S2 = 'fk-m2' as SessionId
|
||||||
@@ -113,6 +113,46 @@ describe('list lifecycle', () => {
|
|||||||
expect(manager.getListSnapshot().items.map(item => item.sessionId)).toEqual([S2, S1])
|
expect(manager.getListSnapshot().items.map(item => item.sessionId)).toEqual([S2, S1])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('advances list activity only for direct user messages', async () => {
|
||||||
|
const api = new FakeApiClient()
|
||||||
|
api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] }))
|
||||||
|
const manager = new SessionManager(api)
|
||||||
|
await manager.refreshList()
|
||||||
|
|
||||||
|
// Both a new prompt and an admitted steer land as a user-sourced message.
|
||||||
|
const activity = { ...ev.user(10, 'new'), time: 500 }
|
||||||
|
manager.handleMuxEnvelope({
|
||||||
|
rpcId: 'activity' as never,
|
||||||
|
payload: { type: 'session/event', sessionId: S1, event: activity },
|
||||||
|
})
|
||||||
|
expect(manager.getListSnapshot().items[0]?.updatedAt).toBe(500)
|
||||||
|
|
||||||
|
manager.handleMuxEnvelope({
|
||||||
|
rpcId: 'older' as never,
|
||||||
|
payload: { type: 'session/event', sessionId: S1, event: { ...activity, time: 400 } },
|
||||||
|
})
|
||||||
|
manager.handleMuxEnvelope({
|
||||||
|
rpcId: 'assistant' as never,
|
||||||
|
payload: { type: 'session/event', sessionId: S1, event: { ...ev.assistant(11, 0, 'reply'), time: 600 } },
|
||||||
|
})
|
||||||
|
|
||||||
|
const injected = ev.user(12, 'context')
|
||||||
|
if (injected.type !== 'user/message') throw new Error('user builder returned another event type')
|
||||||
|
manager.handleMuxEnvelope({
|
||||||
|
rpcId: 'injected' as never,
|
||||||
|
payload: {
|
||||||
|
type: 'session/event',
|
||||||
|
sessionId: S1,
|
||||||
|
event: {
|
||||||
|
...injected,
|
||||||
|
time: 700,
|
||||||
|
data: { ...injected.data, source: { kind: 'plugin', plugin: 'test' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(manager.getListSnapshot().items[0]?.updatedAt).toBe(500)
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps the error in the list snapshot on failure', async () => {
|
it('keeps the error in the list snapshot on failure', async () => {
|
||||||
const api = new FakeApiClient()
|
const api = new FakeApiClient()
|
||||||
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'boom', details: {} }))
|
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'boom', details: {} }))
|
||||||
|
|||||||
Reference in New Issue
Block a user