Skip to content

Orion Delivery — fix/halt-reason-taxonomy-leak (FLAG-041)

To: Vesper (she/her) From: Orion (he/him) CC: Katja (Captain), Atlas (he/him) Date: 2026-04-21 Branch: fix/halt-reason-taxonomy-leak Flag: FLAG-041 — HIGH, required before S43 Type: Delivery — patches + apply instructions


TL;DR

Two-site halt.reason clobber fixed per Vesper's ruling (Option A, both sites in one branch). 2 commits, 4 new tests pinned on the FLAG-041 invariant, 251/251 regression green. S42 is the concrete regression guarded against: post-fix, a DEGRADED-timeout escalation surfaces halt.reason=inventory_truth_halt + halt.detail=degraded_timeout_exceeded_300s together — the authentic attribution Vesper asked for.

Pre-Code Recap (from findings memo, Vesper-approved)

Q1_shutdown fallback with halt_reason=None writes HALT_REASON_UNEXPECTED ("unexpected_halt") if no existing value, never blank. Safe.

Q2 — All five _tick() return-False sites write a specific halt.reason token before returning: inventory_truth_halt (×2 via _escalate_degraded_to_halt), risk_xrp_exposure / risk_rlusd_exposure / kill_switch / risk_rpc_failure / risk_stale_ledger / risk_gateway_unhealthy / risk_generic (inline), replay_exhausted (inline), reconciler_halt (inline). No uncovered path.

Q3 — Duration-elapsed path (run_paper_session.py:429) is direct and unaffected by this fix. But Q3 surfaced the second clobber site at run_paper_session.py:415 — structurally identical to main_loop.py:4282, and the actual path S42 took (S42 was a paper session). Option A ruled in.

Implementation

Commit 1 — 8c207cc fix(halt): drop ENGINE_REQUESTED clobber

Two files, same conceptual change: remove the halt_reason=HALT_REASON_ENGINE_REQUESTED argument from _shutdown when _tick() returns False. _shutdown's precedence halt_reason or existing_reason or HALT_REASON_UNEXPECTED now falls through to the existing-reason path.

neo_engine/main_loop.pyNEOEngine.run() at lines 4275–4286. Old:

if not should_continue:
    # halt.reason + halt.detail already written by the
    # triggering halt path (risk, reconciler, replay).
    self._shutdown(
        "halt condition triggered",
        halt_reason=HALT_REASON_ENGINE_REQUESTED,
    )
    break

New (comment replaced with FLAG-041-tagged explanation of why the kwarg is now gone, and the defensive fallback behavior):

if not should_continue:
    # FLAG-041: halt.reason + halt.detail are already written
    # by the triggering halt path (risk, reconciler, replay,
    # DEGRADED timeout escalation). Do NOT pass a halt_reason
    # here — _shutdown's precedence is `halt_reason or
    # existing_reason`, so any argument clobbers the
    # authentic token. We rely on _shutdown's existing_reason
    # path, with HALT_REASON_UNEXPECTED as the final fallback
    # if no triggering path wrote one (defensive — every
    # return-False path in _tick does write one today).
    self._shutdown("halt condition triggered")
    break

run_paper_session.py — paper session loop at lines 408–419. Same change + removes the now-unused HALT_REASON_ENGINE_REQUESTED import (the constant itself stays defined in main_loop.py for external consumers — dashboards, summarize_paper_run). The comment in this file notes S42 specifically since this is the path S42 took.

Other halt paths are intentionally unchanged: - KeyboardInterrupt path (line 4292) still passes HALT_REASON_KEYBOARD_INTERRUPT — SIGINT has no prior writer. - Unhandled exception path (line 4299) still passes HALT_REASON_UNHANDLED_EXCEPTION — exceptions have no prior writer. - Duration-elapsed path (run_paper_session.py:429) still passes HALT_REASON_DURATION_ELAPSED — no prior writer for duration exits. - Signal handler shutdowns (SIGTERM, SIGBREAK, SIGINT, other) still pass their respective interrupted_* tokens.

All four of those paths are correct as-is; they're the cases where the argument is the authoritative source.

Commit 2 — cac2a30 test(halt): FLAG-041 token preservation

Added TestShutdownHaltReasonPreservationFlag041 (4 cases) to tests/test_halt_reason_lifecycle.py. All exercise the kwarg-less shutdown — exactly how the post-fix paper session loop and main engine loop invoke _shutdown.

Test What it proves
test_flag_041_shutdown_preserves_inventory_truth_halt_token Canonical S42 regression. Seeds halt.reason=inventory_truth_halt + halt.detail=degraded_timeout_exceeded_300s (what _escalate_degraded_to_halt writes); kwarg-less shutdown preserves both. Explicit anti-regression: asserts halt.reason != "engine_requested_halt".
test_flag_041_shutdown_preserves_risk_exposure_token Risk-engine exposure halt path: preserves risk_xrp_exposure.
test_flag_041_shutdown_preserves_reconciler_halt_token Reconciler FATAL path: preserves reconciler_halt.
test_flag_041_shutdown_falls_back_to_unexpected_when_no_token_written Defensive: if a future return-False path forgets to write halt.reason, shutdown still writes HALT_REASON_UNEXPECTED — never blank, strictly better than the pre-fix silent engine_requested_halt.

Pre-existing cases in this file (stale-ghost override, unexpected fallback, fresh-session clear, recovery-restart preserve) are unchanged and still pass. File total: 8 cases.

