From 09bd4e7f9e6d7db41d30eea164853f3a18e3bd1e Mon Sep 17 00:00:00 2001 From: Adam Stracener Date: Wed, 18 Feb 2026 14:51:13 -0600 Subject: [PATCH 1/3] Fix zellij process accumulation by caching shell binary lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Sources/CodexBarCore/PathEnvironment.swift | 72 ++++++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/Sources/CodexBarCore/PathEnvironment.swift b/Sources/CodexBarCore/PathEnvironment.swift index ece93eda2..7025db2f6 100644 --- a/Sources/CodexBarCore/PathEnvironment.swift +++ b/Sources/CodexBarCore/PathEnvironment.swift @@ -150,18 +150,28 @@ public enum BinaryLocator { return pathHit } - // 4) Interactive login shell lookup (captures nvm/fnm/mise paths from .zshrc/.bashrc) - if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager), - fileManager.isExecutableFile(atPath: shellHit) - { - return shellHit - } - - // 4b) Alias fallback (login shell); only attempt after all standard lookups fail. - if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home), - fileManager.isExecutableFile(atPath: aliasHit) - { - return aliasHit + // 4) Interactive login shell lookup — use cache to avoid repeated shell spawns + // (shell auto-start hooks like zellij create new daemon instances on each invocation) + let cached = BinaryResolutionCache.shared.cachedResult(for: name) + if let cached { + if let path = cached.path, fileManager.isExecutableFile(atPath: path) { + return path + } + // Cached nil = not found; skip shell spawns + } else { + // Not yet cached — run the shell lookups once and cache the result + var resolved: String? = nil + if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager), + fileManager.isExecutableFile(atPath: shellHit) + { + resolved = shellHit + } else if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home), + fileManager.isExecutableFile(atPath: aliasHit) + { + resolved = aliasHit + } + BinaryResolutionCache.shared.store(path: resolved, for: name) + if let resolved { return resolved } } // 5) Minimal fallback @@ -440,6 +450,44 @@ enum LoginShellPathCapturer { } } +/// Caches the results of interactive-shell binary lookups (steps 4/4b of resolveBinary). +/// Prevents repeated login-shell spawns — which can trigger auto-start hooks like zellij — +/// on every refresh cycle when a binary is not found on the standard PATH. +final class BinaryResolutionCache: @unchecked Sendable { + static let shared = BinaryResolutionCache() + + private let lock = NSLock() + // nil value = binary confirmed not found; missing key = not yet looked up + private var cache: [String: String?] = [:] + + func cachedResult(for name: String) -> (found: Bool, path: String?)? { + lock.lock() + defer { lock.unlock() } + guard cache.keys.contains(name) else { return nil } + return (found: cache[name] != nil, path: cache[name] ?? nil) + } + + func store(path: String?, for name: String) { + lock.lock() + defer { lock.unlock() } + cache[name] = path + } + + /// Resets a single entry (e.g., when a binary is installed/uninstalled at runtime). + func invalidate(_ name: String) { + lock.lock() + defer { lock.unlock() } + cache.removeValue(forKey: name) + } + + /// Resets all cached entries. + func invalidateAll() { + lock.lock() + defer { lock.unlock() } + cache.removeAll() + } +} + public final class LoginShellPathCache: @unchecked Sendable { public static let shared = LoginShellPathCache() From d4ad4d3093ee0a4eb42defafc0257f213aa3b425 Mon Sep 17 00:00:00 2001 From: Adam Stracener Date: Wed, 18 Feb 2026 14:54:25 -0600 Subject: [PATCH 2/3] Fix @main conflict with top-level await in CodexBarClaudeWebProbe 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 --- Sources/CodexBarClaudeWebProbe/main.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarClaudeWebProbe/main.swift b/Sources/CodexBarClaudeWebProbe/main.swift index 63d9ce867..81aab4a16 100644 --- a/Sources/CodexBarClaudeWebProbe/main.swift +++ b/Sources/CodexBarClaudeWebProbe/main.swift @@ -1,7 +1,6 @@ import CodexBarCore import Foundation -@main enum CodexBarClaudeWebProbe { private static let defaultEndpoints: [String] = [ "https://claude.ai/api/organizations", @@ -19,7 +18,7 @@ enum CodexBarClaudeWebProbe { "https://claude.ai/settings/usage", ] - static func main() async { + static func run() async { let args = CommandLine.arguments.dropFirst() let endpoints = args.isEmpty ? Self.defaultEndpoints : Array(args) let includePreview = ProcessInfo.processInfo.environment["CLAUDE_WEB_PROBE_PREVIEW"] == "1" @@ -61,3 +60,5 @@ enum CodexBarClaudeWebProbe { print("") } } +await CodexBarClaudeWebProbe.run() + From b9e639ce3e73f22e98b6cd846ea512ee091194d3 Mon Sep 17 00:00:00 2001 From: Adam Stracener Date: Wed, 18 Feb 2026 15:01:49 -0600 Subject: [PATCH 3/3] Address PR review: fix cache key context and stale path handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Sources/CodexBarCore/PathEnvironment.swift | 41 +++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBarCore/PathEnvironment.swift b/Sources/CodexBarCore/PathEnvironment.swift index 7025db2f6..7f3797d32 100644 --- a/Sources/CodexBarCore/PathEnvironment.swift +++ b/Sources/CodexBarCore/PathEnvironment.swift @@ -52,6 +52,7 @@ public enum BinaryLocator { loginPATH: loginPATH, commandV: commandV, aliasResolver: aliasResolver, + useShellCache: true, fileManager: fileManager, home: home) } @@ -72,6 +73,7 @@ public enum BinaryLocator { loginPATH: loginPATH, commandV: commandV, aliasResolver: aliasResolver, + useShellCache: true, fileManager: fileManager, home: home) } @@ -92,6 +94,7 @@ public enum BinaryLocator { loginPATH: loginPATH, commandV: commandV, aliasResolver: aliasResolver, + useShellCache: true, fileManager: fileManager, home: home) } @@ -112,6 +115,7 @@ public enum BinaryLocator { loginPATH: loginPATH, commandV: commandV, aliasResolver: aliasResolver, + useShellCache: true, fileManager: fileManager, home: home) } @@ -124,6 +128,7 @@ public enum BinaryLocator { loginPATH: [String]?, commandV: (String, String?, TimeInterval, FileManager) -> String?, aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?, + useShellCache: Bool, fileManager: FileManager, home: String) -> String? { @@ -151,15 +156,24 @@ public enum BinaryLocator { } // 4) Interactive login shell lookup — use cache to avoid repeated shell spawns - // (shell auto-start hooks like zellij create new daemon instances on each invocation) - let cached = BinaryResolutionCache.shared.cachedResult(for: name) - if let cached { - if let path = cached.path, fileManager.isExecutableFile(atPath: path) { - return path + // (shell auto-start hooks like zellij create new daemon instances on each invocation). + // Cache is bypassed when custom commandV/aliasResolver closures are injected (e.g. tests) + // to prevent a cached result from one call context affecting another. + if useShellCache { + let cached = BinaryResolutionCache.shared.cachedResult(for: name) + if let cached { + if let path = cached.path { + if fileManager.isExecutableFile(atPath: path) { + return path + } + // Cached path is no longer executable (e.g. CLI uninstalled/moved) — retry + BinaryResolutionCache.shared.invalidate(name) + } else { + // Cached nil = confirmed not found; skip shell spawns + return nil + } } - // Cached nil = not found; skip shell spawns - } else { - // Not yet cached — run the shell lookups once and cache the result + // Not yet cached (or just invalidated) — run the shell lookups once and cache the result var resolved: String? = nil if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager), fileManager.isExecutableFile(atPath: shellHit) @@ -172,6 +186,17 @@ public enum BinaryLocator { } BinaryResolutionCache.shared.store(path: resolved, for: name) if let resolved { return resolved } + } else { + if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager), + fileManager.isExecutableFile(atPath: shellHit) + { + return shellHit + } + if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home), + fileManager.isExecutableFile(atPath: aliasHit) + { + return aliasHit + } } // 5) Minimal fallback