Skip to content

Move to session last prediction outcome - #46

Merged
dmigwi merged 8 commits into
masterfrom
move-to-session-critical-turn-info
Sep 14, 2026
Merged

dmigwi merged 8 commits into
masterfrom
move-to-session-critical-turn-info

Conversation

@dmigwi

@dmigwi dmigwi commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

An agent's view of its own last turn now survives a reload, traversal speed is computed exactly, and the downloaded log records what it was written under and can be checked end to end.

  • The previous prediction outcome is saved with the round, so get_last_prediction_outcome no longer resets to a first-turn null after a reload.
  • Traversal speed rounds to nearest using exact integer arithmetic, so a speed rebuilt from its components downstream matches the one Tapoo logged.
  • The downloaded log records its storage schema and a checksum over its entries. Tapoo Oracle verifies the checksum.

Deploying this bumps the storage schema from 5.1 to 5.2, which discards every stored value: agent seats (including saved credentials and headers), logs, progress and win records. Download any logs worth keeping first.

master this branch
Tests 713 728
Statements 93.83% 93.74%
Branches 87.05% 87.29%

Statement coverage dips because two pieces of new code are not executed by the tests; both are named under Verification.

27 files, +762 / −52. App version 2.6.12.6.2. typecheck, lint, test and build:frontend are clean. Each new test was mutation-checked by reverting the logic it covers.


1. The last prediction outcome, saved with the round

get_last_prediction_outcome used to read the previous turn's outcome from memory only. After a same-tab reload or a poller rebind, the maze kept its progress while the tool reported a first-turn null, so the agent was told nothing had happened. lastActionResult now lives in game state and in the saved round snapshot.

Four decisions:

  • Copied at every boundary. cloneMazeActionResult runs wherever the outcome passes between the game, the agent control, the poller and storage. It copies every nested value (the submitted moves, the replay start cell, the moves schema), so nothing that still holds the original can change a saved outcome.
  • Validated on restore. An impossible outcome, such as a negative applied-move index, makes isValidPersistedRound reject the snapshot like any other inconsistency in a restored round.
  • Scoped to the round. It is restored when the agent control is rebound and cleared when a new round starts, in every control mode. Interactive mode's clearActionResult does nothing, so the reset inside startRoundWithDimensions is the only thing clearing it there.
  • Recorded before the commit, not after. __commitAgentTurn saves the snapshot. Both commit paths (replayed moves and rejected responses) now record the turn's merged outcome first. Before, a turn was saved holding the previous turn's outcome, so a tab killed before the next save restored this turn's board with last turn's moves. The two tests for this fail on the old order and pass on the new one.

2. Traversal speed: rounding and exact arithmetic

Speed units now round to nearest. The one exception is within one unit of 1.0000x, where plain rounding could push a value into the wrong class: there, values below are floored and values above are ceiled.

Before, backtracker speeds were always floored and trailblazer speeds always ceiled. So 4/3 showed as 1.3334x while other values rounded down, and a speed rebuilt from its components downstream could land on a different figure than the one logged.

The arithmetic now multiplies before it divides:

const scaledSpeed = (uniqueCellsVisited * scoring.traversalSpeedScaleUnits) / scoreDecayUnits

uniqueCellsVisited * scale is an exact integer, so this is a single correctly-rounded division. The old (U / D) * scale rounds twice and can push an exact tie below .5. Checked against exact integer rounding for every U and D up to 6000:

Disagreements
(U / D) * scale 599
(U * scale) / D 0

57/800 is exactly 712.5 units but used to come out as 712. The parity harness's copy of the calculation uses the same formula. The benchmark's conservative minimum winning speed example moves from 0.9900x to 0.9901x.

3. Storage schema 5.2 and the privacy policy

Win-speed records saved under the old rounding can sit one unit off the new rounding, so repeating an identical win could read as "0.0001 slower". Tapoo has no legacy data support, so the fix is not an allowance for old records but a schema bump, which clears everything written under 5.1.

The privacy policy now states what that guarantees: a downloaded log contains only data written under the storage schema the running build supports. Old entries are never migrated or read, so a version change cannot rewrite or corrupt stored data.

4. The downloaded log file

before: tapoo-v2.6.1-agent-api-logs-1789378730.json
after:  tapoo-logs-schema5.2-v2.6.2-1789378730123.json

Three decisions:

  • No mode in the name. Only agent-api logs can be downloaded, so it never varied.
  • The schema is in the name, so logs from different schemas can be told apart before either is opened.
  • The full millisecond timestamp comes last. Downstream tabs trim a long name from the front; at a 320px viewport only nine digits and .json remain. Those last characters are what tell runs apart, including experiments started within the same second on one machine. The number is the first entry's own epochMs, so a filename can be searched for inside its log.

