PIT-1 — the pit snapshotter greens while producing nothing (or garbage that permanently occupies its slot): three silent shapes, bleeding under the displaced-cron regime
status: open · raised panel run 16 (2026-09-01), executing the 2026-08-31 daily-audit memo's detector hand-off (item c), change-review verified and adversarially narrowed · class: moat / loudness (capture path — the P0 shape: a green step with an empty product) · judgement: YES · effort ~0.5 pd · horizon: live now — bleeds on any momentum run displaced past Tiingo's EOD publication; two of the last four trading days already bled permanently
Plain-language summary for an owner reading one paragraph. The point-in-time signal snapshotter — one of the forward-capture streams the charter calls irreplaceable — can finish green while writing nothing, and can write a nearly-empty file that then blocks its own repair forever. Last week's displaced crons exercised all of it: 08-26 is missing outright, 08-27 and 08-28 hold 123 and 58 rows against a ~890 median and can never be healed (write-once), and 08-31's run wrote nothing at all behind a green "Point-in-time snapshot" step. The proposed fix makes the writer loud and self-protecting: refuse thin writes (so the slot stays open for a same-day retry), and make "nothing was written" a visibly non-green outcome — without ever letting a capture failure fail-close trading.
Mechanism — three shapes, all verified at HEAD 29c6f23
- Exit-1 swallowed green. The no-data paths exit 1
(
src/thales/cli.py:4700-4702,:4725-4727) but the workflow step iscontinue-on-error: true(.github/workflows/paper-trading.yml, deliberately, so a capture failure can never fail-close trading — that design choice is correct and this row must preserve it), so the job shows green with nothing produced. - Stale-date idempotent skip.
cli.py:4706defaults the snapshot date toprices["date"].max(). A displaced run that fires before Tiingo publishes today's EOD sees yesterday as max; yesterday's snapshot already exists → write-once skip (src/thales/data/pit_snapshot.py:106-108) → exit 0, honestly green, nothing written. This is what happened 08-31 (run commitbca9ae3contains no pit file). - Thin write permanently occupies the slot.
build_snapshothas no row floor, so a very-late run persists whatever partial EOD rows the vendor has published (08-27 = 123 rows, 08-28 = 58 vs ~890 median), and write-once idempotency then blocks any heal — pit has no heal path (skew has one,options_skew.py:433-448).
Honesty note: the thin writes were not fully silent — capture-qa's existing 50%-of-trailing-median floor caught 08-28, which is the repeat 🔴 mail. The true claim is: green workflow, permanent slot consumption, and misattributed alerting (the alert-naming half is CQA-2's row).
Boundary vs the STAMP family (state it so triage doesn't merge them):
STAMP-1/2 are defects of date.today() — the runner's UTC wall clock used as
stamp and idempotency key. PIT-1's date comes from prices["date"].max(), a
data-lag-derived date; the STAMP fixes (ET normalization, trading-day
guard) never touch cli.py:4706 and would not stop this bleed. Distinct
mechanism, distinct fix site. GAP-1's capture_gaps detects interior holes
after the fact and deliberately stops at the stream's own latest capture, so
it cannot prevent (or even promptly see) a trailing hole; SAC-1 is
trading-path persist sites. No overlap.
Fix shape (propose-only; narrowed by adversarial review)
- Row floor at the writer: refuse to persist a snapshot below the SAME
50%-of-trailing-median convention
capture_qaalready commits to for this stream (src/thales/data/captures.py:819-826) — reuse that function/threshold, introduce no second tunable number. A refused thin write leaves the slot open for a later-same-day retry (heal-lite for free). - "Nothing written" becomes a distinct non-green outcome surfaced on the
observability channel (a separate failing step after the trading steps, or
a QA/digest condition naming
pit_snapshots) — while the trading job still completes. A heal path modeled on skew's— struck by review as a second mechanism/new surface; parked as a separate owner decision if the retry window proves insufficient in practice.
Test design + kill criterion (pre-registered, negative-controlled)
- No-quote fixture → the observability channel reds naming
pit_snapshots, AND a co-registered negative control asserts the trading job still completes on the same fixture. (Without that clause the fix's laziest implementation — deletingcontinue-on-error— passes its own test while violating the never-fail-close-trading doctrine.) - Thin fixture (rows < the reused floor) → write refused loudly, file absent, slot still writable; revert the floor → only its test reds.
- Stale-date fixture (prices max < today on a trading day) → distinct "nothing written for <today>" outcome, not a silent green skip.
- Falsifier: if the reused floor rejects a legitimately small day (real universe shrink) within 20 shadow trading days, the floor convention is wrong — record it and re-derive from history; do not tune the threshold after seeing the failure. Since the floor is capture-qa's own, such a failure indicts a convention the QA layer already committed to, which is the honest outcome either way.
Data plan
None — owned committed state (data/pit_snapshots/, workflow logs, git
history) plus static code. No external source, no cost.
Urgency, stated honestly
The 08-31 hole was healable only if the 09-01 run fired before ~23:30Z; the panel is propose-only and cannot act on that. Under Monday's ~5.7h delay mode the defect is a benign one-day lag; under the ~9h mode it bleeds a permanent day each session. Which mode this week runs in is not knowable from here — the row's case is that the writer should not depend on the scheduler's mood.
Triage note — 2026-09-14 (annotation only; the body above is untouched)
Honest de-escalation, recorded because it cuts against keeping this row high:
no new bleed since it was raised. data/pit_snapshots/ runs 15.1–15.6 KB for
every session 09-01 → 09-10, with no gaps beyond weekends and the 09-07 Labor
Day holiday. The body's one named live harm has healed — it said the 08-31 hole
was recoverable only if the 09-01 run fired before ~23:30Z, and
data/pit_snapshots/2026-08-31.parquet exists at 15,650 B.
The proximate cause was cron displacement, and ops/launchd/ com.thales.dispatch-trading.plist (2026-09-02) now dispatches at local time,
so the ~9h-delay mode is rarer. A sleeping Mac or a GitHub queue delay still
reproduces it, and the body's own argument — the writer should not depend on the
scheduler's mood — survives that.
The mechanism is untouched at HEAD 340bc5f. Shape 2:
cli.py:4958 still snap_date = date_str or str(prices["date"].max()), and
pit_snapshot.py:106-108 still if path.exists() and not force: return None.
Shape 3: build_snapshot (pit_snapshot.py:34) has no row floor;
cli.py:4977-4979 exits 1 only on a fully empty frame. Shape 1:
paper-trading.yml:180 keeps continue-on-error: true on the snapshot step
(correctly).
Partial closure not recorded in the body: paper-trading.yml:207-226 now
carries a PiT freshness heartbeat in a step that is NOT continue-on-error,
redding at PIT_AGE > 5 days, fixed 2026-09-02 to read the filename date rather
than mtime. That catches a stopped snapshotter — not a single lost day and
not a thin write, which are the two shapes this row is about.
Damage already on the books, unchanged: 2026-08-26 absent; 2026-08-27 at 4,804 B and 2026-08-28 at 3,807 B against a ~15.3 KB median — both permanently thin, and both are why the 2026-09-09 decision-grade placebo null rested on ~56 usable panel dates.
TRIAGE ANNOTATION 2026-09-21 — de-escalated a second week running; no new bleed
data/pit_snapshots/ is unbroken through 2026-09-17 (the 09-18 run writes
the 09-17 date by design). No new thin or missing day since this row was
raised on 09-01. The mechanism is entirely unchanged at HEAD — cli.py:4958
snap_date = date_str or str(prices["date"].max()); pit_snapshot.py:106-108
write-once skip; build_snapshot (:34) has no row floor; cli.py:4979-4981
exits 1 only on a fully empty frame; the workflow step is still
continue-on-error: true, which is correct and must be preserved.
So the honest reading, third time of asking: the damage already on the books (08-26 missing, 08-27/08-28 thin at 123 and 58 rows against a ~890 median) is permanent and unrecoverable, and the row buys protection against the NEXT displaced-cron regime, not repair of the last one. It inherits CQA-2's missing-today detection leg as of this run's overflow dismissal; CQA-2's reopen condition (d) fires if this row leaves the queue without it.
Watch item for the next sweep: 2026-09-21 produced no trading run at all (Actions billing outage), so the 09-18-dated snapshot is the last one — check whether 09-21 is a permanent hole once Actions is restored, and whether the write-once skip blocks a same-day retry of it.
Triage annotation — 2026-09-28 (annotation only; the body above is untouched)
THE 2026-09-21 WATCH ITEM HAS FIRED — and the answer is better than feared.
That annotation asked whether 2026-09-21 would become a permanent hole once
Actions was restored. Measured this run: data/pit_snapshots/ holds 67
files, newest 2026-09-17.parquet (15,222 B). Missing data-dates:
2026-09-18, 09-21, 09-22, 09-23, 09-24, 09-25 — six trading days, up from
zero missing at the last triage.
But the cause is the billing outage, not this row's mechanism, and the
distinction decides the disposition. No run fired at all, so no thin file
occupies those slots — the write-once skip (pit_snapshot.py:106-108) does
not block a heal, and thales snapshot-pit takes an explicit date_str
(cli.py:4958), so all six days are backfillable once data/raw is
refreshed. That is categorically different from 2026-08-27/28, which are
occupied by thin files and are permanent. Those remain the row's evidence and
are re-measured unchanged: 08-26 absent; 08-27 = 4,804 B; 08-28 = 3,807 B
against a ~15.3 KB median (next-smallest is 08-17 at 12,825 B, correctly not
flagged).
One thing will be loud on restore, and it is worth expecting. The PiT
freshness heartbeat (paper-trading.yml:212-226, not continue-on-error,
reds at PIT_AGE > 5) sees a newest file of 2026-09-17 — 11 days old
today. The first restored momentum run will red on it. That is the gate
working, not a new defect; it does not cover either of the row's two shapes
(a single lost day, and a thin write), which remain uncovered.
Mechanism unchanged at HEAD 864da4e: paper-trading.yml:181
continue-on-error: true on the snapshot step (correct, must be preserved);
cli.py:4958 unchanged; pit_snapshot.py:106-108 write-once skip; and
build_snapshot (pit_snapshot.py:34) still has no row floor, with
cli.py:4979-4981 exiting 1 only on a fully empty frame.
Blocked on: nothing technical. It still inherits CQA-2's missing-today
detection leg — CQA-2's reopen condition (d) fires if PIT-1 ever leaves the
queue without that leg, so a builder must carry it. Adjacent but separate:
dismissed GAP-1 was re-annotated 2026-09-26 with a derived ≈2026-10-23
deadline to enter the outage days into ACKNOWLEDGED_GAPS (captures.py:295);
that is GAP-1's clock, not this row's, and nothing sweeps a dismissed row.
De-escalated for a third week on the permanent-damage measure, escalated on nothing. Effort unchanged at ~0.5 pd.
Triage annotation — 2026-10-05 (annotation only; the body above is untouched)
THIS ROW'S OWN WATCH ITEM IS ANSWERED, AND THE ANSWER IS THE WORST ONE. The 09-21 annotation asked whether 2026-09-21 would become a permanent hole, and recorded "no new thin or missing day since this row was raised on 09-01." That sentence is now false. The store runs 2026-06-05 → 2026-10-02 (70 files, each run writing the previous trading day). Missing trading dates from 08-10 forward: 2026-08-26, 09-18, 09-21, 09-22, 09-23, 09-24, 09-25, 09-28, 09-29 — eight new permanent holes since the row was raised, all but one inside the 2026-09-21 → 09-30 Actions billing outage.
These holes were LOUD, not silent (Actions refused every job; nothing pretended to succeed), so they are not this row's three shapes firing. They matter here for a different reason: the equity ledgers were healed from broker truth for exactly those dates, and this stream has no heal path. The same outage that cost the account nothing cost the point-in-time signal panel eight sessions that cannot be reconstructed at any price. That is the asymmetry the charter calls irreplaceable, measured.
All three shapes still live at HEAD 502690b. Shape 2:
src/thales/cli.py:4958 snap_date = date_str or str(prices["date"].max())
plus the write-once skip at src/thales/data/pit_snapshot.py:106-108. Shape 3:
build_snapshot (pit_snapshot.py:34) has no row floor — grep "floor|median|min_rows" pit_snapshot.py returns zero — and cli.py:4977-4979
exits 1 only on a fully empty frame. Shape 1:
.github/workflows/paper-trading.yml:221-228 still continue-on-error: true
(correct, and must be preserved), with the PiT age heartbeat non-soft at
:256-269. Thin-day damage re-measured from the parquet footers: 2026-08-27
= 123 rows, 2026-08-28 = 58 rows against 887 on both 2026-09-17 and
2026-10-02 — the body's "~890 median" is accurate. The reusable 50%-of-
trailing-median floor has moved to src/thales/data/captures.py:832-839
(was :819-826).
NEW SURFACE — the fix brief is now bigger than the body says, and tomorrow
is why. Since PR #200 the trading day also runs off GitHub:
scripts/win/session.py:267 is Step("pit snapshot", th("snapshot-pit"), soft=True), which reproduces the swallow exactly, with the same >5-day
heartbeat at session.py:197-204. After the tbx-win cutover (scheduled
2026-10-06 evening) that becomes the primary surface and GitHub the backup,
so any fix must land in BOTH dispatchers or it will protect only the leg that
no longer runs the job. A builder taking this row after the cutover and
patching paper-trading.yml alone would ship a fix that never executes.
Rank this week: 4 of 12 (P0 — the only row in the tree where the loss is permanent and the clock is every trading day). BLOCKED on: nothing technical. No owner ruling, no data, no credentials — it needs the approval move and 0.5 pd. It is the clearest single answer to "what would I regret not having decided a month from now": a month of this row sitting open is ~21 more sessions of a stream whose gaps cannot be bought back.
Approved by the steward — 2026-10-08
Approved by the steward under the owner's 2026-10-07 delegation: P0 under
NORTHSTAR §4 (a green step with an empty or garbage product on a forward
capture whose holes are permanent); the fix shape is narrowed by adversarial
review to two legs that reuse capture_qa's existing 50%-of-trailing-median
threshold and introduce no new tunable number; the test design and kill
criterion are pre-registered and negative-controlled; triage records it needs
no owner ruling, data or credentials. Build legs 1–2 only (leg 3, the heal
path, stays struck). Constraint for the build: the trading job must still
complete — the new non-green outcome belongs on the observability channel,
never in the pre-trade gate, and the PR must state that tomorrow's orders are
unchanged.
Built — 2026-10-15 (interactive session for the owner) · outcome: shipped
Route, recorded honestly. Built in an interactive Claude Code session on the owner's instruction ("take care of what you can for me", 2026-10-15), not by the implementer: every cloud routine, the implementer included, was refused on an account usage limit from 10-12, and this row's bleed fired twice in that window (10-12 = 49 rows, 10-13 = 1 row; CAPTURES.md). The approval is the steward's of 2026-10-08, unchanged. The session that wrote this does not merge it: a later steward run reviews it cold, or the owner merges.
Leg 1, the writer floor. write_snapshot refuses a snapshot below
captures.pit_row_floor: the same 50%-of-trailing-median floor capture_qa
applies to this stream, now one function with two callers and no new number.
A refusal writes nothing, so the slot stays open for the next morning's run;
--force does not bypass it. snapshot-pit names each outcome: written,
already captured (exit 0), refused thin (exit 1), slot held by a thin file
(exit 1). No dispatcher changed, because both call the same command:
paper-trading.yml keeps continue-on-error: true and tbx-win keeps
soft=True.
Leg 2, "nothing written" is red on the observability channel.
capture-qa now fails pit_snapshots, naming the date, when the previous
session has no snapshot at all. This is CQA-2's missing-today leg, built here
for the PiT stream per CQA-2 reopen condition (d). It runs where capture-qa
already runs (the capture job on GitHub and tbx-win's skew session), never in
a trading job. The lag negative control holds: the normal one-session lag,
Mondays, weekends and holidays stay green. Recorded notches in
ACKNOWLEDGED_GAPS stay dim.
One departure from the body, with the reason. The stale-date fixture asked that a run which writes nothing be non-green. Since the tbx-win cutover, GitHub's backup leg finds the session already captured by the box nearly every day, so a red for that case would be a daily false alarm: the class CQA-2's lag control forbids. The run now says so ("Nothing written: <date> is already captured") and stays green. The red belongs to what is actually lost: a session with no snapshot (capture-qa), or a slot held by a fragment (exit 1).
Tests: tests/test_data/test_pit_floor.py, in the observability tier, so
it can never fail-close the pre-trade gate. They implement the pre-registered
fixtures: thin, no-quote, stale-date, the lag control, and the co-registered
trading-completes control on BOTH dispatchers. Negative controls are recorded
in the PR.
Falsifier, carried: if the floor refuses a legitimately small day (a real universe shrink) within the first 20 trading days after merge, the convention is wrong: record it and re-derive it from history; never tune it after the failure.
Not done: leg 3 (the heal path) stays struck. 10-12 and 10-13 stay truncated; rewriting a write-once capture is the owner's decision.