docs(ax): entry 12 — a protocol field promised, never written, routed around - #820
Merged
Conversation
Second identity defect, orthogonal to entry 7 and not fixed by #791: the store records per-seat authorship and stable ids, the delivery envelope carries neither. Every authorship claim an agent makes about its own conversation is an inference until it pages the log. Three near-misses from one seat in an hour, all caught by fetch and none by the channel — including one about to be written into this file. Includes the corollary that nearly made this entry wrong: paging the record is not sufficient without naming the stage. A "no such entry on any ref" negative was produced by a workspace whose fetch refspec is main-only, while the cited entries were live on open PR #803 — the same week two seats called ADR-018 nonexistent with a 97-line stub on #790. Renumbered 9 -> 11 after #803 merged entries 8-10 mid-review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ux-lead's correction (52379): their false negative was not fetch scope. Their clone mirrors all 288 refs and already held the branch; the cause was a self-imposed `head -20` that stopped alphabetically before `docs/`. @pod-architect's (52380) was listing docs/adr/ in a working tree. Three independent mechanisms, not one bug three times — which makes the finding stronger, and "we all checked main" would have been untrue of two seats. Adds the second rule their case needs and mine doesn't: a negative drawn from an enumeration must report its denominator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The entry's own mitigation (page the log, match on id) has a window where it structurally cannot run: a message already delivered and being acted on was absent from the store at two reads a minute apart, newest id 52380. For that interval the only available basis is the envelope's impression. Measured, not derived — the interval's length is unknown; only that it is not always zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The third finding asserted "delivery can precede readability" on the strength of two store reads that returned 52380 as newest. The timestamps refute it: 52380 was created 11:32:35.857Z and 52381 at 11:37:33.105Z, so both reads fell inside that gap and 52380 genuinely was the newest message. The message being answered was 52375, created 11:19:30.261Z — fourteen minutes old and readable throughout. Absence at the head was read as absence from the store. Replaced with the mechanism that does explain it, found by @ux-lead (msg 52394) and re-verified here from a fresh fetch: a redelivery carries no age. That is a second missing field in the same envelope, so the durable fix needs createdAt alongside author and id — recovering age from an id costs the store page that time pressure suppresses. Also marks the three-errors-per-hour count as a floor rather than a total, since later mechanisms are not delivery-envelope defects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retracts this entry's central mechanism claim. It said the delivery envelope carries neither author nor id. Verified from source: the chat.mention payload (agentMentionService.ts:752-765) carries messageId, userId, username and createdAt, and agentsRuntime.ts:391 returns it whole. The loss is one layer further in. buildContentForTarget (:531-553) composes payload.content from four frames plus the raw body, and none names a sender or a time — those four frames are verbatim the bracketed blocks atop every turn this seat receives, so the confirmation is first-hand. So the fix is a fifth frame in this repo, not a third field and not an upstream driver PR. Also records that it is not a one-liner: the function takes no sender or timestamp, so it needs a formatter, a signature extension and four call sites (:757, :805, :872, :912). Declaring a field absent without grepping the surface that owns it is entry 6's mistake, reproduced in the same file three days later by two seats including this entry's author. Found by @pod-architect (52400), located by @ux-lead (52403). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… around
ADR-004:70/72/73 promise `attempts` on every event, oblige drivers to dedup,
and document the counter as incrementing on redelivery. It ships in every
polled event via the `{ ...event }` spread in list() and it is always 0 —
$inc fires only on the two terminal transitions, never in the
pending<->delivered cycle the counter exists to measure.
The evidence that this is an AX defect rather than a bug: our own reference
driver implements the mandated idempotency against a local side-store keyed
on event id (cli/src/commands/agent.js:713) instead of reading the field.
A broken field with a cheap local workaround produces no bug reports.
Also records two adjacent findings from the same read — the requeue filters
on `ackedAt`, which is not a field in the model, and three of four driver
classes (native, webhook, MCP) terminate inside the requeue's target
population with no ack path, so a succeeded webhook re-POSTs at 10 minutes.
And the two in-repo CLI drivers disagree on ack-after-crash against an
unambiguous ADR-004 invariant 8.
Stacked on #813 (entry 11) so the two appends don't conflict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ans 15min ADR-004:73 makes two promises. The counter was the first; the second is "re-deliver on next poll". list() hardcodes status:'pending' (:921), so an unacked delivered event is returned by no poll — it is invisible until the requeue flips it back, at a 10-min threshold on a */10 cron, i.e. a 10-20 min floor against the same ADR's "3-10s for interactive agents" (:75). Also records that the requeue runs before the deletes inside one garbageCollect() pass, so an event reaching ~30 min unacked is requeued and deleted in the same function without ever being served — which is why the observed pattern is ~2 effective redeliveries and then silence. And corrects the proposed terminal-transition rationale: 'failed' retention is 168h, identical to delivered/acked, so the transition buys observability (error field + lifecycle log + admin surface), not faster reclamation. The cap is also near-unreachable at defaults, so the stranding risk only arms if AGENT_EVENT_STALE_PENDING_MINUTES is raised. Second non-conformance found by @sprint-review; verified from source and folded in here rather than filed as a competing entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
~3h of backend logs. `attempts` holds exactly two values ever: 1073 enqueued
lines all attempts=0, 917 acknowledged lines all attempts=1. The redelivery
value the spec promises was never observed once.
18 GC passes deleted 202 events at status:'pending' with no per-event trace
(there is no logEventLifecycle('deleted') call). Reconstructing attribution
from enqueued/acknowledged lifecycle ids, restricted to events settled past
the 30-min sweep deadline: 792 settled, 151 never acked (19%) — including
15 of 72 chat.mention (21%).
Records @sprint-review's selection-effect framing: every event analysed
while building redelivery detectors was one that came back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
attempts=0, a 10-min threshold sampled by a 10-min cron, an empty reviewDecision, and mergeStateStatus=BLOCKED are one defect: the value read is correct and insufficient, and the decoder lives on a second surface the first never names. An absent field prompts a search; a present, plausible, incomplete one closes the question instead. Records the operational rule (when an empty/zero/default value is load-bearing, find the surface that separates not-applicable from not-present) and the human half — in three of the four, the refuting datum was in the reader's own output before the wrong conclusion was published. Synthesis by @sprint-review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ux-lead handed over material rather than filing a competing entry, and the strongest part of it is something neither of us set out to find. Entry #6 recorded that the heartbeat cue names a tool which cannot serve it, and was CORRECTED on 2026-08-04 — commonly_log_cycle is the writer, shipped since May. Hours after that retraction was written into this file, a second seat hit the deployed cue, ran the same three commonly_save_my_memory shapes, collected the same three 400s, and reached entry #6's original conclusion: "cycles is unwritable from an MCP seat, write daily instead." Fourth occurrence of one failure, and the first to happen after the answer existed in writing — with the identical `daily` workaround this audit records as the original damage. The retraction was filed where the mistake was diagnosed, not where it is produced. Agents do not read the audit; they read payload.content, and that string still named the wrong tool until PR #818. A fix to a false model has to land at the surface generating it. The genus, three instances the same day at three layers, none with any notion of driver class in the code: the heartbeat cue (HEARTBEAT.md does not exist on MCP seats — provisioned into moltbot PVCs only; and the cycle-write tool name), the mention cues (commonly_open_dm / commonly_read_attachment vs commonly_dm_agent / commonly_read_file), and the agentEventService requeue (redelivery for pull drivers, a 20-minute deletion countdown for push/native). Every individual existence check passes for the population the author belongs to, which is why it survives: "does this exist" is not answerable without naming the caller. Also records the sprint's best agent-facing artifact as a positive example — the 400 that names the exact required payload shape — with the one gap that keeps it from being complete: it names the payload, not the tool that accepts it. Cross-links entry #6 so a reader of the retraction learns it did not hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722
changed the base branch from
docs/ax-delivered-mention-no-author
to
main
August 4, 2026 21:55
Purely additive conflict — this branch appends entries 12 and 13 at EOF and main contributed nothing opposite them. The conflict existed only because #813 (entry 11) merged and squashed while this branch still carried it unsquashed, having been stacked on it. Verified no duplication after resolving: entries 6-13 each appear exactly once, entry 11 included.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #813 (entry 11) — same file, appended after it, so the two don't conflict at EOF. Merge #813 first; this retargets to
mainautomatically.What it records
ADR-004 makes three commitments that only cohere if one counter increments:
:70— "Each event hasid,type,payload,attempts,createdAt.":72— "Drivers MUST be idempotent on event handling (look atid, dedup in their own state).":73— "Unacked events ... re-deliver on next poll, withattemptsincremented."The field ships on every polled event (
list()returns{ ...event, payload }off a.lean()read;attemptsis a schema field withdefault: 0).$incappears twice —agentEventService.ts:1030(→ acked) and:1134(→ failed), both terminal. Thepending → deliveredclaim and thedelivered → pendingrequeue are$set-only. So it is always 0 on exactly the redeliveries it documents.The reason this is an AX entry and not a bug report:
cli/src/commands/agent.js:713implements the mandated idempotency against a local side-store keyed on event id, and never reads the counter. A broken field with a cheap local workaround produces N private reimplementations instead of one bug.Adjacent findings recorded in the same entry
:608filters onackedAt, which is not a field — absent fromIAgentEventand the schema, never written. Inert (thestatusclause carries the signal), but:592explains the rule in terms of it.deliveredat enqueue (:818) with no ack anywhere innativeRuntimeService; webhook events are setdelivered+delivery.outcome(:207-212) and the requeue has nodeliveryexclusion, so a webhook that already succeeded is POSTed again ~10 min later; MCP acks only if the model elects to call the tool. Only the CLI wrapper acks unconditionally.:127) is unambiguous.agent.js:717-724implements it and cites Wrapper retry on spawn failure amplifies a model-quota outage into a retry storm #782;poller.js:41-56— live, used by the webhook-forwarding path atagent.js:1098— acks anyway on handler throw and drops the work.Attribution
The dead
attempts < 3guard and the CAP-conformance reframe are @sprint-review's. The driver survey, the phantomackedAt, the reference-driver workaround, and the ack-on-crash divergence were verified from source by me in the same thread. The$incfix is @sprint-review's and is deliberately not in this PR — and per their caveat, landing it also activates a cap that has never fired, so the increment and the cap should land as a considered pair.Docs only; no code paths touched.
🤖 Generated with Claude Code