The envelope gains two fields:

  • storageVersion: the schema the entries were written under. It is separate from the app version, since two releases can share a schema. It is a string because it is an identifier: as a number, 5.10 would read back as 5.1.
  • entriesChecksum: FNV-1a 64 over the entries serialized compactly. A consumer can verify it from the file alone: fnv1a64Checksum(JSON.stringify(JSON.parse(text).entries)). It catches entries edited between download and report generation. It is not a signature: the algorithm is public and uses no key, so a deliberate rewrite can simply recompute it.

5. The checksum at log scale

Logs can reach 100 MB. fnv1a64Checksum did one BigInt operation per byte and hashed a full serialized copy, which made it the slowest step of a download. Measured in Node's V8 on 92,152 generated entries (about 100 MB):

Time Extra memory
BigInt per byte, one joined string 1163 ms a 100 MB string plus a 100 MB byte copy
16-bit limbs, entry by entry 346 ms one reused buffer the size of the largest entry
for scale: JSON.stringify(payload, null, 2) 131 ms

The rewrite exposes three functions:

createFnv1a64(): {update(text): void; digest(): string}       // the hash, a piece at a time
fnv1a64Checksum(text): string                                 // one update over one string
checksumEntries(entries, yieldToPage?, now?): Promise<string> // "[", each entry's JSON, ",", "]"
  • Limbs instead of BigInt. The prime is 2^40 + 435, so each multiply is each limb times 435, plus two limbs shifted up by two limbs and 8 bits. Every intermediate value stays below 2^31.
  • The encoder's own UTF-8. encodeInto writes into one reused buffer, so the bytes are exactly TextEncoder's, lone surrogates included.
  • Entry by entry. An array's JSON is its elements' JSON joined by commas, so hashing those pieces in order gives the same digest as JSON.stringify(entries) while holding one entry's text at a time.
  • Yields to the page. Whenever a slice runs past one frame (16 ms), it yields via MessageChannel rather than setTimeout, which browsers clamp to at least 4 ms per call. A log small enough to finish in one slice never yields.

A first attempt with two 32-bit halves was both wrong and slower. JavaScript's ^ returns a signed integer, which corrupted the carry into the high half, and a function call per byte cost 1760 ms. The test suite keeps a BigInt implementation written straight from the specification as a reference, so the limb version is checked against an independent implementation rather than against itself.

6. Tapoo Oracle

Submodule 82d835ffc36ea4:

  • speed decomposition into efficiency, batching and accuracy (#10)
  • the speed parsing fix (#11)
  • entries checksum verification (#12)

Oracle shows storageVersion in the report's provenance. It recomputes entriesChecksum over envelope.entries exactly as stored, before unreadable entries are filtered out, so a log carrying a decode stand-in does not falsely fail. A mismatch stops the report and states both digests. A log without the field, written before this branch, loads as before.


Verification

  • 728 tests across 36 files; typecheck, lint and build:frontend are clean. The branch does not touch Go, and the Go tests were not run.

  • Mutations confirmed to fail their tests, each applied and then reverted:

    • the two-step speed formula restored
    • seconds restored in place of the millisecond timestamp
    • the checksum taken over pretty-printed entries
    • the top limb left unmasked
    • entries joined without JSON's comma
    • a slice that never yields
    • the snapshot saving a null outcome
    • restore skipping outcome validation
    • a rebind that does not restore the outcome
    • the clone sharing any of its nested values (the moves array, the start cell, the schema)
    • a new round keeping the previous outcome, tested in interactive mode

    The save-before-commit tests fail on the old order and pass on the new one.

  • Coverage dip: the new statements the tests don't execute are:

    • yieldToEventLoop (logs.ts:426-433). Its MessageChannel path only runs in a browser, because the tests inject the yield.
    • the non-object rejection in isValidMazeActionResult (traversal.ts:396).
  • The checksum implementations agree:

    • the limb hash matches the BigInt reference on 14 hand-picked strings (2-, 3- and 4-byte characters, lone high and low surrogates, a 70,000-unit string) and on 2,000 random strings across the full UTF-16 range
    • over the 100 MB of entries, the entry-by-entry checksum equals the whole-string one
  • Oracle agrees with Tapoo: Oracle's own checksumEntries was run against the specification reference through a real pretty-print and parse round trip, with emoji, lone surrogates, -0, non-ASCII keys and decode stand-ins in the entries. It matched, and an entry edited after download was refused. Oracle's suite passes at fc36ea4 (717 tests).

@dmigwi dmigwi changed the title Move to session critical turn info Move to session last prediction outcome Sep 14, 2026
@dmigwi
dmigwi merged commit 70074a7 into master Sep 14, 2026
6 checks passed
@dmigwi
dmigwi deleted the move-to-session-critical-turn-info branch September 14, 2026 15:17
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