package agents import ( "context" "errors" "log/slog" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/git" "g.fc.run/ai/byre/internal/store" ) const allTasks = "*" func (m *Manager) observeFacts(ev api.Event) { switch ev.Kind { case "task.created", "turn.ended", "snapshot.created", "review.started", "review.finished", "review.finding", "user.message", "board.posted": if ev.TaskId != nil { m.MarkFacts(*ev.TaskId) } case "task.merged", "task.merged_to_main": if ev.ProjectId != nil { m.MarkFacts("project:" + *ev.ProjectId) } case "user.updated": m.MarkFacts(allTasks) } } func (m *Manager) MarkFacts(key string) { m.factsMu.Lock() m.factsDirty[key] = true m.factsMu.Unlock() select { case m.factsPoke <- struct{}{}: default: } } func (m *Manager) factsLoop() { for { select { case <-m.watchStop: return case <-m.factsPoke: } m.factsMu.Lock() dirty := m.factsDirty m.factsDirty = map[string]bool{} m.factsMu.Unlock() err := m.settleFacts(context.Background(), dirty) if err != nil { slog.Error("settle merge facts", "err", err) } } } func (m *Manager) settleFacts(ctx context.Context, dirty map[string]bool) error { tasks, err := m.store.ActiveTasks(ctx) if err != nil { return err } projects := map[string]api.Project{} for _, t := range tasks { if !dirty[allTasks] && !dirty[t.Id] && !dirty["project:"+t.ProjectId] { continue } p, ok := projects[t.ProjectId] if !ok { p, err = m.store.GetProject(ctx, t.ProjectId) if err != nil { return err } projects[t.ProjectId] = p } err = m.settleTaskFacts(ctx, t, p) if err != nil { return err } } return nil } func (m *Manager) refreshFacts(ctx context.Context, taskID string) { t, err := m.store.GetTask(ctx, taskID) if err == nil { var p api.Project p, err = m.store.GetProject(ctx, t.ProjectId) if err == nil { err = m.settleTaskFacts(ctx, t, p) } } if err != nil { slog.Error("refresh merge facts", "task", taskID, "err", err) } } func (m *Manager) settleTaskFacts(ctx context.Context, t api.Task, p api.Project) error { m.factsRun.Lock() defer m.factsRun.Unlock() g, err := m.gitFacts(ctx, t, p) if err != nil { slog.Error("compute merge facts", "task", t.Id, "err", err) return nil } err = m.store.SettleMergeFacts(ctx, t.Id, g) if errors.Is(err, store.ErrNotFound) { return nil } return err } func (m *Manager) gitFacts(ctx context.Context, t api.Task, p api.Project) (store.GitFacts, error) { g := store.GitFacts{} if t.WorktreePath == "" { return g, nil } tree, err := git.Snapshot(t.WorktreePath) if err != nil { return g, err } g.UnmergedCommits, err = git.Ahead(t.WorktreePath, p.DefaultBranch) if err != nil { return g, err } g.UnmergedFiles, err = git.ChangedFileCount(t.WorktreePath, p.DefaultBranch, tree) if err != nil { return g, err } latest, err := m.store.LastReviewable(ctx, t.Id) if errors.Is(err, store.ErrNotFound) { return g, nil } if err != nil { return g, err } g.UnreviewedChanges = latest.TreeSha != tree return g, nil }