Invariants / DC-NODE-09

DC-NODE-09

DC derived enforced

Once --mode node has spawned a --listen serve task (run_node_serve_task) over a ServedChainView, the end of the upstream feed (the relay loop returning -- e.g. a clean feed-end HaltCleanly with the operator shutdown watch still false) alone MUST NOT terminate that serve task. The serve listener's lifetime is owned by the node lifecycle owner: it terminates ONLY on (a) explicit node shutdown (the operator shutdown watch), (b) a fatal serve error (a post-bind accept fault), or (c) lifecycle-owner cancellation. The serve task stays read-only over ServedChainView (fed only by forge -> self_accept -> SelfAcceptedHandoff -> push_atomic, DC-NODE-06 / DC-NODE-07); it holds no ChainDb / WAL / forge handle, so extending its lifetime grants AVAILABILITY, not authority -- a peer that retries after the feed ended can still BlockFetch the already-self-accepted block. The process-termination guarantee is PRESERVED (moved from the feed-end stop to the lifecycle owner, never removed): operator shutdown ends BOTH the relay loop and the serve task. No serve of bytes outside ServedChainView; no durable tip advance; no peer-block admission; no RO-LIVE flip; no unbounded never-terminating serve.

Source

docs/clusters/PHASE4-N-F-G-K/cluster.md

Cluster
PHASE4-N-F-G-K
Introduced in
PHASE4-N-F-G-K

Enforcement trace

Tests 4

  • serve_task_outlives_feed_end_and_serves_late_fetch
  • serve_task_terminates_on_shutdown_no_hang
  • served_view_projects_durable_chain
  • node_serve_start_failure_is_surfaced_not_silent

Cross-references

Evidence notes

PHASE4-N-F-G-K S1 (2026-06-04). Grounded in the live C1 genesis-successor rehearsal: Ade --mode node forged + self-accepted block 0 (forge_attempted -> forge_result:succeeded) against a healthy Haskell follower session, but the follower never fetched it -- the On-arm tore down the serve listener the moment the upstream feed ended (node_serve_stop flipped right after run_relay_loop returned), before the follower's ~160s :3002 retry landed. Fix: the serve task is gated on the operator shutdown watch (shutdown.clone()), not a feed-end stop; node_serve_handle is awaited (it ends only on shutdown / fatal serve error). run_node_serve_task is byte-unchanged (already read-only over ServedChainView; no ChainDb/WAL/forge handle). RED-only; no BLUE change; no new canonical type; serving is RED I/O over already-self-accepted bytes (no replay weight). Tests: serve_task_outlives_feed_end_and_serves_late_fetch (the serve task stays alive across a feed-end + a LATE follower still BlockFetches the served block byte-identically; only shutdown ends it) + serve_task_terminates_on_shutdown_no_hang (clean termination on shutdown, no hang/leak); the existing loopback fetch + bind-failure tests are preserved unchanged. Gate ci_check_node_serve_lifetime.sh pins: the On-arm spawns the serve task with the shutdown watch (not a feed-end stop channel), no node_serve_stop.send after run_relay_loop, run_node_serve_task takes no ChainDb/forge handle. CE-G-K-3 (the live C1 rerun where the follower fetches + validates -> correlate -> PrivateRehearsalManifest) stays operator-gated (blocked_until_operator_c1_genesis_successor_rehearsal); no RO-LIVE flip; acceptance only via the follower log through correlate.