Orion Delivery — fix/reconciler-disappeared-order-conservative¶
To: Vesper
From: Orion
Date: 2026-04-19
Branch: fix/reconciler-disappeared-order-conservative
Status: READY FOR REVIEW (updated — C4 teardown fix applied per HOLD)
FLAG-037 age-gated conservative phantom-fill handling. Three commits, 10 new tests, 87/87 green across the relevant regression surface.
Summary¶
Orders that disappear from the ledger are now age-gated instead of unconditionally phantom-filled. Young orders (age < age_threshold_seconds) keep the pre-FLAG-037 behavior unchanged. Old orders (age ≥ threshold) are held pending operator review: no fill is applied, the reconciler writes a reconciler_anomaly_log row with action_taken='held_pending_review', emits a distinct WARNING, and the main loop calls _enter_degraded_mode("reconciler_held_pending_review") so live orders are cancelled and quoting pauses.
Fail-closed on parse failure of order.created_at per the Atlas invariant: if we cannot prove the order is young, we do not act.
Defaults shipped per Atlas lock 2026-04-19: enabled: true, age_threshold_seconds: 300.0.
Commits (4)¶
de632d2 feat(config): ReconcilerConservativeConfig dataclass + YAML defaults (FLAG-037)
db0c2fc fix(reconciler,main_loop): age-gate disappeared orders — held_pending_review (FLAG-037)
9cfc9e4 test(reconciler): FLAG-037 age gate + held_pending_review (10 tests)
0114101 test(reconciler): close StateManager before TemporaryDirectory cleanup
Branch base: 2277176 (last drift-guard commit on the in-flight stack). Patch bundle attached as patches/fix-reconciler-disappeared-order-conservative/000{1,2,3,4}-*.patch.
C1 — de632d2 — ReconcilerConservativeConfig + YAML defaults¶
neo_engine/config.py:
- New frozen @dataclass ReconcilerConservativeConfig(enabled: bool = True, age_threshold_seconds: float = 300.0).
- Top-level field on Config (follows anchor-saturation-guard / directional-drift-guard precedent; NOT nested under StrategyConfig).
- Loader block reads top-level YAML key reconciler_conservative with the Atlas-locked defaults.
- _validate_reconciler_conservative() raises ConfigError when age_threshold_seconds <= 0.
- __repr__ entry added for observability parity with other guards.
YAML added to config/config.yaml, config/config.example.yaml, config/config_live_stage1.yaml with the Atlas-locked comment block citing the 2026-04-19 ruling.
C2 — db0c2fc — Age gate + held_pending_review + main-loop escalation¶
neo_engine/models.py:
- ReconciliationResult gains held_pending_review: int = 0 with full FLAG-037 docstring explaining that this counter drives the main-loop DEGRADED escalation.
neo_engine/ledger_reconciler.py:
- LedgerReconciler.__init__ now takes conservative_config: Optional[ReconcilerConservativeConfig] = None; defaults to a fresh ReconcilerConservativeConfig() on None so legacy test call sites that pass only (engine, state) still work.
- _handle_disappeared_active_order: after the cancel-race short-circuit, evaluates _age_seconds_since(order.created_at) and checks cons_cfg.enabled and (age is None or age >= threshold). On hold:
- calls _record_disappeared_order_anomaly(..., action_taken="held_pending_review", age_seconds_override=..., age_threshold_seconds=...)
- increments result.held_pending_review
- escalates result.engine_signal to DEGRADED (preserves HALTED)
- returns WITHOUT calling _apply_full_fill — the order stays in ACTIVE / PARTIALLY_FILLED
- _record_disappeared_order_anomaly now emits three distinct WARNING formats:
1. [RECONCILER_ANOMALY] disappeared active order — phantom fill applied (legacy)
2. [RECONCILER_ANOMALY] disappeared active order — HELD PENDING REVIEW, NOT applying fill (old)
3. [RECONCILER_ANOMALY] disappeared active order — age unknown (parse failure on created_at) — HELD PENDING REVIEW, NOT applying fill (fail-closed)
- reconcile() signal-finalizer OR-clause extended with result.held_pending_review > 0 so a held event escalates to DEGRADED even if there are no ambiguous/cancel_races counters set.
neo_engine/main_loop.py:
- LedgerReconciler instantiation now threads conservative_config=config.reconciler_conservative.
- New gate at the top of the post-reconcile block in _tick:
if recon_result.held_pending_review > 0:
self._enter_degraded_mode("reconciler_held_pending_review")
_enter_degraded_mode is idempotent and preserves degraded_since on re-entry, so subsequent same-session holds are safe.
tests/test_reconciler_anomaly_log.py:
- Two MagicMock-based tests fixed to supply reconciler._conservative = ReconcilerConservativeConfig() (and the from neo_engine.config import ReconcilerConservativeConfig that goes with it). Without this they failed with TypeError: '>=' not supported between instances of 'int' and 'MagicMock' under the new gate — no production behavior change, just MagicMock hygiene.
C3 — 9cfc9e4 — 10-test suite¶
tests/test_reconciler_conservative.py — matches the Vesper-approved test plan exactly:
| # | Part | Test | What it proves |
|---|---|---|---|
| 1 | A | test_defaults_validate_ok |
Shipped defaults pass validation. |
| 2 | A | test_validator_rejects_nonpositive_age_threshold |
0, −1, −300 all raise ConfigError. |
| 3 | B | test_young_order_takes_phantom_fill_path |
30s order → phantom, action='phantom_fill_applied', full_fills=1, held_pending_review=0. |
| 4 | B | test_old_order_is_held_pending_review |
600s order → held, no fill, held_pending_review=1, signal=DEGRADED, anomaly row with correct action. |
| 5 | B | test_unparseable_created_at_is_fail_closed |
created_at='not-a-real-timestamp' → held (fail-closed), age_seconds NULL in the row, distinct 'age unknown' log. |
| 6 | B | test_disabled_config_keeps_phantom_fill_regardless_of_age |
enabled=False on an OLD order still goes phantom — regression escape hatch works. |
| 7 | B | test_age_equals_threshold_is_held |
age==threshold → held (inclusive >=). Uses a 5s threshold to sidestep clock drift. |
| 8 | B | test_cancel_race_short_circuit_unchanged |
cancel_tx_hash='DEADBEEF' short-circuits BEFORE the age gate: cancel_races=1, no anomaly row, no fill, no held. Behavior identical to pre-FLAG-037. |
| 9 | C | test_held_writes_row_with_expected_action_taken |
End-to-end against real StateManager: row persisted with action_taken='held_pending_review', correct side / quantity / session_id / xrp_equivalent. |
| 10 | C | test_tick_escalates_held_to_degraded_mode |
Source-level check: NEOEngine._tick contains both the recon_result.held_pending_review > 0 predicate AND the _enter_degraded_mode("reconciler_held_pending_review") call. Guards against accidental removal of the escalation wiring. |
Static-check note on test 10. A full runtime exercise of _tick() requires an extensive fixture stack (gateway, execution engine, inventory manager, strategy). We chose inspect.getsource(NEOEngine._tick) for two reasons: (i) the escalation is a short literal wiring whose only failure mode is accidental removal or reason-token drift, (ii) the rest of _tick has much larger runtime coverage via test_main_loop.py and friends. If you'd prefer a runtime fixture instead, say the word and I'll add it — but the source check fails on the exact regression class we care about.
C4 — 0114101 — Windows teardown fix (addressing HOLD)¶
tests/test_reconciler_conservative.py:
The state pytest fixture previously used tempfile.TemporaryDirectory() as a context manager, with the StateManager constructed inside the with block and yield sm nested inside. On Windows, SQLite holds a file lock on the .db file for the lifetime of the connection, so when __exit__ ran rmtree on the tmpdir while the connection was still open, 7 of the 10 tests teardown-errored with PermissionError: [WinError 32].
Fix applied (pytest equivalent of the addCleanup(sm.close) pattern used by C5a on feat/anchor-saturation-guard, commit d46456e):
@pytest.fixture
def state():
tmpdir = tempfile.TemporaryDirectory()
sm = StateManager(os.path.join(tmpdir.name, "test.db"))
sm.initialize_database()
sm.create_session(...)
sm.set_engine_state(...)
try:
yield sm
finally:
# LIFO: close the connection FIRST (releases the file handle),
# then cleanup the tmpdir. Without this, Windows rmtree fails.
sm.close()
tmpdir.cleanup()
test_reconciler_anomaly_log.py intentionally NOT touched — those teardown errors are pre-existing debt per Vesper's HOLD note.
No production code changed; behavior on POSIX is identical (both orderings succeed on Linux/macOS since there are no exclusive file locks).
Test Results¶
New suite¶
$ python -m pytest tests/test_reconciler_conservative.py -v
============================= 10 passed in 0.90s ==============================
Regression across touched modules (post-C4)¶
$ python -m pytest \
tests/test_reconciler_conservative.py \
tests/test_reconciler_anomaly_log.py \
tests/test_ledger_reconciler.py \
tests/test_config.py \
tests/test_anchor_saturation_guard.py \
tests/test_directional_drift_guard.py -q
87 passed in 2.29s
Windows expectation per Vesper's HOLD: 87 passed, 10 errors, where the 10 errors come from test_reconciler_anomaly_log.py pre-existing debt only. Zero errors from test_reconciler_conservative.py.
Full suite¶
The 378 failures are pre-existing FLAG-016 test-suite debt (verified by stashing my branch and re-running a representative failure: same OrderSizeConfig.__init__() missing 1 required positional argument: 'max_size_pct_of_portfolio' trace with my work absent). Not introduced by this branch.
Q1–Q5 + D1–D4 confirmation¶
All Vesper rulings applied as written:
- Q1 — Pattern. Pattern A (discriminator field on
ReconciliationResult).held_pending_review: intadded; main loop keys off it. - Q2 — Escalation point. Main loop
_tick, immediately after the existing reconciler-signal handling, before the inventory snapshot. - Q3 — Age parse failure. Fail-closed per Atlas invariant:
age_seconds is Noneevaluatesshould_hold=True. Log format:age unknown (parse failure on created_at) — HELD PENDING REVIEW, NOT applying fill. - Q4 — WARNING log formats. Three distinct messages implemented per the ruling (phantom / held / age-unknown). Structured
extrafields includeorder_id,age_seconds,age_threshold_seconds,action_taken, market context. - Q5 — One-shot vs per-tick. Per-tick: every held event writes an anomaly row; the DEGRADED transition is idempotent via
_enter_degraded_mode(preservesdegraded_since, suppresses repeat cancel-all on re-entry). - D1 — Top-level config. Applied.
reconciler_conservativeis a top-levelConfigfield and top-level YAML key, matching anchor_saturation_guard and directional_drift_guard precedent. - D2 — Fail-closed log. Applied (see Q3).
- D3 — WARNING log split. Applied (see Q4).
- D4 — Branch off main. Branch stacked on the drift-guard commits per the in-flight review stack. Rebase to main is a no-op once drift guard merges; no conflicts expected since FLAG-037 touches disjoint regions of
ledger_reconciler.py/main_loop.py.
Risk surface¶
- Production behavior change on young orders: none. Phantom-fill path is byte-for-byte identical (same code path, same
_apply_full_fill, same log/row). - Production behavior change on old orders: the change is exactly the point of FLAG-037 — no auto-fill, DEGRADED, operator review required.
- Disabled-config escape hatch verified by test #6. Flipping
reconciler_conservative.enabled: falsein YAML restores pre-FLAG-037 behavior. - Backward-compat of
LedgerReconciler.__init__:conservative_config=Nonedefault-constructs; no existing test or call site breaks on signature.
Files in the patch bundle¶
patches/fix-reconciler-disappeared-order-conservative/
0001-feat-config-ReconcilerConservativeConfig-dataclass-Y.patch
0002-fix-reconciler-main_loop-age-gate-disappeared-orders.patch
0003-test-reconciler-FLAG-037-age-gate-held_pending_revie.patch
0004-test-reconciler-close-StateManager-before-TemporaryD.patch
Apply on Katja's Windows repo root C:\Users\Katja\Documents\NEO GitHub\neo-2026\ (PowerShell):
git checkout main
git pull
git branch -D fix/reconciler-disappeared-order-conservative 2>$null
git checkout -b fix/reconciler-disappeared-order-conservative
Get-ChildItem "C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\patches\fix-reconciler-disappeared-order-conservative" -Filter "*.patch" | Sort-Object Name | ForEach-Object { git am $_.FullName }
python -m pytest tests/test_reconciler_conservative.py tests/test_reconciler_anomaly_log.py tests/test_ledger_reconciler.py tests/test_config.py tests/test_anchor_saturation_guard.py tests/test_directional_drift_guard.py -q
Rule compliance:
- git branch -D runs before git checkout -b so a pre-existing local branch is silently cleared.
- Get-ChildItem ... | Sort-Object Name | ForEach-Object { git am $_.FullName } replaces the *.patch glob, which does not expand in PowerShell.
- Expected Windows result: 87 passed, 10 errors (all from pre-existing test_reconciler_anomaly_log.py teardown debt).
Diffstat:
config/config.example.yaml | 16 ++
config/config.yaml | 15 ++
config/config_live_stage1.yaml | 8 +
neo_engine/config.py | 68 ++++-
neo_engine/ledger_reconciler.py | 124 +++++++--
neo_engine/main_loop.py | 18 +-
neo_engine/models.py | 11 +
tests/test_reconciler_anomaly_log.py | 6 +
tests/test_reconciler_conservative.py | 501 +++++++++++++++++++++++++++++++++
9 files changed, 750 insertions(+), 17 deletions(-)
Standing by for review.
— Orion