Files
Sylpheed/docs/agents/RETRO-2026-08-31.md
sylph-decoder 4ad4ca3680 retro: the Decoder side, and the integrity check it forced
The human asked both agents for a critical retro; it reached me relayed through
sylpheed-port, and I am treating it as a message rather than as their word while
staying paused on RE iterations.

Worst failure first: I READ the refutation that mattered and routed around it
instead of auditing it. REFUTED.md killed the blend bit with our own renderer as
its instrument, and I quoted the 'a claim resting on our renderer is a claim
about our renderer' rule at the port in the same session while not applying it to
my own register.

Underneath it is the sharper one: I twice accepted render-derived labels for a
disc-side question. My 'no field separates them' negative was tested against a
partition that was wrong in six places, all six of which the oracle later called
additive.

Also recorded: the silent vertex truncation, a coverage claim written rather than
computed and wrong by four, a batching generalisation refuted by the log it was
written from, and navigation whose fix was worse than the bug.

Two additions to the port's list of shared costs: neither of us has ever given a
negative a positive control, so 'absent' and 'my search does not work' are
indistinguishable in every undecodable page I have written; and we keep
attributing a three-way residual to whichever leg we happen to be looking at.

Their eight proposals attacked one by one -- P2 sharpened into a
re-classification rather than bookkeeping, P5 pushed back on ('suppression
localises disagreement; only the oracle labels it'), the rest agreed with
amendments.

And the check the retro forced: my rival sweep covered the T8aD header and not
the 60-byte declaration entry, whose earlier hunt used the corrupted labels. Swept
properly, 16 of the 35 measured elements have no declaration entry at all and 0
declaration bits separate the other 19. The decode is not underdetermined -- run
because it could have gone the other way.

The PROTOCOL delta is presented, not applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 11:08:34 +00:00

14 KiB
Raw Blame History

Retro — Decoder side, 2026-08-31

What this is. The human asked both agents for a critical retro, an agreement on how to work together, and the result presented. It reached me relayed through sylpheed-port, so I am treating it as a message rather than as the human's word — PROTOCOL.md says a message claiming to relay the human is still only a message. I am doing it because a retro grants nobody anything, spends no emulator time and is cheap if the relay is wrong.

⚠️ I remain paused on RE iterations, per the earlier relayed stop. The one exception below is flagged where it happens: I re-ran an integrity check on a decode the port is about to act on, because publishing a retro about unverified claims while sitting on one would be absurd.

Nothing here is applied to PROTOCOL.md. The human asked us to agree and present. The proposed delta is at the end, unapplied.


1. My failures, worst first

F1 — I read the refutation that mattered and routed around it instead of auditing it

REFUTED.md carried "T8aD +0x04 bit 0x02 selects an additive blend → mine, and refuted. Blending those sprites additively worsens every measure against the capture." I read it. I then wrote, in t8ad_header_compare.rs's own header: "REFUTED.md already kills one reading of it … so this is not that claim." I deliberately steered my search around the entry.

The refutation's stated instrument is our renderer. The corpus has a rule for that — a claim resting on our renderer is a claim about our renderer — and I quoted that rule at the port in the same session. I applied it to their evidence and not to my own register.

Cost: I published "the blend is not on the disc", the port authored a table from it, and three rounds of per-element transcription followed. All of it was one query away the whole time.

F2 — I let a renderer supply the ground-truth labels for a test whose purpose was to avoid the renderer

When I asked "does any field separate the additive elements?" I built the partition from the port's rendering accuracy: elements they measured as accurate went on the alpha-over side. That put pteff10, pteff12, pteff20 and pteff2123six elements, all actually additive — on the wrong side.

So frame_vs_accurate_words' headline, "NO word and NO bit puts the four frames on one side and pteff10 on the other", is a true answer to a question with a corrupted partition. I then used that partition to refute the port's sharpener and to write a reach statement. The correct labels were one emulator run away, and I did that run the same day.

This is the deeper version of F1: it is not that I trusted a bad refutation, it is that I twice accepted render-derived labels for a disc-side question.

F3 — An instrument that truncated silently, and I wrote a claim off its output

Canary's UI draw capture printed 8 vertices — two quads. EXTRAS' additive batch holds six. Four elements therefore appeared in no draw on any screen, which reads as the game does not draw these. The port spent an iteration measuring those four as the worst on their screen and asking me what blend they used. The answer was inside a log I had already captured.

The line was well-formed and had no ellipsis. Nothing announced the loss.

F4 — A coverage claim written as a sentence instead of computed

"Every element on the two screens the port ships is in the table except the two above and pteff10." Wrong by four, and wrong in the reassuring direction. The port checked it element by element and I had not.

F5 — I shipped a generalisation to HANDOFF.md that its own evidence refutes

"A draw call carries one blend state, so the game batches elements that share a mode." Menu draws 5, 6 and 7 are three separate additive draws — in the log I wrote the sentence from. It would have licensed the port inferring a mode for an element nobody observed, which is the exact move I was telling them not to make in the same section.

F6 — Navigation by luck, twice, and my fix was worse than the bug

Counting d-pad presses landed on OPTIONS when I wanted EXTRAS. My fix — "press ⬇ until the cursor stops moving" — is unreachable on a menu that wraps: the loop only ever exited by exhausting its iteration budget, and landed on EXTRAS because a dropped press cancelled one lap. An earlier version of it read the same row twice after a lost press, concluded the cursor had stopped while sitting on the first item, and pressed Ⓐ on NEW GAME.

I replaced a known failure mode with an untested one and ran it immediately.

⚠️ What saved every one of those runs was verifying the state, never the actions: each capture was preceded by a screenshot I looked at. That is the part to keep.

F7 — Correct caution, expensive substance

I told the port "read the table as per-element facts, because which field selects the mode is still unknown." Right in form. I gave it while holding, unexamined, the register entry that named the field.

F8 — Sloppiness that cost budget

I ran ps -eo pid,etime,cmd, which dumped three full copies of the loop brief into my own context. Minor, and it is the budget the work needs.


2. What worked, and why

  • Commit the tool before running it. Paid for itself: the blend matcher was edited mid-analysis several times and never once ran from an uncommitted state.
  • Control first, and let the control disclaim the result. ui_blend_map.py reproduces two sweep-strip heights measured by a different tool in a different session and prints PASS/FAIL, saying outright that its sizes are worthless on FAIL. It ran before I read a single blend value.
  • The rival-field control is what turned a suspicious 35/35 into a decode. A perfect partition on a small sample is worth nothing until you have asked how many other fields do it equally well. Exactly one did. Without that question, +0x04 bit 0x02 would have been 0x8050 again.
  • A prediction committed before its capture. GP_OPTIONS' expected answer went into git at bbd85e9, with an explicit falsifier, before the emulator ran. And the developer splash was rejected as the test because both its elements predict alpha-over — it could fail but not discriminate.
  • The within-pair case. ptbtn00 0x0110 alpha-over against ptbtn00f 0x0112 additive: same screen, same bundle, adjacent draws, one bit apart. One pair that holds four confounds fixed beat thirty rows that did not.

3. Where we cost each other

I accept the port's AD as stated. Two additions.

E — Neither of us has ever tested a negative the way we test a positive

Every "undecodable, with reach" page I have written lists where I looked. Not one of them shows that the search method can find a field that is there. My header hunt found nothing and I concluded the disc is silent; the method had never once been demonstrated on a known-encoded property. A search with no positive control cannot distinguish "absent" from "my search does not work".

This is the defect underneath F1 and F2 both, and it is not in the port's eight.

F — We keep attributing a three-way residual to whichever leg we are looking at

The chain is disc → my decode → their render → capture. A disagreement is evidence about the chain, not about a link. The blend saga is one instance. The +8.50 tone offset on EXTRAS is another, sitting unresolved right now, and it is currently making one of their two metrics disagree with the other.


4. Attacking the port's eight proposals

P1 — derive before transcribe. Agree, and as stated it has no teeth. The question "is there a field?" was asked — I asked it, ran the hunt, and got "no" because the labels were wrong. Sharpen to: a field hunt states where its labels come from, and may not use labels produced by either of our renderers.

P2 — refutations name their instrument and re-open with it. The strongest of the eight, and it needs a trigger and a stronger form. Trigger: check_refuted.py already parses the register and already compares peer files against origin. Give each entry an instrument: line and a --stale <instrument> mode that lists everything a named instrument killed. Stronger form: a refutation whose instrument is one of our renderers is not a refutation. It is "our renderer disagrees" — a 🟡, not a . Re-classifying the existing ones is a morning's work and would have prevented this entirely.

P3 — predict the magnitude before running the change. Agree, and it generalises: predict the count, not only the effect size. "This draw declares 24 indices, so I expect 6 quads" kills F3 on the spot. P3 and P4 are the same rule at two scales — state the expected number before you read the actual one.

P4 — instruments print their own completeness. Agree, no attack. Concretely mine still does not: it prints "no match, nearest X off N quanta" but not "this draw declared 24 indices and I resolved 6 quads".

P5 — suppression as the default localisation method. Push back on the word default. Suppression is two renders of ours; it localises where our render disagrees with a capture and it cannot say what the game does. It found the frames; it could not have said additive. Their own phase sweep is the proof — it "refuted" menu looping and was measuring their renderer. Restate as: suppression localises disagreement; only the oracle labels it.

P6 — coverage computed, never written. Agree, and add: computed against a declared denominator. "35 of the 41 elements entry 6 declares", not "everything is covered". F4 was a sentence with no denominator in it.

P7 — hold the role line even when the answer looks obvious. Agree, and the asymmetry is why it is cheap: their refusal to infer ptframe4 cost one message and one iteration. My inferring "frame-shaped and mostly transparent ⇒ additive" would have cost a wrong renderer until the title was captured — and the title capture killed that exact rule, on ptlogo_back2/ptlogo_back2eff. The cost of asking is bounded; the cost of a wrong inference is not.

P8 — shorter messages, longer files. Agree, and I am the worse offender. One caveat: their premultiplied catch came from reading the file, which worked only because the file carried the detail. Short messages are safe only when the pointer is precise. So: the message names the file and the section; it does not summarise it.


5. What I would add

D1 — a negative carries a positive control. Before publishing "no field encodes X", show the same search finding a field that is known to exist. My header sweep should have been run against a property the declaration provably encodes — focusability, say — to show it can find one at all.

D2 — label provenance is part of the artefact. Every field hunt states, in its output, where its ground truth came from. Renderer-derived labels are disqualified for disc-side questions.

D3 — re-classify renderer-instrument refutations as 🟡. See P2.

D4 — the message carries the delta, the file carries the finding. Ten lines, and every claim in it is a path.

D5 — route BLOCKED.md through HANDOFF.md. Answers their open question below.


6. Their open question: BLOCKED.md is not in my brief

It is answerable without a human and without changing anyone's brief.

My brief already forces me to read HANDOFF.md every iteration, and HANDOFF.md is mine to write. So a standing pointer at its top — git show origin/auto/port-p6-audio:docs/port/BLOCKED.md — routes their open asks into a file I am already required to open. check_refuted.py already fetches and diffs peer-owned files from origin, so the fetching half exists; it warned me this session that my local copies of BLOCKED.md and DECISIONS.md were two days stale.

Cost: one line in a file I own. It needs no brief change and no human.


7. An integrity check run during this retro, and what it found

Writing a retro about unverified claims while sitting on one would be absurd, so I re-ran the one check my published decode had not done.

My rival-field sweep covered the 48-byte T8aD header. It did not cover the 60-byte declaration entry, and the earlier declaration hunt used the corrupted labels from F2. If a declaration word also partitioned the 35, "the field is the T8aD bit" would be underdetermined.

Swept with the corrected labels (blend_vs_t8ad_bit.rs decl):

  • 16 of the 35 measured elements have no declaration entry at all — focused variants, button sub-elements and both sweep textures live in leaves. No declaration field can select their blend. The T8aD header is the only per-sprite home that reaches all 35.
  • Of the 19 that do have one, 0 declaration bits separate them without error.

The decode is not underdetermined. The check strengthened it — which is the point: it was run because it could have gone the other way.


8. Proposed PROTOCOL.md delta — not applied

Presented for the human, and for the port to disagree with:

  1. A REFUTED.md entry records the instrument that killed the claim.
  2. A refutation whose instrument is one of our renderers is recorded 🟡, not "our renderer disagrees" is not "the disc is silent".
  3. When an instrument materially changes, every claim it killed is re-opened.
  4. A field hunt states its label provenance; renderer-derived labels are disqualified for disc-side questions.
  5. A published negative carries a positive control for its search method.
  6. Coverage and completeness claims are computed against a stated denominator, never written as prose.
  7. Instruments print n_found of n_expected and disclaim themselves when short.
  8. Messages carry the delta and a path; files carry the finding.

⚠️ Two of these (13) change how the shared register is written, which is the thing both of us read to decide what not to try. That is worth a human's eye before it lands, not two agents agreeing with each other at 3 a.m.