@@ -154,6 +154,8 @@ export async function killTaggedWindows(jobId: string): Promise<number> {
154154 const [ target , taggedId ] = line . split ( ' ' , 2 ) ;
155155 if ( taggedId === jobId && target ) {
156156 try {
157+ const pids = await getPaneProcesses ( target ) ;
158+ await killPaneProcesses ( pids ) ;
157159 const killProc = spawn ( [ "tmux" , "kill-window" , "-t" , target ] , { stderr : "pipe" } ) ;
158160 await killProc . exited ;
159161 killed ++ ;
@@ -199,14 +201,66 @@ export async function windowExists(
199201}
200202
201203/**
202- * Kill a tmux session
204+ * Get PIDs of all panes within a tmux target (session or window).
205+ * Returns an array of PIDs. Best-effort — returns empty on failure.
206+ */
207+ async function getPaneProcesses ( target : string ) : Promise < number [ ] > {
208+ try {
209+ const proc = spawn (
210+ [ "tmux" , "list-panes" , "-t" , target , "-F" , "#{pane_pid}" ] ,
211+ { stderr : "pipe" } ,
212+ ) ;
213+ const output = await new Response ( proc . stdout ) . text ( ) ;
214+ const exitCode = await proc . exited ;
215+ if ( exitCode !== 0 ) return [ ] ;
216+
217+ return output
218+ . trim ( )
219+ . split ( '\n' )
220+ . map ( ( line ) => parseInt ( line . trim ( ) , 10 ) )
221+ . filter ( ( pid ) => ! isNaN ( pid ) && pid > 0 ) ;
222+ } catch {
223+ return [ ] ;
224+ }
225+ }
226+
227+ /**
228+ * Kill processes running inside tmux panes before destroying them.
229+ * Sends SIGTERM first, waits briefly, then SIGKILL for survivors.
230+ * This prevents orphaned processes (e.g., opencode ignoring SIGHUP from tmux).
231+ */
232+ async function killPaneProcesses ( pids : number [ ] ) : Promise < void > {
233+ if ( pids . length === 0 ) return ;
234+
235+ for ( const pid of pids ) {
236+ try { process . kill ( pid , 'SIGTERM' ) ; } catch { }
237+ }
238+
239+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
240+
241+ for ( const pid of pids ) {
242+ try { process . kill ( pid , 'SIGKILL' ) ; } catch { }
243+ }
244+ }
245+
246+ /**
247+ * Kill a tmux session.
248+ * Kills pane processes first to prevent orphans — opencode ignores SIGHUP
249+ * from tmux, so kill-session alone leaves zombie processes consuming CPU.
203250 */
204251export async function killSession ( name : string ) : Promise < void > {
252+ const pids = await getPaneProcesses ( name ) ;
253+ const killedProcesses = pids . length > 0 ;
254+ await killPaneProcesses ( pids ) ;
255+
205256 try {
206257 const proc = spawn ( [ "tmux" , "kill-session" , "-t" , name ] , { stderr : "pipe" } ) ;
207258 const exitCode = await proc . exited ;
208259 if ( exitCode !== 0 ) {
209- throw new Error ( `tmux kill-session failed with exit code ${ exitCode } ` ) ;
260+ // Tolerate failure only if we killed pane processes (session may have auto-closed)
261+ if ( ! killedProcesses ) {
262+ throw new Error ( `tmux kill-session failed with exit code ${ exitCode } ` ) ;
263+ }
210264 }
211265 } catch ( error ) {
212266 throw new Error (
@@ -216,18 +270,25 @@ export async function killSession(name: string): Promise<void> {
216270}
217271
218272/**
219- * Kill a window in a tmux session
273+ * Kill a window in a tmux session.
274+ * First kills pane processes to prevent orphans (see killSession).
220275 */
221276export async function killWindow (
222277 session : string ,
223278 window : string
224279) : Promise < void > {
280+ const target = `${ session } :${ window } ` ;
281+ const pids = await getPaneProcesses ( target ) ;
282+ const killedProcesses = pids . length > 0 ;
283+ await killPaneProcesses ( pids ) ;
284+
225285 try {
226- const target = `${ session } :${ window } ` ;
227286 const proc = spawn ( [ "tmux" , "kill-window" , "-t" , target ] , { stderr : "pipe" } ) ;
228287 const exitCode = await proc . exited ;
229288 if ( exitCode !== 0 ) {
230- throw new Error ( `tmux kill-window failed with exit code ${ exitCode } ` ) ;
289+ if ( ! killedProcesses ) {
290+ throw new Error ( `tmux kill-window failed with exit code ${ exitCode } ` ) ;
291+ }
231292 }
232293 } catch ( error ) {
233294 throw new Error (
0 commit comments