From f71066197d292f195420bc56ff95d6bfd41963b6 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 03:46:09 +0000 Subject: [PATCH] feat(agentic): schedule pr management loop Co-Authored-By: Virgil --- pkg/agentic/prep.go | 3 +++ pkg/agentic/review_queue.go | 24 ++++++++++++++++++++++++ pkg/agentic/review_queue_extra_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index a3e681c..6d8e116 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -290,6 +290,9 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { if planRetentionDays(core.NewOptions()) > 0 { go s.runPlanCleanupLoop(ctx, planRetentionScheduleInterval) } + if s.forgeToken != "" { + go s.runPRManageLoop(ctx, prManageScheduleInterval) + } c.RegisterQuery(s.handleWorkspaceQuery) diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index 4e4b9c2..33d0ff1 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -45,6 +45,8 @@ type RateLimitInfo struct { var retryAfterPattern = compileRetryAfterPattern() +const prManageScheduleInterval = 5 * time.Minute + func compileRetryAfterPattern() *regexp.Regexp { pattern, err := regexp.Compile(`(\d+)\s*minutes?\s*(?:and\s*)?(\d+)?\s*seconds?`) if err != nil { @@ -114,6 +116,28 @@ func (s *PrepSubsystem) cmdPRManage(options core.Options) core.Result { return s.cmdReviewQueue(options) } +// ctx, cancel := context.WithCancel(context.Background()) +// go s.runPRManageLoop(ctx, 5*time.Minute) +func (s *PrepSubsystem) runPRManageLoop(ctx context.Context, interval time.Duration) { + if ctx == nil || interval <= 0 { + return + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if result := s.cmdPRManage(core.NewOptions()); !result.OK { + core.Warn("pr-manage scheduled run failed", "error", result.Value) + } + } + } +} + func (s *PrepSubsystem) reviewQueue(ctx context.Context, _ *mcp.CallToolRequest, input ReviewQueueInput) (*mcp.CallToolResult, ReviewQueueOutput, error) { limit := input.Limit if limit <= 0 { diff --git a/pkg/agentic/review_queue_extra_test.go b/pkg/agentic/review_queue_extra_test.go index 948f517..670acc5 100644 --- a/pkg/agentic/review_queue_extra_test.go +++ b/pkg/agentic/review_queue_extra_test.go @@ -87,6 +87,32 @@ func TestReviewqueue_StoreReviewOutput_Good(t *testing.T) { }) } +func TestReviewqueue_RunPRManageLoop_Good_StopsOnCancel(t *testing.T) { + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + + go func() { + s.runPRManageLoop(ctx, time.Hour) + close(done) + }() + + cancel() + require.Eventually(t, func() bool { + select { + case <-done: + return true + default: + return false + } + }, time.Second, 5*time.Millisecond) +} + // --- reviewQueue --- func TestReviewqueue_NoCandidates_Good(t *testing.T) {