@@ -64,58 +64,22 @@ async def start(
6464 logger .info (f"Starting Claude from directory: { src_dir } " )
6565 logger .info (f"Command: { ' ' .join (cmd )} " )
6666
67- # Claude CLI runs to completion, so we run it and capture all output
67+ # Start process asynchronously
6868 self .process = await asyncio .create_subprocess_exec (
6969 * cmd ,
7070 cwd = src_dir ,
7171 stdout = asyncio .subprocess .PIPE ,
72- stderr = asyncio .subprocess .PIPE
72+ stderr = asyncio .subprocess .PIPE ,
73+ stdin = asyncio .subprocess .PIPE
7374 )
7475
75- # Wait for process to complete and capture all output
76- stdout , stderr = await self .process .communicate ()
76+ self .is_running = True
7777
78- logger .info (
79- "Claude process completed" ,
80- session_id = self .session_id ,
81- return_code = self .process .returncode ,
82- stdout_length = len (stdout ) if stdout else 0 ,
83- stderr_length = len (stderr ) if stderr else 0 ,
84- stderr_preview = stderr .decode ()[:200 ] if stderr else "empty" ,
85- stdout_preview = stdout .decode ()[:200 ] if stdout else "empty"
86- )
78+ # Start background tasks to read output
79+ asyncio .create_task (self ._read_output ())
80+ asyncio .create_task (self ._read_error ())
8781
88- if self .process .returncode == 0 :
89- # Parse the output lines and put them in the queue
90- output_lines = stdout .decode ().strip ().split ('\n ' )
91- claude_session_id = None
92-
93- for line in output_lines :
94- if line .strip ():
95- try :
96- data = json .loads (line )
97- # Extract Claude's session ID from the first message
98- if not claude_session_id and data .get ("session_id" ):
99- claude_session_id = data ["session_id" ]
100- logger .info (f"Extracted Claude session ID: { claude_session_id } " )
101- # Update our session_id to match Claude's
102- self .session_id = claude_session_id
103- await self .output_queue .put (data )
104- except json .JSONDecodeError :
105- # Handle non-JSON output
106- await self .output_queue .put ({"type" : "text" , "content" : line })
107-
108- # Signal end of output
109- await self .output_queue .put (None )
110- self .is_running = False
111- return True
112- else :
113- # Handle error
114- error_text = stderr .decode ().strip ()
115- logger .error (f"Claude process failed with exit code { self .process .returncode } : { error_text } " )
116- await self .error_queue .put (error_text )
117- await self .error_queue .put (None )
118- return False
82+ return True
11983
12084 except Exception as e :
12185 logger .error (
@@ -124,6 +88,66 @@ async def start(
12488 error = str (e )
12589 )
12690 return False
91+
92+ async def _read_output (self ):
93+ """Read stdout from process line by line."""
94+ claude_session_id = None
95+
96+ try :
97+ while self .is_running and self .process :
98+ line = await self .process .stdout .readline ()
99+ if not line :
100+ break
101+
102+ line_text = line .decode ().strip ()
103+ if not line_text :
104+ continue
105+
106+ try :
107+ data = json .loads (line_text )
108+ # Extract Claude's session ID from the first message
109+ if not claude_session_id and data .get ("session_id" ):
110+ claude_session_id = data ["session_id" ]
111+ logger .info (f"Extracted Claude session ID: { claude_session_id } " )
112+ # Update our session_id to match Claude's
113+ self .session_id = claude_session_id
114+ await self .output_queue .put (data )
115+ except json .JSONDecodeError :
116+ # Handle non-JSON output
117+ await self .output_queue .put ({"type" : "text" , "content" : line_text })
118+ except Exception as e :
119+ logger .error ("Error reading output" , error = str (e ))
120+ finally :
121+ await self .output_queue .put (None )
122+ self .is_running = False
123+
124+ # Wait for process to exit
125+ if self .process :
126+ try :
127+ # Don't wait forever, just check if it's done or wait a bit
128+ # But actually we should let it run until it's done or stopped
129+ pass
130+ except Exception :
131+ pass
132+
133+ logger .info (
134+ "Claude process output stream ended" ,
135+ session_id = self .session_id
136+ )
137+
138+ async def _read_error (self ):
139+ """Read stderr from process."""
140+ try :
141+ while self .is_running and self .process :
142+ line = await self .process .stderr .readline ()
143+ if not line :
144+ break
145+
146+ error_text = line .decode ().strip ()
147+ if error_text :
148+ logger .warning ("Claude stderr" , message = error_text )
149+ except Exception as e :
150+ logger .error ("Error reading stderr" , error = str (e ))
127151
128152
129153 async def get_output (self ) -> AsyncGenerator [Dict [str , Any ], None ]:
0 commit comments