Fix bridge history, profile models, and Windows gateway handling (#845)

* feat: support profile-aware group chat bridge flows

* feat: route cron jobs through hermes cli

* Fix group chat routing and isolate bridge tests

* Add Grok image-to-video media skill

* Default Grok videos to media directory

* Fix bridge profile fallback and cron repeat clearing

* Refine bridge chat and gateway platform handling

* Filter bridge tool-call text deltas

* Preserve structured bridge chat history

* Prepare beta release build artifacts

* Fix Windows run profile resolution

* Fix Windows path compatibility checks

* Fix profile-scoped model page display

* Hide Windows subprocess windows for jobs and updates

* Hide Windows file backend subprocess windows

* Avoid Windows gateway restart lock conflicts

* Treat Windows gateway lock as running on startup

* Force release Windows gateway lock on restart

* Tighten Windows gateway lock cleanup

* Update chat e2e source expectation

* Bump package version to 0.5.30

---------

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
ekko
2026-05-19 16:09:59 +08:00
committed by GitHub
parent 3d74d78698
commit 9a9416c99c
129 changed files with 7017 additions and 1838 deletions
@@ -9,7 +9,6 @@ import { getSession, createSession, addMessage, updateSession, updateSessionStat
import { updateUsage } from '../../../db/hermes/usage-store'
import { logger, bridgeLogger } from '../../logger'
import { AgentBridgeClient, type AgentBridgeMessage, type AgentBridgeOutput } from '../agent-bridge'
import { readConfigYaml } from '../../config-helpers'
import { contentBlocksToString, convertContentBlocksForAgent, extractTextForPreview, isContentBlockArray } from './content-blocks'
import { buildCompressedHistory } from './compression'
import { pushState, replaceState } from './compression'
@@ -24,43 +23,19 @@ import {
import { forceCompressBridgeHistory } from './compression'
import { summarizeToolArguments } from './response-utils'
import { buildDbHistory } from './compression'
import { convertHistoryFormat } from './message-format'
import type { ContentBlock, SessionState } from './types'
import type { ChatMessage } from '../../../lib/context-compressor'
import { resolveBridgeRunModelConfig, type RunModelGroup } from './model-config'
import { filterBridgeToolCallMarkupDelta } from './bridge-delta'
const BRIDGE_USAGE_FLUSH_DELAY_MS = 200
type RunModelGroup = { provider: string; models: string[] }
async function resolveDefaultModelConfig(): Promise<{ model: string; provider: string }> {
try {
const config = await readConfigYaml()
const modelConfig = config?.model
const model = typeof modelConfig === 'string'
? modelConfig.trim()
: String(modelConfig?.default || '').trim()
const provider = typeof modelConfig === 'object'
? String(modelConfig?.provider || '').trim()
: ''
return { model, provider }
} catch {
return { model: '', provider: '' }
}
}
function hasModelInGroups(groups: RunModelGroup[] | undefined, provider: string, model: string): boolean {
if (!groups?.length || !provider || !model) return false
const group = groups.find(item => item.provider === provider)
return Array.isArray(group?.models) && group.models.includes(model)
}
export async function handleBridgeRun(
nsp: ReturnType<Server['of']>,
socket: Socket,
data: { input: string | ContentBlock[]; session_id?: string; model?: string; provider?: string; model_groups?: RunModelGroup[]; instructions?: string; source?: string },
profile: string,
sessionMap: Map<string, SessionState>,
gatewayManager: any,
bridge: AgentBridgeClient,
_skipUserMessage = false,
loadSessionStateFromDbFn: (sid: string, sessionMap: Map<string, SessionState>) => Promise<SessionState>,
@@ -78,14 +53,14 @@ export async function handleBridgeRun(
const sessionRow = getSession(session_id)
const sessionModel = sessionRow?.model || ''
const sessionProvider = sessionRow?.provider || ''
const hasGroups = Array.isArray(data.model_groups) && data.model_groups.length > 0
const sessionModelAvailable = hasGroups && hasModelInGroups(data.model_groups, sessionProvider, sessionModel)
const shouldUseDefault = !sessionModel || !sessionProvider || !sessionModelAvailable
const defaultModelConfig = shouldUseDefault
? await resolveDefaultModelConfig()
: { model: '', provider: '' }
const resolvedModel = shouldUseDefault ? defaultModelConfig.model : sessionModel
const resolvedProvider = shouldUseDefault ? defaultModelConfig.provider : sessionProvider
const { model: resolvedModel, provider: resolvedProvider } = await resolveBridgeRunModelConfig({
profile,
sessionModel,
sessionProvider,
requestedModel: data.model,
requestedProvider: data.provider,
modelGroups: data.model_groups,
})
if (sessionRow) {
const updates: { model?: string; provider?: string } = {}
if (resolvedModel && sessionRow.model !== resolvedModel) updates.model = resolvedModel
@@ -117,6 +92,7 @@ export async function handleBridgeRun(
state.bridgeOutput = ''
state.bridgePendingAssistantContent = ''
state.bridgePendingReasoningContent = ''
state.bridgePendingToolCallMarkup = ''
state.bridgeToolCounter = 0
state.bridgePendingTools = []
state.responseRun = undefined
@@ -154,12 +130,13 @@ export async function handleBridgeRun(
const history = await buildCompressedHistory(
session_id, profile,
gatewayManager.getUpstream(profile).replace(/\/$/, ''),
gatewayManager.getApiKey(profile) || undefined,
'',
undefined,
emit,
sessionMap,
{ model: resolvedModel, provider: resolvedProvider },
)
const bridgeHistory = history.length > 0 ? convertHistoryFormat(history) : history
const bridgeHistory = history
try {
const bridgeInput = isContentBlockArray(input)
@@ -207,7 +184,7 @@ export async function handleBridgeRun(
})
for await (const chunk of bridge.streamOutput(started.run_id)) {
await applyBridgeChunkAsync(nsp, socket, state, session_id, runMarker, chunk, emit, profile, sessionMap, gatewayManager, bridge, dequeueNextQueuedRun)
await applyBridgeChunkAsync(nsp, socket, state, session_id, runMarker, chunk, emit, profile, sessionMap, bridge, dequeueNextQueuedRun)
if (chunk.done) break
}
} catch (err: any) {
@@ -220,6 +197,7 @@ export async function handleBridgeRun(
state.runId = undefined
state.activeRunMarker = undefined
state.events = []
state.bridgePendingToolCallMarkup = undefined
flushBridgePendingToDb(state, session_id)
updateSessionStats(session_id)
const message = err instanceof Error ? err.message : String(err)
@@ -244,7 +222,6 @@ async function applyBridgeChunkAsync(
emit: (event: string, payload: any) => void,
profile: string,
sessionMap: Map<string, SessionState>,
gatewayManager: any,
bridge: AgentBridgeClient,
dequeueNextQueuedRun: (socket: Socket, sessionId: string, fallbackProfile?: string) => void,
): Promise<void> {
@@ -357,8 +334,6 @@ async function applyBridgeChunkAsync(
sessionId,
profile,
ev.messages as ChatMessage[],
(p: string) => gatewayManager.getUpstream(p),
(p: string) => gatewayManager.getApiKey(p),
)
state.bridgeCompressionResults = state.bridgeCompressionResults || {}
state.bridgeCompressionResults[String(ev.request_id)] = compressed
@@ -421,30 +396,33 @@ async function applyBridgeChunkAsync(
}
if (chunk.delta) {
state.bridgeOutput = (state.bridgeOutput || '') + chunk.delta
state.bridgePendingAssistantContent = (state.bridgePendingAssistantContent || '') + chunk.delta
const last = [...state.messages].reverse().find(m => m.runMarker === runMarker)
if (last?.role === 'assistant' && last.finish_reason == null) {
last.content += chunk.delta
syncBridgeReasoningToMessage(last, state.bridgePendingReasoningContent)
} else {
state.messages.push({
id: state.messages.length + 1,
session_id: sessionId,
runMarker,
role: 'assistant',
content: chunk.delta,
reasoning: state.bridgePendingReasoningContent || null,
reasoning_content: state.bridgePendingReasoningContent || null,
timestamp: Math.floor(Date.now() / 1000),
const delta = filterBridgeToolCallMarkupDelta(state, chunk.delta)
if (delta) {
state.bridgeOutput = (state.bridgeOutput || '') + delta
state.bridgePendingAssistantContent = (state.bridgePendingAssistantContent || '') + delta
const last = [...state.messages].reverse().find(m => m.runMarker === runMarker)
if (last?.role === 'assistant' && last.finish_reason == null) {
last.content += delta
syncBridgeReasoningToMessage(last, state.bridgePendingReasoningContent)
} else {
state.messages.push({
id: state.messages.length + 1,
session_id: sessionId,
runMarker,
role: 'assistant',
content: delta,
reasoning: state.bridgePendingReasoningContent || null,
reasoning_content: state.bridgePendingReasoningContent || null,
timestamp: Math.floor(Date.now() / 1000),
})
}
emit('message.delta', {
event: 'message.delta',
run_id: chunk.run_id,
delta,
output: state.bridgeOutput,
})
}
emit('message.delta', {
event: 'message.delta',
run_id: chunk.run_id,
delta: chunk.delta,
output: state.bridgeOutput,
})
}
if (!chunk.done) return
@@ -459,6 +437,7 @@ async function applyBridgeChunkAsync(
}
flushBridgePendingToDb(state, sessionId)
state.bridgePendingToolCallMarkup = undefined
updateSessionStats(sessionId)
await delay(BRIDGE_USAGE_FLUSH_DELAY_MS)
const usage = await calcAndUpdateUsage(sessionId, state, emit)