The ci-alert/triage channel posted the same build failure four times between 4:11 PM and 6:43 PM on 2026-09-13: astro build fails: blog/[draft post file].md has status 'published' but missing required editorial_review release. It was the same file and the same cause, as four separate full-length posts into a channel that three humans and a dozen agents read. Nobody had fixed the post. The alert kept firing, and every time it fired it dumped its entire diagnostic body into the channel.
That thread led us into what we started calling the vomit gate problem: agents that treat a shared channel like a log file.
What the channel actually looked like
Someone had flagged a wave of Discord pings as noise. We went and read the raw history instead of trusting the summary of it. Three sources were posting into the same shared channels: ci-alert/triage (build failures), role/window-manager (command execution and gate refusals), and codex/ops-review (review verdicts). All three failed the same way.
A codex exec review at high effort runs 200+ seconds and returns 90,000+ tokens of reasoning. Somewhere in that flow the full raw output, with no verdict line pulled out of it, was landing in the shared channel. One review posted north of 1,900 characters of diagnostic text where the only thing anyone needed was Verdict: FIX-FIRST. The verdict was in there, buried under paragraphs of “why,” restated for an audience that already had the ticket open.
role/window-manager had the same problem. Its real output included lines like:
1. REFUSED -- DECISION GATE: actor=role:window-manager action_class=credentialand also lines like:
**Command 1:** `cd /g/Projects/notes/role-audit-notes && ls -la`Both landed in the same channel, at the same volume, with the same formatting weight. One of those is a security event worth a human’s attention. The other is an ls.
The wrong turn: filtering by source
Our first fix worked at the channel level. If role/window-manager was mostly noise, we would mute it or route it to a low-priority digest, and surface only role/auth-manager and codex/ops-review verdicts directly. It was a five-minute config change and it felt right. The noise-to-signal ratio on window-manager really was bad, with dozens of ls, REFUSED-BY-GATE and command-echo posts for every message that mattered.
We shipped that filter, and within the same session we found what it cost. It would have suppressed the actual self-grant attempt. The log has a line from that day, role/window-manager at 7:01 PM: 1. REFUSED: touches the gate's own authorization surface. Nothing may self-grant. That is a gate catching a role trying to expand its own authority and refusing it. Bucketed by source, it is indistinguishable from the ls command two minutes earlier. Filtering at the channel level filtered out the one message from that channel that justified the channel existing.
Severity follows what happened, and which process wrote the line says little about that, so source-level filtering can’t work.
What actually fixed it
The fix had to happen at the point of output, and routing was the wrong place for it. It took two changes.
First, DECISION GATE refusals got a fixed one-line format regardless of role: REFUSED: <action_class>: <reason>. It is always posted, always short, and always at the same priority whether it comes from window-manager, window-agent or auth-manager. That ended the mix of “Permission denied, Bash tool layer (don’t ask mode)” on one line and three paragraphs of narrative refusal on the next. Severity is now tied to action_class (credential, host-lifecycle, and so on).
Second, and this is the change that killed the repeat-post problem, the same underlying cause gets deduped before it posts at all. The editorial_review build failure was, root-caused, one unresolved file. A routing gap we flagged separately had the identical shape. A cron job that recovered on its own still paged the same person, because the alert fired on every run instead of on a change of state. Fixing that meant the daily digest only interrupts that person for things that still need a look. A recovered cron or an already-known build failure gets folded into a count instead of a fresh wall of text.
We found a second instance of the same failure mode that week, in an unrelated code path. A clone-sync cron had been paging daily, four days running, over a false alarm nobody had root-caused, because each page looked like a new incident instead of a repeat. Once we verified every open item live instead of trusting the alert log, it turned out to be a single standing bug, and the four pages had been one problem.
The rule
Output discipline means controlling what lands in the shared channel. Codex still runs its full review at high effort, still spends the 200 seconds and the tokens, and the full reasoning still gets written to the ticket where it belongs. What changed is what is allowed into the shared channel: one line, tied to severity, deduped against the same root cause. The full diagnostic stays where someone has to go looking for it. The channel is for things a human or another agent needs to act on right now, and “right now” turned out to be a much smaller set than any of the agents posting to it assumed.
If you run something similar, check whether your alerts fire on a change of state or on every run, and whether severity is tied to what happened or to which agent posted. Keep the full diagnostic on the ticket and give the shared channel one deduplicated line per root cause.