Skip to content

Move reorg detection into Rust BlockStore - #1405

Merged
DZakh merged 47 commits into
mainfrom
claude/block-store-reorg-tracking-2ucqi4
Aug 13, 2026
Merged

DZakh merged 47 commits into
mainfrom
claude/block-store-reorg-tracking-2ucqi4

Conversation

@DZakh

@DZakh DZakh commented Jul 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Reorg detection moves out of the ReScript ReorgDetection module and into the Rust BlockStore, where hash comparison happens directly on stored block hashes as a fetch-response page is merged into the per-chain store. The separate detection module is gone; what remains of ReorgDetection.res is the data shapes and the log formatting.

Two things fall out of that. Response validation becomes a distinct concept from reorg detection: a response that contradicts itself (the same block twice with different hashes, a requested hash missing) is now a bad response to retry, not a chain reorg to roll back for. And Fuel gets a real block store, so it participates in reorg detection (detect-only) and materialises its block from the store like EVM/SVM.

Key changes

Rust BlockStore (packages/cli/src/block_store.rs)

  • merge() compares hashes while draining a page into the persistent store and reports the lowest in-threshold mismatch. On a mismatch nothing merges, so the scanned hashes survive for the rollback comparison — unless report_only (detect-only mode), which merges anyway so the same mismatch doesn't re-report on every response.
  • Within-response conflicts are tracked separately as responseConflict, on the page rather than the chain store, and reset on merge so response-only state can't become persistent chain data.
  • prune(up_to, keep_hashes_from) reduces processed rows to hash-only instead of dropping them, keeping detection working inside the reorg threshold.
  • FuelBlockField (height, id, time) plus Fuel materialisation; from_js_evm/svm/fuel build pages from sparse JS blocks (RPC observations, seeded checkpoints).
  • latest_valid_block_from_store and missing_hashes move the rollback comparison and coverage check into Rust; SVM accepts a missing slot when the HyperSync cursor proved its range was fully processed.

Native block-hash queries

  • getBlockHashes on the EVM and SVM clients: query construction, pagination and overlap checks live in Rust, so block-only response data never crosses into ReScript. Replaces HyperSync.BlockData.
  • Failures carry their request timings across the napi boundary through an ENVIO_NATIVE_FAILURE: envelope, so a retried operation still reports what it spent.

Retry policy (one place, every ecosystem)

  • A backend instance that hasn't reached the queried block yet is now Source.SourceBehindHead — raised by EVM/Fuel getItems and by the Rust paginators via a SOURCE_BEHIND_HEAD:<block> marker (alongside the existing RATE_LIMITED: one). EVM and Fuel each used to build their own backoff schedule and message for this; SourceManager now owns it for getItems and getBlockHashes alike.
  • backoffBeforeRetry centralises the failover decision that only the WithBackoff path implemented. Marking lastFailedAt only demotes a source in the selection order, so when there is no alternative to move to the retry lands right back on the same source — those retries now carry a 50ms floor instead of spinning.

ReScript runtime

  • ChainState holds shouldRollbackOnReorg / maxReorgDepth directly and derives every merge/read/prune boundary from (knownHeight, maxReorgDepth), using the resumed depth rather than the config one.
  • Batch takes an immutable snapshot of in-threshold hashes at batch assembly, so checkpoint hashes can't shift under a concurrent store mutation.
  • Reorg checkpoints are seeded on resume as hash-only rows merged into the store.
  • Every source now returns a blockStore page (no more option): inline sources contribute hash-only rows built from the hashes they observed.

Notes for reviewers

  • The EVM hash column stays fixed 32-byte. A fromJs hash shorter than that is left-padded to the canonical width, so a stored hash reads back padded — MockIndexer.evmBlockHash mirrors this for assertions.
  • blockLag on resume uses the resumed maxReorgDepth, not the config value, so a reduced config depth can't let fetching enter the stored rollback window without history.
  • ENVIO_MAX_SOURCE_RETRIES was introduced earlier in this branch and has been removed again: nothing set it, and its call sites turned a routine head-of-chain condition into a run-ending error.

Tests

packages/cli 480 Rust tests, packages/envio-tests 408, scenarios/test_codegen 711 — all passing. New coverage: block-store merge/prune/rollback and conflict detection in Rust, ChainStateReorgThreshold_test for the threshold arithmetic across a changed maxReorgDepth, SourceManager cases for the behind-head retry and for onReorg firing before an inconsistent-response retry, and the rollback E2E suite reworked onto the new hash storage.

https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a

claude added 4 commits July 10, 2026 12:49
The per-chain BlockStore now owns reorg detection: merging a fetch-response
page compares block hashes and reports the lowest in-threshold mismatch
(discarding the page in rollback mode, overwriting in detect-only mode),
pruning keeps in-threshold hashes as hash-only rows, and rollback reads
(getHash, getHashedBlockNumbers, latestValidBlock) replace the JS-side
ReorgDetection registry.

- Fuel gets a first-class store (height/time/id) built by the Rust client;
  Fuel blocks are materialised from the store instead of carried inline.
- BlockStore.fromJs builds pages from sparse JS blocks: RPC contributes
  hash-only observations, simulate an empty page, and stored reorg
  checkpoints seed the store on resume.
- The EVM HyperSync rollback-guard blocks are inserted into the page store
  on the Rust side; sources no longer return a separate blockHashes array.
- ReorgDetection.res shrinks to the shared data types and log params.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
- Store detection hashes as their JS string form for every ecosystem, so
  pages built from JS observations compare byte-for-byte with fetched blocks
  (and mismatch reports return the original strings).
- Record within-page hash conflicts (the same block observed twice with
  different hashes in one response) while a page is built and report them
  from merge, matching the old duplicate-collision detection.
- Rollback keeps hash-only rows on non-reorg chains — their scanned hashes
  stay valid while refetch repopulates the data — and drops everything above
  the target on the reorg chain.
- Checkpoint block hashes are gated by the chain's own reorg threshold
  (sourceBlockNumber - maxReorgDepth) instead of the global flag.
- Port ReorgDetection/SourceBlockHashes/ChainState/rollback tests to the
  store-backed API and pin Fuel.blockFields against the Rust ordering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
- FuelBlockField orders id first (Fuel.res blockFields matches).
- The within-page hash conflict lives inside the store's single Mutex
  alongside the table instead of a second lock; it stays on the struct
  because a page is built across several insert calls (response blocks,
  then guard rows) and merge reads it later.
- Clarify why the EVM hash column is filled outside evm_block_col.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
EVM/Fuel block hashes are hex-validated and stored as bytes again: fromJs
pages reject non-hex hashes (e.g. arbitrary marker strings) with a
validation error instead of storing them opaquely. The hash column is
variable-width — 32 bytes for fetched blocks — so hex test fixtures can
stay short. Test mocks now use valid even-length hex hashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds Fuel block storage, moves reorg detection into BlockStore merges, replaces ReorgDetection state with BlockStore-backed APIs, updates source responses and native request handling, and adds bounded retries, validation, materialization changes, and related tests.

Changes

BlockStore and source pipeline

