22// Manages task lifecycle independently of the SolidJS renderer,
33// using existing backend primitives (pty, git, tasks).
44
5- import { randomUUID } from 'crypto' ;
5+ import { randomUUID , randomBytes } from 'crypto' ;
66import { execFile } from 'child_process' ;
77import { promisify } from 'util' ;
88import { unlinkSync , readFileSync , existsSync } from 'fs' ;
@@ -223,13 +223,18 @@ export class Coordinator {
223223 for ( const task of this . tasks . values ( ) ) {
224224 if ( task . coordinatorTaskId !== coordinatorTaskId ) continue ;
225225 if ( ! task . mcpConfigPath ) continue ;
226+ // Preserve existing doneToken; generate a fresh one if not yet set (e.g. older persisted task).
227+ if ( ! task . doneToken ) task . doneToken = randomBytes ( 24 ) . toString ( 'base64url' ) ;
226228 const mcpConfig = {
227229 mcpServers : {
228230 'parallel-code' : {
229231 type : 'stdio' as const ,
230232 command : 'node' ,
231233 args : [ serverPath , '--url' , serverUrl , '--task-id' , task . id ] ,
232- env : { PARALLEL_CODE_MCP_TOKEN : subtaskToken } ,
234+ env : {
235+ PARALLEL_CODE_MCP_TOKEN : subtaskToken ,
236+ PARALLEL_CODE_MCP_DONE_TOKEN : task . doneToken ,
237+ } ,
233238 } ,
234239 } ,
235240 } ;
@@ -583,13 +588,18 @@ export class Coordinator {
583588 const mcpServerInfoForTask = coordinatorState . mcpServerInfo ;
584589 if ( mcpServerInfoForTask ) {
585590 const { serverUrl, subtaskToken, serverPath } = mcpServerInfoForTask ;
591+ const doneToken = randomBytes ( 24 ) . toString ( 'base64url' ) ;
592+ task . doneToken = doneToken ;
586593 const mcpConfig = {
587594 mcpServers : {
588595 'parallel-code' : {
589596 type : 'stdio' as const ,
590597 command : 'node' ,
591598 args : [ serverPath , '--url' , serverUrl , '--task-id' , task . id ] ,
592- env : { PARALLEL_CODE_MCP_TOKEN : subtaskToken } ,
599+ env : {
600+ PARALLEL_CODE_MCP_TOKEN : subtaskToken ,
601+ PARALLEL_CODE_MCP_DONE_TOKEN : doneToken ,
602+ } ,
593603 } ,
594604 } ,
595605 } ;
@@ -735,6 +745,10 @@ export class Coordinator {
735745 } ;
736746 }
737747
748+ getTaskDoneToken ( taskId : string ) : string | null {
749+ return this . tasks . get ( taskId ) ?. doneToken ?? null ;
750+ }
751+
738752 async sendPrompt ( taskId : string , prompt : string ) : Promise < void > {
739753 const task = this . tasks . get ( taskId ) ;
740754 if ( ! task ) throw new Error ( `Task not found: ${ taskId } ` ) ;
@@ -1085,6 +1099,9 @@ export class Coordinator {
10851099 mcpConfigPath ?: string ;
10861100 preambleFileExistedBefore ?: boolean ;
10871101 } ) : void {
1102+ if ( ! this . coordinators . has ( opts . coordinatorTaskId ) ) {
1103+ throw new Error ( `coordinator ${ opts . coordinatorTaskId } is not registered` ) ;
1104+ }
10881105 if ( this . tasks . has ( opts . id ) ) return ;
10891106 const task : CoordinatedTask = {
10901107 id : opts . id ,
@@ -1130,13 +1147,17 @@ export class Coordinator {
11301147 // agent gets fresh credentials instead of the stale pre-restart values.
11311148 if ( safeMcpConfigPath && serverInfo ) {
11321149 const { serverUrl, subtaskToken, serverPath } = serverInfo ;
1150+ if ( ! task . doneToken ) task . doneToken = randomBytes ( 24 ) . toString ( 'base64url' ) ;
11331151 const mcpConfig = {
11341152 mcpServers : {
11351153 'parallel-code' : {
11361154 type : 'stdio' as const ,
11371155 command : 'node' ,
11381156 args : [ serverPath , '--url' , serverUrl , '--task-id' , task . id ] ,
1139- env : { PARALLEL_CODE_MCP_TOKEN : subtaskToken } ,
1157+ env : {
1158+ PARALLEL_CODE_MCP_TOKEN : subtaskToken ,
1159+ PARALLEL_CODE_MCP_DONE_TOKEN : task . doneToken ,
1160+ } ,
11401161 } ,
11411162 } ,
11421163 } ;
@@ -1222,11 +1243,17 @@ export class Coordinator {
12221243 } ) ;
12231244 }
12241245
1225- setMcpJsonInfo ( coordinatorTaskId : string , mcpJsonPath : string , createdMcpJson : boolean ) : void {
1246+ setMcpJsonInfo (
1247+ coordinatorTaskId : string ,
1248+ mcpJsonPath : string ,
1249+ createdMcpJson : boolean ,
1250+ previousMcpParallelCode ?: unknown ,
1251+ ) : void {
12261252 const state = this . coordinators . get ( coordinatorTaskId ) ;
12271253 if ( state ) {
12281254 state . mcpJsonPath = mcpJsonPath ;
12291255 state . createdMcpJson = createdMcpJson ;
1256+ state . previousMcpParallelCode = previousMcpParallelCode ;
12301257 }
12311258 }
12321259
@@ -1246,17 +1273,23 @@ export class Coordinator {
12461273 } ) ;
12471274 }
12481275
1249- // Clean up coordinator .mcp.json — remove only the parallel-code key.
1250- // Always read current contents (user may have added keys while running),
1251- // then delete the whole file only if nothing else remains .
1276+ // Clean up coordinator .mcp.json — restore or remove only the parallel-code key.
1277+ // Always read current contents (user may have added keys while running).
1278+ // If there was a pre-existing parallel-code entry, restore it; otherwise delete the key .
12521279 if ( coordinator . mcpJsonPath ) {
12531280 try {
12541281 const raw = existsSync ( coordinator . mcpJsonPath )
12551282 ? readFileSync ( coordinator . mcpJsonPath , 'utf-8' )
12561283 : null ;
12571284 if ( raw !== null ) {
12581285 const content = JSON . parse ( raw ) as { mcpServers ?: Record < string , unknown > } ;
1259- if ( content . mcpServers ) delete content . mcpServers [ 'parallel-code' ] ;
1286+ if ( content . mcpServers ) {
1287+ if ( coordinator . previousMcpParallelCode !== undefined ) {
1288+ content . mcpServers [ 'parallel-code' ] = coordinator . previousMcpParallelCode ;
1289+ } else {
1290+ delete content . mcpServers [ 'parallel-code' ] ;
1291+ }
1292+ }
12601293 const hasServers = Object . keys ( content . mcpServers ?? { } ) . length > 0 ;
12611294 const hasOtherKeys = Object . keys ( content ) . filter ( ( k ) => k !== 'mcpServers' ) . length > 0 ;
12621295 if ( ! hasServers && ! hasOtherKeys ) {
0 commit comments