FOS-3 — the new exit-4 escalation can be masked by yesterday's failed BUY, and its only test reads the source as text: the one exposure-left-on alarm has two ways to stay silent
status: open · raised panel run 21 (2026-09-11, change-review catch on PR #154 — does NOT count toward the frontier cadence) · class: loudness & safety (G2, but the exit-4 path is trading-contract, not observability) · effort ~0.75 pd · horizon: live daily — the AVB streak proves failed de-risking orders are not hypothetical
Plain-language summary for an owner reading one paragraph. Last week's
IGD-1 leg-3 ruling built the escalation this shop had been missing: thales run now exits 4 — redding CI — when a de-risking sell fails for a symbol
that was NOT already failing, so a NEW stuck exit pages instead of joining
the furniture. Two defects in the build, found by this run's change review.
First, "already failing" is side-blind: if symbol X had a failed BUY
yesterday (a symbol halt or one broker-wide buying-power rejection day
poisons every symbol at once) and X's SELL fails today for the first
time, today's exposure-left-on event exits 0 and lands only in the digest —
the ruling's letter is met, its rationale (a failed buy leaves no exposure;
the sell does) is not. Second, the only test pinning the escalation reads
cli.py AS TEXT and asserts substrings — it fails if the code is deleted,
but passes if the code is present and neutered (an exception before the
raise, a later try-wrap, reordering that makes it unreachable). The single
alarm for "the book cannot get out of a name" is guarded by a detector that
cannot fail in its primary failure mode.
Mechanism — ground truth verified this run at HEAD fa55f0d
src/thales/execution/failed_streaks.py:73-78—failed_symbols_onreturns every failed symbol for the prior session with no side condition;fresh_derisking_failures(:111-116) filters today's de-risking failures onsymbol not in failed_prev_session. A prior-day failed buy therefore suppresses a first-ever failed sell. The existing test (:71 region) only exercises a sell-historyprev_failed— the buy-masking case was never considered. Contrast: the unreadable-history fallback (cli.py:1152-1156) correctly fails safe (unknown → all NEW), so the side-blindness is an oversight in otherwise fail-safe-aware code.tests/test_execution/test_failed_streaks.py:83-92— readssrc/thales/cli.pyas a string and asserts substrings between"Orders FAILED:"and"Exit 4: state is persisted". Its own docstring admits why ("the run command builds a live broker"). A blankettry/exceptaroundcli.py:1143-1176, an exception between computingfresh(:1157) andraise typer.Exit(4)(:1176), or reordering all pass green. The behavioral pieces offailed_streaks.pyARE tested; the escalation wiring is not.- The converse defect direction errs loud, not silent — a HALT day (no
orders attempted) resets
prev_failed, so a standing streak re-escalates as NEW after any halt — and is accepted as-is.
Fix shape (propose-only)
Make the masking set side-aware: only prior de-risking failures mask
(at failed_symbols_on or its call site). Build a thin injectable-broker
harness for cli.run (SimulatedBroker + tmp state dir + Typer CliRunner —
src/thales/execution/simulated.py already exists, so this is real work,
not a wish) and delete the text-grep test.
Pre-registered kill criterion / done-when
Two behavioral tests, both red today:
- Symbol X fails a BUY on session N−1 and a first-ever SELL on session N → run exits 4 (currently 0).
- CliRunner-invoked
thales runwith an injected broker exits 4 on a fresh de-risking failure, 0 on a standing one, and treats unreadable history as all-NEW — verified by mutation: wrapping the escalation block intry/exceptor removing the raise must red the suite.
Done when the text-grep test no longer exists and the behavioral tests run in the pre-trade gate (exit-4 is the run command's contract, not observability). Keeping the grep test "alongside" does not satisfy this row.
Triage annotation — 2026-09-28 (FIRST annotation on this row; the body above is untouched)
This row has carried no triage annotation since it was raised 2026-09-11. That was an oversight, not a judgement — it is corrected here.
Both defects present verbatim at HEAD 864da4e.
src/thales/execution/failed_streaks.py:73-78 (failed_symbols_on) has no
side condition, and its _index at :56-70 buckets a failed order into
failed[date][sym] with no side test at :67. :111-116
(fresh_derisking_failures) still masks on o.get("symbol") not in failed_prev_session. The grep-pinned test is still the only pin:
tests/test_execution/test_failed_streaks.py:83-93 reads
Path("src/thales/cli.py").read_text() and asserts on substrings — :83-93,
one line longer than the body's :83-92; the last assert is at :93. The
cli run wiring is unchanged (cli.py:1129, :1142, :1151, :1157,
:1176, with the unreadable-history fail-safe at :1152-1156), and there is
still no CliRunner harness for cli run anywhere in tests/.
THE PREMISE IS NO LONGER AN ANALOGY — the masking set is measurably
non-empty in live data. The body argues by construction that a failed BUY on
day N-1 can mask a first failed SELL on day N. Measured this run from
data/state/meanrev/order_log.jsonl: failed BUY orders exist on seven
sessions — SATS on 2026-07-20/21/22/23 and BRK-B on 2026-08-17/18/21 —
while AVB's failed SELL streak runs 21 consecutive sessions, 2026-08-17
→ 2026-09-18. BRK-B's failed buys on 08-17 and 08-18 populated prev_failed
for 08-18 and 08-21, so a first-ever BRK-B sell failure on either of those days
would have exited 0 — a silent de-risking failure on a live book. The row is
strictly stronger than it claims; this measurement should be written into the
build's fixture set rather than re-derived.
One stale comment found next door, worth fixing in the same PR: cli.py:1143
still describes the AVB situation as "nine-plus" sessions. It is 21.
Compounding, and the fix sites do NOT overlap. OSR-1 shows the same
exit-4 alarm is blind to broker-side terminal rejections (digest.py:114,
state.py); this row shows it is blind to a second path (the side-blind mask,
failed_streaks.py:73). Two independent silences on one alarm, two disjoint
fixes. A builder taking either should read both rows.
Blocked on: nothing. No owner ruling, no credentials, no forward clock —
execution/simulated.py exists. Build-ready. Effort: the body says ~0.75 pd;
the side-aware mask is ~1 h and the whole cost is the missing CliRunner +
SimulatedBroker harness for cli run, for which there is no precedent
fixture. 0.75 pd is honest and possibly light.
Triage annotation — 2026-10-05 (annotation only; the body above is untouched)
Both defects verbatim at HEAD 502690b, line cites unchanged. Side-blind:
src/thales/execution/failed_streaks.py:73-78 — failed_symbols_on(orders, session) returns set(failed.get(session, {})) - ok.get(session, set()) with
no side condition, consumed by fresh_derisking_failures at :111-116
(o.get("symbol") not in failed_prev_session). Wiring intact in
src/thales/cli.py (:1145 import, :1149, :1151, :1157, the 🔴 banner
:1169, raise typer.Exit(4) :1176). Grep-pinned: the only test is still
tests/test_execution/test_failed_streaks.py:83-92, whose four assertions are
all substring matches against cli.py read as text — every neutering mode the
body lists (try-wrap, an exception before the raise, reordering) still passes
green. The buy-masking case remains untested: the behavioural test above it
uses prev_failed = {"AVB"} (sell-side) and its only buy case is a same-day
fresh buy, not a prior-session one.
THE MASKING SCENARIO HAS A DOCUMENTED NEAR-MISS IN LIVE DATA — it failed to
fire by one session. From data/state/meanrev/order_log.jsonl:
2026-08-26 AVB sell failed (emergency_exit)
2026-08-27 AVB BUY failed (selection) <-- a failed BUY
2026-08-28 (no failures)
2026-08-31 AVB sell failed (emergency_exit)
AVB is the only symbol in any sleeve's log carrying both buy- and sell-side
failures. On 08-31 previous_session resolved to 08-28, which was clean, so
the sell escalated as NEW — correct and loud. Had that sell failed on 08-28
instead, the 08-27 failed BUY would have suppressed it, exactly as the body
describes. The row is no longer arguing from a constructed example.
Fleet-wide base rates, re-measured from the three committed order logs (run_summary and market_replace_of rows excluded): 20 sessions carry a failed SELL or close, 8 carry a failed BUY, 3 carry both — meanrev 4,229 orders / 28 failed, momentum 595 / 1 failed, vrp 62 / 0 failed. So failed de-risking orders are common and failed BUYs are frequent enough to be a live masking source.
EXPIRY — the horizon line is now stale and should be read as history. It says "live daily — the AVB streak proves failed de-risking orders are not hypothetical". The AVB streak ended 2026-09-18, and the three sessions since (10-01, 10-02, 10-05) are clean fleet-wide; the momentum sleeve has exactly one failed order in its entire 2026-04-07 → 2026-10-05 log (SATS buy, 2026-07-01). The defect is latent today rather than bleeding. The pre-registered kill criterion ("two behavioural tests, both red today") is unaffected and still accurate.
Rank this week: 3 of 12 (P0 — latent, but it is the one alarm for
exposure left on after a failed exit, and it has two independent ways to stay
silent: a masked escalation and a test that cannot see behaviour). BLOCKED
on: owner approval only. 0.75 pd; src/thales/execution/simulated.py is
still present for the behavioural harness.
Triage annotation — 2026-10-19 (annotation only; the body above is untouched)
This row's 2026-10-05 expiry note — "latent today rather than bleeding, because the AVB streak ended 2026-09-18 and the sessions since are clean fleet-wide" — is false. The streak resumed, and it failed again today.
Re-measured this run across all three sleeves' order logs (run_summary and
market_replace_of rows excluded):
| 2026-10-05 note | this run | |
|---|---|---|
| sessions carrying a failed SELL/close | 20 | 26 |
| sessions carrying a failed BUY | 8 | 10 |
| sessions carrying both | 3 | 4 |
Failed de-risking sells (cause: emergency_exit) on 2026-10-09, 10-12,
10-14, 10-15, 10-16 and 2026-10-19 — six of the last eight sessions that
carry any failure, today included. Per-sleeve: meanrev 35 failed of 4,383
orders; momentum 1 of 476 (SATS, a buy, 2026-07-01); vrp 0 of 17.
A cleaner arming instance than the one in the body. The row argues the mask
from the 2026-08-26 → 08-31 near-miss. On 2026-10-09 AVB's de-risking SELL
failed and WBD's BUY failed (cause: selection) in the same session, so on
2026-10-12 failed_symbols_on(prev_session) = {AVB, WBD} — a buy-only
symbol sitting in the set that fresh_derisking_failures
(failed_streaks.py:111-116) masks against. Nothing was in fact masked (WBD
had no sell failure to suppress), but that is the arming condition the row
describes, six sessions ago, on the daily-selecting sleeve, and it is newer and
tidier than the fixture the body derives. Worth re-deriving the test fixture
from it.
Both defects verbatim at HEAD. failed_streaks.py:73-78
(failed_symbols_on = failed − ok, no side condition; _index at
:56-70 buckets with no side test at :67); the only pin on the escalation is
still tests/test_execution/test_failed_streaks.py:83-92, which reads
src/thales/cli.py as text and asserts four substrings — grep -rn 'invoke(app, \["run"' tests/ returns nothing, so there is still no CliRunner harness for
cli run anywhere. cli.py:1140 still says "nine-plus sessions".
One new consequence the cutover added. The escalation did fire Exit(4)
twice this month (10-09 and 10-14, AVB fresh after a clean prior session). On
the Windows box exit 4 becomes Box-Result: failed (trade), which hands the
sleeve to GitHub's backup leg — and from 2026-10-19T00:15Z that leg is refused
for billing, while the _alert mail is withheld under
config/fleet.yaml:61's weekly_digest_only. So on a day like today, a
correctly-fired exit-4 has no backup runner and no mail; it survives only as a
Box-Result trailer that one reader checks by hand.
Live state: blocked on approval only · effort ~0.75 pd · urgency UP — the defect is armed and de-risking orders are failing today.