Add event-driven daemon architecture (Phase 1 foundation)

- Add fsnotify dependency for file watching
- Create daemon_debouncer.go: batch rapid events (500ms window)
- Create daemon_watcher.go: monitor JSONL and git refs changes
- Create daemon_event_loop.go: event-driven sync loop
- Add mutation channel to RPC server (create/update/close events)
- Add BEADS_DAEMON_MODE env var (poll/events, default: poll)

Phase 1 implementation: opt-in via BEADS_DAEMON_MODE=events
Target: <500ms latency (vs 5000ms), ~60% CPU reduction

Related: bd-49 (epic), bd-50, bd-51, bd-53, bd-54, bd-55, bd-56
Amp-Thread-ID: https://ampcode.com/threads/T-35a3d0d7-4e19-421d-8392-63755035036e
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-28 13:12:37 -07:00
parent 97c676077d
commit 781e300d33
8 changed files with 2527 additions and 1 deletions

View File

@@ -46,6 +46,15 @@ type Server struct {
readyChan chan struct{}
// Auto-import single-flight guard
importInProgress atomic.Bool
// Mutation events for event-driven daemon
mutationChan chan MutationEvent
}
// MutationEvent represents a database mutation for event-driven sync
type MutationEvent struct {
Type string // "create", "update", "delete", "comment"
IssueID string // e.g., "bd-42"
Timestamp time.Time
}
// NewServer creates a new RPC server
@@ -79,7 +88,28 @@ func NewServer(socketPath string, store storage.Storage, workspacePath string, d
connSemaphore: make(chan struct{}, maxConns),
requestTimeout: requestTimeout,
readyChan: make(chan struct{}),
mutationChan: make(chan MutationEvent, 100), // Buffered to avoid blocking
}
s.lastActivityTime.Store(time.Now())
return s
}
// emitMutation sends a mutation event to the daemon's event-driven loop.
// Non-blocking: drops event if channel is full (sync will happen eventually).
func (s *Server) emitMutation(eventType, issueID string) {
select {
case s.mutationChan <- MutationEvent{
Type: eventType,
IssueID: issueID,
Timestamp: time.Now(),
}:
// Event sent successfully
default:
// Channel full, event dropped (not critical - sync will happen eventually)
}
}
// MutationChan returns the mutation event channel for the daemon to consume
func (s *Server) MutationChan() <-chan MutationEvent {
return s.mutationChan
}

View File

@@ -159,6 +159,9 @@ func (s *Server) handleCreate(req *Request) Response {
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("create", issue.ID)
data, _ := json.Marshal(issue)
return Response{
Success: true,
@@ -190,6 +193,9 @@ func (s *Server) handleUpdate(req *Request) Response {
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("update", updateArgs.ID)
issue, err := store.GetIssue(ctx, updateArgs.ID)
if err != nil {
return Response{
@@ -224,6 +230,9 @@ func (s *Server) handleClose(req *Request) Response {
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("update", closeArgs.ID)
issue, _ := store.GetIssue(ctx, closeArgs.ID)
data, _ := json.Marshal(issue)
return Response{