Why: A busy 409 used to enqueue a new id, so the retry created a second user message after the server had already stored the first. Evidence: vitest run passed, 13 files and 69 tests, including the outbox retry case. Impact: Data layer only. sendMessage and outbox flush reuse the queued message id as requestId. No screen changes. Refs: Dev feature/pi-durable-borrow Co-authored-by: Cursor <cursoragent@cursor.com>
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
// 输入排队(纯数据):回合进行中先入本地队列,终态再逐条发送。
|
|
export interface QueuedMessage {
|
|
id: string
|
|
text: string
|
|
attachmentIds: string[]
|
|
attachmentNames: string[]
|
|
}
|
|
|
|
export function newQueuedMessage(text: string, attachmentIds: string[], attachmentNames: string[]): QueuedMessage {
|
|
return {
|
|
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
text,
|
|
attachmentIds,
|
|
attachmentNames,
|
|
}
|
|
}
|
|
|
|
export function enqueueMessage(queue: QueuedMessage[], item: QueuedMessage): QueuedMessage[] {
|
|
return [...queue, item]
|
|
}
|
|
|
|
export function enqueueMessageFront(queue: QueuedMessage[], item: QueuedMessage): QueuedMessage[] {
|
|
return [item, ...queue.filter(q => q.id !== item.id)]
|
|
}
|
|
|
|
export function removeQueuedMessage(queue: QueuedMessage[], id: string): QueuedMessage[] {
|
|
return queue.filter(q => q.id !== id)
|
|
}
|
|
|
|
export function replaceQueuedText(queue: QueuedMessage[], id: string, text: string): QueuedMessage[] {
|
|
return queue.map(q => q.id === id ? { ...q, text } : q)
|
|
}
|
|
|
|
// deliveryRequestId 是这条排队消息重发时必须复用的 requestId。
|
|
// 服务端按它去重,避免忙时 409 再入队后写出第二条用户消息。
|
|
export function deliveryRequestId(item: QueuedMessage): string {
|
|
return item.id
|
|
}
|
|
|
|
export function promoteQueuedMessage(queue: QueuedMessage[], id: string): QueuedMessage[] {
|
|
const item = queue.find(q => q.id === id)
|
|
if (!item) return queue
|
|
return enqueueMessageFront(queue, item)
|
|
}
|