Skip to content

Mephisto 3.x — 2026 site adapters, modern engine lineup, four-player chess - #37

Open
IchNukeDichWeg wants to merge 221 commits into
AlexPetrusca:masterfrom
IchNukeDichWeg:mephisto-3x
Open

Mephisto 3.x — 2026 site adapters, modern engine lineup, four-player chess#37
IchNukeDichWeg wants to merge 221 commits into
AlexPetrusca:masterfrom
IchNukeDichWeg:mephisto-3x

Conversation

@IchNukeDichWeg

@IchNukeDichWeg IchNukeDichWeg commented Aug 5, 2026

Copy link
Copy Markdown

Mephisto 3.x — a full revival of the 2.x base. Every site adapter was re-anchored against 2026 DOMs, the engine lineup was replaced, and detection moved from polling to events. Released builds: https://github.com/IchNukeDichWeg/Mephisto/releases

  • Engines
    • Stockfish dev NNUE, Stockfish 18 / 18 Small NNUE, Stockfish 11 HCE
    • Fairy-Stockfish 14 NNUE — a bundled net per variant
    • Maia — the original human-like nets, 1100–1900 plus a community 2200
    • Maia-3 — one transformer conditioned on a rating you set live, 600–2600
    • Tetrarch — four-player chess, over native messaging
    • Remote / native engines — a real binary outside the browser, auto-launched by Chrome
    • Strength cap per engine, following that engine's real UCI_Elo range
  • Four-player chess
    • Chess.com's 4-player variant, read straight to canonical FEN4
    • 14×14 panel board, corners cut, rotated so you are always at the bottom
    • Evaluation normalised to your team rather than flipping sign every seat
    • Teams mode only — the engine does not search free-for-all; the mode is read from the page and
      can be set by hand when that read is wrong
    • Autoplay on the analysis board as well as in a game
    • macOS, Linux and Windows setup (registry + DLL on Windows)
  • Analysis
    • Continuous evaluation with multiple lines, each drawn as its own arrow
    • Opening explorer (lichess) with book moves on the board, and weighted book play. Lichess put
      that endpoint behind OAuth, so there is a field for a personal API token: their own site
      passes on a session cookie, but the explorer answers access-control-allow-origin: * to
      every origin except lichess.org's, and a wildcard forbids credentials, so no extension can
      use that route. The token is a password field, is dropped from an exported settings file,
      and appears in the diagnostics report only as set/not-set
    • Endgame tablebase, move confidence, eval history graph
    • Read a position off the screen — video, diagram, image — and follow it live
    • Playable panel board: click or drag to walk a line, with underpromotion
    • Game review — a finished game analysed on the extension's own page, from a PGN, a file, or a
      player's public Chess.com archive. Every position searched once, so before and after come from
      the same search; Lichess accuracy and the panel's own move-quality bands; alternate lines; an
      eval graph with the opening/middlegame/endgame boundaries; think time from the clock comments;
      openings named offline from a bundled CC0 table keyed by position, so transpositions name
      correctly; an optional Maia pass reporting where a move sat in the human model's own ranking,
      and a human-likeness report built on it; batch review pooling a player across every game in the
      file against one engine load; fair-play indicators with an overall estimate that says what it is
      worth, presented as measurements and never a verdict; and an export that clones the page with
      its styles inlined into one self-contained file
  • Automated play
    • Autoplay with trusted clicks, human cursor travel and per-move timing
    • Premove, including double premove on forced lines
    • Humanize — move mix and quality thresholds tuned to a target rating, with the mix's own
      accuracy and average win-drop shown as you tune it
    • Clock mode and mirror time
    • Pace to clock — shrinks the simulated think pause and cursor travel when the clock is short,
      separately from clock mode, which paces the search
    • Manual mode, help mode, opponent mistake alerts
  • Puzzles
    • Lichess puzzle database in IndexedDB: known solutions play with no search
    • Chess.com puzzle database as well — TCN and daily-archive rows — in a separate store, so a
      position is never answered from the other site's records
    • Rush, Storm, Racer, Training, BlitzTactics, ChessBase Tactics
    • A tunable pause before a database move's first click. It is the one move with no search in
      front of it, so nothing else lets the board finish animating before it is clicked
  • Sites
    • Chess.com — including Play Bots, variants, and 4-player
    • Lichess — all variants, live and correspondence
    • TakeTakeTake — WebGPU canvas board, read through a MAIN-world probe
    • BlitzTactics, ChessBase Tactics
  • Interface
    • Floating in-page panel that stays open while you play, in a closed shadow root
    • Two-tab layout, configurable hotkeys, compact mode
    • Fourteen languages, switchable live
  • Page footprint
    • No web_accessible_resources; panel assets are streamed from the worker
    • Explorer and tablebase lookups issued from the service worker, not the page
    • De-branded MAIN-world probes
  • Updating
    • Opt-in self-update from the releases page: the ~6 MB update archive is written into the
      extension's own folder in place, so the extension id — and every native-messaging
      registration made against it — survives
    • Once set up, the panel's own update notice becomes the install button
    • Off by default. The archive is unpacked and checked in memory first: an unexpected path, a
      version that disagrees with the release or a missing file means nothing is written at all
    • Two archives per release: the full one for a first install, the small one for an update
    • Every install is staged, backed up and undoable: Roll Back and Finish Interrupted Update, and
      the panel says once what changed
  • Diagnostics
    • Copy Diagnostics puts the version, the bundled assets, the connected hosts, what was detected,
      why the last move was or was not played and the recent trace on the clipboard, with nothing
      identifying in it
    • The reason a move did not happen is shown in the panel rather than left silent
    • Worker cold starts are timed and persisted, so a slow start can be explained by the report even
      though the worker that was slow is long gone
  • Various bug fixes, including two worth naming
    • The +/- buttons on the settings page dispatched only change, while the form layer
      persists a number field on input — so every stepper moved the value on screen and stored
      nothing, and typing into the same field always worked, which is what hid it
    • The build marker was a hand-maintained constant and had been wrong for four releases, so
      every bug report named a build that was not running; it reads the manifest now

