Skip to content

fix(crews): a proposal launches once and reports every seat - #313

Merged
bryantderosier merged 6 commits into
j5/mainfrom
j5/crews-launch-once
Sep 28, 2026
Merged

bryantderosier merged 6 commits into
j5/mainfrom
j5/crews-launch-once

Conversation

@bryantderosier

@bryantderosier bryantderosier commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Important

⚠️ Merge order: merge these in this exact order

This PR is part of the Crews stack. Merge top to bottom, one at a time, and let each land on j5/main before the next. #292 isn't in the stack but has to merge before #313.

  1. fix(crews): Crew notices report only what the platform measured #292: Crew notices report only what the platform measured (adds migration 020). Not stacked, but it must merge before fix(crews): a proposal launches once and reports every seat #313.
  2. fix(crews): no seat launches into a retired Crew or under a gone Captain #271: No seat launches into a retired Crew or under a gone Captain ✅ merged
  3. fix(crews): unit stop and archive finish over a seat that was never created #279: Unit stop and archive finish over a seat that was never created ✅ merged
  4. fix(crews): a proposal launches once and reports every seat #313: A proposal launches once and reports every seat (adds migration 021) ⬅️ this PR
  5. fix(crews): Crew groups keep seats without thread facts as unknown #300: Crew groups keep seats without thread facts as unknown
  6. fix(crews): a Crew follows its Captain through every lifecycle step #315: A Crew follows its Captain through every lifecycle step (adds migration 022)
  7. feat(crews): custom seats default to Full access and the roster flags seats that will stop #347: custom seats default to Full access

Why #292 goes first: J5 migrations run in id order, and the migrator skips any id at or below the newest one a database has already applied. If #313's migration 021 ships before #292's 020, 020 never runs on that database.

Problem

A Crew proposal still acted like something you retry. Approval claimed the gate (approving/declining), a failed spawn handed the card back with a retry prompt, and a boot sweep settled claims the server lost. spawnSeats stopped at the first failing seat, so later seats were never tried and the Captain heard nothing about them. A seat whose thread.create failed kept its roster row, even though nothing could message it. Jackson and I agreed on #271 that every approval launches once and reports what happened (#311).

What I changed

  • apps/server/src/j5/a2a/AgentCrewProposalService.ts: status is open | approved | declined. claim, complete, reopen and listClaimed are replaced by one compare-and-set resolve (WHERE status = 'open'). countHeldSeats is members plus seats in open proposals.

  • CrewProposalService.ts → resolve runs under a per-proposal lock (makeKeyedSerialExecutor), always taken before the crew lock.

    • A roster records the Crew, then onRecorded links it and records the approval before any seat spawns.
    • An addition reserves its rows, then onReserved records the approval. If that write fails, the rows are released.
    • A decline retires an unlaunched linked Crew, notifies the Captain, and resolves.
    • reconcile and bootSweepLayer are gone, and so is their wiring in runtimeLayer.ts.
  • CrewLaunchService.ts → spawnSeats carries on past failures and returns a CrewSeatLaunchOutcome per seat:

    • created
    • not_created: the thread never existed, so its row is dropped.
    • not_started: the thread exists but its home or brief failed.

    launch/addSeats return { instance, seats }. The retry-convergence checks (assertReusableSeats, sameCrewRuntime, spawnBriefWithoutCrewContext) are removed.

  • CrewLaunchReporter.ts → watch(proposalId, outcomes) uses the launch's own outcomes first. A seat with no thread projection reads as not_created, which covers a restart mid-launch. Approved seats missing from the roster are reported by name.

  • crewGateNotice.ts:

    • New seat_not_created: <seat> | <detail> lines, and not_started renders as seat_failed: <seat> | not_started | ….
    • The launch line counts "N not created".
    • The prose points the Captain at request_crew_member.
  • migrations/021_CrewProposalsResolveOnce.ts: rows a server left approving/declining go back to open with no approved seats.

  • packages/contracts/src/j5.ts CrewProposal.status, mcp/tools.ts J5CrewProposalResult.status, apps/web/src/j5/crew/crewProposals.logic.ts: status narrowed to the three values.

  • Web: crewNotices.logic.ts parses seat_not_created into notCreated, which the title and footer count. CrewNoticeRenderer.tsx shows each one as a "Not created" row with its reason. CrewProposalCard.tsx seeds from requestedSeats, because an open proposal no longer holds approved seats.

  • Docs: docs/j5/product/features/crews.md (AC6, AC7, the flaky-launch scenario, History) and docs/j5/product/a2a/agent-tools.md (propose_crew result and launch report, request_crew_member semantics, AC18).

