Skip to content

fix(crews): launch retries converge on the final approved roster - #235

Closed
bryantderosier wants to merge 1 commit into
j5/mainfrom
j5/issue-220-crew-launch-retry-convergence
Closed

bryantderosier wants to merge 1 commit into
j5/mainfrom
j5/issue-220-crew-launch-retry-convergence

Conversation

@bryantderosier

Copy link
Copy Markdown
Collaborator

Closes #220.

A Crew launch or addition that failed partway handed the gate back, but a retry could not land the roster the person approved on the last card. Addition retries never retired a renamed or dropped reservation, so the dead row counted against the twelve-seat cap. A seat dropped on one retry and re-added on the next was refused as a retired seat because its deterministic thread had been archived. A persona or reason change on a seat that never spawned was silently lost to INSERT OR IGNORE.

Roster launches and additions now share one reconcileReservations step that runs before the store write. It compares the reservations this proposal minted against the approved roster: dropped or renamed seats are retired (thread archived, row released), seats with no thread yet take the card's current persona and reason through a new identity-guarded updateMembers, and seats whose thread exists are left alone. A re-added seat whose thread an earlier retry archived is unarchived in spawnSeats with the existing thread.unarchive command and reused under the same deterministic ids, so every id still derives from proposal and seat name.

Two details worth calling out:

  • The archive and unarchive command ids carry a state stamp (retry-archive@<updatedAt>, retry-unarchive@<archivedAt>). The engine replays an accepted command id as a no-op, so a fixed id would leave a live orphan thread on the third retry of a drop, re-add, drop cycle. Seat, thread, participant, and brief ids are unchanged.
  • record() now bases ordinals on MAX(ordinal)+1, so a re-added seat cannot tie another row's ordinal.

All changes are J5-owned under apps/server/src/j5/a2a/; the only other edit is FORK.md's convergence sentence, rewritten in place.

Tests (vp test run on the two files: 27 passing): failed addition then rename at eleven seats converges without hitting the cap; drop then re-add is briefed exactly once, including a repeated drop/re-add cycle; persona change on an uncreated seat lands in the manifest; and a store test for updateMembers identity and ordinals. The "established seats untouched" scenario is a no-regression guard and also passes on the old code; the other three fail on it. The old assertion that re-adding a dropped seat is refused as retired is rewritten to assert revival.

Built by a Crew (scout, planner, builder, reviewer) captained from T3 Code. Claude Fable 5.1 (Captain, planner, reviewer) and Codex GPT-6-Astra (builder) via Claude Code.

🤖 Generated with Claude Code

A Crew launch or addition that failed partway handed the gate back, but a
retry could not land the roster the person approved on the last card. An
addition retry never retired a renamed or dropped reservation, so a dead row
counted against the seat cap. A seat dropped on one retry and re-added on the
next was refused as a retired seat because its deterministic thread had been
archived. A persona or reason change on a seat that never spawned was lost to
INSERT OR IGNORE.

Roster launches and additions now share one reconcile step that runs before
the store write: reservations this proposal minted are compared against the
approved roster, dropped or renamed seats are retired (thread archived, row
released), seats with no thread yet take the card's current persona and
reason through a new identity-guarded updateMembers, and seats whose thread
exists are left alone. A re-added seat whose thread an earlier retry archived
is unarchived in spawnSeats and reused under the same deterministic ids. The
archive and unarchive command ids carry a state stamp so a repeated
drop/re-add cycle is not replayed as a no-op by the command receipt. record()
now bases ordinals on MAX(ordinal)+1 so a re-added seat cannot tie another
row's ordinal.

Tests cover a failed addition then rename at eleven seats, drop then re-add
briefed exactly once, persona change on an uncreated seat, and established
seats untouched (a no-regression guard), plus store identity and ordinal
coverage. FORK.md's convergence sentence is rewritten in place.

Closes #220

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 effective changed lines (test files excluded in mixed PRs). labels Sep 21, 2026
@bryantderosier bryantderosier self-assigned this Sep 21, 2026
@bryantderosier
bryantderosier added this pull request to stack #237 September 22, 2026 12:01
@Jacksondr5 Jacksondr5 added the jackson-direct Taken by Jackson + Astra outside the fleet methodology; lanes never staff these label Sep 23, 2026
@Jacksondr5

Copy link
Copy Markdown
Owner

Claimed for review by Jackson with Claude Fable 5.1 and GPT-6-Astra (the crews review thread). Other agents: please skip this one.

@Jacksondr5

Copy link
Copy Markdown
Owner

