@@ -389,10 +389,41 @@ function spawnRequestKey(
389389 } )
390390}
391391
392+ function personaSpawnRequestKey ( projectId : string , personaId : string ) : string {
393+ return JSON . stringify ( {
394+ projectId,
395+ personaId
396+ } )
397+ }
398+
392399function isRecord ( value : unknown ) : value is Record < string , unknown > {
393400 return typeof value === 'object' && value !== null
394401}
395402
403+ type BrokerSpawnResult = {
404+ name : string
405+ runtime : string
406+ cli ?: string
407+ }
408+
409+ function normalizeSpawnPtyResult ( value : unknown , fallbackName : string , cli ?: string ) : BrokerSpawnResult {
410+ const record = isRecord ( value ) ? value : { }
411+ const name = typeof record . name === 'string' && record . name . trim ( )
412+ ? record . name . trim ( )
413+ : fallbackName
414+ const runtime = typeof record . runtime === 'string' && record . runtime . trim ( )
415+ ? record . runtime . trim ( )
416+ : 'pty'
417+ const result : BrokerSpawnResult = { name, runtime }
418+ const resolvedCli = typeof cli === 'string' && cli . trim ( )
419+ ? cli . trim ( )
420+ : typeof record . cli === 'string' && record . cli . trim ( )
421+ ? record . cli . trim ( )
422+ : undefined
423+ if ( resolvedCli ) result . cli = resolvedCli
424+ return result
425+ }
426+
396427function brokerEventString ( event : BrokerEvent , key : string ) : string | undefined {
397428 const value = ( event as unknown as Record < string , unknown > ) [ key ]
398429 return typeof value === 'string' ? value : undefined
@@ -1176,7 +1207,7 @@ export class BrokerManager {
11761207 private sessions = new Map < string , BrokerSession > ( )
11771208 private startPromises = new Map < string , Promise < boolean | void > > ( )
11781209 private revivePromises = new Map < string , Promise < boolean > > ( )
1179- private inFlightSpawnRequests = new Map < string , Promise < { name : string ; runtime : string } > > ( )
1210+ private inFlightSpawnRequests = new Map < string , Promise < BrokerSpawnResult > > ( )
11801211 // Which broker sessions (by session key) an agent name is registered on.
11811212 // Both a project's local and cloud brokers join the same relay workspace,
11821213 // so agent names are project-unique in practice — the set tracks which
@@ -2306,12 +2337,12 @@ export class BrokerManager {
23062337 projectId : string ,
23072338 spawnInput : SpawnPtyInput & { broker ?: 'local' | 'cloud' } ,
23082339 options : { parentAgentName ?: string } = { }
2309- ) : Promise < { name : string ; runtime : string } > {
2340+ ) : Promise < BrokerSpawnResult > {
23102341 const requestKey = spawnRequestKey ( projectId , spawnInput , options )
23112342 const inFlight = this . inFlightSpawnRequests . get ( requestKey )
23122343 if ( inFlight ) return inFlight
23132344
2314- let promise ! : Promise < { name : string ; runtime : string } >
2345+ let promise ! : Promise < BrokerSpawnResult >
23152346 promise = this . spawnAgentOnce ( projectId , spawnInput , options ) . finally ( ( ) => {
23162347 if ( this . inFlightSpawnRequests . get ( requestKey ) === promise ) {
23172348 this . inFlightSpawnRequests . delete ( requestKey )
@@ -2325,7 +2356,7 @@ export class BrokerManager {
23252356 projectId : string ,
23262357 spawnInput : SpawnPtyInput & { broker ?: 'local' | 'cloud' } ,
23272358 options : { parentAgentName ?: string } = { }
2328- ) : Promise < { name : string ; runtime : string } > {
2359+ ) : Promise < BrokerSpawnResult > {
23292360 // `broker` selects which of the project's sessions the agent spawns on.
23302361 // Default: local-first via getSessionForProject (cloud only when no local
23312362 // broker is running, preserving the cloud-only flow).
@@ -2377,7 +2408,8 @@ export class BrokerManager {
23772408 )
23782409 }
23792410 const spawned = await session . client . spawnPty ( nextInput )
2380- const spawnedName = spawned . name || nextInput . name
2411+ const safeSpawned = normalizeSpawnPtyResult ( spawned , nextInput . name )
2412+ const spawnedName = safeSpawned . name
23812413 this . rememberAgentSession ( spawnedName , sessionKeyFor ( session ) )
23822414 const burnInput = { ...nextInput , name : spawnedName }
23832415 const lineage = session . pearLineage . get ( spawnedName )
@@ -2393,12 +2425,7 @@ export class BrokerManager {
23932425 ) . catch ( ( err ) => {
23942426 console . warn ( '[burn-spawn-hook] post-spawn burn stamp failed:' , err )
23952427 } )
2396- return {
2397- name : spawnedName ,
2398- runtime : typeof spawned . runtime === 'string' && spawned . runtime . trim ( )
2399- ? spawned . runtime
2400- : 'pty'
2401- }
2428+ return safeSpawned
24022429 } catch ( err ) {
24032430 if ( ! isAgentNameConflict ( err ) ) {
24042431 throw buildSpawnFailureError ( err , nextInput , session . cloudSandboxId ? 'cloud' : 'local' )
@@ -2425,13 +2452,29 @@ export class BrokerManager {
24252452 }
24262453 }
24272454
2428- async spawnPersona ( projectId : string , personaId : string ) : Promise < { name : string ; runtime : string ; cli ?: string } > {
2429- const session = this . getSessionForProject ( projectId )
2455+ async spawnPersona ( projectId : string , personaId : string ) : Promise < BrokerSpawnResult > {
24302456 const trimmedPersonaId = personaId . trim ( )
24312457 if ( ! trimmedPersonaId ) {
24322458 throw new Error ( 'Persona id is required' )
24332459 }
24342460
2461+ const requestKey = personaSpawnRequestKey ( projectId , trimmedPersonaId )
2462+ const inFlight = this . inFlightSpawnRequests . get ( requestKey )
2463+ if ( inFlight ) return inFlight
2464+
2465+ let promise ! : Promise < BrokerSpawnResult >
2466+ promise = this . spawnPersonaOnce ( projectId , trimmedPersonaId ) . finally ( ( ) => {
2467+ if ( this . inFlightSpawnRequests . get ( requestKey ) === promise ) {
2468+ this . inFlightSpawnRequests . delete ( requestKey )
2469+ }
2470+ } )
2471+ this . inFlightSpawnRequests . set ( requestKey , promise )
2472+ return promise
2473+ }
2474+
2475+ private async spawnPersonaOnce ( projectId : string , trimmedPersonaId : string ) : Promise < BrokerSpawnResult > {
2476+ const session = this . getSessionForProject ( projectId )
2477+
24352478 const command = resolveAgentWorkforceCommand ( session . cwd )
24362479 const persona = findWorkforcePersona ( session . cwd , trimmedPersonaId , command )
24372480
@@ -2478,7 +2521,7 @@ export class BrokerManager {
24782521 command : { cli : string ; args : string [ ] }
24792522 resolvedHarness : string
24802523 }
2481- ) : Promise < { name : string ; runtime : string ; cli ?: string } > {
2524+ ) : Promise < BrokerSpawnResult > {
24822525 const existingNames = new Set (
24832526 ( await session . client . listAgents ( ) ) . map ( ( agent ) => agent . name )
24842527 )
@@ -2495,15 +2538,9 @@ export class BrokerManager {
24952538 for ( let attempt = 0 ; attempt < 20 ; attempt += 1 ) {
24962539 try {
24972540 const spawned = await session . client . spawnPty ( nextInput )
2498- const spawnedName = spawned . name || nextInput . name
2499- this . rememberAgentSession ( spawnedName , sessionKeyFor ( session ) )
2500- return {
2501- name : spawnedName ,
2502- runtime : typeof spawned . runtime === 'string' && spawned . runtime . trim ( )
2503- ? spawned . runtime
2504- : 'pty' ,
2505- cli : input . resolvedHarness
2506- }
2541+ const safeSpawned = normalizeSpawnPtyResult ( spawned , nextInput . name , input . resolvedHarness )
2542+ this . rememberAgentSession ( safeSpawned . name , sessionKeyFor ( session ) )
2543+ return safeSpawned
25072544 } catch ( err ) {
25082545 if ( ! isAgentNameConflict ( err ) ) {
25092546 throw err
0 commit comments