Repository navigation
feat(team): allow team lead reassignment - #1033
Open
harrytuckerr wants to merge 7 commits into
Open
harrytuckerr wants to merge 7 commits into
harrytuckerr wants to merge 7 commits into
Conversation
POST /api/teams/{id}/lead {slot_id|slotId} promotes an existing member and
demotes the sitting lead without rebuilding the session:
- one update_team writes both roles, lead_agent_id, and lead-first order; both
conversations' extra.role follow through the existing merge patch.
- a per-slot coordinator transfer hold blocks claims for the current and target
slots only, so unrelated members keep running. An active batch returns
TEAM_LEAD_TRANSFER_BUSY 409 with slot/conversation/turn ids; a starting member
keeps the existing TEAM_MEMBER_RUNTIME_STARTING 409.
- unread lead-directed mail is re-pointed to the new lead by id (ids, read
state, history preserved); the new lead's own stranded reports are settled
read; late writes during the hold are caught up after the swap.
- the live swap updates both scheduler roles under ONE lock, arms both role
prompts, resyncs coordinator roles/intents, and wakes only the new lead; the
old lead gets a settled-read projection notice.
- failures before the live swap compensate the roster and both conversation
roles while the hold is still up, so a retry re-applies the whole swap instead
of silently keeping a partial one.
Tests cover persistence/order/extras, unknown/single/no-op/cross-user edges,
the busy hold, mail reroute and settlement, lead-only authorization handoff,
the pre-swap enqueue race, swap-then-remove, and route CSRF/cross-user.
Follow-up on the lead reassignment patch, from root source review: - target-authored reports are no longer silently settled: the original row keeps its author and stays read in history, and the new lead gets one bounded attributed notice listing the excerpts (no payload dumps). - only user mail and actual teammates are handed over; system/control authors and the old lead's own rows stay with the slot they addressed. - the explicitly promoted slot is unpaused (a member paused after delivery failures must not become a permanently silent lead); the demoted slot's pause is preserved. - success and no-op responses now go through build_team_response, matching get_team so icons, pending confirmations, and context-reset capabilities survive the UI cache mutation. In-flight enqueue leases are never aborted by the retarget (a failed commit would mark the row read), and the transfer hold is released before the new lead is explicitly notified, so the wake is not lost to a held next().
…endent Follow-up on the reassignment patch, from root source review: - the move/forward/settle mail change now runs in ONE narrow repo transaction: eligible unread mail is re-pointed, the new lead's own reports are forwarded as an attributed notice, and only then are those rows settled read. A failure cannot leave a report settled without its forward, and no in-flight enqueue lease is aborted (a failed commit would mark the row read). - stopped teams get the same durable handoff without starting any runtime: the live and no-session paths share one mailbox helper, so the next ensure_session finds the pending mail and role notice on the new lead. - the forward notice is projected on the live path; the role notice texts are shared between both paths. - sender eligibility stays user + current teammates; control notices and previously-read rows are untouched. - the explicitly promoted slot is resumed on every promotion, including a swap-back after a pause (covered by the handoff test). Tests: transactional forward/settle, no-session handoff, pause resume on swap-back; all lead-focused tests and the e2e route test pass; rustfmt and clippy -D warnings clean.
…ally Mail-correctness follow-up from root review: - the new lead's own pending reports are forwarded as full attributed copies through the normal mailbox path: body verbatim (line breaks included) and file references preserved; the original row is settled only in the same transaction, so nothing is acknowledged before its forward is durable. The 160-char "settled without action" excerpt is gone. - both role notices are stored by the same mail transaction (new lead unread, old lead already read), so the stopped-team path has no fallible writes after the mail commit and the live path writes nothing fallible either. - the live path projects the stored rows and enqueues only the new lead's notice; the enqueue happens before the slot resyncs so the coordinator cannot create a duplicate intent for the notice. - the stopped-team path is a single mailbox-helper call.
Lead-directed writers selected the current lead and then performed a durable mailbox write / lease commit in separate steps. The handoff hold only fenced `coordinator.next`, so a write could still land after the catch-up and stay on, or wake, the demoted lead. Add a per-team routing barrier on `TeammateManager`: - Lead-directed writers take the shared side for the whole span from "select the current lead" through the durable mailbox write / lease commit (`send_message`, `mark_idle`/`finalize_turn` incl. the event loop's settle enqueue, and the interrupted/spawn-failed/delivery- exhausted/membership-removed/recovery and crash-report notices). - `set_team_lead` takes the exclusive side after the transfer hold is secured and holds it through prepare, roster/conversation role commit, live role swap, and hold release. Direct-unit handoff callers acquire it explicitly. Lock order is membership (`add_agent_locks`) -> routing barrier; the shared side is never re-acquired (locked variants exist for nested calls), and it is never held across a model turn. This replaces relying on catch-up timing alone. Add a delayed-write regression that parks a real `send_message` in the slash recognizer between lead selection and the mailbox write, runs the real prepare+commit swap in that window, and proves the row reaches the promoted lead while the demoted lead has no unread mail. Not run: no Cargo build/tests (root owns the shared build stream); source-only change, rustfmt clean.
10 of 14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Users can promote an existing teammate when the current lead cannot continue, without rebuilding the team.
POST /api/teams/{id}/leadaccepts{ "slot_id": "..." }(alsoslotId) and returns the updated team. The previous lead becomes a teammate and may then be removed through the existing API; removing the current lead remains forbidden.The service persists roster roles, the lead pointer and lead-first ordering in one update, patches both conversation roles, and hands unread user/teammate mail to the new lead. Full report bodies, line breaks and file references are retained. Forwarded reports, read marking and both role notices share one mail transaction. The old lead's notice is stored read and does not wake it; an explicitly promoted paused member is resumed.
Runtime strategy
The swap updates the live session in place. Rebuilding the entire session would stop unrelated members' event loops. Both scheduler roles change under one lock, both next-wake role prompts are armed, and coordinator roles are refreshed. A shared routing barrier serializes lead selection and durable mail writes with the handoff, including idle/settle and failure notifications. Explicit slot-addressed messages keep their target.
Starting runtimes or active old/new lead batches return a conflict without cancellation.
TEAM_LEAD_TRANSFER_BUSYincludes slot, conversation and nullable turn identifiers for the companion UI's explicit interrupt-and-retry action. Re-selecting the current lead is an event-free no-op; single-member teams, unknown slots and cross-user access are rejected.Source references
crates/aionui-api-types/src/team.rs:188,crates/aionui-team/src/routes.rs:198,crates/aionui-team/src/routes.rs:448.crates/aionui-team/src/service.rs:1187.crates/aionui-team/src/mailbox.rs:228,crates/aionui-db/src/repository/sqlite_team.rs:323.crates/aionui-team/src/session.rs:1595,crates/aionui-team/src/scheduler/mod.rs:215.Validation
just pushgate: Pass on5d816a6e: migration immutability, lint, formatting, and 9,404 tests passed (52 skipped).Related and scope
Related: iOfficeAI/AionUi#4269 and the lead-removal guard in iOfficeAI/AionUi#2357. Companion UI: iOfficeAI/AionUi#4335 (draft pending a released backend version).
No database migration, automatic failover, model-callable handover tool, or in-place backend/model replacement. The existing task board, conversation bindings and workspace are retained. Public changes and PR text were checked for machine-specific paths and credential patterns; no raw logs or captures are attached, and commit email addresses use GitHub noreply.