Skip to content

The picker names conversations and their runs, the ledger keeps rows only for the groups the reader is on, the body is sent as bytes, and the caches that hold them are bounded - #70

Open
avatasia wants to merge 94 commits into
MiniMax-AI:mainfrom
avatasia:add-mmc-trajectory-session-picker
Open

avatasia wants to merge 94 commits into
MiniMax-AI:mainfrom
avatasia:add-mmc-trajectory-session-picker

Conversation

@avatasia

@avatasia avatasia commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

The picker names conversations and their runs. This PR makes every name on the wire name a
session instead of a path, so two conversations that share a basename cannot share anything, and a
recorded path is checked against the session it claims to be. It makes the picker build its list
from the groups the reader is on, so the ledger holds rows only for those groups. And it makes the
service hold the rendered body as bytes and send it in slices, so the body is not one string.

Two caches are bounded here as well: the service's session cache is capped by entry count, and the
parsed-rows cache has a byte budget, a sweeper and a charge per source byte, so a conversation the
reader walked away from is released without a request. And the service refuses to write its derived
artifacts inside the package.

How it works

  • One id per session. A session directory is published under one id, and every parent on the
    wire names that id. A path counts as a session's path only when the directory's name and its
    manifest agree.
  • Two selects in a cascade. A run never enters the conversation select, and the list never
    moves backwards.
  • The ledger holds what the reader is on. The rows of the groups the reader is not on are
    released. Derived artifacts go under the runtime's own data directory, and the service refuses a
    data directory that resolves inside the package.
  • The body is bytes. The rendered body is kept as bytes and sent in slices that respect a
    budget; a position read says which record of the body it means, and a cached answer says which
    answer it is holding.
  • The cache is charged. The parsed-rows cache holds parsed rows under a byte budget, releases
    them on a sweeper that runs on the interval the cache itself reports, and charges for what it
    holds by the source bytes it read.

Evidence

Measured on one machine, each side run twice in the order before/after/after/before so a drift in
the machine's state could not be read as a difference:

  • The page, 2026-10-22, one live session of about 50,600 records. Every figure below is the
    reading taken after the scroll to the middle of the document, one reading from each of the four
    runs; each run read three positions and only that one is quoted here. With every group's rows
    held: 463,122 and 463,356 DOM nodes, renderer's private memory 785–820 MB. With the rows of the
    groups the reader is not on released: 14,801 nodes on both runs, 199–268 MB. The node count is
    the reading that repeats; the memory reading moved by 69 MB between two runs of the same build,
    so it is a range and not a threshold. The node count itself follows how many groups are live —
    10,243, 14,801 and 15,172 in the three positions of one run.
  • The same state on the same session two days later, 2026-10-24: that session had grown from about
    50,600 records to 68,968, and with the reader's groups holding their rows and the rest released
    (340 groups) it measured 12,349 DOM nodes and 187.4 MB of renderer private memory at the middle
    of the document with 3 live groups, and 17,216 nodes and 189.3 MB at 75% down with 4 live groups.
    That is the released side on its own — there is no held-rows counterpart for that day — so it is
    not a before/after of the pair above but sits beside it. The node count follows how many groups
    are live, as the reading above says.
  • The service process on its own, 2026-10-22 21:17, the same session (51,746 records in 308
    turns, a 48,276,400-byte body): 73.8 MB resident and 7.5 MB of heap before anything was read;
    after the body had been read once and dropped it retained 273.7 MB of heap and 1,006 MB resident.
  • The body as bytes, 2026-10-23, same script and machine, the same session, by then 57,420
    records in 317 turns after the change and 57,436 records before it: one session measured twice,
    minutes apart, while it was still being written to, so the process's high-water mark fell from
    1,548.0 MB to 1,396.8 MB, the heap it retained after the body had been read once and dropped
    fell from 302.7 MB to 202.4 MB, and the share of that heap held by the rendered body fell from
    106.3 MB to 5.9 MB — the double-byte string is gone.
  • The service on the installed build, 2026-10-24, one live session of 67,996 records in 328
    turns (a 63.4 MB body), read once and then left alone.
    The heap read 215.7 MB right after the
    read and 215.6 MB at every sample over the next ten minutes; at the first sample past the
    600-second limit — 750 seconds in, with no request in between — it read 24.0 MB. The rows were
    released by the sweeper's own tick, and nothing had to arrive to release them.

These are measurements of one session at a time on one machine and are not reproducible elsewhere;
no check in the package asserts a number.

Review this

Useful feedback: a read of miniapp/node/server.mjs (the caches, the body slicing) and of
miniapp/client/index.html (the picker, the ledger). The two caches are pinned by
tests/verify-parsed-rows.mjs (95 assertions), tests/mutate-parsed-rows.py (36 cases, each
caught by name) and tests/mutate-parsed-rows-owner-escapes.py (6 cases, each caught by name).

Known weaknesses:

  • The service does not yet declare the boundary at which it releases rows, so changing the shipped
    limit constant is not something the suite objects to.
  • The parsed-rows cache still holds a whole conversation's rows while the reader is on it; the
    limit bounds the bytes, not the number of conversations.

Disclosures

  • The package used to assert a number that a comment stated, by reading the number out of the
    sentence and comparing it with the constant the service declares. That contract is gone from
    this PR: the comments that used to state those numbers now state the rule and name the
    constants that hold it, and the nineteen assertions that held the number went with it. Every
    assertion about behaviour stays, and the guard still reports a failure to set up its own cache
    by name instead of as a stack trace.
  • The comments this pull request rewrote no longer restate a constant's value: each of them
    now says what the code does instead. The two poll intervals and the page step among them are
    named where the comment used to carry the number. The comments that still spell out a
    constant's value — in words, or in another unit — were left as they are.
  • The ledger's own comment no longer states a measured session. It keeps the rule — what the
    reader gives up is the DOM, and how many rows the page holds follows how many groups are live —
    and leaves out the numbers, which were one session's reading.
  • The frozen control of the cache's own behaviour therefore holds 36 cases instead of 40. The four
    that went were cases whose break was a change of a number stated in a comment, and that comment
    no longer states one; the rest of the control, and every case in it, is unchanged.
  • Version 1.2.0 → 1.3.0 in the manifest and both READMEs.
  • The contract that was dropped is answered item by item in a review comment on this pull request:
    each item names what was dropped, why, and what covers the behaviour now.
  • This PR changes how the plugin uses the host's session store, and adds two columns to the
    session lookup. The id a directory's manifest.json carries is now checked against the expected
    session id before the directory is read (server.mjs:1218-1221, 1294-1320, 1674-1698); the
    picker's sub-agent layer is derived from the parent link the lookup already selected
    (server.mjs:1860-1876); and the lookup now also selects purpose and workspace_dir
    (server.mjs:1484-1485), two columns it did not select before. Reading the host's
    session storage directly is not something a plugin should rely on long-term: when a supported
    way to obtain this data exists, this should move to it.

… still in use

The list was built from the mtime of the transcript file, which answers
"when did this session last write a message". What a reader of a picker
needs answered is "is this session still in use", and for a conversation
that was just resumed the two differ until the first new message lands.
Until then the conversation the reader is looking at is not in the list at
all, and the slots it would have taken are taken by other sessions
instead. How long the window lasted was decided by the reader's own
behaviour rather than by the product: open a conversation and say
something and it comes back on its own, open it and read and it stays
missing for as long as nothing is written.

The current conversation is now resolved from the runtime database -- the
same resolution the "latest" binding already used, with the same ranking
and the same qualification -- and is always present in the picker, even
when its transcript file is the oldest of all the candidates. When it
falls outside the recent window its entry is built from the recorded
relative path, through the containment check the trajectory route
already applies to that value, so a recorded path cannot point the list
outside the session store.

Two things the list was getting wrong are fixed in the same place,
because they are the same mistake. The server never read the runtime
database's purpose column, so it had no way to tell a scheduled run from
the reader's own conversation, and it presented one exactly like the
other: a scheduled run appeared in the list under a title that looked
like something the reader had asked for. The purpose column is now read
and mapped to a closed set of tags, and a scheduled run, a background
task and a tool-less lightweight conversation each say what they are.
They are still listed -- the reader may want to look at one -- but they
no longer look like the reader's own work.

Sessions of the current workspace are listed before sessions of other
workspaces, each group keeping its own order by when it was last written.
The comparison happens on the server and only the resulting order is
published: the workspace path never reaches the page, and neither does
the raw purpose string, which is free text and can carry anything a
person wrote.

The order of the recent-entry window is deliberately unchanged. Its first
entry is the fallback answer for "latest" when the runtime database
cannot be read, so reordering it would change what "latest" means -- a
different question from which sessions the list offers.

The version moves 1.2.0 -> 1.3.0 across the manifest and both READMEs,
which is what CONTRIBUTING asks for when an existing app is updated.
The picker was covered, where it was covered at all, by regexes over the
source: a line that mentions the field, a line that mentions the marker.
Those are satisfied by a query that works, and never by a query that
fails -- which is the whole case here, because the database this reads is
written by another program, on a reader's machine, at a version nobody
here controls, and the columns it has are exactly the thing that went
wrong.

The new guard builds the databases by hand and asks the list what it
answers in each. One database with every column. One without the purpose
column. One without the workspace column. One where the schema stops at
the base three. And no database at all. Then it asks five questions of
the answer: is the current conversation in the list, is it first, is the
list still the length it was, do the sessions of the current workspace
come before the sessions of other workspaces, and does each group keep
its own order by when it was last written. It also asks what happens when
the recorded path of the current conversation points outside the session
store: the answer is that it is refused, and that the conversation is not
injected from it.

Two of those questions cannot be asked of a source file at all, which is
why the guard is a program and not a pattern. "The current conversation is
first" is a statement about the order of a list that is assembled from a
filesystem walk, a database read and a sort -- no line of the source
states it. And "the list is still the length it was" is a statement about
a number that lives in two places, the limit the caller asks for and the
number of entries that end up in the answer.

The label assertions run the shipped function rather than matching its
text, because the failure worth catching is not a typo in a marker: it
is the marker being applied to every row, or to none, and both of those
still contain the right characters. The decisive case is the one where
two rows differ only in the field that decides the marker, and the only
thing allowed to differ between their labels is the marker.

One thing had to be re-anchored rather than added. The mutation control
for the review guard names the shipped version literally, so moving the
version stopped four of its mutations from resolving -- and a mutation
whose anchor no longer matches is worse than no mutation, because it
reports green while testing nothing. They now name the new literal and
the one after it.

Two of the four mutations that cover the new behaviour live in controls
that were written for other rules, because that is where the code they
rewrite already is: removing a rung of the column ladder, and stopping
the validator from covering the kind badge.

verify-list-cache.mjs 25 -> 46, verify-sessions-etag.mjs 32 -> 35,
client-check.mjs 217 -> 233, verify-session-picker.mjs new with 25
assertions; mutate-list-cache-ttl.py 18 -> 22, mutate-sessions-etag.py
16 -> 17, mutate-session-picker.py new with 10 mutations, all caught.
Suite 92 -> 94, package byte-identical across the run.
Two places where the wording had drifted away from the code, and four rules
that had nothing able to fail.

The wording first. The READMEs and a server comment said the conversation the
reader is in, and the function that answers that question returns a ranking --
a live lease, then an open turn, then the most recently updated -- not a
certainty, and it reports the ambiguity when two conversations hold a lease
at once. The picker pins the database's answer, so it now says that, and the
wording agrees with itself.

The rules without a failure. The kind badge's order against the branch mark
had an assertion and no control, so nothing could break it. The control
exists now, along with three the change did not name: the recorded path is
checked against the session it claims to be, the tag is written on every row
rather than only on the rows that have a database entry, and the row the
reader pinned by hand is the first session row.

