Skip to content

milestone bridge: distinguish a settled-then-cancelled run, fix the leader-facing worker model - #183

Open
nikolasd wants to merge 5 commits into
mainfrom
crew-153-cancelled-digest-wording
Open

nikolasd wants to merge 5 commits into
mainfrom
crew-153-cancelled-digest-wording

Conversation

@nikolasd

Copy link
Copy Markdown
Owner

What

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 (the maintainer's own 2026-09-09 ruling, shared with the leader-disconnect case: ADR-0027 reserves succeeded for an explicit run/finish). Only the digest text now distinguishes them.

Four commits, in order:

  1. The digest branch. MilestoneTracker gets a new public hasSettledTurn(runId): boolean, reading the same #sawSettled bookkeeping isMilestone's own runFlagsEvent arm already keeps (no new state). formatDigest takes a required 3rd argument (tracker: MilestoneTracker) and branches on it inside the cancelled case only: settled → "finished its turn and ended without a verdict; the result is complete. Read it via crew_run { op: "result", runId }." (reusing the existing READ_THE_REPORT constant, same tone as succeeded's digest); not settled → unchanged wording.

    Read-first: formatDigest/MilestoneTracker have exactly two consumers in the whole extension — milestones.ts itself (the one production call site inside attachMilestoneBridge) and milestones.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 cancelled for different reasons but get the identical next action: a leader that never called run/finish on a real, completed turn, and the daemon's own recovery sweep ending a waitingUser/waitingPeer/paused run at restart (traced against crates/runtime/src/recovery.rs's target_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: hasSettledTurn only 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).

  2. The leader-facing worker-model premise. 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 — 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 (the crew_send/crew_finish framing 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.

  3. 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_finish rejects any terminal run outright (invalid_params, "already finished", crates/runtime/src/service/orchestration.rs:1449-1456). So the joined instruction told the leader to call finish on an already-terminal run and get refused. Split: settled-but-non-terminal (waitingUser) → result then finish, as before; already terminal (including cancelled after a settled turn) → result is the whole of it, finish will 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, the crew_send/finish bullets).

  4. 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 required tracker argument, + 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).
  • Commits 2-4 are doc/description-only, no behavior change — verified with the marker guard (clean) and a full read of every changed sentence, not just a diff skim.

Gate

bun run check full pass on the four-commit tip: marker guard (446 files, clean), generate --check clean, format/typecheck/extension-build clean, bun test 639/639, cargo test --workspace all green.

Note: this branch's runs.ts edit changes the extension bundle's embedded text; the committed dist/ is expected to go stale against it until CI's bundle-check/auto-commit-dist rebuilds it (not hand-built here — a macOS/arm64 build wouldn't byte-match CI's linux-x64 one).

nikolasd and others added 5 commits September 16, 2026 13:26
…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
nikolasd force-pushed the crew-153-cancelled-digest-wording branch from e243eac to 2a00348 Compare September 16, 2026 12:40
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