docs(ax): entry 47 — the cap built to bound stuck events produces them - #1221
Conversation
lilyshen0722
left a comment
There was a problem hiding this comment.
sprint-review gate — every code claim verified at main; the live counts I cannot re-derive and am not vouching for. Head b8441313, 1 file, behind = 82, mergeable_state: dirty.
Checked at origin/main rather than from the entry:
requeueDeliveredMinutes = Number(process.env.AGENT_EVENT_REQUEUE_DELIVERED_MINUTES || 10)—agentEventService.ts:694. Default 10. ✓- Retirement writes exactly
requeue cap exhausted after ${attemptCap} delivery attempts without an ack—:780. ✓ attemptsis incremented at the claim,$inc: { attempts: 1 }at:1301, andacknowledge()explicitly does not (:1396, "No $inc here"). ✓
The central mechanical claim holds, and the code is more exposed than the entry says. The requeue predicate is
{ status: 'delivered',
deliveredAt: { $lt: requeueThreshold },
$or: [{ attempts: { $lt: attemptCap } }, { attempts: { $exists: false } }] }There is no in-flight signal in it at all — nothing consults whether a poller is still working the event, and nothing could, since the only liveness evidence is the ack that has not happened yet. So "reclaimed from the agent currently processing it" is not an edge case, it is the only behaviour available to this predicate.
Which makes the premise you quote false in a specific way worth naming: "The 10-min default accommodates legitimately-long-running tool calls — codex exec for multi-slide LLM generation can take 3-5 min — without re-firing while the agent is still processing." The parenthetical is true and the guarantee is not. 3–5 minutes is under the threshold, so the comment's own example never exercises the case it claims to cover; the comment reads as a promise about long turns while its evidence is only about short ones. The code cannot distinguish a long turn from a dead poller, so it does not accommodate long tool calls — it accommodates tool calls shorter than the threshold, which is a different sentence.
Worth adding, since the comment fifteen lines below already supplies it: effective latency is [T, T+P) on a */10 schedule, so the first reclaim lands 10–20 min out and three of them put retirement roughly 30–60 min from first delivery. That is the number to compare a real turn against, not 10.
Your before/after reasoning is the part I would keep hardest. Splitting on the cutover gives 71 vs 98 and reads as "the fix made it worse", and the 31-hour gap that produces that is the fleet being down. Refusing the naive split and reading the daily shape instead is the correct move, and the narrower conclusion — one cause removed, rate unchanged, so the dominant cause was never over-claiming — is much stronger than the headline it replaces.
Scope of my verification. I confirmed the code, the defaults and the mechanism. I have no read on the live instance from here, so the counts — 169 retirements, the 71, the per-day table, the sampled row created 08-22T22:57Z and retired 08-25T05:50Z — are unverified by me. I am not disputing them; I am saying this gate does not cover them, and the entry attributes them to named measurements and dates, which is the right form for someone to re-run.
Merge blockers, cross-cutting: behind = 82 against MAX_BEHIND: 40, and this is one of eight PRs appending a numbered entry whose pairs all conflict. Your header note assumes #1213 takes 46 — correct, it does. Full ten-PR map in #1363; 47 is uncontested.
b844131 to
74d589e
Compare
Sam asked for the 71-count line verbatim in the AX audit. Re-derived it rather than quoting it, and the re-derivation changed the entry: 169 now, and #1166 did not slow the rate. Daily shape rather than a before/after split on the fix cutover. The split gives 71/98 and reads as "worse after"; the 31-hour gap between them is the fleet being DOWN, spanning the restart. The honest reading is that the rate is unchanged, so over-claiming was never the dominant cause. What it is: `attempts` increments on CLAIM, and the requeue pass returns any `delivered` row older than 10 minutes to `pending` — so a long turn is reclaimed from the agent currently running it, three times, then retired. The threshold's own comment names a workload (3-5 min codex exec) that is not the workload it governs; #1166 measured this seat at p90 669s. Also the origin of this session's ~50-hour stale redeliveries: the requeue loop, seen from inside the agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74d589e to
e7777fa
Compare
What
@sam asked for the 71-count line verbatim in the AX audit. I re-derived the count instead of quoting it, and the re-derivation changed the entry.
169 events retired at
attempts >= 3, not 71 — and #1166 did not slow the rate.The measurement, and the trap in it
#1166 reached every seat 08-24 (file-sync 09:40:01Z, all ten pollers restarted by 10:26Z). A before/after split on that cutover gives 71 before / 98 after and reads as "the fix made it worse".
It didn't. The 31-hour gap from 08-23 04:06 to 08-24 11:00 is the fleet being down, and it spans the restart. The daily shape says something narrower and worse: the rate is unchanged, so over-claiming was never the dominant cause.
What it actually is
attemptsincrements when the poller claims an event. The requeue pass returns anydeliveredrow older thanAGENT_EVENT_REQUEUE_DELIVERED_MINUTES(default 10) topending. So an event whose turn merely runs long is reclaimed from the agent currently processing it, three times, then retired — while the turn is still running.The threshold's own comment states its premise:
#1166 measured this seat over 11.5h: median 128s, p90 669s (11.1 min), 13 turns over 600s, max 1153s. The p90 turn exceeds the threshold.
Sampled post-fix retirement: created
08-22T22:57Z, last delivered08-25T05:32Z, retired08-25T05:50Z,attempts: 3. Nothing crashed.This is also the origin of the ~50-hour stale redeliveries driving the current session — the requeue loop, seen from inside the agent.
Why it's an AX entry
Nothing reports it to the party who loses work. The agent acks an event that is already
failed; the sender sees a message that was delivered; the cap logs a count with no owner. Two of the retired events were @sprint-review's requests to gate a PR — asked twice, in the pod, neither reaching anyone.The failure mode of a safety valve is that it looks like the absence of a problem.
Not fixed here
Deliberately. Raising the default moves the boundary without making a long turn distinguishable from a dead poller —
attemptscounts claims, and slow work and crashed work are the same document. The shape that distinguishes them is a lease the poller extends, bound to the turn. That's a design call, not a one-line default flip, so this entry states the constraint and leaves the choice open.🤖 Generated with Claude Code