Implement daemon RPC with per-request context routing (bd-115)
- Added per-request storage routing in daemon server - Server now supports Cwd field in requests for database discovery - Tree-walking to find .beads/*.db from any working directory - Storage caching for performance across requests - Created Python daemon client (bd_daemon_client.py) - RPC over Unix socket communication - Implements full BdClientBase interface - Auto-discovery of daemon socket from working directory - Refactored bd_client.py with abstract interface - BdClientBase abstract class for common interface - BdCliClient for CLI-based operations (renamed from BdClient) - create_bd_client() factory with daemon/CLI fallback - Backwards-compatible BdClient alias Next: Update MCP server to use daemon client when available
This commit is contained in:
@@ -26,6 +26,7 @@ type Request struct {
|
||||
Args json.RawMessage `json:"args"`
|
||||
Actor string `json:"actor,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Cwd string `json:"cwd,omitempty"` // Working directory for database discovery
|
||||
}
|
||||
|
||||
// Response represents an RPC response from daemon to client
|
||||
|
||||
@@ -13,23 +13,28 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// Server represents the RPC server that runs in the daemon
|
||||
type Server struct {
|
||||
socketPath string
|
||||
storage storage.Storage
|
||||
storage storage.Storage // Default storage (for backward compat)
|
||||
listener net.Listener
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
shutdown bool
|
||||
// Per-request storage routing
|
||||
storageCache map[string]storage.Storage // path -> storage
|
||||
cacheMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewServer creates a new RPC server
|
||||
func NewServer(socketPath string, store storage.Storage) *Server {
|
||||
return &Server{
|
||||
socketPath: socketPath,
|
||||
storage: store,
|
||||
socketPath: socketPath,
|
||||
storage: store,
|
||||
storageCache: make(map[string]storage.Storage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +252,14 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
var design, acceptance, assignee *string
|
||||
if createArgs.Design != "" {
|
||||
design = &createArgs.Design
|
||||
@@ -271,7 +284,7 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.CreateIssue(ctx, issue, s.reqActor(req)); err != nil {
|
||||
if err := store.CreateIssue(ctx, issue, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to create issue: %v", err),
|
||||
@@ -294,20 +307,28 @@ func (s *Server) handleUpdate(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
updates := updatesFromArgs(updateArgs)
|
||||
if len(updates) == 0 {
|
||||
return Response{Success: true}
|
||||
}
|
||||
|
||||
if err := s.storage.UpdateIssue(ctx, updateArgs.ID, updates, s.reqActor(req)); err != nil {
|
||||
if err := store.UpdateIssue(ctx, updateArgs.ID, updates, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to update issue: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
issue, err := s.storage.GetIssue(ctx, updateArgs.ID)
|
||||
issue, err := store.GetIssue(ctx, updateArgs.ID)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
@@ -331,15 +352,23 @@ func (s *Server) handleClose(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.CloseIssue(ctx, closeArgs.ID, closeArgs.Reason, s.reqActor(req)); err != nil {
|
||||
if err := store.CloseIssue(ctx, closeArgs.ID, closeArgs.Reason, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to close issue: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
issue, _ := s.storage.GetIssue(ctx, closeArgs.ID)
|
||||
issue, _ := store.GetIssue(ctx, closeArgs.ID)
|
||||
data, _ := json.Marshal(issue)
|
||||
return Response{
|
||||
Success: true,
|
||||
@@ -356,6 +385,14 @@ func (s *Server) handleList(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
filter := types.IssueFilter{
|
||||
Limit: listArgs.Limit,
|
||||
}
|
||||
@@ -375,7 +412,7 @@ func (s *Server) handleList(req *Request) Response {
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
issues, err := s.storage.SearchIssues(ctx, listArgs.Query, filter)
|
||||
issues, err := store.SearchIssues(ctx, listArgs.Query, filter)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
@@ -399,8 +436,16 @@ func (s *Server) handleShow(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
issue, err := s.storage.GetIssue(ctx, showArgs.ID)
|
||||
issue, err := store.GetIssue(ctx, showArgs.ID)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
@@ -424,6 +469,14 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
wf := types.WorkFilter{
|
||||
Status: types.StatusOpen,
|
||||
Priority: readyArgs.Priority,
|
||||
@@ -434,7 +487,7 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
issues, err := s.storage.GetReadyWork(ctx, wf)
|
||||
issues, err := store.GetReadyWork(ctx, wf)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
@@ -450,8 +503,16 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
}
|
||||
|
||||
func (s *Server) handleStats(req *Request) Response {
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
stats, err := s.storage.GetStatistics(ctx)
|
||||
stats, err := store.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
@@ -475,6 +536,14 @@ func (s *Server) handleDepAdd(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
dep := &types.Dependency{
|
||||
IssueID: depArgs.FromID,
|
||||
DependsOnID: depArgs.ToID,
|
||||
@@ -482,7 +551,7 @@ func (s *Server) handleDepAdd(req *Request) Response {
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.AddDependency(ctx, dep, s.reqActor(req)); err != nil {
|
||||
if err := store.AddDependency(ctx, dep, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to add dependency: %v", err),
|
||||
@@ -501,8 +570,16 @@ func (s *Server) handleDepRemove(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, s.reqActor(req)); err != nil {
|
||||
if err := store.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to remove dependency: %v", err),
|
||||
@@ -521,8 +598,16 @@ func (s *Server) handleLabelAdd(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.AddLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
|
||||
if err := store.AddLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to add label: %v", err),
|
||||
@@ -541,8 +626,16 @@ func (s *Server) handleLabelRemove(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := s.getStorageForRequest(req)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("storage error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
if err := s.storage.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
|
||||
if err := store.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to remove label: %v", err),
|
||||
@@ -569,6 +662,7 @@ func (s *Server) handleBatch(req *Request) Response {
|
||||
Args: op.Args,
|
||||
Actor: req.Actor,
|
||||
RequestID: req.RequestID,
|
||||
Cwd: req.Cwd, // Pass through context
|
||||
}
|
||||
|
||||
resp := s.handleRequest(subReq)
|
||||
@@ -593,6 +687,73 @@ func (s *Server) handleBatch(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
// getStorageForRequest returns the appropriate storage for the request
|
||||
// If req.Cwd is set, it finds the database for that directory
|
||||
// Otherwise, it uses the default storage
|
||||
func (s *Server) getStorageForRequest(req *Request) (storage.Storage, error) {
|
||||
// If no cwd specified, use default storage
|
||||
if req.Cwd == "" {
|
||||
return s.storage, nil
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
s.cacheMu.RLock()
|
||||
cached, ok := s.storageCache[req.Cwd]
|
||||
s.cacheMu.RUnlock()
|
||||
if ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// Find database for this cwd
|
||||
dbPath := s.findDatabaseForCwd(req.Cwd)
|
||||
if dbPath == "" {
|
||||
return nil, fmt.Errorf("no .beads database found for path: %s", req.Cwd)
|
||||
}
|
||||
|
||||
// Open storage
|
||||
store, err := sqlite.New(dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database at %s: %w", dbPath, err)
|
||||
}
|
||||
|
||||
// Cache it
|
||||
s.cacheMu.Lock()
|
||||
s.storageCache[req.Cwd] = store
|
||||
s.cacheMu.Unlock()
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// findDatabaseForCwd walks up from cwd to find .beads/*.db
|
||||
func (s *Server) findDatabaseForCwd(cwd string) string {
|
||||
dir, err := filepath.Abs(cwd)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Walk up directory tree
|
||||
for {
|
||||
beadsDir := filepath.Join(dir, ".beads")
|
||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||
// Found .beads/ directory, look for *.db files
|
||||
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
||||
if err == nil && len(matches) > 0 {
|
||||
return matches[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// Reached filesystem root
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Server) writeResponse(writer *bufio.Writer, resp Response) {
|
||||
data, _ := json.Marshal(resp)
|
||||
writer.Write(data)
|
||||
|
||||
Reference in New Issue
Block a user