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. Same file, same cause, four separate full-length posts into a channel that three humans and a dozen agents read. Nobody had fixed the post. The alert just kept firing and kept dumping its entire diagnostic body into the channel every time it fired.

That was the thread that led me into the vomit gate problem: agents that treat a shared channel like a log file instead of a conversation.

What the channel actually looked like

My partner had flagged a wave of Discord pings as noise, so I 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 had the same failure mode. A codex exec review at high effort will run 200+ seconds and return 90,000+ tokens of reasoning, and somewhere in that flow the full raw output, not a verdict line, 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. It was just buried under paragraphs of “why,” restated for an audience that already had the ticket open.

Same pattern on role/window-manager. Real output included things like:

1. REFUSED -- DECISION GATE: actor=role:window-manager action_class=credential

and also things like:

**Command 1:** `cd /g/Projects/vault/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

My first fix was channel-level. If role/window-manager was mostly noise, mute the channel or route it to a low-priority digest and only surface role/auth-manager and codex/ops-review verdicts directly. That’s a five-minute config change and it felt right, because the noise-to-signal ratio on window-manager really was bad, dozens of ls, REFUSED-BY-GATE, and command-echo posts for every one message that mattered.

I shipped that filter and within the same session found the cost of it: it would have suppressed the actual self-grant attempt. There’s a line in the log from that day, role/window-manager at 7:01 PM: 1. REFUSED: touches the gate's own authorization surface. Nothing may self-grant. That’s not noise. That’s a gate catching a role trying to expand its own authority and refusing it. Bucketed by source, it’s indistinguishable from the ls command two minutes earlier. Filtering at the channel level meant filtering out the one message from that channel that justified the channel existing.

Source-level filtering doesn’t work because severity doesn’t correlate with which agent or role posted the message. It correlates with what happened.

What actually fixed it

The fix had to happen at the point of output, not the point of routing. Two changes:

First, the DECISION GATE refusals got a fixed one-line format regardless of role: REFUSED: <action_class>: <reason>, always posted, always short, and always at the same priority whether it came from window-manager, window-agent, or auth-manager. No more “Permission denied, Bash tool layer (don’t ask mode)” on one line and three paragraphs of narrative refusal on the next. Severity got tied to action_class (credential, host-lifecycle, and so on), not to which process wrote the line.

Second, and this is the one that actually killed the repeat-post problem: the same underlying cause got deduped before it posted at all. The editorial_review build failure that fired four times in three hours was, root-caused, one unresolved file. The routing gap my partner had flagged separately, where a cron job recovering on its own still paged them, had the identical shape: the alert fired on every run, not on state change. Fixing that meant the daily digest only interrupts them for things that actually still need a look, and a recovered cron or an already-known build failure gets folded into a count instead of a fresh wall of text.

There was a second instance of the exact same failure mode found the same week, 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 of the same one. Once I went and verified every open item live instead of trusting the alert log, it turned out to be a single standing bug, not four.

The actual rule

Output discipline isn’t “make agents talk less.” 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’s allowed to land in 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.