Skip to content

Commit ac54681

Browse files
committed
fix(chat): keep a steered message restored past the age limit truthful
Restoring a steered entry past the age limit marked it not delivered without reading, so a message its turn had applied showed as undelivered and invited a resend. It now stays steered so its turn's marker or a disposition read settles it; only the auto-send is blocked, and Retry lifts that.
1 parent 5038691 commit ac54681

3 files changed

Lines changed: 105 additions & 31 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/outgoing-state.test.ts‎

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,67 @@ describe('a journal restored past the age limit', () => {
346346
NOW
347347
).status
348348

349-
it('shows a message on its way as not delivered', () => {
349+
it('shows a message that never reached a turn as not delivered', () => {
350350
expect(restore('pending', ORPHAN_AUTO_SEND_MAX_AGE_MS)).toBe('failed')
351-
expect(restore('steered', ORPHAN_AUTO_SEND_MAX_AGE_MS)).toBe('failed')
352351
expect(restore('sending', ORPHAN_AUTO_SEND_MAX_AGE_MS)).toBe('failed')
353352
})
354353

354+
it('keeps one steered as steered, since its turn may have applied it', () => {
355+
expect(restore('steered', ORPHAN_AUTO_SEND_MAX_AGE_MS)).toBe('steered')
356+
const aged = fromJournaled(
357+
'chat',
358+
toJournaled(
359+
entry('a', {
360+
status: 'steered',
361+
streamId: 'turn',
362+
writtenAt: NOW - ORPHAN_AUTO_SEND_MAX_AGE_MS,
363+
})
364+
),
365+
NOW
366+
)
367+
const context = { streamId: 'turn', turnErrored: false, now: NOW }
368+
expect(afterDispositionRead(aged, { status: 'applied', textOffset: 3 }, context).status).toBe(
369+
'applied'
370+
)
371+
expect(afterDispositionRead(aged, { status: 'not_applied' }, context).status).toBe('failed')
372+
})
373+
374+
it('lets the user send it once they ask, after no turn took it', () => {
375+
const aged = fromJournaled(
376+
'chat',
377+
toJournaled(
378+
entry('a', {
379+
status: 'steered',
380+
streamId: 'turn',
381+
writtenAt: NOW - ORPHAN_AUTO_SEND_MAX_AGE_MS,
382+
})
383+
),
384+
NOW
385+
)
386+
const retried = { ...aged, ...afterRetry(aged) }
387+
const read = afterDispositionRead(
388+
retried,
389+
{ status: 'not_applied' },
390+
{ streamId: 'turn', turnErrored: false, now: NOW }
391+
)
392+
expect(read.status).toBe('pending')
393+
})
394+
395+
it('sends a recent steered message no turn took on its own', () => {
396+
const recent = fromJournaled(
397+
'chat',
398+
toJournaled(entry('a', { status: 'steered', streamId: 'turn', writtenAt: NOW - 1 })),
399+
NOW
400+
)
401+
expect(
402+
afterDispositionRead(
403+
recent,
404+
{ status: 'not_applied' },
405+
{ streamId: 'turn', turnErrored: false, now: NOW }
406+
).status
407+
).toBe('pending')
408+
})
409+
355410
it('resumes a recent one where it stood, and leaves one already applied alone', () => {
356411
expect(restore('pending', ORPHAN_AUTO_SEND_MAX_AGE_MS - 1)).toBe('pending')
357412
expect(restore('steered', ORPHAN_AUTO_SEND_MAX_AGE_MS - 1)).toBe('steered')

‎apps/sim/app/workspace/[workspaceId]/home/hooks/outgoing-state.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export interface OutgoingMessage {
7878
writtenAt: number
7979
/** Epoch ms before which it is not tried again. */
8080
notBefore?: number
81+
/** Restored past the age limit: if no turn took it, it waits for the user rather than going out. */
82+
noAutoSend?: boolean
8183
}
8284

8385
/** What became of a steer, as the client reads it; `unknown`: no answer, so it may be held. */
@@ -152,21 +154,21 @@ export function toJournaled(message: OutgoingMessage): JournaledOutgoing {
152154
}
153155

154156
/**
155-
* The entry a journal restores. One on its way that has sat past the age limit (a
156-
* browser session restored days later) shows as not delivered, with no network call
157-
* and nothing going out until the user asks.
157+
* The entry a journal restores. Past the age limit (a browser session restored days
158+
* later) nothing goes out until the user asks: one that never reached a turn shows as
159+
* not delivered, with no network call. One steered stays steered, because its turn may
160+
* well have applied it, and only a read saying no turn took it shows it as not delivered.
158161
*/
159162
export function fromJournaled(
160163
chatKey: string,
161164
entry: JournaledOutgoing,
162165
now: number
163166
): OutgoingMessage {
164-
const stillOnItsWay = entry.status === 'pending' || entry.status === 'steered'
167+
const pastAgeLimit = now - entry.writtenAt >= ORPHAN_AUTO_SEND_MAX_AGE_MS
165168
return {
166169
...entry,
167-
...(stillOnItsWay && now - entry.writtenAt >= ORPHAN_AUTO_SEND_MAX_AGE_MS
168-
? { status: 'failed' as const }
169-
: {}),
170+
...(pastAgeLimit && entry.status === 'pending' ? { status: 'failed' as const } : {}),
171+
...(pastAgeLimit && entry.status === 'steered' ? { noAutoSend: true } : {}),
170172
chatKey,
171173
display: optimisticUserMessageFor(entry.id, entry.payload),
172174
attempts: 0,
@@ -307,7 +309,7 @@ export function afterDispositionRead(
307309
}
308310
if (outcome.status === 'not_applied') {
309311
return {
310-
...notSteered(context.streamId, context.turnErrored),
312+
...notSteered(context.streamId, context.turnErrored || message.noAutoSend === true),
311313
...(message.attempts > 0
312314
? { notBefore: context.now + outgoingRetryDelay(message.attempts) }
313315
: {}),
@@ -336,6 +338,7 @@ export function afterRetry(message: OutgoingMessage): Partial<OutgoingMessage> {
336338
attempts: 0,
337339
reads: 0,
338340
notBefore: undefined,
341+
noAutoSend: undefined,
339342
}
340343
}
341344

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx‎

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3556,28 +3556,44 @@ describe('useChat remount send recovery', () => {
35563556
}
35573557
)
35583558

3559-
it.each([
3560-
['steered', { status: 'steered', streamId: 'earlier-turn' }],
3561-
['pending', { status: 'pending' }],
3562-
] as const)(
3563-
'shows a %s entry a session restore brought back days later as not delivered',
3564-
async (_name, standing) => {
3565-
const id = '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f7'
3566-
state.steerDispositions = ['not_applied']
3567-
const view = await restoredChat([
3568-
{
3569-
id,
3570-
payload: { content: 'from days ago' },
3571-
writtenAt: Date.now() - 3 * 24 * 60 * 60_000,
3572-
...standing,
3573-
},
3574-
])
3559+
const restoredDaysLater = (standing: { status: 'steered' | 'pending'; streamId?: string }) =>
3560+
restoredChat([
3561+
{
3562+
id: '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f7',
3563+
payload: { content: 'from days ago' },
3564+
writtenAt: Date.now() - 3 * 24 * 60 * 60_000,
3565+
...standing,
3566+
},
3567+
])
35753568

3576-
expect(deliveryOf(view, id)).toBe('failed')
3577-
expect(state.steerBodies).toEqual([])
3578-
expect(state.postBodies).toEqual([])
3579-
}
3580-
)
3569+
it('shows a pending entry a session restore brought back days later as not delivered, with no call', async () => {
3570+
const view = await restoredDaysLater({ status: 'pending' })
3571+
3572+
expect(deliveryOf(view, '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f7')).toBe('failed')
3573+
expect(state.steerBodies).toEqual([])
3574+
expect(state.postBodies).toEqual([])
3575+
})
3576+
3577+
it('never sends a steered entry a session restore brought back days later when no turn took it', async () => {
3578+
state.steerDispositions = ['not_applied']
3579+
const view = await restoredDaysLater({ status: 'steered', streamId: 'earlier-turn' })
3580+
3581+
await vi.waitFor(() =>
3582+
expect(deliveryOf(view, '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f7')).toBe('failed')
3583+
)
3584+
expect(state.postBodies).toEqual([])
3585+
})
3586+
3587+
it('does not call a steered entry restored days later undelivered when its turn applied it', async () => {
3588+
state.steerDispositions = ['applied']
3589+
const view = await restoredDaysLater({ status: 'steered', streamId: 'earlier-turn' })
3590+
3591+
await vi.waitFor(() => expect(state.steerBodies.length).toBeGreaterThan(0))
3592+
await vi.waitFor(() =>
3593+
expect(deliveryOf(view, '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f7')).toBeUndefined()
3594+
)
3595+
expect(state.postBodies).toEqual([])
3596+
})
35813597

35823598
it('does not let a message not delivered hold back a later plain send', async () => {
35833599
const id = '6f1e2d3c-4b5a-4978-8695-a4b3c2d1e0f9'

0 commit comments

Comments
 (0)