Launch card for a roster whose critic seat was never created, before → after:

Before, the card said every seat started. After, it names the seat that was never created and why.

Why this shape

  • The A failed proposal-to-Crew attach write no longer strands the recorded Crew #221 rule still holds: if the link write fails, the approval fails before any seat spawns, and approving again is safe. Recording the Crew is idempotent, and linking plus the compare-and-set happen before the first spawn, all under the proposal lock.
  • Once spawning starts, it is final. A partial failure is information for the Captain, not a reason to hand the gate back. That is what lets the claim states, reopen, and boot sweep go.
  • I rejected keeping claims with a "partially launched" state. Every consumer would have had to handle a fourth state, just to support a retry we decided not to offer.
  • A never-created seat's row is dropped instead of kept as a tombstone. Nothing can message it, and keeping it would count against the twelve-seat cap.

Invariants

  • A proposal changes status at most once, from open. A second approve or a racing decline gets NotOpen.
  • Lock order is always proposal lock → crew lock.
  • No seat spawns before the approval is recorded. For an addition, a failed approval write leaves no reserved rows behind.
  • Every approved seat appears in the launch report exactly once: as a roster line or as a seat_not_created line.
  • An open proposal's approvedSeats is null.

Surfaces

Surface Decision
Entry points (chat, Settings, command palette, keybinding) Changed: the inline roster gate and the Inbox addition card resolve once. No new entry points.
Clients (web, desktop, mobile) Web and desktop show not-created seats on the launch card. Mobile unaffected because it doesn't render Crew gate notices.
Providers Unaffected because launch outcomes are provider-agnostic.
Contracts (packages/contracts) Changed: CrewProposal.status narrowed to open, approved, declined (J5-owned j5.ts).
Reverse states A decline still retires an unlaunched Crew. Re-asking for a never-created seat goes through request_crew_member.
Connection modes (local, remote, tunnel) Unaffected because this doesn't touch transport.
Upstream files / FORK.md Unaffected: every file touched is J5-owned.
Docs Changed: crews.md and agent-tools.md.

Out of scope

Upgrade and data

Verification

  • vp test run apps/server/src/j5 apps/web/src/j5/crew: 733 tests pass (1 skipped).
  • New tests:
    • A resolve-once store test.
    • A launch where the middle seat fails to create: the others still start, its row is dropped, and every seat is reported.
    • An addition whose approval write fails releases its reservation and spawns nothing.
    • Concurrent approve and decline: exactly one wins.
    • The reporter names a partial launch, and after a restart treats a row with no thread as not created.
    • Migration 021 reopens claimed rows.
    • The gate notice renders not-created and not-started seats.
    • The web parser reads seat_not_created.
  • Typecheck for server, web, and contracts (exit 0); lint is clean on the changed files.
  • The screenshots come from an isolated dev server on a copy of my data. Every thread in the copy points at a provider instance that doesn't exist, and scheduled tasks and outbox effects are cleared.

Review focus

  • CrewProposalService.resolve and CrewLaunchService.launch/addSeats: lock order, and whether any failure path can spawn before the approval is recorded.
  • CrewLaunchReporter.refreshSeat: reading a missing projection as not_created. Challenge whether a slow projection could misreport a seat that is still being created.

Closes #311

Claude Opus 5.5 via Claude Code in J5 Code

🤖 Generated with Claude Code

