v2.4.0
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.exampleAn 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
schemafor 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
boolcolumns 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 writecol = 'true'or
CAST(col AS BOOLEAN). The derived footgun is also declared insemantic.tomlasbools
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:
/explaindegraded to cold-only in silence at the third hot-scan call site the previous fix
never reached. It now logs and reportstip_unavailablelike/sqldoesservewithout--hot-storecreated the redb it claimed only to read, and took an exclusive
flockwhile 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
/readynever got to
reportstalled. Cold-start tip lookups are now retried like the steady-state loop already was prunerefused amounts.tomlwith zero mounts - the state with the most to reclaimnuthatch sql's API fallback asked for/sql, which amounts.tomlruntime does not serve.
It is now prefixed with the mount's route, so the same command really does work either wayPOST /_admin/nestsresolved nests from the pre-2.0nests/<name>/layout. It now resolves
data/<nid>/, with the nid validated through a newtype before it reaches the filesystem- A live-mounted nest served
/sqlprovenance asnid: nulluntil 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 blocksGET /nestsnow reflects mounts made throughPOST /_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/sqlsurface was leaking DuckDB's raw"Interrupted!"where the arm
beside it translates a watchdog interrupt into the budget message docs/verification.mdnow 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_tablesilently 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