fix(api): optimize opencode api health check time and workspace switching

This commit is contained in:
Bohdan Triapitsyn
2025-12-26 02:29:00 +02:00
parent e62bf9e811
commit 96fd94fa7b
4 changed files with 40 additions and 72 deletions
+1 -1
View File
@@ -2847,7 +2847,7 @@ dependencies = [
[[package]] [[package]]
name = "openchamber-desktop" name = "openchamber-desktop"
version = "1.3.3" version = "1.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -108,7 +108,7 @@ impl OpenCodeManager {
is_ready: Arc::new(AtomicBool::new(false)), is_ready: Arc::new(AtomicBool::new(false)),
shutting_down: Arc::new(AtomicBool::new(false)), shutting_down: Arc::new(AtomicBool::new(false)),
http_client: Client::builder() http_client: Client::builder()
.timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(2))
.build() .build()
.unwrap(), .unwrap(),
} }
@@ -406,11 +406,9 @@ impl OpenCodeManager {
while tokio::time::Instant::now() < deadline { while tokio::time::Instant::now() < deadline {
let api_prefix = self.api_prefix(); let api_prefix = self.api_prefix();
// Try /health, /config, /agent endpoints // Try /config, /agent endpoints
match self.check_endpoints(port, &api_prefix).await { match self.check_endpoints(port, &api_prefix).await {
Ok(()) => { Ok(()) => {
// Once ready, attempt to detect and persist the API prefix for proxying
let _ = self.detect_api_prefix().await;
return Ok(()); return Ok(());
} }
Err(e) => { Err(e) => {
@@ -431,23 +429,20 @@ impl OpenCodeManager {
async fn check_endpoints(&self, port: u16, prefix: &str) -> Result<()> { async fn check_endpoints(&self, port: u16, prefix: &str) -> Result<()> {
let base_url = format!("http://127.0.0.1:{port}{prefix}"); let base_url = format!("http://127.0.0.1:{port}{prefix}");
// Check /health
let health_url = format!("{base_url}/health");
let health_resp = self.http_client.get(&health_url).send().await?;
if !health_resp.status().is_success() {
return Err(anyhow!("/health returned {}", health_resp.status()));
}
// Check /config
let config_url = format!("{base_url}/config"); let config_url = format!("{base_url}/config");
let config_resp = self.http_client.get(&config_url).send().await?; let agent_url = format!("{base_url}/agent");
let (config_resp, agent_resp) = tokio::join!(
self.http_client.get(&config_url).send(),
self.http_client.get(&agent_url).send()
);
let config_resp = config_resp?;
if !config_resp.status().is_success() { if !config_resp.status().is_success() {
return Err(anyhow!("/config returned {}", config_resp.status())); return Err(anyhow!("/config returned {}", config_resp.status()));
} }
// Check /agent let agent_resp = agent_resp?;
let agent_url = format!("{base_url}/agent");
let agent_resp = self.http_client.get(&agent_url).send().await?;
if !agent_resp.status().is_success() { if !agent_resp.status().is_success() {
return Err(anyhow!("/agent returned {}", agent_resp.status())); return Err(anyhow!("/agent returned {}", agent_resp.status()));
} }
@@ -497,8 +492,7 @@ impl OpenCodeManager {
// SIGKILL // SIGKILL
let _ = child.kill().await; let _ = child.kill().await;
// Wait up to 5 seconds for hard kill match timeout(Duration::from_secs(2), child.wait()).await {
match timeout(Duration::from_secs(5), child.wait()).await {
Ok(_) => { Ok(_) => {
info!("[desktop:opencode] exited after SIGKILL"); info!("[desktop:opencode] exited after SIGKILL");
} }
+9 -15
View File
@@ -182,16 +182,10 @@ async function checkHealth(apiUrl: string, quick = false): Promise<boolean> {
const controller = new AbortController(); const controller = new AbortController();
const timeoutMs = quick ? 1500 : 3000; const timeoutMs = quick ? 1500 : 3000;
const timeout = setTimeout(() => controller.abort(), timeoutMs); const timeout = setTimeout(() => controller.abort(), timeoutMs);
const normalized = apiUrl.replace(/\/+$/, ''); const normalized = apiUrl.replace(/\/+$/, '');
const candidates: string[] = [`${normalized}/config`]; const candidates: string[] = [`${normalized}/config`];
// Some deployments expose a /health endpoint (not guaranteed for OpenCode).
if (!quick) {
const healthUrl = normalized.endsWith('/api') ? `${normalized.slice(0, -4)}/health` : `${normalized}/health`;
candidates.push(healthUrl);
}
for (const target of candidates) { for (const target of candidates) {
try { try {
const response = await fetch(target, { const response = await fetch(target, {
@@ -227,7 +221,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
let lastStartAt: number | null = null; let lastStartAt: number | null = null;
let lastConnectedAt: number | null = null; let lastConnectedAt: number | null = null;
let lastExitCode: number | null = null; let lastExitCode: number | null = null;
// Port detection state (like desktop) // Port detection state (like desktop)
let detectedPort: number | null = null; let detectedPort: number | null = null;
let portWaiters: Array<(port: number) => void> = []; let portWaiters: Array<(port: number) => void> = [];
@@ -235,12 +229,12 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
// OpenCode API prefix detection (some versions serve under /api) // OpenCode API prefix detection (some versions serve under /api)
let apiPrefix: string = ''; let apiPrefix: string = '';
let apiPrefixDetected = false; let apiPrefixDetected = false;
// Check if user configured a specific API URL // Check if user configured a specific API URL
const config = vscode.workspace.getConfiguration('openchamber'); const config = vscode.workspace.getConfiguration('openchamber');
const configuredApiUrl = config.get<string>('apiUrl') || ''; const configuredApiUrl = config.get<string>('apiUrl') || '';
const useConfiguredUrl = configuredApiUrl && configuredApiUrl.trim().length > 0; const useConfiguredUrl = configuredApiUrl && configuredApiUrl.trim().length > 0;
// Parse configured URL to extract port if specified // Parse configured URL to extract port if specified
let configuredPort: number | null = null; let configuredPort: number | null = null;
if (useConfiguredUrl) { if (useConfiguredUrl) {
@@ -367,7 +361,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
function setDetectedPort(port: number) { function setDetectedPort(port: number) {
if (detectedPort !== port) { if (detectedPort !== port) {
detectedPort = port; detectedPort = port;
// Notify all waiters // Notify all waiters
const waiters = portWaiters; const waiters = portWaiters;
portWaiters = []; portWaiters = [];
@@ -429,7 +423,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
async function waitForReady(apiUrl: string, timeoutMs: number): Promise<boolean> { async function waitForReady(apiUrl: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) { while (Date.now() < deadline) {
// Use quick health check during startup for faster response // Use quick health check during startup for faster response
if (await checkHealth(apiUrl, true)) { if (await checkHealth(apiUrl, true)) {
@@ -437,7 +431,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
} }
await new Promise(r => setTimeout(r, READY_CHECK_INTERVAL_MS)); await new Promise(r => setTimeout(r, READY_CHECK_INTERVAL_MS));
} }
return false; return false;
} }
@@ -461,7 +455,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
} }
return; return;
} }
const healthy = await checkHealth(apiUrl); const healthy = await checkHealth(apiUrl);
if (healthy && status !== 'connected') { if (healthy && status !== 'connected') {
setStatus('connected'); setStatus('connected');
@@ -529,7 +523,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
lastExitCode = null; lastExitCode = null;
const spawnCwd = workingDirectory || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir(); const spawnCwd = workingDirectory || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
// Use port 0 for dynamic assignment unless user configured a specific port // Use port 0 for dynamic assignment unless user configured a specific port
const portArg = configuredPort !== null ? configuredPort.toString() : '0'; const portArg = configuredPort !== null ? configuredPort.toString() : '0';
+18 -38
View File
@@ -1356,29 +1356,18 @@ async function waitForOpenCodeReady(timeoutMs = 20000, intervalMs = 400) {
for (const prefix of prefixes) { for (const prefix of prefixes) {
try { try {
const normalizedPrefix = normalizeApiPrefix(prefix); const normalizedPrefix = normalizeApiPrefix(prefix);
const healthPromise = fetch(buildOpenCodeUrl('/health', normalizedPrefix), {
method: 'GET',
headers: { Accept: 'application/json' }
}).catch((error) => error);
const configPromise = fetch(buildOpenCodeUrl('/config', normalizedPrefix), { const configPromise = fetch(buildOpenCodeUrl('/config', normalizedPrefix), {
method: 'GET', method: 'GET',
headers: { Accept: 'application/json' } headers: { Accept: 'application/json' }
}).catch((error) => error); }).catch((error) => error);
const [healthResult, configResult] = await Promise.all([healthPromise, configPromise]); const agentPromise = fetch(buildOpenCodeUrl('/agent', normalizedPrefix), {
method: 'GET',
headers: { Accept: 'application/json' }
}).catch((error) => error);
if (healthResult instanceof Error) { const [configResult, agentResult] = await Promise.all([configPromise, agentPromise]);
lastError = healthResult;
} else if (healthResult.ok) {
const healthData = await healthResult.json().catch(() => null);
if (healthData && healthData.isOpenCodeReady === false) {
lastError = new Error('OpenCode health indicates not ready');
continue;
}
} else {
lastError = new Error(`OpenCode health endpoint responded with status ${healthResult.status}`);
}
if (configResult instanceof Error) { if (configResult instanceof Error) {
lastError = configResult; lastError = configResult;
@@ -1402,30 +1391,21 @@ async function waitForOpenCodeReady(timeoutMs = 20000, intervalMs = 400) {
setDetectedOpenCodeApiPrefix(normalizedPrefix); setDetectedOpenCodeApiPrefix(normalizedPrefix);
} }
if (agentResult instanceof Error) {
lastError = agentResult;
continue;
}
if (!agentResult.ok) {
lastError = new Error(`Agent endpoint responded with status ${agentResult.status}`);
continue;
}
await agentResult.json().catch(() => []);
const effectivePrefix = detectedPrefix !== null ? detectedPrefix : normalizedPrefix; const effectivePrefix = detectedPrefix !== null ? detectedPrefix : normalizedPrefix;
const agentResponse = await fetch(
buildOpenCodeUrl('/agent', effectivePrefix),
{
method: 'GET',
headers: { Accept: 'application/json' }
}
).catch((error) => error);
if (agentResponse instanceof Error) {
lastError = agentResponse;
continue;
}
if (!agentResponse.ok) {
lastError = new Error(`Agent endpoint responded with status ${agentResponse.status}`);
continue;
}
await agentResponse.json().catch(() => []);
if (detectedPrefix === null) { if (detectedPrefix === null) {
const agentPrefix = extractApiPrefixFromUrl(agentResponse.url, '/agent'); const agentPrefix = extractApiPrefixFromUrl(agentResult.url, '/agent');
if (agentPrefix !== null) { if (agentPrefix !== null) {
setDetectedOpenCodeApiPrefix(agentPrefix); setDetectedOpenCodeApiPrefix(agentPrefix);
} else if (normalizedPrefix) { } else if (normalizedPrefix) {