1- import { LLM , LLMClient , LLMError , LLMEvent , SystemPart } from "@opencode-ai/llm"
2- import { Cause , DateTime , Effect , FiberSet , Layer , Schema , Semaphore , Stream } from "effect"
1+ import {
2+ LLM ,
3+ LLMClient ,
4+ LLMError ,
5+ LLMEvent ,
6+ SystemPart ,
7+ isContextOverflowFailure ,
8+ type ProviderErrorEvent ,
9+ } from "@opencode-ai/llm"
10+ import { Cause , DateTime , Effect , FiberSet , Layer , Option , Schema , Semaphore , Stream } from "effect"
311import { AgentV2 } from "../../agent"
412import { Config } from "../../config"
513import { Database } from "../../database/database"
@@ -91,7 +99,7 @@ export const layer = Layer.effect(
9199 const skillGuidance = yield * SkillGuidance . Service
92100 const config = yield * Config . Service
93101 const db = ( yield * Database . Service ) . db
94- const compact = SessionCompaction . make ( { events, llm, config : yield * config . entries ( ) } )
102+ const compaction = SessionCompaction . make ( { events, llm, config : yield * config . entries ( ) } )
95103 const getSession = Effect . fn ( "SessionRunner.getSession" ) ( function * ( sessionID : SessionSchema . ID ) {
96104 const session = yield * store . get ( sessionID )
97105 if ( ! session ) return yield * Effect . die ( `Session not found: ${ sessionID } ` )
@@ -130,14 +138,29 @@ export const layer = Layer.effect(
130138 const isQuestionRejected = ( cause : Cause . Cause < unknown > ) =>
131139 cause . reasons . some ( ( reason ) => Cause . isDieReason ( reason ) && reason . defect instanceof QuestionV2 . RejectedError )
132140
133- class RetryTurn extends Error {
134- constructor ( readonly promotion : SessionInput . Delivery | undefined ) {
141+ type TurnTransition =
142+ // Request preparation observed a concurrent Session change and must restart from durable state.
143+ | { readonly _tag : "RebuildPreparedTurn" ; readonly promotion ?: SessionInput . Delivery }
144+ // Overflow compaction completed; rebuild once through the path without overflow recovery.
145+ | { readonly _tag : "ContinueAfterOverflowCompaction" }
146+
147+ class TurnTransitionError extends Error {
148+ constructor ( readonly transition : TurnTransition ) {
135149 super ( )
136150 }
137151 }
152+
153+ const rebuildPreparedTurn = ( promotion ?: SessionInput . Delivery ) =>
154+ new TurnTransitionError ( { _tag : "RebuildPreparedTurn" , promotion } )
155+ const continueAfterOverflowCompaction = new TurnTransitionError ( {
156+ _tag : "ContinueAfterOverflowCompaction" ,
157+ } )
158+
138159 const retryAgentMismatch = ( promotion : SessionInput . Delivery | undefined ) =>
139160 Effect . catchDefect ( ( defect ) =>
140- defect instanceof SessionContextEpoch . AgentMismatch ? Effect . die ( new RetryTurn ( promotion ) ) : Effect . die ( defect ) ,
161+ defect instanceof SessionContextEpoch . AgentMismatch
162+ ? Effect . die ( rebuildPreparedTurn ( promotion ) )
163+ : Effect . die ( defect ) ,
141164 )
142165
143166 const sameModel = Schema . toEquivalence ( Schema . UndefinedOr ( ModelV2 . Ref ) )
@@ -149,6 +172,7 @@ export const layer = Layer.effect(
149172 const runTurnAttempt = Effect . fn ( "SessionRunner.runTurn" ) ( function * (
150173 sessionID : SessionSchema . ID ,
151174 promotion : SessionInput . Delivery | undefined ,
175+ recoverOverflow ?: typeof compaction . compactAfterOverflow ,
152176 ) {
153177 const session = yield * getSession ( sessionID )
154178 if ( session . location . directory !== location . directory || session . location . workspaceID !== location . workspaceID )
@@ -183,7 +207,7 @@ export const layer = Layer.effect(
183207 ) . pipe ( retryAgentMismatch ( undefined ) ) )
184208 const current = yield * getSession ( sessionID )
185209 if ( ( yield * agents . select ( current . agent ) ) . id !== agent . id || ! sameModel ( current . model , session . model ) )
186- return yield * Effect . die ( new RetryTurn ( undefined ) )
210+ return yield * Effect . die ( rebuildPreparedTurn ( ) )
187211 const model = yield * models . resolve ( session )
188212 const entries = yield * SessionHistory . entriesForRunner ( db , session . id , system . baselineSeq )
189213 const context = entries . map ( ( entry ) => entry . message )
@@ -195,8 +219,8 @@ export const layer = Layer.effect(
195219 messages : toLLMMessages ( context , model ) ,
196220 tools : yield * tools . definitions ( ) ,
197221 } )
198- if ( yield * compact ( { sessionID : session . id , entries, model, request } ) )
199- return yield * Effect . die ( new RetryTurn ( undefined ) )
222+ if ( yield * compaction . compactIfNeeded ( { sessionID : session . id , entries, model, request } ) )
223+ return yield * Effect . die ( rebuildPreparedTurn ( ) )
200224 const publisher = createLLMEventPublisher ( events , {
201225 sessionID : session . id ,
202226 agent : agent . id ,
@@ -209,11 +233,19 @@ export const layer = Layer.effect(
209233 const withPublication = Semaphore . makeUnsafe ( 1 ) . withPermit
210234 const publish = ( event : LLMEvent , outputPaths : ReadonlyArray < string > = [ ] ) =>
211235 withPublication ( publisher . publish ( event , outputPaths ) )
236+ let overflowFailure : ProviderErrorEvent | undefined
212237 if ( ! ( yield * SessionContextEpoch . current ( db , session . id , agent . id , system . revision ) ) )
213- return yield * Effect . die ( new RetryTurn ( undefined ) )
238+ return yield * Effect . die ( rebuildPreparedTurn ( ) )
214239 const providerStream = llm . stream ( request ) . pipe (
215240 Stream . runForEach ( ( event ) =>
216241 Effect . gen ( function * ( ) {
242+ if ( overflowFailure || publisher . hasProviderError ( ) ) return
243+ if ( LLMEvent . is . providerError ( event ) ) {
244+ if ( isContextOverflowFailure ( event ) && ! publisher . hasAssistantStarted ( ) ) {
245+ overflowFailure = event
246+ return
247+ }
248+ }
217249 yield * publish ( event )
218250 if ( event . type !== "tool-call" || event . providerExecuted ) return
219251 needsContinuation = true
@@ -248,13 +280,17 @@ export const layer = Layer.effect(
248280 return yield * Effect . uninterruptibleMask ( ( restore ) =>
249281 Effect . gen ( function * ( ) {
250282 const stream = yield * restore ( providerStream ) . pipe ( Effect . exit )
251- let llmFailure : LLMError | undefined
252- if ( stream . _tag === "Failure" ) {
253- for ( const reason of stream . cause . reasons ) {
254- if ( ! Cause . isFailReason ( reason ) ) continue
255- if ( reason . error instanceof LLMError ) llmFailure = reason . error
256- }
257- }
283+ const failure =
284+ stream . _tag === "Failure" ? Option . getOrUndefined ( Cause . findErrorOption ( stream . cause ) ) : undefined
285+ if (
286+ recoverOverflow &&
287+ ! publisher . hasAssistantStarted ( ) &&
288+ isContextOverflowFailure ( overflowFailure ?? failure ) &&
289+ ( yield * restore ( recoverOverflow ( { sessionID : session . id , entries, model, request } ) ) )
290+ )
291+ return yield * Effect . die ( continueAfterOverflowCompaction )
292+ if ( overflowFailure ) yield * publish ( overflowFailure )
293+ const llmFailure = failure instanceof LLMError ? failure : undefined
258294 if ( llmFailure && ! publisher . hasProviderError ( ) ) {
259295 yield * withPublication ( publisher . failUnsettledTools ( "Provider did not return a tool result" , true ) )
260296 yield * withPublication (
@@ -290,17 +326,38 @@ export const layer = Layer.effect(
290326 } ) ,
291327 )
292328 } , Effect . scoped )
293- const runTurn : (
329+ type RunTurn = (
294330 sessionID : SessionSchema . ID ,
295331 promotion : SessionInput . Delivery | undefined ,
296- ) => Effect . Effect < boolean , RunError > = ( sessionID , promotion ) =>
297- runTurnAttempt ( sessionID , promotion ) . pipe (
298- Effect . catchDefect ( ( defect ) =>
299- defect instanceof RetryTurn
300- ? Effect . yieldNow . pipe ( Effect . andThen ( runTurn ( sessionID , defect . promotion ) ) )
301- : Effect . die ( defect ) ,
332+ ) => Effect . Effect < boolean , RunError >
333+
334+ const runAfterOverflowCompaction : RunTurn = Effect . fnUntraced ( function * ( sessionID , promotion ) {
335+ return yield * runTurnAttempt ( sessionID , promotion ) . pipe (
336+ Effect . catchDefect (
337+ Effect . fnUntraced ( function * ( defect ) {
338+ if ( ! ( defect instanceof TurnTransitionError ) ) return yield * Effect . die ( defect )
339+ if ( defect . transition . _tag === "ContinueAfterOverflowCompaction" )
340+ return yield * Effect . die ( "Post-compaction provider attempt cannot recover another overflow" )
341+ yield * Effect . yieldNow
342+ return yield * runAfterOverflowCompaction ( sessionID , defect . transition . promotion )
343+ } ) ,
344+ ) ,
345+ )
346+ } )
347+
348+ const runTurn : RunTurn = Effect . fnUntraced ( function * ( sessionID , promotion ) {
349+ return yield * runTurnAttempt ( sessionID , promotion , compaction . compactAfterOverflow ) . pipe (
350+ Effect . catchDefect (
351+ Effect . fnUntraced ( function * ( defect ) {
352+ if ( ! ( defect instanceof TurnTransitionError ) ) return yield * Effect . die ( defect )
353+ yield * Effect . yieldNow
354+ if ( defect . transition . _tag === "ContinueAfterOverflowCompaction" )
355+ return yield * runAfterOverflowCompaction ( sessionID , undefined )
356+ return yield * runTurn ( sessionID , defect . transition . promotion )
357+ } ) ,
302358 ) ,
303359 )
360+ } )
304361
305362 const run = Effect . fn ( "SessionRunner.run" ) ( function * ( input : {
306363 readonly sessionID : SessionSchema . ID
0 commit comments