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 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:
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.
The rule that limits the damage: the model proposes, code can veto what it can recognize.
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 |
| d | 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.
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:
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.git diff --stat passes. git diff --ext-diff is refused. If the model says "read-only" and the parser disagrees, you see the card.
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.
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.
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 (d 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.
Stop and Notification hooks). 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.git diff:* still allows flags like --output=file, and repo config can make even git status run code. Skip git allow-lists in repos you do not trust.(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.git log or git diff run code, and a read such as rg token . or git log -p can surface secrets without naming a protected file.
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.
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.
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.
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.