The recorded path is the one that could go wrong quietly. A path can sit
inside the sessions root and still belong to a different session, and then
the picker would list that other session twice while the conversation the
page is about was nowhere in the list. The check went into the function both
callers already share, so there is one copy of it.

The visible set's size was described two ways at once -- as able to grow by
one entry, and two lines later as not changing at all. The implementation
displaces the last entry, so the count is what it was and the composition
changed, and the wording now says that.

Each guard section used to report the first assertion in it when it threw,
which made a control that crashed the file look like a caught rule. Every
section reports itself now, and the controls expect those names.
…h was which

The list was flat. Every run the runtime spawned from another session sat next
to the reader's own conversations, with nothing on the row to say what it was,
and on this machine thirteen of the twenty rows were runs of the one
conversation being read -- so a reader comparing two entries could not tell
somebody else's work from their own.

They are not hidden either, because those runs were where that conversation
actually happened. What changed is the shape. There are two controls now, and
they cascade: the upper one, 「会话」, names conversations and nothing else --
the ones with no parent in the runtime database -- and it offers twenty of them
plus the follow-latest option, so every row in it is a conversation. The lower
one, 「子代理」, names the run inside the conversation that was picked. It is
empty by default, it has values once a conversation is picked, and it is empty
and disabled while the page follows the newest conversation: that conversation
changes under the reader, and a run picked out of it would belong to a
conversation that is no longer on screen. Its first option says 「(主会话本身)」
in words, so there is a way back.

A run is a session with its own transcript, and the parent's transcript does not
contain its turns -- so picking a run shows that run's own trajectory, and the
conversation pick moves to its parent. The two controls never disagree about
what is on screen: the run pick names the run, the conversation pick names the
conversation it hangs off, and neither one can be read as the other.

The label says nothing about the layer. It used to carry a word for it -- a session the
runtime spawned from another one was marked 「子会话」 in the picker -- and the word is
gone. One fact, one signal: the layer is the control the row is in.

The service sends the conversations and every run in the pool, and every run
row names the conversation it hangs off; the page groups them by that one id
rather than keeping a hierarchy of its own. The pool is the whole walk, not the
thirty most recent sessions, so a run of a conversation that has aged out of
the list still travels -- a reader reading that conversation still has to be
able to reach its runs.

The guards that read the picker came with it, because what they claimed was
about the shape that just changed. The page's own guard stopped asking what a
row's label says and started asking what the two picks hold, read back out of
a rendered pair of them; the guard on the two poll cadences asks that both
picks are redrawn after the runs are stored; the one on the list cache stopped
asking where a mark sits in the label and started asking how the page decides
which rows are runs; and the two on the service ask the validator for the runs
as a fourth argument, because they are inside its material now.
The rules that decide which sessions are conversations and which are their runs
were rules in prose. A main session and a run of it looked the same, a run
could be listed twice or dropped, the order inside a group could invert without
anything noticing, and the layer could be decided by the wrong column -- which
gets a run with no purpose and a conversation whose purpose says "task" exactly
backwards.

This adds the guard for them. It builds the store and the runtime database by
hand, with the two columns deliberately disagreeing, and asks the service four
things: the conversation list holds conversations and nothing else, every run in
the pool is listed -- the pool is the whole walk, not a window -- the runs of
one conversation arrive most recently active first, and the layer follows the
parent column rather than the purpose column. The fixture also holds a
conversation that is not on the page, because the runs of a conversation the
reader is reading have to travel even after that conversation ages out of the
list. It asks whether the validator moves when a run starts and when it runs
again, because a validator that does not move lets a caller keep a body that
never had it, and it reads the payload back from a real route rather than from
a function, because a cap on the way out is invisible to a test of the
service's own functions. One more question is asked of the payload: that every
field it carries has a reader in the page, read out of the page's own fetch
rather than out of a list written here.

The controls break each of them one at a time in the shipped source and require
the rule to be reported by name -- the ones on the page by the page's own
guard, the ones on the service by this one. Fifteen of them, six on the service
and nine on the page. The ones on the page break the two picks: a run in the
conversation pick, a run pick that is never empty, a run pick that is not
disabled while the page follows the newest conversation, a run pick that lists
somebody else's runs, a run pick with no row for the conversation itself, an
owner that is the run instead of its parent, and a pick that does not switch.

One thing had to be fixed for that to work. The guard read the text of an
option that was not there and threw, which meant a control that removed the
rows died before any assertion could report it. A guard that breaks along with
the code it is checking reports nothing, and a control that dies that way looks
exactly like a control that caught something.
…ation list

The picker has two layers now, and the layer is decided by the parent column and
by nothing else. The service splits the pool into conversations and runs, sends
both, and the page puts each row in the control that says what it is.

The route did not keep that promise on the way out. It splits, and then it pins
the conversation the runtime database reports as current into the visible set,
and that pinning step never asked what the current session was. A run with no
purpose of its own, holding a live lease, could win the current-conversation
resolution; the pinning step then built its entry from the recorded path and
prepended it to the conversation list. The same session sat in both controls at
once, which is the defect the two layers exist to prevent.

The pinning step asks now. A session with a parent is a run, and a run is not
promoted into the conversation layer -- it is already in the run layer, where it
belongs. The parent id comes from the same titles map the split reads, so there
is one judgement of the layer and one source for it.

The current-conversation resolution itself is untouched. Its answer feeds the
runtime diagnostic, the session list, and the trajectory route's 'latest'
fallback, and narrowing it there would move 'latest' as well. The question here
is not whether the current conversation may be a run -- it may -- but whether a
run may be promoted into the conversation list. It may not.

The second defect is on the page, and it is about order. The session list is
polled and every poll replaces the one before it. A replaced request cleared the
shared controller on its way out, so the request that replaced it had nothing to
abort; the request before that answered last and won, and the picker moved
backwards under the reader, runs and all.

Two rules now, each with one job. The controller says which request to stop, and
only the request that installed a controller may clear it. A sequence number
says which answer may write, and only the newest request writes. The trajectory
poll and the search already work this way; the session list was the last one
without it. An aborted request is also no longer swallowed into "the list
failed" -- being replaced is not a failure.
… easiest to test

The layer rule was guarded by a unit test of the split, and the split was not
where it had been broken. The route splits the pool and then pins the current
conversation into the visible set; the pinning step is the one that put a run
back into the conversation list, and nothing drove it. A guard that tests the
function that happens to be most like the rule is not a guard of the rule.

So the new section drives the route itself. The fixture is built for the case:
one conversation, and one run with a parent, no purpose of its own, and a live
lease -- the strongest signal the resolver has, so it wins the current
conversation outright. The body the service answers with is read back, and the
run must be absent from the conversation list, present in the run list, and
absent from both at once.

The page's defect had the same shape: nobody drove three overlapping list
requests, so nobody noticed that the replaced one took the controller with it.
The new section drives exactly that -- a controllable fetch, three requests
overlapping, the middle one answering last with the older body -- and reads
back what the page ends up showing. The second request must have been stopped by
the third, and the newer list must still be on screen.

Both come with mutations that break the rule in the shipped source and require
the guard to report it by name. There are three: the pinning step loses the
parent judgement, the controller check becomes unconditional again, and the
sequence check is dropped. The last two are separate on purpose -- each leaves
the other defect in place, so one mutation cannot stand in for the pair.
The page groups the runs it is sent by the parent the service named, and it
grouped them into a plain object. The parent id is data that crossed the wire
from the runtime database, and a plain object inherits names like '__proto__' and
'constructor': they are truthy and they are not arrays, so one of them was read
as a group that already existed, and the run was then pushed into it. The list
response was perfectly fine; the picker stopped updating after it.

The map has no prototype now, so no value can be read as a group that is not one.

The service checks the other end of the same boundary. `parent_session_id` is a
foreign key into the runtime database's own session table, and a value that names
no session there is not a parent: it was published as one, and the page groups by
it. The check lives where the column is read, so the body and the validator --
which read the same map -- agree about it without a second writer.

A value that names no session is not published, and the session it was on is a
conversation rather than a run: it is a real session on disk with its own
transcript, and dropping it would hide a record the reader can open.

The id grammar the service applies to the ids it derives from directory names
would not have done it: that grammar lets '__proto__' and 'constructor' through,
so a value like those would still have been published.
The service's rule is about what the page receives, so the guard for it drives
the route: the fixture holds two rows whose parent names no session -- one with
an id that looks like a session id and does not exist, one named after an object
property -- and the body is read back to say that neither is published as a
parent, and that the sessions they were on are conversations.

The page's rule is about a value the page has no business assuming, so the
guard for it drives the shipped function with those same names on the page: the
grouping must not throw, the runs must not be folded into a conversation that
owns them, and the run pick must still draw.

Each comes with a mutation that breaks its rule in the shipped source and
requires the guard to report it by name. They are separate on purpose: a
service that published the value would still break a page that grouped into a
plain object, and a page that grouped into a plain object would break even on a
value the service had checked.

The guard for the column ladder had a fixture the runtime cannot write. It
built a run whose parent named a session the database had no row for, and read
the parent back as though that were ordinary -- so once the service started
refusing such a parent, four of that guard's assertions went red for a reason
that had nothing to do with the rule they were written for. It writes the
parent row now, in the order the runtime writes them, and a second fixture
builds the run whose parent names nothing: that is the case the service has to
refuse. The paired mutation control gains one control for it, and a service
that published such a parent has to be caught by name.
The list is polled, and every poll replaces the one before it. Two rules keep
that from moving backwards: a request that has been replaced must not take the
next request's controller with it, and only the newest answer may write.

The second rule covered the answer that carries a body and not the one that
does not. A 304 was writing the list's validator itself, before the sequence
gate, so a 304 that arrived late put its own older tag over the newer one --
and the list on screen and the tag that described it were then two different
things. The next poll asked the server about the old list, was told the list
had not changed, and kept the new one.

The 304 branch now writes nothing. It hands the validator back as data, and
the one writer -- the one that runs after the sequence gate -- puts it on
screen. Both answers go through the same writer now, so there is one place
where the list's validator is decided.

The service's parent check is closed the same way. `parent_session_id` is a
foreign key into the runtime database's own session table, and a value that
names no session there is not a parent. The check used to prepare a second
query to make that lookup, and the catch around it published the parents it
had not managed to check -- so the promise that every published parent names a
session held on the ordinary path and not on the other one.

The lookup now uses the statement that read the row in the first place, so
there is no second query that could fail to prepare, and no path on which an
unchecked parent is published.
The 304 case is driven, not read. The overlap section answers one request with
a 200 and a newer tag, lets the replaced request answer late with a 304
carrying the old one, and reads back two things: the validator on screen is
still the newer one, and the poll after it asks the server about the list that
is on screen. A manual refresh is driven too -- it must send no validator at
all, because the reader asked for the list again.

The parent's promise is driven on both sides. The route is asked over HTTP
against a store built for the case, and the answer is read as BYTES: the raw
purpose and the workspace path are not in them. The runtime route is asked the
same way, against a temporary home, and its bytes carry no absolute path --
the route that promises to publish names and structure only is now asked
rather than read.

The parent's existence check is pinned at the unit level too, against a
database whose run names a session that has no row: the parent is not
published, and the run is still a session with its title and its kind.

Each of these comes with a mutation that breaks its rule in the shipped source
and requires the guard to report it by name. The ones that used to point at
the old shape of the check were re-anchored on the new one, because an anchor
that no longer resolves is a mutation that no longer proves anything.
…n's layer

is decided by the chain of parents it forms

Two places on the service read a session's identity and read it differently.

