Thales
← research journal

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

SAC-1 — census of trading-path persist sites: "state advanced or keyed without regard to run completion" (member four already found)

status: open · raised panel run 15 (2026-08-31), answering the owner handoff in 60a6630 / RESEARCH.md's top banner ("sweep for member four instead of waiting for it") · class: loudness & safety (live path) · judgement: YES · effort ~0.5–1 pd · horizon: before momentum's next halted-selection retry sequence (the 08-19/20 meanrev sequence puts the trigger cadence at roughly monthly)


Plain-language summary for an owner reading one paragraph. The last month produced three separately-discovered defects with one shape: the system wrote down what it intended to do — or what day it believed it was — without checking that the run actually completed. RWG-1 leg 2 (the turnover-blend base advanced during a safety halt), STAMP-1/STAMP-2 (a wall-clock date consumed the next session's idempotency slot), and the Kelly double-booking fixed in PR #120. You asked the panel to sweep for member four instead of waiting for it to fire. The sweep found it in under an hour — which is the evidence the class is productive and a one-shot census is worth a row.

Member four (verified this run; recorded here so the census consumes it rather than rediscovers it)

src/thales/execution/daily.py:821-831 persists last_construction (including the intended vol_scalar) inside _size_targets, gated only on dry_run. _size_targets runs at daily.py:1638; execute_orders at :1719 — the persist precedes the safety gate. Consumption chain: daily.py:658-662 reads last_construction.vol_scalar into every subsequent daily portfolio snapshot, and _gross_returns (:714-721) divides each realized return by it for the vol-target estimator. So a selection that sizes, persists the scalar, then safety-HALTs leaves the estimator attributing the real (old-scalar) book's returns to a scalar that was never applied — for up to a month on momentum, which sizes from state this corrupts.

Mechanism — the census

One-shot enumeration of every persist site in the trading path (daily.py, vrp_daily.py, pipeline.py, state.py, tca.py), each classified on two axes:

  • (a) gated on run completion (halt/exception) vs ungated;
  • (b) keyed on the unit of work (period/session) vs wall-clock.

For each ungated site, a ruling: deliberate (fact-recording/telemetry — e.g. append_equity, portfolio snapshots record what IS, regardless of what executed) vs defect (intent persisted as if executed — e.g. last_construction, the class members above). Deliverable: the classification table as a memo in research/. Propose-only; no gate ships from this row.

Scope boundary (C0 — stated so this cannot collide with existing rows)

  • Trading-path persist sites only. The capture writers are STAMP-1/STAMP-2's owned territory and are out of scope here.
  • Per-site rulings for last_targets and the kill-switch Kelly-snapshot wipe defer to RWG-1 (legs 2–3), which owns them. The census enumerates and classifies; it does not re-litigate a row that already owns a site.

Data plan

Static code reading, cross-checked against the committed natural experiment: for each site, diff its persisted value across meanrev's 08-19/20 halted runs and the 08-21 recovery (the same commits that confirmed the Kelly double-booking). Observed advancement during a halt is ground truth, not inference. Owned; zero external data; zero registry rows.

Test design

For every site the owner rules "must be gated": a halt-injection test — force a safety HALT, assert the site's persisted state is unchanged — with a negative control per gate (revert the gate; only its own test reds). Sites ruled deliberately ungated get a pinned comment naming the ruling, not a test.

Kill criterion (pre-registered)

If the census finds zero ungated-defect sites beyond the already-recorded members (RWG-1 legs 2/3) plus last_construction (named above), the class is declared fully enumerated, the row closes with the table as its record, and the class may not be reopened without a new fired instance.

Cap note for triage

queue/open/ is at/over the 12-cap. Rank this against the tree explicitly rather than letting it slide in — if something must yield, the natural fold-partner is the STAMP-1+STAMP-2 shared fix-sitting (same family), never RWG-1 (deliberately narrowed).


Dated note — 2026-09-05 (maintainer, from the external design review r2 verification)

Two more sites for the census, both verified at cd571b2, neither owned by another row at the time of writing:

  • _record_safety_event arms the same-day idempotency gate on a HALT. pipeline.py:274 writes a run_summary for any safety HALT; already_ran_today (state.py:180-187) honours any run_summary for the date; daily.py:1392 then skips with "Already ran today", which is not in cli.py:1107's critical_skips, so the run exits 0. RUNBOOK.md's post-halt instruction ("fix the cause, then thales run --skip-market-check locally", :253-254) is therefore a green no-op without --force — which the same sentence says never to use casually. Axis (a): ungated — a HALT is recorded as if the day's unit of work completed. Ruling needed: is a HALT a completed unit of work for idempotency purposes? Damage is bounded to one lost day because the month stays open via selection_completed=False (daily.py:502-511), which is why the reviewer filed it as a near-miss rather than a finding.
  • Kill-switch active: True persisted before the liquidation is attempted (daily.py:1024-1026, then _liquidate_to_cashexecute_orders at :1133). Owned by KSP-1 (raised the same day) for the fix; listed here so the census consumes it rather than rediscovers it. Same class: intent written as fact.

Neither changes this row's kill criterion; both count as members beyond the already-recorded ones, so the criterion's zero-branch cannot fire on the current tree.


Triage note — 2026-09-07 (annotation only; the body above is untouched)

Two of the five named members are CLOSED by KSP-1 (#151, 47ea024); three stand, and they are the three that corrupt sizing state. Verified at HEAD e25f80b:

memberstatus
last_construction / vol_scalar (member four)livedaily.py:841-849, gated only on dry_run (:840); _size_targets at :1781, execute_orders at :1862
_record_safety_event arms same-day idempotencylivepipeline.py:274 writes a run_summary on any HALT; state.already_ran_today honours any of them (state.py:180-187); daily.py:1523 then skips, and "Already ran today" is not in cli.py:1107's critical_skips, so the run exits 0
kill-switch active: True before liquidationCLOSED (KSP-1: deferred publish, daily.py:1103-1106:1227-1230)
Kelly-snapshot wipe on a halted liquidationCLOSED (KSP-1: non-halt branch only, daily.py:1231-1234)
last_targets (deferred to RWG-1)livedaily.py:1795-1803

The kill criterion's zero-branch therefore cannot fire, exactly as the 09-05 dated note anticipated. STAMP-1/2/3 closed the wall-clock keying axis for the two run wrappers; they closed none of the persist sites above, which sit on axis (a).

The deliverable does not exist yet: no census memo among research/2026-09-*; a grep of research/ for SAC-1 returns only queue files and the two design-review memos. Triage carries this into the decide-list folded with RWG-1 legs 2+3, per this row's own sequencing request — one ruling sitting, not two.