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
@@ -14,7 +14,9 @@
*/
import { encodingForModel, getEncoding } from 'js-tiktoken'
import { randomUUID } from 'crypto'
import { logger } from '../../services/logger'
import { AgentBridgeClient, type AgentBridgeRunResult } from '../../services/hermes/agent-bridge'
import {
getCompressionSnapshot,
saveCompressionSnapshot,
@@ -70,6 +72,12 @@ export interface CompressedResult {
}
}
export interface SummarizerOptions {
profile?: string
model?: string | null
provider?: string | null
}
// ─── Token counting ─────────────────────────────────────
let _encoder: ReturnType<typeof getEncoding> | null = null
@@ -372,8 +380,14 @@ export async function callSummarizer(
history: Array<{ role: string; content: string }>,
timeoutMs: number,
previousSummary?: string,
profile?: string,
summarizer?: string | SummarizerOptions,
): Promise<string> {
void upstream
void apiKey
const options: SummarizerOptions = typeof summarizer === 'string'
? { profile: summarizer }
: summarizer || {}
const profile = options.profile || 'default'
const convHistory: Array<{ role: string; content: string }> = [...history]
if (previousSummary) {
@@ -383,60 +397,38 @@ export async function callSummarizer(
)
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
const bridge = new AgentBridgeClient({ timeoutMs: timeoutMs + 15_000 })
const sessionId = `compress_${Date.now().toString(36)}_${randomUUID().replace(/-/g, '').slice(0, 12)}`
const res = await fetch(`${upstream.replace(/\/$/, '')}/v1/responses`, {
method: 'POST',
headers,
body: JSON.stringify({
input: prompt,
try {
const result = await bridge.request<AgentBridgeRunResult>({
action: 'chat',
session_id: sessionId,
message: prompt,
conversation_history: convHistory,
stream: true,
store: false,
}),
signal: AbortSignal.timeout(timeoutMs),
})
profile,
source: 'api_server',
wait: true,
timeout: Math.ceil(timeoutMs / 1000),
...(options.model ? { model: options.model } : {}),
...(options.provider ? { provider: options.provider } : {}),
}, { timeoutMs: timeoutMs + 15_000 })
if (!res.ok) {
throw new Error(`Summarization response failed: ${res.status}`)
if (result.status === 'error') {
throw new Error(result.error || 'Summarization bridge run failed')
}
const payload = result.result as any
const output = String(
payload?.final_response ||
result.output ||
'',
).trim()
if (!output) throw new Error('Empty summarization response')
return output
} finally {
await bridge.destroy(sessionId, profile).catch(() => undefined)
}
if (!res.body) {
throw new Error('Summarization response stream missing')
}
let output = ''
for await (const frame of readSseFrames(res.body)) {
let parsed: any
try {
parsed = JSON.parse(frame.data)
} catch {
continue
}
const eventType = parsed.type || frame.event || parsed.event
if (eventType === 'response.output_text.delta' && parsed.delta) {
output += parsed.delta
continue
}
if (eventType === 'response.completed') {
const response = parsed.response || parsed
const finalText = extractResponseText(response)
if (!output && finalText) output = finalText
if (!output || output.trim() === '') {
throw new Error('Empty summarization response')
}
return output.trim()
}
if (eventType === 'response.failed') {
throw new Error(parsed.error?.message || parsed.error || 'Summarization response failed')
}
}
throw new Error('Summarization response stream ended without a terminal event')
}
// ─── Main Compressor ────────────────────────────────────
@@ -465,7 +457,7 @@ export class ChatContextCompressor {
upstream: string,
apiKey: string | undefined,
sessionId?: string,
profile?: string,
summarizer?: string | SummarizerOptions,
): Promise<CompressedResult> {
const total = messages.length
@@ -489,7 +481,7 @@ export class ChatContextCompressor {
sessionId, snapshot.lastMessageIndex,
)
return this.incrementalCompress(
messages, snapshot, upstream, apiKey, sessionId!, makeMeta(), profile,
messages, snapshot, upstream, apiKey, sessionId!, makeMeta(), summarizer,
)
} else {
// No snapshot → full compress (compress all messages)
@@ -497,7 +489,7 @@ export class ChatContextCompressor {
'[context-compressor] session=%s: full compress %d messages',
sessionId, total,
)
return this.fullCompress(messages, upstream, apiKey, sessionId!, makeMeta(), profile)
return this.fullCompress(messages, upstream, apiKey, sessionId!, makeMeta(), summarizer)
}
}
@@ -508,7 +500,7 @@ export class ChatContextCompressor {
apiKey: string | undefined,
sessionId: string,
meta: CompressedResult['meta'],
profile?: string,
summarizer?: string | SummarizerOptions,
): Promise<CompressedResult> {
const { summary: previousSummary, lastMessageIndex } = snapshot
const total = messages.length
@@ -550,7 +542,7 @@ export class ChatContextCompressor {
const history = buildConversationHistory(toCompress)
const t0 = Date.now()
summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, previousSummary, profile)
summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, previousSummary, summarizer)
logger.info('[context-compressor] incremental-llm done in %dms, %d chars', Date.now() - t0, summary.length)
} catch (err: any) {
logger.warn('[context-compressor] incremental-llm failed: %s — keeping new messages verbatim', err.message)
@@ -599,7 +591,7 @@ export class ChatContextCompressor {
apiKey: string | undefined,
sessionId: string,
meta: CompressedResult['meta'],
profile?: string,
summarizer?: string | SummarizerOptions,
): Promise<CompressedResult> {
const total = messages.length
const cleaned = pruneOldToolResults(messages, this.config.tailMessageCount)
@@ -625,7 +617,7 @@ export class ChatContextCompressor {
let summary: string | null = null
try {
const t0 = Date.now()
summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, undefined, profile)
summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, undefined, summarizer)
logger.info('[context-compressor] full-llm done in %dms, %d chars', Date.now() - t0, summary.length)
} catch (err: any) {
logger.warn('[context-compressor] full-llm failed: %s', err.message)