Skip to content

fix: hierarchical branch crash, git hangs, and adapter timeouts#136

Merged
marcus merged 4 commits intomarcus:mainfrom
AnganSamadder:fix/hierarchical-branches-and-timeouts
Feb 26, 2026
Merged

fix: hierarchical branch crash, git hangs, and adapter timeouts#136
marcus merged 4 commits intomarcus:mainfrom
AnganSamadder:fix/hierarchical-branches-and-timeouts

Conversation

@AnganSamadder
Copy link
Contributor

@AnganSamadder AnganSamadder commented Feb 11, 2026

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:

  1. Worktree Crash: When creating worktrees for branches with slashes (e.g. feat/ui), sidecar attempted to create a directory .../feat/ui without ensuring the parent feat/ directory existed, causing a crash.
  2. OpenCode Adapter Crash (Concurrent Map Write): The OpenCode adapter had a sessionIndex map 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.
  3. Process Exhaustion: detectDefaultBranch was spawning new git processes on every update tick without caching, leading to syscall.forkExec errors (file descriptor exhaustion) in active repositories.
  4. Infinite Hangs:
    • git status commands could hang indefinitely on large repos/slow disks.
    • Conversation adapters (like Cursor) could hang indefinitely during Sessions() loading, causing the app to freeze on startup.
  5. Adapter Crash: The Cursor adapter would crash if it encountered malformed JSON in chat history files.

Changes

  • internal/plugins/workspace/worktree.go: Use os.MkdirAll to ensure parent directories exist when creating worktrees for hierarchical branches.
  • internal/adapter/opencode/adapter.go: Removed unused sessionIndex map that was causing fatal error: concurrent map writes.
  • internal/plugins/workspace/diff.go: Added simple in-memory caching to detectDefaultBranch to prevent spawning thousands of git processes.
  • internal/plugins/gitstatus/tree.go: Added 10s timeout to git status commands.
  • 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:

  • Multiple hierarchical branches (feat/a, fix/b)
  • Large commit history
  • Cursor chat history present
  • OpenCode adapter enabled

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.

…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
Copy link
Owner

@marcus marcus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 loadSessionsSessions() → 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
@AnganSamadder
Copy link
Contributor Author

Thanks for the detailed review — I’ve addressed all requested items and pushed the updates to this branch.

What was fixed:

  • Removed stale sessionIndex references from internal/adapter/opencode/adapter_test.go so tests compile against the updated adapter struct.
  • Fixed the timeout/channel-mixing bug in internal/plugins/conversations/plugin_loading.go by using a per-worktree channel for each Sessions() call.
  • Made defaultBranchCache thread-safe in internal/plugins/workspace/diff.go via sync.RWMutex.

Additional hardening:

  • Added an in-flight guard in conversations loading keyed by adapter/worktree with token validation, so repeated refreshes won’t spawn duplicate timed-out loads for the same key while an earlier one is still running.
  • Added regression tests for that guard in internal/plugins/conversations/plugin_loading_test.go.

Verification run:

  • go build ./...
  • go test ./...
  • go vet ./...
  • go test -race ./internal/plugins/conversations ./internal/plugins/workspace ./internal/plugins/gitstatus ./internal/adapter/opencode ./internal/adapter/cursor

All checks are passing on my end.

…anches-and-timeouts

# Conflicts:
#	internal/plugins/gitstatus/tree.go
@AnganSamadder
Copy link
Contributor Author

AnganSamadder commented Feb 26, 2026

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:

  • go build ./...
  • go test ./...

Everything passes locally.

Copy link
Owner

@marcus marcus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🪶

@marcus marcus merged commit ec7e776 into marcus:main Feb 26, 2026
@marcus
Copy link
Owner

marcus commented Feb 26, 2026

Thanks for the solid contribution @AnganSamadder — great investigation and clean fixes. Merged! 🪶

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Critical crashes and hangs with hierarchical branches & multiple adapters

2 participants