Skip to content

fix(crews): the twelve-seat cap counts pending additions atomically - #239

Merged
bryantderosier merged 2 commits into
j5/mainfrom
j5/issue-222-seat-cap-atomic
Sep 23, 2026
Merged

bryantderosier merged 2 commits into
j5/mainfrom
j5/issue-222-seat-cap-atomic

Conversation

@bryantderosier

@bryantderosier bryantderosier commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #222.

Problem

A Crew may hold at most twelve seats counting requests still open, but requestMember counted pending seats and inserted the proposal in two separate steps, so two requests racing at eleven held seats were both filed. A replayed request counted its own proposal before finding it, so the request holding seat twelve was refused as a thirteenth on retry. The refusal told the Captain to let a member finish, which frees no seat. Separately, the proposal-to-Crew link write was ignored on failure, so an approved roster could end up not naming its Crew.

Fix

The proposal store's new admit runs the existence check, the held-seat count (member rows plus the seats of every open request), and the insert in one transaction. A replay finds its row before anything is counted, and only one of two requests for the last seat is filed. While a failed addition launch still reopens its proposal, the rows it reserved count once; that subtraction goes when reopen does. Approval only has to fit the live rows, and addMembers still refuses a thirteenth row atomically before any seat spawns.

The refusal now says the Crew is full and a new Crew is how you get more hands, and the MCP next_step carries the error's own next step.

The link write's failure now aborts the approval before any seat spawns. Declining a reopened proposal retires the Crew through that link; once reopen goes, the write stays as plain correctness.

Two commits: 1a97982 (admission transaction) and b0366c0 (link write). All changes are under apps/server/src/j5/a2a/; no upstream file, migration, or contract change.

Tests

  • Store: admit counts rows and open requests and files one of two requests racing for the last seat; a failed addition's reserved row is counted once.
  • Gate: two requests racing at eleven held seats admit exactly one, and the winner's retry is not refused as a thirteenth.
  • A roster whose Crew link cannot be written fails its approval.

Follow-up (launch once, report once)

That PR deletes the reserved-row subtraction here, along with its test in AgentCrewProposalService.test.ts and the failed-approval test in CrewProposalService.test.ts that asserts a reopened status and a null link. Until then, one known hole stays open: claim P for decline, admit Q, P's cleanup fails and reopens P, and thirteen commitments exist until one approval is refused. It closes when reopen goes.

Claude Opus 5.5 via Claude Code.

🤖 Generated with Claude Code

@bryantderosier
bryantderosier added this pull request to stack #237 September 22, 2026 12:44
@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 22, 2026
@bryantderosier bryantderosier self-assigned this Sep 22, 2026
@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

The launch model these three PRs share is being replaced with "launch once, report once"; the full note, with an apology for the churn on our side, is on #235: #235 (comment). This PR is affected as described there.

A Crew may hold at most twelve seats counting requests still open, but
`requestMember` counted pending seats and inserted the proposal in two
separate steps, so two requests racing at eleven held seats were both
filed. A replayed request also counted its own proposal before finding
it, so the request holding seat twelve was refused as a thirteenth on
retry. And the refusal told the Captain to let a member finish, which
frees no seat.

The proposal store's new `admit` runs the existence check, the count
(member rows plus the seats of every open request), and the insert in one
transaction. A replay finds its row before anything is counted, and only
one of two requests for the last seat is filed. While a failed addition
launch still reopens its proposal, the rows it reserved count as members
and not again as its pending seats; that subtraction goes when reopen
does. Approval only has to fit
the live rows. The card refuses that up front, and `addMembers` still
enforces it when it reserves the rows. Under launch-once a proposal is
resolved once, so the claim takes no count of its own.

The refusal now says the Crew is full and a new Crew is how you get more
hands, and the MCP `next_step` carries the error's own next step instead
of a fixed "correct and retry".

