@@ -164,6 +164,11 @@ const blockingProcessor = Layer.succeed(
164164 } ) ,
165165)
166166
167+ const providerGetModelStarted : Array < ( ) => void > = [ ]
168+ const blockingProvider = Layer . mock ( ProviderSvc . Service , {
169+ getModel : ( ) => Effect . sync ( ( ) => providerGetModelStarted . shift ( ) ?.( ) ) . pipe ( Effect . andThen ( Effect . never ) ) ,
170+ } )
171+
167172const runtimeFlags = RuntimeFlags . layer ( { experimentalEventSystem : true } )
168173
169174const testLLMServerNode = LayerNode . make ( { service : TestLLMServer , layer : TestLLMServer . layer , deps : [ ] } )
@@ -208,7 +213,11 @@ const promptRoot = LayerNode.group([
208213 RuntimeFlags . node ,
209214] )
210215
211- function makePrompt ( input ?: { mcpInstructions ?: MCP . ServerInstructions [ ] ; processor ?: "blocking" } ) {
216+ function makePrompt ( input ?: {
217+ mcpInstructions ?: MCP . ServerInstructions [ ]
218+ processor ?: "blocking"
219+ provider ?: "blocking"
220+ } ) {
212221 const replacements = [
213222 [ SessionSummary . node , summary ] ,
214223 [ LSP . node , lsp ] ,
@@ -218,6 +227,9 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
218227 if ( input ?. processor === "blocking" ) {
219228 return LayerNode . compile ( promptRoot , [ ...replacements , [ SessionProcessor . node , blockingProcessor ] ] )
220229 }
230+ if ( input ?. provider === "blocking" ) {
231+ return LayerNode . compile ( promptRoot , [ ...replacements , [ ProviderSvc . node , blockingProvider ] ] )
232+ }
221233 return LayerNode . compile ( promptRoot , replacements )
222234}
223235
@@ -242,6 +254,7 @@ function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[
242254const it = testEffect ( makeHttp ( ) )
243255const noLLMServer = testEffect ( makeHttpNoLLMServer ( ) )
244256const raceNoLLMServer = testEffect ( makeHttpNoLLMServer ( { processor : "blocking" } ) )
257+ const rolloverNoLLMServer = testEffect ( makePrompt ( { provider : "blocking" } ) )
245258const withMcpInstructions = testEffect (
246259 makeHttp ( {
247260 mcpInstructions : [
@@ -500,6 +513,73 @@ noLLMServer.instance(
500513 { config : cfg } ,
501514)
502515
516+ rolloverNoLLMServer . instance (
517+ "loop starts a new user turn after message ID rollover" ,
518+ ( ) =>
519+ Effect . gen ( function * ( ) {
520+ providerGetModelStarted . length = 0
521+ yield * Effect . addFinalizer ( ( ) =>
522+ Effect . sync ( ( ) => {
523+ providerGetModelStarted . length = 0
524+ } ) ,
525+ )
526+
527+ const prompt = yield * SessionPrompt . Service
528+ const sessions = yield * Session . Service
529+ const chat = yield * sessions . create ( { title : "Pinned" } )
530+ const oldUserID = MessageID . make ( "msg_ffffffffe001olduser" )
531+ const oldAssistantID = MessageID . make ( "msg_fffffffff001oldassistant" )
532+ const newUserID = MessageID . make ( "msg_000000001001newuser" )
533+ yield * sessions . updateMessage ( {
534+ id : oldUserID ,
535+ role : "user" ,
536+ sessionID : chat . id ,
537+ agent : "build" ,
538+ model : ref ,
539+ time : { created : 100 } ,
540+ } )
541+ yield * sessions . updateMessage ( {
542+ id : oldAssistantID ,
543+ role : "assistant" ,
544+ parentID : oldUserID ,
545+ sessionID : chat . id ,
546+ mode : "build" ,
547+ agent : "build" ,
548+ cost : 0 ,
549+ path : { cwd : "/tmp" , root : "/tmp" } ,
550+ tokens : { input : 0 , output : 0 , reasoning : 0 , cache : { read : 0 , write : 0 } } ,
551+ modelID : ref . modelID ,
552+ providerID : ref . providerID ,
553+ time : { created : 200 , completed : 201 } ,
554+ finish : "stop" ,
555+ } )
556+ yield * sessions . updateMessage ( {
557+ id : newUserID ,
558+ role : "user" ,
559+ sessionID : chat . id ,
560+ agent : "build" ,
561+ model : ref ,
562+ time : { created : 300 } ,
563+ } )
564+ yield * sessions . updatePart ( {
565+ id : PartID . ascending ( ) ,
566+ messageID : newUserID ,
567+ sessionID : chat . id ,
568+ type : "text" ,
569+ text : "continue after rollover" ,
570+ } )
571+ const latest = MessageV2 . latest ( yield * sessions . messages ( { sessionID : chat . id } ) )
572+ expect ( latest . user ?. id ) . toBe ( newUserID )
573+ expect ( latest . assistant ?. id ) . toBe ( oldAssistantID )
574+
575+ const started = defer < void > ( )
576+ providerGetModelStarted . push ( started . resolve )
577+ const loop = yield * prompt . loop ( { sessionID : chat . id } ) . pipe ( Effect . forkChild )
578+ yield * awaitWithTimeout ( Effect . promise ( ( ) => started . promise ) , "rollover turn never started" )
579+ yield * Fiber . interrupt ( loop )
580+ } ) ,
581+ )
582+
503583it . instance ( "loop exits without an LLM request for interrupted orphan tool calls" , ( ) =>
504584 Effect . gen ( function * ( ) {
505585 const { llm } = yield * useServerConfig ( providerCfg )
0 commit comments