diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index e127701c7..78e64f382 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -2473,6 +2473,57 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect( + "restores default permission mode on sendTurn when interactionMode is chat for approval-required sessions", + () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeAgent", + runtimeMode: "approval-required", + }); + + yield* adapter.sendTurn({ + threadId: session.threadId, + input: "plan this", + interactionMode: "plan", + attachments: [], + }); + + const turnCompletedFiber = yield* Stream.filter( + adapter.streamEvents, + (event) => event.type === "turn.completed", + ).pipe(Stream.runHead, Effect.forkChild); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-approval-plan-restore", + uuid: "result-approval-plan", + } as unknown as SDKMessage); + + yield* Fiber.join(turnCompletedFiber); + + yield* adapter.sendTurn({ + threadId: session.threadId, + input: "now answer directly", + interactionMode: "chat", + attachments: [], + }); + + assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", "default"]); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }, + ); + it.effect("does not call setPermissionMode when interactionMode is absent", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 1f45b8569..4b6e4e1c0 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -157,7 +157,7 @@ interface ClaudeSessionContext { readonly query: ClaudeQueryRuntime; streamFiber: Fiber.Fiber | undefined; readonly startedAt: string; - readonly basePermissionMode: PermissionMode | undefined; + readonly basePermissionMode: PermissionMode; resumeSessionId: string | undefined; readonly pendingApprovals: Map; readonly pendingUserInputs: Map; @@ -2790,6 +2790,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { const permissionMode = toPermissionMode(providerOptions?.permissionMode) ?? (input.runtimeMode === "full-access" ? "bypassPermissions" : undefined); + const basePermissionMode = permissionMode ?? "default"; const settings = { ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), @@ -2859,7 +2860,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { query: queryRuntime, streamFiber: undefined, startedAt, - basePermissionMode: permissionMode, + basePermissionMode, resumeSessionId: sessionId, pendingApprovals, pendingUserInputs, @@ -2965,8 +2966,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { }); } else if (input.interactionMode === "chat" || input.interactionMode === "code") { yield* Effect.tryPromise({ - try: () => - context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"), + try: () => context.query.setPermissionMode(context.basePermissionMode), catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), }); }