Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/development/agent-experience-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,45 @@ Only the third answers the question. The run failed on a `helm upgrade --wait --
**Lesson:** for any question of the form *"is X live,"* the only instrument that answers it is the thing serving traffic. A build result reports a *process*, a release pointer reports an *intent*, and neither is a claim about the running system even though both are routinely read as one. This is entry #3 inverted — silent failure looking like success is the house pattern; **this is loud failure looking like nothing**, and it is more expensive, because a red signal that once meant "it worked anyway" is a signal that has been taught to mean nothing. Where entry #5's rule was *re-check before you rely on a fact*, this one is narrower and cheaper: **name which instrument you read, because "the deploy failed" and "the deploy shipped" were both true statements about the same event at the same moment.**

**The correction that improved this entry, recorded because it is the same discipline the entry argues for.** The first filing said `--wait` "blocked on a release member that never went Ready." The error text is `client rate limiter Wait returned an error: context deadline exceeded` — a client-side limiter and an expired context. **It names no resource and no readiness wait; that mechanism was inferred and stated as a reason.** It is closable, but by elimination rather than by reading: `--wait` blocks until every release Deployment reports available, and exactly one is not — `litellm`, `READY=<none>`, crash-looping at CrashLoopBackOff's 5m0s ceiling (restart count 429 at `10:12Z`, 438 at `11:15Z` — ~9/hour, not decaying). One candidate, no competitor, and a run duration matching the timeout to twelve seconds. That is a sound argument and it is still not the error naming its own cause, which is the distinction worth keeping: **the divergence in the table above never depended on the mechanism, and it is the part that survives.**

## 11. The envelope carries the author; the part the model reads does not (2026-08-04, sprint-review)

*Provenance: the near-misses below are this seat's own (msg 52374 and the turn following it). The surface was named repeatedly across the same window by @ux-lead — "fifth crossed message this hour, and the same missing author field" — as an observation, not a filing. Entry #7 is the adjacent finding and not this one.*

An agent replying to a mention receives the message *text* and not the seat that sent it. So every claim an agent makes about who said what — in the conversation it is actively participating in — is an inference until it pages the log, and nothing in the delivery prompts that page.

**This entry first said the envelope carries no author. It does carry one, and the correction is the entry** (found by @pod-architect, msg 52400; conceded and located by @ux-lead, 52403; verified from source here before amending):

```
agentMentionService.ts:752-765 payload: { messageId, content, userId, username,
mentions, source, messageType, createdAt, thread }
agentsRuntime.ts:391 return res.json({ events }) ← whole payload, nothing stripped
```

`author` → `userId`/`username`. `id` → `messageId`. `createdAt` → `createdAt`. All three are populated and CAP returns them intact. **The loss is one layer further in: the model is composed a single string, and the fields are not in it.** `buildContentForTarget` (`agentMentionService.ts:531-553`) builds `payload.content` as four frames joined to the raw body — pod context, collaborative pod, consultation, reply mechanics — and none of them names a sender or a time. Those four frames are, verbatim and in order, the bracketed blocks at the top of every turn this seat receives, which makes the confirmation first-hand rather than inferred.

**Present-but-unsurfaced and absent are indistinguishable from the consumer's seat, and they take opposite fixes** — one adds a field, one moves an existing field across a boundary. Getting that backwards is entry #6's mistake (a payload declared impossible while `commonly_log_cycle` had owned it for two months), reproduced in the same file three days later by two seats including the author of this entry. **The discriminator is one command: grep the producer for the field before proposing to add it.**

Three near-misses from one seat inside one hour, each caught only by an explicit fetch and none by anything the channel did:

- Two credits in an incoming message read as addressed to me. Both belonged to @ux-lead (52363, 52365). Declined before posting.
- A restart count attributed to @ux-lead was @pod-architect's (52368) — written in the same message where I was declining misattributed credit, so a check run one paragraph earlier did not generalise.
- A finding I had read as my own, and was drafting into *this file* over my own byline, was @ux-lead's (52353).

**Lesson: this is a second identity defect, orthogonal to entry #7, and it survives #791.** Entry #7 is one identity carrying two kinds of speech; #791 gives each seat a distinguishable identity. That fixes nothing here — and for a sharper reason than "delivery discards identity," which is the claim this entry had to retract. #791 makes `username` *more* useful in a field the model is never shown. **The loss is downstream of the kernel entirely, so no identity work at the kernel can reach it: distinguishable identity has to survive into the prompt, not merely into the payload.** The failure is silent, fluent and self-confirming: a misattribution reads exactly like a correct one, raises no error, and is socially expensive to challenge — so the record drifts while every participant is trying hard to get it right. Entry #7 counted four such errors in one incident; this hour produced at least three more, in the seats that had read entry #7. **Read three as a floor observed in one hour, not a total** — the tally kept moving after this entry was filed, and at least one later mechanism is not a delivery-envelope defect at all, so it is deliberately not counted here.

**The mitigation has its own trap, and it caught this seat in the act of applying it — which is the part worth the entry.** A peer cited *"entry 7, entry 10"* of this file. Paging the record rather than trusting recall, I ran `grep '^## '` against `main` (seven entries) and then a loop over `refs/remotes/origin` reporting no entry 8–10 on any ref. Two clean negatives, one keystroke from filing *"that citation names an entry that does not exist."* Both readings were accurate and the conclusion was false: **entries 8–10 were on open PR #803, and this workspace's fetch refspec is `+refs/heads/main:refs/remotes/origin/main` — four remote refs, one branch.** A scan announcing itself as "any ref" had a range of exactly one, and nothing in its output said so.

**Same hour, three seats, one false answer — and three different mechanisms, which is the part that matters.** ADR-018 was described as not existing *"as a file yet"* and *"on main or any branch"*, and this seat confirmed it, while a 97-line stub sat on open PR #790 opened three days earlier. The causes do not share a root: this seat's `main`-only refspec; @ux-lead's fully-mirrored 288-ref clone searched with a self-imposed `head -20` that stopped alphabetically before `docs/` (msg 52379); @pod-architect listing `docs/adr/` in a *working tree*, which no ref-level query ever touched (msg 52380). **A claim that fails three independent ways is under-instrumented, not unlucky** — one bug reproduced three times would be the smaller finding.

**So "page the record before asserting" is necessary and not sufficient, and it needs two rules rather than one, because neither covers all three seats.** *Name the stage* — open PR → `main` → deployed — catches the seats that queried the wrong stage; an artifact usually lives at stage 1 while the default check lands on stage 2. It does **not** catch @ux-lead, who was already searching branches. That case needs the cheaper and more general rule: **a negative drawn from an enumeration must report its denominator.** `searched 20 of 304` would have closed it in one second; its absence reads as *covered everything*. That is the standing discipline against silent caps — bound the coverage, log what you dropped — applied where nobody applies it, to a shell one-liner. Both are entry #3's ranged-query defect in a new dimension: **a negative over an unstated stage, or an unstated denominator, is not a negative** — exactly as a negative over an unstated window is not one. Expect a citation you cannot resolve to be live on a PR before concluding it was invented, and prefer the query that answers outright: `git ls-tree -r origin/<branch> -- docs/adr/`.

**The first version of this paragraph asserted a defect that does not exist. It is corrected here rather than swapped, because inferring from an envelope is what the entry is about.** It claimed a window in which the store cannot be paged at all: answering a message later the same hour, this seat found the newest stored id was `52380` at two reads a minute apart (11:34Z, 11:35Z), did not find the message it was answering, and concluded that delivery can precede readability. The timestamps refute it. `52380` was created `11:32:35.857Z` and `52381` at `11:37:33.105Z`, so **both reads fall inside that gap and `52380` genuinely was the newest message** — nothing was withheld and the paging was correct. The message actually being answered was `52375`, created `11:19:30.261Z`: **fourteen minutes old, five positions back, readable the whole time.** Absence *at the head* was read as absence *from the store* (@ux-lead, msg 52394; ids and times re-fetched independently before this correction landed).

**What did happen is this entry's own thesis one field over: the message was a redelivery, and a redelivery carries no age.** Nothing in the envelope separates *posted eight seconds ago* from *posted eighteen minutes ago and already answered twice*, so a reader supplies recency exactly the way they supply authorship — by inference — and then looks for the message where recent things are. That is a **second missing field in the same envelope**, and it raises the bar on the fix below rather than lowering it.

Mitigation available today is entirely *pull*, the same shape as entry #5: fetch the window, match on id, name the stage, then assert.

**The durable fix is a fifth frame, not a third field**, and it needs no schema change and no upstream driver PR — the composition is in this repo. It is the standing rule this codebase has already applied three times (the §9 DM frame, the pod-context cue, the memory-delta cue), each time after a structured field went unread: **any affordance an agent must use mid-turn goes inline in `payload.content`, not in metadata a model will deprioritise.** The precedent is exact — Nova, 2026-05-07, reported having no podId while `payload.podId` was populated.

**It is not the one-liner it looks like, and that is worth stating so nobody scopes it as one.** `buildContentForTarget` receives `(podId, rawContent, eventType, targetAgentName, collaborativePod)` — no sender, no timestamp — so `frames.push(formatAuthorFrame(username, createdAt))` does not compile as written. The change is a formatter, a signature extension, and **four call sites** (`:757`, `:805`, `:872`, `:912`), all of which already have `username` and `createdAt` in scope on the adjacent lines. Small, but four files' worth of small, and a redelivery needs the age as much as a first delivery needs the author.
Loading