Supersedes #33, which carries the earlier discussion (including @IazLur's anti-cheat notes and #34).

IchNukeDichWeg and others added 30 commits July 11, 2026 13:18
…toplay

Engines: add Stockfish dev (2026-06) and SF18 small-net WASM builds from
@lichess-org/stockfish-web 0.4.0; drop the broken SF6/SF16/SF17-79 builds
(SF17-79 crashed with unaligned/OOB access on some machines). Default is
now Stockfish dev. (SF18 full-net is omitted here because its net exceeds
GitHub's file-size limit; the dev build is an SF18 build and covers it.)

Detection: follow Lichess's obfuscated move-list DOM -- real-time games
nest it under .col1-moves, correspondence games directly under <app>;
scrape the full live move list instead of trusting the churning
selected-move class; drop the end-of-game result/status element so it is
not parsed as a bogus move.

Turn: derive side-to-move robustly (start position = White, move-count
parity fallback, full live-game scrape) so autoplay fires on the right
turn and plays White's opening move.

Crash safety: validate scraped positions before feeding the engine (a
king-en-prise / missing-king / back-rank-pawn FEN OOB-crashes the WASM
engine) and skip mid-animation piece scrapes.

Autoplay: 15s watchdog on the move-in-progress flag; ignore invalid
click coordinates.

remote-engine.py: support engines without MultiPV and surface opening-book
(depth-0) moves, so pygin/UCI backends work.

Defaults: engine=stockfish-dev, search 500ms, fen-refresh 50ms, threads 4,
memory 256MB, autoplay on, evaluation shown, threat/ponder off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a new position arrived while the engine was still searching the
previous one, the 'stop' sent by on_new_pos made the engine flush a
bestmove for the OLD position. It arrived after 'turn' was already
updated, so if the old search was for the opponent's side (they replied
within the search window), that stale move was autoplayed as ours --
stray board clicks that could select a piece and corrupt the next real
automove. Now all output of a stopped search, up to and including its
flushed bestmove, is discarded; the flag resets on engine (re)init so a
crashed engine can't eat the fresh engine's first result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- stockfish-17-79/ restored byte-identical from upstream (blob oids
  unchanged) and rewired in popup.js/general.html; removed from the
  REMOVED_ENGINES migration list.
