Skip to content

Commit e409cf5

Browse files
committed
fix: ensure Claude child process is aborted on timeout by moving AbortController outside Promise
Move the AbortController creation outside the Effect.tryPromise async function and use Effect.ensuring to guarantee abort() is called when the Effect completes or is interrupted (e.g. by timeoutOption). This prevents leaked child processes when initializationResult() hangs indefinitely.
1 parent 151d9aa commit e409cf5

File tree

1 file changed

+22
-20
lines changed

1 file changed

+22
-20
lines changed

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -324,34 +324,36 @@ const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000;
324324
* This is used as a fallback when `claude auth status` does not include
325325
* subscription type information.
326326
*/
327-
const probeClaudeCapabilities = (binaryPath: string) =>
328-
Effect.tryPromise(async () => {
329-
const abort = new AbortController();
330-
try {
331-
const q = claudeQuery({
332-
prompt: ".",
333-
options: {
334-
pathToClaudeCodeExecutable: binaryPath,
335-
abortController: abort,
336-
maxTurns: 0,
337-
settingSources: [],
338-
allowedTools: [],
339-
stderr: () => {},
340-
},
341-
});
342-
const init = await q.initializationResult();
343-
return { subscriptionType: init.account?.subscriptionType };
344-
} finally {
345-
if (!abort.signal.aborted) abort.abort();
346-
}
327+
const probeClaudeCapabilities = (binaryPath: string) => {
328+
const abort = new AbortController();
329+
return Effect.tryPromise(async () => {
330+
const q = claudeQuery({
331+
prompt: ".",
332+
options: {
333+
pathToClaudeCodeExecutable: binaryPath,
334+
abortController: abort,
335+
maxTurns: 0,
336+
settingSources: [],
337+
allowedTools: [],
338+
stderr: () => {},
339+
},
340+
});
341+
const init = await q.initializationResult();
342+
return { subscriptionType: init.account?.subscriptionType };
347343
}).pipe(
344+
Effect.ensuring(
345+
Effect.sync(() => {
346+
if (!abort.signal.aborted) abort.abort();
347+
}),
348+
),
348349
Effect.timeoutOption(CAPABILITIES_PROBE_TIMEOUT_MS),
349350
Effect.result,
350351
Effect.map((result) => {
351352
if (Result.isFailure(result)) return undefined;
352353
return Option.isSome(result.success) ? result.success.value : undefined;
353354
}),
354355
);
356+
};
355357

356358
const runClaudeCommand = (args: ReadonlyArray<string>) =>
357359
Effect.gen(function* () {

0 commit comments

Comments
 (0)