@@ -4845,7 +4845,7 @@ describe("daemon mode helpers", () => {
48454845 }
48464846 } ) ;
48474847
4848- it ( "closes passive hydration when a runtime-open guard is cancelled during hydration " , async ( ) => {
4848+ it ( "leaves passive hydration resident when its runtime-open guard is cancelled" , async ( ) => {
48494849 const tempDir = mkdtempSync ( join ( tmpdir ( ) , "prime-agent-daemon-guarded-hydration-" ) ) ;
48504850 let releaseHydration ! : ( ) => void ;
48514851 const hydrationGate = new Promise < void > ( ( resolve ) => {
@@ -4861,27 +4861,148 @@ describe("daemon mode helpers", () => {
48614861 childRuntimeGate : hydrationGate ,
48624862 } ) ;
48634863 const internals = fixture . daemon as unknown as {
4864+ cronStore : AgentCronJobStore ;
48644865 sessions : Map < string , ActiveSessionState > ;
4865- createRuntime (
4866- command : Extract < DaemonCommand , { type : "create" } > ,
4867- guard ?: ( ) => boolean ,
4868- ) : Promise < ActiveSessionState > ;
4866+ createRuntime ( command : Extract < DaemonCommand , { type : "create" } > ) : Promise < ActiveSessionState > ;
4867+ getRunnableCronJob ( id : string ) : AgentCronJob | undefined ;
4868+ runCronJob ( job : AgentCronJob ) : Promise < "skipped" | undefined > ;
4869+ passivateIdleChildren ( threshold : number , now : number , limit : number ) : Promise < number > ;
48694870 } ;
4870- await internals . createRuntime ( { type : "create" , sessionPath : fixture . parentSessionFile } ) ;
4871+ const parentState = await internals . createRuntime ( {
4872+ type : "create" ,
4873+ sessionPath : fixture . parentSessionFile ,
4874+ } ) ;
4875+ const parentSession = parentState . runtime . session as unknown as {
4876+ releaseFinishedRlmChildSession : ReturnType < typeof vi . fn > ;
4877+ } ;
4878+ parentSession . releaseFinishedRlmChildSession = vi . fn ( ( ) => true ) ;
4879+ const childInfo = await readSessionInfo ( fixture . childSessionFile ) ;
4880+ if ( ! childInfo ) throw new Error ( "Missing child session info" ) ;
4881+ const heartbeat = internals . cronStore . createRlmHeartbeat ( {
4882+ activeSessionId : "stale-child-active-id" ,
4883+ sessionId : childInfo . id ,
4884+ sessionFile : fixture . childSessionFile ,
4885+ cwd : tempDir ,
4886+ runtimeKind : "subagent" ,
4887+ scheduleText : "every 30s" ,
4888+ prompt : "must not run" ,
4889+ now : new Date ( "2026-01-01T00:00:00.000Z" ) ,
4890+ } ) ;
48714891 let runnable = true ;
4872- const opening = internals . createRuntime (
4873- { type : "create" , sessionPath : fixture . childSessionFile } ,
4874- ( ) => runnable ,
4875- ) ;
4892+ internals . getRunnableCronJob = vi . fn ( ( ) => ( runnable ? heartbeat : undefined ) ) ;
4893+
4894+ const run = internals . runCronJob ( heartbeat ) ;
48764895 await hydrationStarted ;
48774896 runnable = false ;
48784897 releaseHydration ( ) ;
48794898
4880- await expect ( opening ) . rejects . toThrow ( ) ;
4881- expect (
4882- [ ...internals . sessions . values ( ) ] . some ( ( state ) => state . runtime . metadata . rlmChildId === fixture . childId ) ,
4883- ) . toBe ( false ) ;
4899+ await expect ( run ) . resolves . toBe ( "skipped" ) ;
4900+ internals . cronStore . cancel ( heartbeat . id ) ;
4901+ const childState = [ ...internals . sessions . values ( ) ] . find (
4902+ ( state ) => state . runtime . metadata . rlmChildId === fixture . childId ,
4903+ ) ;
4904+ expect ( childState ) . toBeDefined ( ) ;
4905+ expect ( fixture . runtimeSessions [ 1 ] ?. disposeAsync ) . not . toHaveBeenCalled ( ) ;
4906+ expect ( parentSession . releaseFinishedRlmChildSession ) . not . toHaveBeenCalled ( ) ;
4907+
4908+ await expect ( internals . passivateIdleChildren ( 90 , Date . parse ( "2036-08-01T12:00:00Z" ) , 1 ) ) . resolves . toBe ( 1 ) ;
4909+ expect ( internals . sessions . has ( childState ! . activeSessionId ) ) . toBe ( false ) ;
48844910 expect ( fixture . runtimeSessions [ 1 ] ?. disposeAsync ) . toHaveBeenCalledOnce ( ) ;
4911+ expect ( parentSession . releaseFinishedRlmChildSession ) . toHaveBeenCalledWith (
4912+ fixture . childId ,
4913+ childState ! . runtime . session ,
4914+ ) ;
4915+ } finally {
4916+ releaseHydration ( ) ;
4917+ rmSync ( tempDir , { recursive : true , force : true } ) ;
4918+ }
4919+ } ) ;
4920+
4921+ it ( "keeps an attach-owned passive hydration usable when a joining heartbeat is cancelled" , async ( ) => {
4922+ const tempDir = mkdtempSync ( join ( tmpdir ( ) , "prime-agent-daemon-shared-guarded-hydration-" ) ) ;
4923+ let releaseHydration ! : ( ) => void ;
4924+ const hydrationGate = new Promise < void > ( ( resolve ) => {
4925+ releaseHydration = resolve ;
4926+ } ) ;
4927+ let markHydrationStarted ! : ( ) => void ;
4928+ const hydrationStarted = new Promise < void > ( ( resolve ) => {
4929+ markHydrationStarted = resolve ;
4930+ } ) ;
4931+ let markHeartbeatJoinStarted ! : ( ) => void ;
4932+ const heartbeatJoinStarted = new Promise < void > ( ( resolve ) => {
4933+ markHeartbeatJoinStarted = resolve ;
4934+ } ) ;
4935+ try {
4936+ const fixture = makePersistedRlmDaemonFixture ( tempDir , {
4937+ childRuntimeStarted : markHydrationStarted ,
4938+ childRuntimeGate : hydrationGate ,
4939+ } ) ;
4940+ const internals = fixture . daemon as unknown as {
4941+ cronStore : AgentCronJobStore ;
4942+ sessions : Map < string , ActiveSessionState > ;
4943+ createRuntime ( command : Extract < DaemonCommand , { type : "create" } > ) : Promise < ActiveSessionState > ;
4944+ createAgentMessageController (
4945+ getCurrentState : ( ) => ActiveSessionState | undefined ,
4946+ ) : AgentSessionMessageController ;
4947+ getRunnableCronJob ( id : string ) : AgentCronJob | undefined ;
4948+ runCronJob ( job : AgentCronJob ) : Promise < "skipped" | undefined > ;
4949+ hydratePassiveRlmSubagent ( passive : unknown ) : Promise < ActiveSessionState > ;
4950+ } ;
4951+ const parentState = await internals . createRuntime ( {
4952+ type : "create" ,
4953+ sessionPath : fixture . parentSessionFile ,
4954+ } ) ;
4955+ const parentSession = parentState . runtime . session as unknown as {
4956+ releaseFinishedRlmChildSession : ReturnType < typeof vi . fn > ;
4957+ retainFinishedRlmChildSession : ReturnType < typeof vi . fn > ;
4958+ } ;
4959+ parentSession . releaseFinishedRlmChildSession = vi . fn ( ( ) => true ) ;
4960+ const childInfo = await readSessionInfo ( fixture . childSessionFile ) ;
4961+ if ( ! childInfo ) throw new Error ( "Missing child session info" ) ;
4962+ const heartbeat = internals . cronStore . createRlmHeartbeat ( {
4963+ activeSessionId : "stale-child-active-id" ,
4964+ sessionId : childInfo . id ,
4965+ sessionFile : fixture . childSessionFile ,
4966+ cwd : tempDir ,
4967+ runtimeKind : "subagent" ,
4968+ scheduleText : "every 30s" ,
4969+ prompt : "must not run" ,
4970+ now : new Date ( "2026-01-01T00:00:00.000Z" ) ,
4971+ } ) ;
4972+ let runnable = true ;
4973+ internals . getRunnableCronJob = vi . fn ( ( ) => ( runnable ? heartbeat : undefined ) ) ;
4974+
4975+ const attach = internals . createRuntime ( { type : "create" , sessionPath : fixture . childSessionFile } ) ;
4976+ await hydrationStarted ;
4977+ const hydratePassive = internals . hydratePassiveRlmSubagent . bind ( fixture . daemon ) ;
4978+ internals . hydratePassiveRlmSubagent = vi . fn ( async ( passive ) => {
4979+ markHeartbeatJoinStarted ( ) ;
4980+ return hydratePassive ( passive ) ;
4981+ } ) ;
4982+ const run = internals . runCronJob ( heartbeat ) ;
4983+ await heartbeatJoinStarted ;
4984+ runnable = false ;
4985+ releaseHydration ( ) ;
4986+
4987+ const attachedState = await attach ;
4988+ await expect ( run ) . resolves . toBe ( "skipped" ) ;
4989+ expect ( internals . sessions . get ( attachedState . activeSessionId ) ) . toBe ( attachedState ) ;
4990+ expect ( fixture . runtimeSessions [ 1 ] ?. disposeAsync ) . not . toHaveBeenCalled ( ) ;
4991+ expect ( parentSession . releaseFinishedRlmChildSession ) . not . toHaveBeenCalled ( ) ;
4992+ expect ( parentSession . retainFinishedRlmChildSession ) . toHaveBeenCalledWith (
4993+ fixture . childId ,
4994+ attachedState . runtime . session ,
4995+ ) ;
4996+
4997+ await expect (
4998+ internals
4999+ . createAgentMessageController ( ( ) => parentState )
5000+ . sendAgentMessage ( { target : fixture . childId , message : "still usable" } ) ,
5001+ ) . resolves . toMatchObject ( { deliveryStatus : "delivered" , target : { runtimeKind : "subagent" } } ) ;
5002+ expect ( fixture . acceptAgentMessagePrompt ) . toHaveBeenCalledWith (
5003+ expect . stringContaining ( "still usable" ) ,
5004+ expect . any ( Object ) ,
5005+ ) ;
48855006 } finally {
48865007 releaseHydration ( ) ;
48875008 rmSync ( tempDir , { recursive : true , force : true } ) ;
0 commit comments