Layer / File(s) Summary
Rust BlockStore and storage semantics
packages/cli/src/block_store.rs, packages/cli/src/field_table.rs
Adds Fuel rows and fields, byte-level conflict detection, threshold-aware merge results, hash retention, rollback controls, and Fuel field-order exports.
Source BlockStore pages and native requests
packages/cli/src/*_hypersync_source/*, packages/envio/src/sources/*
Sources build and return BlockStore pages, including Fuel decoding, RPC parent hashes, SVM pagination, EVM rollback guards, and request statistics.
ChainState reorg flow and retry validation
packages/envio/src/ChainState.*, packages/envio/src/sources/SourceManager.*, packages/envio/src/Batch.res
ChainState stores reorg configuration and delegates comparisons to BlockStore; SourceManager validates pages, retries inconsistent responses, and enforces retry limits.
Mocks, fixtures, and test configuration
scenarios/test_codegen/test/*, packages/envio/src/TestIndexer.res, packages/envio/src/bindings/Vitest.res, packages/cli/templates/*, packages/e2e-tests/*
Tests and mocks adopt BlockStore responses, cover reorg and retry behavior, normalize hashes, and adjust retry and timeout settings.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: moving reorganization detection into the Rust BlockStore.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
scenarios/test_codegen/test/ReorgDetection_test.res (2)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the refactor narration.

This describes where reorg detection moved rather than a non-obvious behavioral constraint.

As per coding guidelines, **/*.res: “Never narrate the refactor itself.”