@bryantderosier bryantderosier added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. jackson-direct Taken by Jackson + Astra outside the fleet methodology; lanes never staff these labels Sep 25, 2026
@bryantderosier bryantderosier self-assigned this Sep 25, 2026
@bryantderosier
bryantderosier added this pull request to stack #280 September 25, 2026 12:53
@coderabbitai

coderabbitai Bot commented Sep 25, 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: bcccf52d-b16a-429f-bb45-527b6597f81f


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

bryantderosier and others added 3 commits September 25, 2026 16:23
A Crew proposal now resolves exactly once, open to approved or declined,
by compare-and-set under a per-proposal lock. The claimed states, reopen,
and the boot sweep for lost claims are gone; migration 021 hands any
leftover approving or declining row back as open.

Once seats start spawning, a seat that fails does not stop the others.
A seat whose thread was never created is dropped from the roster and the
launch report names it on a seat_not_created line; a seat whose brief
never went out reports not_started. The web launch card shows both.

Closes #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migration 021 test this branch adds still called NodeSqliteClient.layerMemory(), which the sync replaced with NodeSqliteClient.layer({ filename: ":memory:" }).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upstream sync's Badge lint (shadcn/no-restyle) refuses spacing and typography classes on <Badge>; the not-created row now uses size="sm" like the roster rows beside it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread apps/server/src/j5/a2a/CrewLaunchReporter.ts
@Jacksondr5

Copy link
Copy Markdown
Owner

[Review panel: Opus 5.5 + coordinator (Opus 5.5)]

Docs: missing History line. This PR changes docs/j5/product/a2a/agent-tools.md: result statuses are now open, approved, declined; AC18 is rewritten; the launch-report and request semantics change. But it adds no History line to that file. Suggested: "- 2026-09-25 — a proposal resolves once (open, approved, declined); a seat that fails to spawn is reported in the launch report, not retried (Bryant; #311)."

@Jacksondr5

Copy link
Copy Markdown
Owner

[Review panel: Opus 5.5 + Astra] FORK.md: this PR leaves the Crew proposal-flow paragraph inaccurate.

The PR says FORK.md is unaffected, but the "proposal flow's server modules" paragraph (FORK.md, just before case 35's section, around line 190) still describes the retry model this PR deletes:

  • "the gate is handed back with the human's approved seats kept, a retry converges on the same seats (a renamed seat's stale reservation is dropped, and a seat whose brief already dispatched is compared on the Captain's brief and its own instructions…)": reopen, assertReusableSeats, sameCrewRuntime, and spawnBriefWithoutCrewContext are gone, and a proposal now resolves once.
  • "a decline retires the record and archives whatever seat threads the failed launch created": a decline now only retires an unlaunched record, and nothing it declines has spawned.
  • It lists "migration 16 (proposal claims and launch-report receipts)" as current, but migration 021 removes the claim states, and 021 isn't mentioned.

A rebaser following this text would look for machinery that no longer exists. Suggested fix: rewrite that sentence in place, not as an appended history note. Something like: "an approval records the Crew and resolves the proposal once before any seat spawns; every seat is attempted, a seat whose thread was never created is dropped from the roster, and the launch report names each outcome." Then add migration 21 next to 14/15/16.

@Jacksondr5

Copy link
Copy Markdown
Owner

Decisions (Jackson, 2026-09-26).

  • Restart mid-launch is out of scope. If the server restarts while a Crew is launching, the person should expect things to need manual repair. The panel's restart comment (a reserved row left over for a seat that was never created) is withdrawn, and no fix is needed for it. Don't add restart handling to cover it.
  • Migration 021 reopening proposals caught mid-approval at upgrade: accepted, under the same principle.
  • A seat whose thread fails to create: as built, the row is dropped and the Captain hears "Not created" with the reason. The Captain and the person decide what to do, e.g. via request_crew_member.
  • Still needed: the FORK.md proposal-flow paragraph rewrite and the agent-tools.md History line.

bryantderosier and others added 2 commits September 28, 2026 08:54
FORK.md's proposal-flow paragraph still described the retry model this PR
removes (the handed-back gate, converging retries, a decline that archives
spawned seats) and listed migration 16's claims as current. It now says a
proposal resolves once before any seat spawns and names migration 21. The
agent-tools page gains its History line for the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). and removed size:XL 500-999 effective changed lines (test files excluded in mixed PRs). labels Sep 28, 2026
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

