authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-03 02:34:54-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 02:34:54-03:00
log8283b481a34f5cc6f984f290e7cb93cc35b4dc56
tree3dd9f9417e9a06c733603de5c83586a331758caa
parentddcd3102827f520e3dff53b902111744c0ba2ced
signature Signed by PGP key B5690EEEBB952194

fix(server): Claude V2 turns start on Windows with the default binary path (#15021)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 90 insertions(+), 57 deletions(-)

apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+84-56
......@@ -45,6 +45,8 @@ import * as Scope from "effect/Scope";
4545import * as Stream from "effect/Stream";
4646import { Tool } from "effect/unstable/ai";
4747import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction";
48import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
49import { SpawnExecutableResolution } from "@t3tools/shared/shell";
4850
4951import { attachmentRelativePath } from "../../attachmentStore.ts";
5052import * as ServerConfig from "../../config.ts";
......@@ -55,6 +57,7 @@ import { ProjectToolkit } from "../../mcp/toolkits/project/tools.ts";
5557import { WorktreeToolkit } from "../../mcp/toolkits/worktree/tools.ts";
5658import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts";
5759import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts";
60import { ClaudeExecutableFileCheck } from "../../provider/Drivers/ClaudeExecutable.ts";
5861import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";
5962import {
6063 ProviderAdapterV2RuntimePolicy,
......@@ -1064,72 +1067,97 @@ describe("ClaudeAdapterV2 approval cancellation", () => {
10641067 );
10651068});
10661069
1070// Opens a session with the given configured binary path, runs one turn, and
1071// returns the executable paths the SDK was asked to spawn.
1072const captureSdkExecutablePaths = Effect.fn("captureSdkExecutablePaths")(function* (
1073 binaryPath: string,
1074) {
1075 const executablePaths: Array<string | undefined> = [];
1076 const adapter = yield* ClaudeAdapterV2.createClaudeAdapterV2(
1077 {
1078 instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
1079 displayName: undefined,
1080 environment: [],
1081 enabled: true,
1082 config: { ...DEFAULT_CLAUDE_SETTINGS, binaryPath },
1083 },
1084 {},
1085 ).pipe(
1086 Effect.provide(
1087 ServerConfig.layerTest(process.cwd(), {
1088 prefix: "t3-claude-binary-path-",
1089 }),
1090 ),
1091 Effect.provideService(ClaudeAdapterV2.ClaudeAgentSdkQueryRunner, {
1092 allocateSessionId: Effect.succeed("native-thread-claude-binary-path"),
1093 open: (input) =>
1094 Effect.sync(() => {
1095 executablePaths.push(input.options.pathToClaudeCodeExecutable);
1096 return {
1097 messages: Stream.never,
1098 offer: () => Effect.void,
1099 setModel: () => Effect.void,
1100 interrupt: Effect.void,
1101 close: Effect.void,
1102 };
1103 }),
1104 forkSession: () => Effect.die("unused"),
1105 subagentLaunchToolUseId: () => Effect.succeed(null),
1106 assertComplete: Effect.void,
1107 }),
1108 );
1109 const threadId = ThreadId.make("thread-claude-binary-path");
1110 const runtime = yield* adapter.openSession({
1111 threadId,
1112 providerSessionId: ProviderSessionId.make("provider-session-claude-binary-path"),
1113 modelSelection: CLAUDE_TEST_MODEL_SELECTION,
1114 runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
1115 });
1116 const providerThread = yield* runtime.ensureThread({
1117 threadId,
1118 modelSelection: CLAUDE_TEST_MODEL_SELECTION,
1119 runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
1120 });
1121 yield* runtime.startTurn(
1122 makeClaudeTestTurnInput({
1123 threadId,
1124 providerThread,
1125 now: yield* DateTime.now,
1126 attemptId: RunAttemptId.make("attempt-claude-binary-path"),
1127 text: "hello",
1128 attachments: [],
1129 }),
1130 );
1131 return executablePaths;
1132});
1133
10671134describe("ClaudeAdapterV2 executable path", () => {
10681135 it.effect("expands ~ in the configured binary path for the SDK", () =>
10691136 Effect.scoped(
10701137 Effect.gen(function* () {
10711138 const path = yield* Path.Path;
1072 const executablePaths: Array<string | undefined> = [];
1073 const adapter = yield* ClaudeAdapterV2.createClaudeAdapterV2(
1074 {
1075 instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
1076 displayName: undefined,
1077 environment: [],
1078 enabled: true,
1079 config: { ...DEFAULT_CLAUDE_SETTINGS, binaryPath: "~/bin/claude" },
1080 },
1081 {},
1082 ).pipe(
1083 Effect.provide(
1084 ServerConfig.layerTest(process.cwd(), {
1085 prefix: "t3-claude-binary-home-",
1086 }),
1087 ),
1088 Effect.provideService(ClaudeAdapterV2.ClaudeAgentSdkQueryRunner, {
1089 allocateSessionId: Effect.succeed("native-thread-claude-binary-home"),
1090 open: (input) =>
1091 Effect.sync(() => {
1092 executablePaths.push(input.options.pathToClaudeCodeExecutable);
1093 return {
1094 messages: Stream.never,
1095 offer: () => Effect.void,
1096 setModel: () => Effect.void,
1097 interrupt: Effect.void,
1098 close: Effect.void,
1099 };
1100 }),
1101 forkSession: () => Effect.die("unused"),
1102 subagentLaunchToolUseId: () => Effect.succeed(null),
1103 assertComplete: Effect.void,
1104 }),
1105 );
1106 const threadId = ThreadId.make("thread-claude-binary-home");
1107 const runtime = yield* adapter.openSession({
1108 threadId,
1109 providerSessionId: ProviderSessionId.make("provider-session-claude-binary-home"),
1110 modelSelection: CLAUDE_TEST_MODEL_SELECTION,
1111 runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
1112 });
1113 const providerThread = yield* runtime.ensureThread({
1114 threadId,
1115 modelSelection: CLAUDE_TEST_MODEL_SELECTION,
1116 runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
1117 });
1118 yield* runtime.startTurn(
1119 makeClaudeTestTurnInput({
1120 threadId,
1121 providerThread,
1122 now: yield* DateTime.now,
1123 attemptId: RunAttemptId.make("attempt-claude-binary-home"),
1124 text: "hello",
1125 attachments: [],
1126 }),
1127 );
1139 const executablePaths = yield* captureSdkExecutablePaths("~/bin/claude");
11281140
11291141 assert.deepEqual(executablePaths, [path.join(NodeOS.homedir(), "bin", "claude")]);
11301142 }),
11311143 ).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
11321144 );
1145
1146 it.effect("follows a bare claude on Windows to the npm package executable", () =>
1147 Effect.scoped(
1148 Effect.gen(function* () {
1149 const npmDir = "C:\\Users\\dev\\AppData\\Roaming\\npm";
1150 const packageExe = `${npmDir}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`;
1151 const executablePaths = yield* captureSdkExecutablePaths("claude").pipe(
1152 Effect.provideService(HostProcessPlatform, "win32"),
1153 Effect.provideService(SpawnExecutableResolution, () => `${npmDir}\\claude.cmd`),
1154 Effect.provideService(ClaudeExecutableFileCheck, (filePath) => filePath === packageExe),
1155 );
1156
1157 assert.deepEqual(executablePaths, [packageExe]);
1158 }),
1159 ).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
1160 );
11331161});
11341162
11351163describe("ClaudeAdapterV2 resume compaction", () => {
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+6-1
......@@ -87,6 +87,7 @@ import * as Schema from "effect/Schema";
8787import * as Stream from "effect/Stream";
8888
8989import { resolveAttachmentPath } from "../../attachmentStore.ts";
90import { resolveClaudeSdkExecutablePath } from "../../provider/Drivers/ClaudeExecutable.ts";
9091import { planClaudeSkillDispatch } from "../../provider/Drivers/ClaudeSkillDispatch.ts";
9192import { discoverClaudeSkills } from "../../provider/Drivers/ClaudeSkills.ts";
9293import { compileClaudeModelSelection } from "../../claudeModelOptions.ts";
......@@ -7666,9 +7667,13 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
76667667 const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment);
76677668 const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
76687669 const path = yield* Path.Path;
7670 const binaryPath = yield* resolveClaudeSdkExecutablePath(
7671 expandHomePath(config.binaryPath),
7672 claudeEnvironment,
7673 );
76697674 return makeClaudeAdapterV2({
76707675 instanceId,
7671 settings: { ...config, enabled, binaryPath: expandHomePath(config.binaryPath) },
7676 settings: { ...config, enabled, binaryPath },
76727677 environment: claudeEnvironment,
76737678 attachmentsDir: serverConfig.attachmentsDir,
76747679 fileSystem,