The recorded path of the current conversation -- the column the runtime
database keeps for it -- was checked against the directory name alone. The
directory name is one of the two places the id is written down: the runtime
also writes it into the manifest beside the transcript, and the service reads a
session's identity from that manifest, preferring it over the name. A directory
whose name encodes one session and whose manifest names another is the other
session's directory, whatever the folder is called, and accepting it puts the
current conversation's id on somebody else's messages. The path is now checked
against both: the name has to be the session, and the manifest has to be the
session. A directory with no manifest is judged by its name alone, which is the
case that was always there.

The two layers of the picker -- conversations, and the runs inside one of them
-- were decided by the parent column, and the column answers a different
question. It says whether a session HAS a parent; the layer is whether that
parent is a CONVERSATION, and that is a question about the chain. Three shapes
of chain have no conversation at the end of them. A cycle -- one session's
parent is another and the other way round -- read the column alone puts both
in the run layer, hanging off each other, so neither is a conversation and the
conversation list comes back with neither in it: they are in neither control,
and the reader can reach neither. A parent with a row in the runtime database
but no directory in the pool is a conversation the reader cannot pick, so the
run of it is a run nobody can reach. And a run of a run is a run of a run,
which neither of the two controls can show.

The layer is now decided by the chain. A session is a run iff its parent is a
conversation, and a conversation is a session with no parent or one whose
parent is not a conversation; the two halves have to be given together,
because one half alone has no answer for a cycle. A cycle is dissolved into
conversations, a run of a parent that is not in the list becomes a
conversation, and so does a run of a run.

The map the layer is decided from is corrected in place: a session that ends
up in the conversation layer has its parent column cleared. The payload, the
validator and the current-conversation promotion all read that one map, so all
three now say the same thing about the same row.
The recorded path is asked about in three directions, because a name and a
manifest can disagree in both directions. A directory whose name is the
current session and whose manifest names another one is refused: the manifest
is what says which session the directory holds, and the name cannot vouch for
it. A directory whose name is another session and whose manifest names the
current one is refused too: the walk already lists that directory under its
own name, and accepting it would list the same one twice. And a manifest that
agrees with the name is accepted -- the third fixture is there so that a check
which refuses everything cannot pass as a check.

The chain gets a fixture of its own, at the route. Two sessions that name each
other as parent, both with a row, are both conversations, neither is a run,
and neither names a parent on the wire -- and a run of one of them is still a
run of it. Two more shapes are asked at the function: a run whose parent has a
row but no directory in the pool, and a run of a run. Both are conversations,
and both name no parent, because a conversation that names a parent names a
run.

The existence check on the parent column keeps a unit-level owner. The chain
rule covers it on the wire -- a parent that names no session is not in the
list, so the session that named it is a conversation and the column is
cleared -- which means the wire looks the same whether that check is there or
not. It is therefore read off the map the read produces rather than off the
bytes, so that the rule still has something that can fail.

Each of these comes with a mutation that breaks its rule in the shipped
source and requires the guard to report it by name: the two halves of the
identity check, the cycle, the parent that is not in the list, and the run of
a run. The two mutations that used to point at the old shape of the parent
read were re-anchored on the new one, because an anchor that no longer
resolves is a mutation that no longer proves anything.
…sion's only when its manifest says so

A row came out of a cache and was patched with the runtime's facts, so a session whose
row had left the runtime database went on publishing the title, the kind and the parent
it had last time, while the validator, which reads the same rows, reported none of
them. The row is built instead: every runtime fact is written on every pass, and a row
with no runtime facts carries none of them -- the label falls back to the one the
filesystem gives, and the kind, the parent and the tag are null.

The layer of a session was decided by whether its parent was in the map, and the map
holds sessions the reader cannot pick: the current conversation is in it even when it
has no transcript in the store. The membership test is about the walk now, so a parent
the reader cannot reach is a parent the reader cannot reach, whatever the database
remembers about it.

Two paths took a directory's name for a session's identity: the picker promoting the
current conversation, and the route that serves the latest transcript binding the
current session to it. Both confirm the directory against the manifest beside the
transcript now, through one function, so a directory whose name says one session and
whose manifest says another is that other session's directory.
A row whose runtime facts are gone no longer publishes them -- checked on the response
body and on the body the service reuses for the next request, after the caches have had
their time. A run whose parent the reader cannot pick is a conversation of its own. A
directory whose manifest names another session is neither promoted nor bound to the
current session, in both window positions. The workspace path is looked for in the
parsed body, where JSON cannot hide it by escaping a backslash.

The control that leaked the workspace also leaked the purpose, so it was caught by the
purpose assertion and the workspace assertion was never asked whether it could fail.
The two have a control each now, and each is caught by the assertion that is about it.
…est agree

A published row took its identity from one place and its metadata from another: the id
came from the manifest beside the transcript, while the title, the kind and the parent
came from the id the directory's name decodes to, and the layer of a session was
decided against that same decoded id. A directory whose name says one session and
whose manifest says another therefore produced a row that named the manifest's session
and carried the name's metadata, and a child session whose parent was that name was
published under a parent no reader could pick -- neither control could reach it.

The walk now refuses a directory whose name and manifest disagree. It is the judgment
the recorded path and the latest binding already made, extended to the walk rather
than written a second time, so one rule says which directory holds which session. A
directory that fails it is not published at all: not in the session list, not in the
run list, and not served by id. The id it claims is not among the published ones, so
a run hanging off it becomes a conversation of its own, by the rule that a parent the
reader cannot reach is not a parent.
Five assertions at the route level: the disagreeing directory is not in the list, its
database row is not published under any id, a run of it becomes a conversation the
reader can pick, it is not left in the run list either, and the route does not serve
that directory either. One mutation turns the walk's refusal off, and it has to be
caught by name.

Two mutations had stopped changing anything, because the walk now removes those
directories before the two shortcuts can reach them. They are anchored on the checks
that are still alive instead, and the fixture that covered one of them was rebuilt so
that it still covers it: the current conversation now has no directory of its own in
the store, which leaves the recorded path as the only way in and the directory name
the only thing that can refuse it.

The check that says the id index is built from the decoded directory name was looking
for that expression in the whole function, and the walk now decodes a name of its own
when it decides whether a directory is publishable. It is pinned to the index
construction instead. The four anchors in the reachability control were re-anchored to
the walk as it is written now: a stale anchor stops that control before the mutations
after it are run at all.
…s that one

A directory whose name does not decode is admitted by the walk on the strength of the
manifest beside its transcript, and that admission is supposed to make it a session.
The grouping set and the runtime-facts lookup still read the id decoded from the
directory name, so such a directory was published under its manifest's id while the
layer and the metadata did not know that id: a run hanging off it became a conversation
of its own, and the parent lost the title and the kind the runtime database still had
for it.

The slow path of the explicit-id route had the mirror problem. It re-read the manifest
of each candidate and admitted the id that manifest now claimed, without asking
whether that id still agreed with the directory's name, so a candidate the walk had
admitted under one id could be served under another one as soon as its manifest was
rewritten inside the walk cache -- and the id it used to claim stopped working.

The walk now decides the id a directory is published under once, when it reads the
manifest it reads anyway, and puts it on the entry. The grouping set, the runtime-facts
lookup, the row, the validator, the index the route resolves an id through and the
recorded path all read that one field instead of decoding the name again. The index is
keyed by the published id, so every id the list shows is an id the route can resolve; a
directory whose name does not decode is keyed by the manifest's id, and one with no
manifest at all by the folder name it can be reached by.

A name that does not decode names no session, which is not the same as disagreeing
with one, so such a directory is identified by its manifest alone. And the slow path
now asks the same rule the walk asks, so a directory whose name and manifest disagree
is served under neither id -- not the one its manifest now claims, and not the one the
walk admitted it under.
A route-level fixture now asks the two cases that were open: a parent identified by
its manifest alone keeps its runs, keeps its title, and the id the list shows is the
id the route resolves; and a candidate whose manifest changes under a cached walk is
served under neither id. The list fixture also says that every row the list publishes
carries an id, since a row with no id cannot be grouped, labelled or selected.

The reachability fixture was rewritten for the new index: it is keyed by the id the
walk published, a directory whose name does not decode is in it under the id it is
published under, and one with no manifest at all is in it under the folder name it
can be reached by.

The mutation controls grew with the rules. The layer control asks the grouping set,
the runtime-facts lookup and the slow path to go back to the name, and asks the
published row to lose its id; the picker control asks the recorded path to refuse a
directory whose name does not decode and to build an entry with no id; the
reachability control asks the index to go back to the decoded name. The anchors that
the renames moved were re-anchored, because a stale anchor stops its control before
the mutations after it are run at all.

One mutation moved house. The rule that an entry built from a recorded path carries
the session id sat in the layer control, where the visible set is built from the whole
walk and the recorded path is only consulted for a directory the walk never reached --
so the mutation changed nothing any assertion could see. It sits in the picker control
now, whose visible set is built from the window of thirty, where that path is reached.
The list's validator covered the directory, the size, the mtime, the title, the kind,
the parent and the tag -- but not the id the walk published. A manifest rewritten from one
id to another left the validator where it was, so a conditional request was answered 304
with the tag of the old id, and the body the service reused for the next request kept
publishing the old id while the route reported that id as 404.

The trajectory cache key had the same gap. It carried the directory, the lineage, the
size, the mtime, the catalog bytes and the binding, but not the identity the body is
about: one directory holding two ids shared one entry, so the body of the first id could
be served for the second, with the same ETag.

And the `latest` fallback asked no question at all. The explicit-id route asks, on both
its fast and its slow path, whether the directory still holds the session it was published
under; the fallback took the first candidate of the cached walk instead, so a candidate
whose manifest no longer agreed with its name was served anyway.

The validator now names the published id, for the main sessions and for the sub-agents.
The trajectory cache key now names the identity the body is about. The fallback now asks
the same question the other two paths ask, and reports the session as unavailable when
no candidate still holds one.
Each of the three rules gets a check of its own, and each check has a mutation control
that is caught by name.

- the list publishes the id the manifest names; a changed published id moves the list
  validator and replaces its body, and the body the service reuses for the next request
  says the same thing; `latest` never serves a cached candidate whose manifest now
  disagrees with its name; an identity-only manifest change cannot reuse another session
  identity's trajectory body
- the kind the runtime database holds for a parent session survives
- a changed published session id moves the tag
- the cache key is read as the set of components it is made of rather than as one frozen
  line of source: the family prefix comes from the same helper the cache uses, the key
  names the identity the body is about, and the key names the lineage it stitches. Two of
  those checks read no source at all -- two identities are two keys, and an identity is
  not the same key as no identity

The anchors of the mutation controls were re-anchored where the material they take apart
grew a field, and the two that take the cache key apart were re-anchored to the key as it
is written now. An anchor that no longer resolves stops the control before the mutations
after it are run at all.
… about

The trajectory poll is a second implementation of the rule the session list already follows: a
request that has been replaced writes neither a body nor a validator. It wrote its validator in
the response handler, before the sequence gate, so a 304 that arrived late put its own older tag
over the newer one. The body on screen and the tag that described it were then two different
things -- the next poll asked the server about the old body and kept the new one on screen.

The poll now has the shape the list already has: the response handler carries the answer out,
and one writer, after the gate, writes the validator.

And the two secondary reads asked for a different session than the one they were caching. A
record index is a position inside one body, so the raw read and the detail read have to ask
about the session that body was built for. The page's own cache key already named the displayed
session; the requests asked for "latest", which is the session that is current now. The runtime
can make another session current while the page still shows this one, and the page then showed
that other session's words under this session's record.

Both requests now name the displayed session, so the key and the request say the same thing.
- two overlapping trajectory polls are driven against a controllable transport, and a late 304
  and a late 200 must both leave the newer answer alone; the poll after them asks the server
  about the body that is on screen
- the raw read and the detail read are driven against a payload, and both must name the
  session on screen rather than whichever one is current now
