Skip to content

fix(crews): drop never-created seats from the roster before the launch report - #377

Merged
bryantderosier merged 2 commits into
j5/mainfrom
j5/crews-leftover-seat-376
Sep 29, 2026
Merged

bryantderosier merged 2 commits into
j5/mainfrom
j5/crews-leftover-seat-376

Conversation

@bryantderosier

Copy link
Copy Markdown
Collaborator

Problem

If the server stops after a Crew launch has recorded its seats but before every seat thread exists, a seat with no thread stays on the Crew's roster. The live launch drops a never-created seat, but only while it's running. After a restart, the startup sweep reports the seat as "its thread was never created" but leaves the row. That row then blocks the Captain: request_crew_member refuses the seat name as a duplicate, addMembers reports a conflict, and the row counts toward the 12-seat cap. The launch report also lists the seat on its roster while saying it isn't there. This applies to additions as well as initial rosters. Found while closing #346 (#376).

What I changed

  • apps/server/src/j5/a2a/CrewLaunchReporter.ts → dropSeatsNeverCreated, called at the start of watch, before the reporter's gate. It returns early unless the proposal is approved, linked to a Crew, and not yet reported. Then, in one crews.serialize(crewInstanceId) section, it re-reads the Crew, takes only the rows this proposal minted (crewSeatReservedBy), skips any seat the launch reported as created or not started, and removes the ones whose thread is a genuine not-found (getThreadProjectionIfPresent returns null). The rest of watch is unchanged apart from reindenting, so git diff -w shows the real change. It already re-reads the roster afterwards, so the report no longer lists the dropped seat and names it as not created.
  • CrewLaunchReporterShape.watch doc: states the drop, that a failed drop fails the report, and the locking assumption.
  • apps/server/src/j5/a2a/CrewLaunchService.ts → comment in spawnAndBrief only: the report now drops a row the launch couldn't.
  • apps/server/src/j5/a2a/CrewLaunchReporter.test.ts: four tests, plus a wrapCrews fixture option and an approvedAddition helper.

Why this shape

The fix goes where the missing seat is already measured. The launch report is the one place that runs on both the live path and the startup sweep, so dropping there fixes both without a new sweep or recovery job. That keeps to "repair beats edge-case machinery" (#327) and to the ruling that Crews have no general restart recovery (#346, closed).

A missing thread can safely count as final under the Crew's lock. A launch approves its proposal inside the step that holds crews.serialize for the Crew. So once the reporter holds that lock, the step has either finished or died with the process, and nothing recreates a seat for a proposal that's no longer open.

A failed drop fails the whole report instead of being logged and skipped. Nothing gets posted or stamped, so the next startup sweep tries again, rather than a durable report claiming a seat is gone while its row is still there.

I rejected a lock-ordering race test because it couldn't fail reliably on the unfixed code without adding a test seam to production code.

Invariants

  • Only rows the watched proposal minted are removed, only when their thread is a genuine not-found, and the read, selection, check and delete all happen inside one crews.serialize section.
  • A seat the launch reported as created or not started keeps its row, whatever its detail.
  • The cleanup finishes before anything is dispatched or stamped. A failure lands in watch's existing catchCause.
  • watch is never called while holding crews.serialize for the same Crew (the executor isn't reentrant). Today it's called only from CrewProposalService.resolve after fulfil returns, and from the startup sweep.
  • crews.serialize is taken and released before the reporter's gate, so the two locks never nest.
  • Every production writer of member rows holds crews.serialize for the Crew. That's true today; nothing enforces it, and the watch doc says so.

Surfaces

Surface Decision
Entry points (chat, Settings, command palette, keybinding) Unaffected because this is server behavior during a Crew launch report.
Clients (web, desktop, mobile) Unaffected because no client code changes. Every client sees the corrected roster.
Providers Unaffected because the change is provider-agnostic.
Contracts (packages/contracts) Unaffected because no wire shape changes.
Reverse states Changed: this is the way out of a stuck seat name. The Captain can request the seat again after the report.
Connection modes (local, remote, tunnel) Unaffected because the change is server-internal.
Upstream files / FORK.md Unaffected because every file is J5-owned, so there's no FORK.md entry.
Docs Unaffected because crews.md, agent-tools.md ("left off the roster") and FORK.md already promise this. The fix makes them true across a restart.

Out of scope

Upgrade and data

No migration. Existing reported proposals keep any leftover row (see Out of scope). Older clients and servers are unaffected.

Verification

  • vp test run apps/server/src/j5/a2a/CrewLaunchReporter.test.ts apps/server/src/j5/a2a/CrewLaunchService.test.ts apps/server/src/j5/a2a/CrewProposalService.test.ts apps/server/src/j5/a2a/CrewSeatFinishNotifier.test.ts: 4 files, 56 tests passed.
  • cd apps/server && npx tsc --noEmit: exit 0. vp lint on the three changed files: clean.
  • Each new test fails against the unfixed reporter. I checked with origin/j5/main's CrewLaunchReporter.ts and the new tests:
    • "after a restart mid-launch, a seat row with no thread leaves the roster and is reported as not created": the report still listed - second:.
    • "a never-created seat whose drop failed in the launch is dropped by the report": the roster kept second.
    • "an addition's seat with no thread after a restart leaves the roster and frees its name": the roster kept extra. The test also drops the roster seat's thread, to prove rows another proposal minted are untouched. It fails if the crewSeatReservedBy filter is removed.
    • "a cleanup that fails leaves the launch unreported, and the next sweep converges": the unfixed reporter never attempted the cleanup.

Review focus

  1. The single crews.serialize section in dropSeatsNeverCreated, and that it's released before gate.withPermit.
  2. That created and not-started outcomes are excluded from the drop, and that no caller runs watch while holding the same Crew's lock.

Closes #376

Claude Opus 5.5 via J5 Code (Claude Code), with Codex GPT-6 Sol and GPT-6-Astra and Claude Fable 5.1 reviewing

🤖 Generated with Claude Code

bryantderosier and others added 2 commits September 29, 2026 11:44
…h report

A seat reserved before a restart mid-launch kept its roster row with no thread, so the
report listed it on the roster and its name stayed taken. The launch reporter now drops
every row the watched proposal minted whose thread does not exist, inside the Crew's
serialize section and before the report gate, for initial rosters and additions, on the
live path and the boot sweep. Seats the launch reported created or not started keep their
rows, and a failed drop leaves the report owed for the next sweep.

Closes #376

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The addition test now leaves the roster seat without a thread too, so it fails if the
report stops filtering by the proposal's reserved seats.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryantderosier bryantderosier added the bug Something isn't working label Sep 29, 2026
@bryantderosier bryantderosier self-assigned this Sep 29, 2026
@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: Jacksondr5/j5code/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: da84725e-8b56-4be9-9f6f-2ef474951dda


Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:M 30-99 effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Sep 29, 2026
@bryantderosier
bryantderosier merged commit 9b53980 into j5/main Sep 29, 2026
32 checks passed
@bryantderosier
bryantderosier deleted the j5/crews-leftover-seat-376 branch September 29, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M 30-99 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.

fix(crews): a seat reserved before a restart mid-launch stays on the roster with no thread

2 participants