@@ -19,6 +19,14 @@ vi.mock("../gateway/call.js", () => ({
1919 callGateway : vi . fn ( async ( ) => ( { runId : "test-run-id" } ) ) ,
2020} ) ) ;
2121
22+ vi . mock ( "../gateway/session-utils.fs.js" , ( ) => ( {
23+ readSessionMessages : vi . fn ( ( ) => [ ] ) ,
24+ } ) ) ;
25+
26+ vi . mock ( "./subagent-registry.js" , ( ) => ( {
27+ replaceSubagentRunAfterSteer : vi . fn ( ( ) => true ) ,
28+ } ) ) ;
29+
2230function createTestRunRecord ( overrides : Partial < SubagentRunRecord > = { } ) : SubagentRunRecord {
2331 return {
2432 runId : "run-1" ,
@@ -45,6 +53,7 @@ describe("subagent-orphan-recovery", () => {
4553 it ( "recovers orphaned sessions with abortedLastRun=true" , async ( ) => {
4654 const sessions = await import ( "../config/sessions.js" ) ;
4755 const gateway = await import ( "../gateway/call.js" ) ;
56+ const subagentRegistry = await import ( "./subagent-registry.js" ) ;
4857
4958 const sessionEntry = {
5059 sessionId : "session-abc" ,
@@ -78,6 +87,10 @@ describe("subagent-orphan-recovery", () => {
7887 expect ( params . sessionKey ) . toBe ( "agent:main:subagent:test-session-1" ) ;
7988 expect ( params . message ) . toContain ( "gateway reload" ) ;
8089 expect ( params . message ) . toContain ( "Test task: implement feature X" ) ;
90+ expect ( subagentRegistry . replaceSubagentRunAfterSteer ) . toHaveBeenCalledWith ( {
91+ previousRunId : "run-1" ,
92+ nextRunId : "test-run-id" ,
93+ } ) ;
8194 } ) ;
8295
8396 it ( "skips sessions that are not aborted" , async ( ) => {
@@ -321,4 +334,100 @@ describe("subagent-orphan-recovery", () => {
321334 expect ( message . length ) . toBeLessThan ( 5000 ) ;
322335 expect ( message ) . toContain ( "..." ) ;
323336 } ) ;
337+
338+ it ( "includes last human message in resume when available" , async ( ) => {
339+ const sessions = await import ( "../config/sessions.js" ) ;
340+ const gateway = await import ( "../gateway/call.js" ) ;
341+ const sessionUtils = await import ( "../gateway/session-utils.fs.js" ) ;
342+
343+ vi . mocked ( sessions . loadSessionStore ) . mockReturnValue ( {
344+ "agent:main:subagent:test-session-1" : {
345+ sessionId : "session-abc" ,
346+ updatedAt : Date . now ( ) ,
347+ abortedLastRun : true ,
348+ sessionFile : "session-abc.jsonl" ,
349+ } ,
350+ } ) ;
351+
352+ vi . mocked ( sessionUtils . readSessionMessages ) . mockReturnValue ( [
353+ { role : "user" , content : [ { type : "text" , text : "Please build feature Y" } ] } ,
354+ { role : "assistant" , content : [ { type : "text" , text : "Working on it..." } ] } ,
355+ { role : "user" , content : [ { type : "text" , text : "Also add tests for it" } ] } ,
356+ { role : "assistant" , content : [ { type : "text" , text : "Sure, adding tests now." } ] } ,
357+ ] ) ;
358+
359+ const activeRuns = new Map < string , SubagentRunRecord > ( ) ;
360+ activeRuns . set ( "run-1" , createTestRunRecord ( ) ) ;
361+
362+ const { recoverOrphanedSubagentSessions } = await import ( "./subagent-orphan-recovery.js" ) ;
363+ await recoverOrphanedSubagentSessions ( { getActiveRuns : ( ) => activeRuns } ) ;
364+
365+ const callArgs = vi . mocked ( gateway . callGateway ) . mock . calls [ 0 ] ;
366+ const params = callArgs [ 0 ] . params as Record < string , unknown > ;
367+ const message = params . message as string ;
368+ expect ( message ) . toContain ( "Also add tests for it" ) ;
369+ expect ( message ) . toContain ( "last message from the user" ) ;
370+ } ) ;
371+
372+ it ( "adds config change hint when assistant messages reference config modifications" , async ( ) => {
373+ const sessions = await import ( "../config/sessions.js" ) ;
374+ const gateway = await import ( "../gateway/call.js" ) ;
375+ const sessionUtils = await import ( "../gateway/session-utils.fs.js" ) ;
376+
377+ vi . mocked ( sessions . loadSessionStore ) . mockReturnValue ( {
378+ "agent:main:subagent:test-session-1" : {
379+ sessionId : "session-abc" ,
380+ updatedAt : Date . now ( ) ,
381+ abortedLastRun : true ,
382+ } ,
383+ } ) ;
384+
385+ vi . mocked ( sessionUtils . readSessionMessages ) . mockReturnValue ( [
386+ { role : "user" , content : "Update the config" } ,
387+ { role : "assistant" , content : "I've modified openclaw.json to add the new setting." } ,
388+ ] ) ;
389+
390+ const activeRuns = new Map < string , SubagentRunRecord > ( ) ;
391+ activeRuns . set ( "run-1" , createTestRunRecord ( ) ) ;
392+
393+ const { recoverOrphanedSubagentSessions } = await import ( "./subagent-orphan-recovery.js" ) ;
394+ await recoverOrphanedSubagentSessions ( { getActiveRuns : ( ) => activeRuns } ) ;
395+
396+ const callArgs = vi . mocked ( gateway . callGateway ) . mock . calls [ 0 ] ;
397+ const params = callArgs [ 0 ] . params as Record < string , unknown > ;
398+ const message = params . message as string ;
399+ expect ( message ) . toContain ( "config changes from your previous run were already applied" ) ;
400+ } ) ;
401+
402+ it ( "prevents duplicate resume when updateSessionStore fails" , async ( ) => {
403+ const sessions = await import ( "../config/sessions.js" ) ;
404+ const gateway = await import ( "../gateway/call.js" ) ;
405+
406+ vi . mocked ( gateway . callGateway ) . mockResolvedValue ( { runId : "new-run" } as never ) ;
407+ vi . mocked ( sessions . updateSessionStore ) . mockRejectedValue ( new Error ( "write failed" ) ) ;
408+
409+ vi . mocked ( sessions . loadSessionStore ) . mockReturnValue ( {
410+ "agent:main:subagent:test-session-1" : {
411+ sessionId : "session-abc" ,
412+ updatedAt : Date . now ( ) ,
413+ abortedLastRun : true ,
414+ } ,
415+ } ) ;
416+
417+ const activeRuns = new Map < string , SubagentRunRecord > ( ) ;
418+ activeRuns . set ( "run-1" , createTestRunRecord ( ) ) ;
419+ activeRuns . set (
420+ "run-2" ,
421+ createTestRunRecord ( {
422+ runId : "run-2" ,
423+ } ) ,
424+ ) ;
425+
426+ const { recoverOrphanedSubagentSessions } = await import ( "./subagent-orphan-recovery.js" ) ;
427+ const result = await recoverOrphanedSubagentSessions ( { getActiveRuns : ( ) => activeRuns } ) ;
428+
429+ expect ( result . recovered ) . toBe ( 1 ) ;
430+ expect ( result . skipped ) . toBe ( 1 ) ;
431+ expect ( gateway . callGateway ) . toHaveBeenCalledOnce ( ) ;
432+ } ) ;
324433} ) ;
0 commit comments