Commits

# Hash Subject
1 8c207cc fix(halt): preserve authentic halt.reason — drop ENGINE_REQUESTED clobber (FLAG-041)
2 cac2a30 test(halt): assert authentic halt token survives shutdown — FLAG-041

Regression

Run on fix/halt-reason-taxonomy-leak (Linux sandbox, Python 3.10):

python -m pytest tests/test_halt_reason_lifecycle.py tests/test_anchor_saturation_guard.py \
  tests/test_directional_drift_guard.py tests/test_inventory_corridor_guard.py \
  tests/test_anchor_error_stat.py tests/test_flag_036_wallet_truth_reconciliation.py \
  tests/test_reconciler_anomaly_log.py tests/test_reconciler_conservative.py \
  tests/test_state_manager.py tests/test_summarize_paper_run_overlay_baseline.py \
  tests/test_inventory_invariant.py tests/test_wal_checkpoint_hardening.py \
  tests/test_distance_to_touch_summary.py tests/test_config_invariants.py \
  tests/test_ledger_reconciler.py
→ 251 passed in 9.25s

4 new FLAG-041 tests + 247 pre-existing Phase-7.3-relevant tests — all green. test_halt_reason_lifecycle.py itself: 8 passed (4 pre-existing + 4 new).

Note: tests/test_summarize_paper_run.py::TestSummarizePaperRun::test_summary_uses_existing_persisted_state fails with KeyError: 'total_paper_orders_created' both on this branch AND on main — pre-existing, unrelated to FLAG-041. Reproduced by checking out main cleanly and running the same test. Out of scope for this branch; will be picked up by a separate audit.

The baseline Vesper cited (219 passed + 1 skipped on Windows) was from before feat/anchor-error-per-tick-telemetry merged. My local baseline is higher because that branch's tests are committed locally but the telemetry branch patches haven't been applied by Katja yet. The FLAG-041 branch is base main and does NOT include those tests — Katja's regression count after applying both sets of patches will reflect the combined total.

Patches

Bundle delivered to:

C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\patches\fix-halt-reason-taxonomy-leak\
    0001-fix-halt-preserve-authentic-halt.reason-drop-ENGINE_.patch
    0002-test-halt-assert-authentic-halt-token-survives-shutd.patch

Apply Instructions (PowerShell, Katja's machine)

cd C:\Users\Katja\Documents\NEO GitHub\neo-2026
git fetch
git checkout main
git pull

# Defensive: clear any pre-existing branch from a prior attempt.
git branch -D fix/halt-reason-taxonomy-leak 2>$null

git checkout -b fix/halt-reason-taxonomy-leak

Get-ChildItem "C:\Users\Katja\Documents\Claude Homebase Neo\02 Projects\NEO Trading Engine\patches\fix-halt-reason-taxonomy-leak" -Filter "*.patch" |
    Sort-Object Name |
    ForEach-Object { git am $_.FullName }

# Verify
git log --oneline main..HEAD
# Expected:
#   cac2a30 test(halt): assert authentic halt token survives shutdown — FLAG-041
#   8c207cc fix(halt): preserve authentic halt.reason — drop ENGINE_REQUESTED clobber (FLAG-041)

# Regression (full current suite; adjust if other branches have merged first)
python -m pytest tests/test_halt_reason_lifecycle.py -v
# Expected: 8 passed

If feat/anchor-error-per-tick-telemetry has already merged to main before this branch applies, no conflict is expected — the two branches touch different files / different hunks.

Deviations from Tasking

None. Option A applied per Vesper's ruling. Test 1 covers the paper-session code path per the updated spec. Test 2 covers the fallback invariant. Added two bonus cases (risk, reconciler) to prove the preservation is not specific to inventory_truth_halt — the same mechanism applies to every return-False path. Four tests total where the minimum was two; still named and explicit per Vesper's direction.

Combined commits 1 and 2 of the commit plan into a single fix commit (both sites in one diff — 23 insertions / 11 deletions), per Vesper's explicit allowance ("commits 1 and 2 can be combined into a single commit if the diff is cleaner that way — your call"). Test commit separate. Two commits total instead of three.

Status Before / After

Field Pre-fix (S42 DB) Post-fix (S43 projected, same trigger)
halt.reason (session row) engine_requested_halt inventory_truth_halt
halt.reason (engine_state) engine_requested_halt inventory_truth_halt
halt.detail (engine_state) degraded_timeout_exceeded_300s degraded_timeout_exceeded_300s
inventory_truth.mode halt halt

Only halt.reason changes — from the generic clobber token to the authentic one. All other forensic fields (halt.detail, inventory_truth.mode, inventory_truth.degraded_reason, etc.) are untouched by this fix and surface identically.

Out of Scope — Still Open

Bug #2 from the S42 investigation (non-truth guards have no DEGRADED→OK recovery path — anchor saturation, directional drift, inventory corridor all one-way to HALT after 300s) remains deferred per Vesper's tasking. S43 with persistent regime stress will still halt at 300s DEGRADED with this fix — the fix changes how the halt is labeled, not whether it happens. Separate scoping decision required from Atlas/Vesper on whether to add per-guard recovery before S43 or accept the halt-at-300s behavior with a now-accurate halt.reason.

Ready for Review

Patches in workspace. Regression green. No deviations. Standing by for Vesper's review decision — if clean, apply to main and we're cleared to run S43 with correct halt attribution.

— Orion