| ... | @@ -194,6 +194,8 @@ function toSessionPermissionUpdates( | ... | @@ -194,6 +194,8 @@ function toSessionPermissionUpdates( |
| 194 | interface PendingUserInput { | 194 | interface PendingUserInput { |
| 195 | readonly questions: ReadonlyArray<UserInputQuestion>; | 195 | readonly questions: ReadonlyArray<UserInputQuestion>; |
| 196 | readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; | 196 | readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; |
| | 197 | /** Unparks the waiting handler as cancelled. Session teardown must run it. */ |
| | 198 | readonly cancel: Effect.Effect<void>; |
| 197 | } | 199 | } |
| 198 | | 200 | |
| 199 | interface ToolInFlight { | 201 | interface ToolInFlight { |
| ... | @@ -3656,6 +3658,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3656,6 +3658,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3656 | } | 3658 | } |
| 3657 | context.pendingApprovals.clear(); | 3659 | context.pendingApprovals.clear(); |
| 3658 | | 3660 | |
| | 3661 | // Same reason as the approvals above: a request nobody can answer any more |
| | 3662 | // must not stay open, or the thread can never be settled. |
| | 3663 | for (const pending of [...context.pendingUserInputs.values()]) { |
| | 3664 | yield* pending.cancel; |
| | 3665 | } |
| | 3666 | |
| 3659 | if (context.turnState) { | 3667 | if (context.turnState) { |
| 3660 | yield* completeTurn(context, "interrupted", "Session stopped."); | 3668 | yield* completeTurn(context, "interrupted", "Session stopped."); |
| 3661 | } | 3669 | } |
| ... | @@ -3837,9 +3845,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3837,9 +3845,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3837 | | 3845 | |
| 3838 | const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>(); | 3846 | const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>(); |
| 3839 | let aborted = false; | 3847 | let aborted = false; |
| | 3848 | const settleAsAborted = Effect.suspend(() => { |
| | 3849 | if (!pendingUserInputs.has(requestId)) { |
| | 3850 | return Effect.void; |
| | 3851 | } |
| | 3852 | aborted = true; |
| | 3853 | pendingUserInputs.delete(requestId); |
| | 3854 | return Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers).pipe( |
| | 3855 | Effect.ignore, |
| | 3856 | ); |
| | 3857 | }); |
| 3840 | const pendingInput: PendingUserInput = { | 3858 | const pendingInput: PendingUserInput = { |
| 3841 | questions, | 3859 | questions, |
| 3842 | answers: answersDeferred, | 3860 | answers: answersDeferred, |
| | 3861 | cancel: settleAsAborted, |
| 3843 | }; | 3862 | }; |
| 3844 | | 3863 | |
| 3845 | // Emit user-input.requested so the UI can present the questions. | 3864 | // Emit user-input.requested so the UI can present the questions. |
| ... | @@ -3874,12 +3893,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3874,12 +3893,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3874 | | 3893 | |
| 3875 | // Handle abort (e.g. turn interrupted while waiting for user input). | 3894 | // Handle abort (e.g. turn interrupted while waiting for user input). |
| 3876 | const onAbort = () => { | 3895 | const onAbort = () => { |
| 3877 | if (!pendingUserInputs.has(requestId)) { | 3896 | runFork(settleAsAborted); |
| 3878 | return; | | |
| 3879 | } | | |
| 3880 | aborted = true; | | |
| 3881 | pendingUserInputs.delete(requestId); | | |
| 3882 | runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers)); | | |
| 3883 | }; | 3897 | }; |
| 3884 | callbackOptions.signal.addEventListener("abort", onAbort, { | 3898 | callbackOptions.signal.addEventListener("abort", onAbort, { |
| 3885 | once: true, | 3899 | once: true, |