@@ -4,7 +4,7 @@ import { spawn } from 'node:child_process';
44import { randomBytes } from 'node:crypto' ;
55import { mkdir , mkdtemp , readFile , readdir , realpath , rename , rm , stat , symlink , unlink , writeFile } from 'node:fs/promises' ;
66import { tmpdir } from 'node:os' ;
7- import { basename , join } from 'node:path' ;
7+ import { basename , join , sep } from 'node:path' ;
88import { fileURLToPath } from 'node:url' ;
99import test from 'node:test' ;
1010import { PassThrough } from 'node:stream' ;
@@ -784,11 +784,38 @@ test('prepared Rescue canonicalizes a resolvable cwd alias before exact binding
784784 parentSessionId : 'aliased-parent' , parentTurnId : 'aliased-origin' , childId, childTurnId : 'aliased-child-turn' ,
785785 prompt : '$zcode:rescue --fresh --wait preserve canonical workspace identity' ,
786786 } ) ;
787+ const aliasedWorkspace = `${ context . workspace } ${ sep } nested${ sep } ..` ;
788+ const canonicalWorkspace = ( await resolveWorkspaceStorage ( context ) ) . workspacePath ;
789+ assert . notEqual ( aliasedWorkspace , canonicalWorkspace ) ;
787790 const output = await runDirectInvocation ( [ 'invoke-prepared' , 'rescue' ] , {
788- cwd : join ( context . workspace , 'nested' , '..' ) , env : { ...context . env , CODEX_THREAD_ID : childId } ,
791+ cwd : aliasedWorkspace , env : { ...context . env , CODEX_THREAD_ID : childId } ,
789792 } ) ;
790793 assert . equal ( output . job . status , 'succeeded' ) ;
791- assert . equal ( output . job . workspace , ( await resolveWorkspaceStorage ( context ) ) . workspacePath ) ;
794+ assert . equal ( output . job . workspace , canonicalWorkspace ) ;
795+ } ) ;
796+
797+ test ( 'bound Rescue choice canonicalizes the persisted caller workspace before reservation' , async ( ) => {
798+ const context = await fixture ( ) ; const parentSessionId = 'aliased-choice-parent' ; const childId = 'aliased-choice-child' ; const childTurnId = 'aliased-choice-turn' ;
799+ await mkdir ( join ( context . workspace , 'nested' ) ) ;
800+ await prepareDirectRescueChild ( context , {
801+ parentSessionId, parentTurnId : 'aliased-choice-origin' , childId, childTurnId,
802+ prompt : '$zcode:rescue --fresh --wait establish exact session' ,
803+ } ) ;
804+ const initial = await runDirectInvocation ( [ 'invoke-prepared' , 'rescue' ] , { cwd : context . workspace , env : { ...context . env , CODEX_THREAD_ID : childId } } ) ;
805+ assert . equal ( initial . job . status , 'succeeded' ) ;
806+ await markForwarding ( context . dataRoot , {
807+ session_id : parentSessionId , turn_id : childTurnId , cwd : context . workspace , hook_event_name : 'SubagentStop' ,
808+ agent_id : childId , agent_type : 'zcode-rescue' ,
809+ } ) ;
810+ const identity = createIdentityStore ( { dataRoot : context . dataRoot } ) ;
811+ await identity . beginCallerTurn ( { sessionId : parentSessionId , turnId : 'aliased-choice-next' , workspace : context . workspace , permissionMode : 'workspace-write' , prompt : '$zcode:rescue continue exact session' } ) ;
812+ const preparation = new PassThrough ( ) ; preparation . end ( `${ JSON . stringify ( { version : 1 , source : 'explicit' , task : 'continue exact session' , options : { execution : 'foreground' } } ) } \n` ) ;
813+ await runDirectInvocation ( [ 'prepare' , 'rescue' ] , { cwd : context . workspace , env : { ...context . env , CODEX_THREAD_ID : parentSessionId } , input : preparation } ) ;
814+ assert . equal ( ( await runDirectInvocation ( [ 'invoke-prepared' , 'rescue' ] , { cwd : context . workspace , env : { ...context . env , CODEX_THREAD_ID : childId } } ) ) . type , 'needs-choice' ) ;
815+ const aliasedWorkspace = `${ context . workspace } ${ sep } nested${ sep } ..` ;
816+ const resumed = await runDirectInvocation ( [ 'invoke-choice' , 'rescue' , 'resume' ] , { cwd : aliasedWorkspace , env : { ...context . env , CODEX_THREAD_ID : childId } } ) ;
817+ assert . equal ( resumed . job . status , 'succeeded' ) ;
818+ assert . equal ( resumed . job . zcodeSessionId , initial . job . zcodeSessionId ) ;
792819} ) ;
793820
794821test ( 'isolated child loss recovers the accepted parent-owned turn without another session send' , { skip : windowsRealSignalSkip } , async ( t ) => {
0 commit comments