Make refining a real task state backed by a session row - #117
Merged
Conversation
A refine in flight was invisible to the store: the proposal sat in the triage queue mid-rewrite, advertising a body about to be replaced, and any window could hand down a verdict racing the agent's own `voro set --body-file`. Nothing recorded the round's completion or failure either — `spawn_expansion` wrote no session row, so a dead refine agent left the proposal looking untouched forever. `refining` is now a state. The task leaves the queue in every window at once, because the scheduler's next-action query simply does not list it, and a triage verdict from there is an illegal transition the store refuses — the race closes by construction rather than by a guard. Both flavours open a `sessions` row, so the round rides the cockpit's running strip with its elapsed time and reconcile-on-read can probe its pid. Four triggers conclude a round, all landing on one transition that also closes the session: the rewritten body arriving through `set --body-file` (completed), a dead agent caught by reconcile (failed), quitting an interactive session without writing (aborted), and the new `C` key on a strip row, which kills the agent as well — the escape hatch for a hung round reconcile cannot catch. The returned proposal is marked from the round that just concluded, so `↻ refined` now promises the body *is* the rewritten one, and a failed round says `⚠ refine failed` rather than leaving the operator to notice an absence. Refine sessions are pid-checked whatever verbs their agent defines: they are direct `sh -c` children Voro holds the pid of, so the `claude --bg` caveat that gates the pid probe for dispatches does not apply. DESIGN.md §3/§5/§6/§8/§9 updated in the same change, including the removal of the "refine is an event, not a state" paragraph and the softening of "an open session implies executing". Verified: `cargo test --workspace` (597 tests) and `cargo clippy --workspace --all-targets -- -D warnings` pass. Driven end to end against a scratch database with stub agents — headless refine leaves the queue and returns marked, a killed agent returns it as failed within one read, the `C` key kills a hung round, an interactive round applies through `set`, and quitting one without writing returns it unmarked. A copy of the real 334-task database migrates to schema 16 with every row intact.
MJohnson459
force-pushed
the
refining-state
branch
from
August 3, 2026 10:27
3d842aa to
f92c671
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.
Problem
A refine in flight was invisible to the store. The proposal sat in the triage
queue mid-rewrite, advertising a body about to be replaced, and any window could
hand down a verdict racing the agent's own
voro set --body-file. There was nocompletion or failure signal either:
spawn_expansionrecorded nothing, so arefine agent that died left the proposal looking exactly as it had before anyone
asked for a rewrite.
Change
refiningis a first-class task state (migration 0016 widens the tasks CHECK).The task leaves the queue in every window at once, because the scheduler's
next-action query does not list the new state, and a triage verdict from
refiningis an illegal transition the store refuses — the mid-refine racecloses by construction rather than by guard code.
legal_actions(Refining)offers only the cancel.
Both refine flavours open a
sessionsrow. The headless one records it in thesame transaction as
proposed → refining(Store::record_refine_launch, theshape
record_dispatchestablished) and kills its agent if that write fails;the interactive one is recorded by the foreground round-trip, which now spawns
rather than running to completion so the child's pid is known.
PlanTarget:: Createsessions still record nothing, having no task to transition.Four triggers conclude a round, all landing on the one
refining → proposedtransition, which also closes the round's session with the matching outcome:
voro setcarrying--body/--body-fileona refining task, which is the verb the refine prompts already end with
(session
completed);failed);aborted, no marker — a quitis a no-op, not a failure);
Ckey on a running-strip row, which kills the agent's process groupas well as moving the state — the escape hatch for a hung round reconcile
cannot catch.
Reconcile pid-checks a refining session whatever verbs its agent defines: a
refine round is a direct
sh -cchild whose pid Voro holds, so the supervisor-owned
claude --bgcaveat that gates the probe for dispatches does not apply.Cockpit:
running_rowswidens tostate IN ('running','refining')and thestrip renders a round as
⟳ refiningwith its elapsed time; the detail card andCwork on it, while attach/dispatch/open no-op with an explanation aselsewhere.
state_countsgainsrefining, rendered in the header so a proposalthat has left the triage count stays felt.
↻ refinedis now derived from theround that just concluded rather than from any
refinedevent ever, so itpromises the body is the rewritten one; a failed round gets its own red
⚠ refine failed, same lifecycle, because an absence is not something theoperator should have to notice. Both markers ride the queue digest as counts.
DESIGN.md is updated in the same change — §3 (next-action), §5 (schema), §6 (the
state, its transitions, the removal of the "refine is an event, not a state"
paragraph, the four triggers and the two markers), §8 (refine sessions, the
softened "an open session implies executing", the reconcile bullet), §9 (the
strip and the
c/Ckey pair) — along with thetriage/sethelp text.Verification
cargo test --workspace(597 tests) andcargo clippy --workspace --all-targets -- -D warningspass. New coverage: the transitions in the §6 matrix, the markerderivations and session-outcome mapping,
running_rowsincluding refining,queue exclusion and the state counts, migration 0016 against a pre-0016
database, both reconcile halves, and — in the TUI — queue exclusion, strip
rendering, the cancel key, the transition menu offering only the cancel, and the
dispatch keys explaining themselves on a refining row.
Driven end to end against scratch databases with stub agents: a headless refine
leaves the queue and returns marked
↻ refined; a killed agent returns it as⚠ refine failedwithin one read with the sessionfailed;Con the stripkills a hung round and returns it unmarked; an interactive round applies through
set --body-file(sessioncompleted); and quitting one without writing returnsit unmarked (session
aborted). A copy of the real 334-task database migratesfrom schema 15 to 16 with every row and state intact.
One cosmetic consequence worth knowing: eight proposals in the live database
carry a legacy
refinedevent with no concluded round, so they lose their↻ refinedmarker. That is the point of the new derivation — the old markeronly ever promised that a rewrite had been asked for — and the next round on
any of them re-marks it.