Skip to content

v2.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 13:08
· 569 commits to main since this release
cf23991

Upgrading is a binary swap. No data migration, no re-backfill. On-disk state from 2.0 through
2.3 is read as it is.

Sprint fastidious-ferret - eleven issues, all closed - plus a silent-data-loss fix found by the
board's audit afterwards. The theme was make the binary do what the page says: the half of the
previous sprint that could not be fixed by correcting a document, because in these the document was
right and the product was wrong.

init works on any EVM chain now

nuthatch init 0xADDR --chain bsc --rpc https://bsc-rpc.example

An unregistered chain name is accepted the moment --rpc says where it lives, and the chain id comes
from the endpoint itself rather than a hand-typed flag. A known chain never dials: --rpc is ignored
for mainnet, arbitrum-one and base, so nothing changes for the built-in path.

Everything downstream already worked on any chain - dev, sql and bench are chain-agnostic, and
the indexer has carried unregistered-chain finality and window defaults for a while. init's
allow-list was the only thing narrower than what nuthatch actually scaffolds.
Without --rpc the
refusal now names the remedy rather than just the three built-ins.

A failed query teaches instead of relaying

New in /sql, the MCP sql tool and the nuthatch sql REPL alike: a DuckDB error is classified
against the nest's own schema and an actionable line is appended. The engine's raw message is always
preserved - we never restate what the engine said - the hint is added after it.

  • An unknown table names the closest real one, and points at schema for the list
  • An unknown column does the same against the table's real columns
  • A view that failed to build is no longer reported as "does not exist", which sent people
    looking for a missing view rather than a broken one
  • Solidity bool columns get their own hint. They are stored as exact text 'true'/'false',
    not a SQL boolean. Direct comparison and boolean operators cast implicitly and work; anything
    needing one type across its arguments - COALESCE, CASE, UNION, bool_and/bool_or - does
    not, and fails with "an explicit cast is required". The hint tells you to write col = 'true' or
    CAST(col AS BOOLEAN). The derived footgun is also declared in semantic.toml as bools

A factory nest no longer loses children in silence

rebuild_children reconstructs the set of contracts a factory nest indexes, on every warm restart.
It was swallowing three separate read failures, so a rebuild that came back short did not fail -
the nest started, quietly watching fewer children than it had discovered, and never indexed them. No
error, no log, no degraded flag. Data that simply never arrives.

The cold read is now decided from the segment catalogue rather than inferred from an error, which
is the distinction that makes the fix safe: a factory table with no sealed segment yet legitimately
fails that query, and "never sealed" and "cannot be read" used to arrive as the same Err and be
discarded together. The benign case was hiding the fatal one.

Backfill survives a bad window

Seal-direct backfill gave up after five attempts spanning about four seconds - shorter than an
endpoint's own thirty-second cooldown
, so it outlasted every retry and killed multi-hour backfills
over a blip a bare restart walked straight past. It now retries transient failures indefinitely with
capped exponential backoff, warn! on every attempt and error! every tenth once the backoff caps,
so a genuinely stuck endpoint is loud rather than silent. Terminal faults still propagate immediately,
and the cursor never advances past the failing window.

Told the truth about itself, in seven places

Each of these was found by running the 2.2.0 binary, not by reading code:

  • /explain degraded to cold-only in silence at the third hot-scan call site the previous fix
    never reached. It now logs and reports tip_unavailable like /sql does
  • serve without --hot-store created the redb it claimed only to read, and took an exclusive
    flock while three nearby claims said it was shareable. It now opens non-creating, refuses if the
    store is absent, and its log line says plainly that it cannot run beside a writer or a second serve
  • A dead RPC pool at cold start exited moments after logging "API live", so /ready never got to
    report stalled. Cold-start tip lookups are now retried like the steady-state loop already was
  • prune refused a mounts.toml with zero mounts - the state with the most to reclaim
  • nuthatch sql's API fallback asked for /sql, which a mounts.toml runtime does not serve.
    It is now prefixed with the mount's route, so the same command really does work either way
  • POST /_admin/nests resolved nests from the pre-2.0 nests/<name>/ layout. It now resolves
    data/<nid>/, with the nid validated through a newtype before it reaches the filesystem
  • A live-mounted nest served /sql provenance as nid: null until the next restart

Also

  • init's ABI-fit probe window is sized from the chain rather than a fixed 1,000 blocks. It was
    warning that the README's own USDC quickstart would "index zero rows, silently" - it indexes 1,348
    in 20 blocks
  • GET /nests now reflects mounts made through POST /_admin/nests
  • Three deadline tests that failed on a busy machine and passed on a quiet one are fixed
    structurally rather than by widening margins. Demonstrating it under deliberate load turned up a
    real defect: the guarded /sql surface was leaking DuckDB's raw "Interrupted!" where the arm
    beside it translates a watchdog interrupt into the budget message
  • docs/verification.md now states which release each level was verified on rather than a bare
    "yes", because a bare yes ages silently. Two rows had drifted; production verification stops at
    2.0.0, and scaled mode was measured across machines on v0.9.3 only

Known

  • Production verification is four releases stale (#441). 2.1.0 through 2.4.0 have shipped
    without the two-machine check. The rule is ours and we have not kept it
  • Scaled mode's cross-machine evidence is two majors old (#570), measured on v0.9.3
  • Store::recent_by_table silently drops unparseable rows (#567), so a corrupt row shortens any
    caller's result. Found while fixing the factory rebuild above, and it lives a layer lower