Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,48 @@ describe("ClaudeAdapterV2 background wake turns", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, IdAllocator.layer))),
);

it.effect("reuses the CLI permission mode when auto falls back to default", () =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
const now = yield* DateTime.now;
const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
...CLAUDE_TEST_RUNTIME_POLICY,
runtimeMode: "auto",
});
const turnInput = makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now,
attemptId: RunAttemptId.make("attempt-claude-auto-fallback-1"),
text: "Hello.",
attachments: [],
runtimePolicy,
});
yield* harness.runtime.startTurn(turnInput);
const openedOptions = harness.getOpenedOptions();
assert.equal(openedOptions?.permissionMode, "auto");
yield* harness.offerAndWait(claudeSdkFrame({ ...wakeTurnInit, permissionMode: "default" }));
yield* Queue.offer(harness.sdkMessages, turnOneResult);
yield* Queue.take(harness.terminalReceipts);

yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now,
attemptId: RunAttemptId.make("attempt-claude-auto-fallback-2"),
providerTurnOrdinal: 2,
text: "Continue.",
attachments: [],
runtimePolicy,
}),
);
assert.strictEqual(harness.getOpenedOptions(), openedOptions);
assert.lengthOf(harness.offeredMessages, 2);
assert.deepEqual(harness.permissionModeChanges, []);
}).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
);

it.effect("preserves typed Claude plans and todos through generic tool completion", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down Expand Up @@ -2998,6 +3040,9 @@ describe("ClaudeAdapterV2 background wake turns", () => {
harness.sdkMessages,
toolResults("00000000-0000-4000-8000-000000000502", ["tool-todo-1"]),
);
yield* harness.offerAndWait(
claudeSdkFrame({ ...wakeTurnInit, permissionMode: "bypassPermissions" }),
);
// Claude entered plan mode on its own (EnterPlanMode).
yield* Queue.offer(
harness.sdkMessages,
Expand All @@ -3010,6 +3055,13 @@ describe("ClaudeAdapterV2 background wake turns", () => {
session_id: WAKE_NATIVE_SESSION,
}),
);
yield* harness.offerAndWait(
claudeSdkFrame({
...wakeTurnInit,
uuid: "00000000-0000-4000-8000-000000000509",
permissionMode: "plan",
}),
);
yield* Queue.offer(
harness.sdkMessages,
makeResultFrame({
Expand Down
22 changes: 17 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2739,10 +2739,12 @@ interface ClaudeLiveQueryContext {
// uuid before any echo, so it echoes, but a resume's own turns can still
// run ahead of that prompt.
promptEchoMode: "unknown" | "acknowledged" | "early" | "result_only";
// The mode this process was opened in, and the mode the CLI last reported
// (init and status frames). Claude changes the latter itself through
// EnterPlanMode.
readonly openedPermissionMode: PermissionMode;
// The mode this process opened in (first init frame; the CLI falls back
// from modes the model lacks, such as Auto on Haiku), and the mode the CLI
// last reported (init and status frames). Claude changes the latter itself
// through EnterPlanMode.
openedPermissionMode: PermissionMode;
receivedInit: boolean;
permissionMode: PermissionMode;
// Stop, rollback or fork is closing this process; its work is ending.
stopping: boolean;
Expand Down Expand Up @@ -6929,7 +6931,7 @@ export function makeClaudeAdapterV2(
) {
// Claude can switch its own mode mid-session (EnterPlanMode), and
// a denied ExitPlanMode leaves it there. Put the live process back
// in the thread's mode before the next prompt.
// in its initial mode before the next prompt.
if (existing.permissionMode !== existing.openedPermissionMode) {
yield* existing.query.setPermissionMode(existing.openedPermissionMode);
existing.permissionMode = existing.openedPermissionMode;
Expand Down Expand Up @@ -7049,6 +7051,7 @@ export function makeClaudeAdapterV2(
closed,
promptEchoMode: "unknown",
openedPermissionMode: queryOptions.permissionMode,
receivedInit: false,
permissionMode: queryOptions.permissionMode,
stopping: false,
subagentsFromEarlierProcesses: new Set(
Expand All @@ -7060,6 +7063,15 @@ export function makeClaudeAdapterV2(
yield* Ref.set(queryContext, context);
yield* querySession.messages.pipe(
Stream.runForEach((message) => {
if (
message.type === "system" &&
message.subtype === "init" &&
!context.receivedInit
) {
context.openedPermissionMode =
message.permissionMode ?? context.openedPermissionMode;
context.receivedInit = true;
}
if (
message.type === "system" &&
(message.subtype === "init" || message.subtype === "status") &&
Expand Down
Loading