66import { accessSync , constants as fsConstants } from 'node:fs' ;
77import { dirname , join } from 'node:path' ;
88
9- import { Codex , type McpServerConfig } from '@browseros/codex-sdk-ts' ;
9+ import { Codex , Thread , type McpServerConfig } from '@browseros/codex-sdk-ts' ;
1010import { logger } from '@browseros/common' ;
1111import type { ControllerBridge } from '@browseros/controller-server' ;
1212import { allControllerTools } from '@browseros/tools/controller-based' ;
@@ -66,6 +66,7 @@ export class CodexSDKAgent extends BaseAgent {
6666 private codex : Codex | null = null ;
6767 private codexExecutablePath : string | null = null ;
6868 private codexConfigPath : string | null = null ;
69+ private currentThread : Thread | null = null ;
6970
7071 constructor ( config : AgentConfig , _controllerBridge : ControllerBridge ) {
7172 const mcpServerConfig = buildMcpServerConfig ( config ) ;
@@ -338,7 +339,17 @@ export class CodexSDKAgent extends BaseAgent {
338339 } ) ;
339340 }
340341
341- const thread = this . codex . startThread ( threadOptions ) ;
342+ // Reuse existing thread for follow-up messages, or create new one
343+ // CRITICAL: Check both existence AND thread ID (ID is null if cancelled before thread.started event)
344+ if ( ! this . currentThread || ! this . currentThread . id ) {
345+ this . currentThread = this . codex . startThread ( threadOptions ) ;
346+ logger . info ( '🆕 Created new thread for session' ) ;
347+ } else {
348+ logger . info ( '♻️ Reusing existing thread for follow-up message' , {
349+ threadId : this . currentThread . id ,
350+ } ) ;
351+ }
352+ const thread = this . currentThread ;
342353
343354 // Get streaming events from thread
344355 const messages : Array < { type : 'text' ; text : string } > = [ ] ;
@@ -368,6 +379,9 @@ export class CodexSDKAgent extends BaseAgent {
368379 logger . info (
369380 '⚠️ Agent execution aborted by client (breaking loop)' ,
370381 ) ;
382+ // Clear thread - next message will create fresh thread
383+ this . currentThread = null ;
384+ logger . debug ( '🔄 Cleared thread reference due to abort' ) ;
371385 break ;
372386 }
373387
@@ -454,9 +468,14 @@ export class CodexSDKAgent extends BaseAgent {
454468 }
455469 } finally {
456470 // CRITICAL: Close iterator to trigger SIGKILL in forked SDK's finally block
471+ // Fire-and-forget to avoid blocking markIdle() - subprocess cleanup can happen async
457472 if ( iterator . return ) {
458473 logger . debug ( '🔒 Closing iterator to terminate Codex subprocess' ) ;
459- await iterator . return ( undefined ) ;
474+ iterator . return ( undefined ) . catch ( ( error ) => {
475+ logger . warn ( '⚠️ Iterator cleanup error (non-fatal)' , {
476+ error : error instanceof Error ? error . message : String ( error ) ,
477+ } ) ;
478+ } ) ;
460479 }
461480 }
462481
@@ -469,6 +488,10 @@ export class CodexSDKAgent extends BaseAgent {
469488 duration : Date . now ( ) - this . executionStartTime ,
470489 } ) ;
471490 } catch ( error ) {
491+ // Clear thread on error - next call will create fresh thread
492+ this . currentThread = null ;
493+ logger . debug ( '🔄 Cleared thread reference due to error' ) ;
494+
472495 // Mark execution error
473496 this . errorExecution (
474497 error instanceof Error ? error : new Error ( String ( error ) ) ,
@@ -486,6 +509,24 @@ export class CodexSDKAgent extends BaseAgent {
486509 }
487510 }
488511
512+ /**
513+ * Abort current execution
514+ * Triggers abort signal to stop the current task gracefully
515+ */
516+ abort ( ) : void {
517+ if ( this . abortController ) {
518+ logger . info ( '🛑 Aborting CodexSDKAgent execution' ) ;
519+ this . abortController . abort ( ) ;
520+ }
521+ }
522+
523+ /**
524+ * Check if agent is currently executing
525+ */
526+ isExecuting ( ) : boolean {
527+ return this . metadata . state === 'executing' && this . abortController !== null ;
528+ }
529+
489530 /**
490531 * Cleanup agent resources
491532 *
@@ -500,6 +541,9 @@ export class CodexSDKAgent extends BaseAgent {
500541
501542 this . markDestroyed ( ) ;
502543
544+ // Clear thread reference
545+ this . currentThread = null ;
546+
503547 // Trigger abort controller for cleanup
504548 if ( this . abortController ) {
505549 this . abortController . abort ( ) ;
0 commit comments