From a0db1101b23f3823f4e7087232ca350fef5d4885 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sat, 23 May 2026 13:54:59 -0700 Subject: [PATCH] fix: refine model capabilities, composer settings, and runtime safety - Filter the auto approval policy to supported Claude models and define a safe fallback - Apply the default context size when patching composer settings on model change - Validate cached agent executables exist and are runnable to prevent opaque spawn errors - Enable the "max" effort setting for the Claude "sonnet" model in detection and probe - Use the sanitized process environment when initializing the PTY session environment --- .../components/providers/claude/index.tsx | 30 ++++++++++++++----- .../thread/ThreadComposerSection.tsx | 5 +--- src/supervisor/agents/binaryResolver.ts | 17 +++++++++-- src/supervisor/agents/claude/detection.ts | 2 +- src/supervisor/agents/claude/probe.ts | 2 +- .../runtime/threadSessionManager.ts | 2 +- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/providers/claude/index.tsx b/src/renderer/components/providers/claude/index.tsx index d0fd4ad6..7227e8a4 100644 --- a/src/renderer/components/providers/claude/index.tsx +++ b/src/renderer/components/providers/claude/index.tsx @@ -34,6 +34,26 @@ registerConflictResolverDefaults("claude", { registerComposerControls("claude", ({ capabilities, config, isDisabled, onConfigChange }) => { const isPlanMode = (config.mode ?? "agent") !== "agent"; + + // Auto mode is only supported for Sonnet 4.6+, Opus 4.6, and Opus 4.7. + // Filter it out for Haiku and other models that don't support it. + const AUTO_CAPABLE_MODELS = new Set(["sonnet", "claude-opus-4-6", "claude-opus-4-7"]); + const modelSupportsAuto = !config.model || AUTO_CAPABLE_MODELS.has(config.model); + const filteredPolicies = modelSupportsAuto + ? capabilities.approvalPolicies + : capabilities.approvalPolicies.filter((p) => p.id !== "auto"); + + const currentPolicy = + config.approvalPolicy ?? + capabilities.bypassPermissions?.approvalPolicy ?? + capabilities.approvalPolicies[0]?.id ?? + "default"; + // If the current policy is not available for this model, fall back to + // bypassPermissions since auto mode was the reason it was filtered. + const effectivePolicy = filteredPolicies.some((p) => p.id === currentPolicy) + ? currentPolicy + : "bypassPermissions"; + return [ ...(capabilities.modes.length === 2 ? [ @@ -44,17 +64,13 @@ registerComposerControls("claude", ({ capabilities, config, isDisabled, onConfig }), ] : []), - ...(capabilities.approvalPolicies.length > 0 + ...(filteredPolicies.length > 0 ? [ { iconKind: "permission" as const, - options: capabilities.approvalPolicies, + options: filteredPolicies, hideLabelOnWrap: true, - value: - config.approvalPolicy ?? - capabilities.bypassPermissions?.approvalPolicy ?? - capabilities.approvalPolicies[0]?.id ?? - "default", + value: effectivePolicy, isDisabled, onChange: (value: string) => onConfigChange({ approvalPolicy: value }), }, diff --git a/src/renderer/components/thread/ThreadComposerSection.tsx b/src/renderer/components/thread/ThreadComposerSection.tsx index 211aea9e..731ce005 100644 --- a/src/renderer/components/thread/ThreadComposerSection.tsx +++ b/src/renderer/components/thread/ThreadComposerSection.tsx @@ -163,14 +163,11 @@ function buildControls( ? nextEfforts.includes(effectiveConfig.effort) : true; const nextContextIds = filteredCaps.modelContextSizes?.[model]; - const contextValid = - !effectiveConfig.contextSize || - (nextContextIds ? nextContextIds.includes(effectiveConfig.contextSize) : false); const nextContextDefault = nextContextIds?.[0] ?? filteredCaps.defaultContextSize; onPatch({ model, ...(!effortValid && nextEfforts.length > 0 ? { effort: nextEfforts[0] } : {}), - ...(!contextValid && nextContextDefault ? { contextSize: nextContextDefault } : {}), + ...(nextContextDefault ? { contextSize: nextContextDefault } : {}), ...(filteredCaps.fastModels?.includes(model) ? {} : { fast: false }), ...(filteredCaps.thinkingModels?.includes(model) ? {} : { thinking: false }), }); diff --git a/src/supervisor/agents/binaryResolver.ts b/src/supervisor/agents/binaryResolver.ts index 60f9d4df..5a4c8656 100644 --- a/src/supervisor/agents/binaryResolver.ts +++ b/src/supervisor/agents/binaryResolver.ts @@ -1,3 +1,4 @@ +import { statSync } from "node:fs"; import type { ProjectLocation } from "@/shared/contracts"; import { getCachedExecutablePath, resolveExecutablePath, resolveWslExecutablePath } from "./base"; @@ -39,8 +40,20 @@ export function resolveAgentBinaryPath( return resolved; } // posix: piggy-back on the shared exec-path cache populated by - // primeExecutablePathCache during agent detection. - return getCachedExecutablePath(binary); + // primeExecutablePathCache during agent detection. The cached path may come + // from a temporary login shell (e.g. fnm multishell) that has since been + // cleaned up — verify the file still exists so node-pty doesn't get a stale + // absolute path and fail with opaque "posix_spawnp failed". + const cached = getCachedExecutablePath(binary); + if (cached) { + try { + const s = statSync(cached); + if (!s.isFile() || (s.mode & 0o111) === 0) return undefined; + } catch { + return undefined; + } + } + return cached; } /** diff --git a/src/supervisor/agents/claude/detection.ts b/src/supervisor/agents/claude/detection.ts index c4a7d661..da33beec 100644 --- a/src/supervisor/agents/claude/detection.ts +++ b/src/supervisor/agents/claude/detection.ts @@ -26,7 +26,7 @@ export const claudeCapabilities: AgentCapability = { modelEfforts: { "claude-opus-4-6": ["low", "medium", "high", "max"], haiku: [], - sonnet: ["low", "medium", "high"], + sonnet: ["low", "medium", "high", "max"], }, contextSizes: [ { id: "200k", label: "200k" }, diff --git a/src/supervisor/agents/claude/probe.ts b/src/supervisor/agents/claude/probe.ts index bdcd0fff..03d14c94 100644 --- a/src/supervisor/agents/claude/probe.ts +++ b/src/supervisor/agents/claude/probe.ts @@ -20,7 +20,7 @@ const BUILTIN_MODELS: AgentCapability["models"] = [ const BUILTIN_MODEL_EFFORTS: AgentCapability["modelEfforts"] = { "claude-opus-4-6": ["low", "medium", "high", "max"], haiku: [], - sonnet: ["low", "medium", "high"], + sonnet: ["low", "medium", "high", "max"], }; function parseSemverTriplet(version: string): [number, number, number] | null { diff --git a/src/supervisor/runtime/threadSessionManager.ts b/src/supervisor/runtime/threadSessionManager.ts index 4c2a8450..e61d41f8 100644 --- a/src/supervisor/runtime/threadSessionManager.ts +++ b/src/supervisor/runtime/threadSessionManager.ts @@ -1365,7 +1365,7 @@ export class ThreadSessionManager { let pty; if (command) { const ptyEnv = { - ...process.env, + ...sanitizedProcessEnv, ...(command.env ?? {}), ...agentEnv, ...terminalEnv,