Skip to content

Deliver quick-messages by resume-after-rest-stop: one session, one name, no kill in the send path - #170

Merged
MJohnson459 merged 3 commits into
mainfrom
task-428-resume-after-rest-stop
Aug 14, 2026
Merged

MJohnson459 merged 3 commits into
mainfrom
task-428-resume-after-rest-stop

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

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 message verb resumes in placeclaude -p --resume {session} --permission-mode auto ..., dropping --fork-session and
--session-id {new_session}. The {new_session} mechanism stays for fork-style
agents, and voro agent list still marks such a verb message(fork).

Reconcile gained the rest-stop arm (reconcile.rs::rest_stop). A task in
needs-input, review or waiting whose open session's listing entry reads
done has that session released through the agent's stop verb, 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
done test is the guard — blocked is a turn still under way, and the handover
verbs 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 the
task as filed and was decided with the operator mid-task. A capped session's
supervisor is alive, so it reads blocked, never done, and no reconcile pass
will 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 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, the bypass
has to be complete. So nudge_one stops first, waits, and abandons the nudge if
the 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.md documents all three stop
triggers and what each requires of the verb. CHANGELOG amends the two unreleased
entries this supersedes.

Verification

cargo test --workspace (872 tests) and cargo clippy --workspace --all-targets -- -D warnings both clean.

New tests: each rest state releasing once at the right reference with the row and
event log untouched; blocked, working, absent, running, refless and
stopless 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 inline
fallback firing only on a done-listed target, a failed release refusing the
send 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 --bg dispatch: the session
handed 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 done between them — both as claude -p --resume against
the 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.md intact in the
listing and in the transcript's customTitle, and A reopening the same
conversation with every turn in it.

One recorded assumption did not survive that run and is corrected in both docs:
a finished -p turn did not re-register the session into the default listing
on 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_clock keeps only
the clock half of Aug 14, 9pm while reset_passed resolves it to the nearest
occurrence.

#453 — the cap fixture Session limit reached · Retrying in 5m ... attempt 2/10
is 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.

MJohnson459 and others added 3 commits August 14, 2026 17:21
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
MJohnson459 merged commit 474d809 into main Aug 14, 2026
7 checks passed
@MJohnson459
MJohnson459 deleted the task-428-resume-after-rest-stop branch August 14, 2026 17:10
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.
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