feat(bot-mode): Group Chats survive the authority gateway dying — replicated log and fenced takeover (#97681) - #99047
Conversation
… replication and fenced takeover Every participant gateway can now keep a durable copy of a hosted room's ordered log and continue the room when its authority host is gone: - gateway/hosted_room_replicas.py: replica store in root state.db. ingest_page() persists authority-stamped groups.log pages idempotently, refusing sequence gaps and authority-epoch regressions. promote_replica() continues the room locally at epoch+1 with a lineage-proving authority.claimed event; the stale owner is fenced everywhere the claim replicates. demote_room() lets a returning stale authority fence itself (authority.lost) upon observing a newer epoch, killing split-brain writes. - tui_gateway/methods_groups.py: groups.replicate / groups.replica_state / groups.promote / groups.demote RPC surface. Promotion requires confirm=true — storage decides HOW takeover is atomic and provable, the caller (user action now, lease/quorum driver later) decides WHEN it is safe, matching the boundary blessed on #97681. Validation: 20 new tests incl. a full failover round-trip (A hosts, B replicates incrementally, A dies, B promotes with complete history, A returns demoted and fenced); 69 total across the hosted-rooms area; E2E with two real gateway stores and real install identities.
૮ >ﻌ< ა ci reviewran on 6af548a — feat(bot-mode): Group Chats survive the authority gateway dy
|
|
I reproduced four current-head blockers on exact P1 data loss: a partial replica can be promoted
Promotion must require P1 integrity: contradictory overlap is silently acceptedIngest drops every event with P1 authority forgery: page stamp is not tied to the event lineageThe caller supplies Because P1 divergent canonical history on demotionPromotion appends The returning host must first ingest the actual promotion claim and converge on that canonical event. A local demotion receipt, if needed, belongs outside the room log; it cannot consume the claim's sequence. P1 split brain: two survivors can both become epoch 2
P1 tombstone resurrectionThe replica schema carries no terminal/disband state. Ingesting the complete replay of a real These are storage-contract blockers independent of the later automatic driver. The PR body is also currently stronger than the implementation: no code continuously sends pages or invokes promotion/demotion, so this head provides replica/takeover primitives, not yet a Group Chat that automatically survives authority-host death. Two claim corrections also need attention: the two new test files collect 16 tests, not the stated 20, and the replica store's 256 MiB per-room allowance bypasses #99007's 16 MiB root-database safety budget during direct SQL promotion. Recommended gate: add hostile regressions for all six P1 shapes, then run the two-store round trip again with partial pages, duplicate conflicting pages, A -> B -> C claim validation, concurrent B/C takeover, disbanded rooms, crash boundaries, capacity accounting, and old-host convergence onto the exact promoted log. |
|
Corrective draft is now #99107. It preserves passive replicas but disables unfenced takeover, adds database-enforced room-ID reservation for mixed-version processes, quarantines any #99047 promotion/demotion lineage already written, preserves disband tombstones, validates overlap/coverage, and shares the root-DB byte budget. The current exact safety suite is 84/84; final independent review and CI are in progress. |
…sband Follow-ups on the salvaged #97744 runner: - tui_gateway/hosted_room_driver.py: HostedRoomRuntime.cancel() treated its initial status read as truth, so a task transitioning queued->running (or settling) between the read and the state call surfaced a transient 'running work requires acknowledged two-phase cancellation' / StaleTaskError to the caller and failed groups.disband. Deterministic repro on the PR head: test_client_event_id_cannot_squat_disband_receipt failed 5/5 locally. cancel() now re-reads and re-routes on every race-shaped failure (bounded retries), returns already-cancelled tasks idempotently, and rejects truly terminal states honestly. - methods_groups conflict resolution keeps both method sets: the replication surface from #99047 (groups.replicate/replica_state/promote/demote) and the runner surface from this layer (groups.stop/retry/approve). - test_groups_replication_methods.py updated to the runner's stricter create contract (2-6 profile-backed members, live worker service).
…sband Follow-ups on the salvaged NousResearch#97744 runner: - tui_gateway/hosted_room_driver.py: HostedRoomRuntime.cancel() treated its initial status read as truth, so a task transitioning queued->running (or settling) between the read and the state call surfaced a transient 'running work requires acknowledged two-phase cancellation' / StaleTaskError to the caller and failed groups.disband. Deterministic repro on the PR head: test_client_event_id_cannot_squat_disband_receipt failed 5/5 locally. cancel() now re-reads and re-routes on every race-shaped failure (bounded retries), returns already-cancelled tasks idempotently, and rejects truly terminal states honestly. - methods_groups conflict resolution keeps both method sets: the replication surface from NousResearch#99047 (groups.replicate/replica_state/promote/demote) and the runner surface from this layer (groups.stop/retry/approve). - test_groups_replication_methods.py updated to the runner's stricter create contract (2-6 profile-backed members, live worker service).
…sband Follow-ups on the salvaged NousResearch#97744 runner: - tui_gateway/hosted_room_driver.py: HostedRoomRuntime.cancel() treated its initial status read as truth, so a task transitioning queued->running (or settling) between the read and the state call surfaced a transient 'running work requires acknowledged two-phase cancellation' / StaleTaskError to the caller and failed groups.disband. Deterministic repro on the PR head: test_client_event_id_cannot_squat_disband_receipt failed 5/5 locally. cancel() now re-reads and re-routes on every race-shaped failure (bounded retries), returns already-cancelled tasks idempotently, and rejects truly terminal states honestly. - methods_groups conflict resolution keeps both method sets: the replication surface from NousResearch#99047 (groups.replicate/replica_state/promote/demote) and the runner surface from this layer (groups.stop/retry/approve). - test_groups_replication_methods.py updated to the runner's stricter create contract (2-6 profile-backed members, live worker service).
Add versioned passive history and retirement negotiation, exact claim-prefix validation, explicit passive-only grants, and generation-scoped publisher recovery. Retain v1 behavior and fail closed on unsupported lineage, stale grants, retirement races, and bounded-store exhaustion. Reuse the existing authority-history helper and authored authority-claim foundation from NousResearch#99047/NousResearch#105197. No recovery activation or work-record v2 is enabled.
…r Desktop closes Since v2026.8.31 (#99007 gateway-owned room authority, #99047 replica takeover, #99099 gateway-side turn driver) a room whose members all live on one gateway continues without any Desktop attached. The Bot Mode page carries the cross-machine and replication beats but never states this durability, so readers still assume the Desktop drives the room. Adds one bullet under Groups and group chats; groups.capabilities.driver flag per tui_gateway/methods_groups.py.
Summary
A hosted Group Chat now survives its authority gateway dying: every participant gateway can keep a durable replica of the room's ordered log and continue the room at
epoch + 1with a lineage-proving takeover, while the returning stale host fences itself out. Implements the durability requirement from #97681 on top of the authority foundation merged in #99007.Changes
gateway/hosted_room_replicas.py(new, 570 LOC): replica store in rootstate.db—ingest_page()persists authority-stampedgroups.logpages idempotently (gap- and epoch-regression-safe),promote_replica()continues the room locally atepoch + 1with anauthority.claimedclaim event carrying the full lineage,demote_room()recordsauthority.lostand fences a returning stale authority so split-brain writes are impossibletui_gateway/methods_groups.py:groups.replicate/groups.replica_state/groups.promote/groups.demoteRPC methods; capabilities now advertiselog_replication+authority_takeoverconfirm: true: storage makes takeover atomic and provable; the caller (explicit user action today, a lease/quorum driver later) decides when it is safe — the exact boundary blessed on Bot Group Chats should keep working after Desktop closes #97681Validation
Infographic