@Jacksondr5 Both remaining items are done (eff0f68):

  • FORK.md's proposal-flow paragraph is rewritten in place. An approval records the Crew and resolves the proposal once before any seat spawns, every seat is attempted, a seat whose thread was never created is dropped, and the launch report names each outcome. The retry-and-converge wording is gone, and migration 21 is listed next to 14, 15, and 16.
  • agent-tools.md has its History line: "2026-09-25 — a proposal resolves once (open, approved, declined); a seat that fails to spawn is reported in the launch report, not retried (Bryant; A Crew proposal launches once and reports every seat #311)."

I've resolved the restart thread as withdrawn, merged the latest j5/main, and carried the change up through #300 and #315. CI is running.

@Jacksondr5

Copy link
Copy Markdown
Owner

[Review panel: Opus 5.5 + Astra] Verified in eff0f68:

  • The FORK.md proposal-flow paragraph (round one) is rewritten in place to describe launch-once as built: record and resolve once before any seat spawns, never-created seats dropped, each outcome reported. Migration 21 is listed next to 14/15/16, and the retry-and-converge wording is gone.
  • The agent-tools.md History line (round one) is under ## History.

The restart-mid-launch finding is withdrawn per Jackson's decision and not re-ruled.

@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.

Posted by an AI agent on Jackson's behalf.

Approved. The FORK.md proposal-flow paragraph and the agent-tools.md History line are both in, verified by the review panel (Opus 5.5 + Astra). The restart finding stays withdrawn, per Jackson's decision. Live: a partial launch still shows "Not created".

Merge order: #292, #313, #300, then #315 (after #349 lands). #306 rebases after the stack.

Conflict in the J5 migration list: #292's migration 020 now sits on j5/main, so 020 and this branch's 021 both stay, in id order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryantderosier
bryantderosier merged commit 3a0c158 into j5/main Sep 28, 2026
29 checks passed
@bryantderosier
bryantderosier deleted the j5/crews-launch-once branch September 28, 2026 17:32
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Conflicts: the J5 migration list now holds 020 (#292), 021 (#313), and this branch's 022 in id order; kept this branch's cascade comment in runtimeLayer.ts and its crews.md History line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/j5/product/features/crews.md
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Conflict: #313 removed spawnBriefWithoutCrewContext along with the retry comparison that used it, so this branch's edits to it and its tests are dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Conflict in FORK.md: kept main's rewritten proposal-flow paragraph (migration 21, resolve-once approvals) with this branch's shared J5_PREAPPROVED_TOOLS wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 28, 2026
Conflict: #313 rewrote CrewProposalService.resolve; kept main's version and re-applied this branch's stale-preview refusal text naming the roster, a seat's runtime, and the Captain's branch or worktree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bryantderosier added a commit that referenced this pull request Sep 29, 2026
Conflicts in the Crews docs resolved to the shipped wording from #313 and #315, keeping this branch's corrections:
- crews.md: the Lifecycle sentence now says a Crew comes back only with its Captain, and the 2026-09-24 History line drops "Crews never unarchive as a unit" (Jackson's suggestion). The roster snapshot records each member's reason and no approver. History is back in date order.
- agent-tools.md: the launch report keeps not_started, seat_not_created, and resolve-once, and the seat finish notice adds the unavailable status and over-read-limit case.
- fleet-page.md: AC28 takes #315's wording, with both History lines.
- personas.md: #315's paragraph, with Retired crews described as one section across every Squadron.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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:XXL 1,000+ 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.

A Crew proposal launches once and reports every seat

2 participants