- the sub-agent's published id moves the list validator on its own: the main session's id had
  an assertion of its own, and the sub-agent's tuple is a different tuple
- the controls beside them: three for the page and one for the validator, and one control each
  for the raw read and the detail read -- two requests, and a control that changed both would
  leave the detail assertion never asked whether it could fail
The two READMEs described the upper select as "the rows with no `parent_session_id`". That was an
earlier and simpler version of the rule: the layer is decided by the chain of parents, so a
session whose parent names no session, whose parent chain ends in a cycle, or whose parent is
itself a run is in the conversation select too, and a row that ends up in that layer carries no
parent. The description was narrower than the behaviour, and the rule that made it narrower is
part of this change.

Both READMEs now describe the chain, name the three shapes that have no conversation at the end
of it, and say that a row in the conversation layer carries no parent. No implementation byte
moved with them.
A cache key that names its requester cannot also decide which entry a read looks for. The
`bind` segment carries the runtime binding that a `latest` request resolved; a request that
names a session by its id resolves none and computes a different key for the same directory.
The body the poll stored and the body the read looked for were therefore never the same entry,
and reading one record of the conversation on screen answered 「会话数据已释放,请重新加载」 for a
session the service was holding -- and the client's own retry did not rescue it, because the
retry reloads the current conversation rather than the displayed one.

The key names a version and the family names the question, and every entry already records the
family it answers for. The two secondary readers now look the exact key up first and the
family second, so a poll that asked for `latest` and a read that asked for that session by its
id reach the same entry -- before the runtime moves on, after it, and after a reload.

The search was the third place that asked about the current session rather than the displayed
one; the other two were changed in the previous round. It now asks about the displayed session,
and it checks that the answer names that session and that slice before the answer's indexes may
filter the body on screen. An answer about another session leaves the previous result standing
and says the search failed, which is a different sentence from 「matched nothing」.
… name it

- the trajectory-cache guard now drives the route over HTTP: a store with two conversations and
  a runtime database that says which one is live, a poll that loads one of them, a read of one
  of its records by that conversation's id, the runtime moving on, and the read again -- before
  the switch, after it, and after a reload
- it asserts the pieces separately as well: that the request resolves the family, that the cache
  can be asked for one, that reading by family refreshes recency, and that each of the two
  readers falls back to it
- the client guard now asks the search about the session on screen, and checks the answer's
  session and slice against the displayed payload before its indexes may filter it -- including
  the other direction, that the answer about the conversation the page moved to is installed
- the controls beside them: five for the cache -- one per piece, and one each for the two
  readers, since a control that changed both would leave the other assertion never asked whether
  it could fail -- and three for the search
…session

A generation the reader picked and the body on screen are two different things, and they come
apart when the load of the picked generation fails: the selection says one thing and the body
says another. The search asked about the selection, so its answer carried indexes of a slice
that was not on screen -- and those indexes then filtered the rows the reader was looking at,
reporting a miss for a row that matches and a hit for one that does not.

The payload now says which slice it IS, instead of how many generations it happens to show, so
the page can ask the server about the body it is showing. The search, the raw read, the record
read and the browser's own raw cache key all follow it, and the answer's slice is checked
against the body's before its indexes may filter that body.

A cache entry now records which session it is about. The family names the question -- the
directory and the slice -- and one family is asked by many readers, so the session cannot live
in the family; but the fallback may not hand back another session's body either, and a
directory can stop holding one session and start holding another. A read for the new session
now says the body is not held, rather than answering with the old session's words.

The handoff captures the session together with the record it is about. Its two reads are
asynchronous, and the text it hands to the Host names the conversation -- a session read after
the reads would name the conversation the reader had already moved on to, over another
conversation's message.
- the trajectory-cache guard grew a section that drives the route over HTTP for the session
  identity: a directory whose name does not decode, so the manifest beside the transcript is
  the only identity it has; a load of that conversation; a read of one of its records that
  warms the decode cache; the manifest and the transcript replaced; and a read for the new
  conversation, which must not answer with the old one's message
- it asserts the pieces separately as well: that an entry is found for the session it is about
  and not for another one, and that the route records the session while the request resolves
  it. Those two shape assertions come after the HTTP section on purpose -- the behaviour has to
  be asked first, or a mutation is reported as caught while the behaviour it is about was never
  asked whether it could fail
- the client guard grew three sections: the search's slice, where the body on screen and the
  generation the reader picked come apart; the secondary reads' slice, including the browser's
  own raw cache key; and the handoff, where a deferred raw read lets the page move to another
  conversation while the read is still in flight
- the controls beside them: three for the cache -- the lookup that no longer checks the
  session, the route that no longer records it, the request that no longer resolves it -- four
  for the search client, one each for the search, the raw read, the record read and the cache
  key -- and a new control file for the handoff, with one that reads the session after the
  reads and one that does not read it at all
- the raw-locators guard stopped taking a fixed number of characters after the cache key
  function and now takes the whole function: a window that stops being the function when a
  comment grows fails on the edit rather than on the defect it is about
- the cache control also started requiring that a failure NAME a rule, the way the other
  controls already did. It printed the name before, but did not insist on one.
The session and the slice answer "which body". They do not answer "which version of the record at
this index", and a transcript rewritten under a running page leaves the indexes alone while it
changes the records behind them. The search answer is rebuilt from the current disk while the page
filters the body it is showing, so a hit could mark a record as matching when it is a different
record; and a secondary read named a position without naming the record behind it, so it could serve
another record's content under the number the reader had open.

Every record of a body now carries a version: the digest of the line it came from, together with
its position inside that line. It is stable when the session grows -- the change that happens on
every poll -- and it changes when the line is rewritten. A version over the body as a whole would
have made every record on screen stale on every poll, which turns the most common change into a
failure; a version per record does not.

The search answer's hits carry the same version for the same record, and the page compares a hit's
version with the record it is about to filter before installing the hit set. A hit whose record is
not on screen is dropped and is not counted, so the number the reader sees and the rows that are
highlighted agree.

The two secondary reads carry the version in the request. The service answers one only when the
record it would serve is the record that was asked for, and refuses otherwise: a read that names no
version is refused, and a read whose version does not match the record now at that index is refused
rather than answered with it. Old bodies are not kept -- the service declines to serve a record that
moved, and the page says so.

The route now publishes the slice it is for instead of how many generations it happens to show, and
the body and the search answer name the same slice. That field was published to the reader while
no assertion asked what it said.

One comment in the page claimed that an answer's identity was checked before its indexes filtered
the body. It checked the session and the slice and never the version. It now says what it does.
The cache guard now asks which version of a record it is serving. Every record of a body carries
one, the search answer's hits carry the same one for the same record, and a secondary read that
names an older version is refused instead of answering with the record that now sits at that
index. A read that names no version at all is refused too, because the service cannot tell "the
record you meant" from "whatever is at that index now". The body names the slice it is for, and
the search answer names the same slice as the body it answers about.

The client harness now asks whether a search answer for a newer body can filter the displayed older
body, and what the page does when it cannot: the answer is not installed at all, the body is
fetched again and the question asked again, at most twice, and after that the search reports a
failure rather than an answer. A hit whose record is not on screen is dropped, is not counted, and
does not start a refetch.

The guard's own store moved to a temporary directory. A search writes its segment cache under the
data directory, and the package directory installs onto every reader's machine, so a guard that
left a cache in there was a guard that shipped a cache.

The mutation controls grew to match what the guards now own: seven in the cache control -- the body
stops stamping its records, either secondary read stops checking the version, a read that names no
version is answered anyway, the hits stop carrying the version, the body stops naming its slice,
and the search answer names a generation when no slice was asked for -- and three in the client
control: the search stops comparing the version of the record a hit names, and the two reads stop
naming the version of the record they ask for.
…of zero

A review of the committed bytes found these things in the parsed-rows cache, and
the code did not say any of them:

  - the promise was tighter than the implementation. The limit is when the rows stop
    being wanted; the RELEASE happens on the next sweep, and the sweep runs every two
    and a half minutes, so the rows are gone no later than twelve and a half minutes
    after the last request -- not ten. The number is now the real one everywhere it is
    stated.
  - a limit of zero meant two contradictory things. The rows expired at once -- every
    read a miss, every request a re-parse -- while the sweeper never started, so
    nothing was ever released. Zero now means no limit and nothing else: the rows never
    expire and the sweeper never runs.
  - the assertion that the sweeper does not keep the process alive matched any unref in
    the file, so a later timer with an unref of its own would have kept the rule passing
    after the sweeper had lost it. It matches the sweeper's own call now.
  - the relation between the limit and the sweep interval held only for the numbers
    this package happens to ship. A caller who asked for a longer interval got one, and
    the promise to a reader was then wrong by that much; the two numbers were also only
    related in the source, not in the cache, so changing the place the interval is
    taken from left the assertion passing.

The sweep interval is a named constant rather than a fraction written at the call site,
because the promise to a reader is the limit plus that interval, and the two have to stay
in that relation.

And that relation is now enforced where the interval is TAKEN rather than only in a
comment: the interval is clamped to a quarter of the limit the cache was given, so
"never later than a quarter of the limit beyond it" holds for every limit -- and for the
interval a caller asks for -- and not only for the one this package ships with. The guard
reads the interval the running cache uses instead of two numbers in the source, and the
control has a case on the line that takes it: reading the source would have kept the
assertion passing after that line had changed.
…nted against the budget

A review of the committed bytes found two things in the body serialization, and both of
them are about a comparison that was too weak:

  - the send path compared the LENGTHS of what reached the socket and of what the
    serializer produced. Two escapes passed it: one that joins the pieces into a single
    write, and one that changes the bytes on the way out without changing their length.
    The guard now compares the bytes against an expectation it derives itself, checks
    that one write per piece reached the socket, and checks that each of them is bytes.
    Each of the three has a mutation of its own, and the control for that guard went
    from seven cases to twelve, every one of them caught by name.
  - the closing bracket of the array was appended after the size of a piece had been
    decided, so a piece holding more than one element could come out one byte over the
    budget. The bracket is counted where the size is decided now, and the guard has the
    boundary fixture that sees it.

Two wordings in the package were wrong as well, and they are corrected here:

  - three places said the process never holds the whole body as a string. A value in the
    payload that is not an array is serialized on its own and appended to the piece being
    built, so that one is still a string. The three places now say the payload's BIG
    ARRAY, and write the exception out.
  - the byte account said that turning a Chinese transcript into bytes makes it one byte
    per character instead of two. UTF-8 gives a Chinese character three bytes, so that is
    not half the size. V8 holds a string in ONE representation for the WHOLE string, so a
    single character outside Latin-1 raises the whole string to two bytes per character
    and the ASCII majority goes up with it; what the change removes is the STRING, and
    the bytes live outside the JS heap.

The suite passes with every case of that control caught by name.
A read of the committed bytes left two owners weaker than the promises they carry, and both
of them are fixed here.

  - the socket check asked whether the NUMBER of writes matched the number of pieces. A send
    path that keeps that number and cuts the pieces up differently passes it: the bytes are
    the same bytes, every write is still bytes, and the body still declares the same length,
    but the first write is not the first piece. The check compares the writes to the pieces
    one for one now, and a write has to BE the piece, not merely be the same number of them.
  - the live check compared two of the record ids, so a body can carry the right number of
    records, the right first and the right last record, and the wrong records in between,
    with the count and the parse both green. The whole sequence of ids is compared now,
    against the ids the file was written with.

Each of the two has a mutation of its own, and both mutations are the reproductions that were
run against the fixed code. The two owners are separate: under the break that belongs to one
of them the other stays green.

The control for that guard went from twelve cases to fourteen, and the suite passes with every
one of them caught by name.
A read of the committed bytes asked for two things: keep the piece correspondence as an identity
check, and give the byte comparison a break that only it catches.