Bryant, first an apology from Jackson and me. #220, #221, and #222 grew out of findings we raised across the review rounds on the crews stack, and the retry-convergence model they describe is one we pushed you toward with those findings. Reading the three fixes together made it clear the model itself is the problem, not your implementation of it. That cost you real time and we are sorry for it. We think you'll agree the replacement is simpler.

The new rule: the platform launches once and reports once

Approval is a single event. Whatever spawned, spawned. Whatever failed, failed. One launch report lands in the Captain's thread (the one #148 already built) saying which seats started and which did not, with the reason. After that the platform is done with the launch: it never re-shows the roster card, never tracks a retry, never holds a claim on the proposal, and never sweeps for lost decisions at boot. The proposal is resolved the moment the person clicked and its status never changes again.

Everything after that is ordinary chat. A seat that failed is a thread with a brief and no run; the Captain can message it once the provider is back and it starts like any thread. The person can open it, remove it, or ask for a replacement through the existing add-seat request, which is just another proposal. If the Captain decides the Crew cannot do the job without that seat, it says so and the person decides. None of that is platform logic.

The one thing this rule has to say about a launch that never happened: if the Crew record write itself fails, no thread exists, the approval errors in the card with the reason, and the person clicks again. That is a failed request, not a recovery state, and it needs nothing remembered.

Why not rollback or convergence

There is no transaction to roll back. The Crew record is one SQLite write, each seat thread is an orchestrator command that commits in its own transaction by design, and each brief is another. So "revert to before the Crew existed" can only ever be compensation, and compensation is exactly what can fail halfway, which is what #221 fixes and what #220's cleanup can do too. The reopen-the-gate loop is the root: every state it introduces (claimed, reopened, reserved-but-unspawned, archived-by-a-retry) is a state the next fix has to unstick. This is the same shape as the earlier rulings on the settler and the notifier: the platform stops trying to know things only the agents and the person can sort out.

What this means for the three PRs

Also coming out with the model, from the earlier round: the approving/declining claim states and their boot sweep in #148 (migration 016), since a decision resolves once and a lost write is a failed request.

The definition change

AC7 today: "A spawn that fails hands the gate back to the person; retrying converges on the same seats rather than refusing them as taken. A decision the server lost mid-way is settled at the next boot." It becomes: "A launch reports each seat's outcome to the Captain once; a seat that failed to start stays on the roster as failed until the Captain re-briefs it or the person removes it. An approval whose Crew record could not be written fails in the card with the reason." The flaky-launch scenario (the gate reopens, the person approves again, the seat spawns onto its reserved place) goes with it.

This is Jackson's call and he has made it; the reason for posting rather than just closing is so you can push back on anything here that you know from building it that we do not. If you agree, the smallest next step is one PR that rewrites AC7, removes the reopen and claim machinery, and keeps record-before-spawn, the launch report, and the admission transaction.

Reviewed by Claude Fable 5.1 in Claude Code; the ruling is Jackson's.

@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Thanks, and no apology needed. I agree the loop was the problem, and I'm fine closing this one; nothing in it has a job once a proposal resolves once. A few things I know from building it that the launch-once PR has to cover:

  1. spawnSeats stops at the first seat that fails, so today later seats are never attempted. "Whatever failed, failed" needs it to carry on to the next seat and report each seat's outcome. The Crew link still has to be written before the first spawn, not after the loop, or a partial launch reports nothing.
  2. A seat whose thread.create failed has no thread, so there is nothing for the Captain to message. Its member row is still there from record-before-spawn and still counts against the twelve. My preference is to drop that row at launch so the roster matches what exists, and have the launch report name the seat as not created.
  3. Spawn failures currently error back to the card with "retry with the same request key", and the launch report only watches seats whose brief went out. Both change: spawn-time failures go in the report and the card resolves.
  4. The link write can't stay ignored. CrewProposalService approve runs attachInstance(...).pipe(Effect.ignore), and the launch reporter skips a proposal with no crewInstanceId, so a lost link means the report never lands. I moved that fix into fix(crews): the twelve-seat cap counts pending additions atomically #239 (af22b50): the error now fails the approval in the card before any seat spawns, and clicking again is safe because the record is keyed by a deterministic id.

I'll fold the AC7 rewrite, the removal of the claim states, reopen, and the boot sweep, and the leftover approvedSeats seeding in the proposal card into that one PR. #221/#236 closes too, and #239 is now the admission transaction plus the link fix on j5/main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jackson-direct Taken by Jackson + Astra outside the fleet methodology; lanes never staff these size:L 100-499 effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crew launch retries converge on the final approved roster

2 participants