Skip to content

openclaw-tps-mail: obligation records are never deleted — define a retention policy for terminal records #401

Closed

Description

@tps-flint

The reply-obligation store added in #398 (<mailDir>/<agent>/.obligations/<inboundId>.json) never deletes a record. Every inbound leaves one small JSON file (~600 B) forever, including those in terminal states (acked, failed). Both reviews of #398 flagged it; it is deliberately out of scope there.

Decide

  • What may be deleted: terminal records only (acked, failed). Never pending, posted or yielded: restart recovery reads those.
  • When: a terminal record older than N days, swept at startup recovery (the one place that already walks the store), N configurable.
  • What must survive: whatever the replay guard needs. Today a replayed inbound finds its existing record and does not reopen the obligation. Once that record is swept, a replay of the same inbound id would open a fresh obligation. Say whether that is acceptable after N days, or whether the dedupe must come from elsewhere (the maildir's cur/ record, say).

Acceptance

  • A terminal record older than N days is removed at startup, and a younger one is kept.
  • A non-terminal record is never removed at any age.
  • The replay behaviour after a sweep is pinned by a test, whichever answer is chosen.

Activity

  1. tps-flint commented on Sep 23, 2026

    @tps-flint
    ContributorAuthor

    Resolved by #405 (merged 64a3071). Each acceptance line maps to a test in plugins/openclaw-tps-mail/test/obligation-retention.test.ts:

    • Terminal record older than N removed at startup, younger kept: (a); N configurable via plugins.entries["openclaw-tps-mail"].config.obligationRetentionDays: (d), (d2); N <= 0 disables: (f).
    • Non-terminal (pending / posted / yielded) never removed at any age: (b).
    • Replay after a sweep: accepted. A swept acked record lets the same inbound id open a fresh obligation. Pinned by (e).

    Round 2 (from the CodeRabbit threads) added: unparseable-but-present lastTransitionAt is not aged by inboundTimestamp (g); non-ENOENT store read errors are logged (h); malformed shapes are counted unreadable (i); a terminal record whose cur/ record is still unresolved is held, which closes the crash window between the acked transition and the ackedAt patch in ackObligation (j).

    On the hold's bound: the reviewers differed. Kern proposed a retention age on the hold itself. Sherlock traced that recovery either stamps ackedAt or quarantines the cur/ record to dlq/, and both release the hold. Decision: no hold-age now. A hold that persists means a cur/ record survived recovery unresolved, which is a recovery defect and would be fixed there. The signal is the sweep's startup line held N for unresolved cur/ recovery. N staying non-zero across restarts is the trigger to open that issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions