Added dry-mode skip visibility to ingest_for_consumer in runner.rs. Previously a dry rehearsal that was gated out (stale or dispatch-skipped) was indistinguishable from “nothing happened.” Now every gate-out in dry mode surfaces a Telegram notify and a tracing::info log entry. Branch: fix-dry-skip-visibility (off main, NOT merged). Gate: GATE_EXIT=0, all 334+87 tests pass, clippy/fmt clean.
What Changed
1. dry_skip_msg helper
fn dry_skip_msg(source_id: &str, condition_id: &str, reason: &str) -> StringFormats a consistent message: "🧪 DRY skipped (<source_id> <condition_id>): <reason>". The 4 substrings are locked by a unit test.
2. per_message_mode() call moved above stale_gate
Previously eff (the effective mode for this source) was resolved AFTER the stale-gate check, so the stale path returned without ever knowing whether the consumer was in Dry. The call was hoisted above the stale check so eff is available at both skip points.
3. Stale path — now mode-aware
GateOutcome::Skip(reason) after stale_gate:
eff == Mode::Dry→notify(dry_skip_msg(...))+tracing::info!- otherwise →
tracing::debug!
Return is still ConsumerOutcome::Stale in both cases (no behavior change for Live).
4. Post-dispatch skip path — now mode-aware
Dispatched::Skipped(reason) after dispatch_ingest:
eff == Mode::Dry→notify(dry_skip_msg(...))+tracing::info!- otherwise →
tracing::debug!
dispatched_to_consumer call is unchanged.
5. Unit test
dry_skip_msg_includes_source_condition_reason — verifies that the output contains "DRY", "skipped", the source_id, the condition_id, and the reason as substrings.
Key Insight
A dry rehearsal whose signal was filtered by the stale gate or the dispatch gate was previously invisible to the operator. There was no way to distinguish “engine gated this signal out” from “no signal arrived at all.” With this change, dry mode produces a Telegram alert and a log entry for every gate-out, giving the operator full rehearsal visibility before arming live.
For Agents
This change is additive to Live behavior (Live paths are unchanged). The only new symbols are
dry_skip_msg(private fn inrunner.rs) and the testdry_skip_msg_includes_source_condition_reason. The mode-branching logic is iningest_for_consumerat both the stale-gate return and theDispatched::Skippedarm. Effective mode is still resolved viaper_message_mode()→config::effective_mode(global, source).
Quick Reference
- Helper:
dry_skip_msg(source_id, condition_id, reason)inrunner.rs- Skip points: stale-gate return +
Dispatched::Skippedarm iningest_for_consumer- Live paths: untouched — no behavior change when
eff != Mode::Dry- Test:
dry_skip_msg_includes_source_condition_reasoninrunner.rs