1- import type { Process } from '@repo/shared' ;
1+ import type { Logger , Process } from '@repo/shared' ;
22import type { Sandbox } from '../sandbox' ;
33import { createSandboxFetch } from './fetch' ;
44import type {
@@ -34,14 +34,15 @@ async function ensureSdkLoaded(): Promise<void> {
3434 * Returns the process if found and still active, null otherwise.
3535 */
3636async function findExistingOpencodeProcess (
37- sandbox : Sandbox < any > ,
37+ sandbox : Sandbox < unknown > ,
3838 port : number
3939) : Promise < Process | null > {
4040 const processes = await sandbox . listProcesses ( ) ;
41- const command = `opencode serve --port ${ port } ` ;
41+ // Use exact command match to avoid matching unrelated processes
42+ const expectedCommand = `opencode serve --port ${ port } --hostname 0.0.0.0` ;
4243
4344 for ( const proc of processes ) {
44- if ( proc . command . includes ( command ) ) {
45+ if ( proc . command === expectedCommand ) {
4546 if ( proc . status === 'starting' || proc . status === 'running' ) {
4647 return proc ;
4748 }
@@ -54,37 +55,84 @@ async function findExistingOpencodeProcess(
5455/**
5556 * Ensures OpenCode server is running in the container.
5657 * Reuses existing process if one is already running on the specified port.
58+ * Handles concurrent startup attempts gracefully by retrying on failure.
5759 * Returns the process handle.
5860 */
5961async function ensureOpencodeServer (
60- sandbox : Sandbox < any > ,
62+ sandbox : Sandbox < unknown > ,
6163 port : number ,
62- config ?: Record < string , unknown >
64+ config ?: Record < string , unknown > ,
65+ logger ?: Logger
6366) : Promise < Process > {
6467 // Check if OpenCode is already running on this port
65- let process = await findExistingOpencodeProcess ( sandbox , port ) ;
66-
67- if ( process ) {
68+ const existingProcess = await findExistingOpencodeProcess ( sandbox , port ) ;
69+ if ( existingProcess ) {
70+ logger ?. debug ( 'Reusing existing OpenCode process' , {
71+ port,
72+ processId : existingProcess . id
73+ } ) ;
6874 // Reuse existing process - wait for it to be ready if still starting
69- if ( process . status === 'starting' ) {
75+ if ( existingProcess . status === 'starting' ) {
7076 try {
71- await process . waitForPort ( port , {
77+ await existingProcess . waitForPort ( port , {
7278 mode : 'http' ,
7379 path : '/' ,
7480 timeout : 60_000
7581 } ) ;
7682 } catch ( e ) {
77- const logs = await process . getLogs ( ) ;
83+ const logs = await existingProcess . getLogs ( ) ;
7884 throw new OpencodeStartupError (
7985 `OpenCode server failed to start. Stderr: ${ logs . stderr || '(empty)' } ` ,
8086 { cause : e }
8187 ) ;
8288 }
8389 }
84- return process ;
90+ return existingProcess ;
91+ }
92+
93+ // Try to start a new OpenCode server
94+ try {
95+ return await startOpencodeServer ( sandbox , port , config , logger ) ;
96+ } catch ( startupError ) {
97+ // Startup failed - check if another concurrent request started the server
98+ // This handles the race condition where multiple requests try to start simultaneously
99+ logger ?. debug ( 'Startup failed, checking for concurrent server start' , {
100+ port
101+ } ) ;
102+
103+ const retryProcess = await findExistingOpencodeProcess ( sandbox , port ) ;
104+ if ( retryProcess ) {
105+ logger ?. debug ( 'Found server started by concurrent request' , {
106+ port,
107+ processId : retryProcess . id
108+ } ) ;
109+ // Wait for the concurrent server to be ready
110+ if ( retryProcess . status === 'starting' ) {
111+ await retryProcess . waitForPort ( port , {
112+ mode : 'http' ,
113+ path : '/' ,
114+ timeout : 60_000
115+ } ) ;
116+ }
117+ return retryProcess ;
118+ }
119+
120+ // No concurrent server found - the failure was genuine
121+ throw startupError ;
85122 }
123+ }
124+
125+ /**
126+ * Internal function to start a new OpenCode server process.
127+ */
128+ async function startOpencodeServer (
129+ sandbox : Sandbox < unknown > ,
130+ port : number ,
131+ config ?: Record < string , unknown > ,
132+ logger ?: Logger
133+ ) : Promise < Process > {
134+ logger ?. info ( 'Starting OpenCode server' , { port } ) ;
86135
87- // Start new OpenCode server
88136 // Pass config via OPENCODE_CONFIG_CONTENT and also extract API keys to env vars
89137 // because OpenCode's provider auth looks for env vars like ANTHROPIC_API_KEY
90138 const env : Record < string , string > = { } ;
@@ -107,7 +155,7 @@ async function ensureOpencodeServer(
107155 }
108156 }
109157
110- process = await sandbox . startProcess (
158+ const process = await sandbox . startProcess (
111159 `opencode serve --port ${ port } --hostname 0.0.0.0` ,
112160 { env : Object . keys ( env ) . length > 0 ? env : undefined }
113161 ) ;
@@ -119,8 +167,13 @@ async function ensureOpencodeServer(
119167 path : '/' ,
120168 timeout : 60_000
121169 } ) ;
170+ logger ?. debug ( 'OpenCode server started' , { operation : 'opencode.start' } ) ;
122171 } catch ( e ) {
123172 const logs = await process . getLogs ( ) ;
173+ const error = e instanceof Error ? e : undefined ;
174+ logger ?. error ( 'OpenCode server failed to start' , error , {
175+ operation : 'opencode.start'
176+ } ) ;
124177 throw new OpencodeStartupError (
125178 `OpenCode server failed to start. Stderr: ${ logs . stderr || '(empty)' } ` ,
126179 { cause : e }
@@ -158,13 +211,18 @@ async function ensureOpencodeServer(
158211 * ```
159212 */
160213export async function createOpencode < TClient = unknown > (
161- sandbox : Sandbox < any > ,
214+ sandbox : Sandbox < unknown > ,
162215 options ?: OpencodeOptions
163216) : Promise < OpencodeResult < TClient > > {
164217 await ensureSdkLoaded ( ) ;
165218
166219 const port = options ?. port ?? DEFAULT_PORT ;
167- const process = await ensureOpencodeServer ( sandbox , port , options ?. config ) ;
220+ const process = await ensureOpencodeServer (
221+ sandbox ,
222+ port ,
223+ options ?. config ,
224+ options ?. logger
225+ ) ;
168226
169227 // Create SDK client with Sandbox transport
170228 // Cast from unknown - SDK is optional peer dependency loaded dynamically
@@ -220,7 +278,7 @@ export async function createOpencode<TClient = unknown>(
220278 */
221279export async function proxyToOpencode (
222280 request : Request ,
223- sandbox : Sandbox < any > ,
281+ sandbox : Sandbox < unknown > ,
224282 options ?: ProxyToOpencodeOptions
225283) : Promise < Response > {
226284 const url = new URL ( request . url ) ;
@@ -233,17 +291,32 @@ export async function proxyToOpencode(
233291 request . method === 'GET' &&
234292 request . headers . get ( 'Accept' ) ?. includes ( 'text/html' ) ;
235293
236- if (
237- url . hostname === 'localhost' &&
238- ! url . searchParams . has ( 'url' ) &&
239- isNavigation
240- ) {
294+ // Also handle 127.0.0.1 which OpenCode may use
295+ const isLocalhost =
296+ url . hostname === 'localhost' || url . hostname === '127.0.0.1' ;
297+
298+ if ( isLocalhost && ! url . searchParams . has ( 'url' ) && isNavigation ) {
241299 url . searchParams . set ( 'url' , url . origin ) ;
242300 return Response . redirect ( url . toString ( ) , 302 ) ;
243301 }
244302
245303 // Ensure OpenCode server is running
246- await ensureOpencodeServer ( sandbox , port , options ?. config ) ;
304+ try {
305+ await ensureOpencodeServer ( sandbox , port , options ?. config , options ?. logger ) ;
306+ } catch ( err ) {
307+ const error = err instanceof Error ? err : undefined ;
308+ options ?. logger ?. error ( 'Failed to start OpenCode server' , error , {
309+ operation : 'opencode.proxy'
310+ } ) ;
311+ const message =
312+ err instanceof OpencodeStartupError
313+ ? err . message
314+ : 'Failed to start OpenCode server' ;
315+ return new Response ( JSON . stringify ( { error : message } ) , {
316+ status : 503 ,
317+ headers : { 'Content-Type' : 'application/json' }
318+ } ) ;
319+ }
247320
248321 // Proxy the request to OpenCode
249322 return sandbox . containerFetch ( request , port ) ;
0 commit comments