The control has that break now. The send path changes the bytes of the piece IN PLACE -- the same
object, the same length -- so the correspondence stays green (the object did not change) and the
type stays green (it is still bytes), and the byte comparison is the only thing that goes red.
Every occurrence is replaced, and not only the first one: replacing the first is not idempotent,
and the warm read sends the same pieces again, so it would replace the next one and the cold body
and the warm body would stop being the same bytes.

The guard says the rest of it in prose. The three owners are not independent of each other and
they are not meant to be: the correspondence is an identity check, so a send path that hands the
socket a different object -- a copy, a string, a modified buffer -- is not writing piece N and
goes red there as well, whatever else it breaks. That is the strength of the check rather than a
false alarm: a body the socket receives as copies is the memory shape this change was made to
remove. Each of the three is judged on its own property, and each of the three has a break that
only it catches.

The control for that guard went from fourteen cases to fifteen, and the suite passes with every
one of them caught by name.
Three things about the parsed-rows cache were open, and they are fixed here.

  - the promise to a reader -- the rows are gone no later than one sweep interval after the
    limit -- had half an owner. The number in the prose was compared with the limit and with
    the interval the cache reports, but nothing watched the call that starts the timer, so an
    interval multiplied by two left every check green while the real release moved to "the
    limit plus twice the interval" and the promise went on saying twelve and a half minutes.
    The timer is not reachable from outside the cache, so the check reads the call that starts
    it: it has to take the interval the cache itself reports, with nothing done to it on the
    way. It has a mutation of its own, and it is caught by name.
  - the assertion that owns that number said "no later than", which is an upper bound, while
    what it compares is an equality. The name says what it compares now, and the two mutations
    that break it name the new one.
  - the control did not check that its anchors occur exactly once, so a duplicated anchor would
    have been edited in the wrong place, and the case would have passed or failed for a reason
    that had nothing to do with the rule it is about. It counts them now, the same way the
    control for the body does, and a case whose anchor is not there exactly once is reported as
    missed instead of being run.

The interval is written into the comment of the constant as well, so a reader meets the timer and
the promise in the same place.

The control for that guard went from twenty-six cases to twenty-seven, and the suite passes with
every one of them caught by name.
Two owners of the parsed-rows cache did not hold the rule they name, and both are fixed here. The
cache itself is not changed: the shipped product behaves as it is documented, and what was missing
was an owner that could see a break.

  - the owner of the timer compared the way the call SPELLED the interval, not the interval the
    timer was given. It looked for one setInterval in the source and required the argument to read
    as the name the cache reports -- and a local binding that shadows that name keeps the spelling
    while changing the number. The cache kept reporting the interval it was asked for, the number
    in the prose kept matching that reported interval, and the real release moved out to nearly
    fifteen minutes while every check that read the source stayed green. The owner now observes the
    timer instead: it stands in for the clock's own timer call while the cache runs, records the
    interval the timer was actually given, and compares that with the interval the cache reports.
    The rule it names is the relation it now holds.
  - the owner of a limit of zero covered one half of its promise. The promise has two -- the rows
    never expire AND the sweeper never runs -- and the owner checked a hit and the retained bytes,
    which say nothing about whether a timer was started. Moving the boundary of the sweeper's start
    condition by one comparison let the sweeper run under a limit of zero; and since nothing in such
    a cache ever expires, that timer would scan it for ever. The absence of a sweeper now has its
    own owner, which observes the timer calls of a zero-limit cache and requires that there are
    none.

Each of the two has a mutation of its own, and each is caught by name. Three cases were added: a
local binding that shadows the reported name and doubles the interval the timer really gets, one
that shadows it with a number of its own, and one that lets a limit of zero start the sweeper.

The two owners were measured against each other, one break at a time: the interval breaks leave the
zero-limit owner green, and the zero-limit break leaves the interval owner green. The break that
replaces the interval with a number of its own also turns the live release assertions red, because
that break really does stop the rows from being released without a request -- it is reported as
breaking more than one rule, not as breaking the one it was measured for.

The guard went from eighty-nine assertions to ninety.
Two owners in this package decided the truth of a rule by matching text against the source, and a
match on text can be satisfied by text that is not the thing the rule is about. Both are fixed
here, and the product is not changed: what was missing was an owner that could see a break.

  - the owner that the sweeper's timer does not keep the process alive looked for the text of an
    unref call anywhere in the service. An unrelated function with a parameter of the same name
    satisfied it -- a function that is never called -- while the real timer stayed referenced and
    the process would have waited for it. The owner now watches the handle the cache was given: it
    stands in for the clock's own timer call while the cache runs, and the timer the cache created
    has to be one that does not hold the process open. That is the platform's own answer about the
    handle, not this file's reading of the text. The handle is cleared as soon as the window
    closes, so no timer is left behind and its callback can never run.
  - the owner that the parsed-row cache exposes clear() looked for the shape of a clear method
    after the name of the cache factory, and the search could run on past the cache's own body and
    find that shape in an unrelated object further down the file: the parsed cache's own clear()
    could be renamed -- and the dispose path would then throw when it reached the cache it meant
    to release -- while another clear() supplied the text. The owner now asks the cache: it creates
    one, puts rows in it, calls clear(), and checks that the method is there and that the cache is
    empty afterwards.

Each of the two has a mutation of its own that reproduces the escape it was found by -- one that
puts an unrelated unref where the real one was, one that renames the method and gives an unrelated
object a clear() of its own -- and each is caught by name. The two controls went from thirty cases
to thirty-one and from fifty-four to fifty-five, and every case in both is still caught by name.

The two owners were measured against each other, one break at a time: each of the four breaks
trips exactly one assertion, and the file is restored byte for byte after each one.

The guards went from eighty-nine assertions to ninety and from fifty to fifty; no assertion was
added or removed, only what each one looks at.
Two owners in this package were standing in for each other: a break in one rule turned an owner of
another rule red, so a guard could not tell which rule had actually been broken. Both are fixed
here, and the product is not changed.

  - the owner of the interval the release timer runs on required that a timer had been started at
    all -- exactly one call, and the interval it was given. A cache whose sweeper was never started
    therefore reported that owner, even though the rule it names was not broken by that: with no
    timer there is no timer running on the wrong interval. The owner now rules about the interval
    and about nothing else. It holds for every timer the cache starts, and holds vacuously when it
    starts none.
  - the owner that the sweeper does not keep the process alive required that the cache had been
    given exactly one handle. The same break reported that owner too, and nothing about a cache
    that starts no timer can hold the process open. It now holds for every handle the cache was
    given, and holds vacuously when it was given none.

The rule that a sweeper has to start at all is a separate rule, and it keeps an owner of its own:
the rows have to be released without a request. That owner still fails when the sweeper is never
started, and a mutation of its own now says so -- a case that takes the call that starts the
sweeper away and expects the release owner to name itself. The control went from thirty-one cases
to thirty-two, and every case in it is still caught by name.

The two owners were measured against that break, one at a time: taking the call away trips exactly
the three release owners and nothing else, so neither of the two owners is left accusing another
rule. The file is restored byte for byte after each break.

The guard stayed at ninety assertions; no assertion was added or removed, only what each one looks
at.
…does

The five owners of this package decided the truth of a rule by reading the source, and a source says
what the code intends, not what it does. All five are fixed here, and the product is not changed.

  - the owner of the lifetime the cache gives its rows when nobody tells it one read the
    declaration of the limit. A cache whose default came from somewhere else than the constant
    answered every declaration-based check with the documented number while the rows it actually
    held lived twice as long. The owner now moves a clock until the cache lets the rows go by
    itself, and the moment it does is the limit.
  - the promise to a reader is written twice -- as a number of minutes and as a number of
    milliseconds -- and the two forms have to be the same promise. The owner took the number out of
    the whole documentation block, so a number left in an annotation beside the promise answered
    the pattern while the sentence a reader reads said something else. It now takes the sentence
    that makes the promise, reads the number out of that sentence, and compares the two forms with
    each other and with the limit the cache really gives its rows.
  - the owner of the delay the poll waits read the number the page declares. A poll that multiplies
    the interval it declares loses its rows between polls while every check that reads the
    declaration stays green. The owner now finds every use of that name in the page, evaluates the
    expression the timer is really given, and requires the delay to be the declared one.
  - the owner of the charge the route makes looked for the text of the charge somewhere in the file,
    which a commented-out line satisfies, and the fixture it ran left the options out -- so it
    took a different branch of the same function from the route. It now makes the call the route
    makes and reads back the bytes the cache was charged.
  - the owner of the proxy constant took the first match of its pattern in the file, and a
    declaration that is commented out is not a declaration: an annotation left beside the real one
    answered the pattern while the binding was a string. The owner now requires the line the match
    is on to declare it -- nothing but whitespace before it, and no block comment open around it.

Sixth, a handle that could not answer the question was accepted as one that did not hold the
process open: the owner of that rule let a handle pass when it had no way of saying whether it
keeps the process alive. It now requires every handle to answer, and to say that it does not hold
the process open. Under the clock this package runs on, every recorded handle is a real timer and
can answer, so no case in the control tells the two forms apart; the owner was tightened anyway,
because the rule it names is about what the handle says.

Six mutations were added and each is caught by name: the default doubled, the promise rewritten in
one of its two forms, the promised number moved into an obsolete annotation, the poll waiting a
thousand times the interval it declares, the route charging the source bytes while a commented-out
line supplies the old text, and the proxy declaration commented out with the binding a string. The
control went from thirty-two cases to thirty-eight, and every case in it is still caught by name.
One of the new cases breaks the page rather than the service, so the control was extended to let a
case name the file it breaks; each file is restored byte for byte after its own break.

The guard went from ninety assertions to ninety-three. The owners of the lifetime, the promise and
the poll now read numbers that were observed rather than declared, and three owners were added:
that the limit the service declares is the limit the cache gives its rows, that the poll waits as
long as the client declares, and that the promise to a reader is the same promise in both of the
forms it is written in.
…s not about

Five owners in this package were reporting rules as broken when the break had not broken them, and
the control could not tell the difference. All five are fixed here, and the product is not changed.

  - the three owners that compare the cache's accounting against the heap proxy read the proxy out
    of the source, and when it could not be read the expected charge became zero. A cache whose
    proxy is the string "2" is still charged twice the source bytes -- that is what the service's
    own arithmetic does with it -- so the rule those three name was not broken by the break that
    made them red: a break of one rule was reported by the owners of three other rules. They now
    hold vacuously when there is no proxy to charge with, and the rule that the proxy has to be a
    number the service declares keeps the whole of that report -- it is the only owner that goes
    red, and it names itself. A vacuous holding is counted and printed with the result, so a
    reader can tell an owner that was checked and found true from one that said nothing at all.
  - the probe that finds the lifetime the cache gives its rows returned nothing when the cache
    never let the rows go, and the owners that read it went red -- including two whose rules were
    not broken by that. A cache that never releases its rows has a lifetime longer than any poll,
    and that is what those two rules say. The probe now returns the ceiling it reached, which is a
    floor on the lifetime rather than a measurement of it, and every owner reads it as one.
  - three owners named a weaker rule than the one they checked. The owner of the rule that the
    cached rows are the same array and not a copy of it compared the whole value a read hands
    back, so a read that returned a fresh wrapper around the very same array was reported as a
    copy of the array. The owner of the rule that the second read of an empty file is a hit
    compared objects, and "is a hit" is not what an object comparison can tell you: a cache that
    served a copy on a hit would still have been a hit. And the owner of the rule that the floor of
    the quarter is a millisecond compared for exactly a millisecond while its name said the sweep
    is not finer than one, so a sweep that was far coarser than a millisecond -- and therefore
    late rather than early -- was reported as a rule broken. The first now compares the arrays,
    which is the rule it names; the second now says what it compares, that the hit hands back the
    same object the cold read gave; the third now says what it holds, that the sweep runs every
    millisecond so the release is at most a millisecond late.