Closes #222

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bryantderosier
bryantderosier force-pushed the j5/issue-222-seat-cap-atomic branch from 959e98c to af22b50 Compare September 23, 2026 12:39
@bryantderosier
bryantderosier removed this pull request from stack #237 September 23, 2026 12:39
@bryantderosier
bryantderosier changed the base branch from j5/issue-221-attach-recovery to j5/main September 23, 2026 12:39
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Agreed. I rebased this onto j5/main on its own, since #235 and #236 are closing. What's left is the admission transaction: admit checks for an existing proposal, counts member rows plus every open request's seats, and inserts, all in one transaction. So a replay finds itself before anything is counted, and two requests for the last seat can't both be filed. I dropped the claim-time recount, the approving counting and the preview recount. Approval only has to fit the live rows, and addMembers already refuses a thirteenth row atomically, which under launch-once is just a failed request in the card. I kept the reserved-row subtraction for now because reopen is still on main, and it comes out with reopen. The wording and next_step fixes stay.

I also carried the one live piece of #236 here as its own commit, af22b50: the proposal-to-Crew link write no longer gets ignored, so a failed link fails the approval before any seat spawns instead of launching a Crew the report can't find. It's two commits now, 1a97982 and af22b50.

@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Merge dependencies: none. This now sits directly on j5/main and depends on nothing else, since #235 and #236 are closed. The follow-up launch-once PR should land after this one, because it deletes the reserved-row subtraction this PR keeps (it goes when reopen does).

@Jacksondr5 Jacksondr5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The narrowed PR is right and I am approving it. The admission transaction does what #222 asked: existence lookup, held-seat count, and insert in one transaction on the single serialized connection, replay found before anything is counted, reserved rows subtracted once, and both race tests present. With the claim-time recount gone, approval only has to fit live rows, and addMembers' transactional cap is the backstop before any seat spawns, so no thirteenth member can land. No upstream file touched.

Three notes, none blocking:

  • The declining-reopen hole is still on this branch and correctly scoped as leaving with reopen: claim P for decline, admit Q, P's cleanup fails and reopens, and thirteen commitments exist until one approval is refused. The code comment ties the subtraction to reopen's removal but does not name this case; one line in the launch-once follow-up so it is not rediscovered.
  • The link-write commit's stated reason is off (inline). The behavior is right; the rationale in the comment and commit message is not.
  • The body predates the narrowing. It still says stacked on #236, describes the dropped claim recount, approving-count, and preview check, references PRs 223 and 224 that do not exist, and lists tests that were deleted. AGENTS.md: the body is the problem in a sentence or two, then how you fixed it.

For the launch-once follow-up: the reopen-dependent tests here are AgentCrewProposalService.test.ts (the subtraction) and the failed-approval test in CrewProposalService.test.ts that asserts reopened status and a null link; both go with reopen. And #222's "approve an addition the person expanded while another request is open" has no gate-level test after the narrowing; the addMembers test covers the invariant, so this is a judgment call.

Reviewed by Claude Fable 5.1 in Claude Code, with an independent pass by GPT-6-Astra in Codex.

Comment thread apps/server/src/j5/a2a/CrewProposalService.ts
A roster launch records its Crew and then links the proposal to it before
any seat spawns. The link write was wrapped in Effect.ignore, so a lost
write left an approved proposal that does not name its Crew. Declining a
reopened proposal retires the Crew through that link, so the cleanup
would find nothing to retire.

The link write's failure is now a CrewLaunchOperationError that aborts
the launch before any seat spawns, the same as a failed record write, and
onRecorded's type says it can fail. The record's id is deterministic, so
clicking approve again is safe. When reopen goes, this stays as plain
correctness: an approved proposal names its Crew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bryantderosier
bryantderosier force-pushed the j5/issue-222-seat-cap-atomic branch from af22b50 to b0366c0 Compare September 23, 2026 15:19
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

I addressed the review in b0366c0: the link-write comment and commit message now give the real reason. I also rewrote the body for the narrowed PR. The declining-reopen hole and the reopen-dependent tests are listed under the launch-once follow-up so nobody has to rediscover them. I'm leaving the expanded-approval case covered by the addMembers test, not a gate test.

@bryantderosier
bryantderosier merged commit cbf8b1c into j5/main Sep 23, 2026
30 checks passed
@bryantderosier
bryantderosier deleted the j5/issue-222-seat-cap-atomic branch September 23, 2026 15:49
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.

The twelve-seat cap counts pending additions atomically

2 participants