Skip to content

bot: stop the redundant join and full replay after a table closes - #970

Merged
svanlabs[bot] merged 3 commits into
mainfrom
fix/queued-join-survives-seat
Oct 12, 2026
Merged

svanlabs[bot] merged 3 commits into
mainfrom
fix/queued-join-survives-seat

Conversation

@svanlabs

@svanlabs svanlabs Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

What generated this?

Generated-by: claude-code/claude-sonnet-5-5

Motivation

Closes #967.

After a table closes the server re-seats the bot within 0.1 to 0.2 s, but the join that table_closed queued stayed set, because Seat::joined cleared only the leave. The session loop sent join_lobby about 10 s later and drew already_seated. The handler then asked for a resync from sequence 0 although the tracker already held the table, so the reply replayed the table's whole history over live state.

Live evidence on build 7a1494c (2026-10-10 11:00 to 2026-10-11 22:12 UTC), from artifacts/logs/svanbot10.log:

  • Four table closes; three drew already_seated (Svanism twice, Svanar once) and one already_in_lobby.
  • 37 false warnings "a replayed result for hand X cannot be settled: its start was not saved". Their number equals the hands played at the table before the bot sat down (16 at table 49a1713a, 3 at table edd14b44), and none of the 37 hand ids is in the history store. played_seat matches a result by seat number, so an earlier occupant of the seat counts as the bot.
  • One stored hand carries state from replayed earlier hands: Svanism hand e1b37e33 records seat 2 folding preflop with bet_before 744 against a 30-chip pot. 1 of 13,486 hands since the install.

No row was lost and no action was illegal; the cost is false alarms on the one warning that exists to catch a lost row, and one polluted summary.

Solution

Three commits in crates/apps/bot/src/client/, two for the defects and one pinning a call site the review found loose. Each defect has a test that failed before its fix, run on the unmodified code first:

  1. Seat::joined also clears pending_join. Test: a_seat_the_server_gives_us_makes_a_queued_join_moot (failed with "seated: nothing is left to join").
  2. The already_seated handler goes through new recover::adopt_table: a table the tracker holds asks from its watermark, as a reconnect does; any other table still asks from 0. Test: an_already_seated_for_the_table_we_hold_asks_from_the_watermark (failed with left 0, right 40; both halves run against a tracker that holds another table, so an implementation that ignored the table id would fail). The protocol spec row says so. handler.rs goes from 500 to 499 lines.
  3. A handler-level test that table_closed then table_joined leaves no join queued, which pins the table_joined arm that calls Seat::joined (it fails with the clearing line removed), and the two spec rows that said a close rejoins the lobby now say "unless table_joined arrives first".

Off the decision path: client lifecycle, nothing the golden snapshot or sim paired exercises, so no paired simulation. Not released: the running build is 7a1494c.

Not changed, filed separately: the stale resumable entry behind three other false "row is lost" warnings (#968), and the stale history store (#969).

Checklist

  • scripts/check.sh full passes on this exact commit (e4e3df2), run in a worktree on the shared warm target/dev:
test.py: 896 passed, 0 failed, 4 ignored in 35 binaries; build 0.3s, run 4.5s (6 jobs x 2 threads, profile gate, peak test RSS 196 MB)
== ok (full, 144 s)
check.sh full: exit 0
  • This is one change: the redundant join after a table close and the replay it triggers. Three commits: two defects in one area, and one test that pins a call site.
  • Not a behaviour change on the decision path: no paired simulation; the golden snapshot is untouched.
  • Not a performance change.
  • The document that names the changed behaviour is updated (docs/SPEC-protocol.md, three rows).
  • Generated-by: is in every commit footer and in the section above.

🤖 Generated with Claude Code

svanlabs Bot and others added 3 commits October 11, 2026 22:32
The server seats the bot at another table within 0.2 s of a table
closing, but `Seat::joined` cleared only the leave, so the join the
close had queued stayed set. The session loop sent `join_lobby` about
10 s later and drew `already_seated`, twice for Svanism on 2026-10-10.
`joined` now clears `pending_join` as well: a seat from the server
leaves nothing to join.

Part of #967.

Generated-by: claude-code/claude-sonnet-5-5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The `already_seated` handler always asked for a resync from sequence 0.
When the tracker already held the named table, the reply replayed the
table's whole history over live state: 37 false "cannot be settled"
warnings on 2026-10-10 and 2026-10-11 (one per hand played before the
bot sat down, because `played_seat` matches a result by seat number),
and one stored hand carrying a stale seat bet from replayed earlier
hands.

The handler now goes through `recover::adopt_table`: a table the tracker
holds asks from its watermark, as a reconnect does, and any other table
still asks from 0. The protocol spec row says so.

Closes #967.

Generated-by: claude-code/claude-sonnet-5-5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The review of #967 found the call site of `Seat::joined` unpinned: the
seat unit test pins the method, not the `table_joined` arm that calls
it. The new handler test feeds `table_closed` then `table_joined` and
asserts no join is queued; it fails when `joined` leaves the flag set.
The two spec rows that said a close rejoins the lobby now say "unless
`table_joined` arrives first".

Part of #967.

Generated-by: claude-code/claude-sonnet-5-5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@svanlabs
svanlabs Bot merged commit 1aee5d9 into main Oct 12, 2026
3 checks passed
@svanlabs
svanlabs Bot deleted the fix/queued-join-survives-seat branch October 12, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix: a queued join survives the server seating the bot, and the already_seated reply replays the whole table

0 participants