@@ -30,6 +30,7 @@ const (
3030// ServerManager manages the background web server
3131type ServerManager struct {
3232 mu sync.RWMutex
33+ captureMu sync.Mutex // serializes handlers that hijack process stdio and REPL config
3334 status ServerStatus
3435 server * http.Server
3536 config * llm.Config
@@ -263,6 +264,13 @@ func (sm *ServerManager) executeInputWithCapture(input string, stream bool, syst
263264 return "" , fmt .Errorf ("REPL not available" )
264265 }
265266
267+ // This handler mutates process-global stdio and REPL config to thread
268+ // per-request overrides into sendToAI. Serialize against any other
269+ // capturing handler so concurrent requests cannot steal each other's
270+ // output or restore the wrong config values.
271+ sm .captureMu .Lock ()
272+ defer sm .captureMu .Unlock ()
273+
266274 // Optionally override streaming and system prompt for this call
267275 oldStream := sm .repl .configOptions .Get ("llm.stream" )
268276 oldSystem := sm .repl .configOptions .Get ("llm.systemprompt" )
@@ -386,6 +394,11 @@ func (sm *ServerManager) GetStatusString() string {
386394
387395// executeCommandWithCapture executes a REPL command and captures its output
388396func (sm * ServerManager ) executeCommandWithCapture (command string ) (string , error ) {
397+ // Shares process-global stdio with executeInputWithCapture, so it must
398+ // take the same lock to avoid two concurrent requests racing on os.Stdout.
399+ sm .captureMu .Lock ()
400+ defer sm .captureMu .Unlock ()
401+
389402 // Create pipes to capture both stdout and stderr
390403 oldStdout := os .Stdout
391404 oldStderr := os .Stderr
0 commit comments