Fix: zellij process accumulation caused by uncached interactive shell binary lookups#399
Open
NeckBeardPrince wants to merge 3 commits intosteipete:mainfrom
Open
Conversation
Each refresh cycle, resolveBinary() fell through to interactive login shell spawns (-l -i -c) for any binary not found on the standard PATH. Users with zellij configured to auto-start in their shell init files (~/.config/fish/config.fish, ~/.zshrc, etc.) saw a new zellij server daemon created on every spawn — resulting in 180+ persistent processes accumulating under CodexBar in Activity Monitor, each consuming 30–74 MB of RAM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 09bd4e7f9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Swift does not allow @main in a file that also contains top-level executable code. Removing @main and replacing static func main() with static func run(), called explicitly at the top level, resolves the build error: 'main' attribute cannot be used in a module that contains top-level code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues raised in code review: 1. Cache bypassed for non-default closures (P1) The cache was keyed only by binary name, so a call with custom commandV/aliasResolver closures (e.g. in tests) could receive a result cached by a prior call with different lookup logic. Added useShellCache: Bool to resolveBinary — the four public resolve* functions pass true (default closures); any caller with custom closures passes false and bypasses the cache entirely. 2. Stale cached path not retried (P2) A cached path that is no longer executable (CLI uninstalled/moved) caused step 4 to return nil permanently for the session with no retry. Now, when a cached path fails the isExecutableFile check, the entry is invalidated and the shell lookups run again. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Disclaimer by a real human
This was 100% done by Claude. I've never touched Swift in my life, but I love this app and wanted to try and fix it so that I can use it without 180+ zellij processes starting. I did test this build, and it's running locally without any issue.
Changes
This PR contains two independent fixes found during local testing.
Fix 1: zellij process accumulation (main fix)
Problem
Users with zellij configured to auto-start in their shell init files (e.g.
~/.config/fish/config.fish,~/.zshrc) see hundreds of zellij server processes accumulate under CodexBar in Activity Monitor. Reports of 180+ processes, each consuming 30–74 MB of RAM.Root Cause
BinaryLocator.resolveBinary()is called on every refresh cycle for each provider binary (claude,codex,gemini,auggie). Steps 4 and 4b of the resolution chain spawn interactive login shells using-l -i -cflags to locate binaries not found on the standard PATH:When the user's shell init file contains something like:
...each shell spawn triggers a new zellij server daemon. These daemons persist after the shell exits. With a 5-minute refresh interval and 4 binaries, that's up to 8 new zellij processes every 5 minutes — indefinitely.
Note:
LoginShellPathCapturer.capture()has the same spawn pattern but is already protected byLoginShellPathCache.captureOnce()and runs exactly once at startup. Steps 4/4b had no equivalent caching.Fix
Added
BinaryResolutionCache— a thread-safe singleton that mirrors theLoginShellPathCachepattern — to cache the result of shell-based binary lookups keyed by binary name.commandVthenaliasResolveronce, stores the result (includingnilas a "not found" sentinel), returns if resolved.The two separate shell invocations in steps 4 and 4b are now consolidated into a single cached block, so neither is ever repeated across refresh cycles.
Trade-off
The cache persists for the app session. If a user installs a new binary while CodexBar is running, a cached "not found" entry will prevent detection until restart. This is an acceptable trade-off — the prior behavior (re-running an interactive shell every 5 minutes) was the bug.
Fix 2:
@mainconflict with top-levelawaitinCodexBarClaudeWebProbeProblem
Building with Xcode produced:
Sources/CodexBarClaudeWebProbe/main.swiftused@mainwithstatic func main() asyncwhile also containing a top-levelawaitcall — Swift does not allow both in the same module.Fix
Removed
@main, renamedmain()torun(), and kept the explicit top-levelawait CodexBarClaudeWebProbe.run()call. Behaviour is identical.What Is Not Changed
LoginShellPathCapturer.capture()— still runs once at startup, unchanged.commandV/aliasResolverclosures via parameters and bypassBinaryResolutionCacheentirely; no test changes required.Testing
~/.config/fish/config.fish.Closes #228