Deliver quick-messages by resume-after-rest-stop: one session, one name, no kill in the send path - #170
Merged
Conversation
Fork delivery moved a task's conversation out from under the name Voro
composed for it on every send: the live continuation never appeared in
the agent's session view, the row that did was the stale parent, and the
picker accumulated one same-named transcript per message. That name is
the operator's addressing scheme — the join key between the Voro pane
and the agent's session list, and the only handle away from the desktop
— so the fork was patching a symptom it had itself created.
Delivery is now an in-place headless resume. The hold that made a plain
resume impossible is removed rather than routed around: reconcile fires
the agent's `stop` verb at a session once it comes to rest, judged from
the two sources it already reads — a task in `needs-input`, `review` or
`waiting` whose listing entry reports its turn ended. That is the guard
that matters, because the handover verbs run mid-turn: `blocked` is a
turn still going and is never released, and an absent entry has nothing
to release. Nothing about the row changes, so the session stays the
task's conversation and `A` still opens it with full context.
The send path carries no unconditional stop. It gates on liveness as
before, and only where the target is still listed at rest — the
operator outrunning a reconcile tick — makes the same release inline
and waited-on, refusing the send if it fails so a message that could
not land commits nothing.
`{new_session}` stays for agents that can only be joined by forking; the
built-in claude verb simply no longer uses it.
Verified: cargo test --workspace and cargo clippy --workspace
--all-targets -D warnings, both clean. New tests cover each rest state
releasing once, the blocked/absent/running cases releasing nothing, an
agent without `stop`, the row and event log staying untouched, and the
send path's inline fallback in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRcr84jtEiRYwKfryUqwYd
The end-to-end run against a real `claude --bg` dispatch confirms the rule: the session hands over, reconcile releases it, and two quick messages then land back to back with a `voro done` between them — both `claude -p --resume` against the same session id, one transcript file, the `voro-<id>` name intact in the listing and the picker, and `A` reopening the same conversation with all three turns in it. One recorded assumption did not survive it. A finished `-p` turn did not put the session back into the default listing on this version, so no second release was needed and none was made. The rule is indifferent to that either way, but the design doc asserted the re-registration as fact; both it and the integration guide now say what was observed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRcr84jtEiRYwKfryUqwYd
The rest-stop cannot reach a capped session. Its supervisor is alive, so its listing entry reads `blocked` — the same word a permission prompt earns — and never `done`, while the hold that refuses an in-place resume sits right there. The sweep also cannot wait for the rule: that rule's `done` test is only sufficient because the liveness gate refuses every session it does not cover, and the sweep has already walked past that gate on the strength of the cap reading. Having stood down the guard that makes the test enough, it cannot then lean on the test. So `nudge_one` releases its target unconditionally and waits for the answer, abandoning the nudge if the release fails rather than spawning a send that could only be refused. The badge stays and the sweep reports the refusal, so a session that could not be reached is not counted as nudged. Two costs are recorded in §8 rather than left to be discovered. The stop is as safe as the cap reading is right — the same bet the sweep already makes — but a wrong reading now kills a live turn where it used to cost a redundant one; and the reading is debounced to a minute, so a session restarted by hand can still be stopped from under the operator. Both are why the sweep stays on a keypress rather than on the clock. Verified: cargo test --workspace and cargo clippy --workspace --all-targets -D warnings, both clean. Two new tests cover the release firing at the right reference with the send following it in place, and a failed release sending nothing; both fail if the release is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRcr84jtEiRYwKfryUqwYd
MJohnson459
added a commit
that referenced
this pull request
Aug 14, 2026
The Config screen test asserted the literal `[sessions attach resume message(fork) logs stop plan]`, which pinned a template detail rather than the width it meant to check: #170 dropped `--fork-session` from the built-in claude `message`, so `verbs()` now reports plain `message` and the assertion failed on the pull request's merge with main while passing on the older base the branch was written against. Both assertions now read the expected text off the rendered agent's own row — the bracketed verb list from `verbs`, the annotation from `models` — so the test still fails if the row clips at 100 columns and no longer fails when a template changes what the row correctly says.
MJohnson459
added a commit
that referenced
this pull request
Aug 14, 2026
* Give the Config screen's model map a line of its own The agent row on the Config screen is one unwrapped line — marker, name, provenance, verbs, then a dim `<model … · deep … · plan …>` tail — in a Paragraph with no Wrap, so it clips at the pane's width. Listing every optional verb (#167) grew the built-in claude row from ~98 to ~121 columns, which pushed the model annotation off the end at 110, where it used to fit. The annotation is the half that cannot fall off: it exists so the `{model}` in the dispatch line below reads without opening voro.toml. So it moves onto its own dim continuation line directly under that command, spelled `{model}: opus · deep fable · plan fable` — anchored to the placeholder it explains rather than trailing a row whose length is set by how many verbs an agent happens to define. That costs a line per agent carrying a model map, which the pane's fixed 14-line cap would have paid for by hiding agents off the bottom. The cap goes: the pane now sizes to the rows it has and yields only what the viewers list below needs to keep a row. That list scrolls with its selection and this paragraph does not, so an agent hidden here is the more expensive truncation. Verified in a scratch tmux at 100 columns: the built-in claude row shows every verb and its model map, and six model-carrying agents all render with the viewers list intact. Covered by two ui tests at that width. * Read the built-in claude row's verbs and model map off the row The Config screen test asserted the literal `[sessions attach resume message(fork) logs stop plan]`, which pinned a template detail rather than the width it meant to check: #170 dropped `--fork-session` from the built-in claude `message`, so `verbs()` now reports plain `message` and the assertion failed on the pull request's merge with main while passing on the older base the branch was written against. Both assertions now read the expected text off the rendered agent's own row — the bracketed verb list from `verbs`, the annotation from `models` — so the test still fails if the row clips at 100 columns and no longer fails when a template changes what the row correctly says.
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 delivery moves from fork-based to resume-after-rest-stop: Voro
releases a claude session once it has handed back, at rest in reconcile rather
than in the send path, and a quick message is then a plain in-place headless
resume. One session id, one name, one linear transcript for a task's whole life.
What changed
The built-in claude
messageverb resumes in place —claude -p --resume {session} --permission-mode auto ..., dropping--fork-sessionand--session-id {new_session}. The{new_session}mechanism stays for fork-styleagents, and
voro agent liststill marks such a verbmessage(fork).Reconcile gained the rest-stop arm (
reconcile.rs::rest_stop). A task inneeds-input,revieworwaitingwhose open session's listing entry readsdonehas that session released through the agent'sstopverb, best-effort,through the same seam #433 stops at close. The row is untouched: it stays open,
stays the task's conversation, and no event or transition is recorded. The
donetest is the guard —blockedis a turn still under way, and the handoververbs fire mid-turn, so reading rest off task state alone would clip the tail of
the very turn that reported. An absent entry has nothing to release, and the
stop is idempotent, so firing on every pass converges rather than repeating.
The send path carries no unconditional stop. It gates on liveness as before,
and only where the target is still listed at rest — the operator outrunning a
reconcile tick — makes the same release inline and waited-on, refusing the send
outright if it fails so a message that could not land commits nothing.
The capped-session sweep (
u) releases unconditionally. This was not in thetask as filed and was decided with the operator mid-task. A capped session's
supervisor is alive, so it reads
blocked, neverdone, and no reconcile passwill ever release it — while the hold that refuses an in-place resume sits right
there. The sweep cannot lean on the rest rule either: that rule's
donetest isonly sufficient because the liveness gate refuses every session it does not
cover, and the sweep has already walked past that gate on the strength of the
cap reading. Having stood down the guard that makes the test enough, the bypass
has to be complete. So
nudge_onestops first, waits, and abandons the nudge ifthe release fails. Two costs are priced in §8 rather than left to be found: a
wrong cap reading now kills a live turn where it used to cost a redundant one,
and the reading can be a minute stale, so a hand-restarted session can be
stopped from under the operator. Both are why the sweep stays on a keypress.
Docs. DESIGN.md §8 rewrites the fork passage: delivery is in-place resume
against sessions the rest rule has released; the rule, its mid-turn rationale
and the handed-back invariant are stated; fork delivery shipped, lived briefly
and was reverted because the session name is the operator's addressing scheme;
both stop and fork are back doors around the daemon's ownership model and the
verb swaps to the sanctioned front door when one exists; the agents-view trade
is recorded. The nudge paragraph and the session-lifecycle paragraph are
rewritten to match.
docs/agent-integration.mddocuments all threestoptriggers and what each requires of the verb. CHANGELOG amends the two unreleased
entries this supersedes.
Verification
cargo test --workspace(872 tests) andcargo clippy --workspace --all-targets -- -D warningsboth clean.New tests: each rest state releasing once at the right reference with the row and
event log untouched;
blocked,working, absent,running, refless andstopless cases releasing nothing; the built-in verb rendering in place with no
{new_session}while a template carrying it still binds; the send path's inlinefallback firing only on a
done-listed target, a failed release refusing thesend and committing nothing, and a confirmed send moving the pid with the
reference unchanged; the nudge releasing before it resumes, and a failed release
sending nothing. Each new arm was mutation-checked — disabling it fails the tests
that cover it.
Live check against a scratch db with a real
claude --bgdispatch: the sessionhanded over, reconcile released it (its entry left the default listing; a
duplicate stop a second later exited 0), and two quick messages then landed back
to back with a
voro donebetween them — both asclaude -p --resumeagainstthe same session id, no stop between them, the work of all three turns present,
one transcript file with no fork siblings,
voro-1-write-hello.mdintact in thelisting and in the transcript's
customTitle, andAreopening the sameconversation with every turn in it.
One recorded assumption did not survive that run and is corrected in both docs:
a finished
-pturn did not re-register the session into the default listingon this version, so no second release was needed and none was made. The rule is
written to be indifferent either way.
Follow-ups filed
#452 — a cap is account-wide, so the sweep's per-session reset parsing
reconstructs one global boolean; the "still before its reset" tally says almost
nothing, and a weekly cap reads ready days early because
parse_clockkeeps onlythe clock half of
Aug 14, 9pmwhilereset_passedresolves it to the nearestoccurrence.
#453 — the cap fixture
Session limit reached · Retrying in 5m ... attempt 2/10is documented as observed but was read out of the agent's binary. Whether a
backgrounded session can actually sit in a retry loop decides whether the sweep
is stopping sessions that would have resumed themselves.