Add 5-second timeouts to convoy panel subprocess calls

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 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/jack
2025-12-30 23:25:58 -08:00
committed by Steve Yegge
parent f464f87d5b
commit f3f46de20d

View File

@@ -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