mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #73178 from NousResearch/bb/dupe-message-id
fix(desktop): stop a duplicate message id from crashing the workspace pane
This commit is contained in:
commit
a9c9467dd8
3 changed files with 88 additions and 2 deletions
58
apps/desktop/src/app/chat/runtime-repository.test.ts
Normal file
58
apps/desktop/src/app/chat/runtime-repository.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { MessageRepository } from '@assistant-ui/core/internal'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ChatMessage } from '@/lib/chat-messages'
|
||||
import { syncRepositoryIncrementally } from '@/lib/incremental-external-store-runtime'
|
||||
|
||||
import { useRuntimeMessageRepository } from './runtime-repository'
|
||||
|
||||
const text = (id: string, role: ChatMessage['role'], body: string): ChatMessage => ({
|
||||
id,
|
||||
role,
|
||||
parts: [{ type: 'text', text: body }]
|
||||
})
|
||||
|
||||
/** The repository the runtime drives — it throws on a duplicate link. */
|
||||
const feedToRepository = (repository: ExportedRepository) => {
|
||||
const runtime = { repository: new MessageRepository() } as unknown as Parameters<
|
||||
typeof syncRepositoryIncrementally
|
||||
>[0]
|
||||
|
||||
return syncRepositoryIncrementally(runtime, repository)
|
||||
}
|
||||
|
||||
type ExportedRepository = ReturnType<typeof useRuntimeMessageRepository>
|
||||
|
||||
describe('useRuntimeMessageRepository', () => {
|
||||
it('emits each id once when the transcript repeats one', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRuntimeMessageRepository([
|
||||
text('user-1', 'user', 'hi'),
|
||||
text('assistant-1', 'assistant', 'hello'),
|
||||
text('user-1', 'user', 'hi')
|
||||
])
|
||||
)
|
||||
|
||||
const ids = result.current.messages.map(item => item.message.id)
|
||||
|
||||
expect(ids).toEqual(['user-1', 'assistant-1'])
|
||||
})
|
||||
|
||||
it('builds a repository the runtime can link without throwing', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRuntimeMessageRepository([
|
||||
text('user-1', 'user', 'hi'),
|
||||
text('assistant-stream-1', 'assistant', 'partial'),
|
||||
text('assistant-stream-1', 'assistant', 'partial'),
|
||||
text('user-2', 'user', 'more')
|
||||
])
|
||||
)
|
||||
|
||||
expect(feedToRepository(result.current).map(item => item.id)).toEqual([
|
||||
'user-1',
|
||||
'assistant-stream-1',
|
||||
'user-2'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -30,10 +30,22 @@ export function useRuntimeMessageRepository(messages: ChatMessage[]): ExportedMe
|
|||
return useMemo(() => {
|
||||
const items: { message: ThreadMessage; parentId: string | null }[] = []
|
||||
const branchParentByGroup = new Map<string, string | null>()
|
||||
const seenIds = new Set<string>()
|
||||
let visibleParentId: string | null = null
|
||||
let headId: string | null = null
|
||||
|
||||
for (const message of coalesceToolOnlyAssistants(messages, toolMergeCacheRef.current)) {
|
||||
// A repeated id is a transcript bug upstream, but it must not reach the
|
||||
// repository: MessageRepository throws on the second link ("A message
|
||||
// with the same id already exists in the parent tree") and takes the
|
||||
// whole workspace pane down with it. Keep the first occurrence — the
|
||||
// later copy carries the same id, so it is the row we already rendered.
|
||||
if (seenIds.has(message.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
seenIds.add(message.id)
|
||||
|
||||
let parentId = visibleParentId
|
||||
|
||||
if (message.role === 'assistant' && message.branchGroupId) {
|
||||
|
|
|
|||
|
|
@ -291,6 +291,16 @@ function overlayProjectionRow(projection: ChatMessage, journalRow: ChatMessage):
|
|||
return { ...merged, parts }
|
||||
}
|
||||
|
||||
/** Rows the base transcript doesn't already hold by id. The journal and the
|
||||
* base can both carry the same row (a resume that replays a still-journaled
|
||||
* turn), and appending it twice puts a duplicate id in the transcript —
|
||||
* which assistant-ui's MessageRepository rejects by throwing. */
|
||||
function withoutBaseIds(rows: ChatMessage[], baseMessages: ChatMessage[]): ChatMessage[] {
|
||||
const baseIds = new Set(baseMessages.map(message => message.id))
|
||||
|
||||
return rows.filter(row => !baseIds.has(row.id))
|
||||
}
|
||||
|
||||
export function mergeInFlightMessages(
|
||||
baseMessages: ChatMessage[],
|
||||
tailMessages: ChatMessage[],
|
||||
|
|
@ -321,7 +331,13 @@ export function mergeInFlightMessages(
|
|||
// append the whole tail.
|
||||
const streamId = lastJournalRow?.id ?? null
|
||||
|
||||
return { applied: true, caughtUp: false, messages: [...baseMessages, ...tail], streamId, turnStartedAt: null }
|
||||
return {
|
||||
applied: true,
|
||||
caughtUp: false,
|
||||
messages: [...baseMessages, ...withoutBaseIds(tail, baseMessages)],
|
||||
streamId,
|
||||
turnStartedAt: null
|
||||
}
|
||||
}
|
||||
|
||||
const afterUser = baseMessages.slice(matchingUserIndex + 1)
|
||||
|
|
@ -350,7 +366,7 @@ export function mergeInFlightMessages(
|
|||
return {
|
||||
applied: true,
|
||||
caughtUp: false,
|
||||
messages: [...baseMessages, ...tailAssistants],
|
||||
messages: [...baseMessages, ...withoutBaseIds(tailAssistants, baseMessages)],
|
||||
streamId,
|
||||
turnStartedAt: null
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue