A review queue for people who run many AI agents, and the rule that keeps it safe: the model proposes, code can veto.
By Arnold Wang, Chief Software Architect at Ingeniux Corporation.
The examples and numbers come from DSH (DeepSeek Harness), an agent harness with a plugin system that I use every day. I have only run this pattern there.
Disclosure: the ideas, the data and the design are mine, taken from my own logs and design notes. An AI agent wrote the draft text under my direction, several fresh AI reviewers critiqued it, and I reviewed and approved the result.
On November 2 at 10:18 in the morning, ten of my coding-agent sessions were waiting for my next instruction, eleven messages in all (one session had two), the oldest 16 minutes old. The last one was not cleared until 84 minutes later. Without a queue, I would find that out by opening each session in turn. With one, I saw all eleven on a single screen.
Over November 2 to 6 I had at least one unanswered card in 73% of the minutes between 8am and 6pm, and fifteen at once at the peak.
The agents ran in parallel. I did not. Starting another session makes you faster only until the sessions begin waiting for you. After that the bottleneck is your attention, not the model.
The problem, and my approach
The problem is not that the agents are slow. Every agent that needs me stops and waits, silently, in its own window, and I cannot see all of them at once. Three things follow. I find out late. I open sessions only to learn what they want. And most of what they want is small: a "done" message, or a question with one obvious answer.
My approach has three parts:
- One list. Every session raises a card (one waiting message) in a single review queue, oldest first. I answer on the card and do not open the session.
- A cheap triage model. It reads each new card and proposes one of two things: "ask the human" or "I can handle this". Small cards stop reaching me.
- Code has the last word. Code vetoes anything risky before the model sees it, and the model's proposal acts only if code accepts it. The model proposes, code can veto.
The rest of the article defines the terms, describes the queue, explains why a model must not decide alone, reports what five days of real logs show, and lists where the design breaks.
Four terms
- Card: one waiting message. It is finished work, a question, a permission request, or a stalled session.
- Review queue: one list of cards from every session, oldest first.
- Triage model: a cheap model that reads each new card and answers one of two things: "ask the human" or "I can handle this".
- Rule and mode: a rule is one narrow job the triage model may do, such as "approve a read-only command". I run three: permissions, questions and done cards. Each rule is in observe mode (the model writes "would have handled this" on the card and does nothing) or auto mode (it acts and files a receipt in a Resolved by AI list, with a one-click Mark wrong). If you overrule the model, the queue stores a lesson: a short record that cards like this one are not for the model.
The rule that limits the damage: the model proposes, code can veto what it can recognize.
The queue in one paragraph
Agents push, you pull. A session that needs something raises a card instead of waiting silently, and you work one list instead of visiting N sessions. Four kinds of card cover most of the traffic:
| Card | What it means | What you do |
|---|---|---|
| Done | A session finished a turn | Read a short summary, reply or mark reviewed |
| Question | The agent asks you to choose | Pick an option or type your own |
| Permission | A tool call needs approval | See the full command, approve or deny |
| Paused | A session stopped mid-task | Resume it |
You answer on the card. Opening the session is the expensive step the queue exists to avoid. Every card appears in the queue first, and triage may resolve or hide it afterwards, so a triage outage cannot lose a card on its way in.
Why not just...
- ...allow-list safe commands in the tool's own permission settings? Do that too. It does nothing for done cards and questions, and those were nearly all of my traffic: 895 done cards and 287 questions against one permission request, out of 1,183 triaged cards (and that one was the only permission request in the whole queue).
- ...get a notification when a session stops? A notification says something happened. A card carries the decision and can be answered in place.
- ...use a dashboard, a terminal multiplexer or a supervisor agent? They show you sessions, so you still open each one to learn what it wants. A card carries the decision itself.
- ...run fewer agents? The queue turns that into a measurement. If the list only grows, start fewer sessions.
Why a model must not decide alone
The obvious design lets a language model read each card and decide whether to bother you. It is the wrong design for one reason: the text on a card was written by the agent being judged, or by something the agent read, such as a web page or an issue comment. If the model is what grants permission, the queue is only as safe as the weakest prompt injection. So code does the work that matters for safety:
- Vetoes run in code, before the model.
git push --force, a write outside the workspace, a read of.env: any one keeps the card in front of you, and those cards never reach the model. - A strict parser checks commands. No chaining, no redirects, every flag allow-listed one by one.
git diff --statpasses.git diff --ext-diffis refused. If the model says "read-only" and the parser disagrees, you see the card. - Approve or answer, never deny. A wrong "no" silently derails an agent and nobody notices. A wrong "yes" on a command is bounded by the allow-list, if the parser is right and the repo config can be trusted. A wrong "yes" on a question is only visible afterwards, and it may be irreversible, because the agent has already read it.
- Evidence has to come from you. To answer a question for you, the model must quote something you wrote, for example your instruction "use the staging database". Code checks that the quote exists word for word. It does not check that the quote answers this question. That makes it necessary, not sufficient, as the last section before the repo shows.
- Observe first, then learn from corrections. In observe mode, when you resolve a card, your action is compared with the model's note. If you overrule the model, the queue stores a lesson and keeps similar cards for you. Lessons exist only if you notice and overrule, and how "similar" is matched is itself a fuzzy spot.
- Fail toward the human. A timeout, broken output or an outage leaves the card on your list.
Two parts of this are weaker than they sound. For questions and done cards, the "low risk" call is the model's own, and code can only veto it with a word scan over the card and a scan of the end of the message. Code can say no. It cannot verify yes.
Three cards from a normal day
A permission that should never reach you. An agent runs git log --oneline -20 in its own workspace. The parser accepts it, the path is inside the workspace, no protected file is touched. It is approved and listed. rg token .env is vetoed in code and lands in your queue with a risk warning and the exact command.
A question that is a real decision. "Approve this tab-based design for the settings page?" The triage model flags that it needs a decision. It stays in your queue.
A question that is not. "Rename the helper to parseConfig? (Recommended) Yes." One option is marked, nothing risky in the wording, the action is reversible. Auto mode picks exactly that label and nothing else.
And a done card. "Renamed the helper, 120 tests pass, committed" is hidden, with a Reopen button. "Tests pass, but you must run the migration by hand" never is.
What five days say
Code alone, with no model call, kept 552 of 1,183 triaged cards (47%) for me. Auto mode ran for about a day and a half of the five days, and in that time code acted on one card the model proposed to handle.
One person, one workload, 168 sessions, no control group, November 2 to 6, 2026 (the last day partial). The queue received 1,645 cards. 1,183 of them went through triage: 895 done cards, 287 questions and one permission request. That is 72% of everything received. The rest were other card types (paused sessions, archive offers, todos) and some done cards and questions that never went through triage; I did not break that down. The "held by code" cards are the ones a veto stopped or no rule covers; I did not separate those two.
Everything below comes from one snapshot of the log, the first 1,183 triaged cards (the log kept growing while I wrote this).
| Triage funnel | Cards |
|---|---|
| Reached triage (1,183 = 552 + 5 + 626) | 1,183 |
| Kept for me by code vetoes alone, no model call | 552 (47%) |
| Skipped, or held back by a past-correction lesson | 5 |
| Judged by the triage model | 626 |
| ...of which unusable output, shown to me | 62 |
| ...of which answered "ask the human" | 511 |
| ...of which answered "I can handle this" | 53 |
| ...of those 53, acted on automatically | 1 |
I switched all three rules to auto on the morning of November 5. Before the switch, 647 cards reached triage: 282 were held by code, 2 were skipped and 363 were judged, and the model proposed handling 52 of them (14%). After it, 536 cards reached triage: 270 were held by code, 3 were skipped and 263 were judged, and the model proposed handling one (under 0.4%). That one was acted on, by code, on the model's proposal.
So "one card acted on" says mostly that the auto rules almost never fire. All 51 done-card proposals came before the switch and none after. I do not know why the rate fell (a prompt change, a stricter evidence rule, or drift), and it does not show that auto mode is safe.
Unusable output. 62 of 626 judged cards, one in ten, came back unusable: 61 of them in the first three days and one later. The log only records "the model answered something unusable", so I cannot split them into broken JSON and timeouts. Each one fell back to showing me the card, so the cost was an extra card, not a wrong action.
Waiting. Over all 1,645 cards, done cards I reviewed by hand waited a median of 1.6 minutes, and the slowest 10% waited 16 minutes or more (n = 438). Questions I answered on the card waited a median of 1.9 minutes, the slowest 10% 22 or more (n = 433). These exclude cards that were never resolved, such as two questions left overnight for about 13 hours, so they are biased low, and I have no before-and-after baseline. They describe life with the queue, not a comparison.
Agreement. Only 11 done cards gave a usable comparison, and that is too few to mean anything. The log compared the model's "I can handle this" proposal with what happened on 49 done cards. 38 of them had already been hidden by an older "nothing for you" filter, which the queue records as resolved automatically, so they say nothing about me. (Two more feedback records mark a lesson as too strict, which is why 51 done-card proposals give 49 comparisons here.) Of the 11 I handled myself, I marked 9 reviewed, which matched, and I replied to 2 that the model would have cleared. I did not read back what those replies said. The model's note was visible while I decided, so those matches are anchored, not independent. Of the 53 proposals, 51 were done cards and 2 were questions. I overruled one question proposal. The other is the story below.
Questions. Of the 287 questions that reached triage, code held or skipped 64 and the triage model judged 223. It called only 2 safe to answer. That measures its caution at least as much as the questions.
Permissions. One permission request reached triage in five days (and only one reached the queue at all), so the parser and the vetoes were exercised by one live card. They are unit-tested, not field-tested.
Most of the benefit I feel does not come from automation (unmeasured, one user). It comes from one list, from answering in place and from not seeing the empty cards.
If you start on Monday
- Count your waiting sessions once an hour. My own rule of thumb: if you cannot clear them in ten minutes, start fewer.
- Make stalls loud with a hook. Most agent CLIs can run a command when a session stops or needs input (Claude Code has
StopandNotificationhooks). Send yourself a count only: never the command, the message text or the session title, which can carry secrets or an agent-written phishing line. Make the hook exit 0 so it cannot re-trigger the agent. - Allow-list exact commands, not prefixes. A prefix rule such as
git diff:*still allows flags like--output=file, and repo config can make evengit statusrun code. Skip git allow-lists in repos you do not trust. - Ask agents for structured questions: a short question, two to four options, one marked
(Recommended). For anything risky, decide before you look at the mark, because it anchors you. Do not let anything pick it for you without a check. - If you build the tooling, set the bar for letting a rule act before you start. I had no numeric threshold before enabling auto; I just felt ready, and that is a weakness. Write one in advance: a minimum number of resolved cases, a lower bound on agreement and a bound on hidden cards you needed. With zero misses in n cases, the 95% upper bound on the miss rate is about 3/n (the "rule of three"), so you need about 300 cases to say "under 1%". That holds only for independent, representative cases, not against an adversary who picks the inputs, and only if you can actually observe a miss: a wrongly hidden card you never reopen counts as zero misses. Treat any rule that can hide a card as the most dangerous one.
Where it breaks
- Hidden cards are invisible. Putting every card in the queue first protects cards in transit. It does nothing for a wrong hide you never reopen. A periodic digest of hidden cards would help. I do not have one.
- The veto is a denylist and a word scan. Paraphrase, odd encodings or another language can slip past it.
- A path can change between check and approval (a time-of-check to time-of-use gap), for example be swapped for a symlink, and the path check does not catch it. The parser and a re-check of the stored command carry the safety.
- Untrusted repo config and indirect reads. Repo config can make even an auto-approved
git logorgit diffrun code, and a read such asrg token .orgit log -pcan surface secrets without naming a protected file. - Lessons can be wrong. One careless overrule stores a lesson, and the receipts and logs are only as trustworthy as whatever writes them.
- Hostile text reaches rules. An agent or an injected page can plant "(Recommended)" next to a bad option or write "all complete, nothing needed". A "quote from you" can be third-party text you pasted. A rule that continues a parked session when a ticket changes feeds third-party comments into an agent.
- Correlated failure and leakage. The triage model is probably from the same family as the agent, so they can fail in the same way, and card text goes to a model provider.
- The queue invites rubber-stamping. It is oldest first, with no priority, so a flood of low-value cards can bury a risky one, and a careless tap on a phone is easy.
- Drift and scale. A model or prompt change invalidates the record a rule earned. It is built for one person. Who owns a card, or may approve what, are team problems I have not solved.
The one time it acted
The only card the queue ever acted on for the triage model (code acted on its proposal) happened while I was writing this article with an agent. The agent asked me a confirmation, "Use this prompt as shown above?", with a recommended option. I had just asked to see the full prompt before deciding. Five seconds after the card appeared, the queue answered with the recommended option and filed a receipt. As evidence it quoted an earlier instruction of mine: "improve the above prompt first with a prompt tuner skill". Code confirmed that the quote existed word for word. Nothing confirmed that it answered that question.
The answer was harmless, and probably the one I would have given. It was still not my decision, and Mark wrong cannot unsend an answer the agent has already read. My earlier words authorized the topic, not that choice. The model can propose. Only something that checks the specific decision, in code, should be allowed to act.
Agents talking to each other, through you
The queue has a second use. With a companion plugin, dsh-session-link, one agent session can find another session, ask it to do something, start a new session, or offer to archive itself. Each of those requests lands in the same queue as a card, and nothing happens until I confirm it.
That puts me in the important decisions. Another session starting work, one agent giving instructions to another, and a session closing itself are critical operations, and they go through me on a card that names the session asking. Agent frameworks put hurdles in the way of agent-to-agent messages, because a message from one agent into another is a way to push instructions past the user. This design goes around those hurdles instead of removing them: the agents can cooperate, but the person grants each cross-agent action, and a started session gets the queue's default access, not a copy of the sender's.
It has the same weakness as the rest of the queue. The label that marks a message as coming from another agent can be imitated, so the card is the only safeguard, and a tired person tapping through cards is the risk. Reading another session's last reply needs no approval and covers every workspace of the app, which I accepted knowingly.
What's next (planned, not built)
The queue shows cards oldest first, and that is its biggest weakness: a flood of small cards can bury the one that matters. Two changes are next.
Priority. Code, not the card text, will rank the cards, because an agent can write "urgent" in a summary just as it can plant "(Recommended)". The signals are the card type, the risk flags from the vetoes, whether the card needs a real decision, and how long it has waited, with a slow age boost so nothing starves. Each card will show why it ranks where it does, and a switch will bring back oldest first.
Timeline. The queue does not yet record when I first saw a card, or how long an agent sat blocked waiting for me. I plan to add an append-only event log for cards and sessions, storing ids, types and timestamps but never card text. From it I can measure wait time, time to first look and blocked minutes, and use them to weigh priority by the cost of waiting.
The rollout follows the same rule as the triage rules: observe first. Record the events with no change in behavior. Show a computed priority as a badge while the list stays oldest first, and replay the 1,645 cards behind this article to see where the risky ones would have ranked. Reorder only after that looks right, behind a switch. The test is simple: the cards that block an agent or carry risk should wait less than they do today, and none should starve.
Where the numbers come from
The numbers above are the logs of dsh-review-queue, an open-source plugin for DSH. The code is in the dsh-plugins repository, and the v0.15.0 tag is the version described here.