diff --git a/cmd/email-agent/main.go b/cmd/email-agent/main.go index 6ac4695..cc6909d 100644 --- a/cmd/email-agent/main.go +++ b/cmd/email-agent/main.go @@ -346,9 +346,7 @@ func runRetain(cfg *config.Config, logger *slog.Logger) error { } // The audit row is written inside the same transaction, before the cap, so // the cap counts it. This is what keeps actions_log bounded across runs. - detail := fmt.Sprintf("bodies_cleared=%%d retention_days=%d max_rows=%d", - cfg.RetentionDays, cfg.ActionsLogMaxRows) - res, err := st.RetainWithAudit(ctx, policy, "scheduler", detail) + res, err := st.RetainWithAudit(ctx, policy, "scheduler") if err != nil { return err } diff --git a/internal/store/retention.go b/internal/store/retention.go index 706a983..f47b2fd 100644 --- a/internal/store/retention.go +++ b/internal/store/retention.go @@ -49,14 +49,19 @@ type RetentionResult struct { // pass never leaves the database in a mixed state. It is safe to run on a // schedule: every statement is idempotent. func (s *Store) Retain(ctx context.Context, p RetentionPolicy) (RetentionResult, error) { - return s.RetainWithAudit(ctx, p, "", "") + return s.retain(ctx, p, false, "") } -// RetainWithAudit is Retain plus one actions_log row (FR-13), appended inside -// the same transaction before the cap is applied so the audit row is itself -// counted by the cap. An empty detail writes no audit row. This keeps the -// action log bounded even though every run records that it ran. -func (s *Store) RetainWithAudit(ctx context.Context, p RetentionPolicy, actor, detail string) (RetentionResult, error) { +// RetainWithAudit is Retain plus one 'retain' row appended to actions_log +// (FR-13). The row is written inside the same transaction before the cap is +// applied, so the cap counts it and the log stays bounded even though every +// run records that it ran. +func (s *Store) RetainWithAudit(ctx context.Context, p RetentionPolicy, actor string) (RetentionResult, error) { + return s.retain(ctx, p, true, actor) +} + +// retain is the shared implementation. audit=false writes no audit row. +func (s *Store) retain(ctx context.Context, p RetentionPolicy, audit bool, actor string) (RetentionResult, error) { var res RetentionResult tx, err := s.db.BeginTx(ctx, nil) @@ -82,7 +87,9 @@ WHERE body_text IS NOT NULL AND body_text <> '' AND received_at < ?`, } } - if detail != "" { + if audit { + detail := fmt.Sprintf("retention_days=%d max_rows=%d bodies_cleared=%d", + int(p.BodyMaxAge.Hours()/24), p.ActionsLogMaxRows, res.BodiesCleared) if _, err := tx.ExecContext(ctx, ` INSERT INTO actions_log (message_id, action, actor, detail) VALUES (NULL, 'retain', ?, ?)`, nullString(actor), detail); err != nil { diff --git a/internal/store/retention_test.go b/internal/store/retention_test.go index 50a08dd..c6846ba 100644 --- a/internal/store/retention_test.go +++ b/internal/store/retention_test.go @@ -130,7 +130,7 @@ func TestRetainWithAuditCountsItsOwnRow(t *testing.T) { } } - res, err := s.RetainWithAudit(ctx, RetentionPolicy{ActionsLogMaxRows: 3}, "scheduler", "bodies_cleared=0") + res, err := s.RetainWithAudit(ctx, RetentionPolicy{ActionsLogMaxRows: 3}, "scheduler") if err != nil { t.Fatalf("RetainWithAudit() error = %v", err) }