DEV Community

Cover image for Headless Claude Code asks, exits, then resumes on the answer
Panth Patel
Panth Patel

Posted on Originally published at panth.vardayinitech.in

Headless Claude Code asks, exits, then resumes on the answer

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
Enter fullscreen mode Exit fullscreen mode

"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_question with blocking: true and real options. Then stop working and finish your turn with status: "waiting" and the question_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'],
};
Enter fullscreen mode Exit fullscreen mode

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.`,
Enter fullscreen mode Exit fullscreen mode

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]),
Enter fullscreen mode Exit fullscreen mode

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 (10)

Collapse
 
arhancanli profile image
Arhan Canli •

The resume path has two inputs that can arrive together: the form submit and a comment in the thread both resume the same session id. If I read the article right, the orchestrator drains an inbox and sees an event for a ticket in "waiting", so one of them wins. What I'd check is that moving the phase from waiting to running happens before the spawn, in the same state-file write, because two claude -p --resume <id> processes on one session file will both append to it and neither will know about the other's turn.

The fallback when the session file is gone worries me more. A fresh --session-id that gets "Values: {...} Carry on with the work" has the answer but not the question: no title, no options, no record of what it was deciding. For that branch the prompt could carry the original question card and the ticket brief, so the new session is not guessing what "yes" refers to. A cheap test: delete the session file while a ticket is waiting, answer a yes/no question, and see whether the agent can say what it said yes to.

Collapse
 
panthpatel profile image
Panth Patel •

Two good checks, and I haven't run either of them.

On the race: you read it right that the form and a thread comment both resume the same session id. What I haven't checked is whether the phase leaves waiting in the same state-file write as the spawn, or after it. That is the line to read, and a form submit plus a comment in the same second is the test.

On the fallback: the prompt does carry the question's title ("The owner answered your question ..."), so a fresh session knows roughly what it was asked. It doesn't get the options, the context field or its own plan. Adding the card and the ticket brief to that branch is cheap, and your test (delete the session file while a ticket waits, answer a yes/no, ask the agent what it agreed to) goes on my list.

Have you seen a session file go missing in practice, or is this from reading the code path?

Collapse
 
arhancanli profile image
Arhan Canli •

No, this is from reading the code path and from how --resume works in general, not from seeing a file vanish. I haven't watched it happen on your setup, so treat it as a failure mode to rule out rather than one that bites.

The cases where I'd expect it are the boring ones: a cleanup job or a fresh checkout that clears the per-project session directory, running the orchestrator under a different working directory or user than the first run (sessions are stored per project path, so the id looks missing), and a ticket that waits long enough for retention to prune it. If the orchestrator ever moves machines or containers, every waiting ticket hits the fallback at once.

That last case is the argument for the cheap fix: if the fallback branch is rare it costs nothing to make it carry the card, and if it fires for every waiting ticket after a deploy it is the main path.

Thread Thread
 
panthpatel profile image
Panth Patel •

The working-directory case is the one I'd have missed. If sessions are keyed by project path, a resume from another directory or user looks exactly like a lost file.

Your deploy point settles it for me: a branch that is rare on a normal day and universal after a move should carry the card and the brief either way. That goes on the list with the delete-the-file test.

Do you log which branch a resume took, or only find out from what the agent says next?

Thread Thread
 
arhancanli profile image
Arhan Canli •

I don't run an orchestrator like yours, so I can't say what mine would show. For yours, I'd log it, because the agent's next message is a poor detector: a fresh session that was handed the card and brief will sound just as confident as a resumed one.

One line per resume is enough: ticket id, session id, branch taken (resumed or fresh), and why (file missing, resume exited non-zero, or the retry after a failure). Then the delete-the-file test can assert on the log line as well as on what the agent says it agreed to. A count of fresh-branch lines per day also shows the move-machines case directly: it should sit at zero and jump after a deploy.

Thread Thread
 
panthpatel profile image
Panth Patel •

That's the reason to log it. A fresh session handed the card and the brief would answer the same way a resumed one does, so the transcript can't tell me which branch ran.

One line per resume (ticket id, session id, branch, reason) is cheap, and it turns the delete-the-file test into an assertion instead of a read. The daily count of fresh-branch lines is the useful part: zero on a normal day, a jump after a deploy, and anything in between is something I don't understand yet.

Thanks for working through this with me.

Collapse
 
rulestack profile image
Rulestack •

Ours ends the turn too, though what brings it back is the owner answering a report, not an event. When our Claude Code agent reaches a decision reserved for its owner, it files the question in a ledger in the repo, finishes the rest of the run, and lists every open question at the end of its report. The list repeats in each report until the owner answers; the answer is filed as a ruling, and asking again on a topic that already has one is refused unless the agent writes down what has changed. Like yours, the price is latency.

Collapse
 
panthpatel profile image
Panth Patel •

The part I don't have is the refusal. In mine the answer lives in that ticket's thread, and the resumed session has it in context, but nothing refuses a repeat: another ticket can ask the same thing again. A ledger of rulings the agent has to argue against is a better shape for that.

The other difference is what happens to the rest of the work. Mine ends the whole turn on a blocking question, so nothing that depends on the decision gets built on a guess. Yours finishes everything else first, which keeps the run useful while the owner is away.

How do you keep the agent from doing work that the ruling later undoes?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.