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.

NOL-1 — the owner's one email cuts every NEEDS-OWNER: decision at 240 characters in both renderers, and ops/STEWARD.md mandates the recommendation and the deadline in exactly the positions the cut removes: today's digest carried 5 asks and 0 recommendations, and it is the only digest that will ever carry them

status: open · raised 2026-10-09 (panel run 38; surfaced independently by all three lenses and the chair, then narrowed at adversarial review from three legs to two) · class: a producer and its single reader disagree about what was delivered — the class named by the 2026-10-08 daily-audit memo, third instance · judgement: YES · effort ~0.1 pd · horizon: fired 2026-10-09 and repeats every Friday until fixed


Plain-language summary for an owner reading one paragraph. On 2026-10-07 you stepped back to one email a week, and the steward now writes the decisions only you can make as NEEDS-OWNER: lines that lead that email. Two documents contradict each other. ops/STEWARD.md tells the steward to write each line as <decision> — <recommendation> — <deadline if any>, with "a one-paragraph recommendation". The digest then prints only the first 240 characters of it. So what survives is the problem statement, and what is thrown away is always your answer and your deadline. In this week's email the phrase "My recommendation" appears zero times across all five asks. Nothing marks the lines as cut — no ellipsis, and in the HTML version no pointer to where the full text lives.

The fact that makes this irreversible rather than cosmetic

_gather_steward_week windows on lo = today - 7 days. Measured against the real committed files:

_gather_steward_week(2026-10-09) -> reports ['2026-10-07','2026-10-08'], 5 needs
_gather_steward_week(2026-10-16) -> reports [],                          0 needs

The 2026-10-09 digest is the only digest that will ever carry those five recommendations. Next Friday's window starts at 2026-10-09 and excludes the 10-08 report entirely. This is not "he sees it in full next week" — the text left the email channel permanently when today's digest rendered. (The scheduled send is 15:50Z but the last eight scheduled runs started 20:10Z–22:12Z, so by the time this row is read the send has most likely already happened. The row claims a loss, not a preventable event.)

Ground truth — the live render driven against HEAD c680133 for 2026-10-09

Five NEEDS-OWNER: lines of length 655 · 590 · 408 · 263 · 344 after _steward_needs strips the 13-character prefix. All five exceed 240, so 5 of 5 are truncated; the first loses 63% of itself. Probing the rendered body:

probein the owner's email
My recommendation (in all 5 source asks)LOST, 5 of 5
before 10-16 · Rule before 10-19LOST
60-minute grace (the B4 countersign recommendation)LOST
2026-10-20 (Actions-allowance exhaustion warning)LOST
BTB-1's ruling · dated appendLOST
captured sessionspresent — but only inside the premise "captured sessions or elapsed trading days"; the recommendation choosing it is lost

Verbatim cut points: ask 1 drops "My recommendation: spend ten minutes before 10-16 …"; ask 2 "… My recommendation: countersign with a 60-minute grace …"; ask 3 "… Choosing after the result is visible would be gate-shopping. My recommendation is captured sessions …"; ask 4 "BTB-1's ruling waits on it."; ask 5 "Record the choice as a dated append before the gate matures (~2027-01)."

Report headlines are truncated too (headline[:90], :834) at source lengths 142 and 191.

The HTML twin is strictly worse than the text twin. The text renderer at :833 writes "{n} steward report(s) (research/steward/):", at least naming the directory holding the full text. The HTML block (:1055-1058) renders only {date} — {headline[:90]} and omits research/steward/ entirely. In a mail client the owner sees five half-sentences with no ellipsis telling him they are half-sentences and no pointer, anywhere, to the rest.

LEG 1 — the 240-character cut

The causal chain, all four links verified at HEAD:

  1. ops/STEWARD.md:64 (repeated at :153) requires "NEEDS-OWNER: lines (§5) with a one-paragraph recommendation" — the lines are longer than 240 characters by instruction, not by accident.
  2. ops/STEWARD.md:243 pins the field order NEEDS-OWNER: <decision> — <recommendation> — <deadline if any> — recommendation second, deadline third, placing the payload in the discarded tail.
  3. fleet_digest.py:828 f" • {n[:240]}" and :1051 _html.escape(n[:240]). The comment at :1041 calls the HTML block an "HTML twin … (content parity)"; parity faithfully reproduces the truncation.
  4. ops/STEWARD.md:250-253 states the requirement the renderer breaks: "That section is the owner's only view of you — make each line stand on its own for someone who has not followed the week."

LEG 2 — the overflow cap drops the NEWEST asks, not the oldest

