Chessolve 6.6.0 is primarily a reliability release for live games on Chess.com.
The visible result is simple: positions update more reliably, stale suggestions disappear sooner, mid-game refreshes recover correctly, and review navigation is less likely to analyse the wrong board.
The implementation was less simple.
Before 6.6.0, Chessolve inferred committed game state mainly from DOM mutations, board pieces, clocks, and the visible move list. That worked, but the browser page does not update those signals atomically. A premove, board animation, coalesced mutation, temporarily unmounted move list, or review-board transition could leave several individually reasonable signals describing different moments of the game.
Version 6.6.0 introduces a new authority for live human games: Chess.com's RSocket game stream. The branch contains 59 commits across 67 files, with 10,181 additions and 452 deletions, including the capture pipeline, validation layers, fallback behavior, UI state changes, and regression tests.
This post explains the interesting parts of that work and the decisions behind them.

The real problem was authority, not raw speed
During live-game investigation, we observed that Chess.com publishes complete game snapshots over an RSocket connection.
Each snapshot includes:
- A game identifier.
- The complete compact move list, not only the latest move.
- White and Black clocks.
- A server revision timestamp.
Across 71 observed snapshots from plies 35 through 104, every update contained the complete history. Acknowledgements also carried the snapshot revision, which gave us a stable way to recognize duplicates and ordering.
The transport was slightly earlier than the DOM for opponent moves: 6–27 ms in the captured game, with a 17 ms median. For the local player's moves, however, the board updated roughly 103–124 ms before the server echoed the new snapshot.
That led to an important design conclusion:
The transport's main value is atomic, recoverable game state. Lower latency is a secondary benefit.
If we had optimized only for the earliest visible signal, we would still have been joining partially updated DOM state. Instead, we built a layered model that distinguishes provisional observations from committed history.
The new data path
The pipeline is deliberately split across the browser extension's execution worlds:
Chess.com WebSocket
│
▼
MAIN-world capture bootstrap
observes raw inbound and outbound frames
│
▼
MAIN-world bridge
normalizes, sequences, journals, and session-fences records
│
▼
window.postMessage boundary
│
▼
Content-world transport client
accepts only the current session token
│
▼
Move decoder + legal replay synchronizer
produces an immutable committed-position snapshot
│
▼
Chess.com adapter → BoardUpdateController
│
└── DOM fallback when transport evidence is unavailable or unsafeThis separation keeps interception close to the page APIs while keeping chess rules and application policy in testable extension modules.
Capturing traffic without taking ownership of it
The capture bootstrap runs at document_start in the page's MAIN world. It sees the native WebSocket before the site constructs its game connection.
The most important rule is that capture must remain observational. Even if parsing fails, Chess.com's original call still runs:
const nativeSend = proto.send;
proto.send = function (...args) {
try {
adopt(this);
retainFrame(this, args[0], Date.now(), "out");
} catch {}
return nativeSend.apply(this, args);
};The wrapper also preserves the native prototype and constructor behavior. Incoming messages are observed through listeners attached to adopted sockets; outgoing frames are copied immediately before the native send call.
There are explicit memory limits for retained frames and printable payloads. Capture failures increment diagnostics or drop the record—they never block the site's socket.
This pass-through property mattered more than making the interception code elegant. A browser extension observing a host application's transport should not become part of that application's correctness path.
Why capture raw frames and decode later?
We considered decoding RSocket and chess moves directly inside the capture bootstrap. That would have reduced the number of modules, but it would also have put protocol interpretation, move validation, and chess legality inside page-world code.
Instead, the bootstrap records a small normalized frame envelope:
- Direction: inbound or outbound.
- Socket URL.
- Capture time.
- RSocket stream ID, type, and flags when available.
- The bounded printable portion of the payload.
The bridge then recognizes only the payload shapes Chessolve understands. The content side remains responsible for decoding moves and replaying chess rules.
This gives each layer one reason to change:
- The bootstrap changes when browser or wire interception changes.
- The bridge changes when Chess.com's message envelope changes.
- The decoder changes when compact move encoding changes.
- The synchronizer changes when position-authority policy changes.
An ordered journal closes the startup race
document_start introduces another race: socket frames can arrive before the isolated content-world client finishes its handshake with the bridge.
Dropping those frames would defeat mid-game recovery. Broadcasting them without a session boundary could deliver old data to a newly initialized adapter.
The bridge solves both problems with a bounded, ordered journal:
const retainRecord = (record, action = TRANSPORT_ACTION) => {
sequence += 1;
const stamped = { ...record, sourceSequence: sequence };
if (currentSessionToken) {
deliver(stamped, action);
return;
}
journal.push({ action, record: stamped });
if (journal.length > JOURNAL_LIMIT) {
journal.splice(0, journal.length - JOURNAL_LIMIT);
}
};The content client creates a unique session token, announces it, and retries the handshake up to three times at 500 ms intervals. The bridge acknowledges that session and replays retained entries in source order.
State snapshots and outbound local-move commands use the same sequence, but retain their distinct action types. This prevents a replayed move command from being flattened into an ordinary state update.
The token is also checked on every content-world message. It is not a substitute for validating game identity, but it prevents a prior adapter session in the same page from consuming the new session's records.
Decoding Chess.com's compact moves
The stream does not contain SAN, UCI, or FEN. Moves are represented by two characters from Chess.com's compact alphabet.
For ordinary moves, each character maps to a rank-major square index. Promotions encode a destination relative to the source square. The same alphabet can also represent variant piece drops, which standard-game history intentionally rejects.
The core decoder is small enough to audit:
export const decodeChessComMove = (code) => {
const value = String(code ?? "");
if (value.length !== 2) return null;
const from = MOVE_CODE_ALPHABET.indexOf(value[0]);
const to = MOVE_CODE_ALPHABET.indexOf(value[1]);
if (from < 0 || to < 0) return null;
if (from > 75) {
const drop = MOVE_CODE_PIECES[from - DROP_INDEX_BASE];
if (!drop || to > MAX_SQUARE_INDEX) return null;
return { drop, from: null, promotion: null, to: squareName(to) };
}
if (from > MAX_SQUARE_INDEX) return null;
if (to > MAX_PROMOTION_INDEX) {
const promotion = MOVE_CODE_PIECES[Math.floor((to - 64) / 3)];
const destination = from + (from < 16 ? -8 : 8) + ((to - 1) % 3) - 1;
if (!promotion || destination < 0 || destination > MAX_SQUARE_INDEX) return null;
return { drop: null, from: squareName(from), promotion, to: squareName(destination) };
}
return { drop: null, from: squareName(from), promotion: null, to: squareName(to) };
};One detail from the investigation shaped the implementation: an initially inferred alphabet character was wrong. We therefore took the alphabet from Chess.com's own shipped client bundle instead of extrapolating from captured games.
That is a useful reverse-engineering rule: observations can establish behavior, but shipped code is a better source for a closed symbol table.
Legal replay is the validation boundary
Decoding two characters into something that resembles UCI is not enough. A malformed or misidentified payload could still produce syntactically valid squares.
The synchronizer treats legal replay as the final acceptance test:
const history = decodeChessComHistory(event.moves);
if (!history) return reject("undecodable-history", event);
const fen = replayLegalUciMoves(rootFen, history);
if (!fen) return reject("illegal-history", event);
return publish({
clocks: Array.isArray(event.clocks) ? event.clocks : [0, 0],
fen,
history,
serverRevision,
sourceSequence,
});The synchronizer also rejects:
- Records for another game.
- Missing revisions.
- Stale source sequences.
- Undecodable histories.
- Histories that cannot be replayed legally from the root position.
Duplicate server revisions are accepted as unchanged rather than republished. Successful snapshots are cloned and frozen before leaving the synchronizer, so downstream code cannot accidentally mutate the authority it is evaluating.
This is a fail-closed pipeline. A rejected transport update does not become a partial position. The adapter simply stops preferring it and uses the established DOM path when that path can prove a committed state.
Full snapshots were simpler than delta recovery
Because each server event includes the complete move list, the synchronizer does not apply incremental chess deltas to its previous state. It decodes and replays the complete history.
That costs a small amount of repeated work, but it has valuable properties:
- A missed message does not require a special recovery protocol.
- A mid-game refresh can rebuild the whole game from the next snapshot.
- Duplicate and stale records are easy to recognize.
- The resulting FEN is derived from one atomic history rather than mixed old and new fields.
For a chess game, replaying at most a few hundred half-moves is inexpensive. Choosing simple recovery over clever incremental state was an easy trade.
The local-move race needed a second signal
The server snapshot is authoritative, but it is not the first event for the local player's move. Chess.com draws a legal local move immediately, submits it, and only later receives the echoed state.
If Chessolve waited only for the inbound snapshot, an update pass could briefly repaint the suggestion for the previous position over the new board.
Version 6.6.0 therefore observes the outbound games.<gameId>.moves command. It carries the game ID, compact move code, and exact ply index. The site does not send this command for a premove until that move becomes legal and is actually played, making it useful commitment evidence.
The adapter records a bounded pending window:
pendingLocalMove = {
index: Number(command.index),
until: Date.now() + LOCAL_MOVE_PENDING_MAX_MS,
};
localMoveCommittedHandler();The board-update controller invalidates the old suggestion immediately. Subsequent passes are held while their resolved position still sits behind the board:
const reflectedByBoard = Number(observedUciHistoryLen) > pending.index;
const reflectedByTransport = !!snapshot && snapshot.history.length > pending.index;
if (reflectedByBoard || reflectedByTransport || Date.now() >= pending.until) {
pendingLocalMove = null;
return false;
}
return true;The latch clears when either the observed board history or the server snapshot includes the submitted ply. A five-second timeout prevents a lost echo or page transition from blocking analysis indefinitely.
There is also an earlier visual guard for the case where the board redraw arrives before the outbound command is observed. Separating “invalidate the previous result” from “the next position is safe to analyse” was essential; one event could not reliably answer both questions.
Transport authority is scoped, not universal
The RSocket stream is used only where it has demonstrated authority: live human games on Chess.com.
It does not provide everything Chessolve needs:
- It does not include player identity or color.
- It does not include SAN or FEN.
- It does not reliably announce the game result.
- Bot games do not use this live-game transport.
- Review pages can display an earlier ply while the latest transport snapshot remains at the end of the game.
For those reasons, the adapter checks the current surface, URL-derived game key, bound game ID, displayed board, and selected move before exposing a transport snapshot.
On review surfaces, the latest game history is useful evidence but not automatically the displayed position. Chessolve replays the matching history prefix and verifies its piece placement against the board. If the board and selected ply disagree, the snapshot is declined rather than forcing the latest live position onto the review UI.
Finished-game history can also be recovered from Chess.com's callback response when the visible move list is incomplete. That source receives its own synthetic revision based on ply count and a move-list hash.
The general rule is:
Prefer the strongest source available for the current surface, but never extend a source beyond the facts it actually carries.
Cached analysis became part of position transitions
Transport correctness exposed a separate UI issue. Even when the next position was known, the best-move card could flash a loading state over an answer already present in cache, or retain the previous position's evaluation while the board changed.
The board-update controller now treats card painting as part of the position transition:
if (this.#tryPaintCachedSuggestion({ fen, fenPosition, observedUciHistory, currentTurn })) return true;
this.#state.cachedSuggestionFen = "";
if (card === "pending") {
this.#app.panel.setUI({ type: "calc", move: "...", evalStr: "", clearEvaluation: true });
} else if (card === "verdict") {
this.#app.panel.setUI({ type: "skip", move: "⏸", evalStr: "", clearEvaluation: true });
}
this.#app.canvas.clearCanvas();If an exact cached result exists, it is painted immediately. Otherwise the controller shows only what it can state honestly: a pending skeleton, a turn verdict, or nothing while another guard still owns the transition. In every case, the previous arrow and evaluation are cleared when they no longer belong to the displayed position.
Cache entries themselves now undergo stricter shape validation. Scores, terminal states, move fields, and analysis lines must form a coherent result before a cached answer is considered reusable.
Review and evaluation fixes
The release also tightened asynchronous review behavior:
- A review result is checked against the position, orientation, game epoch, and engine state that requested it.
- A later navigation event invalidates a pending result before it can paint.
- Side variations that were not part of the played game use their own board FEN as the analysis root.
- Turn detection for side variations comes from that position rather than the reviewed game's clock.
- A move-review verdict reads the evaluation from its own score object instead of reusing previously formatted display text.
These changes follow the same principle as the transport work: asynchronous answers are valid only for the immutable context that produced them.
Sign-in validation was made explicit
Not every 6.6.0 change belongs to the game-state pipeline.
The email sign-in view now includes a localized Privacy Policy consent control. The send-code action remains disabled until the email is valid and consent is selected, and the policy is linked directly from the label.
The new copy was added in English, Spanish, Brazilian Portuguese, and Russian. Tests cover the default checked state, invalid email handling, consent changes, stable form hooks, and the policy link.
Alternatives we rejected
Decode everything inside the bridge
This would have reduced file count, but it would mix page-world interception with chess legality and application policy. It would also duplicate the existing legal replay utilities.
We kept the bridge focused on transport normalization and moved chess interpretation to ordinary extension modules.
Poll an HTTP endpoint
No live-game endpoint was observed serving the same complete state. Polling would add interval latency, duplicate a push mechanism the page already uses, and still require recovery logic.
The socket was the only verified live authority.
Replace the DOM pipeline entirely
That would break bot games, puzzles, and any Chess.com surface without the RSocket game stream. It would also discard useful visual evidence when a review board intentionally displays a historical or off-game position.
The transport therefore became a preferred authority, not a universal dependency.
Trust decoded squares without replaying them
This would make malformed payloads look like real positions. Legal replay is cheap and converts protocol uncertainty into a clear accept-or-reject result.
Testing the boundaries, not only the happy path
The new tests exercise each layer independently:
- The capture bootstrap is loaded in isolated VM realms and tested with inbound and outbound frames.
- The bridge is tested for payload recognition, ordering, journaling, replay, session fencing, and callback history.
- The move decoder covers normal moves, captures, castling, promotions, invalid symbols, and unsupported drops.
- The synchronizer covers duplicates, stale sequences, wrong games, illegal histories, and immutable snapshots.
- Adapter tests cover mid-game refresh, temporarily missing boards, transport desynchronization, URL changes, game identity, historical plies, review roots, and DOM fallback.
- Race tests cover local submission, server echo, premoves, staged board renders, board flips, and temporarily unmounted move lists.
- Controller tests verify cached paints, loading skeletons, stale-result rejection, arrow clearing, and turn verdicts.
Many regressions in this area are valid states arriving in an unsafe order. Tests therefore assert not only the final FEN, but also which intermediate states must never be published or painted.
What we learned
The most reusable lessons from Chessolve 6.6.0 are not specific to chess:
- Choose an authority before optimizing event timing. Fast inconsistent signals are still inconsistent.
- Keep host-page interception transparent. Observation code should fail without changing the host application's behavior.
- Separate capture, normalization, validation, and policy. Each boundary makes protocol changes easier to diagnose.
- Replay complete snapshots when the state space is small. Recovery becomes simpler and missed events stop being special.
- Validate semantics, not only syntax. A UCI-shaped move is not trustworthy until the complete history is legal.
- Treat asynchronous results as context-bound values. Position, game, perspective, engine, and settings changes can all make a once-correct answer stale.
- Keep a fallback until the new authority covers every surface. Progressive replacement is safer than a flag-day rewrite.
Chessolve 6.6.0 does more than add an RSocket parser. It establishes a clearer hierarchy of evidence for Chess.com: committed transport history where available, verified board state where necessary, and no answer when neither source can support one safely.
That hierarchy is what makes the release feel faster and calmer to users—even though the most important work happens in the moments when the code decides not to guess.