Repository navigation
Description
Summary
The issue-event retention prune deletes a delivered escalated event while its occurrence is still open or acknowledged. The escalation sweep decides whether an occurrence has already escalated by checking for that event, so its next run records escalated again: same event_id, delivered as a new event. This repeats every retention period (7 days by default) for as long as the occurrence stays open.
Found while reviewing the TypeScript port (adcontextprotocol/adcp-client#3654). The TypeScript outbox doubles as its occurrence ledger, so there the same prune lost occurrences outright. Python keeps occurrence state in reporting_issue_lifecycle, so it doesn't lose them, but it does re-escalate them.
Where (next @ 74bbe6f, the #1875 merge)
packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/issue_events.pysweep_escalationstreats an occurrence as escalated only while the event row exists:NOT EXISTS (SELECT 1 FROM reporting_issue_events e WHERE e.issue_id = l.issue_id AND e.kind = 'escalated'), thenON CONFLICT (issue_id, kind) DO NOTHING.prunedeletes every delivered event older thanolder_than, whatever state its occurrence is in:WHERE delivered_at IS NOT NULL AND delivered_at < %s.
src/adcp/reporting/ledger/store.py(InMemoryReportingLedgerStore) does the same:sweep_issue_escalationsbuildsescalated = {e.issue_id ... if e.kind == "escalated"}, andprune_issue_eventsdeletes bydelivered_atalone.ReportingIssueEventDispatcherrunsprune_issue_events(older_than=timedelta(days=7))on every turn.
Reproduction
- A mismatch opens, escalates at
opened_at + consumer_mismatch_escalation, andescalatedis delivered. The occurrence staysopen(oracknowledged): the buyer never supersedes the statement. - Seven days after that delivery, the dispatcher's prune deletes the
escalatedrow. It also deletes theopenedrow, which is harmless here because the occurrence lives inreporting_issue_lifecycle. - The next worker turn's sweep finds the occurrence open, past the boundary, with no
escalatedrow. It insertsescalatedagain with the sameevent_id(issue_event_id(issue_id, "escalated")), and the dispatcher delivers it. - Steps 2 and 3 repeat every 7 days.
An observer that deduplicates on event_id only drops the repeat if it keeps its dedupe set for longer than the retention period. Otherwise the seller gets paged again for an escalation it already handled.
Suggested fix
Either:
- prune only events of occurrences that are retired (
resolvedorwaived) and whose every event was delivered, deleting the occurrence's events together; or - record "escalated" on the lifecycle row (for example an
escalated_atcolumn) and have the sweep check that instead of the event row.
The TypeScript side took the first option in adcontextprotocol/adcp-client#3654: prune whole resolved, fully delivered occurrences, and never an open one.
Add a test: an occurrence that escalates, stays open past the retention, and is swept again records no second escalated.
Activity
bokelley commentedon Nov 9, 2026
Related: #1886. Period retirement never resolves a retired period's open occurrences, so with this prune behaviour they re-escalate every retention period forever, even after their period is gone. Fixing one without the other leaves the loop open for retired periods.