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.