From f3f46de20d1f315c3a720c0487477982eaa07778 Mon Sep 17 00:00:00 2001 From: gastown/crew/jack Date: Tue, 30 Dec 2025 23:25:58 -0800 Subject: [PATCH] Add 5-second timeouts to convoy panel subprocess calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds context timeouts to bd and sqlite3 calls in the convoy panel TUI. If these commands hang, the TUI will no longer freeze - it will timeout after 5 seconds and return empty/error state. Functions updated: - listConvoys: bd list with context timeout - getTrackedIssueStatus: sqlite3 query with context timeout - getIssueStatus: bd show with context timeout (gt-14xej) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/tui/feed/convoy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/tui/feed/convoy.go b/internal/tui/feed/convoy.go index 49e47d29..0eed85fa 100644 --- a/internal/tui/feed/convoy.go +++ b/internal/tui/feed/convoy.go @@ -2,6 +2,7 @@ package feed import ( "bytes" + "context" "encoding/json" "fmt" "os/exec" @@ -17,6 +18,10 @@ import ( // convoyIDPattern validates convoy IDs to prevent SQL injection var convoyIDPattern = regexp.MustCompile(`^hq-[a-zA-Z0-9-]+$`) +// convoySubprocessTimeout is the timeout for bd and sqlite3 calls in the convoy panel. +// Prevents TUI freezing if these commands hang. +const convoySubprocessTimeout = 5 * time.Second + // Convoy represents a convoy's status for the dashboard type Convoy struct { ID string `json:"id"` @@ -85,7 +90,10 @@ func FetchConvoys(townRoot string) (*ConvoyState, error) { func listConvoys(beadsDir, status string) ([]convoyListItem, error) { listArgs := []string{"list", "--type=convoy", "--status=" + status, "--json"} - cmd := exec.Command("bd", listArgs...) + ctx, cancel := context.WithTimeout(context.Background(), convoySubprocessTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "bd", listArgs...) cmd.Dir = beadsDir var stdout bytes.Buffer cmd.Stdout = &stdout @@ -156,9 +164,12 @@ func getTrackedIssueStatus(beadsDir, convoyID string) []trackedStatus { dbPath := filepath.Join(beadsDir, "beads.db") + ctx, cancel := context.WithTimeout(context.Background(), convoySubprocessTimeout) + defer cancel() + // Query tracked dependencies from SQLite // convoyID is validated above to match ^hq-[a-zA-Z0-9-]+$ - cmd := exec.Command("sqlite3", "-json", dbPath, + cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath, fmt.Sprintf(`SELECT depends_on_id FROM dependencies WHERE issue_id = '%s' AND type = 'tracks'`, convoyID)) var stdout bytes.Buffer @@ -196,7 +207,10 @@ func getTrackedIssueStatus(beadsDir, convoyID string) []trackedStatus { // getIssueStatus fetches just the status of an issue func getIssueStatus(issueID string) string { - cmd := exec.Command("bd", "show", issueID, "--json") + ctx, cancel := context.WithTimeout(context.Background(), convoySubprocessTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "bd", "show", issueID, "--json") var stdout bytes.Buffer cmd.Stdout = &stdout