Quick-message delivers via fork-session and rejects only after a confirmed send - #133
Merged
Conversation
Quick-message (`a`) could not reach a `claude --bg` session at all, and
failed destructively when it tried. A backgrounded session that has
finished its turn is still owned by a live supervisor process, and that
supervisor refuses `claude -p --resume` for as long as it lives — which
is exactly the command the built-in `message` verb ran. The refusal
exited in under a second into the launch log, while the transition had
already appended the operator's feedback and returned the task to
`running`; the next reconcile pass then read the session as dead and
stalled the task for redispatch. On task 384 that left review feedback
recorded against an agent that never received it.
The message verb now forks: `--fork-session --session-id {new_session}`,
a new optional placeholder Voro binds to a freshly generated v4 UUID.
Forking is the one scriptable channel into a supervisor-held session, it
continues the same conversation, and because the caller names the fork up
front the session row can follow it — so later messages, jump-in and
reconciliation all address the conversation where it actually continued.
A `message` template without the placeholder resumes in place and keeps
its reference, so headless-resume agents such as codex are untouched.
The ordering is inverted to match. The send is spawned first and watched
for a two-second grace window; an early non-zero exit is reported as a
message that never happened, quoting the agent's own last log line, and
the task stays exactly where it was with its body untouched. Only a send
still running past the window — or one that exited cleanly inside it — is
followed by the session-row update and `RejectWork`, together, so no
other window's reconcile sees one without the other. A store failure
after the spawn kills the agent's process group rather than leaving it
working on feedback nothing records.
Reconciliation gains the matching rule: a recorded pid that is *alive*
proves its session is live whatever the listing says, because a forked
`-p` turn never appears in `claude agents`. The check stays directional —
a dead pid still proves nothing, since a dispatch's pid is a launcher
that exits at birth, and falls back to the listing verdict as before.
That is what keeps the task `running` while the agent answers instead of
stalling it seconds after the operator sent the rejection.
The `uuid` crate (v4) is the one new dependency; it was already in the
lock file transitively via ratatui, so this adds a feature rather than a
tree.
DESIGN.md §6/§8 and docs/agent-integration.md are updated: delivery is
fork-based for agents that opt in via `{new_session}`, the rejection
commits only behind a confirmed send, and the session ref follows the
fork.
Verified with `cargo test --workspace` and `cargo clippy --workspace
--all-targets -- -D warnings`, and against a scratch database driving the
real TUI: a stub whose message verb exits non-zero leaves the task in
`review` with no `## Feedback` section and the stub's own stderr on the
status line; a forking stub records the fork's UUID on the session row,
lands the rejection, and the task rides `running` through reconcile even
though the listing still reports the original session done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JezTBcq4Y4DJA9Ge1Vt8Sx
MJohnson459
force-pushed
the
fork-quick-message
branch
from
August 12, 2026 21:34
f25d3ac to
48bdabb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Quick-message (
a) could not reach aclaude --bgsession at all, and faileddestructively when it tried. A backgrounded session that has finished its turn
is still owned by a live supervisor process, and that supervisor refuses
claude -p --resumefor as long as it lives — exactly the command the built-inmessageverb ran. The refusal exited in under a second into the launch log,while the transition had already appended the operator's feedback and returned
the task to
running; the next reconcile pass then read the session as dead andstalled the task for redispatch. On task 384 that left review feedback recorded
against an agent that never received it.
What changed
The message verb forks.
claude's built-inmessageis now-p --resume {session} --fork-session --session-id {new_session}, where{new_session}is anew optional placeholder Voro binds to a freshly generated v4 UUID. Forking is
the one scriptable channel into a supervisor-held session, it continues the same
conversation, and because the caller names the fork up front the session row can
follow it — so later messages, jump-in and reconciliation all address the
conversation where it actually continued. A
messagetemplate without theplaceholder resumes in place and keeps its reference, so headless-resume agents
such as
codexare untouched; the placeholder is refused on every other verb.The send goes before the transition.
send_messagenow spawns and watchesthe child for a two-second grace window. An early non-zero exit is reported as a
message that never happened — with the agent's own last log line quoted on the
status line — and the task stays exactly where it was, body untouched, no
feedbackevent. Only a send still running past the window, or one that exitedcleanly inside it, is followed by the session-row update (new ref where the verb
forked, plus the spawned pid, in one statement) and
RejectWork, together, so noother window's reconcile sees one without the other. A
needs-inputtask getsthe row update and no transition, as before. A store failure after the spawn
kills the agent's process group rather than leaving it working on feedback
nothing records.
Reconcile trusts a live pid. A recorded pid that is alive proves its
session is live whatever the listing says, because a forked
-pturn neverappears in
claude agents. The check stays directional — a dead pid still provesnothing, since a dispatch's pid is a launcher that exits at birth, and falls back
to the listing verdict as before. That is what keeps the task
runningwhile theagent answers instead of stalling it seconds after the rejection was sent.
The
uuidcrate (v4 feature) is the one new dependency, justified per CLAUDE.md:it was already in the lock file transitively via ratatui, so this adds a feature
rather than a tree.
DESIGN.md §6/§8 and
docs/agent-integration.mdare updated in the same change:delivery is fork-based for agents that opt in via
{new_session}, the rejectioncommits only behind a confirmed send, and the session ref follows the fork. The
unreleased CHANGELOG entry for quick-message is corrected — it still described
the old "transition first" ordering.
Verification
cargo test --workspace(689 tests) andcargo clippy --workspace --all-targets -- -D warningsare clean. New tests:{new_session}binds shell-quoted and isrefused off the
messageverb;record_session_sendmoves the pid and follows afork without blanking an existing ref; a refused send leaves a review task in
reviewwith no## Feedbackand the log error on the status line; a forkingsend records the new ref and live pid and lands
RejectWork; aneeds-inputsend updates the row without transitioning; a live pid outlives its absence from
the listing. Three reconcile fixtures that used the current process id as a
stand-in launcher pid now use a genuinely dead one, so they still test what they
meant to — that the listing decides.
Also verified against a scratch database driving the real TUI: a stub whose
message verb exits non-zero leaves the task in
reviewwith the stub's ownstderr on the status line; a forking stub records the fork's UUID on the session
row (
Athen jumps into that UUID), lands the rejection, and the task ridesrunningthrough reconcile even though the listing still reports the originalsession done.
Follow-up filed as task #393:
voro agent listomitsmessagefrom eachagent's verb list.