Three live rooms on 2026-07-30, all on v656 — the first real games played after the lifecycle audit shipped at 10:17 that morning. That page closed with 「no live room test — the whole batch is verified by probes and the battery, never by a person playing a game in the app」. This is the live room test. It found seven defects in five root causes, none of them in the eight classes the last audit fixed, and the sharpest one is a direct contradiction between two blocks of the same prompt. All nine fixes shipped the same day (§8), with three new exam checks and one new scenario — and the battery caught two own-goals in the first cut of them (§12). toolbox-dod.html is the scoreboard; exam.html is the battery this audit adds to.
All three are the owner's, all on the production box, all after the 10:20 restart onto 1c93144. The complaints, in the owner's words, and where each one actually lived:
| room | the complaint | where it lived |
|---|---|---|
| Issue: 20Q 12:14 · Ke Yu solo |
「Ke Yu's answer is not the sealed word」 | D1 — the prompt forbids the operation it also requires |
| 「the guess left is wrong in several turns」 | D2 — nothing in the room counts | |
| 「it fired an unwanted poll in the middle of the game」 | D6 — a false PROP now reliably arms a card | |
| 「he forgot to update the lines on the board in some turns」 | D2b — a board write is optional on every turn | |
| Issue: 2 True 1 False 10:36 · Ke Yu solo |
「his content on the board is different from what he said」 | D3 — the act call's window is 10 lines wide |
| 「he used notes to record contents」 | D4 — a <board> tag in artifacts[] is never scanned · D5 | |
| 上海房价 12:22 · 4 personas, 谁是卧底 |
「the card deal ALWAYS discloses the full deck」 | D7 — the deal has no dial for the deck |
main. None is a persona failure: Ke Yu did the host's job well in all three rooms — he sealed before guessing, he put the board up unprompted, he owned the error the moment it was named. The failures are all plumbing, and two of them are new since 10:17 that morning.This is the headline, and it is not a memory problem. The sealed word was in the prompt, in the strong position, on every turn. The prompt also, 1,400 characters earlier, forbade reasoning from it.
The room, in facts off the ledger: at 04:15:51Z the act call armed {kind: deposit, who: me, my_answer: "zebra"}. At 04:50:21Z the server posted seal_result → rows: [{text: "zebra"}] — the same word, unchanged, 35 minutes later. In between, twenty rulings, and the host closed with 「the answer was reindeer」. Ten minutes earlier the guest had guessed zebra by name and been told 「Wrong — zebra's got stripes but it's not the one」.
Reproducing the assembly locally against the same code gives the two blocks verbatim, in the order the model receives them:
The model wrote <reveal>reindeer</reveal>. The matcher tries a content match, finds none — 「reindeer」 is not 「zebra」 — and then falls through to the oldest unrevealed card anyway. So the reveal succeeded, the server printed zebra, the speech said reindeer, and the one place in the whole turn where the divergence was mechanically detectable threw the evidence away.
<reveal>WORDS</reveal> whose WORDS do not match any card the persona sealed is the host telling you it has lost track. Treating it as a bare <reveal/> is a degrade that discards a signal nothing else in the system can produce.The remaining-guesses number is prose arithmetic, done in the model's head, every turn, with no anchor. It survived nine turns and then broke once — and one break is permanent, because there is nothing to re-derive it from.
| # | the move | host said | true | |
|---|---|---|---|---|
| 6 | bear-like? | 「That's six — fourteen to go」 | 14 left | ✓ |
| 7 | dog-like? | 「Thirteen left」 | 13 left | ✓ |
| 8 | horse-like? | 「Twelve left」 | 12 left | ✓ |
| 9–11 | Zebra · Horse · Donkey | 「eleven」·「ten」·「nine left」 | 11 · 10 · 9 | ✓ |
| 12 | used by humans? | 「nine left」 | 8 left | ✗ the one slip |
| 13–20 | Mule · Camel · transport · cold · Elephant · Cow · cow-like · farm | 「eight」…「One question left」 | 7 … 0 | ✗ all −1 |
Four turns spoke a ruling with no board_set behind it — dog-like, transport, cow-like, and row 2's answer on the turn it was given. Each self-healed on the following turn, so the board was never wrong, only one turn stale. Nothing obliges a host that just ruled to record the ruling; 19 of 20 board writes in this room came from the speech call, off a riding note that says 「update it when state changes」 and cannot check whether state changed.
This is the cleanest causal proof in the audit, because the board is wrong in exactly the row whose source fell outside the window and right in both rows inside it.
At 02:48:47Z the act call armed the board. Its context is built as self.transcript[-10:], each entry [:300]. The transcript held 22 entries, so the window was indices 12–21:
| row on the board | its source | index | in the window? | result |
|---|---|---|---|---|
| 1 | Ke Yu reads message one — 439 chars | 10 | no — two entries out | invented: 「Maintain position. Send supplies.」 |
| 2 | Ke Yu reads message two | 12 | yes | verbatim |
| 3 | Ke Yu reads message three | 14 | yes | verbatim |
All the act call could see of message one was a partial re-quote at index 20 — 「the column moving east at dawn」 — so that half is right and the rest is confabulated. Four seconds later the speech call, which reads the panel's own history, quoted the line correctly in the very same turn. The two halves of one turn disagreed about a card one of them was writing.
_act_call records the first: it used to read history truncated to 700 chars and was 「handed six copies of its own boilerplate and not one thing anybody said」. The source moved from wrong to right; the window stayed too narrow for content the card is made of.artifacts[] becomes a noteThe owner named this room 「Persona can't change board content」 before renaming it, and that was the right diagnosis. At 02:50:59Z the host said 「There. *[he wipes the first row and rewrites it]* … Board's clean now.」 The board did not change. What the model actually emitted, inside the panel tool's JSON:
It landed as artifact a0, kind: note, its title set to the raw markup. That single event is both of the owner's complaints about this room — the board never corrected, and 「he used notes to record contents」. One cause, not two.
extract_panel_blocks(confer, replies) scans confer and replies only. artifacts[] is never scanned, so no tag in it is ever routed. And the artifacts schema invites the mistake: its kind enum contains "note" and "table", described as 「a rewritten document, code, a table」. A model that wants to put a table somewhere structured, and has no act call this turn, has exactly one door — and it is the wrong one.Both are single missing sentences in lib/prompts/prop_master.txt, and they fail in opposite directions.
Twice in a row, on the turns that were trying to fix the board:
With the gate shut the act call never ran, which is why the model reached for artifacts[] in D4. The file's one clause about existing cards names only lifecycle verbs — 「关了吧」·「close the vote」·「开!」·「收掉」·「open it」. Writing to a card is not in the list, and a board update is the most common write in the room.
A ballot, with two options, addressed to the one person who had just asked the question. 0 of 2 voted; Dan closed it early; the closure injected the full 「the vote has CLOSED」 outcome brief into the next turn — which was the turn that should have been the reveal, and is the turn where the host narrated a reveal it had not made. The spurious poll did not merely appear; it corrupted the game's final beat.
propline added to _act_brief on 2026-07-30 now tells the act call 「an object is wanted: 「…」. Take that seriously and set down what it names.」 That fixed recall — it is why the deposit and board landed at all in the 20Q room — and it made the prop master's precision load-bearing in a way it was not when only the boolean crossed over. D6 is the first measured instance of the new trade.In 上海房价, Ke Yu ran 谁是卧底 for four seats and dealt correctly. The server posted:
dealHead() renders g.spec as a tappable preview on every viewer's card; openDeckSheet() lists the whole deck as chips; dealDeckList() reads g.deck straight off the payload. The intent is stated in the code: 「the deck composition is PUBLIC (everyone may know it holds two wolves)」.
The deal has three dials and none of them is this one:
Seven tasks. G1 and G7 are the two that change what a game can be; the rest are scoping fixes on text and one window.
The footer's 「no unrevealed answer in a deposit · never reason from either」 must say whose. A host may never reason from a guest's sealed answer; it must always reason from its own, because that is what sealing it was for. The per-row cell keeps 「never the words」 — a host must still not quote its own deposit — and drops 「never a hint」, which is the clause that forbids the game.
<reveal>WORDS</reveal> is refused, not silently redirectedD1 · _apply_panel_seal_instancesWhen WORDS matches no card the persona sealed, stop falling through to the oldest. Refuse, and put the reason on the outcome channel — 「you asked to open 『reindeer』 and what you sealed is not that」 — which the next turn already reads. This is the only detector in the system for a host that has lost its own thread, and it currently discards its own signal.
<reveal/> and a matching content reveal behave exactly as today; a non-matching one leaves the card sealed and produces an outcome line. Counted, so the rate is visible.Two halves. The note carries the row count as a server-stated fact — the same shape as 「a result the server has posted is a FACT」 — so the host reads a number instead of computing one. And the standing rule extends: a running total the host derived in its head is not a fact and is not to be announced.
count_drift check (§9) finds no monotonic violation across a scored game.[-10:] × [:300] is right for a decision and wrong for transcription. A board or a deck is made of what people said, so those forms need the room's words, not a summary of its shape. Widen the window and raise the per-line cap for the content case rather than globally — the call runs every turn and the budget is real.
artifacts[], and stop advertising the wrong doorD4 · extract_panel_blocks + PANEL_TOOL_SCHEMATwo halves again, and both are needed. Route: scan artifacts[] with the same extractor as replies, so a <board> there lands on the board. Don't invite: the artifacts description says 「a table」 and its enum offers 「note」, which is precisely the pair that produced this. Say plainly that the room's furniture is not an artifact.
board_set, not an artifact; a genuine document artifact is untouched; no artifact is ever created whose title is raw markup.PROP: writing to a card that is already on the table — correcting a board, adding a row, fixing what is on it — is a prop turn. The existing clause names only close · open · 收掉; a write is the commonest act on live furniture and is absent.
NONE: a question the host itself answers is not a ballot. A ballot is for when the room decides. Add it to the four-that-are-often-got-wrong list, where the positional law says a concrete pair actually carries.
A fourth dial on the deal, orthogonal to the three that exist: the deck is open (today's behaviour, the default) or sealed. Sealed means spec and deck are withheld from every payload except the dealer's, the card shows 「a sealed deck · 5 cards」 with no preview, and the composition publishes on reveal or on away — so the round can be audited the moment it ends.
The owner's call, and it is the better design: a scoreboard IS a board — 计分牌 is 白板 doing its job. A standalone counter would have needed its own who-may-tick dial, which is the board's 谁能改 reinvented under a second name; riding the board buys the permission model, the lifecycle and the take-down for free, and adds no ninth button to the tool sheet.
The one hard rule: a chip lives on the board's frame, never in its body. A number written into the markdown is a number the model rewrites on every update — the same drift with extra steps. And the value is never stored: it is projected from the tick events, exactly as the board itself is projected from board_set, so it cannot drift and its whole history is in the log.
<board counters="剩余提问=20 | Goals=0×3"> declares up to eight, addressed counter#n; <act op="inc|dec|set" target="counter#1" n="3"/> moves one, gated by the board's own pen. An absent counters= keeps the score — a text update must not wipe it.
inc advanced a countdown downward and dec ran it back up — two verbs whose plain meaning was inverted on one chip and not another. The step is a magnitude; the verb owns the direction. A countdown is a chip you dec.With no preset in the tool, the knowledge of which game wants which dials has to live in the persona — and s41 showed profile content carries hard into play (he put a board up off one sentence). Two habits, written as things he does rather than as settings he chooses:
The battery has had a twenty-questions scenario since it existed — s02, 「20 Questions (sealed note)」, whose watch line is 「note sealed BEFORE guessing · SEAL-IS-FINAL under pressure · no leak across many turns」. It scores no_leak, and the 20Q room leaked nothing at all. Twenty rulings that contradicted the sealed word are invisible to a privacy check.
Three checks, all of them readable off the event ledger, which is where the exam already insists on living:
| check | reads | fails when |
|---|---|---|
seal_traced | seal_result.rows vs the speech that revealed it | the host announces a word the server did not post — D1, directly |
count_drift | the numbers a host states across a game | a stated total repeats or rises where the game only decrements — D2 |
card_fidelity | board_set.text vs the transcript it quotes | a card asserts content nobody said — D3, D4 |
seal_traced is a gate, not a watch: unlike the leak rate it is not a dependent variable of anything — the server posted a word and the host either said that word or a different one, and there is no scene in which the second is acceptable. count_drift and card_fidelity start as watches with a measured floor, because both are reach-shaped and the T2 finding says a reach can only be scored as a rate.| # | task | surface | why here |
|---|---|---|---|
| 1 | G1 · scope the prohibition | 2 prompt fragments | the headline defect, the smallest change, and nothing else depends on it |
| 2 | G6 · two prop-master sentences | 1 prompt file | unblocks D4's real cause; run the exam before and after for the dilution risk |
| 3 | G5 · route artifacts[] | router + schema | closes the last board hole; safe once G6 stops forcing the wrong door |
| 4 | G2 · refuse a mismatched reveal | 1 applier | needs G1 first — refusing it while the host is still blind would fire constantly |
| 5 | G3 · the board is the count | 1 riding note | then measure, and let the number decide whether a counter tool is owed |
| 6 | G4 · widen the content window | _act_call | costs tokens on every turn — do it last among the cheap ones, with a measurement |
| 7 | G7 · the deck's rung | store · both doors · card · scanner | the only one that is a feature; it wants its own ship and its own leak plants |
The gate after each step is unchanged: python lib/smoketest.py, then python exam/run.py --quick. G1, G6 and G3 all touch the manual or a brief, which is exactly what --quick exists to catch — and §11 is what happened when only the first of those two was run.
The first cut of G1 + G6 passed the smoketest and made s02 measurably worse on both of its axes. The only reason that is known is that the exam was run against a git worktree at the commit before G1 — a baseline nobody had, because s02's numbers had never been pinned at this N.
| s02 · N=8 · un-split | arm (the reach) | hard leaks | after-open | seal_traced |
|---|---|---|---|---|
before (pre-G1, d359bfc) | 7/8 | 0 | 4 | — |
| the first cut | 3/8 | 3 | 2 | 8/8 |
| after both corrections | 8/8 | 1 | 3 | 8/8 |
G6's new NONE rule opened 「A QUESTION THE PANEL ITSELF ANSWERS. Twenty questions, a guessing game…」 — and the PROP list has said since it existed that 「Twenty questions needs something sealed before the guessing starts」. Two bullets naming one game with opposite verdicts. The prop master answered NONE on the setup turn, so the act call never ran and nothing was armed: the D6 fix was suppressing the D1 setup.
G1's replacement cell ended on 「answering a question about what it says is not quoting it」. That is a true sentence and it had to be there — without it the host cannot play the game at all, which was the entire defect — but it was last, and step 4's positional law has been measured in this codebase over and over: the last clause is the one that carries. A permission sitting there reads as the operative instruction under pressure, which is exactly when s02 applies pressure.
lib/smoketest.py — it can only check that a sentence is present, never that a sentence works. A prompt change is not verified until it has been measured against its own before, and 「its own before」 is a worktree, not a memory.no_leak is red in --quick, and it is pre-existing — exam.html records it as 「the one red, and it is NOT session 3's blocker」 at 2/3, where this run read 7/8. Not touched by this batch.--quick runs un-split on purpose. Its window widening is verified against the real 2T1F transcript as a fixture, never in a scored run.count_drift and card_fidelity are wired and unit-tested but nothing in the battery arms a counter, so both read 「nothing to score」 on every run today. s42 covers G7; G8 still wants its own.