Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/client/src/api/hermes/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ export interface ChatMessage {
reasoning_details?: string | null
reasoning_content?: string | null
isStreaming?: boolean
isIncomplete?: boolean
toolName?: string
toolCallId?: string
toolArgs?: unknown
toolPreview?: string
toolResult?: unknown
toolStatus?: 'running' | 'done' | 'error'
attachments?: Array<{ id: string; name: string; type: string; size: number; url: string }>
/** Captured at stream_start and never overwritten; used for stable sort order. */
firstSeenAt?: number
}

export interface MemberInfo {
Expand Down
12 changes: 10 additions & 2 deletions packages/client/src/stores/hermes/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ function mergeFinalMessage(existing: ChatMessage | null, msg: ChatMessage): Chat
reasoning: hasText(msg.reasoning) ? msg.reasoning : existing?.reasoning ?? msg.reasoning ?? null,
reasoning_content: hasText(msg.reasoning_content) ? msg.reasoning_content : existing?.reasoning_content ?? msg.reasoning_content ?? null,
isStreaming: false,
isIncomplete: msg.isIncomplete ?? existing?.isIncomplete ?? undefined,
firstSeenAt: existing?.firstSeenAt || msg.firstSeenAt,
attachments: existing?.attachments || msg.attachments,
}
}
Expand Down Expand Up @@ -284,7 +286,7 @@ const currentUserAvatar = ref('')
}

// ─── Computed ───────────────────────────────────────────
const sortedMessages = computed(() => mapGroupMessages([...messages.value].sort((a, b) => a.timestamp - b.timestamp)))
const sortedMessages = computed(() => mapGroupMessages([...messages.value].sort((a, b) => (a.firstSeenAt || a.timestamp) - (b.firstSeenAt || b.timestamp))))

const memberNames = computed(() => {
return members.value.map(m => m.name)
Expand Down Expand Up @@ -374,6 +376,8 @@ const currentUserAvatar = ref('')
!m.tool_calls?.length
))
msg.isStreaming = true
// Capture firstSeenAt once — never overwritten, used for stable sort
msg.firstSeenAt = msg.firstSeenAt || msg.timestamp || Date.now()
const idx = messages.value.findIndex(m => m.id === msg.id)
if (idx >= 0) {
const existing = messages.value[idx]
Expand Down Expand Up @@ -418,9 +422,10 @@ const currentUserAvatar = ref('')
messages.value = [...messages.value]
})

socket.on('message_stream_end', (data: { roomId: string; id: string }) => {
socket.on('message_stream_end', (data: { roomId: string; id: string; finishReason?: string }) => {
if (data.roomId !== currentRoomId.value) return
const idx = messages.value.findIndex(m => m.id === data.id)
const isIncomplete = data.finishReason && data.finishReason !== 'stop'
if (
idx >= 0 &&
!messages.value[idx].content?.trim() &&
Expand All @@ -429,9 +434,12 @@ const currentUserAvatar = ref('')
) {
messages.value.splice(idx, 1)
} else if (idx >= 0) {
const content = messages.value[idx].content || ''
messages.value[idx] = {
...messages.value[idx],
isStreaming: false,
isIncomplete: isIncomplete || undefined,
content: isIncomplete && content ? content + ' ⚠️ [回复中断]' : content,
}
messages.value = [...messages.value]
if (needsFinalContentRecovery(messages.value[idx])) {
Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/services/hermes/group-chat/agent-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ class AgentClient {
this.socket!.emit('message_reasoning_delta', { roomId, id: messageId, delta })
}

emitMessageStreamEnd(roomId: string, messageId: string): void {
emitMessageStreamEnd(roomId: string, messageId: string, finishReason?: string): void {
this.ensureConnected()
this.socket!.emit('message_stream_end', { roomId, id: messageId })
this.socket!.emit('message_stream_end', { roomId, id: messageId, finishReason: finishReason || 'stop' })
}

getJoinedRooms(): string[] {
Expand Down Expand Up @@ -561,7 +561,7 @@ class AgentClient {
if (lastChunk?.status === 'error') {
logger.error(`[AgentClients] ${this.name}: bridge response failed: ${lastChunk.error || 'unknown error'}`)
await this.sendAgentErrorMessage(roomId, streamMessageId, lastChunk.error || 'Run failed', msg, reasoningContent)
this.emitMessageStreamEnd(roomId, streamMessageId)
this.emitMessageStreamEnd(roomId, streamMessageId, 'error')
this.stopTyping(roomId)
onStatus?.('ready')
return
Expand All @@ -581,20 +581,20 @@ class AgentClient {
reasoning: reasoningContent || null,
reasoning_content: reasoningContent || null,
})
this.emitMessageStreamEnd(roomId, streamMessageId)
this.emitMessageStreamEnd(roomId, streamMessageId, 'stop')
await this.refreshRoomFullContextEstimate(roomId, sessionId, bridge, instructions, modelContext)
onStatus?.('ready')
return
}
logger.warn(`[AgentClients] ${this.name}: bridge response completed without content`)
this.emitMessageStreamEnd(roomId, streamMessageId)
this.emitMessageStreamEnd(roomId, streamMessageId, 'stop')
this.stopTyping(roomId)
onStatus?.('ready')
} catch (err: any) {
logger.error(`[AgentClients] ${this.name}: error handling message: ${err.message}`)
try {
await this.sendAgentErrorMessage(roomId, streamMessageId, err, msg, reasoningContent)
if (streamStarted) this.emitMessageStreamEnd(roomId, streamMessageId)
if (streamStarted) this.emitMessageStreamEnd(roomId, streamMessageId, 'error')
} catch (sendErr: any) {
logger.warn(`[AgentClients] ${this.name}: failed to send error message: ${sendErr.message}`)
}
Expand Down