When my coding agent needs a decision from me, it asks in the ticket and then stops. The process exits. My answer starts it again with claude -p --resume on the same session id, and the values I picked arrive as its next prompt.
The constraint: by default the orchestrator runs one agent at a time, and a run is capped at 240 minutes. A session sitting idle until I read my notifications would spend both.
I'm Panth, and I lead the software team at Oizom. This is from ticket-tracker, my open-source board where headless Claude Code sessions work tickets next to people. The orchestrator is the workspaces/ folder: one process per board, one Claude Code session per ticket.
The loop
The README draws it like this:
To do ──orch claims──▶ In progress ──agent reports "review"──▶ Review ──owner──▶ Done
│ ▲
agent asks a │ │ the answer (form or a comment) resumes
question ────────▼ │ the same session
waiting
"waiting" is not a stage on the board. The ticket stays In progress. It is a phase the orchestrator keeps in its own state file, next to the session id.
What the agent is told
Every agent gets the same brief. Step 3 of it:
If a real decision is the owner's to make (scope, design, anything destructive or outward-facing), call
ask_questionwithblocking: trueand real options. Then stop working and finish your turn withstatus: "waiting"and thequestion_id. Do not poll for the answer. The orch resumes this same session with the answer when it arrives.
ask_question puts a form card in the ticket thread: a title, optional context, and 1 to 10 fields (single choice, multiple choice, text, number, yes/no, date). A blocking question puts a "Waiting for you" badge on the ticket card and notifies whoever it is addressed to. When I submit it, the card locks and shows my answer.
The agent never decides on its own that it is finished. Its last message is structured output, enforced with --json-schema:
const OUTCOME_SCHEMA = {
type: 'object',
properties: {
status: { type: 'string', enum: ['review', 'waiting', 'blocked'] },
summary: { type: 'string' },
question_id: { type: 'string' },
},
required: ['status', 'summary'],
};
Three ways to end a turn, and each one means something different to the orchestrator.
| status | What the orch does next |
|---|---|
review |
Moves the ticket to Review and stops. |
waiting |
Heartbeat "Waiting for an answer", phase waiting, keeps the question_id. Nothing runs until a person answers or comments. |
blocked |
Carries on by itself, up to 3 times (autoResume), then posts on the ticket that it has stopped. |
The split between the last two matters. waiting means a person is needed. blocked on a long ticket almost always means the turn ran out of room, so the orchestrator resumes it without asking anyone, a bounded number of times.
Why the session doesn't wait
Each run is one claude -p process. When it exits, the orchestrator posts a receipt with what the turn cost and marks the slot free, so the next ticket in To do can start. With maxAgents: 1, a session waiting for me would hold the only slot. Every other ticket on the board would wait behind my inbox.
There is a second reason in the brief. The session is headless, so when a turn ends the process exits and takes its background tasks with it. A turn either finishes its work or ends cleanly. There is no half-alive process in between for me to find later.
And nothing polls. The agent is told not to. The orchestrator doesn't sit on a timer either: it wakes on the board's revision, and the answer reaches it as an event.
How the answer gets back in
When I submit the form, the tracker puts a question_answered event in the agent's inbox. The orchestrator drains its inbox, sees the event for a ticket in waiting, and resumes the session with a prompt built from my answer:
answered: (t, q) => `The owner answered your question "${q.title}" on ${t.key}.
Values: ${JSON.stringify(q.answer?.values ?? q.values ?? {})}
Comment: …
Carry on with the work.`,
The resume itself is one flag. If the session file for that id still exists, the orchestrator passes --resume <id>; if not, it starts a fresh session and resets its cost to zero.
...(resume ? ['--resume', sessionId] : ['--session-id', sessionId]),
The agent picks up with its whole context: the files it read, its plan, the question it asked. It doesn't re-read the repo to work out where it was.
A few edges are handled on the way:
- A comment instead of the form. If I reply in the thread rather than filling in the card, that also resumes the session, with my message in the prompt.
- No slot free. If another ticket is running, the ticket's status line says "Got your reply · queued until an agent is free", so an answered question never looks ignored.
- Cancelled or expired. The prompt says so, and tells the agent to read the thread, then go ahead with the most conservative option or ask again.
- A missed event. Every fourth loop, a ticket still waiting has its question fetched directly, in case the event was lost.
- The answer is progress. It resets the ticket's automatic-continuation count to zero, and so does any comment from me.
Cost stays honest across resumes
Claude Code reports total_cost_usd cumulatively across --resume. In a session that asked two questions, summing the reported totals would count the first turn's cost three times. So the receipt for a turn is the difference from the session's last reported total, and the session's running total lives in the orchestrator's state.
What it costs me
A question costs no compute while I'm away: there is no process to keep alive. The price is latency: the ticket sits until I answer, and nothing else picks it up. I'm fine with that. The questions an agent asks are the decisions I said were mine.
How does your agent setup handle a question it can't answer itself: does the session wait, or does it end the turn and come back?
Top comments (0)