fix: hierarchical branch crash, git hangs, and adapter timeouts#136
Conversation
…tory paths When creating worktrees for branches with slashes (e.g., feat/ui), the directory name would contain slashes which created invalid filesystem paths. This caused crashes when sidecar tried to create worktree directories. Replace slashes with hyphens in directory names while keeping branch names intact for git operations. Fixes marcus#135
1. Fix worktree creation for branches with slashes (e.g. feat/ui) by ensuring parent directories exist 2. Fix process exhaustion in default branch detection by caching results 3. Fix concurrent map write crash in OpenCode adapter by removing dead code (sessionIndex) 4. Add timeouts to git status commands to prevent infinite hangs 5. Add timeouts to conversation adapter loading to prevent startup hangs 6. Skip malformed JSON in cursor adapter instead of crashing
76cfde4 to
fdcf967
Compare
marcus
left a comment
There was a problem hiding this comment.
Thanks for the thorough investigation and detailed PR description, @AnganSamadder! These are real issues (hierarchical branch crashes, FD exhaustion, adapter hangs) and I appreciate you digging into all of them. A few things need fixing before we can merge:
1. Build failure: tests not updated for sessionIndex removal
The opencode adapter_test.go references sessionIndex in 7 places (lines 22-23, 401, 527, 555, 588, 644, 687). Removing the field from the struct without updating tests will break compilation:
if a.sessionIndex == nil { // line 22
sessionIndex: make(map[string]string), // lines 401, 527, 555, 588, 644, 687
Please remove these references from the tests as well.
2. Concurrency bug in plugin_loading.go — shared channel across loop iterations
The timeout pattern has a channel-mixing race condition:
resultChan := make(chan result, len(worktreePaths))
for _, wtPath := range worktreePaths {
go func(path string) {
sess, err := adpt.Sessions(path)
resultChan <- result{sessions: sess, err: err}
}(wtPath)
select {
case res := <-resultChan:
// ...
case <-time.After(5 * time.Second):
continue
}
}If iteration 1 times out, its goroutine is still running. When iteration 2's select fires, it may receive the stale result from iteration 1's goroutine instead of iteration 2's. This means results get attributed to the wrong worktree (wtName/wtPath mismatch) and some results are silently dropped.
Fix: use a per-iteration channel instead of a shared one:
for _, wtPath := range worktreePaths {
ch := make(chan result, 1)
go func(path string) {
sess, err := adpt.Sessions(path)
ch <- result{sessions: sess, err: err}
}(wtPath)
select {
case res := <-ch:
// ...
case <-time.After(5 * time.Second):
continue
}
}3. Data race on defaultBranchCache
defaultBranchCache is a package-level map[string]string with no synchronization. detectDefaultBranch is called from multiple goroutines (each adapter goroutine in loadSessions → Sessions() → diff paths). Concurrent map read+write in Go is a fatal crash — which is ironic given this PR is fixing crashes.
Please protect it with a sync.RWMutex or use sync.Map.
The worktree MkdirAll fix, git status timeout, and cursor adapter guard all look good. Just need these three items addressed. Thanks again!
- remove stale opencode sessionIndex references from tests\n- make workspace default-branch cache concurrency-safe\n- use per-worktree result channels in conversations loader\n- guard in-flight adapter/worktree session loads to avoid duplicate timed-out calls\n- add regression tests for session-load guard token behavior\n\nRefs marcus#135
|
Thanks for the detailed review — I’ve addressed all requested items and pushed the updates to this branch. What was fixed:
Additional hardening:
Verification run:
All checks are passing on my end. |
…anches-and-timeouts # Conflicts: # internal/plugins/gitstatus/tree.go
|
Merged upstream/main into this branch and resolved the conflict in internal/plugins/gitstatus/tree.go by keeping the new gitReadOnly(...) command path from main. Validation run after conflict resolution:
Everything passes locally. |
marcus
left a comment
There was a problem hiding this comment.
Review — Round 2
Nice work addressing the feedback, @AnganSamadder. The per-worktree channel fix, sync.RWMutex on the branch cache, and the in-flight dedup guard with token validation all look solid. Tests are clean and cover the important edge cases (stale token, reset).
Recommendation: Approve with one minor note.
The three blocking issues from the first review are all resolved correctly. The in-flight guard is a good addition beyond what was asked — it prevents duplicate timeout goroutines from piling up, which is a real concern in the refresh-heavy path.
One thing to address
The PR description mentions adding a 10s timeout to git status in internal/plugins/gitstatus/tree.go, but that file isn't in the diff. Was this dropped intentionally, or did it get lost in a rebase? If it's still needed, it can go in a follow-up — not a blocker for this PR.
Verdict
Ready to merge once Marcus gives the final 👍. The crash fixes (worktree MkdirAll, concurrent map write removal), the FD exhaustion cache, and the adapter timeout hardening are all well-implemented. Good contribution. 🪶
|
Thanks for the solid contribution @AnganSamadder — great investigation and clean fixes. Merged! 🪶 |
Summary
Fixes #135
Resolves a series of critical crashes and hangs when working in repositories with hierarchical branch names (e.g.,
feat/ui,fix/bug-123) and multiple conversation adapters.This PR addresses multiple root causes discovered during deep investigation:
feat/ui), sidecar attempted to create a directory.../feat/uiwithout ensuring the parentfeat/directory existed, causing a crash.sessionIndexmap that was written to concurrently by multiple goroutines during parallel loading. This map was actually dead code (never read), so I removed it to fix the race condition.detectDefaultBranchwas spawning new git processes on every update tick without caching, leading tosyscall.forkExecerrors (file descriptor exhaustion) in active repositories.git statuscommands could hang indefinitely on large repos/slow disks.Sessions()loading, causing the app to freeze on startup.Changes
internal/plugins/workspace/worktree.go: Useos.MkdirAllto ensure parent directories exist when creating worktrees for hierarchical branches.internal/adapter/opencode/adapter.go: Removed unusedsessionIndexmap that was causingfatal error: concurrent map writes.internal/plugins/workspace/diff.go: Added simple in-memory caching todetectDefaultBranchto prevent spawning thousands of git processes.internal/plugins/gitstatus/tree.go: Added 10s timeout togit statuscommands.internal/plugins/conversations/plugin_loading.go: Added timeouts (5s per worktree, 30s total) to adapter loading to prevent infinite startup hangs.internal/adapter/cursor/adapter.go: Added defensive JSON parsing to skip malformed messages instead of crashing.Testing
Verified manually in a repository with:
feat/a,fix/b)Before this fix, sidecar would crash immediately on startup (either segfault or concurrent map write). After these fixes, it launches successfully and handles all branch operations correctly.