Opened 2026-08-07 on the owner's report: 「we've made lots of changes in order to make tools and kits work — the system is not as stable as before the kits era」. Twelve symptoms were named; this page is the ledger for all of them plus whatever the sweep turns up. One case per row, each carrying the symptom as reported, the root cause once proved, and the fix. A case is not closed on a plausible story — it closes on a reproduction that goes red and then green.
| # | Symptom, as reported | Status | Root cause |
|---|---|---|---|
| 1 | Can't dismount cleanly — the kit's invoked tools are left on the table | fixed | A dismount cleared kit and nothing else — every OPEN instrument stayed live (§A) |
| 2 | Can't dismount cleanly — still in kit mode; the persona thinks the kit session is still running | fixed | Mounting a kit OVER another never handed over: the old engine stayed armed (§B) |
| 3 | Bubble flakiness — after a refresh, bubbles are gone | fixed | Replay is faithful; the one real loss was the record wipe at first open (§F) |
| 4 | Ghost thinking bubble — appears and never lands as a message | fixed | The egg was raised on four paths the drain then refuses (§C) |
| 5 | Uninvited thinking bubbles | fixed | Same seam as §C — the device raised an egg over a blocked drain |
| 6 | Missing thinking bubbles | fixed | The utility kit's turn returned before the only line that raises it (§D) |
| 7 | No response of any kind to a user input the room wasn't designed for | fixed | Two seams: §C's silent refusal and §E's front-door loop |
| 8 | Dead loop | fixed | The interrogation's confirm gate was an exact-match word list (§E) |
| 9 | Token bleeding | fixed | §E spent a model call per lap; and the whole engine was outside the call fuse (§I) |
| 10 | The board doesn't auto-refresh when its content updates | explained | The board does refresh live — but §E meant it had no content to update (§G) |
| 11 | Tools are shared across kits — find any kit-specific ones; unify the visual language | fixed | No kit-specific TOOLS — but two kit-local NAMES rendered no glyph at all (§H) |
| 12 | Unstable bubbles — refresh the browser and the bubbles disappear | fixed | Same as #3 |
| 13 | found in the sweep — a kit mounted before the room's first open is erased from the record | fixed | Room.open() called events.clear() (§F) |
| 14 | found in the sweep — the interrogation's model calls answer to no meter | fixed | Its pump ran outside the call fuse — the app's one hard stop (§I) |
| 15 | found in the sweep — a handover left the old kit's ballot standing, freezing the new kit's engine | fixed | A gap in §B's own first cut — the sweep runs on a replace too |
| 16 | found in the sweep — one line after a dismount bought three panel turns | fixed | The take-off was the one kit route that never scanned the wake door (§J) |
| 17 | found in the sweep — a provider retry, the presence gate's own note and a private card look each bought a panel turn | fixed | The wake door's catch-all arm was a default, not a decision (§K) |
| 18 | found answering the owner's question — mental 大话骰: a cartridge below its player floor mounted anyway and the persona could only narrate the game | fixed | A「classic」tier the design does not have, reachable by any solo player (§L) |
| 19 | the owner's design, not a defect — THE FREEZE: the open world stops while the mini-game runs | built | A kit shuts the drawer, suspends what is standing, and hands it back untouched (§M) |
| 20 | the owner's question, not a defect — should the two engines be merged behind one abstraction? | answered | Merge the lifecycle and the contract; never the language — the narrowness IS the guarantee (§N) |
| 21 | found by the live run — a dismount mid-turn was undone by the turn: the engine and its board came straight back | fixed | An unconditional state write-back at the end of an off-lock turn (§O) |
| 22 | found by the live run — the freeze was invisible to the client: a suspended card looked live and a paused clock kept counting to zero | fixed | frozen reached neither the gate payload nor the clock's (§O) |
| 23 | found by the sim's first run — one answer woke every seat, and a twelve-answer sitting ended at one | fixed | The router was asked for thread ids, and the world obeyed all of them (§P) |
| § | The fix | Where |
|---|---|---|
| A | A dismount sweeps the table: every open instrument is called off, the clock comes down, the board clears, an undealt hand goes back in the box. Every capsule and settled card stays. One door for all four take-off paths. | _kit_sweep_table |
| B | A replace is a handover: whatever engine is running stops — through the lawful abort, so an aborted game still publishes its pile — before the next kit starts. | _kit_engines_off |
| C | The egg has a door. turn_will_run() asks the drain's own refusals BEFORE the promise is made, and the worker lowers the egg on every silent refusal — with mute:false, so it doesn't gag the next turn. | turn_will_run · _worker · the egg handler |
| D | The interrogation raises the egg for the whole room, not just for whoever typed. | the probe branch of drain_turn |
| E | The front door reads meaning, not equality — a line that agrees and proposes no change is a go — and two brakes make a loop impossible: an unchanged re-reading starts the run, and so does the third read-back. | _is_go · _on_readback |
| F | The first open drops the prior opening only. Everything else recorded before it is somebody's record and stays. | _drop_prior_open |
| H | One object, one name: the client resolves synonyms so an alias can never render blank, the three kit files are corrected to the canonical names, and a lint over every kit on disk keeps them there. | TOOL_ALIAS · the kit files · the smoketest |
| L | A cartridge below its player floor is REFUSED at the door — no「classic」tier to fall into — and the shelf greys the row with the reason before the tap. One copy of the floor arithmetic serves the door, the device and the shelf. | kit_floor_miss · kitFloorWhy |
| P | One message wakes exactly one seat, however many the router names — and the sim harness stopped reading half a conversation, returning mid-think, and accepting zero as「exactly one」. | _on_route · exam/live_sim.py |
| O | The lifecycle driven over the REAL routes for every kit — 64 checks. frozen now reaches the client on both payloads, and a turn landing after a dismount no longer resurrects the engine it was holding. | exam/kit_lifecycle_live.py · _life_payload · clock_state · _probe_emit |
| N | The five axes as one contract both engines are driven through — plus engine(), the「is any engine running?」question nothing could ask before. Merge the lifecycle, never the language. | exam/kit_contract.py · engine() |
| M | The open world freezes while a kit plays: the drawer shuts, empty cards clear, everything holding a commitment is suspended and handed back with its deadline moved forward by the time it was held. Plus a 24h self-sweep for empty cards. | freeze_table · thaw_table · stale_card_sweep · _frozen_stop |
| I | The interrogation's pump answers to the call fuse like every other model call the room makes. Refused → it stops, leaves the call pending, and resumes when a human acts. | _probe_pump |
Sixteen pins in lib/smoketest.py hold all of it. Each was watched go red against the old code before being called done — including the §H lint, whose first cut passed vacuously because it scoped itself to the shelf and both offending kits were withdrawn.
The owner's question, 2026-08-08: 「Before, a world event triggers a wake — human message, tool mount, tool state change, a long silence — the general form is still tic-toc. After, in kit mode the device takes over the wake?」 Close, with one thing inverted, and the inversion is the whole reason this page exists.
| Rung | What is on disk | Who owns the turn | What can wake the room |
|---|---|---|---|
| no kit | — | the persona | a human · the world door |
a cartridge```flowevery play kit | a rulebook + a program | the server deals, counts the ballot, applies the elimination, checks the END. The persona is asked only for moments — facts and obligations, never wording | + its own: dead-man timers, a settle, 「everyone has described」, after-speech |
a utility kit```probe | a rulebook + an engine | the engine runs everything — which seat speaks, what it is asked — and makes its own model calls. The whole ordinary turn(prop master, floor producer, act call, tool manual, dispatch)is OFF | + its own beats |
So the device never「takes over the wake」. It becomes one more source pushing into the same queue, beside humans and the world door — not a replacement for either.
The toolbox(v635–v642)introduced the first self-wakes, but each was a one-shot tied to an object a person had placed: a clock rings once. The kits era turned self-waking from an EVENT into a PROCESS — a cartridge advances on its own until the game ends, and a utility kit runs a whole sitting.
This was already felt once. The commits of 2026-08-05 are literally feat(bleed): the presence gate — the room stops talking when nobody is there and fix(bleed): a ruling is not a pulse, and the fuse must outlive the process. Three brakes exist for exactly this reason:
Taking a kit off the table clears self.kit, aborts a running cartridge and (since 2026-08-07) clears the interrogation's engine. It does not touch the instruments the kit armed. An open ballot, an open roll, an open deposit box and a running clock all survive the dismount — pinned to the top bar, still counting, still holding their dead-man timers.
13 rooms in rooms-dev/ carry a dismounted kit and a still-open instrument. Three sampled: a roll and a deal (…073015-37d3), a roll (…085022-2a5d), a deposit (…064601-a727).
An open ballot with blocking=host makes _blocking_open() true forever, and that predicate silently refuses every drain (§C). So a leftover ballot from a kit nobody is playing any more can mute the room permanently — and its lifecycle timer keeps waking the room to narrate a game that is over.
_unload_kit_core's own comment says a play kit's objects「are the room's objects now, and a rulebook leaving is not a reason to sweep the table」. That reading produced the report. An open instrument is not an object — it is an unfinished obligation. The fix sweeps what is still OPEN and leaves every stream record standing.
load_kit on a table that already holds a kit sets self.kit to the new slug and clears the standing offer. It never clears self.probe (the interrogation engine) or self.game (the device). Both survive into the new kit's life.
The interrogation's intercept sits at the top of the drain and keys on probe.on alone, with no check that its kit is still on the table. So every line goes to an engine whose rulebook left, while the room shows a different game — which is exactly「the persona thinks that kit session is still running」.
The device's kick raises the egg for everyone whenever a turn is queued. The drain then has four gates that return nothing — no reply, no error, no egg-down — and the worker treats that as「queue empty」and goes back to sleep:
not self.openednot self.cast — the last persona retired_blocking_open() — a host-blocking ballot is up (§A makes this permanent)quiet_room() — the presence gate dropped a world beatThe egg breathes forever. Seen live at 127 seconds on a mounted room whose beat the drain had refused.
Why it is #4, #5 and #7 at onceGhost (it never lands) · uninvited (nobody asked for that beat) · and no-response (the user's own line is the thing being silently refused).
The one call that publishes responding inside a turn sits far below the utility kit's intercept, so an interrogation turn never publishes it. The person who typed sees an egg only because their own client raises one optimistically; every other member of the room sees nothing at all while the engine makes its calls.
The chair reads your claim back and waits. The confirm gate matches your reply against an exact whole-string list of 18 short words. Anything else is filed as a correction — which re-reads the same claim back and returns to the same gate.
Evidence — a live room, two turns"yes" passes. "Yes, that is what I mean." does not. "对" passes; "对,就是这个意思" does not. There is no loop counter and no escape: the run cannot start until the pitcher happens to type one of eighteen bare tokens, and every lap costs a model call. This is the dead loop and the token bleed in one seam.
Room.open() calls self.events.clear() before writing its own opening event. Anything recorded between a room being created and its first open is destroyed — proved by mounting a kit on a freshly-created room: the kit_load record was gone from the log while kit and probe survived in state, so the room ran the interrogation with no capsule saying it had ever been mounted. This is the one real instance of「bubbles gone after refresh」the sweep found: the bubble was live, the record was not.
Replay itself is faithful. Across three real interrogation rooms every recorded line comes back: 45/45, 38/38, 19/19 persona bubbles and 19/19, 24/24, 13/13 human lines. A live room re-rendered identically before and after a browser refresh (47 nodes, same tally). The renderer handles every turn kind replay can emit.
Checked, then re-checked after §E was fixed. With the front door unblocked the run reached its first sweep, and the board went from its setup row to two live rows in the same turn, over SSE, with no refresh — each row carrying its seat, its colour, its anchor into the transcript and its move stamp:
The reading: the board was not failing to refresh — it had nothing to refresh to. §E held every interrogation at the confirm gate, so the only board the owner ever saw was the setup row, re-written identically on every lap. A stuck board and a stuck run look the same from the outside. Left open on the ledger as explained rather than fixed: if it recurs on a room whose run is demonstrably moving, it is a different defect and deserves its own case.
The standing law is written on the react route: every route that records a world event scans the wake door and, if it cued, wakes. Mounting by the menu did it. Mounting by the offer card did it. Taking a kit off did not. So the kit leaving, the board coming down and every card being called off all sat unread in the ledger — until the next human line, where they cued off the back of it, and that cue's own turn cued again.
Measured live, beforeBetter on both counts: it costs less, and the room remarks on the dismount when it happens rather than thirty seconds later, attached to a question about the weather.
The owner asked for live sims across all five kits. 诘问 — the newest engine, and the one every defect on this page came out of — had never had one. Its first run found this in ninety seconds.
The law, written at the top of its own moduleThe code contradicted it. An untargeted line goes through a route call whose schema asked for 「the thread ids this message actually answers」 — plural — and the handler armed a ruling for every one.
One message collapsed the entire run. The world takes the first match and only it now; a seat with more to ask presses on its own next beat, which is the design and costs nothing. The route's ask says what the world will do, because asking a mind for a list the world discards is how the two halves drift apart.
transcript() dropped the human half of every conversation(it expected a dict, the field is a string), so the UX judge has always read one-sided; wait_quiet returned mid-think, so every「what landed after I spoke」check read an empty window; and this very check was written <= 1, which zero satisfies. Fixed in that order — and only then did the law violation become visible.「Have you run some sims to test if now all 5 kits work?」 The honest answer was no, and the gap was specific: every change of this sweep lands on the mount/dismount path, and the dry runs go straight to the Room object without touching a route. 590-odd scripted checks were green with not one cartridge ever having been mounted live.
exam/kit_lifecycle_live.py is that gap closed — the lifecycle driven over the same HTTP routes a phone hits, on the dev server, with three real humans in the room, for every kit on the shelf. 64 checks. It found two defects in code shipped hours earlier, and neither was reachable from a unit test.
The server refused every door correctly — and said nothing about it on the wire. frozen reached neither gate_payload nor clock_state, so a suspended card arrived looking exactly like a live one, and a paused clock went on counting down to a deadline that would never fire, hit zero, and sat there. The server was right and the screen lied.
Both now ride the one dict every payload already spreads — the same seam the card's address uses, so no kind can reach a client without it and no future kind can forget it. Three unit tests missed this because they read the room's own dict rather than the payload a client receives.
2 · A dismount mid-turn was undone by the turnA probe turn runs off-lock because it makes its own model calls, so a dismount can land in the middle of one. The take-off cleared the engine; the turn's unconditional write-back then put it straight back from the run object it was already holding.
The owner, having settled that both families are one animal: 「Do you recommend merging them with high abstraction, so we have less class and fix on fix all?」 The measurement answers it:
Merge the lifecycle. Do not merge the language. A cartridge's safety comes entirely from its vocabulary being closed — the validator can say 「did=X」 is not one of DID. Union the two and you get ~81 terms of which each kit uses a quarter, and the validator can no longer reject anything. The narrowness is the guarantee: a higher abstraction over the flow would destroy the very thing it was meant to spread.
| Layer | Verdict | Why |
|---|---|---|
| the lifecycle | merge — done | mount · dismount · freeze · thaw · floor · fuse · presence · wake · exits. 11 shared functions |
| the predicate | merge — done | engine() / engines_running(): the question「is ANY engine running?」that no caller and no test could ask before — and whose absence was §B |
| the contract | merge — done | exam/kit_contract.py: the five axes, both engines, one file, gated in the smoketest |
| the flow vocabulary | no | 2 shared terms out of 81; the union cannot be validated |
| the execution | no | the interrogation makes 2–12 direct calls to produce one turn's questions; a cartridge queues moments through the ordinary drain. Forcing one onto the other is slower and dearer |
| Axis | How each engine is held to it | |
|---|---|---|
| 0 | one table, one engine | mount, hand over, hand back, take off — engines_running() is never longer than one |
| 1 | constrained | cartridge: phases declared, and a missing one is refused · utility: every front door terminates even when the human never types a bare「yes」 |
| 2 | exit | each has its own END; the four house exits are proved present and proved to funnel through one take-off |
| 3 | code math | a mind answering nonsense(budget 9999, spend −5, verdict「banana」)cannot move a counter or enter the closed vocabulary |
| 4 | surface | both put state on the board — a mini-game with no HUD is played blind |
| 5 | never hangs | a mind answering nothing still settles; and a wait with no dead man is a schema error |
clear: True, so the run went straight to the sweep and the phase that loops was never entered. A harness that cannot reach the failure is not a harness, and that is the third time this sweep a check has passed vacuously(the shelf-scoped tool lint, the roster-wiped drawer check, and this).「Our app is like an open-world ARPG — the user can talk to any persona at any time, no restrictions, no order required. When we enter kit mode it's like starting a mini-game inside it: the player walked into a pub and started a gamble game with the owner. Now the flow is taken over by that mini-game, it's clear what can be said and done at what moment, and there's a clear exit condition. The other states in the open world freeze until the linear gameplay ends.」
Scored against the code, before| The spec | Before | |
|---|---|---|
| a clear exit | ✓ held | five doors, all through one place; two of them the kit's own |
| clear what can be said | ◐ partly | held by a utility kit; deliberately loose on a cartridge — a board game at a party doesn't stop the table talking |
| clear what can be done | ✗ not at all | the drawer never narrowed. Mid-谁是卧底 a player could arm their own ballot, roll their own dice, deal their own cards |
kit_idle_sweep's sibling, judged on the card's own age. A card holding somebody's answer is never swept by a clock: it has its own trigger, and that trigger is a promise._life_arm's own rule is that a deadline already past fires on a one-second fuse, so the instant the kit came off every frozen card would fire at once. Three parts, and the third is the one that is easy to forget:Not from a symptom. The owner read the mental-model section above and said: 「in my design there is no kit played from persona's memory. All kits are in the form of a cartridge. There should be no mental liar's dice (flaky). I don't know where you got the impression that there are mental kits.」 The shelf backs that up completely — every live play kit carries a flow block:
_game_boot declines a cartridge whose player floor the room misses, and the kit mounted anyway — into what the code called「the second tier, the classic path, whole」. What that state actually is: the rulebook sits in the persona's context, _flow_kit() stays true so the act call is gated OFF, and the device is not running. The persona cannot deal a real card or open a real ballot. It can only describe a game. That is mental 大话骰 exactly — not a broken game, a narrated one.
The floor counts HUMANS(the device arms and narrates through a persona seat, so a persona hosts and never plays). Measured across the shelf:
One person playing with AI personas — surely the commonest way this app is used — could never get anything else out of those two kits.
And the guard built for exactly this was switched offThe hand-build suppressor exists to stop a persona laying a shelf game's table by hand. Its condition is not (_sup and not self.kit) — it only fires when no kit is mounted. With a declined cartridge on the table self.kit is set, so it returned immediately. The only thing left standing between the room and an improvised game was a clause in the mount cue(「this game cannot run here, so say so plainly」)— and this project's own ledger has the rule for that: a clause is a request, not a guarantee.
「needs 3+ people · 1 here」, with the reason in coral because that is the whole question a greyed row raises.kit_floor_miss)for all three readers: the door, the device's own boot(which still runs it — a room can lose a player between a mount and a rematch), and the shelf.「under the floor the device declines and hands the kit back to the classic path」 and 「two seats → the cartridge DECLINES and the kit runs classic」 both pinned the behaviour the design forbids. A third — the wake door's kit-landing cue — had been reaching that path for a different reason: its fixture seated one human against a three-player cartridge, so it had only ever tested the declined case. All three are rewritten against the real rule, and the assertions came out stronger.The wake door sorts every event the room records into three piles: silent, covered elsewhere, or cue the panel. Cueing means a whole turn. Sixteen event types have bespoke words written for them; everything else falls through to one line:
— an internal identifier, handed to a model, bought with a turn. That arm is right for a real world change nobody has written words for yet. It was catching eight things that are not world changes at all:
| Event | What it actually is |
|---|---|
model_empty | the provider returned nothing and the room retried |
room_quiet | the presence gate's own note — and it fires precisely when nobody is there to read the turn it was buying |
promise_check | the prop master's internal flash question |
ghost_who | the harness dropping a line from a non-cast speaker |
deal_seen | one person privately looked at their own card — cueing it also says out loud that they did |
dispatch · artifact | both have their own card and ride the turn that produced them |
setting_change | an archive, a trash, a rename — not table news |
Those eight are silent now. But the real repair is that the catch-all is a ratchet rather than a default: the types that still cue through it are pinned by name(26 at the sweep; 23 since 2026-08-23, when join · leave · member_leave were ruled COVERED — a newcomer walks in without a greeting and rides the next turn's capsule; room 8db0 had bought a second turn for an arrival already played), so a new event type joining them fails the smoketest until somebody decides which pile it belongs in. A default that silently spends money is how all eight got there.
The call fuse is the app's hard stop — one meter over every model call a room makes, reset by any human action, and its own comment claims it 「makes runaway impossible, including runaways from bugs not yet written」. The interrogation's pump was outside it: up to twelve calls a turn, each with a retry, on the newest engine in the app, counted by nothing.
That is why §E reached a person at all. A front door that loops is a bug; a front door that loops and is not on the meter is a bill. The pump now spends through fuse_spend like every other call — refused, it stops, leaves the call pending, says nothing, and resumes the moment somebody acts.
The answer to the question as asked: every kit draws from the same toolbox. There is no kit-private instrument anywhere — not one. What breaks the muscle memory is vocabulary, and the cause is that the app carries three names for the same nine things:
| Vocabulary | Where it lives | calls the bid box | calls the wheel |
|---|---|---|---|
| the canon | USER_TOOLS · INSTRUMENT_ALIASES · and the drawer the user reads | deposit | spinner |
| the client's icon table | TOOL_ICONS — two legacy keys | seal | wheel |
| the kit files | whatever each author typed | both, inconsistently | |
What actually broke: a kit naming the canon word got no glyph and the wrong accent, because the icon table had never heard of it. Measured in the live app before the fix: toolIcon('deposit') and toolIcon('spinner') both returned "" and fell back to coral, while toolIcon('seal') and toolIcon('wheel') returned their SVGs in blue and plum. So the same object wore a different face depending on which kit put it on the table — the exact opposite of muscle memory.
The canon is the word on the screen. Muscle memory is built on what a person reads off the drawer, not on an internal key — so the kit sheets speak deposit and spinner, and the client's TOOL_ALIAS makes every spelling resolve to the one drawing. Two lints hold it: one that every kit on disk names a canon object, and one that every canon object resolves to a glyph — the half that was actually broken, and the half a name-only lint would have missed.
TOOL_ICONS keeps seal and wheel as its keys, bridged rather than renamed. Renaming them touches every call site for no user-visible gain, and the lint now makes a blind name impossible — but it is one canon too many, and it is what let this drift in the first place. Worth a deliberate pass, not a bug fix.