From 9c495d828eb49f32321e4dea3093e3330bce8e92 Mon Sep 17 00:00:00 2001 From: Yaacov Date: Sun, 18 Oct 2026 21:33:21 +0300 Subject: [PATCH 1/3] fix(server): give the Claude version check 20 seconds The native Claude CLI can take several seconds just to answer `--version` on Windows (2.2-3.5 s measured on an idle machine). The shared 4-second probe deadline therefore reported a healthy install as "installed but failed to run" whenever the machine was busy. The check now allows 20 seconds, as the Codex app-server status check does. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/ClaudeProvider.ts | 5 +- .../src/provider/ProviderRegistry.test.ts | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 6b7907dd5c4f..0387d5a99368 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -193,6 +193,9 @@ function claudeAuthStatus( // Amazon Bedrock initializes far slower than first-party auth: the SDK boots the Bedrock // backend and runs the `awsAuthRefresh` credential hook before returning account info. const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; +// The native Claude CLI can take several seconds just to answer `--version` on Windows, +// so the shared 4 s probe deadline reported a healthy install as failing on a busy machine. +const CLAUDE_VERSION_CHECK_TIMEOUT_MS = 20_000; export const CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES = [ "user", @@ -507,7 +510,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( ["--version"], resolvedEnvironment, cwd, - ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result); + ).pipe(Effect.timeoutOption(CLAUDE_VERSION_CHECK_TIMEOUT_MS), Effect.result); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 6ce4f08d8596..07724f150fd8 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -3121,6 +3121,53 @@ it.layer( ), ); + it.effect("waits for a healthy claude CLI that is slow to answer --version", () => + Effect.gen(function* () { + const statusFiber = yield* checkClaudeProviderStatus( + defaultClaudeSettings, + claudeCapabilities(), + ).pipe( + Effect.provide( + Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => { + const joined = (command as unknown as { args: ReadonlyArray }).args.join( + " ", + ); + if (joined !== "--version") throw new Error(`Unexpected args: ${joined}`); + // The native CLI can take several seconds to start on a busy Windows host. + const answer = Effect.sleep("19 seconds"); + return Effect.succeed( + ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(1), + exitCode: answer.pipe(Effect.as(ChildProcessSpawner.ExitCode(0))), + isRunning: Effect.succeed(true), + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + stdin: Sink.drain, + stdout: Stream.fromEffect(answer.pipe(Effect.as(encoder.encode("1.0.0 +")))), + stderr: Stream.empty, + all: Stream.empty, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }), + ); + }), + ), + ), + Effect.forkChild, + ); + for (let second = 0; second < 20; second++) { + yield* Effect.yieldNow; + yield* TestClock.adjust("1 second"); + } + const status = yield* Fiber.join(statusFiber); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.version, "1.0.0"); + }), + ); + it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () => Effect.gen(function* () { // Bedrock authenticates via external AWS credentials, so the SDK init From 0235b99099794df4177ab528a7f5d986129f7528 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 20:36:37 +0000 Subject: [PATCH 2/3] test(server): cover slow and hung Claude version probes --- .../src/provider/ProviderRegistry.test.ts | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 121cc2968947..6ba9ac109441 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -3500,8 +3500,24 @@ it.layer( ), ); - it.effect("waits for a healthy claude CLI that is slow to answer --version", () => + it.effect.each([ + { + name: "accepts a healthy claude --version response after 19 seconds", + answer: Effect.sleep("19 seconds"), + elapsed: 19_000, + status: "ready", + version: "1.0.0", + }, + { + name: "bounds a hung claude --version probe", + answer: Effect.never, + elapsed: 21_000, + status: "error", + version: null, + }, + ])("$name", ({ answer, elapsed, status: expectedStatus, version }) => Effect.gen(function* () { + const started = yield* Deferred.make(); const statusFiber = yield* checkClaudeProviderStatus( defaultClaudeSettings, claudeCapabilities(), @@ -3510,26 +3526,20 @@ it.layer( Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { - const joined = (command as unknown as { args: ReadonlyArray }).args.join( - " ", + assert.deepStrictEqual( + (command as unknown as { args: ReadonlyArray }).args, + ["--version"], ); - if (joined !== "--version") throw new Error(`Unexpected args: ${joined}`); - // The native CLI can take several seconds to start on a busy Windows host. - const answer = Effect.sleep("19 seconds"); return Effect.succeed( ChildProcessSpawner.makeHandle({ - pid: ChildProcessSpawner.ProcessId(1), + ...mockHandle({ stdout: "", stderr: "", code: 0 }), exitCode: answer.pipe(Effect.as(ChildProcessSpawner.ExitCode(0))), - isRunning: Effect.succeed(true), - kill: () => Effect.void, - unref: Effect.succeed(Effect.void), - stdin: Sink.drain, - stdout: Stream.fromEffect(answer.pipe(Effect.as(encoder.encode("1.0.0 -")))), - stderr: Stream.empty, - all: Stream.empty, - getInputFd: () => Sink.drain, - getOutputFd: () => Stream.empty, + stdout: Stream.fromEffect( + Deferred.succeed(started, undefined).pipe( + Effect.andThen(answer), + Effect.as(encoder.encode("1.0.0\n")), + ), + ), }), ); }), @@ -3537,13 +3547,16 @@ it.layer( ), Effect.forkChild, ); - for (let second = 0; second < 20; second++) { - yield* Effect.yieldNow; - yield* TestClock.adjust("1 second"); - } + yield* Deferred.await(started); + yield* TestClock.adjust(elapsed); + const status = yield* Fiber.join(statusFiber); - assert.strictEqual(status.status, "ready"); - assert.strictEqual(status.version, "1.0.0"); + assert.strictEqual(status.status, expectedStatus); + assert.strictEqual(status.installed, true); + assert.strictEqual(status.version, version); + if (expectedStatus === "error") { + assert.include(status.message, "Timed out while running command."); + } }), ); From 0a17c749f102ce5df3482547ce6f79526dbee964 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 20:38:31 +0000 Subject: [PATCH 3/3] refactor(server): clarify Claude version probe deadline --- apps/server/src/provider/ClaudeProvider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 4e5fee47fb6a..cc6975683108 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -194,8 +194,7 @@ function claudeAuthStatus( // Amazon Bedrock initializes far slower than first-party auth: the SDK boots the Bedrock // backend and runs the `awsAuthRefresh` credential hook before returning account info. const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; -// The native Claude CLI can take several seconds just to answer `--version` on Windows, -// so the shared 4 s probe deadline reported a healthy install as failing on a busy machine. +// Native CLI startup on Windows can exceed the shared probe deadline. const CLAUDE_VERSION_CHECK_TIMEOUT_MS = 20_000; export const CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES = [