Skip to content

Quick-message delivers via fork-session and rejects only after a confirmed send - #133

Merged
MJohnson459 merged 1 commit into
mainfrom
fork-quick-message
Aug 13, 2026
Merged

Quick-message delivers via fork-session and rejects only after a confirmed send#133
MJohnson459 merged 1 commit into
mainfrom
fork-quick-message

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

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 — 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.

What changed

The message verb forks. claude's built-in message is now -p --resume {session} --fork-session --session-id {new_session}, where {new_session} is 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 placeholder is refused on every other verb.

The send goes before the transition. send_message now spawns and watches
the 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
feedback event. Only a send still running past the window, or one that exited
cleanly 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 no
other window's reconcile sees one without the other. A needs-input task gets
the 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 -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 rejection was sent.

The uuid crate (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.md are updated in the same change:
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. The
unreleased CHANGELOG entry for quick-message is corrected — it still described
the old "transition first" ordering.

Verification

cargo test --workspace (689 tests) and cargo clippy --workspace --all-targets -- -D warnings are clean. New tests: {new_session} binds shell-quoted and is
refused off the message verb; record_session_send moves the pid and follows a
fork without blanking an existing ref; a refused send leaves a review task in
review with no ## Feedback and the log error on the status line; a forking
send records the new ref and live pid and lands RejectWork; a needs-input
send 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 review with the stub's own
stderr on the status line; a forking stub records the fork's UUID on the session
row (A then jumps into that UUID), lands the rejection, and the task rides
running through reconcile even though the listing still reports the original
session done.

Follow-up filed as task #393: voro agent list omits message from each
agent's verb list.

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
MJohnson459 merged commit 4884b92 into main Aug 13, 2026
6 checks passed
@MJohnson459
MJohnson459 deleted the fork-quick-message branch August 13, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant