/** * Bridge message management — flush pending content to DB, * track tool calls, manage assistant message lifecycle. */ import { addMessage } from '../../../db/hermes/session-store' import { logger } from '../../logger' import type { SessionMessage, SessionState } from './types' export function flushBridgePendingToDb(state: SessionState, sessionId: string, runMarker?: string) { const content = state.bridgePendingAssistantContent || '' const reasoning = state.bridgePendingReasoningContent || '' if (!content.trim()) return if (runMarker) { const last = findOpenBridgeAssistantMessage(state, runMarker) if (last) syncBridgeReasoningToMessage(last, reasoning) } addMessage({ session_id: sessionId, role: 'assistant', content, reasoning: reasoning || null, reasoning_content: reasoning || null, timestamp: Math.floor(Date.now() / 1000), }) state.bridgePendingAssistantContent = '' state.bridgePendingReasoningContent = '' if (runMarker) { const last = findOpenBridgeAssistantMessage(state, runMarker) if (last && last.finish_reason == null) last.finish_reason = 'stop' } } export function findOpenBridgeAssistantMessage(state: SessionState, runMarker: string): SessionMessage | undefined { return [...state.messages] .reverse() .find(m => m.runMarker === runMarker && m.role === 'assistant' && m.finish_reason == null) } export function ensureOpenBridgeAssistantMessage( state: SessionState, sessionId: string, runMarker: string, ): SessionMessage { const existing = findOpenBridgeAssistantMessage(state, runMarker) if (existing) return existing const message: SessionMessage = { id: state.messages.length + 1, session_id: sessionId, runMarker, role: 'assistant', content: '', timestamp: Math.floor(Date.now() / 1000), } state.messages.push(message) return message } export function syncBridgeReasoningToMessage(message: SessionMessage, reasoning?: string) { if (!reasoning) return message.reasoning = reasoning message.reasoning_content = reasoning } export function recordBridgeToolStarted( state: SessionState, sessionId: string, runMarker: string, toolName: string, args: Record | undefined, rawToolCallId: unknown, ): { id: string; name: string; arguments: string } { const id = bridgeToolCallId(state, rawToolCallId, toolName) const argsString = args ? JSON.stringify(args) : '{}' const reasoning = state.bridgePendingReasoningContent || '' const toolCall = { id, type: 'function', function: { name: toolName, arguments: argsString, }, } const timestamp = Math.floor(Date.now() / 1000) state.bridgePendingTools = state.bridgePendingTools || [] state.bridgePendingTools.push({ id, name: toolName, arguments: argsString, startedAt: Date.now(), }) const openMessage = findOpenBridgeAssistantMessage(state, runMarker) if (openMessage && !openMessage.content && !openMessage.tool_calls?.length) { openMessage.tool_calls = [toolCall] openMessage.finish_reason = 'tool_calls' openMessage.reasoning = reasoning || openMessage.reasoning || null openMessage.reasoning_content = reasoning || openMessage.reasoning_content || null openMessage.timestamp = timestamp } else { state.messages.push({ id: state.messages.length + 1, session_id: sessionId, runMarker, role: 'assistant', content: '', tool_calls: [toolCall], finish_reason: 'tool_calls', reasoning: reasoning || null, reasoning_content: reasoning || null, timestamp, }) } addMessage({ session_id: sessionId, role: 'assistant', content: '', tool_calls: [toolCall], finish_reason: 'tool_calls', reasoning: reasoning || null, reasoning_content: reasoning || null, timestamp, }) state.bridgePendingReasoningContent = '' return { id, name: toolName, arguments: argsString } } export function recordBridgeToolCompleted( state: SessionState, sessionId: string, runMarker: string, toolName: string, ev: Record, ): { id: string; output: string; duration?: number } { state.bridgePendingTools = state.bridgePendingTools || [] const rawId = ev.tool_call_id let idx = rawId ? state.bridgePendingTools.findIndex(tool => tool.id === String(rawId)) : -1 if (idx < 0 && toolName) { idx = state.bridgePendingTools.findIndex(tool => tool.name === toolName) } if (idx < 0) { idx = state.bridgePendingTools.length - 1 } const pending = idx >= 0 ? state.bridgePendingTools.splice(idx, 1)[0] : undefined const id = pending?.id || bridgeToolCallId(state, rawId, toolName) const output = bridgeToolOutput(ev) const timestamp = Math.floor(Date.now() / 1000) logger.info( '[chat-run-socket][bridge] recording CLI tool result session=%s tool=%s tool_call_id=%s raw_tool_call_id=%s output_len=%d has_result=%s has_output=%s has_result_preview=%s has_preview=%s event_keys=%s', sessionId, toolName, id, String(rawId || ''), output.length, String(ev.result != null), String(ev.output != null), String(ev.result_preview != null), String(ev.preview != null), Object.keys(ev).join(','), ) state.messages.push({ id: state.messages.length + 1, session_id: sessionId, runMarker, role: 'tool', content: output, tool_call_id: id, tool_name: toolName || pending?.name || null, timestamp, }) addMessage({ session_id: sessionId, role: 'tool', content: output, tool_call_id: id, tool_name: toolName || pending?.name || null, timestamp, }) const duration = pending?.startedAt ? Math.round((Date.now() - pending.startedAt) / 10) / 100 : undefined return { id, output, duration } } export function bridgeToolCallId(state: SessionState, rawToolCallId: unknown, toolName: string): string { const raw = String(rawToolCallId || '').trim() if (raw) return raw state.bridgeToolCounter = (state.bridgeToolCounter || 0) + 1 const safeName = (toolName || 'tool').replace(/[^a-zA-Z0-9_-]/g, '_') return `cli_${safeName}_${state.bridgeToolCounter}` } export function bridgeToolOutput(ev: Record): string { const value = ev.result ?? ev.output ?? ev.result_preview ?? ev.preview ?? '' return typeof value === 'string' ? value : JSON.stringify(value ?? '') }