diff --git a/pkg/agentic/plan_retention.go b/pkg/agentic/plan_retention.go index b80525b..b64f7fa 100644 --- a/pkg/agentic/plan_retention.go +++ b/pkg/agentic/plan_retention.go @@ -3,6 +3,7 @@ package agentic import ( + "context" "sort" "time" @@ -10,6 +11,7 @@ import ( ) const planRetentionDefaultDays = 90 +const planRetentionScheduleInterval = 24 * time.Hour type PlanCleanupOutput struct { Success bool `json:"success"` @@ -55,6 +57,26 @@ func (s *PrepSubsystem) cmdPlanCleanup(options core.Options) core.Result { return core.Result{Value: output, OK: true} } +// ctx, cancel := context.WithCancel(context.Background()) +// go s.runPlanCleanupLoop(ctx, time.Minute) +func (s *PrepSubsystem) runPlanCleanupLoop(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: + s.planCleanup(core.NewOptions()) + } + } +} + func (s *PrepSubsystem) planCleanup(options core.Options) core.Result { days := planRetentionDays(options) if days <= 0 { diff --git a/pkg/agentic/plan_retention_test.go b/pkg/agentic/plan_retention_test.go index 9fec9e7..bb6cfa9 100644 --- a/pkg/agentic/plan_retention_test.go +++ b/pkg/agentic/plan_retention_test.go @@ -3,6 +3,7 @@ package agentic import ( + "context" "testing" "time" @@ -141,3 +142,43 @@ func TestPlanRetention_PlanArchivedAt_Good_FallsBackToFileModifiedTime(t *testin _, ok := stat.Value.(interface{ ModTime() time.Time }) assert.True(t, ok) } + +func TestPlanRetention_RunPlanCleanupLoop_Good_DeletesExpiredPlans(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + + plan := &Plan{ + ID: "scheduled-plan-abc123", + Title: "Scheduled Plan", + Status: "archived", + Objective: "Remove me on the next retention pass", + ArchivedAt: time.Now().AddDate(0, 0, -100), + } + + _, err := writePlan(PlansRoot(), plan) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + + go func() { + s.runPlanCleanupLoop(ctx, time.Millisecond) + close(done) + }() + + require.Eventually(t, func() bool { + return !fs.Exists(core.JoinPath(PlansRoot(), "scheduled-plan-abc123.json")) + }, time.Second, 5*time.Millisecond) + + cancel() + require.Eventually(t, func() bool { + select { + case <-done: + return true + default: + return false + } + }, time.Second, 5*time.Millisecond) +} diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 16e15c9..3fa88cf 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -257,6 +257,9 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { c.Action("agentic.complete", s.handleComplete).Description = "Run completion pipeline (QA → PR → Verify → Ingest → Poke) in background" s.hydrateWorkspaces() + if planRetentionDays(core.NewOptions()) > 0 { + go s.runPlanCleanupLoop(ctx, planRetentionScheduleInterval) + } c.RegisterQuery(s.handleWorkspaceQuery)