The control now says which rules each break is ALLOWED to report, and not only which rule it has
to report. A control that asks "was the rule named among those that went red" is also satisfied by
a guard that reported three further rules which that break did not break -- so it could not tell
the two apart. Every case now names the exact set of rules its break may report, in one table that
the control reads, and a case fails when the probe reports anything outside it. What the set pins
is which assertions go red; whether each of those rules really was broken by that break is a
judgement, and it is the reader's.

Two cases were added, one for each of the two owners that were fixed by comparison: a hit that
hands back a copy of the rows array, and a hit that hands back a copy of the value. The control
went from thirty-eight cases to forty, and every case in it is still caught by name. The guard
stayed at ninety-three assertions: the three owners that now hold vacuously are still three
assertions, they are green rather than red when there is nothing to compare against, and the
count of those is printed with the result.

One note on how the new helper compares. The helper this commit introduces compares its two sides
with ===, while the comparison the guard already used compares the serialized values. Every side
this commit compares with the helper is a number, so the two ways give the same answer here. The
difference is written down because both ways of comparing are now in the same file, and a reader
who took them for the same one would be wrong about one of them.
… page really do

Three rules in this package were decided by reading the source, and a source says what the code
intends, not what it does. All three are fixed here, and the product is not changed.

  - the owner of the rule that the promise to a reader is the same promise in both of the forms it
    is written in read the minutes out of the sentence with a parser that knew some of the number
    words and not the rest. A promise that said "thirteen minutes (750000 ms)" was therefore a
    promise nobody could check, and the owner accepted that answer as agreement: the reader was
    told one number and the service another, and the guard said the two forms agreed. Every number
    word a promise can be written in is understood now. The phrase is also taken from the word
    the millisecond form is attached to, so a sentence that mentions minutes elsewhere in passing
    cannot be read as the promise.
  - the owner of the rule that the poll waits as long as the client declares read the characters
    around the interval's name. A comma expression hides a second value behind the first, so
    "(TRAJECTORY_POLL_INTERVAL, 8000000)" gave the timer eight million milliseconds while the
    characters around the name still read "TRAJECTORY_POLL_INTERVAL". The page's own script is now
    run in a context that answers the DOM with stubs, the delay is what the page really hands to
    its timer, and the timer that polls the trajectory is found by the handle the page itself
    stores -- not by the order the timers were started in, and not by which delay is larger.
  - the owner of the rule that the heap proxy is a number the service declares read the proxy out
    of the source, so a template literal holding a declaration impersonated it while the binding
    the service really uses was the string "2". The numbers the service declares are now read out
    of the module's own bindings: a copy of the shipped module with one export line appended is
    imported next to a copy of the module it imports relatively, and the values are read out of
    that namespace. A copy that will not import is reported as a failure of the guard, because the
    guard imports the same module statically -- that is a fact about the guard and not about the
    service, and it must not become a quiet holding.

The premise of the second one is worth stating, because it is a premise and not a fact: the
page's inline script has to run in the sandbox. If the page ever uses a browser API the stubs do
not provide at the top level, the script throws, the owner cannot read the delay, and the guard
says so and stops. It does not go quiet, because a quiet owner is a guard that has stopped holding
the rule it names.

Two comments in the guard are brought into line with that in the same commit. The header said the
file imports the pure helpers and nothing else, and the block above the poll owners said the delay
the poll waits was read out of the source. Neither was false -- the file still never starts the
runtime -- but a reader of it would have been told it never runs anything, and it now runs the
page's own inline script in a sandbox. A comment that understates what the code does has the same
shape as an owner that checks less than it claims.

Three mutations were added, one for each of the three. They live in a control of their own: the
existing control owns forty cases of the cache's own behaviour, and the table it reads is the
single source of truth for which rules each of those breaks may report. Those forty cases and
that table are not this change's to rewrite, and adding to them would have meant two owners for
one table. So the three escapes get their own control, and it holds them to the same standard:
each break has to be caught by name, and by the only rules that break really breaks. The suite
runs one more file; the guard stayed at ninety-three assertions.

The service and the page are not changed by this commit.
…r the guard could not read accuses nothing

Two owners were still deciding a rule from a piece of a sentence. Both are fixed here, and the
product is not changed.

  - the owner of the rule that the promise to a reader is the same promise in both of the forms it
    is written in read the minutes out of the END of the words. The words of a number are not only
    its last word: "one hundred twelve and a half minutes (750000 ms)" states 112.5 minutes, and
    the parser dropped "one hundred" and answered 12.5 -- which is what 750000 ms says. So the
    promise could state one number in words and another in milliseconds, and the owner called that
    agreement. The trailing run of number words is now collected by its boundary and parsed as a
    whole, "hundred" included, and a phrase that holds a compound this parser does not state is
    reported as unreadable rather than answered with the part that happened to be understood. The
    comment above the parser already claimed that the number words are parsed completely; it is
    now true, and the fix was in the parser rather than in the comment, because a comment that
    understates what the code does has the same shape as an owner that checks less than it
    claims.
  - the owner of the rule that the shipped default is the interval the service declares compared
    the interval the cache really uses with the number the service declares -- even when that
    number could not be read. A copy of the module that would not import left the declared value
    empty, and "150000 === nothing" was reported as a broken rule: the harness broke, and the
    guard accused a rule nobody had broken. The comparison is made only when the declaration
    could be observed, the failure to read has a rule of its own that is named, and the owners
    that depend on the observation are listed, so the guard can check that it accused none of
    them. That check has teeth of its own: a case that breaks the copy AND leaves the comparison
    without its gate has to make the guard name that rule.

The premise is worth stating, because it is a premise and not a fact: the guard reads the
service's declared numbers out of a copy of the module, and that copy can fail to import. When it
does, the guard fails loudly and by name, the owners that could not be compared count themselves
as vacuous instead of holding quietly, and no owner that depends on the observation accuses
anything. An owner that goes quiet is a guard that has stopped holding the rule it names.

Three mutations were added, one for each of the three shapes: the compound minutes, the copy that
will not import, and the two together. They live in the control of the escapes, which holds them
to the same standard as the three before them -- each break has to be caught by name, and by the
only rules that break really breaks. The control of the cache's own forty cases and the table it
reads are not this change's to rewrite. The suite runs the same number of files; the guard now
reports ninety-five assertions, and the two it gained are the rules about its own observation.

The service and the page are not changed by this commit.
…n read them

Two things, both in the test layer, and the product is not changed.

  - the control of the escapes wrote its cases as dictionaries. The guard that holds every
    mutation control to the name contract reads a control's SOURCE to decide whether it judges a
    case by membership -- whether the case's own expected rule name is tested against the names
    the guard reported -- and it reads a case table as a list of TUPPLES: a case is a tuple in a
    table, and that is what the guard's own documentation says. A table of dictionaries is
    invisible to it, so the control was reported as judging by the guard's exit code alone, and
    the suite went red on it -- that guard and its own control failed together. The cases are
    tuples again, and a case that needs more than one edit carries its edits in parallel lists,
    because the escape the last case is about exists only in that combination.
  - the rule that says the guard accuses no rule it could not observe now says it per observed
    VALUE rather than per observation. One value that could not be read is not a reason to
    silence every owner that depends on the service's numbers: an owner whose own value WAS read
    may still accuse -- a declared interval that is a string is a broken declaration, and that is
    a true accusation. The three groups are the owners of the three values, and the check asks of
    each group only whether its own value was missing.

The first one is worth stating plainly, because it is the shape of a mistake that hides: the
control did decide its cases by membership, the guard could not see that it did, and the report
said the opposite. A control the guard cannot read is a control the guard cannot hold to the
contract, and the fix belongs in the control rather than in the guard -- the guard's rule is about
how a control judges its cases, and a control written in a shape the guard cannot read has not
broken that rule, it has left it unchecked.

The service and the page are not changed by this commit.
…unobserved

Three owners of the parsed-rows rules decided the truth of a rule by reading something
other than the thing they were about.

The minutes of the promise to a reader were read out of the TAIL of the phrase. The walk
back from the millisecond form stopped at a thousands separator and at an ampersand, and
both of those can appear inside a number, so "one thousand, twelve and a half minutes
(750000 ms)" was read as twelve and a half -- the number the promise states in
milliseconds -- and the two forms were reported as the same promise. The walk now covers
the characters a number can be written with, and the parser understands them: the
separator is dropped as a separator, the ampersand reads as "and", and the scales are
stated, so "one thousand, twelve and a half" is 1012.5 and "1,012.5" is 1012.5. A phrase
that was cut inside a number is reported as unreadable rather than answered with its tail.

The promise was taken from the first sentence of the doc block that contained some words,
so a sentence written before it could own the promise while the sentence a reader reads
said something else. It is now found by the promise's own form -- the sentence that carries
the minutes and the number in brackets -- and a block that states that form zero times or
more than once cannot say which sentence was meant, so the promise is then unreadable,
which a rule of its own now says by name. The owners that depend on it hold vacuously
instead of accusing a rule that was not broken.

When the page's inline script did not run, three owners compared a delay they had never
seen, so the premise failed loudly and three rules were accused anyway. The failure now
has a rule of its own that is named, the three owners hold vacuously and count themselves
when the page could not be observed, and the list of the owners that depend on an
observation covers the page's half as well as the service's.

The product is not changed by this commit: the service and the page are byte-for-byte the
same. The guard has three new assertions, and the two that say that a phrase or a promise
can be read are held by the assertion names themselves.
The control of the owner escapes holds a case per escape, each caught BY NAME and by the
only rules that break really breaks. It now holds eleven cases: the three the sixteenth
run found, the three the seventeenth run found, and the five the eighteenth run found --
a compound number written with a thousands separator in the words, with an ampersand and
in the digits, a sentence written before the promise that owns it, and a page whose inline
script does not run.

The last of those five is the shape of a premise: the page's script is run in a sandbox,
and the failure to run has to be loud, named, and followed by silence from the owners that
depend on what the page says. The case holds the guard to exactly that -- it must report
the premise and NOTHING else, and the count of vacuous holdings has to say so.

One entry of the frozen table changed, and it is the entry that says which rule owns the
break "the number promised to a reader is moved into an obsolete annotation". That rule is
now the one that says the promise can be read: the promise is located by its own form, so
a block that no longer states that form states no promise, and the owner that used to
compare a number the guard could not locate would have accused a rule that was not broken
-- the block still promises 750000, in the annotation. The other thirty-nine cases and the
rest of the table are unchanged.
The assertion that the promised minutes are read from the whole phrase, and not from a tail
of it, was a rule that no case could make fail: the cases that break the punctuated compound
break the SHIPPED prose, and the shipped prose is read correctly however the reader is
written. So the rule was not watched by anything.

The control now holds a twelfth case, and it is a different shape from the other eleven: it
breaks the GUARD, and it breaks the boundary that the reader walks back over, so that the
reader answers with the tail of the phrase again. The guard then has to fail by naming that
assertion and nothing else -- the shipped promise is untouched by the break, so no other rule
was broken.

The other eleven cases and the frozen table are unchanged.
Three owner defects were found by an independent review of the parsed-rows range, and all
three are in the owners that read the promise to a reader. None of them is a defect in the
service: the promise the package ships is true, and this commit does not change the service or
the page at all. It changes the three owners, so that each of them decides the truth of a rule
by reading the thing it is about.

