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
This commit is contained in:
Bohdan Triapitsyn
2026-06-16 14:05:44 +03:00
parent 9199798a14
commit 8f1da2f728
2 changed files with 82 additions and 52 deletions
+1 -1
View File
@@ -235,7 +235,7 @@ export const buildOpenCodeStatusReport = async (): Promise<string> => {
};
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 },
+81 -51
View File
@@ -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' });
}
});
}