P0: Expire Stale Generated Media Sends
Problem
Generated media action results can become stale if the production bridge is down, paused, misconfigured, or restarted after a long outage. When the bridge comes back, the current drain behavior may send every pending generated voice/photo result, even if the chat moment has expired. That can spam users with out-of-context media.
This happened during the July 6-7, 2026 production incident. SPECTRUM_ACTION_OUTBOUND_POLL_MS was set to 2147483647, effectively disabling action-result draining. After restoring the poll interval to 2000, the bridge began replaying old generated media. We manually cancelled the remaining stale action-result:% media backlog to avoid further spam.
Desired Invariant
The bridge must never send stale generated media after a restart or delayed recovery. Async generated-media action results are only deliverable while they are fresh enough to still belong to the current conversational context.
Plan
1. Add an expiry policy for action-result media jobs.
- Introduce an environment variable such as
SPECTRUM_ACTION_RESULT_MAX_AGE_MS. - Start with a conservative default, for example 10 minutes.
- Apply only to
client_guid like 'action-result:%'media jobs.
2. Store or compute expiry.
- Preferred: add
expires_attophoton_outbound_jobsand set it when enqueueing action-result media. - Acceptable first pass: compute expiry from
created_at + SPECTRUM_ACTION_RESULT_MAX_AGE_MS.
3. Cancel expired jobs before they can send.
- On bridge startup.
- At the beginning of each outbound drain tick.
- Immediately before
sendOutboundContentas a final guard. - Mark stale jobs
cancelledwith a clearlast_error, such asexpired-action-result-media.
4. Scope cancellation to the bridge's owned delivery surface.
- WeChat bridge cancels only WeChat jobs it owns.
- iMessage bridges cancel only jobs for their configured line scope.
- Avoid one bridge cancelling another bridge's active backlog.
5. Preserve live behavior.
- Fresh generated voice/photo sends normally.
- Normal text replies and current inline sends are unaffected.
- Retryable provider failures remain retryable only while the job is fresh.
6. Add observability.
- Log counts on startup and each cleanup pass: fresh deliverable, expired cancelled, retryable, sent.
- Emit enough metadata to identify platform, line, job type, and age bucket without logging message content.
7. Add tests.
- Fresh action-result media drains and sends.
- Expired action-result media is cancelled and never sent.
- Retryable failed media older than the TTL is cancelled.
- Restart with an old media backlog does not replay stale jobs.
- Non-action-result media and normal outbound text are not accidentally cancelled.
Acceptance Criteria
- A bridge restart after a long outage cannot replay generated media older than the configured TTL.
- Expired media jobs end in
cancelled, notpending,sending, or retryablefailed. - The cancellation reason is searchable in production data.
- The fix is covered by focused tests around
drainCachedOutboundJobsand outbound job claiming. - The rollout includes a one-time production dry-run query before enabling cancellation behavior.