1- import { CommandId , type OrchestrationV2ThreadProjection } from "@t3tools/contracts" ;
1+ import {
2+ CommandId ,
3+ type MessageId ,
4+ type OrchestrationV2ThreadProjection ,
5+ } from "@t3tools/contracts" ;
26import * as Effect from "effect/Effect" ;
37import * as Layer from "effect/Layer" ;
8+ import * as Option from "effect/Option" ;
49import * as Ref from "effect/Ref" ;
10+ import * as Stream from "effect/Stream" ;
511
612import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator" ;
713import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests" ;
14+ import type * as Orchestrator from "./Orchestrator.ts" ;
815import * as ThreadManagementService from "./ThreadManagementService.ts" ;
916import { isUndeliveredMailboxSteer } from "./NotificationMailbox.ts" ;
1017
@@ -56,6 +63,7 @@ export const layer = Layer.effectDiscard(
5663 const requests = yield * ProviderContinuationRequests . ProviderContinuationRequests ;
5764 const threads = yield * ThreadManagementService . ThreadManagementService ;
5865 const retryAttempts = yield * Ref . make ( new Map < string , number > ( ) ) ;
66+ const layerScope = yield * Effect . scope ;
5967
6068 const clearRetryAttempt = ( key : string ) =>
6169 Ref . update ( retryAttempts , ( current ) => {
@@ -73,6 +81,50 @@ export const layer = Layer.effectDiscard(
7381 return [ Math . min ( 100 * 2 ** Math . min ( attempt , 6 ) , 5_000 ) , updated ] as const ;
7482 } ) ;
7583
84+ // A continuation run can end without starting (a queued cancel), so the adapter never sees it.
85+ const releaseWhenRunEnds = Effect . fn ( "ProviderContinuationService.releaseWhenRunEnds" ) (
86+ function * ( input : {
87+ readonly request : ProviderContinuationRequests . ProviderContinuationRequest ;
88+ readonly clearIfCurrent : ( ) => Effect . Effect < void > ;
89+ readonly messageId : MessageId ;
90+ readonly dispatched : Orchestrator . OrchestratorV2DispatchResult ;
91+ } ) {
92+ const runId = input . dispatched . storedEvents . flatMap ( ( { event } ) =>
93+ event . type === "run.created" && event . payload . userMessageId === input . messageId
94+ ? [ event . payload . id ]
95+ : [ ] ,
96+ ) [ 0 ] ;
97+ if ( runId === undefined ) return ;
98+ // Replays from the dispatch so a run that ended before this subscription still counts.
99+ const ended = yield * threads
100+ . streamStoredEventsFrom ( {
101+ threadId : input . request . threadId ,
102+ afterSequence : input . dispatched . sequence ,
103+ eventType : "run.updated" ,
104+ } )
105+ . pipe (
106+ Stream . filter (
107+ ( { event } ) =>
108+ event . type === "run.updated" &&
109+ event . payload . id === runId &&
110+ ThreadManagementService . isTerminalRunStatus ( event . payload . status ) ,
111+ ) ,
112+ Stream . runHead ,
113+ ) ;
114+ if ( Option . isSome ( ended ) ) yield * input . clearIfCurrent ( ) ;
115+ } ,
116+ ( effect , input ) =>
117+ effect . pipe (
118+ Effect . catchCause ( ( cause ) =>
119+ Effect . logWarning ( "orchestration-v2.provider-continuation.release-watch-failed" , {
120+ threadId : input . request . threadId ,
121+ providerThreadId : input . request . providerThreadId ,
122+ cause,
123+ } ) ,
124+ ) ,
125+ ) ,
126+ ) ;
127+
76128 const dispatchContinuation = Effect . fn ( "ProviderContinuationService.dispatchContinuation" ) (
77129 function * ( request : ProviderContinuationRequests . ProviderContinuationRequest ) {
78130 const projection = yield * threads . getThreadRecords (
@@ -218,19 +270,26 @@ export const layer = Layer.effectDiscard(
218270 request . clearIfCurrent !== undefined ? request . clearIfCurrent ( ) : Effect . void ,
219271 ) ,
220272 ) ;
221- if ( request . dispatchIfCurrent === undefined ) {
222- yield * dispatch . pipe (
223- Effect . catchTags ( {
224- ThreadManagementProviderInstanceDisabledError : logInstanceDisabled ,
225- } ) ,
226- ) ;
227- return ;
273+ const refused = (
274+ error : ThreadManagementService . ThreadManagementProviderInstanceDisabledError ,
275+ ) => logInstanceDisabled ( error ) . pipe ( Effect . as ( Option . none ( ) ) ) ;
276+ const dispatched =
277+ request . dispatchIfCurrent === undefined
278+ ? yield * dispatch . pipe (
279+ Effect . asSome ,
280+ Effect . catchTags ( { ThreadManagementProviderInstanceDisabledError : refused } ) ,
281+ )
282+ : yield * request
283+ . dispatchIfCurrent ( dispatch )
284+ . pipe ( Effect . catchTags ( { ThreadManagementProviderInstanceDisabledError : refused } ) ) ;
285+ if ( Option . isSome ( dispatched ) && request . clearIfCurrent !== undefined ) {
286+ yield * releaseWhenRunEnds ( {
287+ request,
288+ clearIfCurrent : request . clearIfCurrent ,
289+ messageId,
290+ dispatched : dispatched . value ,
291+ } ) . pipe ( Effect . forkIn ( layerScope ) ) ;
228292 }
229- yield * request . dispatchIfCurrent ( dispatch ) . pipe (
230- Effect . catchTags ( {
231- ThreadManagementProviderInstanceDisabledError : logInstanceDisabled ,
232- } ) ,
233- ) ;
234293 } ,
235294 ) ;
236295
0 commit comments