@@ -6005,6 +6005,149 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
60056005 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) , TestClock . withLive ) ,
60066006 ) ;
60076007
6008+ it . effect ( "subscribeThread sends a fresh snapshot instead of replaying a large gap" , ( ) =>
6009+ Effect . gen ( function * ( ) {
6010+ let readEventsCalls = 0 ;
6011+ const thread = makeDefaultOrchestrationReadModel ( ) . threads [ 0 ] ! ;
6012+
6013+ yield * buildAppUnderTest ( {
6014+ layers : {
6015+ orchestrationEngine : {
6016+ // Head is far ahead of the client's afterSequence (gap > 1000).
6017+ latestSequence : Effect . succeed ( 100_000 ) ,
6018+ readEvents : ( ) =>
6019+ Stream . sync ( ( ) => {
6020+ readEventsCalls += 1 ;
6021+ return { } as OrchestrationEvent ;
6022+ } ) ,
6023+ } ,
6024+ projectionSnapshotQuery : {
6025+ getThreadDetailSnapshot : ( ) =>
6026+ Effect . succeed ( Option . some ( { snapshotSequence : 100_000 , thread } ) ) ,
6027+ } ,
6028+ } ,
6029+ } ) ;
6030+
6031+ const wsUrl = yield * getWsServerUrl ( "/ws" ) ;
6032+ const items = yield * Effect . scoped (
6033+ withWsRpcClient ( wsUrl , ( client ) =>
6034+ client [ ORCHESTRATION_WS_METHODS . subscribeThread ] ( {
6035+ threadId : defaultThreadId ,
6036+ afterSequence : 5 ,
6037+ requestCompletionMarker : true ,
6038+ } ) . pipe ( Stream . take ( 2 ) , Stream . runCollect ) ,
6039+ ) ,
6040+ ) ;
6041+
6042+ const [ first , second ] = Array . from ( items ) ;
6043+ // Large gap => fresh thread snapshot, and the global replay never starts.
6044+ assert . equal ( first ?. kind , "snapshot" ) ;
6045+ if ( first ?. kind === "snapshot" ) {
6046+ assert . equal ( first . snapshot . thread . id , defaultThreadId ) ;
6047+ assert . equal ( first . snapshot . snapshotSequence , 100_000 ) ;
6048+ }
6049+ assert . equal ( second ?. kind , "synchronized" ) ;
6050+ assert . equal ( readEventsCalls , 0 ) ;
6051+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
6052+ ) ;
6053+
6054+ it . effect ( "subscribeThread replaces a cursor ahead of the authoritative head" , ( ) =>
6055+ Effect . gen ( function * ( ) {
6056+ let readEventsCalls = 0 ;
6057+ const thread = makeDefaultOrchestrationReadModel ( ) . threads [ 0 ] ! ;
6058+
6059+ yield * buildAppUnderTest ( {
6060+ layers : {
6061+ orchestrationEngine : {
6062+ latestSequence : Effect . succeed ( 5 ) ,
6063+ readEvents : ( ) =>
6064+ Stream . sync ( ( ) => {
6065+ readEventsCalls += 1 ;
6066+ return { } as OrchestrationEvent ;
6067+ } ) ,
6068+ } ,
6069+ projectionSnapshotQuery : {
6070+ getThreadDetailSnapshot : ( ) =>
6071+ Effect . succeed ( Option . some ( { snapshotSequence : 5 , thread } ) ) ,
6072+ } ,
6073+ } ,
6074+ } ) ;
6075+
6076+ const wsUrl = yield * getWsServerUrl ( "/ws" ) ;
6077+ const first = yield * Effect . scoped (
6078+ withWsRpcClient ( wsUrl , ( client ) =>
6079+ client [ ORCHESTRATION_WS_METHODS . subscribeThread ] ( {
6080+ threadId : defaultThreadId ,
6081+ afterSequence : 10 ,
6082+ } ) . pipe ( Stream . runHead ) ,
6083+ ) ,
6084+ ) ;
6085+
6086+ assert . equal ( Option . getOrThrow ( first ) . kind , "snapshot" ) ;
6087+ assert . equal ( readEventsCalls , 0 ) ;
6088+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
6089+ ) ;
6090+
6091+ it . effect ( "subscribeThread bounds catch-up replay to the captured head" , ( ) =>
6092+ Effect . gen ( function * ( ) {
6093+ let replayLimit : number | undefined ;
6094+ const now = "2026-01-01T00:00:00.000Z" ;
6095+ const messageEvent = {
6096+ sequence : 3 ,
6097+ eventId : EventId . make ( "event-replay-message" ) ,
6098+ aggregateKind : "thread" ,
6099+ aggregateId : defaultThreadId ,
6100+ occurredAt : now ,
6101+ commandId : null ,
6102+ causationEventId : null ,
6103+ correlationId : null ,
6104+ metadata : { } ,
6105+ type : "thread.message-sent" ,
6106+ payload : {
6107+ threadId : defaultThreadId ,
6108+ messageId : MessageId . make ( "message-replay" ) ,
6109+ role : "user" ,
6110+ text : "Replayed message" ,
6111+ turnId : null ,
6112+ streaming : false ,
6113+ createdAt : now ,
6114+ updatedAt : now ,
6115+ } ,
6116+ } satisfies Extract < OrchestrationEvent , { type : "thread.message-sent" } > ;
6117+
6118+ yield * buildAppUnderTest ( {
6119+ layers : {
6120+ orchestrationEngine : {
6121+ latestSequence : Effect . succeed ( 50 ) ,
6122+ readEvents : ( _afterSequence , limit ) => {
6123+ replayLimit = limit ;
6124+ return Stream . make ( messageEvent ) ;
6125+ } ,
6126+ } ,
6127+ } ,
6128+ } ) ;
6129+
6130+ const wsUrl = yield * getWsServerUrl ( "/ws" ) ;
6131+ const items = yield * Effect . scoped (
6132+ withWsRpcClient ( wsUrl , ( client ) =>
6133+ client [ ORCHESTRATION_WS_METHODS . subscribeThread ] ( {
6134+ threadId : defaultThreadId ,
6135+ afterSequence : 0 ,
6136+ requestCompletionMarker : true ,
6137+ } ) . pipe ( Stream . take ( 2 ) , Stream . runCollect ) ,
6138+ ) ,
6139+ ) ;
6140+
6141+ const [ first , second ] = Array . from ( items ) ;
6142+ assert . equal ( first ?. kind , "event" ) ;
6143+ assert . equal ( first ?. kind === "event" ? first . event . sequence : null , 3 ) ;
6144+ assert . equal ( second ?. kind , "synchronized" ) ;
6145+ // The replay is bounded to the head captured before the read, not
6146+ // Number.MAX_SAFE_INTEGER.
6147+ assert . equal ( replayLimit , 50 ) ;
6148+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
6149+ ) ;
6150+
60086151 it . effect ( "subscribeShell sends a fresh snapshot instead of replaying a large gap" , ( ) =>
60096152 Effect . gen ( function * ( ) {
60106153 let readEventsCalls = 0 ;
0 commit comments