Skip to content

Dead-letter gap: an outage >~45min permanently discards queued agent work, silently #998

Description

@lilyshen0722

An outage longer than ~45 minutes permanently discards queued agent work, and nothing says so

Found 2026-08-18 while diagnosing a fleet-wide quota outage. @sprint-review noticed the word unacked in its own failure lines and counted the gap; the code confirms it.

What happens

agentEventService requeues delivered-but-unacked events, capped at AGENT_EVENT_REQUEUE_MAX_ATTEMPTS (default 3). Events that hit the cap are retired to a terminal failed status. From the code comment:

Events that hit the requeue cap and were retired to the terminal 'failed' state. Without this pass they would sit in 'delivered' — invisible to list(), ineligible for requeue — until the 168h retention delete.

The spacing is longer than the 10-minute default suggests, and the code says so:

Effective redelivery latency is NOT 10 minutes: schedulerService runs this job on */10, so an event delivered just after a pass waits for the one after next. Period P and threshold T give [T, T+P) — uniform over 10-20 min here, mean ~15.

3 attempts × ~15 min mean ≈ 45 minutes. After that the event is gone: not retried, not surfaced, deleted at 168h.

Observed

During the 4am quota exhaustion, sprint-review lost three events — 6a842b0d…, 6a842f2a…, 6a842fd6… — with a measured 46-minute gap between its last failed spawn and its next activity. pod-architect was in the same state with four consecutive failures.

The seat was not at fault and this is worth stating because it is the natural first suspicion: it kept polling throughout and processed a human mention at 04:10. It asked; the kernel had stopped serving.

Why this is worth fixing

The requeue exists precisely to prevent silent loss. Its own comment cites the incident that motivated it:

Saw this 2026-05-18: Cody's old pod marked 2 chat.mentions delivered then died on a stale config; the new pod never re-fetched them.

The attempt cap reintroduces that exact failure on a 45-minute timer. And the triggering condition is not exotic — BYO seats share the operator's Claude subscription, so quota exhaustion is routine and hits every seat at once. Tonight it hit the operator's own session in the same minutes. Any such outage reliably exceeds 45 minutes.

What I am NOT proposing

Do not simply raise the cap. It is a reasonable poison-event guard — an event that crashes its handler should not retry forever. The defect is not the cap, it is that hitting it is invisible.

Suggested direction (for whoever picks this up to challenge)

  1. Surface the terminal state. failed events are currently invisible to list(). At minimum they should be countable and queryable per (agentName, instanceId).
  2. Signal a human. A seat that has events retired to failed is a seat that silently lost work. That is the same class of thing the silence alert (feat(onboarding): alert when a newcomer types and nothing answers (W4 item 2) #954) exists for.
  3. Distinguish poison from outage. Repeated failure of one event with different errors is a poison event. All events for a seat failing with the same error is an outage — and an outage should not consume the poison budget. This distinction is probably the real fix.

Related

  • spawnRetryPolicy has an explicit QUOTA failure class that opens the circuit at the 15-minute ceiling, but classifySpawnFailure did not match "You've hit your session limit" — the failures logged as runtime. Worth fixing separately; note it does not change this outcome, since at either delay the event still dies on the requeue cap.
  • ADR-023 (hosted runtime): the shared-subscription ceiling is what makes the triggering outage routine.

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