perf(rpc): use bd daemon protocol to reduce subprocess overhead
Replace bd subprocess calls in gt commands with daemon RPC when available. Each subprocess call has ~40ms overhead for Go binary startup, so using the daemon's Unix socket protocol significantly reduces latency. Changes: - Add RPC client to beads package (beads_rpc.go) - Modify List/Show/Update/Close methods to try RPC first, fall back to subprocess - Replace runBdPrime() with direct content output (avoids bd subprocess) - Replace checkPendingEscalations() to use beads.List() with RPC - Replace hook.go bd subprocess calls with beads package methods The RPC client: - Connects to daemon via Unix socket at .beads/bd.sock - Uses JSON-based request/response protocol (same as bd daemon) - Falls back gracefully to subprocess if daemon unavailable - Lazy-initializes connection on first use Performance improvement targets (from bd-2zd.2): - gt prime < 100ms (was 5.8s with subprocess chain) - gt hook < 100ms (was ~323ms) Closes: bd-2zd.2
This commit is contained in:
@@ -119,6 +119,12 @@ type Beads struct {
|
||||
// Populated on first call to getTownRoot() to avoid filesystem walk on every operation.
|
||||
townRoot string
|
||||
searchedRoot bool
|
||||
|
||||
// RPC client for daemon communication (lazy-initialized).
|
||||
// When available, RPC is preferred over subprocess for performance.
|
||||
rpcClient *rpcClient
|
||||
rpcChecked bool
|
||||
rpcAvailable bool
|
||||
}
|
||||
|
||||
// New creates a new Beads wrapper for the given directory.
|
||||
@@ -287,7 +293,14 @@ func filterBeadsEnv(environ []string) []string {
|
||||
}
|
||||
|
||||
// List returns issues matching the given options.
|
||||
// Uses daemon RPC when available for better performance (~40ms faster).
|
||||
func (b *Beads) List(opts ListOptions) ([]*Issue, error) {
|
||||
// Try RPC first (faster when daemon is running)
|
||||
if issues, err := b.listViaRPC(opts); err == nil {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
args := []string{"list", "--json"}
|
||||
|
||||
if opts.Status != "" {
|
||||
@@ -400,7 +413,14 @@ func (b *Beads) ReadyWithType(issueType string) ([]*Issue, error) {
|
||||
}
|
||||
|
||||
// Show returns detailed information about an issue.
|
||||
// Uses daemon RPC when available for better performance (~40ms faster).
|
||||
func (b *Beads) Show(id string) (*Issue, error) {
|
||||
// Try RPC first (faster when daemon is running)
|
||||
if issue, err := b.showViaRPC(id); err == nil {
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
out, err := b.run("show", id, "--json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -559,7 +579,14 @@ func (b *Beads) CreateWithID(id string, opts CreateOptions) (*Issue, error) {
|
||||
}
|
||||
|
||||
// Update updates an existing issue.
|
||||
// Uses daemon RPC when available for better performance (~40ms faster).
|
||||
func (b *Beads) Update(id string, opts UpdateOptions) error {
|
||||
// Try RPC first (faster when daemon is running)
|
||||
if err := b.updateViaRPC(id, opts); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
args := []string{"update", id}
|
||||
|
||||
if opts.Title != nil {
|
||||
@@ -598,15 +625,26 @@ func (b *Beads) Update(id string, opts UpdateOptions) error {
|
||||
// Close closes one or more issues.
|
||||
// If a runtime session ID is set in the environment, it is passed to bd close
|
||||
// for work attribution tracking (see decision 009-session-events-architecture.md).
|
||||
// Uses daemon RPC when available for better performance (~40ms faster per call).
|
||||
func (b *Beads) Close(ids ...string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionID := runtime.SessionIDFromEnv()
|
||||
|
||||
// Try RPC for single-issue closes (faster when daemon is running)
|
||||
if len(ids) == 1 {
|
||||
if err := b.closeViaRPC(ids[0], "", sessionID, false); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
args := append([]string{"close"}, ids...)
|
||||
|
||||
// Pass session ID for work attribution if available
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
if sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
|
||||
@@ -617,16 +655,51 @@ func (b *Beads) Close(ids ...string) error {
|
||||
// CloseWithReason closes one or more issues with a reason.
|
||||
// If a runtime session ID is set in the environment, it is passed to bd close
|
||||
// for work attribution tracking (see decision 009-session-events-architecture.md).
|
||||
// Uses daemon RPC when available for better performance (~40ms faster per call).
|
||||
func (b *Beads) CloseWithReason(reason string, ids ...string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionID := runtime.SessionIDFromEnv()
|
||||
|
||||
// Try RPC for single-issue closes (faster when daemon is running)
|
||||
if len(ids) == 1 {
|
||||
if err := b.closeViaRPC(ids[0], reason, sessionID, false); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
args := append([]string{"close"}, ids...)
|
||||
args = append(args, "--reason="+reason)
|
||||
|
||||
// Pass session ID for work attribution if available
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
if sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
|
||||
_, err := b.run(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseForced closes an issue with force flag and optional reason.
|
||||
// The force flag bypasses blockers and other validation checks.
|
||||
// Uses daemon RPC when available for better performance (~40ms faster).
|
||||
func (b *Beads) CloseForced(id, reason string) error {
|
||||
sessionID := runtime.SessionIDFromEnv()
|
||||
|
||||
// Try RPC first (faster when daemon is running)
|
||||
if err := b.closeViaRPC(id, reason, sessionID, true); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fall back to subprocess
|
||||
args := []string{"close", id, "--force"}
|
||||
if reason != "" {
|
||||
args = append(args, "--reason="+reason)
|
||||
}
|
||||
if sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
|
||||
@@ -800,3 +873,19 @@ func ProvisionPrimeMDForWorktree(worktreePath string) error {
|
||||
// Provision PRIME.md in the target directory
|
||||
return ProvisionPrimeMD(beadsDir)
|
||||
}
|
||||
|
||||
// GetPrimeContent returns the beads workflow context content.
|
||||
// It checks for a custom PRIME.md file first, otherwise returns the default.
|
||||
// This eliminates the need to spawn a bd subprocess for gt prime.
|
||||
func GetPrimeContent(workDir string) string {
|
||||
beadsDir := ResolveBeadsDir(workDir)
|
||||
primePath := filepath.Join(beadsDir, "PRIME.md")
|
||||
|
||||
// Check for custom PRIME.md
|
||||
if content, err := os.ReadFile(primePath); err == nil {
|
||||
return strings.TrimSpace(string(content))
|
||||
}
|
||||
|
||||
// Return default content
|
||||
return strings.TrimSpace(primeContent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user