@@ -32,6 +32,7 @@ import { createSessionConversationTestRegistry } from "../../test-utils/session-
3232import { drainFormattedSystemEvents } from "./session-updates.js" ;
3333import { persistSessionUsageUpdate } from "./session-usage.js" ;
3434import { initSessionState } from "./session.js" ;
35+ import { replyRunRegistry } from "./reply-run-registry.js" ;
3536
3637const sessionForkMocks = vi . hoisted ( ( ) => ( {
3738 forkSessionFromParent : vi . fn ( ) ,
@@ -3709,6 +3710,177 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
37093710 }
37103711 } ) ;
37113712
3713+ it ( "defers implicit daily rollover while the same session has an active run" , async ( ) => {
3714+ vi . useFakeTimers ( ) ;
3715+ const existingSessionId = "active-stale-session" ;
3716+ let operation : ReturnType < typeof replyRunRegistry . begin > | undefined ;
3717+ try {
3718+ vi . setSystemTime ( new Date ( 2026 , 0 , 18 , 5 , 0 , 0 ) ) ;
3719+ const storePath = await createStorePath ( "openclaw-active-stale-archive-" ) ;
3720+ const sessionKey = "agent:main:telegram:dm:active-stale-user" ;
3721+ const transcriptPath = path . join ( path . dirname ( storePath ) , `${ existingSessionId } .jsonl` ) ;
3722+ const sessionStartedAt = new Date ( 2026 , 0 , 18 , 3 , 0 , 0 ) . getTime ( ) ;
3723+
3724+ await writeSessionStoreFast ( storePath , {
3725+ [ sessionKey ] : {
3726+ sessionId : existingSessionId ,
3727+ updatedAt : sessionStartedAt ,
3728+ sessionStartedAt,
3729+ } ,
3730+ } ) ;
3731+ await fs . writeFile ( transcriptPath , '{"type":"message"}\n' , "utf8" ) ;
3732+ operation = replyRunRegistry . begin ( {
3733+ sessionKey,
3734+ sessionId : existingSessionId ,
3735+ resetTriggered : false ,
3736+ } ) ;
3737+ operation . setPhase ( "running" ) ;
3738+
3739+ const cfg = { session : { store : storePath } } as OpenClawConfig ;
3740+ const result = await initSessionState ( {
3741+ ctx : {
3742+ Body : "hello while active" ,
3743+ RawBody : "hello while active" ,
3744+ CommandBody : "hello while active" ,
3745+ From : "user-active-stale" ,
3746+ To : "bot" ,
3747+ ChatType : "direct" ,
3748+ SessionKey : sessionKey ,
3749+ Provider : "telegram" ,
3750+ Surface : "telegram" ,
3751+ } ,
3752+ cfg,
3753+ commandAuthorized : true ,
3754+ } ) ;
3755+
3756+ expect ( result . isNewSession ) . toBe ( false ) ;
3757+ expect ( result . resetTriggered ) . toBe ( false ) ;
3758+ expect ( result . sessionId ) . toBe ( existingSessionId ) ;
3759+ expect ( result . previousSessionEntry ) . toBeUndefined ( ) ;
3760+ expect ( result . sessionEntry . sessionStartedAt ) . toBe ( sessionStartedAt ) ;
3761+ expect ( await fs . stat ( transcriptPath ) . catch ( ( ) => null ) ) . not . toBeNull ( ) ;
3762+ const archived = ( await fs . readdir ( path . dirname ( storePath ) ) ) . filter ( ( entry ) =>
3763+ entry . startsWith ( `${ existingSessionId } .jsonl.reset.` ) ,
3764+ ) ;
3765+ expect ( archived ) . toHaveLength ( 0 ) ;
3766+ } finally {
3767+ operation ?. complete ( ) ;
3768+ vi . useRealTimers ( ) ;
3769+ }
3770+ } ) ;
3771+
3772+ it ( "does not defer stale archival for the current turn's queued reservation" , async ( ) => {
3773+ vi . useFakeTimers ( ) ;
3774+ let operation : ReturnType < typeof replyRunRegistry . begin > | undefined ;
3775+ try {
3776+ vi . setSystemTime ( new Date ( 2026 , 0 , 18 , 5 , 0 , 0 ) ) ;
3777+ const storePath = await createStorePath ( "openclaw-queued-stale-archive-" ) ;
3778+ const sessionKey = "agent:main:telegram:dm:queued-stale-user" ;
3779+ const existingSessionId = "queued-stale-session" ;
3780+ const transcriptPath = path . join ( path . dirname ( storePath ) , `${ existingSessionId } .jsonl` ) ;
3781+
3782+ await writeSessionStoreFast ( storePath , {
3783+ [ sessionKey ] : {
3784+ sessionId : existingSessionId ,
3785+ updatedAt : new Date ( 2026 , 0 , 18 , 3 , 0 , 0 ) . getTime ( ) ,
3786+ } ,
3787+ } ) ;
3788+ await fs . writeFile ( transcriptPath , '{"type":"message"}\n' , "utf8" ) ;
3789+ operation = replyRunRegistry . begin ( {
3790+ sessionKey,
3791+ sessionId : existingSessionId ,
3792+ resetTriggered : false ,
3793+ } ) ;
3794+
3795+ const cfg = { session : { store : storePath } } as OpenClawConfig ;
3796+ const result = await initSessionState ( {
3797+ ctx : {
3798+ Body : "hello after boundary" ,
3799+ RawBody : "hello after boundary" ,
3800+ CommandBody : "hello after boundary" ,
3801+ From : "user-queued-stale" ,
3802+ To : "bot" ,
3803+ ChatType : "direct" ,
3804+ SessionKey : sessionKey ,
3805+ Provider : "telegram" ,
3806+ Surface : "telegram" ,
3807+ } ,
3808+ cfg,
3809+ commandAuthorized : true ,
3810+ } ) ;
3811+
3812+ expect ( operation . phase ) . toBe ( "queued" ) ;
3813+ expect ( result . isNewSession ) . toBe ( true ) ;
3814+ expect ( result . resetTriggered ) . toBe ( false ) ;
3815+ expect ( result . sessionId ) . not . toBe ( existingSessionId ) ;
3816+ expect ( result . previousSessionEntry ?. sessionId ) . toBe ( existingSessionId ) ;
3817+ expect ( await fs . stat ( transcriptPath ) . catch ( ( ) => null ) ) . toBeNull ( ) ;
3818+ const archived = ( await fs . readdir ( path . dirname ( storePath ) ) ) . filter ( ( entry ) =>
3819+ entry . startsWith ( `${ existingSessionId } .jsonl.reset.` ) ,
3820+ ) ;
3821+ expect ( archived ) . toHaveLength ( 1 ) ;
3822+ } finally {
3823+ operation ?. complete ( ) ;
3824+ vi . useRealTimers ( ) ;
3825+ }
3826+ } ) ;
3827+
3828+ it ( "does not defer stale archival for a different active session id" , async ( ) => {
3829+ vi . useFakeTimers ( ) ;
3830+ let operation : ReturnType < typeof replyRunRegistry . begin > | undefined ;
3831+ try {
3832+ vi . setSystemTime ( new Date ( 2026 , 0 , 18 , 5 , 0 , 0 ) ) ;
3833+ const storePath = await createStorePath ( "openclaw-active-other-stale-archive-" ) ;
3834+ const sessionKey = "agent:main:telegram:dm:active-other-stale-user" ;
3835+ const existingSessionId = "inactive-stale-session" ;
3836+ const transcriptPath = path . join ( path . dirname ( storePath ) , `${ existingSessionId } .jsonl` ) ;
3837+
3838+ await writeSessionStoreFast ( storePath , {
3839+ [ sessionKey ] : {
3840+ sessionId : existingSessionId ,
3841+ updatedAt : new Date ( 2026 , 0 , 18 , 3 , 0 , 0 ) . getTime ( ) ,
3842+ } ,
3843+ } ) ;
3844+ await fs . writeFile ( transcriptPath , '{"type":"message"}\n' , "utf8" ) ;
3845+ operation = replyRunRegistry . begin ( {
3846+ sessionKey,
3847+ sessionId : "different-active-session" ,
3848+ resetTriggered : false ,
3849+ } ) ;
3850+ operation . setPhase ( "running" ) ;
3851+
3852+ const cfg = { session : { store : storePath } } as OpenClawConfig ;
3853+ const result = await initSessionState ( {
3854+ ctx : {
3855+ Body : "hello after boundary" ,
3856+ RawBody : "hello after boundary" ,
3857+ CommandBody : "hello after boundary" ,
3858+ From : "user-active-other-stale" ,
3859+ To : "bot" ,
3860+ ChatType : "direct" ,
3861+ SessionKey : sessionKey ,
3862+ Provider : "telegram" ,
3863+ Surface : "telegram" ,
3864+ } ,
3865+ cfg,
3866+ commandAuthorized : true ,
3867+ } ) ;
3868+
3869+ expect ( result . isNewSession ) . toBe ( true ) ;
3870+ expect ( result . resetTriggered ) . toBe ( false ) ;
3871+ expect ( result . sessionId ) . not . toBe ( existingSessionId ) ;
3872+ expect ( result . previousSessionEntry ?. sessionId ) . toBe ( existingSessionId ) ;
3873+ expect ( await fs . stat ( transcriptPath ) . catch ( ( ) => null ) ) . toBeNull ( ) ;
3874+ const archived = ( await fs . readdir ( path . dirname ( storePath ) ) ) . filter ( ( entry ) =>
3875+ entry . startsWith ( `${ existingSessionId } .jsonl.reset.` ) ,
3876+ ) ;
3877+ expect ( archived ) . toHaveLength ( 1 ) ;
3878+ } finally {
3879+ operation ?. complete ( ) ;
3880+ vi . useRealTimers ( ) ;
3881+ }
3882+ } ) ;
3883+
37123884 it ( "keeps provider-owned CLI sessions on implicit daily reset boundaries" , async ( ) => {
37133885 vi . useFakeTimers ( ) ;
37143886 try {
0 commit comments