Phase 7 follow-up: 1-month body retention and actions_log cap (AUD-75)
Implements the AGENTS.md §7 data rule: keep message bodies for one month, and size-cap the append-only action log. - internal/store/retention.go: Store.Retain clears messages.body_text older than the window (metadata stays) and deletes actions_log rows beyond the newest N, in one transaction. - internal/config: EMAIL_AGENT_RETENTION_DAYS (30) and EMAIL_AGENT_ACTIONS_LOG_MAX_ROWS (50000). - cmd/email-agent: bounded -retain flag for the n8n schedule. - Tests for body clearing, metadata retention, log cap, and disabled policy. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -37,6 +37,7 @@ func main() {
|
||||
summary = flag.Bool("summary", false, "build today's inbox summary, store it, and print it; sends nothing")
|
||||
summarySend = flag.Bool("summary-send", false, "build today's inbox summary and deliver it by email via Apprise")
|
||||
notify = flag.Bool("notify", false, "send one test notification to the Hub (POST /notify) and exit")
|
||||
retain = flag.Bool("retain", false, "enforce retention (AGENTS.md §7): clear old message bodies and cap actions_log, then exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -111,6 +112,14 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if *retain {
|
||||
if err := runRetain(cfg, logger); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "retain failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := run(cfg, logger); err != nil {
|
||||
logger.Error("fatal", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -317,6 +326,44 @@ func runNotify(cfg *config.Config, logger *slog.Logger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runRetain enforces the AGENTS.md §7 data rules: message bodies older than the
|
||||
// configured window are cleared (metadata stays), and the append-only
|
||||
// actions_log is capped. It is a bounded, explicit job for the n8n schedule;
|
||||
// the HTTP service never runs it on a request. Running it twice is safe.
|
||||
func runRetain(cfg *config.Config, logger *slog.Logger) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
st, err := store.Open(ctx, cfg.DBPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
defer func() { _ = st.Close() }()
|
||||
|
||||
policy := store.RetentionPolicy{
|
||||
BodyMaxAge: time.Duration(cfg.RetentionDays) * 24 * time.Hour,
|
||||
ActionsLogMaxRows: cfg.ActionsLogMaxRows,
|
||||
}
|
||||
res, err := st.Retain(ctx, policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := st.LogAction(ctx, "retain", "scheduler",
|
||||
fmt.Sprintf("bodies_cleared=%d actions_deleted=%d retention_days=%d max_rows=%d",
|
||||
res.BodiesCleared, res.ActionsDeleted, cfg.RetentionDays, cfg.ActionsLogMaxRows)); err != nil {
|
||||
// Retention already committed; a failed audit row must not fail the job.
|
||||
logger.Error("log retention action", "error", err)
|
||||
}
|
||||
|
||||
cutoff := "disabled"
|
||||
if !res.BodyCutoff.IsZero() {
|
||||
cutoff = res.BodyCutoff.Format("2006-01-02")
|
||||
}
|
||||
fmt.Printf("bodies_cleared=%d body_cutoff=%s actions_deleted=%d actions_max_rows=%d\n",
|
||||
res.BodiesCleared, cutoff, res.ActionsDeleted, cfg.ActionsLogMaxRows)
|
||||
return nil
|
||||
}
|
||||
|
||||
// probeHealth requests the local health endpoint. It exists so the container
|
||||
// healthcheck does not need an external HTTP client in the image.
|
||||
func probeHealth(port int) error {
|
||||
|
||||
Reference in New Issue
Block a user