needs[:10] (:828, :1051) caps the list at ten. The gather iterates for p in sorted(Path(root).glob("*.md")) — filename order, which for YYYY-MM-DD.md is date-ascending — and needs.extends in that order with no de-duplication. So the cap retains the oldest lines and silently drops the newest.

Demonstrated (six reports × two asks = 12 needs against the cap of 10):

KEPT    ask 1/2 written on 2026-10-09 … through … 2026-10-15
DROPPED ask 1 written on 2026-10-16
DROPPED ask 2 written on 2026-10-16

The freshest and most deadline-bound lines are the ones that vanish, beneath a count line printing the true N. Two further consequences:

  • The 🙋 N need you count is a count of lines, not of decisions. Because the steward's "Carried forward" practice restates standing asks and nothing de-duplicates, one decision restated across six daily reports renders six bullets and a 🙋 6 need you subject. Demonstrated: len(needs_owner) == 6 against len(set(...)) == 1.
  • Restatements therefore consume cap slots before distinct asks do, so the cap is reached by carry-forward rather than by volume.

Today the cap did not fire (5 of 10). Honest note on size: an earlier draft of this row projected ~25 needs next Friday from "~5 reports × ~5 needs". That projection is withdrawn — the only two real reports carry 0 and 5 NEEDS-OWNER: lines (the 10-07 report's two were correctly re-prefixed DISCHARGED/SUPERSEDED), and n=2 cannot support a weekly rate. The leg rests on the mechanism, which is verified from the code and needs no rate assumption.

Why nothing caught it — two tests, each blind one layer off

  • tests/test_execution/test_digest_cadence.py:337-349 — added by #228, the fix for panel run 37's own lead finding — asserts on _gathered(...)["needs_owner"], i.e. the gather, which returns full lines. It never renders. Its fixture at :305 writes "NEEDS-OWNER: decide {d}", ~29 characters. #228's own merge verification stopped at the same layer, recording that "_gather_steward_week(2026-10-09) returns exactly the five intended NEEDS-OWNER lines". The verification stopped one layer above the truncation.
  • tests/test_execution/test_email_policy.py:367-376 does render and asserts the full line appears in both twins — the right property — but its fixture is 47 characters, so it never reaches the cut. Substituting today's real 655-character line into that same pair of assertions makes both FAIL, in text and in HTML.

Two negative controls, run twice (chair and adversarial reviewer independently): transiently setting n[:240] → n in both renderers leaves the full suite at 1745 passed / 1 skipped — identical to baseline; so does needs[:10] → needs[:1]. Nothing in 1745 tests pins either the length or the cap, and 240 appears in no config file — it is not a knob. Both edits reverted; tree clean. ops/RESEARCH_PANEL.md's rule 4 — "a detector that cannot fail is the finding" — applies literally.

What the loss cost this week

The sharpest measure is in the discarded tail of ask 1: "the Actions allowance is forecast to run out again around 2026-10-20, which would darken the backup trading leg and the digest." Three facts compound it:

  1. It is the only place in the repository that records the forecast — grep -rn "2026-10-20|allowance" over research/steward/, ops/STEWARD.md and the 10-08 audit memo returns exactly that one line.
  2. The precedent is recent. CLAUDE.md:389 records that the allowance's exhaustion "refused every job 2026-09-21 → 09-30", and web/public/notes/2026-09-21_actions_refused_every_job_over_billing.md records the result: no trading, no state, no site export, no capture, for eight trading days.
  3. The topology changed hours before the send. Since 2026-10-09 01:00Z the box is primary and Actions is the only backup — and Actions also runs fleet-digest.yml, the site export and the beacons. An exhaustion near 2026-10-20 would darken the backup trading leg and the owner's only reporting channel together.

Ask 3 is the one with a near deadline: it asks the owner to pin whether the 60-trading-day kill criteria for shortability and options_iv_term count captured sessions or elapsed trading days. Measured both ways: shortability 44 of 60 captured (60th = 2026-10-30) vs 53 of 60 elapsed (60th = 2026-10-19); options_iv_term 40 of 60 captured (2026-11-05) vs 48 of 60 elapsed (2026-10-26). Choosing after the result is visible is what C0 forbids ("no threshold moved after its data exists"). The owner received the question and the date; the recommendation that would let him answer in one word was cut.

What this row does NOT claim. It is information loss, not a safety incident. Ask 1's lost advice — "read the cutover log on the Mac and fix the SSH transport" — was already stale when the email rendered: the cutover had succeeded at 2026-10-09 01:00:22Z. So this week the truncation happened to delete advice that was obsolete and mildly misleading. An earlier draft argued the lost line might lead the owner to restore the retired dispatch plists; that was dramatization and was killed at review — the line never says to restore them, and both cutover.sh's header and RUNBOOK.md:16 forbid it. The row stands on the structural claim alone.

Killed at review — recorded so it is not re-proposed

A third leg, "the steward section has no freshness check", was KILLED. The discharge mechanism exists and works: research/steward/2026-10-07.md:50 shows the steward rewriting its own line to begin "DISCHARGED 2026-10-07 23:58Z (… marked by the steward 2026-10-08 so the Friday digest stops presenting it as open)", and _gather_steward_week then correctly excluded it — the 10-07 report contributes 0 needs, measured. What remains is a ~19-hour race (the cutover succeeded 01:00Z, the digest fires ~20:00Z, the steward's next run is 23:00Z). A one-cycle lag with a functioning discharge mechanism is not a queue row. Resurrection condition: a NEEDS-OWNER: line survives two consecutive steward runs after the fact motivating it is demonstrably resolved — i.e. the discharge mechanism misses, rather than merely lagging one cycle.

Fix shape (propose-only)

  1. Raise or drop the per-line cap for NEEDS-OWNER: bullets in both twins. If a layout bound is wanted, set it far higher, append an explicit …, and add a pointer to research/steward/<date>.md in the HTML twin, which today has none.
  2. Replace needs[:10] with all-lines-plus-a-count, or at minimum reverse the retention order so the newest survive; de-duplicate restated asks and make the 🙋 N count a count of distinct decisions.
  3. Carry each bullet's source report date (_steward_needs discards it), so a 7-day-old Friday ask is distinguishable from today's.
  4. Or fix it upstream: have ops/STEWARD.md:243 mandate recommendation-first. One line, no code — but see the kill criterion: that alone is not sufficient.
  5. Re-fixture test_email_policy.py's render assertion with a realistic line (≥400 chars with a trailing "My recommendation: …" clause) so it can fail.

Observability tier only — no trading path, no config value, no pinned rule, no frozen text; it cannot fail-close trading. ops/STEWARD.md §0 authorizes the steward to author observability fixes but not to merge its own work; per the 10-08 memo's lesson ("author and merger must be different agents") the daily audit should author and the steward merge, as they did for #228.

Pre-registered kill criterion

An earlier draft proposed "the fraction of lines whose recommendation clause survives, ≥0.90 kills the row". That was rejected at review as gameable: it is not mechanically measurable (the same data reads 0/5 under the probe "My recommendation" and 1/5 under "any imperative advice in the surviving 240 characters" — ask 4 carries its recommendation inside the cut), and its own named cure games it, since moving "My recommendation:" to character 1 scores 1.00 while the deadline and the reasoning are still cut.

The criterion is structural instead. Over the research/steward/*.md set gathered by the Friday digest at evaluation time, measure (i) the count of NEEDS-OWNER: lines whose rendered form differs from the gathered form, in each twin, and (ii) whether len(rendered bullets) == len(gathered needs). The row dies when (i) is 0 in both twins and (ii) holds, on two consecutive Friday sends in which at least one gathered line exceeds 240 characters — the long-line precondition stops a quiet week from discharging it. Today: (i) = 5 and 5; (ii) 5 == 5 holds, so leg 2 is not yet firing.

Also dies if the owner rules that a 240-character brief plus a pointer is what he wants from this channel — but the pointer must then exist in the HTML twin, which today it does not.

Reopens if a fix lands and a later digest is nonetheless observed to deliver a NEEDS-OWNER: ask whose rendered form differs from its gathered form.

Prior art (C0) — clean, checked before proposing

grep -rni "truncat|\[:240\]|240 char|NEEDS-OWNER" over research/queue/ (all four dirs), research/steward/, RESEARCH.md, TECH_DEBT.md, AUDIT.md, web/public/data/killlist.json and research/2026-08-01_quant_panel_suggestions.md: the only truncat hits are dismissed/lab-3 (a hash digest), dismissed/auto-approve (shallow clone) and built/ksp-1 (a kill-switch band). No nol-style or owner-line slug exists anywhere in the tree. Nearest neighbours, all different surfaces: open/dgx-1 (the cadence gate vs rendered alarms), dismissed/fos-2 (the fleet-email streak split), and run 37's killed-at-review F1(c) (rendering thales captures as a digest exception). Not a relitigation of any of them, and distinct from the handed-off C2/C3/C4/Q1/Q2 set.