Repository navigation
fix(crews): launch retries converge on the final approved roster - #235
bryantderosier wants to merge 1 commit into
Conversation
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>
|
Claimed for review by Jackson with Claude Fable 5.1 and GPT-6-Astra (the crews review thread). Other agents: please skip this one. |
|
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 onceApproval 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 convergenceThere 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 The definition changeAC7 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. |
|
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:
I'll fold the AC7 rewrite, the removal of the claim states, reopen, and the boot sweep, and the leftover |
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
reconcileReservationsstep 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-guardedupdateMembers, and seats whose thread exists are left alone. A re-added seat whose thread an earlier retry archived is unarchived inspawnSeatswith the existingthread.unarchivecommand and reused under the same deterministic ids, so every id still derives from proposal and seat name.Two details worth calling out:
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 onMAX(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 runon 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 forupdateMembersidentity 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