fix: auto-detect and safely connect to existing OpenCode server on startup (#402)

Add probeExternalOpenCode() to detect a running OpenCode instance via
GET /global/health before attempting to spawn a new one. When an external
server is detected (or OPENCODE_SKIP_START is set), the isExternalOpenCode
flag prevents gracefulShutdown() and restartOpenCode() from killing the
user's independent OpenCode process.

- Probe configured port, then default 4096 on startup
- Use /global/health + body.healthy === true for reliable detection
- Skip kill -9 on shutdown/restart when server is externally owned
- Re-probe (instead of kill + respawn) on restart for external servers
This commit is contained in:
Ruslan Kurchenko
2026-02-12 18:06:40 +02:00
committed by GitHub
parent cbd952ca1b
commit 3bf0843ebb
+74 -1
View File
@@ -2687,6 +2687,7 @@ let openCodeApiDetectionTimer = null;
let lastOpenCodeError = null;
let isOpenCodeReady = false;
let openCodeNotReadySince = 0;
let isExternalOpenCode = false;
let exitOnShutdown = true;
let uiAuthController = null;
let cloudflareTunnelController = null;
@@ -2740,6 +2741,36 @@ async function isOpenCodeProcessHealthy() {
}
}
/**
* Probe if an external OpenCode instance is already running on the given port.
* Unlike isOpenCodeProcessHealthy(), this doesn't require openCodeProcess to be set.
* Used to auto-detect and connect to an existing OpenCode instance on startup.
*/
async function probeExternalOpenCode(port) {
if (!port || port <= 0) {
return false;
}
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
method: 'GET',
headers: {
Accept: 'application/json',
...getOpenCodeAuthHeaders(),
},
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) return false;
const body = await response.json().catch(() => null);
return body?.healthy === true;
} catch {
return false;
}
}
const ENV_CONFIGURED_OPENCODE_PORT = (() => {
const raw =
process.env.OPENCODE_PORT ||
@@ -4338,6 +4369,31 @@ async function restartOpenCode() {
openCodeNotReadySince = Date.now();
console.log('Restarting OpenCode process...');
// For external OpenCode servers, re-probe instead of kill + respawn
if (isExternalOpenCode) {
console.log('Re-probing external OpenCode server...');
const probePort = openCodePort || ENV_CONFIGURED_OPENCODE_PORT || 4096;
const healthy = await probeExternalOpenCode(probePort);
if (healthy) {
console.log(`External OpenCode server on port ${probePort} is healthy`);
setOpenCodePort(probePort);
isOpenCodeReady = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else {
lastOpenCodeError = `External OpenCode server on port ${probePort} is not responding`;
console.error(lastOpenCodeError);
throw new Error(lastOpenCodeError);
}
if (expressApp) {
setupProxy(expressApp);
ensureOpenCodeApiPrefix();
}
return;
}
const portToKill = openCodePort;
if (openCodeProcess) {
@@ -5018,7 +5074,7 @@ async function gracefulShutdown(options = {}) {
}
// Only stop OpenCode if we started it ourselves (not when using external server)
if (!ENV_SKIP_OPENCODE_START) {
if (!ENV_SKIP_OPENCODE_START && !isExternalOpenCode) {
const portToKill = openCodePort;
if (openCodeProcess) {
@@ -10592,6 +10648,23 @@ Context:
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(ENV_CONFIGURED_OPENCODE_PORT)) {
console.log(`Auto-detected existing OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT}`);
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (!ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(4096)) {
console.log('Auto-detected existing OpenCode server on default port 4096');
setOpenCodePort(4096);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();