F39 -- a scale word the number table does not know was dropped, and the tail was read instead.
The run that collects the number of a phrase stopped at the first word the parser did not know,
which is the same thing as dropping that word: "one myriad twelve and a half minutes (750000
ms)" was read as twelve and a half, and the tail is exactly the number the promise's other form
states, so the two forms were declared the same. The number table now states the English scales
past billion, so a phrase written in one of them is read as the number it is; a word the table
still does not know is refused rather than dropped, and the refusal is narrow: the word just
left of the run is refused when the word before it is a number word, and the word in front of
the run is refused when it is the only word in front of it. A wider net would reach into the
sentence around the phrase and report a readable promise as unreadable.

F40 -- the form that finds the promise knew one spelling of the unit, so a promise written
"600000 milliseconds" was invisible and the annotation beside it became the only candidate: both
owners of the number read the annotation and reported the promise to be sound. The form accepts
every spelling now, and a number in brackets is read in the unit the sentence wrote it in --
milliseconds are milliseconds and seconds are a thousand milliseconds.

F41 -- what has to be unique is the number, not the sentence. Two promise forms inside one
sentence were counted as one promise, so the owner of the bound accused a limit the block
states correctly: the number it accused was one of two, and neither of them was the number. The
numbers in brackets are counted across all the candidate sentences now, and the owners that
depend on them hold vacuously when the block states more than one -- a number nobody can locate
is not a broken rule.

The guard's own reader and locator now have owners of their own: two assertions hold the
locator to finding the promise in every spelling of the unit, and to reading a number in the
unit it was written in, and the reader's assertion about reading a whole phrase now holds the
phrases with a scale word the table knows, with one it does not know in the middle and with one
at the head. The control of the escapes grows from twelve cases to fifteen, one for each of
the three, each caught by name and each caught by the only rule its break really breaks.

The cost of the head rule is written down where the rule is: a promise whose minutes are written
with one context word stuck to the front of them is reported as unreadable even though its two
forms agree. That is the wrong answer about a promise that is not broken, and it is paid on
purpose -- the promise the package ships is written with the number on its own.

Not closed by this commit: a promise written with no bracketed form at all -- "ten minutes",
with the annotation beside it carrying the brackets -- is still not found, because the shipped
doc block has a sentence of its own that mentions minutes in words, and a rule that took every
sentence that mentions minutes would make the shipped bytes ambiguous. That needs an owner of
its own, and it does not have one yet.
Three owner defects were found by an independent review of the parsed-rows range, and all three
are in the owners that read the promise to a reader. None of them is a defect in the service: the
promise the package ships is true, and this commit does not change the service or the page at
all. It changes the owners, so that each of them decides the truth of a rule by reading the thing
it is about, and so that a rule it cannot read is reported as a rule it cannot read.

F42 -- the reader decided where the number ends by guessing, and where the guess failed it
dropped what it did not understand. Two unsupported words in front of the number, or an
unsupported head behind ordinary sentence context, were left in the sentence around the number
and the TAIL was read instead: "one great myriad twelve and a half minutes (750000 ms)" was read
as twelve and a half, and the tail is exactly the number the promise's other form states, so the
two forms were declared the same. The words in front of the number are now the sentence around it
only when every one of them is a word this reader knows cannot be part of a number, and that is
a closed list. A word that is not on it makes the phrase a number nobody can read, and the phrase
is reported BY NAME rather than answered with its tail. The list and the number table are kept
apart, and an assertion says so, because a number word in the list would put a second number into
the phrase and the tail would be read again.

F43 -- the form that finds the promise recognised only the shapes it could read, so a promise
written "600000.0 milliseconds" -- the same number with a decimal point -- was not a candidate
at all, and the annotation beside it became the only candidate and answered for it. The sentence
is found by its shape now: the word "minutes" and a bracketed group. The number in the brackets
is read in the unit the sentence wrote it in, and it may carry a decimal point. A group this
reader cannot read -- an unknown unit, a spelling it has no factor for -- is refused by name and
counted, not skipped: a skipped form is a promise nobody looked at.

F44 -- one ordinary context word in front of the number made the minutes unreadable, and the
owner of the two forms then reported that they DISAGREE. Both forms state twelve and a half
minutes, so that accusation was false, and a false accusation of a broken promise is worse than a
promise nobody can check. An unreadable phrase is not a disagreement: the owner of the two forms
compares the minutes with the number only when the minutes could be read, and when they could
not it holds vacuously, counts itself, and says nothing. Two owners of its own name the failure
-- one for the minutes and one for the number in the brackets -- and they do not overlap with the
owner of the promise being stated exactly once: a block that states the promise twice is that
owner's, and a block that states it once in a shape nobody can read is the premise owner's.

The dash in front of the number ends the phrase whichever dash it is written with. The promise
the package ships is written with an em dash, and a writer may write that same dash as a hyphen;
the scan has to stop there too, or it eats the whole sentence and the number behind the dash
cannot be located. A hyphen inside a word is not a dash, and the scan walks over it.

The guard's own reader and locator have owners of their own: seven assertions hold them to
reading the whole phrase, to refusing a word they do not know, to keeping the two lists of words
apart, to finding the promise whatever shape the number in it is written in, to reading a decimal
point, to refusing a group they cannot read, and to the dash. The control of the escapes grows
from fifteen cases to twenty-five, ten of them new, each caught by name and each caught by the
only rule its break really breaks.

The cost of the closed list is written down where the rule is: a promise whose minutes are
written with a lead-in word this reader has no rule for is reported as unreadable. That is the
wrong answer about a promise that is not broken, and it is paid on purpose -- an unreadable
promise is reported by name and loudly, and a promise nobody can check is never mistaken for a
promise that was checked and agreed. The promise the package ships is written with the number on
its own, so nothing stands in front of it there.

Not closed by this commit: a promise written with no bracketed form at all -- "ten minutes", with
the annotation beside it carrying the brackets -- is still not found, because the shipped doc
block has a sentence of its own that mentions minutes in words, and a rule that took every
sentence that mentions minutes would make the shipped bytes ambiguous. That needs an owner of
its own, and it does not have one yet.
The twenty-first review of the parsed-rows owners returned `not acceptable` with three
findings of its own, and this commit answers those three. The product does not move: the
service and the page are byte-for-byte the same, and the owners are the only thing that
changed.

R29-F45  The reader erased a sign along with the hyphens it walks over, so a promise
          written "-12.5 minutes (750000 ms)" was read as twelve and a half, and the two
          forms were declared the same promise. A hyphen inside a word is one of the ways
          a number is written; a hyphen that is not inside a word is a sign, and a signed
          number is not a number of minutes a promise can state. Such a phrase is now
          unreadable, and the owner of the minutes it could not read says so by name.

R29-F46  The unit table was asked for a name it inherits: "constructor" resolves to
          Object's constructor, the multiplication gave NaN, and that NaN counted as one
          observed number -- so the owners of the number accused a promise that stated no
          number at all, while the premise that says the number could not be read stayed
          green. The table is now asked for a name it carries, and a number that is not
          finite is refused as well: a promise written with more digits than a double can
          hold overflows, and an overflow that reads as a number is the same kind of hole.

R29-F47  The locator counted only the promise forms it could read: a promise written
          "minutes ((600000 ms))" had its number hidden, the annotation beside it became
          the only candidate, and the owners of the number read the annotation and
          reported the promise to be sound. The forms are now found by their anchor, and
          their group is taken by the balance of their brackets, so a form nobody can read
          is counted as unreadable rather than disappearing from the count.

The guard holds four assertions more than it did: a sign is refused rather than read as
its absolute value, a unit this reader does not know supplies no number whatever name it
carries, a number of milliseconds that overflows is not a number of milliseconds, and a
promise form with brackets inside it is counted rather than skipped.

The control of the escapes grows from twenty-seven cases to thirty-one, four of them new.
Each of the four is caught by name and by the only rules its break really breaks. One
case of that control was re-anchored, because the line it broke was rewritten, and its
exact set of rules was widened to the two the break really breaks.

The twenty-second review of this range has not been run yet, so the answers above have
not been through a pass of their own.
The twenty-second review of the parsed-rows owners returned `not acceptable` with three
findings of its own, and this commit answers those three. The product does not move: the
service and the page are byte-for-byte the same, and the owners are the only thing that
changed.

R29-F48  The characters a hyphen may be written with were not one family: the reader knew the
          ASCII hyphen alone, so a promise written with the Unicode MINUS SIGN was not read as
          a signed number at all, and the two forms of that promise were declared the same.
          The family is now used as one -- the phrase class, the dash boundary, the test for a
          sign and the erasure of those characters in a number all use it -- so a sign written
          in another spelling is a sign, such a phrase is unreadable, and the owner of the
          minutes it could not read says so by name.

R29-F49  A phrase that holds a slash was cut at the slash, so the denominator of a fraction
          answered for the phrase it was part of: "25/12.5 minutes (750000 ms)" was read as
          twelve and a half, and the two forms were declared the same promise. A fraction is
          not a number of minutes a promise can state, so the scan walks over the slash and the
          phrase is refused rather than read as its denominator.

R29-F50  The guard read the first spelling of a name in the file, so a decoy could answer for
          the promise: a local copy of the constant inside a function that is never called,
          with its own comment above it, supplied the prose, while the declaration the module
          really uses said something else -- and a comment that stands above other code was
          read as the declaration's documentation too. The documentation the guard reads now
          has to belong to the declaration it reads: the declaration has to be the module's,
          found at the top level of the code, and the comment has to touch it.

The guard holds two assertions more than it did: a number phrase written as a fraction is
refused rather than read as its denominator, and the documentation the guard reads belongs to
the declaration, not to a decoy. The assertion about a sign now covers the spellings of that
sign, and the assertion about a dash covers the same family.

The control of the escapes grows from thirty-one cases to thirty-five, four of them new.
Each of the four is caught by name and by the only rules its break really breaks. Three
cases of that control were re-anchored, because the lines they broke were rewritten.

The twenty-third review of this range has not been run yet, so the answers above have not
been through a pass of their own.
The comments above the cache's limit and above its sweep interval used to state a
NUMBER -- twelve and a half minutes -- and the guard read that number out of the
sentence and compared it with the constant the service declares. Nineteen assertions
did that: a phrase class, a locator that found the sentence, a reader of minutes and
of the unit and of the sign, and the owners that compared what they read with the code.

That contract is gone. The comments now state the RULE and name the constants that
hold it, so there is no number in them to read. The prose reader, the locator and the
nineteen assertions that depended on them are removed with it. Every assertion about
behaviour stays, and the guard holds ninety-five of them: what the cache does with the
rows it holds, when it releases them, on which interval, and what it charges for them
are all still held, each by a name that says what it checks.

WHAT THIS LEAVES UNCOVERED IS STATED HERE RATHER THAN IMPLIED. Changing the shipped
limit constant is no longer something the suite objects to: the rule that held it was
the promise in the comment, and the promise is not there any more. The rule that would
cover it is a product change -- the service declaring the boundary at which it
releases rows, so that a change of the constant is a change of a declaration -- and it
is not in this change.

A failure to set up the guard's own cache is now reported by name. The cache starts
its sweeper when the first row is written, so a service that cannot start one throws
inside the guard's setup; the throw used to escape, and a stack trace says where the
guard stopped rather than which rule it could not hold. The setup is wrapped, the
failure is named, and the guard stops there.

The control of the escapes holds six cases now: the five that were about the page, the
service's own bindings and the guard's own copy of the module, plus one new. The
thirty that went are the escapes of the reader, and the reader they escaped is not in
the package any more. The new one breaks the sweeper's creation, and it has to be
caught by the name of the rule that says the setup failed -- and by nothing else, so
the control fails a guard that accuses another owner.

The frozen control of the cache's own behaviour holds thirty-six. The four that went
are the cases whose break was a change of a promise that is not there any more. The
two of them that named the promise in the set of rules a break may report no longer
name it, because the guard does not have that rule any more.

