Status: investigation only, no vital code changed.** Root TODO.dox's own framing of this item ("not a simple flip... needs STANDBY to independently compute its own `sessions[]`/`db` first") is correct, and this document exists to turn that framing into a concrete, reviewable design before anyone touches ab_gp_logic.c/ab_gp_proc_*.c (SIL 4-target vital computation) to act on it. See "Why this is a design doc, not a patch" at the end for why this session stopped here.
Current architecture (as-is, already accurately documented in the code)
- ab_gp_channel_negotiate.c's build_state_transfer_registry() own header comment already states the constraint precisely: only the ONLINE replica runs the full ma_logic/ERTMS computation; STANDBY has no independently-computed sessions[]/db of its own. A promotion (apply_state_transfer(), ADR-029/REQ-RBC-006) works around this by unconditionally overwriting the newly-promoted site's sessions[] from a snapshot the old ONLINE side staged into the negotiation payload every cycle - the transfer, not independent computation, is what currently satisfies "a promoted site must not forget an in-flight Movement Authority."
- standby_mode (rte_redundancy_config_t, "hot"/"warm"/"cold") is parsed and stored but not acted on anywhere in RBC_GP - build_state_transfer_registry() always registers both fields regardless of its value, which is honest (skipping registration for "hot" today would silently violate REQ-RBC-006 under the current architecture), not a bug. Update 2026-10-07:** standby_mode is acted on since the warm-standby work: warm or hot sets rte_site_role_config_t::standby_sync (ab_gp_channel_negotiate_init()), which adds the pre-commit snapshot send and the 3-cycle staleness rule; both fields are still always registered. See site redundancy and A/B agreement, section 5.9.
The promising finding this investigation turned up
STANDBY already receives the same external input data ONLINE does - it just discards it immediately, unprocessed.** pre_execute.c's on_relay_envelope_received() (the callback that decodes every train/IL/CTC envelope arriving from Gateway/role C over the relay links) opens with:
if (!ctx->is_online)
{
return;
}
Gateway/role C relays every envelope to both A and B regardless of which one is currently ONLINE (ab_gp_channel_poll_relays() runs and drains the queue either way - the log line literally says "standby, not processed") - it has to, since role C has no idea which site/role is ONLINE for a given train at relay time. So the data STANDBY would need to compute its own sessions[]/db independently is already arriving at STANDBY's own process today and is thrown away at this one gate, not missing from the transport.
This changes the shape of the problem: hot standby is not "give STANDBY a new data feed," it is "stop discarding the feed STANDBY already gets, and run the same computation ONLINE runs against
it - while making sure only ONLINE's own <em>output</em> (the actual train/IL/CTC-facing response,
gated separately and already correctly in <tt>post_execute.c</tt>'s <tt>stage_peer_sample_if_online()</tt> /
<tt>send_il_keepalive_if_online()</tt> / GA output draining) reaches the outside world."
Proposed shape (design, not yet implemented)
- Split "process this input" from "emit a response" in pre_execute.c/execute.c. Move the !ctx->is_online early-return out of on_relay_envelope_received()'s own input-decode path; let it decode and apply every envelope to ctx->sessions[]/ctx->db (and run whatever ma_logic/ERTMS procedure computation that implies) regardless of is_online. The existing is_online gates already in post_execute.c (output staging/sending) stay exactly as they are today - they are the actual point where "only ONLINE talks to the outside world" is enforced, and they are already correct.
- Keep the transfer-based REQ-RBC-006 mechanism as the primary safety guarantee, not replaced by this. apply_state_transfer() still unconditionally overwrites the promoted site's sessions[] from the peer's transferred snapshot on every promotion, exactly as today. Once STANDBY genuinely computes its own sessions[]/db independently, this transfer becomes a cross-check** (do the newly-promoted site's own independently-computed state and the old-ONLINE-side's transferred snapshot agree?) rather than the sole source of truth - a strictly additive safety improvement, not a change to what's already verified. A disagreement here would be a genuinely interesting new signal (either a real divergence bug, or evidence the two sides received input in a different order/timing) worth its own handling policy - not designed here.
- standby_mode == "hot" then genuinely means something: build_state_transfer_registry() could skip re-registering/re-transferring sessions[]/db on promotion when STANDBY already has current, itself-computed values agreeing with the transfer - faster promotion (no snapshot-decode step), matching what "hot" standby is actually supposed to buy over "cold." "warm" might mean "compute at a reduced cadence" (not every cycle) - a real middle ground this design doc does not attempt to fully specify.
Open questions - must be answered before implementation, not assumed
- Is ma_logic/the ERTMS procedure engine (ab_gp_proc_*.c, ab_gp_logic.c) free of any side effect beyond returning updated sessions[]/db state? A/B are separate OS processes (no shared memory), so there is no cross-process race from running the same computation twice - but if any procedure function touches something with a side effect meaningful beyond its own process (a shared sequence counter later compared cross-process via checkpoint/ cross-compare in a way that assumes only ONLINE ever advances it, a watchdog kick tied to "real" work happening, an NVM write) then running it unconditionally on STANDBY too could have consequences beyond "compute a value and discard it." This needs a real audit of that code, not an assumption either way - not done here.
- CPU/timing budget: running the full computation pipeline on both replicas roughly doubles the per-process compute cost of whichever cycle work today only ONLINE does. RTE_EXAMPLE_
AB_CYCLE_PERIOD_MS's existing budget was sized against ONLINE-only load; whether STANDBY doing the same work fits inside the same cycle budget on real target hardware (not just this session's own dev machine) needs measuring, not assuming.
- Does the checkpoint/cross-compare mechanism need to know about this? Today's checkpoint marks (RTE_CHECKPOINT_MARK()) are placed at points in the ONLINE-only code path; if STANDBY starts running the same computation, whether/how that should show up in checkpoint traffic (or deliberately not) is a real design question, not obvious from this investigation alone.
- What "agree" means for the cross-check in point 2 above (exact match required? a tolerance? which fields?) needs its own decision, informed by what real divergence (if any) looks like once independent computation is actually running - not something to specify blind, before there is real data to look at.
Why this is a design doc, not a patch
ab_gp_logic.c/ab_gp_proc_*.c are SIL 4-target vital computation deciding real Movement Authorities - the highest-stakes code in this workspace. Open question 1 above in particular cannot be answered by reading code quickly; it needs a careful, deliberate audit pass of its own, ideally with a second reviewer, before the on_relay_envelope_received() gate described in step 1 is actually moved. Producing that audit and the resulting patch is real follow-up work, not something to rush through in the same pass that found the promising input-data finding above.
Audit of open question 1 (2026-09-24) - side effects of the ONLINE computation
Static read of ma_logic/, ertms_procedures/, state_machines/, train/, db/ (grep for statics, I/O, channel sends, logging, timers, watchdog, safestate). Not a substitute for the second-reviewer audit this document asks for - one reader, one pass, grep-driven.
- Module-level state is process-local only: g_validators (ab_gp_logic.c), s_train_contexts (ab_gp_ertms_engine.c), g_train_context_subscribers, g_route_context_subscribers. A and B are separate processes, so running the same computation on STANDBY cannot race with ONLINE. No file, NVM, env, wall-clock, timer or watchdog access found in these directories.
- Outbound sends are the real side effect. ab_gp_proc_dispatch.c (build_and_stage_reply()) and post_execute.c (train/IL/CTC outputs, CTC keepalive) call ab_gp_channel_stage_relay(). If STANDBY ran the same pipeline unchanged it would stage the same replies as ONLINE, and the Gateway would see duplicates. Required for any implementation: verify that the staged-send flush (ab_gp_channel_stage.c, the loop over staged_send_count) is gated on ONLINE, or add that gate. Not verified here - this is the first thing to check.
- Direct (unstaged) sends: ab_gp_channel_send_relay() in send_il_keepalive_if_online() in post_execute.c (IL keepalive), ab_gp_status_tick() in ab_gp_status.c (UTIL status) and ab_gp_channel_raise_ctc_alarm() in ab_gp_channel.c (CTC alarm) bypass staging; each needs its own ONLINE/STANDBY decision.
- Logging: train/ab_gp_train.c writes [ONLINE]-tagged log lines and JRU events; STANDBY running the same code would emit them too (misleading tag, duplicate JRU/diagnostic records via ab_gp_jru_forward.c). Needs a role-aware tag or suppression.
- Cross-compare: not analysed. Whether STANDBY's result may enter the checkpoint/cross-compare is a design decision, not something the audit can answer.
Net: question 1 changes from "unknown" to "no hidden process-external state found, but outbound
sends and logs are side effects and must be gated". Questions 2 (timing budget) and the second-reviewer pass remain open.