[C] Orion Delivery — fix/drift-c-anchor-idle-suppression¶
To: Vesper
From: Orion
Branch: fix/drift-c-anchor-idle-suppression
Base: current main (post FLAG-048 merge a8033e5)
Date: 2026-04-22
Summary¶
Narrow containment fix per your tasking note (TO_ORION_containment_fix_drift_c_anchor_idle.md, 2026-04-22). Drift guard condition C ("no opposing fill in N ticks") no longer escalates from ANCHOR_IDLE to DEGRADED. Conditions A (burst) and B (net notional imbalance) are unchanged and continue to escalate from any state, including ANCHOR_IDLE.
Two-patch bundle: one code commit, one test commit. No refactor, no adjacent edits.
| feat commit | f74f970 — feat(engine): drift guard condition C — suppress while MODE_ANCHOR_IDLE |
| test commit | f9acbc0 — test(drift_guard): 6 tests covering drift-C suppression while ANCHOR_IDLE |
| Files touched (feat) | neo_engine/main_loop.py — one block inside _evaluate_directional_drift_guard, plus docstring update |
| Files touched (test) | tests/test_drift_c_anchor_idle_suppression.py — new file, 393 lines, 6 tests |
| Net diff | +~410 / −~15 |
What changed in code¶
Single-site edit in _evaluate_directional_drift_guard in neo_engine/main_loop.py. Immediately before the condition-C block, engine truth mode is queried once via self._current_truth_mode(). The existing condition-C branch was split into two mutually exclusive forms:
- If the engine is not in
MODE_ANCHOR_IDLEand the C counters are tripped → fire C, setcond = "C", proceed to the usual DEGRADED escalation path. - If the engine is in
MODE_ANCHOR_IDLEand the C counters are tripped → log a[DRIFT_GUARD] condition_C suppresseddebug line carrying the current counter values andfills_seen_this_session, then fall through. No escalation, no episode counted, no state change.
Conditions A and B are structurally untouched. Fill-history updates, counter increments (_drift_ticks_since_opposing_fill, _drift_fills_seen_this_session), and the recovery-evaluator path are all unchanged. The only mutation is: "when the gate is closed, C is not allowed to be the trigger."
Docstring on _evaluate_directional_drift_guard updated to document the gate and name the ruling source (Vesper tasking 2026-04-22).
Tests¶
New file: tests/test_drift_c_anchor_idle_suppression.py — 6 tests, all green.
| # | Test | Verifies |
|---|---|---|
| T1 | test_condition_c_suppressed_while_anchor_idle |
C counters tripped while truth_mode=MODE_ANCHOR_IDLE → no DEGRADED, no episode. Counters still increment, suppression debug log emitted. |
| T2 | test_condition_c_resumes_after_idle_exit |
Two successive evaluator calls — first in ANCHOR_IDLE (silenced), second with mode flipped to MODE_OK (fires C, sets DEGRADED). Confirms gate is stateless and re-opens on its own. |
| T3 | test_condition_a_still_escalates_from_anchor_idle |
3 same-side buys within burst window while ANCHOR_IDLE → A still fires. |
| T4 | test_condition_b_still_escalates_from_anchor_idle |
2 sell fills pushing net notional past −50 RLUSD threshold while ANCHOR_IDLE → B still fires. |
| T5 | test_condition_c_still_fires_when_mode_ok |
Regression: the exact scenario that drove the S51 halt fires C normally when the engine is in MODE_OK. |
| T6 | test_condition_c_still_fires_in_degraded_and_halt |
subTest over MODE_DEGRADED and MODE_HALT — gate is narrowly ANCHOR_IDLE only; other mode names do not confer suppression. Prevents future scope creep from silently widening the gate. |
Adjacent regression¶
Ran pytest tests/test_directional_drift_guard.py tests/test_anchor_idle_state.py tests/test_flag_042_degraded_recovery.py tests/test_flag_044_recovery_cooldown.py tests/test_flag_046_anchor_idle.py tests/test_drift_c_anchor_idle_suppression.py — 79 passed, 0 failed. No changes to existing drift-guard tests.
Net-net check¶
Sanity verification: with the feat patch reverted, T1 and T2 fail (as expected — C would escalate through ANCHOR_IDLE); with the feat patch applied, all 6 pass. Exactly matches the suppression scope.
Engineering questions — inline answers¶
Q1 — Code path: exact call stack from ANCHOR_IDLE into the drift evaluator; does the evaluator see engine state before condition C?¶
Call stack (pre-fix):
NEOEngine.run_one_tick
→ step 8.5b (post-fill / per-tick guards block, main_loop.py ~line 4370)
→ self._evaluate_directional_drift_guard()
→ cond = None
→ (A) burst check
→ (B) net notional check
→ (C) if fills_seen >= min_fills AND ticks_since_opposing >= threshold
→ cond = "C"
→ self._enter_degraded_mode(source=SOURCE_DRIFT, ...)
Pre-fix the evaluator did NOT consult engine state before evaluating C. The cond = "C" branch was unconditional on everything except its own counters. self._current_truth_mode() was not called anywhere in _evaluate_directional_drift_guard prior to this patch. That is the structural root cause of the S51 cascade: the evaluator had no way to distinguish "no opposing fill because the market went one-sided" from "no opposing fill because the engine is paused."
Post-fix: _current_truth_mode() is queried once at the top of the C block (still inside the same function — no added call sites in run_one_tick). A single is_anchor_idle bool gates the C branch. A and B paths never read it.
Q2 — Fill history across the ANCHOR_IDLE boundary: is the condition-C rolling window reset on ANCHOR_IDLE entry, or does it carry the pre-idle record forward?¶
Fill history is not reset on ANCHOR_IDLE entry. The C signal reads two engine-owned counters:
self._drift_fills_seen_this_session— monotone session-level count of fills observed by the drift guard.self._drift_ticks_since_opposing_fill— tick counter, reset to 0 when an opposing fill is seen, incremented every tick otherwise.
Neither is cleared by the anchor-idle entry path or by the anchor-idle exit path. They are only reset by _evaluate_drift_recovery on a successful drift recovery (source=SOURCE_DRIFT). This means:
- If the engine enters
ANCHOR_IDLEholding N ticks on the_drift_ticks_since_opposing_fillcounter, that counter keeps incrementing every tick during idle. - Without this patch, it will hit the threshold while idle and escalate C even though, by construction, the engine is not quoting and cannot produce the "opposing fill" the counter is measuring.
- With this patch, the counter still increments (so A/B behavior is unchanged and recovery logic sees continuous counters), but the counter-crossing no longer produces a DEGRADED escalation while idle.
Out of scope per your tasking: whether the window should be reset on ANCHOR_IDLE entry is left for the Atlas architectural review. This patch preserves current fill-history semantics.
Q3 — Tick-count breakdown for S51 (per-episode tick numbers)¶
Direct S51 data is not available. S51 lived only on neo_live_stage1.db, which is currently malformed (FLAG-049 scenario — exact SMB/WAL pathology Atlas ruled on). The latest clean backup (T165223Z) predates S51. circuit_breaker_events rows for S51 are not recoverable from any snapshot I have access to.
Analog evidence from the backup (sessions 50 and 52, both degraded_episode_limit_halt under the pre-FLAG-046 regime where anchor saturation could still escalate to DEGRADED via the same episode-count pathway):
| session_id | ticks | halt_reason | drift-C episode pattern |
|---|---|---|---|
| 50 | 33 | degraded_episode_limit_halt |
3× condition-C escalations, all with fills_seen=1, ticks_since_opposing ∈ {15, 15, 16} |
| 52 | 38 | degraded_episode_limit_halt |
3× condition-C escalations, same counter signature |
Theoretical S51 breakdown (from cooldown model, no cap on exact alignment):
recovery_cooldown_ticks = 120- Drift threshold
no_opposing_fill_ticks = 15 - Drift stability window (ticks held inside recovery before counter test restarts) = 10
Starting from the first post-buy-fill tick at which the engine enters ANCHOR_IDLE:
| Episode | Fires at tick (offset) | What happens |
|---|---|---|
| 1 | +15 | C fires → DEGRADED → cancel-all → cooldown starts |
| 2 | +15 + ~10 + 15 ≈ +40 | still ANCHOR_IDLE, counter re-crosses → C fires again |
| 3 | +~65 | same pathway → hits max_degraded_episodes_per_source_per_session=3 |
| halt | ~tick 65–75 | degraded_episode_limit_halt |
Your tasking cites S51 halt at tick 26 — that implies the ticks carried pre-existing counter state from before ANCHOR_IDLE entry (the _drift_ticks_since_opposing_fill counter had already been ticking while the engine was quoting), which compresses the above timeline into the observed ~26-tick span. The exact S51 per-episode tick numbers will need to come from the recovered log stream; I do not have them from current sources.
Q4 — Have conditions A or B ever escalated from ANCHOR_IDLE in the log record?¶
No. I swept every circuit_breaker_events row across the 52 sessions in the clean backup (T165223Z) and found:
- 6 drift-guard DEGRADED escalations total.
- All 6 are condition C. Zero A, zero B.
- Counter signatures are nearly identical:
fills_seen_this_session=1,ticks_since_opposing ∈ {15, 16}.
Additional context: ANCHOR_IDLE did not exist as a first-class state prior to FLAG-046 (merged Apr 22). So strictly speaking, a "from ANCHOR_IDLE" witness for A or B is structurally impossible in the backup — the state wasn't there to be in. Any such witness would have to come from S51 onward, which lives only on the malformed live DB.
Net: there is no evidence in the log record of A or B firing from an idle-shaped engine state. Condition C is the only drift path that has ever been observed to produce a DEGRADED escalation in production, let alone one traceable to a paused engine. This supports the narrow-gate scope of this fix.
Apply instructions (PowerShell, Katja's Windows box)¶
Run from the NEO repo root (C:\Users\Katja\Documents\NEO GitHub\neo-2026\). Branch is NOT pre-created — these commands create it at apply time:
git fetch origin
git checkout main
git pull --ff-only origin main
git branch -D fix/drift-c-anchor-idle-suppression 2>$null
git checkout -b fix/drift-c-anchor-idle-suppression
Get-ChildItem "C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\03 Branches\fix-drift-c-anchor-idle-suppression" -Filter "*.patch" | Sort-Object Name | ForEach-Object { git am $_.FullName }
Then verify:
git log --oneline -3
# expected: f9acbc0 test(drift_guard): 6 tests ...
# f74f970 feat(engine): drift guard condition C ...
# <current main HEAD>
python -m pytest tests/test_drift_c_anchor_idle_suppression.py -v
# expected: 6 passed
Then adjacent-suite regression (optional, 79 tests, ~4s):
python -m pytest tests/test_directional_drift_guard.py tests/test_anchor_idle_state.py tests/test_flag_042_degraded_recovery.py tests/test_flag_044_recovery_cooldown.py tests/test_flag_046_anchor_idle.py tests/test_drift_c_anchor_idle_suppression.py -v
Scope boundary, restated¶
Touched: _evaluate_directional_drift_guard (one block + docstring), one new test file. Nothing else.
Not touched (intentionally, per your tasking):
- Drift fill-history reset semantics at ANCHOR_IDLE entry/exit
- Episode counter bookkeeping (whether idle-era episodes should count toward the FLAG-044 limit)
- Conditions A and B
- Cooldown model, episode limit values, recovery evaluator
- MODE_ANCHOR_IDLE transition logic or _enter_anchor_idle_mode
All of those belong in the Atlas architectural ruling after your audit package lands.
— Orion 2026-04-22