@@ -51,7 +51,9 @@ import {
5151 DateTime ,
5252 Deferred ,
5353 Effect ,
54+ Exit ,
5455 FileSystem ,
56+ Fiber ,
5557 Layer ,
5658 Queue ,
5759 Random ,
@@ -141,6 +143,7 @@ interface ClaudeSessionContext {
141143 session : ProviderSession ;
142144 readonly promptQueue : Queue . Queue < PromptQueueItem > ;
143145 readonly query : ClaudeQueryRuntime ;
146+ streamFiber : Fiber . Fiber < void , Error > | undefined ;
144147 readonly startedAt : string ;
145148 readonly basePermissionMode : PermissionMode | undefined ;
146149 resumeSessionId : string | undefined ;
@@ -189,6 +192,47 @@ function toMessage(cause: unknown, fallback: string): string {
189192 return fallback ;
190193}
191194
195+ function toError ( cause : unknown , fallback : string ) : Error {
196+ return cause instanceof Error ? cause : new Error ( toMessage ( cause , fallback ) ) ;
197+ }
198+
199+ function normalizeClaudeStreamMessages ( cause : Cause . Cause < Error > ) : ReadonlyArray < string > {
200+ const errors = Cause . prettyErrors ( cause )
201+ . map ( ( error ) => error . message . trim ( ) )
202+ . filter ( ( message ) => message . length > 0 ) ;
203+ if ( errors . length > 0 ) {
204+ return errors ;
205+ }
206+
207+ const squashed = toMessage ( Cause . squash ( cause ) , "" ) . trim ( ) ;
208+ return squashed . length > 0 ? [ squashed ] : [ ] ;
209+ }
210+
211+ function isClaudeInterruptedMessage ( message : string ) : boolean {
212+ const normalized = message . toLowerCase ( ) ;
213+ return (
214+ normalized . includes ( "all fibers interrupted without error" ) ||
215+ normalized . includes ( "request was aborted" ) ||
216+ normalized . includes ( "interrupted by user" )
217+ ) ;
218+ }
219+
220+ function isClaudeInterruptedCause ( cause : Cause . Cause < Error > ) : boolean {
221+ return (
222+ Cause . hasInterruptsOnly ( cause ) ||
223+ normalizeClaudeStreamMessages ( cause ) . some ( isClaudeInterruptedMessage )
224+ ) ;
225+ }
226+
227+ function messageFromClaudeStreamCause ( cause : Cause . Cause < Error > , fallback : string ) : string {
228+ return normalizeClaudeStreamMessages ( cause ) [ 0 ] ?? fallback ;
229+ }
230+
231+ function interruptionMessageFromClaudeCause ( cause : Cause . Cause < Error > ) : string {
232+ const message = messageFromClaudeStreamCause ( cause , "Claude runtime interrupted." ) ;
233+ return isClaudeInterruptedMessage ( message ) ? "Claude runtime interrupted." : message ;
234+ }
235+
192236function resultErrorsText ( result : SDKResultMessage ) : string {
193237 return "errors" in result && Array . isArray ( result . errors )
194238 ? result . errors . join ( " " ) . toLowerCase ( )
@@ -2045,21 +2089,48 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
20452089 }
20462090 } ) ;
20472091
2048- const runSdkStream = ( context : ClaudeSessionContext ) : Effect . Effect < void > =>
2049- Stream . fromAsyncIterable ( context . query , ( cause ) => cause ) . pipe (
2092+ const runSdkStream = ( context : ClaudeSessionContext ) : Effect . Effect < void , Error > =>
2093+ Stream . fromAsyncIterable ( context . query , ( cause ) =>
2094+ toError ( cause , "Claude runtime stream failed." ) ,
2095+ ) . pipe (
20502096 Stream . takeWhile ( ( ) => ! context . stopped ) ,
20512097 Stream . runForEach ( ( message ) => handleSdkMessage ( context , message ) ) ,
2052- Effect . catchCause ( ( cause ) =>
2053- Effect . gen ( function * ( ) {
2054- if ( Cause . hasInterruptsOnly ( cause ) || context . stopped ) {
2055- return ;
2098+ ) ;
2099+
2100+ const handleStreamExit = (
2101+ context : ClaudeSessionContext ,
2102+ exit : Exit . Exit < void , Error > ,
2103+ ) : Effect . Effect < void > =>
2104+ Effect . gen ( function * ( ) {
2105+ if ( context . stopped ) {
2106+ return ;
2107+ }
2108+
2109+ if ( Exit . isFailure ( exit ) ) {
2110+ if ( isClaudeInterruptedCause ( exit . cause ) ) {
2111+ if ( context . turnState ) {
2112+ yield * completeTurn (
2113+ context ,
2114+ "interrupted" ,
2115+ interruptionMessageFromClaudeCause ( exit . cause ) ,
2116+ ) ;
20562117 }
2057- const message = toMessage ( Cause . squash ( cause ) , "Claude runtime stream failed." ) ;
2058- yield * emitRuntimeError ( context , message , cause ) ;
2118+ } else {
2119+ const message = messageFromClaudeStreamCause (
2120+ exit . cause ,
2121+ "Claude runtime stream failed." ,
2122+ ) ;
2123+ yield * emitRuntimeError ( context , message , Cause . pretty ( exit . cause ) ) ;
20592124 yield * completeTurn ( context , "failed" , message ) ;
2060- } ) ,
2061- ) ,
2062- ) ;
2125+ }
2126+ } else if ( context . turnState ) {
2127+ yield * completeTurn ( context , "interrupted" , "Claude runtime stream ended." ) ;
2128+ }
2129+
2130+ yield * stopSessionInternal ( context , {
2131+ emitExitEvent : true ,
2132+ } ) ;
2133+ } ) ;
20632134
20642135 const stopSessionInternal = (
20652136 context : ClaudeSessionContext ,
@@ -2096,7 +2167,18 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
20962167
20972168 yield * Queue . shutdown ( context . promptQueue ) ;
20982169
2099- context . query . close ( ) ;
2170+ const streamFiber = context . streamFiber ;
2171+ context . streamFiber = undefined ;
2172+ if ( streamFiber && streamFiber . pollUnsafe ( ) === undefined ) {
2173+ yield * Fiber . interrupt ( streamFiber ) ;
2174+ }
2175+
2176+ // @effect -diagnostics-next-line tryCatchInEffectGen:off
2177+ try {
2178+ context . query . close ( ) ;
2179+ } catch ( cause ) {
2180+ yield * emitRuntimeError ( context , "Failed to close Claude runtime query." , cause ) ;
2181+ }
21002182
21012183 const updatedAt = yield * nowIso ;
21022184 context . session = {
@@ -2536,6 +2618,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
25362618 session,
25372619 promptQueue,
25382620 query : queryRuntime ,
2621+ streamFiber : undefined ,
25392622 startedAt,
25402623 basePermissionMode : permissionMode ,
25412624 resumeSessionId : sessionId ,
@@ -2597,7 +2680,17 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
25972680 providerRefs : { } ,
25982681 } ) ;
25992682
2600- Effect . runFork ( runSdkStream ( context ) ) ;
2683+ const streamFiber = Effect . runFork ( runSdkStream ( context ) ) ;
2684+ context . streamFiber = streamFiber ;
2685+ streamFiber . addObserver ( ( exit ) => {
2686+ if ( context . stopped ) {
2687+ return ;
2688+ }
2689+ if ( context . streamFiber === streamFiber ) {
2690+ context . streamFiber = undefined ;
2691+ }
2692+ Effect . runFork ( handleStreamExit ( context , exit ) ) ;
2693+ } ) ;
26012694
26022695 return {
26032696 ...session ,
0 commit comments