feat(adaptivefetch): subtreeData fetch gate, armed on FSM RUNNING#745
Conversation
Introduces pkg/adaptivefetch with Mode, Config, Observation, State types and a New constructor. State starts pessimistic, Record appends to a ring-buffer window but does not yet transition modes. Transitions are implemented in follow-up commits. Spec: docs/superpowers/specs/2026-04-23-adaptive-subtreedata-fetch-design.md
Explicit assertions for BootstrapMode=Optimistic and BootstrapMode=Auto so the resolve-auto-to-pessimistic behaviour is pinned.
Covers every validator branch. If a future edit weakens a check, the matching sub-test fails with a clear name.
Evaluates the rolling-window average hit rate on each Record and switches to optimistic when it clears PessToOptHitRateThreshold. Transition only evaluates once the window is full so early observations can't flip the mode.
A single observation with MissingFetches > OptToPessMissThreshold flips the mode back to pessimistic without waiting for the window to fill. This bounds the recovery cost — the node never attempts thousands of per-tx fetches before realising it should have downloaded subtreeData.
Covers the case where no single block breaches OptToPessMissThreshold but the window-wide average of MissingFetches exceeds OptToPessAvgMissThreshold.
Pins the requirement that State is safe for concurrent use — the integrations call Record from multiple goroutines per block.
Fails the build if someone re-introduces wall-clock or FSM-state gating into adaptivefetch. The whole point of this package is to avoid the class of bug that caused PR bsv-blockchain#598 to be reverted via bsv-blockchain#647.
…nputs - Pin Mode enum values explicitly (ModePessimistic=0, Optimistic=1, Auto=2). Previously iota-based; the metrics gauge treats 0/1 as load-bearing so reordering the const block could silently remap persisted values. - Validate Observation inputs at Record entry. TotalTxs<=0, negative LocalHits, LocalHits>TotalTxs, or negative MissingFetches are silently dropped. Prevents a corrupted observation from skewing the rolling average or triggering spurious transitions. - Simplify avgHitRateLocked — the TotalTxs==0 guard is now unreachable since such observations never enter the window. Adds TestRecord_IgnoresInvalidObservations and TestRecord_RingBufferWraparound to pin both behaviours. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four collectors under teranode_adaptive_fetch_*: mode gauge, hit-rate histogram, missing-fetches counter, transitions counter. Registered against a caller-supplied Registerer so tests can isolate. New's signature gains a prometheus.Registerer parameter. Callers (tests today; service constructors in follow-up commits) pass prometheus.DefaultRegisterer in production and prometheus.NewRegistry in tests.
The initial Observe(0) was to make registry.Gather() return the histogram before any real observations arrived, but that poisoned the histogram with a fake 0% hit-rate data point for eternity. Remove the prefill; keep the gauge Set(0) and counter Add(0) since those are genuine no-ops on the underlying metric. Adjust TestMetrics_RegisteredNamesMatchSpec to seed one real observation before Gather so the test still asserts the histogram is registered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared struct for the adaptive fetch knobs consumed by both blockvalidation and subtreevalidation. Defaults match the values documented in the design spec.
Centralises the string-to-Mode conversion so both service wirings don't need to duplicate the switch.
Field is unused until the gate is wired into blockWorker in the next commit. Kept as two separate commits so the wiring change has a single-purpose diff. Bootstrap mode parsing and Config validation errors fall back to hardcoded-defaults rather than failing Server construction — adaptive-fetch is an optimisation gate, not a correctness gate.
blockWorker now consults Server.adaptiveFetch before calling fetchSubtreeDataForBlock. In optimistic mode the call is skipped and contributingPeers is returned as nil; the optimistic-mode miss recovery happens at the subtree-validation layer (Task 18) which maintains its own adaptive-fetch state. Post-work we record a 'perfect' observation so the pessimistic-mode rolling hit rate can drive Pess→Opt transitions. If the node has no tx distributor, the first optimistic block will trip Opt→Pess via subtreevalidation's miss counts. Introduces Server.fetchSubtreeDataForBlockFn test seam so integration tests can count fetch calls without a full block validation stack.
Asserts that optimistic mode does NOT invoke fetchSubtreeDataForBlock while pessimistic mode does. Uses the fetchSubtreeDataForBlockFn seam introduced in the previous commit.
Field is unused until CheckBlockSubtrees is gated in the next commit. Errors in config parsing fall back to hardcoded defaults — adaptive-fetch is an optimisation, not a correctness gate. Also fixes pkg/adaptivefetch/metrics.go to use registerOrReuse instead of MustRegister so that calling New() against prometheus.DefaultRegisterer multiple times (e.g. in subtests that each create a Server via New()) does not panic on duplicate collector registration. Test helpers in check_block_subtrees_test.go and check_block_subtrees_large_test.go updated with a pessimistic-mode adaptiveFetch instance so nil-pointer risks are eliminated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-subtree worker consults Server.adaptiveFetch before the FileTypeSubtreeData Exists probe (and the HTTP fallback that follows it when the file is absent). In optimistic mode the probe and fetch are skipped entirely — the subtree's txs are expected to already be in the local UTXO store via propagation, and downstream processTransactionsInLevels naturally handles that by processing 0 transactions from the empty subtreeTxs[subtreeIdx] slice. Post-work we record an observation so the state machine can transition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drives an adaptivefetch.State through pessimistic → optimistic → pessimistic with synthetic observations, pinning the default config's behaviour at the layer where blockWorker uses it.
…ckFn in tests Three defensive fixes found during the Task-20 test sweep: 1. adaptivefetch.State: Mode/ShouldSkipSubtreeData/Record are now nil-safe (nil receiver falls back to pessimistic behaviour — always fetch subtreeData). 2. get_blocks.go blockWorker: use fetchSubtreeDataForBlockFn with nil-fallback to fetchSubtreeDataForBlock directly, so test Servers constructed without the function pointer still work. 3. Server_test.go / catchup_test.go: wire adaptiveFetch + fetchSubtreeDataForBlockFn on the two hand-built Server structs in TestServer_catchup and setupTestCatchupServer / setupTestCatchupServerWithConfig, providing the idiomatic full setup even though the nil-safety above would have covered them.
- Add adaptivefetch.DefaultConfig() so the 6-line config literal lives in one place. Use it in both Server fallback branches and in every test fixture that wanted the canonical defaults. - Replace the "perfect-observation" comments in both services with an honest explanation: the pessimistic-mode observation is synthetic, the Pess→Opt transition is effectively a WindowSize-block warm-up gate, and the Opt→Pess safety currently relies on node-restart rather than self-correction. Future improvement is to plumb real miss counts through the validation path. Feedback from final code review. Code logic unchanged; defaults and semantics both preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
🤖 Claude Code Review Status: Complete SummaryThis PR implements an adaptive subtreeData fetching mechanism using a two-mode state machine (pessimistic/optimistic) that learns from transaction locality patterns. The implementation is well-architected with strong safety guarantees and comprehensive test coverage. Key Strengths
Code QualityThe implementation follows project conventions well:
Areas NotedAcknowledged limitations (documented in PR description and code):
No blocking issues found. The synthetic observations are explicitly acknowledged as intellectual debt with a clear path forward (TODO at state.go:205-209). The safety argument is sound: wrong optimistic guesses only cost bandwidth, not correctness. History:
|
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-06-05 12:32 UTC |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| if prev != s.mode { | ||
| s.metrics.transitions.WithLabelValues(s.serviceName, prev.String(), s.mode.String()).Inc() | ||
| s.emitMode() | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Good catch - real bug. Mode transitions now reset the rolling window (s.window = s.window[:0], s.windowHead = 0) inside recordWithMode, immediately after the transition is detected. Each mode's thresholds are now evaluated only against observations collected while in that mode. Added regression test TestTransition_ClearsWindow_NoImmediateBackflip that demonstrates the bounce-back case (Pess→Opt trip into Opt→Pess single-block trip via MissingFetches=100 with LocalHits=TotalTxs, then a single perfect pessimistic Record which previously would have re-tripped Pess→Opt immediately — now correctly stays pessimistic until the new window fills from scratch). The test fails on the old code and passes on the new.
Copilot flagged that mode transitions did not clear the rolling window, so observations recorded while in the previous mode polluted the next mode's threshold evaluation. Concrete failure case: Pess->Opt trip leaves the window full of hit-rate=1.0 samples; an Opt->Pess single-block trip via MissingFetches=100 with LocalHits=TotalTxs replaces one slot but the other slots remain hit-rate=1.0; the very next pessimistic Record satisfies the Pess->Opt threshold and bounces back to optimistic immediately. Fix: clear s.window and s.windowHead inside recordWithMode immediately after a transition is detected. Each mode's thresholds are now evaluated only against observations collected while in that mode. Added TestTransition_ClearsWindow_NoImmediateBackflip as a regression guard.
| logger.Errorf("[SubtreeValidation] adaptive_fetch config invalid (%v) — using hardcoded defaults", afErr) | ||
| af, _ = adaptivefetch.New(adaptivefetch.DefaultConfig(), "subtreevalidation", prometheus.DefaultRegisterer) |
There was a problem hiding this comment.
Good catch — fixed in both Servers. The invalid-config fallback now starts from DefaultConfig() but overrides BootstrapMode to ModePessimistic before calling New(). A typo in any of the numeric adaptive_fetch_* settings can no longer silently re-enable optimistic skipping via the ModeAuto default. Updated log message and comments to match.
| logger.Errorf("[BlockValidation] adaptive_fetch config invalid (%v) — using hardcoded defaults", afErr) | ||
| af, _ = adaptivefetch.New(adaptivefetch.DefaultConfig(), "blockvalidation", prometheus.DefaultRegisterer) |
There was a problem hiding this comment.
Good catch — fixed in both Servers. The invalid-config fallback now starts from DefaultConfig() but overrides BootstrapMode to ModePessimistic before calling New(). A typo in any of the numeric adaptive_fetch_* settings can no longer silently re-enable optimistic skipping via the ModeAuto default. Updated log message and comments to match.
Copilot flagged that adaptivefetch.DefaultConfig() ships with BootstrapMode ModeAuto for direct package callers / tests. The Server.go fallback used on adaptivefetch.New() validation failure (numeric adaptive_fetch_* typo) therefore silently re-enabled auto transitions / optimistic skipping despite the operator-facing default being pessimistic. Override BootstrapMode to ModePessimistic in both Server fallbacks so invalid config never results in a less-safe mode.
| // The Opt→Pess transition will not fire from observations alone — | ||
| // it relies on downstream processTransactionsInLevels surfacing a | ||
| // validation error and the block failing. Until real |
There was a problem hiding this comment.
Reworded — clearer now that adaptivefetch.maybeTransition() only inspects the values passed to Record/RecordIfMode, and downstream processTransactionsInLevels validation failures are not fed back into the state machine. So the Opt→Pess trip cannot fire from validation failures either; it requires the missing-fetches plumbing or an operator-driven config change + restart.
| Namespace: "teranode", | ||
| Subsystem: "adaptive_fetch", | ||
| Name: "missing_fetches_total", | ||
| Help: "Running total of transactions recovered via processMissingTransactions, by service.", |
There was a problem hiding this comment.
Good point — the Help string now reads 'Running total of transactions recovered/fetched individually after an optimistic-mode skip, by service.' (service-agnostic, no more reference to subtreevalidation-specific processMissingTransactions).
… note Copilot review: - pkg/adaptivefetch/metrics.go: missing_fetches_total Help no longer references subtreevalidation-specific processMissingTransactions, since the metric is emitted by multiple services (blockvalidation + subtree- validation). Reworded to 'transactions recovered/fetched individually after an optimistic-mode skip, by service.' - services/subtreevalidation/check_block_subtrees.go: clarified that the Opt->Pess transition cannot fire from validation failures either; adaptivefetch only inspects Record/RecordIfMode arguments.
|
# Conflicts: # services/subtreevalidation/Server.go # services/subtreevalidation/check_block_subtrees.go # services/subtreevalidation/check_block_subtrees_test.go
Until the validation hot paths plumb real LocalHits/MissingFetches counts, callers stamp synthetic observations (LocalHits=TotalTxs, MissingFetches=0), so teranode_adaptive_fetch_hit_rate was always 1.0 and teranode_adaptive_fetch_missing_fetches_total always 0 — a permanently perfect, meaningless series that risks false operator confidence. Remove both collectors and their per-observation emission. Keep the mode gauge and mode_transitions_total, which carry real signal. The Observation fields and maybeTransition logic are unchanged — they remain the state machine's decision inputs and the hook for real counts later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback addressedConstant metrics removed ( Default mode — confirmed already correct: Out-of-scope subtreeData fetch paths — reviewed ( Still open (operational, not code)
🤖 Generated with Claude Code |
The gate only toggles an eager subtreeData *prewarm*. Full validation runs in every mode and recovers genuinely-missing txs on demand (ValidateSubtreeInternal -> getSubtreeMissingTxs -> processMissingTransactions), so a wrong optimistic guess costs bandwidth at validation time, never correctness or data integrity. The previous comments and the bootstrap_mode longdesc implied optimistic mode was a safety gamble requiring an operator restart to "recover". Reword in plain English to state accurately: - optimistic is a bandwidth/performance choice, not a safety switch - a missing tx is still fetched and validated normally - the unfired Opt->Pess auto-trip is a metrics-accuracy gap (mode gauge can stay stuck on optimistic), not a correctness risk - setting bootstrap_mode=pessimistic + restart is a tuning choice, not recovery Comments/docs only — no behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oskarszoon
left a comment
There was a problem hiding this comment.
Re-reviewed against my three earlier asks. The state machine itself is in good shape — both transition directions, inclusive thresholds, the RecordIfMode stale-mode guard, and the no-wall-clock invariant are all tested now, pkg/adaptivefetch is at 94.4%, and you've made the docs honestly describe the one-way limitation. Three things still open:
- The subtreevalidation optimistic-skip path has zero test coverage (
check_block_subtrees.go:348-429). Coverage profiling shows the optimistic branch is never taken — only the pessimistic-fetch body runs.blockvalidationhasTestBlockWorker_Optimistic_SkipsFetchSubtreeData; subtreevalidation needs the equivalent (setModeOptimistic, assert thesubtree_datafetch is skipped). This was the primary ask and the skip is the whole point of the feature here. - SonarQube duplication is still failing — now 3.8% (was 3.1%, threshold 3%). The two ~34-line observation-recording blocks (
get_blocks.go:263-299,check_block_subtrees.go:431-464) are un-extracted; a sharedrecordAdaptiveFetchObservationhelper fixes the gate and centralizes theMissingFetchesplumbing. - "self-adjusting" still overclaims.
MissingFetches=0is still hardcoded at both call sites, so the Opt→Pess auto-recovery trip can't fire — it's one-way Pess→Opt. The honest prose/longdesc is good, but please either rename to reflect one-way semantics or add a trackedTODO(#issue)at both sites for wiring the real count (len(missingTxHashesCompacted),SubtreeValidation.go:685-693). Also the PR body lists 4 metrics; the code registers 2.
Minor: adaptivefetch.New config-validation failure is swallowed silently in blockvalidation/Server.go:317 (the pessimistic fallback is safe, but log that the setting was ignored). Add the skip test + extract the dup + fix the title/TODO and I'll approve.
…ockchain#745 review Pin the adaptive-fetch gate pessimistic until the node first reaches FSM RUNNING, so cold-start IBD / restart-while-behind never skips subtreeData at the moment txs are least likely to be local. New() now always starts pessimistic; a one-way, idempotent Arm() latch (tripped by each service on the first RUNNING) applies the configured bootstrap mode and clears the pre-arm window so fake-perfect IBD samples can't cause an instant flip. The latch never re-locks, so a post-RUNNING catch-up burst may still use optimistic mode. pkg/adaptivefetch stays FSM-free: Arm is a generic trigger and the FSM wait (WaitForFSMtoTransitionToGivenState RUNNING) lives in the service layer, so TestNoWallClockOrFSMDependency still holds and the bsv-blockchain#598/bsv-blockchain#647 clock/FSM-gating class-of-bug is not reintroduced into the algorithm. Also addresses the review: - Add TestCheckBlockSubtrees_Optimistic_SkipsFetchSubtreeData covering the previously-untested optimistic skip branch in subtreevalidation (runs both modes so it cannot pass trivially). - Extract (*State).RecordSyntheticWarmup, collapsing the two duplicated ~34-line observation-recording blocks (SonarQube duplication). - Log the previously-swallowed fallback adaptivefetch.New error in both blockvalidation and subtreevalidation Server constructors. - Honest docs/TODO for the synthetic MissingFetches=0 one-way limitation; fix "four collectors" -> two comment drift; update settings longdesc to describe the arm-on-RUNNING behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in 874232d (pushed, fast-forward). Mapping to each point: 1. subtreevalidation optimistic-skip coverage — Added 2. SonarQube duplication — Extracted 3. "self-adjusting" overclaim — Reworked rather than just renamed (title is now Minor — Verification: |
|
oskarszoon
left a comment
There was a problem hiding this comment.
Re-review @ 874232d — LGTM, approving. The arming rework holds up: consensus-safe (the optimistic skip is prewarm-only; block.Valid()/CheckMerkleRoot and the SubtreeValidation.go:793-795 fence are unconditional, so it can't admit an unvalidated block), race-clean (-race on pkg/adaptivefetch passes), and no data-withholding exploit (recovery cost caps at the pessimistic baseline via the 20% full-fetch knee). All the substantive earlier asks are resolved — the subtreevalidation skip test is non-trivial, the overclaim language is gone, metrics are down to 2, and both the primary and fallback New() failures log.
Two non-blocking follow-ups (not gating this approval):
- The SonarQube duplication gate is still red (3.8%). The
RecordSyntheticWarmupextraction didn't move it — the failing dup is the near-identicaladaptivefetch.New+ bootstrap-parse + fallback-ctor block insubtreevalidation/Server.go:200-233andblockvalidation/Server.go:301-337(deltas = service name + log prefix). A sharedadaptivefetch.NewFromSettings(settings, name, reg)would clear it — keep all four per-service log lines. Worth doing whenever, the gate will keep flagging it. pkg/adaptivefetch/state.go:205is a bareTODO(adaptivefetch): plumb real recovered-tx count. Give it an issue ref (TODO(adaptivefetch, #NNNN):) so the one-way-mode debt stays tracked.
ordishs
left a comment
There was a problem hiding this comment.
Approve. Reviewed the full diff plus the surrounding CheckBlockSubtrees flow to verify the load-bearing claim.
Correctness verified
missingSubtreesis computed independently of the gate, andvalidateMissingSubtreesWithOrderedRetryrunsValidateSubtreeInternalfor every subtree regardless of mode — the optimistic path only nils out the bulk prewarm. Missing txs are still recovered on demand viagetSubtreeMissingTxs → processMissingTransactions. End state is identical; a wrong optimistic guess costs bandwidth, not correctness.FileTypeSubtreeToCheckis stored before the gate, so validation always has the tx list.contributingPeers = nilin optimistic mode only affects peer-reputation crediting — correct, since no peer provided data.- Fully nil-safe; the
modeAtSample+RecordIfModesnapshot correctly drops cross-mode observations; sharedDefaultRegistererregistration handled viaregisterOrReuse.go vet ./pkg/adaptivefetch/clean.
The arm-on-RUNNING latch genuinely closes the cold-start hazard that sank #598, and the source-scan guard (TestNoWallClockOrFSMDependency) keeps the package clock/FSM-free. Test coverage is strong — the integration tests run both modes against identical inputs with a positive control, so they can't pass trivially.
Follow-ups (none blocking)
- Medium / operational: because
MissingFetchesis hardcoded0, the windowed Opt→Pess auto-recovery never fires. A node mistakenly run optimistic on a distributor-less deployment will recover txs individually forever — potentially more expensive than the bulk prewarm — with themodegauge under-reporting the cost. Correctness is never at risk, but please track theRecordSyntheticWarmupTODO (plumblen(missingTxHashesCompacted)) as a real issue before relying on optimistic/auto outside a known-good cluster. - Low: no
armed{service}metric is exported, so the pinned-pessimistic window is indistinguishable from configured-pessimistic on themodegauge — an armed gauge would make the deploy-verification step trivial to confirm. - Low: arm-goroutine wiring is asymmetric (
g.Goin blockvalidation vs barego func()in subtreevalidation); both correct, worth aligning.
Recommend completing the dev-scale-1/2 deploy verification (the one unchecked box) before enabling optimistic in anger.


Summary
Replaces the always-fetch behaviour for block
subtreeDatawith an adaptive two-mode state machine:subtreeData. Safe for every deployment.subtreeData; txs are expected to already be in the local UTXO store via propagation. Activated when a rolling window shows > 99% local hit rate.State transitions are driven entirely by counts observed during normal work — no FSM state, no wall-clock time inside the state machine. Deliberately avoids the class of bug that caused PR #598 to be reverted via #647. A regression test (
TestNoWallClockOrFSMDependency) source-scanspkg/adaptivefetchfor forbidden imports ("time",blockchain_api,FSMStateType,time.Now,time.Since) and fails the build if anything reintroduces clock / FSM gating.Replaces:
Cold-start safety: armed on first FSM RUNNING
The gate is pinned pessimistic until the node first reaches FSM
RUNNING(fully synced), regardless of the configured bootstrap mode. This is the safety primitive that makes the feature usable: a cold-start IBD, or a restart while well behind the tip, must never skipsubtreeDataat exactly the moment txs are least likely to be local.pkg/adaptivefetch.Statealways starts pessimistic. A one-way, idempotentArm()latch applies the configured bootstrap mode and clears the pre-arm window (so the fake-perfect synthetic samples gathered during IBD can't instantly satisfy the Pess→Opt threshold).Arm()the first time the blockchain FSM reachesRUNNING, viaWaitForFSMtoTransitionToGivenState. The latch never re-locks — a later catch-up burst (CATCHINGBLOCKS) after the node has been RUNNING may still use optimistic mode, because the node has already proven it is synced.Arm()is a generic "you may now apply bootstrap" trigger; the FSM knowledge lives entirely in the service layer (which already holds the blockchain client). The no-FSM / no-wall-clock invariant — and the regression test enforcing it — therefore still hold.Architecture
Each of
blockvalidationandsubtreevalidationconstructs its own*adaptivefetch.Stateat server init and arms it on first RUNNING. Independent per-service instances — no cross-service RPC, no shared state.blockvalidation.blockWorkerconsults the state before callingfetchSubtreeDataForBlockin the catch-up path (the only place blockvalidation's gate is reachable; live blocks bypass it).subtreevalidation.CheckBlockSubtreesconsults the state before probingFileTypeSubtreeData/ falling back to HTTP — the gate that governs steady-state / live traffic.Bootstrap hint:
AdaptiveFetch.BootstrapModesetting (pessimisticdefault;optimisticorautotake effect only once the node has first reached RUNNING).Intellectual debt acknowledged up-front
The MVP emits synthetic
LocalHits=TotalTxs, MissingFetches=0observations (centralised now in(*State).RecordSyntheticWarmup), so:WindowSizewarm-up gate (post-arm), not a real hit-rate measurement.OptToPessMissThresholdtrip still can), becauseMissingFetchesis hardcoded0. ATODOonRecordSyntheticWarmupmarks the single place to plumb the real recovered-tx count (len(missingTxHashesCompacted)).This is safe: a wrong optimistic guess only costs bandwidth — downstream validation still recovers any genuinely-missing txs, so correctness is never at risk. The arm-on-RUNNING latch removes the one dangerous case (going optimistic during cold-start IBD).
Metrics
Two metrics under
teranode_adaptive_fetch_*(the hit-rate / missing-fetches series were intentionally not exported while the underlying counts are synthetic — they would publish a permanently-perfect, meaningless series; add them when real counts are plumbed):mode{service}— gauge (0=pess, 1=opt)mode_transitions_total{service,from,to}— counterTest plan
pkg/adaptivefetch— unit tests incl. transitions truth table, bootstrap modes, Arm latch (starts-pessimistic-regardless, defers-until-armed, clears-window-no-instant-flip, idempotent, nil-safe, race-clean), config-validation matrix, ring-buffer wraparound,TestNoWallClockOrFSMDependencyguard — 51 tests,-racecleanservices/subtreevalidation— full package green, incl. newTestCheckBlockSubtrees_Optimistic_SkipsFetchSubtreeData(runs both modes; pessimistic control fetches, optimistic skips)services/blockvalidation— full package green;Test_Start*-raceclean (arming goroutine)make lint(affected) — clean ·go build ./...— cleanadaptive_fetch_bootstrap_mode=optimistic; verifyteranode_adaptive_fetch_mode{service}shows pessimistic during sync and optimistic only after RUNNING, with no transition storms.🤖 Generated with Claude Code