Proposed change
-// Reorg detection now lives in the Rust BlockStore: merging a page compares
-// block hashes and reports the lowest in-threshold mismatch; pruning keeps
-// in-threshold hashes; rollback reads find the last valid block.
 describe("Block store reorg detection", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/ReorgDetection_test.res` around lines 3 - 5,
Remove the refactor-narration comment at the top of the test file; leave the
test implementation unchanged.

Source: Coding guidelines


35-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use one aggregate assertion per test.

  • scenarios/test_codegen/test/ReorgDetection_test.res#L35-L59: collect threshold-query results into one record assertion.
  • scenarios/test_codegen/test/ReorgDetection_test.res#L88-L122: collect rollback and report-only outcomes into one final assertion, or split the modes into separate tests.
  • scenarios/test_codegen/test/ReorgDetection_test.res#L141-L164: compare conflicting and identical duplicate outcomes together.
  • scenarios/test_codegen/test/ReorgDetection_test.res#L191-L218: compare all latestValidBlock cases in one array or record.
  • scenarios/test_codegen/test/SourceBlockHashes_test.res#L195-L217: aggregate item count and both hash-presence checks.
  • scenarios/test_codegen/test/SourceBlockHashes_test.res#L253-L271: aggregate item count, fetched block number, and hashes.

As per coding guidelines, **/*_test.res: “Always use single assert to check the whole value instead of multiple asserts for every field.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/ReorgDetection_test.res` around lines 35 - 59,
Replace the multiple assertions in the affected tests with one aggregate
assertion per test: in scenarios/test_codegen/test/ReorgDetection_test.res
ranges 35-59, 88-122, 141-164, and 191-218, collect the threshold,
rollback/report-only, duplicate, and latestValidBlock outcomes into a single
record or array assertion; in
scenarios/test_codegen/test/SourceBlockHashes_test.res ranges 195-217 and
253-271, aggregate the item count, fetched block number, and hash-presence
results into one assertion while preserving all expected values.

Source: Coding guidelines

packages/envio/src/Batch.res (1)

15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the caller-oriented field comment.

The comment only explains where blockStore is consumed rather than a non-obvious constraint.

As per coding guidelines, “Don't write a comment that restates what the code already says — … which callers use a value.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/Batch.res` around lines 15 - 16, Remove the
caller-oriented comment above the chain block store field in Batch.res, leaving
the field declaration and surrounding code unchanged.

Source: Coding guidelines

packages/envio/src/sources/RpcSource.res (1)

1118-1122: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Dedupe observedBlocks entries per block number before pushing per-log.

This loop pushes one {blockNumber, blockHash} entry per log item. For a block containing many matched logs (common for popular contracts), the same (blockNumber, blockHash) pair gets pushed once per log, inflating the page passed to BlockStore.fromJs/fromJsEvm across the napi boundary for no benefit — the block's hash doesn't change between logs.

♻️ Proposed dedup
+    let seenLogBlocks = Utils.Set.make()
     items->Array.forEach(({log}) =>
-      observedBlocks
-      ->Array.push({BlockStore.blockNumber: log.blockNumber, blockHash: log.blockHash})
-      ->ignore
+      if !(seenLogBlocks->Utils.Set.has(log.blockNumber)) {
+        seenLogBlocks->Utils.Set.add(log.blockNumber)->ignore
+        observedBlocks
+        ->Array.push({BlockStore.blockNumber: log.blockNumber, blockHash: log.blockHash})
+        ->ignore
+      }
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/sources/RpcSource.res` around lines 1118 - 1122,
Deduplicate observedBlocks by block number before adding entries in the items
iteration around the log handling flow. Ensure each block contributes only one
{BlockStore.blockNumber, blockHash} pair, while preserving the existing block
hash and the downstream BlockStore.fromJs/fromJsEvm input behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/block_store.rs`:
- Around line 658-665: Update the merge method containing the ecosystem
assertion to enforce compatibility in all builds: replace the debug-only check
with a runtime discriminant comparison, return the method’s existing error type
on mismatch, and perform this validation before accessing either table. Preserve
the current merge behavior for matching ecosystems.
- Around line 995-999: Update the Height and ParentSlot conversions in the
block-store field factory to propagate u64 conversion failures like the existing
slot handling. Reject negative values as errors instead of converting them to
missing fields, while preserving successful conversion behavior for valid
values.

In `@packages/envio/src/ChainState.res`:
- Around line 25-26: Use cs.maxReorgDepth as the authoritative runtime value
across reorg detection, pruning, rollback, and checkpoint retention. In
packages/envio/src/ChainState.res:25-26 and getHighestBlockBelowThreshold, use
cs.maxReorgDepth; at packages/envio/src/ChainState.res:743-744, include
maxReorgDepth: cs.maxReorgDepth in Batch.chainBeforeBatch; and in
packages/envio/src/Batch.res:208-209, calculate the retention threshold from
chainBeforeBatch.maxReorgDepth instead of chainConfig.maxReorgDepth.

In `@scenarios/test_codegen/test/rollback/Rollback_test.res`:
- Around line 2084-2089: The comments in the rollback test still reference
removed reorg helper methods. Update the affected comments near the second reorg
and the additional referenced sections to describe BlockStore’s current hash
comparison and rollback behavior, including the relevant stored-block and
threshold outcomes without naming getThresholdBlockNumbersBelowBlock or
registerReorgGuard.

---

Nitpick comments:
In `@packages/envio/src/Batch.res`:
- Around line 15-16: Remove the caller-oriented comment above the chain block
store field in Batch.res, leaving the field declaration and surrounding code
unchanged.

In `@packages/envio/src/sources/RpcSource.res`:
- Around line 1118-1122: Deduplicate observedBlocks by block number before
adding entries in the items iteration around the log handling flow. Ensure each
block contributes only one {BlockStore.blockNumber, blockHash} pair, while
preserving the existing block hash and the downstream
BlockStore.fromJs/fromJsEvm input behavior.

In `@scenarios/test_codegen/test/ReorgDetection_test.res`:
- Around line 3-5: Remove the refactor-narration comment at the top of the test
file; leave the test implementation unchanged.
- Around line 35-59: Replace the multiple assertions in the affected tests with
one aggregate assertion per test: in
scenarios/test_codegen/test/ReorgDetection_test.res ranges 35-59, 88-122,
141-164, and 191-218, collect the threshold, rollback/report-only, duplicate,
and latestValidBlock outcomes into a single record or array assertion; in
scenarios/test_codegen/test/SourceBlockHashes_test.res ranges 195-217 and
253-271, aggregate the item count, fetched block number, and hash-presence
results into one assertion while preserving all expected values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cc44269-3e3e-4112-aaa9-fd867381eeb0

📥 Commits

Reviewing files that changed from the base of the PR and between 12c9fa4 and 7e4d2ad.

📒 Files selected for processing (37)
  • packages/cli/src/block_store.rs
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/field_table.rs
  • packages/cli/src/fuel_hypersync_source/mod.rs
  • packages/envio/src/Batch.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/Core.res
  • packages/envio/src/EventConfigBuilder.res
  • packages/envio/src/EventProcessing.res
  • packages/envio/src/ReorgDetection.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/sources/BlockStore.res
  • packages/envio/src/sources/Fuel.res
  • packages/envio/src/sources/HyperFuel.res
  • packages/envio/src/sources/HyperFuel.resi
  • packages/envio/src/sources/HyperFuelClient.res
  • packages/envio/src/sources/HyperFuelSource.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/Source.res
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/fuel_test/src/Indexer.res
  • scenarios/test_codegen/test/BlockStore_test.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SourceBlockHashes_test.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/lib_tests/ChainState_materialize_test.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/IndexerLoop_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res

Comment thread packages/cli/src/block_store.rs Outdated
Comment thread packages/cli/src/block_store.rs Outdated
Comment thread packages/envio/src/ChainState.res
Comment thread scenarios/test_codegen/test/rollback/Rollback_test.res Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e4d2ad4a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +668 to +672
switch cs.blockStore->BlockStore.merge(
blockStore,
~fromBlock=Pervasives.max(knownHeight - cs.maxReorgDepth, 0),
~reportOnly=!cs.shouldRollbackOnReorg,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prune stale hash-only observations during merge

When a response page is merged here, fromBlock is only used as the comparison lower bound; rows below that threshold are still appended to cs.blockStore and are only removed later by applyBatchProgress. For sparse/RPC ranges with no parsedQueueItems, no batch progress runs, so each empty fetch can leave hash-only observations (for example the latest block and parent) in the store indefinitely, whereas the old ReorgDetection.registerReorgGuard rebuilt its map from only in-threshold rows on every response. This lets long no-event backfills or polling grow the block store unboundedly; drop/prune hash-only observations outside the current reorg threshold when merging reorg pages.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/ChainState.res Outdated
cs.blockStore->BlockStore.rollback(newProgressBlockNumber)
// A non-reorg chain's scanned hashes above the target are still valid, so
// keep them for reorg detection while the refetch repopulates the data.
cs.blockStore->BlockStore.rollback(newProgressBlockNumber, ~keepHashes=!isReorgChain)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Roll the reorg chain's hashes back to the valid block

When the rollback target checkpoint is below newProgressBlockNumber (for example there were no events/checkpoints in the intervening blocks), rolling the reorg chain's block store only to newProgressBlockNumber leaves orphaned hashes between the last valid block and that progress point. The next refetch can include parent/guard observations for those blocks and compare them against stale hashes, immediately reporting another reorg or choosing the wrong depth. For the reorg chain, clear hashes above rollbackTargetBlockNumber even if fetch/progress are restored to newProgressBlockNumber.

Useful? React with 👍 / 👎.

…org-tracking-2ucqi4

# Conflicts:
#	packages/envio/src/ChainState.res
#	packages/envio/src/sources/SimulateSource.res

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52fa0321e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/ChainState.res Outdated
~ecosystem=config.ecosystem.name,
~shouldChecksum=!lowercaseAddresses,
)
blockStore->BlockStore.merge(seedPage, ~fromBlock=0, ~reportOnly=false)->ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve checkpoint hashes when seeding after rollback

When a restarted indexer has checkpoint rows from both before and after a prior rollback, chainReorgCheckpoints can contain the same block number with different hashes because the checkpoint query does not dedupe by block. BlockStore.fromJs records that as a within-page mismatch, and this merge(..., reportOnly=false) then returns the mismatch without appending any of the seed page; since the result is ignored, the chain resumes with no persisted reorg hashes at all until new pages rebuild them. Deduplicate/order the checkpoints before building the page, or merge the seed in a mode that still preserves usable hashes.

Useful? React with 👍 / 👎.

Comment thread packages/cli/src/block_store.rs Outdated
Comment on lines +911 to +915
.pending_mismatch
.as_ref()
.is_none_or(|p| (key as i64) < p.block_number)
{
inner.pending_mismatch = Some(HashMismatch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain page conflicts past the reorg threshold

When a page records more than one same-block conflict, this keeps only the lowest block before merge applies fromBlock. If the lowest conflict is below the reorg threshold but a higher conflict is inside it (for example conflicts at blocks 50 and 100 with fromBlock=75), merge filters out the saved block 50 conflict and the block 100 conflict has already been discarded, so the self-conflicting page is appended without reporting a reorg. Keep all pending conflicts or choose the lowest conflict after applying the threshold.

Useful? React with 👍 / 👎.

Comment thread packages/cli/src/block_store.rs Outdated
inner.table.rollback_keeping_field(target, self.hash_field())
}
Ok(target) => inner.table.rollback(target),
Err(_) => inner.table.clear(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep hashes when preserving a pre-genesis rollback

When a non-reorg chain is rolled back alongside another chain to a checkpoint before its first block, newProgressBlockNumber can be -1; with keepHashes=true the intent is to reduce all buffered rows to hash-only rows, but this branch clears the whole store instead. That drops valid in-threshold hashes for the non-reorg chain and creates a reorg-detection gap until those blocks are fetched again; handle negative targets specially when keep_hashes is set.

Useful? React with 👍 / 👎.

* Validate block-store responses before retry

* Address block hash query review feedback

* Track SVM cursor coverage in block stores

* Fix SVM parent validation and metrics
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0db045d918

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +214 to +216
if !shouldSwitch {
await Utils.delay(backoffMillis)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the inconsistent-response backoff without an alternative

When this source is the only usable source, or all alternatives are still inside recoveryTimeout, setting lastFailedAt does not prevent the next loop from selecting it again because getNextSources falls back to primaries that are still in recovery. This branch then skips the delay on every even retry, so repeated internally inconsistent responses immediately reissue the same request against the same drifting provider instead of backing off; keep the delay when no alternative source is actually available.

Useful? React with 👍 / 👎.

retryRef := retryRef.contents + 1

| Source.InconsistentResponse(_) as err => {
await retryInconsistentResponse(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset source state before sleeping on inconsistent pages

When an RPC-backed source returns an internally inconsistent page, its onReorg hook is what clears cached block/transaction/receipt loaders. Because this await runs before invoking that hook, any other partition that runs during the 100ms/backoff window can still read orphaned cached data from the same source; invalidate the source state before waiting to retry.

Useful? React with 👍 / 👎.

claude added 2 commits July 16, 2026 15:18
…org-tracking-2ucqi4

# Conflicts:
#	packages/envio/src/SimulateItems.res
#	packages/envio/src/sources/SimulateSource.res
…g-2ucqi4' into claude/block-store-reorg-tracking-2ucqi4

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/envio/src/sources/HyperSyncClient.res (1)

305-306: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove these implementation-location comments.

  • packages/envio/src/sources/HyperSyncClient.res#L305-L306: remove the comment; the method signature already expresses the boundary.
  • packages/envio/src/sources/SvmHyperSyncClient.res#L208-L209: remove the equivalent comment.

As per coding guidelines, .res comments must not restate code or point to where behavior is defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/sources/HyperSyncClient.res` around lines 305 - 306,
Remove the implementation-location comment near the relevant method in
packages/envio/src/sources/HyperSyncClient.res (lines 305-306) and remove the
equivalent comment in packages/envio/src/sources/SvmHyperSyncClient.res (lines
208-209); leave both method implementations unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/evm_hypersync_source/mod.rs`:
- Around line 177-180: Update the EVM source polling loop around the
next_block/cursor check to return the same structured failure used by the SVM
path when next_block does not advance beyond cursor. Remove the
sleep-and-continue retry behavior for this stalled response so the source
manager receives an error instead of growing request_stats indefinitely.

In `@packages/envio/src/sources/HyperSync.res`:
- Around line 1-17: The rate-limit exception mapping currently discards native
request statistics. Update Source.res lines 112-113 to extend rateLimited with
the requestStats payload, then update mapRateLimitedExn in
packages/envio/src/sources/HyperSync.res lines 1-17 to pass failure.requestStats
when constructing Source.RateLimited, preserving those timings for retry
metrics.

In `@scenarios/test_codegen/test/helpers/RpcSourcePins.res`:
- Around line 1-3: In scenarios/test_codegen/test/helpers/RpcSourcePins.res
lines 1-3, remove the module-purpose comment; at lines 85-87, replace the
migration narrative with only the invariant that BlockStore keys hashes by block
number, making the projection deduplicated and ascending.

In `@scenarios/test_codegen/test/RateLimit_test.res`:
- Around line 107-110: Replace the multiple field-level assertions with one
whole-value assertion per test. In
scenarios/test_codegen/test/RateLimit_test.res lines 107-110 and 125-126,
combine the hash-range and wait-time results; in
scenarios/test_codegen/test/SvmHyperSyncSource_test.res lines 208-209, combine
forwarded blocks and request statistics; in
scenarios/test_codegen/test/lib_tests/SourceManager_test.res lines 101-107,
combine projected timing and mapped reset delay; and in lines 1489-1504, combine
pre/post-reorg state with the final response. Build a single record or tuple
containing each expected value and compare it once in each affected test.

---

Nitpick comments:
In `@packages/envio/src/sources/HyperSyncClient.res`:
- Around line 305-306: Remove the implementation-location comment near the
relevant method in packages/envio/src/sources/HyperSyncClient.res (lines
305-306) and remove the equivalent comment in
packages/envio/src/sources/SvmHyperSyncClient.res (lines 208-209); leave both
method implementations unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6648dca-d679-4287-8e3c-386456625565

📥 Commits

Reviewing files that changed from the base of the PR and between 52fa032 and 85a1930.

📒 Files selected for processing (39)
  • packages/cli/src/block_store.rs
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/evm_rpc_source/mod.rs
  • packages/cli/src/field_table.rs
  • packages/cli/src/lib.rs
  • packages/cli/src/request_stats.rs
  • packages/cli/src/svm_hypersync_source/mod.rs
  • packages/cli/src/svm_hypersync_source/query.rs
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/Core.res
  • packages/envio/src/ReorgDetection.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/sources/BlockStore.res
  • packages/envio/src/sources/HyperSync.res
  • packages/envio/src/sources/HyperSync.resi
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/Source.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/SourceManager.resi
  • packages/envio/src/sources/SvmHyperSyncClient.res
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/RateLimit_test.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • scenarios/test_codegen/test/RpcSourceContract_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SvmHyperSyncSource_test.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/envio/src/Core.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/Rollback.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/sources/BlockStore.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • packages/envio/src/ChainState.res
  • packages/cli/src/block_store.rs
  • scenarios/test_codegen/test/rollback/Rollback_test.res

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/envio/src/sources/HyperSyncClient.res (1)

305-306: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove these implementation-location comments.

  • packages/envio/src/sources/HyperSyncClient.res#L305-L306: remove the comment; the method signature already expresses the boundary.
  • packages/envio/src/sources/SvmHyperSyncClient.res#L208-L209: remove the equivalent comment.

As per coding guidelines, .res comments must not restate code or point to where behavior is defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/sources/HyperSyncClient.res` around lines 305 - 306,
Remove the implementation-location comment near the relevant method in
packages/envio/src/sources/HyperSyncClient.res (lines 305-306) and remove the
equivalent comment in packages/envio/src/sources/SvmHyperSyncClient.res (lines
208-209); leave both method implementations unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/evm_hypersync_source/mod.rs`:
- Around line 177-180: Update the EVM source polling loop around the
next_block/cursor check to return the same structured failure used by the SVM
path when next_block does not advance beyond cursor. Remove the
sleep-and-continue retry behavior for this stalled response so the source
manager receives an error instead of growing request_stats indefinitely.

In `@packages/envio/src/sources/HyperSync.res`:
- Around line 1-17: The rate-limit exception mapping currently discards native
request statistics. Update Source.res lines 112-113 to extend rateLimited with
the requestStats payload, then update mapRateLimitedExn in
packages/envio/src/sources/HyperSync.res lines 1-17 to pass failure.requestStats
when constructing Source.RateLimited, preserving those timings for retry
metrics.

In `@scenarios/test_codegen/test/helpers/RpcSourcePins.res`:
- Around line 1-3: In scenarios/test_codegen/test/helpers/RpcSourcePins.res
lines 1-3, remove the module-purpose comment; at lines 85-87, replace the
migration narrative with only the invariant that BlockStore keys hashes by block
number, making the projection deduplicated and ascending.

In `@scenarios/test_codegen/test/RateLimit_test.res`:
- Around line 107-110: Replace the multiple field-level assertions with one
whole-value assertion per test. In
scenarios/test_codegen/test/RateLimit_test.res lines 107-110 and 125-126,
combine the hash-range and wait-time results; in
scenarios/test_codegen/test/SvmHyperSyncSource_test.res lines 208-209, combine
forwarded blocks and request statistics; in
scenarios/test_codegen/test/lib_tests/SourceManager_test.res lines 101-107,
combine projected timing and mapped reset delay; and in lines 1489-1504, combine
pre/post-reorg state with the final response. Build a single record or tuple
containing each expected value and compare it once in each affected test.

---

Nitpick comments:
In `@packages/envio/src/sources/HyperSyncClient.res`:
- Around line 305-306: Remove the implementation-location comment near the
relevant method in packages/envio/src/sources/HyperSyncClient.res (lines
305-306) and remove the equivalent comment in
packages/envio/src/sources/SvmHyperSyncClient.res (lines 208-209); leave both
method implementations unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6648dca-d679-4287-8e3c-386456625565

📥 Commits

Reviewing files that changed from the base of the PR and between 52fa032 and 85a1930.

📒 Files selected for processing (39)
  • packages/cli/src/block_store.rs
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/evm_rpc_source/mod.rs
  • packages/cli/src/field_table.rs
  • packages/cli/src/lib.rs
  • packages/cli/src/request_stats.rs
  • packages/cli/src/svm_hypersync_source/mod.rs
  • packages/cli/src/svm_hypersync_source/query.rs
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/Core.res
  • packages/envio/src/ReorgDetection.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/sources/BlockStore.res
  • packages/envio/src/sources/HyperSync.res
  • packages/envio/src/sources/HyperSync.resi
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/Source.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/SourceManager.resi
  • packages/envio/src/sources/SvmHyperSyncClient.res
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/RateLimit_test.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • scenarios/test_codegen/test/RpcSourceContract_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SvmHyperSyncSource_test.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/envio/src/Core.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/Rollback.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/sources/BlockStore.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • packages/envio/src/ChainState.res
  • packages/cli/src/block_store.rs
  • scenarios/test_codegen/test/rollback/Rollback_test.res
🛑 Comments failed to post (4)
packages/cli/src/evm_hypersync_source/mod.rs (1)

177-180: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop retrying an unadvanced EVM cursor indefinitely.

A stalled backend response keeps this loop alive forever and continuously grows request_stats; the source manager never receives an error to retry. Return a structured failure here, as the SVM path does.

Proposed fix
 if next_block <= cursor {
-    tokio::time::sleep(Duration::from_millis(100)).await;
-    continue;
+    let error = map_err(anyhow::anyhow!(
+        "EVM block hash query made no progress: cursor={cursor}, next_block={next_block}"
+    ));
+    return Err(error_with_request_stats(error, &request_stats));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            if next_block <= cursor {
                let error = map_err(anyhow::anyhow!(
                    "EVM block hash query made no progress: cursor={cursor}, next_block={next_block}"
                ));
                return Err(error_with_request_stats(error, &request_stats));
            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/evm_hypersync_source/mod.rs` around lines 177 - 180, Update
the EVM source polling loop around the next_block/cursor check to return the
same structured failure used by the SVM path when next_block does not advance
beyond cursor. Remove the sleep-and-continue retry behavior for this stalled
response so the source manager receives an error instead of growing
request_stats indefinitely.
packages/envio/src/sources/HyperSync.res (1)

1-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve native request timings through rate-limit retries.

The native payload includes requestStats, but the ReScript exception mapping drops them before SourceManager can update its metrics.

  • packages/envio/src/sources/HyperSync.res#L1-L17: propagate failure.requestStats when constructing the rate-limit exception.
  • packages/envio/src/sources/Source.res#L112-L113: extend rateLimited to carry those statistics so the retry handler can record them.
📍 Affects 2 files
  • packages/envio/src/sources/HyperSync.res#L1-L17 (this comment)
  • packages/envio/src/sources/Source.res#L112-L113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/sources/HyperSync.res` around lines 1 - 17, The rate-limit
exception mapping currently discards native request statistics. Update
Source.res lines 112-113 to extend rateLimited with the requestStats payload,
then update mapRateLimitedExn in packages/envio/src/sources/HyperSync.res lines
1-17 to pass failure.requestStats when constructing Source.RateLimited,
preserving those timings for retry metrics.
scenarios/test_codegen/test/helpers/RpcSourcePins.res (1)

1-3: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove refactor narration from comments.

  • scenarios/test_codegen/test/helpers/RpcSourcePins.res#L1-L3: remove the module-purpose comment.
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res#L85-L87: replace the migration narrative with only the invariant: BlockStore keys hashes by block number, so the projection is deduplicated and ascending.

As per coding guidelines, .res comments must not restate module purpose or narrate refactors.

📍 Affects 1 file
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res#L1-L3 (this comment)
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res#L85-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/helpers/RpcSourcePins.res` around lines 1 - 3, In
scenarios/test_codegen/test/helpers/RpcSourcePins.res lines 1-3, remove the
module-purpose comment; at lines 85-87, replace the migration narrative with
only the invariant that BlockStore keys hashes by block number, making the
projection deduplicated and ascending.

Source: Coding guidelines

scenarios/test_codegen/test/RateLimit_test.res (1)

107-110: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use one whole-value assertion per test.

  • scenarios/test_codegen/test/RateLimit_test.res#L107-L110: combine hash-range and wait-time checks into one record assertion.
  • scenarios/test_codegen/test/RateLimit_test.res#L125-L126: combine hash-range and zero-wait checks.
  • scenarios/test_codegen/test/SvmHyperSyncSource_test.res#L208-L209: assert forwarded blocks and request stats together.
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res#L101-L107: project timing and mapped reset delay into one result.
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res#L1489-L1504: collect pre/post-reorg state and final response into one assertion.

As per coding guidelines, **/*_test.res: “Always use single assert to check the whole value instead of multiple asserts for every field.”

📍 Affects 3 files
  • scenarios/test_codegen/test/RateLimit_test.res#L107-L110 (this comment)
  • scenarios/test_codegen/test/RateLimit_test.res#L125-L126
  • scenarios/test_codegen/test/SvmHyperSyncSource_test.res#L208-L209
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res#L101-L107
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res#L1489-L1504
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/RateLimit_test.res` around lines 107 - 110,
Replace the multiple field-level assertions with one whole-value assertion per
test. In scenarios/test_codegen/test/RateLimit_test.res lines 107-110 and
125-126, combine the hash-range and wait-time results; in
scenarios/test_codegen/test/SvmHyperSyncSource_test.res lines 208-209, combine
forwarded blocks and request statistics; in
scenarios/test_codegen/test/lib_tests/SourceManager_test.res lines 101-107,
combine projected timing and mapped reset delay; and in lines 1489-1504, combine
pre/post-reorg state with the final response. Build a single record or tuple
containing each expected value and compare it once in each affected test.

Source: Coding guidelines

…ilience, seam validation (#1442)

* Address review findings: threshold authority, EVM parent-link check, test coverage

- Use the resumed-from-DB maxReorgDepth and per-chain shouldRollbackOnReorg
  everywhere (Batch checkpoints, getHighestBlockBelowThreshold, reorg
  logging/rollback decision) instead of mixing them with config values
- Validate EVM parent links in response stores: block N's parentHash must
  match block N-1's hash, within a page and across page seams; select
  parentHash in the EVM getBlockHashes re-fetch (Fuel has no parent-id
  field, so the check stays EVM/SVM-only)
- Make shouldRollbackOnReorg/maxReorgDepth required in ChainState.make
- Cover ChainState threshold arithmetic (depth changes across restarts,
  clamping, registerReorgGuard boundary) and applyBatchProgress hash
  retention with new tests
- Harden field_table: hard width checks on fixed columns, 64-field cap
- Drop stale ReorgDetection comments, dedupe native-failure unpacking,
  map rate-limited errors in the SVM getBlockHashes path too, fix test
  indentation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Surface EVM block-hash no-progress as an error for SourceManager to retry

Matches the SVM path: instead of silently sleeping 100ms in-process
(unbounded, no logging, no failover), the paginator returns a
RequestFailed error carrying the accumulated request stats. SourceManager
logs each retry with backoff and switches to another source on repeated
failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Friendlier no-progress message and a fast first retry for block-hash fetches

The replica-drift error now explains itself (routing to a replica slightly
behind the head, safe to continue after a retry), and SourceManager's
first block-hash retry backs off only 100ms to match how quickly a lagging
replica usually catches up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Polish block-hash retry: doubling backoff, message-first logging

- Backoff doubles from 100ms (capped at 60s) instead of stepping by 1s
- The native failure's own message becomes the retry log's msg (the
  replica-drift text is self-explanatory); generic failures keep the
  err payload
- Native failure causes are plain JS errors now, so logs no longer show
  a NativeRequestFailed wrapper
- Drop the logType field from the block-hash query logger and reword the
  replica-drift message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Shorten the replica-drift message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Use the resumed reorg depth for the pre-threshold fetch lag

Codex review: with a reduced configured depth, blockLag from the config
value would let fetching enter the stored rollback window without
history while detection still compares the resumed window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Fail test runs with the source error instead of retrying forever

In production a failing source is retried indefinitely (backoff and
failover keep the indexer alive), which in tests turns an unreachable
endpoint into a bare test-runner timeout with no context. SourceManager
now accepts a maxRetries cap (ENVIO_MAX_SOURCE_RETRIES) enforced across
the height, getItems, and getBlockHashes retry loops; when exhausted the
run fails with the underlying error. The test indexer worker defaults
the cap to 1, and generated templates give vitest 60s so the real error
surfaces before the runner's axe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Make live-endpoint tests resilient to hung connections

The test indexer worker now caps the HyperSync request timeout at 10s
(production default is 120s, which outlives every test timeout and turns
a hung connection into a context-free failure) so a hang fails fast and
retries on a fresh connection. The scenarios suite retries a failed test
once on CI - tests run sequentially with a fresh indexer per test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Reject EVM block-hash pages that don't cover their range densely

Codex review: the parent-link check skips absent neighbours (event pages
are sparse by design), so an include_all_blocks page omitting an interior
block or a parentHash could hide a mixed-fork seam. Each page is now
validated to carry every block in its covered range with hash and parent
hash before it joins the aggregate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Honor the retry cap in the subscription fallback poller

Codex review: the stale-subscription fallback swallowed every
getHeightOrThrow failure, so with a subscription installed the cap never
fired and a dead endpoint could still hang a test run. The fallback's
rejection is separately observed since it can lose the height race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Retry a template smoke test once before failing the job

The template suites index real blocks through live HyperSync; one hung
connection on the runner shouldn't fail the job now that a failed
attempt surfaces quickly with a real error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Retry e2e smoke tests once on CI

Same rationale as the scenarios suite: the smoke tests hit live
HyperSync, and a hung runner connection now fails fast with a real
error, so a single retry absorbs it without hiding deterministic
failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Replace the global CI retry with per-test retries on live-endpoint tests

Only the tests that hit live HyperSync/RPC endpoints retry (following the
existing SourceBlockHashes pattern of {retry: 3}): the HyperSync client
live tests, the RPC height check, the createTestIndexer tests that fetch
mainnet, and the e2e smoke test. Deterministic tests fail on the first
attempt again. The Vitest binding options gained a timeout field so the
corrupted-token test keeps its 60s budget alongside the retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Keep the test-worker HyperSync timeout at 30s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Widen template harness timeout padding from 10s to 30s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Simplify test-run resilience knobs

Drop the template harness re-run hack and the derived outer timeout (the
config value is the single budget now), run template suites with a 30s
per-test vitest timeout, and rely on ENVIO_MAX_SOURCE_RETRIES=3 in test
workers instead of overriding the HyperSync client timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

* Replace parent-hash validation with hash-collision checks

A single HyperSync response is internally consistent, so parent-link
validation only guarded the seams between paginated block-hash requests.
Those seams are now covered directly: each follow-up page re-requests the
last returned block, and a fork switch between requests surfaces as a
hash collision on the overlapping block via the existing duplicate
detection. The EVM/SVM parent-link checks, the dense-range validation,
and the parent fields in block-hash queries are gone.

RPC responses can mix forks since every block is fetched separately, so
getBlockHashes now derives a minimal (number-1, parentHash) row from each
block - like the items path already did - letting the page's existing
collision check cross-validate separately fetched blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RcUijSCoEY19MCEbdh6fL

---------

Co-authored-by: Claude <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20a8fc4066

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +259 to +260
cursor = next_slot;
overlap_slot = last_slot.or(overlap_slot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset the overlap anchor after an empty SVM page

When a paginated SVM block-hash request advances through a range containing only skipped slots, last_slot is None but overlap_slot retains the last block from an earlier page. The next request then starts at that old slot rather than cursor; once it receives the same cursor boundary again, next_slot <= cursor treats normal progress as a replica failure and retries indefinitely. Clear the overlap anchor when the current page has no block, while retaining it only for the immediately preceding page.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scenarios/test_codegen/test/SvmHyperSyncSource_test.res (1)

208-209: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use one assertion for the complete response contract.

Combine the forwarded range and request statistics into a single expected value.

Suggested adjustment
-    t.expect(capturedBlockHashRequests->Utils.Array.lastUnsafe).toEqual(blockNumbers)
-    t.expect(response.requestStats).toEqual([{Source.method: "getBlockHashes", seconds: 0.25}])
+    t.expect({
+      "blockNumbers": capturedBlockHashRequests->Utils.Array.lastUnsafe,
+      "requestStats": response.requestStats,
+    }).toEqual({
+      "blockNumbers": blockNumbers,
+      "requestStats": [{Source.method: "getBlockHashes", seconds: 0.25}],
+    })

As per coding guidelines, **/*_test.res: Always use single assert to check the whole value instead of multiple asserts for every field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/SvmHyperSyncSource_test.res` around lines 208 -
209, Update the test assertions around the captured block hash request and
response statistics to use one assertion against the complete expected response
contract. Combine the forwarded blockNumbers range and requestStats into the
single expected value, removing the separate field-level assertions while
preserving both expected values.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/e2e-tests/src/template-tests/templates.test.ts`:
- Line 191: Update the enclosing test timeout in the template test around the
child process invocation to exceed config.timeouts.test by a settling buffer,
such as 30 seconds. Keep the child process timeout unchanged, and apply the
cushioned deadline only to the outer test.

---

Nitpick comments:
In `@scenarios/test_codegen/test/SvmHyperSyncSource_test.res`:
- Around line 208-209: Update the test assertions around the captured block hash
request and response statistics to use one assertion against the complete
expected response contract. Combine the forwarded blockNumbers range and
requestStats into the single expected value, removing the separate field-level
assertions while preserving both expected values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd3e85ea-17af-4ac5-996b-5d084157e97d

📥 Commits

Reviewing files that changed from the base of the PR and between 85a1930 and 20a8fc4.

📒 Files selected for processing (32)
  • packages/cli/src/block_store.rs
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/field_table.rs
  • packages/cli/src/svm_hypersync_source/mod.rs
  • packages/cli/templates/dynamic/init_templates/shared/package.json.hbs
  • packages/e2e-tests/src/template-tests/templates.test.ts
  • packages/envio/src/Batch.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/Env.res
  • packages/envio/src/ReorgDetection.res
  • packages/envio/src/TestIndexer.res
  • packages/envio/src/bindings/Vitest.res
  • packages/envio/src/sources/HyperSync.res
  • packages/envio/src/sources/HyperSync.resi
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/Source.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/SourceManager.resi
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/e2e_test/src/indexer.test.ts
  • scenarios/test_codegen/test/EventHandler.test.ts
  • scenarios/test_codegen/test/HyperSyncClient_test.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/ReorgDetection_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SvmHyperSyncSource_test.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/IndexerLoop_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
💤 Files with no reviewable changes (1)
  • packages/envio/src/ReorgDetection.res
🚧 Files skipped from review as they are similar to previous changes (20)
  • packages/envio/src/sources/SourceManager.resi
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/test_codegen/test/lib_tests/IndexerLoop_test.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • packages/envio/src/Batch.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/HyperSync.res
  • packages/envio/src/ChainFetching.res
  • packages/cli/src/field_table.rs
  • packages/envio/src/sources/Source.res
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/cli/src/svm_hypersync_source/mod.rs
  • packages/envio/src/ChainState.res
  • packages/cli/src/block_store.rs
  • packages/envio/src/sources/HyperSync.resi

Comment thread packages/e2e-tests/src/template-tests/templates.test.ts Outdated
claude added 3 commits July 17, 2026 14:18
…org-tracking-2ucqi4

# Conflicts:
#	packages/envio/src/ChainState.res
#	scenarios/test_codegen/test/rollback/Rollback_test.res
…g-2ucqi4' into claude/block-store-reorg-tracking-2ucqi4
The review-findings commit made ~shouldRollbackOnReorg/~maxReorgDepth
required; the CrossChainState_test call site introduced by the query-sizing
merge needed ~shouldRollbackOnReorg added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
claude added 2 commits August 12, 2026 10:10
A page returning no rows kept the previous page's last block as the
overlap anchor. On SVM that anchor sits behind the cursor whenever a
window is all skipped slots, so the next request replayed the same empty
window, the cursor never advanced, and the source-behind-head error
reproduced on every retry.

The anchor is now the block the page itself ended on, and a rewound
request that covers no new ground drops the anchor and resumes from the
cursor instead of being reported as a stalled source — an overlap page
capped at the block it re-requested is this loop's own doing, not an
instance behind the head.

The SVM caller no longer marks coverage for a range the cursor did not
advance past, so that case surfaces through the paginator's own error
rather than a malformed coverage range.

Covers the paginator with unit tests over an injected backend: the
overlap seam, the empty page, the rewound page, a genuinely stalled
source, and an empty range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JscwnEcKUbmcQbPtotGZ48
The snapshot is ascending, so the gap's first block is a bisection away.
Scanning it whole for every gap made checkpoint building quadratic in the
number of blocks a wide batch spans.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JscwnEcKUbmcQbPtotGZ48
~requiredBlockNumbers: array<int>=[],
) => {
let conflict = blockStore->BlockStore.responseConflict->Null.toOption
let missingBlockNumbers = blockStore->BlockStore.missingHashes(requiredBlockNumbers)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's calculate this only when there are no conflicts.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even group the two functions into one and call it something like validateInconsistentResponse?

let backoffMillis = retry === 0 ? 100 : 500 * retry
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
logger->log({
"msg": `Block #${blockNumber->Int.toString} is not available on the ${sourceState.source.name} source yet. Instances of a load-balanced backend drift slightly around the head, so this is expected — indexing continues after an automatic retry.`,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use - instead of — everywhere

// Floor for the retries driven by a condition the source reported itself
// (behind the head, inconsistent response). Unlike a caller-supplied backoff of
// 0, these must never busy-loop when there is no other source to move to.
let minRecoverableBackoffMillis = 50

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have it as 100

~err: exn,
~excludedSources=?,
) => {
let backoffMillis = retry === 0 ? 100 : 500 * retry

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's reuse minRecoverableBackoffMillis for retry 0?

~err: exn,
~excludedSources=?,
) => {
let backoffMillis = retry === 0 ? 100 : 500 * retry

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even get the value from backoffBeforeRetry

Comment thread packages/envio/src/Rollback.res Outdated
Comment on lines 58 to 60
chainState->ChainState.sourceManager->SourceManager.onReorg

let rollbackTargetBlockNumber = await chainState->getLastKnownValidBlock(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move
chainState->ChainState.sourceManager->SourceManager.onReorg
inside of getLastKnownValidBlock?

Comment on lines +782 to +784
let getFreshBlockInfo = blockNumber =>
blockLoader.contents
->LazyLoader.set(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert it - it's fine that a request might be read from cache, what's important is that the next block will be read from rpc (not in cache yet) and we'll use parentHash for reorg detection.

Comment thread packages/envio/src/db/InternalTable.res Outdated
WHERE cp."${(#block_hash: field :> string)}" IS NOT NULL
AND cp."${(#block_number: field :> string)}" >= rc.safe_block;` // Include safe_block checkpoint to use it for safe checkpoint tracking
AND cp."${(#block_number: field :> string)}" >= rc.safe_block
ORDER BY cp."${(#id: field :> string)}";` // Include safe_block checkpoint to use it for safe checkpoint tracking

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this changed? Not a critic, just curiosity

Comment on lines +76 to +83
if request_from < cursor {
// Rewinding to the overlap block covered no ground the cursor
// had not already passed. That is this loop's own anchor
// failing to advance, not an instance stuck behind the head, so
// drop the anchor and ask again from the cursor.
overlap = None;
continue;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the check - investigate whether it's correct and shouldn't be fixed.

continue;
}
return Err(error_with_request_stats(
source_behind_head_err(cursor),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check how the logic is handled for getLogs queries and give me overview whether we should apply the same source_behind_head_err to getLogs for all sources we have?

claude added 2 commits August 12, 2026 13:29
- block_store merge: reject cross-ecosystem pages at runtime (was debug-only),
  reset stale old-fork hashes above the divergence on detect-only reorgs, and
  prune hash-only observations below the reorg threshold so long no-event
  ranges stay bounded.
- EVM/SVM sources: keep a hash-only row for every returned block/slot header
  whose logs/instructions were all dropped by client-side routing, so a fork
  on such a block can still be detected.
- SourceManager: compute missing hashes only when there is no response
  conflict; replace em-dashes with hyphens in the reorg runtime files.
- e2e template test: give the outer `pnpm test` case a timeout cushion over
  the command timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
…g-2ucqi4' into claude/block-store-reorg-tracking-2ucqi4

# Conflicts:
#	packages/cli/src/evm_hypersync_source/mod.rs
#	packages/envio/src/sources/SourceManager.res

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac9adeb390

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/block_store.rs Outdated
Comment on lines +598 to +600
if let Some(mismatch) = &cross {
if let Ok(target) = u64::try_from(mismatch.block_number - 1) {
dst.table.rollback(target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve buffered block rows in detect-only mode

When rollbackOnReorg is disabled and a mismatch is detected, this rolls back the entire table above the divergence, including full block rows already merged by other completed partitions whose items remain in the fetch buffer. Detect-only mode performs no corresponding item rollback/refetch, so those buffered events are later materialized with missing selected block fields. Reset only the stale reorg hashes here rather than deleting unrelated buffered block data.

Useful? React with 👍 / 👎.

Comment on lines +1008 to +1012
// The guard carries the head block's timestamp, and the head is
// often outside the queried range's returned blocks — keeping it
// makes the row materialisable rather than hash-only.
timestamp: Some(
Quantity::try_from(guard.timestamp).context("guard timestamp negative")?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prune timestamped rollback-guard rows below threshold

On EVM HyperSync ranges with no parsed items and therefore no batch-progress pruning, each advancing rollback guard can leave its head row behind indefinitely. Fresh evidence in the current code is that this row now stores both hash and timestamp, while merge only calls prune_field_only_below, which explicitly retains any row containing fields besides the hash; consequently the attempted stale-observation fix does not prune these old guard heads and long empty backfills or polling can still grow the store without bound.

Useful? React with 👍 / 👎.

- block_store detect-only reset: clear only the stale hash field above the
  divergence instead of dropping the rows, so full block data other partitions
  buffered still materialises (regression from the previous reset).
- SourceManager: reduce the inconsistent-response stall threshold from ~10 to
  ~5 minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
- Revert "Read the range's own blocks for detection, not the block cache": the
  block cache read is fine, since the next block comes from the RPC and its
  parentHash carries the reorg detection. Drops `LazyLoader.set` with it.
- Rollback now drops every block above its target, hashes included, on every
  chain. A non-reorg chain therefore replays without its hashes until it
  refetches, so its checkpoints in that range carry no hash.
- Move the source-cache drop into `getLastKnownValidBlock`, so the depth search
  cannot be run against a cache filled on the orphaned fork.
- FetchState: replace the `blockRef` record with a plain `latestFetchedBlock:
  int`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

- merge takes `pruneHashesBelow`, so a hash for a block the chain has not
  processed yet survives even when it falls outside the reorg threshold. A
  backfill sits far below the threshold, and those blocks are still to be read.
- SVM get_event_items reports a replica that has not reached the queried range
  through `source_behind_head_err`, like the paginator and the EVM/Fuel item
  paths already do, and the source re-raises the recoverable markers instead of
  burying them in a generic retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

- Detect-only reorgs no longer reset anything: the page overwrites what it
  observed and later pages converge the rest.
- Drop the reorg-checkpoint dedup and its ORDER BY. One block number carrying
  two hashes needs a rollback torn between its delete and its insert, and both
  run in the same transaction.
- One backoff schedule for every same-request retry, replacing the two linear
  ones and the exponential one.
- The block-hash paginator reports a behind-head instance from the overlap
  request instead of spending a second request from the cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

A real source returns the header of every block a matched log came from, but
the mock only reported the range's seam and its last block. Reorg detection
therefore never saw the blocks events actually landed on: checkpoints written
on them carried no hash, and the rollback depth search skipped them.

The rollback tests now search those blocks too, so they answer for them - with
a differing hash where the block belongs to the reorg they set up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Merging a page no longer prunes. The progress-time prune already drops every
processed row and reduces the ones still inside the reorg depth to their hash,
and a response with no items still advances progress to the fetched frontier,
so it runs on empty ranges too. Dropping the merge-time prune removes the need
to bound it by the processed progress, and the rollback-guard rows it could
never recognise are trimmed by position like any other processed row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj6SS9KbG9mytdzuYnMs3a
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@DZakh
DZakh merged commit 2aec372 into main Aug 13, 2026
8 checks passed
@DZakh
DZakh deleted the claude/block-store-reorg-tracking-2ucqi4 branch August 13, 2026 14:45
DZakh pushed a commit that referenced this pull request Aug 14, 2026
`makeGetBlockHashes(~query=client.getBlockHashes)` passed a napi class
method as a value, so calling it later lost the instance and every
block-hash query threw "Illegal invocation" — the reorg rollback depth
search on any HyperSync source, EVM or SVM, since #1405. Every existing
test of that path mocks `Source`, so nothing called the real client.

The mock server can now answer at the HTTP level, which is what rate
limiting and payload-too-large need. New tests cover the block-hash
query and its pagination, the 429 -> retry wait mapping, a page that
made no progress, a partial page, range halving on 413, logs that route
to no registration, address checksumming, and a page that withholds a
selected block field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr
DZakh added a commit that referenced this pull request Aug 18, 2026
* Add a mock HyperSync server for source-level tests

The HyperSync query is built, sent, and decoded inside the native addon,
so nothing on the JS side could observe or answer it — the source was
only ever exercised against the real service.

MockHyperSyncServer is a local HTTP server speaking the same protocol:
it captures the JSON query bodies the client POSTs and answers them with
a page encoded the way HyperSync does, a packed Cap'n Proto message
carrying one Arrow IPC file per table. Tests supply pages as JSON rows
keyed by HyperSync field names.

HyperSyncSourceContract_test drives a real EvmHyperSyncSource against it
and pins the query's field selection and log selection, the fields each
registration's item carries after materialisation, the height stream,
the rollback guard's blocks, and the error a page that withholds a
selected field produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr

* Fix detached block-hash client method, cover more source responses

`makeGetBlockHashes(~query=client.getBlockHashes)` passed a napi class
method as a value, so calling it later lost the instance and every
block-hash query threw "Illegal invocation" — the reorg rollback depth
search on any HyperSync source, EVM or SVM, since #1405. Every existing
test of that path mocks `Source`, so nothing called the real client.

The mock server can now answer at the HTTP level, which is what rate
limiting and payload-too-large need. New tests cover the block-hash
query and its pagination, the 429 -> retry wait mapping, a page that
made no progress, a partial page, range halving on 413, logs that route
to no registration, address checksumming, and a page that withholds a
selected block field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr

* Remove TODO.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr

* Register the source test's handlers through the user API

The test built its registrations by calling HandlerRegister.setHandler,
which is what `indexer.onEvent` calls — so it reproduced the API instead
of using it, and skipped the type check that makes an inline `fields`
selection meaningful.

fromUserApi grows `~registerHandlers`: it runs the handlers module the
way a project does and exposes what registered as `parsed.registrations`,
readable from a test body since the module is imported while vitest
collects. The `~test` path is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr

* Reject ~registerHandlers without a handlers source

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQnf4SXnntuNhEiZW5CBLr

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
internal — a2e4e0e4 Deployed Aug 13, 2026 by DZakh via authorize #2725
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.

2 participants