Skip to content

feat(strategies): persist generator state so warm starts resume streams - #1121

Open
bilhokista wants to merge 1 commit into
drydocs:mainfrom
bilhokista:feat/persist-generator-state
Open

bilhokista wants to merge 1 commit into
drydocs:mainfrom
bilhokista:feat/persist-generator-state

Conversation

@bilhokista

@bilhokista bilhokista commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • XorShift64 state access (rng.ts):
    • getState(): bigint reads the state without advancing the stream.
    • static fromState(state) builds a generator at a given state. It goes through the existing constructor, so behaviour is unchanged.
    • fromState rejects anything outside 1..2^64-1 instead of masking it. No generator can hold such a state, so masking would quietly resume a different stream.
    • The existing sequences are unchanged. A test pins the first four draws of seed 42n, recorded from main before this change.
  • RunReport.generators: { start, end } (report.ts). Each side is a Record<streamId, bigint>, one entry per stream (e.g. price:USDC, or one per derivePathSeed path index), so every state says which stream it belongs to.
    • start is the seeded state for a fresh run, or the base report's end for a continuation.
    • end is the state at finalState.
    • Both sides are needed so the merge can verify continuity: start has to match where the base stopped, and end is where the next continuation resumes from.
  • Wire format. States are stored as decimal strings, like FixedPointDecimal stroops, so the 64-bit values survive JSON. On read, a malformed value throws TypeError and an out-of-range value throws RangeError. A missing generators block is also rejected.
  • Warm start. WarmStartContext.generatorStates carries the base's end states. The new restoreGenerators(ctx) turns them into XorShift64 instances keyed by stream id.
  • Merge. mergeReports throws RangeError naming the stream when continuation.generators.start doesn't match base.generators.end: a differing value, a missing or extra stream, or states swapped between paths. The merged report gets { start: base.start, end: continuation.end }.
  • REPORT_FORMAT_VERSION 1 → 2. Reading a v1 report (no state) throws UnknownReportVersionError.
  • I removed the "does not cover RNG continuity" caveat from the equivalence test and from the RunReport doc comment, as the issue suggests.

Breaking

The run-report wire format gains generators and the version goes up. Code that builds a RunReport by hand now has to supply generators ({ start: {}, end: {} } for a run that draws nothing). Within the monorepo, only report.ts and its tests build reports.

Out of scope / follow-up

scenarioPriceFeed still builds its path from market.startPrice with a fresh XorShift64(seedToBigInt(seed)), and generateGbmPath doesn't accept an existing generator. This PR gives reports and warm starts what they need to resume a stream. Switching the GBM scenario feed to resume from a stored state plus the last price is a separate change to the feed's API, and I've kept it out of this PR so it stays limited to the issue's five steps. I can open a follow-up if you want it.

Test plan

  • pnpm lint && pnpm typecheck && pnpm test in packages/strategies: lint/typecheck clean, 36 files / 714 tests pass
  • rng.test.ts:
    • a generator restored mid-stream (73 + 127 draws) produces the same sequence as an uninterrupted 200-draw run
    • states round-trip through decimal strings
    • out-of-range states are rejected
    • the pinned sequence for seed 42n is unchanged
  • report.test.ts:
    • path-driven warm-start equivalence: a toy strategy draws from one stream per asset. A T0→T2 run plus a warm start T2→T3 (via restoreGenerators) gives exactly the holdings, total value and generator states of an uninterrupted T0→T3 run.
    • the old behaviour (continuation restarted from the seed) gives a different result, and mergeReports now refuses it
    • merge rejects a differing state, a missing stream, an extra stream, and swapped states
    • the merged report's generator span is correct
    • wire round-trip exactness: a state > 2^53 is stored as "18364758544493064721"
    • malformed, out-of-range and missing states are rejected
    • a v1 report is rejected with UnknownReportVersionError
  • pnpm turbo run typecheck build --filter='!@meridian/contracts' passes. @meridian/contracts#build fails the same way on main locally because I don't have the Soroban toolchain.
  • api tests: 80/80 pass when run on their own, on both main and this branch. Under the combined turbo test run, @meridian/api#test failed for me with no Upstash/Redis env set. I haven't rerun the combined turbo run on main, but this PR doesn't touch api.

Closes #1018

🤖 Generated with Claude Code

XorShift64 gains getState() and fromState(), and RunReport records each
generator stream's start and end state as decimal strings in the wire
format. buildWarmStartContext hands the end states to the continuation,
and mergeReports rejects a continuation whose streams did not start where
the base run stopped. REPORT_FORMAT_VERSION moves to 2, so a version 1
report, which carries no state, is rejected on read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 18, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Collins' projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@bilhokista Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

This branch has not been deployed

No deployments
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.

[Feature] Persist generator state so a resumed run continues its stream

1 participant