OSR-1 — 130 market replacements were REJECTED by the broker after the local log recorded them ACCEPTED, and no channel can see a broker-side terminal rejection: the order log is write-once at submit, the digest counts local failed only, reconcile compares ID sets
status: open · raised 2026-09-05 (maintainer verification of the external design review r2 — a side finding from pulling broker truth for its finding 1; NOT in the review) · class: truth divergence between layers (the AI-native audit's recurring class) · judgement: YES · effort ~0.25–0.5 pd · horizon: fired on the 08-28 and 08-31 displaced-cron sessions; recurs on any post-close submission, and applies to every sleeve's exits
Plain-language summary for an owner reading one paragraph. The system writes down an order's status once, at the moment it hands the order to the broker. If the broker later refuses it, nothing comes back to correct that line. On the two displaced-cron days, 130 of the market orders that replaced unfilled limits were refused by Alpaca at the next morning's pre-market processing. The local log says ACCEPTED for every one of them; the daily digest counted zero failures (it only counts orders that failed at submission); the reconciler reported no drift (it checks that the two sides hold the same order IDs, not what became of them); and the 08-31 displaced-cron memo assumed those orders would "fill at the next open". The positions self-heal because each run recomputes from the broker's actual positions, so the economic effect on the never-promotable meanrev control was a one-day delay. But the blindness is structural and sleeve-agnostic: a broker-refused exit on momentum would look identical — green digest, clean reconcile, ACCEPTED in the log.
Ground truth (read-only broker pull, 2026-09-05; 45-day window 07-23 → 09-04, 3,255 broker orders)
- local meanrev replacements (
market_replace_ofset): 546, of which 544 in window; - replacement terminal statuses at the broker: filled 412 · rejected 130 · expired 2 (2 predate the window);
- rejected, by local batch date: 08-28 → 45 (broker
submitted_at2026-08-31T11:00Z, i.e. Monday 07:00 ET pre-market) · 08-31 → 85 (2026-09-01T11:00Z); - side: 90 buy / 40 sell · type market · all fractional quantities;
- local
order_log.jsonlstatus for all 130:OrderStatus.ACCEPTED; - the fresh selection limit orders of the same two batches: none rejected — only the post-close market replacements were;
- cause: the orders endpoint carries no reject reason; not determined here — see data plan. Not recorded anywhere in the repo before this row (the 08-31 memo counts the batches and assumes fills).
Why nothing sees it — verified at HEAD cd571b2
state.log_orderis append-only; the replacement's record is written at submit with"status": res.status(daily.py:1252-1258), the batch's the same way inpipeline.py. There is no status-update record type.digest.py:114:failed = [o for o in todays_orders if str(o.get("status","")).lower() == "failed"]— local submit-time failures only; the!! N FAILED ordersline (:550-551) that the 07-21 audit added cannot count these.reconcile.py:135-160: symmetric difference of order-ID sets;statusis carried in the report rows and never compared.- FOS-1 (open) computes per-symbol streaks from the same local log, so it inherits the blindness.
ops/DAILY_AUDIT.md:146asks the daily audit to check failed/rejected orders against broker truth (portfolio orders) — the routine was quota-starved on exactly those days (self-audit 2026-09-02, M1), which is the only reason this was found by hand.
Fix shape (propose-only)
- Terminal-status sweep, at the start of the next run or in the digest
gather: for local orders from the prior N sessions whose recorded status
is non-terminal, fetch broker status by
client_order_id(AlpacaBroker.get_order_by_client_idalready exists) and append astatus_updaterecord — the log stays append-only, and the sweep is idempotent per (order, terminal status). - Digest: count broker-terminal
rejected/expired(andcancelednot initiated by us) as failed; FOS-1's streak logic reads the updated status, which closes its inherited blind spot for free. - Reconcile: a status-divergence section — local non-terminal vs broker terminal — report-only, so the RUNBOOK's step 2 actually answers "did what we sent happen?".
- Data plan for the cause: read the rejected orders'
failed_at/ account activities, or reproduce on the paper account by submitting one fractional market order after the close. Record the reason on this row before designing any behavioural change (e.g. holding replacements until the session opens is a policy change that goes through the owner).
Test design + negative control (CQA-1 doctrine)
SimulatedBrokerfixture where one submitted order transitions torejectedafter submit → the next run's sweep appends astatus_updateand the digest renders1 FAILED; revert the sweep →0 FAILED(today's behaviour).- Reconcile fixture with the same divergence → the new section names the order; revert → "OK — local matches broker".
- Idempotency: running the sweep twice appends one update, not two.
Kill criterion — pre-registered
If the sweep shows the broker exposes a reject reason that explains all 130 as one benign class already healed by re-selection, and the owner rules the one-day delay acceptable for the control sleeve, the policy question closes — but legs 1–3 still stand: the defect is that no layer could see it, not that it happened.
Cap note for triage
Fold partner FOS-1 (same digest surface, same log; leg 2 is literally its missing input). The reconcile leg must survive any fold.
Triage note — 2026-09-07 (annotation only; the body above is untouched)
FOS-1 shipped, and it shipped ON TOP of this row's blind spot. Leg 2 is no longer merely a fix — it is now load-bearing for an alarm that is live.
This row's own cap note said "Fold partner FOS-1 (same digest surface, same
log; leg 2 is literally its missing input)." FOS-1 was built instead, alone,
in PR #154 (ee68ebe, 2026-09-06) and now sits in queue/built/ recorded as
shipped. Verified leg by leg at HEAD e25f80b — #154 closed none of this
row's four legs:
- Leg 1 (terminal-status sweep) — not closed. #154 modified
cli.py,digest.py,failed_streaks.py(new),pipeline.pyand tests;state.py,daily.pyandreconcile.pywere not touched. There is still nostatus_updaterecord type instate.py, and nothing callsget_order_by_client_idto refresh a recorded status. The market replacement is still written once at submit with"status": res.status(daily.py:1385-1392). - Leg 2 (digest counts broker-terminal rejections) — not closed, and now
worse.
digest.py:114is byte-for-byte the line this row quotes:failed = [o for o in todays_orders if str(o.get("status","")).lower() == "failed"]. Everything #154 added reads from that same list — the streak machinery runs onlyif failed:(digest.py:121) andfailed_streaks._indexclassifies on the same local field (failed_streaks.py:66). A broker-side rejection leavesOrderStatus.ACCEPTEDon the record, so it enters neitherfailed, norfailed_new/failed_standing, nor the new exit-4 escalation (cli.py:1138-1163). - Leg 3 (reconcile status divergence) — not closed.
reconcile.py:133still merely carries"status"into the report row; the comparison is still the symmetric difference of order-ID sets (:137,:140,:144,:165). - Leg 4 (the cause of the 130) — not closed.
pipeline.py:355-356now keeps the broker's rejection text, but only on the submit-timerecord["status"] = "failed"branch (:354) — the AVB case. Nothing records why an order accepted at submit was refused later.
Why this raises the row rather than lowering it. Before #154 the blindness was a reporting gap. Now the system's newest alarm — the one that reds the run (exit 4) when a de-risking order fails — is built on the field that cannot see the failure class this row measured 130 instances of. The row predicted exactly this ("FOS-1 ... inherits the blindness"); it is no longer a prediction. And 2026-09-08 is the first live session to exercise the exit-4 path at all, so its first exercise is a blind one.
Ground truth unchanged and not re-pulled this run (no broker credentials in this container): the 130 rejections of the 08-28 and 08-31 batches are recorded in the row above from a credentialed pull on 2026-09-05.
Triage note — 2026-09-14 (annotation only; the body above is untouched)
Raised again, for the second consecutive week, and this time by a merge that
was not about it. PR #163 (3fc2dba, 2026-09-12) moved the fleet email to
weekly-plus-exception-days. The "NEW failed order(s)" exception is derived at
fleet_digest.py:1184-1187 from d.get("failed_orders") / failed_standing —
i.e. from digest.py:114, the local submit-time field this row exists about.
Consequence, stated plainly: a broker-side rejection can no longer even make a day an exception. Before #163 this class was invisible for one day. It is now invisible for up to a week. No ruling produced that; a cadence change did.
All four legs re-verified OPEN at HEAD 340bc5f, leg by leg:
- Leg 1 —
grep -rn "status_update" src/thales/returns zero hits; no such record type exists instate.py.get_order_by_client_idexists (broker.py:82,alpaca_broker.py:429) but every caller is submit-time idempotency dedup.daily.py:1385-1390still writes"status": res.statusonce and never refreshes it. - Leg 2 —
digest.py:114is byte-identical to the body's quote. FOS-1's streak machinery (:115-130) is gatedif failed:at:121, so it reads the same local-only field. - Leg 3 —
reconcile.py:133carries"status"into the row, but the only comparisons are:143and:159, a pure ID-set symmetric difference. Status is used for display only (:51). - Leg 4 —
pipeline.py:354-360keeps the broker's rejection text only on the submit-timefailedbranch (the AVB case), not this one.
Fold partner updated. FOS-1 has shipped, so the live fold partner is now
DGX-1 — same file, same send-gate, same "renderer and gate read different
sources" class. They are distinct: DGX-1 covers corrupt_position and
missed=None for the option sleeve; neither covers this. Legs 1–3 are buildable
today with SimulatedBroker fixtures and need no owner ruling; only leg 4 (why
the 130 were refused) needs credentials.
TRIAGE ANNOTATION 2026-09-21 — unchanged and still the cheapest P0 in the queue
All four legs re-verified at HEAD a325c3c: grep -rn "status_update" src/thales/ returns zero hits (leg 1); digest.py:114 byte-identical
failed = [o for o in todays_orders if str(o.get("status","")).lower() == "failed"] (leg 2); reconcile.py:142-144 and :159-165 are still pure
order-ID set differences (leg 3); pipeline.py:354-359 still records the
broker's rejection text only on the submit-time failed branch (leg 4). No
merged commit since 09-14 touched any of them.
This is one of the three rows that need no ruling from the owner at all —
only the filesystem move. Legs 1-3 build against execution/simulated.py
with no credentials; only leg 4's forensics needs a broker pull. It has been
named as build-ready in three consecutive triage reports.
Triage annotation — 2026-09-28 (annotation only; the body above is untouched)
Re-verified at HEAD 864da4e: all four legs open, byte-identical to the
09-21 reading. Still the most build-ready row in the queue. Leg 1:
grep -rn "status_update" src/thales/ returns 0 hits; daily.py:1388
still writes "status": res.status once, at submit. Leg 2: digest.py:114
unchanged — failed = [o for o in todays_orders if str(o.get("status","")).lower() == "failed"], with the FOS-1 streak block gated on if failed: at :121.
Leg 3: reconcile.py:133 carries "status" for display only; the comparisons
at :142-144 and :157-165 are pure ID-set differences. Leg 4:
pipeline.py:354-360 attaches record["error"] only on the submit-time
"failed" branch.
Compounding with FOS-3, and the fix sites do not overlap. This row shows
the exit-4 alarm reads a field that cannot see broker-side rejections; FOS-3
(annotated this run with live measurements) shows the same alarm has a second,
independent silence — a side-blind mask at failed_streaks.py:73. Two blind
spots, one alarm, disjoint fixes (digest.py:114 + state.py here;
failed_streaks.py there). Whoever builds either should read both.
The 130-rejection ground truth could not be re-pulled (no broker
credentials in this container; it dates from a 2026-09-05 credentialed pull)
and nothing in committed state contradicts it — data/state/meanrev/order_log.jsonl
still holds OrderStatus.*-prefixed statuses, consistent with the write-once
claim.
Blocked on: nothing for legs 1-3 — buildable against execution/simulated.py
with no credentials. Leg 4 (why those 130 were refused) needs a broker pull,
i.e. the owner's environment, and should be split rather than allowed to block
the rest. Honest effort: legs 2+3 ~0.2 pd; leg 1's idempotent sweep plus a
status_update record type and three fixtures ~0.4 pd; ~0.5 pd total — the
body's 0.25 low end is optimistic. Fold partner remains DGX-1 (same file, same
send gate).
Triage annotation — 2026-10-05 (annotation only; the body above is untouched)
All four legs open at HEAD 502690b, and the premise was exercised again
TODAY. Leg 1: grep -rn "status_update" src/thales/ returns zero;
src/thales/execution/daily.py:1385-1390 still writes the market replacement
once with "status": res.status; every get_order_by_client_id caller is
submit-time dedup (alpaca_broker.py:213,251,322, pipeline.py:435); grep -rn terminal src/thales/execution/*.py returns zero. Leg 2:
src/thales/execution/digest.py:114 is byte-identical — failed = [o for o in todays_orders if str(o.get("status","")).lower() == "failed"] — with FOS-1's
streak block gated if failed: at :121. Leg 3:
src/thales/execution/reconcile.py:133 carries "status" into the row while
the comparisons at :142-144 and :157-165 remain pure order-ID set
differences (PR #213 changed only the equity-date keying, per its own comment
at :167-170). Leg 4: src/thales/execution/pipeline.py:354-359 keeps the
broker's text only on the submit-time failed branch. The cadence amplifier is
intact at fleet_digest.py:1184-1187, which derives "NEW failed order(s)" from
digest.py:114.
Re-measured, and the number in the body is stale in the row's favour:
data/state/meanrev/order_log.jsonl now holds 550 market-replacement
records (body says 546), with statuses 528 OrderStatus.ACCEPTED + 22
OrderStatus.PENDING_NEW — 100% non-terminal, which is precisely the
premise. Replacements landed on 09-11, 09-18 and two on 2026-10-05, so the
path is live, not historical. Momentum carries 39, all from 2026-06-11.
Rank this week: 10 of 12 (P2 instrument calibration). It holds its slot on
cheapness and breadth rather than urgency: legs 1-3 are blocked on nothing
(buildable against src/thales/execution/simulated.py, 0.25-0.5 pd), and the
fix applies to every sleeve's exits. Only leg 4 needs a credentialed broker
pull, which is the one thing this routine cannot do. The 130 broker rejections
remain un-re-verifiable without credentials, as the row itself states.
BLOCKED on: owner approval for legs 1-3.
Approved by the steward — 2026-10-08 (legs 1–3 only)
Approved by the steward under the owner's 2026-10-07 delegation: this is P0
under NORTHSTAR §4 (a silent-failure class — the order log records ACCEPTED
for orders the broker later rejected, and no channel can see it); it has a
testable done-when built on SimulatedBroker fixtures with the row's own
negative control; and triage has recorded on four consecutive Mondays that
legs 1–3 need no owner ruling, no data and no credentials. Scope is legs 1–3
only (terminal-status sweep as an append-only status_update record; digest
counts broker-terminal rejected/expired as failed; reconcile gains a
report-only status-divergence section). Leg 4 needs broker access the
implementer does not have, and any behavioural change it motivates (e.g.
holding replacements until the open) is a policy change that stays with the
owner — do not build it. The build touches src/thales/execution/, so its PR
will be reviewed under STEWARD.md §3's trading-path rule: it must change no
order the sleeves submit, and say so in one sentence.
BUILT — 2026-10-21, implementer, PR #262 — outcome: shipped (legs 1-3)
Built exactly to the steward's 2026-10-08 approval: legs 1-3 only. Leg 4 (why those 130 were refused) is NOT built — it needs a credentialed broker pull this container does not have, and any policy change it motivates stays with the owner, as the approval says.
- Leg 1 —
execution/status_sweep.py+StateManager.log_status_update/read_status_updates. After each run (and viathales portfolio sweep-status), recent orders whose recorded status is still non-terminal are re-asked of the broker byclient_order_id, and astatus_updaterecord is appended. The order log stays append-only: the submit-time line is never rewritten. Idempotent per (order, terminal status); capped at 300 lookups; every broker call individually wrapped. - Leg 2 —
digest.pynow counts broker-terminalrejected/expiredas failed, andcanceledonly where no record claims the cancel as ours. It scans market-REPLACE records too (the 130 were all replacements, whichtodays_ordersexcludes) and reports a failure on the day it was LEARNED, which for a post-close submission is a later date than the order's own. FOS-1's streak machinery reads the corrected history. - Leg 3 —
reconcile.pygains a report-only STALE LOCAL STATUS section (local non-terminal vs broker terminal). Deliberately not part ofhas_drift: a stale status is a reporting lag, and redding the trading gate for it would be worse than the disease.
Kill criterion verdict: NOT FIRED, and not evaluable in this scope. The pre-registered criterion turns on what the broker's reject reason shows — leg 4, excluded by the approval. The criterion's own second clause already settles the legs built here: "legs 1-3 still stand: the defect is that no layer could see it, not that it happened."
Trading-path statement (STEWARD.md §3): this changes no order any sleeve
submits. The sweep runs AFTER the wrapper has finished submitting and reads
only prior sessions' records; nothing in portfolio/construct.py,
execution/pipeline.py::execute_orders, execution/safety.py or
execution/daily.py is touched.
Still open: leg 4 (forensics, owner's environment). Adjacent defect found
while building and FILED, not fixed: VRD-1 (queue/open/) —
VRPRunResult has no run_date, so the exit-4 de-risking escalation silently
degrades on the option sleeve.