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 } = {}) => {
|
||||||
|
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');
|
||||||
|
|
||||||
|
if (!isJson) {
|
||||||
|
return { upstream, contentType, bodyText, payload: null, isJson: false };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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
|
const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0
|
||||||
? req.originalUrl
|
? req.originalUrl
|
||||||
: (typeof req.url === 'string' ? req.url : '');
|
: (typeof req.url === 'string' ? req.url : '');
|
||||||
const upstreamPathRaw = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl;
|
const upstreamPathRaw = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl;
|
||||||
const upstreamPath = await canonicalizeDirectoryQuery(upstreamPathRaw);
|
return canonicalizeDirectoryQuery(upstreamPathRaw);
|
||||||
const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
|
};
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
...collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders()),
|
|
||||||
accept: 'application/json',
|
|
||||||
'accept-encoding': 'identity',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(upstream.status);
|
const forwardSanitizedSessionListRequest = async (req, res, next, logLabel) => {
|
||||||
applyForwardProxyResponseHeaders(upstream.headers, res);
|
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload;
|
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(bodyText);
|
const upstreamPath = await getRequestUpstreamPath(req);
|
||||||
} catch {
|
const result = await fetchSessionListPayload(upstreamPath, { req });
|
||||||
res.setHeader('content-type', contentType);
|
|
||||||
res.end(bodyText);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('content-type', contentType);
|
if (result.parseError || !Array.isArray(result.payload)) {
|
||||||
res.json(sanitizeSessionListPayload(payload));
|
res.setHeader('content-type', result.contentType);
|
||||||
|
res.end(result.bodyText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('content-type', result.contentType);
|
||||||
|
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();
|
||||||
|
|
||||||
try {
|
const fetchWindowsSessionList = async (sessionPath) => {
|
||||||
const authHeaders = getOpenCodeAuthHeaders();
|
const result = await fetchSessionListPayload(sessionPath, { req, timeoutMs: 10000 });
|
||||||
const fetchOpts = {
|
if (!result.upstream.ok || !Array.isArray(result.payload)) return null;
|
||||||
method: 'GET',
|
return sanitizeSessionListPayload(result.payload);
|
||||||
headers: { Accept: 'application/json', ...authHeaders },
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
};
|
};
|
||||||
const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
|
|
||||||
const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : [];
|
try {
|
||||||
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');
|
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,34 +593,37 @@ 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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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