- stockfish-18/ (full dual-net build from @lichess-org/stockfish-web
  0.4.0) added: its big net nn-c288c895ea92.nnue is 104MB, over
  GitHub's 100MB blob limit, so it ships as three <50MB .part0..2
  chunks. fetch_nnue() in popup.js stitches chunks at load time and
  falls back transparently for whole-file nets; verified reassembly
  sha256 == c288c895ea92... (the net's content-addressed name).
- .gitattributes: keep the new nets as regular blobs (public forks
  cannot upload new Git-LFS objects).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bar, continuous analysis

- Draggable floating panel injected into the page replaces the anchored
  popup (toolbar icon toggles it); it stays open while playing, making
  the old 'Inspect Popup' workaround obsolete. Extension pages now ship
  COOP/COEP so the engine's SharedArrayBuffer threads work inside the
  embedded frame; engine files are web-accessible for its workers.
- Quick Settings sidebar: live Autoplay/Premove/Puzzle/Help toggles +
  engine/threads/memory/lines/search/fen/think/move settings.
- Orientation-aware eval bar next to the mini board.
- Help mode: mirrors all analysis arrows onto the site's board while
  evaluating continuously; user plays the move themselves.
- Continuous analysis (go infinite) whenever autoplay is off.
- Safe premove: replies certified for stability across depths 6/9/10+;
  instant reply only when the opponent plays the predicted move, real
  premove only when provably unable to misfire (forced moves,
  recaptures). Guarded against stale stopped-search bestmoves.
- 'From Position' games (custom starting position) supported.
- New defaults; version 3.0.0; README updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, live depth, 2026 DOM fixes

Supersedes AlexPetrusca#30. Full changelog in the PR description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents fen-poll pile-ups when the scrape round-trip is slower than the
poll interval, with a 500ms self-heal so a dropped reply (content-script
busy performing a move) can never wedge polling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dark Mode switch under Appearance settings; themes the options pages and
  the floating panel (single class toggled from one setting).
- Getting Started updated for the floating panel (drops the obsolete
  'Inspect Popup' workaround) and documents Help Mode and Premove.
- Footer copyright 2025 -> 2026.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anded

- The content-script now always answers a fen poll (with 'no' while it is
  mid-move) so the popup's in-flight poll guard clears immediately instead
  of waiting out the fallback timeout -> the board updates promptly again.
- simulateMoveVerified: after autoplaying a move, confirm the move list
  actually grew and retry (up to 2x) if a click silently failed. The
  move-count check can't double-move -- if a move registered we treat it
  as success even if the opponent already replied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A premove is clicked during the opponent's turn -- the site queues it and
it never grows the move list until they move, so the move-count check
false-failed and retried, clobbering the queued premove. Verification now
runs only when it is our turn to move; premoves are played once and left
alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Never auto-move on a stale automove message if Autoplay has since been
turned off. Harmless in normal play (autoplay/premove both require the
flag on).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition

A certified instant reply is now gated by premove_reply_playable(): it
must move OUR piece and, on our turn, be fully legal right now. Guards
against a stale/mismatched reply making the extension click the
opponent's move or an illegal move (then thrashing the retry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The debugger click attached to the active tab, so a move firing while you
were on another tab (e.g. chrome://extensions) dispatched there and failed
with 'Cannot access a chrome:// URL'. Clicks now target the content-script
sender's tab, so autoplay works regardless of which tab is focused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only autoplay/premove a move that moves OUR piece and (on our turn) is
  legal in the current position -- never the opponent's move if the turn
  was mis-scraped.
- Before each move's clicks, click a safe empty square (one the moving
  piece can't reach) to clear any stale selection, so a retry's from-click
  can't deselect an already-selected piece and no-op the move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simulateMoveVerified re-read the move-list length at the start of every
retry, so a move that registered LATE (chess.com board animation slower
than the fixed 250ms wait) was seen only after the baseline had already
advanced -- the retry then replayed into a changed board and still logged
'failed to register'. Capture the baseline once before the first attempt
and thread it through retries, and poll up to 1.5s for the count to grow
instead of a single snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… re-derivation

The move-registration check (simulateMoveVerified) was gated by the
content-script's isOurTurn(), which re-derives whose turn it is from
lichess DOM highlights. That throws on boards where the piece selector
differs (e.g. the analysis board), so isOurTurn() returned false and
verification was SILENTLY SKIPPED -- a failed move was never retried and
no warning was logged. The popup already knows the turn authoritatively;
it now decides real-move-vs-blind-premove from the position's side-to-move
and passes a  flag in the automove message. Blind premoves (played
during the opponent's turn) still skip verification as before.

Also drop the per-poll DIRECT/INDIRECT/FULL cache-path logs and the
per-scrape position log -- they fire ~20x/second at the default refresh
and bury the move-verification warnings in the console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scale 0.85 to 0.75 (the popup is a fixed 548px layout, so scaling shrinks
it uniformly without clipping the board or the quick-settings column) and
pin right: 0 instead of 12px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ark mode

Minimize button in the panel title bar hides the overlay via visibility:hidden
and drops a small restore badge. The iframe stays in the DOM and running, so
autoplay / premove / help mode keep working while minimized. This also fixes
autoplay clicks that were being eaten by the panel: clicks are dispatched at
screen coordinates, so whenever the panel sat over a destination square it got
the click instead of the board -- minimizing removes it from hit-testing.

Dark mode: deeper cohesive slate palette, dimmed secondary text, a titled/
underlined settings header, teal focus rings on inputs, a dark slim scrollbar
for the settings panel, and flattened action buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'Eval Bar' quick-setting draws a vertical eval bar just to the left of
the game board, styled like the popup's bar (dark = black's share, white =
white's share) with the score shown inside it chess.com-style, on the
winning side's end. Reuses the existing hint-overlay plumbing: the popup
computes the numbers in update_eval_bar and pushes them to the content-
script on every eval update; the bar is absolutely positioned over the
board, orientation-aware, pointer-events:none so it never blocks a click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two tab-isolation fixes:

- The popup sent to the globally-active tab and the content-script broadcast
  fen/config/click to every tab's popup, so a background tab's Mephisto drove
  the foreground tab (e.g. turning on help there). The content-script now
  learns its own tab id from the background and passes it to the popup iframe
  via a ?tab= param; the popup messages only that tab and ignores any
  content-script message from a different tab. Falls back to the old behaviour
  if the id isn't known yet.

- Closing the panel (X) removed the iframe but left the eval bar / hint arrows
  drawn on the board. removeOverlay() now clears them too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Minimize now hides the panel with opacity:0 + pointer-events:none instead
  of visibility:hidden, so the iframe stays full-size and in-viewport and
  Chrome never throttles its timers -- autoplay/premove/help keep running at
  full speed exactly as if open. Closing with X still removes the iframe and
  stops everything until reopened.
- Options-page dark mode: lift the dim field labels and make the live slider
  values ('Threads (6)', 'Memory (128 MB)', ...) white so they're readable,
  plus bordered section cards, an accented header underline, and a toned-down
  range track.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mainline Stockfish (dev/18/17/11) that isn't told it's playing Chess960
treats the game as standard chess and mishandles castling whenever the king
or rooks aren't on their standard files. Send 'setoption name UCI_Chess960
value true' during setup when the variant is fischerandom and the engine is
a mainline SF, so SF -- far stronger than the SF14-based Fairy-Stockfish --
can be used correctly for 960. Fairy-Stockfish is unchanged: its
'fischerandom' UCI_Variant is a 960-typed variant that already handles it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The variant was only settable on the full options page, so panel users had
no way to tell Fairy which variant a game is -- it silently analysed every
variant as standard chess (e.g. a Three-check game evaluated as winning
while it was lost by checks). Add a Variant dropdown to the Quick Settings
panel, shown only when Fairy-Stockfish is the engine; changing it reloads
so the matching NNUE loads and UCI_Variant is set. New nets slot in by
adding one <option>. Next to it, a detect button reads the variant off the
lichess game page (variant name links to a stable /variant/<key> URL) and
switches to it. Panel is ~40px taller so the extra row fits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the Stockfish 17 NNUE engine: removed from both engine dropdowns,
  the engineMap and the wasm-engine list, added to REMOVED_ENGINES so a
  stale selection migrates to the default, and deleted its ~79MB of engine
  files. Remaining: SF dev / SF 18 / SF 18 Small / SF 11 HCE / Fairy SF 14
  / Remote Engine.
- Options-page dark mode: drop the hard 1px border + shadow + header
  underline that made each section look like a stray boxed-off frame (the
  section background alone now groups it), and actually darken the range
  slider track (materialize's bright default line) so only the teal thumb
  stands out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When minimized the panel is opacity:0 + pointer-events:none, but the drag
handler's mouseup leaves an inline pointer-events:auto on the iframe, which
overrides the wrapper's inherited none -- so after ever dragging the panel,
the invisible frame kept eating clicks meant for the page underneath. Set
pointer-events:none explicitly on the iframe when minimizing (restore it on
un-minimize) so you can click through to whatever is under the panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bright slider line in dark mode wasn't the ::-webkit-slider-runnable-track
pseudo (that's transparent) -- materialize puts border:1px solid white on the
range ELEMENT itself, and that white border was the visible line. Overriding the
track pseudo did nothing. Recolour the element border to a subtle #45454b so only
the teal thumb stands out. Verified in-browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark-mode sliders were a harsh light line (materialize's 1px white border
on the range element). Reset the control and paint a real progress track: a teal
fill up to the thumb + a subtle dark remainder, via a gradient on the element
background driven by a --fill custom property. options.js sets --fill from each
range's value (on page render + live while dragging). Verified in-browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rack

- Drop the boxed-off card look: html.dark .big-section is now transparent (no
  background/border/shadow/radius), so settings sit flat on the page.
- The white bit past the slider thumb was materialize's ::-webkit-slider-runnable
  -track rendering over our gradient -- make the track pseudo transparent so only
  the teal-fill element background shows. Verified in-browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chess.com runs variants at www.chess.com/variants/<slug>/game/<id> with a React
'TheBoard' component that shares nothing with the main site's DOM -- the extension
saw 'No Chess Game Detected' on every variant game. Now:

- getBoard/getMoveContainer/getMoveRecords recognize the variants board
  (.TheBoard-layers, .moves-moves-list, one SAN per .moves-table-cell.moves-move,
  filtered to real moves so placeholder cells and result markers don't parse).
- scrapePositionFen ships the SAN list; Fairy-Stockfish rebuilds the position
  from UCI_Variant + SANs through the existing variant path.
- The variant 'detect' button now also works on chess.com: detectVariant()
  dispatches per site, reading the chess.com variant from the URL slug.
- Autoplay works on variant boards. The click pipeline was already geometry-
  based and board-agnostic; the one missing piece was getOrientation() -- the
  variants board has no coordinate labels or flip class, and piece data-color
  codes are per-game numbers. getChesscomVariantsOrientation() classifies the
  two sides RELATIVELY: decode each colour's SVG sprite, the lighter-filled
  side is White; the board is white-oriented when White sits lower on screen.
  Verified against a live game: all pieces' squares mapped to their exact
  screen centers.

Variants supported end-to-end (detection + analysis + autoplay): 3check,
kingofthehill, crazyhouse, antichess, atomic, horde, racingkings -- the ones
the bundled chess.js can reconstruct. Promotion popups and crazyhouse drops
on the variants UI are known gaps (watchdog prevents any hang).
The panel used to learn about position changes by sending {queryfen} every
fen_refresh ms (10ms default), each request a full DOM scrape with forced
layout flushes -- 100 scrapes/second, for the lifetime of the tab, to observe
a board that changes a few times a minute. With the 3.x panel staying open for
whole games this ran for hours and taxed the page it was supposed to read.

Now the content-script PUSHES positions when the DOM actually changes:

- A MutationObserver on document.body (childList + class/style attributes;
  characterData deliberately excluded so clock ticks can't wake the pipeline).
- Mutations inside Mephisto's own overlays are filtered out -- dragging the
  panel or redrawing the eval bar can never feed back into the pipeline.
- 30ms debounce collapses a mutation burst (a piece animation is dozens of
  mutations) into at most ~33 samples/s, always ending with one settled scrape
  ~30ms after the burst's last mutation; mid-animation samples are rejected by
  the scrapers and never pushed.
- Dedupe on an orientation|scrape key: the panel only hears about real changes.
- Pushes pause during automoves and catch up immediately after (including the
  15s watchdog path); a config push resets the dedupe so a (re)opened panel
  syncs instantly.
- Pushes reuse the exact {dom, orient, fenresponse} shape of the old poll
  replies, so the panel consumes both through one code path.
- The {queryfen} poll survives as a fallback only, clamped to >=1s -- a saved
  10ms fen_refresh from an old install can't reinstate the stampede. UI renamed
  to 'Fallback Poll'; fresh-install default is now 1000ms.
- Puzzle mode's reply-confirm loop gets a fixed 50ms cadence instead of
  piggybacking on fen_refresh.

At idle this is 0 scrapes/s and 0 forced layouts/s (plus the 1/s fallback);
detection latency is mutation+30ms instead of being bounded by the poll round
trip. Verified by a jsdom harness driving the shipped content-script: 16
assertions covering initial push on config, dedupe, burst coalescing, overlay
filtering, automove suppression + catch-up, transient-'no' rejection, and the
intact queryfen fallback. All pass; node --check on every modified file.
IchNukeDichWeg and others added 30 commits August 7, 2026 01:26
Four changes, all of them answers to the same problem: a move that does not happen looks exactly
like a bug, and the evidence was either absent or hidden.

WHY. Every path that ended in "no move" ended in silence. They now end in one sentence in the panel,
under the readout -- autoplay off, tab not focused, board moved on, a move already in flight, a
board that is not playable, a square that is not on screen. dropMove() traces AND tells the panel;
the seven drop sites all go through it. The four-player skip says which gate stopped it, including
the case where the seat could not be read.

VERBOSE LOGGING. The trace is quiet while the tab is focused unless Premove happens to be on. That
is the right default and the wrong thing to have to guess at, and it hid the cause of a bug for
three sessions. Settings -> Diagnostics -> Verbose Logging forces it on, live, no reload.

COPY DIAGNOSTICS. A clipboard button in the panel: version, browser, bundled assets, connected
hosts, granted permissions, what was detected, why the last move was or was not played, the toggles
that are on, and the last 200 trace lines from the worker's ring. Deliberately carries no full URL,
no query string and no extension id, so it can be pasted into a public issue without a second
thought -- the ladder asserts all three.

SCRAPE WATCHDOG. Above 60 scrapes/second -- a rate real play never reaches -- scraping backs off for
two seconds and says so once. A mutation loop pinning the main thread is how a browser gets wedged
with nothing in the log but thousands of identical lines.

background-script.js: a bounded trace ring and the report that reads it
content-script.js: verbose gate, the scrape watchdog, dropMove at every drop site
popup.js/popup.html/popup.css: the reason line and the clipboard button, light and dark
general.html/general.js: the Diagnostics section and the Verbose Logging switch
src/i18n/locales/*.json: five strings in fourteen languages

Type: improvement
STAGED. Every file goes to .mephisto-staging first, so a complete copy of the new version is on
disk before anything in place is touched. True atomicity is not available -- the extension id comes
from the folder path, so the folder cannot be swapped out from under itself -- and calling this
recoverable rather than atomic is the honest description.

UNDOABLE. The files an update is about to replace are copied to .mephisto-backup/<version> with a
manifest naming them, and the ones it ADDS are recorded separately so rolling back deletes them
rather than leaving two builds mixed together. Roll Back appears in Settings only when there is
something to go back to. One generation: what you want is the version that worked ten minutes ago.

RESUMABLE. A staging folder left behind means the move step did not finish. Settings offers to
finish it from disk -- the bytes are already there and already verified -- and takes its own backup
first if the interrupted run never got that far. It will not overwrite a good backup with a
half-updated folder, which would destroy the only copy of the version being restored to.

WHAT CHANGED. The update check now carries the release's first sentence, and the panel shows it once
after an installation: "Updated to v3.1.217 — ...". It outranks both the incomplete-install warning
and the next-update notice, because straight after an install that is the only thing worth saying.

Verified on a real filesystem against the published 3.1.216 archive: staging leaves the install
untouched, the backup holds the OLD bytes and never the engines, the install lands without
disturbing files the archive does not contain, and rolling back leaves the folder BYTE-IDENTICAL to
before the update. The interrupted-install path completes to the same result.

updater.js: staging, backup, rollback, rollbackInfo, pendingInstall, finishStaged
background-script.js: updateCheck carries the release name and its first sentence
popup.js: the what's-new note, and one UPDATE_REPO_SLUG instead of three copies of the url
general.html/general.js: Roll Back and Finish Interrupted Update, shown only when they apply
src/i18n/locales/*.json: five strings in fourteen languages

Type: new idea
The button row is absolutely positioned by id on a 45px pitch, and its own comment does the
arithmetic: five icons plus FEN/PGN is exactly what fits in a 378px column. A sixth had no rule at
all, so it fell out of the layout, took Materialize's default filled teal, and landed on the board.
Reported as "horrible placement", which it was.

It is now D on the game page -- rebindable under Hotkeys, beside Copy FEN and Copy PGN, which is
where someone would look for it -- and a Copy Diagnostics button in Settings, for when the panel is
not open. The hotkey confirms itself in the reason line, since there is no longer a button to flash
a tick on and a shortcut that appears to do nothing is worse than no shortcut.

The ladder now asserts the toolbar still holds exactly its five, and the live panel check asserts no
button is visible outside it -- the class of mistake that put one on the board in the first place.

popup.html: the stray button removed
popup.js: copy_diagnostics() as a function, bound to the hotkey, confirming in the reason line
config-store.js: default binding 'd'
general.html/general.js: a Copy Diagnostics button, and the hotkey listed and rebindable
src/i18n/locales/*.json: two strings in fourteen languages

Type: bug fix
A hotkey is not something people use. Copy Diagnostics is now a labelled button in the panel's
Engine tab, confirming with a tick on the button itself. Not the toolbar row: five icons at a 45px
pitch is exactly what fits there, which is what put a stray one on the board last time. The hotkey
stays wired, since it is one line, but the button is the way in.

THE REASON LINE WAS TOO CHATTY. Four of the seven cases were removed after seeing them in use:
"not your turn" fired on every four-player ply, "Autoplay is off" says what the toggle already
says (and could barely fire anyway -- the panel does not send a move with autoplay off, so it
needed the two config copies to disagree), "this tab is not focused" cannot be read by definition,
and "a move is already being played" is transient and self-correcting. All four are still traced.
What is left is rare and actionable: a four-player board that is not playable, a move the board
cannot understand, a square not on screen, and a position that moved on mid-search.

It was also off-centre, because everything in that flow spans the full 378px panel while only the
left 188px is visible beside the settings column. Same 190px clearance the notices use.

popup.html/popup.css: the Copy Diagnostics row, and the reason line inside the visible column
popup.js: the button, and the four-player lane no longer narrates every ply
content-script.js: three drops back to trace-only

Type: improvement
MEASURED: the panel payload was 973,659 bytes of CSS, of which 784,724 was base64 for the wood,
marble and newspaper board textures. Only one of the twelve themes can be on screen at a time, and
nine of them are flat colours needing none at all -- so on the default theme every byte of that was
waste, shipped over sendMessage and parsed into the shadow root on EVERY open of the floating panel.
The toolbar popup never paid it: it is a real page and loads its own CSS from disk.

The panel now says which theme it is about to use and gets that texture, or none.

    default (brown)   973,659 -> 189,909 bytes
    wood selected     973,659 -> 538,519 bytes
    first open, cold worker   145ms -> 82ms
    second open, warm          11ms ->  5ms

FOOTPRINT: a texture that is not carried is replaced with `none`, not left as
url(/res/chessboards/wood.jpeg). Left alone, that resolves against THE SITE if the rule is ever
applied -- a 404 the page can see in its Resource Timing, which is the exact thing this whole
asset-shipping design exists to avoid. The ladder asserts no /res/chessboards/ reference survives in
either case, and that the neutralising happens before the remaining refs are collected.

The cache is keyed by theme, so switching board rebuilds rather than serving the wrong one.

background-script.js: buildPanelAssets takes the theme; inlineCssUrls carries one texture at most
content-script.js: the panel asks for the theme it is about to use

Type: improvement
Reported: ten seconds of nothing on screen after a browser restart, then switching to the toolbar
popup did nothing either. Both modes go through the service worker, so the wait was there -- but
toggleOverlay awaited the assets BEFORE creating any visible element, so the entire time there was
no frame, no spinner and no error. Indistinguishable from the extension being broken, which is how
it was reported.

A placeholder now goes up before anything is awaited, styled inline because the panel's own CSS is
exactly what is still being waited for. It carries the panel id, so clicking the icon again
dismisses it. After 2.5s it stops saying "starting" and names the reason -- the background worker
can be slow after a browser restart. The request is time-boxed at 20s, because sendMessage to a
worker that never answers hangs for as long as Chrome likes. Every failure path now writes into
that frame instead of into a console nobody had open, and traces ungated so it reaches the
diagnostics report.

This does NOT explain the ten seconds; it makes them visible and survivable. The stall itself needs
a trace from a real occurrence.

content-script.js: a placeholder before the await, a slow-start message, a timeout, visible failures

Type: bug fix
PROMOTION is no longer left to you. chess.com opens a 2x2 panel over the board -- queen and bishop
on the top row, rook and knight beneath -- and the clicker now picks from it. FOUND BY SHAPE, NOT BY
CLASS NAME: every class on that board is generated, and a selector guessed from a screenshot is one
that clicks the wrong thing the day it changes. It looks for a small container over the board with
exactly four similarly-sized children in two rows of two. If nothing matches that description
NOTHING IS CLICKED -- the move is already played, so leaving the piece to you is what happened
before, and a wrong guess must never become a wrong piece. Eight cases pin the shapes it must
refuse: too large, three children, a single row of four, one nowhere near the board.

ELIMINATION was worse than untested, it was wrong. The turn came from counting filled cells in the
move table and taking count % 4. chess.com lays that table out as four columns in seat order, so
once a seat is knocked out its column stops filling, every later round holds three moves instead of
four, and the modulo drifts one seat further out per round. Reading the POSITION of the last filled
cell instead is correct with or without an elimination, and gives the identical answer while all
four are alive (verified against the measured live case: 20 cells, Red to move). The logic moved
into a pure function so a synthetic elimination can be driven without a DOM -- which is the only
test it will ever get, since no real game has been seen past one.

MULTI-PV was one option away the whole time: Tetrarch declares MultiPV 1-64 and emits one
`info ... multipv N` per line, and the host already keys them by rank and returns them in order.
The panel now asks, and draws an arrow per line in the same palette and widths the 8x8 board uses,
deduped because transposing lines share a first move.

The ladder caught my promotion wait building its deadline out of its own step size -- the shape it
forbids outright, because Chrome clamps timers to one a second in a hidden tab and a summed
deadline is then out by 20x. It uses a real clock now.

content-script.js: promote4PC found by shape; fourPCTurnFrom, elimination-correct
popup.js: MultiPV sent to Tetrarch, one hint arrow per line

Type: new idea
The engine lines were reaching the PAGE board and nowhere else: the panel's own 14x14 board still
highlighted only the best move, and the eval list under it -- the coloured legend the 8x8 board gets
-- was never populated at all. Three different amounts of information about one search.

The arrow specs are built ONCE now and used three times. The 14x14 board takes a list rather than a
single move, draws them back to front so the best sits on top, and uses each line's own colour and
width in one svg rather than one per arrow. The eval list gets a four-player renderer: no chess.js
for a 14x14 board means no SAN, so the moves are the coordinates the rest of this lane already uses,
and the score is normalised to YOUR team the same way the readout is -- otherwise a line reads +3
and -3 on alternate plies.

SPACING, both reported. The Copy Diagnostics row wrapped into two cramped lines because a plain
.qs-row carries no layout of its own: the toggle rows bring their own flex and the select rows are
laid out by a full-width select. It is label-left/button-right on one line now. And the settings
page title sat directly on top of the first section heading, since `#title` has no bottom padding
and every section only spaces itself from the one BEFORE it -- the first had nothing above it.

Measured in a real browser: the row is on one line, and there are 19px under the page title.

panel-board.js: highlight() takes a list; one svg, best drawn last
popup.js: fourpc_arrow_specs and render_alt_lines_4pc, shared by both boards and the list
popup.css: the diagnostics row on one line
options.css: air between the page title and the first section

Type: improvement
A queen promotion picking the queen proves index 0 is the top-left cell FOR A SEAT AT THE BOTTOM --
Red, in the game it was tested in. It does not prove the picker keeps that layout for the other
three seats: chess.com rotates everything else with the board, and if it rotates this too the order
is wrong for Blue, Yellow and Green. The symptom would be a QUEEN promotion picking something else
while Red kept working, which is worth knowing before it is puzzled over.

content-script.js: note what is confirmed and what is assumed, where the order is declared

Type: docs
The promotion picker is wired, so it no longer leaves the piece to you; multiple lines are drawn on
both boards with a score list beneath; and the roadmap item still listed two blockers that are gone.
The settings reference gained the diagnostics pair.

README.md: four-player promotions and multi-PV, the roadmap item, a Verbose Logging row

Type: docs
Carries everything since 3.1.216: the diagnostics cluster, updates that are undoable and resumable,
four-player promotions, elimination-correct turn order and multi-PV, a panel payload cut by 79%,
and a floating panel that can no longer open into silence.

manifest.json: 3.1.216 -> 3.1.217

Type: tooling
Clock Mode paces the engine's SEARCH. Nothing paced the simulated human
delay, so in a ten-second scramble a move still spent a fixed think pause
plus 400ms of cursor travel, which is exactly when it cannot be afforded.
Its own opt-in rather than a change to Clock Mode: that toggle has always
meant "pace the search", and widening it would change what it does for
everyone already using it. Off, the timing object is returned untouched.

popup.js: clock_move_budget_ms split out ungated, clock_pace_timing, fresh_timing routes through it
popup.html: Pace to Clock row after Mirror Time
config-store.js: default hotkey k
general.html: Pace to Clock row in Engine settings
general.js: form element, rebind label and order
i18n/locales: panel.pace_to_clock + its tooltip, 14 languages

Type: new idea

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A new options page that takes a PGN -- pasted, from a file, or fetched from
chess.com's public archive -- and reviews it with the extension's own engines.
Every position is searched once, so the score before a move and the score after
it come from the same search at the same budget. Accuracy, move classification,
alternate lines, an eval graph, think time from the clock comments, an optional
Maia pass for how expected each move was, and a set of fair-play MEASUREMENTS
that deliberately refuse to add up to a verdict. Nothing is uploaded and the
report exports as one self-contained HTML file.

options/pages/review/review.html: the page
options/pages/review/review.css: its styles, on the options page's own variables
options/pages/review/review-core.js: PGN reader, eval bookkeeping, accuracy, indicators
options/pages/review/review.js: engine drivers, rendering, export
options/options.html: nav entry, panel-board + review-core, chessboard.css
i18n/locales: nav.review + the page's strings, 14 languages

Type: new idea

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hand-maintained constant with a comment saying "bump on every content-script
change". It sat at 3.1.213 through eighty-one commits to that file, so the page
console line and the stuck-scraper trace both named a build that had not run
since v3.1.213. A version marker nobody can trust is worse than none, so it
reads the manifest instead.

content-script.js: MEPHISTO_BUILD comes from chrome.runtime.getManifest()

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A search that outlived the tab it was started from was the real one: the worker
frees a panel's engine when its tab closes, keyed by tab id, and this client is
deliberately not one -- so closing the options page mid-run left a
multi-threaded search burning cores with nobody watching it.

review.js: dispose the engine on pagehide; thread default from the machine; DOM writes survive a page swap
review-core.js: one unanalysed position no longer renders the whole game's accuracy as NaN; unexported the helpers with no caller
review.html: dropped two ids nothing reads

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md: Game review section, Pace to Clock beside Clock Mode, settings-reference row, shipped list
docs/game-review.png: the report on the sample game

Type: docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trace ring lives in the worker's memory, so when the WORKER is the thing
that was asleep -- the ten seconds of nothing after a browser restart -- the
ring is empty by definition and Copy Diagnostics explains nothing. The startup
marks are persisted instead, so the next report describes a cold start the
worker did not live to describe. Measured on this Mac: a genuine worker restart
runs the whole script body in 262ms, while the panel open on that same restart
took 3,405ms -- so the cost is Chrome spawning the worker, not anything in
here. First install is different again (offscreen document +18s), which is
exactly the sort of thing that should be a number rather than a hunch.

background-script.js: WORKER_T0 + per-import marks, persisted to storage, shown in the report
content-script.js: the panel times its own round trip to the worker, traced ungated

Type: tooling

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Declared inside the try and read from the catch, where a block-scoped const does
not exist -- so the one path whose entire job is to report a failed start threw
its own ReferenceError instead of the message it was holding. Stamped above the
try, where both paths can see it.

content-script.js: assetsAsked is stamped before the request, not inside it

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t that exports itself

Nine changes asked for after the first round of use.

The human model moves up beside the engine, where the choice actually belongs,
and now reports Maia's OWN RANKING of the played move rather than a yes/no --
"the human model had this fourth" says something a boolean cannot. Maia 1 gains
the bands it actually ships, up to 2200.

Native engines are no longer locked out of depth mode: the request carries both
budgets, so a host that understands `depth` uses it and one that predates the
field ignores it and uses `time` exactly as before.

The fair-play section is the other half. The headline rate now excludes book
moves, forced moves and recaptures -- finding the only legal recapture is not
evidence of anything, and counting it is what makes a naive match rate read 80%
for everyone. Added: per-phase rates, the longest unbroken engine streak, how
uniform the accuracy is, whether the longer thinks went to the harder positions,
and the engine-said-yes / human-model-said-no count. Four levels now, with a key
that is printed from the same list the levels are assigned from.

The export is no longer a second, simpler renderer that drifted from the real one
within a day. It clones what is on screen, inlines the three stylesheets and the
twelve piece images, and adds the full move table the page shows one row at a
time. No script, nothing to fetch.

review.js: Maia ranking, native depth, arrows, phases on the graph, titles, DD.MM.YYYY, export
review-core.js: the divider ported for phases, date formatting, the wider indicator set and its levels
review.html: human model beside the engine, bands to 2200, the level key
review.css: first-section spacing, level key, move table, two columns down to 900px
uci-native-host.py: an analyse request may carry a depth

Type: improvement

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Lichess opening explorer answers 401 at its proxy, before the application
sees the request -- both hosts, both databases, with and without a token, from
a plain unauthenticated fetch. Whether that is an auth requirement or a network
block is not something this code can find out or fix.

Naming an opening never needed a server anyway: the answer is a property of the
position. This is lichess-org/chess-openings (CC0, a collection of facts in the
public domain), 3,810 named lines replayed once and keyed on the POSITION rather
than the move order -- so a transposition is named correctly, which a move-list
lookup cannot do. Offline, instant, and one fewer thing that can go down.

openings.json: 3,810 openings keyed by position

Type: new idea

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…runs

The fair-play section ended in eleven numbers and no summary, which is not more
honest than a summary -- it just moves the summarising to whoever has less
information. It now leads with one estimate per player: a weighted vote over the
evidence lines, with the weights set by how hard a line is to reach by playing
well, plus how much the estimate is worth given what the game actually carried.
It is not a probability and says so.

Human likeness is a second reading of the same game by the other judge: how
EXPECTED each move was from a player of that rating, rather than how good. Its
own opt-in section, off by default, and it names the moves the engine ranked
first that the human model did not see coming.

Batch review answers the question one game cannot. Every game in the box is
analysed against one engine load, and each player's numbers are pooled across
all of them.

THE BUG THIS FOUND, which the batch refactor introduced: both engines used the
same offscreen client id. The host keys everything on it, so starting the human
model DISPOSED the analysis engine and answered in its place -- every eval after
that was a depth-1 human-likelihood score. It read as a sawtooth on the eval
graph and as a flawless game in the numbers. Each engine has its own id now.

review.js: the estimate, human-likeness and batch sections; openings read from the bundled table; one client id per engine; depth 16 / 1000ms defaults; the export carries the PGN
review-core.js: estimate() and its weights
review.html: two toggles, two sections, two table-of-contents entries
review.css: the estimate block, the pooled tables, the PGN block
i18n/locales: four strings and two tooltips

Type: improvement

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lockout was written when the host took a time budget and nothing else. The
host takes a depth now, and one that predates the field falls back to the time
cap, so there is no combination left that cannot run -- but the option stayed
disabled and still said "needs a time budget". A missing host is the only
reason an engine is unselectable now.

An earlier pass thought it had removed this. It replaced a block that had
already moved, without asserting the anchor matched, so the edit silently did
nothing and the old text shipped. The ladder pins the rule now rather than the
edit.

review.js: updateEngineOptions derives disabled from the host alone

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirmed in real games from every colour, which was the one thing the shape
matcher could not be sure of. Chess.com rotates the BOARD per player but draws
the promotion panel in screen orientation, so index 0 is the top-left cell
whoever is promoting and one reading order serves all four seats.

content-script.js: the note above FOURPC_PROMO_ORDER says confirmed, not assumed
README.md: the same, where it describes the picker

Type: docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md: Game review rewritten for the estimate, human likeness, batch runs, offline openings, phases, titles and the export; a Game Review row in the settings reference; Copy Diagnostics carries the worker cold starts

Type: docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.217 -> 3.1.218

Type: tooling

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lichess put the opening explorer behind OAuth: every explorer route answers 401
at its proxy while the tablebase, which declares `security: []`, still answers
anonymously. Their own site gets through on a session cookie, but the explorer
answers `access-control-allow-origin: *` to every origin except lichess.org's,
and a wildcard forbids credentials -- so no extension can ever use that route.
A per-user token is the only mechanism left open.

It is a credential and is handled as one: a password field, dropped from an
exported settings file, and present in the diagnostics report only as "set" or
"not set". Both of those files exist to be sent to other people. A 401 now names
the setting that fixes it instead of reading as a dead endpoint.

Not verified end to end: I have no token and did not make one, so that a VALID
token is accepted is inferred from lichess's own API spec, not tested. The
header is confirmed to be sent, and rejected as expected with a fake one.

background-script.js: lichessAuthHeader on the explorer fetch, a 401 that names the fix, "token set" in the report
general.html / general.js: the field, and its registration
SettingsPage.js: an export never carries it
options.css: .set-secret
README.md: the setting, and what it is for
i18n/locales: two strings

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.218 -> 3.1.219

Type: tooling

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They dispatched only 'change', and FormElement binds 'input' for a number field
and 'change' only for checkbox/select -- so every stepper on that page moved the
number on screen and stored nothing. Leave the page and it had reverted. Typing
into the same field always worked, which is exactly what hid it: the value was
there until you navigated away.

Affects every number on the page: Elo, Search Time, Fallback Poll Interval, and
all four Simulated Think/Move fields. Confirmed on Search Time and Elo before
and after -- the + button showed 350 with nothing stored, and now stores 350.
'change' is still dispatched, because the humanize mix rows listen for it.

general.js: initSteppers dispatches 'input' as well, and its comment no longer says the wrong thing

Type: bug fix

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 300ms pause before a database move's first click is the one number that has
to be tuned per machine: it exists because a known solution has no search in
front of it to let the board finish animating, and how long that takes depends
on the page and the machine. Raise it if moves are being dropped as mismatches,
lower it to solve faster, zero to disable.

Read fresh per move rather than from the config snapshot, so it can be dragged
while watching puzzles solve. An unset, junk or out-of-range value falls back to
the 300 it has always been.

popup.js: puzzle_move_delay_ms(), and the constant is now the documented default
general.html: the row, with the same steppers every other number has
i18n/locales: the label and its tooltip

Type: new idea

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
manifest.json: 3.1.219 -> 3.1.220

Type: tooling

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant