1- import fs , { mkdirSync , rmSync , symlinkSync } from "node:fs" ;
1+ import fs , { existsSync , mkdirSync , symlinkSync } from "node:fs" ;
22import { tmpdir } from "node:os" ;
3- import { isAbsolute , join , relative , resolve , sep } from "node:path" ;
3+ import { delimiter , isAbsolute , join , relative , resolve , sep } from "node:path" ;
44import {
55 type Client ,
66 ClientSideConnection ,
@@ -46,6 +46,8 @@ export type { InterruptReason };
4646
4747const log = logger . scope ( "agent-service" ) ;
4848
49+ const SHARED_MOCK_NODE_DIR = join ( tmpdir ( ) , "agent-node-shared" ) ;
50+
4951/** Mark all content blocks as hidden so the renderer doesn't show a duplicate user message on retry. */
5052function hidePromptBlocks ( prompt : ContentBlock [ ] ) : ContentBlock [ ] {
5153 return prompt . map ( ( block ) => {
@@ -201,7 +203,6 @@ interface ManagedSession {
201203 channel : string ;
202204 createdAt : number ;
203205 lastActivityAt : number ;
204- mockNodeDir : string ;
205206 config : SessionConfig ;
206207 interruptReason ?: InterruptReason ;
207208 needsRecreation : boolean ;
@@ -246,6 +247,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
246247 private sessions = new Map < string , ManagedSession > ( ) ;
247248 private currentToken : string | null = null ;
248249 private pendingPermissions = new Map < string , PendingPermission > ( ) ;
250+ private mockNodeReady = false ;
249251 private processTracking : ProcessTrackingService ;
250252 private sleepService : SleepService ;
251253 private fsService : FsService ;
@@ -494,7 +496,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
494496 }
495497
496498 const channel = `agent-event:${ taskRunId } ` ;
497- const mockNodeDir = this . setupMockNodeEnvironment ( taskRunId ) ;
499+ const mockNodeDir = this . setupMockNodeEnvironment ( ) ;
498500 this . setupEnvironment ( credentials , mockNodeDir ) ;
499501
500502 // Preview sessions don't persist logs — no real task exists
@@ -658,7 +660,6 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
658660 channel,
659661 createdAt : Date . now ( ) ,
660662 lastActivityAt : Date . now ( ) ,
661- mockNodeDir,
662663 config,
663664 needsRecreation : false ,
664665 promptPending : false ,
@@ -676,7 +677,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
676677 } catch {
677678 log . debug ( "Agent cleanup failed during error handling" , { taskRunId } ) ;
678679 }
679- this . cleanupMockNodeEnvironment ( mockNodeDir ) ;
680+
680681 if ( ! isRetry && isAuthError ( err ) ) {
681682 log . warn (
682683 `Auth error during ${ isReconnect ? "reconnect" : "create" } , retrying` ,
@@ -1003,8 +1004,10 @@ For git operations while detached:
10031004 mockNodeDir : string ,
10041005 ) : void {
10051006 const token = this . getToken ( credentials . apiKey ) ;
1006- const newPath = `${ mockNodeDir } :${ process . env . PATH || "" } ` ;
1007- process . env . PATH = newPath ;
1007+ const currentPath = process . env . PATH || "" ;
1008+ if ( ! currentPath . split ( delimiter ) . includes ( mockNodeDir ) ) {
1009+ process . env . PATH = `${ mockNodeDir } ${ delimiter } ${ currentPath } ` ;
1010+ }
10081011 process . env . POSTHOG_AUTH_HEADER = `Bearer ${ token } ` ;
10091012 process . env . ANTHROPIC_API_KEY = token ;
10101013 process . env . ANTHROPIC_AUTH_TOKEN = token ;
@@ -1026,29 +1029,20 @@ For git operations while detached:
10261029 process . env . POSTHOG_PROJECT_ID = String ( credentials . projectId ) ;
10271030 }
10281031
1029- private setupMockNodeEnvironment ( sessionId : string ) : string {
1030- const mockNodeDir = join ( tmpdir ( ) , `array-agent-node-${ sessionId } ` ) ;
1031- try {
1032- mkdirSync ( mockNodeDir , { recursive : true } ) ;
1033- const nodeSymlinkPath = join ( mockNodeDir , "node" ) ;
1032+ private setupMockNodeEnvironment ( ) : string {
1033+ if ( ! this . mockNodeReady ) {
10341034 try {
1035- rmSync ( nodeSymlinkPath , { force : true } ) ;
1036- } catch {
1037- /* ignore */
1035+ mkdirSync ( SHARED_MOCK_NODE_DIR , { recursive : true } ) ;
1036+ const nodeSymlinkPath = join ( SHARED_MOCK_NODE_DIR , "node" ) ;
1037+ if ( ! existsSync ( nodeSymlinkPath ) ) {
1038+ symlinkSync ( process . execPath , nodeSymlinkPath ) ;
1039+ }
1040+ this . mockNodeReady = true ;
1041+ } catch ( err ) {
1042+ log . warn ( "Failed to setup mock node environment" , err ) ;
10381043 }
1039- symlinkSync ( process . execPath , nodeSymlinkPath ) ;
1040- } catch ( err ) {
1041- log . warn ( "Failed to setup mock node environment" , err ) ;
1042- }
1043- return mockNodeDir ;
1044- }
1045-
1046- private cleanupMockNodeEnvironment ( mockNodeDir : string ) : void {
1047- try {
1048- rmSync ( mockNodeDir , { recursive : true , force : true } ) ;
1049- } catch {
1050- /* ignore */
10511044 }
1045+ return SHARED_MOCK_NODE_DIR ;
10521046 }
10531047
10541048 private async cleanupSession ( taskRunId : string ) : Promise < void > {
@@ -1060,7 +1054,7 @@ For git operations while detached:
10601054 } catch {
10611055 log . debug ( "Agent cleanup failed" , { taskRunId } ) ;
10621056 }
1063- this . cleanupMockNodeEnvironment ( session . mockNodeDir ) ;
1057+
10641058 this . sessions . delete ( taskRunId ) ;
10651059 }
10661060 }
0 commit comments