From 8f1da2f728abb947e4ec2fccb33267221f214d40 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 16 Jun 2026 14:05:44 +0300 Subject: [PATCH] fix: stabilize session diagnostics and Windows session loading Fix duplicated health probe URL in diagnostics Share session list proxy handling across platforms Avoid repeated hanging session requests on Windows --- packages/ui/src/lib/openCodeStatus.ts | 2 +- packages/web/server/lib/opencode/proxy.js | 132 +++++++++++++--------- 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/packages/ui/src/lib/openCodeStatus.ts b/packages/ui/src/lib/openCodeStatus.ts index 68652db2..a6b1f429 100644 --- a/packages/ui/src/lib/openCodeStatus.ts +++ b/packages/ui/src/lib/openCodeStatus.ts @@ -235,7 +235,7 @@ export const buildOpenCodeStatusReport = async (): Promise => { }; const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [ - { label: 'health', path: '/api/health', includeDirectory: false }, + { label: 'health', path: '/health', includeDirectory: false }, { label: 'config', path: '/config', includeDirectory: true }, { label: 'providers', path: '/config/providers', includeDirectory: true }, { label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 }, diff --git a/packages/web/server/lib/opencode/proxy.js b/packages/web/server/lib/opencode/proxy.js index 327e2232..73897026 100644 --- a/packages/web/server/lib/opencode/proxy.js +++ b/packages/web/server/lib/opencode/proxy.js @@ -411,44 +411,69 @@ export const registerOpenCodeProxy = (app, deps) => { } }; - const forwardSanitizedSessionListRequest = async (req, res, next, logLabel) => { - try { - const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0 - ? req.originalUrl - : (typeof req.url === 'string' ? req.url : ''); - const upstreamPathRaw = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl; - const upstreamPath = await canonicalizeDirectoryQuery(upstreamPathRaw); - const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), { - method: 'GET', - headers: { + const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null } = {}) => { + const headers = req + ? { ...collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders()), accept: 'application/json', 'accept-encoding': 'identity', - }, - }); + } + : { + Accept: 'application/json', + ...getOpenCodeAuthHeaders(), + 'accept-encoding': 'identity', + }; + const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), { + method: 'GET', + headers, + ...(typeof timeoutMs === 'number' ? { signal: AbortSignal.timeout(timeoutMs) } : {}), + }); + const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8'; + const bodyText = await upstream.text(); + const isJson = contentType.toLowerCase().includes('application/json'); - res.status(upstream.status); - applyForwardProxyResponseHeaders(upstream.headers, res); + if (!isJson) { + return { upstream, contentType, bodyText, payload: null, isJson: false }; + } - const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8'; - const bodyText = await upstream.text(); - if (!contentType.toLowerCase().includes('application/json')) { - res.setHeader('content-type', contentType); - res.end(bodyText); + try { + const payload = JSON.parse(bodyText); + return { upstream, contentType, bodyText, payload, isJson: true, parseError: null }; + } catch (parseError) { + return { upstream, contentType, bodyText, payload: null, isJson: true, parseError }; + } + }; + + const getRequestUpstreamPath = async (req) => { + const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0 + ? req.originalUrl + : (typeof req.url === 'string' ? req.url : ''); + const upstreamPathRaw = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl; + return canonicalizeDirectoryQuery(upstreamPathRaw); + }; + + const forwardSanitizedSessionListRequest = async (req, res, next, logLabel) => { + try { + const upstreamPath = await getRequestUpstreamPath(req); + const result = await fetchSessionListPayload(upstreamPath, { req }); + + res.status(result.upstream.status); + applyForwardProxyResponseHeaders(result.upstream.headers, res); + + if (!result.isJson) { + res.setHeader('content-type', result.contentType); + res.end(result.bodyText); return; } - let payload; - try { - payload = JSON.parse(bodyText); - } catch { - res.setHeader('content-type', contentType); - res.end(bodyText); + if (result.parseError || !Array.isArray(result.payload)) { + res.setHeader('content-type', result.contentType); + res.end(result.bodyText); return; } - res.setHeader('content-type', contentType); - res.json(sanitizeSessionListPayload(payload)); + res.setHeader('content-type', result.contentType); + res.json(sanitizeSessionListPayload(result.payload)); } catch (error) { if (isAbortError(error)) { return; @@ -529,16 +554,17 @@ export const registerOpenCodeProxy = (app, deps) => { const rawUrl = req.originalUrl || req.url || ''; if (rawUrl.includes('directory=')) return next(); + const fetchWindowsSessionList = async (sessionPath) => { + const result = await fetchSessionListPayload(sessionPath, { req, timeoutMs: 10000 }); + if (!result.upstream.ok || !Array.isArray(result.payload)) return null; + return sanitizeSessionListPayload(result.payload); + }; + try { - const authHeaders = getOpenCodeAuthHeaders(); - const fetchOpts = { - method: 'GET', - headers: { Accept: 'application/json', ...authHeaders }, - signal: AbortSignal.timeout(10000), - }; - const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts); - const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : []; - const globalSessions = Array.isArray(globalPayload) ? globalPayload : []; + const globalSessions = await fetchWindowsSessionList('/session').catch((error) => { + console.log(`[SessionMerge] Global session list failed: ${error.message}`); + return null; + }); const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json'); let projectDirs = []; @@ -552,11 +578,12 @@ export const registerOpenCodeProxy = (app, deps) => { } const seen = new Set( - globalSessions + (globalSessions || []) .map((session) => (session && typeof session.id === 'string' ? session.id : null)) .filter((id) => typeof id === 'string') ); const extraSessions = []; + let successfulProjectReads = 0; for (const dir of projectDirs) { const candidates = Array.from(new Set([ dir, @@ -566,16 +593,15 @@ export const registerOpenCodeProxy = (app, deps) => { for (const candidateDir of candidates) { const encoded = encodeURIComponent(candidateDir); try { - const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts); - if (dirRes.ok) { - const dirPayload = await dirRes.json().catch(() => []); - const dirSessions = Array.isArray(dirPayload) ? dirPayload : []; - for (const session of dirSessions) { - const id = session && typeof session.id === 'string' ? session.id : null; - if (id && !seen.has(id)) { - seen.add(id); - extraSessions.push(session); - } + const dirSessions = await fetchWindowsSessionList(`/session?directory=${encoded}`); + if (dirSessions) { + successfulProjectReads += 1; + } + for (const session of dirSessions || []) { + const id = session && typeof session.id === 'string' ? session.id : null; + if (id && !seen.has(id)) { + seen.add(id); + extraSessions.push(session); } } } catch { @@ -583,17 +609,21 @@ export const registerOpenCodeProxy = (app, deps) => { } } - const merged = [...globalSessions, ...extraSessions]; + if (!globalSessions && successfulProjectReads === 0) { + return res.status(504).json({ error: 'OpenCode session list timed out' }); + } + + const merged = [...(globalSessions || []), ...extraSessions]; merged.sort((a, b) => { const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0; const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0; return bTime - aTime; }); - console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`); + console.log(`[SessionMerge] ${globalSessions?.length || 0} global + ${extraSessions.length} extra = ${merged.length} total`); return res.json(sanitizeSessionListPayload(merged)); } catch (error) { - console.log(`[SessionMerge] Error: ${error.message}, falling through`); - next(); + console.log(`[SessionMerge] Error: ${error.message}`); + return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' }); } }); }