The twenty-third review of this range returned `not acceptable` with three findings of
its own, and all three were escapes of an owner rather than defects of the service: a
promise cut in the middle, a decoy that could answer for it, and an assembly failure
that was not reported by name. The third is answered above. The first two belonged to
the contract this commit removes, so they are answered by the removal rather than by a
fix: there is no promise to cut and no sentence for a decoy to impersonate.
The comment above the cache's limit said the page rebuilds the trajectory
"every 8 seconds". The next paragraph already says the page polls as often as
the client declares, so the number was a second copy of a constant that lives
in the client -- a copy that goes stale the moment that constant changes. The
comment now says what the page does: it rebuilds the whole trajectory on
every poll.

A comment only; no behaviour changes.
@avatasia

Copy link
Copy Markdown
Contributor Author

The contract this PR dropped, item by item

The package used to assert a number that a comment stated: the guard read the number out of the
sentence and compared it with the constant the service declares. That contract is gone from this
PR — the comments above the cache's limit now state the rule and name the constants that hold it —
and with it went the nineteen assertions that held the number, and the four cases of the frozen
control whose break was a change of a number stated in a comment.

The obligations below are about that contract. They are withdrawn, not left open: an obligation
about a contract that is not in the range cannot be closed, and leaving one open would ask a
reviewer to check a promise the package does not make. Each row says what the obligation asked,
why it was dropped, and what covers the behaviour it was about now.

The identifiers are the ones these obligations were given in the review of this change; nothing else
in this pull request is numbered this way. The assertions named in the last column are the ones that
hold the behaviour now, and each of them is in tests/verify-parsed-rows.mjs at this pull
request's head.

Obligation What it asked Why it was dropped What covers it now
F1 Whether the number the package promises a reader is the limit plus one sweep interval. The comments no longer state a number to compare — they state the rule and name the constants, so there is nothing to read a number out of. the limit the service declares is the limit the cache gives its rows, the sweep interval the cache uses is at most a quarter of the limit, the timer that releases the rows runs on the interval the cache reports
F24 Whether an obsolete annotation can own the prose promise — which sentence of a doc block a number is read out of. Same: no number is stated in a sentence any more, so there is no sentence for an annotation to own. the limit the service declares is the limit the cache gives its rows
F31 Whether minutes written in words the reader did not know are a promise anybody could check. The reader that parsed the words of a number is gone with the contract it served. the limit the service declares is the limit the cache gives its rows
F34 Whether the minutes are read out of the END of the words. Same: there is no reader of the words of a number left to walk back through. the limit the service declares is the limit the cache gives its rows
F36 Whether the phrase is cut at a thousands separator. Same: the characters a number may be written with are not read at all any more. the limit the service declares is the limit the cache gives its rows
F37 Which sentence of a doc block is the promise. Same: the promise is no longer located in a doc block, because no doc block states one. the limit the service declares is the limit the cache gives its rows
R29-F39 Whether an unsupported scale word disappears from the minute phrase. Same: there is no table of number words left, because nothing parses the words of a number. the limit the service declares is the limit the cache gives its rows
R29-F40 Whether an annotation supplies the number when the promise spells the unit out. Same: the spellings of a unit in a sentence are not read any more. the limit the service declares is the limit the cache gives its rows
R29-F41 Whether two forms in one sentence bypass the ambiguity owner — how many numbers a doc block states. Same: a doc block states no number, so there is no ambiguity of forms to own. the limit the service declares is the limit the cache gives its rows
R29-F42 Whether unsupported prefix words disappear — where the number ends in a phrase of words. Same: there is no phrase of words to find the end of. the limit the service declares is the limit the cache gives its rows
R29-F43 Whether a bracketed number with a decimal point hides the promise. Same: the shape of a number inside brackets is not inspected. the limit the service declares is the limit the cache gives its rows
R29-F44 Whether a harmless context word is reported as a disagreement. Same: the words in front of a number are not read, so they can disagree with nothing. the limit the service declares is the limit the cache gives its rows
R29-F45 Whether a negative minute number loses its sign. Same: the sign of a number of minutes is not read. the limit the service declares is the limit the cache gives its rows
R29-F46 Whether an inherited property impersonates a unit — the table of units a reader consults. Same: the table of units went with the reader that consulted it. the limit the service declares is the limit the cache gives its rows
R29-F47 Whether nested parentheses hide a promise form — how a bracketed group is counted. Same: promise forms in brackets are not counted, because none is looked for. the limit the service declares is the limit the cache gives its rows
R29-F48 Whether the Unicode minus disappears before the reader. Same: the family of characters a sign is written with is not read. the limit the service declares is the limit the cache gives its rows
R29-F49 Whether a slash cuts a number. Same: the characters a number may be written with are not read. the limit the service declares is the limit the cache gives its rows
R29-F50 Whether a decoy declaration can answer for the promise — which declaration's documentation is read. Same: no declaration's documentation is read for a number any more. the limit the service declares is the limit the cache gives its rows

What a reader of this pull request should take from the table. The dropped contract was a check
on a sentence; what remains is a check on the running cache. The limit the service declares is
compared with the limit the cache actually gives its rows, the interval the cache reports is the
interval the timer is given, the rows are released without a request, and the charge for the rows
the cache holds is computed from the bytes it read. None of those are read out of a comment, and
that is why none of the obligations above could be closed.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful work on the session/run picker.

I went through the server changes and ran the tests locally. The plugin's verify and client checks pass, mutate-parsed-rows.py catches all 36 mutations, mutate-trajectory-cache.py catches all 39, verify-mutation-contracts.py passes, root npm run check passes (69 tests), and CI is green. Everything stays inside plugins/avatasia/mmc-trajectory/, with no lockfiles, build output, or workflow changes.

Must-fix

None found.

Worth addressing (optional)

  1. Parsed-rows cache can go over its byte budget. miniapp/node/server.mjs:3788 evicts only while entries.size > 1, so a single large current session stays cached even past PARSED_ROWS_CACHE_BYTES (server.mjs:147, 256 MiB). That matches the gap you already listed. Please either state this in the README/comment as "budget applies to non-current sessions" or add a per-session ceiling with a fallback to re-parsing.
  2. Release-row bounds. Also on your list. A declared limit (or a test showing what happens on a very long session) would make the cache behaviour easier to reason about.
  3. Data-dir guard when pluginRoot is missing. assertDataDirOutsidePlugin (server.mjs:4176-4185) passes '' to isUnderDirectory (server.mjs:1026), which resolves to the process cwd. If the runtime ever omits pluginRoot, a valid dataDir under the cwd would be refused. Skipping the check, or failing with a clear message, when pluginRoot is empty would be safer.
  4. New reads of session-store data. The plugin's existing sources on main stay the same. What this PR adds on top: manifest.json now decides a directory's identity and is checked against the expected id (server.mjs:1218-1221, 1296-1322, 1676-1700); the picker's sub-agent layer is derived from that data (server.mjs:1862-1878); and the session lookup now also selects purpose and workspace_dir (server.mjs:1486-1487). Could you list these additions in the PR description so maintainers can review the extra fields and files? Reading the host's session storage directly isn't something we want plugins to rely on long-term. No need to change it in this PR. Once a supported way to get this data is available in a future release, please plan to switch over.
  5. PR size. At 88 commits and +17.5k lines (most of it tests and long comments), this is hard to review in one go. Squashing on merge, or splitting the picker UI from the cache work next time, would help. Some of the very long block comments in server.mjs (e.g. around 1660-1700) could also be shortened.

Verified as described

  • A path is accepted only when both the directory name and the manifest match the expected session id: server.mjs:1676-1700, plus the walk at server.mjs:1296-1322. Paths outside the sessions root are rejected at server.mjs:1679.
  • Refusing to write into the package dir: server.mjs:4176-4191, called first in start(). Derived files are written only under context.dataDir/trajectory-artifacts (miniapp/node/search-segments.mjs:430-449).
  • Cache bounds: session cache by count (SESSION_CACHE_ENTRIES, server.mjs:91, enforced at 4338), parsed rows by bytes plus a TTL sweep (server.mjs:147, 196, 222, 3788), and trajectory body chunked at 1 MiB (server.mjs:111).

Retired comment-number obligations

Thanks for the item-by-item table in your comment. F1, F24, F31, F34, F36, F37, and R29-F39..F50 are fine to close as withdrawn. I checked it again at 9a2840c: no test reads a number out of a comment and compares it with a constant any more. tests/verify-parsed-rows.mjs now imports the module's declared values (PARSED_ROWS_CACHE_TTL_MS, PARSED_ROWS_CACHE_SWEEP_INTERVAL_MS, PARSED_ROWS_HEAP_PER_SOURCE_BYTE) and compares them with what the running cache does. It passes (95 assertions). I also tried a few mutations on a local copy. Doubling the effective TTL in createParsedRowsCache fails 8 assertions. Doubling the interval passed to setInterval fails 1, and so does raising the declared sweep interval above a quarter of the TTL. Writing a wrong number into the doc comment changes nothing, which is what we want now.

The client, the service and the segment reader quoted the value of a constant
that the code beside them declares: the session poll (2.5s), the trajectory
poll (8s), the window size (200), the session-list cap (30), the read caps
(64 KB, 64 MB), the search cap (500), the batch size (25), the inspector's share of
the window (2/5). A reader of those sentences never sees the declaration, and
each sentence goes stale the moment the constant changes -- which is what a
constant is for.

Those sentences now state the rule instead of the number: the constant's own
name where the sentence is about that constant, the window's wording where it
is about the window, and a shape with no number at all where the number was
one session's measurement.

Nine of them are not in the list this change started from. The sweep that
produced that list read the word after a number as a unit, so "the newest 200
and then" and "one of the 30 most recently touched" were invisible to it: the
sweep saw "200 and" and "30 most" and moved on. The same habit makes a sweep
report things that are not copies -- HTTP status codes, pixel sizes, hash
widths and encoding names all match a declared constant's value by accident.
Those are left alone, and each one is listed by name with its reason.

A comment only; no behaviour, no constant, no test, no file added or removed.
…ehind

The branch forked from the base before the base moved on: sixteen commits
landed upstream since, and none of them touch this plugin. This pull request
touches nothing outside this plugin's directory either, so the two histories
meet without overlapping.

The merge is conflict-free and leaves every shipped file exactly as it was:
the plugin's own tree is unchanged by it, and only the base's other plugins
come along.
@avatasia

avatasia commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

This branch has more commits now, so two sentences in the comment above need an update.

What changed: the comments these commits rewrote in the client, the service and the segment reader no longer restate a constant's value: each of them now says what the code does instead. One sentence in the service that stated the heap multiple it charges per source byte now names the constant that holds it, and the client comments that called the dock's default a quarter now say what the code does. The disclosure in the description now says what these commits changed rather than what the package as a whole contains.

Everything here changes comments only — no constant, no behaviour and no test moves — except the merge of this branch with the base, which touches none of those files.

Why this note: "CI is green" above was about the previous head, and "ready to merge" above was about the previous tree. The new head re-ran CI, and the tree the review covered has moved on — but everything since then is comments and a merge, so the behaviour under review is unchanged.

The service wrote the heap multiple it charges per source byte as "2x" -- the
value of the constant that holds it. A comment that states the number instead
of the constant goes stale the moment the constant changes, which is what a
constant is for. This one now names it.

The earlier pass over the comments missed it: its measuring tool skips bare
numbers below 8, and this one is 2. Closing that blind spot is what found it.

A comment only; no behaviour, no constant, no test, no file added or removed.
Two comments still called the dock's default height a quarter of the window.
The dock's share was a quarter before it was changed, and these sentences were
not moved with it, so they now give the dock a number that belongs to the
ledger's reserve instead.

Comments only; no behaviour, no constant, no test, no file added or removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants