@@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
66use std:: sync:: Arc ;
77use std:: time:: Duration ;
88
9- use chrono:: { DateTime , Utc } ;
109use futures_util:: StreamExt ;
1110use reqwest_eventsource:: { Event , EventSource } ;
1211use tokio:: process:: { Child , Command } ;
@@ -54,7 +53,6 @@ const REST_PORT: u16 = 14096;
5453struct ServerProcess {
5554 child : Child ,
5655 base_url : String ,
57- started_at : DateTime < Utc > ,
5856}
5957
6058fn rest_base_url ( ) -> String {
@@ -80,7 +78,7 @@ fn pid_file_path() -> Option<PathBuf> {
8078}
8179
8280/// Write PID file after starting the server.
83- async fn write_pid_file ( pid : u32 , port : u16 , started_at : DateTime < Utc > ) -> Result < ( ) , String > {
81+ async fn write_pid_file ( pid : u32 , port : u16 ) -> Result < ( ) , String > {
8482 let path = pid_file_path ( ) . ok_or ( "Could not determine PID file path" ) ?;
8583 if let Some ( parent) = path. parent ( ) {
8684 tokio:: fs:: create_dir_all ( parent)
@@ -90,7 +88,7 @@ async fn write_pid_file(pid: u32, port: u16, started_at: DateTime<Utc>) -> Resul
9088 let data = PidFileData {
9189 pid,
9290 port,
93- started_at : started_at . to_rfc3339 ( ) ,
91+ started_at : chrono :: Utc :: now ( ) . to_rfc3339 ( ) ,
9492 } ;
9593 let json = serde_json:: to_string_pretty ( & data) . map_err ( |e| e. to_string ( ) ) ?;
9694 tokio:: fs:: write ( & path, json)
@@ -119,105 +117,31 @@ fn is_process_running(pid: u32) -> bool {
119117}
120118
121119/// Try to reclaim an orphaned server (one we previously started but lost track of).
122- /// Returns PID metadata when we successfully reclaimed, None otherwise.
123- async fn try_reclaim_orphaned_server ( ) -> Option < PidFileData > {
120+ /// Returns true if we successfully reclaimed, false otherwise.
121+ async fn try_reclaim_orphaned_server ( ) -> bool {
124122 let pid_data = match read_pid_file ( ) . await {
125123 Some ( data) => data,
126- None => return None ,
124+ None => return false ,
127125 } ;
128126
129127 // Check if the process is still running
130128 if !is_process_running ( pid_data. pid ) {
131129 // Stale PID file, clean it up
132130 delete_pid_file ( ) . await ;
133- return None ;
131+ return false ;
134132 }
135133
136134 // Process is running - check if it's actually our server on the expected port
137135 let base_url = rest_base_url ( ) ;
138136 if health_check ( & base_url) . await . is_err ( ) {
139137 // Process exists but isn't responding as our server - stale PID file
140138 delete_pid_file ( ) . await ;
141- return None ;
139+ return false ;
142140 }
143141
144142 // Server is alive and healthy - this is an orphaned server we can reclaim
145143 // We can't actually adopt the Child handle, but we can track that we own it via PID
146- Some ( pid_data)
147- }
148-
149- fn parse_rfc3339_timestamp ( value : & str ) -> Option < DateTime < Utc > > {
150- DateTime :: parse_from_rfc3339 ( value)
151- . ok ( )
152- . map ( |ts| ts. with_timezone ( & Utc ) )
153- }
154-
155- async fn server_config_path ( base_url : & str ) -> Option < PathBuf > {
156- let client = reqwest:: Client :: builder ( )
157- . timeout ( Duration :: from_secs ( 3 ) )
158- . build ( )
159- . ok ( ) ?;
160- let response = client
161- . get ( format ! ( "{base_url}/path" ) )
162- . send ( )
163- . await
164- . ok ( ) ?;
165- if !response. status ( ) . is_success ( ) {
166- return None ;
167- }
168- let payload = response. json :: < Value > ( ) . await . ok ( ) ?;
169- let config = payload. get ( "config" ) ?. as_str ( ) ?. trim ( ) ;
170- if config. is_empty ( ) {
171- return None ;
172- }
173- Some ( PathBuf :: from ( config) )
174- }
175-
176- async fn latest_directory_change ( root : PathBuf ) -> Option < DateTime < Utc > > {
177- let mut latest = None ;
178- let mut stack = vec ! [ root] ;
179-
180- while let Some ( dir) = stack. pop ( ) {
181- let mut entries = match tokio:: fs:: read_dir ( & dir) . await {
182- Ok ( entries) => entries,
183- Err ( _) => continue ,
184- } ;
185-
186- while let Ok ( Some ( entry) ) = entries. next_entry ( ) . await {
187- let metadata = match entry. metadata ( ) . await {
188- Ok ( metadata) => metadata,
189- Err ( _) => continue ,
190- } ;
191-
192- if let Ok ( modified) = metadata. modified ( ) {
193- let modified_at = DateTime :: < Utc > :: from ( modified) ;
194- latest = Some ( match latest {
195- Some ( current) if current >= modified_at => current,
196- _ => modified_at,
197- } ) ;
198- }
199-
200- if metadata. is_dir ( ) {
201- stack. push ( entry. path ( ) ) ;
202- }
203- }
204- }
205-
206- latest
207- }
208-
209- async fn should_restart_for_config_change ( server_started_at : DateTime < Utc > , base_url : & str ) -> bool {
210- let config_root = server_config_path ( base_url)
211- . await
212- . or_else ( crate :: codex:: home:: resolve_default_codex_home) ;
213- let Some ( config_root) = config_root else {
214- return false ;
215- } ;
216-
217- latest_directory_change ( config_root)
218- . await
219- . map ( |latest_change| latest_change > server_started_at)
220- . unwrap_or ( false )
144+ true
221145}
222146
223147/// Kill process listening on the REST port (for takeover functionality).
@@ -291,7 +215,6 @@ async fn start_managed_server_process(
291215 codex_args : Option < & str > ,
292216) -> Result < ServerProcess , String > {
293217 let base_url = rest_base_url ( ) ;
294- let started_at = Utc :: now ( ) ;
295218 let mut command = build_codex_command_with_bin (
296219 codex_bin,
297220 codex_args,
@@ -316,7 +239,7 @@ async fn start_managed_server_process(
316239
317240 // Write PID file for ownership tracking
318241 if let Some ( pid) = child. id ( ) {
319- if let Err ( e) = write_pid_file ( pid, REST_PORT , started_at . clone ( ) ) . await {
242+ if let Err ( e) = write_pid_file ( pid, REST_PORT ) . await {
320243 eprintln ! ( "Warning: failed to write PID file: {e}" ) ;
321244 }
322245 }
@@ -333,11 +256,7 @@ async fn start_managed_server_process(
333256 sleep ( Duration :: from_millis ( 200 ) ) . await ;
334257 }
335258
336- Ok ( ServerProcess {
337- child,
338- base_url,
339- started_at,
340- } )
259+ Ok ( ServerProcess { child, base_url } )
341260}
342261
343262async fn ensure_server_running (
@@ -346,29 +265,14 @@ async fn ensure_server_running(
346265) -> Result < String , String > {
347266 let base_url = rest_base_url ( ) ;
348267
349- // Fast path: if already initialized and config has not changed, return the URL.
350- if let Some ( server_mutex) = SERVER_PROCESS . get ( ) {
351- let ( started_at, server_base_url) = {
352- let guard = server_mutex. lock ( ) . await ;
353- ( guard. started_at , guard. base_url . clone ( ) )
354- } ;
355- if should_restart_for_config_change ( started_at, & server_base_url) . await {
356- restart_opencode_server ( codex_bin. clone ( ) , codex_args) . await ?;
357- }
268+ // Fast path: if already initialized, just return the URL.
269+ if SERVER_PROCESS . get ( ) . is_some ( ) {
358270 return Ok ( base_url) ;
359271 }
360272
361273 // Check if we have an orphaned server we can reclaim (via PID file).
362274 // This happens when the app crashed/exited but the server kept running.
363- if let Some ( pid_data) = try_reclaim_orphaned_server ( ) . await {
364- if let Some ( started_at) = parse_rfc3339_timestamp ( & pid_data. started_at ) {
365- if should_restart_for_config_change ( started_at, & base_url) . await {
366- restart_opencode_server ( codex_bin, codex_args) . await ?;
367- }
368- } else {
369- // Legacy/invalid PID timestamp - safest option is to restart.
370- restart_opencode_server ( codex_bin, codex_args) . await ?;
371- }
275+ if try_reclaim_orphaned_server ( ) . await {
372276 return Ok ( base_url) ;
373277 }
374278
@@ -584,10 +488,6 @@ pub(crate) async fn takeover_external_server(
584488
585489pub ( crate ) struct WorkspaceSession {
586490 pub ( crate ) entry : WorkspaceEntry ,
587- /// Resolved OpenCode binary used to (re)start the managed server when needed.
588- pub ( crate ) codex_bin : Option < String > ,
589- /// Resolved OpenCode args used to (re)start the managed server when needed.
590- pub ( crate ) codex_args : Option < String > ,
591491 /// HTTP client for REST calls to the OpenCode server.
592492 pub ( crate ) http_client : reqwest:: Client ,
593493 /// Base URL of the OpenCode server (e.g. "http://127.0.0.1:14096").
@@ -630,7 +530,6 @@ async fn route_translated_event_to_background_callback(
630530impl WorkspaceSession {
631531 /// Send a GET request to the OpenCode REST API, scoped to this workspace.
632532 pub ( crate ) async fn rest_get ( & self , path : & str ) -> Result < Value , String > {
633- ensure_server_running ( self . codex_bin . clone ( ) , self . codex_args . as_deref ( ) ) . await ?;
634533 let separator = if path. contains ( '?' ) { "&" } else { "?" } ;
635534 let url = format ! (
636535 "{}{path}{separator}directory={}" ,
@@ -653,7 +552,6 @@ impl WorkspaceSession {
653552
654553 /// Send a POST request to the OpenCode REST API, scoped to this workspace.
655554 pub ( crate ) async fn rest_post ( & self , path : & str , body : Value ) -> Result < Value , String > {
656- ensure_server_running ( self . codex_bin . clone ( ) , self . codex_args . as_deref ( ) ) . await ?;
657555 let separator = if path. contains ( '?' ) { "&" } else { "?" } ;
658556 let url = format ! (
659557 "{}{path}{separator}directory={}" ,
@@ -686,7 +584,6 @@ impl WorkspaceSession {
686584
687585 /// Send a POST request that returns a boolean (e.g. abort, permissions).
688586 pub ( crate ) async fn rest_post_bool ( & self , path : & str , body : Value ) -> Result < bool , String > {
689- ensure_server_running ( self . codex_bin . clone ( ) , self . codex_args . as_deref ( ) ) . await ?;
690587 let separator = if path. contains ( '?' ) { "&" } else { "?" } ;
691588 let url = format ! (
692589 "{}{path}{separator}directory={}" ,
@@ -712,7 +609,6 @@ impl WorkspaceSession {
712609
713610 /// Send a PATCH request to the OpenCode REST API, scoped to this workspace.
714611 pub ( crate ) async fn rest_patch ( & self , path : & str , body : Value ) -> Result < Value , String > {
715- ensure_server_running ( self . codex_bin . clone ( ) , self . codex_args . as_deref ( ) ) . await ?;
716612 let separator = if path. contains ( '?' ) { "&" } else { "?" } ;
717613 let url = format ! (
718614 "{}{path}{separator}directory={}" ,
@@ -1087,12 +983,10 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(
1087983 . clone ( )
1088984 . filter ( |value| !value. trim ( ) . is_empty ( ) )
1089985 . or ( default_codex_bin) ;
1090- let resolved_codex_args = codex_args;
1091986 let _ = check_codex_installation ( codex_bin. clone ( ) ) . await ?;
1092987
1093988 // Ensure the shared `opencode serve` process is running.
1094- let base_url =
1095- ensure_server_running ( codex_bin. clone ( ) , resolved_codex_args. as_deref ( ) ) . await ?;
989+ let base_url = ensure_server_running ( codex_bin, codex_args. as_deref ( ) ) . await ?;
1096990
1097991 let http_client = reqwest:: Client :: builder ( )
1098992 . timeout ( Duration :: from_secs ( 300 ) )
@@ -1103,8 +997,6 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(
1103997
1104998 let session = Arc :: new ( WorkspaceSession {
1105999 entry : entry. clone ( ) ,
1106- codex_bin,
1107- codex_args : resolved_codex_args,
11081000 http_client,
11091001 base_url,
11101002 background_thread_callbacks : Mutex :: new ( HashMap :: new ( ) ) ,
0 commit comments