Incident Report: Generated Media Send Outage
Summary
On July 6, 2026, generated voice/photo action results stopped sending from production bridges even though media generation webhooks continued to complete. Text replies continued to work.
The immediate cause was a production Railway environment variable:
SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647
That value is about 24.8 days. The bridge accepted it as a valid finite integer and used it as the interval for the async action-result outbound drain. In practice, this disabled generated-media result sending.
The outage began immediately after the production bridge services redeployed around 2026-07-06 02:06 PDT. Those deployments picked up the bad environment value. The value is not present in the repo, and the code default is 2000 ms.
Impact
- Affected: generated media sends from async action results, primarily voice and photos.
- Unaffected: text replies and inline send paths.
- Platforms affected: iMessage and WeChat bridge sends.
- User impact: generated media was acknowledged/generated but not delivered; after recovery, stale backlog began to replay until manually cancelled.
At 2026-07-07 00:02:46 PDT, there were 372 actionable stale generated-media jobs left:
- 322 pending
- 50 retryable failed
- 0 currently sending
At 2026-07-07 00:04 PDT, 366 remaining stale action-result:% media jobs were manually cancelled to avoid out-of-context sends. The delta from 372 to 366 was due to live bridge drain movement between measurement and cancellation.
Earlier stale cleanup had already cancelled 256 jobs with:
stale-cleanup 2026-07-06: pending>30m, no active sending chain
Timeline
All times are America/Los_Angeles unless noted.
- 2026-06-28 13:17 -
SPECTRUM_ACTION_OUTBOUND_POLL_MSand the action-result drain were introduced with a code default of 2,000 ms. - 2026-07-06 01:53 - Last successful generated iMessage voice send observed before outage.
- 2026-07-06 01:58 - Last successful generated WeChat voice send observed before outage.
- 2026-07-06 02:02 to 02:06 - Production bridge services redeployed for commit
51700a4(Replace dead Railway-deploy Action with test-only CI). - 2026-07-06 02:08 - First generated-media outbound job after the redeploy failed to send and remained unsent.
- 2026-07-06 13:07 - Stale cleanup cancelled 256 old pending generated-media jobs.
- 2026-07-06 23:51 -
SPECTRUM_ACTION_OUTBOUND_POLL_MSwas set to2000on prod bridge services and the services were redeployed. - 2026-07-06 23:52 - Recovery began; bridge started draining/sending generated-media jobs again.
- 2026-07-07 00:02 - 372 actionable stale generated-media jobs remained.
- 2026-07-07 00:04 - 366 remaining stale
action-result:%media jobs were manually cancelled.
What Happened
Generated media actions are asynchronous:
1. The agent requests media generation. 2. A child app/action webhook acknowledges the result. 3. The result is inserted into photon_outbound_jobs with a client_guid like action-result:<eventId>. 4. scripts/spectrum-bridge.ts runs drainCachedOutboundJobs() on an interval. 5. The drain claims pending action-result jobs and calls sendOutboundContent().
The bridge interval is configured by:
SPECTRUM_ACTION_OUTBOUND_POLL_MS
The code default is:
DEFAULT_ACTION_OUTBOUND_POLL_MS = 2_000
But production bridge containers were running with:
SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647
Because the env parser accepted any finite number and only clamped to a minimum of zero, this value was accepted. The resulting setInterval() was effectively scheduled once every 24.8 days, so drainCachedOutboundJobs() did not run during normal operation.
Media generation itself was healthy. Prod action_events after the outage start showed generated-media action events being acknowledged:
send_voice: 753 acknowledgedsend_photo: 32 acknowledgedsend_image: 9 acknowledgedsend_audio: 7 acknowledged
The failure was downstream: generated media result jobs were created but not drained/sent.
Why It Started Then
The variable and drain code existed before the outage. The drain code was introduced on 2026-06-28 with a 2 second default. Generated media continued sending successfully until shortly before the 2026-07-06 02:06 PDT redeploy.
The correlated trigger was the bridge redeploy around 02:06 PDT:
bridgebridge-sharedbridge-yu-dedicatedbridge-wechat
Those deployments used commit 51700a4, whose message was Replace dead Railway-deploy Action with test-only CI. That commit did not change media sending or bridge drain behavior. The likely sequence is:
1. The bad Railway env var existed in production config. 2. Existing containers were likely still running from an earlier env snapshot. 3. The 02:06 redeploy recreated bridge containers. 4. New containers picked up SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647. 5. Generated media action-result jobs stopped draining.
The Railway CLI does not expose variable edit history or audit metadata. The remaining unknown is who or what set SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647, and when. That must be checked in Railway dashboard/audit logs if available.
Detection
The incident was detected by user report: image/voice generation and sending appeared down in production.
Diagnosis showed:
- Generation/action webhook side was healthy.
- Outbound jobs were accumulating as
pendingor retryablefailed. - Runtime env probes in all bridge containers showed
SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647. - Repo search found no
2147483647; the code default remained2000.
Resolution
Immediate mitigation:
1. Set SPECTRUM_ACTION_OUTBOUND_POLL_MS=2000 on all production bridge services. 2. Redeployed:
bridgebridge-sharedbridge-yu-dedicatedbridge-wechat
3. Verified live containers reported SPECTRUM_ACTION_OUTBOUND_POLL_MS=2000. 4. Confirmed generated-media sends resumed. 5. Manually cancelled remaining stale generated-media backlog to avoid out-of-context user spam.
Manual cancellation reason:
manual-cancel 2026-07-07 incident: stale generated-media backlog; avoid out-of-context resend
Root Cause
Production bridge services had an unsafe environment value for the action-result drain interval:
SPECTRUM_ACTION_OUTBOUND_POLL_MS=2147483647
The bridge accepted the value without an upper bound or startup warning. This disabled the async generated-media outbound drain for practical purposes.
Contributing Factors
- No upper bound validation for
SPECTRUM_ACTION_OUTBOUND_POLL_MS. - No startup log/alert for suspiciously large drain intervals.
- No monitor for old pending
action-result:%media jobs. - Generated-media jobs had no TTL, so stale jobs remained deliverable after recovery.
- Recovery drain could replay old media into chats where the conversational context had expired.
- Railway variable edit history was not available from the CLI during incident response.
Corrective Actions
P0:
- Implement stale generated-media expiry. See
docs/p0-action-result-media-expiry.md. - Cancel expired
action-result:%media jobs before send, including on startup and immediately beforesendOutboundContent(). - Add focused tests proving bridge restart cannot replay old generated media.
P1:
- Add upper-bound validation for interval env vars, especially
SPECTRUM_ACTION_OUTBOUND_POLL_MS. - Log effective bridge timing config on startup.
- Alert when pending
action-result:%jobs exceed age/count thresholds. - Add a prod dry-run command/runbook for stale generated-media cancellation.
P2:
- Add Railway variable change audit procedure to the production runbook.
- Consider requiring explicit deploy notes or approvals for changes to bridge timing env vars.
Status
Mitigated.
Current production bridge runtime has SPECTRUM_ACTION_OUTBOUND_POLL_MS=2000, and the stale generated-media backlog from the incident has been cancelled.