Save the record log of a JavaScript run that failed in production and replay it anywhere with OneEcma: the same clock readings, random numbers and host-call results come back from the log, so the replay fails the same way and gives the same report, with each frame’s locals added.
The script below fails only sometimes: half of the time Math.random() picks a discount code that doesn’t exist. The program records runs until one fails, saves that run’s log, and replays it from the file in a second process.
using RecursiveAI.OneEcma; using RecursiveAI.OneEcma.Diagnostics; const string script = """ function pickDiscount() { const codes = { SPRING: 0.1, VIP: 0.2 }; const roll = Math.random(); return roll < 0.5 ? codes.SPRING : codes.VIP_PLUS; } const rate = exchangeRate("EUR"); const price = (100 * (1 - pickDiscount()) * rate).toFixed(2); if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`); price; """; if (args is ["replay", string path]) { // Later, anywhere: no host functions, no clock, no randomness. Everything comes from the log. RecordLog saved = RecordLog.FromArray(File.ReadAllBytes(path)); ReplayResult replay = ScriptEngine.Replay(saved); Console.WriteLine($"replayed {replay.Entries} entry call(s), divergence: {replay.Divergence ?? "none"}"); Console.WriteLine(DiagnosticRenderer.Render(replay.Report!, new RenderOptions { ShowLocals = true })); return; } // Record runs until one fails, then keep its log. for (int attempt = 1; ; attempt++) { var engine = new ScriptEngine(); engine.DefineGlobalFunction("exchangeRate", 1, (_, a) => JsValue.FromNumber(0.92)); // remembered by the log try { JsValue price = engine.Evaluate(script, "price.js"); Console.WriteLine($"run {attempt}: {engine.ToDisplayString(price)}"); } catch (JsException e) { byte[] log = engine.RecordLog.ToArray(); File.WriteAllBytes("failure.oerl", log); Console.WriteLine($"run {attempt} failed; saved {log.Length} bytes, {engine.RecordLog.EventCount} events, replayable: {engine.RecordLog.IsReplayable}"); Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); break; } }
dotnet runrun 1 failed; saved 462 bytes, 6 events, replayable: True error[RangeError]: price is NaN at 2026-10-27T19:17:21.167Z --> price.js:8:32 | 8 | if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`); | ^^^^^^^^^^ | = stack: <top-level> (price.js:8)
How many runs pass before one fails depends on the random numbers. Then, in another process, with no exchangeRate function defined:
dotnet run -- replay failure.oerlreplayed 1 entry call(s), divergence: none error[RangeError]: price is NaN at 2026-10-27T19:17:21.167Z --> price.js:8:32 | 8 | if (price === "NaN") throw new RangeError(`price is NaN at ${new Date().toISOString()}`); | ^^^^^^^^^^ | = stack: <top-level> (price.js:8) = locals in <top-level>: rate = 0.92, price = "NaN"
The replay throws at the same place with the same timestamp in the message, because new Date() read the recorded clock. Replaying the same file twice gives byte-identical output.
What the log holds
Recording is on by default: engine.RecordLog is the log of everything the run took from outside the engine, in order. That is every script and module evaluated, clock readings, Math.random() results, time zone and locale answers, host function results and exceptions, the settling of host tasks, job-queue drains and weak-reference collections. It costs a few appends per host interaction and nothing per instruction. A run with no host interaction logs its source plus about 20 bytes, Math.random() costs 9 bytes a call, and the log stops growing at 8 MiB by default (new RecordLog(maxBytes) through ScriptEngineOptions.Log).
ScriptEngine.Replay(log) builds a fresh engine whose host answers from the log alone. Host functions don’t run again, so their side effects don’t happen twice. Every event is checked against what the script asks for, and ReplayResult.Divergence says what differed if the code, the host functions or the engine changed since the recording.
Good to know
- This is the same mechanism that explains errors: when a run fails, the engine replays it with tracing on to find where values came from. That is why host setup belongs in
ScriptEngineOptions.Configure, which a replay calls again; pass the sameConfigureinReplayOptionswhen you replay a log yourself. - Functions made with
engine.DefineGlobalFunctionare rebuilt from the log, asexchangeRateis here. RecordLog.IsReplayableis false, with the reason inNotReplayableReason, after things a log can’t reproduce, such as using a second realm or a log that outgrew its limit.ReplayOptions.StopAfterEntrystops after a given entry call, andReplayResult.Engineis the replayed engine, for inspecting state at that point.- The byte format (version 1) and every event are in the record log.