[PR #539] [MERGED] fix(replay): share one seeded generator and present builtins with real identity #2060

Closed
opened 2026-10-16 21:13:51 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/539
Author: @morluto
Created: 10/5/2026
Status: ✅ Merged
Merged: 10/5/2026
Merged by: @morluto

Base: main ← Head: cleanup/replay-determinism


📝 Commits (1)

  • 173e5f6 fix(replay): share one seeded generator and present builtins with real identity

📊 Changes

5 files changed (+176 additions, -19 deletions)

View changed files

📝 src/application/JavaScriptReplayPlanning.ts (+7 -7)
📝 src/replay/JavaScriptReplayWorker.ts (+19 -12)
➕ src/replay/seededRandom.test.ts (+43 -0)
➕ src/replay/seededRandom.ts (+53 -0)
📝 tests/boundary/providers/replay/javascriptReplayWorker.test.ts (+54 -0)

📄 Description

Two replay-sandbox defects, both verified by running the real worker first.

1. A zero seed silently destroyed the random stream. xorshift32 has a fixed point at 0 — seeded there it returns 0 forever, so Math.random() returned 0 on every call and two runs matched for entirely the wrong reason. Confirmed: seed 0 produced 0.0000 0.0000 0.0000; seed 1 produced 0.0001 0.0157 0.6164. normalizeXorshift32Seed substitutes a fixed non-zero state, and the planner side keeps its existing golden constant so its generated corpus is unchanged.

The deeper problem is that the identical loop was inlined in two modules (JavaScriptReplayWorker and JavaScriptReplayPlanning). A fix to one would not have reached the other, and the planner's seed || 0x9e3779b9 already masked the bug there by accident. The algorithm now lives in one module, src/replay/seededRandom.ts, with unit tests at its interface — so the two callers cannot drift.

2. The substitutes broke feature detection. Bundles routinely branch on builtin identity, so these took a different path in replay than in production:

  • Date.name was "ReplayDate" and Date.length was 0 (real: "Date", 7).
  • Math was built with Object.create(Math), which gave it Math as its prototype and made Math.random non-writable and non-configurable, where the real builtin is writable and configurable.

Math is now a proxy that intercepts only random and forwards everything else, so the prototype chain and descriptors are the real ones. Date gets its name/length defined explicitly. Verified through the actual compiled worker subprocess: [Date.name, Date.length] returns ["Date", 7], and the Math.random descriptor now reports configurable: true, writable: true with Math.max/Math.floor/Math.PI untouched.

One test assertion I dropped rather than "fixed": Object.getPrototypeOf(Math) === Object.prototype is false inside the vm regardless of implementation, because the host and the vm are separate realms with distinct Object.prototype. That is realm semantics, not a defect, so the test asserts the descriptor and the untouched members instead.

Tests: new seededRandom.test.ts at the module interface plus four end-to-end worker cases. Full suite green: 1929 passed.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/539 **Author:** [@morluto](https://github.com/morluto) **Created:** 10/5/2026 **Status:** ✅ Merged **Merged:** 10/5/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `cleanup/replay-determinism` --- ### 📝 Commits (1) - [`173e5f6`](https://github.com/morluto/rea/commit/173e5f658224496cb5618d0e4c6ffa3ff903398e) fix(replay): share one seeded generator and present builtins with real identity ### 📊 Changes **5 files changed** (+176 additions, -19 deletions) <details> <summary>View changed files</summary> 📝 `src/application/JavaScriptReplayPlanning.ts` (+7 -7) 📝 `src/replay/JavaScriptReplayWorker.ts` (+19 -12) ➕ `src/replay/seededRandom.test.ts` (+43 -0) ➕ `src/replay/seededRandom.ts` (+53 -0) 📝 `tests/boundary/providers/replay/javascriptReplayWorker.test.ts` (+54 -0) </details> ### 📄 Description Two replay-sandbox defects, both verified by running the real worker first. **1. A zero seed silently destroyed the random stream.** xorshift32 has a fixed point at 0 — seeded there it returns 0 forever, so `Math.random()` returned `0` on every call and two runs matched for entirely the wrong reason. Confirmed: seed 0 produced `0.0000 0.0000 0.0000`; seed 1 produced `0.0001 0.0157 0.6164`. `normalizeXorshift32Seed` substitutes a fixed non-zero state, and the planner side keeps its existing golden constant so its generated corpus is unchanged. The deeper problem is that the identical loop was inlined in **two** modules (`JavaScriptReplayWorker` and `JavaScriptReplayPlanning`). A fix to one would not have reached the other, and the planner's `seed || 0x9e3779b9` already masked the bug there by accident. The algorithm now lives in one module, `src/replay/seededRandom.ts`, with unit tests at its interface — so the two callers cannot drift. **2. The substitutes broke feature detection.** Bundles routinely branch on builtin identity, so these took a different path in replay than in production: - `Date.name` was `"ReplayDate"` and `Date.length` was `0` (real: `"Date"`, `7`). - `Math` was built with `Object.create(Math)`, which gave it `Math` as its prototype and made `Math.random` non-writable and non-configurable, where the real builtin is writable and configurable. `Math` is now a proxy that intercepts only `random` and forwards everything else, so the prototype chain and descriptors are the real ones. `Date` gets its `name`/`length` defined explicitly. Verified through the actual compiled worker subprocess: `[Date.name, Date.length]` returns `["Date", 7]`, and the `Math.random` descriptor now reports `configurable: true, writable: true` with `Math.max`/`Math.floor`/`Math.PI` untouched. One test assertion I dropped rather than "fixed": `Object.getPrototypeOf(Math) === Object.prototype` is `false` inside the vm regardless of implementation, because the host and the vm are separate realms with distinct `Object.prototype`. That is realm semantics, not a defect, so the test asserts the descriptor and the untouched members instead. Tests: new `seededRandom.test.ts` at the module interface plus four end-to-end worker cases. Full suite green: 1929 passed. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-16 21:13:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2060
No description provided.