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:
@@ -235,7 +235,7 @@ export const buildOpenCodeStatusReport = async (): Promise<string> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
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: 'config', path: '/config', includeDirectory: true },
|
||||||
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
||||||
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
|
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
|
||||||
|
|||||||
@@ -411,44 +411,69 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardSanitizedSessionListRequest = async (req, res, next, logLabel) => {
|
const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null } = {}) => {
|
||||||
try {
|
const headers = 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;
|
|
||||||
const upstreamPath = await canonicalizeDirectoryQuery(upstreamPathRaw);
|
|
||||||
const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
...collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders()),
|
...collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders()),
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
'accept-encoding': 'identity',
|
'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);
|
if (!isJson) {
|
||||||
applyForwardProxyResponseHeaders(upstream.headers, res);
|
return { upstream, contentType, bodyText, payload: null, isJson: false };
|
||||||
|
}
|
||||||
|
|
||||||
const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8';
|
try {
|
||||||
const bodyText = await upstream.text();
|
const payload = JSON.parse(bodyText);
|
||||||
if (!contentType.toLowerCase().includes('application/json')) {
|
return { upstream, contentType, bodyText, payload, isJson: true, parseError: null };
|
||||||
res.setHeader('content-type', contentType);
|
} catch (parseError) {
|
||||||
res.end(bodyText);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload;
|
if (result.parseError || !Array.isArray(result.payload)) {
|
||||||
try {
|
res.setHeader('content-type', result.contentType);
|
||||||
payload = JSON.parse(bodyText);
|
res.end(result.bodyText);
|
||||||
} catch {
|
|
||||||
res.setHeader('content-type', contentType);
|
|
||||||
res.end(bodyText);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('content-type', contentType);
|
res.setHeader('content-type', result.contentType);
|
||||||
res.json(sanitizeSessionListPayload(payload));
|
res.json(sanitizeSessionListPayload(result.payload));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isAbortError(error)) {
|
if (isAbortError(error)) {
|
||||||
return;
|
return;
|
||||||
@@ -529,16 +554,17 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|||||||
const rawUrl = req.originalUrl || req.url || '';
|
const rawUrl = req.originalUrl || req.url || '';
|
||||||
if (rawUrl.includes('directory=')) return next();
|
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 {
|
try {
|
||||||
const authHeaders = getOpenCodeAuthHeaders();
|
const globalSessions = await fetchWindowsSessionList('/session').catch((error) => {
|
||||||
const fetchOpts = {
|
console.log(`[SessionMerge] Global session list failed: ${error.message}`);
|
||||||
method: 'GET',
|
return null;
|
||||||
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 settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
||||||
let projectDirs = [];
|
let projectDirs = [];
|
||||||
@@ -552,11 +578,12 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seen = new Set(
|
const seen = new Set(
|
||||||
globalSessions
|
(globalSessions || [])
|
||||||
.map((session) => (session && typeof session.id === 'string' ? session.id : null))
|
.map((session) => (session && typeof session.id === 'string' ? session.id : null))
|
||||||
.filter((id) => typeof id === 'string')
|
.filter((id) => typeof id === 'string')
|
||||||
);
|
);
|
||||||
const extraSessions = [];
|
const extraSessions = [];
|
||||||
|
let successfulProjectReads = 0;
|
||||||
for (const dir of projectDirs) {
|
for (const dir of projectDirs) {
|
||||||
const candidates = Array.from(new Set([
|
const candidates = Array.from(new Set([
|
||||||
dir,
|
dir,
|
||||||
@@ -566,16 +593,15 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|||||||
for (const candidateDir of candidates) {
|
for (const candidateDir of candidates) {
|
||||||
const encoded = encodeURIComponent(candidateDir);
|
const encoded = encodeURIComponent(candidateDir);
|
||||||
try {
|
try {
|
||||||
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
|
const dirSessions = await fetchWindowsSessionList(`/session?directory=${encoded}`);
|
||||||
if (dirRes.ok) {
|
if (dirSessions) {
|
||||||
const dirPayload = await dirRes.json().catch(() => []);
|
successfulProjectReads += 1;
|
||||||
const dirSessions = Array.isArray(dirPayload) ? dirPayload : [];
|
}
|
||||||
for (const session of dirSessions) {
|
for (const session of dirSessions || []) {
|
||||||
const id = session && typeof session.id === 'string' ? session.id : null;
|
const id = session && typeof session.id === 'string' ? session.id : null;
|
||||||
if (id && !seen.has(id)) {
|
if (id && !seen.has(id)) {
|
||||||
seen.add(id);
|
seen.add(id);
|
||||||
extraSessions.push(session);
|
extraSessions.push(session);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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) => {
|
merged.sort((a, b) => {
|
||||||
const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
|
const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
|
||||||
const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
|
const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
|
||||||
return bTime - aTime;
|
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));
|
return res.json(sanitizeSessionListPayload(merged));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[SessionMerge] Error: ${error.message}, falling through`);
|
console.log(`[SessionMerge] Error: ${error.message}`);
|
||||||
next();
|
return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user