Conversation
…dinary cancellation A completed protocol turn (real work done, a real answer produced, but nothing ever called run/finish to render a verdict on it) lands in the same "cancelled" run state as a run that never got that far, and the milestone digest told the leader the same thing either way: "was cancelled. Any partial output it produced is readable ..., though there may be none." For a run that genuinely finished its turn, that reads as if the run failed and might have produced nothing, when in fact the result is complete and just sitting unread. The run state itself does not change -- cancelled is the correct terminal state for both cases, and stays that way. Only the digest text now distinguishes them, using the same turnSettled bookkeeping the bridge already keeps (MilestoneTracker's one-shot settle-episode tracking, previously private to the settled-turn digest). Exposed as MilestoneTracker.hasSettledTurn(runId) and threaded into formatDigest as a required third argument, since formatDigest itself has no state of its own to answer the question from. Read the whole path this touches before changing it: formatDigest and MilestoneTracker have exactly two consumers in the whole extension -- milestones.ts's own attachMilestoneBridge (the one production call site, updated to pass its own tracker) and milestones.test.ts (every existing call site updated to pass a tracker; most pass a fresh one since they don't depend on settle state). Nothing else in the extension references either export. Reproduce-first: two new tests pin both branches of the cancelled case. Confirmed red against the pre-fix formatDigest (temporarily reverted, tests otherwise unchanged) -- the settled-turn case failed with the old wording -- then green after restoring the fix.
…lled comment Two independently visible errors in the leader-facing worker model, shipped together since fixing one without the other leaves a leader reading a mixed, still-wrong picture: - crew_run's own tool description and crew-orchestration/SKILL.md both stated "a TUI vendor's process outlives its turn" / "a TUI vendor process never exits between turns" as the reason a settled turn is readable before a run goes terminal. That is only true for a TUI worker; a protocol worker's own process exits when its turn ends. Both now state the model that actually covers both adapter shapes: a TUI worker persists between turns, a protocol worker exits when its turn ends, and in both cases a settled turn is when the answer is readable. - crew_run's own description also told the leader, unconditionally, "If the run fails, retry... If stuck, cancel" -- with no observed-state qualifier and no mention that reading the result and finishing the run is a run's own normal end. Reworded to name op: 'result' + op: 'finish' as the normal path, and to gate retry/cancel on what op: 'get' actually reports, not on how the run merely looks. The four already-correct sentences the same read-first pass checked (crew-orchestration/SKILL.md:29-30, crew-approvals/SKILL.md:35, docs/user-guide.md:187) are untouched -- each already gates its own advice on an observed state rather than asserting one unconditionally. Also sharpens the previous commit's own `cancelled` branch comment: it now names, explicitly, the two populations that land in `cancelled` for different reasons but get the identical next action from this digest -- a leader that never called run/finish on a real, completed turn, and the daemon's own recovery sweep ending a waiting run at restart, whose turn may have genuinely settled before the crash -- plus the one bound on relying on the bridge's own settle bookkeeping across a reconnect, consolidated into that one comment rather than split across two. Doc/description text only, no behavior change -- the test is the marker guard (clean) and a read of every changed sentence.
…ning them
The previous commit's own fix re-committed the premise error it was
written to remove, four lines below the fix: it told the leader "once
the run is terminal or settled, read via result and close via finish",
joining two outcomes that need opposite handling on the protocol path.
Measured end to end: a protocol worker's clean exit with a settled turn
lands as RunState::unrendered_verdict() (crates/protocol/src/run.rs),
which is literally "cancelled" -- a terminal state. run_finish rejects
any terminal run outright (invalid_params, "already finished"). So a
leader following the joined instruction calls finish on a run that is
already terminal and gets refused. The waitingUser park is real, but a
protocol worker's process typically exits behind it within milliseconds
(the same drain the run/submit-awaits-start deadlock measured), so
terminal is what's actually observed almost every time, and it is
specifically the terminal -> finish edge that was wrong.
Split it instead: settled but still non-terminal (waitingUser) -> result
then finish, same as before. Already terminal (including cancelled after
a settled turn) -> result is the whole of it; finish will refuse, because
there is no verdict left to render. That is exactly what this branch's
own milestone digest already says ("finished its turn and ended without
a verdict; the result is complete") -- the tool description and the
digest now agree instead of pointing opposite ways.
In scope for the same reason: crew-orchestration/SKILL.md's run-lifecycle
section named both worker shapes in its own header but its numbered list
still described only the TUI trajectory underneath -- the settled-turn
park as if it always persists, "no need to cancel-and-resubmit" for a
process that, on the protocol path, is already gone by the time you'd
send anything. Split the same way: item 1 (the park is transient for a
protocol worker), item 2 (result is readable either way), item 3's
crew_send bullet (steering requires a live process; a protocol worker's
own exit means a fresh submit, not a follow-up) and finish bullet (works
on any non-terminal run is exactly why it refuses once already terminal).
Doc/description text only, no behavior change -- the test is the marker
guard (clean) and a read of every changed sentence.
The structural reason a protocol worker's waitingUser park reads transient is that it runs one turn and exits -- true by construction, and stronger than any timing figure since it cannot rot. "Within milliseconds" was a reconstructed number, not a measured one: nothing in the proof-run artifacts times the turn-settle-to-process-exit interval specifically (the nearest figures measure different intervals entirely). Replaced with the repo's own established phrasing for this shape, already used to describe the run/submit-awaits-start root cause. Doc/description text only, no behavior change.
nikolasd
force-pushed
the
crew-153-cancelled-digest-wording
branch
from
September 16, 2026 12:40
e243eac to
2a00348
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.
What
A completed protocol turn (real work done, a real answer produced, but nothing ever called
run/finishto render a verdict on it) lands in the samecancelledrun state as a run that never got that far, and the milestone digest told the leader the same thing either way: "was cancelled. Any partial output it produced is readable ..., though there may be none." For a run that genuinely finished its turn, that reads as if the run failed and might have produced nothing, when in fact the result is complete and just sitting unread.The run state itself does not change —
cancelledis the correct terminal state for both cases, and stays that way (the maintainer's own 2026-09-09 ruling, shared with the leader-disconnect case: ADR-0027 reservessucceededfor an explicitrun/finish). Only the digest text now distinguishes them.Four commits, in order:
The digest branch.
MilestoneTrackergets a new publichasSettledTurn(runId): boolean, reading the same#sawSettledbookkeepingisMilestone's ownrunFlagsEventarm already keeps (no new state).formatDigesttakes a required 3rd argument (tracker: MilestoneTracker) and branches on it inside thecancelledcase only: settled → "finished its turn and ended without a verdict; the result is complete. Read it via crew_run { op: "result", runId }." (reusing the existingREAD_THE_REPORTconstant, same tone assucceeded's digest); not settled → unchanged wording.Read-first:
formatDigest/MilestoneTrackerhave exactly two consumers in the whole extension —milestones.tsitself (the one production call site insideattachMilestoneBridge) andmilestones.test.ts(all 11 existing call sites, updated to pass a tracker). Nothing else imports either export.Reproduce-first: two new tests pin both branches. Confirmed RED by temporarily stashing just
milestones.ts(keeping the new tests) and running against the unpatched source — the settled-turn case failed with the old wording. Restored the fix, confirmed GREEN.This branch deliberately covers two populations that land in
cancelledfor different reasons but get the identical next action: a leader that never calledrun/finishon a real, completed turn, and the daemon's own recovery sweep ending awaitingUser/waitingPeer/pausedrun at restart (traced againstcrates/runtime/src/recovery.rs'starget_state_for) — if that run's turn had genuinely settled before the restart, the new sentence is still true and the leader's next move is identical. No third branch needed. One bound is named in the same comment:hasSettledTurnonly knows what this bridge instance observed — a connection opened strictly between a run's settle and its terminal event, with the settle already behind the replay cursor, falls back to the unchanged wording (not a regression this branch introduces; that run got the same wording before it existed).The leader-facing worker-model premise.
crew_run's own tool description andcrew-orchestration/SKILL.mdboth stated "a TUI vendor's process outlives its turn" / "a TUI vendor process never exits between turns" as the reason a settled turn is readable before a run goes terminal — only true for a TUI worker. Replaced with the model that covers both adapter shapes: a TUI worker persists between turns, a protocol worker exits when its turn ends, and in both cases a settled turn is when the answer is readable.Confirmed correct and left untouched:
crew-orchestration/SKILL.md:29-30(thecrew_send/crew_finishframing predating this list, since superseded — see commit 3),crew-approvals/SKILL.md:35,docs/user-guide.md:187— each already gates its own advice on an observed state.The terminal/parked split. Commit 2's own fix re-committed a version of the premise error four lines below the fix: it told the leader "once the run is terminal or settled, read via result and close via finish" — joining two outcomes that need opposite handling. Measured end to end: a protocol worker's clean exit with a settled turn lands as
RunState::unrendered_verdict()(crates/protocol/src/run.rs), i.e."cancelled", a terminal state;run_finishrejects any terminal run outright (invalid_params, "already finished",crates/runtime/src/service/orchestration.rs:1449-1456). So the joined instruction told the leader to callfinishon an already-terminal run and get refused. Split: settled-but-non-terminal (waitingUser) →resultthenfinish, as before; already terminal (includingcancelledafter a settled turn) →resultis the whole of it,finishwill refuse (names the exact error).crew-orchestration/SKILL.md's own numbered list split the same way in all three spots that carried the same joined claim (the park's transience,result's readability, thecrew_send/finishbullets).Drop an unmeasured timing claim introduced while writing commit 3 ("exits ... within milliseconds") — nothing in the proof-run artifacts times that specific interval. Replaced with the structural reason (a protocol worker runs one turn and exits), which is both true and can't rot, using the repo's own established phrasing for this shape.
Tests
packages/extension/src/milestones.test.ts: 31/31 pass (29 existing, updated for the new requiredtrackerargument, + 2 new:cancelled after a settled turn says the result is complete, not 'may be none',cancelled with no settled turn keeps the existing partial-output wording).Gate
bun run checkfull pass on the four-commit tip: marker guard (446 files, clean),generate --checkclean, format/typecheck/extension-build clean,bun test639/639,cargo test --workspaceall green.Note: this branch's
runs.tsedit changes the extension bundle's embedded text; the committeddist/is expected to go stale against it until CI'sbundle-check/auto-commit-distrebuilds it (not hand-built here — a macOS/arm64 build wouldn't byte-match CI's linux-x64 one).